create-oke 0.2.1 → 0.2.3
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 +10 -6
- package/examples/linkly/package.json +1 -1
- package/examples/notes/package.json +1 -1
- package/examples/provisions/package.json +1 -1
- package/examples/skyport/package.json +1 -1
- package/package.json +1 -1
- package/src/agents-md.ts +99 -0
- package/src/cli.test.ts +80 -9
- package/src/cli.ts +240 -34
- package/src/docs-origin.ts +19 -0
- package/src/index.ts +1 -0
- package/src/scaffold.ts +9 -0
- package/src/transform.ts +2 -0
- package/templates/full/README.md +49 -0
- package/templates/full/oke.config.ts +4 -0
- package/templates/full/package.json +1 -1
- package/templates/full/src/flows/main/index.ts +18 -0
- package/templates/hello/README.md +30 -5
- package/templates/hello/package.json +1 -1
- package/templates/hello/src/flows/hello/index.ts +17 -0
- package/templates/minimal/README.md +41 -0
- package/templates/minimal/package.json +1 -1
- package/templates/minimal/src/flows/main/index.ts +17 -0
- package/templates/standard/README.md +47 -0
- package/templates/standard/oke.config.ts +4 -0
- package/templates/standard/package.json +1 -1
- package/templates/standard/src/flows/main/index.ts +17 -0
package/README.md
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
Scaffold an [okengine](https://www.npmjs.com/package/okengine) app.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
bunx create-oke@latest my-app
|
|
6
|
+
bunx create-oke@latest my-app
|
|
7
7
|
cd my-app
|
|
8
8
|
oke dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
On a TTY, create-oke opens a Clack wizard: confirm name, pick a template, and
|
|
12
|
+
optionally install + start `oke dev`. Pass `--yes` or `--template` /
|
|
13
|
+
`--from-example` for non-interactive (CI / agents).
|
|
14
|
+
|
|
11
15
|
| Port | What |
|
|
12
16
|
|---|---|
|
|
13
17
|
| `:6530` | your app |
|
|
@@ -21,6 +25,7 @@ Pick another with `--template`, or start from a teaching example with `--from-ex
|
|
|
21
25
|
bunx create-oke@latest my-app --template hello
|
|
22
26
|
bunx create-oke@latest my-app --template minimal
|
|
23
27
|
bunx create-oke@latest my-app --template full
|
|
28
|
+
bunx create-oke@latest my-app --yes
|
|
24
29
|
bunx create-oke@latest my-notes --from-example notes
|
|
25
30
|
```
|
|
26
31
|
|
|
@@ -31,6 +36,9 @@ bunx create-oke@latest my-notes --from-example notes
|
|
|
31
36
|
| `standard` | Full recommended file layout, empty scaffolding (default) |
|
|
32
37
|
| `full` | Every element present and wired, no business logic |
|
|
33
38
|
|
|
39
|
+
Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
|
|
40
|
+
(`--no-agents-md` to skip).
|
|
41
|
+
|
|
34
42
|
`--from-example` copies a teaching example (including its business logic and
|
|
35
43
|
comments) — most new projects want `--template` instead.
|
|
36
44
|
|
|
@@ -41,10 +49,6 @@ comments) — most new projects want `--template` instead.
|
|
|
41
49
|
| `provisions` | durable flows · `vault` · `channel` · plugins |
|
|
42
50
|
| `skyport` | `ai` · multi-tenancy · distributed topology |
|
|
43
51
|
|
|
44
|
-
With no arguments in a real terminal, create-oke asks for a project name and
|
|
45
|
-
template interactively. Non-TTY / CI / any explicit flag stays fully scriptable
|
|
46
|
-
— zero prompts.
|
|
47
|
-
|
|
48
52
|
## Registries
|
|
49
53
|
|
|
50
54
|
| Registry | Package |
|
|
@@ -52,4 +56,4 @@ template interactively. Non-TTY / CI / any explicit flag stays fully scriptable
|
|
|
52
56
|
| npm (CLI) | [`create-oke`](https://www.npmjs.com/package/create-oke) — use this with `bunx` |
|
|
53
57
|
| JSR | [`@omqkhafi/create-oke`](https://jsr.io/@omqkhafi/create-oke) — same sources; prefer npm for the binary |
|
|
54
58
|
|
|
55
|
-
Requires **Bun ≥ 1.3**.
|
|
59
|
+
Requires **Bun ≥ 1.3**. Docs: [okengine.vercel.app](https://okengine.vercel.app) · [handbook](https://okengine.vercel.app/docs) · [unified theory](https://github.com/omqkhafi/okengine/blob/main/docs/spec/unified-theory.md).
|
package/package.json
CHANGED
package/src/agents-md.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slim `AGENTS.md` written into every scaffolded app for coding agents.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { docsUrl } from "./docs-origin.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render the app-facing agent contract for a new project.
|
|
9
|
+
*
|
|
10
|
+
* @param projectName - Sanitized package / folder name
|
|
11
|
+
*/
|
|
12
|
+
export function agentsMdContent(projectName: string): string {
|
|
13
|
+
const handbook = docsUrl("/docs");
|
|
14
|
+
return `# ${projectName} — OKE Agent Contract
|
|
15
|
+
|
|
16
|
+
This file is for coding agents (Cursor, Claude Code, Codex, …). Read it before
|
|
17
|
+
changing backend code. Handbook: ${handbook}
|
|
18
|
+
|
|
19
|
+
**If the docs are silent, stop and ask.**
|
|
20
|
+
|
|
21
|
+
## The one law
|
|
22
|
+
|
|
23
|
+
Every backend behavior is a Flow:
|
|
24
|
+
|
|
25
|
+
\`\`\`text
|
|
26
|
+
on(Trigger) → Effects
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
There are no separate species called endpoints, handlers, consumers, jobs,
|
|
30
|
+
subscribers, or workflows. There is one species — the **Flow** — and triggers
|
|
31
|
+
are typed values.
|
|
32
|
+
|
|
33
|
+
## The eight elements
|
|
34
|
+
|
|
35
|
+
| Element | Essence |
|
|
36
|
+
|---|---|
|
|
37
|
+
| **Flow** | behavior |
|
|
38
|
+
| **Signal** | data in motion |
|
|
39
|
+
| **Store** | data at rest (\`sql\` · \`kv\` · \`files\` · \`index\`) |
|
|
40
|
+
| **Clock** | time |
|
|
41
|
+
| **Gate** | permission to act |
|
|
42
|
+
| **Vault** | protected knowledge |
|
|
43
|
+
| **Channel** | reaching humans |
|
|
44
|
+
| **AI** | reaching machine intelligence |
|
|
45
|
+
|
|
46
|
+
An element earns its place only if it has irreducible physics. New
|
|
47
|
+
infrastructure becomes a new **driver** for an existing element — never a
|
|
48
|
+
ninth element.
|
|
49
|
+
|
|
50
|
+
## The ten exports
|
|
51
|
+
|
|
52
|
+
\`\`\`typescript
|
|
53
|
+
import { on, flow, signal, store, clock, gate, vault, channel, ai, plugin } from "okengine";
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
That is the entire public vocabulary. Everything else is derived.
|
|
57
|
+
|
|
58
|
+
## The fx rule
|
|
59
|
+
|
|
60
|
+
**All world access goes through \`fx\`. No exceptions.**
|
|
61
|
+
|
|
62
|
+
A direct \`node:\` import (or any other side-channel I/O) in element or flow
|
|
63
|
+
code is a **defect**. \`fx\` is the single door to the outside world; effects
|
|
64
|
+
are inferred from what a Flow touches through it.
|
|
65
|
+
|
|
66
|
+
## Ports
|
|
67
|
+
|
|
68
|
+
| Port | Surface |
|
|
69
|
+
|---|---|
|
|
70
|
+
| **6530** | App |
|
|
71
|
+
| **6533** | Console |
|
|
72
|
+
| **6535** | MCP |
|
|
73
|
+
|
|
74
|
+
Mnemonic: O·K·E = 6·5·3.
|
|
75
|
+
|
|
76
|
+
## How to run
|
|
77
|
+
|
|
78
|
+
\`\`\`bash
|
|
79
|
+
bun install
|
|
80
|
+
oke dev # or: bun run dev
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
App \`:6530\` · Console \`:6533\` · MCP \`:6535\`.
|
|
84
|
+
|
|
85
|
+
## Common mistakes
|
|
86
|
+
|
|
87
|
+
- ❌ I/O outside \`fx\` (raw \`fetch\`, \`node:fs\`, vendor SDKs in flow bodies)
|
|
88
|
+
- ✅ \`fx.store\` / \`fx.emit\` / \`fx.ask\` / channel sends through \`fx\`
|
|
89
|
+
- ❌ Inventing a ninth “element” or a parallel handler stack beside Flows
|
|
90
|
+
- ✅ New capability = new **driver** on an existing element, or a new Flow
|
|
91
|
+
- ❌ Untyped HTTP handlers that skip \`on\` / \`flow\` / contracts
|
|
92
|
+
- ✅ \`on(http.get("/…"), flow({ in, out, do }))\`
|
|
93
|
+
|
|
94
|
+
## Learn more
|
|
95
|
+
|
|
96
|
+
- Handbook: ${handbook}
|
|
97
|
+
- Get started: ${docsUrl("/docs/get-started/introduction")}
|
|
98
|
+
`;
|
|
99
|
+
}
|
package/src/cli.test.ts
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
} from "node:fs";
|
|
7
13
|
import { tmpdir } from "node:os";
|
|
8
14
|
import { join } from "node:path";
|
|
9
15
|
import {
|
|
@@ -72,9 +78,17 @@ describe("shouldPrompt", () => {
|
|
|
72
78
|
expect(shouldPrompt(parseArgs([]), true)).toBe(true);
|
|
73
79
|
});
|
|
74
80
|
|
|
75
|
-
test("
|
|
81
|
+
test("TTY + name alone → still interactive (confirm name / template)", () => {
|
|
82
|
+
expect(shouldPrompt(parseArgs(["my-app"]), true)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("non-TTY, --yes, or config flags → no prompts", () => {
|
|
76
86
|
expect(shouldPrompt(parseArgs([]), false)).toBe(false);
|
|
77
|
-
expect(shouldPrompt(parseArgs(["my-app"]),
|
|
87
|
+
expect(shouldPrompt(parseArgs(["my-app"]), false)).toBe(false);
|
|
88
|
+
expect(shouldPrompt(parseArgs(["my-app", "--yes"]), true)).toBe(false);
|
|
89
|
+
expect(shouldPrompt(parseArgs(["my-app", "--template", "hello"]), true)).toBe(
|
|
90
|
+
false,
|
|
91
|
+
);
|
|
78
92
|
expect(shouldPrompt(parseArgs(["--template", "hello"]), true)).toBe(false);
|
|
79
93
|
expect(shouldPrompt(parseArgs(["--from-example", "notes"]), true)).toBe(
|
|
80
94
|
false,
|
|
@@ -83,6 +97,23 @@ describe("shouldPrompt", () => {
|
|
|
83
97
|
});
|
|
84
98
|
});
|
|
85
99
|
|
|
100
|
+
describe("parseArgs flags", () => {
|
|
101
|
+
test("accepts --yes / --install / --no-install / --no-agents-md", () => {
|
|
102
|
+
expect(parseArgs(["x", "--yes"]).yes).toBe(true);
|
|
103
|
+
expect(parseArgs(["x", "-y"]).yes).toBe(true);
|
|
104
|
+
expect(parseArgs(["x", "--install"]).install).toBe(true);
|
|
105
|
+
expect(parseArgs(["x", "--no-install"]).install).toBe(false);
|
|
106
|
+
expect(parseArgs(["x", "--no-agents-md"]).agentsMd).toBe(false);
|
|
107
|
+
expect(parseArgs(["x"]).agentsMd).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("rejects --install with --no-install", () => {
|
|
111
|
+
expect(() => parseArgs(["x", "--install", "--no-install"])).toThrow(
|
|
112
|
+
/install/,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
86
117
|
describe("sourceFromArgs", () => {
|
|
87
118
|
test("prefers example when set", () => {
|
|
88
119
|
expect(sourceFromArgs(parseArgs(["x", "--from-example", "linkly"]))).toEqual(
|
|
@@ -98,7 +129,12 @@ describe("sourceFromArgs", () => {
|
|
|
98
129
|
describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
|
|
99
130
|
test("each --template path matches interactive choice", () => {
|
|
100
131
|
for (const id of TEMPLATES) {
|
|
101
|
-
const answers: InteractiveAnswers = {
|
|
132
|
+
const answers: InteractiveAnswers = {
|
|
133
|
+
name: "x",
|
|
134
|
+
choice: id,
|
|
135
|
+
installAndRun: false,
|
|
136
|
+
agentsMd: true,
|
|
137
|
+
};
|
|
102
138
|
const fromAnswers = scaffoldArgsFromAnswers(answers);
|
|
103
139
|
const fromFlags = scaffoldArgsFromCli(
|
|
104
140
|
parseArgs(["x", "--template", id]),
|
|
@@ -114,6 +150,8 @@ describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
|
|
|
114
150
|
name: "x",
|
|
115
151
|
choice: FROM_EXAMPLE_CHOICE,
|
|
116
152
|
example: id,
|
|
153
|
+
installAndRun: false,
|
|
154
|
+
agentsMd: true,
|
|
117
155
|
};
|
|
118
156
|
const fromAnswers = scaffoldArgsFromAnswers(answers);
|
|
119
157
|
const fromFlags = scaffoldArgsFromCli(
|
|
@@ -144,6 +182,7 @@ describe("transformPackageJson", () => {
|
|
|
144
182
|
"0.0.26",
|
|
145
183
|
);
|
|
146
184
|
expect(next.name).toBe("my-app");
|
|
185
|
+
expect(next.version).toBe("0.0.1");
|
|
147
186
|
expect(next.dependencies?.["okengine"]).toBe("0.0.26");
|
|
148
187
|
expect(next.dependencies?.["okengine"]).not.toBe("file:../..");
|
|
149
188
|
expect(next.dependencies?.["zod"]).toBe("^4.4.3");
|
|
@@ -186,7 +225,20 @@ describe("scaffold structure", () => {
|
|
|
186
225
|
name: `app-${id}`,
|
|
187
226
|
source: { kind: "template", id },
|
|
188
227
|
});
|
|
189
|
-
expect([...result.files].sort()).toEqual(
|
|
228
|
+
expect([...result.files].sort()).toEqual(
|
|
229
|
+
[...expected, "AGENTS.md"].sort(),
|
|
230
|
+
);
|
|
231
|
+
expect(result.files).toContain(".gitignore");
|
|
232
|
+
expect(result.files).toContain("README.md");
|
|
233
|
+
expect(
|
|
234
|
+
readFileSync(join(result.targetDir, "AGENTS.md"), "utf8"),
|
|
235
|
+
).toMatch(/one law|on\(Trigger\)/i);
|
|
236
|
+
const readme = readFileSync(join(result.targetDir, "README.md"), "utf8");
|
|
237
|
+
expect(readme).toMatch(/oke dev/);
|
|
238
|
+
expect(readme).toMatch(new RegExp(`^# ${id}`, "m"));
|
|
239
|
+
expect(
|
|
240
|
+
readFileSync(join(result.targetDir, ".gitignore"), "utf8"),
|
|
241
|
+
).toMatch(/node_modules/);
|
|
190
242
|
const pkg = JSON.parse(
|
|
191
243
|
readFileSync(join(result.targetDir, "package.json"), "utf8"),
|
|
192
244
|
) as { name: string; dependencies: { okengine: string } };
|
|
@@ -271,17 +323,34 @@ describe("scaffold structure", () => {
|
|
|
271
323
|
name: "from-notes",
|
|
272
324
|
source: { kind: "example", id: "notes" },
|
|
273
325
|
});
|
|
274
|
-
expect([...result.files].sort()).toEqual(
|
|
326
|
+
expect([...result.files].sort()).toEqual(
|
|
327
|
+
[...expected, "AGENTS.md"].sort(),
|
|
328
|
+
);
|
|
275
329
|
expect(result.files).toContain("src/flows/notes/index.ts");
|
|
276
330
|
} finally {
|
|
277
331
|
rmSync(dir, { recursive: true, force: true });
|
|
278
332
|
}
|
|
279
333
|
});
|
|
334
|
+
|
|
335
|
+
test("--no-agents-md skips AGENTS.md", () => {
|
|
336
|
+
const dir = mkdtempSync(join(tmpdir(), "create-oke-no-agents-"));
|
|
337
|
+
try {
|
|
338
|
+
const result = scaffold({
|
|
339
|
+
targetDir: join(dir, "hello"),
|
|
340
|
+
name: "no-agents",
|
|
341
|
+
source: { kind: "template", id: "hello" },
|
|
342
|
+
writeAgentsMd: false,
|
|
343
|
+
});
|
|
344
|
+
expect(result.files).not.toContain("AGENTS.md");
|
|
345
|
+
} finally {
|
|
346
|
+
rmSync(dir, { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
280
349
|
});
|
|
281
350
|
|
|
282
351
|
describe("non-TTY CLI", () => {
|
|
283
352
|
test("no args + non-TTY → zero prompts, exit 1", async () => {
|
|
284
|
-
const code = await run([], { stdinIsTTY: false });
|
|
353
|
+
const code = await run([], { stdinIsTTY: false, runPostScaffold: false });
|
|
285
354
|
expect(code).toBe(1);
|
|
286
355
|
});
|
|
287
356
|
|
|
@@ -308,11 +377,12 @@ describe("non-TTY CLI", () => {
|
|
|
308
377
|
const target = join(root, "flag-app");
|
|
309
378
|
try {
|
|
310
379
|
const code = await run(
|
|
311
|
-
[target, "--template", "hello"],
|
|
312
|
-
{ stdinIsTTY: false },
|
|
380
|
+
[target, "--template", "hello", "--no-install"],
|
|
381
|
+
{ stdinIsTTY: false, runPostScaffold: false },
|
|
313
382
|
);
|
|
314
383
|
expect(code).toBe(0);
|
|
315
384
|
expect(readdirSync(target).length).toBeGreaterThan(0);
|
|
385
|
+
expect(existsSync(join(target, "AGENTS.md"))).toBe(true);
|
|
316
386
|
const result = {
|
|
317
387
|
targetDir: target,
|
|
318
388
|
name: "flag-app",
|
|
@@ -323,6 +393,7 @@ describe("non-TTY CLI", () => {
|
|
|
323
393
|
};
|
|
324
394
|
expect(nextStepsText(result)).toContain("oke dev");
|
|
325
395
|
expect(nextStepsText(result)).toContain("bun install");
|
|
396
|
+
expect(nextStepsText(result)).toContain("okengine.vercel.app");
|
|
326
397
|
} finally {
|
|
327
398
|
rmSync(root, { recursive: true, force: true });
|
|
328
399
|
}
|
package/src/cli.ts
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* ```bash
|
|
5
5
|
* bunx create-oke@latest <name> [--template hello|minimal|standard|full]
|
|
6
6
|
* bunx create-oke@latest <name> --from-example notes|linkly|provisions|skyport
|
|
7
|
+
* bunx create-oke@latest <name> --yes
|
|
7
8
|
* bunx create-oke@latest # interactive when stdin is a TTY
|
|
8
9
|
* ```
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
cancel,
|
|
14
|
+
confirm,
|
|
13
15
|
intro,
|
|
14
16
|
isCancel,
|
|
17
|
+
note,
|
|
15
18
|
outro,
|
|
16
19
|
select,
|
|
17
20
|
spinner,
|
|
@@ -19,6 +22,8 @@ import {
|
|
|
19
22
|
} from "@clack/prompts";
|
|
20
23
|
import { basename, relative, resolve } from "node:path";
|
|
21
24
|
import { existsSync, rmSync } from "node:fs";
|
|
25
|
+
import { agentsMdContent } from "./agents-md.ts";
|
|
26
|
+
import { docsUrl } from "./docs-origin.ts";
|
|
22
27
|
import { scaffold, type ScaffoldResult, type ScaffoldSource } from "./scaffold.ts";
|
|
23
28
|
import {
|
|
24
29
|
DEFAULT_TEMPLATE,
|
|
@@ -40,6 +45,15 @@ export type CliArgs = {
|
|
|
40
45
|
/** True when `--template` / `-t` was present on the argv. */
|
|
41
46
|
readonly templateExplicit: boolean;
|
|
42
47
|
readonly help: boolean;
|
|
48
|
+
/** Skip all prompts; use defaults. */
|
|
49
|
+
readonly yes: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Install after scaffold.
|
|
52
|
+
* `undefined` = default (interactive asks; `--yes` installs; else skip).
|
|
53
|
+
*/
|
|
54
|
+
readonly install: boolean | undefined;
|
|
55
|
+
/** Write `AGENTS.md` (default true). */
|
|
56
|
+
readonly agentsMd: boolean;
|
|
43
57
|
readonly targetDir: string | undefined;
|
|
44
58
|
};
|
|
45
59
|
|
|
@@ -54,11 +68,15 @@ export type InteractiveAnswers =
|
|
|
54
68
|
| {
|
|
55
69
|
readonly name: string;
|
|
56
70
|
readonly choice: TemplateId;
|
|
71
|
+
readonly installAndRun: boolean;
|
|
72
|
+
readonly agentsMd: boolean;
|
|
57
73
|
}
|
|
58
74
|
| {
|
|
59
75
|
readonly name: string;
|
|
60
76
|
readonly choice: typeof FROM_EXAMPLE_CHOICE;
|
|
61
77
|
readonly example: ExampleId;
|
|
78
|
+
readonly installAndRun: boolean;
|
|
79
|
+
readonly agentsMd: boolean;
|
|
62
80
|
};
|
|
63
81
|
|
|
64
82
|
/**
|
|
@@ -68,6 +86,7 @@ export type ScaffoldCallArgs = {
|
|
|
68
86
|
readonly name: string;
|
|
69
87
|
readonly targetDir: string;
|
|
70
88
|
readonly source: ScaffoldSource;
|
|
89
|
+
readonly agentsMd: boolean;
|
|
71
90
|
};
|
|
72
91
|
|
|
73
92
|
/**
|
|
@@ -81,6 +100,9 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
81
100
|
let fromExample: ExampleId | undefined;
|
|
82
101
|
let templateExplicit = false;
|
|
83
102
|
let help = false;
|
|
103
|
+
let yes = false;
|
|
104
|
+
let install: boolean | undefined;
|
|
105
|
+
let agentsMd = true;
|
|
84
106
|
let targetDir: string | undefined;
|
|
85
107
|
|
|
86
108
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -89,6 +111,26 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
89
111
|
help = true;
|
|
90
112
|
continue;
|
|
91
113
|
}
|
|
114
|
+
if (a === "--yes" || a === "-y") {
|
|
115
|
+
yes = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (a === "--install") {
|
|
119
|
+
install = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (a === "--no-install") {
|
|
123
|
+
install = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (a === "--no-agents-md") {
|
|
127
|
+
agentsMd = false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (a === "--agents-md") {
|
|
131
|
+
agentsMd = true;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
92
134
|
if (a === "--template" || a === "-t") {
|
|
93
135
|
const next = argv[++i];
|
|
94
136
|
if (!next || !isTemplateId(next)) {
|
|
@@ -147,11 +189,25 @@ export function parseArgs(argv: readonly string[]): CliArgs {
|
|
|
147
189
|
);
|
|
148
190
|
}
|
|
149
191
|
|
|
192
|
+
if (argv.includes("--install") && argv.includes("--no-install")) {
|
|
193
|
+
throw new Error("create-oke: use either --install or --no-install, not both");
|
|
194
|
+
}
|
|
195
|
+
|
|
150
196
|
if (name !== undefined) {
|
|
151
197
|
targetDir = resolve(name);
|
|
152
198
|
}
|
|
153
199
|
|
|
154
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
name,
|
|
202
|
+
template,
|
|
203
|
+
fromExample,
|
|
204
|
+
templateExplicit,
|
|
205
|
+
help,
|
|
206
|
+
yes,
|
|
207
|
+
install,
|
|
208
|
+
agentsMd,
|
|
209
|
+
targetDir,
|
|
210
|
+
};
|
|
155
211
|
}
|
|
156
212
|
|
|
157
213
|
/**
|
|
@@ -179,6 +235,8 @@ Next steps:
|
|
|
179
235
|
cd ${formatCdPath(result.targetDir)}
|
|
180
236
|
bun install
|
|
181
237
|
oke dev # app :6530 · Console :6533 · MCP :6535
|
|
238
|
+
|
|
239
|
+
Docs: ${docsUrl("/docs")}
|
|
182
240
|
`;
|
|
183
241
|
}
|
|
184
242
|
|
|
@@ -199,8 +257,19 @@ export function helpText(): string {
|
|
|
199
257
|
Usage:
|
|
200
258
|
bunx create-oke@latest <name> [--template hello|minimal|standard|full]
|
|
201
259
|
bunx create-oke@latest <name> --from-example notes|linkly|provisions|skyport
|
|
260
|
+
bunx create-oke@latest <name> --yes
|
|
202
261
|
bunx create-oke@latest # interactive (TTY only)
|
|
203
262
|
|
|
263
|
+
Options:
|
|
264
|
+
-t, --template <id> Clean starter (default: ${DEFAULT_TEMPLATE})
|
|
265
|
+
--from-example <id> Teaching example (non-interactive)
|
|
266
|
+
-y, --yes No prompts; defaults + bun install (no oke dev)
|
|
267
|
+
--install Run bun install after scaffold
|
|
268
|
+
--no-install Skip bun install
|
|
269
|
+
--agents-md Write AGENTS.md (default)
|
|
270
|
+
--no-agents-md Skip AGENTS.md
|
|
271
|
+
-h, --help Show this help
|
|
272
|
+
|
|
204
273
|
Templates (clean starters from templates/):
|
|
205
274
|
${templateLines}
|
|
206
275
|
|
|
@@ -208,16 +277,17 @@ ${templateLines}
|
|
|
208
277
|
comments — most new projects want --template instead):
|
|
209
278
|
${exampleLines}
|
|
210
279
|
|
|
211
|
-
No telemetry. Bun only.
|
|
212
|
-
name / --
|
|
280
|
+
No telemetry. Bun only. On a TTY, a project name alone still opens the wizard
|
|
281
|
+
(confirm name, template, install). Non-TTY / --yes / --template /
|
|
282
|
+
--from-example stay fully scriptable.
|
|
213
283
|
`;
|
|
214
284
|
}
|
|
215
285
|
|
|
216
286
|
/**
|
|
217
|
-
* Whether the CLI should open the
|
|
287
|
+
* Whether the CLI should open the interactive Clack flow.
|
|
218
288
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
289
|
+
* TTY humans get the wizard even when a name is pre-filled. Config flags
|
|
290
|
+
* (`--template`, `--from-example`) or `--yes` skip prompts for CI/agents.
|
|
221
291
|
*
|
|
222
292
|
* @param args - Parsed args
|
|
223
293
|
* @param stdinIsTTY - `process.stdin.isTTY`
|
|
@@ -228,7 +298,7 @@ export function shouldPrompt(
|
|
|
228
298
|
): boolean {
|
|
229
299
|
if (!stdinIsTTY) return false;
|
|
230
300
|
if (args.help) return false;
|
|
231
|
-
if (args.
|
|
301
|
+
if (args.yes) return false;
|
|
232
302
|
if (args.templateExplicit) return false;
|
|
233
303
|
if (args.fromExample !== undefined) return false;
|
|
234
304
|
return true;
|
|
@@ -261,6 +331,7 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
|
|
|
261
331
|
name: basename(resolve(args.name)),
|
|
262
332
|
targetDir: args.targetDir,
|
|
263
333
|
source: sourceFromArgs(args),
|
|
334
|
+
agentsMd: args.agentsMd,
|
|
264
335
|
};
|
|
265
336
|
}
|
|
266
337
|
|
|
@@ -282,12 +353,14 @@ export function scaffoldArgsFromAnswers(
|
|
|
282
353
|
name,
|
|
283
354
|
targetDir,
|
|
284
355
|
source: { kind: "example", id: answers.example },
|
|
356
|
+
agentsMd: answers.agentsMd,
|
|
285
357
|
};
|
|
286
358
|
}
|
|
287
359
|
return {
|
|
288
360
|
name,
|
|
289
361
|
targetDir,
|
|
290
362
|
source: { kind: "template", id: answers.choice },
|
|
363
|
+
agentsMd: answers.agentsMd,
|
|
291
364
|
};
|
|
292
365
|
}
|
|
293
366
|
|
|
@@ -296,34 +369,32 @@ export function scaffoldArgsFromAnswers(
|
|
|
296
369
|
*
|
|
297
370
|
* Injectable for tests; production uses {@link askInteractiveAnswers}.
|
|
298
371
|
*
|
|
299
|
-
* @param partial - Name already known (
|
|
372
|
+
* @param partial - Name already known (pre-filled in the prompt)
|
|
300
373
|
*/
|
|
301
374
|
export type AskInteractiveFn = (
|
|
302
|
-
partial: { readonly name?: string },
|
|
375
|
+
partial: { readonly name?: string; readonly agentsMd?: boolean },
|
|
303
376
|
) => Promise<InteractiveAnswers | null>;
|
|
304
377
|
|
|
305
378
|
/**
|
|
306
379
|
* Collect interactive answers via `@clack/prompts`.
|
|
307
380
|
*
|
|
308
|
-
* @param partial - Optional pre-filled project name
|
|
381
|
+
* @param partial - Optional pre-filled project name / agents-md default
|
|
309
382
|
* @returns Answers, or `null` if the user cancelled
|
|
310
383
|
*/
|
|
311
384
|
export async function askInteractiveAnswers(
|
|
312
|
-
partial: { readonly name?: string } = {},
|
|
385
|
+
partial: { readonly name?: string; readonly agentsMd?: boolean } = {},
|
|
313
386
|
): Promise<InteractiveAnswers | null> {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
name = String(nameValue).trim();
|
|
326
|
-
}
|
|
387
|
+
const nameValue = await text({
|
|
388
|
+
message: "Project name",
|
|
389
|
+
placeholder: "my-app",
|
|
390
|
+
initialValue: partial.name ?? "my-app",
|
|
391
|
+
validate: (value) => {
|
|
392
|
+
if (!value?.trim()) return "Project name is required";
|
|
393
|
+
return undefined;
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
if (isCancel(nameValue)) return null;
|
|
397
|
+
const name = String(nameValue).trim();
|
|
327
398
|
|
|
328
399
|
const templateValue = await select({
|
|
329
400
|
message: "Template",
|
|
@@ -343,6 +414,8 @@ export async function askInteractiveAnswers(
|
|
|
343
414
|
});
|
|
344
415
|
if (isCancel(templateValue)) return null;
|
|
345
416
|
|
|
417
|
+
const agentsMd = partial.agentsMd ?? true;
|
|
418
|
+
|
|
346
419
|
if (templateValue === "__example__") {
|
|
347
420
|
const exampleValue = await select({
|
|
348
421
|
message: "Example",
|
|
@@ -353,21 +426,52 @@ export async function askInteractiveAnswers(
|
|
|
353
426
|
})),
|
|
354
427
|
});
|
|
355
428
|
if (isCancel(exampleValue)) return null;
|
|
429
|
+
|
|
430
|
+
const installAndRunValue = await confirm({
|
|
431
|
+
message: "Install dependencies and start oke dev?",
|
|
432
|
+
initialValue: true,
|
|
433
|
+
});
|
|
434
|
+
if (isCancel(installAndRunValue)) return null;
|
|
435
|
+
|
|
356
436
|
return {
|
|
357
437
|
name,
|
|
358
438
|
choice: FROM_EXAMPLE_CHOICE,
|
|
359
439
|
example: exampleValue as ExampleId,
|
|
440
|
+
installAndRun: Boolean(installAndRunValue),
|
|
441
|
+
agentsMd,
|
|
360
442
|
};
|
|
361
443
|
}
|
|
362
444
|
|
|
363
|
-
|
|
445
|
+
const installAndRunValue = await confirm({
|
|
446
|
+
message: "Install dependencies and start oke dev?",
|
|
447
|
+
initialValue: true,
|
|
448
|
+
});
|
|
449
|
+
if (isCancel(installAndRunValue)) return null;
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
name,
|
|
453
|
+
choice: templateValue as TemplateId,
|
|
454
|
+
installAndRun: Boolean(installAndRunValue),
|
|
455
|
+
agentsMd,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Resolve whether to run `bun install` after scaffold (non-interactive).
|
|
461
|
+
*
|
|
462
|
+
* @param args - Parsed args
|
|
463
|
+
*/
|
|
464
|
+
export function shouldInstall(args: CliArgs): boolean {
|
|
465
|
+
if (args.install === true) return true;
|
|
466
|
+
if (args.install === false) return false;
|
|
467
|
+
return args.yes;
|
|
364
468
|
}
|
|
365
469
|
|
|
366
470
|
/**
|
|
367
471
|
* Run the CLI.
|
|
368
472
|
*
|
|
369
473
|
* @param argv - Args after the binary
|
|
370
|
-
* @param options - Test seams (stdin TTY, ask injection)
|
|
474
|
+
* @param options - Test seams (stdin TTY, ask injection, skip real install/dev)
|
|
371
475
|
* @returns Exit code
|
|
372
476
|
*/
|
|
373
477
|
export async function run(
|
|
@@ -375,6 +479,8 @@ export async function run(
|
|
|
375
479
|
options: {
|
|
376
480
|
readonly stdinIsTTY?: boolean | undefined;
|
|
377
481
|
readonly ask?: AskInteractiveFn;
|
|
482
|
+
/** When false, skip spawning bun install / oke dev (tests). Default true. */
|
|
483
|
+
readonly runPostScaffold?: boolean;
|
|
378
484
|
} = {},
|
|
379
485
|
): Promise<number> {
|
|
380
486
|
let args: CliArgs;
|
|
@@ -392,34 +498,49 @@ export async function run(
|
|
|
392
498
|
|
|
393
499
|
const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
|
|
394
500
|
if (shouldPrompt(args, stdinIsTTY)) {
|
|
395
|
-
return runInteractive(args, options.ask ?? askInteractiveAnswers);
|
|
501
|
+
return runInteractive(args, options.ask ?? askInteractiveAnswers, options);
|
|
396
502
|
}
|
|
397
503
|
|
|
398
504
|
if (args.name === undefined) {
|
|
399
505
|
console.log(helpText());
|
|
400
|
-
console.error(
|
|
506
|
+
console.error(
|
|
507
|
+
"create-oke: missing <name>. Pass a name, or run in a TTY for the wizard.",
|
|
508
|
+
);
|
|
509
|
+
console.error(" Example: bunx create-oke@latest my-app --yes");
|
|
401
510
|
return 1;
|
|
402
511
|
}
|
|
403
512
|
|
|
513
|
+
if (args.yes) {
|
|
514
|
+
const source = sourceFromArgs(args);
|
|
515
|
+
console.log(
|
|
516
|
+
`Using defaults: ${source.kind}=${source.id} agents-md=${args.agentsMd} install=${shouldInstall(args)}`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
404
520
|
return runScaffold({
|
|
405
521
|
...scaffoldArgsFromCli(args),
|
|
406
522
|
interactive: false,
|
|
523
|
+
install: shouldInstall(args),
|
|
524
|
+
startDev: false,
|
|
525
|
+
runPostScaffold: options.runPostScaffold ?? true,
|
|
407
526
|
});
|
|
408
527
|
}
|
|
409
528
|
|
|
410
529
|
/**
|
|
411
530
|
* Interactive TTY flow — ask → map answers → scaffold.
|
|
412
531
|
*
|
|
413
|
-
* @param args - Parsed args (name may already be set
|
|
532
|
+
* @param args - Parsed args (name may already be set)
|
|
414
533
|
* @param ask - Injectable ask step
|
|
534
|
+
* @param options - Post-scaffold seams
|
|
415
535
|
*/
|
|
416
536
|
async function runInteractive(
|
|
417
537
|
args: CliArgs,
|
|
418
538
|
ask: AskInteractiveFn,
|
|
539
|
+
options: { readonly runPostScaffold?: boolean },
|
|
419
540
|
): Promise<number> {
|
|
420
541
|
intro("create-oke");
|
|
421
542
|
|
|
422
|
-
const answers = await ask({ name: args.name });
|
|
543
|
+
const answers = await ask({ name: args.name, agentsMd: args.agentsMd });
|
|
423
544
|
if (answers === null) {
|
|
424
545
|
cancel("Cancelled.");
|
|
425
546
|
return 1;
|
|
@@ -428,18 +549,35 @@ async function runInteractive(
|
|
|
428
549
|
return runScaffold({
|
|
429
550
|
...scaffoldArgsFromAnswers(answers),
|
|
430
551
|
interactive: true,
|
|
552
|
+
install: answers.installAndRun,
|
|
553
|
+
startDev: answers.installAndRun,
|
|
554
|
+
runPostScaffold: options.runPostScaffold ?? true,
|
|
431
555
|
});
|
|
432
556
|
}
|
|
433
557
|
|
|
434
558
|
/**
|
|
435
559
|
* Scaffold with optional spinner / outro, cleaning up on failure or cancel.
|
|
436
560
|
*
|
|
437
|
-
* @param options - Scaffold inputs + interactive
|
|
561
|
+
* @param options - Scaffold inputs + interactive / install flags
|
|
438
562
|
*/
|
|
439
563
|
async function runScaffold(
|
|
440
|
-
options: ScaffoldCallArgs & {
|
|
564
|
+
options: ScaffoldCallArgs & {
|
|
565
|
+
readonly interactive: boolean;
|
|
566
|
+
readonly install: boolean;
|
|
567
|
+
readonly startDev: boolean;
|
|
568
|
+
readonly runPostScaffold: boolean;
|
|
569
|
+
},
|
|
441
570
|
): Promise<number> {
|
|
442
|
-
const {
|
|
571
|
+
const {
|
|
572
|
+
name,
|
|
573
|
+
targetDir,
|
|
574
|
+
source,
|
|
575
|
+
agentsMd,
|
|
576
|
+
interactive,
|
|
577
|
+
install,
|
|
578
|
+
startDev,
|
|
579
|
+
runPostScaffold,
|
|
580
|
+
} = options;
|
|
443
581
|
const existed = existsSync(targetDir);
|
|
444
582
|
let spun: ReturnType<typeof spinner> | undefined;
|
|
445
583
|
|
|
@@ -462,14 +600,53 @@ async function runScaffold(
|
|
|
462
600
|
spun = spinner();
|
|
463
601
|
spun.start("Scaffolding…");
|
|
464
602
|
}
|
|
465
|
-
const result = scaffold({
|
|
603
|
+
const result = scaffold({
|
|
604
|
+
targetDir,
|
|
605
|
+
name,
|
|
606
|
+
source,
|
|
607
|
+
writeAgentsMd: agentsMd,
|
|
608
|
+
});
|
|
466
609
|
if (spun) spun.stop("Scaffolded.");
|
|
610
|
+
|
|
611
|
+
if (runPostScaffold && install) {
|
|
612
|
+
const installSpun = interactive ? spinner() : undefined;
|
|
613
|
+
installSpun?.start("Installing dependencies…");
|
|
614
|
+
const installOk = await runCommand(["bun", "install"], targetDir);
|
|
615
|
+
if (!installOk) {
|
|
616
|
+
installSpun?.stop("Install failed.");
|
|
617
|
+
cleanup();
|
|
618
|
+
console.error("create-oke: bun install failed");
|
|
619
|
+
return 1;
|
|
620
|
+
}
|
|
621
|
+
installSpun?.stop("Installed.");
|
|
622
|
+
}
|
|
623
|
+
|
|
467
624
|
const message = nextStepsText(result);
|
|
468
625
|
if (interactive) {
|
|
626
|
+
note(
|
|
627
|
+
[
|
|
628
|
+
`App http://127.0.0.1:6530`,
|
|
629
|
+
`Console http://127.0.0.1:6533`,
|
|
630
|
+
`MCP http://127.0.0.1:6535`,
|
|
631
|
+
`Docs ${docsUrl("/docs")}`,
|
|
632
|
+
agentsMd ? `Agents AGENTS.md` : undefined,
|
|
633
|
+
]
|
|
634
|
+
.filter(Boolean)
|
|
635
|
+
.join("\n"),
|
|
636
|
+
"Ports",
|
|
637
|
+
);
|
|
469
638
|
outro(message.trim());
|
|
470
639
|
} else {
|
|
471
640
|
console.log(message);
|
|
472
641
|
}
|
|
642
|
+
|
|
643
|
+
if (runPostScaffold && startDev) {
|
|
644
|
+
const ok = await runCommand(["bun", "run", "dev"], targetDir, {
|
|
645
|
+
inherit: true,
|
|
646
|
+
});
|
|
647
|
+
return ok ? 0 : 1;
|
|
648
|
+
}
|
|
649
|
+
|
|
473
650
|
return 0;
|
|
474
651
|
} catch (e) {
|
|
475
652
|
spun?.stop("Failed.");
|
|
@@ -480,3 +657,32 @@ async function runScaffold(
|
|
|
480
657
|
process.off("SIGINT", onSigInt);
|
|
481
658
|
}
|
|
482
659
|
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Spawn a command in `cwd`.
|
|
663
|
+
*
|
|
664
|
+
* @param cmd - Argv
|
|
665
|
+
* @param cwd - Working directory
|
|
666
|
+
* @param options - Inherit stdio for long-running processes
|
|
667
|
+
*/
|
|
668
|
+
async function runCommand(
|
|
669
|
+
cmd: readonly string[],
|
|
670
|
+
cwd: string,
|
|
671
|
+
options: { readonly inherit?: boolean } = {},
|
|
672
|
+
): Promise<boolean> {
|
|
673
|
+
const proc = Bun.spawn([...cmd], {
|
|
674
|
+
cwd,
|
|
675
|
+
stdout: options.inherit ? "inherit" : "pipe",
|
|
676
|
+
stderr: options.inherit ? "inherit" : "pipe",
|
|
677
|
+
stdin: options.inherit ? "inherit" : undefined,
|
|
678
|
+
});
|
|
679
|
+
const code = await proc.exited;
|
|
680
|
+
if (code !== 0 && !options.inherit) {
|
|
681
|
+
const err = await new Response(proc.stderr).text();
|
|
682
|
+
if (err.trim()) console.error(err);
|
|
683
|
+
}
|
|
684
|
+
return code === 0;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Re-export for tests that assert agents content shape. */
|
|
688
|
+
export { agentsMdContent };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical public docs origin for scaffolded AGENTS.md and next-steps.
|
|
3
|
+
*
|
|
4
|
+
* Keep in lockstep with `src/docs-origin.ts` in the okengine package.
|
|
5
|
+
* Flip both when the custom domain is attached: `https://oke.omqkhafi.dev`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Live docs site (Vercel). Planned cutover: `https://oke.omqkhafi.dev`. */
|
|
9
|
+
export const DOCS_ORIGIN = "https://okengine.vercel.app" as const;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Absolute docs URL for a path under the handbook / site.
|
|
13
|
+
*
|
|
14
|
+
* @param path - Path beginning with `/` (e.g. `/docs`)
|
|
15
|
+
*/
|
|
16
|
+
export function docsUrl(path: string): string {
|
|
17
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
18
|
+
return `${DOCS_ORIGIN}${normalized}`;
|
|
19
|
+
}
|
package/src/index.ts
CHANGED
package/src/scaffold.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type ExampleId,
|
|
21
21
|
type TemplateId,
|
|
22
22
|
} from "./templates.ts";
|
|
23
|
+
import { agentsMdContent } from "./agents-md.ts";
|
|
23
24
|
import {
|
|
24
25
|
sanitizeProjectName,
|
|
25
26
|
shouldSkipTemplatePath,
|
|
@@ -41,6 +42,8 @@ export type ScaffoldOptions = {
|
|
|
41
42
|
readonly name: string;
|
|
42
43
|
/** Template or teaching-example source. */
|
|
43
44
|
readonly source: ScaffoldSource;
|
|
45
|
+
/** Write root `AGENTS.md` (default true). */
|
|
46
|
+
readonly writeAgentsMd?: boolean;
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
/** Result of a successful scaffold. */
|
|
@@ -98,6 +101,12 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
|
98
101
|
const nextPkg = transformPackageJson(sourcePkg, name, okengineDependency);
|
|
99
102
|
writeFileSync(pkgPath, `${JSON.stringify(nextPkg, null, 2)}\n`, "utf8");
|
|
100
103
|
|
|
104
|
+
if (options.writeAgentsMd !== false) {
|
|
105
|
+
const agentsPath = join(targetDir, "AGENTS.md");
|
|
106
|
+
writeFileSync(agentsPath, agentsMdContent(name), "utf8");
|
|
107
|
+
if (!written.includes("AGENTS.md")) written.push("AGENTS.md");
|
|
108
|
+
}
|
|
109
|
+
|
|
101
110
|
written.sort();
|
|
102
111
|
return {
|
|
103
112
|
targetDir,
|
package/src/transform.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# full
|
|
2
|
+
|
|
3
|
+
**Every element present and wired** — no business logic. Use when you want the
|
|
4
|
+
complete surface ready to fill in.
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bun install
|
|
10
|
+
oke dev # app :6530 · Console :6533 · MCP :6535
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| Port | Try |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `:6530` | `GET /` · `GET /health` |
|
|
16
|
+
| `:6533` | Console |
|
|
17
|
+
| `:6535` | MCP |
|
|
18
|
+
|
|
19
|
+
## What’s in this template
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
full/
|
|
23
|
+
├── oke.config.ts
|
|
24
|
+
├── src/
|
|
25
|
+
│ ├── app.ts # gates, secrets, signals, stores, channel, ai
|
|
26
|
+
│ ├── core.ts / schema.ts # Store
|
|
27
|
+
│ ├── gates.ts # open gate (used on HTTP)
|
|
28
|
+
│ ├── vault.ts # appSecret
|
|
29
|
+
│ ├── channels.ts # welcome template
|
|
30
|
+
│ ├── ai.ts # stub model + echo prompt
|
|
31
|
+
│ ├── locales/en.ts · ar.ts
|
|
32
|
+
│ └── flows/main/
|
|
33
|
+
│ ├── index.ts # GET / · GET /health · every("1d")
|
|
34
|
+
│ ├── shapes.ts
|
|
35
|
+
│ └── signals.ts # pinged
|
|
36
|
+
└── tests/full.test.ts
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Elements wired:** Flow · Store · Signal · Clock (`every`) · Gate · Vault · Channel · AI
|
|
40
|
+
- **Unit:** `main`
|
|
41
|
+
- **App `$options`:** `gates`, `secrets`, `signals`, `stores`, `channel`, `ai`
|
|
42
|
+
- **Flows:** `root`, `health` (emits `pinged`), plus a trivial daily cron
|
|
43
|
+
|
|
44
|
+
Replace stubs with real models, prompts, channels, and domain flows. Prefer
|
|
45
|
+
`standard` if you do not need AI / clock / vault on day one.
|
|
46
|
+
|
|
47
|
+
## Agent contract
|
|
48
|
+
|
|
49
|
+
See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { on, flow, http, every } from "okengine";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { open } from "../../gates";
|
|
3
4
|
import { Health } from "./shapes";
|
|
4
5
|
import { pinged } from "./signals";
|
|
5
6
|
|
|
7
|
+
/** First-run welcome — visit :6530/ after `oke dev`. */
|
|
8
|
+
export const root = on(
|
|
9
|
+
http.get("/").gate(open),
|
|
10
|
+
flow({
|
|
11
|
+
out: z.object({
|
|
12
|
+
ok: z.literal(true),
|
|
13
|
+
console: z.string(),
|
|
14
|
+
try: z.string(),
|
|
15
|
+
}),
|
|
16
|
+
do: () => ({
|
|
17
|
+
ok: true as const,
|
|
18
|
+
console: "http://127.0.0.1:6533",
|
|
19
|
+
try: "/health",
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
6
24
|
export const health = on(
|
|
7
25
|
http.get("/health").gate(open),
|
|
8
26
|
flow({
|
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
# hello
|
|
2
2
|
|
|
3
|
-
Fastest possible
|
|
4
|
-
|
|
3
|
+
Fastest possible “it works” — one unit, two HTTP flows, **no Store**.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
bun install
|
|
8
9
|
oke dev # app :6530 · Console :6533 · MCP :6535
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
| Port | Try |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `:6530` | `GET /` · `GET /hello` |
|
|
15
|
+
| `:6533` | Console |
|
|
16
|
+
| `:6535` | MCP |
|
|
17
|
+
|
|
18
|
+
## What’s in this template
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
hello/
|
|
22
|
+
├── oke.config.ts
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── app.ts # oke({ name: "hello" }).adopt({ hello })
|
|
25
|
+
│ └── flows/hello/index.ts # GET / · GET /hello
|
|
26
|
+
└── tests/hello.test.ts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **Elements:** Flow only (no `store`, `gate`, `vault`, `channel`, or `ai`)
|
|
30
|
+
- **Unit:** `hello`
|
|
31
|
+
- **Flows:** `root` (`GET /`), `hello` (`GET /hello` → `{ message: "ok" }`)
|
|
32
|
+
|
|
33
|
+
Replace `/hello` with your first real route. When you need persistence, move up to
|
|
34
|
+
`minimal` or `standard` — or keep growing this tree.
|
|
35
|
+
|
|
36
|
+
## Agent contract
|
|
37
|
+
|
|
38
|
+
See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { on, flow, http } from "okengine";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
+
/** First-run welcome — visit :6530/ after `oke dev`. */
|
|
5
|
+
export const root = on(
|
|
6
|
+
http.get("/"),
|
|
7
|
+
flow({
|
|
8
|
+
out: z.object({
|
|
9
|
+
ok: z.literal(true),
|
|
10
|
+
console: z.string(),
|
|
11
|
+
try: z.string(),
|
|
12
|
+
}),
|
|
13
|
+
do: () => ({
|
|
14
|
+
ok: true as const,
|
|
15
|
+
console: "http://127.0.0.1:6533",
|
|
16
|
+
try: "/hello",
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
|
|
4
21
|
export const hello = on(
|
|
5
22
|
http.get("/hello"),
|
|
6
23
|
flow({
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# minimal
|
|
2
|
+
|
|
3
|
+
Smallest shape you’d actually ship — **Store + HTTP flows** over a single table.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
oke dev # app :6530 · Console :6533 · MCP :6535
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Port | Try |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `:6530` | `GET /` · `GET /entries` · `POST /entries` |
|
|
15
|
+
| `:6533` | Console |
|
|
16
|
+
| `:6535` | MCP |
|
|
17
|
+
|
|
18
|
+
## What’s in this template
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
minimal/
|
|
22
|
+
├── oke.config.ts
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── app.ts # adopt({ main }), stores: [db]
|
|
25
|
+
│ ├── core.ts # store.sql("…")
|
|
26
|
+
│ ├── schema.ts # entries table (Drizzle)
|
|
27
|
+
│ └── flows/main/index.ts # GET / · GET|POST /entries
|
|
28
|
+
└── tests/minimal.test.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **Elements:** Flow · Store (`sql`)
|
|
32
|
+
- **Unit:** `main`
|
|
33
|
+
- **Flows:** `root`, `list` (`GET /entries`), `create` (`POST /entries` with `{ body }`)
|
|
34
|
+
- **Schema:** `entries` — `id`, `body`, `createdAt`
|
|
35
|
+
|
|
36
|
+
Swap `entries` for your domain table and grow flows beside it. Need gates,
|
|
37
|
+
channels, or locales? Use `standard`.
|
|
38
|
+
|
|
39
|
+
## Agent contract
|
|
40
|
+
|
|
41
|
+
See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
|
|
@@ -11,6 +11,23 @@ const Entry = z.object({
|
|
|
11
11
|
createdAt: z.number(),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
/** First-run welcome — visit :6530/ after `oke dev`. */
|
|
15
|
+
export const root = on(
|
|
16
|
+
http.get("/"),
|
|
17
|
+
flow({
|
|
18
|
+
out: z.object({
|
|
19
|
+
ok: z.literal(true),
|
|
20
|
+
console: z.string(),
|
|
21
|
+
try: z.string(),
|
|
22
|
+
}),
|
|
23
|
+
do: () => ({
|
|
24
|
+
ok: true as const,
|
|
25
|
+
console: "http://127.0.0.1:6533",
|
|
26
|
+
try: "GET /entries",
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
14
31
|
export const create = on(
|
|
15
32
|
http.post("/entries"),
|
|
16
33
|
flow({
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# standard
|
|
2
|
+
|
|
3
|
+
Full recommended file layout with **empty scaffolding** — structure without
|
|
4
|
+
business logic. Default `create-oke` template.
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bun install
|
|
10
|
+
oke dev # app :6530 · Console :6533 · MCP :6535
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| Port | Try |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `:6530` | `GET /` · `GET /health` |
|
|
16
|
+
| `:6533` | Console |
|
|
17
|
+
| `:6535` | MCP |
|
|
18
|
+
|
|
19
|
+
## What’s in this template
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
standard/
|
|
23
|
+
├── oke.config.ts
|
|
24
|
+
├── src/
|
|
25
|
+
│ ├── app.ts # adopt({ main }); wires stores + side modules
|
|
26
|
+
│ ├── core.ts / schema.ts # Store (replace the placeholder table)
|
|
27
|
+
│ ├── gates.ts # Gate stubs
|
|
28
|
+
│ ├── vault.ts # Vault stubs
|
|
29
|
+
│ ├── channels.ts # Channel stubs
|
|
30
|
+
│ ├── locales/en.ts · ar.ts # i18n stubs
|
|
31
|
+
│ └── flows/main/
|
|
32
|
+
│ ├── index.ts # GET / · GET /health
|
|
33
|
+
│ ├── shapes.ts # contracts live next to flows
|
|
34
|
+
│ └── signals.ts # Signal stubs
|
|
35
|
+
└── tests/standard.test.ts
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **Elements stubbed:** Flow · Store · Signal · Gate · Vault · Channel (+ locales)
|
|
39
|
+
- **Unit:** `main`
|
|
40
|
+
- **Flows to replace:** `root`, `health` — delete or rename once your domain exists
|
|
41
|
+
|
|
42
|
+
Fill `schema.ts`, then add real flows under `src/flows/<unit>/`. Prefer this
|
|
43
|
+
layout for new products; use `hello` / `minimal` only when you want less surface.
|
|
44
|
+
|
|
45
|
+
## Agent contract
|
|
46
|
+
|
|
47
|
+
See [`AGENTS.md`](./AGENTS.md). Handbook: [okengine.vercel.app/docs](https://okengine.vercel.app/docs)
|
|
@@ -15,5 +15,9 @@ export default defineConfig({
|
|
|
15
15
|
whatsapp: { dev: "console", test: "console", prod: "wa-cloud" },
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
|
+
images: {
|
|
19
|
+
"store.sql": "postgres:18-alpine",
|
|
20
|
+
"store.kv": "redis:8-alpine",
|
|
21
|
+
},
|
|
18
22
|
i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
|
|
19
23
|
});
|
|
@@ -4,6 +4,23 @@ import { z } from "zod";
|
|
|
4
4
|
import "./shapes";
|
|
5
5
|
import "./signals";
|
|
6
6
|
|
|
7
|
+
/** First-run welcome — visit :6530/ after `oke dev`. */
|
|
8
|
+
export const root = on(
|
|
9
|
+
http.get("/"),
|
|
10
|
+
flow({
|
|
11
|
+
out: z.object({
|
|
12
|
+
ok: z.literal(true),
|
|
13
|
+
console: z.string(),
|
|
14
|
+
try: z.string(),
|
|
15
|
+
}),
|
|
16
|
+
do: () => ({
|
|
17
|
+
ok: true as const,
|
|
18
|
+
console: "http://127.0.0.1:6533",
|
|
19
|
+
try: "/health",
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
7
24
|
/** Replace with your unit's flows. */
|
|
8
25
|
export const health = on(
|
|
9
26
|
http.get("/health"),
|