17 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 110 | refactor: split external-directory.ts into focused modules |
Split external-directory.ts into focused modules
Problem Statement
src/external-directory.ts is ~760 lines bundling four unrelated concerns: global node_modules discovery, path classification utilities, prompt/reason message formatting, and a tree-sitter bash parser with AST walker.
These concerns have no coupling to each other and should be independently testable, loadable, and changeable.
Issue #109 already extracted normalizePathForComparison and isPathWithinDirectory into src/path-utils.ts, but the remaining path-classification helpers, discovery logic, message formatters, and the entire tree-sitter parser still live in the monolithic file.
Goals
- Split
src/external-directory.tsinto three new modules (path-classification helpers extend the existingsrc/path-utils.tsfrom #109). - Preserve all existing behavior — pure extraction refactoring, no logic changes.
- Keep
src/external-directory.tsas a barrel re-export so downstream imports can migrate incrementally. - Enable independent testing of each concern without pulling in heavy dependencies (tree-sitter, child_process).
Non-Goals
- Changing any permission logic or policy semantics.
- Re-deduplicating
normalizePathForComparison/isPathWithinDirectory— already completed in #109.
Background
The file touches the external_directory permission surface.
Current consumers of src/external-directory.ts:
| Consumer | Imports used |
|---|---|
src/runtime.ts |
discoverGlobalNodeModulesRoot |
src/handlers/gates/external-directory.ts |
formatExternalDirectoryAskPrompt, formatExternalDirectoryDenyReason, formatExternalDirectoryUserDeniedReason, getPathBearingToolPath, isPathOutsideWorkingDirectory, isPiInfrastructureRead (note: normalizePathForComparison already imports from path-utils) |
src/handlers/gates/bash-external-directory.ts |
extractExternalPathsFromBashCommand, formatBashExternalDirectoryAskPrompt, formatBashExternalDirectoryDenyReason, formatExternalDirectoryHardStopHint |
src/handlers/gates/tool.ts |
PATH_BEARING_TOOLS |
src/handlers/gates/skill-read.ts |
normalizePathForComparison (already imports from path-utils) |
Test files:
tests/external-directory.test.ts— tests path utils, discovery, messages, and constants.tests/bash-external-directory.test.ts— tests tree-sitter path extraction.tests/handlers/gates/external-directory.test.ts— gate-level integration.tests/handlers/gates/bash-external-directory.test.ts— gate-level integration.
Design Overview
Split into four new modules, each owning one concern:
src/node-modules-discovery.ts
walkUpToNodeModules()(internal)discoverGlobalNodeModulesViaSubprocess()(internal)discoverGlobalNodeModulesRoot()(exported)
Dependencies: node:child_process, node:fs, node:path, node:url.
src/path-utils.ts (extend existing)
Issue #109 already created this file with normalizePathForComparison and isPathWithinDirectory.
Add the remaining path-classification helpers:
isPathOutsideWorkingDirectory()(moved fromexternal-directory.ts)getPathBearingToolPath()(moved fromexternal-directory.ts)isPiInfrastructureRead()(moved fromexternal-directory.ts)isSafeSystemPath()(moved fromexternal-directory.ts)SAFE_SYSTEM_PATHS(moved fromexternal-directory.ts)PATH_BEARING_TOOLS(moved fromexternal-directory.ts)READ_ONLY_PATH_BEARING_TOOLS(moved fromexternal-directory.ts)
Dependencies: adds node:path (for join), ./common (for getNonEmptyString, toRecord) to existing node:os, node:path.
src/external-directory-messages.ts
formatExternalDirectoryHardStopHint()(exported)formatExternalDirectoryAskPrompt()(exported)formatExternalDirectoryDenyReason()(exported)formatExternalDirectoryUserDeniedReason()(exported)formatBashExternalDirectoryAskPrompt()(exported)formatBashExternalDirectoryDenyReason()(exported)
Dependencies: none (pure string builders).
src/bash-path-extractor.ts
- All tree-sitter types (
TSNode,TSParser) - Parser lifecycle (
initParser,getParser,resetParserForTesting) - AST walker (
resolveNodeText,collectPathCandidateTokens,collectPatternCommandTokens) - Pattern-first command config (
PATTERN_FIRST_COMMANDS,SKIP_SUBTREE_TYPES, etc.) - Token classification (
classifyTokenAsPathCandidate,URL_PATTERN,REGEX_METACHAR_PATTERN) extractExternalPathsFromBashCommand()(exported)
Dependencies: web-tree-sitter, tree-sitter-bash, node:module, node:path.
Imports normalizePathForComparison, isPathOutsideWorkingDirectory from ./path-utils.
src/external-directory.ts (barrel)
Becomes a thin barrel that re-exports everything from the three new modules plus ./path-utils.
All existing consumer imports continue to work unchanged.
Downstream consumers can optionally migrate to direct imports in follow-up work.
Module-Level Changes
New files
| File | Contents |
|---|---|
src/node-modules-discovery.ts |
Global node_modules resolution |
src/external-directory-messages.ts |
6 format* pure string builders |
src/bash-path-extractor.ts |
Tree-sitter parser, AST walker, extractExternalPathsFromBashCommand |
Changed files
| File | Change |
|---|---|
src/path-utils.ts |
Add remaining path-classification helpers (isPathOutsideWorkingDirectory, getPathBearingToolPath, isPiInfrastructureRead, isSafeSystemPath, SAFE_SYSTEM_PATHS, PATH_BEARING_TOOLS, READ_ONLY_PATH_BEARING_TOOLS) |
src/external-directory.ts |
Replace implementation with barrel re-exports from three new modules plus path-utils |
New test files
| File | Contents |
|---|---|
tests/node-modules-discovery.test.ts |
Extracted from tests/external-directory.test.ts — discovery tests |
tests/external-directory-messages.test.ts |
Extracted from tests/external-directory.test.ts — message format tests |
Changed test files
| File | Change |
|---|---|
tests/path-utils.test.ts |
Add tests for newly moved helpers (isPathOutsideWorkingDirectory, getPathBearingToolPath, isPiInfrastructureRead, isSafeSystemPath, constants) |
tests/external-directory.test.ts |
Reduced to a thin smoke test verifying the barrel re-exports work, or deleted entirely if all tests migrate |
tests/bash-external-directory.test.ts |
Update import from ../src/external-directory to ../src/bash-path-extractor (or keep barrel import) |
No changes needed
| File | Reason |
|---|---|
tests/handlers/gates/external-directory.test.ts |
Imports from gate module, not from external-directory.ts directly |
tests/handlers/gates/bash-external-directory.test.ts |
Same — imports from gate module |
src/handlers/gates/* |
All import from ../../external-directory barrel, which continues to work |
src/runtime.ts |
Imports discoverGlobalNodeModulesRoot from barrel |
schemas/permissions.schema.json |
No schema changes |
config/config.example.json |
No config changes |
Architecture docs
| File | Action |
|---|---|
docs/architecture/target-architecture.md |
Check if it references external-directory.ts and update to reflect the split |
Test Impact Analysis
-
New unit tests enabled by extraction:
tests/node-modules-discovery.test.tscan mockchild_processandfswithout affecting path tests.tests/path-utils.test.tsgains path-classification tests that run without tree-sitter or subprocess mocks (file already exists from #109).tests/external-directory-messages.test.tsneeds zero mocks — pure string assertions.
-
Existing tests that become redundant:
tests/external-directory.test.tscurrently tests all four concerns in one file with shared mocks. After extraction, its individual test blocks migrate to the new/extended test files. The barrel file itself needs only a re-export smoke test (or can be deleted).
-
Existing tests that must stay as-is:
tests/bash-external-directory.test.ts— exercisesextractExternalPathsFromBashCommandend-to-end with real tree-sitter.tests/handlers/gates/*.test.ts— gate-level integration, unaffected by internal splits.
TDD Order
Step 1 — Extract node-modules-discovery.ts with tests
- Create
src/node-modules-discovery.tswith the discovery functions. - Create
tests/node-modules-discovery.test.tsby extractingdiscoverGlobalNodeModulesRoottests fromtests/external-directory.test.ts. - Update
src/external-directory.tsto import-and-re-export from./node-modules-discovery. - Remove the original implementation from
external-directory.ts. - Verify all tests pass.
Commit: refactor: extract node-modules-discovery module (#110)
Step 2 — Move remaining path helpers into path-utils.ts
- Move
isPathOutsideWorkingDirectory,getPathBearingToolPath,isPiInfrastructureRead,isSafeSystemPath,SAFE_SYSTEM_PATHS,PATH_BEARING_TOOLS,READ_ONLY_PATH_BEARING_TOOLSfromexternal-directory.tsintosrc/path-utils.ts. - Move corresponding tests from
tests/external-directory.test.tsintotests/path-utils.test.ts. - Update
src/external-directory.tsto re-export the moved symbols from./path-utils. - Verify all tests pass.
Commit: refactor: move path-classification helpers into path-utils (#110)
Step 3 — Extract external-directory-messages.ts with tests
- Create
src/external-directory-messages.tswith the 6format*functions. - Create
tests/external-directory-messages.test.tsby extracting message tests fromtests/external-directory.test.ts. - Update
src/external-directory.tsto import-and-re-export from./external-directory-messages. - Remove the original implementation from
external-directory.ts. - Verify all tests pass.
Commit: refactor: extract external-directory-messages module (#110)
Step 4 — Extract bash-path-extractor.ts
- Create
src/bash-path-extractor.tswith tree-sitter parser, AST walker, andextractExternalPathsFromBashCommand. - Update
tests/bash-external-directory.test.tsimports if needed (or keep barrel). - Update
src/external-directory.tsto import-and-re-export from./bash-path-extractor. - Remove the original implementation from
external-directory.ts. - Verify all tests pass.
Commit: refactor: extract bash-path-extractor module (#110)
Step 5 — Clean up barrel and original test file
- Verify
src/external-directory.tsis now a pure barrel of re-exports. - Reduce
tests/external-directory.test.tsto a smoke test (or delete if fully migrated). - Run full test suite.
Commit: refactor: finalize external-directory barrel re-exports (#110)
Step 6 — Update architecture docs
- Update
docs/architecture/target-architecture.mdif it references the monolithicexternal-directory.ts.
Commit: docs: update architecture for external-directory split (#110)
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Could this silently weaken a permission? | No — pure extraction refactoring; no logic changes; barrel preserves all exports. |
Circular dependency between bash-path-extractor and path-utils |
bash-path-extractor imports from path-utils; no reverse dependency. Barrel re-exports both without creating a cycle. |
| Tree-sitter WASM loading breaks after move | bash-path-extractor.ts preserves the same createRequire(import.meta.url) pattern — import.meta.url resolves to the new file's location, but WASM resolution uses require.resolve which walks node_modules, so it works from any file in src/. |
| Test mocks leak across modules | Each new test file has its own vi.mock() scope. Discovery tests mock child_process/fs; path tests mock os; message tests need no mocks. |
Open Questions
- Should downstream consumers (
src/handlers/gates/*,src/runtime.ts) be updated to import directly from the new modules in this PR, or deferred to a follow-up? Recommendation: defer — the barrel ensures backward compatibility and keeps the PR focused on extraction.