@oh-my-pi/pi-coding-agent 8.0.20 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/docs/session.md +111 -46
- package/examples/custom-tools/hello/index.ts +1 -1
- package/examples/custom-tools/todo/index.ts +3 -4
- package/examples/extensions/api-demo.ts +0 -1
- package/examples/extensions/chalk-logger.ts +2 -3
- package/examples/extensions/hello.ts +0 -1
- package/examples/extensions/pirate.ts +0 -1
- package/examples/extensions/plan-mode.ts +15 -16
- package/examples/extensions/todo.ts +3 -4
- package/examples/extensions/tools.ts +1 -2
- package/examples/extensions/with-deps/index.ts +0 -1
- package/examples/hooks/auto-commit-on-exit.ts +1 -2
- package/examples/hooks/confirm-destructive.ts +0 -1
- package/examples/hooks/custom-compaction.ts +1 -2
- package/examples/hooks/dirty-repo-guard.ts +0 -1
- package/examples/hooks/file-trigger.ts +3 -4
- package/examples/hooks/git-checkpoint.ts +0 -1
- package/examples/hooks/handoff.ts +3 -4
- package/examples/hooks/permission-gate.ts +1 -2
- package/examples/hooks/protected-paths.ts +1 -2
- package/examples/hooks/qna.ts +2 -3
- package/examples/hooks/snake.ts +4 -5
- package/examples/hooks/status-line.ts +0 -1
- package/examples/sdk/01-minimal.ts +2 -3
- package/examples/sdk/02-custom-model.ts +2 -3
- package/examples/sdk/03-custom-prompt.ts +3 -4
- package/examples/sdk/04-skills.ts +2 -3
- package/examples/sdk/06-extensions.ts +1 -2
- package/examples/sdk/06-hooks.ts +6 -7
- package/examples/sdk/07-context-files.ts +0 -1
- package/examples/sdk/08-prompt-templates.ts +0 -1
- package/examples/sdk/08-slash-commands.ts +0 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +0 -1
- package/examples/sdk/10-settings.ts +0 -1
- package/examples/sdk/11-sessions.ts +0 -1
- package/package.json +54 -23
- package/scripts/format-prompts.ts +0 -1
- package/src/capability/context-file.ts +3 -4
- package/src/capability/extension-module.ts +3 -4
- package/src/capability/extension.ts +3 -4
- package/src/capability/fs.ts +20 -21
- package/src/capability/hook.ts +3 -4
- package/src/capability/index.ts +15 -16
- package/src/capability/instruction.ts +3 -4
- package/src/capability/mcp.ts +3 -4
- package/src/capability/prompt.ts +3 -4
- package/src/capability/rule.ts +3 -4
- package/src/capability/settings.ts +2 -3
- package/src/capability/skill.ts +3 -4
- package/src/capability/slash-command.ts +3 -4
- package/src/capability/ssh.ts +3 -4
- package/src/capability/system-prompt.ts +3 -4
- package/src/capability/tool.ts +3 -4
- package/src/cli/args.ts +5 -6
- package/src/cli/config-cli.ts +6 -7
- package/src/cli/file-processor.ts +19 -17
- package/src/cli/jupyter-cli.ts +105 -0
- package/src/cli/list-models.ts +10 -11
- package/src/cli/plugin-cli.ts +20 -25
- package/src/cli/session-picker.ts +2 -3
- package/src/cli/setup-cli.ts +2 -3
- package/src/cli/stats-cli.ts +2 -3
- package/src/cli/update-cli.ts +25 -22
- package/src/commit/agentic/agent.ts +307 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +351 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +26 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +40 -0
- package/src/commit/agentic/state.ts +69 -0
- package/src/commit/agentic/tools/analyze-file.ts +131 -0
- package/src/commit/agentic/tools/git-file-diff.ts +194 -0
- package/src/commit/agentic/tools/git-hunk.ts +50 -0
- package/src/commit/agentic/tools/git-overview.ts +84 -0
- package/src/commit/agentic/tools/index.ts +56 -0
- package/src/commit/agentic/tools/propose-changelog.ts +128 -0
- package/src/commit/agentic/tools/propose-commit.ts +154 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/split-commit.ts +280 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +200 -0
- package/src/commit/analysis/conventional.ts +165 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +112 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +36 -0
- package/src/commit/changelog/generate.ts +110 -0
- package/src/commit/changelog/index.ts +233 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +93 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/git/errors.ts +11 -0
- package/src/commit/git/index.ts +212 -0
- package/src/commit/git/operations.ts +53 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/index.ts +63 -0
- package/src/commit/map-reduce/map-phase.ts +178 -0
- package/src/commit/map-reduce/reduce-phase.ts +145 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +80 -0
- package/src/commit/pipeline.ts +240 -0
- package/src/commit/prompts/analysis-system.md +155 -0
- package/src/commit/prompts/analysis-user.md +41 -0
- package/src/commit/prompts/changelog-system.md +56 -0
- package/src/commit/prompts/changelog-user.md +19 -0
- package/src/commit/prompts/file-observer-system.md +26 -0
- package/src/commit/prompts/file-observer-user.md +9 -0
- package/src/commit/prompts/reduce-system.md +60 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +4 -0
- package/src/commit/prompts/summary-system.md +52 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/types.ts +109 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/config/file-lock.ts +121 -0
- package/src/config/keybindings.ts +6 -8
- package/src/config/model-registry.ts +65 -38
- package/src/config/model-resolver.ts +18 -19
- package/src/config/prompt-templates.ts +11 -11
- package/src/config/settings-manager.ts +141 -50
- package/src/config.ts +64 -66
- package/src/cursor.ts +11 -9
- package/src/discovery/agents-md.ts +11 -12
- package/src/discovery/builtin.ts +68 -73
- package/src/discovery/claude.ts +41 -42
- package/src/discovery/cline.ts +11 -12
- package/src/discovery/codex.ts +52 -53
- package/src/discovery/cursor.ts +9 -10
- package/src/discovery/gemini.ts +17 -22
- package/src/discovery/github.ts +13 -14
- package/src/discovery/helpers.ts +35 -34
- package/src/discovery/index.ts +22 -24
- package/src/discovery/mcp-json.ts +8 -9
- package/src/discovery/ssh.ts +8 -9
- package/src/discovery/vscode.ts +4 -5
- package/src/discovery/windsurf.ts +6 -7
- package/src/exa/company.ts +1 -2
- package/src/exa/index.ts +2 -3
- package/src/exa/linkedin.ts +1 -2
- package/src/exa/mcp-client.ts +14 -16
- package/src/exa/render.ts +10 -11
- package/src/exa/researcher.ts +1 -2
- package/src/exa/search.ts +1 -2
- package/src/exa/types.ts +0 -1
- package/src/exa/websets.ts +1 -2
- package/src/exec/bash-executor.ts +3 -4
- package/src/exec/exec.ts +0 -1
- package/src/export/custom-share.ts +5 -6
- package/src/export/html/index.ts +24 -21
- package/src/export/ttsr.ts +2 -3
- package/src/extensibility/custom-commands/bundled/review/index.ts +7 -8
- package/src/extensibility/custom-commands/loader.ts +18 -15
- package/src/extensibility/custom-commands/types.ts +2 -3
- package/src/extensibility/custom-tools/loader.ts +11 -12
- package/src/extensibility/custom-tools/types.ts +7 -8
- package/src/extensibility/custom-tools/wrapper.ts +2 -3
- package/src/extensibility/extensions/loader.ts +76 -54
- package/src/extensibility/extensions/runner.ts +11 -12
- package/src/extensibility/extensions/types.ts +20 -27
- package/src/extensibility/extensions/wrapper.ts +3 -4
- package/src/extensibility/hooks/index.ts +1 -1
- package/src/extensibility/hooks/loader.ts +9 -10
- package/src/extensibility/hooks/runner.ts +7 -8
- package/src/extensibility/hooks/tool-wrapper.ts +0 -1
- package/src/extensibility/hooks/types.ts +11 -18
- package/src/extensibility/plugins/doctor.ts +3 -3
- package/src/extensibility/plugins/installer.ts +27 -27
- package/src/extensibility/plugins/loader.ts +59 -56
- package/src/extensibility/plugins/manager.ts +211 -171
- package/src/extensibility/plugins/parser.ts +1 -1
- package/src/extensibility/plugins/paths.ts +8 -8
- package/src/extensibility/skills.ts +63 -60
- package/src/extensibility/slash-commands.ts +10 -10
- package/src/index.ts +54 -54
- package/src/internal-urls/agent-protocol.ts +21 -11
- package/src/internal-urls/artifact-protocol.ts +17 -13
- package/src/internal-urls/router.ts +1 -2
- package/src/internal-urls/rule-protocol.ts +3 -4
- package/src/internal-urls/skill-protocol.ts +3 -4
- package/src/ipy/executor.ts +109 -9
- package/src/ipy/gateway-coordinator.ts +79 -90
- package/src/ipy/kernel.ts +32 -30
- package/src/ipy/modules.ts +13 -13
- package/src/lsp/client.ts +21 -10
- package/src/lsp/clients/biome-client.ts +1 -2
- package/src/lsp/clients/index.ts +3 -3
- package/src/lsp/clients/lsp-linter-client.ts +4 -5
- package/src/lsp/config.ts +15 -15
- package/src/lsp/edits.ts +4 -5
- package/src/lsp/index.ts +43 -44
- package/src/lsp/lspmux.ts +8 -8
- package/src/lsp/render.ts +99 -61
- package/src/lsp/utils.ts +3 -3
- package/src/main.ts +71 -37
- package/src/mcp/client.ts +2 -3
- package/src/mcp/config.ts +5 -6
- package/src/mcp/json-rpc.ts +0 -1
- package/src/mcp/loader.ts +6 -7
- package/src/mcp/manager.ts +17 -18
- package/src/mcp/tool-bridge.ts +4 -9
- package/src/mcp/tool-cache.ts +2 -3
- package/src/mcp/transports/http.ts +2 -4
- package/src/mcp/transports/stdio.ts +1 -2
- package/src/migrations.ts +63 -52
- package/src/modes/components/armin.ts +4 -5
- package/src/modes/components/assistant-message.ts +33 -5
- package/src/modes/components/bash-execution.ts +7 -8
- package/src/modes/components/bordered-loader.ts +3 -3
- package/src/modes/components/branch-summary-message.ts +3 -3
- package/src/modes/components/compaction-summary-message.ts +3 -3
- package/src/modes/components/countdown-timer.ts +0 -1
- package/src/modes/components/custom-message.ts +5 -5
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/components/dynamic-border.ts +2 -2
- package/src/modes/components/extensions/extension-dashboard.ts +6 -7
- package/src/modes/components/extensions/extension-list.ts +2 -3
- package/src/modes/components/extensions/inspector-panel.ts +3 -4
- package/src/modes/components/extensions/state-manager.ts +25 -26
- package/src/modes/components/extensions/types.ts +1 -2
- package/src/modes/components/footer.ts +47 -43
- package/src/modes/components/history-search.ts +2 -2
- package/src/modes/components/hook-editor.ts +3 -4
- package/src/modes/components/hook-input.ts +2 -3
- package/src/modes/components/hook-message.ts +5 -5
- package/src/modes/components/hook-selector.ts +2 -3
- package/src/modes/components/keybinding-hints.ts +2 -3
- package/src/modes/components/login-dialog.ts +2 -2
- package/src/modes/components/model-selector.ts +12 -12
- package/src/modes/components/oauth-selector.ts +2 -2
- package/src/modes/components/plugin-settings.ts +20 -20
- package/src/modes/components/python-execution.ts +7 -8
- package/src/modes/components/queue-mode-selector.ts +3 -3
- package/src/modes/components/read-tool-group.ts +2 -2
- package/src/modes/components/session-selector.ts +4 -4
- package/src/modes/components/settings-defs.ts +77 -69
- package/src/modes/components/settings-selector.ts +16 -16
- package/src/modes/components/show-images-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -4
- package/src/modes/components/status-line/separators.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -2
- package/src/modes/components/status-line-segment-editor.ts +7 -8
- package/src/modes/components/status-line.ts +12 -12
- package/src/modes/components/theme-selector.ts +8 -7
- package/src/modes/components/thinking-selector.ts +4 -4
- package/src/modes/components/todo-display.ts +2 -2
- package/src/modes/components/todo-reminder.ts +4 -4
- package/src/modes/components/tool-execution.ts +16 -19
- package/src/modes/components/tree-selector.ts +12 -12
- package/src/modes/components/ttsr-notification.ts +5 -5
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/components/visual-truncate.ts +0 -1
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/command-controller.ts +46 -47
- package/src/modes/controllers/event-controller.ts +16 -20
- package/src/modes/controllers/extension-ui-controller.ts +40 -46
- package/src/modes/controllers/input-controller.ts +17 -18
- package/src/modes/controllers/selector-controller.ts +103 -91
- package/src/modes/index.ts +3 -3
- package/src/modes/interactive-mode.ts +31 -31
- package/src/modes/print-mode.ts +12 -13
- package/src/modes/rpc/rpc-client.ts +7 -8
- package/src/modes/rpc/rpc-mode.ts +24 -28
- package/src/modes/rpc/rpc-types.ts +3 -4
- package/src/modes/theme/mermaid-cache.ts +89 -0
- package/src/modes/theme/theme.ts +130 -53
- package/src/modes/types.ts +10 -10
- package/src/modes/utils/ui-helpers.ts +17 -17
- package/src/patch/applicator.ts +18 -19
- package/src/patch/diff.ts +1 -2
- package/src/patch/fuzzy.ts +1 -2
- package/src/patch/index.ts +11 -18
- package/src/patch/normalize.ts +4 -4
- package/src/patch/normative.ts +1 -2
- package/src/patch/parser.ts +8 -9
- package/src/patch/shared.ts +43 -16
- package/src/prompts/tools/task.md +2 -0
- package/src/sdk.ts +100 -65
- package/src/session/agent-session.ts +84 -85
- package/src/session/agent-storage.ts +43 -39
- package/src/session/artifacts.ts +32 -10
- package/src/session/auth-storage.ts +50 -39
- package/src/session/compaction/branch-summarization.ts +7 -10
- package/src/session/compaction/compaction.ts +8 -19
- package/src/session/compaction/utils.ts +6 -9
- package/src/session/history-storage.ts +10 -10
- package/src/session/messages.ts +4 -5
- package/src/session/session-manager.ts +76 -65
- package/src/session/session-storage.ts +57 -69
- package/src/session/storage-migration.ts +14 -56
- package/src/session/streaming-output.ts +2 -2
- package/src/ssh/connection-manager.ts +43 -50
- package/src/ssh/ssh-executor.ts +2 -2
- package/src/ssh/sshfs-mount.ts +11 -18
- package/src/system-prompt.ts +28 -35
- package/src/task/agents.ts +45 -30
- package/src/task/commands.ts +6 -7
- package/src/task/discovery.ts +39 -76
- package/src/task/executor.ts +14 -15
- package/src/task/index.ts +40 -34
- package/src/task/output-manager.ts +93 -0
- package/src/task/parallel.ts +0 -1
- package/src/task/render.ts +24 -30
- package/src/task/subprocess-tool-registry.ts +1 -2
- package/src/task/worker-protocol.ts +3 -3
- package/src/task/worker.ts +33 -39
- package/src/task/worktree.ts +19 -19
- package/src/tools/ask.ts +41 -20
- package/src/tools/bash-interceptor.ts +1 -5
- package/src/tools/bash.ts +91 -97
- package/src/tools/calculator.ts +49 -47
- package/src/tools/complete.ts +4 -5
- package/src/tools/context.ts +2 -2
- package/src/tools/fetch.ts +84 -124
- package/src/tools/find.ts +94 -98
- package/src/tools/gemini-image.ts +14 -14
- package/src/tools/grep.ts +100 -116
- package/src/tools/index.ts +80 -55
- package/src/tools/list-limit.ts +1 -1
- package/src/tools/ls.ts +44 -70
- package/src/tools/notebook.ts +51 -67
- package/src/tools/output-meta.ts +3 -4
- package/src/tools/output-utils.ts +2 -2
- package/src/tools/path-utils.ts +5 -5
- package/src/tools/python.ts +104 -217
- package/src/tools/read.ts +92 -33
- package/src/tools/render-utils.ts +8 -23
- package/src/tools/renderers.ts +6 -7
- package/src/tools/review.ts +8 -11
- package/src/tools/ssh.ts +69 -49
- package/src/tools/todo-write.ts +37 -25
- package/src/tools/tool-errors.ts +3 -3
- package/src/tools/tool-result.ts +3 -8
- package/src/tools/write.ts +99 -75
- package/src/tui/code-cell.ts +109 -0
- package/src/tui/file-list.ts +47 -0
- package/src/tui/index.ts +11 -0
- package/src/tui/output-block.ts +72 -0
- package/src/tui/status-line.ts +39 -0
- package/src/tui/tree-list.ts +55 -0
- package/src/tui/types.ts +16 -0
- package/src/tui/utils.ts +48 -0
- package/src/utils/changelog.ts +9 -10
- package/src/utils/clipboard.ts +11 -11
- package/src/utils/file-mentions.ts +4 -10
- package/src/utils/frontmatter.ts +6 -3
- package/src/utils/fuzzy.ts +2 -2
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +1 -1
- package/src/utils/mime.ts +2 -2
- package/src/utils/shell-snapshot.ts +11 -13
- package/src/utils/shell.ts +4 -5
- package/src/utils/title-generator.ts +8 -9
- package/src/utils/tools-manager.ts +23 -23
- package/src/vendor/photon/index.js +1099 -1059
- package/src/vendor/photon/photon_rs_bg.wasm +0 -0
- package/src/web/scrapers/artifacthub.ts +1 -1
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/bluesky.ts +2 -2
- package/src/web/scrapers/cheatsh.ts +1 -1
- package/src/web/scrapers/chocolatey.ts +2 -2
- package/src/web/scrapers/choosealicense.ts +5 -5
- package/src/web/scrapers/cisa-kev.ts +1 -1
- package/src/web/scrapers/crossref.ts +2 -2
- package/src/web/scrapers/devto.ts +3 -3
- package/src/web/scrapers/discogs.ts +3 -4
- package/src/web/scrapers/discourse.ts +1 -1
- package/src/web/scrapers/dockerhub.ts +1 -1
- package/src/web/scrapers/fdroid.ts +2 -2
- package/src/web/scrapers/firefox-addons.ts +3 -3
- package/src/web/scrapers/flathub.ts +1 -1
- package/src/web/scrapers/github.ts +3 -3
- package/src/web/scrapers/gitlab.ts +4 -4
- package/src/web/scrapers/hackernews.ts +2 -2
- package/src/web/scrapers/huggingface.ts +1 -1
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/index.ts +0 -1
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/lemmy.ts +2 -2
- package/src/web/scrapers/maven.ts +2 -2
- package/src/web/scrapers/mdn.ts +2 -4
- package/src/web/scrapers/metacpan.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/npm.ts +1 -1
- package/src/web/scrapers/nuget.ts +2 -2
- package/src/web/scrapers/nvd.ts +3 -3
- package/src/web/scrapers/ollama.ts +7 -9
- package/src/web/scrapers/opencorporates.ts +2 -2
- package/src/web/scrapers/openlibrary.ts +6 -6
- package/src/web/scrapers/orcid.ts +0 -1
- package/src/web/scrapers/osv.ts +2 -2
- package/src/web/scrapers/packagist.ts +1 -1
- package/src/web/scrapers/pubmed.ts +1 -2
- package/src/web/scrapers/rawg.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +1 -2
- package/src/web/scrapers/repology.ts +2 -2
- package/src/web/scrapers/rfc.ts +1 -1
- package/src/web/scrapers/searchcode.ts +2 -2
- package/src/web/scrapers/semantic-scholar.ts +1 -1
- package/src/web/scrapers/snapcraft.ts +2 -2
- package/src/web/scrapers/sourcegraph.ts +1 -1
- package/src/web/scrapers/spdx.ts +3 -3
- package/src/web/scrapers/spotify.ts +0 -1
- package/src/web/scrapers/twitter.ts +1 -1
- package/src/web/scrapers/types.ts +1 -2
- package/src/web/scrapers/utils.ts +5 -5
- package/src/web/scrapers/wikidata.ts +3 -3
- package/src/web/scrapers/youtube.ts +9 -14
- package/src/web/search/auth.ts +5 -10
- package/src/web/search/index.ts +11 -21
- package/src/web/search/providers/anthropic.ts +3 -9
- package/src/web/search/providers/exa.ts +6 -10
- package/src/web/search/providers/perplexity.ts +5 -5
- package/src/web/search/render.ts +129 -175
- package/tsconfig.json +0 -42
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Api, AssistantMessage, Model, ToolCall } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" };
|
|
5
|
+
import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
|
|
6
|
+
import type { ChangelogGenerationResult } from "../../commit/types";
|
|
7
|
+
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
8
|
+
|
|
9
|
+
const ChangelogTool = {
|
|
10
|
+
name: "create_changelog_entries",
|
|
11
|
+
description: "Generate changelog entries grouped by Keep a Changelog categories.",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
entries: Type.Record(Type.String(), Type.Array(Type.String())),
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface ChangelogPromptInput {
|
|
18
|
+
model: Model<Api>;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
changelogPath: string;
|
|
21
|
+
isPackageChangelog: boolean;
|
|
22
|
+
existingEntries?: string;
|
|
23
|
+
stat: string;
|
|
24
|
+
diff: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function generateChangelogEntries({
|
|
28
|
+
model,
|
|
29
|
+
apiKey,
|
|
30
|
+
changelogPath,
|
|
31
|
+
isPackageChangelog,
|
|
32
|
+
existingEntries,
|
|
33
|
+
stat,
|
|
34
|
+
diff,
|
|
35
|
+
}: ChangelogPromptInput): Promise<ChangelogGenerationResult> {
|
|
36
|
+
const prompt = renderPromptTemplate(changelogUserPrompt, {
|
|
37
|
+
changelog_path: changelogPath,
|
|
38
|
+
is_package_changelog: isPackageChangelog,
|
|
39
|
+
existing_entries: existingEntries,
|
|
40
|
+
stat,
|
|
41
|
+
diff,
|
|
42
|
+
});
|
|
43
|
+
const response = await completeSimple(
|
|
44
|
+
model,
|
|
45
|
+
{
|
|
46
|
+
systemPrompt: renderPromptTemplate(changelogSystemPrompt),
|
|
47
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
48
|
+
tools: [ChangelogTool],
|
|
49
|
+
},
|
|
50
|
+
{ apiKey, maxTokens: 1200 },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const parsed = parseChangelogResponse(response);
|
|
54
|
+
return { entries: dedupeEntries(parsed.entries) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseChangelogResponse(message: AssistantMessage): ChangelogGenerationResult {
|
|
58
|
+
const toolCall = extractToolCall(message, "create_changelog_entries");
|
|
59
|
+
if (toolCall) {
|
|
60
|
+
const parsed = validateToolCall([ChangelogTool], toolCall) as ChangelogGenerationResult;
|
|
61
|
+
return { entries: parsed.entries ?? {} };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const text = extractTextContent(message);
|
|
65
|
+
const parsed = parseJsonPayload(text) as ChangelogGenerationResult;
|
|
66
|
+
return { entries: parsed.entries ?? {} };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractToolCall(message: AssistantMessage, name: string): ToolCall | undefined {
|
|
70
|
+
return message.content.find(content => content.type === "toolCall" && content.name === name) as ToolCall | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractTextContent(message: AssistantMessage): string {
|
|
74
|
+
return message.content
|
|
75
|
+
.filter(content => content.type === "text")
|
|
76
|
+
.map(content => content.text)
|
|
77
|
+
.join("")
|
|
78
|
+
.trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseJsonPayload(text: string): unknown {
|
|
82
|
+
const trimmed = text.trim();
|
|
83
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
84
|
+
return JSON.parse(trimmed) as unknown;
|
|
85
|
+
}
|
|
86
|
+
const match = trimmed.match(/\{[\s\S]*\}/);
|
|
87
|
+
if (!match) {
|
|
88
|
+
throw new Error("No JSON payload found in changelog response");
|
|
89
|
+
}
|
|
90
|
+
return JSON.parse(match[0]) as unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function dedupeEntries(entries: Record<string, string[]>): Record<string, string[]> {
|
|
94
|
+
const result: Record<string, string[]> = {};
|
|
95
|
+
for (const [category, values] of Object.entries(entries)) {
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
const cleaned: string[] = [];
|
|
98
|
+
for (const value of values) {
|
|
99
|
+
const trimmed = value.trim().replace(/\.$/, "");
|
|
100
|
+
const key = trimmed.toLowerCase();
|
|
101
|
+
if (!trimmed || seen.has(key)) continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
cleaned.push(trimmed);
|
|
104
|
+
}
|
|
105
|
+
if (cleaned.length > 0) {
|
|
106
|
+
result[category] = cleaned;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import type { ControlledGit } from "../../commit/git";
|
|
5
|
+
import { detectChangelogBoundaries } from "./detect";
|
|
6
|
+
import { generateChangelogEntries } from "./generate";
|
|
7
|
+
import { parseUnreleasedSection } from "./parse";
|
|
8
|
+
|
|
9
|
+
const CHANGELOG_SECTIONS = ["Breaking Changes", "Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"];
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
12
|
+
|
|
13
|
+
export interface ChangelogFlowInput {
|
|
14
|
+
git: ControlledGit;
|
|
15
|
+
cwd: string;
|
|
16
|
+
model: Model<Api>;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
stagedFiles: string[];
|
|
19
|
+
dryRun: boolean;
|
|
20
|
+
maxDiffChars?: number;
|
|
21
|
+
onProgress?: (message: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChangelogProposalInput {
|
|
25
|
+
git: ControlledGit;
|
|
26
|
+
cwd: string;
|
|
27
|
+
proposals: Array<{
|
|
28
|
+
path: string;
|
|
29
|
+
entries: Record<string, string[]>;
|
|
30
|
+
deletions?: Record<string, string[]>;
|
|
31
|
+
}>;
|
|
32
|
+
dryRun: boolean;
|
|
33
|
+
onProgress?: (message: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Update CHANGELOG.md entries for staged changes.
|
|
38
|
+
*/
|
|
39
|
+
export async function runChangelogFlow({
|
|
40
|
+
git,
|
|
41
|
+
cwd,
|
|
42
|
+
model,
|
|
43
|
+
apiKey,
|
|
44
|
+
stagedFiles,
|
|
45
|
+
dryRun,
|
|
46
|
+
maxDiffChars,
|
|
47
|
+
onProgress,
|
|
48
|
+
}: ChangelogFlowInput): Promise<string[]> {
|
|
49
|
+
if (stagedFiles.length === 0) return [];
|
|
50
|
+
onProgress?.("Detecting changelog boundaries...");
|
|
51
|
+
const boundaries = await detectChangelogBoundaries(cwd, stagedFiles);
|
|
52
|
+
if (boundaries.length === 0) return [];
|
|
53
|
+
|
|
54
|
+
const updated: string[] = [];
|
|
55
|
+
for (const boundary of boundaries) {
|
|
56
|
+
onProgress?.(`Generating entries for ${boundary.changelogPath}...`);
|
|
57
|
+
const diff = await git.getDiffForFiles(boundary.files, true);
|
|
58
|
+
if (!diff.trim()) continue;
|
|
59
|
+
const stat = await git.getStatForFiles(boundary.files, true);
|
|
60
|
+
const diffForPrompt = truncateDiff(diff, maxDiffChars ?? DEFAULT_MAX_DIFF_CHARS);
|
|
61
|
+
const changelogContent = await Bun.file(boundary.changelogPath).text();
|
|
62
|
+
let unreleased: { startLine: number; endLine: number; entries: Record<string, string[]> };
|
|
63
|
+
try {
|
|
64
|
+
unreleased = parseUnreleasedSection(changelogContent);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.warn("commit changelog parse skipped", { path: boundary.changelogPath, error: String(error) });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const existingEntries = formatExistingEntries(unreleased.entries);
|
|
70
|
+
const isPackageChangelog = path.resolve(boundary.changelogPath) !== path.resolve(cwd, "CHANGELOG.md");
|
|
71
|
+
const generated = await generateChangelogEntries({
|
|
72
|
+
model,
|
|
73
|
+
apiKey,
|
|
74
|
+
changelogPath: boundary.changelogPath,
|
|
75
|
+
isPackageChangelog,
|
|
76
|
+
existingEntries: existingEntries || undefined,
|
|
77
|
+
stat,
|
|
78
|
+
diff: diffForPrompt,
|
|
79
|
+
});
|
|
80
|
+
if (Object.keys(generated.entries).length === 0) continue;
|
|
81
|
+
|
|
82
|
+
const updatedContent = applyChangelogEntries(changelogContent, unreleased, generated.entries);
|
|
83
|
+
if (!dryRun) {
|
|
84
|
+
await Bun.write(boundary.changelogPath, updatedContent);
|
|
85
|
+
await git.stageFiles([path.relative(cwd, boundary.changelogPath)]);
|
|
86
|
+
}
|
|
87
|
+
updated.push(boundary.changelogPath);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return updated;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply changelog entries provided by the commit agent.
|
|
95
|
+
*/
|
|
96
|
+
export async function applyChangelogProposals({
|
|
97
|
+
git,
|
|
98
|
+
cwd,
|
|
99
|
+
proposals,
|
|
100
|
+
dryRun,
|
|
101
|
+
onProgress,
|
|
102
|
+
}: ChangelogProposalInput): Promise<string[]> {
|
|
103
|
+
const updated: string[] = [];
|
|
104
|
+
for (const proposal of proposals) {
|
|
105
|
+
if (
|
|
106
|
+
Object.keys(proposal.entries).length === 0 &&
|
|
107
|
+
(!proposal.deletions || Object.keys(proposal.deletions).length === 0)
|
|
108
|
+
)
|
|
109
|
+
continue;
|
|
110
|
+
onProgress?.(`Applying entries for ${proposal.path}...`);
|
|
111
|
+
const exists = await Bun.file(proposal.path).exists();
|
|
112
|
+
if (!exists) {
|
|
113
|
+
logger.warn("commit changelog path missing", { path: proposal.path });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const changelogContent = await Bun.file(proposal.path).text();
|
|
117
|
+
let unreleased: { startLine: number; endLine: number; entries: Record<string, string[]> };
|
|
118
|
+
try {
|
|
119
|
+
unreleased = parseUnreleasedSection(changelogContent);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logger.warn("commit changelog parse skipped", { path: proposal.path, error: String(error) });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const normalized = normalizeEntries(proposal.entries);
|
|
125
|
+
const normalizedDeletions = proposal.deletions ? normalizeEntries(proposal.deletions) : undefined;
|
|
126
|
+
if (Object.keys(normalized).length === 0 && !normalizedDeletions) continue;
|
|
127
|
+
const updatedContent = applyChangelogEntries(changelogContent, unreleased, normalized, normalizedDeletions);
|
|
128
|
+
if (!dryRun) {
|
|
129
|
+
await Bun.write(proposal.path, updatedContent);
|
|
130
|
+
await git.stageFiles([path.relative(cwd, proposal.path)]);
|
|
131
|
+
}
|
|
132
|
+
updated.push(proposal.path);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return updated;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function truncateDiff(diff: string, maxChars: number): string {
|
|
139
|
+
if (diff.length <= maxChars) return diff;
|
|
140
|
+
return `${diff.slice(0, maxChars)}\n... (truncated)`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatExistingEntries(entries: Record<string, string[]>): string {
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
for (const section of CHANGELOG_SECTIONS) {
|
|
146
|
+
const values = entries[section] ?? [];
|
|
147
|
+
if (values.length === 0) continue;
|
|
148
|
+
lines.push(`${section}:`);
|
|
149
|
+
for (const value of values) {
|
|
150
|
+
lines.push(`- ${value}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applyChangelogEntries(
|
|
157
|
+
content: string,
|
|
158
|
+
unreleased: { startLine: number; endLine: number; entries: Record<string, string[]> },
|
|
159
|
+
entries: Record<string, string[]>,
|
|
160
|
+
deletions?: Record<string, string[]>,
|
|
161
|
+
): string {
|
|
162
|
+
const lines = content.split("\n");
|
|
163
|
+
const before = lines.slice(0, unreleased.startLine + 1);
|
|
164
|
+
const after = lines.slice(unreleased.endLine);
|
|
165
|
+
|
|
166
|
+
let base = unreleased.entries;
|
|
167
|
+
if (deletions) {
|
|
168
|
+
base = applyDeletions(base, deletions);
|
|
169
|
+
}
|
|
170
|
+
const merged = mergeEntries(base, entries);
|
|
171
|
+
const sectionLines = renderUnreleasedSections(merged);
|
|
172
|
+
return [...before, ...sectionLines, ...after].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function applyDeletions(
|
|
176
|
+
existing: Record<string, string[]>,
|
|
177
|
+
deletions: Record<string, string[]>,
|
|
178
|
+
): Record<string, string[]> {
|
|
179
|
+
const result: Record<string, string[]> = {};
|
|
180
|
+
for (const [section, items] of Object.entries(existing)) {
|
|
181
|
+
const toDelete = new Set((deletions[section] ?? []).map(d => d.toLowerCase()));
|
|
182
|
+
const filtered = items.filter(item => !toDelete.has(item.toLowerCase()));
|
|
183
|
+
if (filtered.length > 0) {
|
|
184
|
+
result[section] = filtered;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function mergeEntries(
|
|
191
|
+
existing: Record<string, string[]>,
|
|
192
|
+
incoming: Record<string, string[]>,
|
|
193
|
+
): Record<string, string[]> {
|
|
194
|
+
const merged: Record<string, string[]> = { ...existing };
|
|
195
|
+
for (const [section, items] of Object.entries(incoming)) {
|
|
196
|
+
const current = merged[section] ?? [];
|
|
197
|
+
const lower = new Set(current.map(item => item.toLowerCase()));
|
|
198
|
+
for (const item of items) {
|
|
199
|
+
if (!lower.has(item.toLowerCase())) {
|
|
200
|
+
current.push(item);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
merged[section] = current;
|
|
204
|
+
}
|
|
205
|
+
return merged;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderUnreleasedSections(entries: Record<string, string[]>): string[] {
|
|
209
|
+
const lines: string[] = [""];
|
|
210
|
+
for (const section of CHANGELOG_SECTIONS) {
|
|
211
|
+
const items = entries[section] ?? [];
|
|
212
|
+
if (items.length === 0) continue;
|
|
213
|
+
lines.push(`### ${section}`);
|
|
214
|
+
for (const item of items) {
|
|
215
|
+
lines.push(`- ${item}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push("");
|
|
218
|
+
}
|
|
219
|
+
if (lines[lines.length - 1] === "") {
|
|
220
|
+
lines.pop();
|
|
221
|
+
}
|
|
222
|
+
return lines;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeEntries(entries: Record<string, string[]>): Record<string, string[]> {
|
|
226
|
+
const result: Record<string, string[]> = {};
|
|
227
|
+
for (const [section, items] of Object.entries(entries)) {
|
|
228
|
+
const trimmed = items.map(item => item.trim().replace(/\.$/, "")).filter(item => item.length > 0);
|
|
229
|
+
if (trimmed.length === 0) continue;
|
|
230
|
+
result[section] = Array.from(new Set(trimmed.map(item => item.trim())));
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { UnreleasedSection } from "../../commit/types";
|
|
2
|
+
|
|
3
|
+
const UNRELEASED_PATTERN = /^##\s+\[?Unreleased\]?/i;
|
|
4
|
+
const SECTION_PATTERN = /^###\s+(.*)$/;
|
|
5
|
+
|
|
6
|
+
export function parseUnreleasedSection(content: string): UnreleasedSection {
|
|
7
|
+
const lines = content.split("\n");
|
|
8
|
+
const startIndex = lines.findIndex(line => UNRELEASED_PATTERN.test(line.trim()));
|
|
9
|
+
if (startIndex === -1) {
|
|
10
|
+
throw new Error("No [Unreleased] section found in changelog");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let endIndex = lines.length;
|
|
14
|
+
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
15
|
+
if (lines[i]?.startsWith("## ")) {
|
|
16
|
+
endIndex = i;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sectionLines = lines.slice(startIndex + 1, endIndex);
|
|
22
|
+
const entries: Record<string, string[]> = {};
|
|
23
|
+
let currentSection: string | null = null;
|
|
24
|
+
for (const line of sectionLines) {
|
|
25
|
+
const sectionMatch = line.match(SECTION_PATTERN);
|
|
26
|
+
if (sectionMatch) {
|
|
27
|
+
currentSection = sectionMatch[1]?.trim() || null;
|
|
28
|
+
if (currentSection) {
|
|
29
|
+
entries[currentSection] = entries[currentSection] ?? [];
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!currentSection) continue;
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed.startsWith("-")) continue;
|
|
37
|
+
const entry = trimmed.replace(/^[-*]\s*/, "");
|
|
38
|
+
if (entry) {
|
|
39
|
+
entries[currentSection]?.push(entry);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { startLine: startIndex, endLine: endIndex, entries };
|
|
44
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { CommitCommandArgs } from "./types";
|
|
3
|
+
|
|
4
|
+
const FLAG_ALIASES = new Map<string, string>([
|
|
5
|
+
["-c", "--context"],
|
|
6
|
+
["-m", "--model"],
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export function parseCommitArgs(args: string[]): CommitCommandArgs | undefined {
|
|
10
|
+
if (args.length === 0 || args[0] !== "commit") {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result: CommitCommandArgs = {
|
|
15
|
+
push: false,
|
|
16
|
+
dryRun: false,
|
|
17
|
+
noChangelog: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
21
|
+
const raw = args[i] ?? "";
|
|
22
|
+
const flag = FLAG_ALIASES.get(raw) ?? raw;
|
|
23
|
+
switch (flag) {
|
|
24
|
+
case "--push":
|
|
25
|
+
result.push = true;
|
|
26
|
+
break;
|
|
27
|
+
case "--dry-run":
|
|
28
|
+
result.dryRun = true;
|
|
29
|
+
break;
|
|
30
|
+
case "--no-changelog":
|
|
31
|
+
result.noChangelog = true;
|
|
32
|
+
break;
|
|
33
|
+
case "--legacy":
|
|
34
|
+
result.legacy = true;
|
|
35
|
+
break;
|
|
36
|
+
case "--context": {
|
|
37
|
+
const value = args[i + 1];
|
|
38
|
+
if (!value || value.startsWith("-")) {
|
|
39
|
+
writeStderr(chalk.red("Error: --context requires a value"));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
result.context = value;
|
|
43
|
+
i += 1;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "--model": {
|
|
47
|
+
const value = args[i + 1];
|
|
48
|
+
if (!value || value.startsWith("-")) {
|
|
49
|
+
writeStderr(chalk.red("Error: --model requires a value"));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
result.model = value;
|
|
53
|
+
i += 1;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "--help":
|
|
57
|
+
case "-h":
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
if (flag.startsWith("-")) {
|
|
61
|
+
writeStderr(chalk.red(`Error: Unknown flag ${flag}`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function printCommitHelp(): void {
|
|
71
|
+
const lines = [
|
|
72
|
+
"Usage:",
|
|
73
|
+
" omp commit [options]",
|
|
74
|
+
"",
|
|
75
|
+
"Options:",
|
|
76
|
+
" --push Push after committing",
|
|
77
|
+
" --dry-run Preview without committing",
|
|
78
|
+
" --no-changelog Skip changelog updates",
|
|
79
|
+
" --legacy Use legacy deterministic pipeline",
|
|
80
|
+
" --context, -c Additional context for the model",
|
|
81
|
+
" --model, -m Override model selection",
|
|
82
|
+
" --help, -h Show this help message",
|
|
83
|
+
];
|
|
84
|
+
writeStdout(lines.join("\n"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writeStdout(message: string): void {
|
|
88
|
+
process.stdout.write(`${message}\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeStderr(message: string): void {
|
|
92
|
+
process.stderr.write(`${message}\n`);
|
|
93
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { DiffHunk, FileDiff, FileHunks, NumstatEntry } from "../../commit/types";
|
|
2
|
+
|
|
3
|
+
export function parseNumstat(output: string): NumstatEntry[] {
|
|
4
|
+
const entries: NumstatEntry[] = [];
|
|
5
|
+
for (const line of output.split("\n")) {
|
|
6
|
+
if (!line.trim()) continue;
|
|
7
|
+
const parts = line.split("\t");
|
|
8
|
+
if (parts.length < 3) continue;
|
|
9
|
+
const [addedRaw, deletedRaw, pathRaw] = parts;
|
|
10
|
+
const additions = Number.parseInt(addedRaw, 10);
|
|
11
|
+
const deletions = Number.parseInt(deletedRaw, 10);
|
|
12
|
+
const path = extractPathFromRename(pathRaw);
|
|
13
|
+
entries.push({
|
|
14
|
+
path,
|
|
15
|
+
additions: Number.isNaN(additions) ? 0 : additions,
|
|
16
|
+
deletions: Number.isNaN(deletions) ? 0 : deletions,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return entries;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseFileDiffs(diff: string): FileDiff[] {
|
|
23
|
+
const sections: FileDiff[] = [];
|
|
24
|
+
const parts = diff.split("\ndiff --git ");
|
|
25
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
26
|
+
const part = index === 0 ? parts[index] : `diff --git ${parts[index]}`;
|
|
27
|
+
if (!part.trim()) continue;
|
|
28
|
+
const lines = part.split("\n");
|
|
29
|
+
const header = lines[0] ?? "";
|
|
30
|
+
const match = header.match(/diff --git a\/(.+?) b\/(.+)$/);
|
|
31
|
+
if (!match) continue;
|
|
32
|
+
const filename = match[2];
|
|
33
|
+
const content = part;
|
|
34
|
+
const isBinary = lines.some(line => line.startsWith("Binary files "));
|
|
35
|
+
let additions = 0;
|
|
36
|
+
let deletions = 0;
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
39
|
+
if (line.startsWith("+")) additions += 1;
|
|
40
|
+
else if (line.startsWith("-")) deletions += 1;
|
|
41
|
+
}
|
|
42
|
+
sections.push({
|
|
43
|
+
filename,
|
|
44
|
+
content,
|
|
45
|
+
additions,
|
|
46
|
+
deletions,
|
|
47
|
+
isBinary,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return sections;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseDiffHunks(diff: string): FileHunks[] {
|
|
54
|
+
const files = parseFileDiffs(diff);
|
|
55
|
+
return files.map(file => parseFileHunks(file));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseFileHunks(fileDiff: FileDiff): FileHunks {
|
|
59
|
+
if (fileDiff.isBinary) {
|
|
60
|
+
return { filename: fileDiff.filename, isBinary: true, hunks: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = fileDiff.content.split("\n");
|
|
64
|
+
const hunks: DiffHunk[] = [];
|
|
65
|
+
let current: DiffHunk | null = null;
|
|
66
|
+
let buffer: string[] = [];
|
|
67
|
+
let index = 0;
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.startsWith("@@")) {
|
|
71
|
+
if (current) {
|
|
72
|
+
current.content = buffer.join("\n");
|
|
73
|
+
hunks.push(current);
|
|
74
|
+
}
|
|
75
|
+
const headerData = parseHunkHeader(line);
|
|
76
|
+
current = {
|
|
77
|
+
index,
|
|
78
|
+
header: line,
|
|
79
|
+
oldStart: headerData.oldStart,
|
|
80
|
+
oldLines: headerData.oldLines,
|
|
81
|
+
newStart: headerData.newStart,
|
|
82
|
+
newLines: headerData.newLines,
|
|
83
|
+
content: "",
|
|
84
|
+
};
|
|
85
|
+
buffer = [line];
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (current) {
|
|
90
|
+
buffer.push(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (current) {
|
|
95
|
+
current.content = buffer.join("\n");
|
|
96
|
+
hunks.push(current);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
filename: fileDiff.filename,
|
|
101
|
+
isBinary: fileDiff.isBinary,
|
|
102
|
+
hunks,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractPathFromRename(pathPart: string): string {
|
|
107
|
+
const braceStart = pathPart.indexOf("{");
|
|
108
|
+
if (braceStart !== -1) {
|
|
109
|
+
const arrowPos = pathPart.indexOf(" => ", braceStart);
|
|
110
|
+
if (arrowPos !== -1) {
|
|
111
|
+
const braceEnd = pathPart.indexOf("}", arrowPos);
|
|
112
|
+
if (braceEnd !== -1) {
|
|
113
|
+
const prefix = pathPart.slice(0, braceStart);
|
|
114
|
+
const newName = pathPart.slice(arrowPos + 4, braceEnd).trim();
|
|
115
|
+
return `${prefix}${newName}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (pathPart.includes(" => ")) {
|
|
121
|
+
const parts = pathPart.split(" => ");
|
|
122
|
+
return parts[1]?.trim() ?? pathPart.trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return pathPart.trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseHunkHeader(line: string): {
|
|
129
|
+
oldStart: number;
|
|
130
|
+
oldLines: number;
|
|
131
|
+
newStart: number;
|
|
132
|
+
newLines: number;
|
|
133
|
+
} {
|
|
134
|
+
const match = line.match(/@@\s-([0-9]+)(?:,([0-9]+))?\s\+([0-9]+)(?:,([0-9]+))?\s@@/);
|
|
135
|
+
if (!match) {
|
|
136
|
+
return { oldStart: 0, oldLines: 0, newStart: 0, newLines: 0 };
|
|
137
|
+
}
|
|
138
|
+
const oldStart = Number.parseInt(match[1] ?? "0", 10);
|
|
139
|
+
const oldLines = Number.parseInt(match[2] ?? "1", 10);
|
|
140
|
+
const newStart = Number.parseInt(match[3] ?? "0", 10);
|
|
141
|
+
const newLines = Number.parseInt(match[4] ?? "1", 10);
|
|
142
|
+
return {
|
|
143
|
+
oldStart: Number.isNaN(oldStart) ? 0 : oldStart,
|
|
144
|
+
oldLines: Number.isNaN(oldLines) ? 0 : oldLines,
|
|
145
|
+
newStart: Number.isNaN(newStart) ? 0 : newStart,
|
|
146
|
+
newLines: Number.isNaN(newLines) ? 0 : newLines,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class GitError extends Error {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly stderr: string;
|
|
4
|
+
|
|
5
|
+
constructor(command: string, stderr: string) {
|
|
6
|
+
super(`${command} failed: ${stderr || "unknown error"}`);
|
|
7
|
+
this.command = command;
|
|
8
|
+
this.stderr = stderr;
|
|
9
|
+
this.name = "GitError";
|
|
10
|
+
}
|
|
11
|
+
}
|