workon 2.0.0 → 2.1.1
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/lib/tmux_20250807120751.js +190 -0
- package/.history/lib/tmux_20250807120757.js +190 -0
- package/.history/lib/tmux_20250807120802.js +190 -0
- package/.history/lib/tmux_20250807120808.js +190 -0
- package/CHANGELOG.md +14 -0
- package/cli/open.js +115 -13
- package/demo-colon-syntax.js +57 -0
- package/docs/adr/002-positional-command-arguments.md +8 -2
- package/lib/tmux.js +3 -3
- package/package.json +1 -1
- package/test-colon-syntax.js +85 -0
|
@@ -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 -CC 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;
|
|
@@ -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 -CC 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 -CC 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;
|
|
@@ -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 -CC 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 -CC 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 -CC 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;
|
|
@@ -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 -CC 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 -CC 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 -CC 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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
### [2.1.1](https://github.com/israelroldan/workon/compare/v2.1.0...v2.1.1) (2025-08-07)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* Update tmux attach command to use -CC flag for compatibility with iTerm ([2d3d7c7](https://github.com/israelroldan/workon/commit/2d3d7c73fb164f1718908d276f91b8fba004f42d))
|
|
11
|
+
|
|
12
|
+
## [2.1.0](https://github.com/israelroldan/workon/compare/v2.0.0...v2.1.0) (2025-08-07)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* Introduce colon syntax for selective command execution ([7b2193a](https://github.com/israelroldan/workon/commit/7b2193ace8cc014dfd71894e3e9f49e0fdea33b9))
|
|
18
|
+
|
|
5
19
|
## [2.0.0](https://github.com/israelroldan/workon/compare/v2.0.0-alpha.1...v2.0.0) (2025-08-07)
|
|
6
20
|
|
|
7
21
|
## [2.0.0-alpha.1](https://github.com/israelroldan/workon/compare/v1.4.1...v2.0.0-alpha.1) (2025-08-07)
|
package/cli/open.js
CHANGED
|
@@ -21,24 +21,42 @@ class open extends command {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
async processProject (
|
|
24
|
+
async processProject (projectParam) {
|
|
25
25
|
let me = this;
|
|
26
26
|
let environment = me.root().environment;
|
|
27
27
|
|
|
28
|
+
// Parse colon syntax: project:command1,command2
|
|
29
|
+
const [projectName, commandsString] = projectParam.split(':');
|
|
30
|
+
const requestedCommands = commandsString ? commandsString.split(',').map(cmd => cmd.trim()) : null;
|
|
31
|
+
|
|
32
|
+
// Special case: project:help shows available commands for that project
|
|
33
|
+
if (commandsString === 'help') {
|
|
34
|
+
return me.showProjectHelp(projectName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
me.log.debug(`Project: ${projectName}, Commands: ${requestedCommands ? requestedCommands.join(', ') : 'all'}`);
|
|
38
|
+
|
|
28
39
|
let projects = me.config.get('projects');
|
|
29
40
|
if (!projects) {
|
|
30
41
|
me.config.set('projects', {});
|
|
31
42
|
} else {
|
|
32
|
-
if (environment.$isProjectEnvironment && (
|
|
43
|
+
if (environment.$isProjectEnvironment && (projectName === 'this' || projectName === '.')) {
|
|
33
44
|
me.log.info(`Open current: ${environment.project.name}`);
|
|
34
45
|
} else {
|
|
35
|
-
if (
|
|
36
|
-
let cfg = projects[
|
|
37
|
-
cfg.name =
|
|
38
|
-
|
|
46
|
+
if (projectName in projects) {
|
|
47
|
+
let cfg = projects[projectName];
|
|
48
|
+
cfg.name = projectName;
|
|
49
|
+
|
|
50
|
+
// Validate requested commands if specified
|
|
51
|
+
if (requestedCommands) {
|
|
52
|
+
me.validateRequestedCommands(requestedCommands, cfg, projectName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projectEnv = ProjectEnvironment.load(cfg, me.config.get('project_defaults'));
|
|
56
|
+
await me.switchTo(projectEnv, requestedCommands);
|
|
39
57
|
} else {
|
|
40
|
-
me.log.debug(`Project '${
|
|
41
|
-
return me.startInteractiveMode(
|
|
58
|
+
me.log.debug(`Project '${projectName}' not found, starting interactive mode`);
|
|
59
|
+
return me.startInteractiveMode(projectName);
|
|
42
60
|
}
|
|
43
61
|
}
|
|
44
62
|
}
|
|
@@ -52,16 +70,40 @@ class open extends command {
|
|
|
52
70
|
return interactiveCmd.dispatch(new me.args.constructor([project]))
|
|
53
71
|
}
|
|
54
72
|
|
|
55
|
-
|
|
73
|
+
validateRequestedCommands(requestedCommands, projectConfig, projectName) {
|
|
74
|
+
const configuredEvents = Object.keys(projectConfig.events || {});
|
|
75
|
+
const invalidCommands = requestedCommands.filter(cmd => !configuredEvents.includes(cmd));
|
|
76
|
+
|
|
77
|
+
if (invalidCommands.length > 0) {
|
|
78
|
+
const availableCommands = configuredEvents.join(', ');
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Commands not configured for project '${projectName}': ${invalidCommands.join(', ')}\n` +
|
|
81
|
+
`Available commands: ${availableCommands}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async switchTo (environment, requestedCommands = null) {
|
|
56
87
|
let me = this;
|
|
57
88
|
me.root().environment = environment;
|
|
58
89
|
let project = environment.project;
|
|
59
90
|
|
|
60
|
-
|
|
91
|
+
// Determine which events to execute
|
|
92
|
+
let events;
|
|
93
|
+
if (requestedCommands) {
|
|
94
|
+
// Use requested commands (already validated)
|
|
95
|
+
events = me.resolveCommandDependencies(requestedCommands, project);
|
|
96
|
+
me.log.debug(`Executing requested commands: ${events.join(', ')}`);
|
|
97
|
+
} else {
|
|
98
|
+
// Execute all configured events (current behavior)
|
|
99
|
+
events = Object.keys(project.events).filter((e) => project.events[e]);
|
|
100
|
+
me.log.debug(`Executing all configured commands: ${events.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
61
103
|
me.log.debug(`Shell is ${process.env.SHELL}`);
|
|
62
104
|
me.log.debug(`Project path is ${project.path.path}`);
|
|
63
105
|
me.log.debug(`IDE command is: ${project.ide}`);
|
|
64
|
-
me.log.debug(`
|
|
106
|
+
me.log.debug(`Final events to execute: ${events.join(', ')}`);
|
|
65
107
|
|
|
66
108
|
// Initialize shell commands collector if in shell mode
|
|
67
109
|
let isShellMode = me.params.shell || me.root().params.shell;
|
|
@@ -69,7 +111,7 @@ class open extends command {
|
|
|
69
111
|
me.shellCommands = [];
|
|
70
112
|
}
|
|
71
113
|
|
|
72
|
-
// Intelligent layout detection
|
|
114
|
+
// Intelligent layout detection based on actual events being executed
|
|
73
115
|
const hasCwd = events.includes('cwd');
|
|
74
116
|
const hasClaudeEvent = events.includes('claude');
|
|
75
117
|
const hasNpmEvent = events.includes('npm');
|
|
@@ -96,7 +138,7 @@ class open extends command {
|
|
|
96
138
|
await me.processEvent(event);
|
|
97
139
|
}
|
|
98
140
|
} else {
|
|
99
|
-
// Normal event processing
|
|
141
|
+
// Normal event processing - execute commands individually
|
|
100
142
|
for (const event of events) {
|
|
101
143
|
await me.processEvent(event);
|
|
102
144
|
}
|
|
@@ -108,6 +150,66 @@ class open extends command {
|
|
|
108
150
|
}
|
|
109
151
|
}
|
|
110
152
|
|
|
153
|
+
showProjectHelp(projectName) {
|
|
154
|
+
let me = this;
|
|
155
|
+
let projects = me.config.get('projects');
|
|
156
|
+
|
|
157
|
+
if (!projects || !(projectName in projects)) {
|
|
158
|
+
me.log.error(`Project '${projectName}' not found`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const projectConfig = projects[projectName];
|
|
163
|
+
const configuredEvents = Object.keys(projectConfig.events || {});
|
|
164
|
+
|
|
165
|
+
console.log(`\n📋 Available commands for '${projectName}':`);
|
|
166
|
+
console.log('─'.repeat(50));
|
|
167
|
+
|
|
168
|
+
for (const eventName of configuredEvents) {
|
|
169
|
+
const command = registry.getCommandByName(eventName);
|
|
170
|
+
if (command && command.metadata) {
|
|
171
|
+
const config = projectConfig.events[eventName];
|
|
172
|
+
let configDesc = '';
|
|
173
|
+
if (config !== true && config !== 'true') {
|
|
174
|
+
if (typeof config === 'object') {
|
|
175
|
+
configDesc = ` (${JSON.stringify(config)})`;
|
|
176
|
+
} else {
|
|
177
|
+
configDesc = ` (${config})`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log(` ${eventName.padEnd(8)} - ${command.metadata.description}${configDesc}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log('\n💡 Usage examples:');
|
|
185
|
+
console.log(` workon ${projectName} # Execute all commands`);
|
|
186
|
+
console.log(` workon ${projectName}:cwd # Just change directory`);
|
|
187
|
+
console.log(` workon ${projectName}:claude # Just Claude (auto-adds cwd)`);
|
|
188
|
+
|
|
189
|
+
if (configuredEvents.length > 1) {
|
|
190
|
+
const twoCommands = configuredEvents.slice(0, 2).join(',');
|
|
191
|
+
console.log(` workon ${projectName}:${twoCommands.padEnd(12)} # Multiple commands`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(` workon ${projectName}:cwd --shell # Output shell commands\n`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
resolveCommandDependencies(requestedCommands, project) {
|
|
198
|
+
const resolved = [...requestedCommands];
|
|
199
|
+
|
|
200
|
+
// Auto-add cwd dependency for commands that need it
|
|
201
|
+
const needsCwd = ['claude', 'npm', 'ide'];
|
|
202
|
+
const needsCwdCommands = requestedCommands.filter(cmd => needsCwd.includes(cmd));
|
|
203
|
+
|
|
204
|
+
if (needsCwdCommands.length > 0 && !requestedCommands.includes('cwd')) {
|
|
205
|
+
resolved.unshift('cwd'); // Add cwd at the beginning
|
|
206
|
+
this.log.debug(`Auto-added 'cwd' dependency for commands: ${needsCwdCommands.join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Remove duplicates while preserving order
|
|
210
|
+
return [...new Set(resolved)];
|
|
211
|
+
}
|
|
212
|
+
|
|
111
213
|
async handleSplitTerminal(project, isShellMode) {
|
|
112
214
|
let me = this;
|
|
113
215
|
const tmux = new TmuxManager();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
console.log(`
|
|
4
|
+
🎯 COLON SYNTAX FEATURE DEMONSTRATION
|
|
5
|
+
=====================================
|
|
6
|
+
|
|
7
|
+
The new colon syntax allows selective command execution for projects:
|
|
8
|
+
|
|
9
|
+
📋 SYNTAX:
|
|
10
|
+
workon <project> # Execute all configured commands
|
|
11
|
+
workon <project>:<command> # Execute single command
|
|
12
|
+
workon <project>:<cmd1,cmd2> # Execute multiple commands
|
|
13
|
+
workon <project>:help # Show available commands
|
|
14
|
+
|
|
15
|
+
✨ KEY FEATURES:
|
|
16
|
+
|
|
17
|
+
1. BACKWARD COMPATIBLE
|
|
18
|
+
workon my-project # Still works exactly as before
|
|
19
|
+
|
|
20
|
+
2. SELECTIVE EXECUTION
|
|
21
|
+
workon my-project:cwd # Just change directory
|
|
22
|
+
workon my-project:claude # Just open Claude
|
|
23
|
+
|
|
24
|
+
3. SMART DEPENDENCIES
|
|
25
|
+
workon my-project:claude # Auto-adds 'cwd' dependency
|
|
26
|
+
workon my-project:npm # Auto-adds 'cwd' dependency
|
|
27
|
+
|
|
28
|
+
4. MULTIPLE COMMANDS
|
|
29
|
+
workon my-project:cwd,claude,npm # Custom combinations
|
|
30
|
+
|
|
31
|
+
5. PROJECT HELP
|
|
32
|
+
workon my-project:help # Show what commands are available
|
|
33
|
+
|
|
34
|
+
6. ERROR VALIDATION
|
|
35
|
+
workon my-project:invalid # Clear error messages
|
|
36
|
+
|
|
37
|
+
7. SHELL MODE SUPPORT
|
|
38
|
+
workon my-project:cwd --shell # Works with all flags
|
|
39
|
+
|
|
40
|
+
🏗️ IMPLEMENTATION HIGHLIGHTS:
|
|
41
|
+
|
|
42
|
+
• Zero switchit changes needed - uses existing parameter parsing
|
|
43
|
+
• Simple string split logic: project.split(':')
|
|
44
|
+
• Integrates perfectly with Command-Centric Architecture
|
|
45
|
+
• Smart layout detection works with any command combination
|
|
46
|
+
• Comprehensive validation and dependency resolution
|
|
47
|
+
|
|
48
|
+
🎉 BENEFITS:
|
|
49
|
+
|
|
50
|
+
• Faster startup for individual commands
|
|
51
|
+
• Flexible workflow matching
|
|
52
|
+
• Resource efficiency
|
|
53
|
+
• Better testing and debugging
|
|
54
|
+
• Foundation for future features (aliases, profiles, etc.)
|
|
55
|
+
|
|
56
|
+
This feature transforms workon from "all-or-nothing" to "pick-what-you-need"!
|
|
57
|
+
`);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# ADR-002: Positional Command Arguments
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
4
|
-
**Date:** 2025-08-
|
|
3
|
+
**Status:** Implemented (as Colon Syntax)
|
|
4
|
+
**Date:** 2025-08-07
|
|
5
5
|
**Deciders:** Israel Roldan
|
|
6
6
|
**Related:** ADR-001 (Command-Centric Architecture)
|
|
7
7
|
|
|
8
|
+
**Implementation Note:** Instead of positional arguments, we implemented a cleaner colon syntax approach:
|
|
9
|
+
- `workon project` - Execute all commands
|
|
10
|
+
- `workon project:cwd` - Execute single command
|
|
11
|
+
- `workon project:cwd,claude` - Execute multiple commands
|
|
12
|
+
- `workon project:help` - Show available commands
|
|
13
|
+
|
|
8
14
|
## Context
|
|
9
15
|
|
|
10
16
|
Currently, the workon CLI operates with a "all-or-nothing" approach where running `workon my-project` executes all configured events for that project. However, there are scenarios where users want to execute only specific commands for a project.
|
package/lib/tmux.js
CHANGED
|
@@ -136,7 +136,7 @@ class TmuxManager {
|
|
|
136
136
|
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
137
137
|
process.env.TMUX
|
|
138
138
|
? `tmux switch-client -t "${sessionName}"`
|
|
139
|
-
: `tmux attach-session -t "${sessionName}"`
|
|
139
|
+
: `tmux -CC attach-session -t "${sessionName}"`
|
|
140
140
|
];
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -155,7 +155,7 @@ class TmuxManager {
|
|
|
155
155
|
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
156
156
|
process.env.TMUX
|
|
157
157
|
? `tmux switch-client -t "${sessionName}"`
|
|
158
|
-
: `tmux attach-session -t "${sessionName}"`
|
|
158
|
+
: `tmux -CC attach-session -t "${sessionName}"`
|
|
159
159
|
];
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -170,7 +170,7 @@ class TmuxManager {
|
|
|
170
170
|
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
171
171
|
process.env.TMUX
|
|
172
172
|
? `tmux switch-client -t "${sessionName}"`
|
|
173
|
-
: `tmux attach-session -t "${sessionName}"`
|
|
173
|
+
: `tmux -CC attach-session -t "${sessionName}"`
|
|
174
174
|
];
|
|
175
175
|
}
|
|
176
176
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Comprehensive test of the colon syntax feature
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
console.log('🧪 Testing Colon Syntax Feature\n');
|
|
7
|
+
|
|
8
|
+
const tests = [
|
|
9
|
+
{
|
|
10
|
+
name: 'All commands (backward compatibility)',
|
|
11
|
+
command: 'node bin/workon test-project --shell',
|
|
12
|
+
expectShellCommands: 3 // cwd, claude, npm in tmux
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'Single command: cwd only',
|
|
16
|
+
command: 'node bin/workon test-project:cwd --shell',
|
|
17
|
+
expectOutput: 'cd "/users/israelroldan/code/test-project"'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Single command: claude (auto-adds cwd)',
|
|
21
|
+
command: 'node bin/workon test-project:claude --shell',
|
|
22
|
+
expectTmux: true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Multiple commands: cwd,npm',
|
|
26
|
+
command: 'node bin/workon test-project:cwd,npm --shell',
|
|
27
|
+
expectTmux: true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Project help',
|
|
31
|
+
command: 'node bin/workon test-project:help',
|
|
32
|
+
expectOutput: 'Available commands for'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Invalid command validation',
|
|
36
|
+
command: 'node bin/workon test-project:invalid 2>&1 || true',
|
|
37
|
+
expectOutput: 'Commands not configured'
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
let passed = 0;
|
|
42
|
+
let failed = 0;
|
|
43
|
+
|
|
44
|
+
for (const test of tests) {
|
|
45
|
+
try {
|
|
46
|
+
console.log(`Testing: ${test.name}`);
|
|
47
|
+
const output = execSync(test.command, { encoding: 'utf8', timeout: 10000 });
|
|
48
|
+
|
|
49
|
+
let success = false;
|
|
50
|
+
|
|
51
|
+
if (test.expectOutput) {
|
|
52
|
+
success = output.includes(test.expectOutput);
|
|
53
|
+
} else if (test.expectTmux) {
|
|
54
|
+
success = output.includes('tmux');
|
|
55
|
+
} else if (test.expectShellCommands) {
|
|
56
|
+
const lines = output.trim().split('\n').filter(line => line.trim() && !line.startsWith('#') && !line.startsWith('ℹ'));
|
|
57
|
+
success = lines.length >= test.expectShellCommands;
|
|
58
|
+
} else {
|
|
59
|
+
success = true; // Just check it doesn't crash
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (success) {
|
|
63
|
+
console.log(' ✅ PASS');
|
|
64
|
+
passed++;
|
|
65
|
+
} else {
|
|
66
|
+
console.log(' ❌ FAIL');
|
|
67
|
+
console.log(` Expected: ${test.expectOutput || test.expectTmux || test.expectShellCommands}`);
|
|
68
|
+
console.log(` Got: ${output.substring(0, 200)}...`);
|
|
69
|
+
failed++;
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.log(' ❌ ERROR:', error.message);
|
|
73
|
+
failed++;
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`\n📊 Results: ${passed} passed, ${failed} failed`);
|
|
79
|
+
|
|
80
|
+
if (failed === 0) {
|
|
81
|
+
console.log('🎉 All tests passed! Colon syntax is working perfectly.');
|
|
82
|
+
} else {
|
|
83
|
+
console.log('⚠️ Some tests failed. Please check the implementation.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|