workon 3.5.1 → 3.6.0

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/dist/cli.js CHANGED
@@ -1458,7 +1458,7 @@ function deriveProjectIdentifier(projectPath) {
1458
1458
  function getWorktreesDirForProject(projectIdentifier) {
1459
1459
  return join(homedir(), WORKON_DIR, WORKTREES_SUBDIR, projectIdentifier);
1460
1460
  }
1461
- var exec2, WORKON_DIR, WORKTREES_SUBDIR, HOOK_DIR, SETUP_HOOK, WorktreeManager;
1461
+ var exec2, WORKON_DIR, WORKTREES_SUBDIR, HOOK_DIR, SETUP_HOOK, TEARDOWN_HOOK, WorktreeManager;
1462
1462
  var init_worktree = __esm({
1463
1463
  "src/lib/worktree.ts"() {
1464
1464
  "use strict";
@@ -1467,6 +1467,7 @@ var init_worktree = __esm({
1467
1467
  WORKTREES_SUBDIR = "worktrees";
1468
1468
  HOOK_DIR = ".workon";
1469
1469
  SETUP_HOOK = "worktree-setup.sh";
1470
+ TEARDOWN_HOOK = "worktree-teardown.sh";
1470
1471
  WorktreeManager = class {
1471
1472
  projectPath;
1472
1473
  projectIdentifier;
@@ -1569,11 +1570,12 @@ var init_worktree = __esm({
1569
1570
  if (worktree.isMain) {
1570
1571
  throw new Error("Cannot remove the main worktree");
1571
1572
  }
1572
- if (!force && await this.hasUncommittedChanges(name)) {
1573
+ const pathMissing = !existsSync2(worktree.path);
1574
+ if (!pathMissing && !force && await this.hasUncommittedChanges(name)) {
1573
1575
  throw new Error(`Worktree '${name}' has uncommitted changes. Use --force to remove anyway.`);
1574
1576
  }
1575
1577
  const args = ["worktree", "remove"];
1576
- if (force) {
1578
+ if (force || pathMissing) {
1577
1579
  args.push("--force");
1578
1580
  }
1579
1581
  args.push(worktree.path);
@@ -1592,6 +1594,9 @@ var init_worktree = __esm({
1592
1594
  if (!worktree) {
1593
1595
  throw new Error(`Worktree '${nameOrPath}' not found`);
1594
1596
  }
1597
+ if (!existsSync2(worktree.path)) {
1598
+ return false;
1599
+ }
1595
1600
  const worktreeGit = simpleGit2(worktree.path);
1596
1601
  const status = await worktreeGit.status();
1597
1602
  return !status.isClean();
@@ -1680,6 +1685,42 @@ var init_worktree = __esm({
1680
1685
  });
1681
1686
  return { stdout, stderr };
1682
1687
  }
1688
+ /**
1689
+ * Check if a pre-teardown hook exists
1690
+ */
1691
+ hasTeardownHook() {
1692
+ const hookPath = this.getTeardownHookPath();
1693
+ return existsSync2(hookPath);
1694
+ }
1695
+ /**
1696
+ * Get the path to the teardown hook
1697
+ */
1698
+ getTeardownHookPath() {
1699
+ return join(this.projectPath, HOOK_DIR, TEARDOWN_HOOK);
1700
+ }
1701
+ /**
1702
+ * Run the pre-teardown hook for a worktree
1703
+ */
1704
+ async runPreTeardownHook(worktreePath) {
1705
+ const hookPath = this.getTeardownHookPath();
1706
+ if (!existsSync2(hookPath)) {
1707
+ throw new Error("Teardown hook not found");
1708
+ }
1709
+ try {
1710
+ chmodSync(hookPath, "755");
1711
+ } catch {
1712
+ }
1713
+ const env = {
1714
+ ...process.env,
1715
+ WORKTREE_PATH: worktreePath,
1716
+ PROJECT_PATH: this.projectPath
1717
+ };
1718
+ const { stdout, stderr } = await exec2(hookPath, {
1719
+ cwd: worktreePath,
1720
+ env
1721
+ });
1722
+ return { stdout, stderr };
1723
+ }
1683
1724
  /**
1684
1725
  * Parse the porcelain output of git worktree list
1685
1726
  */
@@ -1933,6 +1974,7 @@ var init_utils = __esm({
1933
1974
  import { Command } from "commander";
1934
1975
  import chalk from "chalk";
1935
1976
  import path2 from "path";
1977
+ import { existsSync as existsSync3 } from "fs";
1936
1978
  function createListCommand(ctx) {
1937
1979
  const { config, log } = ctx;
1938
1980
  return new Command("list").description("List worktrees for the current project").option("-a, --all", "Show all worktrees (including main)").action(async (options) => {
@@ -1966,9 +2008,10 @@ Worktrees for ${displayName}:`));
1966
2008
  for (const wt of worktrees) {
1967
2009
  const isManaged = managedPaths.has(wt.path);
1968
2010
  const mainLabel = wt.isMain ? chalk.gray(" (main)") : "";
1969
- const externalLabel = !wt.isMain && !isManaged ? chalk.yellow(" (external)") : "";
2011
+ const missingLabel = !wt.isMain && !existsSync3(wt.path) ? chalk.red(" (missing)") : "";
2012
+ const externalLabel = !wt.isMain && !isManaged && !missingLabel ? chalk.yellow(" (external)") : "";
1970
2013
  const branchDisplay = wt.branch === "(detached)" ? chalk.yellow(wt.branch) : chalk.green(wt.branch);
1971
- console.log(` ${chalk.cyan(wt.name)}${mainLabel}${externalLabel}`);
2014
+ console.log(` ${chalk.cyan(wt.name)}${mainLabel}${missingLabel}${externalLabel}`);
1972
2015
  console.log(` Branch: ${branchDisplay}`);
1973
2016
  console.log(` Path: ${chalk.gray(wt.path)}`);
1974
2017
  console.log(` HEAD: ${chalk.gray(wt.head.substring(0, 8))}`);
@@ -2105,10 +2148,11 @@ import { Command as Command3 } from "commander";
2105
2148
  import chalk3 from "chalk";
2106
2149
  import ora2 from "ora";
2107
2150
  import path3 from "path";
2151
+ import { existsSync as existsSync4 } from "fs";
2108
2152
  import { confirm as confirm5 } from "@inquirer/prompts";
2109
2153
  function createRemoveCommand(ctx) {
2110
2154
  const { config, log } = ctx;
2111
- return new Command3("remove").description("Remove a worktree").argument("<name>", "Worktree name").option("-f, --force", "Force removal even with uncommitted changes").option("-y, --yes", "Skip confirmation prompt").action(async (name, options) => {
2155
+ return new Command3("remove").description("Remove a worktree").argument("<name>", "Worktree name").option("-f, --force", "Force removal even with uncommitted changes").option("-y, --yes", "Skip confirmation prompt").option("--no-hook", "Skip running the pre-teardown hook").action(async (name, options) => {
2112
2156
  const projectCtx = await resolveProjectFromCwd(config, log);
2113
2157
  if (!projectCtx) {
2114
2158
  log.error("Not in a git repository. Run this command from within a git project.");
@@ -2134,22 +2178,29 @@ function createRemoveCommand(ctx) {
2134
2178
  log.error("Cannot remove the main worktree");
2135
2179
  process.exit(1);
2136
2180
  }
2137
- const hasChanges = await manager.hasUncommittedChanges(name);
2138
- if (hasChanges && !options.force) {
2139
- log.warn(`Worktree '${name}' has uncommitted changes.`);
2140
- if (!options.yes) {
2141
- const shouldForce = await confirm5({
2142
- message: "Do you want to force removal and lose these changes?",
2143
- default: false
2144
- });
2145
- if (!shouldForce) {
2146
- log.info("Removal cancelled.");
2147
- return;
2181
+ const pathMissing = !existsSync4(worktree.path);
2182
+ if (pathMissing) {
2183
+ log.warn(`Worktree directory is missing from disk: ${worktree.path}`);
2184
+ options.force = true;
2185
+ }
2186
+ if (!pathMissing) {
2187
+ const hasChanges = await manager.hasUncommittedChanges(name);
2188
+ if (hasChanges && !options.force) {
2189
+ log.warn(`Worktree '${name}' has uncommitted changes.`);
2190
+ if (!options.yes) {
2191
+ const shouldForce = await confirm5({
2192
+ message: "Do you want to force removal and lose these changes?",
2193
+ default: false
2194
+ });
2195
+ if (!shouldForce) {
2196
+ log.info("Removal cancelled.");
2197
+ return;
2198
+ }
2199
+ options.force = true;
2200
+ } else {
2201
+ log.error("Use --force to remove worktrees with uncommitted changes.");
2202
+ process.exit(1);
2148
2203
  }
2149
- options.force = true;
2150
- } else {
2151
- log.error("Use --force to remove worktrees with uncommitted changes.");
2152
- process.exit(1);
2153
2204
  }
2154
2205
  }
2155
2206
  if (!options.yes) {
@@ -2174,6 +2225,21 @@ ${chalk3.bold("Worktree to remove:")}`);
2174
2225
  log.debug(`Killing tmux session: ${sessionName}`);
2175
2226
  await tmux.killSession(sessionName);
2176
2227
  }
2228
+ if (options.hook !== false && !pathMissing && manager.hasTeardownHook()) {
2229
+ const hookSpinner = ora2("Running pre-teardown hook...").start();
2230
+ try {
2231
+ const { stdout, stderr } = await manager.runPreTeardownHook(worktree.path);
2232
+ hookSpinner.succeed("Pre-teardown hook completed");
2233
+ if (stdout.trim()) {
2234
+ console.log(chalk3.gray(stdout.trim()));
2235
+ }
2236
+ if (stderr.trim()) {
2237
+ console.log(chalk3.yellow(stderr.trim()));
2238
+ }
2239
+ } catch (error) {
2240
+ hookSpinner.warn(`Pre-teardown hook failed: ${error.message}`);
2241
+ }
2242
+ }
2177
2243
  const spinner = ora2(`Removing worktree '${name}'...`).start();
2178
2244
  try {
2179
2245
  await manager.remove(name, options.force);
@@ -2788,6 +2854,7 @@ __export(interactive_exports, {
2788
2854
  runInteractive: () => runInteractive
2789
2855
  });
2790
2856
  import { select as select4, input as input5, checkbox as checkbox2, confirm as confirm8 } from "@inquirer/prompts";
2857
+ import { existsSync as existsSync5 } from "fs";
2791
2858
  import File5 from "phylo";
2792
2859
  import path7 from "path";
2793
2860
  import deepAssign2 from "deep-assign";
@@ -3607,6 +3674,18 @@ async function removeWorktreeManage(projectName, manager, log) {
3607
3674
  });
3608
3675
  if (confirmed) {
3609
3676
  try {
3677
+ if (manager.hasTeardownHook()) {
3678
+ const worktree = await manager.get(worktreeName);
3679
+ if (worktree && existsSync5(worktree.path)) {
3680
+ log.info("Running pre-teardown hook...");
3681
+ try {
3682
+ await manager.runPreTeardownHook(worktree.path);
3683
+ log.info("Pre-teardown hook completed.");
3684
+ } catch (error) {
3685
+ log.warn(`Pre-teardown hook failed: ${error.message}`);
3686
+ }
3687
+ }
3688
+ }
3610
3689
  await manager.remove(worktreeName, true);
3611
3690
  log.info(`Worktree '${worktreeName}' removed.`);
3612
3691
  } catch (error) {
@@ -3656,6 +3735,15 @@ async function mergeWorktreeManage(projectName, manager, log) {
3656
3735
  await manager.merge(worktreeName, { targetBranch, squash });
3657
3736
  log.info(`Merged '${worktree.branch}' into '${targetBranch}'`);
3658
3737
  if (removeAfter) {
3738
+ if (manager.hasTeardownHook() && existsSync5(worktree.path)) {
3739
+ log.info("Running pre-teardown hook...");
3740
+ try {
3741
+ await manager.runPreTeardownHook(worktree.path);
3742
+ log.info("Pre-teardown hook completed.");
3743
+ } catch (error) {
3744
+ log.warn(`Pre-teardown hook failed: ${error.message}`);
3745
+ }
3746
+ }
3659
3747
  await manager.remove(worktreeName, true);
3660
3748
  log.info(`Worktree '${worktreeName}' removed.`);
3661
3749
  }
@@ -4105,7 +4193,7 @@ init_environment();
4105
4193
  init_registry();
4106
4194
  init_open2();
4107
4195
  import { Command as Command16 } from "commander";
4108
- import { readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
4196
+ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
4109
4197
  import { join as join2, dirname as dirname2 } from "path";
4110
4198
  import { fileURLToPath } from "url";
4111
4199
  import loog from "loog";
@@ -4478,7 +4566,7 @@ async function listProjects(ctx) {
4478
4566
 
4479
4567
  // src/commands/add.ts
4480
4568
  import { Command as Command14 } from "commander";
4481
- import { existsSync as existsSync3, readFileSync } from "fs";
4569
+ import { existsSync as existsSync6, readFileSync } from "fs";
4482
4570
  import { basename as basename2, resolve } from "path";
4483
4571
  import File8 from "phylo";
4484
4572
  import { confirm as confirm10 } from "@inquirer/prompts";
@@ -4500,7 +4588,7 @@ async function addProject(pathArg, options, ctx) {
4500
4588
  const projects = config.getProjects();
4501
4589
  const targetPath = resolve(pathArg);
4502
4590
  log.debug(`Resolved path: ${targetPath}`);
4503
- if (!existsSync3(targetPath)) {
4591
+ if (!existsSync6(targetPath)) {
4504
4592
  log.error(`Path does not exist: ${targetPath}`);
4505
4593
  process.exit(1);
4506
4594
  }
@@ -4584,7 +4672,7 @@ function discoverProject(targetPath, log) {
4584
4672
  packageJson: null
4585
4673
  };
4586
4674
  const packageJsonPath = resolve(targetPath, "package.json");
4587
- if (existsSync3(packageJsonPath)) {
4675
+ if (existsSync6(packageJsonPath)) {
4588
4676
  discovery.isNode = true;
4589
4677
  log.debug("Detected Node project (package.json found)");
4590
4678
  try {
@@ -4600,25 +4688,25 @@ function discoverProject(targetPath, log) {
4600
4688
  }
4601
4689
  }
4602
4690
  const bunLockPath = resolve(targetPath, "bun.lockb");
4603
- if (existsSync3(bunLockPath)) {
4691
+ if (existsSync6(bunLockPath)) {
4604
4692
  discovery.isBun = true;
4605
4693
  log.debug("Detected Bun project (bun.lockb found)");
4606
4694
  }
4607
4695
  const vscodeDir = resolve(targetPath, ".vscode");
4608
4696
  const cursorDir = resolve(targetPath, ".cursor");
4609
4697
  const ideaDir = resolve(targetPath, ".idea");
4610
- if (existsSync3(cursorDir)) {
4698
+ if (existsSync6(cursorDir)) {
4611
4699
  discovery.detectedIde = "cursor";
4612
4700
  log.debug("Detected Cursor (.cursor directory found)");
4613
- } else if (existsSync3(vscodeDir)) {
4701
+ } else if (existsSync6(vscodeDir)) {
4614
4702
  discovery.detectedIde = "code";
4615
4703
  log.debug("Detected VS Code (.vscode directory found)");
4616
- } else if (existsSync3(ideaDir)) {
4704
+ } else if (existsSync6(ideaDir)) {
4617
4705
  discovery.detectedIde = "idea";
4618
4706
  log.debug("Detected IntelliJ IDEA (.idea directory found)");
4619
4707
  }
4620
4708
  const claudeMdPath = resolve(targetPath, "CLAUDE.md");
4621
- if (existsSync3(claudeMdPath)) {
4709
+ if (existsSync6(claudeMdPath)) {
4622
4710
  discovery.hasClaude = true;
4623
4711
  log.debug("Detected Claude Code project (CLAUDE.md found)");
4624
4712
  }
@@ -4914,7 +5002,7 @@ function findPackageJson() {
4914
5002
  join2(process.cwd(), "package.json")
4915
5003
  ];
4916
5004
  for (const p of paths) {
4917
- if (existsSync4(p)) {
5005
+ if (existsSync7(p)) {
4918
5006
  return p;
4919
5007
  }
4920
5008
  }