workon 3.2.0 → 3.2.1

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
@@ -236,7 +236,8 @@ var init_environment = __esm({
236
236
  const git = simpleGit(gitDir.path);
237
237
  const branchSummary = await git.branchLocal();
238
238
  base.branch = branchSummary.current;
239
- } catch {
239
+ } catch (error) {
240
+ this.log.debug(`Git branch detection failed: ${error.message}`);
240
241
  }
241
242
  }
242
243
  return this.getProjectEnvironment(base, matching);
@@ -302,7 +303,7 @@ var init_environment = __esm({
302
303
  });
303
304
 
304
305
  // src/events/core/cwd.ts
305
- import { spawn as spawn2 } from "child_process";
306
+ import { spawn } from "child_process";
306
307
  var CwdEvent;
307
308
  var init_cwd = __esm({
308
309
  "src/events/core/cwd.ts"() {
@@ -347,7 +348,7 @@ var init_cwd = __esm({
347
348
  shellCommands.push(`cd "${projectPath}"`);
348
349
  } else {
349
350
  const shell = process.env.SHELL || "/bin/bash";
350
- spawn2(shell, [], {
351
+ spawn(shell, [], {
351
352
  cwd: projectPath,
352
353
  stdio: "inherit"
353
354
  });
@@ -377,7 +378,7 @@ var init_cwd = __esm({
377
378
  });
378
379
 
379
380
  // src/events/core/ide.ts
380
- import { spawn as spawn3 } from "child_process";
381
+ import { spawn as spawn2 } from "child_process";
381
382
  var IdeEvent;
382
383
  var init_ide = __esm({
383
384
  "src/events/core/ide.ts"() {
@@ -422,7 +423,7 @@ var init_ide = __esm({
422
423
  if (isShellMode) {
423
424
  shellCommands.push(`${ide} "${projectPath}" &`);
424
425
  } else {
425
- spawn3(ide, [projectPath], {
426
+ spawn2(ide, [projectPath], {
426
427
  detached: true,
427
428
  stdio: "ignore"
428
429
  }).unref();
@@ -453,7 +454,7 @@ var init_ide = __esm({
453
454
  });
454
455
 
455
456
  // src/events/core/web.ts
456
- import { spawn as spawn4 } from "child_process";
457
+ import { spawn as spawn3 } from "child_process";
457
458
  import { platform } from "os";
458
459
  var WebEvent;
459
460
  var init_web = __esm({
@@ -514,7 +515,7 @@ var init_web = __esm({
514
515
  if (isShellMode) {
515
516
  shellCommands.push(`${openCmd} "${homepage}" &`);
516
517
  } else {
517
- spawn4(openCmd, [homepage], {
518
+ spawn3(openCmd, [homepage], {
518
519
  detached: true,
519
520
  stdio: "ignore"
520
521
  }).unref();
@@ -546,7 +547,7 @@ var init_web = __esm({
546
547
  });
547
548
 
548
549
  // src/events/extensions/claude.ts
549
- import { spawn as spawn5 } from "child_process";
550
+ import { spawn as spawn4 } from "child_process";
550
551
  import { input, confirm } from "@inquirer/prompts";
551
552
  var ClaudeEvent;
552
553
  var init_claude = __esm({
@@ -642,7 +643,7 @@ var init_claude = __esm({
642
643
  shellCommands.push(claudeCommand);
643
644
  } else {
644
645
  const args = claudeCommand.split(" ").slice(1);
645
- spawn5("claude", args, {
646
+ spawn4("claude", args, {
646
647
  cwd: project.path.path,
647
648
  stdio: "inherit"
648
649
  });
@@ -686,7 +687,7 @@ var init_claude = __esm({
686
687
  });
687
688
 
688
689
  // src/events/extensions/docker.ts
689
- import { spawn as spawn6 } from "child_process";
690
+ import { spawn as spawn5 } from "child_process";
690
691
  import { input as input2 } from "@inquirer/prompts";
691
692
  var DockerEvent;
692
693
  var init_docker = __esm({
@@ -784,7 +785,7 @@ var init_docker = __esm({
784
785
  shellCommands.push(dockerCommand);
785
786
  } else {
786
787
  const [cmd, ...args] = dockerCommand.split(" ");
787
- spawn6(cmd, args, {
788
+ spawn5(cmd, args, {
788
789
  cwd: project.path.path,
789
790
  stdio: "inherit"
790
791
  });
@@ -823,7 +824,7 @@ __export(npm_exports, {
823
824
  NpmEvent: () => NpmEvent,
824
825
  default: () => npm_default
825
826
  });
826
- import { spawn as spawn7 } from "child_process";
827
+ import { spawn as spawn6 } from "child_process";
827
828
  import { input as input3, confirm as confirm2 } from "@inquirer/prompts";
828
829
  var NpmEvent, npm_default;
829
830
  var init_npm = __esm({
@@ -924,7 +925,7 @@ var init_npm = __esm({
924
925
  shellCommands.push(npmCommand);
925
926
  } else {
926
927
  const [cmd, ...args] = npmCommand.split(" ");
927
- spawn7(cmd, args, {
928
+ spawn6(cmd, args, {
928
929
  cwd: project.path.path,
929
930
  stdio: "inherit"
930
931
  });
@@ -993,23 +994,19 @@ var init_registry = __esm({
993
994
  */
994
995
  registerEvents() {
995
996
  for (const EventClass of ALL_EVENTS) {
996
- if (this.isValidEvent(EventClass)) {
997
- const metadata = EventClass.metadata;
998
- this._events.set(metadata.name, EventClass);
997
+ if (this.isValidEventClass(EventClass)) {
998
+ this._events.set(EventClass.metadata.name, EventClass);
999
999
  }
1000
1000
  }
1001
1001
  }
1002
1002
  /**
1003
- * Validate if a class is a proper event
1003
+ * Type guard to check if an object is a valid EventHandlerClass
1004
1004
  */
1005
- isValidEvent(EventClass) {
1006
- try {
1007
- if (typeof EventClass !== "function") return false;
1008
- const metadata = EventClass.metadata;
1009
- return metadata !== void 0 && typeof metadata.name === "string" && typeof metadata.displayName === "string" && typeof EventClass.validation === "object" && typeof EventClass.configuration === "object" && typeof EventClass.processing === "object";
1010
- } catch {
1011
- return false;
1012
- }
1005
+ isValidEventClass(obj) {
1006
+ if (typeof obj !== "function" && typeof obj !== "object") return false;
1007
+ if (obj === null) return false;
1008
+ const candidate = obj;
1009
+ return candidate.metadata !== void 0 && typeof candidate.metadata.name === "string" && typeof candidate.metadata.displayName === "string" && candidate.validation !== void 0 && typeof candidate.validation.validateConfig === "function" && candidate.configuration !== void 0 && typeof candidate.configuration.configureInteractive === "function" && candidate.processing !== void 0 && typeof candidate.processing.processEvent === "function";
1013
1010
  }
1014
1011
  /**
1015
1012
  * Get all valid event names from registered events
@@ -1031,12 +1028,11 @@ var init_registry = __esm({
1031
1028
  getEventsForManageUI() {
1032
1029
  this.ensureInitialized();
1033
1030
  const events = [];
1034
- for (const [name, EventClass] of this._events) {
1035
- const metadata = EventClass.metadata;
1031
+ for (const [name, eventClass] of this._events) {
1036
1032
  events.push({
1037
- name: metadata.displayName,
1033
+ name: eventClass.metadata.displayName,
1038
1034
  value: name,
1039
- description: metadata.description
1035
+ description: eventClass.metadata.description
1040
1036
  });
1041
1037
  }
1042
1038
  return events.sort((a, b) => a.name.localeCompare(b.name));
@@ -1047,13 +1043,13 @@ var init_registry = __esm({
1047
1043
  getTmuxEnabledEvents() {
1048
1044
  this.ensureInitialized();
1049
1045
  const tmuxEvents = [];
1050
- for (const [name, EventClass] of this._events) {
1051
- const tmux = EventClass.tmux;
1046
+ for (const [name, eventClass] of this._events) {
1047
+ const tmux = eventClass.tmux;
1052
1048
  if (tmux) {
1053
1049
  tmuxEvents.push({
1054
1050
  name,
1055
- event: EventClass,
1056
- priority: tmux.getLayoutPriority ? tmux.getLayoutPriority() : 0
1051
+ event: eventClass,
1052
+ priority: tmux.getLayoutPriority()
1057
1053
  });
1058
1054
  }
1059
1055
  }
@@ -1065,16 +1061,15 @@ var init_registry = __esm({
1065
1061
  getAllEvents() {
1066
1062
  this.ensureInitialized();
1067
1063
  const events = [];
1068
- for (const [name, EventClass] of this._events) {
1069
- const typedClass = EventClass;
1064
+ for (const [name, eventClass] of this._events) {
1070
1065
  events.push({
1071
1066
  name,
1072
- metadata: typedClass.metadata,
1073
- hasValidation: !!typedClass.validation,
1074
- hasConfiguration: !!typedClass.configuration,
1075
- hasProcessing: !!typedClass.processing,
1076
- hasTmux: !!typedClass.tmux,
1077
- hasHelp: !!typedClass.help
1067
+ metadata: eventClass.metadata,
1068
+ hasValidation: !!eventClass.validation,
1069
+ hasConfiguration: !!eventClass.configuration,
1070
+ hasProcessing: !!eventClass.processing,
1071
+ hasTmux: !!eventClass.tmux,
1072
+ hasHelp: !!eventClass.help
1078
1073
  });
1079
1074
  }
1080
1075
  return events;
@@ -1099,6 +1094,23 @@ var init_registry = __esm({
1099
1094
  }
1100
1095
  });
1101
1096
 
1097
+ // src/types/constants.ts
1098
+ var IDE_CHOICES;
1099
+ var init_constants = __esm({
1100
+ "src/types/constants.ts"() {
1101
+ "use strict";
1102
+ IDE_CHOICES = [
1103
+ { name: "Visual Studio Code", value: "vscode" },
1104
+ { name: "Visual Studio Code (code)", value: "code" },
1105
+ { name: "IntelliJ IDEA", value: "idea" },
1106
+ { name: "Atom", value: "atom" },
1107
+ { name: "Sublime Text", value: "subl" },
1108
+ { name: "Vim", value: "vim" },
1109
+ { name: "Emacs", value: "emacs" }
1110
+ ];
1111
+ }
1112
+ });
1113
+
1102
1114
  // src/commands/interactive.ts
1103
1115
  var interactive_exports = {};
1104
1116
  __export(interactive_exports, {
@@ -1270,8 +1282,10 @@ async function initBranch(defaultName, ctx) {
1270
1282
  });
1271
1283
  const branchName = `${defaultName}#${branch}`;
1272
1284
  const baseProject = projects[defaultName];
1273
- const branchConfig = deepAssign2({}, baseProject, { branch });
1274
- delete branchConfig.name;
1285
+ const { name: _excludedName, ...mergedConfig } = deepAssign2({}, baseProject, {
1286
+ branch
1287
+ });
1288
+ const branchConfig = mergedConfig;
1275
1289
  projects[branchName] = branchConfig;
1276
1290
  config.set("projects", projects);
1277
1291
  log.info("Your branch configuration has been initialized.");
@@ -1315,7 +1329,6 @@ async function switchBranch(projectName, ctx) {
1315
1329
  }
1316
1330
  async function manageProjects(ctx) {
1317
1331
  const { config } = ctx;
1318
- await EventRegistry.initialize();
1319
1332
  const projects = config.getProjects();
1320
1333
  const hasProjects = Object.keys(projects).length > 0;
1321
1334
  const choices = [
@@ -1392,7 +1405,6 @@ async function openProject(projectName, ctx) {
1392
1405
  log.error(`Project '${projectName}' not found.`);
1393
1406
  return;
1394
1407
  }
1395
- await EventRegistry.initialize();
1396
1408
  const projectConfig = projects[projectName];
1397
1409
  const projectCfg = { ...projectConfig, name: projectName };
1398
1410
  const projectEnv = ProjectEnvironment.load(projectCfg, config.getDefaults());
@@ -1402,7 +1414,7 @@ async function openProject(projectName, ctx) {
1402
1414
  );
1403
1415
  for (const event of events) {
1404
1416
  const eventHandler = EventRegistry.getEventByName(event);
1405
- if (eventHandler && eventHandler.processing) {
1417
+ if (eventHandler) {
1406
1418
  await eventHandler.processing.processEvent({
1407
1419
  project: projectEnv.project,
1408
1420
  isShellMode: false,
@@ -1711,23 +1723,19 @@ Branch configurations for '${projectName}':
1711
1723
  console.log();
1712
1724
  }
1713
1725
  }
1714
- var IDE_CHOICES;
1715
1726
  var init_interactive = __esm({
1716
1727
  "src/commands/interactive.ts"() {
1717
1728
  "use strict";
1718
1729
  init_environment();
1719
1730
  init_registry();
1720
- IDE_CHOICES = [
1721
- { name: "Visual Studio Code", value: "vscode" },
1722
- { name: "IntelliJ IDEA", value: "idea" },
1723
- { name: "Atom", value: "atom" }
1724
- ];
1731
+ init_constants();
1725
1732
  }
1726
1733
  });
1727
1734
 
1728
1735
  // src/commands/index.ts
1729
1736
  init_config();
1730
1737
  init_environment();
1738
+ init_registry();
1731
1739
  import { Command as Command8 } from "commander";
1732
1740
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
1733
1741
  import { join, dirname } from "path";
@@ -1742,8 +1750,20 @@ import { Command } from "commander";
1742
1750
  import File4 from "phylo";
1743
1751
 
1744
1752
  // src/lib/tmux.ts
1745
- import { exec as execCallback, spawn } from "child_process";
1753
+ import { exec as execCallback, spawn as spawn7 } from "child_process";
1746
1754
  import { promisify } from "util";
1755
+
1756
+ // src/lib/sanitize.ts
1757
+ function sanitizeForShell(input6) {
1758
+ if (!input6) return "";
1759
+ return input6.replace(/[^a-zA-Z0-9_\-.]/g, "_");
1760
+ }
1761
+ function escapeForSingleQuotes(input6) {
1762
+ if (!input6) return "";
1763
+ return input6.replace(/'/g, "'\\''");
1764
+ }
1765
+
1766
+ // src/lib/tmux.ts
1747
1767
  var exec = promisify(execCallback);
1748
1768
  var TmuxManager = class {
1749
1769
  sessionPrefix = "workon-";
@@ -1757,18 +1777,18 @@ var TmuxManager = class {
1757
1777
  }
1758
1778
  async sessionExists(sessionName) {
1759
1779
  try {
1760
- await exec(`tmux has-session -t "${sessionName}"`);
1780
+ await exec(`tmux has-session -t '${escapeForSingleQuotes(sessionName)}'`);
1761
1781
  return true;
1762
1782
  } catch {
1763
1783
  return false;
1764
1784
  }
1765
1785
  }
1766
1786
  getSessionName(projectName) {
1767
- return `${this.sessionPrefix}${projectName}`;
1787
+ return `${this.sessionPrefix}${sanitizeForShell(projectName)}`;
1768
1788
  }
1769
1789
  async killSession(sessionName) {
1770
1790
  try {
1771
- await exec(`tmux kill-session -t "${sessionName}"`);
1791
+ await exec(`tmux kill-session -t '${escapeForSingleQuotes(sessionName)}'`);
1772
1792
  return true;
1773
1793
  } catch {
1774
1794
  return false;
@@ -1776,53 +1796,72 @@ var TmuxManager = class {
1776
1796
  }
1777
1797
  async createSplitSession(projectName, projectPath, claudeArgs = []) {
1778
1798
  const sessionName = this.getSessionName(projectName);
1799
+ const escapedSession = escapeForSingleQuotes(sessionName);
1800
+ const escapedPath = escapeForSingleQuotes(projectPath);
1779
1801
  if (await this.sessionExists(sessionName)) {
1780
1802
  await this.killSession(sessionName);
1781
1803
  }
1782
1804
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1783
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
1784
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
1785
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
1805
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1806
+ await exec(
1807
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
1808
+ );
1809
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
1810
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
1786
1811
  return sessionName;
1787
1812
  }
1788
1813
  async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
1789
1814
  const sessionName = this.getSessionName(projectName);
1815
+ const escapedSession = escapeForSingleQuotes(sessionName);
1816
+ const escapedPath = escapeForSingleQuotes(projectPath);
1790
1817
  if (await this.sessionExists(sessionName)) {
1791
1818
  await this.killSession(sessionName);
1792
1819
  }
1793
1820
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1794
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
1795
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
1796
- await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
1797
- await exec(`tmux set-option -t "${sessionName}:0.2" remain-on-exit on`);
1798
- await exec(`tmux resize-pane -t "${sessionName}:0.2" -y 10`);
1799
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
1821
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1822
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1823
+ await exec(
1824
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
1825
+ );
1826
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
1827
+ await exec(
1828
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`
1829
+ );
1830
+ await exec(`tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`);
1831
+ await exec(`tmux resize-pane -t '${escapedSession}:0.2' -y 10`);
1832
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
1800
1833
  return sessionName;
1801
1834
  }
1802
1835
  async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
1803
1836
  const sessionName = this.getSessionName(projectName);
1837
+ const escapedSession = escapeForSingleQuotes(sessionName);
1838
+ const escapedPath = escapeForSingleQuotes(projectPath);
1839
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1804
1840
  if (await this.sessionExists(sessionName)) {
1805
1841
  await this.killSession(sessionName);
1806
1842
  }
1807
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
1808
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
1809
- await exec(`tmux set-option -t "${sessionName}:0.1" remain-on-exit on`);
1810
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
1843
+ await exec(`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`);
1844
+ await exec(
1845
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`
1846
+ );
1847
+ await exec(`tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`);
1848
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
1811
1849
  return sessionName;
1812
1850
  }
1813
1851
  async attachToSession(sessionName) {
1852
+ const escapedSession = escapeForSingleQuotes(sessionName);
1814
1853
  if (process.env.TMUX) {
1815
- await exec(`tmux switch-client -t "${sessionName}"`);
1854
+ await exec(`tmux switch-client -t '${escapedSession}'`);
1816
1855
  } else {
1817
1856
  const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
1818
1857
  const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
1819
1858
  if (useiTermIntegration) {
1820
- spawn("tmux", ["-CC", "attach-session", "-t", sessionName], {
1859
+ spawn7("tmux", ["-CC", "attach-session", "-t", sessionName], {
1821
1860
  stdio: "inherit",
1822
1861
  detached: true
1823
1862
  });
1824
1863
  } else {
1825
- spawn("tmux", ["attach-session", "-t", sessionName], {
1864
+ spawn7("tmux", ["attach-session", "-t", sessionName], {
1826
1865
  stdio: "inherit",
1827
1866
  detached: true
1828
1867
  });
@@ -1831,53 +1870,64 @@ var TmuxManager = class {
1831
1870
  }
1832
1871
  buildShellCommands(projectName, projectPath, claudeArgs = []) {
1833
1872
  const sessionName = this.getSessionName(projectName);
1873
+ const escapedSession = escapeForSingleQuotes(sessionName);
1874
+ const escapedPath = escapeForSingleQuotes(projectPath);
1834
1875
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1876
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1835
1877
  return [
1836
- `# Create tmux split session for ${projectName}`,
1837
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
1838
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
1839
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
1840
- `tmux select-pane -t "${sessionName}:0.0"`,
1878
+ `# Create tmux split session for ${sanitizeForShell(projectName)}`,
1879
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
1880
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
1881
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
1882
+ `tmux select-pane -t '${escapedSession}:0.0'`,
1841
1883
  this.getAttachCommand(sessionName)
1842
1884
  ];
1843
1885
  }
1844
1886
  buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
1845
1887
  const sessionName = this.getSessionName(projectName);
1888
+ const escapedSession = escapeForSingleQuotes(sessionName);
1889
+ const escapedPath = escapeForSingleQuotes(projectPath);
1846
1890
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1891
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1892
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1847
1893
  return [
1848
- `# Create tmux three-pane session for ${projectName}`,
1849
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
1850
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
1851
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
1852
- `tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`,
1853
- `tmux set-option -t "${sessionName}:0.2" remain-on-exit on`,
1854
- `tmux resize-pane -t "${sessionName}:0.2" -y 10`,
1855
- `tmux select-pane -t "${sessionName}:0.0"`,
1894
+ `# Create tmux three-pane session for ${sanitizeForShell(projectName)}`,
1895
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
1896
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
1897
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
1898
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`,
1899
+ `tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`,
1900
+ `tmux resize-pane -t '${escapedSession}:0.2' -y 10`,
1901
+ `tmux select-pane -t '${escapedSession}:0.0'`,
1856
1902
  this.getAttachCommand(sessionName)
1857
1903
  ];
1858
1904
  }
1859
1905
  buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = "npm run dev") {
1860
1906
  const sessionName = this.getSessionName(projectName);
1907
+ const escapedSession = escapeForSingleQuotes(sessionName);
1908
+ const escapedPath = escapeForSingleQuotes(projectPath);
1909
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1861
1910
  return [
1862
- `# Create tmux two-pane session with npm for ${projectName}`,
1863
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
1864
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}"`,
1865
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`,
1866
- `tmux set-option -t "${sessionName}:0.1" remain-on-exit on`,
1867
- `tmux select-pane -t "${sessionName}:0.0"`,
1911
+ `# Create tmux two-pane session with npm for ${sanitizeForShell(projectName)}`,
1912
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
1913
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`,
1914
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`,
1915
+ `tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`,
1916
+ `tmux select-pane -t '${escapedSession}:0.0'`,
1868
1917
  this.getAttachCommand(sessionName)
1869
1918
  ];
1870
1919
  }
1871
1920
  getAttachCommand(sessionName) {
1921
+ const escapedSession = escapeForSingleQuotes(sessionName);
1872
1922
  if (process.env.TMUX) {
1873
- return `tmux switch-client -t "${sessionName}"`;
1923
+ return `tmux switch-client -t '${escapedSession}'`;
1874
1924
  }
1875
1925
  const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || process.env.ITERM_SESSION_ID;
1876
1926
  const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
1877
1927
  if (useiTermIntegration) {
1878
- return `tmux -CC attach-session -t "${sessionName}"`;
1928
+ return `tmux -CC attach-session -t '${escapedSession}'`;
1879
1929
  }
1880
- return `tmux attach-session -t "${sessionName}"`;
1930
+ return `tmux attach-session -t '${escapedSession}'`;
1881
1931
  }
1882
1932
  async listWorkonSessions() {
1883
1933
  try {
@@ -1897,7 +1947,6 @@ function createOpenCommand(ctx) {
1897
1947
  if (options.debug) {
1898
1948
  log.setLogLevel("debug");
1899
1949
  }
1900
- await EventRegistry.initialize();
1901
1950
  if (projectArg) {
1902
1951
  await processProject(projectArg, options, ctx);
1903
1952
  } else {
@@ -2004,32 +2053,34 @@ function resolveCommandDependencies(requestedCommands, project) {
2004
2053
  }
2005
2054
  return [...new Set(resolved)];
2006
2055
  }
2007
- async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx) {
2056
+ function getClaudeArgs(project) {
2057
+ const claudeConfig = project.events.claude;
2058
+ return typeof claudeConfig === "object" && claudeConfig.flags ? claudeConfig.flags : [];
2059
+ }
2060
+ async function getNpmCommand(project) {
2061
+ const npmConfig = project.events.npm;
2062
+ const { NpmEvent: NpmEvent2 } = await Promise.resolve().then(() => (init_npm(), npm_exports));
2063
+ return NpmEvent2.getNpmCommand(npmConfig);
2064
+ }
2065
+ async function handleTmuxLayout(project, layout, options, shellCommands, events, ctx) {
2008
2066
  const { log } = ctx;
2009
2067
  const tmux = new TmuxManager();
2010
- const claudeConfig = project.events.claude;
2011
- const claudeArgs = typeof claudeConfig === "object" && claudeConfig.flags ? claudeConfig.flags : [];
2068
+ const { isShellMode, dryRun } = options;
2012
2069
  let tmuxHandled = false;
2013
2070
  if (isShellMode) {
2014
2071
  if (await tmux.isTmuxAvailable()) {
2015
- const commands = tmux.buildShellCommands(project.name, project.path.path, claudeArgs);
2072
+ const commands = buildLayoutShellCommands(tmux, project, layout);
2016
2073
  shellCommands.push(...commands);
2017
2074
  tmuxHandled = true;
2018
2075
  } else {
2019
2076
  log.debug("Tmux not available, falling back to normal mode");
2020
- shellCommands.push(`cd "${project.path.path}"`);
2021
- const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
2022
- shellCommands.push(claudeCommand);
2077
+ buildFallbackCommands(shellCommands, project, layout);
2023
2078
  tmuxHandled = true;
2024
2079
  }
2025
2080
  } else if (!dryRun) {
2026
2081
  if (await tmux.isTmuxAvailable()) {
2027
2082
  try {
2028
- const sessionName = await tmux.createSplitSession(
2029
- project.name,
2030
- project.path.path,
2031
- claudeArgs
2032
- );
2083
+ const sessionName = await createTmuxSession(tmux, project, layout);
2033
2084
  await tmux.attachToSession(sessionName);
2034
2085
  tmuxHandled = true;
2035
2086
  } catch (error) {
@@ -2039,139 +2090,103 @@ async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands,
2039
2090
  log.debug("Tmux not available, falling back to normal event processing");
2040
2091
  }
2041
2092
  } else {
2042
- log.info(`Would create split tmux session '${project.name}' with Claude`);
2093
+ log.info(layout.dryRunMessage);
2043
2094
  tmuxHandled = true;
2044
2095
  }
2045
2096
  if (!tmuxHandled && !dryRun) {
2046
- for (const event of events.filter((e) => ["cwd", "claude"].includes(e))) {
2097
+ for (const event of events.filter((e) => layout.handledEvents.includes(e))) {
2047
2098
  await processEvent(event, { project, isShellMode, shellCommands }, ctx);
2048
2099
  }
2049
2100
  }
2050
2101
  if (!dryRun) {
2051
- for (const event of events.filter((e) => !["cwd", "claude"].includes(e))) {
2102
+ for (const event of events.filter((e) => !layout.handledEvents.includes(e))) {
2052
2103
  await processEvent(event, { project, isShellMode, shellCommands }, ctx);
2053
2104
  }
2054
2105
  }
2055
2106
  }
2056
- async function handleThreePaneLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
2057
- const { log } = ctx;
2058
- const tmux = new TmuxManager();
2059
- const claudeConfig = project.events.claude;
2060
- const claudeArgs = typeof claudeConfig === "object" && claudeConfig.flags ? claudeConfig.flags : [];
2061
- const npmConfig = project.events.npm;
2062
- const { NpmEvent: NpmEvent2 } = await Promise.resolve().then(() => (init_npm(), npm_exports));
2063
- const npmCommand = NpmEvent2.getNpmCommand(npmConfig);
2064
- let tmuxHandled = false;
2065
- if (isShellMode) {
2066
- if (await tmux.isTmuxAvailable()) {
2067
- const commands = tmux.buildThreePaneShellCommands(
2107
+ function buildLayoutShellCommands(tmux, project, layout) {
2108
+ switch (layout.type) {
2109
+ case "split-claude":
2110
+ return tmux.buildShellCommands(project.name, project.path.path, layout.claudeArgs);
2111
+ case "three-pane":
2112
+ return tmux.buildThreePaneShellCommands(
2068
2113
  project.name,
2069
2114
  project.path.path,
2070
- claudeArgs,
2071
- npmCommand
2115
+ layout.claudeArgs,
2116
+ layout.npmCommand
2072
2117
  );
2073
- shellCommands.push(...commands);
2074
- tmuxHandled = true;
2075
- } else {
2076
- log.debug("Tmux not available, falling back to normal mode");
2077
- shellCommands.push(`cd "${project.path.path}"`);
2078
- shellCommands.push(claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude");
2079
- shellCommands.push(npmCommand);
2080
- tmuxHandled = true;
2081
- }
2082
- } else if (!dryRun) {
2083
- if (await tmux.isTmuxAvailable()) {
2084
- try {
2085
- const sessionName = await tmux.createThreePaneSession(
2086
- project.name,
2087
- project.path.path,
2088
- claudeArgs,
2089
- npmCommand
2090
- );
2091
- await tmux.attachToSession(sessionName);
2092
- tmuxHandled = true;
2093
- } catch (error) {
2094
- log.debug(`Failed to create tmux session: ${error.message}`);
2095
- }
2096
- } else {
2097
- log.debug("Tmux not available, falling back to normal event processing");
2098
- }
2099
- } else {
2100
- log.info(`Would create three-pane tmux session '${project.name}' with Claude and NPM`);
2101
- tmuxHandled = true;
2118
+ case "two-pane-npm":
2119
+ return tmux.buildTwoPaneNpmShellCommands(project.name, project.path.path, layout.npmCommand);
2102
2120
  }
2103
- if (!tmuxHandled && !dryRun) {
2104
- for (const event of events.filter((e) => ["cwd", "claude", "npm"].includes(e))) {
2105
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
2106
- }
2121
+ }
2122
+ function buildFallbackCommands(shellCommands, project, layout) {
2123
+ shellCommands.push(`cd "${project.path.path}"`);
2124
+ if (layout.type === "split-claude" || layout.type === "three-pane") {
2125
+ const claudeCommand = layout.claudeArgs.length > 0 ? `claude ${layout.claudeArgs.join(" ")}` : "claude";
2126
+ shellCommands.push(claudeCommand);
2107
2127
  }
2108
- if (!dryRun) {
2109
- for (const event of events.filter((e) => !["cwd", "claude", "npm"].includes(e))) {
2110
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
2111
- }
2128
+ if (layout.npmCommand) {
2129
+ shellCommands.push(layout.npmCommand);
2112
2130
  }
2113
2131
  }
2114
- async function handleTwoPaneNpmLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
2115
- const { log } = ctx;
2116
- const tmux = new TmuxManager();
2117
- const npmConfig = project.events.npm;
2118
- const { NpmEvent: NpmEvent2 } = await Promise.resolve().then(() => (init_npm(), npm_exports));
2119
- const npmCommand = NpmEvent2.getNpmCommand(npmConfig);
2120
- let tmuxHandled = false;
2121
- if (isShellMode) {
2122
- if (await tmux.isTmuxAvailable()) {
2123
- const commands = tmux.buildTwoPaneNpmShellCommands(
2132
+ async function createTmuxSession(tmux, project, layout) {
2133
+ switch (layout.type) {
2134
+ case "split-claude":
2135
+ return tmux.createSplitSession(project.name, project.path.path, layout.claudeArgs);
2136
+ case "three-pane":
2137
+ return tmux.createThreePaneSession(
2124
2138
  project.name,
2125
2139
  project.path.path,
2126
- npmCommand
2140
+ layout.claudeArgs,
2141
+ layout.npmCommand
2127
2142
  );
2128
- shellCommands.push(...commands);
2129
- tmuxHandled = true;
2130
- } else {
2131
- log.debug("Tmux not available, falling back to normal mode");
2132
- shellCommands.push(`cd "${project.path.path}"`);
2133
- shellCommands.push(npmCommand);
2134
- tmuxHandled = true;
2135
- }
2136
- } else if (!dryRun) {
2137
- if (await tmux.isTmuxAvailable()) {
2138
- try {
2139
- const sessionName = await tmux.createTwoPaneNpmSession(
2140
- project.name,
2141
- project.path.path,
2142
- npmCommand
2143
- );
2144
- await tmux.attachToSession(sessionName);
2145
- tmuxHandled = true;
2146
- } catch (error) {
2147
- log.debug(`Failed to create tmux session: ${error.message}`);
2148
- }
2149
- } else {
2150
- log.debug("Tmux not available, falling back to normal event processing");
2151
- }
2152
- } else {
2153
- log.info(`Would create two-pane tmux session '${project.name}' with NPM`);
2154
- tmuxHandled = true;
2155
- }
2156
- if (!tmuxHandled && !dryRun) {
2157
- for (const event of events.filter((e) => ["cwd", "npm"].includes(e))) {
2158
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
2159
- }
2160
- }
2161
- if (!dryRun) {
2162
- for (const event of events.filter((e) => !["cwd", "npm"].includes(e))) {
2163
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
2164
- }
2143
+ case "two-pane-npm":
2144
+ return tmux.createTwoPaneNpmSession(project.name, project.path.path, layout.npmCommand);
2165
2145
  }
2166
2146
  }
2147
+ async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx) {
2148
+ const layout = {
2149
+ type: "split-claude",
2150
+ handledEvents: ["cwd", "claude"],
2151
+ dryRunMessage: `Would create split tmux session '${project.name}' with Claude`,
2152
+ claudeArgs: getClaudeArgs(project),
2153
+ npmCommand: null
2154
+ };
2155
+ await handleTmuxLayout(project, layout, { isShellMode, dryRun }, shellCommands, events, ctx);
2156
+ }
2157
+ async function handleThreePaneLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
2158
+ const layout = {
2159
+ type: "three-pane",
2160
+ handledEvents: ["cwd", "claude", "npm"],
2161
+ dryRunMessage: `Would create three-pane tmux session '${project.name}' with Claude and NPM`,
2162
+ claudeArgs: getClaudeArgs(project),
2163
+ npmCommand: await getNpmCommand(project)
2164
+ };
2165
+ await handleTmuxLayout(project, layout, { isShellMode, dryRun }, shellCommands, events, ctx);
2166
+ }
2167
+ async function handleTwoPaneNpmLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
2168
+ const layout = {
2169
+ type: "two-pane-npm",
2170
+ handledEvents: ["cwd", "npm"],
2171
+ dryRunMessage: `Would create two-pane tmux session '${project.name}' with NPM`,
2172
+ claudeArgs: [],
2173
+ npmCommand: await getNpmCommand(project)
2174
+ };
2175
+ await handleTmuxLayout(project, layout, { isShellMode, dryRun }, shellCommands, events, ctx);
2176
+ }
2167
2177
  async function processEvent(event, context, ctx) {
2168
2178
  const { log } = ctx;
2169
2179
  log.debug(`Processing event ${event}`);
2170
2180
  const eventHandler = EventRegistry.getEventByName(event);
2171
- if (eventHandler && eventHandler.processing) {
2172
- await eventHandler.processing.processEvent(context);
2173
- } else {
2181
+ if (!eventHandler) {
2174
2182
  log.debug(`No event handler found for: ${event}`);
2183
+ return;
2184
+ }
2185
+ try {
2186
+ await eventHandler.processing.processEvent(context);
2187
+ } catch (error) {
2188
+ log.error(`Failed to process event '${event}': ${error.message}`);
2189
+ log.debug(`Event error stack: ${error.stack}`);
2175
2190
  }
2176
2191
  }
2177
2192
  async function showProjectHelp(projectName, ctx) {
@@ -2189,17 +2204,16 @@ Available commands for '${projectName}':`);
2189
2204
  for (const eventName of configuredEvents) {
2190
2205
  const eventHandler = EventRegistry.getEventByName(eventName);
2191
2206
  if (eventHandler) {
2192
- const metadata = eventHandler.metadata;
2193
- const config2 = projectConfig.events[eventName];
2207
+ const eventConfig = projectConfig.events[eventName];
2194
2208
  let configDesc = "";
2195
- if (config2 !== true && config2 !== "true") {
2196
- if (typeof config2 === "object") {
2197
- configDesc = ` (${JSON.stringify(config2)})`;
2209
+ if (eventConfig !== true && eventConfig !== "true") {
2210
+ if (typeof eventConfig === "object") {
2211
+ configDesc = ` (${JSON.stringify(eventConfig)})`;
2198
2212
  } else {
2199
- configDesc = ` (${config2})`;
2213
+ configDesc = ` (${eventConfig})`;
2200
2214
  }
2201
2215
  }
2202
- console.log(` ${eventName.padEnd(8)} - ${metadata.description}${configDesc}`);
2216
+ console.log(` ${eventName.padEnd(8)} - ${eventHandler.metadata.description}${configDesc}`);
2203
2217
  }
2204
2218
  }
2205
2219
  console.log("\nUsage examples:");
@@ -2306,25 +2320,16 @@ function createConfigCommand(ctx) {
2306
2320
 
2307
2321
  // src/commands/manage.ts
2308
2322
  init_registry();
2323
+ init_constants();
2309
2324
  import { Command as Command6 } from "commander";
2310
2325
  import { select as select2, input as input5, confirm as confirm4, checkbox as checkbox2 } from "@inquirer/prompts";
2311
2326
  import File5 from "phylo";
2312
- var IDE_CHOICES2 = [
2313
- { name: "Visual Studio Code", value: "vscode" },
2314
- { name: "Visual Studio Code (code)", value: "code" },
2315
- { name: "IntelliJ IDEA", value: "idea" },
2316
- { name: "Atom", value: "atom" },
2317
- { name: "Sublime Text", value: "subl" },
2318
- { name: "Vim", value: "vim" },
2319
- { name: "Emacs", value: "emacs" }
2320
- ];
2321
2327
  function createManageCommand(ctx) {
2322
2328
  const { log } = ctx;
2323
2329
  return new Command6("manage").description("Interactive project management").option("-d, --debug", "Enable debug logging").action(async (options) => {
2324
2330
  if (options.debug) {
2325
2331
  log.setLogLevel("debug");
2326
2332
  }
2327
- await EventRegistry.initialize();
2328
2333
  await mainMenu(ctx);
2329
2334
  });
2330
2335
  }
@@ -2406,7 +2411,7 @@ async function createProject(ctx) {
2406
2411
  }
2407
2412
  const ide = await select2({
2408
2413
  message: "Select IDE:",
2409
- choices: IDE_CHOICES2
2414
+ choices: IDE_CHOICES
2410
2415
  });
2411
2416
  const homepage = await input5({
2412
2417
  message: "Project homepage URL (optional):",
@@ -2483,7 +2488,7 @@ async function editProject(ctx) {
2483
2488
  }
2484
2489
  const ide = await select2({
2485
2490
  message: "Select IDE:",
2486
- choices: IDE_CHOICES2,
2491
+ choices: IDE_CHOICES,
2487
2492
  default: project.ide || "vscode"
2488
2493
  });
2489
2494
  const homepage = await input5({
@@ -2745,11 +2750,12 @@ function createCli() {
2745
2750
  config.set("pkg", packageJson);
2746
2751
  EnvironmentRecognizer.configure(config, log);
2747
2752
  const completion = setupCompletion(config);
2748
- program2.name("workon").description("Work on something great!").version(packageJson.version).argument("[project]", "Project name to open (supports project:command syntax)").option("-d, --debug", "Enable debug logging").option("--completion", "Setup shell tab completion").option("--shell", "Output shell commands for evaluation").option("--init", "Generate shell integration function").hook("preAction", (thisCommand) => {
2753
+ program2.name("workon").description("Work on something great!").version(packageJson.version).argument("[project]", "Project name to open (supports project:command syntax)").option("-d, --debug", "Enable debug logging").option("--completion", "Setup shell tab completion").option("--shell", "Output shell commands for evaluation").option("--init", "Generate shell integration function").hook("preAction", async (thisCommand) => {
2749
2754
  const opts = thisCommand.opts();
2750
2755
  if (opts.debug) {
2751
2756
  log.setLogLevel("debug");
2752
2757
  }
2758
+ await EventRegistry.initialize();
2753
2759
  }).action(
2754
2760
  async (project, options) => {
2755
2761
  if (options.debug) {