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
@@ -2,9 +2,14 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import type { DependencyConfig } from '../types';
5
+ import { regexUpdate, tomlUpdate, tomlFallback } from '../updater';
5
6
 
6
7
  import { decorateList, type DependencyList } from './shared';
7
8
 
9
+ export interface CargoBuilderOptions {
10
+ syncWorkspaceDeps?: boolean;
11
+ }
12
+
8
13
  /**
9
14
  * Loads a single Cargo project from a target Cargo.toml.
10
15
  */
@@ -15,13 +20,14 @@ export function cargoProject(cwd: string): DependencyList {
15
20
  if (fs.existsSync(rootCargo)) {
16
21
  const fileContent = fs.readFileSync(rootCargo, 'utf8');
17
22
  const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
18
- const parsedName = nameMatch && nameMatch[1] ? nameMatch[1] : '';
23
+ const parsedName = nameMatch?.[1] ?? '';
19
24
  if (parsedName) {
20
25
  configs.push({
21
26
  name: parsedName,
22
27
  watch: ['.'],
23
28
  depends: [],
24
- updates: [{ kind: 'toml', path: 'Cargo.toml', toml: 'package.version' }],
29
+ versionFallback: tomlFallback({ path: 'Cargo.toml', toml: 'package.version' }),
30
+ updates: [tomlUpdate({ path: 'Cargo.toml', toml: 'package.version' })],
25
31
  });
26
32
  }
27
33
  }
@@ -30,11 +36,12 @@ export function cargoProject(cwd: string): DependencyList {
30
36
  }
31
37
 
32
38
  /**
33
- * Loads a Cargo workspace, tracking member paths and dependency links.
39
+ * Loads a Cargo workspace, tracking member paths, dependency links, and recursive nested paths.
34
40
  */
35
- export function cargoWorkspace(cwd: string): DependencyList {
41
+ export function cargoWorkspace(cwd: string, options?: CargoBuilderOptions): DependencyList {
36
42
  const configs: DependencyConfig[] = [];
37
43
  const rootCargo = path.join(cwd, 'Cargo.toml');
44
+ const discoveredPaths = new Set<string>();
38
45
 
39
46
  if (fs.existsSync(rootCargo)) {
40
47
  const content = fs.readFileSync(rootCargo, 'utf8');
@@ -44,47 +51,106 @@ export function cargoWorkspace(cwd: string): DependencyList {
44
51
  const crateInfos: { name: string; memberPath: string; content: string }[] = [];
45
52
 
46
53
  for (const member of rawMembers) {
54
+ if (!member) continue;
47
55
  const globPath = path.join(cwd, member);
48
56
  const cargoPaths = globPath.endsWith('*')
49
- ? fs
50
- .readdirSync(path.dirname(globPath))
51
- .map((dir) => path.join(path.dirname(globPath), dir, 'Cargo.toml'))
57
+ ? fs.existsSync(path.dirname(globPath))
58
+ ? fs
59
+ .readdirSync(path.dirname(globPath))
60
+ .map((dir) => path.join(path.dirname(globPath), dir, 'Cargo.toml'))
61
+ : []
52
62
  : [path.join(globPath, 'Cargo.toml')];
53
63
 
54
64
  for (const p of cargoPaths) {
55
65
  if (fs.existsSync(p)) {
66
+ const memberPath = path.dirname(p);
67
+ discoveredPaths.add(memberPath);
56
68
  const fileContent = fs.readFileSync(p, 'utf8');
57
69
  const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
58
- const parsedName = nameMatch && nameMatch[1] ? nameMatch[1] : '';
70
+ const parsedName = nameMatch?.[1] ?? '';
59
71
  if (parsedName) {
60
72
  crateInfos.push({
61
73
  name: parsedName,
62
- memberPath: path.dirname(p),
74
+ memberPath,
75
+ content: fileContent,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // Discover nested packages dynamically via path dependencies
83
+ let i = 0;
84
+ while (i < crateInfos.length) {
85
+ const info = crateInfos[i]!;
86
+ const pathDepsMatch = [
87
+ ...info.content.matchAll(/([a-zA-Z0-9_-]+)\s*=\s*\{[^}]*path\s*=\s*"([^"]+)"/g),
88
+ ];
89
+ for (const match of pathDepsMatch) {
90
+ const depName = match[1];
91
+ const depPath = match[2];
92
+
93
+ // Verify matches are defined strings to resolve TypeScript's undefined safety warnings
94
+ if (typeof depName !== 'string' || typeof depPath !== 'string') {
95
+ continue;
96
+ }
97
+
98
+ const fullDepPath = path.resolve(info.memberPath, depPath);
99
+
100
+ if (!discoveredPaths.has(fullDepPath)) {
101
+ discoveredPaths.add(fullDepPath);
102
+ const cargoPath = path.join(fullDepPath, 'Cargo.toml');
103
+
104
+ if (fs.existsSync(cargoPath)) {
105
+ const fileContent = fs.readFileSync(cargoPath, 'utf8');
106
+ const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
107
+ const parsedName = nameMatch?.[1] ?? depName;
108
+
109
+ crateInfos.push({
110
+ name: parsedName,
111
+ memberPath: fullDepPath,
63
112
  content: fileContent,
64
113
  });
65
114
  }
66
115
  }
67
116
  }
117
+ i++;
68
118
  }
69
119
 
70
120
  for (const info of crateInfos) {
71
121
  const relativePath = path.relative(cwd, info.memberPath);
72
- const relativeCargo = path.join(relativePath, 'Cargo.toml');
122
+ const posixRelativePath = relativePath.split(path.sep).join(path.posix.sep);
123
+ const relativeCargo = posixRelativePath
124
+ ? path.posix.join(posixRelativePath, 'Cargo.toml')
125
+ : 'Cargo.toml';
73
126
 
74
127
  const depends: string[] = [];
75
128
  for (const other of crateInfos) {
76
129
  if (other.name === info.name) continue;
77
- const depPattern = new RegExp(`^${other.name}\\s*=`, 'm');
130
+ const depPattern = new RegExp(`^\\s*${other.name}\\s*=`, 'm');
78
131
  if (depPattern.test(info.content)) {
79
132
  depends.push(other.name);
80
133
  }
81
134
  }
82
135
 
136
+ const updates = [tomlUpdate({ path: relativeCargo, toml: 'package.version' })];
137
+
138
+ if (options?.syncWorkspaceDeps) {
139
+ updates.push(
140
+ regexUpdate({
141
+ path: 'Cargo.toml',
142
+ search: `(${info.name}\\s*=\\s*\\{[^}]*version\\s*=\\s*")[^"]+(")`,
143
+ replace: `$1{{version}}$2`,
144
+ }),
145
+ );
146
+ }
147
+
83
148
  configs.push({
84
149
  name: info.name,
85
- watch: [relativePath],
150
+ watch: [posixRelativePath || '.'],
86
151
  depends,
87
- updates: [{ kind: 'toml', path: relativeCargo, toml: 'package.version' }],
152
+ versionFallback: tomlFallback({ path: relativeCargo, toml: 'package.version' }),
153
+ updates,
88
154
  });
89
155
  }
90
156
  }
@@ -93,15 +159,12 @@ export function cargoWorkspace(cwd: string): DependencyList {
93
159
  return decorateList(configs);
94
160
  }
95
161
 
96
- /**
97
- * Auto-detects whether the directory corresponds to a workspace or single-project setup.
98
- */
99
- export function cargoDeps(cwd: string): DependencyList {
162
+ export function cargoDeps(cwd: string, options?: CargoBuilderOptions): DependencyList {
100
163
  const rootCargo = path.join(cwd, 'Cargo.toml');
101
164
  if (fs.existsSync(rootCargo)) {
102
165
  const content = fs.readFileSync(rootCargo, 'utf8');
103
166
  if (content.includes('members =')) {
104
- return cargoWorkspace(cwd);
167
+ return cargoWorkspace(cwd, options);
105
168
  }
106
169
  if (content.includes('package =') || content.match(/^name\s*=/m)) {
107
170
  return cargoProject(cwd);
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import type { DependencyConfig } from '../types';
5
+ import { jsonUpdate, jsonFallback } from '../updater';
5
6
 
6
7
  import { decorateList, type DependencyList } from './shared';
7
8
 
@@ -21,13 +22,12 @@ export function pnpmProject(cwd: string): DependencyList {
21
22
  name: parsed.name,
22
23
  watch: ['.'],
23
24
  depends: [],
25
+ versionFallback: jsonFallback({ path: 'package.json', json: 'version' }),
24
26
  updates: [
25
- {
26
- kind: 'regex',
27
+ jsonUpdate({
27
28
  path: 'package.json',
28
- search: '"version"\\s*:\\s*"[^"]+"',
29
- replace: '"version": "{{version}}"',
30
- },
29
+ json: 'version',
30
+ }),
31
31
  ],
32
32
  });
33
33
  }
@@ -114,7 +114,10 @@ export function pnpmWorkspace(cwd: string): DependencyList {
114
114
 
115
115
  for (const info of packageInfos) {
116
116
  const relativePath = path.relative(cwd, info.memberPath);
117
- const relativePJson = path.join(relativePath, 'package.json');
117
+ const posixRelativePath = relativePath.split(path.sep).join(path.posix.sep);
118
+ const relativePJson = posixRelativePath
119
+ ? path.posix.join(posixRelativePath, 'package.json')
120
+ : 'package.json';
118
121
 
119
122
  const parsed = JSON.parse(info.content);
120
123
  const depends: string[] = [];
@@ -134,15 +137,14 @@ export function pnpmWorkspace(cwd: string): DependencyList {
134
137
 
135
138
  configs.push({
136
139
  name: info.name,
137
- watch: [relativePath],
140
+ watch: [posixRelativePath || '.'],
138
141
  depends,
142
+ versionFallback: jsonFallback({ path: relativePJson, json: 'version' }),
139
143
  updates: [
140
- {
141
- kind: 'regex',
144
+ jsonUpdate({
142
145
  path: relativePJson,
143
- search: '"version"\\s*:\\s*"[^"]+"',
144
- replace: '"version": "{{version}}"',
145
- },
146
+ json: 'version',
147
+ }),
146
148
  ],
147
149
  });
148
150
  }
@@ -1,12 +1,5 @@
1
- import type { ChangelogUpdate, RegexUpdate, DependencyConfig, UpdateAction } from '../types';
2
-
3
- export function changelogUpdate(options: Omit<ChangelogUpdate, 'kind'>): ChangelogUpdate {
4
- return { kind: 'changelog', ...options };
5
- }
6
-
7
- export function regexUpdate(options: Omit<RegexUpdate, 'kind'>): RegexUpdate {
8
- return { kind: 'regex', ...options };
9
- }
1
+ import type { DependencyConfig } from '../types';
2
+ import type { UpdateAction } from '../updater';
10
3
 
11
4
  export class SingleDependencyBuilder {
12
5
  constructor(private config: DependencyConfig) { }
@@ -0,0 +1,15 @@
1
+ import type { CascadeRules, SizePatterns } from './types';
2
+
3
+ export const defaultSizes: SizePatterns = {
4
+ major: { pattern: '^[a-z]+(?:\\([^)]+\\))?!:|BREAKING CHANGE' },
5
+ minor: { pattern: '^feat|^revert' },
6
+ patch: { pattern: '^fix|^build|^refactor|^nit|^style' },
7
+ skip: { pattern: '^release|^chore|^infra|^docs|^test|^ci|^build' },
8
+ };
9
+
10
+ export const defaultCascadeRules: Required<CascadeRules> = {
11
+ skip: { skip: 'skip', patch: 'patch', minor: 'patch', major: 'patch' },
12
+ patch: { skip: 'patch', patch: 'patch', minor: 'minor', major: 'minor' },
13
+ minor: { skip: 'minor', patch: 'minor', minor: 'minor', major: 'minor' },
14
+ major: { skip: 'major', patch: 'major', minor: 'major', major: 'major' },
15
+ };
package/src/index.ts CHANGED
@@ -4,3 +4,4 @@ export * from './vcs';
4
4
  export * from './builder';
5
5
  export * from './print';
6
6
  export * from './types';
7
+ export * from './updater';
package/src/prepare.ts CHANGED
@@ -1,28 +1,19 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import {
5
- defaultSizes,
6
- defaultCascadeRules,
7
- type BumpSize,
8
- type Commit,
9
- type VcsProvider,
10
- type DependencyConfig,
11
- type DependencyUpdateReport,
12
- type IntermediateReport,
13
- type PrepareOptions,
14
- type SizePatterns,
15
- type CascadeRules,
16
- type UpdateAction,
17
- type ChangelogContext,
4
+ import { defaultCascadeRules, defaultSizes } from './default-data';
5
+ import type {
6
+ BumpSize,
7
+ Commit,
8
+ VcsProvider,
9
+ DependencyConfig,
10
+ DependencyUpdateReport,
11
+ IntermediateReport,
12
+ PrepareOptions,
13
+ SizePatterns,
14
+ CascadeRules,
18
15
  } from './types';
19
-
20
- export function getTomlValue(content: string): string | null {
21
- const packageMatch = content.match(/\[package\][^]*?(?=^\[|z)/);
22
- const targetText = packageMatch ? packageMatch[0] : content;
23
- const versionMatch = targetText.match(/^version\s*=\s*"([^"]+)"/m);
24
- return versionMatch && versionMatch[1] ? versionMatch[1] : null;
25
- }
16
+ import type { UpdateAction, UpdateActionResolved } from './updater';
26
17
 
27
18
  export function bumpVersion(version: string, size: BumpSize): string {
28
19
  const parts = version.split('.').map(Number);
@@ -37,12 +28,32 @@ export function bumpVersion(version: string, size: BumpSize): string {
37
28
  return version;
38
29
  }
39
30
 
40
- export function getCurrentVersion(dep: DependencyConfig, cwd: string): string {
41
- const tomlUpdate = dep.updates?.find((u) => u.kind === 'toml');
42
- if (!tomlUpdate) return '0.0.0';
43
- const filePath = path.resolve(cwd, tomlUpdate.path);
44
- if (!fs.existsSync(filePath)) return '0.0.0';
45
- return getTomlValue(fs.readFileSync(filePath, 'utf8')) || '0.0.0';
31
+ export async function getCurrentVersion(
32
+ dep: DependencyConfig,
33
+ vcs: VcsProvider,
34
+ cwd: string,
35
+ ): Promise<{ version: string | null; isFallback: boolean }> {
36
+ // 1. Look for releases in git history first
37
+ const latestTag = await vcs.getLatestTag(dep.name);
38
+ if (latestTag) {
39
+ let version = latestTag;
40
+
41
+ // Strip package-specific prefixes (e.g., "api-v1.0.0" or "api/v1.0.0" -> "v1.0.0")
42
+ if (version.startsWith(`${dep.name}-`)) {
43
+ version = version.slice(dep.name.length + 1);
44
+ } else if (version.startsWith(`${dep.name}/`)) {
45
+ version = version.slice(dep.name.length + 1);
46
+ }
47
+
48
+ return { version: version.replace(/^v/, ''), isFallback: false };
49
+ }
50
+
51
+ // 2. Resolve via user-defined fallback
52
+ if (dep.versionFallback) {
53
+ return { version: dep.versionFallback.readFallback(cwd), isFallback: true };
54
+ }
55
+
56
+ return { version: null, isFallback: false };
46
57
  }
47
58
 
48
59
  export function matchBumpSize(message: string, sizes: SizePatterns): BumpSize {
@@ -125,18 +136,35 @@ export async function initReportItems(
125
136
  ): Promise<IntermediateReport[]> {
126
137
  const reports: IntermediateReport[] = [];
127
138
  for (const dep of cargoDeps) {
128
- const currentVersion = getCurrentVersion(dep, cwd);
139
+ const { version: currentVersion, isFallback } = await getCurrentVersion(dep, vcs, cwd);
140
+ const isMissingVersion = currentVersion === null;
141
+ const actualVersion = currentVersion || '0.0.0';
142
+
129
143
  const commits = await vcs.getCommits(dep.name, dep.watch || []);
130
144
  const selfBump = evaluateCommitsBump(commits, sizes);
145
+
146
+ let isErroneous = isMissingVersion;
147
+ for (const u of dep.updates || []) {
148
+ if (u.required) {
149
+ const filePath = path.resolve(cwd, u.path);
150
+ if (!fs.existsSync(filePath)) {
151
+ isErroneous = true;
152
+ break;
153
+ }
154
+ }
155
+ }
156
+
131
157
  reports.push({
132
158
  name: dep.name,
133
- currentVersion,
134
- newVersion: currentVersion,
159
+ currentVersion: actualVersion,
160
+ newVersion: actualVersion,
135
161
  bump: selfBump,
136
162
  originalBump: selfBump,
137
163
  commits,
138
164
  updates: dep.updates || [],
139
165
  depends: dep.depends || [],
166
+ isErroneous,
167
+ isFirstRelease: isFallback,
140
168
  });
141
169
  }
142
170
  return reports;
@@ -155,39 +183,6 @@ export async function prepare(
155
183
  return finalizeReports(sorted);
156
184
  }
157
185
 
158
- export function buildChangelogBlock(version: string, commits: Commit[]): string {
159
- const date = new Date().toISOString().split('T')[0] ?? '';
160
- let block = `## [${version}] - ${date}\n\n`;
161
-
162
- if (commits.length === 0) {
163
- return block + `*No notable changes.*\n`;
164
- }
165
-
166
- const breaking: string[] = [];
167
- const features: string[] = [];
168
- const fixes: string[] = [];
169
- const others: string[] = [];
170
-
171
- for (const c of commits) {
172
- if (c.message.includes('!:') || c.message.includes('BREAKING CHANGE')) {
173
- breaking.push(`- ${c.hash} ${c.message}`);
174
- } else if (c.message.startsWith('feat')) {
175
- features.push(`- ${c.hash} ${c.message}`);
176
- } else if (c.message.startsWith('fix')) {
177
- fixes.push(`- ${c.hash} ${c.message}`);
178
- } else {
179
- others.push(`- ${c.hash} ${c.message}`);
180
- }
181
- }
182
-
183
- if (breaking.length) block += `### ⚠️ BREAKING CHANGES\n${breaking.join('\n')}\n\n`;
184
- if (features.length) block += `### ✨ Features\n${features.join('\n')}\n\n`;
185
- if (fixes.length) block += `### 🐛 Bug Fixes\n${fixes.join('\n')}\n\n`;
186
- if (others.length) block += `### 🛠 Other Changes\n${others.join('\n')}\n\n`;
187
-
188
- return block.trim() + '\n';
189
- }
190
-
191
186
  export function finalizeReports(sorted: IntermediateReport[]): DependencyUpdateReport[] {
192
187
  const allCommitsMap = new Map<string, Commit>();
193
188
  for (const item of sorted) {
@@ -206,57 +201,22 @@ export function finalizeReports(sorted: IntermediateReport[]): DependencyUpdateR
206
201
  commits: item.commits,
207
202
  updates: mapResolvedUpdates(item.updates, item.newVersion, item.commits, globalCommits),
208
203
  depends: item.depends,
204
+ isErroneous: item.isErroneous,
205
+ isFirstRelease: item.isFirstRelease,
209
206
  }));
210
207
  }
211
208
 
212
- export function defaultChangelogTemplate({ version, date, commits }: ChangelogContext): string {
213
- let block = `## [${version}] - ${date}\n\n`;
214
- if (commits.length === 0) return block + `*No notable changes.*\n`;
215
-
216
- const groups: { breaking: string[]; feat: string[]; fix: string[]; other: string[] } = {
217
- breaking: [],
218
- feat: [],
219
- fix: [],
220
- other: [],
221
- };
222
-
223
- for (const c of commits) {
224
- const item = `- ${c.shortHash} ${c.message}`;
225
- if (c.isBreaking) groups.breaking.push(item);
226
- else if (c.type === 'feat') groups.feat.push(item);
227
- else if (c.type === 'fix') groups.fix.push(item);
228
- else groups.other.push(item);
229
- }
230
-
231
- if (groups.breaking.length) block += `### ⚠️ BREAKING CHANGES\n${groups.breaking.join('\n')}\n\n`;
232
- if (groups.feat.length) block += `### ✨ Features\n${groups.feat.join('\n')}\n\n`;
233
- if (groups.fix.length) block += `### 🐛 Bug Fixes\n${groups.fix.join('\n')}\n\n`;
234
- if (groups.other.length) block += `### 🛠 Other Changes\n${groups.other.join('\n')}\n\n`;
235
-
236
- return block.trim() + '\n';
237
- }
238
-
239
209
  export function mapResolvedUpdates(
240
210
  updates: UpdateAction[],
241
211
  newVersion: string,
242
212
  crateCommits: Commit[],
243
213
  globalCommits: Commit[],
244
- ): UpdateAction[] {
245
- return updates.map((u) => {
246
- if (u.kind === 'regex') {
247
- return { ...u, resolvedReplace: u.replace.replace('{{version}}', newVersion) };
248
- }
249
- if (u.kind === 'changelog') {
250
- const targetCommits = u.global ? globalCommits : crateCommits;
251
- const context: ChangelogContext = {
252
- version: newVersion,
253
- date: new Date().toISOString().split('T')[0] ?? '',
254
- commits: targetCommits,
255
- };
256
-
257
- const content = u.template ? u.template(context) : defaultChangelogTemplate(context);
258
- return { ...u, resolvedBlock: content };
259
- }
260
- return u;
261
- });
214
+ ): UpdateActionResolved[] {
215
+ return updates.map((act) =>
216
+ act.prepare({
217
+ newVersion,
218
+ crateCommits,
219
+ globalCommits,
220
+ }),
221
+ );
262
222
  }
package/src/print.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { defaultSizes } from './default-data';
1
2
  import { matchBumpSize } from './prepare';
2
- import { defaultSizes } from './types';
3
3
  import type { DependencyUpdateReport } from './types';
4
4
 
5
5
  // ANSI Escape Codes for Terminal Colors
@@ -57,14 +57,19 @@ function getIconForFile(filename: string): string {
57
57
 
58
58
  return icons[ext] || '󰈚'; // Default file icon
59
59
  }
60
+
60
61
  export function prettyPrint(reports: DependencyUpdateReport[]): void {
61
62
  for (const report of reports) {
62
63
  if (report.bump === 'skip') continue;
63
64
 
64
65
  const bumpColor =
65
66
  report.bump === 'major' ? c.red : report.bump === 'minor' ? c.yellow : c.green;
67
+
68
+ // Check if it's a first release (fallback was used instead of finding tags)
69
+ const firstReleaseBadge = report.isFirstRelease ? ` ${c.cyan}🌱 (first release)${c.reset}` : '';
70
+
66
71
  process.stdout.write(
67
- `${c.bold}📦 ${report.name.padEnd(12)}${c.reset} ${highlightVersion(report.currentVersion, report.newVersion)} ${bumpColor}${c.bold}[${report.bump}]${c.reset}\n`,
72
+ `${c.bold}📦 ${report.name.padEnd(12)}${c.reset} ${highlightVersion(report.currentVersion, report.newVersion)} ${bumpColor}${c.bold}[${report.bump}]${c.reset}${firstReleaseBadge}\n`,
68
73
  );
69
74
 
70
75
  const files = report.updates.map(
@@ -80,8 +85,9 @@ export function prettyPrint(reports: DependencyUpdateReport[]): void {
80
85
 
81
86
  const marker = affectsBump ? `${c.green}✦${c.reset}` : `${c.gray}○${c.reset}`;
82
87
 
88
+ // User string is now wrapped with <>
83
89
  console.log(
84
- ` ${marker} ${c.yellow}${commit.shortHash}${c.reset} ${commit.message} ${c.gray}${commit.author} ${commit.date}${c.reset}`,
90
+ ` ${marker} ${c.yellow}${commit.shortHash}${c.reset} ${commit.message} ${c.gray}<${commit.author}> ${commit.date}${c.reset}`,
85
91
  );
86
92
  }
87
93
 
@@ -103,7 +109,6 @@ export function prettyPrint(reports: DependencyUpdateReport[]): void {
103
109
  );
104
110
  }
105
111
 
106
- // Changelog Preview
107
112
  console.log('');
108
113
  }
109
114
  }
package/src/run.ts CHANGED
@@ -4,73 +4,26 @@ import path from 'node:path';
4
4
  import type { DependencyUpdateReport, VcsProvider } from './types';
5
5
 
6
6
  /**
7
- * Applies calculated updates to files on disk, stages, commits, and tags using the provided VcsProvider.
7
+ * Builds the multi-line commit message showcasing version bumps, included commits,
8
+ * cascade triggers, and first-release indicators.
8
9
  */
9
- export async function run(
10
- reports: DependencyUpdateReport[],
11
- vcs: VcsProvider,
12
- options: { cwd: string },
13
- ): Promise<void> {
14
- const cwd = options.cwd;
15
-
16
- // 1. Apply file updates
17
- for (const report of reports) {
18
- for (const u of report.updates) {
19
- const filePath = path.resolve(cwd, u.path);
20
- const dirPath = path.dirname(filePath);
21
- if (!fs.existsSync(dirPath)) {
22
- fs.mkdirSync(dirPath, { recursive: true });
23
- }
24
-
25
- if (u.kind === 'toml') {
26
- if (fs.existsSync(filePath)) {
27
- let content = fs.readFileSync(filePath, 'utf8');
28
- // Update package version inside the target Cargo.toml package block
29
- content = content.replace(
30
- /(\[package\][^]*?^version\s*=\s*")[^"]+(")/m,
31
- `$1${report.newVersion}$2`,
32
- );
33
- // Sync workspace dependency entries if internal dependencies changed
34
- for (const depReport of reports) {
35
- if (depReport.name === report.name) continue;
36
- const depRegex = new RegExp(
37
- `(${depReport.name}\\s*=\\s*\\{[^}]*version\\s*=\\s*")[^"]+(")`,
38
- 'g',
39
- );
40
- content = content.replace(depRegex, `$1${depReport.newVersion}$2`);
41
- }
42
- fs.writeFileSync(filePath, content);
43
- }
44
- } else if (u.kind === 'regex') {
45
- if (fs.existsSync(filePath)) {
46
- let content = fs.readFileSync(filePath, 'utf8');
47
- const searchRegex = new RegExp(u.search, 'g');
48
- content = content.replace(searchRegex, u.resolvedReplace ?? '');
49
- fs.writeFileSync(filePath, content);
50
- }
51
- } else if (u.kind === 'changelog') {
52
- const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
53
- const newContent = (u.resolvedBlock ?? '') + '\n' + oldContent;
54
- fs.writeFileSync(filePath, newContent.trim() + '\n');
55
- }
56
- }
57
- }
58
-
59
- // 2. Build multi-line commit message
10
+ export function generateCommitMessage(reports: DependencyUpdateReport[]): string | null {
60
11
  const activeReports = reports.filter((r) => r.bump !== 'skip');
61
12
  if (activeReports.length === 0) {
62
- return;
13
+ return null;
63
14
  }
64
15
 
65
16
  const tagList = activeReports.map((r) => `${r.name}-v${r.newVersion}`).join(', ');
66
17
  let commitMessage = `chore: release ${tagList}\n\n`;
67
18
 
68
19
  for (const report of activeReports) {
69
- commitMessage += `${report.name} (${report.currentVersion} -> ${report.newVersion}) [${report.bump}]\n`;
20
+ const firstReleaseBadge = report.isFirstRelease ? ' (first release)' : '';
21
+
22
+ commitMessage += `${report.name} (${report.currentVersion} -> ${report.newVersion}) [${report.bump}]${firstReleaseBadge}\n`;
70
23
 
71
24
  // Add commits
72
25
  for (const commit of report.commits) {
73
- commitMessage += ` - ${commit.shortHash} ${commit.message} (${commit.author} ${commit.date})\n`;
26
+ commitMessage += ` - ${commit.shortHash} ${commit.message} (<${commit.author}> ${commit.date})\n`;
74
27
  }
75
28
 
76
29
  // Add cascades
@@ -86,10 +39,51 @@ export async function run(
86
39
  commitMessage += '\n';
87
40
  }
88
41
 
42
+ return commitMessage.trim();
43
+ }
44
+
45
+ /**
46
+ * Applies calculated updates to files on disk, stages, commits, and tags using the provided VcsProvider.
47
+ */
48
+ export async function run(
49
+ reports: DependencyUpdateReport[],
50
+ vcs: VcsProvider,
51
+ options: { cwd: string },
52
+ ): Promise<void> {
53
+ const erroneous = reports.filter((r) => r.isErroneous);
54
+ if (erroneous.length > 0) {
55
+ const names = erroneous.map((r) => r.name).join(', ');
56
+ throw new Error(
57
+ `Cannot run because the following packages are missing versions or required updates: ${names}`,
58
+ );
59
+ }
60
+
61
+ const cwd = options.cwd;
62
+
63
+ // 1. Apply file updates
64
+ for (const report of reports) {
65
+ for (const u of report.updates) {
66
+ const filePath = path.resolve(cwd, u.path);
67
+ const dirPath = path.dirname(filePath);
68
+ if (!fs.existsSync(dirPath)) {
69
+ fs.mkdirSync(dirPath, { recursive: true });
70
+ }
71
+
72
+ u.apply(report, reports, cwd);
73
+ }
74
+ }
75
+
76
+ // 2. Build multi-line commit message
77
+ const commitMessage = generateCommitMessage(reports);
78
+ if (!commitMessage) {
79
+ return;
80
+ }
81
+
89
82
  // 3. Stage and execute commit
90
- await vcs.commit(commitMessage.trim());
83
+ await vcs.commit(commitMessage);
91
84
 
92
85
  // 4. Create tags
86
+ const activeReports = reports.filter((r) => r.bump !== 'skip');
93
87
  for (const report of activeReports) {
94
88
  const tagName = `${report.name}-v${report.newVersion}`;
95
89
  await vcs.tag(tagName);