workon 2.1.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +19 -4
  2. package/bin/workon +1 -11
  3. package/dist/cli.js +2364 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index.cjs +1216 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +280 -0
  8. package/dist/index.d.ts +280 -0
  9. package/dist/index.js +1173 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +68 -21
  12. package/.claude/settings.local.json +0 -11
  13. package/.cursorindexingignore +0 -3
  14. package/.history/.gitignore_20250806202718 +0 -30
  15. package/.history/.gitignore_20250806231444 +0 -32
  16. package/.history/.gitignore_20250806231450 +0 -32
  17. package/.history/lib/tmux_20250806233103.js +0 -109
  18. package/.history/lib/tmux_20250806233219.js +0 -109
  19. package/.history/lib/tmux_20250806233223.js +0 -109
  20. package/.history/lib/tmux_20250806233230.js +0 -109
  21. package/.history/lib/tmux_20250806233231.js +0 -109
  22. package/.history/lib/tmux_20250807120751.js +0 -190
  23. package/.history/lib/tmux_20250807120757.js +0 -190
  24. package/.history/lib/tmux_20250807120802.js +0 -190
  25. package/.history/lib/tmux_20250807120808.js +0 -190
  26. package/.history/package_20250807114243.json +0 -43
  27. package/.history/package_20250807114257.json +0 -43
  28. package/.history/package_20250807114404.json +0 -43
  29. package/.history/package_20250807114409.json +0 -43
  30. package/.history/package_20250807114510.json +0 -43
  31. package/.history/package_20250807114637.json +0 -43
  32. package/.vscode/launch.json +0 -20
  33. package/.vscode/terminals.json +0 -11
  34. package/CHANGELOG.md +0 -110
  35. package/CLAUDE.md +0 -100
  36. package/cli/base.js +0 -16
  37. package/cli/config/index.js +0 -19
  38. package/cli/config/list.js +0 -26
  39. package/cli/config/set.js +0 -19
  40. package/cli/config/unset.js +0 -26
  41. package/cli/index.js +0 -184
  42. package/cli/interactive.js +0 -290
  43. package/cli/manage.js +0 -413
  44. package/cli/open.js +0 -414
  45. package/commands/base.js +0 -105
  46. package/commands/core/cwd/index.js +0 -86
  47. package/commands/core/ide/index.js +0 -84
  48. package/commands/core/web/index.js +0 -109
  49. package/commands/extensions/claude/index.js +0 -211
  50. package/commands/extensions/docker/index.js +0 -167
  51. package/commands/extensions/npm/index.js +0 -208
  52. package/commands/registry.js +0 -196
  53. package/demo-colon-syntax.js +0 -57
  54. package/docs/adr/001-command-centric-architecture.md +0 -304
  55. package/docs/adr/002-positional-command-arguments.md +0 -402
  56. package/docs/ideas.md +0 -93
  57. package/lib/config.js +0 -51
  58. package/lib/environment/base.js +0 -12
  59. package/lib/environment/index.js +0 -108
  60. package/lib/environment/project.js +0 -26
  61. package/lib/project.js +0 -68
  62. package/lib/tmux.js +0 -223
  63. package/lib/validation.js +0 -120
  64. package/test-architecture.js +0 -145
  65. package/test-colon-syntax.js +0 -85
  66. package/test-registry.js +0 -57
package/dist/index.js ADDED
@@ -0,0 +1,1173 @@
1
+ // src/lib/config.ts
2
+ import Conf from "conf";
3
+ var TRANSIENT_PROPS = ["pkg", "work"];
4
+ var Config = class {
5
+ _transient = {};
6
+ _store;
7
+ constructor() {
8
+ this._store = new Conf({
9
+ projectName: "workon"
10
+ });
11
+ }
12
+ get(key, defaultValue) {
13
+ const rootKey = key.split(".")[0];
14
+ if (TRANSIENT_PROPS.includes(rootKey)) {
15
+ return this._transient[key] ?? defaultValue;
16
+ }
17
+ return this._store.get(key, defaultValue);
18
+ }
19
+ set(key, value) {
20
+ const rootKey = key.split(".")[0];
21
+ if (TRANSIENT_PROPS.includes(rootKey)) {
22
+ this._transient[key] = value;
23
+ } else {
24
+ if (value === void 0) {
25
+ this._store.set(key, value);
26
+ } else {
27
+ this._store.set(key, value);
28
+ }
29
+ }
30
+ }
31
+ has(key) {
32
+ const rootKey = key.split(".")[0];
33
+ if (TRANSIENT_PROPS.includes(rootKey)) {
34
+ return Object.prototype.hasOwnProperty.call(this._transient, key);
35
+ }
36
+ return this._store.has(key);
37
+ }
38
+ delete(key) {
39
+ const rootKey = key.split(".")[0];
40
+ if (TRANSIENT_PROPS.includes(rootKey)) {
41
+ delete this._transient[key];
42
+ } else {
43
+ this._store.delete(key);
44
+ }
45
+ }
46
+ getProjects() {
47
+ return this.get("projects") ?? {};
48
+ }
49
+ getProject(name) {
50
+ const projects = this.getProjects();
51
+ return projects[name];
52
+ }
53
+ setProject(name, config) {
54
+ const projects = this.getProjects();
55
+ projects[name] = config;
56
+ this.set("projects", projects);
57
+ }
58
+ deleteProject(name) {
59
+ const projects = this.getProjects();
60
+ delete projects[name];
61
+ this.set("projects", projects);
62
+ }
63
+ getDefaults() {
64
+ return this.get("project_defaults");
65
+ }
66
+ setDefaults(defaults) {
67
+ this.set("project_defaults", defaults);
68
+ }
69
+ get path() {
70
+ return this._store.path;
71
+ }
72
+ get store() {
73
+ return this._store.store;
74
+ }
75
+ };
76
+
77
+ // src/lib/project.ts
78
+ import File from "phylo";
79
+ import deepAssign from "deep-assign";
80
+ var Project = class {
81
+ name;
82
+ _base;
83
+ _path;
84
+ _ide;
85
+ _events = {};
86
+ _branch;
87
+ _homepage;
88
+ _defaults;
89
+ _initialCfg;
90
+ constructor(name, cfg, defaults) {
91
+ this._defaults = defaults ?? { base: "" };
92
+ this._initialCfg = { path: name, events: {}, ...cfg };
93
+ this.name = cfg?.name ?? name;
94
+ const merged = deepAssign({}, this._defaults, this._initialCfg);
95
+ if (merged.base) {
96
+ this.base = merged.base;
97
+ }
98
+ if (merged.path) {
99
+ this.path = merged.path;
100
+ }
101
+ if (merged.ide) {
102
+ this._ide = merged.ide;
103
+ }
104
+ if (merged.events) {
105
+ this._events = merged.events;
106
+ }
107
+ if (merged.branch) {
108
+ this._branch = merged.branch;
109
+ }
110
+ if (merged.homepage) {
111
+ this._homepage = merged.homepage;
112
+ }
113
+ }
114
+ set base(path) {
115
+ this._base = File.from(path).absolutify();
116
+ }
117
+ get base() {
118
+ return this._base;
119
+ }
120
+ set ide(cmd) {
121
+ this._ide = cmd;
122
+ }
123
+ get ide() {
124
+ return this._ide;
125
+ }
126
+ set events(eventCfg) {
127
+ this._events = eventCfg;
128
+ }
129
+ get events() {
130
+ return this._events;
131
+ }
132
+ set path(path) {
133
+ if (this._base) {
134
+ this._path = this._base.join(path);
135
+ } else {
136
+ this._path = File.from(path);
137
+ }
138
+ this._path = this._path.absolutify();
139
+ }
140
+ get path() {
141
+ if (!this._path) {
142
+ throw new Error("Project path not set");
143
+ }
144
+ return this._path;
145
+ }
146
+ set branch(branch) {
147
+ this._branch = branch;
148
+ }
149
+ get branch() {
150
+ return this._branch;
151
+ }
152
+ set homepage(url) {
153
+ this._homepage = url;
154
+ }
155
+ get homepage() {
156
+ return this._homepage;
157
+ }
158
+ static $isProject = true;
159
+ $isProject = true;
160
+ };
161
+
162
+ // src/lib/tmux.ts
163
+ import { exec as execCallback, spawn } from "child_process";
164
+ import { promisify } from "util";
165
+ var exec = promisify(execCallback);
166
+ var TmuxManager = class {
167
+ sessionPrefix = "workon-";
168
+ async isTmuxAvailable() {
169
+ try {
170
+ await exec("which tmux");
171
+ return true;
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+ async sessionExists(sessionName) {
177
+ try {
178
+ await exec(`tmux has-session -t "${sessionName}"`);
179
+ return true;
180
+ } catch {
181
+ return false;
182
+ }
183
+ }
184
+ getSessionName(projectName) {
185
+ return `${this.sessionPrefix}${projectName}`;
186
+ }
187
+ async killSession(sessionName) {
188
+ try {
189
+ await exec(`tmux kill-session -t "${sessionName}"`);
190
+ return true;
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+ async createSplitSession(projectName, projectPath, claudeArgs = []) {
196
+ const sessionName = this.getSessionName(projectName);
197
+ if (await this.sessionExists(sessionName)) {
198
+ await this.killSession(sessionName);
199
+ }
200
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
201
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
202
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
203
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
204
+ return sessionName;
205
+ }
206
+ async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
207
+ const sessionName = this.getSessionName(projectName);
208
+ if (await this.sessionExists(sessionName)) {
209
+ await this.killSession(sessionName);
210
+ }
211
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
212
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
213
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
214
+ await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
215
+ await exec(`tmux set-option -t "${sessionName}:0.2" remain-on-exit on`);
216
+ await exec(`tmux resize-pane -t "${sessionName}:0.2" -y 10`);
217
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
218
+ return sessionName;
219
+ }
220
+ async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
221
+ const sessionName = this.getSessionName(projectName);
222
+ if (await this.sessionExists(sessionName)) {
223
+ await this.killSession(sessionName);
224
+ }
225
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
226
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
227
+ await exec(`tmux set-option -t "${sessionName}:0.1" remain-on-exit on`);
228
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
229
+ return sessionName;
230
+ }
231
+ async attachToSession(sessionName) {
232
+ if (process.env.TMUX) {
233
+ await exec(`tmux switch-client -t "${sessionName}"`);
234
+ } else {
235
+ const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
236
+ const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
237
+ if (useiTermIntegration) {
238
+ spawn("tmux", ["-CC", "attach-session", "-t", sessionName], {
239
+ stdio: "inherit",
240
+ detached: true
241
+ });
242
+ } else {
243
+ spawn("tmux", ["attach-session", "-t", sessionName], {
244
+ stdio: "inherit",
245
+ detached: true
246
+ });
247
+ }
248
+ }
249
+ }
250
+ buildShellCommands(projectName, projectPath, claudeArgs = []) {
251
+ const sessionName = this.getSessionName(projectName);
252
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
253
+ return [
254
+ `# Create tmux split session for ${projectName}`,
255
+ `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
256
+ `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
257
+ `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
258
+ `tmux select-pane -t "${sessionName}:0.0"`,
259
+ this.getAttachCommand(sessionName)
260
+ ];
261
+ }
262
+ buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
263
+ const sessionName = this.getSessionName(projectName);
264
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
265
+ return [
266
+ `# Create tmux three-pane session for ${projectName}`,
267
+ `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
268
+ `tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
269
+ `tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
270
+ `tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`,
271
+ `tmux set-option -t "${sessionName}:0.2" remain-on-exit on`,
272
+ `tmux resize-pane -t "${sessionName}:0.2" -y 10`,
273
+ `tmux select-pane -t "${sessionName}:0.0"`,
274
+ this.getAttachCommand(sessionName)
275
+ ];
276
+ }
277
+ buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = "npm run dev") {
278
+ const sessionName = this.getSessionName(projectName);
279
+ return [
280
+ `# Create tmux two-pane session with npm for ${projectName}`,
281
+ `tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
282
+ `tmux new-session -d -s "${sessionName}" -c "${projectPath}"`,
283
+ `tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`,
284
+ `tmux set-option -t "${sessionName}:0.1" remain-on-exit on`,
285
+ `tmux select-pane -t "${sessionName}:0.0"`,
286
+ this.getAttachCommand(sessionName)
287
+ ];
288
+ }
289
+ getAttachCommand(sessionName) {
290
+ if (process.env.TMUX) {
291
+ return `tmux switch-client -t "${sessionName}"`;
292
+ }
293
+ const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || process.env.ITERM_SESSION_ID;
294
+ const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
295
+ if (useiTermIntegration) {
296
+ return `tmux -CC attach-session -t "${sessionName}"`;
297
+ }
298
+ return `tmux attach-session -t "${sessionName}"`;
299
+ }
300
+ async listWorkonSessions() {
301
+ try {
302
+ const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
303
+ return stdout.trim().split("\n").filter((session) => session.startsWith(this.sessionPrefix)).map((session) => session.replace(this.sessionPrefix, ""));
304
+ } catch {
305
+ return [];
306
+ }
307
+ }
308
+ };
309
+
310
+ // src/lib/environment.ts
311
+ import File2 from "phylo";
312
+ import { simpleGit } from "simple-git";
313
+ var BaseEnvironment = class {
314
+ $isProjectEnvironment = false;
315
+ };
316
+ var ProjectEnvironment = class _ProjectEnvironment {
317
+ $isProjectEnvironment = true;
318
+ project;
319
+ constructor(projectCfg) {
320
+ this.project = new Project(projectCfg.name, projectCfg);
321
+ }
322
+ static load(cfg, defaults) {
323
+ const project = new Project(cfg.name, cfg, defaults);
324
+ return new _ProjectEnvironment({ ...cfg, name: project.name });
325
+ }
326
+ };
327
+ var EnvironmentRecognizer = class {
328
+ static config;
329
+ static log;
330
+ static projects = [];
331
+ static configured = false;
332
+ static configure(config, log) {
333
+ if (this.configured) {
334
+ return;
335
+ }
336
+ this.config = config;
337
+ this.log = log;
338
+ this.configured = true;
339
+ }
340
+ static async recognize(dir) {
341
+ this.ensureConfigured();
342
+ const theDir = File2.from(dir).canonicalize();
343
+ this.log.debug("Directory to recognize is: " + theDir.canonicalPath());
344
+ const allProjects = this.getAllProjects();
345
+ const matching = allProjects.filter((p) => p.path.canonicalPath() === theDir.path);
346
+ if (matching.length === 0) {
347
+ return new BaseEnvironment();
348
+ }
349
+ this.log.debug(`Found ${matching.length} matching projects`);
350
+ const base = matching.find((p) => !p.name.includes("#")) ?? matching[0];
351
+ this.log.debug("Base project is: " + base.name);
352
+ const gitDir = base.path.up(".git");
353
+ if (gitDir) {
354
+ try {
355
+ const git = simpleGit(gitDir.path);
356
+ const branchSummary = await git.branchLocal();
357
+ base.branch = branchSummary.current;
358
+ } catch {
359
+ }
360
+ }
361
+ return this.getProjectEnvironment(base, matching);
362
+ }
363
+ static getAllProjects(refresh = false) {
364
+ if (this.projects.length > 0 && !refresh) {
365
+ return this.projects;
366
+ }
367
+ const defaults = this.config.getDefaults();
368
+ if (!defaults?.base) {
369
+ this.projects = [];
370
+ return this.projects;
371
+ }
372
+ const baseDir = File2.from(defaults.base);
373
+ const projectsMap = this.config.getProjects();
374
+ this.projects = Object.entries(projectsMap).map(([name, project]) => ({
375
+ ...project,
376
+ name,
377
+ path: baseDir.join(project.path)
378
+ }));
379
+ return this.projects;
380
+ }
381
+ static getProjectEnvironment(base, _matching) {
382
+ const exactName = `${base.name}#${base.branch}`;
383
+ const exactProj = this.projects.find((p) => p.name === exactName);
384
+ const toProjectConfig = (p) => ({
385
+ name: p.name,
386
+ path: p.path.path,
387
+ // Convert PhyloFile to string path
388
+ ide: p.ide,
389
+ homepage: p.homepage,
390
+ events: p.events,
391
+ branch: p.branch,
392
+ exactName
393
+ });
394
+ if (exactProj) {
395
+ return new ProjectEnvironment({ ...toProjectConfig(exactProj), branch: base.branch });
396
+ }
397
+ return new ProjectEnvironment(toProjectConfig(base));
398
+ }
399
+ static ensureConfigured() {
400
+ if (!this.configured) {
401
+ this.config = new Config();
402
+ this.log = {
403
+ debug: () => {
404
+ },
405
+ info: () => {
406
+ },
407
+ log: () => {
408
+ },
409
+ warn: () => {
410
+ },
411
+ error: () => {
412
+ },
413
+ setLogLevel: () => {
414
+ }
415
+ };
416
+ this.configured = true;
417
+ }
418
+ }
419
+ };
420
+
421
+ // src/events/core/cwd.ts
422
+ import { spawn as spawn2 } from "child_process";
423
+ var CwdEvent = class {
424
+ static get metadata() {
425
+ return {
426
+ name: "cwd",
427
+ displayName: "Change directory (cwd)",
428
+ description: "Change current working directory to project path",
429
+ category: "core",
430
+ requiresTmux: false,
431
+ dependencies: []
432
+ };
433
+ }
434
+ static get validation() {
435
+ return {
436
+ validateConfig(config) {
437
+ if (typeof config === "boolean" || config === "true" || config === "false") {
438
+ return true;
439
+ }
440
+ return "cwd config must be a boolean (true/false)";
441
+ }
442
+ };
443
+ }
444
+ static get configuration() {
445
+ return {
446
+ async configureInteractive() {
447
+ return true;
448
+ },
449
+ getDefaultConfig() {
450
+ return true;
451
+ }
452
+ };
453
+ }
454
+ static get processing() {
455
+ return {
456
+ async processEvent(context) {
457
+ const { project, isShellMode, shellCommands } = context;
458
+ const projectPath = project.path.path;
459
+ if (isShellMode) {
460
+ shellCommands.push(`cd "${projectPath}"`);
461
+ } else {
462
+ const shell = process.env.SHELL || "/bin/bash";
463
+ spawn2(shell, [], {
464
+ cwd: projectPath,
465
+ stdio: "inherit"
466
+ });
467
+ }
468
+ },
469
+ generateShellCommand(context) {
470
+ const projectPath = context.project.path.path;
471
+ return [`cd "${projectPath}"`];
472
+ }
473
+ };
474
+ }
475
+ static get tmux() {
476
+ return null;
477
+ }
478
+ static get help() {
479
+ return {
480
+ usage: "cwd: true | false",
481
+ description: "Change the current working directory to the project path",
482
+ examples: [
483
+ { config: true, description: "Enable directory change" },
484
+ { config: false, description: "Disable directory change" }
485
+ ]
486
+ };
487
+ }
488
+ };
489
+
490
+ // src/events/core/ide.ts
491
+ import { spawn as spawn3 } from "child_process";
492
+ var IdeEvent = class {
493
+ static get metadata() {
494
+ return {
495
+ name: "ide",
496
+ displayName: "Open in IDE",
497
+ description: "Open project in configured IDE/editor",
498
+ category: "core",
499
+ requiresTmux: false,
500
+ dependencies: []
501
+ };
502
+ }
503
+ static get validation() {
504
+ return {
505
+ validateConfig(config) {
506
+ if (typeof config === "boolean" || config === "true" || config === "false") {
507
+ return true;
508
+ }
509
+ return "ide config must be a boolean (true/false)";
510
+ }
511
+ };
512
+ }
513
+ static get configuration() {
514
+ return {
515
+ async configureInteractive() {
516
+ return true;
517
+ },
518
+ getDefaultConfig() {
519
+ return true;
520
+ }
521
+ };
522
+ }
523
+ static get processing() {
524
+ return {
525
+ async processEvent(context) {
526
+ const { project, isShellMode, shellCommands } = context;
527
+ const projectPath = project.path.path;
528
+ const ide = project.ide || "code";
529
+ if (isShellMode) {
530
+ shellCommands.push(`${ide} "${projectPath}" &`);
531
+ } else {
532
+ spawn3(ide, [projectPath], {
533
+ detached: true,
534
+ stdio: "ignore"
535
+ }).unref();
536
+ }
537
+ },
538
+ generateShellCommand(context) {
539
+ const projectPath = context.project.path.path;
540
+ const ide = context.project.ide || "code";
541
+ return [`${ide} "${projectPath}" &`];
542
+ }
543
+ };
544
+ }
545
+ static get tmux() {
546
+ return null;
547
+ }
548
+ static get help() {
549
+ return {
550
+ usage: "ide: true | false",
551
+ description: "Open the project in the configured IDE",
552
+ examples: [
553
+ { config: true, description: "Enable IDE opening" },
554
+ { config: false, description: "Disable IDE opening" }
555
+ ]
556
+ };
557
+ }
558
+ };
559
+
560
+ // src/events/core/web.ts
561
+ import { spawn as spawn4 } from "child_process";
562
+ import { platform } from "os";
563
+ var WebEvent = class _WebEvent {
564
+ static get metadata() {
565
+ return {
566
+ name: "web",
567
+ displayName: "Open homepage in browser",
568
+ description: "Open project homepage in web browser",
569
+ category: "core",
570
+ requiresTmux: false,
571
+ dependencies: []
572
+ };
573
+ }
574
+ static get validation() {
575
+ return {
576
+ validateConfig(config) {
577
+ if (typeof config === "boolean" || config === "true" || config === "false") {
578
+ return true;
579
+ }
580
+ return "web config must be a boolean (true/false)";
581
+ }
582
+ };
583
+ }
584
+ static get configuration() {
585
+ return {
586
+ async configureInteractive() {
587
+ return true;
588
+ },
589
+ getDefaultConfig() {
590
+ return true;
591
+ }
592
+ };
593
+ }
594
+ static getOpenCommand() {
595
+ const os = platform();
596
+ switch (os) {
597
+ case "darwin":
598
+ return "open";
599
+ case "win32":
600
+ return "start";
601
+ default:
602
+ return "xdg-open";
603
+ }
604
+ }
605
+ static get processing() {
606
+ return {
607
+ async processEvent(context) {
608
+ const { project, isShellMode, shellCommands } = context;
609
+ const homepage = project.homepage;
610
+ if (!homepage) {
611
+ console.warn("No homepage configured for project");
612
+ return;
613
+ }
614
+ const openCmd = _WebEvent.getOpenCommand();
615
+ if (isShellMode) {
616
+ shellCommands.push(`${openCmd} "${homepage}" &`);
617
+ } else {
618
+ spawn4(openCmd, [homepage], {
619
+ detached: true,
620
+ stdio: "ignore"
621
+ }).unref();
622
+ }
623
+ },
624
+ generateShellCommand(context) {
625
+ const homepage = context.project.homepage;
626
+ if (!homepage) return [];
627
+ const openCmd = _WebEvent.getOpenCommand();
628
+ return [`${openCmd} "${homepage}" &`];
629
+ }
630
+ };
631
+ }
632
+ static get tmux() {
633
+ return null;
634
+ }
635
+ static get help() {
636
+ return {
637
+ usage: "web: true | false",
638
+ description: "Open the project homepage in the default browser",
639
+ examples: [
640
+ { config: true, description: "Enable browser opening" },
641
+ { config: false, description: "Disable browser opening" }
642
+ ]
643
+ };
644
+ }
645
+ };
646
+
647
+ // src/events/extensions/claude.ts
648
+ import { spawn as spawn5 } from "child_process";
649
+ import { input, confirm } from "@inquirer/prompts";
650
+ var ClaudeEvent = class _ClaudeEvent {
651
+ static get metadata() {
652
+ return {
653
+ name: "claude",
654
+ displayName: "Launch Claude Code",
655
+ description: "Launch Claude Code with optional flags and configuration",
656
+ category: "development",
657
+ requiresTmux: true,
658
+ dependencies: ["claude"]
659
+ };
660
+ }
661
+ static get validation() {
662
+ return {
663
+ validateConfig(config) {
664
+ if (typeof config === "boolean" || config === "true" || config === "false") {
665
+ return true;
666
+ }
667
+ if (typeof config === "object" && config !== null) {
668
+ const cfg = config;
669
+ if (cfg.flags !== void 0) {
670
+ if (!Array.isArray(cfg.flags)) {
671
+ return "claude.flags must be an array of strings";
672
+ }
673
+ for (const flag of cfg.flags) {
674
+ if (typeof flag !== "string") {
675
+ return "claude.flags must contain only strings";
676
+ }
677
+ if (!flag.startsWith("-")) {
678
+ return `Invalid flag "${flag}": flags must start with - or --`;
679
+ }
680
+ }
681
+ }
682
+ if (cfg.split_terminal !== void 0 && typeof cfg.split_terminal !== "boolean") {
683
+ return "claude.split_terminal must be a boolean";
684
+ }
685
+ return true;
686
+ }
687
+ return "claude config must be a boolean or object with flags/split_terminal";
688
+ }
689
+ };
690
+ }
691
+ static get configuration() {
692
+ return {
693
+ async configureInteractive() {
694
+ const useAdvanced = await confirm({
695
+ message: "Configure advanced Claude options?",
696
+ default: false
697
+ });
698
+ if (!useAdvanced) {
699
+ return true;
700
+ }
701
+ const flagsInput = await input({
702
+ message: "Enter Claude flags (comma-separated, e.g., --resume, --debug):",
703
+ default: ""
704
+ });
705
+ const flags = flagsInput.split(",").map((f) => f.trim()).filter((f) => f.length > 0 && f.startsWith("-"));
706
+ const splitTerminal = await confirm({
707
+ message: "Use split terminal layout (Claude + shell)?",
708
+ default: true
709
+ });
710
+ if (flags.length === 0 && !splitTerminal) {
711
+ return true;
712
+ }
713
+ const config = {};
714
+ if (flags.length > 0) config.flags = flags;
715
+ if (splitTerminal) config.split_terminal = splitTerminal;
716
+ return config;
717
+ },
718
+ getDefaultConfig() {
719
+ return true;
720
+ }
721
+ };
722
+ }
723
+ static getClaudeCommand(config) {
724
+ if (typeof config === "boolean" || config === void 0) {
725
+ return "claude";
726
+ }
727
+ const flags = config.flags || [];
728
+ return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
729
+ }
730
+ static get processing() {
731
+ return {
732
+ async processEvent(context) {
733
+ const { project, isShellMode, shellCommands } = context;
734
+ const claudeConfig = project.events.claude;
735
+ const claudeCommand = _ClaudeEvent.getClaudeCommand(claudeConfig);
736
+ if (isShellMode) {
737
+ shellCommands.push(claudeCommand);
738
+ } else {
739
+ const args = claudeCommand.split(" ").slice(1);
740
+ spawn5("claude", args, {
741
+ cwd: project.path.path,
742
+ stdio: "inherit"
743
+ });
744
+ }
745
+ },
746
+ generateShellCommand(context) {
747
+ const claudeConfig = context.project.events.claude;
748
+ return [_ClaudeEvent.getClaudeCommand(claudeConfig)];
749
+ }
750
+ };
751
+ }
752
+ static get tmux() {
753
+ return {
754
+ getLayoutPriority() {
755
+ return 100;
756
+ },
757
+ contributeToLayout(enabledCommands) {
758
+ if (enabledCommands.includes("npm")) {
759
+ return "three-pane";
760
+ }
761
+ return "split";
762
+ }
763
+ };
764
+ }
765
+ static get help() {
766
+ return {
767
+ usage: "claude: true | { flags: string[], split_terminal: boolean }",
768
+ description: "Launch Claude Code in the project directory",
769
+ examples: [
770
+ { config: true, description: "Launch Claude with defaults" },
771
+ { config: { flags: ["--resume"] }, description: "Resume previous session" },
772
+ {
773
+ config: { flags: ["--model", "opus"], split_terminal: true },
774
+ description: "Use Opus model with split terminal"
775
+ }
776
+ ]
777
+ };
778
+ }
779
+ };
780
+
781
+ // src/events/extensions/docker.ts
782
+ import { spawn as spawn6 } from "child_process";
783
+ import { input as input2 } from "@inquirer/prompts";
784
+ var DockerEvent = class _DockerEvent {
785
+ static get metadata() {
786
+ return {
787
+ name: "docker",
788
+ displayName: "Docker container management",
789
+ description: "Start/stop Docker containers for the project",
790
+ category: "development",
791
+ requiresTmux: false,
792
+ dependencies: ["docker"]
793
+ };
794
+ }
795
+ static get validation() {
796
+ return {
797
+ validateConfig(config) {
798
+ if (typeof config === "boolean" || config === "true" || config === "false") {
799
+ return true;
800
+ }
801
+ if (typeof config === "string") {
802
+ return true;
803
+ }
804
+ if (typeof config === "object" && config !== null) {
805
+ const cfg = config;
806
+ if (cfg.compose_file !== void 0 && typeof cfg.compose_file !== "string") {
807
+ return "docker.compose_file must be a string";
808
+ }
809
+ if (cfg.services !== void 0) {
810
+ if (!Array.isArray(cfg.services)) {
811
+ return "docker.services must be an array";
812
+ }
813
+ for (const service of cfg.services) {
814
+ if (typeof service !== "string") {
815
+ return "docker.services must contain only strings";
816
+ }
817
+ }
818
+ }
819
+ return true;
820
+ }
821
+ return "docker config must be a boolean, string (compose file), or object";
822
+ }
823
+ };
824
+ }
825
+ static get configuration() {
826
+ return {
827
+ async configureInteractive() {
828
+ const composeFile = await input2({
829
+ message: "Enter docker-compose file path:",
830
+ default: "docker-compose.yml"
831
+ });
832
+ const servicesInput = await input2({
833
+ message: "Enter services to start (comma-separated, leave empty for all):",
834
+ default: ""
835
+ });
836
+ const services = servicesInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
837
+ if (composeFile === "docker-compose.yml" && services.length === 0) {
838
+ return { compose_file: "docker-compose.yml" };
839
+ }
840
+ if (services.length === 0) {
841
+ return composeFile;
842
+ }
843
+ return {
844
+ compose_file: composeFile,
845
+ services
846
+ };
847
+ },
848
+ getDefaultConfig() {
849
+ return { compose_file: "docker-compose.yml" };
850
+ }
851
+ };
852
+ }
853
+ static getDockerCommand(config) {
854
+ if (typeof config === "boolean" || config === void 0) {
855
+ return "docker-compose up -d";
856
+ }
857
+ if (typeof config === "string") {
858
+ return `docker-compose -f ${config} up -d`;
859
+ }
860
+ const composeFile = config.compose_file || "docker-compose.yml";
861
+ const services = config.services?.join(" ") || "";
862
+ return `docker-compose -f ${composeFile} up -d ${services}`.trim();
863
+ }
864
+ static get processing() {
865
+ return {
866
+ async processEvent(context) {
867
+ const { project, isShellMode, shellCommands } = context;
868
+ const dockerConfig = project.events.docker;
869
+ const dockerCommand = _DockerEvent.getDockerCommand(
870
+ dockerConfig
871
+ );
872
+ if (isShellMode) {
873
+ shellCommands.push(dockerCommand);
874
+ } else {
875
+ const [cmd, ...args] = dockerCommand.split(" ");
876
+ spawn6(cmd, args, {
877
+ cwd: project.path.path,
878
+ stdio: "inherit"
879
+ });
880
+ }
881
+ },
882
+ generateShellCommand(context) {
883
+ const dockerConfig = context.project.events.docker;
884
+ return [_DockerEvent.getDockerCommand(dockerConfig)];
885
+ }
886
+ };
887
+ }
888
+ static get tmux() {
889
+ return null;
890
+ }
891
+ static get help() {
892
+ return {
893
+ usage: 'docker: true | "compose-file.yml" | { compose_file: string, services?: string[] }',
894
+ description: "Start Docker containers for the project",
895
+ examples: [
896
+ { config: true, description: "Use default docker-compose.yml" },
897
+ { config: "docker-compose.dev.yml", description: "Use custom compose file" },
898
+ {
899
+ config: { compose_file: "docker-compose.yml", services: ["web", "db"] },
900
+ description: "Start specific services"
901
+ }
902
+ ]
903
+ };
904
+ }
905
+ };
906
+
907
+ // src/events/extensions/npm.ts
908
+ import { spawn as spawn7 } from "child_process";
909
+ import { input as input3, confirm as confirm2 } from "@inquirer/prompts";
910
+ var NpmEvent = class _NpmEvent {
911
+ static get metadata() {
912
+ return {
913
+ name: "npm",
914
+ displayName: "Run NPM command",
915
+ description: "Execute NPM scripts in project directory",
916
+ category: "development",
917
+ requiresTmux: true,
918
+ dependencies: ["npm"]
919
+ };
920
+ }
921
+ static get validation() {
922
+ return {
923
+ validateConfig(config) {
924
+ if (typeof config === "boolean" || config === "true" || config === "false") {
925
+ return true;
926
+ }
927
+ if (typeof config === "string") {
928
+ if (config.trim().length === 0) {
929
+ return "npm script name cannot be empty";
930
+ }
931
+ return true;
932
+ }
933
+ if (typeof config === "object" && config !== null) {
934
+ const cfg = config;
935
+ if (typeof cfg.command !== "string" || cfg.command.trim().length === 0) {
936
+ return "npm.command must be a non-empty string";
937
+ }
938
+ if (cfg.watch !== void 0 && typeof cfg.watch !== "boolean") {
939
+ return "npm.watch must be a boolean";
940
+ }
941
+ if (cfg.auto_restart !== void 0 && typeof cfg.auto_restart !== "boolean") {
942
+ return "npm.auto_restart must be a boolean";
943
+ }
944
+ return true;
945
+ }
946
+ return "npm config must be a boolean, string (script name), or object";
947
+ }
948
+ };
949
+ }
950
+ static get configuration() {
951
+ return {
952
+ async configureInteractive() {
953
+ const scriptName = await input3({
954
+ message: "Enter NPM script to run:",
955
+ default: "dev"
956
+ });
957
+ const useAdvanced = await confirm2({
958
+ message: "Configure advanced NPM options?",
959
+ default: false
960
+ });
961
+ if (!useAdvanced) {
962
+ return scriptName;
963
+ }
964
+ const watch = await confirm2({
965
+ message: "Enable watch mode?",
966
+ default: false
967
+ });
968
+ const autoRestart = await confirm2({
969
+ message: "Auto-restart on crash?",
970
+ default: false
971
+ });
972
+ if (!watch && !autoRestart) {
973
+ return scriptName;
974
+ }
975
+ return {
976
+ command: scriptName,
977
+ watch,
978
+ auto_restart: autoRestart
979
+ };
980
+ },
981
+ getDefaultConfig() {
982
+ return "dev";
983
+ }
984
+ };
985
+ }
986
+ static getNpmCommand(config) {
987
+ if (typeof config === "boolean" || config === void 0) {
988
+ return "npm run dev";
989
+ }
990
+ if (typeof config === "string") {
991
+ return `npm run ${config}`;
992
+ }
993
+ return `npm run ${config.command}`;
994
+ }
995
+ static get processing() {
996
+ return {
997
+ async processEvent(context) {
998
+ const { project, isShellMode, shellCommands } = context;
999
+ const npmConfig = project.events.npm;
1000
+ const npmCommand = _NpmEvent.getNpmCommand(npmConfig);
1001
+ if (isShellMode) {
1002
+ shellCommands.push(npmCommand);
1003
+ } else {
1004
+ const [cmd, ...args] = npmCommand.split(" ");
1005
+ spawn7(cmd, args, {
1006
+ cwd: project.path.path,
1007
+ stdio: "inherit"
1008
+ });
1009
+ }
1010
+ },
1011
+ generateShellCommand(context) {
1012
+ const npmConfig = context.project.events.npm;
1013
+ return [_NpmEvent.getNpmCommand(npmConfig)];
1014
+ }
1015
+ };
1016
+ }
1017
+ static get tmux() {
1018
+ return {
1019
+ getLayoutPriority() {
1020
+ return 50;
1021
+ },
1022
+ contributeToLayout(enabledCommands) {
1023
+ if (enabledCommands.includes("claude")) {
1024
+ return "three-pane";
1025
+ }
1026
+ return "two-pane-npm";
1027
+ }
1028
+ };
1029
+ }
1030
+ static get help() {
1031
+ return {
1032
+ usage: 'npm: true | "script" | { command: string, watch?: boolean, auto_restart?: boolean }',
1033
+ description: "Run an NPM script in the project directory",
1034
+ examples: [
1035
+ { config: true, description: "Run npm run dev" },
1036
+ { config: "test", description: "Run npm run test" },
1037
+ { config: { command: "dev", watch: true }, description: "Run dev with watch mode" }
1038
+ ]
1039
+ };
1040
+ }
1041
+ };
1042
+
1043
+ // src/events/registry.ts
1044
+ var ALL_EVENTS = [CwdEvent, IdeEvent, WebEvent, ClaudeEvent, DockerEvent, NpmEvent];
1045
+ var EventRegistryClass = class {
1046
+ _events = /* @__PURE__ */ new Map();
1047
+ _initialized = false;
1048
+ /**
1049
+ * Initialize the registry by registering all events
1050
+ */
1051
+ async initialize() {
1052
+ if (this._initialized) return;
1053
+ this.registerEvents();
1054
+ this._initialized = true;
1055
+ }
1056
+ /**
1057
+ * Register all event classes
1058
+ */
1059
+ registerEvents() {
1060
+ for (const EventClass of ALL_EVENTS) {
1061
+ if (this.isValidEvent(EventClass)) {
1062
+ const metadata = EventClass.metadata;
1063
+ this._events.set(metadata.name, EventClass);
1064
+ }
1065
+ }
1066
+ }
1067
+ /**
1068
+ * Validate if a class is a proper event
1069
+ */
1070
+ isValidEvent(EventClass) {
1071
+ try {
1072
+ if (typeof EventClass !== "function") return false;
1073
+ const metadata = EventClass.metadata;
1074
+ return metadata !== void 0 && typeof metadata.name === "string" && typeof metadata.displayName === "string" && typeof EventClass.validation === "object" && typeof EventClass.configuration === "object" && typeof EventClass.processing === "object";
1075
+ } catch {
1076
+ return false;
1077
+ }
1078
+ }
1079
+ /**
1080
+ * Get all valid event names from registered events
1081
+ */
1082
+ getValidEventNames() {
1083
+ this.ensureInitialized();
1084
+ return Array.from(this._events.keys());
1085
+ }
1086
+ /**
1087
+ * Get event by name
1088
+ */
1089
+ getEventByName(name) {
1090
+ this.ensureInitialized();
1091
+ return this._events.get(name) ?? null;
1092
+ }
1093
+ /**
1094
+ * Get all events for management UI
1095
+ */
1096
+ getEventsForManageUI() {
1097
+ this.ensureInitialized();
1098
+ const events = [];
1099
+ for (const [name, EventClass] of this._events) {
1100
+ const metadata = EventClass.metadata;
1101
+ events.push({
1102
+ name: metadata.displayName,
1103
+ value: name,
1104
+ description: metadata.description
1105
+ });
1106
+ }
1107
+ return events.sort((a, b) => a.name.localeCompare(b.name));
1108
+ }
1109
+ /**
1110
+ * Get events that support tmux integration
1111
+ */
1112
+ getTmuxEnabledEvents() {
1113
+ this.ensureInitialized();
1114
+ const tmuxEvents = [];
1115
+ for (const [name, EventClass] of this._events) {
1116
+ const tmux = EventClass.tmux;
1117
+ if (tmux) {
1118
+ tmuxEvents.push({
1119
+ name,
1120
+ event: EventClass,
1121
+ priority: tmux.getLayoutPriority ? tmux.getLayoutPriority() : 0
1122
+ });
1123
+ }
1124
+ }
1125
+ return tmuxEvents.sort((a, b) => b.priority - a.priority);
1126
+ }
1127
+ /**
1128
+ * Get all available events with their metadata
1129
+ */
1130
+ getAllEvents() {
1131
+ this.ensureInitialized();
1132
+ const events = [];
1133
+ for (const [name, EventClass] of this._events) {
1134
+ const typedClass = EventClass;
1135
+ events.push({
1136
+ name,
1137
+ metadata: typedClass.metadata,
1138
+ hasValidation: !!typedClass.validation,
1139
+ hasConfiguration: !!typedClass.configuration,
1140
+ hasProcessing: !!typedClass.processing,
1141
+ hasTmux: !!typedClass.tmux,
1142
+ hasHelp: !!typedClass.help
1143
+ });
1144
+ }
1145
+ return events;
1146
+ }
1147
+ /**
1148
+ * Ensure registry is initialized
1149
+ */
1150
+ ensureInitialized() {
1151
+ if (!this._initialized) {
1152
+ throw new Error("EventRegistry must be initialized before use. Call initialize() first.");
1153
+ }
1154
+ }
1155
+ /**
1156
+ * Clear the registry (useful for testing)
1157
+ */
1158
+ clear() {
1159
+ this._events.clear();
1160
+ this._initialized = false;
1161
+ }
1162
+ };
1163
+ var EventRegistry = new EventRegistryClass();
1164
+ export {
1165
+ BaseEnvironment,
1166
+ Config,
1167
+ EnvironmentRecognizer,
1168
+ EventRegistry,
1169
+ Project,
1170
+ ProjectEnvironment,
1171
+ TmuxManager
1172
+ };
1173
+ //# sourceMappingURL=index.js.map