@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 +21 -0
- package/README.md +207 -0
- package/bin/kpm.js +2 -0
- package/index.ts +16 -0
- package/package.json +54 -0
- package/src/commands/add.ts +187 -0
- package/src/lib/findGradleFile.ts +41 -0
- package/src/lib/googleMaven.ts +48 -0
- package/src/lib/gradleWriter.ts +59 -0
- package/src/lib/mavenCentral.ts +100 -0
- package/src/lib/ranking.ts +25 -0
- package/src/lib/specialDeps.ts +59 -0
- package/src/lib/types.ts +14 -0
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](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
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
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -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
|
+
}
|