Skip to content

Commit a71c058

Browse files
Lots
1 parent ed1da4d commit a71c058

8 files changed

Lines changed: 301 additions & 125 deletions

File tree

briefing_support_dirs.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Changes to Support Directory Removal
2+
3+
- **Interactive Removal**: Updated `completeAppUninstall` in `lib/system.js` to prompt the user before removing each support directory (prompts for `Application Support`, `Caches`, `Preferences`, etc.).
4+
- **Confirmation Logic**: Uses the existing `confirm` utility. Defaults to "y".
5+
- **Yes Flag Support**: `completeAppUninstall` now accepts a `yesFlag` argument. If `yesFlag` (from `--yes`) is true, these prompts are automatically confirmed.
6+
- **Caller Update**: Updated `lib/updater.js` to pass the `yesFlag` from `performUninstall` down to `completeAppUninstall`.
7+
8+
## Technical Details
9+
10+
- Modified `lib/system.js`:
11+
- Imported `confirm` from `./utils`.
12+
- Refactored `completeAppUninstall` loop for `supportDirs`.
13+
- Restored accidentally clobbered functions `findLaunchAgents` and `unloadLaunchAgent`.
14+
- Modified `lib/updater.js`:
15+
- Updated call site for `completeAppUninstall`.

lib/config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const createInstallationRecord = (source, selected, metadata = {}) => {
8080
url: source.url,
8181
owner: source.owner,
8282
repo: source.repo,
83-
originalArgs: source.originalArgs,
83+
originalArgs: Array.isArray(source.originalArgs) ? source.originalArgs : [source.originalArgs],
8484
},
8585
selected: {
8686
name: selected.name,
@@ -95,6 +95,7 @@ const createInstallationRecord = (source, selected, metadata = {}) => {
9595
destinations: metadata.destinations || [],
9696
preferredMethod: metadata.preferredMethod || metadata.installMethod,
9797
selectedAssetPattern: metadata.selectedAssetPattern || null,
98+
script: metadata.script || null,
9899
},
99100
version: metadata.version,
100101
commit: metadata.commit,

lib/installer-scripts.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,19 @@ const getInstallerScriptConfig = (extension) => {
102102

103103
/**
104104
* Score an installer script based on filename relevance to current platform
105-
* @param {string} filename - Full filename of the script
105+
* @param {string} filePath - Path to the script (relative or absolute)
106106
* @param {string} extension - File extension
107107
* @returns {number} Score (higher is better match)
108108
*/
109-
const scoreInstallerScript = (filename, extension) => {
109+
const scoreInstallerScript = (filePath, extension) => {
110110
if (!isInstallerScriptCompatible(extension)) return -1
111111

112112
const config = INSTALLER_SCRIPT_CONFIG[extension.toLowerCase()]
113113
let score = config.priority
114114

115+
const filename = path.basename(filePath);
115116
const lowerFilename = filename.toLowerCase()
117+
const lowerPath = filePath.toLowerCase()
116118
const platform = process.platform
117119

118120
const platformNameMatches = {
@@ -139,6 +141,24 @@ const scoreInstallerScript = (filename, extension) => {
139141
}
140142
}
141143

144+
// Penalize completion scripts heavily
145+
// Check directory path for 'completion' or 'completions'
146+
const dirname = path.dirname(filePath).toLowerCase();
147+
148+
if (
149+
dirname.includes('completion') ||
150+
dirname.includes('completions') ||
151+
dirname.includes('examples') ||
152+
dirname.includes('samples') ||
153+
dirname.includes('test') ||
154+
dirname.includes('t/') || // Common test directory
155+
lowerFilename.includes('completion') ||
156+
lowerFilename.startsWith('_') || // Common zsh completion prefix
157+
lowerFilename.includes('.min.') // Minified files are rarely installers
158+
) {
159+
return -1; // Exclude completely
160+
}
161+
142162
for (const pattern of config.patterns) {
143163
if (lowerFilename.includes(pattern)) {
144164
score += 3
@@ -164,7 +184,7 @@ const detectInstallerScripts = (files, basePath = '') => {
164184
if (!isInstallerScript(ext)) continue
165185
if (!isInstallerScriptCompatible(ext)) continue
166186

167-
const score = scoreInstallerScript(filename, ext)
187+
const score = scoreInstallerScript(file, ext)
168188
if (score < 0) continue
169189

170190
scripts.push({

lib/installer.js

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,6 @@ const displayScriptList = (installScripts, log) => {
377377

378378
log.log(` ${continueOption}. Continue to regular installation`);
379379
log.log(` ${exitOption}. Exit installation`);
380-
log.log(` e. Edit selected script before running`);
381380
};
382381

383382
const handleSingleScript = async (script, log, yesFlag = false) => {
@@ -431,51 +430,54 @@ const handleMultipleScripts = async (installScripts, log, yesFlag = false) => {
431430
});
432431

433432
const choice = await new Promise((resolve) => {
434-
rli.question(`${colors.fg.yellow}Preview install script (1-${EXIT_OPTION}) or 'e' to edit:${colors.reset} `, (ans) => {
433+
rli.question(`${colors.fg.yellow}Select install script (1-${EXIT_OPTION}):${colors.reset} `, (ans) => {
435434
rli.close();
436435
resolve(ans.trim().toLowerCase());
437436
});
438437
});
439438

440-
if (choice === 'e') {
441-
const scriptChoice = await promptChoice(
442-
`Which script would you like to edit? (1-${installScripts.length}): `,
443-
installScripts.length,
444-
yesFlag
445-
);
446-
const scriptToEdit = installScripts[scriptChoice - 1];
447-
const editedScript = await editScriptInEditor(scriptToEdit);
448-
displayScriptPreview(editedScript, log);
449-
450-
const shouldRun = await confirm(
451-
"Run this edited install script (y) or choose a different one (n)?",
452-
"y",
453-
yesFlag
454-
);
455-
456-
if (shouldRun) return editedScript;
457-
// Continue loop if user chooses 'n'
458-
} else {
459-
const numericChoice = parseInt(choice);
460-
if (numericChoice <= installScripts.length) {
461-
const script = installScripts[numericChoice - 1];
462-
displayScriptPreview(script, log);
439+
const numericChoice = parseInt(choice);
463440

464-
const shouldRun = await confirm(
465-
"Run this install script (y) or choose a different one (n)?",
466-
"y",
467-
yesFlag
468-
);
441+
if (isNaN(numericChoice)) {
442+
console.log(`${colors.fg.red}Invalid choice. Please enter a number.${colors.reset}`);
443+
continue;
444+
}
469445

470-
if (shouldRun) return script;
471-
// Continue loop if user chooses 'n'
472-
} else if (numericChoice === CONTINUE_OPTION) {
473-
return null; // Continue to regular installation
474-
} else if (numericChoice === EXIT_OPTION) {
475-
throw new Error("Installation aborted by user");
476-
} else {
477-
console.log(`${colors.fg.red}Invalid choice. Please try again.${colors.reset}`);
446+
if (numericChoice === CONTINUE_OPTION) {
447+
return null; // Continue to regular installation
448+
} else if (numericChoice === EXIT_OPTION) {
449+
throw new Error("Installation aborted by user");
450+
} else if (numericChoice > 0 && numericChoice <= installScripts.length) {
451+
let script = installScripts[numericChoice - 1];
452+
displayScriptPreview(script, log);
453+
454+
let actionLoop = true;
455+
while (actionLoop) {
456+
const rliAction = readline.createInterface({
457+
input: process.stdin,
458+
output: process.stdout,
459+
});
460+
461+
const action = await new Promise((resolve) => {
462+
rliAction.question(`${colors.fg.yellow}Run (y), Edit (e), or Select Different (n)?${colors.reset} `, (ans) => {
463+
rliAction.close();
464+
resolve(ans.trim().toLowerCase());
465+
});
466+
});
467+
468+
if (action === 'y' || action === 'yes') {
469+
return script;
470+
} else if (action === 'n' || action === 'no') {
471+
actionLoop = false;
472+
} else if (action === 'e') {
473+
script = await editScriptInEditor(script);
474+
displayScriptPreview(script, log);
475+
} else {
476+
// Optional: Handle invalid input if needed, strictly asking for y/e/n
477+
}
478478
}
479+
} else {
480+
console.log(`${colors.fg.red}Invalid choice. Please try again.${colors.reset}`);
479481
}
480482
}
481483
};
@@ -506,6 +508,7 @@ const handleGitHubSource = async (source, platformInfo, capabilities, log, yesFl
506508
const previousInstallation = getInstallation(source.repo);
507509
const preferredMethod = previousInstallation?.installation?.preferredMethod;
508510
const previousAssetPattern = previousInstallation?.installation?.selectedAssetPattern;
511+
const storedScript = previousInstallation?.installation?.script;
509512

510513
if (isUpdate && preferredMethod) {
511514
log.debug(`Previous installation used method: ${preferredMethod}`);
@@ -538,21 +541,36 @@ const handleGitHubSource = async (source, platformInfo, capabilities, log, yesFl
538541
}
539542

540543
const selected = selectBestAsset(assets, platformInfo, capabilities);
541-
const binaryScore = selected ? (selected.points || 10) : 0;
544+
// Boost binary score significantly if we found a compatible binary
545+
// This ensures we prioritize direct binary downloads over install scripts unless strict overrides exist
546+
const binaryScore = selected ? (selected.points || 10) + 100 : 0;
542547

543548
const skipScriptDetection = isUpdate && preferredMethod &&
544549
!["script", "installer_script"].includes(preferredMethod);
545550

546551
let installScripts = [];
547-
if (!skipScriptDetection) {
552+
if (isUpdate && preferredMethod === "script" && storedScript) {
553+
log.debug("Using stored install script from previous installation");
554+
installScripts = [
555+
{
556+
code: storedScript,
557+
source: "stored script",
558+
score: 1000,
559+
},
560+
];
561+
} else if (!skipScriptDetection) {
548562
installScripts = await findInstallScripts(
549563
source.owner,
550564
source.repo,
551565
body,
552566
);
553-
log.debug(`Found ${installScripts.length} platform-compatible install scripts`);
567+
log.debug(
568+
`Found ${installScripts.length} platform-compatible install scripts`,
569+
);
554570
} else {
555-
log.debug(`Skipping script detection - previous install used: ${preferredMethod}`);
571+
log.debug(
572+
`Skipping script detection - previous install used: ${preferredMethod}`,
573+
);
556574
}
557575

558576
const installerScript = assets.find((i) => i.name.includes("installer.sh"));
@@ -608,6 +626,7 @@ const handleGitHubSource = async (source, platformInfo, capabilities, log, yesFl
608626
const selectedScript = await selectInstallScript(installScripts, log, yesFlag);
609627

610628
if (selectedScript) {
629+
const processedCode = processInstallSnippetReplacements(selectedScript.code);
611630
executeInstallScript(selectedScript, log);
612631

613632
const installRecord = createInstallationRecord(
@@ -617,6 +636,7 @@ const handleGitHubSource = async (source, platformInfo, capabilities, log, yesFl
617636
name: source.repo,
618637
installMethod: "script",
619638
preferredMethod: "script",
639+
script: processedCode,
620640
version: tag,
621641
commit: commit,
622642
prerelease: prerelease,

lib/installers.js

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,13 @@ const getBinaries = (dir) => {
312312
return [...new Set(binaries)]; // Remove duplicates
313313
};
314314

315+
/**
316+
* Prompt user to choose from multiple binaries found in a package
317+
* @param {string[]} binaries - List of binary files found
318+
* @param {string} packageName - Name of the package being installed
319+
* @param {Object} logger - Logger instance
320+
* @returns {Promise<string[]>} Selected binaries
321+
*/
315322
/**
316323
* Prompt user to choose from multiple binaries found in a package
317324
* @param {string[]} binaries - List of binary files found
@@ -329,34 +336,90 @@ const selectBinaries = async (
329336
return binaries;
330337
}
331338

332-
// Filter out obvious non-binaries for cleaner selection
333-
const filteredBinaries = binaries.filter((f) => {
334-
const filename = path.basename(f).toLowerCase();
335-
return (
336-
!filename.includes("readme") &&
337-
!filename.includes("license") &&
338-
!filename.includes("changelog") &&
339-
!filename.includes("notice") &&
340-
!filename.includes(".md") &&
341-
!filename.includes(".txt") &&
342-
!filename.includes(".1") && // man pages
343-
!f.includes("doc/") &&
344-
!f.includes("docs/") &&
345-
!f.includes("man/")
346-
);
339+
// Helper to score binaries based on name and location
340+
const scoreBinary = (binaryPath) => {
341+
let score = 0;
342+
const filename = path.basename(binaryPath).toLowerCase();
343+
const dirname = path.dirname(binaryPath).toLowerCase();
344+
const cleanPackageName = extractName({ name: packageName }).toLowerCase();
345+
346+
// High penalty for completion scripts
347+
if (
348+
dirname.includes("completion") ||
349+
dirname.includes("completions") ||
350+
filename.endsWith(".bash") ||
351+
filename.endsWith(".zsh") ||
352+
filename.endsWith(".fish") ||
353+
filename.endsWith(".csh") ||
354+
filename.endsWith("completion")
355+
) {
356+
return -100;
357+
}
358+
359+
// Penalty for other non-primary scripts
360+
if (filename.endsWith(".sh") || filename.endsWith(".bat") || filename.endsWith(".ps1")) {
361+
score -= 20;
362+
}
363+
364+
// Major Boost for exact match with package name
365+
// e.g. "gum" matches "gum"
366+
if (filename === cleanPackageName) {
367+
score += 100;
368+
}
369+
370+
// Boost for name containing package name (e.g. "gum-cli")
371+
else if (filename.includes(cleanPackageName)) {
372+
score += 50;
373+
}
374+
375+
// Penalize documentation/garbage
376+
if (
377+
filename.includes("readme") ||
378+
filename.includes("license") ||
379+
filename.includes("changelog") ||
380+
filename.includes("notice") ||
381+
filename.includes(".md") ||
382+
filename.includes(".txt") ||
383+
filename.includes(".1") || // man pages
384+
binaryPath.includes("doc/") ||
385+
binaryPath.includes("docs/") ||
386+
binaryPath.includes("man/")
387+
) {
388+
score -= 100;
389+
}
390+
391+
392+
393+
return score;
394+
};
395+
396+
// Sort binaries by score
397+
const sortedBinaries = [...binaries].sort((a, b) => {
398+
const scoreA = scoreBinary(a);
399+
const scoreB = scoreBinary(b);
400+
return scoreB - scoreA;
347401
});
348402

349-
// If filtering leaves us with just one, use it
350-
if (filteredBinaries.length === 1) {
351-
if (logger) {
352-
logger.log(`Auto-selected binary: ${filteredBinaries[0]}`);
403+
// Filter out heavily penalized items if we have good candidates
404+
let binariesToChoose = sortedBinaries;
405+
const bestScore = scoreBinary(sortedBinaries[0]);
406+
407+
if (bestScore > 0) {
408+
// If we have a good candidate, strictly filter out negative scoring items (completions, docs)
409+
// unless they are the only options
410+
const goodBinaries = sortedBinaries.filter(b => scoreBinary(b) > -10);
411+
if (goodBinaries.length > 0) {
412+
binariesToChoose = goodBinaries;
353413
}
354-
return filteredBinaries;
355414
}
356415

357-
// If filtering leaves none, fall back to original list
358-
const binariesToChoose =
359-
filteredBinaries.length > 0 ? filteredBinaries : binaries;
416+
// If filtering leaves us with just one (or the top one is significantly better), use it
417+
if (binariesToChoose.length === 1 || (binariesToChoose.length > 0 && scoreBinary(binariesToChoose[0]) > 50 && scoreBinary(binariesToChoose[1] || "") < 0)) {
418+
if (logger) {
419+
logger.log(`Auto-selected binary: ${binariesToChoose[0]}`);
420+
}
421+
return [binariesToChoose[0]];
422+
}
360423

361424
if (logger) {
362425
logger.log(

0 commit comments

Comments
 (0)