workon 3.2.1 → 3.2.3

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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import File from 'phylo';
2
2
 
3
- type IdeType = 'vscode' | 'idea' | 'atom' | 'code' | 'subl' | 'vim' | 'emacs';
3
+ type IdeType = 'vscode' | 'idea' | 'atom' | 'code' | 'subl' | 'vim' | 'emacs' | 'cursor';
4
4
  interface ClaudeConfig {
5
5
  flags?: string[];
6
6
  split_terminal?: boolean;
@@ -135,17 +135,51 @@ interface OpenOptions extends GlobalOptions {
135
135
  dryRun?: boolean;
136
136
  }
137
137
 
138
+ /**
139
+ * Config class with singleton pattern and file locking to prevent
140
+ * race conditions that could clear the config.
141
+ */
138
142
  declare class Config {
143
+ private static _instance;
139
144
  private _transient;
140
145
  private _store;
146
+ private _lock;
141
147
  constructor();
148
+ /**
149
+ * Get the singleton instance (creates one if needed)
150
+ */
151
+ static getInstance(): Config;
152
+ /**
153
+ * Reset the singleton instance (for testing purposes)
154
+ */
155
+ static resetInstance(): void;
142
156
  get<T = unknown>(key: string, defaultValue?: T): T | undefined;
143
- set(key: string, value?: unknown): void;
157
+ set(key: string, value: unknown): void;
144
158
  has(key: string): boolean;
145
159
  delete(key: string): void;
160
+ /**
161
+ * Get all projects. Returns a fresh copy from the store.
162
+ */
146
163
  getProjects(): Record<string, ProjectConfig>;
147
164
  getProject(name: string): ProjectConfig | undefined;
165
+ /**
166
+ * Set a project with file locking to prevent race conditions.
167
+ * This ensures atomic read-modify-write operations.
168
+ */
169
+ setProjectSafe(name: string, config: ProjectConfig): Promise<void>;
170
+ /**
171
+ * Synchronous version of setProject for backwards compatibility.
172
+ * Note: This is less safe than setProjectSafe() in concurrent scenarios.
173
+ * Consider migrating to setProjectSafe() for critical operations.
174
+ */
148
175
  setProject(name: string, config: ProjectConfig): void;
176
+ /**
177
+ * Delete a project with file locking to prevent race conditions.
178
+ */
179
+ deleteProjectSafe(name: string): Promise<void>;
180
+ /**
181
+ * Synchronous version of deleteProject for backwards compatibility.
182
+ */
149
183
  deleteProject(name: string): void;
150
184
  getDefaults(): ProjectDefaults | undefined;
151
185
  setDefaults(defaults: ProjectDefaults): void;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import File from 'phylo';
2
2
 
3
- type IdeType = 'vscode' | 'idea' | 'atom' | 'code' | 'subl' | 'vim' | 'emacs';
3
+ type IdeType = 'vscode' | 'idea' | 'atom' | 'code' | 'subl' | 'vim' | 'emacs' | 'cursor';
4
4
  interface ClaudeConfig {
5
5
  flags?: string[];
6
6
  split_terminal?: boolean;
@@ -135,17 +135,51 @@ interface OpenOptions extends GlobalOptions {
135
135
  dryRun?: boolean;
136
136
  }
137
137
 
138
+ /**
139
+ * Config class with singleton pattern and file locking to prevent
140
+ * race conditions that could clear the config.
141
+ */
138
142
  declare class Config {
143
+ private static _instance;
139
144
  private _transient;
140
145
  private _store;
146
+ private _lock;
141
147
  constructor();
148
+ /**
149
+ * Get the singleton instance (creates one if needed)
150
+ */
151
+ static getInstance(): Config;
152
+ /**
153
+ * Reset the singleton instance (for testing purposes)
154
+ */
155
+ static resetInstance(): void;
142
156
  get<T = unknown>(key: string, defaultValue?: T): T | undefined;
143
- set(key: string, value?: unknown): void;
157
+ set(key: string, value: unknown): void;
144
158
  has(key: string): boolean;
145
159
  delete(key: string): void;
160
+ /**
161
+ * Get all projects. Returns a fresh copy from the store.
162
+ */
146
163
  getProjects(): Record<string, ProjectConfig>;
147
164
  getProject(name: string): ProjectConfig | undefined;
165
+ /**
166
+ * Set a project with file locking to prevent race conditions.
167
+ * This ensures atomic read-modify-write operations.
168
+ */
169
+ setProjectSafe(name: string, config: ProjectConfig): Promise<void>;
170
+ /**
171
+ * Synchronous version of setProject for backwards compatibility.
172
+ * Note: This is less safe than setProjectSafe() in concurrent scenarios.
173
+ * Consider migrating to setProjectSafe() for critical operations.
174
+ */
148
175
  setProject(name: string, config: ProjectConfig): void;
176
+ /**
177
+ * Delete a project with file locking to prevent race conditions.
178
+ */
179
+ deleteProjectSafe(name: string): Promise<void>;
180
+ /**
181
+ * Synchronous version of deleteProject for backwards compatibility.
182
+ */
149
183
  deleteProject(name: string): void;
150
184
  getDefaults(): ProjectDefaults | undefined;
151
185
  setDefaults(defaults: ProjectDefaults): void;
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");
@@ -441,7 +559,7 @@ var EnvironmentRecognizer = class {
441
559
  }
442
560
  static ensureConfigured() {
443
561
  if (!this.configured) {
444
- this.config = new Config();
562
+ this.config = Config.getInstance();
445
563
  this.log = {
446
564
  debug: () => {
447
565
  },
@@ -500,18 +618,22 @@ var CwdEvent = class {
500
618
  const { project, isShellMode, shellCommands } = context;
501
619
  const projectPath = project.path.path;
502
620
  if (isShellMode) {
503
- shellCommands.push(`cd "${projectPath}"`);
621
+ shellCommands.push(`pushd "${projectPath}" > /dev/null`);
504
622
  } else {
505
623
  const shell = process.env.SHELL || "/bin/bash";
506
- spawn2(shell, [], {
624
+ const child = spawn2(shell, ["-i"], {
507
625
  cwd: projectPath,
508
626
  stdio: "inherit"
509
627
  });
628
+ await new Promise((resolve, reject) => {
629
+ child.on("close", () => resolve());
630
+ child.on("error", (err) => reject(err));
631
+ });
510
632
  }
511
633
  },
512
634
  generateShellCommand(context) {
513
635
  const projectPath = context.project.path.path;
514
- return [`cd "${projectPath}"`];
636
+ return [`pushd "${projectPath}" > /dev/null`];
515
637
  }
516
638
  };
517
639
  }
@@ -570,7 +692,7 @@ var IdeEvent = class {
570
692
  const projectPath = project.path.path;
571
693
  const ide = project.ide || "code";
572
694
  if (isShellMode) {
573
- shellCommands.push(`${ide} "${projectPath}" &`);
695
+ shellCommands.push(`set +m; ${ide} "${projectPath}" &>/dev/null &`);
574
696
  } else {
575
697
  spawn3(ide, [projectPath], {
576
698
  detached: true,
@@ -581,7 +703,7 @@ var IdeEvent = class {
581
703
  generateShellCommand(context) {
582
704
  const projectPath = context.project.path.path;
583
705
  const ide = context.project.ide || "code";
584
- return [`${ide} "${projectPath}" &`];
706
+ return [`set +m; ${ide} "${projectPath}" &>/dev/null &`];
585
707
  }
586
708
  };
587
709
  }
@@ -765,10 +887,10 @@ var ClaudeEvent = class _ClaudeEvent {
765
887
  }
766
888
  static getClaudeCommand(config) {
767
889
  if (typeof config === "boolean" || config === void 0) {
768
- return "claude";
890
+ return "claude --dangerously-skip-permissions";
769
891
  }
770
892
  const flags = config.flags || [];
771
- return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
893
+ return flags.length > 0 ? `claude --dangerously-skip-permissions ${flags.join(" ")}` : "claude --dangerously-skip-permissions";
772
894
  }
773
895
  static get processing() {
774
896
  return {