@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.
- package/README.md +347 -0
- package/package.json +32 -0
- package/src/cli.ts +176 -0
- package/src/commands/bug-search.ts +32 -0
- package/src/commands/config.ts +109 -0
- package/src/commands/cron.ts +295 -0
- package/src/commands/daemon.ts +46 -0
- package/src/commands/dashboard.ts +21 -0
- package/src/commands/designqc.ts +160 -0
- package/src/commands/detect-waste.ts +81 -0
- package/src/commands/framework-advisor.ts +52 -0
- package/src/commands/init.ts +159 -0
- package/src/commands/post-read.ts +123 -0
- package/src/commands/post-write.ts +157 -0
- package/src/commands/pre-read.ts +109 -0
- package/src/commands/pre-write.ts +136 -0
- package/src/commands/reflect.ts +39 -0
- package/src/commands/restore.ts +31 -0
- package/src/commands/scan.ts +101 -0
- package/src/commands/session-start.ts +21 -0
- package/src/commands/session-stop.ts +115 -0
- package/src/commands/status.ts +152 -0
- package/src/commands/update.ts +121 -0
- package/src/core/action-log.ts +341 -0
- package/src/core/backup.ts +122 -0
- package/src/core/bug-memory.ts +223 -0
- package/src/core/cron-parser.ts +94 -0
- package/src/core/daemon.ts +152 -0
- package/src/core/dashboard-api.ts +280 -0
- package/src/core/dashboard-server.ts +580 -0
- package/src/core/description.ts +232 -0
- package/src/core/design-eval/capture.ts +269 -0
- package/src/core/design-eval/route-detect.ts +165 -0
- package/src/core/design-eval/server-detect.ts +91 -0
- package/src/core/framework-advisor/catalog.ts +360 -0
- package/src/core/framework-advisor/decision-tree.ts +287 -0
- package/src/core/framework-advisor/generate.ts +132 -0
- package/src/core/framework-advisor/migration-prompts.ts +502 -0
- package/src/core/framework-advisor/validate.ts +137 -0
- package/src/core/fs-utils.ts +30 -0
- package/src/core/global-config.ts +74 -0
- package/src/core/index-store.ts +72 -0
- package/src/core/learning-memory.ts +120 -0
- package/src/core/paths.ts +86 -0
- package/src/core/pattern-engine.ts +108 -0
- package/src/core/project-id.ts +19 -0
- package/src/core/project-registry.ts +64 -0
- package/src/core/reflection.ts +256 -0
- package/src/core/scanner.ts +99 -0
- package/src/core/scheduler.ts +352 -0
- package/src/core/seed.ts +239 -0
- package/src/core/session.ts +128 -0
- package/src/core/stdin.ts +13 -0
- package/src/core/task-registry.ts +202 -0
- package/src/core/token-estimate.ts +36 -0
- package/src/core/token-ledger.ts +185 -0
- package/src/core/waste-detection.ts +214 -0
- package/src/core/write-exclusions.ts +24 -0
- package/src/types/action-log.ts +20 -0
- package/src/types/backup.ts +6 -0
- package/src/types/bug-memory.ts +24 -0
- package/src/types/config.ts +59 -0
- package/src/types/dashboard.ts +104 -0
- package/src/types/design-eval.ts +64 -0
- package/src/types/file-index.ts +38 -0
- package/src/types/framework-advisor.ts +97 -0
- package/src/types/hook-input.ts +27 -0
- package/src/types/learning-memory.ts +36 -0
- package/src/types/scheduler.ts +82 -0
- package/src/types/session.ts +50 -0
- package/src/types/token-ledger.ts +43 -0
- package/src/types/waste-detection.ts +21 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { startDaemon, stopDaemon, getDaemonStatus, removePidFile } from "../core/daemon";
|
|
2
|
+
import { createScheduler, loadManifest, removeFromDeadLetter, saveManifest, createInitialManifest, recoverManifest } from "../core/scheduler";
|
|
3
|
+
import { getBuiltInTasks, getTaskById, executeTask } from "../core/task-registry";
|
|
4
|
+
import { schedulerManifestPath } from "../core/paths";
|
|
5
|
+
|
|
6
|
+
// ── Subcommand: start ───────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function cronStart(cwd: string): void {
|
|
9
|
+
startDaemon(cwd);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Subcommand: stop ────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
async function cronStop(): Promise<void> {
|
|
15
|
+
await stopDaemon();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Subcommand: status ──────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function cronStatus(cwd: string): void {
|
|
21
|
+
const status = getDaemonStatus(cwd);
|
|
22
|
+
|
|
23
|
+
if (!status.running) {
|
|
24
|
+
console.log("[mink] scheduler is not running");
|
|
25
|
+
} else {
|
|
26
|
+
const uptimeMs = Date.now() - new Date(status.startedAt!).getTime();
|
|
27
|
+
const uptimeMin = Math.floor(uptimeMs / 60_000);
|
|
28
|
+
const uptimeHrs = Math.floor(uptimeMin / 60);
|
|
29
|
+
const uptimeStr =
|
|
30
|
+
uptimeHrs > 0
|
|
31
|
+
? `${uptimeHrs}h ${uptimeMin % 60}m`
|
|
32
|
+
: `${uptimeMin}m`;
|
|
33
|
+
|
|
34
|
+
console.log(`[mink] scheduler running (PID: ${status.pid})`);
|
|
35
|
+
console.log(` Started: ${status.startedAt}`);
|
|
36
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
37
|
+
console.log(` Project: ${status.projectCwd}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const manifest = loadManifest(cwd);
|
|
41
|
+
if (manifest) {
|
|
42
|
+
console.log(` Last heartbeat: ${manifest.lastHeartbeat}`);
|
|
43
|
+
console.log(` Dead letter queue: ${manifest.deadLetterQueue.length} task(s)`);
|
|
44
|
+
console.log();
|
|
45
|
+
cronList(cwd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Subcommand: list ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function cronList(cwd: string): void {
|
|
52
|
+
const tasks = getBuiltInTasks();
|
|
53
|
+
const manifest = loadManifest(cwd);
|
|
54
|
+
|
|
55
|
+
console.log("Tasks:");
|
|
56
|
+
console.log(
|
|
57
|
+
" " +
|
|
58
|
+
"ID".padEnd(30) +
|
|
59
|
+
"Schedule".padEnd(18) +
|
|
60
|
+
"Status".padEnd(16) +
|
|
61
|
+
"Last Run"
|
|
62
|
+
);
|
|
63
|
+
console.log(" " + "-".repeat(80));
|
|
64
|
+
|
|
65
|
+
for (const task of tasks) {
|
|
66
|
+
const record = manifest?.tasks.find((r) => r.taskId === task.id);
|
|
67
|
+
const status = record?.status ?? "idle";
|
|
68
|
+
const lastRun = record?.lastRunAt
|
|
69
|
+
? new Date(record.lastRunAt).toISOString().replace("T", " ").slice(0, 19)
|
|
70
|
+
: "never";
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
" " +
|
|
74
|
+
task.id.padEnd(30) +
|
|
75
|
+
task.schedule.padEnd(18) +
|
|
76
|
+
status.padEnd(16) +
|
|
77
|
+
lastRun
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Subcommand: run ─────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async function cronRun(cwd: string, taskId: string | undefined): Promise<void> {
|
|
85
|
+
if (!taskId) {
|
|
86
|
+
console.error("Usage: mink cron run <task-id>");
|
|
87
|
+
console.error(
|
|
88
|
+
"Available tasks: " + getBuiltInTasks().map((t) => t.id).join(", ")
|
|
89
|
+
);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const task = getTaskById(taskId);
|
|
94
|
+
if (!task) {
|
|
95
|
+
console.error(`[mink] unknown task: ${taskId}`);
|
|
96
|
+
console.error(
|
|
97
|
+
"Available tasks: " + getBuiltInTasks().map((t) => t.id).join(", ")
|
|
98
|
+
);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`[mink] running task: ${task.name}`);
|
|
103
|
+
|
|
104
|
+
// Execute directly, with retry logic
|
|
105
|
+
let lastError: Error | null = null;
|
|
106
|
+
for (let attempt = 0; attempt < task.retryPolicy.maxAttempts; attempt++) {
|
|
107
|
+
try {
|
|
108
|
+
await executeTask(taskId, cwd);
|
|
109
|
+
console.log(`[mink] task ${taskId} completed successfully`);
|
|
110
|
+
return;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
113
|
+
console.error(
|
|
114
|
+
`[mink] task ${taskId} failed (attempt ${attempt + 1}/${task.retryPolicy.maxAttempts}): ${lastError.message}`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (attempt + 1 < task.retryPolicy.maxAttempts) {
|
|
118
|
+
const delay = task.retryPolicy.baseDelayMs * Math.pow(2, attempt);
|
|
119
|
+
console.log(`[mink] retrying in ${Math.round(delay / 1000)}s...`);
|
|
120
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// All retries exhausted — dead letter
|
|
126
|
+
console.error(
|
|
127
|
+
`[mink] task ${taskId} failed after ${task.retryPolicy.maxAttempts} attempts`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Update manifest if it exists
|
|
131
|
+
let manifest = loadManifest(cwd);
|
|
132
|
+
if (manifest) {
|
|
133
|
+
const record = manifest.tasks.find((r) => r.taskId === taskId);
|
|
134
|
+
if (record) {
|
|
135
|
+
record.status = "dead-lettered";
|
|
136
|
+
}
|
|
137
|
+
manifest.deadLetterQueue.push({
|
|
138
|
+
taskId,
|
|
139
|
+
deadLetteredAt: new Date().toISOString(),
|
|
140
|
+
failureTimestamps: [new Date().toISOString()],
|
|
141
|
+
errorMessages: [lastError?.message ?? "Unknown error"],
|
|
142
|
+
attemptCount: task.retryPolicy.maxAttempts,
|
|
143
|
+
});
|
|
144
|
+
saveManifest(cwd, manifest);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Subcommand: dead-letter ─────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
async function cronDeadLetter(
|
|
153
|
+
cwd: string,
|
|
154
|
+
args: string[]
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
const action = args[0];
|
|
157
|
+
|
|
158
|
+
if (action === "list") {
|
|
159
|
+
const manifest = loadManifest(cwd);
|
|
160
|
+
if (!manifest || manifest.deadLetterQueue.length === 0) {
|
|
161
|
+
console.log("[mink] dead letter queue is empty");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log("Dead-lettered tasks:");
|
|
166
|
+
for (const entry of manifest.deadLetterQueue) {
|
|
167
|
+
console.log(` ${entry.taskId}`);
|
|
168
|
+
console.log(` Dead-lettered: ${entry.deadLetteredAt}`);
|
|
169
|
+
console.log(` Attempts: ${entry.attemptCount}`);
|
|
170
|
+
console.log(
|
|
171
|
+
` Last error: ${entry.errorMessages[entry.errorMessages.length - 1] ?? "unknown"}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (action === "retry") {
|
|
178
|
+
const taskId = args[1];
|
|
179
|
+
if (!taskId) {
|
|
180
|
+
console.error("Usage: mink cron dead-letter retry <task-id>");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const manifest = loadManifest(cwd);
|
|
185
|
+
if (!manifest) {
|
|
186
|
+
console.error("[mink] no scheduler manifest found");
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const entry = removeFromDeadLetter(manifest, taskId);
|
|
191
|
+
if (!entry) {
|
|
192
|
+
console.error(`[mink] task ${taskId} is not in the dead letter queue`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Reset the task record
|
|
197
|
+
const record = manifest.tasks.find((r) => r.taskId === taskId);
|
|
198
|
+
if (record) {
|
|
199
|
+
record.status = "idle";
|
|
200
|
+
record.currentAttempt = 0;
|
|
201
|
+
}
|
|
202
|
+
saveManifest(cwd, manifest);
|
|
203
|
+
|
|
204
|
+
console.log(`[mink] retrying dead-lettered task: ${taskId}`);
|
|
205
|
+
try {
|
|
206
|
+
await executeTask(taskId, cwd);
|
|
207
|
+
console.log(`[mink] task ${taskId} completed successfully`);
|
|
208
|
+
if (record) {
|
|
209
|
+
record.lastSuccessAt = new Date().toISOString();
|
|
210
|
+
record.lastRunAt = new Date().toISOString();
|
|
211
|
+
record.consecutiveFailures = 0;
|
|
212
|
+
}
|
|
213
|
+
saveManifest(cwd, manifest);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
216
|
+
console.error(`[mink] task ${taskId} failed again: ${errorMsg}`);
|
|
217
|
+
// Re-add to dead letter
|
|
218
|
+
manifest.deadLetterQueue.push({
|
|
219
|
+
taskId,
|
|
220
|
+
deadLetteredAt: new Date().toISOString(),
|
|
221
|
+
failureTimestamps: [...entry.failureTimestamps, new Date().toISOString()],
|
|
222
|
+
errorMessages: [...entry.errorMessages, errorMsg],
|
|
223
|
+
attemptCount: entry.attemptCount + 1,
|
|
224
|
+
});
|
|
225
|
+
if (record) {
|
|
226
|
+
record.status = "dead-lettered";
|
|
227
|
+
}
|
|
228
|
+
saveManifest(cwd, manifest);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.error("Usage: mink cron dead-letter <list|retry> [task-id]");
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Subcommand: __daemon (internal) ─────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
async function cronDaemon(cwd: string): Promise<void> {
|
|
241
|
+
console.log(`[mink] daemon starting for project: ${cwd}`);
|
|
242
|
+
|
|
243
|
+
const scheduler = createScheduler(cwd);
|
|
244
|
+
|
|
245
|
+
// Signal handlers for graceful shutdown
|
|
246
|
+
process.on("SIGTERM", () => {
|
|
247
|
+
console.log("[mink] received SIGTERM");
|
|
248
|
+
scheduler.stop();
|
|
249
|
+
removePidFile();
|
|
250
|
+
process.exit(0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
process.on("SIGINT", () => {
|
|
254
|
+
console.log("[mink] received SIGINT");
|
|
255
|
+
scheduler.stop();
|
|
256
|
+
removePidFile();
|
|
257
|
+
process.exit(0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
scheduler.start();
|
|
261
|
+
|
|
262
|
+
// Keep the process alive
|
|
263
|
+
await new Promise(() => {});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Main Entry Point ────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
export async function cron(cwd: string, args: string[]): Promise<void> {
|
|
269
|
+
const subcommand = args[0];
|
|
270
|
+
|
|
271
|
+
switch (subcommand) {
|
|
272
|
+
case "start":
|
|
273
|
+
return cronStart(cwd);
|
|
274
|
+
case "stop":
|
|
275
|
+
return await cronStop();
|
|
276
|
+
case "status":
|
|
277
|
+
return cronStatus(cwd);
|
|
278
|
+
case "list":
|
|
279
|
+
return cronList(cwd);
|
|
280
|
+
case "run":
|
|
281
|
+
return await cronRun(cwd, args[1]);
|
|
282
|
+
case "dead-letter":
|
|
283
|
+
return await cronDeadLetter(cwd, args.slice(1));
|
|
284
|
+
case "__daemon":
|
|
285
|
+
return await cronDaemon(cwd);
|
|
286
|
+
default:
|
|
287
|
+
console.error(
|
|
288
|
+
`[mink] unknown cron subcommand: ${subcommand ?? "(none)"}`
|
|
289
|
+
);
|
|
290
|
+
console.error(
|
|
291
|
+
"Usage: mink cron <start|stop|status|list|run|dead-letter>"
|
|
292
|
+
);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { startDaemon, stopDaemon } from "../core/daemon";
|
|
3
|
+
import { schedulerLogPath } from "../core/paths";
|
|
4
|
+
|
|
5
|
+
export async function daemon(cwd: string, args: string[]): Promise<void> {
|
|
6
|
+
const subcommand = args[0];
|
|
7
|
+
|
|
8
|
+
switch (subcommand) {
|
|
9
|
+
case "start":
|
|
10
|
+
startDaemon(cwd);
|
|
11
|
+
break;
|
|
12
|
+
|
|
13
|
+
case "stop":
|
|
14
|
+
await stopDaemon();
|
|
15
|
+
break;
|
|
16
|
+
|
|
17
|
+
case "restart":
|
|
18
|
+
await stopDaemon();
|
|
19
|
+
startDaemon(cwd);
|
|
20
|
+
break;
|
|
21
|
+
|
|
22
|
+
case "logs": {
|
|
23
|
+
const logPath = schedulerLogPath();
|
|
24
|
+
if (!existsSync(logPath)) {
|
|
25
|
+
console.log("[mink] no log file found");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(logPath, "utf-8");
|
|
30
|
+
const lines = content.split("\n");
|
|
31
|
+
const tail = lines.slice(-50).join("\n");
|
|
32
|
+
console.log(tail);
|
|
33
|
+
} catch {
|
|
34
|
+
console.error("[mink] error reading log file");
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
default:
|
|
40
|
+
console.error(
|
|
41
|
+
`[mink] unknown daemon subcommand: ${subcommand ?? "(none)"}`
|
|
42
|
+
);
|
|
43
|
+
console.error("Usage: mink daemon <start|stop|restart|logs>");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { projectDir } from "../core/paths";
|
|
3
|
+
|
|
4
|
+
export async function dashboard(cwd: string, args: string[]): Promise<void> {
|
|
5
|
+
if (!existsSync(projectDir(cwd))) {
|
|
6
|
+
console.error("[mink] project not initialized. Run: mink init");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const portArg = args.find((a) => a.startsWith("--port="));
|
|
11
|
+
const port = portArg ? parseInt(portArg.split("=")[1], 10) : 4040;
|
|
12
|
+
const noOpen = args.includes("--no-open");
|
|
13
|
+
|
|
14
|
+
const { startDashboardServer } = await import("../core/dashboard-server");
|
|
15
|
+
const { url } = startDashboardServer(cwd, { port, open: !noOpen });
|
|
16
|
+
|
|
17
|
+
console.log(`[mink] dashboard running at ${url}`);
|
|
18
|
+
console.log("[mink] press Ctrl+C to stop");
|
|
19
|
+
|
|
20
|
+
await new Promise(() => {});
|
|
21
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { designCapturesDir, designReportPath } from "../core/paths";
|
|
2
|
+
import { atomicWriteJson } from "../core/fs-utils";
|
|
3
|
+
import { findRunningServer, detectDevCommand } from "../core/design-eval/server-detect";
|
|
4
|
+
import { detectRoutes } from "../core/design-eval/route-detect";
|
|
5
|
+
import { captureAllRoutes } from "../core/design-eval/capture";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_VIEWPORTS,
|
|
8
|
+
DEFAULT_QUALITY,
|
|
9
|
+
DEFAULT_MAX_SECTIONS,
|
|
10
|
+
} from "../types/design-eval";
|
|
11
|
+
import type { DesignQcOptions, Viewport } from "../types/design-eval";
|
|
12
|
+
|
|
13
|
+
// ── Arg Parsing ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export function parseDesignQcArgs(args: string[]): DesignQcOptions {
|
|
16
|
+
const options: DesignQcOptions = {
|
|
17
|
+
quality: DEFAULT_QUALITY,
|
|
18
|
+
desktopOnly: false,
|
|
19
|
+
maxSections: DEFAULT_MAX_SECTIONS,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const arg = args[i];
|
|
24
|
+
|
|
25
|
+
if (arg === "--url" && i + 1 < args.length) {
|
|
26
|
+
options.url = args[++i];
|
|
27
|
+
} else if (arg === "--routes") {
|
|
28
|
+
const routes: string[] = [];
|
|
29
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
30
|
+
routes.push(args[++i]);
|
|
31
|
+
}
|
|
32
|
+
if (routes.length > 0) options.routes = routes;
|
|
33
|
+
} else if (arg === "--quality" && i + 1 < args.length) {
|
|
34
|
+
const q = parseInt(args[++i], 10);
|
|
35
|
+
if (!isNaN(q)) options.quality = Math.max(1, Math.min(100, q));
|
|
36
|
+
} else if (arg === "--max-sections" && i + 1 < args.length) {
|
|
37
|
+
const m = parseInt(args[++i], 10);
|
|
38
|
+
if (!isNaN(m) && m > 0) options.maxSections = m;
|
|
39
|
+
} else if (arg === "--desktop-only") {
|
|
40
|
+
options.desktopOnly = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return options;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Command ───────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export async function designqc(cwd: string, args: string[]): Promise<void> {
|
|
50
|
+
const options = parseDesignQcArgs(args);
|
|
51
|
+
|
|
52
|
+
// ── Resolve server URL ────────────────────────────────────────────────
|
|
53
|
+
let serverUrl: string;
|
|
54
|
+
|
|
55
|
+
if (options.url) {
|
|
56
|
+
// Extract base URL from --url flag
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(options.url);
|
|
59
|
+
serverUrl = `${parsed.protocol}//${parsed.host}`;
|
|
60
|
+
|
|
61
|
+
// If --url has a path and no --routes, capture that single path
|
|
62
|
+
if (!options.routes && parsed.pathname !== "/") {
|
|
63
|
+
options.routes = [parsed.pathname];
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
console.error(`[mink] Invalid URL: ${options.url}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
console.log("[mink] Probing for running dev server...");
|
|
71
|
+
const found = await findRunningServer();
|
|
72
|
+
|
|
73
|
+
if (!found) {
|
|
74
|
+
const devCmd = detectDevCommand(cwd);
|
|
75
|
+
if (devCmd) {
|
|
76
|
+
console.error(
|
|
77
|
+
`[mink] No dev server detected. Start one with: ${devCmd}`
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
console.error(
|
|
81
|
+
"[mink] No dev server detected and no start command found in package.json."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
serverUrl = found;
|
|
88
|
+
console.log(`[mink] Found dev server at ${serverUrl}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Resolve routes ────────────────────────────────────────────────────
|
|
92
|
+
let routes: string[];
|
|
93
|
+
|
|
94
|
+
if (options.routes) {
|
|
95
|
+
routes = options.routes;
|
|
96
|
+
console.log(`[mink] Using specified routes: ${routes.join(", ")}`);
|
|
97
|
+
} else {
|
|
98
|
+
routes = detectRoutes(cwd);
|
|
99
|
+
console.log(`[mink] Detected ${routes.length} route(s): ${routes.join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Resolve viewports ─────────────────────────────────────────────────
|
|
103
|
+
const viewports: Viewport[] = options.desktopOnly
|
|
104
|
+
? [DEFAULT_VIEWPORTS[0]]
|
|
105
|
+
: DEFAULT_VIEWPORTS;
|
|
106
|
+
|
|
107
|
+
console.log(
|
|
108
|
+
`[mink] Viewports: ${viewports.map((v) => `${v.name} (${v.width}×${v.height})`).join(", ")}`
|
|
109
|
+
);
|
|
110
|
+
console.log(`[mink] Quality: ${options.quality}, Max sections: ${options.maxSections}`);
|
|
111
|
+
|
|
112
|
+
// ── Capture ───────────────────────────────────────────────────────────
|
|
113
|
+
const outputDir = designCapturesDir(cwd);
|
|
114
|
+
|
|
115
|
+
console.log("[mink] Starting capture...");
|
|
116
|
+
const report = await captureAllRoutes(
|
|
117
|
+
routes,
|
|
118
|
+
serverUrl,
|
|
119
|
+
viewports,
|
|
120
|
+
options,
|
|
121
|
+
outputDir
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// ── Save report ───────────────────────────────────────────────────────
|
|
125
|
+
atomicWriteJson(designReportPath(cwd), report);
|
|
126
|
+
|
|
127
|
+
// ── Summary ───────────────────────────────────────────────────────────
|
|
128
|
+
const totalSize = report.captures.reduce((sum, c) => sum + c.fileSize, 0);
|
|
129
|
+
const sizeKb = (totalSize / 1024).toFixed(1);
|
|
130
|
+
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(`[mink] Capture complete:`);
|
|
133
|
+
console.log(` Screenshots: ${report.captures.length}`);
|
|
134
|
+
console.log(` Total size: ${sizeKb} KB`);
|
|
135
|
+
console.log(` Output dir: ${outputDir}`);
|
|
136
|
+
console.log(` Report: ${designReportPath(cwd)}`);
|
|
137
|
+
|
|
138
|
+
if (report.errors.length > 0) {
|
|
139
|
+
console.log(` Errors: ${report.errors.length}`);
|
|
140
|
+
for (const err of report.errors) {
|
|
141
|
+
console.log(` • ${err.route} (${err.viewport}): ${err.error}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Note any non-200 status codes
|
|
146
|
+
const errorPages = report.captures.filter(
|
|
147
|
+
(c) => c.statusCode >= 400
|
|
148
|
+
);
|
|
149
|
+
if (errorPages.length > 0) {
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log(`[mink] Warning: ${errorPages.length} page(s) returned error status codes:`);
|
|
152
|
+
const seen = new Set<string>();
|
|
153
|
+
for (const c of errorPages) {
|
|
154
|
+
const key = `${c.route}:${c.statusCode}`;
|
|
155
|
+
if (seen.has(key)) continue;
|
|
156
|
+
seen.add(key);
|
|
157
|
+
console.log(` • ${c.route} → ${c.statusCode}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { statSync } from "fs";
|
|
2
|
+
import {
|
|
3
|
+
tokenLedgerPath,
|
|
4
|
+
fileIndexPath,
|
|
5
|
+
actionLogPath,
|
|
6
|
+
learningMemoryPath,
|
|
7
|
+
} from "../core/paths";
|
|
8
|
+
import { createEmptyLedger, isTokenLedger, saveLedger } from "../core/token-ledger";
|
|
9
|
+
import { isFileIndex, createEmptyIndex } from "../core/index-store";
|
|
10
|
+
import { safeReadLog } from "../core/action-log";
|
|
11
|
+
import { safeReadJson } from "../core/fs-utils";
|
|
12
|
+
import { runDetection } from "../core/waste-detection";
|
|
13
|
+
import type { TokenLedger } from "../types/token-ledger";
|
|
14
|
+
import type { FileIndex } from "../types/file-index";
|
|
15
|
+
|
|
16
|
+
export function detectWaste(cwd: string): void {
|
|
17
|
+
const ledgerPath = tokenLedgerPath(cwd);
|
|
18
|
+
const idxPath = fileIndexPath(cwd);
|
|
19
|
+
const logPath = actionLogPath(cwd);
|
|
20
|
+
const lmPath = learningMemoryPath(cwd);
|
|
21
|
+
|
|
22
|
+
// Load and validate ledger — distinguish empty vs corrupted
|
|
23
|
+
const rawLedger = safeReadJson(ledgerPath);
|
|
24
|
+
let ledger: TokenLedger;
|
|
25
|
+
|
|
26
|
+
if (rawLedger === null) {
|
|
27
|
+
ledger = createEmptyLedger();
|
|
28
|
+
} else if (!isTokenLedger(rawLedger)) {
|
|
29
|
+
console.warn("[mink] Warning: corrupt token ledger, skipping waste detection");
|
|
30
|
+
return;
|
|
31
|
+
} else {
|
|
32
|
+
ledger = rawLedger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Load file index
|
|
36
|
+
const rawIndex = safeReadJson(idxPath);
|
|
37
|
+
let fileIndex: FileIndex;
|
|
38
|
+
if (rawIndex !== null && isFileIndex(rawIndex)) {
|
|
39
|
+
fileIndex = rawIndex;
|
|
40
|
+
} else {
|
|
41
|
+
fileIndex = createEmptyIndex();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Load action log content
|
|
45
|
+
const actionLogContent = safeReadLog(logPath);
|
|
46
|
+
|
|
47
|
+
// Get learning memory mtime
|
|
48
|
+
let learningMemoryMtimeMs: number | null = null;
|
|
49
|
+
try {
|
|
50
|
+
learningMemoryMtimeMs = statSync(lmPath).mtimeMs;
|
|
51
|
+
} catch {
|
|
52
|
+
// File missing — will be flagged as stale
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Run detection
|
|
56
|
+
const flags = runDetection(
|
|
57
|
+
ledger,
|
|
58
|
+
fileIndex.entries,
|
|
59
|
+
fileIndex.header,
|
|
60
|
+
actionLogContent,
|
|
61
|
+
learningMemoryMtimeMs
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Store flags in ledger (replaces previous)
|
|
65
|
+
ledger.wasteFlags = flags;
|
|
66
|
+
saveLedger(ledgerPath, ledger);
|
|
67
|
+
|
|
68
|
+
// Output summary
|
|
69
|
+
if (flags.length === 0) {
|
|
70
|
+
console.log("[mink] Waste detection: no issues found.");
|
|
71
|
+
} else {
|
|
72
|
+
console.log(`[mink] Waste detection: ${flags.length} issue(s) found.`);
|
|
73
|
+
for (const flag of flags) {
|
|
74
|
+
console.log(` - [${flag.pattern}] ${flag.description}`);
|
|
75
|
+
if (flag.estimatedTokensWasted > 0) {
|
|
76
|
+
console.log(` Estimated waste: ~${flag.estimatedTokensWasted} tokens`);
|
|
77
|
+
}
|
|
78
|
+
console.log(` Suggestion: ${flag.suggestion}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
frameworkAdvisorPath,
|
|
3
|
+
frameworkAdvisorJsonPath,
|
|
4
|
+
} from "../core/paths";
|
|
5
|
+
import { atomicWriteJson, atomicWriteText } from "../core/fs-utils";
|
|
6
|
+
import { buildKnowledge, generateKnowledgeMarkdown } from "../core/framework-advisor/generate";
|
|
7
|
+
import { validateKnowledge } from "../core/framework-advisor/validate";
|
|
8
|
+
|
|
9
|
+
export async function frameworkAdvisor(
|
|
10
|
+
cwd: string,
|
|
11
|
+
args: string[]
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const isValidate = args.includes("--validate");
|
|
14
|
+
const isJson = args.includes("--json");
|
|
15
|
+
|
|
16
|
+
const knowledge = buildKnowledge();
|
|
17
|
+
|
|
18
|
+
if (isValidate) {
|
|
19
|
+
const result = validateKnowledge(knowledge);
|
|
20
|
+
if (result.valid) {
|
|
21
|
+
console.log("[mink] Framework advisor knowledge is valid.");
|
|
22
|
+
console.log(
|
|
23
|
+
` ${knowledge.frameworks.length} frameworks, ${knowledge.decisionTree.length} decision nodes, ${knowledge.migrationPrompts.length} migration prompts`
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
console.error("[mink] Framework advisor validation failed:");
|
|
27
|
+
for (const err of result.errors) {
|
|
28
|
+
console.error(` - ${err}`);
|
|
29
|
+
}
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isJson) {
|
|
36
|
+
console.log(JSON.stringify(knowledge, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Default: generate both files
|
|
41
|
+
const markdown = generateKnowledgeMarkdown(knowledge);
|
|
42
|
+
|
|
43
|
+
atomicWriteText(frameworkAdvisorPath(cwd), markdown);
|
|
44
|
+
atomicWriteJson(frameworkAdvisorJsonPath(cwd), knowledge);
|
|
45
|
+
|
|
46
|
+
console.log("[mink] Framework advisor knowledge generated:");
|
|
47
|
+
console.log(` Markdown: ${frameworkAdvisorPath(cwd)}`);
|
|
48
|
+
console.log(` JSON: ${frameworkAdvisorJsonPath(cwd)}`);
|
|
49
|
+
console.log(
|
|
50
|
+
` ${knowledge.frameworks.length} frameworks, ${knowledge.decisionTree.length} decision nodes, ${knowledge.migrationPrompts.length} migration prompts`
|
|
51
|
+
);
|
|
52
|
+
}
|