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.js CHANGED
@@ -162,6 +162,18 @@ var Project = class {
162
162
  // src/lib/tmux.ts
163
163
  import { exec as execCallback, spawn } from "child_process";
164
164
  import { promisify } from "util";
165
+
166
+ // src/lib/sanitize.ts
167
+ function sanitizeForShell(input4) {
168
+ if (!input4) return "";
169
+ return input4.replace(/[^a-zA-Z0-9_\-.]/g, "_");
170
+ }
171
+ function escapeForSingleQuotes(input4) {
172
+ if (!input4) return "";
173
+ return input4.replace(/'/g, "'\\''");
174
+ }
175
+
176
+ // src/lib/tmux.ts
165
177
  var exec = promisify(execCallback);
166
178
  var TmuxManager = class {
167
179
  sessionPrefix = "workon-";
@@ -175,18 +187,18 @@ var TmuxManager = class {
175
187
  }
176
188
  async sessionExists(sessionName) {
177
189
  try {
178
- await exec(`tmux has-session -t "${sessionName}"`);
190
+ await exec(`tmux has-session -t '${escapeForSingleQuotes(sessionName)}'`);
179
191
  return true;
180
192
  } catch {
181
193
  return false;
182
194
  }
183
195
  }
184
196
  getSessionName(projectName) {
185
- return `${this.sessionPrefix}${projectName}`;
197
+ return `${this.sessionPrefix}${sanitizeForShell(projectName)}`;
186
198
  }
187
199
  async killSession(sessionName) {
188
200
  try {
189
- await exec(`tmux kill-session -t "${sessionName}"`);
201
+ await exec(`tmux kill-session -t '${escapeForSingleQuotes(sessionName)}'`);
190
202
  return true;
191
203
  } catch {
192
204
  return false;
@@ -194,43 +206,62 @@ var TmuxManager = class {
194
206
  }
195
207
  async createSplitSession(projectName, projectPath, claudeArgs = []) {
196
208
  const sessionName = this.getSessionName(projectName);
209
+ const escapedSession = escapeForSingleQuotes(sessionName);
210
+ const escapedPath = escapeForSingleQuotes(projectPath);
197
211
  if (await this.sessionExists(sessionName)) {
198
212
  await this.killSession(sessionName);
199
213
  }
200
214
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
201
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
202
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
203
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
215
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
216
+ await exec(
217
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
218
+ );
219
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
220
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
204
221
  return sessionName;
205
222
  }
206
223
  async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
207
224
  const sessionName = this.getSessionName(projectName);
225
+ const escapedSession = escapeForSingleQuotes(sessionName);
226
+ const escapedPath = escapeForSingleQuotes(projectPath);
208
227
  if (await this.sessionExists(sessionName)) {
209
228
  await this.killSession(sessionName);
210
229
  }
211
230
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
212
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
213
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
214
- await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
215
- await exec(`tmux set-option -t "${sessionName}:0.2" remain-on-exit on`);
216
- await exec(`tmux resize-pane -t "${sessionName}:0.2" -y 10`);
217
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
231
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
232
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
233
+ await exec(
234
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
235
+ );
236
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
237
+ await exec(
238
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`
239
+ );
240
+ await exec(`tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`);
241
+ await exec(`tmux resize-pane -t '${escapedSession}:0.2' -y 10`);
242
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
218
243
  return sessionName;
219
244
  }
220
245
  async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
221
246
  const sessionName = this.getSessionName(projectName);
247
+ const escapedSession = escapeForSingleQuotes(sessionName);
248
+ const escapedPath = escapeForSingleQuotes(projectPath);
249
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
222
250
  if (await this.sessionExists(sessionName)) {
223
251
  await this.killSession(sessionName);
224
252
  }
225
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
226
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
227
- await exec(`tmux set-option -t "${sessionName}:0.1" remain-on-exit on`);
228
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
253
+ await exec(`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`);
254
+ await exec(
255
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`
256
+ );
257
+ await exec(`tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`);
258
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
229
259
  return sessionName;
230
260
  }
231
261
  async attachToSession(sessionName) {
262
+ const escapedSession = escapeForSingleQuotes(sessionName);
232
263
  if (process.env.TMUX) {
233
- await exec(`tmux switch-client -t "${sessionName}"`);
264
+ await exec(`tmux switch-client -t '${escapedSession}'`);
234
265
  } else {
235
266
  const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
236
267
  const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
@@ -249,53 +280,64 @@ var TmuxManager = class {
249
280
  }
250
281
  buildShellCommands(projectName, projectPath, claudeArgs = []) {
251
282
  const sessionName = this.getSessionName(projectName);
283
+ const escapedSession = escapeForSingleQuotes(sessionName);
284
+ const escapedPath = escapeForSingleQuotes(projectPath);
252
285
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
286
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
253
287
  return [
254
- `# Create tmux split session for ${projectName}`,
255
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
256
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
257
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
258
- `tmux select-pane -t "${sessionName}:0.0"`,
288
+ `# Create tmux split session for ${sanitizeForShell(projectName)}`,
289
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
290
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
291
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
292
+ `tmux select-pane -t '${escapedSession}:0.0'`,
259
293
  this.getAttachCommand(sessionName)
260
294
  ];
261
295
  }
262
296
  buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
263
297
  const sessionName = this.getSessionName(projectName);
298
+ const escapedSession = escapeForSingleQuotes(sessionName);
299
+ const escapedPath = escapeForSingleQuotes(projectPath);
264
300
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
301
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
302
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
265
303
  return [
266
- `# Create tmux three-pane session for ${projectName}`,
267
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
268
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
269
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
270
- `tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`,
271
- `tmux set-option -t "${sessionName}:0.2" remain-on-exit on`,
272
- `tmux resize-pane -t "${sessionName}:0.2" -y 10`,
273
- `tmux select-pane -t "${sessionName}:0.0"`,
304
+ `# Create tmux three-pane session for ${sanitizeForShell(projectName)}`,
305
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
306
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
307
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
308
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`,
309
+ `tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`,
310
+ `tmux resize-pane -t '${escapedSession}:0.2' -y 10`,
311
+ `tmux select-pane -t '${escapedSession}:0.0'`,
274
312
  this.getAttachCommand(sessionName)
275
313
  ];
276
314
  }
277
315
  buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = "npm run dev") {
278
316
  const sessionName = this.getSessionName(projectName);
317
+ const escapedSession = escapeForSingleQuotes(sessionName);
318
+ const escapedPath = escapeForSingleQuotes(projectPath);
319
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
279
320
  return [
280
- `# Create tmux two-pane session with npm for ${projectName}`,
281
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
282
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}"`,
283
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`,
284
- `tmux set-option -t "${sessionName}:0.1" remain-on-exit on`,
285
- `tmux select-pane -t "${sessionName}:0.0"`,
321
+ `# Create tmux two-pane session with npm for ${sanitizeForShell(projectName)}`,
322
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
323
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`,
324
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`,
325
+ `tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`,
326
+ `tmux select-pane -t '${escapedSession}:0.0'`,
286
327
  this.getAttachCommand(sessionName)
287
328
  ];
288
329
  }
289
330
  getAttachCommand(sessionName) {
331
+ const escapedSession = escapeForSingleQuotes(sessionName);
290
332
  if (process.env.TMUX) {
291
- return `tmux switch-client -t "${sessionName}"`;
333
+ return `tmux switch-client -t '${escapedSession}'`;
292
334
  }
293
335
  const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || process.env.ITERM_SESSION_ID;
294
336
  const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
295
337
  if (useiTermIntegration) {
296
- return `tmux -CC attach-session -t "${sessionName}"`;
338
+ return `tmux -CC attach-session -t '${escapedSession}'`;
297
339
  }
298
- return `tmux attach-session -t "${sessionName}"`;
340
+ return `tmux attach-session -t '${escapedSession}'`;
299
341
  }
300
342
  async listWorkonSessions() {
301
343
  try {
@@ -355,7 +397,8 @@ var EnvironmentRecognizer = class {
355
397
  const git = simpleGit(gitDir.path);
356
398
  const branchSummary = await git.branchLocal();
357
399
  base.branch = branchSummary.current;
358
- } catch {
400
+ } catch (error) {
401
+ this.log.debug(`Git branch detection failed: ${error.message}`);
359
402
  }
360
403
  }
361
404
  return this.getProjectEnvironment(base, matching);
@@ -457,18 +500,22 @@ var CwdEvent = class {
457
500
  const { project, isShellMode, shellCommands } = context;
458
501
  const projectPath = project.path.path;
459
502
  if (isShellMode) {
460
- shellCommands.push(`cd "${projectPath}"`);
503
+ shellCommands.push(`pushd "${projectPath}" > /dev/null`);
461
504
  } else {
462
505
  const shell = process.env.SHELL || "/bin/bash";
463
- spawn2(shell, [], {
506
+ const child = spawn2(shell, ["-i"], {
464
507
  cwd: projectPath,
465
508
  stdio: "inherit"
466
509
  });
510
+ await new Promise((resolve, reject) => {
511
+ child.on("close", () => resolve());
512
+ child.on("error", (err) => reject(err));
513
+ });
467
514
  }
468
515
  },
469
516
  generateShellCommand(context) {
470
517
  const projectPath = context.project.path.path;
471
- return [`cd "${projectPath}"`];
518
+ return [`pushd "${projectPath}" > /dev/null`];
472
519
  }
473
520
  };
474
521
  }
@@ -527,7 +574,7 @@ var IdeEvent = class {
527
574
  const projectPath = project.path.path;
528
575
  const ide = project.ide || "code";
529
576
  if (isShellMode) {
530
- shellCommands.push(`${ide} "${projectPath}" &`);
577
+ shellCommands.push(`set +m; ${ide} "${projectPath}" &>/dev/null &`);
531
578
  } else {
532
579
  spawn3(ide, [projectPath], {
533
580
  detached: true,
@@ -538,7 +585,7 @@ var IdeEvent = class {
538
585
  generateShellCommand(context) {
539
586
  const projectPath = context.project.path.path;
540
587
  const ide = context.project.ide || "code";
541
- return [`${ide} "${projectPath}" &`];
588
+ return [`set +m; ${ide} "${projectPath}" &>/dev/null &`];
542
589
  }
543
590
  };
544
591
  }
@@ -1058,23 +1105,19 @@ var EventRegistryClass = class {
1058
1105
  */
1059
1106
  registerEvents() {
1060
1107
  for (const EventClass of ALL_EVENTS) {
1061
- if (this.isValidEvent(EventClass)) {
1062
- const metadata = EventClass.metadata;
1063
- this._events.set(metadata.name, EventClass);
1108
+ if (this.isValidEventClass(EventClass)) {
1109
+ this._events.set(EventClass.metadata.name, EventClass);
1064
1110
  }
1065
1111
  }
1066
1112
  }
1067
1113
  /**
1068
- * Validate if a class is a proper event
1114
+ * Type guard to check if an object is a valid EventHandlerClass
1069
1115
  */
1070
- isValidEvent(EventClass) {
1071
- try {
1072
- if (typeof EventClass !== "function") return false;
1073
- const metadata = EventClass.metadata;
1074
- return metadata !== void 0 && typeof metadata.name === "string" && typeof metadata.displayName === "string" && typeof EventClass.validation === "object" && typeof EventClass.configuration === "object" && typeof EventClass.processing === "object";
1075
- } catch {
1076
- return false;
1077
- }
1116
+ isValidEventClass(obj) {
1117
+ if (typeof obj !== "function" && typeof obj !== "object") return false;
1118
+ if (obj === null) return false;
1119
+ const candidate = obj;
1120
+ 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";
1078
1121
  }
1079
1122
  /**
1080
1123
  * Get all valid event names from registered events
@@ -1096,12 +1139,11 @@ var EventRegistryClass = class {
1096
1139
  getEventsForManageUI() {
1097
1140
  this.ensureInitialized();
1098
1141
  const events = [];
1099
- for (const [name, EventClass] of this._events) {
1100
- const metadata = EventClass.metadata;
1142
+ for (const [name, eventClass] of this._events) {
1101
1143
  events.push({
1102
- name: metadata.displayName,
1144
+ name: eventClass.metadata.displayName,
1103
1145
  value: name,
1104
- description: metadata.description
1146
+ description: eventClass.metadata.description
1105
1147
  });
1106
1148
  }
1107
1149
  return events.sort((a, b) => a.name.localeCompare(b.name));
@@ -1112,13 +1154,13 @@ var EventRegistryClass = class {
1112
1154
  getTmuxEnabledEvents() {
1113
1155
  this.ensureInitialized();
1114
1156
  const tmuxEvents = [];
1115
- for (const [name, EventClass] of this._events) {
1116
- const tmux = EventClass.tmux;
1157
+ for (const [name, eventClass] of this._events) {
1158
+ const tmux = eventClass.tmux;
1117
1159
  if (tmux) {
1118
1160
  tmuxEvents.push({
1119
1161
  name,
1120
- event: EventClass,
1121
- priority: tmux.getLayoutPriority ? tmux.getLayoutPriority() : 0
1162
+ event: eventClass,
1163
+ priority: tmux.getLayoutPriority()
1122
1164
  });
1123
1165
  }
1124
1166
  }
@@ -1130,16 +1172,15 @@ var EventRegistryClass = class {
1130
1172
  getAllEvents() {
1131
1173
  this.ensureInitialized();
1132
1174
  const events = [];
1133
- for (const [name, EventClass] of this._events) {
1134
- const typedClass = EventClass;
1175
+ for (const [name, eventClass] of this._events) {
1135
1176
  events.push({
1136
1177
  name,
1137
- metadata: typedClass.metadata,
1138
- hasValidation: !!typedClass.validation,
1139
- hasConfiguration: !!typedClass.configuration,
1140
- hasProcessing: !!typedClass.processing,
1141
- hasTmux: !!typedClass.tmux,
1142
- hasHelp: !!typedClass.help
1178
+ metadata: eventClass.metadata,
1179
+ hasValidation: !!eventClass.validation,
1180
+ hasConfiguration: !!eventClass.configuration,
1181
+ hasProcessing: !!eventClass.processing,
1182
+ hasTmux: !!eventClass.tmux,
1183
+ hasHelp: !!eventClass.help
1143
1184
  });
1144
1185
  }
1145
1186
  return events;