@drewpayment/mink 0.1.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.
Files changed (72) hide show
  1. package/README.md +347 -0
  2. package/package.json +32 -0
  3. package/src/cli.ts +176 -0
  4. package/src/commands/bug-search.ts +32 -0
  5. package/src/commands/config.ts +109 -0
  6. package/src/commands/cron.ts +295 -0
  7. package/src/commands/daemon.ts +46 -0
  8. package/src/commands/dashboard.ts +21 -0
  9. package/src/commands/designqc.ts +160 -0
  10. package/src/commands/detect-waste.ts +81 -0
  11. package/src/commands/framework-advisor.ts +52 -0
  12. package/src/commands/init.ts +159 -0
  13. package/src/commands/post-read.ts +123 -0
  14. package/src/commands/post-write.ts +157 -0
  15. package/src/commands/pre-read.ts +109 -0
  16. package/src/commands/pre-write.ts +136 -0
  17. package/src/commands/reflect.ts +39 -0
  18. package/src/commands/restore.ts +31 -0
  19. package/src/commands/scan.ts +101 -0
  20. package/src/commands/session-start.ts +21 -0
  21. package/src/commands/session-stop.ts +115 -0
  22. package/src/commands/status.ts +152 -0
  23. package/src/commands/update.ts +121 -0
  24. package/src/core/action-log.ts +341 -0
  25. package/src/core/backup.ts +122 -0
  26. package/src/core/bug-memory.ts +223 -0
  27. package/src/core/cron-parser.ts +94 -0
  28. package/src/core/daemon.ts +152 -0
  29. package/src/core/dashboard-api.ts +280 -0
  30. package/src/core/dashboard-server.ts +580 -0
  31. package/src/core/description.ts +232 -0
  32. package/src/core/design-eval/capture.ts +269 -0
  33. package/src/core/design-eval/route-detect.ts +165 -0
  34. package/src/core/design-eval/server-detect.ts +91 -0
  35. package/src/core/framework-advisor/catalog.ts +360 -0
  36. package/src/core/framework-advisor/decision-tree.ts +287 -0
  37. package/src/core/framework-advisor/generate.ts +132 -0
  38. package/src/core/framework-advisor/migration-prompts.ts +502 -0
  39. package/src/core/framework-advisor/validate.ts +137 -0
  40. package/src/core/fs-utils.ts +30 -0
  41. package/src/core/global-config.ts +74 -0
  42. package/src/core/index-store.ts +72 -0
  43. package/src/core/learning-memory.ts +120 -0
  44. package/src/core/paths.ts +86 -0
  45. package/src/core/pattern-engine.ts +108 -0
  46. package/src/core/project-id.ts +19 -0
  47. package/src/core/project-registry.ts +64 -0
  48. package/src/core/reflection.ts +256 -0
  49. package/src/core/scanner.ts +99 -0
  50. package/src/core/scheduler.ts +352 -0
  51. package/src/core/seed.ts +239 -0
  52. package/src/core/session.ts +128 -0
  53. package/src/core/stdin.ts +13 -0
  54. package/src/core/task-registry.ts +202 -0
  55. package/src/core/token-estimate.ts +36 -0
  56. package/src/core/token-ledger.ts +185 -0
  57. package/src/core/waste-detection.ts +214 -0
  58. package/src/core/write-exclusions.ts +24 -0
  59. package/src/types/action-log.ts +20 -0
  60. package/src/types/backup.ts +6 -0
  61. package/src/types/bug-memory.ts +24 -0
  62. package/src/types/config.ts +59 -0
  63. package/src/types/dashboard.ts +104 -0
  64. package/src/types/design-eval.ts +64 -0
  65. package/src/types/file-index.ts +38 -0
  66. package/src/types/framework-advisor.ts +97 -0
  67. package/src/types/hook-input.ts +27 -0
  68. package/src/types/learning-memory.ts +36 -0
  69. package/src/types/scheduler.ts +82 -0
  70. package/src/types/session.ts +50 -0
  71. package/src/types/token-ledger.ts +43 -0
  72. package/src/types/waste-detection.ts +21 -0
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { parseLearningMemory, serializeLearningMemory } from "../core/learning-memory";
3
+ import { reflectMemory } from "../core/reflection";
4
+ import { atomicWriteText, safeReadJson } from "../core/fs-utils";
5
+ import type { ReflectionResult } from "../types/learning-memory";
6
+ import type { ProjectConfig } from "../types/file-index";
7
+
8
+ const DEFAULT_TOKEN_BUDGET = 2000;
9
+
10
+ export function reflect(
11
+ projectDir: string,
12
+ memoryPath: string,
13
+ configPath: string
14
+ ): ReflectionResult | null {
15
+ if (!existsSync(memoryPath)) {
16
+ console.log("[mink] no learning memory found");
17
+ return null;
18
+ }
19
+
20
+ const markdown = readFileSync(memoryPath, "utf-8");
21
+ const mem = parseLearningMemory(markdown);
22
+
23
+ const config = safeReadJson(configPath) as ProjectConfig | null;
24
+ const tokenBudget = config?.learningMemoryTokenBudget ?? DEFAULT_TOKEN_BUDGET;
25
+
26
+ const { memory: updated, result } = reflectMemory(mem, tokenBudget);
27
+
28
+ if (result.mergedCount > 0 || result.trimmedCount > 0) {
29
+ atomicWriteText(memoryPath, serializeLearningMemory(updated));
30
+ }
31
+
32
+ console.log(
33
+ `[mink] reflect: ${result.beforeTokens} → ${result.afterTokens} tokens` +
34
+ ` | merged: ${result.mergedCount} | trimmed: ${result.trimmedCount}` +
35
+ ` | within budget: ${result.withinBudget}`
36
+ );
37
+
38
+ return result;
39
+ }
@@ -0,0 +1,31 @@
1
+ import { listBackups, restoreBackup } from "../core/backup";
2
+
3
+ export function restore(cwd: string, args: string[]): void {
4
+ const backupName = args[0];
5
+
6
+ if (!backupName) {
7
+ // List available backups
8
+ const backups = listBackups(cwd);
9
+ if (backups.length === 0) {
10
+ console.log("[mink] no backups available");
11
+ return;
12
+ }
13
+
14
+ console.log("[mink] available backups:");
15
+ for (const b of backups) {
16
+ console.log(
17
+ ` ${b.name} (${b.timestamp.toISOString().replace("T", " ").slice(0, 19)}, ${b.fileCount} files)`
18
+ );
19
+ }
20
+ return;
21
+ }
22
+
23
+ try {
24
+ restoreBackup(cwd, backupName);
25
+ console.log(`[mink] restored from: ${backupName}`);
26
+ } catch (err) {
27
+ const msg = err instanceof Error ? err.message : String(err);
28
+ console.error(`[mink] restore failed: ${msg}`);
29
+ process.exit(1);
30
+ }
31
+ }
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { fileIndexPath, configPath } from "../core/paths";
4
+ import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
5
+ import { scanProject, loadConfig, getExcludes } from "../core/scanner";
6
+ import { extractDescription } from "../core/description";
7
+ import { estimateTokens } from "../core/token-estimate";
8
+ import {
9
+ createEmptyIndex,
10
+ isFileIndex,
11
+ upsertEntry,
12
+ checkStaleness,
13
+ } from "../core/index-store";
14
+ import type { FileIndex, FileIndexEntry } from "../types/file-index";
15
+
16
+ function loadExistingIndex(indexPath: string): FileIndex {
17
+ const raw = safeReadJson(indexPath);
18
+ if (isFileIndex(raw)) return raw;
19
+ if (raw !== null) {
20
+ console.error("[mink] file-index.json is corrupt — starting fresh");
21
+ }
22
+ return createEmptyIndex();
23
+ }
24
+
25
+ export function scan(cwd: string, options: { check: boolean }): void {
26
+ const idxPath = fileIndexPath(cwd);
27
+ const cfgPath = configPath(cwd);
28
+ const config = loadConfig(cfgPath);
29
+ const excludes = getExcludes(config);
30
+ const maxFiles = config.maxFiles ?? 500;
31
+
32
+ if (options.check) {
33
+ const existing = safeReadJson(idxPath);
34
+ if (!isFileIndex(existing)) {
35
+ console.error("[mink] no index found — run mink scan first");
36
+ process.exit(1);
37
+ }
38
+
39
+ const scanned = scanProject(cwd, excludes, maxFiles);
40
+ const scannedPaths = scanned.map((f) => f.relativePath);
41
+ const report = checkStaleness(existing, scannedPaths);
42
+
43
+ if (!report.isStale) {
44
+ console.log("[mink] index is up to date");
45
+ return;
46
+ }
47
+
48
+ if (report.missingFromIndex.length > 0) {
49
+ console.log(`Missing from index (${report.missingFromIndex.length}):`);
50
+ for (const f of report.missingFromIndex) {
51
+ console.log(` + ${f}`);
52
+ }
53
+ }
54
+ if (report.orphanedEntries.length > 0) {
55
+ console.log(`Orphaned entries (${report.orphanedEntries.length}):`);
56
+ for (const f of report.orphanedEntries) {
57
+ console.log(` - ${f}`);
58
+ }
59
+ }
60
+ process.exit(1);
61
+ }
62
+
63
+ // Full scan
64
+ const start = Date.now();
65
+ const index = loadExistingIndex(idxPath);
66
+
67
+ const scanned = scanProject(cwd, excludes, maxFiles);
68
+
69
+ // Build new entries, preserving lifetime counters
70
+ const newIndex = createEmptyIndex();
71
+ newIndex.header.lifetimeHits = index.header.lifetimeHits;
72
+ newIndex.header.lifetimeMisses = index.header.lifetimeMisses;
73
+
74
+ for (const file of scanned) {
75
+ const fullPath = join(cwd, file.relativePath);
76
+ let content: string;
77
+ try {
78
+ content = readFileSync(fullPath, "utf-8");
79
+ } catch {
80
+ continue; // Skip unreadable files
81
+ }
82
+
83
+ const entry: FileIndexEntry = {
84
+ filePath: file.relativePath,
85
+ description: extractDescription(file.relativePath, content),
86
+ estimatedTokens: estimateTokens(content, file.relativePath),
87
+ lastModified: new Date(file.mtimeMs).toISOString(),
88
+ lastIndexed: new Date().toISOString(),
89
+ };
90
+ upsertEntry(newIndex, entry);
91
+ }
92
+
93
+ newIndex.header.lastScanTimestamp = new Date().toISOString();
94
+
95
+ atomicWriteJson(idxPath, newIndex);
96
+
97
+ const elapsed = Date.now() - start;
98
+ console.log(
99
+ `[mink] indexed ${newIndex.header.totalFiles} files in ${elapsed}ms`
100
+ );
101
+ }
@@ -0,0 +1,21 @@
1
+ import { mkdirSync } from "fs";
2
+ import { createSessionState } from "../core/session";
3
+ import { projectDir, sessionPath, actionLogPath } from "../core/paths";
4
+ import { atomicWriteJson } from "../core/fs-utils";
5
+ import { createActionLogWriter } from "../core/action-log";
6
+
7
+ export function sessionStart(cwd: string): void {
8
+ const dir = projectDir(cwd);
9
+ mkdirSync(dir, { recursive: true });
10
+
11
+ const state = createSessionState();
12
+ atomicWriteJson(sessionPath(cwd), state);
13
+
14
+ // Append session header to action log
15
+ try {
16
+ const logWriter = createActionLogWriter(actionLogPath(cwd));
17
+ logWriter.appendSessionHeader(state.startTimestamp);
18
+ } catch {
19
+ // Never crash hooks
20
+ }
21
+ }
@@ -0,0 +1,115 @@
1
+ import { statSync, existsSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { safeReadJson, atomicWriteJson } from "../core/fs-utils";
4
+ import { isSessionState, buildSummary } from "../core/session";
5
+ import { reflect } from "./reflect";
6
+ import { createLedgerFinalizer } from "../core/token-ledger";
7
+ import { loadBugMemory, hasBugForFileInSession } from "../core/bug-memory";
8
+ import { createActionLogWriter, consolidateLog } from "../core/action-log";
9
+ import type { SessionState, SessionFinalizer } from "../types/session";
10
+ import type { ProjectConfig } from "../types/file-index";
11
+
12
+ function hasActivity(state: SessionState): boolean {
13
+ return Object.keys(state.reads).length > 0 || state.writes.length > 0;
14
+ }
15
+
16
+ function getEditCounts(state: SessionState): Record<string, number> {
17
+ const counts: Record<string, number> = {};
18
+ for (const write of state.writes) {
19
+ counts[write.filePath] = (counts[write.filePath] || 0) + 1;
20
+ }
21
+ return counts;
22
+ }
23
+
24
+ function isLearningMemoryStale(memoryPath: string): boolean {
25
+ try {
26
+ const stat = statSync(memoryPath);
27
+ const ageMs = Date.now() - stat.mtimeMs;
28
+ const twentyFourHours = 24 * 60 * 60 * 1000;
29
+ return ageMs > twentyFourHours;
30
+ } catch {
31
+ // File doesn't exist yet — not stale, just absent
32
+ return false;
33
+ }
34
+ }
35
+
36
+ export function sessionStop(
37
+ sessionFile: string,
38
+ finalizer?: SessionFinalizer,
39
+ onReminder: (msg: string) => void = (msg) => console.error(msg)
40
+ ): void {
41
+ const raw = safeReadJson(sessionFile);
42
+ if (!isSessionState(raw)) {
43
+ if (raw !== null) {
44
+ console.error("[mink] session.json is corrupt — skipping finalization");
45
+ }
46
+ return;
47
+ }
48
+
49
+ const state: SessionState = raw;
50
+ state.stopCount++;
51
+
52
+ const projDir = dirname(sessionFile);
53
+ const effectiveFinalizer = finalizer ?? createLedgerFinalizer(projDir);
54
+
55
+ if (hasActivity(state)) {
56
+ const summary = buildSummary(state);
57
+
58
+ if (state.stopCount === 1) {
59
+ effectiveFinalizer.appendSession(summary);
60
+ } else {
61
+ effectiveFinalizer.updateSession(summary);
62
+ }
63
+
64
+ // Append session end to action log and run consolidation
65
+ try {
66
+ const logPath = join(projDir, "action-log.md");
67
+ const logWriter = createActionLogWriter(logPath);
68
+ logWriter.appendSessionEnd(summary);
69
+
70
+ const cfgRaw = safeReadJson(join(projDir, "config.json")) as ProjectConfig | null;
71
+ consolidateLog(logPath, {
72
+ maxEntries: cfgRaw?.actionLogMaxEntries ?? 200,
73
+ retentionDays: cfgRaw?.actionLogRetentionDays ?? 7,
74
+ });
75
+ } catch {
76
+ // Never crash
77
+ }
78
+ }
79
+
80
+ // Check for files edited 3+ times without a corresponding bug entry
81
+ const editCounts = getEditCounts(state);
82
+ const bugMemoryFile = join(projDir, "bug-memory.json");
83
+ const bugMemory = loadBugMemory(bugMemoryFile);
84
+
85
+ for (const [filePath, count] of Object.entries(editCounts)) {
86
+ if (count >= 3) {
87
+ const hasBug = hasBugForFileInSession(
88
+ bugMemory,
89
+ filePath,
90
+ state.startTimestamp
91
+ );
92
+ if (!hasBug) {
93
+ onReminder(
94
+ `[mink] ${filePath} was edited ${count} times — consider logging a bug`
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ // Run reflection to merge duplicates and prune oversized memory
101
+ const memoryPath = join(projDir, "learning-memory.md");
102
+ const cfgPath = join(projDir, "config.json");
103
+ if (existsSync(memoryPath)) {
104
+ reflect(projDir, memoryPath, cfgPath);
105
+ }
106
+
107
+ // Check if learning memory is stale (>24h since last update)
108
+ if (isLearningMemoryStale(memoryPath)) {
109
+ onReminder(
110
+ "[mink] learning memory hasn't been updated in 24+ hours — consider reviewing it"
111
+ );
112
+ }
113
+
114
+ atomicWriteJson(sessionFile, state);
115
+ }
@@ -0,0 +1,152 @@
1
+ import { existsSync, readFileSync, statSync } from "fs";
2
+ import {
3
+ sessionPath,
4
+ fileIndexPath,
5
+ configPath,
6
+ learningMemoryPath,
7
+ tokenLedgerPath,
8
+ bugMemoryPath,
9
+ actionLogPath,
10
+ } from "../core/paths";
11
+ import { safeReadJson } from "../core/fs-utils";
12
+ import { isFileIndex } from "../core/index-store";
13
+ import { loadLedger } from "../core/token-ledger";
14
+ import { parseLearningMemory, totalEntryCount } from "../core/learning-memory";
15
+ import { loadBugMemory } from "../core/bug-memory";
16
+ import { getDaemonStatus } from "../core/daemon";
17
+
18
+ interface FileCheck {
19
+ name: string;
20
+ path: string;
21
+ status: "ok" | "missing" | "corrupt";
22
+ }
23
+
24
+ function checkJsonFile(name: string, filePath: string, validator?: (v: unknown) => boolean): FileCheck {
25
+ if (!existsSync(filePath)) return { name, path: filePath, status: "missing" };
26
+ const data = safeReadJson(filePath);
27
+ if (data === null) return { name, path: filePath, status: "corrupt" };
28
+ if (validator && !validator(data)) return { name, path: filePath, status: "corrupt" };
29
+ return { name, path: filePath, status: "ok" };
30
+ }
31
+
32
+ function checkTextFile(name: string, filePath: string): FileCheck {
33
+ if (!existsSync(filePath)) return { name, path: filePath, status: "missing" };
34
+ try {
35
+ readFileSync(filePath, "utf-8");
36
+ return { name, path: filePath, status: "ok" };
37
+ } catch {
38
+ return { name, path: filePath, status: "corrupt" };
39
+ }
40
+ }
41
+
42
+ export function status(cwd: string): void {
43
+ console.log("[mink] project status");
44
+ console.log();
45
+
46
+ // Section 1: State directory integrity
47
+ const checks: FileCheck[] = [
48
+ checkJsonFile("session.json", sessionPath(cwd)),
49
+ checkJsonFile("file-index.json", fileIndexPath(cwd), isFileIndex),
50
+ checkJsonFile("config.json", configPath(cwd)),
51
+ checkTextFile("learning-memory.md", learningMemoryPath(cwd)),
52
+ checkJsonFile("token-ledger.json", tokenLedgerPath(cwd)),
53
+ checkJsonFile("bug-memory.json", bugMemoryPath(cwd)),
54
+ checkTextFile("action-log.md", actionLogPath(cwd)),
55
+ ];
56
+
57
+ console.log(" State files:");
58
+ for (const check of checks) {
59
+ const icon = check.status === "ok" ? "ok" : check.status === "missing" ? "missing" : "corrupt";
60
+ console.log(` ${check.name}: ${icon}`);
61
+ }
62
+
63
+ const corrupt = checks.filter((c) => c.status === "corrupt");
64
+ if (corrupt.length > 0) {
65
+ console.log();
66
+ console.log(" Warning: corrupted files detected. Consider running: mink restore");
67
+ }
68
+ console.log();
69
+
70
+ // Section 2: File index
71
+ try {
72
+ const raw = safeReadJson(fileIndexPath(cwd));
73
+ if (raw && isFileIndex(raw)) {
74
+ const h = raw.header;
75
+ const total = h.lifetimeHits + h.lifetimeMisses;
76
+ const ratio = total > 0 ? ((h.lifetimeHits / total) * 100).toFixed(1) : "N/A";
77
+ console.log(" File index:");
78
+ console.log(` Files: ${h.totalFiles}`);
79
+ console.log(` Last scan: ${h.lastScanTimestamp || "never"}`);
80
+ console.log(` Hit/miss ratio: ${ratio}${total > 0 ? "%" : ""} (${h.lifetimeHits} hits, ${h.lifetimeMisses} misses)`);
81
+ } else {
82
+ console.log(" File index: not available");
83
+ }
84
+ } catch {
85
+ console.log(" File index: error reading");
86
+ }
87
+ console.log();
88
+
89
+ // Section 3: Token ledger
90
+ try {
91
+ const ledger = loadLedger(tokenLedgerPath(cwd));
92
+ const lt = ledger.lifetime;
93
+ console.log(" Token ledger:");
94
+ console.log(` Sessions: ${lt.totalSessions}`);
95
+ console.log(` Total tokens: ${lt.totalTokens.toLocaleString()}`);
96
+ console.log(` Reads: ${lt.totalReads} Writes: ${lt.totalWrites}`);
97
+ console.log(` Estimated savings: ${lt.totalEstimatedSavings.toLocaleString()} tokens`);
98
+ } catch {
99
+ console.log(" Token ledger: error reading");
100
+ }
101
+ console.log();
102
+
103
+ // Section 4: Learning memory
104
+ try {
105
+ const memPath = learningMemoryPath(cwd);
106
+ if (existsSync(memPath)) {
107
+ const content = readFileSync(memPath, "utf-8");
108
+ const mem = parseLearningMemory(content);
109
+ const total = totalEntryCount(mem);
110
+ const mtime = statSync(memPath).mtime;
111
+ console.log(" Learning memory:");
112
+ console.log(` User Preferences: ${mem.sections["User Preferences"].length}`);
113
+ console.log(` Key Learnings: ${mem.sections["Key Learnings"].length}`);
114
+ console.log(` Do-Not-Repeat: ${mem.sections["Do-Not-Repeat"].length}`);
115
+ console.log(` Decision Log: ${mem.sections["Decision Log"].length}`);
116
+ console.log(` Total entries: ${total}`);
117
+ console.log(` Last modified: ${mtime.toISOString()}`);
118
+ } else {
119
+ console.log(" Learning memory: not initialized");
120
+ }
121
+ } catch {
122
+ console.log(" Learning memory: error reading");
123
+ }
124
+ console.log();
125
+
126
+ // Section 5: Bug log
127
+ try {
128
+ const bugs = loadBugMemory(bugMemoryPath(cwd));
129
+ console.log(` Bug log: ${bugs.entries.length} entries`);
130
+ } catch {
131
+ console.log(" Bug log: error reading");
132
+ }
133
+ console.log();
134
+
135
+ // Section 6: Daemon status
136
+ try {
137
+ const daemon = getDaemonStatus(cwd);
138
+ if (daemon.running) {
139
+ const uptimeMs = Date.now() - new Date(daemon.startedAt!).getTime();
140
+ const uptimeMin = Math.floor(uptimeMs / 60_000);
141
+ const uptimeHrs = Math.floor(uptimeMin / 60);
142
+ const uptimeStr = uptimeHrs > 0
143
+ ? `${uptimeHrs}h ${uptimeMin % 60}m`
144
+ : `${uptimeMin}m`;
145
+ console.log(` Daemon: running (PID: ${daemon.pid}, uptime: ${uptimeStr})`);
146
+ } else {
147
+ console.log(" Daemon: stopped");
148
+ }
149
+ } catch {
150
+ console.log(" Daemon: unknown");
151
+ }
152
+ }
@@ -0,0 +1,121 @@
1
+ import { resolve, dirname, basename } from "path";
2
+ import { existsSync } from "fs";
3
+ import { listRegisteredProjects } from "../core/project-registry";
4
+ import { createBackup } from "../core/backup";
5
+ import { projectMetaPath } from "../core/paths";
6
+ import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
7
+ import { buildHooksConfig, detectRuntime, mergeHooksIntoSettings } from "./init";
8
+
9
+ function parseArgs(args: string[]): {
10
+ dryRun: boolean;
11
+ project: string | null;
12
+ list: boolean;
13
+ } {
14
+ let dryRun = false;
15
+ let project: string | null = null;
16
+ let list = false;
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === "--dry-run") dryRun = true;
20
+ else if (args[i] === "--list") list = true;
21
+ else if (args[i] === "--project" && i + 1 < args.length) {
22
+ project = args[++i];
23
+ }
24
+ }
25
+
26
+ return { dryRun, project, list };
27
+ }
28
+
29
+ export async function update(cwd: string, args: string[]): Promise<void> {
30
+ const { dryRun, project, list } = parseArgs(args);
31
+
32
+ const registered = listRegisteredProjects();
33
+
34
+ if (list) {
35
+ if (registered.length === 0) {
36
+ console.log("[mink] no registered projects found");
37
+ console.log(" Run 'mink init' in a project directory to register it.");
38
+ return;
39
+ }
40
+ console.log("[mink] registered projects:");
41
+ console.log(
42
+ " " +
43
+ "ID".padEnd(30) +
44
+ "Name".padEnd(20) +
45
+ "Version".padEnd(12) +
46
+ "Path"
47
+ );
48
+ console.log(" " + "-".repeat(80));
49
+ for (const p of registered) {
50
+ console.log(
51
+ " " +
52
+ p.id.padEnd(30) +
53
+ p.name.padEnd(20) +
54
+ p.version.padEnd(12) +
55
+ p.cwd
56
+ );
57
+ }
58
+ return;
59
+ }
60
+
61
+ let targets = registered;
62
+ if (project) {
63
+ targets = registered.filter(
64
+ (p) => p.name === project || p.id === project
65
+ );
66
+ if (targets.length === 0) {
67
+ console.error(`[mink] project not found: ${project}`);
68
+ console.error(
69
+ " Available: " + registered.map((p) => p.name).join(", ")
70
+ );
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ if (targets.length === 0) {
76
+ console.log("[mink] no registered projects found");
77
+ console.log(" Run 'mink init' in a project directory to register it.");
78
+ return;
79
+ }
80
+
81
+ const runtime = detectRuntime();
82
+ const cliPath = resolve(
83
+ dirname(new URL(import.meta.url).pathname),
84
+ "../cli.ts"
85
+ );
86
+ const newHooks = buildHooksConfig(runtime, cliPath);
87
+
88
+ for (const target of targets) {
89
+ console.log(`[mink] updating: ${target.name} (${target.id})`);
90
+
91
+ if (dryRun) {
92
+ console.log(" [dry-run] would update hooks and project metadata");
93
+ console.log(` [dry-run] would create backup before changes`);
94
+ continue;
95
+ }
96
+
97
+ // Create backup
98
+ const backupName = createBackup(target.cwd);
99
+ console.log(` backup: ${backupName}`);
100
+
101
+ // Update hooks
102
+ const settingsPath = resolve(target.cwd, ".claude", "settings.json");
103
+ mergeHooksIntoSettings(settingsPath, newHooks);
104
+ console.log(" hooks: updated");
105
+
106
+ // Update project meta
107
+ const metaPath = projectMetaPath(target.cwd);
108
+ const existing = safeReadJson(metaPath) as Record<string, unknown> | null;
109
+ atomicWriteJson(metaPath, {
110
+ ...(existing ?? {}),
111
+ cwd: target.cwd,
112
+ name: target.name,
113
+ version: "0.1.0",
114
+ });
115
+ console.log(" metadata: updated");
116
+ }
117
+
118
+ if (!dryRun) {
119
+ console.log(`[mink] ${targets.length} project(s) updated`);
120
+ }
121
+ }