@agentwhy/cli 0.0.0-dev
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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +181 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/adapter/claude-code/contract/assumptions.js +208 -0
- package/dist/adapter/claude-code/contract/denials.js +15 -0
- package/dist/adapter/claude-code/contract/entry-points.js +15 -0
- package/dist/adapter/claude-code/contract/fields.js +73 -0
- package/dist/adapter/claude-code/contract/hooks.js +96 -0
- package/dist/adapter/claude-code/contract/identifiers.js +15 -0
- package/dist/adapter/claude-code/contract/layout.js +29 -0
- package/dist/adapter/claude-code/contract/line-types.js +26 -0
- package/dist/adapter/claude-code/contract/projects.js +23 -0
- package/dist/adapter/claude-code/contract/recognition.js +14 -0
- package/dist/adapter/claude-code/contract/session-title.js +20 -0
- package/dist/adapter/claude-code/contract/settings.js +27 -0
- package/dist/adapter/claude-code/contract/task-notifications.js +33 -0
- package/dist/adapter/claude-code/contract/tools.js +37 -0
- package/dist/adapter/claude-code/contract/version.js +51 -0
- package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +129 -0
- package/dist/adapter/claude-code/discovery/claude-code-recognition.js +46 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +100 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +61 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +28 -0
- package/dist/adapter/claude-code/discovery/discovered-session.js +1 -0
- package/dist/adapter/claude-code/discovery/session-entries.js +41 -0
- package/dist/adapter/claude-code/discovery/transcript-tail.js +81 -0
- package/dist/adapter/claude-code/events/claude-code-session-source.js +197 -0
- package/dist/adapter/claude-code/events/delegation-index.js +15 -0
- package/dist/adapter/claude-code/events/transcript-scan.js +179 -0
- package/dist/adapter/claude-code/hooks/hook-output.js +42 -0
- package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +23 -0
- package/dist/adapter/claude-code/hooks/stop-input.js +25 -0
- package/dist/adapter/claude-code/hooks/subagent-stop-input.js +26 -0
- package/dist/adapter/claude-code/policy/deny-rules.js +46 -0
- package/dist/adapter/claude-code/probe/claude-code-probe.js +86 -0
- package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +11 -0
- package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +19 -0
- package/dist/adapter/claude-code/probe/collectors/line-collector.js +1 -0
- package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +29 -0
- package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +51 -0
- package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +31 -0
- package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +28 -0
- package/dist/adapter/claude-code/probe/doctor-report.js +1 -0
- package/dist/adapter/claude-code/probe/meta-tally.js +42 -0
- package/dist/adapter/claude-code/probe/session-probe.js +1 -0
- package/dist/adapter/claude-code/probe/session-tally.js +41 -0
- package/dist/adapter/claude-code/probe/transcript-line.js +30 -0
- package/dist/adapter/claude-code/probe/transcript-tally.js +74 -0
- package/dist/adapter/claude-code/settings/deny-entries.js +65 -0
- package/dist/adapter/claude-code/settings/hook-entries.js +303 -0
- package/dist/adapter/codex/contract/actions.js +69 -0
- package/dist/adapter/codex/contract/assumptions.js +55 -0
- package/dist/adapter/codex/contract/capabilities.js +24 -0
- package/dist/adapter/codex/contract/delegations.js +30 -0
- package/dist/adapter/codex/contract/deliveries.js +25 -0
- package/dist/adapter/codex/contract/envelope.js +33 -0
- package/dist/adapter/codex/contract/hook-refusals.js +14 -0
- package/dist/adapter/codex/contract/hooks.js +46 -0
- package/dist/adapter/codex/contract/messages.js +47 -0
- package/dist/adapter/codex/contract/permissions.js +27 -0
- package/dist/adapter/codex/contract/reviews.js +13 -0
- package/dist/adapter/codex/contract/session.js +31 -0
- package/dist/adapter/codex/contract/turns.js +23 -0
- package/dist/adapter/codex/contract/version.js +15 -0
- package/dist/adapter/codex/discovery/codex-conversations.js +44 -0
- package/dist/adapter/codex/discovery/codex-project-catalogue.js +49 -0
- package/dist/adapter/codex/discovery/codex-recognition.js +20 -0
- package/dist/adapter/codex/discovery/codex-session-catalogue.js +28 -0
- package/dist/adapter/codex/discovery/codex-session-discovery.js +115 -0
- package/dist/adapter/codex/discovery/codex-session-index.js +21 -0
- package/dist/adapter/codex/discovery/codex-session-titles.js +135 -0
- package/dist/adapter/codex/discovery/session-header.js +86 -0
- package/dist/adapter/codex/discovery/session-owners.js +11 -0
- package/dist/adapter/codex/discovery/session-roots.js +49 -0
- package/dist/adapter/codex/events/action-items.js +138 -0
- package/dist/adapter/codex/events/capability-records.js +26 -0
- package/dist/adapter/codex/events/codex-session-source.js +121 -0
- package/dist/adapter/codex/events/hook-refusals.js +27 -0
- package/dist/adapter/codex/events/rollout-scan.js +614 -0
- package/dist/adapter/codex/events/turn-permissions.js +60 -0
- package/dist/adapter/codex/hooks/pre-tool-use-input.js +24 -0
- package/dist/adapter/codex/hooks/stop-refusals.js +57 -0
- package/dist/adapter/codex/probe/codex-doctor-report.js +1 -0
- package/dist/adapter/codex/probe/codex-probe.js +268 -0
- package/dist/adapter/codex/settings/codex-hooks.js +83 -0
- package/dist/cli/cli-command.js +1 -0
- package/dist/cli/command-router.js +42 -0
- package/dist/cli/commands/check-cli-command.js +128 -0
- package/dist/cli/commands/codex-stop-cli-command.js +27 -0
- package/dist/cli/commands/doctor-arguments.js +32 -0
- package/dist/cli/commands/doctor-cli-command.js +31 -0
- package/dist/cli/commands/doctor-usage.js +14 -0
- package/dist/cli/commands/init-cli-command.js +123 -0
- package/dist/cli/commands/menu-cli-command.js +58 -0
- package/dist/cli/commands/notify-cli-command.js +158 -0
- package/dist/cli/commands/refuse-cli-command.js +79 -0
- package/dist/cli/commands/report-arguments.js +62 -0
- package/dist/cli/commands/report-cli-command.js +36 -0
- package/dist/cli/commands/report-usage.js +41 -0
- package/dist/cli/commands/sessions-cli-command.js +184 -0
- package/dist/cli/commands/start-cli-command.js +83 -0
- package/dist/cli/commands/watch-cli-command.js +142 -0
- package/dist/cli/exit-codes.js +13 -0
- package/dist/cli/project-switches.js +46 -0
- package/dist/cli.js +80 -0
- package/dist/composition-root.js +411 -0
- package/dist/core/access/command-line.js +454 -0
- package/dist/core/access/listing.js +58 -0
- package/dist/core/access/path-shape.js +59 -0
- package/dist/core/access/path-tokens.js +37 -0
- package/dist/core/access/protected-access.js +310 -0
- package/dist/core/access/protected-values.js +102 -0
- package/dist/core/access/recorded-effect.js +67 -0
- package/dist/core/access/returns.js +173 -0
- package/dist/core/access/search-output.js +34 -0
- package/dist/core/access/search-reach.js +268 -0
- package/dist/core/access/uses.js +143 -0
- package/dist/core/access/words-before.js +52 -0
- package/dist/core/agent.js +1 -0
- package/dist/core/capability.js +11 -0
- package/dist/core/combined-project-catalogue.js +36 -0
- package/dist/core/combined-session-catalogue.js +25 -0
- package/dist/core/completeness.js +10 -0
- package/dist/core/context.js +1 -0
- package/dist/core/correlation/correlate.js +329 -0
- package/dist/core/delegation.js +1 -0
- package/dist/core/detector/prompt-signals.js +78 -0
- package/dist/core/entry-point.js +1 -0
- package/dist/core/event.js +1 -0
- package/dist/core/evidence.js +9 -0
- package/dist/core/late-message.js +32 -0
- package/dist/core/message.js +1 -0
- package/dist/core/nearest-project.js +28 -0
- package/dist/core/policy/default-policy.js +32 -0
- package/dist/core/policy/glob.js +56 -0
- package/dist/core/policy/parse-policy.js +70 -0
- package/dist/core/policy/policy.js +20 -0
- package/dist/core/policy/resolve-policy.js +16 -0
- package/dist/core/project-catalogue.js +1 -0
- package/dist/core/project-root.js +133 -0
- package/dist/core/provider-titles.js +10 -0
- package/dist/core/redaction/entropy.js +46 -0
- package/dist/core/redaction/redacted.js +4 -0
- package/dist/core/redaction/redactor.js +339 -0
- package/dist/core/redaction/secret-patterns.js +22 -0
- package/dist/core/redaction/value-shapes.js +81 -0
- package/dist/core/redaction/value-trace.js +110 -0
- package/dist/core/review.js +1 -0
- package/dist/core/session-catalogue.js +19 -0
- package/dist/core/session-filter.js +42 -0
- package/dist/core/session-format.js +27 -0
- package/dist/core/session-model.js +1 -0
- package/dist/core/session-reader.js +22 -0
- package/dist/core/session-records.js +1 -0
- package/dist/core/session-source.js +1 -0
- package/dist/core/session-titles.js +21 -0
- package/dist/core/turn.js +1 -0
- package/dist/doctor/codex-doctor.js +14 -0
- package/dist/doctor/doctor-use-case.js +1 -0
- package/dist/doctor/format-selecting-doctor.js +28 -0
- package/dist/doctor/render/attention/attention-rule.js +1 -0
- package/dist/doctor/render/attention/doctor-attention-rules.js +28 -0
- package/dist/doctor/render/attention/rules/damaged-meta-files-rule.js +8 -0
- package/dist/doctor/render/attention/rules/damaged-transcripts-rule.js +9 -0
- package/dist/doctor/render/attention/rules/incomplete-subagent-pairs-rule.js +6 -0
- package/dist/doctor/render/attention/rules/missing-main-transcript-rule.js +5 -0
- package/dist/doctor/render/attention/rules/missing-spilled-results-rule.js +6 -0
- package/dist/doctor/render/attention/rules/multiple-working-directories-rule.js +15 -0
- package/dist/doctor/render/attention/rules/unknown-denial-kinds-rule.js +5 -0
- package/dist/doctor/render/attention/rules/unknown-line-types-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unrecognised-entries-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unusable-directories-rule.js +12 -0
- package/dist/doctor/render/attention/transcript-scopes.js +6 -0
- package/dist/doctor/render/codex-text-doctor-renderer.js +97 -0
- package/dist/doctor/render/doctor-renderers.js +1 -0
- package/dist/doctor/render/json-doctor-renderer.js +7 -0
- package/dist/doctor/render/output-format.js +1 -0
- package/dist/doctor/render/text-doctor-renderer.js +124 -0
- package/dist/doctor/session-doctor.js +16 -0
- package/dist/infrastructure/clack-asker.js +24 -0
- package/dist/infrastructure/clack-chooser.js +40 -0
- package/dist/infrastructure/clack-multi-chooser.js +31 -0
- package/dist/infrastructure/file-alert-store.js +120 -0
- package/dist/infrastructure/file-checked-store.js +55 -0
- package/dist/infrastructure/file-mark-store.js +67 -0
- package/dist/infrastructure/file-onboarding-store.js +93 -0
- package/dist/infrastructure/folder-window.js +26 -0
- package/dist/infrastructure/node-agentwhy-invocation.js +99 -0
- package/dist/infrastructure/node-browser.js +40 -0
- package/dist/infrastructure/node-file-system.js +111 -0
- package/dist/infrastructure/node-local-server.js +115 -0
- package/dist/infrastructure/node-text-input.js +26 -0
- package/dist/infrastructure/os-notifier.js +21 -0
- package/dist/infrastructure/osascript-folder-chooser.js +53 -0
- package/dist/infrastructure/powershell-folder-chooser.js +78 -0
- package/dist/infrastructure/real-directory.js +18 -0
- package/dist/infrastructure/stream-printer.js +11 -0
- package/dist/infrastructure/terminal-banner.js +19 -0
- package/dist/ports/agentwhy-invocation.js +1 -0
- package/dist/ports/alert-store.js +1 -0
- package/dist/ports/asker.js +1 -0
- package/dist/ports/browser.js +1 -0
- package/dist/ports/checked-store.js +1 -0
- package/dist/ports/chooser.js +1 -0
- package/dist/ports/directory-reader.js +1 -0
- package/dist/ports/file-access-error.js +11 -0
- package/dist/ports/file-reader.js +1 -0
- package/dist/ports/file-tail-reader.js +1 -0
- package/dist/ports/file-writer.js +1 -0
- package/dist/ports/folder-chooser.js +1 -0
- package/dist/ports/local-server.js +1 -0
- package/dist/ports/mark-store.js +10 -0
- package/dist/ports/multi-chooser.js +1 -0
- package/dist/ports/notifier.js +1 -0
- package/dist/ports/onboarding-store.js +1 -0
- package/dist/ports/printer.js +1 -0
- package/dist/ports/text-input.js +1 -0
- package/dist/refuse/command-reach.js +135 -0
- package/dist/refuse/command-refusal.js +88 -0
- package/dist/refuse/render/codex-stop-words.js +21 -0
- package/dist/refuse/render/refusal-words.js +34 -0
- package/dist/report/build-report.js +1037 -0
- package/dist/report/check/actions-digest.js +73 -0
- package/dist/report/check/check-lines.js +83 -0
- package/dist/report/check/mark-request.js +90 -0
- package/dist/report/check/marks.js +84 -0
- package/dist/report/check/render/text-digest-renderer.js +332 -0
- package/dist/report/check/session-actions.js +90 -0
- package/dist/report/check/session-check.js +91 -0
- package/dist/report/choose-policy.js +61 -0
- package/dist/report/flow-reads.js +18 -0
- package/dist/report/private-files/tell-lists.js +84 -0
- package/dist/report/project-rules.js +33 -0
- package/dist/report/render/home-relative.js +18 -0
- package/dist/report/render/html-head.js +26 -0
- package/dist/report/render/html-report-components.js +48 -0
- package/dist/report/render/html-report-styles.js +500 -0
- package/dist/report/render/html-sidebar.js +42 -0
- package/dist/report/render/path-tail.js +14 -0
- package/dist/report/render/report-copy.js +1177 -0
- package/dist/report/render/report-page/advanced-view.js +167 -0
- package/dist/report/render/report-page/file-story.js +114 -0
- package/dist/report/render/report-page/files-view.js +381 -0
- package/dist/report/render/report-page/files.js +77 -0
- package/dist/report/render/report-page/fix-wizard-script.js +269 -0
- package/dist/report/render/report-page/fix-wizard.js +310 -0
- package/dist/report/render/report-page/helpers-view.js +383 -0
- package/dist/report/render/report-page/helpers.js +51 -0
- package/dist/report/render/report-page/item-names.js +63 -0
- package/dist/report/render/report-page/protect-patterns.js +13 -0
- package/dist/report/render/report-page/providers.js +208 -0
- package/dist/report/render/report-page/record-view.js +367 -0
- package/dist/report/render/report-page/report-page-renderer.js +129 -0
- package/dist/report/render/report-page/report-views.js +31 -0
- package/dist/report/render/report-page/story-window.js +356 -0
- package/dist/report/render/report-page/times.js +25 -0
- package/dist/report/render/report-page/to-do-view.js +299 -0
- package/dist/report/render/report-page/to-do.js +152 -0
- package/dist/report/render/report-page.js +1 -0
- package/dist/report/render/session-view.js +250 -0
- package/dist/report/render/text-report-renderer.js +981 -0
- package/dist/report/render/ui/add-file-popup.js +122 -0
- package/dist/report/render/ui/app-sidebar.js +78 -0
- package/dist/report/render/ui/ask-panel.js +80 -0
- package/dist/report/render/ui/avatar.js +19 -0
- package/dist/report/render/ui/backdrop.js +29 -0
- package/dist/report/render/ui/brand-mark.js +4 -0
- package/dist/report/render/ui/button.js +67 -0
- package/dist/report/render/ui/callout.js +20 -0
- package/dist/report/render/ui/chat-example.js +17 -0
- package/dist/report/render/ui/checklist.js +50 -0
- package/dist/report/render/ui/confirm-dialog.js +34 -0
- package/dist/report/render/ui/data-table.js +49 -0
- package/dist/report/render/ui/drawer.js +25 -0
- package/dist/report/render/ui/file-chip.js +37 -0
- package/dist/report/render/ui/fold-line.js +22 -0
- package/dist/report/render/ui/guide-card.js +25 -0
- package/dist/report/render/ui/hero.js +37 -0
- package/dist/report/render/ui/labelled-select.js +10 -0
- package/dist/report/render/ui/live-script.js +222 -0
- package/dist/report/render/ui/local-date.js +58 -0
- package/dist/report/render/ui/mode-icon.js +6 -0
- package/dist/report/render/ui/page-shell.js +101 -0
- package/dist/report/render/ui/pill-tabs.js +47 -0
- package/dist/report/render/ui/popup.js +77 -0
- package/dist/report/render/ui/progress.js +35 -0
- package/dist/report/render/ui/project-list.js +284 -0
- package/dist/report/render/ui/scope-choice.js +19 -0
- package/dist/report/render/ui/stats.js +21 -0
- package/dist/report/render/ui/status-icon.js +30 -0
- package/dist/report/render/ui/status-look.js +21 -0
- package/dist/report/render/ui/switch.js +11 -0
- package/dist/report/render/ui/tag.js +19 -0
- package/dist/report/render/ui/task-list.js +56 -0
- package/dist/report/render/ui/tokens.js +46 -0
- package/dist/report/render/ui/update-notice.js +146 -0
- package/dist/report/render/ui/words/app-words.js +163 -0
- package/dist/report/render/ui/words/conversations-words.js +457 -0
- package/dist/report/render/ui/words/month-words.js +59 -0
- package/dist/report/render/ui/words/onboarding-words.js +446 -0
- package/dist/report/render/ui/words/projects-words.js +217 -0
- package/dist/report/render/ui/words/report-words.js +2099 -0
- package/dist/report/render/ui/words/settings-words.js +611 -0
- package/dist/report/render/ui/words/to-fix-words.js +287 -0
- package/dist/report/report-model.js +2 -0
- package/dist/report/report-use-case.js +1 -0
- package/dist/report/rule-names.js +20 -0
- package/dist/report/session-report.js +163 -0
- package/dist/report/start/app-nav.js +37 -0
- package/dist/report/start/conversations/calendar-view.js +104 -0
- package/dist/report/start/conversations/conversation-columns.js +187 -0
- package/dist/report/start/conversations/conversations-renderer.js +54 -0
- package/dist/report/start/conversations/empty-week.js +120 -0
- package/dist/report/start/conversations/files-window.js +166 -0
- package/dist/report/start/conversations/period-section.js +280 -0
- package/dist/report/start/conversations/periods-script.js +440 -0
- package/dist/report/start/conversations/periods.js +110 -0
- package/dist/report/start/conversations/week-view.js +30 -0
- package/dist/report/start/conversations/weeks.js +17 -0
- package/dist/report/start/month/month-renderer.js +50 -0
- package/dist/report/start/month/month-view.js +135 -0
- package/dist/report/start/month/months.js +17 -0
- package/dist/report/start/onboarding/done.js +171 -0
- package/dist/report/start/onboarding/finish-onboarding.js +93 -0
- package/dist/report/start/onboarding/intro.js +100 -0
- package/dist/report/start/onboarding/onboarding-changes.js +40 -0
- package/dist/report/start/onboarding/onboarding-renderer.js +195 -0
- package/dist/report/start/onboarding/onboarding-script.js +400 -0
- package/dist/report/start/onboarding/onboarding-view.js +98 -0
- package/dist/report/start/onboarding/project-step.js +210 -0
- package/dist/report/start/onboarding/steps.js +239 -0
- package/dist/report/start/onboarding/welcome.js +66 -0
- package/dist/report/start/page-notice.js +10 -0
- package/dist/report/start/projects/index-projects.js +43 -0
- package/dist/report/start/projects/moved-page.js +82 -0
- package/dist/report/start/projects/project-switch.js +138 -0
- package/dist/report/start/projects/projects-window.js +67 -0
- package/dist/report/start/projects/temporary-space.js +18 -0
- package/dist/report/start/render/air-datepicker-vendor.js +27 -0
- package/dist/report/start/render/session-status.js +41 -0
- package/dist/report/start/render/start-words.js +84 -0
- package/dist/report/start/repository.js +28 -0
- package/dist/report/start/serve/index-handler.js +372 -0
- package/dist/report/start/serve/onboarding-request.js +50 -0
- package/dist/report/start/serve/settings-request.js +1 -0
- package/dist/report/start/serve/settings-setup.js +193 -0
- package/dist/report/start/session-index.js +1 -0
- package/dist/report/start/session-start.js +974 -0
- package/dist/report/start/settings/alerts-tab.js +104 -0
- package/dist/report/start/settings/files-tab.js +169 -0
- package/dist/report/start/settings/general-tab.js +71 -0
- package/dist/report/start/settings/settings-renderer.js +254 -0
- package/dist/report/start/settings/settings-script.js +161 -0
- package/dist/report/start/settings/settings-view.js +240 -0
- package/dist/report/start/settings/settings-windows.js +334 -0
- package/dist/report/start/settings-files.js +43 -0
- package/dist/report/start/to-fix/done-list.js +45 -0
- package/dist/report/start/to-fix/file-window.js +110 -0
- package/dist/report/start/to-fix/to-fix-list.js +70 -0
- package/dist/report/start/to-fix/to-fix-renderer.js +237 -0
- package/dist/report/start/to-fix/to-fix-script.js +94 -0
- package/dist/report/start/to-fix/to-fix-view.js +124 -0
- package/dist/report/watch/agent-alert.js +72 -0
- package/dist/report/watch/notice-choices.js +35 -0
- package/dist/report/watch/notice-settings.js +65 -0
- package/dist/report/watch/preferences.js +100 -0
- package/dist/report/watch/render/instruction-words.js +54 -0
- package/dist/report/watch/render/notice-words.js +182 -0
- package/dist/report/watch/subagent-watch.js +345 -0
- package/dist/report/watch/watch-notice.js +1 -0
- package/dist/setup/behind.js +18 -0
- package/dist/setup/codex-mirror.js +171 -0
- package/dist/setup/not-a-project.js +19 -0
- package/dist/setup/project-setup.js +644 -0
- package/dist/setup/protected-patterns.js +30 -0
- package/dist/shared/colour.js +23 -0
- package/dist/shared/compare.js +4 -0
- package/dist/shared/content-version.js +13 -0
- package/dist/shared/counter.js +11 -0
- package/dist/shared/file-stamp.js +4 -0
- package/dist/shared/json.js +13 -0
- package/dist/shared/label.js +13 -0
- package/dist/shared/package-name.js +6 -0
- package/dist/shared/plain-invocation.js +44 -0
- package/dist/shared/plain-version.js +21 -0
- package/dist/shared/printable.js +25 -0
- package/dist/shared/renderer.js +1 -0
- package/dist/shared/sentence.js +20 -0
- package/dist/shared/terminal-logo.js +41 -0
- package/dist/shared/text-table.js +15 -0
- package/dist/shared/wrap.js +29 -0
- package/package.json +70 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { SETTINGS_FILES } from '../adapter/claude-code/contract/settings.js';
|
|
3
|
+
import { denyEntriesFor, fileRulesIn, isDenied, withDenyEntries, withoutDenyEntries } from '../adapter/claude-code/settings/deny-entries.js';
|
|
4
|
+
import { installedHooks, missingHookEntries, runningInvocation, withHookEntries, withHooksPinnedTo, withHooksReading, withoutAgentwhyHooks, } from '../adapter/claude-code/settings/hook-entries.js';
|
|
5
|
+
import { DEFAULT_POLICY } from '../core/policy/default-policy.js';
|
|
6
|
+
import { FileAccessError } from '../ports/file-access-error.js';
|
|
7
|
+
import { parseJsonObject } from '../shared/json.js';
|
|
8
|
+
import { plainVersion } from '../shared/plain-version.js';
|
|
9
|
+
import { printable } from '../shared/printable.js';
|
|
10
|
+
import { notAProject } from './not-a-project.js';
|
|
11
|
+
import { patternsIn, patternsOf } from './protected-patterns.js';
|
|
12
|
+
/** Each line of the list: a word, a dash, what it does - and beside it what it costs (R4a). */
|
|
13
|
+
const HOOK_CHOICES = {
|
|
14
|
+
watch: {
|
|
15
|
+
label: 'Notify me - an agent copied a protected value',
|
|
16
|
+
detail: 'for you only; nothing is blocked',
|
|
17
|
+
},
|
|
18
|
+
refuse: {
|
|
19
|
+
label: 'Block - shell commands that open protected files',
|
|
20
|
+
detail: 'also blocks cat .env.example; text match, not a boundary',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const HOOKS = ['watch', 'refuse'];
|
|
24
|
+
/** The third line of the list: not a hook, but the other thing `init` can do (R4a, R4c). */
|
|
25
|
+
const PROTECT_CHOICE = {
|
|
26
|
+
label: 'Protect - more files of my own',
|
|
27
|
+
detail: 'deny rules Claude Code applies, and refuse for shell commands',
|
|
28
|
+
};
|
|
29
|
+
/** Why `refuse` is in a plan nobody ticked it in (`block-means-blocked` K4, K5). */
|
|
30
|
+
const REFUSE_WITH_RULES = 'refuse comes with protected files: the deny rules stop Claude Code\'s own file tools, and refuse keeps shell commands like cat and grep -r away from them';
|
|
31
|
+
/** Why nothing is written where no project is (`which-project.md` V8), and where to run instead. */
|
|
32
|
+
export const NOT_A_PROJECT = {
|
|
33
|
+
home: 'agentwhy doesn\'t change settings in your home folder: it isn\'t a project, and Claude Code reads its .claude/settings.json in every project. Nothing was written. Run agentwhy in your project\'s folder.\n',
|
|
34
|
+
root: 'agentwhy doesn\'t change settings at the root of a disk: it isn\'t a project. Nothing was written. Run agentwhy in your project\'s folder.\n',
|
|
35
|
+
};
|
|
36
|
+
/** What a deny rule is and is not, said where rules are written (R4e). */
|
|
37
|
+
const WHAT_A_RULE_IS = 'a deny rule names a tool, not a file: cat .env walks past Read(.env) unless refuse is on, and neither is a boundary below the agent';
|
|
38
|
+
/**
|
|
39
|
+
* `agentwhy init`: asks who the project's setup is for, what agentwhy should do in it, and what else to protect; then
|
|
40
|
+
* shows exactly what it will write and writes it (`specs/2026-09-16-worth-running-every-day.md` R4-R10, R4a-R4h). It
|
|
41
|
+
* never rewrites a file it cannot parse, never edits a rule somebody else wrote, and takes out exactly what it put in.
|
|
42
|
+
*/
|
|
43
|
+
export class ProjectSetup {
|
|
44
|
+
#dependencies;
|
|
45
|
+
constructor(dependencies) {
|
|
46
|
+
this.#dependencies = dependencies;
|
|
47
|
+
}
|
|
48
|
+
async run(options) {
|
|
49
|
+
// which-project V8: the home directory's .claude/settings.json is Claude Code's settings for every project, and a root
|
|
50
|
+
// is no project, so nothing is written there - by `init`, Settings or the onboarding alike. Taking out is the one
|
|
51
|
+
// exception: a setup written there before this check has to be removable by the command that wrote it.
|
|
52
|
+
const notOne = notAProject(this.#dependencies.workingDirectory, this.#dependencies.home, this.#dependencies.realHome);
|
|
53
|
+
if (notOne !== undefined && (options.update === true || !options.remove))
|
|
54
|
+
return { outcome: 'refused', output: NOT_A_PROJECT[notOne] };
|
|
55
|
+
const directory = join(this.#dependencies.workingDirectory, SETTINGS_FILES.directory);
|
|
56
|
+
// Before "Who is this for?": an update changes the hooks wherever they run, and asks nothing but consent.
|
|
57
|
+
if (options.update === true)
|
|
58
|
+
return this.#update(directory, options);
|
|
59
|
+
// Who it is for is asked first: it decides which file is read, and a person knows that answer before any other.
|
|
60
|
+
const target = await this.#whereToWrite(options);
|
|
61
|
+
if (target === undefined)
|
|
62
|
+
return { outcome: 'declined', output: 'Nothing was written.\n' };
|
|
63
|
+
const file = SETTINGS_FILES[target];
|
|
64
|
+
const path = join(directory, file);
|
|
65
|
+
const where = `${SETTINGS_FILES.directory}/${file}`;
|
|
66
|
+
const read = await this.#read(path);
|
|
67
|
+
if (read.kind === 'broken' || read.kind === 'unreadable') {
|
|
68
|
+
return {
|
|
69
|
+
outcome: 'refused',
|
|
70
|
+
output: `${where} ${read.kind === 'broken' ? 'is not a JSON object' : 'could not be read'}, so nothing was written. Fix it, or add the hooks by hand: agentwhy watch --help\n`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const settings = read.kind === 'object' ? read.settings : {};
|
|
74
|
+
const settled = { ...options, invoke: options.invoke ?? (await this.#invocation(directory)) };
|
|
75
|
+
if (options.remove)
|
|
76
|
+
return this.#remove(settings, path, where, settled);
|
|
77
|
+
const chosen = await this.#whatToDo(options, await this.#alreadyRunning(directory));
|
|
78
|
+
if (chosen === undefined)
|
|
79
|
+
return { outcome: 'declined', output: 'Nothing was written.\n' };
|
|
80
|
+
const protect = await this.#patternsToProtect(options, await this.#protectedNow(directory), chosen.protect);
|
|
81
|
+
if (protect === undefined)
|
|
82
|
+
return { outcome: 'declined', output: 'Nothing was written.\n' };
|
|
83
|
+
return this.#install(settings, { directory, target, path, where, before: read.kind === 'object' ? read.text : undefined }, settled, chosen, protect);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* J1, where `--command` names none: the command the project's agentwhy hooks already run, the local file first, so
|
|
87
|
+
* a hook written now runs the way they do (R42); else the way this agentwhy runs. Asked once, before anything is
|
|
88
|
+
* written, and the finder only where the files say nothing.
|
|
89
|
+
*/
|
|
90
|
+
async #invocation(directory) {
|
|
91
|
+
for (const file of [SETTINGS_FILES.local, SETTINGS_FILES.shared]) {
|
|
92
|
+
const read = await this.#read(join(directory, file));
|
|
93
|
+
const running = read.kind === 'object' ? runningInvocation(read.settings) : undefined;
|
|
94
|
+
if (running !== undefined)
|
|
95
|
+
return running;
|
|
96
|
+
}
|
|
97
|
+
return this.#dependencies.invocation.find();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `init --update` (`nothing-updates-by-itself.md` U7): every agentwhy hook in either file pinned to an older release is
|
|
101
|
+
* pinned to this one's, after the same consent as any run (R7). Nothing is installed, removed or protected, and a hook
|
|
102
|
+
* that was never pinned stays as it is - pinning one is `init` writing a hook (J1), not an update.
|
|
103
|
+
*/
|
|
104
|
+
async #update(directory, options) {
|
|
105
|
+
const version = await this.#dependencies.invocation.version();
|
|
106
|
+
if (version === undefined || plainVersion(version) === undefined) {
|
|
107
|
+
return { outcome: 'unchanged', output: `This agentwhy${version === undefined ? '' : ` (${version})`} is not a release, so there is nothing to update to.\n` };
|
|
108
|
+
}
|
|
109
|
+
const changes = [];
|
|
110
|
+
for (const file of [SETTINGS_FILES.local, SETTINGS_FILES.shared]) {
|
|
111
|
+
const path = join(directory, file);
|
|
112
|
+
const where = `${SETTINGS_FILES.directory}/${file}`;
|
|
113
|
+
const read = await this.#read(path);
|
|
114
|
+
// One file that cannot be read stops the whole update: half an update would leave the two files on two releases.
|
|
115
|
+
if (read.kind === 'broken' || read.kind === 'unreadable') {
|
|
116
|
+
return { outcome: 'refused', output: `${where} ${read.kind === 'broken' ? 'is not a JSON object' : 'could not be read'}, so nothing was written.\n` };
|
|
117
|
+
}
|
|
118
|
+
if (read.kind !== 'object')
|
|
119
|
+
continue;
|
|
120
|
+
const { settings: next, changed } = withHooksPinnedTo(read.settings, version);
|
|
121
|
+
if (changed.length > 0)
|
|
122
|
+
changes.push({ path, where, text: read.text, next, changed });
|
|
123
|
+
}
|
|
124
|
+
if (changes.length === 0) {
|
|
125
|
+
return { outcome: 'unchanged', output: `Nothing to update: no hook here runs a release of agentwhy older than ${version}.\n` };
|
|
126
|
+
}
|
|
127
|
+
const plan = [
|
|
128
|
+
`agentwhy init --update will pin agentwhy's hooks to ${version}:`,
|
|
129
|
+
'',
|
|
130
|
+
...changes.flatMap(({ where, changed }) => [
|
|
131
|
+
` ${where}`,
|
|
132
|
+
// The event is a key of a file that may have come with a clone: shown through `printable`, like its rules.
|
|
133
|
+
...changed.map(({ hook, event, from }) => ` ${`${hook} (${printable(event)})`.padEnd(24)}${from} -> ${version}`),
|
|
134
|
+
]),
|
|
135
|
+
'',
|
|
136
|
+
' Nothing else in either file changes. Each file is rewritten as two-space JSON.',
|
|
137
|
+
];
|
|
138
|
+
const consent = await this.#consent(options, 'Update it?', plan);
|
|
139
|
+
if (typeof consent !== 'string')
|
|
140
|
+
return consent;
|
|
141
|
+
const said = consent === 'asked' ? '' : `${plan.join('\n')}\n\n`;
|
|
142
|
+
const written = [];
|
|
143
|
+
for (const change of changes) {
|
|
144
|
+
try {
|
|
145
|
+
await this.#dependencies.files.writeText(change.path, `${JSON.stringify(change.next, null, 2)}\n`);
|
|
146
|
+
written.push(change);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// Found by a review: the file written before this one stayed written, and the project ran two releases. By a
|
|
150
|
+
// second: only on an error the port names - a full or read-only disk left it so all the same - and a write that
|
|
151
|
+
// failed part way may have cut its own file short, so that one is put back too.
|
|
152
|
+
const stuck = await this.#putBack([...written, change]);
|
|
153
|
+
if (!(error instanceof FileAccessError))
|
|
154
|
+
throw error;
|
|
155
|
+
const ahead = stuck.filter((where) => where !== change.where);
|
|
156
|
+
const cut = stuck.includes(change.where) ? ` ${change.where} may be cut short: it could not be put back as it was.` : '';
|
|
157
|
+
return {
|
|
158
|
+
outcome: 'unwritable',
|
|
159
|
+
output: stuck.length === 0
|
|
160
|
+
? `${said}${change.where} could not be written, so nothing was changed.\n`
|
|
161
|
+
: ahead.length === 0
|
|
162
|
+
? `${said}${change.where} could not be written.${cut}\n`
|
|
163
|
+
: `${said}${change.where} could not be written, and ${ahead.join(' and ')} could not be put back: it runs agentwhy ${version} now, and ${change.where} does not.${cut} Run agentwhy init --update again once ${change.where} can be written.\n`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { outcome: 'written', output: `${said}Updated: from your AI's next reply, the hooks here run agentwhy ${version}.\n` };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Each file back to the text it held before a write, where it does not hold it still; the ones that could not be, by
|
|
171
|
+
* name. Whatever stops one is recorded, not thrown: this runs after a write has already failed, and that failure is
|
|
172
|
+
* what the caller reports.
|
|
173
|
+
*
|
|
174
|
+
* The text is what `readText` decoded, so a file in UTF-8 - which JSON is - comes back byte for byte, and a byte that
|
|
175
|
+
* is not UTF-8 comes back as U+FFFD, as the write being undone wrote it (found by a review: this said byte for byte
|
|
176
|
+
* of every file). Putting back the bytes themselves needs a port that reads and writes bytes.
|
|
177
|
+
*/
|
|
178
|
+
async #putBack(files) {
|
|
179
|
+
const stuck = [];
|
|
180
|
+
for (const { path, where, text } of files) {
|
|
181
|
+
if (await this.#holds(path, text))
|
|
182
|
+
continue;
|
|
183
|
+
try {
|
|
184
|
+
await this.#dependencies.files.writeText(path, text);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
stuck.push(where);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return stuck;
|
|
191
|
+
}
|
|
192
|
+
/** Whether a file holds exactly this text: a write that failed before it opened the file left it as it was. */
|
|
193
|
+
async #holds(path, text) {
|
|
194
|
+
try {
|
|
195
|
+
return (await this.#dependencies.files.readText(path)) === text;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Whether a person is there to answer. `--yes` says "do not ask me", and is honoured as that. */
|
|
202
|
+
get #canAsk() {
|
|
203
|
+
return this.#dependencies.interactive;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* R4, R4g: the local file unless the person says otherwise - it stays out of the repository, so a tool one person
|
|
207
|
+
* installs is not installed for everyone who clones. A terminal is asked; `--shared` answers off one.
|
|
208
|
+
*/
|
|
209
|
+
async #whereToWrite(options) {
|
|
210
|
+
if (options.target !== undefined)
|
|
211
|
+
return options.target;
|
|
212
|
+
if (!this.#canAsk || options.yes || options.remove)
|
|
213
|
+
return 'local';
|
|
214
|
+
const chosen = await this.#dependencies.chooser.choose('Who is this for? (change it later: agentwhy init --shared, or --remove)', [
|
|
215
|
+
{ label: 'Just me', detail: `${SETTINGS_FILES.directory}/${SETTINGS_FILES.local} - stays out of the repository` },
|
|
216
|
+
{
|
|
217
|
+
label: 'Everyone here',
|
|
218
|
+
detail: `${SETTINGS_FILES.directory}/${SETTINGS_FILES.shared} - committed, so it runs for everyone who clones`,
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
return chosen === undefined ? undefined : chosen === 0 ? 'local' : 'shared';
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* R4a: one list for everything `init` can do here - the two hooks, and whether to name protected files of its own.
|
|
225
|
+
* Off a terminal the flags are the answer, and `watch` alone is the default, as the first version had it.
|
|
226
|
+
*/
|
|
227
|
+
async #whatToDo(options, installed) {
|
|
228
|
+
const chosen = await this.#chosen(options, installed);
|
|
229
|
+
// An empty list names no hook on purpose - the page changing or moving a rule that is already there (K3) - and is
|
|
230
|
+
// kept as it is; everywhere else, protecting files installs `refuse` with them.
|
|
231
|
+
return chosen === undefined || options.hooks?.length === 0 ? chosen : withRefuse(chosen);
|
|
232
|
+
}
|
|
233
|
+
async #chosen(options, installed) {
|
|
234
|
+
if (options.hooks !== undefined) {
|
|
235
|
+
return { hooks: options.hooks, protect: options.protect.length > 0, whole: options.hooks.length > 0 && options.keep !== true };
|
|
236
|
+
}
|
|
237
|
+
if (!this.#canAsk || options.yes)
|
|
238
|
+
return { hooks: ['watch'], protect: options.protect.length > 0, whole: false };
|
|
239
|
+
const rows = [
|
|
240
|
+
...HOOKS.map((hook) => ({
|
|
241
|
+
...HOOK_CHOICES[hook],
|
|
242
|
+
// What is already there stays ticked, so that answering without touching anything changes nothing.
|
|
243
|
+
selected: installed.has(hook) || hook === 'watch',
|
|
244
|
+
})),
|
|
245
|
+
// Ticked only when it was asked for on the command line: the default is one thing, and it is the notice.
|
|
246
|
+
{ ...PROTECT_CHOICE, selected: options.protect.length > 0 },
|
|
247
|
+
];
|
|
248
|
+
const chosen = await this.#dependencies.hookChooser.chooseMany('What should agentwhy do here? Tick everything you want.', rows);
|
|
249
|
+
if (chosen === undefined)
|
|
250
|
+
return undefined;
|
|
251
|
+
return {
|
|
252
|
+
hooks: chosen.flatMap((position) => (HOOKS[position] === undefined ? [] : [HOOKS[position]])),
|
|
253
|
+
// The last row is the question of R4c; a `--protect` on the command line asks it whether it is ticked or not.
|
|
254
|
+
protect: chosen.includes(rows.length - 1) || options.protect.length > 0,
|
|
255
|
+
whole: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** R4c, R4h: what `--protect` named, plus what a person types once they have been shown what is protected today. */
|
|
259
|
+
async #patternsToProtect(options, held, asked) {
|
|
260
|
+
const given = patternsOf(options.protect);
|
|
261
|
+
if (!asked || !this.#canAsk || options.yes)
|
|
262
|
+
return given;
|
|
263
|
+
const now = held.length === 0
|
|
264
|
+
? `${shorten(DEFAULT_POLICY.protected.map((entry) => entry.pattern))} (the built-in list)`
|
|
265
|
+
: shorten(held.map((entry) => printable(pathOfRule(entry))));
|
|
266
|
+
const typed = await this.#dependencies.asker.ask(`Protected now: ${now}. Anything else? Comma-separated; Enter to skip`, '*.pem, config/credentials.json');
|
|
267
|
+
if (typed === undefined)
|
|
268
|
+
return undefined;
|
|
269
|
+
const answered = patternsIn(typed);
|
|
270
|
+
// The same pattern given on the command line and typed again is one rule, not two.
|
|
271
|
+
return { patterns: [...new Set([...given.patterns, ...answered.patterns])], refused: [...given.refused, ...answered.refused] };
|
|
272
|
+
}
|
|
273
|
+
/** What the project protects today, for R4h: the local file's rules, else the shared file's, else none. */
|
|
274
|
+
async #protectedNow(directory) {
|
|
275
|
+
for (const file of [SETTINGS_FILES.local, SETTINGS_FILES.shared]) {
|
|
276
|
+
const read = await this.#read(join(directory, file));
|
|
277
|
+
const rules = read.kind === 'object' ? fileRulesIn(read.settings) : [];
|
|
278
|
+
if (rules.length > 0)
|
|
279
|
+
return rules;
|
|
280
|
+
}
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
/** Which hooks the project already runs, for the ticks of the list: read from both files, whichever is written. */
|
|
284
|
+
async #alreadyRunning(directory) {
|
|
285
|
+
const running = new Set();
|
|
286
|
+
for (const file of [SETTINGS_FILES.local, SETTINGS_FILES.shared]) {
|
|
287
|
+
const read = await this.#read(join(directory, file));
|
|
288
|
+
if (read.kind === 'object')
|
|
289
|
+
for (const hook of installedHooks(read.settings))
|
|
290
|
+
running.add(hook);
|
|
291
|
+
}
|
|
292
|
+
return running;
|
|
293
|
+
}
|
|
294
|
+
async #install(settings, place, options, chosen, protect) {
|
|
295
|
+
const { directory, target, path, where, before } = place;
|
|
296
|
+
const installed = installedHooks(settings, options.invoke);
|
|
297
|
+
const wanted = chosen.hooks;
|
|
298
|
+
const otherFile = SETTINGS_FILES[target === 'local' ? 'shared' : 'local'];
|
|
299
|
+
const other = await this.#read(join(directory, otherFile));
|
|
300
|
+
const otherSettings = other.kind === 'object' ? other.settings : {};
|
|
301
|
+
const otherRules = fileRulesIn(otherSettings);
|
|
302
|
+
// R4g: a hook this run installs that already runs from the other file is moved, not left to run twice.
|
|
303
|
+
const running = installedHooks(otherSettings, options.invoke);
|
|
304
|
+
const moving = wanted.filter((hook) => running.has(hook));
|
|
305
|
+
// Whatever happens next, a pattern that could not be written is said: silence would read as "it is protected now".
|
|
306
|
+
const refused = protect.refused.map(({ pattern, reason }) => `Not written: "${pattern}" - ${reason}.\n`).join('');
|
|
307
|
+
// Per event, not per hook: a file that runs `watch` on `SubagentStop` alone is missing its `Stop`, and asking
|
|
308
|
+
// for `watch` again has to add it rather than answer that there is nothing to do. Which events are missing does
|
|
309
|
+
// not depend on where the rules come from, so this reads them before that file is settled, and the entries
|
|
310
|
+
// themselves are built once it is.
|
|
311
|
+
const missing = [...new Set(missingHookEntries(settings, wanted, options.invoke, undefined).map((entry) => entry.hook))];
|
|
312
|
+
// A hook that was running and is not ticked is taken out here too: the list shows what is on, and unticking a
|
|
313
|
+
// line is how a person turns it off, the same as ticking one is how they turn it on.
|
|
314
|
+
const unwanted = chosen.whole ? HOOKS.filter((hook) => installed.has(hook) && !wanted.includes(hook)) : [];
|
|
315
|
+
const newRules = protect.patterns.filter((pattern) => !isDenied(settings, pattern)).flatMap(denyEntriesFor);
|
|
316
|
+
if (wanted.length === 0 && installed.size === 0 && protect.patterns.length === 0) {
|
|
317
|
+
return { outcome: 'declined', output: `${refused}Nothing was chosen, so nothing was written.\n` };
|
|
318
|
+
}
|
|
319
|
+
if (missing.length === 0 && unwanted.length === 0 && newRules.length === 0 && moving.length === 0) {
|
|
320
|
+
const hooks = wanted.length === 0 ? `${where} runs none of agentwhy's hooks` : `${where} already runs ${wanted.map((hook) => `agentwhy ${hook}`).join(' and ')}`;
|
|
321
|
+
const paths = protect.patterns.length === 0 ? '' : ', and every path named is already denied there';
|
|
322
|
+
return { outcome: 'unchanged', output: `${refused}${hooks}${paths}. Nothing to add.\n` };
|
|
323
|
+
}
|
|
324
|
+
// R4d: rules written here move where the hooks read, so what the shared file protects is copied in with them. Only
|
|
325
|
+
// in that direction: a person's own rules copied into a shared file would publish a choice they made for themselves.
|
|
326
|
+
const copied = newRules.length === 0 || target === 'shared' ? [] : otherRules.filter((entry) => !fileRulesIn(settings).includes(entry));
|
|
327
|
+
const denyToWrite = [...newRules, ...copied];
|
|
328
|
+
const rules = rulesFileFor(target, settings, otherRules, denyToWrite);
|
|
329
|
+
const entries = missingHookEntries(settings, wanted, options.invoke, rules);
|
|
330
|
+
const ruleCount = rules === target ? fileRulesIn(settings).length + denyToWrite.length : otherRules.length;
|
|
331
|
+
const { settings: withoutUnwanted } = withoutAgentwhyHooks(settings, options.invoke, unwanted);
|
|
332
|
+
// `withHookEntries` always writes a `hooks` key, even an empty one; skipped when there is nothing to add, so
|
|
333
|
+
// taking out the last hook does not leave a stray `"hooks": {}` behind.
|
|
334
|
+
const written = withDenyEntries(entries.length === 0 ? withoutUnwanted : withHookEntries(withoutUnwanted, entries), denyToWrite);
|
|
335
|
+
// R4d: a hook already here goes on reading what it read unless it is pointed again, and a rule written for it would
|
|
336
|
+
// be enforced by Claude Code and never read by the hook. Only where this run writes rules: a run that writes none
|
|
337
|
+
// changes nothing about where the hooks read from.
|
|
338
|
+
const { settings: next, changed: repointed } = rules === undefined || denyToWrite.length === 0 ? { settings: written, changed: [] } : withHooksReading(written, rules, options.invoke);
|
|
339
|
+
const plan = [
|
|
340
|
+
`agentwhy will change this in ${where}${target === 'local' ? ' (your own file, not committed)' : ' (committed, so it runs for everyone who clones)'}:`,
|
|
341
|
+
'',
|
|
342
|
+
...(entries.length === 0 ? [] : [' Run automatically', ...entries.map((entry) => ` ${describe(entry)}`), '']),
|
|
343
|
+
...(unwanted.length === 0 ? [] : [' Stop running', ...unwanted.map((hook) => ` agentwhy ${hook}`), '']),
|
|
344
|
+
...(denyToWrite.length === 0 ? [] : [' Protect', ...protectLines(newRules, copied), '']),
|
|
345
|
+
...(moving.length === 0
|
|
346
|
+
? []
|
|
347
|
+
: [
|
|
348
|
+
` Moving out of ${SETTINGS_FILES.directory}/${otherFile}: ${moving.join(' and ')}, so ${moving.length === 1 ? 'it does' : 'they do'} not run twice.`,
|
|
349
|
+
'',
|
|
350
|
+
]),
|
|
351
|
+
...(rules === undefined
|
|
352
|
+
? [' Protected after this: the built-in list, since no deny rule here names a file.', ` ${defaultPatterns()}`, '']
|
|
353
|
+
: [
|
|
354
|
+
` Protected after this: ${count(ruleCount, 'rule')} in ${SETTINGS_FILES.directory}/${SETTINGS_FILES[rules]}, which is what the hooks read.`,
|
|
355
|
+
...(repointed.length === 0
|
|
356
|
+
? []
|
|
357
|
+
: [` Pointed at it: ${repointed.map((hook) => `agentwhy ${hook}`).join(' and ')}, which read other rules until now.`]),
|
|
358
|
+
'',
|
|
359
|
+
]),
|
|
360
|
+
' Good to know',
|
|
361
|
+
...[...(chosen.withRules === true && missing.includes('refuse') ? [REFUSE_WITH_RULES] : []), ...notes(missing, copied.length, options.invoke)].map((note) => ` - ${note}`),
|
|
362
|
+
];
|
|
363
|
+
// R4f: the JSON is there for whoever wants it, behind a choice - a wall of it above the question is not read.
|
|
364
|
+
const json = jsonPreview(entries, denyToWrite).map((line) => ` ${line}`);
|
|
365
|
+
const consent = await this.#consent(options, 'Write it?', plan, json);
|
|
366
|
+
if (typeof consent !== 'string')
|
|
367
|
+
return consent;
|
|
368
|
+
// A person at the terminal has just read the plan and chose whether to see the JSON. A run with --yes asked
|
|
369
|
+
// nobody anything, so its output carries both: it is the only record that the change was described at all.
|
|
370
|
+
const said = consent === 'asked' ? '' : `${[...plan, '', ' The exact JSON:', ...json].join('\n')}\n\n`;
|
|
371
|
+
// R4g, found by a review: a moved hook comes out of the other file first, so a failure writing this one can put that
|
|
372
|
+
// one back - its text is known, since it held the hook. Written the other way round, a failure left the hook running
|
|
373
|
+
// from both files, and the answer named the file that had been written.
|
|
374
|
+
const otherWhere = `${SETTINGS_FILES.directory}/${otherFile}`;
|
|
375
|
+
const tookOut = moving.length > 0 && other.kind === 'object' ? { path: join(directory, otherFile), where: otherWhere, text: other.text } : undefined;
|
|
376
|
+
let writing = where;
|
|
377
|
+
try {
|
|
378
|
+
await this.#dependencies.files.ensureDirectory(directory);
|
|
379
|
+
if (tookOut !== undefined) {
|
|
380
|
+
writing = otherWhere;
|
|
381
|
+
const { settings: without } = withoutAgentwhyHooks(otherSettings, options.invoke, moving);
|
|
382
|
+
await this.#dependencies.files.writeText(tookOut.path, `${JSON.stringify(without, null, 2)}\n`);
|
|
383
|
+
writing = where;
|
|
384
|
+
}
|
|
385
|
+
await this.#dependencies.files.writeText(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// Found by a review: only the file a hook moved out of was put back, and a write that failed part way - a full or
|
|
389
|
+
// read-only disk - may have cut this one short, as `#update` already knew. A file this run created is not
|
|
390
|
+
// there to put back: the port writes, and does not delete.
|
|
391
|
+
const thisFile = writing === where && before !== undefined ? [{ path, where, text: before }] : [];
|
|
392
|
+
const stuck = await this.#putBack([...(tookOut === undefined ? [] : [tookOut]), ...thisFile]);
|
|
393
|
+
if (!(error instanceof FileAccessError))
|
|
394
|
+
throw error;
|
|
395
|
+
const moved = moving.map((hook) => `agentwhy ${hook}`).join(' and ');
|
|
396
|
+
const otherStuck = stuck.includes(otherWhere);
|
|
397
|
+
const after = tookOut === undefined ? ''
|
|
398
|
+
: writing === otherWhere ? (otherStuck ? `, and may be cut short: it could not be put back as it was` : ', so nothing was changed')
|
|
399
|
+
: otherStuck ? `, and ${otherWhere} could not be put back: ${moved} runs from neither file now`
|
|
400
|
+
: `, so ${otherWhere} was put back as it was`;
|
|
401
|
+
const cut = writing === where && stuck.includes(where) ? ` ${where} may be cut short: it could not be put back as it was.` : '';
|
|
402
|
+
return { outcome: 'unwritable', output: `${refused}${said}${writing} could not be written${after}.${cut}\n` };
|
|
403
|
+
}
|
|
404
|
+
return { outcome: 'written', output: `${refused}${said}${whatHappensNow(missing, unwanted, denyToWrite.length, where, before !== undefined)}` };
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* R26, R27: what to take out is chosen from a list at a terminal - the hooks the file runs, and the deny rules it
|
|
408
|
+
* holds, each shown as the path it protects. Off a terminal the flags decide: `--remove` takes the hooks out, and
|
|
409
|
+
* `--unprotect` names rules, so nothing is removed that nobody named.
|
|
410
|
+
*/
|
|
411
|
+
async #remove(settings, path, where, options) {
|
|
412
|
+
const running = [...installedHooks(settings, options.invoke)];
|
|
413
|
+
const rules = fileRulesIn(settings);
|
|
414
|
+
const taking = await this.#whatToRemove(options, running, rules, where);
|
|
415
|
+
if (taking === undefined)
|
|
416
|
+
return { outcome: 'declined', output: 'Nothing was removed.\n' };
|
|
417
|
+
const { settings: withoutHooks, removed } = withoutAgentwhyHooks(settings, options.invoke, taking.hooks);
|
|
418
|
+
const { settings: next, removed: rulesRemoved } = withoutDenyEntries(withoutHooks, taking.rules);
|
|
419
|
+
if (removed === 0 && rulesRemoved === 0) {
|
|
420
|
+
return { outcome: 'unchanged', output: `Nothing of agentwhy's was found in ${where} to remove.\n` };
|
|
421
|
+
}
|
|
422
|
+
const plan = [
|
|
423
|
+
`agentwhy init --remove will take this out of ${where}:`,
|
|
424
|
+
'',
|
|
425
|
+
...(removed === 0 ? [] : [` Hooks ${taking.hooks === undefined ? 'every hook that runs agentwhy' : taking.hooks.join(' and ')}`]),
|
|
426
|
+
...(taking.rules.length === 0 ? [] : [' Deny rules', ...taking.rules.map((entry) => ` ${entry}`)]),
|
|
427
|
+
'',
|
|
428
|
+
' Everything else in the file stays. It is rewritten as two-space JSON.',
|
|
429
|
+
];
|
|
430
|
+
const consent = await this.#consent(options, 'Remove it?', plan);
|
|
431
|
+
if (typeof consent !== 'string')
|
|
432
|
+
return consent;
|
|
433
|
+
const said = consent === 'asked' ? '' : `${plan.join('\n')}\n\n`;
|
|
434
|
+
try {
|
|
435
|
+
await this.#dependencies.files.writeText(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (!(error instanceof FileAccessError))
|
|
439
|
+
throw error;
|
|
440
|
+
return { outcome: 'unwritable', output: `${said}${where} could not be written.\n` };
|
|
441
|
+
}
|
|
442
|
+
// K6: taking `refuse` out while rules stay leaves them half a block, and that is said with the way back. The way back
|
|
443
|
+
// names `watch` where it still runs here, since `init --refuse` alone would take it out (R4b).
|
|
444
|
+
const tookRefuse = running.includes('refuse') && (taking.hooks === undefined || taking.hooks.includes('refuse'));
|
|
445
|
+
const left = new Set(fileRulesIn(next).map(pathOfRule)).size;
|
|
446
|
+
const back = `agentwhy init --refuse${installedHooks(next, options.invoke).has('watch') ? ' --watch' : ''}${options.target === 'shared' ? ' --shared' : ''}`;
|
|
447
|
+
const halfBlocked = tookRefuse && left > 0
|
|
448
|
+
? `${count(left, 'protected path')} ${left === 1 ? 'is' : 'are'} no longer kept from shell commands: a command or a search can print ${left === 1 ? 'it' : 'them'}. ${back} puts it back.`
|
|
449
|
+
: '';
|
|
450
|
+
return {
|
|
451
|
+
outcome: 'written',
|
|
452
|
+
output: [
|
|
453
|
+
`${said}Removed${removed === 0 ? '' : `: ${count(removed, 'hook command')}`}${rulesRemoved === 0 ? '' : `${removed === 0 ? ': ' : ', '}${count(rulesRemoved, 'deny rule')}`}.`,
|
|
454
|
+
rulesRemoved === 0 && rules.length > 0 ? `The ${count(rules.length, 'deny rule')} in that file stayed; agentwhy init --remove lists them to pick from at a terminal.` : '',
|
|
455
|
+
halfBlocked,
|
|
456
|
+
'To set it up again: agentwhy init',
|
|
457
|
+
'',
|
|
458
|
+
]
|
|
459
|
+
.filter((line) => line !== '')
|
|
460
|
+
.join('\n'),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/** The hooks and rules to take out: ticked at a terminal, or named by the flags where nobody can be asked. */
|
|
464
|
+
async #whatToRemove(options, running, rules, where) {
|
|
465
|
+
const named = patternsOf(options.unprotect ?? []).patterns.flatMap(denyEntriesFor).filter((entry) => rules.includes(entry));
|
|
466
|
+
if (options.hooks !== undefined || !this.#canAsk || options.yes) {
|
|
467
|
+
return { ...(options.hooks === undefined ? {} : { hooks: options.hooks }), rules: named };
|
|
468
|
+
}
|
|
469
|
+
if (running.length === 0 && rules.length === 0)
|
|
470
|
+
return { rules: [] };
|
|
471
|
+
const byPath = [...rules.reduce((paths, entry) => paths.set(pathOfRule(entry), [...(paths.get(pathOfRule(entry)) ?? []), entry]), new Map())];
|
|
472
|
+
const rows = [
|
|
473
|
+
...running.map((hook) => ({ label: `${HOOK_CHOICES[hook].label.split(' - ')[0] ?? hook} - stop running agentwhy ${hook}`, detail: '', selected: false })),
|
|
474
|
+
// One row per path, not per rule: `Read(*.pem)` and `Edit(*.pem)` protect the same thing and go together.
|
|
475
|
+
...byPath.map(([path, entries]) => ({
|
|
476
|
+
label: `Unprotect ${printable(path)}`,
|
|
477
|
+
detail: `takes ${entries.join(' and ')} out of ${where}`,
|
|
478
|
+
selected: false,
|
|
479
|
+
})),
|
|
480
|
+
];
|
|
481
|
+
const chosen = await this.#dependencies.hookChooser.chooseMany('What should agentwhy stop doing here? Tick what to remove.', rows);
|
|
482
|
+
if (chosen === undefined)
|
|
483
|
+
return undefined;
|
|
484
|
+
const hooks = chosen.filter((position) => position < running.length).flatMap((position) => (running[position] === undefined ? [] : [running[position]]));
|
|
485
|
+
const picked = chosen.filter((position) => position >= running.length).flatMap((position) => {
|
|
486
|
+
const entry = byPath[position - running.length];
|
|
487
|
+
return entry === undefined ? [] : entry[1];
|
|
488
|
+
});
|
|
489
|
+
return { hooks, rules: [...new Set([...named, ...picked])] };
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* R7: `--yes` (`given`), or a person's answer at a terminal (`asked`). Anywhere else the plan is printed and nothing is
|
|
493
|
+
* written.
|
|
494
|
+
*/
|
|
495
|
+
async #consent(options, question, plan, json = []) {
|
|
496
|
+
if (options.yes)
|
|
497
|
+
return 'given';
|
|
498
|
+
const withJson = json.length === 0 ? plan : [...plan, '', ' The exact JSON:', ...json];
|
|
499
|
+
if (!this.#canAsk) {
|
|
500
|
+
// Nobody to ask: everything is printed, the JSON included, because this output is the whole record of the run.
|
|
501
|
+
return { outcome: 'not-confirmed', output: `${withJson.join('\n')}\n\nNothing was written. To write it: the same command with --yes\n` };
|
|
502
|
+
}
|
|
503
|
+
for (const shown of [false, true]) {
|
|
504
|
+
const heading = `${(shown ? withJson : plan).join('\n')}\n\n${question}`;
|
|
505
|
+
const chosen = await this.#dependencies.chooser.choose(heading, [
|
|
506
|
+
{ label: 'Yes', detail: 'write the file' },
|
|
507
|
+
...(shown || json.length === 0 ? [] : [{ label: 'Show me the exact JSON', detail: 'the lines it will add, before you decide' }]),
|
|
508
|
+
{ label: 'No', detail: 'leave it as it is' },
|
|
509
|
+
]);
|
|
510
|
+
if (chosen === 0)
|
|
511
|
+
return 'asked';
|
|
512
|
+
// The middle row asks the same question again with the JSON above it; anything else is a no.
|
|
513
|
+
if (shown || json.length === 0 || chosen !== 1)
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
return { outcome: 'declined', output: 'Nothing was written.\n' };
|
|
517
|
+
}
|
|
518
|
+
async #read(path) {
|
|
519
|
+
let text;
|
|
520
|
+
try {
|
|
521
|
+
text = await this.#dependencies.files.readText(path);
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (!(error instanceof FileAccessError))
|
|
525
|
+
throw error;
|
|
526
|
+
return { kind: error.failure === 'not-found' ? 'absent' : 'unreadable' };
|
|
527
|
+
}
|
|
528
|
+
const settings = parseJsonObject(text);
|
|
529
|
+
return settings === undefined ? { kind: 'broken' } : { kind: 'object', settings, text };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* What a person reads after the file is written: what changes for them from now on, and the two commands that follow.
|
|
534
|
+
* A command that ends without saying what to do next leaves someone who has just installed it with nothing to do.
|
|
535
|
+
*
|
|
536
|
+
* When it applies was measured (`the-agent-tells-you.md` D17): in a file that was there when the session started, a
|
|
537
|
+
* changed hook, a hook added on an event that had none and a deny rule added all applied from the next reply, with no
|
|
538
|
+
* restart. A file created during a session was not measured, so for one this run creates, only what is certain is said.
|
|
539
|
+
*/
|
|
540
|
+
function whatHappensNow(installed, stopped, rules, where, existed) {
|
|
541
|
+
return [
|
|
542
|
+
existed ? "Done. Claude Code applies it from your AI's next reply." : 'Done. Claude Code picks it up when the next session starts.',
|
|
543
|
+
'',
|
|
544
|
+
'What happens now',
|
|
545
|
+
// Both of `watch`'s events, said as one line: an agent it started, and the conversation itself, which has no
|
|
546
|
+
// `SubagentStop` of its own and is where the motivating case happened.
|
|
547
|
+
...(installed.includes('watch') ? [' - a notice, for you, when this conversation or an agent it starts copies a protected value'] : []),
|
|
548
|
+
...(installed.includes('refuse') ? [' - a shell command that names a protected file, or searches through one, is refused before it runs'] : []),
|
|
549
|
+
...(stopped.length === 0 ? [] : [` - stopped: ${stopped.map((hook) => `agentwhy ${hook}`).join(' and ')} no longer runs here`]),
|
|
550
|
+
...(rules === 0 ? [] : [` - ${count(rules, 'deny rule')} more in ${where}, which Claude Code applies to Read and Edit`]),
|
|
551
|
+
' - nothing else changes: your agents work as they did',
|
|
552
|
+
'',
|
|
553
|
+
'Next',
|
|
554
|
+
' - see what has happened so far agentwhy check',
|
|
555
|
+
' - a page with every session agentwhy start',
|
|
556
|
+
' - undo this setup agentwhy init --remove',
|
|
557
|
+
'',
|
|
558
|
+
].join('\n');
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* `block-means-blocked` K4, K5: a file protected is a file blocked, and a block is its deny rules and `refuse` both -
|
|
562
|
+
* without `refuse`, `cat .env` walks past `Read(.env)` (R4e) and `grep -r` prints it (R21a). So wherever files are
|
|
563
|
+
* protected, `refuse` is installed with them, whatever else was ticked or named; every other hook is as chosen.
|
|
564
|
+
*/
|
|
565
|
+
function withRefuse(chosen) {
|
|
566
|
+
if (!chosen.protect || chosen.hooks.includes('refuse'))
|
|
567
|
+
return chosen;
|
|
568
|
+
return { ...chosen, hooks: [...chosen.hooks, 'refuse'], withRules: true };
|
|
569
|
+
}
|
|
570
|
+
function describe(entry) {
|
|
571
|
+
const when = entry.hook === 'watch' ? 'when an agent finishes' : 'before a shell command';
|
|
572
|
+
return `${when.padEnd(24)}${entry.command}`;
|
|
573
|
+
}
|
|
574
|
+
function count(many, thing) {
|
|
575
|
+
return `${many} ${many === 1 ? thing : `${thing}s`}`;
|
|
576
|
+
}
|
|
577
|
+
function defaultPatterns() {
|
|
578
|
+
return DEFAULT_POLICY.protected.map((entry) => entry.pattern).join(', ');
|
|
579
|
+
}
|
|
580
|
+
/** A list a person reads in one line: the first few, and how many were left out. */
|
|
581
|
+
function shorten(paths, few = 3) {
|
|
582
|
+
const shown = paths.slice(0, few).join(', ');
|
|
583
|
+
return paths.length <= few ? shown : `${shown} and ${paths.length - few} more`;
|
|
584
|
+
}
|
|
585
|
+
/** The path inside a deny rule, for a line a person reads: `Read(*.pem)` is shown as `*.pem`. */
|
|
586
|
+
function pathOfRule(entry) {
|
|
587
|
+
return entry.replace(/^[A-Za-z]+\(/, '').replace(/\)$/, '');
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* One line per path being protected, saying which are new and which were copied so they stay protected (R4d). Only
|
|
591
|
+
* what is shown passes through `printable`: the rule itself stays exact, because `--unprotect` matches the string the
|
|
592
|
+
* file holds, and a path changed on its way to a map key would join two rules into one.
|
|
593
|
+
*/
|
|
594
|
+
function protectLines(added, copied) {
|
|
595
|
+
const paths = (entries) => [...new Set(entries.map(pathOfRule))];
|
|
596
|
+
return [
|
|
597
|
+
...paths(added).map((path) => ` ${printable(path).padEnd(28)}added, for reading and for editing`),
|
|
598
|
+
...paths(copied).map((path) => ` ${printable(path).padEnd(28)}copied from ${SETTINGS_FILES.shared}, so it stays protected`),
|
|
599
|
+
];
|
|
600
|
+
}
|
|
601
|
+
/** The short lines under "Good to know": only what applies to this run. */
|
|
602
|
+
function notes(installing, copied, invoke) {
|
|
603
|
+
return [
|
|
604
|
+
...(installing.includes('watch')
|
|
605
|
+
? ['watch notifies you and never the model: a macOS notification anywhere, and a terminal one in terminal claude']
|
|
606
|
+
: []),
|
|
607
|
+
...(installing.includes('refuse') ? ['refuse also blocks cat .env.example, and it reads a command as text, so a path in a variable passes'] : []),
|
|
608
|
+
WHAT_A_RULE_IS,
|
|
609
|
+
...(copied === 0
|
|
610
|
+
? []
|
|
611
|
+
: [`the ${count(copied, 'copied rule')} will not follow later changes to ${SETTINGS_FILES.shared}; Claude Code reads both files anyway`]),
|
|
612
|
+
`the hooks run "${invoke}", so it has to be on PATH when Claude Code runs them${invoke.startsWith('npx') ? ' (npx adds its start-up time to every hook)' : ''}`,
|
|
613
|
+
'nothing else in the file changes, and agentwhy init --remove takes the hooks back out',
|
|
614
|
+
];
|
|
615
|
+
}
|
|
616
|
+
/** R4f: the exact JSON, so nobody has to take the description on trust. */
|
|
617
|
+
function jsonPreview(entries, deny) {
|
|
618
|
+
const preview = {
|
|
619
|
+
...(entries.length === 0 ? {} : { hooks: Object.fromEntries(eventsOf(entries)) }),
|
|
620
|
+
...(deny.length === 0 ? {} : { permissions: { deny } }),
|
|
621
|
+
};
|
|
622
|
+
return JSON.stringify(preview, null, 2).split('\n');
|
|
623
|
+
}
|
|
624
|
+
function eventsOf(entries) {
|
|
625
|
+
const events = new Map();
|
|
626
|
+
for (const entry of entries) {
|
|
627
|
+
const list = events.get(entry.event) ?? [];
|
|
628
|
+
list.push({
|
|
629
|
+
...(entry.matcher === undefined ? {} : { matcher: entry.matcher }),
|
|
630
|
+
hooks: [{ type: 'command', command: entry.command }],
|
|
631
|
+
});
|
|
632
|
+
events.set(entry.event, list);
|
|
633
|
+
}
|
|
634
|
+
return [...events];
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* R6, R4d: which file the hooks are pointed at - the one being written when it ends up holding file rules, and the
|
|
638
|
+
* other one when only it has any. Neither holding one means the built-in default, and the hooks get no flag at all.
|
|
639
|
+
*/
|
|
640
|
+
function rulesFileFor(target, settings, otherRules, writing) {
|
|
641
|
+
if (writing.length > 0 || fileRulesIn(settings).length > 0)
|
|
642
|
+
return target;
|
|
643
|
+
return otherRules.length > 0 ? (target === 'local' ? 'shared' : 'local') : undefined;
|
|
644
|
+
}
|