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.
Files changed (56) hide show
  1. package/dist/cli/commands/dashboard.js +51 -4
  2. package/dist/cli/commands/explain.js +3 -2
  3. package/dist/cli/commands/help.js +0 -1
  4. package/dist/cli/commands/run.js +41 -4
  5. package/dist/cli/i18n/en.js +2 -0
  6. package/dist/cli/i18n/es.js +2 -0
  7. package/dist/cli/i18n/fr.js +2 -0
  8. package/dist/cli/i18n/hi.js +2 -0
  9. package/dist/cli/i18n/ko.js +2 -0
  10. package/dist/cli/i18n/zh.js +2 -0
  11. package/dist/cli/lib/cli-output.js +1 -1
  12. package/dist/cli/lib/dashboard-html/client-script.js +9 -0
  13. package/dist/cli/lib/dashboard-html/styles.js +48 -1
  14. package/dist/cli/lib/doc-review-ledger.js +1 -1
  15. package/dist/cli/lib/local-index/index.js +324 -298
  16. package/dist/cli/lib/repo-map.js +19 -5
  17. package/dist/cli/lib/validation/index.js +6 -2
  18. package/dist/core/active-run-locks.js +36 -8
  19. package/dist/core/atomic-state-write.js +5 -20
  20. package/dist/core/change-verification.js +18 -2
  21. package/dist/core/contract-lint.js +3 -3
  22. package/dist/core/repeated-failure.js +1 -1
  23. package/dist/core/run-write-drift.js +30 -17
  24. package/dist/core/safe-filesystem.js +54 -5
  25. package/dist/core/skill-route-explanation.js +2 -1
  26. package/dist/core/source-anchors.js +7 -3
  27. package/dist/core/validation-ratchet.js +61 -18
  28. package/dist/core/verification-decision-graph.js +8 -1
  29. package/package.json +1 -1
  30. package/templates/default/i18n.toml +139 -1
  31. package/templates/default/locales/en/.mustflow/skills/INDEX.md +24 -1
  32. package/templates/default/locales/en/.mustflow/skills/api-contract-change/SKILL.md +212 -0
  33. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +184 -0
  34. package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +194 -0
  35. package/templates/default/locales/en/.mustflow/skills/config-env-change/SKILL.md +189 -0
  36. package/templates/default/locales/en/.mustflow/skills/css-code-change/SKILL.md +199 -0
  37. package/templates/default/locales/en/.mustflow/skills/dart-code-change/SKILL.md +179 -0
  38. package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +178 -0
  39. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +151 -0
  40. package/templates/default/locales/en/.mustflow/skills/elysia-code-change/SKILL.md +115 -0
  41. package/templates/default/locales/en/.mustflow/skills/file-path-cross-platform-change/SKILL.md +147 -0
  42. package/templates/default/locales/en/.mustflow/skills/flutter-code-change/SKILL.md +116 -0
  43. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +156 -0
  44. package/templates/default/locales/en/.mustflow/skills/hono-code-change/SKILL.md +117 -0
  45. package/templates/default/locales/en/.mustflow/skills/html-code-change/SKILL.md +173 -0
  46. package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +149 -0
  47. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +154 -0
  48. package/templates/default/locales/en/.mustflow/skills/release-publish-change/SKILL.md +172 -0
  49. package/templates/default/locales/en/.mustflow/skills/routes.toml +138 -0
  50. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +154 -0
  51. package/templates/default/locales/en/.mustflow/skills/svelte-code-change/SKILL.md +186 -0
  52. package/templates/default/locales/en/.mustflow/skills/tailwind-code-change/SKILL.md +164 -0
  53. package/templates/default/locales/en/.mustflow/skills/tauri-code-change/SKILL.md +185 -0
  54. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +184 -0
  55. package/templates/default/locales/en/.mustflow/skills/unocss-code-change/SKILL.md +186 -0
  56. 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
- return request.headers['x-mustflow-dashboard-token'] === token;
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
- sendBadRequest(response);
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, readFileSync } from 'node:fs';
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(readFileSync(latestPath, 'utf8'));
229
+ parsed = JSON.parse(readMustflowTextFile(projectRoot, LATEST_RUN_RECEIPT_RELATIVE_PATH, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
229
230
  }
230
231
  catch {
231
232
  return {
@@ -140,6 +140,5 @@ export function runHelp(args, reporter, lang = 'en') {
140
140
  return 0;
141
141
  }
142
142
  reporter.stderr(renderCliError(t(lang, 'help.error.unknownTopic', { topic }), 'mf help --help', lang));
143
- reporter.stdout(getHelpHelp(lang));
144
143
  return 1;
145
144
  }
@@ -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
- if (plan.commandArgv) {
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 runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
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();
@@ -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',
@@ -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',
@@ -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',
@@ -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 कमांड चलाई जा सकती हैं',
@@ -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)인 명령만 실행할 수 있습니다',
@@ -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.stdout(helpText);
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: #11141a;
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, { allowMissingLeaf: true });
78
+ ensureInsideWithoutSymlinks(projectRoot, ledgerDirectoryPath, { allowMissingDescendant: true });
79
79
  if (!existsSync(ledgerDirectoryPath)) {
80
80
  return { schema_version: '1', documents: [] };
81
81
  }