workon 1.2.1 → 1.4.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.
package/cli/manage.js CHANGED
@@ -127,18 +127,29 @@ class manage extends command {
127
127
  { name: 'Change directory (cwd)', value: 'cwd', checked: true },
128
128
  { name: 'Open in IDE', value: 'ide', checked: true },
129
129
  { name: 'Open homepage in browser', value: 'web' },
130
- { name: 'Launch Claude Code', value: 'claude' }
130
+ { name: 'Launch Claude Code', value: 'claude' },
131
+ { name: 'Run NPM command', value: 'npm' }
131
132
  ]
132
133
  }
133
134
  ];
134
135
 
135
136
  const answers = await inquirer.prompt(questions);
136
137
 
137
- // Convert events array to object
138
+ // Convert events array to object and configure advanced options
138
139
  const events = {};
139
- answers.events.forEach(event => {
140
- events[event] = 'true';
141
- });
140
+ for (const event of answers.events) {
141
+ if (event === 'claude') {
142
+ // Ask for Claude-specific configuration
143
+ const claudeConfig = await me.configureClaudeEvent();
144
+ events[event] = claudeConfig;
145
+ } else if (event === 'npm') {
146
+ // Ask for NPM-specific configuration
147
+ const npmConfig = await me.configureNpmEvent();
148
+ events[event] = npmConfig;
149
+ } else {
150
+ events[event] = 'true';
151
+ }
152
+ }
142
153
 
143
154
  const projectConfig = {
144
155
  path: answers.path,
@@ -241,18 +252,59 @@ class manage extends command {
241
252
  { name: 'Change directory (cwd)', value: 'cwd', checked: currentEvents.includes('cwd') },
242
253
  { name: 'Open in IDE', value: 'ide', checked: currentEvents.includes('ide') },
243
254
  { name: 'Open homepage in browser', value: 'web', checked: currentEvents.includes('web') },
244
- { name: 'Launch Claude Code', value: 'claude', checked: currentEvents.includes('claude') }
255
+ { name: 'Launch Claude Code', value: 'claude', checked: currentEvents.includes('claude') },
256
+ { name: 'Run NPM command', value: 'npm', checked: currentEvents.includes('npm') }
245
257
  ]
246
258
  }
247
259
  ];
248
260
 
249
261
  const answers = await inquirer.prompt(questions);
250
262
 
251
- // Convert events array to object
263
+ // Convert events array to object and configure advanced options
252
264
  const events = {};
253
- answers.events.forEach(event => {
254
- events[event] = 'true';
255
- });
265
+ for (const event of answers.events) {
266
+ if (event === 'claude') {
267
+ // If claude was previously configured with advanced options, preserve or update them
268
+ const existingClaudeConfig = project.events && project.events.claude;
269
+ if (existingClaudeConfig && typeof existingClaudeConfig === 'object') {
270
+ const keepConfig = await inquirer.prompt([{
271
+ type: 'confirm',
272
+ name: 'keep',
273
+ message: 'Keep existing Claude configuration?',
274
+ default: true
275
+ }]);
276
+
277
+ if (keepConfig.keep) {
278
+ events[event] = existingClaudeConfig;
279
+ } else {
280
+ events[event] = await me.configureClaudeEvent();
281
+ }
282
+ } else {
283
+ events[event] = await me.configureClaudeEvent();
284
+ }
285
+ } else if (event === 'npm') {
286
+ // If npm was previously configured with advanced options, preserve or update them
287
+ const existingNpmConfig = project.events && project.events.npm;
288
+ if (existingNpmConfig && typeof existingNpmConfig === 'object') {
289
+ const keepConfig = await inquirer.prompt([{
290
+ type: 'confirm',
291
+ name: 'keep',
292
+ message: 'Keep existing NPM configuration?',
293
+ default: true
294
+ }]);
295
+
296
+ if (keepConfig.keep) {
297
+ events[event] = existingNpmConfig;
298
+ } else {
299
+ events[event] = await me.configureNpmEvent();
300
+ }
301
+ } else {
302
+ events[event] = await me.configureNpmEvent();
303
+ }
304
+ } else {
305
+ events[event] = 'true';
306
+ }
307
+ }
256
308
 
257
309
  const updatedProject = {
258
310
  path: answers.path,
@@ -371,6 +423,123 @@ class manage extends command {
371
423
  return me.startManagement();
372
424
  }
373
425
  }
426
+
427
+ async configureClaudeEvent() {
428
+ let me = this;
429
+
430
+ me.log.log('\n⚙️ Configure Claude Event\n');
431
+
432
+ const claudeQuestions = [
433
+ {
434
+ type: 'confirm',
435
+ name: 'useAdvanced',
436
+ message: 'Configure advanced Claude options?',
437
+ default: false
438
+ }
439
+ ];
440
+
441
+ const claudeAnswer = await inquirer.prompt(claudeQuestions);
442
+
443
+ if (!claudeAnswer.useAdvanced) {
444
+ return 'true';
445
+ }
446
+
447
+ const advancedQuestions = [
448
+ {
449
+ type: 'input',
450
+ name: 'flags',
451
+ message: 'Claude flags (comma-separated, e.g. --resume,--debug):',
452
+ filter: (input) => {
453
+ if (!input.trim()) return [];
454
+ return input.split(',').map(flag => flag.trim()).filter(flag => flag);
455
+ }
456
+ },
457
+ {
458
+ type: 'confirm',
459
+ name: 'split_terminal',
460
+ message: 'Enable split terminal (Claude + shell side-by-side with tmux)?',
461
+ default: false
462
+ }
463
+ ];
464
+
465
+ const advancedAnswers = await inquirer.prompt(advancedQuestions);
466
+
467
+ const config = {};
468
+
469
+ if (advancedAnswers.flags && advancedAnswers.flags.length > 0) {
470
+ config.flags = advancedAnswers.flags;
471
+ }
472
+
473
+ if (advancedAnswers.split_terminal) {
474
+ config.split_terminal = true;
475
+ }
476
+
477
+ return config;
478
+ }
479
+
480
+ async configureNpmEvent() {
481
+ let me = this;
482
+
483
+ me.log.log('\n📦 Configure NPM Event\n');
484
+
485
+ const npmQuestions = [
486
+ {
487
+ type: 'input',
488
+ name: 'command',
489
+ message: 'NPM script to run (e.g., dev, start, test):',
490
+ default: 'dev',
491
+ validate: (value) => {
492
+ if (!value.trim()) {
493
+ return 'NPM command cannot be empty';
494
+ }
495
+ return true;
496
+ }
497
+ },
498
+ {
499
+ type: 'confirm',
500
+ name: 'useAdvanced',
501
+ message: 'Configure advanced NPM options?',
502
+ default: false
503
+ }
504
+ ];
505
+
506
+ const basicAnswers = await inquirer.prompt(npmQuestions);
507
+
508
+ if (!basicAnswers.useAdvanced) {
509
+ return basicAnswers.command;
510
+ }
511
+
512
+ const advancedQuestions = [
513
+ {
514
+ type: 'confirm',
515
+ name: 'watch',
516
+ message: 'Enable watch mode (if supported by command)?',
517
+ default: true
518
+ },
519
+ {
520
+ type: 'confirm',
521
+ name: 'auto_restart',
522
+ message: 'Auto-restart on crashes?',
523
+ default: false
524
+ }
525
+ ];
526
+
527
+ const advancedAnswers = await inquirer.prompt(advancedQuestions);
528
+
529
+ const config = {
530
+ command: basicAnswers.command
531
+ };
532
+
533
+ if (advancedAnswers.watch) {
534
+ config.watch = true;
535
+ }
536
+
537
+ if (advancedAnswers.auto_restart) {
538
+ config.auto_restart = true;
539
+ }
540
+
541
+ return config;
542
+ }
374
543
  }
375
544
 
376
545
  manage.define({
package/cli/open.js CHANGED
@@ -3,6 +3,7 @@ const Project = require('../lib/project');
3
3
  const { ProjectEnvironment } = require('../lib/environment');
4
4
  const spawn = require('child_process').spawn;
5
5
  const File = require('phylo');
6
+ const TmuxManager = require('../lib/tmux');
6
7
 
7
8
  class open extends command {
8
9
  execute (params) {
@@ -15,7 +16,7 @@ class open extends command {
15
16
  }
16
17
  }
17
18
 
18
- processProject (project) {
19
+ async processProject (project) {
19
20
  let me = this;
20
21
  let environment = me.root().environment;
21
22
 
@@ -29,7 +30,7 @@ class open extends command {
29
30
  if (project in projects) {
30
31
  let cfg = projects[project];
31
32
  cfg.name = project;
32
- me.switchTo(ProjectEnvironment.load(cfg, me.config.get('project_defaults')));
33
+ await me.switchTo(ProjectEnvironment.load(cfg, me.config.get('project_defaults')));
33
34
  } else {
34
35
  me.log.debug(`Project '${project}' not found, starting interactive mode`);
35
36
  return me.startInteractiveMode(project);
@@ -46,7 +47,7 @@ class open extends command {
46
47
  return interactiveCmd.dispatch(new me.args.constructor([project]))
47
48
  }
48
49
 
49
- switchTo (environment) {
50
+ async switchTo (environment) {
50
51
  let me = this;
51
52
  me.root().environment = environment;
52
53
  let project = environment.project;
@@ -63,7 +64,30 @@ class open extends command {
63
64
  me.shellCommands = [];
64
65
  }
65
66
 
66
- events.forEach(me.processEvent.bind(me));
67
+ // Intelligent layout detection
68
+ const hasCwd = events.includes('cwd');
69
+ const hasClaudeEvent = events.includes('claude');
70
+ const hasNpmEvent = events.includes('npm');
71
+
72
+ if (hasCwd && hasClaudeEvent && hasNpmEvent) {
73
+ // Three-pane layout: Claude + Terminal + NPM
74
+ await me.handleThreePaneLayout(project, isShellMode);
75
+ // Process other events except cwd, claude, and npm
76
+ events.filter(e => !['cwd', 'claude', 'npm'].includes(e)).forEach(me.processEvent.bind(me));
77
+ } else if (hasCwd && hasNpmEvent) {
78
+ // Two-pane layout: Terminal + NPM (no Claude)
79
+ await me.handleTwoPaneNpmLayout(project, isShellMode);
80
+ // Process other events except cwd and npm
81
+ events.filter(e => !['cwd', 'npm'].includes(e)).forEach(me.processEvent.bind(me));
82
+ } else if (hasCwd && hasClaudeEvent) {
83
+ // Two-pane layout: Claude + Terminal (existing split terminal)
84
+ await me.handleSplitTerminal(project, isShellMode);
85
+ // Process other events except cwd and claude
86
+ events.filter(e => !['cwd', 'claude'].includes(e)).forEach(me.processEvent.bind(me));
87
+ } else {
88
+ // Normal event processing
89
+ events.forEach(me.processEvent.bind(me));
90
+ }
67
91
 
68
92
  // Output collected shell commands if in shell mode
69
93
  if (isShellMode && me.shellCommands.length > 0) {
@@ -71,6 +95,167 @@ class open extends command {
71
95
  }
72
96
  }
73
97
 
98
+ async handleSplitTerminal(project, isShellMode) {
99
+ let me = this;
100
+ const tmux = new TmuxManager();
101
+ const claudeConfig = project.events.claude;
102
+ const claudeArgs = (claudeConfig && claudeConfig.flags) ? claudeConfig.flags : [];
103
+
104
+ if (isShellMode) {
105
+ // Check if tmux is available
106
+ if (await tmux.isTmuxAvailable()) {
107
+ const commands = tmux.buildShellCommands(
108
+ project.name,
109
+ project.path.path,
110
+ claudeArgs
111
+ );
112
+ me.shellCommands.push(...commands);
113
+ } else {
114
+ // Fall back to normal behavior if tmux is not available
115
+ me.log.debug('Tmux not available, falling back to normal mode');
116
+ me.shellCommands.push(`cd "${project.path.path}"`);
117
+ const claudeCommand = claudeArgs.length > 0
118
+ ? `claude ${claudeArgs.join(' ')}`
119
+ : 'claude';
120
+ me.shellCommands.push(claudeCommand);
121
+ }
122
+ } else {
123
+ // Direct execution mode
124
+ if (await tmux.isTmuxAvailable()) {
125
+ try {
126
+ const sessionName = await tmux.createSplitSession(
127
+ project.name,
128
+ project.path.path,
129
+ claudeArgs
130
+ );
131
+ await tmux.attachToSession(sessionName);
132
+ } catch (error) {
133
+ me.log.debug(`Failed to create tmux session: ${error.message}`);
134
+ // Fall back to normal behavior
135
+ me.processEvent('cwd');
136
+ me.processEvent('claude');
137
+ }
138
+ } else {
139
+ me.log.debug('Tmux not available, falling back to normal mode');
140
+ // Fall back to normal behavior
141
+ me.processEvent('cwd');
142
+ me.processEvent('claude');
143
+ }
144
+ }
145
+ }
146
+
147
+ async handleThreePaneLayout(project, isShellMode) {
148
+ let me = this;
149
+ const tmux = new TmuxManager();
150
+ const claudeConfig = project.events.claude;
151
+ const claudeArgs = (claudeConfig && claudeConfig.flags) ? claudeConfig.flags : [];
152
+ const npmConfig = project.events.npm;
153
+ const npmCommand = me.getNpmCommand(npmConfig);
154
+
155
+ if (isShellMode) {
156
+ // Check if tmux is available
157
+ if (await tmux.isTmuxAvailable()) {
158
+ const commands = tmux.buildThreePaneShellCommands(
159
+ project.name,
160
+ project.path.path,
161
+ claudeArgs,
162
+ npmCommand
163
+ );
164
+ me.shellCommands.push(...commands);
165
+ } else {
166
+ // Fall back to normal behavior if tmux is not available
167
+ me.log.debug('Tmux not available, falling back to normal mode');
168
+ me.shellCommands.push(`cd "${project.path.path}"`);
169
+ const claudeCommand = claudeArgs.length > 0
170
+ ? `claude ${claudeArgs.join(' ')}`
171
+ : 'claude';
172
+ me.shellCommands.push(claudeCommand);
173
+ me.shellCommands.push(npmCommand);
174
+ }
175
+ } else {
176
+ // Direct execution mode
177
+ if (await tmux.isTmuxAvailable()) {
178
+ try {
179
+ const sessionName = await tmux.createThreePaneSession(
180
+ project.name,
181
+ project.path.path,
182
+ claudeArgs,
183
+ npmCommand
184
+ );
185
+ await tmux.attachToSession(sessionName);
186
+ } catch (error) {
187
+ me.log.debug(`Failed to create tmux session: ${error.message}`);
188
+ // Fall back to normal behavior
189
+ me.processEvent('cwd');
190
+ me.processEvent('claude');
191
+ me.processEvent('npm');
192
+ }
193
+ } else {
194
+ me.log.debug('Tmux not available, falling back to normal mode');
195
+ // Fall back to normal behavior
196
+ me.processEvent('cwd');
197
+ me.processEvent('claude');
198
+ me.processEvent('npm');
199
+ }
200
+ }
201
+ }
202
+
203
+ async handleTwoPaneNpmLayout(project, isShellMode) {
204
+ let me = this;
205
+ const tmux = new TmuxManager();
206
+ const npmConfig = project.events.npm;
207
+ const npmCommand = me.getNpmCommand(npmConfig);
208
+
209
+ if (isShellMode) {
210
+ // Check if tmux is available
211
+ if (await tmux.isTmuxAvailable()) {
212
+ const commands = tmux.buildTwoPaneNpmShellCommands(
213
+ project.name,
214
+ project.path.path,
215
+ npmCommand
216
+ );
217
+ me.shellCommands.push(...commands);
218
+ } else {
219
+ // Fall back to normal behavior if tmux is not available
220
+ me.log.debug('Tmux not available, falling back to normal mode');
221
+ me.shellCommands.push(`cd "${project.path.path}"`);
222
+ me.shellCommands.push(npmCommand);
223
+ }
224
+ } else {
225
+ // Direct execution mode
226
+ if (await tmux.isTmuxAvailable()) {
227
+ try {
228
+ const sessionName = await tmux.createTwoPaneNpmSession(
229
+ project.name,
230
+ project.path.path,
231
+ npmCommand
232
+ );
233
+ await tmux.attachToSession(sessionName);
234
+ } catch (error) {
235
+ me.log.debug(`Failed to create tmux session: ${error.message}`);
236
+ // Fall back to normal behavior
237
+ me.processEvent('cwd');
238
+ me.processEvent('npm');
239
+ }
240
+ } else {
241
+ me.log.debug('Tmux not available, falling back to normal mode');
242
+ // Fall back to normal behavior
243
+ me.processEvent('cwd');
244
+ me.processEvent('npm');
245
+ }
246
+ }
247
+ }
248
+
249
+ getNpmCommand(npmConfig) {
250
+ if (typeof npmConfig === 'string') {
251
+ return `npm run ${npmConfig}`;
252
+ } else if (npmConfig && typeof npmConfig === 'object' && npmConfig.command) {
253
+ return `npm run ${npmConfig.command}`;
254
+ } else {
255
+ return 'npm run dev';
256
+ }
257
+ }
258
+
74
259
  processEvent (event) {
75
260
  let me = this;
76
261
  let environment = me.root().environment;
@@ -126,10 +311,36 @@ class open extends command {
126
311
  }
127
312
  break;
128
313
  case 'claude':
314
+ let claudeArgs = [];
315
+ let claudeConfig = project.events.claude;
316
+
317
+ // Handle advanced Claude configuration
318
+ if (claudeConfig && typeof claudeConfig === 'object') {
319
+ if (claudeConfig.flags && Array.isArray(claudeConfig.flags)) {
320
+ claudeArgs = claudeArgs.concat(claudeConfig.flags);
321
+ }
322
+ // Additional config options can be handled here in the future
323
+ }
324
+
325
+ if (isShellMode) {
326
+ let claudeCommand = claudeArgs.length > 0
327
+ ? `claude ${claudeArgs.join(' ')}`
328
+ : 'claude';
329
+ me.shellCommands.push(claudeCommand);
330
+ } else {
331
+ spawn('claude', claudeArgs, {
332
+ cwd: environment.project.path.path,
333
+ stdio: 'inherit'
334
+ });
335
+ }
336
+ break;
337
+ case 'npm':
338
+ let npmCommand = me.getNpmCommand(project.events.npm);
339
+
129
340
  if (isShellMode) {
130
- me.shellCommands.push(`claude`);
341
+ me.shellCommands.push(npmCommand);
131
342
  } else {
132
- spawn('claude', [], {
343
+ spawn('npm', ['run', npmCommand.replace('npm run ', '')], {
133
344
  cwd: environment.project.path.path,
134
345
  stdio: 'inherit'
135
346
  });
package/docs/ideas.md CHANGED
@@ -2,48 +2,92 @@
2
2
 
3
3
  This document contains ideas for future enhancements to the workon project.
4
4
 
5
- ## Interactive Project Management
6
-
7
- ### Project Configuration Editor
8
- Create an interactive mode for editing project configurations through guided prompts instead of manual file editing.
9
-
10
- **Features:**
11
- - Interactive project creation wizard with step-by-step guidance
12
- - Edit existing project properties (name, path, IDE, events) through prompts
13
- - Validate project paths and IDE commands during configuration
14
- - Preview configuration changes before saving
15
- - Bulk operations for managing multiple projects
16
-
17
- **Implementation considerations:**
18
- - Extend existing inquirer-based interactive system
19
- - Add new command like `workon config` or `workon manage --interactive`
20
- - Provide different flows for:
21
- - Creating new projects
22
- - Editing existing projects
23
- - Bulk project management
24
- - Include validation for:
25
- - Directory paths existence
26
- - IDE command availability
27
- - Event configuration correctness
5
+ ## NPM Command Integration
28
6
 
29
- **Benefits:**
30
- - Lower barrier to entry for new users
31
- - Reduced configuration errors through validation
32
- - More discoverable project management features
33
- - Better UX compared to manual JSON editing
7
+ ### Three-Pane Development Layout
8
+ When `cwd`, `claude`, and `npm` events are enabled, create a three-pane tmux layout:
9
+ - **Left pane**: Claude Code running in project directory (full height)
10
+ - **Top-right pane**: Shell terminal in project directory
11
+ - **Bottom-right pane**: NPM command running (e.g., `npm run dev`, `npm test`)
12
+
13
+ **Implementation approach:**
14
+ - Extend current split terminal to support three panes
15
+ - Create initial vertical split (Claude | Terminal)
16
+ - Split the right terminal pane horizontally (Terminal | npm)
17
+ - Use tmux: `split-window -v` on the right pane
18
+ - Auto-run specified npm command in bottom-right pane
34
19
 
35
- ## Enhanced Events
20
+ **Configuration:**
21
+ ```json
22
+ {
23
+ "events": {
24
+ "cwd": "true",
25
+ "claude": {
26
+ "flags": ["--resume"],
27
+ "split_terminal": true
28
+ },
29
+ "npm": "dev"
30
+ }
31
+ }
32
+ ```
36
33
 
37
- ### Advanced claude Event Options
38
- Extend the claude event with more configuration options:
34
+ **Alternative configuration:**
39
35
  ```json
40
- "claude": {
41
- "mode": "interactive",
42
- "flags": ["--resume"],
43
- "project_context": true
36
+ {
37
+ "events": {
38
+ "cwd": "true",
39
+ "claude": "true",
40
+ "npm": {
41
+ "command": "dev",
42
+ "watch": true,
43
+ "auto_restart": false
44
+ }
45
+ }
44
46
  }
45
47
  ```
46
48
 
49
+ **Benefits:**
50
+ - Complete development environment in one tmux session
51
+ - Claude AI + Terminal + Development server all visible
52
+ - Perfect for web development workflows
53
+ - Automatic npm script execution
54
+
55
+ **Tmux Layout:**
56
+ ```
57
+ ┌──────────────┬──────────────┐
58
+ │ │ Terminal │
59
+ │ Claude ├──────────────┤
60
+ │ (full │ npm run dev │
61
+ │ height) │ │
62
+ └──────────────┴──────────────┘
63
+ ```
64
+
65
+ ### Two-Pane Terminal + NPM Layout
66
+ When `cwd` and `npm` events are enabled (without Claude), create a two-pane tmux layout:
67
+ - **Left pane**: Shell terminal in project directory
68
+ - **Right pane**: NPM command running (e.g., `npm run dev`, `npm test`)
69
+
70
+ **Tmux Layout:**
71
+ ```
72
+ ┌──────────────┬──────────────┐
73
+ │ │ │
74
+ │ Terminal │ npm run dev │
75
+ │ │ │
76
+ │ │ │
77
+ └──────────────┴──────────────┘
78
+ ```
79
+
80
+ **Use cases:**
81
+ - Traditional development workflow without AI assistance
82
+ - Monitoring build output while running commands
83
+ - Side-by-side terminal and dev server
84
+
47
85
  ## Future Ideas
48
86
 
87
+ ### Auto-enable Split Terminal
88
+ When both `cwd` and `claude` events are enabled, automatically enable split terminal mode without requiring explicit configuration.
89
+
90
+ ### Project Templates
91
+ Pre-configured project templates for common development stacks (React, Node.js, Python, etc.) with appropriate events and npm commands.
92
+
49
93
  *Add more ideas here as they come up...*