relacher 0.0.2 → 0.0.4-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Emilien Jegou
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -6,12 +6,7 @@
6
6
 
7
7
  **Relacher** is a scriptable, workspace-aware release orchestration library designed for monorepos.
8
8
 
9
- ```
10
- 📦 cli_tool 3.1.2 → 3.3.0 [minor]
11
- flake.nix README.md CHANGELOG.md CHANGELOG.md
12
- ✦ 4a0ce62 feat(ui): add progress indicator to execution flow Author 2025-01-30
13
- ✦ Cascaded as [minor] from deps: plugin_api [minor]
14
- ```
9
+ ![Relacher](./docs/assets/relacher.png)
15
10
 
16
11
  ## Features
17
12
 
@@ -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';
@@ -0,0 +1,56 @@
1
+ import type { Commit, DependencyUpdateReport } from '../types';
2
+ export type UpdateActionOptions = {
3
+ newVersion: string;
4
+ globalCommits: Commit[];
5
+ crateCommits: Commit[];
6
+ };
7
+ export interface UpdateActionResolved {
8
+ kind: string;
9
+ targetPath: string;
10
+ apply(report: DependencyUpdateReport, reports: DependencyUpdateReport[], cwd: string): void;
11
+ params: any;
12
+ preparedData: any;
13
+ }
14
+ export interface UpdateAction {
15
+ kind: string;
16
+ path: string;
17
+ required?: boolean;
18
+ params: any;
19
+ prepare(data: UpdateActionOptions): UpdateActionResolved;
20
+ }
21
+ export interface VersionFallback {
22
+ readFallback(cwd: string): string | null;
23
+ }
24
+ export type ApplyActionFnArgs<T, K = undefined> = {
25
+ params: T;
26
+ targetPath: string;
27
+ options: UpdateActionOptions;
28
+ report: DependencyUpdateReport;
29
+ reports: DependencyUpdateReport[];
30
+ cwd: string;
31
+ preparedData: K;
32
+ };
33
+ type ApplyActionFn<T, K = undefined> = (args: ApplyActionFnArgs<T, K>) => void;
34
+ export type PrepareActionFnArgs<T> = {
35
+ params: T;
36
+ targetPath: string;
37
+ options: UpdateActionOptions;
38
+ };
39
+ type PrepareActionFn<T, K> = (args: PrepareActionFnArgs<T>) => K;
40
+ type UpdateBuilderParams<T, K = undefined> = {
41
+ kind: string;
42
+ apply: ApplyActionFn<T, K>;
43
+ prepare?: PrepareActionFn<T, K>;
44
+ };
45
+ export declare const updateBuilder: <T, K = undefined>({ kind, apply, prepare }: UpdateBuilderParams<T, K>) => (targetPath: string, params: T & {
46
+ required?: boolean;
47
+ }) => {
48
+ kind: string;
49
+ path: string;
50
+ params: T & {
51
+ required?: boolean;
52
+ };
53
+ required: boolean | undefined;
54
+ prepare(options: UpdateActionOptions): UpdateActionResolved;
55
+ };
56
+ export {};
@@ -1,10 +1,18 @@
1
- import type { ChangelogContext } from '../types';
2
- import type { UpdateAction } from '.';
1
+ import { type ChangelogContext } from '../types';
3
2
  export type ChangelogUpdateParams = {
4
- path: string;
5
3
  global?: boolean;
6
4
  template?: (ctx: ChangelogContext) => string;
7
5
  required?: boolean;
8
6
  };
9
- export declare const changelogUpdate: (params: ChangelogUpdateParams) => UpdateAction;
7
+ export declare const changelogUpdate: (targetPath: string, params: ChangelogUpdateParams & {
8
+ required?: boolean;
9
+ }) => {
10
+ kind: string;
11
+ path: string;
12
+ params: ChangelogUpdateParams & {
13
+ required?: boolean;
14
+ };
15
+ required: boolean | undefined;
16
+ prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
17
+ };
10
18
  export declare function defaultChangelogTemplate({ version, date, commits }: ChangelogContext): string;
@@ -1,26 +1,5 @@
1
- import type { Commit, DependencyUpdateReport } from '../types';
1
+ export * from './builder';
2
2
  export * from './changelog';
3
3
  export * from './json';
4
4
  export * from './regex';
5
5
  export * from './toml';
6
- export type UpdateActionOptions = {
7
- newVersion: string;
8
- globalCommits: Commit[];
9
- crateCommits: Commit[];
10
- };
11
- export interface UpdateActionResolved {
12
- kind: string;
13
- path: string;
14
- apply(report: DependencyUpdateReport, reports: DependencyUpdateReport[], cwd: string): void;
15
- params: any;
16
- }
17
- export interface UpdateAction {
18
- kind: string;
19
- path: string;
20
- required?: boolean;
21
- params: any;
22
- prepare(data: UpdateActionOptions): UpdateActionResolved;
23
- }
24
- export interface VersionFallback {
25
- readFallback(cwd: string): string | null;
26
- }
@@ -1,12 +1,19 @@
1
- import type { VersionFallback, UpdateAction } from '.';
1
+ import type { DependencyUpdateReport } from '../types';
2
+ import { type VersionFallback } from '.';
2
3
  export type JsonFallbackParams = {
3
4
  path: string;
4
- json: string;
5
+ read: (parsed: any) => string | null | undefined;
5
6
  };
6
- export type JsonUpdateParams = {
7
- path: string;
8
- json: string;
7
+ export type JsonUpdateParams = (parsed: any, report: DependencyUpdateReport, reports: DependencyUpdateReport[]) => void;
8
+ export declare const jsonFallback: (params: JsonFallbackParams) => VersionFallback;
9
+ export declare const jsonUpdate: (targetPath: string, params: JsonUpdateParams & {
9
10
  required?: boolean;
11
+ }) => {
12
+ kind: string;
13
+ path: string;
14
+ params: JsonUpdateParams & {
15
+ required?: boolean;
16
+ };
17
+ required: boolean | undefined;
18
+ prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
10
19
  };
11
- export declare const jsonFallback: (params: JsonFallbackParams) => VersionFallback;
12
- export declare const jsonUpdate: (params: JsonUpdateParams) => UpdateAction;
@@ -1,13 +1,22 @@
1
- import type { VersionFallback, UpdateAction } from '.';
1
+ import { type VersionFallback } from '.';
2
2
  export type RegexFallbackParams = {
3
3
  path: string;
4
4
  search: string;
5
5
  };
6
6
  export type RegexUpdateParams = {
7
- path: string;
8
7
  search: string;
9
8
  replace: string;
10
9
  required?: boolean;
11
10
  };
12
11
  export declare const regexFallback: (params: RegexFallbackParams) => VersionFallback;
13
- export declare const regexUpdate: (params: RegexUpdateParams) => UpdateAction;
12
+ export declare const regexUpdate: (targetPath: string, params: RegexUpdateParams & {
13
+ required?: boolean;
14
+ }) => {
15
+ kind: string;
16
+ path: string;
17
+ params: RegexUpdateParams & {
18
+ required?: boolean;
19
+ };
20
+ required: boolean | undefined;
21
+ prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
22
+ };
@@ -1,12 +1,19 @@
1
- import type { VersionFallback, UpdateAction } from '.';
1
+ import type { DependencyUpdateReport } from '../types';
2
+ import { type VersionFallback } from '.';
2
3
  export type TomlFallbackParams = {
3
4
  path: string;
4
- toml: string;
5
+ read: (parsed: any) => string | null | undefined;
5
6
  };
6
- export type TomlUpdateParams = {
7
- path: string;
8
- toml: string;
7
+ export type TomlUpdateParams = (parsed: any, report: DependencyUpdateReport, reports: DependencyUpdateReport[]) => void;
8
+ export declare const tomlFallback: (params: TomlFallbackParams) => VersionFallback;
9
+ export declare const tomlUpdate: (targetPath: string, params: TomlUpdateParams & {
9
10
  required?: boolean;
11
+ }) => {
12
+ kind: string;
13
+ path: string;
14
+ params: TomlUpdateParams & {
15
+ required?: boolean;
16
+ };
17
+ required: boolean | undefined;
18
+ prepare(options: import("./builder").UpdateActionOptions): import("./builder").UpdateActionResolved;
10
19
  };
11
- export declare const tomlFallback: (params: TomlFallbackParams) => VersionFallback;
12
- export declare const tomlUpdate: (params: TomlUpdateParams) => UpdateAction;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relacher",
3
- "version": "0.0.2",
3
+ "version": "0.0.4-rc.0",
4
4
  "description": "Scriptable release orchestration library for monorepos",
5
5
  "type": "module",
6
6
  "private": false,
@@ -56,6 +56,7 @@
56
56
  "oxfmt": "^0.19.0",
57
57
  "oxlint": "^1.64.0",
58
58
  "prettier": "^3.8.3",
59
+ "smol-toml": "^1.6.1",
59
60
  "typescript": "^6.0.3"
60
61
  }
61
62
  }
@@ -1,8 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ import { parse } from 'smol-toml';
5
+
4
6
  import type { DependencyConfig } from '../types';
5
- import { regexUpdate, tomlUpdate, tomlFallback } from '../updater';
7
+ import { tomlUpdate, tomlFallback } from '../updater';
6
8
 
7
9
  import { decorateList, type DependencyList } from './shared';
8
10
 
@@ -16,19 +18,49 @@ export interface CargoBuilderOptions {
16
18
  export function cargoProject(cwd: string): DependencyList {
17
19
  const configs: DependencyConfig[] = [];
18
20
  const rootCargo = path.join(cwd, 'Cargo.toml');
21
+ const lockCargo = path.join(cwd, 'Cargo.lock');
22
+ const hasLock = fs.existsSync(lockCargo);
19
23
 
20
24
  if (fs.existsSync(rootCargo)) {
21
- const fileContent = fs.readFileSync(rootCargo, 'utf8');
22
- const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
23
- const parsedName = nameMatch?.[1] ?? '';
24
- if (parsedName) {
25
- configs.push({
26
- name: parsedName,
27
- watch: ['.'],
28
- depends: [],
29
- versionFallback: tomlFallback({ path: 'Cargo.toml', toml: 'package.version' }),
30
- updates: [tomlUpdate({ path: 'Cargo.toml', toml: 'package.version' })],
31
- });
25
+ try {
26
+ const fileContent = fs.readFileSync(rootCargo, 'utf8');
27
+ const doc = parse(fileContent) as any;
28
+ const parsedName = doc.package?.name;
29
+
30
+ if (parsedName) {
31
+ const updates = [
32
+ tomlUpdate('Cargo.toml', (d, r) => {
33
+ if (d.package) d.package.version = r.newVersion;
34
+ }),
35
+ ];
36
+
37
+ if (hasLock) {
38
+ updates.push(
39
+ tomlUpdate('Cargo.lock', (d, r) => {
40
+ if (Array.isArray(d.package)) {
41
+ // Only update local packages (missing source/checksum attributes)
42
+ const pkg = d.package.find(
43
+ (p: any) => p.name === parsedName && !p.source && !p.checksum,
44
+ );
45
+ if (pkg) pkg.version = r.newVersion;
46
+ }
47
+ }),
48
+ );
49
+ }
50
+
51
+ configs.push({
52
+ name: parsedName,
53
+ watch: ['.'],
54
+ depends: [],
55
+ versionFallback: tomlFallback({
56
+ path: 'Cargo.toml',
57
+ read: (d) => d.package?.version,
58
+ }),
59
+ updates,
60
+ });
61
+ }
62
+ } catch {
63
+ // Ignore parse error
32
64
  }
33
65
  }
34
66
 
@@ -41,118 +73,162 @@ export function cargoProject(cwd: string): DependencyList {
41
73
  export function cargoWorkspace(cwd: string, options?: CargoBuilderOptions): DependencyList {
42
74
  const configs: DependencyConfig[] = [];
43
75
  const rootCargo = path.join(cwd, 'Cargo.toml');
76
+ const lockCargo = path.join(cwd, 'Cargo.lock');
77
+ const hasLock = fs.existsSync(lockCargo);
44
78
  const discoveredPaths = new Set<string>();
45
79
 
46
80
  if (fs.existsSync(rootCargo)) {
47
- const content = fs.readFileSync(rootCargo, 'utf8');
48
- const membersMatch = content.match(/members\s*=\s*\[([^\]]+)\]/);
49
- if (membersMatch && membersMatch[1]) {
50
- const rawMembers = membersMatch[1].split(',').map((m) => m.replace(/["\s]/g, ''));
51
- const crateInfos: { name: string; memberPath: string; content: string }[] = [];
52
-
53
- for (const member of rawMembers) {
54
- if (!member) continue;
55
- const globPath = path.join(cwd, member);
56
- const cargoPaths = globPath.endsWith('*')
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
- : []
62
- : [path.join(globPath, 'Cargo.toml')];
63
-
64
- for (const p of cargoPaths) {
65
- if (fs.existsSync(p)) {
66
- const memberPath = path.dirname(p);
67
- discoveredPaths.add(memberPath);
68
- const fileContent = fs.readFileSync(p, 'utf8');
69
- const nameMatch = fileContent.match(/^name\s*=\s*"([^"]+)"/m);
70
- const parsedName = nameMatch?.[1] ?? '';
71
- if (parsedName) {
72
- crateInfos.push({
73
- name: parsedName,
74
- memberPath,
75
- content: fileContent,
76
- });
81
+ try {
82
+ const content = fs.readFileSync(rootCargo, 'utf8');
83
+ const rootDoc = parse(content) as any;
84
+ const members = rootDoc.workspace?.members;
85
+
86
+ if (Array.isArray(members)) {
87
+ const crateInfos: { name: string; memberPath: string; doc: any }[] = [];
88
+
89
+ for (const member of members) {
90
+ const globPath = path.join(cwd, member);
91
+ const cargoPaths = globPath.endsWith('*')
92
+ ? fs.existsSync(path.dirname(globPath))
93
+ ? fs
94
+ .readdirSync(path.dirname(globPath))
95
+ .map((dir) => path.join(path.dirname(globPath), dir, 'Cargo.toml'))
96
+ : []
97
+ : [path.join(globPath, 'Cargo.toml')];
98
+
99
+ for (const p of cargoPaths) {
100
+ if (fs.existsSync(p)) {
101
+ const memberPath = path.dirname(p);
102
+ discoveredPaths.add(memberPath);
103
+ const fileContent = fs.readFileSync(p, 'utf8');
104
+ const doc = parse(fileContent) as any;
105
+ const parsedName = doc.package?.name;
106
+ if (parsedName) {
107
+ crateInfos.push({ name: parsedName, memberPath, doc });
108
+ }
77
109
  }
78
110
  }
79
111
  }
80
- }
81
112
 
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;
113
+ // Discover nested packages dynamically via path dependencies on the parsed objects
114
+ let i = 0;
115
+ while (i < crateInfos.length) {
116
+ const info = crateInfos[i]!;
117
+ const deps = [
118
+ ...(Object.values(info.doc.dependencies || {}) as any[]),
119
+ ...(Object.values(info.doc['dev-dependencies'] || {}) as any[]),
120
+ ...(Object.values(info.doc['build-dependencies'] || {}) as any[]),
121
+ ];
122
+
123
+ for (const dep of deps) {
124
+ if (dep && typeof dep === 'object' && dep.path) {
125
+ const fullDepPath = path.resolve(info.memberPath, dep.path);
126
+ if (!discoveredPaths.has(fullDepPath)) {
127
+ discoveredPaths.add(fullDepPath);
128
+ const cargoPath = path.join(fullDepPath, 'Cargo.toml');
129
+
130
+ if (fs.existsSync(cargoPath)) {
131
+ const fileContent = fs.readFileSync(cargoPath, 'utf8');
132
+ const doc = parse(fileContent) as any;
133
+ const parsedName = doc.package?.name;
134
+ if (parsedName) {
135
+ crateInfos.push({ name: parsedName, memberPath: fullDepPath, doc });
136
+ }
137
+ }
138
+ }
139
+ }
96
140
  }
141
+ i++;
142
+ }
97
143
 
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,
112
- content: fileContent,
113
- });
144
+ for (const info of crateInfos) {
145
+ const relativePath = path.relative(cwd, info.memberPath);
146
+ const posixRelativePath = relativePath.split(path.sep).join(path.posix.sep);
147
+ const relativeCargo = posixRelativePath
148
+ ? path.posix.join(posixRelativePath, 'Cargo.toml')
149
+ : 'Cargo.toml';
150
+
151
+ const depends: string[] = [];
152
+ const allDeps = {
153
+ ...(info.doc.dependencies || {}),
154
+ ...(info.doc['dev-dependencies'] || {}),
155
+ ...(info.doc['build-dependencies'] || {}),
156
+ };
157
+
158
+ for (const other of crateInfos) {
159
+ if (other.name === info.name) continue;
160
+ if (allDeps[other.name]) {
161
+ depends.push(other.name);
114
162
  }
115
163
  }
116
- }
117
- i++;
118
- }
119
164
 
120
- for (const info of crateInfos) {
121
- const relativePath = path.relative(cwd, info.memberPath);
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';
126
-
127
- const depends: string[] = [];
128
- for (const other of crateInfos) {
129
- if (other.name === info.name) continue;
130
- const depPattern = new RegExp(`^\\s*${other.name}\\s*=`, 'm');
131
- if (depPattern.test(info.content)) {
132
- depends.push(other.name);
165
+ const updates = [
166
+ tomlUpdate(relativeCargo, (doc, report, reports) => {
167
+ // Update self
168
+ if (doc.package) {
169
+ doc.package.version = report.newVersion;
170
+ }
171
+
172
+ // Keep local path workspace dependencies perfectly in sync internally
173
+ const depTypes = ['dependencies', 'dev-dependencies', 'build-dependencies'];
174
+ for (const depType of depTypes) {
175
+ if (!doc[depType]) continue;
176
+ for (const depReport of reports) {
177
+ if (depReport.name === report.name) continue;
178
+ const dep = doc[depType][depReport.name];
179
+ if (dep && typeof dep === 'object' && dep.version) {
180
+ dep.version = depReport.newVersion;
181
+ } else if (typeof dep === 'string') {
182
+ doc[depType][depReport.name] = depReport.newVersion;
183
+ }
184
+ }
185
+ }
186
+ }),
187
+ ];
188
+
189
+ if (hasLock) {
190
+ updates.push(
191
+ tomlUpdate('Cargo.lock', (doc, report) => {
192
+ if (Array.isArray(doc.package)) {
193
+ // Only update local packages (missing source/checksum attributes)
194
+ const pkg = doc.package.find(
195
+ (p: any) => p.name === info.name && !p.source && !p.checksum,
196
+ );
197
+ if (pkg) pkg.version = report.newVersion;
198
+ }
199
+ }),
200
+ );
133
201
  }
134
- }
135
202
 
136
- const updates = [tomlUpdate({ path: relativeCargo, toml: 'package.version' })];
203
+ if (options?.syncWorkspaceDeps) {
204
+ updates.push(
205
+ tomlUpdate('Cargo.toml', (doc, report) => {
206
+ if (doc.workspace?.dependencies?.[info.name]) {
207
+ const dep = doc.workspace.dependencies[info.name];
208
+ if (typeof dep === 'object' && dep.version) {
209
+ dep.version = report.newVersion;
210
+ } else if (typeof dep === 'string') {
211
+ doc.workspace.dependencies[info.name] = report.newVersion;
212
+ }
213
+ }
214
+ }),
215
+ );
216
+ }
137
217
 
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`,
218
+ configs.push({
219
+ name: info.name,
220
+ watch: [posixRelativePath || '.'],
221
+ depends,
222
+ versionFallback: tomlFallback({
223
+ path: relativeCargo,
224
+ read: (doc) => doc.package?.version,
144
225
  }),
145
- );
226
+ updates,
227
+ });
146
228
  }
147
-
148
- configs.push({
149
- name: info.name,
150
- watch: [posixRelativePath || '.'],
151
- depends,
152
- versionFallback: tomlFallback({ path: relativeCargo, toml: 'package.version' }),
153
- updates,
154
- });
155
229
  }
230
+ } catch {
231
+ // Ignore parse error
156
232
  }
157
233
  }
158
234
 
@@ -162,12 +238,17 @@ export function cargoWorkspace(cwd: string, options?: CargoBuilderOptions): Depe
162
238
  export function cargoDeps(cwd: string, options?: CargoBuilderOptions): DependencyList {
163
239
  const rootCargo = path.join(cwd, 'Cargo.toml');
164
240
  if (fs.existsSync(rootCargo)) {
165
- const content = fs.readFileSync(rootCargo, 'utf8');
166
- if (content.includes('members =')) {
167
- return cargoWorkspace(cwd, options);
168
- }
169
- if (content.includes('package =') || content.match(/^name\s*=/m)) {
170
- return cargoProject(cwd);
241
+ try {
242
+ const content = fs.readFileSync(rootCargo, 'utf8');
243
+ const doc = parse(content) as any;
244
+ if (doc.workspace?.members) {
245
+ return cargoWorkspace(cwd, options);
246
+ }
247
+ if (doc.package?.name) {
248
+ return cargoProject(cwd);
249
+ }
250
+ } catch {
251
+ // Fallback below
171
252
  }
172
253
  }
173
254
  return decorateList([]);
@@ -22,11 +22,13 @@ export function pnpmProject(cwd: string): DependencyList {
22
22
  name: parsed.name,
23
23
  watch: ['.'],
24
24
  depends: [],
25
- versionFallback: jsonFallback({ path: 'package.json', json: 'version' }),
25
+ versionFallback: jsonFallback({
26
+ path: 'package.json',
27
+ read: (parsed) => parsed.version,
28
+ }),
26
29
  updates: [
27
- jsonUpdate({
28
- path: 'package.json',
29
- json: 'version',
30
+ jsonUpdate('package.json', (parsed, report) => {
31
+ parsed.version = report.newVersion;
30
32
  }),
31
33
  ],
32
34
  });
@@ -139,11 +141,38 @@ export function pnpmWorkspace(cwd: string): DependencyList {
139
141
  name: info.name,
140
142
  watch: [posixRelativePath || '.'],
141
143
  depends,
142
- versionFallback: jsonFallback({ path: relativePJson, json: 'version' }),
144
+ versionFallback: jsonFallback({
145
+ path: relativePJson,
146
+ read: (parsed) => parsed.version,
147
+ }),
143
148
  updates: [
144
- jsonUpdate({
145
- path: relativePJson,
146
- json: 'version',
149
+ jsonUpdate(relativePJson, (parsed, report, reports) => {
150
+ // Update local package version
151
+ parsed.version = report.newVersion;
152
+
153
+ // Sync internal workspace dependencies and preserve prefixing if present
154
+ const depSections = [
155
+ 'dependencies',
156
+ 'devDependencies',
157
+ 'peerDependencies',
158
+ 'optionalDependencies',
159
+ ] as const;
160
+
161
+ for (const depReport of reports) {
162
+ if (depReport.name === report.name) continue;
163
+
164
+ for (const section of depSections) {
165
+ if (parsed[section] && parsed[section][depReport.name]) {
166
+ const currentVal = parsed[section][depReport.name];
167
+ if (typeof currentVal === 'string') {
168
+ // Match prefix structures such as '^', '~', 'workspace:^', or 'workspace:'
169
+ const match = currentVal.match(/^((?:workspace:)?([~^]?))/);
170
+ const prefix = match ? match[1] : '';
171
+ parsed[section][depReport.name] = `${prefix}${depReport.newVersion}`;
172
+ }
173
+ }
174
+ }
175
+ }
147
176
  }),
148
177
  ],
149
178
  });
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/print.ts CHANGED
@@ -74,7 +74,7 @@ export function prettyPrint(reports: DependencyUpdateReport[]): void {
74
74
 
75
75
  const files = report.updates.map(
76
76
  (u) =>
77
- `${getIconForFile(u.path.split('/').pop() || '')} ${c.dim}${u.path.split('/').pop()}${c.reset}`,
77
+ `${getIconForFile(u.targetPath.split('/').pop() || '')} ${c.dim}${u.targetPath.split('/').pop()}${c.reset}`,
78
78
  );
79
79
  console.log(` ${files.join(' ')}`);
80
80
 
package/src/run.ts CHANGED
@@ -63,7 +63,7 @@ export async function run(
63
63
  // 1. Apply file updates
64
64
  for (const report of reports) {
65
65
  for (const u of report.updates) {
66
- const filePath = path.resolve(cwd, u.path);
66
+ const filePath = path.resolve(cwd, u.targetPath);
67
67
  const dirPath = path.dirname(filePath);
68
68
  if (!fs.existsSync(dirPath)) {
69
69
  fs.mkdirSync(dirPath, { recursive: true });
@@ -0,0 +1,83 @@
1
+ import type { Commit, DependencyUpdateReport } from '../types';
2
+
3
+ export type UpdateActionOptions = {
4
+ newVersion: string;
5
+ globalCommits: Commit[];
6
+ crateCommits: Commit[];
7
+ };
8
+
9
+ export interface UpdateActionResolved {
10
+ kind: string;
11
+ targetPath: string;
12
+ apply(report: DependencyUpdateReport, reports: DependencyUpdateReport[], cwd: string): void;
13
+ params: any;
14
+ preparedData: any;
15
+ }
16
+
17
+ export interface UpdateAction {
18
+ kind: string;
19
+ path: string;
20
+ required?: boolean;
21
+ params: any;
22
+ prepare(data: UpdateActionOptions): UpdateActionResolved;
23
+ }
24
+
25
+ export interface VersionFallback {
26
+ readFallback(cwd: string): string | null;
27
+ }
28
+
29
+ export type ApplyActionFnArgs<T, K = undefined> = {
30
+ params: T;
31
+ targetPath: string;
32
+ options: UpdateActionOptions;
33
+ report: DependencyUpdateReport;
34
+ reports: DependencyUpdateReport[];
35
+ cwd: string;
36
+ preparedData: K; // Added preparedData to the arguments
37
+ };
38
+
39
+ type ApplyActionFn<T, K = undefined> = (args: ApplyActionFnArgs<T, K>) => void;
40
+
41
+ export type PrepareActionFnArgs<T> = {
42
+ params: T;
43
+ targetPath: string;
44
+ options: UpdateActionOptions;
45
+ };
46
+
47
+ type PrepareActionFn<T, K> = (args: PrepareActionFnArgs<T>) => K;
48
+
49
+ type UpdateBuilderParams<T, K = undefined> = {
50
+ kind: string;
51
+ apply: ApplyActionFn<T, K>; // Passed K here
52
+ prepare?: PrepareActionFn<T, K>;
53
+ };
54
+
55
+ export const updateBuilder =
56
+ <T, K = undefined>({ kind, apply, prepare }: UpdateBuilderParams<T, K>) =>
57
+ (targetPath: string, params: T & { required?: boolean }) => ({
58
+ kind,
59
+ path: targetPath,
60
+ params,
61
+ required: params.required,
62
+ prepare(options: UpdateActionOptions): UpdateActionResolved {
63
+ const preparedData = prepare?.({ params, targetPath, options }) as K;
64
+
65
+ return {
66
+ kind,
67
+ targetPath,
68
+ params,
69
+ preparedData,
70
+ apply(report: DependencyUpdateReport, reports: DependencyUpdateReport[], cwd: string) {
71
+ apply({
72
+ preparedData,
73
+ targetPath,
74
+ params,
75
+ options,
76
+ report,
77
+ reports,
78
+ cwd,
79
+ });
80
+ },
81
+ };
82
+ },
83
+ });
@@ -1,50 +1,46 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import type { ChangelogContext } from '../types';
4
+ import { type ChangelogContext, type Commit } from '../types'; // Assuming these types are defined here
5
5
 
6
- import type { UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
6
+ import { updateBuilder, type PrepareActionFnArgs, type ApplyActionFnArgs } from './builder';
7
7
 
8
8
  export type ChangelogUpdateParams = {
9
- path: string;
10
9
  global?: boolean;
11
10
  template?: (ctx: ChangelogContext) => string;
12
11
  required?: boolean;
13
12
  };
14
13
 
15
- export const changelogUpdate = (params: ChangelogUpdateParams): UpdateAction => ({
14
+ // Define the type for data prepared by the 'prepare' function
15
+ type ChangelogPreparedData = {
16
+ resolvedBlock: string;
17
+ };
18
+
19
+ export const changelogUpdate = updateBuilder<ChangelogUpdateParams, ChangelogPreparedData>({
16
20
  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;
21
+ prepare: ({ params, options }: PrepareActionFnArgs<ChangelogUpdateParams>) => {
22
+ const targetCommits = params.global ? options.globalCommits : options.crateCommits;
22
23
  const context: ChangelogContext = {
23
- version: data.newVersion,
24
+ version: options.newVersion,
24
25
  date: new Date().toISOString().split('T')[0] ?? '',
25
- commits: targetCommits,
26
+ commits: targetCommits || [],
26
27
  };
27
28
 
28
29
  const resolvedBlock = params.template
29
30
  ? params.template(context)
30
31
  : defaultChangelogTemplate(context);
31
32
 
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;
33
+ return { resolvedBlock };
34
+ },
35
+ apply: ({
36
+ targetPath,
37
+ preparedData,
38
+ cwd,
39
+ }: ApplyActionFnArgs<ChangelogUpdateParams, ChangelogPreparedData>) => {
40
+ const filePath = path.resolve(cwd, targetPath);
41
+ const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
42
+ const newContent = (preparedData.resolvedBlock ?? '') + '\n' + oldContent;
43
+ fs.writeFileSync(filePath, newContent.trim() + '\n');
48
44
  },
49
45
  });
50
46
 
@@ -52,7 +48,7 @@ export function defaultChangelogTemplate({ version, date, commits }: ChangelogCo
52
48
  let block = `## [${version}] - ${date}\n\n`;
53
49
  if (commits.length === 0) return block + `*No notable changes.*\n`;
54
50
 
55
- const groups: { breaking: string[]; feat: string[]; fix: string[]; other: string[] } = {
51
+ const groups: { breaking: Commit[]; feat: Commit[]; fix: Commit[]; other: Commit[] } = {
56
52
  breaking: [],
57
53
  feat: [],
58
54
  fix: [],
@@ -60,17 +56,19 @@ export function defaultChangelogTemplate({ version, date, commits }: ChangelogCo
60
56
  };
61
57
 
62
58
  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);
59
+ if (c.isBreaking) groups.breaking.push(c);
60
+ else if (c.type === 'feat') groups.feat.push(c);
61
+ else if (c.type === 'fix') groups.fix.push(c);
62
+ else groups.other.push(c);
68
63
  }
69
64
 
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`;
65
+ const formatGroup = (arr: Commit[]) => arr.map((c) => `- ${c.shortHash} ${c.message}`).join('\n');
66
+
67
+ if (groups.breaking.length)
68
+ block += `### ⚠️ BREAKING CHANGES\n${formatGroup(groups.breaking)}\n\n`;
69
+ if (groups.feat.length) block += `### ✨ Features\n${formatGroup(groups.feat)}\n\n`;
70
+ if (groups.fix.length) block += `### 🐛 Bug Fixes\n${formatGroup(groups.fix)}\n\n`;
71
+ if (groups.other.length) block += `### 🛠 Other Changes\n${formatGroup(groups.other)}\n\n`;
74
72
 
75
73
  return block.trim() + '\n';
76
74
  }
@@ -1,31 +1,6 @@
1
- import type { Commit, DependencyUpdateReport } from '../types';
2
-
1
+ export * from './builder';
3
2
  export * from './changelog';
4
3
  export * from './json';
5
4
  export * from './regex';
6
5
  export * from './toml';
7
6
 
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
- }
@@ -1,95 +1,61 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import type { VersionFallback, UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
4
+ import type { DependencyUpdateReport } from '../types';
5
5
 
6
- export type JsonFallbackParams = { path: string; json: string };
7
- export type JsonUpdateParams = { path: string; json: string; required?: boolean };
6
+ import { type VersionFallback, updateBuilder } from '.';
7
+
8
+ export type JsonFallbackParams = {
9
+ path: string;
10
+ read: (parsed: any) => string | null | undefined;
11
+ };
12
+
13
+ export type JsonUpdateParams = (
14
+ parsed: any,
15
+ report: DependencyUpdateReport,
16
+ reports: DependencyUpdateReport[],
17
+ ) => void;
8
18
 
9
19
  export const jsonFallback = (params: JsonFallbackParams): VersionFallback => ({
10
20
  readFallback(cwd: string): string | null {
11
21
  const filePath = path.resolve(cwd, params.path);
12
22
  if (!fs.existsSync(filePath)) return null;
13
23
  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;
24
+ const content = fs.readFileSync(filePath, 'utf8');
25
+ const parsed = JSON.parse(content);
26
+ return params.read(parsed) ?? null;
22
27
  } catch {
23
28
  return null;
24
29
  }
25
30
  },
26
31
  });
27
32
 
28
- export const jsonUpdate = (params: JsonUpdateParams): UpdateAction => ({
33
+ export const jsonUpdate = updateBuilder<JsonUpdateParams>({
29
34
  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;
35
+ apply({ targetPath, params, report, reports, cwd }) {
36
+ const filePath = path.resolve(cwd, targetPath);
37
+ if (!fs.existsSync(filePath)) return;
47
38
 
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
- }
39
+ const content = fs.readFileSync(filePath, 'utf8');
40
+ let parsed: any;
41
+ try {
42
+ parsed = JSON.parse(content);
43
+ } catch (err) {
44
+ console.error(`Failed to parse JSON file at ${filePath}:`, err);
45
+ return;
46
+ }
69
47
 
70
- // Sync internal workspace dependency entries
71
- const depSections = [
72
- 'dependencies',
73
- 'devDependencies',
74
- 'peerDependencies',
75
- 'optionalDependencies',
76
- ];
48
+ // Detect indent size
49
+ const indentMatch = content.match(/^[ \t]+/m);
50
+ const indent = indentMatch ? indentMatch[0] : 2;
77
51
 
78
- for (const depReport of reports) {
79
- if (depReport.name === report.name) continue;
52
+ // Mutate the object using the provided callback
53
+ params(parsed, report, reports);
80
54
 
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
- }
55
+ // Keep trailing newline if it originally existed
56
+ const hasTrailingNewline = content.endsWith('\n');
57
+ const updatedContent = JSON.stringify(parsed, null, indent) + (hasTrailingNewline ? '\n' : '');
90
58
 
91
- fs.writeFileSync(filePath, JSON.stringify(data, null, indent) + '\n');
92
- },
93
- };
59
+ fs.writeFileSync(filePath, updatedContent);
94
60
  },
95
61
  });
@@ -1,11 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import type { VersionFallback, UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
4
+ import { updateBuilder, type PrepareActionFnArgs, type ApplyActionFnArgs } from './builder';
5
+
6
+ import { type VersionFallback } from '.';
5
7
 
6
8
  export type RegexFallbackParams = { path: string; search: string };
7
9
  export type RegexUpdateParams = {
8
- path: string;
9
10
  search: string;
10
11
  replace: string;
11
12
  required?: boolean;
@@ -22,27 +23,35 @@ export const regexFallback = (params: RegexFallbackParams): VersionFallback => (
22
23
  },
23
24
  });
24
25
 
25
- export const regexUpdate = (params: RegexUpdateParams): UpdateAction => ({
26
+ // Define the type for data prepared by the 'prepare' function
27
+ type RegexPreparedData = {
28
+ resolvedReplace: string;
29
+ };
30
+
31
+ export const regexUpdate = updateBuilder<RegexUpdateParams, RegexPreparedData>({
26
32
  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
- };
33
+
34
+ prepare: ({ params, options }: PrepareActionFnArgs<RegexUpdateParams>) => {
35
+ // Pre-calculate the replacement string with the new version
36
+ const resolvedReplace = params.replace.replace('{{version}}', options.newVersion);
37
+ return { resolvedReplace };
38
+ },
39
+
40
+ apply: ({
41
+ targetPath,
42
+ params,
43
+ preparedData,
44
+ cwd,
45
+ }: ApplyActionFnArgs<RegexUpdateParams, RegexPreparedData>) => {
46
+ const filePath = path.resolve(cwd, targetPath);
47
+ if (!fs.existsSync(filePath)) {
48
+ console.warn(`File not found for regex update (path: ${filePath}). Skipping.`);
49
+ return;
50
+ }
51
+
52
+ let content = fs.readFileSync(filePath, 'utf8');
53
+ const searchRegex = new RegExp(params.search, 'g');
54
+ content = content.replace(searchRegex, preparedData.resolvedReplace);
55
+ fs.writeFileSync(filePath, content);
47
56
  },
48
57
  });
@@ -1,57 +1,49 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import type { VersionFallback, UpdateAction, UpdateActionResolved, UpdateActionOptions } from '.';
4
+ import { parse, stringify } from 'smol-toml';
5
5
 
6
- export type TomlFallbackParams = { path: string; toml: string };
7
- export type TomlUpdateParams = { path: string; toml: string; required?: boolean };
6
+ import type { DependencyUpdateReport } from '../types';
7
+
8
+ import { type VersionFallback, updateBuilder } from '.';
9
+
10
+ export type TomlFallbackParams = {
11
+ path: string;
12
+ read: (parsed: any) => string | null | undefined;
13
+ };
14
+
15
+ export type TomlUpdateParams = (
16
+ parsed: any,
17
+ report: DependencyUpdateReport,
18
+ reports: DependencyUpdateReport[],
19
+ ) => void;
8
20
 
9
21
  export const tomlFallback = (params: TomlFallbackParams): VersionFallback => ({
10
22
  readFallback(cwd: string): string | null {
11
23
  const filePath = path.resolve(cwd, params.path);
12
24
  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;
25
+ try {
26
+ const content = fs.readFileSync(filePath, 'utf8');
27
+ const parsed = parse(content);
28
+ return params.read(parsed) ?? null;
29
+ } catch {
30
+ return null;
31
+ }
18
32
  },
19
33
  });
20
34
 
21
- export const tomlUpdate = (params: TomlUpdateParams): UpdateAction => ({
35
+ export const tomlUpdate = updateBuilder<TomlUpdateParams>({
22
36
  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
- };
37
+ apply({ targetPath, params, report, reports, cwd }) {
38
+ const filePath = path.resolve(cwd, targetPath);
39
+ if (!fs.existsSync(filePath)) return;
40
+
41
+ const content = fs.readFileSync(filePath, 'utf8');
42
+ const parsed = parse(content);
43
+
44
+ // Safely mutate the object using the user provided callback
45
+ params(parsed, report, reports);
46
+
47
+ fs.writeFileSync(filePath, stringify(parsed));
56
48
  },
57
49
  });
package/src/vcs/jj.ts CHANGED
@@ -7,7 +7,7 @@ export function runJj(cmd: string, cwd: string): string {
7
7
  return execSync(`jj --color=never ${cmd}`, {
8
8
  cwd,
9
9
  encoding: 'utf8',
10
- stdio: ['ignore', 'pipe', 'inherit'],
10
+ stdio: ['ignore', 'pipe', 'ignore'],
11
11
  }).trim();
12
12
  } catch {
13
13
  return '';