@hmh-3080/kpm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 kpm contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # kpm
2
+
3
+ > Kotlin/Java package manager for Gradle. Search, pick, done.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Bun](https://img.shields.io/badge/bun-1.3+-fbf0df?logo=bun)](https://bun.sh)
7
+
8
+ kpm is a CLI tool that lets you search Maven Central and Google Maven interactively, pick a version, and add the dependency to your `build.gradle` or `build.gradle.kts` — including required plugins and annotation processors.
9
+
10
+ ```
11
+ $ kpm add room
12
+ ┌ kpm add
13
+
14
+ ◇ Search complete
15
+
16
+ ◆ Choose a library (12 results)
17
+ │ ● androidx.room:room-runtime (2.8.4 • 45 versions • aar • 2d ago)
18
+ │ ○ androidx.room:room-ktx
19
+ │ ○ androidx.room:room-compiler
20
+ │ ○ → Refine search...
21
+
22
+ ◆ Choose a version
23
+ │ 2.8.4 ✓ latest stable
24
+ │ 2.8.3
25
+ │ 2.8.2
26
+ │ ── pre-release ──
27
+ │ 2.9.0-alpha01
28
+
29
+ ◇ Added androidx.room:room-runtime:2.8.4 to app/build.gradle.kts
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Features
35
+
36
+ - **Search across sources** — queries Maven Central and Google Maven in parallel
37
+ - **Ranked results** — official/first-party libraries appear first, sorted by popularity
38
+ - **Smart hints** — shows version count, packaging type, and last update for each result
39
+ - **Stable vs pre-release** — clearly separated in the version picker
40
+ - **Auto plugin management** — detects libraries that need KSP, kapt, or Compose plugins and adds them automatically
41
+ - **Auto processor detection** — adds annotation processors (Room compiler, Hilt compiler, etc.) alongside the main dependency
42
+ - **Idempotent** — running `kpm add` twice won't duplicate entries
43
+ - **Multi-module support** — detects and lets you choose which module to add the dependency to
44
+ - **Refine search** — if too many results, refine your query without restarting
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ Requires [Bun](https://bun.sh) v1.3+.
51
+
52
+ ```bash
53
+ # Clone
54
+ git clone https://github.com/HMH-3080/kpm.git
55
+ cd kpm
56
+
57
+ # Install dependencies
58
+ bun install
59
+
60
+ # Run directly
61
+ bun run index.ts add <library>
62
+
63
+ # Or build a standalone binary
64
+ bun run build
65
+ ./kpm add <library>
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Usage
71
+
72
+ ### Add a dependency
73
+
74
+ ```bash
75
+ # Search and add interactively
76
+ kpm add coil-compose
77
+
78
+ # Specify a module
79
+ kpm add retrofit --module app
80
+
81
+ # Search with a specific query
82
+ kpm add "ktor client"
83
+ ```
84
+
85
+ ### How it works
86
+
87
+ 1. **Search** — queries Maven Central (Solr API) and Google Maven (XML API) in parallel
88
+ 2. **Pick library** — shows top 10 results ranked by popularity; use `→ Refine search...` to narrow down
89
+ 3. **Pick version** — stable versions listed first, pre-release clearly marked
90
+ 4. **Choose module** — if multiple modules exist, pick one
91
+ 5. **Write** — adds `implementation(...)` line (and plugins/processors if needed)
92
+
93
+ ---
94
+
95
+ ## Auto-detected libraries
96
+
97
+ These libraries get automatic plugin and processor handling:
98
+
99
+ | Library | Plugin | Processor |
100
+ |---------|--------|-----------|
101
+ | `androidx.room:room-runtime` | KSP | `room-compiler` (ksp) |
102
+ | `androidx.room:room-ktx` | KSP | `room-compiler` (ksp) |
103
+ | `com.google.dagger:hilt-android` | Hilt | `hilt-android-compiler` (kapt) |
104
+ | `com.google.dagger:dagger` | KSP | `dagger-compiler` (ksp) |
105
+ | `com.squareup.moshi:moshi-kotlin` | — | `moshi-kotlin-codegen` (ksp) |
106
+ | `androidx.compose.ui:ui` | Compose | — |
107
+ | `androidx.compose.material3:material3` | Compose | — |
108
+ | `androidx.compose.runtime:runtime` | Compose | — |
109
+ | `androidx.compose.foundation:foundation` | Compose | — |
110
+ | `org.jetbrains.kotlinx:kotlinx-serialization-json` | Serialization | — |
111
+ | `com.github.bumptech.glide:glide` | KSP | `glide:ksp` |
112
+
113
+ For example, adding `room-runtime` will:
114
+
115
+ 1. Add `id("com.google.devtools.ksp")` to the `plugins {}` block
116
+ 2. Add `implementation("androidx.room:room-runtime:2.8.4")` to `dependencies {}`
117
+ 3. Add `ksp("androidx.room:room-compiler:2.8.4")` to `dependencies {}`
118
+
119
+ ---
120
+
121
+ ## Ranking system
122
+
123
+ Search results are ranked by:
124
+
125
+ 1. **Group match** — libraries whose group ID matches the query get a massive boost (official libraries first)
126
+ 2. **Version count** — more versions = more mature project
127
+ 3. **Recency** — recently updated libraries score higher
128
+
129
+ This ensures `io.ktor:ktor` appears before third-party wrappers when you search for "ktor".
130
+
131
+ ---
132
+
133
+ ## Project structure
134
+
135
+ ```
136
+ kpm/
137
+ ├── index.ts # CLI entry point
138
+ ├── src/
139
+ │ ├── commands/
140
+ │ │ └── add.ts # Add command orchestrator
141
+ │ └── lib/
142
+ │ ├── types.ts # Artifact, SpecialDep interfaces
143
+ │ ├── ranking.ts # Popularity scoring algorithm
144
+ │ ├── mavenCentral.ts # Maven Central Solr API + metadata.xml
145
+ │ ├── googleMaven.ts # Google Maven XML API
146
+ │ ├── findGradleFile.ts # Recursive walk for build.gradle(.kts)
147
+ │ ├── gradleWriter.ts # Writes dependencies into Gradle files
148
+ │ └── specialDeps.ts # Auto-detected plugin/processor map
149
+ ├── ARCHITECTURE.md # Detailed architecture and known issues
150
+ ├── IMPROVEMENTS.md # Roadmap and improvement proposals
151
+ └── package.json
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Dependencies
157
+
158
+ | Package | Purpose |
159
+ |---------|---------|
160
+ | `commander` | CLI framework |
161
+ | `@clack/prompts` | Interactive terminal UI |
162
+ | `picocolors` | Terminal colors |
163
+
164
+ Zero runtime dependencies beyond these three.
165
+
166
+ ---
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ # Install
172
+ bun install
173
+
174
+ # Run in dev mode
175
+ bun run dev
176
+
177
+ # Build standalone binary
178
+ bun run build
179
+
180
+ # Type check
181
+ bunx tsc --noEmit
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Contributing
187
+
188
+ 1. Fork the repo
189
+ 2. Create a feature branch
190
+ 3. Make your changes
191
+ 4. Run `bunx tsc --noEmit` to verify types
192
+ 5. Submit a PR
193
+
194
+ See `ARCHITECTURE.md` for codebase structure and `IMPROVEMENTS.md` for planned features.
195
+
196
+ ---
197
+
198
+ ## Documentation
199
+
200
+ - [`ARCHITECTURE.md`](./ARCHITECTURE.md) — codebase structure, known issues, and resolved bugs
201
+ - [`IMPROVEMENTS.md`](./IMPROVEMENTS.md) — roadmap, API analysis, and improvement proposals
202
+
203
+ ---
204
+
205
+ ## License
206
+
207
+ MIT
package/bin/kpm.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../index.ts";
package/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import { addCommand } from "./src/commands/add";
4
+
5
+ const program = new Command();
6
+
7
+ program.name("kpm").description("Kotlin/Java Package Manager for Gradle").version("1.0.0");
8
+
9
+ program
10
+ .command("add")
11
+ .description("Search and add a dependency to your Gradle project")
12
+ .argument("<query>", "Library name to search for")
13
+ .option("-m, --module <path>", "Target module directory")
14
+ .action(addCommand);
15
+
16
+ program.parseAsync();
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@hmh-3080/kpm",
3
+ "version": "1.0.0",
4
+ "description": "Kotlin/Java package manager for Gradle. Search Maven Central & Google Maven interactively, pick a version, and add the dependency — including plugins and annotation processors.",
5
+ "type": "module",
6
+ "bin": {
7
+ "kpm": "./bin/kpm.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "bun run index.ts",
11
+ "build": "bun build ./index.ts --compile --minify --outfile kpm"
12
+ },
13
+ "keywords": [
14
+ "kotlin",
15
+ "java",
16
+ "gradle",
17
+ "maven",
18
+ "package-manager",
19
+ "dependency",
20
+ "android",
21
+ "compose",
22
+ "room",
23
+ "hilt",
24
+ "ktor"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/HMH-3080/kpm.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/HMH-3080/kpm/issues"
34
+ },
35
+ "homepage": "https://github.com/HMH-3080/kpm#readme",
36
+ "engines": {
37
+ "bun": ">=1.3.0"
38
+ },
39
+ "files": [
40
+ "index.ts",
41
+ "bin/",
42
+ "src/",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "dependencies": {
47
+ "commander": "^12.1.0",
48
+ "@clack/prompts": "^0.7.0",
49
+ "picocolors": "^1.0.1"
50
+ },
51
+ "devDependencies": {
52
+ "@types/bun": "latest"
53
+ }
54
+ }
@@ -0,0 +1,187 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { searchMavenCentral, getMavenCentralVersions } from "../lib/mavenCentral";
4
+ import { searchGoogleMaven, getGoogleMavenVersions } from "../lib/googleMaven";
5
+ import { findGradleModules } from "../lib/findGradleFile";
6
+ import { addDependency } from "../lib/gradleWriter";
7
+ import { resolveSpecialDep } from "../lib/specialDeps";
8
+ import type { Artifact } from "../lib/types";
9
+
10
+ const PAGE_SIZE = 10;
11
+
12
+ function formatHint(a: Artifact): string {
13
+ const parts = [a.latestVersion];
14
+ if (a.versionCount) parts.push(`${a.versionCount} versions`);
15
+ if (a.packaging) parts.push(a.packaging);
16
+ if (a.lastUpdate) {
17
+ const days = Math.floor((Date.now() - a.lastUpdate) / 86_400_000);
18
+ if (days === 0) parts.push("today");
19
+ else if (days === 1) parts.push("1d ago");
20
+ else if (days < 30) parts.push(`${days}d ago`);
21
+ else if (days < 365) parts.push(`${Math.floor(days / 30)}mo ago`);
22
+ else parts.push(`${Math.floor(days / 365)}y ago`);
23
+ }
24
+ return parts.join(" • ");
25
+ }
26
+
27
+ const REFINE = Symbol("REFINE");
28
+
29
+ async function searchAll(query: string): Promise<Artifact[]> {
30
+ const spinner = p.spinner();
31
+ spinner.start("Searching Maven Central and Google Maven");
32
+
33
+ const [centralResults, googleResults] = await Promise.all([
34
+ searchMavenCentral(query),
35
+ searchGoogleMaven(query),
36
+ ]);
37
+
38
+ spinner.stop("Search complete");
39
+
40
+ const all = [...googleResults, ...centralResults];
41
+ const seen = new Set<string>();
42
+ return all.filter((a) => {
43
+ const key = `${a.group}:${a.artifact}`;
44
+ if (seen.has(key)) return false;
45
+ seen.add(key);
46
+ return true;
47
+ });
48
+ }
49
+
50
+ async function pickArtifact(initialQuery: string): Promise<Artifact | null> {
51
+ let query = initialQuery;
52
+
53
+ while (true) {
54
+ const results = await searchAll(query);
55
+
56
+ if (results.length === 0) {
57
+ p.log.error(`No artifacts found for "${query}"`);
58
+ const retry = await p.text({
59
+ message: "Try a different search?",
60
+ placeholder: "e.g. retrofit, glide, room",
61
+ });
62
+ if (p.isCancel(retry)) return null;
63
+ query = retry;
64
+ continue;
65
+ }
66
+
67
+ const page = results.slice(0, PAGE_SIZE);
68
+ const hasMore = results.length > PAGE_SIZE;
69
+
70
+ const options = [
71
+ ...page.map((a) => ({
72
+ value: a as Artifact | typeof REFINE,
73
+ label: `${a.group}:${a.artifact}`,
74
+ hint: formatHint(a),
75
+ })),
76
+ ...(hasMore
77
+ ? [{ value: REFINE as Artifact | typeof REFINE, label: pc.dim("→ Refine search..."), hint: pc.dim(`${results.length} total results`) }]
78
+ : []),
79
+ ];
80
+
81
+ const selected = await p.select({
82
+ message: `Choose a library (${results.length} results)`,
83
+ options,
84
+ });
85
+
86
+ if (p.isCancel(selected)) return null;
87
+
88
+ if (selected === REFINE) {
89
+ const refined = await p.text({
90
+ message: "Refine your search",
91
+ placeholder: query,
92
+ defaultValue: query,
93
+ });
94
+ if (p.isCancel(refined)) return null;
95
+ query = refined;
96
+ continue;
97
+ }
98
+
99
+ return selected as Artifact;
100
+ }
101
+ }
102
+
103
+ export async function addCommand(query: string, options: { module?: string }) {
104
+ p.intro(pc.bgCyan(pc.black(" kpm add ")));
105
+
106
+ try {
107
+ const artifact = await pickArtifact(query);
108
+
109
+ if (!artifact) {
110
+ p.cancel("Cancelled");
111
+ return;
112
+ }
113
+
114
+ const versionsSpinner = p.spinner();
115
+ versionsSpinner.start("Fetching versions");
116
+
117
+ const rawVersions =
118
+ artifact.source === "google"
119
+ ? await getGoogleMavenVersions(artifact.group, artifact.artifact)
120
+ : await getMavenCentralVersions(artifact.group, artifact.artifact);
121
+
122
+ versionsSpinner.stop("Versions fetched");
123
+
124
+ const stableVersions = rawVersions.filter(
125
+ (v) => !/(alpha|beta|rc|^m\d|dev|snapshot|preview|eap)/i.test(v)
126
+ );
127
+ const preReleaseVersions = rawVersions.filter(
128
+ (v) => /(alpha|beta|rc|^m\d|dev|snapshot|preview|eap)/i.test(v)
129
+ );
130
+ const latestStable = stableVersions[0] ?? rawVersions[0] ?? artifact.latestVersion;
131
+
132
+ const versionOptions = [
133
+ ...stableVersions.slice(0, 10).map((v) => ({
134
+ value: v,
135
+ label: v,
136
+ hint: v === latestStable ? pc.green("latest stable") : undefined,
137
+ })),
138
+ ...preReleaseVersions.slice(0, 5).map((v) => ({
139
+ value: v,
140
+ label: v,
141
+ hint: pc.yellow("pre-release"),
142
+ })),
143
+ ];
144
+
145
+ const selectedVersion = await p.select({
146
+ message: "Choose a version",
147
+ options: versionOptions,
148
+ });
149
+
150
+ if (p.isCancel(selectedVersion)) {
151
+ p.cancel("Cancelled");
152
+ return;
153
+ }
154
+
155
+ const modules = findGradleModules(options.module ?? process.cwd());
156
+
157
+ if (modules.length === 0) {
158
+ p.outro(pc.red("No build.gradle or build.gradle.kts found"));
159
+ return;
160
+ }
161
+
162
+ let targetModule = modules[0];
163
+
164
+ if (modules.length > 1) {
165
+ const moduleChoice = await p.select({
166
+ message: "Choose a module",
167
+ options: modules.map((m) => ({ value: m, label: m.path })),
168
+ });
169
+ if (p.isCancel(moduleChoice)) {
170
+ p.cancel("Cancelled");
171
+ return;
172
+ }
173
+ targetModule = moduleChoice as typeof modules[number];
174
+ }
175
+
176
+ const special = resolveSpecialDep(artifact.group, artifact.artifact);
177
+
178
+ addDependency(targetModule, artifact.group, artifact.artifact, selectedVersion as string, special);
179
+
180
+ p.outro(
181
+ pc.green(`Added ${pc.bold(`${artifact.group}:${artifact.artifact}:${selectedVersion}`)} to ${targetModule.path}`)
182
+ );
183
+ } catch (err) {
184
+ p.cancel(err instanceof Error ? err.message : String(err));
185
+ process.exit(1);
186
+ }
187
+ }
@@ -0,0 +1,41 @@
1
+ import { readdirSync, statSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ export interface GradleModule {
5
+ path: string;
6
+ kotlinDsl: boolean;
7
+ }
8
+
9
+ export function findGradleModules(root: string): GradleModule[] {
10
+ const modules: GradleModule[] = [];
11
+ const ignore = new Set(["node_modules", ".git", "build", ".gradle", ".idea"]);
12
+
13
+ function walk(dir: string) {
14
+ let entries: string[];
15
+ try {
16
+ entries = readdirSync(dir);
17
+ } catch {
18
+ return;
19
+ }
20
+ for (const entry of entries) {
21
+ if (ignore.has(entry)) continue;
22
+ const full = join(dir, entry);
23
+ let stats;
24
+ try {
25
+ stats = statSync(full);
26
+ } catch {
27
+ continue;
28
+ }
29
+ if (stats.isDirectory()) {
30
+ walk(full);
31
+ } else if (entry === "build.gradle.kts") {
32
+ modules.push({ path: full, kotlinDsl: true });
33
+ } else if (entry === "build.gradle") {
34
+ modules.push({ path: full, kotlinDsl: false });
35
+ }
36
+ }
37
+ }
38
+
39
+ walk(root);
40
+ return modules;
41
+ }
@@ -0,0 +1,48 @@
1
+ import type { Artifact } from "./types";
2
+ import { sortVersionsDesc } from "./mavenCentral";
3
+ import { computeScore } from "./ranking";
4
+
5
+ let masterIndexCache: string[] | null = null;
6
+
7
+ async function getGroups(): Promise<string[]> {
8
+ if (masterIndexCache) return masterIndexCache;
9
+ const res = await fetch("https://maven.google.com/master-index.xml");
10
+ const xml = await res.text();
11
+ const matches = [...xml.matchAll(/<([\w.\-]+)\/?>/g)].map((m) => m[1]);
12
+ masterIndexCache = matches.filter((g) => g !== "metadata");
13
+ return masterIndexCache;
14
+ }
15
+
16
+ export async function searchGoogleMaven(query: string): Promise<Artifact[]> {
17
+ const groups = await getGroups();
18
+ const lower = query.toLowerCase();
19
+ const matchedGroups = groups.filter((g) => g.toLowerCase().includes(lower)).slice(0, 8);
20
+ const results: Artifact[] = [];
21
+ await Promise.all(
22
+ matchedGroups.map(async (group) => {
23
+ const path = group.replace(/\./g, "/");
24
+ const res = await fetch(`https://maven.google.com/${path}/group-index.xml`);
25
+ if (!res.ok) return;
26
+ const xml = await res.text();
27
+ const entries = [...xml.matchAll(/<([\w.\-]+)\s+versions="([^"]+)"/g)];
28
+ for (const [, artifact, versions] of entries) {
29
+ if (artifact.toLowerCase().includes(lower) || group.toLowerCase().includes(lower)) {
30
+ const versionList = versions.split(",");
31
+ results.push({ group, artifact, latestVersion: sortVersionsDesc(versionList)[0], source: "google" });
32
+ }
33
+ }
34
+ })
35
+ );
36
+ results.sort((a, b) => computeScore({ group: b.group, artifact: b.artifact, versionCount: 0, timestamp: 0 }, query) - computeScore({ group: a.group, artifact: a.artifact, versionCount: 0, timestamp: 0 }, query));
37
+ return results;
38
+ }
39
+
40
+ export async function getGoogleMavenVersions(group: string, artifact: string): Promise<string[]> {
41
+ const path = group.replace(/\./g, "/");
42
+ const res = await fetch(`https://maven.google.com/${path}/group-index.xml`);
43
+ if (!res.ok) return [];
44
+ const xml = await res.text();
45
+ const match = xml.match(new RegExp(`<${artifact}\\s+versions="([^"]+)"`));
46
+ if (!match) return [];
47
+ return sortVersionsDesc(match[1].split(","));
48
+ }
@@ -0,0 +1,59 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+ import type { GradleModule } from "./findGradleFile";
3
+ import type { SpecialDep } from "./types";
4
+
5
+ function formatDependencyLine(kotlinDsl: boolean, scope: string, coordinate: string): string {
6
+ return kotlinDsl ? ` ${scope}("${coordinate}")` : ` ${scope} '${coordinate}'`;
7
+ }
8
+
9
+ function insertIntoBlock(content: string, blockName: string, line: string): string {
10
+ const regex = new RegExp(`(${blockName}\\s*\\{)`);
11
+ const match = content.match(regex);
12
+ if (match) {
13
+ const index = match.index! + match[0].length;
14
+ return content.slice(0, index) + "\n" + line + content.slice(index);
15
+ }
16
+ return content + `\n${blockName} {\n${line}\n}\n`;
17
+ }
18
+
19
+ function ensurePlugin(content: string, kotlinDsl: boolean, pluginId: string, version?: string): string {
20
+ if (content.includes(pluginId)) return content;
21
+ const line = kotlinDsl
22
+ ? version
23
+ ? ` id("${pluginId}") version "${version}"`
24
+ : ` id("${pluginId}")`
25
+ : version
26
+ ? ` id '${pluginId}' version '${version}'`
27
+ : ` id '${pluginId}'`;
28
+ return insertIntoBlock(content, "plugins", line);
29
+ }
30
+
31
+ export function addDependency(
32
+ module: GradleModule,
33
+ group: string,
34
+ artifact: string,
35
+ version: string,
36
+ special?: SpecialDep
37
+ ): void {
38
+ let content = readFileSync(module.path, "utf-8");
39
+
40
+ if (special?.plugin) {
41
+ content = ensurePlugin(content, module.kotlinDsl, special.plugin.id, special.plugin.version);
42
+ }
43
+
44
+ const coordinate = `${group}:${artifact}:${version}`;
45
+ if (!content.includes(coordinate)) {
46
+ const mainLine = formatDependencyLine(module.kotlinDsl, "implementation", coordinate);
47
+ content = insertIntoBlock(content, "dependencies", mainLine);
48
+ }
49
+
50
+ if (special?.processor) {
51
+ const processorCoordinate = `${special.processor.coordinate}:${version}`;
52
+ if (!content.includes(processorCoordinate)) {
53
+ const processorLine = formatDependencyLine(module.kotlinDsl, special.processor.type, processorCoordinate);
54
+ content = insertIntoBlock(content, "dependencies", processorLine);
55
+ }
56
+ }
57
+
58
+ writeFileSync(module.path, content, "utf-8");
59
+ }
@@ -0,0 +1,100 @@
1
+ import type { Artifact } from "./types";
2
+ import { computeScore } from "./ranking";
3
+
4
+ function normalizeVersion(v: string): string {
5
+ return v.replace(/[-+]/g, ".").replace(/\.\.+/g, ".");
6
+ }
7
+
8
+ function compareVersions(a: string, b: string): number {
9
+ const isPreReleaseA = /(alpha|beta|rc|^m\d|dev|snapshot|preview|eap)/i.test(a);
10
+ const isPreReleaseB = /(alpha|beta|rc|^m\d|dev|snapshot|preview|eap)/i.test(b);
11
+ if (isPreReleaseA !== isPreReleaseB) return isPreReleaseA ? -1 : 1;
12
+
13
+ const partsA = normalizeVersion(a).split(".").map((p) => (isNaN(Number(p)) ? p : Number(p)));
14
+ const partsB = normalizeVersion(b).split(".").map((p) => (isNaN(Number(p)) ? p : Number(p)));
15
+ const len = Math.max(partsA.length, partsB.length);
16
+
17
+ for (let i = 0; i < len; i++) {
18
+ const pa = partsA[i];
19
+ const pb = partsB[i];
20
+ if (pa === undefined) return -1;
21
+ if (pb === undefined) return 1;
22
+ if (typeof pa === "number" && typeof pb === "number") {
23
+ if (pa !== pb) return pa - pb;
24
+ } else {
25
+ const sa = String(pa);
26
+ const sb = String(pb);
27
+ if (sa !== sb) return sa.localeCompare(sb);
28
+ }
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ export function sortVersionsDesc(versions: string[]): string[] {
34
+ return [...versions].sort(compareVersions).reverse();
35
+ }
36
+
37
+ interface RawDoc {
38
+ g: string;
39
+ a: string;
40
+ latestVersion: string;
41
+ timestamp: number;
42
+ versionCount: number;
43
+ packaging: string;
44
+ }
45
+
46
+ export async function searchMavenCentral(query: string): Promise<Artifact[]> {
47
+ const url = `https://search.maven.org/solrsearch/select?q=${encodeURIComponent(query)}&rows=30&wt=json`;
48
+ const res = await fetch(url);
49
+ if (!res.ok) return [];
50
+ const data = await res.json();
51
+
52
+ const docs: RawDoc[] = data.response.docs.map((doc: any) => ({
53
+ g: doc.g,
54
+ a: doc.a,
55
+ latestVersion: doc.latestVersion,
56
+ timestamp: doc.timestamp ?? 0,
57
+ versionCount: doc.versionCount ?? 0,
58
+ packaging: doc.p ?? "jar",
59
+ }));
60
+
61
+ const seen = new Set<string>();
62
+ const unique = docs.filter((d) => {
63
+ const key = `${d.g}:${d.a}`;
64
+ if (seen.has(key)) return false;
65
+ seen.add(key);
66
+ return true;
67
+ });
68
+
69
+ unique.sort((a, b) => computeScore({ group: b.g, artifact: b.a, versionCount: b.versionCount, timestamp: b.timestamp }, query) - computeScore({ group: a.g, artifact: a.a, versionCount: a.versionCount, timestamp: a.timestamp }, query));
70
+
71
+ return unique.map((d) => ({
72
+ group: d.g,
73
+ artifact: d.a,
74
+ latestVersion: d.latestVersion,
75
+ source: "mavenCentral" as const,
76
+ versionCount: d.versionCount,
77
+ packaging: d.packaging,
78
+ lastUpdate: d.timestamp,
79
+ }));
80
+ }
81
+
82
+ export async function getMavenCentralVersions(group: string, artifact: string): Promise<string[]> {
83
+ const path = group.replace(/\./g, "/");
84
+ const metadataUrl = `https://repo1.maven.org/maven2/${path}/${artifact}/maven-metadata.xml`;
85
+
86
+ try {
87
+ const res = await fetch(metadataUrl);
88
+ if (res.ok) {
89
+ const xml = await res.text();
90
+ const versions = [...xml.matchAll(/<version>([^<]+)<\/version>/g)].map((m) => m[1]);
91
+ if (versions.length > 0) return sortVersionsDesc(versions);
92
+ }
93
+ } catch {}
94
+
95
+ const fallbackUrl = `https://search.maven.org/solrsearch/select?q=g:%22${encodeURIComponent(group)}%22+AND+a:%22${encodeURIComponent(artifact)}%22&core=gav&rows=200&wt=json`;
96
+ const fallbackRes = await fetch(fallbackUrl);
97
+ if (!fallbackRes.ok) return [];
98
+ const data = await fallbackRes.json();
99
+ return sortVersionsDesc(data.response.docs.map((doc: any) => doc.v));
100
+ }
@@ -0,0 +1,25 @@
1
+ export interface RankingMetrics {
2
+ group: string;
3
+ artifact: string;
4
+ versionCount: number;
5
+ timestamp: number;
6
+ }
7
+
8
+ export function computeScore(m: RankingMetrics, query?: string): number {
9
+ const daysSinceUpdate = (Date.now() - m.timestamp) / 86_400_000;
10
+ const recency = Math.max(0, 1 - daysSinceUpdate / 365);
11
+ const base = m.versionCount * 0.6 + recency * 100 * 0.4;
12
+
13
+ if (!query) return base;
14
+
15
+ const q = query.toLowerCase();
16
+ const g = m.group.toLowerCase();
17
+ const a = m.artifact.toLowerCase();
18
+
19
+ if (g === q || g.startsWith(q + ".") || g.startsWith(q + ":")) return base + 10000;
20
+ if (g.includes(q)) return base + 5000;
21
+ if (a.startsWith(q)) return base + 2000;
22
+ if (a.includes(q)) return base + 1000;
23
+
24
+ return base;
25
+ }
@@ -0,0 +1,59 @@
1
+ import type { SpecialDep } from "./types";
2
+
3
+ export const specialDeps: Record<string, SpecialDep> = {
4
+ // Room
5
+ "androidx.room:room-runtime": {
6
+ plugin: { id: "com.google.devtools.ksp" },
7
+ processor: { coordinate: "androidx.room:room-compiler", type: "ksp" },
8
+ },
9
+ "androidx.room:room-ktx": {
10
+ plugin: { id: "com.google.devtools.ksp" },
11
+ processor: { coordinate: "androidx.room:room-compiler", type: "ksp" },
12
+ },
13
+
14
+ // Hilt
15
+ "com.google.dagger:hilt-android": {
16
+ plugin: { id: "com.google.dagger.hilt.android" },
17
+ processor: { coordinate: "com.google.dagger:hilt-android-compiler", type: "kapt" },
18
+ },
19
+
20
+ // Dagger (non-Hilt)
21
+ "com.google.dagger:dagger": {
22
+ plugin: { id: "com.google.devtools.ksp" },
23
+ processor: { coordinate: "com.google.dagger:dagger-compiler", type: "ksp" },
24
+ },
25
+
26
+ // Moshi
27
+ "com.squareup.moshi:moshi-kotlin": {
28
+ processor: { coordinate: "com.squareup.moshi:moshi-kotlin-codegen", type: "ksp" },
29
+ },
30
+
31
+ // Compose
32
+ "androidx.compose.ui:ui": {
33
+ plugin: { id: "org.jetbrains.kotlin.plugin.compose" },
34
+ },
35
+ "androidx.compose.material3:material3": {
36
+ plugin: { id: "org.jetbrains.kotlin.plugin.compose" },
37
+ },
38
+ "androidx.compose.runtime:runtime": {
39
+ plugin: { id: "org.jetbrains.kotlin.plugin.compose" },
40
+ },
41
+ "androidx.compose.foundation:foundation": {
42
+ plugin: { id: "org.jetbrains.kotlin.plugin.compose" },
43
+ },
44
+
45
+ // Kotlin Serialization
46
+ "org.jetbrains.kotlinx:kotlinx-serialization-json": {
47
+ plugin: { id: "org.jetbrains.kotlin.plugin.serialization" },
48
+ },
49
+
50
+ // Glide
51
+ "com.github.bumptech.glide:glide": {
52
+ plugin: { id: "com.google.devtools.ksp" },
53
+ processor: { coordinate: "com.github.bumptech.glide:ksp", type: "ksp" },
54
+ },
55
+ };
56
+
57
+ export function resolveSpecialDep(group: string, artifact: string): SpecialDep | undefined {
58
+ return specialDeps[`${group}:${artifact}`];
59
+ }
@@ -0,0 +1,14 @@
1
+ export interface Artifact {
2
+ group: string;
3
+ artifact: string;
4
+ latestVersion: string;
5
+ source: "mavenCentral" | "google";
6
+ versionCount?: number;
7
+ packaging?: string;
8
+ lastUpdate?: number;
9
+ }
10
+
11
+ export interface SpecialDep {
12
+ plugin?: { id: string; version?: string };
13
+ processor?: { coordinate: string; type: "ksp" | "kapt" };
14
+ }