okengine 0.6.1 → 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.
Files changed (130) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +73 -181
  3. package/package.json +2 -2
  4. package/site/content/docs/console/clock.mdx +25 -5
  5. package/site/content/docs/console/signals.mdx +13 -4
  6. package/site/content/docs/elements/ai.mdx +29 -2
  7. package/site/content/docs/elements/channel.mdx +19 -13
  8. package/site/content/docs/elements/clock.mdx +56 -24
  9. package/site/content/docs/elements/flow.mdx +2 -0
  10. package/site/content/docs/elements/gate.mdx +8 -1
  11. package/site/content/docs/elements/signal.mdx +173 -29
  12. package/site/content/docs/elements/store.mdx +464 -73
  13. package/site/content/docs/elements/vault.mdx +15 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +13 -6
  15. package/site/content/docs/get-started/installation.mdx +49 -13
  16. package/site/content/docs/get-started/introduction.mdx +1 -1
  17. package/site/content/docs/reference/cli.md +30 -2
  18. package/site/content/docs/reference/configuration.mdx +13 -13
  19. package/site/content/docs/reference/environment-variables.mdx +12 -10
  20. package/site/content/docs/reference/errors.mdx +12 -11
  21. package/site/content/docs/reference/fx.mdx +15 -13
  22. package/site/content/docs/reference/security.md +3 -1
  23. package/src/cli/ai-setup/ai-setup.test.ts +144 -0
  24. package/src/cli/ai-setup/apply.ts +214 -0
  25. package/src/cli/ai-setup/catalog.ts +263 -0
  26. package/src/cli/ai-setup/detect-ollama.ts +166 -0
  27. package/src/cli/ai-setup/index.ts +228 -0
  28. package/src/cli/ai-setup/prompts.ts +649 -0
  29. package/src/cli/ai-setup/recommend.test.ts +100 -0
  30. package/src/cli/ai-setup/recommend.ts +203 -0
  31. package/src/cli/ai.ts +38 -0
  32. package/src/cli/db-auto-push.test.ts +6 -1
  33. package/src/cli/db-auto-push.ts +8 -3
  34. package/src/cli/dev-db-push.test.ts +59 -0
  35. package/src/cli/dev.test.ts +40 -0
  36. package/src/cli/dev.ts +55 -1
  37. package/src/cli/docker-clean.test.ts +179 -0
  38. package/src/cli/docker-clean.ts +406 -0
  39. package/src/cli/docker.ts +16 -4
  40. package/src/cli/ensure-drizzle-config.ts +1 -1
  41. package/src/cli/hero-meta.test.ts +6 -5
  42. package/src/cli/hero-meta.ts +7 -1
  43. package/src/cli/index.ts +5 -0
  44. package/src/cli/load-config.ts +4 -4
  45. package/src/cli/openbao-bootstrap.test.ts +1 -0
  46. package/src/cli/openbao-bootstrap.ts +9 -2
  47. package/src/cli/registry.ts +79 -1
  48. package/src/client/live-gap.test.ts +35 -0
  49. package/src/client/transport.test.ts +21 -0
  50. package/src/client/transport.ts +27 -5
  51. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  52. package/src/config/define-config.test.ts +6 -6
  53. package/src/config/index.ts +1 -1
  54. package/src/console/server/console.test.ts +45 -0
  55. package/src/console/server/flows.ts +39 -9
  56. package/src/console/server/i18n.ts +41 -0
  57. package/src/console/server/vault.ts +12 -38
  58. package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
  59. package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
  60. package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
  61. package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
  62. package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
  63. package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
  64. package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
  65. package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
  66. package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
  67. package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
  68. package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
  69. package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
  70. package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
  71. package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
  72. package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
  73. package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
  74. package/src/console/ui/dist/index.html +1 -1
  75. package/src/console/ui/shell/components/ui.tsx +5 -1
  76. package/src/console/ui/shell/setup/Wizard.tsx +26 -5
  77. package/src/docker/cleanup.test.ts +193 -0
  78. package/src/docker/cleanup.ts +355 -0
  79. package/src/docker/docker.test.ts +4 -0
  80. package/src/docker/index.ts +19 -0
  81. package/src/docker/recipes/ollama.ts +10 -5
  82. package/src/drivers/ai-ollama.ts +45 -2
  83. package/src/drivers/ai-providers.test.ts +31 -0
  84. package/src/drivers/index.ts +3 -0
  85. package/src/drivers/signal-engine.ts +98 -25
  86. package/src/drivers/signal-nats.ts +4 -4
  87. package/src/drivers/signal-postgres.ts +158 -64
  88. package/src/drivers/signal-redis.ts +4 -4
  89. package/src/drivers/signal-types.ts +61 -2
  90. package/src/drivers/vault-driver-removal.test.ts +5 -1
  91. package/src/elements/clock/chaos-child.ts +150 -0
  92. package/src/elements/clock/chaos.test.ts +204 -0
  93. package/src/elements/clock/reconcile.ts +110 -0
  94. package/src/elements/clock/runtime.ts +11 -0
  95. package/src/elements/clock.test.ts +164 -2
  96. package/src/elements/clock.ts +1 -0
  97. package/src/elements/gate/runtime.ts +6 -0
  98. package/src/elements/signal/chaos-child.ts +41 -4
  99. package/src/elements/signal/declare.ts +4 -1
  100. package/src/elements/signal/delivery-modes.test.ts +179 -0
  101. package/src/elements/signal/key-ordering.test.ts +306 -0
  102. package/src/elements/signal/lease-reclaim.test.ts +123 -0
  103. package/src/elements/signal/optional-emit.test.ts +76 -0
  104. package/src/elements/signal/order-lifecycle.test.ts +130 -0
  105. package/src/elements/signal/orphan-messages.test.ts +152 -0
  106. package/src/elements/signal/runtime.ts +11 -4
  107. package/src/elements/signal/schema-emit.test.ts +107 -0
  108. package/src/elements/signal.test.ts +89 -5
  109. package/src/elements/store/files-fx.ts +104 -0
  110. package/src/elements/store/files-image.test.ts +251 -0
  111. package/src/elements/store/files-image.ts +494 -0
  112. package/src/elements/store/runtime.ts +60 -0
  113. package/src/elements/store.ts +24 -0
  114. package/src/elements/vault/boot-chain.ts +150 -0
  115. package/src/elements/vault/runtime.ts +8 -0
  116. package/src/kernel/app.ts +3 -1
  117. package/src/kernel/boot-bind/clock.ts +52 -7
  118. package/src/kernel/boot-bind/gate.ts +71 -3
  119. package/src/kernel/boot-bind/honor-config.test.ts +247 -0
  120. package/src/kernel/boot-bind/signal.ts +64 -7
  121. package/src/kernel/boot-bind/vault.ts +27 -8
  122. package/src/kernel/boot.ts +10 -2
  123. package/src/kernel/errors.ts +8 -2
  124. package/src/kernel/fx.test.ts +13 -0
  125. package/src/kernel/fx.ts +27 -4
  126. package/src/runtime/dev-request-log.test.ts +20 -1
  127. package/src/runtime/dev-request-log.ts +38 -0
  128. package/src/term.test.ts +15 -0
  129. package/src/term.ts +8 -3
  130. 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 [--prod|-p] [--out|-o dir]`.
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
- return 0;
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 `packages/create-oke/template/drizzle.config.ts`). */
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: "postgres", prod: "postgres" },
22
- clock: { local: "memory", docker: "postgres", prod: "postgres" },
23
- vault: { local: "dotenv", docker: "openbao", prod: "openbao" },
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("postgres");
66
- expect(rows.find((r) => r.element === "clock")?.detail).toBe("postgres");
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
 
@@ -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) ?? "—",