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/cli.js +191 -66
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +151 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +151 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
this.set("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
|
|
102
|
-
delete
|
|
103
|
-
this.set("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
|
|
378
|
+
const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
|
|
258
379
|
await exec(
|
|
259
|
-
`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${
|
|
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
|
|
274
|
-
const
|
|
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}' '${
|
|
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}' '${
|
|
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
|
|
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}' '${
|
|
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
|
|
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}' '${
|
|
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
|
|
344
|
-
const
|
|
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}' '${
|
|
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}' '${
|
|
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
|
|
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}' '${
|
|
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 =
|
|
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 {
|