workon 1.1.0 → 1.2.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,42 @@
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
+ }
@@ -0,0 +1,43 @@
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
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
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.2.0"></a>
6
+ # [1.2.0](https://github.com/israelroldan/workon/compare/v1.1.0...v1.2.0) (2025-08-06)
7
+
8
+
9
+ ### Features
10
+
11
+ * Introduce interactive project management feature ([3157e27](https://github.com/israelroldan/workon/commit/3157e27))
12
+
13
+
14
+
5
15
  <a name="1.1.0"></a>
6
16
  # [1.1.0](https://github.com/israelroldan/workon/compare/v1.0.0...v1.1.0) (2025-08-06)
7
17
 
package/cli/index.js CHANGED
@@ -8,6 +8,7 @@ const {EnvironmentRecognizer} = require('../lib/environment');
8
8
  const config = require('./config');
9
9
  const interactive = require('./interactive');
10
10
  const open = require('./open');
11
+ const manage = require('./manage');
11
12
 
12
13
  class workon extends container {
13
14
  constructor (log) {
@@ -108,7 +109,7 @@ class workon extends container {
108
109
  let shellFunction = `
109
110
  # workon shell integration
110
111
  workon() {
111
- # Commands that should NOT use shell mode
112
+ # Commands and flags that should NOT use shell mode
112
113
  case "$1" in
113
114
  ${casePattern})
114
115
  command workon "$@"
@@ -116,6 +117,12 @@ workon() {
116
117
  ;;
117
118
  esac
118
119
 
120
+ # If no arguments provided, run interactive mode directly
121
+ if [[ $# -eq 0 ]]; then
122
+ command workon "$@"
123
+ return $?
124
+ fi
125
+
119
126
  # Default behavior: use shell mode for project opening
120
127
  local output
121
128
  output=$(command workon --shell "$@" 2>&1)
@@ -169,7 +176,8 @@ workon.define({
169
176
  private: true
170
177
  },
171
178
  open,
172
- config
179
+ config,
180
+ manage
173
181
  }
174
182
  });
175
183
 
package/cli/manage.js ADDED
@@ -0,0 +1,381 @@
1
+ const { command } = require('./base');
2
+ const inquirer = require('inquirer');
3
+ const File = require('phylo');
4
+ const ProjectValidator = require('../lib/validation');
5
+ const path = require('path');
6
+
7
+ class manage extends command {
8
+ execute(params) {
9
+ let me = this;
10
+ me.validator = new ProjectValidator(me.config);
11
+ me.showLogo();
12
+ return me.startManagement();
13
+ }
14
+
15
+ showLogo() {
16
+ let me = this;
17
+ let version = me.config.get('pkg').version;
18
+
19
+ if (!this._hasShownLogo) {
20
+ this.log.log(` 8\u001b[2m${' '.repeat(Math.max(15-version.length-1, 1))+'v'+version}\u001b[22m\nYb db dP .d8b. 8d8b 8.dP \u001b[92m.d8b. 8d8b.\u001b[0m\n YbdPYbdP 8' .8 8P 88b \u001b[92m8' .8 8P Y8\u001b[0m\n YP YP \`Y8P' 8 8 Yb \u001b[92m\`Y8P' 8 8\u001b[0m`);
21
+ this._hasShownLogo = true;
22
+ }
23
+ }
24
+
25
+ async startManagement() {
26
+ let me = this;
27
+ const projects = me.config.get('projects') || {};
28
+ const projectNames = Object.keys(projects);
29
+
30
+ const mainMenu = {
31
+ type: 'list',
32
+ name: 'action',
33
+ message: 'Project Management',
34
+ choices: [
35
+ {
36
+ name: 'Create new project',
37
+ value: 'create'
38
+ },
39
+ ...(projectNames.length > 0 ? [
40
+ {
41
+ name: 'Edit existing project',
42
+ value: 'edit'
43
+ },
44
+ {
45
+ name: 'Delete project',
46
+ value: 'delete'
47
+ },
48
+ {
49
+ name: 'List all projects',
50
+ value: 'list'
51
+ }
52
+ ] : []),
53
+ new inquirer.Separator(),
54
+ {
55
+ name: 'Exit',
56
+ value: 'exit'
57
+ }
58
+ ]
59
+ };
60
+
61
+ const answer = await inquirer.prompt([mainMenu]);
62
+
63
+ switch (answer.action) {
64
+ case 'create':
65
+ return me.createProject();
66
+ case 'edit':
67
+ return me.editProject();
68
+ case 'delete':
69
+ return me.deleteProject();
70
+ case 'list':
71
+ return me.listProjects();
72
+ case 'exit':
73
+ return;
74
+ }
75
+ }
76
+
77
+ async createProject() {
78
+ let me = this;
79
+ const defaults = me.config.get('project_defaults') || {};
80
+ const projects = me.config.get('projects') || {};
81
+
82
+ me.log.log('\n🚀 Create New Project\n');
83
+
84
+ const questions = [
85
+ {
86
+ type: 'input',
87
+ name: 'name',
88
+ message: 'Project name:',
89
+ default: File.cwd().name,
90
+ validate: (value) => me.validator.validateProjectName(value, projects)
91
+ },
92
+ {
93
+ type: 'input',
94
+ name: 'path',
95
+ message: 'Project path (relative to base directory):',
96
+ default: (answers) => answers.name,
97
+ validate: (value) => {
98
+ const basePath = defaults.base || process.cwd();
99
+ return me.validator.validateProjectPath(value, basePath);
100
+ }
101
+ },
102
+ {
103
+ type: 'list',
104
+ name: 'ide',
105
+ message: 'IDE/Editor:',
106
+ choices: [
107
+ { name: 'Visual Studio Code', value: 'code' },
108
+ { name: 'IntelliJ IDEA', value: 'idea' },
109
+ { name: 'Atom', value: 'atom' },
110
+ { name: 'Sublime Text', value: 'subl' },
111
+ { name: 'Vim', value: 'vim' },
112
+ { name: 'Emacs', value: 'emacs' }
113
+ ],
114
+ default: defaults.ide || 'code'
115
+ },
116
+ {
117
+ type: 'input',
118
+ name: 'homepage',
119
+ message: 'Project homepage (optional):',
120
+ validate: (value) => me.validator.validateUrl(value)
121
+ },
122
+ {
123
+ type: 'checkbox',
124
+ name: 'events',
125
+ message: 'Select events to enable:',
126
+ choices: [
127
+ { name: 'Change directory (cwd)', value: 'cwd', checked: true },
128
+ { name: 'Open in IDE', value: 'ide', checked: true },
129
+ { name: 'Open homepage in browser', value: 'web' },
130
+ { name: 'Launch Claude Code', value: 'claude' }
131
+ ]
132
+ }
133
+ ];
134
+
135
+ const answers = await inquirer.prompt(questions);
136
+
137
+ // Convert events array to object
138
+ const events = {};
139
+ answers.events.forEach(event => {
140
+ events[event] = 'true';
141
+ });
142
+
143
+ const projectConfig = {
144
+ path: answers.path,
145
+ ide: answers.ide,
146
+ events: events
147
+ };
148
+
149
+ if (answers.homepage) {
150
+ projectConfig.homepage = answers.homepage;
151
+ }
152
+
153
+ const confirm = await inquirer.prompt([{
154
+ type: 'confirm',
155
+ name: 'confirmed',
156
+ message: 'Create this project?',
157
+ default: true
158
+ }]);
159
+
160
+ if (confirm.confirmed) {
161
+ projects[answers.name] = projectConfig;
162
+ me.config.set('projects', projects);
163
+ me.log.log(`\n✅ Project '${answers.name}' created successfully!`);
164
+ }
165
+
166
+ const continuePrompt = await inquirer.prompt([{
167
+ type: 'confirm',
168
+ name: 'continue',
169
+ message: 'Return to main menu?',
170
+ default: true
171
+ }]);
172
+
173
+ if (continuePrompt.continue) {
174
+ return me.startManagement();
175
+ }
176
+ }
177
+
178
+ async editProject() {
179
+ let me = this;
180
+ const projects = me.config.get('projects') || {};
181
+ const projectNames = Object.keys(projects);
182
+
183
+ if (projectNames.length === 0) {
184
+ me.log.log('No projects found.');
185
+ return me.startManagement();
186
+ }
187
+
188
+ const projectChoice = await inquirer.prompt([{
189
+ type: 'list',
190
+ name: 'projectName',
191
+ message: 'Select project to edit:',
192
+ choices: projectNames.map(name => ({
193
+ name: `${name} (${projects[name].path})`,
194
+ value: name
195
+ }))
196
+ }]);
197
+
198
+ const project = projects[projectChoice.projectName];
199
+ const currentEvents = Object.keys(project.events || {}).filter(key => project.events[key] === 'true');
200
+
201
+ me.log.log(`\n✏️ Edit Project: ${projectChoice.projectName}\n`);
202
+
203
+ const questions = [
204
+ {
205
+ type: 'input',
206
+ name: 'path',
207
+ message: 'Project path:',
208
+ default: project.path,
209
+ validate: (value) => {
210
+ const defaults = me.config.get('project_defaults') || {};
211
+ const basePath = defaults.base || process.cwd();
212
+ return me.validator.validateProjectPath(value, basePath);
213
+ }
214
+ },
215
+ {
216
+ type: 'list',
217
+ name: 'ide',
218
+ message: 'IDE/Editor:',
219
+ choices: [
220
+ { name: 'Visual Studio Code', value: 'code' },
221
+ { name: 'IntelliJ IDEA', value: 'idea' },
222
+ { name: 'Atom', value: 'atom' },
223
+ { name: 'Sublime Text', value: 'subl' },
224
+ { name: 'Vim', value: 'vim' },
225
+ { name: 'Emacs', value: 'emacs' }
226
+ ],
227
+ default: project.ide || 'code'
228
+ },
229
+ {
230
+ type: 'input',
231
+ name: 'homepage',
232
+ message: 'Project homepage:',
233
+ default: project.homepage || '',
234
+ validate: (value) => me.validator.validateUrl(value)
235
+ },
236
+ {
237
+ type: 'checkbox',
238
+ name: 'events',
239
+ message: 'Select events to enable:',
240
+ choices: [
241
+ { name: 'Change directory (cwd)', value: 'cwd', checked: currentEvents.includes('cwd') },
242
+ { name: 'Open in IDE', value: 'ide', checked: currentEvents.includes('ide') },
243
+ { name: 'Open homepage in browser', value: 'web', checked: currentEvents.includes('web') },
244
+ { name: 'Launch Claude Code', value: 'claude', checked: currentEvents.includes('claude') }
245
+ ]
246
+ }
247
+ ];
248
+
249
+ const answers = await inquirer.prompt(questions);
250
+
251
+ // Convert events array to object
252
+ const events = {};
253
+ answers.events.forEach(event => {
254
+ events[event] = 'true';
255
+ });
256
+
257
+ const updatedProject = {
258
+ path: answers.path,
259
+ ide: answers.ide,
260
+ events: events
261
+ };
262
+
263
+ if (answers.homepage) {
264
+ updatedProject.homepage = answers.homepage;
265
+ }
266
+
267
+ const confirm = await inquirer.prompt([{
268
+ type: 'confirm',
269
+ name: 'confirmed',
270
+ message: 'Save changes?',
271
+ default: true
272
+ }]);
273
+
274
+ if (confirm.confirmed) {
275
+ projects[projectChoice.projectName] = updatedProject;
276
+ me.config.set('projects', projects);
277
+ me.log.log(`\n✅ Project '${projectChoice.projectName}' updated successfully!`);
278
+ }
279
+
280
+ const continuePrompt = await inquirer.prompt([{
281
+ type: 'confirm',
282
+ name: 'continue',
283
+ message: 'Return to main menu?',
284
+ default: true
285
+ }]);
286
+
287
+ if (continuePrompt.continue) {
288
+ return me.startManagement();
289
+ }
290
+ }
291
+
292
+ async deleteProject() {
293
+ let me = this;
294
+ const projects = me.config.get('projects') || {};
295
+ const projectNames = Object.keys(projects);
296
+
297
+ if (projectNames.length === 0) {
298
+ me.log.log('No projects found.');
299
+ return me.startManagement();
300
+ }
301
+
302
+ const projectChoice = await inquirer.prompt([{
303
+ type: 'list',
304
+ name: 'projectName',
305
+ message: 'Select project to delete:',
306
+ choices: projectNames.map(name => ({
307
+ name: `${name} (${projects[name].path})`,
308
+ value: name
309
+ }))
310
+ }]);
311
+
312
+ const confirm = await inquirer.prompt([{
313
+ type: 'confirm',
314
+ name: 'confirmed',
315
+ message: `Are you sure you want to delete '${projectChoice.projectName}'?`,
316
+ default: false
317
+ }]);
318
+
319
+ if (confirm.confirmed) {
320
+ delete projects[projectChoice.projectName];
321
+ me.config.set('projects', projects);
322
+ me.log.log(`\n✅ Project '${projectChoice.projectName}' deleted successfully!`);
323
+ }
324
+
325
+ const continuePrompt = await inquirer.prompt([{
326
+ type: 'confirm',
327
+ name: 'continue',
328
+ message: 'Return to main menu?',
329
+ default: true
330
+ }]);
331
+
332
+ if (continuePrompt.continue) {
333
+ return me.startManagement();
334
+ }
335
+ }
336
+
337
+ async listProjects() {
338
+ let me = this;
339
+ const projects = me.config.get('projects') || {};
340
+ const projectNames = Object.keys(projects);
341
+
342
+ if (projectNames.length === 0) {
343
+ me.log.log('No projects found.');
344
+ return me.startManagement();
345
+ }
346
+
347
+ me.log.log('\n📋 All Projects:\n');
348
+
349
+ projectNames.forEach(name => {
350
+ const project = projects[name];
351
+ const events = Object.keys(project.events || {}).filter(key => project.events[key] === 'true');
352
+
353
+ me.log.log(`${name}:`);
354
+ me.log.log(` Path: ${project.path}`);
355
+ me.log.log(` IDE: ${project.ide}`);
356
+ if (project.homepage) {
357
+ me.log.log(` Homepage: ${project.homepage}`);
358
+ }
359
+ me.log.log(` Events: ${events.join(', ') || 'none'}`);
360
+ me.log.log('');
361
+ });
362
+
363
+ const continuePrompt = await inquirer.prompt([{
364
+ type: 'confirm',
365
+ name: 'continue',
366
+ message: 'Return to main menu?',
367
+ default: true
368
+ }]);
369
+
370
+ if (continuePrompt.continue) {
371
+ return me.startManagement();
372
+ }
373
+ }
374
+ }
375
+
376
+ manage.define({
377
+ help: 'Interactive project management',
378
+ switches: '[d#debug:boolean=false] [h#help:boolean=false]'
379
+ });
380
+
381
+ module.exports = manage;
@@ -0,0 +1,104 @@
1
+ const File = require('phylo');
2
+ const { spawn } = require('child_process');
3
+ const path = require('path');
4
+
5
+ class ProjectValidator {
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+
10
+ validateProjectName(name, existingProjects = {}) {
11
+ if (!name || name.trim() === '') {
12
+ return 'Project name cannot be empty';
13
+ }
14
+
15
+ if (name in existingProjects) {
16
+ return 'Project already exists';
17
+ }
18
+
19
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
20
+ return 'Project name can only contain letters, numbers, underscores, and hyphens';
21
+ }
22
+
23
+ return true;
24
+ }
25
+
26
+ validateProjectPath(projectPath, basePath) {
27
+ if (!projectPath || projectPath.trim() === '') {
28
+ return 'Project path cannot be empty';
29
+ }
30
+
31
+ const fullPath = basePath ? path.resolve(basePath, projectPath) : path.resolve(projectPath);
32
+
33
+ try {
34
+ const fileObj = new File(fullPath);
35
+ if (!fileObj.exists) {
36
+ return `Directory does not exist: ${fullPath}`;
37
+ }
38
+ if (!fileObj.isDirectory) {
39
+ return `Path is not a directory: ${fullPath}`;
40
+ }
41
+ } catch (error) {
42
+ return `Invalid path: ${error.message}`;
43
+ }
44
+
45
+ return true;
46
+ }
47
+
48
+ validateIdeCommand(ideCommand) {
49
+ if (!ideCommand || ideCommand.trim() === '') {
50
+ return 'IDE command cannot be empty';
51
+ }
52
+
53
+ const validIdeCommands = ['code', 'idea', 'atom', 'subl', 'vim', 'emacs'];
54
+ if (!validIdeCommands.includes(ideCommand)) {
55
+ return `Unknown IDE command. Supported: ${validIdeCommands.join(', ')}`;
56
+ }
57
+
58
+ return true;
59
+ }
60
+
61
+ async validateIdeAvailability(ideCommand) {
62
+ return new Promise((resolve) => {
63
+ const child = spawn('which', [ideCommand], { stdio: 'pipe' });
64
+ child.on('close', (code) => {
65
+ if (code === 0) {
66
+ resolve(true);
67
+ } else {
68
+ resolve(`IDE command '${ideCommand}' not found in PATH`);
69
+ }
70
+ });
71
+ child.on('error', () => {
72
+ resolve(`Could not check if '${ideCommand}' is available`);
73
+ });
74
+ });
75
+ }
76
+
77
+ validateEvents(events) {
78
+ if (!events || typeof events !== 'object') {
79
+ return 'Events must be an object';
80
+ }
81
+
82
+ const validEvents = ['cwd', 'ide', 'web', 'claude'];
83
+ const invalidEvents = Object.keys(events).filter(event => !validEvents.includes(event));
84
+
85
+ if (invalidEvents.length > 0) {
86
+ return `Invalid events: ${invalidEvents.join(', ')}. Valid events: ${validEvents.join(', ')}`;
87
+ }
88
+
89
+ return true;
90
+ }
91
+
92
+ validateUrl(url) {
93
+ if (!url) return true; // Optional field
94
+
95
+ try {
96
+ new URL(url);
97
+ return true;
98
+ } catch {
99
+ return 'Invalid URL format';
100
+ }
101
+ }
102
+ }
103
+
104
+ module.exports = ProjectValidator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workon",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Work on something great!",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -37,6 +37,7 @@
37
37
  "switchit": "^1.0.7"
38
38
  },
39
39
  "bin": {
40
- "workon": "bin/workon"
40
+ "workon": "bin/workon",
41
+ "wo": "bin/workon"
41
42
  }
42
43
  }