@prestyj/cli 5.28.0 → 5.29.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/README.md +2 -2
- package/assets/motion/THIRD-PARTY.md +14 -19
- package/assets/motion/bin/contact-sheet.mjs +9 -3
- package/assets/motion/bin/cues.mjs +337 -0
- package/assets/motion/bin/flash-check.mjs +314 -0
- package/assets/motion/bin/fonts.mjs +17 -13
- package/assets/motion/bin/library.mjs +61 -18
- package/assets/motion/bin/motion-blur.mjs +943 -0
- package/assets/motion/bin/motion-check.mjs +366 -9
- package/assets/motion/bin/music-fit.mjs +436 -0
- package/assets/motion/bin/pdf-extract.mjs +22 -10
- package/assets/motion/bin/reference-study.mjs +346 -0
- package/assets/motion/bin/score-synth.mjs +1052 -93
- package/assets/motion/fonts/finger-paint/OFL.txt +93 -0
- package/assets/motion/fonts/finger-paint/finger-paint-normal.woff2 +0 -0
- package/assets/motion/fonts/fonts.json +56 -0
- package/assets/motion/fonts/short-stack/OFL.txt +94 -0
- package/assets/motion/fonts/short-stack/short-stack-normal.woff2 +0 -0
- package/assets/motion/fonts/sora/OFL.txt +93 -0
- package/assets/motion/fonts/sora/sora-normal.woff2 +0 -0
- package/assets/motion/fonts/specimen.jpg +0 -0
- package/assets/motion/fonts/unbounded/OFL.txt +93 -0
- package/assets/motion/fonts/unbounded/unbounded-normal.woff2 +0 -0
- package/assets/motion/library/README.md +52 -14
- package/assets/motion/library/kit/moves.js +1981 -0
- package/assets/motion/library/library.json +232 -0
- package/assets/motion/library/pieces/camera-rig/meta.json +13 -0
- package/assets/motion/library/pieces/camera-rig/piece.html +153 -0
- package/assets/motion/library/pieces/camera-rig/preview.jpg +0 -0
- package/assets/motion/library/pieces/chain-knock/meta.json +13 -0
- package/assets/motion/library/pieces/chain-knock/piece.html +195 -0
- package/assets/motion/library/pieces/chain-knock/preview.jpg +0 -0
- package/assets/motion/library/pieces/gather-to-logo/meta.json +13 -0
- package/assets/motion/library/pieces/gather-to-logo/piece.html +159 -0
- package/assets/motion/library/pieces/gather-to-logo/preview.jpg +0 -0
- package/assets/motion/library/pieces/morph-carry/meta.json +13 -0
- package/assets/motion/library/pieces/morph-carry/piece.html +173 -0
- package/assets/motion/library/pieces/morph-carry/preview.jpg +0 -0
- package/assets/motion/library/pieces/one-shape-journey/meta.json +13 -0
- package/assets/motion/library/pieces/one-shape-journey/piece.html +195 -0
- package/assets/motion/library/pieces/one-shape-journey/preview.jpg +0 -0
- package/assets/motion/library/pieces/open-from-subject/meta.json +13 -0
- package/assets/motion/library/pieces/open-from-subject/piece.html +168 -0
- package/assets/motion/library/pieces/open-from-subject/preview.jpg +0 -0
- package/assets/motion/library/pieces/request-to-result/meta.json +13 -0
- package/assets/motion/library/pieces/request-to-result/piece.html +212 -0
- package/assets/motion/library/pieces/request-to-result/preview.jpg +0 -0
- package/assets/motion/library/pieces/scale-dive/meta.json +13 -0
- package/assets/motion/library/pieces/scale-dive/piece.html +321 -0
- package/assets/motion/library/pieces/scale-dive/preview.jpg +0 -0
- package/assets/motion/library/pieces/screen-replica-steps/meta.json +13 -0
- package/assets/motion/library/pieces/screen-replica-steps/piece.html +366 -0
- package/assets/motion/library/pieces/screen-replica-steps/preview.jpg +0 -0
- package/assets/motion/library/pieces/zoom-into-card/meta.json +13 -0
- package/assets/motion/library/pieces/zoom-into-card/piece.html +179 -0
- package/assets/motion/library/pieces/zoom-into-card/preview.jpg +0 -0
- package/assets/motion/library/sheets/diagram.jpg +0 -0
- package/assets/motion/library/sheets/frame.jpg +0 -0
- package/assets/motion/library/sheets/transition.jpg +0 -0
- package/assets/motion/library/sheets/ui.jpg +0 -0
- package/assets/motion/plugin.json +1 -1
- package/assets/motion/references/build-sheet.md +206 -0
- package/assets/motion/references/runtime/determinism-rules.md +3 -3
- package/assets/motion/references/runtime/gsap-easing-and-stagger.md +28 -28
- package/assets/motion/references/runtime/gsap.md +3 -3
- package/assets/motion/references/runtime/inputs-and-assets.md +21 -16
- package/assets/motion/references/runtime/lint-validate-inspect.md +3 -3
- package/assets/motion/references/runtime/minimal-composition.md +6 -0
- package/assets/motion/references/runtime/preview-render.md +3 -3
- package/assets/motion/skills/app-walkthrough/SKILL.md +66 -0
- package/assets/motion/skills/before-after/SKILL.md +53 -0
- package/assets/motion/skills/brand-kit/SKILL.md +17 -16
- package/assets/motion/skills/dev-tool-video/SKILL.md +57 -0
- package/assets/motion/skills/launch-video/SKILL.md +62 -0
- package/assets/motion/skills/match-reference/SKILL.md +58 -0
- package/assets/motion/skills/motion/SKILL.md +167 -98
- package/assets/motion/skills/source-ingest/SKILL.md +23 -15
- package/assets/motion/skills/website-video/SKILL.md +59 -0
- package/assets/skills/bulletproof/SKILL.md +36 -11
- package/assets/skills/bulletproof/references/agent-surface.md +19 -9
- package/assets/skills/bulletproof/references/audit-protocol.md +20 -5
- package/assets/skills/bulletproof/references/platform-playbooks.md +5 -4
- package/assets/skills/bulletproof/references/provenance.md +26 -1
- package/assets/skills/bulletproof/references/secure-defaults.md +6 -5
- package/assets/skills/bulletproof/references/supply-chain.md +21 -17
- package/assets/skills/bulletproof/references/threat-landscape.md +28 -26
- package/assets/skills/bulletproof/references/verification.md +2 -0
- package/assets/skills/clarify/SKILL.md +25 -16
- package/assets/skills/code-review/SKILL.md +71 -13
- package/assets/skills/code-review/references/agent-diffs.md +27 -0
- package/assets/skills/code-review/references/tests.md +19 -0
- package/assets/skills/compliance-guard/SKILL.md +20 -5
- package/assets/skills/compliance-guard/references/artifacts.md +1 -1
- package/assets/skills/compliance-guard/references/eu-uk.md +16 -16
- package/assets/skills/compliance-guard/references/lawsuit-vectors.md +5 -5
- package/assets/skills/compliance-guard/references/provenance.md +41 -2
- package/assets/skills/compliance-guard/references/sector-gates.md +3 -3
- package/assets/skills/compliance-guard/references/security-baseline.md +2 -2
- package/assets/skills/compliance-guard/references/trigger-map.md +5 -5
- package/assets/skills/compliance-guard/references/us.md +27 -21
- package/assets/skills/durable/SKILL.md +87 -79
- package/assets/skills/durable/references/agent-db-safety.md +69 -0
- package/assets/skills/durable/references/backups-and-runtime.md +19 -12
- package/assets/skills/durable/references/migrations-and-schema.md +13 -6
- package/assets/skills/evidence-led-ui/SKILL.md +69 -127
- package/assets/skills/evidence-led-ui/references/anti-defaults.md +107 -208
- package/assets/skills/evidence-led-ui/references/direction.md +124 -0
- package/assets/skills/evidence-led-ui/references/production-contract.md +8 -0
- package/assets/skills/evidence-led-ui/references/provenance.md +24 -1
- package/assets/skills/lean/SKILL.md +90 -71
- package/assets/skills/lean/references/memory-and-processes.md +3 -2
- package/assets/skills/lean/references/playbooks.md +37 -12
- package/assets/skills/refactoring/SKILL.md +24 -3
- package/assets/skills/refactoring/references/agent-pitfalls.md +4 -1
- package/assets/skills/refactoring/references/legacy.md +21 -0
- package/assets/skills/root-cause/SKILL.md +20 -10
- package/assets/skills/shared-language/SKILL.md +16 -14
- package/assets/skills/tdd/SKILL.md +27 -15
- package/dist/app-sidecar.js +203 -47
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +20 -28
- package/dist/cli.js.map +1 -1
- package/dist/core/acceptance-checks.d.ts +48 -0
- package/dist/core/acceptance-checks.js +144 -0
- package/dist/core/acceptance-checks.js.map +1 -0
- package/dist/core/agent-session.d.ts +119 -75
- package/dist/core/agent-session.js +561 -395
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.d.ts +6 -5
- package/dist/core/agents.js +12 -3
- package/dist/core/agents.js.map +1 -1
- package/dist/core/ask-user.d.ts +90 -8
- package/dist/core/ask-user.js +124 -13
- package/dist/core/ask-user.js.map +1 -1
- package/dist/core/auth-providers.js +1 -1
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/autopilot-verdict.d.ts +5 -1
- package/dist/core/autopilot-verdict.js +30 -13
- package/dist/core/autopilot-verdict.js.map +1 -1
- package/dist/core/bundled-agents.js +1 -3
- package/dist/core/bundled-agents.js.map +1 -1
- package/dist/core/cache-diagnostics.d.ts +68 -0
- package/dist/core/cache-diagnostics.js +196 -0
- package/dist/core/cache-diagnostics.js.map +1 -0
- package/dist/core/cache-expiry.d.ts +87 -0
- package/dist/core/cache-expiry.js +111 -0
- package/dist/core/cache-expiry.js.map +1 -0
- package/dist/core/compaction/compactor.js +78 -48
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/plan-step-policy.d.ts +46 -0
- package/dist/core/compaction/plan-step-policy.js +57 -0
- package/dist/core/compaction/plan-step-policy.js.map +1 -0
- package/dist/core/destructive-git-guard.d.ts +90 -0
- package/dist/core/destructive-git-guard.js +871 -0
- package/dist/core/destructive-git-guard.js.map +1 -0
- package/dist/core/event-bus.d.ts +3 -0
- package/dist/core/event-bus.js +5 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/fast-apply-benchmark.d.ts +1 -1
- package/dist/core/fast-apply-benchmark.js +2 -2
- package/dist/core/fast-apply-benchmark.js.map +1 -1
- package/dist/core/injection-detect.d.ts +38 -0
- package/dist/core/injection-detect.js +232 -0
- package/dist/core/injection-detect.js.map +1 -0
- package/dist/core/keep-awake.d.ts +88 -0
- package/dist/core/keep-awake.js +251 -0
- package/dist/core/keep-awake.js.map +1 -0
- package/dist/core/mcp/client.d.ts +72 -0
- package/dist/core/mcp/client.js +264 -41
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/content.js +6 -2
- package/dist/core/mcp/content.js.map +1 -1
- package/dist/core/mcp/store.d.ts +6 -1
- package/dist/core/mcp/store.js +12 -1
- package/dist/core/mcp/store.js.map +1 -1
- package/dist/core/mcp/types.d.ts +18 -0
- package/dist/core/model-unavailable.d.ts +14 -0
- package/dist/core/model-unavailable.js +23 -0
- package/dist/core/model-unavailable.js.map +1 -0
- package/dist/core/node-debugger.d.ts +148 -0
- package/dist/core/node-debugger.js +642 -0
- package/dist/core/node-debugger.js.map +1 -0
- package/dist/core/nolan-context.d.ts +7 -5
- package/dist/core/nolan-context.js +106 -16
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-prompt.js +24 -21
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/package-threats.d.ts +18 -0
- package/dist/core/package-threats.js +168 -0
- package/dist/core/package-threats.js.map +1 -0
- package/dist/core/persistent-shell.d.ts +58 -6
- package/dist/core/persistent-shell.js +331 -49
- package/dist/core/persistent-shell.js.map +1 -1
- package/dist/core/process-manager.d.ts +14 -0
- package/dist/core/process-manager.js +61 -0
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/progress/git-xp.js +8 -14
- package/dist/core/progress/git-xp.js.map +1 -1
- package/dist/core/project-discovery.js +77 -26
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/semantic-search-benchmark.d.ts +1 -1
- package/dist/core/semantic-search-benchmark.js +2 -2
- package/dist/core/semantic-search-benchmark.js.map +1 -1
- package/dist/core/session-history.d.ts +12 -0
- package/dist/core/session-history.js +27 -0
- package/dist/core/session-history.js.map +1 -1
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.js +38 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-summary-index.d.ts +37 -0
- package/dist/core/session-summary-index.js +172 -0
- package/dist/core/session-summary-index.js.map +1 -0
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +10 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/shell-threats-popular-packages.d.ts +11 -0
- package/dist/core/shell-threats-popular-packages.js +675 -0
- package/dist/core/shell-threats-popular-packages.js.map +1 -0
- package/dist/core/shell-threats.d.ts +8 -0
- package/dist/core/shell-threats.js +186 -0
- package/dist/core/shell-threats.js.map +1 -0
- package/dist/core/skills.js +16 -4
- package/dist/core/skills.js.map +1 -1
- package/dist/core/stream-rules.d.ts +30 -0
- package/dist/core/stream-rules.js +151 -0
- package/dist/core/stream-rules.js.map +1 -0
- package/dist/core/subagent-manager.d.ts +23 -6
- package/dist/core/subagent-manager.js +25 -9
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-policy.js +1 -1
- package/dist/core/subagent-policy.js.map +1 -1
- package/dist/core/subagent-receipt.d.ts +54 -0
- package/dist/core/subagent-receipt.js +276 -0
- package/dist/core/subagent-receipt.js.map +1 -0
- package/dist/core/subagent-turn-record.d.ts +2 -0
- package/dist/core/subagent-turn-record.js.map +1 -1
- package/dist/core/test-impact.d.ts +73 -0
- package/dist/core/test-impact.js +467 -0
- package/dist/core/test-impact.js.map +1 -0
- package/dist/core/thinking-level.d.ts +1 -1
- package/dist/core/thinking-level.js +1 -1
- package/dist/core/thinking-level.js.map +1 -1
- package/dist/core/verification-gate.d.ts +2 -0
- package/dist/core/verification-gate.js +4 -0
- package/dist/core/verification-gate.js.map +1 -1
- package/dist/core/verification-snapshot.js +3 -5
- package/dist/core/verification-snapshot.js.map +1 -1
- package/dist/core/workspace-guard.d.ts +18 -7
- package/dist/core/workspace-guard.js +227 -60
- package/dist/core/workspace-guard.js.map +1 -1
- package/dist/core/worktree-setup.d.ts +23 -0
- package/dist/core/worktree-setup.js +128 -9
- package/dist/core/worktree-setup.js.map +1 -1
- package/dist/core/worktree.d.ts +20 -2
- package/dist/core/worktree.js +74 -31
- package/dist/core/worktree.js.map +1 -1
- package/dist/interactive.js +2 -1
- package/dist/interactive.js.map +1 -1
- package/dist/modes/json-mode.js +11 -2
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +36 -1
- package/dist/modes/subagent-worker-mode.js +100 -42
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/motion-agent/motion-agent.d.ts +7 -3
- package/dist/motion-agent/motion-agent.js +6 -8
- package/dist/motion-agent/motion-agent.js.map +1 -1
- package/dist/motion-agent/motion-prompt.d.ts +1 -1
- package/dist/motion-agent/motion-prompt.js +16 -25
- package/dist/motion-agent/motion-prompt.js.map +1 -1
- package/dist/motion-agent/motion-review-session.js +1 -1
- package/dist/motion-agent/motion-review-session.js.map +1 -1
- package/dist/motion-agent/motion-review.d.ts +10 -3
- package/dist/motion-agent/motion-review.js +15 -7
- package/dist/motion-agent/motion-review.js.map +1 -1
- package/dist/motion-agent/motion-studio-context.js +1 -1
- package/dist/motion-agent/motion-studio-context.js.map +1 -1
- package/dist/system-prompt.d.ts +2 -1
- package/dist/system-prompt.js +15 -4
- package/dist/system-prompt.js.map +1 -1
- package/dist/test-support/keep-alive.d.ts +14 -0
- package/dist/test-support/keep-alive.js +17 -0
- package/dist/test-support/keep-alive.js.map +1 -0
- package/dist/tools/ask-user.js +3 -3
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/bash-read-evidence.d.ts +10 -0
- package/dist/tools/bash-read-evidence.js +133 -0
- package/dist/tools/bash-read-evidence.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -1
- package/dist/tools/bash.js +179 -7
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/debug.d.ts +54 -0
- package/dist/tools/debug.js +233 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/edit.js +13 -5
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/goals.d.ts +1 -1
- package/dist/tools/index.d.ts +25 -2
- package/dist/tools/index.js +48 -7
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.js +2 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read-tracker.d.ts +35 -2
- package/dist/tools/read-tracker.js +108 -11
- package/dist/tools/read-tracker.js.map +1 -1
- package/dist/tools/read.js +39 -7
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/skill.js +5 -0
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/subagent-control.js +44 -8
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +48 -8
- package/dist/tools/subagent-shared.js +75 -14
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +8 -2
- package/dist/tools/subagent.js +28 -10
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/task-output.js +3 -2
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/task-send.d.ts +1 -1
- package/dist/tools/task-send.js +15 -1
- package/dist/tools/task-send.js.map +1 -1
- package/dist/tools/tool-tiers.d.ts +2 -2
- package/dist/tools/tool-tiers.js +3 -2
- package/dist/tools/tool-tiers.js.map +1 -1
- package/dist/tools/truncate.d.ts +21 -0
- package/dist/tools/truncate.js +187 -0
- package/dist/tools/truncate.js.map +1 -1
- package/dist/tools/ui-adopt.js +2 -0
- package/dist/tools/ui-adopt.js.map +1 -1
- package/dist/tools/write.js +4 -3
- package/dist/tools/write.js.map +1 -1
- package/dist/ui/App.d.ts +0 -4
- package/dist/ui/App.js +5 -28
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/Footer.js +1 -1
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +1 -8
- package/dist/ui/hooks/useAgentLoop.js +1 -119
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/render.d.ts +2 -4
- package/dist/ui/render.js +3 -2
- package/dist/ui/render.js.map +1 -1
- package/dist/utils/git.d.ts +77 -0
- package/dist/utils/git.js +285 -21
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/github-ci.js +2 -1
- package/dist/utils/github-ci.js.map +1 -1
- package/dist/utils/github.js +11 -9
- package/dist/utils/github.js.map +1 -1
- package/dist/utils/image.d.ts +14 -0
- package/dist/utils/image.js +16 -0
- package/dist/utils/image.js.map +1 -1
- package/dist/utils/process.d.ts +20 -0
- package/dist/utils/process.js +98 -0
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/text.d.ts +12 -0
- package/dist/utils/text.js +11 -0
- package/dist/utils/text.js.map +1 -1
- package/package.json +6 -6
- package/assets/motion/skills/mixkit-split-text-617/SKILL.md +0 -115
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-480.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-494.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-5.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-508.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6525.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6539.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6647.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6663.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6677.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6691.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6706.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6722.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6736.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6750.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6764.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6780.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6794.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6810.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6823.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6872.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6936.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-76.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/manifest.json +0 -495
- package/assets/motion/skills/mixkit-split-text-617/references/RECONSTRUCTION.md +0 -201
- package/assets/motion/skills/mixkit-split-text-617/references/VERIFICATION.md +0 -107
- package/assets/motion/skills/mixkit-split-text-617/tools/inspect_motion.py +0 -130
- package/assets/motion/skills/video-qa/SKILL.md +0 -66
- package/dist/core/ideal-review-subagent.d.ts +0 -42
- package/dist/core/ideal-review-subagent.js +0 -95
- package/dist/core/ideal-review-subagent.js.map +0 -1
- package/dist/core/ideal-review.d.ts +0 -82
- package/dist/core/ideal-review.js +0 -242
- package/dist/core/ideal-review.js.map +0 -1
- package/dist/motion-agent/motion-check-tool.d.ts +0 -21
- package/dist/motion-agent/motion-check-tool.js +0 -206
- package/dist/motion-agent/motion-check-tool.js.map +0 -1
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { BackgroundGitError, runBackgroundGit } from "../utils/git.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
const MAX_DEPTH = 6;
|
|
7
|
+
const GIT_TIMEOUT_MS = 3_000;
|
|
8
|
+
const MAX_LISTED_FILES = 5;
|
|
9
|
+
// ── Shell scanning ─────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Split shell source into simple commands (arrays of unquoted words).
|
|
12
|
+
* Not a full shell parser: it errs toward surfacing commands (heredoc bodies
|
|
13
|
+
* fed to a shell, substitutions inside double quotes), because a false
|
|
14
|
+
* positive costs only a `git status` call.
|
|
15
|
+
*/
|
|
16
|
+
function scanShell(src, depth, out) {
|
|
17
|
+
let words = [];
|
|
18
|
+
let word = "";
|
|
19
|
+
let inWord = false;
|
|
20
|
+
let skipNextWord = false;
|
|
21
|
+
let heredocPending = false;
|
|
22
|
+
const heredocs = [];
|
|
23
|
+
let heredocStripTabs = false;
|
|
24
|
+
const nested = (inner) => {
|
|
25
|
+
if (depth < MAX_DEPTH)
|
|
26
|
+
scanShell(inner, depth + 1, out);
|
|
27
|
+
};
|
|
28
|
+
const endWord = () => {
|
|
29
|
+
if (inWord) {
|
|
30
|
+
if (skipNextWord) {
|
|
31
|
+
skipNextWord = false;
|
|
32
|
+
if (heredocPending) {
|
|
33
|
+
heredocs.push({ delimiter: word, stripTabs: heredocStripTabs });
|
|
34
|
+
heredocPending = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
words.push(word);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
word = "";
|
|
42
|
+
inWord = false;
|
|
43
|
+
};
|
|
44
|
+
const endCommand = () => {
|
|
45
|
+
endWord();
|
|
46
|
+
if (words.length > 0)
|
|
47
|
+
out.push(words);
|
|
48
|
+
words = [];
|
|
49
|
+
skipNextWord = false;
|
|
50
|
+
heredocPending = false;
|
|
51
|
+
};
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < src.length) {
|
|
54
|
+
const ch = src.charAt(i);
|
|
55
|
+
const next = src.charAt(i + 1);
|
|
56
|
+
if (ch === "\\") {
|
|
57
|
+
if (next !== "\n") {
|
|
58
|
+
word += next;
|
|
59
|
+
inWord = true;
|
|
60
|
+
}
|
|
61
|
+
i += 2;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === "'") {
|
|
65
|
+
const close = src.indexOf("'", i + 1);
|
|
66
|
+
const end = close === -1 ? src.length : close;
|
|
67
|
+
word += src.slice(i + 1, end);
|
|
68
|
+
inWord = true;
|
|
69
|
+
i = end + 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch === '"') {
|
|
73
|
+
i = scanDoubleQuoted(src, i + 1, (text) => (word += text), nested);
|
|
74
|
+
inWord = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch === "`") {
|
|
78
|
+
const close = findBacktickClose(src, i + 1);
|
|
79
|
+
nested(src.slice(i + 1, close));
|
|
80
|
+
inWord = true;
|
|
81
|
+
i = close + 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === "$" && next === "(") {
|
|
85
|
+
const close = findParenClose(src, i + 2);
|
|
86
|
+
nested(src.slice(i + 2, close));
|
|
87
|
+
inWord = true;
|
|
88
|
+
i = close + 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === "$" && next === "'") {
|
|
92
|
+
i += 1; // ANSI-C quoting: treat the body as a plain single-quoted string
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if ((ch === "<" || ch === ">") && next === "(") {
|
|
96
|
+
endWord();
|
|
97
|
+
const close = findParenClose(src, i + 2);
|
|
98
|
+
nested(src.slice(i + 2, close));
|
|
99
|
+
i = close + 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (ch === " " || ch === "\t") {
|
|
103
|
+
endWord();
|
|
104
|
+
i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (ch === "\n") {
|
|
108
|
+
const commandWords = [...words, ...(inWord && !skipNextWord ? [word] : [])];
|
|
109
|
+
endCommand();
|
|
110
|
+
i += 1;
|
|
111
|
+
// Heredoc bodies start on the next line. Scan them only when they feed
|
|
112
|
+
// a shell (`bash <<EOF`); for `cat <<EOF > notes.md` they are data.
|
|
113
|
+
while (heredocs.length > 0) {
|
|
114
|
+
const doc = heredocs.shift();
|
|
115
|
+
if (!doc)
|
|
116
|
+
break;
|
|
117
|
+
const { body, resume } = readHeredoc(src, i, doc.delimiter, doc.stripTabs);
|
|
118
|
+
if (commandWords.some((w) => SHELLS.has(commandName(w))))
|
|
119
|
+
nested(body);
|
|
120
|
+
i = resume;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === "#" && !inWord) {
|
|
125
|
+
const nl = src.indexOf("\n", i);
|
|
126
|
+
i = nl === -1 ? src.length : nl;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (ch === ";" || ch === "(" || ch === ")") {
|
|
130
|
+
endCommand();
|
|
131
|
+
i += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (ch === "|") {
|
|
135
|
+
endCommand();
|
|
136
|
+
i += next === "|" || next === "&" ? 2 : 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === "&") {
|
|
140
|
+
if (next === ">") {
|
|
141
|
+
endWord();
|
|
142
|
+
i += src.charAt(i + 2) === ">" ? 3 : 2;
|
|
143
|
+
skipNextWord = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
endCommand();
|
|
147
|
+
i += next === "&" ? 2 : 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ch === "<" || ch === ">") {
|
|
151
|
+
// A bare fd number glued to the operator (`2>`) is not a word.
|
|
152
|
+
if (inWord && /^\d+$/.test(word)) {
|
|
153
|
+
word = "";
|
|
154
|
+
inWord = false;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
endWord();
|
|
158
|
+
}
|
|
159
|
+
const op = /^(?:<<<|<<-|<<|<>|>>|>\||>&|<&|>|<)/.exec(src.slice(i))?.[0] ?? ch;
|
|
160
|
+
if (op === "<<" || op === "<<-") {
|
|
161
|
+
heredocPending = true;
|
|
162
|
+
heredocStripTabs = op === "<<-";
|
|
163
|
+
}
|
|
164
|
+
skipNextWord = true;
|
|
165
|
+
i += op.length;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
word += ch;
|
|
169
|
+
inWord = true;
|
|
170
|
+
i += 1;
|
|
171
|
+
}
|
|
172
|
+
endCommand();
|
|
173
|
+
}
|
|
174
|
+
/** Consume a double-quoted string starting after the opening quote; returns
|
|
175
|
+
* the index after the closing quote. Substitutions inside are scanned. */
|
|
176
|
+
function scanDoubleQuoted(src, start, append, nested) {
|
|
177
|
+
let i = start;
|
|
178
|
+
while (i < src.length) {
|
|
179
|
+
const ch = src.charAt(i);
|
|
180
|
+
const next = src.charAt(i + 1);
|
|
181
|
+
if (ch === '"')
|
|
182
|
+
return i + 1;
|
|
183
|
+
if (ch === "\\" && '$`"\\\n'.includes(next) && next !== "") {
|
|
184
|
+
if (next !== "\n")
|
|
185
|
+
append(next);
|
|
186
|
+
i += 2;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ch === "$" && next === "(") {
|
|
190
|
+
const close = findParenClose(src, i + 2);
|
|
191
|
+
nested(src.slice(i + 2, close));
|
|
192
|
+
append(src.slice(i, close + 1));
|
|
193
|
+
i = close + 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (ch === "`") {
|
|
197
|
+
const close = findBacktickClose(src, i + 1);
|
|
198
|
+
nested(src.slice(i + 1, close));
|
|
199
|
+
i = close + 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
append(ch);
|
|
203
|
+
i += 1;
|
|
204
|
+
}
|
|
205
|
+
return src.length;
|
|
206
|
+
}
|
|
207
|
+
/** Index of the `)` closing a `$(`/`(` whose body starts at `start`. */
|
|
208
|
+
function findParenClose(src, start) {
|
|
209
|
+
let depth = 1;
|
|
210
|
+
let i = start;
|
|
211
|
+
while (i < src.length) {
|
|
212
|
+
const ch = src.charAt(i);
|
|
213
|
+
if (ch === "\\") {
|
|
214
|
+
i += 2;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (ch === "'") {
|
|
218
|
+
const close = src.indexOf("'", i + 1);
|
|
219
|
+
i = close === -1 ? src.length : close + 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (ch === '"') {
|
|
223
|
+
i = scanDoubleQuoted(src, i + 1, () => { }, () => { });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (ch === "(")
|
|
227
|
+
depth += 1;
|
|
228
|
+
if (ch === ")") {
|
|
229
|
+
depth -= 1;
|
|
230
|
+
if (depth === 0)
|
|
231
|
+
return i;
|
|
232
|
+
}
|
|
233
|
+
i += 1;
|
|
234
|
+
}
|
|
235
|
+
return src.length;
|
|
236
|
+
}
|
|
237
|
+
function findBacktickClose(src, start) {
|
|
238
|
+
let i = start;
|
|
239
|
+
while (i < src.length) {
|
|
240
|
+
const ch = src.charAt(i);
|
|
241
|
+
if (ch === "\\") {
|
|
242
|
+
i += 2;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (ch === "`")
|
|
246
|
+
return i;
|
|
247
|
+
i += 1;
|
|
248
|
+
}
|
|
249
|
+
return src.length;
|
|
250
|
+
}
|
|
251
|
+
function readHeredoc(src, start, delimiter, stripTabs) {
|
|
252
|
+
const lines = [];
|
|
253
|
+
let i = start;
|
|
254
|
+
while (i < src.length) {
|
|
255
|
+
const nl = src.indexOf("\n", i);
|
|
256
|
+
const end = nl === -1 ? src.length : nl;
|
|
257
|
+
const line = src.slice(i, end);
|
|
258
|
+
i = end + 1;
|
|
259
|
+
if ((stripTabs ? line.replace(/^\t+/, "") : line) === delimiter)
|
|
260
|
+
break;
|
|
261
|
+
lines.push(line);
|
|
262
|
+
}
|
|
263
|
+
return { body: lines.join("\n"), resume: Math.min(i, src.length) };
|
|
264
|
+
}
|
|
265
|
+
// ── Command unwrapping ─────────────────────────────────────
|
|
266
|
+
const SHELLS = new Set(["bash", "sh", "zsh", "ksh", "dash", "ash", "fish"]);
|
|
267
|
+
/** Commands that run their operands as a command (possibly after options). */
|
|
268
|
+
const EXEC_WRAPPERS = new Set([
|
|
269
|
+
"sudo",
|
|
270
|
+
"doas",
|
|
271
|
+
"env",
|
|
272
|
+
"nohup",
|
|
273
|
+
"timeout",
|
|
274
|
+
"gtimeout",
|
|
275
|
+
"nice",
|
|
276
|
+
"ionice",
|
|
277
|
+
"exec",
|
|
278
|
+
"command",
|
|
279
|
+
"builtin",
|
|
280
|
+
"time",
|
|
281
|
+
"stdbuf",
|
|
282
|
+
"setsid",
|
|
283
|
+
"xargs",
|
|
284
|
+
"chronic",
|
|
285
|
+
"caffeinate",
|
|
286
|
+
"unbuffer",
|
|
287
|
+
"flock",
|
|
288
|
+
"watch",
|
|
289
|
+
]);
|
|
290
|
+
/** Words that do not run their operands; stop looking for a wrapped child. */
|
|
291
|
+
const NON_EXEC_COMMANDS = new Set([
|
|
292
|
+
"echo",
|
|
293
|
+
"printf",
|
|
294
|
+
"cat",
|
|
295
|
+
"grep",
|
|
296
|
+
"egrep",
|
|
297
|
+
"fgrep",
|
|
298
|
+
"rg",
|
|
299
|
+
"sed",
|
|
300
|
+
"awk",
|
|
301
|
+
"head",
|
|
302
|
+
"tail",
|
|
303
|
+
"less",
|
|
304
|
+
"more",
|
|
305
|
+
"man",
|
|
306
|
+
"which",
|
|
307
|
+
"type",
|
|
308
|
+
"tee",
|
|
309
|
+
"wc",
|
|
310
|
+
]);
|
|
311
|
+
const SHELL_KEYWORDS = new Set([
|
|
312
|
+
"if",
|
|
313
|
+
"then",
|
|
314
|
+
"else",
|
|
315
|
+
"elif",
|
|
316
|
+
"fi",
|
|
317
|
+
"do",
|
|
318
|
+
"done",
|
|
319
|
+
"while",
|
|
320
|
+
"until",
|
|
321
|
+
"case",
|
|
322
|
+
"esac",
|
|
323
|
+
"!",
|
|
324
|
+
"{",
|
|
325
|
+
"}",
|
|
326
|
+
"function",
|
|
327
|
+
]);
|
|
328
|
+
/** Basename of a command word, `.exe` stripped (`/usr/bin/git`, `git.exe`). */
|
|
329
|
+
export function commandName(word) {
|
|
330
|
+
return (word.split(/[\\/]/).pop() ?? word).replace(/\.exe$/i, "");
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Expand the home and working-directory forms a model writes in paths:
|
|
334
|
+
* `~`, `$HOME`/`${HOME}` (any case, which also covers PowerShell `$home`),
|
|
335
|
+
* `$env:USERPROFILE`/`$env:HOME`, cmd `%USERPROFILE%`, and `$PWD`/`${PWD}`. Other variables stay
|
|
336
|
+
* literal, so they resolve to a harmless relative path.
|
|
337
|
+
*/
|
|
338
|
+
export function expandShellPath(p, cwd) {
|
|
339
|
+
return p
|
|
340
|
+
.replace(/^(?:~|\$\{?home\}?|\$env:(?:userprofile|home)|%userprofile%)(?=[\\/]|$)/i, os.homedir())
|
|
341
|
+
.replace(/^\$\{?PWD\}?(?=[\\/]|$)/, cwd);
|
|
342
|
+
}
|
|
343
|
+
function isPowerShell(name) {
|
|
344
|
+
const lower = name.toLowerCase();
|
|
345
|
+
return lower === "powershell" || lower === "pwsh";
|
|
346
|
+
}
|
|
347
|
+
function walkScript(script, ctx) {
|
|
348
|
+
const commands = [];
|
|
349
|
+
scanShell(script, ctx.depth, commands);
|
|
350
|
+
// `cd` persists for later commands in the same script (approximation: a
|
|
351
|
+
// subshell's cd leaks too, which only ever widens what gets checked).
|
|
352
|
+
const local = { ...ctx };
|
|
353
|
+
for (const words of commands)
|
|
354
|
+
walkWords(words, local);
|
|
355
|
+
}
|
|
356
|
+
function walkWords(input, ctx) {
|
|
357
|
+
let words = input;
|
|
358
|
+
while (words.length > 0 &&
|
|
359
|
+
(SHELL_KEYWORDS.has(words[0] ?? "") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0] ?? ""))) {
|
|
360
|
+
words = words.slice(1);
|
|
361
|
+
}
|
|
362
|
+
const head = words[0];
|
|
363
|
+
if (head === undefined)
|
|
364
|
+
return;
|
|
365
|
+
const name = commandName(head);
|
|
366
|
+
const args = words.slice(1);
|
|
367
|
+
if (name === "cd" || name === "pushd") {
|
|
368
|
+
const target = args.find((a) => !a.startsWith("-"));
|
|
369
|
+
// `cd -` goes somewhere we cannot know; keep the current guess.
|
|
370
|
+
if (target === undefined && !args.includes("-"))
|
|
371
|
+
ctx.cwd = os.homedir();
|
|
372
|
+
else if (target !== undefined)
|
|
373
|
+
ctx.cwd = path.resolve(ctx.cwd, expandShellPath(target, ctx.cwd));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (ctx.wanted(name)) {
|
|
377
|
+
ctx.out.push({ name, args, cwd: ctx.cwd, viaXargs: ctx.viaXargs });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (ctx.depth >= MAX_DEPTH)
|
|
381
|
+
return;
|
|
382
|
+
const deeper = { ...ctx, depth: ctx.depth + 1 };
|
|
383
|
+
if (SHELLS.has(name)) {
|
|
384
|
+
// `bash -c 'script'`, also clustered (`-lc`, `-ec`).
|
|
385
|
+
const flagIndex = args.findIndex((a) => /^-[A-Za-z]*c[A-Za-z]*$/.test(a));
|
|
386
|
+
const script = flagIndex === -1 ? undefined : args[flagIndex + 1];
|
|
387
|
+
if (script !== undefined)
|
|
388
|
+
walkScript(script, deeper);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (isPowerShell(name)) {
|
|
392
|
+
// `pwsh -c '…'` / `powershell -Command "…"` (any unambiguous prefix).
|
|
393
|
+
const flagIndex = args.findIndex((a) => /^-c(?:o(?:m(?:m(?:a(?:n(?:d)?)?)?)?)?)?$/i.test(a));
|
|
394
|
+
const script = flagIndex === -1 ? undefined : args.slice(flagIndex + 1).join(" ");
|
|
395
|
+
if (script)
|
|
396
|
+
walkScript(script, deeper);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (name === "eval") {
|
|
400
|
+
walkScript(args.join(" "), deeper);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (EXEC_WRAPPERS.has(name)) {
|
|
404
|
+
// Wrapper options vary (`timeout 5`, `sudo -u bob`, `env X=1`, `nice -n 5`);
|
|
405
|
+
// rather than model each, take the first word that is itself something we
|
|
406
|
+
// analyse, stopping at a command that merely displays its operands.
|
|
407
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
408
|
+
const candidate = commandName(args[index] ?? "");
|
|
409
|
+
if (ctx.wanted(candidate) ||
|
|
410
|
+
candidate === "cd" ||
|
|
411
|
+
candidate === "eval" ||
|
|
412
|
+
SHELLS.has(candidate) ||
|
|
413
|
+
isPowerShell(candidate) ||
|
|
414
|
+
EXEC_WRAPPERS.has(candidate)) {
|
|
415
|
+
walkWords(args.slice(index), { ...deeper, viaXargs: ctx.viaXargs || name === "xargs" });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (NON_EXEC_COMMANDS.has(candidate))
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Every simple command in `command` whose name satisfies `wanted`, in source
|
|
425
|
+
* order, after splitting on `&&`/`;`/`||`/pipes, entering subshells,
|
|
426
|
+
* substitutions and `if`/`for`/`while` bodies, unwrapping `bash -c`/`eval`/
|
|
427
|
+
* `pwsh -c` scripts and exec wrappers (`sudo`, `env`, `timeout`, `xargs`, …),
|
|
428
|
+
* and following `cd`. Pure: nothing is executed.
|
|
429
|
+
*/
|
|
430
|
+
export function walkShell(command, cwd, wanted) {
|
|
431
|
+
const out = [];
|
|
432
|
+
walkScript(command, { cwd, depth: 0, viaXargs: false, wanted, out });
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
// ── git classification ─────────────────────────────────────
|
|
436
|
+
/** True for a short-option cluster (`-fdx`) containing `letter`. */
|
|
437
|
+
function hasShort(tokens, letter) {
|
|
438
|
+
return tokens.some((t) => /^-[A-Za-z]+$/.test(t) && t.slice(1).includes(letter));
|
|
439
|
+
}
|
|
440
|
+
/** Exact long option, or an unambiguous abbreviation of at least `minLength`. */
|
|
441
|
+
function hasLong(tokens, option, minLength = option.length) {
|
|
442
|
+
return tokens.some((t) => {
|
|
443
|
+
const name = t.split("=", 1)[0] ?? t;
|
|
444
|
+
return name.startsWith("--") && name.length >= minLength && option.startsWith(name);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function splitAtDoubleDash(tokens) {
|
|
448
|
+
const index = tokens.indexOf("--");
|
|
449
|
+
return index === -1
|
|
450
|
+
? { before: [...tokens], after: null }
|
|
451
|
+
: { before: tokens.slice(0, index), after: tokens.slice(index + 1) };
|
|
452
|
+
}
|
|
453
|
+
/** Non-option operands, skipping the values of options in `withValue`. */
|
|
454
|
+
function operands(tokens, withValue) {
|
|
455
|
+
const result = [];
|
|
456
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
457
|
+
const token = tokens[i] ?? "";
|
|
458
|
+
if (token.startsWith("-")) {
|
|
459
|
+
if (withValue.has(token))
|
|
460
|
+
i += 1;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
result.push(token);
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
function isPathspecShaped(operand) {
|
|
468
|
+
return (operand === "." ||
|
|
469
|
+
operand === ".." ||
|
|
470
|
+
/^\.\.?[\\/]/.test(operand) ||
|
|
471
|
+
/^(?:[\\/]|[A-Za-z]:[\\/])/.test(operand) ||
|
|
472
|
+
operand.endsWith("/") ||
|
|
473
|
+
operand.startsWith(":") ||
|
|
474
|
+
/[*?[]/.test(operand));
|
|
475
|
+
}
|
|
476
|
+
const CHECKOUT_VALUE_OPTS = new Set(["--conflict", "-U", "--unified"]);
|
|
477
|
+
const RESTORE_VALUE_OPTS = new Set([
|
|
478
|
+
"-s",
|
|
479
|
+
"--source",
|
|
480
|
+
"--conflict",
|
|
481
|
+
"-U",
|
|
482
|
+
"--unified",
|
|
483
|
+
"--inter-hunk-context",
|
|
484
|
+
]);
|
|
485
|
+
const CLEAN_VALUE_OPTS = new Set(["-e", "--exclude"]);
|
|
486
|
+
const BRANCH_VALUE_OPTS = new Set([
|
|
487
|
+
"-t",
|
|
488
|
+
"--track",
|
|
489
|
+
"-u",
|
|
490
|
+
"--set-upstream-to",
|
|
491
|
+
]);
|
|
492
|
+
const PUSH_VALUE_OPTS = new Set([
|
|
493
|
+
"-o",
|
|
494
|
+
"--push-option",
|
|
495
|
+
"--repo",
|
|
496
|
+
"--receive-pack",
|
|
497
|
+
"--exec",
|
|
498
|
+
]);
|
|
499
|
+
function analyzeGit(args, cwd) {
|
|
500
|
+
let dir = cwd;
|
|
501
|
+
const gitArgs = [];
|
|
502
|
+
let i = 0;
|
|
503
|
+
while (i < args.length) {
|
|
504
|
+
const arg = args[i] ?? "";
|
|
505
|
+
if (arg === "-C") {
|
|
506
|
+
dir = path.resolve(dir, expandShellPath(args[i + 1] ?? ".", dir));
|
|
507
|
+
i += 2;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (arg === "--git-dir" || arg === "--work-tree") {
|
|
511
|
+
gitArgs.push(`${arg}=${path.resolve(dir, expandShellPath(args[i + 1] ?? ".", dir))}`);
|
|
512
|
+
i += 2;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const assigned = /^(--git-dir|--work-tree)=(.*)$/.exec(arg);
|
|
516
|
+
if (assigned) {
|
|
517
|
+
gitArgs.push(`${assigned[1]}=${path.resolve(dir, expandShellPath(assigned[2] ?? ".", dir))}`);
|
|
518
|
+
i += 1;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (arg === "-c" ||
|
|
522
|
+
arg === "--namespace" ||
|
|
523
|
+
arg === "--config-env" ||
|
|
524
|
+
arg === "--super-prefix") {
|
|
525
|
+
i += 2;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (arg.startsWith("-")) {
|
|
529
|
+
i += 1;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
const subcommand = args[i];
|
|
535
|
+
if (subcommand === undefined)
|
|
536
|
+
return null;
|
|
537
|
+
const rest = args.slice(i + 1);
|
|
538
|
+
const base = {
|
|
539
|
+
display: truncate(["git", subcommand, ...rest].join(" ")),
|
|
540
|
+
dir,
|
|
541
|
+
gitArgs,
|
|
542
|
+
pathspecs: [],
|
|
543
|
+
refs: [],
|
|
544
|
+
};
|
|
545
|
+
switch (subcommand) {
|
|
546
|
+
case "reset":
|
|
547
|
+
// Git rejects paths with --hard, so it is always the whole tree.
|
|
548
|
+
return hasLong(rest, "--hard", 4) ? { ...base, kind: "reset-hard" } : null;
|
|
549
|
+
case "checkout": {
|
|
550
|
+
const { before, after } = splitAtDoubleDash(rest);
|
|
551
|
+
if (before.some((t) => t === "-b" || t === "-B" || t === "--orphan"))
|
|
552
|
+
return null;
|
|
553
|
+
if (hasLong(before, "--force") || hasShort(before, "f")) {
|
|
554
|
+
return { ...base, kind: "checkout-paths", pathspecs: after ?? [] };
|
|
555
|
+
}
|
|
556
|
+
if (hasLong(before, "--pathspec-from-file", 12))
|
|
557
|
+
return { ...base, kind: "checkout-paths" };
|
|
558
|
+
if (after !== null) {
|
|
559
|
+
return after.length > 0 ? { ...base, kind: "checkout-paths", pathspecs: after } : null;
|
|
560
|
+
}
|
|
561
|
+
const positional = operands(before, CHECKOUT_VALUE_OPTS);
|
|
562
|
+
if (positional.length >= 2) {
|
|
563
|
+
return { ...base, kind: "checkout-paths", pathspecs: positional.slice(1) };
|
|
564
|
+
}
|
|
565
|
+
const operand = positional[0];
|
|
566
|
+
if (operand === undefined || hasShort(before, "d") || hasLong(before, "--detach", 5)) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
if (isPathspecShaped(operand)) {
|
|
570
|
+
return { ...base, kind: "checkout-paths", pathspecs: [operand] };
|
|
571
|
+
}
|
|
572
|
+
if (existsSync(path.resolve(dir, operand))) {
|
|
573
|
+
return { ...base, kind: "checkout-paths", pathspecs: [operand], maybeRef: operand };
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
case "restore": {
|
|
578
|
+
const { before, after } = splitAtDoubleDash(rest);
|
|
579
|
+
if (before.some((t) => t === "-h" || t === "--help"))
|
|
580
|
+
return null;
|
|
581
|
+
const staged = before.includes("--staged") || hasShort(before, "S");
|
|
582
|
+
const worktree = before.includes("--worktree") || hasShort(before, "W");
|
|
583
|
+
if (staged && !worktree)
|
|
584
|
+
return null;
|
|
585
|
+
const pathspecs = [...operands(before, RESTORE_VALUE_OPTS), ...(after ?? [])];
|
|
586
|
+
const fromFile = hasLong(before, "--pathspec-from-file", 12);
|
|
587
|
+
if (pathspecs.length === 0 && !fromFile)
|
|
588
|
+
return null;
|
|
589
|
+
return { ...base, kind: "restore-worktree", pathspecs: fromFile ? [] : pathspecs };
|
|
590
|
+
}
|
|
591
|
+
case "clean": {
|
|
592
|
+
const { before, after } = splitAtDoubleDash(rest);
|
|
593
|
+
if (hasShort(before, "n") || hasLong(before, "--dry-run", 5))
|
|
594
|
+
return null;
|
|
595
|
+
if (!hasShort(before, "f") && !hasLong(before, "--force"))
|
|
596
|
+
return null;
|
|
597
|
+
const ignored = hasShort(before, "x") ? "also" : hasShort(before, "X") ? "only" : undefined;
|
|
598
|
+
return {
|
|
599
|
+
...base,
|
|
600
|
+
kind: "clean",
|
|
601
|
+
pathspecs: [...operands(before, CLEAN_VALUE_OPTS), ...(after ?? [])],
|
|
602
|
+
ignored,
|
|
603
|
+
directories: hasShort(before, "d"),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
case "stash": {
|
|
607
|
+
const [action, target] = operands(rest, new Set());
|
|
608
|
+
if (action === "drop")
|
|
609
|
+
return { ...base, kind: "stash-drop", refs: [target ?? "stash@{0}"] };
|
|
610
|
+
if (action === "clear")
|
|
611
|
+
return { ...base, kind: "stash-clear" };
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
case "branch": {
|
|
615
|
+
if (hasShort(rest, "r") || hasLong(rest, "--remotes", 5))
|
|
616
|
+
return null;
|
|
617
|
+
const forceDelete = hasShort(rest, "D") ||
|
|
618
|
+
((hasShort(rest, "d") || hasLong(rest, "--delete", 5)) &&
|
|
619
|
+
(hasShort(rest, "f") || hasLong(rest, "--force")));
|
|
620
|
+
if (!forceDelete)
|
|
621
|
+
return null;
|
|
622
|
+
const refs = operands(rest, BRANCH_VALUE_OPTS);
|
|
623
|
+
return refs.length > 0 ? { ...base, kind: "branch-force-delete", refs } : null;
|
|
624
|
+
}
|
|
625
|
+
case "push": {
|
|
626
|
+
// --force-with-lease / --force-if-includes are the safe forms: exact
|
|
627
|
+
// `--force` and `-f` only. A `+refspec` is a per-ref force.
|
|
628
|
+
const forced = rest.includes("--force") ||
|
|
629
|
+
hasShort(rest, "f") ||
|
|
630
|
+
operands(rest, PUSH_VALUE_OPTS).some((ref) => ref.startsWith("+"));
|
|
631
|
+
return forced ? { ...base, kind: "force-push" } : null;
|
|
632
|
+
}
|
|
633
|
+
default:
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function truncate(text, max = 120) {
|
|
638
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Parse `command` and return every git invocation in it that can destroy
|
|
642
|
+
* work, with the directory each would run in. Pure apart from `existsSync`
|
|
643
|
+
* for ambiguous `git checkout <name>`.
|
|
644
|
+
*/
|
|
645
|
+
export function findDestructiveGitCommands(command, cwd) {
|
|
646
|
+
const out = [];
|
|
647
|
+
for (const call of walkShell(command, cwd, (name) => name === "git")) {
|
|
648
|
+
// xargs appends operands read from stdin; stand in "." (whole tree)
|
|
649
|
+
// so `… | xargs git checkout --` is checked rather than parsed as empty.
|
|
650
|
+
const match = analyzeGit(call.viaXargs ? [...call.args, "."] : call.args, call.cwd);
|
|
651
|
+
if (!match)
|
|
652
|
+
continue;
|
|
653
|
+
// A pathspec still holding a variable (`for f in …; do git checkout -- $f`)
|
|
654
|
+
// could name anything once expanded: check the whole tree instead of a
|
|
655
|
+
// literal `$f` that matches no file.
|
|
656
|
+
if (match.pathspecs.some((spec) => spec.includes("$")))
|
|
657
|
+
match.pathspecs = [];
|
|
658
|
+
out.push(match);
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
class GuardError extends Error {
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Run an inspection git command against the target repo with repo-config
|
|
666
|
+
* execution sinks (fsmonitor, hooks, filters, …) disabled — see
|
|
667
|
+
* runBackgroundGit. `gitArgs` are the command's own `--git-dir`/`--work-tree`.
|
|
668
|
+
*/
|
|
669
|
+
async function runGit(dir, gitArgs, args) {
|
|
670
|
+
try {
|
|
671
|
+
const { stdout } = await runBackgroundGit(args, {
|
|
672
|
+
cwd: dir,
|
|
673
|
+
globalArgs: gitArgs,
|
|
674
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
675
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
676
|
+
env: { LC_ALL: "C" },
|
|
677
|
+
});
|
|
678
|
+
return { code: 0, stdout };
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
// A numeric code is git's exit status; a string (ENOENT) or a kill
|
|
682
|
+
// (timeout) means the guard itself could not inspect the repo.
|
|
683
|
+
if (error instanceof BackgroundGitError && typeof error.code === "number" && !error.killed) {
|
|
684
|
+
return { code: error.code, stdout: error.stdout };
|
|
685
|
+
}
|
|
686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
687
|
+
throw new GuardError(`git ${args.join(" ")} failed: ${message}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** `git status --porcelain=v1 -z`, parsed. Null when `dir` is not a repo. */
|
|
691
|
+
async function gitStatus(match, extra) {
|
|
692
|
+
const result = await runGit(match.dir, match.gitArgs, [
|
|
693
|
+
"status",
|
|
694
|
+
"--porcelain=v1",
|
|
695
|
+
"-z",
|
|
696
|
+
...extra,
|
|
697
|
+
"--",
|
|
698
|
+
...match.pathspecs,
|
|
699
|
+
]);
|
|
700
|
+
if (result.code !== 0)
|
|
701
|
+
return null;
|
|
702
|
+
const parts = result.stdout.split("\0");
|
|
703
|
+
const entries = [];
|
|
704
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
705
|
+
const part = parts[i] ?? "";
|
|
706
|
+
if (part.length < 4)
|
|
707
|
+
continue;
|
|
708
|
+
const code = part.slice(0, 2);
|
|
709
|
+
entries.push({ code, file: part.slice(3) });
|
|
710
|
+
if (code.startsWith("R") || code.startsWith("C"))
|
|
711
|
+
i += 1; // rename source path
|
|
712
|
+
}
|
|
713
|
+
return entries;
|
|
714
|
+
}
|
|
715
|
+
function listFiles(entries) {
|
|
716
|
+
const names = entries.slice(0, MAX_LISTED_FILES).map((e) => e.file);
|
|
717
|
+
const more = entries.length - names.length;
|
|
718
|
+
return names.join(", ") + (more > 0 ? `, and ${more} more` : "");
|
|
719
|
+
}
|
|
720
|
+
function describeTracked(entries) {
|
|
721
|
+
const counts = new Map();
|
|
722
|
+
for (const { code } of entries) {
|
|
723
|
+
const label = code.includes("D")
|
|
724
|
+
? "deleted"
|
|
725
|
+
: code.includes("A")
|
|
726
|
+
? "added"
|
|
727
|
+
: code.includes("U")
|
|
728
|
+
? "conflicted"
|
|
729
|
+
: code.startsWith("R")
|
|
730
|
+
? "renamed"
|
|
731
|
+
: "modified";
|
|
732
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
733
|
+
}
|
|
734
|
+
const breakdown = [...counts].map(([label, n]) => `${n} ${label}`).join(", ");
|
|
735
|
+
const noun = entries.length === 1 ? "file" : "files";
|
|
736
|
+
return `${entries.length} changed ${noun} (${breakdown}): ${listFiles(entries)}`;
|
|
737
|
+
}
|
|
738
|
+
const NO_UNDO = "Checkpoints do not capture shell commands, so this could not be undone.";
|
|
739
|
+
function stashAdvice(label, untracked = "-u") {
|
|
740
|
+
return (`Safe alternative: first save the work with \`git stash push ${untracked} -m "gg: before ${label}"\` ` +
|
|
741
|
+
"(that alone also leaves a clean tree; recover with `git stash pop`), or use ask_user to " +
|
|
742
|
+
"confirm the user really wants these changes discarded.");
|
|
743
|
+
}
|
|
744
|
+
async function evaluate(match) {
|
|
745
|
+
const where = match.pathspecs.length > 0 ? ` (${match.pathspecs.join(" ")})` : "";
|
|
746
|
+
switch (match.kind) {
|
|
747
|
+
case "force-push":
|
|
748
|
+
return (`Blocked \`${match.display}\`: a plain force-push overwrites remote commits that are ` +
|
|
749
|
+
"not in your local branch, and nothing here can restore them. Use " +
|
|
750
|
+
"`git push --force-with-lease` (add `--force-if-includes` for extra safety) instead. " +
|
|
751
|
+
"If the user explicitly wants a plain force-push, ask them to run it themselves.");
|
|
752
|
+
case "reset-hard":
|
|
753
|
+
case "checkout-paths":
|
|
754
|
+
case "restore-worktree": {
|
|
755
|
+
if (match.maybeRef !== undefined) {
|
|
756
|
+
const ref = await runGit(match.dir, match.gitArgs, [
|
|
757
|
+
"rev-parse",
|
|
758
|
+
"--verify",
|
|
759
|
+
"--quiet",
|
|
760
|
+
`${match.maybeRef}^{commit}`,
|
|
761
|
+
]);
|
|
762
|
+
if (ref.code === 0)
|
|
763
|
+
return null; // a branch switch, which git refuses if it would clobber
|
|
764
|
+
}
|
|
765
|
+
const status = await gitStatus(match, ["--untracked-files=no"]);
|
|
766
|
+
const tracked = (status ?? []).filter((e) => !e.code.startsWith("?") && !e.code.startsWith("!"));
|
|
767
|
+
if (tracked.length === 0)
|
|
768
|
+
return null;
|
|
769
|
+
return (`Blocked \`${match.display}\`: it would permanently discard uncommitted changes in ` +
|
|
770
|
+
`${match.dir}${where} — ${describeTracked(tracked)}. ${NO_UNDO} ` +
|
|
771
|
+
stashAdvice(match.kind === "reset-hard" ? "reset" : "discarding changes"));
|
|
772
|
+
}
|
|
773
|
+
case "clean": {
|
|
774
|
+
const extra = ["--untracked-files=normal", ...(match.ignored ? ["--ignored"] : [])];
|
|
775
|
+
const status = await gitStatus(match, extra);
|
|
776
|
+
const removable = (status ?? []).filter((e) => {
|
|
777
|
+
const untracked = e.code === "??";
|
|
778
|
+
const ignored = e.code === "!!";
|
|
779
|
+
if (!untracked && !ignored)
|
|
780
|
+
return false;
|
|
781
|
+
if (untracked && match.ignored === "only")
|
|
782
|
+
return false;
|
|
783
|
+
if (ignored && match.ignored === undefined)
|
|
784
|
+
return false;
|
|
785
|
+
// Without -d (and no explicit pathspec), untracked directories stay.
|
|
786
|
+
if (e.file.endsWith("/") && !match.directories && match.pathspecs.length === 0)
|
|
787
|
+
return false;
|
|
788
|
+
return true;
|
|
789
|
+
});
|
|
790
|
+
if (removable.length === 0)
|
|
791
|
+
return null;
|
|
792
|
+
const noun = removable.length === 1 ? "file" : "files";
|
|
793
|
+
return (`Blocked \`${match.display}\`: it would permanently delete ${removable.length} untracked ` +
|
|
794
|
+
`${noun} in ${match.dir}${where}: ${listFiles(removable)}. ${NO_UNDO} ` +
|
|
795
|
+
"Preview with `git clean -n` (same flags). " +
|
|
796
|
+
stashAdvice("clean", match.ignored ? "-a" : "-u"));
|
|
797
|
+
}
|
|
798
|
+
case "stash-drop":
|
|
799
|
+
case "stash-clear": {
|
|
800
|
+
const list = await runGit(match.dir, match.gitArgs, ["stash", "list"]);
|
|
801
|
+
const entries = list.code === 0 ? list.stdout.split("\n").filter((l) => l.length > 0) : [];
|
|
802
|
+
if (entries.length === 0)
|
|
803
|
+
return null;
|
|
804
|
+
const target = match.kind === "stash-clear"
|
|
805
|
+
? `all ${entries.length} stash ${entries.length === 1 ? "entry" : "entries"}`
|
|
806
|
+
: (entries.find((l) => l.startsWith(`${match.refs[0] ?? ""}:`)) ?? match.refs[0] ?? "");
|
|
807
|
+
return (`Blocked \`${match.display}\`: it would permanently delete ${target}. ` +
|
|
808
|
+
"Inspect with `git stash list` / `git stash show -p <entry>`, or keep it as a branch with " +
|
|
809
|
+
"`git stash branch <name> <entry>`. If the user explicitly wants it gone, ask them " +
|
|
810
|
+
"(ask_user) and have them run it themselves.");
|
|
811
|
+
}
|
|
812
|
+
case "branch-force-delete": {
|
|
813
|
+
const losses = [];
|
|
814
|
+
for (const branch of match.refs) {
|
|
815
|
+
const ref = `refs/heads/${branch}`;
|
|
816
|
+
const result = await runGit(match.dir, match.gitArgs, [
|
|
817
|
+
"rev-list",
|
|
818
|
+
"--count",
|
|
819
|
+
ref,
|
|
820
|
+
"--not",
|
|
821
|
+
`--exclude=${ref}`,
|
|
822
|
+
"--all",
|
|
823
|
+
]);
|
|
824
|
+
// Nonzero: no such branch — git branch -D will just fail.
|
|
825
|
+
const count = result.code === 0 ? Number.parseInt(result.stdout.trim(), 10) : 0;
|
|
826
|
+
if (count > 0)
|
|
827
|
+
losses.push(`${branch} (${count} commit${count === 1 ? "" : "s"})`);
|
|
828
|
+
}
|
|
829
|
+
if (losses.length === 0)
|
|
830
|
+
return null;
|
|
831
|
+
return (`Blocked \`${match.display}\`: these branches have commits that exist on no other branch, ` +
|
|
832
|
+
`tag or remote, so deleting them loses that work: ${losses.join(", ")}. ` +
|
|
833
|
+
"Use `git branch -d` (refuses unmerged branches), push or merge the branch first, or — if " +
|
|
834
|
+
"the user explicitly asked to delete it — keep a backup ref first " +
|
|
835
|
+
"(`git tag backup/<branch> <branch>`) and then delete.");
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Returns a block message for the model when `command` would destroy
|
|
841
|
+
* uncommitted work (or force-push), or null to let it run.
|
|
842
|
+
*/
|
|
843
|
+
export async function checkDestructiveGit(command, options) {
|
|
844
|
+
try {
|
|
845
|
+
if (!/\bgit\b/.test(command))
|
|
846
|
+
return null;
|
|
847
|
+
let matches = findDestructiveGitCommands(command, options.cwd);
|
|
848
|
+
if (matches.length === 0)
|
|
849
|
+
return null;
|
|
850
|
+
if (options.resolveCwd) {
|
|
851
|
+
const real = await options.resolveCwd();
|
|
852
|
+
if (real && path.isAbsolute(real) && real !== options.cwd)
|
|
853
|
+
matches = findDestructiveGitCommands(command, real);
|
|
854
|
+
}
|
|
855
|
+
const reasons = [];
|
|
856
|
+
for (const match of matches) {
|
|
857
|
+
// A directory the command creates first cannot hold uncommitted work yet.
|
|
858
|
+
if (match.kind !== "force-push" && !existsSync(match.dir))
|
|
859
|
+
continue;
|
|
860
|
+
const reason = await evaluate(match);
|
|
861
|
+
if (reason)
|
|
862
|
+
reasons.push(reason);
|
|
863
|
+
}
|
|
864
|
+
return reasons.length > 0 ? reasons.join("\n") : null;
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
log("WARN", "git-guard", `Destructive-git guard failed open: ${error.message}`);
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
//# sourceMappingURL=destructive-git-guard.js.map
|