@fredlackey/devutils 0.0.19 → 0.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.
Files changed (122) hide show
  1. package/README.md +223 -32
  2. package/package.json +7 -5
  3. package/src/api/loader.js +229 -0
  4. package/src/api/registry.json +62 -0
  5. package/src/cli.js +305 -0
  6. package/src/commands/ai/index.js +16 -0
  7. package/src/commands/ai/launch.js +112 -0
  8. package/src/commands/ai/list.js +54 -0
  9. package/src/commands/ai/resume.js +70 -0
  10. package/src/commands/ai/sessions.js +121 -0
  11. package/src/commands/ai/set.js +131 -0
  12. package/src/commands/ai/show.js +74 -0
  13. package/src/commands/ai/tools.js +46 -0
  14. package/src/commands/alias/add.js +93 -0
  15. package/src/commands/alias/helpers.js +107 -0
  16. package/src/commands/alias/index.js +14 -0
  17. package/src/commands/alias/list.js +55 -0
  18. package/src/commands/alias/remove.js +62 -0
  19. package/src/commands/alias/sync.js +109 -0
  20. package/src/commands/api/disable.js +73 -0
  21. package/src/commands/api/enable.js +148 -0
  22. package/src/commands/api/index.js +15 -0
  23. package/src/commands/api/list.js +66 -0
  24. package/src/commands/api/update.js +87 -0
  25. package/src/commands/auth/index.js +15 -0
  26. package/src/commands/auth/list.js +49 -0
  27. package/src/commands/auth/login.js +384 -0
  28. package/src/commands/auth/logout.js +111 -0
  29. package/src/commands/auth/refresh.js +184 -0
  30. package/src/commands/auth/services.js +169 -0
  31. package/src/commands/auth/status.js +104 -0
  32. package/src/commands/config/export.js +224 -0
  33. package/src/commands/config/get.js +52 -0
  34. package/src/commands/config/import.js +308 -0
  35. package/src/commands/config/index.js +17 -0
  36. package/src/commands/config/init.js +143 -0
  37. package/src/commands/config/reset.js +57 -0
  38. package/src/commands/config/set.js +93 -0
  39. package/src/commands/config/show.js +35 -0
  40. package/src/commands/help.js +338 -0
  41. package/src/commands/identity/add.js +133 -0
  42. package/src/commands/identity/index.js +17 -0
  43. package/src/commands/identity/link.js +76 -0
  44. package/src/commands/identity/list.js +48 -0
  45. package/src/commands/identity/remove.js +72 -0
  46. package/src/commands/identity/show.js +65 -0
  47. package/src/commands/identity/sync.js +172 -0
  48. package/src/commands/identity/unlink.js +57 -0
  49. package/src/commands/ignore/add.js +165 -0
  50. package/src/commands/ignore/index.js +14 -0
  51. package/src/commands/ignore/list.js +89 -0
  52. package/src/commands/ignore/markers.js +43 -0
  53. package/src/commands/ignore/remove.js +164 -0
  54. package/src/commands/ignore/show.js +169 -0
  55. package/src/commands/machine/detect.js +122 -0
  56. package/src/commands/machine/index.js +14 -0
  57. package/src/commands/machine/list.js +74 -0
  58. package/src/commands/machine/set.js +106 -0
  59. package/src/commands/machine/show.js +35 -0
  60. package/src/commands/schema.js +152 -0
  61. package/src/commands/search/collections.js +134 -0
  62. package/src/commands/search/get.js +71 -0
  63. package/src/commands/search/index-cmd.js +54 -0
  64. package/src/commands/search/index.js +21 -0
  65. package/src/commands/search/keyword.js +60 -0
  66. package/src/commands/search/qmd.js +70 -0
  67. package/src/commands/search/query.js +64 -0
  68. package/src/commands/search/semantic.js +62 -0
  69. package/src/commands/search/status.js +46 -0
  70. package/src/commands/status.js +276 -0
  71. package/src/commands/tools/check.js +79 -0
  72. package/src/commands/tools/index.js +14 -0
  73. package/src/commands/tools/install.js +110 -0
  74. package/src/commands/tools/list.js +91 -0
  75. package/src/commands/tools/search.js +60 -0
  76. package/src/commands/update.js +113 -0
  77. package/src/commands/util/add.js +151 -0
  78. package/src/commands/util/index.js +15 -0
  79. package/src/commands/util/list.js +97 -0
  80. package/src/commands/util/remove.js +76 -0
  81. package/src/commands/util/run.js +79 -0
  82. package/src/commands/util/show.js +67 -0
  83. package/src/commands/version.js +33 -0
  84. package/src/installers/_template.js +104 -0
  85. package/src/installers/git.js +150 -0
  86. package/src/installers/homebrew.js +190 -0
  87. package/src/installers/node.js +223 -0
  88. package/src/installers/registry.json +29 -0
  89. package/src/lib/config.js +125 -0
  90. package/src/lib/detect.js +74 -0
  91. package/src/lib/errors.js +114 -0
  92. package/src/lib/github.js +315 -0
  93. package/src/lib/installer.js +225 -0
  94. package/src/lib/output.js +239 -0
  95. package/src/lib/platform.js +112 -0
  96. package/src/lib/platforms/amazon-linux.js +41 -0
  97. package/src/lib/platforms/gitbash.js +46 -0
  98. package/src/lib/platforms/macos.js +45 -0
  99. package/src/lib/platforms/raspbian.js +41 -0
  100. package/src/lib/platforms/ubuntu.js +39 -0
  101. package/src/lib/platforms/windows.js +45 -0
  102. package/src/lib/prompt.js +161 -0
  103. package/src/lib/schema.js +211 -0
  104. package/src/lib/shell.js +75 -0
  105. package/src/patterns/gitignore/claude-code.txt +25 -0
  106. package/src/patterns/gitignore/docker.txt +15 -0
  107. package/src/patterns/gitignore/go.txt +24 -0
  108. package/src/patterns/gitignore/java.txt +38 -0
  109. package/src/patterns/gitignore/jetbrains.txt +26 -0
  110. package/src/patterns/gitignore/linux.txt +18 -0
  111. package/src/patterns/gitignore/macos.txt +27 -0
  112. package/src/patterns/gitignore/node.txt +51 -0
  113. package/src/patterns/gitignore/python.txt +55 -0
  114. package/src/patterns/gitignore/rust.txt +14 -0
  115. package/src/patterns/gitignore/terraform.txt +30 -0
  116. package/src/patterns/gitignore/vscode.txt +15 -0
  117. package/src/patterns/gitignore/windows.txt +25 -0
  118. package/src/utils/clone/index.js +165 -0
  119. package/src/utils/git-push/index.js +230 -0
  120. package/src/utils/git-status/index.js +116 -0
  121. package/src/utils/git-status/unix.sh +75 -0
  122. package/src/utils/registry.json +41 -0
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { AI_TOOLS } = require('./tools');
7
+
8
+ const AI_CONFIG_FILE = path.join(os.homedir(), '.devutils', 'ai.json');
9
+
10
+ /**
11
+ * Reads ~/.devutils/ai.json and returns its contents.
12
+ * Returns an empty object if the file does not exist or is unreadable.
13
+ *
14
+ * @returns {object} The parsed AI config, or {}.
15
+ */
16
+ function readAiConfig() {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(AI_CONFIG_FILE, 'utf8'));
19
+ } catch (err) {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ const VALID_KEYS = ['mode', 'model', 'flags'];
25
+
26
+ const meta = {
27
+ description: 'Set a default configuration value for an AI tool',
28
+ arguments: [
29
+ { name: 'tool', description: 'AI tool name (e.g., claude, gemini)', required: true },
30
+ { name: 'key', description: 'Configuration key to set (mode, model, flags)', required: true },
31
+ { name: 'value', description: 'Value to set', required: true }
32
+ ],
33
+ flags: []
34
+ };
35
+
36
+ /**
37
+ * Sets a default configuration value for a specified AI tool.
38
+ * Validates the tool name, key, and value before writing to ai.json.
39
+ *
40
+ * @param {object} args - Parsed CLI arguments { positional, flags }.
41
+ * @param {object} context - CLI context { output, errors }.
42
+ */
43
+ async function run(args, context) {
44
+ const toolName = args.positional[0];
45
+ const key = args.positional[1];
46
+ const rawValue = args.positional[2];
47
+
48
+ if (!toolName) {
49
+ context.errors.throwError(400, 'Missing required argument: <tool>. Example: dev ai set claude mode danger', 'ai');
50
+ return;
51
+ }
52
+
53
+ if (!key) {
54
+ context.errors.throwError(400, 'Missing required argument: <key>. Example: dev ai set claude mode danger', 'ai');
55
+ return;
56
+ }
57
+
58
+ if (rawValue === undefined || rawValue === null) {
59
+ context.errors.throwError(400, 'Missing required argument: <value>. Example: dev ai set claude mode danger', 'ai');
60
+ return;
61
+ }
62
+
63
+ // Validate the tool name
64
+ const toolConfig = AI_TOOLS[toolName];
65
+ if (!toolConfig) {
66
+ const available = Object.keys(AI_TOOLS).join(', ');
67
+ context.output.info(`Unknown AI tool "${toolName}". Available: ${available}`);
68
+ return;
69
+ }
70
+
71
+ // Validate the key
72
+ if (!VALID_KEYS.includes(key)) {
73
+ context.output.info(`Unknown configuration key "${key}". Valid keys: ${VALID_KEYS.join(', ')}`);
74
+ return;
75
+ }
76
+
77
+ // Validate and parse the value based on the key
78
+ let parsedValue;
79
+
80
+ if (key === 'mode') {
81
+ // Mode must be one of the tool's known modes
82
+ const availableModes = Object.keys(toolConfig.modes);
83
+ if (!availableModes.includes(rawValue)) {
84
+ context.output.info(
85
+ `Unknown mode "${rawValue}" for ${toolConfig.displayName}. Available modes: ${availableModes.join(', ')}`
86
+ );
87
+ return;
88
+ }
89
+ parsedValue = rawValue;
90
+ } else if (key === 'model') {
91
+ // "none" or "null" clears the model
92
+ if (rawValue === 'none' || rawValue === 'null') {
93
+ parsedValue = null;
94
+ } else {
95
+ parsedValue = rawValue;
96
+ }
97
+ } else if (key === 'flags') {
98
+ // Parse as a comma-separated list, trim whitespace, reject empty strings
99
+ parsedValue = rawValue
100
+ .split(',')
101
+ .map(flag => flag.trim())
102
+ .filter(flag => flag.length > 0);
103
+ }
104
+
105
+ // Read the current config (or start fresh)
106
+ const aiConfig = readAiConfig();
107
+ if (!aiConfig[toolName]) {
108
+ aiConfig[toolName] = {};
109
+ }
110
+ aiConfig[toolName][key] = parsedValue;
111
+
112
+ // Ensure the parent directory exists
113
+ const configDir = path.dirname(AI_CONFIG_FILE);
114
+ if (!fs.existsSync(configDir)) {
115
+ fs.mkdirSync(configDir, { recursive: true });
116
+ }
117
+
118
+ // Write the updated config
119
+ fs.writeFileSync(AI_CONFIG_FILE, JSON.stringify(aiConfig, null, 2) + '\n');
120
+
121
+ // Print confirmation
122
+ if (key === 'model' && parsedValue === null) {
123
+ context.output.info(`Set ${toolName}.${key} = (cleared)`);
124
+ } else if (key === 'flags') {
125
+ context.output.info(`Set ${toolName}.${key} = ${JSON.stringify(parsedValue)}`);
126
+ } else {
127
+ context.output.info(`Set ${toolName}.${key} = ${parsedValue}`);
128
+ }
129
+ }
130
+
131
+ module.exports = { meta, run };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const shell = require('../../lib/shell');
7
+ const { AI_TOOLS } = require('./tools');
8
+
9
+ const AI_CONFIG_FILE = path.join(os.homedir(), '.devutils', 'ai.json');
10
+
11
+ /**
12
+ * Reads ~/.devutils/ai.json and returns its contents.
13
+ * Returns an empty object if the file does not exist or is unreadable.
14
+ *
15
+ * @returns {object} The parsed AI config, or {}.
16
+ */
17
+ function readAiConfig() {
18
+ try {
19
+ return JSON.parse(fs.readFileSync(AI_CONFIG_FILE, 'utf8'));
20
+ } catch (err) {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ const meta = {
26
+ description: 'Show the current configuration for an AI tool',
27
+ arguments: [
28
+ { name: 'tool', description: 'AI tool name (e.g., claude, gemini)', required: true }
29
+ ],
30
+ flags: []
31
+ };
32
+
33
+ /**
34
+ * Displays the current configuration for a specified AI tool.
35
+ * Shows defaults when no configuration has been set yet.
36
+ *
37
+ * @param {object} args - Parsed CLI arguments { positional, flags }.
38
+ * @param {object} context - CLI context { output, errors }.
39
+ */
40
+ async function run(args, context) {
41
+ const toolName = args.positional[0];
42
+
43
+ if (!toolName) {
44
+ context.errors.throwError(400, 'Missing required argument: <tool>. Example: dev ai show claude', 'ai');
45
+ return;
46
+ }
47
+
48
+ // Validate the tool name
49
+ const toolConfig = AI_TOOLS[toolName];
50
+ if (!toolConfig) {
51
+ const available = Object.keys(AI_TOOLS).join(', ');
52
+ context.output.info(`Unknown AI tool "${toolName}". Available: ${available}`);
53
+ return;
54
+ }
55
+
56
+ // Read the user's config, falling back to defaults
57
+ const aiConfig = readAiConfig();
58
+ const config = aiConfig[toolName] || {};
59
+
60
+ const result = {
61
+ tool: toolName,
62
+ displayName: toolConfig.displayName,
63
+ binary: toolConfig.binary,
64
+ installed: shell.commandExists(toolConfig.binary),
65
+ mode: config.mode || 'default',
66
+ model: config.model || '(not set)',
67
+ flags: config.flags || [],
68
+ availableModes: Object.keys(toolConfig.modes)
69
+ };
70
+
71
+ context.output.out(result);
72
+ }
73
+
74
+ module.exports = { meta, run };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ /**
7
+ * Registry of known AI coding tools.
8
+ * Maps tool names to their CLI configuration, including binary names,
9
+ * mode flags, session storage paths, and display names.
10
+ *
11
+ * Adding a new AI tool is just adding an entry here.
12
+ */
13
+ const AI_TOOLS = {
14
+ claude: {
15
+ binary: 'claude',
16
+ modes: {
17
+ default: [],
18
+ danger: ['--dangerously-skip-permissions']
19
+ },
20
+ modelFlag: '--model',
21
+ promptFlag: '--prompt',
22
+ resumeFlag: '--resume',
23
+ displayName: 'Claude Code',
24
+ sessionPaths: [
25
+ path.join(os.homedir(), '.claude', 'projects'),
26
+ path.join(os.homedir(), '.config', 'claude', 'projects')
27
+ ]
28
+ },
29
+ gemini: {
30
+ binary: 'gemini',
31
+ modes: {
32
+ default: [],
33
+ yolo: ['--sandbox=false']
34
+ },
35
+ modelFlag: '--model',
36
+ promptFlag: '--prompt',
37
+ resumeFlag: '--resume',
38
+ displayName: 'Gemini CLI',
39
+ sessionPaths: [
40
+ path.join(os.homedir(), '.gemini', 'sessions'),
41
+ path.join(os.homedir(), '.config', 'gemini', 'sessions')
42
+ ]
43
+ }
44
+ };
45
+
46
+ module.exports = { AI_TOOLS };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const { loadAliases, saveAliases, generateWrapper, BIN_DIR } = require('./helpers');
4
+
5
+ const meta = {
6
+ description: 'Create a global shorthand command that maps to any dev command.',
7
+ arguments: [
8
+ { name: 'name', required: true, description: 'The alias name (what the user will type)' },
9
+ { name: 'command', required: true, description: 'The full command to run (quoted string)' }
10
+ ],
11
+ flags: [
12
+ { name: 'force', type: 'boolean', description: 'Overwrite an existing alias without prompting' }
13
+ ]
14
+ };
15
+
16
+ /**
17
+ * Creates a new alias by writing the mapping to aliases.json and generating
18
+ * a wrapper script in ~/.devutils/bin/.
19
+ *
20
+ * @param {object} args - Parsed command arguments (positional and flags).
21
+ * @param {object} context - The command context (output, prompt, errors, shell, platform).
22
+ */
23
+ async function run(args, context) {
24
+ // Step 1: Parse arguments
25
+ // The first positional arg is the alias name. Everything after it is the command.
26
+ const name = args.positional[0];
27
+ const command = args.positional.slice(1).join(' ');
28
+
29
+ if (!name || !command) {
30
+ context.output.error('Usage: dev alias add <name> "<command>"');
31
+ context.output.error('');
32
+ context.output.error('Examples:');
33
+ context.output.error(' dev alias add gs "dev util run git-status"');
34
+ context.output.error(' dev alias add clone "dev util run clone"');
35
+ context.output.error(' dev alias add claude-danger "dev ai launch claude"');
36
+ return;
37
+ }
38
+
39
+ // Step 2: Validate the alias name
40
+ // Must be lowercase letters, numbers, and hyphens. Must start with a letter or number.
41
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
42
+ context.output.error('Alias names must be lowercase letters, numbers, and hyphens only.');
43
+ return;
44
+ }
45
+
46
+ // Step 3: Check for system command conflicts
47
+ // Use shell.which to see if the name matches an existing command on the system PATH.
48
+ // Exclude our own bin directory from the check so re-creating an alias is not a conflict.
49
+ const binDir = BIN_DIR;
50
+ const existingPath = context.shell.which(name);
51
+
52
+ if (existingPath && !existingPath.startsWith(binDir)) {
53
+ context.output.error(`Warning: "${name}" already exists on your system at ${existingPath}`);
54
+ context.output.error('Creating this alias will shadow the existing command.');
55
+
56
+ if (!args.flags.force) {
57
+ const proceed = await context.prompt.confirm(
58
+ 'Create the alias anyway?',
59
+ false
60
+ );
61
+ if (!proceed) {
62
+ context.output.info('Cancelled.');
63
+ return;
64
+ }
65
+ }
66
+ }
67
+
68
+ // Step 4: Check for existing aliases
69
+ // If an alias with this name already exists, warn the user unless --force is set.
70
+ const aliases = loadAliases();
71
+
72
+ if (aliases[name] && !args.flags.force) {
73
+ context.output.error(`Alias "${name}" already exists: ${aliases[name]}`);
74
+ context.output.error('Use --force to overwrite it.');
75
+ return;
76
+ }
77
+
78
+ // Step 5: Write the alias to aliases.json
79
+ aliases[name] = command;
80
+ saveAliases(aliases);
81
+
82
+ // Step 6: Generate the wrapper script
83
+ const platform = context.platform.detect();
84
+ generateWrapper(name, command, binDir, platform.type);
85
+
86
+ // Step 7: Print confirmation
87
+ context.output.info(`Alias "${name}" created.`);
88
+ context.output.info(` ${name} -> ${command}`);
89
+ context.output.info('');
90
+ context.output.info(`You can now run: ${name}`);
91
+ }
92
+
93
+ module.exports = { meta, run };
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ /**
8
+ * Path to the aliases.json file in the user's .devutils directory.
9
+ * This is the source of truth for all alias definitions.
10
+ * @type {string}
11
+ */
12
+ const ALIASES_FILE = path.join(os.homedir(), '.devutils', 'aliases.json');
13
+
14
+ /**
15
+ * Path to the bin directory where wrapper scripts are generated.
16
+ * This directory should be on the user's PATH.
17
+ * @type {string}
18
+ */
19
+ const BIN_DIR = path.join(os.homedir(), '.devutils', 'bin');
20
+
21
+ /**
22
+ * Reads aliases.json and returns the parsed object.
23
+ * Returns an empty object if the file does not exist or cannot be parsed.
24
+ *
25
+ * @returns {object} A flat object mapping alias names to command strings.
26
+ */
27
+ function loadAliases() {
28
+ if (!fs.existsSync(ALIASES_FILE)) {
29
+ return {};
30
+ }
31
+ try {
32
+ return JSON.parse(fs.readFileSync(ALIASES_FILE, 'utf8'));
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Writes the aliases object to aliases.json.
40
+ * Creates the parent directory if it does not exist.
41
+ *
42
+ * @param {object} aliases - A flat object mapping alias names to command strings.
43
+ */
44
+ function saveAliases(aliases) {
45
+ const dir = path.dirname(ALIASES_FILE);
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+ fs.writeFileSync(ALIASES_FILE, JSON.stringify(aliases, null, 2) + '\n');
50
+ }
51
+
52
+ /**
53
+ * Generates a wrapper script for an alias in the bin directory.
54
+ * On Unix (macOS, Linux, Git Bash), writes a shell script with exec.
55
+ * On Windows, writes a .cmd file.
56
+ *
57
+ * @param {string} name - The alias name (used as the filename).
58
+ * @param {string} command - The full command the alias maps to.
59
+ * @param {string} binDir - The directory to write the wrapper script into.
60
+ * @param {string} platformType - The platform type from platform.detect().type.
61
+ */
62
+ function generateWrapper(name, command, binDir, platformType) {
63
+ if (!fs.existsSync(binDir)) {
64
+ fs.mkdirSync(binDir, { recursive: true });
65
+ }
66
+
67
+ if (platformType === 'windows') {
68
+ // Windows .cmd file
69
+ const scriptPath = path.join(binDir, name + '.cmd');
70
+ const content = `@${command} %*\r\n`;
71
+ fs.writeFileSync(scriptPath, content);
72
+ } else {
73
+ // Unix shell script (macOS, Linux, Git Bash)
74
+ const scriptPath = path.join(binDir, name);
75
+ const content = `#!/bin/sh\nexec ${command} "$@"\n`;
76
+ fs.writeFileSync(scriptPath, content, { mode: 0o755 });
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Deletes wrapper scripts for an alias from the bin directory.
82
+ * Removes both Unix (no extension) and Windows (.cmd) formats to handle
83
+ * cross-platform scenarios.
84
+ *
85
+ * @param {string} name - The alias name to delete.
86
+ * @param {string} binDir - The directory containing the wrapper scripts.
87
+ */
88
+ function deleteWrapper(name, binDir) {
89
+ const unixPath = path.join(binDir, name);
90
+ const windowsPath = path.join(binDir, name + '.cmd');
91
+
92
+ if (fs.existsSync(unixPath)) {
93
+ fs.unlinkSync(unixPath);
94
+ }
95
+ if (fs.existsSync(windowsPath)) {
96
+ fs.unlinkSync(windowsPath);
97
+ }
98
+ }
99
+
100
+ module.exports = {
101
+ ALIASES_FILE,
102
+ BIN_DIR,
103
+ loadAliases,
104
+ saveAliases,
105
+ generateWrapper,
106
+ deleteWrapper,
107
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Alias service registration.
3
+ * Shorthand bin entries (user-controlled).
4
+ */
5
+ module.exports = {
6
+ name: 'alias',
7
+ description: 'Shorthand bin entries (user-controlled)',
8
+ commands: {
9
+ add: () => require('./add'),
10
+ remove: () => require('./remove'),
11
+ list: () => require('./list'),
12
+ sync: () => require('./sync'),
13
+ }
14
+ };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { loadAliases, BIN_DIR } = require('./helpers');
6
+
7
+ const meta = {
8
+ description: 'List all registered aliases and the commands they map to.',
9
+ arguments: [],
10
+ flags: []
11
+ };
12
+
13
+ /**
14
+ * Lists all registered aliases from aliases.json, sorted alphabetically.
15
+ * Shows a status indicator for aliases that are missing their wrapper script.
16
+ *
17
+ * @param {object} args - Parsed command arguments (positional and flags).
18
+ * @param {object} context - The command context (output).
19
+ */
20
+ async function run(args, context) {
21
+ // Step 1: Load aliases.json
22
+ const aliases = loadAliases();
23
+ const entries = Object.entries(aliases);
24
+
25
+ // Step 2: Handle the empty case
26
+ if (entries.length === 0) {
27
+ context.output.info('No aliases registered.');
28
+ context.output.info('');
29
+ context.output.info('Create one with: dev alias add <name> "<command>"');
30
+ context.output.info('Example: dev alias add gs "dev util run git-status"');
31
+ return;
32
+ }
33
+
34
+ // Step 3: Sort alphabetically by name
35
+ entries.sort((a, b) => a[0].localeCompare(b[0]));
36
+
37
+ // Step 4: Display the aliases
38
+ const binDir = BIN_DIR;
39
+
40
+ context.output.info(`Aliases (${entries.length}):`);
41
+ context.output.info('');
42
+
43
+ for (const [name, command] of entries) {
44
+ // Check if the wrapper script exists (either Unix or Windows format)
45
+ const scriptExists = fs.existsSync(path.join(binDir, name))
46
+ || fs.existsSync(path.join(binDir, name + '.cmd'));
47
+ const status = scriptExists ? '' : ' (no script -- run dev alias sync)';
48
+ const nameCol = name.padEnd(25);
49
+ context.output.info(` ${nameCol} -> ${command}${status}`);
50
+ }
51
+
52
+ context.output.info('');
53
+ }
54
+
55
+ module.exports = { meta, run };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const { loadAliases, saveAliases, deleteWrapper, BIN_DIR } = require('./helpers');
4
+
5
+ const meta = {
6
+ description: 'Remove an alias and delete its wrapper script from ~/.devutils/bin/.',
7
+ arguments: [
8
+ { name: 'name', required: true, description: 'The alias name to remove' }
9
+ ],
10
+ flags: [
11
+ { name: 'confirm', type: 'boolean', description: 'Skip the confirmation prompt' }
12
+ ]
13
+ };
14
+
15
+ /**
16
+ * Removes an alias by deleting its entry from aliases.json and removing
17
+ * the wrapper script from ~/.devutils/bin/.
18
+ *
19
+ * @param {object} args - Parsed command arguments (positional and flags).
20
+ * @param {object} context - The command context (output, prompt, errors).
21
+ */
22
+ async function run(args, context) {
23
+ // Step 1: Validate the name and check it exists
24
+ const name = args.positional[0];
25
+ if (!name) {
26
+ context.output.error('Usage: dev alias remove <name>');
27
+ return;
28
+ }
29
+
30
+ const aliases = loadAliases();
31
+
32
+ if (!aliases[name]) {
33
+ context.output.error(`Alias "${name}" is not registered.`);
34
+ context.output.error('Run "dev alias list" to see all aliases.');
35
+ return;
36
+ }
37
+
38
+ // Step 2: Confirm removal unless --confirm flag is set
39
+ if (!args.flags.confirm) {
40
+ const ok = await context.prompt.confirm(
41
+ `Remove alias "${name}" (${aliases[name]})?`,
42
+ true
43
+ );
44
+ if (!ok) {
45
+ context.output.info('Cancelled.');
46
+ return;
47
+ }
48
+ }
49
+
50
+ // Step 3: Delete the wrapper script (both Unix and Windows formats)
51
+ const binDir = BIN_DIR;
52
+ deleteWrapper(name, binDir);
53
+
54
+ // Step 4: Remove from aliases.json
55
+ delete aliases[name];
56
+ saveAliases(aliases);
57
+
58
+ // Step 5: Print confirmation
59
+ context.output.info(`Alias "${name}" removed.`);
60
+ }
61
+
62
+ module.exports = { meta, run };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { loadAliases, generateWrapper, BIN_DIR } = require('./helpers');
6
+
7
+ const meta = {
8
+ description: 'Rebuild all alias wrapper scripts from aliases.json. Cleans up orphaned scripts.',
9
+ arguments: [],
10
+ flags: [
11
+ { name: 'dry-run', type: 'boolean', description: 'Show what would be done without doing it' }
12
+ ]
13
+ };
14
+
15
+ /**
16
+ * Regenerates all wrapper scripts from aliases.json and removes orphaned
17
+ * scripts that no longer have a matching alias entry. This is the repair
18
+ * command for the alias system -- after importing config on a new machine,
19
+ * run sync to rebuild all the wrapper scripts.
20
+ *
21
+ * @param {object} args - Parsed command arguments (positional and flags).
22
+ * @param {object} context - The command context (output, platform, flags).
23
+ */
24
+ async function run(args, context) {
25
+ // Step 1: Load aliases.json
26
+ const aliases = loadAliases();
27
+
28
+ // Step 2: Ensure the bin directory exists
29
+ const binDir = BIN_DIR;
30
+ if (!fs.existsSync(binDir)) {
31
+ fs.mkdirSync(binDir, { recursive: true });
32
+ }
33
+
34
+ // Step 3: Scan existing scripts in the bin directory
35
+ const existingFiles = fs.readdirSync(binDir);
36
+
37
+ // Step 4: Determine what to create and what to remove
38
+ const platform = context.platform.detect();
39
+
40
+ // Find orphans: files in bin/ that are not in aliases.json
41
+ const orphans = existingFiles.filter(file => {
42
+ // Strip .cmd extension for comparison
43
+ const baseName = file.endsWith('.cmd') ? file.slice(0, -4) : file;
44
+ return !aliases[baseName];
45
+ });
46
+
47
+ // Find missing: aliases that do not have a wrapper script yet
48
+ const missing = Object.keys(aliases).filter(name => {
49
+ return !fs.existsSync(path.join(binDir, name))
50
+ && !fs.existsSync(path.join(binDir, name + '.cmd'));
51
+ });
52
+
53
+ // Step 5: Handle dry-run mode
54
+ // --dry-run is a global flag (context.flags.dryRun) or a command flag (args.flags['dry-run'])
55
+ const isDryRun = context.flags.dryRun || args.flags['dry-run'];
56
+
57
+ if (isDryRun) {
58
+ if (missing.length === 0 && orphans.length === 0) {
59
+ context.output.info('Everything is in sync. Nothing to do.');
60
+ return;
61
+ }
62
+
63
+ if (missing.length > 0) {
64
+ context.output.info(`Would create ${missing.length} wrapper script(s):`);
65
+ for (const name of missing) {
66
+ context.output.info(` + ${name} -> ${aliases[name]}`);
67
+ }
68
+ }
69
+
70
+ if (orphans.length > 0) {
71
+ context.output.info(`Would remove ${orphans.length} orphaned script(s):`);
72
+ for (const file of orphans) {
73
+ context.output.info(` - ${file}`);
74
+ }
75
+ }
76
+ return;
77
+ }
78
+
79
+ // Step 6: Generate all wrapper scripts
80
+ // Regenerate every script (not just missing ones) to ensure they are all up to date.
81
+ // This handles cases where the command mapping changed in aliases.json.
82
+ let created = 0;
83
+ for (const [name, command] of Object.entries(aliases)) {
84
+ generateWrapper(name, command, binDir, platform.type);
85
+ created++;
86
+ }
87
+
88
+ // Step 7: Remove orphaned scripts
89
+ let removed = 0;
90
+ for (const file of orphans) {
91
+ const filePath = path.join(binDir, file);
92
+ fs.unlinkSync(filePath);
93
+ removed++;
94
+ }
95
+
96
+ // Step 8: Report results
97
+ context.output.info('Alias sync complete.');
98
+ context.output.info(` ${created} script(s) generated`);
99
+ if (removed > 0) {
100
+ context.output.info(` ${removed} orphaned script(s) removed`);
101
+ }
102
+ context.output.info('');
103
+
104
+ if (created > 0) {
105
+ context.output.info('Make sure ~/.devutils/bin is in your PATH.');
106
+ }
107
+ }
108
+
109
+ module.exports = { meta, run };