@drewpayment/mink 0.11.0-beta.2 → 0.11.0-beta.4
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 +114 -20
- package/package.json +1 -1
- package/src/commands/sync-migrate.ts +175 -25
- package/src/core/sync.ts +7 -0
- /package/dashboard/out/_next/static/{WyN-sdaVY7cZaACRaK7vq → fci7mSuW5y3ri6IlmLojm}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{WyN-sdaVY7cZaACRaK7vq → fci7mSuW5y3ri6IlmLojm}/_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,70 @@ 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 new
|
|
476
|
+
// dir has no project-meta.json (e.g. the daemon wrote state under the
|
|
477
|
+
// git-derived id before any init or migrate ran), addProjectAlias would
|
|
478
|
+
// silently no-op and we'd leave the old dir stranded forever. Repair
|
|
479
|
+
// that case by writing the old meta forward — the daemon-authored
|
|
480
|
+
// payload state under the new dir is preserved; only the missing meta
|
|
481
|
+
// gets reconstructed, with the alias already in place.
|
|
482
|
+
let aliasOnRecord = false;
|
|
417
483
|
try {
|
|
418
|
-
|
|
484
|
+
if (entry.action === "skip-evict") {
|
|
485
|
+
aliasOnRecord = true;
|
|
486
|
+
} else {
|
|
487
|
+
addProjectAlias(newProjDir, entry.oldId);
|
|
488
|
+
let newMeta = getProjectMeta(newProjDir);
|
|
489
|
+
if (!newMeta) {
|
|
490
|
+
const oldMeta = getProjectMeta(oldProjDir);
|
|
491
|
+
if (oldMeta) {
|
|
492
|
+
atomicWriteJson(join(newProjDir, "project-meta.json"), {
|
|
493
|
+
cwd: oldMeta.cwd,
|
|
494
|
+
name: oldMeta.name,
|
|
495
|
+
initTimestamp: oldMeta.initTimestamp,
|
|
496
|
+
version: oldMeta.version,
|
|
497
|
+
aliases: [...(oldMeta.aliases ?? []), entry.oldId],
|
|
498
|
+
pathsByDevice: oldMeta.pathsByDevice,
|
|
499
|
+
});
|
|
500
|
+
newMeta = getProjectMeta(newProjDir);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
aliasOnRecord = newMeta?.aliases?.includes(entry.oldId) ?? false;
|
|
504
|
+
}
|
|
419
505
|
if (entry.cwd) {
|
|
420
506
|
setProjectPathForDevice(newProjDir, deviceId, entry.cwd);
|
|
421
507
|
}
|
|
422
508
|
} catch {
|
|
423
509
|
// best-effort
|
|
424
510
|
}
|
|
511
|
+
|
|
512
|
+
if (aliasOnRecord && backupRoot) {
|
|
513
|
+
// Snapshot the old dir before eviction so any local-only state
|
|
514
|
+
// (writes that landed here before sync converged) is recoverable.
|
|
515
|
+
const ok = backupProjectForRollback(
|
|
516
|
+
oldProjDir,
|
|
517
|
+
join(backupRoot, entry.oldId)
|
|
518
|
+
);
|
|
519
|
+
if (ok) {
|
|
520
|
+
let removed = false;
|
|
521
|
+
try {
|
|
522
|
+
rmSync(oldProjDir, { recursive: true, force: true });
|
|
523
|
+
removed = true;
|
|
524
|
+
} catch {
|
|
525
|
+
// best-effort; leave the directory rather than partially deleted
|
|
526
|
+
}
|
|
527
|
+
if (removed) {
|
|
528
|
+
evictions.push(entry.oldId);
|
|
529
|
+
evicted++;
|
|
530
|
+
if (entry.action === "skip-converged") converged++;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
425
534
|
continue;
|
|
426
535
|
}
|
|
427
536
|
|
|
@@ -456,9 +565,18 @@ function migrateProjectIdentities(
|
|
|
456
565
|
// best-effort
|
|
457
566
|
}
|
|
458
567
|
renamed++;
|
|
568
|
+
renames.push({ from: entry.oldId, to: entry.newId });
|
|
459
569
|
}
|
|
460
570
|
|
|
461
|
-
return {
|
|
571
|
+
return {
|
|
572
|
+
renamed,
|
|
573
|
+
converged,
|
|
574
|
+
evicted,
|
|
575
|
+
visited,
|
|
576
|
+
backupDir: backupRoot,
|
|
577
|
+
renames,
|
|
578
|
+
evictions,
|
|
579
|
+
};
|
|
462
580
|
}
|
|
463
581
|
|
|
464
582
|
// ── v3 identity rollback ──────────────────────────────────────────────────
|
|
@@ -545,6 +663,7 @@ export interface MigrateResult {
|
|
|
545
663
|
fromVersion: number;
|
|
546
664
|
toVersion: number;
|
|
547
665
|
message?: string;
|
|
666
|
+
identity?: IdentityMigrationOutcome;
|
|
548
667
|
}
|
|
549
668
|
|
|
550
669
|
// Idempotent. Safe to invoke from `mink sync migrate` directly or from a
|
|
@@ -636,7 +755,15 @@ export function migrateSyncLayout(): MigrateResult {
|
|
|
636
755
|
// flag is off or every project's identifier already matches its directory.
|
|
637
756
|
// Pass the pre-stash snapshot of identityMode so we don't re-read the
|
|
638
757
|
// config from a stash-hidden working tree.
|
|
639
|
-
let identity = {
|
|
758
|
+
let identity: IdentityMigrationOutcome = {
|
|
759
|
+
renamed: 0,
|
|
760
|
+
converged: 0,
|
|
761
|
+
evicted: 0,
|
|
762
|
+
visited: 0,
|
|
763
|
+
backupDir: null,
|
|
764
|
+
renames: [],
|
|
765
|
+
evictions: [],
|
|
766
|
+
};
|
|
640
767
|
try {
|
|
641
768
|
identity = migrateProjectIdentities(deviceId, identityMode);
|
|
642
769
|
} catch {
|
|
@@ -650,16 +777,19 @@ export function migrateSyncLayout(): MigrateResult {
|
|
|
650
777
|
writeSyncVersion(MINK_SYNC_VERSION);
|
|
651
778
|
}
|
|
652
779
|
|
|
653
|
-
if (
|
|
780
|
+
if (
|
|
781
|
+
isSyncInitialized() &&
|
|
782
|
+
(processed > 0 || identity.renamed > 0 || identity.evicted > 0)
|
|
783
|
+
) {
|
|
654
784
|
// Skip the lock file — it's part of migration coordination, not state.
|
|
655
785
|
gitSafe("add -A");
|
|
656
786
|
gitSafe(`reset HEAD ".sync-migrate.lock"`);
|
|
657
|
-
const
|
|
658
|
-
identity.renamed > 0
|
|
659
|
-
?
|
|
660
|
-
:
|
|
787
|
+
const identityNote =
|
|
788
|
+
identity.renamed > 0 || identity.evicted > 0
|
|
789
|
+
? `, ${identity.renamed} renamed + ${identity.evicted} evicted for identity v3`
|
|
790
|
+
: "";
|
|
661
791
|
gitSafe(
|
|
662
|
-
`commit -m "mink: migrate sync layout v${fromVersion} -> v${MINK_SYNC_VERSION} (device ${deviceId.slice(0, 8)}, ${
|
|
792
|
+
`commit -m "mink: migrate sync layout v${fromVersion} -> v${MINK_SYNC_VERSION} (device ${deviceId.slice(0, 8)}, ${processed} projects${identityNote})"`
|
|
663
793
|
);
|
|
664
794
|
}
|
|
665
795
|
|
|
@@ -671,6 +801,7 @@ export function migrateSyncLayout(): MigrateResult {
|
|
|
671
801
|
ranMigration: true,
|
|
672
802
|
fromVersion,
|
|
673
803
|
toVersion: MINK_SYNC_VERSION,
|
|
804
|
+
identity,
|
|
674
805
|
};
|
|
675
806
|
} finally {
|
|
676
807
|
releaseLock();
|
|
@@ -696,20 +827,24 @@ export function syncMigrateCommand(args: string[] = []): void {
|
|
|
696
827
|
}
|
|
697
828
|
const renames = plan.filter((p) => p.action === "rename");
|
|
698
829
|
const converged = plan.filter((p) => p.action === "skip-converged");
|
|
830
|
+
const evictOnly = plan.filter((p) => p.action === "skip-evict");
|
|
699
831
|
const skippedNoCwd = plan.filter((p) => p.action === "skip-no-cwd");
|
|
700
832
|
const unchanged = plan.filter((p) => p.action === "skip-unchanged");
|
|
701
833
|
|
|
702
834
|
console.log(
|
|
703
|
-
`[mink] sync migrate --dry-run: ${renames.length} rename(s), ${converged.length} alias-only, ${skippedNoCwd.length} skipped (no cwd), ${unchanged.length} unchanged`
|
|
835
|
+
`[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
836
|
);
|
|
705
837
|
for (const p of renames) {
|
|
706
|
-
console.log(` rename:
|
|
838
|
+
console.log(` rename: ${p.oldId} → ${p.newId}`);
|
|
707
839
|
}
|
|
708
840
|
for (const p of converged) {
|
|
709
|
-
console.log(`
|
|
841
|
+
console.log(` converge: ${p.oldId} → ${p.newId} (record alias on ${p.newId}, evict ${p.oldId} to .identity-rollback/)`);
|
|
842
|
+
}
|
|
843
|
+
for (const p of evictOnly) {
|
|
844
|
+
console.log(` evict: ${p.oldId} → .identity-rollback/ (alias already on ${p.newId})`);
|
|
710
845
|
}
|
|
711
846
|
for (const p of skippedNoCwd) {
|
|
712
|
-
console.log(` skip:
|
|
847
|
+
console.log(` skip: ${p.oldId} — ${p.reason}`);
|
|
713
848
|
}
|
|
714
849
|
return;
|
|
715
850
|
}
|
|
@@ -749,4 +884,19 @@ export function syncMigrateCommand(args: string[] = []): void {
|
|
|
749
884
|
console.log(
|
|
750
885
|
`[mink] sync migrate: v${result.fromVersion} → v${result.toVersion} complete`
|
|
751
886
|
);
|
|
887
|
+
const identity = result.identity;
|
|
888
|
+
if (identity && (identity.renamed > 0 || identity.evicted > 0)) {
|
|
889
|
+
console.log(
|
|
890
|
+
` identity: ${identity.renamed} renamed, ${identity.converged} converged, ${identity.evicted} evicted`
|
|
891
|
+
);
|
|
892
|
+
for (const r of identity.renames) {
|
|
893
|
+
console.log(` renamed: ${r.from} → ${r.to}`);
|
|
894
|
+
}
|
|
895
|
+
for (const id of identity.evictions) {
|
|
896
|
+
console.log(` evicted: ${id} → .identity-rollback/`);
|
|
897
|
+
}
|
|
898
|
+
if (identity.backupDir) {
|
|
899
|
+
console.log(` rollback snapshot: ${identity.backupDir}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
752
902
|
}
|
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 → fci7mSuW5y3ri6IlmLojm}/_ssgManifest.js
RENAMED
|
File without changes
|