@drewpayment/mink 0.11.0-beta.2 → 0.11.0-beta.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.
- package/dashboard/out/404.html +1 -1
- package/dashboard/out/action-log.html +1 -1
- package/dashboard/out/action-log.txt +1 -1
- package/dashboard/out/activity.html +1 -1
- package/dashboard/out/activity.txt +1 -1
- package/dashboard/out/bugs.html +1 -1
- package/dashboard/out/bugs.txt +1 -1
- package/dashboard/out/capture.html +1 -1
- package/dashboard/out/capture.txt +1 -1
- package/dashboard/out/config.html +1 -1
- package/dashboard/out/config.txt +1 -1
- package/dashboard/out/daemon.html +1 -1
- package/dashboard/out/daemon.txt +1 -1
- package/dashboard/out/design.html +1 -1
- package/dashboard/out/design.txt +1 -1
- package/dashboard/out/discord.html +1 -1
- package/dashboard/out/discord.txt +1 -1
- package/dashboard/out/file-index.html +1 -1
- package/dashboard/out/file-index.txt +1 -1
- package/dashboard/out/index.html +1 -1
- package/dashboard/out/index.txt +1 -1
- package/dashboard/out/insights.html +1 -1
- package/dashboard/out/insights.txt +1 -1
- package/dashboard/out/learning.html +1 -1
- package/dashboard/out/learning.txt +1 -1
- package/dashboard/out/overview.html +1 -1
- package/dashboard/out/overview.txt +1 -1
- package/dashboard/out/scheduler.html +1 -1
- package/dashboard/out/scheduler.txt +1 -1
- package/dashboard/out/sync.html +1 -1
- package/dashboard/out/sync.txt +1 -1
- package/dashboard/out/tokens.html +1 -1
- package/dashboard/out/tokens.txt +1 -1
- package/dashboard/out/waste.html +1 -1
- package/dashboard/out/waste.txt +1 -1
- package/dashboard/out/wiki.html +1 -1
- package/dashboard/out/wiki.txt +1 -1
- package/dist/cli.js +100 -20
- package/package.json +1 -1
- package/src/commands/sync-migrate.ts +158 -25
- package/src/core/sync.ts +7 -0
- /package/dashboard/out/_next/static/{WyN-sdaVY7cZaACRaK7vq → Mmf6YVSNZzpPOZiW-DG5M}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{WyN-sdaVY7cZaACRaK7vq → Mmf6YVSNZzpPOZiW-DG5M}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
writeFileSync,
|
|
7
7
|
readFileSync,
|
|
8
8
|
renameSync,
|
|
9
|
+
rmSync,
|
|
9
10
|
unlinkSync,
|
|
10
11
|
} from "fs";
|
|
11
12
|
import { join } from "path";
|
|
@@ -236,7 +237,24 @@ function listProjectsNeedingMigration(): string[] {
|
|
|
236
237
|
// Idempotent: re-running after a clean pass walks every project, finds every
|
|
237
238
|
// id matches its directory name, and does nothing.
|
|
238
239
|
|
|
239
|
-
|
|
240
|
+
// Plan actions:
|
|
241
|
+
// rename old dir present, new dir absent → rename old → new, record alias.
|
|
242
|
+
// skip-converged old dir + new dir both present, alias NOT yet recorded → record
|
|
243
|
+
// alias on new meta and evict old dir to .identity-rollback/. Named
|
|
244
|
+
// "skip-converged" because the rename itself is unnecessary; the
|
|
245
|
+
// convergence work (alias + eviction) is the action.
|
|
246
|
+
// skip-evict old dir + new dir both present, alias already recorded → only
|
|
247
|
+
// evict the old dir. Reached when a previous migration recorded
|
|
248
|
+
// the alias but didn't (or couldn't) finish evicting. Without
|
|
249
|
+
// this, dry-run would keep proposing skip-converged forever.
|
|
250
|
+
// skip-no-cwd project's working-copy path is on a different device — leave alone.
|
|
251
|
+
// skip-unchanged newId === oldId — no work needed at all.
|
|
252
|
+
export type IdentityPlanAction =
|
|
253
|
+
| "rename"
|
|
254
|
+
| "skip-converged"
|
|
255
|
+
| "skip-evict"
|
|
256
|
+
| "skip-no-cwd"
|
|
257
|
+
| "skip-unchanged";
|
|
240
258
|
|
|
241
259
|
export interface IdentityPlanEntry {
|
|
242
260
|
oldId: string;
|
|
@@ -309,12 +327,23 @@ export function planIdentityMigration(flagOverride?: string): IdentityPlanEntry[
|
|
|
309
327
|
|
|
310
328
|
const newProjDir = join(projectsRoot, newId);
|
|
311
329
|
if (existsSync(newProjDir)) {
|
|
330
|
+
// If the canonical (new) project already records this oldId in its
|
|
331
|
+
// aliases list, the convergence bookkeeping is already done — only the
|
|
332
|
+
// leftover old directory remains to be cleaned up. Surfacing this as a
|
|
333
|
+
// distinct action (skip-evict) makes dry-run idempotent: once the alias
|
|
334
|
+
// is recorded AND the old directory is gone, the planner stops seeing
|
|
335
|
+
// the project entirely. Pre-fix it would keep proposing the same
|
|
336
|
+
// skip-converged action forever.
|
|
337
|
+
const newMeta = getProjectMeta(newProjDir);
|
|
338
|
+
const aliasAlreadyRecorded = newMeta?.aliases?.includes(oldId) ?? false;
|
|
312
339
|
plan.push({
|
|
313
340
|
oldId,
|
|
314
341
|
newId,
|
|
315
342
|
cwd: meta.cwd,
|
|
316
|
-
action: "skip-converged",
|
|
317
|
-
reason:
|
|
343
|
+
action: aliasAlreadyRecorded ? "skip-evict" : "skip-converged",
|
|
344
|
+
reason: aliasAlreadyRecorded
|
|
345
|
+
? "alias already recorded; will evict leftover old directory"
|
|
346
|
+
: "destination already exists (from sync); will record alias and evict old directory",
|
|
318
347
|
});
|
|
319
348
|
continue;
|
|
320
349
|
}
|
|
@@ -373,29 +402,55 @@ function copyDirRecursive(src: string, dest: string, excludeNames: Set<string>):
|
|
|
373
402
|
// that drives this very decision). Falls back to a fresh read for callers that
|
|
374
403
|
// don't operate inside a stash window (e.g. session-start triggers and the
|
|
375
404
|
// --dry-run path).
|
|
376
|
-
|
|
377
|
-
deviceId: string,
|
|
378
|
-
flag: string = resolveConfigValue("projects.identity").value
|
|
379
|
-
): {
|
|
405
|
+
export interface IdentityMigrationOutcome {
|
|
380
406
|
renamed: number;
|
|
407
|
+
converged: number;
|
|
408
|
+
evicted: number;
|
|
381
409
|
visited: number;
|
|
382
410
|
backupDir: string | null;
|
|
383
|
-
|
|
411
|
+
renames: Array<{ from: string; to: string }>;
|
|
412
|
+
evictions: string[];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function migrateProjectIdentities(
|
|
416
|
+
deviceId: string,
|
|
417
|
+
flag: string = resolveConfigValue("projects.identity").value
|
|
418
|
+
): IdentityMigrationOutcome {
|
|
384
419
|
if (flag !== "git-remote") {
|
|
385
|
-
return {
|
|
420
|
+
return {
|
|
421
|
+
renamed: 0,
|
|
422
|
+
converged: 0,
|
|
423
|
+
evicted: 0,
|
|
424
|
+
visited: 0,
|
|
425
|
+
backupDir: null,
|
|
426
|
+
renames: [],
|
|
427
|
+
evictions: [],
|
|
428
|
+
};
|
|
386
429
|
}
|
|
387
430
|
|
|
388
431
|
const plan = planIdentityMigration(flag);
|
|
389
|
-
|
|
432
|
+
// Any action that moves an old directory aside needs a backup destination.
|
|
433
|
+
// rename, skip-converged (record alias + evict), and skip-evict (alias
|
|
434
|
+
// already recorded; only evict) all qualify.
|
|
435
|
+
const willTouchOldDir = plan.filter(
|
|
436
|
+
(p) =>
|
|
437
|
+
p.action === "rename" ||
|
|
438
|
+
p.action === "skip-converged" ||
|
|
439
|
+
p.action === "skip-evict"
|
|
440
|
+
);
|
|
390
441
|
|
|
391
442
|
// Compute the backup root up-front so all snapshots for this migration pass
|
|
392
443
|
// land in one timestamped directory the user can find and reason about.
|
|
393
444
|
let backupRoot: string | null = null;
|
|
394
|
-
if (
|
|
445
|
+
if (willTouchOldDir.length > 0) {
|
|
395
446
|
backupRoot = identityBackupRoot(ensureIdentityBackupTimestamp());
|
|
396
447
|
}
|
|
397
448
|
|
|
398
449
|
let renamed = 0;
|
|
450
|
+
let converged = 0;
|
|
451
|
+
let evicted = 0;
|
|
452
|
+
const renames: Array<{ from: string; to: string }> = [];
|
|
453
|
+
const evictions: string[] = [];
|
|
399
454
|
let visited = plan.length;
|
|
400
455
|
const projectsRoot = join(minkRoot(), "projects");
|
|
401
456
|
|
|
@@ -412,16 +467,53 @@ function migrateProjectIdentities(
|
|
|
412
467
|
}
|
|
413
468
|
}
|
|
414
469
|
|
|
415
|
-
if (
|
|
470
|
+
if (
|
|
471
|
+
(entry.action === "skip-converged" || entry.action === "skip-evict") &&
|
|
472
|
+
entry.newId
|
|
473
|
+
) {
|
|
416
474
|
const newProjDir = join(projectsRoot, entry.newId);
|
|
475
|
+
// Record the alias and lift the device path before evicting — if the
|
|
476
|
+
// canonical meta is broken (read returns null in addProjectAlias), the
|
|
477
|
+
// safer path is to leave the old directory alone rather than discard
|
|
478
|
+
// it without ever surfacing the alias. We detect that case below.
|
|
479
|
+
let aliasOnRecord = false;
|
|
417
480
|
try {
|
|
418
|
-
|
|
481
|
+
if (entry.action === "skip-evict") {
|
|
482
|
+
aliasOnRecord = true;
|
|
483
|
+
} else {
|
|
484
|
+
addProjectAlias(newProjDir, entry.oldId);
|
|
485
|
+
const newMeta = getProjectMeta(newProjDir);
|
|
486
|
+
aliasOnRecord = newMeta?.aliases?.includes(entry.oldId) ?? false;
|
|
487
|
+
}
|
|
419
488
|
if (entry.cwd) {
|
|
420
489
|
setProjectPathForDevice(newProjDir, deviceId, entry.cwd);
|
|
421
490
|
}
|
|
422
491
|
} catch {
|
|
423
492
|
// best-effort
|
|
424
493
|
}
|
|
494
|
+
|
|
495
|
+
if (aliasOnRecord && backupRoot) {
|
|
496
|
+
// Snapshot the old dir before eviction so any local-only state
|
|
497
|
+
// (writes that landed here before sync converged) is recoverable.
|
|
498
|
+
const ok = backupProjectForRollback(
|
|
499
|
+
oldProjDir,
|
|
500
|
+
join(backupRoot, entry.oldId)
|
|
501
|
+
);
|
|
502
|
+
if (ok) {
|
|
503
|
+
let removed = false;
|
|
504
|
+
try {
|
|
505
|
+
rmSync(oldProjDir, { recursive: true, force: true });
|
|
506
|
+
removed = true;
|
|
507
|
+
} catch {
|
|
508
|
+
// best-effort; leave the directory rather than partially deleted
|
|
509
|
+
}
|
|
510
|
+
if (removed) {
|
|
511
|
+
evictions.push(entry.oldId);
|
|
512
|
+
evicted++;
|
|
513
|
+
if (entry.action === "skip-converged") converged++;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
425
517
|
continue;
|
|
426
518
|
}
|
|
427
519
|
|
|
@@ -456,9 +548,18 @@ function migrateProjectIdentities(
|
|
|
456
548
|
// best-effort
|
|
457
549
|
}
|
|
458
550
|
renamed++;
|
|
551
|
+
renames.push({ from: entry.oldId, to: entry.newId });
|
|
459
552
|
}
|
|
460
553
|
|
|
461
|
-
return {
|
|
554
|
+
return {
|
|
555
|
+
renamed,
|
|
556
|
+
converged,
|
|
557
|
+
evicted,
|
|
558
|
+
visited,
|
|
559
|
+
backupDir: backupRoot,
|
|
560
|
+
renames,
|
|
561
|
+
evictions,
|
|
562
|
+
};
|
|
462
563
|
}
|
|
463
564
|
|
|
464
565
|
// ── v3 identity rollback ──────────────────────────────────────────────────
|
|
@@ -545,6 +646,7 @@ export interface MigrateResult {
|
|
|
545
646
|
fromVersion: number;
|
|
546
647
|
toVersion: number;
|
|
547
648
|
message?: string;
|
|
649
|
+
identity?: IdentityMigrationOutcome;
|
|
548
650
|
}
|
|
549
651
|
|
|
550
652
|
// Idempotent. Safe to invoke from `mink sync migrate` directly or from a
|
|
@@ -636,7 +738,15 @@ export function migrateSyncLayout(): MigrateResult {
|
|
|
636
738
|
// flag is off or every project's identifier already matches its directory.
|
|
637
739
|
// Pass the pre-stash snapshot of identityMode so we don't re-read the
|
|
638
740
|
// config from a stash-hidden working tree.
|
|
639
|
-
let identity = {
|
|
741
|
+
let identity: IdentityMigrationOutcome = {
|
|
742
|
+
renamed: 0,
|
|
743
|
+
converged: 0,
|
|
744
|
+
evicted: 0,
|
|
745
|
+
visited: 0,
|
|
746
|
+
backupDir: null,
|
|
747
|
+
renames: [],
|
|
748
|
+
evictions: [],
|
|
749
|
+
};
|
|
640
750
|
try {
|
|
641
751
|
identity = migrateProjectIdentities(deviceId, identityMode);
|
|
642
752
|
} catch {
|
|
@@ -650,16 +760,19 @@ export function migrateSyncLayout(): MigrateResult {
|
|
|
650
760
|
writeSyncVersion(MINK_SYNC_VERSION);
|
|
651
761
|
}
|
|
652
762
|
|
|
653
|
-
if (
|
|
763
|
+
if (
|
|
764
|
+
isSyncInitialized() &&
|
|
765
|
+
(processed > 0 || identity.renamed > 0 || identity.evicted > 0)
|
|
766
|
+
) {
|
|
654
767
|
// Skip the lock file — it's part of migration coordination, not state.
|
|
655
768
|
gitSafe("add -A");
|
|
656
769
|
gitSafe(`reset HEAD ".sync-migrate.lock"`);
|
|
657
|
-
const
|
|
658
|
-
identity.renamed > 0
|
|
659
|
-
?
|
|
660
|
-
:
|
|
770
|
+
const identityNote =
|
|
771
|
+
identity.renamed > 0 || identity.evicted > 0
|
|
772
|
+
? `, ${identity.renamed} renamed + ${identity.evicted} evicted for identity v3`
|
|
773
|
+
: "";
|
|
661
774
|
gitSafe(
|
|
662
|
-
`commit -m "mink: migrate sync layout v${fromVersion} -> v${MINK_SYNC_VERSION} (device ${deviceId.slice(0, 8)}, ${
|
|
775
|
+
`commit -m "mink: migrate sync layout v${fromVersion} -> v${MINK_SYNC_VERSION} (device ${deviceId.slice(0, 8)}, ${processed} projects${identityNote})"`
|
|
663
776
|
);
|
|
664
777
|
}
|
|
665
778
|
|
|
@@ -671,6 +784,7 @@ export function migrateSyncLayout(): MigrateResult {
|
|
|
671
784
|
ranMigration: true,
|
|
672
785
|
fromVersion,
|
|
673
786
|
toVersion: MINK_SYNC_VERSION,
|
|
787
|
+
identity,
|
|
674
788
|
};
|
|
675
789
|
} finally {
|
|
676
790
|
releaseLock();
|
|
@@ -696,20 +810,24 @@ export function syncMigrateCommand(args: string[] = []): void {
|
|
|
696
810
|
}
|
|
697
811
|
const renames = plan.filter((p) => p.action === "rename");
|
|
698
812
|
const converged = plan.filter((p) => p.action === "skip-converged");
|
|
813
|
+
const evictOnly = plan.filter((p) => p.action === "skip-evict");
|
|
699
814
|
const skippedNoCwd = plan.filter((p) => p.action === "skip-no-cwd");
|
|
700
815
|
const unchanged = plan.filter((p) => p.action === "skip-unchanged");
|
|
701
816
|
|
|
702
817
|
console.log(
|
|
703
|
-
`[mink] sync migrate --dry-run: ${renames.length} rename(s), ${converged.length} alias-only, ${skippedNoCwd.length} skipped (no cwd), ${unchanged.length} unchanged`
|
|
818
|
+
`[mink] sync migrate --dry-run: ${renames.length} rename(s), ${converged.length} converge (alias + evict), ${evictOnly.length} evict-only, ${skippedNoCwd.length} skipped (no cwd), ${unchanged.length} unchanged`
|
|
704
819
|
);
|
|
705
820
|
for (const p of renames) {
|
|
706
|
-
console.log(` rename:
|
|
821
|
+
console.log(` rename: ${p.oldId} → ${p.newId}`);
|
|
707
822
|
}
|
|
708
823
|
for (const p of converged) {
|
|
709
|
-
console.log(`
|
|
824
|
+
console.log(` converge: ${p.oldId} → ${p.newId} (record alias on ${p.newId}, evict ${p.oldId} to .identity-rollback/)`);
|
|
825
|
+
}
|
|
826
|
+
for (const p of evictOnly) {
|
|
827
|
+
console.log(` evict: ${p.oldId} → .identity-rollback/ (alias already on ${p.newId})`);
|
|
710
828
|
}
|
|
711
829
|
for (const p of skippedNoCwd) {
|
|
712
|
-
console.log(` skip:
|
|
830
|
+
console.log(` skip: ${p.oldId} — ${p.reason}`);
|
|
713
831
|
}
|
|
714
832
|
return;
|
|
715
833
|
}
|
|
@@ -749,4 +867,19 @@ export function syncMigrateCommand(args: string[] = []): void {
|
|
|
749
867
|
console.log(
|
|
750
868
|
`[mink] sync migrate: v${result.fromVersion} → v${result.toVersion} complete`
|
|
751
869
|
);
|
|
870
|
+
const identity = result.identity;
|
|
871
|
+
if (identity && (identity.renamed > 0 || identity.evicted > 0)) {
|
|
872
|
+
console.log(
|
|
873
|
+
` identity: ${identity.renamed} renamed, ${identity.converged} converged, ${identity.evicted} evicted`
|
|
874
|
+
);
|
|
875
|
+
for (const r of identity.renames) {
|
|
876
|
+
console.log(` renamed: ${r.from} → ${r.to}`);
|
|
877
|
+
}
|
|
878
|
+
for (const id of identity.evictions) {
|
|
879
|
+
console.log(` evicted: ${id} → .identity-rollback/`);
|
|
880
|
+
}
|
|
881
|
+
if (identity.backupDir) {
|
|
882
|
+
console.log(` rollback snapshot: ${identity.backupDir}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
752
885
|
}
|
package/src/core/sync.ts
CHANGED
|
@@ -51,6 +51,13 @@ config.local
|
|
|
51
51
|
# Migration coordination — never sync this
|
|
52
52
|
.sync-migrate.lock
|
|
53
53
|
|
|
54
|
+
# Per-device identity migration recovery snapshots — local recovery state
|
|
55
|
+
# for the migrating device, not authoritative project state. A device that
|
|
56
|
+
# needs to recover from its migration must do so from its own snapshot;
|
|
57
|
+
# syncing would make rollback dirs appear on devices that never produced
|
|
58
|
+
# the corresponding migration.
|
|
59
|
+
.identity-rollback/
|
|
60
|
+
|
|
54
61
|
# Local backups and per-device caches — machine-specific snapshots
|
|
55
62
|
projects/*/backups/
|
|
56
63
|
projects/*/session.json
|
|
File without changes
|
/package/dashboard/out/_next/static/{WyN-sdaVY7cZaACRaK7vq → Mmf6YVSNZzpPOZiW-DG5M}/_ssgManifest.js
RENAMED
|
File without changes
|