workon 1.3.0 ā 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/CHANGELOG.md +10 -0
- package/cli/manage.js +91 -2
- package/cli/open.js +140 -7
- package/docs/ideas.md +71 -65
- package/lib/tmux.js +81 -0
- package/lib/validation.js +34 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
<a name="1.4.0"></a>
|
|
6
|
+
# [1.4.0](https://github.com/israelroldan/workon/compare/v1.3.0...v1.4.0) (2025-08-06)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Integrate NPM command support and enhance terminal layouts ([f877ec4](https://github.com/israelroldan/workon/commit/f877ec4))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
5
15
|
<a name="1.3.0"></a>
|
|
6
16
|
# [1.3.0](https://github.com/israelroldan/workon/compare/v1.2.1...v1.3.0) (2025-08-06)
|
|
7
17
|
|
package/cli/manage.js
CHANGED
|
@@ -127,7 +127,8 @@ 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
|
];
|
|
@@ -141,6 +142,10 @@ class manage extends command {
|
|
|
141
142
|
// Ask for Claude-specific configuration
|
|
142
143
|
const claudeConfig = await me.configureClaudeEvent();
|
|
143
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;
|
|
144
149
|
} else {
|
|
145
150
|
events[event] = 'true';
|
|
146
151
|
}
|
|
@@ -247,7 +252,8 @@ class manage extends command {
|
|
|
247
252
|
{ name: 'Change directory (cwd)', value: 'cwd', checked: currentEvents.includes('cwd') },
|
|
248
253
|
{ name: 'Open in IDE', value: 'ide', checked: currentEvents.includes('ide') },
|
|
249
254
|
{ name: 'Open homepage in browser', value: 'web', checked: currentEvents.includes('web') },
|
|
250
|
-
{ 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') }
|
|
251
257
|
]
|
|
252
258
|
}
|
|
253
259
|
];
|
|
@@ -276,6 +282,25 @@ class manage extends command {
|
|
|
276
282
|
} else {
|
|
277
283
|
events[event] = await me.configureClaudeEvent();
|
|
278
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
|
+
}
|
|
279
304
|
} else {
|
|
280
305
|
events[event] = 'true';
|
|
281
306
|
}
|
|
@@ -451,6 +476,70 @@ class manage extends command {
|
|
|
451
476
|
|
|
452
477
|
return config;
|
|
453
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
|
+
}
|
|
454
543
|
}
|
|
455
544
|
|
|
456
545
|
manage.define({
|
package/cli/open.js
CHANGED
|
@@ -64,17 +64,26 @@ class open extends command {
|
|
|
64
64
|
me.shellCommands = [];
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
//
|
|
67
|
+
// Intelligent layout detection
|
|
68
68
|
const hasCwd = events.includes('cwd');
|
|
69
69
|
const hasClaudeEvent = events.includes('claude');
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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)
|
|
75
84
|
await me.handleSplitTerminal(project, isShellMode);
|
|
76
85
|
// Process other events except cwd and claude
|
|
77
|
-
events.filter(e =>
|
|
86
|
+
events.filter(e => !['cwd', 'claude'].includes(e)).forEach(me.processEvent.bind(me));
|
|
78
87
|
} else {
|
|
79
88
|
// Normal event processing
|
|
80
89
|
events.forEach(me.processEvent.bind(me));
|
|
@@ -135,6 +144,118 @@ class open extends command {
|
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
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
|
+
|
|
138
259
|
processEvent (event) {
|
|
139
260
|
let me = this;
|
|
140
261
|
let environment = me.root().environment;
|
|
@@ -213,6 +334,18 @@ class open extends command {
|
|
|
213
334
|
});
|
|
214
335
|
}
|
|
215
336
|
break;
|
|
337
|
+
case 'npm':
|
|
338
|
+
let npmCommand = me.getNpmCommand(project.events.npm);
|
|
339
|
+
|
|
340
|
+
if (isShellMode) {
|
|
341
|
+
me.shellCommands.push(npmCommand);
|
|
342
|
+
} else {
|
|
343
|
+
spawn('npm', ['run', npmCommand.replace('npm run ', '')], {
|
|
344
|
+
cwd: environment.project.path.path,
|
|
345
|
+
stdio: 'inherit'
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
216
349
|
}
|
|
217
350
|
}
|
|
218
351
|
if (`before${capitalEvt}` in scripts) {
|
package/docs/ideas.md
CHANGED
|
@@ -2,86 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
This document contains ideas for future enhancements to the workon project.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## NPM Command Integration
|
|
6
6
|
|
|
7
|
-
###
|
|
8
|
-
|
|
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`)
|
|
9
12
|
|
|
10
|
-
**
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
|
28
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
## Enhanced Events
|
|
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
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
The claude event now supports advanced configuration options:
|
|
20
|
+
**Configuration:**
|
|
39
21
|
```json
|
|
40
|
-
|
|
41
|
-
"
|
|
22
|
+
{
|
|
23
|
+
"events": {
|
|
24
|
+
"cwd": "true",
|
|
25
|
+
"claude": {
|
|
26
|
+
"flags": ["--resume"],
|
|
27
|
+
"split_terminal": true
|
|
28
|
+
},
|
|
29
|
+
"npm": "dev"
|
|
30
|
+
}
|
|
42
31
|
}
|
|
43
32
|
```
|
|
44
33
|
|
|
45
|
-
**
|
|
46
|
-
|
|
47
|
-
### Split Terminal with Claude + CWD
|
|
48
|
-
When both `claude` and `cwd` events are enabled, automatically create a split terminal layout:
|
|
49
|
-
- **Left pane**: Claude Code running in project directory
|
|
50
|
-
- **Right pane**: Shell terminal in project directory
|
|
51
|
-
|
|
52
|
-
**Implementation approach:**
|
|
53
|
-
- Use tmux to create split session
|
|
54
|
-
- Detect when both events are present
|
|
55
|
-
- Create session: `tmux new-session -d -s "workon-{project}"`
|
|
56
|
-
- Split horizontally: `tmux split-window -h`
|
|
57
|
-
- Run claude in left pane, shell in right pane
|
|
58
|
-
- Attach to session
|
|
59
|
-
|
|
60
|
-
**Configuration:**
|
|
34
|
+
**Alternative configuration:**
|
|
61
35
|
```json
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
36
|
+
{
|
|
37
|
+
"events": {
|
|
38
|
+
"cwd": "true",
|
|
39
|
+
"claude": "true",
|
|
40
|
+
"npm": {
|
|
41
|
+
"command": "dev",
|
|
42
|
+
"watch": true,
|
|
43
|
+
"auto_restart": false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
65
46
|
}
|
|
66
47
|
```
|
|
67
48
|
|
|
68
49
|
**Benefits:**
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
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
|
|
72
54
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
āāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāā
|
|
83
78
|
```
|
|
84
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
|
+
|
|
85
85
|
## Future Ideas
|
|
86
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
|
+
|
|
87
93
|
*Add more ideas here as they come up...*
|
package/lib/tmux.js
CHANGED
|
@@ -62,6 +62,53 @@ class TmuxManager {
|
|
|
62
62
|
return sessionName;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
async createThreePaneSession(projectName, projectPath, claudeArgs = [], npmCommand = 'npm run dev') {
|
|
66
|
+
const sessionName = this.getSessionName(projectName);
|
|
67
|
+
|
|
68
|
+
// Kill existing session if it exists
|
|
69
|
+
if (await this.sessionExists(sessionName)) {
|
|
70
|
+
await this.killSession(sessionName);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const claudeCommand = claudeArgs.length > 0
|
|
74
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
75
|
+
: 'claude';
|
|
76
|
+
|
|
77
|
+
// Create new tmux session with claude in the first pane (left side)
|
|
78
|
+
await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
|
|
79
|
+
|
|
80
|
+
// Split window vertically - creates right side
|
|
81
|
+
await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
|
|
82
|
+
|
|
83
|
+
// Split the right pane horizontally - creates top-right and bottom-right
|
|
84
|
+
await exec(`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`);
|
|
85
|
+
|
|
86
|
+
// Set focus on claude pane (left pane)
|
|
87
|
+
await exec(`tmux select-pane -t "${sessionName}:0.0"`);
|
|
88
|
+
|
|
89
|
+
return sessionName;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async createTwoPaneNpmSession(projectName, projectPath, npmCommand = 'npm run dev') {
|
|
93
|
+
const sessionName = this.getSessionName(projectName);
|
|
94
|
+
|
|
95
|
+
// Kill existing session if it exists
|
|
96
|
+
if (await this.sessionExists(sessionName)) {
|
|
97
|
+
await this.killSession(sessionName);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create new tmux session with shell in the first pane (left side)
|
|
101
|
+
await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`);
|
|
102
|
+
|
|
103
|
+
// Split window vertically and run npm command in right pane
|
|
104
|
+
await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`);
|
|
105
|
+
|
|
106
|
+
// Set focus on terminal pane (left pane)
|
|
107
|
+
await exec(`tmux select-pane -t "${sessionName}:0.0"`);
|
|
108
|
+
|
|
109
|
+
return sessionName;
|
|
110
|
+
}
|
|
111
|
+
|
|
65
112
|
async attachToSession(sessionName) {
|
|
66
113
|
// Check if we're already in a tmux session
|
|
67
114
|
if (process.env.TMUX) {
|
|
@@ -93,6 +140,40 @@ class TmuxManager {
|
|
|
93
140
|
];
|
|
94
141
|
}
|
|
95
142
|
|
|
143
|
+
buildThreePaneShellCommands(projectName, projectPath, claudeArgs = [], npmCommand = 'npm run dev') {
|
|
144
|
+
const sessionName = this.getSessionName(projectName);
|
|
145
|
+
const claudeCommand = claudeArgs.length > 0
|
|
146
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
147
|
+
: 'claude';
|
|
148
|
+
|
|
149
|
+
return [
|
|
150
|
+
`# Create tmux three-pane session for ${projectName}`,
|
|
151
|
+
`tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
|
|
152
|
+
`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
153
|
+
`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
154
|
+
`tmux split-window -v -t "${sessionName}:0.1" -c "${projectPath}" '${npmCommand}'`,
|
|
155
|
+
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
156
|
+
process.env.TMUX
|
|
157
|
+
? `tmux switch-client -t "${sessionName}"`
|
|
158
|
+
: `tmux attach-session -t "${sessionName}"`
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
buildTwoPaneNpmShellCommands(projectName, projectPath, npmCommand = 'npm run dev') {
|
|
163
|
+
const sessionName = this.getSessionName(projectName);
|
|
164
|
+
|
|
165
|
+
return [
|
|
166
|
+
`# Create tmux two-pane session with npm for ${projectName}`,
|
|
167
|
+
`tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
|
|
168
|
+
`tmux new-session -d -s "${sessionName}" -c "${projectPath}"`,
|
|
169
|
+
`tmux split-window -h -t "${sessionName}" -c "${projectPath}" '${npmCommand}'`,
|
|
170
|
+
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
171
|
+
process.env.TMUX
|
|
172
|
+
? `tmux switch-client -t "${sessionName}"`
|
|
173
|
+
: `tmux attach-session -t "${sessionName}"`
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
96
177
|
async listWorkonSessions() {
|
|
97
178
|
try {
|
|
98
179
|
const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
|
package/lib/validation.js
CHANGED
|
@@ -79,7 +79,7 @@ class ProjectValidator {
|
|
|
79
79
|
return 'Events must be an object';
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const validEvents = ['cwd', 'ide', 'web', 'claude'];
|
|
82
|
+
const validEvents = ['cwd', 'ide', 'web', 'claude', 'npm'];
|
|
83
83
|
const invalidEvents = Object.keys(events).filter(event => !validEvents.includes(event));
|
|
84
84
|
|
|
85
85
|
if (invalidEvents.length > 0) {
|
|
@@ -94,6 +94,14 @@ class ProjectValidator {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Validate npm event configuration if present
|
|
98
|
+
if (events.npm && typeof events.npm === 'object') {
|
|
99
|
+
const npmValidation = this.validateNpmConfig(events.npm);
|
|
100
|
+
if (npmValidation !== true) {
|
|
101
|
+
return npmValidation;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
return true;
|
|
98
106
|
}
|
|
99
107
|
|
|
@@ -126,6 +134,31 @@ class ProjectValidator {
|
|
|
126
134
|
return true;
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
validateNpmConfig(config) {
|
|
138
|
+
if (typeof config !== 'object') {
|
|
139
|
+
return 'NPM configuration must be an object';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate command if present
|
|
143
|
+
if (config.command) {
|
|
144
|
+
if (typeof config.command !== 'string' || config.command.trim() === '') {
|
|
145
|
+
return 'NPM command must be a non-empty string';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Validate watch if present
|
|
150
|
+
if (config.watch !== undefined && typeof config.watch !== 'boolean') {
|
|
151
|
+
return 'NPM watch must be a boolean';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate auto_restart if present
|
|
155
|
+
if (config.auto_restart !== undefined && typeof config.auto_restart !== 'boolean') {
|
|
156
|
+
return 'NPM auto_restart must be a boolean';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
129
162
|
validateUrl(url) {
|
|
130
163
|
if (!url) return true; // Optional field
|
|
131
164
|
|