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.
@@ -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.forEach(event => {
140
- events[event] = 'true';
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.forEach(event => {
254
- events[event] = 'true';
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
- events.forEach(me.processEvent.bind(me));
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
- me.shellCommands.push(`claude`);
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
- Extend the claude event with more configuration options:
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
- "project_context": true
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,6 +1,6 @@
1
1
  {
2
2
  "name": "workon",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Work on something great!",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- }