Skip to content

Commit e359b53

Browse files
Refactor
1 parent 23d4329 commit e359b53

44 files changed

Lines changed: 3749 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

install.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
# Run the build script
5+
./build.sh
6+
7+
# Get version from package.json
8+
VERSION=$(cat package.json | jq -r '.version')
9+
10+
# Detect OS and Architecture
11+
OS="$(uname -s)"
12+
ARCH="$(uname -m)"
13+
14+
BINARY_NAME=""
15+
16+
if [ "$OS" == "Darwin" ]; then
17+
if [ "$ARCH" == "arm64" ]; then
18+
BINARY_NAME="justinstall-${VERSION}-darwin-arm64"
19+
else
20+
BINARY_NAME="justinstall-${VERSION}-darwin-x64"
21+
fi
22+
elif [ "$OS" == "Linux" ]; then
23+
if [ "$ARCH" == "aarch64" ]; then
24+
BINARY_NAME="justinstall-${VERSION}-linux-arm64"
25+
else
26+
BINARY_NAME="justinstall-${VERSION}-linux-x64"
27+
fi
28+
else
29+
echo "Unsupported OS: $OS"
30+
exit 1
31+
fi
32+
33+
SOURCE="build/$BINARY_NAME"
34+
DEST_DIR="$HOME/.local/bin"
35+
DEST="$DEST_DIR/justinstall"
36+
37+
if [ ! -f "$SOURCE" ]; then
38+
echo "Error: Binary $SOURCE not found!"
39+
exit 1
40+
fi
41+
42+
# Ensure destination directory exists
43+
mkdir -p "$DEST_DIR"
44+
45+
# Install
46+
echo "Installing $SOURCE to $DEST..."
47+
cp "$SOURCE" "$DEST"
48+
chmod +x "$DEST"
49+
50+
echo "Successfully installed justinstall to $DEST"
51+
echo "Make sure $DEST_DIR is in your PATH."

lib/config.test.js

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
const { describe, test, expect } = require("bun:test");
2+
const { extractName } = require("./config");
3+
4+
describe("extractName", () => {
5+
describe("version removal", () => {
6+
test("removes semver versions with v prefix", () => {
7+
expect(extractName({ name: "app-v1.2.3.tar.gz" })).toBe("app");
8+
});
9+
10+
test("removes semver versions without v prefix", () => {
11+
expect(extractName({ name: "tool-1.0.0.zip" })).toBe("tool");
12+
});
13+
14+
test("removes versions in the middle of name", () => {
15+
expect(extractName({ name: "cli-v2.5.1-darwin.tar.gz" })).toBe("cli");
16+
});
17+
18+
test("handles real-world versioned filenames", () => {
19+
const ripgrepResult = extractName({ name: "ripgrep-14.1.0-x86_64-apple-darwin.tar.gz" });
20+
expect(ripgrepResult.toLowerCase()).toContain("ripgrep");
21+
const fzfResult = extractName({ name: "fzf-0.54.0-darwin_arm64.tar.gz" });
22+
expect(fzfResult.toLowerCase()).toContain("fzf");
23+
const helixResult = extractName({ name: "helix-24.07-x86_64-macos.tar.gz" });
24+
expect(helixResult.toLowerCase()).toContain("helix");
25+
});
26+
27+
test("handles versions at the start", () => {
28+
expect(extractName({ name: "v1.2.3-myapp.dmg" })).toBe("myapp");
29+
});
30+
});
31+
32+
describe("platform removal", () => {
33+
test("removes darwin platform indicator", () => {
34+
expect(extractName({ name: "tool_darwin.zip" })).toBe("tool");
35+
});
36+
37+
test("removes macos platform indicator", () => {
38+
expect(extractName({ name: "app-macos.dmg" })).toBe("app");
39+
});
40+
41+
test("removes osx platform indicator", () => {
42+
expect(extractName({ name: "binary_osx.tar.gz" })).toBe("binary");
43+
});
44+
45+
test("removes apple platform indicator", () => {
46+
expect(extractName({ name: "cli-apple.pkg" })).toBe("cli");
47+
});
48+
49+
test("removes linux platform indicator", () => {
50+
expect(extractName({ name: "tool_linux.tar.gz" })).toBe("tool");
51+
});
52+
53+
test("removes windows platform indicator", () => {
54+
expect(extractName({ name: "app-windows.zip" })).toBe("app");
55+
});
56+
57+
test("handles real-world platform filenames", () => {
58+
const deltaResult = extractName({ name: "delta-0.16.5-x86_64-apple-darwin.tar.gz" });
59+
expect(deltaResult.toLowerCase()).toContain("delta");
60+
const batResult = extractName({ name: "bat-v0.24.0-x86_64-unknown-linux-gnu.tar.gz" });
61+
expect(batResult.toLowerCase()).toContain("bat");
62+
});
63+
});
64+
65+
describe("architecture removal", () => {
66+
test("removes x64 architecture", () => {
67+
expect(extractName({ name: "app_x64.zip" })).toBe("app");
68+
});
69+
70+
test("removes x86_64 architecture when possible", () => {
71+
const result = extractName({ name: "tool-x86_64.tar.gz" });
72+
expect(result).toBeDefined();
73+
expect(result.length).toBeGreaterThan(0);
74+
});
75+
76+
test("removes arm64 architecture", () => {
77+
expect(extractName({ name: "binary-arm64.dmg" })).toBe("binary");
78+
});
79+
80+
test("removes aarch64 architecture", () => {
81+
expect(extractName({ name: "cli_aarch64.pkg" })).toBe("cli");
82+
});
83+
84+
test("removes universal architecture", () => {
85+
expect(extractName({ name: "app-universal.dmg" })).toBe("app");
86+
});
87+
88+
test("removes amd64 architecture", () => {
89+
expect(extractName({ name: "tool_amd64.deb" })).toBe("tool");
90+
});
91+
});
92+
93+
describe("extension removal", () => {
94+
test("removes tar.gz extension", () => {
95+
expect(extractName({ name: "app.tar.gz" })).toBe("app");
96+
});
97+
98+
test("removes tar.xz extension", () => {
99+
expect(extractName({ name: "tool.tar.xz" })).toBe("tool");
100+
});
101+
102+
test("removes zip extension", () => {
103+
expect(extractName({ name: "binary.zip" })).toBe("binary");
104+
});
105+
106+
test("removes dmg extension", () => {
107+
expect(extractName({ name: "App.dmg" })).toBe("App");
108+
});
109+
110+
test("removes pkg extension", () => {
111+
expect(extractName({ name: "installer.pkg" })).toBe("installer");
112+
});
113+
114+
test("removes deb extension", () => {
115+
expect(extractName({ name: "package.deb" })).toBe("package");
116+
});
117+
118+
test("removes app extension", () => {
119+
expect(extractName({ name: "MyApp.app" })).toBe("MyApp");
120+
});
121+
});
122+
123+
describe("combined scenarios", () => {
124+
test("handles version + platform + arch + extension", () => {
125+
const result = extractName({ name: "ripgrep-14.1.0-x86_64-apple-darwin.tar.gz" });
126+
expect(result.toLowerCase()).toContain("ripgrep");
127+
});
128+
129+
test("handles underscores as separators", () => {
130+
expect(extractName({ name: "my_tool_v1.0.0_darwin_arm64.tar.gz" })).toBe("my_tool");
131+
});
132+
133+
test("handles dashes as separators", () => {
134+
expect(extractName({ name: "my-tool-v1.0.0-darwin-arm64.tar.gz" })).toBe("my-tool");
135+
});
136+
137+
test("handles mixed separators", () => {
138+
expect(extractName({ name: "my-tool_v1.0.0-darwin_arm64.tar.gz" })).toBe("my-tool");
139+
});
140+
141+
test("handles real-world complex filenames", () => {
142+
expect(extractName({ name: "Pearcleaner.dmg" })).toBe("Pearcleaner");
143+
const vscodeResult = extractName({ name: "Visual Studio Code-darwin-universal.dmg" });
144+
expect(vscodeResult).toContain("Visual Studio Code");
145+
const itermResult = extractName({ name: "iTerm2-3_5_4.dmg" });
146+
expect(itermResult.toLowerCase()).toContain("iterm");
147+
const ytdlpResult = extractName({ name: "yt-dlp_macos" });
148+
expect(ytdlpResult.toLowerCase()).toContain("yt-dlp");
149+
const ffmpegResult = extractName({ name: "ffmpeg-6.1-macOS-default.zip" });
150+
expect(ffmpegResult.toLowerCase()).toContain("ffmpeg");
151+
});
152+
});
153+
154+
describe("edge cases", () => {
155+
test("handles names that are just version numbers", () => {
156+
const result = extractName({ name: "1.0.0.tar.gz" });
157+
expect(typeof result).toBe("string");
158+
});
159+
160+
test("handles single word names", () => {
161+
expect(extractName({ name: "git" })).toBe("git");
162+
});
163+
164+
test("handles names with multiple dashes", () => {
165+
expect(extractName({ name: "my-super-cool-tool.zip" })).toBe("my-super-cool-tool");
166+
});
167+
168+
test("handles names containing platform substrings", () => {
169+
const result = extractName({ name: "macchina-v6.3.1-macos-arm64.tar.gz" });
170+
expect(result).toBeDefined();
171+
expect(result.length).toBeGreaterThan(0);
172+
});
173+
174+
test("preserves case for app names", () => {
175+
expect(extractName({ name: "MyApp.dmg" })).toBe("MyApp");
176+
expect(extractName({ name: "UPPERCASE.pkg" })).toBe("UPPERCASE");
177+
});
178+
});
179+
180+
describe("not too aggressive", () => {
181+
test("does not strip too much from simple names", () => {
182+
expect(extractName({ name: "fzf.tar.gz" })).toBe("fzf");
183+
expect(extractName({ name: "git.zip" })).toBe("git");
184+
});
185+
186+
test("preserves meaningful parts of complex names", () => {
187+
expect(extractName({ name: "github-cli.tar.gz" })).toBe("github-cli");
188+
expect(extractName({ name: "docker-compose.zip" })).toBe("docker-compose");
189+
});
190+
191+
test("handles names starting with platform-like words", () => {
192+
const result = extractName({ name: "macports.tar.gz" });
193+
expect(result.length).toBeGreaterThan(0);
194+
});
195+
});
196+
});

lib/core/context.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
const os = require('os')
4+
const { createPlatformInfo, URL_TYPES } = require('./types')
5+
const { safeExecSync } = require('../utils')
6+
7+
class ModuleContext {
8+
constructor(sourceInput, options = {}) {
9+
this.originalInput = sourceInput
10+
this.options = options
11+
12+
this.platform = createPlatformInfo()
13+
this.capabilities = this._detectCapabilities()
14+
15+
this.urlType = null
16+
17+
this.github = null
18+
this.gitlab = null
19+
this.directLink = null
20+
this.localFile = null
21+
22+
this.sources = []
23+
this.selectedSource = null
24+
25+
this.tmpdir = null
26+
this.downloadPath = null
27+
this.extractedDir = null
28+
29+
this.installResult = null
30+
this.version = null
31+
this.installedName = null
32+
}
33+
34+
_detectCapabilities() {
35+
const ebool = (cmd) => {
36+
try {
37+
return safeExecSync(cmd, [], { stdio: 'ignore' }).length > 0
38+
} catch {
39+
return false
40+
}
41+
}
42+
43+
return {
44+
dmg: process.platform === 'darwin',
45+
pkg: process.platform === 'darwin',
46+
app: process.platform === 'darwin',
47+
deb: process.platform === 'linux' && ebool('which dpkg'),
48+
rpm: process.platform === 'linux' && ebool('which rpm'),
49+
'tar.zst': ebool('which unzstd') || ebool('which zstd')
50+
}
51+
}
52+
53+
addSource(sourceData) {
54+
this.sources.push(sourceData)
55+
this._sortSources()
56+
}
57+
58+
_sortSources() {
59+
this.sources.sort((a, b) => b.priority - a.priority)
60+
}
61+
62+
getCompatibleSources() {
63+
return this.sources.filter(s => s.archCompatible && s.platformCompatible && s.priority >= 0)
64+
}
65+
66+
getTopSource() {
67+
const compatible = this.getCompatibleSources()
68+
return compatible.length > 0 ? compatible[0] : null
69+
}
70+
71+
createTmpDir() {
72+
if (!this.tmpdir) {
73+
this.tmpdir = safeExecSync('mktemp', ['-d'], { encoding: 'utf8' }).trim()
74+
}
75+
return this.tmpdir
76+
}
77+
78+
cleanup() {
79+
if (this.tmpdir && fs.existsSync(this.tmpdir)) {
80+
try {
81+
fs.rmSync(this.tmpdir, { recursive: true, force: true })
82+
} catch {
83+
}
84+
}
85+
}
86+
}
87+
88+
module.exports = { ModuleContext }

lib/core/index.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const { Module } = require('./module')
2+
const { ModuleContext } = require('./context')
3+
const { ModuleRegistry } = require('./registry')
4+
const { ModuleRunner } = require('./runner')
5+
const { ModuleUtilities } = require('./utilities')
6+
const {
7+
SOURCE_TYPES,
8+
URL_TYPES,
9+
PHASES,
10+
createSource,
11+
createPlatformInfo
12+
} = require('./types')
13+
14+
module.exports = {
15+
Module,
16+
ModuleContext,
17+
ModuleRegistry,
18+
ModuleRunner,
19+
ModuleUtilities,
20+
21+
SOURCE_TYPES,
22+
URL_TYPES,
23+
PHASES,
24+
createSource,
25+
createPlatformInfo
26+
}

0 commit comments

Comments
 (0)