create-oke 0.1.6 → 0.2.2
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 +26 -6
- package/{templates → examples}/linkly/package.json +1 -1
- package/{templates → examples}/notes/package.json +1 -1
- package/{templates → examples}/provisions/package.json +1 -1
- package/{templates → examples}/skyport/package.json +1 -1
- package/package.json +6 -2
- package/src/agents-md.ts +99 -0
- package/src/cli.test.ts +318 -19
- package/src/cli.ts +561 -29
- package/src/docs-origin.ts +19 -0
- package/src/index.ts +3 -1
- package/src/scaffold.ts +59 -32
- package/src/sync-templates.ts +28 -7
- package/src/templates.ts +85 -12
- package/src/transform.ts +5 -2
- package/templates/full/README.md +49 -0
- package/templates/full/oke.config.ts +23 -0
- package/templates/full/package.json +15 -0
- package/templates/full/src/ai.ts +11 -0
- package/templates/full/src/app.ts +34 -0
- package/templates/full/src/channels.ts +9 -0
- package/templates/full/src/core.ts +4 -0
- package/templates/full/src/flows/main/index.ts +45 -0
- package/templates/full/src/flows/main/shapes.ts +3 -0
- package/templates/full/src/flows/main/signals.ts +8 -0
- package/templates/full/src/gates.ts +4 -0
- package/templates/full/src/locales/ar.ts +3 -0
- package/templates/full/src/locales/en.ts +3 -0
- package/templates/full/src/schema.ts +8 -0
- package/templates/full/src/vault.ts +9 -0
- package/templates/full/tests/full.test.ts +11 -0
- package/templates/hello/README.md +38 -0
- package/templates/hello/oke.config.ts +3 -0
- package/templates/hello/package.json +14 -0
- package/templates/hello/src/app.ts +10 -0
- package/templates/hello/src/flows/hello/index.ts +27 -0
- package/templates/hello/tests/hello.test.ts +10 -0
- package/templates/minimal/README.md +41 -0
- package/templates/minimal/oke.config.ts +7 -0
- package/templates/minimal/package.json +15 -0
- package/templates/minimal/src/app.ts +13 -0
- package/templates/minimal/src/core.ts +4 -0
- package/templates/minimal/src/flows/main/index.ts +49 -0
- package/templates/minimal/src/schema.ts +8 -0
- package/templates/minimal/tests/minimal.test.ts +13 -0
- package/templates/standard/README.md +47 -0
- package/templates/standard/oke.config.ts +19 -0
- package/templates/standard/package.json +15 -0
- package/templates/standard/src/app.ts +18 -0
- package/templates/standard/src/channels.ts +4 -0
- package/templates/standard/src/core.ts +4 -0
- package/templates/standard/src/flows/main/index.ts +31 -0
- package/templates/standard/src/flows/main/shapes.ts +2 -0
- package/templates/standard/src/flows/main/signals.ts +4 -0
- package/templates/standard/src/gates.ts +4 -0
- package/templates/standard/src/locales/ar.ts +2 -0
- package/templates/standard/src/locales/en.ts +2 -0
- package/templates/standard/src/schema.ts +9 -0
- package/templates/standard/src/vault.ts +4 -0
- package/templates/standard/tests/standard.test.ts +10 -0
- /package/{templates → examples}/linkly/oke.config.ts +0 -0
- /package/{templates → examples}/linkly/src/app.ts +0 -0
- /package/{templates → examples}/linkly/src/core.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/analytics/index.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/links/index.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/links/shapes.ts +0 -0
- /package/{templates → examples}/linkly/src/flows/links/signals.ts +0 -0
- /package/{templates → examples}/linkly/src/gates.ts +0 -0
- /package/{templates → examples}/linkly/src/schema.ts +0 -0
- /package/{templates → examples}/linkly/tests/linkly.test.ts +0 -0
- /package/{templates → examples}/notes/oke.config.ts +0 -0
- /package/{templates → examples}/notes/src/app.ts +0 -0
- /package/{templates → examples}/notes/src/core.ts +0 -0
- /package/{templates → examples}/notes/src/flows/notes/index.ts +0 -0
- /package/{templates → examples}/notes/src/schema.ts +0 -0
- /package/{templates → examples}/notes/tests/notes.test.ts +0 -0
- /package/{templates → examples}/provisions/oke.config.ts +0 -0
- /package/{templates → examples}/provisions/src/app.ts +0 -0
- /package/{templates → examples}/provisions/src/channels.ts +0 -0
- /package/{templates → examples}/provisions/src/core.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/notifications/index.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/orders/index.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/orders/shapes.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/orders/signals.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/payments/index.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/payments/shapes.ts +0 -0
- /package/{templates → examples}/provisions/src/flows/payments/stripe.ts +0 -0
- /package/{templates → examples}/provisions/src/gates.ts +0 -0
- /package/{templates → examples}/provisions/src/locales/ar.ts +0 -0
- /package/{templates → examples}/provisions/src/locales/en.ts +0 -0
- /package/{templates → examples}/provisions/src/plugins/audit-schema.ts +0 -0
- /package/{templates → examples}/provisions/src/plugins/audit.ts +0 -0
- /package/{templates → examples}/provisions/src/schema.ts +0 -0
- /package/{templates → examples}/provisions/src/vault.ts +0 -0
- /package/{templates → examples}/provisions/tests/orders.test.ts +0 -0
- /package/{templates → examples}/skyport/evals/triage.jsonl +0 -0
- /package/{templates → examples}/skyport/oke.config.ts +0 -0
- /package/{templates → examples}/skyport/oke.images.lock +0 -0
- /package/{templates → examples}/skyport/src/ai.ts +0 -0
- /package/{templates → examples}/skyport/src/app.ts +0 -0
- /package/{templates → examples}/skyport/src/channels.ts +0 -0
- /package/{templates → examples}/skyport/src/core.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/bookings/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/bookings/shapes.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/bookings/signals.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/notifications/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/payments/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/payments/shapes.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/support/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/users/elements.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/users/index.ts +0 -0
- /package/{templates → examples}/skyport/src/flows/users/shapes.ts +0 -0
- /package/{templates → examples}/skyport/src/gates.ts +0 -0
- /package/{templates → examples}/skyport/src/journeys.ts +0 -0
- /package/{templates → examples}/skyport/src/locales/ar.ts +0 -0
- /package/{templates → examples}/skyport/src/locales/en.ts +0 -0
- /package/{templates → examples}/skyport/src/plugins/audit-schema.ts +0 -0
- /package/{templates → examples}/skyport/src/plugins/audit.ts +0 -0
- /package/{templates → examples}/skyport/src/plugins/panel.tsx +0 -0
- /package/{templates → examples}/skyport/src/schema.ts +0 -0
- /package/{templates → examples}/skyport/src/vault.ts +0 -0
- /package/{templates → examples}/skyport/tests/support.test.ts +0 -0
package/README.md
CHANGED
|
@@ -8,21 +8,41 @@ 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 |
|
|
14
18
|
| `:6533` | Console |
|
|
15
19
|
| `:6535` | MCP |
|
|
16
20
|
|
|
17
|
-
Default template is **
|
|
21
|
+
Default template is **standard** (full recommended file layout, empty scaffolding).
|
|
22
|
+
Pick another with `--template`, or start from a teaching example with `--from-example`:
|
|
18
23
|
|
|
19
24
|
```bash
|
|
20
|
-
bunx create-oke@latest my-
|
|
21
|
-
bunx create-oke@latest my-
|
|
22
|
-
bunx create-oke@latest my-
|
|
25
|
+
bunx create-oke@latest my-app --template hello
|
|
26
|
+
bunx create-oke@latest my-app --template minimal
|
|
27
|
+
bunx create-oke@latest my-app --template full
|
|
28
|
+
bunx create-oke@latest my-app --yes
|
|
29
|
+
bunx create-oke@latest my-notes --from-example notes
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
| Template |
|
|
32
|
+
| Template | Purpose |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `hello` | Fastest possible "it works" — one flow, no Store |
|
|
35
|
+
| `minimal` | Smallest shape you'd actually ship — Store + 1–2 flows |
|
|
36
|
+
| `standard` | Full recommended file layout, empty scaffolding (default) |
|
|
37
|
+
| `full` | Every element present and wired, no business logic |
|
|
38
|
+
|
|
39
|
+
Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
|
|
40
|
+
(`--no-agents-md` to skip).
|
|
41
|
+
|
|
42
|
+
`--from-example` copies a teaching example (including its business logic and
|
|
43
|
+
comments) — most new projects want `--template` instead.
|
|
44
|
+
|
|
45
|
+
| Example | Adds |
|
|
26
46
|
|---|---|
|
|
27
47
|
| `notes` | `oke` · `flow` · `http` · `store.sql` · typed client |
|
|
28
48
|
| `linkly` | `signal` · `clock` · `gate` |
|
|
@@ -36,4 +56,4 @@ bunx create-oke@latest my-sky --template skyport
|
|
|
36
56
|
| npm (CLI) | [`create-oke`](https://www.npmjs.com/package/create-oke) — use this with `bunx` |
|
|
37
57
|
| JSR | [`@omqkhafi/create-oke`](https://jsr.io/@omqkhafi/create-oke) — same sources; prefer npm for the binary |
|
|
38
58
|
|
|
39
|
-
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-oke",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Scaffold an okengine app — bunx create-oke@latest <name>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
|
-
"templates"
|
|
21
|
+
"templates",
|
|
22
|
+
"examples"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"prepack": "bun ./src/sync-templates.ts",
|
|
@@ -29,6 +30,9 @@
|
|
|
29
30
|
"engines": {
|
|
30
31
|
"bun": ">=1.3"
|
|
31
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@clack/prompts": "^1.7.0"
|
|
35
|
+
},
|
|
32
36
|
"devDependencies": {
|
|
33
37
|
"@types/bun": "latest",
|
|
34
38
|
"typescript": "^7.0.2"
|
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
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for create-oke argument parsing and
|
|
2
|
+
* Unit tests for create-oke argument parsing, transforms, and non-TTY behavior.
|
|
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
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
FROM_EXAMPLE_CHOICE,
|
|
17
|
+
formatCdPath,
|
|
18
|
+
nextStepsText,
|
|
19
|
+
parseArgs,
|
|
20
|
+
run,
|
|
21
|
+
scaffoldArgsFromAnswers,
|
|
22
|
+
scaffoldArgsFromCli,
|
|
23
|
+
shouldPrompt,
|
|
24
|
+
sourceFromArgs,
|
|
25
|
+
type InteractiveAnswers,
|
|
26
|
+
} from "./cli.ts";
|
|
10
27
|
import { listTemplateFiles, scaffold } from "./scaffold.ts";
|
|
11
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_TEMPLATE,
|
|
30
|
+
EXAMPLES,
|
|
31
|
+
TEMPLATES,
|
|
32
|
+
packageRoot,
|
|
33
|
+
resolveExampleDir,
|
|
34
|
+
resolveTemplateDir,
|
|
35
|
+
} from "./templates.ts";
|
|
12
36
|
import {
|
|
13
37
|
sanitizeProjectName,
|
|
14
38
|
shouldSkipTemplatePath,
|
|
@@ -16,22 +40,130 @@ import {
|
|
|
16
40
|
} from "./transform.ts";
|
|
17
41
|
|
|
18
42
|
describe("parseArgs", () => {
|
|
19
|
-
test("defaults template to
|
|
43
|
+
test("defaults template to standard", () => {
|
|
20
44
|
const a = parseArgs(["my-app"]);
|
|
21
45
|
expect(a.name).toBe("my-app");
|
|
22
|
-
expect(a.template).toBe("
|
|
46
|
+
expect(a.template).toBe("standard");
|
|
47
|
+
expect(a.templateExplicit).toBe(false);
|
|
48
|
+
expect(a.fromExample).toBeUndefined();
|
|
23
49
|
});
|
|
24
50
|
|
|
25
51
|
test("accepts --template and -t", () => {
|
|
26
|
-
expect(parseArgs(["x", "--template", "
|
|
27
|
-
expect(parseArgs(["x", "-t", "
|
|
28
|
-
expect(parseArgs(["x", "--template=
|
|
29
|
-
|
|
52
|
+
expect(parseArgs(["x", "--template", "hello"]).template).toBe("hello");
|
|
53
|
+
expect(parseArgs(["x", "-t", "full"]).template).toBe("full");
|
|
54
|
+
expect(parseArgs(["x", "--template=minimal"]).template).toBe("minimal");
|
|
55
|
+
expect(parseArgs(["x", "--template", "hello"]).templateExplicit).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("accepts --from-example", () => {
|
|
59
|
+
expect(parseArgs(["x", "--from-example", "notes"]).fromExample).toBe(
|
|
60
|
+
"notes",
|
|
61
|
+
);
|
|
62
|
+
expect(parseArgs(["x", "--from-example=skyport"]).fromExample).toBe(
|
|
63
|
+
"skyport",
|
|
30
64
|
);
|
|
31
65
|
});
|
|
32
66
|
|
|
33
|
-
test("rejects unknown template", () => {
|
|
67
|
+
test("rejects unknown template / example / both", () => {
|
|
34
68
|
expect(() => parseArgs(["x", "--template", "nope"])).toThrow(/template/);
|
|
69
|
+
expect(() => parseArgs(["x", "--from-example", "nope"])).toThrow(/example/);
|
|
70
|
+
expect(() =>
|
|
71
|
+
parseArgs(["x", "--template", "hello", "--from-example", "notes"]),
|
|
72
|
+
).toThrow(/either/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("shouldPrompt", () => {
|
|
77
|
+
test("TTY + no args → interactive", () => {
|
|
78
|
+
expect(shouldPrompt(parseArgs([]), true)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
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", () => {
|
|
86
|
+
expect(shouldPrompt(parseArgs([]), false)).toBe(false);
|
|
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
|
+
);
|
|
92
|
+
expect(shouldPrompt(parseArgs(["--template", "hello"]), true)).toBe(false);
|
|
93
|
+
expect(shouldPrompt(parseArgs(["--from-example", "notes"]), true)).toBe(
|
|
94
|
+
false,
|
|
95
|
+
);
|
|
96
|
+
expect(shouldPrompt(parseArgs(["--help"]), true)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
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
|
+
|
|
117
|
+
describe("sourceFromArgs", () => {
|
|
118
|
+
test("prefers example when set", () => {
|
|
119
|
+
expect(sourceFromArgs(parseArgs(["x", "--from-example", "linkly"]))).toEqual(
|
|
120
|
+
{ kind: "example", id: "linkly" },
|
|
121
|
+
);
|
|
122
|
+
expect(sourceFromArgs(parseArgs(["x"]))).toEqual({
|
|
123
|
+
kind: "template",
|
|
124
|
+
id: DEFAULT_TEMPLATE,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
|
|
130
|
+
test("each --template path matches interactive choice", () => {
|
|
131
|
+
for (const id of TEMPLATES) {
|
|
132
|
+
const answers: InteractiveAnswers = {
|
|
133
|
+
name: "x",
|
|
134
|
+
choice: id,
|
|
135
|
+
installAndRun: false,
|
|
136
|
+
agentsMd: true,
|
|
137
|
+
};
|
|
138
|
+
const fromAnswers = scaffoldArgsFromAnswers(answers);
|
|
139
|
+
const fromFlags = scaffoldArgsFromCli(
|
|
140
|
+
parseArgs(["x", "--template", id]),
|
|
141
|
+
);
|
|
142
|
+
expect(fromAnswers).toEqual(fromFlags);
|
|
143
|
+
expect(fromAnswers.source).toEqual({ kind: "template", id });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("each --from-example path matches interactive from-example choice", () => {
|
|
148
|
+
for (const id of EXAMPLES) {
|
|
149
|
+
const answers: InteractiveAnswers = {
|
|
150
|
+
name: "x",
|
|
151
|
+
choice: FROM_EXAMPLE_CHOICE,
|
|
152
|
+
example: id,
|
|
153
|
+
installAndRun: false,
|
|
154
|
+
agentsMd: true,
|
|
155
|
+
};
|
|
156
|
+
const fromAnswers = scaffoldArgsFromAnswers(answers);
|
|
157
|
+
const fromFlags = scaffoldArgsFromCli(
|
|
158
|
+
parseArgs(["x", "--from-example", id]),
|
|
159
|
+
);
|
|
160
|
+
expect(fromAnswers).toEqual(fromFlags);
|
|
161
|
+
expect(fromAnswers.source).toEqual({ kind: "example", id });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("8 paths cover every template and every example", () => {
|
|
166
|
+
expect(TEMPLATES.length + EXAMPLES.length).toBe(8);
|
|
35
167
|
});
|
|
36
168
|
});
|
|
37
169
|
|
|
@@ -39,7 +171,7 @@ describe("transformPackageJson", () => {
|
|
|
39
171
|
test("rewrites name and okengine file:../.. to installable ref", () => {
|
|
40
172
|
const next = transformPackageJson(
|
|
41
173
|
{
|
|
42
|
-
name: "@oke/
|
|
174
|
+
name: "@oke/template-hello",
|
|
43
175
|
private: true,
|
|
44
176
|
dependencies: {
|
|
45
177
|
okengine: "file:../..",
|
|
@@ -50,6 +182,7 @@ describe("transformPackageJson", () => {
|
|
|
50
182
|
"0.0.26",
|
|
51
183
|
);
|
|
52
184
|
expect(next.name).toBe("my-app");
|
|
185
|
+
expect(next.version).toBe("0.0.1");
|
|
53
186
|
expect(next.dependencies?.["okengine"]).toBe("0.0.26");
|
|
54
187
|
expect(next.dependencies?.["okengine"]).not.toBe("file:../..");
|
|
55
188
|
expect(next.dependencies?.["zod"]).toBe("^4.4.3");
|
|
@@ -81,7 +214,7 @@ describe("formatCdPath", () => {
|
|
|
81
214
|
});
|
|
82
215
|
|
|
83
216
|
describe("scaffold structure", () => {
|
|
84
|
-
test("each template produces
|
|
217
|
+
test("each clean template produces its source tree (minus skips)", () => {
|
|
85
218
|
for (const id of TEMPLATES) {
|
|
86
219
|
const dir = mkdtempSync(join(tmpdir(), `create-oke-${id}-`));
|
|
87
220
|
try {
|
|
@@ -90,21 +223,187 @@ describe("scaffold structure", () => {
|
|
|
90
223
|
const result = scaffold({
|
|
91
224
|
targetDir: join(dir, id),
|
|
92
225
|
name: `app-${id}`,
|
|
93
|
-
|
|
226
|
+
source: { kind: "template", id },
|
|
94
227
|
});
|
|
95
|
-
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/);
|
|
96
242
|
const pkg = JSON.parse(
|
|
97
243
|
readFileSync(join(result.targetDir, "package.json"), "utf8"),
|
|
98
244
|
) as { name: string; dependencies: { okengine: string } };
|
|
99
245
|
expect(pkg.name).toBe(`app-${id}`);
|
|
100
246
|
expect(pkg.dependencies.okengine).not.toBe("file:../..");
|
|
101
|
-
expect(
|
|
102
|
-
pkg.dependencies.okengine.startsWith("file:") ||
|
|
103
|
-
/^\d+\.\d+\.\d+/.test(pkg.dependencies.okengine),
|
|
104
|
-
).toBe(true);
|
|
105
247
|
} finally {
|
|
106
248
|
rmSync(dir, { recursive: true, force: true });
|
|
107
249
|
}
|
|
108
250
|
}
|
|
109
251
|
});
|
|
252
|
+
|
|
253
|
+
test("hello has exactly one flow and no Store", () => {
|
|
254
|
+
const dir = mkdtempSync(join(tmpdir(), "create-oke-hello-assert-"));
|
|
255
|
+
try {
|
|
256
|
+
const result = scaffold({
|
|
257
|
+
targetDir: join(dir, "hello"),
|
|
258
|
+
name: "hello-app",
|
|
259
|
+
source: { kind: "template", id: "hello" },
|
|
260
|
+
});
|
|
261
|
+
const flowFiles = result.files.filter(
|
|
262
|
+
(f) => f.startsWith("src/flows/") && f.endsWith(".ts"),
|
|
263
|
+
);
|
|
264
|
+
expect(flowFiles).toEqual(["src/flows/hello/index.ts"]);
|
|
265
|
+
const flowSrc = readFileSync(
|
|
266
|
+
join(result.targetDir, "src/flows/hello/index.ts"),
|
|
267
|
+
"utf8",
|
|
268
|
+
);
|
|
269
|
+
expect(flowSrc).toMatch(/export const hello/);
|
|
270
|
+
expect(flowSrc).not.toMatch(/\bstore\b/);
|
|
271
|
+
expect(result.files.some((f) => f.includes("core.ts"))).toBe(false);
|
|
272
|
+
expect(result.files.some((f) => f.includes("schema.ts"))).toBe(false);
|
|
273
|
+
const all = result.files
|
|
274
|
+
.filter((f) => f.endsWith(".ts"))
|
|
275
|
+
.map((f) => readFileSync(join(result.targetDir, f), "utf8"))
|
|
276
|
+
.join("\n");
|
|
277
|
+
expect(all).not.toMatch(/store\.sql/);
|
|
278
|
+
expect(all).not.toMatch(/\bbookings\b|\borders\b|\blinks\b|\bnotes\b/);
|
|
279
|
+
} finally {
|
|
280
|
+
rmSync(dir, { recursive: true, force: true });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("standard has full four-applications layout and no business domain", () => {
|
|
285
|
+
const dir = mkdtempSync(join(tmpdir(), "create-oke-standard-assert-"));
|
|
286
|
+
try {
|
|
287
|
+
const result = scaffold({
|
|
288
|
+
targetDir: join(dir, "standard"),
|
|
289
|
+
name: "standard-app",
|
|
290
|
+
source: { kind: "template", id: "standard" },
|
|
291
|
+
});
|
|
292
|
+
for (const path of [
|
|
293
|
+
"src/gates.ts",
|
|
294
|
+
"src/vault.ts",
|
|
295
|
+
"src/channels.ts",
|
|
296
|
+
"src/locales/en.ts",
|
|
297
|
+
"src/locales/ar.ts",
|
|
298
|
+
"src/flows/main/shapes.ts",
|
|
299
|
+
"src/flows/main/signals.ts",
|
|
300
|
+
"src/core.ts",
|
|
301
|
+
"src/schema.ts",
|
|
302
|
+
"src/app.ts",
|
|
303
|
+
]) {
|
|
304
|
+
expect(result.files).toContain(path);
|
|
305
|
+
}
|
|
306
|
+
const all = result.files
|
|
307
|
+
.filter((f) => f.endsWith(".ts") || f.endsWith(".md"))
|
|
308
|
+
.map((f) => readFileSync(join(result.targetDir, f), "utf8"))
|
|
309
|
+
.join("\n");
|
|
310
|
+
expect(all).not.toMatch(/\bbookings\b|\borders\b|\blinks\b|\bstripe\b/i);
|
|
311
|
+
} finally {
|
|
312
|
+
rmSync(dir, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("--from-example notes matches today's example tree", () => {
|
|
317
|
+
const dir = mkdtempSync(join(tmpdir(), "create-oke-from-example-"));
|
|
318
|
+
try {
|
|
319
|
+
const exampleDir = resolveExampleDir("notes");
|
|
320
|
+
const expected = listTemplateFiles(exampleDir);
|
|
321
|
+
const result = scaffold({
|
|
322
|
+
targetDir: join(dir, "notes"),
|
|
323
|
+
name: "from-notes",
|
|
324
|
+
source: { kind: "example", id: "notes" },
|
|
325
|
+
});
|
|
326
|
+
expect([...result.files].sort()).toEqual(
|
|
327
|
+
[...expected, "AGENTS.md"].sort(),
|
|
328
|
+
);
|
|
329
|
+
expect(result.files).toContain("src/flows/notes/index.ts");
|
|
330
|
+
} finally {
|
|
331
|
+
rmSync(dir, { recursive: true, force: true });
|
|
332
|
+
}
|
|
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
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("non-TTY CLI", () => {
|
|
352
|
+
test("no args + non-TTY → zero prompts, exit 1", async () => {
|
|
353
|
+
const code = await run([], { stdinIsTTY: false, runPostScaffold: false });
|
|
354
|
+
expect(code).toBe(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("spawned with piped stdin shows no clack prompts", async () => {
|
|
358
|
+
const proc = Bun.spawn(["bun", "run", join(packageRoot(), "src/index.ts")], {
|
|
359
|
+
cwd: packageRoot(),
|
|
360
|
+
stdin: "pipe",
|
|
361
|
+
stdout: "pipe",
|
|
362
|
+
stderr: "pipe",
|
|
363
|
+
});
|
|
364
|
+
proc.stdin.end();
|
|
365
|
+
const code = await proc.exited;
|
|
366
|
+
const out = await new Response(proc.stdout).text();
|
|
367
|
+
const err = await new Response(proc.stderr).text();
|
|
368
|
+
expect(code).toBe(1);
|
|
369
|
+
expect(err).toMatch(/missing <name>/);
|
|
370
|
+
// Clack intro / select chrome must not appear.
|
|
371
|
+
expect(out + err).not.toMatch(/Project name|Start from a worked example/);
|
|
372
|
+
expect(out + err).not.toMatch(/│/);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("flag-driven scaffold prints the shared next-steps text", async () => {
|
|
376
|
+
const root = mkdtempSync(join(tmpdir(), "create-oke-flag-"));
|
|
377
|
+
const target = join(root, "flag-app");
|
|
378
|
+
try {
|
|
379
|
+
const code = await run(
|
|
380
|
+
[target, "--template", "hello", "--no-install"],
|
|
381
|
+
{ stdinIsTTY: false, runPostScaffold: false },
|
|
382
|
+
);
|
|
383
|
+
expect(code).toBe(0);
|
|
384
|
+
expect(readdirSync(target).length).toBeGreaterThan(0);
|
|
385
|
+
expect(existsSync(join(target, "AGENTS.md"))).toBe(true);
|
|
386
|
+
const result = {
|
|
387
|
+
targetDir: target,
|
|
388
|
+
name: "flag-app",
|
|
389
|
+
source: { kind: "template" as const, id: "hello" as const },
|
|
390
|
+
label: "hello",
|
|
391
|
+
okengineDependency: "x",
|
|
392
|
+
files: [],
|
|
393
|
+
};
|
|
394
|
+
expect(nextStepsText(result)).toContain("oke dev");
|
|
395
|
+
expect(nextStepsText(result)).toContain("bun install");
|
|
396
|
+
expect(nextStepsText(result)).toContain("okengine.vercel.app");
|
|
397
|
+
} finally {
|
|
398
|
+
rmSync(root, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("examples catalogue", () => {
|
|
404
|
+
test("all four teaching examples resolve", () => {
|
|
405
|
+
for (const id of EXAMPLES) {
|
|
406
|
+
expect(resolveExampleDir(id)).toBeTruthy();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
110
409
|
});
|