@drewpayment/mink 0.8.0 → 0.9.1
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 +2105 -1068
- package/package.json +1 -1
- package/src/commands/bug-search.ts +3 -3
- package/src/commands/detect-waste.ts +34 -25
- package/src/commands/init.ts +21 -21
- package/src/commands/post-read.ts +6 -3
- package/src/commands/post-write.ts +6 -3
- package/src/commands/pre-read.ts +14 -10
- package/src/commands/pre-write.ts +8 -5
- package/src/commands/reflect.ts +12 -7
- package/src/commands/session-start.ts +34 -3
- package/src/commands/session-stop.ts +10 -6
- package/src/commands/status.ts +29 -17
- package/src/commands/sync-migrate.ts +330 -0
- package/src/commands/sync.ts +75 -1
- package/src/commands/update.ts +4 -9
- package/src/core/conflict-park.ts +84 -0
- package/src/core/dashboard-api.ts +12 -31
- package/src/core/note-writer.ts +52 -6
- package/src/core/paths.ts +66 -10
- package/src/core/state-aggregator.ts +304 -0
- package/src/core/state-counters.ts +46 -0
- package/src/core/sync-merge-drivers.ts +247 -0
- package/src/core/sync.ts +150 -68
- package/src/core/token-ledger.ts +19 -3
- /package/dashboard/out/_next/static/{EC-_8nIOf1GnPrIqZ7Mk3 → r7Xr9mrUpunsz4QtD3jh1}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{EC-_8nIOf1GnPrIqZ7Mk3 → r7Xr9mrUpunsz4QtD3jh1}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
statSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
import {
|
|
14
|
+
minkRoot,
|
|
15
|
+
fileIndexCountersPath,
|
|
16
|
+
} from "../core/paths";
|
|
17
|
+
import {
|
|
18
|
+
MINK_SYNC_VERSION,
|
|
19
|
+
readSyncVersion,
|
|
20
|
+
writeSyncVersion,
|
|
21
|
+
ensureGitignore,
|
|
22
|
+
ensureGitAttributes,
|
|
23
|
+
ensureMergeDriversRegistered,
|
|
24
|
+
isSyncInitialized,
|
|
25
|
+
} from "../core/sync";
|
|
26
|
+
import { getOrCreateDeviceId } from "../core/device";
|
|
27
|
+
import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
|
|
28
|
+
import type { FileIndex } from "../types/file-index";
|
|
29
|
+
|
|
30
|
+
const MIGRATE_LOCK = ".sync-migrate.lock";
|
|
31
|
+
const MIGRATE_LOCK_STALE_MS = 300_000; // 5 minutes
|
|
32
|
+
const MIGRATE_BUDGET_MS = 5_000;
|
|
33
|
+
|
|
34
|
+
function gitSafe(args: string, timeoutMs: number = 5_000): string | null {
|
|
35
|
+
try {
|
|
36
|
+
return execSync(`git ${args}`, {
|
|
37
|
+
cwd: minkRoot(),
|
|
38
|
+
timeout: timeoutMs,
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
+
})
|
|
41
|
+
.toString()
|
|
42
|
+
.trim();
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function acquireLock(): boolean {
|
|
49
|
+
const path = join(minkRoot(), MIGRATE_LOCK);
|
|
50
|
+
if (existsSync(path)) {
|
|
51
|
+
try {
|
|
52
|
+
const ageMs = Date.now() - statSync(path).mtimeMs;
|
|
53
|
+
if (ageMs < MIGRATE_LOCK_STALE_MS) return false;
|
|
54
|
+
} catch {
|
|
55
|
+
// If stat fails, treat as stale and reclaim.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
writeFileSync(path, `${process.pid}\n`);
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function releaseLock(): void {
|
|
67
|
+
try {
|
|
68
|
+
unlinkSync(join(minkRoot(), MIGRATE_LOCK));
|
|
69
|
+
} catch {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Move a file from `from` to `to` using `git mv` when possible (preserves
|
|
75
|
+
// history) and a plain rename otherwise. Returns true if the move succeeded
|
|
76
|
+
// or the source did not exist.
|
|
77
|
+
function migrateFile(from: string, to: string): boolean {
|
|
78
|
+
if (!existsSync(from)) return true;
|
|
79
|
+
mkdirSync(join(to, ".."), { recursive: true });
|
|
80
|
+
// Prefer `git mv` so blame/history follow the file. Fall back to a plain
|
|
81
|
+
// rename if git can't handle it (e.g. the file isn't tracked yet).
|
|
82
|
+
if (gitSafe(`mv "${from}" "${to}"`) !== null) return true;
|
|
83
|
+
try {
|
|
84
|
+
renameSync(from, to);
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function migrateProject(projDir: string, deviceId: string): void {
|
|
92
|
+
const shardDir = join(projDir, "state", deviceId);
|
|
93
|
+
mkdirSync(shardDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
// Move per-device-rewritten files into the device shard. `git mv` preserves
|
|
96
|
+
// history; if a sibling shard already exists for this file (re-running the
|
|
97
|
+
// migration after a partial first run), we leave the sibling alone — it's
|
|
98
|
+
// already in the right place.
|
|
99
|
+
for (const file of [
|
|
100
|
+
"token-ledger.json",
|
|
101
|
+
"token-ledger-archive.json",
|
|
102
|
+
"bug-memory.json",
|
|
103
|
+
"action-log.md",
|
|
104
|
+
]) {
|
|
105
|
+
const legacy = join(projDir, file);
|
|
106
|
+
const shard = join(shardDir, file);
|
|
107
|
+
if (existsSync(shard)) continue;
|
|
108
|
+
migrateFile(legacy, shard);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// learning-memory.md: leave canonical in place. Touch an empty sidecar so
|
|
112
|
+
// future incremental writes have a target.
|
|
113
|
+
const sidecar = join(projDir, `learning-memory.${deviceId}.md`);
|
|
114
|
+
if (!existsSync(sidecar)) {
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(sidecar, "");
|
|
117
|
+
} catch {
|
|
118
|
+
// best-effort
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Drop session.json + scheduler-manifest.json from the index — they remain
|
|
123
|
+
// on disk but stop being synced (they're now gitignored under v2).
|
|
124
|
+
for (const f of ["session.json", "scheduler-manifest.json"]) {
|
|
125
|
+
if (existsSync(join(projDir, f))) {
|
|
126
|
+
gitSafe(`rm --cached "${join(projDir, f)}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Split file-index counters out into a per-device counter file.
|
|
131
|
+
const indexPath = join(projDir, "file-index.json");
|
|
132
|
+
if (existsSync(indexPath)) {
|
|
133
|
+
const raw = safeReadJson(indexPath) as FileIndex | null;
|
|
134
|
+
if (
|
|
135
|
+
raw &&
|
|
136
|
+
typeof raw.header === "object" &&
|
|
137
|
+
raw.header !== null &&
|
|
138
|
+
(raw.header.lifetimeHits > 0 || raw.header.lifetimeMisses > 0)
|
|
139
|
+
) {
|
|
140
|
+
// Carry forward the existing counters so the per-device telemetry
|
|
141
|
+
// continues uninterrupted on this device.
|
|
142
|
+
atomicWriteJson(fileIndexCountersPathFor(projDir), {
|
|
143
|
+
fileIndexHits: raw.header.lifetimeHits,
|
|
144
|
+
fileIndexMisses: raw.header.lifetimeMisses,
|
|
145
|
+
});
|
|
146
|
+
raw.header.lifetimeHits = 0;
|
|
147
|
+
raw.header.lifetimeMisses = 0;
|
|
148
|
+
atomicWriteJson(indexPath, raw);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function fileIndexCountersPathFor(projDir: string): string {
|
|
154
|
+
return join(projDir, ".mink-state-counters.json");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function listProjects(): string[] {
|
|
158
|
+
const projectsRoot = join(minkRoot(), "projects");
|
|
159
|
+
if (!existsSync(projectsRoot)) return [];
|
|
160
|
+
try {
|
|
161
|
+
return readdirSync(projectsRoot)
|
|
162
|
+
.filter((name) => {
|
|
163
|
+
try {
|
|
164
|
+
return statSync(join(projectsRoot, name)).isDirectory();
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.map((name) => join(projectsRoot, name));
|
|
170
|
+
} catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// True if any legacy v1 state shape still lives at the top level of projDir
|
|
176
|
+
// AND no per-device shard has been populated yet. Once a shard directory has
|
|
177
|
+
// any contents, this project counts as migrated even if a stale legacy file
|
|
178
|
+
// is still on disk — that's the case where the user opened a session
|
|
179
|
+
// mid-migration and writes started landing in the shard. The aggregator unions
|
|
180
|
+
// across legacy + shards on read, so the stale file is harmless until cleaned
|
|
181
|
+
// up; what we must avoid is a permanent re-migrate loop on every session-start.
|
|
182
|
+
function projectNeedsMigration(projDir: string): boolean {
|
|
183
|
+
const stateDir = join(projDir, "state");
|
|
184
|
+
if (existsSync(stateDir)) {
|
|
185
|
+
try {
|
|
186
|
+
const shards = readdirSync(stateDir).filter((d) => {
|
|
187
|
+
try {
|
|
188
|
+
return statSync(join(stateDir, d)).isDirectory();
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (shards.length > 0) return false;
|
|
194
|
+
} catch {
|
|
195
|
+
// fall through
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const f of [
|
|
199
|
+
"token-ledger.json",
|
|
200
|
+
"token-ledger-archive.json",
|
|
201
|
+
"bug-memory.json",
|
|
202
|
+
"action-log.md",
|
|
203
|
+
]) {
|
|
204
|
+
if (existsSync(join(projDir, f))) return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function listProjectsNeedingMigration(): string[] {
|
|
210
|
+
return listProjects().filter(projectNeedsMigration);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface MigrateResult {
|
|
214
|
+
ranMigration: boolean;
|
|
215
|
+
fromVersion: number;
|
|
216
|
+
toVersion: number;
|
|
217
|
+
message?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Idempotent. Safe to invoke from `mink sync migrate` directly or from a
|
|
221
|
+
// session-start auto-trigger when readSyncVersion() < MINK_SYNC_VERSION.
|
|
222
|
+
//
|
|
223
|
+
// We treat the version marker as a hint, not a gate — a previous partial run
|
|
224
|
+
// (interrupted by the budget cap) may have written v2 with projects still
|
|
225
|
+
// pending. We re-run as long as any project on disk still has legacy files at
|
|
226
|
+
// its top level, regardless of marker.
|
|
227
|
+
export function migrateSyncLayout(): MigrateResult {
|
|
228
|
+
const fromVersion = readSyncVersion();
|
|
229
|
+
const pending = listProjectsNeedingMigration();
|
|
230
|
+
if (fromVersion >= MINK_SYNC_VERSION && pending.length === 0) {
|
|
231
|
+
return {
|
|
232
|
+
ranMigration: false,
|
|
233
|
+
fromVersion,
|
|
234
|
+
toVersion: MINK_SYNC_VERSION,
|
|
235
|
+
message: `already at v${MINK_SYNC_VERSION}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
|
|
241
|
+
if (!acquireLock()) {
|
|
242
|
+
return {
|
|
243
|
+
ranMigration: false,
|
|
244
|
+
fromVersion,
|
|
245
|
+
toVersion: MINK_SYNC_VERSION,
|
|
246
|
+
message: "another migration is in progress",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// Refresh .gitignore/.gitattributes/merge drivers regardless of whether
|
|
252
|
+
// sync is initialised — they're cheap and idempotent. The merge-driver
|
|
253
|
+
// registration is a no-op when .git/ doesn't exist.
|
|
254
|
+
ensureGitignore();
|
|
255
|
+
if (isSyncInitialized()) {
|
|
256
|
+
ensureGitAttributes();
|
|
257
|
+
ensureMergeDriversRegistered();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const deviceId = getOrCreateDeviceId();
|
|
261
|
+
|
|
262
|
+
// Stash uncommitted changes so the migrating commit doesn't sweep up
|
|
263
|
+
// unrelated edits. Best-effort — if nothing to stash, this is a no-op.
|
|
264
|
+
let stashed = false;
|
|
265
|
+
if (isSyncInitialized()) {
|
|
266
|
+
const status = gitSafe("status --porcelain");
|
|
267
|
+
if (status && status.trim().length > 0) {
|
|
268
|
+
if (gitSafe("stash push -m mink-sync-migrate") !== null) {
|
|
269
|
+
stashed = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Process pending projects only — already-migrated projects are skipped
|
|
275
|
+
// for free, and we resume work from any prior partial run.
|
|
276
|
+
let processed = 0;
|
|
277
|
+
let remaining = 0;
|
|
278
|
+
for (const projDir of listProjectsNeedingMigration()) {
|
|
279
|
+
if (Date.now() - start > MIGRATE_BUDGET_MS) {
|
|
280
|
+
remaining++;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
migrateProject(projDir, deviceId);
|
|
285
|
+
processed++;
|
|
286
|
+
} catch {
|
|
287
|
+
// best-effort per project — never block migration on one project
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Only stamp the version marker once nothing is left to migrate. If we
|
|
292
|
+
// still have pending projects, leave the marker as-is so the next session
|
|
293
|
+
// knows to keep going.
|
|
294
|
+
if (remaining === 0 && listProjectsNeedingMigration().length === 0) {
|
|
295
|
+
writeSyncVersion(MINK_SYNC_VERSION);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isSyncInitialized() && processed > 0) {
|
|
299
|
+
// Skip the lock file — it's part of migration coordination, not state.
|
|
300
|
+
gitSafe("add -A");
|
|
301
|
+
gitSafe(`reset HEAD ".sync-migrate.lock"`);
|
|
302
|
+
gitSafe(
|
|
303
|
+
`commit -m "mink: migrate sync layout v${fromVersion} -> v${MINK_SYNC_VERSION} (device ${deviceId.slice(0, 8)}, ${processed} projects)"`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (stashed) {
|
|
308
|
+
gitSafe("stash pop");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
ranMigration: true,
|
|
313
|
+
fromVersion,
|
|
314
|
+
toVersion: MINK_SYNC_VERSION,
|
|
315
|
+
};
|
|
316
|
+
} finally {
|
|
317
|
+
releaseLock();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function syncMigrateCommand(): void {
|
|
322
|
+
const result = migrateSyncLayout();
|
|
323
|
+
if (!result.ranMigration) {
|
|
324
|
+
console.log(`[mink] sync migrate: ${result.message ?? "no-op"}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
console.log(
|
|
328
|
+
`[mink] sync migrate: v${result.fromVersion} → v${result.toVersion} complete`
|
|
329
|
+
);
|
|
330
|
+
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
isSyncInitialized,
|
|
8
8
|
} from "../core/sync";
|
|
9
9
|
import { setConfigValue } from "../core/global-config";
|
|
10
|
+
import { runMergeDriver } from "../core/sync-merge-drivers";
|
|
11
|
+
import {
|
|
12
|
+
listParkedConflicts,
|
|
13
|
+
dropParkedConflict,
|
|
14
|
+
} from "../core/conflict-park";
|
|
10
15
|
|
|
11
16
|
export async function sync(args: string[]): Promise<void> {
|
|
12
17
|
const subcommand = args[0];
|
|
@@ -39,13 +44,82 @@ export async function sync(args: string[]): Promise<void> {
|
|
|
39
44
|
case "disconnect":
|
|
40
45
|
return handleDisconnect();
|
|
41
46
|
|
|
47
|
+
case "merge-driver":
|
|
48
|
+
return handleMergeDriver(args.slice(1));
|
|
49
|
+
|
|
50
|
+
case "reconcile":
|
|
51
|
+
return handleReconcile(args.slice(1));
|
|
52
|
+
|
|
53
|
+
case "migrate": {
|
|
54
|
+
const { syncMigrateCommand } = await import("./sync-migrate");
|
|
55
|
+
syncMigrateCommand();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
default:
|
|
43
60
|
console.error(`[mink] unknown sync subcommand: ${subcommand}`);
|
|
44
|
-
console.error(
|
|
61
|
+
console.error(
|
|
62
|
+
"Usage: mink sync [init|status|push|pull|pause|resume|disconnect|reconcile|migrate|merge-driver]"
|
|
63
|
+
);
|
|
45
64
|
process.exit(1);
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
67
|
|
|
68
|
+
function handleReconcile(args: string[]): void {
|
|
69
|
+
const sub = args[0];
|
|
70
|
+
if (sub === undefined || sub === "list") {
|
|
71
|
+
const refs = listParkedConflicts();
|
|
72
|
+
if (refs.length === 0) {
|
|
73
|
+
console.log("[mink] no parked conflicts");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(`[mink] ${refs.length} parked conflict ref(s):`);
|
|
77
|
+
for (const r of refs) console.log(` ${r}`);
|
|
78
|
+
console.log(
|
|
79
|
+
"Inspect with: cd ~/.mink && git log <ref> | git diff main..<ref>"
|
|
80
|
+
);
|
|
81
|
+
console.log("Drop with: mink sync reconcile drop <ref>");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (sub === "drop") {
|
|
85
|
+
const ref = args[1];
|
|
86
|
+
if (!ref) {
|
|
87
|
+
console.error("Usage: mink sync reconcile drop <ref>");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (dropParkedConflict(ref)) {
|
|
91
|
+
console.log(`[mink] dropped ${ref}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.error(`[mink] failed to drop ${ref} — only refs/mink/conflicts/* are droppable`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.error("Usage: mink sync reconcile [list|drop <ref>]");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Invoked by git for paths matched in .gitattributes. Always exits 0 so a
|
|
103
|
+
// merge can never block — the driver itself logs warnings and falls back to
|
|
104
|
+
// "ours" when inputs are unparseable.
|
|
105
|
+
function handleMergeDriver(args: string[]): void {
|
|
106
|
+
const [name, basePath, oursPath, theirsPath, filePath] = args;
|
|
107
|
+
if (!name || !basePath || !oursPath || !theirsPath) {
|
|
108
|
+
console.error(
|
|
109
|
+
"Usage: mink sync merge-driver <name> <base> <ours> <theirs> [path]"
|
|
110
|
+
);
|
|
111
|
+
process.exit(0); // exit 0 so git never sees a failure here
|
|
112
|
+
}
|
|
113
|
+
const code = runMergeDriver(
|
|
114
|
+
name,
|
|
115
|
+
basePath,
|
|
116
|
+
oursPath,
|
|
117
|
+
theirsPath,
|
|
118
|
+
filePath ?? oursPath
|
|
119
|
+
);
|
|
120
|
+
process.exit(code);
|
|
121
|
+
}
|
|
122
|
+
|
|
49
123
|
function handleManualSync(): void {
|
|
50
124
|
if (!isSyncInitialized()) {
|
|
51
125
|
console.error("[mink] sync is not initialized");
|
package/src/commands/update.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { resolve
|
|
2
|
-
import { existsSync } from "fs";
|
|
1
|
+
import { resolve } from "path";
|
|
3
2
|
import { listRegisteredProjects } from "../core/project-registry";
|
|
4
3
|
import { createBackup } from "../core/backup";
|
|
5
4
|
import { projectMetaPath } from "../core/paths";
|
|
6
5
|
import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
|
|
7
|
-
import { buildHooksConfig,
|
|
6
|
+
import { buildHooksConfig, mergeHooksIntoSettings, resolveCliPath } from "./init";
|
|
8
7
|
|
|
9
8
|
function parseArgs(args: string[]): {
|
|
10
9
|
dryRun: boolean;
|
|
@@ -78,12 +77,8 @@ export async function update(cwd: string, args: string[]): Promise<void> {
|
|
|
78
77
|
return;
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
dirname(new URL(import.meta.url).pathname),
|
|
84
|
-
"../cli.ts"
|
|
85
|
-
);
|
|
86
|
-
const newHooks = buildHooksConfig(runtime, cliPath);
|
|
80
|
+
const cliPath = resolveCliPath();
|
|
81
|
+
const newHooks = buildHooksConfig(cliPath);
|
|
87
82
|
|
|
88
83
|
for (const target of targets) {
|
|
89
84
|
console.log(`[mink] updating: ${target.name} (${target.id})`);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { minkRoot } from "./paths";
|
|
5
|
+
import { getOrCreateDeviceId } from "./device";
|
|
6
|
+
|
|
7
|
+
const GIT_TIMEOUT = 5_000;
|
|
8
|
+
|
|
9
|
+
function git(args: string): string {
|
|
10
|
+
return execSync(`git ${args}`, {
|
|
11
|
+
cwd: minkRoot(),
|
|
12
|
+
timeout: GIT_TIMEOUT,
|
|
13
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
14
|
+
})
|
|
15
|
+
.toString()
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function gitSafe(args: string): string | null {
|
|
20
|
+
try {
|
|
21
|
+
return git(args);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Park the current local state onto a hidden ref so an unresolvable merge
|
|
28
|
+
// never blocks sync. Sequence:
|
|
29
|
+
// 1. If a merge is in progress, abort it cleanly (`git merge --abort`).
|
|
30
|
+
// 2. Save HEAD as `refs/mink/conflicts/<deviceId>/<iso-utc>`.
|
|
31
|
+
// 3. Hard-reset working tree to upstream (origin/<branch>) so subsequent
|
|
32
|
+
// writes start from a clean, fast-forwardable state.
|
|
33
|
+
// Returns the parked refname (or null if the operation was a no-op or failed —
|
|
34
|
+
// callers must NEVER throw on the result).
|
|
35
|
+
export function parkConflictingState(reason: string): string | null {
|
|
36
|
+
const root = minkRoot();
|
|
37
|
+
const inMerge =
|
|
38
|
+
existsSync(join(root, ".git", "MERGE_HEAD")) ||
|
|
39
|
+
existsSync(join(root, ".git", "rebase-merge")) ||
|
|
40
|
+
existsSync(join(root, ".git", "rebase-apply"));
|
|
41
|
+
|
|
42
|
+
if (inMerge) {
|
|
43
|
+
gitSafe("merge --abort");
|
|
44
|
+
gitSafe("rebase --abort");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const branch = gitSafe("rev-parse --abbrev-ref HEAD") ?? "main";
|
|
48
|
+
const upstream = `origin/${branch}`;
|
|
49
|
+
|
|
50
|
+
// Don't park if there's nothing to save (HEAD already matches upstream).
|
|
51
|
+
const headSha = gitSafe("rev-parse HEAD");
|
|
52
|
+
const upstreamSha = gitSafe(`rev-parse ${upstream}`);
|
|
53
|
+
if (headSha && headSha === upstreamSha) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const deviceId = getOrCreateDeviceId();
|
|
58
|
+
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
59
|
+
const ref = `refs/mink/conflicts/${deviceId}/${iso}`;
|
|
60
|
+
|
|
61
|
+
if (!headSha) return null;
|
|
62
|
+
|
|
63
|
+
if (gitSafe(`update-ref ${ref} ${headSha}`) === null) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
gitSafe(`reset --hard ${upstream}`);
|
|
67
|
+
|
|
68
|
+
return ref;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// List previously-parked conflict refs. Used by `mink sync reconcile list`.
|
|
72
|
+
export function listParkedConflicts(): string[] {
|
|
73
|
+
const out = gitSafe("for-each-ref --format=%(refname) refs/mink/conflicts");
|
|
74
|
+
if (!out) return [];
|
|
75
|
+
return out
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((s) => s.trim())
|
|
78
|
+
.filter((s) => s.startsWith("refs/mink/conflicts/"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function dropParkedConflict(ref: string): boolean {
|
|
82
|
+
if (!ref.startsWith("refs/mink/conflicts/")) return false;
|
|
83
|
+
return gitSafe(`update-ref -d ${ref}`) !== null;
|
|
84
|
+
}
|
|
@@ -18,6 +18,12 @@ import { loadLedger } from "./token-ledger";
|
|
|
18
18
|
import { parseLearningMemory } from "./learning-memory";
|
|
19
19
|
import { loadBugMemory } from "./bug-memory";
|
|
20
20
|
import { safeReadLog, parseLogSessions } from "./action-log";
|
|
21
|
+
import {
|
|
22
|
+
aggregateTokenLedger,
|
|
23
|
+
aggregateBugMemory,
|
|
24
|
+
aggregateActionLog,
|
|
25
|
+
aggregateLearningMemory,
|
|
26
|
+
} from "./state-aggregator";
|
|
21
27
|
import { getDaemonStatus, startDaemon, stopDaemon } from "./daemon";
|
|
22
28
|
import { loadManifest, removeFromDeadLetter, saveManifest } from "./scheduler";
|
|
23
29
|
import { getBuiltInTasks, executeTask } from "./task-registry";
|
|
@@ -147,8 +153,8 @@ export function loadOverview(cwd: string): OverviewPayload {
|
|
|
147
153
|
: undefined,
|
|
148
154
|
};
|
|
149
155
|
|
|
150
|
-
// Token ledger summary
|
|
151
|
-
const ledger =
|
|
156
|
+
// Token ledger summary (aggregated across all device shards + legacy)
|
|
157
|
+
const ledger = aggregateTokenLedger(cwd);
|
|
152
158
|
const summary = {
|
|
153
159
|
totalSessions: ledger.lifetime.totalSessions,
|
|
154
160
|
totalTokens: ledger.lifetime.totalTokens,
|
|
@@ -173,7 +179,7 @@ export function loadOverview(cwd: string): OverviewPayload {
|
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
export function loadTokenLedgerPanel(cwd: string): TokenLedgerPayload {
|
|
176
|
-
const ledger =
|
|
182
|
+
const ledger = aggregateTokenLedger(cwd);
|
|
177
183
|
return {
|
|
178
184
|
lifetime: ledger.lifetime,
|
|
179
185
|
sessions: ledger.sessions,
|
|
@@ -216,42 +222,17 @@ export function loadSchedulerPanel(cwd: string): SchedulerPayload {
|
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
export function loadLearningMemoryPanel(cwd: string): LearningMemory {
|
|
219
|
-
|
|
220
|
-
if (!existsSync(memPath)) {
|
|
221
|
-
return {
|
|
222
|
-
projectName: "unknown",
|
|
223
|
-
sections: {
|
|
224
|
-
"User Preferences": [],
|
|
225
|
-
"Key Learnings": [],
|
|
226
|
-
"Do-Not-Repeat": [],
|
|
227
|
-
"Decision Log": [],
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
const content = readFileSync(memPath, "utf-8");
|
|
233
|
-
return parseLearningMemory(content);
|
|
234
|
-
} catch {
|
|
235
|
-
return {
|
|
236
|
-
projectName: "unknown",
|
|
237
|
-
sections: {
|
|
238
|
-
"User Preferences": [],
|
|
239
|
-
"Key Learnings": [],
|
|
240
|
-
"Do-Not-Repeat": [],
|
|
241
|
-
"Decision Log": [],
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
}
|
|
225
|
+
return aggregateLearningMemory(cwd);
|
|
245
226
|
}
|
|
246
227
|
|
|
247
228
|
export function loadActionLogPanel(cwd: string): ActionLogPayload {
|
|
248
|
-
const content =
|
|
229
|
+
const content = aggregateActionLog(cwd);
|
|
249
230
|
const sessions = parseLogSessions(content);
|
|
250
231
|
return { sessions };
|
|
251
232
|
}
|
|
252
233
|
|
|
253
234
|
export function loadBugLogPanel(cwd: string): BugLogPayload {
|
|
254
|
-
const memory =
|
|
235
|
+
const memory = aggregateBugMemory(cwd);
|
|
255
236
|
return { entries: memory.entries, nextId: memory.nextId };
|
|
256
237
|
}
|
|
257
238
|
|