workon 1.4.0 → 2.0.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/cli/open.js CHANGED
@@ -4,10 +4,15 @@ const { ProjectEnvironment } = require('../lib/environment');
4
4
  const spawn = require('child_process').spawn;
5
5
  const File = require('phylo');
6
6
  const TmuxManager = require('../lib/tmux');
7
+ const registry = require('../commands/registry');
7
8
 
8
9
  class open extends command {
9
- execute (params) {
10
+ async execute (params) {
10
11
  let me = this;
12
+
13
+ // Initialize command registry
14
+ await registry.initialize();
15
+
11
16
  if (params.project) {
12
17
  return me.processProject(params.project);
13
18
  } else {
@@ -73,20 +78,28 @@ class open extends command {
73
78
  // Three-pane layout: Claude + Terminal + NPM
74
79
  await me.handleThreePaneLayout(project, isShellMode);
75
80
  // Process other events except cwd, claude, and npm
76
- events.filter(e => !['cwd', 'claude', 'npm'].includes(e)).forEach(me.processEvent.bind(me));
81
+ for (const event of events.filter(e => !['cwd', 'claude', 'npm'].includes(e))) {
82
+ await me.processEvent(event);
83
+ }
77
84
  } else if (hasCwd && hasNpmEvent) {
78
85
  // Two-pane layout: Terminal + NPM (no Claude)
79
86
  await me.handleTwoPaneNpmLayout(project, isShellMode);
80
87
  // Process other events except cwd and npm
81
- events.filter(e => !['cwd', 'npm'].includes(e)).forEach(me.processEvent.bind(me));
88
+ for (const event of events.filter(e => !['cwd', 'npm'].includes(e))) {
89
+ await me.processEvent(event);
90
+ }
82
91
  } else if (hasCwd && hasClaudeEvent) {
83
92
  // Two-pane layout: Claude + Terminal (existing split terminal)
84
93
  await me.handleSplitTerminal(project, isShellMode);
85
94
  // Process other events except cwd and claude
86
- events.filter(e => !['cwd', 'claude'].includes(e)).forEach(me.processEvent.bind(me));
95
+ for (const event of events.filter(e => !['cwd', 'claude'].includes(e))) {
96
+ await me.processEvent(event);
97
+ }
87
98
  } else {
88
99
  // Normal event processing
89
- events.forEach(me.processEvent.bind(me));
100
+ for (const event of events) {
101
+ await me.processEvent(event);
102
+ }
90
103
  }
91
104
 
92
105
  // Output collected shell commands if in shell mode
@@ -132,14 +145,14 @@ class open extends command {
132
145
  } catch (error) {
133
146
  me.log.debug(`Failed to create tmux session: ${error.message}`);
134
147
  // Fall back to normal behavior
135
- me.processEvent('cwd');
136
- me.processEvent('claude');
148
+ await me.processEvent('cwd');
149
+ await me.processEvent('claude');
137
150
  }
138
151
  } else {
139
152
  me.log.debug('Tmux not available, falling back to normal mode');
140
153
  // Fall back to normal behavior
141
- me.processEvent('cwd');
142
- me.processEvent('claude');
154
+ await me.processEvent('cwd');
155
+ await me.processEvent('claude');
143
156
  }
144
157
  }
145
158
  }
@@ -150,7 +163,8 @@ class open extends command {
150
163
  const claudeConfig = project.events.claude;
151
164
  const claudeArgs = (claudeConfig && claudeConfig.flags) ? claudeConfig.flags : [];
152
165
  const npmConfig = project.events.npm;
153
- const npmCommand = me.getNpmCommand(npmConfig);
166
+ const NpmCommand = registry.getCommandByName('npm');
167
+ const npmCommand = NpmCommand ? NpmCommand._getNpmCommand(npmConfig) : 'npm run dev';
154
168
 
155
169
  if (isShellMode) {
156
170
  // Check if tmux is available
@@ -186,16 +200,16 @@ class open extends command {
186
200
  } catch (error) {
187
201
  me.log.debug(`Failed to create tmux session: ${error.message}`);
188
202
  // Fall back to normal behavior
189
- me.processEvent('cwd');
190
- me.processEvent('claude');
191
- me.processEvent('npm');
203
+ await me.processEvent('cwd');
204
+ await me.processEvent('claude');
205
+ await me.processEvent('npm');
192
206
  }
193
207
  } else {
194
208
  me.log.debug('Tmux not available, falling back to normal mode');
195
209
  // Fall back to normal behavior
196
- me.processEvent('cwd');
197
- me.processEvent('claude');
198
- me.processEvent('npm');
210
+ await me.processEvent('cwd');
211
+ await me.processEvent('claude');
212
+ await me.processEvent('npm');
199
213
  }
200
214
  }
201
215
  }
@@ -204,7 +218,8 @@ class open extends command {
204
218
  let me = this;
205
219
  const tmux = new TmuxManager();
206
220
  const npmConfig = project.events.npm;
207
- const npmCommand = me.getNpmCommand(npmConfig);
221
+ const NpmCommand = registry.getCommandByName('npm');
222
+ const npmCommand = NpmCommand ? NpmCommand._getNpmCommand(npmConfig) : 'npm run dev';
208
223
 
209
224
  if (isShellMode) {
210
225
  // Check if tmux is available
@@ -234,34 +249,24 @@ class open extends command {
234
249
  } catch (error) {
235
250
  me.log.debug(`Failed to create tmux session: ${error.message}`);
236
251
  // Fall back to normal behavior
237
- me.processEvent('cwd');
238
- me.processEvent('npm');
252
+ await me.processEvent('cwd');
253
+ await me.processEvent('npm');
239
254
  }
240
255
  } else {
241
256
  me.log.debug('Tmux not available, falling back to normal mode');
242
257
  // Fall back to normal behavior
243
- me.processEvent('cwd');
244
- me.processEvent('npm');
258
+ await me.processEvent('cwd');
259
+ await me.processEvent('npm');
245
260
  }
246
261
  }
247
262
  }
248
263
 
249
- getNpmCommand(npmConfig) {
250
- if (typeof npmConfig === 'string') {
251
- return `npm run ${npmConfig}`;
252
- } else if (npmConfig && typeof npmConfig === 'object' && npmConfig.command) {
253
- return `npm run ${npmConfig.command}`;
254
- } else {
255
- return 'npm run dev';
256
- }
257
- }
258
264
 
259
- processEvent (event) {
265
+ async processEvent (event) {
260
266
  let me = this;
261
267
  let environment = me.root().environment;
262
268
  let project = environment.project;
263
269
  let scripts = project.scripts || {};
264
- let homepage = project.homepage;
265
270
  let capitalEvt = `${event[0].toUpperCase()}${event.substring(1)}`;
266
271
 
267
272
  me.log.debug(`Processing event ${event}`);
@@ -276,79 +281,19 @@ class open extends command {
276
281
  let isShellMode = me.params.shell || me.root().params.shell;
277
282
  me.log.debug(`Shell mode is: ${isShellMode}`);
278
283
 
279
- switch (event) {
280
- case 'ide':
281
- if (isShellMode) {
282
- me.shellCommands.push(`${project.ide} "${project.path.path}" &`);
283
- } else {
284
- spawn(project.ide, [project.path.path]);
285
- }
286
- break;
287
- case 'cwd':
288
- if (isShellMode) {
289
- me.shellCommands.push(`cd "${environment.project.path.path}"`);
290
- } else {
291
- spawn(process.env.SHELL, ['-i'], {
292
- cwd: environment.project.path.path,
293
- stdio: 'inherit'
294
- });
295
- }
296
- break;
297
- case 'web':
298
- if (homepage) {
299
- if (isShellMode) {
300
- // Different approaches based on OS
301
- let openCmd;
302
- switch (process.platform) {
303
- case 'darwin': openCmd = 'open'; break;
304
- case 'win32': openCmd = 'start'; break;
305
- default: openCmd = 'xdg-open'; break;
306
- }
307
- me.shellCommands.push(`${openCmd} "${homepage}" &`);
308
- } else {
309
- require("openurl2").open(homepage);
310
- }
311
- }
312
- break;
313
- case 'claude':
314
- let claudeArgs = [];
315
- let claudeConfig = project.events.claude;
316
-
317
- // Handle advanced Claude configuration
318
- if (claudeConfig && typeof claudeConfig === 'object') {
319
- if (claudeConfig.flags && Array.isArray(claudeConfig.flags)) {
320
- claudeArgs = claudeArgs.concat(claudeConfig.flags);
321
- }
322
- // Additional config options can be handled here in the future
323
- }
324
-
325
- if (isShellMode) {
326
- let claudeCommand = claudeArgs.length > 0
327
- ? `claude ${claudeArgs.join(' ')}`
328
- : 'claude';
329
- me.shellCommands.push(claudeCommand);
330
- } else {
331
- spawn('claude', claudeArgs, {
332
- cwd: environment.project.path.path,
333
- stdio: 'inherit'
334
- });
335
- }
336
- break;
337
- case 'npm':
338
- let npmCommand = me.getNpmCommand(project.events.npm);
339
-
340
- if (isShellMode) {
341
- me.shellCommands.push(npmCommand);
342
- } else {
343
- spawn('npm', ['run', npmCommand.replace('npm run ', '')], {
344
- cwd: environment.project.path.path,
345
- stdio: 'inherit'
346
- });
347
- }
348
- break;
284
+ // Use CommandRegistry to process the event
285
+ const command = registry.getCommandByName(event);
286
+ if (command && command.processing) {
287
+ await command.processing.processEvent({
288
+ project,
289
+ isShellMode,
290
+ shellCommands: me.shellCommands || []
291
+ });
292
+ } else {
293
+ me.log.debug(`No command handler found for event: ${event}`);
349
294
  }
350
295
  }
351
- if (`before${capitalEvt}` in scripts) {
296
+ if (`after${capitalEvt}` in scripts) {
352
297
  me.log.debug(`Found 'after' script, unfortunately scripts are not yet supported.`);
353
298
  }
354
299
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Base command interface that all commands must implement
3
+ * Provides standardized structure for command-centric architecture
4
+ */
5
+ class BaseCommand {
6
+ /**
7
+ * Command metadata - must be implemented by each command
8
+ * @returns {Object} metadata object with name, displayName, description, etc.
9
+ */
10
+ static get metadata() {
11
+ throw new Error('Command must implement static metadata getter');
12
+ }
13
+
14
+ /**
15
+ * Validation rules for command configuration
16
+ * @returns {Object} validation methods
17
+ */
18
+ static get validation() {
19
+ return {
20
+ /**
21
+ * Validate command-specific configuration
22
+ * @param {*} config - Configuration to validate
23
+ * @returns {true|string} true if valid, error message if invalid
24
+ */
25
+ validateConfig(config) {
26
+ return true; // Default: accept any config
27
+ }
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Interactive configuration setup
33
+ * @returns {Object} configuration methods
34
+ */
35
+ static get configuration() {
36
+ return {
37
+ /**
38
+ * Interactive setup prompts for the command
39
+ * @returns {*} Configuration object or primitive value
40
+ */
41
+ async configureInteractive() {
42
+ return 'true'; // Default: simple boolean enable
43
+ },
44
+
45
+ /**
46
+ * Get default configuration for the command
47
+ * @returns {*} Default configuration
48
+ */
49
+ getDefaultConfig() {
50
+ return 'true';
51
+ }
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Event processing logic
57
+ * @returns {Object} processing methods
58
+ */
59
+ static get processing() {
60
+ return {
61
+ /**
62
+ * Process the command event
63
+ * @param {Object} context - Processing context
64
+ * @param {Object} context.project - Project configuration
65
+ * @param {boolean} context.isShellMode - Whether in shell mode
66
+ * @param {string[]} context.shellCommands - Array to collect shell commands
67
+ * @returns {Promise<void>}
68
+ */
69
+ async processEvent(context) {
70
+ throw new Error('Command must implement processEvent method');
71
+ },
72
+
73
+ /**
74
+ * Generate shell command for the event
75
+ * @param {Object} context - Processing context
76
+ * @returns {string[]} Array of shell commands
77
+ */
78
+ generateShellCommand(context) {
79
+ return [];
80
+ }
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Tmux integration (optional)
86
+ * @returns {Object|null} tmux methods or null if not supported
87
+ */
88
+ static get tmux() {
89
+ return null; // Default: no tmux integration
90
+ }
91
+
92
+ /**
93
+ * Help and documentation
94
+ * @returns {Object} help information
95
+ */
96
+ static get help() {
97
+ return {
98
+ usage: `${this.metadata.name}: <configuration>`,
99
+ description: this.metadata.description,
100
+ examples: []
101
+ };
102
+ }
103
+ }
104
+
105
+ module.exports = BaseCommand;
@@ -0,0 +1,86 @@
1
+ const BaseCommand = require('../../base');
2
+ const spawn = require('child_process').spawn;
3
+
4
+ /**
5
+ * CWD (Change Working Directory) Command
6
+ * Changes the current working directory to the project path
7
+ */
8
+ class CwdCommand extends BaseCommand {
9
+ static get metadata() {
10
+ return {
11
+ name: 'cwd',
12
+ displayName: 'Change directory (cwd)',
13
+ description: 'Change current working directory to project path',
14
+ category: 'core',
15
+ requiresTmux: false,
16
+ dependencies: []
17
+ };
18
+ }
19
+
20
+ static get validation() {
21
+ return {
22
+ validateConfig(config) {
23
+ // CWD command accepts boolean or string 'true'
24
+ if (config === true || config === 'true' || config === false || config === 'false') {
25
+ return true;
26
+ }
27
+ return 'CWD configuration must be a boolean or string "true"/"false"';
28
+ }
29
+ };
30
+ }
31
+
32
+ static get configuration() {
33
+ return {
34
+ async configureInteractive() {
35
+ // CWD is typically just enabled/disabled, no advanced config needed
36
+ return 'true';
37
+ },
38
+
39
+ getDefaultConfig() {
40
+ return 'true';
41
+ }
42
+ };
43
+ }
44
+
45
+ static get processing() {
46
+ return {
47
+ async processEvent(context) {
48
+ const { project, isShellMode, shellCommands } = context;
49
+
50
+ if (isShellMode) {
51
+ shellCommands.push(`cd "${project.path.path}"`);
52
+ } else {
53
+ // In non-shell mode, spawn a new shell in the project directory
54
+ spawn(process.env.SHELL, ['-i'], {
55
+ cwd: project.path.path,
56
+ stdio: 'inherit'
57
+ });
58
+ }
59
+ },
60
+
61
+ generateShellCommand(context) {
62
+ const { project } = context;
63
+ return [`cd "${project.path.path}"`];
64
+ }
65
+ };
66
+ }
67
+
68
+ static get help() {
69
+ return {
70
+ usage: 'cwd: true',
71
+ description: 'Changes the current working directory to the project path',
72
+ examples: [
73
+ {
74
+ config: 'cwd: true',
75
+ description: 'Enable directory change when opening project'
76
+ },
77
+ {
78
+ config: 'cwd: false',
79
+ description: 'Disable directory change (stay in current directory)'
80
+ }
81
+ ]
82
+ };
83
+ }
84
+ }
85
+
86
+ module.exports = CwdCommand;
@@ -0,0 +1,84 @@
1
+ const BaseCommand = require('../../base');
2
+ const spawn = require('child_process').spawn;
3
+
4
+ /**
5
+ * IDE Command
6
+ * Opens the project in the configured IDE/editor
7
+ */
8
+ class IdeCommand extends BaseCommand {
9
+ static get metadata() {
10
+ return {
11
+ name: 'ide',
12
+ displayName: 'Open in IDE',
13
+ description: 'Open project in configured IDE/editor',
14
+ category: 'core',
15
+ requiresTmux: false,
16
+ dependencies: []
17
+ };
18
+ }
19
+
20
+ static get validation() {
21
+ return {
22
+ validateConfig(config) {
23
+ // IDE command accepts boolean or string 'true'
24
+ if (config === true || config === 'true' || config === false || config === 'false') {
25
+ return true;
26
+ }
27
+ return 'IDE configuration must be a boolean or string "true"/"false"';
28
+ }
29
+ };
30
+ }
31
+
32
+ static get configuration() {
33
+ return {
34
+ async configureInteractive() {
35
+ // IDE configuration is handled at the project level (project.ide)
36
+ // This event just enables/disables opening the IDE
37
+ return 'true';
38
+ },
39
+
40
+ getDefaultConfig() {
41
+ return 'true';
42
+ }
43
+ };
44
+ }
45
+
46
+ static get processing() {
47
+ return {
48
+ async processEvent(context) {
49
+ const { project, isShellMode, shellCommands } = context;
50
+
51
+ if (isShellMode) {
52
+ shellCommands.push(`${project.ide} "${project.path.path}" &`);
53
+ } else {
54
+ // In non-shell mode, spawn the IDE directly
55
+ spawn(project.ide, [project.path.path]);
56
+ }
57
+ },
58
+
59
+ generateShellCommand(context) {
60
+ const { project } = context;
61
+ return [`${project.ide} "${project.path.path}" &`];
62
+ }
63
+ };
64
+ }
65
+
66
+ static get help() {
67
+ return {
68
+ usage: 'ide: true',
69
+ description: 'Opens the project in the configured IDE/editor',
70
+ examples: [
71
+ {
72
+ config: 'ide: true',
73
+ description: 'Enable opening project in IDE when switching to project'
74
+ },
75
+ {
76
+ config: 'ide: false',
77
+ description: 'Disable automatic IDE opening'
78
+ }
79
+ ]
80
+ };
81
+ }
82
+ }
83
+
84
+ module.exports = IdeCommand;
@@ -0,0 +1,109 @@
1
+ const BaseCommand = require('../../base');
2
+
3
+ /**
4
+ * Web Command
5
+ * Opens the project homepage in a web browser
6
+ */
7
+ class WebCommand extends BaseCommand {
8
+ static get metadata() {
9
+ return {
10
+ name: 'web',
11
+ displayName: 'Open homepage in browser',
12
+ description: 'Open project homepage in web browser',
13
+ category: 'core',
14
+ requiresTmux: false,
15
+ dependencies: []
16
+ };
17
+ }
18
+
19
+ static get validation() {
20
+ return {
21
+ validateConfig(config) {
22
+ // Web command accepts boolean or string 'true'
23
+ if (config === true || config === 'true' || config === false || config === 'false') {
24
+ return true;
25
+ }
26
+ return 'Web configuration must be a boolean or string "true"/"false"';
27
+ }
28
+ };
29
+ }
30
+
31
+ static get configuration() {
32
+ return {
33
+ async configureInteractive() {
34
+ // Web event just enables/disables opening the homepage
35
+ // The actual homepage URL is configured at the project level
36
+ return 'true';
37
+ },
38
+
39
+ getDefaultConfig() {
40
+ return 'true';
41
+ }
42
+ };
43
+ }
44
+
45
+ static get processing() {
46
+ return {
47
+ async processEvent(context) {
48
+ const { project, isShellMode, shellCommands } = context;
49
+ const homepage = project.homepage;
50
+
51
+ if (!homepage) {
52
+ // No homepage configured, skip
53
+ return;
54
+ }
55
+
56
+ if (isShellMode) {
57
+ // Different approaches based on OS
58
+ let openCmd;
59
+ switch (process.platform) {
60
+ case 'darwin': openCmd = 'open'; break;
61
+ case 'win32': openCmd = 'start'; break;
62
+ default: openCmd = 'xdg-open'; break;
63
+ }
64
+ shellCommands.push(`${openCmd} "${homepage}" &`);
65
+ } else {
66
+ // In non-shell mode, use openurl2
67
+ require("openurl2").open(homepage);
68
+ }
69
+ },
70
+
71
+ generateShellCommand(context) {
72
+ const { project } = context;
73
+ const homepage = project.homepage;
74
+
75
+ if (!homepage) {
76
+ return [];
77
+ }
78
+
79
+ let openCmd;
80
+ switch (process.platform) {
81
+ case 'darwin': openCmd = 'open'; break;
82
+ case 'win32': openCmd = 'start'; break;
83
+ default: openCmd = 'xdg-open'; break;
84
+ }
85
+
86
+ return [`${openCmd} "${homepage}" &`];
87
+ }
88
+ };
89
+ }
90
+
91
+ static get help() {
92
+ return {
93
+ usage: 'web: true',
94
+ description: 'Opens the project homepage in the default web browser',
95
+ examples: [
96
+ {
97
+ config: 'web: true',
98
+ description: 'Enable opening project homepage when switching to project'
99
+ },
100
+ {
101
+ config: 'web: false',
102
+ description: 'Disable automatic homepage opening'
103
+ }
104
+ ]
105
+ };
106
+ }
107
+ }
108
+
109
+ module.exports = WebCommand;