cuelo 0.10.52 → 0.10.53
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +35 -35
- package/.next/build-manifest.json +4 -4
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +549 -561
- package/.next/required-server-files.js +25 -25
- package/.next/required-server-files.json +25 -25
- package/.next/server/app/_global-error/page.js +3 -3
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +2 -2
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/agent/[id]/account/route.js +1 -1
- package/.next/server/app/api/agent/[id]/account/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/bash-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agent/running/route.js +1 -1
- package/.next/server/app/api/agent/running/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/attachment-settings/route.js +1 -1
- package/.next/server/app/api/attachment-settings/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/attachments/[id]/transcription/route.js +2 -2
- package/.next/server/app/api/attachments/[id]/transcription/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/attachments/draft/route.js +1 -1
- package/.next/server/app/api/attachments/draft/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/attachments/route.js +2 -2
- package/.next/server/app/api/attachments/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/all-providers/route.js +1 -1
- package/.next/server/app/api/auth/all-providers/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/api-key/[provider]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
- package/.next/server/app/api/auth/logout/[provider]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/providers/route.js +1 -1
- package/.next/server/app/api/auth/providers/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/character-mode/route.js +1 -2
- package/.next/server/app/api/character-mode/route.js.nft.json +1 -1
- package/.next/server/app/api/character-mode/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/cwd/browse/route.js +1 -1
- package/.next/server/app/api/cwd/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/default-cwd/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/display-settings/route.js +1 -1
- package/.next/server/app/api/display-settings/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/document-text/route.js +2 -2
- package/.next/server/app/api/document-text/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/experiments/route.js +1 -1
- package/.next/server/app/api/experiments/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/file-index/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/git/diff/route.js +1 -1
- package/.next/server/app/api/git/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/git/status/route.js +2 -2
- package/.next/server/app/api/git/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/github/projects/route.js +1 -1
- package/.next/server/app/api/github/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/gpt6/handles/route.js +1 -1
- package/.next/server/app/api/gpt6/handles/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/gpt6/reply/route.js +1 -1
- package/.next/server/app/api/gpt6/reply/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/gpt6/token/route.js +1 -1
- package/.next/server/app/api/gpt6/token/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/gpt6/web6-session/route.js +1 -1
- package/.next/server/app/api/gpt6/web6-session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/home/route.js +1 -1
- package/.next/server/app/api/home/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/live/call/[callId]/route.js +2 -2
- package/.next/server/app/api/live/call/[callId]/route.js.nft.json +1 -1
- package/.next/server/app/api/live/call/[callId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/live/events/route.js +2 -2
- package/.next/server/app/api/live/events/route.js.nft.json +1 -1
- package/.next/server/app/api/live/events/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/live/interpret/route.js +2 -2
- package/.next/server/app/api/live/interpret/route.js.nft.json +1 -1
- package/.next/server/app/api/live/interpret/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/live/offer/route.js +1 -1
- package/.next/server/app/api/live/offer/route.js.nft.json +1 -1
- package/.next/server/app/api/live/offer/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/live/voices/route.js +1 -1
- package/.next/server/app/api/live/voices/route.js.nft.json +1 -1
- package/.next/server/app/api/live/voices/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/mcp/route.js +2 -2
- package/.next/server/app/api/mcp/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/model-roles/route.js +1 -1
- package/.next/server/app/api/model-roles/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/catalog/route.js +1 -1
- package/.next/server/app/api/models-config/catalog/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/discover/route.js +1 -1
- package/.next/server/app/api/models-config/discover/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/route.js +3 -3
- package/.next/server/app/api/models-config/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/models-config/test/route.js +1 -1
- package/.next/server/app/api/models-config/test/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/office-characters/[...path]/route.js +1 -2
- package/.next/server/app/api/office-characters/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/office-characters/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/project-docs/route.js +1 -1
- package/.next/server/app/api/project-docs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/project-trust/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route.js +2 -2
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/runs/[runId]/prelude/route.js +1 -1
- package/.next/server/app/api/runs/[runId]/prelude/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/runs/[runId]/route.js +1 -1
- package/.next/server/app/api/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/runs/[runId]/turns/route.js +1 -1
- package/.next/server/app/api/runs/[runId]/turns/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/runs/route.js +1 -1
- package/.next/server/app/api/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/search/route.js +1 -1
- package/.next/server/app/api/sessions/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/settings/route.js +1 -1
- package/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sidecars/[service]/[...path]/route.js +1 -1
- package/.next/server/app/api/sidecars/[service]/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/install/route.js +2 -2
- package/.next/server/app/api/skills/install/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/route.js +1 -1
- package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/search/route.js +1 -1
- package/.next/server/app/api/skills/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/subagents/eta/route.js +1 -1
- package/.next/server/app/api/subagents/eta/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/theme/route.js +1 -1
- package/.next/server/app/api/theme/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tool-image/route.js +1 -1
- package/.next/server/app/api/tool-image/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/update-maintenance/route.js +2 -2
- package/.next/server/app/api/update-maintenance/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/updates/route.js +1 -1
- package/.next/server/app/api/updates/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/web-access/recovery/route.js +1 -1
- package/.next/server/app/api/web-access/recovery/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/web-access/route.js +1 -1
- package/.next/server/app/api/web-access/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/web6/consults/route.js +1 -1
- package/.next/server/app/api/web6/consults/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +4 -4
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/manifest.webmanifest/route.js +2 -2
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +5 -5
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover/page.js +2 -2
- package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover.html +1 -1
- package/.next/server/app/recover.rsc +3 -3
- package/.next/server/app/recover.segments/_full.segment.rsc +3 -3
- package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
- package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
- package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +35 -35
- package/.next/server/chunks/1008.js +2 -2
- package/.next/server/chunks/1966.js +2 -0
- package/.next/server/chunks/319.js +3 -3
- package/.next/server/chunks/6429.js +2 -2
- package/.next/server/chunks/6775.js +7 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/middleware.js +9 -9
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/_not-found/page-041b64675adcdf82.js +1 -0
- package/.next/static/chunks/app/layout-3b1f8b74c7fc5e77.js +1 -0
- package/.next/static/chunks/app/{page-3640086c732eb87d.js → page-efae4fdcf84b7a14.js} +4 -4
- package/.next/static/chunks/app/recover/{page-c0c40b3c215078a4.js → page-2bcd6a351f3eda3d.js} +1 -1
- package/.next/static/chunks/{main-app-fbc4aa3c15d48613.js → main-app-ca46ab75db2b4e4b.js} +1 -1
- package/.next/static/chunks/main-dd4c9daba44bef88.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-dac2a58a3b9eb28e.js +1 -0
- package/.next/static/chunks/webpack-546c8221d1906945.js +1 -0
- package/.next/trace +107 -107
- package/.next/trace-build +1 -1
- package/.next/types/app/api/agent/[id]/account/route.ts +1 -1
- package/.next/types/app/api/agent/[id]/bash-output/route.ts +1 -1
- package/.next/types/app/api/agent/[id]/events/route.ts +1 -1
- package/.next/types/app/api/agent/[id]/route.ts +1 -1
- package/.next/types/app/api/agent/new/route.ts +1 -1
- package/.next/types/app/api/agent/running/route.ts +1 -1
- package/.next/types/app/api/attachment-settings/route.ts +1 -1
- package/.next/types/app/api/attachments/[id]/transcription/route.ts +1 -1
- package/.next/types/app/api/attachments/draft/route.ts +1 -1
- package/.next/types/app/api/attachments/route.ts +1 -1
- package/.next/types/app/api/auth/all-providers/route.ts +1 -1
- package/.next/types/app/api/auth/api-key/[provider]/route.ts +1 -1
- package/.next/types/app/api/auth/login/[provider]/route.ts +1 -1
- package/.next/types/app/api/auth/logout/[provider]/route.ts +1 -1
- package/.next/types/app/api/auth/providers/route.ts +1 -1
- package/.next/types/app/api/character-mode/route.ts +1 -1
- package/.next/types/app/api/cwd/browse/route.ts +1 -1
- package/.next/types/app/api/cwd/validate/route.ts +1 -1
- package/.next/types/app/api/default-cwd/route.ts +1 -1
- package/.next/types/app/api/display-settings/route.ts +1 -1
- package/.next/types/app/api/document-text/route.ts +1 -1
- package/.next/types/app/api/experiments/route.ts +1 -1
- package/.next/types/app/api/file-index/route.ts +1 -1
- package/.next/types/app/api/files/[...path]/route.ts +1 -1
- package/.next/types/app/api/git/diff/route.ts +1 -1
- package/.next/types/app/api/git/status/route.ts +1 -1
- package/.next/types/app/api/github/projects/route.ts +1 -1
- package/.next/types/app/api/gpt6/handles/route.ts +1 -1
- package/.next/types/app/api/gpt6/reply/route.ts +1 -1
- package/.next/types/app/api/gpt6/token/route.ts +1 -1
- package/.next/types/app/api/gpt6/web6-session/route.ts +1 -1
- package/.next/types/app/api/home/route.ts +1 -1
- package/.next/types/app/api/live/call/[callId]/route.ts +1 -1
- package/.next/types/app/api/live/events/route.ts +1 -1
- package/.next/types/app/api/live/interpret/route.ts +1 -1
- package/.next/types/app/api/live/offer/route.ts +1 -1
- package/.next/types/app/api/live/voices/route.ts +1 -1
- package/.next/types/app/api/mcp/route.ts +1 -1
- package/.next/types/app/api/model-roles/route.ts +1 -1
- package/.next/types/app/api/models/route.ts +1 -1
- package/.next/types/app/api/models-config/catalog/route.ts +1 -1
- package/.next/types/app/api/models-config/discover/route.ts +1 -1
- package/.next/types/app/api/models-config/route.ts +1 -1
- package/.next/types/app/api/models-config/test/route.ts +1 -1
- package/.next/types/app/api/office-characters/[...path]/route.ts +1 -1
- package/.next/types/app/api/plugins/route.ts +1 -1
- package/.next/types/app/api/project-docs/route.ts +1 -1
- package/.next/types/app/api/project-trust/route.ts +1 -1
- package/.next/types/app/api/projects/route.ts +1 -1
- package/.next/types/app/api/runs/[runId]/prelude/route.ts +1 -1
- package/.next/types/app/api/runs/[runId]/route.ts +1 -1
- package/.next/types/app/api/runs/[runId]/turns/route.ts +1 -1
- package/.next/types/app/api/runs/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/auto-name/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/context/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/entries/[entryId]/thinking/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/export/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/route.ts +1 -1
- package/.next/types/app/api/sessions/[id]/state/route.ts +1 -1
- package/.next/types/app/api/sessions/route.ts +1 -1
- package/.next/types/app/api/sessions/search/route.ts +1 -1
- package/.next/types/app/api/settings/route.ts +1 -1
- package/.next/types/app/api/sidecars/[service]/[...path]/route.ts +1 -1
- package/.next/types/app/api/skills/check/route.ts +1 -1
- package/.next/types/app/api/skills/install/route.ts +1 -1
- package/.next/types/app/api/skills/route.ts +1 -1
- package/.next/types/app/api/skills/search/route.ts +1 -1
- package/.next/types/app/api/skills/update/route.ts +1 -1
- package/.next/types/app/api/subagents/eta/route.ts +1 -1
- package/.next/types/app/api/theme/route.ts +1 -1
- package/.next/types/app/api/tool-image/route.ts +1 -1
- package/.next/types/app/api/update-maintenance/route.ts +1 -1
- package/.next/types/app/api/updates/route.ts +1 -1
- package/.next/types/app/api/web-access/recovery/route.ts +1 -1
- package/.next/types/app/api/web-access/route.ts +1 -1
- package/.next/types/app/api/web6/consults/route.ts +1 -1
- package/.next/types/app/api/worktrees/route.ts +1 -1
- package/.next/types/app/layout.ts +1 -1
- package/.next/types/app/page.ts +1 -1
- package/.next/types/app/recover/page.ts +1 -1
- package/LICENSE +22 -22
- package/README.md +273 -271
- package/Tools/CUELO_Setup/files/btw-server.js +346 -346
- package/Tools/CUELO_Setup/files/native-runtime-patch.js +238 -238
- package/Tools/CUELO_Setup/files/subagent-server.js +587 -587
- package/Tools/CUELO_Setup/files/usage-server.js +1057 -1057
- package/Tools/OMP_Global_Config/agent/AGENTS.md +144 -144
- package/Tools/OMP_Global_Config/agent/RULES.md +140 -140
- package/Tools/OMP_Global_Config/agent/TITLE_SYSTEM.md +17 -17
- package/Tools/OMP_Global_Config/agent/WATCHDOG.md +55 -55
- package/Tools/OMP_Global_Config/agent/agents/maker.md +413 -413
- package/Tools/OMP_Global_Config/agent/extensions/character-voice.ts +1035 -1035
- package/Tools/OMP_Global_Config/agent/extensions/command-guard/index.ts +1110 -1110
- package/Tools/OMP_Global_Config/agent/extensions/command-guard/interop-hint.ts +133 -133
- package/Tools/OMP_Global_Config/agent/extensions/command-guard/matcher.ts +2258 -2258
- package/Tools/OMP_Global_Config/agent/extensions/command-guard/powershell-syntax.ts +210 -210
- package/Tools/OMP_Global_Config/agent/extensions/command-guard/task-guard.ts +1137 -1121
- package/Tools/OMP_Global_Config/agent/extensions/cross-review.ts +132 -132
- package/Tools/OMP_Global_Config/agent/extensions/docmap-route.ts +133 -133
- package/Tools/OMP_Global_Config/agent/extensions/draft/markdown.ts +226 -226
- package/Tools/OMP_Global_Config/agent/extensions/draft/ooxml.ts +222 -222
- package/Tools/OMP_Global_Config/agent/extensions/draft/pdf.ts +63 -63
- package/Tools/OMP_Global_Config/agent/extensions/draft/zip.ts +73 -73
- package/Tools/OMP_Global_Config/agent/extensions/draft.ts +204 -204
- package/Tools/OMP_Global_Config/agent/extensions/edit-lessons.ts +159 -159
- package/Tools/OMP_Global_Config/agent/extensions/external-advice-check.ts +36 -36
- package/Tools/OMP_Global_Config/agent/extensions/gitnexus-autosync.ts +406 -406
- package/Tools/OMP_Global_Config/agent/extensions/image-question-router.ts +55 -55
- package/Tools/OMP_Global_Config/agent/extensions/jev-runtime.ts +2700 -2700
- package/Tools/OMP_Global_Config/agent/extensions/korean-reply-guard.ts +190 -190
- package/Tools/OMP_Global_Config/agent/extensions/lib/cross-review.ts +1208 -1208
- package/Tools/OMP_Global_Config/agent/extensions/lib/docmap-search.ts +612 -612
- package/Tools/OMP_Global_Config/agent/extensions/lib/job-observation.ts +182 -182
- package/Tools/OMP_Global_Config/agent/extensions/lib/maker-routing.ts +1332 -1332
- package/Tools/OMP_Global_Config/agent/extensions/lib/mcp-selection.ts +361 -361
- package/Tools/OMP_Global_Config/agent/extensions/lib/memory-excerpt.ts +5 -5
- package/Tools/OMP_Global_Config/agent/extensions/lib/native-judge.ts +113 -113
- package/Tools/OMP_Global_Config/agent/extensions/lib/prepared-task.ts +211 -211
- package/Tools/OMP_Global_Config/agent/extensions/lib/routing-ledger.ts +538 -538
- package/Tools/OMP_Global_Config/agent/extensions/lib/session-identity.ts +34 -34
- package/Tools/OMP_Global_Config/agent/extensions/lib/task-progress.ts +492 -492
- package/Tools/OMP_Global_Config/agent/extensions/mcp-selection.ts +301 -301
- package/Tools/OMP_Global_Config/agent/extensions/prompt-compact.ts +67 -67
- package/Tools/OMP_Global_Config/agent/extensions/report-chart-guard.ts +102 -102
- package/Tools/OMP_Global_Config/agent/extensions/skim.ts +332 -332
- package/Tools/OMP_Global_Config/agent/extensions/steering-reply-gate.ts +207 -207
- package/Tools/OMP_Global_Config/agent/extensions/todo-nudge.ts +80 -80
- package/Tools/OMP_Global_Config/agent/extensions/turn-end-guard.ts +421 -421
- package/Tools/OMP_Global_Config/agent/prompt-copies/AGENTS.en.md +147 -147
- package/Tools/OMP_Global_Config/agent/prompt-copies/RULES.en.md +49 -49
- package/Tools/OMP_Global_Config/agent/rules/docs-handoff.md +39 -39
- package/Tools/OMP_Global_Config/agent/rules/frontend.md +74 -74
- package/Tools/OMP_Global_Config/agent/rules/harness-policy.json +1224 -1224
- package/Tools/OMP_Global_Config/agent/rules/legacy-encoding.md +21 -21
- package/Tools/OMP_Global_Config/agent/rules/meta-harness.md +45 -45
- package/Tools/OMP_Global_Config/agent/rules/omp-harness.md +151 -151
- package/Tools/OMP_Global_Config/agent/rules/subagent.md +750 -750
- package/Tools/OMP_Global_Config/agent/rules/task-guard.md +62 -62
- package/Tools/OMP_Global_Config/agent/rules/verdict.md +210 -210
- package/Tools/OMP_Global_Config/agent/rules/web6-consult.md +83 -83
- package/Tools/OMP_Global_Config/agent/sop/_common.md +136 -136
- package/Tools/OMP_Global_Config/agent/sop/_writer.md +259 -259
- package/Tools/OMP_Global_Config/agent/sop/maker.md +9 -9
- package/Tools/OMP_Global_Config/agent/tools/git-finalizer/finalizer.ps1 +409 -409
- package/Tools/OMP_Global_Config/agent/tools/git-finalizer/index.ts +303 -303
- package/Tools/OMP_Global_Config/agent/tools/git-finalizer/sync-checkout.ts +184 -184
- package/Tools/OMP_Global_Config/agent/tools/git-finalizer/wsl-git.ts +96 -96
- package/Tools/OMP_Global_Config/agent/tools/windows-build/index.ts +387 -387
- package/Tools/OMP_Global_Config/agent/tools/windows-control/control.ps1 +200 -200
- package/Tools/OMP_Global_Config/agent/tools/windows-control/index.ts +255 -255
- package/Tools/OMP_Global_Config/notices/orchestrate.md +26 -26
- package/Tools/OMP_Global_Config/notices/ultrathink.md +9 -9
- package/Tools/OMP_Global_Config/notices/workflow.md +16 -16
- package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +18339 -18304
- package/Tools/OMP_Global_Config/patches/apply-notices.mjs +135 -135
- package/bin/cuelo-options.js +38 -38
- package/bin/cuelo.js +273 -273
- package/bin/docker-entrypoint.mjs +19 -19
- package/bin/password-prompt.js +100 -100
- package/bin/prepare-runtime.js +112 -112
- package/bin/runtime.js +134 -134
- package/bin/web-auth-store.d.ts +104 -104
- package/bin/web-auth-store.js +593 -593
- package/install.mjs +427 -427
- package/next.config.ts +104 -104
- package/package.json +141 -141
- package/public/audio/completion/voices.json +1393 -1393
- package/public/fonts/cuelo-display/NOTICE.txt +15 -15
- package/public/fonts/fonts.css +288 -288
- package/public/fonts/jetbrains-mono/OFL.txt +93 -93
- package/public/fonts/wanted-sans/OFL.txt +93 -93
- package/public/icons/catppuccin/LICENSE +22 -22
- package/public/icons/catppuccin/README.md +8 -8
- package/public/icons/catppuccin/latte/_file.svg +3 -3
- package/public/icons/catppuccin/latte/_folder.svg +3 -3
- package/public/icons/catppuccin/latte/_folder_open.svg +3 -3
- package/public/icons/catppuccin/latte/bash.svg +7 -7
- package/public/icons/catppuccin/latte/bun-lock.svg +8 -8
- package/public/icons/catppuccin/latte/config.svg +3 -3
- package/public/icons/catppuccin/latte/css.svg +6 -6
- package/public/icons/catppuccin/latte/database.svg +3 -3
- package/public/icons/catppuccin/latte/docker.svg +3 -3
- package/public/icons/catppuccin/latte/env.svg +3 -3
- package/public/icons/catppuccin/latte/eslint.svg +6 -6
- package/public/icons/catppuccin/latte/git.svg +6 -6
- package/public/icons/catppuccin/latte/go.svg +3 -3
- package/public/icons/catppuccin/latte/graphql.svg +3 -3
- package/public/icons/catppuccin/latte/html.svg +6 -6
- package/public/icons/catppuccin/latte/javascript-react.svg +8 -8
- package/public/icons/catppuccin/latte/javascript.svg +6 -6
- package/public/icons/catppuccin/latte/json.svg +3 -3
- package/public/icons/catppuccin/latte/lock.svg +3 -3
- package/public/icons/catppuccin/latte/markdown.svg +3 -3
- package/public/icons/catppuccin/latte/ms-word.svg +6 -6
- package/public/icons/catppuccin/latte/next.svg +6 -6
- package/public/icons/catppuccin/latte/npm-lock.svg +7 -7
- package/public/icons/catppuccin/latte/pdf.svg +3 -3
- package/public/icons/catppuccin/latte/python.svg +8 -8
- package/public/icons/catppuccin/latte/rust.svg +6 -6
- package/public/icons/catppuccin/latte/sass.svg +3 -3
- package/public/icons/catppuccin/latte/terraform.svg +3 -3
- package/public/icons/catppuccin/latte/toml.svg +3 -3
- package/public/icons/catppuccin/latte/typescript-react.svg +8 -8
- package/public/icons/catppuccin/latte/typescript.svg +6 -6
- package/public/icons/catppuccin/latte/yaml.svg +3 -3
- package/public/icons/catppuccin/mocha/_file.svg +3 -3
- package/public/icons/catppuccin/mocha/_folder.svg +3 -3
- package/public/icons/catppuccin/mocha/_folder_open.svg +3 -3
- package/public/icons/catppuccin/mocha/bash.svg +7 -7
- package/public/icons/catppuccin/mocha/bun-lock.svg +8 -8
- package/public/icons/catppuccin/mocha/config.svg +3 -3
- package/public/icons/catppuccin/mocha/css.svg +6 -6
- package/public/icons/catppuccin/mocha/database.svg +3 -3
- package/public/icons/catppuccin/mocha/docker.svg +3 -3
- package/public/icons/catppuccin/mocha/env.svg +3 -3
- package/public/icons/catppuccin/mocha/eslint.svg +6 -6
- package/public/icons/catppuccin/mocha/git.svg +6 -6
- package/public/icons/catppuccin/mocha/go.svg +3 -3
- package/public/icons/catppuccin/mocha/graphql.svg +3 -3
- package/public/icons/catppuccin/mocha/html.svg +6 -6
- package/public/icons/catppuccin/mocha/javascript-react.svg +8 -8
- package/public/icons/catppuccin/mocha/javascript.svg +6 -6
- package/public/icons/catppuccin/mocha/json.svg +3 -3
- package/public/icons/catppuccin/mocha/lock.svg +3 -3
- package/public/icons/catppuccin/mocha/markdown.svg +3 -3
- package/public/icons/catppuccin/mocha/ms-word.svg +6 -6
- package/public/icons/catppuccin/mocha/next.svg +6 -6
- package/public/icons/catppuccin/mocha/npm-lock.svg +7 -7
- package/public/icons/catppuccin/mocha/pdf.svg +3 -3
- package/public/icons/catppuccin/mocha/python.svg +8 -8
- package/public/icons/catppuccin/mocha/rust.svg +6 -6
- package/public/icons/catppuccin/mocha/sass.svg +3 -3
- package/public/icons/catppuccin/mocha/terraform.svg +3 -3
- package/public/icons/catppuccin/mocha/toml.svg +3 -3
- package/public/icons/catppuccin/mocha/typescript-react.svg +8 -8
- package/public/icons/catppuccin/mocha/typescript.svg +6 -6
- package/public/icons/catppuccin/mocha/yaml.svg +3 -3
- package/public/icons/cuelo-icon.svg +8 -8
- package/public/offline.html +145 -145
- package/public/sw.js +135 -135
- package/.next/server/chunks/205.js +0 -7
- package/.next/server/chunks/7738.js +0 -1
- package/.next/static/chunks/1966.b8f83f7ba376c962.js +0 -1
- package/.next/static/chunks/3899.3f761f7e3a944e8c.js +0 -1
- package/.next/static/chunks/app/_not-found/page-9e5295df095e2e41.js +0 -1
- package/.next/static/chunks/app/layout-8b29ba206a96135e.js +0 -1
- package/.next/static/chunks/main-1ddb405481ce21f6.js +0 -5
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-50782c8c9c06c8b5.js +0 -1
- package/.next/static/chunks/webpack-7b514b9855bd748c.js +0 -1
- /package/.next/static/{dQA--5NX6NZt_A6pLZUde → -mDQEyWQ8VQsAfLpiACIP}/_buildManifest.js +0 -0
- /package/.next/static/{dQA--5NX6NZt_A6pLZUde → -mDQEyWQ8VQsAfLpiACIP}/_ssgManifest.js +0 -0
|
@@ -1,2258 +1,2258 @@
|
|
|
1
|
-
import { isAbsolute, resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
export interface GuardContext {
|
|
4
|
-
/** bash tool_call이 지정한 cwd. 없으면 세션 cwd. */
|
|
5
|
-
cwd?: string;
|
|
6
|
-
/** 주어진 디렉터리가 세션 저장소와 같은 git 저장소인지. 미제공이면 차단. */
|
|
7
|
-
isSessionRepository?: (dir: string) => boolean;
|
|
8
|
-
/**
|
|
9
|
-
* 주어진 디렉터리를 하네스가 Windows `git.exe`로 다루는지(WSL의 `/mnt/<드라이브>` 저장소). 미제공이면 판정하지 않는다.
|
|
10
|
-
*/
|
|
11
|
-
usesWindowsGit?: (dir: string) => boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Separator = ";" | "&&" | "||" | "|" | "&" | "\n";
|
|
15
|
-
|
|
16
|
-
interface ShellToken {
|
|
17
|
-
value: string;
|
|
18
|
-
quoted: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface Heredoc {
|
|
22
|
-
delimiter: string;
|
|
23
|
-
/** 구분자에 따옴표·역슬래시가 있으면 본문은 확장 없이 그대로다. */
|
|
24
|
-
quoted: boolean;
|
|
25
|
-
stripTabs: boolean;
|
|
26
|
-
/** `<<` 뒤 줄바꿈부터 구분자 줄 앞까지. 줄바꿈을 만나기 전에는 비어 있다. */
|
|
27
|
-
body: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface ShellSegment {
|
|
31
|
-
tokens: ShellToken[];
|
|
32
|
-
separatorBefore?: Separator;
|
|
33
|
-
/** 괄호 subshell 깊이. `cd`의 효과는 같은 깊이 이하로만 이어진다. */
|
|
34
|
-
depth: number;
|
|
35
|
-
/** 따옴표 밖·큰따옴표 안 `$(…)`·백틱 명령 치환의 본문(작은따옴표 안과 `\$`는 제외). */
|
|
36
|
-
substitutions: string[];
|
|
37
|
-
/** 이 세그먼트의 stdin으로 들어가는 heredoc. */
|
|
38
|
-
heredocs: Heredoc[];
|
|
39
|
-
/** `<<<` here-string 값. */
|
|
40
|
-
hereStrings: string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface TokenizedShell {
|
|
44
|
-
segments: ShellSegment[];
|
|
45
|
-
/** 실행 세그먼트에 붙지 않은 치환 본문(`> $(…)`처럼 리다이렉션 대상 등). */
|
|
46
|
-
loose: string[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface Invocation {
|
|
50
|
-
index: number;
|
|
51
|
-
name: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface GitInvocation {
|
|
55
|
-
subcommand: string;
|
|
56
|
-
args: string[];
|
|
57
|
-
globalArgs: string[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type StringLookup = Readonly<Record<string, true>>;
|
|
61
|
-
|
|
62
|
-
const GIT_GLOBAL_OPTIONS_WITH_VALUE: StringLookup = {
|
|
63
|
-
"-c": true,
|
|
64
|
-
"-C": true,
|
|
65
|
-
"--config-env": true,
|
|
66
|
-
"--exec-path": true,
|
|
67
|
-
"--git-dir": true,
|
|
68
|
-
"--namespace": true,
|
|
69
|
-
"--super-prefix": true,
|
|
70
|
-
"--work-tree": true,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const CLI_OPTIONS_WITH_VALUE: StringLookup = {
|
|
74
|
-
"--config": true,
|
|
75
|
-
"--cwd": true,
|
|
76
|
-
"--dir": true,
|
|
77
|
-
"--filter": true,
|
|
78
|
-
"--prefix": true,
|
|
79
|
-
"--project": true,
|
|
80
|
-
"--repo": true,
|
|
81
|
-
"--repository": true,
|
|
82
|
-
"--workspace": true,
|
|
83
|
-
"-C": true,
|
|
84
|
-
"-R": true,
|
|
85
|
-
"-c": true,
|
|
86
|
-
"-w": true,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const FINALIZER_OWNED_GIT_COMMANDS: StringLookup = {
|
|
90
|
-
add: true,
|
|
91
|
-
commit: true,
|
|
92
|
-
"commit-tree": true,
|
|
93
|
-
push: true,
|
|
94
|
-
"update-ref": true,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/** 저장소 없이 도는 git 하위 명령. 어느 git으로 돌려도 index를 건드리지 않는다. */
|
|
98
|
-
const REPOSITORY_FREE_GIT_COMMANDS: StringLookup = {
|
|
99
|
-
help: true,
|
|
100
|
-
version: true,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const DB_CLIENTS: StringLookup = {
|
|
104
|
-
duckdb: true,
|
|
105
|
-
"invoke-sqlcmd": true,
|
|
106
|
-
mariadb: true,
|
|
107
|
-
mysql: true,
|
|
108
|
-
psql: true,
|
|
109
|
-
sqlite3: true,
|
|
110
|
-
sqlcmd: true,
|
|
111
|
-
sqlplus: true,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const SQLITE_OPTIONS_WITH_VALUE: StringLookup = {
|
|
115
|
-
"-cmd": true,
|
|
116
|
-
"-init": true,
|
|
117
|
-
"-separator": true,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const BROAD_PATHS: StringLookup = {
|
|
121
|
-
"*": true,
|
|
122
|
-
"**": true,
|
|
123
|
-
"**/*": true,
|
|
124
|
-
".": true,
|
|
125
|
-
"./": true,
|
|
126
|
-
"./*": true,
|
|
127
|
-
"./**": true,
|
|
128
|
-
"./**/*": true,
|
|
129
|
-
"..": true,
|
|
130
|
-
"../": true,
|
|
131
|
-
"../*": true,
|
|
132
|
-
"/": true,
|
|
133
|
-
"/*": true,
|
|
134
|
-
"~": true,
|
|
135
|
-
"~/": true,
|
|
136
|
-
"~/*": true,
|
|
137
|
-
"~/**": true,
|
|
138
|
-
"~/**/*": true,
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const BROAD_ENVIRONMENT_PATHS: StringLookup = {
|
|
142
|
-
"$HOME": true,
|
|
143
|
-
"$HOME/": true,
|
|
144
|
-
"$HOME/*": true,
|
|
145
|
-
"$HOME/**": true,
|
|
146
|
-
"${HOME}": true,
|
|
147
|
-
"${HOME}/": true,
|
|
148
|
-
"${HOME}/*": true,
|
|
149
|
-
"${HOME}/**": true,
|
|
150
|
-
"$PWD": true,
|
|
151
|
-
"$PWD/": true,
|
|
152
|
-
"$PWD/*": true,
|
|
153
|
-
"$PWD/**": true,
|
|
154
|
-
"${PWD}": true,
|
|
155
|
-
"${PWD}/": true,
|
|
156
|
-
"${PWD}/*": true,
|
|
157
|
-
"${PWD}/**": true,
|
|
158
|
-
"%CD%": true,
|
|
159
|
-
"%CD%/": true,
|
|
160
|
-
"%CD%/*": true,
|
|
161
|
-
"%CD%/**": true,
|
|
162
|
-
"%USERPROFILE%": true,
|
|
163
|
-
"%USERPROFILE%/": true,
|
|
164
|
-
"%USERPROFILE%/*": true,
|
|
165
|
-
"%USERPROFILE%/**": true,
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const INVOCATION_PREFIXES: StringLookup = {
|
|
169
|
-
"!": true,
|
|
170
|
-
"&": true,
|
|
171
|
-
"{": true,
|
|
172
|
-
"}": true,
|
|
173
|
-
do: true,
|
|
174
|
-
elif: true,
|
|
175
|
-
else: true,
|
|
176
|
-
if: true,
|
|
177
|
-
then: true,
|
|
178
|
-
until: true,
|
|
179
|
-
while: true,
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const COMMAND_WRAPPERS: StringLookup = {
|
|
183
|
-
builtin: true,
|
|
184
|
-
command: true,
|
|
185
|
-
nohup: true,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const ENV_OPTIONS_WITH_VALUE: StringLookup = {
|
|
189
|
-
"--chdir": true,
|
|
190
|
-
"--unset": true,
|
|
191
|
-
"-C": true,
|
|
192
|
-
"-u": true,
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const SUDO_OPTIONS_WITH_VALUE: StringLookup = {
|
|
196
|
-
"-C": true,
|
|
197
|
-
"-D": true,
|
|
198
|
-
"-g": true,
|
|
199
|
-
"-h": true,
|
|
200
|
-
"-p": true,
|
|
201
|
-
"-R": true,
|
|
202
|
-
"-T": true,
|
|
203
|
-
"-u": true,
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const TIME_OPTIONS_WITH_VALUE: StringLookup = {
|
|
207
|
-
"--format": true,
|
|
208
|
-
"--output": true,
|
|
209
|
-
"-f": true,
|
|
210
|
-
"-o": true,
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const TIMEOUT_OPTIONS_WITH_VALUE: StringLookup = {
|
|
214
|
-
"--kill-after": true,
|
|
215
|
-
"--signal": true,
|
|
216
|
-
"-k": true,
|
|
217
|
-
"-s": true,
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const REMOVE_ITEM_PATH_OPTIONS: StringLookup = {
|
|
222
|
-
"-literalpath": true,
|
|
223
|
-
"-path": true,
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const REMOVE_ITEM_OPTIONS_WITH_VALUE: StringLookup = {
|
|
227
|
-
"-credential": true,
|
|
228
|
-
"-exclude": true,
|
|
229
|
-
"-filter": true,
|
|
230
|
-
"-include": true,
|
|
231
|
-
"-stream": true,
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const PACKAGE_EXECUTORS: StringLookup = {
|
|
235
|
-
bunx: true,
|
|
236
|
-
npx: true,
|
|
237
|
-
pnpx: true,
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const PACKAGE_MANAGERS: StringLookup = {
|
|
241
|
-
bun: true,
|
|
242
|
-
npm: true,
|
|
243
|
-
pnpm: true,
|
|
244
|
-
yarn: true,
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const POWERSHELL_OPTIONS_WITH_VALUE: StringLookup = {
|
|
248
|
-
"-configurationname": true,
|
|
249
|
-
"-ep": true,
|
|
250
|
-
"-executionpolicy": true,
|
|
251
|
-
"-inputformat": true,
|
|
252
|
-
"-outputformat": true,
|
|
253
|
-
"-settingsfile": true,
|
|
254
|
-
"-version": true,
|
|
255
|
-
"-windowstyle": true,
|
|
256
|
-
"-wd": true,
|
|
257
|
-
"-workingdirectory": true,
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const GH_RELEASE_MUTATIONS: StringLookup = {
|
|
261
|
-
create: true,
|
|
262
|
-
delete: true,
|
|
263
|
-
edit: true,
|
|
264
|
-
upload: true,
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const DEPLOY_TARGET_RUNNERS: StringLookup = {
|
|
268
|
-
just: true,
|
|
269
|
-
make: true,
|
|
270
|
-
task: true,
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const SCRIPT_INTERPRETERS: StringLookup = {
|
|
274
|
-
bash: true,
|
|
275
|
-
deno: true,
|
|
276
|
-
node: true,
|
|
277
|
-
python: true,
|
|
278
|
-
python3: true,
|
|
279
|
-
sh: true,
|
|
280
|
-
zsh: true,
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const POWERSHELL_COMMAND_OPTIONS: StringLookup = {
|
|
284
|
-
"-c": true,
|
|
285
|
-
"-command": true,
|
|
286
|
-
"-commandwithargs": true,
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
interface WrapperSpec {
|
|
290
|
-
optionsWithValue: StringLookup;
|
|
291
|
-
/** 옵션 뒤, 감싸는 명령 앞에 오는 위치 인자 수(`taskset <mask>`, `flock <lockfile>`). */
|
|
292
|
-
positionals: number;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** 뒤따르는 명령을 그대로 실행하는 래퍼. 옵션 형태는 각 명령의 실제 CLI를 따른다. */
|
|
296
|
-
const EXEC_WRAPPERS: Readonly<Record<string, WrapperSpec>> = {
|
|
297
|
-
exec: { optionsWithValue: { "-a": true }, positionals: 0 },
|
|
298
|
-
nice: { optionsWithValue: { "-n": true, "--adjustment": true }, positionals: 0 },
|
|
299
|
-
setsid: { optionsWithValue: {}, positionals: 0 },
|
|
300
|
-
ionice: {
|
|
301
|
-
optionsWithValue: { "-c": true, "--class": true, "-n": true, "--classdata": true },
|
|
302
|
-
positionals: 0,
|
|
303
|
-
},
|
|
304
|
-
stdbuf: {
|
|
305
|
-
optionsWithValue: { "-i": true, "-o": true, "-e": true, "--input": true, "--output": true, "--error": true },
|
|
306
|
-
positionals: 0,
|
|
307
|
-
},
|
|
308
|
-
taskset: { optionsWithValue: {}, positionals: 1 },
|
|
309
|
-
chrt: { optionsWithValue: {}, positionals: 1 },
|
|
310
|
-
flock: {
|
|
311
|
-
optionsWithValue: { "-w": true, "--wait": true, "--timeout": true, "-E": true, "--conflict-exit-code": true },
|
|
312
|
-
positionals: 1,
|
|
313
|
-
},
|
|
314
|
-
xargs: {
|
|
315
|
-
optionsWithValue: {
|
|
316
|
-
"-a": true, "--arg-file": true, "-d": true, "--delimiter": true, "-E": true, "--eof": true, "-I": true,
|
|
317
|
-
"-L": true, "--max-lines": true, "-n": true, "--max-args": true, "-P": true, "--max-procs": true,
|
|
318
|
-
"-s": true, "--max-chars": true,
|
|
319
|
-
},
|
|
320
|
-
positionals: 0,
|
|
321
|
-
},
|
|
322
|
-
doas: { optionsWithValue: { "-u": true, "-C": true }, positionals: 0 },
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
/** 명령 문자열을 `-c`로 받아 셸로 실행하는 인터프리터. */
|
|
326
|
-
const SHELLS: StringLookup = {
|
|
327
|
-
ash: true,
|
|
328
|
-
bash: true,
|
|
329
|
-
dash: true,
|
|
330
|
-
fish: true,
|
|
331
|
-
ksh: true,
|
|
332
|
-
mksh: true,
|
|
333
|
-
sh: true,
|
|
334
|
-
zsh: true,
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
/** 최대 중첩 깊이(`bash -c`·`$(…)`·heredoc 본문). 넘으면 더 파고들지 않는다. */
|
|
338
|
-
const MAX_NESTING = 5;
|
|
339
|
-
|
|
340
|
-
const WRANGLER_OPTIONS_WITH_VALUE: StringLookup = {
|
|
341
|
-
...CLI_OPTIONS_WITH_VALUE,
|
|
342
|
-
"--env": true,
|
|
343
|
-
"-e": true,
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
/** 패키지 러너가 도구 이름 앞에서 값을 받는 옵션. */
|
|
347
|
-
const PACKAGE_RUNNER_OPTIONS_WITH_VALUE: StringLookup = {
|
|
348
|
-
...CLI_OPTIONS_WITH_VALUE,
|
|
349
|
-
"--package": true,
|
|
350
|
-
"-p": true,
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
/** 패키지 매니저가 도구 이름 앞에 두는 실행 하위 명령(`pnpm dlx wrangler`, `bun x wrangler`). */
|
|
354
|
-
const PACKAGE_MANAGER_EXEC_LEADERS: StringLookup = {
|
|
355
|
-
dlx: true,
|
|
356
|
-
exec: true,
|
|
357
|
-
run: true,
|
|
358
|
-
"run-script": true,
|
|
359
|
-
x: true,
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
interface ShortQueryOptions {
|
|
363
|
-
/** 뒤따르는 SQL을 값으로 받는 짧은 옵션 글자. */
|
|
364
|
-
query: string;
|
|
365
|
-
/** 값을 받지 않는 짧은 옵션 글자. 묶음(`-Atc`)을 앞에서부터 풀 때 건너뛴다. */
|
|
366
|
-
withoutValue: string;
|
|
367
|
-
/** 값을 받는 다른 옵션 글자(`-d db`, `-U user`). */
|
|
368
|
-
withValue: string;
|
|
369
|
-
/** 값이 같은 낱말에만 붙는 옵션 글자(mysql `-p<password>`). 뒤 글자는 옵션이 아니라 값이다. */
|
|
370
|
-
attachedOnly: string;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const PSQL_SHORT_OPTIONS: ShortQueryOptions = {
|
|
374
|
-
query: "c",
|
|
375
|
-
withoutValue: "AabeEHlnqsStVwWxXz01?",
|
|
376
|
-
withValue: "dfFhLopPRTUv",
|
|
377
|
-
attachedOnly: "",
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const MYSQL_SHORT_OPTIONS: ShortQueryOptions = {
|
|
381
|
-
query: "e",
|
|
382
|
-
withoutValue: "AaBbCcEfHIiLnNoqrstTUvVwWXj?",
|
|
383
|
-
withValue: "DhPuSO",
|
|
384
|
-
attachedOnly: "p",
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const REASONS = {
|
|
388
|
-
gitFinalize:
|
|
389
|
-
"자동 staging·commit·push와 ref 갱신은 저장소 잠금과 경로·ancestry 검증을 수행하는 git_finalize 도구로만 실행하세요.",
|
|
390
|
-
gitReset:
|
|
391
|
-
"작업 내용을 파괴할 수 있는 git reset --hard 명령은 정책상 차단됩니다.",
|
|
392
|
-
gitClean:
|
|
393
|
-
"디렉터리를 강제 정리하는 git clean 명령은 정책상 차단됩니다.",
|
|
394
|
-
gitDelete:
|
|
395
|
-
"브랜치·태그를 강제로 삭제하거나 원격 ref를 삭제하는 명령은 정책상 차단됩니다.",
|
|
396
|
-
gitDiscard:
|
|
397
|
-
"작업 트리나 stash의 변경을 되돌릴 수 없게 버리는 git checkout·restore·switch·stash·worktree 명령은 정책상 차단됩니다. " +
|
|
398
|
-
"변경을 남기려면 먼저 commit하거나 git stash push로 보관하세요.",
|
|
399
|
-
broadRemove:
|
|
400
|
-
"현재 작업 트리나 루트 경로를 광범위하게 삭제하는 명령은 정책상 차단됩니다.",
|
|
401
|
-
database:
|
|
402
|
-
"실제 데이터베이스 클라이언트로 DROP/TRUNCATE를 실행하는 명령은 정책상 차단됩니다.",
|
|
403
|
-
deploy:
|
|
404
|
-
"배포·게시·릴리스 작업을 실행하는 명령은 정책상 차단됩니다.",
|
|
405
|
-
linuxGitOnWindowsDrive:
|
|
406
|
-
"이 저장소는 Windows 드라이브(/mnt/<드라이브>)에 있어 하네스·앱·git_finalize가 Windows git.exe로 다룹니다. " +
|
|
407
|
-
"Linux git을 섞어 쓰면 index의 파일 정보가 서로 맞지 않아 추적 파일 전체를 다시 확인하고 index를 다시 씁니다. " +
|
|
408
|
-
"큰 저장소에서는 수 분이 걸려 시간 제한에 끊기고 index.lock이 남습니다(--no-optional-locks로도 git diff의 갱신은 막히지 않습니다). " +
|
|
409
|
-
"같은 명령을 git.exe로 실행하세요. 예: cd <저장소 안 폴더> && git.exe status. -C에는 \"$(wslpath -w <경로>)\"로 바꾼 경로를 넘깁니다.",
|
|
410
|
-
} as const;
|
|
411
|
-
|
|
412
|
-
function isRedirectionToken(value: string): boolean {
|
|
413
|
-
return /^(?:(?:\d+)?(?:<|>|<<|>>|<<<|<>|>&|<&|>\|)|&>|&>>)$/.test(value);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function stripRedirections(tokens: ShellToken[]): ShellToken[] {
|
|
417
|
-
const executable: ShellToken[] = [];
|
|
418
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
419
|
-
if (!isRedirectionToken(tokens[index].value)) {
|
|
420
|
-
executable.push(tokens[index]);
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
if (index + 1 < tokens.length && !isRedirectionToken(tokens[index + 1].value)) {
|
|
424
|
-
index += 1;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
return executable;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
interface Substitution {
|
|
431
|
-
inner: string;
|
|
432
|
-
/** 치환이 끝난 다음 위치. */
|
|
433
|
-
end: number;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function readBacktickSubstitution(text: string, start: number): Substitution {
|
|
437
|
-
let inner = "";
|
|
438
|
-
let index = start + 1;
|
|
439
|
-
while (index < text.length) {
|
|
440
|
-
const character = text[index];
|
|
441
|
-
if (character === "\\" && index + 1 < text.length && /[`\\$]/.test(text[index + 1])) {
|
|
442
|
-
inner += text[index + 1];
|
|
443
|
-
index += 2;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
if (character === "`") return { inner, end: index + 1 };
|
|
447
|
-
inner += character;
|
|
448
|
-
index += 1;
|
|
449
|
-
}
|
|
450
|
-
return { inner, end: text.length };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function skipDoubleQuoted(text: string, start: number): number {
|
|
454
|
-
let index = start;
|
|
455
|
-
while (index < text.length) {
|
|
456
|
-
const character = text[index];
|
|
457
|
-
if (character === "\\") index += 2;
|
|
458
|
-
else if (character === '"') return index + 1;
|
|
459
|
-
else if (character === "$" && text[index + 1] === "(") index = readCommandSubstitution(text, index).end;
|
|
460
|
-
else if (character === "`") index = readBacktickSubstitution(text, index).end;
|
|
461
|
-
else index += 1;
|
|
462
|
-
}
|
|
463
|
-
return text.length;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/** `text[start]`가 `$`이고 다음이 `(`일 때 짝이 맞는 `)`까지 읽는다. 따옴표·이스케이프·중첩 치환을 따라간다. */
|
|
467
|
-
function readCommandSubstitution(text: string, start: number): Substitution {
|
|
468
|
-
let depth = 1;
|
|
469
|
-
let index = start + 2;
|
|
470
|
-
while (index < text.length) {
|
|
471
|
-
const character = text[index];
|
|
472
|
-
if (character === "\\") {
|
|
473
|
-
index += 2;
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
if (character === "'") {
|
|
477
|
-
const close = text.indexOf("'", index + 1);
|
|
478
|
-
index = close < 0 ? text.length : close + 1;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
if (character === '"') {
|
|
482
|
-
index = skipDoubleQuoted(text, index + 1);
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
if (character === "`") {
|
|
486
|
-
index = readBacktickSubstitution(text, index).end;
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
if (character === "(") depth += 1;
|
|
490
|
-
else if (character === ")") {
|
|
491
|
-
depth -= 1;
|
|
492
|
-
if (depth === 0) return { inner: text.slice(start + 2, index), end: index + 1 };
|
|
493
|
-
}
|
|
494
|
-
index += 1;
|
|
495
|
-
}
|
|
496
|
-
return { inner: text.slice(start + 2), end: text.length };
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/** 따옴표 없는 heredoc 본문에서 셸이 실행하는 `$(…)`·백틱 치환만 꺼낸다. 본문의 따옴표는 그냥 글자다. */
|
|
500
|
-
function heredocExpansions(body: string): string[] {
|
|
501
|
-
const scripts: string[] = [];
|
|
502
|
-
let index = 0;
|
|
503
|
-
while (index < body.length) {
|
|
504
|
-
const character = body[index];
|
|
505
|
-
if (character === "\\") {
|
|
506
|
-
index += 2;
|
|
507
|
-
} else if (character === "$" && body[index + 1] === "(") {
|
|
508
|
-
const substitution = readCommandSubstitution(body, index);
|
|
509
|
-
scripts.push(substitution.inner);
|
|
510
|
-
index = substitution.end;
|
|
511
|
-
} else if (character === "`") {
|
|
512
|
-
const substitution = readBacktickSubstitution(body, index);
|
|
513
|
-
scripts.push(substitution.inner);
|
|
514
|
-
index = substitution.end;
|
|
515
|
-
} else {
|
|
516
|
-
index += 1;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return scripts;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* heredoc 구분자 낱말을 읽는다. 따옴표(`'EOF'`·`"EOF"`·빈 `''`)나 역슬래시(`\EOF`, `E\OF`)가 한 글자라도 있으면 본문은
|
|
524
|
-
* 확장 없이 그대로다. 구분자 값은 따옴표·역슬래시를 벗긴 글자이고, 빈 따옴표는 빈 구분자(빈 줄이 끝)다.
|
|
525
|
-
*/
|
|
526
|
-
function scanHeredocWord(command: string, start: number): { delimiter: string; quoted: boolean; end: number } {
|
|
527
|
-
let index = start;
|
|
528
|
-
let delimiter = "";
|
|
529
|
-
let quoted = false;
|
|
530
|
-
while (index < command.length) {
|
|
531
|
-
const character = command[index];
|
|
532
|
-
if (character === "'" || character === '"') {
|
|
533
|
-
quoted = true;
|
|
534
|
-
const close = command.indexOf(character, index + 1);
|
|
535
|
-
const end = close < 0 ? command.length : close;
|
|
536
|
-
delimiter += command.slice(index + 1, end);
|
|
537
|
-
index = close < 0 ? command.length : close + 1;
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
if (character === "\\" && index + 1 < command.length && command[index + 1] !== "\n") {
|
|
541
|
-
quoted = true;
|
|
542
|
-
delimiter += command[index + 1];
|
|
543
|
-
index += 2;
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
if (/[\s;&|<>()]/.test(character)) break;
|
|
547
|
-
delimiter += character;
|
|
548
|
-
index += 1;
|
|
549
|
-
}
|
|
550
|
-
return { delimiter, quoted, end: index };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/** `<<` 바로 뒤(`-`·공백 다음)의 구분자 낱말이 시작하는 위치와 `<<-` 여부. */
|
|
554
|
-
function heredocWordStart(command: string, start: number): { index: number; stripTabs: boolean } {
|
|
555
|
-
let index = start;
|
|
556
|
-
const stripTabs = command[index] === "-";
|
|
557
|
-
if (stripTabs) index += 1;
|
|
558
|
-
while (command[index] === " " || command[index] === "\t") index += 1;
|
|
559
|
-
return { index, stripTabs };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/** `<<` 바로 뒤의 구분자 낱말을 읽는다. 낱말이 없으면 undefined. */
|
|
563
|
-
function readHeredocDelimiter(command: string, start: number): { heredoc: Heredoc; end: number } | undefined {
|
|
564
|
-
const { index, stripTabs } = heredocWordStart(command, start);
|
|
565
|
-
const word = scanHeredocWord(command, index);
|
|
566
|
-
if (word.end === index) return undefined;
|
|
567
|
-
return { heredoc: { delimiter: word.delimiter, quoted: word.quoted, stripTabs, body: "" }, end: word.end };
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/** `position`(줄 시작)부터 대기 중인 heredoc 본문을 차례로 읽고 다음 위치를 돌려준다. 구분자 줄이 없으면 끝까지가 본문이다. */
|
|
571
|
-
function readHeredocBodies(command: string, position: number, pending: Heredoc[]): number {
|
|
572
|
-
let index = position;
|
|
573
|
-
for (const heredoc of pending) {
|
|
574
|
-
const lines: string[] = [];
|
|
575
|
-
while (index < command.length) {
|
|
576
|
-
const lineEnd = command.indexOf("\n", index);
|
|
577
|
-
const raw = command.slice(index, lineEnd < 0 ? command.length : lineEnd);
|
|
578
|
-
index = lineEnd < 0 ? command.length : lineEnd + 1;
|
|
579
|
-
const line = raw.replace(/\r$/, "");
|
|
580
|
-
if ((heredoc.stripTabs ? line.replace(/^\t+/, "") : line) === heredoc.delimiter) break;
|
|
581
|
-
lines.push(raw);
|
|
582
|
-
}
|
|
583
|
-
heredoc.body = lines.join("\n");
|
|
584
|
-
}
|
|
585
|
-
return index;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function tokenizeShell(command: string): TokenizedShell {
|
|
589
|
-
const segments: ShellSegment[] = [];
|
|
590
|
-
const loose: string[] = [];
|
|
591
|
-
const looseHeredocs: Heredoc[] = [];
|
|
592
|
-
let tokens: ShellToken[] = [];
|
|
593
|
-
let substitutions: string[] = [];
|
|
594
|
-
let heredocs: Heredoc[] = [];
|
|
595
|
-
let pendingBodies: Heredoc[] = [];
|
|
596
|
-
let value = "";
|
|
597
|
-
let tokenStarted = false;
|
|
598
|
-
let tokenQuoted = false;
|
|
599
|
-
let quote: "'" | '"' | undefined;
|
|
600
|
-
let separatorBefore: Separator | undefined;
|
|
601
|
-
let depth = 0;
|
|
602
|
-
|
|
603
|
-
const endToken = (): void => {
|
|
604
|
-
if (!tokenStarted) return;
|
|
605
|
-
tokens.push({ value, quoted: tokenQuoted });
|
|
606
|
-
value = "";
|
|
607
|
-
tokenStarted = false;
|
|
608
|
-
tokenQuoted = false;
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
const hereStrings = (): string[] => {
|
|
612
|
-
const values: string[] = [];
|
|
613
|
-
tokens.forEach((token, position) => {
|
|
614
|
-
if (!token.quoted && /^\d*<<<$/.test(token.value) && tokens[position + 1]) {
|
|
615
|
-
values.push(tokens[position + 1].value);
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
return values;
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
const endSegment = (separator?: Separator): void => {
|
|
622
|
-
endToken();
|
|
623
|
-
let pushed = false;
|
|
624
|
-
if (tokens.length > 0) {
|
|
625
|
-
const executableTokens = stripRedirections(tokens);
|
|
626
|
-
if (executableTokens.length > 0) {
|
|
627
|
-
segments.push({
|
|
628
|
-
tokens: executableTokens,
|
|
629
|
-
separatorBefore,
|
|
630
|
-
depth,
|
|
631
|
-
substitutions,
|
|
632
|
-
heredocs,
|
|
633
|
-
hereStrings: hereStrings(),
|
|
634
|
-
});
|
|
635
|
-
pushed = true;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (!pushed) {
|
|
639
|
-
loose.push(...substitutions);
|
|
640
|
-
looseHeredocs.push(...heredocs);
|
|
641
|
-
}
|
|
642
|
-
tokens = [];
|
|
643
|
-
substitutions = [];
|
|
644
|
-
heredocs = [];
|
|
645
|
-
if (separator !== undefined && (pushed || separatorBefore === undefined)) separatorBefore = separator;
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
/** 단어 안의 치환을 단어 값에 그대로 두고 본문만 따로 모은다. 읽은 마지막 위치를 돌려준다. */
|
|
649
|
-
const takeSubstitution = (index: number): number => {
|
|
650
|
-
const substitution =
|
|
651
|
-
command[index] === "`" ? readBacktickSubstitution(command, index) : readCommandSubstitution(command, index);
|
|
652
|
-
substitutions.push(substitution.inner);
|
|
653
|
-
value += command.slice(index, substitution.end);
|
|
654
|
-
tokenStarted = true;
|
|
655
|
-
return substitution.end - 1;
|
|
656
|
-
};
|
|
657
|
-
|
|
658
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
659
|
-
const character = command[index];
|
|
660
|
-
const next = command[index + 1];
|
|
661
|
-
const afterNext = command[index + 2];
|
|
662
|
-
|
|
663
|
-
if (
|
|
664
|
-
character === "\\" &&
|
|
665
|
-
(next === "\n" || (next === "\r" && afterNext === "\n")) &&
|
|
666
|
-
quote !== "'"
|
|
667
|
-
) {
|
|
668
|
-
index += next === "\r" ? 2 : 1;
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (quote) {
|
|
673
|
-
if (character === quote) {
|
|
674
|
-
quote = undefined;
|
|
675
|
-
tokenQuoted = true;
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (
|
|
680
|
-
quote === '"' &&
|
|
681
|
-
character === "\\" &&
|
|
682
|
-
next !== undefined &&
|
|
683
|
-
(next === '"' || next === "\\" || next === "$" || next === "`")
|
|
684
|
-
) {
|
|
685
|
-
value += next;
|
|
686
|
-
tokenStarted = true;
|
|
687
|
-
index += 1;
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (quote === '"' && (character === "`" || (character === "$" && next === "("))) {
|
|
692
|
-
index = takeSubstitution(index);
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
value += character;
|
|
697
|
-
tokenStarted = true;
|
|
698
|
-
continue;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (character === "'" || character === '"') {
|
|
702
|
-
quote = character;
|
|
703
|
-
tokenStarted = true;
|
|
704
|
-
tokenQuoted = true;
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
if (character === "`" || (character === "$" && next === "(")) {
|
|
709
|
-
index = takeSubstitution(index);
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (character === "\\" && next !== undefined) {
|
|
714
|
-
if (/['";|&#\\()<>]/.test(next)) {
|
|
715
|
-
value += next;
|
|
716
|
-
tokenStarted = true;
|
|
717
|
-
index += 1;
|
|
718
|
-
} else if (next === "$" || next === "`") {
|
|
719
|
-
value += character + next;
|
|
720
|
-
tokenStarted = true;
|
|
721
|
-
index += 1;
|
|
722
|
-
} else {
|
|
723
|
-
value += character;
|
|
724
|
-
tokenStarted = true;
|
|
725
|
-
}
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (character === "#" && !tokenStarted) {
|
|
730
|
-
while (index + 1 < command.length && command[index + 1] !== "\n") {
|
|
731
|
-
index += 1;
|
|
732
|
-
}
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
if (character === "&" && next === ">") {
|
|
736
|
-
endToken();
|
|
737
|
-
let operator = "&>";
|
|
738
|
-
index += 1;
|
|
739
|
-
if (command[index + 1] === ">") {
|
|
740
|
-
operator += ">";
|
|
741
|
-
index += 1;
|
|
742
|
-
}
|
|
743
|
-
tokens.push({ value: operator, quoted: false });
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (character === "<" || character === ">") {
|
|
748
|
-
let descriptor = "";
|
|
749
|
-
if (tokenStarted && !tokenQuoted && /^\d+$/.test(value)) {
|
|
750
|
-
descriptor = value;
|
|
751
|
-
value = "";
|
|
752
|
-
tokenStarted = false;
|
|
753
|
-
tokenQuoted = false;
|
|
754
|
-
} else {
|
|
755
|
-
endToken();
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
let operator = character;
|
|
759
|
-
if (
|
|
760
|
-
(character === ">" && (next === ">" || next === "|" || next === "&")) ||
|
|
761
|
-
(character === "<" && (next === "<" || next === ">" || next === "&"))
|
|
762
|
-
) {
|
|
763
|
-
operator += next;
|
|
764
|
-
index += 1;
|
|
765
|
-
if (operator === "<<" && command[index + 1] === "<") {
|
|
766
|
-
operator += "<";
|
|
767
|
-
index += 1;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
tokens.push({ value: `${descriptor}${operator}`, quoted: false });
|
|
771
|
-
if (operator === "<<") {
|
|
772
|
-
const delimiter = readHeredocDelimiter(command, index + 1);
|
|
773
|
-
if (delimiter) {
|
|
774
|
-
index = delimiter.end - 1;
|
|
775
|
-
tokens.push({ value: delimiter.heredoc.delimiter, quoted: true });
|
|
776
|
-
heredocs.push(delimiter.heredoc);
|
|
777
|
-
pendingBodies.push(delimiter.heredoc);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
continue;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
if (character === "(" || character === ")") {
|
|
784
|
-
endSegment(";");
|
|
785
|
-
depth = character === "(" ? depth + 1 : Math.max(0, depth - 1);
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (character === "\n") {
|
|
791
|
-
endSegment("\n");
|
|
792
|
-
if (pendingBodies.length > 0) {
|
|
793
|
-
index = readHeredocBodies(command, index + 1, pendingBodies) - 1;
|
|
794
|
-
pendingBodies = [];
|
|
795
|
-
}
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
if (character === "\r") {
|
|
800
|
-
continue;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (/\s/.test(character)) {
|
|
804
|
-
endToken();
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (character === ";") {
|
|
809
|
-
endSegment(";");
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (character === "|" || character === "&") {
|
|
814
|
-
if (next === character) {
|
|
815
|
-
endSegment(character === "|" ? "||" : "&&");
|
|
816
|
-
index += 1;
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
endSegment(character === "|" ? "|" : "&");
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
value += character;
|
|
825
|
-
tokenStarted = true;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
endSegment();
|
|
829
|
-
for (const heredoc of looseHeredocs) {
|
|
830
|
-
if (!heredoc.quoted) loose.push(...heredocExpansions(heredoc.body));
|
|
831
|
-
}
|
|
832
|
-
return { segments, loose };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function commandName(value: string): string {
|
|
836
|
-
const normalized = value.replace(/\\/g, "/");
|
|
837
|
-
const basename = normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
838
|
-
return basename.replace(/\.(?:exe|cmd|bat)$/i, "");
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function isEnvironmentAssignment(value: string): boolean {
|
|
842
|
-
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function skipWrapperArguments(tokens: ShellToken[], start: number, spec: WrapperSpec): number {
|
|
846
|
-
let index = start;
|
|
847
|
-
while (index < tokens.length) {
|
|
848
|
-
const value = tokens[index].value;
|
|
849
|
-
if (value === "--") {
|
|
850
|
-
index += 1;
|
|
851
|
-
break;
|
|
852
|
-
}
|
|
853
|
-
if (!value.startsWith("-") || value === "-") break;
|
|
854
|
-
const equalsIndex = value.indexOf("=");
|
|
855
|
-
const option = equalsIndex >= 0 ? value.slice(0, equalsIndex) : value;
|
|
856
|
-
index += equalsIndex < 0 && spec.optionsWithValue[option] ? 2 : 1;
|
|
857
|
-
}
|
|
858
|
-
return index + spec.positionals;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function resolveInvocation(tokens: ShellToken[]): Invocation | undefined {
|
|
862
|
-
let index = 0;
|
|
863
|
-
|
|
864
|
-
while (index < tokens.length && isEnvironmentAssignment(tokens[index].value)) index += 1;
|
|
865
|
-
|
|
866
|
-
while (index < tokens.length) {
|
|
867
|
-
const name = commandName(tokens[index].value);
|
|
868
|
-
|
|
869
|
-
if (INVOCATION_PREFIXES[tokens[index].value]) {
|
|
870
|
-
index += 1;
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
if (name === "command") {
|
|
875
|
-
const wrapperIndex = index;
|
|
876
|
-
index += 1;
|
|
877
|
-
while (index < tokens.length) {
|
|
878
|
-
const option = tokens[index].value;
|
|
879
|
-
if (option === "-v" || option === "-V") return { index: wrapperIndex, name };
|
|
880
|
-
if (option === "-p" || option === "--") {
|
|
881
|
-
index += 1;
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
break;
|
|
885
|
-
}
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
if (name === "nohup") {
|
|
890
|
-
const wrapperIndex = index;
|
|
891
|
-
index += 1;
|
|
892
|
-
if (
|
|
893
|
-
tokens[index]?.value === "--help" ||
|
|
894
|
-
tokens[index]?.value === "--version"
|
|
895
|
-
) {
|
|
896
|
-
return { index: wrapperIndex, name };
|
|
897
|
-
}
|
|
898
|
-
if (tokens[index]?.value === "--") index += 1;
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (name === "time") {
|
|
903
|
-
const wrapperIndex = index;
|
|
904
|
-
index += 1;
|
|
905
|
-
while (index < tokens.length) {
|
|
906
|
-
const value = tokens[index].value;
|
|
907
|
-
if (value === "--") {
|
|
908
|
-
index += 1;
|
|
909
|
-
break;
|
|
910
|
-
}
|
|
911
|
-
if (value === "--help" || value === "--version") {
|
|
912
|
-
return { index: wrapperIndex, name };
|
|
913
|
-
}
|
|
914
|
-
if (!value.startsWith("-")) break;
|
|
915
|
-
const option = value.includes("=") ? value.slice(0, value.indexOf("=")) : value;
|
|
916
|
-
if (!value.includes("=") && TIME_OPTIONS_WITH_VALUE[option]) index += 2;
|
|
917
|
-
else index += 1;
|
|
918
|
-
}
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
if (name === "timeout") {
|
|
923
|
-
const wrapperIndex = index;
|
|
924
|
-
index += 1;
|
|
925
|
-
while (index < tokens.length) {
|
|
926
|
-
const value = tokens[index].value;
|
|
927
|
-
if (value === "--") {
|
|
928
|
-
index += 1;
|
|
929
|
-
break;
|
|
930
|
-
}
|
|
931
|
-
if (value === "--help" || value === "--version") {
|
|
932
|
-
return { index: wrapperIndex, name };
|
|
933
|
-
}
|
|
934
|
-
if (!value.startsWith("-")) break;
|
|
935
|
-
const option = value.includes("=") ? value.slice(0, value.indexOf("=")) : value;
|
|
936
|
-
if (!value.includes("=") && TIMEOUT_OPTIONS_WITH_VALUE[option]) index += 2;
|
|
937
|
-
else index += 1;
|
|
938
|
-
}
|
|
939
|
-
if (index >= tokens.length) return undefined;
|
|
940
|
-
index += 1;
|
|
941
|
-
if (tokens[index]?.value === "--") index += 1;
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
if (COMMAND_WRAPPERS[name]) {
|
|
946
|
-
index += 1;
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if (name === "env") {
|
|
951
|
-
index += 1;
|
|
952
|
-
while (index < tokens.length) {
|
|
953
|
-
const value = tokens[index].value;
|
|
954
|
-
if (isEnvironmentAssignment(value)) {
|
|
955
|
-
index += 1;
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
if (ENV_OPTIONS_WITH_VALUE[value]) {
|
|
959
|
-
index += 2;
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
if (value.startsWith("-")) {
|
|
963
|
-
index += 1;
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
break;
|
|
967
|
-
}
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (name === "sudo") {
|
|
972
|
-
index += 1;
|
|
973
|
-
while (index < tokens.length && tokens[index].value.startsWith("-")) {
|
|
974
|
-
const option = tokens[index].value;
|
|
975
|
-
if (SUDO_OPTIONS_WITH_VALUE[option]) {
|
|
976
|
-
index += 2;
|
|
977
|
-
} else {
|
|
978
|
-
index += 1;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
continue;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
const wrapper = Object.hasOwn(EXEC_WRAPPERS, name) ? EXEC_WRAPPERS[name] : undefined;
|
|
985
|
-
if (wrapper) {
|
|
986
|
-
const wrapperIndex = index;
|
|
987
|
-
index = skipWrapperArguments(tokens, index + 1, wrapper);
|
|
988
|
-
// `flock <lockfile> -c '<명령>'`은 감싼 명령이 따로 없고 문자열을 셸이 실행한다. 그 문자열은 호출한 쪽이 푼다.
|
|
989
|
-
if (name === "flock" && /^(?:-c|--command)$/.test(tokens[index]?.value ?? "")) {
|
|
990
|
-
return { index: wrapperIndex, name };
|
|
991
|
-
}
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
return { index, name };
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
return undefined;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
function positionalArguments(
|
|
1002
|
-
args: string[],
|
|
1003
|
-
optionsWithValue: StringLookup = CLI_OPTIONS_WITH_VALUE,
|
|
1004
|
-
): string[] {
|
|
1005
|
-
const positionals: string[] = [];
|
|
1006
|
-
let parseOptions = true;
|
|
1007
|
-
|
|
1008
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1009
|
-
const value = args[index];
|
|
1010
|
-
|
|
1011
|
-
if (parseOptions && value === "--") {
|
|
1012
|
-
parseOptions = false;
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
if (parseOptions && value.startsWith("-")) {
|
|
1017
|
-
const option = value.includes("=") ? value.slice(0, value.indexOf("=")) : value;
|
|
1018
|
-
if (!value.includes("=") && optionsWithValue[option]) index += 1;
|
|
1019
|
-
continue;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
positionals.push(value);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
return positionals;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/** git 하위 명령별로 짧은 옵션이 값을 받는 글자. 그 글자 뒤는 값이지 옵션 묶음이 아니다(`-enode_modules`, `-sSTABLE`). */
|
|
1029
|
-
const GIT_SHORT_VALUE_OPTIONS: Readonly<Record<string, string>> = {
|
|
1030
|
-
branch: "u",
|
|
1031
|
-
checkout: "bBU",
|
|
1032
|
-
clean: "e",
|
|
1033
|
-
commit: "mFCct",
|
|
1034
|
-
push: "o",
|
|
1035
|
-
restore: "sU",
|
|
1036
|
-
switch: "cC",
|
|
1037
|
-
tag: "mFu",
|
|
1038
|
-
};
|
|
1039
|
-
|
|
1040
|
-
/** git 하위 명령별로 `--name value`처럼 다음 낱말을 값으로 받는 긴 옵션(`--name=value`는 값이 붙어 있어 해당 없음). */
|
|
1041
|
-
const GIT_LONG_VALUE_OPTIONS: Readonly<Record<string, readonly string[]>> = {
|
|
1042
|
-
add: ["--pathspec-from-file"],
|
|
1043
|
-
branch: ["--format", "--points-at", "--set-upstream-to", "--sort"],
|
|
1044
|
-
checkout: ["--conflict", "--inter-hunk-context", "--orphan", "--pathspec-from-file", "--unified"],
|
|
1045
|
-
clean: ["--exclude"],
|
|
1046
|
-
commit: [
|
|
1047
|
-
"--author", "--cleanup", "--date", "--file", "--fixup", "--message", "--pathspec-from-file",
|
|
1048
|
-
"--reedit-message", "--reuse-message", "--squash", "--template", "--trailer",
|
|
1049
|
-
],
|
|
1050
|
-
push: ["--exec", "--push-option", "--receive-pack", "--repo"],
|
|
1051
|
-
restore: ["--conflict", "--inter-hunk-context", "--pathspec-from-file", "--source", "--unified"],
|
|
1052
|
-
rm: ["--pathspec-from-file"],
|
|
1053
|
-
switch: ["--conflict", "--create", "--force-create", "--orphan"],
|
|
1054
|
-
tag: ["--cleanup", "--file", "--format", "--local-user", "--message", "--points-at", "--sort"],
|
|
1055
|
-
};
|
|
1056
|
-
|
|
1057
|
-
/**
|
|
1058
|
-
* 하위 명령 인자에서 옵션이 받는 값을 걷어 낸다. 값은 옵션처럼 보여도(`--exclude -n`, `-m --dry-run`, `-sSTABLE`의 `S`) 옵션이
|
|
1059
|
-
* 아니므로, 이 뒤의 `shortFlags`·`hasLongOption`이 값을 플래그로 읽지 못하게 한다. 값을 받는 짧은 옵션 글자 앞까지만 남기고,
|
|
1060
|
-
* `--` 뒤는 그대로 둔다. 긴 옵션은 git처럼 모호하지 않은 줄임말(`--exc`)도 같은 옵션으로 본다.
|
|
1061
|
-
*/
|
|
1062
|
-
function withoutOptionValues(subcommand: string, args: string[]): string[] {
|
|
1063
|
-
const shortValues = Object.hasOwn(GIT_SHORT_VALUE_OPTIONS, subcommand) ? GIT_SHORT_VALUE_OPTIONS[subcommand] : "";
|
|
1064
|
-
const longValues = Object.hasOwn(GIT_LONG_VALUE_OPTIONS, subcommand) ? GIT_LONG_VALUE_OPTIONS[subcommand] : [];
|
|
1065
|
-
const kept: string[] = [];
|
|
1066
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1067
|
-
const value = args[index];
|
|
1068
|
-
if (value === "--") return [...kept, ...args.slice(index)];
|
|
1069
|
-
if (value.startsWith("--")) {
|
|
1070
|
-
kept.push(value);
|
|
1071
|
-
const takesNext =
|
|
1072
|
-
!value.includes("=") && longValues.some((name) => name === value || (value.length >= 4 && name.startsWith(value)));
|
|
1073
|
-
if (takesNext) index += 1;
|
|
1074
|
-
continue;
|
|
1075
|
-
}
|
|
1076
|
-
if (/^-[^-]/.test(value)) {
|
|
1077
|
-
const cut = [...value.slice(1)].findIndex((letter) => shortValues.includes(letter));
|
|
1078
|
-
if (cut < 0) {
|
|
1079
|
-
kept.push(value);
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1082
|
-
if (cut > 0) kept.push(value.slice(0, cut + 1));
|
|
1083
|
-
if (cut === value.length - 2) index += 1;
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
kept.push(value);
|
|
1087
|
-
}
|
|
1088
|
-
return kept;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
function parseGitInvocation(tokens: ShellToken[], invocation: Invocation): GitInvocation | undefined {
|
|
1092
|
-
if (invocation.name !== "git") return undefined;
|
|
1093
|
-
|
|
1094
|
-
const values = tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1095
|
-
let index = 0;
|
|
1096
|
-
|
|
1097
|
-
while (index < values.length) {
|
|
1098
|
-
const value = values[index];
|
|
1099
|
-
if (value === "--") {
|
|
1100
|
-
index += 1;
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
if (!value.startsWith("-")) break;
|
|
1104
|
-
|
|
1105
|
-
const equalsIndex = value.indexOf("=");
|
|
1106
|
-
const option = equalsIndex >= 0 ? value.slice(0, equalsIndex) : value;
|
|
1107
|
-
const hasAttachedShortValue =
|
|
1108
|
-
(value.startsWith("-c") || value.startsWith("-C")) && value.length > 2;
|
|
1109
|
-
|
|
1110
|
-
if (
|
|
1111
|
-
equalsIndex < 0 &&
|
|
1112
|
-
!hasAttachedShortValue &&
|
|
1113
|
-
GIT_GLOBAL_OPTIONS_WITH_VALUE[option]
|
|
1114
|
-
) {
|
|
1115
|
-
index += 2;
|
|
1116
|
-
} else {
|
|
1117
|
-
index += 1;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (index >= values.length) return undefined;
|
|
1122
|
-
const subcommand = values[index].toLowerCase();
|
|
1123
|
-
return {
|
|
1124
|
-
subcommand,
|
|
1125
|
-
args: withoutOptionValues(subcommand, values.slice(index + 1)),
|
|
1126
|
-
globalArgs: values.slice(0, index),
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// `~`는 단어 맨 앞이나 assignment 꼴의 `=`·`:` 뒤에서만 셸이 확장한다. 경로 중간의 literal
|
|
1131
|
-
// `~`(8.3 짧은 이름 `RUNNER~1` 등)는 확장이 아니므로 저장소 판정으로 넘긴다.
|
|
1132
|
-
function hasShellExpansion(value: string): boolean {
|
|
1133
|
-
return /[$%`*?]|(?:^|[=:])~/.test(value);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* git이 실제로 여는 위치. `cwd`가 null이면(앞선 cd를 확정할 수 없음) 절대경로 `-C`·`--git-dir`만으로 정해질 때만
|
|
1138
|
-
* 값을 돌려주고, 그 밖에는 undefined다. undefined `cwd`는 현재 프로세스 위치(".")다.
|
|
1139
|
-
*/
|
|
1140
|
-
function gitTargetDirectory(
|
|
1141
|
-
git: GitInvocation,
|
|
1142
|
-
cwd: string | null | undefined,
|
|
1143
|
-
): string | undefined {
|
|
1144
|
-
let directory = cwd === null ? undefined : resolve(cwd ?? ".");
|
|
1145
|
-
let gitDirectoryPath: string | undefined;
|
|
1146
|
-
|
|
1147
|
-
for (let index = 0; index < git.globalArgs.length; index += 1) {
|
|
1148
|
-
const value = git.globalArgs[index];
|
|
1149
|
-
if (value === "--") break;
|
|
1150
|
-
|
|
1151
|
-
if (value === "-C" || value === "--git-dir") {
|
|
1152
|
-
const path = git.globalArgs[index + 1];
|
|
1153
|
-
if (path === undefined || hasShellExpansion(path)) return undefined;
|
|
1154
|
-
if (value === "-C") {
|
|
1155
|
-
directory = isAbsolute(path) ? resolve(path) : directory === undefined ? undefined : resolve(directory, path);
|
|
1156
|
-
} else gitDirectoryPath = path;
|
|
1157
|
-
index += 1;
|
|
1158
|
-
continue;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
if (value.startsWith("-C") && value.length > 2) {
|
|
1162
|
-
const path = value.slice(2);
|
|
1163
|
-
if (hasShellExpansion(path)) return undefined;
|
|
1164
|
-
directory = isAbsolute(path) ? resolve(path) : directory === undefined ? undefined : resolve(directory, path);
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
if (value.startsWith("--git-dir=")) {
|
|
1169
|
-
const path = value.slice("--git-dir=".length);
|
|
1170
|
-
if (hasShellExpansion(path)) return undefined;
|
|
1171
|
-
gitDirectoryPath = path;
|
|
1172
|
-
continue;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
const equalsIndex = value.indexOf("=");
|
|
1176
|
-
const option = equalsIndex >= 0 ? value.slice(0, equalsIndex) : value;
|
|
1177
|
-
const hasAttachedShortValue =
|
|
1178
|
-
(value.startsWith("-c") || value.startsWith("-C")) && value.length > 2;
|
|
1179
|
-
if (
|
|
1180
|
-
equalsIndex < 0 &&
|
|
1181
|
-
!hasAttachedShortValue &&
|
|
1182
|
-
GIT_GLOBAL_OPTIONS_WITH_VALUE[option]
|
|
1183
|
-
) {
|
|
1184
|
-
index += 1;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
if (gitDirectoryPath === undefined) return directory;
|
|
1189
|
-
if (isAbsolute(gitDirectoryPath)) return resolve(gitDirectoryPath);
|
|
1190
|
-
return directory === undefined ? undefined : resolve(directory, gitDirectoryPath);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
function hasLongOption(
|
|
1194
|
-
args: string[],
|
|
1195
|
-
names: string[],
|
|
1196
|
-
minimumPrefixLength = 4,
|
|
1197
|
-
): boolean {
|
|
1198
|
-
for (const value of args) {
|
|
1199
|
-
if (value === "--") return false;
|
|
1200
|
-
const normalized = value.toLowerCase();
|
|
1201
|
-
const equalsIndex = normalized.indexOf("=");
|
|
1202
|
-
const option = equalsIndex >= 0 ? normalized.slice(0, equalsIndex) : normalized;
|
|
1203
|
-
if (!option.startsWith("--")) continue;
|
|
1204
|
-
for (const name of names) {
|
|
1205
|
-
if (
|
|
1206
|
-
option === name ||
|
|
1207
|
-
(option.length >= minimumPrefixLength && name.startsWith(option))
|
|
1208
|
-
) {
|
|
1209
|
-
return true;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
return false;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function shortFlags(args: string[]): Set<string> {
|
|
1217
|
-
const flags = new Set<string>();
|
|
1218
|
-
for (const value of args) {
|
|
1219
|
-
if (value === "--") break;
|
|
1220
|
-
if (/^-[^-]/.test(value)) {
|
|
1221
|
-
for (const flag of value.slice(1)) flags.add(flag);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
return flags;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
/** `git checkout .`·`git restore :/`처럼 작업 트리 전체를 가리키는 pathspec. */
|
|
1228
|
-
function isWorktreeWidePathspec(value: string): boolean {
|
|
1229
|
-
return isBroadPath(value) || /^:(?:\/\*?|\(top\)[.*]?)$/i.test(value.trim());
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
/**
|
|
1233
|
-
* 작업 트리·stash의 변경을 되돌릴 수 없게 버리는 git 명령. 파일 하나를 지정한 복원, 브랜치 전환, `restore --staged`처럼
|
|
1234
|
-
* index만 바꾸는 명령, 새 stash 저장·적용은 막지 않는다.
|
|
1235
|
-
*/
|
|
1236
|
-
function matchWorkDiscard(git: GitInvocation): string | undefined {
|
|
1237
|
-
const flags = shortFlags(git.args);
|
|
1238
|
-
const force = flags.has("f") || hasLongOption(git.args, ["--force"], 3);
|
|
1239
|
-
const action = git.args.find((value) => !value.startsWith("-"))?.toLowerCase();
|
|
1240
|
-
|
|
1241
|
-
switch (git.subcommand) {
|
|
1242
|
-
case "checkout":
|
|
1243
|
-
return force || positionalArguments(git.args, {}).some(isWorktreeWidePathspec)
|
|
1244
|
-
? REASONS.gitDiscard
|
|
1245
|
-
: undefined;
|
|
1246
|
-
case "switch":
|
|
1247
|
-
return force || hasLongOption(git.args, ["--discard-changes"], 4) ? REASONS.gitDiscard : undefined;
|
|
1248
|
-
case "restore": {
|
|
1249
|
-
const staged = flags.has("S") || hasLongOption(git.args, ["--staged"], 4);
|
|
1250
|
-
const worktree = flags.has("W") || hasLongOption(git.args, ["--worktree"], 4);
|
|
1251
|
-
const wide = positionalArguments(git.args, {}).some(isWorktreeWidePathspec);
|
|
1252
|
-
return wide && (worktree || !staged) ? REASONS.gitDiscard : undefined;
|
|
1253
|
-
}
|
|
1254
|
-
case "stash":
|
|
1255
|
-
return action === "clear" || action === "drop" ? REASONS.gitDiscard : undefined;
|
|
1256
|
-
case "worktree":
|
|
1257
|
-
return action === "remove" && force ? REASONS.gitDiscard : undefined;
|
|
1258
|
-
default:
|
|
1259
|
-
return undefined;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
function matchGit(
|
|
1264
|
-
tokens: ShellToken[],
|
|
1265
|
-
invocation: Invocation,
|
|
1266
|
-
context?: GuardContext,
|
|
1267
|
-
): string | undefined {
|
|
1268
|
-
const git = parseGitInvocation(tokens, invocation);
|
|
1269
|
-
if (!git) return undefined;
|
|
1270
|
-
|
|
1271
|
-
if (FINALIZER_OWNED_GIT_COMMANDS[git.subcommand]) {
|
|
1272
|
-
// 미리보기는 index·ref·원격을 바꾸지 않는다. commit의 `-n`은 `--no-verify`라 commit은 long option만 본다.
|
|
1273
|
-
const dryRun =
|
|
1274
|
-
(git.subcommand === "add" || git.subcommand === "commit" || git.subcommand === "push") &&
|
|
1275
|
-
(hasLongOption(git.args, ["--dry-run"]) ||
|
|
1276
|
-
(git.subcommand !== "commit" && shortFlags(git.args).has("n")));
|
|
1277
|
-
if (dryRun) return undefined;
|
|
1278
|
-
if (!context?.isSessionRepository) return REASONS.gitFinalize;
|
|
1279
|
-
const targetDirectory = gitTargetDirectory(git, context.cwd);
|
|
1280
|
-
if (!targetDirectory) return REASONS.gitFinalize;
|
|
1281
|
-
try {
|
|
1282
|
-
return context.isSessionRepository(targetDirectory)
|
|
1283
|
-
? REASONS.gitFinalize
|
|
1284
|
-
: undefined;
|
|
1285
|
-
} catch {
|
|
1286
|
-
return REASONS.gitFinalize;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
if (git.subcommand === "reset" && hasLongOption(git.args, ["--hard"], 3)) {
|
|
1291
|
-
return REASONS.gitReset;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
if (git.subcommand === "clean") {
|
|
1295
|
-
const flags = shortFlags(git.args);
|
|
1296
|
-
const force = flags.has("f") || hasLongOption(git.args, ["--force"], 3);
|
|
1297
|
-
// `-n`·`--dry-run`은 지울 목록만 보여 주므로 `-f`가 함께 있어도 아무것도 지우지 않는다.
|
|
1298
|
-
const dryRun = flags.has("n") || hasLongOption(git.args, ["--dry-run"], 3);
|
|
1299
|
-
if (force && !dryRun) return REASONS.gitClean;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
const discardReason = matchWorkDiscard(git);
|
|
1303
|
-
if (discardReason) return discardReason;
|
|
1304
|
-
|
|
1305
|
-
if (git.subcommand === "rm") {
|
|
1306
|
-
const flags = shortFlags(git.args);
|
|
1307
|
-
const paths = positionalArguments(git.args);
|
|
1308
|
-
const recursive = flags.has("r") || flags.has("R") || hasLongOption(git.args, ["--recursive"], 3);
|
|
1309
|
-
const force = flags.has("f") || hasLongOption(git.args, ["--force"], 3);
|
|
1310
|
-
if (recursive && force && paths.some(isBroadPath)) return REASONS.broadRemove;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
if (git.subcommand === "branch") {
|
|
1315
|
-
const flags = shortFlags(git.args);
|
|
1316
|
-
const forcedShortDelete = flags.has("D") || (flags.has("d") && flags.has("f"));
|
|
1317
|
-
const forcedLongDelete =
|
|
1318
|
-
hasLongOption(git.args, ["--delete"], 3) && hasLongOption(git.args, ["--force"], 3);
|
|
1319
|
-
if (forcedShortDelete || forcedLongDelete) return REASONS.gitDelete;
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
if (git.subcommand === "tag") {
|
|
1323
|
-
const flags = shortFlags(git.args);
|
|
1324
|
-
if (flags.has("d") || hasLongOption(git.args, ["--delete"], 3)) return REASONS.gitDelete;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
return undefined;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
/**
|
|
1331
|
-
* WSL에서 하네스가 Windows `git.exe`로 다루는 저장소(`/mnt/<드라이브>`)를 Linux git으로 여는 호출을 막는다.
|
|
1332
|
-
* 두 git은 index에 적는 stat 정보가 달라, 번갈아 쓰면 바뀐 쪽이 추적 파일 전체를 다시 확인하고 index를 통째로 다시 쓴다.
|
|
1333
|
-
* 2026-10-12 SJF 저장소(추적 217,935개)에서 Linux `git --no-optional-locks diff`가 그 갱신 도중 bash 30초 제한에 끊겨
|
|
1334
|
-
* `index.lock`을 남겼다. `--no-optional-locks`는 diff의 index 갱신을 막지 못한다(git 2.43 재현).
|
|
1335
|
-
* `windowsShell`은 Windows PowerShell(`*.exe`) 안의 호출이라 그 안의 `git`이 이미 Windows git이라는 뜻이다.
|
|
1336
|
-
* 앞선 cd를 확정할 수 없어도(`cwd` null) 절대경로 `-C`·`--git-dir`이면 판정하고, 그 밖에 위치를 모르면 판정하지 않는다.
|
|
1337
|
-
*/
|
|
1338
|
-
function matchLinuxGitOnWindowsDrive(
|
|
1339
|
-
tokens: ShellToken[],
|
|
1340
|
-
invocation: Invocation,
|
|
1341
|
-
context: GuardContext | undefined,
|
|
1342
|
-
cwd: string | null | undefined,
|
|
1343
|
-
windowsShell: boolean,
|
|
1344
|
-
): string | undefined {
|
|
1345
|
-
if (windowsShell || !context?.usesWindowsGit) return undefined;
|
|
1346
|
-
// commandName은 `.exe`를 떼므로 원래 토큰으로 Windows git을 구분한다.
|
|
1347
|
-
if (/\.exe$/i.test(tokens[invocation.index].value)) return undefined;
|
|
1348
|
-
const git = parseGitInvocation(tokens, invocation);
|
|
1349
|
-
if (!git || REPOSITORY_FREE_GIT_COMMANDS[git.subcommand]) return undefined;
|
|
1350
|
-
const directory = gitTargetDirectory(git, cwd);
|
|
1351
|
-
return directory !== undefined && context.usesWindowsGit(directory)
|
|
1352
|
-
? REASONS.linuxGitOnWindowsDrive
|
|
1353
|
-
: undefined;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
function isBroadPath(value: string): boolean {
|
|
1357
|
-
const normalized = value.trim().replace(/\\/g, "/");
|
|
1358
|
-
const upper = normalized.toUpperCase();
|
|
1359
|
-
|
|
1360
|
-
if (BROAD_PATHS[normalized]) return true;
|
|
1361
|
-
if (BROAD_ENVIRONMENT_PATHS[upper]) return true;
|
|
1362
|
-
|
|
1363
|
-
if (/^[A-Za-z]:\/(?:\*|\.\*|\*\*)?$/.test(normalized)) return true;
|
|
1364
|
-
return /^\/\/[^/]+\/[^/]+\/?(?:\*)?$/.test(normalized);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
function matchRm(args: string[]): string | undefined {
|
|
1368
|
-
let recursive = false;
|
|
1369
|
-
let force = false;
|
|
1370
|
-
const targets: string[] = [];
|
|
1371
|
-
let parseOptions = true;
|
|
1372
|
-
|
|
1373
|
-
for (const value of args) {
|
|
1374
|
-
if (parseOptions && value === "--") {
|
|
1375
|
-
parseOptions = false;
|
|
1376
|
-
continue;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
if (parseOptions && value.startsWith("--")) {
|
|
1380
|
-
const option = value.toLowerCase();
|
|
1381
|
-
if (option.length >= 3 && "--recursive".startsWith(option)) recursive = true;
|
|
1382
|
-
if (option.length >= 3 && "--force".startsWith(option)) force = true;
|
|
1383
|
-
continue;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
if (parseOptions && /^-[^-]/.test(value)) {
|
|
1387
|
-
const flags = value.slice(1);
|
|
1388
|
-
if (flags.includes("r") || flags.includes("R")) recursive = true;
|
|
1389
|
-
if (flags.includes("f")) force = true;
|
|
1390
|
-
continue;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
targets.push(value);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
return recursive && force && targets.some(isBroadPath) ? REASONS.broadRemove : undefined;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
function matchRemoveItem(args: string[]): string | undefined {
|
|
1400
|
-
let recursive = false;
|
|
1401
|
-
let force = false;
|
|
1402
|
-
const targets: string[] = [];
|
|
1403
|
-
|
|
1404
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1405
|
-
const value = args[index];
|
|
1406
|
-
const option = value.toLowerCase().replace(/:(?:\$?true)$/i, "");
|
|
1407
|
-
|
|
1408
|
-
if (option === "/s") {
|
|
1409
|
-
recursive = true;
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
if (option === "/q" || option === "/f") {
|
|
1413
|
-
force = true;
|
|
1414
|
-
continue;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
if (
|
|
1418
|
-
option === "-r" ||
|
|
1419
|
-
(option.length >= 3 && "-recurse".startsWith(option))
|
|
1420
|
-
) {
|
|
1421
|
-
recursive = true;
|
|
1422
|
-
continue;
|
|
1423
|
-
}
|
|
1424
|
-
if (option === "-fo" || (option.length >= 3 && "-force".startsWith(option))) {
|
|
1425
|
-
force = true;
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
if (REMOVE_ITEM_PATH_OPTIONS[option]) {
|
|
1429
|
-
if (index + 1 < args.length) targets.push(args[(index += 1)]);
|
|
1430
|
-
continue;
|
|
1431
|
-
}
|
|
1432
|
-
if (REMOVE_ITEM_OPTIONS_WITH_VALUE[option]) {
|
|
1433
|
-
index += 1;
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
if (!value.startsWith("-")) targets.push(value);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
const broadTarget = targets.some((target) =>
|
|
1440
|
-
target
|
|
1441
|
-
.split(",")
|
|
1442
|
-
.map((part) => part.trim())
|
|
1443
|
-
.some(isBroadPath),
|
|
1444
|
-
);
|
|
1445
|
-
return recursive && force && broadTarget ? REASONS.broadRemove : undefined;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
function stripSqlLiteralsAndComments(sql: string): string {
|
|
1449
|
-
let result = "";
|
|
1450
|
-
let state: "code" | "single" | "double" | "bracket" | "backtick" | "line" | "block" =
|
|
1451
|
-
"code";
|
|
1452
|
-
|
|
1453
|
-
for (let index = 0; index < sql.length; index += 1) {
|
|
1454
|
-
const character = sql[index];
|
|
1455
|
-
const next = sql[index + 1];
|
|
1456
|
-
|
|
1457
|
-
if (state === "line") {
|
|
1458
|
-
if (character === "\n") {
|
|
1459
|
-
result += "\n";
|
|
1460
|
-
state = "code";
|
|
1461
|
-
}
|
|
1462
|
-
continue;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
if (state === "block") {
|
|
1466
|
-
if (character === "*" && next === "/") {
|
|
1467
|
-
result += " ";
|
|
1468
|
-
state = "code";
|
|
1469
|
-
index += 1;
|
|
1470
|
-
}
|
|
1471
|
-
continue;
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
if (state !== "code") {
|
|
1475
|
-
const closer = state === "single" ? "'" : state === "double" ? '"' : state === "bracket" ? "]" : "`";
|
|
1476
|
-
if (character === closer) {
|
|
1477
|
-
if ((state === "single" || state === "double") && next === closer) {
|
|
1478
|
-
index += 1;
|
|
1479
|
-
} else {
|
|
1480
|
-
result += " ";
|
|
1481
|
-
state = "code";
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
continue;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
if (character === "-" && next === "-") {
|
|
1488
|
-
state = "line";
|
|
1489
|
-
index += 1;
|
|
1490
|
-
continue;
|
|
1491
|
-
}
|
|
1492
|
-
if (character === "/" && next === "*") {
|
|
1493
|
-
state = "block";
|
|
1494
|
-
index += 1;
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
1497
|
-
if (character === "'") state = "single";
|
|
1498
|
-
else if (character === '"') state = "double";
|
|
1499
|
-
else if (character === "[") state = "bracket";
|
|
1500
|
-
else if (character === "`") state = "backtick";
|
|
1501
|
-
else result += character;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
return result;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function containsDestructiveSql(sql: string): boolean {
|
|
1508
|
-
const executableSql = stripSqlLiteralsAndComments(sql);
|
|
1509
|
-
return /(?:^|[;\r\n])\s*(?:drop|truncate)\b/i.test(executableSql);
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
function queryOptionValues(
|
|
1513
|
-
args: string[],
|
|
1514
|
-
shortOptions: string[],
|
|
1515
|
-
longOptions: string[],
|
|
1516
|
-
): string[] {
|
|
1517
|
-
const queries: string[] = [];
|
|
1518
|
-
const normalizedShort = shortOptions.map((option) => option.toLowerCase());
|
|
1519
|
-
const normalizedLong = longOptions.map((option) => option.toLowerCase());
|
|
1520
|
-
|
|
1521
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1522
|
-
const value = args[index];
|
|
1523
|
-
const lower = value.toLowerCase();
|
|
1524
|
-
|
|
1525
|
-
if (normalizedShort.includes(lower) || normalizedLong.includes(lower)) {
|
|
1526
|
-
if (index + 1 < args.length) queries.push(args[(index += 1)]);
|
|
1527
|
-
continue;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
const long = normalizedLong.find((option) => lower.startsWith(`${option}=`));
|
|
1531
|
-
if (long) {
|
|
1532
|
-
queries.push(value.slice(long.length + 1));
|
|
1533
|
-
continue;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
const short = normalizedShort.find(
|
|
1537
|
-
(option) => lower.startsWith(option) && value.length > option.length,
|
|
1538
|
-
);
|
|
1539
|
-
if (short) queries.push(value.slice(short.length));
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
return queries;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
/**
|
|
1546
|
-
* 짧은 옵션 묶음(`psql -Atc`, `mysql -Nse`, `-1c"…"`)을 앞에서부터 풀어 SQL을 받는 옵션의 값을 모은다. 값을 받는 다른
|
|
1547
|
-
* 옵션을 만나면 그 묶음의 해석을 멈추고, 값이 따로 떨어져 있으면 그 값은 건너뛴다. 글자 구분은 대소문자를 지킨다.
|
|
1548
|
-
*/
|
|
1549
|
-
function shortOptionQueries(args: string[], options: ShortQueryOptions): string[] {
|
|
1550
|
-
const queries: string[] = [];
|
|
1551
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1552
|
-
const value = args[index];
|
|
1553
|
-
if (!/^-[^-]/.test(value)) continue;
|
|
1554
|
-
for (let position = 1; position < value.length; position += 1) {
|
|
1555
|
-
const flag = value[position];
|
|
1556
|
-
if (options.withoutValue.includes(flag)) continue;
|
|
1557
|
-
const attached = value.slice(position + 1);
|
|
1558
|
-
if (options.query.includes(flag)) {
|
|
1559
|
-
if (attached) queries.push(attached);
|
|
1560
|
-
else if (index + 1 < args.length) queries.push(args[(index += 1)]);
|
|
1561
|
-
} else if (options.withValue.includes(flag) && !attached) {
|
|
1562
|
-
index += 1;
|
|
1563
|
-
}
|
|
1564
|
-
break;
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
return queries;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
function directDatabaseQueries(name: string, args: string[]): string[] {
|
|
1571
|
-
if (name === "sqlcmd") return queryOptionValues(args, ["-q"], []);
|
|
1572
|
-
if (name === "psql") {
|
|
1573
|
-
return [...shortOptionQueries(args, PSQL_SHORT_OPTIONS), ...queryOptionValues(args, [], ["--command"])];
|
|
1574
|
-
}
|
|
1575
|
-
if (name === "mysql" || name === "mariadb") {
|
|
1576
|
-
return [...shortOptionQueries(args, MYSQL_SHORT_OPTIONS), ...queryOptionValues(args, [], ["--execute"])];
|
|
1577
|
-
}
|
|
1578
|
-
if (name === "invoke-sqlcmd") {
|
|
1579
|
-
return queryOptionValues(args, [], ["-query"]);
|
|
1580
|
-
}
|
|
1581
|
-
if (name === "sqlite3" || name === "duckdb") {
|
|
1582
|
-
const commandQueries = queryOptionValues(args, ["-cmd"], []);
|
|
1583
|
-
const positionals = positionalArguments(args, SQLITE_OPTIONS_WITH_VALUE);
|
|
1584
|
-
return positionals.length > 1
|
|
1585
|
-
? [...commandQueries, positionals.slice(1).join(" ")]
|
|
1586
|
-
: commandQueries;
|
|
1587
|
-
}
|
|
1588
|
-
return [];
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
/** 세그먼트의 stdin으로 들어가는 heredoc 본문과 here-string 값. */
|
|
1592
|
-
function stdinTexts(segment: ShellSegment): string[] {
|
|
1593
|
-
return [...segment.heredocs.map((heredoc) => heredoc.body), ...segment.hereStrings];
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
const PRINTF_ESCAPES: Readonly<Record<string, string>> = {
|
|
1597
|
-
n: "\n",
|
|
1598
|
-
t: "\t",
|
|
1599
|
-
r: "\r",
|
|
1600
|
-
"\\": "\\",
|
|
1601
|
-
'"': '"',
|
|
1602
|
-
"'": "'",
|
|
1603
|
-
a: "\x07",
|
|
1604
|
-
b: "\b",
|
|
1605
|
-
f: "\f",
|
|
1606
|
-
v: "\v",
|
|
1607
|
-
e: "\x1b",
|
|
1608
|
-
E: "\x1b",
|
|
1609
|
-
};
|
|
1610
|
-
|
|
1611
|
-
/** 서식 문자열의 이스케이프(`\NNN`은 1~3자리 8진수, `\c`는 글자 그대로). 그룹: 1 단순, 2 `\x`, 3 `\u`, 4 `\U`, 5 8진수. */
|
|
1612
|
-
const PRINTF_FORMAT_ESCAPE =
|
|
1613
|
-
/\\(?:([ntrabfveE\\"'])|x([0-9a-fA-F]{1,2})|u([0-9a-fA-F]{1,4})|U([0-9a-fA-F]{1,8})|([0-7]{1,3}))/y;
|
|
1614
|
-
/** `%b` 인자의 이스케이프(`\0NNN`, `\c`는 이후 출력 전체를 끊는다). 그룹 6이 `\c`다. */
|
|
1615
|
-
const PRINTF_ARGUMENT_ESCAPE =
|
|
1616
|
-
/\\(?:([ntrabfveE\\"'])|x([0-9a-fA-F]{1,2})|u([0-9a-fA-F]{1,4})|U([0-9a-fA-F]{1,8})|(0[0-7]{0,3}|[1-7][0-7]{0,2})|(c))/y;
|
|
1617
|
-
/** `%` 변환 명세. 그룹: 1 `%%`, 2 플래그, 3 너비, 4 정밀도, 5 변환 글자(`hh`·`l` 등 길이 수식자는 무시). */
|
|
1618
|
-
const PRINTF_SPEC = /%(?:(%)|([-+ #0]*)(\*|\d+)?(?:\.(\*|\d*))?(?:hh|h|ll|l|L|j|z|t)?([A-Za-z]))/y;
|
|
1619
|
-
/** 너비·정밀도 상한. 스캔이 거대한 문자열을 만들지 못하게 한다. */
|
|
1620
|
-
const PRINTF_MAX_WIDTH = 1024;
|
|
1621
|
-
|
|
1622
|
-
interface PrintfEscape {
|
|
1623
|
-
text: string;
|
|
1624
|
-
length: number;
|
|
1625
|
-
stop: boolean;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
/** `text[index]`의 역슬래시 이스케이프를 읽는다. 알 수 없는 이스케이프(서식의 `\c`·`\q`)는 undefined라 글자 그대로 둔다. */
|
|
1629
|
-
function readPrintfEscape(text: string, index: number, pattern: RegExp): PrintfEscape | undefined {
|
|
1630
|
-
pattern.lastIndex = index;
|
|
1631
|
-
const match = pattern.exec(text);
|
|
1632
|
-
if (!match) return undefined;
|
|
1633
|
-
const [whole, simple, hex, unicode, longUnicode, octal, cut] = match;
|
|
1634
|
-
if (cut !== undefined) return { text: "", length: whole.length, stop: true };
|
|
1635
|
-
let value: string;
|
|
1636
|
-
if (simple !== undefined) value = PRINTF_ESCAPES[simple];
|
|
1637
|
-
else if (hex !== undefined) value = String.fromCharCode(Number.parseInt(hex, 16));
|
|
1638
|
-
else if (octal !== undefined) value = String.fromCharCode(Number.parseInt(octal, 8) & 0xff);
|
|
1639
|
-
else {
|
|
1640
|
-
const code = Number.parseInt(unicode ?? longUnicode ?? "0", 16);
|
|
1641
|
-
value = code <= 0x10ffff ? String.fromCodePoint(code) : "\ufffd";
|
|
1642
|
-
}
|
|
1643
|
-
return { text: value, length: whole.length, stop: false };
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
/** `%b` 인자를 푼다. `\c`를 만나면 거기까지만 돌려주고 `stop`으로 이후 출력 전체를 끊는다. */
|
|
1647
|
-
function expandPrintfArgument(value: string): { text: string; stop: boolean } {
|
|
1648
|
-
let text = "";
|
|
1649
|
-
let index = 0;
|
|
1650
|
-
while (index < value.length) {
|
|
1651
|
-
const escape = value[index] === "\\" ? readPrintfEscape(value, index, PRINTF_ARGUMENT_ESCAPE) : undefined;
|
|
1652
|
-
if (!escape) {
|
|
1653
|
-
text += value[index];
|
|
1654
|
-
index += 1;
|
|
1655
|
-
continue;
|
|
1656
|
-
}
|
|
1657
|
-
text += escape.text;
|
|
1658
|
-
if (escape.stop) return { text, stop: true };
|
|
1659
|
-
index += escape.length;
|
|
1660
|
-
}
|
|
1661
|
-
return { text, stop: false };
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
/** `%d` 류의 정수 인자. `'A`는 글자 코드, 앞의 숫자만 읽고 못 읽으면 0(printf는 오류를 내지만 0을 출력한다). */
|
|
1665
|
-
function parsePrintfInteger(raw: string | undefined): bigint {
|
|
1666
|
-
const text = raw ?? "";
|
|
1667
|
-
const character = /^['"](.)/su.exec(text);
|
|
1668
|
-
if (character) return BigInt(character[1].codePointAt(0) ?? 0);
|
|
1669
|
-
const number = /^[-+]?(?:0[xX][0-9a-fA-F]+|0[0-7]*|[1-9][0-9]*)/.exec(text);
|
|
1670
|
-
if (!number) return BigInt(0);
|
|
1671
|
-
const negative = number[0].startsWith("-");
|
|
1672
|
-
const digits = number[0].replace(/^[-+]/, "");
|
|
1673
|
-
const magnitude = BigInt(/^0[0-7]+$/.test(digits) ? `0o${digits.slice(1)}` : digits);
|
|
1674
|
-
return negative ? -magnitude : magnitude;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
interface PrintfConversion {
|
|
1678
|
-
text: string;
|
|
1679
|
-
numeric: boolean;
|
|
1680
|
-
stop: boolean;
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
/** 변환 글자 하나를 값에 적용한다(너비 채움은 호출자). 모르는 변환 글자는 undefined(printf는 오류로 멈춘다). */
|
|
1684
|
-
function convertPrintfValue(
|
|
1685
|
-
letter: string,
|
|
1686
|
-
raw: string | undefined,
|
|
1687
|
-
flags: string,
|
|
1688
|
-
precision: number | undefined,
|
|
1689
|
-
): PrintfConversion | undefined {
|
|
1690
|
-
const limit = (text: string): string => (precision === undefined ? text : Array.from(text).slice(0, precision).join(""));
|
|
1691
|
-
const sign = (negative: boolean): string => (negative ? "-" : flags.includes("+") ? "+" : flags.includes(" ") ? " " : "");
|
|
1692
|
-
if (letter === "s") return { text: limit(raw ?? ""), numeric: false, stop: false };
|
|
1693
|
-
if (letter === "c") return { text: Array.from(raw ?? "")[0] ?? "\0", numeric: false, stop: false };
|
|
1694
|
-
if (letter === "q") {
|
|
1695
|
-
const quoted = raw === undefined || raw === "" ? "''" : raw.replace(/[^A-Za-z0-9_@%+=:,./-]/g, "\\$&");
|
|
1696
|
-
return { text: limit(quoted), numeric: false, stop: false };
|
|
1697
|
-
}
|
|
1698
|
-
if (letter === "b") {
|
|
1699
|
-
const expanded = expandPrintfArgument(raw ?? "");
|
|
1700
|
-
return { text: limit(expanded.text), numeric: false, stop: expanded.stop };
|
|
1701
|
-
}
|
|
1702
|
-
if ("diuoxX".includes(letter)) {
|
|
1703
|
-
const value = parsePrintfInteger(raw);
|
|
1704
|
-
const signed = letter === "d" || letter === "i";
|
|
1705
|
-
const magnitude = signed ? (value < 0 ? -value : value) : BigInt.asUintN(64, value);
|
|
1706
|
-
const base = letter === "o" ? 8 : letter === "x" || letter === "X" ? 16 : 10;
|
|
1707
|
-
let digits = magnitude.toString(base);
|
|
1708
|
-
if (letter === "X") digits = digits.toUpperCase();
|
|
1709
|
-
if (precision === 0 && magnitude === BigInt(0)) digits = "";
|
|
1710
|
-
if (precision !== undefined) digits = digits.padStart(Math.min(precision, PRINTF_MAX_WIDTH), "0");
|
|
1711
|
-
let prefix = "";
|
|
1712
|
-
if (flags.includes("#") && magnitude !== BigInt(0)) prefix = base === 16 ? (letter === "X" ? "0X" : "0x") : "";
|
|
1713
|
-
if (flags.includes("#") && base === 8 && !digits.startsWith("0")) digits = `0${digits}`;
|
|
1714
|
-
return { text: (signed ? sign(value < 0) : "") + prefix + digits, numeric: true, stop: false };
|
|
1715
|
-
}
|
|
1716
|
-
if ("eEfFgGaA".includes(letter)) {
|
|
1717
|
-
// 부동소수 변환은 숫자·부호·`.`·`e`만 만들어 명령 글자를 이룰 수 없다. 값은 근사로 쓴다.
|
|
1718
|
-
const parsed = Number.parseFloat(raw ?? "");
|
|
1719
|
-
const value = Number.isFinite(parsed) ? parsed : 0;
|
|
1720
|
-
const places = Math.min(precision ?? 6, 100);
|
|
1721
|
-
const lower = letter.toLowerCase();
|
|
1722
|
-
let body: string;
|
|
1723
|
-
if (lower === "f") body = Math.abs(value).toFixed(places);
|
|
1724
|
-
else if (lower === "e") body = Math.abs(value).toExponential(places).replace(/e([+-])(\d)$/, "e$10$2");
|
|
1725
|
-
else if (lower === "g") body = String(Number(Math.abs(value).toPrecision(precision === 0 ? 1 : Math.max(1, places))));
|
|
1726
|
-
else body = `0x${Math.abs(value).toString(16)}p+0`;
|
|
1727
|
-
if (letter !== lower) body = body.toUpperCase();
|
|
1728
|
-
return { text: sign(value < 0) + body, numeric: true, stop: false };
|
|
1729
|
-
}
|
|
1730
|
-
return undefined;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
/**
|
|
1734
|
-
* 리터럴 인자만으로 정해지는 `printf`의 출력. 서식을 읽으며 `%s %b %c %q %d %i %u %o %x %X`와 부동소수 변환을 값으로 채우고,
|
|
1735
|
-
* 플래그·너비·정밀도(`%.3s`, `%-5s`, `%*s`, `%.*s`)를 적용하며, 값이 남으면 서식을 되풀이한다(변환 없는 서식은 한 번).
|
|
1736
|
-
* `%b`의 `\c`는 이후 출력을 모두 끊고, 변환 글자가 없거나 모르는 명세는 printf처럼 거기서 멈춘다. `printf -v`는 출력이 없다.
|
|
1737
|
-
* 변수·치환으로 만든 서식·값은 정적으로 알 수 없어 글자 그대로 쓴다.
|
|
1738
|
-
*/
|
|
1739
|
-
function printfOutput(args: string[]): string {
|
|
1740
|
-
const start = args[0] === "--" ? 1 : 0;
|
|
1741
|
-
if (args[start] === "-v") return "";
|
|
1742
|
-
const [format = "", ...values] = args.slice(start);
|
|
1743
|
-
let output = "";
|
|
1744
|
-
let used = 0;
|
|
1745
|
-
let stopped = false;
|
|
1746
|
-
|
|
1747
|
-
const render = (): void => {
|
|
1748
|
-
let index = 0;
|
|
1749
|
-
while (index < format.length && !stopped) {
|
|
1750
|
-
const character = format[index];
|
|
1751
|
-
if (character === "\\") {
|
|
1752
|
-
const escape = readPrintfEscape(format, index, PRINTF_FORMAT_ESCAPE);
|
|
1753
|
-
output += escape?.text ?? "\\";
|
|
1754
|
-
index += escape?.length ?? 1;
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
if (character !== "%") {
|
|
1758
|
-
output += character;
|
|
1759
|
-
index += 1;
|
|
1760
|
-
continue;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
PRINTF_SPEC.lastIndex = index;
|
|
1764
|
-
const spec = PRINTF_SPEC.exec(format);
|
|
1765
|
-
if (!spec) {
|
|
1766
|
-
stopped = true;
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
index += spec[0].length;
|
|
1770
|
-
if (spec[1] !== undefined) {
|
|
1771
|
-
output += "%";
|
|
1772
|
-
continue;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
const flags = spec[2];
|
|
1776
|
-
let width = spec[3] === "*" ? Number(parsePrintfInteger(values[used++])) : Number(spec[3] ?? 0);
|
|
1777
|
-
let precision: number | undefined;
|
|
1778
|
-
if (spec[4] === "*") precision = Number(parsePrintfInteger(values[used++]));
|
|
1779
|
-
else if (spec[4] !== undefined) precision = Number(spec[4] || 0);
|
|
1780
|
-
if (precision !== undefined && precision < 0) precision = undefined;
|
|
1781
|
-
const left = flags.includes("-") || width < 0;
|
|
1782
|
-
width = Math.min(Math.abs(width), PRINTF_MAX_WIDTH);
|
|
1783
|
-
|
|
1784
|
-
const converted = convertPrintfValue(spec[5], values[used++], flags, precision === undefined ? undefined : Math.min(precision, PRINTF_MAX_WIDTH));
|
|
1785
|
-
if (!converted) {
|
|
1786
|
-
stopped = true;
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
let { text } = converted;
|
|
1790
|
-
const padding = Math.max(0, width - Array.from(text).length);
|
|
1791
|
-
if (left) text += " ".repeat(padding);
|
|
1792
|
-
else if (flags.includes("0") && converted.numeric && !(precision !== undefined && "diuoxX".includes(spec[5]))) {
|
|
1793
|
-
const lead = /^[-+ ]?(?:0[xX])?/.exec(text)?.[0] ?? "";
|
|
1794
|
-
text = lead + "0".repeat(padding) + text.slice(lead.length);
|
|
1795
|
-
} else text = " ".repeat(padding) + text;
|
|
1796
|
-
output += text;
|
|
1797
|
-
if (converted.stop) stopped = true;
|
|
1798
|
-
}
|
|
1799
|
-
};
|
|
1800
|
-
|
|
1801
|
-
let before = used;
|
|
1802
|
-
render();
|
|
1803
|
-
while (!stopped && used > before && used < values.length) {
|
|
1804
|
-
before = used;
|
|
1805
|
-
render();
|
|
1806
|
-
}
|
|
1807
|
-
return output;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
function pipelineText(segment: ShellSegment | undefined): string | undefined {
|
|
1811
|
-
if (!segment || segment.tokens.length === 0) return undefined;
|
|
1812
|
-
const invocation = resolveInvocation(segment.tokens);
|
|
1813
|
-
if (!invocation) return undefined;
|
|
1814
|
-
|
|
1815
|
-
if (segment.tokens.length === 1 && segment.tokens[0].quoted) {
|
|
1816
|
-
return segment.tokens[0].value;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1820
|
-
if (invocation.name === "echo" || invocation.name === "write-output") {
|
|
1821
|
-
return args.filter((value) => value !== "-n").join(" ");
|
|
1822
|
-
}
|
|
1823
|
-
if (invocation.name === "printf") return printfOutput(args);
|
|
1824
|
-
if (invocation.name === "cat" && positionalArguments(args).length === 0) {
|
|
1825
|
-
const stdin = stdinTexts(segment);
|
|
1826
|
-
return stdin.length > 0 ? stdin.join("\n") : undefined;
|
|
1827
|
-
}
|
|
1828
|
-
return undefined;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
function matchDatabase(
|
|
1832
|
-
segment: ShellSegment,
|
|
1833
|
-
invocation: Invocation,
|
|
1834
|
-
previousSegment: ShellSegment | undefined,
|
|
1835
|
-
): string | undefined {
|
|
1836
|
-
if (!DB_CLIENTS[invocation.name]) return undefined;
|
|
1837
|
-
|
|
1838
|
-
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1839
|
-
const queries = directDatabaseQueries(invocation.name, args);
|
|
1840
|
-
if (queries.some(containsDestructiveSql)) return REASONS.database;
|
|
1841
|
-
if (stdinTexts(segment).some(containsDestructiveSql)) return REASONS.database;
|
|
1842
|
-
|
|
1843
|
-
if (segment.separatorBefore === "|") {
|
|
1844
|
-
const pipedText = pipelineText(previousSegment);
|
|
1845
|
-
if (pipedText && containsDestructiveSql(pipedText)) return REASONS.database;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
return undefined;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
function isDeployScriptName(value: string): boolean {
|
|
1852
|
-
const normalized = value.replace(/\\/g, "/").toLowerCase();
|
|
1853
|
-
const basename = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
1854
|
-
if (basename === "deploy" || basename === "release" || basename === "deploy-live") return true;
|
|
1855
|
-
return /^(?:deploy|release)(?:[-_](?:canary|live|preview|prod|production|staging))?\.(?:bat|cmd|cjs|js|mjs|ps1|py|sh|ts)$/.test(
|
|
1856
|
-
basename,
|
|
1857
|
-
);
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
function isDeployScriptLabel(value: string): boolean {
|
|
1861
|
-
return /^(?:pre|post)?(?:deploy|release)(?:$|[:_-])/i.test(value);
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
function isWranglerName(value: string): boolean {
|
|
1865
|
-
return commandName(value).replace(/@[^/]*$/, "") === "wrangler";
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
/** 패키지 러너·매니저 인자에서 `wrangler` 도구 이름 뒤의 인자를 돌려준다(`@version`·`dlx`·`exec`·`x` 허용). */
|
|
1869
|
-
function argumentsAfterWrangler(args: string[], leaders: StringLookup): string[] | undefined {
|
|
1870
|
-
let index = 0;
|
|
1871
|
-
let leaderSeen = false;
|
|
1872
|
-
while (index < args.length) {
|
|
1873
|
-
const value = args[index];
|
|
1874
|
-
if (value === "--") {
|
|
1875
|
-
index += 1;
|
|
1876
|
-
} else if (value.startsWith("-")) {
|
|
1877
|
-
index += !value.includes("=") && PACKAGE_RUNNER_OPTIONS_WITH_VALUE[value] ? 2 : 1;
|
|
1878
|
-
} else if (isWranglerName(value)) {
|
|
1879
|
-
return args.slice(index + 1);
|
|
1880
|
-
} else if (!leaderSeen && leaders[value.toLowerCase()]) {
|
|
1881
|
-
leaderSeen = true;
|
|
1882
|
-
index += 1;
|
|
1883
|
-
} else {
|
|
1884
|
-
return undefined;
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
return undefined;
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
/** `wrangler` 뒤 위치 인자(옵션 값 제외)가 배포 하위 명령인지. */
|
|
1891
|
-
function isWranglerDeploy(args: string[]): boolean {
|
|
1892
|
-
const [first, second] = positionalArguments(args, WRANGLER_OPTIONS_WITH_VALUE).map((value) => value.toLowerCase());
|
|
1893
|
-
if (first === "deploy" || first === "publish") return true;
|
|
1894
|
-
if (first === "versions" || first === "triggers") return second === "deploy";
|
|
1895
|
-
return first === "pages" && (second === "deploy" || second === "publish");
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
/** PowerShell 옵션(값을 받는 옵션 포함)을 지난 첫 위치 인자. `-Command`·`-File`에서 멈춘다(각각 따로 판정한다). */
|
|
1899
|
-
function firstPowerShellPositional(args: string[]): string | undefined {
|
|
1900
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1901
|
-
const option = args[index].toLowerCase();
|
|
1902
|
-
if (!option.startsWith("-")) return args[index];
|
|
1903
|
-
if (POWERSHELL_COMMAND_OPTIONS[option] || option === "-f" || option === "-file") return undefined;
|
|
1904
|
-
if (POWERSHELL_OPTIONS_WITH_VALUE[option]) index += 1;
|
|
1905
|
-
}
|
|
1906
|
-
return undefined;
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
function matchDeployment(tokens: ShellToken[], invocation: Invocation): string | undefined {
|
|
1910
|
-
const args = tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1911
|
-
const positionals = positionalArguments(args);
|
|
1912
|
-
|
|
1913
|
-
if (invocation.name === "wrangler" && isWranglerDeploy(args)) {
|
|
1914
|
-
return REASONS.deploy;
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
if (PACKAGE_EXECUTORS[invocation.name] || PACKAGE_MANAGERS[invocation.name]) {
|
|
1918
|
-
const afterTool = argumentsAfterWrangler(args, PACKAGE_EXECUTORS[invocation.name] ? {} : PACKAGE_MANAGER_EXEC_LEADERS);
|
|
1919
|
-
if (afterTool && isWranglerDeploy(afterTool)) return REASONS.deploy;
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
if (PACKAGE_MANAGERS[invocation.name]) {
|
|
1923
|
-
const action = positionals[0]?.toLowerCase();
|
|
1924
|
-
if (action === "publish" || (action === "npm" && positionals[1]?.toLowerCase() === "publish")) {
|
|
1925
|
-
return REASONS.deploy;
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
if ((action === "run" || action === "run-script") && isDeployScriptLabel(positionals[1] ?? "")) {
|
|
1929
|
-
return REASONS.deploy;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
if (invocation.name === "yarn" && action && isDeployScriptLabel(action)) {
|
|
1933
|
-
return REASONS.deploy;
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
const action = positionals[0]?.toLowerCase();
|
|
1938
|
-
if (
|
|
1939
|
-
((invocation.name === "cargo" ||
|
|
1940
|
-
invocation.name === "deno" ||
|
|
1941
|
-
invocation.name === "poetry" ||
|
|
1942
|
-
invocation.name === "uv") &&
|
|
1943
|
-
action === "publish") ||
|
|
1944
|
-
(invocation.name === "twine" && action === "upload") ||
|
|
1945
|
-
(invocation.name === "gem" && action === "push") ||
|
|
1946
|
-
(invocation.name === "dotnet" &&
|
|
1947
|
-
action === "nuget" &&
|
|
1948
|
-
positionals[1]?.toLowerCase() === "push")
|
|
1949
|
-
) {
|
|
1950
|
-
return REASONS.deploy;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
if (invocation.name === "gh" && action === "release") {
|
|
1954
|
-
const releaseAction = positionals[1]?.toLowerCase();
|
|
1955
|
-
if (releaseAction && GH_RELEASE_MUTATIONS[releaseAction]) return REASONS.deploy;
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
if (DEPLOY_TARGET_RUNNERS[invocation.name] && positionals.some(isDeployScriptLabel)) {
|
|
1959
|
-
return REASONS.deploy;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
if (isDeployScriptName(tokens[invocation.index].value)) return REASONS.deploy;
|
|
1963
|
-
|
|
1964
|
-
if (SCRIPT_INTERPRETERS[invocation.name]) {
|
|
1965
|
-
if (positionals[0] && isDeployScriptName(positionals[0])) return REASONS.deploy;
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
if (invocation.name === "powershell" || invocation.name === "pwsh") {
|
|
1969
|
-
const fileIndex = args.findIndex((value) => {
|
|
1970
|
-
const option = value.toLowerCase();
|
|
1971
|
-
return option === "-f" || option === "-file";
|
|
1972
|
-
});
|
|
1973
|
-
if (fileIndex >= 0 && args[fileIndex + 1] && isDeployScriptName(args[fileIndex + 1])) {
|
|
1974
|
-
return REASONS.deploy;
|
|
1975
|
-
}
|
|
1976
|
-
// `pwsh Tools/x/deploy-live.ps1`: 옵션을 지난 첫 위치 인자는 실행할 스크립트다.
|
|
1977
|
-
const script = firstPowerShellPositional(args);
|
|
1978
|
-
if (script && isDeployScriptName(script)) return REASONS.deploy;
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
return undefined;
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
/** `powershell -Command`의 명령 문자열. */
|
|
1985
|
-
function powerShellCommand(args: string[]): string | undefined {
|
|
1986
|
-
const commandIndex = args.findIndex(
|
|
1987
|
-
(value) => Boolean(POWERSHELL_COMMAND_OPTIONS[value.toLowerCase()]),
|
|
1988
|
-
);
|
|
1989
|
-
if (commandIndex < 0 || commandIndex + 1 >= args.length) return undefined;
|
|
1990
|
-
return args.slice(commandIndex + 1).join(" ");
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
/**
|
|
1994
|
-
* `bash -lc '…'`·`bash -o pipefail -c '…'`처럼 옵션을 지난 첫 위치 인자를 명령 문자열로 쓰는 셸의 인자 해석.
|
|
1995
|
-
* `-c`가 없고 스크립트 파일도 없으면(또는 `-s`·`-`) 셸은 stdin을 읽는다.
|
|
1996
|
-
*/
|
|
1997
|
-
function parseShellArguments(args: string[]): { commandString?: string; readsStdin: boolean } {
|
|
1998
|
-
let commandFlag = false;
|
|
1999
|
-
let stdinFlag = false;
|
|
2000
|
-
let index = 0;
|
|
2001
|
-
for (; index < args.length; index += 1) {
|
|
2002
|
-
const value = args[index];
|
|
2003
|
-
if (value === "--") {
|
|
2004
|
-
index += 1;
|
|
2005
|
-
break;
|
|
2006
|
-
}
|
|
2007
|
-
if (value.length < 2 || !/^[-+]/.test(value)) break;
|
|
2008
|
-
if (value === "--rcfile" || value === "--init-file") {
|
|
2009
|
-
index += 1;
|
|
2010
|
-
continue;
|
|
2011
|
-
}
|
|
2012
|
-
if (/^-[A-Za-z]+$/.test(value)) {
|
|
2013
|
-
if (value.includes("c")) commandFlag = true;
|
|
2014
|
-
if (value.includes("s")) stdinFlag = true;
|
|
2015
|
-
}
|
|
2016
|
-
if (/^[-+][A-Za-z]*[oO]$/.test(value)) index += 1;
|
|
2017
|
-
}
|
|
2018
|
-
const rest = args.slice(index);
|
|
2019
|
-
if (commandFlag) return { commandString: rest[0], readsStdin: false };
|
|
2020
|
-
return { readsStdin: stdinFlag || rest.length === 0 || rest[0] === "-" };
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
/** `su -c '…'`·`script -qc '…'`·`flock <lock> -c '…'`의 명령 문자열. */
|
|
2024
|
-
function commandOptionValue(args: string[]): string | undefined {
|
|
2025
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
2026
|
-
const value = args[index];
|
|
2027
|
-
if (value.startsWith("--command=")) return value.slice("--command=".length);
|
|
2028
|
-
if (value === "--command" || /^-[A-Za-z]*c$/.test(value)) return args[index + 1];
|
|
2029
|
-
}
|
|
2030
|
-
return undefined;
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
/** `watch -n 5 <명령…>`은 이어 붙인 명령 전체를 `sh -c`로 실행한다. */
|
|
2034
|
-
function watchCommand(args: string[]): string {
|
|
2035
|
-
let index = 0;
|
|
2036
|
-
while (index < args.length) {
|
|
2037
|
-
const value = args[index];
|
|
2038
|
-
if (value === "--") {
|
|
2039
|
-
index += 1;
|
|
2040
|
-
break;
|
|
2041
|
-
}
|
|
2042
|
-
if (!value.startsWith("-")) break;
|
|
2043
|
-
index += value === "-n" || value === "--interval" ? 2 : 1;
|
|
2044
|
-
}
|
|
2045
|
-
return args.slice(index).join(" ");
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
/**
|
|
2049
|
-
* 이 세그먼트가 문자열로 실행하게 만드는 하위 스크립트 전부. 명령 치환, 확장이 일어나는 heredoc 본문의 치환,
|
|
2050
|
-
* `bash -c '…'`·`eval`·`su -c`·`watch` 같은 리터럴 명령 문자열, stdin을 읽는 셸에 먹이는 heredoc·here-string·파이프 텍스트.
|
|
2051
|
-
* 변수·동적으로 만든 문자열(`bash -c "$cmd"`)은 정적으로 알 수 없으므로 본문 그대로 넘어가 아무것도 찾지 못한다.
|
|
2052
|
-
*/
|
|
2053
|
-
function nestedScripts(
|
|
2054
|
-
segment: ShellSegment,
|
|
2055
|
-
invocation: Invocation | undefined,
|
|
2056
|
-
previousSegment: ShellSegment | undefined,
|
|
2057
|
-
): string[] {
|
|
2058
|
-
const scripts = [...segment.substitutions];
|
|
2059
|
-
for (const heredoc of segment.heredocs) {
|
|
2060
|
-
if (!heredoc.quoted) scripts.push(...heredocExpansions(heredoc.body));
|
|
2061
|
-
}
|
|
2062
|
-
if (!invocation) return scripts;
|
|
2063
|
-
|
|
2064
|
-
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
2065
|
-
const { name } = invocation;
|
|
2066
|
-
if (name === "powershell" || name === "pwsh") {
|
|
2067
|
-
const command = powerShellCommand(args);
|
|
2068
|
-
if (command) scripts.push(command);
|
|
2069
|
-
} else if (SHELLS[name]) {
|
|
2070
|
-
const shell = parseShellArguments(args);
|
|
2071
|
-
if (shell.commandString !== undefined) {
|
|
2072
|
-
scripts.push(shell.commandString);
|
|
2073
|
-
} else if (shell.readsStdin) {
|
|
2074
|
-
scripts.push(...stdinTexts(segment));
|
|
2075
|
-
const piped = segment.separatorBefore === "|" ? pipelineText(previousSegment) : undefined;
|
|
2076
|
-
if (piped) scripts.push(piped);
|
|
2077
|
-
}
|
|
2078
|
-
} else if (name === "eval") {
|
|
2079
|
-
if (args.length > 0) scripts.push(args.join(" "));
|
|
2080
|
-
} else if (name === "watch") {
|
|
2081
|
-
const command = watchCommand(args);
|
|
2082
|
-
if (command) scripts.push(command);
|
|
2083
|
-
} else if (name === "su" || name === "script" || name === "flock") {
|
|
2084
|
-
const command = commandOptionValue(args);
|
|
2085
|
-
if (command !== undefined) scripts.push(command);
|
|
2086
|
-
}
|
|
2087
|
-
return scripts;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
/**
|
|
2091
|
-
* 명령이 실제로 부르는 실행 파일 이름(따옴표를 벗긴 첫 낱말) 전부. 따옴표 안 언급·주석·heredoc 본문의 글자는 포함하지 않고,
|
|
2092
|
-
* 명령 치환·`bash -c '…'`·래퍼 뒤의 명령은 포함한다. 부르는 낱말이 `"$S"`처럼 변수면 무엇인지 정적으로 알 수 없으므로,
|
|
2093
|
-
* 같은 명령에서 앞서 `S=…`로 대입한 값을 그 호출 후보로 함께 돌려준다. WSL interop이 Windows 프로그램 호출을 알아보는 데 쓴다.
|
|
2094
|
-
*/
|
|
2095
|
-
export function invokedCommands(command: string): string[] {
|
|
2096
|
-
const invoked: string[] = [];
|
|
2097
|
-
const assigned: string[] = [];
|
|
2098
|
-
const visit = (text: string, depth: number): void => {
|
|
2099
|
-
const tokenized = tokenizeShell(text);
|
|
2100
|
-
const descend = (script: string): void => {
|
|
2101
|
-
if (depth < MAX_NESTING) visit(script, depth + 1);
|
|
2102
|
-
};
|
|
2103
|
-
tokenized.loose.forEach(descend);
|
|
2104
|
-
tokenized.segments.forEach((segment, index) => {
|
|
2105
|
-
for (const token of segment.tokens) {
|
|
2106
|
-
if (!isEnvironmentAssignment(token.value)) break;
|
|
2107
|
-
assigned.push(token.value.slice(token.value.indexOf("=") + 1));
|
|
2108
|
-
}
|
|
2109
|
-
const invocation = resolveInvocation(segment.tokens);
|
|
2110
|
-
nestedScripts(segment, invocation, tokenized.segments[index - 1]).forEach(descend);
|
|
2111
|
-
if (!invocation) return;
|
|
2112
|
-
const word = segment.tokens[invocation.index].value;
|
|
2113
|
-
invoked.push(word);
|
|
2114
|
-
if (word.startsWith("$")) invoked.push(...assigned);
|
|
2115
|
-
if (invocation.name === "find") {
|
|
2116
|
-
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
2117
|
-
args.forEach((value, position) => {
|
|
2118
|
-
if (/^-(?:exec|execdir|ok|okdir)$/.test(value) && args[position + 1]) invoked.push(args[position + 1]);
|
|
2119
|
-
});
|
|
2120
|
-
}
|
|
2121
|
-
});
|
|
2122
|
-
};
|
|
2123
|
-
visit(command, 0);
|
|
2124
|
-
return invoked;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
function matchBlockedCommandInternal(
|
|
2128
|
-
command: string,
|
|
2129
|
-
depth: number,
|
|
2130
|
-
context: GuardContext | undefined,
|
|
2131
|
-
windowsShell: boolean,
|
|
2132
|
-
): string | undefined {
|
|
2133
|
-
const tokenized = tokenizeShell(command);
|
|
2134
|
-
const segments = tokenized.segments;
|
|
2135
|
-
const descend = (script: string, scriptContext: GuardContext | undefined, shell: boolean): string | undefined =>
|
|
2136
|
-
depth < MAX_NESTING ? matchBlockedCommandInternal(script, depth + 1, scriptContext, shell) : undefined;
|
|
2137
|
-
for (const script of tokenized.loose) {
|
|
2138
|
-
const reason = descend(script, context, windowsShell);
|
|
2139
|
-
if (reason) return reason;
|
|
2140
|
-
}
|
|
2141
|
-
// `cd <리터럴 경로>` 뒤 `&&`로만 이어진 호출은 그 경로에서 돈다. `;`·`||`·`|`·`&`·줄바꿈으로 이어지면 cd가 실패해도
|
|
2142
|
-
// 다음 호출이 돌 수 있어 위치를 모르는 것(null)으로 보고 finalizer 소유 git을 막는다. 괄호 subshell의 cd는 닫히면
|
|
2143
|
-
// 사라진다. GIT_DIR·GIT_WORK_TREE를 쓰는 명령은 위치가 저장소를 정하지 않으므로 cd를 따라가지 않는다.
|
|
2144
|
-
const followCd = !/\bGIT_(?:DIR|WORK_TREE)\b/.test(command);
|
|
2145
|
-
const cwds: (string | null | undefined)[] = [context?.cwd];
|
|
2146
|
-
const moved: boolean[] = [false];
|
|
2147
|
-
|
|
2148
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
2149
|
-
const segment = segments[index];
|
|
2150
|
-
for (let level = cwds.length; level <= segment.depth; level += 1) {
|
|
2151
|
-
cwds[level] = cwds[level - 1];
|
|
2152
|
-
moved[level] = moved[level - 1];
|
|
2153
|
-
}
|
|
2154
|
-
cwds.length = moved.length = segment.depth + 1;
|
|
2155
|
-
if (index > 0 && segment.separatorBefore !== "&&" && moved[segment.depth]) cwds[segment.depth] = null;
|
|
2156
|
-
|
|
2157
|
-
const invocation = resolveInvocation(segment.tokens);
|
|
2158
|
-
const segmentCwd = cwds[segment.depth];
|
|
2159
|
-
const gitContext = segmentCwd === null ? undefined : context && { ...context, cwd: segmentCwd };
|
|
2160
|
-
// 하위 스크립트는 이 세그먼트가 도는 위치에서 실행된다. 이 세그먼트의 `cd`는 그 뒤에 적용한다.
|
|
2161
|
-
const nestedWindowsShell =
|
|
2162
|
-
windowsShell ||
|
|
2163
|
-
Boolean(
|
|
2164
|
-
invocation &&
|
|
2165
|
-
(invocation.name === "powershell" || invocation.name === "pwsh") &&
|
|
2166
|
-
/\.exe$/i.test(segment.tokens[invocation.index].value),
|
|
2167
|
-
);
|
|
2168
|
-
for (const script of nestedScripts(segment, invocation, segments[index - 1])) {
|
|
2169
|
-
const reason = descend(script, gitContext, nestedWindowsShell);
|
|
2170
|
-
if (reason) return reason;
|
|
2171
|
-
}
|
|
2172
|
-
if (!invocation) continue;
|
|
2173
|
-
|
|
2174
|
-
if (invocation.name === "cd" || invocation.name === "pushd" || invocation.name === "popd") {
|
|
2175
|
-
const targets = segment.tokens
|
|
2176
|
-
.slice(invocation.index + 1)
|
|
2177
|
-
.map((token) => token.value)
|
|
2178
|
-
.filter((value) => value !== "-L" && value !== "-P" && value !== "--");
|
|
2179
|
-
const from = cwds[segment.depth];
|
|
2180
|
-
const target = targets[0];
|
|
2181
|
-
const literal =
|
|
2182
|
-
followCd && invocation.name === "cd" && targets.length === 1 && target !== "-" && !hasShellExpansion(target);
|
|
2183
|
-
cwds[segment.depth] = !literal
|
|
2184
|
-
? null
|
|
2185
|
-
: from === null
|
|
2186
|
-
? isAbsolute(target) ? resolve(target) : null
|
|
2187
|
-
: resolve(from ?? ".", target);
|
|
2188
|
-
moved[segment.depth] = true;
|
|
2189
|
-
continue;
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
const gitReason = matchGit(segment.tokens, invocation, gitContext);
|
|
2193
|
-
if (gitReason) return gitReason;
|
|
2194
|
-
const linuxGitReason = matchLinuxGitOnWindowsDrive(segment.tokens, invocation, context, segmentCwd, windowsShell);
|
|
2195
|
-
if (linuxGitReason) return linuxGitReason;
|
|
2196
|
-
|
|
2197
|
-
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
2198
|
-
if (invocation.name === "rm") {
|
|
2199
|
-
const reason = matchRm(args);
|
|
2200
|
-
if (reason) return reason;
|
|
2201
|
-
}
|
|
2202
|
-
if (
|
|
2203
|
-
invocation.name === "remove-item" ||
|
|
2204
|
-
invocation.name === "del" ||
|
|
2205
|
-
invocation.name === "erase" ||
|
|
2206
|
-
invocation.name === "rd" ||
|
|
2207
|
-
invocation.name === "ri" ||
|
|
2208
|
-
invocation.name === "rm" ||
|
|
2209
|
-
invocation.name === "rmdir"
|
|
2210
|
-
) {
|
|
2211
|
-
const reason = matchRemoveItem(args);
|
|
2212
|
-
if (reason) return reason;
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
const databaseReason = matchDatabase(segment, invocation, segments[index - 1]);
|
|
2216
|
-
if (databaseReason) return databaseReason;
|
|
2217
|
-
|
|
2218
|
-
const deployReason = matchDeployment(segment.tokens, invocation);
|
|
2219
|
-
if (deployReason) return deployReason;
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
return undefined;
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
function unescapePosix(command: string): string {
|
|
2226
|
-
let result = "";
|
|
2227
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
2228
|
-
const character = command[index];
|
|
2229
|
-
if (character === "<" && command[index + 1] === "<" && command[index + 2] !== "<" && command[index - 1] !== "<") {
|
|
2230
|
-
const wordStart = heredocWordStart(command, index + 2).index;
|
|
2231
|
-
const wordEnd = scanHeredocWord(command, wordStart).end;
|
|
2232
|
-
result += command.slice(index, wordEnd);
|
|
2233
|
-
index = wordEnd - 1;
|
|
2234
|
-
continue;
|
|
2235
|
-
}
|
|
2236
|
-
if (character === "\\" && index + 1 < command.length && !/[\r\n$`]/.test(command[index + 1])) {
|
|
2237
|
-
result += command[index + 1];
|
|
2238
|
-
index += 1;
|
|
2239
|
-
continue;
|
|
2240
|
-
}
|
|
2241
|
-
result += character;
|
|
2242
|
-
}
|
|
2243
|
-
return result;
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
export function matchBlockedCommand(
|
|
2247
|
-
command: string,
|
|
2248
|
-
context?: GuardContext,
|
|
2249
|
-
): string | undefined {
|
|
2250
|
-
const directReason = matchBlockedCommandInternal(command, 0, context, false);
|
|
2251
|
-
if (directReason) return directReason;
|
|
2252
|
-
|
|
2253
|
-
// 역슬래시를 POSIX처럼 벗긴 2차 판정. `\$`·`\``는 치환이 일어나지 않게 막은 것이고, heredoc 구분자 낱말(`<<\EOF`,
|
|
2254
|
-
// `<<E\OF`, `<<'E'OF`)의 `\`·따옴표는 본문을 확장 없이 그대로 두라는 표시라 둘 다 벗기지 않는다.
|
|
2255
|
-
const posixUnescaped = unescapePosix(command);
|
|
2256
|
-
if (posixUnescaped === command) return undefined;
|
|
2257
|
-
return matchBlockedCommandInternal(posixUnescaped, 0, context, false);
|
|
2258
|
-
}
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface GuardContext {
|
|
4
|
+
/** bash tool_call이 지정한 cwd. 없으면 세션 cwd. */
|
|
5
|
+
cwd?: string;
|
|
6
|
+
/** 주어진 디렉터리가 세션 저장소와 같은 git 저장소인지. 미제공이면 차단. */
|
|
7
|
+
isSessionRepository?: (dir: string) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 주어진 디렉터리를 하네스가 Windows `git.exe`로 다루는지(WSL의 `/mnt/<드라이브>` 저장소). 미제공이면 판정하지 않는다.
|
|
10
|
+
*/
|
|
11
|
+
usesWindowsGit?: (dir: string) => boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Separator = ";" | "&&" | "||" | "|" | "&" | "\n";
|
|
15
|
+
|
|
16
|
+
interface ShellToken {
|
|
17
|
+
value: string;
|
|
18
|
+
quoted: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Heredoc {
|
|
22
|
+
delimiter: string;
|
|
23
|
+
/** 구분자에 따옴표·역슬래시가 있으면 본문은 확장 없이 그대로다. */
|
|
24
|
+
quoted: boolean;
|
|
25
|
+
stripTabs: boolean;
|
|
26
|
+
/** `<<` 뒤 줄바꿈부터 구분자 줄 앞까지. 줄바꿈을 만나기 전에는 비어 있다. */
|
|
27
|
+
body: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ShellSegment {
|
|
31
|
+
tokens: ShellToken[];
|
|
32
|
+
separatorBefore?: Separator;
|
|
33
|
+
/** 괄호 subshell 깊이. `cd`의 효과는 같은 깊이 이하로만 이어진다. */
|
|
34
|
+
depth: number;
|
|
35
|
+
/** 따옴표 밖·큰따옴표 안 `$(…)`·백틱 명령 치환의 본문(작은따옴표 안과 `\$`는 제외). */
|
|
36
|
+
substitutions: string[];
|
|
37
|
+
/** 이 세그먼트의 stdin으로 들어가는 heredoc. */
|
|
38
|
+
heredocs: Heredoc[];
|
|
39
|
+
/** `<<<` here-string 값. */
|
|
40
|
+
hereStrings: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface TokenizedShell {
|
|
44
|
+
segments: ShellSegment[];
|
|
45
|
+
/** 실행 세그먼트에 붙지 않은 치환 본문(`> $(…)`처럼 리다이렉션 대상 등). */
|
|
46
|
+
loose: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Invocation {
|
|
50
|
+
index: number;
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface GitInvocation {
|
|
55
|
+
subcommand: string;
|
|
56
|
+
args: string[];
|
|
57
|
+
globalArgs: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type StringLookup = Readonly<Record<string, true>>;
|
|
61
|
+
|
|
62
|
+
const GIT_GLOBAL_OPTIONS_WITH_VALUE: StringLookup = {
|
|
63
|
+
"-c": true,
|
|
64
|
+
"-C": true,
|
|
65
|
+
"--config-env": true,
|
|
66
|
+
"--exec-path": true,
|
|
67
|
+
"--git-dir": true,
|
|
68
|
+
"--namespace": true,
|
|
69
|
+
"--super-prefix": true,
|
|
70
|
+
"--work-tree": true,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const CLI_OPTIONS_WITH_VALUE: StringLookup = {
|
|
74
|
+
"--config": true,
|
|
75
|
+
"--cwd": true,
|
|
76
|
+
"--dir": true,
|
|
77
|
+
"--filter": true,
|
|
78
|
+
"--prefix": true,
|
|
79
|
+
"--project": true,
|
|
80
|
+
"--repo": true,
|
|
81
|
+
"--repository": true,
|
|
82
|
+
"--workspace": true,
|
|
83
|
+
"-C": true,
|
|
84
|
+
"-R": true,
|
|
85
|
+
"-c": true,
|
|
86
|
+
"-w": true,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const FINALIZER_OWNED_GIT_COMMANDS: StringLookup = {
|
|
90
|
+
add: true,
|
|
91
|
+
commit: true,
|
|
92
|
+
"commit-tree": true,
|
|
93
|
+
push: true,
|
|
94
|
+
"update-ref": true,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** 저장소 없이 도는 git 하위 명령. 어느 git으로 돌려도 index를 건드리지 않는다. */
|
|
98
|
+
const REPOSITORY_FREE_GIT_COMMANDS: StringLookup = {
|
|
99
|
+
help: true,
|
|
100
|
+
version: true,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const DB_CLIENTS: StringLookup = {
|
|
104
|
+
duckdb: true,
|
|
105
|
+
"invoke-sqlcmd": true,
|
|
106
|
+
mariadb: true,
|
|
107
|
+
mysql: true,
|
|
108
|
+
psql: true,
|
|
109
|
+
sqlite3: true,
|
|
110
|
+
sqlcmd: true,
|
|
111
|
+
sqlplus: true,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const SQLITE_OPTIONS_WITH_VALUE: StringLookup = {
|
|
115
|
+
"-cmd": true,
|
|
116
|
+
"-init": true,
|
|
117
|
+
"-separator": true,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const BROAD_PATHS: StringLookup = {
|
|
121
|
+
"*": true,
|
|
122
|
+
"**": true,
|
|
123
|
+
"**/*": true,
|
|
124
|
+
".": true,
|
|
125
|
+
"./": true,
|
|
126
|
+
"./*": true,
|
|
127
|
+
"./**": true,
|
|
128
|
+
"./**/*": true,
|
|
129
|
+
"..": true,
|
|
130
|
+
"../": true,
|
|
131
|
+
"../*": true,
|
|
132
|
+
"/": true,
|
|
133
|
+
"/*": true,
|
|
134
|
+
"~": true,
|
|
135
|
+
"~/": true,
|
|
136
|
+
"~/*": true,
|
|
137
|
+
"~/**": true,
|
|
138
|
+
"~/**/*": true,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const BROAD_ENVIRONMENT_PATHS: StringLookup = {
|
|
142
|
+
"$HOME": true,
|
|
143
|
+
"$HOME/": true,
|
|
144
|
+
"$HOME/*": true,
|
|
145
|
+
"$HOME/**": true,
|
|
146
|
+
"${HOME}": true,
|
|
147
|
+
"${HOME}/": true,
|
|
148
|
+
"${HOME}/*": true,
|
|
149
|
+
"${HOME}/**": true,
|
|
150
|
+
"$PWD": true,
|
|
151
|
+
"$PWD/": true,
|
|
152
|
+
"$PWD/*": true,
|
|
153
|
+
"$PWD/**": true,
|
|
154
|
+
"${PWD}": true,
|
|
155
|
+
"${PWD}/": true,
|
|
156
|
+
"${PWD}/*": true,
|
|
157
|
+
"${PWD}/**": true,
|
|
158
|
+
"%CD%": true,
|
|
159
|
+
"%CD%/": true,
|
|
160
|
+
"%CD%/*": true,
|
|
161
|
+
"%CD%/**": true,
|
|
162
|
+
"%USERPROFILE%": true,
|
|
163
|
+
"%USERPROFILE%/": true,
|
|
164
|
+
"%USERPROFILE%/*": true,
|
|
165
|
+
"%USERPROFILE%/**": true,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const INVOCATION_PREFIXES: StringLookup = {
|
|
169
|
+
"!": true,
|
|
170
|
+
"&": true,
|
|
171
|
+
"{": true,
|
|
172
|
+
"}": true,
|
|
173
|
+
do: true,
|
|
174
|
+
elif: true,
|
|
175
|
+
else: true,
|
|
176
|
+
if: true,
|
|
177
|
+
then: true,
|
|
178
|
+
until: true,
|
|
179
|
+
while: true,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const COMMAND_WRAPPERS: StringLookup = {
|
|
183
|
+
builtin: true,
|
|
184
|
+
command: true,
|
|
185
|
+
nohup: true,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const ENV_OPTIONS_WITH_VALUE: StringLookup = {
|
|
189
|
+
"--chdir": true,
|
|
190
|
+
"--unset": true,
|
|
191
|
+
"-C": true,
|
|
192
|
+
"-u": true,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const SUDO_OPTIONS_WITH_VALUE: StringLookup = {
|
|
196
|
+
"-C": true,
|
|
197
|
+
"-D": true,
|
|
198
|
+
"-g": true,
|
|
199
|
+
"-h": true,
|
|
200
|
+
"-p": true,
|
|
201
|
+
"-R": true,
|
|
202
|
+
"-T": true,
|
|
203
|
+
"-u": true,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const TIME_OPTIONS_WITH_VALUE: StringLookup = {
|
|
207
|
+
"--format": true,
|
|
208
|
+
"--output": true,
|
|
209
|
+
"-f": true,
|
|
210
|
+
"-o": true,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const TIMEOUT_OPTIONS_WITH_VALUE: StringLookup = {
|
|
214
|
+
"--kill-after": true,
|
|
215
|
+
"--signal": true,
|
|
216
|
+
"-k": true,
|
|
217
|
+
"-s": true,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
const REMOVE_ITEM_PATH_OPTIONS: StringLookup = {
|
|
222
|
+
"-literalpath": true,
|
|
223
|
+
"-path": true,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const REMOVE_ITEM_OPTIONS_WITH_VALUE: StringLookup = {
|
|
227
|
+
"-credential": true,
|
|
228
|
+
"-exclude": true,
|
|
229
|
+
"-filter": true,
|
|
230
|
+
"-include": true,
|
|
231
|
+
"-stream": true,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const PACKAGE_EXECUTORS: StringLookup = {
|
|
235
|
+
bunx: true,
|
|
236
|
+
npx: true,
|
|
237
|
+
pnpx: true,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const PACKAGE_MANAGERS: StringLookup = {
|
|
241
|
+
bun: true,
|
|
242
|
+
npm: true,
|
|
243
|
+
pnpm: true,
|
|
244
|
+
yarn: true,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const POWERSHELL_OPTIONS_WITH_VALUE: StringLookup = {
|
|
248
|
+
"-configurationname": true,
|
|
249
|
+
"-ep": true,
|
|
250
|
+
"-executionpolicy": true,
|
|
251
|
+
"-inputformat": true,
|
|
252
|
+
"-outputformat": true,
|
|
253
|
+
"-settingsfile": true,
|
|
254
|
+
"-version": true,
|
|
255
|
+
"-windowstyle": true,
|
|
256
|
+
"-wd": true,
|
|
257
|
+
"-workingdirectory": true,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const GH_RELEASE_MUTATIONS: StringLookup = {
|
|
261
|
+
create: true,
|
|
262
|
+
delete: true,
|
|
263
|
+
edit: true,
|
|
264
|
+
upload: true,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const DEPLOY_TARGET_RUNNERS: StringLookup = {
|
|
268
|
+
just: true,
|
|
269
|
+
make: true,
|
|
270
|
+
task: true,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const SCRIPT_INTERPRETERS: StringLookup = {
|
|
274
|
+
bash: true,
|
|
275
|
+
deno: true,
|
|
276
|
+
node: true,
|
|
277
|
+
python: true,
|
|
278
|
+
python3: true,
|
|
279
|
+
sh: true,
|
|
280
|
+
zsh: true,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const POWERSHELL_COMMAND_OPTIONS: StringLookup = {
|
|
284
|
+
"-c": true,
|
|
285
|
+
"-command": true,
|
|
286
|
+
"-commandwithargs": true,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
interface WrapperSpec {
|
|
290
|
+
optionsWithValue: StringLookup;
|
|
291
|
+
/** 옵션 뒤, 감싸는 명령 앞에 오는 위치 인자 수(`taskset <mask>`, `flock <lockfile>`). */
|
|
292
|
+
positionals: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 뒤따르는 명령을 그대로 실행하는 래퍼. 옵션 형태는 각 명령의 실제 CLI를 따른다. */
|
|
296
|
+
const EXEC_WRAPPERS: Readonly<Record<string, WrapperSpec>> = {
|
|
297
|
+
exec: { optionsWithValue: { "-a": true }, positionals: 0 },
|
|
298
|
+
nice: { optionsWithValue: { "-n": true, "--adjustment": true }, positionals: 0 },
|
|
299
|
+
setsid: { optionsWithValue: {}, positionals: 0 },
|
|
300
|
+
ionice: {
|
|
301
|
+
optionsWithValue: { "-c": true, "--class": true, "-n": true, "--classdata": true },
|
|
302
|
+
positionals: 0,
|
|
303
|
+
},
|
|
304
|
+
stdbuf: {
|
|
305
|
+
optionsWithValue: { "-i": true, "-o": true, "-e": true, "--input": true, "--output": true, "--error": true },
|
|
306
|
+
positionals: 0,
|
|
307
|
+
},
|
|
308
|
+
taskset: { optionsWithValue: {}, positionals: 1 },
|
|
309
|
+
chrt: { optionsWithValue: {}, positionals: 1 },
|
|
310
|
+
flock: {
|
|
311
|
+
optionsWithValue: { "-w": true, "--wait": true, "--timeout": true, "-E": true, "--conflict-exit-code": true },
|
|
312
|
+
positionals: 1,
|
|
313
|
+
},
|
|
314
|
+
xargs: {
|
|
315
|
+
optionsWithValue: {
|
|
316
|
+
"-a": true, "--arg-file": true, "-d": true, "--delimiter": true, "-E": true, "--eof": true, "-I": true,
|
|
317
|
+
"-L": true, "--max-lines": true, "-n": true, "--max-args": true, "-P": true, "--max-procs": true,
|
|
318
|
+
"-s": true, "--max-chars": true,
|
|
319
|
+
},
|
|
320
|
+
positionals: 0,
|
|
321
|
+
},
|
|
322
|
+
doas: { optionsWithValue: { "-u": true, "-C": true }, positionals: 0 },
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/** 명령 문자열을 `-c`로 받아 셸로 실행하는 인터프리터. */
|
|
326
|
+
const SHELLS: StringLookup = {
|
|
327
|
+
ash: true,
|
|
328
|
+
bash: true,
|
|
329
|
+
dash: true,
|
|
330
|
+
fish: true,
|
|
331
|
+
ksh: true,
|
|
332
|
+
mksh: true,
|
|
333
|
+
sh: true,
|
|
334
|
+
zsh: true,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/** 최대 중첩 깊이(`bash -c`·`$(…)`·heredoc 본문). 넘으면 더 파고들지 않는다. */
|
|
338
|
+
const MAX_NESTING = 5;
|
|
339
|
+
|
|
340
|
+
const WRANGLER_OPTIONS_WITH_VALUE: StringLookup = {
|
|
341
|
+
...CLI_OPTIONS_WITH_VALUE,
|
|
342
|
+
"--env": true,
|
|
343
|
+
"-e": true,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** 패키지 러너가 도구 이름 앞에서 값을 받는 옵션. */
|
|
347
|
+
const PACKAGE_RUNNER_OPTIONS_WITH_VALUE: StringLookup = {
|
|
348
|
+
...CLI_OPTIONS_WITH_VALUE,
|
|
349
|
+
"--package": true,
|
|
350
|
+
"-p": true,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/** 패키지 매니저가 도구 이름 앞에 두는 실행 하위 명령(`pnpm dlx wrangler`, `bun x wrangler`). */
|
|
354
|
+
const PACKAGE_MANAGER_EXEC_LEADERS: StringLookup = {
|
|
355
|
+
dlx: true,
|
|
356
|
+
exec: true,
|
|
357
|
+
run: true,
|
|
358
|
+
"run-script": true,
|
|
359
|
+
x: true,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
interface ShortQueryOptions {
|
|
363
|
+
/** 뒤따르는 SQL을 값으로 받는 짧은 옵션 글자. */
|
|
364
|
+
query: string;
|
|
365
|
+
/** 값을 받지 않는 짧은 옵션 글자. 묶음(`-Atc`)을 앞에서부터 풀 때 건너뛴다. */
|
|
366
|
+
withoutValue: string;
|
|
367
|
+
/** 값을 받는 다른 옵션 글자(`-d db`, `-U user`). */
|
|
368
|
+
withValue: string;
|
|
369
|
+
/** 값이 같은 낱말에만 붙는 옵션 글자(mysql `-p<password>`). 뒤 글자는 옵션이 아니라 값이다. */
|
|
370
|
+
attachedOnly: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const PSQL_SHORT_OPTIONS: ShortQueryOptions = {
|
|
374
|
+
query: "c",
|
|
375
|
+
withoutValue: "AabeEHlnqsStVwWxXz01?",
|
|
376
|
+
withValue: "dfFhLopPRTUv",
|
|
377
|
+
attachedOnly: "",
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const MYSQL_SHORT_OPTIONS: ShortQueryOptions = {
|
|
381
|
+
query: "e",
|
|
382
|
+
withoutValue: "AaBbCcEfHIiLnNoqrstTUvVwWXj?",
|
|
383
|
+
withValue: "DhPuSO",
|
|
384
|
+
attachedOnly: "p",
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const REASONS = {
|
|
388
|
+
gitFinalize:
|
|
389
|
+
"자동 staging·commit·push와 ref 갱신은 저장소 잠금과 경로·ancestry 검증을 수행하는 git_finalize 도구로만 실행하세요.",
|
|
390
|
+
gitReset:
|
|
391
|
+
"작업 내용을 파괴할 수 있는 git reset --hard 명령은 정책상 차단됩니다.",
|
|
392
|
+
gitClean:
|
|
393
|
+
"디렉터리를 강제 정리하는 git clean 명령은 정책상 차단됩니다.",
|
|
394
|
+
gitDelete:
|
|
395
|
+
"브랜치·태그를 강제로 삭제하거나 원격 ref를 삭제하는 명령은 정책상 차단됩니다.",
|
|
396
|
+
gitDiscard:
|
|
397
|
+
"작업 트리나 stash의 변경을 되돌릴 수 없게 버리는 git checkout·restore·switch·stash·worktree 명령은 정책상 차단됩니다. " +
|
|
398
|
+
"변경을 남기려면 먼저 commit하거나 git stash push로 보관하세요.",
|
|
399
|
+
broadRemove:
|
|
400
|
+
"현재 작업 트리나 루트 경로를 광범위하게 삭제하는 명령은 정책상 차단됩니다.",
|
|
401
|
+
database:
|
|
402
|
+
"실제 데이터베이스 클라이언트로 DROP/TRUNCATE를 실행하는 명령은 정책상 차단됩니다.",
|
|
403
|
+
deploy:
|
|
404
|
+
"배포·게시·릴리스 작업을 실행하는 명령은 정책상 차단됩니다.",
|
|
405
|
+
linuxGitOnWindowsDrive:
|
|
406
|
+
"이 저장소는 Windows 드라이브(/mnt/<드라이브>)에 있어 하네스·앱·git_finalize가 Windows git.exe로 다룹니다. " +
|
|
407
|
+
"Linux git을 섞어 쓰면 index의 파일 정보가 서로 맞지 않아 추적 파일 전체를 다시 확인하고 index를 다시 씁니다. " +
|
|
408
|
+
"큰 저장소에서는 수 분이 걸려 시간 제한에 끊기고 index.lock이 남습니다(--no-optional-locks로도 git diff의 갱신은 막히지 않습니다). " +
|
|
409
|
+
"같은 명령을 git.exe로 실행하세요. 예: cd <저장소 안 폴더> && git.exe status. -C에는 \"$(wslpath -w <경로>)\"로 바꾼 경로를 넘깁니다.",
|
|
410
|
+
} as const;
|
|
411
|
+
|
|
412
|
+
function isRedirectionToken(value: string): boolean {
|
|
413
|
+
return /^(?:(?:\d+)?(?:<|>|<<|>>|<<<|<>|>&|<&|>\|)|&>|&>>)$/.test(value);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function stripRedirections(tokens: ShellToken[]): ShellToken[] {
|
|
417
|
+
const executable: ShellToken[] = [];
|
|
418
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
419
|
+
if (!isRedirectionToken(tokens[index].value)) {
|
|
420
|
+
executable.push(tokens[index]);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (index + 1 < tokens.length && !isRedirectionToken(tokens[index + 1].value)) {
|
|
424
|
+
index += 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return executable;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
interface Substitution {
|
|
431
|
+
inner: string;
|
|
432
|
+
/** 치환이 끝난 다음 위치. */
|
|
433
|
+
end: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function readBacktickSubstitution(text: string, start: number): Substitution {
|
|
437
|
+
let inner = "";
|
|
438
|
+
let index = start + 1;
|
|
439
|
+
while (index < text.length) {
|
|
440
|
+
const character = text[index];
|
|
441
|
+
if (character === "\\" && index + 1 < text.length && /[`\\$]/.test(text[index + 1])) {
|
|
442
|
+
inner += text[index + 1];
|
|
443
|
+
index += 2;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (character === "`") return { inner, end: index + 1 };
|
|
447
|
+
inner += character;
|
|
448
|
+
index += 1;
|
|
449
|
+
}
|
|
450
|
+
return { inner, end: text.length };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function skipDoubleQuoted(text: string, start: number): number {
|
|
454
|
+
let index = start;
|
|
455
|
+
while (index < text.length) {
|
|
456
|
+
const character = text[index];
|
|
457
|
+
if (character === "\\") index += 2;
|
|
458
|
+
else if (character === '"') return index + 1;
|
|
459
|
+
else if (character === "$" && text[index + 1] === "(") index = readCommandSubstitution(text, index).end;
|
|
460
|
+
else if (character === "`") index = readBacktickSubstitution(text, index).end;
|
|
461
|
+
else index += 1;
|
|
462
|
+
}
|
|
463
|
+
return text.length;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** `text[start]`가 `$`이고 다음이 `(`일 때 짝이 맞는 `)`까지 읽는다. 따옴표·이스케이프·중첩 치환을 따라간다. */
|
|
467
|
+
function readCommandSubstitution(text: string, start: number): Substitution {
|
|
468
|
+
let depth = 1;
|
|
469
|
+
let index = start + 2;
|
|
470
|
+
while (index < text.length) {
|
|
471
|
+
const character = text[index];
|
|
472
|
+
if (character === "\\") {
|
|
473
|
+
index += 2;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (character === "'") {
|
|
477
|
+
const close = text.indexOf("'", index + 1);
|
|
478
|
+
index = close < 0 ? text.length : close + 1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (character === '"') {
|
|
482
|
+
index = skipDoubleQuoted(text, index + 1);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (character === "`") {
|
|
486
|
+
index = readBacktickSubstitution(text, index).end;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (character === "(") depth += 1;
|
|
490
|
+
else if (character === ")") {
|
|
491
|
+
depth -= 1;
|
|
492
|
+
if (depth === 0) return { inner: text.slice(start + 2, index), end: index + 1 };
|
|
493
|
+
}
|
|
494
|
+
index += 1;
|
|
495
|
+
}
|
|
496
|
+
return { inner: text.slice(start + 2), end: text.length };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** 따옴표 없는 heredoc 본문에서 셸이 실행하는 `$(…)`·백틱 치환만 꺼낸다. 본문의 따옴표는 그냥 글자다. */
|
|
500
|
+
function heredocExpansions(body: string): string[] {
|
|
501
|
+
const scripts: string[] = [];
|
|
502
|
+
let index = 0;
|
|
503
|
+
while (index < body.length) {
|
|
504
|
+
const character = body[index];
|
|
505
|
+
if (character === "\\") {
|
|
506
|
+
index += 2;
|
|
507
|
+
} else if (character === "$" && body[index + 1] === "(") {
|
|
508
|
+
const substitution = readCommandSubstitution(body, index);
|
|
509
|
+
scripts.push(substitution.inner);
|
|
510
|
+
index = substitution.end;
|
|
511
|
+
} else if (character === "`") {
|
|
512
|
+
const substitution = readBacktickSubstitution(body, index);
|
|
513
|
+
scripts.push(substitution.inner);
|
|
514
|
+
index = substitution.end;
|
|
515
|
+
} else {
|
|
516
|
+
index += 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return scripts;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* heredoc 구분자 낱말을 읽는다. 따옴표(`'EOF'`·`"EOF"`·빈 `''`)나 역슬래시(`\EOF`, `E\OF`)가 한 글자라도 있으면 본문은
|
|
524
|
+
* 확장 없이 그대로다. 구분자 값은 따옴표·역슬래시를 벗긴 글자이고, 빈 따옴표는 빈 구분자(빈 줄이 끝)다.
|
|
525
|
+
*/
|
|
526
|
+
function scanHeredocWord(command: string, start: number): { delimiter: string; quoted: boolean; end: number } {
|
|
527
|
+
let index = start;
|
|
528
|
+
let delimiter = "";
|
|
529
|
+
let quoted = false;
|
|
530
|
+
while (index < command.length) {
|
|
531
|
+
const character = command[index];
|
|
532
|
+
if (character === "'" || character === '"') {
|
|
533
|
+
quoted = true;
|
|
534
|
+
const close = command.indexOf(character, index + 1);
|
|
535
|
+
const end = close < 0 ? command.length : close;
|
|
536
|
+
delimiter += command.slice(index + 1, end);
|
|
537
|
+
index = close < 0 ? command.length : close + 1;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (character === "\\" && index + 1 < command.length && command[index + 1] !== "\n") {
|
|
541
|
+
quoted = true;
|
|
542
|
+
delimiter += command[index + 1];
|
|
543
|
+
index += 2;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (/[\s;&|<>()]/.test(character)) break;
|
|
547
|
+
delimiter += character;
|
|
548
|
+
index += 1;
|
|
549
|
+
}
|
|
550
|
+
return { delimiter, quoted, end: index };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** `<<` 바로 뒤(`-`·공백 다음)의 구분자 낱말이 시작하는 위치와 `<<-` 여부. */
|
|
554
|
+
function heredocWordStart(command: string, start: number): { index: number; stripTabs: boolean } {
|
|
555
|
+
let index = start;
|
|
556
|
+
const stripTabs = command[index] === "-";
|
|
557
|
+
if (stripTabs) index += 1;
|
|
558
|
+
while (command[index] === " " || command[index] === "\t") index += 1;
|
|
559
|
+
return { index, stripTabs };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** `<<` 바로 뒤의 구분자 낱말을 읽는다. 낱말이 없으면 undefined. */
|
|
563
|
+
function readHeredocDelimiter(command: string, start: number): { heredoc: Heredoc; end: number } | undefined {
|
|
564
|
+
const { index, stripTabs } = heredocWordStart(command, start);
|
|
565
|
+
const word = scanHeredocWord(command, index);
|
|
566
|
+
if (word.end === index) return undefined;
|
|
567
|
+
return { heredoc: { delimiter: word.delimiter, quoted: word.quoted, stripTabs, body: "" }, end: word.end };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** `position`(줄 시작)부터 대기 중인 heredoc 본문을 차례로 읽고 다음 위치를 돌려준다. 구분자 줄이 없으면 끝까지가 본문이다. */
|
|
571
|
+
function readHeredocBodies(command: string, position: number, pending: Heredoc[]): number {
|
|
572
|
+
let index = position;
|
|
573
|
+
for (const heredoc of pending) {
|
|
574
|
+
const lines: string[] = [];
|
|
575
|
+
while (index < command.length) {
|
|
576
|
+
const lineEnd = command.indexOf("\n", index);
|
|
577
|
+
const raw = command.slice(index, lineEnd < 0 ? command.length : lineEnd);
|
|
578
|
+
index = lineEnd < 0 ? command.length : lineEnd + 1;
|
|
579
|
+
const line = raw.replace(/\r$/, "");
|
|
580
|
+
if ((heredoc.stripTabs ? line.replace(/^\t+/, "") : line) === heredoc.delimiter) break;
|
|
581
|
+
lines.push(raw);
|
|
582
|
+
}
|
|
583
|
+
heredoc.body = lines.join("\n");
|
|
584
|
+
}
|
|
585
|
+
return index;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function tokenizeShell(command: string): TokenizedShell {
|
|
589
|
+
const segments: ShellSegment[] = [];
|
|
590
|
+
const loose: string[] = [];
|
|
591
|
+
const looseHeredocs: Heredoc[] = [];
|
|
592
|
+
let tokens: ShellToken[] = [];
|
|
593
|
+
let substitutions: string[] = [];
|
|
594
|
+
let heredocs: Heredoc[] = [];
|
|
595
|
+
let pendingBodies: Heredoc[] = [];
|
|
596
|
+
let value = "";
|
|
597
|
+
let tokenStarted = false;
|
|
598
|
+
let tokenQuoted = false;
|
|
599
|
+
let quote: "'" | '"' | undefined;
|
|
600
|
+
let separatorBefore: Separator | undefined;
|
|
601
|
+
let depth = 0;
|
|
602
|
+
|
|
603
|
+
const endToken = (): void => {
|
|
604
|
+
if (!tokenStarted) return;
|
|
605
|
+
tokens.push({ value, quoted: tokenQuoted });
|
|
606
|
+
value = "";
|
|
607
|
+
tokenStarted = false;
|
|
608
|
+
tokenQuoted = false;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const hereStrings = (): string[] => {
|
|
612
|
+
const values: string[] = [];
|
|
613
|
+
tokens.forEach((token, position) => {
|
|
614
|
+
if (!token.quoted && /^\d*<<<$/.test(token.value) && tokens[position + 1]) {
|
|
615
|
+
values.push(tokens[position + 1].value);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
return values;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const endSegment = (separator?: Separator): void => {
|
|
622
|
+
endToken();
|
|
623
|
+
let pushed = false;
|
|
624
|
+
if (tokens.length > 0) {
|
|
625
|
+
const executableTokens = stripRedirections(tokens);
|
|
626
|
+
if (executableTokens.length > 0) {
|
|
627
|
+
segments.push({
|
|
628
|
+
tokens: executableTokens,
|
|
629
|
+
separatorBefore,
|
|
630
|
+
depth,
|
|
631
|
+
substitutions,
|
|
632
|
+
heredocs,
|
|
633
|
+
hereStrings: hereStrings(),
|
|
634
|
+
});
|
|
635
|
+
pushed = true;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (!pushed) {
|
|
639
|
+
loose.push(...substitutions);
|
|
640
|
+
looseHeredocs.push(...heredocs);
|
|
641
|
+
}
|
|
642
|
+
tokens = [];
|
|
643
|
+
substitutions = [];
|
|
644
|
+
heredocs = [];
|
|
645
|
+
if (separator !== undefined && (pushed || separatorBefore === undefined)) separatorBefore = separator;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/** 단어 안의 치환을 단어 값에 그대로 두고 본문만 따로 모은다. 읽은 마지막 위치를 돌려준다. */
|
|
649
|
+
const takeSubstitution = (index: number): number => {
|
|
650
|
+
const substitution =
|
|
651
|
+
command[index] === "`" ? readBacktickSubstitution(command, index) : readCommandSubstitution(command, index);
|
|
652
|
+
substitutions.push(substitution.inner);
|
|
653
|
+
value += command.slice(index, substitution.end);
|
|
654
|
+
tokenStarted = true;
|
|
655
|
+
return substitution.end - 1;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
659
|
+
const character = command[index];
|
|
660
|
+
const next = command[index + 1];
|
|
661
|
+
const afterNext = command[index + 2];
|
|
662
|
+
|
|
663
|
+
if (
|
|
664
|
+
character === "\\" &&
|
|
665
|
+
(next === "\n" || (next === "\r" && afterNext === "\n")) &&
|
|
666
|
+
quote !== "'"
|
|
667
|
+
) {
|
|
668
|
+
index += next === "\r" ? 2 : 1;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (quote) {
|
|
673
|
+
if (character === quote) {
|
|
674
|
+
quote = undefined;
|
|
675
|
+
tokenQuoted = true;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (
|
|
680
|
+
quote === '"' &&
|
|
681
|
+
character === "\\" &&
|
|
682
|
+
next !== undefined &&
|
|
683
|
+
(next === '"' || next === "\\" || next === "$" || next === "`")
|
|
684
|
+
) {
|
|
685
|
+
value += next;
|
|
686
|
+
tokenStarted = true;
|
|
687
|
+
index += 1;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (quote === '"' && (character === "`" || (character === "$" && next === "("))) {
|
|
692
|
+
index = takeSubstitution(index);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
value += character;
|
|
697
|
+
tokenStarted = true;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (character === "'" || character === '"') {
|
|
702
|
+
quote = character;
|
|
703
|
+
tokenStarted = true;
|
|
704
|
+
tokenQuoted = true;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (character === "`" || (character === "$" && next === "(")) {
|
|
709
|
+
index = takeSubstitution(index);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (character === "\\" && next !== undefined) {
|
|
714
|
+
if (/['";|&#\\()<>]/.test(next)) {
|
|
715
|
+
value += next;
|
|
716
|
+
tokenStarted = true;
|
|
717
|
+
index += 1;
|
|
718
|
+
} else if (next === "$" || next === "`") {
|
|
719
|
+
value += character + next;
|
|
720
|
+
tokenStarted = true;
|
|
721
|
+
index += 1;
|
|
722
|
+
} else {
|
|
723
|
+
value += character;
|
|
724
|
+
tokenStarted = true;
|
|
725
|
+
}
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (character === "#" && !tokenStarted) {
|
|
730
|
+
while (index + 1 < command.length && command[index + 1] !== "\n") {
|
|
731
|
+
index += 1;
|
|
732
|
+
}
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (character === "&" && next === ">") {
|
|
736
|
+
endToken();
|
|
737
|
+
let operator = "&>";
|
|
738
|
+
index += 1;
|
|
739
|
+
if (command[index + 1] === ">") {
|
|
740
|
+
operator += ">";
|
|
741
|
+
index += 1;
|
|
742
|
+
}
|
|
743
|
+
tokens.push({ value: operator, quoted: false });
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (character === "<" || character === ">") {
|
|
748
|
+
let descriptor = "";
|
|
749
|
+
if (tokenStarted && !tokenQuoted && /^\d+$/.test(value)) {
|
|
750
|
+
descriptor = value;
|
|
751
|
+
value = "";
|
|
752
|
+
tokenStarted = false;
|
|
753
|
+
tokenQuoted = false;
|
|
754
|
+
} else {
|
|
755
|
+
endToken();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let operator = character;
|
|
759
|
+
if (
|
|
760
|
+
(character === ">" && (next === ">" || next === "|" || next === "&")) ||
|
|
761
|
+
(character === "<" && (next === "<" || next === ">" || next === "&"))
|
|
762
|
+
) {
|
|
763
|
+
operator += next;
|
|
764
|
+
index += 1;
|
|
765
|
+
if (operator === "<<" && command[index + 1] === "<") {
|
|
766
|
+
operator += "<";
|
|
767
|
+
index += 1;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
tokens.push({ value: `${descriptor}${operator}`, quoted: false });
|
|
771
|
+
if (operator === "<<") {
|
|
772
|
+
const delimiter = readHeredocDelimiter(command, index + 1);
|
|
773
|
+
if (delimiter) {
|
|
774
|
+
index = delimiter.end - 1;
|
|
775
|
+
tokens.push({ value: delimiter.heredoc.delimiter, quoted: true });
|
|
776
|
+
heredocs.push(delimiter.heredoc);
|
|
777
|
+
pendingBodies.push(delimiter.heredoc);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (character === "(" || character === ")") {
|
|
784
|
+
endSegment(";");
|
|
785
|
+
depth = character === "(" ? depth + 1 : Math.max(0, depth - 1);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
if (character === "\n") {
|
|
791
|
+
endSegment("\n");
|
|
792
|
+
if (pendingBodies.length > 0) {
|
|
793
|
+
index = readHeredocBodies(command, index + 1, pendingBodies) - 1;
|
|
794
|
+
pendingBodies = [];
|
|
795
|
+
}
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (character === "\r") {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (/\s/.test(character)) {
|
|
804
|
+
endToken();
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (character === ";") {
|
|
809
|
+
endSegment(";");
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (character === "|" || character === "&") {
|
|
814
|
+
if (next === character) {
|
|
815
|
+
endSegment(character === "|" ? "||" : "&&");
|
|
816
|
+
index += 1;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
endSegment(character === "|" ? "|" : "&");
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
value += character;
|
|
825
|
+
tokenStarted = true;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
endSegment();
|
|
829
|
+
for (const heredoc of looseHeredocs) {
|
|
830
|
+
if (!heredoc.quoted) loose.push(...heredocExpansions(heredoc.body));
|
|
831
|
+
}
|
|
832
|
+
return { segments, loose };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function commandName(value: string): string {
|
|
836
|
+
const normalized = value.replace(/\\/g, "/");
|
|
837
|
+
const basename = normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
838
|
+
return basename.replace(/\.(?:exe|cmd|bat)$/i, "");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function isEnvironmentAssignment(value: string): boolean {
|
|
842
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function skipWrapperArguments(tokens: ShellToken[], start: number, spec: WrapperSpec): number {
|
|
846
|
+
let index = start;
|
|
847
|
+
while (index < tokens.length) {
|
|
848
|
+
const value = tokens[index].value;
|
|
849
|
+
if (value === "--") {
|
|
850
|
+
index += 1;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
if (!value.startsWith("-") || value === "-") break;
|
|
854
|
+
const equalsIndex = value.indexOf("=");
|
|
855
|
+
const option = equalsIndex >= 0 ? value.slice(0, equalsIndex) : value;
|
|
856
|
+
index += equalsIndex < 0 && spec.optionsWithValue[option] ? 2 : 1;
|
|
857
|
+
}
|
|
858
|
+
return index + spec.positionals;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function resolveInvocation(tokens: ShellToken[]): Invocation | undefined {
|
|
862
|
+
let index = 0;
|
|
863
|
+
|
|
864
|
+
while (index < tokens.length && isEnvironmentAssignment(tokens[index].value)) index += 1;
|
|
865
|
+
|
|
866
|
+
while (index < tokens.length) {
|
|
867
|
+
const name = commandName(tokens[index].value);
|
|
868
|
+
|
|
869
|
+
if (INVOCATION_PREFIXES[tokens[index].value]) {
|
|
870
|
+
index += 1;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (name === "command") {
|
|
875
|
+
const wrapperIndex = index;
|
|
876
|
+
index += 1;
|
|
877
|
+
while (index < tokens.length) {
|
|
878
|
+
const option = tokens[index].value;
|
|
879
|
+
if (option === "-v" || option === "-V") return { index: wrapperIndex, name };
|
|
880
|
+
if (option === "-p" || option === "--") {
|
|
881
|
+
index += 1;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (name === "nohup") {
|
|
890
|
+
const wrapperIndex = index;
|
|
891
|
+
index += 1;
|
|
892
|
+
if (
|
|
893
|
+
tokens[index]?.value === "--help" ||
|
|
894
|
+
tokens[index]?.value === "--version"
|
|
895
|
+
) {
|
|
896
|
+
return { index: wrapperIndex, name };
|
|
897
|
+
}
|
|
898
|
+
if (tokens[index]?.value === "--") index += 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (name === "time") {
|
|
903
|
+
const wrapperIndex = index;
|
|
904
|
+
index += 1;
|
|
905
|
+
while (index < tokens.length) {
|
|
906
|
+
const value = tokens[index].value;
|
|
907
|
+
if (value === "--") {
|
|
908
|
+
index += 1;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
if (value === "--help" || value === "--version") {
|
|
912
|
+
return { index: wrapperIndex, name };
|
|
913
|
+
}
|
|
914
|
+
if (!value.startsWith("-")) break;
|
|
915
|
+
const option = value.includes("=") ? value.slice(0, value.indexOf("=")) : value;
|
|
916
|
+
if (!value.includes("=") && TIME_OPTIONS_WITH_VALUE[option]) index += 2;
|
|
917
|
+
else index += 1;
|
|
918
|
+
}
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (name === "timeout") {
|
|
923
|
+
const wrapperIndex = index;
|
|
924
|
+
index += 1;
|
|
925
|
+
while (index < tokens.length) {
|
|
926
|
+
const value = tokens[index].value;
|
|
927
|
+
if (value === "--") {
|
|
928
|
+
index += 1;
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
if (value === "--help" || value === "--version") {
|
|
932
|
+
return { index: wrapperIndex, name };
|
|
933
|
+
}
|
|
934
|
+
if (!value.startsWith("-")) break;
|
|
935
|
+
const option = value.includes("=") ? value.slice(0, value.indexOf("=")) : value;
|
|
936
|
+
if (!value.includes("=") && TIMEOUT_OPTIONS_WITH_VALUE[option]) index += 2;
|
|
937
|
+
else index += 1;
|
|
938
|
+
}
|
|
939
|
+
if (index >= tokens.length) return undefined;
|
|
940
|
+
index += 1;
|
|
941
|
+
if (tokens[index]?.value === "--") index += 1;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (COMMAND_WRAPPERS[name]) {
|
|
946
|
+
index += 1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (name === "env") {
|
|
951
|
+
index += 1;
|
|
952
|
+
while (index < tokens.length) {
|
|
953
|
+
const value = tokens[index].value;
|
|
954
|
+
if (isEnvironmentAssignment(value)) {
|
|
955
|
+
index += 1;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (ENV_OPTIONS_WITH_VALUE[value]) {
|
|
959
|
+
index += 2;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (value.startsWith("-")) {
|
|
963
|
+
index += 1;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (name === "sudo") {
|
|
972
|
+
index += 1;
|
|
973
|
+
while (index < tokens.length && tokens[index].value.startsWith("-")) {
|
|
974
|
+
const option = tokens[index].value;
|
|
975
|
+
if (SUDO_OPTIONS_WITH_VALUE[option]) {
|
|
976
|
+
index += 2;
|
|
977
|
+
} else {
|
|
978
|
+
index += 1;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const wrapper = Object.hasOwn(EXEC_WRAPPERS, name) ? EXEC_WRAPPERS[name] : undefined;
|
|
985
|
+
if (wrapper) {
|
|
986
|
+
const wrapperIndex = index;
|
|
987
|
+
index = skipWrapperArguments(tokens, index + 1, wrapper);
|
|
988
|
+
// `flock <lockfile> -c '<명령>'`은 감싼 명령이 따로 없고 문자열을 셸이 실행한다. 그 문자열은 호출한 쪽이 푼다.
|
|
989
|
+
if (name === "flock" && /^(?:-c|--command)$/.test(tokens[index]?.value ?? "")) {
|
|
990
|
+
return { index: wrapperIndex, name };
|
|
991
|
+
}
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return { index, name };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function positionalArguments(
|
|
1002
|
+
args: string[],
|
|
1003
|
+
optionsWithValue: StringLookup = CLI_OPTIONS_WITH_VALUE,
|
|
1004
|
+
): string[] {
|
|
1005
|
+
const positionals: string[] = [];
|
|
1006
|
+
let parseOptions = true;
|
|
1007
|
+
|
|
1008
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1009
|
+
const value = args[index];
|
|
1010
|
+
|
|
1011
|
+
if (parseOptions && value === "--") {
|
|
1012
|
+
parseOptions = false;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (parseOptions && value.startsWith("-")) {
|
|
1017
|
+
const option = value.includes("=") ? value.slice(0, value.indexOf("=")) : value;
|
|
1018
|
+
if (!value.includes("=") && optionsWithValue[option]) index += 1;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
positionals.push(value);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return positionals;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** git 하위 명령별로 짧은 옵션이 값을 받는 글자. 그 글자 뒤는 값이지 옵션 묶음이 아니다(`-enode_modules`, `-sSTABLE`). */
|
|
1029
|
+
const GIT_SHORT_VALUE_OPTIONS: Readonly<Record<string, string>> = {
|
|
1030
|
+
branch: "u",
|
|
1031
|
+
checkout: "bBU",
|
|
1032
|
+
clean: "e",
|
|
1033
|
+
commit: "mFCct",
|
|
1034
|
+
push: "o",
|
|
1035
|
+
restore: "sU",
|
|
1036
|
+
switch: "cC",
|
|
1037
|
+
tag: "mFu",
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
/** git 하위 명령별로 `--name value`처럼 다음 낱말을 값으로 받는 긴 옵션(`--name=value`는 값이 붙어 있어 해당 없음). */
|
|
1041
|
+
const GIT_LONG_VALUE_OPTIONS: Readonly<Record<string, readonly string[]>> = {
|
|
1042
|
+
add: ["--pathspec-from-file"],
|
|
1043
|
+
branch: ["--format", "--points-at", "--set-upstream-to", "--sort"],
|
|
1044
|
+
checkout: ["--conflict", "--inter-hunk-context", "--orphan", "--pathspec-from-file", "--unified"],
|
|
1045
|
+
clean: ["--exclude"],
|
|
1046
|
+
commit: [
|
|
1047
|
+
"--author", "--cleanup", "--date", "--file", "--fixup", "--message", "--pathspec-from-file",
|
|
1048
|
+
"--reedit-message", "--reuse-message", "--squash", "--template", "--trailer",
|
|
1049
|
+
],
|
|
1050
|
+
push: ["--exec", "--push-option", "--receive-pack", "--repo"],
|
|
1051
|
+
restore: ["--conflict", "--inter-hunk-context", "--pathspec-from-file", "--source", "--unified"],
|
|
1052
|
+
rm: ["--pathspec-from-file"],
|
|
1053
|
+
switch: ["--conflict", "--create", "--force-create", "--orphan"],
|
|
1054
|
+
tag: ["--cleanup", "--file", "--format", "--local-user", "--message", "--points-at", "--sort"],
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* 하위 명령 인자에서 옵션이 받는 값을 걷어 낸다. 값은 옵션처럼 보여도(`--exclude -n`, `-m --dry-run`, `-sSTABLE`의 `S`) 옵션이
|
|
1059
|
+
* 아니므로, 이 뒤의 `shortFlags`·`hasLongOption`이 값을 플래그로 읽지 못하게 한다. 값을 받는 짧은 옵션 글자 앞까지만 남기고,
|
|
1060
|
+
* `--` 뒤는 그대로 둔다. 긴 옵션은 git처럼 모호하지 않은 줄임말(`--exc`)도 같은 옵션으로 본다.
|
|
1061
|
+
*/
|
|
1062
|
+
function withoutOptionValues(subcommand: string, args: string[]): string[] {
|
|
1063
|
+
const shortValues = Object.hasOwn(GIT_SHORT_VALUE_OPTIONS, subcommand) ? GIT_SHORT_VALUE_OPTIONS[subcommand] : "";
|
|
1064
|
+
const longValues = Object.hasOwn(GIT_LONG_VALUE_OPTIONS, subcommand) ? GIT_LONG_VALUE_OPTIONS[subcommand] : [];
|
|
1065
|
+
const kept: string[] = [];
|
|
1066
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1067
|
+
const value = args[index];
|
|
1068
|
+
if (value === "--") return [...kept, ...args.slice(index)];
|
|
1069
|
+
if (value.startsWith("--")) {
|
|
1070
|
+
kept.push(value);
|
|
1071
|
+
const takesNext =
|
|
1072
|
+
!value.includes("=") && longValues.some((name) => name === value || (value.length >= 4 && name.startsWith(value)));
|
|
1073
|
+
if (takesNext) index += 1;
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (/^-[^-]/.test(value)) {
|
|
1077
|
+
const cut = [...value.slice(1)].findIndex((letter) => shortValues.includes(letter));
|
|
1078
|
+
if (cut < 0) {
|
|
1079
|
+
kept.push(value);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (cut > 0) kept.push(value.slice(0, cut + 1));
|
|
1083
|
+
if (cut === value.length - 2) index += 1;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
kept.push(value);
|
|
1087
|
+
}
|
|
1088
|
+
return kept;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function parseGitInvocation(tokens: ShellToken[], invocation: Invocation): GitInvocation | undefined {
|
|
1092
|
+
if (invocation.name !== "git") return undefined;
|
|
1093
|
+
|
|
1094
|
+
const values = tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1095
|
+
let index = 0;
|
|
1096
|
+
|
|
1097
|
+
while (index < values.length) {
|
|
1098
|
+
const value = values[index];
|
|
1099
|
+
if (value === "--") {
|
|
1100
|
+
index += 1;
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
if (!value.startsWith("-")) break;
|
|
1104
|
+
|
|
1105
|
+
const equalsIndex = value.indexOf("=");
|
|
1106
|
+
const option = equalsIndex >= 0 ? value.slice(0, equalsIndex) : value;
|
|
1107
|
+
const hasAttachedShortValue =
|
|
1108
|
+
(value.startsWith("-c") || value.startsWith("-C")) && value.length > 2;
|
|
1109
|
+
|
|
1110
|
+
if (
|
|
1111
|
+
equalsIndex < 0 &&
|
|
1112
|
+
!hasAttachedShortValue &&
|
|
1113
|
+
GIT_GLOBAL_OPTIONS_WITH_VALUE[option]
|
|
1114
|
+
) {
|
|
1115
|
+
index += 2;
|
|
1116
|
+
} else {
|
|
1117
|
+
index += 1;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (index >= values.length) return undefined;
|
|
1122
|
+
const subcommand = values[index].toLowerCase();
|
|
1123
|
+
return {
|
|
1124
|
+
subcommand,
|
|
1125
|
+
args: withoutOptionValues(subcommand, values.slice(index + 1)),
|
|
1126
|
+
globalArgs: values.slice(0, index),
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// `~`는 단어 맨 앞이나 assignment 꼴의 `=`·`:` 뒤에서만 셸이 확장한다. 경로 중간의 literal
|
|
1131
|
+
// `~`(8.3 짧은 이름 `RUNNER~1` 등)는 확장이 아니므로 저장소 판정으로 넘긴다.
|
|
1132
|
+
function hasShellExpansion(value: string): boolean {
|
|
1133
|
+
return /[$%`*?]|(?:^|[=:])~/.test(value);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* git이 실제로 여는 위치. `cwd`가 null이면(앞선 cd를 확정할 수 없음) 절대경로 `-C`·`--git-dir`만으로 정해질 때만
|
|
1138
|
+
* 값을 돌려주고, 그 밖에는 undefined다. undefined `cwd`는 현재 프로세스 위치(".")다.
|
|
1139
|
+
*/
|
|
1140
|
+
function gitTargetDirectory(
|
|
1141
|
+
git: GitInvocation,
|
|
1142
|
+
cwd: string | null | undefined,
|
|
1143
|
+
): string | undefined {
|
|
1144
|
+
let directory = cwd === null ? undefined : resolve(cwd ?? ".");
|
|
1145
|
+
let gitDirectoryPath: string | undefined;
|
|
1146
|
+
|
|
1147
|
+
for (let index = 0; index < git.globalArgs.length; index += 1) {
|
|
1148
|
+
const value = git.globalArgs[index];
|
|
1149
|
+
if (value === "--") break;
|
|
1150
|
+
|
|
1151
|
+
if (value === "-C" || value === "--git-dir") {
|
|
1152
|
+
const path = git.globalArgs[index + 1];
|
|
1153
|
+
if (path === undefined || hasShellExpansion(path)) return undefined;
|
|
1154
|
+
if (value === "-C") {
|
|
1155
|
+
directory = isAbsolute(path) ? resolve(path) : directory === undefined ? undefined : resolve(directory, path);
|
|
1156
|
+
} else gitDirectoryPath = path;
|
|
1157
|
+
index += 1;
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (value.startsWith("-C") && value.length > 2) {
|
|
1162
|
+
const path = value.slice(2);
|
|
1163
|
+
if (hasShellExpansion(path)) return undefined;
|
|
1164
|
+
directory = isAbsolute(path) ? resolve(path) : directory === undefined ? undefined : resolve(directory, path);
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (value.startsWith("--git-dir=")) {
|
|
1169
|
+
const path = value.slice("--git-dir=".length);
|
|
1170
|
+
if (hasShellExpansion(path)) return undefined;
|
|
1171
|
+
gitDirectoryPath = path;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const equalsIndex = value.indexOf("=");
|
|
1176
|
+
const option = equalsIndex >= 0 ? value.slice(0, equalsIndex) : value;
|
|
1177
|
+
const hasAttachedShortValue =
|
|
1178
|
+
(value.startsWith("-c") || value.startsWith("-C")) && value.length > 2;
|
|
1179
|
+
if (
|
|
1180
|
+
equalsIndex < 0 &&
|
|
1181
|
+
!hasAttachedShortValue &&
|
|
1182
|
+
GIT_GLOBAL_OPTIONS_WITH_VALUE[option]
|
|
1183
|
+
) {
|
|
1184
|
+
index += 1;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (gitDirectoryPath === undefined) return directory;
|
|
1189
|
+
if (isAbsolute(gitDirectoryPath)) return resolve(gitDirectoryPath);
|
|
1190
|
+
return directory === undefined ? undefined : resolve(directory, gitDirectoryPath);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function hasLongOption(
|
|
1194
|
+
args: string[],
|
|
1195
|
+
names: string[],
|
|
1196
|
+
minimumPrefixLength = 4,
|
|
1197
|
+
): boolean {
|
|
1198
|
+
for (const value of args) {
|
|
1199
|
+
if (value === "--") return false;
|
|
1200
|
+
const normalized = value.toLowerCase();
|
|
1201
|
+
const equalsIndex = normalized.indexOf("=");
|
|
1202
|
+
const option = equalsIndex >= 0 ? normalized.slice(0, equalsIndex) : normalized;
|
|
1203
|
+
if (!option.startsWith("--")) continue;
|
|
1204
|
+
for (const name of names) {
|
|
1205
|
+
if (
|
|
1206
|
+
option === name ||
|
|
1207
|
+
(option.length >= minimumPrefixLength && name.startsWith(option))
|
|
1208
|
+
) {
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function shortFlags(args: string[]): Set<string> {
|
|
1217
|
+
const flags = new Set<string>();
|
|
1218
|
+
for (const value of args) {
|
|
1219
|
+
if (value === "--") break;
|
|
1220
|
+
if (/^-[^-]/.test(value)) {
|
|
1221
|
+
for (const flag of value.slice(1)) flags.add(flag);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return flags;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** `git checkout .`·`git restore :/`처럼 작업 트리 전체를 가리키는 pathspec. */
|
|
1228
|
+
function isWorktreeWidePathspec(value: string): boolean {
|
|
1229
|
+
return isBroadPath(value) || /^:(?:\/\*?|\(top\)[.*]?)$/i.test(value.trim());
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* 작업 트리·stash의 변경을 되돌릴 수 없게 버리는 git 명령. 파일 하나를 지정한 복원, 브랜치 전환, `restore --staged`처럼
|
|
1234
|
+
* index만 바꾸는 명령, 새 stash 저장·적용은 막지 않는다.
|
|
1235
|
+
*/
|
|
1236
|
+
function matchWorkDiscard(git: GitInvocation): string | undefined {
|
|
1237
|
+
const flags = shortFlags(git.args);
|
|
1238
|
+
const force = flags.has("f") || hasLongOption(git.args, ["--force"], 3);
|
|
1239
|
+
const action = git.args.find((value) => !value.startsWith("-"))?.toLowerCase();
|
|
1240
|
+
|
|
1241
|
+
switch (git.subcommand) {
|
|
1242
|
+
case "checkout":
|
|
1243
|
+
return force || positionalArguments(git.args, {}).some(isWorktreeWidePathspec)
|
|
1244
|
+
? REASONS.gitDiscard
|
|
1245
|
+
: undefined;
|
|
1246
|
+
case "switch":
|
|
1247
|
+
return force || hasLongOption(git.args, ["--discard-changes"], 4) ? REASONS.gitDiscard : undefined;
|
|
1248
|
+
case "restore": {
|
|
1249
|
+
const staged = flags.has("S") || hasLongOption(git.args, ["--staged"], 4);
|
|
1250
|
+
const worktree = flags.has("W") || hasLongOption(git.args, ["--worktree"], 4);
|
|
1251
|
+
const wide = positionalArguments(git.args, {}).some(isWorktreeWidePathspec);
|
|
1252
|
+
return wide && (worktree || !staged) ? REASONS.gitDiscard : undefined;
|
|
1253
|
+
}
|
|
1254
|
+
case "stash":
|
|
1255
|
+
return action === "clear" || action === "drop" ? REASONS.gitDiscard : undefined;
|
|
1256
|
+
case "worktree":
|
|
1257
|
+
return action === "remove" && force ? REASONS.gitDiscard : undefined;
|
|
1258
|
+
default:
|
|
1259
|
+
return undefined;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function matchGit(
|
|
1264
|
+
tokens: ShellToken[],
|
|
1265
|
+
invocation: Invocation,
|
|
1266
|
+
context?: GuardContext,
|
|
1267
|
+
): string | undefined {
|
|
1268
|
+
const git = parseGitInvocation(tokens, invocation);
|
|
1269
|
+
if (!git) return undefined;
|
|
1270
|
+
|
|
1271
|
+
if (FINALIZER_OWNED_GIT_COMMANDS[git.subcommand]) {
|
|
1272
|
+
// 미리보기는 index·ref·원격을 바꾸지 않는다. commit의 `-n`은 `--no-verify`라 commit은 long option만 본다.
|
|
1273
|
+
const dryRun =
|
|
1274
|
+
(git.subcommand === "add" || git.subcommand === "commit" || git.subcommand === "push") &&
|
|
1275
|
+
(hasLongOption(git.args, ["--dry-run"]) ||
|
|
1276
|
+
(git.subcommand !== "commit" && shortFlags(git.args).has("n")));
|
|
1277
|
+
if (dryRun) return undefined;
|
|
1278
|
+
if (!context?.isSessionRepository) return REASONS.gitFinalize;
|
|
1279
|
+
const targetDirectory = gitTargetDirectory(git, context.cwd);
|
|
1280
|
+
if (!targetDirectory) return REASONS.gitFinalize;
|
|
1281
|
+
try {
|
|
1282
|
+
return context.isSessionRepository(targetDirectory)
|
|
1283
|
+
? REASONS.gitFinalize
|
|
1284
|
+
: undefined;
|
|
1285
|
+
} catch {
|
|
1286
|
+
return REASONS.gitFinalize;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (git.subcommand === "reset" && hasLongOption(git.args, ["--hard"], 3)) {
|
|
1291
|
+
return REASONS.gitReset;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (git.subcommand === "clean") {
|
|
1295
|
+
const flags = shortFlags(git.args);
|
|
1296
|
+
const force = flags.has("f") || hasLongOption(git.args, ["--force"], 3);
|
|
1297
|
+
// `-n`·`--dry-run`은 지울 목록만 보여 주므로 `-f`가 함께 있어도 아무것도 지우지 않는다.
|
|
1298
|
+
const dryRun = flags.has("n") || hasLongOption(git.args, ["--dry-run"], 3);
|
|
1299
|
+
if (force && !dryRun) return REASONS.gitClean;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const discardReason = matchWorkDiscard(git);
|
|
1303
|
+
if (discardReason) return discardReason;
|
|
1304
|
+
|
|
1305
|
+
if (git.subcommand === "rm") {
|
|
1306
|
+
const flags = shortFlags(git.args);
|
|
1307
|
+
const paths = positionalArguments(git.args);
|
|
1308
|
+
const recursive = flags.has("r") || flags.has("R") || hasLongOption(git.args, ["--recursive"], 3);
|
|
1309
|
+
const force = flags.has("f") || hasLongOption(git.args, ["--force"], 3);
|
|
1310
|
+
if (recursive && force && paths.some(isBroadPath)) return REASONS.broadRemove;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
if (git.subcommand === "branch") {
|
|
1315
|
+
const flags = shortFlags(git.args);
|
|
1316
|
+
const forcedShortDelete = flags.has("D") || (flags.has("d") && flags.has("f"));
|
|
1317
|
+
const forcedLongDelete =
|
|
1318
|
+
hasLongOption(git.args, ["--delete"], 3) && hasLongOption(git.args, ["--force"], 3);
|
|
1319
|
+
if (forcedShortDelete || forcedLongDelete) return REASONS.gitDelete;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (git.subcommand === "tag") {
|
|
1323
|
+
const flags = shortFlags(git.args);
|
|
1324
|
+
if (flags.has("d") || hasLongOption(git.args, ["--delete"], 3)) return REASONS.gitDelete;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return undefined;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* WSL에서 하네스가 Windows `git.exe`로 다루는 저장소(`/mnt/<드라이브>`)를 Linux git으로 여는 호출을 막는다.
|
|
1332
|
+
* 두 git은 index에 적는 stat 정보가 달라, 번갈아 쓰면 바뀐 쪽이 추적 파일 전체를 다시 확인하고 index를 통째로 다시 쓴다.
|
|
1333
|
+
* 2026-10-12 SJF 저장소(추적 217,935개)에서 Linux `git --no-optional-locks diff`가 그 갱신 도중 bash 30초 제한에 끊겨
|
|
1334
|
+
* `index.lock`을 남겼다. `--no-optional-locks`는 diff의 index 갱신을 막지 못한다(git 2.43 재현).
|
|
1335
|
+
* `windowsShell`은 Windows PowerShell(`*.exe`) 안의 호출이라 그 안의 `git`이 이미 Windows git이라는 뜻이다.
|
|
1336
|
+
* 앞선 cd를 확정할 수 없어도(`cwd` null) 절대경로 `-C`·`--git-dir`이면 판정하고, 그 밖에 위치를 모르면 판정하지 않는다.
|
|
1337
|
+
*/
|
|
1338
|
+
function matchLinuxGitOnWindowsDrive(
|
|
1339
|
+
tokens: ShellToken[],
|
|
1340
|
+
invocation: Invocation,
|
|
1341
|
+
context: GuardContext | undefined,
|
|
1342
|
+
cwd: string | null | undefined,
|
|
1343
|
+
windowsShell: boolean,
|
|
1344
|
+
): string | undefined {
|
|
1345
|
+
if (windowsShell || !context?.usesWindowsGit) return undefined;
|
|
1346
|
+
// commandName은 `.exe`를 떼므로 원래 토큰으로 Windows git을 구분한다.
|
|
1347
|
+
if (/\.exe$/i.test(tokens[invocation.index].value)) return undefined;
|
|
1348
|
+
const git = parseGitInvocation(tokens, invocation);
|
|
1349
|
+
if (!git || REPOSITORY_FREE_GIT_COMMANDS[git.subcommand]) return undefined;
|
|
1350
|
+
const directory = gitTargetDirectory(git, cwd);
|
|
1351
|
+
return directory !== undefined && context.usesWindowsGit(directory)
|
|
1352
|
+
? REASONS.linuxGitOnWindowsDrive
|
|
1353
|
+
: undefined;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function isBroadPath(value: string): boolean {
|
|
1357
|
+
const normalized = value.trim().replace(/\\/g, "/");
|
|
1358
|
+
const upper = normalized.toUpperCase();
|
|
1359
|
+
|
|
1360
|
+
if (BROAD_PATHS[normalized]) return true;
|
|
1361
|
+
if (BROAD_ENVIRONMENT_PATHS[upper]) return true;
|
|
1362
|
+
|
|
1363
|
+
if (/^[A-Za-z]:\/(?:\*|\.\*|\*\*)?$/.test(normalized)) return true;
|
|
1364
|
+
return /^\/\/[^/]+\/[^/]+\/?(?:\*)?$/.test(normalized);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function matchRm(args: string[]): string | undefined {
|
|
1368
|
+
let recursive = false;
|
|
1369
|
+
let force = false;
|
|
1370
|
+
const targets: string[] = [];
|
|
1371
|
+
let parseOptions = true;
|
|
1372
|
+
|
|
1373
|
+
for (const value of args) {
|
|
1374
|
+
if (parseOptions && value === "--") {
|
|
1375
|
+
parseOptions = false;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (parseOptions && value.startsWith("--")) {
|
|
1380
|
+
const option = value.toLowerCase();
|
|
1381
|
+
if (option.length >= 3 && "--recursive".startsWith(option)) recursive = true;
|
|
1382
|
+
if (option.length >= 3 && "--force".startsWith(option)) force = true;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (parseOptions && /^-[^-]/.test(value)) {
|
|
1387
|
+
const flags = value.slice(1);
|
|
1388
|
+
if (flags.includes("r") || flags.includes("R")) recursive = true;
|
|
1389
|
+
if (flags.includes("f")) force = true;
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
targets.push(value);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return recursive && force && targets.some(isBroadPath) ? REASONS.broadRemove : undefined;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function matchRemoveItem(args: string[]): string | undefined {
|
|
1400
|
+
let recursive = false;
|
|
1401
|
+
let force = false;
|
|
1402
|
+
const targets: string[] = [];
|
|
1403
|
+
|
|
1404
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1405
|
+
const value = args[index];
|
|
1406
|
+
const option = value.toLowerCase().replace(/:(?:\$?true)$/i, "");
|
|
1407
|
+
|
|
1408
|
+
if (option === "/s") {
|
|
1409
|
+
recursive = true;
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (option === "/q" || option === "/f") {
|
|
1413
|
+
force = true;
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (
|
|
1418
|
+
option === "-r" ||
|
|
1419
|
+
(option.length >= 3 && "-recurse".startsWith(option))
|
|
1420
|
+
) {
|
|
1421
|
+
recursive = true;
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
if (option === "-fo" || (option.length >= 3 && "-force".startsWith(option))) {
|
|
1425
|
+
force = true;
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
if (REMOVE_ITEM_PATH_OPTIONS[option]) {
|
|
1429
|
+
if (index + 1 < args.length) targets.push(args[(index += 1)]);
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (REMOVE_ITEM_OPTIONS_WITH_VALUE[option]) {
|
|
1433
|
+
index += 1;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (!value.startsWith("-")) targets.push(value);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const broadTarget = targets.some((target) =>
|
|
1440
|
+
target
|
|
1441
|
+
.split(",")
|
|
1442
|
+
.map((part) => part.trim())
|
|
1443
|
+
.some(isBroadPath),
|
|
1444
|
+
);
|
|
1445
|
+
return recursive && force && broadTarget ? REASONS.broadRemove : undefined;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function stripSqlLiteralsAndComments(sql: string): string {
|
|
1449
|
+
let result = "";
|
|
1450
|
+
let state: "code" | "single" | "double" | "bracket" | "backtick" | "line" | "block" =
|
|
1451
|
+
"code";
|
|
1452
|
+
|
|
1453
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
1454
|
+
const character = sql[index];
|
|
1455
|
+
const next = sql[index + 1];
|
|
1456
|
+
|
|
1457
|
+
if (state === "line") {
|
|
1458
|
+
if (character === "\n") {
|
|
1459
|
+
result += "\n";
|
|
1460
|
+
state = "code";
|
|
1461
|
+
}
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (state === "block") {
|
|
1466
|
+
if (character === "*" && next === "/") {
|
|
1467
|
+
result += " ";
|
|
1468
|
+
state = "code";
|
|
1469
|
+
index += 1;
|
|
1470
|
+
}
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (state !== "code") {
|
|
1475
|
+
const closer = state === "single" ? "'" : state === "double" ? '"' : state === "bracket" ? "]" : "`";
|
|
1476
|
+
if (character === closer) {
|
|
1477
|
+
if ((state === "single" || state === "double") && next === closer) {
|
|
1478
|
+
index += 1;
|
|
1479
|
+
} else {
|
|
1480
|
+
result += " ";
|
|
1481
|
+
state = "code";
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (character === "-" && next === "-") {
|
|
1488
|
+
state = "line";
|
|
1489
|
+
index += 1;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (character === "/" && next === "*") {
|
|
1493
|
+
state = "block";
|
|
1494
|
+
index += 1;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (character === "'") state = "single";
|
|
1498
|
+
else if (character === '"') state = "double";
|
|
1499
|
+
else if (character === "[") state = "bracket";
|
|
1500
|
+
else if (character === "`") state = "backtick";
|
|
1501
|
+
else result += character;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return result;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function containsDestructiveSql(sql: string): boolean {
|
|
1508
|
+
const executableSql = stripSqlLiteralsAndComments(sql);
|
|
1509
|
+
return /(?:^|[;\r\n])\s*(?:drop|truncate)\b/i.test(executableSql);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function queryOptionValues(
|
|
1513
|
+
args: string[],
|
|
1514
|
+
shortOptions: string[],
|
|
1515
|
+
longOptions: string[],
|
|
1516
|
+
): string[] {
|
|
1517
|
+
const queries: string[] = [];
|
|
1518
|
+
const normalizedShort = shortOptions.map((option) => option.toLowerCase());
|
|
1519
|
+
const normalizedLong = longOptions.map((option) => option.toLowerCase());
|
|
1520
|
+
|
|
1521
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1522
|
+
const value = args[index];
|
|
1523
|
+
const lower = value.toLowerCase();
|
|
1524
|
+
|
|
1525
|
+
if (normalizedShort.includes(lower) || normalizedLong.includes(lower)) {
|
|
1526
|
+
if (index + 1 < args.length) queries.push(args[(index += 1)]);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const long = normalizedLong.find((option) => lower.startsWith(`${option}=`));
|
|
1531
|
+
if (long) {
|
|
1532
|
+
queries.push(value.slice(long.length + 1));
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const short = normalizedShort.find(
|
|
1537
|
+
(option) => lower.startsWith(option) && value.length > option.length,
|
|
1538
|
+
);
|
|
1539
|
+
if (short) queries.push(value.slice(short.length));
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return queries;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* 짧은 옵션 묶음(`psql -Atc`, `mysql -Nse`, `-1c"…"`)을 앞에서부터 풀어 SQL을 받는 옵션의 값을 모은다. 값을 받는 다른
|
|
1547
|
+
* 옵션을 만나면 그 묶음의 해석을 멈추고, 값이 따로 떨어져 있으면 그 값은 건너뛴다. 글자 구분은 대소문자를 지킨다.
|
|
1548
|
+
*/
|
|
1549
|
+
function shortOptionQueries(args: string[], options: ShortQueryOptions): string[] {
|
|
1550
|
+
const queries: string[] = [];
|
|
1551
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1552
|
+
const value = args[index];
|
|
1553
|
+
if (!/^-[^-]/.test(value)) continue;
|
|
1554
|
+
for (let position = 1; position < value.length; position += 1) {
|
|
1555
|
+
const flag = value[position];
|
|
1556
|
+
if (options.withoutValue.includes(flag)) continue;
|
|
1557
|
+
const attached = value.slice(position + 1);
|
|
1558
|
+
if (options.query.includes(flag)) {
|
|
1559
|
+
if (attached) queries.push(attached);
|
|
1560
|
+
else if (index + 1 < args.length) queries.push(args[(index += 1)]);
|
|
1561
|
+
} else if (options.withValue.includes(flag) && !attached) {
|
|
1562
|
+
index += 1;
|
|
1563
|
+
}
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return queries;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function directDatabaseQueries(name: string, args: string[]): string[] {
|
|
1571
|
+
if (name === "sqlcmd") return queryOptionValues(args, ["-q"], []);
|
|
1572
|
+
if (name === "psql") {
|
|
1573
|
+
return [...shortOptionQueries(args, PSQL_SHORT_OPTIONS), ...queryOptionValues(args, [], ["--command"])];
|
|
1574
|
+
}
|
|
1575
|
+
if (name === "mysql" || name === "mariadb") {
|
|
1576
|
+
return [...shortOptionQueries(args, MYSQL_SHORT_OPTIONS), ...queryOptionValues(args, [], ["--execute"])];
|
|
1577
|
+
}
|
|
1578
|
+
if (name === "invoke-sqlcmd") {
|
|
1579
|
+
return queryOptionValues(args, [], ["-query"]);
|
|
1580
|
+
}
|
|
1581
|
+
if (name === "sqlite3" || name === "duckdb") {
|
|
1582
|
+
const commandQueries = queryOptionValues(args, ["-cmd"], []);
|
|
1583
|
+
const positionals = positionalArguments(args, SQLITE_OPTIONS_WITH_VALUE);
|
|
1584
|
+
return positionals.length > 1
|
|
1585
|
+
? [...commandQueries, positionals.slice(1).join(" ")]
|
|
1586
|
+
: commandQueries;
|
|
1587
|
+
}
|
|
1588
|
+
return [];
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/** 세그먼트의 stdin으로 들어가는 heredoc 본문과 here-string 값. */
|
|
1592
|
+
function stdinTexts(segment: ShellSegment): string[] {
|
|
1593
|
+
return [...segment.heredocs.map((heredoc) => heredoc.body), ...segment.hereStrings];
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const PRINTF_ESCAPES: Readonly<Record<string, string>> = {
|
|
1597
|
+
n: "\n",
|
|
1598
|
+
t: "\t",
|
|
1599
|
+
r: "\r",
|
|
1600
|
+
"\\": "\\",
|
|
1601
|
+
'"': '"',
|
|
1602
|
+
"'": "'",
|
|
1603
|
+
a: "\x07",
|
|
1604
|
+
b: "\b",
|
|
1605
|
+
f: "\f",
|
|
1606
|
+
v: "\v",
|
|
1607
|
+
e: "\x1b",
|
|
1608
|
+
E: "\x1b",
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
/** 서식 문자열의 이스케이프(`\NNN`은 1~3자리 8진수, `\c`는 글자 그대로). 그룹: 1 단순, 2 `\x`, 3 `\u`, 4 `\U`, 5 8진수. */
|
|
1612
|
+
const PRINTF_FORMAT_ESCAPE =
|
|
1613
|
+
/\\(?:([ntrabfveE\\"'])|x([0-9a-fA-F]{1,2})|u([0-9a-fA-F]{1,4})|U([0-9a-fA-F]{1,8})|([0-7]{1,3}))/y;
|
|
1614
|
+
/** `%b` 인자의 이스케이프(`\0NNN`, `\c`는 이후 출력 전체를 끊는다). 그룹 6이 `\c`다. */
|
|
1615
|
+
const PRINTF_ARGUMENT_ESCAPE =
|
|
1616
|
+
/\\(?:([ntrabfveE\\"'])|x([0-9a-fA-F]{1,2})|u([0-9a-fA-F]{1,4})|U([0-9a-fA-F]{1,8})|(0[0-7]{0,3}|[1-7][0-7]{0,2})|(c))/y;
|
|
1617
|
+
/** `%` 변환 명세. 그룹: 1 `%%`, 2 플래그, 3 너비, 4 정밀도, 5 변환 글자(`hh`·`l` 등 길이 수식자는 무시). */
|
|
1618
|
+
const PRINTF_SPEC = /%(?:(%)|([-+ #0]*)(\*|\d+)?(?:\.(\*|\d*))?(?:hh|h|ll|l|L|j|z|t)?([A-Za-z]))/y;
|
|
1619
|
+
/** 너비·정밀도 상한. 스캔이 거대한 문자열을 만들지 못하게 한다. */
|
|
1620
|
+
const PRINTF_MAX_WIDTH = 1024;
|
|
1621
|
+
|
|
1622
|
+
interface PrintfEscape {
|
|
1623
|
+
text: string;
|
|
1624
|
+
length: number;
|
|
1625
|
+
stop: boolean;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** `text[index]`의 역슬래시 이스케이프를 읽는다. 알 수 없는 이스케이프(서식의 `\c`·`\q`)는 undefined라 글자 그대로 둔다. */
|
|
1629
|
+
function readPrintfEscape(text: string, index: number, pattern: RegExp): PrintfEscape | undefined {
|
|
1630
|
+
pattern.lastIndex = index;
|
|
1631
|
+
const match = pattern.exec(text);
|
|
1632
|
+
if (!match) return undefined;
|
|
1633
|
+
const [whole, simple, hex, unicode, longUnicode, octal, cut] = match;
|
|
1634
|
+
if (cut !== undefined) return { text: "", length: whole.length, stop: true };
|
|
1635
|
+
let value: string;
|
|
1636
|
+
if (simple !== undefined) value = PRINTF_ESCAPES[simple];
|
|
1637
|
+
else if (hex !== undefined) value = String.fromCharCode(Number.parseInt(hex, 16));
|
|
1638
|
+
else if (octal !== undefined) value = String.fromCharCode(Number.parseInt(octal, 8) & 0xff);
|
|
1639
|
+
else {
|
|
1640
|
+
const code = Number.parseInt(unicode ?? longUnicode ?? "0", 16);
|
|
1641
|
+
value = code <= 0x10ffff ? String.fromCodePoint(code) : "\ufffd";
|
|
1642
|
+
}
|
|
1643
|
+
return { text: value, length: whole.length, stop: false };
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/** `%b` 인자를 푼다. `\c`를 만나면 거기까지만 돌려주고 `stop`으로 이후 출력 전체를 끊는다. */
|
|
1647
|
+
function expandPrintfArgument(value: string): { text: string; stop: boolean } {
|
|
1648
|
+
let text = "";
|
|
1649
|
+
let index = 0;
|
|
1650
|
+
while (index < value.length) {
|
|
1651
|
+
const escape = value[index] === "\\" ? readPrintfEscape(value, index, PRINTF_ARGUMENT_ESCAPE) : undefined;
|
|
1652
|
+
if (!escape) {
|
|
1653
|
+
text += value[index];
|
|
1654
|
+
index += 1;
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
text += escape.text;
|
|
1658
|
+
if (escape.stop) return { text, stop: true };
|
|
1659
|
+
index += escape.length;
|
|
1660
|
+
}
|
|
1661
|
+
return { text, stop: false };
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/** `%d` 류의 정수 인자. `'A`는 글자 코드, 앞의 숫자만 읽고 못 읽으면 0(printf는 오류를 내지만 0을 출력한다). */
|
|
1665
|
+
function parsePrintfInteger(raw: string | undefined): bigint {
|
|
1666
|
+
const text = raw ?? "";
|
|
1667
|
+
const character = /^['"](.)/su.exec(text);
|
|
1668
|
+
if (character) return BigInt(character[1].codePointAt(0) ?? 0);
|
|
1669
|
+
const number = /^[-+]?(?:0[xX][0-9a-fA-F]+|0[0-7]*|[1-9][0-9]*)/.exec(text);
|
|
1670
|
+
if (!number) return BigInt(0);
|
|
1671
|
+
const negative = number[0].startsWith("-");
|
|
1672
|
+
const digits = number[0].replace(/^[-+]/, "");
|
|
1673
|
+
const magnitude = BigInt(/^0[0-7]+$/.test(digits) ? `0o${digits.slice(1)}` : digits);
|
|
1674
|
+
return negative ? -magnitude : magnitude;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
interface PrintfConversion {
|
|
1678
|
+
text: string;
|
|
1679
|
+
numeric: boolean;
|
|
1680
|
+
stop: boolean;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/** 변환 글자 하나를 값에 적용한다(너비 채움은 호출자). 모르는 변환 글자는 undefined(printf는 오류로 멈춘다). */
|
|
1684
|
+
function convertPrintfValue(
|
|
1685
|
+
letter: string,
|
|
1686
|
+
raw: string | undefined,
|
|
1687
|
+
flags: string,
|
|
1688
|
+
precision: number | undefined,
|
|
1689
|
+
): PrintfConversion | undefined {
|
|
1690
|
+
const limit = (text: string): string => (precision === undefined ? text : Array.from(text).slice(0, precision).join(""));
|
|
1691
|
+
const sign = (negative: boolean): string => (negative ? "-" : flags.includes("+") ? "+" : flags.includes(" ") ? " " : "");
|
|
1692
|
+
if (letter === "s") return { text: limit(raw ?? ""), numeric: false, stop: false };
|
|
1693
|
+
if (letter === "c") return { text: Array.from(raw ?? "")[0] ?? "\0", numeric: false, stop: false };
|
|
1694
|
+
if (letter === "q") {
|
|
1695
|
+
const quoted = raw === undefined || raw === "" ? "''" : raw.replace(/[^A-Za-z0-9_@%+=:,./-]/g, "\\$&");
|
|
1696
|
+
return { text: limit(quoted), numeric: false, stop: false };
|
|
1697
|
+
}
|
|
1698
|
+
if (letter === "b") {
|
|
1699
|
+
const expanded = expandPrintfArgument(raw ?? "");
|
|
1700
|
+
return { text: limit(expanded.text), numeric: false, stop: expanded.stop };
|
|
1701
|
+
}
|
|
1702
|
+
if ("diuoxX".includes(letter)) {
|
|
1703
|
+
const value = parsePrintfInteger(raw);
|
|
1704
|
+
const signed = letter === "d" || letter === "i";
|
|
1705
|
+
const magnitude = signed ? (value < 0 ? -value : value) : BigInt.asUintN(64, value);
|
|
1706
|
+
const base = letter === "o" ? 8 : letter === "x" || letter === "X" ? 16 : 10;
|
|
1707
|
+
let digits = magnitude.toString(base);
|
|
1708
|
+
if (letter === "X") digits = digits.toUpperCase();
|
|
1709
|
+
if (precision === 0 && magnitude === BigInt(0)) digits = "";
|
|
1710
|
+
if (precision !== undefined) digits = digits.padStart(Math.min(precision, PRINTF_MAX_WIDTH), "0");
|
|
1711
|
+
let prefix = "";
|
|
1712
|
+
if (flags.includes("#") && magnitude !== BigInt(0)) prefix = base === 16 ? (letter === "X" ? "0X" : "0x") : "";
|
|
1713
|
+
if (flags.includes("#") && base === 8 && !digits.startsWith("0")) digits = `0${digits}`;
|
|
1714
|
+
return { text: (signed ? sign(value < 0) : "") + prefix + digits, numeric: true, stop: false };
|
|
1715
|
+
}
|
|
1716
|
+
if ("eEfFgGaA".includes(letter)) {
|
|
1717
|
+
// 부동소수 변환은 숫자·부호·`.`·`e`만 만들어 명령 글자를 이룰 수 없다. 값은 근사로 쓴다.
|
|
1718
|
+
const parsed = Number.parseFloat(raw ?? "");
|
|
1719
|
+
const value = Number.isFinite(parsed) ? parsed : 0;
|
|
1720
|
+
const places = Math.min(precision ?? 6, 100);
|
|
1721
|
+
const lower = letter.toLowerCase();
|
|
1722
|
+
let body: string;
|
|
1723
|
+
if (lower === "f") body = Math.abs(value).toFixed(places);
|
|
1724
|
+
else if (lower === "e") body = Math.abs(value).toExponential(places).replace(/e([+-])(\d)$/, "e$10$2");
|
|
1725
|
+
else if (lower === "g") body = String(Number(Math.abs(value).toPrecision(precision === 0 ? 1 : Math.max(1, places))));
|
|
1726
|
+
else body = `0x${Math.abs(value).toString(16)}p+0`;
|
|
1727
|
+
if (letter !== lower) body = body.toUpperCase();
|
|
1728
|
+
return { text: sign(value < 0) + body, numeric: true, stop: false };
|
|
1729
|
+
}
|
|
1730
|
+
return undefined;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* 리터럴 인자만으로 정해지는 `printf`의 출력. 서식을 읽으며 `%s %b %c %q %d %i %u %o %x %X`와 부동소수 변환을 값으로 채우고,
|
|
1735
|
+
* 플래그·너비·정밀도(`%.3s`, `%-5s`, `%*s`, `%.*s`)를 적용하며, 값이 남으면 서식을 되풀이한다(변환 없는 서식은 한 번).
|
|
1736
|
+
* `%b`의 `\c`는 이후 출력을 모두 끊고, 변환 글자가 없거나 모르는 명세는 printf처럼 거기서 멈춘다. `printf -v`는 출력이 없다.
|
|
1737
|
+
* 변수·치환으로 만든 서식·값은 정적으로 알 수 없어 글자 그대로 쓴다.
|
|
1738
|
+
*/
|
|
1739
|
+
function printfOutput(args: string[]): string {
|
|
1740
|
+
const start = args[0] === "--" ? 1 : 0;
|
|
1741
|
+
if (args[start] === "-v") return "";
|
|
1742
|
+
const [format = "", ...values] = args.slice(start);
|
|
1743
|
+
let output = "";
|
|
1744
|
+
let used = 0;
|
|
1745
|
+
let stopped = false;
|
|
1746
|
+
|
|
1747
|
+
const render = (): void => {
|
|
1748
|
+
let index = 0;
|
|
1749
|
+
while (index < format.length && !stopped) {
|
|
1750
|
+
const character = format[index];
|
|
1751
|
+
if (character === "\\") {
|
|
1752
|
+
const escape = readPrintfEscape(format, index, PRINTF_FORMAT_ESCAPE);
|
|
1753
|
+
output += escape?.text ?? "\\";
|
|
1754
|
+
index += escape?.length ?? 1;
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
if (character !== "%") {
|
|
1758
|
+
output += character;
|
|
1759
|
+
index += 1;
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
PRINTF_SPEC.lastIndex = index;
|
|
1764
|
+
const spec = PRINTF_SPEC.exec(format);
|
|
1765
|
+
if (!spec) {
|
|
1766
|
+
stopped = true;
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
index += spec[0].length;
|
|
1770
|
+
if (spec[1] !== undefined) {
|
|
1771
|
+
output += "%";
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const flags = spec[2];
|
|
1776
|
+
let width = spec[3] === "*" ? Number(parsePrintfInteger(values[used++])) : Number(spec[3] ?? 0);
|
|
1777
|
+
let precision: number | undefined;
|
|
1778
|
+
if (spec[4] === "*") precision = Number(parsePrintfInteger(values[used++]));
|
|
1779
|
+
else if (spec[4] !== undefined) precision = Number(spec[4] || 0);
|
|
1780
|
+
if (precision !== undefined && precision < 0) precision = undefined;
|
|
1781
|
+
const left = flags.includes("-") || width < 0;
|
|
1782
|
+
width = Math.min(Math.abs(width), PRINTF_MAX_WIDTH);
|
|
1783
|
+
|
|
1784
|
+
const converted = convertPrintfValue(spec[5], values[used++], flags, precision === undefined ? undefined : Math.min(precision, PRINTF_MAX_WIDTH));
|
|
1785
|
+
if (!converted) {
|
|
1786
|
+
stopped = true;
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
let { text } = converted;
|
|
1790
|
+
const padding = Math.max(0, width - Array.from(text).length);
|
|
1791
|
+
if (left) text += " ".repeat(padding);
|
|
1792
|
+
else if (flags.includes("0") && converted.numeric && !(precision !== undefined && "diuoxX".includes(spec[5]))) {
|
|
1793
|
+
const lead = /^[-+ ]?(?:0[xX])?/.exec(text)?.[0] ?? "";
|
|
1794
|
+
text = lead + "0".repeat(padding) + text.slice(lead.length);
|
|
1795
|
+
} else text = " ".repeat(padding) + text;
|
|
1796
|
+
output += text;
|
|
1797
|
+
if (converted.stop) stopped = true;
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
let before = used;
|
|
1802
|
+
render();
|
|
1803
|
+
while (!stopped && used > before && used < values.length) {
|
|
1804
|
+
before = used;
|
|
1805
|
+
render();
|
|
1806
|
+
}
|
|
1807
|
+
return output;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function pipelineText(segment: ShellSegment | undefined): string | undefined {
|
|
1811
|
+
if (!segment || segment.tokens.length === 0) return undefined;
|
|
1812
|
+
const invocation = resolveInvocation(segment.tokens);
|
|
1813
|
+
if (!invocation) return undefined;
|
|
1814
|
+
|
|
1815
|
+
if (segment.tokens.length === 1 && segment.tokens[0].quoted) {
|
|
1816
|
+
return segment.tokens[0].value;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1820
|
+
if (invocation.name === "echo" || invocation.name === "write-output") {
|
|
1821
|
+
return args.filter((value) => value !== "-n").join(" ");
|
|
1822
|
+
}
|
|
1823
|
+
if (invocation.name === "printf") return printfOutput(args);
|
|
1824
|
+
if (invocation.name === "cat" && positionalArguments(args).length === 0) {
|
|
1825
|
+
const stdin = stdinTexts(segment);
|
|
1826
|
+
return stdin.length > 0 ? stdin.join("\n") : undefined;
|
|
1827
|
+
}
|
|
1828
|
+
return undefined;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function matchDatabase(
|
|
1832
|
+
segment: ShellSegment,
|
|
1833
|
+
invocation: Invocation,
|
|
1834
|
+
previousSegment: ShellSegment | undefined,
|
|
1835
|
+
): string | undefined {
|
|
1836
|
+
if (!DB_CLIENTS[invocation.name]) return undefined;
|
|
1837
|
+
|
|
1838
|
+
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1839
|
+
const queries = directDatabaseQueries(invocation.name, args);
|
|
1840
|
+
if (queries.some(containsDestructiveSql)) return REASONS.database;
|
|
1841
|
+
if (stdinTexts(segment).some(containsDestructiveSql)) return REASONS.database;
|
|
1842
|
+
|
|
1843
|
+
if (segment.separatorBefore === "|") {
|
|
1844
|
+
const pipedText = pipelineText(previousSegment);
|
|
1845
|
+
if (pipedText && containsDestructiveSql(pipedText)) return REASONS.database;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return undefined;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function isDeployScriptName(value: string): boolean {
|
|
1852
|
+
const normalized = value.replace(/\\/g, "/").toLowerCase();
|
|
1853
|
+
const basename = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
1854
|
+
if (basename === "deploy" || basename === "release" || basename === "deploy-live") return true;
|
|
1855
|
+
return /^(?:deploy|release)(?:[-_](?:canary|live|preview|prod|production|staging))?\.(?:bat|cmd|cjs|js|mjs|ps1|py|sh|ts)$/.test(
|
|
1856
|
+
basename,
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function isDeployScriptLabel(value: string): boolean {
|
|
1861
|
+
return /^(?:pre|post)?(?:deploy|release)(?:$|[:_-])/i.test(value);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function isWranglerName(value: string): boolean {
|
|
1865
|
+
return commandName(value).replace(/@[^/]*$/, "") === "wrangler";
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/** 패키지 러너·매니저 인자에서 `wrangler` 도구 이름 뒤의 인자를 돌려준다(`@version`·`dlx`·`exec`·`x` 허용). */
|
|
1869
|
+
function argumentsAfterWrangler(args: string[], leaders: StringLookup): string[] | undefined {
|
|
1870
|
+
let index = 0;
|
|
1871
|
+
let leaderSeen = false;
|
|
1872
|
+
while (index < args.length) {
|
|
1873
|
+
const value = args[index];
|
|
1874
|
+
if (value === "--") {
|
|
1875
|
+
index += 1;
|
|
1876
|
+
} else if (value.startsWith("-")) {
|
|
1877
|
+
index += !value.includes("=") && PACKAGE_RUNNER_OPTIONS_WITH_VALUE[value] ? 2 : 1;
|
|
1878
|
+
} else if (isWranglerName(value)) {
|
|
1879
|
+
return args.slice(index + 1);
|
|
1880
|
+
} else if (!leaderSeen && leaders[value.toLowerCase()]) {
|
|
1881
|
+
leaderSeen = true;
|
|
1882
|
+
index += 1;
|
|
1883
|
+
} else {
|
|
1884
|
+
return undefined;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return undefined;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/** `wrangler` 뒤 위치 인자(옵션 값 제외)가 배포 하위 명령인지. */
|
|
1891
|
+
function isWranglerDeploy(args: string[]): boolean {
|
|
1892
|
+
const [first, second] = positionalArguments(args, WRANGLER_OPTIONS_WITH_VALUE).map((value) => value.toLowerCase());
|
|
1893
|
+
if (first === "deploy" || first === "publish") return true;
|
|
1894
|
+
if (first === "versions" || first === "triggers") return second === "deploy";
|
|
1895
|
+
return first === "pages" && (second === "deploy" || second === "publish");
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/** PowerShell 옵션(값을 받는 옵션 포함)을 지난 첫 위치 인자. `-Command`·`-File`에서 멈춘다(각각 따로 판정한다). */
|
|
1899
|
+
function firstPowerShellPositional(args: string[]): string | undefined {
|
|
1900
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1901
|
+
const option = args[index].toLowerCase();
|
|
1902
|
+
if (!option.startsWith("-")) return args[index];
|
|
1903
|
+
if (POWERSHELL_COMMAND_OPTIONS[option] || option === "-f" || option === "-file") return undefined;
|
|
1904
|
+
if (POWERSHELL_OPTIONS_WITH_VALUE[option]) index += 1;
|
|
1905
|
+
}
|
|
1906
|
+
return undefined;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function matchDeployment(tokens: ShellToken[], invocation: Invocation): string | undefined {
|
|
1910
|
+
const args = tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
1911
|
+
const positionals = positionalArguments(args);
|
|
1912
|
+
|
|
1913
|
+
if (invocation.name === "wrangler" && isWranglerDeploy(args)) {
|
|
1914
|
+
return REASONS.deploy;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (PACKAGE_EXECUTORS[invocation.name] || PACKAGE_MANAGERS[invocation.name]) {
|
|
1918
|
+
const afterTool = argumentsAfterWrangler(args, PACKAGE_EXECUTORS[invocation.name] ? {} : PACKAGE_MANAGER_EXEC_LEADERS);
|
|
1919
|
+
if (afterTool && isWranglerDeploy(afterTool)) return REASONS.deploy;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
if (PACKAGE_MANAGERS[invocation.name]) {
|
|
1923
|
+
const action = positionals[0]?.toLowerCase();
|
|
1924
|
+
if (action === "publish" || (action === "npm" && positionals[1]?.toLowerCase() === "publish")) {
|
|
1925
|
+
return REASONS.deploy;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if ((action === "run" || action === "run-script") && isDeployScriptLabel(positionals[1] ?? "")) {
|
|
1929
|
+
return REASONS.deploy;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (invocation.name === "yarn" && action && isDeployScriptLabel(action)) {
|
|
1933
|
+
return REASONS.deploy;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
const action = positionals[0]?.toLowerCase();
|
|
1938
|
+
if (
|
|
1939
|
+
((invocation.name === "cargo" ||
|
|
1940
|
+
invocation.name === "deno" ||
|
|
1941
|
+
invocation.name === "poetry" ||
|
|
1942
|
+
invocation.name === "uv") &&
|
|
1943
|
+
action === "publish") ||
|
|
1944
|
+
(invocation.name === "twine" && action === "upload") ||
|
|
1945
|
+
(invocation.name === "gem" && action === "push") ||
|
|
1946
|
+
(invocation.name === "dotnet" &&
|
|
1947
|
+
action === "nuget" &&
|
|
1948
|
+
positionals[1]?.toLowerCase() === "push")
|
|
1949
|
+
) {
|
|
1950
|
+
return REASONS.deploy;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
if (invocation.name === "gh" && action === "release") {
|
|
1954
|
+
const releaseAction = positionals[1]?.toLowerCase();
|
|
1955
|
+
if (releaseAction && GH_RELEASE_MUTATIONS[releaseAction]) return REASONS.deploy;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
if (DEPLOY_TARGET_RUNNERS[invocation.name] && positionals.some(isDeployScriptLabel)) {
|
|
1959
|
+
return REASONS.deploy;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
if (isDeployScriptName(tokens[invocation.index].value)) return REASONS.deploy;
|
|
1963
|
+
|
|
1964
|
+
if (SCRIPT_INTERPRETERS[invocation.name]) {
|
|
1965
|
+
if (positionals[0] && isDeployScriptName(positionals[0])) return REASONS.deploy;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
if (invocation.name === "powershell" || invocation.name === "pwsh") {
|
|
1969
|
+
const fileIndex = args.findIndex((value) => {
|
|
1970
|
+
const option = value.toLowerCase();
|
|
1971
|
+
return option === "-f" || option === "-file";
|
|
1972
|
+
});
|
|
1973
|
+
if (fileIndex >= 0 && args[fileIndex + 1] && isDeployScriptName(args[fileIndex + 1])) {
|
|
1974
|
+
return REASONS.deploy;
|
|
1975
|
+
}
|
|
1976
|
+
// `pwsh Tools/x/deploy-live.ps1`: 옵션을 지난 첫 위치 인자는 실행할 스크립트다.
|
|
1977
|
+
const script = firstPowerShellPositional(args);
|
|
1978
|
+
if (script && isDeployScriptName(script)) return REASONS.deploy;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
return undefined;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/** `powershell -Command`의 명령 문자열. */
|
|
1985
|
+
function powerShellCommand(args: string[]): string | undefined {
|
|
1986
|
+
const commandIndex = args.findIndex(
|
|
1987
|
+
(value) => Boolean(POWERSHELL_COMMAND_OPTIONS[value.toLowerCase()]),
|
|
1988
|
+
);
|
|
1989
|
+
if (commandIndex < 0 || commandIndex + 1 >= args.length) return undefined;
|
|
1990
|
+
return args.slice(commandIndex + 1).join(" ");
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* `bash -lc '…'`·`bash -o pipefail -c '…'`처럼 옵션을 지난 첫 위치 인자를 명령 문자열로 쓰는 셸의 인자 해석.
|
|
1995
|
+
* `-c`가 없고 스크립트 파일도 없으면(또는 `-s`·`-`) 셸은 stdin을 읽는다.
|
|
1996
|
+
*/
|
|
1997
|
+
function parseShellArguments(args: string[]): { commandString?: string; readsStdin: boolean } {
|
|
1998
|
+
let commandFlag = false;
|
|
1999
|
+
let stdinFlag = false;
|
|
2000
|
+
let index = 0;
|
|
2001
|
+
for (; index < args.length; index += 1) {
|
|
2002
|
+
const value = args[index];
|
|
2003
|
+
if (value === "--") {
|
|
2004
|
+
index += 1;
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
if (value.length < 2 || !/^[-+]/.test(value)) break;
|
|
2008
|
+
if (value === "--rcfile" || value === "--init-file") {
|
|
2009
|
+
index += 1;
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
if (/^-[A-Za-z]+$/.test(value)) {
|
|
2013
|
+
if (value.includes("c")) commandFlag = true;
|
|
2014
|
+
if (value.includes("s")) stdinFlag = true;
|
|
2015
|
+
}
|
|
2016
|
+
if (/^[-+][A-Za-z]*[oO]$/.test(value)) index += 1;
|
|
2017
|
+
}
|
|
2018
|
+
const rest = args.slice(index);
|
|
2019
|
+
if (commandFlag) return { commandString: rest[0], readsStdin: false };
|
|
2020
|
+
return { readsStdin: stdinFlag || rest.length === 0 || rest[0] === "-" };
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/** `su -c '…'`·`script -qc '…'`·`flock <lock> -c '…'`의 명령 문자열. */
|
|
2024
|
+
function commandOptionValue(args: string[]): string | undefined {
|
|
2025
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2026
|
+
const value = args[index];
|
|
2027
|
+
if (value.startsWith("--command=")) return value.slice("--command=".length);
|
|
2028
|
+
if (value === "--command" || /^-[A-Za-z]*c$/.test(value)) return args[index + 1];
|
|
2029
|
+
}
|
|
2030
|
+
return undefined;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/** `watch -n 5 <명령…>`은 이어 붙인 명령 전체를 `sh -c`로 실행한다. */
|
|
2034
|
+
function watchCommand(args: string[]): string {
|
|
2035
|
+
let index = 0;
|
|
2036
|
+
while (index < args.length) {
|
|
2037
|
+
const value = args[index];
|
|
2038
|
+
if (value === "--") {
|
|
2039
|
+
index += 1;
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
if (!value.startsWith("-")) break;
|
|
2043
|
+
index += value === "-n" || value === "--interval" ? 2 : 1;
|
|
2044
|
+
}
|
|
2045
|
+
return args.slice(index).join(" ");
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* 이 세그먼트가 문자열로 실행하게 만드는 하위 스크립트 전부. 명령 치환, 확장이 일어나는 heredoc 본문의 치환,
|
|
2050
|
+
* `bash -c '…'`·`eval`·`su -c`·`watch` 같은 리터럴 명령 문자열, stdin을 읽는 셸에 먹이는 heredoc·here-string·파이프 텍스트.
|
|
2051
|
+
* 변수·동적으로 만든 문자열(`bash -c "$cmd"`)은 정적으로 알 수 없으므로 본문 그대로 넘어가 아무것도 찾지 못한다.
|
|
2052
|
+
*/
|
|
2053
|
+
function nestedScripts(
|
|
2054
|
+
segment: ShellSegment,
|
|
2055
|
+
invocation: Invocation | undefined,
|
|
2056
|
+
previousSegment: ShellSegment | undefined,
|
|
2057
|
+
): string[] {
|
|
2058
|
+
const scripts = [...segment.substitutions];
|
|
2059
|
+
for (const heredoc of segment.heredocs) {
|
|
2060
|
+
if (!heredoc.quoted) scripts.push(...heredocExpansions(heredoc.body));
|
|
2061
|
+
}
|
|
2062
|
+
if (!invocation) return scripts;
|
|
2063
|
+
|
|
2064
|
+
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
2065
|
+
const { name } = invocation;
|
|
2066
|
+
if (name === "powershell" || name === "pwsh") {
|
|
2067
|
+
const command = powerShellCommand(args);
|
|
2068
|
+
if (command) scripts.push(command);
|
|
2069
|
+
} else if (SHELLS[name]) {
|
|
2070
|
+
const shell = parseShellArguments(args);
|
|
2071
|
+
if (shell.commandString !== undefined) {
|
|
2072
|
+
scripts.push(shell.commandString);
|
|
2073
|
+
} else if (shell.readsStdin) {
|
|
2074
|
+
scripts.push(...stdinTexts(segment));
|
|
2075
|
+
const piped = segment.separatorBefore === "|" ? pipelineText(previousSegment) : undefined;
|
|
2076
|
+
if (piped) scripts.push(piped);
|
|
2077
|
+
}
|
|
2078
|
+
} else if (name === "eval") {
|
|
2079
|
+
if (args.length > 0) scripts.push(args.join(" "));
|
|
2080
|
+
} else if (name === "watch") {
|
|
2081
|
+
const command = watchCommand(args);
|
|
2082
|
+
if (command) scripts.push(command);
|
|
2083
|
+
} else if (name === "su" || name === "script" || name === "flock") {
|
|
2084
|
+
const command = commandOptionValue(args);
|
|
2085
|
+
if (command !== undefined) scripts.push(command);
|
|
2086
|
+
}
|
|
2087
|
+
return scripts;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* 명령이 실제로 부르는 실행 파일 이름(따옴표를 벗긴 첫 낱말) 전부. 따옴표 안 언급·주석·heredoc 본문의 글자는 포함하지 않고,
|
|
2092
|
+
* 명령 치환·`bash -c '…'`·래퍼 뒤의 명령은 포함한다. 부르는 낱말이 `"$S"`처럼 변수면 무엇인지 정적으로 알 수 없으므로,
|
|
2093
|
+
* 같은 명령에서 앞서 `S=…`로 대입한 값을 그 호출 후보로 함께 돌려준다. WSL interop이 Windows 프로그램 호출을 알아보는 데 쓴다.
|
|
2094
|
+
*/
|
|
2095
|
+
export function invokedCommands(command: string): string[] {
|
|
2096
|
+
const invoked: string[] = [];
|
|
2097
|
+
const assigned: string[] = [];
|
|
2098
|
+
const visit = (text: string, depth: number): void => {
|
|
2099
|
+
const tokenized = tokenizeShell(text);
|
|
2100
|
+
const descend = (script: string): void => {
|
|
2101
|
+
if (depth < MAX_NESTING) visit(script, depth + 1);
|
|
2102
|
+
};
|
|
2103
|
+
tokenized.loose.forEach(descend);
|
|
2104
|
+
tokenized.segments.forEach((segment, index) => {
|
|
2105
|
+
for (const token of segment.tokens) {
|
|
2106
|
+
if (!isEnvironmentAssignment(token.value)) break;
|
|
2107
|
+
assigned.push(token.value.slice(token.value.indexOf("=") + 1));
|
|
2108
|
+
}
|
|
2109
|
+
const invocation = resolveInvocation(segment.tokens);
|
|
2110
|
+
nestedScripts(segment, invocation, tokenized.segments[index - 1]).forEach(descend);
|
|
2111
|
+
if (!invocation) return;
|
|
2112
|
+
const word = segment.tokens[invocation.index].value;
|
|
2113
|
+
invoked.push(word);
|
|
2114
|
+
if (word.startsWith("$")) invoked.push(...assigned);
|
|
2115
|
+
if (invocation.name === "find") {
|
|
2116
|
+
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
2117
|
+
args.forEach((value, position) => {
|
|
2118
|
+
if (/^-(?:exec|execdir|ok|okdir)$/.test(value) && args[position + 1]) invoked.push(args[position + 1]);
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
};
|
|
2123
|
+
visit(command, 0);
|
|
2124
|
+
return invoked;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function matchBlockedCommandInternal(
|
|
2128
|
+
command: string,
|
|
2129
|
+
depth: number,
|
|
2130
|
+
context: GuardContext | undefined,
|
|
2131
|
+
windowsShell: boolean,
|
|
2132
|
+
): string | undefined {
|
|
2133
|
+
const tokenized = tokenizeShell(command);
|
|
2134
|
+
const segments = tokenized.segments;
|
|
2135
|
+
const descend = (script: string, scriptContext: GuardContext | undefined, shell: boolean): string | undefined =>
|
|
2136
|
+
depth < MAX_NESTING ? matchBlockedCommandInternal(script, depth + 1, scriptContext, shell) : undefined;
|
|
2137
|
+
for (const script of tokenized.loose) {
|
|
2138
|
+
const reason = descend(script, context, windowsShell);
|
|
2139
|
+
if (reason) return reason;
|
|
2140
|
+
}
|
|
2141
|
+
// `cd <리터럴 경로>` 뒤 `&&`로만 이어진 호출은 그 경로에서 돈다. `;`·`||`·`|`·`&`·줄바꿈으로 이어지면 cd가 실패해도
|
|
2142
|
+
// 다음 호출이 돌 수 있어 위치를 모르는 것(null)으로 보고 finalizer 소유 git을 막는다. 괄호 subshell의 cd는 닫히면
|
|
2143
|
+
// 사라진다. GIT_DIR·GIT_WORK_TREE를 쓰는 명령은 위치가 저장소를 정하지 않으므로 cd를 따라가지 않는다.
|
|
2144
|
+
const followCd = !/\bGIT_(?:DIR|WORK_TREE)\b/.test(command);
|
|
2145
|
+
const cwds: (string | null | undefined)[] = [context?.cwd];
|
|
2146
|
+
const moved: boolean[] = [false];
|
|
2147
|
+
|
|
2148
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
2149
|
+
const segment = segments[index];
|
|
2150
|
+
for (let level = cwds.length; level <= segment.depth; level += 1) {
|
|
2151
|
+
cwds[level] = cwds[level - 1];
|
|
2152
|
+
moved[level] = moved[level - 1];
|
|
2153
|
+
}
|
|
2154
|
+
cwds.length = moved.length = segment.depth + 1;
|
|
2155
|
+
if (index > 0 && segment.separatorBefore !== "&&" && moved[segment.depth]) cwds[segment.depth] = null;
|
|
2156
|
+
|
|
2157
|
+
const invocation = resolveInvocation(segment.tokens);
|
|
2158
|
+
const segmentCwd = cwds[segment.depth];
|
|
2159
|
+
const gitContext = segmentCwd === null ? undefined : context && { ...context, cwd: segmentCwd };
|
|
2160
|
+
// 하위 스크립트는 이 세그먼트가 도는 위치에서 실행된다. 이 세그먼트의 `cd`는 그 뒤에 적용한다.
|
|
2161
|
+
const nestedWindowsShell =
|
|
2162
|
+
windowsShell ||
|
|
2163
|
+
Boolean(
|
|
2164
|
+
invocation &&
|
|
2165
|
+
(invocation.name === "powershell" || invocation.name === "pwsh") &&
|
|
2166
|
+
/\.exe$/i.test(segment.tokens[invocation.index].value),
|
|
2167
|
+
);
|
|
2168
|
+
for (const script of nestedScripts(segment, invocation, segments[index - 1])) {
|
|
2169
|
+
const reason = descend(script, gitContext, nestedWindowsShell);
|
|
2170
|
+
if (reason) return reason;
|
|
2171
|
+
}
|
|
2172
|
+
if (!invocation) continue;
|
|
2173
|
+
|
|
2174
|
+
if (invocation.name === "cd" || invocation.name === "pushd" || invocation.name === "popd") {
|
|
2175
|
+
const targets = segment.tokens
|
|
2176
|
+
.slice(invocation.index + 1)
|
|
2177
|
+
.map((token) => token.value)
|
|
2178
|
+
.filter((value) => value !== "-L" && value !== "-P" && value !== "--");
|
|
2179
|
+
const from = cwds[segment.depth];
|
|
2180
|
+
const target = targets[0];
|
|
2181
|
+
const literal =
|
|
2182
|
+
followCd && invocation.name === "cd" && targets.length === 1 && target !== "-" && !hasShellExpansion(target);
|
|
2183
|
+
cwds[segment.depth] = !literal
|
|
2184
|
+
? null
|
|
2185
|
+
: from === null
|
|
2186
|
+
? isAbsolute(target) ? resolve(target) : null
|
|
2187
|
+
: resolve(from ?? ".", target);
|
|
2188
|
+
moved[segment.depth] = true;
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const gitReason = matchGit(segment.tokens, invocation, gitContext);
|
|
2193
|
+
if (gitReason) return gitReason;
|
|
2194
|
+
const linuxGitReason = matchLinuxGitOnWindowsDrive(segment.tokens, invocation, context, segmentCwd, windowsShell);
|
|
2195
|
+
if (linuxGitReason) return linuxGitReason;
|
|
2196
|
+
|
|
2197
|
+
const args = segment.tokens.slice(invocation.index + 1).map((token) => token.value);
|
|
2198
|
+
if (invocation.name === "rm") {
|
|
2199
|
+
const reason = matchRm(args);
|
|
2200
|
+
if (reason) return reason;
|
|
2201
|
+
}
|
|
2202
|
+
if (
|
|
2203
|
+
invocation.name === "remove-item" ||
|
|
2204
|
+
invocation.name === "del" ||
|
|
2205
|
+
invocation.name === "erase" ||
|
|
2206
|
+
invocation.name === "rd" ||
|
|
2207
|
+
invocation.name === "ri" ||
|
|
2208
|
+
invocation.name === "rm" ||
|
|
2209
|
+
invocation.name === "rmdir"
|
|
2210
|
+
) {
|
|
2211
|
+
const reason = matchRemoveItem(args);
|
|
2212
|
+
if (reason) return reason;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
const databaseReason = matchDatabase(segment, invocation, segments[index - 1]);
|
|
2216
|
+
if (databaseReason) return databaseReason;
|
|
2217
|
+
|
|
2218
|
+
const deployReason = matchDeployment(segment.tokens, invocation);
|
|
2219
|
+
if (deployReason) return deployReason;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
return undefined;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
function unescapePosix(command: string): string {
|
|
2226
|
+
let result = "";
|
|
2227
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
2228
|
+
const character = command[index];
|
|
2229
|
+
if (character === "<" && command[index + 1] === "<" && command[index + 2] !== "<" && command[index - 1] !== "<") {
|
|
2230
|
+
const wordStart = heredocWordStart(command, index + 2).index;
|
|
2231
|
+
const wordEnd = scanHeredocWord(command, wordStart).end;
|
|
2232
|
+
result += command.slice(index, wordEnd);
|
|
2233
|
+
index = wordEnd - 1;
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
if (character === "\\" && index + 1 < command.length && !/[\r\n$`]/.test(command[index + 1])) {
|
|
2237
|
+
result += command[index + 1];
|
|
2238
|
+
index += 1;
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
result += character;
|
|
2242
|
+
}
|
|
2243
|
+
return result;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
export function matchBlockedCommand(
|
|
2247
|
+
command: string,
|
|
2248
|
+
context?: GuardContext,
|
|
2249
|
+
): string | undefined {
|
|
2250
|
+
const directReason = matchBlockedCommandInternal(command, 0, context, false);
|
|
2251
|
+
if (directReason) return directReason;
|
|
2252
|
+
|
|
2253
|
+
// 역슬래시를 POSIX처럼 벗긴 2차 판정. `\$`·`\``는 치환이 일어나지 않게 막은 것이고, heredoc 구분자 낱말(`<<\EOF`,
|
|
2254
|
+
// `<<E\OF`, `<<'E'OF`)의 `\`·따옴표는 본문을 확장 없이 그대로 두라는 표시라 둘 다 벗기지 않는다.
|
|
2255
|
+
const posixUnescaped = unescapePosix(command);
|
|
2256
|
+
if (posixUnescaped === command) return undefined;
|
|
2257
|
+
return matchBlockedCommandInternal(posixUnescaped, 0, context, false);
|
|
2258
|
+
}
|