workon 2.0.0 โ†’ 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
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.0](https://github.com/israelroldan/workon/compare/v2.0.0...v2.1.0) (2025-08-07)
6
+
7
+
8
+ ### Features
9
+
10
+ * Introduce colon syntax for selective command execution ([7b2193a](https://github.com/israelroldan/workon/commit/7b2193ace8cc014dfd71894e3e9f49e0fdea33b9))
11
+
5
12
  ## [2.0.0](https://github.com/israelroldan/workon/compare/v2.0.0-alpha.1...v2.0.0) (2025-08-07)
6
13
 
7
14
  ## [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 (project) {
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 && (project === 'this' || project === '.')) {
43
+ if (environment.$isProjectEnvironment && (projectName === 'this' || projectName === '.')) {
33
44
  me.log.info(`Open current: ${environment.project.name}`);
34
45
  } else {
35
- if (project in projects) {
36
- let cfg = projects[project];
37
- cfg.name = project;
38
- await me.switchTo(ProjectEnvironment.load(cfg, me.config.get('project_defaults')));
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 '${project}' not found, starting interactive mode`);
41
- return me.startInteractiveMode(project);
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
- async switchTo (environment) {
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
- let events = Object.keys(project.events).filter((e) => project.events[e]);
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(`Actions are: ${events}`);
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:** Proposed
4
- **Date:** 2025-08-06
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workon",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Work on something great!",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
+ }