@fredlackey/devutils 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fredlackey/devutils",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
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": {
@@ -11,8 +11,8 @@ const readline = require('readline');
11
11
  const shell = require('../utils/common/shell');
12
12
  const osUtils = require('../utils/common/os');
13
13
 
14
- // Essential tools that DevUtils CLI requires
15
- const ESSENTIAL_TOOLS = [
14
+ // Essential tools that DevUtils CLI requires (shared across all platforms)
15
+ const CORE_TOOLS = [
16
16
  {
17
17
  name: 'git',
18
18
  command: 'git',
@@ -39,6 +39,31 @@ const ESSENTIAL_TOOLS = [
39
39
  }
40
40
  ];
41
41
 
42
+ // Homebrew is required on macOS before installing other tools
43
+ const HOMEBREW_TOOL = {
44
+ name: 'brew',
45
+ command: 'brew',
46
+ description: 'Package manager (required)',
47
+ install: 'homebrew'
48
+ };
49
+
50
+ /**
51
+ * Get the list of essential tools for the current platform.
52
+ * On macOS, Homebrew is included first since other tools depend on it.
53
+ * @returns {Array<object>} Array of tool definitions
54
+ */
55
+ function getEssentialTools() {
56
+ const platform = osUtils.detect();
57
+
58
+ // On macOS, Homebrew must be installed first since all other tools use it
59
+ if (platform.type === 'macos') {
60
+ return [HOMEBREW_TOOL, ...CORE_TOOLS];
61
+ }
62
+
63
+ // On other platforms, just use the core tools
64
+ return CORE_TOOLS;
65
+ }
66
+
42
67
  /**
43
68
  * Create readline interface for prompts
44
69
  * @returns {readline.Interface}
@@ -69,8 +94,9 @@ function confirm(rl, question) {
69
94
  * @returns {Array<object>} Array of missing tools
70
95
  */
71
96
  function checkMissingTools() {
97
+ const tools = getEssentialTools();
72
98
  const missing = [];
73
- for (const tool of ESSENTIAL_TOOLS) {
99
+ for (const tool of tools) {
74
100
  if (!shell.commandExists(tool.command)) {
75
101
  missing.push(tool);
76
102
  }
@@ -83,7 +109,8 @@ function checkMissingTools() {
83
109
  * @returns {Array<{ tool: object, installed: boolean }>}
84
110
  */
85
111
  function getToolStatuses() {
86
- return ESSENTIAL_TOOLS.map(tool => ({
112
+ const tools = getEssentialTools();
113
+ return tools.map(tool => ({
87
114
  tool,
88
115
  installed: shell.commandExists(tool.command)
89
116
  }));
@@ -97,11 +124,18 @@ function getToolStatuses() {
97
124
  async function installTool(tool) {
98
125
  try {
99
126
  const installer = require(`../installs/${tool.install}`);
100
- if (typeof installer.install === 'function') {
101
- return await installer.install();
127
+ if (typeof installer.install !== 'function') {
128
+ console.error(` No install function found for ${tool.name}`);
129
+ return false;
102
130
  }
103
- console.error(` No install function found for ${tool.name}`);
104
- return false;
131
+
132
+ // Run the installer
133
+ await installer.install();
134
+
135
+ // Verify installation by checking if the command now exists
136
+ // This is more reliable than trusting return values from installers
137
+ const isNowInstalled = shell.commandExists(tool.command);
138
+ return isNowInstalled;
105
139
  } catch (err) {
106
140
  console.error(` Failed to install ${tool.name}: ${err.message}`);
107
141
  return false;
@@ -277,21 +277,20 @@ async function install_macos() {
277
277
  console.log(' - Xcode Command Line Tools (if not already installed)');
278
278
  console.log('');
279
279
 
280
- // Run the official Homebrew installer in non-interactive mode
281
- // NONINTERACTIVE=1 prevents prompts for confirmation
280
+ // Run the official Homebrew installer with interactive terminal support
281
+ // This allows the installer to prompt for sudo password when needed
282
282
  console.log('Downloading and running the Homebrew installer...');
283
283
  console.log('This may take several minutes...');
284
+ console.log('You may be prompted for your password to complete the installation.');
284
285
  console.log('');
285
286
 
286
- const installResult = await shell.exec(
287
- `NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`,
288
- { timeout: 600000 } // 10 minute timeout for slow connections
287
+ const exitCode = await shell.spawnInteractive(
288
+ `/bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`
289
289
  );
290
290
 
291
- if (installResult.code !== 0) {
291
+ if (exitCode !== 0) {
292
292
  throw new Error(
293
- `Failed to install Homebrew.\n` +
294
- `Output: ${installResult.stderr || installResult.stdout}\n\n` +
293
+ `Failed to install Homebrew (exit code: ${exitCode}).\n\n` +
295
294
  `Troubleshooting:\n` +
296
295
  ` 1. If Xcode Command Line Tools installation hung, run:\n` +
297
296
  ` xcode-select --install\n` +
@@ -419,18 +418,17 @@ async function install_ubuntu() {
419
418
  console.log('');
420
419
  console.log('Downloading and running the Homebrew installer...');
421
420
  console.log('This may take several minutes...');
421
+ console.log('You may be prompted for your password to complete the installation.');
422
422
  console.log('');
423
423
 
424
- // Run the official Homebrew installer in non-interactive mode
425
- const installResult = await shell.exec(
426
- `NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`,
427
- { timeout: 600000 } // 10 minute timeout
424
+ // Run the official Homebrew installer with interactive terminal support
425
+ const exitCode = await shell.spawnInteractive(
426
+ `/bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`
428
427
  );
429
428
 
430
- if (installResult.code !== 0) {
429
+ if (exitCode !== 0) {
431
430
  throw new Error(
432
- `Failed to install Homebrew.\n` +
433
- `Output: ${installResult.stderr || installResult.stdout}\n\n` +
431
+ `Failed to install Homebrew (exit code: ${exitCode}).\n\n` +
434
432
  `Troubleshooting:\n` +
435
433
  ` 1. Ensure all dependencies are installed:\n` +
436
434
  ` sudo apt-get install -y build-essential procps curl file git\n` +
@@ -548,18 +546,17 @@ async function install_raspbian() {
548
546
  console.log('');
549
547
  console.log('Downloading and running the Homebrew installer...');
550
548
  console.log('This may take several minutes (or longer on slower hardware)...');
549
+ console.log('You may be prompted for your password to complete the installation.');
551
550
  console.log('');
552
551
 
553
- // Run the official Homebrew installer in non-interactive mode
554
- const installResult = await shell.exec(
555
- `NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`,
556
- { timeout: 1200000 } // 20 minute timeout for slower Raspberry Pi hardware
552
+ // Run the official Homebrew installer with interactive terminal support
553
+ const exitCode = await shell.spawnInteractive(
554
+ `/bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`
557
555
  );
558
556
 
559
- if (installResult.code !== 0) {
557
+ if (exitCode !== 0) {
560
558
  throw new Error(
561
- `Failed to install Homebrew.\n` +
562
- `Output: ${installResult.stderr || installResult.stdout}\n\n` +
559
+ `Failed to install Homebrew (exit code: ${exitCode}).\n\n` +
563
560
  `Troubleshooting:\n` +
564
561
  ` 1. Ensure all dependencies are installed:\n` +
565
562
  ` sudo apt-get install -y build-essential procps curl file git\n` +
@@ -706,18 +703,17 @@ async function install_amazon_linux() {
706
703
  console.log('');
707
704
  console.log('Downloading and running the Homebrew installer...');
708
705
  console.log('This may take several minutes...');
706
+ console.log('You may be prompted for your password to complete the installation.');
709
707
  console.log('');
710
708
 
711
- // Run the official Homebrew installer in non-interactive mode
712
- const installResult = await shell.exec(
713
- `NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`,
714
- { timeout: 600000 } // 10 minute timeout
709
+ // Run the official Homebrew installer with interactive terminal support
710
+ const exitCode = await shell.spawnInteractive(
711
+ `/bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`
715
712
  );
716
713
 
717
- if (installResult.code !== 0) {
714
+ if (exitCode !== 0) {
718
715
  throw new Error(
719
- `Failed to install Homebrew.\n` +
720
- `Output: ${installResult.stderr || installResult.stdout}\n\n` +
716
+ `Failed to install Homebrew (exit code: ${exitCode}).\n\n` +
721
717
  `Troubleshooting:\n` +
722
718
  ` 1. Ensure Development Tools are installed:\n` +
723
719
  ` sudo dnf groupinstall -y "Development Tools" # Amazon Linux 2023\n` +
@@ -835,18 +831,17 @@ async function install_ubuntu_wsl() {
835
831
  console.log('');
836
832
  console.log('Downloading and running the Homebrew installer...');
837
833
  console.log('This may take several minutes...');
834
+ console.log('You may be prompted for your password to complete the installation.');
838
835
  console.log('');
839
836
 
840
- // Run the official Homebrew installer in non-interactive mode
841
- const installResult = await shell.exec(
842
- `NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`,
843
- { timeout: 600000 } // 10 minute timeout
837
+ // Run the official Homebrew installer with interactive terminal support
838
+ const exitCode = await shell.spawnInteractive(
839
+ `/bin/bash -c "$(curl -fsSL ${HOMEBREW_INSTALL_URL})"`
844
840
  );
845
841
 
846
- if (installResult.code !== 0) {
842
+ if (exitCode !== 0) {
847
843
  throw new Error(
848
- `Failed to install Homebrew in WSL.\n` +
849
- `Output: ${installResult.stderr || installResult.stdout}\n\n` +
844
+ `Failed to install Homebrew in WSL (exit code: ${exitCode}).\n\n` +
850
845
  `Troubleshooting:\n` +
851
846
  ` 1. Ensure WSL 2 is being used (WSL 1 has known issues):\n` +
852
847
  ` Run in PowerShell: wsl --set-version Ubuntu 2\n` +
@@ -207,10 +207,55 @@ async function spawnAsync(command, args = [], options = {}) {
207
207
  });
208
208
  }
209
209
 
210
+ /**
211
+ * Spawns an interactive command with terminal passthrough.
212
+ *
213
+ * Use this function for commands that need user interaction, such as:
214
+ * - Commands that prompt for sudo password
215
+ * - Interactive installers that ask questions
216
+ * - Commands that show progress with cursor movement
217
+ *
218
+ * Unlike exec() or spawnAsync(), this function connects the child process
219
+ * directly to the parent's terminal (stdin, stdout, stderr), allowing the
220
+ * user to interact with the command as if they ran it directly.
221
+ *
222
+ * @param {string} command - The command to run (passed to shell)
223
+ * @param {Object} [options] - Options
224
+ * @param {string} [options.cwd] - Working directory for the command
225
+ * @param {Object} [options.env] - Environment variables (defaults to process.env)
226
+ * @returns {Promise<number>} Exit code (0 = success, non-zero = failure)
227
+ *
228
+ * @example
229
+ * // Run an installer that needs sudo password
230
+ * const exitCode = await spawnInteractive('/bin/bash -c "$(curl -fsSL https://example.com/install.sh)"');
231
+ * if (exitCode !== 0) {
232
+ * console.error('Installation failed');
233
+ * }
234
+ */
235
+ async function spawnInteractive(command, options = {}) {
236
+ return new Promise((resolve) => {
237
+ const child = spawn(command, [], {
238
+ stdio: 'inherit', // Connect to parent's terminal
239
+ shell: true, // Run through shell to support complex commands
240
+ cwd: options.cwd,
241
+ env: options.env || process.env
242
+ });
243
+
244
+ child.on('close', (code) => {
245
+ resolve(code || 0);
246
+ });
247
+
248
+ child.on('error', () => {
249
+ resolve(1);
250
+ });
251
+ });
252
+ }
253
+
210
254
  module.exports = {
211
255
  exec,
212
256
  execSync,
213
257
  which,
214
258
  commandExists,
215
- spawnAsync
259
+ spawnAsync,
260
+ spawnInteractive
216
261
  };