Skip to content

Commit ed1da4d

Browse files
Update
1 parent e359b53 commit ed1da4d

14 files changed

Lines changed: 1806 additions & 133 deletions

index.js

Lines changed: 213 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
22

3+
const path = require("path");
34
const { performInstallation } = require("./lib/installer");
45
const {
56
checkForUpdates,
@@ -18,10 +19,25 @@ const {
1819
createLink,
1920
parseFlags,
2021
confirm,
22+
colors,
2123
} = require("./lib/utils");
24+
const { createMultiSelect, createModuleProgress } = require("./lib/progress");
25+
const { detectOrphanedInstallations } = require("./lib/system");
26+
const { loadConfig } = require("./lib/config");
27+
28+
const getVersion = () => {
29+
try {
30+
const packageJson = require(path.join(__dirname, "package.json"));
31+
return packageJson.version || "unknown";
32+
} catch {
33+
return "unknown";
34+
}
35+
};
36+
37+
const VERSION = getVersion();
2238

2339
const HELP = `justinstall <github-url|website-url|file-url|local-file> [options]
24-
\tv1.2.0 - Just install anything. Supports .tar.gz, .zip, .dmg, .app, .pkg, and .deb files.
40+
\t${VERSION} - Just install anything. Supports .tar.gz, .zip, .dmg, .app, .pkg, and .deb files.
2541
\tZIP files containing DMG or PKG packages are automatically detected and installed.
2642
\tBinaries will be installed to ~/.local/bin.
2743
@@ -34,9 +50,10 @@ const HELP = `justinstall <github-url|website-url|file-url|local-file> [options]
3450
\t --search [query] Interactive search for GitHub repositories, or direct search with query
3551
\t --first <query> Find and install most-starred repo matching query
3652
\t --update [package] Update all packages or specific package
37-
\t --uninstall <name> Uninstall a previously installed package
53+
\t --uninstall [name] Uninstall a previously installed package (interactive if no name provided)
3854
\t --list List installed packages
3955
\t --yes Answer yes to all prompts
56+
\t --version Show version
4057
\t -h, --help Show this help
4158
4259
\tExamples:
@@ -52,6 +69,8 @@ const HELP = `justinstall <github-url|website-url|file-url|local-file> [options]
5269
\t justinstall --update
5370
\t justinstall --update tailscale
5471
\t justinstall --update tailscale ./new-tailscale.pkg
72+
\t justinstall --uninstall
73+
\t justinstall --uninstall tailscale
5574
5675
\tMade by ${createLink(
5776
"Explosion-Scratch",
@@ -60,21 +79,20 @@ const HELP = `justinstall <github-url|website-url|file-url|local-file> [options]
6079

6180
const handleUpdateCommand = async (flags, args) => {
6281
const log = createLogger();
82+
const progress = createModuleProgress();
6383

6484
if (flags.updatePackage) {
65-
// Update specific package
66-
const customFilePath = args[0]; // Optional custom file path
85+
progress.startModule("Checking for updates", flags.updatePackage);
86+
const customFilePath = args[0];
6787

68-
log.debug(`Checking for updates: ${flags.updatePackage}`);
6988
const updateInfo = await checkForUpdates(flags.updatePackage);
89+
progress.completeModule(true);
7090

71-
// If we definitively know there is no update and there wasn't an error, exit early
7291
if (updateInfo.hasUpdate === false && !updateInfo.error) {
73-
log.log(`${flags.updatePackage} is already up to date`);
92+
log.log(`${colors.fg.green}${colors.reset} ${flags.updatePackage} is already up to date`);
7493
return;
7594
}
7695

77-
// If we can't verify the version or canUpdate flag is missing, attempt reinstall
7896
const unverifiable =
7997
updateInfo.error === true ||
8098
updateInfo.hasUpdate === undefined ||
@@ -85,58 +103,198 @@ const handleUpdateCommand = async (flags, args) => {
85103
`Unable to verify current version for ${flags.updatePackage}. Will reinstall to ensure freshness.`
86104
);
87105
if (await confirm(`Proceed to reinstall ${flags.updatePackage}?`, "y", flags.yes)) {
106+
progress.startModule("Reinstalling", flags.updatePackage);
88107
await performUpdate(
89108
{
90109
name: flags.updatePackage,
91110
source: updateInfo.source,
92111
},
93112
customFilePath,
94-
flags.yes
113+
true
95114
);
115+
progress.completeModule(true);
96116
}
97117
return;
98118
}
99119

100120
if (!updateInfo.canUpdate) {
101121
log.warn(
102-
`Update available for ${flags.updatePackage} but ${updateInfo.reason}`
122+
`${colors.fg.yellow}${colors.reset} Update available for ${flags.updatePackage} but ${updateInfo.reason}`
103123
);
104124
return;
105125
}
106126

107127
log.log(
108-
`Update available for ${flags.updatePackage}: ${updateInfo.reason}`
128+
`${colors.fg.cyan}${colors.reset} Update available for ${flags.updatePackage}: ${updateInfo.reason}`
109129
);
110130
if (await confirm(`Proceed with update?`, "y", flags.yes)) {
111-
await performUpdate(updateInfo, customFilePath, flags.yes);
131+
progress.startModule("Updating", flags.updatePackage);
132+
await performUpdate(updateInfo, customFilePath, true);
133+
progress.completeModule(true);
112134
}
113135
} else {
114-
// Update all packages
115-
log.debug("Checking for updates for all packages...");
136+
progress.startModule("Checking for updates", "all packages");
116137
const updates = await checkForUpdates();
138+
progress.completeModule(true);
139+
140+
progress.startModule("Checking for orphaned installations");
141+
const config = loadConfig();
142+
const orphaned = detectOrphanedInstallations(config);
143+
progress.completeModule(true, orphaned.length > 0 ? `Found ${orphaned.length} orphaned` : "None found");
144+
145+
if (orphaned.length > 0) {
146+
log.log(`\n${colors.fg.yellow}Found ${orphaned.length} orphaned installation(s):${colors.reset}`);
147+
for (const orphan of orphaned) {
148+
log.log(` ${colors.fg.red}${colors.reset} ${orphan.name}: ${orphan.reason}`);
149+
}
150+
151+
if (await confirm(`\nRemove orphaned installation records?`, "y", flags.yes)) {
152+
const { removeInstallation } = require("./lib/config");
153+
for (const orphan of orphaned) {
154+
removeInstallation(orphan.name);
155+
log.log(` Removed record: ${orphan.name}`);
156+
}
157+
}
158+
}
117159

118160
if (updates.length === 0) {
119-
log.log("All packages are up to date");
161+
log.log(`\n${colors.fg.green}${colors.reset} All packages are up to date`);
120162
return;
121163
}
122164

123-
log.log(`Found ${updates.length} package(s) with updates:`);
165+
log.log(`\n${colors.fg.cyan}Found ${updates.length} package(s) with updates:${colors.reset}`);
124166
for (const update of updates) {
125-
log.log(` ${update.name}: ${update.reason}`);
167+
const canUpdateIcon = update.canUpdate
168+
? `${colors.fg.green}${colors.reset}`
169+
: `${colors.fg.yellow}${colors.reset}`;
170+
log.log(` ${canUpdateIcon} ${update.name}: ${update.reason}`);
126171
}
127172

128-
if (await confirm(`Update all ${updates.length} package(s)?`, "y", flags.yes)) {
129-
for (const update of updates) {
130-
if (update.canUpdate) {
131-
try {
132-
await performUpdate(update, null, flags.yes);
133-
} catch (error) {
134-
log.error(`Failed to update ${update.name}: ${error.message}`);
135-
}
173+
const updatablePackages = updates.filter((u) => u.canUpdate);
174+
175+
if (updatablePackages.length === 0) {
176+
log.log(`\n${colors.fg.yellow}No packages can be automatically updated${colors.reset}`);
177+
return;
178+
}
179+
180+
const readline = require("readline");
181+
const rl = readline.createInterface({
182+
input: process.stdin,
183+
output: process.stdout,
184+
});
185+
186+
const choice = await new Promise((resolve) => {
187+
if (flags.yes) {
188+
resolve("all");
189+
return;
190+
}
191+
rl.question(
192+
`\n${colors.fg.yellow}Update options:${colors.reset} (a)ll ${updatablePackages.length} packages, (s)elect which to update, (n)one? `,
193+
(ans) => {
194+
rl.close();
195+
resolve(ans.trim().toLowerCase());
136196
}
197+
);
198+
});
199+
200+
if (choice === "n" || choice === "no" || choice === "none") {
201+
log.log("Update cancelled");
202+
return;
203+
}
204+
205+
let packagesToUpdate = updatablePackages;
206+
207+
if (choice === "s" || choice === "select" || choice === "some") {
208+
const selectedItems = await createMultiSelect(
209+
updatablePackages.map((u) => ({
210+
label: `${u.name} - ${u.reason}`,
211+
value: u,
212+
})),
213+
"Select packages to update:"
214+
);
215+
216+
if (selectedItems.length === 0) {
217+
log.log("No packages selected");
218+
return;
137219
}
220+
221+
packagesToUpdate = selectedItems.map((item) => item.value);
138222
}
223+
224+
log.log(`\n${colors.fg.cyan}Updating ${packagesToUpdate.length} package(s)...${colors.reset}\n`);
225+
226+
for (let i = 0; i < packagesToUpdate.length; i++) {
227+
const update = packagesToUpdate[i];
228+
progress.startModule(
229+
`Updating (${i + 1}/${packagesToUpdate.length})`,
230+
update.name
231+
);
232+
233+
try {
234+
await performUpdate(update, null, true);
235+
progress.completeModule(true);
236+
} catch (error) {
237+
progress.completeModule(false, error.message);
238+
log.error(`Failed to update ${update.name}: ${error.message}`);
239+
}
240+
}
241+
242+
log.log(`\n${colors.fg.green}${colors.reset} Update complete`);
243+
}
244+
};
245+
246+
const handleUninstallCommand = async (flags) => {
247+
const log = createLogger();
248+
const config = loadConfig();
249+
250+
if (!flags.uninstallPackage) {
251+
if (config.length === 0) {
252+
log.log(`${colors.fg.yellow}No packages installed via justinstall${colors.reset}`);
253+
log.log(`\nTo install a package, run: ${colors.fg.cyan}justinstall <github-repo>${colors.reset}`);
254+
return;
255+
}
256+
257+
log.log(`${colors.fg.cyan}Select packages to uninstall:${colors.reset}\n`);
258+
259+
const selectedItems = await createMultiSelect(
260+
config.map((item) => {
261+
const versionInfo = item.version ? ` (${item.version})` : "";
262+
return {
263+
label: `${item.name}${versionInfo}`,
264+
value: item,
265+
};
266+
}),
267+
"Select packages to uninstall:"
268+
);
269+
270+
if (selectedItems.length === 0) {
271+
log.log("No packages selected");
272+
return;
273+
}
274+
275+
const progress = createModuleProgress();
276+
277+
for (let i = 0; i < selectedItems.length; i++) {
278+
const item = selectedItems[i].value;
279+
progress.startModule(
280+
`Uninstalling (${i + 1}/${selectedItems.length})`,
281+
item.name
282+
);
283+
284+
try {
285+
await performUninstall(item.name, true);
286+
progress.completeModule(true);
287+
} catch (error) {
288+
progress.completeModule(false, error.message);
289+
log.error(`Failed to uninstall ${item.name}: ${error.message}`);
290+
}
291+
}
292+
293+
log.log(`\n${colors.fg.green}${colors.reset} Uninstall complete`);
294+
return;
139295
}
296+
297+
await performUninstall(flags.uninstallPackage, flags.yes);
140298
};
141299

142300
const main = async () => {
@@ -149,17 +307,18 @@ const main = async () => {
149307
return;
150308
}
151309

310+
if (flags.version) {
311+
log.log(`justinstall ${VERSION}`);
312+
return;
313+
}
314+
152315
if (flags.update !== undefined) {
153316
await handleUpdateCommand(flags, remainingArgs);
154317
return;
155318
}
156319

157320
if (flags.uninstall) {
158-
const name = flags.uninstallPackage;
159-
if (!name) {
160-
throw new Error("--uninstall requires a package name");
161-
}
162-
await performUninstall(name, flags.yes);
321+
await handleUninstallCommand(flags);
163322
return;
164323
}
165324

@@ -170,16 +329,13 @@ const main = async () => {
170329

171330
if (flags.search) {
172331
if (flags.searchQuery) {
173-
// Direct search with query
174332
const repos = await displaySearchResults(flags.searchQuery);
175333
if (repos && repos.length > 0) {
176-
const log = createLogger();
177334
if (
178335
await confirm(`Install the first result (${repos[0].full_name})?`, "y", flags.yes)
179336
) {
180337
await performInstallation([repos[0].full_name]);
181338
} else {
182-
// If user doesn't want the first result, show interactive selection
183339
const selectedRepo = await selectRepositoryFromResults(
184340
repos,
185341
flags.searchQuery
@@ -188,9 +344,12 @@ const main = async () => {
188344
await performInstallation([selectedRepo.full_name]);
189345
}
190346
}
347+
} else if (!repos || repos.length === 0) {
348+
log.log(`\n${colors.fg.yellow}No repositories found matching "${flags.searchQuery}"${colors.reset}`);
349+
log.log(`\nTry a different search term or browse GitHub directly:`);
350+
log.log(` ${colors.fg.cyan}https://github.com/search?q=${encodeURIComponent(flags.searchQuery)}&type=repositories${colors.reset}`);
191351
}
192352
} else {
193-
// Interactive search
194353
const selectedRepo = await interactiveSearch();
195354
if (selectedRepo) {
196355
await performInstallation([selectedRepo.full_name]);
@@ -213,7 +372,6 @@ const main = async () => {
213372
await performInstallation(remainingArgs, false, flags.yes);
214373
};
215374

216-
// Main execution
217375
(async () => {
218376
process.on("SIGINT", () => {
219377
process.exit(0);
@@ -223,8 +381,26 @@ const main = async () => {
223381
await main();
224382
} catch (error) {
225383
const log = createLogger();
226-
log.error(error.message);
227-
log.error(error.stack);
384+
385+
if (error.message.includes("not found")) {
386+
log.error(`${colors.fg.red}Error:${colors.reset} ${error.message}`);
387+
log.log(`\n${colors.fg.yellow}Troubleshooting tips:${colors.reset}`);
388+
log.log(` • Check that the package name or URL is correct`);
389+
log.log(` • Try searching: ${colors.fg.cyan}justinstall --search <query>${colors.reset}`);
390+
log.log(` • Check GitHub releases page directly`);
391+
} else if (error.message.includes("No suitable package") || error.message.includes("No compatible")) {
392+
log.error(`${colors.fg.red}Error:${colors.reset} ${error.message}`);
393+
log.log(`\n${colors.fg.yellow}This package may not have compatible releases for your system${colors.reset}`);
394+
log.log(`Platform: ${process.platform}, Architecture: ${process.arch}`);
395+
} else if (error.message.includes("sudo") || error.message.includes("permission")) {
396+
log.error(`${colors.fg.red}Permission error:${colors.reset} ${error.message}`);
397+
log.log(`\n${colors.fg.yellow}Try running with administrator privileges${colors.reset}`);
398+
} else {
399+
log.error(`${colors.fg.red}Error:${colors.reset} ${error.message}`);
400+
if (process.env.DEBUG) {
401+
log.error(error.stack);
402+
}
403+
}
228404
process.exit(1);
229405
}
230406
})();

0 commit comments

Comments
 (0)