veryfront 0.0.81 → 0.0.83
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/README.md +15 -1
- package/esm/deno.js +1 -1
- package/esm/proxy/cache/index.d.ts +41 -0
- package/esm/proxy/cache/index.d.ts.map +1 -0
- package/esm/proxy/cache/index.js +75 -0
- package/esm/proxy/cache/memory-cache.d.ts +18 -0
- package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
- package/esm/proxy/cache/memory-cache.js +100 -0
- package/esm/proxy/cache/redis-cache.d.ts +27 -0
- package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
- package/esm/proxy/cache/redis-cache.js +183 -0
- package/esm/proxy/cache/resilient-cache.d.ts +44 -0
- package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
- package/esm/proxy/cache/resilient-cache.js +178 -0
- package/esm/proxy/cache/types.d.ts +65 -0
- package/esm/proxy/cache/types.d.ts.map +1 -0
- package/esm/proxy/cache/types.js +7 -0
- package/esm/proxy/handler.d.ts +81 -0
- package/esm/proxy/handler.d.ts.map +1 -0
- package/esm/proxy/handler.js +417 -0
- package/esm/proxy/logger.d.ts +29 -0
- package/esm/proxy/logger.d.ts.map +1 -0
- package/esm/proxy/logger.js +258 -0
- package/esm/proxy/oauth-client.d.ts +15 -0
- package/esm/proxy/oauth-client.d.ts.map +1 -0
- package/esm/proxy/oauth-client.js +52 -0
- package/esm/proxy/token-manager.d.ts +59 -0
- package/esm/proxy/token-manager.d.ts.map +1 -0
- package/esm/proxy/token-manager.js +125 -0
- package/esm/proxy/tracing.d.ts +39 -0
- package/esm/proxy/tracing.d.ts.map +1 -0
- package/esm/proxy/tracing.js +194 -0
- package/esm/src/cache/backend.d.ts +22 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +59 -0
- package/esm/src/cache/cache-key-builder.d.ts +0 -4
- package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
- package/esm/src/cache/cache-key-builder.js +0 -6
- package/esm/src/cache/hash.d.ts +107 -0
- package/esm/src/cache/hash.d.ts.map +1 -0
- package/esm/src/cache/hash.js +166 -0
- package/esm/src/cache/index.d.ts +3 -0
- package/esm/src/cache/index.d.ts.map +1 -1
- package/esm/src/cache/index.js +3 -0
- package/esm/src/cache/module-cache.d.ts +82 -0
- package/esm/src/cache/module-cache.d.ts.map +1 -0
- package/esm/src/cache/module-cache.js +214 -0
- package/esm/src/cache/multi-tier.d.ts +148 -0
- package/esm/src/cache/multi-tier.d.ts.map +1 -0
- package/esm/src/cache/multi-tier.js +326 -0
- package/esm/src/cli/app/actions.d.ts +26 -0
- package/esm/src/cli/app/actions.d.ts.map +1 -0
- package/esm/src/cli/app/actions.js +152 -0
- package/esm/src/cli/app/components/inline-input.d.ts +35 -0
- package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
- package/esm/src/cli/app/components/inline-input.js +220 -0
- package/esm/src/cli/app/components/list-select.d.ts +69 -0
- package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
- package/esm/src/cli/app/components/list-select.js +137 -0
- package/esm/src/cli/app/index.d.ts +45 -0
- package/esm/src/cli/app/index.d.ts.map +1 -0
- package/esm/src/cli/app/index.js +1252 -0
- package/esm/src/cli/app/state.d.ts +122 -0
- package/esm/src/cli/app/state.d.ts.map +1 -0
- package/esm/src/cli/app/state.js +232 -0
- package/esm/src/cli/app/views/dashboard.d.ts +19 -0
- package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
- package/esm/src/cli/app/views/dashboard.js +178 -0
- package/esm/src/cli/index/command-router.d.ts.map +1 -1
- package/esm/src/cli/index/command-router.js +9 -39
- package/esm/src/cli/index/start-handler.d.ts +3 -0
- package/esm/src/cli/index/start-handler.d.ts.map +1 -0
- package/esm/src/cli/index/start-handler.js +145 -0
- package/esm/src/cli/mcp/index.d.ts +11 -0
- package/esm/src/cli/mcp/index.d.ts.map +1 -0
- package/esm/src/cli/mcp/index.js +10 -0
- package/esm/src/cli/templates/integration-loader.d.ts.map +1 -1
- package/esm/src/cli/templates/integration-loader.js +2 -4
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +148 -20
- package/esm/src/observability/tracing/span-names.d.ts +2 -0
- package/esm/src/observability/tracing/span-names.d.ts.map +1 -1
- package/esm/src/observability/tracing/span-names.js +2 -0
- package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
- package/esm/src/rendering/orchestrator/module-loader/cache.d.ts +10 -2
- package/esm/src/rendering/orchestrator/module-loader/cache.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/module-loader/cache.js +11 -6
- package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/module-loader/index.js +72 -77
- package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
- package/esm/src/server/context/cache-invalidation.js +4 -0
- package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
- package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +145 -93
- package/esm/src/transforms/esm/transform-cache.d.ts +25 -0
- package/esm/src/transforms/esm/transform-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/transform-cache.js +45 -0
- package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts.map +1 -1
- package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +2 -36
- package/esm/src/utils/constants/cache.d.ts +4 -0
- package/esm/src/utils/constants/cache.d.ts.map +1 -1
- package/esm/src/utils/constants/cache.js +14 -1
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/package.json +2 -1
- package/src/deno.js +1 -1
- package/src/proxy/cache/index.ts +93 -0
- package/src/proxy/cache/memory-cache.ts +120 -0
- package/src/proxy/cache/redis-cache.ts +203 -0
- package/src/proxy/cache/resilient-cache.ts +205 -0
- package/src/proxy/cache/types.ts +72 -0
- package/src/proxy/handler.ts +593 -0
- package/src/proxy/logger.ts +329 -0
- package/src/proxy/oauth-client.ts +91 -0
- package/src/proxy/token-manager.ts +174 -0
- package/src/proxy/tracing.ts +237 -0
- package/src/src/cache/backend.ts +65 -0
- package/src/src/cache/cache-key-builder.ts +0 -9
- package/src/src/cache/hash.ts +205 -0
- package/src/src/cache/index.ts +3 -0
- package/src/src/cache/module-cache.ts +252 -0
- package/src/src/cache/multi-tier.ts +462 -0
- package/src/src/cli/app/actions.ts +190 -0
- package/src/src/cli/app/components/inline-input.ts +255 -0
- package/src/src/cli/app/components/list-select.ts +215 -0
- package/src/src/cli/app/index.ts +1471 -0
- package/src/src/cli/app/state.ts +385 -0
- package/src/src/cli/app/views/dashboard.ts +212 -0
- package/src/src/cli/index/command-router.ts +9 -40
- package/src/src/cli/index/start-handler.ts +195 -0
- package/src/src/cli/mcp/index.ts +11 -0
- package/src/src/cli/templates/integration-loader.ts +2 -8
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +168 -25
- package/src/src/observability/tracing/span-names.ts +2 -0
- package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
- package/src/src/rendering/orchestrator/module-loader/cache.ts +14 -8
- package/src/src/rendering/orchestrator/module-loader/index.ts +94 -89
- package/src/src/server/context/cache-invalidation.ts +4 -0
- package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
- package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
- package/src/src/transforms/esm/http-cache.ts +160 -105
- package/src/src/transforms/esm/transform-cache.ts +53 -0
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +2 -40
- package/src/src/utils/constants/cache.ts +21 -1
- package/src/src/utils/index.ts +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI App Actions
|
|
3
|
+
*
|
|
4
|
+
* Handlers for opening projects in browser, Studio, and IDE.
|
|
5
|
+
* Uses cross-runtime platform abstractions for filesystem and command execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { openBrowser } from "../auth/browser.js";
|
|
9
|
+
import { createFileSystem } from "../../platform/compat/fs.js";
|
|
10
|
+
import { getOsType, runCommand } from "../../platform/compat/process.js";
|
|
11
|
+
import { join } from "../../platform/compat/path/index.js";
|
|
12
|
+
import type { ProjectInfo } from "./state.js";
|
|
13
|
+
import { getRuntimeEnv, type RuntimeEnv } from "../../config/runtime-env.js";
|
|
14
|
+
|
|
15
|
+
export type IDE = "cursor" | "code" | "zed" | "idea" | "webstorm";
|
|
16
|
+
|
|
17
|
+
export interface ActionResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
message?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** IDE command-line executables */
|
|
23
|
+
const IDE_COMMANDS: Record<IDE, string> = {
|
|
24
|
+
cursor: "cursor",
|
|
25
|
+
code: "code",
|
|
26
|
+
zed: "zed",
|
|
27
|
+
idea: "idea",
|
|
28
|
+
webstorm: "webstorm",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** IDE display names */
|
|
32
|
+
const IDE_NAMES: Record<IDE, string> = {
|
|
33
|
+
cursor: "Cursor",
|
|
34
|
+
code: "VS Code",
|
|
35
|
+
zed: "Zed",
|
|
36
|
+
idea: "IntelliJ IDEA",
|
|
37
|
+
webstorm: "WebStorm",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** IDE detection order (preferred first) */
|
|
41
|
+
const IDE_DETECTION_ORDER: IDE[] = ["cursor", "code", "zed", "idea", "webstorm"];
|
|
42
|
+
|
|
43
|
+
/** Cache directories to clear relative to project path */
|
|
44
|
+
const PROJECT_CACHE_DIRS = [".cache", "node_modules/.cache"];
|
|
45
|
+
|
|
46
|
+
function formatError(error: unknown): string {
|
|
47
|
+
return error instanceof Error ? error.message : String(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function commandExists(cmd: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const whichCmd = getOsType() === "windows" ? "where" : "which";
|
|
53
|
+
const result = await runCommand(whichCmd, { args: [cmd] });
|
|
54
|
+
return result.success;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runCommandLocal(cmd: string, args: string[]): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
const result = await runCommand(cmd, { args });
|
|
63
|
+
return result.success;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function openInBrowser(project: ProjectInfo, port: number): Promise<ActionResult> {
|
|
70
|
+
const url = `http://${project.slug}.veryfront.me:${port}`;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await openBrowser(url);
|
|
74
|
+
return { success: true, message: `Opened ${url}` };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { success: false, message: `Failed to open browser: ${formatError(error)}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function openInStudio(project: ProjectInfo): Promise<ActionResult> {
|
|
81
|
+
const url = `https://veryfront.com/projects/${project.slug}`;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await openBrowser(url);
|
|
85
|
+
return { success: true, message: `Opened Studio for ${project.slug}` };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { success: false, message: `Failed to open Studio: ${formatError(error)}` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function detectIDEs(): Promise<IDE[]> {
|
|
92
|
+
const available: IDE[] = [];
|
|
93
|
+
|
|
94
|
+
for (const ide of IDE_DETECTION_ORDER) {
|
|
95
|
+
if (await commandExists(IDE_COMMANDS[ide])) {
|
|
96
|
+
available.push(ide);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return available;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getPreferredIDE(): Promise<IDE | null> {
|
|
104
|
+
const ides = await detectIDEs();
|
|
105
|
+
return ides[0] ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function openPathInIDE(path: string, ide?: IDE): Promise<ActionResult> {
|
|
109
|
+
const targetIDE = ide ?? (await getPreferredIDE());
|
|
110
|
+
|
|
111
|
+
if (!targetIDE) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
message: "No supported IDE found. Install VS Code, Cursor, or Zed.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cmd = IDE_COMMANDS[targetIDE];
|
|
119
|
+
const name = IDE_NAMES[targetIDE];
|
|
120
|
+
|
|
121
|
+
if (await runCommandLocal(cmd, [path])) {
|
|
122
|
+
return { success: true, message: `Opened in ${name}` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { success: false, message: `Failed to open ${name}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function openInIDE(project: ProjectInfo, ide?: IDE): Promise<ActionResult> {
|
|
129
|
+
const result = await openPathInIDE(project.path, ide);
|
|
130
|
+
if (!result.success) return result;
|
|
131
|
+
|
|
132
|
+
const ideName = result.message?.split(" in ")[1];
|
|
133
|
+
return { success: true, message: `Opened ${project.slug} in ${ideName}` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function openFileInIDE(filePath: string, ide?: IDE): Promise<ActionResult> {
|
|
137
|
+
return openPathInIDE(filePath, ide);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function clearProjectCache(project: ProjectInfo): Promise<ActionResult> {
|
|
141
|
+
const fs = createFileSystem();
|
|
142
|
+
let cleared = 0;
|
|
143
|
+
|
|
144
|
+
for (const relativeDir of PROJECT_CACHE_DIRS) {
|
|
145
|
+
const dir = join(project.path, relativeDir);
|
|
146
|
+
try {
|
|
147
|
+
await fs.remove(dir, { recursive: true });
|
|
148
|
+
cleared++;
|
|
149
|
+
} catch {
|
|
150
|
+
// Directory doesn't exist
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const message = cleared > 0 ? `Cleared ${cleared} cache directories` : "No caches to clear";
|
|
155
|
+
return { success: true, message };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function openMCPSettings(env: RuntimeEnv = getRuntimeEnv()): Promise<ActionResult> {
|
|
159
|
+
const home = env.homeDir || "";
|
|
160
|
+
const claudeDir = join(home, ".claude");
|
|
161
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
162
|
+
const fs = createFileSystem();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
166
|
+
} catch {
|
|
167
|
+
// Already exists
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!(await fs.exists(settingsPath))) {
|
|
171
|
+
const defaultSettings = { mcpServers: {} };
|
|
172
|
+
await fs.writeTextFile(settingsPath, JSON.stringify(defaultSettings, null, 2));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return openFileInIDE(settingsPath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function quickOpen(
|
|
179
|
+
projects: Array<{ slug: string; path: string }>,
|
|
180
|
+
num: number,
|
|
181
|
+
port: number,
|
|
182
|
+
): Promise<ActionResult> {
|
|
183
|
+
const index = num - 1;
|
|
184
|
+
if (index < 0 || index >= projects.length) {
|
|
185
|
+
return Promise.resolve({ success: false, message: `No project at position ${num}` });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const project = projects[index]!;
|
|
189
|
+
return openInBrowser({ slug: project.slug, path: project.path, type: "local" }, port);
|
|
190
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/****
|
|
2
|
+
* Inline Text Input Component
|
|
3
|
+
*
|
|
4
|
+
* Renders an input prompt at the bottom of the TUI that stays inline
|
|
5
|
+
* without exiting alternate screen mode.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { brand, dim, muted } from "../../ui/colors.js";
|
|
9
|
+
import type { InputState, LogEntry } from "../state.js";
|
|
10
|
+
|
|
11
|
+
export interface InlineInputOptions {
|
|
12
|
+
maxWidth?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render the inline input prompt
|
|
17
|
+
*/
|
|
18
|
+
export function renderInput(input: InputState, _options: InlineInputOptions = {}): string {
|
|
19
|
+
if (!input.active) return "";
|
|
20
|
+
|
|
21
|
+
// Build the input line with cursor
|
|
22
|
+
const prompt = ` ${brand(">")} ${input.prompt}: `;
|
|
23
|
+
const beforeCursor = input.value.slice(0, input.cursorPos);
|
|
24
|
+
const cursorChar = input.value[input.cursorPos] ?? " ";
|
|
25
|
+
const afterCursor = input.value.slice(input.cursorPos + 1);
|
|
26
|
+
|
|
27
|
+
// Cursor is rendered as inverse video
|
|
28
|
+
const cursor = `\x1b[7m${cursorChar}\x1b[27m`;
|
|
29
|
+
|
|
30
|
+
const inputLine = `${prompt}${beforeCursor}${cursor}${afterCursor}`;
|
|
31
|
+
|
|
32
|
+
// Hint line
|
|
33
|
+
const hintLine = ` ${dim("Enter")} ${muted("to submit")} ${dim("Esc")} ${muted("to cancel")}`;
|
|
34
|
+
|
|
35
|
+
return `${inputLine}\n${hintLine}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RenderLogsOptions {
|
|
39
|
+
maxLines?: number;
|
|
40
|
+
maxWidth?: number;
|
|
41
|
+
scroll?: number;
|
|
42
|
+
expanded?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render the logs area with optional scrolling
|
|
47
|
+
*/
|
|
48
|
+
export function renderLogs(
|
|
49
|
+
logs: LogEntry[],
|
|
50
|
+
options: RenderLogsOptions = {},
|
|
51
|
+
): string {
|
|
52
|
+
const { maxLines = 5, maxWidth = 80, scroll = 0, expanded = false } = options;
|
|
53
|
+
|
|
54
|
+
if (logs.length === 0) return "";
|
|
55
|
+
|
|
56
|
+
const visibleLines = expanded ? Math.max(maxLines, 15) : maxLines;
|
|
57
|
+
const end = logs.length - scroll;
|
|
58
|
+
const start = Math.max(0, end - visibleLines);
|
|
59
|
+
const visibleLogs = logs.slice(start, end);
|
|
60
|
+
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const log of visibleLogs) {
|
|
64
|
+
const time = formatTime(log.time);
|
|
65
|
+
const levelColor = getLevelColor(log.level);
|
|
66
|
+
const levelPrefix = getLevelPrefix(log.level);
|
|
67
|
+
|
|
68
|
+
if (expanded) {
|
|
69
|
+
// When expanded, show structured info if available
|
|
70
|
+
if (log.meta?.method) {
|
|
71
|
+
// Request log - show all details in clean format
|
|
72
|
+
const meta = log.meta;
|
|
73
|
+
const statusColor = getStatusColor(meta.status || 200);
|
|
74
|
+
const methodStr = (meta.method || "GET").padEnd(7);
|
|
75
|
+
const pathStr = meta.path || "/";
|
|
76
|
+
const statusStr = String(meta.status || 200);
|
|
77
|
+
const durationStr = `${meta.durationMs || 0}ms`.padStart(6);
|
|
78
|
+
|
|
79
|
+
// Line 1: time + method + path
|
|
80
|
+
lines.push(
|
|
81
|
+
` ${dim(time)} ${levelColor(levelPrefix)} ${methodStr}${pathStr}`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Line 2: status + duration + project info
|
|
85
|
+
const projectInfo: string[] = [];
|
|
86
|
+
if (meta.project) projectInfo.push(brand(meta.project));
|
|
87
|
+
if (meta.env) projectInfo.push(dim(meta.env));
|
|
88
|
+
if (meta.releaseId) projectInfo.push(dim(`#${meta.releaseId.slice(0, 8)}`));
|
|
89
|
+
|
|
90
|
+
lines.push(
|
|
91
|
+
` ${"".padEnd(12)}${statusColor(statusStr)} ${dim(durationStr)}${
|
|
92
|
+
projectInfo.length ? ` ${projectInfo.join(" ")}` : ""
|
|
93
|
+
}`,
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
// Regular log - show full message (may wrap to multiple lines)
|
|
97
|
+
const prefix = ` ${dim(time)} ${levelColor(levelPrefix)} `;
|
|
98
|
+
const msgLines = wrapText(log.message, maxWidth - 15);
|
|
99
|
+
lines.push(`${prefix}${msgLines[0] || ""}`);
|
|
100
|
+
// Indent continuation lines
|
|
101
|
+
for (let i = 1; i < msgLines.length; i++) {
|
|
102
|
+
lines.push(` ${"".padEnd(12)}${msgLines[i]}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// When collapsed, truncate
|
|
107
|
+
const maxMsgLen = maxWidth - 15;
|
|
108
|
+
const msg = log.message.length > maxMsgLen
|
|
109
|
+
? `${log.message.slice(0, maxMsgLen - 3)}...`
|
|
110
|
+
: log.message;
|
|
111
|
+
lines.push(` ${dim(time)} ${levelColor(levelPrefix)} ${msg}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (expanded && logs.length > visibleLines) {
|
|
116
|
+
const canScrollUp = start > 0;
|
|
117
|
+
const canScrollDown = scroll > 0;
|
|
118
|
+
const scrollHint = [];
|
|
119
|
+
if (canScrollUp) scrollHint.push("↑");
|
|
120
|
+
if (canScrollDown) scrollHint.push("↓");
|
|
121
|
+
if (scrollHint.length > 0) {
|
|
122
|
+
lines.push(` ${dim(`[${scrollHint.join(" ")}] ${logs.length} total`)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function wrapText(text: string, maxWidth: number): string[] {
|
|
130
|
+
if (text.length <= maxWidth) return [text];
|
|
131
|
+
|
|
132
|
+
const lines: string[] = [];
|
|
133
|
+
let remaining = text;
|
|
134
|
+
|
|
135
|
+
while (remaining.length > maxWidth) {
|
|
136
|
+
let breakPoint = remaining.lastIndexOf(" ", maxWidth);
|
|
137
|
+
if (breakPoint <= 0) breakPoint = maxWidth;
|
|
138
|
+
lines.push(remaining.slice(0, breakPoint));
|
|
139
|
+
remaining = remaining.slice(breakPoint).trimStart();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (remaining) lines.push(remaining);
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format time as HH:MM:SS
|
|
148
|
+
*/
|
|
149
|
+
function formatTime(date: Date): string {
|
|
150
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
151
|
+
const m = String(date.getMinutes()).padStart(2, "0");
|
|
152
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
153
|
+
return `${h}:${m}:${s}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get color function for log level
|
|
158
|
+
*/
|
|
159
|
+
function getLevelColor(level: LogEntry["level"]): (s: string) => string {
|
|
160
|
+
switch (level) {
|
|
161
|
+
case "error":
|
|
162
|
+
return (s: string) => `\x1b[31m${s}\x1b[0m`; // red
|
|
163
|
+
case "warn":
|
|
164
|
+
return (s: string) => `\x1b[33m${s}\x1b[0m`; // yellow
|
|
165
|
+
case "info":
|
|
166
|
+
return (s: string) => `\x1b[36m${s}\x1b[0m`; // cyan
|
|
167
|
+
case "debug":
|
|
168
|
+
return dim;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get color function for HTTP status code
|
|
174
|
+
*/
|
|
175
|
+
function getStatusColor(status: number): (s: string) => string {
|
|
176
|
+
if (status >= 500) return (s: string) => `\x1b[31m${s}\x1b[0m`; // red
|
|
177
|
+
if (status >= 400) return (s: string) => `\x1b[33m${s}\x1b[0m`; // yellow
|
|
178
|
+
if (status >= 300) return (s: string) => `\x1b[36m${s}\x1b[0m`; // cyan
|
|
179
|
+
return (s: string) => `\x1b[32m${s}\x1b[0m`; // green
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get prefix for log level
|
|
184
|
+
*/
|
|
185
|
+
function getLevelPrefix(level: LogEntry["level"]): string {
|
|
186
|
+
switch (level) {
|
|
187
|
+
case "error":
|
|
188
|
+
return "ERR";
|
|
189
|
+
case "warn":
|
|
190
|
+
return "WRN";
|
|
191
|
+
case "info":
|
|
192
|
+
return "INF";
|
|
193
|
+
case "debug":
|
|
194
|
+
return "DBG";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Handle input key press
|
|
200
|
+
* Returns the new value and cursor position, or null if the key should end input
|
|
201
|
+
*/
|
|
202
|
+
export function handleInputKey(
|
|
203
|
+
key: string,
|
|
204
|
+
value: string,
|
|
205
|
+
cursorPos: number,
|
|
206
|
+
): { value: string; cursorPos: number } | { action: "submit" | "cancel" } {
|
|
207
|
+
if (key === "\r" || key === "\n") return { action: "submit" };
|
|
208
|
+
if (key === "\x1b" || key === "\x03") return { action: "cancel" };
|
|
209
|
+
|
|
210
|
+
if (key === "\x7f" || key === "\b") {
|
|
211
|
+
if (cursorPos === 0) return { value, cursorPos };
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
value: value.slice(0, cursorPos - 1) + value.slice(cursorPos),
|
|
215
|
+
cursorPos: cursorPos - 1,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (key === "\x1b[3~") {
|
|
220
|
+
if (cursorPos >= value.length) return { value, cursorPos };
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
value: value.slice(0, cursorPos) + value.slice(cursorPos + 1),
|
|
224
|
+
cursorPos,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (key === "\x1b[D") return { value, cursorPos: Math.max(0, cursorPos - 1) };
|
|
229
|
+
if (key === "\x1b[C") return { value, cursorPos: Math.min(value.length, cursorPos + 1) };
|
|
230
|
+
if (key === "\x01" || key === "\x1b[H") return { value, cursorPos: 0 };
|
|
231
|
+
if (key === "\x05" || key === "\x1b[F") return { value, cursorPos: value.length };
|
|
232
|
+
if (key === "\x15") return { value: "", cursorPos: 0 };
|
|
233
|
+
|
|
234
|
+
if (key === "\x17") {
|
|
235
|
+
if (cursorPos === 0) return { value, cursorPos };
|
|
236
|
+
|
|
237
|
+
let newPos = cursorPos - 1;
|
|
238
|
+
while (newPos > 0 && value[newPos] === " ") newPos--;
|
|
239
|
+
while (newPos > 0 && value[newPos - 1] !== " ") newPos--;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
value: value.slice(0, newPos) + value.slice(cursorPos),
|
|
243
|
+
cursorPos: newPos,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (key.length === 1 && key >= " " && key <= "~") {
|
|
248
|
+
return {
|
|
249
|
+
value: value.slice(0, cursorPos) + key + value.slice(cursorPos),
|
|
250
|
+
cursorPos: cursorPos + 1,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { value, cursorPos };
|
|
255
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive List Select Component
|
|
3
|
+
*
|
|
4
|
+
* Keyboard-navigable list with selection support.
|
|
5
|
+
* Supports arrow keys, j/k vim bindings, and number shortcuts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { brand, dim } from "../../ui/colors.js";
|
|
9
|
+
import { truncate } from "../../ui/layout.js";
|
|
10
|
+
|
|
11
|
+
export interface ListItem<T = unknown> {
|
|
12
|
+
/** Unique identifier */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Display label */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Optional description */
|
|
17
|
+
description?: string;
|
|
18
|
+
/** Optional path or metadata */
|
|
19
|
+
meta?: string;
|
|
20
|
+
/** Associated data */
|
|
21
|
+
data?: T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ListSelectOptions {
|
|
25
|
+
/** Maximum width for the list */
|
|
26
|
+
maxWidth?: number;
|
|
27
|
+
/** Number of visible items (for scrolling) */
|
|
28
|
+
visibleCount?: number;
|
|
29
|
+
/** Show number shortcuts (1-9) */
|
|
30
|
+
showNumbers?: boolean;
|
|
31
|
+
/** Offset for number shortcuts (e.g., 1 means start at [2]) */
|
|
32
|
+
numberOffset?: number;
|
|
33
|
+
/** Empty state message */
|
|
34
|
+
emptyMessage?: string;
|
|
35
|
+
/** Show selection cursor (default true). Set false for inactive sections */
|
|
36
|
+
showSelection?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ListSelectState<T = unknown> {
|
|
40
|
+
/** All items in the list */
|
|
41
|
+
items: ListItem<T>[];
|
|
42
|
+
/** Currently selected index */
|
|
43
|
+
selectedIndex: number;
|
|
44
|
+
/** Scroll offset for long lists */
|
|
45
|
+
scrollOffset: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create initial list state
|
|
50
|
+
*/
|
|
51
|
+
export function createListState<T>(items: ListItem<T>[]): ListSelectState<T> {
|
|
52
|
+
return {
|
|
53
|
+
items,
|
|
54
|
+
selectedIndex: 0,
|
|
55
|
+
scrollOffset: 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Move selection up
|
|
61
|
+
*/
|
|
62
|
+
export function moveUp<T>(state: ListSelectState<T>): ListSelectState<T> {
|
|
63
|
+
if (state.items.length === 0) return state;
|
|
64
|
+
|
|
65
|
+
const newIndex = state.selectedIndex > 0 ? state.selectedIndex - 1 : state.items.length - 1;
|
|
66
|
+
|
|
67
|
+
const scrollOffset = newIndex < state.scrollOffset ? newIndex : state.scrollOffset;
|
|
68
|
+
|
|
69
|
+
return { ...state, selectedIndex: newIndex, scrollOffset };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Move selection down
|
|
74
|
+
*/
|
|
75
|
+
export function moveDown<T>(
|
|
76
|
+
state: ListSelectState<T>,
|
|
77
|
+
visibleCount = 10,
|
|
78
|
+
): ListSelectState<T> {
|
|
79
|
+
if (state.items.length === 0) return state;
|
|
80
|
+
|
|
81
|
+
const newIndex = state.selectedIndex < state.items.length - 1 ? state.selectedIndex + 1 : 0;
|
|
82
|
+
|
|
83
|
+
let scrollOffset = state.scrollOffset;
|
|
84
|
+
|
|
85
|
+
if (newIndex === 0) {
|
|
86
|
+
scrollOffset = 0;
|
|
87
|
+
} else if (newIndex >= scrollOffset + visibleCount) {
|
|
88
|
+
scrollOffset = newIndex - visibleCount + 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ...state, selectedIndex: newIndex, scrollOffset };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Select item by number (1-9)
|
|
96
|
+
*/
|
|
97
|
+
export function selectByNumber<T>(
|
|
98
|
+
state: ListSelectState<T>,
|
|
99
|
+
num: number,
|
|
100
|
+
): ListSelectState<T> {
|
|
101
|
+
const index = num - 1;
|
|
102
|
+
if (index < 0 || index >= state.items.length) return state;
|
|
103
|
+
return { ...state, selectedIndex: index };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get currently selected item
|
|
108
|
+
*/
|
|
109
|
+
export function getSelectedItem<T>(
|
|
110
|
+
state: ListSelectState<T>,
|
|
111
|
+
): ListItem<T> | undefined {
|
|
112
|
+
return state.items[state.selectedIndex];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render the list as a string
|
|
117
|
+
*/
|
|
118
|
+
export function renderList<T>(
|
|
119
|
+
state: ListSelectState<T>,
|
|
120
|
+
options: ListSelectOptions = {},
|
|
121
|
+
): string {
|
|
122
|
+
const {
|
|
123
|
+
maxWidth = 60,
|
|
124
|
+
visibleCount = 10,
|
|
125
|
+
showNumbers = true,
|
|
126
|
+
numberOffset = 0,
|
|
127
|
+
emptyMessage = "No items",
|
|
128
|
+
showSelection = true,
|
|
129
|
+
} = options;
|
|
130
|
+
|
|
131
|
+
if (state.items.length === 0) return ` ${dim(emptyMessage)}`;
|
|
132
|
+
|
|
133
|
+
const lines: string[] = [];
|
|
134
|
+
const start = state.scrollOffset;
|
|
135
|
+
const end = Math.min(start + visibleCount, state.items.length);
|
|
136
|
+
const visibleItems = state.items.slice(start, end);
|
|
137
|
+
|
|
138
|
+
const numberWidth = showNumbers ? 4 : 0; // " [1] "
|
|
139
|
+
const cursorWidth = 2; // "› " or " "
|
|
140
|
+
const prefixWidth = numberWidth + cursorWidth;
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
143
|
+
const item = visibleItems[i];
|
|
144
|
+
if (!item) continue;
|
|
145
|
+
|
|
146
|
+
const actualIndex = start + i;
|
|
147
|
+
const isSelected = showSelection && actualIndex === state.selectedIndex;
|
|
148
|
+
const displayNum = actualIndex + 1 + numberOffset;
|
|
149
|
+
|
|
150
|
+
const parts: string[] = [];
|
|
151
|
+
|
|
152
|
+
parts.push(isSelected ? brand("›") : " ", " ");
|
|
153
|
+
|
|
154
|
+
if (showNumbers) {
|
|
155
|
+
if (displayNum <= 35) {
|
|
156
|
+
const shortcut = displayNum <= 9
|
|
157
|
+
? String(displayNum)
|
|
158
|
+
: String.fromCharCode(96 + displayNum - 9); // 10='a', 11='b', etc.
|
|
159
|
+
parts.push(isSelected ? brand(`[${shortcut}]`) : dim(`[${shortcut}]`), " ");
|
|
160
|
+
} else {
|
|
161
|
+
parts.push(" ");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Render label, then use remaining space for meta
|
|
166
|
+
const labelText = item.label;
|
|
167
|
+
const availableForContent = maxWidth - prefixWidth;
|
|
168
|
+
|
|
169
|
+
if (item.meta) {
|
|
170
|
+
// Split space between label and meta dynamically
|
|
171
|
+
const metaText = item.meta;
|
|
172
|
+
const totalNeeded = labelText.length + 1 + metaText.length; // 1 for space
|
|
173
|
+
|
|
174
|
+
if (totalNeeded <= availableForContent) {
|
|
175
|
+
// Both fit - no truncation needed
|
|
176
|
+
parts.push(isSelected ? labelText : dim(labelText));
|
|
177
|
+
const padding = availableForContent - labelText.length - metaText.length;
|
|
178
|
+
parts.push(" ".repeat(Math.max(1, padding)), dim(metaText));
|
|
179
|
+
} else {
|
|
180
|
+
// Need to truncate - prioritize label, give rest to meta
|
|
181
|
+
const labelMax = Math.min(labelText.length, Math.floor(availableForContent * 0.4));
|
|
182
|
+
const metaMax = availableForContent - labelMax - 1;
|
|
183
|
+
const label = truncate(labelText, labelMax);
|
|
184
|
+
parts.push(isSelected ? label : dim(label));
|
|
185
|
+
parts.push(" ", dim(truncate(metaText, metaMax)));
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const label = truncate(labelText, availableForContent);
|
|
189
|
+
parts.push(isSelected ? label : dim(label));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push(parts.join(""));
|
|
193
|
+
|
|
194
|
+
if (isSelected && item.description) {
|
|
195
|
+
lines.push(` ${dim(truncate(item.description, maxWidth - 5))}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (start > 0) lines.unshift(` ${dim("↑ more above")}`);
|
|
200
|
+
if (end < state.items.length) lines.push(` ${dim("↓ more below")}`);
|
|
201
|
+
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create a list section with title
|
|
207
|
+
*/
|
|
208
|
+
export function listSection<T>(
|
|
209
|
+
title: string,
|
|
210
|
+
state: ListSelectState<T>,
|
|
211
|
+
options: ListSelectOptions = {},
|
|
212
|
+
): string {
|
|
213
|
+
const header = ` ${dim(title)} ${dim(`(${state.items.length})`)}`;
|
|
214
|
+
return `${header}\n${renderList(state, options)}`;
|
|
215
|
+
}
|