@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,46 @@
1
+ 'use strict';
2
+
3
+ const shell = require('../../lib/shell');
4
+ const { checkQmd, parseSearchResults } = require('./qmd');
5
+
6
+ const meta = {
7
+ description: 'Show search index health, collection count, and document count',
8
+ arguments: [],
9
+ flags: []
10
+ };
11
+
12
+ /**
13
+ * Shows the health and status of the QMD search index.
14
+ * Displays collection count, document count, last update time, and health.
15
+ *
16
+ * @param {object} args - Parsed CLI arguments { positional, flags }.
17
+ * @param {object} context - CLI context { output, errors }.
18
+ */
19
+ async function run(args, context) {
20
+ // Check for QMD availability first
21
+ const qmd = checkQmd();
22
+ if (!qmd.available) {
23
+ context.errors.throwError(1, qmd.message, 'search');
24
+ return;
25
+ }
26
+
27
+ // Run qmd status and capture the output
28
+ const result = await shell.exec('qmd status');
29
+
30
+ if (result.exitCode !== 0) {
31
+ context.errors.throwError(1, result.stderr || 'Failed to get search index status.', 'search');
32
+ return;
33
+ }
34
+
35
+ // Try to parse the output as JSON for structured data
36
+ const output = result.stdout;
37
+ try {
38
+ const parsed = JSON.parse(output);
39
+ context.output.out(parsed);
40
+ } catch (err) {
41
+ // QMD returned non-JSON output, pass it through as-is
42
+ context.output.info(output || 'No status information available.');
43
+ }
44
+ }
45
+
46
+ module.exports = { meta, run };
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * status command.
5
+ * An overall health check that shows the current state of the DevUtils
6
+ * installation in a single-screen dashboard. Checks config, machine
7
+ * profile, auth services, aliases, API plugins, and sync status.
8
+ *
9
+ * This command is read-only and never modifies any files. It handles
10
+ * missing files gracefully, showing helpful placeholder messages
11
+ * instead of crashing.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ const meta = {
19
+ description: 'Overall health check',
20
+ arguments: [],
21
+ flags: []
22
+ };
23
+
24
+ /**
25
+ * Checks the config.json status. Reports whether the file exists
26
+ * and whether it contains valid JSON.
27
+ *
28
+ * @param {object} config - The context.config module.
29
+ * @returns {{ exists: boolean, valid: boolean }}
30
+ */
31
+ function checkConfig(config) {
32
+ if (!config.exists('config.json')) {
33
+ return { exists: false, valid: false };
34
+ }
35
+ const data = config.read('config.json');
36
+ if (data === null) {
37
+ return { exists: true, valid: false };
38
+ }
39
+ return { exists: true, valid: true };
40
+ }
41
+
42
+ /**
43
+ * Checks the machine profile. Reads machines/current.json and pulls
44
+ * out key fields like type, arch, hostname, and packageManager.
45
+ *
46
+ * @param {object} config - The context.config module.
47
+ * @returns {{ detected: boolean, type?: string, arch?: string, hostname?: string, packageManager?: string }}
48
+ */
49
+ function checkMachine(config) {
50
+ const profile = config.read('machines/current.json');
51
+ if (!profile) {
52
+ return { detected: false };
53
+ }
54
+
55
+ return {
56
+ detected: true,
57
+ type: profile.os ? profile.os.type : (profile.type || 'unknown'),
58
+ arch: profile.arch || 'unknown',
59
+ hostname: profile.hostname || 'unknown',
60
+ packageManager: profile.packageManagers
61
+ ? profile.packageManagers[0] || null
62
+ : (profile.packageManager || null),
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Checks the auth directory for connected services.
68
+ * Each JSON file (excluding subdirectories) represents a connected service.
69
+ *
70
+ * @param {object} config - The context.config module.
71
+ * @returns {{ services: string[], count: number }}
72
+ */
73
+ function checkAuth(config) {
74
+ const authDir = config.getPath('auth');
75
+ if (!fs.existsSync(authDir)) {
76
+ return { services: [], count: 0 };
77
+ }
78
+
79
+ try {
80
+ const entries = fs.readdirSync(authDir);
81
+ const services = entries
82
+ .filter(entry => {
83
+ const fullPath = path.join(authDir, entry);
84
+ return fs.statSync(fullPath).isFile() && entry.endsWith('.json');
85
+ })
86
+ .map(file => path.basename(file, '.json'));
87
+
88
+ return { services, count: services.length };
89
+ } catch {
90
+ return { services: [], count: 0 };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Checks the aliases.json file for registered aliases.
96
+ *
97
+ * @param {object} config - The context.config module.
98
+ * @returns {{ count: number }}
99
+ */
100
+ function checkAliases(config) {
101
+ const data = config.read('aliases.json');
102
+ if (!data) {
103
+ return { count: 0 };
104
+ }
105
+
106
+ // Handle both array and object formats
107
+ if (Array.isArray(data)) {
108
+ return { count: data.length };
109
+ }
110
+ return { count: Object.keys(data).length };
111
+ }
112
+
113
+ /**
114
+ * Checks the plugins.json file for installed API plugins.
115
+ *
116
+ * @param {object} config - The context.config module.
117
+ * @returns {{ count: number, plugins: string[] }}
118
+ */
119
+ function checkPlugins(config) {
120
+ const data = config.read('plugins.json');
121
+ if (!data) {
122
+ return { count: 0, plugins: [] };
123
+ }
124
+
125
+ // Handle both array and object formats
126
+ if (Array.isArray(data)) {
127
+ return { count: data.length, plugins: data };
128
+ }
129
+ const plugins = Object.keys(data);
130
+ return { count: plugins.length, plugins };
131
+ }
132
+
133
+ /**
134
+ * Checks the sync.json file for the last backup timestamp.
135
+ *
136
+ * @param {object} config - The context.config module.
137
+ * @returns {{ lastBackup: string|null }}
138
+ */
139
+ function checkSync(config) {
140
+ const data = config.read('sync.json');
141
+ if (!data || !data.lastBackup) {
142
+ return { lastBackup: null };
143
+ }
144
+ return { lastBackup: data.lastBackup };
145
+ }
146
+
147
+ /**
148
+ * Formats a human-readable summary line for the config section.
149
+ *
150
+ * @param {{ exists: boolean, valid: boolean }} configStatus - The config check result.
151
+ * @returns {string}
152
+ */
153
+ function formatConfig(configStatus) {
154
+ if (!configStatus.exists) {
155
+ return 'Not found. Run \'dev config init\' to get started.';
156
+ }
157
+ if (!configStatus.valid) {
158
+ return 'Invalid JSON. Check ~/.devutils/config.json for syntax errors.';
159
+ }
160
+ return 'OK';
161
+ }
162
+
163
+ /**
164
+ * Formats a human-readable summary line for the machine section.
165
+ *
166
+ * @param {{ detected: boolean, type?: string, arch?: string, packageManager?: string }} machineStatus - The machine check result.
167
+ * @returns {string}
168
+ */
169
+ function formatMachine(machineStatus) {
170
+ if (!machineStatus.detected) {
171
+ return 'Not detected. Run \'dev machine detect\' to set up.';
172
+ }
173
+ const pm = machineStatus.packageManager ? ` (${machineStatus.packageManager})` : '';
174
+ return `${machineStatus.type} ${machineStatus.arch}${pm}`;
175
+ }
176
+
177
+ /**
178
+ * Formats a human-readable summary line for the auth section.
179
+ *
180
+ * @param {{ services: string[], count: number }} authStatus - The auth check result.
181
+ * @returns {string}
182
+ */
183
+ function formatAuth(authStatus) {
184
+ if (authStatus.count === 0) {
185
+ return 'No services connected.';
186
+ }
187
+ return `${authStatus.count} services (${authStatus.services.join(', ')})`;
188
+ }
189
+
190
+ /**
191
+ * Formats a human-readable summary line for the aliases section.
192
+ *
193
+ * @param {{ count: number }} aliasStatus - The alias check result.
194
+ * @returns {string}
195
+ */
196
+ function formatAliases(aliasStatus) {
197
+ if (aliasStatus.count === 0) {
198
+ return 'None registered.';
199
+ }
200
+ return `${aliasStatus.count} registered`;
201
+ }
202
+
203
+ /**
204
+ * Formats a human-readable summary line for the plugins section.
205
+ *
206
+ * @param {{ count: number, plugins: string[] }} pluginStatus - The plugin check result.
207
+ * @returns {string}
208
+ */
209
+ function formatPlugins(pluginStatus) {
210
+ if (pluginStatus.count === 0) {
211
+ return 'None installed.';
212
+ }
213
+ return `${pluginStatus.count} installed (${pluginStatus.plugins.join(', ')})`;
214
+ }
215
+
216
+ /**
217
+ * Formats a human-readable summary line for the sync section.
218
+ *
219
+ * @param {{ lastBackup: string|null }} syncStatus - The sync check result.
220
+ * @returns {string}
221
+ */
222
+ function formatSync(syncStatus) {
223
+ if (!syncStatus.lastBackup) {
224
+ return 'Never.';
225
+ }
226
+ // Format the ISO timestamp into something more readable
227
+ try {
228
+ const date = new Date(syncStatus.lastBackup);
229
+ return date.toLocaleString();
230
+ } catch {
231
+ return syncStatus.lastBackup;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Runs the status health check. Gathers information from config files,
237
+ * machine profile, auth directory, aliases, plugins, and sync state.
238
+ * Outputs a compact summary for humans or a structured object for JSON.
239
+ *
240
+ * @param {object} args - Parsed command arguments (none expected).
241
+ * @param {object} context - The CLI context object with config, platform, shell, output, flags.
242
+ */
243
+ async function run(args, context) {
244
+ const configStatus = checkConfig(context.config);
245
+ const machineStatus = checkMachine(context.config);
246
+ const authStatus = checkAuth(context.config);
247
+ const aliasStatus = checkAliases(context.config);
248
+ const pluginStatus = checkPlugins(context.config);
249
+ const syncStatus = checkSync(context.config);
250
+
251
+ const result = {
252
+ config: configStatus,
253
+ machine: machineStatus,
254
+ auth: authStatus,
255
+ aliases: aliasStatus,
256
+ plugins: pluginStatus,
257
+ sync: syncStatus,
258
+ };
259
+
260
+ if (context.flags.format === 'json') {
261
+ context.output.out(result);
262
+ return;
263
+ }
264
+
265
+ // Human-friendly output
266
+ context.output.info('DevUtils Status');
267
+ context.output.info('---------------');
268
+ context.output.info(`Config: ${formatConfig(configStatus)}`);
269
+ context.output.info(`Machine: ${formatMachine(machineStatus)}`);
270
+ context.output.info(`Auth: ${formatAuth(authStatus)}`);
271
+ context.output.info(`Aliases: ${formatAliases(aliasStatus)}`);
272
+ context.output.info(`API Plugins: ${formatPlugins(pluginStatus)}`);
273
+ context.output.info(`Last Sync: ${formatSync(syncStatus)}`);
274
+ }
275
+
276
+ module.exports = { meta, run };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Command: dev tools check <tool-name>
5
+ *
6
+ * Checks if a tool is installed and shows its version.
7
+ */
8
+
9
+ /**
10
+ * @type {{ description: string, arguments: Array, flags: Array }}
11
+ */
12
+ const meta = {
13
+ description: 'Check if a tool is installed and show its version',
14
+ arguments: [
15
+ { name: 'tool', description: 'Name of the tool to check', required: true }
16
+ ],
17
+ flags: []
18
+ };
19
+
20
+ /**
21
+ * Runs the check command.
22
+ * @param {{ positional: string[], flags: object }} args - Parsed command arguments.
23
+ * @param {object} context - The CLI context object.
24
+ */
25
+ async function run(args, context) {
26
+ const toolName = args.positional[0];
27
+
28
+ if (!toolName) {
29
+ context.errors.throwError(400, 'Missing required argument: tool. Usage: dev tools check <tool-name>', 'tools');
30
+ return;
31
+ }
32
+
33
+ const installer = require('../../lib/installer');
34
+
35
+ // Look up the tool in the registry
36
+ const tool = installer.findTool(toolName);
37
+ if (!tool) {
38
+ context.output.error(`Tool '${toolName}' not found in registry. Run "dev tools search ${toolName}" to search.`);
39
+ return;
40
+ }
41
+
42
+ try {
43
+ // Load the installer and check if the tool is installed
44
+ const mod = installer.loadInstaller(tool);
45
+ const installed = await mod.isInstalled(context);
46
+
47
+ if (installed) {
48
+ // Try to get the version
49
+ let version = 'version unknown';
50
+ if (typeof mod.getVersion === 'function') {
51
+ const ver = await mod.getVersion(context);
52
+ if (ver) {
53
+ version = `version ${ver}`;
54
+ }
55
+ } else {
56
+ // Fall back to --version
57
+ try {
58
+ const result = await context.shell.exec(`${tool.name} --version`);
59
+ if (result.exitCode === 0 && result.stdout) {
60
+ const match = result.stdout.trim().match(/(\d+\.\d+[\.\d]*)/);
61
+ if (match) {
62
+ version = `version ${match[1]}`;
63
+ }
64
+ }
65
+ } catch {
66
+ // version remains "version unknown"
67
+ }
68
+ }
69
+
70
+ context.output.out({ name: tool.name, installed: true, version });
71
+ } else {
72
+ context.output.out({ name: tool.name, installed: false, version: null });
73
+ }
74
+ } catch (err) {
75
+ context.output.error(err.message);
76
+ }
77
+ }
78
+
79
+ module.exports = { meta, run };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tools service registration.
3
+ * Tool installation and management.
4
+ */
5
+ module.exports = {
6
+ name: 'tools',
7
+ description: 'Tool installation and management',
8
+ commands: {
9
+ install: () => require('./install'),
10
+ check: () => require('./check'),
11
+ list: () => require('./list'),
12
+ search: () => require('./search'),
13
+ }
14
+ };
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Command: dev tools install <tool-name>
5
+ *
6
+ * Installs a development tool using the platform-appropriate method.
7
+ * Resolves and installs dependencies automatically.
8
+ */
9
+
10
+ /**
11
+ * @type {{ description: string, arguments: Array, flags: Array }}
12
+ */
13
+ const meta = {
14
+ description: 'Install a development tool',
15
+ arguments: [
16
+ { name: 'tool', description: 'Name of the tool to install', required: true }
17
+ ],
18
+ flags: [
19
+ { name: '--dry-run', description: 'Show what would be installed without doing it' },
20
+ { name: '--skip-deps', description: 'Skip dependency installation' }
21
+ ]
22
+ };
23
+
24
+ /**
25
+ * Runs the install command.
26
+ * @param {{ positional: string[], flags: object }} args - Parsed command arguments.
27
+ * @param {object} context - The CLI context object.
28
+ */
29
+ async function run(args, context) {
30
+ const toolName = args.positional[0];
31
+
32
+ if (!toolName) {
33
+ context.errors.throwError(400, 'Missing required argument: tool. Usage: dev tools install <tool-name>', 'tools');
34
+ return;
35
+ }
36
+
37
+ const installer = require('../../lib/installer');
38
+
39
+ // Look up the tool in the registry
40
+ const tool = installer.findTool(toolName);
41
+ if (!tool) {
42
+ context.output.error(`Tool '${toolName}' not found in registry. Run "dev tools search ${toolName}" to search.`);
43
+ return;
44
+ }
45
+
46
+ // Check platform support
47
+ const platformType = context.platform.detect().type;
48
+ if (!tool.platforms.includes(platformType)) {
49
+ context.output.error(
50
+ `Tool '${tool.name}' is not supported on ${platformType}. ` +
51
+ `Supported platforms: ${tool.platforms.join(', ')}`
52
+ );
53
+ return;
54
+ }
55
+
56
+ // Dry run: show what would be installed without doing it
57
+ if (context.flags.dryRun || args.flags['dry-run']) {
58
+ try {
59
+ const chain = installer.resolveDependencies(tool.name);
60
+
61
+ // Check which tools are already installed
62
+ const statuses = [];
63
+ for (const name of chain) {
64
+ const isAlready = await installer.checkInstalled(name, context);
65
+ statuses.push({ name, installed: isAlready });
66
+ }
67
+
68
+ const needsInstall = statuses.filter(s => !s.installed);
69
+ const alreadyInstalled = statuses.filter(s => s.installed);
70
+
71
+ if (needsInstall.length === 0) {
72
+ context.output.info(`${tool.name} is already installed (and all dependencies).`);
73
+ } else {
74
+ context.output.info('Would install (in order):');
75
+ for (const s of needsInstall) {
76
+ context.output.info(` - ${s.name}`);
77
+ }
78
+ if (alreadyInstalled.length > 0) {
79
+ context.output.info('Already installed:');
80
+ for (const s of alreadyInstalled) {
81
+ context.output.info(` - ${s.name}`);
82
+ }
83
+ }
84
+ }
85
+ } catch (err) {
86
+ context.output.error(err.message);
87
+ }
88
+ return;
89
+ }
90
+
91
+ // Actually install
92
+ try {
93
+ const result = await installer.installTool(tool.name, context);
94
+
95
+ if (result.alreadyInstalled) {
96
+ context.output.info(`${tool.name} is already installed.`);
97
+ } else {
98
+ if (result.dependenciesInstalled.length > 0) {
99
+ context.output.info(`Dependencies installed: ${result.dependenciesInstalled.join(', ')}`);
100
+ }
101
+ if (result.installed) {
102
+ context.output.info(`${tool.name} installed successfully.`);
103
+ }
104
+ }
105
+ } catch (err) {
106
+ context.output.error(err.message);
107
+ }
108
+ }
109
+
110
+ module.exports = { meta, run };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Command: dev tools list
5
+ *
6
+ * Lists all available tools from the registry, with their install status.
7
+ * Can filter by --installed, --available, or --platform.
8
+ */
9
+
10
+ /**
11
+ * @type {{ description: string, arguments: Array, flags: Array }}
12
+ */
13
+ const meta = {
14
+ description: 'List available tools',
15
+ arguments: [],
16
+ flags: [
17
+ { name: '--installed', description: 'Show only installed tools' },
18
+ { name: '--available', description: 'Show only tools not yet installed' },
19
+ { name: '--platform', description: 'Filter by platform (default: current)' }
20
+ ]
21
+ };
22
+
23
+ /**
24
+ * Runs the list command.
25
+ * @param {{ positional: string[], flags: object }} args - Parsed command arguments.
26
+ * @param {object} context - The CLI context object.
27
+ */
28
+ async function run(args, context) {
29
+ const installer = require('../../lib/installer');
30
+
31
+ // Load the full registry
32
+ const tools = installer.loadRegistry();
33
+
34
+ // Determine platform filter
35
+ const platformFilter = args.flags.platform || context.platform.detect().type;
36
+
37
+ // Filter to tools that support the target platform
38
+ let filtered = tools.filter(t => t.platforms.includes(platformFilter));
39
+
40
+ // If --installed or --available, check each tool's install status
41
+ const showInstalled = args.flags.installed === true;
42
+ const showAvailable = args.flags.available === true;
43
+
44
+ if (showInstalled || showAvailable) {
45
+ const statusResults = [];
46
+ for (const tool of filtered) {
47
+ try {
48
+ const mod = installer.loadInstaller(tool);
49
+ const isInst = await mod.isInstalled(context);
50
+ statusResults.push({ tool, installed: isInst });
51
+ } catch {
52
+ // If the installer can't be loaded, treat as not installed
53
+ statusResults.push({ tool, installed: false });
54
+ }
55
+ }
56
+
57
+ if (showInstalled) {
58
+ filtered = statusResults.filter(r => r.installed).map(r => r.tool);
59
+ } else {
60
+ filtered = statusResults.filter(r => !r.installed).map(r => r.tool);
61
+ }
62
+ }
63
+
64
+ if (filtered.length === 0) {
65
+ context.output.info('No tools match the current filters.');
66
+ return;
67
+ }
68
+
69
+ // Build output rows with install status
70
+ const rows = [];
71
+ for (const tool of filtered) {
72
+ let status = '-';
73
+ try {
74
+ const mod = installer.loadInstaller(tool);
75
+ const isInst = await mod.isInstalled(context);
76
+ status = isInst ? 'yes' : 'no';
77
+ } catch {
78
+ status = '?';
79
+ }
80
+
81
+ rows.push({
82
+ Name: tool.name,
83
+ Description: tool.description,
84
+ Installed: status,
85
+ });
86
+ }
87
+
88
+ context.output.out(rows);
89
+ }
90
+
91
+ module.exports = { meta, run };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Command: dev tools search <query>
5
+ *
6
+ * Searches for tools by name or keyword in the registry.
7
+ * Matches against tool name and description (case-insensitive).
8
+ */
9
+
10
+ /**
11
+ * @type {{ description: string, arguments: Array, flags: Array }}
12
+ */
13
+ const meta = {
14
+ description: 'Search for tools by name or keyword',
15
+ arguments: [
16
+ { name: 'query', description: 'Search term (matches name and description)', required: true }
17
+ ],
18
+ flags: []
19
+ };
20
+
21
+ /**
22
+ * Runs the search command.
23
+ * @param {{ positional: string[], flags: object }} args - Parsed command arguments.
24
+ * @param {object} context - The CLI context object.
25
+ */
26
+ async function run(args, context) {
27
+ const query = args.positional[0];
28
+
29
+ if (!query) {
30
+ context.errors.throwError(400, 'Missing required argument: query. Usage: dev tools search <query>', 'tools');
31
+ return;
32
+ }
33
+
34
+ const installer = require('../../lib/installer');
35
+
36
+ // Load the registry and search
37
+ const tools = installer.loadRegistry();
38
+ const lower = query.toLowerCase();
39
+
40
+ const matches = tools.filter(t =>
41
+ t.name.toLowerCase().includes(lower) ||
42
+ t.description.toLowerCase().includes(lower)
43
+ );
44
+
45
+ if (matches.length === 0) {
46
+ context.output.info(`No tools found matching '${query}'.`);
47
+ return;
48
+ }
49
+
50
+ // Format as table with name, description, and platforms
51
+ const rows = matches.map(t => ({
52
+ Name: t.name,
53
+ Description: t.description,
54
+ Platforms: t.platforms.join(', '),
55
+ }));
56
+
57
+ context.output.out(rows);
58
+ }
59
+
60
+ module.exports = { meta, run };