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/.history/.gitignore_20250806202718 +30 -0
- package/.history/.gitignore_20250806231444 +32 -0
- package/.history/.gitignore_20250806231450 +32 -0
- package/.history/lib/tmux_20250806233103.js +109 -0
- package/.history/lib/tmux_20250806233219.js +109 -0
- package/.history/lib/tmux_20250806233223.js +109 -0
- package/.history/lib/tmux_20250806233230.js +109 -0
- package/.history/lib/tmux_20250806233231.js +109 -0
- package/CHANGELOG.md +21 -0
- package/cli/manage.js +179 -10
- package/cli/open.js +217 -6
- package/docs/ideas.md +79 -35
- package/lib/tmux.js +190 -0
- package/lib/validation.js +71 -1
- package/package.json +1 -1
- package/.history/package_20250806214300.json +0 -42
- package/.history/package_20250806215528.json +0 -43
package/lib/tmux.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const exec = promisify(require('child_process').exec);
|
|
4
|
+
|
|
5
|
+
class TmuxManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.sessionPrefix = 'workon-';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async isTmuxAvailable() {
|
|
11
|
+
try {
|
|
12
|
+
await exec('which tmux');
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async sessionExists(sessionName) {
|
|
20
|
+
try {
|
|
21
|
+
await exec(`tmux has-session -t "${sessionName}"`);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getSessionName(projectName) {
|
|
29
|
+
return `${this.sessionPrefix}${projectName}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async killSession(sessionName) {
|
|
33
|
+
try {
|
|
34
|
+
await exec(`tmux kill-session -t "${sessionName}"`);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async createSplitSession(projectName, projectPath, claudeArgs = []) {
|
|
42
|
+
const sessionName = this.getSessionName(projectName);
|
|
43
|
+
|
|
44
|
+
// Kill existing session if it exists
|
|
45
|
+
if (await this.sessionExists(sessionName)) {
|
|
46
|
+
await this.killSession(sessionName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const claudeCommand = claudeArgs.length > 0
|
|
50
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
51
|
+
: 'claude';
|
|
52
|
+
|
|
53
|
+
// Create new tmux session with claude in the first pane
|
|
54
|
+
await exec(`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
|
|
55
|
+
|
|
56
|
+
// Split window horizontally and run shell in second pane
|
|
57
|
+
await exec(`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`);
|
|
58
|
+
|
|
59
|
+
// Set focus on claude pane (left pane)
|
|
60
|
+
await exec(`tmux select-pane -t "${sessionName}:0.0"`);
|
|
61
|
+
|
|
62
|
+
return sessionName;
|
|
63
|
+
}
|
|
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
|
+
|
|
112
|
+
async attachToSession(sessionName) {
|
|
113
|
+
// Check if we're already in a tmux session
|
|
114
|
+
if (process.env.TMUX) {
|
|
115
|
+
// If we're already in tmux, switch to the session
|
|
116
|
+
await exec(`tmux switch-client -t "${sessionName}"`);
|
|
117
|
+
} else {
|
|
118
|
+
// If not in tmux, attach to the session
|
|
119
|
+
spawn('tmux', ['-CC', 'attach-session', '-t', sessionName], {
|
|
120
|
+
stdio: 'inherit'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
buildShellCommands(projectName, projectPath, claudeArgs = []) {
|
|
126
|
+
const sessionName = this.getSessionName(projectName);
|
|
127
|
+
const claudeCommand = claudeArgs.length > 0
|
|
128
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
129
|
+
: 'claude';
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
`# Create tmux split session for ${projectName}`,
|
|
133
|
+
`tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
|
|
134
|
+
`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
135
|
+
`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
136
|
+
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
137
|
+
process.env.TMUX
|
|
138
|
+
? `tmux switch-client -t "${sessionName}"`
|
|
139
|
+
: `tmux attach-session -t "${sessionName}"`
|
|
140
|
+
];
|
|
141
|
+
}
|
|
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
|
+
|
|
177
|
+
async listWorkonSessions() {
|
|
178
|
+
try {
|
|
179
|
+
const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
|
|
180
|
+
return stdout.trim()
|
|
181
|
+
.split('\n')
|
|
182
|
+
.filter(session => session.startsWith(this.sessionPrefix))
|
|
183
|
+
.map(session => session.replace(this.sessionPrefix, ''));
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = TmuxManager;
|
package/lib/validation.js
CHANGED
|
@@ -79,13 +79,83 @@ 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) {
|
|
86
86
|
return `Invalid events: ${invalidEvents.join(', ')}. Valid events: ${validEvents.join(', ')}`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Validate claude event configuration if present
|
|
90
|
+
if (events.claude && typeof events.claude === 'object') {
|
|
91
|
+
const claudeValidation = this.validateClaudeConfig(events.claude);
|
|
92
|
+
if (claudeValidation !== true) {
|
|
93
|
+
return claudeValidation;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
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
|
+
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
validateClaudeConfig(config) {
|
|
109
|
+
if (typeof config !== 'object') {
|
|
110
|
+
return 'Claude configuration must be an object';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validate flags if present
|
|
114
|
+
if (config.flags) {
|
|
115
|
+
if (!Array.isArray(config.flags)) {
|
|
116
|
+
return 'Claude flags must be an array';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Basic flag validation - ensure they start with - or --
|
|
120
|
+
const invalidFlags = config.flags.filter(flag =>
|
|
121
|
+
typeof flag !== 'string' || (!flag.startsWith('-') && !flag.startsWith('--'))
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (invalidFlags.length > 0) {
|
|
125
|
+
return `Invalid Claude flags: ${invalidFlags.join(', ')}. Flags must start with - or --`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validate split_terminal if present
|
|
130
|
+
if (config.split_terminal !== undefined && typeof config.split_terminal !== 'boolean') {
|
|
131
|
+
return 'Claude split_terminal must be a boolean';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
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
|
+
|
|
89
159
|
return true;
|
|
90
160
|
}
|
|
91
161
|
|
package/package.json
CHANGED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "workon",
|
|
3
|
-
"version": "1.1.0",
|
|
4
|
-
"description": "Work on something great!",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"release": "standard-version"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+ssh://git@github.com/israelroldan/workon.git"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"productivity"
|
|
15
|
-
],
|
|
16
|
-
"author": "Israel Roldan (me@isro.me)",
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"cz-conventional-changelog": "^2.0.0",
|
|
20
|
-
"standard-version": "^4.2.0"
|
|
21
|
-
},
|
|
22
|
-
"config": {
|
|
23
|
-
"commitizen": {
|
|
24
|
-
"path": "./node_modules/cz-conventional-changelog"
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"dependencies": {
|
|
28
|
-
"conf": "^1.1.2",
|
|
29
|
-
"deep-assign": "^2.0.0",
|
|
30
|
-
"flat": "^2.0.1",
|
|
31
|
-
"inquirer": "^3.1.1",
|
|
32
|
-
"loog": "^1.4.0",
|
|
33
|
-
"omelette": "^0.4.4",
|
|
34
|
-
"openurl2": "^1.0.1",
|
|
35
|
-
"phylo": "^1.0.0-beta.7",
|
|
36
|
-
"simple-git": "^1.73.0",
|
|
37
|
-
"switchit": "^1.0.7"
|
|
38
|
-
},
|
|
39
|
-
"bin": {
|
|
40
|
-
"workon": "bin/workon"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "workon",
|
|
3
|
-
"version": "1.1.0",
|
|
4
|
-
"description": "Work on something great!",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"release": "standard-version"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+ssh://git@github.com/israelroldan/workon.git"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"productivity"
|
|
15
|
-
],
|
|
16
|
-
"author": "Israel Roldan (me@isro.me)",
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"cz-conventional-changelog": "^2.0.0",
|
|
20
|
-
"standard-version": "^4.2.0"
|
|
21
|
-
},
|
|
22
|
-
"config": {
|
|
23
|
-
"commitizen": {
|
|
24
|
-
"path": "./node_modules/cz-conventional-changelog"
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"dependencies": {
|
|
28
|
-
"conf": "^1.1.2",
|
|
29
|
-
"deep-assign": "^2.0.0",
|
|
30
|
-
"flat": "^2.0.1",
|
|
31
|
-
"inquirer": "^3.1.1",
|
|
32
|
-
"loog": "^1.4.0",
|
|
33
|
-
"omelette": "^0.4.4",
|
|
34
|
-
"openurl2": "^1.0.1",
|
|
35
|
-
"phylo": "^1.0.0-beta.7",
|
|
36
|
-
"simple-git": "^1.73.0",
|
|
37
|
-
"switchit": "^1.0.7"
|
|
38
|
-
},
|
|
39
|
-
"bin": {
|
|
40
|
-
"workon": "bin/workon",
|
|
41
|
-
"wo": "bin/workon"
|
|
42
|
-
}
|
|
43
|
-
}
|