From 87413c8cbb13c41b0268f8279451fe5a7d632aef Mon Sep 17 00:00:00 2001 From: John Lindquist Date: Tue, 9 Dec 2025 13:37:17 -0700 Subject: [PATCH] feat: Add trust and subfolder config options (closes #33, #34) - Add `wt config set trust true/false` to bypass setup command confirmations without needing to pass -t flag every time (issue #34) - Add `wt config set subfolder true/false` to create worktrees in a my-app-worktrees/feature pattern instead of my-app-feature siblings (issue #33) - Add getter/setter functions in config.ts for both options - Update confirmCommands() to check config trust setting - Update resolveWorktreePath() to support subfolder naming pattern - Add 8 new tests for the config options (98 total tests pass) --- src/commands/config.ts | 42 +++++++++++++++++-- src/config.ts | 33 +++++++++++++++ src/index.ts | 28 +++++++++++++ src/utils/paths.ts | 18 +++++++-- src/utils/tui.ts | 5 ++- test/config.test.ts | 92 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 210 insertions(+), 8 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 86192ca..f1ac2d9 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import { getDefaultEditor, setDefaultEditor, getGitProvider, setGitProvider, getConfigPath, getDefaultWorktreePath, setDefaultWorktreePath, clearDefaultWorktreePath } from '../config.js'; +import { getDefaultEditor, setDefaultEditor, getGitProvider, setGitProvider, getConfigPath, getDefaultWorktreePath, setDefaultWorktreePath, clearDefaultWorktreePath, getTrust, setTrust, getWorktreeSubfolder, setWorktreeSubfolder } from '../config.js'; export async function configHandler(action: 'get' | 'set' | 'path' | 'clear', key?: string, value?: string) { try { @@ -18,9 +18,23 @@ export async function configHandler(action: 'get' | 'set' | 'path' | 'clear', ke } else { console.log(chalk.blue(`Default worktree path is not set (using sibling directory behavior).`)); } + } else if (key === 'trust') { + const trust = getTrust(); + console.log(chalk.blue(`Trust mode is currently: ${chalk.bold(trust ? 'enabled' : 'disabled')}`)); + if (trust) { + console.log(chalk.gray(` Setup commands will run without confirmation prompts.`)); + } + } else if (key === 'subfolder') { + const subfolder = getWorktreeSubfolder(); + console.log(chalk.blue(`Subfolder mode is currently: ${chalk.bold(subfolder ? 'enabled' : 'disabled')}`)); + if (subfolder) { + console.log(chalk.gray(` Worktrees will be created in: my-app-worktrees/feature`)); + } else { + console.log(chalk.gray(` Worktrees will be created as: my-app-feature (siblings)`)); + } } else { console.error(chalk.red(`Unknown configuration key to get: ${key}`)); - console.error(chalk.yellow(`Available keys: editor, provider, worktreepath`)); + console.error(chalk.yellow(`Available keys: editor, provider, worktreepath, trust, subfolder`)); process.exit(1); } break; @@ -40,6 +54,22 @@ export async function configHandler(action: 'get' | 'set' | 'path' | 'clear', ke setDefaultWorktreePath(value); const resolvedPath = getDefaultWorktreePath(); console.log(chalk.green(`Default worktree path set to: ${chalk.bold(resolvedPath)}`)); + } else if (key === 'trust' && value !== undefined) { + const trustValue = value.toLowerCase() === 'true' || value === '1'; + setTrust(trustValue); + console.log(chalk.green(`Trust mode ${trustValue ? 'enabled' : 'disabled'}.`)); + if (trustValue) { + console.log(chalk.gray(` Setup commands will now run without confirmation prompts.`)); + } + } else if (key === 'subfolder' && value !== undefined) { + const subfolderValue = value.toLowerCase() === 'true' || value === '1'; + setWorktreeSubfolder(subfolderValue); + console.log(chalk.green(`Subfolder mode ${subfolderValue ? 'enabled' : 'disabled'}.`)); + if (subfolderValue) { + console.log(chalk.gray(` Worktrees will now be created in: my-app-worktrees/feature`)); + } else { + console.log(chalk.gray(` Worktrees will now be created as: my-app-feature (siblings)`)); + } } else if (key === 'editor') { console.error(chalk.red(`You must provide an editor name.`)); process.exit(1); @@ -49,9 +79,15 @@ export async function configHandler(action: 'get' | 'set' | 'path' | 'clear', ke } else if (key === 'worktreepath') { console.error(chalk.red(`You must provide a path.`)); process.exit(1); + } else if (key === 'trust') { + console.error(chalk.red(`You must provide a value (true or false).`)); + process.exit(1); + } else if (key === 'subfolder') { + console.error(chalk.red(`You must provide a value (true or false).`)); + process.exit(1); } else { console.error(chalk.red(`Unknown configuration key to set: ${key}`)); - console.error(chalk.yellow(`Available keys: editor, provider, worktreepath`)); + console.error(chalk.yellow(`Available keys: editor, provider, worktreepath, trust, subfolder`)); process.exit(1); } break; diff --git a/src/config.ts b/src/config.ts index a0bc91f..c3ce0a0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,6 +14,8 @@ interface ConfigSchema { defaultEditor: string; gitProvider: 'gh' | 'glab'; defaultWorktreePath?: string; + trust?: boolean; + worktreeSubfolder?: boolean; } // Initialize conf with a schema and project name @@ -32,6 +34,15 @@ const schema = { type: 'string', // No default - falls back to sibling directory behavior when not set }, + trust: { + type: 'boolean', + default: false, // Default is to require confirmation for setup commands + }, + worktreeSubfolder: { + type: 'boolean', + default: false, // Default is sibling directory behavior (my-app-feature) + // When true: my-app-worktrees/feature subfolder pattern + }, } as const; const config = new Conf({ @@ -94,4 +105,26 @@ export function setDefaultWorktreePath(worktreePath: string): void { // Function to clear the default worktree path export function clearDefaultWorktreePath(): void { config.delete('defaultWorktreePath'); +} + +// Function to get the trust setting (bypass setup command confirmation) +export function getTrust(): boolean { + return config.get('trust') ?? false; +} + +// Function to set the trust setting +export function setTrust(trust: boolean): void { + config.set('trust', trust); +} + +// Function to get the worktree subfolder setting +// When true: creates worktrees in my-app-worktrees/feature pattern +// When false: creates worktrees as my-app-feature siblings +export function getWorktreeSubfolder(): boolean { + return config.get('worktreeSubfolder') ?? false; +} + +// Function to set the worktree subfolder setting +export function setWorktreeSubfolder(subfolder: boolean): void { + config.set('worktreeSubfolder', subfolder); } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index beaa0e9..a50bd24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,6 +199,24 @@ program .description("Set the default directory for new worktrees.") .action((worktreePath) => configHandler("set", "worktreepath", worktreePath)) ) + .addCommand( + new Command("trust") + .argument( + "", + "Enable or disable trust mode (true/false)" + ) + .description("Set trust mode to skip setup command confirmations.") + .action((value) => configHandler("set", "trust", value)) + ) + .addCommand( + new Command("subfolder") + .argument( + "", + "Enable or disable subfolder mode (true/false)" + ) + .description("Set subfolder mode for worktree paths (my-app-worktrees/feature).") + .action((value) => configHandler("set", "subfolder", value)) + ) ) .addCommand( new Command("get") @@ -218,6 +236,16 @@ program .description("Get the currently configured default worktree directory.") .action(() => configHandler("get", "worktreepath")) ) + .addCommand( + new Command("trust") + .description("Get the current trust mode setting.") + .action(() => configHandler("get", "trust")) + ) + .addCommand( + new Command("subfolder") + .description("Get the current subfolder mode setting.") + .action(() => configHandler("get", "subfolder")) + ) ) .addCommand( new Command("clear") diff --git a/src/utils/paths.ts b/src/utils/paths.ts index 40228d0..7c4f65a 100644 --- a/src/utils/paths.ts +++ b/src/utils/paths.ts @@ -1,5 +1,5 @@ import { join, dirname, basename, resolve } from "node:path"; -import { getDefaultWorktreePath } from "../config.js"; +import { getDefaultWorktreePath, getWorktreeSubfolder } from "../config.js"; import { getRepoName } from "./git.js"; /** @@ -48,10 +48,11 @@ export interface ResolveWorktreePathOptions { /** * Resolve the full path for a new worktree * - * Handles three cases: + * Handles four cases: * 1. Custom path provided - use it directly * 2. Global defaultWorktreePath configured - use it with repo namespace - * 3. No config - create sibling directory + * 3. Subfolder mode enabled - create in my-app-worktrees/feature pattern + * 4. No config - create sibling directory (my-app-feature) * * @param branchName - The branch name to create worktree for * @param options - Configuration options @@ -88,9 +89,18 @@ export async function resolveWorktreePath( return join(defaultWorktreePath, worktreeName); } - // Case 3: No config - create sibling directory + // Check if subfolder mode is enabled + const useSubfolder = getWorktreeSubfolder(); const parentDir = dirname(cwd); const currentDirName = basename(cwd); + + if (useSubfolder) { + // Case 3: Subfolder mode - create in my-app-worktrees/feature pattern + // This keeps worktrees organized in a dedicated folder + return join(parentDir, `${currentDirName}-worktrees`, worktreeName); + } + + // Case 4: No config - create sibling directory (my-app-feature) return join(parentDir, `${currentDirName}-${worktreeName}`); } diff --git a/src/utils/tui.ts b/src/utils/tui.ts index 6aa760c..6c19a5b 100644 --- a/src/utils/tui.ts +++ b/src/utils/tui.ts @@ -1,6 +1,7 @@ import prompts from "prompts"; import chalk from "chalk"; import { getWorktrees, WorktreeInfo } from "./git.js"; +import { getTrust } from "../config.js"; /** * Interactive worktree selector @@ -165,7 +166,9 @@ export async function confirmCommands(commands: string[], options: { } = {}): Promise { const { title = "The following commands will be executed:", trust = false } = options; - if (trust) { + // Check both the flag and the config setting + // If either is true, skip confirmation + if (trust || getTrust()) { return true; } diff --git a/test/config.test.ts b/test/config.test.ts index 247eea2..798b43e 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -381,4 +381,96 @@ describe('Config Management', () => { expect(invalidResult.stderr).toContain('Valid providers: gh, glab'); }); }); + + describe('Trust config (Issue #34)', () => { + it('should get trust mode default (disabled)', async () => { + const result = await runConfig(['get', 'trust']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Trust mode is currently'); + }); + + it('should set trust mode to true', async () => { + const result = await runConfig(['set', 'trust', 'true']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Trust mode enabled'); + + const config = await getConfigFileContent(); + expect(config).toBeDefined(); + expect(config.trust).toBe(true); + }); + + it('should set trust mode to false', async () => { + // First enable it + await runConfig(['set', 'trust', 'true']); + + // Then disable it + const result = await runConfig(['set', 'trust', 'false']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Trust mode disabled'); + + const config = await getConfigFileContent(); + expect(config).toBeDefined(); + expect(config.trust).toBe(false); + }); + + it('should accept 1 as truthy value', async () => { + const result = await runConfig(['set', 'trust', '1']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Trust mode enabled'); + + const config = await getConfigFileContent(); + expect(config.trust).toBe(true); + }); + }); + + describe('Subfolder config (Issue #33)', () => { + it('should get subfolder mode default (disabled)', async () => { + const result = await runConfig(['get', 'subfolder']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Subfolder mode is currently'); + }); + + it('should set subfolder mode to true', async () => { + const result = await runConfig(['set', 'subfolder', 'true']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Subfolder mode enabled'); + expect(result.stdout).toContain('my-app-worktrees/feature'); + + const config = await getConfigFileContent(); + expect(config).toBeDefined(); + expect(config.worktreeSubfolder).toBe(true); + }); + + it('should set subfolder mode to false', async () => { + // First enable it + await runConfig(['set', 'subfolder', 'true']); + + // Then disable it + const result = await runConfig(['set', 'subfolder', 'false']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Subfolder mode disabled'); + expect(result.stdout).toContain('siblings'); + + const config = await getConfigFileContent(); + expect(config).toBeDefined(); + expect(config.worktreeSubfolder).toBe(false); + }); + + it('should accept 1 as truthy value', async () => { + const result = await runConfig(['set', 'subfolder', '1']); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Subfolder mode enabled'); + + const config = await getConfigFileContent(); + expect(config.worktreeSubfolder).toBe(true); + }); + }); });