Skip to content

Commit e850162

Browse files
Yay
1 parent e20a378 commit e850162

8 files changed

Lines changed: 411 additions & 37 deletions

File tree

CLAUDE.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
`justinstall` is a command-line tool that makes it easy to install software from various sources including GitHub repositories, direct URLs, and local files. It supports multiple file formats and platforms, with intelligent detection of install scripts and binaries.
8+
9+
## Development Environment
10+
11+
- **Runtime**: Bun (JavaScript runtime)
12+
- **Main entry point**: `index.js`
13+
- **Configuration**: `jsconfig.json` (TypeScript support without compilation)
14+
- **Build script**: `build.sh` for creating platform-specific binaries
15+
16+
## Build Commands
17+
18+
```bash
19+
# Build for all platforms
20+
./build.sh
21+
22+
# Individual platform builds (manual)
23+
bun build --compile --target=bun-linux-x64 index.js --outfile build/justinstall-v1.2.0-linux-x64
24+
bun build --compile --target=bun-linux-arm64 index.js --outfile build/justinstall-v1.2.0-linux-arm64
25+
bun build --compile --target=bun-windows-x64 index.js --outfile build/justinstall-v1.2.0-windows-x64.exe
26+
bun build --compile --target=bun-darwin-x64 index.js --outfile build/justinstall-v1.2.0-darwin-x64
27+
bun build --compile --target=bun-darwin-arm64 index.js --outfile build/justinstall-v1.2.0-darwin-arm64
28+
```
29+
30+
## Architecture Overview
31+
32+
### Core Components
33+
34+
1. **Main Entry Point (`index.js`)**
35+
- CLI argument parsing and command routing
36+
- Help text and usage information
37+
- Integration of all major modules
38+
39+
2. **Installation System (`lib/installer.js`)**
40+
- Main installation orchestration
41+
- Coordinates between sources and installers
42+
- Handles user confirmation and progress tracking
43+
44+
3. **Source Management (`lib/sources.js`)**
45+
- GitHub repository parsing and asset discovery
46+
- Website scraping and download detection
47+
- Smart URL handling and fallback strategies
48+
- Install script detection from README/release notes
49+
50+
4. **Platform Installers (`lib/installers.js`)**
51+
- Platform-specific installation logic
52+
- Archive extraction (tar.gz, zip, tar.xz)
53+
- Binary detection and installation (~/.local/bin)
54+
- macOS-specific: DMG mounting, PKG installation, .app handling
55+
- Linux-specific: DEB package installation
56+
57+
5. **Update Management (`lib/updater.js`)**
58+
- Configuration management (`~/.config/justinstall/config.json`)
59+
- Update checking and installation
60+
- Package listing and uninstallation
61+
62+
6. **Search Functionality (`lib/search.js`)**
63+
- GitHub repository search
64+
- Interactive selection interface
65+
- Repository metadata fetching
66+
67+
7. **Utilities (`lib/utils.js`)**
68+
- Logging system with configurable levels
69+
- File operations and path management
70+
- Platform detection and capability checking
71+
- User interaction helpers (confirmation prompts)
72+
73+
8. **Configuration (`lib/config.js`)**
74+
- Installation history tracking
75+
- Package metadata storage
76+
- Configuration file management
77+
78+
## Key Features
79+
80+
### Installation Sources
81+
- **GitHub**: Repository owner/repo format, releases, assets
82+
- **Direct URLs**: Website downloads, direct file links
83+
- **Local Files**: Local package installation
84+
- **Smart URLs**: Intelligent fallback and discovery
85+
86+
### Supported Formats
87+
- Archives: `.tar.gz`, `.zip`, `.tar.xz`
88+
- macOS: `.dmg`, `.pkg`, `.app`
89+
- Linux: `.deb`
90+
- Binaries: Direct executable installation
91+
92+
### Platform Support
93+
- **macOS**: Full support with code signing and quarantine handling
94+
- **Linux**: Binary and DEB package support
95+
- **Windows**: Basic binary support
96+
- **BSD**: Limited support for basic operations
97+
98+
## Installation Strategy
99+
100+
The tool uses a sophisticated asset selection algorithm:
101+
1. Platform-specific asset detection (architecture, OS)
102+
2. Fallback chains for compatibility
103+
3. Smart binary detection within archives
104+
4. Install script discovery and execution
105+
5. User confirmation for overwrites and sensitive operations
106+
107+
## Configuration
108+
109+
- **Config location**: `~/.config/justinstall/config.json`
110+
- **Binary install location**: `~/.local/bin`
111+
- **Logging**: Configurable levels with file output option
112+
- **Update tracking**: Automatic version checking and updates
113+
114+
## Testing
115+
116+
The project currently does not have automated tests. Manual testing should cover:
117+
- Installation from various sources
118+
- Different file formats and platforms
119+
- Update and uninstall operations
120+
- Error handling and edge cases
121+
122+
## Dependencies
123+
124+
- **cli-progress**: Progress bar functionality
125+
- **@types/bun**: TypeScript definitions for Bun
126+
- **typescript**: Peer dependency for type checking

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ const main = async () => {
224224
} catch (error) {
225225
const log = createLogger();
226226
log.error(error.message);
227+
log.error(error.stack);
227228
process.exit(1);
228229
}
229230
})();

lib/config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ const extractName = (selected) => {
103103
return selected.name
104104
.replace(/\.(tar\.gz|tar\.xz|zip|dmg|pkg|deb|app)$/i, "")
105105
.replace(/v?[0-9]+\.[0-9]+\.[0-9]+/i, "")
106-
.replace(/[-_]+(?:darwin|linux|windows|mac|osx|apple|x64|arm64|aarch64|universal|amd64)[-_]*/gi, "")
107-
.replace(/(?:darwin|linux|windows|mac|osx|apple|x64|arm64|aarch64|universal|amd64)[-_]*/gi, "")
108-
.replace(/[-_]+(?:darwin|linux|windows|mac|osx|apple|x64|arm64|aarch64|universal|amd64)$/gi, "")
109-
.replace(/^(?:darwin|linux|windows|mac|osx|apple|x64|arm64|aarch64|universal|amd64)[-_]+/gi, "")
106+
.replace(/[-_]+(?:darwin|linux|windows|mac|osx|macos|apple|x64|arm64|aarch64|universal|amd64)[-_]*/gi, "")
107+
.replace(/(?:darwin|linux|windows|mac|osx|macos|apple|x64|arm64|aarch64|universal|amd64)[-_]*/gi, "")
108+
.replace(/[-_]+(?:darwin|linux|windows|mac|osx|macos|apple|x64|arm64|aarch64|universal|amd64)$/gi, "")
109+
.replace(/^(?:darwin|linux|windows|mac|osx|macos|apple|x64|arm64|aarch64|universal|amd64)[-_]+/gi, "")
110110
.replace(/[0-9]+\.[0-9]+\.[0-9]+/i, "")
111111
.replace(/[-_]+$/, "")
112112
.replace(/^[-_]+/, "")

lib/installer.js

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const fs = require("fs");
22
const path = require("path");
3-
const { execSync } = require("child_process");
3+
const { safeExecSync } = require("./utils");
44
const os = require("os");
55

66
const {
@@ -65,7 +65,7 @@ const performInstallation = async (args, isUpdate = false, yesFlag = false) => {
6565
const source = parseSource(args[0]);
6666
const customFilePath = args[1]; // For update with custom file path
6767

68-
tmpdir = execSync("mktemp -d").toString().trim();
68+
tmpdir = safeExecSync("mktemp -d", [], { encoding: "utf8" }).trim();
6969

7070
const platformInfo = getPlatformInfo();
7171
const capabilities = getInstallCapabilities();
@@ -110,6 +110,7 @@ const performInstallation = async (args, isUpdate = false, yesFlag = false) => {
110110
platformInfo,
111111
capabilities,
112112
log,
113+
yesFlag,
113114
);
114115

115116
// Handle script installation case
@@ -128,9 +129,7 @@ const performInstallation = async (args, isUpdate = false, yesFlag = false) => {
128129
}
129130

130131
const shouldInstall = await confirm(
131-
`Ok to ${isUpdate ? "update" : "install"} ${selected.name} (${fileSize(
132-
selected.size,
133-
)})?`,
132+
`Ok to ${isUpdate ? "update" : "install"} ${selected.name} (${selected.size ? fileSize(selected.size) : selected.browser_download_url})?`,
134133
"y",
135134
yesFlag
136135
);
@@ -478,10 +477,10 @@ const selectInstallScript = async (installScripts, log, yesFlag = false) => {
478477
const executeInstallScript = (script, log) => {
479478
log.debug("Running install script...");
480479
const processedCode = processInstallSnippetReplacements(script.code);
481-
execSync(processedCode, { stdio: "inherit" });
480+
safeExecSync("sh", ["-c", processedCode], { stdio: "inherit" });
482481
};
483482

484-
const handleGitHubSource = async (source, platformInfo, capabilities, log) => {
483+
const handleGitHubSource = async (source, platformInfo, capabilities, log, yesFlag = false) => {
485484
let assets = [];
486485
let body = "";
487486
let tag = null;
@@ -535,8 +534,8 @@ const handleGitHubSource = async (source, platformInfo, capabilities, log) => {
535534
scriptPath,
536535
log,
537536
);
538-
execSync(`chmod +x ${JSON.stringify(scriptPath)}`);
539-
execSync(scriptPath, { stdio: "inherit" });
537+
safeExecSync("chmod", ["+x", scriptPath]);
538+
safeExecSync(scriptPath, [], { stdio: "inherit" });
540539

541540
const installRecord = createInstallationRecord(source, installerScript, {
542541
name: source.repo,
@@ -667,7 +666,7 @@ const installSelected = async (selected, downloadPath, log, yesFlag = false) =>
667666

668667
// Ask to open app
669668
if (await confirm(`Open app ${path.basename(destinations[0])}?`)) {
670-
execSync(`open -n ${JSON.stringify(destinations[0])}`);
669+
safeExecSync("open", ["-n", destinations[0]]);
671670
}
672671
} else if (pkgFile) {
673672
log.log(`Installing .pkg file: ${pkgFile}`);
@@ -718,7 +717,7 @@ const installSelected = async (selected, downloadPath, log, yesFlag = false) =>
718717

719718
// Ask to open app
720719
if (await confirm(`Open app ${path.basename(destinations[0])}?`, "y", yesFlag)) {
721-
execSync(`open -n ${JSON.stringify(destinations[0])}`);
720+
safeExecSync("open", ["-n", destinations[0]]);
722721
}
723722
break;
724723

@@ -763,7 +762,7 @@ const installSelected = async (selected, downloadPath, log, yesFlag = false) =>
763762
if (installationMethod === "archive_app") {
764763
// Ask to open app
765764
if (await confirm(`Open app ${path.basename(destinations[0])}?`, "y", yesFlag)) {
766-
execSync(`open -n ${JSON.stringify(destinations[0])}`);
765+
safeExecSync("open", ["-n", destinations[0]]);
767766
}
768767
}
769768
} else {
@@ -793,9 +792,42 @@ const installSelected = async (selected, downloadPath, log, yesFlag = false) =>
793792
const cleanup = () => {
794793
if (tmpdir && fs.existsSync(tmpdir)) {
795794
try {
795+
// First, try to make files writable in case they're read-only
796+
const { safeExecSync } = require('./utils');
797+
try {
798+
safeExecSync('chmod', ['-R', '+w', tmpdir], { stdio: 'pipe' });
799+
} catch (chmodError) {
800+
// If chmod fails, continue with cleanup attempt
801+
}
802+
803+
// Attempt to remove the directory
796804
fs.rmSync(tmpdir, { recursive: true, force: true });
797805
} catch (e) {
798-
// Ignore cleanup errors
806+
// If removal fails, try a more aggressive approach
807+
try {
808+
// Try to remove individual files first
809+
const files = fs.readdirSync(tmpdir, { withFileTypes: true });
810+
for (const file of files) {
811+
const filePath = path.join(tmpdir, file.name);
812+
try {
813+
if (file.isDirectory()) {
814+
fs.rmSync(filePath, { recursive: true, force: true });
815+
} else {
816+
fs.unlinkSync(filePath);
817+
}
818+
} catch (fileError) {
819+
// Continue with other files if one fails
820+
}
821+
}
822+
// Try to remove the directory again
823+
fs.rmSync(tmpdir, { recursive: true, force: true });
824+
} catch (finalError) {
825+
// If all attempts fail, log a warning but don't crash
826+
const { createLogger } = require('./utils');
827+
const logger = createLogger();
828+
logger.warn(`Failed to completely clean up temporary directory: ${tmpdir}`);
829+
logger.warn(`Manual cleanup may be required. Error: ${finalError.message}`);
830+
}
799831
}
800832
}
801833
};

0 commit comments

Comments
 (0)