Development Guides Debugging - aku11i/phantom GitHub Wiki
This guide covers debugging techniques and tools for troubleshooting Phantom during development.
Node.js includes a built-in debugger that works with Chrome DevTools and VS Code.
# Start with inspector
node --inspect dist/phantom.js create test
# Start with inspector and break on first line
node --inspect-brk dist/phantom.js create test
# Specific port
node --inspect=9229 dist/phantom.js list
- Start Node with
--inspect
- Open Chrome and navigate to
chrome://inspect
- Click "inspect" under Remote Target
- Use DevTools to debug
Source: Recommended .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Phantom CLI",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/phantom.js",
"args": ["create", "test-phantom"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"sourceMaps": true,
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
"name": "Debug Current Test",
"skipFiles": ["<node_internals>/**"],
"program": "${file}",
"args": ["--test"],
"console": "integratedTerminal"
},
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"port": 9229,
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true
}
]
}
Since Phantom is built with esbuild, source maps enable debugging TypeScript:
# Build with source maps (default)
pnpm build
# Debug TypeScript directly
node --enable-source-maps --inspect dist/phantom.js
// Add debug logging to handler
export async function createHandler(args: string[]): Promise<void> {
console.log("createHandler called with:", args);
const [name, branch] = args;
console.log("Parsed:", { name, branch });
// Existing code...
}
Debug Git operations:
// In src/core/git/executor.ts
export class GitExecutor {
execute(args: string[]): Result<string> {
console.log("Git command:", "git", args.join(" "));
const { status, stdout, stderr } = spawnSync("git", args);
console.log("Git exit code:", status);
console.log("Git stdout:", stdout);
console.log("Git stderr:", stderr);
// Existing code...
}
}
// Debug path construction
import { getWorktreePath } from "../paths.js";
const path = getWorktreePath(repoName, phantomName);
console.log("Resolved path:", {
repoName,
phantomName,
resultPath: path,
exists: existsSync(path)
});
// Helper to log Result types
function debugResult<T>(result: Result<T>, label: string): void {
if (result.ok) {
console.log(`${label} - Success:`, result.value);
} else {
console.log(`${label} - Error:`, result.error.message);
console.error(result.error.stack);
}
}
// Usage
const result = await createWorktree(name);
debugResult(result, "createWorktree");
# Run specific test file
node --inspect --test src/core/worktree/create.test.ts
# Run test matching pattern
node --inspect --test --test-name-pattern="validation"
import { mock } from "node:test";
// Log mock calls
const mockExecute = mock.fn((args) => {
console.log("Mock called with:", args);
return Result.ok("mocked response");
});
// After test
console.log("Mock call count:", mockExecute.mock.calls.length);
console.log("All calls:", mockExecute.mock.calls);
import { test } from "node:test";
// Increase timeout for debugging
test("slow operation", { timeout: 30000 }, async () => {
// Set breakpoints and debug without timeout
});
When unsure where an issue occurs:
console.log("Point A reached");
// Half of the code
console.log("Point B reached");
// Other half
console.log("Point C reached");
// Create debug helper
function inspectState(label: string, state: any): void {
console.log(`\n=== ${label} ===`);
console.log(JSON.stringify(state, null, 2));
console.log("=================\n");
}
// Use throughout code
inspectState("Before validation", { name, branch });
inspectState("After Git operation", { result, worktrees });
// Enhance error information
try {
await riskyOperation();
} catch (error) {
console.error("Operation failed");
console.error("Error:", error.message);
console.error("Stack:", error.stack);
console.error("Context:", { name, branch, path });
throw error;
}
// Environment-based debugging
const DEBUG = process.env.DEBUG_PHANTOM === "true";
if (DEBUG) {
console.log("Debug info:", data);
}
// Or use a debug function
function debug(...args: any[]): void {
if (process.env.DEBUG_PHANTOM) {
console.log("[DEBUG]", ...args);
}
}
# Check worktree state
git worktree list --porcelain
# Check Git internals
ls -la .git/worktrees/
# View worktree config
cat .git/worktrees/phantom-name/gitdir
// Debug Git worktree issues
async function debugWorktreeState(name: string): Promise<void> {
const git = new GitExecutor();
// List all worktrees
const listResult = git.execute(["worktree", "list", "--porcelain"]);
console.log("All worktrees:", listResult);
// Check specific worktree
const worktreePath = getWorktreePath(repoName, name);
console.log("Checking path:", worktreePath);
console.log("Path exists:", existsSync(worktreePath));
console.log("Is directory:", existsSync(worktreePath) &&
statSync(worktreePath).isDirectory());
// Check Git directory
const gitDir = join(worktreePath, ".git");
console.log("Git dir:", gitDir);
console.log("Git file contents:",
existsSync(gitDir) ? readFileSync(gitDir, "utf-8") : "Not found");
}
// Simple timing
console.time("createWorktree");
const result = await createWorktree(name);
console.timeEnd("createWorktree");
// Detailed timing
const start = performance.now();
const result = await operation();
const duration = performance.now() - start;
console.log(`Operation took ${duration.toFixed(2)}ms`);
// Memory usage tracking
function logMemoryUsage(label: string): void {
const usage = process.memoryUsage();
console.log(`Memory (${label}):`);
console.log(` RSS: ${(usage.rss / 1024 / 1024).toFixed(2)} MB`);
console.log(` Heap Used: ${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
console.log(` Heap Total: ${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`);
}
logMemoryUsage("Before operation");
await heavyOperation();
logMemoryUsage("After operation");
# Start with profiling
node --prof dist/phantom.js create test
# Process the log
node --prof-process isolate-*.log > profile.txt
# Analyze profile.txt for bottlenecks
// Add verbose mode
const VERBOSE = process.env.PHANTOM_VERBOSE === "true";
function verbose(message: string, data?: any): void {
if (VERBOSE) {
console.error(`[VERBOSE] ${message}`, data || "");
}
}
// Use throughout
verbose("Creating worktree", { name, branch });
verbose("Git command", { cmd: args.join(" ") });
// Enhanced error class
class PhantomError extends Error {
constructor(
message: string,
public code: string,
public context: Record<string, any>
) {
super(message);
this.name = "PhantomError";
}
}
// Usage
throw new PhantomError(
"Failed to create worktree",
"WORKTREE_CREATE_FAILED",
{ name, branch, gitError: stderr }
);
Create a debug utility:
// src/core/debug.ts
const DEBUG = process.env.DEBUG?.includes("phantom");
export function debug(namespace: string) {
return (...args: any[]) => {
if (DEBUG || process.env.DEBUG?.includes(namespace)) {
console.error(`[${namespace}]`, ...args);
}
};
}
// Usage
import { debug } from "../debug.js";
const log = debug("phantom:git");
log("Executing command", args);
// Pretty print for debugging
export function inspect(obj: any, label?: string): void {
if (label) console.log(`\n${label}:`);
console.dir(obj, {
depth: null,
colors: true,
compact: false
});
}
// Breakpoint helper
export function breakpoint(condition: boolean = true): void {
if (condition) {
debugger;
}
}
class ExecutionTracer {
private stack: string[] = [];
enter(name: string): void {
this.stack.push(name);
console.log("→".repeat(this.stack.length), name);
}
exit(name: string): void {
console.log("←".repeat(this.stack.length), name);
this.stack.pop();
}
}
const tracer = new ExecutionTracer();
tracer.enter("createWorktree");
// ... code ...
tracer.exit("createWorktree");
function assertDefined<T>(
value: T | undefined,
message: string
): asserts value is T {
if (value === undefined) {
console.error("Assertion failed:", message);
console.trace();
throw new Error(message);
}
}
// Usage
assertDefined(worktreeName, "Worktree name is required");
// Capture state at different points
const stateHistory: any[] = [];
function captureState(label: string, state: any): void {
stateHistory.push({
timestamp: Date.now(),
label,
state: JSON.parse(JSON.stringify(state))
});
}
function dumpStateHistory(): void {
console.log("State History:");
stateHistory.forEach(({ timestamp, label, state }) => {
console.log(`\n[${new Date(timestamp).toISOString()}] ${label}:`);
console.log(JSON.stringify(state, null, 2));
});
}
// Bad
console.log(result);
console.log(name);
// Good
console.log("Worktree creation result:", result);
console.log("Processing phantom name:", name);
// Ensure debug code doesn't reach production
if (process.env.NODE_ENV !== "production") {
console.log("Debug:", sensitiveData);
}
# Find remaining debug statements
grep -r "console\." src/
grep -r "debugger" src/
# Always enable source maps in development
node --enable-source-maps dist/phantom.js
# Build with inline source maps for debugging
# (modify build.ts temporarily)
sourcemap: "inline"
Effective debugging in Phantom involves:
- Right Tools - VS Code debugger, Chrome DevTools
- Strategic Logging - Meaningful debug output
- State Inspection - Understanding data flow
- Isolation - Debug one thing at a time
- Clean Up - Remove debug code before committing
Remember: Good debugging is methodical and patient. Take time to understand the issue before jumping to solutions.