jvm-upgrade 0.1.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.
@@ -0,0 +1,65 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { basename, dirname, join, resolve, sep } from 'node:path';
3
+ import { scanGradle } from './gradle.js';
4
+ import { scanMaven } from './maven.js';
5
+ const SKIP_DIRS = new Set(['build', 'node_modules', 'target', 'out', 'bin', 'intermediates']);
6
+ const SETTINGS = new Set(['settings.gradle', 'settings.gradle.kts']);
7
+ const BUILD_FILES = new Set(['build.gradle', 'build.gradle.kts']);
8
+ async function walk(dir, files) {
9
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
10
+ await Promise.all(entries.map(async (entry) => {
11
+ const path = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ if (!entry.name.startsWith('.') && !SKIP_DIRS.has(entry.name))
14
+ await walk(path, files);
15
+ }
16
+ else if (SETTINGS.has(entry.name) || BUILD_FILES.has(entry.name) || entry.name === 'pom.xml') {
17
+ files.push(path);
18
+ }
19
+ }));
20
+ }
21
+ function isUnder(dir, root) {
22
+ return dir === root || dir.startsWith(root + sep);
23
+ }
24
+ function depth(path) {
25
+ return path.split(sep).length;
26
+ }
27
+ export function groupGradleBuilds(files) {
28
+ const builds = new Map();
29
+ for (const file of files.filter((f) => SETTINGS.has(basename(f)))) {
30
+ builds.set(dirname(file), { root: dirname(file), settings: file, buildFiles: [] });
31
+ }
32
+ const buildFiles = files.filter((f) => BUILD_FILES.has(basename(f))).sort((a, b) => depth(a) - depth(b));
33
+ for (const file of buildFiles) {
34
+ const dir = dirname(file);
35
+ const owner = [...builds.keys()].filter((root) => isUnder(dir, root)).sort((a, b) => b.length - a.length)[0];
36
+ const root = owner && basename(dir) !== 'buildSrc' ? owner : dir;
37
+ if (!builds.has(root))
38
+ builds.set(root, { root, buildFiles: [] });
39
+ builds.get(root).buildFiles.push(file);
40
+ }
41
+ return [...builds.values()];
42
+ }
43
+ function dedupe(deps) {
44
+ const seen = new Map();
45
+ for (const dep of deps) {
46
+ const where = dep.location ? `${dep.location.file}:${dep.location.start}` : `${dep.file}:${dep.version}`;
47
+ const key = `${dep.kind}|${dep.group}|${dep.artifact}|${where}`;
48
+ if (!seen.has(key))
49
+ seen.set(key, dep);
50
+ }
51
+ return [...seen.values()];
52
+ }
53
+ export async function scanProject(root) {
54
+ const files = [];
55
+ await walk(resolve(root), files);
56
+ files.sort();
57
+ const results = await Promise.all([
58
+ ...groupGradleBuilds(files).map(scanGradle),
59
+ scanMaven(files.filter((f) => basename(f) === 'pom.xml')),
60
+ ]);
61
+ return {
62
+ dependencies: dedupe(results.flatMap((r) => r.dependencies)),
63
+ warnings: [...new Set(results.flatMap((r) => r.warnings))],
64
+ };
65
+ }
@@ -0,0 +1,201 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { isDynamic } from './gradle.js';
4
+ import { dedupeRepos, MAVEN_CENTRAL } from './repos.js';
5
+ const TAG_RE = /<(\/?)([\w.\-:]+)(?:\s[^>]*?)?(\/?)>/g;
6
+ const SELF_VERSION_RE = /\$\{(?:project\.|pom\.)?(?:parent\.)?version\}/;
7
+ export function stripXmlComments(src) {
8
+ return src.replace(/<!--[\s\S]*?-->/g, (c) => c.replace(/[^\n]/g, ' '));
9
+ }
10
+ export function parseXml(code) {
11
+ const root = { name: '#root', children: [], textStart: 0, textEnd: code.length };
12
+ const stack = [root];
13
+ for (const m of code.matchAll(TAG_RE)) {
14
+ const [tag, closing, name, selfClosing] = m;
15
+ const top = stack[stack.length - 1];
16
+ if (closing) {
17
+ const idx = stack.map((n) => n.name).lastIndexOf(name);
18
+ if (idx > 0) {
19
+ stack[idx].textEnd = m.index;
20
+ stack.length = idx;
21
+ }
22
+ continue;
23
+ }
24
+ const start = m.index + tag.length;
25
+ const node = { name: name, parent: top, children: [], textStart: start, textEnd: start };
26
+ top.children.push(node);
27
+ if (!selfClosing)
28
+ stack.push(node);
29
+ }
30
+ return root;
31
+ }
32
+ function child(node, name) {
33
+ return node?.children.find((c) => c.name === name);
34
+ }
35
+ function findAll(node, name, parentName) {
36
+ const out = [];
37
+ const visit = (n) => {
38
+ for (const c of n.children) {
39
+ if (c.name === name && n.name === parentName)
40
+ out.push(c);
41
+ visit(c);
42
+ }
43
+ };
44
+ visit(node);
45
+ return out;
46
+ }
47
+ function textOf(pom, node) {
48
+ if (!node || node.children.length)
49
+ return undefined;
50
+ const raw = pom.code.slice(node.textStart, node.textEnd);
51
+ const value = raw.trim();
52
+ if (!value)
53
+ return undefined;
54
+ const start = node.textStart + raw.indexOf(value);
55
+ return { value, loc: { file: pom.file, start, end: start + value.length } };
56
+ }
57
+ function childText(pom, node, name) {
58
+ return textOf(pom, child(node, name))?.value;
59
+ }
60
+ async function loadPom(file) {
61
+ let content;
62
+ try {
63
+ content = await readFile(file, 'utf8');
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ const code = stripXmlComments(content);
69
+ const project = child(parseXml(code), 'project');
70
+ if (!project)
71
+ return undefined;
72
+ const pom = { file, code, project, props: new Map(), parent: child(project, 'parent') };
73
+ pom.artifactId = childText(pom, project, 'artifactId');
74
+ pom.groupId = childText(pom, project, 'groupId') ?? childText(pom, pom.parent, 'groupId');
75
+ pom.version = childText(pom, project, 'version') ?? childText(pom, pom.parent, 'version');
76
+ for (const prop of child(project, 'properties')?.children ?? []) {
77
+ const value = textOf(pom, prop);
78
+ if (value)
79
+ pom.props.set(prop.name, value);
80
+ }
81
+ return pom;
82
+ }
83
+ class PomTree {
84
+ cache = new Map();
85
+ load(file) {
86
+ let pom = this.cache.get(file);
87
+ if (!pom)
88
+ this.cache.set(file, (pom = loadPom(file)));
89
+ return pom;
90
+ }
91
+ async chain(pom) {
92
+ const chain = [pom];
93
+ for (let current = pom; current.parent;) {
94
+ const relative = child(current.parent, 'relativePath');
95
+ const rel = relative ? (textOf(current, relative)?.value ?? '') : '../pom.xml';
96
+ if (!rel)
97
+ break;
98
+ const path = resolve(dirname(current.file), rel);
99
+ const parent = await this.load(path.endsWith('.xml') ? path : join(path, 'pom.xml'));
100
+ if (!parent || chain.includes(parent) || parent.artifactId !== childText(current, current.parent, 'artifactId'))
101
+ break;
102
+ chain.push(parent);
103
+ current = parent;
104
+ }
105
+ return chain;
106
+ }
107
+ }
108
+ function lookup(chain, key) {
109
+ const pom = chain[0];
110
+ if (/^(?:project|pom)\.groupId$/.test(key) && pom.groupId)
111
+ return { value: pom.groupId };
112
+ if (/^(?:project|pom)\.artifactId$/.test(key) && pom.artifactId)
113
+ return { value: pom.artifactId };
114
+ for (const p of chain) {
115
+ const value = p.props.get(key);
116
+ if (value)
117
+ return value;
118
+ }
119
+ return undefined;
120
+ }
121
+ function resolveValue(raw, loc, chain, depth = 0) {
122
+ if (depth > 5 || SELF_VERSION_RE.test(raw))
123
+ return undefined;
124
+ const whole = /^\$\{([^}]+)\}$/.exec(raw);
125
+ if (whole) {
126
+ const found = lookup(chain, whole[1]);
127
+ if (!found)
128
+ return undefined;
129
+ return found.value.includes('${') ? resolveValue(found.value, undefined, chain, depth + 1) : found;
130
+ }
131
+ if (!raw.includes('${'))
132
+ return { value: raw, loc };
133
+ let ok = true;
134
+ const value = raw.replace(/\$\{([^}]+)\}/g, (_, key) => {
135
+ const found = resolveValue(`\${${key}}`, undefined, chain, depth + 1);
136
+ if (!found)
137
+ ok = false;
138
+ return found?.value ?? '';
139
+ });
140
+ return ok ? { value } : undefined;
141
+ }
142
+ function repoUrls(pom, chain, name, parentName) {
143
+ return findAll(pom.project, name, parentName)
144
+ .map((repo) => childText(pom, repo, 'url'))
145
+ .map((url) => url && resolveValue(url, undefined, chain)?.value)
146
+ .filter((url) => !!url && !url.includes('${'));
147
+ }
148
+ function coordinates(pom) {
149
+ return [
150
+ ...findAll(pom.project, 'dependency', 'dependencies').map((node) => ({ node, kind: 'library' })),
151
+ ...findAll(pom.project, 'plugin', 'plugins').map((node) => ({
152
+ node,
153
+ kind: 'plugin',
154
+ defaultGroup: 'org.apache.maven.plugins',
155
+ })),
156
+ ...findAll(pom.project, 'extension', 'extensions').map((node) => ({ node, kind: 'plugin' })),
157
+ ...(pom.parent ? [{ node: pom.parent, kind: 'library' }] : []),
158
+ ];
159
+ }
160
+ export async function scanMaven(pomFiles) {
161
+ const tree = new PomTree();
162
+ const poms = (await Promise.all(pomFiles.map((f) => tree.load(f)))).filter((p) => !!p);
163
+ const modules = new Set(poms.map((p) => `${p.groupId}:${p.artifactId}`));
164
+ const warnings = [];
165
+ const dependencies = [];
166
+ for (const pom of poms) {
167
+ const chain = await tree.chain(pom);
168
+ const libraryRepos = dedupeRepos([...chain.flatMap((p) => repoUrls(p, chain, 'repository', 'repositories')), MAVEN_CENTRAL]);
169
+ const pluginRepos = dedupeRepos([
170
+ ...chain.flatMap((p) => repoUrls(p, chain, 'pluginRepository', 'pluginRepositories')),
171
+ MAVEN_CENTRAL,
172
+ ]);
173
+ for (const { node, kind, defaultGroup } of coordinates(pom)) {
174
+ const rawGroup = childText(pom, node, 'groupId') ?? defaultGroup;
175
+ const rawArtifact = childText(pom, node, 'artifactId');
176
+ const versionText = textOf(pom, child(node, 'version'));
177
+ if (!rawGroup || !rawArtifact || !versionText)
178
+ continue;
179
+ const group = resolveValue(rawGroup, undefined, chain)?.value;
180
+ const artifact = resolveValue(rawArtifact, undefined, chain)?.value;
181
+ if (!group || !artifact || modules.has(`${group}:${artifact}`) || SELF_VERSION_RE.test(versionText.value))
182
+ continue;
183
+ const version = resolveValue(versionText.value, versionText.loc, chain);
184
+ if (!version) {
185
+ warnings.push(`${pom.file}: cannot resolve version "${versionText.value}" of ${group}:${artifact}`);
186
+ continue;
187
+ }
188
+ const location = version.loc && !isDynamic(version.value) ? version.loc : undefined;
189
+ dependencies.push({
190
+ group,
191
+ artifact,
192
+ kind,
193
+ version: version.value,
194
+ file: pom.file,
195
+ repositories: kind === 'plugin' ? pluginRepos : libraryRepos,
196
+ ...(location && { location }),
197
+ });
198
+ }
199
+ }
200
+ return { dependencies, warnings };
201
+ }
@@ -0,0 +1,30 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ export const MAVEN_CENTRAL = 'https://repo.maven.apache.org/maven2/';
5
+ export const GOOGLE = 'https://dl.google.com/dl/android/maven2/';
6
+ export const GRADLE_PLUGIN_PORTAL = 'https://plugins.gradle.org/m2/';
7
+ export const JITPACK = 'https://jitpack.io/';
8
+ export const JCENTER = 'https://jcenter.bintray.com/';
9
+ export function mavenLocal() {
10
+ return withSlash(pathToFileURL(join(homedir(), '.m2', 'repository')).href);
11
+ }
12
+ export const NAMED_REPOS = {
13
+ mavenCentral: () => MAVEN_CENTRAL,
14
+ google: () => GOOGLE,
15
+ gradlePluginPortal: () => GRADLE_PLUGIN_PORTAL,
16
+ jcenter: () => JCENTER,
17
+ mavenLocal,
18
+ };
19
+ export function withSlash(url) {
20
+ return url.endsWith('/') ? url : `${url}/`;
21
+ }
22
+ export function dedupeRepos(urls) {
23
+ const out = new Set();
24
+ for (const url of urls) {
25
+ const normalized = withSlash(url.trim());
26
+ if (normalized !== JCENTER)
27
+ out.add(normalized);
28
+ }
29
+ return [...out];
30
+ }
package/dist/tui.js ADDED
@@ -0,0 +1,165 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ import { styleText } from 'node:util';
3
+ import { formatChangelog } from './format.js';
4
+ import { levelColor } from './rows.js';
5
+ export function initialState(rows) {
6
+ return { cursor: 0, choice: rows.map((r) => (r.selected === undefined ? null : r.selected)) };
7
+ }
8
+ /** Pure key handling so the picker logic is testable without a terminal. */
9
+ export function reduce(state, rows, key) {
10
+ const { cursor } = state;
11
+ const row = rows[cursor];
12
+ const choice = [...state.choice];
13
+ const n = row?.choices.length ?? 0;
14
+ switch (key) {
15
+ case 'up':
16
+ return { ...state, cursor: Math.max(0, cursor - 1) };
17
+ case 'down':
18
+ return { ...state, cursor: Math.min(rows.length - 1, cursor + 1) };
19
+ case 'left':
20
+ if (!row?.location || n === 0)
21
+ return state;
22
+ choice[cursor] = choice[cursor] === null ? n - 1 : choice[cursor] - 1 < 0 ? null : choice[cursor] - 1;
23
+ return { ...state, choice };
24
+ case 'right':
25
+ if (!row?.location || n === 0)
26
+ return state;
27
+ choice[cursor] = choice[cursor] === null ? 0 : choice[cursor] + 1 >= n ? null : choice[cursor] + 1;
28
+ return { ...state, choice };
29
+ case 'space':
30
+ if (!row?.location || n === 0)
31
+ return state;
32
+ choice[cursor] = choice[cursor] === null ? (row.selected ?? n - 1) : null;
33
+ return { ...state, choice };
34
+ case 'all': {
35
+ const anySelected = choice.some((c) => c !== null);
36
+ return { ...state, choice: rows.map((r) => (anySelected || !r.location || !r.choices.length ? null : (r.selected ?? r.choices.length - 1))) };
37
+ }
38
+ case 'enter':
39
+ return 'submit';
40
+ case 'cancel':
41
+ return 'cancel';
42
+ default:
43
+ return state;
44
+ }
45
+ }
46
+ const KEYMAP = {
47
+ up: 'up', k: 'up', down: 'down', j: 'down', left: 'left', h: 'left', right: 'right', l: 'right',
48
+ space: 'space', a: 'all', return: 'enter', enter: 'enter', escape: 'cancel', q: 'cancel', c: 'changelog',
49
+ pageup: 'pageup', pagedown: 'pagedown',
50
+ };
51
+ export async function pick(rows, loadChangelog) {
52
+ const input = process.stdin;
53
+ const out = process.stdout;
54
+ let state = initialState(rows);
55
+ let view;
56
+ let busy = false;
57
+ emitKeypressEvents(input);
58
+ input.setRawMode(true);
59
+ input.resume();
60
+ out.write('\x1b[?1049h\x1b[?25l');
61
+ const render = () => {
62
+ const height = out.rows ?? 24;
63
+ const width = out.columns ?? 80;
64
+ const body = view ? renderView(view, height, width) : renderList(rows, state, height, width, busy);
65
+ out.write(`\x1b[H\x1b[2J${body}`);
66
+ };
67
+ const onResize = () => render();
68
+ out.on('resize', onResize);
69
+ return new Promise((resolvePick) => {
70
+ const finish = (result) => {
71
+ input.off('keypress', onKey);
72
+ out.off('resize', onResize);
73
+ input.setRawMode(false);
74
+ input.pause();
75
+ out.write('\x1b[?25h\x1b[?1049l');
76
+ resolvePick(result);
77
+ };
78
+ const onKey = (_, key) => {
79
+ if (key.ctrl && key.name === 'c')
80
+ return finish(undefined);
81
+ const action = KEYMAP[key.name ?? key.sequence ?? ''];
82
+ if (!action)
83
+ return;
84
+ if (view) {
85
+ const page = Math.max(1, (out.rows ?? 24) - 3);
86
+ if (action === 'up')
87
+ view.top = Math.max(0, view.top - 1);
88
+ else if (action === 'down')
89
+ view.top = Math.min(Math.max(0, view.lines.length - page), view.top + 1);
90
+ else if (action === 'pageup')
91
+ view.top = Math.max(0, view.top - page);
92
+ else if (action === 'pagedown')
93
+ view.top = Math.min(Math.max(0, view.lines.length - page), view.top + page);
94
+ else if (action === 'cancel' || action === 'left' || action === 'changelog')
95
+ view = undefined;
96
+ return render();
97
+ }
98
+ if (action === 'changelog') {
99
+ const row = rows[state.cursor];
100
+ const index = state.choice[state.cursor] ?? row?.selected ?? (row ? row.choices.length - 1 : -1);
101
+ const choice = row?.choices[index];
102
+ if (!row || !choice || busy)
103
+ return;
104
+ busy = true;
105
+ render();
106
+ loadChangelog(row, choice)
107
+ .then((log) => formatChangelog(log, (out.columns ?? 80) - 2).split('\n'))
108
+ .catch((e) => [`Could not load the changelog: ${e.message}`])
109
+ .then((lines) => {
110
+ busy = false;
111
+ view = { lines, top: 0, title: `${row.name} ${row.current} → ${choice.version}` };
112
+ render();
113
+ });
114
+ return;
115
+ }
116
+ const next = reduce(state, rows, action);
117
+ if (next === 'cancel')
118
+ return finish(undefined);
119
+ if (next === 'submit') {
120
+ const result = new Map();
121
+ state.choice.forEach((c, i) => {
122
+ const row = rows[i];
123
+ const choice = c === null ? undefined : row?.choices[c];
124
+ if (row && choice)
125
+ result.set(row, choice);
126
+ });
127
+ return finish(result);
128
+ }
129
+ state = next;
130
+ render();
131
+ };
132
+ input.on('keypress', onKey);
133
+ render();
134
+ });
135
+ }
136
+ function renderList(rows, state, height, width, busy) {
137
+ const header = styleText('bold', 'Choose upgrades') + styleText('gray', ' ↑↓ move ←→ version space toggle a all c changelog enter apply q quit');
138
+ const visible = Math.max(1, height - 3);
139
+ const top = Math.min(Math.max(0, state.cursor - Math.floor(visible / 2)), Math.max(0, rows.length - visible));
140
+ const nameWidth = Math.min(Math.max(...rows.map((r) => r.name.length), 4), Math.max(20, width - 40));
141
+ const lines = rows.slice(top, top + visible).map((row, i) => {
142
+ const index = top + i;
143
+ const active = index === state.cursor;
144
+ const c = state.choice[index] ?? null;
145
+ const mark = c === null ? '○' : styleText('green', '●');
146
+ const name = truncate(row.name, nameWidth).padEnd(nameWidth);
147
+ const options = row.location
148
+ ? row.choices.map((ch, j) => (j === c ? styleText(['inverse', levelColor(ch.level)], ` ${ch.version} `) : styleText(levelColor(ch.level), ` ${ch.version} `))).join(' ')
149
+ : styleText('gray', `${row.choices.at(-1)?.version ?? ''} (not rewritable)`);
150
+ const line = `${active ? styleText('cyan', '❯') : ' '} ${mark} ${active ? styleText('bold', name) : name} ${row.current.padEnd(12)} ${options}`;
151
+ return line;
152
+ });
153
+ const footer = busy ? styleText('yellow', 'Loading changelog…') : styleText('gray', `${state.choice.filter((c) => c !== null).length} of ${rows.length} selected`);
154
+ return [header, '', ...lines, footer].join('\n');
155
+ }
156
+ function renderView(view, height, width) {
157
+ const page = Math.max(1, height - 3);
158
+ const header = styleText('bold', truncate(view.title, width - 30)) + styleText('gray', ' ↑↓ scroll q back');
159
+ const body = view.lines.slice(view.top, view.top + page);
160
+ const footer = styleText('gray', `${Math.min(view.top + page, view.lines.length)}/${view.lines.length}`);
161
+ return [header, '', ...body, footer].join('\n');
162
+ }
163
+ function truncate(text, max) {
164
+ return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`;
165
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export const TARGETS = ['patch', 'minor', 'major'];
@@ -0,0 +1,73 @@
1
+ const SPECIAL = { dev: -1, rc: 1, snapshot: 2, final: 3, ga: 4, release: 5, sp: 6 };
2
+ const PRERELEASE = new Set([
3
+ 'alpha', 'a', 'beta', 'b', 'milestone', 'm', 'rc', 'cr', 'eap', 'dev', 'snapshot', 'preview', 'pre',
4
+ 'canary', 'incubating', 'ea', 'nightly', 'experimental',
5
+ ]);
6
+ const isNum = (s) => /^\d+$/.test(s);
7
+ export function tokenize(v) {
8
+ return v.toLowerCase().replace(/^v(?=\d)/, '').match(/\d+|[^\d._\-+]+/g) ?? [];
9
+ }
10
+ function compareNumeric(a, b) {
11
+ const x = a.replace(/^0+(?=\d)/, '');
12
+ const y = b.replace(/^0+(?=\d)/, '');
13
+ return x.length - y.length || (x < y ? -1 : x > y ? 1 : 0);
14
+ }
15
+ function compareParts(a, b) {
16
+ const an = isNum(a);
17
+ const bn = isNum(b);
18
+ if (an && bn)
19
+ return compareNumeric(a, b);
20
+ if (an !== bn)
21
+ return an ? 1 : -1;
22
+ const sa = SPECIAL[a] ?? 0;
23
+ const sb = SPECIAL[b] ?? 0;
24
+ if (sa !== sb)
25
+ return sa - sb;
26
+ return a < b ? -1 : a > b ? 1 : 0;
27
+ }
28
+ export function compareVersions(a, b) {
29
+ const pa = tokenize(a);
30
+ const pb = tokenize(b);
31
+ for (let i = 0; i < Math.min(pa.length, pb.length); i++) {
32
+ const c = compareParts(pa[i], pb[i]);
33
+ if (c !== 0)
34
+ return Math.sign(c);
35
+ }
36
+ if (pa.length === pb.length)
37
+ return 0;
38
+ const [extra, sign] = pa.length > pb.length ? [pa[pb.length], 1] : [pb[pa.length], -1];
39
+ return isNum(extra) ? sign : -sign;
40
+ }
41
+ export function isSnapshot(v) {
42
+ return tokenize(v).includes('snapshot');
43
+ }
44
+ export function isPrerelease(v) {
45
+ return tokenize(v).some((t) => PRERELEASE.has(t));
46
+ }
47
+ export function isDynamic(v) {
48
+ const s = v.trim();
49
+ return /^[[(]/.test(s) || /[\])]$/.test(s) || s.endsWith('+') || /^latest\./i.test(s) || /^(LATEST|RELEASE)$/.test(s);
50
+ }
51
+ export function upgradeLevel(from, to) {
52
+ if (compareVersions(to, from) <= 0)
53
+ return undefined;
54
+ const f = tokenize(from);
55
+ const t = tokenize(to);
56
+ const part = (p, i) => p[i] ?? '0';
57
+ if (compareParts(part(f, 0), part(t, 0)) !== 0)
58
+ return 'major';
59
+ if (compareParts(part(f, 1), part(t, 1)) !== 0)
60
+ return 'minor';
61
+ return 'patch';
62
+ }
63
+ /** JitPack often has both "v1.2" and "1.2" for one tag. Keep the style the project already uses. */
64
+ export function preferStyle(current, versions) {
65
+ const prefixed = /^v\d/i.test(current);
66
+ const all = new Set(versions);
67
+ return versions.filter((v) => {
68
+ const hasV = /^v\d/i.test(v);
69
+ if (hasV === prefixed)
70
+ return true;
71
+ return !all.has(hasV ? v.slice(1) : `v${v}`);
72
+ });
73
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "jvm-upgrade",
3
+ "version": "0.1.0",
4
+ "description": "Find and apply library upgrades in Java, Kotlin and Android projects (Gradle and Maven), with changelogs.",
5
+ "type": "module",
6
+ "bin": {
7
+ "jvm-upgrade": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20.12"
16
+ },
17
+ "scripts": {
18
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
19
+ "test": "vitest run",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
22
+ },
23
+ "keywords": [
24
+ "gradle",
25
+ "maven",
26
+ "android",
27
+ "kotlin",
28
+ "java",
29
+ "dependencies",
30
+ "upgrade",
31
+ "outdated",
32
+ "version-catalog",
33
+ "changelog",
34
+ "jitpack",
35
+ "cli"
36
+ ],
37
+ "author": "Theofilos Chamalis",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/Theofilos-Chamalis/jvm-upgrade.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/Theofilos-Chamalis/jvm-upgrade/issues"
45
+ },
46
+ "homepage": "https://github.com/Theofilos-Chamalis/jvm-upgrade#readme",
47
+ "devDependencies": {
48
+ "@types/node": "^26.6.3",
49
+ "typescript": "^7.0.2",
50
+ "vitest": "^5.0.2"
51
+ }
52
+ }