@fredlackey/devutils 0.0.1 → 0.0.3

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 (259) hide show
  1. package/README.md +5 -5
  2. package/package.json +1 -1
  3. package/src/commands/install.js +374 -36
  4. package/src/installs/adobe-creative-cloud.js +527 -25
  5. package/src/installs/adobe-creative-cloud.md +605 -0
  6. package/src/installs/appcleaner.js +303 -26
  7. package/src/installs/appcleaner.md +699 -0
  8. package/src/installs/apt-transport-https.js +390 -0
  9. package/src/installs/apt-transport-https.md +678 -0
  10. package/src/installs/atomicparsley.js +624 -26
  11. package/src/installs/atomicparsley.md +795 -0
  12. package/src/installs/aws-cli.js +779 -26
  13. package/src/installs/aws-cli.md +727 -0
  14. package/src/installs/balena-etcher.js +688 -26
  15. package/src/installs/balena-etcher.md +761 -0
  16. package/src/installs/bambu-studio.js +912 -26
  17. package/src/installs/bambu-studio.md +780 -0
  18. package/src/installs/bash-completion.js +554 -23
  19. package/src/installs/bash-completion.md +833 -0
  20. package/src/installs/bash.js +399 -26
  21. package/src/installs/bash.md +993 -0
  22. package/src/installs/beyond-compare.js +585 -26
  23. package/src/installs/beyond-compare.md +813 -0
  24. package/src/installs/build-essential.js +511 -26
  25. package/src/installs/build-essential.md +977 -0
  26. package/src/installs/ca-certificates.js +618 -0
  27. package/src/installs/ca-certificates.md +937 -0
  28. package/src/installs/caffeine.js +490 -26
  29. package/src/installs/caffeine.md +839 -0
  30. package/src/installs/camtasia.js +577 -25
  31. package/src/installs/camtasia.md +762 -0
  32. package/src/installs/chatgpt.js +458 -26
  33. package/src/installs/chatgpt.md +814 -0
  34. package/src/installs/chocolatey.js +447 -0
  35. package/src/installs/chocolatey.md +661 -0
  36. package/src/installs/chrome-canary.js +472 -26
  37. package/src/installs/chrome-canary.md +641 -0
  38. package/src/installs/chromium.js +645 -26
  39. package/src/installs/chromium.md +838 -0
  40. package/src/installs/claude-code.js +558 -26
  41. package/src/installs/claude-code.md +1173 -0
  42. package/src/installs/curl.js +361 -26
  43. package/src/installs/curl.md +714 -0
  44. package/src/installs/cursor.js +561 -26
  45. package/src/installs/cursor.md +970 -0
  46. package/src/installs/dbschema.js +674 -26
  47. package/src/installs/dbschema.md +925 -0
  48. package/src/installs/dependencies.md +435 -0
  49. package/src/installs/development-tools.js +600 -0
  50. package/src/installs/development-tools.md +977 -0
  51. package/src/installs/docker.js +1010 -25
  52. package/src/installs/docker.md +1109 -0
  53. package/src/installs/drawio.js +1001 -26
  54. package/src/installs/drawio.md +795 -0
  55. package/src/installs/elmedia-player.js +328 -25
  56. package/src/installs/elmedia-player.md +556 -0
  57. package/src/installs/ffmpeg.js +870 -25
  58. package/src/installs/ffmpeg.md +852 -0
  59. package/src/installs/file.js +464 -0
  60. package/src/installs/file.md +987 -0
  61. package/src/installs/gemini-cli.js +793 -26
  62. package/src/installs/gemini-cli.md +1153 -0
  63. package/src/installs/git.js +382 -26
  64. package/src/installs/git.md +907 -0
  65. package/src/installs/gitego.js +931 -26
  66. package/src/installs/gitego.md +1172 -0
  67. package/src/installs/go.js +913 -26
  68. package/src/installs/go.md +958 -0
  69. package/src/installs/google-chrome.js +801 -25
  70. package/src/installs/google-chrome.md +862 -0
  71. package/src/installs/gpg.js +412 -73
  72. package/src/installs/gpg.md +1056 -0
  73. package/src/installs/homebrew.js +1015 -26
  74. package/src/installs/homebrew.md +988 -0
  75. package/src/installs/imageoptim.js +950 -26
  76. package/src/installs/imageoptim.md +1119 -0
  77. package/src/installs/installers.json +2297 -0
  78. package/src/installs/jq.js +382 -26
  79. package/src/installs/jq.md +809 -0
  80. package/src/installs/keyboard-maestro.js +701 -26
  81. package/src/installs/keyboard-maestro.md +825 -0
  82. package/src/installs/latex.js +771 -26
  83. package/src/installs/latex.md +1095 -0
  84. package/src/installs/lftp.js +338 -26
  85. package/src/installs/lftp.md +907 -0
  86. package/src/installs/lsb-release.js +346 -0
  87. package/src/installs/lsb-release.md +814 -0
  88. package/src/installs/messenger.js +829 -26
  89. package/src/installs/messenger.md +900 -0
  90. package/src/installs/microsoft-office.js +550 -26
  91. package/src/installs/microsoft-office.md +760 -0
  92. package/src/installs/microsoft-teams.js +782 -25
  93. package/src/installs/microsoft-teams.md +886 -0
  94. package/src/installs/node.js +886 -26
  95. package/src/installs/node.md +1153 -0
  96. package/src/installs/nordpass.js +698 -26
  97. package/src/installs/nordpass.md +921 -0
  98. package/src/installs/nvm.js +977 -26
  99. package/src/installs/nvm.md +1057 -0
  100. package/src/installs/openssh.js +734 -64
  101. package/src/installs/openssh.md +1056 -0
  102. package/src/installs/pandoc.js +644 -26
  103. package/src/installs/pandoc.md +1036 -0
  104. package/src/installs/pinentry.js +492 -26
  105. package/src/installs/pinentry.md +1142 -0
  106. package/src/installs/pngyu.js +851 -26
  107. package/src/installs/pngyu.md +896 -0
  108. package/src/installs/postman.js +781 -26
  109. package/src/installs/postman.md +940 -0
  110. package/src/installs/procps.js +425 -0
  111. package/src/installs/procps.md +851 -0
  112. package/src/installs/safari-tech-preview.js +355 -25
  113. package/src/installs/safari-tech-preview.md +533 -0
  114. package/src/installs/sfnt2woff.js +640 -26
  115. package/src/installs/sfnt2woff.md +795 -0
  116. package/src/installs/shellcheck.js +463 -26
  117. package/src/installs/shellcheck.md +1005 -0
  118. package/src/installs/slack.js +722 -25
  119. package/src/installs/slack.md +865 -0
  120. package/src/installs/snagit.js +566 -25
  121. package/src/installs/snagit.md +844 -0
  122. package/src/installs/software-properties-common.js +372 -0
  123. package/src/installs/software-properties-common.md +805 -0
  124. package/src/installs/spotify.js +858 -25
  125. package/src/installs/spotify.md +901 -0
  126. package/src/installs/studio-3t.js +803 -26
  127. package/src/installs/studio-3t.md +918 -0
  128. package/src/installs/sublime-text.js +780 -25
  129. package/src/installs/sublime-text.md +914 -0
  130. package/src/installs/superwhisper.js +687 -25
  131. package/src/installs/superwhisper.md +630 -0
  132. package/src/installs/tailscale.js +727 -26
  133. package/src/installs/tailscale.md +1100 -0
  134. package/src/installs/tar.js +389 -0
  135. package/src/installs/tar.md +946 -0
  136. package/src/installs/termius.js +780 -26
  137. package/src/installs/termius.md +844 -0
  138. package/src/installs/terraform.js +761 -26
  139. package/src/installs/terraform.md +899 -0
  140. package/src/installs/tidal.js +752 -25
  141. package/src/installs/tidal.md +864 -0
  142. package/src/installs/tmux.js +328 -26
  143. package/src/installs/tmux.md +1030 -0
  144. package/src/installs/tree.js +393 -26
  145. package/src/installs/tree.md +833 -0
  146. package/src/installs/unzip.js +460 -0
  147. package/src/installs/unzip.md +879 -0
  148. package/src/installs/vim.js +403 -26
  149. package/src/installs/vim.md +1040 -0
  150. package/src/installs/vlc.js +803 -26
  151. package/src/installs/vlc.md +927 -0
  152. package/src/installs/vscode.js +825 -26
  153. package/src/installs/vscode.md +1002 -0
  154. package/src/installs/wget.js +415 -0
  155. package/src/installs/wget.md +791 -0
  156. package/src/installs/whatsapp.js +710 -25
  157. package/src/installs/whatsapp.md +854 -0
  158. package/src/installs/winpty.js +352 -0
  159. package/src/installs/winpty.md +620 -0
  160. package/src/installs/woff2.js +535 -26
  161. package/src/installs/woff2.md +977 -0
  162. package/src/installs/wsl.js +572 -0
  163. package/src/installs/wsl.md +699 -0
  164. package/src/installs/xcode-clt.js +520 -0
  165. package/src/installs/xcode-clt.md +351 -0
  166. package/src/installs/xcode.js +542 -26
  167. package/src/installs/xcode.md +573 -0
  168. package/src/installs/yarn.js +806 -26
  169. package/src/installs/yarn.md +1074 -0
  170. package/src/installs/yq.js +636 -26
  171. package/src/installs/yq.md +944 -0
  172. package/src/installs/yt-dlp.js +683 -26
  173. package/src/installs/yt-dlp.md +946 -0
  174. package/src/installs/yum-utils.js +297 -0
  175. package/src/installs/yum-utils.md +648 -0
  176. package/src/installs/zoom.js +740 -25
  177. package/src/installs/zoom.md +884 -0
  178. package/src/scripts/README.md +567 -45
  179. package/src/scripts/STATUS.md +208 -0
  180. package/src/scripts/afk.js +395 -7
  181. package/src/scripts/backup-all.js +731 -9
  182. package/src/scripts/backup-source.js +711 -8
  183. package/src/scripts/brewd.js +373 -7
  184. package/src/scripts/brewi.js +505 -9
  185. package/src/scripts/brewr.js +512 -9
  186. package/src/scripts/brews.js +462 -9
  187. package/src/scripts/brewu.js +488 -7
  188. package/src/scripts/c.js +185 -7
  189. package/src/scripts/ccurl.js +325 -8
  190. package/src/scripts/certbot-crontab-init.js +488 -8
  191. package/src/scripts/certbot-init.js +641 -9
  192. package/src/scripts/ch.js +339 -7
  193. package/src/scripts/claude-danger.js +253 -8
  194. package/src/scripts/clean-dev.js +419 -8
  195. package/src/scripts/clear-dns-cache.js +525 -7
  196. package/src/scripts/clone.js +417 -7
  197. package/src/scripts/code-all.js +420 -7
  198. package/src/scripts/count-files.js +195 -8
  199. package/src/scripts/count-folders.js +195 -8
  200. package/src/scripts/count.js +248 -8
  201. package/src/scripts/d.js +203 -7
  202. package/src/scripts/datauri.js +373 -8
  203. package/src/scripts/delete-files.js +363 -7
  204. package/src/scripts/docker-clean.js +410 -8
  205. package/src/scripts/dp.js +426 -7
  206. package/src/scripts/e.js +375 -9
  207. package/src/scripts/empty-trash.js +497 -7
  208. package/src/scripts/evm.js +428 -9
  209. package/src/scripts/fetch-github-repos.js +441 -10
  210. package/src/scripts/get-channel.js +329 -8
  211. package/src/scripts/get-course.js +384 -11
  212. package/src/scripts/get-dependencies.js +290 -9
  213. package/src/scripts/get-folder.js +783 -10
  214. package/src/scripts/get-tunes.js +411 -10
  215. package/src/scripts/get-video.js +352 -9
  216. package/src/scripts/git-backup.js +561 -9
  217. package/src/scripts/git-clone.js +477 -9
  218. package/src/scripts/git-pup.js +303 -7
  219. package/src/scripts/git-push.js +380 -8
  220. package/src/scripts/h.js +607 -9
  221. package/src/scripts/hide-desktop-icons.js +483 -7
  222. package/src/scripts/hide-hidden-files.js +522 -7
  223. package/src/scripts/install-dependencies-from.js +440 -9
  224. package/src/scripts/ips.js +647 -10
  225. package/src/scripts/iso.js +354 -8
  226. package/src/scripts/killni.js +561 -7
  227. package/src/scripts/ll.js +451 -8
  228. package/src/scripts/local-ip.js +310 -8
  229. package/src/scripts/m.js +508 -8
  230. package/src/scripts/map.js +293 -8
  231. package/src/scripts/mkd.js +287 -7
  232. package/src/scripts/ncu-update-all.js +441 -8
  233. package/src/scripts/nginx-init.js +702 -12
  234. package/src/scripts/npmi.js +366 -7
  235. package/src/scripts/o.js +495 -8
  236. package/src/scripts/org-by-date.js +321 -7
  237. package/src/scripts/p.js +208 -7
  238. package/src/scripts/packages.js +313 -8
  239. package/src/scripts/path.js +209 -7
  240. package/src/scripts/ports.js +582 -8
  241. package/src/scripts/q.js +290 -8
  242. package/src/scripts/refresh-files.js +378 -10
  243. package/src/scripts/remove-smaller-files.js +500 -8
  244. package/src/scripts/rename-files-with-date.js +517 -9
  245. package/src/scripts/resize-image.js +523 -9
  246. package/src/scripts/rm-safe.js +653 -8
  247. package/src/scripts/s.js +525 -9
  248. package/src/scripts/set-git-public.js +349 -7
  249. package/src/scripts/show-desktop-icons.js +459 -7
  250. package/src/scripts/show-hidden-files.js +456 -7
  251. package/src/scripts/tpa.js +265 -8
  252. package/src/scripts/tpo.js +264 -7
  253. package/src/scripts/u.js +489 -7
  254. package/src/scripts/vpush.js +422 -8
  255. package/src/scripts/y.js +267 -7
  256. package/src/utils/common/os.js +94 -2
  257. package/src/utils/ubuntu/apt.js +13 -7
  258. package/src/utils/windows/choco.js +82 -26
  259. package/src/utils/windows/winget.js +89 -27
package/README.md CHANGED
@@ -60,7 +60,7 @@ dev install node
60
60
  dev install vscode
61
61
  ```
62
62
 
63
- DevUtils CLI figures out the right command for macOS, Ubuntu, Debian, Amazon Linux, Fedora, or Windows.
63
+ DevUtils CLI figures out the right command for macOS, Ubuntu, Raspberry Pi OS, Amazon Linux, Windows, or Git Bash.
64
64
 
65
65
  ### Git Identity Management
66
66
 
@@ -112,11 +112,11 @@ Copy this file to a new machine and run `dev identity sync` to regenerate all yo
112
112
  | Platform | Package Manager |
113
113
  |----------|-----------------|
114
114
  | macOS | Homebrew |
115
- | Ubuntu / Debian | APT |
116
- | Raspberry Pi OS | APT |
117
- | Amazon Linux / RHEL / Fedora | DNF / YUM |
115
+ | Ubuntu | APT / Snap |
116
+ | Raspberry Pi OS | APT / Snap |
117
+ | Amazon Linux | DNF / YUM |
118
118
  | Windows | Chocolatey / winget |
119
- | WSL | APT (with Windows integration) |
119
+ | Git Bash | Manual / Portable |
120
120
 
121
121
  ## Available Commands
122
122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fredlackey/devutils",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A globally-installable Node.js CLI toolkit for bootstrapping and configuring development environments across any machine.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -2,7 +2,17 @@
2
2
 
3
3
  /**
4
4
  * @fileoverview Install command - Platform-agnostic installation of development tools.
5
- * Locates and executes install scripts from src/installs/.
5
+ *
6
+ * This command handles the installation of development tools with automatic
7
+ * dependency resolution. It performs the following steps for each technology:
8
+ *
9
+ * 1. Check if already installed using isInstalled() - skip if true
10
+ * 2. Check if eligible for current platform using isEligible() - fail if false
11
+ * 3. Resolve dependencies from installers.json
12
+ * 4. Recursively check and install dependencies first
13
+ * 5. Install the requested technology
14
+ *
15
+ * @module commands/install
6
16
  */
7
17
 
8
18
  const { Command } = require('commander');
@@ -11,10 +21,87 @@ const path = require('path');
11
21
  const readline = require('readline');
12
22
 
13
23
  const INSTALLS_DIR = path.join(__dirname, '..', 'installs');
24
+ const INSTALLERS_JSON = path.join(INSTALLS_DIR, 'installers.json');
25
+
26
+ /**
27
+ * Cache for loaded installer modules to avoid reloading
28
+ * @type {Map<string, object>}
29
+ */
30
+ const installerCache = new Map();
31
+
32
+ /**
33
+ * Cache for installer metadata from installers.json
34
+ * @type {Array<object>|null}
35
+ */
36
+ let installersMetadata = null;
37
+
38
+ /**
39
+ * Load the installers.json metadata file
40
+ * @returns {Array<object>} Array of installer metadata objects
41
+ */
42
+ function loadInstallersMetadata() {
43
+ if (installersMetadata !== null) {
44
+ return installersMetadata;
45
+ }
46
+
47
+ try {
48
+ if (!fs.existsSync(INSTALLERS_JSON)) {
49
+ console.warn('Warning: installers.json not found. Dependency resolution disabled.');
50
+ installersMetadata = [];
51
+ return installersMetadata;
52
+ }
53
+ const content = fs.readFileSync(INSTALLERS_JSON, 'utf8');
54
+ installersMetadata = JSON.parse(content);
55
+ return installersMetadata;
56
+ } catch (err) {
57
+ console.warn(`Warning: Failed to load installers.json: ${err.message}`);
58
+ installersMetadata = [];
59
+ return installersMetadata;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get metadata for a specific installer by filename
65
+ * @param {string} filename - The installer filename (e.g., "node.js")
66
+ * @returns {object|null} The installer metadata or null if not found
67
+ */
68
+ function getInstallerMetadata(filename) {
69
+ const metadata = loadInstallersMetadata();
70
+ return metadata.find(m => m.filename === filename) || null;
71
+ }
72
+
73
+ /**
74
+ * Load an installer module by name
75
+ * @param {string} name - Name of the installer (without .js extension)
76
+ * @returns {object|null} The installer module or null if not found
77
+ */
78
+ function loadInstaller(name) {
79
+ const filename = `${name}.js`;
80
+
81
+ // Check cache first
82
+ if (installerCache.has(filename)) {
83
+ return installerCache.get(filename);
84
+ }
85
+
86
+ const scriptPath = path.join(INSTALLS_DIR, filename);
87
+
88
+ if (!fs.existsSync(scriptPath)) {
89
+ return null;
90
+ }
91
+
92
+ try {
93
+ const installer = require(scriptPath);
94
+ installerCache.set(filename, installer);
95
+ return installer;
96
+ } catch (err) {
97
+ console.error(`Error loading installer ${name}: ${err.message}`);
98
+ return null;
99
+ }
100
+ }
14
101
 
15
102
  /**
16
103
  * Get list of available install scripts
17
- * @returns {string[]} Array of install script names
104
+ * @returns {string[]} Array of install script names (without .js extension)
18
105
  */
19
106
  function getAvailableInstalls() {
20
107
  try {
@@ -48,6 +135,194 @@ function confirm(question) {
48
135
  });
49
136
  }
50
137
 
138
+ /**
139
+ * Check if a technology is installed
140
+ * @param {string} name - Name of the technology (without .js extension)
141
+ * @returns {Promise<boolean>} True if installed, false otherwise
142
+ */
143
+ async function checkIsInstalled(name) {
144
+ const installer = loadInstaller(name);
145
+
146
+ if (!installer) {
147
+ return false;
148
+ }
149
+
150
+ // Check if installer has isInstalled function
151
+ if (typeof installer.isInstalled !== 'function') {
152
+ // If no isInstalled function, assume not installed (will try to install)
153
+ return false;
154
+ }
155
+
156
+ try {
157
+ // isInstalled may be sync or async
158
+ const result = installer.isInstalled();
159
+ if (result instanceof Promise) {
160
+ return await result;
161
+ }
162
+ return result;
163
+ } catch (err) {
164
+ // If check fails, assume not installed
165
+ return false;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Check if a technology is eligible for the current platform
171
+ * @param {string} name - Name of the technology (without .js extension)
172
+ * @returns {boolean} True if eligible, false otherwise
173
+ */
174
+ function checkIsEligible(name) {
175
+ const installer = loadInstaller(name);
176
+
177
+ if (!installer) {
178
+ return false;
179
+ }
180
+
181
+ // Check if installer has isEligible function
182
+ if (typeof installer.isEligible !== 'function') {
183
+ // If no isEligible function, assume eligible (backwards compatibility)
184
+ return true;
185
+ }
186
+
187
+ try {
188
+ return installer.isEligible();
189
+ } catch (err) {
190
+ // If check fails, assume not eligible
191
+ return false;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Get the display name for a technology from metadata
197
+ * @param {string} filename - The installer filename
198
+ * @returns {string} The display name or the filename without extension
199
+ */
200
+ function getDisplayName(filename) {
201
+ const metadata = getInstallerMetadata(filename);
202
+ if (metadata && metadata.name) {
203
+ return metadata.name;
204
+ }
205
+ return filename.replace('.js', '');
206
+ }
207
+
208
+ /**
209
+ * Resolve dependencies for a technology, filtering by eligibility
210
+ *
211
+ * This function recursively resolves all dependencies for a given technology,
212
+ * returning them in the correct installation order (dependencies first).
213
+ *
214
+ * @param {string} name - Name of the technology (without .js extension)
215
+ * @param {Set<string>} visited - Set of already visited technologies (for cycle detection)
216
+ * @param {Set<string>} installing - Set of technologies currently being resolved (for cycle detection)
217
+ * @param {object} options - Command options
218
+ * @returns {Promise<Array<{name: string, displayName: string}>>} Array of technologies to install in order
219
+ */
220
+ async function resolveDependencies(name, visited = new Set(), installing = new Set(), options = {}) {
221
+ const filename = `${name}.js`;
222
+ const result = [];
223
+
224
+ // Detect circular dependencies
225
+ if (installing.has(filename)) {
226
+ if (options.verbose) {
227
+ console.log(` [Skipping circular dependency: ${name}]`);
228
+ }
229
+ return result;
230
+ }
231
+
232
+ // Skip if already processed
233
+ if (visited.has(filename)) {
234
+ return result;
235
+ }
236
+
237
+ // Mark as being processed
238
+ installing.add(filename);
239
+
240
+ // Get metadata for this installer
241
+ const metadata = getInstallerMetadata(filename);
242
+ const dependencies = metadata?.depends_on || [];
243
+
244
+ // Sort dependencies by priority (lower priority = install first)
245
+ const sortedDeps = [...dependencies].sort((a, b) => (a.priority || 0) - (b.priority || 0));
246
+
247
+ // Process each dependency
248
+ for (const dep of sortedDeps) {
249
+ const depName = dep.name.replace('.js', '');
250
+
251
+ // Check if dependency is eligible for this platform
252
+ if (!checkIsEligible(depName)) {
253
+ if (options.verbose) {
254
+ console.log(` [Skipping ineligible dependency: ${depName}]`);
255
+ }
256
+ continue;
257
+ }
258
+
259
+ // Check if dependency is already installed
260
+ const isInstalled = await checkIsInstalled(depName);
261
+ if (isInstalled) {
262
+ if (options.verbose) {
263
+ console.log(` [Dependency already installed: ${depName}]`);
264
+ }
265
+ visited.add(dep.name);
266
+ continue;
267
+ }
268
+
269
+ // Recursively resolve this dependency's dependencies
270
+ const subDeps = await resolveDependencies(depName, visited, installing, options);
271
+ result.push(...subDeps);
272
+
273
+ // Add this dependency if not already visited
274
+ if (!visited.has(dep.name)) {
275
+ result.push({
276
+ name: depName,
277
+ displayName: getDisplayName(dep.name)
278
+ });
279
+ visited.add(dep.name);
280
+ }
281
+ }
282
+
283
+ // Mark as processed (no longer being resolved)
284
+ installing.delete(filename);
285
+
286
+ return result;
287
+ }
288
+
289
+ /**
290
+ * Install a single technology
291
+ * @param {string} name - Name of the technology to install
292
+ * @param {object} options - Command options
293
+ * @returns {Promise<boolean>} True if installation succeeded
294
+ */
295
+ async function installSingle(name, options) {
296
+ const installer = loadInstaller(name);
297
+
298
+ if (!installer) {
299
+ console.error(`Error: Installer for "${name}" not found.`);
300
+ return false;
301
+ }
302
+
303
+ if (typeof installer.install !== 'function') {
304
+ console.error(`Error: Installer "${name}" does not export an install() function.`);
305
+ return false;
306
+ }
307
+
308
+ try {
309
+ const installOptions = {
310
+ dryRun: options.dryRun || false,
311
+ verbose: options.verbose || false,
312
+ force: options.force || false
313
+ };
314
+
315
+ await installer.install(installOptions);
316
+ return true;
317
+ } catch (err) {
318
+ console.error(`Error installing ${name}: ${err.message}`);
319
+ if (options.verbose) {
320
+ console.error(err.stack);
321
+ }
322
+ return false;
323
+ }
324
+ }
325
+
51
326
  /**
52
327
  * List all available install scripts
53
328
  */
@@ -64,7 +339,11 @@ function listInstalls() {
64
339
  console.log('─'.repeat(40));
65
340
 
66
341
  for (const name of installs) {
67
- console.log(` ${name}`);
342
+ const filename = `${name}.js`;
343
+ const displayName = getDisplayName(filename);
344
+ const eligible = checkIsEligible(name);
345
+ const eligibleMark = eligible ? '' : ' (not available on this platform)';
346
+ console.log(` ${name}${eligibleMark}`);
68
347
  }
69
348
 
70
349
  console.log(`\nUsage: dev install <name>`);
@@ -72,7 +351,7 @@ function listInstalls() {
72
351
  }
73
352
 
74
353
  /**
75
- * Run an install script
354
+ * Run an install with dependency resolution
76
355
  * @param {string} name - Name of the install script
77
356
  * @param {object} options - Command options
78
357
  */
@@ -85,6 +364,7 @@ async function runInstall(name, options) {
85
364
  }
86
365
 
87
366
  const scriptPath = path.join(INSTALLS_DIR, `${name}.js`);
367
+ const filename = `${name}.js`;
88
368
 
89
369
  // Check if install script exists
90
370
  if (!fs.existsSync(scriptPath)) {
@@ -93,14 +373,65 @@ async function runInstall(name, options) {
93
373
  process.exit(1);
94
374
  }
95
375
 
96
- // Confirm installation (unless --force)
97
- if (!options.force) {
98
- console.log(`\nPreparing to install: ${name}`);
376
+ // Get display name for output
377
+ const displayName = getDisplayName(filename);
378
+
379
+ console.log(`\nChecking ${displayName}...`);
380
+
381
+ // Step 1: Check if already installed
382
+ const isInstalled = await checkIsInstalled(name);
383
+ if (isInstalled) {
384
+ console.log(`${displayName} is already installed.`);
385
+ return;
386
+ }
387
+
388
+ // Step 2: Check if eligible for this platform
389
+ const isEligible = checkIsEligible(name);
390
+ if (!isEligible) {
391
+ console.log(`${displayName} is not available for this platform.`);
392
+ return;
393
+ }
394
+
395
+ // Step 3: Resolve dependencies
396
+ if (options.verbose) {
397
+ console.log(`Resolving dependencies for ${displayName}...`);
398
+ }
399
+
400
+ const dependencies = await resolveDependencies(name, new Set(), new Set(), options);
401
+
402
+ // Build the full installation list (dependencies + target)
403
+ const toInstall = [
404
+ ...dependencies,
405
+ { name, displayName }
406
+ ];
99
407
 
100
- if (options.dryRun) {
101
- console.log('[Dry run mode - no changes will be made]\n');
408
+ // Remove duplicates while preserving order
409
+ const seen = new Set();
410
+ const uniqueToInstall = toInstall.filter(item => {
411
+ if (seen.has(item.name)) {
412
+ return false;
102
413
  }
414
+ seen.add(item.name);
415
+ return true;
416
+ });
417
+
418
+ // Show what will be installed
419
+ if (uniqueToInstall.length > 1) {
420
+ console.log(`\nThe following will be installed:`);
421
+ for (const item of uniqueToInstall) {
422
+ console.log(` - ${item.displayName}`);
423
+ }
424
+ console.log('');
425
+ } else {
426
+ console.log(`\nPreparing to install: ${displayName}`);
427
+ }
103
428
 
429
+ if (options.dryRun) {
430
+ console.log('[Dry run mode - no changes will be made]\n');
431
+ }
432
+
433
+ // Confirm installation (unless --force)
434
+ if (!options.force) {
104
435
  const shouldProceed = await confirm('Proceed with installation?');
105
436
  if (!shouldProceed) {
106
437
  console.log('Installation cancelled.');
@@ -108,42 +439,49 @@ async function runInstall(name, options) {
108
439
  }
109
440
  }
110
441
 
111
- try {
112
- // Load the install script
113
- const installScript = require(scriptPath);
442
+ // Install each technology in order
443
+ let successCount = 0;
444
+ let failCount = 0;
114
445
 
115
- // Check for install function
116
- if (typeof installScript.install !== 'function') {
117
- console.error(`\nError: Install script "${name}" does not export an install() function.`);
118
- process.exit(1);
119
- }
446
+ for (const item of uniqueToInstall) {
447
+ console.log(`\n${''.repeat(50)}`);
448
+ console.log(`Installing ${item.displayName}...`);
449
+ console.log('─'.repeat(50));
120
450
 
121
- console.log(`\nInstalling ${name}...`);
122
451
  if (options.verbose) {
123
- console.log(`Script: ${scriptPath}`);
452
+ console.log(`Script: ${path.join(INSTALLS_DIR, `${item.name}.js`)}`);
124
453
  }
125
454
 
126
- // Pass options to the install function
127
- const installOptions = {
128
- dryRun: options.dryRun || false,
129
- verbose: options.verbose || false,
130
- force: options.force || false
131
- };
455
+ const success = await installSingle(item.name, options);
132
456
 
133
- // Run the install
134
- await installScript.install(installOptions);
457
+ if (success) {
458
+ successCount++;
459
+ if (!options.dryRun) {
460
+ console.log(`${item.displayName} installed successfully.`);
461
+ }
462
+ } else {
463
+ failCount++;
464
+ console.error(`Failed to install ${item.displayName}.`);
135
465
 
136
- if (!options.dryRun) {
137
- console.log(`\n${name} installation complete.`);
466
+ // Ask if user wants to continue on failure (unless --force)
467
+ if (!options.force && uniqueToInstall.indexOf(item) < uniqueToInstall.length - 1) {
468
+ const shouldContinue = await confirm('Continue with remaining installations?');
469
+ if (!shouldContinue) {
470
+ console.log('Installation cancelled.');
471
+ break;
472
+ }
473
+ }
138
474
  }
475
+ }
139
476
 
140
- } catch (err) {
141
- console.error(`\nError installing ${name}:`, err.message);
142
- if (options.verbose) {
143
- console.error(err.stack);
144
- }
145
- process.exit(1);
477
+ // Summary
478
+ console.log(`\n${'─'.repeat(50)}`);
479
+ console.log('Installation Summary:');
480
+ console.log(` Successful: ${successCount}`);
481
+ if (failCount > 0) {
482
+ console.log(` Failed: ${failCount}`);
146
483
  }
484
+ console.log('');
147
485
  }
148
486
 
149
487
  /**
@@ -162,7 +500,7 @@ async function handleInstall(name, options) {
162
500
 
163
501
  // Create and configure the command
164
502
  const install = new Command('install')
165
- .description('Platform-agnostic installation of development tools')
503
+ .description('Platform-agnostic installation of development tools with automatic dependency resolution')
166
504
  .argument('[name]', 'Name of the package to install')
167
505
  .option('--list', 'List all available install scripts')
168
506
  .option('--dry-run', 'Show what would be executed without running')