@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/integrity.js +11 -11
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +11 -11
- package/dist/integrity.mjs.map +1 -1
- package/dist/server.js +796 -63
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +813 -71
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
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 (
|
|
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
|
-
|
|
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(
|
|
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:
|
|
2281
|
-
//
|
|
2282
|
-
//
|
|
2283
|
-
//
|
|
2284
|
-
|
|
2285
|
-
|
|
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
|
|
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 && !
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 =
|
|
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
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
"
|
|
4588
|
+
"c3be215f05cc48ec51ed75541184b89a8ac123463b80fe02f2adb6c1b5163853"
|
|
4033
4589
|
],
|
|
4034
4590
|
[
|
|
4035
4591
|
"stackwright-pro-dashboard-otter.json",
|
|
4036
|
-
"
|
|
4592
|
+
"9c319d311801730e8dc9bc142eebb8fc5a7f48da48fa0b8d8c3b7431652447be"
|
|
4037
4593
|
],
|
|
4038
4594
|
[
|
|
4039
4595
|
"stackwright-pro-data-otter.json",
|
|
4040
|
-
"
|
|
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
|
-
"
|
|
4604
|
+
"41e3a5838f05f6a51ed272860dd2d5b1df1f20bfc847eb8f39be109b89738e99"
|
|
4049
4605
|
],
|
|
4050
4606
|
[
|
|
4051
4607
|
"stackwright-pro-foreman-otter.json",
|
|
4052
|
-
"
|
|
4608
|
+
"5f18e37ee3f2064c62a2d01dfc541285f27e5762d0b1011e2a37a48af96c74c8"
|
|
4053
4609
|
],
|
|
4054
4610
|
[
|
|
4055
4611
|
"stackwright-pro-geo-otter.json",
|
|
4056
|
-
"
|
|
4612
|
+
"ff1191af8108cc70c09afe6a99b008dcb46f71e20f884da1ee424e431868a1b6"
|
|
4057
4613
|
],
|
|
4058
4614
|
[
|
|
4059
4615
|
"stackwright-pro-page-otter.json",
|
|
4060
|
-
"
|
|
4616
|
+
"1dcdf866be76d18f3aa2a9e56b8bf57a3d43477b43c410eb6d1feac0e7683658"
|
|
4061
4617
|
],
|
|
4062
4618
|
[
|
|
4063
4619
|
"stackwright-pro-polish-otter.json",
|
|
4064
|
-
"
|
|
4620
|
+
"72a52901b4781c9f0be97024ba6e19415cdfbd92369d9274aa3abe7efd208bc8"
|
|
4065
4621
|
],
|
|
4066
4622
|
[
|
|
4067
4623
|
"stackwright-pro-theme-otter.json",
|
|
4068
|
-
"
|
|
4624
|
+
"e8530a1146abc57eb31d2f036ba057d5eae7d4a2d9dca00e415d1c352ef4c5b5"
|
|
4069
4625
|
],
|
|
4070
4626
|
[
|
|
4071
4627
|
"stackwright-pro-workflow-otter.json",
|
|
4072
|
-
"
|
|
4628
|
+
"e02c1a5f492dd6b11f68e293d92f5f661dbb22e57570dcf133a9d0ffc7a8be7d"
|
|
4073
4629
|
],
|
|
4074
4630
|
[
|
|
4075
4631
|
"stackwright-services-otter.json",
|
|
4076
|
-
"
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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 {
|