Skip to content

Commit 32755ed

Browse files
Update system
1 parent 33cc8be commit 32755ed

5 files changed

Lines changed: 177 additions & 24 deletions

File tree

index.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
checkForUpdates,
66
performUpdate,
77
listInstalled,
8+
performUninstall,
89
} = require("./lib/updater");
910
const {
1011
interactiveSearch,
@@ -28,6 +29,7 @@ const HELP = `justinstall <github-url|website-url|file-url|local-file> [options]
2829
\t --search [query] Interactive search for GitHub repositories, or direct search with query
2930
\t --first <query> Find and install most-starred repo matching query
3031
\t --update [package] Update all packages or specific package
32+
\t --uninstall <name> Uninstall a previously installed package
3133
\t --list List installed packages
3234
\t -h, --help Show this help
3335
@@ -57,14 +59,36 @@ const handleUpdateCommand = async (flags, args) => {
5759
const customFilePath = args[0]; // Optional custom file path
5860

5961
log.debug(`Checking for updates: ${flags.updatePackage}`);
60-
const updates = await checkForUpdates(flags.updatePackage);
62+
const updateInfo = await checkForUpdates(flags.updatePackage);
6163

62-
if (updates.length === 0) {
64+
// If we definitively know there is no update and there wasn't an error, exit early
65+
if (updateInfo.hasUpdate === false && !updateInfo.error) {
6366
log.log(`${flags.updatePackage} is already up to date`);
6467
return;
6568
}
6669

67-
const updateInfo = updates[0];
70+
// If we can't verify the version or canUpdate flag is missing, attempt reinstall
71+
const unverifiable =
72+
updateInfo.error === true ||
73+
updateInfo.hasUpdate === undefined ||
74+
updateInfo.canUpdate === undefined;
75+
76+
if (unverifiable) {
77+
log.warn(
78+
`Unable to verify current version for ${flags.updatePackage}. Will reinstall to ensure freshness.`
79+
);
80+
if (await confirm(`Proceed to reinstall ${flags.updatePackage}?`)) {
81+
await performUpdate(
82+
{
83+
name: flags.updatePackage,
84+
source: updateInfo.source,
85+
},
86+
customFilePath
87+
);
88+
}
89+
return;
90+
}
91+
6892
if (!updateInfo.canUpdate) {
6993
log.warn(
7094
`Update available for ${flags.updatePackage} but ${updateInfo.reason}`
@@ -122,6 +146,15 @@ const main = async () => {
122146
return;
123147
}
124148

149+
if (flags.uninstall) {
150+
const name = flags.uninstallPackage;
151+
if (!name) {
152+
throw new Error("--uninstall requires a package name");
153+
}
154+
await performUninstall(name);
155+
return;
156+
}
157+
125158
if (flags.list) {
126159
listInstalled();
127160
return;

lib/installer.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,10 @@ const installSelected = async (selected, downloadPath, log) => {
600600
log
601601
);
602602
binariesList = [selected.name];
603+
// Ask to open app
604+
if (await confirm(`Open app ${path.basename(destinations[0])}?`)) {
605+
execSync(`open -n ${JSON.stringify(destinations[0])}`);
606+
}
603607
break;
604608

605609
default:
@@ -638,6 +642,12 @@ const installSelected = async (selected, downloadPath, log) => {
638642
installationMethod = packageResult.method;
639643
destinations = packageResult.destinations;
640644
binariesList = packageResult.binaries;
645+
if (installationMethod === "archive_app") {
646+
// Ask to open app
647+
if (await confirm(`Open app ${path.basename(destinations[0])}?`)) {
648+
execSync(`open -n ${JSON.stringify(destinations[0])}`);
649+
}
650+
}
641651
} else {
642652
// Fall back to regular binary installation
643653
destinations = await installBinaries(

lib/installers.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const fs = require("fs");
22
const path = require("path");
33
const os = require("os");
44
const { execSync } = require("child_process");
5+
const { extractName } = require("./config");
56

67
const getPlatformInfo = () => {
78
const arch = process.arch;
@@ -237,6 +238,8 @@ const getBinaries = (dir) => {
237238
return [...new Set(binaries)]; // Remove duplicates
238239
};
239240

241+
// Use extractName from config for consistent name normalization
242+
240243
/**
241244
* Process extracted packages (DMG/PKG) found within ZIP archives
242245
* @param {string[]} binaries - List of files found by getBinaries
@@ -253,6 +256,20 @@ const processExtractedPackages = async (
253256
checkPathFn,
254257
logger
255258
) => {
259+
// If an .app bundle was extracted from the archive, install it as a macOS app
260+
const appBundle = binaries.find((f) => f.toLowerCase().endsWith(".app"));
261+
if (appBundle && process.platform === "darwin") {
262+
if (logger) {
263+
logger.log(`Installing .app bundle from archive: ${appBundle}`);
264+
}
265+
const destinations = await installApp(appBundle, outputDir, checkPathFn, logger);
266+
return {
267+
method: "archive_app",
268+
destinations,
269+
binaries: [appBundle],
270+
};
271+
}
272+
256273
// Look for DMG or PKG files in the extracted binaries
257274
const dmgFile = binaries.find((f) => f.toLowerCase().endsWith(".dmg"));
258275
const pkgFile = binaries.find((f) => f.toLowerCase().endsWith(".pkg"));
@@ -323,7 +340,10 @@ const processExtractedPackages = async (
323340
};
324341

325342
const installApp = async (appPath, outputDir, checkPathFn, logger = null) => {
326-
const dest = path.join("/Applications", path.basename(appPath));
343+
const original = path.basename(appPath);
344+
const cleanedBase = extractName({ name: original }) || original.replace(/\.app$/i, "");
345+
const cleaned = cleanedBase.replace(/\.app$/i, "");
346+
const dest = path.join("/Applications", `${cleaned}.app`);
327347
await checkPathFn(dest);
328348

329349
fs.cpSync(path.join(outputDir, appPath), dest, { recursive: true });
@@ -424,7 +444,7 @@ const installBinaries = async (
424444

425445
for (const binary of binaries) {
426446
const binaryPath = path.join(outputDir, binary);
427-
const cleanName = cleanBinaryName(selectedName);
447+
const cleanName = extractName({ name: selectedName });
428448
const dest = path.join(os.homedir(), ".local", "bin", cleanName);
429449

430450
await checkPathFn(dest);
@@ -462,17 +482,6 @@ const installDeb = (debPath) => {
462482
return ["System-wide deb installation"];
463483
};
464484

465-
const cleanBinaryName = (name) => {
466-
return name
467-
.replace(/\.(tar\.gz|zip|dmg|pkg|deb|app)$/i, "")
468-
.replace(/v?[0-9]+\.[0-9]+\.[0-9]+/i, "")
469-
.replace(
470-
/(?:darwin|linux|windows|mac|osx|x64|arm64|aarch64|universal)/gi,
471-
""
472-
)
473-
.replace(/[ _\-\.]+$/, "")
474-
.replace(/^[ _\-\.]+/, "");
475-
};
476485

477486
module.exports = {
478487
getPlatformInfo,
@@ -487,5 +496,4 @@ module.exports = {
487496
ejectDMG,
488497
installBinaries,
489498
installDeb,
490-
cleanBinaryName,
491499
};

lib/updater.js

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const crypto = require("crypto");
33
const fs = require("fs");
44
const path = require("path");
55

6-
const { loadConfig, addInstallation, hashFile } = require("./config");
6+
const { loadConfig, addInstallation, hashFile, removeInstallation } = require("./config");
77
const { parseSource, getGitHubAssets, downloadFromUrl } = require("./sources");
88
const { createLogger, confirm, fileSize } = require("./utils");
99

@@ -65,8 +65,8 @@ const checkGitHubUpdate = async (installation) => {
6565
source.repo
6666
);
6767

68-
// Check if there's a newer commit/tag
69-
const hasUpdate = commit !== oldCommit;
68+
// Check if there's a newer tag (more reliable than commit for releases)
69+
const hasUpdate = tag !== installation.version;
7070

7171
if (!hasUpdate) {
7272
return { name, hasUpdate: false, reason: "Already up to date" };
@@ -76,7 +76,8 @@ const checkGitHubUpdate = async (installation) => {
7676
const newAsset = assets.find(
7777
(asset) =>
7878
asset.name.includes(selected.name.split(".")[0]) ||
79-
asset.extension === selected.extension
79+
asset.extension === selected.extension ||
80+
asset.name.includes(name) // Also check if asset name contains the package name
8081
);
8182

8283
if (!newAsset) {
@@ -161,7 +162,18 @@ const checkFileUpdate = async (installation) => {
161162

162163
const performUpdate = async (updateInfo, customFilePath = null) => {
163164
const log = createLogger();
164-
const { name, source } = updateInfo;
165+
const { name } = updateInfo;
166+
167+
// Ensure we have source info; if missing, fetch from installation history
168+
let source = updateInfo.source;
169+
if (!source) {
170+
const config = loadConfig();
171+
const installation = config.find((item) => item.name === name);
172+
if (!installation) {
173+
throw new Error(`No installation record found for '${name}'`);
174+
}
175+
source = installation.source;
176+
}
165177

166178
log.log(`Updating ${name}...`);
167179

@@ -173,6 +185,29 @@ const performUpdate = async (updateInfo, customFilePath = null) => {
173185

174186
try {
175187
await performInstallation(originalArgs, true); // isUpdate = true
188+
189+
// After successful update, refresh the installation record with new version info
190+
if (updateInfo.newTag) {
191+
const config = loadConfig();
192+
const installation = config.find((item) => item.name === name);
193+
if (installation) {
194+
installation.version = updateInfo.newTag;
195+
installation.commit = updateInfo.newCommit;
196+
installation.date = new Date().toISOString();
197+
198+
// Update the selected asset info if we have it
199+
if (updateInfo.newAsset) {
200+
installation.selected.name = updateInfo.newAsset.name;
201+
installation.selected.size = updateInfo.newAsset.size;
202+
installation.selected.downloadUrl = updateInfo.newAsset.browser_download_url;
203+
}
204+
205+
const { saveConfig } = require("./config");
206+
saveConfig(config);
207+
log.debug(`Updated installation record for ${name} to version ${updateInfo.newTag}`);
208+
}
209+
}
210+
176211
log.log(`Successfully updated ${name}`);
177212
} catch (error) {
178213
log.error(`Failed to update ${name}: ${error.message}`);
@@ -208,9 +243,70 @@ const removePackageHistory = (packageName) => {
208243
removeInstallation(packageName);
209244
};
210245

246+
const getPathSize = (p) => {
247+
const stat = fs.statSync(p);
248+
if (stat.isFile()) return stat.size;
249+
if (stat.isDirectory()) {
250+
let total = 0;
251+
for (const entry of fs.readdirSync(p)) {
252+
total += getPathSize(path.join(p, entry));
253+
}
254+
return total;
255+
}
256+
return 0;
257+
};
258+
259+
const performUninstall = async (packageName) => {
260+
const log = createLogger();
261+
const config = loadConfig();
262+
const installation = config.find((item) => item.name === packageName);
263+
264+
if (!installation) {
265+
throw new Error(`Package '${packageName}' not found in installation history`);
266+
}
267+
268+
const destinations = installation.installation?.destinations || [];
269+
if (destinations.length === 0) {
270+
throw new Error(`No recorded destinations for '${packageName}'`);
271+
}
272+
273+
let totalBytes = 0;
274+
for (const dest of destinations) {
275+
if (fs.existsSync(dest)) {
276+
try {
277+
totalBytes += getPathSize(dest);
278+
} catch (e) {
279+
// Ignore size calc errors; proceed with uninstall
280+
}
281+
}
282+
}
283+
284+
const { fileSize, confirm } = require("./utils");
285+
const pretty = fileSize(totalBytes, true, 1);
286+
log.log(`Uninstalling ${packageName} (${pretty})`);
287+
if (!(await confirm(`Proceed to uninstall ${packageName}?`))) {
288+
throw new Error("Uninstall canceled by user");
289+
}
290+
291+
for (const dest of destinations) {
292+
if (fs.existsSync(dest)) {
293+
try {
294+
fs.rmSync(dest, { recursive: true, force: true });
295+
log.debug(`Removed ${dest}`);
296+
} catch (e) {
297+
log.warn(`Failed to remove ${dest}: ${e.message}`);
298+
}
299+
}
300+
}
301+
302+
removeInstallation(packageName);
303+
log.log(`Successfully uninstalled ${packageName}`);
304+
};
305+
211306
module.exports = {
212307
checkForUpdates,
213308
performUpdate,
214309
listInstalled,
215310
removePackageHistory,
311+
performUninstall,
216312
};

lib/utils.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ const confirm = (question, defaultAnswer = "y") => {
6666
});
6767

6868
rli.question(
69-
`${colors.fg.yellow}${question}${colors.reset} ${colors.fg.blue}(${
70-
defaultAnswer.toLowerCase() === "y" ? "Y" : "y"
69+
`${colors.fg.yellow}${question}${colors.reset} ${colors.fg.blue}(${defaultAnswer.toLowerCase() === "y" ? "Y" : "y"
7170
}/${defaultAnswer.toLowerCase() === "n" ? "N" : "n"})${colors.reset} `,
7271
(ans) => {
7372
let result =
@@ -189,6 +188,13 @@ const parseFlags = (args) => {
189188
flags.updatePackage = args[i + 1];
190189
i++; // Skip the package name in next iteration
191190
}
191+
} else if (flagName === "uninstall") {
192+
flags.uninstall = true;
193+
// Check if next arg is a package name
194+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
195+
flags.uninstallPackage = args[i + 1];
196+
i++; // Skip the package name in next iteration
197+
}
192198
} else if (flagName === "help") {
193199
flags.help = true;
194200
} else if (flagName === "list") {

0 commit comments

Comments
 (0)