Purpose: How Commando enables modules in .commando/ to import packages from the shared commando home.
Updated: 2025-11-01
Commando uses a shared dependency model where packages are installed once in ~/.commando/node_modules/ and made available to all projects. This avoids duplicating dependencies across projects while maintaining isolation.
When a Commando module (e.g., .commando/moo.ts) needs to import a package:
import cowsay from "cowsay";Bun's default behavior is to look for cowsay in:
- Project's
node_modules/directory - Parent directories walking up to root
- Not in commando home by default
We need to tell Bun to also search ~/.commando/node_modules/.
Commando sets the NODE_PATH environment variable before executing Bun:
export NODE_PATH="$COMMANDO_HOME/node_modules"
bun run .commando/module.tsThis instructs Bun to include commando home's node_modules in its module resolution search path.
- ✅ Simple: One environment variable, no configuration files
- ✅ Standard: Industry-standard approach used across Node.js/Bun ecosystem
- ✅ Clean: No warnings, no extra CLI flags
- ✅ Zero maintenance: No files to create or manage
- ✅ Portable: Works on all platforms
When Bun resolves import cowsay from "cowsay", it searches:
- Built-in modules (e.g.,
node:fs) - Relative/absolute paths (if path-like)
node_modules/in current directorynode_modules/in parent directories (walking up)- Directories in NODE_PATH ← Commando home added here
- Global installation directories
By setting NODE_PATH="$COMMANDO_HOME/node_modules", commando home becomes part of the search path.
Per Bun's official documentation and bun-module-resolution.md:
NODE_PATH - Additional module resolution paths (colon-delimited on Unix, semicolon on Windows)
Bun implements Node.js's module resolution algorithm, including NODE_PATH support.
For cases requiring explicit configuration or enhanced IDE support, Commando can use a shared tsconfig.json in commando home.
Setup (one-time):
Create ~/.commando/tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"~/.commando/node_modules/*",
"./node_modules/*",
"*"
]
},
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true
}
}Usage:
bun run --tsconfig-override="$COMMANDO_HOME/tsconfig.json" .commando/module.tsUse the tsconfig approach when:
- Explicit configuration is required for documentation
- IDE path completion needs enhancement
- Team prefers file-based configuration
- More sophisticated path mapping is needed
Pros:
- ✅ Explicit, visible configuration
- ✅ One shared file (not per-project)
- ✅ Better IDE support (potentially)
- ✅ Native TypeScript path mapping
Cons:
⚠️ Requires--tsconfig-overrideflag on everybun run⚠️ Shows harmless Bun warning: "Internal error: directory mismatch"⚠️ More complex wrapper implementation⚠️ Absolute paths required (can't use~)
When using --tsconfig-override with a tsconfig outside the project directory, Bun shows:
Internal error: directory mismatch for directory "~/.commando/tsconfig.json", fd 3.
You don't need to do anything, but this indicates a bug.
This is harmless - module resolution works correctly. It's a Bun internal warning when tsconfig is outside the project root.
Based on comprehensive testing, these approaches do not affect module resolution:
BUN_INSTALL_GLOBAL_DIR="$COMMANDO_HOME" # Only affects `bun install -g`, not resolutionThis environment variable only controls where bun install -g places packages, not where Bun looks during module resolution.
[install]
globalDir = "/path/to/commando" # Only affects install, not resolutionSimilar to BUN_INSTALL_GLOBAL_DIR, this only affects package installation location.
While technically works, this defeats the purpose of a shared configuration:
# Creates .commando/tsconfig.json in every project
# Not recommended - adds files to each projectCommando specifically avoids adding configuration files to .commando/ directories.
#!/usr/bin/env bash
set -euo pipefail
# Determine commando home location
export COMMANDO_HOME="${COMMANDO_HOME:-$HOME/.commando}"
# Set NODE_PATH for module resolution
export NODE_PATH="$COMMANDO_HOME/node_modules"
# Optional: Add project-local node_modules if it exists (for overrides)
if [[ -d ".commando/node_modules" ]]; then
export NODE_PATH=".commando/node_modules:$NODE_PATH"
fi
# Execute commando CLI
exec bun run "$COMMANDO_BIN_DIR/cli.ts" "$@"When --debug flag is used, Commando logs the NODE_PATH value:
if [[ "$COMMANDO_DEBUG" == "1" ]]; then
echo "DEBUG: COMMANDO_HOME=$COMMANDO_HOME"
echo "DEBUG: NODE_PATH=$NODE_PATH"
ficd examples/deps
export NODE_PATH="$HOME/.commando/node_modules"
bun run .commando/moo2.tsExpected output: Cowsay ASCII art (imports work)
cd examples/deps
bun run --tsconfig-override="$HOME/.commando/tsconfig.json" .commando/moo2.tsExpected output: Cowsay ASCII art + harmless warning
// test-resolution.ts
console.log("Resolving 'cowsay' from:", process.cwd());
console.log("NODE_PATH:", process.env.NODE_PATH);
try {
const resolved = Bun.resolveSync("cowsay", process.cwd());
console.log("✓ Resolved to:", resolved);
} catch (e) {
console.log("✗ Failed:", e.message);
}NODE_PATH="$HOME/.commando/node_modules" bun run test-resolution.tsWhen both local and shared packages exist, Bun uses this priority:
- Project's node_modules/ (if exists)
- NODE_PATH directories (commando home)
- Parent directory node_modules/ (walking up)
This means project-local packages override commando home packages, allowing selective overrides when needed.
NODE_PATH supports multiple directories (colon-separated on Unix):
# Local overrides commando home
export NODE_PATH=".commando/node_modules:$COMMANDO_HOME/node_modules"
# Or multiple shared locations
export NODE_PATH="/opt/shared/node_modules:$COMMANDO_HOME/node_modules"First match wins.
-
Unix/macOS: Colon
:separatorNODE_PATH="/path/one:/path/two" -
Windows: Semicolon
;separatorNODE_PATH="C:\path\one;C:\path\two"
# Cross-platform path separator
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
SEP=";"
else
SEP=":"
fi
export NODE_PATH="${local_modules}${SEP}${COMMANDO_HOME}/node_modules"Previous iterations considered symlinking .commando/node_modules → $COMMANDO_HOME/node_modules.
Why we don't use symlinks:
- ❌ Requires creating symlink per project
- ❌ Needs
.gitignoreentry for each project - ❌ Requires cleanup on project removal
- ❌ Platform-specific symlink support varies
- ✅ NODE_PATH/tsconfig are simpler and cleaner
Symlinks work but add unnecessary complexity when NODE_PATH handles it elegantly.
✅ Full support for both NODE_PATH and tsconfig paths
✅ NODE_PATH supported (legacy but works)
❌ NODE_PATH not supported ✅ Import maps (different approach)
Conclusion: NODE_PATH works across Bun and Node.js. If Commando ever supports Deno, we'd need import maps.
For most cases, NODE_PATH provides the best balance of simplicity and functionality.
Use the tsconfig approach only when:
- Explicit config is required for compliance/documentation
- IDE integration demands it
- Complex path mapping is needed
Even though NODE_PATH is environment-based, document it in your project README:
## Dependencies
This project uses Commando's shared dependency model.
Packages are installed to `~/.commando/node_modules/`
and made available via NODE_PATH.If imports fail, check:
# Is NODE_PATH set?
echo $NODE_PATH
# Does package exist in commando home?
ls -la ~/.commando/node_modules/package-name
# Can Bun resolve it?
bun run -e 'console.log(Bun.resolveSync("package-name", process.cwd()))'docs/reference/bun-env-vars.md- Bun environment variablestmp/bun-module-resolution.md- Detailed Bun resolution researchtmp/runtime-comparison-module-resolution.md- Comparison across runtimes
Comprehensive testing in tmp/:
test-experiments.sh- 7 different approaches testedtest-node-path-poc.sh- NODE_PATH proof of concepttest-tsconfig-override-simple.sh- Shared tsconfig testtest-final-comparison.sh- Side-by-side comparisonSOLUTION-SUMMARY.md- Complete analysis
All tests confirm NODE_PATH and --tsconfig-override work reliably.
Primary solution: Set NODE_PATH="$COMMANDO_HOME/node_modules" in wrapper
Alternative: Use --tsconfig-override="$COMMANDO_HOME/tsconfig.json" for explicit config
Result: Modules in .commando/ can import from commando home without per-project configuration or symlinks
Recommendation: Use NODE_PATH unless you have specific needs for explicit configuration