okengine 0.11.0 → 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/gate/config.ts +13 -3
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate/strategies.ts +2 -12
- 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-auth.ts +98 -0
- package/src/kernel/app.ts +120 -78
- package/src/kernel/auto-registry.test.ts +52 -1
- package/src/kernel/boot.test.ts +4 -18
- 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/release/build-lib.ts +3 -0
- package/src/shared/lazy-src.ts +79 -0
- package/src/test/create-test-app.ts +16 -11
- package/src/test/reset-element-registries.ts +17 -9
package/src/cli/dev.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
formatServiceLine,
|
|
39
39
|
formatStackSummary,
|
|
40
40
|
formatStatusLine,
|
|
41
|
+
termColorEnabled,
|
|
41
42
|
type BootProgress,
|
|
42
43
|
type DevStatus,
|
|
43
44
|
type StackSummaryService,
|
|
@@ -358,8 +359,14 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
358
359
|
});
|
|
359
360
|
try {
|
|
360
361
|
await syncAdoptBarrel(cwd);
|
|
361
|
-
} catch {
|
|
362
|
-
|
|
362
|
+
} catch (err) {
|
|
363
|
+
// Best-effort: boot-time assertAdoptBarrelFresh (rootDir opt-in) is the real gate.
|
|
364
|
+
// Still surface so a broken generator is not fully silent in the boot log.
|
|
365
|
+
write(
|
|
366
|
+
formatStatusLine(
|
|
367
|
+
`.adopt() barrel sync skipped — ${err instanceof Error ? err.message : String(err)}`,
|
|
368
|
+
),
|
|
369
|
+
);
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
const preferredApp = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
|
|
@@ -425,7 +432,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
425
432
|
};
|
|
426
433
|
if (typeof pkg.version === "string") okeVersion = pkg.version;
|
|
427
434
|
} catch {
|
|
428
|
-
// shipped
|
|
435
|
+
// Inconsequential: banner version only — shipped binaries may not sit next to package.json.
|
|
429
436
|
}
|
|
430
437
|
const earlyProfile = resolveDevProfile({ docker: true, nodeEnv: "development" });
|
|
431
438
|
write(
|
|
@@ -473,8 +480,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
473
480
|
let loadedConfig: Awaited<ReturnType<typeof loadOkeConfig>>["config"] | null = null;
|
|
474
481
|
try {
|
|
475
482
|
loadedConfig = (await loadOkeConfig(cwd)).config;
|
|
476
|
-
} catch {
|
|
483
|
+
} catch (err) {
|
|
477
484
|
loadedConfig = null;
|
|
485
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
486
|
+
// Missing config is common for injectable/`--images` trees; surface parse bugs only.
|
|
487
|
+
if (!/no oke\.config\.ts found/i.test(msg)) {
|
|
488
|
+
write(formatStatusLine(`oke.config.ts load skipped — ${msg}`));
|
|
489
|
+
}
|
|
478
490
|
}
|
|
479
491
|
|
|
480
492
|
{
|
|
@@ -496,8 +508,12 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
496
508
|
} else if (!loadedConfig) {
|
|
497
509
|
try {
|
|
498
510
|
loadedConfig = (await loadOkeConfig(cwd)).config;
|
|
499
|
-
} catch {
|
|
511
|
+
} catch (err) {
|
|
500
512
|
loadedConfig = null;
|
|
513
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
514
|
+
if (!/no oke\.config\.ts found/i.test(msg)) {
|
|
515
|
+
write(formatStatusLine(`oke.config.ts load skipped — ${msg}`));
|
|
516
|
+
}
|
|
501
517
|
}
|
|
502
518
|
}
|
|
503
519
|
if (Array.isArray(options.docker)) {
|
|
@@ -1003,11 +1019,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1003
1019
|
});
|
|
1004
1020
|
}
|
|
1005
1021
|
} catch (err) {
|
|
1006
|
-
write(
|
|
1007
|
-
formatStatusLine(
|
|
1008
|
-
`oke db push (dev) skipped — ${err instanceof Error ? err.message : String(err)}`,
|
|
1009
|
-
),
|
|
1010
|
-
);
|
|
1022
|
+
write(await formatDevSchemaSyncFailure(cwd, err, "boot"));
|
|
1011
1023
|
}
|
|
1012
1024
|
}
|
|
1013
1025
|
|
|
@@ -1054,7 +1066,8 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1054
1066
|
const next = await extractManifest({ rootDir: cwd });
|
|
1055
1067
|
feedManifest(state, next);
|
|
1056
1068
|
} catch {
|
|
1057
|
-
//
|
|
1069
|
+
// Inconsequential while watching: mid-edit parse failures are expected on every
|
|
1070
|
+
// keystroke — keep the last good Manifest. Logging here would flood the Logs pane.
|
|
1058
1071
|
}
|
|
1059
1072
|
}
|
|
1060
1073
|
|
|
@@ -1130,8 +1143,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1130
1143
|
out: resolve(cwd, "oke-client.d.ts"),
|
|
1131
1144
|
});
|
|
1132
1145
|
write(formatStatusLine("regenerated oke-client.d.ts"));
|
|
1133
|
-
} catch {
|
|
1134
|
-
// App may not be ready yet —
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
// App may not be ready yet on first tick — still surface so a stuck regen is visible.
|
|
1148
|
+
write(
|
|
1149
|
+
formatStatusLine(
|
|
1150
|
+
`oke-client.d.ts regen skipped — ${err instanceof Error ? err.message : String(err)}`,
|
|
1151
|
+
),
|
|
1152
|
+
);
|
|
1135
1153
|
}
|
|
1136
1154
|
});
|
|
1137
1155
|
|
|
@@ -1196,8 +1214,9 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1196
1214
|
write(formatStatusLine("oke db push (schema change)"));
|
|
1197
1215
|
try {
|
|
1198
1216
|
await dbPush(cwd);
|
|
1199
|
-
} catch {
|
|
1200
|
-
//
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
// Dev-loop stays up (next save retries) — but definition errors must not look like a quiet skip.
|
|
1219
|
+
write(await formatDevSchemaSyncFailure(cwd, err, "watch"));
|
|
1201
1220
|
}
|
|
1202
1221
|
});
|
|
1203
1222
|
|
|
@@ -1394,7 +1413,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1394
1413
|
// Esc / unmount without q — still tear down.
|
|
1395
1414
|
stop();
|
|
1396
1415
|
} catch {
|
|
1397
|
-
// Ink optional — fall back to keep-alive without keyboard
|
|
1416
|
+
// Inconsequential: Ink/TUI is optional chrome — fall back to keep-alive without keyboard.
|
|
1398
1417
|
await new Promise(() => {});
|
|
1399
1418
|
}
|
|
1400
1419
|
return { code: 0, plan: session.plan, session };
|
|
@@ -1420,6 +1439,75 @@ function isSparseManifest(manifest: Manifest): boolean {
|
|
|
1420
1439
|
return Object.keys(flows).length === 0;
|
|
1421
1440
|
}
|
|
1422
1441
|
|
|
1442
|
+
/**
|
|
1443
|
+
* Environmental syncDevSchema throws that are fine to soft-skip in the
|
|
1444
|
+
* compose (`dev`) boot path — no Store / no docker images yet, etc.
|
|
1445
|
+
*
|
|
1446
|
+
* @param message - Caught error message
|
|
1447
|
+
*/
|
|
1448
|
+
function isBenignDevSchemaEnvSkip(message: string): boolean {
|
|
1449
|
+
return (
|
|
1450
|
+
message.startsWith("docker mode: oke.config.ts not found") ||
|
|
1451
|
+
message.startsWith("docker mode: no images configured")
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Whether a caught sync/push error is a schema.decl definition failure
|
|
1457
|
+
* (import evaluation, emit, duplicate tables, bad `.references()`, …).
|
|
1458
|
+
*
|
|
1459
|
+
* Message text alone is insufficient — emit-time bugs often look like bare
|
|
1460
|
+
* TypeErrors (`target.tableName`) — so also inspect the stack for declare /
|
|
1461
|
+
* emit frames.
|
|
1462
|
+
*
|
|
1463
|
+
* @param err - Caught value
|
|
1464
|
+
* @param declarePath - Resolved schema.decl path for this project
|
|
1465
|
+
*/
|
|
1466
|
+
function isSchemaDeclDefinitionError(err: unknown, declarePath: string): boolean {
|
|
1467
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1468
|
+
const stack = err instanceof Error ? (err.stack ?? "") : "";
|
|
1469
|
+
const blob = `${msg}\n${stack}`;
|
|
1470
|
+
if (/failed to load schema declare|store schema:|oke schema emit:/i.test(blob)) return true;
|
|
1471
|
+
if (/schema\.decl/i.test(blob)) return true;
|
|
1472
|
+
if (/emit-drizzle|emitColumnSource|emitDrizzleSource|maybeEmitDomainSchema/i.test(blob)) {
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
return declarePath.length > 0 && blob.includes(declarePath);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Frame a schema-sync failure for the `oke dev` loop: warn loud for developer
|
|
1480
|
+
* bugs, soft-skip only known environmental gaps. Never crashes the session.
|
|
1481
|
+
*
|
|
1482
|
+
* Matches the fail-loud status style used by compose controls (`failed —` +
|
|
1483
|
+
* red ●), and ADOPT_BARREL_STALE's "never silent" posture — without turning
|
|
1484
|
+
* this path into a hard boot failure.
|
|
1485
|
+
*
|
|
1486
|
+
* @param cwd - Project root
|
|
1487
|
+
* @param err - Caught value from {@link syncDevSchema} / watch push
|
|
1488
|
+
* @param context - Boot one-shot vs schema-watch auto-push
|
|
1489
|
+
*/
|
|
1490
|
+
async function formatDevSchemaSyncFailure(
|
|
1491
|
+
cwd: string,
|
|
1492
|
+
err: unknown,
|
|
1493
|
+
context: "boot" | "watch",
|
|
1494
|
+
): Promise<string> {
|
|
1495
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1496
|
+
const color = termColorEnabled();
|
|
1497
|
+
if (isBenignDevSchemaEnvSkip(msg)) {
|
|
1498
|
+
return formatStatusLine(`oke db push (dev) skipped — ${msg}`, color);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const { resolveEmitPaths } = await import("../elements/store/emit-drizzle.ts");
|
|
1502
|
+
const { declarePath } = resolveEmitPaths(cwd);
|
|
1503
|
+
if (isSchemaDeclDefinitionError(err, declarePath)) {
|
|
1504
|
+
return formatStatusLine(`schema.decl.ts has an error — ${msg}`, color, "error");
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const label = context === "watch" ? "oke db push (schema change)" : "oke db push (dev)";
|
|
1508
|
+
return formatStatusLine(`${label} failed — ${msg}`, color, "error");
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1423
1511
|
/**
|
|
1424
1512
|
* Load Manifest — prefer AoT extract from `src/` when it has flows;
|
|
1425
1513
|
* otherwise on-disk JSON snapshots; otherwise a sparse extract.
|
|
@@ -1432,7 +1520,8 @@ async function tryLoadProjectManifest(cwd: string): Promise<Manifest | null | un
|
|
|
1432
1520
|
const { extractManifest } = await import("../compiler/extract.ts");
|
|
1433
1521
|
extracted = await extractManifest({ rootDir: cwd });
|
|
1434
1522
|
} catch {
|
|
1435
|
-
//
|
|
1523
|
+
// Inconsequential here: extract is one of two sources — fall through to on-disk JSON
|
|
1524
|
+
// snapshots. A broken source tree still boots Console with the last good Manifest file.
|
|
1436
1525
|
}
|
|
1437
1526
|
|
|
1438
1527
|
let fromDisk: Manifest | undefined;
|
|
@@ -1492,7 +1581,7 @@ async function startAppHot(
|
|
|
1492
1581
|
// open (create-oke afterEach `rmSync` otherwise races the child exit).
|
|
1493
1582
|
proc.kill("SIGKILL");
|
|
1494
1583
|
} catch {
|
|
1495
|
-
// already exited
|
|
1584
|
+
// Inconsequential: process already exited — kill is idempotent best-effort.
|
|
1496
1585
|
}
|
|
1497
1586
|
void unlink(readyPath).catch(() => {});
|
|
1498
1587
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for durable `.oke/state.json` project markers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
isProjectSeeded,
|
|
11
|
+
LEGACY_SEEDED_MARKER,
|
|
12
|
+
markProjectSeeded,
|
|
13
|
+
PROJECT_STATE_REL,
|
|
14
|
+
readProjectState,
|
|
15
|
+
} from "./project-state.ts";
|
|
16
|
+
|
|
17
|
+
describe("project-state", () => {
|
|
18
|
+
test("markProjectSeeded writes seededAt under .oke/state.json", async () => {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-state-"));
|
|
20
|
+
try {
|
|
21
|
+
expect(await isProjectSeeded(dir)).toBe(false);
|
|
22
|
+
await markProjectSeeded(dir, "2026-01-02T03:04:05.000Z");
|
|
23
|
+
expect(await isProjectSeeded(dir)).toBe(true);
|
|
24
|
+
const state = await readProjectState(dir);
|
|
25
|
+
expect(state.seededAt).toBe("2026-01-02T03:04:05.000Z");
|
|
26
|
+
expect(existsSync(join(dir, PROJECT_STATE_REL))).toBe(true);
|
|
27
|
+
expect(existsSync(join(dir, LEGACY_SEEDED_MARKER))).toBe(false);
|
|
28
|
+
} finally {
|
|
29
|
+
rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("migrates legacy .oke/seeded into state.json and removes marker", async () => {
|
|
34
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-state-"));
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(join(dir, ".oke"), { recursive: true });
|
|
37
|
+
writeFileSync(join(dir, LEGACY_SEEDED_MARKER), "2025-12-01T00:00:00.000Z\n");
|
|
38
|
+
const state = await readProjectState(dir);
|
|
39
|
+
expect(state.seededAt).toBe("2025-12-01T00:00:00.000Z");
|
|
40
|
+
expect(existsSync(join(dir, PROJECT_STATE_REL))).toBe(true);
|
|
41
|
+
expect(existsSync(join(dir, LEGACY_SEEDED_MARKER))).toBe(false);
|
|
42
|
+
const raw = JSON.parse(readFileSync(join(dir, PROJECT_STATE_REL), "utf8")) as {
|
|
43
|
+
seededAt?: string;
|
|
44
|
+
};
|
|
45
|
+
expect(raw.seededAt).toBe("2025-12-01T00:00:00.000Z");
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(dir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.oke/state.json` — durable local project markers (survive across `oke dev`
|
|
3
|
+
* sessions). Distinct from `.oke/dev.json` (live session lock) and
|
|
4
|
+
* `.oke/console.secret` (Console session signing key).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdir, unlink } from "node:fs/promises";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** Relative path under the project root. */
|
|
11
|
+
export const PROJECT_STATE_REL = ".oke/state.json";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Legacy one-shot seed marker (pre-state.json). Migrated on read then removed.
|
|
15
|
+
* @deprecated Prefer {@link PROJECT_STATE_REL} `seededAt`.
|
|
16
|
+
*/
|
|
17
|
+
export const LEGACY_SEEDED_MARKER = ".oke/seeded";
|
|
18
|
+
|
|
19
|
+
/** Durable local project state (gitignored under `.oke/`). */
|
|
20
|
+
export type ProjectState = {
|
|
21
|
+
/** ISO timestamp when prompted `oke db seed` last succeeded. */
|
|
22
|
+
readonly seededAt?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Absolute path to `.oke/state.json`.
|
|
27
|
+
*
|
|
28
|
+
* @param cwd - Project root
|
|
29
|
+
*/
|
|
30
|
+
export function projectStatePath(cwd: string): string {
|
|
31
|
+
return resolve(cwd, PROJECT_STATE_REL);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse and validate a state object from JSON.
|
|
36
|
+
*
|
|
37
|
+
* @param raw - Unknown JSON value
|
|
38
|
+
*/
|
|
39
|
+
export function parseProjectState(raw: unknown): ProjectState {
|
|
40
|
+
if (raw === null || typeof raw !== "object") return {};
|
|
41
|
+
const o = raw as Record<string, unknown>;
|
|
42
|
+
const seededAt = o["seededAt"];
|
|
43
|
+
if (typeof seededAt === "string" && seededAt.trim().length > 0) {
|
|
44
|
+
return { seededAt: seededAt.trim() };
|
|
45
|
+
}
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read project state, migrating a legacy `.oke/seeded` marker when present.
|
|
51
|
+
*
|
|
52
|
+
* @param cwd - Project root
|
|
53
|
+
*/
|
|
54
|
+
export async function readProjectState(cwd: string): Promise<ProjectState> {
|
|
55
|
+
const path = projectStatePath(cwd);
|
|
56
|
+
let state: ProjectState = {};
|
|
57
|
+
if (await Bun.file(path).exists()) {
|
|
58
|
+
try {
|
|
59
|
+
state = parseProjectState(await Bun.file(path).json());
|
|
60
|
+
} catch {
|
|
61
|
+
state = {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (state.seededAt) return state;
|
|
66
|
+
|
|
67
|
+
const legacy = resolve(cwd, LEGACY_SEEDED_MARKER);
|
|
68
|
+
if (!(await Bun.file(legacy).exists())) return state;
|
|
69
|
+
|
|
70
|
+
let seededAt = new Date().toISOString();
|
|
71
|
+
try {
|
|
72
|
+
const text = (await Bun.file(legacy).text()).trim();
|
|
73
|
+
if (text.length > 0) seededAt = text.split(/\r?\n/, 1)[0]!.trim() || seededAt;
|
|
74
|
+
} catch {
|
|
75
|
+
// keep generated timestamp
|
|
76
|
+
}
|
|
77
|
+
const migrated: ProjectState = { ...state, seededAt };
|
|
78
|
+
await writeProjectState(cwd, migrated);
|
|
79
|
+
try {
|
|
80
|
+
await unlink(legacy);
|
|
81
|
+
} catch {
|
|
82
|
+
// best-effort
|
|
83
|
+
}
|
|
84
|
+
return migrated;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Write project state (creates `.oke/` as needed).
|
|
89
|
+
*
|
|
90
|
+
* @param cwd - Project root
|
|
91
|
+
* @param state - Full state payload
|
|
92
|
+
*/
|
|
93
|
+
export async function writeProjectState(cwd: string, state: ProjectState): Promise<void> {
|
|
94
|
+
const path = projectStatePath(cwd);
|
|
95
|
+
await mkdir(dirname(path), { recursive: true });
|
|
96
|
+
const body: Record<string, string> = {};
|
|
97
|
+
if (state.seededAt) body.seededAt = state.seededAt;
|
|
98
|
+
await Bun.write(path, `${JSON.stringify(body, null, 2)}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether prompted seed already completed for this project.
|
|
103
|
+
*
|
|
104
|
+
* @param cwd - Project root
|
|
105
|
+
*/
|
|
106
|
+
export async function isProjectSeeded(cwd: string): Promise<boolean> {
|
|
107
|
+
const state = await readProjectState(cwd);
|
|
108
|
+
return typeof state.seededAt === "string" && state.seededAt.length > 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Record a successful prompted seed (ISO timestamp).
|
|
113
|
+
*
|
|
114
|
+
* @param cwd - Project root
|
|
115
|
+
* @param at - Optional timestamp (defaults to now)
|
|
116
|
+
*/
|
|
117
|
+
export async function markProjectSeeded(
|
|
118
|
+
cwd: string,
|
|
119
|
+
at: string = new Date().toISOString(),
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const prev = await readProjectState(cwd);
|
|
122
|
+
await writeProjectState(cwd, { ...prev, seededAt: at });
|
|
123
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* `oke vault` builtin loop — init · status · seal/unseal · rotate · audit ·
|
|
3
3
|
* backup/restore.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Logic-only cases (init / status seal-state) use the in-memory Vault SQL
|
|
6
|
+
* fake so they do not pay PGlite WASM cold-start. Dialect-heavy cases
|
|
7
|
+
* (rotate, backup, audit purge, …) share one warmed file-scoped PGlite
|
|
8
|
+
* (cold WASM once; {@link resetVaultTables} between tests).
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
12
|
import { mkdtemp } from "node:fs/promises";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
14
|
import { join } from "node:path";
|
|
@@ -18,8 +19,20 @@ import {
|
|
|
18
19
|
sqlConnectionAsExec,
|
|
19
20
|
type BuiltinVaultAdapter,
|
|
20
21
|
} from "../elements/vault/builtin-adapter.ts";
|
|
22
|
+
import { createMemoryVaultSql, resetVaultTables } from "../elements/vault/test-helpers.ts";
|
|
21
23
|
import { vaultCli } from "./vault-cmd.ts";
|
|
22
24
|
|
|
25
|
+
/** File-scoped warmed PGlite for dialect-heavy CLI cases. */
|
|
26
|
+
let sharedPglite: SqlConnection;
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
sharedPglite = await connectPglite({ url: "memory://vault-cli-shared" });
|
|
30
|
+
}, 15_000);
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await sharedPglite.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
23
36
|
/** A CLI bound to one in-memory vault, plus its captured stdout. */
|
|
24
37
|
interface Harness {
|
|
25
38
|
readonly sql: SqlConnection;
|
|
@@ -31,13 +44,28 @@ interface Harness {
|
|
|
31
44
|
close(): Promise<void>;
|
|
32
45
|
}
|
|
33
46
|
|
|
47
|
+
/** Options for {@link harness}. */
|
|
48
|
+
interface HarnessOptions {
|
|
49
|
+
/** Project root for relative file arguments. */
|
|
50
|
+
readonly cwd?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Pre-opened SQL. Defaults to the file-scoped warmed PGlite. Pass
|
|
53
|
+
* {@link createMemoryVaultSql} for logic-only cases that must not pay
|
|
54
|
+
* WASM cold-start.
|
|
55
|
+
*/
|
|
56
|
+
readonly sql?: SqlConnection;
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
/**
|
|
35
|
-
* Open a CLI harness over
|
|
60
|
+
* Open a CLI harness over an injected or shared PGlite connection.
|
|
36
61
|
*
|
|
37
|
-
* @param
|
|
62
|
+
* @param options - Cwd / SQL backend
|
|
38
63
|
*/
|
|
39
|
-
async function harness(
|
|
40
|
-
const
|
|
64
|
+
async function harness(options: HarnessOptions = {}): Promise<Harness> {
|
|
65
|
+
const injected = options.sql !== undefined;
|
|
66
|
+
if (!injected) await resetVaultTables(sharedPglite);
|
|
67
|
+
const sql = options.sql ?? sharedPglite;
|
|
68
|
+
const cwd = options.cwd;
|
|
41
69
|
let captured = "";
|
|
42
70
|
return {
|
|
43
71
|
sql,
|
|
@@ -55,7 +83,7 @@ async function harness(cwd?: string): Promise<Harness> {
|
|
|
55
83
|
reset: () => {
|
|
56
84
|
captured = "";
|
|
57
85
|
},
|
|
58
|
-
close: () => sql.close(),
|
|
86
|
+
close: () => (injected ? sql.close() : Promise.resolve()),
|
|
59
87
|
};
|
|
60
88
|
}
|
|
61
89
|
|
|
@@ -84,7 +112,8 @@ async function attach(sql: SqlConnection, masterKey: string): Promise<BuiltinVau
|
|
|
84
112
|
|
|
85
113
|
describe("oke vault — builtin lifecycle", () => {
|
|
86
114
|
test("init prints the master key once and status reflects seal state", async () => {
|
|
87
|
-
|
|
115
|
+
// Logic-only: seal-state + init CLI copy — no Postgres dialect under test.
|
|
116
|
+
const h = await harness({ sql: createMemoryVaultSql() });
|
|
88
117
|
try {
|
|
89
118
|
// A never-initialized vault reports rather than crashes.
|
|
90
119
|
expect(await h.run(["status"])).toBe(0);
|
|
@@ -121,7 +150,7 @@ describe("oke vault — builtin lifecycle", () => {
|
|
|
121
150
|
});
|
|
122
151
|
|
|
123
152
|
test("unseal requires a key and seal records the transition", async () => {
|
|
124
|
-
const h = await harness();
|
|
153
|
+
const h = await harness({});
|
|
125
154
|
try {
|
|
126
155
|
await h.run(["init"]);
|
|
127
156
|
const masterKey = masterKeyFrom(h.output());
|
|
@@ -149,7 +178,7 @@ describe("oke vault — builtin lifecycle", () => {
|
|
|
149
178
|
|
|
150
179
|
describe("oke vault rotate", () => {
|
|
151
180
|
test("rotate re-encrypts the current value under a new version", async () => {
|
|
152
|
-
const h = await harness();
|
|
181
|
+
const h = await harness({});
|
|
153
182
|
try {
|
|
154
183
|
await h.run(["init"]);
|
|
155
184
|
const masterKey = masterKeyFrom(h.output());
|
|
@@ -179,7 +208,7 @@ describe("oke vault rotate", () => {
|
|
|
179
208
|
});
|
|
180
209
|
|
|
181
210
|
test("rotate-master issues a new key and retires the old one", async () => {
|
|
182
|
-
const h = await harness();
|
|
211
|
+
const h = await harness({});
|
|
183
212
|
try {
|
|
184
213
|
await h.run(["init"]);
|
|
185
214
|
const first = masterKeyFrom(h.output());
|
|
@@ -211,7 +240,7 @@ describe("oke vault rotate", () => {
|
|
|
211
240
|
|
|
212
241
|
describe("oke vault audit", () => {
|
|
213
242
|
test("lists rows, verifies the chain, and purges by date", async () => {
|
|
214
|
-
const h = await harness();
|
|
243
|
+
const h = await harness({});
|
|
215
244
|
try {
|
|
216
245
|
await h.run(["init"]);
|
|
217
246
|
const masterKey = masterKeyFrom(h.output());
|
|
@@ -256,7 +285,7 @@ describe("oke vault audit", () => {
|
|
|
256
285
|
describe("oke vault backup / restore", () => {
|
|
257
286
|
test("round-trips every live secret through an encrypted bundle", async () => {
|
|
258
287
|
const dir = await mkdtemp(join(tmpdir(), "oke-vault-cli-"));
|
|
259
|
-
const h = await harness(dir);
|
|
288
|
+
const h = await harness({ cwd: dir });
|
|
260
289
|
try {
|
|
261
290
|
await h.run(["init"]);
|
|
262
291
|
const masterKey = masterKeyFrom(h.output());
|
|
@@ -319,7 +348,7 @@ describe("oke vault help", () => {
|
|
|
319
348
|
|
|
320
349
|
describe("oke vault secure master-key input", () => {
|
|
321
350
|
test("unseal accepts --key - via injected stdin", async () => {
|
|
322
|
-
const h = await harness();
|
|
351
|
+
const h = await harness({});
|
|
323
352
|
try {
|
|
324
353
|
await h.run(["init"]);
|
|
325
354
|
const masterKey = masterKeyFrom(h.output());
|
|
@@ -340,7 +369,7 @@ describe("oke vault secure master-key input", () => {
|
|
|
340
369
|
});
|
|
341
370
|
|
|
342
371
|
test("unseal accepts an injected interactive prompt", async () => {
|
|
343
|
-
const h = await harness();
|
|
372
|
+
const h = await harness({});
|
|
344
373
|
try {
|
|
345
374
|
await h.run(["init"]);
|
|
346
375
|
const masterKey = masterKeyFrom(h.output());
|
|
@@ -367,7 +396,7 @@ describe("oke vault secure master-key input", () => {
|
|
|
367
396
|
|
|
368
397
|
describe("oke vault purge-expired", () => {
|
|
369
398
|
test("dry-run counts without deleting; live purge removes expired rows", async () => {
|
|
370
|
-
const h = await harness();
|
|
399
|
+
const h = await harness({});
|
|
371
400
|
try {
|
|
372
401
|
await h.run(["init"]);
|
|
373
402
|
const masterKey = masterKeyFrom(h.output());
|
package/src/cli/vault-cmd.ts
CHANGED
|
@@ -755,7 +755,8 @@ async function runVaultBackup(
|
|
|
755
755
|
return 1;
|
|
756
756
|
}
|
|
757
757
|
const blob = await withVault(options, flags, key, (a) => a.exportBackup());
|
|
758
|
-
await
|
|
758
|
+
const { writeBackupFileAtomic } = await import("../elements/vault/builtin-adapter.ts");
|
|
759
|
+
await writeBackupFileAtomic(resolve(cwd, file), blob);
|
|
759
760
|
write(`oke vault: wrote ${blob.byteLength} encrypted byte(s) to ${file}\n`);
|
|
760
761
|
return 0;
|
|
761
762
|
}
|
package/src/compiler/extract.ts
CHANGED
|
@@ -803,7 +803,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
803
803
|
}
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
-
// model.prompt("ticket-triage", { version, evals, budget })
|
|
806
|
+
// model.prompt("ticket-triage", { version, evals, budget, via, timeout })
|
|
807
807
|
if (prop === "prompt") {
|
|
808
808
|
const promptName = stringArg(call.arguments[0]);
|
|
809
809
|
const opts = objectArg(call.arguments[1]);
|
|
@@ -811,6 +811,10 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
811
811
|
const version = numberProp(opts, "version");
|
|
812
812
|
const evals = stringProp(opts, "evals");
|
|
813
813
|
const budgetObj = objectProp(opts, "budget");
|
|
814
|
+
const via = stringArrayProp(opts, "via");
|
|
815
|
+
const timeoutStr = stringProp(opts, "timeout");
|
|
816
|
+
const timeoutNum = numberProp(opts, "timeout");
|
|
817
|
+
const timeout = timeoutStr ?? timeoutNum;
|
|
814
818
|
const prompt: AiPrompt = {
|
|
815
819
|
...(version !== undefined ? { version } : {}),
|
|
816
820
|
...(evals ? { evals } : {}),
|
|
@@ -822,9 +826,16 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
822
826
|
maxCostPerCall: numberProp(budgetObj, "maxCostPerCall"),
|
|
823
827
|
}
|
|
824
828
|
: {}),
|
|
829
|
+
...(numberProp(budgetObj, "maxCostPerRun") !== undefined
|
|
830
|
+
? {
|
|
831
|
+
maxCostPerRun: numberProp(budgetObj, "maxCostPerRun"),
|
|
832
|
+
}
|
|
833
|
+
: {}),
|
|
825
834
|
},
|
|
826
835
|
}
|
|
827
836
|
: {}),
|
|
837
|
+
...(via && via.length > 0 ? { via } : {}),
|
|
838
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
828
839
|
};
|
|
829
840
|
scope.ai.prompts = scope.ai.prompts ?? {};
|
|
830
841
|
scope.ai.prompts[promptName] = prompt;
|
|
@@ -323,11 +323,13 @@ describe("console serve security", () => {
|
|
|
323
323
|
secret: "serve-secret",
|
|
324
324
|
silentClaim: true,
|
|
325
325
|
env: "test",
|
|
326
|
+
// Security suite is hermetic — do not inherit a host DATABASE_URL.
|
|
327
|
+
persist: false,
|
|
326
328
|
});
|
|
327
329
|
});
|
|
328
330
|
|
|
329
331
|
afterAll(() => {
|
|
330
|
-
server
|
|
332
|
+
server?.stop(true);
|
|
331
333
|
});
|
|
332
334
|
|
|
333
335
|
test("rejects unexpected Host", async () => {
|