11#!/usr/bin/env node
22
3+ const path = require ( "path" ) ;
34const { performInstallation } = require ( "./lib/installer" ) ;
45const {
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
2339const 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
6180const 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
142300const 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