relacher 0.0.1 → 0.0.3

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.
Files changed (52) hide show
  1. package/README.md +12 -16
  2. package/lib-types/builder/cargo.d.ts +6 -6
  3. package/lib-types/builder/shared.d.ts +2 -3
  4. package/lib-types/default-data.d.ts +3 -0
  5. package/lib-types/index.d.ts +1 -0
  6. package/lib-types/prepare.d.ts +7 -6
  7. package/lib-types/run.d.ts +5 -0
  8. package/lib-types/src/builder/cargo.d.ts +13 -0
  9. package/lib-types/src/builder/index.d.ts +3 -0
  10. package/lib-types/src/builder/pnpm.d.ts +13 -0
  11. package/lib-types/src/builder/shared.d.ts +11 -0
  12. package/lib-types/src/default-data.d.ts +3 -0
  13. package/lib-types/src/index.d.ts +6 -0
  14. package/lib-types/src/prepare.d.ts +15 -0
  15. package/lib-types/src/print.d.ts +2 -0
  16. package/lib-types/src/run.d.ts +12 -0
  17. package/lib-types/src/types.d.ts +71 -0
  18. package/lib-types/src/updater/changelog.d.ts +10 -0
  19. package/lib-types/src/updater/index.d.ts +26 -0
  20. package/lib-types/src/updater/json.d.ts +12 -0
  21. package/lib-types/src/updater/regex.d.ts +13 -0
  22. package/lib-types/src/updater/toml.d.ts +12 -0
  23. package/lib-types/src/vcs/git.d.ts +15 -0
  24. package/lib-types/src/vcs/index.d.ts +9 -0
  25. package/lib-types/src/vcs/jj.d.ts +15 -0
  26. package/lib-types/types.d.ts +9 -25
  27. package/lib-types/updater/changelog.d.ts +10 -0
  28. package/lib-types/updater/index.d.ts +26 -0
  29. package/lib-types/updater/json.d.ts +12 -0
  30. package/lib-types/updater/regex.d.ts +13 -0
  31. package/lib-types/updater/toml.d.ts +12 -0
  32. package/lib-types/vcs/git.d.ts +1 -0
  33. package/lib-types/vcs/index.d.ts +1 -0
  34. package/lib-types/vcs/jj.d.ts +1 -0
  35. package/package.json +2 -2
  36. package/src/builder/cargo.ts +81 -18
  37. package/src/builder/pnpm.ts +14 -12
  38. package/src/builder/shared.ts +2 -9
  39. package/src/default-data.ts +15 -0
  40. package/src/index.ts +1 -0
  41. package/src/prepare.ts +68 -108
  42. package/src/print.ts +9 -4
  43. package/src/run.ts +50 -56
  44. package/src/types.ts +14 -46
  45. package/src/updater/changelog.ts +76 -0
  46. package/src/updater/index.ts +31 -0
  47. package/src/updater/json.ts +95 -0
  48. package/src/updater/regex.ts +48 -0
  49. package/src/updater/toml.ts +57 -0
  50. package/src/vcs/git.ts +24 -3
  51. package/src/vcs/index.ts +1 -0
  52. package/src/vcs/jj.ts +23 -6
package/src/types.ts CHANGED
@@ -1,17 +1,18 @@
1
+ import type { UpdateAction, UpdateActionResolved, VersionFallback } from './updater';
1
2
  import type { VcsProvider } from './vcs';
2
3
 
3
- export type BumpSize = "major" | "minor" | "patch" | "skip";
4
+ export type BumpSize = 'major' | 'minor' | 'patch' | 'skip';
4
5
 
5
6
  export interface Commit {
6
7
  hash: string;
7
8
  shortHash: string;
8
9
  author: string;
9
10
  date: string;
10
- message: string; // The full commit message
11
- type: string; // e.g., 'feat', 'fix', 'chore'
12
- scope: string | null; // e.g., 'ui', 'api'
13
- isBreaking: boolean; // true if contains '!' or 'BREAKING CHANGE'
14
- description: string; // The text after the colon
11
+ message: string; // The full commit message
12
+ type: string; // e.g., 'feat', 'fix', 'chore'
13
+ scope: string | null; // e.g., 'ui', 'api'
14
+ isBreaking: boolean; // true if contains '!' or 'BREAKING CHANGE'
15
+ description: string; // The text after the colon
15
16
  }
16
17
 
17
18
  export interface ChangelogContext {
@@ -20,34 +21,11 @@ export interface ChangelogContext {
20
21
  commits: Commit[];
21
22
  }
22
23
 
23
- export interface ChangelogUpdate {
24
- kind: "changelog";
25
- path: string;
26
- global?: boolean;
27
- template?: (ctx: ChangelogContext) => string;
28
- resolvedBlock?: string;
29
- }
30
-
31
- export interface TomlUpdate {
32
- kind: "toml";
33
- path: string;
34
- toml: string;
35
- }
36
-
37
- export interface RegexUpdate {
38
- kind: "regex";
39
- path: string;
40
- search: string;
41
- replace: string;
42
- resolvedReplace?: string;
43
- }
44
-
45
- export type UpdateAction = TomlUpdate | RegexUpdate | ChangelogUpdate;
46
-
47
24
  export interface DependencyConfig {
48
25
  name: string;
49
26
  watch?: string[];
50
- updates?: UpdateAction[];
27
+ updates: UpdateAction[];
28
+ versionFallback?: VersionFallback;
51
29
  depends?: string[];
52
30
  }
53
31
 
@@ -82,8 +60,10 @@ export interface DependencyUpdateReport {
82
60
  bump: BumpSize;
83
61
  originalBump?: BumpSize;
84
62
  commits: Commit[];
85
- updates: UpdateAction[];
63
+ updates: UpdateActionResolved[];
86
64
  depends?: string[];
65
+ isErroneous?: boolean;
66
+ isFirstRelease?: boolean;
87
67
  }
88
68
 
89
69
  export interface IntermediateReport {
@@ -95,20 +75,8 @@ export interface IntermediateReport {
95
75
  commits: Commit[];
96
76
  updates: UpdateAction[];
97
77
  depends: string[];
78
+ isErroneous?: boolean;
79
+ isFirstRelease?: boolean;
98
80
  }
99
81
 
100
- export const defaultSizes: SizePatterns = {
101
- major: { pattern: "^[a-z]+(?:\\([^)]+\\))?!:|BREAKING CHANGE" },
102
- minor: { pattern: "^feat|^revert" },
103
- patch: { pattern: "^fix|^build|^refactor|^nit|^style" },
104
- skip: { pattern: "^release|^chore|^infra|^docs|^test|^ci|^build" },
105
- };
106
-
107
- export const defaultCascadeRules: Required<CascadeRules> = {
108
- skip: { skip: 'skip', patch: 'patch', minor: 'patch', major: 'patch' },
109
- patch: { skip: 'patch', patch: 'patch', minor: 'minor', major: 'minor' },
110
- minor: { skip: 'minor', patch: 'minor', minor: 'minor', major: 'minor' },
111
- major: { skip: 'major', patch: 'major', minor: 'major', major: 'major' },
112
- };
113
-
114
82
  export type { VcsProvider };
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { ChangelogContext } from '../types';
5
+
6
+ import type { UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
7
+
8
+ export type ChangelogUpdateParams = {
9
+ path: string;
10
+ global?: boolean;
11
+ template?: (ctx: ChangelogContext) => string;
12
+ required?: boolean;
13
+ };
14
+
15
+ export const changelogUpdate = (params: ChangelogUpdateParams): UpdateAction => ({
16
+ kind: 'changelog',
17
+ path: params.path,
18
+ required: params.required,
19
+ params,
20
+ prepare(data: UpdateActionOptions): UpdateActionResolved {
21
+ const targetCommits = params.global ? data.globalCommits : data.crateCommits;
22
+ const context: ChangelogContext = {
23
+ version: data.newVersion,
24
+ date: new Date().toISOString().split('T')[0] ?? '',
25
+ commits: targetCommits,
26
+ };
27
+
28
+ const resolvedBlock = params.template
29
+ ? params.template(context)
30
+ : defaultChangelogTemplate(context);
31
+
32
+ const t = {
33
+ kind: 'changelog',
34
+ path: params.path,
35
+ params: {
36
+ ...params,
37
+ resolvedBlock,
38
+ },
39
+ apply(_report, _reports, cwd) {
40
+ const filePath = path.resolve(cwd, params.path);
41
+ const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
42
+ const newContent = (resolvedBlock ?? '') + '\n' + oldContent;
43
+ fs.writeFileSync(filePath, newContent.trim() + '\n');
44
+ },
45
+ } satisfies UpdateActionResolved;
46
+
47
+ return t;
48
+ },
49
+ });
50
+
51
+ export function defaultChangelogTemplate({ version, date, commits }: ChangelogContext): string {
52
+ let block = `## [${version}] - ${date}\n\n`;
53
+ if (commits.length === 0) return block + `*No notable changes.*\n`;
54
+
55
+ const groups: { breaking: string[]; feat: string[]; fix: string[]; other: string[] } = {
56
+ breaking: [],
57
+ feat: [],
58
+ fix: [],
59
+ other: [],
60
+ };
61
+
62
+ for (const c of commits) {
63
+ const item = `- ${c.shortHash} ${c.message}`;
64
+ if (c.isBreaking) groups.breaking.push(item);
65
+ else if (c.type === 'feat') groups.feat.push(item);
66
+ else if (c.type === 'fix') groups.fix.push(item);
67
+ else groups.other.push(item);
68
+ }
69
+
70
+ if (groups.breaking.length) block += `### ⚠️ BREAKING CHANGES\n${groups.breaking.join('\n')}\n\n`;
71
+ if (groups.feat.length) block += `### ✨ Features\n${groups.feat.join('\n')}\n\n`;
72
+ if (groups.fix.length) block += `### 🐛 Bug Fixes\n${groups.fix.join('\n')}\n\n`;
73
+ if (groups.other.length) block += `### 🛠 Other Changes\n${groups.other.join('\n')}\n\n`;
74
+
75
+ return block.trim() + '\n';
76
+ }
@@ -0,0 +1,31 @@
1
+ import type { Commit, DependencyUpdateReport } from '../types';
2
+
3
+ export * from './changelog';
4
+ export * from './json';
5
+ export * from './regex';
6
+ export * from './toml';
7
+
8
+ export type UpdateActionOptions = {
9
+ newVersion: string;
10
+ globalCommits: Commit[];
11
+ crateCommits: Commit[];
12
+ };
13
+
14
+ export interface UpdateActionResolved {
15
+ kind: string;
16
+ path: string;
17
+ apply(report: DependencyUpdateReport, reports: DependencyUpdateReport[], cwd: string): void;
18
+ params: any;
19
+ }
20
+
21
+ export interface UpdateAction {
22
+ kind: string;
23
+ path: string;
24
+ required?: boolean;
25
+ params: any;
26
+ prepare(data: UpdateActionOptions): UpdateActionResolved;
27
+ }
28
+
29
+ export interface VersionFallback {
30
+ readFallback(cwd: string): string | null;
31
+ }
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { VersionFallback, UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
5
+
6
+ export type JsonFallbackParams = { path: string; json: string };
7
+ export type JsonUpdateParams = { path: string; json: string; required?: boolean };
8
+
9
+ export const jsonFallback = (params: JsonFallbackParams): VersionFallback => ({
10
+ readFallback(cwd: string): string | null {
11
+ const filePath = path.resolve(cwd, params.path);
12
+ if (!fs.existsSync(filePath)) return null;
13
+ try {
14
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
15
+ const keys = params.json.split('.');
16
+ let val: any = data;
17
+ for (const k of keys) {
18
+ if (val == null) return null;
19
+ val = val[k];
20
+ }
21
+ return typeof val === 'string' ? val : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ },
26
+ });
27
+
28
+ export const jsonUpdate = (params: JsonUpdateParams): UpdateAction => ({
29
+ kind: 'json',
30
+ path: params.path,
31
+ required: params.required,
32
+ params,
33
+ prepare(_data: UpdateActionOptions): UpdateActionResolved {
34
+ return {
35
+ kind: 'json',
36
+ path: params.path,
37
+ params,
38
+ apply(report, reports, cwd) {
39
+ const filePath = path.resolve(cwd, params.path);
40
+ if (!fs.existsSync(filePath)) return;
41
+
42
+ const content = fs.readFileSync(filePath, 'utf8');
43
+
44
+ // Detect indent size
45
+ const indentMatch = content.match(/^[ \t]+/m);
46
+ const indent = indentMatch ? indentMatch[0] : 2;
47
+
48
+ let data: Record<string, any>;
49
+ try {
50
+ data = JSON.parse(content);
51
+ } catch (err) {
52
+ console.error(`Failed to parse JSON file at ${filePath}`);
53
+ return;
54
+ }
55
+
56
+ // Update specific JSON path
57
+ const keys = params.json.split('.');
58
+ const lastKey = keys.pop();
59
+ if (lastKey) {
60
+ let target = data;
61
+ for (const k of keys) {
62
+ if (target[k] == null || typeof target[k] !== 'object') {
63
+ target[k] = {};
64
+ }
65
+ target = target[k];
66
+ }
67
+ target[lastKey] = report.newVersion;
68
+ }
69
+
70
+ // Sync internal workspace dependency entries
71
+ const depSections = [
72
+ 'dependencies',
73
+ 'devDependencies',
74
+ 'peerDependencies',
75
+ 'optionalDependencies',
76
+ ];
77
+
78
+ for (const depReport of reports) {
79
+ if (depReport.name === report.name) continue;
80
+
81
+ for (const section of depSections) {
82
+ if (data[section] && data[section][depReport.name]) {
83
+ const currentVal = data[section][depReport.name] as string;
84
+ const prefixMatch = currentVal.match(/^([~^]?)(.*)$/);
85
+ const prefix = prefixMatch ? prefixMatch[1] : '';
86
+ data[section][depReport.name] = `${prefix}${depReport.newVersion}`;
87
+ }
88
+ }
89
+ }
90
+
91
+ fs.writeFileSync(filePath, JSON.stringify(data, null, indent) + '\n');
92
+ },
93
+ };
94
+ },
95
+ });
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { VersionFallback, UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
5
+
6
+ export type RegexFallbackParams = { path: string; search: string };
7
+ export type RegexUpdateParams = {
8
+ path: string;
9
+ search: string;
10
+ replace: string;
11
+ required?: boolean;
12
+ };
13
+
14
+ export const regexFallback = (params: RegexFallbackParams): VersionFallback => ({
15
+ readFallback(cwd: string): string | null {
16
+ const filePath = path.resolve(cwd, params.path);
17
+ if (!fs.existsSync(filePath)) return null;
18
+ const content = fs.readFileSync(filePath, 'utf8');
19
+ const searchRegex = new RegExp(params.search, 'm');
20
+ const match = content.match(searchRegex);
21
+ return match && match[1] ? match[1] : null;
22
+ },
23
+ });
24
+
25
+ export const regexUpdate = (params: RegexUpdateParams): UpdateAction => ({
26
+ kind: 'regex',
27
+ path: params.path,
28
+ required: params.required,
29
+ params,
30
+ prepare(data: UpdateActionOptions): UpdateActionResolved {
31
+ const resolvedReplace = params.replace.replace('{{version}}', data.newVersion);
32
+
33
+ return {
34
+ kind: 'regex',
35
+ path: params.path,
36
+ params: { ...params, resolvedReplace },
37
+ apply(_report, _reports, cwd) {
38
+ const filePath = path.resolve(cwd, params.path);
39
+ if (!fs.existsSync(filePath)) return;
40
+
41
+ let content = fs.readFileSync(filePath, 'utf8');
42
+ const searchRegex = new RegExp(params.search, 'g');
43
+ content = content.replace(searchRegex, resolvedReplace);
44
+ fs.writeFileSync(filePath, content);
45
+ },
46
+ };
47
+ },
48
+ });
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { VersionFallback, UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
5
+
6
+ export type TomlFallbackParams = { path: string; toml: string };
7
+ export type TomlUpdateParams = { path: string; toml: string; required?: boolean };
8
+
9
+ export const tomlFallback = (params: TomlFallbackParams): VersionFallback => ({
10
+ readFallback(cwd: string): string | null {
11
+ const filePath = path.resolve(cwd, params.path);
12
+ if (!fs.existsSync(filePath)) return null;
13
+ const content = fs.readFileSync(filePath, 'utf8');
14
+ const packageMatch = content.match(/\[package\][^]*?(?=^\[|z)/);
15
+ const targetText = packageMatch ? packageMatch[0] : content;
16
+ const versionMatch = targetText.match(/^version\s*=\s*"([^"]+)"/m);
17
+ return versionMatch && versionMatch[1] ? versionMatch[1] : null;
18
+ },
19
+ });
20
+
21
+ export const tomlUpdate = (params: TomlUpdateParams): UpdateAction => ({
22
+ kind: 'toml',
23
+ path: params.path,
24
+ params,
25
+ required: params.required,
26
+ prepare(_data: UpdateActionOptions): UpdateActionResolved {
27
+ return {
28
+ kind: 'toml',
29
+ path: params.path,
30
+ params,
31
+ apply(report, reports, cwd) {
32
+ const filePath = path.resolve(cwd, params.path);
33
+ if (!fs.existsSync(filePath)) return;
34
+
35
+ let content = fs.readFileSync(filePath, 'utf8');
36
+
37
+ // Update package version inside the target Cargo.toml package block
38
+ content = content.replace(
39
+ /(\[package\][^]*?^version\s*=\s*")[^"]+(")/m,
40
+ `$1${report.newVersion}$2`,
41
+ );
42
+
43
+ // Sync workspace dependency entries if internal dependencies changed
44
+ for (const depReport of reports) {
45
+ if (depReport.name === report.name) continue;
46
+ const depRegex = new RegExp(
47
+ `(${depReport.name}\\s*=\\s*\\{[^}]*version\\s*=\\s*")[^"]+(")`,
48
+ 'g',
49
+ );
50
+ content = content.replace(depRegex, `$1${depReport.newVersion}$2`);
51
+ }
52
+
53
+ fs.writeFileSync(filePath, content);
54
+ },
55
+ };
56
+ },
57
+ });
package/src/vcs/git.ts CHANGED
@@ -11,7 +11,11 @@ export function runGit(cmd: string, cwd: string): string {
11
11
  }
12
12
 
13
13
  function parseSemver(tag: string, prefix: string): number[] {
14
- return tag.slice(prefix.length).split('.').map(Number);
14
+ const versionStr = tag.slice(prefix.length);
15
+ return versionStr.split('.').map((part) => {
16
+ const val = Number.parseInt(part, 10);
17
+ return Number.isNaN(val) ? 0 : val;
18
+ });
15
19
  }
16
20
 
17
21
  export function findLatestTag(tags: string[], prefix: string): string | null {
@@ -44,7 +48,6 @@ export function getGitCommits(
44
48
  const watchPaths = watch.join(' ');
45
49
  const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
46
50
 
47
- // Using shell-safe single quotes and disabling signature lines to prevent parsing corruption
48
51
  const gitCmd = watchPaths
49
52
  ? `git log ${range} --no-show-signature --format='%H|%an|%ad|%s' --date=short -- ${watchPaths}`
50
53
  : `git log ${range} --no-show-signature --format='%H|%an|%ad|%s' --date=short`;
@@ -90,7 +93,7 @@ export class GitVcsProvider implements VcsProvider {
90
93
  if (this.allTags === null) {
91
94
  this.allTags = runGit('git tag', this.cwd)
92
95
  .split('\n')
93
- .map((t) => t.trim()) // Strips carriage returns (\r) and trailing whitespaces
96
+ .map((t) => t.trim())
94
97
  .filter(Boolean);
95
98
  }
96
99
  return this.allTags;
@@ -101,6 +104,24 @@ export class GitVcsProvider implements VcsProvider {
101
104
  return getGitCommits(name, watch, tags, this.cwd);
102
105
  }
103
106
 
107
+ async getLatestTag(name?: string): Promise<string | null> {
108
+ const tags = this.getAllTags();
109
+
110
+ if (typeof name === 'string') {
111
+ // Check specific crate tag first (e.g., "my_crate-v1.0.0")
112
+ const specificPrefix = `${name}-v`;
113
+ const specificTag = findLatestTag(tags, specificPrefix);
114
+ if (specificTag) return specificTag.slice(specificPrefix.length);
115
+ } else {
116
+ // Fallback to global release tag (e.g., "v1.0.0")
117
+ const genericPrefix = 'v';
118
+ const genericTag = findLatestTag(tags, genericPrefix);
119
+ if (genericTag) return genericTag.slice(genericPrefix.length);
120
+ }
121
+
122
+ return null;
123
+ }
124
+
104
125
  async commit(message: string): Promise<void> {
105
126
  runGit('git add .', this.cwd);
106
127
  runGit(`git commit -m "${message}"`, this.cwd);
package/src/vcs/index.ts CHANGED
@@ -2,6 +2,7 @@ import type { Commit } from '../types';
2
2
 
3
3
  export interface VcsProvider {
4
4
  getCommits(name: string, watch: string[]): Promise<Commit[]>;
5
+ getLatestTag(name?: string): Promise<string | null>;
5
6
  commit(message: string): Promise<void>;
6
7
  tag(tagName: string): Promise<void>;
7
8
  }
package/src/vcs/jj.ts CHANGED
@@ -15,7 +15,11 @@ export function runJj(cmd: string, cwd: string): string {
15
15
  }
16
16
 
17
17
  function parseSemver(tag: string, prefix: string): number[] {
18
- return tag.slice(prefix.length).split('.').map(Number);
18
+ const versionStr = tag.slice(prefix.length);
19
+ return versionStr.split('.').map((part) => {
20
+ const val = Number.parseInt(part, 10);
21
+ return Number.isNaN(val) ? 0 : val;
22
+ });
19
23
  }
20
24
 
21
25
  export function findLatestJjTag(tags: string[], prefix: string): string | null {
@@ -46,15 +50,12 @@ export function getJjCommits(
46
50
  ): Commit[] {
47
51
  const lastTag = resolveJjTag(name, allTags);
48
52
 
49
- // FIXED: Double quotes explicitly escape the literal tag name for the jj revset engine
50
53
  const range = lastTag ? `"${lastTag}"..@ & ~root()` : `::@ & ~root()`;
51
54
 
52
- // Calling `.short(40)` to cast the CommitId into a 40-character hex String for template concatenation
53
55
  const template =
54
56
  'commit_id.short(40) ++ "|" ++ author.name() ++ "|" ++ author.timestamp().format("%Y-%m-%d") ++ "|" ++ description.first_line() ++ "\\n"';
55
57
 
56
58
  const watchPaths = watch.join(' ');
57
- // Appending --color=never to strip ANSI escape codes so hashes can be parsed cleanly
58
59
  const jjCmd = watchPaths
59
60
  ? `log --no-graph -r '${range}' -T '${template}' -- ${watchPaths}`
60
61
  : `log --no-graph -r '${range}' -T '${template}'`;
@@ -98,7 +99,6 @@ export class JjVcsProvider implements VcsProvider {
98
99
 
99
100
  private getAllTags(): string[] {
100
101
  if (this.allTags === null) {
101
- // Must enforce --color=never to prevent ANSI escape codes from failing the startsWith prefix checks
102
102
  const raw = runJj('tag list', this.cwd);
103
103
  this.allTags = raw
104
104
  .split('\n')
@@ -116,12 +116,29 @@ export class JjVcsProvider implements VcsProvider {
116
116
  return getJjCommits(name, watch, tags, this.cwd);
117
117
  }
118
118
 
119
+ async getLatestTag(name?: string): Promise<string | null> {
120
+ const tags = this.getAllTags();
121
+
122
+ if (typeof name === 'string') {
123
+ // Check specific crate tag first
124
+ const specificPrefix = `${name}-v`;
125
+ const specificTag = findLatestJjTag(tags, specificPrefix);
126
+ if (specificTag) return specificTag.slice(specificPrefix.length);
127
+ } else {
128
+ // Fallback to global release tag
129
+ const genericPrefix = 'v';
130
+ const genericTag = findLatestJjTag(tags, genericPrefix);
131
+ if (genericTag) return genericTag.slice(genericPrefix.length);
132
+ }
133
+
134
+ return null;
135
+ }
136
+
119
137
  async commit(message: string): Promise<void> {
120
138
  runJj(`commit -m "${message}"`, this.cwd);
121
139
  }
122
140
 
123
141
  async tag(tagName: string): Promise<void> {
124
- // Appending --allow-move to update the tag reference cleanly if it already exists
125
142
  runJj(`tag set "${tagName}" -r @- --allow-move`, this.cwd);
126
143
  }
127
144
  }