@stackwright-pro/mcp 0.2.0-alpha.61 → 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,78 @@ 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
+ }
1732
+ if (Object.keys(answers).length === 0 && input.rawAnswers.length > 0) {
1733
+ answers = Object.fromEntries(
1734
+ input.rawAnswers.map((a) => [
1735
+ a.question_header,
1736
+ a.selected_options.length > 1 ? a.selected_options : a.selected_options[0] ?? ""
1737
+ ])
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
+ );
1748
+ }
1681
1749
  } else {
1682
1750
  answers = Object.fromEntries(
1683
1751
  input.rawAnswers.map((a) => [a.question_header, a.selected_options[0] ?? ""])
@@ -1704,7 +1772,8 @@ function handleSavePhaseAnswers(input) {
1704
1772
  text: JSON.stringify({
1705
1773
  success: true,
1706
1774
  path: filePath,
1707
- answersCount: Object.keys(answers).length
1775
+ answersCount: Object.keys(answers).length,
1776
+ ...warnings.length > 0 ? { warnings } : {}
1708
1777
  }),
1709
1778
  isError: false
1710
1779
  };
@@ -1851,15 +1920,31 @@ function registerOrchestrationTools(server2) {
1851
1920
  rawAnswers: jsonCoerce(
1852
1921
  z9.array(
1853
1922
  z9.object({
1854
- question_header: z9.string(),
1855
- 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"),
1856
1925
  other_text: z9.string().nullable().optional()
1857
1926
  })
1858
1927
  )
1859
1928
  ).describe(
1860
1929
  "Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
1861
1930
  ),
1862
- 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(
1863
1948
  "Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
1864
1949
  )
1865
1950
  },
@@ -2277,12 +2362,13 @@ var PHASE_DEPENDENCIES = {
2277
2362
  // workflow states as trigger sources. Produces OpenAPI specs consumed
2278
2363
  // by pages and dashboard for typed data wiring.
2279
2364
  services: ["api", "workflow"],
2280
- // pages/dashboard: now also depend on services for typed endpoint wiring
2281
- // via the OpenAPI specs that services produces.
2282
- // 'api' is still transitive through 'data'; auth removed page-otter has
2283
- // graceful fallback and reads role names from workflow artifacts instead
2284
- pages: ["designer", "theme", "data", "services"],
2285
- dashboard: ["designer", "theme", "data", "services"],
2365
+ // pages/dashboard: depend on services for typed endpoint wiring (OpenAPI
2366
+ // specs) and on geo so the specialist prompt includes geo-manifest.json
2367
+ // page/dashboard otters see which page slugs are already claimed and
2368
+ // won't attempt to overwrite them (swp-73c). 'api' is still transitive
2369
+ // through 'data'; auth removed — page-otter has graceful fallback.
2370
+ pages: ["designer", "theme", "data", "services", "geo"],
2371
+ dashboard: ["designer", "theme", "data", "services", "geo"],
2286
2372
  // auth is the penultimate phase — runs after all content-producing phases
2287
2373
  // so it can read pages, dashboard, workflow, and geo artifacts for route
2288
2374
  // protection and RBAC wiring. Skipped upstream phases still satisfy deps
@@ -2318,7 +2404,7 @@ var PHASE_TO_OTTER2 = {
2318
2404
  polish: "stackwright-pro-polish-otter",
2319
2405
  geo: "stackwright-pro-geo-otter"
2320
2406
  };
2321
- function isValidPhase(phase) {
2407
+ function isValidPhase2(phase) {
2322
2408
  return PHASE_ORDER.includes(phase);
2323
2409
  }
2324
2410
  function defaultPhaseStatus() {
@@ -2414,7 +2500,7 @@ function handleGetPipelineState(_cwd) {
2414
2500
  }
2415
2501
  function handleSetPipelineState(input) {
2416
2502
  const cwd = input._cwd ?? process.cwd();
2417
- if (input.phase && !isValidPhase(input.phase)) {
2503
+ if (input.phase && !isValidPhase2(input.phase)) {
2418
2504
  return {
2419
2505
  text: JSON.stringify({
2420
2506
  error: true,
@@ -2433,6 +2519,28 @@ function handleSetPipelineState(input) {
2433
2519
  isError: true
2434
2520
  };
2435
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
+ }
2436
2544
  try {
2437
2545
  const state = readState(cwd);
2438
2546
  if (input.status) {
@@ -2452,6 +2560,16 @@ function handleSetPipelineState(input) {
2452
2560
  }
2453
2561
  state.currentPhase = phase;
2454
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
+ }
2455
2573
  writeState(cwd, state);
2456
2574
  return { text: JSON.stringify(state), isError: false };
2457
2575
  } catch (err) {
@@ -2461,7 +2579,7 @@ function handleSetPipelineState(input) {
2461
2579
  function handleCheckExecutionReady(_cwd, phase) {
2462
2580
  const cwd = _cwd ?? process.cwd();
2463
2581
  if (phase) {
2464
- if (!isValidPhase(phase)) {
2582
+ if (!isValidPhase2(phase)) {
2465
2583
  return {
2466
2584
  text: JSON.stringify({
2467
2585
  error: true,
@@ -2609,7 +2727,7 @@ function handleListArtifacts(_cwd) {
2609
2727
  function handleWritePhaseQuestions(input) {
2610
2728
  const cwd = input._cwd ?? process.cwd();
2611
2729
  const { phase, responseText } = input;
2612
- if (!isValidPhase(phase)) {
2730
+ if (!isValidPhase2(phase)) {
2613
2731
  return {
2614
2732
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2615
2733
  isError: true
@@ -2667,7 +2785,7 @@ function handleWritePhaseQuestions(input) {
2667
2785
  function handleBuildSpecialistPrompt(input) {
2668
2786
  const cwd = input._cwd ?? process.cwd();
2669
2787
  const { phase } = input;
2670
- if (!isValidPhase(phase)) {
2788
+ if (!isValidPhase2(phase)) {
2671
2789
  return {
2672
2790
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2673
2791
  isError: true
@@ -2760,6 +2878,18 @@ ${JSON.stringify(content, null, 2)}`
2760
2878
  if (buildContextText) {
2761
2879
  parts.push("BUILD_CONTEXT:", buildContextText, "");
2762
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
+ }
2763
2893
  parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
2764
2894
  if (artifactSections.length > 0) {
2765
2895
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
@@ -3042,6 +3172,12 @@ var PHASE_ARTIFACT_SCHEMA = {
3042
3172
  protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
3043
3173
  auditEnabled: true,
3044
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" }
3045
3181
  }
3046
3182
  },
3047
3183
  null,
@@ -3053,7 +3189,12 @@ var PHASE_ARTIFACT_SCHEMA = {
3053
3189
  generatedBy: "stackwright-pro-polish-otter",
3054
3190
  landingPage: { slug: "", rewritten: true },
3055
3191
  navigation: { itemCount: 5, items: ["/dashboard", "/equipment", "/workflows"] },
3056
- 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
+ }
3057
3198
  },
3058
3199
  null,
3059
3200
  2
@@ -3062,7 +3203,7 @@ var PHASE_ARTIFACT_SCHEMA = {
3062
3203
  function handleValidateArtifact(input) {
3063
3204
  const cwd = input._cwd ?? process.cwd();
3064
3205
  const { phase, responseText, artifact: directArtifact } = input;
3065
- if (!isValidPhase(phase)) {
3206
+ if (!isValidPhase2(phase)) {
3066
3207
  return {
3067
3208
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
3068
3209
  isError: true
@@ -3210,7 +3351,16 @@ function registerPipelineTools(server2) {
3210
3351
  status: z11.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
3211
3352
  incrementRetry: boolCoerce(z11.boolean().optional()).describe(
3212
3353
  "Bump retryCount by 1 \u2014 must be a JSON boolean"
3213
- )
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")
3214
3364
  },
3215
3365
  async (args) => res(
3216
3366
  handleSetPipelineState({
@@ -3218,7 +3368,8 @@ function registerPipelineTools(server2) {
3218
3368
  ...args.field != null ? { field: args.field } : {},
3219
3369
  ...args.value != null ? { value: args.value } : {},
3220
3370
  ...args.status != null ? { status: args.status } : {},
3221
- ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {}
3371
+ ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {},
3372
+ ...args.updates != null ? { updates: args.updates } : {}
3222
3373
  })
3223
3374
  )
3224
3375
  );
@@ -3254,7 +3405,6 @@ function registerPipelineTools(server2) {
3254
3405
  },
3255
3406
  async ({ phase, responseText, questions }) => {
3256
3407
  if (phase && questions && Array.isArray(questions)) {
3257
- const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
3258
3408
  if (!SAFE_PHASE.test(phase)) {
3259
3409
  return {
3260
3410
  content: [
@@ -3317,7 +3467,7 @@ function registerPipelineTools(server2) {
3317
3467
 
3318
3468
  // src/tools/safe-write.ts
3319
3469
  import { z as z12 } from "zod";
3320
- import { writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync6 } from "fs";
3470
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync6 } from "fs";
3321
3471
  import { normalize, isAbsolute, dirname, join as join5 } from "path";
3322
3472
  var OTTER_WRITE_ALLOWLISTS = {
3323
3473
  "stackwright-pro-designer-otter": [
@@ -3339,6 +3489,16 @@ var OTTER_WRITE_ALLOWLISTS = {
3339
3489
  prefix: ".env",
3340
3490
  suffix: "",
3341
3491
  description: "Dotenv files (.env, .env.local, .env.production, etc.)"
3492
+ },
3493
+ {
3494
+ prefix: "lib/mock-auth",
3495
+ suffix: ".ts",
3496
+ description: "Mock auth module (DEV_ONLY_MODE role updates)"
3497
+ },
3498
+ {
3499
+ prefix: "package.json",
3500
+ suffix: ".json",
3501
+ description: "Project package.json (DEV_ONLY_MODE script cleanup)"
3342
3502
  }
3343
3503
  ],
3344
3504
  "stackwright-pro-data-otter": [
@@ -3376,7 +3536,8 @@ var OTTER_WRITE_ALLOWLISTS = {
3376
3536
  },
3377
3537
  { prefix: "pages/", suffix: "/content.yml", description: "Landing page content" },
3378
3538
  { prefix: "pages/", suffix: "/content.yaml", description: "Landing page content" },
3379
- { 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" }
3380
3541
  ],
3381
3542
  "stackwright-services-otter": [
3382
3543
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Services config artifact" },
@@ -3396,7 +3557,9 @@ var PROTECTED_PATH_PREFIXES = [
3396
3557
  ".stackwright/artifacts/signatures.json",
3397
3558
  // artifact signature manifest
3398
3559
  ".stackwright/questions/",
3399
- ".stackwright/answers/"
3560
+ ".stackwright/answers/",
3561
+ ".stackwright/page-registry.json"
3562
+ // page ownership — managed by safe_write internally
3400
3563
  ];
3401
3564
  var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
3402
3565
  var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
@@ -3410,6 +3573,58 @@ function getMaxBytesForPath(filePath) {
3410
3573
  return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
3411
3574
  return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
3412
3575
  }
3576
+ var PAGE_REGISTRY_FILE = ".stackwright/page-registry.json";
3577
+ function extractPageSlug(filePath) {
3578
+ const match = normalize(filePath).match(/^pages\/(.+?)\/content\.ya?ml$/);
3579
+ return match?.[1] ?? null;
3580
+ }
3581
+ function extractContentTypes(yamlContent) {
3582
+ const typePattern = /^\s*-?\s*type:\s*(\S+)/gm;
3583
+ const types = [];
3584
+ let m;
3585
+ while ((m = typePattern.exec(yamlContent)) !== null) {
3586
+ const typeName = m[1];
3587
+ if (typeName && !types.includes(typeName)) {
3588
+ types.push(typeName);
3589
+ }
3590
+ }
3591
+ return types.length > 0 ? types : ["unknown"];
3592
+ }
3593
+ function readPageRegistry(cwd) {
3594
+ const regPath = join5(cwd, PAGE_REGISTRY_FILE);
3595
+ if (!existsSync6(regPath)) return {};
3596
+ try {
3597
+ const stat = lstatSync6(regPath);
3598
+ if (stat.isSymbolicLink()) return {};
3599
+ return JSON.parse(readFileSync5(regPath, "utf-8"));
3600
+ } catch {
3601
+ return {};
3602
+ }
3603
+ }
3604
+ function writePageRegistry(cwd, registry) {
3605
+ const dir = join5(cwd, ".stackwright");
3606
+ mkdirSync5(dir, { recursive: true });
3607
+ const regPath = join5(cwd, PAGE_REGISTRY_FILE);
3608
+ if (existsSync6(regPath)) {
3609
+ const stat = lstatSync6(regPath);
3610
+ if (stat.isSymbolicLink()) {
3611
+ throw new Error("Refusing to write page registry through symlink");
3612
+ }
3613
+ }
3614
+ writeFileSync5(regPath, JSON.stringify(registry, null, 2) + "\n", { encoding: "utf-8" });
3615
+ }
3616
+ function checkPageOwnership(cwd, slug, callerOtter) {
3617
+ const registry = readPageRegistry(cwd);
3618
+ const existing = registry[slug];
3619
+ if (!existing || existing.claimedBy === callerOtter) {
3620
+ return { allowed: true };
3621
+ }
3622
+ return {
3623
+ allowed: false,
3624
+ owner: existing.claimedBy,
3625
+ contentTypes: existing.contentTypes
3626
+ };
3627
+ }
3413
3628
  function checkPathAllowed(callerOtter, filePath) {
3414
3629
  const normalized = normalize(filePath);
3415
3630
  if (normalized.includes("..")) {
@@ -3451,6 +3666,16 @@ function checkPathAllowed(callerOtter, filePath) {
3451
3666
  continue;
3452
3667
  }
3453
3668
  }
3669
+ if (rule.prefix === "lib/mock-auth" && rule.suffix === ".ts") {
3670
+ if (normalized !== "lib/mock-auth.ts") {
3671
+ continue;
3672
+ }
3673
+ }
3674
+ if (rule.prefix === "package.json" && rule.suffix === ".json") {
3675
+ if (normalized !== "package.json") {
3676
+ continue;
3677
+ }
3678
+ }
3454
3679
  return { allowed: true, rule: rule.description };
3455
3680
  }
3456
3681
  }
@@ -3576,11 +3801,38 @@ function handleSafeWrite(input) {
3576
3801
  };
3577
3802
  return { text: JSON.stringify(result), isError: true };
3578
3803
  }
3804
+ const pageSlug = extractPageSlug(normalized);
3805
+ if (pageSlug) {
3806
+ const ownership = checkPageOwnership(cwd, pageSlug, callerOtter);
3807
+ if (!ownership.allowed) {
3808
+ const result = {
3809
+ success: false,
3810
+ error: `Page slug "${pageSlug}" is already claimed by ${ownership.owner} (content types: ${ownership.contentTypes.join(", ")}). Use a different slug or coordinate with the owning otter.`,
3811
+ callerOtter,
3812
+ attemptedPath: filePath,
3813
+ allowedPaths: []
3814
+ };
3815
+ return { text: JSON.stringify(result), isError: true };
3816
+ }
3817
+ }
3579
3818
  try {
3580
3819
  if (createDirectories) {
3581
3820
  mkdirSync5(dirname(fullPath), { recursive: true });
3582
3821
  }
3583
3822
  writeFileSync5(fullPath, content, { encoding: "utf-8" });
3823
+ if (pageSlug) {
3824
+ try {
3825
+ const registry = readPageRegistry(cwd);
3826
+ registry[pageSlug] = {
3827
+ slug: pageSlug,
3828
+ claimedBy: callerOtter,
3829
+ contentTypes: extractContentTypes(content),
3830
+ writtenAt: (/* @__PURE__ */ new Date()).toISOString()
3831
+ };
3832
+ writePageRegistry(cwd, registry);
3833
+ } catch {
3834
+ }
3835
+ }
3584
3836
  const result = {
3585
3837
  success: true,
3586
3838
  path: normalized,
@@ -3627,7 +3879,7 @@ function registerSafeWriteTools(server2) {
3627
3879
 
3628
3880
  // src/tools/auth.ts
3629
3881
  import { z as z13 } from "zod";
3630
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
3882
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
3631
3883
  import { join as join6 } from "path";
3632
3884
  function buildHierarchy(roles) {
3633
3885
  const h = {};
@@ -3847,10 +4099,232 @@ ${routeLines}
3847
4099
  ${auditSection}
3848
4100
  `;
3849
4101
  }
4102
+ function generateDevOnlyMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4103
+ const rbacBlock = ` rbac: {
4104
+ roles: ${JSON.stringify(roles)},
4105
+ defaultRole: '${defaultRole}',
4106
+ hierarchy: ${JSON.stringify(hierarchy, null, 4)},
4107
+ },`;
4108
+ const auditBlock = ` audit: {
4109
+ enabled: ${auditEnabled},
4110
+ retentionDays: ${auditRetentionDays},
4111
+ },`;
4112
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4113
+ const configBlock = `export const config = {
4114
+ matcher: ${JSON.stringify(protectedRoutes)},
4115
+ };`;
4116
+ const devHeader = [
4117
+ "// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs (dev-only mock)",
4118
+ "// DEV ONLY \u2014 uses mock authentication from lib/mock-auth.ts",
4119
+ "// Do NOT deploy to production.",
4120
+ "import { createProMiddleware } from '@stackwright-pro/auth-nextjs';",
4121
+ "import { mockAuthProvider } from './lib/mock-auth';"
4122
+ ].join("\n");
4123
+ if (method === "cac") {
4124
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4125
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4126
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4127
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4128
+ return `${devHeader}
4129
+
4130
+ export const middleware = createProMiddleware({
4131
+ method: 'cac',
4132
+ cac: {
4133
+ caBundle: '${caBundle}',
4134
+ edipiLookup: '${edipiLookup}',
4135
+ ocspEndpoint: '${ocspEndpoint}',
4136
+ certHeader: '${certHeader}',
4137
+ provider: mockAuthProvider,
4138
+ },
4139
+ ${rbacBlock}
4140
+ ${auditBlock}
4141
+ ${routesBlock}
4142
+ });
4143
+
4144
+ ${configBlock}
4145
+ `;
4146
+ }
4147
+ if (method === "oidc") {
4148
+ const scopes2 = params.oidcScopes ?? "openid profile email";
4149
+ const roleClaim = params.oidcRoleClaim ?? "roles";
4150
+ return `${devHeader}
4151
+
4152
+ export const middleware = createProMiddleware({
4153
+ method: 'oidc',
4154
+ oidc: {
4155
+ discoveryUrl: 'https://dev-mock-oidc/.well-known/openid-configuration',
4156
+ clientId: 'dev-mock-client',
4157
+ clientSecret: 'dev-mock-secret',
4158
+ scopes: '${scopes2}',
4159
+ roleClaim: '${roleClaim}',
4160
+ provider: mockAuthProvider,
4161
+ },
4162
+ ${rbacBlock}
4163
+ ${auditBlock}
4164
+ ${routesBlock}
4165
+ });
4166
+
4167
+ ${configBlock}
4168
+ `;
4169
+ }
4170
+ const scopes = params.oauth2Scopes ?? "read write";
4171
+ return `${devHeader}
4172
+
4173
+ export const middleware = createProMiddleware({
4174
+ method: 'oauth2',
4175
+ oauth2: {
4176
+ authorizationUrl: 'https://dev-mock-oauth2/authorize',
4177
+ tokenUrl: 'https://dev-mock-oauth2/token',
4178
+ clientId: 'dev-mock-client',
4179
+ clientSecret: 'dev-mock-secret',
4180
+ scopes: '${scopes}',
4181
+ provider: mockAuthProvider,
4182
+ },
4183
+ ${rbacBlock}
4184
+ ${auditBlock}
4185
+ ${routesBlock}
4186
+ });
4187
+
4188
+ ${configBlock}
4189
+ `;
4190
+ }
4191
+ function generateDevOnlyYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4192
+ const rbacSection = ` rbac:
4193
+ roles:
4194
+ ${rolesToYaml(roles, " ")}
4195
+ defaultRole: ${defaultRole}
4196
+ hierarchy:
4197
+ ${hierarchyToYaml(hierarchy, " ")}`;
4198
+ const auditSection = ` audit:
4199
+ enabled: ${auditEnabled}
4200
+ retentionDays: ${auditRetentionDays}`;
4201
+ const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
4202
+ requiredRole: ${defaultRole}`).join("\n");
4203
+ const providerLine = params.provider ? ` provider: ${params.provider}
4204
+ ` : "";
4205
+ if (method === "cac") {
4206
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4207
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4208
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4209
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4210
+ return `auth:
4211
+ method: cac
4212
+ devOnly: true
4213
+ ${providerLine} middleware: ./middleware.ts
4214
+ cac:
4215
+ caBundle: ${caBundle}
4216
+ edipiLookup: ${edipiLookup}
4217
+ ocspEndpoint: ${ocspEndpoint}
4218
+ certHeader: ${certHeader}
4219
+ ${rbacSection}
4220
+ protectedRoutes:
4221
+ ${routeLines}
4222
+ ${auditSection}
4223
+ `;
4224
+ }
4225
+ if (method === "oidc") {
4226
+ const scopes2 = params.oidcScopes ?? "openid profile email";
4227
+ const roleClaim = params.oidcRoleClaim ?? "roles";
4228
+ return `auth:
4229
+ method: oidc
4230
+ devOnly: true
4231
+ ${providerLine} middleware: ./middleware.ts
4232
+ oidc:
4233
+ discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
4234
+ clientId: dev-mock-client
4235
+ clientSecret: dev-mock-secret
4236
+ scopes: ${scopes2}
4237
+ roleClaim: ${roleClaim}
4238
+ ${rbacSection}
4239
+ protectedRoutes:
4240
+ ${routeLines}
4241
+ ${auditSection}
4242
+ `;
4243
+ }
4244
+ const scopes = params.oauth2Scopes ?? "read write";
4245
+ return `auth:
4246
+ method: oauth2
4247
+ devOnly: true
4248
+ ${providerLine} middleware: ./middleware.ts
4249
+ oauth2:
4250
+ authorizationUrl: https://dev-mock-oauth2/authorize
4251
+ tokenUrl: https://dev-mock-oauth2/token
4252
+ clientId: dev-mock-client
4253
+ clientSecret: dev-mock-secret
4254
+ scopes: ${scopes}
4255
+ ${rbacSection}
4256
+ protectedRoutes:
4257
+ ${routeLines}
4258
+ ${auditSection}
4259
+ `;
4260
+ }
4261
+ function deriveDevKey(roleName) {
4262
+ const segment = roleName.split("_")[0];
4263
+ return (segment ?? roleName).toLowerCase();
4264
+ }
4265
+ function generateMockAuthContent(roles, mockUsers) {
4266
+ const entries = [];
4267
+ const scriptLines = [];
4268
+ for (let i = 0; i < roles.length; i++) {
4269
+ const role = roles[i];
4270
+ const devKey = deriveDevKey(role);
4271
+ const persona = mockUsers?.[i];
4272
+ const name = persona?.name ?? `Dev ${role}`;
4273
+ const email = persona?.email ?? `dev-${devKey}@example.mil`;
4274
+ const edipi = String(i + 1).padStart(10, "0");
4275
+ entries.push(` ${devKey}: {
4276
+ id: 'mock-${devKey}-${String(i + 1).padStart(3, "0")}',
4277
+ name: '${name}',
4278
+ email: '${email}',
4279
+ roles: ['${role}'],
4280
+ edipi: '${edipi}',
4281
+ }`);
4282
+ scriptLines.push(` * pnpm dev:${devKey} \u2014 ${name}`);
4283
+ }
4284
+ return `/**
4285
+ * Mock authentication for development mode.
4286
+ *
4287
+ * DEV_ONLY_MODE \u2014 no real auth provider. Select a persona via MOCK_USER env var
4288
+ * or use the convenience scripts in package.json:
4289
+ ${scriptLines.join("\n")}
4290
+ */
4291
+
4292
+ export const MOCK_USERS = {
4293
+ ${entries.join(",\n")}
4294
+ } as const;
4295
+
4296
+ export type MockRole = keyof typeof MOCK_USERS;
4297
+
4298
+ export function getMockUser(role?: string) {
4299
+ const key = (role ?? process.env.MOCK_USER) as MockRole | undefined;
4300
+ if (!key) return null;
4301
+ return MOCK_USERS[key] ?? null;
4302
+ }
4303
+
4304
+ export function mockAuthProvider() {
4305
+ return getMockUser();
4306
+ }
4307
+ `;
4308
+ }
4309
+ function updatePackageJsonScripts(existingJson, roles, mockUsers) {
4310
+ const pkg = JSON.parse(existingJson);
4311
+ const scripts = pkg.scripts ?? {};
4312
+ delete scripts["dev:admin"];
4313
+ delete scripts["dev:analyst"];
4314
+ delete scripts["dev:viewer"];
4315
+ for (let i = 0; i < roles.length; i++) {
4316
+ const devKey = deriveDevKey(roles[i]);
4317
+ scripts[`dev:${devKey}`] = `MOCK_USER=${devKey} next dev`;
4318
+ }
4319
+ pkg.scripts = scripts;
4320
+ return JSON.stringify(pkg, null, 2) + "\n";
4321
+ }
3850
4322
  async function configureAuthHandler(params, cwd) {
3851
4323
  const {
3852
4324
  method,
3853
4325
  provider,
4326
+ devOnly = false,
4327
+ mockUsers,
3854
4328
  rbacRoles = ["SUPER_ADMIN", "ADMIN", "ANALYST"],
3855
4329
  auditEnabled = true,
3856
4330
  auditRetentionDays = 90,
@@ -3880,7 +4354,16 @@ async function configureAuthHandler(params, cwd) {
3880
4354
  }
3881
4355
  const filesWritten = [];
3882
4356
  try {
3883
- const middlewareContent = generateMiddlewareContent(
4357
+ const middlewareContent = devOnly ? generateDevOnlyMiddlewareContent(
4358
+ method,
4359
+ params,
4360
+ roles,
4361
+ defaultRole,
4362
+ hierarchy,
4363
+ auditEnabled,
4364
+ auditRetentionDays,
4365
+ protectedRoutes
4366
+ ) : generateMiddlewareContent(
3884
4367
  method,
3885
4368
  params,
3886
4369
  roles,
@@ -3904,30 +4387,87 @@ async function configureAuthHandler(params, cwd) {
3904
4387
  isError: true
3905
4388
  };
3906
4389
  }
3907
- try {
3908
- const envBlock = generateEnvBlock(method, params);
3909
- const envPath = join6(cwd, ".env.example");
3910
- if (existsSync7(envPath)) {
3911
- const existing = readFileSync5(envPath, "utf8");
3912
- writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
3913
- } else {
3914
- writeFileSync6(envPath, envBlock, "utf8");
4390
+ if (!devOnly) {
4391
+ try {
4392
+ const envBlock = generateEnvBlock(method, params);
4393
+ const envPath = join6(cwd, ".env.example");
4394
+ if (existsSync7(envPath)) {
4395
+ const existing = readFileSync6(envPath, "utf8");
4396
+ writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
4397
+ } else {
4398
+ writeFileSync6(envPath, envBlock, "utf8");
4399
+ }
4400
+ filesWritten.push(".env.example");
4401
+ } catch (err) {
4402
+ const msg = err instanceof Error ? err.message : String(err);
4403
+ return {
4404
+ content: [
4405
+ {
4406
+ type: "text",
4407
+ text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
4408
+ }
4409
+ ],
4410
+ isError: true
4411
+ };
4412
+ }
4413
+ }
4414
+ if (devOnly) {
4415
+ try {
4416
+ const mockAuthDir = join6(cwd, "lib");
4417
+ mkdirSync6(mockAuthDir, { recursive: true });
4418
+ const mockAuthContent = generateMockAuthContent(roles, mockUsers);
4419
+ writeFileSync6(join6(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
4420
+ filesWritten.push("lib/mock-auth.ts");
4421
+ } catch (err) {
4422
+ const msg = err instanceof Error ? err.message : String(err);
4423
+ return {
4424
+ content: [
4425
+ {
4426
+ type: "text",
4427
+ text: JSON.stringify({
4428
+ success: false,
4429
+ error: `Failed writing lib/mock-auth.ts: ${msg}`
4430
+ })
4431
+ }
4432
+ ],
4433
+ isError: true
4434
+ };
4435
+ }
4436
+ try {
4437
+ const pkgPath = join6(cwd, "package.json");
4438
+ if (existsSync7(pkgPath)) {
4439
+ const existingPkg = readFileSync6(pkgPath, "utf8");
4440
+ const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
4441
+ writeFileSync6(pkgPath, updatedPkg, "utf8");
4442
+ filesWritten.push("package.json");
4443
+ }
4444
+ } catch (err) {
4445
+ const msg = err instanceof Error ? err.message : String(err);
4446
+ return {
4447
+ content: [
4448
+ {
4449
+ type: "text",
4450
+ text: JSON.stringify({
4451
+ success: false,
4452
+ error: `Failed updating package.json: ${msg}`
4453
+ })
4454
+ }
4455
+ ],
4456
+ isError: true
4457
+ };
3915
4458
  }
3916
- filesWritten.push(".env.example");
3917
- } catch (err) {
3918
- const msg = err instanceof Error ? err.message : String(err);
3919
- return {
3920
- content: [
3921
- {
3922
- type: "text",
3923
- text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
3924
- }
3925
- ],
3926
- isError: true
3927
- };
3928
4459
  }
3929
4460
  try {
3930
- const authYaml = generateYamlBlock(
4461
+ const authYaml = devOnly ? generateDevOnlyYamlBlock(
4462
+ method,
4463
+ params,
4464
+ roles,
4465
+ defaultRole,
4466
+ hierarchy,
4467
+ auditEnabled,
4468
+ auditRetentionDays,
4469
+ protectedRoutes
4470
+ ) : generateYamlBlock(
3931
4471
  method,
3932
4472
  params,
3933
4473
  roles,
@@ -3941,7 +4481,7 @@ async function configureAuthHandler(params, cwd) {
3941
4481
  if (!existsSync7(ymlPath)) {
3942
4482
  writeFileSync6(ymlPath, authYaml, "utf8");
3943
4483
  } else {
3944
- const existing = readFileSync5(ymlPath, "utf8");
4484
+ const existing = readFileSync6(ymlPath, "utf8");
3945
4485
  writeFileSync6(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
3946
4486
  }
3947
4487
  filesWritten.push("stackwright.yml");
@@ -3970,7 +4510,21 @@ async function configureAuthHandler(params, cwd) {
3970
4510
  rbacDefaultRole: defaultRole,
3971
4511
  protectedRoutesCount: protectedRoutes.length,
3972
4512
  filesWritten,
3973
- 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
+ } : {}
3974
4528
  })
3975
4529
  }
3976
4530
  ]
@@ -4009,7 +4563,9 @@ function registerAuthTools(server2) {
4009
4563
  // Routes
4010
4564
  protectedRoutes: jsonCoerce(z13.array(z13.string()).optional()),
4011
4565
  // Injection for tests
4012
- _cwd: z13.string().optional()
4566
+ _cwd: z13.string().optional(),
4567
+ devOnly: boolCoerce(z13.boolean().optional()),
4568
+ mockUsers: jsonCoerce(z13.array(z13.object({ name: z13.string(), email: z13.string() })).optional())
4013
4569
  },
4014
4570
  async (params) => {
4015
4571
  const cwd = params._cwd ?? process.cwd();
@@ -4020,7 +4576,7 @@ function registerAuthTools(server2) {
4020
4576
 
4021
4577
  // src/integrity.ts
4022
4578
  import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
4023
- import { readFileSync as readFileSync6, readdirSync as readdirSync2, lstatSync as lstatSync7 } from "fs";
4579
+ import { readFileSync as readFileSync7, readdirSync as readdirSync2, lstatSync as lstatSync7 } from "fs";
4024
4580
  import { join as join7, basename } from "path";
4025
4581
  var _checksums = /* @__PURE__ */ new Map([
4026
4582
  [
@@ -4029,15 +4585,15 @@ var _checksums = /* @__PURE__ */ new Map([
4029
4585
  ],
4030
4586
  [
4031
4587
  "stackwright-pro-auth-otter.json",
4032
- "8a6ee02cfe7fede3ca708d05b8b46824eb71f60c7f474b6edf9599da77f779b2"
4588
+ "c3be215f05cc48ec51ed75541184b89a8ac123463b80fe02f2adb6c1b5163853"
4033
4589
  ],
4034
4590
  [
4035
4591
  "stackwright-pro-dashboard-otter.json",
4036
- "f5a83b74ad7c44edc6f39b45a568fa122d82aa4788f741ce14614da56d4e29a4"
4592
+ "9c319d311801730e8dc9bc142eebb8fc5a7f48da48fa0b8d8c3b7431652447be"
4037
4593
  ],
4038
4594
  [
4039
4595
  "stackwright-pro-data-otter.json",
4040
- "c406e1c775bcb1f2b038b40a92d9bd23172b40d774fc0fa50bad4c9714f53445"
4596
+ "4d9369277685a4acc484116920c9622ad8a1838012a493fcfe42a6ae5abe53cf"
4041
4597
  ],
4042
4598
  [
4043
4599
  "stackwright-pro-designer-otter.json",
@@ -4045,35 +4601,35 @@ var _checksums = /* @__PURE__ */ new Map([
4045
4601
  ],
4046
4602
  [
4047
4603
  "stackwright-pro-domain-expert-otter.json",
4048
- "bfe5c167d73fef3f2ef280fff56dcb552073c218e1394a43ecf983a03169ed55"
4604
+ "41e3a5838f05f6a51ed272860dd2d5b1df1f20bfc847eb8f39be109b89738e99"
4049
4605
  ],
4050
4606
  [
4051
4607
  "stackwright-pro-foreman-otter.json",
4052
- "ab38ef53b95ec610a38b2866d78a135cbec16d257a9b35d7e46e2fee2d4de235"
4608
+ "5f18e37ee3f2064c62a2d01dfc541285f27e5762d0b1011e2a37a48af96c74c8"
4053
4609
  ],
4054
4610
  [
4055
4611
  "stackwright-pro-geo-otter.json",
4056
- "6eb7ecf97254dbd79c09ad24348bf16001423cce9585c14bef81afd67b7b901b"
4612
+ "ff1191af8108cc70c09afe6a99b008dcb46f71e20f884da1ee424e431868a1b6"
4057
4613
  ],
4058
4614
  [
4059
4615
  "stackwright-pro-page-otter.json",
4060
- "9a5672f0758c81539337d86955e2892cd412547b4f111c2aa098eed1e62d7626"
4616
+ "1dcdf866be76d18f3aa2a9e56b8bf57a3d43477b43c410eb6d1feac0e7683658"
4061
4617
  ],
4062
4618
  [
4063
4619
  "stackwright-pro-polish-otter.json",
4064
- "d31116995fdb417798af6056efd03bb1c71e0891371aba1774d283c03c9d77e8"
4620
+ "72a52901b4781c9f0be97024ba6e19415cdfbd92369d9274aa3abe7efd208bc8"
4065
4621
  ],
4066
4622
  [
4067
4623
  "stackwright-pro-theme-otter.json",
4068
- "08bb04009fdfb8743b10ac4d503cbaddaf8d7c804ba9b606aaed9cc516fd8e93"
4624
+ "e8530a1146abc57eb31d2f036ba057d5eae7d4a2d9dca00e415d1c352ef4c5b5"
4069
4625
  ],
4070
4626
  [
4071
4627
  "stackwright-pro-workflow-otter.json",
4072
- "c90d6773b2287aa9a640c2715ca0e75f44c13e99fddcfb89ced36603f38930ce"
4628
+ "e02c1a5f492dd6b11f68e293d92f5f661dbb22e57570dcf133a9d0ffc7a8be7d"
4073
4629
  ],
4074
4630
  [
4075
4631
  "stackwright-services-otter.json",
4076
- "4893a596d187110124f78336ee91184a51b3c8d980c455382fe481adb9b487b5"
4632
+ "b8a252d6a2c5090138899925d3ea95bf3339b6e49adc276a1f5f7304b4ae5134"
4077
4633
  ]
4078
4634
  ]);
4079
4635
  Object.freeze(_checksums);
@@ -4120,7 +4676,7 @@ function verifyOtterFile(filePath) {
4120
4676
  }
4121
4677
  let raw;
4122
4678
  try {
4123
- raw = readFileSync6(filePath);
4679
+ raw = readFileSync7(filePath);
4124
4680
  } catch (err) {
4125
4681
  const msg = err instanceof Error ? err.message : String(err);
4126
4682
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -4290,7 +4846,7 @@ function registerIntegrityTools(server2) {
4290
4846
 
4291
4847
  // src/tools/domain.ts
4292
4848
  import { z as z14 } from "zod";
4293
- import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
4849
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
4294
4850
  import { join as join8 } from "path";
4295
4851
  function handleListCollections(input) {
4296
4852
  const cwd = input._cwd ?? process.cwd();
@@ -4315,7 +4871,7 @@ function handleListCollections(input) {
4315
4871
  for (const { path: path3, source, parse } of sources) {
4316
4872
  if (!existsSync8(path3)) continue;
4317
4873
  try {
4318
- const collections = parse(readFileSync7(path3, "utf8"));
4874
+ const collections = parse(readFileSync8(path3, "utf8"));
4319
4875
  return {
4320
4876
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
4321
4877
  isError: false
@@ -4476,7 +5032,7 @@ function handleValidateWorkflow(input) {
4476
5032
  ]);
4477
5033
  }
4478
5034
  try {
4479
- raw = JSON.parse(readFileSync7(artifactPath, "utf8"));
5035
+ raw = JSON.parse(readFileSync8(artifactPath, "utf8"));
4480
5036
  } catch (err) {
4481
5037
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
4482
5038
  }
@@ -4783,6 +5339,191 @@ function registerTypeSchemasTool(server2) {
4783
5339
  );
4784
5340
  }
4785
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
+
4786
5527
  // package.json
4787
5528
  var package_default = {
4788
5529
  dependencies: {
@@ -4806,7 +5547,7 @@ var package_default = {
4806
5547
  "test:coverage": "vitest run --coverage"
4807
5548
  },
4808
5549
  name: "@stackwright-pro/mcp",
4809
- version: "0.2.0-alpha.61",
5550
+ version: "0.2.0-alpha.68",
4810
5551
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
4811
5552
  license: "SEE LICENSE IN LICENSE",
4812
5553
  main: "./dist/server.js",
@@ -4860,6 +5601,7 @@ registerIntegrityTools(server);
4860
5601
  registerArtifactSigningTools(server);
4861
5602
  registerDomainTools(server);
4862
5603
  registerTypeSchemasTool(server);
5604
+ registerScaffoldCleanupTools(server);
4863
5605
  async function main() {
4864
5606
  const transport = new StdioServerTransport();
4865
5607
  try {