create-oke 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents-md.ts +4 -1
- package/src/ai-setup/apply.ts +64 -6
- package/src/ai-setup/from-pref.ts +4 -4
- package/src/ai-setup/prompts.ts +1 -1
- package/src/cli.test.ts +133 -11
- package/src/cli.ts +180 -29
- package/src/create-defaults.test.ts +9 -0
- package/src/create-defaults.ts +27 -0
- package/src/customize-flow.test.ts +14 -7
- package/src/customize-flow.ts +2 -1
- package/src/drivers-catalog.ts +18 -9
- package/src/local-okengine.test.ts +2 -0
- package/src/local-okengine.ts +3 -0
- package/src/scaffold.ts +17 -5
- package/src/transform.test.ts +6 -3
- package/src/transform.ts +24 -1
- package/templates/advanced/.env.example +13 -5
- package/templates/advanced/README.md +3 -0
- package/templates/advanced/oke.config.ts +6 -5
- package/templates/advanced/package.json +14 -3
- package/templates/advanced/src/app.ts +2 -1
- package/templates/advanced/src/core.ts +12 -0
- package/templates/advanced/src/db/seed/index.ts +3 -1
- package/templates/advanced/src/flows/main/index.ts +4 -4
- package/templates/advanced/src/flows/notes/index.ts +73 -26
- package/templates/advanced/src/flows/notes/shapes.ts +9 -4
- package/templates/advanced/tests/advanced.test.ts +3 -2
- package/templates/advanced/web/index.html +12 -0
- package/templates/advanced/web/src/App.css +78 -0
- package/templates/advanced/web/src/App.tsx +100 -0
- package/templates/advanced/web/src/client.ts +38 -0
- package/templates/advanced/web/src/index.css +18 -0
- package/templates/advanced/web/src/main.tsx +13 -0
- package/templates/advanced/web/src/vite-env.d.ts +17 -0
- package/templates/advanced/web/tsconfig.app.json +24 -0
- package/templates/advanced/web/tsconfig.json +4 -0
- package/templates/advanced/web/tsconfig.node.json +20 -0
- package/templates/advanced/web/vite.config.ts +32 -0
- package/templates/standard/.env.example +13 -5
- package/templates/standard/README.md +3 -0
- package/templates/standard/oke.config.ts +4 -3
- package/templates/standard/package.json +14 -3
- package/templates/standard/src/app.ts +2 -1
- package/templates/standard/src/core.ts +12 -0
- package/templates/standard/src/db/seed/index.ts +2 -0
- package/templates/standard/src/flows/main/index.ts +4 -4
- package/templates/standard/src/flows/notes/index.ts +12 -12
- package/templates/standard/src/flows/notes/shapes.ts +2 -3
- package/templates/standard/tests/standard.test.ts +3 -2
- package/templates/standard/web/index.html +12 -0
- package/templates/standard/web/src/App.css +78 -0
- package/templates/standard/web/src/App.tsx +100 -0
- package/templates/standard/web/src/client.ts +38 -0
- package/templates/standard/web/src/index.css +18 -0
- package/templates/standard/web/src/main.tsx +13 -0
- package/templates/standard/web/src/vite-env.d.ts +17 -0
- package/templates/standard/web/tsconfig.app.json +24 -0
- package/templates/standard/web/tsconfig.json +4 -0
- package/templates/standard/web/tsconfig.node.json +20 -0
- package/templates/standard/web/vite.config.ts +32 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
max-width: 40rem;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 2rem 1.25rem 4rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.head {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: baseline;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
gap: 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.head h1 {
|
|
15
|
+
margin: 0;
|
|
16
|
+
font-size: 1.5rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ok,
|
|
20
|
+
.down {
|
|
21
|
+
margin: 0;
|
|
22
|
+
font-size: 0.875rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ok {
|
|
26
|
+
color: #047857;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.down {
|
|
30
|
+
color: #b45309;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.compose {
|
|
34
|
+
display: grid;
|
|
35
|
+
gap: 0.5rem;
|
|
36
|
+
margin: 1.5rem 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.compose input,
|
|
40
|
+
.compose textarea,
|
|
41
|
+
.compose button,
|
|
42
|
+
.list button {
|
|
43
|
+
font: inherit;
|
|
44
|
+
padding: 0.5rem 0.75rem;
|
|
45
|
+
border: 1px solid #cbd5e1;
|
|
46
|
+
border-radius: 0.375rem;
|
|
47
|
+
background: #fff;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.compose button,
|
|
51
|
+
.list button {
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
width: fit-content;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.err {
|
|
57
|
+
color: #b91c1c;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.list {
|
|
61
|
+
list-style: none;
|
|
62
|
+
padding: 0;
|
|
63
|
+
margin: 0;
|
|
64
|
+
display: grid;
|
|
65
|
+
gap: 0.75rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.list li {
|
|
69
|
+
display: flex;
|
|
70
|
+
justify-content: space-between;
|
|
71
|
+
gap: 1rem;
|
|
72
|
+
padding: 0.75rem 0;
|
|
73
|
+
border-top: 1px solid #e2e8f0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.list p {
|
|
77
|
+
margin: 0.25rem 0 0;
|
|
78
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState, type FormEvent, type JSX } from "react";
|
|
2
|
+
import { isOk } from "okengine/client";
|
|
3
|
+
import { api, type Note } from "./client.ts";
|
|
4
|
+
import "./App.css";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Notes SPA — health + list + create + archive against the oke app.
|
|
8
|
+
*/
|
|
9
|
+
export function App(): JSX.Element {
|
|
10
|
+
const [health, setHealth] = useState<"unknown" | "ok" | "down">("unknown");
|
|
11
|
+
const [notes, setNotes] = useState<readonly Note[]>([]);
|
|
12
|
+
const [title, setTitle] = useState("");
|
|
13
|
+
const [body, setBody] = useState("");
|
|
14
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
const refresh = useCallback(async () => {
|
|
17
|
+
const live = await api.main.health({});
|
|
18
|
+
setHealth(isOk(live) && live.data.ok ? "ok" : "down");
|
|
19
|
+
const listed = await api.notes.list({});
|
|
20
|
+
if (isOk(listed) && Array.isArray(listed.data)) {
|
|
21
|
+
setNotes(listed.data as readonly Note[]);
|
|
22
|
+
setMessage(null);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setMessage(listed.error?.code ?? "notes.list failed");
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
void refresh();
|
|
30
|
+
}, [refresh]);
|
|
31
|
+
|
|
32
|
+
async function onCreate(event: FormEvent<HTMLFormElement>): Promise<void> {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
const created = await api.notes.create({ title, body });
|
|
35
|
+
if (!isOk(created)) {
|
|
36
|
+
setMessage(created.error.code);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setTitle("");
|
|
40
|
+
setBody("");
|
|
41
|
+
await refresh();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function onArchive(id: string): Promise<void> {
|
|
45
|
+
const archived = await api.notes.archive({ id });
|
|
46
|
+
if (!isOk(archived)) {
|
|
47
|
+
setMessage(archived.error.code);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await refresh();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<main className="page">
|
|
55
|
+
<header className="head">
|
|
56
|
+
<h1>Notes</h1>
|
|
57
|
+
<p className={health === "ok" ? "ok" : "down"}>
|
|
58
|
+
{health === "unknown" ? "checking…" : health === "ok" ? "app up" : "app down — start oke dev"}
|
|
59
|
+
</p>
|
|
60
|
+
</header>
|
|
61
|
+
|
|
62
|
+
<form className="compose" onSubmit={(event) => void onCreate(event)}>
|
|
63
|
+
<input
|
|
64
|
+
name="title"
|
|
65
|
+
placeholder="Title"
|
|
66
|
+
value={title}
|
|
67
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
68
|
+
required
|
|
69
|
+
maxLength={200}
|
|
70
|
+
/>
|
|
71
|
+
<textarea
|
|
72
|
+
name="body"
|
|
73
|
+
placeholder="Body"
|
|
74
|
+
value={body}
|
|
75
|
+
onChange={(event) => setBody(event.target.value)}
|
|
76
|
+
required
|
|
77
|
+
maxLength={10_000}
|
|
78
|
+
rows={4}
|
|
79
|
+
/>
|
|
80
|
+
<button type="submit">Create</button>
|
|
81
|
+
</form>
|
|
82
|
+
|
|
83
|
+
{message ? <p className="err">{message}</p> : null}
|
|
84
|
+
|
|
85
|
+
<ul className="list">
|
|
86
|
+
{notes.map((note) => (
|
|
87
|
+
<li key={note.id}>
|
|
88
|
+
<div>
|
|
89
|
+
<strong>{note.title}</strong>
|
|
90
|
+
<p>{note.body}</p>
|
|
91
|
+
</div>
|
|
92
|
+
<button type="button" onClick={() => void onArchive(note.id)}>
|
|
93
|
+
Archive
|
|
94
|
+
</button>
|
|
95
|
+
</li>
|
|
96
|
+
))}
|
|
97
|
+
</ul>
|
|
98
|
+
</main>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser client — Vite proxies these paths to `oke dev` (:6530).
|
|
3
|
+
*
|
|
4
|
+
* `$routes` is passed at runtime so HTTP triggers use REST, not `/_oke` RPC.
|
|
5
|
+
* Do not import `src/app.ts` here — that graph is Bun/server-only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createClient } from "okengine/client";
|
|
9
|
+
|
|
10
|
+
/** One note from `notes.list` / `notes.create`. */
|
|
11
|
+
export type Note = {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly title: string;
|
|
14
|
+
readonly body: string;
|
|
15
|
+
readonly archivedAt: number | null;
|
|
16
|
+
readonly createdAt: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const $routes = {
|
|
20
|
+
main: {
|
|
21
|
+
health: { method: "GET", path: "/health" },
|
|
22
|
+
},
|
|
23
|
+
notes: {
|
|
24
|
+
list: { method: "GET", path: "/notes" },
|
|
25
|
+
create: { method: "POST", path: "/notes" },
|
|
26
|
+
get: { method: "GET", path: "/notes/:id" },
|
|
27
|
+
archive: { method: "POST", path: "/notes/:id/archive" },
|
|
28
|
+
},
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
type AppRoutes = { readonly $routes: typeof $routes };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Typed caller for starter Flows. `VITE_API_URL` is empty in dev (proxy).
|
|
35
|
+
*/
|
|
36
|
+
export const api = createClient<AppRoutes>(import.meta.env.VITE_API_URL ?? "", {
|
|
37
|
+
$routes,
|
|
38
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { App } from "./App.tsx";
|
|
4
|
+
import "./index.css";
|
|
5
|
+
|
|
6
|
+
const root = document.getElementById("root");
|
|
7
|
+
if (!root) throw new Error("web: #root missing");
|
|
8
|
+
|
|
9
|
+
createRoot(root).render(
|
|
10
|
+
<StrictMode>
|
|
11
|
+
<App />
|
|
12
|
+
</StrictMode>,
|
|
13
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface ViteTypeOptions {
|
|
2
|
+
// By adding this line, you can make the type of ImportMetaEnv strict
|
|
3
|
+
// to disallow unknown keys.
|
|
4
|
+
// strictImportMetaEnv: unknown
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface ImportMetaEnv {
|
|
8
|
+
/**
|
|
9
|
+
* App origin for `createClient`. Unset / empty = same origin so Vite's
|
|
10
|
+
* `server.proxy` forwards `/notes` · `/health` · `/_oke` to `oke dev`.
|
|
11
|
+
*/
|
|
12
|
+
readonly VITE_API_URL?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ImportMeta {
|
|
16
|
+
readonly env: ImportMetaEnv;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023", "DOM"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"types": ["vite/client"],
|
|
8
|
+
"allowArbitraryExtensions": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"erasableSyntaxOnly": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"types": ["bun"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"module": "nodenext",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"erasableSyntaxOnly": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["vite.config.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite 8 SPA for the Notes starter.
|
|
3
|
+
*
|
|
4
|
+
* `index.html` is the documented entry (not backend-integration HTML).
|
|
5
|
+
* The oke app stays on :6530. `server.proxy` keeps `createClient("")`
|
|
6
|
+
* same-origin — leave CORS at Vite's localhost default. `preview.proxy` inherits
|
|
7
|
+
* this map. Do not proxy `/` — `main.root` is `GET /` and would steal the SPA.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import react from "@vitejs/plugin-react";
|
|
11
|
+
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { defineConfig, type ProxyOptions } from "vite";
|
|
14
|
+
|
|
15
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
/** Local oke app (`oke dev`). */
|
|
18
|
+
const APP_ORIGIN = "http://127.0.0.1:6530";
|
|
19
|
+
|
|
20
|
+
const proxy: Record<string, ProxyOptions> = {
|
|
21
|
+
"/health": { target: APP_ORIGIN, changeOrigin: true },
|
|
22
|
+
"/notes": { target: APP_ORIGIN, changeOrigin: true },
|
|
23
|
+
"/_oke": { target: APP_ORIGIN, changeOrigin: true },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default defineConfig({
|
|
27
|
+
root,
|
|
28
|
+
plugins: [react()],
|
|
29
|
+
// Load `VITE_*` from the app root (same place as `.env.example`).
|
|
30
|
+
envDir: resolve(root, ".."),
|
|
31
|
+
server: { proxy },
|
|
32
|
+
});
|