workon 3.0.0 → 3.2.0

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.
Files changed (3) hide show
  1. package/dist/cli.js +1919 -1273
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,27 +9,313 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/events/extensions/npm.ts
13
- var npm_exports = {};
14
- __export(npm_exports, {
15
- NpmEvent: () => NpmEvent,
16
- default: () => npm_default
12
+ // src/lib/config.ts
13
+ import Conf from "conf";
14
+ var TRANSIENT_PROPS, Config;
15
+ var init_config = __esm({
16
+ "src/lib/config.ts"() {
17
+ "use strict";
18
+ TRANSIENT_PROPS = ["pkg", "work"];
19
+ Config = class {
20
+ _transient = {};
21
+ _store;
22
+ constructor() {
23
+ this._store = new Conf({
24
+ projectName: "workon"
25
+ });
26
+ }
27
+ get(key, defaultValue) {
28
+ const rootKey = key.split(".")[0];
29
+ if (TRANSIENT_PROPS.includes(rootKey)) {
30
+ return this._transient[key] ?? defaultValue;
31
+ }
32
+ return this._store.get(key, defaultValue);
33
+ }
34
+ set(key, value) {
35
+ const rootKey = key.split(".")[0];
36
+ if (TRANSIENT_PROPS.includes(rootKey)) {
37
+ this._transient[key] = value;
38
+ } else {
39
+ if (value === void 0) {
40
+ this._store.set(key, value);
41
+ } else {
42
+ this._store.set(key, value);
43
+ }
44
+ }
45
+ }
46
+ has(key) {
47
+ const rootKey = key.split(".")[0];
48
+ if (TRANSIENT_PROPS.includes(rootKey)) {
49
+ return Object.prototype.hasOwnProperty.call(this._transient, key);
50
+ }
51
+ return this._store.has(key);
52
+ }
53
+ delete(key) {
54
+ const rootKey = key.split(".")[0];
55
+ if (TRANSIENT_PROPS.includes(rootKey)) {
56
+ delete this._transient[key];
57
+ } else {
58
+ this._store.delete(key);
59
+ }
60
+ }
61
+ getProjects() {
62
+ return this.get("projects") ?? {};
63
+ }
64
+ getProject(name) {
65
+ const projects = this.getProjects();
66
+ return projects[name];
67
+ }
68
+ setProject(name, config) {
69
+ const projects = this.getProjects();
70
+ projects[name] = config;
71
+ this.set("projects", projects);
72
+ }
73
+ deleteProject(name) {
74
+ const projects = this.getProjects();
75
+ delete projects[name];
76
+ this.set("projects", projects);
77
+ }
78
+ getDefaults() {
79
+ return this.get("project_defaults");
80
+ }
81
+ setDefaults(defaults) {
82
+ this.set("project_defaults", defaults);
83
+ }
84
+ get path() {
85
+ return this._store.path;
86
+ }
87
+ get store() {
88
+ return this._store.store;
89
+ }
90
+ };
91
+ }
17
92
  });
18
- import { spawn as spawn7 } from "child_process";
19
- import { input as input3, confirm as confirm2 } from "@inquirer/prompts";
20
- var NpmEvent, npm_default;
21
- var init_npm = __esm({
22
- "src/events/extensions/npm.ts"() {
93
+
94
+ // src/lib/project.ts
95
+ import File from "phylo";
96
+ import deepAssign from "deep-assign";
97
+ var Project;
98
+ var init_project = __esm({
99
+ "src/lib/project.ts"() {
23
100
  "use strict";
24
- NpmEvent = class _NpmEvent {
101
+ Project = class {
102
+ name;
103
+ _base;
104
+ _path;
105
+ _ide;
106
+ _events = {};
107
+ _branch;
108
+ _homepage;
109
+ _defaults;
110
+ _initialCfg;
111
+ constructor(name, cfg, defaults) {
112
+ this._defaults = defaults ?? { base: "" };
113
+ this._initialCfg = { path: name, events: {}, ...cfg };
114
+ this.name = cfg?.name ?? name;
115
+ const merged = deepAssign({}, this._defaults, this._initialCfg);
116
+ if (merged.base) {
117
+ this.base = merged.base;
118
+ }
119
+ if (merged.path) {
120
+ this.path = merged.path;
121
+ }
122
+ if (merged.ide) {
123
+ this._ide = merged.ide;
124
+ }
125
+ if (merged.events) {
126
+ this._events = merged.events;
127
+ }
128
+ if (merged.branch) {
129
+ this._branch = merged.branch;
130
+ }
131
+ if (merged.homepage) {
132
+ this._homepage = merged.homepage;
133
+ }
134
+ }
135
+ set base(path) {
136
+ this._base = File.from(path).absolutify();
137
+ }
138
+ get base() {
139
+ return this._base;
140
+ }
141
+ set ide(cmd) {
142
+ this._ide = cmd;
143
+ }
144
+ get ide() {
145
+ return this._ide;
146
+ }
147
+ set events(eventCfg) {
148
+ this._events = eventCfg;
149
+ }
150
+ get events() {
151
+ return this._events;
152
+ }
153
+ set path(path) {
154
+ if (this._base) {
155
+ this._path = this._base.join(path);
156
+ } else {
157
+ this._path = File.from(path);
158
+ }
159
+ this._path = this._path.absolutify();
160
+ }
161
+ get path() {
162
+ if (!this._path) {
163
+ throw new Error("Project path not set");
164
+ }
165
+ return this._path;
166
+ }
167
+ set branch(branch) {
168
+ this._branch = branch;
169
+ }
170
+ get branch() {
171
+ return this._branch;
172
+ }
173
+ set homepage(url) {
174
+ this._homepage = url;
175
+ }
176
+ get homepage() {
177
+ return this._homepage;
178
+ }
179
+ static $isProject = true;
180
+ $isProject = true;
181
+ };
182
+ }
183
+ });
184
+
185
+ // src/lib/environment.ts
186
+ import File2 from "phylo";
187
+ import { simpleGit } from "simple-git";
188
+ var BaseEnvironment, ProjectEnvironment, EnvironmentRecognizer;
189
+ var init_environment = __esm({
190
+ "src/lib/environment.ts"() {
191
+ "use strict";
192
+ init_config();
193
+ init_project();
194
+ BaseEnvironment = class {
195
+ $isProjectEnvironment = false;
196
+ };
197
+ ProjectEnvironment = class _ProjectEnvironment {
198
+ $isProjectEnvironment = true;
199
+ project;
200
+ constructor(projectCfg) {
201
+ this.project = new Project(projectCfg.name, projectCfg);
202
+ }
203
+ static load(cfg, defaults) {
204
+ const project = new Project(cfg.name, cfg, defaults);
205
+ return new _ProjectEnvironment({ ...cfg, name: project.name });
206
+ }
207
+ };
208
+ EnvironmentRecognizer = class {
209
+ static config;
210
+ static log;
211
+ static projects = [];
212
+ static configured = false;
213
+ static configure(config, log) {
214
+ if (this.configured) {
215
+ return;
216
+ }
217
+ this.config = config;
218
+ this.log = log;
219
+ this.configured = true;
220
+ }
221
+ static async recognize(dir) {
222
+ this.ensureConfigured();
223
+ const theDir = File2.from(dir).canonicalize();
224
+ this.log.debug("Directory to recognize is: " + theDir.canonicalPath());
225
+ const allProjects = this.getAllProjects();
226
+ const matching = allProjects.filter((p) => p.path.canonicalPath() === theDir.path);
227
+ if (matching.length === 0) {
228
+ return new BaseEnvironment();
229
+ }
230
+ this.log.debug(`Found ${matching.length} matching projects`);
231
+ const base = matching.find((p) => !p.name.includes("#")) ?? matching[0];
232
+ this.log.debug("Base project is: " + base.name);
233
+ const gitDir = base.path.up(".git");
234
+ if (gitDir) {
235
+ try {
236
+ const git = simpleGit(gitDir.path);
237
+ const branchSummary = await git.branchLocal();
238
+ base.branch = branchSummary.current;
239
+ } catch {
240
+ }
241
+ }
242
+ return this.getProjectEnvironment(base, matching);
243
+ }
244
+ static getAllProjects(refresh = false) {
245
+ if (this.projects.length > 0 && !refresh) {
246
+ return this.projects;
247
+ }
248
+ const defaults = this.config.getDefaults();
249
+ if (!defaults?.base) {
250
+ this.projects = [];
251
+ return this.projects;
252
+ }
253
+ const baseDir = File2.from(defaults.base);
254
+ const projectsMap = this.config.getProjects();
255
+ this.projects = Object.entries(projectsMap).map(([name, project]) => ({
256
+ ...project,
257
+ name,
258
+ path: baseDir.join(project.path)
259
+ }));
260
+ return this.projects;
261
+ }
262
+ static getProjectEnvironment(base, _matching) {
263
+ const exactName = `${base.name}#${base.branch}`;
264
+ const exactProj = this.projects.find((p) => p.name === exactName);
265
+ const toProjectConfig = (p) => ({
266
+ name: p.name,
267
+ path: p.path.path,
268
+ // Convert PhyloFile to string path
269
+ ide: p.ide,
270
+ homepage: p.homepage,
271
+ events: p.events,
272
+ branch: p.branch,
273
+ exactName
274
+ });
275
+ if (exactProj) {
276
+ return new ProjectEnvironment({ ...toProjectConfig(exactProj), branch: base.branch });
277
+ }
278
+ return new ProjectEnvironment(toProjectConfig(base));
279
+ }
280
+ static ensureConfigured() {
281
+ if (!this.configured) {
282
+ this.config = new Config();
283
+ this.log = {
284
+ debug: () => {
285
+ },
286
+ info: () => {
287
+ },
288
+ log: () => {
289
+ },
290
+ warn: () => {
291
+ },
292
+ error: () => {
293
+ },
294
+ setLogLevel: () => {
295
+ }
296
+ };
297
+ this.configured = true;
298
+ }
299
+ }
300
+ };
301
+ }
302
+ });
303
+
304
+ // src/events/core/cwd.ts
305
+ import { spawn as spawn2 } from "child_process";
306
+ var CwdEvent;
307
+ var init_cwd = __esm({
308
+ "src/events/core/cwd.ts"() {
309
+ "use strict";
310
+ CwdEvent = class {
25
311
  static get metadata() {
26
312
  return {
27
- name: "npm",
28
- displayName: "Run NPM command",
29
- description: "Execute NPM scripts in project directory",
30
- category: "development",
31
- requiresTmux: true,
32
- dependencies: ["npm"]
313
+ name: "cwd",
314
+ displayName: "Change directory (cwd)",
315
+ description: "Change current working directory to project path",
316
+ category: "core",
317
+ requiresTmux: false,
318
+ dependencies: []
33
319
  };
34
320
  }
35
321
  static get validation() {
@@ -38,1360 +324,1573 @@ var init_npm = __esm({
38
324
  if (typeof config === "boolean" || config === "true" || config === "false") {
39
325
  return true;
40
326
  }
41
- if (typeof config === "string") {
42
- if (config.trim().length === 0) {
43
- return "npm script name cannot be empty";
44
- }
45
- return true;
46
- }
47
- if (typeof config === "object" && config !== null) {
48
- const cfg = config;
49
- if (typeof cfg.command !== "string" || cfg.command.trim().length === 0) {
50
- return "npm.command must be a non-empty string";
51
- }
52
- if (cfg.watch !== void 0 && typeof cfg.watch !== "boolean") {
53
- return "npm.watch must be a boolean";
54
- }
55
- if (cfg.auto_restart !== void 0 && typeof cfg.auto_restart !== "boolean") {
56
- return "npm.auto_restart must be a boolean";
57
- }
58
- return true;
59
- }
60
- return "npm config must be a boolean, string (script name), or object";
327
+ return "cwd config must be a boolean (true/false)";
61
328
  }
62
329
  };
63
330
  }
64
331
  static get configuration() {
65
332
  return {
66
333
  async configureInteractive() {
67
- const scriptName = await input3({
68
- message: "Enter NPM script to run:",
69
- default: "dev"
70
- });
71
- const useAdvanced = await confirm2({
72
- message: "Configure advanced NPM options?",
73
- default: false
74
- });
75
- if (!useAdvanced) {
76
- return scriptName;
77
- }
78
- const watch = await confirm2({
79
- message: "Enable watch mode?",
80
- default: false
81
- });
82
- const autoRestart = await confirm2({
83
- message: "Auto-restart on crash?",
84
- default: false
85
- });
86
- if (!watch && !autoRestart) {
87
- return scriptName;
88
- }
89
- return {
90
- command: scriptName,
91
- watch,
92
- auto_restart: autoRestart
93
- };
334
+ return true;
94
335
  },
95
336
  getDefaultConfig() {
96
- return "dev";
337
+ return true;
97
338
  }
98
339
  };
99
340
  }
100
- static getNpmCommand(config) {
101
- if (typeof config === "boolean" || config === void 0) {
102
- return "npm run dev";
103
- }
104
- if (typeof config === "string") {
105
- return `npm run ${config}`;
106
- }
107
- return `npm run ${config.command}`;
108
- }
109
341
  static get processing() {
110
342
  return {
111
343
  async processEvent(context) {
112
344
  const { project, isShellMode, shellCommands } = context;
113
- const npmConfig = project.events.npm;
114
- const npmCommand = _NpmEvent.getNpmCommand(npmConfig);
345
+ const projectPath = project.path.path;
115
346
  if (isShellMode) {
116
- shellCommands.push(npmCommand);
347
+ shellCommands.push(`cd "${projectPath}"`);
117
348
  } else {
118
- const [cmd, ...args] = npmCommand.split(" ");
119
- spawn7(cmd, args, {
120
- cwd: project.path.path,
349
+ const shell = process.env.SHELL || "/bin/bash";
350
+ spawn2(shell, [], {
351
+ cwd: projectPath,
121
352
  stdio: "inherit"
122
353
  });
123
354
  }
124
355
  },
125
356
  generateShellCommand(context) {
126
- const npmConfig = context.project.events.npm;
127
- return [_NpmEvent.getNpmCommand(npmConfig)];
357
+ const projectPath = context.project.path.path;
358
+ return [`cd "${projectPath}"`];
128
359
  }
129
360
  };
130
361
  }
131
362
  static get tmux() {
363
+ return null;
364
+ }
365
+ static get help() {
132
366
  return {
133
- getLayoutPriority() {
134
- return 50;
367
+ usage: "cwd: true | false",
368
+ description: "Change the current working directory to the project path",
369
+ examples: [
370
+ { config: true, description: "Enable directory change" },
371
+ { config: false, description: "Disable directory change" }
372
+ ]
373
+ };
374
+ }
375
+ };
376
+ }
377
+ });
378
+
379
+ // src/events/core/ide.ts
380
+ import { spawn as spawn3 } from "child_process";
381
+ var IdeEvent;
382
+ var init_ide = __esm({
383
+ "src/events/core/ide.ts"() {
384
+ "use strict";
385
+ IdeEvent = class {
386
+ static get metadata() {
387
+ return {
388
+ name: "ide",
389
+ displayName: "Open in IDE",
390
+ description: "Open project in configured IDE/editor",
391
+ category: "core",
392
+ requiresTmux: false,
393
+ dependencies: []
394
+ };
395
+ }
396
+ static get validation() {
397
+ return {
398
+ validateConfig(config) {
399
+ if (typeof config === "boolean" || config === "true" || config === "false") {
400
+ return true;
401
+ }
402
+ return "ide config must be a boolean (true/false)";
403
+ }
404
+ };
405
+ }
406
+ static get configuration() {
407
+ return {
408
+ async configureInteractive() {
409
+ return true;
135
410
  },
136
- contributeToLayout(enabledCommands) {
137
- if (enabledCommands.includes("claude")) {
138
- return "three-pane";
411
+ getDefaultConfig() {
412
+ return true;
413
+ }
414
+ };
415
+ }
416
+ static get processing() {
417
+ return {
418
+ async processEvent(context) {
419
+ const { project, isShellMode, shellCommands } = context;
420
+ const projectPath = project.path.path;
421
+ const ide = project.ide || "code";
422
+ if (isShellMode) {
423
+ shellCommands.push(`${ide} "${projectPath}" &`);
424
+ } else {
425
+ spawn3(ide, [projectPath], {
426
+ detached: true,
427
+ stdio: "ignore"
428
+ }).unref();
139
429
  }
140
- return "two-pane-npm";
430
+ },
431
+ generateShellCommand(context) {
432
+ const projectPath = context.project.path.path;
433
+ const ide = context.project.ide || "code";
434
+ return [`${ide} "${projectPath}" &`];
141
435
  }
142
436
  };
143
437
  }
438
+ static get tmux() {
439
+ return null;
440
+ }
144
441
  static get help() {
145
442
  return {
146
- usage: 'npm: true | "script" | { command: string, watch?: boolean, auto_restart?: boolean }',
147
- description: "Run an NPM script in the project directory",
443
+ usage: "ide: true | false",
444
+ description: "Open the project in the configured IDE",
148
445
  examples: [
149
- { config: true, description: "Run npm run dev" },
150
- { config: "test", description: "Run npm run test" },
151
- { config: { command: "dev", watch: true }, description: "Run dev with watch mode" }
446
+ { config: true, description: "Enable IDE opening" },
447
+ { config: false, description: "Disable IDE opening" }
152
448
  ]
153
449
  };
154
450
  }
155
451
  };
156
- npm_default = NpmEvent;
157
452
  }
158
453
  });
159
454
 
160
- // src/commands/interactive.ts
161
- var interactive_exports = {};
162
- __export(interactive_exports, {
163
- runInteractive: () => runInteractive
164
- });
165
- import { select, input as input4, checkbox } from "@inquirer/prompts";
166
- import File3 from "phylo";
167
- import deepAssign2 from "deep-assign";
168
- async function runInteractive(ctx) {
169
- const { config, log, environment, suggestedName } = ctx;
170
- showLogo(config);
171
- log.log("");
172
- const defaultName = suggestedName ?? (environment.$isProjectEnvironment ? environment.project.name : File3.cwd().name);
173
- const fromUser = !!suggestedName;
174
- await startInteractive(defaultName, fromUser, ctx);
175
- }
176
- function showLogo(config) {
177
- const version = config.get("pkg")?.version ?? "unknown";
178
- console.log(
179
- ` 8\x1B[2m${" ".repeat(Math.max(15 - version.length - 1, 1)) + "v" + version}\x1B[22m
180
- Yb db dP .d8b. 8d8b 8.dP \x1B[92m.d8b. 8d8b.\x1B[0m
181
- YbdPYbdP 8' .8 8P 88b \x1B[92m8' .8 8P Y8\x1B[0m
182
- YP YP \`Y8P' 8 8 Yb \x1B[92m\`Y8P' 8 8\x1B[0m`
183
- );
184
- }
185
- async function startInteractive(defaultName, fromUser, ctx, showMain = false) {
186
- const { log, environment } = ctx;
187
- log.debug(`Name '${defaultName}' was${fromUser ? "" : " not"} provided by the user`);
188
- const question = getFirstQuestion(defaultName, fromUser, environment, showMain);
189
- const action = await select(question);
190
- switch (action) {
191
- case "exit":
192
- return;
193
- case "more":
194
- await startInteractive(defaultName, fromUser, ctx, true);
195
- return;
196
- case "init-project":
197
- await initProject(defaultName, fromUser, ctx);
198
- return;
199
- case "init-branch":
200
- await initBranch(defaultName, ctx);
201
- return;
202
- case "switch-project":
203
- log.info("Switch to an existing project");
204
- break;
205
- case "switch-branch":
206
- log.info("Switch to an existing branch");
207
- break;
208
- case "manage-projects":
209
- log.info("Manage existing projects");
210
- break;
211
- case "manage-branches":
212
- log.info("Manage existing branches");
213
- break;
214
- }
215
- }
216
- function getFirstQuestion(defaultName, fromUser, environment, showMain) {
217
- if (!showMain && environment.$isProjectEnvironment && !fromUser) {
218
- return {
219
- message: environment.project.name,
220
- choices: [
221
- { name: "Start a branch", value: "init-branch" },
222
- { name: "Switch branch", value: "switch-branch" },
223
- { name: "Manage branches", value: "manage-branches" },
224
- { name: "---", value: "" },
225
- { name: "More...", value: "more" },
226
- { name: "Exit", value: "exit" }
227
- ].filter((c) => c.value !== "")
228
- };
229
- }
230
- return {
231
- message: "What do you want to do?",
232
- choices: [
233
- { name: "Start a new project", value: "init-project" },
234
- { name: "Open an existing project", value: "switch-project" },
235
- { name: "Manage projects", value: "manage-projects" },
236
- { name: "---", value: "" },
237
- { name: "Exit", value: "exit" }
238
- ].filter((c) => c.value !== "")
239
- };
240
- }
241
- async function initProject(defaultName, fromUser, ctx) {
242
- const { config, log } = ctx;
243
- const defaults = config.getDefaults();
244
- const projects = config.getProjects();
245
- let name;
246
- if (fromUser) {
247
- name = defaultName;
248
- log.log(`Project name: ${name}`);
249
- } else {
250
- name = await input4({
251
- message: "What is the name of the project?",
252
- default: defaultName,
253
- validate: (value) => {
254
- if (value in projects) return "Project already exists.";
255
- if (/\w+#\w+/.test(value)) {
256
- const projectName = value.substring(0, value.indexOf("#"));
257
- if (!(projectName in projects)) {
258
- return `Project '${projectName}' does not exist. Please create it before starting a branch.`;
455
+ // src/events/core/web.ts
456
+ import { spawn as spawn4 } from "child_process";
457
+ import { platform } from "os";
458
+ var WebEvent;
459
+ var init_web = __esm({
460
+ "src/events/core/web.ts"() {
461
+ "use strict";
462
+ WebEvent = class _WebEvent {
463
+ static get metadata() {
464
+ return {
465
+ name: "web",
466
+ displayName: "Open homepage in browser",
467
+ description: "Open project homepage in web browser",
468
+ category: "core",
469
+ requiresTmux: false,
470
+ dependencies: []
471
+ };
472
+ }
473
+ static get validation() {
474
+ return {
475
+ validateConfig(config) {
476
+ if (typeof config === "boolean" || config === "true" || config === "false") {
477
+ return true;
478
+ }
479
+ return "web config must be a boolean (true/false)";
259
480
  }
481
+ };
482
+ }
483
+ static get configuration() {
484
+ return {
485
+ async configureInteractive() {
486
+ return true;
487
+ },
488
+ getDefaultConfig() {
489
+ return true;
490
+ }
491
+ };
492
+ }
493
+ static getOpenCommand() {
494
+ const os = platform();
495
+ switch (os) {
496
+ case "darwin":
497
+ return "open";
498
+ case "win32":
499
+ return "start";
500
+ default:
501
+ return "xdg-open";
260
502
  }
261
- return true;
262
503
  }
263
- });
264
- }
265
- const isBranch = /\w+#\w+/.test(name);
266
- let basePath;
267
- if (isBranch) {
268
- const projectName = name.substring(0, name.indexOf("#"));
269
- basePath = defaults?.base ? File3.from(defaults.base).join(projects[projectName].path).absolutePath() : projects[projectName].path;
270
- log.log(`Project path: ${basePath}`);
271
- } else {
272
- const pathAnswer = await input4({
273
- message: "What is the path to the project?",
274
- default: defaults?.base ? File3.from(defaults.base).join(name).path : name
275
- });
276
- let answerFile = File3.from(pathAnswer);
277
- const defaultBase = defaults?.base ? File3.from(defaults.base) : File3.cwd();
278
- if (!answerFile.isAbsolute()) {
279
- answerFile = defaultBase.join(answerFile.path);
280
- }
281
- try {
282
- const canonical = answerFile.canonicalize();
283
- if (canonical) {
284
- answerFile = canonical;
285
- } else {
286
- answerFile = answerFile.absolutify();
504
+ static get processing() {
505
+ return {
506
+ async processEvent(context) {
507
+ const { project, isShellMode, shellCommands } = context;
508
+ const homepage = project.homepage;
509
+ if (!homepage) {
510
+ console.warn("No homepage configured for project");
511
+ return;
512
+ }
513
+ const openCmd = _WebEvent.getOpenCommand();
514
+ if (isShellMode) {
515
+ shellCommands.push(`${openCmd} "${homepage}" &`);
516
+ } else {
517
+ spawn4(openCmd, [homepage], {
518
+ detached: true,
519
+ stdio: "ignore"
520
+ }).unref();
521
+ }
522
+ },
523
+ generateShellCommand(context) {
524
+ const homepage = context.project.homepage;
525
+ if (!homepage) return [];
526
+ const openCmd = _WebEvent.getOpenCommand();
527
+ return [`${openCmd} "${homepage}" &`];
528
+ }
529
+ };
287
530
  }
288
- } catch {
289
- answerFile = answerFile.absolutify();
290
- }
291
- basePath = answerFile.relativize(defaultBase.path).path;
292
- }
293
- const ide = await select({
294
- message: "What is the IDE?",
295
- choices: IDE_CHOICES
296
- });
297
- const selectedEvents = await checkbox({
298
- message: "Which events should take place when opening?",
299
- choices: [
300
- { name: "Change terminal cwd to project path", value: "cwd", checked: true },
301
- { name: "Open project in IDE", value: "ide", checked: true }
302
- ]
303
- });
304
- const events = {
305
- cwd: selectedEvents.includes("cwd"),
306
- ide: selectedEvents.includes("ide")
307
- };
308
- const projectConfig = {
309
- path: basePath,
310
- ide,
311
- events
312
- };
313
- projects[name] = projectConfig;
314
- config.set("projects", projects);
315
- log.info("Your project has been initialized.");
316
- log.info(`Use 'workon ${name}' to start working!`);
317
- }
318
- async function initBranch(defaultName, ctx) {
319
- const { config, log } = ctx;
320
- const projects = config.getProjects();
321
- const branch = await input4({
322
- message: "What is the name of the branch?",
323
- validate: (value) => {
324
- if (/\w+#\w+/.test(value)) return `Branch name can't contain the "#" sign`;
325
- if (`${defaultName}#${value}` in projects) return "Branch already exists.";
326
- return true;
327
- }
328
- });
329
- const branchName = `${defaultName}#${branch}`;
330
- const baseProject = projects[defaultName];
331
- const branchConfig = deepAssign2({}, baseProject, { branch });
332
- delete branchConfig.name;
333
- projects[branchName] = branchConfig;
334
- config.set("projects", projects);
335
- log.info("Your branch configuration has been initialized.");
336
- log.info(`Use 'workon ${branchName}' to start working!`);
337
- }
338
- var IDE_CHOICES;
339
- var init_interactive = __esm({
340
- "src/commands/interactive.ts"() {
341
- "use strict";
342
- IDE_CHOICES = [
343
- { name: "Visual Studio Code", value: "vscode" },
344
- { name: "IntelliJ IDEA", value: "idea" },
345
- { name: "Atom", value: "atom" }
346
- ];
531
+ static get tmux() {
532
+ return null;
533
+ }
534
+ static get help() {
535
+ return {
536
+ usage: "web: true | false",
537
+ description: "Open the project homepage in the default browser",
538
+ examples: [
539
+ { config: true, description: "Enable browser opening" },
540
+ { config: false, description: "Disable browser opening" }
541
+ ]
542
+ };
543
+ }
544
+ };
347
545
  }
348
546
  });
349
547
 
350
- // src/commands/index.ts
351
- import { Command as Command7 } from "commander";
352
- import { readFileSync, existsSync } from "fs";
353
- import { join, dirname } from "path";
354
- import { fileURLToPath } from "url";
355
- import loog from "loog";
356
- import omelette from "omelette";
357
- import File6 from "phylo";
358
-
359
- // src/lib/config.ts
360
- import Conf from "conf";
361
- var TRANSIENT_PROPS = ["pkg", "work"];
362
- var Config = class {
363
- _transient = {};
364
- _store;
365
- constructor() {
366
- this._store = new Conf({
367
- projectName: "workon"
368
- });
369
- }
370
- get(key, defaultValue) {
371
- const rootKey = key.split(".")[0];
372
- if (TRANSIENT_PROPS.includes(rootKey)) {
373
- return this._transient[key] ?? defaultValue;
374
- }
375
- return this._store.get(key, defaultValue);
376
- }
377
- set(key, value) {
378
- const rootKey = key.split(".")[0];
379
- if (TRANSIENT_PROPS.includes(rootKey)) {
380
- this._transient[key] = value;
381
- } else {
382
- if (value === void 0) {
383
- this._store.set(key, value);
384
- } else {
385
- this._store.set(key, value);
548
+ // src/events/extensions/claude.ts
549
+ import { spawn as spawn5 } from "child_process";
550
+ import { input, confirm } from "@inquirer/prompts";
551
+ var ClaudeEvent;
552
+ var init_claude = __esm({
553
+ "src/events/extensions/claude.ts"() {
554
+ "use strict";
555
+ ClaudeEvent = class _ClaudeEvent {
556
+ static get metadata() {
557
+ return {
558
+ name: "claude",
559
+ displayName: "Launch Claude Code",
560
+ description: "Launch Claude Code with optional flags and configuration",
561
+ category: "development",
562
+ requiresTmux: true,
563
+ dependencies: ["claude"]
564
+ };
386
565
  }
387
- }
388
- }
389
- has(key) {
390
- const rootKey = key.split(".")[0];
391
- if (TRANSIENT_PROPS.includes(rootKey)) {
392
- return Object.prototype.hasOwnProperty.call(this._transient, key);
393
- }
394
- return this._store.has(key);
395
- }
396
- delete(key) {
397
- const rootKey = key.split(".")[0];
398
- if (TRANSIENT_PROPS.includes(rootKey)) {
399
- delete this._transient[key];
400
- } else {
401
- this._store.delete(key);
402
- }
403
- }
404
- getProjects() {
405
- return this.get("projects") ?? {};
406
- }
407
- getProject(name) {
408
- const projects = this.getProjects();
409
- return projects[name];
410
- }
411
- setProject(name, config) {
412
- const projects = this.getProjects();
413
- projects[name] = config;
414
- this.set("projects", projects);
415
- }
416
- deleteProject(name) {
417
- const projects = this.getProjects();
418
- delete projects[name];
419
- this.set("projects", projects);
420
- }
421
- getDefaults() {
422
- return this.get("project_defaults");
423
- }
424
- setDefaults(defaults) {
425
- this.set("project_defaults", defaults);
426
- }
427
- get path() {
428
- return this._store.path;
429
- }
430
- get store() {
431
- return this._store.store;
432
- }
433
- };
434
-
435
- // src/lib/environment.ts
436
- import File2 from "phylo";
437
- import { simpleGit } from "simple-git";
438
-
439
- // src/lib/project.ts
440
- import File from "phylo";
441
- import deepAssign from "deep-assign";
442
- var Project = class {
443
- name;
444
- _base;
445
- _path;
446
- _ide;
447
- _events = {};
448
- _branch;
449
- _homepage;
450
- _defaults;
451
- _initialCfg;
452
- constructor(name, cfg, defaults) {
453
- this._defaults = defaults ?? { base: "" };
454
- this._initialCfg = { path: name, events: {}, ...cfg };
455
- this.name = cfg?.name ?? name;
456
- const merged = deepAssign({}, this._defaults, this._initialCfg);
457
- if (merged.base) {
458
- this.base = merged.base;
459
- }
460
- if (merged.path) {
461
- this.path = merged.path;
462
- }
463
- if (merged.ide) {
464
- this._ide = merged.ide;
465
- }
466
- if (merged.events) {
467
- this._events = merged.events;
468
- }
469
- if (merged.branch) {
470
- this._branch = merged.branch;
471
- }
472
- if (merged.homepage) {
473
- this._homepage = merged.homepage;
474
- }
475
- }
476
- set base(path) {
477
- this._base = File.from(path).absolutify();
478
- }
479
- get base() {
480
- return this._base;
481
- }
482
- set ide(cmd) {
483
- this._ide = cmd;
484
- }
485
- get ide() {
486
- return this._ide;
487
- }
488
- set events(eventCfg) {
489
- this._events = eventCfg;
490
- }
491
- get events() {
492
- return this._events;
493
- }
494
- set path(path) {
495
- if (this._base) {
496
- this._path = this._base.join(path);
497
- } else {
498
- this._path = File.from(path);
499
- }
500
- this._path = this._path.absolutify();
501
- }
502
- get path() {
503
- if (!this._path) {
504
- throw new Error("Project path not set");
505
- }
506
- return this._path;
507
- }
508
- set branch(branch) {
509
- this._branch = branch;
510
- }
511
- get branch() {
512
- return this._branch;
513
- }
514
- set homepage(url) {
515
- this._homepage = url;
516
- }
517
- get homepage() {
518
- return this._homepage;
519
- }
520
- static $isProject = true;
521
- $isProject = true;
522
- };
523
-
524
- // src/lib/environment.ts
525
- var BaseEnvironment = class {
526
- $isProjectEnvironment = false;
527
- };
528
- var ProjectEnvironment = class _ProjectEnvironment {
529
- $isProjectEnvironment = true;
530
- project;
531
- constructor(projectCfg) {
532
- this.project = new Project(projectCfg.name, projectCfg);
533
- }
534
- static load(cfg, defaults) {
535
- const project = new Project(cfg.name, cfg, defaults);
536
- return new _ProjectEnvironment({ ...cfg, name: project.name });
537
- }
538
- };
539
- var EnvironmentRecognizer = class {
540
- static config;
541
- static log;
542
- static projects = [];
543
- static configured = false;
544
- static configure(config, log) {
545
- if (this.configured) {
546
- return;
547
- }
548
- this.config = config;
549
- this.log = log;
550
- this.configured = true;
551
- }
552
- static async recognize(dir) {
553
- this.ensureConfigured();
554
- const theDir = File2.from(dir).canonicalize();
555
- this.log.debug("Directory to recognize is: " + theDir.canonicalPath());
556
- const allProjects = this.getAllProjects();
557
- const matching = allProjects.filter((p) => p.path.canonicalPath() === theDir.path);
558
- if (matching.length === 0) {
559
- return new BaseEnvironment();
560
- }
561
- this.log.debug(`Found ${matching.length} matching projects`);
562
- const base = matching.find((p) => !p.name.includes("#")) ?? matching[0];
563
- this.log.debug("Base project is: " + base.name);
564
- const gitDir = base.path.up(".git");
565
- if (gitDir) {
566
- try {
567
- const git = simpleGit(gitDir.path);
568
- const branchSummary = await git.branchLocal();
569
- base.branch = branchSummary.current;
570
- } catch {
566
+ static get validation() {
567
+ return {
568
+ validateConfig(config) {
569
+ if (typeof config === "boolean" || config === "true" || config === "false") {
570
+ return true;
571
+ }
572
+ if (typeof config === "object" && config !== null) {
573
+ const cfg = config;
574
+ if (cfg.flags !== void 0) {
575
+ if (!Array.isArray(cfg.flags)) {
576
+ return "claude.flags must be an array of strings";
577
+ }
578
+ for (const flag of cfg.flags) {
579
+ if (typeof flag !== "string") {
580
+ return "claude.flags must contain only strings";
581
+ }
582
+ if (!flag.startsWith("-")) {
583
+ return `Invalid flag "${flag}": flags must start with - or --`;
584
+ }
585
+ }
586
+ }
587
+ if (cfg.split_terminal !== void 0 && typeof cfg.split_terminal !== "boolean") {
588
+ return "claude.split_terminal must be a boolean";
589
+ }
590
+ return true;
591
+ }
592
+ return "claude config must be a boolean or object with flags/split_terminal";
593
+ }
594
+ };
571
595
  }
572
- }
573
- return this.getProjectEnvironment(base, matching);
574
- }
575
- static getAllProjects(refresh = false) {
576
- if (this.projects.length > 0 && !refresh) {
577
- return this.projects;
578
- }
579
- const defaults = this.config.getDefaults();
580
- if (!defaults?.base) {
581
- this.projects = [];
582
- return this.projects;
583
- }
584
- const baseDir = File2.from(defaults.base);
585
- const projectsMap = this.config.getProjects();
586
- this.projects = Object.entries(projectsMap).map(([name, project]) => ({
587
- ...project,
588
- name,
589
- path: baseDir.join(project.path)
590
- }));
591
- return this.projects;
592
- }
593
- static getProjectEnvironment(base, _matching) {
594
- const exactName = `${base.name}#${base.branch}`;
595
- const exactProj = this.projects.find((p) => p.name === exactName);
596
- const toProjectConfig = (p) => ({
597
- name: p.name,
598
- path: p.path.path,
599
- // Convert PhyloFile to string path
600
- ide: p.ide,
601
- homepage: p.homepage,
602
- events: p.events,
603
- branch: p.branch,
604
- exactName
605
- });
606
- if (exactProj) {
607
- return new ProjectEnvironment({ ...toProjectConfig(exactProj), branch: base.branch });
608
- }
609
- return new ProjectEnvironment(toProjectConfig(base));
610
- }
611
- static ensureConfigured() {
612
- if (!this.configured) {
613
- this.config = new Config();
614
- this.log = {
615
- debug: () => {
616
- },
617
- info: () => {
618
- },
619
- log: () => {
620
- },
621
- warn: () => {
622
- },
623
- error: () => {
624
- },
625
- setLogLevel: () => {
596
+ static get configuration() {
597
+ return {
598
+ async configureInteractive() {
599
+ const useAdvanced = await confirm({
600
+ message: "Configure advanced Claude options?",
601
+ default: false
602
+ });
603
+ if (!useAdvanced) {
604
+ return true;
605
+ }
606
+ const flagsInput = await input({
607
+ message: "Enter Claude flags (comma-separated, e.g., --resume, --debug):",
608
+ default: ""
609
+ });
610
+ const flags = flagsInput.split(",").map((f) => f.trim()).filter((f) => f.length > 0 && f.startsWith("-"));
611
+ const splitTerminal = await confirm({
612
+ message: "Use split terminal layout (Claude + shell)?",
613
+ default: true
614
+ });
615
+ if (flags.length === 0 && !splitTerminal) {
616
+ return true;
617
+ }
618
+ const config = {};
619
+ if (flags.length > 0) config.flags = flags;
620
+ if (splitTerminal) config.split_terminal = splitTerminal;
621
+ return config;
622
+ },
623
+ getDefaultConfig() {
624
+ return true;
625
+ }
626
+ };
627
+ }
628
+ static getClaudeCommand(config) {
629
+ if (typeof config === "boolean" || config === void 0) {
630
+ return "claude";
626
631
  }
627
- };
628
- this.configured = true;
629
- }
630
- }
631
- };
632
-
633
- // src/commands/open.ts
634
- import { Command } from "commander";
635
- import File4 from "phylo";
636
-
637
- // src/lib/tmux.ts
638
- import { exec as execCallback, spawn } from "child_process";
639
- import { promisify } from "util";
640
- var exec = promisify(execCallback);
641
- var TmuxManager = class {
642
- sessionPrefix = "workon-";
643
- async isTmuxAvailable() {
644
- try {
645
- await exec("which tmux");
646
- return true;
647
- } catch {
648
- return false;
649
- }
650
- }
651
- async sessionExists(sessionName) {
652
- try {
653
- await exec(`tmux has-session -t "${sessionName}"`);
654
- return true;
655
- } catch {
656
- return false;
657
- }
658
- }
659
- getSessionName(projectName) {
660
- return `${this.sessionPrefix}${projectName}`;
661
- }
662
- async killSession(sessionName) {
663
- try {
664
- await exec(`tmux kill-session -t "${sessionName}"`);
665
- return true;
666
- } catch {
667
- return false;
668
- }
669
- }
670
- async createSplitSession(projectName, projectPath, claudeArgs = []) {
671
- const sessionName = this.getSessionName(projectName);
672
- if (await this.sessionExists(sessionName)) {
673
- await this.killSession(sessionName);
674
- }
675
- const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
676
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
677
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
678
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
679
- return sessionName;
680
- }
681
- async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
682
- const sessionName = this.getSessionName(projectName);
683
- if (await this.sessionExists(sessionName)) {
684
- await this.killSession(sessionName);
685
- }
686
- const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
687
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
688
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
689
- await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
690
- await exec(`tmux set-option -t "${sessionName}:0.2" remain-on-exit on`);
691
- await exec(`tmux resize-pane -t "${sessionName}:0.2" -y 10`);
692
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
693
- return sessionName;
694
- }
695
- async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
696
- const sessionName = this.getSessionName(projectName);
697
- if (await this.sessionExists(sessionName)) {
698
- await this.killSession(sessionName);
699
- }
700
- await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
701
- await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
702
- await exec(`tmux set-option -t "${sessionName}:0.1" remain-on-exit on`);
703
- await exec(`tmux select-pane -t "${sessionName}:0.0"`);
704
- return sessionName;
705
- }
706
- async attachToSession(sessionName) {
707
- if (process.env.TMUX) {
708
- await exec(`tmux switch-client -t "${sessionName}"`);
709
- } else {
710
- const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
711
- const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
712
- if (useiTermIntegration) {
713
- spawn("tmux", ["-CC", "attach-session", "-t", sessionName], {
714
- stdio: "inherit",
715
- detached: true
716
- });
717
- } else {
718
- spawn("tmux", ["attach-session", "-t", sessionName], {
719
- stdio: "inherit",
720
- detached: true
721
- });
632
+ const flags = config.flags || [];
633
+ return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
722
634
  }
723
- }
724
- }
725
- buildShellCommands(projectName, projectPath, claudeArgs = []) {
726
- const sessionName = this.getSessionName(projectName);
727
- const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
728
- return [
729
- `# Create tmux split session for ${projectName}`,
730
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
731
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
732
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
733
- `tmux select-pane -t "${sessionName}:0.0"`,
734
- this.getAttachCommand(sessionName)
735
- ];
736
- }
737
- buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
738
- const sessionName = this.getSessionName(projectName);
739
- const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
740
- return [
741
- `# Create tmux three-pane session for ${projectName}`,
742
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
743
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
744
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
745
- `tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`,
746
- `tmux set-option -t "${sessionName}:0.2" remain-on-exit on`,
747
- `tmux resize-pane -t "${sessionName}:0.2" -y 10`,
748
- `tmux select-pane -t "${sessionName}:0.0"`,
749
- this.getAttachCommand(sessionName)
750
- ];
751
- }
752
- buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = "npm run dev") {
753
- const sessionName = this.getSessionName(projectName);
754
- return [
755
- `# Create tmux two-pane session with npm for ${projectName}`,
756
- `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
757
- `tmux new-session -d -s "${sessionName}" -c "${projectPath}"`,
758
- `tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`,
759
- `tmux set-option -t "${sessionName}:0.1" remain-on-exit on`,
760
- `tmux select-pane -t "${sessionName}:0.0"`,
761
- this.getAttachCommand(sessionName)
762
- ];
763
- }
764
- getAttachCommand(sessionName) {
765
- if (process.env.TMUX) {
766
- return `tmux switch-client -t "${sessionName}"`;
767
- }
768
- const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || process.env.ITERM_SESSION_ID;
769
- const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
770
- if (useiTermIntegration) {
771
- return `tmux -CC attach-session -t "${sessionName}"`;
772
- }
773
- return `tmux attach-session -t "${sessionName}"`;
774
- }
775
- async listWorkonSessions() {
776
- try {
777
- const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
778
- return stdout.trim().split("\n").filter((session) => session.startsWith(this.sessionPrefix)).map((session) => session.replace(this.sessionPrefix, ""));
779
- } catch {
780
- return [];
781
- }
635
+ static get processing() {
636
+ return {
637
+ async processEvent(context) {
638
+ const { project, isShellMode, shellCommands } = context;
639
+ const claudeConfig = project.events.claude;
640
+ const claudeCommand = _ClaudeEvent.getClaudeCommand(claudeConfig);
641
+ if (isShellMode) {
642
+ shellCommands.push(claudeCommand);
643
+ } else {
644
+ const args = claudeCommand.split(" ").slice(1);
645
+ spawn5("claude", args, {
646
+ cwd: project.path.path,
647
+ stdio: "inherit"
648
+ });
649
+ }
650
+ },
651
+ generateShellCommand(context) {
652
+ const claudeConfig = context.project.events.claude;
653
+ return [_ClaudeEvent.getClaudeCommand(claudeConfig)];
654
+ }
655
+ };
656
+ }
657
+ static get tmux() {
658
+ return {
659
+ getLayoutPriority() {
660
+ return 100;
661
+ },
662
+ contributeToLayout(enabledCommands) {
663
+ if (enabledCommands.includes("npm")) {
664
+ return "three-pane";
665
+ }
666
+ return "split";
667
+ }
668
+ };
669
+ }
670
+ static get help() {
671
+ return {
672
+ usage: "claude: true | { flags: string[], split_terminal: boolean }",
673
+ description: "Launch Claude Code in the project directory",
674
+ examples: [
675
+ { config: true, description: "Launch Claude with defaults" },
676
+ { config: { flags: ["--resume"] }, description: "Resume previous session" },
677
+ {
678
+ config: { flags: ["--model", "opus"], split_terminal: true },
679
+ description: "Use Opus model with split terminal"
680
+ }
681
+ ]
682
+ };
683
+ }
684
+ };
782
685
  }
783
- };
686
+ });
784
687
 
785
- // src/events/core/cwd.ts
786
- import { spawn as spawn2 } from "child_process";
787
- var CwdEvent = class {
788
- static get metadata() {
789
- return {
790
- name: "cwd",
791
- displayName: "Change directory (cwd)",
792
- description: "Change current working directory to project path",
793
- category: "core",
794
- requiresTmux: false,
795
- dependencies: []
688
+ // src/events/extensions/docker.ts
689
+ import { spawn as spawn6 } from "child_process";
690
+ import { input as input2 } from "@inquirer/prompts";
691
+ var DockerEvent;
692
+ var init_docker = __esm({
693
+ "src/events/extensions/docker.ts"() {
694
+ "use strict";
695
+ DockerEvent = class _DockerEvent {
696
+ static get metadata() {
697
+ return {
698
+ name: "docker",
699
+ displayName: "Docker container management",
700
+ description: "Start/stop Docker containers for the project",
701
+ category: "development",
702
+ requiresTmux: false,
703
+ dependencies: ["docker"]
704
+ };
705
+ }
706
+ static get validation() {
707
+ return {
708
+ validateConfig(config) {
709
+ if (typeof config === "boolean" || config === "true" || config === "false") {
710
+ return true;
711
+ }
712
+ if (typeof config === "string") {
713
+ return true;
714
+ }
715
+ if (typeof config === "object" && config !== null) {
716
+ const cfg = config;
717
+ if (cfg.compose_file !== void 0 && typeof cfg.compose_file !== "string") {
718
+ return "docker.compose_file must be a string";
719
+ }
720
+ if (cfg.services !== void 0) {
721
+ if (!Array.isArray(cfg.services)) {
722
+ return "docker.services must be an array";
723
+ }
724
+ for (const service of cfg.services) {
725
+ if (typeof service !== "string") {
726
+ return "docker.services must contain only strings";
727
+ }
728
+ }
729
+ }
730
+ return true;
731
+ }
732
+ return "docker config must be a boolean, string (compose file), or object";
733
+ }
734
+ };
735
+ }
736
+ static get configuration() {
737
+ return {
738
+ async configureInteractive() {
739
+ const composeFile = await input2({
740
+ message: "Enter docker-compose file path:",
741
+ default: "docker-compose.yml"
742
+ });
743
+ const servicesInput = await input2({
744
+ message: "Enter services to start (comma-separated, leave empty for all):",
745
+ default: ""
746
+ });
747
+ const services = servicesInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
748
+ if (composeFile === "docker-compose.yml" && services.length === 0) {
749
+ return { compose_file: "docker-compose.yml" };
750
+ }
751
+ if (services.length === 0) {
752
+ return composeFile;
753
+ }
754
+ return {
755
+ compose_file: composeFile,
756
+ services
757
+ };
758
+ },
759
+ getDefaultConfig() {
760
+ return { compose_file: "docker-compose.yml" };
761
+ }
762
+ };
763
+ }
764
+ static getDockerCommand(config) {
765
+ if (typeof config === "boolean" || config === void 0) {
766
+ return "docker-compose up -d";
767
+ }
768
+ if (typeof config === "string") {
769
+ return `docker-compose -f ${config} up -d`;
770
+ }
771
+ const composeFile = config.compose_file || "docker-compose.yml";
772
+ const services = config.services?.join(" ") || "";
773
+ return `docker-compose -f ${composeFile} up -d ${services}`.trim();
774
+ }
775
+ static get processing() {
776
+ return {
777
+ async processEvent(context) {
778
+ const { project, isShellMode, shellCommands } = context;
779
+ const dockerConfig = project.events.docker;
780
+ const dockerCommand = _DockerEvent.getDockerCommand(
781
+ dockerConfig
782
+ );
783
+ if (isShellMode) {
784
+ shellCommands.push(dockerCommand);
785
+ } else {
786
+ const [cmd, ...args] = dockerCommand.split(" ");
787
+ spawn6(cmd, args, {
788
+ cwd: project.path.path,
789
+ stdio: "inherit"
790
+ });
791
+ }
792
+ },
793
+ generateShellCommand(context) {
794
+ const dockerConfig = context.project.events.docker;
795
+ return [_DockerEvent.getDockerCommand(dockerConfig)];
796
+ }
797
+ };
798
+ }
799
+ static get tmux() {
800
+ return null;
801
+ }
802
+ static get help() {
803
+ return {
804
+ usage: 'docker: true | "compose-file.yml" | { compose_file: string, services?: string[] }',
805
+ description: "Start Docker containers for the project",
806
+ examples: [
807
+ { config: true, description: "Use default docker-compose.yml" },
808
+ { config: "docker-compose.dev.yml", description: "Use custom compose file" },
809
+ {
810
+ config: { compose_file: "docker-compose.yml", services: ["web", "db"] },
811
+ description: "Start specific services"
812
+ }
813
+ ]
814
+ };
815
+ }
796
816
  };
797
817
  }
798
- static get validation() {
799
- return {
800
- validateConfig(config) {
801
- if (typeof config === "boolean" || config === "true" || config === "false") {
802
- return true;
818
+ });
819
+
820
+ // src/events/extensions/npm.ts
821
+ var npm_exports = {};
822
+ __export(npm_exports, {
823
+ NpmEvent: () => NpmEvent,
824
+ default: () => npm_default
825
+ });
826
+ import { spawn as spawn7 } from "child_process";
827
+ import { input as input3, confirm as confirm2 } from "@inquirer/prompts";
828
+ var NpmEvent, npm_default;
829
+ var init_npm = __esm({
830
+ "src/events/extensions/npm.ts"() {
831
+ "use strict";
832
+ NpmEvent = class _NpmEvent {
833
+ static get metadata() {
834
+ return {
835
+ name: "npm",
836
+ displayName: "Run NPM command",
837
+ description: "Execute NPM scripts in project directory",
838
+ category: "development",
839
+ requiresTmux: true,
840
+ dependencies: ["npm"]
841
+ };
842
+ }
843
+ static get validation() {
844
+ return {
845
+ validateConfig(config) {
846
+ if (typeof config === "boolean" || config === "true" || config === "false") {
847
+ return true;
848
+ }
849
+ if (typeof config === "string") {
850
+ if (config.trim().length === 0) {
851
+ return "npm script name cannot be empty";
852
+ }
853
+ return true;
854
+ }
855
+ if (typeof config === "object" && config !== null) {
856
+ const cfg = config;
857
+ if (typeof cfg.command !== "string" || cfg.command.trim().length === 0) {
858
+ return "npm.command must be a non-empty string";
859
+ }
860
+ if (cfg.watch !== void 0 && typeof cfg.watch !== "boolean") {
861
+ return "npm.watch must be a boolean";
862
+ }
863
+ if (cfg.auto_restart !== void 0 && typeof cfg.auto_restart !== "boolean") {
864
+ return "npm.auto_restart must be a boolean";
865
+ }
866
+ return true;
867
+ }
868
+ return "npm config must be a boolean, string (script name), or object";
869
+ }
870
+ };
871
+ }
872
+ static get configuration() {
873
+ return {
874
+ async configureInteractive() {
875
+ const scriptName = await input3({
876
+ message: "Enter NPM script to run:",
877
+ default: "dev"
878
+ });
879
+ const useAdvanced = await confirm2({
880
+ message: "Configure advanced NPM options?",
881
+ default: false
882
+ });
883
+ if (!useAdvanced) {
884
+ return scriptName;
885
+ }
886
+ const watch = await confirm2({
887
+ message: "Enable watch mode?",
888
+ default: false
889
+ });
890
+ const autoRestart = await confirm2({
891
+ message: "Auto-restart on crash?",
892
+ default: false
893
+ });
894
+ if (!watch && !autoRestart) {
895
+ return scriptName;
896
+ }
897
+ return {
898
+ command: scriptName,
899
+ watch,
900
+ auto_restart: autoRestart
901
+ };
902
+ },
903
+ getDefaultConfig() {
904
+ return "dev";
905
+ }
906
+ };
907
+ }
908
+ static getNpmCommand(config) {
909
+ if (typeof config === "boolean" || config === void 0) {
910
+ return "npm run dev";
803
911
  }
804
- return "cwd config must be a boolean (true/false)";
912
+ if (typeof config === "string") {
913
+ return `npm run ${config}`;
914
+ }
915
+ return `npm run ${config.command}`;
916
+ }
917
+ static get processing() {
918
+ return {
919
+ async processEvent(context) {
920
+ const { project, isShellMode, shellCommands } = context;
921
+ const npmConfig = project.events.npm;
922
+ const npmCommand = _NpmEvent.getNpmCommand(npmConfig);
923
+ if (isShellMode) {
924
+ shellCommands.push(npmCommand);
925
+ } else {
926
+ const [cmd, ...args] = npmCommand.split(" ");
927
+ spawn7(cmd, args, {
928
+ cwd: project.path.path,
929
+ stdio: "inherit"
930
+ });
931
+ }
932
+ },
933
+ generateShellCommand(context) {
934
+ const npmConfig = context.project.events.npm;
935
+ return [_NpmEvent.getNpmCommand(npmConfig)];
936
+ }
937
+ };
938
+ }
939
+ static get tmux() {
940
+ return {
941
+ getLayoutPriority() {
942
+ return 50;
943
+ },
944
+ contributeToLayout(enabledCommands) {
945
+ if (enabledCommands.includes("claude")) {
946
+ return "three-pane";
947
+ }
948
+ return "two-pane-npm";
949
+ }
950
+ };
951
+ }
952
+ static get help() {
953
+ return {
954
+ usage: 'npm: true | "script" | { command: string, watch?: boolean, auto_restart?: boolean }',
955
+ description: "Run an NPM script in the project directory",
956
+ examples: [
957
+ { config: true, description: "Run npm run dev" },
958
+ { config: "test", description: "Run npm run test" },
959
+ { config: { command: "dev", watch: true }, description: "Run dev with watch mode" }
960
+ ]
961
+ };
805
962
  }
806
963
  };
964
+ npm_default = NpmEvent;
807
965
  }
808
- static get configuration() {
809
- return {
810
- async configureInteractive() {
811
- return true;
812
- },
813
- getDefaultConfig() {
814
- return true;
966
+ });
967
+
968
+ // src/events/registry.ts
969
+ var ALL_EVENTS, EventRegistryClass, EventRegistry;
970
+ var init_registry = __esm({
971
+ "src/events/registry.ts"() {
972
+ "use strict";
973
+ init_cwd();
974
+ init_ide();
975
+ init_web();
976
+ init_claude();
977
+ init_docker();
978
+ init_npm();
979
+ ALL_EVENTS = [CwdEvent, IdeEvent, WebEvent, ClaudeEvent, DockerEvent, NpmEvent];
980
+ EventRegistryClass = class {
981
+ _events = /* @__PURE__ */ new Map();
982
+ _initialized = false;
983
+ /**
984
+ * Initialize the registry by registering all events
985
+ */
986
+ async initialize() {
987
+ if (this._initialized) return;
988
+ this.registerEvents();
989
+ this._initialized = true;
990
+ }
991
+ /**
992
+ * Register all event classes
993
+ */
994
+ registerEvents() {
995
+ for (const EventClass of ALL_EVENTS) {
996
+ if (this.isValidEvent(EventClass)) {
997
+ const metadata = EventClass.metadata;
998
+ this._events.set(metadata.name, EventClass);
999
+ }
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Validate if a class is a proper event
1004
+ */
1005
+ isValidEvent(EventClass) {
1006
+ try {
1007
+ if (typeof EventClass !== "function") return false;
1008
+ const metadata = EventClass.metadata;
1009
+ return metadata !== void 0 && typeof metadata.name === "string" && typeof metadata.displayName === "string" && typeof EventClass.validation === "object" && typeof EventClass.configuration === "object" && typeof EventClass.processing === "object";
1010
+ } catch {
1011
+ return false;
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Get all valid event names from registered events
1016
+ */
1017
+ getValidEventNames() {
1018
+ this.ensureInitialized();
1019
+ return Array.from(this._events.keys());
1020
+ }
1021
+ /**
1022
+ * Get event by name
1023
+ */
1024
+ getEventByName(name) {
1025
+ this.ensureInitialized();
1026
+ return this._events.get(name) ?? null;
1027
+ }
1028
+ /**
1029
+ * Get all events for management UI
1030
+ */
1031
+ getEventsForManageUI() {
1032
+ this.ensureInitialized();
1033
+ const events = [];
1034
+ for (const [name, EventClass] of this._events) {
1035
+ const metadata = EventClass.metadata;
1036
+ events.push({
1037
+ name: metadata.displayName,
1038
+ value: name,
1039
+ description: metadata.description
1040
+ });
1041
+ }
1042
+ return events.sort((a, b) => a.name.localeCompare(b.name));
815
1043
  }
816
- };
817
- }
818
- static get processing() {
819
- return {
820
- async processEvent(context) {
821
- const { project, isShellMode, shellCommands } = context;
822
- const projectPath = project.path.path;
823
- if (isShellMode) {
824
- shellCommands.push(`cd "${projectPath}"`);
825
- } else {
826
- const shell = process.env.SHELL || "/bin/bash";
827
- spawn2(shell, [], {
828
- cwd: projectPath,
829
- stdio: "inherit"
1044
+ /**
1045
+ * Get events that support tmux integration
1046
+ */
1047
+ getTmuxEnabledEvents() {
1048
+ this.ensureInitialized();
1049
+ const tmuxEvents = [];
1050
+ for (const [name, EventClass] of this._events) {
1051
+ const tmux = EventClass.tmux;
1052
+ if (tmux) {
1053
+ tmuxEvents.push({
1054
+ name,
1055
+ event: EventClass,
1056
+ priority: tmux.getLayoutPriority ? tmux.getLayoutPriority() : 0
1057
+ });
1058
+ }
1059
+ }
1060
+ return tmuxEvents.sort((a, b) => b.priority - a.priority);
1061
+ }
1062
+ /**
1063
+ * Get all available events with their metadata
1064
+ */
1065
+ getAllEvents() {
1066
+ this.ensureInitialized();
1067
+ const events = [];
1068
+ for (const [name, EventClass] of this._events) {
1069
+ const typedClass = EventClass;
1070
+ events.push({
1071
+ name,
1072
+ metadata: typedClass.metadata,
1073
+ hasValidation: !!typedClass.validation,
1074
+ hasConfiguration: !!typedClass.configuration,
1075
+ hasProcessing: !!typedClass.processing,
1076
+ hasTmux: !!typedClass.tmux,
1077
+ hasHelp: !!typedClass.help
830
1078
  });
831
1079
  }
832
- },
833
- generateShellCommand(context) {
834
- const projectPath = context.project.path.path;
835
- return [`cd "${projectPath}"`];
1080
+ return events;
1081
+ }
1082
+ /**
1083
+ * Ensure registry is initialized
1084
+ */
1085
+ ensureInitialized() {
1086
+ if (!this._initialized) {
1087
+ throw new Error("EventRegistry must be initialized before use. Call initialize() first.");
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Clear the registry (useful for testing)
1092
+ */
1093
+ clear() {
1094
+ this._events.clear();
1095
+ this._initialized = false;
836
1096
  }
837
1097
  };
1098
+ EventRegistry = new EventRegistryClass();
838
1099
  }
839
- static get tmux() {
840
- return null;
841
- }
842
- static get help() {
843
- return {
844
- usage: "cwd: true | false",
845
- description: "Change the current working directory to the project path",
846
- examples: [
847
- { config: true, description: "Enable directory change" },
848
- { config: false, description: "Disable directory change" }
849
- ]
850
- };
851
- }
852
- };
1100
+ });
853
1101
 
854
- // src/events/core/ide.ts
855
- import { spawn as spawn3 } from "child_process";
856
- var IdeEvent = class {
857
- static get metadata() {
858
- return {
859
- name: "ide",
860
- displayName: "Open in IDE",
861
- description: "Open project in configured IDE/editor",
862
- category: "core",
863
- requiresTmux: false,
864
- dependencies: []
865
- };
1102
+ // src/commands/interactive.ts
1103
+ var interactive_exports = {};
1104
+ __export(interactive_exports, {
1105
+ runInteractive: () => runInteractive
1106
+ });
1107
+ import { select, input as input4, checkbox, confirm as confirm3 } from "@inquirer/prompts";
1108
+ import File3 from "phylo";
1109
+ import deepAssign2 from "deep-assign";
1110
+ async function runInteractive(ctx) {
1111
+ const { config, log, environment, suggestedName } = ctx;
1112
+ showLogo(config);
1113
+ log.log("");
1114
+ const defaultName = suggestedName ?? (environment.$isProjectEnvironment ? environment.project.name : File3.cwd().name);
1115
+ const fromUser = !!suggestedName;
1116
+ await startInteractive(defaultName, fromUser, ctx);
1117
+ }
1118
+ function showLogo(config) {
1119
+ const version = config.get("pkg")?.version ?? "unknown";
1120
+ console.log(
1121
+ ` 8\x1B[2m${" ".repeat(Math.max(15 - version.length - 1, 1)) + "v" + version}\x1B[22m
1122
+ Yb db dP .d8b. 8d8b 8.dP \x1B[92m.d8b. 8d8b.\x1B[0m
1123
+ YbdPYbdP 8' .8 8P 88b \x1B[92m8' .8 8P Y8\x1B[0m
1124
+ YP YP \`Y8P' 8 8 Yb \x1B[92m\`Y8P' 8 8\x1B[0m`
1125
+ );
1126
+ }
1127
+ async function startInteractive(defaultName, fromUser, ctx, showMain = false) {
1128
+ const { log, environment } = ctx;
1129
+ log.debug(`Name '${defaultName}' was${fromUser ? "" : " not"} provided by the user`);
1130
+ const question = getFirstQuestion(defaultName, fromUser, environment, showMain);
1131
+ const action = await select(question);
1132
+ switch (action) {
1133
+ case "exit":
1134
+ return;
1135
+ case "more":
1136
+ await startInteractive(defaultName, fromUser, ctx, true);
1137
+ return;
1138
+ case "init-project":
1139
+ await initProject(defaultName, fromUser, ctx);
1140
+ return;
1141
+ case "init-branch":
1142
+ await initBranch(defaultName, ctx);
1143
+ return;
1144
+ case "switch-project":
1145
+ await switchProject(ctx);
1146
+ return;
1147
+ case "switch-branch":
1148
+ await switchBranch(defaultName, ctx);
1149
+ return;
1150
+ case "manage-projects":
1151
+ await manageProjects(ctx);
1152
+ return;
1153
+ case "manage-branches":
1154
+ await manageBranches(defaultName, ctx);
1155
+ return;
866
1156
  }
867
- static get validation() {
1157
+ }
1158
+ function getFirstQuestion(defaultName, fromUser, environment, showMain) {
1159
+ if (!showMain && environment.$isProjectEnvironment && !fromUser) {
868
1160
  return {
869
- validateConfig(config) {
870
- if (typeof config === "boolean" || config === "true" || config === "false") {
871
- return true;
872
- }
873
- return "ide config must be a boolean (true/false)";
874
- }
1161
+ message: environment.project.name,
1162
+ choices: [
1163
+ { name: "Start a branch", value: "init-branch" },
1164
+ { name: "Switch branch", value: "switch-branch" },
1165
+ { name: "Manage branches", value: "manage-branches" },
1166
+ { name: "---", value: "" },
1167
+ { name: "More...", value: "more" },
1168
+ { name: "Exit", value: "exit" }
1169
+ ].filter((c) => c.value !== "")
875
1170
  };
876
1171
  }
877
- static get configuration() {
878
- return {
879
- async configureInteractive() {
880
- return true;
881
- },
882
- getDefaultConfig() {
1172
+ return {
1173
+ message: "What do you want to do?",
1174
+ choices: [
1175
+ { name: "Start a new project", value: "init-project" },
1176
+ { name: "Open an existing project", value: "switch-project" },
1177
+ { name: "Manage projects", value: "manage-projects" },
1178
+ { name: "---", value: "" },
1179
+ { name: "Exit", value: "exit" }
1180
+ ].filter((c) => c.value !== "")
1181
+ };
1182
+ }
1183
+ async function initProject(defaultName, fromUser, ctx) {
1184
+ const { config, log } = ctx;
1185
+ const defaults = config.getDefaults();
1186
+ const projects = config.getProjects();
1187
+ let name;
1188
+ if (fromUser) {
1189
+ name = defaultName;
1190
+ log.log(`Project name: ${name}`);
1191
+ } else {
1192
+ name = await input4({
1193
+ message: "What is the name of the project?",
1194
+ default: defaultName,
1195
+ validate: (value) => {
1196
+ if (value in projects) return "Project already exists.";
1197
+ if (/\w+#\w+/.test(value)) {
1198
+ const projectName = value.substring(0, value.indexOf("#"));
1199
+ if (!(projectName in projects)) {
1200
+ return `Project '${projectName}' does not exist. Please create it before starting a branch.`;
1201
+ }
1202
+ }
883
1203
  return true;
884
1204
  }
885
- };
1205
+ });
886
1206
  }
887
- static get processing() {
888
- return {
889
- async processEvent(context) {
890
- const { project, isShellMode, shellCommands } = context;
891
- const projectPath = project.path.path;
892
- const ide = project.ide || "code";
893
- if (isShellMode) {
894
- shellCommands.push(`${ide} "${projectPath}" &`);
895
- } else {
896
- spawn3(ide, [projectPath], {
897
- detached: true,
898
- stdio: "ignore"
899
- }).unref();
900
- }
901
- },
902
- generateShellCommand(context) {
903
- const projectPath = context.project.path.path;
904
- const ide = context.project.ide || "code";
905
- return [`${ide} "${projectPath}" &`];
1207
+ const isBranch = /\w+#\w+/.test(name);
1208
+ let basePath;
1209
+ if (isBranch) {
1210
+ const projectName = name.substring(0, name.indexOf("#"));
1211
+ basePath = defaults?.base ? File3.from(defaults.base).join(projects[projectName].path).absolutePath() : projects[projectName].path;
1212
+ log.log(`Project path: ${basePath}`);
1213
+ } else {
1214
+ const pathAnswer = await input4({
1215
+ message: "What is the path to the project?",
1216
+ default: defaults?.base ? File3.from(defaults.base).join(name).path : name
1217
+ });
1218
+ let answerFile = File3.from(pathAnswer);
1219
+ const defaultBase = defaults?.base ? File3.from(defaults.base) : File3.cwd();
1220
+ if (!answerFile.isAbsolute()) {
1221
+ answerFile = defaultBase.join(answerFile.path);
1222
+ }
1223
+ try {
1224
+ const canonical = answerFile.canonicalize();
1225
+ if (canonical) {
1226
+ answerFile = canonical;
1227
+ } else {
1228
+ answerFile = answerFile.absolutify();
906
1229
  }
907
- };
1230
+ } catch {
1231
+ answerFile = answerFile.absolutify();
1232
+ }
1233
+ basePath = answerFile.relativize(defaultBase.path).path;
908
1234
  }
909
- static get tmux() {
910
- return null;
1235
+ const ide = await select({
1236
+ message: "What is the IDE?",
1237
+ choices: IDE_CHOICES
1238
+ });
1239
+ const selectedEvents = await checkbox({
1240
+ message: "Which events should take place when opening?",
1241
+ choices: [
1242
+ { name: "Change terminal cwd to project path", value: "cwd", checked: true },
1243
+ { name: "Open project in IDE", value: "ide", checked: true }
1244
+ ]
1245
+ });
1246
+ const events = {
1247
+ cwd: selectedEvents.includes("cwd"),
1248
+ ide: selectedEvents.includes("ide")
1249
+ };
1250
+ const projectConfig = {
1251
+ path: basePath,
1252
+ ide,
1253
+ events
1254
+ };
1255
+ projects[name] = projectConfig;
1256
+ config.set("projects", projects);
1257
+ log.info("Your project has been initialized.");
1258
+ log.info(`Use 'workon ${name}' to start working!`);
1259
+ }
1260
+ async function initBranch(defaultName, ctx) {
1261
+ const { config, log } = ctx;
1262
+ const projects = config.getProjects();
1263
+ const branch = await input4({
1264
+ message: "What is the name of the branch?",
1265
+ validate: (value) => {
1266
+ if (/\w+#\w+/.test(value)) return `Branch name can't contain the "#" sign`;
1267
+ if (`${defaultName}#${value}` in projects) return "Branch already exists.";
1268
+ return true;
1269
+ }
1270
+ });
1271
+ const branchName = `${defaultName}#${branch}`;
1272
+ const baseProject = projects[defaultName];
1273
+ const branchConfig = deepAssign2({}, baseProject, { branch });
1274
+ delete branchConfig.name;
1275
+ projects[branchName] = branchConfig;
1276
+ config.set("projects", projects);
1277
+ log.info("Your branch configuration has been initialized.");
1278
+ log.info(`Use 'workon ${branchName}' to start working!`);
1279
+ }
1280
+ async function switchProject(ctx) {
1281
+ const { config, log } = ctx;
1282
+ const projects = config.getProjects();
1283
+ const baseProjects = Object.keys(projects).filter((name) => !name.includes("#"));
1284
+ if (baseProjects.length === 0) {
1285
+ log.info('No projects configured yet. Use "Start a new project" to create one.');
1286
+ return;
911
1287
  }
912
- static get help() {
913
- return {
914
- usage: "ide: true | false",
915
- description: "Open the project in the configured IDE",
916
- examples: [
917
- { config: true, description: "Enable IDE opening" },
918
- { config: false, description: "Disable IDE opening" }
919
- ]
920
- };
1288
+ const projectName = await select({
1289
+ message: "Select a project to open:",
1290
+ choices: baseProjects.map((name) => ({
1291
+ name: `${name} (${projects[name].path})`,
1292
+ value: name
1293
+ }))
1294
+ });
1295
+ await openProject(projectName, ctx);
1296
+ }
1297
+ async function switchBranch(projectName, ctx) {
1298
+ const { config, log } = ctx;
1299
+ const projects = config.getProjects();
1300
+ const branchPrefix = `${projectName}#`;
1301
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1302
+ if (branches.length === 0) {
1303
+ log.info(`No branch configurations found for '${projectName}'.`);
1304
+ log.info('Use "Start a branch" to create one.');
1305
+ return;
921
1306
  }
922
- };
923
-
924
- // src/events/core/web.ts
925
- import { spawn as spawn4 } from "child_process";
926
- import { platform } from "os";
927
- var WebEvent = class _WebEvent {
928
- static get metadata() {
929
- return {
930
- name: "web",
931
- displayName: "Open homepage in browser",
932
- description: "Open project homepage in web browser",
933
- category: "core",
934
- requiresTmux: false,
935
- dependencies: []
936
- };
1307
+ const branchConfig = await select({
1308
+ message: "Select a branch configuration:",
1309
+ choices: branches.map((name) => ({
1310
+ name: name.substring(branchPrefix.length),
1311
+ value: name
1312
+ }))
1313
+ });
1314
+ await openProject(branchConfig, ctx);
1315
+ }
1316
+ async function manageProjects(ctx) {
1317
+ const { config } = ctx;
1318
+ await EventRegistry.initialize();
1319
+ const projects = config.getProjects();
1320
+ const hasProjects = Object.keys(projects).length > 0;
1321
+ const choices = [
1322
+ { name: "Create new project", value: "create" },
1323
+ ...hasProjects ? [
1324
+ { name: "Edit project", value: "edit" },
1325
+ { name: "Delete project", value: "delete" },
1326
+ { name: "List projects", value: "list" }
1327
+ ] : [],
1328
+ { name: "Back", value: "back" }
1329
+ ];
1330
+ const action = await select({
1331
+ message: "Manage projects:",
1332
+ choices
1333
+ });
1334
+ switch (action) {
1335
+ case "create":
1336
+ await createProjectManage(ctx);
1337
+ break;
1338
+ case "edit":
1339
+ await editProjectManage(ctx);
1340
+ break;
1341
+ case "delete":
1342
+ await deleteProjectManage(ctx);
1343
+ break;
1344
+ case "list":
1345
+ listProjectsManage(ctx);
1346
+ break;
1347
+ case "back":
1348
+ return;
937
1349
  }
938
- static get validation() {
939
- return {
940
- validateConfig(config) {
941
- if (typeof config === "boolean" || config === "true" || config === "false") {
942
- return true;
943
- }
944
- return "web config must be a boolean (true/false)";
945
- }
946
- };
1350
+ await manageProjects(ctx);
1351
+ }
1352
+ async function manageBranches(projectName, ctx) {
1353
+ const { config } = ctx;
1354
+ const projects = config.getProjects();
1355
+ const branchPrefix = `${projectName}#`;
1356
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1357
+ const choices = [
1358
+ { name: "Create new branch config", value: "create" },
1359
+ ...branches.length > 0 ? [
1360
+ { name: "Edit branch config", value: "edit" },
1361
+ { name: "Delete branch config", value: "delete" },
1362
+ { name: "List branch configs", value: "list" }
1363
+ ] : [],
1364
+ { name: "Back", value: "back" }
1365
+ ];
1366
+ const action = await select({
1367
+ message: `Manage branches for '${projectName}':`,
1368
+ choices
1369
+ });
1370
+ switch (action) {
1371
+ case "create":
1372
+ await initBranch(projectName, ctx);
1373
+ break;
1374
+ case "edit":
1375
+ await editBranchManage(projectName, ctx);
1376
+ break;
1377
+ case "delete":
1378
+ await deleteBranchManage(projectName, ctx);
1379
+ break;
1380
+ case "list":
1381
+ listBranchesManage(projectName, ctx);
1382
+ break;
1383
+ case "back":
1384
+ return;
947
1385
  }
948
- static get configuration() {
949
- return {
950
- async configureInteractive() {
951
- return true;
952
- },
953
- getDefaultConfig() {
954
- return true;
955
- }
956
- };
1386
+ await manageBranches(projectName, ctx);
1387
+ }
1388
+ async function openProject(projectName, ctx) {
1389
+ const { config, log } = ctx;
1390
+ const projects = config.getProjects();
1391
+ if (!(projectName in projects)) {
1392
+ log.error(`Project '${projectName}' not found.`);
1393
+ return;
957
1394
  }
958
- static getOpenCommand() {
959
- const os = platform();
960
- switch (os) {
961
- case "darwin":
962
- return "open";
963
- case "win32":
964
- return "start";
965
- default:
966
- return "xdg-open";
1395
+ await EventRegistry.initialize();
1396
+ const projectConfig = projects[projectName];
1397
+ const projectCfg = { ...projectConfig, name: projectName };
1398
+ const projectEnv = ProjectEnvironment.load(projectCfg, config.getDefaults());
1399
+ log.info(`Opening project '${projectName}'...`);
1400
+ const events = Object.keys(projectConfig.events).filter(
1401
+ (e) => projectConfig.events[e]
1402
+ );
1403
+ for (const event of events) {
1404
+ const eventHandler = EventRegistry.getEventByName(event);
1405
+ if (eventHandler && eventHandler.processing) {
1406
+ await eventHandler.processing.processEvent({
1407
+ project: projectEnv.project,
1408
+ isShellMode: false,
1409
+ shellCommands: []
1410
+ });
967
1411
  }
968
1412
  }
969
- static get processing() {
970
- return {
971
- async processEvent(context) {
972
- const { project, isShellMode, shellCommands } = context;
973
- const homepage = project.homepage;
974
- if (!homepage) {
975
- console.warn("No homepage configured for project");
976
- return;
977
- }
978
- const openCmd = _WebEvent.getOpenCommand();
979
- if (isShellMode) {
980
- shellCommands.push(`${openCmd} "${homepage}" &`);
981
- } else {
982
- spawn4(openCmd, [homepage], {
983
- detached: true,
984
- stdio: "ignore"
985
- }).unref();
986
- }
987
- },
988
- generateShellCommand(context) {
989
- const homepage = context.project.homepage;
990
- if (!homepage) return [];
991
- const openCmd = _WebEvent.getOpenCommand();
992
- return [`${openCmd} "${homepage}" &`];
1413
+ }
1414
+ async function createProjectManage(ctx) {
1415
+ const { config, log } = ctx;
1416
+ const defaults = config.getDefaults();
1417
+ const projects = config.getProjects();
1418
+ const name = await input4({
1419
+ message: "Project name:",
1420
+ validate: (value) => {
1421
+ if (!value.trim()) return "Name is required";
1422
+ if (!/^[\w-]+$/.test(value))
1423
+ return "Name can only contain letters, numbers, underscores, and hyphens";
1424
+ if (value in projects) return "Project already exists";
1425
+ return true;
1426
+ }
1427
+ });
1428
+ const defaultPath = defaults?.base ? File3.from(defaults.base).join(name).path : name;
1429
+ const pathInput = await input4({
1430
+ message: "Project path:",
1431
+ default: defaultPath
1432
+ });
1433
+ let relativePath = pathInput;
1434
+ if (defaults?.base) {
1435
+ const baseDir = File3.from(defaults.base);
1436
+ const pathFile = File3.from(pathInput);
1437
+ try {
1438
+ if (pathFile.isAbsolute()) {
1439
+ relativePath = pathFile.relativize(baseDir.path).path;
993
1440
  }
994
- };
1441
+ } catch {
1442
+ relativePath = pathInput;
1443
+ }
995
1444
  }
996
- static get tmux() {
997
- return null;
1445
+ const ide = await select({
1446
+ message: "Select IDE:",
1447
+ choices: IDE_CHOICES
1448
+ });
1449
+ const availableEvents = EventRegistry.getEventsForManageUI();
1450
+ const selectedEvents = await checkbox({
1451
+ message: "Select events to enable:",
1452
+ choices: availableEvents.map((e) => ({
1453
+ name: `${e.name} - ${e.description}`,
1454
+ value: e.value,
1455
+ checked: e.value === "cwd" || e.value === "ide"
1456
+ }))
1457
+ });
1458
+ const events = {};
1459
+ for (const eventName of selectedEvents) {
1460
+ const eventHandler = EventRegistry.getEventByName(eventName);
1461
+ if (eventHandler) {
1462
+ const eventConfig = await eventHandler.configuration.configureInteractive();
1463
+ events[eventName] = eventConfig;
1464
+ }
998
1465
  }
999
- static get help() {
1000
- return {
1001
- usage: "web: true | false",
1002
- description: "Open the project homepage in the default browser",
1003
- examples: [
1004
- { config: true, description: "Enable browser opening" },
1005
- { config: false, description: "Disable browser opening" }
1006
- ]
1007
- };
1466
+ const projectConfig = {
1467
+ path: relativePath,
1468
+ ide,
1469
+ events
1470
+ };
1471
+ const confirmed = await confirm3({
1472
+ message: "Save this project?",
1473
+ default: true
1474
+ });
1475
+ if (confirmed) {
1476
+ config.setProject(name, projectConfig);
1477
+ log.info(`Project '${name}' created successfully!`);
1008
1478
  }
1009
- };
1010
-
1011
- // src/events/extensions/claude.ts
1012
- import { spawn as spawn5 } from "child_process";
1013
- import { input, confirm } from "@inquirer/prompts";
1014
- var ClaudeEvent = class _ClaudeEvent {
1015
- static get metadata() {
1016
- return {
1017
- name: "claude",
1018
- displayName: "Launch Claude Code",
1019
- description: "Launch Claude Code with optional flags and configuration",
1020
- category: "development",
1021
- requiresTmux: true,
1022
- dependencies: ["claude"]
1023
- };
1479
+ }
1480
+ async function editProjectManage(ctx) {
1481
+ const { config, log } = ctx;
1482
+ const projects = config.getProjects();
1483
+ const defaults = config.getDefaults();
1484
+ const baseProjects = Object.keys(projects).filter((name2) => !name2.includes("#"));
1485
+ if (baseProjects.length === 0) {
1486
+ log.info("No projects to edit.");
1487
+ return;
1024
1488
  }
1025
- static get validation() {
1026
- return {
1027
- validateConfig(config) {
1028
- if (typeof config === "boolean" || config === "true" || config === "false") {
1029
- return true;
1030
- }
1031
- if (typeof config === "object" && config !== null) {
1032
- const cfg = config;
1033
- if (cfg.flags !== void 0) {
1034
- if (!Array.isArray(cfg.flags)) {
1035
- return "claude.flags must be an array of strings";
1036
- }
1037
- for (const flag of cfg.flags) {
1038
- if (typeof flag !== "string") {
1039
- return "claude.flags must contain only strings";
1040
- }
1041
- if (!flag.startsWith("-")) {
1042
- return `Invalid flag "${flag}": flags must start with - or --`;
1043
- }
1044
- }
1045
- }
1046
- if (cfg.split_terminal !== void 0 && typeof cfg.split_terminal !== "boolean") {
1047
- return "claude.split_terminal must be a boolean";
1048
- }
1049
- return true;
1050
- }
1051
- return "claude config must be a boolean or object with flags/split_terminal";
1489
+ const name = await select({
1490
+ message: "Select project to edit:",
1491
+ choices: baseProjects.map((n) => ({ name: n, value: n }))
1492
+ });
1493
+ const project = projects[name];
1494
+ const pathInput = await input4({
1495
+ message: "Project path:",
1496
+ default: project.path
1497
+ });
1498
+ let relativePath = pathInput;
1499
+ if (defaults?.base) {
1500
+ const baseDir = File3.from(defaults.base);
1501
+ const pathFile = File3.from(pathInput);
1502
+ try {
1503
+ if (pathFile.isAbsolute()) {
1504
+ relativePath = pathFile.relativize(baseDir.path).path;
1052
1505
  }
1053
- };
1506
+ } catch {
1507
+ relativePath = pathInput;
1508
+ }
1054
1509
  }
1055
- static get configuration() {
1056
- return {
1057
- async configureInteractive() {
1058
- const useAdvanced = await confirm({
1059
- message: "Configure advanced Claude options?",
1060
- default: false
1061
- });
1062
- if (!useAdvanced) {
1063
- return true;
1064
- }
1065
- const flagsInput = await input({
1066
- message: "Enter Claude flags (comma-separated, e.g., --resume, --debug):",
1067
- default: ""
1068
- });
1069
- const flags = flagsInput.split(",").map((f) => f.trim()).filter((f) => f.length > 0 && f.startsWith("-"));
1070
- const splitTerminal = await confirm({
1071
- message: "Use split terminal layout (Claude + shell)?",
1072
- default: true
1073
- });
1074
- if (flags.length === 0 && !splitTerminal) {
1075
- return true;
1510
+ const ide = await select({
1511
+ message: "Select IDE:",
1512
+ choices: IDE_CHOICES,
1513
+ default: project.ide || "vscode"
1514
+ });
1515
+ const keepEvents = await confirm3({
1516
+ message: "Keep existing event configuration?",
1517
+ default: true
1518
+ });
1519
+ let events = project.events;
1520
+ if (!keepEvents) {
1521
+ const availableEvents = EventRegistry.getEventsForManageUI();
1522
+ const currentEvents = Object.keys(project.events);
1523
+ const selectedEvents = await checkbox({
1524
+ message: "Select events to enable:",
1525
+ choices: availableEvents.map((e) => ({
1526
+ name: `${e.name} - ${e.description}`,
1527
+ value: e.value,
1528
+ checked: currentEvents.includes(e.value)
1529
+ }))
1530
+ });
1531
+ events = {};
1532
+ for (const eventName of selectedEvents) {
1533
+ if (project.events[eventName]) {
1534
+ events[eventName] = project.events[eventName];
1535
+ } else {
1536
+ const eventHandler = EventRegistry.getEventByName(eventName);
1537
+ if (eventHandler) {
1538
+ const eventConfig = await eventHandler.configuration.configureInteractive();
1539
+ events[eventName] = eventConfig;
1076
1540
  }
1077
- const config = {};
1078
- if (flags.length > 0) config.flags = flags;
1079
- if (splitTerminal) config.split_terminal = splitTerminal;
1080
- return config;
1081
- },
1082
- getDefaultConfig() {
1083
- return true;
1084
1541
  }
1085
- };
1086
- }
1087
- static getClaudeCommand(config) {
1088
- if (typeof config === "boolean" || config === void 0) {
1089
- return "claude";
1090
1542
  }
1091
- const flags = config.flags || [];
1092
- return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
1093
1543
  }
1094
- static get processing() {
1095
- return {
1096
- async processEvent(context) {
1097
- const { project, isShellMode, shellCommands } = context;
1098
- const claudeConfig = project.events.claude;
1099
- const claudeCommand = _ClaudeEvent.getClaudeCommand(claudeConfig);
1100
- if (isShellMode) {
1101
- shellCommands.push(claudeCommand);
1102
- } else {
1103
- const args = claudeCommand.split(" ").slice(1);
1104
- spawn5("claude", args, {
1105
- cwd: project.path.path,
1106
- stdio: "inherit"
1107
- });
1108
- }
1109
- },
1110
- generateShellCommand(context) {
1111
- const claudeConfig = context.project.events.claude;
1112
- return [_ClaudeEvent.getClaudeCommand(claudeConfig)];
1113
- }
1114
- };
1544
+ const updatedConfig = {
1545
+ path: relativePath,
1546
+ ide,
1547
+ events
1548
+ };
1549
+ const confirmed = await confirm3({
1550
+ message: "Save changes?",
1551
+ default: true
1552
+ });
1553
+ if (confirmed) {
1554
+ config.setProject(name, updatedConfig);
1555
+ log.info(`Project '${name}' updated successfully!`);
1115
1556
  }
1116
- static get tmux() {
1117
- return {
1118
- getLayoutPriority() {
1119
- return 100;
1120
- },
1121
- contributeToLayout(enabledCommands) {
1122
- if (enabledCommands.includes("npm")) {
1123
- return "three-pane";
1124
- }
1125
- return "split";
1557
+ }
1558
+ async function deleteProjectManage(ctx) {
1559
+ const { config, log } = ctx;
1560
+ const projects = config.getProjects();
1561
+ const baseProjects = Object.keys(projects).filter((name2) => !name2.includes("#"));
1562
+ if (baseProjects.length === 0) {
1563
+ log.info("No projects to delete.");
1564
+ return;
1565
+ }
1566
+ const name = await select({
1567
+ message: "Select project to delete:",
1568
+ choices: baseProjects.map((n) => ({ name: n, value: n }))
1569
+ });
1570
+ const branchPrefix = `${name}#`;
1571
+ const branches = Object.keys(projects).filter((n) => n.startsWith(branchPrefix));
1572
+ if (branches.length > 0) {
1573
+ log.warn(`This project has ${branches.length} branch configuration(s).`);
1574
+ const deleteAll = await confirm3({
1575
+ message: "Delete all branch configurations as well?",
1576
+ default: false
1577
+ });
1578
+ if (deleteAll) {
1579
+ for (const branch of branches) {
1580
+ config.deleteProject(branch);
1126
1581
  }
1127
- };
1582
+ }
1128
1583
  }
1129
- static get help() {
1130
- return {
1131
- usage: "claude: true | { flags: string[], split_terminal: boolean }",
1132
- description: "Launch Claude Code in the project directory",
1133
- examples: [
1134
- { config: true, description: "Launch Claude with defaults" },
1135
- { config: { flags: ["--resume"] }, description: "Resume previous session" },
1136
- {
1137
- config: { flags: ["--model", "opus"], split_terminal: true },
1138
- description: "Use Opus model with split terminal"
1139
- }
1140
- ]
1141
- };
1584
+ const confirmed = await confirm3({
1585
+ message: `Are you sure you want to delete '${name}'?`,
1586
+ default: false
1587
+ });
1588
+ if (confirmed) {
1589
+ config.deleteProject(name);
1590
+ log.info(`Project '${name}' deleted.`);
1142
1591
  }
1143
- };
1144
-
1145
- // src/events/extensions/docker.ts
1146
- import { spawn as spawn6 } from "child_process";
1147
- import { input as input2 } from "@inquirer/prompts";
1148
- var DockerEvent = class _DockerEvent {
1149
- static get metadata() {
1150
- return {
1151
- name: "docker",
1152
- displayName: "Docker container management",
1153
- description: "Start/stop Docker containers for the project",
1154
- category: "development",
1155
- requiresTmux: false,
1156
- dependencies: ["docker"]
1157
- };
1592
+ }
1593
+ function listProjectsManage(ctx) {
1594
+ const { config } = ctx;
1595
+ const projects = config.getProjects();
1596
+ const defaults = config.getDefaults();
1597
+ console.log("\nConfigured projects:\n");
1598
+ const baseProjects = Object.keys(projects).filter((name) => !name.includes("#"));
1599
+ for (const name of baseProjects) {
1600
+ const project = projects[name];
1601
+ const fullPath = defaults?.base ? File3.from(defaults.base).join(project.path).path : project.path;
1602
+ console.log(` ${name}`);
1603
+ console.log(` Path: ${fullPath}`);
1604
+ console.log(` IDE: ${project.ide || "not set"}`);
1605
+ console.log(` Events: ${Object.keys(project.events).join(", ") || "none"}`);
1606
+ const branchPrefix = `${name}#`;
1607
+ const branches = Object.keys(projects).filter((n) => n.startsWith(branchPrefix));
1608
+ if (branches.length > 0) {
1609
+ console.log(` Branches: ${branches.length}`);
1610
+ }
1611
+ console.log();
1158
1612
  }
1159
- static get validation() {
1160
- return {
1161
- validateConfig(config) {
1162
- if (typeof config === "boolean" || config === "true" || config === "false") {
1163
- return true;
1164
- }
1165
- if (typeof config === "string") {
1166
- return true;
1167
- }
1168
- if (typeof config === "object" && config !== null) {
1169
- const cfg = config;
1170
- if (cfg.compose_file !== void 0 && typeof cfg.compose_file !== "string") {
1171
- return "docker.compose_file must be a string";
1172
- }
1173
- if (cfg.services !== void 0) {
1174
- if (!Array.isArray(cfg.services)) {
1175
- return "docker.services must be an array";
1176
- }
1177
- for (const service of cfg.services) {
1178
- if (typeof service !== "string") {
1179
- return "docker.services must contain only strings";
1180
- }
1181
- }
1182
- }
1183
- return true;
1184
- }
1185
- return "docker config must be a boolean, string (compose file), or object";
1186
- }
1187
- };
1613
+ }
1614
+ async function editBranchManage(projectName, ctx) {
1615
+ const { config, log } = ctx;
1616
+ const projects = config.getProjects();
1617
+ const branchPrefix = `${projectName}#`;
1618
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1619
+ if (branches.length === 0) {
1620
+ log.info("No branch configurations to edit.");
1621
+ return;
1188
1622
  }
1189
- static get configuration() {
1190
- return {
1191
- async configureInteractive() {
1192
- const composeFile = await input2({
1193
- message: "Enter docker-compose file path:",
1194
- default: "docker-compose.yml"
1195
- });
1196
- const servicesInput = await input2({
1197
- message: "Enter services to start (comma-separated, leave empty for all):",
1198
- default: ""
1199
- });
1200
- const services = servicesInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1201
- if (composeFile === "docker-compose.yml" && services.length === 0) {
1202
- return { compose_file: "docker-compose.yml" };
1203
- }
1204
- if (services.length === 0) {
1205
- return composeFile;
1623
+ const branchName = await select({
1624
+ message: "Select branch configuration to edit:",
1625
+ choices: branches.map((n) => ({
1626
+ name: n.substring(branchPrefix.length),
1627
+ value: n
1628
+ }))
1629
+ });
1630
+ const branch = projects[branchName];
1631
+ const keepEvents = await confirm3({
1632
+ message: "Keep existing event configuration?",
1633
+ default: true
1634
+ });
1635
+ let events = branch.events;
1636
+ if (!keepEvents) {
1637
+ const availableEvents = EventRegistry.getEventsForManageUI();
1638
+ const currentEvents = Object.keys(branch.events);
1639
+ const selectedEvents = await checkbox({
1640
+ message: "Select events to enable:",
1641
+ choices: availableEvents.map((e) => ({
1642
+ name: `${e.name} - ${e.description}`,
1643
+ value: e.value,
1644
+ checked: currentEvents.includes(e.value)
1645
+ }))
1646
+ });
1647
+ events = {};
1648
+ for (const eventName of selectedEvents) {
1649
+ if (branch.events[eventName]) {
1650
+ events[eventName] = branch.events[eventName];
1651
+ } else {
1652
+ const eventHandler = EventRegistry.getEventByName(eventName);
1653
+ if (eventHandler) {
1654
+ const eventConfig = await eventHandler.configuration.configureInteractive();
1655
+ events[eventName] = eventConfig;
1206
1656
  }
1207
- return {
1208
- compose_file: composeFile,
1209
- services
1210
- };
1211
- },
1212
- getDefaultConfig() {
1213
- return { compose_file: "docker-compose.yml" };
1214
1657
  }
1215
- };
1216
- }
1217
- static getDockerCommand(config) {
1218
- if (typeof config === "boolean" || config === void 0) {
1219
- return "docker-compose up -d";
1220
1658
  }
1221
- if (typeof config === "string") {
1222
- return `docker-compose -f ${config} up -d`;
1223
- }
1224
- const composeFile = config.compose_file || "docker-compose.yml";
1225
- const services = config.services?.join(" ") || "";
1226
- return `docker-compose -f ${composeFile} up -d ${services}`.trim();
1227
1659
  }
1228
- static get processing() {
1229
- return {
1230
- async processEvent(context) {
1231
- const { project, isShellMode, shellCommands } = context;
1232
- const dockerConfig = project.events.docker;
1233
- const dockerCommand = _DockerEvent.getDockerCommand(
1234
- dockerConfig
1235
- );
1236
- if (isShellMode) {
1237
- shellCommands.push(dockerCommand);
1238
- } else {
1239
- const [cmd, ...args] = dockerCommand.split(" ");
1240
- spawn6(cmd, args, {
1241
- cwd: project.path.path,
1242
- stdio: "inherit"
1243
- });
1244
- }
1245
- },
1246
- generateShellCommand(context) {
1247
- const dockerConfig = context.project.events.docker;
1248
- return [_DockerEvent.getDockerCommand(dockerConfig)];
1249
- }
1250
- };
1660
+ const updatedConfig = {
1661
+ ...branch,
1662
+ events
1663
+ };
1664
+ const confirmed = await confirm3({
1665
+ message: "Save changes?",
1666
+ default: true
1667
+ });
1668
+ if (confirmed) {
1669
+ config.setProject(branchName, updatedConfig);
1670
+ log.info(`Branch configuration '${branchName}' updated successfully!`);
1251
1671
  }
1252
- static get tmux() {
1253
- return null;
1672
+ }
1673
+ async function deleteBranchManage(projectName, ctx) {
1674
+ const { config, log } = ctx;
1675
+ const projects = config.getProjects();
1676
+ const branchPrefix = `${projectName}#`;
1677
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1678
+ if (branches.length === 0) {
1679
+ log.info("No branch configurations to delete.");
1680
+ return;
1254
1681
  }
1255
- static get help() {
1256
- return {
1257
- usage: 'docker: true | "compose-file.yml" | { compose_file: string, services?: string[] }',
1258
- description: "Start Docker containers for the project",
1259
- examples: [
1260
- { config: true, description: "Use default docker-compose.yml" },
1261
- { config: "docker-compose.dev.yml", description: "Use custom compose file" },
1262
- {
1263
- config: { compose_file: "docker-compose.yml", services: ["web", "db"] },
1264
- description: "Start specific services"
1265
- }
1266
- ]
1267
- };
1682
+ const branchName = await select({
1683
+ message: "Select branch configuration to delete:",
1684
+ choices: branches.map((n) => ({
1685
+ name: n.substring(branchPrefix.length),
1686
+ value: n
1687
+ }))
1688
+ });
1689
+ const confirmed = await confirm3({
1690
+ message: `Are you sure you want to delete '${branchName}'?`,
1691
+ default: false
1692
+ });
1693
+ if (confirmed) {
1694
+ config.deleteProject(branchName);
1695
+ log.info(`Branch configuration '${branchName}' deleted.`);
1268
1696
  }
1269
- };
1697
+ }
1698
+ function listBranchesManage(projectName, ctx) {
1699
+ const { config } = ctx;
1700
+ const projects = config.getProjects();
1701
+ const branchPrefix = `${projectName}#`;
1702
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1703
+ console.log(`
1704
+ Branch configurations for '${projectName}':
1705
+ `);
1706
+ for (const branchName of branches) {
1707
+ const branch = projects[branchName];
1708
+ const shortName = branchName.substring(branchPrefix.length);
1709
+ console.log(` ${shortName}`);
1710
+ console.log(` Events: ${Object.keys(branch.events).join(", ") || "none"}`);
1711
+ console.log();
1712
+ }
1713
+ }
1714
+ var IDE_CHOICES;
1715
+ var init_interactive = __esm({
1716
+ "src/commands/interactive.ts"() {
1717
+ "use strict";
1718
+ init_environment();
1719
+ init_registry();
1720
+ IDE_CHOICES = [
1721
+ { name: "Visual Studio Code", value: "vscode" },
1722
+ { name: "IntelliJ IDEA", value: "idea" },
1723
+ { name: "Atom", value: "atom" }
1724
+ ];
1725
+ }
1726
+ });
1270
1727
 
1271
- // src/events/registry.ts
1272
- init_npm();
1273
- var ALL_EVENTS = [CwdEvent, IdeEvent, WebEvent, ClaudeEvent, DockerEvent, NpmEvent];
1274
- var EventRegistryClass = class {
1275
- _events = /* @__PURE__ */ new Map();
1276
- _initialized = false;
1277
- /**
1278
- * Initialize the registry by registering all events
1279
- */
1280
- async initialize() {
1281
- if (this._initialized) return;
1282
- this.registerEvents();
1283
- this._initialized = true;
1284
- }
1285
- /**
1286
- * Register all event classes
1287
- */
1288
- registerEvents() {
1289
- for (const EventClass of ALL_EVENTS) {
1290
- if (this.isValidEvent(EventClass)) {
1291
- const metadata = EventClass.metadata;
1292
- this._events.set(metadata.name, EventClass);
1293
- }
1728
+ // src/commands/index.ts
1729
+ init_config();
1730
+ init_environment();
1731
+ import { Command as Command8 } from "commander";
1732
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
1733
+ import { join, dirname } from "path";
1734
+ import { fileURLToPath } from "url";
1735
+ import loog from "loog";
1736
+ import omelette from "omelette";
1737
+ import File7 from "phylo";
1738
+
1739
+ // src/commands/open.ts
1740
+ init_environment();
1741
+ import { Command } from "commander";
1742
+ import File4 from "phylo";
1743
+
1744
+ // src/lib/tmux.ts
1745
+ import { exec as execCallback, spawn } from "child_process";
1746
+ import { promisify } from "util";
1747
+ var exec = promisify(execCallback);
1748
+ var TmuxManager = class {
1749
+ sessionPrefix = "workon-";
1750
+ async isTmuxAvailable() {
1751
+ try {
1752
+ await exec("which tmux");
1753
+ return true;
1754
+ } catch {
1755
+ return false;
1294
1756
  }
1295
1757
  }
1296
- /**
1297
- * Validate if a class is a proper event
1298
- */
1299
- isValidEvent(EventClass) {
1758
+ async sessionExists(sessionName) {
1300
1759
  try {
1301
- if (typeof EventClass !== "function") return false;
1302
- const metadata = EventClass.metadata;
1303
- return metadata !== void 0 && typeof metadata.name === "string" && typeof metadata.displayName === "string" && typeof EventClass.validation === "object" && typeof EventClass.configuration === "object" && typeof EventClass.processing === "object";
1760
+ await exec(`tmux has-session -t "${sessionName}"`);
1761
+ return true;
1304
1762
  } catch {
1305
1763
  return false;
1306
1764
  }
1307
1765
  }
1308
- /**
1309
- * Get all valid event names from registered events
1310
- */
1311
- getValidEventNames() {
1312
- this.ensureInitialized();
1313
- return Array.from(this._events.keys());
1314
- }
1315
- /**
1316
- * Get event by name
1317
- */
1318
- getEventByName(name) {
1319
- this.ensureInitialized();
1320
- return this._events.get(name) ?? null;
1321
- }
1322
- /**
1323
- * Get all events for management UI
1324
- */
1325
- getEventsForManageUI() {
1326
- this.ensureInitialized();
1327
- const events = [];
1328
- for (const [name, EventClass] of this._events) {
1329
- const metadata = EventClass.metadata;
1330
- events.push({
1331
- name: metadata.displayName,
1332
- value: name,
1333
- description: metadata.description
1334
- });
1766
+ getSessionName(projectName) {
1767
+ return `${this.sessionPrefix}${projectName}`;
1768
+ }
1769
+ async killSession(sessionName) {
1770
+ try {
1771
+ await exec(`tmux kill-session -t "${sessionName}"`);
1772
+ return true;
1773
+ } catch {
1774
+ return false;
1335
1775
  }
1336
- return events.sort((a, b) => a.name.localeCompare(b.name));
1337
- }
1338
- /**
1339
- * Get events that support tmux integration
1340
- */
1341
- getTmuxEnabledEvents() {
1342
- this.ensureInitialized();
1343
- const tmuxEvents = [];
1344
- for (const [name, EventClass] of this._events) {
1345
- const tmux = EventClass.tmux;
1346
- if (tmux) {
1347
- tmuxEvents.push({
1348
- name,
1349
- event: EventClass,
1350
- priority: tmux.getLayoutPriority ? tmux.getLayoutPriority() : 0
1776
+ }
1777
+ async createSplitSession(projectName, projectPath, claudeArgs = []) {
1778
+ const sessionName = this.getSessionName(projectName);
1779
+ if (await this.sessionExists(sessionName)) {
1780
+ await this.killSession(sessionName);
1781
+ }
1782
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1783
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
1784
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
1785
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
1786
+ return sessionName;
1787
+ }
1788
+ async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
1789
+ const sessionName = this.getSessionName(projectName);
1790
+ if (await this.sessionExists(sessionName)) {
1791
+ await this.killSession(sessionName);
1792
+ }
1793
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1794
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
1795
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
1796
+ await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
1797
+ await exec(`tmux set-option -t "${sessionName}:0.2" remain-on-exit on`);
1798
+ await exec(`tmux resize-pane -t "${sessionName}:0.2" -y 10`);
1799
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
1800
+ return sessionName;
1801
+ }
1802
+ async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
1803
+ const sessionName = this.getSessionName(projectName);
1804
+ if (await this.sessionExists(sessionName)) {
1805
+ await this.killSession(sessionName);
1806
+ }
1807
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
1808
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
1809
+ await exec(`tmux set-option -t "${sessionName}:0.1" remain-on-exit on`);
1810
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
1811
+ return sessionName;
1812
+ }
1813
+ async attachToSession(sessionName) {
1814
+ if (process.env.TMUX) {
1815
+ await exec(`tmux switch-client -t "${sessionName}"`);
1816
+ } else {
1817
+ const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
1818
+ const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
1819
+ if (useiTermIntegration) {
1820
+ spawn("tmux", ["-CC", "attach-session", "-t", sessionName], {
1821
+ stdio: "inherit",
1822
+ detached: true
1823
+ });
1824
+ } else {
1825
+ spawn("tmux", ["attach-session", "-t", sessionName], {
1826
+ stdio: "inherit",
1827
+ detached: true
1351
1828
  });
1352
1829
  }
1353
1830
  }
1354
- return tmuxEvents.sort((a, b) => b.priority - a.priority);
1355
- }
1356
- /**
1357
- * Get all available events with their metadata
1358
- */
1359
- getAllEvents() {
1360
- this.ensureInitialized();
1361
- const events = [];
1362
- for (const [name, EventClass] of this._events) {
1363
- const typedClass = EventClass;
1364
- events.push({
1365
- name,
1366
- metadata: typedClass.metadata,
1367
- hasValidation: !!typedClass.validation,
1368
- hasConfiguration: !!typedClass.configuration,
1369
- hasProcessing: !!typedClass.processing,
1370
- hasTmux: !!typedClass.tmux,
1371
- hasHelp: !!typedClass.help
1372
- });
1831
+ }
1832
+ buildShellCommands(projectName, projectPath, claudeArgs = []) {
1833
+ const sessionName = this.getSessionName(projectName);
1834
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1835
+ return [
1836
+ `# Create tmux split session for ${projectName}`,
1837
+ `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
1838
+ `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
1839
+ `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
1840
+ `tmux select-pane -t "${sessionName}:0.0"`,
1841
+ this.getAttachCommand(sessionName)
1842
+ ];
1843
+ }
1844
+ buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
1845
+ const sessionName = this.getSessionName(projectName);
1846
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1847
+ return [
1848
+ `# Create tmux three-pane session for ${projectName}`,
1849
+ `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
1850
+ `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
1851
+ `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
1852
+ `tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`,
1853
+ `tmux set-option -t "${sessionName}:0.2" remain-on-exit on`,
1854
+ `tmux resize-pane -t "${sessionName}:0.2" -y 10`,
1855
+ `tmux select-pane -t "${sessionName}:0.0"`,
1856
+ this.getAttachCommand(sessionName)
1857
+ ];
1858
+ }
1859
+ buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = "npm run dev") {
1860
+ const sessionName = this.getSessionName(projectName);
1861
+ return [
1862
+ `# Create tmux two-pane session with npm for ${projectName}`,
1863
+ `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
1864
+ `tmux new-session -d -s "${sessionName}" -c "${projectPath}"`,
1865
+ `tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`,
1866
+ `tmux set-option -t "${sessionName}:0.1" remain-on-exit on`,
1867
+ `tmux select-pane -t "${sessionName}:0.0"`,
1868
+ this.getAttachCommand(sessionName)
1869
+ ];
1870
+ }
1871
+ getAttachCommand(sessionName) {
1872
+ if (process.env.TMUX) {
1873
+ return `tmux switch-client -t "${sessionName}"`;
1373
1874
  }
1374
- return events;
1375
- }
1376
- /**
1377
- * Ensure registry is initialized
1378
- */
1379
- ensureInitialized() {
1380
- if (!this._initialized) {
1381
- throw new Error("EventRegistry must be initialized before use. Call initialize() first.");
1875
+ const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || process.env.ITERM_SESSION_ID;
1876
+ const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
1877
+ if (useiTermIntegration) {
1878
+ return `tmux -CC attach-session -t "${sessionName}"`;
1382
1879
  }
1880
+ return `tmux attach-session -t "${sessionName}"`;
1383
1881
  }
1384
- /**
1385
- * Clear the registry (useful for testing)
1386
- */
1387
- clear() {
1388
- this._events.clear();
1389
- this._initialized = false;
1882
+ async listWorkonSessions() {
1883
+ try {
1884
+ const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
1885
+ return stdout.trim().split("\n").filter((session) => session.startsWith(this.sessionPrefix)).map((session) => session.replace(this.sessionPrefix, ""));
1886
+ } catch {
1887
+ return [];
1888
+ }
1390
1889
  }
1391
1890
  };
1392
- var EventRegistry = new EventRegistryClass();
1393
1891
 
1394
1892
  // src/commands/open.ts
1893
+ init_registry();
1395
1894
  function createOpenCommand(ctx) {
1396
1895
  const { config, log } = ctx;
1397
1896
  const command = new Command("open").description("Open a project by passing its project id").argument("[project]", "The id of the project to open (supports project:command syntax)").option("-d, --debug", "Enable debug logging").option("-n, --dry-run", "Show what would happen without executing").option("--shell", "Output shell commands instead of spawning processes").action(async (projectArg, options) => {
@@ -1806,8 +2305,9 @@ function createConfigCommand(ctx) {
1806
2305
  }
1807
2306
 
1808
2307
  // src/commands/manage.ts
2308
+ init_registry();
1809
2309
  import { Command as Command6 } from "commander";
1810
- import { select as select2, input as input5, confirm as confirm3, checkbox as checkbox2 } from "@inquirer/prompts";
2310
+ import { select as select2, input as input5, confirm as confirm4, checkbox as checkbox2 } from "@inquirer/prompts";
1811
2311
  import File5 from "phylo";
1812
2312
  var IDE_CHOICES2 = [
1813
2313
  { name: "Visual Studio Code", value: "vscode" },
@@ -1939,7 +2439,7 @@ async function createProject(ctx) {
1939
2439
  }
1940
2440
  console.log("\nProject configuration:");
1941
2441
  console.log(JSON.stringify(projectConfig, null, 2));
1942
- const confirmed = await confirm3({
2442
+ const confirmed = await confirm4({
1943
2443
  message: "Save this project?",
1944
2444
  default: true
1945
2445
  });
@@ -1990,7 +2490,7 @@ async function editProject(ctx) {
1990
2490
  message: "Project homepage URL:",
1991
2491
  default: project.homepage || ""
1992
2492
  });
1993
- const keepEvents = await confirm3({
2493
+ const keepEvents = await confirm4({
1994
2494
  message: "Keep existing event configuration?",
1995
2495
  default: true
1996
2496
  });
@@ -2029,7 +2529,7 @@ async function editProject(ctx) {
2029
2529
  }
2030
2530
  console.log("\nUpdated configuration:");
2031
2531
  console.log(JSON.stringify(updatedConfig, null, 2));
2032
- const confirmed = await confirm3({
2532
+ const confirmed = await confirm4({
2033
2533
  message: "Save changes?",
2034
2534
  default: true
2035
2535
  });
@@ -2052,7 +2552,7 @@ async function deleteProject(ctx) {
2052
2552
  message: "Select project to delete:",
2053
2553
  choices: projectNames.map((n) => ({ name: n, value: n }))
2054
2554
  });
2055
- const confirmed = await confirm3({
2555
+ const confirmed = await confirm4({
2056
2556
  message: `Are you sure you want to delete '${name}'?`,
2057
2557
  default: false
2058
2558
  });
@@ -2081,6 +2581,142 @@ async function listProjects(ctx) {
2081
2581
  }
2082
2582
  }
2083
2583
 
2584
+ // src/commands/add.ts
2585
+ import { Command as Command7 } from "commander";
2586
+ import { existsSync, readFileSync } from "fs";
2587
+ import { basename, resolve } from "path";
2588
+ import File6 from "phylo";
2589
+ import { confirm as confirm5 } from "@inquirer/prompts";
2590
+ function createAddCommand(ctx) {
2591
+ const { log } = ctx;
2592
+ return new Command7("add").description("Add a project from a directory path").argument("[path]", "Path to the project directory (defaults to current directory)", ".").option("-d, --debug", "Enable debug logging").option("-n, --name <name>", "Override the detected project name").option(
2593
+ "-i, --ide <ide>",
2594
+ "Specify the IDE to use (vscode, idea, atom, code, subl, vim, emacs)"
2595
+ ).option("-f, --force", "Overwrite existing project with same name").action(async (pathArg, options) => {
2596
+ if (options.debug) {
2597
+ log.setLogLevel("debug");
2598
+ }
2599
+ await addProject(pathArg, options, ctx);
2600
+ });
2601
+ }
2602
+ async function addProject(pathArg, options, ctx) {
2603
+ const { config, log } = ctx;
2604
+ const defaults = config.getDefaults();
2605
+ const projects = config.getProjects();
2606
+ const targetPath = resolve(pathArg);
2607
+ log.debug(`Resolved path: ${targetPath}`);
2608
+ if (!existsSync(targetPath)) {
2609
+ log.error(`Path does not exist: ${targetPath}`);
2610
+ process.exit(1);
2611
+ }
2612
+ const pathFile = File6.from(targetPath);
2613
+ try {
2614
+ const stat = pathFile.stat();
2615
+ if (!stat.isDirectory()) {
2616
+ log.error(`Path is not a directory: ${targetPath}`);
2617
+ process.exit(1);
2618
+ }
2619
+ } catch {
2620
+ log.error(`Cannot access path: ${targetPath}`);
2621
+ process.exit(1);
2622
+ }
2623
+ const discovery = discoverProject(targetPath, log);
2624
+ log.debug(`Discovery result: ${JSON.stringify(discovery)}`);
2625
+ const projectName = options.name || discovery.name;
2626
+ log.debug(`Project name: ${projectName}`);
2627
+ if (!/^[\w-]+$/.test(projectName)) {
2628
+ log.error(`Invalid project name: ${projectName}`);
2629
+ log.error("Name can only contain letters, numbers, underscores, and hyphens");
2630
+ process.exit(1);
2631
+ }
2632
+ if (projectName in projects && !options.force) {
2633
+ const overwrite = await confirm5({
2634
+ message: `Project '${projectName}' already exists. Overwrite?`,
2635
+ default: false
2636
+ });
2637
+ if (!overwrite) {
2638
+ log.info("Cancelled.");
2639
+ return;
2640
+ }
2641
+ }
2642
+ const ide = options.ide || discovery.detectedIde || "vscode";
2643
+ log.debug(`IDE: ${ide}`);
2644
+ let relativePath = targetPath;
2645
+ if (defaults?.base) {
2646
+ const baseDir = File6.from(defaults.base);
2647
+ try {
2648
+ const relPath = pathFile.relativize(baseDir.path);
2649
+ if (relPath && !relPath.path.startsWith("..")) {
2650
+ relativePath = relPath.path;
2651
+ }
2652
+ } catch {
2653
+ }
2654
+ }
2655
+ log.debug(`Relative path: ${relativePath}`);
2656
+ const projectConfig = {
2657
+ path: relativePath,
2658
+ ide,
2659
+ events: {
2660
+ cwd: true,
2661
+ ide: true
2662
+ }
2663
+ };
2664
+ if ((discovery.isNode || discovery.isBun) && discovery.packageJson) {
2665
+ const scripts = discovery.packageJson.scripts;
2666
+ if (scripts && (scripts.dev || scripts.start)) {
2667
+ projectConfig.events.npm = scripts.dev ? "dev" : "start";
2668
+ }
2669
+ }
2670
+ config.setProject(projectName, projectConfig);
2671
+ log.info(`Added project '${projectName}'`);
2672
+ log.info(` Path: ${relativePath}`);
2673
+ log.info(` IDE: ${ide}`);
2674
+ log.info(` Events: ${Object.keys(projectConfig.events).join(", ")}`);
2675
+ log.info("");
2676
+ log.info(`Use 'workon ${projectName}' to start working!`);
2677
+ }
2678
+ function discoverProject(targetPath, log) {
2679
+ const dirName = basename(targetPath);
2680
+ const discovery = {
2681
+ name: dirName,
2682
+ isNode: false,
2683
+ isBun: false,
2684
+ detectedIde: null,
2685
+ packageJson: null
2686
+ };
2687
+ const packageJsonPath = resolve(targetPath, "package.json");
2688
+ if (existsSync(packageJsonPath)) {
2689
+ discovery.isNode = true;
2690
+ log.debug("Detected Node project (package.json found)");
2691
+ try {
2692
+ const content = readFileSync(packageJsonPath, "utf-8");
2693
+ discovery.packageJson = JSON.parse(content);
2694
+ const pkgName = discovery.packageJson?.name;
2695
+ if (pkgName && /^[\w-]+$/.test(pkgName)) {
2696
+ discovery.name = pkgName;
2697
+ log.debug(`Using name from package.json: ${pkgName}`);
2698
+ }
2699
+ } catch (error) {
2700
+ log.debug(`Failed to parse package.json: ${error.message}`);
2701
+ }
2702
+ }
2703
+ const bunLockPath = resolve(targetPath, "bun.lockb");
2704
+ if (existsSync(bunLockPath)) {
2705
+ discovery.isBun = true;
2706
+ log.debug("Detected Bun project (bun.lockb found)");
2707
+ }
2708
+ const vscodeDir = resolve(targetPath, ".vscode");
2709
+ const ideaDir = resolve(targetPath, ".idea");
2710
+ if (existsSync(vscodeDir)) {
2711
+ discovery.detectedIde = "vscode";
2712
+ log.debug("Detected VS Code (.vscode directory found)");
2713
+ } else if (existsSync(ideaDir)) {
2714
+ discovery.detectedIde = "idea";
2715
+ log.debug("Detected IntelliJ IDEA (.idea directory found)");
2716
+ }
2717
+ return discovery;
2718
+ }
2719
+
2084
2720
  // src/commands/index.ts
2085
2721
  var __filename = fileURLToPath(import.meta.url);
2086
2722
  var __dirname = dirname(__filename);
@@ -2091,16 +2727,16 @@ function findPackageJson() {
2091
2727
  join(process.cwd(), "package.json")
2092
2728
  ];
2093
2729
  for (const p of paths) {
2094
- if (existsSync(p)) {
2730
+ if (existsSync2(p)) {
2095
2731
  return p;
2096
2732
  }
2097
2733
  }
2098
2734
  throw new Error("Could not find package.json");
2099
2735
  }
2100
2736
  function createCli() {
2101
- const program2 = new Command7();
2737
+ const program2 = new Command8();
2102
2738
  const packageJsonPath = findPackageJson();
2103
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2739
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
2104
2740
  const config = new Config();
2105
2741
  const log = loog({
2106
2742
  prefixStyle: "ascii",
@@ -2109,35 +2745,45 @@ function createCli() {
2109
2745
  config.set("pkg", packageJson);
2110
2746
  EnvironmentRecognizer.configure(config, log);
2111
2747
  const completion = setupCompletion(config);
2112
- program2.name("workon").description("Work on something great!").version(packageJson.version).option("-d, --debug", "Enable debug logging").option("--completion", "Setup shell tab completion").option("--shell", "Output shell commands for evaluation").option("--init", "Generate shell integration function").hook("preAction", (thisCommand) => {
2748
+ program2.name("workon").description("Work on something great!").version(packageJson.version).argument("[project]", "Project name to open (supports project:command syntax)").option("-d, --debug", "Enable debug logging").option("--completion", "Setup shell tab completion").option("--shell", "Output shell commands for evaluation").option("--init", "Generate shell integration function").hook("preAction", (thisCommand) => {
2113
2749
  const opts = thisCommand.opts();
2114
2750
  if (opts.debug) {
2115
2751
  log.setLogLevel("debug");
2116
2752
  }
2117
- }).action(async (options) => {
2118
- if (options.debug) {
2119
- log.setLogLevel("debug");
2120
- }
2121
- if (options.completion) {
2122
- log.debug("Setting up command-line completion");
2123
- completion.setupShellInitFile();
2124
- return;
2125
- }
2126
- if (options.init) {
2127
- log.debug("Generating shell integration function");
2128
- outputShellInit(program2);
2129
- return;
2753
+ }).action(
2754
+ async (project, options) => {
2755
+ if (options.debug) {
2756
+ log.setLogLevel("debug");
2757
+ }
2758
+ if (options.completion) {
2759
+ log.debug("Setting up command-line completion");
2760
+ completion.setupShellInitFile();
2761
+ return;
2762
+ }
2763
+ if (options.init) {
2764
+ log.debug("Generating shell integration function");
2765
+ outputShellInit(program2);
2766
+ return;
2767
+ }
2768
+ if (project) {
2769
+ const args = ["open", project];
2770
+ if (options.shell) args.push("--shell");
2771
+ if (options.debug) args.push("--debug");
2772
+ await program2.parseAsync(["node", "workon", ...args]);
2773
+ return;
2774
+ }
2775
+ const environment = await EnvironmentRecognizer.recognize(File7.cwd());
2776
+ program2.setOptionValue("_environment", environment);
2777
+ program2.setOptionValue("_config", config);
2778
+ program2.setOptionValue("_log", log);
2779
+ const { runInteractive: runInteractive2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
2780
+ await runInteractive2({ config, log, environment });
2130
2781
  }
2131
- const environment = await EnvironmentRecognizer.recognize(File6.cwd());
2132
- program2.setOptionValue("_environment", environment);
2133
- program2.setOptionValue("_config", config);
2134
- program2.setOptionValue("_log", log);
2135
- const { runInteractive: runInteractive2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
2136
- await runInteractive2({ config, log, environment });
2137
- });
2782
+ );
2138
2783
  program2.setOptionValue("_config", config);
2139
2784
  program2.setOptionValue("_log", log);
2140
2785
  program2.addCommand(createOpenCommand({ config, log }));
2786
+ program2.addCommand(createAddCommand({ config, log }));
2141
2787
  program2.addCommand(createConfigCommand({ config, log }));
2142
2788
  program2.addCommand(createManageCommand({ config, log }));
2143
2789
  program2.on("command:*", async (operands) => {