claude-nomad 0.18.0 → 0.20.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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/commands.update.ts +111 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.20.0](https://github.com/funkadelic/claude-nomad/compare/v0.19.0...v0.20.0) (2026-05-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **update:** prompted auto-resolve for release-please artifact conflicts ([#98](https://github.com/funkadelic/claude-nomad/issues/98)) ([958cbf1](https://github.com/funkadelic/claude-nomad/commit/958cbf19839f1bda8ad0e25db0bb8495a88c2ee9))
|
|
9
|
+
|
|
10
|
+
## [0.19.0](https://github.com/funkadelic/claude-nomad/compare/v0.18.0...v0.19.0) (2026-05-22)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* **update:** auto-resolve sole package-lock.json merge conflict ([#96](https://github.com/funkadelic/claude-nomad/issues/96)) ([8d76be9](https://github.com/funkadelic/claude-nomad/commit/8d76be9791b427484d709d97a5d077539c0f9f1a))
|
|
16
|
+
|
|
3
17
|
## [0.18.0](https://github.com/funkadelic/claude-nomad/compare/v0.17.2...v0.18.0) (2026-05-22)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
package/src/commands.update.ts
CHANGED
|
@@ -160,13 +160,109 @@ function reinstallIfNeeded(beforeSha: string): void {
|
|
|
160
160
|
* today.
|
|
161
161
|
*
|
|
162
162
|
* @param opts - Update options; only `dryRun` is observed for this topology.
|
|
163
|
+
* @returns `true` when this path already ran `npm install` and committed the merged lockfile (so the caller should skip `reinstallIfNeeded`). Vanilla `--ff-only` pulls never conflict, so this is always `false`.
|
|
163
164
|
*/
|
|
164
|
-
function runVanilla(opts: CmdUpdateOpts):
|
|
165
|
+
function runVanilla(opts: CmdUpdateOpts): boolean {
|
|
165
166
|
if (opts.dryRun === true) {
|
|
166
167
|
log('DRY-RUN: would run `git pull --ff-only origin main`');
|
|
167
|
-
return;
|
|
168
|
+
return false;
|
|
168
169
|
}
|
|
169
170
|
gitOrFatal(['pull', '--ff-only', 'origin', 'main'], 'git pull', REPO_HOME);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Files release-please touches as a set on every release commit. Multi-file
|
|
176
|
+
* merge conflicts in `nomad update` that consist entirely of paths from this
|
|
177
|
+
* set are diagnostic for a release landing upstream while the mirror has its
|
|
178
|
+
* own local commits on these artifacts. Taking upstream is the canonical
|
|
179
|
+
* resolution (these are all generated artifacts the user has no business
|
|
180
|
+
* editing on a mirror), but multi-file is more aggressive than the lone
|
|
181
|
+
* lockfile case so we prompt before mutating.
|
|
182
|
+
*/
|
|
183
|
+
const RELEASE_PLEASE_ARTIFACTS: ReadonlySet<string> = new Set([
|
|
184
|
+
'package.json',
|
|
185
|
+
'package-lock.json',
|
|
186
|
+
'CHANGELOG.md',
|
|
187
|
+
'.release-please-manifest.json',
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve a merge conflict by taking upstream's version of every listed path,
|
|
192
|
+
* regenerating the lockfile via `npm install`, and committing the merge.
|
|
193
|
+
* Shared body for the lone-lockfile auto-resolve and the release-please
|
|
194
|
+
* multi-file prompted auto-resolve.
|
|
195
|
+
*
|
|
196
|
+
* @param paths - Unmerged paths to resolve via `git checkout --theirs`.
|
|
197
|
+
*/
|
|
198
|
+
function resolveByTakingTheirs(paths: readonly string[]): void {
|
|
199
|
+
for (const p of paths) {
|
|
200
|
+
gitOrFatal(['checkout', '--theirs', '--', p], `git checkout --theirs ${p}`, REPO_HOME);
|
|
201
|
+
}
|
|
202
|
+
gitOrFatal(['add', ...paths], `git add ${paths.join(' ')}`, REPO_HOME);
|
|
203
|
+
execFileSync('npm', ['install'], { cwd: REPO_HOME, stdio: 'inherit' });
|
|
204
|
+
gitOrFatal(['add', 'package-lock.json'], 'git add package-lock.json', REPO_HOME);
|
|
205
|
+
gitOrFatal(['commit', '--no-edit'], 'git commit --no-edit', REPO_HOME);
|
|
206
|
+
log(`auto-resolved merge conflict (took upstream for ${paths.join(', ')}, reinstalled)`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Auto-resolve a merge conflict in the two scenarios both caused by
|
|
211
|
+
* release-please landing upstream while the mirror has local commits:
|
|
212
|
+
*
|
|
213
|
+
* 1. **Sole `package-lock.json`** (silent): the lone-lockfile case from PR
|
|
214
|
+
* #96. Any host that has run `npm install` against the mirror will hit
|
|
215
|
+
* this on the next `nomad update`; take upstream + reinstall is the
|
|
216
|
+
* semantically-correct fix and surprise-free for a generated artifact.
|
|
217
|
+
*
|
|
218
|
+
* 2. **All paths in `RELEASE_PLEASE_ARTIFACTS` and more than one path**
|
|
219
|
+
* (prompted): a release commit conflicting on `package.json`,
|
|
220
|
+
* `CHANGELOG.md`, `.release-please-manifest.json` together with the
|
|
221
|
+
* lockfile. Same semantic resolution, but more files are touched so we
|
|
222
|
+
* require explicit y/N consent before mutating.
|
|
223
|
+
*
|
|
224
|
+
* Returns `false` for any other conflict shape (including probe failure);
|
|
225
|
+
* the caller re-throws the original merge `NomadFatal` unchanged.
|
|
226
|
+
*
|
|
227
|
+
* @param opts - Update options; only `prompt` is consulted (used for the multi-file release-please consent prompt).
|
|
228
|
+
* @returns `true` when the conflict was auto-resolved and the merge committed; `false` when the conflict shape does not match either auto-resolve case (caller should re-throw the original failure).
|
|
229
|
+
*/
|
|
230
|
+
function tryAutoResolveMergeConflict(opts: CmdUpdateOpts): boolean {
|
|
231
|
+
let unmerged: string[];
|
|
232
|
+
try {
|
|
233
|
+
unmerged = execFileSync('git', ['diff', '--name-only', '--diff-filter=U'], {
|
|
234
|
+
cwd: REPO_HOME,
|
|
235
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
236
|
+
})
|
|
237
|
+
.toString()
|
|
238
|
+
.split('\n')
|
|
239
|
+
.filter((line) => line !== '');
|
|
240
|
+
} catch {
|
|
241
|
+
// Probe failure must not mask the original merge NomadFatal. Returning
|
|
242
|
+
// false lets the caller re-throw the merge error unchanged.
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (unmerged.length === 1 && unmerged[0] === 'package-lock.json') {
|
|
247
|
+
resolveByTakingTheirs(['package-lock.json']);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (unmerged.length > 1 && unmerged.every((p) => RELEASE_PLEASE_ARTIFACTS.has(p))) {
|
|
252
|
+
const promptFn = opts.prompt ?? defaultPrompt;
|
|
253
|
+
log(`merge conflict in release-please artifacts: ${unmerged.join(', ')}`);
|
|
254
|
+
const answer = promptFn(
|
|
255
|
+
'Auto-resolve by taking upstream + `npm install` + commit? [y/N] ',
|
|
256
|
+
).toLowerCase();
|
|
257
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
258
|
+
log('skipping auto-resolve (resolve manually then re-run `nomad update`)');
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
resolveByTakingTheirs(unmerged);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return false;
|
|
170
266
|
}
|
|
171
267
|
|
|
172
268
|
/**
|
|
@@ -186,7 +282,7 @@ function runVanilla(opts: CmdUpdateOpts): void {
|
|
|
186
282
|
* - `pushOrigin`: when true, push to `origin/main` without prompting
|
|
187
283
|
* - `prompt`: optional prompt function used for interactive confirmation
|
|
188
284
|
*/
|
|
189
|
-
function runFork(opts: CmdUpdateOpts):
|
|
285
|
+
function runFork(opts: CmdUpdateOpts): boolean {
|
|
190
286
|
const promptFn = opts.prompt ?? defaultPrompt;
|
|
191
287
|
if (opts.dryRun === true) {
|
|
192
288
|
log('DRY-RUN: would run `git fetch upstream`');
|
|
@@ -196,13 +292,19 @@ function runFork(opts: CmdUpdateOpts): void {
|
|
|
196
292
|
} else {
|
|
197
293
|
log('DRY-RUN: would prompt before pushing to origin/main');
|
|
198
294
|
}
|
|
199
|
-
return;
|
|
295
|
+
return false;
|
|
200
296
|
}
|
|
201
297
|
gitOrFatal(['fetch', 'upstream'], 'git fetch upstream', REPO_HOME);
|
|
202
|
-
|
|
298
|
+
let autoResolved = false;
|
|
299
|
+
try {
|
|
300
|
+
gitOrFatal(['merge', 'upstream/main'], 'git merge upstream/main', REPO_HOME);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (!tryAutoResolveMergeConflict(opts)) throw err;
|
|
303
|
+
autoResolved = true;
|
|
304
|
+
}
|
|
203
305
|
if (opts.pushOrigin === true) {
|
|
204
306
|
gitOrFatal(['push', 'origin', 'main'], 'git push origin main', REPO_HOME);
|
|
205
|
-
return;
|
|
307
|
+
return autoResolved;
|
|
206
308
|
}
|
|
207
309
|
const answer = promptFn(
|
|
208
310
|
'Push merge to origin/main? (y publishes to your private mirror so other hosts see it; N keeps it local) [y/N] ',
|
|
@@ -212,6 +314,7 @@ function runFork(opts: CmdUpdateOpts): void {
|
|
|
212
314
|
} else {
|
|
213
315
|
log('skipping push to origin (run `git push origin main` later)');
|
|
214
316
|
}
|
|
317
|
+
return autoResolved;
|
|
215
318
|
}
|
|
216
319
|
|
|
217
320
|
/**
|
|
@@ -273,9 +376,8 @@ export function cmdUpdate(opts: CmdUpdateOpts = {}): void {
|
|
|
273
376
|
}
|
|
274
377
|
|
|
275
378
|
const beforeSha = headSha();
|
|
276
|
-
|
|
277
|
-
else runFork(opts);
|
|
379
|
+
const installAlreadyRan = topology === 'vanilla' ? runVanilla(opts) : runFork(opts);
|
|
278
380
|
|
|
279
|
-
reinstallIfNeeded(beforeSha);
|
|
381
|
+
if (!installAlreadyRan) reinstallIfNeeded(beforeSha);
|
|
280
382
|
cmdDoctor();
|
|
281
383
|
}
|