@stackwright-pro/mcp 0.2.0-alpha.114 → 0.2.0-alpha.117
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 +5 -5
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +5 -5
- package/dist/integrity.mjs.map +1 -1
- package/dist/pipeline-constants.js +229 -41
- package/dist/pipeline-constants.js.map +1 -1
- package/dist/pipeline-constants.mjs +237 -48
- package/dist/pipeline-constants.mjs.map +1 -1
- package/dist/server.js +1179 -479
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1125 -424
- package/dist/server.mjs.map +1 -1
- package/package.json +7 -7
package/dist/server.mjs
CHANGED
|
@@ -1290,7 +1290,7 @@ function answersToManifestFormat(answers, questions) {
|
|
|
1290
1290
|
return qHeader.startsWith(headerLower.split("-")[0]);
|
|
1291
1291
|
});
|
|
1292
1292
|
if (matched) {
|
|
1293
|
-
result[matched.id] = answer.selected_options[0] || "";
|
|
1293
|
+
result[matched.id] = answer.selected_options.length > 1 ? answer.selected_options : answer.selected_options[0] || "";
|
|
1294
1294
|
}
|
|
1295
1295
|
continue;
|
|
1296
1296
|
}
|
|
@@ -1762,7 +1762,10 @@ function handleSavePhaseAnswers(input) {
|
|
|
1762
1762
|
}
|
|
1763
1763
|
} else {
|
|
1764
1764
|
answers = Object.fromEntries(
|
|
1765
|
-
input.rawAnswers.map((a) => [
|
|
1765
|
+
input.rawAnswers.map((a) => [
|
|
1766
|
+
a.question_header,
|
|
1767
|
+
a.selected_options.length > 1 ? a.selected_options : a.selected_options[0] ?? ""
|
|
1768
|
+
])
|
|
1766
1769
|
);
|
|
1767
1770
|
}
|
|
1768
1771
|
const payload = {
|
|
@@ -2006,9 +2009,9 @@ function registerOrchestrationTools(server2) {
|
|
|
2006
2009
|
|
|
2007
2010
|
// src/tools/pipeline.ts
|
|
2008
2011
|
import { z as z14 } from "zod";
|
|
2009
|
-
import { readFileSync as
|
|
2012
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, existsSync as existsSync11, mkdirSync as mkdirSync4, lstatSync as lstatSync6, readdirSync as readdirSync8 } from "fs";
|
|
2010
2013
|
import { lockSync } from "proper-lockfile";
|
|
2011
|
-
import { join as
|
|
2014
|
+
import { join as join11 } from "path";
|
|
2012
2015
|
import { createHash as createHash3 } from "crypto";
|
|
2013
2016
|
|
|
2014
2017
|
// src/artifact-signing.ts
|
|
@@ -2551,6 +2554,598 @@ function formatPulseRetryPrompt(result) {
|
|
|
2551
2554
|
return lines.join("\n");
|
|
2552
2555
|
}
|
|
2553
2556
|
|
|
2557
|
+
// src/tools/validate-theme-tokens.ts
|
|
2558
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
2559
|
+
import { join as join6, relative as relative2 } from "path";
|
|
2560
|
+
import { load as yamlLoad2 } from "js-yaml";
|
|
2561
|
+
|
|
2562
|
+
// src/tools/schema-registry.ts
|
|
2563
|
+
import { z as z11 } from "zod";
|
|
2564
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
2565
|
+
import { join as join5 } from "path";
|
|
2566
|
+
import {
|
|
2567
|
+
WorkflowStepSchema,
|
|
2568
|
+
WorkflowDefinitionSchema,
|
|
2569
|
+
WorkflowFieldSchema,
|
|
2570
|
+
WorkflowActionSchema,
|
|
2571
|
+
WorkflowWizardContentSchema,
|
|
2572
|
+
authConfigSchema,
|
|
2573
|
+
themeTokensFileSchema
|
|
2574
|
+
} from "@stackwright-pro/types";
|
|
2575
|
+
import {
|
|
2576
|
+
MetricCardPulseSchema,
|
|
2577
|
+
DataTablePulseSchema,
|
|
2578
|
+
StatusBadgePulseSchema,
|
|
2579
|
+
MapPulseSchema
|
|
2580
|
+
} from "@stackwright-pro/pulse";
|
|
2581
|
+
function getSchemaForEntry(entry, ctx) {
|
|
2582
|
+
if (entry.schema) return entry.schema;
|
|
2583
|
+
if (entry.schemaFactory) {
|
|
2584
|
+
if (ctx) return entry.schemaFactory(ctx);
|
|
2585
|
+
console.warn(
|
|
2586
|
+
"[schema-registry] schemaFactory entry requires projectRoot context. Pass { projectRoot } to get the project-specific schema. Falling back to z.string() (no enforcement)."
|
|
2587
|
+
);
|
|
2588
|
+
return z11.string();
|
|
2589
|
+
}
|
|
2590
|
+
return z11.unknown();
|
|
2591
|
+
}
|
|
2592
|
+
var NavigationLinkSchema = z11.lazy(
|
|
2593
|
+
() => z11.object({
|
|
2594
|
+
label: z11.string().min(1),
|
|
2595
|
+
href: z11.string().min(1),
|
|
2596
|
+
children: z11.array(NavigationLinkSchema).optional()
|
|
2597
|
+
})
|
|
2598
|
+
);
|
|
2599
|
+
var NavigationSectionSchema = z11.object({
|
|
2600
|
+
section: z11.string().min(1),
|
|
2601
|
+
items: z11.array(NavigationLinkSchema)
|
|
2602
|
+
});
|
|
2603
|
+
var NavigationItemSchema = z11.union([
|
|
2604
|
+
NavigationLinkSchema,
|
|
2605
|
+
NavigationSectionSchema
|
|
2606
|
+
]);
|
|
2607
|
+
var THEME_TOKENS_PATHS = [
|
|
2608
|
+
join5(".stackwright", "artifacts", "theme-tokens.json"),
|
|
2609
|
+
join5("public", "stackwright-content", "_theme.json")
|
|
2610
|
+
];
|
|
2611
|
+
function resolveStatusLadder(projectRoot) {
|
|
2612
|
+
for (const relPath of THEME_TOKENS_PATHS) {
|
|
2613
|
+
const fullPath = join5(projectRoot, relPath);
|
|
2614
|
+
if (!existsSync6(fullPath)) continue;
|
|
2615
|
+
try {
|
|
2616
|
+
const raw = readFileSync5(fullPath, "utf-8");
|
|
2617
|
+
const parsed = JSON.parse(raw);
|
|
2618
|
+
if (Array.isArray(parsed["statusLadder"]) && parsed["statusLadder"].length > 0) {
|
|
2619
|
+
return parsed["statusLadder"].filter(
|
|
2620
|
+
(t) => typeof t === "string" && t.startsWith("status-")
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
const colors = parsed["tokens"]?.["colors"];
|
|
2624
|
+
if (colors && typeof colors === "object") {
|
|
2625
|
+
const statusKeys = Object.keys(colors).filter((k) => k.startsWith("status-"));
|
|
2626
|
+
if (statusKeys.length > 0) return statusKeys;
|
|
2627
|
+
}
|
|
2628
|
+
} catch {
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return [];
|
|
2632
|
+
}
|
|
2633
|
+
function buildThemeStatusTokenSchema(ctx) {
|
|
2634
|
+
const ladder = resolveStatusLadder(ctx.projectRoot);
|
|
2635
|
+
if (ladder.length === 0) {
|
|
2636
|
+
return z11.string();
|
|
2637
|
+
}
|
|
2638
|
+
return z11.enum(ladder);
|
|
2639
|
+
}
|
|
2640
|
+
var REGISTRY = /* @__PURE__ */ new Map([
|
|
2641
|
+
// ── Workflow schemas ──────────────────────────────────────────────────────
|
|
2642
|
+
[
|
|
2643
|
+
"workflow_step",
|
|
2644
|
+
{
|
|
2645
|
+
schema: WorkflowStepSchema,
|
|
2646
|
+
synonyms: {
|
|
2647
|
+
label: ["title", "name"],
|
|
2648
|
+
message: ["description"],
|
|
2649
|
+
"theme.variant": ["style"],
|
|
2650
|
+
"on_submit.transition": ["transitions"]
|
|
2651
|
+
},
|
|
2652
|
+
phaseAffinity: ["workflow"]
|
|
2653
|
+
}
|
|
2654
|
+
],
|
|
2655
|
+
[
|
|
2656
|
+
"workflow_definition",
|
|
2657
|
+
{
|
|
2658
|
+
schema: WorkflowDefinitionSchema,
|
|
2659
|
+
synonyms: {
|
|
2660
|
+
label: ["title", "name"],
|
|
2661
|
+
id: ["type"]
|
|
2662
|
+
},
|
|
2663
|
+
phaseAffinity: ["workflow"]
|
|
2664
|
+
}
|
|
2665
|
+
],
|
|
2666
|
+
[
|
|
2667
|
+
"workflow_field",
|
|
2668
|
+
{
|
|
2669
|
+
schema: WorkflowFieldSchema,
|
|
2670
|
+
synonyms: {
|
|
2671
|
+
name: ["id"],
|
|
2672
|
+
label: ["title", "description"]
|
|
2673
|
+
},
|
|
2674
|
+
phaseAffinity: ["workflow"]
|
|
2675
|
+
}
|
|
2676
|
+
],
|
|
2677
|
+
[
|
|
2678
|
+
"workflow_action",
|
|
2679
|
+
{
|
|
2680
|
+
schema: WorkflowActionSchema,
|
|
2681
|
+
synonyms: {
|
|
2682
|
+
label: ["title", "name"],
|
|
2683
|
+
transition: ["then"],
|
|
2684
|
+
"theme.variant": ["style"],
|
|
2685
|
+
action: ["on_click"]
|
|
2686
|
+
},
|
|
2687
|
+
phaseAffinity: ["workflow"]
|
|
2688
|
+
}
|
|
2689
|
+
],
|
|
2690
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
2691
|
+
[
|
|
2692
|
+
"navigation_item",
|
|
2693
|
+
{
|
|
2694
|
+
schema: NavigationItemSchema,
|
|
2695
|
+
synonyms: {
|
|
2696
|
+
items: ["children"],
|
|
2697
|
+
section: ["label", "title"]
|
|
2698
|
+
},
|
|
2699
|
+
phaseAffinity: ["pages", "dashboard", "polish"]
|
|
2700
|
+
}
|
|
2701
|
+
],
|
|
2702
|
+
// ── Auth ──────────────────────────────────────────────────────────────────
|
|
2703
|
+
[
|
|
2704
|
+
"auth_config",
|
|
2705
|
+
{
|
|
2706
|
+
schema: authConfigSchema,
|
|
2707
|
+
synonyms: {
|
|
2708
|
+
type: ["method", "provider_type", "strategy"]
|
|
2709
|
+
},
|
|
2710
|
+
phaseAffinity: ["auth"]
|
|
2711
|
+
}
|
|
2712
|
+
],
|
|
2713
|
+
// ── Pulse content types ───────────────────────────────────────────────────
|
|
2714
|
+
[
|
|
2715
|
+
"metric_card_pulse",
|
|
2716
|
+
{
|
|
2717
|
+
schema: MetricCardPulseSchema,
|
|
2718
|
+
synonyms: {
|
|
2719
|
+
field: ["valueField", "valuePath", "dataField", "value"]
|
|
2720
|
+
},
|
|
2721
|
+
phaseAffinity: ["dashboard", "pages"]
|
|
2722
|
+
}
|
|
2723
|
+
],
|
|
2724
|
+
[
|
|
2725
|
+
"data_table_pulse",
|
|
2726
|
+
{
|
|
2727
|
+
schema: DataTablePulseSchema,
|
|
2728
|
+
synonyms: {
|
|
2729
|
+
header: ["label"],
|
|
2730
|
+
type: ["renderAs"],
|
|
2731
|
+
colorMap: ["severityMap"]
|
|
2732
|
+
},
|
|
2733
|
+
phaseAffinity: ["dashboard", "pages"]
|
|
2734
|
+
}
|
|
2735
|
+
],
|
|
2736
|
+
[
|
|
2737
|
+
"status_badge_pulse",
|
|
2738
|
+
{
|
|
2739
|
+
schema: StatusBadgePulseSchema,
|
|
2740
|
+
synonyms: {},
|
|
2741
|
+
phaseAffinity: ["dashboard", "pages"]
|
|
2742
|
+
}
|
|
2743
|
+
],
|
|
2744
|
+
[
|
|
2745
|
+
"map_pulse",
|
|
2746
|
+
{
|
|
2747
|
+
schema: MapPulseSchema,
|
|
2748
|
+
synonyms: {},
|
|
2749
|
+
phaseAffinity: ["geo", "dashboard", "pages"]
|
|
2750
|
+
}
|
|
2751
|
+
],
|
|
2752
|
+
// ── Workflow container page content type (swp-dvde) ───────────────────────
|
|
2753
|
+
[
|
|
2754
|
+
"workflow_wizard",
|
|
2755
|
+
{
|
|
2756
|
+
schema: WorkflowWizardContentSchema,
|
|
2757
|
+
synonyms: {
|
|
2758
|
+
workflowFile: ["workflowPath", "file", "path", "yamlFile"],
|
|
2759
|
+
workflowId: ["id", "workflowName", "name"],
|
|
2760
|
+
persistenceKey: ["stateKey", "storageKey"]
|
|
2761
|
+
},
|
|
2762
|
+
phaseAffinity: ["workflow"]
|
|
2763
|
+
}
|
|
2764
|
+
],
|
|
2765
|
+
// ── Theme schemas (swp-cxwu) ──────────────────────────────────────────────
|
|
2766
|
+
[
|
|
2767
|
+
"theme_tokens",
|
|
2768
|
+
{
|
|
2769
|
+
schema: themeTokensFileSchema,
|
|
2770
|
+
synonyms: {},
|
|
2771
|
+
phaseAffinity: ["theme"]
|
|
2772
|
+
}
|
|
2773
|
+
],
|
|
2774
|
+
[
|
|
2775
|
+
"theme_status_token",
|
|
2776
|
+
{
|
|
2777
|
+
// No static schema — this is a project-specific runtime enum.
|
|
2778
|
+
// Call getSchemaForEntry(entry, { projectRoot }) to get the project's ladder.
|
|
2779
|
+
schemaFactory: buildThemeStatusTokenSchema,
|
|
2780
|
+
synonyms: {},
|
|
2781
|
+
phaseAffinity: ["pages", "dashboard", "geo"]
|
|
2782
|
+
}
|
|
2783
|
+
]
|
|
2784
|
+
]);
|
|
2785
|
+
var SCHEMA_REGISTRY = REGISTRY;
|
|
2786
|
+
var REGISTRY_SCHEMA_NAMES = Array.from(REGISTRY.keys());
|
|
2787
|
+
function buildReverseSynonyms(synonyms) {
|
|
2788
|
+
const reverse = {};
|
|
2789
|
+
for (const [correct, wrongs] of Object.entries(synonyms)) {
|
|
2790
|
+
for (const wrong of wrongs) {
|
|
2791
|
+
reverse[wrong] = correct;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return reverse;
|
|
2795
|
+
}
|
|
2796
|
+
function buildPhaseSchemaNames() {
|
|
2797
|
+
const phaseMap = {};
|
|
2798
|
+
for (const [name, entry] of REGISTRY.entries()) {
|
|
2799
|
+
for (const phase of entry.phaseAffinity ?? []) {
|
|
2800
|
+
if (!phaseMap[phase]) phaseMap[phase] = [];
|
|
2801
|
+
phaseMap[phase].push(name);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return phaseMap;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// src/tools/validate-theme-tokens.ts
|
|
2808
|
+
var COLOR_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
2809
|
+
"color",
|
|
2810
|
+
"colorMap",
|
|
2811
|
+
"layerColor",
|
|
2812
|
+
"borderColor",
|
|
2813
|
+
"backgroundColor",
|
|
2814
|
+
"iconColor",
|
|
2815
|
+
"fill",
|
|
2816
|
+
"stroke",
|
|
2817
|
+
"markerColor",
|
|
2818
|
+
"fillColor",
|
|
2819
|
+
"outlineColor",
|
|
2820
|
+
"statusColor",
|
|
2821
|
+
"badgeColor",
|
|
2822
|
+
"tagColor"
|
|
2823
|
+
]);
|
|
2824
|
+
var LEGACY_OSS_TOKENS = /* @__PURE__ */ new Set(["success", "warning", "danger", "info", "muted"]);
|
|
2825
|
+
var LEGACY_MAPPING_HINT = "Suggested mapping: success->status-safe, warning->status-warning, danger->status-critical, info->status-watch, muted->status-at-risk.";
|
|
2826
|
+
var HEX_RE = /^#[0-9a-f]{3,8}$/i;
|
|
2827
|
+
var STATUS_TOKEN_RE = /^status-[a-z0-9-]+$/;
|
|
2828
|
+
function walkYaml(node, fieldName, pathParts, inColorContext, visitor) {
|
|
2829
|
+
if (typeof node === "string") {
|
|
2830
|
+
visitor(node, fieldName, pathParts.join(".") || "(root)", inColorContext);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
if (Array.isArray(node)) {
|
|
2834
|
+
node.forEach((item, idx) => {
|
|
2835
|
+
walkYaml(item, fieldName, [...pathParts, `[${idx}]`], inColorContext, visitor);
|
|
2836
|
+
});
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (node !== null && typeof node === "object") {
|
|
2840
|
+
for (const [key, val] of Object.entries(node)) {
|
|
2841
|
+
const thisIsColorField = COLOR_FIELD_KEYS.has(key);
|
|
2842
|
+
walkYaml(val, key, [...pathParts, key], inColorContext || thisIsColorField, visitor);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
function detectViolationsInYaml(yamlContent, ladder, ladderSet, relPath) {
|
|
2847
|
+
const violations = [];
|
|
2848
|
+
walkYaml(yamlContent, null, [], false, (value, fieldName, path5, inColorContext) => {
|
|
2849
|
+
const isColorField = inColorContext || fieldName !== null && COLOR_FIELD_KEYS.has(fieldName);
|
|
2850
|
+
if (STATUS_TOKEN_RE.test(value) && !ladderSet.has(value)) {
|
|
2851
|
+
violations.push({
|
|
2852
|
+
type: "undefined_token",
|
|
2853
|
+
message: `Undefined theme token: "${value}" not in declared theme ladder. Declared tokens: ${ladder.join(", ")}`,
|
|
2854
|
+
file: relPath,
|
|
2855
|
+
path: path5,
|
|
2856
|
+
value
|
|
2857
|
+
});
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
if (!isColorField) return;
|
|
2861
|
+
if (HEX_RE.test(value)) {
|
|
2862
|
+
violations.push({
|
|
2863
|
+
type: "raw_hex",
|
|
2864
|
+
message: `Raw hex color "${value}" in color field "${fieldName ?? "<color context>"}". Use declared theme tokens instead. Available: ${ladder.join(", ")}`,
|
|
2865
|
+
file: relPath,
|
|
2866
|
+
path: path5,
|
|
2867
|
+
value
|
|
2868
|
+
});
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
if (LEGACY_OSS_TOKENS.has(value)) {
|
|
2872
|
+
violations.push({
|
|
2873
|
+
type: "legacy_vocab",
|
|
2874
|
+
message: `Legacy status token "${value}" in color field "${fieldName ?? "<color context>"}". This project's ladder is: ${ladder.join(", ")}. ` + LEGACY_MAPPING_HINT,
|
|
2875
|
+
file: relPath,
|
|
2876
|
+
path: path5,
|
|
2877
|
+
value
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
return violations;
|
|
2882
|
+
}
|
|
2883
|
+
function findContentFiles(pagesRoot) {
|
|
2884
|
+
if (!existsSync7(pagesRoot)) return [];
|
|
2885
|
+
const files = [];
|
|
2886
|
+
try {
|
|
2887
|
+
const slugDirs = readdirSync3(pagesRoot, { withFileTypes: true });
|
|
2888
|
+
for (const entry of slugDirs) {
|
|
2889
|
+
if (!entry.isDirectory()) continue;
|
|
2890
|
+
const slugDir = join6(pagesRoot, entry.name);
|
|
2891
|
+
for (const candidate of ["content.yml", "content.yaml"]) {
|
|
2892
|
+
const p = join6(slugDir, candidate);
|
|
2893
|
+
if (existsSync7(p)) files.push(p);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
} catch {
|
|
2897
|
+
}
|
|
2898
|
+
return files;
|
|
2899
|
+
}
|
|
2900
|
+
function validateThemeTokenUsage(projectRoot) {
|
|
2901
|
+
const ladder = resolveStatusLadder(projectRoot);
|
|
2902
|
+
if (ladder.length === 0) {
|
|
2903
|
+
return {
|
|
2904
|
+
valid: true,
|
|
2905
|
+
violations: [],
|
|
2906
|
+
skipped: true
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
const ladderSet = new Set(ladder);
|
|
2910
|
+
const pagesRoot = join6(projectRoot, "pages");
|
|
2911
|
+
const contentFiles = findContentFiles(pagesRoot);
|
|
2912
|
+
const allViolations = [];
|
|
2913
|
+
for (const absPath of contentFiles) {
|
|
2914
|
+
const relPath = relative2(projectRoot, absPath);
|
|
2915
|
+
try {
|
|
2916
|
+
const raw = readFileSync6(absPath, "utf-8");
|
|
2917
|
+
const parsed = yamlLoad2(raw);
|
|
2918
|
+
const fileViolations = detectViolationsInYaml(parsed, ladder, ladderSet, relPath);
|
|
2919
|
+
allViolations.push(...fileViolations);
|
|
2920
|
+
} catch {
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
if (allViolations.length === 0) {
|
|
2924
|
+
return { valid: true, violations: [] };
|
|
2925
|
+
}
|
|
2926
|
+
const lines = [
|
|
2927
|
+
`Theme token enforcement found ${allViolations.length} violation(s):`,
|
|
2928
|
+
"",
|
|
2929
|
+
...allViolations.map((v, i) => {
|
|
2930
|
+
const typeLabel = v.type === "undefined_token" ? "UNDEFINED TOKEN" : v.type === "raw_hex" ? "RAW HEX" : "LEGACY VOCAB";
|
|
2931
|
+
return ` ${i + 1}. [${typeLabel}] ${v.message}
|
|
2932
|
+
Emitted in: ${v.file} at ${v.path}`;
|
|
2933
|
+
}),
|
|
2934
|
+
"",
|
|
2935
|
+
`Fix all violations to use declared theme tokens: ${ladder.join(", ")}`
|
|
2936
|
+
];
|
|
2937
|
+
return {
|
|
2938
|
+
valid: false,
|
|
2939
|
+
violations: allViolations,
|
|
2940
|
+
retryPrompt: lines.join("\n")
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// src/tools/validate-build-context-globals.ts
|
|
2945
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
|
|
2946
|
+
import { join as join7, relative as relative3 } from "path";
|
|
2947
|
+
import { load as yamlLoad3 } from "js-yaml";
|
|
2948
|
+
var DATA_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
2949
|
+
"data_table_pulse",
|
|
2950
|
+
"stats_grid",
|
|
2951
|
+
"metric_card_pulse",
|
|
2952
|
+
"map_pulse"
|
|
2953
|
+
]);
|
|
2954
|
+
function readGlobalDefaults(projectRoot) {
|
|
2955
|
+
const bcPath = join7(projectRoot, ".stackwright", "build-context.json");
|
|
2956
|
+
if (!existsSync8(bcPath)) return null;
|
|
2957
|
+
try {
|
|
2958
|
+
const raw = readFileSync7(bcPath, "utf-8");
|
|
2959
|
+
const parsed = JSON.parse(raw);
|
|
2960
|
+
const gd = parsed.globalDefaults;
|
|
2961
|
+
if (!gd || typeof gd !== "object") return null;
|
|
2962
|
+
return gd;
|
|
2963
|
+
} catch {
|
|
2964
|
+
return null;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
function findDataWidgets(node, pathParts, out) {
|
|
2968
|
+
if (node === null || node === void 0 || typeof node !== "object") return;
|
|
2969
|
+
if (Array.isArray(node)) {
|
|
2970
|
+
node.forEach((item, idx) => {
|
|
2971
|
+
findDataWidgets(item, [...pathParts, `[${idx}]`], out);
|
|
2972
|
+
});
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
const obj = node;
|
|
2976
|
+
const contentType = typeof obj["type"] === "string" ? obj["type"] : null;
|
|
2977
|
+
if (contentType && DATA_WIDGET_TYPES.has(contentType)) {
|
|
2978
|
+
out.push({
|
|
2979
|
+
type: contentType,
|
|
2980
|
+
label: typeof obj["label"] === "string" ? obj["label"] : void 0,
|
|
2981
|
+
data: obj,
|
|
2982
|
+
path: pathParts.join(".") || "(root)"
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
2986
|
+
findDataWidgets(val, [...pathParts, key], out);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
function findContentFiles2(pagesRoot) {
|
|
2990
|
+
if (!existsSync8(pagesRoot)) return [];
|
|
2991
|
+
const files = [];
|
|
2992
|
+
try {
|
|
2993
|
+
const slugDirs = readdirSync4(pagesRoot, { withFileTypes: true });
|
|
2994
|
+
for (const entry of slugDirs) {
|
|
2995
|
+
if (!entry.isDirectory()) continue;
|
|
2996
|
+
for (const candidate of ["content.yml", "content.yaml"]) {
|
|
2997
|
+
const p = join7(pagesRoot, entry.name, candidate);
|
|
2998
|
+
if (existsSync8(p)) files.push(p);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
} catch {
|
|
3002
|
+
}
|
|
3003
|
+
return files;
|
|
3004
|
+
}
|
|
3005
|
+
function validateBuildContextGlobals(projectRoot) {
|
|
3006
|
+
const globalDefaults = readGlobalDefaults(projectRoot);
|
|
3007
|
+
if (!globalDefaults) {
|
|
3008
|
+
return { valid: true, violations: [], skipped: true };
|
|
3009
|
+
}
|
|
3010
|
+
const requiredFields = Object.entries(globalDefaults).filter(([, v]) => v === true).map(([k]) => k);
|
|
3011
|
+
if (requiredFields.length === 0) {
|
|
3012
|
+
return { valid: true, violations: [], skipped: true };
|
|
3013
|
+
}
|
|
3014
|
+
const pagesRoot = join7(projectRoot, "pages");
|
|
3015
|
+
const contentFiles = findContentFiles2(pagesRoot);
|
|
3016
|
+
const allViolations = [];
|
|
3017
|
+
for (const absPath of contentFiles) {
|
|
3018
|
+
const relPath = relative3(projectRoot, absPath);
|
|
3019
|
+
try {
|
|
3020
|
+
const raw = readFileSync7(absPath, "utf-8");
|
|
3021
|
+
const parsed = yamlLoad3(raw);
|
|
3022
|
+
const widgets = [];
|
|
3023
|
+
findDataWidgets(parsed, [], widgets);
|
|
3024
|
+
for (const widget of widgets) {
|
|
3025
|
+
for (const field of requiredFields) {
|
|
3026
|
+
if (widget.data[field] !== true) {
|
|
3027
|
+
allViolations.push({
|
|
3028
|
+
field,
|
|
3029
|
+
widgetType: widget.type,
|
|
3030
|
+
widgetLabel: widget.label,
|
|
3031
|
+
file: relPath,
|
|
3032
|
+
path: widget.path
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
} catch {
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
if (allViolations.length === 0) {
|
|
3041
|
+
return { valid: true, violations: [] };
|
|
3042
|
+
}
|
|
3043
|
+
const lines = [
|
|
3044
|
+
`BUILD_CONTEXT global defaults: ${allViolations.length} violation(s) found.`,
|
|
3045
|
+
"",
|
|
3046
|
+
...allViolations.map((v, i) => {
|
|
3047
|
+
const widgetId = v.widgetLabel ? `"${v.widgetLabel}" (${v.widgetType})` : `${v.widgetType}`;
|
|
3048
|
+
return ` ${i + 1}. BUILD_CONTEXT.${v.field} is globally true but widget ${widgetId} omits it.
|
|
3049
|
+
Set ${v.field}: true on this widget.
|
|
3050
|
+
Emitted in: ${v.file} at ${v.path}`;
|
|
3051
|
+
}),
|
|
3052
|
+
"",
|
|
3053
|
+
`Required global defaults: ${requiredFields.map((f) => `${f}: true`).join(", ")}`
|
|
3054
|
+
];
|
|
3055
|
+
return {
|
|
3056
|
+
valid: false,
|
|
3057
|
+
violations: allViolations,
|
|
3058
|
+
retryPrompt: lines.join("\n")
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// src/tools/validate-workflow-container-pages.ts
|
|
3063
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "fs";
|
|
3064
|
+
import { join as join8 } from "path";
|
|
3065
|
+
import { load as yamlLoad4 } from "js-yaml";
|
|
3066
|
+
function discoverWorkflowIds(projectRoot) {
|
|
3067
|
+
const artifactsDir = join8(projectRoot, ".stackwright", "artifacts");
|
|
3068
|
+
if (!existsSync9(artifactsDir)) return [];
|
|
3069
|
+
const entries = [];
|
|
3070
|
+
let files;
|
|
3071
|
+
try {
|
|
3072
|
+
files = readdirSync5(artifactsDir);
|
|
3073
|
+
} catch {
|
|
3074
|
+
return [];
|
|
3075
|
+
}
|
|
3076
|
+
for (const filename of files) {
|
|
3077
|
+
if (filename !== "workflow-config.json" && !filename.startsWith("workflow-config-")) continue;
|
|
3078
|
+
if (!filename.endsWith(".json")) continue;
|
|
3079
|
+
const fullPath = join8(artifactsDir, filename);
|
|
3080
|
+
try {
|
|
3081
|
+
const raw = JSON.parse(readFileSync8(fullPath, "utf-8"));
|
|
3082
|
+
const workflowData = raw["workflow"] ?? raw["workflowConfig"];
|
|
3083
|
+
const workflowId = typeof workflowData?.["id"] === "string" ? workflowData["id"] : null;
|
|
3084
|
+
if (workflowId && /^[a-z0-9-]+$/.test(workflowId)) {
|
|
3085
|
+
entries.push({ workflowId, sourceFile: filename });
|
|
3086
|
+
}
|
|
3087
|
+
} catch {
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
return entries;
|
|
3091
|
+
}
|
|
3092
|
+
function hasWorkflowWizardType(filePath) {
|
|
3093
|
+
try {
|
|
3094
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
3095
|
+
const parsed = yamlLoad4(raw);
|
|
3096
|
+
if (!parsed) return false;
|
|
3097
|
+
const content = parsed["content"];
|
|
3098
|
+
if (!content) return false;
|
|
3099
|
+
const items = content["content_items"];
|
|
3100
|
+
if (!Array.isArray(items)) return false;
|
|
3101
|
+
return items.some((item) => item["type"] === "workflow_wizard");
|
|
3102
|
+
} catch {
|
|
3103
|
+
return false;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
function validateWorkflowContainerPages(projectRoot) {
|
|
3107
|
+
const workflows = discoverWorkflowIds(projectRoot);
|
|
3108
|
+
if (workflows.length === 0) {
|
|
3109
|
+
return { valid: true, violations: [], skipped: true };
|
|
3110
|
+
}
|
|
3111
|
+
const violations = [];
|
|
3112
|
+
for (const { workflowId, sourceFile: _sourceFile } of workflows) {
|
|
3113
|
+
const expectedPath = `pages/workflows/${workflowId}/content.yml`;
|
|
3114
|
+
const fullPath = join8(projectRoot, expectedPath);
|
|
3115
|
+
if (!existsSync9(fullPath)) {
|
|
3116
|
+
violations.push({ workflowId, expectedPath, reason: "missing-file" });
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
if (!hasWorkflowWizardType(fullPath)) {
|
|
3120
|
+
violations.push({ workflowId, expectedPath, reason: "missing-workflow-wizard-type" });
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (violations.length === 0) {
|
|
3124
|
+
return { valid: true, violations: [] };
|
|
3125
|
+
}
|
|
3126
|
+
const lines = [
|
|
3127
|
+
`Workflow container page cross-check failed: ${violations.length} workflow(s) missing container pages.`,
|
|
3128
|
+
"",
|
|
3129
|
+
...violations.map((v, i) => {
|
|
3130
|
+
const reason = v.reason === "missing-file" ? `File does not exist. Create it via stackwright_pro_write_workflow_container_page.` : `File exists but has no workflow_wizard content item. Must contain type: workflow_wizard.`;
|
|
3131
|
+
return ` ${i + 1}. workflowId "${v.workflowId}"
|
|
3132
|
+
Expected: ${v.expectedPath}
|
|
3133
|
+
Issue: ${reason}`;
|
|
3134
|
+
}),
|
|
3135
|
+
"",
|
|
3136
|
+
"Fix: workflow-otter must call stackwright_pro_write_workflow_container_page immediately",
|
|
3137
|
+
"after writing each workflow YAML. Required args:",
|
|
3138
|
+
' { workflowId, workflowFile, pageSlug: "workflows/<workflowId>", label, requiredRoles }',
|
|
3139
|
+
"",
|
|
3140
|
+
"Without the container page, the route 404s and auth-reconcile cannot protect it."
|
|
3141
|
+
];
|
|
3142
|
+
return {
|
|
3143
|
+
valid: false,
|
|
3144
|
+
violations,
|
|
3145
|
+
retryPrompt: lines.join("\n")
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
|
|
2554
3149
|
// src/tools/validate-api-integration.ts
|
|
2555
3150
|
var CANONICAL_API_AUTH_TYPES = ["apiKey", "bearer", "oauth2", "oidc", "none"];
|
|
2556
3151
|
var CANONICAL_SET = new Set(CANONICAL_API_AUTH_TYPES);
|
|
@@ -2743,9 +3338,9 @@ function validateAuthConfig(artifact, artifactPath) {
|
|
|
2743
3338
|
}
|
|
2744
3339
|
|
|
2745
3340
|
// src/tools/auth-manifest-aggregator.ts
|
|
2746
|
-
import { existsSync as
|
|
2747
|
-
import { join as
|
|
2748
|
-
import { z as
|
|
3341
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync6 } from "fs";
|
|
3342
|
+
import { join as join9 } from "path";
|
|
3343
|
+
import { z as z12 } from "zod";
|
|
2749
3344
|
function computeLowestRole(requiredRoles, rbacRoles) {
|
|
2750
3345
|
let lowestIndex = -1;
|
|
2751
3346
|
let lowestRole = requiredRoles[0];
|
|
@@ -2765,17 +3360,17 @@ function extractPatternBaseSlug(pattern) {
|
|
|
2765
3360
|
}
|
|
2766
3361
|
function extractManifestSlugs(artifactsDir) {
|
|
2767
3362
|
const slugs = /* @__PURE__ */ new Set();
|
|
2768
|
-
if (!
|
|
3363
|
+
if (!existsSync10(artifactsDir)) return slugs;
|
|
2769
3364
|
let entries;
|
|
2770
3365
|
try {
|
|
2771
|
-
entries =
|
|
3366
|
+
entries = readdirSync6(artifactsDir);
|
|
2772
3367
|
} catch {
|
|
2773
3368
|
return slugs;
|
|
2774
3369
|
}
|
|
2775
3370
|
for (const entry of entries) {
|
|
2776
3371
|
if (!entry.endsWith("-manifest.json")) continue;
|
|
2777
3372
|
try {
|
|
2778
|
-
const content = JSON.parse(
|
|
3373
|
+
const content = JSON.parse(readFileSync9(join9(artifactsDir, entry), "utf-8"));
|
|
2779
3374
|
const pages = content.pages;
|
|
2780
3375
|
if (!Array.isArray(pages)) continue;
|
|
2781
3376
|
for (const page of pages) {
|
|
@@ -2818,18 +3413,18 @@ function deriveProtectedRoutes(pages, rbacRoles) {
|
|
|
2818
3413
|
}
|
|
2819
3414
|
function readAllManifestPages(artifactsDir) {
|
|
2820
3415
|
const allPages = [];
|
|
2821
|
-
if (!
|
|
3416
|
+
if (!existsSync10(artifactsDir)) return allPages;
|
|
2822
3417
|
let entries;
|
|
2823
3418
|
try {
|
|
2824
|
-
entries =
|
|
3419
|
+
entries = readdirSync6(artifactsDir);
|
|
2825
3420
|
} catch {
|
|
2826
3421
|
return allPages;
|
|
2827
3422
|
}
|
|
2828
3423
|
for (const entry of entries) {
|
|
2829
3424
|
if (entry.endsWith("-manifest.json")) {
|
|
2830
|
-
const filePath =
|
|
3425
|
+
const filePath = join9(artifactsDir, entry);
|
|
2831
3426
|
try {
|
|
2832
|
-
const raw = JSON.parse(
|
|
3427
|
+
const raw = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
2833
3428
|
if (!Array.isArray(raw.pages)) continue;
|
|
2834
3429
|
for (const page of raw.pages) {
|
|
2835
3430
|
if (typeof page.slug !== "string" || !page.slug) continue;
|
|
@@ -2845,9 +3440,9 @@ function readAllManifestPages(artifactsDir) {
|
|
|
2845
3440
|
continue;
|
|
2846
3441
|
}
|
|
2847
3442
|
if (entry === "workflow-config.json" || entry.startsWith("workflow-config-")) {
|
|
2848
|
-
const filePath =
|
|
3443
|
+
const filePath = join9(artifactsDir, entry);
|
|
2849
3444
|
try {
|
|
2850
|
-
const raw = JSON.parse(
|
|
3445
|
+
const raw = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
2851
3446
|
const route = raw.workflow?.route;
|
|
2852
3447
|
if (typeof route !== "string" || !route) continue;
|
|
2853
3448
|
const slug = route.startsWith("/") ? route.slice(1) : route;
|
|
@@ -2869,16 +3464,16 @@ function registerReadAuthManifestsTool(server2) {
|
|
|
2869
3464
|
"stackwright_pro_read_auth_manifests",
|
|
2870
3465
|
"Read ALL route-producing artifacts from the pipeline artifacts directory: *-manifest.json files (pages, dashboard, geo, form-wizard) AND workflow-config.json / workflow-config-*.json (different shape: workflow.route). Deterministically derives protectedRoutes and uncoveredSlugs. Uses the both-forms invariant (swp-n7kk): every authRequired route registers BOTH /{slug} (exact) AND /{slug}/:path* (sub-path). Workflow routes appear in uncoveredSlugs (no requiredRoles declared) for auth-reconcile to resolve via cross-cutting rules from auth-config.json. Extended by swp-hng0 to cover all four manifest types (was: pages+dashboard only).",
|
|
2871
3466
|
{
|
|
2872
|
-
rbacRoles:
|
|
3467
|
+
rbacRoles: z12.array(z12.string()).describe(
|
|
2873
3468
|
'RBAC role hierarchy in descending privilege order, e.g. ["ESF8_COORDINATOR", "HOSPITAL_EM", "OBSERVER"]. Used to compute the lowestRole for each authRequired page.'
|
|
2874
3469
|
),
|
|
2875
|
-
artifactsDir:
|
|
3470
|
+
artifactsDir: z12.string().optional().describe(
|
|
2876
3471
|
"Absolute or relative path to the pipeline artifacts directory. Defaults to .stackwright/artifacts relative to cwd."
|
|
2877
3472
|
)
|
|
2878
3473
|
},
|
|
2879
3474
|
(params) => {
|
|
2880
3475
|
const cwd = process.cwd();
|
|
2881
|
-
const artifactsDir = params.artifactsDir ? params.artifactsDir :
|
|
3476
|
+
const artifactsDir = params.artifactsDir ? params.artifactsDir : join9(cwd, ".stackwright", "artifacts");
|
|
2882
3477
|
const allPages = readAllManifestPages(artifactsDir);
|
|
2883
3478
|
const { protectedRoutes, uncoveredSlugs } = deriveProtectedRoutes(allPages, params.rbacRoles);
|
|
2884
3479
|
const manifestsRead = [...new Set(allPages.map((p) => p.source))].sort();
|
|
@@ -2901,195 +3496,11 @@ function registerReadAuthManifestsTool(server2) {
|
|
|
2901
3496
|
}
|
|
2902
3497
|
|
|
2903
3498
|
// src/tools/pipeline.ts
|
|
2904
|
-
import { WorkflowFileSchema, authConfigSchema as
|
|
3499
|
+
import { WorkflowFileSchema, authConfigSchema as authConfigSchema2 } from "@stackwright-pro/types";
|
|
2905
3500
|
|
|
2906
3501
|
// src/tools/get-schema.ts
|
|
2907
3502
|
import { z as z13 } from "zod";
|
|
2908
|
-
|
|
2909
|
-
WorkflowStepSchema as WorkflowStepSchema2,
|
|
2910
|
-
WorkflowDefinitionSchema as WorkflowDefinitionSchema2,
|
|
2911
|
-
WorkflowFieldSchema as WorkflowFieldSchema2,
|
|
2912
|
-
WorkflowActionSchema as WorkflowActionSchema2,
|
|
2913
|
-
authConfigSchema as authConfigSchema2
|
|
2914
|
-
} from "@stackwright-pro/types";
|
|
2915
|
-
|
|
2916
|
-
// src/tools/validate-yaml-fragment.ts
|
|
2917
|
-
import { z as z12 } from "zod";
|
|
2918
|
-
import { load as yamlLoad2 } from "js-yaml";
|
|
2919
|
-
import {
|
|
2920
|
-
WorkflowStepSchema,
|
|
2921
|
-
WorkflowDefinitionSchema,
|
|
2922
|
-
WorkflowFieldSchema,
|
|
2923
|
-
WorkflowActionSchema,
|
|
2924
|
-
authConfigSchema
|
|
2925
|
-
} from "@stackwright-pro/types";
|
|
2926
|
-
var SUPPORTED_SCHEMA_NAMES = [
|
|
2927
|
-
"workflow_step",
|
|
2928
|
-
"workflow_definition",
|
|
2929
|
-
"workflow_field",
|
|
2930
|
-
"workflow_action",
|
|
2931
|
-
"navigation_item",
|
|
2932
|
-
"auth_config"
|
|
2933
|
-
];
|
|
2934
|
-
var NavigationLinkSchema = z12.lazy(
|
|
2935
|
-
() => z12.object({
|
|
2936
|
-
label: z12.string().min(1),
|
|
2937
|
-
href: z12.string().min(1),
|
|
2938
|
-
children: z12.array(NavigationLinkSchema).optional()
|
|
2939
|
-
})
|
|
2940
|
-
);
|
|
2941
|
-
var NavigationSectionSchema = z12.object({
|
|
2942
|
-
section: z12.string().min(1),
|
|
2943
|
-
items: z12.array(NavigationLinkSchema)
|
|
2944
|
-
});
|
|
2945
|
-
var NavigationItemSchema = z12.union([
|
|
2946
|
-
NavigationLinkSchema,
|
|
2947
|
-
NavigationSectionSchema
|
|
2948
|
-
]);
|
|
2949
|
-
var FIELD_SYNONYMS = {
|
|
2950
|
-
workflow_step: {
|
|
2951
|
-
title: "label",
|
|
2952
|
-
name: "label (at step level, use label: not name:)",
|
|
2953
|
-
description: "message (use message: for step-level descriptive text)",
|
|
2954
|
-
style: 'theme.variant \u2014 use theme: { variant: "primary" } not style: "primary"',
|
|
2955
|
-
transitions: 'on_submit.transition \u2014 use on_submit: { transition: "next_step" } not transitions: [{then:...}]'
|
|
2956
|
-
},
|
|
2957
|
-
workflow_definition: {
|
|
2958
|
-
title: "label",
|
|
2959
|
-
name: "label (workflow-level label, not name:)",
|
|
2960
|
-
description: "description is valid here \u2014 but message: is not (that is a step field)",
|
|
2961
|
-
type: "id (workflows are identified by id:, not type:)"
|
|
2962
|
-
},
|
|
2963
|
-
workflow_field: {
|
|
2964
|
-
id: "name (field-level identifier is name:, NOT id:)",
|
|
2965
|
-
title: "label (field display text is label:, NOT title:)",
|
|
2966
|
-
description: "placeholder or label (no description field on WorkflowField)"
|
|
2967
|
-
},
|
|
2968
|
-
workflow_action: {
|
|
2969
|
-
style: 'theme.variant \u2014 use theme: { variant: "primary" } not style: "primary"',
|
|
2970
|
-
then: "transition (action-level next-step is transition:, NOT then:)",
|
|
2971
|
-
on_click: 'action: "service:..." (extract the service reference from on_click.action)',
|
|
2972
|
-
title: "label (action display text is label:, NOT title:)",
|
|
2973
|
-
name: "label (action display text is label:, NOT name:)"
|
|
2974
|
-
},
|
|
2975
|
-
navigation_item: {
|
|
2976
|
-
children: "items (NavigationSection uses items: not children: \u2014 also use section: instead of label: for section headers)",
|
|
2977
|
-
label: 'section (if this is a nav group/section, use section: "Title" and items: [...], not label:)',
|
|
2978
|
-
title: "section (NavigationSection header field is section:, NOT title:)"
|
|
2979
|
-
},
|
|
2980
|
-
auth_config: {
|
|
2981
|
-
method: 'type \u2014 auth config discriminates on type: "oidc" | "pki", NOT method:',
|
|
2982
|
-
provider_type: 'type \u2014 use type: "oidc" or type: "pki"',
|
|
2983
|
-
strategy: "type \u2014 top-level discriminator is type:, not strategy:"
|
|
2984
|
-
}
|
|
2985
|
-
};
|
|
2986
|
-
function getSchema(name) {
|
|
2987
|
-
switch (name) {
|
|
2988
|
-
case "workflow_step":
|
|
2989
|
-
return WorkflowStepSchema;
|
|
2990
|
-
case "workflow_definition":
|
|
2991
|
-
return WorkflowDefinitionSchema;
|
|
2992
|
-
case "workflow_field":
|
|
2993
|
-
return WorkflowFieldSchema;
|
|
2994
|
-
case "workflow_action":
|
|
2995
|
-
return WorkflowActionSchema;
|
|
2996
|
-
case "navigation_item":
|
|
2997
|
-
return NavigationItemSchema;
|
|
2998
|
-
case "auth_config":
|
|
2999
|
-
return authConfigSchema;
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3002
|
-
function enrichErrors(issues, synonyms) {
|
|
3003
|
-
return issues.map((issue) => {
|
|
3004
|
-
const pathStr = issue.path.join(".");
|
|
3005
|
-
const lastSegment = issue.path[issue.path.length - 1];
|
|
3006
|
-
const fieldName = typeof lastSegment === "string" ? lastSegment : void 0;
|
|
3007
|
-
const didYouMean = fieldName ? synonyms[fieldName] : void 0;
|
|
3008
|
-
return {
|
|
3009
|
-
path: pathStr || "(root)",
|
|
3010
|
-
message: issue.message,
|
|
3011
|
-
...didYouMean ? { didYouMean } : {}
|
|
3012
|
-
};
|
|
3013
|
-
});
|
|
3014
|
-
}
|
|
3015
|
-
function handleValidateYamlFragment(input) {
|
|
3016
|
-
const { schemaName, yaml: yaml4 } = input;
|
|
3017
|
-
if (!SUPPORTED_SCHEMA_NAMES.includes(schemaName)) {
|
|
3018
|
-
return {
|
|
3019
|
-
valid: false,
|
|
3020
|
-
parseError: `Unknown schemaName: "${schemaName}". Supported: ${SUPPORTED_SCHEMA_NAMES.join(", ")}`
|
|
3021
|
-
};
|
|
3022
|
-
}
|
|
3023
|
-
const name = schemaName;
|
|
3024
|
-
let parsed;
|
|
3025
|
-
try {
|
|
3026
|
-
parsed = yamlLoad2(yaml4);
|
|
3027
|
-
} catch (err) {
|
|
3028
|
-
return {
|
|
3029
|
-
valid: false,
|
|
3030
|
-
parseError: `YAML parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
3031
|
-
};
|
|
3032
|
-
}
|
|
3033
|
-
const schema = getSchema(name);
|
|
3034
|
-
const result = schema.safeParse(parsed);
|
|
3035
|
-
if (result.success) {
|
|
3036
|
-
return { valid: true };
|
|
3037
|
-
}
|
|
3038
|
-
const synonyms = FIELD_SYNONYMS[name];
|
|
3039
|
-
const errors = enrichErrors(result.error.issues, synonyms);
|
|
3040
|
-
return { valid: false, errors };
|
|
3041
|
-
}
|
|
3042
|
-
function registerValidateYamlFragmentTool(server2) {
|
|
3043
|
-
server2.tool(
|
|
3044
|
-
"stackwright_pro_validate_yaml_fragment",
|
|
3045
|
-
'Validates a YAML snippet against a canonical Zod schema before writing to disk. Returns actionable errors with "did you mean?" suggestions for common field-name drift (title vs label, id vs name, style vs theme.variant, etc.). Call this before stackwright_pro_safe_write to catch schema violations in-session.',
|
|
3046
|
-
{
|
|
3047
|
-
schemaName: z12.enum(SUPPORTED_SCHEMA_NAMES).describe(
|
|
3048
|
-
"Schema to validate against. One of: workflow_step, workflow_definition, workflow_field, workflow_action, navigation_item, auth_config"
|
|
3049
|
-
),
|
|
3050
|
-
yaml: z12.string().describe("YAML string to validate (can also be JSON \u2014 js-yaml parses both)")
|
|
3051
|
-
},
|
|
3052
|
-
async ({ schemaName, yaml: yaml4 }) => {
|
|
3053
|
-
const result = handleValidateYamlFragment({ schemaName, yaml: yaml4 });
|
|
3054
|
-
return {
|
|
3055
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3056
|
-
};
|
|
3057
|
-
}
|
|
3058
|
-
);
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
// src/tools/get-schema.ts
|
|
3062
|
-
var NavigationLinkSchema2 = z13.lazy(
|
|
3063
|
-
() => z13.object({
|
|
3064
|
-
label: z13.string().min(1),
|
|
3065
|
-
href: z13.string().min(1),
|
|
3066
|
-
children: z13.array(NavigationLinkSchema2).optional()
|
|
3067
|
-
})
|
|
3068
|
-
);
|
|
3069
|
-
var NavigationSectionSchema2 = z13.object({
|
|
3070
|
-
section: z13.string().min(1),
|
|
3071
|
-
items: z13.array(NavigationLinkSchema2)
|
|
3072
|
-
});
|
|
3073
|
-
var NavigationItemSchema2 = z13.union([
|
|
3074
|
-
NavigationLinkSchema2,
|
|
3075
|
-
NavigationSectionSchema2
|
|
3076
|
-
]);
|
|
3077
|
-
function getZodSchema(name) {
|
|
3078
|
-
switch (name) {
|
|
3079
|
-
case "workflow_step":
|
|
3080
|
-
return WorkflowStepSchema2;
|
|
3081
|
-
case "workflow_definition":
|
|
3082
|
-
return WorkflowDefinitionSchema2;
|
|
3083
|
-
case "workflow_field":
|
|
3084
|
-
return WorkflowFieldSchema2;
|
|
3085
|
-
case "workflow_action":
|
|
3086
|
-
return WorkflowActionSchema2;
|
|
3087
|
-
case "navigation_item":
|
|
3088
|
-
return NavigationItemSchema2;
|
|
3089
|
-
case "auth_config":
|
|
3090
|
-
return authConfigSchema2;
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3503
|
+
var PHASE_SCHEMA_NAMES = buildPhaseSchemaNames();
|
|
3093
3504
|
function formatType(node, depth = 0) {
|
|
3094
3505
|
if (node.const !== void 0) return JSON.stringify(node.const);
|
|
3095
3506
|
if (node.enum) return node.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
@@ -3116,7 +3527,7 @@ function formatType(node, depth = 0) {
|
|
|
3116
3527
|
if (node.maximum !== void 0) constraints.push(`max ${node.maximum}`);
|
|
3117
3528
|
return constraints.length > 0 ? `${base} (${constraints.join(", ")})` : base;
|
|
3118
3529
|
}
|
|
3119
|
-
function formatObjectSchema(node, schemaName,
|
|
3530
|
+
function formatObjectSchema(node, schemaName, reverseSynonyms) {
|
|
3120
3531
|
const lines = [];
|
|
3121
3532
|
const displayName = schemaName.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
3122
3533
|
lines.push(`${displayName}:`);
|
|
@@ -3130,7 +3541,8 @@ function formatObjectSchema(node, schemaName, synonyms) {
|
|
|
3130
3541
|
for (const [field, fieldNode] of Object.entries(branch.properties ?? {})) {
|
|
3131
3542
|
if (fieldNode.const !== void 0) continue;
|
|
3132
3543
|
const isRequired = req.has(field);
|
|
3133
|
-
const
|
|
3544
|
+
const correctFor = reverseSynonyms[field];
|
|
3545
|
+
const hint = correctFor ? ` -- WARNING: use ${correctFor}: not ${field}:` : "";
|
|
3134
3546
|
lines.push(
|
|
3135
3547
|
` ${isRequired ? "REQUIRED" : "optional"}: ${field}: ${formatType(fieldNode)}${hint}`
|
|
3136
3548
|
);
|
|
@@ -3138,6 +3550,12 @@ function formatObjectSchema(node, schemaName, synonyms) {
|
|
|
3138
3550
|
}
|
|
3139
3551
|
return lines.join("\n");
|
|
3140
3552
|
}
|
|
3553
|
+
if (!node.properties && !node.oneOf && !node.anyOf) {
|
|
3554
|
+
const typeStr = formatType(node);
|
|
3555
|
+
lines.push(` type: ${typeStr}`);
|
|
3556
|
+
if (node.description) lines.push(` description: ${node.description}`);
|
|
3557
|
+
return lines.join("\n");
|
|
3558
|
+
}
|
|
3141
3559
|
const required = new Set(node.required ?? []);
|
|
3142
3560
|
const properties = node.properties ?? {};
|
|
3143
3561
|
const requiredFields = Object.entries(properties).filter(([k]) => required.has(k));
|
|
@@ -3146,8 +3564,7 @@ function formatObjectSchema(node, schemaName, synonyms) {
|
|
|
3146
3564
|
lines.push(" REQUIRED:");
|
|
3147
3565
|
for (const [field, fieldNode] of requiredFields) {
|
|
3148
3566
|
const typeStr = formatType(fieldNode);
|
|
3149
|
-
|
|
3150
|
-
lines.push(` ${field}: ${typeStr}${warn}`);
|
|
3567
|
+
lines.push(` ${field}: ${typeStr}`);
|
|
3151
3568
|
}
|
|
3152
3569
|
}
|
|
3153
3570
|
if (optionalFields.length > 0) {
|
|
@@ -3161,24 +3578,30 @@ function formatObjectSchema(node, schemaName, synonyms) {
|
|
|
3161
3578
|
}
|
|
3162
3579
|
function handleGetSchema(input) {
|
|
3163
3580
|
const { schemaName } = input;
|
|
3164
|
-
|
|
3581
|
+
const entry = SCHEMA_REGISTRY.get(schemaName);
|
|
3582
|
+
if (!entry) {
|
|
3165
3583
|
return {
|
|
3166
|
-
error: `Unknown schemaName: "${schemaName}". Supported: ${
|
|
3584
|
+
error: `Unknown schemaName: "${schemaName}". Supported: ${REGISTRY_SCHEMA_NAMES.join(", ")}`
|
|
3167
3585
|
};
|
|
3168
3586
|
}
|
|
3169
|
-
const
|
|
3170
|
-
const
|
|
3171
|
-
const synonyms
|
|
3587
|
+
const synonyms = entry.synonyms ?? {};
|
|
3588
|
+
const reverseSynonyms = {};
|
|
3589
|
+
for (const [correct, wrongs] of Object.entries(synonyms)) {
|
|
3590
|
+
for (const wrong of wrongs) {
|
|
3591
|
+
reverseSynonyms[wrong] = correct;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
const ctx = input.projectRoot ? { projectRoot: input.projectRoot } : void 0;
|
|
3595
|
+
const resolvedSchema = getSchemaForEntry(entry, ctx);
|
|
3172
3596
|
let jsonSchema;
|
|
3173
3597
|
try {
|
|
3174
|
-
|
|
3175
|
-
jsonSchema = raw;
|
|
3598
|
+
jsonSchema = z13.toJSONSchema(resolvedSchema, { unrepresentable: "any" });
|
|
3176
3599
|
} catch {
|
|
3177
3600
|
jsonSchema = { type: "object", description: "Schema introspection unavailable for this type" };
|
|
3178
3601
|
}
|
|
3179
|
-
const summary = formatObjectSchema(jsonSchema,
|
|
3602
|
+
const summary = formatObjectSchema(jsonSchema, schemaName, reverseSynonyms);
|
|
3180
3603
|
const synonymWarnings = Object.entries(synonyms).map(
|
|
3181
|
-
([
|
|
3604
|
+
([correct, wrongs]) => ` WARNING: use "${correct}" \u2014 NOT ${wrongs.map((w) => `"${w}"`).join(" or ")}`
|
|
3182
3605
|
);
|
|
3183
3606
|
const fullText = [
|
|
3184
3607
|
summary,
|
|
@@ -3188,19 +3611,12 @@ function handleGetSchema(input) {
|
|
|
3188
3611
|
].join("\n");
|
|
3189
3612
|
const tokenEstimate = Math.ceil(fullText.length / 4);
|
|
3190
3613
|
return {
|
|
3191
|
-
schemaName
|
|
3614
|
+
schemaName,
|
|
3192
3615
|
summary,
|
|
3193
3616
|
synonymWarnings,
|
|
3194
3617
|
tokenEstimate
|
|
3195
3618
|
};
|
|
3196
3619
|
}
|
|
3197
|
-
var PHASE_SCHEMA_NAMES = {
|
|
3198
|
-
workflow: ["workflow_step", "workflow_definition", "workflow_field", "workflow_action"],
|
|
3199
|
-
pages: ["navigation_item"],
|
|
3200
|
-
dashboard: ["navigation_item"],
|
|
3201
|
-
polish: ["navigation_item"],
|
|
3202
|
-
auth: ["auth_config"]
|
|
3203
|
-
};
|
|
3204
3620
|
function buildSchemaReferenceBlock(phase) {
|
|
3205
3621
|
const schemaNames = PHASE_SCHEMA_NAMES[phase];
|
|
3206
3622
|
if (!schemaNames || schemaNames.length === 0) return null;
|
|
@@ -3225,12 +3641,13 @@ function registerGetSchemaTool(server2) {
|
|
|
3225
3641
|
"stackwright_pro_get_schema",
|
|
3226
3642
|
'Returns a compact, LLM-friendly summary of a canonical schema derived programmatically from Zod. Includes required/optional fields, types, constraints, and "did you mean?" warnings for common field-name drift. Use before authoring YAML to confirm exact field names.',
|
|
3227
3643
|
{
|
|
3228
|
-
schemaName: z13.
|
|
3229
|
-
|
|
3644
|
+
schemaName: z13.string().describe(`Schema to summarize. Available: ${REGISTRY_SCHEMA_NAMES.join(", ")}`),
|
|
3645
|
+
projectRoot: z13.string().optional().describe(
|
|
3646
|
+
"Absolute path to the project root. Required for factory schemas like theme_status_token. Defaults to cwd."
|
|
3230
3647
|
)
|
|
3231
3648
|
},
|
|
3232
|
-
async ({ schemaName }) => {
|
|
3233
|
-
const result = handleGetSchema({ schemaName });
|
|
3649
|
+
async ({ schemaName, projectRoot }) => {
|
|
3650
|
+
const result = handleGetSchema({ schemaName, projectRoot: projectRoot ?? process.cwd() });
|
|
3234
3651
|
if ("error" in result) {
|
|
3235
3652
|
return {
|
|
3236
3653
|
content: [{ type: "text", text: JSON.stringify({ error: result.error }) }],
|
|
@@ -3250,8 +3667,8 @@ function registerGetSchemaTool(server2) {
|
|
|
3250
3667
|
}
|
|
3251
3668
|
|
|
3252
3669
|
// src/tools/pipeline-graph.ts
|
|
3253
|
-
import { readFileSync as
|
|
3254
|
-
import { join as
|
|
3670
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync7, lstatSync as lstatSync5 } from "fs";
|
|
3671
|
+
import { join as join10 } from "path";
|
|
3255
3672
|
var MANIFEST_NAME_TO_PHASE = {
|
|
3256
3673
|
"stackwright-pro-designer-otter": "designer",
|
|
3257
3674
|
"stackwright-pro-theme-otter": "theme",
|
|
@@ -3283,8 +3700,8 @@ var OTTER_SEARCH_PATHS = [
|
|
|
3283
3700
|
];
|
|
3284
3701
|
function resolveOtterDirFromCwd() {
|
|
3285
3702
|
const cwd = process.cwd();
|
|
3286
|
-
for (const
|
|
3287
|
-
const candidate =
|
|
3703
|
+
for (const relative4 of OTTER_SEARCH_PATHS) {
|
|
3704
|
+
const candidate = join10(cwd, relative4);
|
|
3288
3705
|
try {
|
|
3289
3706
|
lstatSync5(candidate);
|
|
3290
3707
|
return candidate;
|
|
@@ -3296,14 +3713,14 @@ function resolveOtterDirFromCwd() {
|
|
|
3296
3713
|
);
|
|
3297
3714
|
}
|
|
3298
3715
|
function loadOtterManifestsFromDir(otterDir) {
|
|
3299
|
-
const entries =
|
|
3716
|
+
const entries = readdirSync7(otterDir);
|
|
3300
3717
|
const manifests = [];
|
|
3301
3718
|
for (const filename of entries) {
|
|
3302
3719
|
if (!filename.endsWith("-otter.json")) continue;
|
|
3303
|
-
const filePath =
|
|
3720
|
+
const filePath = join10(otterDir, filename);
|
|
3304
3721
|
if (lstatSync5(filePath).isSymbolicLink()) continue;
|
|
3305
3722
|
try {
|
|
3306
|
-
const raw =
|
|
3723
|
+
const raw = readFileSync10(filePath, "utf-8");
|
|
3307
3724
|
const manifest = JSON.parse(raw);
|
|
3308
3725
|
manifests.push(manifest);
|
|
3309
3726
|
} catch (err) {
|
|
@@ -3408,19 +3825,19 @@ var DISTRIBUTED_NAMESPACES = ["@stackwright-pro", "@stackwright"];
|
|
|
3408
3825
|
function loadDistributedOtterManifests(cwd) {
|
|
3409
3826
|
const results = [];
|
|
3410
3827
|
for (const namespace of DISTRIBUTED_NAMESPACES) {
|
|
3411
|
-
const nsDir =
|
|
3828
|
+
const nsDir = join10(cwd, "node_modules", namespace);
|
|
3412
3829
|
let pkgDirNames;
|
|
3413
3830
|
try {
|
|
3414
|
-
pkgDirNames =
|
|
3831
|
+
pkgDirNames = readdirSync7(nsDir);
|
|
3415
3832
|
} catch {
|
|
3416
3833
|
continue;
|
|
3417
3834
|
}
|
|
3418
3835
|
for (const pkgDirName of pkgDirNames) {
|
|
3419
|
-
const pkgDir =
|
|
3836
|
+
const pkgDir = join10(nsDir, pkgDirName);
|
|
3420
3837
|
const packageName = `${namespace}/${pkgDirName}`;
|
|
3421
3838
|
let otterRelPaths;
|
|
3422
3839
|
try {
|
|
3423
|
-
const raw =
|
|
3840
|
+
const raw = readFileSync10(join10(pkgDir, "package.json"), "utf-8");
|
|
3424
3841
|
const pkgJson = JSON.parse(raw);
|
|
3425
3842
|
const declared = pkgJson.stackwright?.otters;
|
|
3426
3843
|
if (!Array.isArray(declared) || declared.length === 0) continue;
|
|
@@ -3429,9 +3846,9 @@ function loadDistributedOtterManifests(cwd) {
|
|
|
3429
3846
|
continue;
|
|
3430
3847
|
}
|
|
3431
3848
|
for (const relPath of otterRelPaths) {
|
|
3432
|
-
const otterFilePath =
|
|
3849
|
+
const otterFilePath = join10(pkgDir, relPath);
|
|
3433
3850
|
try {
|
|
3434
|
-
const raw =
|
|
3851
|
+
const raw = readFileSync10(otterFilePath, "utf-8");
|
|
3435
3852
|
const manifest = JSON.parse(raw);
|
|
3436
3853
|
results.push({ manifest, packageName });
|
|
3437
3854
|
} catch (err) {
|
|
@@ -3576,18 +3993,18 @@ function createDefaultState() {
|
|
|
3576
3993
|
};
|
|
3577
3994
|
}
|
|
3578
3995
|
function statePath(cwd) {
|
|
3579
|
-
return
|
|
3996
|
+
return join11(cwd, ".stackwright", "pipeline-state.json");
|
|
3580
3997
|
}
|
|
3581
3998
|
function readState(cwd) {
|
|
3582
3999
|
const p = statePath(cwd);
|
|
3583
|
-
if (!
|
|
4000
|
+
if (!existsSync11(p)) return createDefaultState();
|
|
3584
4001
|
const raw = JSON.parse(safeReadSync(p));
|
|
3585
4002
|
const result = PipelineStateSchema.safeParse(raw);
|
|
3586
4003
|
if (!result.success) return createDefaultState();
|
|
3587
4004
|
return result.data;
|
|
3588
4005
|
}
|
|
3589
4006
|
function safeWriteSync(filePath, content) {
|
|
3590
|
-
if (
|
|
4007
|
+
if (existsSync11(filePath)) {
|
|
3591
4008
|
const stat = lstatSync6(filePath);
|
|
3592
4009
|
if (stat.isSymbolicLink()) {
|
|
3593
4010
|
throw new Error(`Refusing to write to symlink: ${filePath}`);
|
|
@@ -3596,25 +4013,25 @@ function safeWriteSync(filePath, content) {
|
|
|
3596
4013
|
writeFileSync4(filePath, content);
|
|
3597
4014
|
}
|
|
3598
4015
|
function safeReadSync(filePath) {
|
|
3599
|
-
if (
|
|
4016
|
+
if (existsSync11(filePath)) {
|
|
3600
4017
|
const stat = lstatSync6(filePath);
|
|
3601
4018
|
if (stat.isSymbolicLink()) {
|
|
3602
4019
|
throw new Error(`Refusing to read symlink: ${filePath}`);
|
|
3603
4020
|
}
|
|
3604
4021
|
}
|
|
3605
|
-
return
|
|
4022
|
+
return readFileSync11(filePath, "utf-8");
|
|
3606
4023
|
}
|
|
3607
4024
|
function writeState(cwd, state) {
|
|
3608
|
-
const dir =
|
|
4025
|
+
const dir = join11(cwd, ".stackwright");
|
|
3609
4026
|
mkdirSync4(dir, { recursive: true });
|
|
3610
4027
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3611
4028
|
safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
3612
4029
|
}
|
|
3613
4030
|
function updateStateAtomic(cwd, mutator) {
|
|
3614
|
-
const dir =
|
|
4031
|
+
const dir = join11(cwd, ".stackwright");
|
|
3615
4032
|
mkdirSync4(dir, { recursive: true });
|
|
3616
4033
|
const path5 = statePath(cwd);
|
|
3617
|
-
if (!
|
|
4034
|
+
if (!existsSync11(path5)) {
|
|
3618
4035
|
safeWriteSync(path5, JSON.stringify(createDefaultState(), null, 2) + "\n");
|
|
3619
4036
|
}
|
|
3620
4037
|
const release = lockSync(path5, {
|
|
@@ -3648,8 +4065,8 @@ function handleGetPipelineState(_cwd) {
|
|
|
3648
4065
|
const cwd = _cwd ?? process.cwd();
|
|
3649
4066
|
try {
|
|
3650
4067
|
const state = readState(cwd);
|
|
3651
|
-
const keyPath =
|
|
3652
|
-
if (!
|
|
4068
|
+
const keyPath = join11(cwd, ".stackwright", "pipeline-keys.json");
|
|
4069
|
+
if (!existsSync11(keyPath)) {
|
|
3653
4070
|
try {
|
|
3654
4071
|
const { fingerprint } = initPipelineKeys(cwd);
|
|
3655
4072
|
state["signingKeyFingerprint"] = fingerprint;
|
|
@@ -3788,8 +4205,8 @@ function handleCheckExecutionReady(_cwd, phase) {
|
|
|
3788
4205
|
if (AUTO_EXECUTE_PHASES.has(phase)) {
|
|
3789
4206
|
return { text: JSON.stringify({ ready: true, phase, autoExecute: true }), isError: false };
|
|
3790
4207
|
}
|
|
3791
|
-
const answerFile =
|
|
3792
|
-
if (!
|
|
4208
|
+
const answerFile = join11(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
4209
|
+
if (!existsSync11(answerFile)) {
|
|
3793
4210
|
return {
|
|
3794
4211
|
text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
|
|
3795
4212
|
isError: false
|
|
@@ -3813,7 +4230,7 @@ function handleCheckExecutionReady(_cwd, phase) {
|
|
|
3813
4230
|
}
|
|
3814
4231
|
}
|
|
3815
4232
|
try {
|
|
3816
|
-
const answersDir =
|
|
4233
|
+
const answersDir = join11(cwd, ".stackwright", "answers");
|
|
3817
4234
|
const answeredPhases = [];
|
|
3818
4235
|
const missingPhases = [];
|
|
3819
4236
|
for (const phase2 of PHASE_ORDER) {
|
|
@@ -3821,8 +4238,8 @@ function handleCheckExecutionReady(_cwd, phase) {
|
|
|
3821
4238
|
answeredPhases.push(phase2);
|
|
3822
4239
|
continue;
|
|
3823
4240
|
}
|
|
3824
|
-
const answerFile =
|
|
3825
|
-
if (
|
|
4241
|
+
const answerFile = join11(answersDir, `${phase2}.json`);
|
|
4242
|
+
if (existsSync11(answerFile)) {
|
|
3826
4243
|
try {
|
|
3827
4244
|
const raw = safeReadSync(answerFile);
|
|
3828
4245
|
const parsed = JSON.parse(raw);
|
|
@@ -3891,7 +4308,7 @@ function handleGetReadyPhases(_cwd) {
|
|
|
3891
4308
|
function handleListArtifacts(_cwd) {
|
|
3892
4309
|
const cwd = _cwd ?? process.cwd();
|
|
3893
4310
|
try {
|
|
3894
|
-
const artifactsDir =
|
|
4311
|
+
const artifactsDir = join11(cwd, ".stackwright", "artifacts");
|
|
3895
4312
|
let manifest = null;
|
|
3896
4313
|
try {
|
|
3897
4314
|
manifest = loadSignatureManifest(cwd);
|
|
@@ -3901,8 +4318,8 @@ function handleListArtifacts(_cwd) {
|
|
|
3901
4318
|
let completedCount = 0;
|
|
3902
4319
|
for (const phase of PHASE_ORDER) {
|
|
3903
4320
|
const expectedFile = PHASE_ARTIFACT[phase];
|
|
3904
|
-
const fullPath =
|
|
3905
|
-
const exists =
|
|
4321
|
+
const fullPath = join11(artifactsDir, expectedFile);
|
|
4322
|
+
const exists = existsSync11(fullPath);
|
|
3906
4323
|
if (exists) completedCount++;
|
|
3907
4324
|
let signed = false;
|
|
3908
4325
|
let signatureValid = null;
|
|
@@ -3955,9 +4372,9 @@ function handleWritePhaseQuestions(input) {
|
|
|
3955
4372
|
}
|
|
3956
4373
|
} catch {
|
|
3957
4374
|
}
|
|
3958
|
-
const questionsDir =
|
|
4375
|
+
const questionsDir = join11(cwd, ".stackwright", "questions");
|
|
3959
4376
|
mkdirSync4(questionsDir, { recursive: true });
|
|
3960
|
-
const filePath =
|
|
4377
|
+
const filePath = join11(questionsDir, `${phase}.json`);
|
|
3961
4378
|
const payload = {
|
|
3962
4379
|
version: "1.0",
|
|
3963
4380
|
phase,
|
|
@@ -3997,14 +4414,14 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
3997
4414
|
};
|
|
3998
4415
|
}
|
|
3999
4416
|
try {
|
|
4000
|
-
const answersPath =
|
|
4417
|
+
const answersPath = join11(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
4001
4418
|
let answers = {};
|
|
4002
|
-
if (
|
|
4419
|
+
if (existsSync11(answersPath)) {
|
|
4003
4420
|
answers = JSON.parse(safeReadSync(answersPath));
|
|
4004
4421
|
}
|
|
4005
4422
|
let buildContextText = "";
|
|
4006
|
-
const buildContextPath =
|
|
4007
|
-
if (
|
|
4423
|
+
const buildContextPath = join11(cwd, ".stackwright", "build-context.json");
|
|
4424
|
+
if (existsSync11(buildContextPath)) {
|
|
4008
4425
|
try {
|
|
4009
4426
|
const bcRaw = JSON.parse(safeReadSync(buildContextPath));
|
|
4010
4427
|
if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
|
|
@@ -4019,8 +4436,8 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
4019
4436
|
const missingDependencies = [];
|
|
4020
4437
|
for (const dep of deps) {
|
|
4021
4438
|
const artifactFile = PHASE_ARTIFACT[dep];
|
|
4022
|
-
const artifactPath =
|
|
4023
|
-
if (
|
|
4439
|
+
const artifactPath = join11(cwd, ".stackwright", "artifacts", artifactFile);
|
|
4440
|
+
if (existsSync11(artifactPath)) {
|
|
4024
4441
|
const rawContent = safeReadSync(artifactPath);
|
|
4025
4442
|
const rawBytes = Buffer.from(rawContent, "utf-8");
|
|
4026
4443
|
const content = JSON.parse(rawContent);
|
|
@@ -4085,8 +4502,8 @@ ${JSON.stringify(content, null, 2)}`
|
|
|
4085
4502
|
parts.push("BUILD_CONTEXT:", buildContextText, "");
|
|
4086
4503
|
}
|
|
4087
4504
|
if (phase === "auth") {
|
|
4088
|
-
const initContextPath =
|
|
4089
|
-
if (
|
|
4505
|
+
const initContextPath = join11(cwd, ".stackwright", "init-context.json");
|
|
4506
|
+
if (existsSync11(initContextPath)) {
|
|
4090
4507
|
try {
|
|
4091
4508
|
const initContext = JSON.parse(safeReadSync(initContextPath));
|
|
4092
4509
|
if (initContext.devOnly === true || initContext.nonInteractive === true) {
|
|
@@ -4227,7 +4644,12 @@ var PHASE_ARTIFACT_SCHEMA = {
|
|
|
4227
4644
|
"--surface": "210 40% 98%",
|
|
4228
4645
|
"--border": "214.3 31.8% 91.4%"
|
|
4229
4646
|
},
|
|
4230
|
-
dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
|
|
4647
|
+
dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" },
|
|
4648
|
+
// Optional — include when design language has status semantics (swp-cxwu).
|
|
4649
|
+
// Array of status-* token names emitted in tokens.colors.
|
|
4650
|
+
// Consumed by validate_artifact theme enforcement to reject unknown status tokens.
|
|
4651
|
+
// OMIT if project has no status-coded data.
|
|
4652
|
+
statusLadder: ["status-ok", "status-warning", "status-error", "status-info"]
|
|
4231
4653
|
},
|
|
4232
4654
|
null,
|
|
4233
4655
|
2
|
|
@@ -4665,7 +5087,7 @@ function _validateArtifactInner(input) {
|
|
|
4665
5087
|
auth: (artifact2) => {
|
|
4666
5088
|
const authConfig = artifact2["authConfig"];
|
|
4667
5089
|
if (!authConfig) return { success: true };
|
|
4668
|
-
const result =
|
|
5090
|
+
const result = authConfigSchema2.safeParse(authConfig);
|
|
4669
5091
|
if (!result.success) {
|
|
4670
5092
|
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
4671
5093
|
return { success: false, error: { message: issues } };
|
|
@@ -4697,21 +5119,21 @@ function _validateArtifactInner(input) {
|
|
|
4697
5119
|
// state-machine in this services-config artifact.
|
|
4698
5120
|
// Skips gracefully when wizard-otter didn't run (Pro-only install) or declared no hooks.
|
|
4699
5121
|
services: (artifact2) => {
|
|
4700
|
-
const artifactsDir =
|
|
5122
|
+
const artifactsDir = join11(cwd, ".stackwright", "artifacts");
|
|
4701
5123
|
const workflowArtifactFiles = [];
|
|
4702
|
-
if (
|
|
5124
|
+
if (existsSync11(artifactsDir)) {
|
|
4703
5125
|
try {
|
|
4704
|
-
const entries =
|
|
5126
|
+
const entries = readdirSync8(artifactsDir);
|
|
4705
5127
|
for (const entry of entries) {
|
|
4706
5128
|
if (entry.startsWith("workflow-config-") && entry.endsWith(".json")) {
|
|
4707
|
-
workflowArtifactFiles.push(
|
|
5129
|
+
workflowArtifactFiles.push(join11(artifactsDir, entry));
|
|
4708
5130
|
}
|
|
4709
5131
|
}
|
|
4710
5132
|
} catch {
|
|
4711
5133
|
}
|
|
4712
5134
|
}
|
|
4713
|
-
const legacyPath =
|
|
4714
|
-
if (
|
|
5135
|
+
const legacyPath = join11(artifactsDir, "workflow-config.json");
|
|
5136
|
+
if (existsSync11(legacyPath)) {
|
|
4715
5137
|
workflowArtifactFiles.push(legacyPath);
|
|
4716
5138
|
}
|
|
4717
5139
|
if (workflowArtifactFiles.length === 0) {
|
|
@@ -4720,7 +5142,7 @@ function _validateArtifactInner(input) {
|
|
|
4720
5142
|
const declaredHooks = [];
|
|
4721
5143
|
for (const filePath of workflowArtifactFiles) {
|
|
4722
5144
|
try {
|
|
4723
|
-
const parsed = JSON.parse(
|
|
5145
|
+
const parsed = JSON.parse(readFileSync11(filePath, "utf-8"));
|
|
4724
5146
|
if (Array.isArray(parsed.serviceHooks)) {
|
|
4725
5147
|
declaredHooks.push(...parsed.serviceHooks);
|
|
4726
5148
|
}
|
|
@@ -4810,6 +5232,42 @@ function _validateArtifactInner(input) {
|
|
|
4810
5232
|
return { text: JSON.stringify(result), isError: false };
|
|
4811
5233
|
}
|
|
4812
5234
|
}
|
|
5235
|
+
if (phase === "pages" || phase === "dashboard" || phase === "geo") {
|
|
5236
|
+
const themeResult = validateThemeTokenUsage(cwd);
|
|
5237
|
+
if (!themeResult.valid && !themeResult.skipped) {
|
|
5238
|
+
const result = {
|
|
5239
|
+
valid: false,
|
|
5240
|
+
phase,
|
|
5241
|
+
violation: "schema-mismatch",
|
|
5242
|
+
retryPrompt: themeResult.retryPrompt
|
|
5243
|
+
};
|
|
5244
|
+
return { text: JSON.stringify(result), isError: false };
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
if (phase === "pages" || phase === "dashboard" || phase === "geo") {
|
|
5248
|
+
const globalsResult = validateBuildContextGlobals(cwd);
|
|
5249
|
+
if (!globalsResult.valid && !globalsResult.skipped) {
|
|
5250
|
+
const result = {
|
|
5251
|
+
valid: false,
|
|
5252
|
+
phase,
|
|
5253
|
+
violation: "schema-mismatch",
|
|
5254
|
+
retryPrompt: globalsResult.retryPrompt
|
|
5255
|
+
};
|
|
5256
|
+
return { text: JSON.stringify(result), isError: false };
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
if (phase === "pages") {
|
|
5260
|
+
const workflowCrossCheck = validateWorkflowContainerPages(cwd);
|
|
5261
|
+
if (!workflowCrossCheck.valid) {
|
|
5262
|
+
const result = {
|
|
5263
|
+
valid: false,
|
|
5264
|
+
phase,
|
|
5265
|
+
violation: "schema-mismatch",
|
|
5266
|
+
retryPrompt: workflowCrossCheck.retryPrompt
|
|
5267
|
+
};
|
|
5268
|
+
return { text: JSON.stringify(result), isError: false };
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
4813
5271
|
if (phase === "api") {
|
|
4814
5272
|
const apiAuthResult = validateApiIntegration(artifact, input.integrationName);
|
|
4815
5273
|
if (!apiAuthResult.valid) {
|
|
@@ -4838,7 +5296,7 @@ function _validateArtifactInner(input) {
|
|
|
4838
5296
|
const authCfg = artifact["authConfig"];
|
|
4839
5297
|
const routes = authCfg?.["protectedRoutes"];
|
|
4840
5298
|
if (Array.isArray(routes) && routes.length > 0) {
|
|
4841
|
-
const artifactsDir =
|
|
5299
|
+
const artifactsDir = join11(cwd, ".stackwright", "artifacts");
|
|
4842
5300
|
const knownSlugs = extractManifestSlugs(artifactsDir);
|
|
4843
5301
|
const orphanPatterns = [];
|
|
4844
5302
|
for (const route of routes) {
|
|
@@ -4855,10 +5313,10 @@ function _validateArtifactInner(input) {
|
|
|
4855
5313
|
}
|
|
4856
5314
|
}
|
|
4857
5315
|
try {
|
|
4858
|
-
const artifactsDir =
|
|
5316
|
+
const artifactsDir = join11(cwd, ".stackwright", "artifacts");
|
|
4859
5317
|
mkdirSync4(artifactsDir, { recursive: true });
|
|
4860
5318
|
const artifactFile = phase === "api" && input.integrationName ? `api-config-${input.integrationName}.json` : phase === "workflow" && input.workflowName ? `workflow-config-${input.workflowName}.json` : PHASE_ARTIFACT[phase];
|
|
4861
|
-
const artifactPath =
|
|
5319
|
+
const artifactPath = join11(artifactsDir, artifactFile);
|
|
4862
5320
|
const serialized = JSON.stringify(artifact, null, 2) + "\n";
|
|
4863
5321
|
const artifactBytes = Buffer.from(serialized, "utf-8");
|
|
4864
5322
|
safeWriteSync(artifactPath, serialized);
|
|
@@ -5088,9 +5546,9 @@ function registerPipelineTools(server2) {
|
|
|
5088
5546
|
isError: true
|
|
5089
5547
|
};
|
|
5090
5548
|
}
|
|
5091
|
-
const questionsDir =
|
|
5549
|
+
const questionsDir = join11(process.cwd(), ".stackwright", "questions");
|
|
5092
5550
|
mkdirSync4(questionsDir, { recursive: true });
|
|
5093
|
-
const outPath =
|
|
5551
|
+
const outPath = join11(questionsDir, `${phase}.json`);
|
|
5094
5552
|
writeFileSync4(
|
|
5095
5553
|
outPath,
|
|
5096
5554
|
JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
@@ -5183,8 +5641,8 @@ function registerPipelineTools(server2) {
|
|
|
5183
5641
|
|
|
5184
5642
|
// src/tools/safe-write.ts
|
|
5185
5643
|
import { z as z15 } from "zod";
|
|
5186
|
-
import { writeFileSync as writeFileSync5, readFileSync as
|
|
5187
|
-
import { normalize, isAbsolute, dirname, join as
|
|
5644
|
+
import { writeFileSync as writeFileSync5, readFileSync as readFileSync12, existsSync as existsSync12, mkdirSync as mkdirSync5, lstatSync as lstatSync7 } from "fs";
|
|
5645
|
+
import { normalize, isAbsolute, dirname, join as join12 } from "path";
|
|
5188
5646
|
import { emit as emit2 } from "@stackwright-pro/telemetry";
|
|
5189
5647
|
var OTTER_WRITE_ALLOWLISTS = {
|
|
5190
5648
|
"stackwright-pro-designer-otter": [
|
|
@@ -5261,7 +5719,18 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
5261
5719
|
"stackwright-pro-form-wizard-otter": [
|
|
5262
5720
|
{ prefix: "workflows/", suffix: ".yml", description: "Workflow definition" },
|
|
5263
5721
|
{ prefix: "workflows/", suffix: ".yaml", description: "Workflow definition" },
|
|
5264
|
-
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" }
|
|
5722
|
+
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" },
|
|
5723
|
+
// swp-dvde: workflow-otter owns the container page to prevent the 404 gap
|
|
5724
|
+
{
|
|
5725
|
+
prefix: "pages/workflows/",
|
|
5726
|
+
suffix: "/content.yml",
|
|
5727
|
+
description: "Workflow container page content (swp-dvde)"
|
|
5728
|
+
},
|
|
5729
|
+
{
|
|
5730
|
+
prefix: "pages/workflows/",
|
|
5731
|
+
suffix: "/content.yaml",
|
|
5732
|
+
description: "Workflow container page content (swp-dvde)"
|
|
5733
|
+
}
|
|
5265
5734
|
],
|
|
5266
5735
|
"stackwright-pro-scaffold-otter": [
|
|
5267
5736
|
{ prefix: "app/", suffix: ".tsx", description: "Next.js App Router shell files" },
|
|
@@ -5396,21 +5865,21 @@ function extractContentTypes(yamlContent) {
|
|
|
5396
5865
|
return types.length > 0 ? types : ["unknown"];
|
|
5397
5866
|
}
|
|
5398
5867
|
function readPageRegistry(cwd) {
|
|
5399
|
-
const regPath =
|
|
5400
|
-
if (!
|
|
5868
|
+
const regPath = join12(cwd, PAGE_REGISTRY_FILE);
|
|
5869
|
+
if (!existsSync12(regPath)) return {};
|
|
5401
5870
|
try {
|
|
5402
5871
|
const stat = lstatSync7(regPath);
|
|
5403
5872
|
if (stat.isSymbolicLink()) return {};
|
|
5404
|
-
return JSON.parse(
|
|
5873
|
+
return JSON.parse(readFileSync12(regPath, "utf-8"));
|
|
5405
5874
|
} catch {
|
|
5406
5875
|
return {};
|
|
5407
5876
|
}
|
|
5408
5877
|
}
|
|
5409
5878
|
function writePageRegistry(cwd, registry) {
|
|
5410
|
-
const dir =
|
|
5879
|
+
const dir = join12(cwd, ".stackwright");
|
|
5411
5880
|
mkdirSync5(dir, { recursive: true });
|
|
5412
|
-
const regPath =
|
|
5413
|
-
if (
|
|
5881
|
+
const regPath = join12(cwd, PAGE_REGISTRY_FILE);
|
|
5882
|
+
if (existsSync12(regPath)) {
|
|
5414
5883
|
const stat = lstatSync7(regPath);
|
|
5415
5884
|
if (stat.isSymbolicLink()) {
|
|
5416
5885
|
throw new Error("Refusing to write page registry through symlink");
|
|
@@ -5582,8 +6051,8 @@ function _safeWriteInner(input) {
|
|
|
5582
6051
|
return { text: JSON.stringify(result), isError: true };
|
|
5583
6052
|
}
|
|
5584
6053
|
const normalized = normalize(filePath);
|
|
5585
|
-
const fullPath =
|
|
5586
|
-
if (
|
|
6054
|
+
const fullPath = join12(cwd, normalized);
|
|
6055
|
+
if (existsSync12(fullPath)) {
|
|
5587
6056
|
try {
|
|
5588
6057
|
const stat = lstatSync7(fullPath);
|
|
5589
6058
|
if (stat.isSymbolicLink()) {
|
|
@@ -5713,8 +6182,8 @@ function registerSafeWriteTools(server2) {
|
|
|
5713
6182
|
|
|
5714
6183
|
// src/tools/auth.ts
|
|
5715
6184
|
import { z as z16 } from "zod";
|
|
5716
|
-
import { readFileSync as
|
|
5717
|
-
import { join as
|
|
6185
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync6, existsSync as existsSync13, mkdirSync as mkdirSync6 } from "fs";
|
|
6186
|
+
import { join as join13 } from "path";
|
|
5718
6187
|
function normalizeProviderSlug(slug) {
|
|
5719
6188
|
return slug.replace(/-/g, "_");
|
|
5720
6189
|
}
|
|
@@ -5744,9 +6213,9 @@ ${indent} requiredRole: ${r.requiredRole}`).join("\n");
|
|
|
5744
6213
|
}
|
|
5745
6214
|
function detectNextMajorVersion(cwd) {
|
|
5746
6215
|
try {
|
|
5747
|
-
const pkgPath =
|
|
5748
|
-
if (!
|
|
5749
|
-
const pkg = JSON.parse(
|
|
6216
|
+
const pkgPath = join13(cwd, "package.json");
|
|
6217
|
+
if (!existsSync13(pkgPath)) return null;
|
|
6218
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf8"));
|
|
5750
6219
|
const nextVersion = pkg.dependencies?.next ?? pkg.devDependencies?.next;
|
|
5751
6220
|
if (!nextVersion) return null;
|
|
5752
6221
|
const match = nextVersion.match(/(\d+)/);
|
|
@@ -6408,7 +6877,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
6408
6877
|
auditRetentionDays,
|
|
6409
6878
|
normalizedRoutes
|
|
6410
6879
|
);
|
|
6411
|
-
writeFileSync6(
|
|
6880
|
+
writeFileSync6(join13(cwd, conventionFile), authFileContent, "utf8");
|
|
6412
6881
|
filesWritten.push(conventionFile);
|
|
6413
6882
|
} catch (err) {
|
|
6414
6883
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -6428,9 +6897,9 @@ async function configureAuthHandler(params, cwd) {
|
|
|
6428
6897
|
if (!devOnly) {
|
|
6429
6898
|
try {
|
|
6430
6899
|
const envBlock = generateEnvBlock(effectiveMethod, params);
|
|
6431
|
-
const envPath =
|
|
6432
|
-
if (
|
|
6433
|
-
const existing =
|
|
6900
|
+
const envPath = join13(cwd, ".env.example");
|
|
6901
|
+
if (existsSync13(envPath)) {
|
|
6902
|
+
const existing = readFileSync13(envPath, "utf8");
|
|
6434
6903
|
writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
6435
6904
|
} else {
|
|
6436
6905
|
writeFileSync6(envPath, envBlock, "utf8");
|
|
@@ -6451,10 +6920,10 @@ async function configureAuthHandler(params, cwd) {
|
|
|
6451
6920
|
}
|
|
6452
6921
|
if (devOnly) {
|
|
6453
6922
|
try {
|
|
6454
|
-
const mockAuthDir =
|
|
6923
|
+
const mockAuthDir = join13(cwd, "lib");
|
|
6455
6924
|
mkdirSync6(mockAuthDir, { recursive: true });
|
|
6456
6925
|
const mockAuthContent = generateMockAuthContent(roles, mockUsers);
|
|
6457
|
-
writeFileSync6(
|
|
6926
|
+
writeFileSync6(join13(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
|
|
6458
6927
|
filesWritten.push("lib/mock-auth.ts");
|
|
6459
6928
|
} catch (err) {
|
|
6460
6929
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -6472,9 +6941,9 @@ async function configureAuthHandler(params, cwd) {
|
|
|
6472
6941
|
};
|
|
6473
6942
|
}
|
|
6474
6943
|
try {
|
|
6475
|
-
const pkgPath =
|
|
6476
|
-
if (
|
|
6477
|
-
const existingPkg =
|
|
6944
|
+
const pkgPath = join13(cwd, "package.json");
|
|
6945
|
+
if (existsSync13(pkgPath)) {
|
|
6946
|
+
const existingPkg = readFileSync13(pkgPath, "utf8");
|
|
6478
6947
|
const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
|
|
6479
6948
|
writeFileSync6(pkgPath, updatedPkg, "utf8");
|
|
6480
6949
|
filesWritten.push("package.json");
|
|
@@ -6517,7 +6986,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
6517
6986
|
normalizedRoutes,
|
|
6518
6987
|
useProxy
|
|
6519
6988
|
);
|
|
6520
|
-
writeFileSync6(
|
|
6989
|
+
writeFileSync6(join13(cwd, "stackwright.auth.yml"), authYaml, "utf8");
|
|
6521
6990
|
filesWritten.push("stackwright.auth.yml");
|
|
6522
6991
|
} catch (err) {
|
|
6523
6992
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -6631,8 +7100,8 @@ function registerAuthTools(server2) {
|
|
|
6631
7100
|
|
|
6632
7101
|
// src/integrity.ts
|
|
6633
7102
|
import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
6634
|
-
import { readFileSync as
|
|
6635
|
-
import { join as
|
|
7103
|
+
import { readFileSync as readFileSync14, readdirSync as readdirSync9, lstatSync as lstatSync8 } from "fs";
|
|
7104
|
+
import { join as join14, basename } from "path";
|
|
6636
7105
|
var _checksums = /* @__PURE__ */ new Map([
|
|
6637
7106
|
[
|
|
6638
7107
|
"stackwright-pro-api-otter.json",
|
|
@@ -6648,7 +7117,7 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
6648
7117
|
],
|
|
6649
7118
|
[
|
|
6650
7119
|
"stackwright-pro-dashboard-otter.json",
|
|
6651
|
-
"
|
|
7120
|
+
"ac0a049da72ee20bfb711f640bac571263bf2b05f80cd22a0c2eb5778c40989c"
|
|
6652
7121
|
],
|
|
6653
7122
|
[
|
|
6654
7123
|
"stackwright-pro-data-otter.json",
|
|
@@ -6668,15 +7137,15 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
6668
7137
|
],
|
|
6669
7138
|
[
|
|
6670
7139
|
"stackwright-pro-form-wizard-otter.json",
|
|
6671
|
-
"
|
|
7140
|
+
"941bb11b767530460f96fdd08af638b3ede9f7a1ae6f19de04b7d81c95cc62ad"
|
|
6672
7141
|
],
|
|
6673
7142
|
[
|
|
6674
7143
|
"stackwright-pro-geo-otter.json",
|
|
6675
|
-
"
|
|
7144
|
+
"bee276605a3ae3203f5b6a18476316f615cf0ea6f4c73ecf57dcd627417de7f3"
|
|
6676
7145
|
],
|
|
6677
7146
|
[
|
|
6678
7147
|
"stackwright-pro-page-otter.json",
|
|
6679
|
-
"
|
|
7148
|
+
"8c04dd8a14c6e1aa9b0b6685a71a657547c20d99c25448e3523f73d670f87a4a"
|
|
6680
7149
|
],
|
|
6681
7150
|
[
|
|
6682
7151
|
"stackwright-pro-polish-otter.json",
|
|
@@ -6692,7 +7161,7 @@ var _checksums = /* @__PURE__ */ new Map([
|
|
|
6692
7161
|
],
|
|
6693
7162
|
[
|
|
6694
7163
|
"stackwright-pro-theme-otter.json",
|
|
6695
|
-
"
|
|
7164
|
+
"e3cb6ef9d9f7cbf662a26f637f3d244ec33cb19cb6a4a55dcd148cbfdaa4d71e"
|
|
6696
7165
|
],
|
|
6697
7166
|
[
|
|
6698
7167
|
"stackwright-services-otter.json",
|
|
@@ -6744,7 +7213,7 @@ function verifyOtterFile(filePath) {
|
|
|
6744
7213
|
}
|
|
6745
7214
|
let raw;
|
|
6746
7215
|
try {
|
|
6747
|
-
raw =
|
|
7216
|
+
raw = readFileSync14(filePath);
|
|
6748
7217
|
} catch (err) {
|
|
6749
7218
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6750
7219
|
return { verified: false, filename, error: `Cannot read file: ${msg}` };
|
|
@@ -6794,7 +7263,7 @@ function verifyAllOtters(otterDir) {
|
|
|
6794
7263
|
const unknown = [];
|
|
6795
7264
|
let entries;
|
|
6796
7265
|
try {
|
|
6797
|
-
entries =
|
|
7266
|
+
entries = readdirSync9(otterDir);
|
|
6798
7267
|
} catch (err) {
|
|
6799
7268
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6800
7269
|
return {
|
|
@@ -6805,7 +7274,7 @@ function verifyAllOtters(otterDir) {
|
|
|
6805
7274
|
}
|
|
6806
7275
|
const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
|
|
6807
7276
|
for (const filename of otterFiles) {
|
|
6808
|
-
const filePath =
|
|
7277
|
+
const filePath = join14(otterDir, filename);
|
|
6809
7278
|
try {
|
|
6810
7279
|
if (lstatSync8(filePath).isSymbolicLink()) {
|
|
6811
7280
|
failed.push({ filename, error: "Skipped: symlink" });
|
|
@@ -6832,8 +7301,8 @@ function verifyAllOtters(otterDir) {
|
|
|
6832
7301
|
var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packages/otters/src/"];
|
|
6833
7302
|
function resolveOtterDir() {
|
|
6834
7303
|
const cwd = process.cwd();
|
|
6835
|
-
for (const
|
|
6836
|
-
const candidate =
|
|
7304
|
+
for (const relative4 of DEFAULT_SEARCH_PATHS) {
|
|
7305
|
+
const candidate = join14(cwd, relative4);
|
|
6837
7306
|
try {
|
|
6838
7307
|
lstatSync8(candidate);
|
|
6839
7308
|
return candidate;
|
|
@@ -6914,13 +7383,13 @@ function registerIntegrityTools(server2) {
|
|
|
6914
7383
|
|
|
6915
7384
|
// src/tools/domain.ts
|
|
6916
7385
|
import { z as z17 } from "zod";
|
|
6917
|
-
import { readFileSync as
|
|
6918
|
-
import { join as
|
|
7386
|
+
import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
|
|
7387
|
+
import { join as join15 } from "path";
|
|
6919
7388
|
function handleListCollections(input) {
|
|
6920
7389
|
const cwd = input._cwd ?? process.cwd();
|
|
6921
7390
|
const sources = [
|
|
6922
7391
|
{
|
|
6923
|
-
path:
|
|
7392
|
+
path: join15(cwd, ".stackwright", "artifacts", "data-config.json"),
|
|
6924
7393
|
source: "data-config.json",
|
|
6925
7394
|
parse: (raw) => {
|
|
6926
7395
|
const parsed = JSON.parse(raw);
|
|
@@ -6931,15 +7400,15 @@ function handleListCollections(input) {
|
|
|
6931
7400
|
}
|
|
6932
7401
|
},
|
|
6933
7402
|
{
|
|
6934
|
-
path:
|
|
7403
|
+
path: join15(cwd, "stackwright.yml"),
|
|
6935
7404
|
source: "stackwright.yml",
|
|
6936
7405
|
parse: extractCollectionsFromYaml
|
|
6937
7406
|
}
|
|
6938
7407
|
];
|
|
6939
7408
|
for (const { path: path5, source, parse } of sources) {
|
|
6940
|
-
if (!
|
|
7409
|
+
if (!existsSync14(path5)) continue;
|
|
6941
7410
|
try {
|
|
6942
|
-
const collections = parse(
|
|
7411
|
+
const collections = parse(readFileSync15(path5, "utf8"));
|
|
6943
7412
|
return {
|
|
6944
7413
|
text: JSON.stringify({ collections, source, collectionCount: collections.length }),
|
|
6945
7414
|
isError: false
|
|
@@ -7090,8 +7559,8 @@ function handleValidateWorkflow(input) {
|
|
|
7090
7559
|
if (input.workflow && Object.keys(input.workflow).length > 0) {
|
|
7091
7560
|
raw = input.workflow;
|
|
7092
7561
|
} else {
|
|
7093
|
-
const artifactPath =
|
|
7094
|
-
if (!
|
|
7562
|
+
const artifactPath = join15(cwd, ".stackwright", "artifacts", "workflow-config.json");
|
|
7563
|
+
if (!existsSync14(artifactPath)) {
|
|
7095
7564
|
return fail([
|
|
7096
7565
|
{
|
|
7097
7566
|
code: "NO_WORKFLOW",
|
|
@@ -7100,7 +7569,7 @@ function handleValidateWorkflow(input) {
|
|
|
7100
7569
|
]);
|
|
7101
7570
|
}
|
|
7102
7571
|
try {
|
|
7103
|
-
raw = JSON.parse(
|
|
7572
|
+
raw = JSON.parse(readFileSync15(artifactPath, "utf8"));
|
|
7104
7573
|
} catch (err) {
|
|
7105
7574
|
return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
|
|
7106
7575
|
}
|
|
@@ -7407,12 +7876,92 @@ function registerTypeSchemasTool(server2) {
|
|
|
7407
7876
|
);
|
|
7408
7877
|
}
|
|
7409
7878
|
|
|
7879
|
+
// src/tools/validate-yaml-fragment.ts
|
|
7880
|
+
import { z as z19 } from "zod";
|
|
7881
|
+
import { load as yamlLoad5 } from "js-yaml";
|
|
7882
|
+
var FIELD_SYNONYMS = Object.fromEntries(
|
|
7883
|
+
Array.from(SCHEMA_REGISTRY.entries()).map(([name, entry]) => [
|
|
7884
|
+
name,
|
|
7885
|
+
buildReverseSynonyms(entry.synonyms ?? {})
|
|
7886
|
+
])
|
|
7887
|
+
);
|
|
7888
|
+
function enrichErrors(issues, reverseSynonyms) {
|
|
7889
|
+
return issues.map((issue) => {
|
|
7890
|
+
const pathStr = issue.path.join(".");
|
|
7891
|
+
const lastSegment = issue.path[issue.path.length - 1];
|
|
7892
|
+
const fieldName = typeof lastSegment === "string" ? lastSegment : void 0;
|
|
7893
|
+
const didYouMean = fieldName ? reverseSynonyms[fieldName] : void 0;
|
|
7894
|
+
return {
|
|
7895
|
+
path: pathStr || "(root)",
|
|
7896
|
+
message: issue.message,
|
|
7897
|
+
...didYouMean ? { didYouMean } : {}
|
|
7898
|
+
};
|
|
7899
|
+
});
|
|
7900
|
+
}
|
|
7901
|
+
function handleValidateYamlFragment(input) {
|
|
7902
|
+
const { schemaName, yaml: yaml4 } = input;
|
|
7903
|
+
const entry = SCHEMA_REGISTRY.get(schemaName);
|
|
7904
|
+
if (!entry) {
|
|
7905
|
+
return {
|
|
7906
|
+
valid: false,
|
|
7907
|
+
parseError: `Unknown schemaName: "${schemaName}". Supported: ${REGISTRY_SCHEMA_NAMES.join(", ")}`
|
|
7908
|
+
};
|
|
7909
|
+
}
|
|
7910
|
+
let parsed;
|
|
7911
|
+
try {
|
|
7912
|
+
parsed = yamlLoad5(yaml4);
|
|
7913
|
+
} catch (err) {
|
|
7914
|
+
return {
|
|
7915
|
+
valid: false,
|
|
7916
|
+
parseError: `YAML parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
7917
|
+
};
|
|
7918
|
+
}
|
|
7919
|
+
const ctx = input.projectRoot ? { projectRoot: input.projectRoot } : void 0;
|
|
7920
|
+
const resolvedSchema = getSchemaForEntry(entry, ctx);
|
|
7921
|
+
const result = resolvedSchema.safeParse(parsed);
|
|
7922
|
+
if (result.success) {
|
|
7923
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
7924
|
+
const ladder = resolveStatusLadder(projectRoot);
|
|
7925
|
+
if (ladder.length > 0) {
|
|
7926
|
+
const ladderSet = new Set(ladder);
|
|
7927
|
+
const tokenViolations = detectViolationsInYaml(parsed, ladder, ladderSet, "fragment");
|
|
7928
|
+
if (tokenViolations.length > 0) {
|
|
7929
|
+
const tokenErrors = tokenViolations.map((v) => ({
|
|
7930
|
+
path: v.path,
|
|
7931
|
+
message: v.message
|
|
7932
|
+
}));
|
|
7933
|
+
return { valid: false, errors: tokenErrors };
|
|
7934
|
+
}
|
|
7935
|
+
}
|
|
7936
|
+
return { valid: true };
|
|
7937
|
+
}
|
|
7938
|
+
const reverseSynonyms = buildReverseSynonyms(entry.synonyms ?? {});
|
|
7939
|
+
const errors = enrichErrors(result.error.issues, reverseSynonyms);
|
|
7940
|
+
return { valid: false, errors };
|
|
7941
|
+
}
|
|
7942
|
+
function registerValidateYamlFragmentTool(server2) {
|
|
7943
|
+
server2.tool(
|
|
7944
|
+
"stackwright_pro_validate_yaml_fragment",
|
|
7945
|
+
'Validates a YAML snippet against a canonical Zod schema before writing to disk. Returns actionable errors with "did you mean?" suggestions for common field-name drift (title vs label, id vs name, style vs theme.variant, etc.). Call this before stackwright_pro_safe_write to catch schema violations in-session.',
|
|
7946
|
+
{
|
|
7947
|
+
schemaName: z19.string().describe(`Schema to validate against. Available: ${REGISTRY_SCHEMA_NAMES.join(", ")}`),
|
|
7948
|
+
yaml: z19.string().describe("YAML string to validate (can also be JSON \u2014 js-yaml parses both)")
|
|
7949
|
+
},
|
|
7950
|
+
async ({ schemaName, yaml: yaml4 }) => {
|
|
7951
|
+
const result = handleValidateYamlFragment({ schemaName, yaml: yaml4 });
|
|
7952
|
+
return {
|
|
7953
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
7954
|
+
};
|
|
7955
|
+
}
|
|
7956
|
+
);
|
|
7957
|
+
}
|
|
7958
|
+
|
|
7410
7959
|
// src/tools/pages.ts
|
|
7411
7960
|
import * as fs2 from "fs";
|
|
7412
7961
|
import * as os from "os";
|
|
7413
7962
|
import * as path3 from "path";
|
|
7414
|
-
import { z as
|
|
7415
|
-
import { load as
|
|
7963
|
+
import { z as z20 } from "zod";
|
|
7964
|
+
import { load as yamlLoad6, dump as yamlDump } from "js-yaml";
|
|
7416
7965
|
import { writePage, resolvePagesDir } from "@stackwright/cli";
|
|
7417
7966
|
import { isProContentType, isOssTypeWithProExtension } from "@stackwright-pro/types";
|
|
7418
7967
|
var BCP47_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
@@ -7466,7 +8015,7 @@ function handleProWritePage(input) {
|
|
|
7466
8015
|
}
|
|
7467
8016
|
let parsed;
|
|
7468
8017
|
try {
|
|
7469
|
-
parsed =
|
|
8018
|
+
parsed = yamlLoad6(content);
|
|
7470
8019
|
} catch (err) {
|
|
7471
8020
|
return {
|
|
7472
8021
|
isError: true,
|
|
@@ -7523,10 +8072,10 @@ function registerProPageTools(server2) {
|
|
|
7523
8072
|
"stackwright_pro_write_page",
|
|
7524
8073
|
"Write or update a page's YAML content with Pro content-type awareness. Validates against the Stackwright schema (including Pro types like stats_grid, data_table_pulse, map_pulse, alert_banner). Invalid YAML or schema violations on OSS-type fields are rejected with field-level errors. Use this instead of stackwright_write_page for any page containing Pro content types.",
|
|
7525
8074
|
{
|
|
7526
|
-
projectRoot:
|
|
7527
|
-
slug:
|
|
7528
|
-
content:
|
|
7529
|
-
locale:
|
|
8075
|
+
projectRoot: z20.string().describe("Absolute path to the Stackwright project root"),
|
|
8076
|
+
slug: z20.string().describe('Page slug (e.g. "dashboard/main" or "/dashboard/main")'),
|
|
8077
|
+
content: z20.string().describe("Full YAML content for the page (content.yml contents)"),
|
|
8078
|
+
locale: z20.string().optional().describe(
|
|
7530
8079
|
'BCP 47 locale tag (e.g. "fr", "de"). If provided, writes content.<locale>.yml instead of content.yml.'
|
|
7531
8080
|
)
|
|
7532
8081
|
},
|
|
@@ -7538,16 +8087,16 @@ function registerProPageTools(server2) {
|
|
|
7538
8087
|
|
|
7539
8088
|
// src/tools/scaffold-cleanup.ts
|
|
7540
8089
|
import {
|
|
7541
|
-
existsSync as
|
|
8090
|
+
existsSync as existsSync16,
|
|
7542
8091
|
lstatSync as lstatSync9,
|
|
7543
8092
|
unlinkSync as unlinkSync2,
|
|
7544
|
-
readFileSync as
|
|
8093
|
+
readFileSync as readFileSync16,
|
|
7545
8094
|
writeFileSync as writeFileSync8,
|
|
7546
|
-
readdirSync as
|
|
8095
|
+
readdirSync as readdirSync10,
|
|
7547
8096
|
rmdirSync,
|
|
7548
8097
|
mkdirSync as mkdirSync8
|
|
7549
8098
|
} from "fs";
|
|
7550
|
-
import { join as
|
|
8099
|
+
import { join as join17, dirname as dirname3 } from "path";
|
|
7551
8100
|
var SCAFFOLD_FILES_TO_DELETE = [
|
|
7552
8101
|
"content/posts/getting-started.yaml",
|
|
7553
8102
|
"content/posts/hello-world.yaml"
|
|
@@ -7572,12 +8121,12 @@ var FALLBACK_COLORS = {
|
|
|
7572
8121
|
mutedForeground: "#737373"
|
|
7573
8122
|
};
|
|
7574
8123
|
function readThemeColors(cwd) {
|
|
7575
|
-
const tokenPath =
|
|
7576
|
-
if (!
|
|
8124
|
+
const tokenPath = join17(cwd, ".stackwright", "artifacts", "theme-tokens.json");
|
|
8125
|
+
if (!existsSync16(tokenPath)) return { ...FALLBACK_COLORS };
|
|
7577
8126
|
const stat = lstatSync9(tokenPath);
|
|
7578
8127
|
if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
|
|
7579
8128
|
try {
|
|
7580
|
-
const raw = JSON.parse(
|
|
8129
|
+
const raw = JSON.parse(readFileSync16(tokenPath, "utf-8"));
|
|
7581
8130
|
const css = raw?.cssVariables ?? {};
|
|
7582
8131
|
const toHsl = (hslValue) => {
|
|
7583
8132
|
if (!hslValue || typeof hslValue !== "string") return null;
|
|
@@ -7637,15 +8186,15 @@ function generateNotFoundPage(colors) {
|
|
|
7637
8186
|
`;
|
|
7638
8187
|
}
|
|
7639
8188
|
function isSafePath(fullPath) {
|
|
7640
|
-
if (!
|
|
8189
|
+
if (!existsSync16(fullPath)) return true;
|
|
7641
8190
|
const stat = lstatSync9(fullPath);
|
|
7642
8191
|
return !stat.isSymbolicLink();
|
|
7643
8192
|
}
|
|
7644
8193
|
function isDirEmpty(dirPath) {
|
|
7645
|
-
if (!
|
|
8194
|
+
if (!existsSync16(dirPath)) return false;
|
|
7646
8195
|
const stat = lstatSync9(dirPath);
|
|
7647
8196
|
if (!stat.isDirectory()) return false;
|
|
7648
|
-
return
|
|
8197
|
+
return readdirSync10(dirPath).length === 0;
|
|
7649
8198
|
}
|
|
7650
8199
|
function handleCleanupScaffold(_cwd) {
|
|
7651
8200
|
const cwd = _cwd ?? process.cwd();
|
|
@@ -7658,8 +8207,8 @@ function handleCleanupScaffold(_cwd) {
|
|
|
7658
8207
|
cleanupWarnings: []
|
|
7659
8208
|
};
|
|
7660
8209
|
for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
|
|
7661
|
-
const fullPath =
|
|
7662
|
-
if (!
|
|
8210
|
+
const fullPath = join17(cwd, relPath);
|
|
8211
|
+
if (!existsSync16(fullPath)) {
|
|
7663
8212
|
result.skipped.push(relPath);
|
|
7664
8213
|
continue;
|
|
7665
8214
|
}
|
|
@@ -7676,13 +8225,13 @@ function handleCleanupScaffold(_cwd) {
|
|
|
7676
8225
|
}
|
|
7677
8226
|
{
|
|
7678
8227
|
const relPath = "pages/getting-started/content.yml";
|
|
7679
|
-
const fullPath =
|
|
7680
|
-
if (!
|
|
8228
|
+
const fullPath = join17(cwd, relPath);
|
|
8229
|
+
if (!existsSync16(fullPath)) {
|
|
7681
8230
|
result.skipped.push(relPath);
|
|
7682
8231
|
} else if (!isSafePath(fullPath)) {
|
|
7683
8232
|
result.errors.push(`Refusing to delete symlink: ${relPath}`);
|
|
7684
8233
|
} else {
|
|
7685
|
-
const content =
|
|
8234
|
+
const content = readFileSync16(fullPath, "utf-8");
|
|
7686
8235
|
const isScaffoldDefault = GETTING_STARTED_SCAFFOLD_FINGERPRINTS.some(
|
|
7687
8236
|
(marker) => content.includes(marker)
|
|
7688
8237
|
);
|
|
@@ -7702,16 +8251,16 @@ function handleCleanupScaffold(_cwd) {
|
|
|
7702
8251
|
}
|
|
7703
8252
|
{
|
|
7704
8253
|
const relPath = `content/posts/${POSTS_COLLECTION_FILE}`;
|
|
7705
|
-
const fullPath =
|
|
7706
|
-
const postsDir =
|
|
7707
|
-
if (!
|
|
8254
|
+
const fullPath = join17(cwd, relPath);
|
|
8255
|
+
const postsDir = join17(cwd, "content/posts");
|
|
8256
|
+
if (!existsSync16(fullPath)) {
|
|
7708
8257
|
result.skipped.push(relPath);
|
|
7709
8258
|
} else if (!isSafePath(fullPath)) {
|
|
7710
8259
|
result.errors.push(`Refusing to delete symlink: ${relPath}`);
|
|
7711
|
-
} else if (!
|
|
8260
|
+
} else if (!existsSync16(postsDir)) {
|
|
7712
8261
|
result.skipped.push(relPath);
|
|
7713
8262
|
} else {
|
|
7714
|
-
const userPostFiles =
|
|
8263
|
+
const userPostFiles = readdirSync10(postsDir).filter(
|
|
7715
8264
|
(f) => f !== POSTS_COLLECTION_FILE && POST_FILE_EXTENSIONS.some((ext) => f.endsWith(ext))
|
|
7716
8265
|
);
|
|
7717
8266
|
if (userPostFiles.length === 0) {
|
|
@@ -7729,8 +8278,8 @@ function handleCleanupScaffold(_cwd) {
|
|
|
7729
8278
|
}
|
|
7730
8279
|
}
|
|
7731
8280
|
for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
|
|
7732
|
-
const fullDir =
|
|
7733
|
-
if (!
|
|
8281
|
+
const fullDir = join17(cwd, relDir);
|
|
8282
|
+
if (!existsSync16(fullDir)) continue;
|
|
7734
8283
|
if (!isSafePath(fullDir)) {
|
|
7735
8284
|
result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
|
|
7736
8285
|
continue;
|
|
@@ -7745,8 +8294,8 @@ function handleCleanupScaffold(_cwd) {
|
|
|
7745
8294
|
}
|
|
7746
8295
|
}
|
|
7747
8296
|
{
|
|
7748
|
-
const fullPath =
|
|
7749
|
-
if (!
|
|
8297
|
+
const fullPath = join17(cwd, NOT_FOUND_PATH);
|
|
8298
|
+
if (!existsSync16(fullPath)) {
|
|
7750
8299
|
result.skipped.push(NOT_FOUND_PATH);
|
|
7751
8300
|
} else if (!isSafePath(fullPath)) {
|
|
7752
8301
|
result.errors.push(`Refusing to rewrite symlink: ${NOT_FOUND_PATH}`);
|
|
@@ -7784,8 +8333,159 @@ function registerScaffoldCleanupTools(server2) {
|
|
|
7784
8333
|
);
|
|
7785
8334
|
}
|
|
7786
8335
|
|
|
8336
|
+
// src/tools/write-workflow-container-page.ts
|
|
8337
|
+
import { z as z21 } from "zod";
|
|
8338
|
+
import { dump as yamlDump2 } from "js-yaml";
|
|
8339
|
+
import { WorkflowWizardContentSchema as WorkflowWizardContentSchema2 } from "@stackwright-pro/types";
|
|
8340
|
+
var WriteWorkflowContainerPageInputSchema = z21.object({
|
|
8341
|
+
/**
|
|
8342
|
+
* Test-only: override process.cwd() for deterministic temp-dir testing.
|
|
8343
|
+
* Never set in production — omit entirely in normal MCP tool calls.
|
|
8344
|
+
*/
|
|
8345
|
+
_cwd: z21.string().optional(),
|
|
8346
|
+
/**
|
|
8347
|
+
* Kebab-case workflow ID — must match `workflow.id` in the referenced YAML.
|
|
8348
|
+
* Example: `patient-evacuation-authorization`
|
|
8349
|
+
*/
|
|
8350
|
+
workflowId: z21.string().regex(/^[a-z0-9-]+$/, {
|
|
8351
|
+
message: "workflowId must be kebab-case (lowercase alphanumeric + hyphens)"
|
|
8352
|
+
}),
|
|
8353
|
+
/**
|
|
8354
|
+
* Relative path to the workflow YAML file from project root.
|
|
8355
|
+
* Example: `workflows/patient-evacuation-authorization.yml`
|
|
8356
|
+
*/
|
|
8357
|
+
workflowFile: z21.string().min(1).regex(/^workflows\/[a-z0-9-]+\.(yml|yaml)$/, {
|
|
8358
|
+
message: "workflowFile must be: workflows/<id>.yml"
|
|
8359
|
+
}),
|
|
8360
|
+
/**
|
|
8361
|
+
* URL slug for the container page route.
|
|
8362
|
+
* Convention: `workflows/<workflowId>` → page at `/workflows/<workflowId>`.
|
|
8363
|
+
* Example: `workflows/patient-evacuation`
|
|
8364
|
+
*
|
|
8365
|
+
* The tool writes to `pages/<pageSlug>/content.yml`.
|
|
8366
|
+
*/
|
|
8367
|
+
pageSlug: z21.string().min(1).regex(/^workflows\/[a-z0-9-]+$/, {
|
|
8368
|
+
message: 'pageSlug must start with "workflows/" and be kebab-case. Example: workflows/patient-evacuation'
|
|
8369
|
+
}),
|
|
8370
|
+
/**
|
|
8371
|
+
* Human-readable label for the workflow (used in page title + breadcrumbs).
|
|
8372
|
+
* Example: `Patient Evacuation Authorization`
|
|
8373
|
+
*/
|
|
8374
|
+
label: z21.string().min(1).max(120),
|
|
8375
|
+
/**
|
|
8376
|
+
* Union of required roles across all steps in the workflow YAML.
|
|
8377
|
+
* Consumed by auth-reconcile to build protectedRoutes.
|
|
8378
|
+
* Must be non-empty — workflow pages are always auth-protected.
|
|
8379
|
+
*/
|
|
8380
|
+
requiredRoles: z21.array(z21.string().min(1)).min(1, {
|
|
8381
|
+
message: "requiredRoles must have at least one role \u2014 workflow pages are always auth-protected"
|
|
8382
|
+
}),
|
|
8383
|
+
/**
|
|
8384
|
+
* Persistence key for workflow state (localStorage slot).
|
|
8385
|
+
* Defaults to `workflow-<workflowId>` when omitted.
|
|
8386
|
+
*/
|
|
8387
|
+
persistenceKey: z21.string().optional(),
|
|
8388
|
+
/**
|
|
8389
|
+
* Layout mode for the container page. Defaults to `app-shell` per Pro convention.
|
|
8390
|
+
* Only override when you have a specific reason — `app-shell` is correct for all
|
|
8391
|
+
* workflow container pages in data-dense Pro apps.
|
|
8392
|
+
*/
|
|
8393
|
+
layoutMode: z21.enum(["app-shell", "page"]).default("app-shell")
|
|
8394
|
+
});
|
|
8395
|
+
function handleWriteWorkflowContainerPage(input) {
|
|
8396
|
+
const parsed = WriteWorkflowContainerPageInputSchema.safeParse(input);
|
|
8397
|
+
if (!parsed.success) {
|
|
8398
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
8399
|
+
return {
|
|
8400
|
+
text: JSON.stringify({ error: `Invalid input: ${issues}` }),
|
|
8401
|
+
isError: true
|
|
8402
|
+
};
|
|
8403
|
+
}
|
|
8404
|
+
const { workflowId, workflowFile, pageSlug, label, requiredRoles, persistenceKey, layoutMode } = parsed.data;
|
|
8405
|
+
const resolvedPersistenceKey = persistenceKey ?? `workflow-${workflowId}`;
|
|
8406
|
+
const wizardItem = {
|
|
8407
|
+
type: "workflow_wizard",
|
|
8408
|
+
label: workflowId,
|
|
8409
|
+
// slug-form label for the content item (page title comes from meta)
|
|
8410
|
+
workflowFile,
|
|
8411
|
+
workflowId,
|
|
8412
|
+
persistenceKey: resolvedPersistenceKey
|
|
8413
|
+
};
|
|
8414
|
+
const schemaCheck = WorkflowWizardContentSchema2.safeParse(wizardItem);
|
|
8415
|
+
if (!schemaCheck.success) {
|
|
8416
|
+
const issues = schemaCheck.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
8417
|
+
return {
|
|
8418
|
+
text: JSON.stringify({ error: `Internal schema validation failed: ${issues}` }),
|
|
8419
|
+
isError: true
|
|
8420
|
+
};
|
|
8421
|
+
}
|
|
8422
|
+
const pageContent = {
|
|
8423
|
+
layoutMode,
|
|
8424
|
+
meta: {
|
|
8425
|
+
title: `${label} | Workflow`,
|
|
8426
|
+
description: `${label} \u2014 multi-step workflow. Requires authorization.`,
|
|
8427
|
+
authRequired: true,
|
|
8428
|
+
requiredRoles
|
|
8429
|
+
},
|
|
8430
|
+
content: {
|
|
8431
|
+
content_items: [wizardItem]
|
|
8432
|
+
}
|
|
8433
|
+
};
|
|
8434
|
+
const yamlContent = yamlDump2(pageContent, {
|
|
8435
|
+
indent: 2,
|
|
8436
|
+
lineWidth: 120,
|
|
8437
|
+
quotingType: "'",
|
|
8438
|
+
forceQuotes: false
|
|
8439
|
+
});
|
|
8440
|
+
const filePath = `pages/${pageSlug}/content.yml`;
|
|
8441
|
+
const writeResult = handleSafeWrite({
|
|
8442
|
+
callerOtter: "stackwright-pro-form-wizard-otter",
|
|
8443
|
+
filePath,
|
|
8444
|
+
content: yamlContent,
|
|
8445
|
+
createDirectories: true,
|
|
8446
|
+
_cwd: parsed.data._cwd
|
|
8447
|
+
});
|
|
8448
|
+
if (writeResult.isError) {
|
|
8449
|
+
return writeResult;
|
|
8450
|
+
}
|
|
8451
|
+
const result = {
|
|
8452
|
+
written: true,
|
|
8453
|
+
path: filePath,
|
|
8454
|
+
contentTypesEmitted: ["workflow_wizard"],
|
|
8455
|
+
content: yamlContent
|
|
8456
|
+
};
|
|
8457
|
+
return {
|
|
8458
|
+
text: JSON.stringify(result),
|
|
8459
|
+
isError: false
|
|
8460
|
+
};
|
|
8461
|
+
}
|
|
8462
|
+
function registerWriteWorkflowContainerPageTool(server2) {
|
|
8463
|
+
server2.tool(
|
|
8464
|
+
"stackwright_pro_write_workflow_container_page",
|
|
8465
|
+
[
|
|
8466
|
+
"Create the canonical container page for a workflow at pages/workflows/<workflowId>/content.yml.",
|
|
8467
|
+
"",
|
|
8468
|
+
"ALWAYS call this immediately after writing the workflow YAML via stackwright_pro_safe_write.",
|
|
8469
|
+
"Without it, the route /workflows/<workflowId> 404s at runtime.",
|
|
8470
|
+
"",
|
|
8471
|
+
"The tool deterministically emits:",
|
|
8472
|
+
" - layoutMode: app-shell",
|
|
8473
|
+
" - meta.authRequired: true, meta.requiredRoles: <your input>",
|
|
8474
|
+
" - One workflow_wizard content item binding workflowFile + workflowId",
|
|
8475
|
+
"",
|
|
8476
|
+
"requiredRoles: supply the UNION of required roles from all steps in your workflow YAML.",
|
|
8477
|
+
'pageSlug: convention is "workflows/<workflowId>" \u2014 must start with "workflows/".'
|
|
8478
|
+
].join("\n"),
|
|
8479
|
+
WriteWorkflowContainerPageInputSchema.shape,
|
|
8480
|
+
async (args) => {
|
|
8481
|
+
const result = handleWriteWorkflowContainerPage(args);
|
|
8482
|
+
return { content: [{ type: "text", text: result.text }] };
|
|
8483
|
+
}
|
|
8484
|
+
);
|
|
8485
|
+
}
|
|
8486
|
+
|
|
7787
8487
|
// src/tools/contrast.ts
|
|
7788
|
-
import { z as
|
|
8488
|
+
import { z as z22 } from "zod";
|
|
7789
8489
|
function linearizeChannel(c255) {
|
|
7790
8490
|
const c = c255 / 255;
|
|
7791
8491
|
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
@@ -7957,8 +8657,8 @@ function registerContrastTools(server2) {
|
|
|
7957
8657
|
"stackwright_pro_check_contrast",
|
|
7958
8658
|
'Compute the exact WCAG 2.1 contrast ratio between a foreground and background color. Returns the ratio and AA/AAA pass/fail flags. Use this to verify any foreground/background color pair before writing it to the token set. Accepts #RGB, #RRGGBB, hsl(...), or shadcn HSL format ("H S% L%").',
|
|
7959
8659
|
{
|
|
7960
|
-
fg:
|
|
7961
|
-
bg:
|
|
8660
|
+
fg: z22.string().describe("Foreground (text) color \u2014 hex or HSL"),
|
|
8661
|
+
bg: z22.string().describe("Background color \u2014 hex or HSL")
|
|
7962
8662
|
},
|
|
7963
8663
|
async ({ fg, bg }) => {
|
|
7964
8664
|
let result;
|
|
@@ -7988,8 +8688,8 @@ function registerContrastTools(server2) {
|
|
|
7988
8688
|
"stackwright_pro_derive_accessible_palette",
|
|
7989
8689
|
'Given a seed color (treated as a background), derive the best accessible foreground (text) color that meets the target contrast ratio. Evaluates #ffffff and #000000 against the seed and picks whichever satisfies the target ratio with the highest contrast. Use this for every {name}-foreground token derivation instead of guessing white vs black. Accepts #RGB, #RRGGBB, hsl(...), or shadcn HSL format ("H S% L%").',
|
|
7990
8690
|
{
|
|
7991
|
-
seed:
|
|
7992
|
-
targetRatio: numCoerce(
|
|
8691
|
+
seed: z22.string().describe("Background (seed) color \u2014 hex or HSL"),
|
|
8692
|
+
targetRatio: numCoerce(z22.number().default(4.5)).describe(
|
|
7993
8693
|
"Minimum required contrast ratio (default 4.5 for WCAG AA)"
|
|
7994
8694
|
)
|
|
7995
8695
|
},
|
|
@@ -8024,7 +8724,7 @@ function registerContrastTools(server2) {
|
|
|
8024
8724
|
}
|
|
8025
8725
|
|
|
8026
8726
|
// src/tools/compile.ts
|
|
8027
|
-
import { z as
|
|
8727
|
+
import { z as z23 } from "zod";
|
|
8028
8728
|
import fs3 from "fs";
|
|
8029
8729
|
import path4 from "path";
|
|
8030
8730
|
import { compileCollections } from "@stackwright-pro/pulse/server";
|
|
@@ -8070,7 +8770,7 @@ async function tryOssCompile(fn, ctx, extra) {
|
|
|
8070
8770
|
}
|
|
8071
8771
|
}
|
|
8072
8772
|
function registerCompileTools(server2) {
|
|
8073
|
-
const projectRootArg =
|
|
8773
|
+
const projectRootArg = z23.string().optional().describe("Absolute path to project root (defaults to cwd)");
|
|
8074
8774
|
server2.tool(
|
|
8075
8775
|
"stackwright_pro_compile_collections",
|
|
8076
8776
|
"Compile stackwright.collections.yml \u2192 public/stackwright-content/_collections.json. Reads and validates the live-collections sidecar config file and emits the JSON sink consumed at runtime by PulseCollectionProvider.",
|
|
@@ -8246,7 +8946,7 @@ ${results.join("\n")}`;
|
|
|
8246
8946
|
"stackwright_pro_compile_page",
|
|
8247
8947
|
"Compile a single page by slug. Delegates to compilePage from @stackwright/build-scripts installed in the project.",
|
|
8248
8948
|
{
|
|
8249
|
-
slug:
|
|
8949
|
+
slug: z23.string().describe('Page slug (relative path under pages/, e.g. "dashboard")'),
|
|
8250
8950
|
projectRoot: projectRootArg
|
|
8251
8951
|
},
|
|
8252
8952
|
async ({ slug, projectRoot }) => {
|
|
@@ -8266,15 +8966,15 @@ ${results.join("\n")}`;
|
|
|
8266
8966
|
}
|
|
8267
8967
|
|
|
8268
8968
|
// src/tools/strip-legacy-integrations.ts
|
|
8269
|
-
import { z as
|
|
8270
|
-
import { existsSync as
|
|
8271
|
-
import { join as
|
|
8969
|
+
import { z as z24 } from "zod";
|
|
8970
|
+
import { existsSync as existsSync17, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
|
|
8971
|
+
import { join as join18 } from "path";
|
|
8272
8972
|
function handleStripLegacyIntegrations(input) {
|
|
8273
8973
|
const { projectRoot } = input;
|
|
8274
|
-
const ymlPath =
|
|
8275
|
-
const yamlPath =
|
|
8276
|
-
if (!
|
|
8277
|
-
if (
|
|
8974
|
+
const ymlPath = join18(projectRoot, "stackwright.yml");
|
|
8975
|
+
const yamlPath = join18(projectRoot, "stackwright.yaml");
|
|
8976
|
+
if (!existsSync17(ymlPath)) {
|
|
8977
|
+
if (existsSync17(yamlPath)) {
|
|
8278
8978
|
return {
|
|
8279
8979
|
changed: false,
|
|
8280
8980
|
removedEntries: 0,
|
|
@@ -8287,7 +8987,7 @@ function handleStripLegacyIntegrations(input) {
|
|
|
8287
8987
|
message: "stackwright.yml not found \u2014 no-op."
|
|
8288
8988
|
};
|
|
8289
8989
|
}
|
|
8290
|
-
const raw =
|
|
8990
|
+
const raw = readFileSync17(ymlPath, "utf-8");
|
|
8291
8991
|
const isCRLF = raw.includes("\r\n");
|
|
8292
8992
|
const content = isCRLF ? raw.replace(/\r\n/g, "\n") : raw;
|
|
8293
8993
|
const lines = content.split("\n");
|
|
@@ -8329,7 +9029,7 @@ function registerStripLegacyIntegrationsTool(server2) {
|
|
|
8329
9029
|
"stackwright_pro_strip_legacy_integrations",
|
|
8330
9030
|
"Removes the deprecated top-level integrations[] block from stackwright.yml. The authoritative source for integration config is stackwright.integrations.yml \u2014 this tool cleans up legacy entries from older raft runs. Idempotent: no-op if no legacy block present. Preserves all other top-level keys, comments, and formatting.",
|
|
8331
9031
|
{
|
|
8332
|
-
projectRoot:
|
|
9032
|
+
projectRoot: z24.string().describe("Absolute path to the project root directory")
|
|
8333
9033
|
},
|
|
8334
9034
|
async ({ projectRoot }) => {
|
|
8335
9035
|
const result = handleStripLegacyIntegrations({ projectRoot });
|
|
@@ -8341,16 +9041,16 @@ function registerStripLegacyIntegrationsTool(server2) {
|
|
|
8341
9041
|
}
|
|
8342
9042
|
|
|
8343
9043
|
// src/tools/emitters.ts
|
|
8344
|
-
import { z as
|
|
8345
|
-
import { readFileSync as
|
|
8346
|
-
import { join as
|
|
9044
|
+
import { z as z25 } from "zod";
|
|
9045
|
+
import { readFileSync as readFileSync18, existsSync as existsSync18, readdirSync as readdirSync11 } from "fs";
|
|
9046
|
+
import { join as join19 } from "path";
|
|
8347
9047
|
import yaml from "js-yaml";
|
|
8348
9048
|
import { emitEnvLocal, emitProvidersFile, emitMockPorts } from "@stackwright-pro/emitters";
|
|
8349
9049
|
function readIntegrations(cwd) {
|
|
8350
|
-
const filePath =
|
|
8351
|
-
if (!
|
|
9050
|
+
const filePath = join19(cwd, "stackwright.integrations.yml");
|
|
9051
|
+
if (!existsSync18(filePath)) return [];
|
|
8352
9052
|
try {
|
|
8353
|
-
const raw = yaml.load(
|
|
9053
|
+
const raw = yaml.load(readFileSync18(filePath, "utf-8"));
|
|
8354
9054
|
if (!raw?.integrations || !Array.isArray(raw.integrations)) return [];
|
|
8355
9055
|
return raw.integrations.filter((i) => typeof i.name === "string").map((i) => ({
|
|
8356
9056
|
name: i.name,
|
|
@@ -8361,15 +9061,15 @@ function readIntegrations(cwd) {
|
|
|
8361
9061
|
}
|
|
8362
9062
|
}
|
|
8363
9063
|
function readServiceTokenRefs(cwd) {
|
|
8364
|
-
const servicesDir =
|
|
8365
|
-
if (!
|
|
9064
|
+
const servicesDir = join19(cwd, "services");
|
|
9065
|
+
if (!existsSync18(servicesDir)) return [];
|
|
8366
9066
|
try {
|
|
8367
|
-
const files =
|
|
9067
|
+
const files = readdirSync11(servicesDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
8368
9068
|
const refs = [];
|
|
8369
9069
|
for (const file of files) {
|
|
8370
9070
|
try {
|
|
8371
9071
|
const raw = yaml.load(
|
|
8372
|
-
|
|
9072
|
+
readFileSync18(join19(servicesDir, file), "utf-8")
|
|
8373
9073
|
);
|
|
8374
9074
|
if (raw?.tokenEnv && typeof raw.tokenEnv === "string") {
|
|
8375
9075
|
refs.push({
|
|
@@ -8400,7 +9100,7 @@ Call this tool, then write both files with stackwright_pro_safe_write:
|
|
|
8400
9100
|
|
|
8401
9101
|
Do NOT compose the contents by hand. This emitter is the source of truth. If an env var is missing, update integrations.yml or the emitter \u2014 not the file. This is the first concrete instance of the swp-zf9y deterministic emitter pattern. ${ENV_LOCAL_DESC}`,
|
|
8402
9102
|
{
|
|
8403
|
-
cwd:
|
|
9103
|
+
cwd: z25.string().describe(
|
|
8404
9104
|
"Absolute path to the project root containing stackwright.integrations.yml and optionally services/*.yml"
|
|
8405
9105
|
)
|
|
8406
9106
|
},
|
|
@@ -8447,8 +9147,8 @@ mapProvider options:
|
|
|
8447
9147
|
|
|
8448
9148
|
This is the second concrete instance of the swp-zf9y deterministic emitter pattern. Refs: swp-57ku, swp-2m89, swp-zf9y, drawer 957. ${PROVIDERS_DESC}`,
|
|
8449
9149
|
{
|
|
8450
|
-
cwd:
|
|
8451
|
-
mapProvider:
|
|
9150
|
+
cwd: z25.string().describe("Absolute path to the project root containing stackwright.integrations.yml"),
|
|
9151
|
+
mapProvider: z25.enum(["maplibre", "cesium", "none"]).optional().describe(
|
|
8452
9152
|
"Map provider to register (default: cesium for Pro \u2014 3D globe + terrain). Use maplibre for OSS-style 2D dashboards (explicit opt-out). Use none if pages have no map content_items."
|
|
8453
9153
|
)
|
|
8454
9154
|
},
|
|
@@ -8489,7 +9189,7 @@ Workflow:
|
|
|
8489
9189
|
|
|
8490
9190
|
This is the third concrete instance of the swp-zf9y deterministic emitter pattern. Refs: swp-phkh, swp-zf9y. ${PORTS_DESC}`,
|
|
8491
9191
|
{
|
|
8492
|
-
cwd:
|
|
9192
|
+
cwd: z25.string().describe("Absolute path to the project root containing stackwright.integrations.yml")
|
|
8493
9193
|
},
|
|
8494
9194
|
async ({ cwd }) => {
|
|
8495
9195
|
const integrations = readIntegrations(cwd);
|
|
@@ -8518,9 +9218,9 @@ This is the third concrete instance of the swp-zf9y deterministic emitter patter
|
|
|
8518
9218
|
}
|
|
8519
9219
|
|
|
8520
9220
|
// src/tools/list-specs.ts
|
|
8521
|
-
import { z as
|
|
8522
|
-
import { readdirSync as
|
|
8523
|
-
import { join as
|
|
9221
|
+
import { z as z26 } from "zod";
|
|
9222
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync2, existsSync as existsSync19 } from "fs";
|
|
9223
|
+
import { join as join20, extname, basename as basename2 } from "path";
|
|
8524
9224
|
var STRIP_SUFFIXES = ["-openapi", "-asyncapi", "-api", "-spec"];
|
|
8525
9225
|
var ALLOWED_EXTS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
|
|
8526
9226
|
function deriveIntegrationName(fileName) {
|
|
@@ -8542,18 +9242,18 @@ function detectFormat(snippet) {
|
|
|
8542
9242
|
}
|
|
8543
9243
|
function handleListSpecs(input) {
|
|
8544
9244
|
const { projectRoot } = input;
|
|
8545
|
-
const specsDir =
|
|
9245
|
+
const specsDir = join20(projectRoot, "specs");
|
|
8546
9246
|
const empty = { projectRoot, specs: [], totalCount: 0 };
|
|
8547
|
-
if (!
|
|
9247
|
+
if (!existsSync19(specsDir)) return empty;
|
|
8548
9248
|
let entries;
|
|
8549
9249
|
try {
|
|
8550
|
-
entries =
|
|
9250
|
+
entries = readdirSync12(specsDir);
|
|
8551
9251
|
} catch {
|
|
8552
9252
|
return empty;
|
|
8553
9253
|
}
|
|
8554
9254
|
const specs = [];
|
|
8555
9255
|
for (const entry of entries) {
|
|
8556
|
-
const fullPath =
|
|
9256
|
+
const fullPath = join20(specsDir, entry);
|
|
8557
9257
|
let stat;
|
|
8558
9258
|
try {
|
|
8559
9259
|
stat = statSync2(fullPath);
|
|
@@ -8564,7 +9264,7 @@ function handleListSpecs(input) {
|
|
|
8564
9264
|
if (!ALLOWED_EXTS.has(extname(entry).toLowerCase())) continue;
|
|
8565
9265
|
let snippet = "";
|
|
8566
9266
|
try {
|
|
8567
|
-
snippet =
|
|
9267
|
+
snippet = readFileSync19(fullPath, "utf-8").slice(0, 2048);
|
|
8568
9268
|
} catch {
|
|
8569
9269
|
}
|
|
8570
9270
|
specs.push({
|
|
@@ -8583,7 +9283,7 @@ function registerListSpecsTool(server2) {
|
|
|
8583
9283
|
"stackwright_pro_list_specs",
|
|
8584
9284
|
"Enumerate OpenAPI/AsyncAPI specs in the project's specs/ directory. Returns one entry per spec with integration name (derived from filename), detected format, and size. Used by the foreman to fan out the api phase with one api-otter invocation per spec.",
|
|
8585
9285
|
{
|
|
8586
|
-
projectRoot:
|
|
9286
|
+
projectRoot: z26.string().describe("Absolute path to the project root directory")
|
|
8587
9287
|
},
|
|
8588
9288
|
async ({ projectRoot }) => {
|
|
8589
9289
|
const result = handleListSpecs({ projectRoot });
|
|
@@ -8595,8 +9295,8 @@ function registerListSpecsTool(server2) {
|
|
|
8595
9295
|
}
|
|
8596
9296
|
|
|
8597
9297
|
// src/tools/describe-endpoint.ts
|
|
8598
|
-
import { z as
|
|
8599
|
-
import { readFileSync as
|
|
9298
|
+
import { z as z27 } from "zod";
|
|
9299
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
8600
9300
|
import { extname as extname2 } from "path";
|
|
8601
9301
|
import yaml2 from "js-yaml";
|
|
8602
9302
|
function resolveRef(root, ref) {
|
|
@@ -8629,7 +9329,7 @@ function deref(root, obj) {
|
|
|
8629
9329
|
return o;
|
|
8630
9330
|
}
|
|
8631
9331
|
function loadSpec(specPath) {
|
|
8632
|
-
const raw =
|
|
9332
|
+
const raw = readFileSync20(specPath, "utf-8");
|
|
8633
9333
|
const ext = extname2(specPath).toLowerCase();
|
|
8634
9334
|
if (ext === ".json") {
|
|
8635
9335
|
return JSON.parse(raw);
|
|
@@ -8742,9 +9442,9 @@ function registerDescribeEndpointTool(server2) {
|
|
|
8742
9442
|
"from required query params and resolved auth query params (swp-8r8i + swp-1l6l)."
|
|
8743
9443
|
].join("\n"),
|
|
8744
9444
|
{
|
|
8745
|
-
specPath:
|
|
8746
|
-
path:
|
|
8747
|
-
method:
|
|
9445
|
+
specPath: z27.string().describe("Absolute path to the OpenAPI spec file (.yaml, .yml, or .json)"),
|
|
9446
|
+
path: z27.string().describe('The API path to inspect (e.g. "/stations/{stationId}/observations")'),
|
|
9447
|
+
method: z27.string().describe('HTTP method (case-insensitive: "get", "post", "put", etc.)')
|
|
8748
9448
|
},
|
|
8749
9449
|
async ({ specPath, path: path5, method }) => {
|
|
8750
9450
|
try {
|
|
@@ -8764,10 +9464,10 @@ function registerDescribeEndpointTool(server2) {
|
|
|
8764
9464
|
}
|
|
8765
9465
|
|
|
8766
9466
|
// src/tools/consolidate-integrations.ts
|
|
8767
|
-
import { z as
|
|
8768
|
-
import { readdirSync as
|
|
9467
|
+
import { z as z28 } from "zod";
|
|
9468
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync10, existsSync as existsSync20, mkdirSync as mkdirSync9 } from "fs";
|
|
8769
9469
|
import { lockSync as lockSync2 } from "proper-lockfile";
|
|
8770
|
-
import { join as
|
|
9470
|
+
import { join as join21, basename as basename3 } from "path";
|
|
8771
9471
|
import yaml3 from "js-yaml";
|
|
8772
9472
|
var BASE_MOCK_PORT = 4011;
|
|
8773
9473
|
var STRIP_SUFFIXES2 = ["-openapi", "-asyncapi", "-api", "-spec"];
|
|
@@ -8780,13 +9480,13 @@ function deriveNameFromFilename(filename) {
|
|
|
8780
9480
|
}
|
|
8781
9481
|
function handleConsolidateIntegrations(input) {
|
|
8782
9482
|
const { projectRoot } = input;
|
|
8783
|
-
const artifactsDir =
|
|
8784
|
-
if (!
|
|
9483
|
+
const artifactsDir = join21(projectRoot, ".stackwright", "artifacts");
|
|
9484
|
+
if (!existsSync20(artifactsDir)) {
|
|
8785
9485
|
return { written: false, integrationCount: 0, reason: "no per-spec artifacts found" };
|
|
8786
9486
|
}
|
|
8787
9487
|
let entries;
|
|
8788
9488
|
try {
|
|
8789
|
-
entries =
|
|
9489
|
+
entries = readdirSync13(artifactsDir).filter(
|
|
8790
9490
|
(f) => f.startsWith("api-config-") && f.endsWith(".json") && f !== "api-config.json"
|
|
8791
9491
|
);
|
|
8792
9492
|
} catch {
|
|
@@ -8801,7 +9501,7 @@ function handleConsolidateIntegrations(input) {
|
|
|
8801
9501
|
entries.forEach((filename, index) => {
|
|
8802
9502
|
let artifact = {};
|
|
8803
9503
|
try {
|
|
8804
|
-
const raw =
|
|
9504
|
+
const raw = readFileSync21(join21(artifactsDir, filename), "utf-8");
|
|
8805
9505
|
artifact = JSON.parse(raw);
|
|
8806
9506
|
} catch {
|
|
8807
9507
|
}
|
|
@@ -8834,19 +9534,19 @@ function handleConsolidateIntegrations(input) {
|
|
|
8834
9534
|
});
|
|
8835
9535
|
const integrations = Array.from(integrationsByName.values());
|
|
8836
9536
|
const dedupedCount = entries.length - skippedIntegrationsList.length - integrations.length;
|
|
8837
|
-
const integrationsYmlPath =
|
|
9537
|
+
const integrationsYmlPath = join21(projectRoot, "stackwright.integrations.yml");
|
|
8838
9538
|
let existing = {};
|
|
8839
|
-
if (
|
|
9539
|
+
if (existsSync20(integrationsYmlPath)) {
|
|
8840
9540
|
try {
|
|
8841
|
-
const raw =
|
|
9541
|
+
const raw = readFileSync21(integrationsYmlPath, "utf-8");
|
|
8842
9542
|
existing = yaml3.load(raw) ?? {};
|
|
8843
9543
|
} catch {
|
|
8844
9544
|
existing = {};
|
|
8845
9545
|
}
|
|
8846
9546
|
}
|
|
8847
9547
|
const merged = { ...existing, integrations };
|
|
8848
|
-
mkdirSync9(
|
|
8849
|
-
if (!
|
|
9548
|
+
mkdirSync9(join21(projectRoot), { recursive: true });
|
|
9549
|
+
if (!existsSync20(integrationsYmlPath)) {
|
|
8850
9550
|
writeFileSync10(integrationsYmlPath, "", "utf-8");
|
|
8851
9551
|
}
|
|
8852
9552
|
const release = lockSync2(integrationsYmlPath, {
|
|
@@ -8885,7 +9585,7 @@ function registerConsolidateIntegrationsTool(server2) {
|
|
|
8885
9585
|
"no per-spec artifacts are found."
|
|
8886
9586
|
].join(" "),
|
|
8887
9587
|
{
|
|
8888
|
-
projectRoot:
|
|
9588
|
+
projectRoot: z28.string().describe("Absolute path to the project root directory")
|
|
8889
9589
|
},
|
|
8890
9590
|
async ({ projectRoot }) => {
|
|
8891
9591
|
try {
|
|
@@ -8921,7 +9621,7 @@ var package_default = {
|
|
|
8921
9621
|
"@stackwright-pro/pulse": "workspace:*",
|
|
8922
9622
|
"@stackwright-pro/telemetry": "workspace:*",
|
|
8923
9623
|
"@stackwright-pro/types": "workspace:*",
|
|
8924
|
-
"@stackwright/mcp": "0.6.0-alpha.
|
|
9624
|
+
"@stackwright/mcp": "0.6.0-alpha.5",
|
|
8925
9625
|
"@types/js-yaml": "^4.0.9",
|
|
8926
9626
|
"js-yaml": "^4.2.0",
|
|
8927
9627
|
"proper-lockfile": "^4.1.2",
|
|
@@ -8943,7 +9643,7 @@ var package_default = {
|
|
|
8943
9643
|
"test:coverage": "vitest run --coverage"
|
|
8944
9644
|
},
|
|
8945
9645
|
name: "@stackwright-pro/mcp",
|
|
8946
|
-
version: "0.2.0-alpha.
|
|
9646
|
+
version: "0.2.0-alpha.117",
|
|
8947
9647
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
8948
9648
|
license: "SEE LICENSE IN LICENSE",
|
|
8949
9649
|
main: "./dist/server.js",
|
|
@@ -9021,6 +9721,7 @@ registerValidateYamlFragmentTool(server);
|
|
|
9021
9721
|
registerProPageTools(server);
|
|
9022
9722
|
registerGetSchemaTool(server);
|
|
9023
9723
|
registerScaffoldCleanupTools(server);
|
|
9724
|
+
registerWriteWorkflowContainerPageTool(server);
|
|
9024
9725
|
registerContrastTools(server);
|
|
9025
9726
|
registerCompileTools(server);
|
|
9026
9727
|
registerStripLegacyIntegrationsTool(server);
|