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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke docker clean` — non-interactive flag matrix + wiring.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdtemp } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { composeProjectName, type DockerRunner } from "../docker/cleanup.ts";
|
|
10
|
+
import { dockerCli } from "./docker.ts";
|
|
11
|
+
import { dockerCleanHelp, parseDockerCleanArgs, runDockerClean } from "./docker-clean.ts";
|
|
12
|
+
import { EXIT_OK, EXIT_USAGE } from "./exit.ts";
|
|
13
|
+
import { formatOkeHelp, flagTokensFor, OKE_COMMANDS } from "./registry.ts";
|
|
14
|
+
|
|
15
|
+
function fakeStacksRunner(projects: readonly string[]): DockerRunner {
|
|
16
|
+
return async (args) => {
|
|
17
|
+
const key = args.join(" ");
|
|
18
|
+
if (key.startsWith("compose ls")) {
|
|
19
|
+
return {
|
|
20
|
+
exitCode: 0,
|
|
21
|
+
stdout: JSON.stringify(projects.map((Name) => ({ Name, Status: "exited" }))),
|
|
22
|
+
stderr: "",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (key.includes("label=com.docker.compose.project=") && key.startsWith("ps")) {
|
|
26
|
+
const filter = args[args.indexOf("--filter")! + 1]!;
|
|
27
|
+
const project = filter.replace(/^label=com\.docker\.compose\.project=/, "");
|
|
28
|
+
if (!projects.includes(project)) return { exitCode: 0, stdout: "", stderr: "" };
|
|
29
|
+
return {
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
stdout: JSON.stringify({
|
|
32
|
+
ID: "id",
|
|
33
|
+
Names: `${project}-store-sql-1`,
|
|
34
|
+
State: "exited",
|
|
35
|
+
Labels: `com.docker.compose.project=${project},com.docker.compose.service=store-sql`,
|
|
36
|
+
}),
|
|
37
|
+
stderr: "",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (key.startsWith("ps -a --filter label=com.docker.compose.project --format")) {
|
|
41
|
+
return {
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
stdout: projects
|
|
44
|
+
.map((project) =>
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
ID: "id",
|
|
47
|
+
Names: `${project}-store-sql-1`,
|
|
48
|
+
State: "exited",
|
|
49
|
+
Labels: `com.docker.compose.project=${project},com.docker.compose.service=store-sql`,
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
.join("\n"),
|
|
53
|
+
stderr: "",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (key.startsWith("volume ls") || key.startsWith("network ls")) {
|
|
57
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
58
|
+
}
|
|
59
|
+
if (key.startsWith("compose -p ") && key.includes("down")) {
|
|
60
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
61
|
+
}
|
|
62
|
+
return { exitCode: 1, stdout: "", stderr: `unexpected: ${key}` };
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("oke docker clean", () => {
|
|
67
|
+
test("parseDockerCleanArgs", () => {
|
|
68
|
+
expect(parseDockerCleanArgs(["--yes", "--all"])).toEqual({ yes: true, all: true });
|
|
69
|
+
expect(parseDockerCleanArgs(["-y"])).toEqual({ yes: true, all: false });
|
|
70
|
+
expect(parseDockerCleanArgs(["--help"]).help).toBe(true);
|
|
71
|
+
expect(() => parseDockerCleanArgs(["--nope"])).toThrow(/unknown flag/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("non-TTY outside without --all --yes → usage", async () => {
|
|
75
|
+
const err: string[] = [];
|
|
76
|
+
const code = await runDockerClean({
|
|
77
|
+
cwd: await mkdtemp(join(tmpdir(), "oke-clean-out-")),
|
|
78
|
+
stdinIsTTY: false,
|
|
79
|
+
yes: true,
|
|
80
|
+
all: false,
|
|
81
|
+
runDocker: fakeStacksRunner(["oke-dev-aaaaaa"]),
|
|
82
|
+
writeErr: (t) => err.push(t),
|
|
83
|
+
});
|
|
84
|
+
expect(code).toBe(EXIT_USAGE);
|
|
85
|
+
expect(err.join("")).toContain("--all --yes");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("non-TTY outside --all --yes tears down every stack", async () => {
|
|
89
|
+
const downs: string[] = [];
|
|
90
|
+
const run: DockerRunner = async (args) => {
|
|
91
|
+
if (args[0] === "compose" && args[1] === "-p" && args.includes("down")) {
|
|
92
|
+
downs.push(args[2]!);
|
|
93
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
94
|
+
}
|
|
95
|
+
return fakeStacksRunner(["oke-dev-aaaaaa", "oke-dev-bbbbbb"])(args);
|
|
96
|
+
};
|
|
97
|
+
const logs: string[] = [];
|
|
98
|
+
const code = await runDockerClean({
|
|
99
|
+
cwd: await mkdtemp(join(tmpdir(), "oke-clean-all-")),
|
|
100
|
+
stdinIsTTY: false,
|
|
101
|
+
yes: true,
|
|
102
|
+
all: true,
|
|
103
|
+
runDocker: run,
|
|
104
|
+
write: (t) => logs.push(t),
|
|
105
|
+
});
|
|
106
|
+
expect(code).toBe(EXIT_OK);
|
|
107
|
+
expect(downs.sort()).toEqual(["oke-dev-aaaaaa", "oke-dev-bbbbbb"]);
|
|
108
|
+
expect(logs.join("")).toContain("down -v");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("non-TTY inside --yes cleans current project only", async () => {
|
|
112
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-clean-in-"));
|
|
113
|
+
await Bun.write(join(dir, "oke.config.ts"), "export default {};\n");
|
|
114
|
+
const current = composeProjectName(dir);
|
|
115
|
+
const other = "oke-dev-ffffff";
|
|
116
|
+
const downs: string[] = [];
|
|
117
|
+
const run: DockerRunner = async (args) => {
|
|
118
|
+
if (args[0] === "compose" && args[1] === "-p" && args.includes("down")) {
|
|
119
|
+
downs.push(args[2]!);
|
|
120
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
121
|
+
}
|
|
122
|
+
return fakeStacksRunner([current, other])(args);
|
|
123
|
+
};
|
|
124
|
+
const code = await runDockerClean({
|
|
125
|
+
cwd: dir,
|
|
126
|
+
stdinIsTTY: false,
|
|
127
|
+
yes: true,
|
|
128
|
+
runDocker: run,
|
|
129
|
+
write: () => {},
|
|
130
|
+
});
|
|
131
|
+
expect(code).toBe(EXIT_OK);
|
|
132
|
+
expect(downs).toEqual([current]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("non-TTY without --yes → usage", async () => {
|
|
136
|
+
const code = await runDockerClean({
|
|
137
|
+
cwd: await mkdtemp(join(tmpdir(), "oke-clean-noy-")),
|
|
138
|
+
stdinIsTTY: false,
|
|
139
|
+
runDocker: fakeStacksRunner(["oke-dev-aaaaaa"]),
|
|
140
|
+
writeErr: () => {},
|
|
141
|
+
});
|
|
142
|
+
expect(code).toBe(EXIT_USAGE);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("TTY empty selection exits 0", async () => {
|
|
146
|
+
const code = await runDockerClean({
|
|
147
|
+
cwd: await mkdtemp(join(tmpdir(), "oke-clean-empty-")),
|
|
148
|
+
stdinIsTTY: true,
|
|
149
|
+
runDocker: fakeStacksRunner(["oke-dev-aaaaaa"]),
|
|
150
|
+
pickKeys: async () => [],
|
|
151
|
+
write: () => {},
|
|
152
|
+
});
|
|
153
|
+
expect(code).toBe(EXIT_OK);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("TTY cancel exits 0", async () => {
|
|
157
|
+
const code = await runDockerClean({
|
|
158
|
+
cwd: await mkdtemp(join(tmpdir(), "oke-clean-cancel-")),
|
|
159
|
+
stdinIsTTY: true,
|
|
160
|
+
runDocker: fakeStacksRunner(["oke-dev-aaaaaa"]),
|
|
161
|
+
pickKeys: async () => null,
|
|
162
|
+
write: () => {},
|
|
163
|
+
});
|
|
164
|
+
expect(code).toBe(EXIT_OK);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("dockerCli routes clean subcommand", async () => {
|
|
168
|
+
const code = await dockerCli(["clean", "--help"]);
|
|
169
|
+
expect(code).toBe(EXIT_OK);
|
|
170
|
+
expect(dockerCleanHelp()).toContain("--all");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("registry lists clean flags and help mentions clean", () => {
|
|
174
|
+
const docker = OKE_COMMANDS.find((c) => c.name === "docker")!;
|
|
175
|
+
expect(flagTokensFor(docker, "clean")).toContain("--yes");
|
|
176
|
+
expect(flagTokensFor(docker, "clean")).toContain("--all");
|
|
177
|
+
expect(formatOkeHelp()).toContain("clean");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke docker clean` — remove leftover `oke-dev-*` compose stacks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cancel, confirm, intro, isCancel, multiselect, outro } from "@clack/prompts";
|
|
6
|
+
import {
|
|
7
|
+
composeProjectName,
|
|
8
|
+
defaultDockerRunner,
|
|
9
|
+
downStack,
|
|
10
|
+
isOkeProjectRoot,
|
|
11
|
+
listOkeComposeProjects,
|
|
12
|
+
resolveSelection,
|
|
13
|
+
selectionKey,
|
|
14
|
+
type DockerRunner,
|
|
15
|
+
type OkeContainer,
|
|
16
|
+
type OkeStack,
|
|
17
|
+
} from "../docker/cleanup.ts";
|
|
18
|
+
import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "./exit.ts";
|
|
19
|
+
|
|
20
|
+
/** Parsed flags for {@link runDockerClean}. */
|
|
21
|
+
export interface DockerCleanFlags {
|
|
22
|
+
readonly yes?: boolean;
|
|
23
|
+
readonly all?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Options for {@link runDockerClean} (injectable for tests). */
|
|
27
|
+
export interface DockerCleanOptions extends DockerCleanFlags {
|
|
28
|
+
readonly cwd?: string;
|
|
29
|
+
readonly stdinIsTTY?: boolean;
|
|
30
|
+
readonly runDocker?: DockerRunner;
|
|
31
|
+
readonly write?: (text: string) => void;
|
|
32
|
+
readonly writeErr?: (text: string) => void;
|
|
33
|
+
/** Override interactive container pick (tests). `null` = cancel. */
|
|
34
|
+
readonly pickKeys?: (
|
|
35
|
+
stacks: readonly OkeStack[],
|
|
36
|
+
scope: "current" | "other" | "all",
|
|
37
|
+
) => Promise<readonly string[] | null>;
|
|
38
|
+
/** Override confirm (tests). */
|
|
39
|
+
readonly confirmDown?: (projects: readonly string[]) => Promise<boolean>;
|
|
40
|
+
/** When inside a project and other stacks exist — ask to include them. */
|
|
41
|
+
readonly askIncludeOthers?: () => Promise<boolean | null>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Help text for `oke docker clean`.
|
|
46
|
+
*/
|
|
47
|
+
export function dockerCleanHelp(): string {
|
|
48
|
+
return `oke docker clean [--yes|-y] [--all]
|
|
49
|
+
|
|
50
|
+
Remove leftover OKE docker compose stacks (containers, networks, volumes).
|
|
51
|
+
|
|
52
|
+
Inside a project: focuses the current stack (oke-dev-<id>); optionally
|
|
53
|
+
include other stacks on this machine.
|
|
54
|
+
Outside a project: list every oke-dev-* stack and pick what to remove.
|
|
55
|
+
|
|
56
|
+
--yes, -y Non-interactive; required when stdin is not a TTY
|
|
57
|
+
--all With --yes outside a project: remove every oke-dev-* stack
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse args after `docker clean`.
|
|
63
|
+
*
|
|
64
|
+
* @param args - Flag tokens
|
|
65
|
+
*/
|
|
66
|
+
export function parseDockerCleanArgs(args: readonly string[]): DockerCleanFlags & {
|
|
67
|
+
readonly help?: boolean;
|
|
68
|
+
} {
|
|
69
|
+
let yes = false;
|
|
70
|
+
let all = false;
|
|
71
|
+
for (const a of args) {
|
|
72
|
+
if (a === "--help" || a === "-h") return { help: true };
|
|
73
|
+
if (a === "--yes" || a === "-y") yes = true;
|
|
74
|
+
else if (a === "--all") all = true;
|
|
75
|
+
else if (a.startsWith("-")) {
|
|
76
|
+
throw new Error(`oke docker clean: unknown flag ${a}`);
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error(`oke docker clean: unexpected argument ${a}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { yes, all };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run stack cleanup.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Flags + seams
|
|
88
|
+
* @returns Exit code
|
|
89
|
+
*/
|
|
90
|
+
export async function runDockerClean(options: DockerCleanOptions = {}): Promise<number> {
|
|
91
|
+
const write = options.write ?? ((t) => process.stdout.write(t));
|
|
92
|
+
const writeErr = options.writeErr ?? ((t) => process.stderr.write(t));
|
|
93
|
+
const cwd = options.cwd ?? process.cwd();
|
|
94
|
+
const run = options.runDocker ?? defaultDockerRunner;
|
|
95
|
+
const tty = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
96
|
+
const yes = options.yes === true;
|
|
97
|
+
const all = options.all === true;
|
|
98
|
+
|
|
99
|
+
let stacks: readonly OkeStack[];
|
|
100
|
+
try {
|
|
101
|
+
stacks = await listOkeComposeProjects(run);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
writeErr(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
104
|
+
return EXIT_RUNTIME;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const inside = await isOkeProjectRoot(cwd);
|
|
108
|
+
const currentProject = inside ? composeProjectName(cwd) : null;
|
|
109
|
+
|
|
110
|
+
if (stacks.length === 0) {
|
|
111
|
+
write("oke docker clean: no oke-dev-* stacks found\n");
|
|
112
|
+
return EXIT_OK;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!tty) {
|
|
116
|
+
return runNonInteractive({
|
|
117
|
+
stacks,
|
|
118
|
+
inside,
|
|
119
|
+
currentProject,
|
|
120
|
+
yes,
|
|
121
|
+
all,
|
|
122
|
+
run,
|
|
123
|
+
write,
|
|
124
|
+
writeErr,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return runInteractive({
|
|
129
|
+
stacks,
|
|
130
|
+
inside,
|
|
131
|
+
currentProject,
|
|
132
|
+
run,
|
|
133
|
+
write,
|
|
134
|
+
writeErr,
|
|
135
|
+
pickKeys: options.pickKeys,
|
|
136
|
+
confirmDown: options.confirmDown,
|
|
137
|
+
askIncludeOthers: options.askIncludeOthers,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* CLI entry for `oke docker clean …`.
|
|
143
|
+
*
|
|
144
|
+
* @param args - Args after `clean`
|
|
145
|
+
*/
|
|
146
|
+
export async function dockerCleanCli(args: readonly string[]): Promise<number> {
|
|
147
|
+
let flags: ReturnType<typeof parseDockerCleanArgs>;
|
|
148
|
+
try {
|
|
149
|
+
flags = parseDockerCleanArgs(args);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
152
|
+
console.error(dockerCleanHelp());
|
|
153
|
+
return EXIT_USAGE;
|
|
154
|
+
}
|
|
155
|
+
if (flags.help) {
|
|
156
|
+
console.log(dockerCleanHelp());
|
|
157
|
+
return EXIT_OK;
|
|
158
|
+
}
|
|
159
|
+
return runDockerClean(flags);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runNonInteractive(options: {
|
|
163
|
+
readonly stacks: readonly OkeStack[];
|
|
164
|
+
readonly inside: boolean;
|
|
165
|
+
readonly currentProject: string | null;
|
|
166
|
+
readonly yes: boolean;
|
|
167
|
+
readonly all: boolean;
|
|
168
|
+
readonly run: DockerRunner;
|
|
169
|
+
readonly write: (t: string) => void;
|
|
170
|
+
readonly writeErr: (t: string) => void;
|
|
171
|
+
}): Promise<number> {
|
|
172
|
+
const { stacks, inside, currentProject, yes, all, run, write, writeErr } = options;
|
|
173
|
+
|
|
174
|
+
if (!yes) {
|
|
175
|
+
writeErr("oke docker clean: non-TTY requires --yes (and --all when outside a project)\n");
|
|
176
|
+
return EXIT_USAGE;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let projects: string[];
|
|
180
|
+
if (inside && currentProject) {
|
|
181
|
+
if (all) {
|
|
182
|
+
projects = stacks.map((s) => s.project);
|
|
183
|
+
} else {
|
|
184
|
+
const hasCurrent = stacks.some((s) => s.project === currentProject);
|
|
185
|
+
if (!hasCurrent) {
|
|
186
|
+
write(`oke docker clean: no stack for this project (${currentProject})\n`);
|
|
187
|
+
return EXIT_OK;
|
|
188
|
+
}
|
|
189
|
+
projects = [currentProject];
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
if (!all) {
|
|
193
|
+
writeErr(
|
|
194
|
+
"oke docker clean: outside a project, pass --all --yes to remove every oke-dev-* stack\n",
|
|
195
|
+
);
|
|
196
|
+
return EXIT_USAGE;
|
|
197
|
+
}
|
|
198
|
+
projects = stacks.map((s) => s.project);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return tearDownProjects(projects, run, write, writeErr);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function runInteractive(options: {
|
|
205
|
+
readonly stacks: readonly OkeStack[];
|
|
206
|
+
readonly inside: boolean;
|
|
207
|
+
readonly currentProject: string | null;
|
|
208
|
+
readonly run: DockerRunner;
|
|
209
|
+
readonly write: (t: string) => void;
|
|
210
|
+
readonly writeErr: (t: string) => void;
|
|
211
|
+
readonly pickKeys?: DockerCleanOptions["pickKeys"];
|
|
212
|
+
readonly confirmDown?: DockerCleanOptions["confirmDown"];
|
|
213
|
+
readonly askIncludeOthers?: DockerCleanOptions["askIncludeOthers"];
|
|
214
|
+
}): Promise<number> {
|
|
215
|
+
const {
|
|
216
|
+
stacks,
|
|
217
|
+
inside,
|
|
218
|
+
currentProject,
|
|
219
|
+
run,
|
|
220
|
+
write,
|
|
221
|
+
writeErr,
|
|
222
|
+
pickKeys,
|
|
223
|
+
confirmDown,
|
|
224
|
+
askIncludeOthers,
|
|
225
|
+
} = options;
|
|
226
|
+
|
|
227
|
+
intro("oke docker clean");
|
|
228
|
+
|
|
229
|
+
const selectedKeys: string[] = [];
|
|
230
|
+
|
|
231
|
+
if (inside && currentProject) {
|
|
232
|
+
const current = stacks.filter((s) => s.project === currentProject);
|
|
233
|
+
const others = stacks.filter((s) => s.project !== currentProject);
|
|
234
|
+
|
|
235
|
+
if (current.length === 0) {
|
|
236
|
+
write(`No stack for this project (${currentProject}).\n`);
|
|
237
|
+
} else {
|
|
238
|
+
const keys = await pickContainerKeys({
|
|
239
|
+
stacks: current,
|
|
240
|
+
scope: "current",
|
|
241
|
+
message: `Containers for this project (${currentProject})`,
|
|
242
|
+
initialAll: true,
|
|
243
|
+
pickKeys,
|
|
244
|
+
});
|
|
245
|
+
if (keys === null) {
|
|
246
|
+
cancel("Cancelled.");
|
|
247
|
+
return EXIT_OK;
|
|
248
|
+
}
|
|
249
|
+
selectedKeys.push(...keys);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (others.length > 0) {
|
|
253
|
+
const include = askIncludeOthers ? await askIncludeOthers() : await askIncludeOthersPrompt();
|
|
254
|
+
if (include === null) {
|
|
255
|
+
cancel("Cancelled.");
|
|
256
|
+
return EXIT_OK;
|
|
257
|
+
}
|
|
258
|
+
if (include) {
|
|
259
|
+
const keys = await pickContainerKeys({
|
|
260
|
+
stacks: others,
|
|
261
|
+
scope: "other",
|
|
262
|
+
message: "Other OKE stacks on this machine",
|
|
263
|
+
initialAll: false,
|
|
264
|
+
pickKeys,
|
|
265
|
+
});
|
|
266
|
+
if (keys === null) {
|
|
267
|
+
cancel("Cancelled.");
|
|
268
|
+
return EXIT_OK;
|
|
269
|
+
}
|
|
270
|
+
selectedKeys.push(...keys);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
const keys = await pickContainerKeys({
|
|
275
|
+
stacks,
|
|
276
|
+
scope: "all",
|
|
277
|
+
message: "OKE docker stacks on this machine",
|
|
278
|
+
initialAll: false,
|
|
279
|
+
pickKeys,
|
|
280
|
+
});
|
|
281
|
+
if (keys === null) {
|
|
282
|
+
cancel("Cancelled.");
|
|
283
|
+
return EXIT_OK;
|
|
284
|
+
}
|
|
285
|
+
selectedKeys.push(...keys);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (selectedKeys.length === 0) {
|
|
289
|
+
outro("Nothing selected.");
|
|
290
|
+
return EXIT_OK;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const { projects } = resolveSelection(selectedKeys, stacks);
|
|
294
|
+
if (projects.length === 0) {
|
|
295
|
+
outro("Nothing selected.");
|
|
296
|
+
return EXIT_OK;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const summary = projects
|
|
300
|
+
.map((p) => {
|
|
301
|
+
const stack = stacks.find((s) => s.project === p);
|
|
302
|
+
const n = stack?.containers.length ?? 0;
|
|
303
|
+
return n === 0 ? ` ${p} (volumes/networks only)` : ` ${p} (${n} container(s))`;
|
|
304
|
+
})
|
|
305
|
+
.join("\n");
|
|
306
|
+
|
|
307
|
+
const ok = confirmDown
|
|
308
|
+
? await confirmDown(projects)
|
|
309
|
+
: await confirm({
|
|
310
|
+
message: `Remove these stack(s) — containers, networks, and volumes?\n${summary}`,
|
|
311
|
+
initialValue: false,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (isCancel(ok) || ok === false) {
|
|
315
|
+
cancel("Cancelled.");
|
|
316
|
+
return EXIT_OK;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const code = await tearDownProjects(projects, run, write, writeErr);
|
|
320
|
+
if (code === EXIT_OK) outro(`Removed ${projects.length} stack(s).`);
|
|
321
|
+
return code;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function askIncludeOthersPrompt(): Promise<boolean | null> {
|
|
325
|
+
const value = await confirm({
|
|
326
|
+
message: "Also clean other OKE stacks on this machine?",
|
|
327
|
+
initialValue: false,
|
|
328
|
+
});
|
|
329
|
+
if (isCancel(value)) return null;
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function pickContainerKeys(options: {
|
|
334
|
+
readonly stacks: readonly OkeStack[];
|
|
335
|
+
readonly scope: "current" | "other" | "all";
|
|
336
|
+
readonly message: string;
|
|
337
|
+
readonly initialAll: boolean;
|
|
338
|
+
readonly pickKeys?: DockerCleanOptions["pickKeys"];
|
|
339
|
+
}): Promise<readonly string[] | null> {
|
|
340
|
+
const { stacks, scope, message, initialAll, pickKeys } = options;
|
|
341
|
+
if (pickKeys) return pickKeys(stacks, scope);
|
|
342
|
+
|
|
343
|
+
const entries = selectionEntries(stacks);
|
|
344
|
+
if (entries.length === 0) return [];
|
|
345
|
+
|
|
346
|
+
const value = await multiselect({
|
|
347
|
+
message: `${message} — selecting any container removes its whole stack`,
|
|
348
|
+
options: entries.map((e) => ({
|
|
349
|
+
value: e.key,
|
|
350
|
+
label: e.label,
|
|
351
|
+
hint: e.hint,
|
|
352
|
+
})),
|
|
353
|
+
initialValues: initialAll ? entries.map((e) => e.key) : [],
|
|
354
|
+
required: false,
|
|
355
|
+
});
|
|
356
|
+
if (isCancel(value)) return null;
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function selectionEntries(stacks: readonly OkeStack[]): readonly {
|
|
361
|
+
readonly key: string;
|
|
362
|
+
readonly label: string;
|
|
363
|
+
readonly hint?: string;
|
|
364
|
+
}[] {
|
|
365
|
+
const out: { key: string; label: string; hint?: string }[] = [];
|
|
366
|
+
for (const stack of stacks) {
|
|
367
|
+
if (stack.containers.length === 0) {
|
|
368
|
+
out.push({
|
|
369
|
+
key: selectionKey(stack.project),
|
|
370
|
+
label: stack.project,
|
|
371
|
+
hint: "no containers · volumes/networks may remain",
|
|
372
|
+
});
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
for (const c of stack.containers) {
|
|
376
|
+
out.push({
|
|
377
|
+
key: selectionKey(stack.project, c.name),
|
|
378
|
+
label: `${stack.project} · ${displayContainer(c)}`,
|
|
379
|
+
hint: c.state || undefined,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function displayContainer(c: OkeContainer): string {
|
|
387
|
+
return c.service && c.service !== c.name ? `${c.service} (${c.name})` : c.name;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function tearDownProjects(
|
|
391
|
+
projects: readonly string[],
|
|
392
|
+
run: DockerRunner,
|
|
393
|
+
write: (t: string) => void,
|
|
394
|
+
writeErr: (t: string) => void,
|
|
395
|
+
): Promise<number> {
|
|
396
|
+
for (const project of projects) {
|
|
397
|
+
try {
|
|
398
|
+
write(`oke docker clean: down -v ${project}\n`);
|
|
399
|
+
await downStack(project, run);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
writeErr(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
402
|
+
return EXIT_RUNTIME;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return EXIT_OK;
|
|
406
|
+
}
|
package/src/cli/docker.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke docker` — Dockerfile + per-role compose files.
|
|
2
|
+
* `oke docker` — Dockerfile + per-role compose files · `clean` leftover stacks.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type DeriveOptions,
|
|
11
11
|
type DeriveResult,
|
|
12
12
|
} from "../docker/index.ts";
|
|
13
|
+
import { dockerCleanCli, dockerCleanHelp } from "./docker-clean.ts";
|
|
14
|
+
import { EXIT_OK } from "./exit.ts";
|
|
13
15
|
import { loadOkeConfig, loadManifest, resolveImages } from "./load-config.ts";
|
|
14
16
|
|
|
15
17
|
/** Options for {@link runDockerDerive}. */
|
|
@@ -112,11 +114,16 @@ export async function runDockerDerive(
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
/**
|
|
115
|
-
* CLI entry for `oke docker [
|
|
117
|
+
* CLI entry for `oke docker [clean] …` / derive flags.
|
|
116
118
|
*
|
|
117
119
|
* @param args - Args after `docker`
|
|
118
120
|
*/
|
|
119
121
|
export async function dockerCli(args: readonly string[]): Promise<number> {
|
|
122
|
+
const [head, ...rest] = args;
|
|
123
|
+
if (head === "clean") {
|
|
124
|
+
return dockerCleanCli(rest);
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
let prod = false;
|
|
121
128
|
let outDir: string | undefined;
|
|
122
129
|
let configPath: string | undefined;
|
|
@@ -132,8 +139,13 @@ export async function dockerCli(args: readonly string[]): Promise<number> {
|
|
|
132
139
|
|
|
133
140
|
Derive Dockerfile + compose.<role>.yml under docker/ (default).
|
|
134
141
|
--prod is opt-in (never the default). Credentials are never written into YAML.
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
|
|
143
|
+
${dockerCleanHelp()}`);
|
|
144
|
+
return EXIT_OK;
|
|
145
|
+
} else if (!a.startsWith("-")) {
|
|
146
|
+
console.error(`oke docker: unknown subcommand ${a}`);
|
|
147
|
+
console.error("Run `oke docker --help` for usage.");
|
|
148
|
+
return 1;
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
const { code } = await runDockerDerive({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
|
|
8
|
-
/** Mode-aware template (matches
|
|
8
|
+
/** Mode-aware template (matches create-oke templates' drizzle.config.ts). */
|
|
9
9
|
const DRIZZLE_CONFIG_TEMPLATE = `import { defineConfig } from "drizzle-kit";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -18,9 +18,9 @@ const SAMPLE: OkeConfig = {
|
|
|
18
18
|
sql: { local: "sqlite", docker: "postgres", prod: "postgres" },
|
|
19
19
|
kv: { local: "memory", docker: "redis", prod: "redis" },
|
|
20
20
|
},
|
|
21
|
-
signal: { local: "memory", docker: "
|
|
22
|
-
clock: { local: "memory", docker: "
|
|
23
|
-
vault: { local: "
|
|
21
|
+
signal: { local: "memory", docker: "redis", prod: "redis" },
|
|
22
|
+
clock: { local: "memory", docker: "file", prod: "file" },
|
|
23
|
+
vault: { local: "env", docker: "openbao", prod: "openbao" },
|
|
24
24
|
channel: {
|
|
25
25
|
email: { local: "console", docker: "smtp", prod: "smtp" },
|
|
26
26
|
},
|
|
@@ -62,8 +62,9 @@ describe("hero-meta", () => {
|
|
|
62
62
|
expect(store).toContain("postgres");
|
|
63
63
|
expect(store).toContain("redis");
|
|
64
64
|
expect(store).not.toContain("sqlite");
|
|
65
|
-
expect(rows.find((r) => r.element === "signal")?.detail).toBe("
|
|
66
|
-
expect(rows.find((r) => r.element === "clock")?.detail).toBe("
|
|
65
|
+
expect(rows.find((r) => r.element === "signal")?.detail).toBe("redis");
|
|
66
|
+
expect(rows.find((r) => r.element === "clock")?.detail).toBe("file");
|
|
67
|
+
expect(rows.find((r) => r.element === "gate")?.detail).toBe("redis");
|
|
67
68
|
expect(rows.find((r) => r.element === "channel")?.detail).toContain("smtp");
|
|
68
69
|
});
|
|
69
70
|
|
package/src/cli/hero-meta.ts
CHANGED
|
@@ -156,6 +156,12 @@ export function resolveHeroElements(
|
|
|
156
156
|
// `-d` reads the `docker` profile for every element (not a store-only override).
|
|
157
157
|
const configEnv: ConfigEnv = options.configEnv ?? (options.docker ? "docker" : "local");
|
|
158
158
|
|
|
159
|
+
// Gate rates share `drivers.store.kv` (no separate drivers.gate).
|
|
160
|
+
const gateKv =
|
|
161
|
+
options.kvDriver ??
|
|
162
|
+
resolveDriverId(drivers?.store?.kv, configEnv) ??
|
|
163
|
+
(options.docker ? "redis" : "memory");
|
|
164
|
+
|
|
159
165
|
const byName: Record<string, string> = {
|
|
160
166
|
flow: "●",
|
|
161
167
|
signal: resolveDriverId(drivers?.signal, configEnv) ?? "—",
|
|
@@ -164,7 +170,7 @@ export function resolveHeroElements(
|
|
|
164
170
|
kv: options.kvDriver,
|
|
165
171
|
}),
|
|
166
172
|
clock: resolveDriverId(drivers?.clock, configEnv) ?? "—",
|
|
167
|
-
gate:
|
|
173
|
+
gate: gateKv,
|
|
168
174
|
vault: resolveDriverId(drivers?.vault, configEnv) ?? "—",
|
|
169
175
|
channel: channelDetail(drivers?.channel, configEnv),
|
|
170
176
|
ai: resolveDriverId(drivers?.ai, configEnv) ?? "—",
|