@bahulam/code 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/package.json +56 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +223 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +312 -0
- package/src/commands/agent.mjs +221 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +202 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/model-catalog.mjs +57 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/prose-chunker.mjs +255 -0
- package/src/context/retriever.mjs +425 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +145 -0
- package/src/core/approval.mjs +700 -0
- package/src/core/attachments.mjs +666 -0
- package/src/core/backend-url.mjs +68 -0
- package/src/core/bundled-runtime.mjs +418 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +331 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +460 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +538 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +388 -0
- package/src/core/safety.mjs +260 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +957 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +2231 -0
- package/src/core/trust.mjs +160 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +374 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +700 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +122 -0
- package/src/terminal/agents.mjs +353 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +310 -0
- package/src/terminal/repl-ask-form.mjs +120 -0
- package/src/terminal/repl-explore.mjs +44 -0
- package/src/terminal/repl-format.mjs +317 -0
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +833 -0
- package/src/terminal/repl-resume.mjs +640 -0
- package/src/terminal/repl-state.mjs +120 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +5032 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +392 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +703 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +510 -0
- package/src/ui/banner.mjs +232 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +170 -0
- package/src/ui/input-dock.mjs +772 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +257 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +167 -0
- package/src/ui/term.mjs +174 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +740 -0
- package/src/ui/tool-details.mjs +504 -0
- package/src/ui/transcript-block.mjs +20 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI Terminal Renderer — cursor control, box drawing, status bars.
|
|
3
|
+
*
|
|
4
|
+
* Color helpers (the `c` object) now route through the semantic palette
|
|
5
|
+
* (`src/ui/palette.mjs`) so the entire CLI honors the Kepler brand and
|
|
6
|
+
* tier fallbacks (truecolor, ansi256, ansi16, none) without touching
|
|
7
|
+
* each call site. Hot-swap-friendly: external semantics like `c.red`,
|
|
8
|
+
* `c.bold`, `c.cyan` are preserved as the legacy contract; new code
|
|
9
|
+
* should prefer importing `paint` directly.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { paint } from '../ui/palette.mjs';
|
|
13
|
+
|
|
14
|
+
const ESC = '\x1b[';
|
|
15
|
+
const write = (s) => process.stderr.write(s);
|
|
16
|
+
|
|
17
|
+
// ── Cursor Control ──
|
|
18
|
+
|
|
19
|
+
export const cursor = {
|
|
20
|
+
hide: () => write(`${ESC}?25l`),
|
|
21
|
+
show: () => write(`${ESC}?25h`),
|
|
22
|
+
save: () => write(`${ESC}s`),
|
|
23
|
+
restore: () => write(`${ESC}u`),
|
|
24
|
+
to: (row, col) => write(`${ESC}${row};${col}H`),
|
|
25
|
+
up: (n = 1) => write(`${ESC}${n}A`),
|
|
26
|
+
down: (n = 1) => write(`${ESC}${n}B`),
|
|
27
|
+
right: (n = 1) => write(`${ESC}${n}C`),
|
|
28
|
+
left: (n = 1) => write(`${ESC}${n}D`),
|
|
29
|
+
col: (n = 1) => write(`${ESC}${n}G`),
|
|
30
|
+
clearLine: () => write(`${ESC}2K`),
|
|
31
|
+
clearDown: () => write(`${ESC}J`),
|
|
32
|
+
clearScreen: () => write(`${ESC}2J${ESC}H`),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ── Colors ──
|
|
36
|
+
// Legacy color names re-mapped onto semantic palette tokens. The CLI's
|
|
37
|
+
// branding is centralized in palette.mjs; this object is preserved only
|
|
38
|
+
// so existing imports keep compiling. Internal Kepler color choices are
|
|
39
|
+
// documented next to each mapping for the next code review.
|
|
40
|
+
|
|
41
|
+
const identity = (s) => String(s ?? '');
|
|
42
|
+
|
|
43
|
+
export const c = {
|
|
44
|
+
reset: identity, // palette already wraps with RESET
|
|
45
|
+
|
|
46
|
+
// Styles — work at every tier
|
|
47
|
+
bold: paint.bold,
|
|
48
|
+
dim: paint.dim,
|
|
49
|
+
italic: paint.italic,
|
|
50
|
+
underline: paint.underline,
|
|
51
|
+
|
|
52
|
+
// State semantics
|
|
53
|
+
red: paint.state.danger, // failure / hard error
|
|
54
|
+
green: paint.state.success, // pass / aligned
|
|
55
|
+
yellow: paint.state.warn, // soft warn / retry
|
|
56
|
+
|
|
57
|
+
// Brand semantics
|
|
58
|
+
blue: paint.brand.primary, // headers, primary brand
|
|
59
|
+
magenta: paint.brand.accent, // attention required
|
|
60
|
+
brand: paint.brand.primary, // primary brand surface
|
|
61
|
+
cyan: paint.brand.data, // code / file paths
|
|
62
|
+
cyanRegular: paint.brand.data,
|
|
63
|
+
cyanBold: (s) => paint.bold(paint.brand.data(s)),
|
|
64
|
+
|
|
65
|
+
// Text semantics
|
|
66
|
+
white: paint.text.primary, // primary text
|
|
67
|
+
gray: paint.text.dim, // hints, metadata, dim text
|
|
68
|
+
|
|
69
|
+
// Backgrounds — kept as raw ANSI; rarely used and have no palette analog
|
|
70
|
+
bgRed: (s) => `${ESC}41m${String(s ?? '')}${ESC}0m`,
|
|
71
|
+
bgGreen: (s) => `${ESC}42m${String(s ?? '')}${ESC}0m`,
|
|
72
|
+
bgCyan: (s) => `${ESC}46m${String(s ?? '')}${ESC}0m`,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ── Box Drawing ──
|
|
76
|
+
|
|
77
|
+
const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
|
|
78
|
+
|
|
79
|
+
export function drawBox(content, { borderColor = 'brand', width } = {}) {
|
|
80
|
+
const w = width || (process.stdout.columns || 80) - 2;
|
|
81
|
+
const colorFn = c[borderColor] || c.brand;
|
|
82
|
+
const lines = content.split('\n');
|
|
83
|
+
|
|
84
|
+
write(colorFn(`${BOX.tl}${BOX.h.repeat(w)}${BOX.tr}`) + '\n');
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const plain = stripAnsi(line);
|
|
87
|
+
const pad = Math.max(0, w - plain.length);
|
|
88
|
+
write(`${colorFn(BOX.v)} ${line}${' '.repeat(pad)}${colorFn(BOX.v)}\n`);
|
|
89
|
+
}
|
|
90
|
+
write(colorFn(`${BOX.bl}${BOX.h.repeat(w)}${BOX.br}`) + '\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Progress Bar ──
|
|
94
|
+
|
|
95
|
+
export function progressBar(percent, width = 20, label = '') {
|
|
96
|
+
const p = Math.max(0, Math.min(100, percent));
|
|
97
|
+
const filled = Math.round((p / 100) * width);
|
|
98
|
+
const empty = width - filled;
|
|
99
|
+
const color = p < 50 ? c.green : p < 80 ? c.yellow : c.red;
|
|
100
|
+
const bar = color('█'.repeat(filled)) + c.gray('░'.repeat(empty));
|
|
101
|
+
const pct = `${Math.round(p)}%`.padStart(4);
|
|
102
|
+
return label ? `${c.gray(label.padEnd(8))}${bar} ${pct}` : `${bar} ${pct}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Spinner ──
|
|
106
|
+
|
|
107
|
+
const SPINNER_FRAMES = ['◐', '◓', '◑', '◒'];
|
|
108
|
+
let _spinnerIdx = 0;
|
|
109
|
+
|
|
110
|
+
export function spinner(text) {
|
|
111
|
+
const frame = SPINNER_FRAMES[_spinnerIdx % SPINNER_FRAMES.length];
|
|
112
|
+
_spinnerIdx++;
|
|
113
|
+
return `${c.brand(frame)} ${c.brand(text)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── In-Place Update ──
|
|
117
|
+
|
|
118
|
+
let _lastLineCount = 0;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Write text, erasing the previous in-place output.
|
|
122
|
+
* Call with '' to clear.
|
|
123
|
+
*/
|
|
124
|
+
export function inPlace(text) {
|
|
125
|
+
// Erase previous lines
|
|
126
|
+
if (_lastLineCount > 0) {
|
|
127
|
+
for (let i = 0; i < _lastLineCount; i++) {
|
|
128
|
+
cursor.up();
|
|
129
|
+
cursor.clearLine();
|
|
130
|
+
cursor.col(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (text) {
|
|
134
|
+
write(text + '\n');
|
|
135
|
+
_lastLineCount = text.split('\n').length;
|
|
136
|
+
} else {
|
|
137
|
+
_lastLineCount = 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Status Bar (persistent bottom) ──
|
|
142
|
+
|
|
143
|
+
export function statusBar(parts) {
|
|
144
|
+
const sep = c.gray(' ┃ ');
|
|
145
|
+
const line = parts.join(sep);
|
|
146
|
+
// Save position, go to bottom, write, restore
|
|
147
|
+
const rows = process.stdout.rows || 30;
|
|
148
|
+
cursor.save();
|
|
149
|
+
cursor.to(rows, 1);
|
|
150
|
+
cursor.clearLine();
|
|
151
|
+
write(` ${line}`);
|
|
152
|
+
cursor.restore();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Helpers ──
|
|
156
|
+
|
|
157
|
+
export function stripAnsi(str) {
|
|
158
|
+
return String(str ?? '').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function visibleWidth(str) {
|
|
162
|
+
let width = 0;
|
|
163
|
+
for (const char of stripAnsi(str)) {
|
|
164
|
+
const code = char.codePointAt(0);
|
|
165
|
+
if (code === undefined) continue;
|
|
166
|
+
if (code === 0) continue;
|
|
167
|
+
if (code < 32 || (code >= 0x7f && code < 0xa0)) continue;
|
|
168
|
+
if (code >= 0x300 && code <= 0x36f) continue;
|
|
169
|
+
if (code >= 0xfe00 && code <= 0xfe0f) continue;
|
|
170
|
+
if (isWideCodePoint(code)) width += 2;
|
|
171
|
+
else width += 1;
|
|
172
|
+
}
|
|
173
|
+
return width;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isWideCodePoint(code) {
|
|
177
|
+
return (
|
|
178
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
179
|
+
code === 0x2329 ||
|
|
180
|
+
code === 0x232a ||
|
|
181
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
182
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
183
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
184
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
185
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
186
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
187
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
188
|
+
(code >= 0x1f300 && code <= 0x1faff)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function truncate(str, max) {
|
|
193
|
+
if (!str) return '';
|
|
194
|
+
const plain = stripAnsi(str);
|
|
195
|
+
if (plain.length <= max) return str;
|
|
196
|
+
return str.slice(0, max - 3) + '...';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function hr(char = '─', color = 'gray') {
|
|
200
|
+
const w = process.stdout.columns || 80;
|
|
201
|
+
write(c[color](char.repeat(w)) + '\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Markdown Rendering ──
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Render markdown to ANSI-styled terminal text.
|
|
208
|
+
* Supports: headers, bold, italic, code, code blocks, tables, blockquotes,
|
|
209
|
+
* task lists, lists, and links.
|
|
210
|
+
*/
|
|
211
|
+
export function renderMarkdown(text) {
|
|
212
|
+
if (!text) return '';
|
|
213
|
+
|
|
214
|
+
const lines = normalizeMarkdownFlow(text).split('\n');
|
|
215
|
+
const out = [];
|
|
216
|
+
let inCodeBlock = false;
|
|
217
|
+
let codeLang = '';
|
|
218
|
+
const columns = markdownColumns();
|
|
219
|
+
|
|
220
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
221
|
+
const line = lines[lineIndex];
|
|
222
|
+
// Code block start/end
|
|
223
|
+
if (line.trimStart().startsWith('```')) {
|
|
224
|
+
if (inCodeBlock) {
|
|
225
|
+
out.push(c.gray(' └' + '─'.repeat(40)));
|
|
226
|
+
inCodeBlock = false;
|
|
227
|
+
codeLang = '';
|
|
228
|
+
} else {
|
|
229
|
+
codeLang = line.trim().slice(3).trim();
|
|
230
|
+
out.push(c.gray(' ┌' + '─'.repeat(4) + (codeLang ? ` ${codeLang} ` : '') + '─'.repeat(Math.max(0, 35 - codeLang.length))));
|
|
231
|
+
inCodeBlock = true;
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (inCodeBlock) {
|
|
237
|
+
out.push(c.gray(' │ ') + renderCodeLine(line, codeLang));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// GitHub-flavored Markdown table
|
|
242
|
+
if (
|
|
243
|
+
line.includes('|') &&
|
|
244
|
+
lineIndex + 1 < lines.length &&
|
|
245
|
+
isTableSeparator(lines[lineIndex + 1])
|
|
246
|
+
) {
|
|
247
|
+
const headers = parseTableRow(line);
|
|
248
|
+
const rows = [];
|
|
249
|
+
lineIndex += 2;
|
|
250
|
+
while (lineIndex < lines.length && lines[lineIndex].includes('|') && lines[lineIndex].trim()) {
|
|
251
|
+
rows.push(parseTableRow(lines[lineIndex]));
|
|
252
|
+
lineIndex++;
|
|
253
|
+
}
|
|
254
|
+
lineIndex--;
|
|
255
|
+
out.push(...renderMarkdownTable(headers, rows));
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Headers
|
|
260
|
+
if (line.startsWith('### ')) {
|
|
261
|
+
out.push(c.bold(c.brand(line.slice(4))));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (line.startsWith('## ')) {
|
|
265
|
+
out.push(c.bold(c.brand(line.slice(3))));
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (line.startsWith('# ')) {
|
|
269
|
+
out.push(c.bold(c.brand(line.slice(2))));
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Horizontal rule
|
|
274
|
+
if (/^---+$/.test(line.trim())) {
|
|
275
|
+
out.push(c.gray('─'.repeat(40)));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Blockquotes
|
|
280
|
+
if (/^\s*>\s?/.test(line)) {
|
|
281
|
+
const content = line.replace(/^\s*>\s?/, '');
|
|
282
|
+
out.push(...renderWrappedMarkdownLine(c.gray(' │') + ' ', ' ', content, columns, c.italic));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Task lists
|
|
287
|
+
const task = line.match(/^(\s*)[-*]\s+\[([ xX])\]\s+(.*)/);
|
|
288
|
+
if (task) {
|
|
289
|
+
const done = task[2].toLowerCase() === 'x';
|
|
290
|
+
const renderState = done ? c.green : c.gray;
|
|
291
|
+
const marker = done ? '✓' : '○';
|
|
292
|
+
out.push(...renderWrappedMarkdownLine(
|
|
293
|
+
`${task[1]} `,
|
|
294
|
+
`${task[1]} `,
|
|
295
|
+
`${marker} ${task[3]}`,
|
|
296
|
+
columns,
|
|
297
|
+
renderState,
|
|
298
|
+
));
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Lists
|
|
303
|
+
if (/^\s*[-*]\s/.test(line)) {
|
|
304
|
+
const indent = line.match(/^(\s*)/)[1];
|
|
305
|
+
const content = line.replace(/^\s*[-*]\s/, '');
|
|
306
|
+
out.push(...renderWrappedMarkdownLine(
|
|
307
|
+
`${indent} ${c.brand('•')} `,
|
|
308
|
+
`${indent} `,
|
|
309
|
+
content,
|
|
310
|
+
columns,
|
|
311
|
+
inlineMarkdown,
|
|
312
|
+
));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Numbered lists
|
|
317
|
+
if (/^\s*\d+\.\s/.test(line)) {
|
|
318
|
+
const match = line.match(/^(\s*)(\d+)\.\s(.*)/);
|
|
319
|
+
if (match) {
|
|
320
|
+
const marker = `${match[2]}.`;
|
|
321
|
+
out.push(...renderWrappedMarkdownLine(
|
|
322
|
+
`${match[1]} ${c.brand(marker)} `,
|
|
323
|
+
`${match[1]}${' '.repeat(marker.length + 3)}`,
|
|
324
|
+
match[3],
|
|
325
|
+
columns,
|
|
326
|
+
inlineMarkdown,
|
|
327
|
+
));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Regular paragraph line. Wrap before writing so the terminal does not
|
|
333
|
+
// split long words at the viewport edge.
|
|
334
|
+
const indent = line.match(/^(\s*)/)[1] || '';
|
|
335
|
+
const content = line.slice(indent.length);
|
|
336
|
+
out.push(...renderWrappedMarkdownLine(indent, indent, content, columns, inlineMarkdown));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return out.join('\n');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function normalizeMarkdownFlow(text) {
|
|
343
|
+
const source = String(text || '').split('\n');
|
|
344
|
+
const normalized = [];
|
|
345
|
+
let paragraph = [];
|
|
346
|
+
let inCodeBlock = false;
|
|
347
|
+
|
|
348
|
+
const flushParagraph = () => {
|
|
349
|
+
if (!paragraph.length) return;
|
|
350
|
+
normalized.push(paragraph.map(line => line.trim()).join(' '));
|
|
351
|
+
paragraph = [];
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const isStructural = (line) => {
|
|
355
|
+
const trimmed = line.trim();
|
|
356
|
+
return (
|
|
357
|
+
!trimmed ||
|
|
358
|
+
trimmed.startsWith('#') ||
|
|
359
|
+
trimmed.startsWith('>') ||
|
|
360
|
+
trimmed.startsWith('|') ||
|
|
361
|
+
/^---+$/.test(trimmed) ||
|
|
362
|
+
/^\s*[-*]\s+/.test(line) ||
|
|
363
|
+
/^\s*[-*]\s+\[[ xX]\]\s+/.test(line) ||
|
|
364
|
+
/^\s*\d+\.\s+/.test(line)
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const appendToPreviousList = (line) => {
|
|
369
|
+
if (!normalized.length) return false;
|
|
370
|
+
const previous = normalized[normalized.length - 1];
|
|
371
|
+
if (!/^\s*(?:[-*]|\d+\.)\s+/.test(previous)) return false;
|
|
372
|
+
if (isStructural(line)) return false;
|
|
373
|
+
normalized[normalized.length - 1] = `${previous} ${line.trim()}`;
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
for (const line of source) {
|
|
378
|
+
if (line.trimStart().startsWith('```')) {
|
|
379
|
+
flushParagraph();
|
|
380
|
+
normalized.push(line);
|
|
381
|
+
inCodeBlock = !inCodeBlock;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (inCodeBlock) {
|
|
386
|
+
normalized.push(line);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!line.trim()) {
|
|
391
|
+
flushParagraph();
|
|
392
|
+
normalized.push('');
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (appendToPreviousList(line)) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (isStructural(line)) {
|
|
401
|
+
flushParagraph();
|
|
402
|
+
normalized.push(line);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
paragraph.push(line);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
flushParagraph();
|
|
410
|
+
return normalized.join('\n');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function markdownColumns() {
|
|
414
|
+
// Every content call site writes rendered lines with a leading " " (2 col
|
|
415
|
+
// indent). If we wrap at the full terminal width, those two extra columns
|
|
416
|
+
// push each line past the viewport and the terminal hard-wraps at the
|
|
417
|
+
// character boundary — splitting words like "wro/ng" or "multip/le". Reserve
|
|
418
|
+
// that indent (plus one column of safety for wide-char surprises) so the
|
|
419
|
+
// renderer's soft-wrap does the whole job.
|
|
420
|
+
const raw = process.stdout.columns || process.stderr.columns || 100;
|
|
421
|
+
return Math.max(40, raw - 3);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderWrappedMarkdownLine(firstPrefix, continuationPrefix, content, columns, renderContent) {
|
|
425
|
+
const firstWidth = Math.max(12, columns - stripAnsi(firstPrefix).length);
|
|
426
|
+
const nextWidth = Math.max(12, columns - stripAnsi(continuationPrefix).length);
|
|
427
|
+
const wrapped = wrapWords(String(content || ''), firstWidth, nextWidth);
|
|
428
|
+
return wrapped.map((part, index) => {
|
|
429
|
+
const prefix = index === 0 ? firstPrefix : continuationPrefix;
|
|
430
|
+
return `${prefix}${renderContent(part)}`;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function wrapWords(text, firstWidth, nextWidth) {
|
|
435
|
+
const words = text.replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
|
|
436
|
+
if (!words.length) return [''];
|
|
437
|
+
|
|
438
|
+
const lines = [];
|
|
439
|
+
let width = firstWidth;
|
|
440
|
+
let current = '';
|
|
441
|
+
|
|
442
|
+
for (const word of words) {
|
|
443
|
+
if (!current && word.length > width) {
|
|
444
|
+
lines.push(...splitLongWord(word, width));
|
|
445
|
+
width = nextWidth;
|
|
446
|
+
current = '';
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (!current) {
|
|
450
|
+
current = word;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (current.length + 1 + word.length <= width) {
|
|
454
|
+
current += ` ${word}`;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
lines.push(current);
|
|
458
|
+
width = nextWidth;
|
|
459
|
+
if (word.length > width) {
|
|
460
|
+
lines.push(...splitLongWord(word, width));
|
|
461
|
+
current = '';
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
current = word;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (current) lines.push(current);
|
|
468
|
+
return lines;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function splitLongWord(word, width) {
|
|
472
|
+
const chunks = [];
|
|
473
|
+
const safeWidth = Math.max(8, width);
|
|
474
|
+
for (let i = 0; i < word.length; i += safeWidth) {
|
|
475
|
+
chunks.push(word.slice(i, i + safeWidth));
|
|
476
|
+
}
|
|
477
|
+
return chunks;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function renderCodeLine(line, language) {
|
|
481
|
+
const lang = String(language || '').toLowerCase();
|
|
482
|
+
if (lang === 'diff') {
|
|
483
|
+
if (line.startsWith('+')) return c.green(line);
|
|
484
|
+
if (line.startsWith('-')) return c.red(line);
|
|
485
|
+
if (line.startsWith('@@')) return c.brand(line);
|
|
486
|
+
}
|
|
487
|
+
if (lang === 'json' || lang === 'yaml' || lang === 'yml' || lang === 'toml') {
|
|
488
|
+
return line.replace(/^(\s*)(["']?[\w.-]+["']?)(\s*[:=])(.*)$/, (_, space, key, separator, value) =>
|
|
489
|
+
`${space}${c.cyanBold(key)}${c.gray(separator)}${c.cyanRegular(value)}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return c.cyan(line);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function parseTableRow(line) {
|
|
496
|
+
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => cell.trim());
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function isTableSeparator(line) {
|
|
500
|
+
const cells = parseTableRow(line);
|
|
501
|
+
return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(cell));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function renderMarkdownTable(headers, rows) {
|
|
505
|
+
const columnCount = Math.max(headers.length, ...rows.map(row => row.length));
|
|
506
|
+
const widths = markdownTableWidths(headers, rows, columnCount);
|
|
507
|
+
const border = c.gray(` ${widths.map(width => '─'.repeat(width + 2)).join('┼')}`);
|
|
508
|
+
const formatRow = (row, header = false) => renderMarkdownTableRow(row, widths, header);
|
|
509
|
+
return [formatRow(headers, true), border, ...rows.map(row => formatRow(row))];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function markdownTableWidths(headers, rows, columnCount) {
|
|
513
|
+
const columns = markdownColumns();
|
|
514
|
+
const contentBudget = Math.max(columnCount * 8, columns - (3 * columnCount) - 1);
|
|
515
|
+
const desired = Array.from({ length: columnCount }, (_, index) => {
|
|
516
|
+
const values = [headers[index] || '', ...rows.map(row => row[index] || '')];
|
|
517
|
+
return Math.max(...values.map(value => visibleWidth(markdownTableCellText(value))), 3);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
if (columnCount === 1) return [Math.min(desired[0], contentBudget)];
|
|
521
|
+
|
|
522
|
+
if (columnCount === 2) {
|
|
523
|
+
const firstMax = Math.max(12, Math.min(32, contentBudget - 24));
|
|
524
|
+
const first = Math.max(8, Math.min(desired[0], firstMax));
|
|
525
|
+
const second = Math.max(16, contentBudget - first);
|
|
526
|
+
return [first, second];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const minWidths = desired.map((width, index) => {
|
|
530
|
+
const headerWidth = visibleWidth(markdownTableCellText(headers[index] || ''));
|
|
531
|
+
return Math.min(width, Math.max(8, Math.min(headerWidth || 8, 16)));
|
|
532
|
+
});
|
|
533
|
+
const widths = desired.map(width => Math.min(width, 30));
|
|
534
|
+
|
|
535
|
+
while (widths.reduce((sum, width) => sum + width, 0) > contentBudget) {
|
|
536
|
+
let shrinkIndex = -1;
|
|
537
|
+
let widest = -1;
|
|
538
|
+
for (let index = 0; index < widths.length; index++) {
|
|
539
|
+
if (widths[index] > minWidths[index] && widths[index] > widest) {
|
|
540
|
+
widest = widths[index];
|
|
541
|
+
shrinkIndex = index;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (shrinkIndex < 0) break;
|
|
545
|
+
widths[shrinkIndex]--;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return widths;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function renderMarkdownTableRow(row, widths, header = false) {
|
|
552
|
+
const wrapped = widths.map((width, index) => {
|
|
553
|
+
const value = markdownTableCellText(row[index] || '');
|
|
554
|
+
const lines = wrapWords(value, width, width);
|
|
555
|
+
return lines.length ? lines : [''];
|
|
556
|
+
});
|
|
557
|
+
const height = Math.max(...wrapped.map(lines => lines.length), 1);
|
|
558
|
+
const rendered = [];
|
|
559
|
+
|
|
560
|
+
for (let lineIndex = 0; lineIndex < height; lineIndex++) {
|
|
561
|
+
const cells = widths.map((width, index) => {
|
|
562
|
+
const value = wrapped[index][lineIndex] || '';
|
|
563
|
+
const padded = value + ' '.repeat(Math.max(0, width - visibleWidth(value)));
|
|
564
|
+
return header ? c.bold(padded) : padded;
|
|
565
|
+
});
|
|
566
|
+
rendered.push(` ${cells.map(cell => ` ${cell} `).join(c.gray('│'))}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return rendered.join('\n');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function markdownTableCellText(value) {
|
|
573
|
+
return stripAnsi(String(value || ''))
|
|
574
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
|
|
575
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
576
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
577
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
578
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
579
|
+
.replace(/_([^_]+)_/g, '$1')
|
|
580
|
+
.replace(/\s+/g, ' ')
|
|
581
|
+
.trim();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Apply inline markdown: **bold**, *italic*, `code`, [links](url)
|
|
586
|
+
*/
|
|
587
|
+
function inlineMarkdown(text) {
|
|
588
|
+
return text
|
|
589
|
+
.replace(/\*\*(.+?)\*\*/g, (_, s) => c.bold(s))
|
|
590
|
+
.replace(/\*(.+?)\*/g, (_, s) => c.italic(s))
|
|
591
|
+
.replace(/`(.+?)`/g, (_, s) => c.cyan(s))
|
|
592
|
+
.replace(
|
|
593
|
+
/\[(.+?)\]\((.+?)\)/g,
|
|
594
|
+
(_, label, url) => `${c.underline(c.white(label))} ${c.gray('(' + url + ')')}`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── Diff Display ──
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Render a unified diff with +/- color highlighting.
|
|
602
|
+
*/
|
|
603
|
+
export function renderDiff(diffText) {
|
|
604
|
+
if (!diffText) return '';
|
|
605
|
+
const lines = diffText.split('\n');
|
|
606
|
+
const out = [];
|
|
607
|
+
for (const line of lines) {
|
|
608
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
609
|
+
out.push(c.bold(line));
|
|
610
|
+
} else if (line.startsWith('@@')) {
|
|
611
|
+
out.push(c.brand(line));
|
|
612
|
+
} else if (line.startsWith('+')) {
|
|
613
|
+
out.push(c.green(line));
|
|
614
|
+
} else if (line.startsWith('-')) {
|
|
615
|
+
out.push(c.red(line));
|
|
616
|
+
} else {
|
|
617
|
+
out.push(c.gray(line));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return out.join('\n');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── Info Panel ──
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Display a labeled info panel (no box borders, just indented content).
|
|
627
|
+
* @param {string} label - Panel title
|
|
628
|
+
* @param {Array<[string, string]>} rows - [label, value] pairs
|
|
629
|
+
* @param {string} [color='brand'] - Title color
|
|
630
|
+
*/
|
|
631
|
+
export function infoPanel(label, rows, color = 'brand') {
|
|
632
|
+
const colorFn = c[color] || c.brand;
|
|
633
|
+
write(` ${colorFn(c.bold(label))}\n`);
|
|
634
|
+
write(` ${c.gray('─'.repeat(Math.min(40, (process.stdout.columns || 80) - 4)))}\n`);
|
|
635
|
+
for (const [key, val] of rows) {
|
|
636
|
+
write(` ${c.gray(key.padEnd(14))} ${val}\n`);
|
|
637
|
+
}
|
|
638
|
+
write('\n');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ── Table Display ──
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Render a simple table.
|
|
645
|
+
* @param {string[]} headers
|
|
646
|
+
* @param {string[][]} rows
|
|
647
|
+
*/
|
|
648
|
+
export function table(headers, rows) {
|
|
649
|
+
const widths = headers.map((h, i) => {
|
|
650
|
+
const maxRow = rows.reduce((max, row) => Math.max(max, stripAnsi(row[i] || '').length), 0);
|
|
651
|
+
return Math.max(stripAnsi(h).length, maxRow) + 2;
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Header
|
|
655
|
+
write(' ' + headers.map((h, i) => c.bold(c.brand(h.padEnd(widths[i])))).join('') + '\n');
|
|
656
|
+
write(' ' + widths.map(w => c.gray('─'.repeat(w))).join('') + '\n');
|
|
657
|
+
|
|
658
|
+
// Rows
|
|
659
|
+
for (const row of rows) {
|
|
660
|
+
write(' ' + row.map((cell, i) => {
|
|
661
|
+
const plain = stripAnsi(cell || '');
|
|
662
|
+
const pad = Math.max(0, widths[i] - plain.length);
|
|
663
|
+
return (cell || '') + ' '.repeat(pad);
|
|
664
|
+
}).join('') + '\n');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── Elapsed Timer ──
|
|
669
|
+
|
|
670
|
+
export function formatElapsed(startMs) {
|
|
671
|
+
const s = Math.floor((Date.now() - startMs) / 1000);
|
|
672
|
+
if (s < 60) return `${s}s`;
|
|
673
|
+
return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ── Format Cost ──
|
|
677
|
+
|
|
678
|
+
import { calculateCost, formatCostValue, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Format cost from token counts.
|
|
682
|
+
* Accepts either (inputTokens, outputTokens) for legacy calls,
|
|
683
|
+
* or a single usage object with optional per-model breakdown.
|
|
684
|
+
*/
|
|
685
|
+
export function formatCost(inputOrUsage, outputTokens) {
|
|
686
|
+
if (typeof inputOrUsage === 'object' && inputOrUsage !== null) {
|
|
687
|
+
const { total } = calculateCost(inputOrUsage);
|
|
688
|
+
return formatCredits(costToCredits(total));
|
|
689
|
+
}
|
|
690
|
+
const { total } = calculateCost({
|
|
691
|
+
input_tokens: inputOrUsage || 0,
|
|
692
|
+
output_tokens: outputTokens || 0,
|
|
693
|
+
});
|
|
694
|
+
return formatCredits(costToCredits(total));
|
|
695
|
+
}
|