okengine 0.10.3 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +76 -10
- package/site/content/docs/deployment/docker-swarm.mdx +48 -111
- package/site/content/docs/deployment/docker.mdx +43 -36
- package/site/content/docs/deployment/index.mdx +9 -9
- package/site/content/docs/deployment/kubernetes.mdx +4 -4
- package/site/content/docs/elements/ai.mdx +7 -7
- package/site/content/docs/elements/channel.mdx +9 -7
- package/site/content/docs/elements/clock.mdx +8 -8
- package/site/content/docs/elements/flow.mdx +46 -47
- package/site/content/docs/elements/gate.mdx +6 -5
- package/site/content/docs/elements/signal.mdx +2 -2
- package/site/content/docs/elements/store.mdx +71 -78
- package/site/content/docs/elements/vault.mdx +89 -70
- package/site/content/docs/get-started/basic-usage.mdx +8 -8
- package/site/content/docs/get-started/installation.mdx +36 -36
- package/site/content/docs/get-started/introduction.mdx +11 -0
- package/site/content/docs/get-started/why.mdx +2 -2
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +11 -11
- package/site/content/docs/plugins/otp.mdx +2 -2
- package/site/content/docs/plugins/passkey.mdx +1 -1
- package/site/content/docs/plugins/two-factor.mdx +1 -1
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +1 -1
- package/site/content/docs/recipes/cockroachdb.mdx +2 -2
- package/site/content/docs/recipes/dragonfly.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -2
- package/site/content/docs/recipes/llama-cpp.mdx +21 -23
- package/site/content/docs/recipes/mailpit.mdx +11 -11
- package/site/content/docs/recipes/meilisearch.mdx +10 -10
- package/site/content/docs/recipes/meta.json +0 -1
- package/site/content/docs/recipes/ollama.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +12 -9
- package/site/content/docs/recipes/postgres.mdx +3 -3
- package/site/content/docs/recipes/redis.mdx +2 -2
- package/site/content/docs/recipes/rustfs.mdx +10 -7
- package/site/content/docs/recipes/sglang.mdx +1 -1
- package/site/content/docs/recipes/supabase-docker.mdx +1 -1
- package/site/content/docs/recipes/timescale.mdx +2 -2
- package/site/content/docs/recipes/valkey.mdx +1 -1
- package/site/content/docs/recipes/vllm.mdx +1 -1
- package/site/content/docs/recipes/yugabytedb.mdx +2 -2
- package/site/content/docs/reference/cli.md +61 -38
- package/site/content/docs/reference/configuration.mdx +145 -70
- package/site/content/docs/reference/environment-variables.mdx +36 -28
- package/site/content/docs/reference/errors.mdx +28 -29
- package/site/content/docs/reference/fx.mdx +11 -4
- package/site/content/docs/reference/i18n.mdx +17 -6
- package/site/content/docs/reference/index.mdx +5 -0
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/migrating-environments.mdx +158 -0
- package/site/content/docs/reference/plugins.mdx +2 -2
- package/src/auth/gate-auth.test.ts +12 -10
- package/src/cli/ai-setup/ai-setup.test.ts +27 -15
- package/src/cli/ai-setup/apply.ts +111 -15
- package/src/cli/ai-setup/catalog.ts +3 -3
- package/src/cli/ai-setup/index.ts +1 -1
- package/src/cli/ai-setup/prompts.ts +1 -1
- package/src/cli/ai-setup/recommend.test.ts +1 -1
- package/src/cli/ask-seed.test.ts +3 -3
- package/src/cli/build.ts +1 -1
- package/src/cli/db-auto-push.test.ts +6 -3
- package/src/cli/db-auto-push.ts +7 -5
- package/src/cli/db-seed.ts +7 -8
- package/src/cli/db.test.ts +70 -71
- package/src/cli/db.ts +86 -12
- package/src/cli/dev-controller.test.ts +35 -0
- package/src/cli/dev-controller.ts +298 -0
- package/src/cli/dev-controls.ts +11 -65
- package/src/cli/dev-db-push.test.ts +47 -15
- package/src/cli/dev-schema-sync.test.ts +11 -11
- package/src/cli/dev-schema-sync.ts +10 -10
- package/src/cli/dev-session-lock.test.ts +106 -0
- package/src/cli/dev-session-lock.ts +209 -0
- package/src/cli/dev.test.ts +166 -121
- package/src/cli/dev.ts +138 -192
- package/src/cli/doc-staleness.test.ts +3 -6
- package/src/cli/docker-clean.test.ts +33 -1
- package/src/cli/docker-clean.ts +28 -33
- package/src/cli/docker-cli.test.ts +12 -11
- package/src/cli/docker.ts +95 -20
- package/src/cli/doctor.test.ts +40 -0
- package/src/cli/doctor.ts +55 -15
- package/src/cli/drizzle-env.test.ts +11 -13
- package/src/cli/drizzle-env.ts +18 -11
- package/src/cli/ensure-drizzle-config.ts +11 -14
- package/src/cli/hero-meta.test.ts +15 -15
- package/src/cli/hero-meta.ts +10 -9
- package/src/cli/index.ts +36 -6
- package/src/cli/load-config.images.test.ts +16 -12
- package/src/cli/load-config.source.test.ts +50 -0
- package/src/cli/load-config.ts +34 -28
- package/src/cli/meilisearch-local.ts +1 -2
- package/src/cli/mode.ts +7 -58
- package/src/cli/registry.ts +175 -19
- package/src/cli/resolve-dev-sql-env.test.ts +8 -16
- package/src/cli/resolve-dev-sql-env.ts +13 -15
- package/src/cli/safe-defaults.test.ts +28 -10
- package/src/cli/schema.ts +65 -16
- package/src/cli/test.ts +58 -0
- package/src/cli/tui/App.tsx +252 -0
- package/src/cli/tui/DevLive.tsx +116 -0
- package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
- package/src/cli/tui/components/HeaderBar.tsx +67 -0
- package/src/cli/tui/components/LogStream.tsx +47 -0
- package/src/cli/tui/components/PanelCard.tsx +39 -0
- package/src/cli/tui/components/SelectRow.tsx +37 -0
- package/src/cli/tui/components/SlashPalette.tsx +172 -0
- package/src/cli/tui/components/Spinner.tsx +32 -0
- package/src/cli/tui/components/StatusBar.tsx +51 -0
- package/src/cli/tui/components/StatusDot.tsx +37 -0
- package/src/cli/tui/components/TabBar.tsx +49 -0
- package/src/cli/tui/components/Wordmark.tsx +43 -0
- package/src/cli/tui/format.ts +38 -0
- package/src/cli/tui/keys.test.ts +18 -0
- package/src/cli/tui/keys.ts +26 -0
- package/src/cli/tui/launch.test.ts +18 -0
- package/src/cli/tui/launch.ts +57 -0
- package/src/cli/tui/panels/Dashboard.tsx +201 -0
- package/src/cli/tui/panels/Database.tsx +198 -0
- package/src/cli/tui/panels/Dev.tsx +257 -0
- package/src/cli/tui/panels/Docker.tsx +357 -0
- package/src/cli/tui/panels/Navigator.test.ts +22 -0
- package/src/cli/tui/panels/Navigator.tsx +213 -0
- package/src/cli/tui/slash-catalog.test.ts +31 -0
- package/src/cli/tui/slash-catalog.ts +184 -0
- package/src/cli/tui/slash-run.ts +54 -0
- package/src/cli/tui/state/ports.ts +95 -0
- package/src/cli/tui/theme.ts +24 -0
- package/src/cli/vault-cmd.test.ts +402 -0
- package/src/cli/vault-cmd.ts +727 -69
- package/src/cli/vault-secure-input.test.ts +56 -0
- package/src/cli/vault-secure-input.ts +124 -0
- package/src/compiler/effects-infer.ts +7 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +3 -3
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler-entry.ts +7 -0
- package/src/config/define-config.test.ts +111 -47
- package/src/config/driver-defaults.test.ts +16 -23
- package/src/config/driver-defaults.ts +33 -63
- package/src/config/index.ts +169 -144
- package/src/config/resolve-driver.test.ts +14 -19
- package/src/console/server/app.ts +4 -2
- package/src/console/server/console.test.ts +58 -1
- package/src/console/server/flows.ts +22 -2
- package/src/console/server/index.ts +3 -0
- package/src/console/server/operator-db.test.ts +55 -13
- package/src/console/server/operator-db.ts +305 -263
- package/src/console/server/serve.ts +16 -7
- package/src/console/server/state.ts +31 -9
- package/src/console/server/vault.test.ts +120 -5
- package/src/console/server/vault.ts +208 -12
- package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
- package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
- package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/shell/client.ts +16 -0
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
- package/src/console/ui/vault/VaultA11yView.tsx +33 -1
- package/src/console/ui/vault/backend.test.ts +75 -0
- package/src/console/ui/vault/backend.ts +117 -0
- package/src/console/ui/vault/fixture.ts +21 -2
- package/src/console/ui/vault/index.ts +11 -1
- package/src/console/ui/vault/types.ts +26 -0
- package/src/docker/compose.ts +164 -61
- package/src/docker/derive.ts +106 -12
- package/src/docker/docker.test.ts +156 -110
- package/src/docker/helpers.ts +98 -0
- package/src/docker/images-config.test.ts +3 -8
- package/src/docker/index.ts +30 -5
- package/src/docker/recipes/index.ts +0 -3
- package/src/docker/recipes/llama-cpp.ts +4 -4
- package/src/docker/recipes/pgdog.ts +9 -2
- package/src/docker/resources.test.ts +71 -0
- package/src/docker/resources.ts +176 -0
- package/src/docker/stack-id.test.ts +11 -4
- package/src/docker/stack-id.ts +42 -32
- package/src/docker/stack.integration.test.ts +4 -15
- package/src/docker/types.ts +35 -3
- package/src/drivers/ai-ollama.integration.test.ts +2 -2
- package/src/drivers/conformance.test.ts +11 -12
- package/src/drivers/drizzle-dialect.test.ts +0 -4
- package/src/drivers/drizzle-dialect.ts +7 -13
- package/src/drivers/index.ts +24 -7
- package/src/drivers/journal-postgres.test.ts +3 -1
- package/src/drivers/journal-postgres.ts +4 -2
- package/src/drivers/meilisearch.ts +1 -1
- package/src/drivers/pglite.ts +28 -5
- package/src/drivers/types.ts +9 -11
- package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
- package/src/drivers/vault-aws-secrets-manager.ts +333 -0
- package/src/drivers/vault-builtin.test.ts +110 -0
- package/src/drivers/vault-builtin.ts +184 -0
- package/src/drivers/vault-driver-removal.test.ts +13 -0
- package/src/drivers/vault-managed.test.ts +95 -0
- package/src/drivers/vault-managed.ts +101 -25
- package/src/drivers/vault-types.ts +25 -5
- package/src/elements/ai/runtime.ts +1 -1
- package/src/elements/clock/chaos.test.ts +4 -1
- package/src/elements/clock/durable.ts +57 -4
- package/src/elements/clock.test.ts +13 -14
- package/src/elements/gate/config.ts +13 -3
- package/src/elements/gate/constants.ts +18 -0
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate/strategies.ts +2 -12
- package/src/elements/gate.test.ts +1 -1
- package/src/elements/gate.ts +2 -2
- package/src/elements/store/domain-ddl.test.ts +5 -12
- package/src/elements/store/emit-drizzle.ts +44 -19
- package/src/elements/store/index-boot.test.ts +15 -116
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/resource-list-docs.fixture.ts +12 -6
- package/src/elements/store/resource-list-docs.test.ts +64 -44
- package/src/elements/store/resource.test.ts +17 -10
- package/src/elements/store/runtime.ts +9 -12
- package/src/elements/store/schema-decl.test.ts +16 -32
- package/src/elements/store/seed.test.ts +1 -2
- package/src/elements/store/seed.ts +7 -8
- package/src/elements/store/sql-condition.test.ts +2 -2
- package/src/elements/store/sql-session.test.ts +8 -10
- package/src/elements/store/sql-session.ts +10 -2
- package/src/elements/store.test.ts +13 -13
- package/src/elements/vault/audit.ts +233 -0
- package/src/elements/vault/boot-chain.test.ts +78 -0
- package/src/elements/vault/boot-chain.ts +43 -39
- package/src/elements/vault/builtin-adapter.test.ts +479 -0
- package/src/elements/vault/builtin-adapter.ts +1184 -0
- package/src/elements/vault/crypto.test.ts +237 -0
- package/src/elements/vault/crypto.ts +431 -0
- package/src/elements/vault/declare.ts +164 -3
- package/src/elements/vault/errors.ts +56 -0
- package/src/elements/vault/kms-unseal.test.ts +76 -0
- package/src/elements/vault/kms-unseal.ts +181 -0
- package/src/elements/vault/path.ts +90 -0
- package/src/elements/vault/resilience.test.ts +102 -0
- package/src/elements/vault/resilience.ts +107 -0
- package/src/elements/vault/runtime.ts +36 -4
- package/src/elements/vault/security-checklist.test.ts +130 -0
- package/src/elements/vault/storage.ts +448 -0
- package/src/elements/vault/types.ts +288 -0
- package/src/elements/vault/unseal.ts +150 -0
- package/src/elements/vault.test.ts +72 -4
- package/src/elements/vault.ts +91 -2
- package/src/full.ts +248 -0
- package/src/http.ts +41 -0
- package/src/i18n/interpolate-simple.ts +37 -0
- package/src/i18n/messages.ts +51 -4
- package/src/i18n-entry.ts +7 -0
- package/src/index.ts +35 -145
- package/src/journal-entry.ts +20 -0
- package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
- package/src/kernel/app-auth.ts +98 -0
- package/src/kernel/app.ts +77 -110
- package/src/kernel/auto-registry.test.ts +2 -2
- package/src/kernel/boot-bind/ai.test.ts +5 -5
- package/src/kernel/boot-bind/ai.ts +2 -1
- package/src/kernel/boot-bind/channel.test.ts +6 -6
- package/src/kernel/boot-bind/channel.ts +3 -10
- package/src/kernel/boot-bind/clock.ts +2 -2
- package/src/kernel/boot-bind/gate.ts +5 -9
- package/src/kernel/boot-bind/honor-config.test.ts +42 -42
- package/src/kernel/boot-bind/journal.ts +2 -2
- package/src/kernel/boot-bind/store.test.ts +43 -44
- package/src/kernel/boot-bind/store.ts +16 -41
- package/src/kernel/boot-bind/vault.ts +16 -1
- package/src/kernel/boot.test.ts +13 -19
- package/src/kernel/boot.ts +31 -19
- package/src/kernel/budget-entry.ts +1 -1
- package/src/kernel/compensate.test.ts +288 -1
- package/src/kernel/compensate.ts +126 -0
- package/src/kernel/concurrency.test.ts +10 -9
- package/src/kernel/effects-stamping.test.ts +27 -2
- package/src/kernel/element-registries.ts +5 -3
- package/src/kernel/flow.ts +4 -2
- package/src/kernel/fx.test.ts +119 -3
- package/src/kernel/fx.ts +215 -40
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal.ts +130 -7
- package/src/kernel/pipeline.test.ts +20 -6
- package/src/kernel/redacted.test.ts +35 -0
- package/src/kernel/redacted.ts +24 -2
- package/src/kernel/router/create-default.ts +16 -0
- package/src/kernel/router/create-edge.ts +18 -0
- package/src/kernel/router/create.ts +23 -0
- package/src/kernel/router/index.ts +23 -0
- package/src/kernel/router/linear.ts +88 -0
- package/src/kernel/router/regexp.ts +188 -0
- package/src/kernel/router/smart.ts +120 -0
- package/src/kernel/router/trie.ts +108 -0
- package/src/kernel/router/types.ts +56 -0
- package/src/kernel/router.ts +18 -532
- package/src/kernel-entry.ts +43 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/plugins/config-source.test.ts +7 -7
- package/src/release/build-lib.ts +61 -0
- package/src/release/exports.test.ts +4 -2
- package/src/release/exports.ts +30 -4
- package/src/release/measure.ts +101 -22
- package/src/runs/duckdb.ts +7 -2
- package/src/shared/lazy-src.ts +79 -0
- package/src/shared/optional-peer.ts +41 -0
- package/src/term.test.ts +3 -44
- package/src/test/create-test-app.ts +12 -0
- package/src/test/provisions.integration.test.ts +1 -1
- package/src/test/reset-element-registries.ts +2 -0
- package/src/upgrade/codemods.test.ts +7 -20
- package/src/upgrade/codemods.ts +93 -60
- package/site/content/docs/recipes/openbao.mdx +0 -141
- package/src/cli/ask-dev-mode.ts +0 -40
- package/src/cli/dev-mode.test.ts +0 -45
- package/src/cli/dev-mode.ts +0 -78
- package/src/cli/openbao-bootstrap.test.ts +0 -150
- package/src/cli/openbao-bootstrap.ts +0 -321
- package/src/cli/openbao-restart.integration.test.ts +0 -145
- package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
- package/src/docker/recipes/openbao.ts +0 -47
- package/src/drivers/libsql.ts +0 -179
- package/src/drivers/sqlite.ts +0 -44
- package/src/drivers/vault-openbao.test.ts +0 -97
- package/src/drivers/vault-openbao.ts +0 -143
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker panel — live stack by project name; clean tears down whole projects.
|
|
3
|
+
*
|
|
4
|
+
* Never shells into interactive `oke docker clean` (clack) while Ink is mounted —
|
|
5
|
+
* that fights stdout and leaves ghost frames.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Box, Text, useInput } from "ink";
|
|
9
|
+
import { useEffect, useState, type ReactElement } from "react";
|
|
10
|
+
import type { DevStatus } from "../../../term.ts";
|
|
11
|
+
import { composeRowToStatus } from "../../../docker/compose-health.ts";
|
|
12
|
+
import {
|
|
13
|
+
composeProjectName,
|
|
14
|
+
downStack,
|
|
15
|
+
listOkeComposeProjects,
|
|
16
|
+
listStackContainers,
|
|
17
|
+
type OkeStack,
|
|
18
|
+
} from "../../../docker/cleanup.ts";
|
|
19
|
+
import { ConfirmDialog } from "../components/ConfirmDialog.tsx";
|
|
20
|
+
import type { FooterStatus } from "../components/StatusBar.tsx";
|
|
21
|
+
import { StatusDot, statusWord } from "../components/StatusDot.tsx";
|
|
22
|
+
import { TUI_ACCENT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
|
|
23
|
+
|
|
24
|
+
/** Props for {@link DockerPanel}. */
|
|
25
|
+
export type DockerPanelProps = {
|
|
26
|
+
readonly cwd: string;
|
|
27
|
+
readonly active: boolean;
|
|
28
|
+
readonly onLog: (line: string) => void;
|
|
29
|
+
readonly onStatus: (status: FooterStatus) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type DockerAction = "derive" | "clean" | "restart";
|
|
33
|
+
|
|
34
|
+
type ServiceRow = {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly status: DevStatus;
|
|
37
|
+
readonly detail: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Mode = "main" | "pickClean" | "confirmClean";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compose-oriented panel.
|
|
44
|
+
*
|
|
45
|
+
* @param props - cwd + log
|
|
46
|
+
*/
|
|
47
|
+
export function DockerPanel(props: DockerPanelProps): ReactElement {
|
|
48
|
+
const project = composeProjectName(props.cwd);
|
|
49
|
+
const [rows, setRows] = useState<readonly ServiceRow[]>([]);
|
|
50
|
+
const [stacks, setStacks] = useState<readonly OkeStack[]>([]);
|
|
51
|
+
const [selected, setSelected] = useState(0);
|
|
52
|
+
const [mode, setMode] = useState<Mode>("main");
|
|
53
|
+
const [cleanFocus, setCleanFocus] = useState(0);
|
|
54
|
+
const [cleanPicked, setCleanPicked] = useState<ReadonlySet<string>>(() => new Set());
|
|
55
|
+
const [busy, setBusy] = useState(false);
|
|
56
|
+
|
|
57
|
+
const actions: readonly { id: DockerAction; label: string }[] = [
|
|
58
|
+
{ id: "derive", label: "[D]erive" },
|
|
59
|
+
{ id: "clean", label: "[C]lean" },
|
|
60
|
+
{ id: "restart", label: "[R]estart stack" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const refresh = async (): Promise<void> => {
|
|
64
|
+
try {
|
|
65
|
+
const containers = await listStackContainers(project);
|
|
66
|
+
setRows(
|
|
67
|
+
containers.map((c) => ({
|
|
68
|
+
name: c.service || c.name,
|
|
69
|
+
status: composeRowToStatus({
|
|
70
|
+
Service: c.service,
|
|
71
|
+
State: c.state,
|
|
72
|
+
Status: c.state,
|
|
73
|
+
}),
|
|
74
|
+
detail: c.state || c.name,
|
|
75
|
+
})),
|
|
76
|
+
);
|
|
77
|
+
const all = await listOkeComposeProjects();
|
|
78
|
+
setStacks(all);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
props.onLog(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
void refresh();
|
|
86
|
+
const id = setInterval(() => void refresh(), 4000);
|
|
87
|
+
return () => clearInterval(id);
|
|
88
|
+
}, [props.cwd]);
|
|
89
|
+
|
|
90
|
+
useInput(
|
|
91
|
+
(input, key) => {
|
|
92
|
+
if (!props.active || busy) return;
|
|
93
|
+
|
|
94
|
+
if (mode === "pickClean") {
|
|
95
|
+
if (key.escape) {
|
|
96
|
+
setMode("main");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (key.upArrow) setCleanFocus((i) => Math.max(0, i - 1));
|
|
100
|
+
if (key.downArrow) setCleanFocus((i) => Math.min(Math.max(0, stacks.length - 1), i + 1));
|
|
101
|
+
if (input === " ") {
|
|
102
|
+
const stack = stacks[cleanFocus];
|
|
103
|
+
if (!stack) return;
|
|
104
|
+
setCleanPicked((prev) => {
|
|
105
|
+
const next = new Set(prev);
|
|
106
|
+
if (next.has(stack.project)) next.delete(stack.project);
|
|
107
|
+
else next.add(stack.project);
|
|
108
|
+
return next;
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (key.return) {
|
|
113
|
+
if (cleanPicked.size === 0) return;
|
|
114
|
+
setMode("confirmClean");
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (mode !== "main") return;
|
|
120
|
+
|
|
121
|
+
if (key.upArrow) setSelected((i) => Math.max(0, i - 1));
|
|
122
|
+
if (key.downArrow) setSelected((i) => Math.min(actions.length - 1, i + 1));
|
|
123
|
+
const lower = input.toLowerCase();
|
|
124
|
+
if (lower === "d") void run("derive");
|
|
125
|
+
else if (lower === "c") void beginClean();
|
|
126
|
+
else if (lower === "r") void run("restart");
|
|
127
|
+
else if (key.return) {
|
|
128
|
+
const a = actions[selected];
|
|
129
|
+
if (!a) return;
|
|
130
|
+
if (a.id === "clean") void beginClean();
|
|
131
|
+
else void run(a.id);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{ isActive: props.active && mode !== "confirmClean" },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const beginClean = async (): Promise<void> => {
|
|
138
|
+
try {
|
|
139
|
+
const all = await listOkeComposeProjects();
|
|
140
|
+
setStacks(all);
|
|
141
|
+
if (all.length === 0) {
|
|
142
|
+
props.onStatus({ kind: "done", label: "No oke-dev-* stacks" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const initial = new Set<string>();
|
|
146
|
+
if (all.some((s) => s.project === project)) initial.add(project);
|
|
147
|
+
else if (all[0]) initial.add(all[0].project);
|
|
148
|
+
setCleanPicked(initial);
|
|
149
|
+
setCleanFocus(
|
|
150
|
+
Math.max(
|
|
151
|
+
0,
|
|
152
|
+
all.findIndex((s) => s.project === project),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
setMode("pickClean");
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
158
|
+
props.onLog(`${m}\n`);
|
|
159
|
+
props.onStatus({ kind: "done", label: m });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const runClean = async (): Promise<void> => {
|
|
164
|
+
setBusy(true);
|
|
165
|
+
setMode("main");
|
|
166
|
+
const targets = [...cleanPicked].sort();
|
|
167
|
+
props.onStatus({
|
|
168
|
+
kind: "running",
|
|
169
|
+
label: `Removing ${targets.length} stack(s)…`,
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
for (const p of targets) {
|
|
173
|
+
props.onLog(`oke docker clean: down -v ${p}\n`);
|
|
174
|
+
await downStack(p);
|
|
175
|
+
}
|
|
176
|
+
props.onStatus({
|
|
177
|
+
kind: "done",
|
|
178
|
+
label: `Removed ${targets.length} stack(s)`,
|
|
179
|
+
});
|
|
180
|
+
await refresh();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
183
|
+
props.onLog(`${m}\n`);
|
|
184
|
+
props.onStatus({ kind: "done", label: m });
|
|
185
|
+
} finally {
|
|
186
|
+
setBusy(false);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const run = async (id: Exclude<DockerAction, "clean">): Promise<void> => {
|
|
191
|
+
setBusy(true);
|
|
192
|
+
const labels = {
|
|
193
|
+
derive: "Running oke docker…",
|
|
194
|
+
restart: `Restarting ${project}…`,
|
|
195
|
+
} as const;
|
|
196
|
+
props.onStatus({ kind: "running", label: labels[id] });
|
|
197
|
+
try {
|
|
198
|
+
if (id === "derive") {
|
|
199
|
+
const { dockerCli } = await import("../../docker.ts");
|
|
200
|
+
const prev = process.cwd();
|
|
201
|
+
process.chdir(props.cwd);
|
|
202
|
+
let code = 1;
|
|
203
|
+
try {
|
|
204
|
+
code = await dockerCli([]);
|
|
205
|
+
} finally {
|
|
206
|
+
process.chdir(prev);
|
|
207
|
+
}
|
|
208
|
+
props.onLog(`oke docker → ${code}\n`);
|
|
209
|
+
props.onStatus({
|
|
210
|
+
kind: "done",
|
|
211
|
+
label: code === 0 ? "Derive ok" : `Derive exited ${code}`,
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
const proc = Bun.spawn(["docker", "compose", "-p", project, "restart"], {
|
|
215
|
+
stdout: "pipe",
|
|
216
|
+
stderr: "pipe",
|
|
217
|
+
});
|
|
218
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
219
|
+
new Response(proc.stdout).text(),
|
|
220
|
+
new Response(proc.stderr).text(),
|
|
221
|
+
proc.exited,
|
|
222
|
+
]);
|
|
223
|
+
if (stdout) props.onLog(stdout);
|
|
224
|
+
if (stderr) props.onLog(stderr);
|
|
225
|
+
props.onStatus({
|
|
226
|
+
kind: "done",
|
|
227
|
+
label: code === 0 ? "Stack restarted" : `Restart exited ${code}`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
await refresh();
|
|
231
|
+
} finally {
|
|
232
|
+
setBusy(false);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (mode === "confirmClean") {
|
|
237
|
+
const list = [...cleanPicked].sort().join(", ");
|
|
238
|
+
return (
|
|
239
|
+
<ConfirmDialog
|
|
240
|
+
message={`Remove stack(s) — containers, networks, volumes?\n${list}`}
|
|
241
|
+
onConfirm={() => void runClean()}
|
|
242
|
+
onCancel={() => setMode("pickClean")}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (mode === "pickClean") {
|
|
248
|
+
return (
|
|
249
|
+
<Box flexDirection="column" gap={1} flexGrow={1}>
|
|
250
|
+
<Text bold color={TUI_ACCENT}>
|
|
251
|
+
Clean stacks
|
|
252
|
+
</Text>
|
|
253
|
+
<Text color={TUI_MUTED} dimColor>
|
|
254
|
+
Space toggle · Enter confirm · Esc cancel — whole project, not per-service
|
|
255
|
+
</Text>
|
|
256
|
+
{stacks.length === 0 ? (
|
|
257
|
+
<Text color={TUI_WARN}>No oke-dev-* stacks on this machine</Text>
|
|
258
|
+
) : (
|
|
259
|
+
stacks.map((s, i) => {
|
|
260
|
+
const on = i === cleanFocus;
|
|
261
|
+
const picked = cleanPicked.has(s.project);
|
|
262
|
+
const n = s.containers.length;
|
|
263
|
+
const current = s.project === project;
|
|
264
|
+
return (
|
|
265
|
+
<Text key={s.project} bold={on} color={on ? TUI_ACCENT : undefined}>
|
|
266
|
+
{on ? "› " : " "}
|
|
267
|
+
{picked ? "●" : "○"} {s.project}
|
|
268
|
+
<Text color={TUI_MUTED}>
|
|
269
|
+
{` ${n} container${n === 1 ? "" : "s"}`}
|
|
270
|
+
{current ? " · this project" : ""}
|
|
271
|
+
</Text>
|
|
272
|
+
</Text>
|
|
273
|
+
);
|
|
274
|
+
})
|
|
275
|
+
)}
|
|
276
|
+
</Box>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const online = rows.filter((r) => r.status === "ready").length;
|
|
281
|
+
const total = rows.length;
|
|
282
|
+
const allOnline = total > 0 && online === total;
|
|
283
|
+
const stackKnown = stacks.some((s) => s.project === project);
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<Box flexDirection="column" gap={1} flexGrow={1}>
|
|
287
|
+
<Text bold color={TUI_ACCENT}>
|
|
288
|
+
Docker Compose
|
|
289
|
+
</Text>
|
|
290
|
+
<Box gap={1}>
|
|
291
|
+
<StatusDot status={allOnline ? "ready" : total === 0 ? "idle" : "pending"} />
|
|
292
|
+
<Text bold color={allOnline ? TUI_OK : TUI_WARN}>
|
|
293
|
+
{total === 0
|
|
294
|
+
? stackKnown
|
|
295
|
+
? `0 services · ${project}`
|
|
296
|
+
: `No stack · ${project}`
|
|
297
|
+
: `${online}/${total} services online`}
|
|
298
|
+
</Text>
|
|
299
|
+
{total > 0 ? <Text color={TUI_MUTED}>· {project}</Text> : null}
|
|
300
|
+
</Box>
|
|
301
|
+
|
|
302
|
+
<Box>
|
|
303
|
+
<Box width={22}>
|
|
304
|
+
<Text bold color={TUI_MUTED}>
|
|
305
|
+
Service
|
|
306
|
+
</Text>
|
|
307
|
+
</Box>
|
|
308
|
+
<Box width={10}>
|
|
309
|
+
<Text bold color={TUI_MUTED}>
|
|
310
|
+
Status
|
|
311
|
+
</Text>
|
|
312
|
+
</Box>
|
|
313
|
+
<Text bold color={TUI_MUTED}>
|
|
314
|
+
Detail
|
|
315
|
+
</Text>
|
|
316
|
+
</Box>
|
|
317
|
+
<Text color={TUI_MUTED}>{"─".repeat(48)}</Text>
|
|
318
|
+
|
|
319
|
+
{rows.length === 0 ? (
|
|
320
|
+
<Text color={TUI_MUTED}>
|
|
321
|
+
No containers for {project}. Start `oke dev` or run [D]erive + compose up.
|
|
322
|
+
</Text>
|
|
323
|
+
) : (
|
|
324
|
+
rows.map((r) => {
|
|
325
|
+
const sw = statusWord(r.status);
|
|
326
|
+
return (
|
|
327
|
+
<Box key={r.name}>
|
|
328
|
+
<Box width={22} gap={1}>
|
|
329
|
+
<StatusDot status={r.status} />
|
|
330
|
+
<Text>{r.name}</Text>
|
|
331
|
+
</Box>
|
|
332
|
+
<Box width={10}>
|
|
333
|
+
<Text color={sw.color}>{sw.word}</Text>
|
|
334
|
+
</Box>
|
|
335
|
+
<Text color={TUI_MUTED}>{r.detail}</Text>
|
|
336
|
+
</Box>
|
|
337
|
+
);
|
|
338
|
+
})
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
<Box gap={2} marginTop={1}>
|
|
342
|
+
{actions.map((a, i) => (
|
|
343
|
+
<Text
|
|
344
|
+
key={a.id}
|
|
345
|
+
bold={props.active && i === selected}
|
|
346
|
+
color={
|
|
347
|
+
props.active && i === selected ? TUI_ACCENT : a.id === "clean" ? TUI_WARN : undefined
|
|
348
|
+
}
|
|
349
|
+
>
|
|
350
|
+
{props.active && i === selected ? "› " : " "}
|
|
351
|
+
{a.label}
|
|
352
|
+
</Text>
|
|
353
|
+
))}
|
|
354
|
+
</Box>
|
|
355
|
+
</Box>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Manifest } from "../../../manifest/types.ts";
|
|
3
|
+
import { manifestToNavTree } from "./Navigator.tsx";
|
|
4
|
+
|
|
5
|
+
describe("manifestToNavTree", () => {
|
|
6
|
+
test("builds element sections from a Manifest", () => {
|
|
7
|
+
const manifest = {
|
|
8
|
+
oke: "1.0",
|
|
9
|
+
app: "demo",
|
|
10
|
+
flows: { hello: {} },
|
|
11
|
+
signals: { ping: {} },
|
|
12
|
+
stores: {},
|
|
13
|
+
ai: { models: { m1: {} }, prompts: {}, agents: {} },
|
|
14
|
+
} as unknown as Manifest;
|
|
15
|
+
const tree = manifestToNavTree(manifest);
|
|
16
|
+
expect(tree.some((n) => n.label.includes("app: demo"))).toBe(true);
|
|
17
|
+
const flows = tree.find((n) => n.id === "flows");
|
|
18
|
+
expect(flows?.children?.some((c) => c.label === "hello")).toBe(true);
|
|
19
|
+
const ai = tree.find((n) => n.id === "ai");
|
|
20
|
+
expect(ai?.children?.some((c) => c.id === "ai.models")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Navigator — Manifest tree with expand/collapse.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text, useInput } from "ink";
|
|
6
|
+
import { useEffect, useState, type ReactElement } from "react";
|
|
7
|
+
import type { Manifest } from "../../../manifest/types.ts";
|
|
8
|
+
import { TUI_ACCENT, TUI_ERR, TUI_MUTED } from "../theme.ts";
|
|
9
|
+
|
|
10
|
+
/** Props for {@link NavigatorPanel}. */
|
|
11
|
+
export type NavigatorPanelProps = {
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
readonly active: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type NavNode = {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly kind?: "category" | "item" | "meta";
|
|
20
|
+
readonly count?: number;
|
|
21
|
+
readonly children?: readonly NavNode[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a tree from a Manifest.
|
|
26
|
+
*
|
|
27
|
+
* @param manifest - Parsed/extracted manifest
|
|
28
|
+
*/
|
|
29
|
+
export function manifestToNavTree(manifest: Manifest): readonly NavNode[] {
|
|
30
|
+
const section = (title: string, record: Record<string, unknown> | undefined): NavNode => {
|
|
31
|
+
const keys = record ? Object.keys(record).sort() : [];
|
|
32
|
+
return {
|
|
33
|
+
id: title,
|
|
34
|
+
label: title,
|
|
35
|
+
kind: "category",
|
|
36
|
+
count: keys.length,
|
|
37
|
+
children: keys.map((name) => ({
|
|
38
|
+
id: `${title}:${name}`,
|
|
39
|
+
label: name,
|
|
40
|
+
kind: "item" as const,
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ai = manifest.ai;
|
|
46
|
+
const aiChildren: NavNode[] = [];
|
|
47
|
+
if (ai?.models) {
|
|
48
|
+
const keys = Object.keys(ai.models);
|
|
49
|
+
aiChildren.push({
|
|
50
|
+
id: "ai.models",
|
|
51
|
+
label: "models",
|
|
52
|
+
kind: "category",
|
|
53
|
+
count: keys.length,
|
|
54
|
+
children: keys.map((n) => ({ id: `ai.models:${n}`, label: n, kind: "item" })),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (ai?.prompts) {
|
|
58
|
+
const keys = Object.keys(ai.prompts);
|
|
59
|
+
aiChildren.push({
|
|
60
|
+
id: "ai.prompts",
|
|
61
|
+
label: "prompts",
|
|
62
|
+
kind: "category",
|
|
63
|
+
count: keys.length,
|
|
64
|
+
children: keys.map((n) => ({ id: `ai.prompts:${n}`, label: n, kind: "item" })),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (ai?.agents) {
|
|
68
|
+
const keys = Object.keys(ai.agents);
|
|
69
|
+
aiChildren.push({
|
|
70
|
+
id: "ai.agents",
|
|
71
|
+
label: "agents",
|
|
72
|
+
kind: "category",
|
|
73
|
+
count: keys.length,
|
|
74
|
+
children: keys.map((n) => ({ id: `ai.agents:${n}`, label: n, kind: "item" })),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
{ id: "app", label: `app: ${manifest.app}`, kind: "meta" },
|
|
80
|
+
section("flows", manifest.flows as Record<string, unknown> | undefined),
|
|
81
|
+
section("signals", manifest.signals as Record<string, unknown> | undefined),
|
|
82
|
+
section("stores", manifest.stores as Record<string, unknown> | undefined),
|
|
83
|
+
section("clocks", manifest.clocks as Record<string, unknown> | undefined),
|
|
84
|
+
section("gates", manifest.gates as Record<string, unknown> | undefined),
|
|
85
|
+
section("vault", manifest.vault as Record<string, unknown> | undefined),
|
|
86
|
+
section("channels", manifest.channels as Record<string, unknown> | undefined),
|
|
87
|
+
{
|
|
88
|
+
id: "ai",
|
|
89
|
+
label: "ai",
|
|
90
|
+
kind: "category",
|
|
91
|
+
count: aiChildren.length,
|
|
92
|
+
children: aiChildren,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Flatten tree for arrow-key navigation.
|
|
99
|
+
*
|
|
100
|
+
* @param nodes - Roots
|
|
101
|
+
* @param expanded - Expanded ids
|
|
102
|
+
*/
|
|
103
|
+
function flatten(
|
|
104
|
+
nodes: readonly NavNode[],
|
|
105
|
+
expanded: ReadonlySet<string>,
|
|
106
|
+
depth = 0,
|
|
107
|
+
): readonly { node: NavNode; depth: number }[] {
|
|
108
|
+
const out: { node: NavNode; depth: number }[] = [];
|
|
109
|
+
for (const node of nodes) {
|
|
110
|
+
out.push({ node, depth });
|
|
111
|
+
if (node.children && expanded.has(node.id)) {
|
|
112
|
+
out.push(...flatten(node.children, expanded, depth + 1));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Manifest browser.
|
|
120
|
+
*
|
|
121
|
+
* @param props - cwd
|
|
122
|
+
*/
|
|
123
|
+
export function NavigatorPanel(props: NavigatorPanelProps): ReactElement {
|
|
124
|
+
const [tree, setTree] = useState<readonly NavNode[]>([]);
|
|
125
|
+
const [expanded, setExpanded] = useState<Set<string>>(
|
|
126
|
+
() => new Set(["flows", "signals", "stores"]),
|
|
127
|
+
);
|
|
128
|
+
const [selected, setSelected] = useState(0);
|
|
129
|
+
const [error, setError] = useState("");
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
let cancelled = false;
|
|
133
|
+
void (async () => {
|
|
134
|
+
try {
|
|
135
|
+
const { extractManifest } = await import("../../../compiler/extract.ts");
|
|
136
|
+
const manifest = await extractManifest({ rootDir: props.cwd });
|
|
137
|
+
if (!cancelled) {
|
|
138
|
+
setTree(manifestToNavTree(manifest));
|
|
139
|
+
setError("");
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
|
|
143
|
+
}
|
|
144
|
+
})();
|
|
145
|
+
return () => {
|
|
146
|
+
cancelled = true;
|
|
147
|
+
};
|
|
148
|
+
}, [props.cwd]);
|
|
149
|
+
|
|
150
|
+
const rows = flatten(tree, expanded);
|
|
151
|
+
|
|
152
|
+
const toggle = (expand: boolean | "toggle"): void => {
|
|
153
|
+
const row = rows[selected];
|
|
154
|
+
if (!row?.node.children?.length) return;
|
|
155
|
+
setExpanded((prev) => {
|
|
156
|
+
const next = new Set(prev);
|
|
157
|
+
if (expand === true) next.add(row.node.id);
|
|
158
|
+
else if (expand === false) next.delete(row.node.id);
|
|
159
|
+
else if (next.has(row.node.id)) next.delete(row.node.id);
|
|
160
|
+
else next.add(row.node.id);
|
|
161
|
+
return next;
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
useInput(
|
|
166
|
+
(_input, key) => {
|
|
167
|
+
if (!props.active) return;
|
|
168
|
+
if (key.upArrow) setSelected((i) => Math.max(0, i - 1));
|
|
169
|
+
if (key.downArrow) setSelected((i) => Math.min(Math.max(0, rows.length - 1), i + 1));
|
|
170
|
+
if (key.rightArrow) toggle(true);
|
|
171
|
+
if (key.leftArrow) toggle(false);
|
|
172
|
+
if (key.return) toggle("toggle");
|
|
173
|
+
},
|
|
174
|
+
{ isActive: props.active },
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Box flexDirection="column" gap={0} flexGrow={1}>
|
|
179
|
+
<Text bold color={TUI_ACCENT}>
|
|
180
|
+
Project Navigator
|
|
181
|
+
</Text>
|
|
182
|
+
<Text color={TUI_MUTED} dimColor>
|
|
183
|
+
Manifest tree · ↑↓ · → expand · ← collapse · Enter toggle
|
|
184
|
+
</Text>
|
|
185
|
+
{error ? <Text color={TUI_ERR}>{error}</Text> : null}
|
|
186
|
+
{rows.map((row, i) => {
|
|
187
|
+
const hasKids = Boolean(row.node.children?.length);
|
|
188
|
+
const open = expanded.has(row.node.id);
|
|
189
|
+
const branch = hasKids ? (open ? "▾ " : "▸ ") : " ";
|
|
190
|
+
const on = props.active && i === selected;
|
|
191
|
+
const isCategory = row.node.kind === "category";
|
|
192
|
+
return (
|
|
193
|
+
<Text
|
|
194
|
+
key={row.node.id}
|
|
195
|
+
bold={on || isCategory}
|
|
196
|
+
color={on ? TUI_ACCENT : isCategory ? TUI_ACCENT : undefined}
|
|
197
|
+
>
|
|
198
|
+
{" ".repeat(row.depth)}
|
|
199
|
+
{on ? "› " : " "}
|
|
200
|
+
{branch}
|
|
201
|
+
{row.node.label}
|
|
202
|
+
{typeof row.node.count === "number" ? (
|
|
203
|
+
<Text color={TUI_MUTED}>{` (${row.node.count})`}</Text>
|
|
204
|
+
) : null}
|
|
205
|
+
</Text>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
{rows.length === 0 && !error ? (
|
|
209
|
+
<Text color={TUI_MUTED}>Loading Manifest… extract from src/ or oke.manifest.json</Text>
|
|
210
|
+
) : null}
|
|
211
|
+
</Box>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { commonPrefix, filterSlashActions, parseSlashArgv, slashCatalog } from "./slash-catalog.ts";
|
|
3
|
+
|
|
4
|
+
describe("slashCatalog", () => {
|
|
5
|
+
test("includes panel jumps and core cli commands", () => {
|
|
6
|
+
const all = slashCatalog();
|
|
7
|
+
expect(all.some((a) => a.id === "panel dashboard")).toBe(true);
|
|
8
|
+
expect(all.some((a) => a.id === "dev")).toBe(true);
|
|
9
|
+
expect(all.some((a) => a.id === "db push")).toBe(true);
|
|
10
|
+
expect(all.some((a) => a.id === "db studio")).toBe(true);
|
|
11
|
+
expect(all.some((a) => a.id === "doctor")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("filterSlashActions prefixes db", () => {
|
|
15
|
+
const hits = filterSlashActions("db", 20);
|
|
16
|
+
expect(hits.length).toBeGreaterThan(0);
|
|
17
|
+
expect(hits.every((h) => h.id.includes("db") || h.summary.toLowerCase().includes("db"))).toBe(
|
|
18
|
+
true,
|
|
19
|
+
);
|
|
20
|
+
expect(hits[0]?.id.startsWith("db")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("filterSlashActions empty returns top slice", () => {
|
|
24
|
+
expect(filterSlashActions("", 5)).toHaveLength(5);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("commonPrefix and parseSlashArgv", () => {
|
|
28
|
+
expect(commonPrefix(["db push", "db studio", "db seed"])).toBe("db ");
|
|
29
|
+
expect(parseSlashArgv(" db push ")).toEqual(["db", "push"]);
|
|
30
|
+
});
|
|
31
|
+
});
|