workon 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,27 +9,314 @@ 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 (error) {
240
+ this.log.debug(`Git branch detection failed: ${error.message}`);
241
+ }
242
+ }
243
+ return this.getProjectEnvironment(base, matching);
244
+ }
245
+ static getAllProjects(refresh = false) {
246
+ if (this.projects.length > 0 && !refresh) {
247
+ return this.projects;
248
+ }
249
+ const defaults = this.config.getDefaults();
250
+ if (!defaults?.base) {
251
+ this.projects = [];
252
+ return this.projects;
253
+ }
254
+ const baseDir = File2.from(defaults.base);
255
+ const projectsMap = this.config.getProjects();
256
+ this.projects = Object.entries(projectsMap).map(([name, project]) => ({
257
+ ...project,
258
+ name,
259
+ path: baseDir.join(project.path)
260
+ }));
261
+ return this.projects;
262
+ }
263
+ static getProjectEnvironment(base, _matching) {
264
+ const exactName = `${base.name}#${base.branch}`;
265
+ const exactProj = this.projects.find((p) => p.name === exactName);
266
+ const toProjectConfig = (p) => ({
267
+ name: p.name,
268
+ path: p.path.path,
269
+ // Convert PhyloFile to string path
270
+ ide: p.ide,
271
+ homepage: p.homepage,
272
+ events: p.events,
273
+ branch: p.branch,
274
+ exactName
275
+ });
276
+ if (exactProj) {
277
+ return new ProjectEnvironment({ ...toProjectConfig(exactProj), branch: base.branch });
278
+ }
279
+ return new ProjectEnvironment(toProjectConfig(base));
280
+ }
281
+ static ensureConfigured() {
282
+ if (!this.configured) {
283
+ this.config = new Config();
284
+ this.log = {
285
+ debug: () => {
286
+ },
287
+ info: () => {
288
+ },
289
+ log: () => {
290
+ },
291
+ warn: () => {
292
+ },
293
+ error: () => {
294
+ },
295
+ setLogLevel: () => {
296
+ }
297
+ };
298
+ this.configured = true;
299
+ }
300
+ }
301
+ };
302
+ }
303
+ });
304
+
305
+ // src/events/core/cwd.ts
306
+ import { spawn } from "child_process";
307
+ var CwdEvent;
308
+ var init_cwd = __esm({
309
+ "src/events/core/cwd.ts"() {
310
+ "use strict";
311
+ CwdEvent = class {
25
312
  static get metadata() {
26
313
  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"]
314
+ name: "cwd",
315
+ displayName: "Change directory (cwd)",
316
+ description: "Change current working directory to project path",
317
+ category: "core",
318
+ requiresTmux: false,
319
+ dependencies: []
33
320
  };
34
321
  }
35
322
  static get validation() {
@@ -38,160 +325,827 @@ var init_npm = __esm({
38
325
  if (typeof config === "boolean" || config === "true" || config === "false") {
39
326
  return true;
40
327
  }
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";
328
+ return "cwd config must be a boolean (true/false)";
61
329
  }
62
330
  };
63
331
  }
64
332
  static get configuration() {
65
333
  return {
66
334
  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
- };
335
+ return true;
94
336
  },
95
337
  getDefaultConfig() {
96
- return "dev";
338
+ return true;
97
339
  }
98
340
  };
99
341
  }
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
342
  static get processing() {
110
343
  return {
111
344
  async processEvent(context) {
112
345
  const { project, isShellMode, shellCommands } = context;
113
- const npmConfig = project.events.npm;
114
- const npmCommand = _NpmEvent.getNpmCommand(npmConfig);
346
+ const projectPath = project.path.path;
115
347
  if (isShellMode) {
116
- shellCommands.push(npmCommand);
348
+ shellCommands.push(`cd "${projectPath}"`);
117
349
  } else {
118
- const [cmd, ...args] = npmCommand.split(" ");
119
- spawn7(cmd, args, {
120
- cwd: project.path.path,
350
+ const shell = process.env.SHELL || "/bin/bash";
351
+ spawn(shell, [], {
352
+ cwd: projectPath,
121
353
  stdio: "inherit"
122
354
  });
123
355
  }
124
356
  },
125
357
  generateShellCommand(context) {
126
- const npmConfig = context.project.events.npm;
127
- return [_NpmEvent.getNpmCommand(npmConfig)];
358
+ const projectPath = context.project.path.path;
359
+ return [`cd "${projectPath}"`];
128
360
  }
129
361
  };
130
362
  }
131
363
  static get tmux() {
364
+ return null;
365
+ }
366
+ static get help() {
132
367
  return {
133
- getLayoutPriority() {
134
- return 50;
368
+ usage: "cwd: true | false",
369
+ description: "Change the current working directory to the project path",
370
+ examples: [
371
+ { config: true, description: "Enable directory change" },
372
+ { config: false, description: "Disable directory change" }
373
+ ]
374
+ };
375
+ }
376
+ };
377
+ }
378
+ });
379
+
380
+ // src/events/core/ide.ts
381
+ import { spawn as spawn2 } from "child_process";
382
+ var IdeEvent;
383
+ var init_ide = __esm({
384
+ "src/events/core/ide.ts"() {
385
+ "use strict";
386
+ IdeEvent = class {
387
+ static get metadata() {
388
+ return {
389
+ name: "ide",
390
+ displayName: "Open in IDE",
391
+ description: "Open project in configured IDE/editor",
392
+ category: "core",
393
+ requiresTmux: false,
394
+ dependencies: []
395
+ };
396
+ }
397
+ static get validation() {
398
+ return {
399
+ validateConfig(config) {
400
+ if (typeof config === "boolean" || config === "true" || config === "false") {
401
+ return true;
402
+ }
403
+ return "ide config must be a boolean (true/false)";
404
+ }
405
+ };
406
+ }
407
+ static get configuration() {
408
+ return {
409
+ async configureInteractive() {
410
+ return true;
135
411
  },
136
- contributeToLayout(enabledCommands) {
137
- if (enabledCommands.includes("claude")) {
138
- return "three-pane";
412
+ getDefaultConfig() {
413
+ return true;
414
+ }
415
+ };
416
+ }
417
+ static get processing() {
418
+ return {
419
+ async processEvent(context) {
420
+ const { project, isShellMode, shellCommands } = context;
421
+ const projectPath = project.path.path;
422
+ const ide = project.ide || "code";
423
+ if (isShellMode) {
424
+ shellCommands.push(`${ide} "${projectPath}" &`);
425
+ } else {
426
+ spawn2(ide, [projectPath], {
427
+ detached: true,
428
+ stdio: "ignore"
429
+ }).unref();
139
430
  }
140
- return "two-pane-npm";
431
+ },
432
+ generateShellCommand(context) {
433
+ const projectPath = context.project.path.path;
434
+ const ide = context.project.ide || "code";
435
+ return [`${ide} "${projectPath}" &`];
141
436
  }
142
437
  };
143
438
  }
439
+ static get tmux() {
440
+ return null;
441
+ }
144
442
  static get help() {
145
443
  return {
146
- usage: 'npm: true | "script" | { command: string, watch?: boolean, auto_restart?: boolean }',
147
- description: "Run an NPM script in the project directory",
444
+ usage: "ide: true | false",
445
+ description: "Open the project in the configured IDE",
148
446
  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" }
447
+ { config: true, description: "Enable IDE opening" },
448
+ { config: false, description: "Disable IDE opening" }
152
449
  ]
153
450
  };
154
451
  }
155
452
  };
156
- npm_default = NpmEvent;
157
453
  }
158
454
  });
159
455
 
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);
456
+ // src/events/core/web.ts
457
+ import { spawn as spawn3 } from "child_process";
458
+ import { platform } from "os";
459
+ var WebEvent;
460
+ var init_web = __esm({
461
+ "src/events/core/web.ts"() {
462
+ "use strict";
463
+ WebEvent = class _WebEvent {
464
+ static get metadata() {
465
+ return {
466
+ name: "web",
467
+ displayName: "Open homepage in browser",
468
+ description: "Open project homepage in web browser",
469
+ category: "core",
470
+ requiresTmux: false,
471
+ dependencies: []
472
+ };
473
+ }
474
+ static get validation() {
475
+ return {
476
+ validateConfig(config) {
477
+ if (typeof config === "boolean" || config === "true" || config === "false") {
478
+ return true;
479
+ }
480
+ return "web config must be a boolean (true/false)";
481
+ }
482
+ };
483
+ }
484
+ static get configuration() {
485
+ return {
486
+ async configureInteractive() {
487
+ return true;
488
+ },
489
+ getDefaultConfig() {
490
+ return true;
491
+ }
492
+ };
493
+ }
494
+ static getOpenCommand() {
495
+ const os = platform();
496
+ switch (os) {
497
+ case "darwin":
498
+ return "open";
499
+ case "win32":
500
+ return "start";
501
+ default:
502
+ return "xdg-open";
503
+ }
504
+ }
505
+ static get processing() {
506
+ return {
507
+ async processEvent(context) {
508
+ const { project, isShellMode, shellCommands } = context;
509
+ const homepage = project.homepage;
510
+ if (!homepage) {
511
+ console.warn("No homepage configured for project");
512
+ return;
513
+ }
514
+ const openCmd = _WebEvent.getOpenCommand();
515
+ if (isShellMode) {
516
+ shellCommands.push(`${openCmd} "${homepage}" &`);
517
+ } else {
518
+ spawn3(openCmd, [homepage], {
519
+ detached: true,
520
+ stdio: "ignore"
521
+ }).unref();
522
+ }
523
+ },
524
+ generateShellCommand(context) {
525
+ const homepage = context.project.homepage;
526
+ if (!homepage) return [];
527
+ const openCmd = _WebEvent.getOpenCommand();
528
+ return [`${openCmd} "${homepage}" &`];
529
+ }
530
+ };
531
+ }
532
+ static get tmux() {
533
+ return null;
534
+ }
535
+ static get help() {
536
+ return {
537
+ usage: "web: true | false",
538
+ description: "Open the project homepage in the default browser",
539
+ examples: [
540
+ { config: true, description: "Enable browser opening" },
541
+ { config: false, description: "Disable browser opening" }
542
+ ]
543
+ };
544
+ }
545
+ };
546
+ }
547
+ });
548
+
549
+ // src/events/extensions/claude.ts
550
+ import { spawn as spawn4 } from "child_process";
551
+ import { input, confirm } from "@inquirer/prompts";
552
+ var ClaudeEvent;
553
+ var init_claude = __esm({
554
+ "src/events/extensions/claude.ts"() {
555
+ "use strict";
556
+ ClaudeEvent = class _ClaudeEvent {
557
+ static get metadata() {
558
+ return {
559
+ name: "claude",
560
+ displayName: "Launch Claude Code",
561
+ description: "Launch Claude Code with optional flags and configuration",
562
+ category: "development",
563
+ requiresTmux: true,
564
+ dependencies: ["claude"]
565
+ };
566
+ }
567
+ static get validation() {
568
+ return {
569
+ validateConfig(config) {
570
+ if (typeof config === "boolean" || config === "true" || config === "false") {
571
+ return true;
572
+ }
573
+ if (typeof config === "object" && config !== null) {
574
+ const cfg = config;
575
+ if (cfg.flags !== void 0) {
576
+ if (!Array.isArray(cfg.flags)) {
577
+ return "claude.flags must be an array of strings";
578
+ }
579
+ for (const flag of cfg.flags) {
580
+ if (typeof flag !== "string") {
581
+ return "claude.flags must contain only strings";
582
+ }
583
+ if (!flag.startsWith("-")) {
584
+ return `Invalid flag "${flag}": flags must start with - or --`;
585
+ }
586
+ }
587
+ }
588
+ if (cfg.split_terminal !== void 0 && typeof cfg.split_terminal !== "boolean") {
589
+ return "claude.split_terminal must be a boolean";
590
+ }
591
+ return true;
592
+ }
593
+ return "claude config must be a boolean or object with flags/split_terminal";
594
+ }
595
+ };
596
+ }
597
+ static get configuration() {
598
+ return {
599
+ async configureInteractive() {
600
+ const useAdvanced = await confirm({
601
+ message: "Configure advanced Claude options?",
602
+ default: false
603
+ });
604
+ if (!useAdvanced) {
605
+ return true;
606
+ }
607
+ const flagsInput = await input({
608
+ message: "Enter Claude flags (comma-separated, e.g., --resume, --debug):",
609
+ default: ""
610
+ });
611
+ const flags = flagsInput.split(",").map((f) => f.trim()).filter((f) => f.length > 0 && f.startsWith("-"));
612
+ const splitTerminal = await confirm({
613
+ message: "Use split terminal layout (Claude + shell)?",
614
+ default: true
615
+ });
616
+ if (flags.length === 0 && !splitTerminal) {
617
+ return true;
618
+ }
619
+ const config = {};
620
+ if (flags.length > 0) config.flags = flags;
621
+ if (splitTerminal) config.split_terminal = splitTerminal;
622
+ return config;
623
+ },
624
+ getDefaultConfig() {
625
+ return true;
626
+ }
627
+ };
628
+ }
629
+ static getClaudeCommand(config) {
630
+ if (typeof config === "boolean" || config === void 0) {
631
+ return "claude";
632
+ }
633
+ const flags = config.flags || [];
634
+ return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
635
+ }
636
+ static get processing() {
637
+ return {
638
+ async processEvent(context) {
639
+ const { project, isShellMode, shellCommands } = context;
640
+ const claudeConfig = project.events.claude;
641
+ const claudeCommand = _ClaudeEvent.getClaudeCommand(claudeConfig);
642
+ if (isShellMode) {
643
+ shellCommands.push(claudeCommand);
644
+ } else {
645
+ const args = claudeCommand.split(" ").slice(1);
646
+ spawn4("claude", args, {
647
+ cwd: project.path.path,
648
+ stdio: "inherit"
649
+ });
650
+ }
651
+ },
652
+ generateShellCommand(context) {
653
+ const claudeConfig = context.project.events.claude;
654
+ return [_ClaudeEvent.getClaudeCommand(claudeConfig)];
655
+ }
656
+ };
657
+ }
658
+ static get tmux() {
659
+ return {
660
+ getLayoutPriority() {
661
+ return 100;
662
+ },
663
+ contributeToLayout(enabledCommands) {
664
+ if (enabledCommands.includes("npm")) {
665
+ return "three-pane";
666
+ }
667
+ return "split";
668
+ }
669
+ };
670
+ }
671
+ static get help() {
672
+ return {
673
+ usage: "claude: true | { flags: string[], split_terminal: boolean }",
674
+ description: "Launch Claude Code in the project directory",
675
+ examples: [
676
+ { config: true, description: "Launch Claude with defaults" },
677
+ { config: { flags: ["--resume"] }, description: "Resume previous session" },
678
+ {
679
+ config: { flags: ["--model", "opus"], split_terminal: true },
680
+ description: "Use Opus model with split terminal"
681
+ }
682
+ ]
683
+ };
684
+ }
685
+ };
686
+ }
687
+ });
688
+
689
+ // src/events/extensions/docker.ts
690
+ import { spawn as spawn5 } from "child_process";
691
+ import { input as input2 } from "@inquirer/prompts";
692
+ var DockerEvent;
693
+ var init_docker = __esm({
694
+ "src/events/extensions/docker.ts"() {
695
+ "use strict";
696
+ DockerEvent = class _DockerEvent {
697
+ static get metadata() {
698
+ return {
699
+ name: "docker",
700
+ displayName: "Docker container management",
701
+ description: "Start/stop Docker containers for the project",
702
+ category: "development",
703
+ requiresTmux: false,
704
+ dependencies: ["docker"]
705
+ };
706
+ }
707
+ static get validation() {
708
+ return {
709
+ validateConfig(config) {
710
+ if (typeof config === "boolean" || config === "true" || config === "false") {
711
+ return true;
712
+ }
713
+ if (typeof config === "string") {
714
+ return true;
715
+ }
716
+ if (typeof config === "object" && config !== null) {
717
+ const cfg = config;
718
+ if (cfg.compose_file !== void 0 && typeof cfg.compose_file !== "string") {
719
+ return "docker.compose_file must be a string";
720
+ }
721
+ if (cfg.services !== void 0) {
722
+ if (!Array.isArray(cfg.services)) {
723
+ return "docker.services must be an array";
724
+ }
725
+ for (const service of cfg.services) {
726
+ if (typeof service !== "string") {
727
+ return "docker.services must contain only strings";
728
+ }
729
+ }
730
+ }
731
+ return true;
732
+ }
733
+ return "docker config must be a boolean, string (compose file), or object";
734
+ }
735
+ };
736
+ }
737
+ static get configuration() {
738
+ return {
739
+ async configureInteractive() {
740
+ const composeFile = await input2({
741
+ message: "Enter docker-compose file path:",
742
+ default: "docker-compose.yml"
743
+ });
744
+ const servicesInput = await input2({
745
+ message: "Enter services to start (comma-separated, leave empty for all):",
746
+ default: ""
747
+ });
748
+ const services = servicesInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
749
+ if (composeFile === "docker-compose.yml" && services.length === 0) {
750
+ return { compose_file: "docker-compose.yml" };
751
+ }
752
+ if (services.length === 0) {
753
+ return composeFile;
754
+ }
755
+ return {
756
+ compose_file: composeFile,
757
+ services
758
+ };
759
+ },
760
+ getDefaultConfig() {
761
+ return { compose_file: "docker-compose.yml" };
762
+ }
763
+ };
764
+ }
765
+ static getDockerCommand(config) {
766
+ if (typeof config === "boolean" || config === void 0) {
767
+ return "docker-compose up -d";
768
+ }
769
+ if (typeof config === "string") {
770
+ return `docker-compose -f ${config} up -d`;
771
+ }
772
+ const composeFile = config.compose_file || "docker-compose.yml";
773
+ const services = config.services?.join(" ") || "";
774
+ return `docker-compose -f ${composeFile} up -d ${services}`.trim();
775
+ }
776
+ static get processing() {
777
+ return {
778
+ async processEvent(context) {
779
+ const { project, isShellMode, shellCommands } = context;
780
+ const dockerConfig = project.events.docker;
781
+ const dockerCommand = _DockerEvent.getDockerCommand(
782
+ dockerConfig
783
+ );
784
+ if (isShellMode) {
785
+ shellCommands.push(dockerCommand);
786
+ } else {
787
+ const [cmd, ...args] = dockerCommand.split(" ");
788
+ spawn5(cmd, args, {
789
+ cwd: project.path.path,
790
+ stdio: "inherit"
791
+ });
792
+ }
793
+ },
794
+ generateShellCommand(context) {
795
+ const dockerConfig = context.project.events.docker;
796
+ return [_DockerEvent.getDockerCommand(dockerConfig)];
797
+ }
798
+ };
799
+ }
800
+ static get tmux() {
801
+ return null;
802
+ }
803
+ static get help() {
804
+ return {
805
+ usage: 'docker: true | "compose-file.yml" | { compose_file: string, services?: string[] }',
806
+ description: "Start Docker containers for the project",
807
+ examples: [
808
+ { config: true, description: "Use default docker-compose.yml" },
809
+ { config: "docker-compose.dev.yml", description: "Use custom compose file" },
810
+ {
811
+ config: { compose_file: "docker-compose.yml", services: ["web", "db"] },
812
+ description: "Start specific services"
813
+ }
814
+ ]
815
+ };
816
+ }
817
+ };
818
+ }
819
+ });
820
+
821
+ // src/events/extensions/npm.ts
822
+ var npm_exports = {};
823
+ __export(npm_exports, {
824
+ NpmEvent: () => NpmEvent,
825
+ default: () => npm_default
826
+ });
827
+ import { spawn as spawn6 } from "child_process";
828
+ import { input as input3, confirm as confirm2 } from "@inquirer/prompts";
829
+ var NpmEvent, npm_default;
830
+ var init_npm = __esm({
831
+ "src/events/extensions/npm.ts"() {
832
+ "use strict";
833
+ NpmEvent = class _NpmEvent {
834
+ static get metadata() {
835
+ return {
836
+ name: "npm",
837
+ displayName: "Run NPM command",
838
+ description: "Execute NPM scripts in project directory",
839
+ category: "development",
840
+ requiresTmux: true,
841
+ dependencies: ["npm"]
842
+ };
843
+ }
844
+ static get validation() {
845
+ return {
846
+ validateConfig(config) {
847
+ if (typeof config === "boolean" || config === "true" || config === "false") {
848
+ return true;
849
+ }
850
+ if (typeof config === "string") {
851
+ if (config.trim().length === 0) {
852
+ return "npm script name cannot be empty";
853
+ }
854
+ return true;
855
+ }
856
+ if (typeof config === "object" && config !== null) {
857
+ const cfg = config;
858
+ if (typeof cfg.command !== "string" || cfg.command.trim().length === 0) {
859
+ return "npm.command must be a non-empty string";
860
+ }
861
+ if (cfg.watch !== void 0 && typeof cfg.watch !== "boolean") {
862
+ return "npm.watch must be a boolean";
863
+ }
864
+ if (cfg.auto_restart !== void 0 && typeof cfg.auto_restart !== "boolean") {
865
+ return "npm.auto_restart must be a boolean";
866
+ }
867
+ return true;
868
+ }
869
+ return "npm config must be a boolean, string (script name), or object";
870
+ }
871
+ };
872
+ }
873
+ static get configuration() {
874
+ return {
875
+ async configureInteractive() {
876
+ const scriptName = await input3({
877
+ message: "Enter NPM script to run:",
878
+ default: "dev"
879
+ });
880
+ const useAdvanced = await confirm2({
881
+ message: "Configure advanced NPM options?",
882
+ default: false
883
+ });
884
+ if (!useAdvanced) {
885
+ return scriptName;
886
+ }
887
+ const watch = await confirm2({
888
+ message: "Enable watch mode?",
889
+ default: false
890
+ });
891
+ const autoRestart = await confirm2({
892
+ message: "Auto-restart on crash?",
893
+ default: false
894
+ });
895
+ if (!watch && !autoRestart) {
896
+ return scriptName;
897
+ }
898
+ return {
899
+ command: scriptName,
900
+ watch,
901
+ auto_restart: autoRestart
902
+ };
903
+ },
904
+ getDefaultConfig() {
905
+ return "dev";
906
+ }
907
+ };
908
+ }
909
+ static getNpmCommand(config) {
910
+ if (typeof config === "boolean" || config === void 0) {
911
+ return "npm run dev";
912
+ }
913
+ if (typeof config === "string") {
914
+ return `npm run ${config}`;
915
+ }
916
+ return `npm run ${config.command}`;
917
+ }
918
+ static get processing() {
919
+ return {
920
+ async processEvent(context) {
921
+ const { project, isShellMode, shellCommands } = context;
922
+ const npmConfig = project.events.npm;
923
+ const npmCommand = _NpmEvent.getNpmCommand(npmConfig);
924
+ if (isShellMode) {
925
+ shellCommands.push(npmCommand);
926
+ } else {
927
+ const [cmd, ...args] = npmCommand.split(" ");
928
+ spawn6(cmd, args, {
929
+ cwd: project.path.path,
930
+ stdio: "inherit"
931
+ });
932
+ }
933
+ },
934
+ generateShellCommand(context) {
935
+ const npmConfig = context.project.events.npm;
936
+ return [_NpmEvent.getNpmCommand(npmConfig)];
937
+ }
938
+ };
939
+ }
940
+ static get tmux() {
941
+ return {
942
+ getLayoutPriority() {
943
+ return 50;
944
+ },
945
+ contributeToLayout(enabledCommands) {
946
+ if (enabledCommands.includes("claude")) {
947
+ return "three-pane";
948
+ }
949
+ return "two-pane-npm";
950
+ }
951
+ };
952
+ }
953
+ static get help() {
954
+ return {
955
+ usage: 'npm: true | "script" | { command: string, watch?: boolean, auto_restart?: boolean }',
956
+ description: "Run an NPM script in the project directory",
957
+ examples: [
958
+ { config: true, description: "Run npm run dev" },
959
+ { config: "test", description: "Run npm run test" },
960
+ { config: { command: "dev", watch: true }, description: "Run dev with watch mode" }
961
+ ]
962
+ };
963
+ }
964
+ };
965
+ npm_default = NpmEvent;
966
+ }
967
+ });
968
+
969
+ // src/events/registry.ts
970
+ var ALL_EVENTS, EventRegistryClass, EventRegistry;
971
+ var init_registry = __esm({
972
+ "src/events/registry.ts"() {
973
+ "use strict";
974
+ init_cwd();
975
+ init_ide();
976
+ init_web();
977
+ init_claude();
978
+ init_docker();
979
+ init_npm();
980
+ ALL_EVENTS = [CwdEvent, IdeEvent, WebEvent, ClaudeEvent, DockerEvent, NpmEvent];
981
+ EventRegistryClass = class {
982
+ _events = /* @__PURE__ */ new Map();
983
+ _initialized = false;
984
+ /**
985
+ * Initialize the registry by registering all events
986
+ */
987
+ async initialize() {
988
+ if (this._initialized) return;
989
+ this.registerEvents();
990
+ this._initialized = true;
991
+ }
992
+ /**
993
+ * Register all event classes
994
+ */
995
+ registerEvents() {
996
+ for (const EventClass of ALL_EVENTS) {
997
+ if (this.isValidEventClass(EventClass)) {
998
+ this._events.set(EventClass.metadata.name, EventClass);
999
+ }
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Type guard to check if an object is a valid EventHandlerClass
1004
+ */
1005
+ isValidEventClass(obj) {
1006
+ if (typeof obj !== "function" && typeof obj !== "object") return false;
1007
+ if (obj === null) return false;
1008
+ const candidate = obj;
1009
+ return candidate.metadata !== void 0 && typeof candidate.metadata.name === "string" && typeof candidate.metadata.displayName === "string" && candidate.validation !== void 0 && typeof candidate.validation.validateConfig === "function" && candidate.configuration !== void 0 && typeof candidate.configuration.configureInteractive === "function" && candidate.processing !== void 0 && typeof candidate.processing.processEvent === "function";
1010
+ }
1011
+ /**
1012
+ * Get all valid event names from registered events
1013
+ */
1014
+ getValidEventNames() {
1015
+ this.ensureInitialized();
1016
+ return Array.from(this._events.keys());
1017
+ }
1018
+ /**
1019
+ * Get event by name
1020
+ */
1021
+ getEventByName(name) {
1022
+ this.ensureInitialized();
1023
+ return this._events.get(name) ?? null;
1024
+ }
1025
+ /**
1026
+ * Get all events for management UI
1027
+ */
1028
+ getEventsForManageUI() {
1029
+ this.ensureInitialized();
1030
+ const events = [];
1031
+ for (const [name, eventClass] of this._events) {
1032
+ events.push({
1033
+ name: eventClass.metadata.displayName,
1034
+ value: name,
1035
+ description: eventClass.metadata.description
1036
+ });
1037
+ }
1038
+ return events.sort((a, b) => a.name.localeCompare(b.name));
1039
+ }
1040
+ /**
1041
+ * Get events that support tmux integration
1042
+ */
1043
+ getTmuxEnabledEvents() {
1044
+ this.ensureInitialized();
1045
+ const tmuxEvents = [];
1046
+ for (const [name, eventClass] of this._events) {
1047
+ const tmux = eventClass.tmux;
1048
+ if (tmux) {
1049
+ tmuxEvents.push({
1050
+ name,
1051
+ event: eventClass,
1052
+ priority: tmux.getLayoutPriority()
1053
+ });
1054
+ }
1055
+ }
1056
+ return tmuxEvents.sort((a, b) => b.priority - a.priority);
1057
+ }
1058
+ /**
1059
+ * Get all available events with their metadata
1060
+ */
1061
+ getAllEvents() {
1062
+ this.ensureInitialized();
1063
+ const events = [];
1064
+ for (const [name, eventClass] of this._events) {
1065
+ events.push({
1066
+ name,
1067
+ metadata: eventClass.metadata,
1068
+ hasValidation: !!eventClass.validation,
1069
+ hasConfiguration: !!eventClass.configuration,
1070
+ hasProcessing: !!eventClass.processing,
1071
+ hasTmux: !!eventClass.tmux,
1072
+ hasHelp: !!eventClass.help
1073
+ });
1074
+ }
1075
+ return events;
1076
+ }
1077
+ /**
1078
+ * Ensure registry is initialized
1079
+ */
1080
+ ensureInitialized() {
1081
+ if (!this._initialized) {
1082
+ throw new Error("EventRegistry must be initialized before use. Call initialize() first.");
1083
+ }
1084
+ }
1085
+ /**
1086
+ * Clear the registry (useful for testing)
1087
+ */
1088
+ clear() {
1089
+ this._events.clear();
1090
+ this._initialized = false;
1091
+ }
1092
+ };
1093
+ EventRegistry = new EventRegistryClass();
1094
+ }
1095
+ });
1096
+
1097
+ // src/types/constants.ts
1098
+ var IDE_CHOICES;
1099
+ var init_constants = __esm({
1100
+ "src/types/constants.ts"() {
1101
+ "use strict";
1102
+ IDE_CHOICES = [
1103
+ { name: "Visual Studio Code", value: "vscode" },
1104
+ { name: "Visual Studio Code (code)", value: "code" },
1105
+ { name: "IntelliJ IDEA", value: "idea" },
1106
+ { name: "Atom", value: "atom" },
1107
+ { name: "Sublime Text", value: "subl" },
1108
+ { name: "Vim", value: "vim" },
1109
+ { name: "Emacs", value: "emacs" }
1110
+ ];
1111
+ }
1112
+ });
1113
+
1114
+ // src/commands/interactive.ts
1115
+ var interactive_exports = {};
1116
+ __export(interactive_exports, {
1117
+ runInteractive: () => runInteractive
1118
+ });
1119
+ import { select, input as input4, checkbox, confirm as confirm3 } from "@inquirer/prompts";
1120
+ import File3 from "phylo";
1121
+ import deepAssign2 from "deep-assign";
1122
+ async function runInteractive(ctx) {
1123
+ const { config, log, environment, suggestedName } = ctx;
1124
+ showLogo(config);
1125
+ log.log("");
1126
+ const defaultName = suggestedName ?? (environment.$isProjectEnvironment ? environment.project.name : File3.cwd().name);
1127
+ const fromUser = !!suggestedName;
1128
+ await startInteractive(defaultName, fromUser, ctx);
1129
+ }
1130
+ function showLogo(config) {
1131
+ const version = config.get("pkg")?.version ?? "unknown";
1132
+ console.log(
1133
+ ` 8\x1B[2m${" ".repeat(Math.max(15 - version.length - 1, 1)) + "v" + version}\x1B[22m
1134
+ Yb db dP .d8b. 8d8b 8.dP \x1B[92m.d8b. 8d8b.\x1B[0m
1135
+ YbdPYbdP 8' .8 8P 88b \x1B[92m8' .8 8P Y8\x1B[0m
1136
+ YP YP \`Y8P' 8 8 Yb \x1B[92m\`Y8P' 8 8\x1B[0m`
1137
+ );
1138
+ }
1139
+ async function startInteractive(defaultName, fromUser, ctx, showMain = false) {
1140
+ const { log, environment } = ctx;
1141
+ log.debug(`Name '${defaultName}' was${fromUser ? "" : " not"} provided by the user`);
1142
+ const question = getFirstQuestion(defaultName, fromUser, environment, showMain);
1143
+ const action = await select(question);
1144
+ switch (action) {
1145
+ case "exit":
1146
+ return;
1147
+ case "more":
1148
+ await startInteractive(defaultName, fromUser, ctx, true);
195
1149
  return;
196
1150
  case "init-project":
197
1151
  await initProject(defaultName, fromUser, ctx);
@@ -200,17 +1154,17 @@ async function startInteractive(defaultName, fromUser, ctx, showMain = false) {
200
1154
  await initBranch(defaultName, ctx);
201
1155
  return;
202
1156
  case "switch-project":
203
- log.info("Switch to an existing project");
204
- break;
1157
+ await switchProject(ctx);
1158
+ return;
205
1159
  case "switch-branch":
206
- log.info("Switch to an existing branch");
207
- break;
1160
+ await switchBranch(defaultName, ctx);
1161
+ return;
208
1162
  case "manage-projects":
209
- log.info("Manage existing projects");
210
- break;
1163
+ await manageProjects(ctx);
1164
+ return;
211
1165
  case "manage-branches":
212
- log.info("Manage existing branches");
213
- break;
1166
+ await manageBranches(defaultName, ctx);
1167
+ return;
214
1168
  }
215
1169
  }
216
1170
  function getFirstQuestion(defaultName, fromUser, environment, showMain) {
@@ -279,364 +1233,537 @@ async function initProject(defaultName, fromUser, ctx) {
279
1233
  answerFile = defaultBase.join(answerFile.path);
280
1234
  }
281
1235
  try {
282
- const canonical = answerFile.canonicalize();
283
- if (canonical) {
284
- answerFile = canonical;
285
- } else {
286
- answerFile = answerFile.absolutify();
1236
+ const canonical = answerFile.canonicalize();
1237
+ if (canonical) {
1238
+ answerFile = canonical;
1239
+ } else {
1240
+ answerFile = answerFile.absolutify();
1241
+ }
1242
+ } catch {
1243
+ answerFile = answerFile.absolutify();
1244
+ }
1245
+ basePath = answerFile.relativize(defaultBase.path).path;
1246
+ }
1247
+ const ide = await select({
1248
+ message: "What is the IDE?",
1249
+ choices: IDE_CHOICES
1250
+ });
1251
+ const selectedEvents = await checkbox({
1252
+ message: "Which events should take place when opening?",
1253
+ choices: [
1254
+ { name: "Change terminal cwd to project path", value: "cwd", checked: true },
1255
+ { name: "Open project in IDE", value: "ide", checked: true }
1256
+ ]
1257
+ });
1258
+ const events = {
1259
+ cwd: selectedEvents.includes("cwd"),
1260
+ ide: selectedEvents.includes("ide")
1261
+ };
1262
+ const projectConfig = {
1263
+ path: basePath,
1264
+ ide,
1265
+ events
1266
+ };
1267
+ projects[name] = projectConfig;
1268
+ config.set("projects", projects);
1269
+ log.info("Your project has been initialized.");
1270
+ log.info(`Use 'workon ${name}' to start working!`);
1271
+ }
1272
+ async function initBranch(defaultName, ctx) {
1273
+ const { config, log } = ctx;
1274
+ const projects = config.getProjects();
1275
+ const branch = await input4({
1276
+ message: "What is the name of the branch?",
1277
+ validate: (value) => {
1278
+ if (/\w+#\w+/.test(value)) return `Branch name can't contain the "#" sign`;
1279
+ if (`${defaultName}#${value}` in projects) return "Branch already exists.";
1280
+ return true;
1281
+ }
1282
+ });
1283
+ const branchName = `${defaultName}#${branch}`;
1284
+ const baseProject = projects[defaultName];
1285
+ const { name: _excludedName, ...mergedConfig } = deepAssign2({}, baseProject, {
1286
+ branch
1287
+ });
1288
+ const branchConfig = mergedConfig;
1289
+ projects[branchName] = branchConfig;
1290
+ config.set("projects", projects);
1291
+ log.info("Your branch configuration has been initialized.");
1292
+ log.info(`Use 'workon ${branchName}' to start working!`);
1293
+ }
1294
+ async function switchProject(ctx) {
1295
+ const { config, log } = ctx;
1296
+ const projects = config.getProjects();
1297
+ const baseProjects = Object.keys(projects).filter((name) => !name.includes("#"));
1298
+ if (baseProjects.length === 0) {
1299
+ log.info('No projects configured yet. Use "Start a new project" to create one.');
1300
+ return;
1301
+ }
1302
+ const projectName = await select({
1303
+ message: "Select a project to open:",
1304
+ choices: baseProjects.map((name) => ({
1305
+ name: `${name} (${projects[name].path})`,
1306
+ value: name
1307
+ }))
1308
+ });
1309
+ await openProject(projectName, ctx);
1310
+ }
1311
+ async function switchBranch(projectName, ctx) {
1312
+ const { config, log } = ctx;
1313
+ const projects = config.getProjects();
1314
+ const branchPrefix = `${projectName}#`;
1315
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1316
+ if (branches.length === 0) {
1317
+ log.info(`No branch configurations found for '${projectName}'.`);
1318
+ log.info('Use "Start a branch" to create one.');
1319
+ return;
1320
+ }
1321
+ const branchConfig = await select({
1322
+ message: "Select a branch configuration:",
1323
+ choices: branches.map((name) => ({
1324
+ name: name.substring(branchPrefix.length),
1325
+ value: name
1326
+ }))
1327
+ });
1328
+ await openProject(branchConfig, ctx);
1329
+ }
1330
+ async function manageProjects(ctx) {
1331
+ const { config } = ctx;
1332
+ const projects = config.getProjects();
1333
+ const hasProjects = Object.keys(projects).length > 0;
1334
+ const choices = [
1335
+ { name: "Create new project", value: "create" },
1336
+ ...hasProjects ? [
1337
+ { name: "Edit project", value: "edit" },
1338
+ { name: "Delete project", value: "delete" },
1339
+ { name: "List projects", value: "list" }
1340
+ ] : [],
1341
+ { name: "Back", value: "back" }
1342
+ ];
1343
+ const action = await select({
1344
+ message: "Manage projects:",
1345
+ choices
1346
+ });
1347
+ switch (action) {
1348
+ case "create":
1349
+ await createProjectManage(ctx);
1350
+ break;
1351
+ case "edit":
1352
+ await editProjectManage(ctx);
1353
+ break;
1354
+ case "delete":
1355
+ await deleteProjectManage(ctx);
1356
+ break;
1357
+ case "list":
1358
+ listProjectsManage(ctx);
1359
+ break;
1360
+ case "back":
1361
+ return;
1362
+ }
1363
+ await manageProjects(ctx);
1364
+ }
1365
+ async function manageBranches(projectName, ctx) {
1366
+ const { config } = ctx;
1367
+ const projects = config.getProjects();
1368
+ const branchPrefix = `${projectName}#`;
1369
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1370
+ const choices = [
1371
+ { name: "Create new branch config", value: "create" },
1372
+ ...branches.length > 0 ? [
1373
+ { name: "Edit branch config", value: "edit" },
1374
+ { name: "Delete branch config", value: "delete" },
1375
+ { name: "List branch configs", value: "list" }
1376
+ ] : [],
1377
+ { name: "Back", value: "back" }
1378
+ ];
1379
+ const action = await select({
1380
+ message: `Manage branches for '${projectName}':`,
1381
+ choices
1382
+ });
1383
+ switch (action) {
1384
+ case "create":
1385
+ await initBranch(projectName, ctx);
1386
+ break;
1387
+ case "edit":
1388
+ await editBranchManage(projectName, ctx);
1389
+ break;
1390
+ case "delete":
1391
+ await deleteBranchManage(projectName, ctx);
1392
+ break;
1393
+ case "list":
1394
+ listBranchesManage(projectName, ctx);
1395
+ break;
1396
+ case "back":
1397
+ return;
1398
+ }
1399
+ await manageBranches(projectName, ctx);
1400
+ }
1401
+ async function openProject(projectName, ctx) {
1402
+ const { config, log } = ctx;
1403
+ const projects = config.getProjects();
1404
+ if (!(projectName in projects)) {
1405
+ log.error(`Project '${projectName}' not found.`);
1406
+ return;
1407
+ }
1408
+ const projectConfig = projects[projectName];
1409
+ const projectCfg = { ...projectConfig, name: projectName };
1410
+ const projectEnv = ProjectEnvironment.load(projectCfg, config.getDefaults());
1411
+ log.info(`Opening project '${projectName}'...`);
1412
+ const events = Object.keys(projectConfig.events).filter(
1413
+ (e) => projectConfig.events[e]
1414
+ );
1415
+ for (const event of events) {
1416
+ const eventHandler = EventRegistry.getEventByName(event);
1417
+ if (eventHandler) {
1418
+ await eventHandler.processing.processEvent({
1419
+ project: projectEnv.project,
1420
+ isShellMode: false,
1421
+ shellCommands: []
1422
+ });
1423
+ }
1424
+ }
1425
+ }
1426
+ async function createProjectManage(ctx) {
1427
+ const { config, log } = ctx;
1428
+ const defaults = config.getDefaults();
1429
+ const projects = config.getProjects();
1430
+ const name = await input4({
1431
+ message: "Project name:",
1432
+ validate: (value) => {
1433
+ if (!value.trim()) return "Name is required";
1434
+ if (!/^[\w-]+$/.test(value))
1435
+ return "Name can only contain letters, numbers, underscores, and hyphens";
1436
+ if (value in projects) return "Project already exists";
1437
+ return true;
1438
+ }
1439
+ });
1440
+ const defaultPath = defaults?.base ? File3.from(defaults.base).join(name).path : name;
1441
+ const pathInput = await input4({
1442
+ message: "Project path:",
1443
+ default: defaultPath
1444
+ });
1445
+ let relativePath = pathInput;
1446
+ if (defaults?.base) {
1447
+ const baseDir = File3.from(defaults.base);
1448
+ const pathFile = File3.from(pathInput);
1449
+ try {
1450
+ if (pathFile.isAbsolute()) {
1451
+ relativePath = pathFile.relativize(baseDir.path).path;
287
1452
  }
288
1453
  } catch {
289
- answerFile = answerFile.absolutify();
1454
+ relativePath = pathInput;
290
1455
  }
291
- basePath = answerFile.relativize(defaultBase.path).path;
292
1456
  }
293
1457
  const ide = await select({
294
- message: "What is the IDE?",
1458
+ message: "Select IDE:",
295
1459
  choices: IDE_CHOICES
296
1460
  });
1461
+ const availableEvents = EventRegistry.getEventsForManageUI();
297
1462
  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
- ]
1463
+ message: "Select events to enable:",
1464
+ choices: availableEvents.map((e) => ({
1465
+ name: `${e.name} - ${e.description}`,
1466
+ value: e.value,
1467
+ checked: e.value === "cwd" || e.value === "ide"
1468
+ }))
303
1469
  });
304
- const events = {
305
- cwd: selectedEvents.includes("cwd"),
306
- ide: selectedEvents.includes("ide")
307
- };
1470
+ const events = {};
1471
+ for (const eventName of selectedEvents) {
1472
+ const eventHandler = EventRegistry.getEventByName(eventName);
1473
+ if (eventHandler) {
1474
+ const eventConfig = await eventHandler.configuration.configureInteractive();
1475
+ events[eventName] = eventConfig;
1476
+ }
1477
+ }
308
1478
  const projectConfig = {
309
- path: basePath,
1479
+ path: relativePath,
310
1480
  ide,
311
1481
  events
312
1482
  };
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!`);
1483
+ const confirmed = await confirm3({
1484
+ message: "Save this project?",
1485
+ default: true
1486
+ });
1487
+ if (confirmed) {
1488
+ config.setProject(name, projectConfig);
1489
+ log.info(`Project '${name}' created successfully!`);
1490
+ }
317
1491
  }
318
- async function initBranch(defaultName, ctx) {
1492
+ async function editProjectManage(ctx) {
319
1493
  const { config, log } = ctx;
320
1494
  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
- ];
347
- }
348
- });
349
-
350
- // src/commands/index.ts
351
- import { Command as Command8 } from "commander";
352
- import { readFileSync as readFileSync2, existsSync as existsSync2 } 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 File7 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
- });
1495
+ const defaults = config.getDefaults();
1496
+ const baseProjects = Object.keys(projects).filter((name2) => !name2.includes("#"));
1497
+ if (baseProjects.length === 0) {
1498
+ log.info("No projects to edit.");
1499
+ return;
369
1500
  }
370
- get(key, defaultValue) {
371
- const rootKey = key.split(".")[0];
372
- if (TRANSIENT_PROPS.includes(rootKey)) {
373
- return this._transient[key] ?? defaultValue;
1501
+ const name = await select({
1502
+ message: "Select project to edit:",
1503
+ choices: baseProjects.map((n) => ({ name: n, value: n }))
1504
+ });
1505
+ const project = projects[name];
1506
+ const pathInput = await input4({
1507
+ message: "Project path:",
1508
+ default: project.path
1509
+ });
1510
+ let relativePath = pathInput;
1511
+ if (defaults?.base) {
1512
+ const baseDir = File3.from(defaults.base);
1513
+ const pathFile = File3.from(pathInput);
1514
+ try {
1515
+ if (pathFile.isAbsolute()) {
1516
+ relativePath = pathFile.relativize(baseDir.path).path;
1517
+ }
1518
+ } catch {
1519
+ relativePath = pathInput;
374
1520
  }
375
- return this._store.get(key, defaultValue);
376
1521
  }
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);
1522
+ const ide = await select({
1523
+ message: "Select IDE:",
1524
+ choices: IDE_CHOICES,
1525
+ default: project.ide || "vscode"
1526
+ });
1527
+ const keepEvents = await confirm3({
1528
+ message: "Keep existing event configuration?",
1529
+ default: true
1530
+ });
1531
+ let events = project.events;
1532
+ if (!keepEvents) {
1533
+ const availableEvents = EventRegistry.getEventsForManageUI();
1534
+ const currentEvents = Object.keys(project.events);
1535
+ const selectedEvents = await checkbox({
1536
+ message: "Select events to enable:",
1537
+ choices: availableEvents.map((e) => ({
1538
+ name: `${e.name} - ${e.description}`,
1539
+ value: e.value,
1540
+ checked: currentEvents.includes(e.value)
1541
+ }))
1542
+ });
1543
+ events = {};
1544
+ for (const eventName of selectedEvents) {
1545
+ if (project.events[eventName]) {
1546
+ events[eventName] = project.events[eventName];
384
1547
  } else {
385
- this._store.set(key, value);
1548
+ const eventHandler = EventRegistry.getEventByName(eventName);
1549
+ if (eventHandler) {
1550
+ const eventConfig = await eventHandler.configuration.configureInteractive();
1551
+ events[eventName] = eventConfig;
1552
+ }
386
1553
  }
387
1554
  }
388
1555
  }
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;
1556
+ const updatedConfig = {
1557
+ path: relativePath,
1558
+ ide,
1559
+ events
1560
+ };
1561
+ const confirmed = await confirm3({
1562
+ message: "Save changes?",
1563
+ default: true
1564
+ });
1565
+ if (confirmed) {
1566
+ config.setProject(name, updatedConfig);
1567
+ log.info(`Project '${name}' updated successfully!`);
429
1568
  }
430
- get store() {
431
- return this._store.store;
1569
+ }
1570
+ async function deleteProjectManage(ctx) {
1571
+ const { config, log } = ctx;
1572
+ const projects = config.getProjects();
1573
+ const baseProjects = Object.keys(projects).filter((name2) => !name2.includes("#"));
1574
+ if (baseProjects.length === 0) {
1575
+ log.info("No projects to delete.");
1576
+ return;
432
1577
  }
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;
1578
+ const name = await select({
1579
+ message: "Select project to delete:",
1580
+ choices: baseProjects.map((n) => ({ name: n, value: n }))
1581
+ });
1582
+ const branchPrefix = `${name}#`;
1583
+ const branches = Object.keys(projects).filter((n) => n.startsWith(branchPrefix));
1584
+ if (branches.length > 0) {
1585
+ log.warn(`This project has ${branches.length} branch configuration(s).`);
1586
+ const deleteAll = await confirm3({
1587
+ message: "Delete all branch configurations as well?",
1588
+ default: false
1589
+ });
1590
+ if (deleteAll) {
1591
+ for (const branch of branches) {
1592
+ config.deleteProject(branch);
1593
+ }
474
1594
  }
475
1595
  }
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();
1596
+ const confirmed = await confirm3({
1597
+ message: `Are you sure you want to delete '${name}'?`,
1598
+ default: false
1599
+ });
1600
+ if (confirmed) {
1601
+ config.deleteProject(name);
1602
+ log.info(`Project '${name}' deleted.`);
501
1603
  }
502
- get path() {
503
- if (!this._path) {
504
- throw new Error("Project path not set");
1604
+ }
1605
+ function listProjectsManage(ctx) {
1606
+ const { config } = ctx;
1607
+ const projects = config.getProjects();
1608
+ const defaults = config.getDefaults();
1609
+ console.log("\nConfigured projects:\n");
1610
+ const baseProjects = Object.keys(projects).filter((name) => !name.includes("#"));
1611
+ for (const name of baseProjects) {
1612
+ const project = projects[name];
1613
+ const fullPath = defaults?.base ? File3.from(defaults.base).join(project.path).path : project.path;
1614
+ console.log(` ${name}`);
1615
+ console.log(` Path: ${fullPath}`);
1616
+ console.log(` IDE: ${project.ide || "not set"}`);
1617
+ console.log(` Events: ${Object.keys(project.events).join(", ") || "none"}`);
1618
+ const branchPrefix = `${name}#`;
1619
+ const branches = Object.keys(projects).filter((n) => n.startsWith(branchPrefix));
1620
+ if (branches.length > 0) {
1621
+ console.log(` Branches: ${branches.length}`);
505
1622
  }
506
- return this._path;
507
- }
508
- set branch(branch) {
509
- this._branch = branch;
1623
+ console.log();
510
1624
  }
511
- get branch() {
512
- return this._branch;
1625
+ }
1626
+ async function editBranchManage(projectName, ctx) {
1627
+ const { config, log } = ctx;
1628
+ const projects = config.getProjects();
1629
+ const branchPrefix = `${projectName}#`;
1630
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1631
+ if (branches.length === 0) {
1632
+ log.info("No branch configurations to edit.");
1633
+ return;
513
1634
  }
514
- set homepage(url) {
515
- this._homepage = url;
1635
+ const branchName = await select({
1636
+ message: "Select branch configuration to edit:",
1637
+ choices: branches.map((n) => ({
1638
+ name: n.substring(branchPrefix.length),
1639
+ value: n
1640
+ }))
1641
+ });
1642
+ const branch = projects[branchName];
1643
+ const keepEvents = await confirm3({
1644
+ message: "Keep existing event configuration?",
1645
+ default: true
1646
+ });
1647
+ let events = branch.events;
1648
+ if (!keepEvents) {
1649
+ const availableEvents = EventRegistry.getEventsForManageUI();
1650
+ const currentEvents = Object.keys(branch.events);
1651
+ const selectedEvents = await checkbox({
1652
+ message: "Select events to enable:",
1653
+ choices: availableEvents.map((e) => ({
1654
+ name: `${e.name} - ${e.description}`,
1655
+ value: e.value,
1656
+ checked: currentEvents.includes(e.value)
1657
+ }))
1658
+ });
1659
+ events = {};
1660
+ for (const eventName of selectedEvents) {
1661
+ if (branch.events[eventName]) {
1662
+ events[eventName] = branch.events[eventName];
1663
+ } else {
1664
+ const eventHandler = EventRegistry.getEventByName(eventName);
1665
+ if (eventHandler) {
1666
+ const eventConfig = await eventHandler.configuration.configureInteractive();
1667
+ events[eventName] = eventConfig;
1668
+ }
1669
+ }
1670
+ }
516
1671
  }
517
- get homepage() {
518
- return this._homepage;
1672
+ const updatedConfig = {
1673
+ ...branch,
1674
+ events
1675
+ };
1676
+ const confirmed = await confirm3({
1677
+ message: "Save changes?",
1678
+ default: true
1679
+ });
1680
+ if (confirmed) {
1681
+ config.setProject(branchName, updatedConfig);
1682
+ log.info(`Branch configuration '${branchName}' updated successfully!`);
519
1683
  }
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);
1684
+ }
1685
+ async function deleteBranchManage(projectName, ctx) {
1686
+ const { config, log } = ctx;
1687
+ const projects = config.getProjects();
1688
+ const branchPrefix = `${projectName}#`;
1689
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1690
+ if (branches.length === 0) {
1691
+ log.info("No branch configurations to delete.");
1692
+ return;
533
1693
  }
534
- static load(cfg, defaults) {
535
- const project = new Project(cfg.name, cfg, defaults);
536
- return new _ProjectEnvironment({ ...cfg, name: project.name });
1694
+ const branchName = await select({
1695
+ message: "Select branch configuration to delete:",
1696
+ choices: branches.map((n) => ({
1697
+ name: n.substring(branchPrefix.length),
1698
+ value: n
1699
+ }))
1700
+ });
1701
+ const confirmed = await confirm3({
1702
+ message: `Are you sure you want to delete '${branchName}'?`,
1703
+ default: false
1704
+ });
1705
+ if (confirmed) {
1706
+ config.deleteProject(branchName);
1707
+ log.info(`Branch configuration '${branchName}' deleted.`);
537
1708
  }
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 {
571
- }
572
- }
573
- return this.getProjectEnvironment(base, matching);
1709
+ }
1710
+ function listBranchesManage(projectName, ctx) {
1711
+ const { config } = ctx;
1712
+ const projects = config.getProjects();
1713
+ const branchPrefix = `${projectName}#`;
1714
+ const branches = Object.keys(projects).filter((name) => name.startsWith(branchPrefix));
1715
+ console.log(`
1716
+ Branch configurations for '${projectName}':
1717
+ `);
1718
+ for (const branchName of branches) {
1719
+ const branch = projects[branchName];
1720
+ const shortName = branchName.substring(branchPrefix.length);
1721
+ console.log(` ${shortName}`);
1722
+ console.log(` Events: ${Object.keys(branch.events).join(", ") || "none"}`);
1723
+ console.log();
574
1724
  }
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: () => {
626
- }
627
- };
628
- this.configured = true;
629
- }
1725
+ }
1726
+ var init_interactive = __esm({
1727
+ "src/commands/interactive.ts"() {
1728
+ "use strict";
1729
+ init_environment();
1730
+ init_registry();
1731
+ init_constants();
630
1732
  }
631
- };
1733
+ });
1734
+
1735
+ // src/commands/index.ts
1736
+ init_config();
1737
+ init_environment();
1738
+ init_registry();
1739
+ import { Command as Command8 } from "commander";
1740
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
1741
+ import { join, dirname } from "path";
1742
+ import { fileURLToPath } from "url";
1743
+ import loog from "loog";
1744
+ import omelette from "omelette";
1745
+ import File7 from "phylo";
632
1746
 
633
1747
  // src/commands/open.ts
1748
+ init_environment();
634
1749
  import { Command } from "commander";
635
1750
  import File4 from "phylo";
636
1751
 
637
1752
  // src/lib/tmux.ts
638
- import { exec as execCallback, spawn } from "child_process";
1753
+ import { exec as execCallback, spawn as spawn7 } from "child_process";
639
1754
  import { promisify } from "util";
1755
+
1756
+ // src/lib/sanitize.ts
1757
+ function sanitizeForShell(input6) {
1758
+ if (!input6) return "";
1759
+ return input6.replace(/[^a-zA-Z0-9_\-.]/g, "_");
1760
+ }
1761
+ function escapeForSingleQuotes(input6) {
1762
+ if (!input6) return "";
1763
+ return input6.replace(/'/g, "'\\''");
1764
+ }
1765
+
1766
+ // src/lib/tmux.ts
640
1767
  var exec = promisify(execCallback);
641
1768
  var TmuxManager = class {
642
1769
  sessionPrefix = "workon-";
@@ -650,755 +1777,176 @@ var TmuxManager = class {
650
1777
  }
651
1778
  async sessionExists(sessionName) {
652
1779
  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
- });
722
- }
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
- }
782
- }
783
- };
784
-
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: []
796
- };
797
- }
798
- static get validation() {
799
- return {
800
- validateConfig(config) {
801
- if (typeof config === "boolean" || config === "true" || config === "false") {
802
- return true;
803
- }
804
- return "cwd config must be a boolean (true/false)";
805
- }
806
- };
807
- }
808
- static get configuration() {
809
- return {
810
- async configureInteractive() {
811
- return true;
812
- },
813
- getDefaultConfig() {
814
- return true;
815
- }
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"
830
- });
831
- }
832
- },
833
- generateShellCommand(context) {
834
- const projectPath = context.project.path.path;
835
- return [`cd "${projectPath}"`];
836
- }
837
- };
838
- }
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
- };
853
-
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
- };
866
- }
867
- static get validation() {
868
- 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
- }
875
- };
876
- }
877
- static get configuration() {
878
- return {
879
- async configureInteractive() {
880
- return true;
881
- },
882
- getDefaultConfig() {
883
- return true;
884
- }
885
- };
886
- }
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}" &`];
906
- }
907
- };
908
- }
909
- static get tmux() {
910
- return null;
911
- }
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
- };
921
- }
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
- };
937
- }
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
- };
947
- }
948
- static get configuration() {
949
- return {
950
- async configureInteractive() {
951
- return true;
952
- },
953
- getDefaultConfig() {
954
- return true;
955
- }
956
- };
957
- }
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";
967
- }
968
- }
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}" &`];
993
- }
994
- };
995
- }
996
- static get tmux() {
997
- return null;
998
- }
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
- };
1008
- }
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
- };
1024
- }
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";
1052
- }
1053
- };
1054
- }
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;
1076
- }
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
- }
1085
- };
1086
- }
1087
- static getClaudeCommand(config) {
1088
- if (typeof config === "boolean" || config === void 0) {
1089
- return "claude";
1780
+ await exec(`tmux has-session -t '${escapeForSingleQuotes(sessionName)}'`);
1781
+ return true;
1782
+ } catch {
1783
+ return false;
1090
1784
  }
1091
- const flags = config.flags || [];
1092
- return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
1093
1785
  }
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
- };
1786
+ getSessionName(projectName) {
1787
+ return `${this.sessionPrefix}${sanitizeForShell(projectName)}`;
1115
1788
  }
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";
1126
- }
1127
- };
1789
+ async killSession(sessionName) {
1790
+ try {
1791
+ await exec(`tmux kill-session -t '${escapeForSingleQuotes(sessionName)}'`);
1792
+ return true;
1793
+ } catch {
1794
+ return false;
1795
+ }
1128
1796
  }
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
- };
1797
+ async createSplitSession(projectName, projectPath, claudeArgs = []) {
1798
+ const sessionName = this.getSessionName(projectName);
1799
+ const escapedSession = escapeForSingleQuotes(sessionName);
1800
+ const escapedPath = escapeForSingleQuotes(projectPath);
1801
+ if (await this.sessionExists(sessionName)) {
1802
+ await this.killSession(sessionName);
1803
+ }
1804
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1805
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1806
+ await exec(
1807
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
1808
+ );
1809
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
1810
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
1811
+ return sessionName;
1142
1812
  }
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
- };
1813
+ async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
1814
+ const sessionName = this.getSessionName(projectName);
1815
+ const escapedSession = escapeForSingleQuotes(sessionName);
1816
+ const escapedPath = escapeForSingleQuotes(projectPath);
1817
+ if (await this.sessionExists(sessionName)) {
1818
+ await this.killSession(sessionName);
1819
+ }
1820
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1821
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1822
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1823
+ await exec(
1824
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`
1825
+ );
1826
+ await exec(`tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`);
1827
+ await exec(
1828
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`
1829
+ );
1830
+ await exec(`tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`);
1831
+ await exec(`tmux resize-pane -t '${escapedSession}:0.2' -y 10`);
1832
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
1833
+ return sessionName;
1158
1834
  }
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
- };
1835
+ async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
1836
+ const sessionName = this.getSessionName(projectName);
1837
+ const escapedSession = escapeForSingleQuotes(sessionName);
1838
+ const escapedPath = escapeForSingleQuotes(projectPath);
1839
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1840
+ if (await this.sessionExists(sessionName)) {
1841
+ await this.killSession(sessionName);
1842
+ }
1843
+ await exec(`tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`);
1844
+ await exec(
1845
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`
1846
+ );
1847
+ await exec(`tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`);
1848
+ await exec(`tmux select-pane -t '${escapedSession}:0.0'`);
1849
+ return sessionName;
1188
1850
  }
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"
1851
+ async attachToSession(sessionName) {
1852
+ const escapedSession = escapeForSingleQuotes(sessionName);
1853
+ if (process.env.TMUX) {
1854
+ await exec(`tmux switch-client -t '${escapedSession}'`);
1855
+ } else {
1856
+ const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
1857
+ const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
1858
+ if (useiTermIntegration) {
1859
+ spawn7("tmux", ["-CC", "attach-session", "-t", sessionName], {
1860
+ stdio: "inherit",
1861
+ detached: true
1195
1862
  });
1196
- const servicesInput = await input2({
1197
- message: "Enter services to start (comma-separated, leave empty for all):",
1198
- default: ""
1863
+ } else {
1864
+ spawn7("tmux", ["attach-session", "-t", sessionName], {
1865
+ stdio: "inherit",
1866
+ detached: true
1199
1867
  });
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;
1206
- }
1207
- return {
1208
- compose_file: composeFile,
1209
- services
1210
- };
1211
- },
1212
- getDefaultConfig() {
1213
- return { compose_file: "docker-compose.yml" };
1214
1868
  }
1215
- };
1216
- }
1217
- static getDockerCommand(config) {
1218
- if (typeof config === "boolean" || config === void 0) {
1219
- return "docker-compose up -d";
1220
1869
  }
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
1870
  }
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
- };
1871
+ buildShellCommands(projectName, projectPath, claudeArgs = []) {
1872
+ const sessionName = this.getSessionName(projectName);
1873
+ const escapedSession = escapeForSingleQuotes(sessionName);
1874
+ const escapedPath = escapeForSingleQuotes(projectPath);
1875
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1876
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1877
+ return [
1878
+ `# Create tmux split session for ${sanitizeForShell(projectName)}`,
1879
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
1880
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
1881
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
1882
+ `tmux select-pane -t '${escapedSession}:0.0'`,
1883
+ this.getAttachCommand(sessionName)
1884
+ ];
1251
1885
  }
1252
- static get tmux() {
1253
- return null;
1886
+ buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
1887
+ const sessionName = this.getSessionName(projectName);
1888
+ const escapedSession = escapeForSingleQuotes(sessionName);
1889
+ const escapedPath = escapeForSingleQuotes(projectPath);
1890
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1891
+ const escapedClaudeCmd = escapeForSingleQuotes(claudeCommand);
1892
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1893
+ return [
1894
+ `# Create tmux three-pane session for ${sanitizeForShell(projectName)}`,
1895
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
1896
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}' '${escapedClaudeCmd}'`,
1897
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}'`,
1898
+ `tmux split-window -v -t '${escapedSession}:0.1' -c '${escapedPath}' '${escapedNpmCmd}'`,
1899
+ `tmux set-option -t '${escapedSession}:0.2' remain-on-exit on`,
1900
+ `tmux resize-pane -t '${escapedSession}:0.2' -y 10`,
1901
+ `tmux select-pane -t '${escapedSession}:0.0'`,
1902
+ this.getAttachCommand(sessionName)
1903
+ ];
1254
1904
  }
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
- };
1905
+ buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = "npm run dev") {
1906
+ const sessionName = this.getSessionName(projectName);
1907
+ const escapedSession = escapeForSingleQuotes(sessionName);
1908
+ const escapedPath = escapeForSingleQuotes(projectPath);
1909
+ const escapedNpmCmd = escapeForSingleQuotes(npmCommand);
1910
+ return [
1911
+ `# Create tmux two-pane session with npm for ${sanitizeForShell(projectName)}`,
1912
+ `tmux has-session -t '${escapedSession}' 2>/dev/null && tmux kill-session -t '${escapedSession}'`,
1913
+ `tmux new-session -d -s '${escapedSession}' -c '${escapedPath}'`,
1914
+ `tmux split-window -h -t '${escapedSession}' -c '${escapedPath}' '${escapedNpmCmd}'`,
1915
+ `tmux set-option -t '${escapedSession}:0.1' remain-on-exit on`,
1916
+ `tmux select-pane -t '${escapedSession}:0.0'`,
1917
+ this.getAttachCommand(sessionName)
1918
+ ];
1268
1919
  }
1269
- };
1270
-
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
- }
1920
+ getAttachCommand(sessionName) {
1921
+ const escapedSession = escapeForSingleQuotes(sessionName);
1922
+ if (process.env.TMUX) {
1923
+ return `tmux switch-client -t '${escapedSession}'`;
1924
+ }
1925
+ const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || process.env.ITERM_SESSION_ID;
1926
+ const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
1927
+ if (useiTermIntegration) {
1928
+ return `tmux -CC attach-session -t '${escapedSession}'`;
1294
1929
  }
1930
+ return `tmux attach-session -t '${escapedSession}'`;
1295
1931
  }
1296
- /**
1297
- * Validate if a class is a proper event
1298
- */
1299
- isValidEvent(EventClass) {
1932
+ async listWorkonSessions() {
1300
1933
  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";
1934
+ const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
1935
+ return stdout.trim().split("\n").filter((session) => session.startsWith(this.sessionPrefix)).map((session) => session.replace(this.sessionPrefix, ""));
1304
1936
  } catch {
1305
- return false;
1306
- }
1307
- }
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
- });
1335
- }
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
1351
- });
1352
- }
1353
- }
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
- });
1373
- }
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.");
1937
+ return [];
1382
1938
  }
1383
1939
  }
1384
- /**
1385
- * Clear the registry (useful for testing)
1386
- */
1387
- clear() {
1388
- this._events.clear();
1389
- this._initialized = false;
1390
- }
1391
1940
  };
1392
- var EventRegistry = new EventRegistryClass();
1393
1941
 
1394
1942
  // src/commands/open.ts
1943
+ init_registry();
1395
1944
  function createOpenCommand(ctx) {
1396
1945
  const { config, log } = ctx;
1397
1946
  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) => {
1398
1947
  if (options.debug) {
1399
1948
  log.setLogLevel("debug");
1400
1949
  }
1401
- await EventRegistry.initialize();
1402
1950
  if (projectArg) {
1403
1951
  await processProject(projectArg, options, ctx);
1404
1952
  } else {
@@ -1505,32 +2053,34 @@ function resolveCommandDependencies(requestedCommands, project) {
1505
2053
  }
1506
2054
  return [...new Set(resolved)];
1507
2055
  }
1508
- async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx) {
2056
+ function getClaudeArgs(project) {
2057
+ const claudeConfig = project.events.claude;
2058
+ return typeof claudeConfig === "object" && claudeConfig.flags ? claudeConfig.flags : [];
2059
+ }
2060
+ async function getNpmCommand(project) {
2061
+ const npmConfig = project.events.npm;
2062
+ const { NpmEvent: NpmEvent2 } = await Promise.resolve().then(() => (init_npm(), npm_exports));
2063
+ return NpmEvent2.getNpmCommand(npmConfig);
2064
+ }
2065
+ async function handleTmuxLayout(project, layout, options, shellCommands, events, ctx) {
1509
2066
  const { log } = ctx;
1510
2067
  const tmux = new TmuxManager();
1511
- const claudeConfig = project.events.claude;
1512
- const claudeArgs = typeof claudeConfig === "object" && claudeConfig.flags ? claudeConfig.flags : [];
2068
+ const { isShellMode, dryRun } = options;
1513
2069
  let tmuxHandled = false;
1514
2070
  if (isShellMode) {
1515
2071
  if (await tmux.isTmuxAvailable()) {
1516
- const commands = tmux.buildShellCommands(project.name, project.path.path, claudeArgs);
2072
+ const commands = buildLayoutShellCommands(tmux, project, layout);
1517
2073
  shellCommands.push(...commands);
1518
2074
  tmuxHandled = true;
1519
2075
  } else {
1520
2076
  log.debug("Tmux not available, falling back to normal mode");
1521
- shellCommands.push(`cd "${project.path.path}"`);
1522
- const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
1523
- shellCommands.push(claudeCommand);
2077
+ buildFallbackCommands(shellCommands, project, layout);
1524
2078
  tmuxHandled = true;
1525
2079
  }
1526
2080
  } else if (!dryRun) {
1527
2081
  if (await tmux.isTmuxAvailable()) {
1528
2082
  try {
1529
- const sessionName = await tmux.createSplitSession(
1530
- project.name,
1531
- project.path.path,
1532
- claudeArgs
1533
- );
2083
+ const sessionName = await createTmuxSession(tmux, project, layout);
1534
2084
  await tmux.attachToSession(sessionName);
1535
2085
  tmuxHandled = true;
1536
2086
  } catch (error) {
@@ -1540,139 +2090,103 @@ async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands,
1540
2090
  log.debug("Tmux not available, falling back to normal event processing");
1541
2091
  }
1542
2092
  } else {
1543
- log.info(`Would create split tmux session '${project.name}' with Claude`);
2093
+ log.info(layout.dryRunMessage);
1544
2094
  tmuxHandled = true;
1545
2095
  }
1546
2096
  if (!tmuxHandled && !dryRun) {
1547
- for (const event of events.filter((e) => ["cwd", "claude"].includes(e))) {
2097
+ for (const event of events.filter((e) => layout.handledEvents.includes(e))) {
1548
2098
  await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1549
2099
  }
1550
2100
  }
1551
2101
  if (!dryRun) {
1552
- for (const event of events.filter((e) => !["cwd", "claude"].includes(e))) {
2102
+ for (const event of events.filter((e) => !layout.handledEvents.includes(e))) {
1553
2103
  await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1554
2104
  }
1555
2105
  }
1556
2106
  }
1557
- async function handleThreePaneLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
1558
- const { log } = ctx;
1559
- const tmux = new TmuxManager();
1560
- const claudeConfig = project.events.claude;
1561
- const claudeArgs = typeof claudeConfig === "object" && claudeConfig.flags ? claudeConfig.flags : [];
1562
- const npmConfig = project.events.npm;
1563
- const { NpmEvent: NpmEvent2 } = await Promise.resolve().then(() => (init_npm(), npm_exports));
1564
- const npmCommand = NpmEvent2.getNpmCommand(npmConfig);
1565
- let tmuxHandled = false;
1566
- if (isShellMode) {
1567
- if (await tmux.isTmuxAvailable()) {
1568
- const commands = tmux.buildThreePaneShellCommands(
2107
+ function buildLayoutShellCommands(tmux, project, layout) {
2108
+ switch (layout.type) {
2109
+ case "split-claude":
2110
+ return tmux.buildShellCommands(project.name, project.path.path, layout.claudeArgs);
2111
+ case "three-pane":
2112
+ return tmux.buildThreePaneShellCommands(
1569
2113
  project.name,
1570
2114
  project.path.path,
1571
- claudeArgs,
1572
- npmCommand
2115
+ layout.claudeArgs,
2116
+ layout.npmCommand
1573
2117
  );
1574
- shellCommands.push(...commands);
1575
- tmuxHandled = true;
1576
- } else {
1577
- log.debug("Tmux not available, falling back to normal mode");
1578
- shellCommands.push(`cd "${project.path.path}"`);
1579
- shellCommands.push(claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude");
1580
- shellCommands.push(npmCommand);
1581
- tmuxHandled = true;
1582
- }
1583
- } else if (!dryRun) {
1584
- if (await tmux.isTmuxAvailable()) {
1585
- try {
1586
- const sessionName = await tmux.createThreePaneSession(
1587
- project.name,
1588
- project.path.path,
1589
- claudeArgs,
1590
- npmCommand
1591
- );
1592
- await tmux.attachToSession(sessionName);
1593
- tmuxHandled = true;
1594
- } catch (error) {
1595
- log.debug(`Failed to create tmux session: ${error.message}`);
1596
- }
1597
- } else {
1598
- log.debug("Tmux not available, falling back to normal event processing");
1599
- }
1600
- } else {
1601
- log.info(`Would create three-pane tmux session '${project.name}' with Claude and NPM`);
1602
- tmuxHandled = true;
2118
+ case "two-pane-npm":
2119
+ return tmux.buildTwoPaneNpmShellCommands(project.name, project.path.path, layout.npmCommand);
1603
2120
  }
1604
- if (!tmuxHandled && !dryRun) {
1605
- for (const event of events.filter((e) => ["cwd", "claude", "npm"].includes(e))) {
1606
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1607
- }
2121
+ }
2122
+ function buildFallbackCommands(shellCommands, project, layout) {
2123
+ shellCommands.push(`cd "${project.path.path}"`);
2124
+ if (layout.type === "split-claude" || layout.type === "three-pane") {
2125
+ const claudeCommand = layout.claudeArgs.length > 0 ? `claude ${layout.claudeArgs.join(" ")}` : "claude";
2126
+ shellCommands.push(claudeCommand);
1608
2127
  }
1609
- if (!dryRun) {
1610
- for (const event of events.filter((e) => !["cwd", "claude", "npm"].includes(e))) {
1611
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1612
- }
2128
+ if (layout.npmCommand) {
2129
+ shellCommands.push(layout.npmCommand);
1613
2130
  }
1614
2131
  }
1615
- async function handleTwoPaneNpmLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
1616
- const { log } = ctx;
1617
- const tmux = new TmuxManager();
1618
- const npmConfig = project.events.npm;
1619
- const { NpmEvent: NpmEvent2 } = await Promise.resolve().then(() => (init_npm(), npm_exports));
1620
- const npmCommand = NpmEvent2.getNpmCommand(npmConfig);
1621
- let tmuxHandled = false;
1622
- if (isShellMode) {
1623
- if (await tmux.isTmuxAvailable()) {
1624
- const commands = tmux.buildTwoPaneNpmShellCommands(
2132
+ async function createTmuxSession(tmux, project, layout) {
2133
+ switch (layout.type) {
2134
+ case "split-claude":
2135
+ return tmux.createSplitSession(project.name, project.path.path, layout.claudeArgs);
2136
+ case "three-pane":
2137
+ return tmux.createThreePaneSession(
1625
2138
  project.name,
1626
2139
  project.path.path,
1627
- npmCommand
2140
+ layout.claudeArgs,
2141
+ layout.npmCommand
1628
2142
  );
1629
- shellCommands.push(...commands);
1630
- tmuxHandled = true;
1631
- } else {
1632
- log.debug("Tmux not available, falling back to normal mode");
1633
- shellCommands.push(`cd "${project.path.path}"`);
1634
- shellCommands.push(npmCommand);
1635
- tmuxHandled = true;
1636
- }
1637
- } else if (!dryRun) {
1638
- if (await tmux.isTmuxAvailable()) {
1639
- try {
1640
- const sessionName = await tmux.createTwoPaneNpmSession(
1641
- project.name,
1642
- project.path.path,
1643
- npmCommand
1644
- );
1645
- await tmux.attachToSession(sessionName);
1646
- tmuxHandled = true;
1647
- } catch (error) {
1648
- log.debug(`Failed to create tmux session: ${error.message}`);
1649
- }
1650
- } else {
1651
- log.debug("Tmux not available, falling back to normal event processing");
1652
- }
1653
- } else {
1654
- log.info(`Would create two-pane tmux session '${project.name}' with NPM`);
1655
- tmuxHandled = true;
1656
- }
1657
- if (!tmuxHandled && !dryRun) {
1658
- for (const event of events.filter((e) => ["cwd", "npm"].includes(e))) {
1659
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1660
- }
1661
- }
1662
- if (!dryRun) {
1663
- for (const event of events.filter((e) => !["cwd", "npm"].includes(e))) {
1664
- await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1665
- }
2143
+ case "two-pane-npm":
2144
+ return tmux.createTwoPaneNpmSession(project.name, project.path.path, layout.npmCommand);
1666
2145
  }
1667
2146
  }
2147
+ async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx) {
2148
+ const layout = {
2149
+ type: "split-claude",
2150
+ handledEvents: ["cwd", "claude"],
2151
+ dryRunMessage: `Would create split tmux session '${project.name}' with Claude`,
2152
+ claudeArgs: getClaudeArgs(project),
2153
+ npmCommand: null
2154
+ };
2155
+ await handleTmuxLayout(project, layout, { isShellMode, dryRun }, shellCommands, events, ctx);
2156
+ }
2157
+ async function handleThreePaneLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
2158
+ const layout = {
2159
+ type: "three-pane",
2160
+ handledEvents: ["cwd", "claude", "npm"],
2161
+ dryRunMessage: `Would create three-pane tmux session '${project.name}' with Claude and NPM`,
2162
+ claudeArgs: getClaudeArgs(project),
2163
+ npmCommand: await getNpmCommand(project)
2164
+ };
2165
+ await handleTmuxLayout(project, layout, { isShellMode, dryRun }, shellCommands, events, ctx);
2166
+ }
2167
+ async function handleTwoPaneNpmLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
2168
+ const layout = {
2169
+ type: "two-pane-npm",
2170
+ handledEvents: ["cwd", "npm"],
2171
+ dryRunMessage: `Would create two-pane tmux session '${project.name}' with NPM`,
2172
+ claudeArgs: [],
2173
+ npmCommand: await getNpmCommand(project)
2174
+ };
2175
+ await handleTmuxLayout(project, layout, { isShellMode, dryRun }, shellCommands, events, ctx);
2176
+ }
1668
2177
  async function processEvent(event, context, ctx) {
1669
2178
  const { log } = ctx;
1670
2179
  log.debug(`Processing event ${event}`);
1671
2180
  const eventHandler = EventRegistry.getEventByName(event);
1672
- if (eventHandler && eventHandler.processing) {
1673
- await eventHandler.processing.processEvent(context);
1674
- } else {
2181
+ if (!eventHandler) {
1675
2182
  log.debug(`No event handler found for: ${event}`);
2183
+ return;
2184
+ }
2185
+ try {
2186
+ await eventHandler.processing.processEvent(context);
2187
+ } catch (error) {
2188
+ log.error(`Failed to process event '${event}': ${error.message}`);
2189
+ log.debug(`Event error stack: ${error.stack}`);
1676
2190
  }
1677
2191
  }
1678
2192
  async function showProjectHelp(projectName, ctx) {
@@ -1690,17 +2204,16 @@ Available commands for '${projectName}':`);
1690
2204
  for (const eventName of configuredEvents) {
1691
2205
  const eventHandler = EventRegistry.getEventByName(eventName);
1692
2206
  if (eventHandler) {
1693
- const metadata = eventHandler.metadata;
1694
- const config2 = projectConfig.events[eventName];
2207
+ const eventConfig = projectConfig.events[eventName];
1695
2208
  let configDesc = "";
1696
- if (config2 !== true && config2 !== "true") {
1697
- if (typeof config2 === "object") {
1698
- configDesc = ` (${JSON.stringify(config2)})`;
2209
+ if (eventConfig !== true && eventConfig !== "true") {
2210
+ if (typeof eventConfig === "object") {
2211
+ configDesc = ` (${JSON.stringify(eventConfig)})`;
1699
2212
  } else {
1700
- configDesc = ` (${config2})`;
2213
+ configDesc = ` (${eventConfig})`;
1701
2214
  }
1702
2215
  }
1703
- console.log(` ${eventName.padEnd(8)} - ${metadata.description}${configDesc}`);
2216
+ console.log(` ${eventName.padEnd(8)} - ${eventHandler.metadata.description}${configDesc}`);
1704
2217
  }
1705
2218
  }
1706
2219
  console.log("\nUsage examples:");
@@ -1806,25 +2319,17 @@ function createConfigCommand(ctx) {
1806
2319
  }
1807
2320
 
1808
2321
  // src/commands/manage.ts
2322
+ init_registry();
2323
+ init_constants();
1809
2324
  import { Command as Command6 } from "commander";
1810
- import { select as select2, input as input5, confirm as confirm3, checkbox as checkbox2 } from "@inquirer/prompts";
2325
+ import { select as select2, input as input5, confirm as confirm4, checkbox as checkbox2 } from "@inquirer/prompts";
1811
2326
  import File5 from "phylo";
1812
- var IDE_CHOICES2 = [
1813
- { name: "Visual Studio Code", value: "vscode" },
1814
- { name: "Visual Studio Code (code)", value: "code" },
1815
- { name: "IntelliJ IDEA", value: "idea" },
1816
- { name: "Atom", value: "atom" },
1817
- { name: "Sublime Text", value: "subl" },
1818
- { name: "Vim", value: "vim" },
1819
- { name: "Emacs", value: "emacs" }
1820
- ];
1821
2327
  function createManageCommand(ctx) {
1822
2328
  const { log } = ctx;
1823
2329
  return new Command6("manage").description("Interactive project management").option("-d, --debug", "Enable debug logging").action(async (options) => {
1824
2330
  if (options.debug) {
1825
2331
  log.setLogLevel("debug");
1826
2332
  }
1827
- await EventRegistry.initialize();
1828
2333
  await mainMenu(ctx);
1829
2334
  });
1830
2335
  }
@@ -1906,7 +2411,7 @@ async function createProject(ctx) {
1906
2411
  }
1907
2412
  const ide = await select2({
1908
2413
  message: "Select IDE:",
1909
- choices: IDE_CHOICES2
2414
+ choices: IDE_CHOICES
1910
2415
  });
1911
2416
  const homepage = await input5({
1912
2417
  message: "Project homepage URL (optional):",
@@ -1939,7 +2444,7 @@ async function createProject(ctx) {
1939
2444
  }
1940
2445
  console.log("\nProject configuration:");
1941
2446
  console.log(JSON.stringify(projectConfig, null, 2));
1942
- const confirmed = await confirm3({
2447
+ const confirmed = await confirm4({
1943
2448
  message: "Save this project?",
1944
2449
  default: true
1945
2450
  });
@@ -1983,14 +2488,14 @@ async function editProject(ctx) {
1983
2488
  }
1984
2489
  const ide = await select2({
1985
2490
  message: "Select IDE:",
1986
- choices: IDE_CHOICES2,
2491
+ choices: IDE_CHOICES,
1987
2492
  default: project.ide || "vscode"
1988
2493
  });
1989
2494
  const homepage = await input5({
1990
2495
  message: "Project homepage URL:",
1991
2496
  default: project.homepage || ""
1992
2497
  });
1993
- const keepEvents = await confirm3({
2498
+ const keepEvents = await confirm4({
1994
2499
  message: "Keep existing event configuration?",
1995
2500
  default: true
1996
2501
  });
@@ -2029,7 +2534,7 @@ async function editProject(ctx) {
2029
2534
  }
2030
2535
  console.log("\nUpdated configuration:");
2031
2536
  console.log(JSON.stringify(updatedConfig, null, 2));
2032
- const confirmed = await confirm3({
2537
+ const confirmed = await confirm4({
2033
2538
  message: "Save changes?",
2034
2539
  default: true
2035
2540
  });
@@ -2052,7 +2557,7 @@ async function deleteProject(ctx) {
2052
2557
  message: "Select project to delete:",
2053
2558
  choices: projectNames.map((n) => ({ name: n, value: n }))
2054
2559
  });
2055
- const confirmed = await confirm3({
2560
+ const confirmed = await confirm4({
2056
2561
  message: `Are you sure you want to delete '${name}'?`,
2057
2562
  default: false
2058
2563
  });
@@ -2086,7 +2591,7 @@ import { Command as Command7 } from "commander";
2086
2591
  import { existsSync, readFileSync } from "fs";
2087
2592
  import { basename, resolve } from "path";
2088
2593
  import File6 from "phylo";
2089
- import { confirm as confirm4 } from "@inquirer/prompts";
2594
+ import { confirm as confirm5 } from "@inquirer/prompts";
2090
2595
  function createAddCommand(ctx) {
2091
2596
  const { log } = ctx;
2092
2597
  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(
@@ -2130,7 +2635,7 @@ async function addProject(pathArg, options, ctx) {
2130
2635
  process.exit(1);
2131
2636
  }
2132
2637
  if (projectName in projects && !options.force) {
2133
- const overwrite = await confirm4({
2638
+ const overwrite = await confirm5({
2134
2639
  message: `Project '${projectName}' already exists. Overwrite?`,
2135
2640
  default: false
2136
2641
  });
@@ -2245,32 +2750,42 @@ function createCli() {
2245
2750
  config.set("pkg", packageJson);
2246
2751
  EnvironmentRecognizer.configure(config, log);
2247
2752
  const completion = setupCompletion(config);
2248
- 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) => {
2753
+ 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", async (thisCommand) => {
2249
2754
  const opts = thisCommand.opts();
2250
2755
  if (opts.debug) {
2251
2756
  log.setLogLevel("debug");
2252
2757
  }
2253
- }).action(async (options) => {
2254
- if (options.debug) {
2255
- log.setLogLevel("debug");
2256
- }
2257
- if (options.completion) {
2258
- log.debug("Setting up command-line completion");
2259
- completion.setupShellInitFile();
2260
- return;
2261
- }
2262
- if (options.init) {
2263
- log.debug("Generating shell integration function");
2264
- outputShellInit(program2);
2265
- return;
2758
+ await EventRegistry.initialize();
2759
+ }).action(
2760
+ async (project, options) => {
2761
+ if (options.debug) {
2762
+ log.setLogLevel("debug");
2763
+ }
2764
+ if (options.completion) {
2765
+ log.debug("Setting up command-line completion");
2766
+ completion.setupShellInitFile();
2767
+ return;
2768
+ }
2769
+ if (options.init) {
2770
+ log.debug("Generating shell integration function");
2771
+ outputShellInit(program2);
2772
+ return;
2773
+ }
2774
+ if (project) {
2775
+ const args = ["open", project];
2776
+ if (options.shell) args.push("--shell");
2777
+ if (options.debug) args.push("--debug");
2778
+ await program2.parseAsync(["node", "workon", ...args]);
2779
+ return;
2780
+ }
2781
+ const environment = await EnvironmentRecognizer.recognize(File7.cwd());
2782
+ program2.setOptionValue("_environment", environment);
2783
+ program2.setOptionValue("_config", config);
2784
+ program2.setOptionValue("_log", log);
2785
+ const { runInteractive: runInteractive2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
2786
+ await runInteractive2({ config, log, environment });
2266
2787
  }
2267
- const environment = await EnvironmentRecognizer.recognize(File7.cwd());
2268
- program2.setOptionValue("_environment", environment);
2269
- program2.setOptionValue("_config", config);
2270
- program2.setOptionValue("_log", log);
2271
- const { runInteractive: runInteractive2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
2272
- await runInteractive2({ config, log, environment });
2273
- });
2788
+ );
2274
2789
  program2.setOptionValue("_config", config);
2275
2790
  program2.setOptionValue("_log", log);
2276
2791
  program2.addCommand(createOpenCommand({ config, log }));