@pugi/cli 0.1.0-beta.7 → 0.1.0-beta.87
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/CHANGELOG.md +96 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +2 -2
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +12 -12
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +140 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +286 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4162 -488
- package/dist/runtime/commands/agents.js +30 -30
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +32 -32
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +244 -13
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +222 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +623 -45
- package/dist/tools/brief.js +224 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +124 -44
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +23 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* — Vim-mode-aware REPL input.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper that sits BETWEEN the REPL session and the legacy
|
|
6
|
+
* `InputBox`. When vim mode is off it forwards every prop unchanged
|
|
7
|
+
* so existing operators see zero behavioural delta. When vim mode is
|
|
8
|
+
* on it intercepts keystrokes through `useInput` BEFORE Ink's normal
|
|
9
|
+
* dispatch and either:
|
|
10
|
+
*
|
|
11
|
+
* - in `insert` mode, lets the keystroke fall through to `InputBox`
|
|
12
|
+
* so the legacy buffer / cursor / history / palette code stays
|
|
13
|
+
* authoritative (we do NOT re-implement insert-mode editing);
|
|
14
|
+
* - in `normal` mode, routes the key through `core/vim/keymap.ts`
|
|
15
|
+
* and applies the result to a shadow buffer + cursor mirror that
|
|
16
|
+
* it then pushes back into `InputBox` via the existing `initial`
|
|
17
|
+
* prop on remount.
|
|
18
|
+
*
|
|
19
|
+
* Why a wrapper instead of inlining into `InputBox`?
|
|
20
|
+
*
|
|
21
|
+
* - Keeps the legacy input surface untouched for non-vim operators
|
|
22
|
+
* (the L26 ship risks zero regression to ~year-old code).
|
|
23
|
+
* - The wrapper renders a thin status row ABOVE the input frame so
|
|
24
|
+
* the active mode + pending sequence + cheat sheet are visible.
|
|
25
|
+
* - Tests can exercise the wrapper without dragging in the full
|
|
26
|
+
* ink + clipboard stack of `InputBox` (the spec drives the
|
|
27
|
+
* keymap directly; the wrapper is exercised via the runtime
|
|
28
|
+
* command tests + manual smoke from the REPL).
|
|
29
|
+
*
|
|
30
|
+
* The component intentionally only models the SINGLE-LINE REPL prompt
|
|
31
|
+
* buffer — that is the surface the upstream tool's `/vim` covers, and that
|
|
32
|
+
* is what the upstream behavior validated. Multi-line + visual-mode +
|
|
33
|
+
* counts are out of scope for this sprint.
|
|
34
|
+
*
|
|
35
|
+
* ─── Closure-staleness contract (post-L26 fix) ───
|
|
36
|
+
*
|
|
37
|
+
* Three-of-three reviewers flagged the original `setShadowLine`
|
|
38
|
+
* functional updater for reading `shadowCursor` and `pending` from
|
|
39
|
+
* the closure, which goes stale across consecutive keystrokes
|
|
40
|
+
* (React batches dispatch, so the second `d` of `dd` saw the
|
|
41
|
+
* `pending` value from the render that scheduled the first `d`,
|
|
42
|
+
* not the post-first-d value). Same problem for cursor advancement
|
|
43
|
+
* across chained `l`/`h`/`w`/`b` presses.
|
|
44
|
+
*
|
|
45
|
+
* Additionally, calling `props.onSubmit` inline inside a setState
|
|
46
|
+
* updater is double-fired by React strict mode (updaters MUST be
|
|
47
|
+
* pure — strict mode runs them twice to surface impurity).
|
|
48
|
+
*
|
|
49
|
+
* The fix moves shadow cursor + pending + mode into `useRef`
|
|
50
|
+
* (refs survive across renders, are read synchronously, and are
|
|
51
|
+
* not subject to closure capture), drives the keymap dispatch
|
|
52
|
+
* OUTSIDE any setState callback, and defers `props.onSubmit` /
|
|
53
|
+
* `props.onExit` invocations until AFTER the render via the
|
|
54
|
+
* `useEffect` queued by toggling a transition ref.
|
|
55
|
+
*
|
|
56
|
+
* The `line` state is the only React state we mutate per keystroke
|
|
57
|
+
* because the inner `InputBox` re-reads it via the `initial` prop
|
|
58
|
+
* on remount — refs alone cannot trigger that remount. Cursor + mode
|
|
59
|
+
* + pending are surfaced to the render path through the same
|
|
60
|
+
* `line`/`tick` rerender, so the status bar stays in sync without
|
|
61
|
+
* itself being captured by a stale closure.
|
|
62
|
+
*/
|
|
63
|
+
import { useEffect, useRef, useState } from 'react';
|
|
64
|
+
import { Box, Text, useInput } from 'ink';
|
|
65
|
+
import { handleNormalKey, PENDING_NONE, describePending, } from '../core/vim/keymap.js';
|
|
66
|
+
import { InputBox } from './input-box.js';
|
|
67
|
+
/**
|
|
68
|
+
* Render the mode badge + pending sequence + cheat sheet above the
|
|
69
|
+
* input frame. Two lines:
|
|
70
|
+
*
|
|
71
|
+
* ─ NORMAL ─ d: Esc=normal · i=insert · :w=submit · :q=cancel
|
|
72
|
+
* ─ INSERT ─ (mode-specific tail dropped when there's nothing to show)
|
|
73
|
+
*
|
|
74
|
+
* Plain ASCII + dim accents to match the rest of the REPL's chrome.
|
|
75
|
+
*/
|
|
76
|
+
function VimStatusBar(props) {
|
|
77
|
+
const modeLabel = props.mode === 'normal' ? '-- NORMAL --' : '-- INSERT --';
|
|
78
|
+
const pendingLabel = describePending(props.pending);
|
|
79
|
+
const hint = props.mode === 'normal'
|
|
80
|
+
? 'h/j/k/l move · i/a insert · x del · dd line · :w submit · :q cancel'
|
|
81
|
+
: 'Esc → normal mode';
|
|
82
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", bold: true, children: modeLabel }), pendingLabel.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: `${pendingLabel}` })] })) : null, _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: hint })] }));
|
|
83
|
+
}
|
|
84
|
+
export function VimInput(props) {
|
|
85
|
+
const { vimEnabled, initialMode, ...inputBoxProps } = props;
|
|
86
|
+
// When vim mode is off we DO NOT mount the `useInput` overlay — the
|
|
87
|
+
// legacy `InputBox` keeps full ownership of the keystrokes and
|
|
88
|
+
// behaves byte-for-byte the way it always did. This is the same
|
|
89
|
+
// pattern `output-style` followed (off → pass through; on → activate
|
|
90
|
+
// the modal surface).
|
|
91
|
+
if (!vimEnabled) {
|
|
92
|
+
return _jsx(InputBox, { ...inputBoxProps });
|
|
93
|
+
}
|
|
94
|
+
// ─── Render-driving state ─────────────────────────────────────
|
|
95
|
+
//
|
|
96
|
+
// `line` + `tick` are the only pieces of state the render path
|
|
97
|
+
// reads — the inner `InputBox` is remounted on every `tick` bump
|
|
98
|
+
// so the `initial` prop is honoured. Cursor / pending / mode are
|
|
99
|
+
// held in refs (see below) and mirrored into render state via the
|
|
100
|
+
// same setLine/setTick burst at the end of each keystroke so the
|
|
101
|
+
// status bar stays in lockstep without ever being captured by a
|
|
102
|
+
// stale closure inside a setState updater.
|
|
103
|
+
const [line, setLine] = useState(props.initial ?? '');
|
|
104
|
+
const [mode, setMode] = useState(initialMode ?? 'normal');
|
|
105
|
+
const [pending, setPending] = useState(PENDING_NONE);
|
|
106
|
+
const [remountTick, setRemountTick] = useState(0);
|
|
107
|
+
// ─── Closure-safe scratch refs ────────────────────────────────
|
|
108
|
+
//
|
|
109
|
+
// These refs are written synchronously inside the `useInput`
|
|
110
|
+
// handler and read on the NEXT keystroke. They survive across
|
|
111
|
+
// renders, are not captured by stale closures, and are NEVER
|
|
112
|
+
// mutated inside a setState updater — so React strict-mode's
|
|
113
|
+
// double-invocation cannot corrupt them.
|
|
114
|
+
//
|
|
115
|
+
// Why not `useReducer`? A reducer would also see fresh state on
|
|
116
|
+
// every dispatch, but the keymap result is a tagged union whose
|
|
117
|
+
// side effects (mode flip, submit, cancel, remount) are easier
|
|
118
|
+
// to read as imperative steps after the pure `handleNormalKey`
|
|
119
|
+
// call. Refs keep the dispatcher linear and the diff vs the
|
|
120
|
+
// pre-fix file minimal.
|
|
121
|
+
const lineRef = useRef(props.initial ?? '');
|
|
122
|
+
const cursorRef = useRef(props.initial?.length ?? 0);
|
|
123
|
+
const pendingRef = useRef(PENDING_NONE);
|
|
124
|
+
const modeRef = useRef(initialMode ?? 'normal');
|
|
125
|
+
// Deferred side-effect queue — submit / exit must NOT run inline
|
|
126
|
+
// inside a setState updater (strict mode double-invokes them).
|
|
127
|
+
// We stash the payload here and flush it from a `useEffect` after
|
|
128
|
+
// the render commits.
|
|
129
|
+
const pendingSubmitRef = useRef(null);
|
|
130
|
+
const pendingExitRef = useRef(false);
|
|
131
|
+
const [sideEffectTick, setSideEffectTick] = useState(0);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
// Flush any deferred host callback exactly once per scheduled
|
|
134
|
+
// burst. The refs are nulled BEFORE the call so a re-entrant
|
|
135
|
+
// host onSubmit handler that re-renders us cannot re-fire the
|
|
136
|
+
// same payload.
|
|
137
|
+
const submit = pendingSubmitRef.current;
|
|
138
|
+
if (submit !== null) {
|
|
139
|
+
pendingSubmitRef.current = null;
|
|
140
|
+
props.onSubmit(submit);
|
|
141
|
+
}
|
|
142
|
+
if (pendingExitRef.current) {
|
|
143
|
+
pendingExitRef.current = false;
|
|
144
|
+
props.onExit();
|
|
145
|
+
}
|
|
146
|
+
// Intentionally only depends on `sideEffectTick`: we want this
|
|
147
|
+
// effect to fire ONLY when the keystroke handler asked for it
|
|
148
|
+
// (via setSideEffectTick), not on every prop change.
|
|
149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
|
+
}, [sideEffectTick]);
|
|
151
|
+
useInput((input, key) => {
|
|
152
|
+
// Esc always returns to normal mode (and clears any pending
|
|
153
|
+
// sequence). This binding wins over `InputBox`'s own Esc handling
|
|
154
|
+
// because we own the `useInput` hook earlier in the render tree.
|
|
155
|
+
if (key.escape) {
|
|
156
|
+
modeRef.current = 'normal';
|
|
157
|
+
pendingRef.current = PENDING_NONE;
|
|
158
|
+
setMode('normal');
|
|
159
|
+
setPending(PENDING_NONE);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// In insert mode we relinquish dispatch entirely so the legacy
|
|
163
|
+
// input box owns typing / palette / history / clipboard / kill
|
|
164
|
+
// ring without interference. Returning `undefined` from a
|
|
165
|
+
// `useInput` callback is a no-op — InputBox's own `useInput` will
|
|
166
|
+
// receive the same event on the next frame.
|
|
167
|
+
if (modeRef.current === 'insert')
|
|
168
|
+
return;
|
|
169
|
+
// In normal mode we drive the buffer through the keymap.
|
|
170
|
+
//
|
|
171
|
+
// CRITICAL: all inputs to `handleNormalKey` are read from refs,
|
|
172
|
+
// not from the React state closure captured at render time.
|
|
173
|
+
// This is what makes consecutive keystrokes ("ll", "dd", ":w")
|
|
174
|
+
// observe the post-previous-keystroke state instead of the
|
|
175
|
+
// pre-batch render snapshot.
|
|
176
|
+
const ch = input ?? '';
|
|
177
|
+
const curLine = lineRef.current;
|
|
178
|
+
const curCursor = cursorRef.current;
|
|
179
|
+
const curPending = pendingRef.current;
|
|
180
|
+
const out = handleNormalKey({
|
|
181
|
+
line: curLine,
|
|
182
|
+
cursor: curCursor,
|
|
183
|
+
pending: curPending,
|
|
184
|
+
ch,
|
|
185
|
+
enter: key.return,
|
|
186
|
+
escape: false,
|
|
187
|
+
backspace: key.backspace || key.delete,
|
|
188
|
+
});
|
|
189
|
+
// Step 1: write the new pending state to BOTH the ref (read by
|
|
190
|
+
// the next keystroke synchronously) AND the React state (drives
|
|
191
|
+
// the status bar render).
|
|
192
|
+
pendingRef.current = out.pending;
|
|
193
|
+
setPending(out.pending);
|
|
194
|
+
// Step 2: apply the discriminated result. Each branch updates
|
|
195
|
+
// the refs first (synchronous, closure-safe) and then schedules
|
|
196
|
+
// any required React state update or deferred side effect.
|
|
197
|
+
switch (out.result.kind) {
|
|
198
|
+
case 'move': {
|
|
199
|
+
cursorRef.current = out.result.cursor;
|
|
200
|
+
// Cursor lives in the ref; the inner InputBox does not need
|
|
201
|
+
// a remount for a pure motion, so we skip the tick bump.
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'edit': {
|
|
205
|
+
cursorRef.current = out.result.cursor;
|
|
206
|
+
lineRef.current = out.result.line;
|
|
207
|
+
setLine(out.result.line);
|
|
208
|
+
setRemountTick((t) => t + 1);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case 'mode': {
|
|
212
|
+
cursorRef.current = out.result.cursor;
|
|
213
|
+
modeRef.current = out.result.mode;
|
|
214
|
+
setMode(out.result.mode);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case 'submit': {
|
|
218
|
+
// Forward to the host's onSubmit and clear the shadow buffer
|
|
219
|
+
// so the next prompt starts empty. We DO NOT re-route
|
|
220
|
+
// through InputBox's Enter handler — that path also writes
|
|
221
|
+
// history; submitting from `:w` should mirror that, so we
|
|
222
|
+
// call props.onSubmit indirectly via the deferred-effect
|
|
223
|
+
// queue. (The host's history append happens inside InputBox;
|
|
224
|
+
// for the modal path the host can observe via props.onSubmit
|
|
225
|
+
// and append manually if needed. The leak-parity surface for
|
|
226
|
+
// L26 documents `:w` as equivalent to Enter so this is fine.)
|
|
227
|
+
const payload = out.result.payload;
|
|
228
|
+
if (payload.trim().length > 0) {
|
|
229
|
+
// Queue the host callback; useEffect flushes it after the
|
|
230
|
+
// render commits. This avoids React strict-mode's
|
|
231
|
+
// double-invocation of setState updaters double-firing
|
|
232
|
+
// onSubmit (which would, e.g., submit the same prompt
|
|
233
|
+
// twice to the agent on every `:w`).
|
|
234
|
+
pendingSubmitRef.current = payload;
|
|
235
|
+
}
|
|
236
|
+
cursorRef.current = 0;
|
|
237
|
+
lineRef.current = '';
|
|
238
|
+
setLine('');
|
|
239
|
+
setRemountTick((t) => t + 1);
|
|
240
|
+
setSideEffectTick((t) => t + 1);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'cancel': {
|
|
244
|
+
cursorRef.current = 0;
|
|
245
|
+
lineRef.current = '';
|
|
246
|
+
setLine('');
|
|
247
|
+
setRemountTick((t) => t + 1);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case 'noop': {
|
|
251
|
+
// No-op result still may have advanced `pending` (e.g. the
|
|
252
|
+
// first `d` of `dd` arms the pending state). The pending ref
|
|
253
|
+
// and state were already updated above, so nothing else to
|
|
254
|
+
// do here.
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(VimStatusBar, { mode: mode, pending: pending }), _jsx(InputBox, { ...inputBoxProps,
|
|
260
|
+
// In normal mode we seed the (possibly mutated) shadow line +
|
|
261
|
+
// suppress the blink so the operator's caret is unambiguously
|
|
262
|
+
// controlled by h/l/0/$/w/b. In insert mode we hand the
|
|
263
|
+
// buffer back to InputBox as-is so the legacy typing path
|
|
264
|
+
// takes over.
|
|
265
|
+
initial: mode === 'normal' ? line : (props.initial ?? line), blinkCursor: mode === 'normal' ? false : (props.blinkCursor ?? true) }, `vim-${remountTick}`)] }));
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=vim-input.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { PUG_MASCOT, PUG_MASCOT_CYAN_MASK, PUG_MASCOT_MAX_WIDTH, } from './repl-splash-art.js';
|
|
4
|
+
/* ------------------------------------------------------------------ */
|
|
5
|
+
/* Layout constants */
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
/** Default left-column width when the parent box doesn't pin one. */
|
|
8
|
+
const LEFT_COLUMN_WIDTH = 44;
|
|
9
|
+
/** Default right-column width. */
|
|
10
|
+
const RIGHT_COLUMN_WIDTH = 44;
|
|
11
|
+
const ACCENT = '#3da9fc';
|
|
12
|
+
const BULLET = '·';
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
/* Component */
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
export function WelcomeBanner(props) {
|
|
17
|
+
const { data } = props;
|
|
18
|
+
const showHandCraftedMascot = props.mascotPrePrinted !== true;
|
|
19
|
+
const initToast = props.autoInitStatus === 'initialized'
|
|
20
|
+
? 'Pugi workspace initialised at .pugi/.'
|
|
21
|
+
: null;
|
|
22
|
+
const accountLine = formatAccountLine(data);
|
|
23
|
+
const modelLine = formatModelLine(data);
|
|
24
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { borderStyle: "round", borderColor: ACCENT, paddingX: 1, flexDirection: "row", children: [_jsx(LeftColumn, { greetingName: data.greetingName, cwd: data.cwd, modelLine: modelLine, accountLine: accountLine, cliVersion: data.cliVersion, showHandCraftedMascot: showHandCraftedMascot, initToast: initToast }), _jsx(Box, { width: 2 }), _jsx(RightColumn, { whatsNew: data.whatsNew })] }) }));
|
|
25
|
+
}
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/* Left column — greeting + mascot + status */
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
function LeftColumn({ greetingName, cwd, modelLine, accountLine, cliVersion, showHandCraftedMascot, initToast, }) {
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", width: LEFT_COLUMN_WIDTH, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: ACCENT, children: ".io" }), _jsx(Text, { dimColor: true, children: ` v${cliVersion}` })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: `Welcome back ${greetingName}!` }) }), showHandCraftedMascot ? (_jsx(Box, { marginTop: 1, children: _jsx(MascotColumn, {}) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: modelLine }), _jsx(Text, { dimColor: true, children: accountLine }), _jsx(Text, { dimColor: true, children: truncatePath(cwd, LEFT_COLUMN_WIDTH - 2) })] }), initToast ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: ACCENT, children: initToast }) })) : null] }));
|
|
31
|
+
}
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
/* Right column — tips + what's new */
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
function RightColumn({ whatsNew, }) {
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", width: RIGHT_COLUMN_WIDTH, children: [_jsx(Text, { dimColor: true, children: "Tips for getting started" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(TipRow, { text: "Run /init to scaffold PUGI.md instructions" }), _jsx(TipRow, { text: "Brief Pugi \u2014 the workforce dispatches" }), _jsx(TipRow, { text: "Triple-review gate before push: /review --triple" }), _jsx(TipRow, { text: "/help for every slash command" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(RIGHT_COLUMN_WIDTH - 2, 18)) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "What's new" }) }), _jsx(Box, { flexDirection: "column", children: whatsNew.length > 0 ? (whatsNew.map((line, index) => (_jsx(TipRow, { text: line }, `whatsnew-${index}`)))) : (_jsx(Text, { dimColor: true, children: ` ${BULLET} /release-notes for the full changelog` })) })] }));
|
|
37
|
+
}
|
|
38
|
+
function TipRow({ text }) {
|
|
39
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: ` ${BULLET} ` }), _jsx(Text, { children: text })] }));
|
|
40
|
+
}
|
|
41
|
+
/* ------------------------------------------------------------------ */
|
|
42
|
+
/* Mascot column (hand-crafted ASCII fallback path) */
|
|
43
|
+
/* ------------------------------------------------------------------ */
|
|
44
|
+
function MascotColumn() {
|
|
45
|
+
return (_jsx(Box, { flexDirection: "column", minWidth: PUG_MASCOT_MAX_WIDTH, children: PUG_MASCOT.map((row, rowIndex) => (_jsx(MascotRow, { row: row, mask: PUG_MASCOT_CYAN_MASK[rowIndex] ?? [] }, `mascot-row-${rowIndex}`))) }));
|
|
46
|
+
}
|
|
47
|
+
function MascotRow({ row, mask, }) {
|
|
48
|
+
// Split into contiguous same-color runs so we emit one <Text> per
|
|
49
|
+
// run instead of one per character (keeps the Ink tree shallow и
|
|
50
|
+
// the snapshot diff readable).
|
|
51
|
+
const runs = [];
|
|
52
|
+
let buffer = '';
|
|
53
|
+
let bufferCyan = false;
|
|
54
|
+
for (let column = 0; column < row.length; column += 1) {
|
|
55
|
+
const ch = row.charAt(column);
|
|
56
|
+
const cyan = mask[column] === true;
|
|
57
|
+
if (buffer.length === 0) {
|
|
58
|
+
buffer = ch;
|
|
59
|
+
bufferCyan = cyan;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (cyan === bufferCyan) {
|
|
63
|
+
buffer += ch;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
runs.push({ text: buffer, cyan: bufferCyan });
|
|
67
|
+
buffer = ch;
|
|
68
|
+
bufferCyan = cyan;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (buffer.length > 0)
|
|
72
|
+
runs.push({ text: buffer, cyan: bufferCyan });
|
|
73
|
+
return (_jsx(Text, { children: runs.map((run, runIndex) => run.cyan ? (_jsx(Text, { color: ACCENT, children: run.text }, runIndex)) : (_jsx(Text, { color: "gray", children: run.text }, runIndex))) }));
|
|
74
|
+
}
|
|
75
|
+
/* ------------------------------------------------------------------ */
|
|
76
|
+
/* Formatters */
|
|
77
|
+
/* ------------------------------------------------------------------ */
|
|
78
|
+
function formatModelLine(data) {
|
|
79
|
+
const tierLabel = data.plan ? ` ${BULLET} ${capitalize(data.plan)} Tier` : '';
|
|
80
|
+
return `${data.model}${tierLabel}`;
|
|
81
|
+
}
|
|
82
|
+
function formatAccountLine(data) {
|
|
83
|
+
if (!data.email) {
|
|
84
|
+
return 'Anonymous (run /login to authenticate)';
|
|
85
|
+
}
|
|
86
|
+
const tenant = data.tenant ? ` ${BULLET} ${data.tenant} Org` : '';
|
|
87
|
+
return `${data.email}${tenant}`;
|
|
88
|
+
}
|
|
89
|
+
function capitalize(value) {
|
|
90
|
+
if (value.length === 0)
|
|
91
|
+
return value;
|
|
92
|
+
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Trim the cwd from the LEFT (preserving the basename) so the operator
|
|
96
|
+
* always sees which project they're in even when the column is narrow.
|
|
97
|
+
* Mirrors the the upstream tool boot card convention: long paths get an
|
|
98
|
+
* ellipsis on the head, never on the tail.
|
|
99
|
+
*/
|
|
100
|
+
export function truncatePath(path, max) {
|
|
101
|
+
if (path.length <= max)
|
|
102
|
+
return path;
|
|
103
|
+
if (max <= 3)
|
|
104
|
+
return path.slice(-max);
|
|
105
|
+
return `…${path.slice(-(max - 1))}`;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=welcome-banner.js.map
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure data layer for the `<WelcomeBanner />` component (CEO P0 #2,
|
|
3
|
+
*). Lives in its own module so it can be unit-tested without
|
|
4
|
+
* rendering Ink, and so the banner component itself contains zero IO.
|
|
5
|
+
*
|
|
6
|
+
* The banner is the 2-column boxed greeting that replaces the
|
|
7
|
+
* wave-3 `<ReplSplash />` on the bare REPL boot path. It mirrors
|
|
8
|
+
* the upstream tool's boot layout:
|
|
9
|
+
*
|
|
10
|
+
* ╭────── Pugi v0.1.0-beta.46 ──────╮
|
|
11
|
+
* │ │ Tips for getting started
|
|
12
|
+
* │ Welcome back Yurii! │ Run /init to create PUGI.md...
|
|
13
|
+
* │ │ ───────────
|
|
14
|
+
* │ ▗ ▗ ▖ ▖ │ What's new
|
|
15
|
+
* │ ▘▘ ▝▝ │ * 0.1.0-beta.26 — RAG ...
|
|
16
|
+
* │ Sonnet 4.6 (1M context) · Founder
|
|
17
|
+
* │ yuriy.bulah@gmail.com · pugi-io Org
|
|
18
|
+
* │ <workspace>
|
|
19
|
+
* ╰──────────────────────────────────╯
|
|
20
|
+
*
|
|
21
|
+
* Data sources, in priority order:
|
|
22
|
+
*
|
|
23
|
+
* - Account email + tenant + plan: JWT principal decoded from the
|
|
24
|
+
* active credential. Falls back to anonymous label when no
|
|
25
|
+
* credential is on disk (operator boots `pugi` before login).
|
|
26
|
+
* - Greeting first-name: best-effort split of the local part of the
|
|
27
|
+
* email. Falls back к "operator" when unauthenticated.
|
|
28
|
+
* - Model: env override `PUGI_ENGINE_MODEL_CODE`, then the operator's
|
|
29
|
+
* workspace settings `defaultModel`, then "Sonnet 4.6 (1M context)"
|
|
30
|
+
* as the locked default.
|
|
31
|
+
* - Cwd: absolute path of `process.cwd()` — banner left column shows
|
|
32
|
+
* the full path so the operator confirms они are в the right repo.
|
|
33
|
+
* - What's new: top 3 release-note titles from `apps/pugi-cli/CHANGELOG.md`
|
|
34
|
+
* newer than `~/.pugi/.last-seen-version`. Falls back к the top 3
|
|
35
|
+
* overall when last-seen marker is missing. Each entry is a one-line
|
|
36
|
+
* headline ("0.1.0-beta.26 — RAG consumer middleware") trimmed
|
|
37
|
+
* к 60 chars so the right column does not wrap on a 100-col terminal.
|
|
38
|
+
*
|
|
39
|
+
* Every resolver swallows its IO error and returns the documented
|
|
40
|
+
* fallback so a partial environment (missing CHANGELOG, malformed JWT,
|
|
41
|
+
* unreadable settings) never blocks the banner.
|
|
42
|
+
*/
|
|
43
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
44
|
+
import { homedir } from 'node:os';
|
|
45
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
46
|
+
import { fileURLToPath } from 'node:url';
|
|
47
|
+
import { DEFAULT_API_URL, normalizeApiUrl, readCredentialsFile, resolveActiveCredential, } from '../core/credentials.js';
|
|
48
|
+
import { parseChangelog } from '../core/release-notes/parser.js';
|
|
49
|
+
function decodeJwtPayload(token) {
|
|
50
|
+
try {
|
|
51
|
+
const parts = token.split('.');
|
|
52
|
+
if (parts.length < 2)
|
|
53
|
+
return null;
|
|
54
|
+
const payload = parts[1];
|
|
55
|
+
if (!payload)
|
|
56
|
+
return null;
|
|
57
|
+
const padded = payload
|
|
58
|
+
.replace(/-/g, '+')
|
|
59
|
+
.replace(/_/g, '/')
|
|
60
|
+
.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), '=');
|
|
61
|
+
const json = Buffer.from(padded, 'base64').toString('utf8');
|
|
62
|
+
const obj = JSON.parse(json);
|
|
63
|
+
if (!obj || typeof obj !== 'object')
|
|
64
|
+
return null;
|
|
65
|
+
return {
|
|
66
|
+
...(typeof obj.sub === 'string' && { sub: obj.sub }),
|
|
67
|
+
...(typeof obj.email === 'string' && { email: obj.email }),
|
|
68
|
+
...(typeof obj.customerId === 'string' && { customerId: obj.customerId }),
|
|
69
|
+
...(typeof obj.plan === 'string' && { plan: obj.plan }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/* ------------------------------------------------------------------ */
|
|
77
|
+
/* Resolvers */
|
|
78
|
+
/* ------------------------------------------------------------------ */
|
|
79
|
+
/**
|
|
80
|
+
* Derive a greeting first-name from an email's local part. The split
|
|
81
|
+
* is intentionally aggressive: dot-separated, hyphen-separated, and
|
|
82
|
+
* digit-stripped so `yuriy.bulah@gmail.com` resolves к "Yuriy" instead
|
|
83
|
+
* of "yuriy.bulah". Title-cases the first segment. Falls back к
|
|
84
|
+
* "operator" when no email is available.
|
|
85
|
+
*
|
|
86
|
+
* Export so the spec can lock the heuristic against edge cases (numeric
|
|
87
|
+
* local part, plus-tag aliases, single-letter local part).
|
|
88
|
+
*/
|
|
89
|
+
export function deriveGreetingName(email) {
|
|
90
|
+
if (!email || typeof email !== 'string')
|
|
91
|
+
return 'operator';
|
|
92
|
+
const at = email.indexOf('@');
|
|
93
|
+
if (at <= 0)
|
|
94
|
+
return 'operator';
|
|
95
|
+
const local = email.slice(0, at);
|
|
96
|
+
// Strip plus-tag aliases (`name+tag@host` → `name`).
|
|
97
|
+
const noTag = local.split('+')[0] ?? local;
|
|
98
|
+
// First dot / hyphen / underscore segment wins; this is the
|
|
99
|
+
// colloquial "given name" surface for the vast majority of work
|
|
100
|
+
// emails ("first.last@..." / "first-last@...").
|
|
101
|
+
const firstSegment = noTag.split(/[._-]/)[0] ?? noTag;
|
|
102
|
+
// Strip trailing digits some signup flows append (`yurii2@`) so the
|
|
103
|
+
// greeting reads as a name, not a username.
|
|
104
|
+
const stripped = firstSegment.replace(/[0-9]+$/u, '');
|
|
105
|
+
if (stripped.length === 0)
|
|
106
|
+
return 'operator';
|
|
107
|
+
return stripped.charAt(0).toUpperCase() + stripped.slice(1);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the model display string. Priority:
|
|
111
|
+
*
|
|
112
|
+
* 1. `PUGI_ENGINE_MODEL_CODE` env override (operator-set in shell).
|
|
113
|
+
* 2. `.pugi/settings.json` → `defaultModel` field (per-workspace).
|
|
114
|
+
* 3. Locked default `"Sonnet 4.6 (1M context)"`.
|
|
115
|
+
*
|
|
116
|
+
* Returns the value verbatim — the banner is responsible for trimming
|
|
117
|
+
* if the string is too long for the column.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveModelDisplay(env, settingsOverride) {
|
|
120
|
+
const envModel = env.PUGI_ENGINE_MODEL_CODE;
|
|
121
|
+
if (typeof envModel === 'string' && envModel.length > 0)
|
|
122
|
+
return envModel;
|
|
123
|
+
const settingsModel = settingsOverride?.defaultModel;
|
|
124
|
+
if (typeof settingsModel === 'string' && settingsModel.length > 0) {
|
|
125
|
+
return settingsModel;
|
|
126
|
+
}
|
|
127
|
+
return 'Sonnet 4.6 (1M context)';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Locate the bundled CHANGELOG.md. The CLI ships к
|
|
131
|
+
* `node_modules/@pugi/cli/dist/tui/welcome-data.js` so the changelog
|
|
132
|
+
* lives at `node_modules/@pugi/cli/CHANGELOG.md` — two directory hops
|
|
133
|
+
* up from this file. In a local pnpm dev checkout the structure is the
|
|
134
|
+
* same (`src/tui/` ⇒ `../../CHANGELOG.md`) because tsx re-resolves the
|
|
135
|
+
* same relative tree.
|
|
136
|
+
*/
|
|
137
|
+
function defaultChangelogPath() {
|
|
138
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
139
|
+
return resolvePath(here, '..', '..', 'CHANGELOG.md');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read top-3 "what's new" headlines from the CLI CHANGELOG.md. Each
|
|
143
|
+
* headline is `<version> — <first non-empty section body line>` trimmed
|
|
144
|
+
* к 60 chars so the right column does not wrap on a 100-col terminal.
|
|
145
|
+
* Returns an empty array when the file is missing / unparseable.
|
|
146
|
+
*
|
|
147
|
+
* The body-line heuristic walks к the first non-empty, non-section-
|
|
148
|
+
* header line below the version header. For Keep-a-Changelog entries
|
|
149
|
+
* the second line is usually `### Added` / `### Fixed`; we skip those
|
|
150
|
+
* headers и land on the first bullet, which is the operator-visible
|
|
151
|
+
* one-liner ("- L30 `pugi theme` ...").
|
|
152
|
+
*/
|
|
153
|
+
export function readWhatsNew(changelogPath) {
|
|
154
|
+
const path = changelogPath ?? defaultChangelogPath();
|
|
155
|
+
try {
|
|
156
|
+
if (!existsSync(path))
|
|
157
|
+
return [];
|
|
158
|
+
const raw = readFileSync(path, 'utf8');
|
|
159
|
+
if (!raw || raw.length === 0)
|
|
160
|
+
return [];
|
|
161
|
+
const sections = parseChangelog(raw);
|
|
162
|
+
const headlines = [];
|
|
163
|
+
for (const section of sections) {
|
|
164
|
+
if (headlines.length >= 3)
|
|
165
|
+
break;
|
|
166
|
+
// Skip the "[Unreleased]" / "[Unreleased] - YYYY-MM-DD" section —
|
|
167
|
+
// the banner is meant к surface SHIPPED notes only. The parser
|
|
168
|
+
// captures Unreleased identically к а tagged version, so we
|
|
169
|
+
// filter here.
|
|
170
|
+
if (/^unreleased$/i.test(section.version))
|
|
171
|
+
continue;
|
|
172
|
+
const firstBullet = pickFirstBullet(section.body);
|
|
173
|
+
if (!firstBullet)
|
|
174
|
+
continue;
|
|
175
|
+
const headline = `${section.version} — ${firstBullet}`;
|
|
176
|
+
headlines.push(truncate(headline, 60));
|
|
177
|
+
}
|
|
178
|
+
return headlines;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Pull the first bullet text out of a Keep-a-Changelog section body.
|
|
186
|
+
* Skips `### <subsection>` headers, blank lines, и non-bullet prose
|
|
187
|
+
* (release-note footers like "### Notes"). Returns undefined when no
|
|
188
|
+
* bullet is present.
|
|
189
|
+
*
|
|
190
|
+
* Body lines arrive verbatim from the parser; bullets follow the
|
|
191
|
+
* `- ` / `* ` Markdown convention. We strip the leading bullet glyph
|
|
192
|
+
* + the first whitespace run so the rendered string is the bullet
|
|
193
|
+
* content only.
|
|
194
|
+
*/
|
|
195
|
+
function pickFirstBullet(body) {
|
|
196
|
+
const lines = body.split('\n');
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const trimmed = line.trim();
|
|
199
|
+
if (trimmed.length === 0)
|
|
200
|
+
continue;
|
|
201
|
+
if (trimmed.startsWith('### '))
|
|
202
|
+
continue;
|
|
203
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
204
|
+
// Strip leading bullet glyph + ws and collapse internal MD ticks.
|
|
205
|
+
return trimmed
|
|
206
|
+
.slice(2)
|
|
207
|
+
.replace(/`/g, '')
|
|
208
|
+
.replace(/\*\*/g, '')
|
|
209
|
+
.trim();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
function truncate(text, max) {
|
|
215
|
+
if (text.length <= max)
|
|
216
|
+
return text;
|
|
217
|
+
// Reserve 1 char for the ellipsis so we land on EXACTLY `max`
|
|
218
|
+
// visible chars including the dot. Single Unicode ellipsis would be
|
|
219
|
+
// narrower but some terminals render it as а full-width glyph in
|
|
220
|
+
// CJK locales — ASCII three-dot stays predictable.
|
|
221
|
+
return `${text.slice(0, max - 1).trimEnd()}…`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolve the active credential and decode the JWT principal. Returns
|
|
225
|
+
* the email / tenant / plan triple when authenticated, null fields when
|
|
226
|
+
* anonymous. Encapsulates the credential + JWT IO so the spec can drive
|
|
227
|
+
* the resolver via an env override.
|
|
228
|
+
*/
|
|
229
|
+
function resolvePrincipal(env, home) {
|
|
230
|
+
const credential = resolveActiveCredential(env, home);
|
|
231
|
+
if (!credential) {
|
|
232
|
+
const file = readCredentialsFile(home);
|
|
233
|
+
return {
|
|
234
|
+
apiUrl: normalizeApiUrl(env.PUGI_API_URL ?? file.activeApiUrl ?? DEFAULT_API_URL),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const principal = decodeJwtPayload(credential.apiKey);
|
|
238
|
+
return {
|
|
239
|
+
apiUrl: credential.apiUrl,
|
|
240
|
+
...(principal?.email && { email: principal.email }),
|
|
241
|
+
...(principal?.customerId && { tenant: principal.customerId }),
|
|
242
|
+
...(principal?.plan && { plan: principal.plan }),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Best-effort read of `.pugi/settings.json`. Returns null when the file
|
|
247
|
+
* is missing / unreadable / malformed so the model resolver can fall
|
|
248
|
+
* back to the env override + locked default.
|
|
249
|
+
*/
|
|
250
|
+
function readSettingsBlob(cwd) {
|
|
251
|
+
try {
|
|
252
|
+
const path = resolvePath(cwd, '.pugi', 'settings.json');
|
|
253
|
+
if (!existsSync(path))
|
|
254
|
+
return null;
|
|
255
|
+
const raw = readFileSync(path, 'utf8');
|
|
256
|
+
if (!raw || raw.length === 0)
|
|
257
|
+
return null;
|
|
258
|
+
const obj = JSON.parse(raw);
|
|
259
|
+
if (typeof obj?.defaultModel === 'string') {
|
|
260
|
+
return { defaultModel: obj.defaultModel };
|
|
261
|
+
}
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/* ------------------------------------------------------------------ */
|
|
269
|
+
/* Entry point */
|
|
270
|
+
/* ------------------------------------------------------------------ */
|
|
271
|
+
export function collectWelcomeData(input) {
|
|
272
|
+
const env = input.env ?? process.env;
|
|
273
|
+
const home = input.home ?? homedir();
|
|
274
|
+
const cwd = input.cwd ?? process.cwd();
|
|
275
|
+
const principal = resolvePrincipal(env, home);
|
|
276
|
+
const settings = input.settingsOverride !== undefined
|
|
277
|
+
? input.settingsOverride
|
|
278
|
+
: readSettingsBlob(cwd);
|
|
279
|
+
const greetingName = deriveGreetingName(principal.email);
|
|
280
|
+
const model = resolveModelDisplay(env, settings);
|
|
281
|
+
const whatsNew = readWhatsNew(input.changelogPath);
|
|
282
|
+
return {
|
|
283
|
+
greetingName,
|
|
284
|
+
...(principal.email && { email: principal.email }),
|
|
285
|
+
...(principal.tenant && { tenant: principal.tenant }),
|
|
286
|
+
...(principal.plan && { plan: principal.plan }),
|
|
287
|
+
model,
|
|
288
|
+
cwd,
|
|
289
|
+
cliVersion: input.cliVersion,
|
|
290
|
+
whatsNew,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=welcome-data.js.map
|