workon 2.1.3 → 3.0.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 +2227 -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/cli.js ADDED
@@ -0,0 +1,2227 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/events/extensions/npm.ts
13
+ var npm_exports = {};
14
+ __export(npm_exports, {
15
+ NpmEvent: () => NpmEvent,
16
+ default: () => npm_default
17
+ });
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"() {
23
+ "use strict";
24
+ NpmEvent = class _NpmEvent {
25
+ static get metadata() {
26
+ 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"]
33
+ };
34
+ }
35
+ static get validation() {
36
+ return {
37
+ validateConfig(config) {
38
+ if (typeof config === "boolean" || config === "true" || config === "false") {
39
+ return true;
40
+ }
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";
61
+ }
62
+ };
63
+ }
64
+ static get configuration() {
65
+ return {
66
+ 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
+ };
94
+ },
95
+ getDefaultConfig() {
96
+ return "dev";
97
+ }
98
+ };
99
+ }
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
+ static get processing() {
110
+ return {
111
+ async processEvent(context) {
112
+ const { project, isShellMode, shellCommands } = context;
113
+ const npmConfig = project.events.npm;
114
+ const npmCommand = _NpmEvent.getNpmCommand(npmConfig);
115
+ if (isShellMode) {
116
+ shellCommands.push(npmCommand);
117
+ } else {
118
+ const [cmd, ...args] = npmCommand.split(" ");
119
+ spawn7(cmd, args, {
120
+ cwd: project.path.path,
121
+ stdio: "inherit"
122
+ });
123
+ }
124
+ },
125
+ generateShellCommand(context) {
126
+ const npmConfig = context.project.events.npm;
127
+ return [_NpmEvent.getNpmCommand(npmConfig)];
128
+ }
129
+ };
130
+ }
131
+ static get tmux() {
132
+ return {
133
+ getLayoutPriority() {
134
+ return 50;
135
+ },
136
+ contributeToLayout(enabledCommands) {
137
+ if (enabledCommands.includes("claude")) {
138
+ return "three-pane";
139
+ }
140
+ return "two-pane-npm";
141
+ }
142
+ };
143
+ }
144
+ static get help() {
145
+ return {
146
+ usage: 'npm: true | "script" | { command: string, watch?: boolean, auto_restart?: boolean }',
147
+ description: "Run an NPM script in the project directory",
148
+ 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" }
152
+ ]
153
+ };
154
+ }
155
+ };
156
+ npm_default = NpmEvent;
157
+ }
158
+ });
159
+
160
+ // src/commands/interactive.ts
161
+ var interactive_exports = {};
162
+ __export(interactive_exports, {
163
+ runInteractive: () => runInteractive
164
+ });
165
+ import { select, input as input4, checkbox } from "@inquirer/prompts";
166
+ import File3 from "phylo";
167
+ import deepAssign2 from "deep-assign";
168
+ async function runInteractive(ctx) {
169
+ const { config, log, environment, suggestedName } = ctx;
170
+ showLogo(config);
171
+ log.log("");
172
+ const defaultName = suggestedName ?? (environment.$isProjectEnvironment ? environment.project.name : File3.cwd().name);
173
+ const fromUser = !!suggestedName;
174
+ await startInteractive(defaultName, fromUser, ctx);
175
+ }
176
+ function showLogo(config) {
177
+ const version = config.get("pkg")?.version ?? "unknown";
178
+ console.log(
179
+ ` 8\x1B[2m${" ".repeat(Math.max(15 - version.length - 1, 1)) + "v" + version}\x1B[22m
180
+ Yb db dP .d8b. 8d8b 8.dP \x1B[92m.d8b. 8d8b.\x1B[0m
181
+ YbdPYbdP 8' .8 8P 88b \x1B[92m8' .8 8P Y8\x1B[0m
182
+ YP YP \`Y8P' 8 8 Yb \x1B[92m\`Y8P' 8 8\x1B[0m`
183
+ );
184
+ }
185
+ async function startInteractive(defaultName, fromUser, ctx, showMain = false) {
186
+ const { log, environment } = ctx;
187
+ log.debug(`Name '${defaultName}' was${fromUser ? "" : " not"} provided by the user`);
188
+ const question = getFirstQuestion(defaultName, fromUser, environment, showMain);
189
+ const action = await select(question);
190
+ switch (action) {
191
+ case "exit":
192
+ return;
193
+ case "more":
194
+ await startInteractive(defaultName, fromUser, ctx, true);
195
+ return;
196
+ case "init-project":
197
+ await initProject(defaultName, fromUser, ctx);
198
+ return;
199
+ case "init-branch":
200
+ await initBranch(defaultName, ctx);
201
+ return;
202
+ case "switch-project":
203
+ log.info("Switch to an existing project");
204
+ break;
205
+ case "switch-branch":
206
+ log.info("Switch to an existing branch");
207
+ break;
208
+ case "manage-projects":
209
+ log.info("Manage existing projects");
210
+ break;
211
+ case "manage-branches":
212
+ log.info("Manage existing branches");
213
+ break;
214
+ }
215
+ }
216
+ function getFirstQuestion(defaultName, fromUser, environment, showMain) {
217
+ if (!showMain && environment.$isProjectEnvironment && !fromUser) {
218
+ return {
219
+ message: environment.project.name,
220
+ choices: [
221
+ { name: "Start a branch", value: "init-branch" },
222
+ { name: "Switch branch", value: "switch-branch" },
223
+ { name: "Manage branches", value: "manage-branches" },
224
+ { name: "---", value: "" },
225
+ { name: "More...", value: "more" },
226
+ { name: "Exit", value: "exit" }
227
+ ].filter((c) => c.value !== "")
228
+ };
229
+ }
230
+ return {
231
+ message: "What do you want to do?",
232
+ choices: [
233
+ { name: "Start a new project", value: "init-project" },
234
+ { name: "Open an existing project", value: "switch-project" },
235
+ { name: "Manage projects", value: "manage-projects" },
236
+ { name: "---", value: "" },
237
+ { name: "Exit", value: "exit" }
238
+ ].filter((c) => c.value !== "")
239
+ };
240
+ }
241
+ async function initProject(defaultName, fromUser, ctx) {
242
+ const { config, log } = ctx;
243
+ const defaults = config.getDefaults();
244
+ const projects = config.getProjects();
245
+ let name;
246
+ if (fromUser) {
247
+ name = defaultName;
248
+ log.log(`Project name: ${name}`);
249
+ } else {
250
+ name = await input4({
251
+ message: "What is the name of the project?",
252
+ default: defaultName,
253
+ validate: (value) => {
254
+ if (value in projects) return "Project already exists.";
255
+ if (/\w+#\w+/.test(value)) {
256
+ const projectName = value.substring(0, value.indexOf("#"));
257
+ if (!(projectName in projects)) {
258
+ return `Project '${projectName}' does not exist. Please create it before starting a branch.`;
259
+ }
260
+ }
261
+ return true;
262
+ }
263
+ });
264
+ }
265
+ const isBranch = /\w+#\w+/.test(name);
266
+ let basePath;
267
+ if (isBranch) {
268
+ const projectName = name.substring(0, name.indexOf("#"));
269
+ basePath = defaults?.base ? File3.from(defaults.base).join(projects[projectName].path).absolutePath() : projects[projectName].path;
270
+ log.log(`Project path: ${basePath}`);
271
+ } else {
272
+ const pathAnswer = await input4({
273
+ message: "What is the path to the project?",
274
+ default: defaults?.base ? File3.from(defaults.base).join(name).path : name
275
+ });
276
+ let answerFile = File3.from(pathAnswer);
277
+ const defaultBase = defaults?.base ? File3.from(defaults.base) : File3.cwd();
278
+ if (!answerFile.isAbsolute()) {
279
+ answerFile = defaultBase.join(answerFile.path);
280
+ }
281
+ try {
282
+ const canonical = answerFile.canonicalize();
283
+ if (canonical) {
284
+ answerFile = canonical;
285
+ } else {
286
+ answerFile = answerFile.absolutify();
287
+ }
288
+ } catch {
289
+ answerFile = answerFile.absolutify();
290
+ }
291
+ basePath = answerFile.relativize(defaultBase.path).path;
292
+ }
293
+ const ide = await select({
294
+ message: "What is the IDE?",
295
+ choices: IDE_CHOICES
296
+ });
297
+ const selectedEvents = await checkbox({
298
+ message: "Which events should take place when opening?",
299
+ choices: [
300
+ { name: "Change terminal cwd to project path", value: "cwd", checked: true },
301
+ { name: "Open project in IDE", value: "ide", checked: true }
302
+ ]
303
+ });
304
+ const events = {
305
+ cwd: selectedEvents.includes("cwd"),
306
+ ide: selectedEvents.includes("ide")
307
+ };
308
+ const projectConfig = {
309
+ path: basePath,
310
+ ide,
311
+ events
312
+ };
313
+ projects[name] = projectConfig;
314
+ config.set("projects", projects);
315
+ log.info("Your project has been initialized.");
316
+ log.info(`Use 'workon ${name}' to start working!`);
317
+ }
318
+ async function initBranch(defaultName, ctx) {
319
+ const { config, log } = ctx;
320
+ const projects = config.getProjects();
321
+ const branch = await input4({
322
+ message: "What is the name of the branch?",
323
+ validate: (value) => {
324
+ if (/\w+#\w+/.test(value)) return `Branch name can't contain the "#" sign`;
325
+ if (`${defaultName}#${value}` in projects) return "Branch already exists.";
326
+ return true;
327
+ }
328
+ });
329
+ const branchName = `${defaultName}#${branch}`;
330
+ const baseProject = projects[defaultName];
331
+ const branchConfig = deepAssign2({}, baseProject, { branch });
332
+ delete branchConfig.name;
333
+ projects[branchName] = branchConfig;
334
+ config.set("projects", projects);
335
+ log.info("Your branch configuration has been initialized.");
336
+ log.info(`Use 'workon ${branchName}' to start working!`);
337
+ }
338
+ var IDE_CHOICES;
339
+ var init_interactive = __esm({
340
+ "src/commands/interactive.ts"() {
341
+ "use strict";
342
+ IDE_CHOICES = [
343
+ { name: "Visual Studio Code", value: "vscode" },
344
+ { name: "IntelliJ IDEA", value: "idea" },
345
+ { name: "Atom", value: "atom" }
346
+ ];
347
+ }
348
+ });
349
+
350
+ // src/commands/index.ts
351
+ import { Command as Command7 } from "commander";
352
+ import { readFileSync, existsSync } from "fs";
353
+ import { join, dirname } from "path";
354
+ import { fileURLToPath } from "url";
355
+ import loog from "loog";
356
+ import omelette from "omelette";
357
+ import File6 from "phylo";
358
+
359
+ // src/lib/config.ts
360
+ import Conf from "conf";
361
+ var TRANSIENT_PROPS = ["pkg", "work"];
362
+ var Config = class {
363
+ _transient = {};
364
+ _store;
365
+ constructor() {
366
+ this._store = new Conf({
367
+ projectName: "workon"
368
+ });
369
+ }
370
+ get(key, defaultValue) {
371
+ const rootKey = key.split(".")[0];
372
+ if (TRANSIENT_PROPS.includes(rootKey)) {
373
+ return this._transient[key] ?? defaultValue;
374
+ }
375
+ return this._store.get(key, defaultValue);
376
+ }
377
+ set(key, value) {
378
+ const rootKey = key.split(".")[0];
379
+ if (TRANSIENT_PROPS.includes(rootKey)) {
380
+ this._transient[key] = value;
381
+ } else {
382
+ if (value === void 0) {
383
+ this._store.set(key, value);
384
+ } else {
385
+ this._store.set(key, value);
386
+ }
387
+ }
388
+ }
389
+ has(key) {
390
+ const rootKey = key.split(".")[0];
391
+ if (TRANSIENT_PROPS.includes(rootKey)) {
392
+ return Object.prototype.hasOwnProperty.call(this._transient, key);
393
+ }
394
+ return this._store.has(key);
395
+ }
396
+ delete(key) {
397
+ const rootKey = key.split(".")[0];
398
+ if (TRANSIENT_PROPS.includes(rootKey)) {
399
+ delete this._transient[key];
400
+ } else {
401
+ this._store.delete(key);
402
+ }
403
+ }
404
+ getProjects() {
405
+ return this.get("projects") ?? {};
406
+ }
407
+ getProject(name) {
408
+ const projects = this.getProjects();
409
+ return projects[name];
410
+ }
411
+ setProject(name, config) {
412
+ const projects = this.getProjects();
413
+ projects[name] = config;
414
+ this.set("projects", projects);
415
+ }
416
+ deleteProject(name) {
417
+ const projects = this.getProjects();
418
+ delete projects[name];
419
+ this.set("projects", projects);
420
+ }
421
+ getDefaults() {
422
+ return this.get("project_defaults");
423
+ }
424
+ setDefaults(defaults) {
425
+ this.set("project_defaults", defaults);
426
+ }
427
+ get path() {
428
+ return this._store.path;
429
+ }
430
+ get store() {
431
+ return this._store.store;
432
+ }
433
+ };
434
+
435
+ // src/lib/environment.ts
436
+ import File2 from "phylo";
437
+ import { simpleGit } from "simple-git";
438
+
439
+ // src/lib/project.ts
440
+ import File from "phylo";
441
+ import deepAssign from "deep-assign";
442
+ var Project = class {
443
+ name;
444
+ _base;
445
+ _path;
446
+ _ide;
447
+ _events = {};
448
+ _branch;
449
+ _homepage;
450
+ _defaults;
451
+ _initialCfg;
452
+ constructor(name, cfg, defaults) {
453
+ this._defaults = defaults ?? { base: "" };
454
+ this._initialCfg = { path: name, events: {}, ...cfg };
455
+ this.name = cfg?.name ?? name;
456
+ const merged = deepAssign({}, this._defaults, this._initialCfg);
457
+ if (merged.base) {
458
+ this.base = merged.base;
459
+ }
460
+ if (merged.path) {
461
+ this.path = merged.path;
462
+ }
463
+ if (merged.ide) {
464
+ this._ide = merged.ide;
465
+ }
466
+ if (merged.events) {
467
+ this._events = merged.events;
468
+ }
469
+ if (merged.branch) {
470
+ this._branch = merged.branch;
471
+ }
472
+ if (merged.homepage) {
473
+ this._homepage = merged.homepage;
474
+ }
475
+ }
476
+ set base(path) {
477
+ this._base = File.from(path).absolutify();
478
+ }
479
+ get base() {
480
+ return this._base;
481
+ }
482
+ set ide(cmd) {
483
+ this._ide = cmd;
484
+ }
485
+ get ide() {
486
+ return this._ide;
487
+ }
488
+ set events(eventCfg) {
489
+ this._events = eventCfg;
490
+ }
491
+ get events() {
492
+ return this._events;
493
+ }
494
+ set path(path) {
495
+ if (this._base) {
496
+ this._path = this._base.join(path);
497
+ } else {
498
+ this._path = File.from(path);
499
+ }
500
+ this._path = this._path.absolutify();
501
+ }
502
+ get path() {
503
+ if (!this._path) {
504
+ throw new Error("Project path not set");
505
+ }
506
+ return this._path;
507
+ }
508
+ set branch(branch) {
509
+ this._branch = branch;
510
+ }
511
+ get branch() {
512
+ return this._branch;
513
+ }
514
+ set homepage(url) {
515
+ this._homepage = url;
516
+ }
517
+ get homepage() {
518
+ return this._homepage;
519
+ }
520
+ static $isProject = true;
521
+ $isProject = true;
522
+ };
523
+
524
+ // src/lib/environment.ts
525
+ var BaseEnvironment = class {
526
+ $isProjectEnvironment = false;
527
+ };
528
+ var ProjectEnvironment = class _ProjectEnvironment {
529
+ $isProjectEnvironment = true;
530
+ project;
531
+ constructor(projectCfg) {
532
+ this.project = new Project(projectCfg.name, projectCfg);
533
+ }
534
+ static load(cfg, defaults) {
535
+ const project = new Project(cfg.name, cfg, defaults);
536
+ return new _ProjectEnvironment({ ...cfg, name: project.name });
537
+ }
538
+ };
539
+ var EnvironmentRecognizer = class {
540
+ static config;
541
+ static log;
542
+ static projects = [];
543
+ static configured = false;
544
+ static configure(config, log) {
545
+ if (this.configured) {
546
+ return;
547
+ }
548
+ this.config = config;
549
+ this.log = log;
550
+ this.configured = true;
551
+ }
552
+ static async recognize(dir) {
553
+ this.ensureConfigured();
554
+ const theDir = File2.from(dir).canonicalize();
555
+ this.log.debug("Directory to recognize is: " + theDir.canonicalPath());
556
+ const allProjects = this.getAllProjects();
557
+ const matching = allProjects.filter((p) => p.path.canonicalPath() === theDir.path);
558
+ if (matching.length === 0) {
559
+ return new BaseEnvironment();
560
+ }
561
+ this.log.debug(`Found ${matching.length} matching projects`);
562
+ const base = matching.find((p) => !p.name.includes("#")) ?? matching[0];
563
+ this.log.debug("Base project is: " + base.name);
564
+ const gitDir = base.path.up(".git");
565
+ if (gitDir) {
566
+ try {
567
+ const git = simpleGit(gitDir.path);
568
+ const branchSummary = await git.branchLocal();
569
+ base.branch = branchSummary.current;
570
+ } catch {
571
+ }
572
+ }
573
+ return this.getProjectEnvironment(base, matching);
574
+ }
575
+ static getAllProjects(refresh = false) {
576
+ if (this.projects.length > 0 && !refresh) {
577
+ return this.projects;
578
+ }
579
+ const defaults = this.config.getDefaults();
580
+ if (!defaults?.base) {
581
+ this.projects = [];
582
+ return this.projects;
583
+ }
584
+ const baseDir = File2.from(defaults.base);
585
+ const projectsMap = this.config.getProjects();
586
+ this.projects = Object.entries(projectsMap).map(([name, project]) => ({
587
+ ...project,
588
+ name,
589
+ path: baseDir.join(project.path)
590
+ }));
591
+ return this.projects;
592
+ }
593
+ static getProjectEnvironment(base, _matching) {
594
+ const exactName = `${base.name}#${base.branch}`;
595
+ const exactProj = this.projects.find((p) => p.name === exactName);
596
+ const toProjectConfig = (p) => ({
597
+ name: p.name,
598
+ path: p.path.path,
599
+ // Convert PhyloFile to string path
600
+ ide: p.ide,
601
+ homepage: p.homepage,
602
+ events: p.events,
603
+ branch: p.branch,
604
+ exactName
605
+ });
606
+ if (exactProj) {
607
+ return new ProjectEnvironment({ ...toProjectConfig(exactProj), branch: base.branch });
608
+ }
609
+ return new ProjectEnvironment(toProjectConfig(base));
610
+ }
611
+ static ensureConfigured() {
612
+ if (!this.configured) {
613
+ this.config = new Config();
614
+ this.log = {
615
+ debug: () => {
616
+ },
617
+ info: () => {
618
+ },
619
+ log: () => {
620
+ },
621
+ warn: () => {
622
+ },
623
+ error: () => {
624
+ },
625
+ setLogLevel: () => {
626
+ }
627
+ };
628
+ this.configured = true;
629
+ }
630
+ }
631
+ };
632
+
633
+ // src/commands/open.ts
634
+ import { Command } from "commander";
635
+ import File4 from "phylo";
636
+
637
+ // src/lib/tmux.ts
638
+ import { exec as execCallback, spawn } from "child_process";
639
+ import { promisify } from "util";
640
+ var exec = promisify(execCallback);
641
+ var TmuxManager = class {
642
+ sessionPrefix = "workon-";
643
+ async isTmuxAvailable() {
644
+ try {
645
+ await exec("which tmux");
646
+ return true;
647
+ } catch {
648
+ return false;
649
+ }
650
+ }
651
+ async sessionExists(sessionName) {
652
+ try {
653
+ await exec(`tmux has-session -t "${sessionName}"`);
654
+ return true;
655
+ } catch {
656
+ return false;
657
+ }
658
+ }
659
+ getSessionName(projectName) {
660
+ return `${this.sessionPrefix}${projectName}`;
661
+ }
662
+ async killSession(sessionName) {
663
+ try {
664
+ await exec(`tmux kill-session -t "${sessionName}"`);
665
+ return true;
666
+ } catch {
667
+ return false;
668
+ }
669
+ }
670
+ async createSplitSession(projectName, projectPath, claudeArgs = []) {
671
+ const sessionName = this.getSessionName(projectName);
672
+ if (await this.sessionExists(sessionName)) {
673
+ await this.killSession(sessionName);
674
+ }
675
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
676
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
677
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
678
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
679
+ return sessionName;
680
+ }
681
+ async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = "npm run dev") {
682
+ const sessionName = this.getSessionName(projectName);
683
+ if (await this.sessionExists(sessionName)) {
684
+ await this.killSession(sessionName);
685
+ }
686
+ const claudeCommand = claudeArgs.length > 0 ? `claude ${claudeArgs.join(" ")}` : "claude";
687
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
688
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
689
+ await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
690
+ await exec(`tmux set-option -t "${sessionName}:0.2" remain-on-exit on`);
691
+ await exec(`tmux resize-pane -t "${sessionName}:0.2" -y 10`);
692
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
693
+ return sessionName;
694
+ }
695
+ async createTwoPaneNpmSession(projectName, projectPath, npmCommand = "npm run dev") {
696
+ const sessionName = this.getSessionName(projectName);
697
+ if (await this.sessionExists(sessionName)) {
698
+ await this.killSession(sessionName);
699
+ }
700
+ await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
701
+ await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
702
+ await exec(`tmux set-option -t "${sessionName}:0.1" remain-on-exit on`);
703
+ await exec(`tmux select-pane -t "${sessionName}:0.0"`);
704
+ return sessionName;
705
+ }
706
+ async attachToSession(sessionName) {
707
+ if (process.env.TMUX) {
708
+ await exec(`tmux switch-client -t "${sessionName}"`);
709
+ } else {
710
+ const isITerm = process.env.TERM_PROGRAM === "iTerm.app" || process.env.LC_TERMINAL === "iTerm2" || !!process.env.ITERM_SESSION_ID;
711
+ const useiTermIntegration = isITerm && !process.env.TMUX_CC_NOT_SUPPORTED;
712
+ if (useiTermIntegration) {
713
+ spawn("tmux", ["-CC", "attach-session", "-t", sessionName], {
714
+ stdio: "inherit",
715
+ detached: true
716
+ });
717
+ } else {
718
+ spawn("tmux", ["attach-session", "-t", sessionName], {
719
+ stdio: "inherit",
720
+ detached: true
721
+ });
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";
1090
+ }
1091
+ const flags = config.flags || [];
1092
+ return flags.length > 0 ? `claude ${flags.join(" ")}` : "claude";
1093
+ }
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
+ };
1115
+ }
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
+ };
1128
+ }
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
+ };
1142
+ }
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
+ };
1158
+ }
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
+ };
1188
+ }
1189
+ static get configuration() {
1190
+ return {
1191
+ async configureInteractive() {
1192
+ const composeFile = await input2({
1193
+ message: "Enter docker-compose file path:",
1194
+ default: "docker-compose.yml"
1195
+ });
1196
+ const servicesInput = await input2({
1197
+ message: "Enter services to start (comma-separated, leave empty for all):",
1198
+ default: ""
1199
+ });
1200
+ const services = servicesInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1201
+ if (composeFile === "docker-compose.yml" && services.length === 0) {
1202
+ return { compose_file: "docker-compose.yml" };
1203
+ }
1204
+ if (services.length === 0) {
1205
+ return composeFile;
1206
+ }
1207
+ return {
1208
+ compose_file: composeFile,
1209
+ services
1210
+ };
1211
+ },
1212
+ getDefaultConfig() {
1213
+ return { compose_file: "docker-compose.yml" };
1214
+ }
1215
+ };
1216
+ }
1217
+ static getDockerCommand(config) {
1218
+ if (typeof config === "boolean" || config === void 0) {
1219
+ return "docker-compose up -d";
1220
+ }
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
+ }
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
+ };
1251
+ }
1252
+ static get tmux() {
1253
+ return null;
1254
+ }
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
+ };
1268
+ }
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
+ }
1294
+ }
1295
+ }
1296
+ /**
1297
+ * Validate if a class is a proper event
1298
+ */
1299
+ isValidEvent(EventClass) {
1300
+ 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";
1304
+ } 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.");
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Clear the registry (useful for testing)
1386
+ */
1387
+ clear() {
1388
+ this._events.clear();
1389
+ this._initialized = false;
1390
+ }
1391
+ };
1392
+ var EventRegistry = new EventRegistryClass();
1393
+
1394
+ // src/commands/open.ts
1395
+ function createOpenCommand(ctx) {
1396
+ const { config, log } = ctx;
1397
+ 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
+ if (options.debug) {
1399
+ log.setLogLevel("debug");
1400
+ }
1401
+ await EventRegistry.initialize();
1402
+ if (projectArg) {
1403
+ await processProject(projectArg, options, ctx);
1404
+ } else {
1405
+ log.debug("No project name provided, starting interactive mode");
1406
+ const { runInteractive: runInteractive2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
1407
+ const environment = await EnvironmentRecognizer.recognize(File4.cwd());
1408
+ await runInteractive2({ config, log, environment });
1409
+ }
1410
+ });
1411
+ return command;
1412
+ }
1413
+ async function processProject(projectParam, options, ctx) {
1414
+ const { config, log } = ctx;
1415
+ const [projectName, commandsString] = projectParam.split(":");
1416
+ const requestedCommands = commandsString ? commandsString.split(",").map((cmd) => cmd.trim()) : null;
1417
+ if (commandsString === "help") {
1418
+ await showProjectHelp(projectName, ctx);
1419
+ return;
1420
+ }
1421
+ log.debug(
1422
+ `Project: ${projectName}, Commands: ${requestedCommands ? requestedCommands.join(", ") : "all"}`
1423
+ );
1424
+ const projects = config.getProjects();
1425
+ const environment = await EnvironmentRecognizer.recognize(File4.cwd());
1426
+ if (environment.$isProjectEnvironment && (projectName === "this" || projectName === ".")) {
1427
+ log.info(`Opening current project: ${environment.project.name}`);
1428
+ await switchTo(environment, requestedCommands, options, ctx);
1429
+ return;
1430
+ }
1431
+ if (projectName in projects) {
1432
+ const cfg = projects[projectName];
1433
+ const projectCfg = { ...cfg, name: projectName };
1434
+ if (requestedCommands) {
1435
+ validateRequestedCommands(requestedCommands, projectCfg, projectName);
1436
+ }
1437
+ const projectEnv = ProjectEnvironment.load(projectCfg, config.getDefaults());
1438
+ await switchTo(projectEnv, requestedCommands, options, ctx);
1439
+ } else {
1440
+ log.debug(`Project '${projectName}' not found, starting interactive mode`);
1441
+ const { runInteractive: runInteractive2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
1442
+ await runInteractive2({ config, log, environment, suggestedName: projectName });
1443
+ }
1444
+ }
1445
+ function validateRequestedCommands(requestedCommands, projectConfig, projectName) {
1446
+ const configuredEvents = Object.keys(projectConfig.events || {});
1447
+ const invalidCommands = requestedCommands.filter((cmd) => !configuredEvents.includes(cmd));
1448
+ if (invalidCommands.length > 0) {
1449
+ const availableCommands = configuredEvents.join(", ");
1450
+ throw new Error(
1451
+ `Commands not configured for project '${projectName}': ${invalidCommands.join(", ")}
1452
+ Available commands: ${availableCommands}`
1453
+ );
1454
+ }
1455
+ }
1456
+ async function switchTo(environment, requestedCommands, options, ctx) {
1457
+ const { log } = ctx;
1458
+ const project = environment.project;
1459
+ let events;
1460
+ if (requestedCommands) {
1461
+ events = resolveCommandDependencies(requestedCommands, project);
1462
+ log.debug(`Executing requested commands: ${events.join(", ")}`);
1463
+ } else {
1464
+ events = Object.keys(project.events).filter(
1465
+ (e) => project.events[e]
1466
+ );
1467
+ log.debug(`Executing all configured commands: ${events.join(", ")}`);
1468
+ }
1469
+ log.debug(`Shell is ${process.env.SHELL}`);
1470
+ log.debug(`Project path is ${project.path.path}`);
1471
+ log.debug(`IDE command is: ${project.ide}`);
1472
+ log.debug(`Final events to execute: ${events.join(", ")}`);
1473
+ const shellCommands = [];
1474
+ const isShellMode = options.shell || false;
1475
+ const hasCwd = events.includes("cwd");
1476
+ const hasClaudeEvent = events.includes("claude");
1477
+ const hasNpmEvent = events.includes("npm");
1478
+ const dryRun = options.dryRun || false;
1479
+ if (hasCwd && hasClaudeEvent && hasNpmEvent) {
1480
+ await handleThreePaneLayout(project, isShellMode, dryRun, shellCommands, events, ctx);
1481
+ } else if (hasCwd && hasNpmEvent) {
1482
+ await handleTwoPaneNpmLayout(project, isShellMode, dryRun, shellCommands, events, ctx);
1483
+ } else if (hasCwd && hasClaudeEvent) {
1484
+ await handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx);
1485
+ } else {
1486
+ for (const event of events) {
1487
+ if (!dryRun) {
1488
+ await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1489
+ }
1490
+ }
1491
+ }
1492
+ if (dryRun) {
1493
+ log.info("Dry run - would execute events:", events.join(", "));
1494
+ }
1495
+ if (isShellMode && shellCommands.length > 0) {
1496
+ console.log(shellCommands.join("\n"));
1497
+ }
1498
+ }
1499
+ function resolveCommandDependencies(requestedCommands, project) {
1500
+ const resolved = [...requestedCommands];
1501
+ const needsCwd = ["claude", "npm", "ide"];
1502
+ const needsCwdCommands = requestedCommands.filter((cmd) => needsCwd.includes(cmd));
1503
+ if (needsCwdCommands.length > 0 && !requestedCommands.includes("cwd") && project.events.cwd) {
1504
+ resolved.unshift("cwd");
1505
+ }
1506
+ return [...new Set(resolved)];
1507
+ }
1508
+ async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx) {
1509
+ const { log } = ctx;
1510
+ const tmux = new TmuxManager();
1511
+ const claudeConfig = project.events.claude;
1512
+ const claudeArgs = typeof claudeConfig === "object" && claudeConfig.flags ? claudeConfig.flags : [];
1513
+ let tmuxHandled = false;
1514
+ if (isShellMode) {
1515
+ if (await tmux.isTmuxAvailable()) {
1516
+ const commands = tmux.buildShellCommands(project.name, project.path.path, claudeArgs);
1517
+ shellCommands.push(...commands);
1518
+ tmuxHandled = true;
1519
+ } else {
1520
+ 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);
1524
+ tmuxHandled = true;
1525
+ }
1526
+ } else if (!dryRun) {
1527
+ if (await tmux.isTmuxAvailable()) {
1528
+ try {
1529
+ const sessionName = await tmux.createSplitSession(
1530
+ project.name,
1531
+ project.path.path,
1532
+ claudeArgs
1533
+ );
1534
+ await tmux.attachToSession(sessionName);
1535
+ tmuxHandled = true;
1536
+ } catch (error) {
1537
+ log.debug(`Failed to create tmux session: ${error.message}`);
1538
+ }
1539
+ } else {
1540
+ log.debug("Tmux not available, falling back to normal event processing");
1541
+ }
1542
+ } else {
1543
+ log.info(`Would create split tmux session '${project.name}' with Claude`);
1544
+ tmuxHandled = true;
1545
+ }
1546
+ if (!tmuxHandled && !dryRun) {
1547
+ for (const event of events.filter((e) => ["cwd", "claude"].includes(e))) {
1548
+ await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1549
+ }
1550
+ }
1551
+ if (!dryRun) {
1552
+ for (const event of events.filter((e) => !["cwd", "claude"].includes(e))) {
1553
+ await processEvent(event, { project, isShellMode, shellCommands }, ctx);
1554
+ }
1555
+ }
1556
+ }
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(
1569
+ project.name,
1570
+ project.path.path,
1571
+ claudeArgs,
1572
+ npmCommand
1573
+ );
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;
1603
+ }
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
+ }
1608
+ }
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
+ }
1613
+ }
1614
+ }
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(
1625
+ project.name,
1626
+ project.path.path,
1627
+ npmCommand
1628
+ );
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
+ }
1666
+ }
1667
+ }
1668
+ async function processEvent(event, context, ctx) {
1669
+ const { log } = ctx;
1670
+ log.debug(`Processing event ${event}`);
1671
+ const eventHandler = EventRegistry.getEventByName(event);
1672
+ if (eventHandler && eventHandler.processing) {
1673
+ await eventHandler.processing.processEvent(context);
1674
+ } else {
1675
+ log.debug(`No event handler found for: ${event}`);
1676
+ }
1677
+ }
1678
+ async function showProjectHelp(projectName, ctx) {
1679
+ const { config } = ctx;
1680
+ const projects = config.getProjects();
1681
+ if (!(projectName in projects)) {
1682
+ console.error(`Project '${projectName}' not found`);
1683
+ return;
1684
+ }
1685
+ const projectConfig = projects[projectName];
1686
+ const configuredEvents = Object.keys(projectConfig.events || {});
1687
+ console.log(`
1688
+ Available commands for '${projectName}':`);
1689
+ console.log("-".repeat(50));
1690
+ for (const eventName of configuredEvents) {
1691
+ const eventHandler = EventRegistry.getEventByName(eventName);
1692
+ if (eventHandler) {
1693
+ const metadata = eventHandler.metadata;
1694
+ const config2 = projectConfig.events[eventName];
1695
+ let configDesc = "";
1696
+ if (config2 !== true && config2 !== "true") {
1697
+ if (typeof config2 === "object") {
1698
+ configDesc = ` (${JSON.stringify(config2)})`;
1699
+ } else {
1700
+ configDesc = ` (${config2})`;
1701
+ }
1702
+ }
1703
+ console.log(` ${eventName.padEnd(8)} - ${metadata.description}${configDesc}`);
1704
+ }
1705
+ }
1706
+ console.log("\nUsage examples:");
1707
+ console.log(` workon ${projectName} # Execute all commands`);
1708
+ console.log(` workon ${projectName}:cwd # Just change directory`);
1709
+ console.log(` workon ${projectName}:claude # Just Claude (auto-adds cwd)`);
1710
+ if (configuredEvents.length > 1) {
1711
+ const twoCommands = configuredEvents.slice(0, 2).join(",");
1712
+ console.log(` workon ${projectName}:${twoCommands.padEnd(12)} # Multiple commands`);
1713
+ }
1714
+ console.log(` workon ${projectName}:cwd --shell # Output shell commands
1715
+ `);
1716
+ }
1717
+
1718
+ // src/commands/config/index.ts
1719
+ import { Command as Command5 } from "commander";
1720
+
1721
+ // src/commands/config/list.ts
1722
+ import { Command as Command2 } from "commander";
1723
+ function flattenObject(obj, prefix = "") {
1724
+ const result = {};
1725
+ for (const [key, value] of Object.entries(obj)) {
1726
+ const newKey = prefix ? `${prefix}.${key}` : key;
1727
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1728
+ Object.assign(result, flattenObject(value, newKey));
1729
+ } else {
1730
+ result[newKey] = value;
1731
+ }
1732
+ }
1733
+ return result;
1734
+ }
1735
+ function createListCommand(ctx) {
1736
+ const { config, log } = ctx;
1737
+ return new Command2("list").description("List configuration parameters").action(() => {
1738
+ log.debug("Listing configuration");
1739
+ console.log(`Configuration file: ${config.path}
1740
+ `);
1741
+ const store = config.store;
1742
+ const flattened = flattenObject(store);
1743
+ for (const [key, value] of Object.entries(flattened)) {
1744
+ const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
1745
+ console.log(`${key}: ${displayValue}`);
1746
+ }
1747
+ });
1748
+ }
1749
+
1750
+ // src/commands/config/set.ts
1751
+ import { Command as Command3 } from "commander";
1752
+ function createSetCommand(ctx) {
1753
+ const { config, log } = ctx;
1754
+ return new Command3("set").description("Set a configuration parameter").argument("<key>", "The configuration parameter to set").argument("<value>", "The value to set").action((key, value) => {
1755
+ log.debug(`Setting ${key} to ${value}`);
1756
+ let parsedValue = value;
1757
+ try {
1758
+ if (value === "true") {
1759
+ parsedValue = true;
1760
+ } else if (value === "false") {
1761
+ parsedValue = false;
1762
+ } else if (!isNaN(Number(value)) && value.trim() !== "") {
1763
+ parsedValue = Number(value);
1764
+ } else {
1765
+ parsedValue = JSON.parse(value);
1766
+ }
1767
+ } catch {
1768
+ parsedValue = value;
1769
+ }
1770
+ config.set(key, parsedValue);
1771
+ console.log(
1772
+ `Set ${key} = ${typeof parsedValue === "object" ? JSON.stringify(parsedValue) : parsedValue}`
1773
+ );
1774
+ });
1775
+ }
1776
+
1777
+ // src/commands/config/unset.ts
1778
+ import { Command as Command4 } from "commander";
1779
+ function createUnsetCommand(ctx) {
1780
+ const { config, log } = ctx;
1781
+ return new Command4("unset").description("Remove a configuration parameter").argument("<key>", "The configuration parameter to remove").option("--silent", "Suppress console output").action((key, options) => {
1782
+ log.debug(`Removing ${key}`);
1783
+ if (config.has(key)) {
1784
+ config.delete(key);
1785
+ if (!options.silent) {
1786
+ console.log(`Removed ${key}`);
1787
+ }
1788
+ } else {
1789
+ if (!options.silent) {
1790
+ console.log(`Key ${key} not found`);
1791
+ }
1792
+ }
1793
+ });
1794
+ }
1795
+
1796
+ // src/commands/config/index.ts
1797
+ function createConfigCommand(ctx) {
1798
+ const command = new Command5("config").description("Manage configuration parameters");
1799
+ command.addCommand(createListCommand(ctx));
1800
+ command.addCommand(createSetCommand(ctx));
1801
+ command.addCommand(createUnsetCommand(ctx));
1802
+ command.action(() => {
1803
+ command.commands.find((c) => c.name() === "list")?.parse();
1804
+ });
1805
+ return command;
1806
+ }
1807
+
1808
+ // src/commands/manage.ts
1809
+ import { Command as Command6 } from "commander";
1810
+ import { select as select2, input as input5, confirm as confirm3, checkbox as checkbox2 } from "@inquirer/prompts";
1811
+ 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
+ function createManageCommand(ctx) {
1822
+ const { log } = ctx;
1823
+ return new Command6("manage").description("Interactive project management").option("-d, --debug", "Enable debug logging").action(async (options) => {
1824
+ if (options.debug) {
1825
+ log.setLogLevel("debug");
1826
+ }
1827
+ await EventRegistry.initialize();
1828
+ await mainMenu(ctx);
1829
+ });
1830
+ }
1831
+ async function mainMenu(ctx) {
1832
+ const { config } = ctx;
1833
+ const projects = config.getProjects();
1834
+ const hasProjects = Object.keys(projects).length > 0;
1835
+ const choices = [
1836
+ { name: "Create new project", value: "create" },
1837
+ ...hasProjects ? [
1838
+ { name: "Edit project", value: "edit" },
1839
+ { name: "Delete project", value: "delete" },
1840
+ { name: "List projects", value: "list" }
1841
+ ] : [],
1842
+ { name: "Exit", value: "exit" }
1843
+ ];
1844
+ const action = await select2({
1845
+ message: "What would you like to do?",
1846
+ choices
1847
+ });
1848
+ switch (action) {
1849
+ case "create":
1850
+ await createProject(ctx);
1851
+ break;
1852
+ case "edit":
1853
+ await editProject(ctx);
1854
+ break;
1855
+ case "delete":
1856
+ await deleteProject(ctx);
1857
+ break;
1858
+ case "list":
1859
+ await listProjects(ctx);
1860
+ break;
1861
+ case "exit":
1862
+ return;
1863
+ }
1864
+ await mainMenu(ctx);
1865
+ }
1866
+ async function createProject(ctx) {
1867
+ const { config, log } = ctx;
1868
+ const defaults = config.getDefaults();
1869
+ const projects = config.getProjects();
1870
+ const name = await input5({
1871
+ message: "Project name:",
1872
+ validate: (value) => {
1873
+ if (!value.trim()) return "Name is required";
1874
+ if (!/^[\w-]+$/.test(value))
1875
+ return "Name can only contain letters, numbers, underscores, and hyphens";
1876
+ if (value in projects) return "Project already exists";
1877
+ return true;
1878
+ }
1879
+ });
1880
+ const defaultPath = defaults?.base ? File5.from(defaults.base).join(name).path : name;
1881
+ const pathInput = await input5({
1882
+ message: "Project path:",
1883
+ default: defaultPath,
1884
+ validate: (value) => {
1885
+ const path = File5.from(value);
1886
+ try {
1887
+ const exists = path.exists();
1888
+ if (!exists) return `Path does not exist: ${value}`;
1889
+ const stat = path.stat();
1890
+ if (!stat.isDirectory()) return "Path must be a directory";
1891
+ return true;
1892
+ } catch {
1893
+ return `Invalid path: ${value}`;
1894
+ }
1895
+ }
1896
+ });
1897
+ let relativePath = pathInput;
1898
+ if (defaults?.base) {
1899
+ const baseDir = File5.from(defaults.base);
1900
+ const pathFile = File5.from(pathInput);
1901
+ try {
1902
+ relativePath = pathFile.relativize(baseDir.path).path;
1903
+ } catch {
1904
+ relativePath = pathInput;
1905
+ }
1906
+ }
1907
+ const ide = await select2({
1908
+ message: "Select IDE:",
1909
+ choices: IDE_CHOICES2
1910
+ });
1911
+ const homepage = await input5({
1912
+ message: "Project homepage URL (optional):",
1913
+ default: ""
1914
+ });
1915
+ const availableEvents = EventRegistry.getEventsForManageUI();
1916
+ const selectedEvents = await checkbox2({
1917
+ message: "Select events to enable:",
1918
+ choices: availableEvents.map((e) => ({
1919
+ name: `${e.name} - ${e.description}`,
1920
+ value: e.value,
1921
+ checked: e.value === "cwd" || e.value === "ide"
1922
+ }))
1923
+ });
1924
+ const events = {};
1925
+ for (const eventName of selectedEvents) {
1926
+ const eventHandler = EventRegistry.getEventByName(eventName);
1927
+ if (eventHandler) {
1928
+ const eventConfig = await eventHandler.configuration.configureInteractive();
1929
+ events[eventName] = eventConfig;
1930
+ }
1931
+ }
1932
+ const projectConfig = {
1933
+ path: relativePath,
1934
+ ide,
1935
+ events
1936
+ };
1937
+ if (homepage.trim()) {
1938
+ projectConfig.homepage = homepage.trim();
1939
+ }
1940
+ console.log("\nProject configuration:");
1941
+ console.log(JSON.stringify(projectConfig, null, 2));
1942
+ const confirmed = await confirm3({
1943
+ message: "Save this project?",
1944
+ default: true
1945
+ });
1946
+ if (confirmed) {
1947
+ config.setProject(name, projectConfig);
1948
+ log.info(`Project '${name}' created successfully!`);
1949
+ log.info(`Use 'workon ${name}' to start working!`);
1950
+ } else {
1951
+ log.info("Project creation cancelled.");
1952
+ }
1953
+ }
1954
+ async function editProject(ctx) {
1955
+ const { config, log } = ctx;
1956
+ const projects = config.getProjects();
1957
+ const projectNames = Object.keys(projects);
1958
+ if (projectNames.length === 0) {
1959
+ log.info("No projects to edit.");
1960
+ return;
1961
+ }
1962
+ const name = await select2({
1963
+ message: "Select project to edit:",
1964
+ choices: projectNames.map((n) => ({ name: n, value: n }))
1965
+ });
1966
+ const project = projects[name];
1967
+ const defaults = config.getDefaults();
1968
+ const pathInput = await input5({
1969
+ message: "Project path:",
1970
+ default: project.path
1971
+ });
1972
+ let relativePath = pathInput;
1973
+ if (defaults?.base) {
1974
+ const baseDir = File5.from(defaults.base);
1975
+ const pathFile = File5.from(pathInput);
1976
+ try {
1977
+ if (pathFile.isAbsolute()) {
1978
+ relativePath = pathFile.relativize(baseDir.path).path;
1979
+ }
1980
+ } catch {
1981
+ relativePath = pathInput;
1982
+ }
1983
+ }
1984
+ const ide = await select2({
1985
+ message: "Select IDE:",
1986
+ choices: IDE_CHOICES2,
1987
+ default: project.ide || "vscode"
1988
+ });
1989
+ const homepage = await input5({
1990
+ message: "Project homepage URL:",
1991
+ default: project.homepage || ""
1992
+ });
1993
+ const keepEvents = await confirm3({
1994
+ message: "Keep existing event configuration?",
1995
+ default: true
1996
+ });
1997
+ let events = project.events;
1998
+ if (!keepEvents) {
1999
+ const availableEvents = EventRegistry.getEventsForManageUI();
2000
+ const currentEvents = Object.keys(project.events);
2001
+ const selectedEvents = await checkbox2({
2002
+ message: "Select events to enable:",
2003
+ choices: availableEvents.map((e) => ({
2004
+ name: `${e.name} - ${e.description}`,
2005
+ value: e.value,
2006
+ checked: currentEvents.includes(e.value)
2007
+ }))
2008
+ });
2009
+ events = {};
2010
+ for (const eventName of selectedEvents) {
2011
+ if (project.events[eventName]) {
2012
+ events[eventName] = project.events[eventName];
2013
+ } else {
2014
+ const eventHandler = EventRegistry.getEventByName(eventName);
2015
+ if (eventHandler) {
2016
+ const eventConfig = await eventHandler.configuration.configureInteractive();
2017
+ events[eventName] = eventConfig;
2018
+ }
2019
+ }
2020
+ }
2021
+ }
2022
+ const updatedConfig = {
2023
+ path: relativePath,
2024
+ ide,
2025
+ events
2026
+ };
2027
+ if (homepage.trim()) {
2028
+ updatedConfig.homepage = homepage.trim();
2029
+ }
2030
+ console.log("\nUpdated configuration:");
2031
+ console.log(JSON.stringify(updatedConfig, null, 2));
2032
+ const confirmed = await confirm3({
2033
+ message: "Save changes?",
2034
+ default: true
2035
+ });
2036
+ if (confirmed) {
2037
+ config.setProject(name, updatedConfig);
2038
+ log.info(`Project '${name}' updated successfully!`);
2039
+ } else {
2040
+ log.info("Edit cancelled.");
2041
+ }
2042
+ }
2043
+ async function deleteProject(ctx) {
2044
+ const { config, log } = ctx;
2045
+ const projects = config.getProjects();
2046
+ const projectNames = Object.keys(projects);
2047
+ if (projectNames.length === 0) {
2048
+ log.info("No projects to delete.");
2049
+ return;
2050
+ }
2051
+ const name = await select2({
2052
+ message: "Select project to delete:",
2053
+ choices: projectNames.map((n) => ({ name: n, value: n }))
2054
+ });
2055
+ const confirmed = await confirm3({
2056
+ message: `Are you sure you want to delete '${name}'?`,
2057
+ default: false
2058
+ });
2059
+ if (confirmed) {
2060
+ config.deleteProject(name);
2061
+ log.info(`Project '${name}' deleted.`);
2062
+ } else {
2063
+ log.info("Delete cancelled.");
2064
+ }
2065
+ }
2066
+ async function listProjects(ctx) {
2067
+ const { config } = ctx;
2068
+ const projects = config.getProjects();
2069
+ const defaults = config.getDefaults();
2070
+ console.log("\nConfigured projects:\n");
2071
+ for (const [name, project] of Object.entries(projects)) {
2072
+ const fullPath = defaults?.base ? File5.from(defaults.base).join(project.path).path : project.path;
2073
+ console.log(` ${name}`);
2074
+ console.log(` Path: ${fullPath}`);
2075
+ console.log(` IDE: ${project.ide || "not set"}`);
2076
+ console.log(` Events: ${Object.keys(project.events).join(", ") || "none"}`);
2077
+ if (project.homepage) {
2078
+ console.log(` Homepage: ${project.homepage}`);
2079
+ }
2080
+ console.log();
2081
+ }
2082
+ }
2083
+
2084
+ // src/commands/index.ts
2085
+ var __filename = fileURLToPath(import.meta.url);
2086
+ var __dirname = dirname(__filename);
2087
+ function findPackageJson() {
2088
+ const paths = [
2089
+ join(__dirname, "../package.json"),
2090
+ join(__dirname, "../../package.json"),
2091
+ join(process.cwd(), "package.json")
2092
+ ];
2093
+ for (const p of paths) {
2094
+ if (existsSync(p)) {
2095
+ return p;
2096
+ }
2097
+ }
2098
+ throw new Error("Could not find package.json");
2099
+ }
2100
+ function createCli() {
2101
+ const program2 = new Command7();
2102
+ const packageJsonPath = findPackageJson();
2103
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2104
+ const config = new Config();
2105
+ const log = loog({
2106
+ prefixStyle: "ascii",
2107
+ logLevel: "info"
2108
+ });
2109
+ config.set("pkg", packageJson);
2110
+ EnvironmentRecognizer.configure(config, log);
2111
+ const completion = setupCompletion(config);
2112
+ program2.name("workon").description("Work on something great!").version(packageJson.version).option("-d, --debug", "Enable debug logging").option("--completion", "Setup shell tab completion").option("--shell", "Output shell commands for evaluation").option("--init", "Generate shell integration function").hook("preAction", (thisCommand) => {
2113
+ const opts = thisCommand.opts();
2114
+ if (opts.debug) {
2115
+ log.setLogLevel("debug");
2116
+ }
2117
+ }).action(async (options) => {
2118
+ if (options.debug) {
2119
+ log.setLogLevel("debug");
2120
+ }
2121
+ if (options.completion) {
2122
+ log.debug("Setting up command-line completion");
2123
+ completion.setupShellInitFile();
2124
+ return;
2125
+ }
2126
+ if (options.init) {
2127
+ log.debug("Generating shell integration function");
2128
+ outputShellInit(program2);
2129
+ return;
2130
+ }
2131
+ const environment = await EnvironmentRecognizer.recognize(File6.cwd());
2132
+ program2.setOptionValue("_environment", environment);
2133
+ program2.setOptionValue("_config", config);
2134
+ program2.setOptionValue("_log", log);
2135
+ const { runInteractive: runInteractive2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
2136
+ await runInteractive2({ config, log, environment });
2137
+ });
2138
+ program2.setOptionValue("_config", config);
2139
+ program2.setOptionValue("_log", log);
2140
+ program2.addCommand(createOpenCommand({ config, log }));
2141
+ program2.addCommand(createConfigCommand({ config, log }));
2142
+ program2.addCommand(createManageCommand({ config, log }));
2143
+ program2.on("command:*", async (operands) => {
2144
+ const projectName = operands[0];
2145
+ if (projectName && !projectName.startsWith("-")) {
2146
+ const openCmd = program2.commands.find((c) => c.name() === "open");
2147
+ if (openCmd) {
2148
+ const args = [
2149
+ "open",
2150
+ ...operands,
2151
+ ...process.argv.slice(2).filter((a) => a.startsWith("-"))
2152
+ ];
2153
+ await program2.parseAsync(["node", "workon", ...args]);
2154
+ return;
2155
+ }
2156
+ }
2157
+ console.error(`Unknown command: ${operands.join(" ")}`);
2158
+ program2.help();
2159
+ });
2160
+ program2.showHelpAfterError(true);
2161
+ return program2;
2162
+ }
2163
+ function setupCompletion(config) {
2164
+ const tree = {
2165
+ config: ["list", "set", "unset"],
2166
+ manage: null
2167
+ };
2168
+ const projects = config.getProjects();
2169
+ if (projects) {
2170
+ Object.keys(projects).forEach((id) => {
2171
+ tree[id] = null;
2172
+ });
2173
+ }
2174
+ const completion = omelette("workon").tree(tree);
2175
+ completion.init();
2176
+ return completion;
2177
+ }
2178
+ function outputShellInit(program2) {
2179
+ const cmdNames = program2.commands.map((c) => c.name());
2180
+ const switchFlags = [];
2181
+ program2.options.forEach((opt) => {
2182
+ switchFlags.push("--" + opt.long?.replace(/^--/, ""));
2183
+ if (opt.short) {
2184
+ switchFlags.push(opt.short);
2185
+ }
2186
+ });
2187
+ const builtinFlags = ["--help", "-h", "--version", "-V", "help"];
2188
+ const nonShellCommands = [.../* @__PURE__ */ new Set([...cmdNames, ...switchFlags, ...builtinFlags])];
2189
+ const casePattern = nonShellCommands.join("|");
2190
+ const shellFunction = `
2191
+ # workon shell integration
2192
+ workon() {
2193
+ # Commands and flags that should NOT use shell mode
2194
+ case "$1" in
2195
+ ${casePattern})
2196
+ command workon "$@"
2197
+ return $?
2198
+ ;;
2199
+ esac
2200
+
2201
+ # If no arguments provided, run interactive mode directly
2202
+ if [[ $# -eq 0 ]]; then
2203
+ command workon "$@"
2204
+ return $?
2205
+ fi
2206
+
2207
+ # Default behavior: use shell mode for project opening
2208
+ local output
2209
+ output=$(command workon --shell "$@" 2>&1)
2210
+ local exit_code=$?
2211
+
2212
+ if [[ $exit_code -eq 0 && -n "$output" ]]; then
2213
+ # Execute shell commands if workon succeeded and output exists
2214
+ eval "$output"
2215
+ else
2216
+ # Show any error output
2217
+ [[ -n "$output" ]] && echo "$output" >&2
2218
+ return $exit_code
2219
+ fi
2220
+ }`;
2221
+ console.log(shellFunction);
2222
+ }
2223
+
2224
+ // src/cli.ts
2225
+ var program = createCli();
2226
+ program.parse();
2227
+ //# sourceMappingURL=cli.js.map