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.js CHANGED
@@ -1,13 +1,94 @@
1
1
  // src/lib/config.ts
2
2
  import Conf from "conf";
3
+ import { openSync, closeSync, unlinkSync, existsSync, mkdirSync } from "fs";
4
+ import { dirname } from "path";
3
5
  var TRANSIENT_PROPS = ["pkg", "work"];
4
- var Config = class {
6
+ var FileLock = class _FileLock {
7
+ lockPath;
8
+ fd = null;
9
+ static LOCK_TIMEOUT_MS = 5e3;
10
+ static RETRY_INTERVAL_MS = 50;
11
+ constructor(configPath) {
12
+ this.lockPath = `${configPath}.lock`;
13
+ }
14
+ async acquire() {
15
+ const startTime = Date.now();
16
+ const lockDir = dirname(this.lockPath);
17
+ if (!existsSync(lockDir)) {
18
+ mkdirSync(lockDir, { recursive: true });
19
+ }
20
+ while (Date.now() - startTime < _FileLock.LOCK_TIMEOUT_MS) {
21
+ try {
22
+ this.fd = openSync(this.lockPath, "wx");
23
+ return;
24
+ } catch (error) {
25
+ if (error.code === "EEXIST") {
26
+ try {
27
+ const stat = await import("fs").then((fs) => fs.promises.stat(this.lockPath));
28
+ const age = Date.now() - stat.mtimeMs;
29
+ if (age > _FileLock.LOCK_TIMEOUT_MS) {
30
+ try {
31
+ unlinkSync(this.lockPath);
32
+ } catch {
33
+ }
34
+ }
35
+ } catch {
36
+ }
37
+ await new Promise((resolve) => setTimeout(resolve, _FileLock.RETRY_INTERVAL_MS));
38
+ } else {
39
+ throw error;
40
+ }
41
+ }
42
+ }
43
+ throw new Error("Failed to acquire config lock: timeout");
44
+ }
45
+ release() {
46
+ if (this.fd !== null) {
47
+ try {
48
+ closeSync(this.fd);
49
+ } catch {
50
+ }
51
+ this.fd = null;
52
+ }
53
+ try {
54
+ unlinkSync(this.lockPath);
55
+ } catch {
56
+ }
57
+ }
58
+ };
59
+ var Config = class _Config {
60
+ static _instance = null;
5
61
  _transient = {};
62
+ // Using definite assignment assertion since singleton pattern may return existing instance
6
63
  _store;
64
+ _lock;
7
65
  constructor() {
66
+ if (_Config._instance && process.env.NODE_ENV !== "test") {
67
+ return _Config._instance;
68
+ }
8
69
  this._store = new Conf({
9
- projectName: "workon"
70
+ projectName: "workon",
71
+ ...process.env.WORKON_CONFIG_DIR && { cwd: process.env.WORKON_CONFIG_DIR }
10
72
  });
73
+ this._lock = new FileLock(this._store.path);
74
+ if (process.env.NODE_ENV !== "test") {
75
+ _Config._instance = this;
76
+ }
77
+ }
78
+ /**
79
+ * Get the singleton instance (creates one if needed)
80
+ */
81
+ static getInstance() {
82
+ if (!_Config._instance) {
83
+ _Config._instance = new _Config();
84
+ }
85
+ return _Config._instance;
86
+ }
87
+ /**
88
+ * Reset the singleton instance (for testing purposes)
89
+ */
90
+ static resetInstance() {
91
+ _Config._instance = null;
11
92
  }
12
93
  get(key, defaultValue) {
13
94
  const rootKey = key.split(".")[0];
@@ -22,10 +103,9 @@ var Config = class {
22
103
  this._transient[key] = value;
23
104
  } else {
24
105
  if (value === void 0) {
25
- this._store.set(key, value);
26
- } else {
27
- this._store.set(key, value);
106
+ throw new Error(`Cannot set '${key}' to undefined. Use delete() to remove keys.`);
28
107
  }
108
+ this._store.set(key, value);
29
109
  }
30
110
  }
31
111
  has(key) {
@@ -43,6 +123,9 @@ var Config = class {
43
123
  this._store.delete(key);
44
124
  }
45
125
  }
126
+ /**
127
+ * Get all projects. Returns a fresh copy from the store.
128
+ */
46
129
  getProjects() {
47
130
  return this.get("projects") ?? {};
48
131
  }
@@ -50,15 +133,50 @@ var Config = class {
50
133
  const projects = this.getProjects();
51
134
  return projects[name];
52
135
  }
136
+ /**
137
+ * Set a project with file locking to prevent race conditions.
138
+ * This ensures atomic read-modify-write operations.
139
+ */
140
+ async setProjectSafe(name, config) {
141
+ await this._lock.acquire();
142
+ try {
143
+ const freshProjects = this._store.get("projects") ?? {};
144
+ freshProjects[name] = config;
145
+ this._store.set("projects", freshProjects);
146
+ } finally {
147
+ this._lock.release();
148
+ }
149
+ }
150
+ /**
151
+ * Synchronous version of setProject for backwards compatibility.
152
+ * Note: This is less safe than setProjectSafe() in concurrent scenarios.
153
+ * Consider migrating to setProjectSafe() for critical operations.
154
+ */
53
155
  setProject(name, config) {
54
- const projects = this.getProjects();
55
- projects[name] = config;
56
- this.set("projects", projects);
156
+ const freshProjects = this._store.get("projects") ?? {};
157
+ freshProjects[name] = config;
158
+ this._store.set("projects", freshProjects);
159
+ }
160
+ /**
161
+ * Delete a project with file locking to prevent race conditions.
162
+ */
163
+ async deleteProjectSafe(name) {
164
+ await this._lock.acquire();
165
+ try {
166
+ const freshProjects = this._store.get("projects") ?? {};
167
+ delete freshProjects[name];
168
+ this._store.set("projects", freshProjects);
169
+ } finally {
170
+ this._lock.release();
171
+ }
57
172
  }
173
+ /**
174
+ * Synchronous version of deleteProject for backwards compatibility.
175
+ */
58
176
  deleteProject(name) {
59
- const projects = this.getProjects();
60
- delete projects[name];
61
- this.set("projects", projects);
177
+ const freshProjects = this._store.get("projects") ?? {};
178
+ delete freshProjects[name];
179
+ this._store.set("projects", freshProjects);
62
180
  }
63
181
  getDefaults() {
64
182
  return this.get("project_defaults");
@@ -175,6 +293,9 @@ function escapeForSingleQuotes(input4) {
175
293
 
176
294
  // src/lib/tmux.ts
177
295
  var exec = promisify(execCallback);
296
+ function wrapWithShellFallback(command) {
297
+ return `${command}; exec $SHELL`;
298
+ }
178
299
  var TmuxManager = class {
179
300
  sessionPrefix = "workon-";
180
301
  async isTmuxAvailable() {
@@ -212,9 +333,9 @@ var TmuxManager = class {
212
333
  await this.killSession(sessionName);
213
334
  }
214
335
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
215
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
336
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
216
337
  await exec(
217
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
338
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`
218
339
  );
219
340
  await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
220
341
  await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
@@ -228,16 +349,15 @@ var TmuxManager = class {
228
349
  await this.killSession(sessionName);
229
350
  }
230
351
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
231
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
232
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
352
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
353
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
233
354
  await exec(
234
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
355
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`
235
356
  );
236
357
  await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
237
358
  await exec(
238
- `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`
359
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${wrappedNpmCmd}'`
239
360
  );
240
- await exec(`tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`);
241
361
  await exec(`tmux resize-pane -t '${escapedSession}:0.2' -y 10`);
242
362
  await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
243
363
  return sessionName;
@@ -246,15 +366,14 @@ var TmuxManager = class {
246
366
  const sessionName = this.getSessionName(projectName);
247
367
  const escapedSession = escapeForSingleQuotes(sessionName);
248
368
  const escapedPath = escapeForSingleQuotes(projectPath);
249
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
369
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
250
370
  if (await this.sessionExists(sessionName)) {
251
371
  await this.killSession(sessionName);
252
372
  }
253
373
  await exec(`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`);
254
374
  await exec(
255
- `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`
375
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${wrappedNpmCmd}'`
256
376
  );
257
- await exec(`tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`);
258
377
  await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
259
378
  return sessionName;
260
379
  }
@@ -283,11 +402,11 @@ var TmuxManager = class {
283
402
  const escapedSession = escapeForSingleQuotes(sessionName);
284
403
  const escapedPath = escapeForSingleQuotes(projectPath);
285
404
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
286
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
405
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
287
406
  return [
288
407
  `# Create tmux split session for ${sanitizeForShell(projectName)}`,
289
408
  `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
290
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
409
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`,
291
410
  `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
292
411
  `tmux select-pane -t '${escapedSession}:0.0'`,
293
412
  this.getAttachCommand(sessionName)
@@ -298,15 +417,14 @@ var TmuxManager = class {
298
417
  const escapedSession = escapeForSingleQuotes(sessionName);
299
418
  const escapedPath = escapeForSingleQuotes(projectPath);
300
419
  const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
301
- const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
302
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
420
+ const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
421
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
303
422
  return [
304
423
  `# Create tmux three-pane session for ${sanitizeForShell(projectName)}`,
305
424
  `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
306
- `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
425
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${wrappedClaudeCmd}'`,
307
426
  `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`,
427
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${wrappedNpmCmd}'`,
310
428
  `tmux resize-pane -t '${escapedSession}:0.2' -y 10`,
311
429
  `tmux select-pane -t '${escapedSession}:0.0'`,
312
430
  this.getAttachCommand(sessionName)
@@ -316,13 +434,12 @@ var TmuxManager = class {
316
434
  const sessionName = this.getSessionName(projectName);
317
435
  const escapedSession = escapeForSingleQuotes(sessionName);
318
436
  const escapedPath = escapeForSingleQuotes(projectPath);
319
- const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
437
+ const wrappedNpmCmd = escapeForSingleQuotes(wrapWithShellFallback(npmCommand));
320
438
  return [
321
439
  `# Create tmux two-pane session with npm for ${sanitizeForShell(projectName)}`,
322
440
  `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
323
441
  `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`,
442
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${wrappedNpmCmd}'`,
326
443
  `tmux select-pane -t '${escapedSession}:0.0'`,
327
444
  this.getAttachCommand(sessionName)
328
445
  ];
@@ -441,7 +558,7 @@ var EnvironmentRecognizer = class {
441
558
  }
442
559
  static ensureConfigured() {
443
560
  if (!this.configured) {
444
- this.config = new Config();
561
+ this.config = Config.getInstance();
445
562
  this.log = {
446
563
  debug: () => {
447
564
  },
@@ -769,10 +886,10 @@ var ClaudeEvent = class _ClaudeEvent {
769
886
  }
770
887
  static getClaudeCommand(config) {
771
888
  if (typeof config === "boolean" || config === void 0) {
772
- return "claude";
889
+ return "claude --dangerously-skip-permissions";
773
890
  }
774
891
  const flags = config.flags || [];
775
- return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
892
+ return flags.length > 0 ? `claude --dangerously-skip-permissions ${flags.join(" ")}` : "claude --dangerously-skip-permissions";
776
893
  }
777
894
  static get processing() {
778
895
  return {