okengine 0.10.2 → 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/ai/mcp.mdx +0 -1
- 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 -13
- package/site/content/docs/elements/channel.mdx +9 -13
- package/site/content/docs/elements/clock.mdx +8 -14
- package/site/content/docs/elements/flow.mdx +46 -54
- package/site/content/docs/elements/gate.mdx +6 -11
- package/site/content/docs/elements/signal.mdx +2 -8
- package/site/content/docs/elements/store.mdx +79 -93
- package/site/content/docs/elements/vault.mdx +89 -76
- package/site/content/docs/get-started/basic-usage.mdx +8 -14
- 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 +3 -4
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- 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 +14 -7
- 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 +30 -16
- 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 +8 -9
- 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 +246 -114
- package/src/cli/dev.ts +153 -205
- 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 +142 -9
- 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 +44 -10
- package/src/cli/load-config.source.test.ts +50 -0
- package/src/cli/load-config.ts +35 -29
- 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 +29 -11
- 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/extract.ts +12 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +7 -5
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/generate-adopt.ts +27 -1
- package/src/compiler-entry.ts +7 -0
- package/src/config/define-config.test.ts +111 -47
- package/src/config/driver-defaults.test.ts +115 -0
- package/src/config/driver-defaults.ts +156 -0
- package/src/config/index.ts +246 -131
- 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 +163 -0
- 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 +5 -4
- package/src/kernel/boot-bind/clock.ts +5 -5
- package/src/kernel/boot-bind/gate.ts +6 -7
- package/src/kernel/boot-bind/honor-config.test.ts +42 -42
- package/src/kernel/boot-bind/journal.ts +5 -5
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +55 -37
- package/src/kernel/boot-bind/store.ts +24 -34
- package/src/kernel/boot-bind/vault.ts +19 -4
- 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/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
- 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,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
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/` command palette — autofill over the full `oke` CLI + panel jumps.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text, useInput } from "ink";
|
|
6
|
+
import { useMemo, useState, type ReactElement } from "react";
|
|
7
|
+
import {
|
|
8
|
+
commonPrefix,
|
|
9
|
+
filterSlashActions,
|
|
10
|
+
parseSlashArgv,
|
|
11
|
+
type SlashAction,
|
|
12
|
+
} from "../slash-catalog.ts";
|
|
13
|
+
import { TUI_BRAND, TUI_HINT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
|
|
14
|
+
|
|
15
|
+
/** Props for {@link SlashPalette}. */
|
|
16
|
+
export type SlashPaletteProps = {
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
readonly onClose: () => void;
|
|
19
|
+
readonly onJumpPanel: (panelId: string) => void;
|
|
20
|
+
readonly onRunCli: (argv: readonly string[]) => void | Promise<void>;
|
|
21
|
+
readonly busy?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Slash palette overlay — type to filter, Tab autofills, Enter runs.
|
|
26
|
+
*
|
|
27
|
+
* @param props - Callbacks
|
|
28
|
+
*/
|
|
29
|
+
export function SlashPalette(props: SlashPaletteProps): ReactElement {
|
|
30
|
+
const [query, setQuery] = useState("");
|
|
31
|
+
const [selected, setSelected] = useState(0);
|
|
32
|
+
const [status, setStatus] = useState("");
|
|
33
|
+
|
|
34
|
+
const matches = useMemo(() => filterSlashActions(query, 14), [query]);
|
|
35
|
+
const sel = Math.min(selected, Math.max(0, matches.length - 1));
|
|
36
|
+
const active = matches[sel] ?? null;
|
|
37
|
+
|
|
38
|
+
useInput(
|
|
39
|
+
(input, key) => {
|
|
40
|
+
if (props.busy) return;
|
|
41
|
+
|
|
42
|
+
if (key.escape) {
|
|
43
|
+
props.onClose();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (key.upArrow) {
|
|
48
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (key.downArrow) {
|
|
52
|
+
setSelected((i) => Math.min(Math.max(0, matches.length - 1), i + 1));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (key.tab) {
|
|
57
|
+
if (active) {
|
|
58
|
+
const ids = matches.map((m) => m.id);
|
|
59
|
+
const prefix = commonPrefix(ids);
|
|
60
|
+
const next =
|
|
61
|
+
prefix.length > query.length && ids.every((id) => id.startsWith(prefix))
|
|
62
|
+
? prefix
|
|
63
|
+
: active.id;
|
|
64
|
+
setQuery(next);
|
|
65
|
+
setSelected(0);
|
|
66
|
+
setStatus(`autofill → /${next}`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (key.return) {
|
|
72
|
+
void submit(active, query);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (key.backspace || key.delete) {
|
|
77
|
+
setQuery((q) => q.slice(0, -1));
|
|
78
|
+
setSelected(0);
|
|
79
|
+
setStatus("");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!input || key.ctrl || key.meta) return;
|
|
84
|
+
// Ignore a second leading `/`; otherwise append printable chars.
|
|
85
|
+
if (input === "/" && query.length === 0) return;
|
|
86
|
+
setQuery((q) => q + input);
|
|
87
|
+
setSelected(0);
|
|
88
|
+
setStatus("");
|
|
89
|
+
},
|
|
90
|
+
{ isActive: true },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const submit = async (action: SlashAction | null, raw: string): Promise<void> => {
|
|
94
|
+
if (action?.kind === "panel" && action.panel) {
|
|
95
|
+
props.onJumpPanel(action.panel);
|
|
96
|
+
props.onClose();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (action?.kind === "cli" && action.argv) {
|
|
101
|
+
const typed = parseSlashArgv(raw);
|
|
102
|
+
const base = action.argv;
|
|
103
|
+
// Append trailing free tokens the user typed beyond the suggestion.
|
|
104
|
+
let argv: readonly string[] = base;
|
|
105
|
+
if (typed.length > base.length && typed.slice(0, base.length).join(" ") === base.join(" ")) {
|
|
106
|
+
argv = typed;
|
|
107
|
+
}
|
|
108
|
+
setStatus(`running oke ${argv.join(" ")}…`);
|
|
109
|
+
await props.onRunCli(argv);
|
|
110
|
+
props.onClose();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const argv = parseSlashArgv(raw);
|
|
115
|
+
if (argv.length === 0) {
|
|
116
|
+
setStatus("type a command · Tab to autofill");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
setStatus(`running oke ${argv.join(" ")}…`);
|
|
120
|
+
await props.onRunCli(argv);
|
|
121
|
+
props.onClose();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Box
|
|
126
|
+
flexDirection="column"
|
|
127
|
+
borderStyle="round"
|
|
128
|
+
borderColor={TUI_BRAND}
|
|
129
|
+
paddingX={1}
|
|
130
|
+
width="100%"
|
|
131
|
+
flexGrow={1}
|
|
132
|
+
>
|
|
133
|
+
<Box>
|
|
134
|
+
<Text bold color={TUI_BRAND}>
|
|
135
|
+
/
|
|
136
|
+
</Text>
|
|
137
|
+
<Text bold>{query}</Text>
|
|
138
|
+
<Text color={TUI_BRAND}>█</Text>
|
|
139
|
+
<Text color={TUI_MUTED}> Tab autofill · ↑↓ · Enter run · Esc</Text>
|
|
140
|
+
</Box>
|
|
141
|
+
|
|
142
|
+
<Box flexDirection="column" marginTop={1} flexGrow={1}>
|
|
143
|
+
{matches.length === 0 ? (
|
|
144
|
+
<Text color={TUI_WARN}>No matches — Enter runs free-form `oke …`</Text>
|
|
145
|
+
) : (
|
|
146
|
+
matches.map((m, i) => {
|
|
147
|
+
const on = i === sel;
|
|
148
|
+
return (
|
|
149
|
+
<Box key={m.id} gap={1}>
|
|
150
|
+
<Text bold={on} color={on ? TUI_BRAND : TUI_MUTED}>
|
|
151
|
+
{on ? "›" : " "}
|
|
152
|
+
</Text>
|
|
153
|
+
<Text bold={on} color={on ? TUI_BRAND : undefined}>
|
|
154
|
+
/{m.label}
|
|
155
|
+
</Text>
|
|
156
|
+
<Text color={m.kind === "panel" ? TUI_OK : TUI_MUTED}>
|
|
157
|
+
{m.kind === "panel" ? "panel" : "cli"}
|
|
158
|
+
</Text>
|
|
159
|
+
<Text color={TUI_MUTED} wrap="truncate">
|
|
160
|
+
{m.summary}
|
|
161
|
+
</Text>
|
|
162
|
+
</Box>
|
|
163
|
+
);
|
|
164
|
+
})
|
|
165
|
+
)}
|
|
166
|
+
</Box>
|
|
167
|
+
|
|
168
|
+
{status ? <Text color={TUI_HINT}>{status}</Text> : null}
|
|
169
|
+
{props.busy ? <Text color={TUI_WARN}>Running…</Text> : null}
|
|
170
|
+
</Box>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight terminal spinner (no extra deps).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Text } from "ink";
|
|
6
|
+
import { useEffect, useState, type ReactElement } from "react";
|
|
7
|
+
import { TUI_WARN } from "../theme.ts";
|
|
8
|
+
|
|
9
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
10
|
+
|
|
11
|
+
/** Props for {@link Spinner}. */
|
|
12
|
+
export type SpinnerProps = {
|
|
13
|
+
readonly label?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Animated spinner + optional label.
|
|
18
|
+
*
|
|
19
|
+
* @param props - Label
|
|
20
|
+
*/
|
|
21
|
+
export function Spinner(props: SpinnerProps): ReactElement {
|
|
22
|
+
const [i, setI] = useState(0);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const id = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 80);
|
|
25
|
+
return () => clearInterval(id);
|
|
26
|
+
}, []);
|
|
27
|
+
return (
|
|
28
|
+
<Text color={TUI_WARN}>
|
|
29
|
+
{FRAMES[i]} {props.label ?? "Working…"}
|
|
30
|
+
</Text>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single global footer — shortcuts + transient action status (no own border;
|
|
3
|
+
* the app chrome already frames the shell).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import type { ReactElement } from "react";
|
|
8
|
+
import { Spinner } from "./Spinner.tsx";
|
|
9
|
+
import { TUI_HINT, TUI_MUTED, TUI_OK } from "../theme.ts";
|
|
10
|
+
|
|
11
|
+
/** Footer mode. */
|
|
12
|
+
export type FooterStatus =
|
|
13
|
+
| { readonly kind: "idle" }
|
|
14
|
+
| { readonly kind: "running"; readonly label: string }
|
|
15
|
+
| { readonly kind: "done"; readonly label?: string };
|
|
16
|
+
|
|
17
|
+
/** Props for {@link StatusBar}. */
|
|
18
|
+
export type StatusBarProps = {
|
|
19
|
+
readonly status?: FooterStatus;
|
|
20
|
+
readonly hints?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shortcut strip + optional running/done feedback.
|
|
25
|
+
*
|
|
26
|
+
* @param props - Status + hints
|
|
27
|
+
*/
|
|
28
|
+
export function StatusBar(props: StatusBarProps): ReactElement {
|
|
29
|
+
const hints =
|
|
30
|
+
props.hints ?? "/ command · Tab panels · ↑↓ navigate · Enter action · 1-5 jump · Esc";
|
|
31
|
+
const status = props.status ?? { kind: "idle" as const };
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Box paddingX={1} paddingY={0} marginTop={0} justifyContent="space-between">
|
|
35
|
+
<Box>
|
|
36
|
+
{status.kind === "running" ? (
|
|
37
|
+
<Spinner label={status.label} />
|
|
38
|
+
) : status.kind === "done" ? (
|
|
39
|
+
<Text color={TUI_OK}>✓ {status.label ?? "Done"}</Text>
|
|
40
|
+
) : (
|
|
41
|
+
<Text color={TUI_HINT}>{hints}</Text>
|
|
42
|
+
)}
|
|
43
|
+
</Box>
|
|
44
|
+
{status.kind !== "idle" ? (
|
|
45
|
+
<Text color={TUI_MUTED} dimColor>
|
|
46
|
+
{hints}
|
|
47
|
+
</Text>
|
|
48
|
+
) : null}
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink readiness glyph for {@link DevStatus}.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Text } from "ink";
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import type { DevStatus } from "../../../term.ts";
|
|
8
|
+
import { TUI_ERR, TUI_OK, TUI_WARN } from "../theme.ts";
|
|
9
|
+
|
|
10
|
+
/** Props for {@link StatusDot}. */
|
|
11
|
+
export type StatusDotProps = {
|
|
12
|
+
readonly status: DevStatus;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ● online/pending/error · ○ offline (yellow/red, not gray).
|
|
17
|
+
*
|
|
18
|
+
* @param props - Status
|
|
19
|
+
*/
|
|
20
|
+
export function StatusDot(props: StatusDotProps): ReactElement {
|
|
21
|
+
if (props.status === "idle") {
|
|
22
|
+
return <Text color={TUI_WARN}>○</Text>;
|
|
23
|
+
}
|
|
24
|
+
const color = props.status === "ready" ? TUI_OK : props.status === "pending" ? TUI_WARN : TUI_ERR;
|
|
25
|
+
return <Text color={color}>●</Text>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Fixed-width status word. */
|
|
29
|
+
export function statusWord(status: DevStatus): {
|
|
30
|
+
readonly word: string;
|
|
31
|
+
readonly color: typeof TUI_OK | typeof TUI_WARN | typeof TUI_ERR;
|
|
32
|
+
} {
|
|
33
|
+
if (status === "ready") return { word: "online ", color: TUI_OK };
|
|
34
|
+
if (status === "pending") return { word: "start ", color: TUI_WARN };
|
|
35
|
+
if (status === "error") return { word: "error ", color: TUI_ERR };
|
|
36
|
+
return { word: "offline", color: TUI_WARN };
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level panel tabs — green active chip, no enclosing border.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import { TUI_BRAND, TUI_MUTED } from "../theme.ts";
|
|
8
|
+
|
|
9
|
+
/** One tab descriptor. */
|
|
10
|
+
export type TabItem = {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Props for {@link TabBar}. */
|
|
16
|
+
export type TabBarProps = {
|
|
17
|
+
readonly tabs: readonly TabItem[];
|
|
18
|
+
readonly activeIndex: number;
|
|
19
|
+
/** When true, tab strip owns focus (reserved for future emphasis). */
|
|
20
|
+
readonly focused?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Horizontal tab bar.
|
|
25
|
+
*
|
|
26
|
+
* @param props - Tabs + selection
|
|
27
|
+
*/
|
|
28
|
+
export function TabBar(props: TabBarProps): ReactElement {
|
|
29
|
+
return (
|
|
30
|
+
<Box paddingX={1} gap={0}>
|
|
31
|
+
{props.tabs.map((tab, i) => {
|
|
32
|
+
const active = i === props.activeIndex;
|
|
33
|
+
const label = ` ${i + 1} ${tab.label} `;
|
|
34
|
+
if (active) {
|
|
35
|
+
return (
|
|
36
|
+
<Text key={tab.id} backgroundColor={TUI_BRAND} color="black" bold>
|
|
37
|
+
{label}
|
|
38
|
+
</Text>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
<Text key={tab.id} color={TUI_MUTED}>
|
|
43
|
+
{label}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|