okengine 0.6.0 → 0.7.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/AGENTS.md +2 -2
- package/README.md +98 -71
- package/package.json +4 -4
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +90 -20
- package/site/content/docs/elements/clock.mdx +56 -24
- package/site/content/docs/elements/flow.mdx +2 -0
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +464 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +49 -13
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/reference/cli.md +30 -2
- package/site/content/docs/reference/configuration.mdx +18 -17
- package/site/content/docs/reference/environment-variables.mdx +44 -18
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- package/src/cli/dev-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.ts +4 -4
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/openbao-restart.integration.test.ts +106 -97
- package/src/cli/registry.ts +79 -1
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +1 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/dockerfile.integration.test.ts +126 -119
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/docker/stack.integration.test.ts +118 -102
- package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
- package/src/drivers/ai-ollama.integration.test.ts +3 -19
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-fcm.ts +49 -53
- package/src/drivers/channel-msegat.ts +61 -0
- package/src/drivers/channel-sently-map.ts +57 -0
- package/src/drivers/channel-sently.test.ts +99 -0
- package/src/drivers/channel-sndr.ts +28 -0
- package/src/drivers/channel-taqnyat.ts +57 -0
- package/src/drivers/channel-types.ts +79 -2
- package/src/drivers/channel-unifonic.ts +26 -43
- package/src/drivers/channel-wa-cloud.ts +33 -47
- package/src/drivers/channel-webpush.ts +39 -239
- package/src/drivers/index.ts +7 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/channel/costs.test.ts +2 -2
- package/src/elements/channel/costs.ts +14 -2
- package/src/elements/channel/mime.ts +11 -0
- package/src/elements/channel/runtime.ts +94 -0
- package/src/elements/channel/sndr-webhooks.test.ts +26 -0
- package/src/elements/channel.ts +10 -1
- package/src/elements/clock/chaos-child.ts +150 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +11 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +1 -0
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/index.ts +9 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- package/src/elements/store.ts +24 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/kernel/app.ts +3 -1
- package/src/kernel/boot-bind/channel.test.ts +68 -3
- package/src/kernel/boot-bind/channel.ts +93 -2
- package/src/kernel/boot-bind/clock.ts +52 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +247 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.ts +10 -2
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +13 -0
- package/src/kernel/fx.ts +27 -4
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
- package/src/release/exports.test.ts +26 -0
- package/src/release/exports.ts +64 -5
- package/src/release/index.ts +5 -0
- package/src/release/measure.exports.test.ts +13 -1
- package/src/release/measure.ts +76 -13
- package/src/release/official-plugins.ts +46 -0
- package/src/release/readme.test.ts +30 -2
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover and tear down leftover `oke-dev-<id>` compose stacks.
|
|
3
|
+
*
|
|
4
|
+
* Identity matches {@link stackAppSlug}: compose project `oke-dev-<6hex>`.
|
|
5
|
+
* Cleanup uses `docker compose -p <project> down -v` (no compose files required).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { stackAppSlug } from "./stack-id.ts";
|
|
10
|
+
|
|
11
|
+
/** Compose project names created by `oke dev --docker`. */
|
|
12
|
+
export const OKE_DEV_PROJECT_RE = /^oke-dev-[0-9a-f]{6}$/;
|
|
13
|
+
|
|
14
|
+
/** Result of one `docker …` invocation. */
|
|
15
|
+
export interface DockerRunResult {
|
|
16
|
+
readonly stdout: string;
|
|
17
|
+
readonly stderr: string;
|
|
18
|
+
readonly exitCode: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Injectable docker CLI runner (tests). */
|
|
22
|
+
export type DockerRunner = (args: readonly string[]) => Promise<DockerRunResult>;
|
|
23
|
+
|
|
24
|
+
/** One container belonging to an OKE compose project. */
|
|
25
|
+
export interface OkeContainer {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly service: string;
|
|
29
|
+
readonly state: string;
|
|
30
|
+
readonly project: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One compose project with zero or more containers. */
|
|
34
|
+
export interface OkeStack {
|
|
35
|
+
readonly project: string;
|
|
36
|
+
readonly containers: readonly OkeContainer[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compose project name for a project root (`oke-dev-<id>`).
|
|
41
|
+
*
|
|
42
|
+
* @param cwd - Project root
|
|
43
|
+
*/
|
|
44
|
+
export function composeProjectName(cwd: string): string {
|
|
45
|
+
return `oke-${stackAppSlug(cwd)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a compose project name is an OKE local stack.
|
|
50
|
+
*
|
|
51
|
+
* @param name - Compose project name
|
|
52
|
+
*/
|
|
53
|
+
export function isOkeDevProject(name: string): boolean {
|
|
54
|
+
return OKE_DEV_PROJECT_RE.test(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Unique compose projects implied by a container selection.
|
|
59
|
+
*
|
|
60
|
+
* @param containers - Selected containers
|
|
61
|
+
*/
|
|
62
|
+
export function projectsFromContainerSelection(
|
|
63
|
+
containers: readonly OkeContainer[],
|
|
64
|
+
): readonly string[] {
|
|
65
|
+
return [...new Set(containers.map((c) => c.project))].sort();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* True when cwd looks like an okengine app root.
|
|
70
|
+
*
|
|
71
|
+
* @param cwd - Directory to probe
|
|
72
|
+
*/
|
|
73
|
+
export async function isOkeProjectRoot(cwd: string): Promise<boolean> {
|
|
74
|
+
for (const name of ["oke.config.ts", "oke.config.mts", "oke.config.js"] as const) {
|
|
75
|
+
if (await Bun.file(join(cwd, name)).exists()) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default runner — `docker` on PATH.
|
|
82
|
+
*
|
|
83
|
+
* @param args - Args after `docker`
|
|
84
|
+
*/
|
|
85
|
+
export async function defaultDockerRunner(args: readonly string[]): Promise<DockerRunResult> {
|
|
86
|
+
const proc = Bun.spawn(["docker", ...args], {
|
|
87
|
+
stdout: "pipe",
|
|
88
|
+
stderr: "pipe",
|
|
89
|
+
});
|
|
90
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
91
|
+
new Response(proc.stdout).text(),
|
|
92
|
+
new Response(proc.stderr).text(),
|
|
93
|
+
proc.exited,
|
|
94
|
+
]);
|
|
95
|
+
return { stdout, stderr, exitCode };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Discover every `oke-dev-*` compose project on this machine.
|
|
100
|
+
*
|
|
101
|
+
* Sources: `compose ls`, container labels, volume/network name prefixes.
|
|
102
|
+
*
|
|
103
|
+
* @param run - Docker runner
|
|
104
|
+
*/
|
|
105
|
+
export async function listOkeComposeProjects(
|
|
106
|
+
run: DockerRunner = defaultDockerRunner,
|
|
107
|
+
): Promise<readonly OkeStack[]> {
|
|
108
|
+
const projectNames = new Set<string>();
|
|
109
|
+
|
|
110
|
+
for (const name of await listComposeProjectNames(run)) {
|
|
111
|
+
if (isOkeDevProject(name)) projectNames.add(name);
|
|
112
|
+
}
|
|
113
|
+
for (const c of await listAllOkeContainers(run)) {
|
|
114
|
+
projectNames.add(c.project);
|
|
115
|
+
}
|
|
116
|
+
for (const name of await listOkeResourceProjectNames(run, "volume")) {
|
|
117
|
+
projectNames.add(name);
|
|
118
|
+
}
|
|
119
|
+
for (const name of await listOkeResourceProjectNames(run, "network")) {
|
|
120
|
+
projectNames.add(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const stacks: OkeStack[] = [];
|
|
124
|
+
for (const project of [...projectNames].sort()) {
|
|
125
|
+
const containers = await listStackContainers(project, run);
|
|
126
|
+
stacks.push({ project, containers });
|
|
127
|
+
}
|
|
128
|
+
return stacks;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List containers for one compose project.
|
|
133
|
+
*
|
|
134
|
+
* @param project - Compose project name
|
|
135
|
+
* @param run - Docker runner
|
|
136
|
+
*/
|
|
137
|
+
export async function listStackContainers(
|
|
138
|
+
project: string,
|
|
139
|
+
run: DockerRunner = defaultDockerRunner,
|
|
140
|
+
): Promise<readonly OkeContainer[]> {
|
|
141
|
+
const result = await run([
|
|
142
|
+
"ps",
|
|
143
|
+
"-a",
|
|
144
|
+
"--filter",
|
|
145
|
+
`label=com.docker.compose.project=${project}`,
|
|
146
|
+
"--format",
|
|
147
|
+
"{{json .}}",
|
|
148
|
+
]);
|
|
149
|
+
if (result.exitCode !== 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`oke docker clean: docker ps failed (${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return parseContainerJsonLines(result.stdout, project);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Tear down one stack — containers, networks, and named volumes.
|
|
159
|
+
*
|
|
160
|
+
* @param project - Compose project name
|
|
161
|
+
* @param run - Docker runner
|
|
162
|
+
*/
|
|
163
|
+
export async function downStack(
|
|
164
|
+
project: string,
|
|
165
|
+
run: DockerRunner = defaultDockerRunner,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
if (!isOkeDevProject(project)) {
|
|
168
|
+
throw new Error(`oke docker clean: refusing non-OKE project ${project}`);
|
|
169
|
+
}
|
|
170
|
+
const result = await run(["compose", "-p", project, "down", "-v", "--remove-orphans"]);
|
|
171
|
+
if (result.exitCode !== 0) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`oke docker clean: docker compose down failed for ${project} (${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Stable selection key for a container (or empty stack sentinel).
|
|
180
|
+
*
|
|
181
|
+
* @param project - Compose project
|
|
182
|
+
* @param containerName - Container name, or empty for volume-only stacks
|
|
183
|
+
*/
|
|
184
|
+
export function selectionKey(project: string, containerName = ""): string {
|
|
185
|
+
return containerName ? `${project}\0${containerName}` : `${project}\0`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse selection keys back into projects (and optional container filter).
|
|
190
|
+
*
|
|
191
|
+
* @param keys - Values from multiselect
|
|
192
|
+
* @param stacks - Known stacks (for container lookup)
|
|
193
|
+
*/
|
|
194
|
+
export function resolveSelection(
|
|
195
|
+
keys: readonly string[],
|
|
196
|
+
stacks: readonly OkeStack[],
|
|
197
|
+
): {
|
|
198
|
+
readonly projects: readonly string[];
|
|
199
|
+
readonly containers: readonly OkeContainer[];
|
|
200
|
+
} {
|
|
201
|
+
const byProject = new Map(stacks.map((s) => [s.project, s] as const));
|
|
202
|
+
const projects = new Set<string>();
|
|
203
|
+
const containers: OkeContainer[] = [];
|
|
204
|
+
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
const sep = key.indexOf("\0");
|
|
207
|
+
const project = sep === -1 ? key : key.slice(0, sep);
|
|
208
|
+
const containerName = sep === -1 ? "" : key.slice(sep + 1);
|
|
209
|
+
if (!isOkeDevProject(project)) continue;
|
|
210
|
+
projects.add(project);
|
|
211
|
+
const stack = byProject.get(project);
|
|
212
|
+
if (!stack) continue;
|
|
213
|
+
if (!containerName) continue;
|
|
214
|
+
const found = stack.containers.find((c) => c.name === containerName);
|
|
215
|
+
if (found) containers.push(found);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { projects: [...projects].sort(), containers };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function listComposeProjectNames(run: DockerRunner): Promise<readonly string[]> {
|
|
222
|
+
const result = await run(["compose", "ls", "-a", "--format", "json"]);
|
|
223
|
+
if (result.exitCode !== 0) {
|
|
224
|
+
// Compose plugin missing or Docker down — other probes may still find leftovers.
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const names: string[] = [];
|
|
228
|
+
for (const row of parseJsonRecords(result.stdout)) {
|
|
229
|
+
const name = stringField(row, "Name") ?? stringField(row, "name");
|
|
230
|
+
if (name) names.push(name);
|
|
231
|
+
}
|
|
232
|
+
return names;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function listAllOkeContainers(run: DockerRunner): Promise<readonly OkeContainer[]> {
|
|
236
|
+
const result = await run([
|
|
237
|
+
"ps",
|
|
238
|
+
"-a",
|
|
239
|
+
"--filter",
|
|
240
|
+
"label=com.docker.compose.project",
|
|
241
|
+
"--format",
|
|
242
|
+
"{{json .}}",
|
|
243
|
+
]);
|
|
244
|
+
if (result.exitCode !== 0) return [];
|
|
245
|
+
const out: OkeContainer[] = [];
|
|
246
|
+
for (const row of parseJsonRecords(result.stdout)) {
|
|
247
|
+
const project =
|
|
248
|
+
labelValue(row, "com.docker.compose.project") ??
|
|
249
|
+
stringField(row, "ComposeProject") ??
|
|
250
|
+
stringField(row, "Project");
|
|
251
|
+
if (!project || !isOkeDevProject(project)) continue;
|
|
252
|
+
out.push(containerFromRow(row, project));
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function listOkeResourceProjectNames(
|
|
258
|
+
run: DockerRunner,
|
|
259
|
+
kind: "volume" | "network",
|
|
260
|
+
): Promise<readonly string[]> {
|
|
261
|
+
const result = await run([kind, "ls", "--format", "{{json .}}"]);
|
|
262
|
+
if (result.exitCode !== 0) return [];
|
|
263
|
+
const names = new Set<string>();
|
|
264
|
+
for (const row of parseJsonRecords(result.stdout)) {
|
|
265
|
+
const name = stringField(row, "Name") ?? stringField(row, "name");
|
|
266
|
+
if (!name) continue;
|
|
267
|
+
const project = projectFromResourceName(name);
|
|
268
|
+
if (project) names.add(project);
|
|
269
|
+
}
|
|
270
|
+
return [...names];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Extract `oke-dev-<id>` from a volume/network name like `oke-dev-abc123_oke`.
|
|
275
|
+
*
|
|
276
|
+
* @param name - Docker resource name
|
|
277
|
+
*/
|
|
278
|
+
export function projectFromResourceName(name: string): string | null {
|
|
279
|
+
const m = /^(oke-dev-[0-9a-f]{6})(?:_|$)/.exec(name);
|
|
280
|
+
return m?.[1] ?? null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseContainerJsonLines(stdout: string, project: string): readonly OkeContainer[] {
|
|
284
|
+
return parseJsonRecords(stdout).map((row) => containerFromRow(row, project));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function containerFromRow(row: Record<string, unknown>, project: string): OkeContainer {
|
|
288
|
+
const names = stringField(row, "Names") ?? stringField(row, "Name") ?? "";
|
|
289
|
+
const name = names.split(",")[0]?.replace(/^\//, "") || names;
|
|
290
|
+
const service =
|
|
291
|
+
labelValue(row, "com.docker.compose.service") ?? stringField(row, "Service") ?? name;
|
|
292
|
+
const id = stringField(row, "ID") ?? stringField(row, "Id") ?? name;
|
|
293
|
+
const state =
|
|
294
|
+
stringField(row, "State") ?? stringField(row, "Status") ?? stringField(row, "state") ?? "";
|
|
295
|
+
return { id, name, service, state, project };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function labelValue(row: Record<string, unknown>, key: string): string | null {
|
|
299
|
+
const labels = row.Labels ?? row.labels;
|
|
300
|
+
if (typeof labels === "string") {
|
|
301
|
+
for (const part of labels.split(",")) {
|
|
302
|
+
const eq = part.indexOf("=");
|
|
303
|
+
if (eq === -1) continue;
|
|
304
|
+
if (part.slice(0, eq) === key) return part.slice(eq + 1);
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (labels && typeof labels === "object") {
|
|
309
|
+
const v = (labels as Record<string, unknown>)[key];
|
|
310
|
+
return typeof v === "string" ? v : null;
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function stringField(row: Record<string, unknown>, key: string): string | null {
|
|
316
|
+
const v = row[key];
|
|
317
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Parse Docker `--format json` / `{{json .}}` output (array or NDJSON).
|
|
322
|
+
*
|
|
323
|
+
* @param text - stdout
|
|
324
|
+
*/
|
|
325
|
+
export function parseJsonRecords(text: string): readonly Record<string, unknown>[] {
|
|
326
|
+
const trimmed = text.trim();
|
|
327
|
+
if (!trimmed) return [];
|
|
328
|
+
try {
|
|
329
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
330
|
+
if (Array.isArray(parsed)) {
|
|
331
|
+
return parsed.filter(
|
|
332
|
+
(x): x is Record<string, unknown> => typeof x === "object" && x !== null,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
336
|
+
return [parsed as Record<string, unknown>];
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// NDJSON — one object per line
|
|
340
|
+
}
|
|
341
|
+
const out: Record<string, unknown>[] = [];
|
|
342
|
+
for (const line of trimmed.split("\n")) {
|
|
343
|
+
const t = line.trim();
|
|
344
|
+
if (!t) continue;
|
|
345
|
+
try {
|
|
346
|
+
const row: unknown = JSON.parse(t);
|
|
347
|
+
if (typeof row === "object" && row !== null && !Array.isArray(row)) {
|
|
348
|
+
out.push(row as Record<string, unknown>);
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
// skip malformed lines
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
@@ -81,6 +81,10 @@ describe("image recipes", () => {
|
|
|
81
81
|
expect(applied.volumes).toContain("ai-data:/root/.ollama");
|
|
82
82
|
expect(applied.healthcheck?.test.join(" ")).toContain("ollama list");
|
|
83
83
|
expect(String(applied.command)).toContain("ollama pull");
|
|
84
|
+
// Compose must see `$$` so `$pid` / `$i` are not treated as project env.
|
|
85
|
+
expect(String(applied.command)).toContain("pid=$$!");
|
|
86
|
+
expect(String(applied.command)).toContain("wait $$pid");
|
|
87
|
+
expect(String(applied.command)).not.toMatch(/(?<!\$)\$pid\b/);
|
|
84
88
|
const url = recipeFor(spec.image).url(spec, {
|
|
85
89
|
host: "127.0.0.1",
|
|
86
90
|
port: 11434,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integration: generated Dockerfile builds (and optionally runs).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in via `OKE_TEST_DOCKER=1` plus a live Docker daemon. Never an empty pass.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import { describe, expect, test } from "bun:test";
|
|
@@ -8,47 +10,50 @@ import { tmpdir } from "node:os";
|
|
|
8
10
|
import { join } from "node:path";
|
|
9
11
|
import { deriveInfrastructure, writeDerivedFiles } from "./index.ts";
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
function dockerAvailable(): boolean {
|
|
12
14
|
try {
|
|
13
|
-
|
|
14
|
-
stdout: "pipe",
|
|
15
|
-
stderr: "pipe",
|
|
16
|
-
});
|
|
17
|
-
const code = await proc.exited;
|
|
18
|
-
return code === 0;
|
|
15
|
+
return Bun.spawnSync(["docker", "info"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
|
|
19
16
|
} catch {
|
|
20
17
|
return false;
|
|
21
18
|
}
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
const WANT = process.env.OKE_TEST_DOCKER === "1";
|
|
22
|
+
const DOCKER = WANT && dockerAvailable();
|
|
23
|
+
if (!DOCKER) {
|
|
24
|
+
console.log(
|
|
25
|
+
WANT
|
|
26
|
+
? "skip: generated Dockerfile e2e (docker daemon not available)"
|
|
27
|
+
: "skip: generated Dockerfile e2e (OKE_TEST_DOCKER≠1)",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const live = DOCKER ? test : test.skip;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
describe("generated Dockerfile integration", () => {
|
|
33
|
+
live(
|
|
34
|
+
"oke docker output builds and runs",
|
|
35
|
+
async () => {
|
|
36
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-df-build-"));
|
|
37
|
+
try {
|
|
38
|
+
// Minimal app that `oke start` can import.
|
|
39
|
+
await Bun.write(
|
|
40
|
+
join(dir, "package.json"),
|
|
41
|
+
JSON.stringify(
|
|
42
|
+
{
|
|
43
|
+
name: "oke-docker-fixture",
|
|
44
|
+
private: true,
|
|
45
|
+
type: "module",
|
|
46
|
+
bin: { oke: "./oke.ts" },
|
|
47
|
+
scripts: { start: "bun ./app.ts" },
|
|
48
|
+
okengine: { entry: "./app.ts" },
|
|
49
|
+
},
|
|
50
|
+
null,
|
|
51
|
+
2,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
await Bun.write(
|
|
55
|
+
join(dir, "oke.ts"),
|
|
56
|
+
`#!/usr/bin/env bun
|
|
52
57
|
const [cmd] = process.argv.slice(2);
|
|
53
58
|
if (cmd === "start") {
|
|
54
59
|
await import("./app.ts");
|
|
@@ -57,10 +62,10 @@ if (cmd === "start") {
|
|
|
57
62
|
process.exit(1);
|
|
58
63
|
}
|
|
59
64
|
`,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
);
|
|
66
|
+
await Bun.write(
|
|
67
|
+
join(dir, "app.ts"),
|
|
68
|
+
`const server = Bun.serve({
|
|
64
69
|
port: Number(process.env.PORT ?? 6530),
|
|
65
70
|
hostname: "0.0.0.0",
|
|
66
71
|
fetch() {
|
|
@@ -69,95 +74,97 @@ if (cmd === "start") {
|
|
|
69
74
|
});
|
|
70
75
|
console.log("listening", server.port);
|
|
71
76
|
`,
|
|
72
|
-
|
|
77
|
+
);
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
// No lockfile — adjust Dockerfile install for the fixture.
|
|
80
|
+
const derived = deriveInfrastructure({
|
|
81
|
+
images: { "store.sql": "postgres:18-alpine" },
|
|
82
|
+
credentials: {
|
|
83
|
+
"store.sql": {
|
|
84
|
+
user: "oke",
|
|
85
|
+
password: "fixture-pass",
|
|
86
|
+
database: "oke",
|
|
87
|
+
},
|
|
82
88
|
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
await writeDerivedFiles(derived, dir);
|
|
89
|
+
app: "fixture",
|
|
90
|
+
// Flat fixture root (build context `.`).
|
|
91
|
+
composeDir: ".",
|
|
92
|
+
});
|
|
93
|
+
await writeDerivedFiles(derived, dir);
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Fixture has no bun.lock — rewrite install step.
|
|
96
|
+
let df = await Bun.file(join(dir, "Dockerfile")).text();
|
|
97
|
+
df = df.replace(
|
|
98
|
+
"RUN bun install --frozen-lockfile --production",
|
|
99
|
+
"RUN bun install --production",
|
|
100
|
+
);
|
|
101
|
+
await Bun.write(join(dir, "Dockerfile"), df);
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
const tag = `oke-docker-fixture:${Date.now()}`;
|
|
104
|
+
const build = Bun.spawn(["docker", "build", "-t", tag, dir], {
|
|
105
|
+
stdout: "pipe",
|
|
106
|
+
stderr: "pipe",
|
|
107
|
+
});
|
|
108
|
+
const [buildOut, buildErr, buildCode] = await Promise.all([
|
|
109
|
+
new Response(build.stdout).text(),
|
|
110
|
+
new Response(build.stderr).text(),
|
|
111
|
+
build.exited,
|
|
112
|
+
]);
|
|
113
|
+
expect(buildCode).toBe(0);
|
|
114
|
+
if (buildCode !== 0) {
|
|
115
|
+
console.error(buildOut, buildErr);
|
|
116
|
+
}
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
const name = `oke-fixture-run-${Date.now()}`;
|
|
119
|
+
const run = Bun.spawn(["docker", "run", "-d", "--name", name, "-p", "0:6530", tag], {
|
|
120
|
+
stdout: "pipe",
|
|
121
|
+
stderr: "pipe",
|
|
122
|
+
});
|
|
123
|
+
const [runOut, runErr, runCode] = await Promise.all([
|
|
124
|
+
new Response(run.stdout).text(),
|
|
125
|
+
new Response(run.stderr).text(),
|
|
126
|
+
run.exited,
|
|
127
|
+
]);
|
|
128
|
+
expect(runCode).toBe(0);
|
|
129
|
+
if (runCode !== 0) console.error(runOut, runErr);
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// Resolve published port
|
|
132
|
+
const portProc = Bun.spawn(["docker", "port", name, "6530"], {
|
|
133
|
+
stdout: "pipe",
|
|
134
|
+
stderr: "pipe",
|
|
135
|
+
});
|
|
136
|
+
const portOut = await new Response(portProc.stdout).text();
|
|
137
|
+
await portProc.exited;
|
|
138
|
+
const m = portOut.match(/:(\d+)/);
|
|
139
|
+
expect(m).toBeTruthy();
|
|
140
|
+
const hostPort = m![1]!;
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
let body: { ok?: boolean } | null = null;
|
|
143
|
+
for (let i = 0; i < 20; i++) {
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(`http://127.0.0.1:${hostPort}/`);
|
|
146
|
+
if (res.ok) {
|
|
147
|
+
body = (await res.json()) as { ok?: boolean };
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
await Bun.sleep(250);
|
|
144
152
|
}
|
|
145
|
-
} catch {
|
|
146
|
-
await Bun.sleep(250);
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
expect(body?.ok).toBe(true);
|
|
154
|
+
expect(body?.ok).toBe(true);
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
await Bun.spawn(["docker", "rm", "-f", name], {
|
|
157
|
+
stdout: "pipe",
|
|
158
|
+
stderr: "pipe",
|
|
159
|
+
}).exited;
|
|
160
|
+
await Bun.spawn(["docker", "rmi", "-f", tag], {
|
|
161
|
+
stdout: "pipe",
|
|
162
|
+
stderr: "pipe",
|
|
163
|
+
}).exited;
|
|
164
|
+
} finally {
|
|
165
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
180_000,
|
|
169
|
+
);
|
|
163
170
|
});
|
package/src/docker/index.ts
CHANGED
|
@@ -66,6 +66,25 @@ export {
|
|
|
66
66
|
stackAppSlug,
|
|
67
67
|
stackInstanceId,
|
|
68
68
|
} from "./stack-id.ts";
|
|
69
|
+
export {
|
|
70
|
+
OKE_DEV_PROJECT_RE,
|
|
71
|
+
composeProjectName,
|
|
72
|
+
defaultDockerRunner,
|
|
73
|
+
downStack,
|
|
74
|
+
isOkeDevProject,
|
|
75
|
+
isOkeProjectRoot,
|
|
76
|
+
listOkeComposeProjects,
|
|
77
|
+
listStackContainers,
|
|
78
|
+
parseJsonRecords,
|
|
79
|
+
projectFromResourceName,
|
|
80
|
+
projectsFromContainerSelection,
|
|
81
|
+
resolveSelection,
|
|
82
|
+
selectionKey,
|
|
83
|
+
type DockerRunResult,
|
|
84
|
+
type DockerRunner,
|
|
85
|
+
type OkeContainer,
|
|
86
|
+
type OkeStack,
|
|
87
|
+
} from "./cleanup.ts";
|
|
69
88
|
export {
|
|
70
89
|
formatImagesLock,
|
|
71
90
|
pinImages,
|