@quonfig/cli 0.0.33 → 0.0.35

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.
@@ -222,6 +222,41 @@ export const canFastForward = async (dir) => {
222
222
  return false;
223
223
  }
224
224
  };
225
+ /**
226
+ * Returns true iff origin/main is NOT an ancestor of the local HEAD —
227
+ * covering both "local strictly behind" and "diverged" in one boolean.
228
+ *
229
+ * Used by the clone-path stale-HEAD guard in `qfg push` (qfg-fboj):
230
+ * either of those two states would otherwise produce a `HEAD..origin/main`
231
+ * diff that ships REVERSAL deltas to the server, silently undoing
232
+ * remote-newer commits. Both must refuse.
233
+ *
234
+ * Returns false on any git failure (no `origin/main` ref yet, no `.git/`,
235
+ * etc.) so the caller falls through to its other guards rather than
236
+ * aborting on an opaque error.
237
+ */
238
+ export const isLocalBehindOrDivergedFromRemote = async (dir) => {
239
+ try {
240
+ const { stdout: localSha } = await runGit(['-C', dir, 'rev-parse', 'HEAD']);
241
+ const { stdout: remoteSha } = await runGit(['-C', dir, 'rev-parse', 'origin/main']);
242
+ const local = localSha.trim();
243
+ const remote = remoteSha.trim();
244
+ if (local === remote)
245
+ return false;
246
+ try {
247
+ // exit 0 → remote is an ancestor of local → local is strictly ahead, fine.
248
+ await runGit(['-C', dir, 'merge-base', '--is-ancestor', remote, local]);
249
+ return false;
250
+ }
251
+ catch {
252
+ // non-zero exit → remote is NOT an ancestor → behind or diverged.
253
+ return true;
254
+ }
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ };
225
260
  /**
226
261
  * Returns true if local has commits not reachable from origin/main (diverged).
227
262
  */
@@ -310,3 +345,50 @@ export const hasAtLeastOneCommit = async (dir) => {
310
345
  * Get the URL stripped of credentials for display purposes.
311
346
  */
312
347
  export const displayUrl = (url) => redactToken(url);
348
+ /**
349
+ * `git pull --rebase origin main`. Replays local commits on top of the
350
+ * remote tip. On conflicts, leaves the repo in rebase-in-progress state
351
+ * with `<<<<<<<` / `=======` / `>>>>>>>` markers planted by git so the
352
+ * user can resolve via standard git tools (`git rebase --continue` /
353
+ * `git rebase --abort`).
354
+ *
355
+ * Caller is responsible for surfacing recovery instructions; this function
356
+ * only reports the outcome.
357
+ */
358
+ export const gitPullRebase = async (dir) => {
359
+ // Count local-only commits BEFORE the rebase so the success path can
360
+ // tell the user how many got moved. After a clean rebase, origin/main
361
+ // is the merge-base and ahead-count == commits-rebased.
362
+ let commitsRebased = 0;
363
+ try {
364
+ const { stdout } = await runGit(['-C', dir, 'rev-list', '--count', 'origin/main..HEAD']);
365
+ commitsRebased = Number.parseInt(stdout.trim(), 10) || 0;
366
+ }
367
+ catch {
368
+ // Non-fatal — counting is for UX, not correctness.
369
+ }
370
+ try {
371
+ await runGit(['-C', dir, 'pull', '--rebase', 'origin', 'main']);
372
+ return { kind: 'clean', commitsRebased };
373
+ }
374
+ catch (err) {
375
+ // Distinguish "rebase paused on conflicts" from "rebase never started".
376
+ // Conflicts: `diff --diff-filter=U` lists the unmerged paths and
377
+ // `.git/rebase-merge/` is present.
378
+ try {
379
+ const { stdout } = await runGit(['-C', dir, 'diff', '--name-only', '--diff-filter=U']);
380
+ const conflictedFiles = stdout
381
+ .split('\n')
382
+ .map((s) => s.trim())
383
+ .filter(Boolean);
384
+ if (conflictedFiles.length > 0) {
385
+ return { kind: 'conflicts', conflictedFiles };
386
+ }
387
+ }
388
+ catch {
389
+ // Fall through to 'failed' — diff itself errored.
390
+ }
391
+ const reason = err instanceof Error ? err.message : String(err);
392
+ return { kind: 'failed', reason };
393
+ }
394
+ };
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "0.0.33";
1
+ declare const _default: "0.0.35";
2
2
  export default _default;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
2
- export default '0.0.33';
2
+ export default '0.0.35';
@@ -1415,6 +1415,12 @@
1415
1415
  "multiple": false,
1416
1416
  "type": "option"
1417
1417
  },
1418
+ "rebase": {
1419
+ "description": "Replay local commits on top of origin/main when the two have diverged. Conflicts are surfaced via standard git markers; resolve with `git rebase --continue`.",
1420
+ "name": "rebase",
1421
+ "allowNo": false,
1422
+ "type": "boolean"
1423
+ },
1418
1424
  "workspace": {
1419
1425
  "description": "Workspace ID (defaults to active profile)",
1420
1426
  "name": "workspace",
@@ -2910,5 +2916,5 @@
2910
2916
  ]
2911
2917
  }
2912
2918
  },
2913
- "version": "0.0.33"
2919
+ "version": "0.0.35"
2914
2920
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
3
3
  "name": "@quonfig/cli",
4
- "version": "0.0.33",
4
+ "version": "0.0.35",
5
5
  "bugs": {
6
6
  "url": "https://github.com/quonfig/cli/issues"
7
7
  },