workon 1.2.1 → 1.3.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 +11 -0
- package/cli/manage.js +88 -8
- package/cli/open.js +84 -6
- package/docs/ideas.md +42 -4
- package/lib/tmux.js +109 -0
- package/lib/validation.js +37 -0
- package/package.json +1 -1
- package/.history/package_20250806214300.json +0 -42
- package/.history/package_20250806215528.json +0 -43
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ---> Node
|
|
2
|
+
# Logs
|
|
3
|
+
logs
|
|
4
|
+
*.log
|
|
5
|
+
npm-debug.log*
|
|
6
|
+
|
|
7
|
+
# Runtime data
|
|
8
|
+
pids
|
|
9
|
+
*.pid
|
|
10
|
+
*.seed
|
|
11
|
+
|
|
12
|
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
13
|
+
lib-cov
|
|
14
|
+
|
|
15
|
+
# Coverage directory used by tools like istanbul
|
|
16
|
+
coverage
|
|
17
|
+
|
|
18
|
+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
19
|
+
.grunt
|
|
20
|
+
|
|
21
|
+
# node-waf configuration
|
|
22
|
+
.lock-wscript
|
|
23
|
+
|
|
24
|
+
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
|
25
|
+
build/Release
|
|
26
|
+
|
|
27
|
+
# Dependency directory
|
|
28
|
+
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
|
29
|
+
node_modules
|
|
30
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ---> Node
|
|
2
|
+
# Logs
|
|
3
|
+
logs
|
|
4
|
+
*.log
|
|
5
|
+
npm-debug.log*
|
|
6
|
+
|
|
7
|
+
# Runtime data
|
|
8
|
+
pids
|
|
9
|
+
*.pid
|
|
10
|
+
*.seed
|
|
11
|
+
|
|
12
|
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
13
|
+
lib-cov
|
|
14
|
+
|
|
15
|
+
# Coverage directory used by tools like istanbul
|
|
16
|
+
coverage
|
|
17
|
+
|
|
18
|
+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
19
|
+
.grunt
|
|
20
|
+
|
|
21
|
+
# node-waf configuration
|
|
22
|
+
.lock-wscript
|
|
23
|
+
|
|
24
|
+
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
|
25
|
+
build/Release
|
|
26
|
+
|
|
27
|
+
# Dependency directory
|
|
28
|
+
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
|
29
|
+
node_modules
|
|
30
|
+
|
|
31
|
+
.history
|
|
32
|
+
.specstory
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ---> Node
|
|
2
|
+
# Logs
|
|
3
|
+
logs
|
|
4
|
+
*.log
|
|
5
|
+
npm-debug.log*
|
|
6
|
+
|
|
7
|
+
# Runtime data
|
|
8
|
+
pids
|
|
9
|
+
*.pid
|
|
10
|
+
*.seed
|
|
11
|
+
|
|
12
|
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
13
|
+
lib-cov
|
|
14
|
+
|
|
15
|
+
# Coverage directory used by tools like istanbul
|
|
16
|
+
coverage
|
|
17
|
+
|
|
18
|
+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
19
|
+
.grunt
|
|
20
|
+
|
|
21
|
+
# node-waf configuration
|
|
22
|
+
.lock-wscript
|
|
23
|
+
|
|
24
|
+
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
|
25
|
+
build/Release
|
|
26
|
+
|
|
27
|
+
# Dependency directory
|
|
28
|
+
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
|
29
|
+
node_modules
|
|
30
|
+
|
|
31
|
+
.history
|
|
32
|
+
.specstory
|
|
@@ -0,0 +1,109 @@
|
|
|
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 -CC 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 -CC 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 -CC new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
|
|
55
|
+
|
|
56
|
+
// Split window horizontally and run shell in second pane
|
|
57
|
+
await exec(`tmux -CC split-window -h -t "${sessionName}" -c "${projectPath}"`);
|
|
58
|
+
|
|
59
|
+
// Set focus on claude pane (left pane)
|
|
60
|
+
await exec(`tmux -CC select-pane -t "${sessionName}:0.0"`);
|
|
61
|
+
|
|
62
|
+
return sessionName;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async attachToSession(sessionName) {
|
|
66
|
+
// Check if we're already in a tmux session
|
|
67
|
+
if (process.env.TMUX) {
|
|
68
|
+
// If we're already in tmux, switch to the session
|
|
69
|
+
await exec(`tmux -CC switch-client -t "${sessionName}"`);
|
|
70
|
+
} else {
|
|
71
|
+
// If not in tmux, attach to the session
|
|
72
|
+
spawn('tmux', ['-CC', 'attach-session', '-t', sessionName], {
|
|
73
|
+
stdio: 'inherit'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildShellCommands(projectName, projectPath, claudeArgs = []) {
|
|
79
|
+
const sessionName = this.getSessionName(projectName);
|
|
80
|
+
const claudeCommand = claudeArgs.length > 0
|
|
81
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
82
|
+
: 'claude';
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
`# Create tmux split session for ${projectName}`,
|
|
86
|
+
`tmux -CC has-session -t "${sessionName}" 2>/dev/null && tmux -CC kill-session -t "${sessionName}"`,
|
|
87
|
+
`tmux -CC new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
88
|
+
`tmux -CC split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
89
|
+
`tmux -CC select-pane -t "${sessionName}:0.0"`,
|
|
90
|
+
process.env.TMUX
|
|
91
|
+
? `tmux -CC switch-client -t "${sessionName}"`
|
|
92
|
+
: `tmux -CC attach-session -t "${sessionName}"`
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listWorkonSessions() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec('tmux -CC list-sessions -F "#{session_name}"');
|
|
99
|
+
return stdout.trim()
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(session => session.startsWith(this.sessionPrefix))
|
|
102
|
+
.map(session => session.replace(this.sessionPrefix, ''));
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = TmuxManager;
|
|
@@ -0,0 +1,109 @@
|
|
|
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 attachToSession(sessionName) {
|
|
66
|
+
// Check if we're already in a tmux session
|
|
67
|
+
if (process.env.TMUX) {
|
|
68
|
+
// If we're already in tmux, switch to the session
|
|
69
|
+
await exec(`tmux switch-client -t "${sessionName}"`);
|
|
70
|
+
} else {
|
|
71
|
+
// If not in tmux, attach to the session
|
|
72
|
+
spawn('tmux', ['', 'attach-session', '-t', sessionName], {
|
|
73
|
+
stdio: 'inherit'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildShellCommands(projectName, projectPath, claudeArgs = []) {
|
|
79
|
+
const sessionName = this.getSessionName(projectName);
|
|
80
|
+
const claudeCommand = claudeArgs.length > 0
|
|
81
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
82
|
+
: 'claude';
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
`# Create tmux split session for ${projectName}`,
|
|
86
|
+
`tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
|
|
87
|
+
`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
88
|
+
`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
89
|
+
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
90
|
+
process.env.TMUX
|
|
91
|
+
? `tmux switch-client -t "${sessionName}"`
|
|
92
|
+
: `tmux attach-session -t "${sessionName}"`
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listWorkonSessions() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
|
|
99
|
+
return stdout.trim()
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(session => session.startsWith(this.sessionPrefix))
|
|
102
|
+
.map(session => session.replace(this.sessionPrefix, ''));
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = TmuxManager;
|
|
@@ -0,0 +1,109 @@
|
|
|
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 -CC 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 -CC 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 -CC new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`);
|
|
55
|
+
|
|
56
|
+
// Split window horizontally and run shell in second pane
|
|
57
|
+
await exec(`tmux -CC split-window -h -t "${sessionName}" -c "${projectPath}"`);
|
|
58
|
+
|
|
59
|
+
// Set focus on claude pane (left pane)
|
|
60
|
+
await exec(`tmux -CC select-pane -t "${sessionName}:0.0"`);
|
|
61
|
+
|
|
62
|
+
return sessionName;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async attachToSession(sessionName) {
|
|
66
|
+
// Check if we're already in a tmux session
|
|
67
|
+
if (process.env.TMUX) {
|
|
68
|
+
// If we're already in tmux, switch to the session
|
|
69
|
+
await exec(`tmux -CC switch-client -t "${sessionName}"`);
|
|
70
|
+
} else {
|
|
71
|
+
// If not in tmux, attach to the session
|
|
72
|
+
spawn('tmux', ['-CC', 'attach-session', '-t', sessionName], {
|
|
73
|
+
stdio: 'inherit'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildShellCommands(projectName, projectPath, claudeArgs = []) {
|
|
79
|
+
const sessionName = this.getSessionName(projectName);
|
|
80
|
+
const claudeCommand = claudeArgs.length > 0
|
|
81
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
82
|
+
: 'claude';
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
`# Create tmux split session for ${projectName}`,
|
|
86
|
+
`tmux -CC has-session -t "${sessionName}" 2>/dev/null && tmux -CC kill-session -t "${sessionName}"`,
|
|
87
|
+
`tmux -CC new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
88
|
+
`tmux -CC split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
89
|
+
`tmux -CC select-pane -t "${sessionName}:0.0"`,
|
|
90
|
+
process.env.TMUX
|
|
91
|
+
? `tmux -CC switch-client -t "${sessionName}"`
|
|
92
|
+
: `tmux -CC attach-session -t "${sessionName}"`
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listWorkonSessions() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec('tmux -CC list-sessions -F "#{session_name}"');
|
|
99
|
+
return stdout.trim()
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(session => session.startsWith(this.sessionPrefix))
|
|
102
|
+
.map(session => session.replace(this.sessionPrefix, ''));
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = TmuxManager;
|
|
@@ -0,0 +1,109 @@
|
|
|
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 attachToSession(sessionName) {
|
|
66
|
+
// Check if we're already in a tmux session
|
|
67
|
+
if (process.env.TMUX) {
|
|
68
|
+
// If we're already in tmux, switch to the session
|
|
69
|
+
await exec(`tmux switch-client -t "${sessionName}"`);
|
|
70
|
+
} else {
|
|
71
|
+
// If not in tmux, attach to the session
|
|
72
|
+
spawn('tmux', ['-CC', 'attach-session', '-t', sessionName], {
|
|
73
|
+
stdio: 'inherit'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildShellCommands(projectName, projectPath, claudeArgs = []) {
|
|
79
|
+
const sessionName = this.getSessionName(projectName);
|
|
80
|
+
const claudeCommand = claudeArgs.length > 0
|
|
81
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
82
|
+
: 'claude';
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
`# Create tmux split session for ${projectName}`,
|
|
86
|
+
`tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
|
|
87
|
+
`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
88
|
+
`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
89
|
+
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
90
|
+
process.env.TMUX
|
|
91
|
+
? `tmux switch-client -t "${sessionName}"`
|
|
92
|
+
: `tmux attach-session -t "${sessionName}"`
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listWorkonSessions() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
|
|
99
|
+
return stdout.trim()
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(session => session.startsWith(this.sessionPrefix))
|
|
102
|
+
.map(session => session.replace(this.sessionPrefix, ''));
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = TmuxManager;
|
|
@@ -0,0 +1,109 @@
|
|
|
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 attachToSession(sessionName) {
|
|
66
|
+
// Check if we're already in a tmux session
|
|
67
|
+
if (process.env.TMUX) {
|
|
68
|
+
// If we're already in tmux, switch to the session
|
|
69
|
+
await exec(`tmux switch-client -t "${sessionName}"`);
|
|
70
|
+
} else {
|
|
71
|
+
// If not in tmux, attach to the session
|
|
72
|
+
spawn('tmux', ['-CC', 'attach-session', '-t', sessionName], {
|
|
73
|
+
stdio: 'inherit'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildShellCommands(projectName, projectPath, claudeArgs = []) {
|
|
79
|
+
const sessionName = this.getSessionName(projectName);
|
|
80
|
+
const claudeCommand = claudeArgs.length > 0
|
|
81
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
82
|
+
: 'claude';
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
`# Create tmux split session for ${projectName}`,
|
|
86
|
+
`tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
|
|
87
|
+
`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
88
|
+
`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
89
|
+
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
90
|
+
process.env.TMUX
|
|
91
|
+
? `tmux switch-client -t "${sessionName}"`
|
|
92
|
+
: `tmux attach-session -t "${sessionName}"`
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listWorkonSessions() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
|
|
99
|
+
return stdout.trim()
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(session => session.startsWith(this.sessionPrefix))
|
|
102
|
+
.map(session => session.replace(this.sessionPrefix, ''));
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = TmuxManager;
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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.3.0"></a>
|
|
6
|
+
# [1.3.0](https://github.com/israelroldan/workon/compare/v1.2.1...v1.3.0) (2025-08-06)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Add split terminal support for 'claude' event (tmux based) ([c700afc](https://github.com/israelroldan/workon/commit/c700afc))
|
|
12
|
+
* Enhance 'claude' event configuration and validation ([12387e4](https://github.com/israelroldan/workon/commit/12387e4))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
<a name="1.2.1"></a>
|
|
6
17
|
## [1.2.1](https://github.com/israelroldan/workon/compare/v1.2.0...v1.2.1) (2025-08-06)
|
|
7
18
|
|
package/cli/manage.js
CHANGED
|
@@ -134,11 +134,17 @@ class manage extends command {
|
|
|
134
134
|
|
|
135
135
|
const answers = await inquirer.prompt(questions);
|
|
136
136
|
|
|
137
|
-
// Convert events array to object
|
|
137
|
+
// Convert events array to object and configure advanced options
|
|
138
138
|
const events = {};
|
|
139
|
-
answers.events
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
for (const event of answers.events) {
|
|
140
|
+
if (event === 'claude') {
|
|
141
|
+
// Ask for Claude-specific configuration
|
|
142
|
+
const claudeConfig = await me.configureClaudeEvent();
|
|
143
|
+
events[event] = claudeConfig;
|
|
144
|
+
} else {
|
|
145
|
+
events[event] = 'true';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
142
148
|
|
|
143
149
|
const projectConfig = {
|
|
144
150
|
path: answers.path,
|
|
@@ -248,11 +254,32 @@ class manage extends command {
|
|
|
248
254
|
|
|
249
255
|
const answers = await inquirer.prompt(questions);
|
|
250
256
|
|
|
251
|
-
// Convert events array to object
|
|
257
|
+
// Convert events array to object and configure advanced options
|
|
252
258
|
const events = {};
|
|
253
|
-
answers.events
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
for (const event of answers.events) {
|
|
260
|
+
if (event === 'claude') {
|
|
261
|
+
// If claude was previously configured with advanced options, preserve or update them
|
|
262
|
+
const existingClaudeConfig = project.events && project.events.claude;
|
|
263
|
+
if (existingClaudeConfig && typeof existingClaudeConfig === 'object') {
|
|
264
|
+
const keepConfig = await inquirer.prompt([{
|
|
265
|
+
type: 'confirm',
|
|
266
|
+
name: 'keep',
|
|
267
|
+
message: 'Keep existing Claude configuration?',
|
|
268
|
+
default: true
|
|
269
|
+
}]);
|
|
270
|
+
|
|
271
|
+
if (keepConfig.keep) {
|
|
272
|
+
events[event] = existingClaudeConfig;
|
|
273
|
+
} else {
|
|
274
|
+
events[event] = await me.configureClaudeEvent();
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
events[event] = await me.configureClaudeEvent();
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
events[event] = 'true';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
256
283
|
|
|
257
284
|
const updatedProject = {
|
|
258
285
|
path: answers.path,
|
|
@@ -371,6 +398,59 @@ class manage extends command {
|
|
|
371
398
|
return me.startManagement();
|
|
372
399
|
}
|
|
373
400
|
}
|
|
401
|
+
|
|
402
|
+
async configureClaudeEvent() {
|
|
403
|
+
let me = this;
|
|
404
|
+
|
|
405
|
+
me.log.log('\n⚙️ Configure Claude Event\n');
|
|
406
|
+
|
|
407
|
+
const claudeQuestions = [
|
|
408
|
+
{
|
|
409
|
+
type: 'confirm',
|
|
410
|
+
name: 'useAdvanced',
|
|
411
|
+
message: 'Configure advanced Claude options?',
|
|
412
|
+
default: false
|
|
413
|
+
}
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
const claudeAnswer = await inquirer.prompt(claudeQuestions);
|
|
417
|
+
|
|
418
|
+
if (!claudeAnswer.useAdvanced) {
|
|
419
|
+
return 'true';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const advancedQuestions = [
|
|
423
|
+
{
|
|
424
|
+
type: 'input',
|
|
425
|
+
name: 'flags',
|
|
426
|
+
message: 'Claude flags (comma-separated, e.g. --resume,--debug):',
|
|
427
|
+
filter: (input) => {
|
|
428
|
+
if (!input.trim()) return [];
|
|
429
|
+
return input.split(',').map(flag => flag.trim()).filter(flag => flag);
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
type: 'confirm',
|
|
434
|
+
name: 'split_terminal',
|
|
435
|
+
message: 'Enable split terminal (Claude + shell side-by-side with tmux)?',
|
|
436
|
+
default: false
|
|
437
|
+
}
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
const advancedAnswers = await inquirer.prompt(advancedQuestions);
|
|
441
|
+
|
|
442
|
+
const config = {};
|
|
443
|
+
|
|
444
|
+
if (advancedAnswers.flags && advancedAnswers.flags.length > 0) {
|
|
445
|
+
config.flags = advancedAnswers.flags;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (advancedAnswers.split_terminal) {
|
|
449
|
+
config.split_terminal = true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return config;
|
|
453
|
+
}
|
|
374
454
|
}
|
|
375
455
|
|
|
376
456
|
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,21 @@ class open extends command {
|
|
|
63
64
|
me.shellCommands = [];
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
// Check for split terminal scenario
|
|
68
|
+
const hasCwd = events.includes('cwd');
|
|
69
|
+
const hasClaudeEvent = events.includes('claude');
|
|
70
|
+
const claudeConfig = project.events.claude;
|
|
71
|
+
const shouldUseSplitTerminal = hasCwd && hasClaudeEvent &&
|
|
72
|
+
claudeConfig && typeof claudeConfig === 'object' && claudeConfig.split_terminal;
|
|
73
|
+
|
|
74
|
+
if (shouldUseSplitTerminal) {
|
|
75
|
+
await me.handleSplitTerminal(project, isShellMode);
|
|
76
|
+
// Process other events except cwd and claude
|
|
77
|
+
events.filter(e => e !== 'cwd' && e !== 'claude').forEach(me.processEvent.bind(me));
|
|
78
|
+
} else {
|
|
79
|
+
// Normal event processing
|
|
80
|
+
events.forEach(me.processEvent.bind(me));
|
|
81
|
+
}
|
|
67
82
|
|
|
68
83
|
// Output collected shell commands if in shell mode
|
|
69
84
|
if (isShellMode && me.shellCommands.length > 0) {
|
|
@@ -71,6 +86,55 @@ class open extends command {
|
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
async handleSplitTerminal(project, isShellMode) {
|
|
90
|
+
let me = this;
|
|
91
|
+
const tmux = new TmuxManager();
|
|
92
|
+
const claudeConfig = project.events.claude;
|
|
93
|
+
const claudeArgs = (claudeConfig && claudeConfig.flags) ? claudeConfig.flags : [];
|
|
94
|
+
|
|
95
|
+
if (isShellMode) {
|
|
96
|
+
// Check if tmux is available
|
|
97
|
+
if (await tmux.isTmuxAvailable()) {
|
|
98
|
+
const commands = tmux.buildShellCommands(
|
|
99
|
+
project.name,
|
|
100
|
+
project.path.path,
|
|
101
|
+
claudeArgs
|
|
102
|
+
);
|
|
103
|
+
me.shellCommands.push(...commands);
|
|
104
|
+
} else {
|
|
105
|
+
// Fall back to normal behavior if tmux is not available
|
|
106
|
+
me.log.debug('Tmux not available, falling back to normal mode');
|
|
107
|
+
me.shellCommands.push(`cd "${project.path.path}"`);
|
|
108
|
+
const claudeCommand = claudeArgs.length > 0
|
|
109
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
110
|
+
: 'claude';
|
|
111
|
+
me.shellCommands.push(claudeCommand);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
// Direct execution mode
|
|
115
|
+
if (await tmux.isTmuxAvailable()) {
|
|
116
|
+
try {
|
|
117
|
+
const sessionName = await tmux.createSplitSession(
|
|
118
|
+
project.name,
|
|
119
|
+
project.path.path,
|
|
120
|
+
claudeArgs
|
|
121
|
+
);
|
|
122
|
+
await tmux.attachToSession(sessionName);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
me.log.debug(`Failed to create tmux session: ${error.message}`);
|
|
125
|
+
// Fall back to normal behavior
|
|
126
|
+
me.processEvent('cwd');
|
|
127
|
+
me.processEvent('claude');
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
me.log.debug('Tmux not available, falling back to normal mode');
|
|
131
|
+
// Fall back to normal behavior
|
|
132
|
+
me.processEvent('cwd');
|
|
133
|
+
me.processEvent('claude');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
74
138
|
processEvent (event) {
|
|
75
139
|
let me = this;
|
|
76
140
|
let environment = me.root().environment;
|
|
@@ -126,10 +190,24 @@ class open extends command {
|
|
|
126
190
|
}
|
|
127
191
|
break;
|
|
128
192
|
case 'claude':
|
|
193
|
+
let claudeArgs = [];
|
|
194
|
+
let claudeConfig = project.events.claude;
|
|
195
|
+
|
|
196
|
+
// Handle advanced Claude configuration
|
|
197
|
+
if (claudeConfig && typeof claudeConfig === 'object') {
|
|
198
|
+
if (claudeConfig.flags && Array.isArray(claudeConfig.flags)) {
|
|
199
|
+
claudeArgs = claudeArgs.concat(claudeConfig.flags);
|
|
200
|
+
}
|
|
201
|
+
// Additional config options can be handled here in the future
|
|
202
|
+
}
|
|
203
|
+
|
|
129
204
|
if (isShellMode) {
|
|
130
|
-
|
|
205
|
+
let claudeCommand = claudeArgs.length > 0
|
|
206
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
207
|
+
: 'claude';
|
|
208
|
+
me.shellCommands.push(claudeCommand);
|
|
131
209
|
} else {
|
|
132
|
-
spawn('claude',
|
|
210
|
+
spawn('claude', claudeArgs, {
|
|
133
211
|
cwd: environment.project.path.path,
|
|
134
212
|
stdio: 'inherit'
|
|
135
213
|
});
|
package/docs/ideas.md
CHANGED
|
@@ -34,13 +34,51 @@ Create an interactive mode for editing project configurations through guided pro
|
|
|
34
34
|
|
|
35
35
|
## Enhanced Events
|
|
36
36
|
|
|
37
|
-
### Advanced claude Event Options
|
|
38
|
-
|
|
37
|
+
### Advanced claude Event Options ✅ IMPLEMENTED
|
|
38
|
+
The claude event now supports advanced configuration options:
|
|
39
|
+
```json
|
|
40
|
+
"claude": {
|
|
41
|
+
"flags": ["--resume", "--debug"]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Available through:** `workon manage` → Configure advanced Claude options
|
|
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:**
|
|
61
|
+
```json
|
|
62
|
+
"claude": {
|
|
63
|
+
"flags": ["--resume"],
|
|
64
|
+
"split_terminal": true
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Benefits:**
|
|
69
|
+
- Claude and terminal side-by-side for optimal workflow
|
|
70
|
+
- Easy switching between AI assistance and command execution
|
|
71
|
+
- Persistent session that can be reattached
|
|
72
|
+
|
|
73
|
+
### Future claude Event Enhancements
|
|
74
|
+
Additional options that could be implemented:
|
|
39
75
|
```json
|
|
40
76
|
"claude": {
|
|
41
|
-
"mode": "interactive",
|
|
42
77
|
"flags": ["--resume"],
|
|
43
|
-
"
|
|
78
|
+
"mode": "interactive",
|
|
79
|
+
"project_context": true,
|
|
80
|
+
"working_directory": "src/",
|
|
81
|
+
"tmux_layout": "even-horizontal"
|
|
44
82
|
}
|
|
45
83
|
```
|
|
46
84
|
|
package/lib/tmux.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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 attachToSession(sessionName) {
|
|
66
|
+
// Check if we're already in a tmux session
|
|
67
|
+
if (process.env.TMUX) {
|
|
68
|
+
// If we're already in tmux, switch to the session
|
|
69
|
+
await exec(`tmux switch-client -t "${sessionName}"`);
|
|
70
|
+
} else {
|
|
71
|
+
// If not in tmux, attach to the session
|
|
72
|
+
spawn('tmux', ['-CC', 'attach-session', '-t', sessionName], {
|
|
73
|
+
stdio: 'inherit'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
buildShellCommands(projectName, projectPath, claudeArgs = []) {
|
|
79
|
+
const sessionName = this.getSessionName(projectName);
|
|
80
|
+
const claudeCommand = claudeArgs.length > 0
|
|
81
|
+
? `claude ${claudeArgs.join(' ')}`
|
|
82
|
+
: 'claude';
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
`# Create tmux split session for ${projectName}`,
|
|
86
|
+
`tmux has-session -t "${sessionName}" 2>/dev/null && tmux kill-session -t "${sessionName}"`,
|
|
87
|
+
`tmux new-session -d -s "${sessionName}" -c "${projectPath}" '${claudeCommand}'`,
|
|
88
|
+
`tmux split-window -h -t "${sessionName}" -c "${projectPath}"`,
|
|
89
|
+
`tmux select-pane -t "${sessionName}:0.0"`,
|
|
90
|
+
process.env.TMUX
|
|
91
|
+
? `tmux switch-client -t "${sessionName}"`
|
|
92
|
+
: `tmux attach-session -t "${sessionName}"`
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listWorkonSessions() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec('tmux list-sessions -F "#{session_name}"');
|
|
99
|
+
return stdout.trim()
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(session => session.startsWith(this.sessionPrefix))
|
|
102
|
+
.map(session => session.replace(this.sessionPrefix, ''));
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = TmuxManager;
|
package/lib/validation.js
CHANGED
|
@@ -86,6 +86,43 @@ class ProjectValidator {
|
|
|
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
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
validateClaudeConfig(config) {
|
|
101
|
+
if (typeof config !== 'object') {
|
|
102
|
+
return 'Claude configuration must be an object';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate flags if present
|
|
106
|
+
if (config.flags) {
|
|
107
|
+
if (!Array.isArray(config.flags)) {
|
|
108
|
+
return 'Claude flags must be an array';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Basic flag validation - ensure they start with - or --
|
|
112
|
+
const invalidFlags = config.flags.filter(flag =>
|
|
113
|
+
typeof flag !== 'string' || (!flag.startsWith('-') && !flag.startsWith('--'))
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (invalidFlags.length > 0) {
|
|
117
|
+
return `Invalid Claude flags: ${invalidFlags.join(', ')}. Flags must start with - or --`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validate split_terminal if present
|
|
122
|
+
if (config.split_terminal !== undefined && typeof config.split_terminal !== 'boolean') {
|
|
123
|
+
return 'Claude split_terminal must be a boolean';
|
|
124
|
+
}
|
|
125
|
+
|
|
89
126
|
return true;
|
|
90
127
|
}
|
|
91
128
|
|
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
|
-
}
|