okengine 0.11.1 → 0.11.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/package.json +1 -1
- package/site/content/docs/elements/ai.mdx +22 -1
- package/site/content/docs/elements/store.mdx +3 -1
- package/site/content/docs/elements/vault.mdx +19 -11
- package/site/content/docs/get-started/installation.mdx +7 -1
- package/site/content/docs/recipes/llama-cpp.mdx +10 -9
- package/site/content/docs/reference/cli.md +6 -2
- package/site/content/docs/reference/environment-variables.mdx +9 -9
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +61 -1
- package/src/cli/ask-seed.test.ts +4 -3
- package/src/cli/ask-seed.ts +5 -6
- package/src/cli/client-add.test.ts +2 -1
- package/src/cli/dev.test.ts +116 -0
- package/src/cli/dev.ts +107 -18
- package/src/cli/project-state.test.ts +50 -0
- package/src/cli/project-state.ts +123 -0
- package/src/cli/vault-cmd.test.ts +47 -18
- package/src/cli/vault-cmd.ts +2 -1
- package/src/compiler/extract.ts +12 -1
- package/src/console/server/console.test.ts +3 -1
- package/src/console/server/operator-db.test.ts +48 -17
- package/src/console/server/operator-db.ts +5 -1
- package/src/docker/derive.ts +24 -3
- package/src/docker/docker.test.ts +4 -2
- package/src/docker/index.ts +1 -0
- package/src/docker/recipes/index.ts +1 -0
- package/src/docker/recipes/llama-cpp.ts +20 -4
- package/src/drivers/ai-openai-compatible.ts +15 -3
- package/src/drivers/vault-builtin.test.ts +50 -42
- package/src/elements/ai/declare.ts +73 -3
- package/src/elements/ai/errors.test.ts +35 -0
- package/src/elements/ai/errors.ts +139 -0
- package/src/elements/ai/eval.ts +26 -1
- package/src/elements/ai/runtime.ts +140 -80
- package/src/elements/ai/tools.test.ts +1 -1
- package/src/elements/ai.test.ts +99 -2
- package/src/elements/ai.ts +11 -1
- package/src/elements/index.ts +2 -0
- package/src/elements/store/index-boot.test.ts +23 -6
- package/src/elements/store/resource.test.ts +38 -19
- package/src/elements/store/sql-session.test.ts +55 -58
- package/src/elements/vault/builtin-adapter.test.ts +115 -58
- package/src/elements/vault/builtin-adapter.ts +241 -47
- package/src/elements/vault/chaos-child.ts +424 -0
- package/src/elements/vault/chaos.test.ts +651 -0
- package/src/elements/vault/resilience.ts +6 -1
- package/src/elements/vault/security-checklist.test.ts +10 -8
- package/src/elements/vault/storage.ts +130 -27
- package/src/elements/vault/test-helpers.ts +368 -0
- package/src/elements/vault.ts +6 -0
- package/src/index.ts +2 -0
- package/src/kernel/app.ts +83 -10
- package/src/kernel/auto-registry.test.ts +52 -1
- package/src/kernel/element-registries.ts +19 -4
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/fx.test.ts +25 -0
- package/src/kernel/fx.ts +4 -1
- package/src/manifest/types.ts +4 -0
- package/src/test/create-test-app.ts +16 -11
- package/src/test/reset-element-registries.ts +17 -9
package/package.json
CHANGED
|
@@ -86,7 +86,28 @@ export const triage = smart.prompt("ticket-triage", {
|
|
|
86
86
|
| `in` / `out` | zod / Standard Schema | Input and output contracts — responses validated against `out` |
|
|
87
87
|
| `version` | number | Artifact version — diffs and regressions tracked per version |
|
|
88
88
|
| `evals` | string | Path to a `.jsonl` eval set, regression-gated via `oke eval` |
|
|
89
|
-
| `budget` | object | `maxCostPerCall` — cost
|
|
89
|
+
| `budget` | object | `maxCostPerCall` / `maxCostPerRun` — cost, not time |
|
|
90
|
+
| `via` | `string[]` | Ordered recovery chain of logical model names for this prompt |
|
|
91
|
+
| `timeout` | `string \| number` | Deadline — clock duration (`"30s"`) or milliseconds |
|
|
92
|
+
|
|
93
|
+
## Recovery chain + per-command timeout
|
|
94
|
+
|
|
95
|
+
Fallback is **recovery**, not a fixed cloud→local doctrine. Declare the chain and deadline on the prompt; the call site stays thin:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
export const summarizeNote = smart.prompt("summarize-note", {
|
|
99
|
+
via: ["smart", "local"],
|
|
100
|
+
timeout: "30s",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// in a flow
|
|
104
|
+
const out = await fx.ask(summarizeNote, { title, body });
|
|
105
|
+
// out.via is the logical model that answered
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Resolution: `fx.ask(…, { via })` overrides `prompt.via`, else the prompt’s bound model. On each model the runtime retries **once** for retryable failures (timeout / 429 / 5xx / network), then advances. Permanent failures (401 / other 4xx / schema invalid) stop the chain. When every eligible attempt fails, `fx.ask` throws — map that to a typed error in the flow (no silent text excerpt).
|
|
109
|
+
|
|
110
|
+
`timeout` uses the same duration vocabulary as Clock (`"30s"`, `"2m"`, or a millisecond number). Ask-time `fx.ask(…, { timeout: "10s" })` overrides the prompt. Omit = no artificial cap. Cost caps stay on `budget`.
|
|
90
111
|
|
|
91
112
|
## Declared guardrails
|
|
92
113
|
|
|
@@ -234,7 +234,9 @@ export const relations = store.schema.relations({ links, daily }, (r) => ({
|
|
|
234
234
|
|
|
235
235
|
`oke dev` auto-pushes when you save a schema _input_ (`schema.decl.ts`,
|
|
236
236
|
hand-written `schema.ts`, `app.ts` for plugin tables, or `drizzle.config.ts`) —
|
|
237
|
-
not when emit rewrites `schema.drizzle.ts`.
|
|
237
|
+
not when emit rewrites `schema.drizzle.ts`. A definition error in
|
|
238
|
+
`schema.decl.ts` prints `schema.decl.ts has an error — …` (red ●) and keeps the
|
|
239
|
+
session running — it is not framed as a quiet skip.
|
|
238
240
|
|
|
239
241
|
Opt out with `--no-db-push` or `db: { autoPush: false }`. `prod` **never**
|
|
240
242
|
auto-applies DDL; a missing table fails as **OKE1101** (`oke db migrate`).
|
|
@@ -154,22 +154,30 @@ VaultBootError: 1 secret(s) missing:
|
|
|
154
154
|
|
|
155
155
|
## Setting and rotating values
|
|
156
156
|
|
|
157
|
-
| Command | What it does
|
|
158
|
-
| -------------------------------------------------- |
|
|
159
|
-
| `oke vault set NAME value` | Write a value to the env file (`.env.local` by default)
|
|
160
|
-
| `oke vault list` | List names present in the active store
|
|
161
|
-
| `oke vault import <file>` | Bulk-import names from a dotenv file
|
|
162
|
-
| `oke vault init` | Initialize the built-in encrypted store (prints the master key once)
|
|
163
|
-
| `oke vault status` / `seal` / `unseal` | Built-in seal lifecycle
|
|
164
|
-
| `oke vault rotate <path>` / `rotate-master` | Re-encrypt a secret, or rewrap every DEK under a new master
|
|
165
|
-
| `oke vault audit` / `audit verify` / `audit purge` | Hash-chained audit (no secret values)
|
|
166
|
-
| `oke vault purge-expired [--dry-run] [--before]` | Hard-delete secret rows past `expires_at` (operator cleanup)
|
|
167
|
-
| `oke vault backup` / `restore` | Encrypted backup
|
|
157
|
+
| Command | What it does |
|
|
158
|
+
| -------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
159
|
+
| `oke vault set NAME value` | Write a value to the env file (`.env.local` by default) |
|
|
160
|
+
| `oke vault list` | List names present in the active store |
|
|
161
|
+
| `oke vault import <file>` | Bulk-import names from a dotenv file |
|
|
162
|
+
| `oke vault init` | Initialize the built-in encrypted store (prints the master key once) |
|
|
163
|
+
| `oke vault status` / `seal` / `unseal` | Built-in seal lifecycle |
|
|
164
|
+
| `oke vault rotate <path>` / `rotate-master` | Re-encrypt a secret, or rewrap every DEK under a new master (exclusive lease) |
|
|
165
|
+
| `oke vault audit` / `audit verify` / `audit purge` | Hash-chained audit (serialized appends; no secret values) |
|
|
166
|
+
| `oke vault purge-expired [--dry-run] [--before]` | Hard-delete secret rows past `expires_at` (operator cleanup) |
|
|
167
|
+
| `oke vault backup` / `restore` | Encrypted backup (atomic write + end marker; separate backup KEK) |
|
|
168
168
|
|
|
169
169
|
Flows can write too, through the same capability gate as reads: `fx.vault.set(path, value, { ttlMs?, metadata? })`, `fx.vault.rotate(path, value)`, `fx.vault.delete(path)`, plus `fx.vault.list(prefix?)` and `fx.vault.status()`. These need the encrypted-at-rest backend (`drivers.vault = "vault"`) and throw without it; `fx.vault.get` works on every app.
|
|
170
170
|
|
|
171
171
|
The **Console** (`:6533`) can also set and rotate values, but it is **write-only**: it shows a salted fingerprint (`sha256:…`) per secret, never the cleartext. When you rotate a key there, the panel shows the **blast radius** — which in-flight durable runs will wake up with the new value.
|
|
172
172
|
|
|
173
|
+
<Callout title="Builtin concurrency (rotate / audit / backup)">
|
|
174
|
+
`rotate-master` uses the same SKIP LOCKED + lease-expiry physics as Clock and Signal: one holder
|
|
175
|
+
at a time; a crashed holder is reclaimed after the lease TTL. Audit rows append under a
|
|
176
|
+
transactional row lock so the hash chain stays verifiable under concurrent writers. Backups are
|
|
177
|
+
written via temp → fsync → rename and carry an end marker + checksum — restore rejects incomplete
|
|
178
|
+
files before decrypt.
|
|
179
|
+
</Callout>
|
|
180
|
+
|
|
173
181
|
<Callout title="Builtin master keys and expired rows">
|
|
174
182
|
Resolve the master key with `--key -` (stdin), `OKE_VAULT_MASTER_KEY`, or a hidden TTY prompt —
|
|
175
183
|
never put keys on argv in shared shells (history risk). Expired rows stay until `oke vault
|
|
@@ -67,7 +67,8 @@ Both are Docker-first (`dev`/`prod` share production protocols; `test` uses PGLi
|
|
|
67
67
|
memory). Both ship a minimal GitHub Actions workflow (typecheck + `bun test`).
|
|
68
68
|
|
|
69
69
|
On a TTY: pick a template, then **recommended defaults**, **customize**, or
|
|
70
|
-
**reuse** matching saved settings (`~/.oke/create-defaults.json`).
|
|
70
|
+
**reuse** matching saved settings (`~/.oke/create-defaults.json`). Extra locales
|
|
71
|
+
and PgDog answers are saved into that file on every TTY run (not only customize).
|
|
71
72
|
|
|
72
73
|
Customize walks Docker-first facets (including `store.index` with `none`), then
|
|
73
74
|
**AI setup** (Recommended / Customize / Off). Project name rejects a non-empty
|
|
@@ -107,6 +108,11 @@ configured.**
|
|
|
107
108
|
stays in sync. Opt out with `oke dev --no-db-push` or
|
|
108
109
|
`db: { autoPush: false }` in `oke.config.ts`.
|
|
109
110
|
|
|
111
|
+
A broken `schema.decl.ts` (bad table / relation definition) prints a red ●
|
|
112
|
+
`schema.decl.ts has an error — …` line and keeps the session up — it is never
|
|
113
|
+
framed as a quiet `skipped`. Environmental gaps (no `oke.config.ts` / no
|
|
114
|
+
images yet) still soft-skip.
|
|
115
|
+
|
|
110
116
|
For production, generate and apply migrations deliberately — never automatic on
|
|
111
117
|
boot:
|
|
112
118
|
|
|
@@ -45,8 +45,9 @@ OKE_AI_MODEL=granite3.3:2b
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
`OKE_AI_MODEL` is a Docker Hub `ai/<id>` model. The recipe writes
|
|
48
|
-
|
|
49
|
-
`llama-server -m <gguf> --alias <id>`
|
|
48
|
+
`.oke/llama-entrypoint.py` (generated, gitignored — not app source), which
|
|
49
|
+
Hub-pulls into the cache volume, then starts `llama-server -m <gguf> --alias <id>`
|
|
50
|
+
(single-model).
|
|
50
51
|
|
|
51
52
|
Never set `LLAMA_ARG_MODELS_PRESET` / `LLAMA_ARG_DOCKER_REPO` — those hang on
|
|
52
53
|
b10290+. `oke dev` shows the model id and polls `/v1/models` until `ready`
|
|
@@ -93,13 +94,13 @@ Postgres/Redis, stricter on the published interface because of GGUF CVE risk.
|
|
|
93
94
|
|
|
94
95
|
## What the recipe configures
|
|
95
96
|
|
|
96
|
-
| Field | Value
|
|
97
|
-
| -------------- |
|
|
98
|
-
| Container port | `8080`
|
|
99
|
-
| Host publish | `127.0.0.1:8080:8080`
|
|
100
|
-
| Healthcheck | `GET /health`, every 5s, 24 retries, 900s start
|
|
101
|
-
| Connection URL | `http://host:8080/v1`
|
|
102
|
-
| Model load |
|
|
97
|
+
| Field | Value |
|
|
98
|
+
| -------------- | ------------------------------------------------------------ |
|
|
99
|
+
| Container port | `8080` |
|
|
100
|
+
| Host publish | `127.0.0.1:8080:8080` |
|
|
101
|
+
| Healthcheck | `GET /health`, every 5s, 24 retries, 900s start |
|
|
102
|
+
| Connection URL | `http://host:8080/v1` |
|
|
103
|
+
| Model load | `.oke/llama-entrypoint.py` → Hub pull (incl. CNCF) then `-m` |
|
|
103
104
|
|
|
104
105
|
## When to choose something else
|
|
105
106
|
|
|
@@ -85,7 +85,10 @@ prints immediately (wordmark + Starting + profile), then streams background work
|
|
|
85
85
|
yellow pending/loading · red error · dim idle. Compose health keeps polling
|
|
86
86
|
(`docker compose ps -a`); AI ● tracks model phase while the AI container is up.
|
|
87
87
|
Boot does not wait for the model to become ready. A successful session writes
|
|
88
|
-
`.oke/dev.json` (pid · ports · startedAt) and clears it on stop.
|
|
88
|
+
`.oke/dev.json` (pid · ports · startedAt) and clears it on stop. Durable local
|
|
89
|
+
markers live in `.oke/state.json` (e.g. `seededAt` after the one-shot seed
|
|
90
|
+
prompt) — not in the session lock. Console session signing uses
|
|
91
|
+
`.oke/console.secret` (or `OKE_CONSOLE_SECRET`); that is not a Vault secret.
|
|
89
92
|
|
|
90
93
|
On a TTY, Ink keyboard controls stay active after boot (`useInput` — same
|
|
91
94
|
shortcuts as before). Press `r` to clear the log pane and reprint the latest
|
|
@@ -122,7 +125,8 @@ On a TTY: pick **standard** or **advanced**, then recommended defaults or
|
|
|
122
125
|
customize. Customize walks **Docker-first** facets once (including `store.index`
|
|
123
126
|
with a `none` opt-out), then **AI setup**: Recommended (llama.cpp) · Customize ·
|
|
124
127
|
Off. Writes user-global `~/.oke/create-defaults.json` (reuse only when `template`
|
|
125
|
-
matches).
|
|
128
|
+
matches). Extra locales and PgDog answers are persisted on every TTY run — not
|
|
129
|
+
only after customize. Non-TTY / `--yes` / explicit `--template` never prompt.
|
|
126
130
|
|
|
127
131
|
### Additional commands
|
|
128
132
|
|
|
@@ -128,15 +128,15 @@ provider's real credentials — credentials alone never send.
|
|
|
128
128
|
|
|
129
129
|
## Framework behavior
|
|
130
130
|
|
|
131
|
-
| Variable | Used for
|
|
132
|
-
| --------------------- |
|
|
133
|
-
| `OKE_DOCKER` | `"1"` marks Compose / `oke dev` posture (set by the CLI)
|
|
134
|
-
| `OKE_DB_AUTO_PUSH` | Overrides `db.autoPush` at boot
|
|
135
|
-
| `OKE_DRIZZLE_DIALECT` | `"postgresql"` for drizzle-kit overlays (templates hardcode it)
|
|
136
|
-
| `OKE_DEV_REQUEST_LOG` | `"1"` logs requests during `oke dev` (set by the CLI)
|
|
137
|
-
| `OKE_CONSOLE_SECRET` | Console session signing secret — set
|
|
138
|
-
| `PORT` | App port in production containers (default `6530`)
|
|
139
|
-
| `NODE_ENV` | `"production"` switches the Console to its production posture
|
|
131
|
+
| Variable | Used for |
|
|
132
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
133
|
+
| `OKE_DOCKER` | `"1"` marks Compose / `oke dev` posture (set by the CLI) |
|
|
134
|
+
| `OKE_DB_AUTO_PUSH` | Overrides `db.autoPush` at boot |
|
|
135
|
+
| `OKE_DRIZZLE_DIALECT` | `"postgresql"` for drizzle-kit overlays (templates hardcode it) |
|
|
136
|
+
| `OKE_DEV_REQUEST_LOG` | `"1"` logs requests during `oke dev` (set by the CLI) |
|
|
137
|
+
| `OKE_CONSOLE_SECRET` | Console operator-session signing secret (HMAC) — set in production; else `.oke/console.secret`. Not a Vault contract. |
|
|
138
|
+
| `PORT` | App port in production containers (default `6530`) |
|
|
139
|
+
| `NODE_ENV` | `"production"` switches the Console to its production posture |
|
|
140
140
|
|
|
141
141
|
Console operator rows are **not** stored in `.oke/console.sqlite`. With `DATABASE_URL` (or
|
|
142
142
|
`OKE_STORE_SQL_URL`) they live in Postgres schema `oke_console`. Without a Postgres URL,
|
|
@@ -90,7 +90,7 @@ describe("apply", () => {
|
|
|
90
90
|
expect(upsertEnv(env, "OPENAI_API_KEY", "sk-test")).toContain("OPENAI_API_KEY=sk-test");
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
test("renderAiTs includes vision + embed", () => {
|
|
93
|
+
test("renderAiTs includes vision + embed + summarize-note + local", () => {
|
|
94
94
|
const ts = renderAiTs({
|
|
95
95
|
driver: "ollama",
|
|
96
96
|
chatModel: "gemma4:e4b",
|
|
@@ -98,8 +98,10 @@ describe("apply", () => {
|
|
|
98
98
|
embedModel: "nomic-embed-text",
|
|
99
99
|
});
|
|
100
100
|
expect(ts).toContain('ai.model("smart"');
|
|
101
|
+
expect(ts).toContain('ai.model("local"');
|
|
101
102
|
expect(ts).toContain('ai.model("vision"');
|
|
102
103
|
expect(ts).toContain("docsEmbed");
|
|
104
|
+
expect(ts).toContain('smart.prompt("summarize-note"');
|
|
103
105
|
});
|
|
104
106
|
|
|
105
107
|
test("applyAiSetup writes config, env, AI models in core.ts", () => {
|
|
@@ -208,9 +208,27 @@ export function renderAiTs(input: AiSetupApplyInput): string {
|
|
|
208
208
|
const lines = [
|
|
209
209
|
`import { ai } from "okengine";`,
|
|
210
210
|
``,
|
|
211
|
+
`/** Cloud OpenAI-compatible binding (OpenAI / Groq / OpenRouter / …). */`,
|
|
211
212
|
`export const smart = ai.model("smart", {`,
|
|
212
213
|
` provider: "${provider}",`,
|
|
213
|
-
` model: process.env.OKE_AI_MODEL ?? "${chat}",`,
|
|
214
|
+
` model: process.env.OKE_AI_CLOUD_MODEL ?? process.env.OKE_AI_MODEL ?? "${chat}",`,
|
|
215
|
+
` baseUrl: process.env.OPENAI_BASE_URL?.trim() || "https://api.openai.com/v1",`,
|
|
216
|
+
` ...(process.env.OPENAI_API_KEY?.trim()`,
|
|
217
|
+
` ? { apiKey: process.env.OPENAI_API_KEY.trim() }`,
|
|
218
|
+
` : {}),`,
|
|
219
|
+
`});`,
|
|
220
|
+
``,
|
|
221
|
+
`/** Local inference binding (docker llama.cpp / Ollama via \`OKE_AI_URL\`). */`,
|
|
222
|
+
`export const local = ai.model("local", {`,
|
|
223
|
+
` provider: "${provider === "ollama" ? "ollama" : "openai-compatible"}",`,
|
|
224
|
+
` model: process.env.OKE_AI_LOCAL_MODEL ?? "${chat}",`,
|
|
225
|
+
` ...(process.env.OKE_AI_URL?.trim() ? { baseUrl: process.env.OKE_AI_URL.trim() } : {}),`,
|
|
226
|
+
`});`,
|
|
227
|
+
``,
|
|
228
|
+
`/** Advanced Notes summarize — used by \`notes.summarize\` via \`fx.ask\`. */`,
|
|
229
|
+
`export const summarizeNote = smart.prompt("summarize-note", {`,
|
|
230
|
+
` via: ["smart", "local"],`,
|
|
231
|
+
` timeout: "30s",`,
|
|
214
232
|
`});`,
|
|
215
233
|
];
|
|
216
234
|
|
|
@@ -265,6 +283,10 @@ function writeAiModels(cwd: string, input: AiSetupApplyInput): string {
|
|
|
265
283
|
if (existsSync(coreTsPath)) {
|
|
266
284
|
const existing = readFileSync(coreTsPath, "utf8");
|
|
267
285
|
if (hasAiModels(existing)) {
|
|
286
|
+
const withPrompt = ensureSummarizeNotePrompt(existing);
|
|
287
|
+
if (withPrompt !== existing) {
|
|
288
|
+
writeFileSync(coreTsPath, withPrompt, "utf8");
|
|
289
|
+
}
|
|
268
290
|
return coreTsPath;
|
|
269
291
|
}
|
|
270
292
|
writeFileSync(coreTsPath, mergeAiIntoCore(existing, rendered), "utf8");
|
|
@@ -286,6 +308,44 @@ function hasAiModels(source: string): boolean {
|
|
|
286
308
|
);
|
|
287
309
|
}
|
|
288
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Append the advanced Notes `summarize-note` prompt when a `smart` model
|
|
313
|
+
* exists but the prompt was never declared (common after older `--ai` runs).
|
|
314
|
+
*
|
|
315
|
+
* @param source - Existing `src/core.ts` (or AI sidecar) source
|
|
316
|
+
*/
|
|
317
|
+
export function ensureSummarizeNotePrompt(source: string): string {
|
|
318
|
+
let next = source;
|
|
319
|
+
if (
|
|
320
|
+
!/\bai\.model\s*\(\s*["']local["']/.test(next) &&
|
|
321
|
+
/\bai\.model\s*\(\s*["']smart["']/.test(next)
|
|
322
|
+
) {
|
|
323
|
+
next = `${next.trimEnd()}
|
|
324
|
+
|
|
325
|
+
/** Local inference binding (docker llama.cpp / Ollama via \`OKE_AI_URL\`). */
|
|
326
|
+
export const local = ai.model("local", {
|
|
327
|
+
provider: "openai-compatible",
|
|
328
|
+
model: process.env.OKE_AI_LOCAL_MODEL ?? "granite3.3:2b",
|
|
329
|
+
...(process.env.OKE_AI_URL?.trim() ? { baseUrl: process.env.OKE_AI_URL.trim() } : {}),
|
|
330
|
+
});
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
if (/summarize-note/.test(next) || /summarizeNote/.test(next)) {
|
|
334
|
+
return next;
|
|
335
|
+
}
|
|
336
|
+
if (!/\bai\.model\s*\(\s*["']smart["']/.test(next)) {
|
|
337
|
+
return next;
|
|
338
|
+
}
|
|
339
|
+
const prompt = `
|
|
340
|
+
/** Advanced Notes summarize — used by \`notes.summarize\` via \`fx.ask\`. */
|
|
341
|
+
export const summarizeNote = smart.prompt("summarize-note", {
|
|
342
|
+
via: ["smart", "local"],
|
|
343
|
+
timeout: "30s",
|
|
344
|
+
});
|
|
345
|
+
`;
|
|
346
|
+
return `${next.trimEnd()}\n${prompt}\n`;
|
|
347
|
+
}
|
|
348
|
+
|
|
289
349
|
/**
|
|
290
350
|
* Merge rendered AI module into an existing `src/core.ts`.
|
|
291
351
|
*
|
package/src/cli/ask-seed.test.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from "node:
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import { maybeAskSeed
|
|
9
|
+
import { maybeAskSeed } from "./ask-seed.ts";
|
|
10
|
+
import { PROJECT_STATE_REL } from "./project-state.ts";
|
|
10
11
|
|
|
11
12
|
describe("maybeAskSeed", () => {
|
|
12
13
|
test("skips when non-TTY", async () => {
|
|
@@ -31,7 +32,7 @@ describe("maybeAskSeed", () => {
|
|
|
31
32
|
}
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
test("asks once then writes
|
|
35
|
+
test("asks once then writes .oke/state.json seededAt", async () => {
|
|
35
36
|
const dir = mkdtempSync(join(tmpdir(), "ask-seed-"));
|
|
36
37
|
try {
|
|
37
38
|
mkdirSync(join(dir, "src", "db", "seed"), { recursive: true });
|
|
@@ -48,7 +49,7 @@ describe("maybeAskSeed", () => {
|
|
|
48
49
|
},
|
|
49
50
|
});
|
|
50
51
|
expect(calls).toBe(1);
|
|
51
|
-
expect(existsSync(join(dir,
|
|
52
|
+
expect(existsSync(join(dir, PROJECT_STATE_REL))).toBe(true);
|
|
52
53
|
|
|
53
54
|
await maybeAskSeed({
|
|
54
55
|
cwd: dir,
|
package/src/cli/ask-seed.ts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { confirm, isCancel } from "@clack/prompts";
|
|
6
|
-
import { resolve } from "node:path";
|
|
7
6
|
import type { ConfigEnv } from "../config/index.ts";
|
|
8
7
|
import { formatCliChrome } from "../term.ts";
|
|
9
8
|
import { resolveSeedModulePath, runSeed } from "./db-seed.ts";
|
|
9
|
+
import { isProjectSeeded, markProjectSeeded } from "./project-state.ts";
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
11
|
+
/** @deprecated Use `.oke/state.json` `seededAt` via {@link isProjectSeeded}. */
|
|
12
|
+
export { LEGACY_SEEDED_MARKER as SEEDED_MARKER } from "./project-state.ts";
|
|
13
13
|
|
|
14
14
|
/** Options for {@link maybeAskSeed}. */
|
|
15
15
|
export interface AskSeedOptions {
|
|
@@ -37,8 +37,7 @@ export async function maybeAskSeed(options: AskSeedOptions): Promise<void> {
|
|
|
37
37
|
if (!tty) return;
|
|
38
38
|
|
|
39
39
|
const cwd = options.cwd;
|
|
40
|
-
|
|
41
|
-
if (await Bun.file(marker).exists()) return;
|
|
40
|
+
if (await isProjectSeeded(cwd)) return;
|
|
42
41
|
|
|
43
42
|
const seedPath = await resolveSeedModulePath(cwd);
|
|
44
43
|
if (!(await Bun.file(seedPath).exists())) return;
|
|
@@ -74,6 +73,6 @@ export async function maybeAskSeed(options: AskSeedOptions): Promise<void> {
|
|
|
74
73
|
}));
|
|
75
74
|
const code = await seed(cwd, options.env);
|
|
76
75
|
if (code === 0) {
|
|
77
|
-
await
|
|
76
|
+
await markProjectSeeded(cwd);
|
|
78
77
|
}
|
|
79
78
|
}
|
|
@@ -96,6 +96,7 @@ void seats;
|
|
|
96
96
|
}),
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
+
// `bunx tsc` under full-suite contention can exceed the default 5s budget.
|
|
99
100
|
const proc = Bun.spawn(["bunx", "tsc", "--project", tsconfig], {
|
|
100
101
|
cwd: dir,
|
|
101
102
|
stdout: "pipe",
|
|
@@ -110,7 +111,7 @@ void seats;
|
|
|
110
111
|
if (code !== 0) {
|
|
111
112
|
throw new Error(`tsc failed (${code})\n${stdout}\n${stderr}\n--- d.ts ---\n${result.source}`);
|
|
112
113
|
}
|
|
113
|
-
});
|
|
114
|
+
}, 15_000);
|
|
114
115
|
|
|
115
116
|
test("fetches /_oke/client.json from url", async () => {
|
|
116
117
|
const dir = await mkdtemp(join(tmpdir(), "oke-client-add-"));
|
package/src/cli/dev.test.ts
CHANGED
|
@@ -872,3 +872,119 @@ describe("oke dev Docker-first", () => {
|
|
|
872
872
|
}
|
|
873
873
|
});
|
|
874
874
|
});
|
|
875
|
+
|
|
876
|
+
describe("oke dev schema.decl sync framing", () => {
|
|
877
|
+
let session: DevSession | undefined;
|
|
878
|
+
|
|
879
|
+
afterEach(() => {
|
|
880
|
+
session?.stop();
|
|
881
|
+
session = undefined;
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
test("schema-definition error is a loud failure line, not a benign skip", async () => {
|
|
885
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-dev-schema-decl-err-"));
|
|
886
|
+
await mkdir(join(dir, "src/db"), { recursive: true });
|
|
887
|
+
await Bun.write(
|
|
888
|
+
join(dir, "oke.config.ts"),
|
|
889
|
+
`export default {
|
|
890
|
+
name: "schema-decl-err",
|
|
891
|
+
images: {
|
|
892
|
+
"store.sql": "postgres:16-alpine",
|
|
893
|
+
"store.kv": "redis:7-alpine",
|
|
894
|
+
},
|
|
895
|
+
drivers: {
|
|
896
|
+
store: { sql: { dev: "postgres", test: "pglite", prod: "postgres" } },
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
`,
|
|
900
|
+
);
|
|
901
|
+
await Bun.write(join(dir, "src/app.ts"), "export {}\n");
|
|
902
|
+
// Genuine definition error: `.references()` target is undefined → emit throws.
|
|
903
|
+
await Bun.write(
|
|
904
|
+
join(dir, "src/db/schema.decl.ts"),
|
|
905
|
+
`import { store, field } from ${JSON.stringify(OKE_INDEX)};
|
|
906
|
+
|
|
907
|
+
export const authors = store.schema.table("authors", {
|
|
908
|
+
id: field.text().primaryKey(),
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
export const posts = store.schema.table("posts", {
|
|
912
|
+
id: field.text().primaryKey(),
|
|
913
|
+
authorId: field.text().notNull().references(() => (authors as { missingColumn: never }).missingColumn),
|
|
914
|
+
});
|
|
915
|
+
`,
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
const writes: string[] = [];
|
|
919
|
+
const result = await runDev({
|
|
920
|
+
stdinIsTTY: false,
|
|
921
|
+
cwd: dir,
|
|
922
|
+
silentClaim: true,
|
|
923
|
+
keepAlive: false,
|
|
924
|
+
appPort: 0,
|
|
925
|
+
consolePort: 0,
|
|
926
|
+
mcpPort: 0,
|
|
927
|
+
docsMcpPort: 0,
|
|
928
|
+
...stubCompose({ noDbPush: false }),
|
|
929
|
+
startApp: async () => ({ stop() {} }),
|
|
930
|
+
regenClient: async () => {},
|
|
931
|
+
write: (t) => {
|
|
932
|
+
writes.push(t);
|
|
933
|
+
},
|
|
934
|
+
serveConsole: async () => ({ stop() {} }),
|
|
935
|
+
serveMcp: async () => ({ stop() {} }),
|
|
936
|
+
serveDocsMcp: async () => ({
|
|
937
|
+
stop() {},
|
|
938
|
+
port: 1,
|
|
939
|
+
url: new URL("http://127.0.0.1:1"),
|
|
940
|
+
}),
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
expect(result.code).toBe(0);
|
|
944
|
+
session = result.session;
|
|
945
|
+
|
|
946
|
+
const plain = writes.join("").replace(/\x1b\[[0-9;]*m/g, "");
|
|
947
|
+
expect(plain).toContain("schema.decl.ts has an error");
|
|
948
|
+
expect(plain).toMatch(/●\s*schema\.decl\.ts has an error/);
|
|
949
|
+
expect(plain).not.toContain("oke db push (dev) skipped");
|
|
950
|
+
}, 60_000);
|
|
951
|
+
|
|
952
|
+
test("benign environmental sync gap still uses skip framing", async () => {
|
|
953
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-dev-schema-skip-"));
|
|
954
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
955
|
+
// No oke.config.ts — syncDevSchema(env=dev) throws the known docker-mode skip.
|
|
956
|
+
await Bun.write(join(dir, "src/app.ts"), "export {}\n");
|
|
957
|
+
|
|
958
|
+
const writes: string[] = [];
|
|
959
|
+
const result = await runDev({
|
|
960
|
+
stdinIsTTY: false,
|
|
961
|
+
cwd: dir,
|
|
962
|
+
silentClaim: true,
|
|
963
|
+
keepAlive: false,
|
|
964
|
+
appPort: 0,
|
|
965
|
+
consolePort: 0,
|
|
966
|
+
mcpPort: 0,
|
|
967
|
+
docsMcpPort: 0,
|
|
968
|
+
...stubCompose({ noDbPush: false }),
|
|
969
|
+
startApp: async () => ({ stop() {} }),
|
|
970
|
+
regenClient: async () => {},
|
|
971
|
+
write: (t) => {
|
|
972
|
+
writes.push(t);
|
|
973
|
+
},
|
|
974
|
+
serveConsole: async () => ({ stop() {} }),
|
|
975
|
+
serveMcp: async () => ({ stop() {} }),
|
|
976
|
+
serveDocsMcp: async () => ({
|
|
977
|
+
stop() {},
|
|
978
|
+
port: 1,
|
|
979
|
+
url: new URL("http://127.0.0.1:1"),
|
|
980
|
+
}),
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
expect(result.code).toBe(0);
|
|
984
|
+
session = result.session;
|
|
985
|
+
|
|
986
|
+
const plain = writes.join("").replace(/\x1b\[[0-9;]*m/g, "");
|
|
987
|
+
expect(plain).toContain("oke db push (dev) skipped — docker mode: oke.config.ts not found");
|
|
988
|
+
expect(plain).not.toContain("schema.decl.ts has an error");
|
|
989
|
+
}, 60_000);
|
|
990
|
+
});
|