@stackwright-pro/mcp 0.2.0-alpha.66 → 0.2.0-alpha.68

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/server.mjs CHANGED
@@ -1307,6 +1307,13 @@ function answersToManifestFormat(answers, questions) {
1307
1307
  return result;
1308
1308
  }
1309
1309
 
1310
+ // src/validation.ts
1311
+ var SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1312
+ var SAFE_QUESTION_ID = /^[a-z0-9][a-z0-9-]{0,63}$/i;
1313
+ function isValidPhase(phase) {
1314
+ return SAFE_PHASE.test(phase);
1315
+ }
1316
+
1310
1317
  // src/tools/questions.ts
1311
1318
  var ManifestQuestionSchema = z8.object({
1312
1319
  id: z8.string(),
@@ -1341,7 +1348,6 @@ function registerQuestionTools(server2) {
1341
1348
  },
1342
1349
  async ({ phase, questions, answers }) => {
1343
1350
  let resolvedQuestions;
1344
- const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1345
1351
  if (!SAFE_PHASE.test(phase)) {
1346
1352
  return {
1347
1353
  content: [
@@ -1443,7 +1449,6 @@ function registerQuestionTools(server2) {
1443
1449
  "Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
1444
1450
  { phase: z8.string().describe('Phase name e.g. "designer", "api", "auth"') },
1445
1451
  async ({ phase }) => {
1446
- const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1447
1452
  if (!SAFE_PHASE.test(phase)) {
1448
1453
  return {
1449
1454
  content: [
@@ -1508,7 +1513,6 @@ function registerQuestionTools(server2) {
1508
1513
  answer: z8.string().describe("The user's free-text answer")
1509
1514
  },
1510
1515
  async ({ phase, questionId, answer }) => {
1511
- const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1512
1516
  if (!SAFE_PHASE.test(phase)) {
1513
1517
  return {
1514
1518
  content: [
@@ -1517,7 +1521,7 @@ function registerQuestionTools(server2) {
1517
1521
  isError: true
1518
1522
  };
1519
1523
  }
1520
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
1524
+ if (!SAFE_QUESTION_ID.test(questionId)) {
1521
1525
  return {
1522
1526
  content: [
1523
1527
  { type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
@@ -1670,14 +1674,61 @@ function handleSaveManifest(input) {
1670
1674
  }
1671
1675
  }
1672
1676
  function handleSavePhaseAnswers(input) {
1677
+ if (!isValidPhase(input.phase)) {
1678
+ return {
1679
+ text: JSON.stringify({
1680
+ success: false,
1681
+ error: `Invalid phase name: "${input.phase.slice(0, 50)}". Must be lowercase alphanumeric with hyphens, max 31 chars.`
1682
+ }),
1683
+ isError: true
1684
+ };
1685
+ }
1686
+ for (let i = 0; i < input.rawAnswers.length; i++) {
1687
+ const a = input.rawAnswers[i];
1688
+ if (!a.question_header || a.question_header.trim().length === 0) {
1689
+ return {
1690
+ text: JSON.stringify({
1691
+ success: false,
1692
+ error: `rawAnswers[${i}].question_header is empty \u2014 every answer must have a non-empty question_header.`
1693
+ }),
1694
+ isError: true
1695
+ };
1696
+ }
1697
+ if (!a.selected_options || a.selected_options.length === 0) {
1698
+ return {
1699
+ text: JSON.stringify({
1700
+ success: false,
1701
+ error: `rawAnswers[${i}].selected_options is empty for question_header "${a.question_header}" \u2014 every answer must have at least one selected option.`
1702
+ }),
1703
+ isError: true
1704
+ };
1705
+ }
1706
+ }
1707
+ if (input.questions && input.questions.length > 0 && input.rawAnswers.length === 0) {
1708
+ return {
1709
+ text: JSON.stringify({
1710
+ success: false,
1711
+ error: `${input.questions.length} questions provided but rawAnswers is empty \u2014 this is a malformed payload. If the phase truly has no questions, omit the questions parameter.`
1712
+ }),
1713
+ isError: true
1714
+ };
1715
+ }
1673
1716
  const cwd = input._cwd ?? process.cwd();
1674
1717
  const dir = join2(cwd, ".stackwright", "answers");
1675
1718
  const filePath = join2(dir, `${input.phase}.json`);
1676
1719
  try {
1677
1720
  mkdirSync2(dir, { recursive: true });
1721
+ const warnings = [];
1678
1722
  let answers;
1679
1723
  if (input.questions && input.questions.length > 0) {
1680
- answers = answersToManifestFormat(input.rawAnswers, input.questions);
1724
+ try {
1725
+ answers = answersToManifestFormat(input.rawAnswers, input.questions);
1726
+ } catch (mapErr) {
1727
+ answers = {};
1728
+ warnings.push(
1729
+ `Reverse-mapping threw (${mapErr instanceof Error ? mapErr.message : String(mapErr)}) \u2014 falling back to direct key mapping.`
1730
+ );
1731
+ }
1681
1732
  if (Object.keys(answers).length === 0 && input.rawAnswers.length > 0) {
1682
1733
  answers = Object.fromEntries(
1683
1734
  input.rawAnswers.map((a) => [
@@ -1685,6 +1736,15 @@ function handleSavePhaseAnswers(input) {
1685
1736
  a.selected_options.length > 1 ? a.selected_options : a.selected_options[0] ?? ""
1686
1737
  ])
1687
1738
  );
1739
+ warnings.push(
1740
+ `Reverse-mapping produced empty answers despite ${input.rawAnswers.length} rawAnswers \u2014 used direct key mapping as fallback. Ensure question_header values match the manifest header format (e.g. "DESI-1", not descriptive keys).`
1741
+ );
1742
+ }
1743
+ const requiredCount = input.questions.filter((q) => q.required !== false).length;
1744
+ if (input.rawAnswers.length < requiredCount) {
1745
+ warnings.push(
1746
+ `Only ${input.rawAnswers.length} answers provided for ${requiredCount} required questions (${input.questions.length} total) \u2014 answers may be incomplete.`
1747
+ );
1688
1748
  }
1689
1749
  } else {
1690
1750
  answers = Object.fromEntries(
@@ -1712,7 +1772,8 @@ function handleSavePhaseAnswers(input) {
1712
1772
  text: JSON.stringify({
1713
1773
  success: true,
1714
1774
  path: filePath,
1715
- answersCount: Object.keys(answers).length
1775
+ answersCount: Object.keys(answers).length,
1776
+ ...warnings.length > 0 ? { warnings } : {}
1716
1777
  }),
1717
1778
  isError: false
1718
1779
  };
@@ -1859,15 +1920,31 @@ function registerOrchestrationTools(server2) {
1859
1920
  rawAnswers: jsonCoerce(
1860
1921
  z9.array(
1861
1922
  z9.object({
1862
- question_header: z9.string(),
1863
- selected_options: z9.array(z9.string()),
1923
+ question_header: z9.string().min(1, "question_header must not be empty"),
1924
+ selected_options: z9.array(z9.string()).min(1, "selected_options must have at least one entry"),
1864
1925
  other_text: z9.string().nullable().optional()
1865
1926
  })
1866
1927
  )
1867
1928
  ).describe(
1868
1929
  "Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
1869
1930
  ),
1870
- questions: jsonCoerce(z9.array(z9.any()).optional()).describe(
1931
+ questions: jsonCoerce(
1932
+ z9.array(
1933
+ z9.object({
1934
+ id: z9.string(),
1935
+ question: z9.string(),
1936
+ type: z9.string(),
1937
+ options: z9.array(z9.object({ label: z9.string(), value: z9.string() })).optional(),
1938
+ required: z9.boolean().optional(),
1939
+ default: z9.union([z9.string(), z9.boolean(), z9.array(z9.string())]).optional(),
1940
+ help: z9.string().optional(),
1941
+ dependsOn: z9.object({
1942
+ questionId: z9.string(),
1943
+ value: z9.union([z9.string(), z9.array(z9.string())])
1944
+ }).optional()
1945
+ }).passthrough()
1946
+ ).optional()
1947
+ ).describe(
1871
1948
  "Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
1872
1949
  )
1873
1950
  },
@@ -2327,7 +2404,7 @@ var PHASE_TO_OTTER2 = {
2327
2404
  polish: "stackwright-pro-polish-otter",
2328
2405
  geo: "stackwright-pro-geo-otter"
2329
2406
  };
2330
- function isValidPhase(phase) {
2407
+ function isValidPhase2(phase) {
2331
2408
  return PHASE_ORDER.includes(phase);
2332
2409
  }
2333
2410
  function defaultPhaseStatus() {
@@ -2423,7 +2500,7 @@ function handleGetPipelineState(_cwd) {
2423
2500
  }
2424
2501
  function handleSetPipelineState(input) {
2425
2502
  const cwd = input._cwd ?? process.cwd();
2426
- if (input.phase && !isValidPhase(input.phase)) {
2503
+ if (input.phase && !isValidPhase2(input.phase)) {
2427
2504
  return {
2428
2505
  text: JSON.stringify({
2429
2506
  error: true,
@@ -2442,6 +2519,28 @@ function handleSetPipelineState(input) {
2442
2519
  isError: true
2443
2520
  };
2444
2521
  }
2522
+ if (input.updates && input.updates.length > 0) {
2523
+ for (const update of input.updates) {
2524
+ if (!isValidPhase2(update.phase)) {
2525
+ return {
2526
+ text: JSON.stringify({
2527
+ error: true,
2528
+ message: `Invalid phase in batch update: ${update.phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
2529
+ }),
2530
+ isError: true
2531
+ };
2532
+ }
2533
+ if (!VALID_FIELDS.includes(update.field)) {
2534
+ return {
2535
+ text: JSON.stringify({
2536
+ error: true,
2537
+ message: `Invalid field in batch update: ${update.field}. Valid fields are: ${VALID_FIELDS.join(", ")}`
2538
+ }),
2539
+ isError: true
2540
+ };
2541
+ }
2542
+ }
2543
+ }
2445
2544
  try {
2446
2545
  const state = readState(cwd);
2447
2546
  if (input.status) {
@@ -2461,6 +2560,16 @@ function handleSetPipelineState(input) {
2461
2560
  }
2462
2561
  state.currentPhase = phase;
2463
2562
  }
2563
+ if (input.updates && input.updates.length > 0) {
2564
+ for (const update of input.updates) {
2565
+ if (!state.phases[update.phase]) {
2566
+ state.phases[update.phase] = defaultPhaseStatus();
2567
+ }
2568
+ const batchPhaseState = state.phases[update.phase];
2569
+ batchPhaseState[update.field] = update.value;
2570
+ state.currentPhase = update.phase;
2571
+ }
2572
+ }
2464
2573
  writeState(cwd, state);
2465
2574
  return { text: JSON.stringify(state), isError: false };
2466
2575
  } catch (err) {
@@ -2470,7 +2579,7 @@ function handleSetPipelineState(input) {
2470
2579
  function handleCheckExecutionReady(_cwd, phase) {
2471
2580
  const cwd = _cwd ?? process.cwd();
2472
2581
  if (phase) {
2473
- if (!isValidPhase(phase)) {
2582
+ if (!isValidPhase2(phase)) {
2474
2583
  return {
2475
2584
  text: JSON.stringify({
2476
2585
  error: true,
@@ -2618,7 +2727,7 @@ function handleListArtifacts(_cwd) {
2618
2727
  function handleWritePhaseQuestions(input) {
2619
2728
  const cwd = input._cwd ?? process.cwd();
2620
2729
  const { phase, responseText } = input;
2621
- if (!isValidPhase(phase)) {
2730
+ if (!isValidPhase2(phase)) {
2622
2731
  return {
2623
2732
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2624
2733
  isError: true
@@ -2676,7 +2785,7 @@ function handleWritePhaseQuestions(input) {
2676
2785
  function handleBuildSpecialistPrompt(input) {
2677
2786
  const cwd = input._cwd ?? process.cwd();
2678
2787
  const { phase } = input;
2679
- if (!isValidPhase(phase)) {
2788
+ if (!isValidPhase2(phase)) {
2680
2789
  return {
2681
2790
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2682
2791
  isError: true
@@ -2769,6 +2878,18 @@ ${JSON.stringify(content, null, 2)}`
2769
2878
  if (buildContextText) {
2770
2879
  parts.push("BUILD_CONTEXT:", buildContextText, "");
2771
2880
  }
2881
+ if (phase === "auth") {
2882
+ const initContextPath = join4(cwd, ".stackwright", "init-context.json");
2883
+ if (existsSync5(initContextPath)) {
2884
+ try {
2885
+ const initContext = JSON.parse(safeReadSync(initContextPath));
2886
+ if (initContext.devOnly === true || initContext.nonInteractive === true) {
2887
+ answers["devOnly"] = true;
2888
+ }
2889
+ } catch {
2890
+ }
2891
+ }
2892
+ }
2772
2893
  parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
2773
2894
  if (artifactSections.length > 0) {
2774
2895
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
@@ -3051,6 +3172,12 @@ var PHASE_ARTIFACT_SCHEMA = {
3051
3172
  protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
3052
3173
  auditEnabled: true,
3053
3174
  auditRetentionDays: 90
3175
+ },
3176
+ // devScripts is OPTIONAL — only present when devOnly: true was used
3177
+ // Polish otter and foreman MUST check devScripts.written before referencing scripts
3178
+ devScripts: {
3179
+ written: true,
3180
+ scripts: { "dev:admin": "MOCK_USER=admin next dev" }
3054
3181
  }
3055
3182
  },
3056
3183
  null,
@@ -3062,7 +3189,12 @@ var PHASE_ARTIFACT_SCHEMA = {
3062
3189
  generatedBy: "stackwright-pro-polish-otter",
3063
3190
  landingPage: { slug: "", rewritten: true },
3064
3191
  navigation: { itemCount: 5, items: ["/dashboard", "/equipment", "/workflows"] },
3065
- gettingStarted: "<rewritten|redirected|not-found>"
3192
+ gettingStarted: "<rewritten|redirected|not-found>",
3193
+ scaffoldCleanup: {
3194
+ deleted: ["content/posts/getting-started.yaml"],
3195
+ rewritten: ["app/not-found.tsx"],
3196
+ skipped: []
3197
+ }
3066
3198
  },
3067
3199
  null,
3068
3200
  2
@@ -3071,7 +3203,7 @@ var PHASE_ARTIFACT_SCHEMA = {
3071
3203
  function handleValidateArtifact(input) {
3072
3204
  const cwd = input._cwd ?? process.cwd();
3073
3205
  const { phase, responseText, artifact: directArtifact } = input;
3074
- if (!isValidPhase(phase)) {
3206
+ if (!isValidPhase2(phase)) {
3075
3207
  return {
3076
3208
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
3077
3209
  isError: true
@@ -3219,7 +3351,16 @@ function registerPipelineTools(server2) {
3219
3351
  status: z11.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
3220
3352
  incrementRetry: boolCoerce(z11.boolean().optional()).describe(
3221
3353
  "Bump retryCount by 1 \u2014 must be a JSON boolean"
3222
- )
3354
+ ),
3355
+ updates: jsonCoerce(
3356
+ z11.array(
3357
+ z11.object({
3358
+ phase: z11.string(),
3359
+ field: z11.enum(["questionsCollected", "answered", "executed", "artifactWritten"]),
3360
+ value: z11.boolean()
3361
+ })
3362
+ ).optional()
3363
+ ).describe("Batch updates \u2014 apply multiple field changes in one atomic read\u2192modify\u2192write")
3223
3364
  },
3224
3365
  async (args) => res(
3225
3366
  handleSetPipelineState({
@@ -3227,7 +3368,8 @@ function registerPipelineTools(server2) {
3227
3368
  ...args.field != null ? { field: args.field } : {},
3228
3369
  ...args.value != null ? { value: args.value } : {},
3229
3370
  ...args.status != null ? { status: args.status } : {},
3230
- ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {}
3371
+ ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {},
3372
+ ...args.updates != null ? { updates: args.updates } : {}
3231
3373
  })
3232
3374
  )
3233
3375
  );
@@ -3263,7 +3405,6 @@ function registerPipelineTools(server2) {
3263
3405
  },
3264
3406
  async ({ phase, responseText, questions }) => {
3265
3407
  if (phase && questions && Array.isArray(questions)) {
3266
- const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
3267
3408
  if (!SAFE_PHASE.test(phase)) {
3268
3409
  return {
3269
3410
  content: [
@@ -3395,7 +3536,8 @@ var OTTER_WRITE_ALLOWLISTS = {
3395
3536
  },
3396
3537
  { prefix: "pages/", suffix: "/content.yml", description: "Landing page content" },
3397
3538
  { prefix: "pages/", suffix: "/content.yaml", description: "Landing page content" },
3398
- { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Polish artifact" }
3539
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Polish artifact" },
3540
+ { prefix: "README.md", suffix: "", description: "Project README rewrite" }
3399
3541
  ],
3400
3542
  "stackwright-services-otter": [
3401
3543
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Services config artifact" },
@@ -4368,7 +4510,21 @@ async function configureAuthHandler(params, cwd) {
4368
4510
  rbacDefaultRole: defaultRole,
4369
4511
  protectedRoutesCount: protectedRoutes.length,
4370
4512
  filesWritten,
4371
- securityWarning
4513
+ securityWarning,
4514
+ ...devOnly ? {
4515
+ devScripts: {
4516
+ written: filesWritten.includes("package.json"),
4517
+ scripts: filesWritten.includes("package.json") ? Object.fromEntries(
4518
+ roles.map((r) => {
4519
+ const k = deriveDevKey(r);
4520
+ return [`dev:${k}`, `MOCK_USER=${k} next dev`];
4521
+ })
4522
+ ) : {},
4523
+ ...filesWritten.includes("package.json") ? {} : {
4524
+ warning: "package.json not found \u2014 dev scripts were not written. Run the auth phase again after scaffolding creates package.json."
4525
+ }
4526
+ }
4527
+ } : {}
4372
4528
  })
4373
4529
  }
4374
4530
  ]
@@ -4429,7 +4585,7 @@ var _checksums = /* @__PURE__ */ new Map([
4429
4585
  ],
4430
4586
  [
4431
4587
  "stackwright-pro-auth-otter.json",
4432
- "4bf6beba7150d08c74c5f6fbbeb20e988aba52a2029ff2892615e71f6ab12ed1"
4588
+ "c3be215f05cc48ec51ed75541184b89a8ac123463b80fe02f2adb6c1b5163853"
4433
4589
  ],
4434
4590
  [
4435
4591
  "stackwright-pro-dashboard-otter.json",
@@ -4445,35 +4601,35 @@ var _checksums = /* @__PURE__ */ new Map([
4445
4601
  ],
4446
4602
  [
4447
4603
  "stackwright-pro-domain-expert-otter.json",
4448
- "6055a2efc78f54a8393f628839e2a2563bf0c6de3ad32de00c82779a53381efd"
4604
+ "41e3a5838f05f6a51ed272860dd2d5b1df1f20bfc847eb8f39be109b89738e99"
4449
4605
  ],
4450
4606
  [
4451
4607
  "stackwright-pro-foreman-otter.json",
4452
- "ab38ef53b95ec610a38b2866d78a135cbec16d257a9b35d7e46e2fee2d4de235"
4608
+ "5f18e37ee3f2064c62a2d01dfc541285f27e5762d0b1011e2a37a48af96c74c8"
4453
4609
  ],
4454
4610
  [
4455
4611
  "stackwright-pro-geo-otter.json",
4456
- "9e09aaf2bb10197c6d1c05d0fd5f5f9380acc0cb697a410fcae839ffba648561"
4612
+ "ff1191af8108cc70c09afe6a99b008dcb46f71e20f884da1ee424e431868a1b6"
4457
4613
  ],
4458
4614
  [
4459
4615
  "stackwright-pro-page-otter.json",
4460
- "532bb7e9a25a5c832edd1ff1ea0886dd4453905d86e6f9331eb957ae5e121833"
4616
+ "1dcdf866be76d18f3aa2a9e56b8bf57a3d43477b43c410eb6d1feac0e7683658"
4461
4617
  ],
4462
4618
  [
4463
4619
  "stackwright-pro-polish-otter.json",
4464
- "8f284d4d6a204137cd786824fc584d5bddac1bc757204769b99ca5412cf2cea2"
4620
+ "72a52901b4781c9f0be97024ba6e19415cdfbd92369d9274aa3abe7efd208bc8"
4465
4621
  ],
4466
4622
  [
4467
4623
  "stackwright-pro-theme-otter.json",
4468
- "08bb04009fdfb8743b10ac4d503cbaddaf8d7c804ba9b606aaed9cc516fd8e93"
4624
+ "e8530a1146abc57eb31d2f036ba057d5eae7d4a2d9dca00e415d1c352ef4c5b5"
4469
4625
  ],
4470
4626
  [
4471
4627
  "stackwright-pro-workflow-otter.json",
4472
- "c90d6773b2287aa9a640c2715ca0e75f44c13e99fddcfb89ced36603f38930ce"
4628
+ "e02c1a5f492dd6b11f68e293d92f5f661dbb22e57570dcf133a9d0ffc7a8be7d"
4473
4629
  ],
4474
4630
  [
4475
4631
  "stackwright-services-otter.json",
4476
- "4893a596d187110124f78336ee91184a51b3c8d980c455382fe481adb9b487b5"
4632
+ "b8a252d6a2c5090138899925d3ea95bf3339b6e49adc276a1f5f7304b4ae5134"
4477
4633
  ]
4478
4634
  ]);
4479
4635
  Object.freeze(_checksums);
@@ -5183,6 +5339,191 @@ function registerTypeSchemasTool(server2) {
5183
5339
  );
5184
5340
  }
5185
5341
 
5342
+ // src/tools/scaffold-cleanup.ts
5343
+ import {
5344
+ existsSync as existsSync9,
5345
+ lstatSync as lstatSync8,
5346
+ unlinkSync as unlinkSync2,
5347
+ readFileSync as readFileSync9,
5348
+ writeFileSync as writeFileSync7,
5349
+ readdirSync as readdirSync3,
5350
+ rmdirSync,
5351
+ mkdirSync as mkdirSync7
5352
+ } from "fs";
5353
+ import { join as join9, dirname as dirname2 } from "path";
5354
+ var SCAFFOLD_FILES_TO_DELETE = [
5355
+ "content/posts/getting-started.yaml",
5356
+ "content/posts/hello-world.yaml"
5357
+ ];
5358
+ var SCAFFOLD_DIRS_TO_PRUNE = [
5359
+ "content/posts"
5360
+ // Don't prune 'content' — user may have other content directories
5361
+ ];
5362
+ var NOT_FOUND_PATH = "app/not-found.tsx";
5363
+ var FALLBACK_COLORS = {
5364
+ background: "#ffffff",
5365
+ foreground: "#171717",
5366
+ primary: "#525252",
5367
+ mutedForeground: "#737373"
5368
+ };
5369
+ function readThemeColors(cwd) {
5370
+ const tokenPath = join9(cwd, ".stackwright", "artifacts", "theme-tokens.json");
5371
+ if (!existsSync9(tokenPath)) return { ...FALLBACK_COLORS };
5372
+ const stat = lstatSync8(tokenPath);
5373
+ if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
5374
+ try {
5375
+ const raw = JSON.parse(readFileSync9(tokenPath, "utf-8"));
5376
+ const css = raw?.cssVariables ?? {};
5377
+ const toHsl = (hslValue) => {
5378
+ if (!hslValue || typeof hslValue !== "string") return null;
5379
+ return `hsl(${hslValue})`;
5380
+ };
5381
+ return {
5382
+ background: toHsl(css["--background"]) ?? FALLBACK_COLORS.background,
5383
+ foreground: toHsl(css["--foreground"]) ?? FALLBACK_COLORS.foreground,
5384
+ primary: toHsl(css["--primary"]) ?? FALLBACK_COLORS.primary,
5385
+ mutedForeground: toHsl(css["--muted-foreground"]) ?? FALLBACK_COLORS.mutedForeground
5386
+ };
5387
+ } catch {
5388
+ return { ...FALLBACK_COLORS };
5389
+ }
5390
+ }
5391
+ function generateNotFoundPage(colors) {
5392
+ return `export default function NotFound() {
5393
+ return (
5394
+ <div
5395
+ style={{
5396
+ display: 'flex',
5397
+ flexDirection: 'column',
5398
+ alignItems: 'center',
5399
+ justifyContent: 'center',
5400
+ minHeight: '100vh',
5401
+ backgroundColor: '${colors.background}',
5402
+ color: '${colors.foreground}',
5403
+ fontFamily: 'system-ui, -apple-system, sans-serif',
5404
+ padding: '2rem',
5405
+ textAlign: 'center',
5406
+ }}
5407
+ >
5408
+ <h1 style={{ fontSize: '4rem', fontWeight: 700, margin: 0, color: '${colors.primary}' }}>
5409
+ 404
5410
+ </h1>
5411
+ <p style={{ fontSize: '1.25rem', color: '${colors.mutedForeground}', marginTop: '0.5rem' }}>
5412
+ Page not found
5413
+ </p>
5414
+ <a
5415
+ href="/"
5416
+ style={{
5417
+ marginTop: '2rem',
5418
+ padding: '0.75rem 1.5rem',
5419
+ backgroundColor: '${colors.primary}',
5420
+ color: '${colors.background}',
5421
+ borderRadius: '0.5rem',
5422
+ textDecoration: 'none',
5423
+ fontSize: '0.875rem',
5424
+ fontWeight: 500,
5425
+ }}
5426
+ >
5427
+ Go Home
5428
+ </a>
5429
+ </div>
5430
+ );
5431
+ }
5432
+ `;
5433
+ }
5434
+ function isSafePath(fullPath) {
5435
+ if (!existsSync9(fullPath)) return true;
5436
+ const stat = lstatSync8(fullPath);
5437
+ return !stat.isSymbolicLink();
5438
+ }
5439
+ function isDirEmpty(dirPath) {
5440
+ if (!existsSync9(dirPath)) return false;
5441
+ const stat = lstatSync8(dirPath);
5442
+ if (!stat.isDirectory()) return false;
5443
+ return readdirSync3(dirPath).length === 0;
5444
+ }
5445
+ function handleCleanupScaffold(_cwd) {
5446
+ const cwd = _cwd ?? process.cwd();
5447
+ const result = {
5448
+ deleted: [],
5449
+ rewritten: [],
5450
+ skipped: [],
5451
+ errors: [],
5452
+ prunedDirs: []
5453
+ };
5454
+ for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
5455
+ const fullPath = join9(cwd, relPath);
5456
+ if (!existsSync9(fullPath)) {
5457
+ result.skipped.push(relPath);
5458
+ continue;
5459
+ }
5460
+ if (!isSafePath(fullPath)) {
5461
+ result.errors.push(`Refusing to delete symlink: ${relPath}`);
5462
+ continue;
5463
+ }
5464
+ try {
5465
+ unlinkSync2(fullPath);
5466
+ result.deleted.push(relPath);
5467
+ } catch (err) {
5468
+ result.errors.push(`Failed to delete ${relPath}: ${String(err)}`);
5469
+ }
5470
+ }
5471
+ for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
5472
+ const fullDir = join9(cwd, relDir);
5473
+ if (!existsSync9(fullDir)) continue;
5474
+ if (!isSafePath(fullDir)) {
5475
+ result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
5476
+ continue;
5477
+ }
5478
+ if (isDirEmpty(fullDir)) {
5479
+ try {
5480
+ rmdirSync(fullDir);
5481
+ result.prunedDirs.push(relDir);
5482
+ } catch (err) {
5483
+ result.errors.push(`Failed to prune directory ${relDir}: ${String(err)}`);
5484
+ }
5485
+ }
5486
+ }
5487
+ {
5488
+ const fullPath = join9(cwd, NOT_FOUND_PATH);
5489
+ if (!existsSync9(fullPath)) {
5490
+ result.skipped.push(NOT_FOUND_PATH);
5491
+ } else if (!isSafePath(fullPath)) {
5492
+ result.errors.push(`Refusing to rewrite symlink: ${NOT_FOUND_PATH}`);
5493
+ } else {
5494
+ try {
5495
+ const colors = readThemeColors(cwd);
5496
+ const content = generateNotFoundPage(colors);
5497
+ const dir = dirname2(fullPath);
5498
+ mkdirSync7(dir, { recursive: true });
5499
+ writeFileSync7(fullPath, content, "utf-8");
5500
+ result.rewritten.push(NOT_FOUND_PATH);
5501
+ } catch (err) {
5502
+ result.errors.push(`Failed to rewrite ${NOT_FOUND_PATH}: ${String(err)}`);
5503
+ }
5504
+ }
5505
+ }
5506
+ return {
5507
+ text: JSON.stringify(result),
5508
+ isError: false
5509
+ };
5510
+ }
5511
+ function registerScaffoldCleanupTools(server2) {
5512
+ const DESC = "Deterministic scaffold remnant cleanup \u2014 removes stale OSS scaffold files after raft run.";
5513
+ server2.tool(
5514
+ "stackwright_pro_cleanup_scaffold",
5515
+ `Remove/rewrite stale scaffold files (blog posts, hardcoded not-found page) after the raft pipeline completes. ${DESC}`,
5516
+ {},
5517
+ async () => {
5518
+ const r = handleCleanupScaffold();
5519
+ return {
5520
+ content: [{ type: "text", text: r.text }],
5521
+ isError: r.isError
5522
+ };
5523
+ }
5524
+ );
5525
+ }
5526
+
5186
5527
  // package.json
5187
5528
  var package_default = {
5188
5529
  dependencies: {
@@ -5206,7 +5547,7 @@ var package_default = {
5206
5547
  "test:coverage": "vitest run --coverage"
5207
5548
  },
5208
5549
  name: "@stackwright-pro/mcp",
5209
- version: "0.2.0-alpha.61",
5550
+ version: "0.2.0-alpha.68",
5210
5551
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
5211
5552
  license: "SEE LICENSE IN LICENSE",
5212
5553
  main: "./dist/server.js",
@@ -5260,6 +5601,7 @@ registerIntegrityTools(server);
5260
5601
  registerArtifactSigningTools(server);
5261
5602
  registerDomainTools(server);
5262
5603
  registerTypeSchemasTool(server);
5604
+ registerScaffoldCleanupTools(server);
5263
5605
  async function main() {
5264
5606
  const transport = new StdioServerTransport();
5265
5607
  try {