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.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
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
this.set("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
|
|
60
|
-
delete
|
|
61
|
-
this.set("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
|
|
336
|
+
const wrappedClaudeCmd = escapeForSingleQuotes(wrapWithShellFallback(claudeCommand));
|
|
216
337
|
await exec(
|
|
217
|
-
`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${
|
|
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
|
|
232
|
-
const
|
|
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}' '${
|
|
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}' '${
|
|
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
|
|
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}' '${
|
|
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
|
|
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}' '${
|
|
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
|
|
302
|
-
const
|
|
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}' '${
|
|
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}' '${
|
|
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
|
|
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}' '${
|
|
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 =
|
|
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 {
|