workon 3.2.2 → 3.2.4

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
@@ -42,14 +42,95 @@ module.exports = __toCommonJS(src_exports);
42
42
 
43
43
  // src/lib/config.ts
44
44
  var import_conf = __toESM(require("conf"), 1);
45
+ var import_fs = require("fs");
46
+ var import_path = require("path");
45
47
  var TRANSIENT_PROPS = ["pkg", "work"];
46
- var Config = class {
48
+ var FileLock = class _FileLock {
49
+ lockPath;
50
+ fd = null;
51
+ static LOCK_TIMEOUT_MS = 5e3;
52
+ static RETRY_INTERVAL_MS = 50;
53
+ constructor(configPath) {
54
+ this.lockPath = `${configPath}.lock`;
55
+ }
56
+ async acquire() {
57
+ const startTime = Date.now();
58
+ const lockDir = (0, import_path.dirname)(this.lockPath);
59
+ if (!(0, import_fs.existsSync)(lockDir)) {
60
+ (0, import_fs.mkdirSync)(lockDir, { recursive: true });
61
+ }
62
+ while (Date.now() - startTime < _FileLock.LOCK_TIMEOUT_MS) {
63
+ try {
64
+ this.fd = (0, import_fs.openSync)(this.lockPath, "wx");
65
+ return;
66
+ } catch (error) {
67
+ if (error.code === "EEXIST") {
68
+ try {
69
+ const stat = await import("fs").then((fs) => fs.promises.stat(this.lockPath));
70
+ const age = Date.now() - stat.mtimeMs;
71
+ if (age > _FileLock.LOCK_TIMEOUT_MS) {
72
+ try {
73
+ (0, import_fs.unlinkSync)(this.lockPath);
74
+ } catch {
75
+ }
76
+ }
77
+ } catch {
78
+ }
79
+ await new Promise((resolve) => setTimeout(resolve, _FileLock.RETRY_INTERVAL_MS));
80
+ } else {
81
+ throw error;
82
+ }
83
+ }
84
+ }
85
+ throw new Error("Failed to acquire config lock: timeout");
86
+ }
87
+ release() {
88
+ if (this.fd !== null) {
89
+ try {
90
+ (0, import_fs.closeSync)(this.fd);
91
+ } catch {
92
+ }
93
+ this.fd = null;
94
+ }
95
+ try {
96
+ (0, import_fs.unlinkSync)(this.lockPath);
97
+ } catch {
98
+ }
99
+ }
100
+ };
101
+ var Config = class _Config {
102
+ static _instance = null;
47
103
  _transient = {};
104
+ // Using definite assignment assertion since singleton pattern may return existing instance
48
105
  _store;
106
+ _lock;
49
107
  constructor() {
108
+ if (_Config._instance && process.env.NODE_ENV !== "test") {
109
+ return _Config._instance;
110
+ }
50
111
  this._store = new import_conf.default({
51
- projectName: "workon"
112
+ projectName: "workon",
113
+ ...process.env.WORKON_CONFIG_DIR && { cwd: process.env.WORKON_CONFIG_DIR }
52
114
  });
115
+ this._lock = new FileLock(this._store.path);
116
+ if (process.env.NODE_ENV !== "test") {
117
+ _Config._instance = this;
118
+ }
119
+ }
120
+ /**
121
+ * Get the singleton instance (creates one if needed)
122
+ */
123
+ static getInstance() {
124
+ if (!_Config._instance) {
125
+ _Config._instance = new _Config();
126
+ }
127
+ return _Config._instance;
128
+ }
129
+ /**
130
+ * Reset the singleton instance (for testing purposes)
131
+ */
132
+ static resetInstance() {
133
+ _Config._instance = null;
53
134
  }
54
135
  get(key, defaultValue) {
55
136
  const rootKey = key.split(".")[0];
@@ -64,10 +145,9 @@ var Config = class {
64
145
  this._transient[key] = value;
65
146
  } else {
66
147
  if (value === void 0) {
67
- this._store.set(key, value);
68
- } else {
69
- this._store.set(key, value);
148
+ throw new Error(`Cannot set '${key}' to undefined. Use delete() to remove keys.`);
70
149
  }
150
+ this._store.set(key, value);
71
151
  }
72
152
  }
73
153
  has(key) {
@@ -85,6 +165,9 @@ var Config = class {
85
165
  this._store.delete(key);
86
166
  }
87
167
  }
168
+ /**
169
+ * Get all projects. Returns a fresh copy from the store.
170
+ */
88
171
  getProjects() {
89
172
  return this.get("projects") ?? {};
90
173
  }
@@ -92,15 +175,50 @@ var Config = class {
92
175
  const projects = this.getProjects();
93
176
  return projects[name];
94
177
  }
178
+ /**
179
+ * Set a project with file locking to prevent race conditions.
180
+ * This ensures atomic read-modify-write operations.
181
+ */
182
+ async setProjectSafe(name, config) {
183
+ await this._lock.acquire();
184
+ try {
185
+ const freshProjects = this._store.get("projects") ?? {};
186
+ freshProjects[name] = config;
187
+ this._store.set("projects", freshProjects);
188
+ } finally {
189
+ this._lock.release();
190
+ }
191
+ }
192
+ /**
193
+ * Synchronous version of setProject for backwards compatibility.
194
+ * Note: This is less safe than setProjectSafe() in concurrent scenarios.
195
+ * Consider migrating to setProjectSafe() for critical operations.
196
+ */
95
197
  setProject(name, config) {
96
- const projects = this.getProjects();
97
- projects[name] = config;
98
- this.set("projects", projects);
198
+ const freshProjects = this._store.get("projects") ?? {};
199
+ freshProjects[name] = config;
200
+ this._store.set("projects", freshProjects);
201
+ }
202
+ /**
203
+ * Delete a project with file locking to prevent race conditions.
204
+ */
205
+ async deleteProjectSafe(name) {
206
+ await this._lock.acquire();
207
+ try {
208
+ const freshProjects = this._store.get("projects") ?? {};
209
+ delete freshProjects[name];
210
+ this._store.set("projects", freshProjects);
211
+ } finally {
212
+ this._lock.release();
213
+ }
99
214
  }
215
+ /**
216
+ * Synchronous version of deleteProject for backwards compatibility.
217
+ */
100
218
  deleteProject(name) {
101
- const projects = this.getProjects();
102
- delete projects[name];
103
- this.set("projects", projects);
219
+ const freshProjects = this._store.get("projects") ?? {};
220
+ delete freshProjects[name];
221
+ this._store.set("projects", freshProjects);
104
222
  }
105
223
  getDefaults() {
106
224
  return this.get("project_defaults");
@@ -217,6 +335,9 @@ function escapeForSingleQuotes(input4) {
217
335
 
218
336
  // src/lib/tmux.ts
219
337
  var exec = (0, import_util.promisify)(import_child_process.exec);
338
+ function wrapWithShellFallback(command) {
339
+ return `${command}; exec $SHELL`;
340
+ }
220
341
  var TmuxManager = class {
221
342
  sessionPrefix = "workon-";
222
343
  async isTmuxAvailable() {
@@ -254,9 +375,9 @@ var TmuxManager = class {
254
375
  await this.killSession(sessionName);
255
376
  }
256
377
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
257
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
378
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
258
379
  await exec(
259
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
380
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`
260
381
  );
261
382
  await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
262
383
  await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
@@ -270,16 +391,15 @@ var TmuxManager = class {
270
391
  await this.killSession(sessionName);
271
392
  }
272
393
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
273
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
274
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
394
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
395
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
275
396
  await exec(
276
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
397
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`
277
398
  );
278
399
  await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
279
400
  await exec(
280
- `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`
401
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${wrappedNpmCmd}'`
281
402
  );
282
- await exec(`tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`);
283
403
  await exec(`tmux resize-pane -t '${escapedSession}:0.2' -y 10`);
284
404
  await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
285
405
  return sessionName;
@@ -288,15 +408,14 @@ var TmuxManager = class {
288
408
  const sessionName = this.getSessionName(projectName);
289
409
  const escapedSession = escapeForSingleQuotes(sessionName);
290
410
  const escapedPath = escapeForSingleQuotes(projectPath);
291
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
411
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
292
412
  if (await this.sessionExists(sessionName)) {
293
413
  await this.killSession(sessionName);
294
414
  }
295
415
  await exec(`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`);
296
416
  await exec(
297
- `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`
417
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${wrappedNpmCmd}'`
298
418
  );
299
- await exec(`tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`);
300
419
  await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
301
420
  return sessionName;
302
421
  }
@@ -325,11 +444,11 @@ var TmuxManager = class {
325
444
  const escapedSession = escapeForSingleQuotes(sessionName);
326
445
  const escapedPath = escapeForSingleQuotes(projectPath);
327
446
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
328
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
447
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
329
448
  return [
330
449
  `# Create tmux split session for ${sanitizeForShell(projectName)}`,
331
450
  `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
332
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
451
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`,
333
452
  `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
334
453
  `tmux select-pane -t '${escapedSession}:0.0'`,
335
454
  this.getAttachCommand(sessionName)
@@ -340,15 +459,14 @@ var TmuxManager = class {
340
459
  const escapedSession = escapeForSingleQuotes(sessionName);
341
460
  const escapedPath = escapeForSingleQuotes(projectPath);
342
461
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
343
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
344
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
462
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
463
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
345
464
  return [
346
465
  `# Create tmux three-pane session for ${sanitizeForShell(projectName)}`,
347
466
  `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
348
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
467
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`,
349
468
  `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`,
469
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${wrappedNpmCmd}'`,
352
470
  `tmux resize-pane -t '${escapedSession}:0.2' -y 10`,
353
471
  `tmux select-pane -t '${escapedSession}:0.0'`,
354
472
  this.getAttachCommand(sessionName)
@@ -358,13 +476,12 @@ var TmuxManager = class {
358
476
  const sessionName = this.getSessionName(projectName);
359
477
  const escapedSession = escapeForSingleQuotes(sessionName);
360
478
  const escapedPath = escapeForSingleQuotes(projectPath);
361
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
479
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
362
480
  return [
363
481
  `# Create tmux two-pane session with npm for ${sanitizeForShell(projectName)}`,
364
482
  `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
365
483
  `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`,
484
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${wrappedNpmCmd}'`,
368
485
  `tmux select-pane -t '${escapedSession}:0.0'`,
369
486
  this.getAttachCommand(sessionName)
370
487
  ];
@@ -483,7 +600,7 @@ var EnvironmentRecognizer = class {
483
600
  }
484
601
  static ensureConfigured() {
485
602
  if (!this.configured) {
486
- this.config = new Config();
603
+ this.config = Config.getInstance();
487
604
  this.log = {
488
605
  debug: () => {
489
606
  },
@@ -811,10 +928,10 @@ var ClaudeEvent = class _ClaudeEvent {
811
928
  }
812
929
  static getClaudeCommand(config) {
813
930
  if (typeof config === "boolean" || config === void 0) {
814
- return "claude";
931
+ return "claude --dangerously-skip-permissions";
815
932
  }
816
933
  const flags = config.flags || [];
817
- return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
934
+ return flags.length > 0 ? `claude --dangerously-skip-permissions ${flags.join(" ")}` : "claude --dangerously-skip-permissions";
818
935
  }
819
936
  static get processing() {
820
937
  return {