mustflow 2.22.17 → 2.22.46
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/dist/cli/commands/dashboard.js +51 -4
- package/dist/cli/commands/explain.js +3 -2
- package/dist/cli/commands/help.js +0 -1
- package/dist/cli/commands/run.js +41 -4
- package/dist/cli/i18n/en.js +2 -0
- package/dist/cli/i18n/es.js +2 -0
- package/dist/cli/i18n/fr.js +2 -0
- package/dist/cli/i18n/hi.js +2 -0
- package/dist/cli/i18n/ko.js +2 -0
- package/dist/cli/i18n/zh.js +2 -0
- package/dist/cli/lib/cli-output.js +1 -1
- package/dist/cli/lib/dashboard-html/client-script.js +9 -0
- package/dist/cli/lib/dashboard-html/styles.js +48 -1
- package/dist/cli/lib/doc-review-ledger.js +1 -1
- package/dist/cli/lib/local-index/index.js +324 -298
- package/dist/cli/lib/repo-map.js +19 -5
- package/dist/cli/lib/validation/index.js +6 -2
- package/dist/core/active-run-locks.js +36 -8
- package/dist/core/atomic-state-write.js +5 -20
- package/dist/core/change-verification.js +18 -2
- package/dist/core/contract-lint.js +3 -3
- package/dist/core/repeated-failure.js +1 -1
- package/dist/core/run-write-drift.js +30 -17
- package/dist/core/safe-filesystem.js +54 -5
- package/dist/core/skill-route-explanation.js +2 -1
- package/dist/core/source-anchors.js +7 -3
- package/dist/core/validation-ratchet.js +61 -18
- package/dist/core/verification-decision-graph.js +8 -1
- package/package.json +1 -1
- package/templates/default/i18n.toml +139 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +24 -1
- package/templates/default/locales/en/.mustflow/skills/api-contract-change/SKILL.md +212 -0
- package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +184 -0
- package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +194 -0
- package/templates/default/locales/en/.mustflow/skills/config-env-change/SKILL.md +189 -0
- package/templates/default/locales/en/.mustflow/skills/css-code-change/SKILL.md +199 -0
- package/templates/default/locales/en/.mustflow/skills/dart-code-change/SKILL.md +179 -0
- package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +178 -0
- package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +151 -0
- package/templates/default/locales/en/.mustflow/skills/elysia-code-change/SKILL.md +115 -0
- package/templates/default/locales/en/.mustflow/skills/file-path-cross-platform-change/SKILL.md +147 -0
- package/templates/default/locales/en/.mustflow/skills/flutter-code-change/SKILL.md +116 -0
- package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +156 -0
- package/templates/default/locales/en/.mustflow/skills/hono-code-change/SKILL.md +117 -0
- package/templates/default/locales/en/.mustflow/skills/html-code-change/SKILL.md +173 -0
- package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +149 -0
- package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +154 -0
- package/templates/default/locales/en/.mustflow/skills/release-publish-change/SKILL.md +172 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +138 -0
- package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +154 -0
- package/templates/default/locales/en/.mustflow/skills/svelte-code-change/SKILL.md +186 -0
- package/templates/default/locales/en/.mustflow/skills/tailwind-code-change/SKILL.md +164 -0
- package/templates/default/locales/en/.mustflow/skills/tauri-code-change/SKILL.md +185 -0
- package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +184 -0
- package/templates/default/locales/en/.mustflow/skills/unocss-code-change/SKILL.md +186 -0
- package/templates/default/manifest.toml +158 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -306,8 +306,36 @@ function sendText(response, statusCode, value) {
|
|
|
306
306
|
function sendBadRequest(response) {
|
|
307
307
|
sendText(response, 400, 'Bad request');
|
|
308
308
|
}
|
|
309
|
+
function isDashboardBadRequestError(error, message) {
|
|
310
|
+
return (error instanceof SyntaxError ||
|
|
311
|
+
message === 'Request body is too large.' ||
|
|
312
|
+
message === 'Invalid review status.' ||
|
|
313
|
+
message === 'Request body must be a JSON object.' ||
|
|
314
|
+
message === 'Request body must include an updates array.' ||
|
|
315
|
+
message === 'Each update must be a JSON object.' ||
|
|
316
|
+
message === 'Each update must include an id.' ||
|
|
317
|
+
message === 'Bulk documentation review updates require a separate confirmed flow.' ||
|
|
318
|
+
message.startsWith('Unknown dashboard preference: ') ||
|
|
319
|
+
message.endsWith(' is locked in the dashboard.') ||
|
|
320
|
+
message.endsWith(' is required.') ||
|
|
321
|
+
message.endsWith(' must be a boolean.') ||
|
|
322
|
+
message.endsWith(' must be an integer.') ||
|
|
323
|
+
message.endsWith(' must be a string.') ||
|
|
324
|
+
message.endsWith(' must not be empty.') ||
|
|
325
|
+
/^.+ must be at (?:least|most) \d+\.$/u.test(message) ||
|
|
326
|
+
/^.+ must be one of: .+\.$/u.test(message) ||
|
|
327
|
+
message.startsWith('status must be ') ||
|
|
328
|
+
message.startsWith('reviewerKind must be '));
|
|
329
|
+
}
|
|
309
330
|
function isAuthorized(request, token) {
|
|
310
|
-
|
|
331
|
+
const rawToken = request.headers['x-mustflow-dashboard-token'];
|
|
332
|
+
const candidate = Array.isArray(rawToken) ? rawToken[0] : rawToken;
|
|
333
|
+
if (typeof candidate !== 'string') {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const expected = Buffer.from(token);
|
|
337
|
+
const actual = Buffer.from(candidate);
|
|
338
|
+
return expected.byteLength === actual.byteLength && timingSafeEqual(actual, expected);
|
|
311
339
|
}
|
|
312
340
|
async function readRequestJson(request) {
|
|
313
341
|
const chunks = [];
|
|
@@ -841,19 +869,38 @@ export async function runDashboard(args, reporter, lang = 'en') {
|
|
|
841
869
|
}
|
|
842
870
|
sendText(response, 404, 'Not found');
|
|
843
871
|
}
|
|
844
|
-
catch {
|
|
845
|
-
|
|
872
|
+
catch (error) {
|
|
873
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
874
|
+
if (isDashboardBadRequestError(error, message)) {
|
|
875
|
+
sendBadRequest(response);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
reporter.stderr(message);
|
|
879
|
+
sendText(response, 500, 'Internal server error');
|
|
846
880
|
}
|
|
847
881
|
});
|
|
882
|
+
server.headersTimeout = 10_000;
|
|
883
|
+
server.requestTimeout = 30_000;
|
|
884
|
+
server.keepAliveTimeout = 1_000;
|
|
848
885
|
return new Promise((resolve) => {
|
|
849
886
|
let resolved = false;
|
|
887
|
+
const sockets = new Set();
|
|
850
888
|
const close = () => {
|
|
851
889
|
if (resolved) {
|
|
852
890
|
return;
|
|
853
891
|
}
|
|
854
892
|
resolved = true;
|
|
855
893
|
server.close(() => resolve(0));
|
|
894
|
+
for (const socket of sockets) {
|
|
895
|
+
socket.destroy();
|
|
896
|
+
}
|
|
856
897
|
};
|
|
898
|
+
server.on('connection', (socket) => {
|
|
899
|
+
sockets.add(socket);
|
|
900
|
+
socket.on('close', () => {
|
|
901
|
+
sockets.delete(socket);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
857
904
|
server.on('error', (error) => {
|
|
858
905
|
if (!resolved) {
|
|
859
906
|
resolved = true;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
4
4
|
import { t } from '../lib/i18n.js';
|
|
5
|
+
import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile } from '../lib/mustflow-read.js';
|
|
5
6
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
6
7
|
import { explainAssetOptimization, explainCommandIntent, } from '../../core/command-explanation.js';
|
|
7
8
|
import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
|
|
@@ -225,7 +226,7 @@ function getLatestFailureExplainOutput(projectRoot) {
|
|
|
225
226
|
}
|
|
226
227
|
let parsed;
|
|
227
228
|
try {
|
|
228
|
-
parsed = JSON.parse(
|
|
229
|
+
parsed = JSON.parse(readMustflowTextFile(projectRoot, LATEST_RUN_RECEIPT_RELATIVE_PATH, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
|
|
229
230
|
}
|
|
230
231
|
catch {
|
|
231
232
|
return {
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -117,6 +117,31 @@ function renderActiveLockConflictMessage(intentName, conflicts, lang) {
|
|
|
117
117
|
: t(lang, 'run.error.activeLockConflictUnknown');
|
|
118
118
|
return t(lang, 'run.error.activeLockConflict', { intent: intentName, detail });
|
|
119
119
|
}
|
|
120
|
+
function createRunProgressReporter(input) {
|
|
121
|
+
if (!input.enabled) {
|
|
122
|
+
return () => undefined;
|
|
123
|
+
}
|
|
124
|
+
input.reporter.stderr(t(input.lang, 'run.progress.started', { intent: input.intentName, seconds: input.timeoutSeconds }));
|
|
125
|
+
const timers = [];
|
|
126
|
+
for (const ratio of [0.5, 0.8]) {
|
|
127
|
+
const delayMs = Math.max(1, Math.floor(input.timeoutSeconds * 1000 * ratio));
|
|
128
|
+
const elapsedSeconds = Math.max(1, Math.round(input.timeoutSeconds * ratio));
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
input.reporter.stderr(t(input.lang, 'run.progress.timeoutWarning', {
|
|
131
|
+
intent: input.intentName,
|
|
132
|
+
seconds: elapsedSeconds,
|
|
133
|
+
percent: Math.round(ratio * 100),
|
|
134
|
+
}));
|
|
135
|
+
}, delayMs);
|
|
136
|
+
timer.unref?.();
|
|
137
|
+
timers.push(timer);
|
|
138
|
+
}
|
|
139
|
+
return () => {
|
|
140
|
+
for (const timer of timers) {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
120
145
|
export function getRunHelp(lang = 'en') {
|
|
121
146
|
return renderHelp({
|
|
122
147
|
usage: 'mf run <intent> [options]',
|
|
@@ -241,13 +266,25 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
241
266
|
let streamedOutput = false;
|
|
242
267
|
const childStartedAtMs = performance.now();
|
|
243
268
|
const startedAt = new Date();
|
|
269
|
+
const stopRunProgress = createRunProgressReporter({
|
|
270
|
+
enabled: !json && Boolean(reporter.writeStderr),
|
|
271
|
+
intentName,
|
|
272
|
+
timeoutSeconds: plan.timeoutSeconds,
|
|
273
|
+
reporter,
|
|
274
|
+
lang,
|
|
275
|
+
});
|
|
244
276
|
const result = await profiler.measureAsync('child_command', async () => {
|
|
245
|
-
|
|
277
|
+
try {
|
|
278
|
+
if (plan.commandArgv) {
|
|
279
|
+
streamedOutput = !json;
|
|
280
|
+
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
281
|
+
}
|
|
246
282
|
streamedOutput = !json;
|
|
247
|
-
return
|
|
283
|
+
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
stopRunProgress();
|
|
248
287
|
}
|
|
249
|
-
streamedOutput = !json;
|
|
250
|
-
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
251
288
|
});
|
|
252
289
|
const childDurationMs = performance.now() - childStartedAtMs;
|
|
253
290
|
const finishedAt = new Date();
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -665,6 +665,8 @@ Read these files before working:
|
|
|
665
665
|
"run.help.exit.ok": "The command completed with an allowed exit code",
|
|
666
666
|
"run.help.exit.fail": "The command was invalid, refused, timed out, or failed",
|
|
667
667
|
"run.label.suggestedIntentSnippet": "Suggested command contract snippet",
|
|
668
|
+
"run.progress.started": "Running {intent} (timeout: {seconds}s)...",
|
|
669
|
+
"run.progress.timeoutWarning": "Still running {intent}... ({seconds}s elapsed, {percent}% of timeout)",
|
|
668
670
|
"run.error.missingIntent": "Missing command name",
|
|
669
671
|
"run.error.unknownIntent": "Unknown command: {intent}",
|
|
670
672
|
"run.error.statusNotConfigured": 'Command "{intent}" is {status}; only configured commands can be run',
|
package/dist/cli/i18n/es.js
CHANGED
|
@@ -665,6 +665,8 @@ Lee estos archivos antes de trabajar:
|
|
|
665
665
|
"run.help.exit.ok": "El comando se completo con un codigo de salida permitido",
|
|
666
666
|
"run.help.exit.fail": "El comando no era válido, fue rechazado, agotó el tiempo o falló",
|
|
667
667
|
"run.label.suggestedIntentSnippet": "Snippet sugerido para el contrato de comandos",
|
|
668
|
+
"run.progress.started": "Ejecutando {intent} (timeout: {seconds}s)...",
|
|
669
|
+
"run.progress.timeoutWarning": "{intent} sigue ejecutándose... ({seconds}s transcurridos, {percent}% del timeout)",
|
|
668
670
|
"run.error.missingIntent": "Falta el nombre del comando",
|
|
669
671
|
"run.error.unknownIntent": "Comando desconocido: {intent}",
|
|
670
672
|
"run.error.statusNotConfigured": 'El comando "{intent}" está en estado {status}; sólo se pueden ejecutar comandos configurados',
|
package/dist/cli/i18n/fr.js
CHANGED
|
@@ -665,6 +665,8 @@ Lisez ces fichiers avant de travailler :
|
|
|
665
665
|
"run.help.exit.ok": "La commande s'est terminée avec un code de sortie autorisé",
|
|
666
666
|
"run.help.exit.fail": "La commande était non valide, refusée, expirée ou a échoué",
|
|
667
667
|
"run.label.suggestedIntentSnippet": "Extrait suggéré de contrat de commande",
|
|
668
|
+
"run.progress.started": "Exécution de {intent} (timeout : {seconds}s)...",
|
|
669
|
+
"run.progress.timeoutWarning": "{intent} est toujours en cours... ({seconds}s écoulées, {percent}% du timeout)",
|
|
668
670
|
"run.error.missingIntent": "Nom de commande manquant",
|
|
669
671
|
"run.error.unknownIntent": "Commande inconnue : {intent}",
|
|
670
672
|
"run.error.statusNotConfigured": 'La commande "{intent}" est {status} ; seules les commandes configurées peuvent être exécutées',
|
package/dist/cli/i18n/hi.js
CHANGED
|
@@ -665,6 +665,8 @@ export const hiMessages = {
|
|
|
665
665
|
"run.help.exit.ok": "कमांड अनुमत exit code के साथ पूरी हुई",
|
|
666
666
|
"run.help.exit.fail": "कमांड अमान्य थी, अस्वीकार हुई, timed out हुई या विफल हुई",
|
|
667
667
|
"run.label.suggestedIntentSnippet": "Suggested command contract snippet",
|
|
668
|
+
"run.progress.started": "{intent} चल रहा है (timeout: {seconds}s)...",
|
|
669
|
+
"run.progress.timeoutWarning": "{intent} अभी भी चल रहा है... ({seconds}s बीते, timeout का {percent}%)",
|
|
668
670
|
"run.error.missingIntent": "कमांड नाम नहीं दिया गया",
|
|
669
671
|
"run.error.unknownIntent": "अज्ञात कमांड: {intent}",
|
|
670
672
|
"run.error.statusNotConfigured": 'कमांड "{intent}" {status} है; केवल configured कमांड चलाई जा सकती हैं',
|
package/dist/cli/i18n/ko.js
CHANGED
|
@@ -665,6 +665,8 @@ export const koMessages = {
|
|
|
665
665
|
"run.help.exit.ok": "명령이 허용된 종료 코드로 완료되었습니다",
|
|
666
666
|
"run.help.exit.fail": "명령이 잘못되었거나, 거부되었거나, 시간 초과되었거나, 실패했습니다",
|
|
667
667
|
"run.label.suggestedIntentSnippet": "제안 명령 계약 조각",
|
|
668
|
+
"run.progress.started": "{intent} 실행 중(timeout: {seconds}초)...",
|
|
669
|
+
"run.progress.timeoutWarning": "{intent} 계속 실행 중... ({seconds}초 경과, timeout의 {percent}%)",
|
|
668
670
|
"run.error.missingIntent": "명령 이름이 없습니다",
|
|
669
671
|
"run.error.unknownIntent": "알 수 없는 명령: {intent}",
|
|
670
672
|
"run.error.statusNotConfigured": '명령 "{intent}"의 상태는 {status}입니다. 설정된 상태(configured)인 명령만 실행할 수 있습니다',
|
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -665,6 +665,8 @@ export const zhMessages = {
|
|
|
665
665
|
"run.help.exit.ok": "命令已以允许的退出码完成",
|
|
666
666
|
"run.help.exit.fail": "命令无效、被拒绝、超时或失败",
|
|
667
667
|
"run.label.suggestedIntentSnippet": "建议的命令契约片段",
|
|
668
|
+
"run.progress.started": "正在运行 {intent}(超时:{seconds} 秒)...",
|
|
669
|
+
"run.progress.timeoutWarning": "{intent} 仍在运行...(已用 {seconds} 秒,达到超时的 {percent}%)",
|
|
668
670
|
"run.error.missingIntent": "缺少命令名称",
|
|
669
671
|
"run.error.unknownIntent": "未知命令:{intent}",
|
|
670
672
|
"run.error.statusNotConfigured": '命令 "{intent}" 的状态为 {status};只能运行已配置的命令',
|
|
@@ -32,5 +32,5 @@ export function renderCliError(message, helpCommand, lang = 'en') {
|
|
|
32
32
|
}
|
|
33
33
|
export function printUsageError(reporter, message, helpCommand, helpText, lang = 'en') {
|
|
34
34
|
reporter.stderr(renderCliError(message, helpCommand, lang));
|
|
35
|
-
reporter.
|
|
35
|
+
reporter.stderr(helpText);
|
|
36
36
|
}
|
|
@@ -146,6 +146,10 @@ function updateSaveState() {
|
|
|
146
146
|
document.getElementById("save").disabled = pending.size === 0;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function hasUnsavedChanges() {
|
|
150
|
+
return pending.size > 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
149
153
|
function setPending(id, value) {
|
|
150
154
|
const original = snapshot.settings.find((setting) => setting.id === id)?.value;
|
|
151
155
|
if (Object.is(original, value)) {
|
|
@@ -1899,6 +1903,11 @@ document.getElementById("reload").addEventListener("click", () => {
|
|
|
1899
1903
|
document.getElementById("save").addEventListener("click", () => {
|
|
1900
1904
|
save().catch((error) => statusText(error.message, "error"));
|
|
1901
1905
|
});
|
|
1906
|
+
window.addEventListener("beforeunload", (event) => {
|
|
1907
|
+
if (!hasUnsavedChanges()) return;
|
|
1908
|
+
event.preventDefault();
|
|
1909
|
+
event.returnValue = "";
|
|
1910
|
+
});
|
|
1902
1911
|
document.getElementById("open-mustflow").addEventListener("click", () => {
|
|
1903
1912
|
openMustflowFolder().catch((error) => statusText(error.message, "error"));
|
|
1904
1913
|
});
|
|
@@ -9,6 +9,9 @@ export function renderDashboardStyles() {
|
|
|
9
9
|
--accent: #8fb4ff;
|
|
10
10
|
--danger: #ff9a9a;
|
|
11
11
|
--ok: #9be7ba;
|
|
12
|
+
--control-bg: #11141a;
|
|
13
|
+
--control-hover-bg: #171b23;
|
|
14
|
+
--control-active-bg: #0d1015;
|
|
12
15
|
--row-bg: rgba(255, 255, 255, 0.018);
|
|
13
16
|
--row-bg-alt: rgba(255, 255, 255, 0.035);
|
|
14
17
|
--status-neutral-bg: rgba(174, 182, 197, 0.1);
|
|
@@ -16,6 +19,27 @@ export function renderDashboardStyles() {
|
|
|
16
19
|
--status-warn-bg: rgba(255, 154, 154, 0.1);
|
|
17
20
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
18
21
|
}
|
|
22
|
+
@media (prefers-color-scheme: light) {
|
|
23
|
+
:root {
|
|
24
|
+
color-scheme: light;
|
|
25
|
+
--bg: #f6f8fb;
|
|
26
|
+
--panel: #ffffff;
|
|
27
|
+
--line: #d9e0ea;
|
|
28
|
+
--text: #162033;
|
|
29
|
+
--muted: #5d6b82;
|
|
30
|
+
--accent: #285fc2;
|
|
31
|
+
--danger: #b4232d;
|
|
32
|
+
--ok: #197a47;
|
|
33
|
+
--control-bg: #ffffff;
|
|
34
|
+
--control-hover-bg: #eef3f9;
|
|
35
|
+
--control-active-bg: #e4ebf5;
|
|
36
|
+
--row-bg: rgba(40, 95, 194, 0.035);
|
|
37
|
+
--row-bg-alt: rgba(40, 95, 194, 0.065);
|
|
38
|
+
--status-neutral-bg: rgba(93, 107, 130, 0.11);
|
|
39
|
+
--status-ok-bg: rgba(25, 122, 71, 0.11);
|
|
40
|
+
--status-warn-bg: rgba(180, 35, 45, 0.1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
19
43
|
* { box-sizing: border-box; }
|
|
20
44
|
body {
|
|
21
45
|
margin: 0;
|
|
@@ -75,6 +99,11 @@ main {
|
|
|
75
99
|
gap: 8px;
|
|
76
100
|
margin-bottom: 14px;
|
|
77
101
|
overflow-x: auto;
|
|
102
|
+
-webkit-overflow-scrolling: touch;
|
|
103
|
+
scrollbar-width: none;
|
|
104
|
+
}
|
|
105
|
+
.tabs::-webkit-scrollbar {
|
|
106
|
+
display: none;
|
|
78
107
|
}
|
|
79
108
|
.tab {
|
|
80
109
|
border-color: transparent;
|
|
@@ -120,17 +149,35 @@ input:focus-visible {
|
|
|
120
149
|
white-space: nowrap;
|
|
121
150
|
}
|
|
122
151
|
button, select, input {
|
|
123
|
-
background:
|
|
152
|
+
background: var(--control-bg);
|
|
124
153
|
border: 1px solid var(--line);
|
|
125
154
|
border-radius: 6px;
|
|
126
155
|
color: var(--text);
|
|
127
156
|
font: inherit;
|
|
128
157
|
min-height: 38px;
|
|
158
|
+
transition: background-color 160ms ease, border-color 160ms ease;
|
|
129
159
|
}
|
|
130
160
|
button {
|
|
131
161
|
cursor: pointer;
|
|
132
162
|
padding: 0 14px;
|
|
133
163
|
}
|
|
164
|
+
button:not(:disabled):hover,
|
|
165
|
+
select:hover,
|
|
166
|
+
input:hover {
|
|
167
|
+
background: var(--control-hover-bg);
|
|
168
|
+
border-color: var(--accent);
|
|
169
|
+
}
|
|
170
|
+
button:not(:disabled):active {
|
|
171
|
+
background: var(--control-active-bg);
|
|
172
|
+
}
|
|
173
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
174
|
+
button {
|
|
175
|
+
transition: background-color 160ms ease, border-color 160ms ease, transform 120ms ease;
|
|
176
|
+
}
|
|
177
|
+
button:not(:disabled):active {
|
|
178
|
+
transform: translateY(1px);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
134
181
|
button:disabled {
|
|
135
182
|
cursor: not-allowed;
|
|
136
183
|
opacity: 0.6;
|
|
@@ -75,7 +75,7 @@ function readLedgerFile(projectRoot) {
|
|
|
75
75
|
const ledgerPath = path.join(projectRoot, DOC_REVIEW_LEDGER_RELATIVE_PATH);
|
|
76
76
|
const ledgerDirectoryPath = path.dirname(ledgerPath);
|
|
77
77
|
ensureInside(projectRoot, ledgerPath);
|
|
78
|
-
ensureInsideWithoutSymlinks(projectRoot, ledgerDirectoryPath, {
|
|
78
|
+
ensureInsideWithoutSymlinks(projectRoot, ledgerDirectoryPath, { allowMissingDescendant: true });
|
|
79
79
|
if (!existsSync(ledgerDirectoryPath)) {
|
|
80
80
|
return { schema_version: '1', documents: [] };
|
|
81
81
|
}
|