@pugi/cli 0.1.0-beta.9 → 0.1.0-beta.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/LICENSE +1 -1
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +3 -3
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +13 -13
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +333 -7
- package/dist/core/edits/format-detector.js +260 -0
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +5 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +29 -29
- package/dist/core/engine/anvil-client.js +214 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +129 -19
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1731 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +46 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +216 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/hooks/worktree-events.js +158 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +551 -41
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/server-detect.js +173 -0
- package/dist/core/lsp/symbol-cache.js +162 -0
- package/dist/core/lsp/symbol-tools.js +664 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2148 -217
- package/dist/core/repl/slash-commands.js +501 -41
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +324 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +30 -30
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree/include-parser.js +249 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +36 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4185 -549
- package/dist/runtime/commands/agents.js +31 -31
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +73 -39
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +27 -4
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +187 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +200 -38
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +12 -12
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +8 -8
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +22 -22
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +89 -28
- package/dist/tools/ask-user-question.js +337 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +377 -1
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +86 -4
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-chips.js +315 -0
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +36 -1
- package/dist/tui/repl-render.js +176 -25
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +125 -45
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/package.json +31 -16
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +12 -0
- package/test/scenarios/identity.scenario.txt +12 -0
- package/test/scenarios/persona-handoff.scenario.txt +12 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backlog — CLAUDE.md `@import` + `paths:` glob
|
|
3
|
+
* rule loader, the upstream tool parity for the ambient-context
|
|
4
|
+
* pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Why this module is separate from `core/context/markdown-loader.ts`:
|
|
7
|
+
*
|
|
8
|
+
* - `markdown-loader.ts` is the *workspace-bounded* loader. Its
|
|
9
|
+
* `@import` rejects anything that escapes the workspace root, and
|
|
10
|
+
* it inlines imports into the body without preserving per-rule
|
|
11
|
+
* metadata. That model fits PUGI.md / AGENTS.md at the workspace
|
|
12
|
+
* root but does NOT match the upstream tool's CLAUDE.md semantics where
|
|
13
|
+
* `@~/global.md` (homedir-anchored) is a first-class citizen.
|
|
14
|
+
*
|
|
15
|
+
* - This module implements the *home-aware* CLAUDE.md loader the
|
|
16
|
+
* ambient walk-up needs. It walks `@import` directives anchored to
|
|
17
|
+
* `./`, `../`, or `~/`, refuses bare or absolute paths, and
|
|
18
|
+
* surfaces `paths:` frontmatter glob arrays so downstream layers
|
|
19
|
+
* can decide per-rule whether to inject a given rule based on the
|
|
20
|
+
* files the operator is editing right now.
|
|
21
|
+
*
|
|
22
|
+
* Contract recap (see backlog task):
|
|
23
|
+
*
|
|
24
|
+
* - `loadRulesFile(entryPath, opts)` returns one `LoadedRule` per
|
|
25
|
+
* loaded markdown file. The first entry is always the entry file;
|
|
26
|
+
* subsequent entries are recursively-imported children in DFS
|
|
27
|
+
* order. Each rule's `body` has its frontmatter stripped and
|
|
28
|
+
* `@import` lines removed. Imports are NOT inlined into the
|
|
29
|
+
* parent's body — the caller decides how to compose the multi-file
|
|
30
|
+
* rule set.
|
|
31
|
+
*
|
|
32
|
+
* - `@import` resolution:
|
|
33
|
+
* `@./foo.md` → relative to dirname(currentFile)
|
|
34
|
+
* `@../foo.md` → relative parent
|
|
35
|
+
* `@~/foo.md` → relative to options.homedir
|
|
36
|
+
* Absolute (`@/...`) and bare (`@org/pkg`) imports are rejected.
|
|
37
|
+
*
|
|
38
|
+
* - Cycle detection through `realpathSync` so symlink loops trip the
|
|
39
|
+
* visited-set even when they go via different surface paths.
|
|
40
|
+
*
|
|
41
|
+
* - Hard hop cap `maxHops` (default 4); throws `RuleImportError` on
|
|
42
|
+
* overflow. Each loaded file counts as one hop — the entry file is
|
|
43
|
+
* hop 1, its direct `@import` children are hop 2, and so on. A
|
|
44
|
+
* chain of 5 files (A → B → C → D → E) trips the default 4-hop
|
|
45
|
+
* cap on hop 5 (file E).
|
|
46
|
+
*
|
|
47
|
+
* - Byte caps: per-file `maxFileBytes` (default 256 KiB) and
|
|
48
|
+
* aggregate `maxTotalBytes` (default 1 MiB) refuse the load.
|
|
49
|
+
*
|
|
50
|
+
* - Minimal YAML frontmatter parser scoped strictly to
|
|
51
|
+
* `paths: [glob, glob, ...]`. No dependency on a full YAML lib.
|
|
52
|
+
*
|
|
53
|
+
* - `pathMatchesRule(filePath, rule)` is pure (no I/O): given a rule
|
|
54
|
+
* and a candidate path, it returns true when ANY of the rule's
|
|
55
|
+
* `paths` globs match. A rule with no `paths` matches every path
|
|
56
|
+
* (sentinel "rule applies globally").
|
|
57
|
+
*
|
|
58
|
+
* Determinism:
|
|
59
|
+
*
|
|
60
|
+
* - Glob matching uses a hand-rolled converter to a `RegExp`
|
|
61
|
+
* supporting `**` (any depth), `*` (single segment wildcard), `?`
|
|
62
|
+
* (single char), and `[abc]` character classes. We deliberately do
|
|
63
|
+
* NOT pull in `node:fs.globSync` for matching — globSync is an fs
|
|
64
|
+
* walker, not a string matcher, and shimming it would re-introduce
|
|
65
|
+
* I/O into `pathMatchesRule` which the contract forbids.
|
|
66
|
+
*
|
|
67
|
+
* - All paths are canonicalized through `path.resolve` then
|
|
68
|
+
* `realpathSync` BEFORE the visited-set check so an A → symlink → A
|
|
69
|
+
* loop is detected the same way as a direct A → A loop.
|
|
70
|
+
*
|
|
71
|
+
* - UTF-8 BOM is stripped from every loaded file before frontmatter
|
|
72
|
+
* parse — operators sometimes save CLAUDE.md via Windows tools
|
|
73
|
+
* that prepend `` and a BOM-prefixed `---` would fail the
|
|
74
|
+
* frontmatter detection.
|
|
75
|
+
*
|
|
76
|
+
* No I/O, no fs writes; only `readFileSync` + `realpathSync` +
|
|
77
|
+
* `statSync`. No new dependencies.
|
|
78
|
+
*/
|
|
79
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
80
|
+
import { dirname, isAbsolute, posix, resolve, sep } from 'node:path';
|
|
81
|
+
/**
|
|
82
|
+
* Default per-file byte cap. 256 KiB is generous for an operator-
|
|
83
|
+
* authored CLAUDE.md (typical 5-20 KiB) while keeping a runaway
|
|
84
|
+
* generated file from blowing the prompt budget on its own.
|
|
85
|
+
*/
|
|
86
|
+
export const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
87
|
+
/**
|
|
88
|
+
* Default aggregate byte cap across the entire import graph. 1 MiB is
|
|
89
|
+
* higher than any realistic CLAUDE.md graph and exists as a hard floor
|
|
90
|
+
* against pathological imports (e.g. an accidentally-imported full
|
|
91
|
+
* source-code dump).
|
|
92
|
+
*/
|
|
93
|
+
export const DEFAULT_MAX_TOTAL_BYTES = 1024 * 1024;
|
|
94
|
+
/**
|
|
95
|
+
* Default hop cap matches the upstream tool's documented 4-deep recursion
|
|
96
|
+
* for `@import` directives. The entry file is hop 1; each
|
|
97
|
+
* `@import` it issues lands at hop 2, and so on. A 5-level chain
|
|
98
|
+
* (A → B → C → D → E) exceeds the default 4 and throws on file E.
|
|
99
|
+
*/
|
|
100
|
+
export const DEFAULT_MAX_HOPS = 4;
|
|
101
|
+
/**
|
|
102
|
+
* Error thrown when the loader refuses to complete a load. The
|
|
103
|
+
* `path` carries the offending file (or the importer's path for
|
|
104
|
+
* resolution failures). `cycle` is populated for the `cycle` reason
|
|
105
|
+
* with the import stack at the point of detection.
|
|
106
|
+
*/
|
|
107
|
+
export class RuleImportError extends Error {
|
|
108
|
+
path;
|
|
109
|
+
reason;
|
|
110
|
+
cycle;
|
|
111
|
+
constructor(message, detail) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = 'RuleImportError';
|
|
114
|
+
this.path = detail.path;
|
|
115
|
+
this.reason = detail.reason;
|
|
116
|
+
if (detail.cycle)
|
|
117
|
+
this.cycle = detail.cycle;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Load `entryPath` and recursively expand its `@import` directives.
|
|
122
|
+
* Returns one `LoadedRule` per visited file. The first entry is always
|
|
123
|
+
* the entry file (`source: 'file'`); transitively-imported children
|
|
124
|
+
* follow in depth-first order (`source: 'imported'`).
|
|
125
|
+
*
|
|
126
|
+
* Throws `RuleImportError` on cycle, hop overflow, byte overflow,
|
|
127
|
+
* or any rejected import target. fs errors on the entry file itself
|
|
128
|
+
* propagate as `RuleImportError` with reason `read_failed`.
|
|
129
|
+
*/
|
|
130
|
+
export async function loadRulesFile(entryPath, options) {
|
|
131
|
+
const maxHops = options.maxHops ?? DEFAULT_MAX_HOPS;
|
|
132
|
+
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
133
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
134
|
+
const visited = new Set();
|
|
135
|
+
const stack = [];
|
|
136
|
+
const rules = [];
|
|
137
|
+
const totalBytes = { value: 0 };
|
|
138
|
+
const allowedRoots = resolveAllowedRoots(options);
|
|
139
|
+
await loadRecursive({
|
|
140
|
+
rawPath: entryPath,
|
|
141
|
+
importerDir: options.cwd,
|
|
142
|
+
sourceLabel: 'file',
|
|
143
|
+
hop: 1,
|
|
144
|
+
maxHops,
|
|
145
|
+
maxFileBytes,
|
|
146
|
+
maxTotalBytes,
|
|
147
|
+
homedir: options.homedir,
|
|
148
|
+
visited,
|
|
149
|
+
stack,
|
|
150
|
+
rules,
|
|
151
|
+
totalBytes,
|
|
152
|
+
allowedRoots,
|
|
153
|
+
});
|
|
154
|
+
return rules;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the canonical allowed-root set. Each root is realpath'd so
|
|
158
|
+
* symlinked workspace / home directories still match the descendant
|
|
159
|
+
* predicate. Roots that do not exist on disk are silently dropped —
|
|
160
|
+
* an operator without `~/.claude` should not see a load failure on
|
|
161
|
+
* every file, but their `@~/foo.md` will fail the containment check
|
|
162
|
+
* with a clear `path_escape` error.
|
|
163
|
+
*
|
|
164
|
+
* Triple-review P0 fix.
|
|
165
|
+
*/
|
|
166
|
+
function resolveAllowedRoots(options) {
|
|
167
|
+
if (options.allowedRoots && options.allowedRoots.length > 0) {
|
|
168
|
+
return options.allowedRoots
|
|
169
|
+
.map((root) => safeRealpath(root))
|
|
170
|
+
.filter((root) => root !== null);
|
|
171
|
+
}
|
|
172
|
+
const candidates = [
|
|
173
|
+
options.cwd,
|
|
174
|
+
resolve(options.homedir, '.claude'),
|
|
175
|
+
resolve(options.homedir, '.pugi'),
|
|
176
|
+
];
|
|
177
|
+
return candidates
|
|
178
|
+
.map((c) => safeRealpath(c))
|
|
179
|
+
.filter((c) => c !== null);
|
|
180
|
+
}
|
|
181
|
+
function safeRealpath(p) {
|
|
182
|
+
try {
|
|
183
|
+
return realpathSync(resolve(p));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Containment predicate: is `candidate` a descendant of any allowed
|
|
191
|
+
* root? Trailing-separator normalised so `/foo/bar` is NOT a
|
|
192
|
+
* descendant of `/foo/ba`. Both `candidate` and `roots` must already
|
|
193
|
+
* be realpath'd by the caller — this is a pure path predicate, no
|
|
194
|
+
* I/O.
|
|
195
|
+
*/
|
|
196
|
+
function isContainedIn(candidate, roots) {
|
|
197
|
+
for (const root of roots) {
|
|
198
|
+
if (candidate === root)
|
|
199
|
+
return true;
|
|
200
|
+
const rootWithSep = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
201
|
+
if (candidate.startsWith(rootWithSep))
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Recursive worker. Resolves `rawPath` to an absolute realpath, gates
|
|
208
|
+
* it against the byte + hop budgets, parses frontmatter, then walks
|
|
209
|
+
* any `@import` directives in source order.
|
|
210
|
+
*/
|
|
211
|
+
async function loadRecursive(input) {
|
|
212
|
+
const { rawPath, importerDir, sourceLabel, hop, maxHops, maxFileBytes, maxTotalBytes, homedir, visited, stack, rules, totalBytes, } = input;
|
|
213
|
+
// Hop cap is enforced BEFORE the read so a cycle that gets reported
|
|
214
|
+
// as `cycle` first (preferred diagnostic) does not first trip the
|
|
215
|
+
// hop cap when both apply.
|
|
216
|
+
if (hop > maxHops) {
|
|
217
|
+
throw new RuleImportError(`import hop cap exceeded at depth ${hop} (max ${maxHops}) loading ${rawPath}`, { path: rawPath, reason: 'hop_cap_exceeded' });
|
|
218
|
+
}
|
|
219
|
+
const absolutePath = resolve(rawPath);
|
|
220
|
+
let realPath;
|
|
221
|
+
try {
|
|
222
|
+
realPath = realpathSync(absolutePath);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new RuleImportError(`realpath failed for ${absolutePath}: ${error.message}`, { path: absolutePath, reason: 'realpath_failed' });
|
|
226
|
+
}
|
|
227
|
+
// Security gates run on the post-realpath canonical identity so a
|
|
228
|
+
// symlink to `/etc/passwd` is rejected the same as a literal
|
|
229
|
+
// `@/etc/passwd`. Triple-review P0 fix.
|
|
230
|
+
if (!isContainedIn(realPath, input.allowedRoots)) {
|
|
231
|
+
throw new RuleImportError(`rule import escapes allowed roots: ${realPath}`, { path: realPath, reason: 'path_escape' });
|
|
232
|
+
}
|
|
233
|
+
if (!/\.md$/i.test(realPath)) {
|
|
234
|
+
throw new RuleImportError(`rule import target must be a .md file: ${realPath}`, { path: realPath, reason: 'extension_not_allowed' });
|
|
235
|
+
}
|
|
236
|
+
// Reject dotfile segments AFTER the allowed root prefix. We allow
|
|
237
|
+
// dotfile ROOTS (e.g. `.claude`, `.pugi`) but not deeper dotfile
|
|
238
|
+
// segments like `.ssh/id_rsa.md`. Compute the path relative to the
|
|
239
|
+
// matched root and split on `sep`.
|
|
240
|
+
for (const root of input.allowedRoots) {
|
|
241
|
+
if (realPath === root)
|
|
242
|
+
break;
|
|
243
|
+
const rootWithSep = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
244
|
+
if (!realPath.startsWith(rootWithSep))
|
|
245
|
+
continue;
|
|
246
|
+
const relative = realPath.slice(rootWithSep.length);
|
|
247
|
+
const segments = relative.split(sep);
|
|
248
|
+
for (const segment of segments) {
|
|
249
|
+
if (segment.length > 0 && segment.startsWith('.')) {
|
|
250
|
+
throw new RuleImportError(`rule import traverses dotfile segment: ${realPath}`, { path: realPath, reason: 'dotfile_segment' });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
// Cycle detection uses the post-realpath absolute path so a loop
|
|
256
|
+
// routed through a symlink lands in the same visited-set bucket as
|
|
257
|
+
// a direct loop. The stack carries the full chain for the
|
|
258
|
+
// diagnostic message.
|
|
259
|
+
if (visited.has(realPath)) {
|
|
260
|
+
const cycle = [...stack, realPath];
|
|
261
|
+
throw new RuleImportError(`import cycle detected: ${cycle.join(' -> ')}`, { path: realPath, reason: 'cycle', cycle });
|
|
262
|
+
}
|
|
263
|
+
let stats;
|
|
264
|
+
try {
|
|
265
|
+
stats = statSync(realPath);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
throw new RuleImportError(`stat failed for ${realPath}: ${error.message}`, { path: realPath, reason: 'read_failed' });
|
|
269
|
+
}
|
|
270
|
+
if (stats.size > maxFileBytes) {
|
|
271
|
+
throw new RuleImportError(`file exceeds per-file byte cap (${stats.size} > ${maxFileBytes}): ${realPath}`, { path: realPath, reason: 'file_too_large' });
|
|
272
|
+
}
|
|
273
|
+
if (totalBytes.value + stats.size > maxTotalBytes) {
|
|
274
|
+
throw new RuleImportError(`import graph exceeds aggregate byte cap (${totalBytes.value + stats.size} > ${maxTotalBytes}) at ${realPath}`, { path: realPath, reason: 'aggregate_too_large' });
|
|
275
|
+
}
|
|
276
|
+
let raw;
|
|
277
|
+
try {
|
|
278
|
+
raw = readFileSync(realPath, 'utf8');
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
throw new RuleImportError(`read failed for ${realPath}: ${error.message}`, { path: realPath, reason: 'read_failed' });
|
|
282
|
+
}
|
|
283
|
+
// Reserve the budget before recursing so children see the post-
|
|
284
|
+
// increment value. We never refund: a partially-loaded graph still
|
|
285
|
+
// counts against the cap.
|
|
286
|
+
totalBytes.value += stats.size;
|
|
287
|
+
visited.add(realPath);
|
|
288
|
+
stack.push(realPath);
|
|
289
|
+
// Strip UTF-8 BOM. Saved-from-Windows CLAUDE.md often carries ``
|
|
290
|
+
// at byte 0 and a BOM-prefixed `---` would defeat the frontmatter
|
|
291
|
+
// detection regex below.
|
|
292
|
+
const stripped = stripBom(raw);
|
|
293
|
+
const { paths, bodyAfterFrontmatter } = parseFrontmatter(stripped);
|
|
294
|
+
// Split into lines once; we use the line slice both for `@import`
|
|
295
|
+
// detection and for body assembly. Lines that match the import
|
|
296
|
+
// pattern are removed from the body — the loader's job is to expand
|
|
297
|
+
// them, not preserve the source-level directive.
|
|
298
|
+
const lines = bodyAfterFrontmatter.split('\n');
|
|
299
|
+
const bodyLines = [];
|
|
300
|
+
const importTargets = [];
|
|
301
|
+
for (const line of lines) {
|
|
302
|
+
const match = IMPORT_LINE_PATTERN.exec(line);
|
|
303
|
+
if (!match) {
|
|
304
|
+
bodyLines.push(line);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const target = match[1];
|
|
308
|
+
// Capture group is required — defensive guard for strict TS.
|
|
309
|
+
if (typeof target !== 'string' || target.length === 0) {
|
|
310
|
+
throw new RuleImportError(`empty @import target in ${realPath}`, { path: realPath, reason: 'invalid_import_target' });
|
|
311
|
+
}
|
|
312
|
+
importTargets.push(target);
|
|
313
|
+
}
|
|
314
|
+
rules.push({
|
|
315
|
+
path: realPath,
|
|
316
|
+
body: bodyLines.join('\n'),
|
|
317
|
+
paths,
|
|
318
|
+
source: sourceLabel,
|
|
319
|
+
});
|
|
320
|
+
// Resolve + recurse imports in source order so the resulting
|
|
321
|
+
// `LoadedRule[]` reflects the DFS traversal — the operator can
|
|
322
|
+
// reason about which `@import` produced which rule simply by reading
|
|
323
|
+
// top-to-bottom.
|
|
324
|
+
for (const target of importTargets) {
|
|
325
|
+
const resolved = resolveImportTarget(target, {
|
|
326
|
+
importerFile: realPath,
|
|
327
|
+
importerDir: dirname(realPath),
|
|
328
|
+
homedir,
|
|
329
|
+
});
|
|
330
|
+
await loadRecursive({
|
|
331
|
+
rawPath: resolved,
|
|
332
|
+
importerDir: dirname(realPath),
|
|
333
|
+
sourceLabel: 'imported',
|
|
334
|
+
hop: hop + 1,
|
|
335
|
+
maxHops,
|
|
336
|
+
maxFileBytes,
|
|
337
|
+
maxTotalBytes,
|
|
338
|
+
homedir,
|
|
339
|
+
visited,
|
|
340
|
+
stack,
|
|
341
|
+
rules,
|
|
342
|
+
totalBytes,
|
|
343
|
+
allowedRoots: input.allowedRoots,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Pop AFTER the recursion so sibling imports do not see each other
|
|
347
|
+
// in the stack (they are not on the same DFS path).
|
|
348
|
+
stack.pop();
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Line pattern for `@import <target>`. Anchored to line start so a
|
|
352
|
+
* literal `@import` inside a fenced code block (indented or otherwise
|
|
353
|
+
* not at column 0) is left alone. Trailing whitespace is tolerated.
|
|
354
|
+
*/
|
|
355
|
+
const IMPORT_LINE_PATTERN = /^@import\s+(\S+)\s*$/;
|
|
356
|
+
/**
|
|
357
|
+
* Resolve an `@import` target string to an absolute path. Enforces the
|
|
358
|
+
* security rules from the contract:
|
|
359
|
+
*
|
|
360
|
+
* - `./foo.md`, `../foo.md` → resolved against `importerDir`
|
|
361
|
+
* - `~/foo.md` → resolved against `homedir`
|
|
362
|
+
* - `/etc/passwd` → REJECTED (absolute path)
|
|
363
|
+
* - `org/pkg` → REJECTED (bare prefix without ./ ~/ ../)
|
|
364
|
+
*
|
|
365
|
+
* The returned path is NOT realpath-resolved here — the caller does
|
|
366
|
+
* that on read so the cycle-detection set always keys on the canonical
|
|
367
|
+
* file identity.
|
|
368
|
+
*/
|
|
369
|
+
function resolveImportTarget(target, ctx) {
|
|
370
|
+
if (target.length === 0) {
|
|
371
|
+
throw new RuleImportError(`empty @import target from ${ctx.importerFile}`, { path: ctx.importerFile, reason: 'invalid_import_target' });
|
|
372
|
+
}
|
|
373
|
+
if (isAbsolute(target)) {
|
|
374
|
+
throw new RuleImportError(`@import absolute path rejected: ${target} (from ${ctx.importerFile})`, { path: ctx.importerFile, reason: 'absolute_import' });
|
|
375
|
+
}
|
|
376
|
+
if (target.startsWith('~/')) {
|
|
377
|
+
return resolve(ctx.homedir, target.slice(2));
|
|
378
|
+
}
|
|
379
|
+
if (target.startsWith('./') || target.startsWith('../')) {
|
|
380
|
+
return resolve(ctx.importerDir, target);
|
|
381
|
+
}
|
|
382
|
+
// Anything else (bare prefix like `org/pkg` or `some-file.md`) is
|
|
383
|
+
// rejected so the operator can never accidentally import a sibling
|
|
384
|
+
// file without an explicit `./` qualifier. The strict prefix rule
|
|
385
|
+
// also makes the loader's behaviour easy to predict by inspection.
|
|
386
|
+
throw new RuleImportError(`@import target must start with ./, ../ or ~/: ${target} (from ${ctx.importerFile})`, { path: ctx.importerFile, reason: 'bare_prefix_import' });
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Parse the file's YAML frontmatter block, scoped strictly to the
|
|
390
|
+
* single key we need: `paths`. Returns the body with the frontmatter
|
|
391
|
+
* block removed and the parsed `paths` array (or `undefined` when no
|
|
392
|
+
* frontmatter / no `paths:` key was present).
|
|
393
|
+
*
|
|
394
|
+
* Supported `paths:` syntax:
|
|
395
|
+
*
|
|
396
|
+
* paths:
|
|
397
|
+
* - src/**\/*.ts
|
|
398
|
+
* - "tests/**"
|
|
399
|
+
* - 'docs/*.md'
|
|
400
|
+
*
|
|
401
|
+
* AND the inline-flow form:
|
|
402
|
+
*
|
|
403
|
+
* paths: [src/**\/*.ts, "tests/**", 'docs/*.md']
|
|
404
|
+
*
|
|
405
|
+
* Unsupported (intentionally — out of scope for the CLAUDE.md
|
|
406
|
+
* compat surface):
|
|
407
|
+
* - nested keys
|
|
408
|
+
* - multi-line strings
|
|
409
|
+
* - YAML anchors / aliases
|
|
410
|
+
* - any key other than `paths`
|
|
411
|
+
*
|
|
412
|
+
* If `paths:` exists but is empty (`paths: []` or `paths:\n` with no
|
|
413
|
+
* entries) the result is `paths: []` so the caller can distinguish
|
|
414
|
+
* "no paths key" (matches everything) from "paths explicitly empty"
|
|
415
|
+
* (matches nothing).
|
|
416
|
+
*/
|
|
417
|
+
function parseFrontmatter(input) {
|
|
418
|
+
// Frontmatter must start at byte 0 with `---` on its own line. We
|
|
419
|
+
// tolerate a trailing CR (Windows line endings) but nothing else
|
|
420
|
+
// before the opening fence.
|
|
421
|
+
const fence = /^---\s*\r?\n/;
|
|
422
|
+
if (!fence.test(input)) {
|
|
423
|
+
return { paths: undefined, bodyAfterFrontmatter: input };
|
|
424
|
+
}
|
|
425
|
+
// Search for the closing `---` on its own line. We use a multi-line
|
|
426
|
+
// anchored regex so a literal `---` inside the body (not on its own
|
|
427
|
+
// line) does not falsely close the frontmatter.
|
|
428
|
+
const closing = /\r?\n---\s*(?:\r?\n|$)/;
|
|
429
|
+
const opening = input.match(fence);
|
|
430
|
+
if (!opening) {
|
|
431
|
+
return { paths: undefined, bodyAfterFrontmatter: input };
|
|
432
|
+
}
|
|
433
|
+
const afterOpening = input.slice(opening[0].length);
|
|
434
|
+
const closingMatch = afterOpening.match(closing);
|
|
435
|
+
if (!closingMatch || typeof closingMatch.index !== 'number') {
|
|
436
|
+
// Malformed frontmatter (opening fence, no closing fence). We
|
|
437
|
+
// refuse to guess where the operator intended the YAML to end and
|
|
438
|
+
// treat the file as having no frontmatter; the body is then the
|
|
439
|
+
// raw input minus the opening fence-line for safety.
|
|
440
|
+
return { paths: undefined, bodyAfterFrontmatter: input };
|
|
441
|
+
}
|
|
442
|
+
const yamlBlock = afterOpening.slice(0, closingMatch.index);
|
|
443
|
+
const body = afterOpening.slice(closingMatch.index + closingMatch[0].length);
|
|
444
|
+
const paths = extractPathsKey(yamlBlock);
|
|
445
|
+
return { paths, bodyAfterFrontmatter: body };
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Extract the `paths:` array from a YAML frontmatter block. Returns
|
|
449
|
+
* `undefined` when the key is absent (rule matches every file) and an
|
|
450
|
+
* empty array when the key is present but explicitly empty (rule
|
|
451
|
+
* matches nothing).
|
|
452
|
+
*/
|
|
453
|
+
function extractPathsKey(yaml) {
|
|
454
|
+
const lines = yaml.split('\n');
|
|
455
|
+
let i = 0;
|
|
456
|
+
while (i < lines.length) {
|
|
457
|
+
const raw = lines[i] ?? '';
|
|
458
|
+
const line = raw.replace(/\r$/, '');
|
|
459
|
+
i += 1;
|
|
460
|
+
// Top-level keys only — anything indented belongs to a nested
|
|
461
|
+
// structure we explicitly do not support.
|
|
462
|
+
if (line.length === 0 || /^\s/.test(line))
|
|
463
|
+
continue;
|
|
464
|
+
if (line.startsWith('#'))
|
|
465
|
+
continue;
|
|
466
|
+
const keyMatch = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line);
|
|
467
|
+
if (!keyMatch)
|
|
468
|
+
continue;
|
|
469
|
+
const key = keyMatch[1];
|
|
470
|
+
const after = keyMatch[2] ?? '';
|
|
471
|
+
if (key !== 'paths')
|
|
472
|
+
continue;
|
|
473
|
+
// Inline flow form: `paths: [a, b, c]`.
|
|
474
|
+
if (after.startsWith('[')) {
|
|
475
|
+
return parseInlineFlowArray(after);
|
|
476
|
+
}
|
|
477
|
+
// Block form: a sequence of `- entry` lines that follow.
|
|
478
|
+
if (after.length === 0 || /^\s*#/.test(after)) {
|
|
479
|
+
return parseBlockSequence(lines, i);
|
|
480
|
+
}
|
|
481
|
+
// Anything else (e.g. `paths: ./src`) is unsupported. Fail closed
|
|
482
|
+
// by returning an empty array so the rule matches nothing rather
|
|
483
|
+
// than silently behaving as a global rule.
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Parse an inline-flow YAML array like `[a, "b c", 'd']`. Accepts
|
|
490
|
+
* single-quoted, double-quoted, and bare entries. Trailing comments
|
|
491
|
+
* after the closing `]` are ignored. Whitespace around commas is
|
|
492
|
+
* trimmed. Returns an empty array for `[]`.
|
|
493
|
+
*/
|
|
494
|
+
function parseInlineFlowArray(input) {
|
|
495
|
+
// Strip the opening `[` and find the matching `]`. We scan
|
|
496
|
+
// character-by-character so quoted commas do not split entries.
|
|
497
|
+
if (input[0] !== '[')
|
|
498
|
+
return [];
|
|
499
|
+
const items = [];
|
|
500
|
+
let i = 1;
|
|
501
|
+
let buffer = '';
|
|
502
|
+
let quote = null;
|
|
503
|
+
while (i < input.length) {
|
|
504
|
+
const ch = input[i];
|
|
505
|
+
if (quote) {
|
|
506
|
+
if (ch === quote) {
|
|
507
|
+
quote = null;
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
buffer += ch;
|
|
511
|
+
}
|
|
512
|
+
i += 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (ch === '"' || ch === "'") {
|
|
516
|
+
quote = ch;
|
|
517
|
+
i += 1;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (ch === ',' || ch === ']') {
|
|
521
|
+
const trimmed = buffer.trim();
|
|
522
|
+
if (trimmed.length > 0)
|
|
523
|
+
items.push(trimmed);
|
|
524
|
+
buffer = '';
|
|
525
|
+
if (ch === ']')
|
|
526
|
+
return items;
|
|
527
|
+
i += 1;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
buffer += ch;
|
|
531
|
+
i += 1;
|
|
532
|
+
}
|
|
533
|
+
// Unterminated `[ ... ]` — fail closed and return what we have so
|
|
534
|
+
// far so a typo does not silently match everything.
|
|
535
|
+
return items;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Parse a YAML block sequence starting at line `start`. Entries look
|
|
539
|
+
* like ` - foo` or ` - "foo bar"`. Stops at the first non-indented
|
|
540
|
+
* non-comment line, returning the collected entries.
|
|
541
|
+
*/
|
|
542
|
+
function parseBlockSequence(lines, start) {
|
|
543
|
+
const items = [];
|
|
544
|
+
for (let j = start; j < lines.length; j += 1) {
|
|
545
|
+
const raw = lines[j] ?? '';
|
|
546
|
+
const line = raw.replace(/\r$/, '');
|
|
547
|
+
if (line.length === 0)
|
|
548
|
+
continue;
|
|
549
|
+
if (/^\s*#/.test(line))
|
|
550
|
+
continue;
|
|
551
|
+
// Indented `-` only — once we hit a flush-left line we have left
|
|
552
|
+
// the sequence and the parent key's value is complete.
|
|
553
|
+
if (!/^\s/.test(line))
|
|
554
|
+
break;
|
|
555
|
+
const itemMatch = /^\s+-\s+(.*)$/.exec(line);
|
|
556
|
+
if (!itemMatch)
|
|
557
|
+
break;
|
|
558
|
+
const rawValue = itemMatch[1] ?? '';
|
|
559
|
+
items.push(unquoteScalar(rawValue.trim()));
|
|
560
|
+
}
|
|
561
|
+
return items;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Strip surrounding single or double quotes from a YAML scalar. Bare
|
|
565
|
+
* scalars are returned as-is. We do NOT process YAML escape sequences
|
|
566
|
+
* — glob patterns never contain them in practice and supporting them
|
|
567
|
+
* would push us toward a full YAML library.
|
|
568
|
+
*/
|
|
569
|
+
function unquoteScalar(input) {
|
|
570
|
+
if (input.length >= 2) {
|
|
571
|
+
const first = input[0];
|
|
572
|
+
const last = input[input.length - 1];
|
|
573
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
574
|
+
return input.slice(1, -1);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return input;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Strip a leading UTF-8 BOM (``) if present. Idempotent.
|
|
581
|
+
*/
|
|
582
|
+
function stripBom(input) {
|
|
583
|
+
return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Pure path-vs-rule predicate. Returns true when `filePath` matches
|
|
587
|
+
* any glob in the rule's `paths` array. A rule with `paths === undefined`
|
|
588
|
+
* matches every path (the rule applies globally). A rule with an empty
|
|
589
|
+
* `paths` array matches nothing (operator explicitly limited the rule
|
|
590
|
+
* to zero files, e.g. via `paths: []`).
|
|
591
|
+
*
|
|
592
|
+
* `filePath` may be relative or absolute. The matcher operates on the
|
|
593
|
+
* POSIX-style normalised form so glob patterns authored on macOS /
|
|
594
|
+
* Linux (forward slashes) match consistently on Windows where the
|
|
595
|
+
* runtime may produce backslash-separated paths.
|
|
596
|
+
*
|
|
597
|
+
* No I/O. No fs calls. Pure string match.
|
|
598
|
+
*/
|
|
599
|
+
export function pathMatchesRule(filePath, rule) {
|
|
600
|
+
if (rule.paths === undefined)
|
|
601
|
+
return true;
|
|
602
|
+
if (rule.paths.length === 0)
|
|
603
|
+
return false;
|
|
604
|
+
const normalized = toPosix(filePath);
|
|
605
|
+
for (const pattern of rule.paths) {
|
|
606
|
+
if (matchGlob(normalized, toPosix(pattern)))
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Convert any platform-native path separator to POSIX `/`. Glob
|
|
613
|
+
* patterns are authored with forward slashes by convention; this
|
|
614
|
+
* normalisation keeps the matcher single-codepath.
|
|
615
|
+
*/
|
|
616
|
+
function toPosix(input) {
|
|
617
|
+
if (sep === '/')
|
|
618
|
+
return input;
|
|
619
|
+
return input.split(sep).join('/');
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Compile a glob pattern to a `RegExp` and test against the candidate
|
|
623
|
+
* path. Cached patterns are not memoised — `pathMatchesRule` is called
|
|
624
|
+
* at most a few dozen times per session and a Map cache would add
|
|
625
|
+
* surface area for no measurable win.
|
|
626
|
+
*
|
|
627
|
+
* Glob grammar supported:
|
|
628
|
+
* `**` — zero or more path segments (any depth)
|
|
629
|
+
* `*` — zero or more characters within a single segment
|
|
630
|
+
* `?` — exactly one character
|
|
631
|
+
* `[abc]` / `[a-z]` — character class
|
|
632
|
+
*
|
|
633
|
+
* Anything else is treated as a literal and regex-escaped.
|
|
634
|
+
*/
|
|
635
|
+
function matchGlob(filePath, pattern) {
|
|
636
|
+
const regex = compileGlob(pattern);
|
|
637
|
+
return regex.test(filePath);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Translate a glob pattern into an equivalent anchored `RegExp`. The
|
|
641
|
+
* conversion handles the four wildcard tokens individually:
|
|
642
|
+
*
|
|
643
|
+
* - `**\/` → `(?:.*\/)?` (zero or more leading segments)
|
|
644
|
+
* - `/**` → `(?:\/.*)?` (zero or more trailing segments)
|
|
645
|
+
* - bare `**` → `.*`
|
|
646
|
+
* - `*` → `[^/]*` (single-segment wildcard)
|
|
647
|
+
* - `?` → `[^/]`
|
|
648
|
+
* - `[abc]` → preserved as a regex character class
|
|
649
|
+
*
|
|
650
|
+
* The result is anchored with `^` / `$` so partial matches do not
|
|
651
|
+
* surface as positives.
|
|
652
|
+
*/
|
|
653
|
+
function compileGlob(pattern) {
|
|
654
|
+
let out = '';
|
|
655
|
+
let i = 0;
|
|
656
|
+
while (i < pattern.length) {
|
|
657
|
+
const ch = pattern[i] ?? '';
|
|
658
|
+
if (ch === '*') {
|
|
659
|
+
// Check for `**`.
|
|
660
|
+
if (pattern[i + 1] === '*') {
|
|
661
|
+
// Distinguish `**/`, `/**`, and bare `**`. The slash-bearing
|
|
662
|
+
// forms eat the slash too so a pattern like `src/**\/*.ts`
|
|
663
|
+
// matches both `src/foo.ts` and `src/a/b.ts`.
|
|
664
|
+
const before = i > 0 ? pattern[i - 1] : '';
|
|
665
|
+
const after = pattern[i + 2] ?? '';
|
|
666
|
+
if (after === '/') {
|
|
667
|
+
out += '(?:.*\\/)?';
|
|
668
|
+
i += 3;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (before === '/' && (i + 2 === pattern.length)) {
|
|
672
|
+
// Trailing `/**` consumed: pop the slash we already emitted
|
|
673
|
+
// and replace with the optional-segment form.
|
|
674
|
+
out = out.slice(0, -2);
|
|
675
|
+
out += '(?:\\/.*)?';
|
|
676
|
+
i += 2;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
out += '.*';
|
|
680
|
+
i += 2;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
out += '[^/]*';
|
|
684
|
+
i += 1;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (ch === '?') {
|
|
688
|
+
out += '[^/]';
|
|
689
|
+
i += 1;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (ch === '[') {
|
|
693
|
+
// Take the class verbatim until the matching `]`. We do not
|
|
694
|
+
// attempt to validate the class contents — Node's RegExp parser
|
|
695
|
+
// is the source of truth there.
|
|
696
|
+
const close = pattern.indexOf(']', i + 1);
|
|
697
|
+
if (close < 0) {
|
|
698
|
+
// Unterminated class — escape the literal `[` and continue.
|
|
699
|
+
out += '\\[';
|
|
700
|
+
i += 1;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
out += pattern.slice(i, close + 1);
|
|
704
|
+
i = close + 1;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
// Everything else is a literal. Escape the regex metacharacters
|
|
708
|
+
// that survive the wildcard handling above. The `/` separator is
|
|
709
|
+
// emitted as `\/` so the compiled regex is readable when
|
|
710
|
+
// inspected via the debugger.
|
|
711
|
+
if (REGEX_ESCAPE_CHARS.has(ch)) {
|
|
712
|
+
out += '\\' + ch;
|
|
713
|
+
i += 1;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
out += ch;
|
|
717
|
+
i += 1;
|
|
718
|
+
}
|
|
719
|
+
return new RegExp('^' + out + '$');
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Regex special characters that need escaping when emitted as glob
|
|
723
|
+
* literals. `*`, `?`, and `[` are handled by their own branches above
|
|
724
|
+
* so they are NOT in this set.
|
|
725
|
+
*/
|
|
726
|
+
const REGEX_ESCAPE_CHARS = new Set([
|
|
727
|
+
'.', '+', '(', ')', '{', '}', '^', '$', '|', '\\', '/',
|
|
728
|
+
]);
|
|
729
|
+
/**
|
|
730
|
+
* Re-export `posix` as a type-level utility for downstream callers
|
|
731
|
+
* that want to canonicalise their own paths consistently with the
|
|
732
|
+
* matcher's normalisation. Cheap re-export, no runtime cost.
|
|
733
|
+
*/
|
|
734
|
+
export const PATH_POSIX_NAMESPACE = posix;
|
|
735
|
+
//# sourceMappingURL=cc-compat-rules.js.map
|