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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing panel — OKE wordmark, session, aligned services, quick actions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text, useInput } from "ink";
|
|
6
|
+
import { useEffect, useState, type ReactElement } from "react";
|
|
7
|
+
import type { FooterStatus } from "../components/StatusBar.tsx";
|
|
8
|
+
import { StatusDot, statusWord } from "../components/StatusDot.tsx";
|
|
9
|
+
import { Wordmark } from "../components/Wordmark.tsx";
|
|
10
|
+
import { formatUptime, openUrl } from "../format.ts";
|
|
11
|
+
import { pollPortSnapshot, type PortSnapshot } from "../state/ports.ts";
|
|
12
|
+
import { TUI_ACCENT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
|
|
13
|
+
|
|
14
|
+
/** Props for {@link DashboardPanel}. */
|
|
15
|
+
export type DashboardPanelProps = {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly active: boolean;
|
|
18
|
+
readonly onJumpPanel: (panelId: string) => void;
|
|
19
|
+
readonly onStatus: (status: FooterStatus) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ACTIONS = [
|
|
23
|
+
{ id: "open", label: "[O]pen Console", key: "o" },
|
|
24
|
+
{ id: "mcp", label: "[M]CP Docs", key: "m" },
|
|
25
|
+
{ id: "dev", label: "[D]ev Panel", key: "d" },
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Dashboard panel — polls ports every 1.5s.
|
|
30
|
+
*
|
|
31
|
+
* @param props - cwd + focus/actions
|
|
32
|
+
*/
|
|
33
|
+
export function DashboardPanel(props: DashboardPanelProps): ReactElement {
|
|
34
|
+
const [snap, setSnap] = useState<PortSnapshot | null>(null);
|
|
35
|
+
const [startedAt, setStartedAt] = useState<number | null>(null);
|
|
36
|
+
const [now, setNow] = useState(Date.now());
|
|
37
|
+
const [selected, setSelected] = useState(0);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
const tick = async (): Promise<void> => {
|
|
42
|
+
const next = await pollPortSnapshot(props.cwd);
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
setSnap(next);
|
|
45
|
+
if (next.ownership === "managed") {
|
|
46
|
+
setStartedAt((prev) => prev ?? Date.now());
|
|
47
|
+
} else {
|
|
48
|
+
setStartedAt(null);
|
|
49
|
+
}
|
|
50
|
+
setNow(Date.now());
|
|
51
|
+
};
|
|
52
|
+
void tick();
|
|
53
|
+
const id = setInterval(() => void tick(), 1500);
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
clearInterval(id);
|
|
57
|
+
};
|
|
58
|
+
}, [props.cwd]);
|
|
59
|
+
|
|
60
|
+
// Prefer lock startedAt when available via re-read
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
let cancelled = false;
|
|
63
|
+
void (async () => {
|
|
64
|
+
const { readDevSessionLock } = await import("../../dev-session-lock.ts");
|
|
65
|
+
const lock = await readDevSessionLock(props.cwd);
|
|
66
|
+
if (!cancelled && lock) {
|
|
67
|
+
const t = Date.parse(lock.startedAt);
|
|
68
|
+
if (Number.isFinite(t)) setStartedAt(t);
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
}, [props.cwd, snap?.ownership]);
|
|
75
|
+
|
|
76
|
+
useInput(
|
|
77
|
+
(input, key) => {
|
|
78
|
+
if (!props.active) return;
|
|
79
|
+
if (key.upArrow) setSelected((i) => Math.max(0, i - 1));
|
|
80
|
+
if (key.downArrow) setSelected((i) => Math.min(ACTIONS.length - 1, i + 1));
|
|
81
|
+
const lower = input.toLowerCase();
|
|
82
|
+
if (lower === "o") void runAction("open");
|
|
83
|
+
else if (lower === "m") void runAction("mcp");
|
|
84
|
+
else if (lower === "d") void runAction("dev");
|
|
85
|
+
else if (key.return) {
|
|
86
|
+
const a = ACTIONS[selected];
|
|
87
|
+
if (a) void runAction(a.id);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{ isActive: props.active },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const runAction = async (id: (typeof ACTIONS)[number]["id"]): Promise<void> => {
|
|
94
|
+
if (id === "dev") {
|
|
95
|
+
props.onJumpPanel("dev");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const consoleSurf = snap?.surfaces.find((s) => s.id === "console");
|
|
99
|
+
const docsSurf = snap?.surfaces.find((s) => s.id === "docsMcp");
|
|
100
|
+
if (id === "open") {
|
|
101
|
+
const url = consoleSurf?.url ?? "http://127.0.0.1:6533";
|
|
102
|
+
props.onStatus({ kind: "running", label: `Opening ${url}…` });
|
|
103
|
+
await openUrl(url);
|
|
104
|
+
props.onStatus({ kind: "done", label: "Opened Console" });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const url = docsSurf?.url ?? "http://127.0.0.1:6536";
|
|
108
|
+
props.onStatus({ kind: "running", label: `Opening ${url}…` });
|
|
109
|
+
await openUrl(url);
|
|
110
|
+
props.onStatus({ kind: "done", label: "Opened MCP Docs" });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const ownership = snap?.ownership ?? "stopped";
|
|
114
|
+
const ownershipLabel =
|
|
115
|
+
ownership === "managed"
|
|
116
|
+
? "Managed"
|
|
117
|
+
: ownership === "external"
|
|
118
|
+
? "Connected (external)"
|
|
119
|
+
: "Stopped";
|
|
120
|
+
const ownershipColor = ownership === "managed" || ownership === "external" ? TUI_OK : TUI_WARN;
|
|
121
|
+
const uptime = startedAt && ownership === "managed" ? formatUptime(now - startedAt) : null;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Box flexDirection="column" gap={1} flexGrow={1}>
|
|
125
|
+
<Box justifyContent="space-between" alignItems="flex-start">
|
|
126
|
+
<Wordmark version="0.10.3" />
|
|
127
|
+
</Box>
|
|
128
|
+
|
|
129
|
+
<Box flexDirection="column">
|
|
130
|
+
<Text bold color={TUI_ACCENT}>
|
|
131
|
+
Session
|
|
132
|
+
</Text>
|
|
133
|
+
<Box gap={1}>
|
|
134
|
+
<StatusDot
|
|
135
|
+
status={ownership === "managed" || ownership === "external" ? "ready" : "idle"}
|
|
136
|
+
/>
|
|
137
|
+
<Text bold color={ownershipColor}>
|
|
138
|
+
{ownershipLabel}
|
|
139
|
+
</Text>
|
|
140
|
+
{uptime ? <Text color={TUI_MUTED}>· uptime {uptime}</Text> : null}
|
|
141
|
+
</Box>
|
|
142
|
+
{ownership === "external" ? (
|
|
143
|
+
<Text color={TUI_WARN}>Read-only — will not start a second stack</Text>
|
|
144
|
+
) : null}
|
|
145
|
+
{ownership === "stopped" ? (
|
|
146
|
+
<Text color={TUI_MUTED}>No live oke dev — press [D] or open Dev panel</Text>
|
|
147
|
+
) : null}
|
|
148
|
+
</Box>
|
|
149
|
+
|
|
150
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
151
|
+
<Box>
|
|
152
|
+
<Box width={14}>
|
|
153
|
+
<Text bold color={TUI_ACCENT}>
|
|
154
|
+
Services
|
|
155
|
+
</Text>
|
|
156
|
+
</Box>
|
|
157
|
+
<Box width={8}>
|
|
158
|
+
<Text color={TUI_MUTED}>Port</Text>
|
|
159
|
+
</Box>
|
|
160
|
+
<Box width={10}>
|
|
161
|
+
<Text color={TUI_MUTED}>Status</Text>
|
|
162
|
+
</Box>
|
|
163
|
+
<Text color={TUI_MUTED}>URL</Text>
|
|
164
|
+
</Box>
|
|
165
|
+
<Text color={TUI_MUTED}>{"─".repeat(56)}</Text>
|
|
166
|
+
{(snap?.surfaces ?? []).map((s) => {
|
|
167
|
+
const sw = statusWord(s.status);
|
|
168
|
+
return (
|
|
169
|
+
<Box key={s.id}>
|
|
170
|
+
<Box width={14} gap={1}>
|
|
171
|
+
<StatusDot status={s.status} />
|
|
172
|
+
<Text>{s.label.padEnd(9)}</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
<Box width={8}>
|
|
175
|
+
<Text color={TUI_ACCENT}>{`:${String(s.port).padEnd(5)}`}</Text>
|
|
176
|
+
</Box>
|
|
177
|
+
<Box width={10}>
|
|
178
|
+
<Text color={sw.color}>{sw.word}</Text>
|
|
179
|
+
</Box>
|
|
180
|
+
<Text color={TUI_MUTED}>{s.url}</Text>
|
|
181
|
+
</Box>
|
|
182
|
+
);
|
|
183
|
+
})}
|
|
184
|
+
{!snap ? <Text color={TUI_MUTED}>Probing ports…</Text> : null}
|
|
185
|
+
</Box>
|
|
186
|
+
|
|
187
|
+
<Box gap={2}>
|
|
188
|
+
{ACTIONS.map((a, i) => (
|
|
189
|
+
<Text
|
|
190
|
+
key={a.id}
|
|
191
|
+
bold={props.active && i === selected}
|
|
192
|
+
color={props.active && i === selected ? TUI_ACCENT : TUI_MUTED}
|
|
193
|
+
>
|
|
194
|
+
{props.active && i === selected ? "› " : " "}
|
|
195
|
+
{a.label}
|
|
196
|
+
</Text>
|
|
197
|
+
))}
|
|
198
|
+
</Box>
|
|
199
|
+
</Box>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database panel — 3-column action cards, no nested footer.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text, useInput } from "ink";
|
|
6
|
+
import { useState, type ReactElement } from "react";
|
|
7
|
+
import { ConfirmDialog } from "../components/ConfirmDialog.tsx";
|
|
8
|
+
import type { FooterStatus } from "../components/StatusBar.tsx";
|
|
9
|
+
import { TUI_ACCENT, TUI_MUTED, TUI_WARN } from "../theme.ts";
|
|
10
|
+
|
|
11
|
+
/** Props for {@link DatabasePanel}. */
|
|
12
|
+
export type DatabasePanelProps = {
|
|
13
|
+
readonly cwd: string;
|
|
14
|
+
readonly active: boolean;
|
|
15
|
+
readonly onLog: (line: string) => void;
|
|
16
|
+
readonly onStatus: (status: FooterStatus) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type DbAction = "push" | "generate" | "migrate" | "seed" | "studio";
|
|
20
|
+
|
|
21
|
+
type Card = {
|
|
22
|
+
readonly id: DbAction;
|
|
23
|
+
readonly key: string;
|
|
24
|
+
readonly title: string;
|
|
25
|
+
readonly blurb: string;
|
|
26
|
+
readonly confirm: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const CARDS: readonly Card[] = [
|
|
30
|
+
{
|
|
31
|
+
id: "push",
|
|
32
|
+
key: "P",
|
|
33
|
+
title: "Push",
|
|
34
|
+
blurb: "sync schema → live DB",
|
|
35
|
+
confirm: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "generate",
|
|
39
|
+
key: "G",
|
|
40
|
+
title: "Generate",
|
|
41
|
+
blurb: "write SQL under drizzle/",
|
|
42
|
+
confirm: false,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "migrate",
|
|
46
|
+
key: "M",
|
|
47
|
+
title: "Migrate",
|
|
48
|
+
blurb: "apply pending migrations",
|
|
49
|
+
confirm: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "seed",
|
|
53
|
+
key: "E",
|
|
54
|
+
title: "Seed",
|
|
55
|
+
blurb: "run defineSeed",
|
|
56
|
+
confirm: true,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "studio",
|
|
60
|
+
key: "T",
|
|
61
|
+
title: "Studio",
|
|
62
|
+
blurb: "drizzle-kit (browser)",
|
|
63
|
+
confirm: false,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Database actions as a selectable card grid.
|
|
69
|
+
*
|
|
70
|
+
* @param props - cwd + log sink
|
|
71
|
+
*/
|
|
72
|
+
export function DatabasePanel(props: DatabasePanelProps): ReactElement {
|
|
73
|
+
const [selected, setSelected] = useState(0);
|
|
74
|
+
const [pending, setPending] = useState<DbAction | null>(null);
|
|
75
|
+
const [busy, setBusy] = useState(false);
|
|
76
|
+
|
|
77
|
+
useInput(
|
|
78
|
+
(input, key) => {
|
|
79
|
+
if (!props.active || pending || busy) return;
|
|
80
|
+
if (key.leftArrow) setSelected((i) => Math.max(0, i - 1));
|
|
81
|
+
if (key.rightArrow) setSelected((i) => Math.min(CARDS.length - 1, i + 1));
|
|
82
|
+
if (key.upArrow) setSelected((i) => Math.max(0, i - 3));
|
|
83
|
+
if (key.downArrow) setSelected((i) => Math.min(CARDS.length - 1, i + 3));
|
|
84
|
+
const upper = input.toUpperCase();
|
|
85
|
+
const byKey = CARDS.findIndex((c) => c.key === upper);
|
|
86
|
+
if (byKey >= 0) {
|
|
87
|
+
setSelected(byKey);
|
|
88
|
+
const card = CARDS[byKey]!;
|
|
89
|
+
if (card.confirm) setPending(card.id);
|
|
90
|
+
else void run(card.id);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.return) {
|
|
94
|
+
const action = CARDS[selected];
|
|
95
|
+
if (!action) return;
|
|
96
|
+
if (action.confirm) setPending(action.id);
|
|
97
|
+
else void run(action.id);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{ isActive: props.active && !pending && !busy },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const run = async (id: DbAction): Promise<void> => {
|
|
104
|
+
setBusy(true);
|
|
105
|
+
setPending(null);
|
|
106
|
+
props.onStatus({ kind: "running", label: `Running oke db ${id}…` });
|
|
107
|
+
props.onLog(`oke db ${id}\n`);
|
|
108
|
+
const prevCwd = process.cwd();
|
|
109
|
+
try {
|
|
110
|
+
process.chdir(props.cwd);
|
|
111
|
+
const { dbCli } = await import("../../db.ts");
|
|
112
|
+
const writes: string[] = [];
|
|
113
|
+
const origLog = console.log;
|
|
114
|
+
const origErr = console.error;
|
|
115
|
+
console.log = (...args: unknown[]) => {
|
|
116
|
+
writes.push(args.map(String).join(" ") + "\n");
|
|
117
|
+
};
|
|
118
|
+
console.error = (...args: unknown[]) => {
|
|
119
|
+
writes.push(args.map(String).join(" ") + "\n");
|
|
120
|
+
};
|
|
121
|
+
let code = 1;
|
|
122
|
+
try {
|
|
123
|
+
code = await dbCli([id, "--force"]);
|
|
124
|
+
} finally {
|
|
125
|
+
console.log = origLog;
|
|
126
|
+
console.error = origErr;
|
|
127
|
+
}
|
|
128
|
+
for (const w of writes) props.onLog(w);
|
|
129
|
+
props.onStatus({
|
|
130
|
+
kind: "done",
|
|
131
|
+
label: code === 0 ? `db ${id} ok` : `db ${id} exited ${code}`,
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
135
|
+
props.onLog(`${m}\n`);
|
|
136
|
+
props.onStatus({ kind: "done", label: m });
|
|
137
|
+
} finally {
|
|
138
|
+
process.chdir(prevCwd);
|
|
139
|
+
setBusy(false);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (pending) {
|
|
144
|
+
return (
|
|
145
|
+
<ConfirmDialog
|
|
146
|
+
message={`Run oke db ${pending}? This may change the live database.`}
|
|
147
|
+
onConfirm={() => void run(pending)}
|
|
148
|
+
onCancel={() => setPending(null)}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const row1 = CARDS.slice(0, 3);
|
|
154
|
+
const row2 = CARDS.slice(3);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Box flexDirection="column" gap={1} flexGrow={1}>
|
|
158
|
+
<Text bold color={TUI_ACCENT}>
|
|
159
|
+
Database
|
|
160
|
+
</Text>
|
|
161
|
+
<Text color={TUI_MUTED} dimColor>
|
|
162
|
+
Domain schema via drizzle-kit
|
|
163
|
+
</Text>
|
|
164
|
+
<Box gap={1}>
|
|
165
|
+
{row1.map((card, i) => (
|
|
166
|
+
<ActionCard key={card.id} card={card} selected={props.active && selected === i} />
|
|
167
|
+
))}
|
|
168
|
+
</Box>
|
|
169
|
+
<Box gap={1}>
|
|
170
|
+
{row2.map((card, i) => (
|
|
171
|
+
<ActionCard key={card.id} card={card} selected={props.active && selected === i + 3} />
|
|
172
|
+
))}
|
|
173
|
+
</Box>
|
|
174
|
+
<Text color={TUI_WARN}>! Push & Migrate modify the database — confirmation required</Text>
|
|
175
|
+
</Box>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One database action card.
|
|
181
|
+
*/
|
|
182
|
+
function ActionCard(props: { readonly card: Card; readonly selected: boolean }): ReactElement {
|
|
183
|
+
const { card, selected } = props;
|
|
184
|
+
return (
|
|
185
|
+
<Box
|
|
186
|
+
flexDirection="column"
|
|
187
|
+
borderStyle="single"
|
|
188
|
+
borderColor={selected ? TUI_ACCENT : TUI_MUTED}
|
|
189
|
+
width="32%"
|
|
190
|
+
paddingX={1}
|
|
191
|
+
>
|
|
192
|
+
<Text bold color={selected ? TUI_ACCENT : undefined}>
|
|
193
|
+
{selected ? "› " : " "}[{card.key}] {card.title}
|
|
194
|
+
</Text>
|
|
195
|
+
<Text color={TUI_MUTED}>{card.blurb}</Text>
|
|
196
|
+
</Box>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev panel — Not Running / Running states, inline shortcuts, live logs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Box, Text, useInput } from "ink";
|
|
6
|
+
import { useEffect, useMemo, useState, type ReactElement } from "react";
|
|
7
|
+
import { DevController } from "../../dev-controller.ts";
|
|
8
|
+
import type { DevOwnership } from "../../dev-session-lock.ts";
|
|
9
|
+
import type { FooterStatus } from "../components/StatusBar.tsx";
|
|
10
|
+
import { StatusDot } from "../components/StatusDot.tsx";
|
|
11
|
+
import { formatUptime } from "../format.ts";
|
|
12
|
+
import { mapDevControlInput } from "../keys.ts";
|
|
13
|
+
import { pollPortSnapshot, type PortSnapshot } from "../state/ports.ts";
|
|
14
|
+
import { TUI_ACCENT, TUI_MUTED, TUI_OK, TUI_WARN } from "../theme.ts";
|
|
15
|
+
|
|
16
|
+
/** Props for {@link DevPanel}. */
|
|
17
|
+
export type DevPanelProps = {
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
readonly controller: DevController;
|
|
20
|
+
readonly onLog: (line: string) => void;
|
|
21
|
+
readonly active: boolean;
|
|
22
|
+
readonly logs: readonly string[];
|
|
23
|
+
readonly onStatus: (status: FooterStatus) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ActionId = "start" | "stop" | "restart" | "attach";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Dev controls panel.
|
|
30
|
+
*
|
|
31
|
+
* @param props - Controller + cwd
|
|
32
|
+
*/
|
|
33
|
+
export function DevPanel(props: DevPanelProps): ReactElement {
|
|
34
|
+
const [ownership, setOwnership] = useState<DevOwnership>("stopped");
|
|
35
|
+
const [snap, setSnap] = useState<PortSnapshot | null>(null);
|
|
36
|
+
const [busy, setBusy] = useState(false);
|
|
37
|
+
const [startedAt, setStartedAt] = useState<number | null>(null);
|
|
38
|
+
const [now, setNow] = useState(Date.now());
|
|
39
|
+
const [selected, setSelected] = useState(0);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const off = props.controller.on((e) => {
|
|
43
|
+
if (e.type === "ownership") setOwnership(e.ownership);
|
|
44
|
+
if (e.type === "ready") {
|
|
45
|
+
setOwnership(e.ownership);
|
|
46
|
+
if (e.ownership === "managed") setStartedAt(Date.now());
|
|
47
|
+
}
|
|
48
|
+
if (e.type === "log") props.onLog(e.text);
|
|
49
|
+
if (e.type === "error") {
|
|
50
|
+
props.onLog(`${e.message}\n`);
|
|
51
|
+
props.onStatus({ kind: "done", label: e.message });
|
|
52
|
+
}
|
|
53
|
+
if (e.type === "exit") setStartedAt(null);
|
|
54
|
+
});
|
|
55
|
+
void props.controller.attach(props.cwd).then((r) => {
|
|
56
|
+
setOwnership(r.ownership);
|
|
57
|
+
if (r.ownership === "managed" && r.lock) {
|
|
58
|
+
setStartedAt(Date.parse(r.lock.startedAt) || Date.now());
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return off;
|
|
62
|
+
}, [props.controller, props.cwd, props.onLog, props.onStatus]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const id = setInterval(() => {
|
|
66
|
+
void pollPortSnapshot(props.cwd).then((s) => {
|
|
67
|
+
setOwnership(s.ownership);
|
|
68
|
+
setSnap(s);
|
|
69
|
+
});
|
|
70
|
+
setNow(Date.now());
|
|
71
|
+
}, 1500);
|
|
72
|
+
return () => clearInterval(id);
|
|
73
|
+
}, [props.cwd]);
|
|
74
|
+
|
|
75
|
+
const actions = useMemo(() => {
|
|
76
|
+
const running = ownership === "managed";
|
|
77
|
+
const external = ownership === "external";
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
id: "start" as const,
|
|
81
|
+
label: "[S]tart",
|
|
82
|
+
enabled: ownership === "stopped",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "restart" as const,
|
|
86
|
+
label: "[R]estart",
|
|
87
|
+
enabled: running,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "stop" as const,
|
|
91
|
+
label: "[X]top",
|
|
92
|
+
enabled: running,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "attach" as const,
|
|
96
|
+
label: "[A]ttach",
|
|
97
|
+
enabled: true,
|
|
98
|
+
},
|
|
99
|
+
].filter((a) => !(external && (a.id === "start" || a.id === "restart" || a.id === "stop")));
|
|
100
|
+
}, [ownership]);
|
|
101
|
+
|
|
102
|
+
useInput(
|
|
103
|
+
(input, key) => {
|
|
104
|
+
if (!props.active || busy) return;
|
|
105
|
+
const ctrl = mapDevControlInput(input, key);
|
|
106
|
+
if (ctrl === "q") return; // Esc handled by App
|
|
107
|
+
if (ctrl === "r" || input === "r" || input === "R") {
|
|
108
|
+
void runAction("restart");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (input === "s" || input === "S") {
|
|
112
|
+
void runAction("start");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (input === "x" || input === "X") {
|
|
116
|
+
void runAction("stop");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (input === "a" || input === "A") {
|
|
120
|
+
void runAction("attach");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (key.upArrow) {
|
|
124
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (key.downArrow) {
|
|
128
|
+
setSelected((i) => Math.min(actions.length - 1, i + 1));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (key.return) {
|
|
132
|
+
const action = actions[selected];
|
|
133
|
+
if (action) void runAction(action.id);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{ isActive: props.active },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const runAction = async (id: ActionId): Promise<void> => {
|
|
140
|
+
if (busy) return;
|
|
141
|
+
setBusy(true);
|
|
142
|
+
const labels: Record<ActionId, string> = {
|
|
143
|
+
start: "Starting oke dev…",
|
|
144
|
+
stop: "Stopping…",
|
|
145
|
+
restart: "Restarting…",
|
|
146
|
+
attach: "Attaching…",
|
|
147
|
+
};
|
|
148
|
+
props.onStatus({ kind: "running", label: labels[id] });
|
|
149
|
+
try {
|
|
150
|
+
if (id === "start") {
|
|
151
|
+
const code = await props.controller.start({ cwd: props.cwd });
|
|
152
|
+
props.onStatus({
|
|
153
|
+
kind: "done",
|
|
154
|
+
label: code === 0 ? "Started" : "Start failed",
|
|
155
|
+
});
|
|
156
|
+
if (code === 0) setStartedAt(Date.now());
|
|
157
|
+
} else if (id === "stop") {
|
|
158
|
+
await props.controller.stop();
|
|
159
|
+
props.onStatus({ kind: "done", label: "Stopped" });
|
|
160
|
+
setStartedAt(null);
|
|
161
|
+
} else if (id === "restart") {
|
|
162
|
+
if (ownership !== "managed") {
|
|
163
|
+
props.onStatus({ kind: "done", label: "Nothing to restart" });
|
|
164
|
+
} else {
|
|
165
|
+
const code = await props.controller.restart({ cwd: props.cwd });
|
|
166
|
+
props.onStatus({
|
|
167
|
+
kind: "done",
|
|
168
|
+
label: code === 0 ? "Restarted" : "Restart failed",
|
|
169
|
+
});
|
|
170
|
+
if (code === 0) setStartedAt(Date.now());
|
|
171
|
+
}
|
|
172
|
+
} else if (id === "attach") {
|
|
173
|
+
const r = await props.controller.attach(props.cwd);
|
|
174
|
+
setOwnership(r.ownership);
|
|
175
|
+
props.onStatus({ kind: "done", label: `Ownership: ${r.ownership}` });
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
setBusy(false);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const running = ownership === "managed" || ownership === "external";
|
|
183
|
+
const uptime = startedAt && ownership === "managed" ? formatUptime(now - startedAt) : null;
|
|
184
|
+
const app = snap?.surfaces.find((s) => s.id === "app");
|
|
185
|
+
const consoleSurf = snap?.surfaces.find((s) => s.id === "console");
|
|
186
|
+
const logTail = props.logs.slice(-Math.max(6, 12));
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<Box flexDirection="column" gap={1} flexGrow={1}>
|
|
190
|
+
<Box flexDirection="column">
|
|
191
|
+
<Text bold color={TUI_ACCENT}>
|
|
192
|
+
Dev Server
|
|
193
|
+
</Text>
|
|
194
|
+
{!running ? (
|
|
195
|
+
<Box flexDirection="column">
|
|
196
|
+
<Box gap={1}>
|
|
197
|
+
<Text color={TUI_WARN} bold>
|
|
198
|
+
●
|
|
199
|
+
</Text>
|
|
200
|
+
<Text bold color={TUI_WARN}>
|
|
201
|
+
Not Running
|
|
202
|
+
</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
<Text color={TUI_MUTED}>Press [S] to start</Text>
|
|
205
|
+
</Box>
|
|
206
|
+
) : (
|
|
207
|
+
<Box flexDirection="column">
|
|
208
|
+
<Box gap={1}>
|
|
209
|
+
<StatusDot status="ready" />
|
|
210
|
+
<Text bold color={TUI_OK}>
|
|
211
|
+
{ownership === "external" ? "Running (external)" : "Running"}
|
|
212
|
+
</Text>
|
|
213
|
+
{uptime ? <Text color={TUI_MUTED}>· uptime {uptime}</Text> : null}
|
|
214
|
+
</Box>
|
|
215
|
+
<Box gap={2}>
|
|
216
|
+
<Text color={TUI_MUTED}>Ports</Text>
|
|
217
|
+
<Text color={TUI_ACCENT}>App :{app?.port ?? "—"}</Text>
|
|
218
|
+
<Text color={TUI_ACCENT}>Console :{consoleSurf?.port ?? "—"}</Text>
|
|
219
|
+
</Box>
|
|
220
|
+
{ownership === "external" ? (
|
|
221
|
+
<Text color={TUI_WARN}>Read-only — will not start a second stack</Text>
|
|
222
|
+
) : null}
|
|
223
|
+
</Box>
|
|
224
|
+
)}
|
|
225
|
+
</Box>
|
|
226
|
+
|
|
227
|
+
<Box gap={2}>
|
|
228
|
+
{actions.map((a, i) => (
|
|
229
|
+
<Text
|
|
230
|
+
key={a.id}
|
|
231
|
+
bold={props.active && i === selected}
|
|
232
|
+
color={!a.enabled ? TUI_MUTED : props.active && i === selected ? TUI_ACCENT : undefined}
|
|
233
|
+
dimColor={!a.enabled}
|
|
234
|
+
>
|
|
235
|
+
{props.active && i === selected ? "› " : " "}
|
|
236
|
+
{a.label}
|
|
237
|
+
</Text>
|
|
238
|
+
))}
|
|
239
|
+
</Box>
|
|
240
|
+
|
|
241
|
+
<Box flexDirection="column" flexGrow={1} minHeight={6}>
|
|
242
|
+
<Text bold color={TUI_ACCENT}>
|
|
243
|
+
Live Logs
|
|
244
|
+
</Text>
|
|
245
|
+
{logTail.length === 0 ? (
|
|
246
|
+
<Text color={TUI_MUTED}>Start the dev server to see logs</Text>
|
|
247
|
+
) : (
|
|
248
|
+
logTail.map((line, i) => (
|
|
249
|
+
<Text key={`${i}-${line.slice(0, 24)}`} wrap="truncate">
|
|
250
|
+
{line.replace(/\n$/, "")}
|
|
251
|
+
</Text>
|
|
252
|
+
))
|
|
253
|
+
)}
|
|
254
|
+
</Box>
|
|
255
|
+
</Box>
|
|
256
|
+
);
|
|
257
|
+
}
|