workon 3.2.0 → 3.2.2

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/index.cjs CHANGED
@@ -204,6 +204,18 @@ var Project = class {
204
204
  // src/lib/tmux.ts
205
205
  var import_child_process = require("child_process");
206
206
  var import_util = require("util");
207
+
208
+ // src/lib/sanitize.ts
209
+ function sanitizeForShell(input4) {
210
+ if (!input4) return "";
211
+ return input4.replace(/[^a-zA-Z0-9_\-.]/g, "_");
212
+ }
213
+ function escapeForSingleQuotes(input4) {
214
+ if (!input4) return "";
215
+ return input4.replace(/'/g, "'\\''");
216
+ }
217
+
218
+ // src/lib/tmux.ts
207
219
  var exec = (0, import_util.promisify)(import_child_process.exec);
208
220
  var TmuxManager = class {
209
221
  sessionPrefix = "workon-";
@@ -217,18 +229,18 @@ var TmuxManager = class {
217
229
  }
218
230
  async sessionExists(sessionName) {
219
231
  try {
220
- await exec(`tmux has-session -t "${sessionName}"`);
232
+ await exec(`tmux has-session -t '${escapeForSingleQuotes(sessionName)}'`);
221
233
  return true;
222
234
  } catch {
223
235
  return false;
224
236
  }
225
237
  }
226
238
  getSessionName(projectName) {
227
- return `${this.sessionPrefix}${projectName}`;
239
+ return `${this.sessionPrefix}${sanitizeForShell(projectName)}`;
228
240
  }
229
241
  async killSession(sessionName) {
230
242
  try {
231
- await exec(`tmux kill-session -t "${sessionName}"`);
243
+ await exec(`tmux kill-session -t '${escapeForSingleQuotes(sessionName)}'`);
232
244
  return true;
233
245
  } catch {
234
246
  return false;
@@ -236,43 +248,62 @@ var TmuxManager = class {
236
248
  }
237
249
  async createSplitSession(projectName, projectPath, claudeArgs = []) {
238
250
  const sessionName = this.getSessionName(projectName);
251
+ const escapedSession = escapeForSingleQuotes(sessionName);
252
+ const escapedPath = escapeForSingleQuotes(projectPath);
239
253
  if (await this.sessionExists(sessionName)) {
240
254
  await this.killSession(sessionName);
241
255
  }
242
256
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
243
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
244
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
245
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
257
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
258
+ await exec(
259
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
260
+ );
261
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
262
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
246
263
  return sessionName;
247
264
  }
248
265
  async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
249
266
  const sessionName = this.getSessionName(projectName);
267
+ const escapedSession = escapeForSingleQuotes(sessionName);
268
+ const escapedPath = escapeForSingleQuotes(projectPath);
250
269
  if (await this.sessionExists(sessionName)) {
251
270
  await this.killSession(sessionName);
252
271
  }
253
272
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
254
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
255
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
256
- await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
257
- await exec(`tmux set-option -t "${sessionName}:0.2" remain-on-exit on`);
258
- await exec(`tmux resize-pane -t "${sessionName}:0.2" -y 10`);
259
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
273
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
274
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
275
+ await exec(
276
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
277
+ );
278
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
279
+ await exec(
280
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`
281
+ );
282
+ await exec(`tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`);
283
+ await exec(`tmux resize-pane -t '${escapedSession}:0.2' -y 10`);
284
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
260
285
  return sessionName;
261
286
  }
262
287
  async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
263
288
  const sessionName = this.getSessionName(projectName);
289
+ const escapedSession = escapeForSingleQuotes(sessionName);
290
+ const escapedPath = escapeForSingleQuotes(projectPath);
291
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
264
292
  if (await this.sessionExists(sessionName)) {
265
293
  await this.killSession(sessionName);
266
294
  }
267
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
268
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
269
- await exec(`tmux set-option -t "${sessionName}:0.1" remain-on-exit on`);
270
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
295
+ await exec(`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`);
296
+ await exec(
297
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`
298
+ );
299
+ await exec(`tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`);
300
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
271
301
  return sessionName;
272
302
  }
273
303
  async attachToSession(sessionName) {
304
+ const escapedSession = escapeForSingleQuotes(sessionName);
274
305
  if (process.env.TMUX) {
275
- await exec(`tmux switch-client -t "${sessionName}"`);
306
+ await exec(`tmux switch-client -t '${escapedSession}'`);
276
307
  } else {
277
308
  const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
278
309
  const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
@@ -291,53 +322,64 @@ var TmuxManager = class {
291
322
  }
292
323
  buildShellCommands(projectName, projectPath, claudeArgs = []) {
293
324
  const sessionName = this.getSessionName(projectName);
325
+ const escapedSession = escapeForSingleQuotes(sessionName);
326
+ const escapedPath = escapeForSingleQuotes(projectPath);
294
327
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
328
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
295
329
  return [
296
- `# Create tmux split session for ${projectName}`,
297
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
298
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
299
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
300
- `tmux select-pane -t "${sessionName}:0.0"`,
330
+ `# Create tmux split session for ${sanitizeForShell(projectName)}`,
331
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
332
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
333
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
334
+ `tmux select-pane -t '${escapedSession}:0.0'`,
301
335
  this.getAttachCommand(sessionName)
302
336
  ];
303
337
  }
304
338
  buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
305
339
  const sessionName = this.getSessionName(projectName);
340
+ const escapedSession = escapeForSingleQuotes(sessionName);
341
+ const escapedPath = escapeForSingleQuotes(projectPath);
306
342
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
343
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
344
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
307
345
  return [
308
- `# Create tmux three-pane session for ${projectName}`,
309
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
310
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
311
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
312
- `tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`,
313
- `tmux set-option -t "${sessionName}:0.2" remain-on-exit on`,
314
- `tmux resize-pane -t "${sessionName}:0.2" -y 10`,
315
- `tmux select-pane -t "${sessionName}:0.0"`,
346
+ `# Create tmux three-pane session for ${sanitizeForShell(projectName)}`,
347
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
348
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
349
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
350
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`,
351
+ `tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`,
352
+ `tmux resize-pane -t '${escapedSession}:0.2' -y 10`,
353
+ `tmux select-pane -t '${escapedSession}:0.0'`,
316
354
  this.getAttachCommand(sessionName)
317
355
  ];
318
356
  }
319
357
  buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = "npm run dev") {
320
358
  const sessionName = this.getSessionName(projectName);
359
+ const escapedSession = escapeForSingleQuotes(sessionName);
360
+ const escapedPath = escapeForSingleQuotes(projectPath);
361
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
321
362
  return [
322
- `# Create tmux two-pane session with npm for ${projectName}`,
323
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
324
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}"`,
325
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`,
326
- `tmux set-option -t "${sessionName}:0.1" remain-on-exit on`,
327
- `tmux select-pane -t "${sessionName}:0.0"`,
363
+ `# Create tmux two-pane session with npm for ${sanitizeForShell(projectName)}`,
364
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
365
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`,
366
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`,
367
+ `tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`,
368
+ `tmux select-pane -t '${escapedSession}:0.0'`,
328
369
  this.getAttachCommand(sessionName)
329
370
  ];
330
371
  }
331
372
  getAttachCommand(sessionName) {
373
+ const escapedSession = escapeForSingleQuotes(sessionName);
332
374
  if (process.env.TMUX) {
333
- return `tmux switch-client -t "${sessionName}"`;
375
+ return `tmux switch-client -t '${escapedSession}'`;
334
376
  }
335
377
  const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || process.env.ITERM_SESSION_ID;
336
378
  const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
337
379
  if (useiTermIntegration) {
338
- return `tmux -CC attach-session -t "${sessionName}"`;
380
+ return `tmux -CC attach-session -t '${escapedSession}'`;
339
381
  }
340
- return `tmux attach-session -t "${sessionName}"`;
382
+ return `tmux attach-session -t '${escapedSession}'`;
341
383
  }
342
384
  async listWorkonSessions() {
343
385
  try {
@@ -397,7 +439,8 @@ var EnvironmentRecognizer = class {
397
439
  const git = (0, import_simple_git.simpleGit)(gitDir.path);
398
440
  const branchSummary = await git.branchLocal();
399
441
  base.branch = branchSummary.current;
400
- } catch {
442
+ } catch (error) {
443
+ this.log.debug(`Git branch detection failed: ${error.message}`);
401
444
  }
402
445
  }
403
446
  return this.getProjectEnvironment(base, matching);
@@ -499,18 +542,22 @@ var CwdEvent = class {
499
542
  const { project, isShellMode, shellCommands } = context;
500
543
  const projectPath = project.path.path;
501
544
  if (isShellMode) {
502
- shellCommands.push(`cd "${projectPath}"`);
545
+ shellCommands.push(`pushd "${projectPath}" > /dev/null`);
503
546
  } else {
504
547
  const shell = process.env.SHELL || "/bin/bash";
505
- (0, import_child_process2.spawn)(shell, [], {
548
+ const child = (0, import_child_process2.spawn)(shell, ["-i"], {
506
549
  cwd: projectPath,
507
550
  stdio: "inherit"
508
551
  });
552
+ await new Promise((resolve, reject) => {
553
+ child.on("close", () => resolve());
554
+ child.on("error", (err) => reject(err));
555
+ });
509
556
  }
510
557
  },
511
558
  generateShellCommand(context) {
512
559
  const projectPath = context.project.path.path;
513
- return [`cd "${projectPath}"`];
560
+ return [`pushd "${projectPath}" > /dev/null`];
514
561
  }
515
562
  };
516
563
  }
@@ -569,7 +616,7 @@ var IdeEvent = class {
569
616
  const projectPath = project.path.path;
570
617
  const ide = project.ide || "code";
571
618
  if (isShellMode) {
572
- shellCommands.push(`${ide} "${projectPath}" &`);
619
+ shellCommands.push(`set +m; ${ide} "${projectPath}" &>/dev/null &`);
573
620
  } else {
574
621
  (0, import_child_process3.spawn)(ide, [projectPath], {
575
622
  detached: true,
@@ -580,7 +627,7 @@ var IdeEvent = class {
580
627
  generateShellCommand(context) {
581
628
  const projectPath = context.project.path.path;
582
629
  const ide = context.project.ide || "code";
583
- return [`${ide} "${projectPath}" &`];
630
+ return [`set +m; ${ide} "${projectPath}" &>/dev/null &`];
584
631
  }
585
632
  };
586
633
  }
@@ -1100,23 +1147,19 @@ var EventRegistryClass = class {
1100
1147
  */
1101
1148
  registerEvents() {
1102
1149
  for (const EventClass of ALL_EVENTS) {
1103
- if (this.isValidEvent(EventClass)) {
1104
- const metadata = EventClass.metadata;
1105
- this._events.set(metadata.name, EventClass);
1150
+ if (this.isValidEventClass(EventClass)) {
1151
+ this._events.set(EventClass.metadata.name, EventClass);
1106
1152
  }
1107
1153
  }
1108
1154
  }
1109
1155
  /**
1110
- * Validate if a class is a proper event
1156
+ * Type guard to check if an object is a valid EventHandlerClass
1111
1157
  */
1112
- isValidEvent(EventClass) {
1113
- try {
1114
- if (typeof EventClass !== "function") return false;
1115
- const metadata = EventClass.metadata;
1116
- return metadata !== void 0 && typeof metadata.name === "string" && typeof metadata.displayName === "string" && typeof EventClass.validation === "object" && typeof EventClass.configuration === "object" && typeof EventClass.processing === "object";
1117
- } catch {
1118
- return false;
1119
- }
1158
+ isValidEventClass(obj) {
1159
+ if (typeof obj !== "function" && typeof obj !== "object") return false;
1160
+ if (obj === null) return false;
1161
+ const candidate = obj;
1162
+ 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";
1120
1163
  }
1121
1164
  /**
1122
1165
  * Get all valid event names from registered events
@@ -1138,12 +1181,11 @@ var EventRegistryClass = class {
1138
1181
  getEventsForManageUI() {
1139
1182
  this.ensureInitialized();
1140
1183
  const events = [];
1141
- for (const [name, EventClass] of this._events) {
1142
- const metadata = EventClass.metadata;
1184
+ for (const [name, eventClass] of this._events) {
1143
1185
  events.push({
1144
- name: metadata.displayName,
1186
+ name: eventClass.metadata.displayName,
1145
1187
  value: name,
1146
- description: metadata.description
1188
+ description: eventClass.metadata.description
1147
1189
  });
1148
1190
  }
1149
1191
  return events.sort((a, b) => a.name.localeCompare(b.name));
@@ -1154,13 +1196,13 @@ var EventRegistryClass = class {
1154
1196
  getTmuxEnabledEvents() {
1155
1197
  this.ensureInitialized();
1156
1198
  const tmuxEvents = [];
1157
- for (const [name, EventClass] of this._events) {
1158
- const tmux = EventClass.tmux;
1199
+ for (const [name, eventClass] of this._events) {
1200
+ const tmux = eventClass.tmux;
1159
1201
  if (tmux) {
1160
1202
  tmuxEvents.push({
1161
1203
  name,
1162
- event: EventClass,
1163
- priority: tmux.getLayoutPriority ? tmux.getLayoutPriority() : 0
1204
+ event: eventClass,
1205
+ priority: tmux.getLayoutPriority()
1164
1206
  });
1165
1207
  }
1166
1208
  }
@@ -1172,16 +1214,15 @@ var EventRegistryClass = class {
1172
1214
  getAllEvents() {
1173
1215
  this.ensureInitialized();
1174
1216
  const events = [];
1175
- for (const [name, EventClass] of this._events) {
1176
- const typedClass = EventClass;
1217
+ for (const [name, eventClass] of this._events) {
1177
1218
  events.push({
1178
1219
  name,
1179
- metadata: typedClass.metadata,
1180
- hasValidation: !!typedClass.validation,
1181
- hasConfiguration: !!typedClass.configuration,
1182
- hasProcessing: !!typedClass.processing,
1183
- hasTmux: !!typedClass.tmux,
1184
- hasHelp: !!typedClass.help
1220
+ metadata: eventClass.metadata,
1221
+ hasValidation: !!eventClass.validation,
1222
+ hasConfiguration: !!eventClass.configuration,
1223
+ hasProcessing: !!eventClass.processing,
1224
+ hasTmux: !!eventClass.tmux,
1225
+ hasHelp: !!eventClass.help
1185
1226
  });
1186
1227
  }
1187
1228
  return events;