okengine 0.10.3 → 0.11.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/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/constants.ts +18 -0
- 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.ts +40 -42
- 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 +12 -4
- 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 +58 -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/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
package/src/cli/test.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke test` — run `bun test` with PGLite test posture (no Docker Compose).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CLI entry for `oke test`.
|
|
7
|
+
*
|
|
8
|
+
* Sets `NODE_ENV=test` and a default in-memory PGLite URL, verifies
|
|
9
|
+
* `@electric-sql/pglite` resolves, then forwards remaining argv to `bun test`.
|
|
10
|
+
*
|
|
11
|
+
* @param args - Args after `test` (forwarded to `bun test`)
|
|
12
|
+
*/
|
|
13
|
+
export async function testCli(args: readonly string[]): Promise<number> {
|
|
14
|
+
for (const a of args) {
|
|
15
|
+
if (a === "--help" || a === "-h") {
|
|
16
|
+
console.log(`oke test [...bun test args]
|
|
17
|
+
|
|
18
|
+
Run bun test with PGLite test posture (NODE_ENV=test).
|
|
19
|
+
Does not start Docker Compose.
|
|
20
|
+
|
|
21
|
+
Environment (set if unset):
|
|
22
|
+
NODE_ENV=test
|
|
23
|
+
OKE_PGLITE_URL=memory://
|
|
24
|
+
`);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (process.env.NODE_ENV === undefined || process.env.NODE_ENV === "") {
|
|
30
|
+
process.env.NODE_ENV = "test";
|
|
31
|
+
}
|
|
32
|
+
if (process.env.OKE_PGLITE_URL === undefined || process.env.OKE_PGLITE_URL === "") {
|
|
33
|
+
process.env.OKE_PGLITE_URL = "memory://";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await import("@electric-sql/pglite");
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
Bun.resolveSync("@electric-sql/pglite", process.cwd());
|
|
41
|
+
} catch {
|
|
42
|
+
console.error(
|
|
43
|
+
"oke test: `@electric-sql/pglite` is not installed — add it as a dependency (okengine peers it for the pglite SQL driver)",
|
|
44
|
+
);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const proc = Bun.spawn(["bun", "test", ...args], {
|
|
50
|
+
cwd: process.cwd(),
|
|
51
|
+
env: process.env as Record<string, string>,
|
|
52
|
+
stdin: "inherit",
|
|
53
|
+
stdout: "inherit",
|
|
54
|
+
stderr: "inherit",
|
|
55
|
+
});
|
|
56
|
+
const code = await proc.exited;
|
|
57
|
+
return code ?? 1;
|
|
58
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive `oke` shell — brand chrome, focus layers, `/` palette.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, useApp, useInput } from "ink";
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
|
|
7
|
+
import { DevController } from "../dev-controller.ts";
|
|
8
|
+
import type { DevOwnership } from "../dev-session-lock.ts";
|
|
9
|
+
import { ConfirmDialog } from "./components/ConfirmDialog.tsx";
|
|
10
|
+
import { HeaderBar } from "./components/HeaderBar.tsx";
|
|
11
|
+
import { SlashPalette } from "./components/SlashPalette.tsx";
|
|
12
|
+
import { StatusBar, type FooterStatus } from "./components/StatusBar.tsx";
|
|
13
|
+
import { TabBar } from "./components/TabBar.tsx";
|
|
14
|
+
import { DashboardPanel } from "./panels/Dashboard.tsx";
|
|
15
|
+
import { DatabasePanel } from "./panels/Database.tsx";
|
|
16
|
+
import { DevPanel } from "./panels/Dev.tsx";
|
|
17
|
+
import { DockerPanel } from "./panels/Docker.tsx";
|
|
18
|
+
import { NavigatorPanel } from "./panels/Navigator.tsx";
|
|
19
|
+
import { runSlashCli } from "./slash-run.ts";
|
|
20
|
+
import { pollPortSnapshot } from "./state/ports.ts";
|
|
21
|
+
import { TUI_MUTED } from "./theme.ts";
|
|
22
|
+
|
|
23
|
+
const PANELS = ["dashboard", "dev", "database", "docker", "navigator"] as const;
|
|
24
|
+
type PanelId = (typeof PANELS)[number];
|
|
25
|
+
|
|
26
|
+
const TABS = [
|
|
27
|
+
{ id: "dashboard", label: "Dashboard" },
|
|
28
|
+
{ id: "dev", label: "Dev" },
|
|
29
|
+
{ id: "database", label: "Database" },
|
|
30
|
+
{ id: "docker", label: "Docker" },
|
|
31
|
+
{ id: "navigator", label: "Navigator" },
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** Props for {@link OkeTuiApp}. */
|
|
35
|
+
export type OkeTuiAppProps = {
|
|
36
|
+
readonly cwd: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Root Ink application.
|
|
41
|
+
*
|
|
42
|
+
* @param props - cwd
|
|
43
|
+
*/
|
|
44
|
+
export function OkeTuiApp(props: OkeTuiAppProps): ReactElement {
|
|
45
|
+
const { exit } = useApp();
|
|
46
|
+
const [panelIndex, setPanelIndex] = useState(0);
|
|
47
|
+
const [focusLayer, setFocusLayer] = useState<"tabs" | "panel">("tabs");
|
|
48
|
+
const [logs, setLogs] = useState<string[]>([]);
|
|
49
|
+
const [slashOpen, setSlashOpen] = useState(false);
|
|
50
|
+
const [slashBusy, setSlashBusy] = useState(false);
|
|
51
|
+
const [quitConfirm, setQuitConfirm] = useState(false);
|
|
52
|
+
const [ownership, setOwnership] = useState<DevOwnership>("stopped");
|
|
53
|
+
const [footerStatus, setFooterStatus] = useState<FooterStatus>({ kind: "idle" });
|
|
54
|
+
const doneTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
55
|
+
const controller = useMemo(() => new DevController(), []);
|
|
56
|
+
|
|
57
|
+
const onLog = useCallback((line: string) => {
|
|
58
|
+
setLogs((prev) => {
|
|
59
|
+
const next = [...prev, ...line.split("\n").filter((l) => l.length > 0)];
|
|
60
|
+
return next.slice(-200);
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const onStatus = useCallback((status: FooterStatus) => {
|
|
65
|
+
if (doneTimer.current) {
|
|
66
|
+
clearTimeout(doneTimer.current);
|
|
67
|
+
doneTimer.current = null;
|
|
68
|
+
}
|
|
69
|
+
setFooterStatus(status);
|
|
70
|
+
if (status.kind === "done") {
|
|
71
|
+
doneTimer.current = setTimeout(() => {
|
|
72
|
+
setFooterStatus({ kind: "idle" });
|
|
73
|
+
doneTimer.current = null;
|
|
74
|
+
}, 2000);
|
|
75
|
+
}
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const tick = (): void => {
|
|
80
|
+
void pollPortSnapshot(props.cwd).then((s) => setOwnership(s.ownership));
|
|
81
|
+
};
|
|
82
|
+
tick();
|
|
83
|
+
const id = setInterval(tick, 2000);
|
|
84
|
+
const off = controller.on((e) => {
|
|
85
|
+
if (e.type === "ownership" || e.type === "ready") {
|
|
86
|
+
setOwnership(e.ownership);
|
|
87
|
+
}
|
|
88
|
+
if (e.type === "exit") setOwnership("stopped");
|
|
89
|
+
});
|
|
90
|
+
return () => {
|
|
91
|
+
clearInterval(id);
|
|
92
|
+
off();
|
|
93
|
+
if (doneTimer.current) clearTimeout(doneTimer.current);
|
|
94
|
+
};
|
|
95
|
+
}, [controller, props.cwd]);
|
|
96
|
+
|
|
97
|
+
const panel = (PANELS[panelIndex] ?? "dashboard") as PanelId;
|
|
98
|
+
const panelActive = !slashOpen && !quitConfirm && focusLayer === "panel";
|
|
99
|
+
|
|
100
|
+
const jumpPanel = useCallback((panelId: string) => {
|
|
101
|
+
const idx = PANELS.indexOf(panelId as PanelId);
|
|
102
|
+
if (idx >= 0) {
|
|
103
|
+
setPanelIndex(idx);
|
|
104
|
+
setFocusLayer("panel");
|
|
105
|
+
}
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const runCli = useCallback(
|
|
109
|
+
async (argv: readonly string[]) => {
|
|
110
|
+
setSlashBusy(true);
|
|
111
|
+
onStatus({ kind: "running", label: `Running oke ${argv.join(" ")}…` });
|
|
112
|
+
try {
|
|
113
|
+
await runSlashCli(props.cwd, argv, onLog);
|
|
114
|
+
onStatus({ kind: "done", label: "Done" });
|
|
115
|
+
} catch {
|
|
116
|
+
onStatus({ kind: "done", label: "Failed" });
|
|
117
|
+
} finally {
|
|
118
|
+
setSlashBusy(false);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[props.cwd, onLog, onStatus],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const requestQuit = useCallback(() => {
|
|
125
|
+
if (ownership === "managed") {
|
|
126
|
+
setQuitConfirm(true);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
exit();
|
|
130
|
+
}, [ownership, exit]);
|
|
131
|
+
|
|
132
|
+
useInput((input, key) => {
|
|
133
|
+
if (slashOpen || quitConfirm) return;
|
|
134
|
+
|
|
135
|
+
if (key.escape) {
|
|
136
|
+
if (focusLayer === "panel") {
|
|
137
|
+
setFocusLayer("tabs");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
requestQuit();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (input === "/") {
|
|
144
|
+
setSlashOpen(true);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const n = Number(input);
|
|
148
|
+
if (n >= 1 && n <= PANELS.length) {
|
|
149
|
+
setPanelIndex(n - 1);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Global panel switch: Tab always; ←→ when focus is on the tab strip.
|
|
153
|
+
if (key.tab) {
|
|
154
|
+
setPanelIndex((i) => (i + 1) % PANELS.length);
|
|
155
|
+
setFocusLayer("tabs");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (focusLayer === "tabs") {
|
|
159
|
+
if (key.leftArrow) {
|
|
160
|
+
setPanelIndex((i) => (i - 1 + PANELS.length) % PANELS.length);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (key.rightArrow) {
|
|
164
|
+
setPanelIndex((i) => (i + 1) % PANELS.length);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (key.downArrow || key.return) {
|
|
168
|
+
setFocusLayer("panel");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (quitConfirm) {
|
|
174
|
+
return (
|
|
175
|
+
<Box
|
|
176
|
+
flexDirection="column"
|
|
177
|
+
borderStyle="round"
|
|
178
|
+
borderColor={TUI_MUTED}
|
|
179
|
+
width="100%"
|
|
180
|
+
padding={1}
|
|
181
|
+
>
|
|
182
|
+
<ConfirmDialog
|
|
183
|
+
message="Stop managed oke dev and quit TUI?"
|
|
184
|
+
onConfirm={() => {
|
|
185
|
+
void controller.stop().then(() => exit());
|
|
186
|
+
}}
|
|
187
|
+
onCancel={() => setQuitConfirm(false)}
|
|
188
|
+
/>
|
|
189
|
+
</Box>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<Box
|
|
195
|
+
flexDirection="column"
|
|
196
|
+
borderStyle="round"
|
|
197
|
+
borderColor={TUI_MUTED}
|
|
198
|
+
width="100%"
|
|
199
|
+
paddingX={1}
|
|
200
|
+
paddingY={0}
|
|
201
|
+
>
|
|
202
|
+
<Box paddingY={1}>
|
|
203
|
+
<HeaderBar cwd={props.cwd} />
|
|
204
|
+
</Box>
|
|
205
|
+
<TabBar tabs={TABS} activeIndex={panelIndex} focused={focusLayer === "tabs"} />
|
|
206
|
+
|
|
207
|
+
{slashOpen ? (
|
|
208
|
+
<Box marginTop={1} flexGrow={1} minHeight={10}>
|
|
209
|
+
<SlashPalette
|
|
210
|
+
cwd={props.cwd}
|
|
211
|
+
busy={slashBusy}
|
|
212
|
+
onClose={() => setSlashOpen(false)}
|
|
213
|
+
onJumpPanel={jumpPanel}
|
|
214
|
+
onRunCli={runCli}
|
|
215
|
+
/>
|
|
216
|
+
</Box>
|
|
217
|
+
) : (
|
|
218
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1} minHeight={10} gap={1}>
|
|
219
|
+
{panel === "dashboard" ? (
|
|
220
|
+
<DashboardPanel
|
|
221
|
+
cwd={props.cwd}
|
|
222
|
+
active={panelActive}
|
|
223
|
+
onJumpPanel={jumpPanel}
|
|
224
|
+
onStatus={onStatus}
|
|
225
|
+
/>
|
|
226
|
+
) : null}
|
|
227
|
+
{panel === "dev" ? (
|
|
228
|
+
<DevPanel
|
|
229
|
+
cwd={props.cwd}
|
|
230
|
+
controller={controller}
|
|
231
|
+
onLog={onLog}
|
|
232
|
+
logs={logs}
|
|
233
|
+
active={panelActive}
|
|
234
|
+
onStatus={onStatus}
|
|
235
|
+
/>
|
|
236
|
+
) : null}
|
|
237
|
+
{panel === "database" ? (
|
|
238
|
+
<DatabasePanel cwd={props.cwd} active={panelActive} onLog={onLog} onStatus={onStatus} />
|
|
239
|
+
) : null}
|
|
240
|
+
{panel === "docker" ? (
|
|
241
|
+
<DockerPanel cwd={props.cwd} active={panelActive} onLog={onLog} onStatus={onStatus} />
|
|
242
|
+
) : null}
|
|
243
|
+
{panel === "navigator" ? <NavigatorPanel cwd={props.cwd} active={panelActive} /> : null}
|
|
244
|
+
</Box>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
<Box marginTop={0}>
|
|
248
|
+
<StatusBar status={footerStatus} />
|
|
249
|
+
</Box>
|
|
250
|
+
</Box>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink live controls for an already-running `oke dev` session.
|
|
3
|
+
*
|
|
4
|
+
* Flat single-line chrome (no nested borders / width="100%") so Ink does not
|
|
5
|
+
* remount at a second column count and leave a ghost frame above the bar.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
9
|
+
import { useState, type ReactElement } from "react";
|
|
10
|
+
import { mapDevControlInput } from "./keys.ts";
|
|
11
|
+
import { TUI_BRAND, TUI_HINT, TUI_MUTED, TUI_WARN } from "./theme.ts";
|
|
12
|
+
|
|
13
|
+
/** Props for {@link DevLiveControls}. */
|
|
14
|
+
export type DevLiveControlsProps = {
|
|
15
|
+
readonly onRefresh: () => void | Promise<void>;
|
|
16
|
+
readonly onComposeUp: () => void | Promise<void>;
|
|
17
|
+
readonly onComposeStop: () => void | Promise<void>;
|
|
18
|
+
readonly onQuit: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compact key-driven controls mounted after `oke dev` boot.
|
|
23
|
+
*
|
|
24
|
+
* @param props - Actions
|
|
25
|
+
*/
|
|
26
|
+
export function DevLiveControls(props: DevLiveControlsProps): ReactElement {
|
|
27
|
+
const { exit } = useApp();
|
|
28
|
+
const [help, setHelp] = useState(false);
|
|
29
|
+
const [busy, setBusy] = useState(false);
|
|
30
|
+
const [status, setStatus] = useState("");
|
|
31
|
+
|
|
32
|
+
useInput((input, key) => {
|
|
33
|
+
const ctrl = mapDevControlInput(input, key);
|
|
34
|
+
if (!ctrl || busy) return;
|
|
35
|
+
if (ctrl === "?") {
|
|
36
|
+
setHelp((h) => !h);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (ctrl === "q") {
|
|
40
|
+
props.onQuit();
|
|
41
|
+
exit();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (ctrl === "r") {
|
|
45
|
+
setBusy(true);
|
|
46
|
+
void Promise.resolve(props.onRefresh())
|
|
47
|
+
.then(() => setStatus("refreshed"))
|
|
48
|
+
.catch((err: unknown) => setStatus(err instanceof Error ? err.message : String(err)))
|
|
49
|
+
.finally(() => setBusy(false));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (ctrl === "u" || ctrl === "x") {
|
|
53
|
+
setBusy(true);
|
|
54
|
+
const run = ctrl === "u" ? props.onComposeUp : props.onComposeStop;
|
|
55
|
+
void Promise.resolve(run())
|
|
56
|
+
.then(() => setStatus(ctrl === "u" ? "compose up" : "compose stop"))
|
|
57
|
+
.catch((err: unknown) => setStatus(err instanceof Error ? err.message : String(err)))
|
|
58
|
+
.finally(() => setBusy(false));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Box flexDirection="column" marginTop={1}>
|
|
64
|
+
<Text>
|
|
65
|
+
<Text bold color={TUI_BRAND}>
|
|
66
|
+
oke dev
|
|
67
|
+
</Text>
|
|
68
|
+
<Text color={TUI_MUTED}> · controls · </Text>
|
|
69
|
+
<Text color={TUI_HINT}>? help · r refresh · q quit · u up · x stop</Text>
|
|
70
|
+
{busy ? <Text color={TUI_WARN}> · working…</Text> : null}
|
|
71
|
+
{status ? <Text color={TUI_MUTED}>{` · ${status}`}</Text> : null}
|
|
72
|
+
</Text>
|
|
73
|
+
{help ? (
|
|
74
|
+
<Text color={TUI_MUTED}>
|
|
75
|
+
r clear/refresh board · q quit · u compose up · x compose stop
|
|
76
|
+
</Text>
|
|
77
|
+
) : null}
|
|
78
|
+
</Box>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Render {@link DevLiveControls} until quit.
|
|
84
|
+
*
|
|
85
|
+
* Clears Ink's frame before `onRefresh` reprints the hero so stdout + Ink
|
|
86
|
+
* never stack two control boxes.
|
|
87
|
+
*
|
|
88
|
+
* @param props - Actions
|
|
89
|
+
*/
|
|
90
|
+
export async function launchDevLiveControls(props: DevLiveControlsProps): Promise<void> {
|
|
91
|
+
const React = await import("react");
|
|
92
|
+
const { render } = await import("ink");
|
|
93
|
+
|
|
94
|
+
type InkInstance = ReturnType<typeof render>;
|
|
95
|
+
let instance: InkInstance | null = null;
|
|
96
|
+
|
|
97
|
+
const afterStdout = async (run: () => void | Promise<void>): Promise<void> => {
|
|
98
|
+
instance?.clear();
|
|
99
|
+
await run();
|
|
100
|
+
if (instance) {
|
|
101
|
+
instance.rerender(React.createElement(DevLiveControls, bound));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const bound: DevLiveControlsProps = {
|
|
106
|
+
onRefresh: () => afterStdout(props.onRefresh),
|
|
107
|
+
onComposeUp: () => afterStdout(props.onComposeUp),
|
|
108
|
+
onComposeStop: () => afterStdout(props.onComposeStop),
|
|
109
|
+
onQuit: props.onQuit,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
instance = render(React.createElement(DevLiveControls, bound), {
|
|
113
|
+
exitOnCtrlC: false,
|
|
114
|
+
});
|
|
115
|
+
await instance.waitUntilExit();
|
|
116
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple y/n confirm overlay for destructive TUI actions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text, useInput } from "ink";
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import { TUI_HINT, TUI_OK, TUI_WARN } from "../theme.ts";
|
|
8
|
+
|
|
9
|
+
/** Props for {@link ConfirmDialog}. */
|
|
10
|
+
export type ConfirmDialogProps = {
|
|
11
|
+
readonly message: string;
|
|
12
|
+
readonly onConfirm: () => void;
|
|
13
|
+
readonly onCancel: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Confirm with y / n / Enter / Esc.
|
|
18
|
+
*
|
|
19
|
+
* @param props - Message + callbacks
|
|
20
|
+
*/
|
|
21
|
+
export function ConfirmDialog(props: ConfirmDialogProps): ReactElement {
|
|
22
|
+
useInput((input, key) => {
|
|
23
|
+
if (input === "y" || input === "Y" || key.return) {
|
|
24
|
+
props.onConfirm();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (input === "n" || input === "N" || key.escape) {
|
|
28
|
+
props.onCancel();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return (
|
|
32
|
+
<Box
|
|
33
|
+
flexDirection="column"
|
|
34
|
+
borderStyle="round"
|
|
35
|
+
borderColor={TUI_WARN}
|
|
36
|
+
paddingX={1}
|
|
37
|
+
paddingY={0}
|
|
38
|
+
>
|
|
39
|
+
<Text bold color={TUI_WARN}>
|
|
40
|
+
Confirm
|
|
41
|
+
</Text>
|
|
42
|
+
<Text>{props.message}</Text>
|
|
43
|
+
<Text color={TUI_HINT}>
|
|
44
|
+
<Text color={TUI_OK}>y</Text> confirm · <Text color={TUI_WARN}>n</Text> cancel
|
|
45
|
+
</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact project chrome — no border (spacing separates).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import { useEffect, useState, type ReactElement } from "react";
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import { TUI_ACCENT, TUI_MUTED, TUI_WARN } from "../theme.ts";
|
|
9
|
+
|
|
10
|
+
/** Props for {@link HeaderBar}. */
|
|
11
|
+
export type HeaderBarProps = {
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function resolveBranch(cwd: string): Promise<string> {
|
|
16
|
+
try {
|
|
17
|
+
const proc = Bun.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
18
|
+
cwd,
|
|
19
|
+
stdout: "pipe",
|
|
20
|
+
stderr: "pipe",
|
|
21
|
+
});
|
|
22
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
23
|
+
if (code !== 0) return "dev";
|
|
24
|
+
const name = out.trim();
|
|
25
|
+
return name.length > 0 && name !== "HEAD" ? name : "dev";
|
|
26
|
+
} catch {
|
|
27
|
+
return "dev";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Project · branch · path.
|
|
33
|
+
*
|
|
34
|
+
* @param props - cwd
|
|
35
|
+
*/
|
|
36
|
+
export function HeaderBar(props: HeaderBarProps): ReactElement {
|
|
37
|
+
const [branch, setBranch] = useState("dev");
|
|
38
|
+
const project = basename(props.cwd) || props.cwd;
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
void resolveBranch(props.cwd).then((b) => {
|
|
43
|
+
if (!cancelled) setBranch(b);
|
|
44
|
+
});
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true;
|
|
47
|
+
};
|
|
48
|
+
}, [props.cwd]);
|
|
49
|
+
|
|
50
|
+
const home = process.env["HOME"] ?? "";
|
|
51
|
+
const shortCwd =
|
|
52
|
+
home && props.cwd.startsWith(home) ? `~${props.cwd.slice(home.length)}` : props.cwd;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Box paddingX={1} paddingY={0} marginBottom={0}>
|
|
56
|
+
<Text bold color={TUI_ACCENT}>
|
|
57
|
+
{project}
|
|
58
|
+
</Text>
|
|
59
|
+
<Text color={TUI_MUTED}> · </Text>
|
|
60
|
+
<Text color={TUI_WARN}>{branch}</Text>
|
|
61
|
+
<Text color={TUI_MUTED}> · </Text>
|
|
62
|
+
<Text color={TUI_MUTED} wrap="truncate">
|
|
63
|
+
{shortCwd}
|
|
64
|
+
</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent log footer — last N lines from the active service.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import { TUI_MUTED } from "../theme.ts";
|
|
8
|
+
|
|
9
|
+
/** Props for {@link LogStream}. */
|
|
10
|
+
export type LogStreamProps = {
|
|
11
|
+
readonly lines: readonly string[];
|
|
12
|
+
readonly max?: number;
|
|
13
|
+
readonly title?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Scrollable-ish log tail (shows last `max` lines).
|
|
18
|
+
*
|
|
19
|
+
* @param props - Lines + title
|
|
20
|
+
*/
|
|
21
|
+
export function LogStream(props: LogStreamProps): ReactElement {
|
|
22
|
+
const max = props.max ?? 6;
|
|
23
|
+
const tail = props.lines.slice(-max);
|
|
24
|
+
return (
|
|
25
|
+
<Box
|
|
26
|
+
flexDirection="column"
|
|
27
|
+
borderStyle="single"
|
|
28
|
+
borderColor={TUI_MUTED}
|
|
29
|
+
paddingX={1}
|
|
30
|
+
minHeight={4}
|
|
31
|
+
flexGrow={0}
|
|
32
|
+
>
|
|
33
|
+
<Text bold color={TUI_MUTED}>
|
|
34
|
+
{props.title ?? "Logs"}
|
|
35
|
+
</Text>
|
|
36
|
+
{tail.length === 0 ? (
|
|
37
|
+
<Text color={TUI_MUTED}>(quiet)</Text>
|
|
38
|
+
) : (
|
|
39
|
+
tail.map((line, i) => (
|
|
40
|
+
<Text key={`${i}-${line.slice(0, 24)}`} wrap="truncate">
|
|
41
|
+
{line.replace(/\n$/, "")}
|
|
42
|
+
</Text>
|
|
43
|
+
))
|
|
44
|
+
)}
|
|
45
|
+
</Box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content card — cyan title, single border for distinct sections only.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import type { ReactElement, ReactNode } from "react";
|
|
7
|
+
import { TUI_ACCENT, TUI_MUTED } from "../theme.ts";
|
|
8
|
+
|
|
9
|
+
/** Props for {@link PanelCard}. */
|
|
10
|
+
export type PanelCardProps = {
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly children: ReactNode;
|
|
13
|
+
readonly flexGrow?: number;
|
|
14
|
+
readonly accent?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Single bordered panel section.
|
|
19
|
+
*
|
|
20
|
+
* @param props - Title + body
|
|
21
|
+
*/
|
|
22
|
+
export function PanelCard(props: PanelCardProps): ReactElement {
|
|
23
|
+
return (
|
|
24
|
+
<Box
|
|
25
|
+
flexDirection="column"
|
|
26
|
+
borderStyle="single"
|
|
27
|
+
borderColor={props.accent ? TUI_ACCENT : TUI_MUTED}
|
|
28
|
+
paddingX={1}
|
|
29
|
+
paddingY={0}
|
|
30
|
+
flexGrow={props.flexGrow}
|
|
31
|
+
minHeight={3}
|
|
32
|
+
>
|
|
33
|
+
<Text bold color={TUI_ACCENT}>
|
|
34
|
+
{props.title}
|
|
35
|
+
</Text>
|
|
36
|
+
<Box flexDirection="column">{props.children}</Box>
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selectable list row with › marker + cyan focus.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Text } from "ink";
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import { TUI_ACCENT, TUI_MUTED } from "../theme.ts";
|
|
8
|
+
|
|
9
|
+
/** Props for {@link SelectRow}. */
|
|
10
|
+
export type SelectRowProps = {
|
|
11
|
+
readonly label: string;
|
|
12
|
+
readonly selected: boolean;
|
|
13
|
+
readonly enabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One navigable action/list row.
|
|
18
|
+
*
|
|
19
|
+
* @param props - Label + selection
|
|
20
|
+
*/
|
|
21
|
+
export function SelectRow(props: SelectRowProps): ReactElement {
|
|
22
|
+
const enabled = props.enabled !== false;
|
|
23
|
+
if (!enabled) {
|
|
24
|
+
return (
|
|
25
|
+
<Text dimColor color={TUI_MUTED}>
|
|
26
|
+
{props.selected ? "› " : " "}
|
|
27
|
+
{props.label}
|
|
28
|
+
</Text>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return (
|
|
32
|
+
<Text bold={props.selected} color={props.selected ? TUI_ACCENT : undefined}>
|
|
33
|
+
{props.selected ? "› " : " "}
|
|
34
|
+
{props.label}
|
|
35
|
+
</Text>
|
|
36
|
+
);
|
|
37
|
+
}
|