@stackwright-pro/mcp 0.2.0-alpha.9 → 0.2.0-alpha.90
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/LICENSE +21 -0
- package/dist/integrity.d.mts +29 -1
- package/dist/integrity.d.ts +29 -1
- package/dist/integrity.js +69 -11
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +68 -11
- package/dist/integrity.mjs.map +1 -1
- package/dist/server.js +3486 -444
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +3536 -457
- package/dist/server.mjs.map +1 -1
- package/dist/tools/type-schemas.d.mts +51 -0
- package/dist/tools/type-schemas.d.ts +51 -0
- package/dist/tools/type-schemas.js +120 -0
- package/dist/tools/type-schemas.js.map +1 -0
- package/dist/tools/type-schemas.mjs +94 -0
- package/dist/tools/type-schemas.mjs.map +1 -0
- package/package.json +19 -8
package/dist/server.js
CHANGED
|
@@ -27,15 +27,44 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
27
27
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
28
28
|
|
|
29
29
|
// src/tools/data-explorer.ts
|
|
30
|
-
var
|
|
30
|
+
var import_zod2 = require("zod");
|
|
31
31
|
var import_cli_data_explorer = require("@stackwright-pro/cli-data-explorer");
|
|
32
|
+
|
|
33
|
+
// src/coerce.ts
|
|
34
|
+
var import_zod = require("zod");
|
|
35
|
+
function jsonCoerce(schema) {
|
|
36
|
+
return import_zod.z.preprocess((v) => {
|
|
37
|
+
if (typeof v === "string") {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(v);
|
|
40
|
+
} catch {
|
|
41
|
+
return v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
}, schema);
|
|
46
|
+
}
|
|
47
|
+
function boolCoerce(schema) {
|
|
48
|
+
return import_zod.z.preprocess((v) => v === "true" ? true : v === "false" ? false : v, schema);
|
|
49
|
+
}
|
|
50
|
+
function numCoerce(schema) {
|
|
51
|
+
return import_zod.z.preprocess((v) => {
|
|
52
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
53
|
+
const n = Number(v);
|
|
54
|
+
if (!Number.isNaN(n)) return n;
|
|
55
|
+
}
|
|
56
|
+
return v;
|
|
57
|
+
}, schema);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/tools/data-explorer.ts
|
|
32
61
|
function registerDataExplorerTools(server2) {
|
|
33
62
|
server2.tool(
|
|
34
63
|
"stackwright_pro_list_entities",
|
|
35
64
|
"List all available API entities from OpenAPI specs or generated Zod schemas. Use this to discover what entities are available before generating endpoint filters. Returns entity names, endpoints, and field counts. Part of the Pro Otter Raft for building API-integrated Stackwright applications.",
|
|
36
65
|
{
|
|
37
|
-
specPath:
|
|
38
|
-
projectRoot:
|
|
66
|
+
specPath: import_zod2.z.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
|
|
67
|
+
projectRoot: import_zod2.z.string().optional().describe("Project root directory (auto-detected if omitted)")
|
|
39
68
|
},
|
|
40
69
|
async ({ specPath, projectRoot }) => {
|
|
41
70
|
const result = (0, import_cli_data_explorer.listEntities)({
|
|
@@ -83,9 +112,13 @@ function registerDataExplorerTools(server2) {
|
|
|
83
112
|
"stackwright_pro_generate_filter",
|
|
84
113
|
"Generate endpoint filter configuration from selected entities. Creates include/exclude patterns for stackwright.yml OpenAPI integration. Use this after stackwright_pro_list_entities to select which API endpoints the application needs. Only selected endpoints will generate client code, reducing bundle size and improving security.",
|
|
85
114
|
{
|
|
86
|
-
selectedEntities:
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
selectedEntities: jsonCoerce(import_zod2.z.array(import_zod2.z.string())).describe(
|
|
116
|
+
'Entity slugs to include (e.g., ["equipment", "supplies"])'
|
|
117
|
+
),
|
|
118
|
+
excludePatterns: jsonCoerce(import_zod2.z.array(import_zod2.z.string()).optional()).describe(
|
|
119
|
+
'Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'
|
|
120
|
+
),
|
|
121
|
+
projectRoot: import_zod2.z.string().optional().describe("Project root directory")
|
|
89
122
|
},
|
|
90
123
|
async ({ selectedEntities, excludePatterns, projectRoot }) => {
|
|
91
124
|
const result = (0, import_cli_data_explorer.generateFilter)({
|
|
@@ -141,7 +174,7 @@ function registerDataExplorerTools(server2) {
|
|
|
141
174
|
}
|
|
142
175
|
|
|
143
176
|
// src/tools/security.ts
|
|
144
|
-
var
|
|
177
|
+
var import_zod3 = require("zod");
|
|
145
178
|
var import_crypto = require("crypto");
|
|
146
179
|
var import_fs = __toESM(require("fs"));
|
|
147
180
|
var import_path = __toESM(require("path"));
|
|
@@ -150,8 +183,8 @@ function registerSecurityTools(server2) {
|
|
|
150
183
|
"stackwright_pro_validate_spec",
|
|
151
184
|
"Validate an OpenAPI spec against the enterprise approved-specs configuration. Checks if the spec URL is on the allowlist and verifies SHA-256 hash integrity. Use this in enterprise environments where only pre-approved API specs are allowed. Fails build if spec is not approved or has been modified.",
|
|
152
185
|
{
|
|
153
|
-
specPath:
|
|
154
|
-
configPath:
|
|
186
|
+
specPath: import_zod3.z.string().describe("URL or file path to the OpenAPI spec to validate"),
|
|
187
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml with prebuild.security config")
|
|
155
188
|
},
|
|
156
189
|
async ({ specPath, configPath }) => {
|
|
157
190
|
let securityEnabled = false;
|
|
@@ -259,16 +292,15 @@ Status: Valid (${allowlist.length} specs on allowlist)`
|
|
|
259
292
|
"stackwright_pro_add_approved_spec",
|
|
260
293
|
"Add an OpenAPI spec to the approved-specs allowlist in stackwright.yml. Computes the SHA-256 hash of the spec and adds it to the security configuration. Use this when onboarding a new API in enterprise environments.",
|
|
261
294
|
{
|
|
262
|
-
name:
|
|
263
|
-
url:
|
|
264
|
-
configPath:
|
|
295
|
+
name: import_zod3.z.string().describe("Human-readable name for the spec"),
|
|
296
|
+
url: import_zod3.z.string().describe("URL or file path to the OpenAPI spec"),
|
|
297
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
|
|
265
298
|
},
|
|
266
299
|
async ({ name, url, configPath }) => {
|
|
267
300
|
const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
|
|
268
301
|
let sha256 = "<computed-at-build>";
|
|
269
302
|
try {
|
|
270
303
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
271
|
-
sha256 = "<computed-at-build>";
|
|
272
304
|
} else if (import_fs.default.existsSync(url)) {
|
|
273
305
|
const specContent = import_fs.default.readFileSync(url, "utf8");
|
|
274
306
|
sha256 = (0, import_crypto.createHash)("sha256").update(specContent).digest("hex");
|
|
@@ -315,7 +347,7 @@ SHA-256 computed: ${sha256}`
|
|
|
315
347
|
"stackwright_pro_list_approved_specs",
|
|
316
348
|
"List all specs currently on the approved-specs allowlist. Shows spec names, URLs, and hash prefixes. Use this to audit what APIs are approved in the project.",
|
|
317
349
|
{
|
|
318
|
-
configPath:
|
|
350
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
|
|
319
351
|
},
|
|
320
352
|
async ({ configPath }) => {
|
|
321
353
|
const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
|
|
@@ -384,16 +416,18 @@ SHA-256 computed: ${sha256}`
|
|
|
384
416
|
}
|
|
385
417
|
|
|
386
418
|
// src/tools/isr.ts
|
|
387
|
-
var
|
|
419
|
+
var import_zod4 = require("zod");
|
|
388
420
|
function registerIsrTools(server2) {
|
|
389
421
|
server2.tool(
|
|
390
422
|
"stackwright_pro_configure_isr",
|
|
391
423
|
"Configure Incremental Static Regeneration (ISR) for an API-backed collection. ISR allows API data to be cached and refreshed on a schedule, providing real-time data with the performance of static generation. Use this after stackwright_pro_generate_filter to set revalidation intervals.",
|
|
392
424
|
{
|
|
393
|
-
collection:
|
|
394
|
-
revalidateSeconds:
|
|
395
|
-
|
|
396
|
-
|
|
425
|
+
collection: import_zod4.z.string().describe('Collection name (e.g., "equipment", "supplies")'),
|
|
426
|
+
revalidateSeconds: numCoerce(import_zod4.z.number().optional()).describe(
|
|
427
|
+
"Revalidation interval in seconds (default: 60)"
|
|
428
|
+
),
|
|
429
|
+
fallback: import_zod4.z.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
|
|
430
|
+
configPath: import_zod4.z.string().optional().describe("Path to stackwright.yml")
|
|
397
431
|
},
|
|
398
432
|
async ({ collection, revalidateSeconds = 60, fallback = "blocking", configPath }) => {
|
|
399
433
|
const revalidate = revalidateSeconds;
|
|
@@ -404,7 +438,7 @@ function registerIsrTools(server2) {
|
|
|
404
438
|
isr:
|
|
405
439
|
revalidate: ${revalidate}
|
|
406
440
|
fallback: ${fallback}`;
|
|
407
|
-
let description
|
|
441
|
+
let description;
|
|
408
442
|
if (revalidate < 60) {
|
|
409
443
|
description = "\u26A1 Very fresh data (revalidate every " + revalidate + "s)";
|
|
410
444
|
} else if (revalidate < 300) {
|
|
@@ -436,14 +470,16 @@ ${yamlSnippet}
|
|
|
436
470
|
"stackwright_pro_configure_isr_batch",
|
|
437
471
|
"Configure ISR for multiple collections at once. More efficient than calling stackwright_pro_configure_isr multiple times. Provide different revalidation intervals based on data freshness requirements.",
|
|
438
472
|
{
|
|
439
|
-
collections:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
473
|
+
collections: jsonCoerce(
|
|
474
|
+
import_zod4.z.array(
|
|
475
|
+
import_zod4.z.object({
|
|
476
|
+
name: import_zod4.z.string().describe("Collection name"),
|
|
477
|
+
revalidateSeconds: numCoerce(import_zod4.z.number().optional()).describe("Revalidation interval")
|
|
478
|
+
})
|
|
479
|
+
)
|
|
444
480
|
).describe("Array of collection configurations"),
|
|
445
|
-
defaultFallback:
|
|
446
|
-
configPath:
|
|
481
|
+
defaultFallback: import_zod4.z.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
|
|
482
|
+
configPath: import_zod4.z.string().optional().describe("Path to stackwright.yml")
|
|
447
483
|
},
|
|
448
484
|
async ({ collections, defaultFallback = "blocking", configPath }) => {
|
|
449
485
|
const lines = [`\u2699\uFE0F Batch ISR Configuration:
|
|
@@ -471,17 +507,17 @@ Fallback: ${defaultFallback}`);
|
|
|
471
507
|
}
|
|
472
508
|
|
|
473
509
|
// src/tools/dashboard.ts
|
|
474
|
-
var
|
|
510
|
+
var import_zod5 = require("zod");
|
|
475
511
|
var import_cli_data_explorer2 = require("@stackwright-pro/cli-data-explorer");
|
|
476
512
|
function registerDashboardTools(server2) {
|
|
477
513
|
server2.tool(
|
|
478
514
|
"stackwright_pro_generate_dashboard",
|
|
479
515
|
"Generate a dashboard page configuration for displaying API data. Creates YAML content for a Stackwright page with grid, metric_card, data_table, and collection_list content types. Use this after stackwright_pro_generate_filter to create pages for your API collections.",
|
|
480
516
|
{
|
|
481
|
-
entities:
|
|
482
|
-
layout:
|
|
483
|
-
pageTitle:
|
|
484
|
-
specPath:
|
|
517
|
+
entities: jsonCoerce(import_zod5.z.array(import_zod5.z.string())).describe("Entity slugs to include in dashboard"),
|
|
518
|
+
layout: import_zod5.z.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
|
|
519
|
+
pageTitle: import_zod5.z.string().optional().describe("Page title"),
|
|
520
|
+
specPath: import_zod5.z.string().optional().describe("Path to OpenAPI spec for entity details")
|
|
485
521
|
},
|
|
486
522
|
async ({ entities, layout = "mixed", pageTitle, specPath }) => {
|
|
487
523
|
let entityDetails = [];
|
|
@@ -567,9 +603,9 @@ ${yaml}
|
|
|
567
603
|
"stackwright_pro_generate_detail_page",
|
|
568
604
|
"Generate a detail view page for a single API entity. Creates YAML content for displaying all fields of an individual record. Use this to create detail pages that complement collection listing pages.",
|
|
569
605
|
{
|
|
570
|
-
entity:
|
|
571
|
-
slugField:
|
|
572
|
-
specPath:
|
|
606
|
+
entity: import_zod5.z.string().describe('Entity slug (e.g., "equipment")'),
|
|
607
|
+
slugField: import_zod5.z.string().optional().describe('Field to use as URL slug (default: "id")'),
|
|
608
|
+
specPath: import_zod5.z.string().optional().describe("Path to OpenAPI spec for field details")
|
|
573
609
|
},
|
|
574
610
|
async ({ entity, slugField = "id", specPath }) => {
|
|
575
611
|
let fields = [];
|
|
@@ -592,39 +628,49 @@ ${yaml}
|
|
|
592
628
|
` title: "${entityName} Details | {{ ${entity}.${slugField} }}"`,
|
|
593
629
|
"",
|
|
594
630
|
" content_items:",
|
|
595
|
-
" -
|
|
596
|
-
'
|
|
597
|
-
"
|
|
598
|
-
`
|
|
599
|
-
'
|
|
600
|
-
"
|
|
601
|
-
`
|
|
602
|
-
'
|
|
603
|
-
' background: "primary"',
|
|
604
|
-
' color: "text"',
|
|
631
|
+
" - type: text_block",
|
|
632
|
+
' label: "detail-header"',
|
|
633
|
+
" heading:",
|
|
634
|
+
` text: "{{ ${entity}.${slugField} }}"`,
|
|
635
|
+
' textSize: "h1"',
|
|
636
|
+
" textBlocks:",
|
|
637
|
+
` - text: "Details for this ${entity}"`,
|
|
638
|
+
' textSize: "body1"',
|
|
605
639
|
"",
|
|
606
|
-
" - grid
|
|
607
|
-
'
|
|
608
|
-
"
|
|
609
|
-
" items:"
|
|
640
|
+
" - type: grid",
|
|
641
|
+
' label: "detail-fields"',
|
|
642
|
+
" columns:"
|
|
610
643
|
];
|
|
611
644
|
const displayFields = fields.length > 0 ? fields.slice(0, 8) : [
|
|
612
645
|
{ name: slugField, type: "string" },
|
|
613
646
|
{ name: "created_at", type: "datetime" },
|
|
614
647
|
{ name: "updated_at", type: "datetime" }
|
|
615
648
|
];
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
yamlLines.push(
|
|
621
|
-
yamlLines.push(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
649
|
+
const mid = Math.ceil(displayFields.length / 2);
|
|
650
|
+
const col1 = displayFields.slice(0, mid);
|
|
651
|
+
const col2 = displayFields.slice(mid);
|
|
652
|
+
for (const [colIndex, colFields] of [col1, col2].entries()) {
|
|
653
|
+
yamlLines.push(" - width: 1");
|
|
654
|
+
yamlLines.push(" content_items:");
|
|
655
|
+
for (const field of colFields) {
|
|
656
|
+
const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
657
|
+
yamlLines.push(" - type: text_block");
|
|
658
|
+
yamlLines.push(` label: "${field.name}-field"`);
|
|
659
|
+
yamlLines.push(" heading:");
|
|
660
|
+
yamlLines.push(` text: "${label}"`);
|
|
661
|
+
yamlLines.push(' textSize: "h4"');
|
|
662
|
+
yamlLines.push(" textBlocks:");
|
|
663
|
+
yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
|
|
664
|
+
yamlLines.push(' textSize: "body1"');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
yamlLines.push("");
|
|
668
|
+
yamlLines.push(" - type: action_bar");
|
|
669
|
+
yamlLines.push(" actions:");
|
|
670
|
+
yamlLines.push(` - label: "<- Back to ${entityName}"`);
|
|
671
|
+
yamlLines.push(" action: navigate");
|
|
672
|
+
yamlLines.push(` href: "/${entity}"`);
|
|
673
|
+
yamlLines.push(" style: secondary");
|
|
628
674
|
const yaml = yamlLines.join("\n");
|
|
629
675
|
return {
|
|
630
676
|
content: [
|
|
@@ -645,7 +691,7 @@ ${yaml}
|
|
|
645
691
|
}
|
|
646
692
|
|
|
647
693
|
// src/tools/clarification.ts
|
|
648
|
-
var
|
|
694
|
+
var import_zod6 = require("zod");
|
|
649
695
|
var CONTRADICTION_PATTERNS = [
|
|
650
696
|
{
|
|
651
697
|
keywords: ["minimal", "clean", "simple"],
|
|
@@ -733,12 +779,14 @@ function registerClarificationTools(server2) {
|
|
|
733
779
|
"stackwright_pro_clarify",
|
|
734
780
|
"Ask the user for clarification when a specialist otter encounters ambiguity. This is for MID-EXECUTION questions, NOT upfront question collection (use the Question Manifest Protocol for that). Returns a structured response for the foreman to present to the user via ask_user_question (closed_choice) or directly (open_text).",
|
|
735
781
|
{
|
|
736
|
-
context:
|
|
737
|
-
question_type:
|
|
738
|
-
question:
|
|
739
|
-
choices:
|
|
740
|
-
|
|
741
|
-
|
|
782
|
+
context: import_zod6.z.string().optional().describe("Context about what the otter is trying to do"),
|
|
783
|
+
question_type: import_zod6.z.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
|
|
784
|
+
question: import_zod6.z.string().describe("The clarification question to ask the user"),
|
|
785
|
+
choices: jsonCoerce(import_zod6.z.array(import_zod6.z.string()).optional()).describe(
|
|
786
|
+
"Options for closed_choice questions"
|
|
787
|
+
),
|
|
788
|
+
priority: import_zod6.z.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
|
|
789
|
+
target_field: import_zod6.z.string().optional().describe("What field/config does this clarify?")
|
|
742
790
|
},
|
|
743
791
|
async ({ context, question_type, question, choices, priority, target_field }) => {
|
|
744
792
|
const result = handleClarify({
|
|
@@ -767,8 +815,10 @@ function registerClarificationTools(server2) {
|
|
|
767
815
|
"stackwright_pro_detect_conflict",
|
|
768
816
|
"Detect when a user's stated preference conflicts with their selected choices. Uses keyword heuristics against known contradiction patterns (minimal vs vibrant, dark vs light, etc). Returns conflict details and resolution options.",
|
|
769
817
|
{
|
|
770
|
-
stated_preference:
|
|
771
|
-
selected_values:
|
|
818
|
+
stated_preference: import_zod6.z.string().describe("What the user said they wanted"),
|
|
819
|
+
selected_values: jsonCoerce(import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string())).describe(
|
|
820
|
+
"What the user actually selected (field \u2192 value)"
|
|
821
|
+
)
|
|
772
822
|
},
|
|
773
823
|
async ({ stated_preference, selected_values }) => {
|
|
774
824
|
const result = handleDetectConflict({ stated_preference, selected_values });
|
|
@@ -813,7 +863,7 @@ ${result.resolution_options?.map((o) => ` \u2022 ${o}`).join("\n")}
|
|
|
813
863
|
}
|
|
814
864
|
|
|
815
865
|
// src/tools/packages.ts
|
|
816
|
-
var
|
|
866
|
+
var import_zod7 = require("zod");
|
|
817
867
|
var import_fs2 = require("fs");
|
|
818
868
|
var import_child_process = require("child_process");
|
|
819
869
|
var import_path2 = __toESM(require("path"));
|
|
@@ -834,17 +884,23 @@ function registerPackageTools(server2) {
|
|
|
834
884
|
"Ensures pro packages are present in a project's package.json. Safe to call multiple times \u2014 never overwrites existing version pins. Use this to bootstrap dependencies before specialist otters run. Pass includeBaseline: true to automatically include all required @stackwright-pro/* baseline dependencies. Safe to call on existing projects \u2014 never overwrites pinned versions.",
|
|
835
885
|
{
|
|
836
886
|
// FIX 3 (B-new-1): Zod v4 requires two-arg z.record(keySchema, valueSchema)
|
|
837
|
-
packages:
|
|
838
|
-
'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }'
|
|
887
|
+
packages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional().default({})).describe(
|
|
888
|
+
'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }. Omit or pass {} when using includeBaseline: true.'
|
|
889
|
+
),
|
|
890
|
+
devPackages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
|
|
891
|
+
"devDependencies to add. Same format as packages."
|
|
839
892
|
),
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
893
|
+
scripts: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
|
|
894
|
+
"npm scripts to add. Only adds if key does not already exist."
|
|
895
|
+
),
|
|
896
|
+
targetDir: import_zod7.z.string().optional().describe(
|
|
843
897
|
"Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
|
|
844
898
|
),
|
|
845
|
-
runInstall:
|
|
846
|
-
|
|
847
|
-
|
|
899
|
+
runInstall: boolCoerce(import_zod7.z.boolean().optional().default(true)).describe(
|
|
900
|
+
"Run pnpm install after writing package.json. Defaults to true. Pass boolean true/false."
|
|
901
|
+
),
|
|
902
|
+
includeBaseline: boolCoerce(import_zod7.z.boolean().optional().default(false)).describe(
|
|
903
|
+
"When true, automatically merges BASELINE_DEPS and BASELINE_DEV_DEPS before applying packages/devPackages args. Safe to call on existing projects \u2014 never overwrites pinned versions. Pass boolean true/false."
|
|
848
904
|
)
|
|
849
905
|
},
|
|
850
906
|
async ({ packages, devPackages, scripts, targetDir, runInstall, includeBaseline }) => {
|
|
@@ -1002,11 +1058,11 @@ function setupPackages(opts) {
|
|
|
1002
1058
|
}
|
|
1003
1059
|
}
|
|
1004
1060
|
const raw = (0, import_fs2.readFileSync)(realPackageJsonPath, "utf8");
|
|
1005
|
-
const PackageJsonSchema =
|
|
1061
|
+
const PackageJsonSchema = import_zod7.z.object({
|
|
1006
1062
|
// Zod v4: z.record(keySchema, valueSchema) — two-arg form required
|
|
1007
|
-
dependencies:
|
|
1008
|
-
devDependencies:
|
|
1009
|
-
scripts:
|
|
1063
|
+
dependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
|
|
1064
|
+
devDependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
|
|
1065
|
+
scripts: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()
|
|
1010
1066
|
}).passthrough();
|
|
1011
1067
|
const schemaResult = PackageJsonSchema.safeParse(JSON.parse(raw));
|
|
1012
1068
|
if (!schemaResult.success) {
|
|
@@ -1088,8 +1144,9 @@ function setupPackages(opts) {
|
|
|
1088
1144
|
|
|
1089
1145
|
// src/tools/questions.ts
|
|
1090
1146
|
var import_promises = require("fs/promises");
|
|
1147
|
+
var import_node_fs = require("fs");
|
|
1091
1148
|
var import_node_path = require("path");
|
|
1092
|
-
var
|
|
1149
|
+
var import_zod8 = require("zod");
|
|
1093
1150
|
|
|
1094
1151
|
// src/question-adapter.ts
|
|
1095
1152
|
function truncate(str, maxLength) {
|
|
@@ -1274,23 +1331,30 @@ function answersToManifestFormat(answers, questions) {
|
|
|
1274
1331
|
return result;
|
|
1275
1332
|
}
|
|
1276
1333
|
|
|
1334
|
+
// src/validation.ts
|
|
1335
|
+
var SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1336
|
+
var SAFE_QUESTION_ID = /^[a-z0-9][a-z0-9-]{0,63}$/i;
|
|
1337
|
+
function isValidPhase(phase) {
|
|
1338
|
+
return SAFE_PHASE.test(phase);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1277
1341
|
// src/tools/questions.ts
|
|
1278
|
-
var ManifestQuestionSchema =
|
|
1279
|
-
id:
|
|
1280
|
-
question:
|
|
1281
|
-
type:
|
|
1282
|
-
required:
|
|
1283
|
-
options:
|
|
1284
|
-
|
|
1285
|
-
label:
|
|
1286
|
-
value:
|
|
1342
|
+
var ManifestQuestionSchema = import_zod8.z.object({
|
|
1343
|
+
id: import_zod8.z.string(),
|
|
1344
|
+
question: import_zod8.z.string(),
|
|
1345
|
+
type: import_zod8.z.enum(["text", "select", "multi-select", "confirm"]),
|
|
1346
|
+
required: import_zod8.z.boolean().optional(),
|
|
1347
|
+
options: import_zod8.z.array(
|
|
1348
|
+
import_zod8.z.object({
|
|
1349
|
+
label: import_zod8.z.string(),
|
|
1350
|
+
value: import_zod8.z.string()
|
|
1287
1351
|
})
|
|
1288
1352
|
).optional(),
|
|
1289
|
-
default:
|
|
1290
|
-
help:
|
|
1291
|
-
dependsOn:
|
|
1292
|
-
questionId:
|
|
1293
|
-
value:
|
|
1353
|
+
default: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.boolean(), import_zod8.z.array(import_zod8.z.string())]).optional(),
|
|
1354
|
+
help: import_zod8.z.string().optional(),
|
|
1355
|
+
dependsOn: import_zod8.z.object({
|
|
1356
|
+
questionId: import_zod8.z.string(),
|
|
1357
|
+
value: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.array(import_zod8.z.string())])
|
|
1294
1358
|
}).optional()
|
|
1295
1359
|
});
|
|
1296
1360
|
function registerQuestionTools(server2) {
|
|
@@ -1298,15 +1362,16 @@ function registerQuestionTools(server2) {
|
|
|
1298
1362
|
"stackwright_pro_present_phase_questions",
|
|
1299
1363
|
"Adapt manifest-format questions from a specialist otter and present them to the user via ask_user_question. Pass only the phase name \u2014 this tool reads questions from .stackwright/question-manifest.json automatically. The questions parameter is optional and only needed if the manifest has not been written yet. Use this instead of calling ask_user_question directly \u2014 it handles label truncation (50-char limit), header generation, confirm/text defaults, and correct array formatting automatically. IMPORTANT: This is the ONLY approved way to prepare questions before calling ask_user_question. Never call ask_user_question with raw manifest questions. Never retry ask_user_question validation errors by calling it directly \u2014 always re-call this tool.",
|
|
1300
1364
|
{
|
|
1301
|
-
phase:
|
|
1302
|
-
questions:
|
|
1365
|
+
phase: import_zod8.z.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
|
|
1366
|
+
questions: jsonCoerce(import_zod8.z.array(ManifestQuestionSchema).optional()).describe(
|
|
1303
1367
|
"Questions in Question Manifest format. If omitted, questions are read from .stackwright/question-manifest.json using the phase name."
|
|
1304
1368
|
),
|
|
1305
|
-
answers:
|
|
1369
|
+
answers: jsonCoerce(
|
|
1370
|
+
import_zod8.z.record(import_zod8.z.string(), import_zod8.z.union([import_zod8.z.string(), import_zod8.z.array(import_zod8.z.string()), import_zod8.z.boolean()])).optional()
|
|
1371
|
+
).describe("Previously collected answers used to resolve dependsOn conditions")
|
|
1306
1372
|
},
|
|
1307
1373
|
async ({ phase, questions, answers }) => {
|
|
1308
1374
|
let resolvedQuestions;
|
|
1309
|
-
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1310
1375
|
if (!SAFE_PHASE.test(phase)) {
|
|
1311
1376
|
return {
|
|
1312
1377
|
content: [
|
|
@@ -1353,17 +1418,36 @@ function registerQuestionTools(server2) {
|
|
|
1353
1418
|
}
|
|
1354
1419
|
}
|
|
1355
1420
|
const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
|
|
1421
|
+
const labelSchema = import_zod8.z.string().max(50, "Value should have at most 50 characters");
|
|
1422
|
+
for (const q of adapted) {
|
|
1423
|
+
for (const opt of q.options) {
|
|
1424
|
+
const check = labelSchema.safeParse(opt.label);
|
|
1425
|
+
if (!check.success) {
|
|
1426
|
+
return {
|
|
1427
|
+
content: [
|
|
1428
|
+
{
|
|
1429
|
+
type: "text",
|
|
1430
|
+
text: JSON.stringify({
|
|
1431
|
+
error: `Option label for phase "${phase}" exceeds 50 characters: ${check.error.issues[0]?.message ?? "label too long"}. Truncate option labels to \u226450 characters before calling this tool.`
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
1434
|
+
],
|
|
1435
|
+
isError: true
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1356
1440
|
if (adapted.length === 0) {
|
|
1357
1441
|
return {
|
|
1358
1442
|
content: [
|
|
1359
1443
|
{
|
|
1360
1444
|
type: "text",
|
|
1361
|
-
text:
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1445
|
+
text: `Phase "${phase}" has no questions to present. Do NOT call ask_user_question. Call stackwright_pro_save_phase_answers({ phase: "${phase}", rawAnswers: [] }) directly, then proceed to the execution step for this phase.`
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
type: "text",
|
|
1449
|
+
// Empty array — second block always present so the foreman's two-block contract holds
|
|
1450
|
+
text: JSON.stringify([])
|
|
1367
1451
|
}
|
|
1368
1452
|
],
|
|
1369
1453
|
isError: false
|
|
@@ -1384,31 +1468,175 @@ function registerQuestionTools(server2) {
|
|
|
1384
1468
|
};
|
|
1385
1469
|
}
|
|
1386
1470
|
);
|
|
1471
|
+
server2.tool(
|
|
1472
|
+
"stackwright_pro_get_next_question",
|
|
1473
|
+
"Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
|
|
1474
|
+
{ phase: import_zod8.z.string().describe('Phase name e.g. "designer", "api", "auth"') },
|
|
1475
|
+
async ({ phase }) => {
|
|
1476
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1477
|
+
return {
|
|
1478
|
+
content: [
|
|
1479
|
+
{
|
|
1480
|
+
type: "text",
|
|
1481
|
+
text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
|
|
1482
|
+
}
|
|
1483
|
+
],
|
|
1484
|
+
isError: true
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
const cwd = process.cwd();
|
|
1488
|
+
const questionsPath = (0, import_node_path.join)(cwd, ".stackwright", "questions", `${phase}.json`);
|
|
1489
|
+
let questions = [];
|
|
1490
|
+
try {
|
|
1491
|
+
const raw = await (0, import_promises.readFile)(questionsPath, "utf-8");
|
|
1492
|
+
const parsed = JSON.parse(raw);
|
|
1493
|
+
questions = parsed.questions ?? [];
|
|
1494
|
+
} catch {
|
|
1495
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1496
|
+
}
|
|
1497
|
+
if (questions.length === 0) {
|
|
1498
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1499
|
+
}
|
|
1500
|
+
const answersPath = (0, import_node_path.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1501
|
+
let answeredIds = /* @__PURE__ */ new Set();
|
|
1502
|
+
try {
|
|
1503
|
+
const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
|
|
1504
|
+
const parsed = JSON.parse(raw);
|
|
1505
|
+
answeredIds = new Set(Object.keys(parsed.answers ?? {}));
|
|
1506
|
+
} catch {
|
|
1507
|
+
}
|
|
1508
|
+
const next = questions.find((q) => !answeredIds.has(q.id));
|
|
1509
|
+
if (!next) {
|
|
1510
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
content: [
|
|
1514
|
+
{
|
|
1515
|
+
type: "text",
|
|
1516
|
+
text: JSON.stringify({
|
|
1517
|
+
done: false,
|
|
1518
|
+
questionId: next.id,
|
|
1519
|
+
question: next.question,
|
|
1520
|
+
type: next.type,
|
|
1521
|
+
options: next.options ?? null,
|
|
1522
|
+
help: next.help ?? null,
|
|
1523
|
+
index: questions.indexOf(next) + 1,
|
|
1524
|
+
total: questions.length
|
|
1525
|
+
})
|
|
1526
|
+
}
|
|
1527
|
+
]
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
);
|
|
1531
|
+
server2.tool(
|
|
1532
|
+
"stackwright_pro_record_answer",
|
|
1533
|
+
"Records a single answer to a phase question. Appends to .stackwright/answers/{phase}.json incrementally. Idempotent \u2014 calling twice for the same questionId overwrites. Call after receiving each user reply in the conversational question loop.",
|
|
1534
|
+
{
|
|
1535
|
+
phase: import_zod8.z.string().describe('Phase name e.g. "designer"'),
|
|
1536
|
+
questionId: import_zod8.z.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
|
|
1537
|
+
answer: import_zod8.z.string().describe("The user's free-text answer")
|
|
1538
|
+
},
|
|
1539
|
+
async ({ phase, questionId, answer }) => {
|
|
1540
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1541
|
+
return {
|
|
1542
|
+
content: [
|
|
1543
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
|
|
1544
|
+
],
|
|
1545
|
+
isError: true
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
if (!SAFE_QUESTION_ID.test(questionId)) {
|
|
1549
|
+
return {
|
|
1550
|
+
content: [
|
|
1551
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
|
|
1552
|
+
],
|
|
1553
|
+
isError: true
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
const safeAnswer = answer.slice(0, 2e3);
|
|
1557
|
+
const cwd = process.cwd();
|
|
1558
|
+
const answersDir = (0, import_node_path.join)(cwd, ".stackwright", "answers");
|
|
1559
|
+
const answersPath = (0, import_node_path.join)(answersDir, `${phase}.json`);
|
|
1560
|
+
if ((0, import_node_fs.existsSync)(answersDir) && (0, import_node_fs.lstatSync)(answersDir).isSymbolicLink()) {
|
|
1561
|
+
return {
|
|
1562
|
+
content: [
|
|
1563
|
+
{
|
|
1564
|
+
type: "text",
|
|
1565
|
+
text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
|
|
1566
|
+
}
|
|
1567
|
+
],
|
|
1568
|
+
isError: true
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
(0, import_node_fs.mkdirSync)(answersDir, { recursive: true });
|
|
1572
|
+
let existing = {
|
|
1573
|
+
version: "1.0",
|
|
1574
|
+
phase,
|
|
1575
|
+
answers: {}
|
|
1576
|
+
};
|
|
1577
|
+
try {
|
|
1578
|
+
if ((0, import_node_fs.existsSync)(answersPath)) {
|
|
1579
|
+
if ((0, import_node_fs.lstatSync)(answersPath).isSymbolicLink()) {
|
|
1580
|
+
return {
|
|
1581
|
+
content: [
|
|
1582
|
+
{
|
|
1583
|
+
type: "text",
|
|
1584
|
+
text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
|
|
1585
|
+
}
|
|
1586
|
+
],
|
|
1587
|
+
isError: true
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
|
|
1591
|
+
existing = JSON.parse(raw);
|
|
1592
|
+
}
|
|
1593
|
+
} catch {
|
|
1594
|
+
}
|
|
1595
|
+
existing.answers[questionId] = safeAnswer;
|
|
1596
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1597
|
+
const tmp = `${answersPath}.tmp`;
|
|
1598
|
+
await (0, import_promises.writeFile)(tmp, JSON.stringify(existing, null, 2), "utf-8");
|
|
1599
|
+
(0, import_node_fs.renameSync)(tmp, answersPath);
|
|
1600
|
+
return {
|
|
1601
|
+
content: [
|
|
1602
|
+
{ type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
|
|
1603
|
+
]
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
);
|
|
1387
1607
|
}
|
|
1388
1608
|
|
|
1389
1609
|
// src/tools/orchestration.ts
|
|
1390
|
-
var
|
|
1610
|
+
var import_zod9 = require("zod");
|
|
1391
1611
|
var import_fs3 = require("fs");
|
|
1392
1612
|
var import_path3 = require("path");
|
|
1393
1613
|
var OTTER_NAME_TO_PHASE = [
|
|
1394
1614
|
["designer", "designer"],
|
|
1395
1615
|
["theme", "theme"],
|
|
1616
|
+
["scaffold", "scaffold"],
|
|
1396
1617
|
["api", "api"],
|
|
1397
1618
|
["auth", "auth"],
|
|
1398
1619
|
["dashboard", "dashboard"],
|
|
1399
1620
|
["data", "data"],
|
|
1400
1621
|
["page", "pages"],
|
|
1401
|
-
["workflow", "workflow"]
|
|
1622
|
+
["workflow", "workflow"],
|
|
1623
|
+
["services", "services"],
|
|
1624
|
+
["polish", "polish"],
|
|
1625
|
+
["geo", "geo"]
|
|
1402
1626
|
];
|
|
1403
1627
|
var PHASE_TO_OTTER = {
|
|
1404
1628
|
designer: "stackwright-pro-designer-otter",
|
|
1405
1629
|
theme: "stackwright-pro-theme-otter",
|
|
1630
|
+
scaffold: "stackwright-pro-scaffold-otter",
|
|
1406
1631
|
api: "stackwright-pro-api-otter",
|
|
1407
1632
|
auth: "stackwright-pro-auth-otter",
|
|
1408
1633
|
pages: "stackwright-pro-page-otter",
|
|
1409
1634
|
dashboard: "stackwright-pro-dashboard-otter",
|
|
1410
1635
|
data: "stackwright-pro-data-otter",
|
|
1411
|
-
workflow: "stackwright-pro-workflow-otter"
|
|
1636
|
+
workflow: "stackwright-pro-workflow-otter",
|
|
1637
|
+
services: "stackwright-services-otter",
|
|
1638
|
+
polish: "stackwright-pro-polish-otter",
|
|
1639
|
+
geo: "stackwright-pro-geo-otter"
|
|
1412
1640
|
};
|
|
1413
1641
|
function detectPhase(otterName) {
|
|
1414
1642
|
const lower = otterName.toLowerCase();
|
|
@@ -1474,14 +1702,78 @@ function handleSaveManifest(input) {
|
|
|
1474
1702
|
}
|
|
1475
1703
|
}
|
|
1476
1704
|
function handleSavePhaseAnswers(input) {
|
|
1705
|
+
if (!isValidPhase(input.phase)) {
|
|
1706
|
+
return {
|
|
1707
|
+
text: JSON.stringify({
|
|
1708
|
+
success: false,
|
|
1709
|
+
error: `Invalid phase name: "${input.phase.slice(0, 50)}". Must be lowercase alphanumeric with hyphens, max 31 chars.`
|
|
1710
|
+
}),
|
|
1711
|
+
isError: true
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
for (let i = 0; i < input.rawAnswers.length; i++) {
|
|
1715
|
+
const a = input.rawAnswers[i];
|
|
1716
|
+
if (!a.question_header || a.question_header.trim().length === 0) {
|
|
1717
|
+
return {
|
|
1718
|
+
text: JSON.stringify({
|
|
1719
|
+
success: false,
|
|
1720
|
+
error: `rawAnswers[${i}].question_header is empty \u2014 every answer must have a non-empty question_header.`
|
|
1721
|
+
}),
|
|
1722
|
+
isError: true
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
if (!a.selected_options || a.selected_options.length === 0) {
|
|
1726
|
+
return {
|
|
1727
|
+
text: JSON.stringify({
|
|
1728
|
+
success: false,
|
|
1729
|
+
error: `rawAnswers[${i}].selected_options is empty for question_header "${a.question_header}" \u2014 every answer must have at least one selected option.`
|
|
1730
|
+
}),
|
|
1731
|
+
isError: true
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (input.questions && input.questions.length > 0 && input.rawAnswers.length === 0) {
|
|
1736
|
+
return {
|
|
1737
|
+
text: JSON.stringify({
|
|
1738
|
+
success: false,
|
|
1739
|
+
error: `${input.questions.length} questions provided but rawAnswers is empty \u2014 this is a malformed payload. If the phase truly has no questions, omit the questions parameter.`
|
|
1740
|
+
}),
|
|
1741
|
+
isError: true
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1477
1744
|
const cwd = input._cwd ?? process.cwd();
|
|
1478
1745
|
const dir = (0, import_path3.join)(cwd, ".stackwright", "answers");
|
|
1479
1746
|
const filePath = (0, import_path3.join)(dir, `${input.phase}.json`);
|
|
1480
1747
|
try {
|
|
1481
1748
|
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
1749
|
+
const warnings = [];
|
|
1482
1750
|
let answers;
|
|
1483
1751
|
if (input.questions && input.questions.length > 0) {
|
|
1484
|
-
|
|
1752
|
+
try {
|
|
1753
|
+
answers = answersToManifestFormat(input.rawAnswers, input.questions);
|
|
1754
|
+
} catch (mapErr) {
|
|
1755
|
+
answers = {};
|
|
1756
|
+
warnings.push(
|
|
1757
|
+
`Reverse-mapping threw (${mapErr instanceof Error ? mapErr.message : String(mapErr)}) \u2014 falling back to direct key mapping.`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
if (Object.keys(answers).length === 0 && input.rawAnswers.length > 0) {
|
|
1761
|
+
answers = Object.fromEntries(
|
|
1762
|
+
input.rawAnswers.map((a) => [
|
|
1763
|
+
a.question_header,
|
|
1764
|
+
a.selected_options.length > 1 ? a.selected_options : a.selected_options[0] ?? ""
|
|
1765
|
+
])
|
|
1766
|
+
);
|
|
1767
|
+
warnings.push(
|
|
1768
|
+
`Reverse-mapping produced empty answers despite ${input.rawAnswers.length} rawAnswers \u2014 used direct key mapping as fallback. Ensure question_header values match the manifest header format (e.g. "DESI-1", not descriptive keys).`
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
const requiredCount = input.questions.filter((q) => q.required !== false).length;
|
|
1772
|
+
if (input.rawAnswers.length < requiredCount) {
|
|
1773
|
+
warnings.push(
|
|
1774
|
+
`Only ${input.rawAnswers.length} answers provided for ${requiredCount} required questions (${input.questions.length} total) \u2014 answers may be incomplete.`
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1485
1777
|
} else {
|
|
1486
1778
|
answers = Object.fromEntries(
|
|
1487
1779
|
input.rawAnswers.map((a) => [a.question_header, a.selected_options[0] ?? ""])
|
|
@@ -1508,7 +1800,8 @@ function handleSavePhaseAnswers(input) {
|
|
|
1508
1800
|
text: JSON.stringify({
|
|
1509
1801
|
success: true,
|
|
1510
1802
|
path: filePath,
|
|
1511
|
-
answersCount: Object.keys(answers).length
|
|
1803
|
+
answersCount: Object.keys(answers).length,
|
|
1804
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
1512
1805
|
}),
|
|
1513
1806
|
isError: false
|
|
1514
1807
|
};
|
|
@@ -1555,13 +1848,63 @@ function handleGetOtterName(input) {
|
|
|
1555
1848
|
isError: false
|
|
1556
1849
|
};
|
|
1557
1850
|
}
|
|
1851
|
+
function handleSaveBuildContext(input) {
|
|
1852
|
+
const cwd = input._cwd ?? process.cwd();
|
|
1853
|
+
const dir = (0, import_path3.join)(cwd, ".stackwright");
|
|
1854
|
+
const filePath = (0, import_path3.join)(dir, "build-context.json");
|
|
1855
|
+
try {
|
|
1856
|
+
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
1857
|
+
if ((0, import_fs3.existsSync)(filePath)) {
|
|
1858
|
+
const stat = (0, import_fs3.lstatSync)(filePath);
|
|
1859
|
+
if (stat.isSymbolicLink()) {
|
|
1860
|
+
return {
|
|
1861
|
+
text: JSON.stringify({
|
|
1862
|
+
success: false,
|
|
1863
|
+
error: `Refusing to write to symlink: ${filePath}`
|
|
1864
|
+
}),
|
|
1865
|
+
isError: true
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
const payload = {
|
|
1870
|
+
version: "1.0",
|
|
1871
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1872
|
+
buildContext: input.buildContext
|
|
1873
|
+
};
|
|
1874
|
+
(0, import_fs3.writeFileSync)(filePath, JSON.stringify(payload, null, 2) + "\n");
|
|
1875
|
+
return {
|
|
1876
|
+
text: JSON.stringify({ success: true, path: filePath }),
|
|
1877
|
+
isError: false
|
|
1878
|
+
};
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1881
|
+
return {
|
|
1882
|
+
text: JSON.stringify({ success: false, error: message }),
|
|
1883
|
+
isError: true
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1558
1887
|
function registerOrchestrationTools(server2) {
|
|
1888
|
+
server2.tool(
|
|
1889
|
+
"stackwright_pro_save_build_context",
|
|
1890
|
+
`Save the user's initial build description to .stackwright/build-context.json. Call this once at startup after the user answers the opening "what are you building" question. The saved context is automatically prepended to specialist prompts by stackwright_pro_build_specialist_prompt.`,
|
|
1891
|
+
{
|
|
1892
|
+
buildContext: import_zod9.z.string().describe("Free-text description of what the user wants to build")
|
|
1893
|
+
},
|
|
1894
|
+
async ({ buildContext }) => {
|
|
1895
|
+
const { text, isError } = handleSaveBuildContext({ buildContext });
|
|
1896
|
+
return {
|
|
1897
|
+
content: [{ type: "text", text }],
|
|
1898
|
+
isError
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
);
|
|
1559
1902
|
server2.tool(
|
|
1560
1903
|
"stackwright_pro_parse_otter_response",
|
|
1561
1904
|
"Parse and validate a specialist otter's QUESTION_COLLECTION_MODE JSON response. Handles JSON extraction from LLM responses (strips markdown, fixes single quotes, trailing commas). Detects the phase from the otter name. Use this immediately after invoke_agent() to get a validated manifest phase object.",
|
|
1562
1905
|
{
|
|
1563
|
-
otterName:
|
|
1564
|
-
responseText:
|
|
1906
|
+
otterName: import_zod9.z.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
|
|
1907
|
+
responseText: import_zod9.z.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
|
|
1565
1908
|
},
|
|
1566
1909
|
async ({ otterName, responseText }) => {
|
|
1567
1910
|
const { result, isError } = handleParseOtterResponse({ otterName, responseText });
|
|
@@ -1575,16 +1918,18 @@ function registerOrchestrationTools(server2) {
|
|
|
1575
1918
|
"stackwright_pro_save_manifest",
|
|
1576
1919
|
"Write the question manifest to .stackwright/question-manifest.json. Call this after collecting and parsing questions from all otters via stackwright_pro_parse_otter_response.",
|
|
1577
1920
|
{
|
|
1578
|
-
phases:
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1921
|
+
phases: jsonCoerce(
|
|
1922
|
+
import_zod9.z.array(
|
|
1923
|
+
import_zod9.z.object({
|
|
1924
|
+
phase: import_zod9.z.string(),
|
|
1925
|
+
otter: import_zod9.z.string(),
|
|
1926
|
+
questions: import_zod9.z.array(import_zod9.z.any()),
|
|
1927
|
+
requiredPackages: import_zod9.z.object({
|
|
1928
|
+
dependencies: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional(),
|
|
1929
|
+
devPackages: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional()
|
|
1930
|
+
}).optional()
|
|
1931
|
+
})
|
|
1932
|
+
)
|
|
1588
1933
|
).describe("Array of parsed phase objects from stackwright_pro_parse_otter_response")
|
|
1589
1934
|
},
|
|
1590
1935
|
async ({ phases }) => {
|
|
@@ -1599,15 +1944,37 @@ function registerOrchestrationTools(server2) {
|
|
|
1599
1944
|
"stackwright_pro_save_phase_answers",
|
|
1600
1945
|
"Save user answers for a phase to .stackwright/answers/{phase}.json. Pass rawAnswers directly from ask_user_question and the original manifest questions for label-to-value reverse mapping.",
|
|
1601
1946
|
{
|
|
1602
|
-
phase:
|
|
1603
|
-
rawAnswers:
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1947
|
+
phase: import_zod9.z.string().describe('Phase name, e.g. "designer"'),
|
|
1948
|
+
rawAnswers: jsonCoerce(
|
|
1949
|
+
import_zod9.z.array(
|
|
1950
|
+
import_zod9.z.object({
|
|
1951
|
+
question_header: import_zod9.z.string().min(1, "question_header must not be empty"),
|
|
1952
|
+
selected_options: import_zod9.z.array(import_zod9.z.string()).min(1, "selected_options must have at least one entry"),
|
|
1953
|
+
other_text: import_zod9.z.string().nullable().optional()
|
|
1954
|
+
})
|
|
1955
|
+
)
|
|
1956
|
+
).describe(
|
|
1957
|
+
"Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
|
|
1958
|
+
),
|
|
1959
|
+
questions: jsonCoerce(
|
|
1960
|
+
import_zod9.z.array(
|
|
1961
|
+
import_zod9.z.object({
|
|
1962
|
+
id: import_zod9.z.string(),
|
|
1963
|
+
question: import_zod9.z.string(),
|
|
1964
|
+
type: import_zod9.z.string(),
|
|
1965
|
+
options: import_zod9.z.array(import_zod9.z.object({ label: import_zod9.z.string(), value: import_zod9.z.string() })).optional(),
|
|
1966
|
+
required: import_zod9.z.boolean().optional(),
|
|
1967
|
+
default: import_zod9.z.union([import_zod9.z.string(), import_zod9.z.boolean(), import_zod9.z.array(import_zod9.z.string())]).optional(),
|
|
1968
|
+
help: import_zod9.z.string().optional(),
|
|
1969
|
+
dependsOn: import_zod9.z.object({
|
|
1970
|
+
questionId: import_zod9.z.string(),
|
|
1971
|
+
value: import_zod9.z.union([import_zod9.z.string(), import_zod9.z.array(import_zod9.z.string())])
|
|
1972
|
+
}).optional()
|
|
1973
|
+
}).passthrough()
|
|
1974
|
+
).optional()
|
|
1975
|
+
).describe(
|
|
1976
|
+
"Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
|
|
1977
|
+
)
|
|
1611
1978
|
},
|
|
1612
1979
|
async ({ phase, rawAnswers, questions }) => {
|
|
1613
1980
|
const { text, isError } = handleSavePhaseAnswers({
|
|
@@ -1625,7 +1992,7 @@ function registerOrchestrationTools(server2) {
|
|
|
1625
1992
|
"stackwright_pro_read_phase_answers",
|
|
1626
1993
|
"Read saved answers for a phase from .stackwright/answers/{phase}.json. Returns { missing: true } when no answers exist yet \u2014 use this to skip phases safely in the execution loop.",
|
|
1627
1994
|
{
|
|
1628
|
-
phase:
|
|
1995
|
+
phase: import_zod9.z.string().describe('Phase name, e.g. "designer"')
|
|
1629
1996
|
},
|
|
1630
1997
|
async ({ phase }) => {
|
|
1631
1998
|
const { text, isError } = handleReadPhaseAnswers({ phase });
|
|
@@ -1639,7 +2006,7 @@ function registerOrchestrationTools(server2) {
|
|
|
1639
2006
|
"stackwright_pro_get_otter_name",
|
|
1640
2007
|
"Get the agent name for a phase (e.g. 'designer' \u2192 'stackwright-pro-designer-otter'). Use this in the execution loop to invoke the correct specialist otter without hardcoding names in the prompt.",
|
|
1641
2008
|
{
|
|
1642
|
-
phase:
|
|
2009
|
+
phase: import_zod9.z.string().describe('Phase name, e.g. "designer", "api", "pages"')
|
|
1643
2010
|
},
|
|
1644
2011
|
async ({ phase }) => {
|
|
1645
2012
|
const { text, isError } = handleGetOtterName({ phase });
|
|
@@ -1652,50 +2019,746 @@ function registerOrchestrationTools(server2) {
|
|
|
1652
2019
|
}
|
|
1653
2020
|
|
|
1654
2021
|
// src/tools/pipeline.ts
|
|
1655
|
-
var
|
|
2022
|
+
var import_zod13 = require("zod");
|
|
2023
|
+
var import_fs5 = require("fs");
|
|
2024
|
+
var import_path5 = require("path");
|
|
2025
|
+
var import_crypto3 = require("crypto");
|
|
2026
|
+
|
|
2027
|
+
// src/artifact-signing.ts
|
|
2028
|
+
var import_crypto2 = require("crypto");
|
|
1656
2029
|
var import_fs4 = require("fs");
|
|
1657
2030
|
var import_path4 = require("path");
|
|
1658
|
-
var
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
2031
|
+
var import_zod10 = require("zod");
|
|
2032
|
+
var ALGORITHM = "ECDSA-P384-SHA384";
|
|
2033
|
+
var KEY_FILE = "pipeline-keys.json";
|
|
2034
|
+
var KEY_DIR = ".stackwright";
|
|
2035
|
+
var SIGNATURE_MANIFEST = "signatures.json";
|
|
2036
|
+
var ARTIFACTS_DIR = ".stackwright/artifacts";
|
|
2037
|
+
function rejectSymlink(filePath, context) {
|
|
2038
|
+
if (!(0, import_fs4.existsSync)(filePath)) return;
|
|
2039
|
+
const stat = (0, import_fs4.lstatSync)(filePath);
|
|
2040
|
+
if (stat.isSymbolicLink()) {
|
|
2041
|
+
throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function computeSha384(data) {
|
|
2045
|
+
return (0, import_crypto2.createHash)("sha384").update(data).digest("hex");
|
|
2046
|
+
}
|
|
2047
|
+
function safeDigestEqual(a, b) {
|
|
2048
|
+
if (a.length !== b.length) return false;
|
|
2049
|
+
return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
2050
|
+
}
|
|
2051
|
+
function emptyManifest() {
|
|
2052
|
+
return {
|
|
2053
|
+
version: "1.0",
|
|
2054
|
+
algorithm: ALGORITHM,
|
|
2055
|
+
signatures: {}
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
function initPipelineKeys(cwd) {
|
|
2059
|
+
const keyDir = (0, import_path4.join)(cwd, KEY_DIR);
|
|
2060
|
+
const keyPath = (0, import_path4.join)(keyDir, KEY_FILE);
|
|
2061
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
2062
|
+
(0, import_fs4.mkdirSync)(keyDir, { recursive: true });
|
|
2063
|
+
const { publicKey, privateKey } = (0, import_crypto2.generateKeyPairSync)("ec", {
|
|
2064
|
+
namedCurve: "P-384"
|
|
2065
|
+
});
|
|
2066
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
2067
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
2068
|
+
const fingerprint = (0, import_crypto2.createHash)("sha256").update(publicKeyPem).digest("hex");
|
|
2069
|
+
const keyFile = {
|
|
2070
|
+
version: "1.0",
|
|
2071
|
+
algorithm: ALGORITHM,
|
|
2072
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2073
|
+
publicKeyPem,
|
|
2074
|
+
privateKeyPem
|
|
2075
|
+
};
|
|
2076
|
+
(0, import_fs4.writeFileSync)(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
|
|
2077
|
+
return { publicKeyPem, fingerprint };
|
|
2078
|
+
}
|
|
2079
|
+
function loadPipelineKeys(cwd) {
|
|
2080
|
+
const keyPath = (0, import_path4.join)(cwd, KEY_DIR, KEY_FILE);
|
|
2081
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
2082
|
+
if (!(0, import_fs4.existsSync)(keyPath)) {
|
|
2083
|
+
throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
|
|
2084
|
+
}
|
|
2085
|
+
let raw;
|
|
2086
|
+
try {
|
|
2087
|
+
raw = (0, import_fs4.readFileSync)(keyPath, "utf-8");
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2090
|
+
throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
|
|
2091
|
+
}
|
|
2092
|
+
let parsed;
|
|
2093
|
+
try {
|
|
2094
|
+
parsed = JSON.parse(raw);
|
|
2095
|
+
} catch {
|
|
2096
|
+
throw new Error("Pipeline keys file is not valid JSON");
|
|
2097
|
+
}
|
|
2098
|
+
if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
2099
|
+
throw new Error("Invalid public key PEM in pipeline keys file");
|
|
2100
|
+
}
|
|
2101
|
+
if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
|
|
2102
|
+
throw new Error("Invalid private key PEM in pipeline keys file");
|
|
2103
|
+
}
|
|
2104
|
+
const publicKey = (0, import_crypto2.createPublicKey)(parsed.publicKeyPem);
|
|
2105
|
+
const privateKey = (0, import_crypto2.createPrivateKey)(parsed.privateKeyPem);
|
|
2106
|
+
return { privateKey, publicKey };
|
|
2107
|
+
}
|
|
2108
|
+
function signArtifact(artifactBytes, privateKey) {
|
|
2109
|
+
const digest = computeSha384(artifactBytes);
|
|
2110
|
+
const sig = (0, import_crypto2.sign)("SHA384", artifactBytes, privateKey);
|
|
2111
|
+
return {
|
|
2112
|
+
digest,
|
|
2113
|
+
signature: sig.toString("base64"),
|
|
2114
|
+
algorithm: ALGORITHM,
|
|
2115
|
+
signedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function verifyArtifact(artifactBytes, signature, publicKey) {
|
|
2119
|
+
const sigValid = (0, import_crypto2.verify)(
|
|
2120
|
+
"SHA384",
|
|
2121
|
+
artifactBytes,
|
|
2122
|
+
publicKey,
|
|
2123
|
+
Buffer.from(signature.signature, "base64")
|
|
2124
|
+
);
|
|
2125
|
+
if (!sigValid) return false;
|
|
2126
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2127
|
+
return safeDigestEqual(actualDigest, signature.digest);
|
|
2128
|
+
}
|
|
2129
|
+
function loadSignatureManifest(cwd) {
|
|
2130
|
+
const manifestPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
|
|
2131
|
+
rejectSymlink(manifestPath, "signatures.json");
|
|
2132
|
+
if (!(0, import_fs4.existsSync)(manifestPath)) return emptyManifest();
|
|
2133
|
+
let raw;
|
|
2134
|
+
try {
|
|
2135
|
+
raw = (0, import_fs4.readFileSync)(manifestPath, "utf-8");
|
|
2136
|
+
} catch (err) {
|
|
2137
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2138
|
+
throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
|
|
2139
|
+
}
|
|
2140
|
+
try {
|
|
2141
|
+
const parsed = JSON.parse(raw);
|
|
2142
|
+
if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
|
|
2143
|
+
throw new Error("Malformed signature manifest: invalid version or missing signatures object");
|
|
2144
|
+
}
|
|
2145
|
+
return parsed;
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
|
|
2148
|
+
throw new Error("Signature manifest is not valid JSON", { cause: err });
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
|
|
2152
|
+
const artifactsDir = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
|
|
2153
|
+
const manifestPath = (0, import_path4.join)(artifactsDir, SIGNATURE_MANIFEST);
|
|
2154
|
+
rejectSymlink(manifestPath, "signatures.json (save)");
|
|
2155
|
+
(0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
|
|
2156
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2157
|
+
manifest.signatures[artifactFilename] = {
|
|
2158
|
+
...sig,
|
|
2159
|
+
signedBy: signerOtter
|
|
2160
|
+
};
|
|
2161
|
+
(0, import_fs4.writeFileSync)(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
|
|
2162
|
+
}
|
|
2163
|
+
function getArtifactSignature(cwd, artifactFilename) {
|
|
2164
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2165
|
+
const entry = manifest.signatures[artifactFilename];
|
|
2166
|
+
if (!entry) return null;
|
|
2167
|
+
return {
|
|
2168
|
+
digest: entry.digest,
|
|
2169
|
+
signature: entry.signature,
|
|
2170
|
+
algorithm: entry.algorithm,
|
|
2171
|
+
signedAt: entry.signedAt
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
function emitSignatureAuditEvent(params) {
|
|
2175
|
+
const record = JSON.stringify({
|
|
2176
|
+
level: "AUDIT",
|
|
2177
|
+
event: "ARTIFACT_SIGNATURE_FAIL",
|
|
2178
|
+
timestamp: params.timestamp,
|
|
2179
|
+
source: params.source,
|
|
2180
|
+
artifactFilename: params.artifactFilename,
|
|
2181
|
+
expectedDigest: params.expectedDigest,
|
|
2182
|
+
actualDigest: params.actualDigest,
|
|
2183
|
+
phase: params.phase
|
|
2184
|
+
});
|
|
2185
|
+
process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
|
|
2186
|
+
`);
|
|
2187
|
+
}
|
|
2188
|
+
function registerArtifactSigningTools(server2) {
|
|
2189
|
+
server2.tool(
|
|
2190
|
+
"stackwright_pro_verify_artifact_signatures",
|
|
2191
|
+
"Verify ECDSA P-384 signatures for all pipeline artifacts in .stackwright/artifacts/. Auto-discovers keys from .stackwright/pipeline-keys.json. Returns per-artifact verification status.",
|
|
2192
|
+
{
|
|
2193
|
+
cwd: import_zod10.z.string().optional().describe("Project root directory. Defaults to process.cwd().")
|
|
2194
|
+
},
|
|
2195
|
+
async ({ cwd: cwdParam }) => {
|
|
2196
|
+
const cwd = cwdParam ?? process.cwd();
|
|
2197
|
+
let publicKey;
|
|
2198
|
+
try {
|
|
2199
|
+
const keys = loadPipelineKeys(cwd);
|
|
2200
|
+
publicKey = keys.publicKey;
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2203
|
+
return {
|
|
2204
|
+
content: [
|
|
2205
|
+
{
|
|
2206
|
+
type: "text",
|
|
2207
|
+
text: JSON.stringify({
|
|
2208
|
+
error: true,
|
|
2209
|
+
message: `Cannot load pipeline keys: ${msg}`
|
|
2210
|
+
})
|
|
2211
|
+
}
|
|
2212
|
+
],
|
|
2213
|
+
isError: true
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
let manifest;
|
|
2217
|
+
try {
|
|
2218
|
+
manifest = loadSignatureManifest(cwd);
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2221
|
+
return {
|
|
2222
|
+
content: [
|
|
2223
|
+
{
|
|
2224
|
+
type: "text",
|
|
2225
|
+
text: JSON.stringify({
|
|
2226
|
+
error: true,
|
|
2227
|
+
message: `Cannot load signature manifest: ${msg}`
|
|
2228
|
+
})
|
|
2229
|
+
}
|
|
2230
|
+
],
|
|
2231
|
+
isError: true
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
const artifactsPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
|
|
2235
|
+
let artifactFiles = [];
|
|
2236
|
+
try {
|
|
2237
|
+
if ((0, import_fs4.existsSync)(artifactsPath)) {
|
|
2238
|
+
artifactFiles = (0, import_fs4.readdirSync)(artifactsPath).filter(
|
|
2239
|
+
(f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
} catch {
|
|
2243
|
+
}
|
|
2244
|
+
const results = [];
|
|
2245
|
+
let hasFailure = false;
|
|
2246
|
+
for (const filename of artifactFiles) {
|
|
2247
|
+
const filePath = (0, import_path4.join)(artifactsPath, filename);
|
|
2248
|
+
try {
|
|
2249
|
+
rejectSymlink(filePath, `artifact ${filename}`);
|
|
2250
|
+
} catch {
|
|
2251
|
+
results.push({
|
|
2252
|
+
filename,
|
|
2253
|
+
verified: false,
|
|
2254
|
+
error: "Refusing to verify symlink"
|
|
2255
|
+
});
|
|
2256
|
+
hasFailure = true;
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
let artifactBytes;
|
|
2260
|
+
try {
|
|
2261
|
+
artifactBytes = (0, import_fs4.readFileSync)(filePath);
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2264
|
+
results.push({
|
|
2265
|
+
filename,
|
|
2266
|
+
verified: false,
|
|
2267
|
+
error: `Cannot read artifact: ${msg}`
|
|
2268
|
+
});
|
|
2269
|
+
hasFailure = true;
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
const entry = manifest.signatures[filename];
|
|
2273
|
+
if (!entry) {
|
|
2274
|
+
results.push({
|
|
2275
|
+
filename,
|
|
2276
|
+
verified: false,
|
|
2277
|
+
error: "No signature found in manifest"
|
|
2278
|
+
});
|
|
2279
|
+
hasFailure = true;
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
const sig = {
|
|
2283
|
+
digest: entry.digest,
|
|
2284
|
+
signature: entry.signature,
|
|
2285
|
+
algorithm: entry.algorithm,
|
|
2286
|
+
signedAt: entry.signedAt
|
|
2287
|
+
};
|
|
2288
|
+
const verified = verifyArtifact(artifactBytes, sig, publicKey);
|
|
2289
|
+
if (!verified) {
|
|
2290
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2291
|
+
emitSignatureAuditEvent({
|
|
2292
|
+
artifactFilename: filename,
|
|
2293
|
+
expectedDigest: sig.digest,
|
|
2294
|
+
actualDigest,
|
|
2295
|
+
phase: entry.signedBy ?? "unknown",
|
|
2296
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2297
|
+
source: "stackwright_pro_verify_artifact_signatures"
|
|
2298
|
+
});
|
|
2299
|
+
results.push({
|
|
2300
|
+
filename,
|
|
2301
|
+
verified: false,
|
|
2302
|
+
error: `Signature verification failed \u2014 artifact may have been tampered with`,
|
|
2303
|
+
signedBy: entry.signedBy,
|
|
2304
|
+
signedAt: entry.signedAt
|
|
2305
|
+
});
|
|
2306
|
+
hasFailure = true;
|
|
2307
|
+
} else {
|
|
2308
|
+
results.push({
|
|
2309
|
+
filename,
|
|
2310
|
+
verified: true,
|
|
2311
|
+
signedBy: entry.signedBy,
|
|
2312
|
+
signedAt: entry.signedAt
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
for (const manifestFilename of Object.keys(manifest.signatures)) {
|
|
2317
|
+
if (!artifactFiles.includes(manifestFilename)) {
|
|
2318
|
+
results.push({
|
|
2319
|
+
filename: manifestFilename,
|
|
2320
|
+
verified: false,
|
|
2321
|
+
error: "Artifact referenced in manifest but missing from disk"
|
|
2322
|
+
});
|
|
2323
|
+
hasFailure = true;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
const verifiedCount = results.filter((r) => r.verified).length;
|
|
2327
|
+
const failedCount = results.filter((r) => !r.verified).length;
|
|
2328
|
+
return {
|
|
2329
|
+
content: [
|
|
2330
|
+
{
|
|
2331
|
+
type: "text",
|
|
2332
|
+
text: JSON.stringify({
|
|
2333
|
+
totalArtifacts: artifactFiles.length,
|
|
2334
|
+
verifiedCount,
|
|
2335
|
+
failedCount,
|
|
2336
|
+
results,
|
|
2337
|
+
...hasFailure ? {
|
|
2338
|
+
error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
|
|
2339
|
+
} : {}
|
|
2340
|
+
})
|
|
2341
|
+
}
|
|
2342
|
+
],
|
|
2343
|
+
isError: hasFailure
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// src/tools/pipeline.ts
|
|
2350
|
+
var import_types3 = require("@stackwright-pro/types");
|
|
2351
|
+
|
|
2352
|
+
// src/tools/get-schema.ts
|
|
2353
|
+
var import_zod12 = require("zod");
|
|
2354
|
+
var import_types2 = require("@stackwright-pro/types");
|
|
2355
|
+
|
|
2356
|
+
// src/tools/validate-yaml-fragment.ts
|
|
2357
|
+
var import_zod11 = require("zod");
|
|
2358
|
+
var import_js_yaml = require("js-yaml");
|
|
2359
|
+
var import_types = require("@stackwright-pro/types");
|
|
2360
|
+
var SUPPORTED_SCHEMA_NAMES = [
|
|
2361
|
+
"workflow_step",
|
|
2362
|
+
"workflow_definition",
|
|
2363
|
+
"workflow_field",
|
|
2364
|
+
"workflow_action",
|
|
2365
|
+
"navigation_item",
|
|
2366
|
+
"auth_config"
|
|
2367
|
+
];
|
|
2368
|
+
var NavigationLinkSchema = import_zod11.z.lazy(
|
|
2369
|
+
() => import_zod11.z.object({
|
|
2370
|
+
label: import_zod11.z.string().min(1),
|
|
2371
|
+
href: import_zod11.z.string().min(1),
|
|
2372
|
+
children: import_zod11.z.array(NavigationLinkSchema).optional()
|
|
2373
|
+
})
|
|
2374
|
+
);
|
|
2375
|
+
var NavigationSectionSchema = import_zod11.z.object({
|
|
2376
|
+
section: import_zod11.z.string().min(1),
|
|
2377
|
+
items: import_zod11.z.array(NavigationLinkSchema)
|
|
2378
|
+
});
|
|
2379
|
+
var NavigationItemSchema = import_zod11.z.union([
|
|
2380
|
+
NavigationLinkSchema,
|
|
2381
|
+
NavigationSectionSchema
|
|
2382
|
+
]);
|
|
2383
|
+
var FIELD_SYNONYMS = {
|
|
2384
|
+
workflow_step: {
|
|
2385
|
+
title: "label",
|
|
2386
|
+
name: "label (at step level, use label: not name:)",
|
|
2387
|
+
description: "message (use message: for step-level descriptive text)",
|
|
2388
|
+
style: 'theme.variant \u2014 use theme: { variant: "primary" } not style: "primary"',
|
|
2389
|
+
transitions: 'on_submit.transition \u2014 use on_submit: { transition: "next_step" } not transitions: [{then:...}]'
|
|
2390
|
+
},
|
|
2391
|
+
workflow_definition: {
|
|
2392
|
+
title: "label",
|
|
2393
|
+
name: "label (workflow-level label, not name:)",
|
|
2394
|
+
description: "description is valid here \u2014 but message: is not (that is a step field)",
|
|
2395
|
+
type: "id (workflows are identified by id:, not type:)"
|
|
2396
|
+
},
|
|
2397
|
+
workflow_field: {
|
|
2398
|
+
id: "name (field-level identifier is name:, NOT id:)",
|
|
2399
|
+
title: "label (field display text is label:, NOT title:)",
|
|
2400
|
+
description: "placeholder or label (no description field on WorkflowField)"
|
|
2401
|
+
},
|
|
2402
|
+
workflow_action: {
|
|
2403
|
+
style: 'theme.variant \u2014 use theme: { variant: "primary" } not style: "primary"',
|
|
2404
|
+
then: "transition (action-level next-step is transition:, NOT then:)",
|
|
2405
|
+
on_click: 'action: "service:..." (extract the service reference from on_click.action)',
|
|
2406
|
+
title: "label (action display text is label:, NOT title:)",
|
|
2407
|
+
name: "label (action display text is label:, NOT name:)"
|
|
2408
|
+
},
|
|
2409
|
+
navigation_item: {
|
|
2410
|
+
children: "items (NavigationSection uses items: not children: \u2014 also use section: instead of label: for section headers)",
|
|
2411
|
+
label: 'section (if this is a nav group/section, use section: "Title" and items: [...], not label:)',
|
|
2412
|
+
title: "section (NavigationSection header field is section:, NOT title:)"
|
|
2413
|
+
},
|
|
2414
|
+
auth_config: {
|
|
2415
|
+
method: 'type \u2014 auth config discriminates on type: "oidc" | "pki", NOT method:',
|
|
2416
|
+
provider_type: 'type \u2014 use type: "oidc" or type: "pki"',
|
|
2417
|
+
strategy: "type \u2014 top-level discriminator is type:, not strategy:"
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
function getSchema(name) {
|
|
2421
|
+
switch (name) {
|
|
2422
|
+
case "workflow_step":
|
|
2423
|
+
return import_types.WorkflowStepSchema;
|
|
2424
|
+
case "workflow_definition":
|
|
2425
|
+
return import_types.WorkflowDefinitionSchema;
|
|
2426
|
+
case "workflow_field":
|
|
2427
|
+
return import_types.WorkflowFieldSchema;
|
|
2428
|
+
case "workflow_action":
|
|
2429
|
+
return import_types.WorkflowActionSchema;
|
|
2430
|
+
case "navigation_item":
|
|
2431
|
+
return NavigationItemSchema;
|
|
2432
|
+
case "auth_config":
|
|
2433
|
+
return import_types.authConfigSchema;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
function enrichErrors(issues, synonyms) {
|
|
2437
|
+
return issues.map((issue) => {
|
|
2438
|
+
const pathStr = issue.path.join(".");
|
|
2439
|
+
const lastSegment = issue.path[issue.path.length - 1];
|
|
2440
|
+
const fieldName = typeof lastSegment === "string" ? lastSegment : void 0;
|
|
2441
|
+
const didYouMean = fieldName ? synonyms[fieldName] : void 0;
|
|
2442
|
+
return {
|
|
2443
|
+
path: pathStr || "(root)",
|
|
2444
|
+
message: issue.message,
|
|
2445
|
+
...didYouMean ? { didYouMean } : {}
|
|
2446
|
+
};
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
function handleValidateYamlFragment(input) {
|
|
2450
|
+
const { schemaName, yaml } = input;
|
|
2451
|
+
if (!SUPPORTED_SCHEMA_NAMES.includes(schemaName)) {
|
|
2452
|
+
return {
|
|
2453
|
+
valid: false,
|
|
2454
|
+
parseError: `Unknown schemaName: "${schemaName}". Supported: ${SUPPORTED_SCHEMA_NAMES.join(", ")}`
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
const name = schemaName;
|
|
2458
|
+
let parsed;
|
|
2459
|
+
try {
|
|
2460
|
+
parsed = (0, import_js_yaml.load)(yaml);
|
|
2461
|
+
} catch (err) {
|
|
2462
|
+
return {
|
|
2463
|
+
valid: false,
|
|
2464
|
+
parseError: `YAML parse error: ${err instanceof Error ? err.message : String(err)}`
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
const schema = getSchema(name);
|
|
2468
|
+
const result = schema.safeParse(parsed);
|
|
2469
|
+
if (result.success) {
|
|
2470
|
+
return { valid: true };
|
|
2471
|
+
}
|
|
2472
|
+
const synonyms = FIELD_SYNONYMS[name];
|
|
2473
|
+
const errors = enrichErrors(result.error.issues, synonyms);
|
|
2474
|
+
return { valid: false, errors };
|
|
2475
|
+
}
|
|
2476
|
+
function registerValidateYamlFragmentTool(server2) {
|
|
2477
|
+
server2.tool(
|
|
2478
|
+
"stackwright_pro_validate_yaml_fragment",
|
|
2479
|
+
'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.',
|
|
2480
|
+
{
|
|
2481
|
+
schemaName: import_zod11.z.enum(SUPPORTED_SCHEMA_NAMES).describe(
|
|
2482
|
+
"Schema to validate against. One of: workflow_step, workflow_definition, workflow_field, workflow_action, navigation_item, auth_config"
|
|
2483
|
+
),
|
|
2484
|
+
yaml: import_zod11.z.string().describe("YAML string to validate (can also be JSON \u2014 js-yaml parses both)")
|
|
2485
|
+
},
|
|
2486
|
+
async ({ schemaName, yaml }) => {
|
|
2487
|
+
const result = handleValidateYamlFragment({ schemaName, yaml });
|
|
2488
|
+
return {
|
|
2489
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// src/tools/get-schema.ts
|
|
2496
|
+
var NavigationLinkSchema2 = import_zod12.z.lazy(
|
|
2497
|
+
() => import_zod12.z.object({
|
|
2498
|
+
label: import_zod12.z.string().min(1),
|
|
2499
|
+
href: import_zod12.z.string().min(1),
|
|
2500
|
+
children: import_zod12.z.array(NavigationLinkSchema2).optional()
|
|
2501
|
+
})
|
|
2502
|
+
);
|
|
2503
|
+
var NavigationSectionSchema2 = import_zod12.z.object({
|
|
2504
|
+
section: import_zod12.z.string().min(1),
|
|
2505
|
+
items: import_zod12.z.array(NavigationLinkSchema2)
|
|
2506
|
+
});
|
|
2507
|
+
var NavigationItemSchema2 = import_zod12.z.union([
|
|
2508
|
+
NavigationLinkSchema2,
|
|
2509
|
+
NavigationSectionSchema2
|
|
2510
|
+
]);
|
|
2511
|
+
function getZodSchema(name) {
|
|
2512
|
+
switch (name) {
|
|
2513
|
+
case "workflow_step":
|
|
2514
|
+
return import_types2.WorkflowStepSchema;
|
|
2515
|
+
case "workflow_definition":
|
|
2516
|
+
return import_types2.WorkflowDefinitionSchema;
|
|
2517
|
+
case "workflow_field":
|
|
2518
|
+
return import_types2.WorkflowFieldSchema;
|
|
2519
|
+
case "workflow_action":
|
|
2520
|
+
return import_types2.WorkflowActionSchema;
|
|
2521
|
+
case "navigation_item":
|
|
2522
|
+
return NavigationItemSchema2;
|
|
2523
|
+
case "auth_config":
|
|
2524
|
+
return import_types2.authConfigSchema;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function formatType(node, depth = 0) {
|
|
2528
|
+
if (node.const !== void 0) return JSON.stringify(node.const);
|
|
2529
|
+
if (node.enum) return node.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
2530
|
+
if (node.oneOf || node.anyOf) {
|
|
2531
|
+
const branches = node.oneOf ?? node.anyOf ?? [];
|
|
2532
|
+
return branches.map((b) => {
|
|
2533
|
+
const disc = b.properties ? Object.entries(b.properties).filter(([, v]) => v.const !== void 0).map(([k, v]) => `${k}: ${JSON.stringify(v.const)}`).join(", ") : "";
|
|
2534
|
+
return disc ? `{ ${disc}, ... }` : "{ ... }";
|
|
2535
|
+
}).join(" | ");
|
|
2536
|
+
}
|
|
2537
|
+
if (node.type === "array" && node.items) {
|
|
2538
|
+
return `${formatType(node.items, depth)}[]`;
|
|
2539
|
+
}
|
|
2540
|
+
if (node.type === "object" && node.properties && depth < 2) {
|
|
2541
|
+
const inner = Object.entries(node.properties).map(([k, v]) => `${k}: ${formatType(v, depth + 1)}`).join(", ");
|
|
2542
|
+
return `{ ${inner} }`;
|
|
2543
|
+
}
|
|
2544
|
+
const base = Array.isArray(node.type) ? node.type.join(" | ") : node.type ?? "unknown";
|
|
2545
|
+
const constraints = [];
|
|
2546
|
+
if (node.maxLength !== void 0) constraints.push(`max ${node.maxLength} chars`);
|
|
2547
|
+
if (node.minLength !== void 0) constraints.push(`min ${node.minLength} chars`);
|
|
2548
|
+
if (node.pattern) constraints.push(`pattern: ${node.pattern}`);
|
|
2549
|
+
if (node.minimum !== void 0) constraints.push(`min ${node.minimum}`);
|
|
2550
|
+
if (node.maximum !== void 0) constraints.push(`max ${node.maximum}`);
|
|
2551
|
+
return constraints.length > 0 ? `${base} (${constraints.join(", ")})` : base;
|
|
2552
|
+
}
|
|
2553
|
+
function formatObjectSchema(node, schemaName, synonyms) {
|
|
2554
|
+
const lines = [];
|
|
2555
|
+
const displayName = schemaName.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
2556
|
+
lines.push(`${displayName}:`);
|
|
2557
|
+
if (node.oneOf || node.anyOf) {
|
|
2558
|
+
const branches = node.oneOf ?? node.anyOf ?? [];
|
|
2559
|
+
lines.push(" Discriminated union \u2014 choose one variant:");
|
|
2560
|
+
for (const branch of branches) {
|
|
2561
|
+
const req = new Set(branch.required ?? []);
|
|
2562
|
+
const disc = Object.entries(branch.properties ?? {}).filter(([, v]) => v.const !== void 0).map(([k, v]) => `${k}: ${JSON.stringify(v.const)}`).join(", ");
|
|
2563
|
+
lines.push(` VARIANT (${disc}):`);
|
|
2564
|
+
for (const [field, fieldNode] of Object.entries(branch.properties ?? {})) {
|
|
2565
|
+
if (fieldNode.const !== void 0) continue;
|
|
2566
|
+
const isRequired = req.has(field);
|
|
2567
|
+
const hint = synonyms[field] ? ` -- WARNING: use ${field}: not ${synonyms[field]}` : "";
|
|
2568
|
+
lines.push(
|
|
2569
|
+
` ${isRequired ? "REQUIRED" : "optional"}: ${field}: ${formatType(fieldNode)}${hint}`
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
return lines.join("\n");
|
|
2574
|
+
}
|
|
2575
|
+
const required = new Set(node.required ?? []);
|
|
2576
|
+
const properties = node.properties ?? {};
|
|
2577
|
+
const requiredFields = Object.entries(properties).filter(([k]) => required.has(k));
|
|
2578
|
+
const optionalFields = Object.entries(properties).filter(([k]) => !required.has(k));
|
|
2579
|
+
if (requiredFields.length > 0) {
|
|
2580
|
+
lines.push(" REQUIRED:");
|
|
2581
|
+
for (const [field, fieldNode] of requiredFields) {
|
|
2582
|
+
const typeStr = formatType(fieldNode);
|
|
2583
|
+
const warn = synonyms[field] ? ` -- CAUTION: this schema uses ${field}` : "";
|
|
2584
|
+
lines.push(` ${field}: ${typeStr}${warn}`);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (optionalFields.length > 0) {
|
|
2588
|
+
lines.push(" OPTIONAL:");
|
|
2589
|
+
for (const [field, fieldNode] of optionalFields) {
|
|
2590
|
+
const typeStr = formatType(fieldNode);
|
|
2591
|
+
lines.push(` ${field}: ${typeStr}`);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
return lines.join("\n");
|
|
2595
|
+
}
|
|
2596
|
+
function handleGetSchema(input) {
|
|
2597
|
+
const { schemaName } = input;
|
|
2598
|
+
if (!SUPPORTED_SCHEMA_NAMES.includes(schemaName)) {
|
|
2599
|
+
return {
|
|
2600
|
+
error: `Unknown schemaName: "${schemaName}". Supported: ${SUPPORTED_SCHEMA_NAMES.join(", ")}`
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
const name = schemaName;
|
|
2604
|
+
const schema = getZodSchema(name);
|
|
2605
|
+
const synonyms = FIELD_SYNONYMS[name];
|
|
2606
|
+
let jsonSchema;
|
|
2607
|
+
try {
|
|
2608
|
+
const raw = import_zod12.z.toJSONSchema(schema, { unrepresentable: "any" });
|
|
2609
|
+
jsonSchema = raw;
|
|
2610
|
+
} catch {
|
|
2611
|
+
jsonSchema = { type: "object", description: "Schema introspection unavailable for this type" };
|
|
2612
|
+
}
|
|
2613
|
+
const summary = formatObjectSchema(jsonSchema, name, synonyms);
|
|
2614
|
+
const synonymWarnings = Object.entries(synonyms).map(
|
|
2615
|
+
([wrong, correct]) => ` WARNING: use "${correct}" \u2014 NOT "${wrong}"`
|
|
2616
|
+
);
|
|
2617
|
+
const fullText = [
|
|
2618
|
+
summary,
|
|
2619
|
+
"",
|
|
2620
|
+
"FIELD NAME WARNINGS (common drift patterns):",
|
|
2621
|
+
...synonymWarnings
|
|
2622
|
+
].join("\n");
|
|
2623
|
+
const tokenEstimate = Math.ceil(fullText.length / 4);
|
|
2624
|
+
return {
|
|
2625
|
+
schemaName: name,
|
|
2626
|
+
summary,
|
|
2627
|
+
synonymWarnings,
|
|
2628
|
+
tokenEstimate
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
var PHASE_SCHEMA_NAMES = {
|
|
2632
|
+
workflow: ["workflow_step", "workflow_definition", "workflow_field", "workflow_action"],
|
|
2633
|
+
pages: ["navigation_item"],
|
|
2634
|
+
dashboard: ["navigation_item"],
|
|
2635
|
+
polish: ["navigation_item"],
|
|
2636
|
+
auth: ["auth_config"]
|
|
2637
|
+
};
|
|
2638
|
+
function buildSchemaReferenceBlock(phase) {
|
|
2639
|
+
const schemaNames = PHASE_SCHEMA_NAMES[phase];
|
|
2640
|
+
if (!schemaNames || schemaNames.length === 0) return null;
|
|
2641
|
+
const sections = [
|
|
2642
|
+
"CANONICAL_SCHEMA_REFERENCE (authoritative \u2014 use exact field names below):",
|
|
2643
|
+
""
|
|
2644
|
+
];
|
|
2645
|
+
for (const name of schemaNames) {
|
|
2646
|
+
const result = handleGetSchema({ schemaName: name });
|
|
2647
|
+
if ("error" in result) continue;
|
|
2648
|
+
sections.push(result.summary);
|
|
2649
|
+
if (result.synonymWarnings.length > 0) {
|
|
2650
|
+
sections.push(" Common mistakes to avoid:");
|
|
2651
|
+
sections.push(...result.synonymWarnings);
|
|
2652
|
+
}
|
|
2653
|
+
sections.push("");
|
|
2654
|
+
}
|
|
2655
|
+
return sections.join("\n");
|
|
2656
|
+
}
|
|
2657
|
+
function registerGetSchemaTool(server2) {
|
|
2658
|
+
server2.tool(
|
|
2659
|
+
"stackwright_pro_get_schema",
|
|
2660
|
+
'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.',
|
|
2661
|
+
{
|
|
2662
|
+
schemaName: import_zod12.z.enum(SUPPORTED_SCHEMA_NAMES).describe(
|
|
2663
|
+
"Schema to summarize. One of: workflow_step, workflow_definition, workflow_field, workflow_action, navigation_item, auth_config"
|
|
2664
|
+
)
|
|
2665
|
+
},
|
|
2666
|
+
async ({ schemaName }) => {
|
|
2667
|
+
const result = handleGetSchema({ schemaName });
|
|
2668
|
+
if ("error" in result) {
|
|
2669
|
+
return {
|
|
2670
|
+
content: [{ type: "text", text: JSON.stringify({ error: result.error }) }],
|
|
2671
|
+
isError: true
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
return {
|
|
2675
|
+
content: [
|
|
2676
|
+
{
|
|
2677
|
+
type: "text",
|
|
2678
|
+
text: JSON.stringify(result, null, 2)
|
|
2679
|
+
}
|
|
2680
|
+
]
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// src/tools/pipeline.ts
|
|
2687
|
+
var PHASE_ORDER = [
|
|
2688
|
+
"designer",
|
|
2689
|
+
"theme",
|
|
2690
|
+
"scaffold",
|
|
2691
|
+
// generates app/ directory from config
|
|
2692
|
+
"api",
|
|
2693
|
+
"data",
|
|
2694
|
+
"geo",
|
|
2695
|
+
"workflow",
|
|
2696
|
+
"services",
|
|
2697
|
+
"pages",
|
|
2698
|
+
"dashboard",
|
|
2699
|
+
"auth",
|
|
2700
|
+
"polish"
|
|
2701
|
+
];
|
|
2702
|
+
var PHASE_DEPENDENCIES = {
|
|
2703
|
+
designer: [],
|
|
2704
|
+
theme: ["designer"],
|
|
2705
|
+
scaffold: ["designer", "theme"],
|
|
2706
|
+
// needs stackwright.yml + theme-tokens
|
|
2707
|
+
api: [],
|
|
2708
|
+
data: ["api"],
|
|
2709
|
+
geo: ["data"],
|
|
2710
|
+
// workflow: no hard deps — uses service: references with Prism mock fallback
|
|
2711
|
+
// when API artifacts aren't available; roles come from user answers
|
|
2712
|
+
workflow: [],
|
|
2713
|
+
// services: needs API endpoints to know what to wire, and optionally
|
|
2714
|
+
// workflow states as trigger sources. Produces OpenAPI specs consumed
|
|
2715
|
+
// by pages and dashboard for typed data wiring.
|
|
2716
|
+
services: ["api", "workflow"],
|
|
2717
|
+
// pages/dashboard: depend on services for typed endpoint wiring (OpenAPI
|
|
2718
|
+
// specs) and on geo so the specialist prompt includes geo-manifest.json
|
|
2719
|
+
// — page/dashboard otters see which page slugs are already claimed and
|
|
2720
|
+
// won't attempt to overwrite them (swp-73c). 'api' is still transitive
|
|
2721
|
+
// through 'data'; auth removed — page-otter has graceful fallback.
|
|
2722
|
+
pages: ["designer", "theme", "data", "services", "geo", "scaffold"],
|
|
2723
|
+
dashboard: ["designer", "theme", "data", "services", "geo", "scaffold"],
|
|
2724
|
+
// auth is the penultimate phase — runs after all content-producing phases
|
|
2725
|
+
// so it can read pages, dashboard, workflow, and geo artifacts for route
|
|
2726
|
+
// protection and RBAC wiring. Skipped upstream phases still satisfy deps
|
|
2727
|
+
// (the foreman marks them executed=true), so auth always runs.
|
|
2728
|
+
auth: ["pages", "dashboard", "workflow", "geo"],
|
|
2729
|
+
// polish is the terminal phase — runs after auth so the landing page and
|
|
2730
|
+
// nav reflect the final set of protected routes and all generated pages.
|
|
2731
|
+
polish: ["auth"]
|
|
2732
|
+
};
|
|
2733
|
+
var PHASE_ARTIFACT = {
|
|
2734
|
+
designer: "design-language.json",
|
|
2735
|
+
theme: "theme-tokens.json",
|
|
2736
|
+
scaffold: "scaffold-manifest.json",
|
|
2737
|
+
api: "api-config.json",
|
|
2738
|
+
auth: "auth-config.json",
|
|
2739
|
+
data: "data-config.json",
|
|
2740
|
+
pages: "pages-manifest.json",
|
|
2741
|
+
dashboard: "dashboard-manifest.json",
|
|
2742
|
+
workflow: "workflow-config.json",
|
|
2743
|
+
services: "services-config.json",
|
|
2744
|
+
polish: "polish-manifest.json",
|
|
2745
|
+
geo: "geo-manifest.json"
|
|
2746
|
+
};
|
|
2747
|
+
var PHASE_TO_OTTER2 = {
|
|
2748
|
+
designer: "stackwright-pro-designer-otter",
|
|
2749
|
+
theme: "stackwright-pro-theme-otter",
|
|
2750
|
+
scaffold: "stackwright-pro-scaffold-otter",
|
|
2751
|
+
api: "stackwright-pro-api-otter",
|
|
2752
|
+
auth: "stackwright-pro-auth-otter",
|
|
2753
|
+
data: "stackwright-pro-data-otter",
|
|
2754
|
+
pages: "stackwright-pro-page-otter",
|
|
1695
2755
|
dashboard: "stackwright-pro-dashboard-otter",
|
|
1696
|
-
workflow: "stackwright-pro-workflow-otter"
|
|
2756
|
+
workflow: "stackwright-pro-workflow-otter",
|
|
2757
|
+
services: "stackwright-services-otter",
|
|
2758
|
+
polish: "stackwright-pro-polish-otter",
|
|
2759
|
+
geo: "stackwright-pro-geo-otter"
|
|
1697
2760
|
};
|
|
1698
|
-
function
|
|
2761
|
+
function isValidPhase2(phase) {
|
|
1699
2762
|
return PHASE_ORDER.includes(phase);
|
|
1700
2763
|
}
|
|
1701
2764
|
function defaultPhaseStatus() {
|
|
@@ -1723,11 +2786,11 @@ function createDefaultState() {
|
|
|
1723
2786
|
};
|
|
1724
2787
|
}
|
|
1725
2788
|
function statePath(cwd) {
|
|
1726
|
-
return (0,
|
|
2789
|
+
return (0, import_path5.join)(cwd, ".stackwright", "pipeline-state.json");
|
|
1727
2790
|
}
|
|
1728
2791
|
function readState(cwd) {
|
|
1729
2792
|
const p = statePath(cwd);
|
|
1730
|
-
if (!(0,
|
|
2793
|
+
if (!(0, import_fs5.existsSync)(p)) return createDefaultState();
|
|
1731
2794
|
const raw = JSON.parse(safeReadSync(p));
|
|
1732
2795
|
if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
|
|
1733
2796
|
return createDefaultState();
|
|
@@ -1735,26 +2798,26 @@ function readState(cwd) {
|
|
|
1735
2798
|
return raw;
|
|
1736
2799
|
}
|
|
1737
2800
|
function safeWriteSync(filePath, content) {
|
|
1738
|
-
if ((0,
|
|
1739
|
-
const stat = (0,
|
|
2801
|
+
if ((0, import_fs5.existsSync)(filePath)) {
|
|
2802
|
+
const stat = (0, import_fs5.lstatSync)(filePath);
|
|
1740
2803
|
if (stat.isSymbolicLink()) {
|
|
1741
2804
|
throw new Error(`Refusing to write to symlink: ${filePath}`);
|
|
1742
2805
|
}
|
|
1743
2806
|
}
|
|
1744
|
-
(0,
|
|
2807
|
+
(0, import_fs5.writeFileSync)(filePath, content);
|
|
1745
2808
|
}
|
|
1746
2809
|
function safeReadSync(filePath) {
|
|
1747
|
-
if ((0,
|
|
1748
|
-
const stat = (0,
|
|
2810
|
+
if ((0, import_fs5.existsSync)(filePath)) {
|
|
2811
|
+
const stat = (0, import_fs5.lstatSync)(filePath);
|
|
1749
2812
|
if (stat.isSymbolicLink()) {
|
|
1750
2813
|
throw new Error(`Refusing to read symlink: ${filePath}`);
|
|
1751
2814
|
}
|
|
1752
2815
|
}
|
|
1753
|
-
return (0,
|
|
2816
|
+
return (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
1754
2817
|
}
|
|
1755
2818
|
function writeState(cwd, state) {
|
|
1756
|
-
const dir = (0,
|
|
1757
|
-
(0,
|
|
2819
|
+
const dir = (0, import_path5.join)(cwd, ".stackwright");
|
|
2820
|
+
(0, import_fs5.mkdirSync)(dir, { recursive: true });
|
|
1758
2821
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1759
2822
|
safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
1760
2823
|
}
|
|
@@ -1775,6 +2838,15 @@ function handleGetPipelineState(_cwd) {
|
|
|
1775
2838
|
const cwd = _cwd ?? process.cwd();
|
|
1776
2839
|
try {
|
|
1777
2840
|
const state = readState(cwd);
|
|
2841
|
+
const keyPath = (0, import_path5.join)(cwd, ".stackwright", "pipeline-keys.json");
|
|
2842
|
+
if (!(0, import_fs5.existsSync)(keyPath)) {
|
|
2843
|
+
try {
|
|
2844
|
+
const { fingerprint } = initPipelineKeys(cwd);
|
|
2845
|
+
state["signingKeyFingerprint"] = fingerprint;
|
|
2846
|
+
writeState(cwd, state);
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
1778
2850
|
return { text: JSON.stringify(state), isError: false };
|
|
1779
2851
|
} catch (err) {
|
|
1780
2852
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
@@ -1782,7 +2854,7 @@ function handleGetPipelineState(_cwd) {
|
|
|
1782
2854
|
}
|
|
1783
2855
|
function handleSetPipelineState(input) {
|
|
1784
2856
|
const cwd = input._cwd ?? process.cwd();
|
|
1785
|
-
if (input.phase && !
|
|
2857
|
+
if (input.phase && !isValidPhase2(input.phase)) {
|
|
1786
2858
|
return {
|
|
1787
2859
|
text: JSON.stringify({
|
|
1788
2860
|
error: true,
|
|
@@ -1801,6 +2873,28 @@ function handleSetPipelineState(input) {
|
|
|
1801
2873
|
isError: true
|
|
1802
2874
|
};
|
|
1803
2875
|
}
|
|
2876
|
+
if (input.updates && input.updates.length > 0) {
|
|
2877
|
+
for (const update of input.updates) {
|
|
2878
|
+
if (!isValidPhase2(update.phase)) {
|
|
2879
|
+
return {
|
|
2880
|
+
text: JSON.stringify({
|
|
2881
|
+
error: true,
|
|
2882
|
+
message: `Invalid phase in batch update: ${update.phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
|
|
2883
|
+
}),
|
|
2884
|
+
isError: true
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
if (!VALID_FIELDS.includes(update.field)) {
|
|
2888
|
+
return {
|
|
2889
|
+
text: JSON.stringify({
|
|
2890
|
+
error: true,
|
|
2891
|
+
message: `Invalid field in batch update: ${update.field}. Valid fields are: ${VALID_FIELDS.join(", ")}`
|
|
2892
|
+
}),
|
|
2893
|
+
isError: true
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
1804
2898
|
try {
|
|
1805
2899
|
const state = readState(cwd);
|
|
1806
2900
|
if (input.status) {
|
|
@@ -1820,34 +2914,78 @@ function handleSetPipelineState(input) {
|
|
|
1820
2914
|
}
|
|
1821
2915
|
state.currentPhase = phase;
|
|
1822
2916
|
}
|
|
2917
|
+
if (input.updates && input.updates.length > 0) {
|
|
2918
|
+
for (const update of input.updates) {
|
|
2919
|
+
if (!state.phases[update.phase]) {
|
|
2920
|
+
state.phases[update.phase] = defaultPhaseStatus();
|
|
2921
|
+
}
|
|
2922
|
+
const batchPhaseState = state.phases[update.phase];
|
|
2923
|
+
batchPhaseState[update.field] = update.value;
|
|
2924
|
+
state.currentPhase = update.phase;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
1823
2927
|
writeState(cwd, state);
|
|
1824
2928
|
return { text: JSON.stringify(state), isError: false };
|
|
1825
2929
|
} catch (err) {
|
|
1826
2930
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
1827
2931
|
}
|
|
1828
2932
|
}
|
|
1829
|
-
function handleCheckExecutionReady(_cwd) {
|
|
2933
|
+
function handleCheckExecutionReady(_cwd, phase) {
|
|
1830
2934
|
const cwd = _cwd ?? process.cwd();
|
|
2935
|
+
if (phase) {
|
|
2936
|
+
if (!isValidPhase2(phase)) {
|
|
2937
|
+
return {
|
|
2938
|
+
text: JSON.stringify({
|
|
2939
|
+
error: true,
|
|
2940
|
+
message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
|
|
2941
|
+
}),
|
|
2942
|
+
isError: true
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
const answerFile = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
2946
|
+
if (!(0, import_fs5.existsSync)(answerFile)) {
|
|
2947
|
+
return {
|
|
2948
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
|
|
2949
|
+
isError: false
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
try {
|
|
2953
|
+
const raw = safeReadSync(answerFile);
|
|
2954
|
+
const parsed = JSON.parse(raw);
|
|
2955
|
+
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
2956
|
+
return {
|
|
2957
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
|
|
2958
|
+
isError: false
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
return { text: JSON.stringify({ ready: true, phase }), isError: false };
|
|
2962
|
+
} catch {
|
|
2963
|
+
return {
|
|
2964
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
|
|
2965
|
+
isError: false
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
1831
2969
|
try {
|
|
1832
|
-
const answersDir = (0,
|
|
2970
|
+
const answersDir = (0, import_path5.join)(cwd, ".stackwright", "answers");
|
|
1833
2971
|
const answeredPhases = [];
|
|
1834
2972
|
const missingPhases = [];
|
|
1835
|
-
for (const
|
|
1836
|
-
const answerFile = (0,
|
|
1837
|
-
if ((0,
|
|
2973
|
+
for (const phase2 of PHASE_ORDER) {
|
|
2974
|
+
const answerFile = (0, import_path5.join)(answersDir, `${phase2}.json`);
|
|
2975
|
+
if ((0, import_fs5.existsSync)(answerFile)) {
|
|
1838
2976
|
try {
|
|
1839
2977
|
const raw = safeReadSync(answerFile);
|
|
1840
2978
|
const parsed = JSON.parse(raw);
|
|
1841
2979
|
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
1842
|
-
missingPhases.push(
|
|
2980
|
+
missingPhases.push(phase2);
|
|
1843
2981
|
continue;
|
|
1844
2982
|
}
|
|
1845
|
-
answeredPhases.push(
|
|
2983
|
+
answeredPhases.push(phase2);
|
|
1846
2984
|
} catch {
|
|
1847
|
-
missingPhases.push(
|
|
2985
|
+
missingPhases.push(phase2);
|
|
1848
2986
|
}
|
|
1849
2987
|
} else {
|
|
1850
|
-
missingPhases.push(
|
|
2988
|
+
missingPhases.push(phase2);
|
|
1851
2989
|
}
|
|
1852
2990
|
}
|
|
1853
2991
|
return {
|
|
@@ -1863,18 +3001,74 @@ function handleCheckExecutionReady(_cwd) {
|
|
|
1863
3001
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
1864
3002
|
}
|
|
1865
3003
|
}
|
|
3004
|
+
function handleGetReadyPhases(_cwd) {
|
|
3005
|
+
const cwd = _cwd ?? process.cwd();
|
|
3006
|
+
try {
|
|
3007
|
+
const state = readState(cwd);
|
|
3008
|
+
const completed = [];
|
|
3009
|
+
const ready = [];
|
|
3010
|
+
const blocked = [];
|
|
3011
|
+
for (const phase of PHASE_ORDER) {
|
|
3012
|
+
const ps = state.phases[phase];
|
|
3013
|
+
if (ps?.executed) {
|
|
3014
|
+
completed.push(phase);
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
3017
|
+
const deps = PHASE_DEPENDENCIES[phase];
|
|
3018
|
+
const unmetDeps = deps.filter((dep) => !state.phases[dep]?.executed);
|
|
3019
|
+
if (unmetDeps.length === 0) {
|
|
3020
|
+
ready.push(phase);
|
|
3021
|
+
} else {
|
|
3022
|
+
blocked.push(phase);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return {
|
|
3026
|
+
text: JSON.stringify({
|
|
3027
|
+
readyPhases: ready,
|
|
3028
|
+
completedPhases: completed,
|
|
3029
|
+
blockedPhases: blocked,
|
|
3030
|
+
waveSize: ready.length,
|
|
3031
|
+
allComplete: ready.length === 0 && blocked.length === 0
|
|
3032
|
+
}),
|
|
3033
|
+
isError: false
|
|
3034
|
+
};
|
|
3035
|
+
} catch (err) {
|
|
3036
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3037
|
+
return { text: JSON.stringify({ error: true, message }), isError: true };
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
1866
3040
|
function handleListArtifacts(_cwd) {
|
|
1867
3041
|
const cwd = _cwd ?? process.cwd();
|
|
1868
3042
|
try {
|
|
1869
|
-
const artifactsDir = (0,
|
|
3043
|
+
const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
|
|
3044
|
+
let manifest = null;
|
|
3045
|
+
try {
|
|
3046
|
+
manifest = loadSignatureManifest(cwd);
|
|
3047
|
+
} catch {
|
|
3048
|
+
}
|
|
1870
3049
|
const artifacts = [];
|
|
1871
3050
|
let completedCount = 0;
|
|
1872
3051
|
for (const phase of PHASE_ORDER) {
|
|
1873
3052
|
const expectedFile = PHASE_ARTIFACT[phase];
|
|
1874
|
-
const fullPath = (0,
|
|
1875
|
-
const exists = (0,
|
|
3053
|
+
const fullPath = (0, import_path5.join)(artifactsDir, expectedFile);
|
|
3054
|
+
const exists = (0, import_fs5.existsSync)(fullPath);
|
|
1876
3055
|
if (exists) completedCount++;
|
|
1877
|
-
|
|
3056
|
+
let signed = false;
|
|
3057
|
+
let signatureValid = null;
|
|
3058
|
+
if (exists && manifest) {
|
|
3059
|
+
const entry = manifest.signatures[expectedFile];
|
|
3060
|
+
if (entry) {
|
|
3061
|
+
signed = true;
|
|
3062
|
+
try {
|
|
3063
|
+
const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
|
|
3064
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
3065
|
+
signatureValid = verifyArtifact(rawBytes, entry, publicKey);
|
|
3066
|
+
} catch {
|
|
3067
|
+
signatureValid = null;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
|
|
1878
3072
|
}
|
|
1879
3073
|
return {
|
|
1880
3074
|
text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
|
|
@@ -1887,7 +3081,7 @@ function handleListArtifacts(_cwd) {
|
|
|
1887
3081
|
function handleWritePhaseQuestions(input) {
|
|
1888
3082
|
const cwd = input._cwd ?? process.cwd();
|
|
1889
3083
|
const { phase, responseText } = input;
|
|
1890
|
-
if (!
|
|
3084
|
+
if (!isValidPhase2(phase)) {
|
|
1891
3085
|
return {
|
|
1892
3086
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
1893
3087
|
isError: true
|
|
@@ -1910,9 +3104,9 @@ function handleWritePhaseQuestions(input) {
|
|
|
1910
3104
|
}
|
|
1911
3105
|
} catch {
|
|
1912
3106
|
}
|
|
1913
|
-
const questionsDir = (0,
|
|
1914
|
-
(0,
|
|
1915
|
-
const filePath = (0,
|
|
3107
|
+
const questionsDir = (0, import_path5.join)(cwd, ".stackwright", "questions");
|
|
3108
|
+
(0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
|
|
3109
|
+
const filePath = (0, import_path5.join)(questionsDir, `${phase}.json`);
|
|
1916
3110
|
const payload = {
|
|
1917
3111
|
version: "1.0",
|
|
1918
3112
|
phase,
|
|
@@ -1945,127 +3139,494 @@ function handleWritePhaseQuestions(input) {
|
|
|
1945
3139
|
function handleBuildSpecialistPrompt(input) {
|
|
1946
3140
|
const cwd = input._cwd ?? process.cwd();
|
|
1947
3141
|
const { phase } = input;
|
|
1948
|
-
if (!
|
|
3142
|
+
if (!isValidPhase2(phase)) {
|
|
1949
3143
|
return {
|
|
1950
3144
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
1951
3145
|
isError: true
|
|
1952
3146
|
};
|
|
1953
3147
|
}
|
|
1954
3148
|
try {
|
|
1955
|
-
const answersPath = (0,
|
|
3149
|
+
const answersPath = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1956
3150
|
let answers = {};
|
|
1957
|
-
if ((0,
|
|
3151
|
+
if ((0, import_fs5.existsSync)(answersPath)) {
|
|
1958
3152
|
answers = JSON.parse(safeReadSync(answersPath));
|
|
1959
3153
|
}
|
|
3154
|
+
let buildContextText = "";
|
|
3155
|
+
const buildContextPath = (0, import_path5.join)(cwd, ".stackwright", "build-context.json");
|
|
3156
|
+
if ((0, import_fs5.existsSync)(buildContextPath)) {
|
|
3157
|
+
try {
|
|
3158
|
+
const bcRaw = JSON.parse(safeReadSync(buildContextPath));
|
|
3159
|
+
if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
|
|
3160
|
+
buildContextText = bcRaw.buildContext.trim();
|
|
3161
|
+
}
|
|
3162
|
+
} catch {
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
1960
3165
|
const deps = PHASE_DEPENDENCIES[phase];
|
|
1961
3166
|
const artifactSections = [];
|
|
1962
3167
|
const missingDependencies = [];
|
|
1963
3168
|
for (const dep of deps) {
|
|
1964
3169
|
const artifactFile = PHASE_ARTIFACT[dep];
|
|
1965
|
-
const artifactPath = (0,
|
|
1966
|
-
if ((0,
|
|
1967
|
-
const
|
|
3170
|
+
const artifactPath = (0, import_path5.join)(cwd, ".stackwright", "artifacts", artifactFile);
|
|
3171
|
+
if ((0, import_fs5.existsSync)(artifactPath)) {
|
|
3172
|
+
const rawContent = safeReadSync(artifactPath);
|
|
3173
|
+
const rawBytes = Buffer.from(rawContent, "utf-8");
|
|
3174
|
+
const content = JSON.parse(rawContent);
|
|
3175
|
+
let signatureVerified = false;
|
|
3176
|
+
let signatureAvailable = false;
|
|
3177
|
+
try {
|
|
3178
|
+
const sig = getArtifactSignature(cwd, artifactFile);
|
|
3179
|
+
if (sig) {
|
|
3180
|
+
signatureAvailable = true;
|
|
3181
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
3182
|
+
signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
|
|
3183
|
+
if (!signatureVerified) {
|
|
3184
|
+
const actualDigest = (0, import_crypto3.createHash)("sha384").update(rawBytes).digest("hex");
|
|
3185
|
+
emitSignatureAuditEvent({
|
|
3186
|
+
artifactFilename: artifactFile,
|
|
3187
|
+
expectedDigest: sig.digest,
|
|
3188
|
+
actualDigest,
|
|
3189
|
+
phase: dep,
|
|
3190
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3191
|
+
source: "stackwright_pro_build_specialist_prompt"
|
|
3192
|
+
});
|
|
3193
|
+
missingDependencies.push(dep);
|
|
3194
|
+
artifactSections.push(
|
|
3195
|
+
`[${artifactFile}]:
|
|
3196
|
+
(integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
|
|
3197
|
+
);
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
} catch {
|
|
3202
|
+
}
|
|
1968
3203
|
const expectedOtter = PHASE_TO_OTTER2[dep];
|
|
1969
3204
|
const artifactOtter = content["generatedBy"];
|
|
3205
|
+
const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
|
|
1970
3206
|
if (!artifactOtter) {
|
|
1971
3207
|
missingDependencies.push(dep);
|
|
1972
3208
|
artifactSections.push(
|
|
1973
3209
|
`[${artifactFile}]:
|
|
1974
3210
|
(integrity check failed: missing generatedBy field)`
|
|
1975
3211
|
);
|
|
1976
|
-
} else if (
|
|
3212
|
+
} else if (normalizedOtter !== expectedOtter) {
|
|
1977
3213
|
missingDependencies.push(dep);
|
|
1978
3214
|
artifactSections.push(
|
|
1979
3215
|
`[${artifactFile}]:
|
|
1980
3216
|
(integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
|
|
1981
3217
|
);
|
|
1982
3218
|
} else {
|
|
1983
|
-
|
|
1984
|
-
|
|
3219
|
+
const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
|
|
3220
|
+
artifactSections.push(
|
|
3221
|
+
`[${artifactFile}]${sigStatus}:
|
|
3222
|
+
${JSON.stringify(content, null, 2)}`
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
} else {
|
|
3226
|
+
missingDependencies.push(dep);
|
|
3227
|
+
artifactSections.push(`[${artifactFile}]:
|
|
3228
|
+
(not yet available)`);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
const parts = [];
|
|
3232
|
+
if (buildContextText) {
|
|
3233
|
+
parts.push("BUILD_CONTEXT:", buildContextText, "");
|
|
3234
|
+
}
|
|
3235
|
+
if (phase === "auth") {
|
|
3236
|
+
const initContextPath = (0, import_path5.join)(cwd, ".stackwright", "init-context.json");
|
|
3237
|
+
if ((0, import_fs5.existsSync)(initContextPath)) {
|
|
3238
|
+
try {
|
|
3239
|
+
const initContext = JSON.parse(safeReadSync(initContextPath));
|
|
3240
|
+
if (initContext.devOnly === true || initContext.nonInteractive === true) {
|
|
3241
|
+
answers["devOnly"] = true;
|
|
3242
|
+
}
|
|
3243
|
+
} catch {
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
|
|
3248
|
+
if (artifactSections.length > 0) {
|
|
3249
|
+
parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
|
|
3250
|
+
}
|
|
3251
|
+
const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
|
|
3252
|
+
parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
|
|
3253
|
+
parts.push(artifactSchema);
|
|
3254
|
+
const schemaRef = buildSchemaReferenceBlock(phase);
|
|
3255
|
+
if (schemaRef) {
|
|
3256
|
+
parts.push("", schemaRef);
|
|
3257
|
+
}
|
|
3258
|
+
parts.push("", "Execute using these answers and the upstream artifacts provided.");
|
|
3259
|
+
const prompt = parts.join("\n");
|
|
3260
|
+
const dependenciesSatisfied = missingDependencies.length === 0;
|
|
3261
|
+
return {
|
|
3262
|
+
text: JSON.stringify({
|
|
3263
|
+
otterName: PHASE_TO_OTTER2[phase],
|
|
3264
|
+
phase,
|
|
3265
|
+
prompt,
|
|
3266
|
+
dependenciesSatisfied,
|
|
3267
|
+
missingDependencies
|
|
3268
|
+
}),
|
|
3269
|
+
isError: false
|
|
3270
|
+
};
|
|
3271
|
+
} catch (err) {
|
|
3272
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3273
|
+
return { text: JSON.stringify({ error: true, phase, message }), isError: true };
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
var OFF_SCRIPT_PATTERNS = [
|
|
3277
|
+
{
|
|
3278
|
+
pattern: /```(?:ts|tsx|js|jsx|python|bash|sh|sql|ruby|go|rust|java|csharp|c\+\+)\b/,
|
|
3279
|
+
label: "code fence"
|
|
3280
|
+
},
|
|
3281
|
+
{ pattern: /\bimport\s+[\w{]/, label: "import statement" },
|
|
3282
|
+
{ pattern: /\bexport\s+(?:const|function|default|class)\b/, label: "export statement" },
|
|
3283
|
+
{ pattern: /\brequire\s*\(/, label: "require() call" },
|
|
3284
|
+
{ pattern: /\beval\s*\(/, label: "eval() call" },
|
|
3285
|
+
{ pattern: /^#!/m, label: "shebang" },
|
|
3286
|
+
{ pattern: /<script[\s>]/i, label: "script tag" },
|
|
3287
|
+
{ pattern: /\.(ts|tsx|js|jsx)\b.*\bfile\b/i, label: "code file reference" }
|
|
3288
|
+
];
|
|
3289
|
+
var PHASE_REQUIRED_KEYS = {
|
|
3290
|
+
designer: ["designLanguage", "themeTokenSeeds"],
|
|
3291
|
+
theme: ["tokens"],
|
|
3292
|
+
scaffold: ["version", "generatedBy", "appRouterFiles"],
|
|
3293
|
+
api: ["entities", "version", "generatedBy"],
|
|
3294
|
+
auth: ["version", "generatedBy"],
|
|
3295
|
+
data: ["version", "generatedBy", "strategy", "collections"],
|
|
3296
|
+
pages: ["version", "generatedBy"],
|
|
3297
|
+
dashboard: ["version", "generatedBy"],
|
|
3298
|
+
workflow: ["version", "generatedBy"],
|
|
3299
|
+
services: ["version", "generatedBy", "flows"],
|
|
3300
|
+
polish: ["version", "generatedBy"],
|
|
3301
|
+
geo: ["version", "generatedBy", "geoCollections"]
|
|
3302
|
+
};
|
|
3303
|
+
var PHASE_ARTIFACT_SCHEMA = {
|
|
3304
|
+
designer: JSON.stringify(
|
|
3305
|
+
{
|
|
3306
|
+
version: "1.0",
|
|
3307
|
+
generatedBy: "stackwright-pro-designer-otter",
|
|
3308
|
+
application: {
|
|
3309
|
+
type: "<operational|data-explorer|admin|logistics|general>",
|
|
3310
|
+
environment: "<workstation|field|control-room|mixed>",
|
|
3311
|
+
density: "<compact|balanced|spacious>",
|
|
3312
|
+
accessibility: "<wcag-aa|section-508|none>",
|
|
3313
|
+
colorScheme: "<light|dark|both>"
|
|
3314
|
+
},
|
|
3315
|
+
designLanguage: {
|
|
3316
|
+
rationale: "<design rationale>",
|
|
3317
|
+
spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
|
|
3318
|
+
colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
|
|
3319
|
+
typography: {
|
|
3320
|
+
dataFont: "Inter",
|
|
3321
|
+
headingFont: "Inter",
|
|
3322
|
+
monoFont: "monospace",
|
|
3323
|
+
dataSizePx: 12,
|
|
3324
|
+
bodySizePx: 14
|
|
3325
|
+
},
|
|
3326
|
+
contrastRatio: "4.5",
|
|
3327
|
+
borderRadius: "4",
|
|
3328
|
+
shadowElevation: "standard"
|
|
3329
|
+
},
|
|
3330
|
+
themeTokenSeeds: {
|
|
3331
|
+
light: {
|
|
3332
|
+
background: "#ffffff",
|
|
3333
|
+
foreground: "#1a1a1a",
|
|
3334
|
+
primary: "#1a365d",
|
|
3335
|
+
surface: "#f7f7f7",
|
|
3336
|
+
border: "#e2e8f0"
|
|
3337
|
+
},
|
|
3338
|
+
dark: {
|
|
3339
|
+
background: "#1a1a1a",
|
|
3340
|
+
foreground: "#ffffff",
|
|
3341
|
+
primary: "#90cdf4",
|
|
3342
|
+
surface: "#2d2d2d",
|
|
3343
|
+
border: "#4a5568"
|
|
3344
|
+
}
|
|
3345
|
+
},
|
|
3346
|
+
conformsTo: null,
|
|
3347
|
+
operationalNotes: []
|
|
3348
|
+
},
|
|
3349
|
+
null,
|
|
3350
|
+
2
|
|
3351
|
+
),
|
|
3352
|
+
theme: JSON.stringify(
|
|
3353
|
+
{
|
|
3354
|
+
version: "1.0",
|
|
3355
|
+
generatedBy: "stackwright-pro-theme-otter",
|
|
3356
|
+
componentLibrary: "shadcn",
|
|
3357
|
+
colorScheme: "<light|dark|both>",
|
|
3358
|
+
tokens: {
|
|
3359
|
+
colors: { "primary-500": "#1a365d", background: "#ffffff" },
|
|
3360
|
+
spacing: { "spacing-1": "8px", "spacing-2": "16px" },
|
|
3361
|
+
typography: { "font-data": "Inter", "text-sm": "12px" },
|
|
3362
|
+
shape: { "radius-sm": "4px", "radius-md": "8px" },
|
|
3363
|
+
shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
|
|
3364
|
+
},
|
|
3365
|
+
cssVariables: {
|
|
3366
|
+
"--background": "0 0% 100%",
|
|
3367
|
+
"--foreground": "222.2 84% 4.9%",
|
|
3368
|
+
"--primary": "222.2 47.4% 11.2%",
|
|
3369
|
+
"--primary-foreground": "210 40% 98%",
|
|
3370
|
+
"--surface": "210 40% 98%",
|
|
3371
|
+
"--border": "214.3 31.8% 91.4%"
|
|
3372
|
+
},
|
|
3373
|
+
dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
|
|
3374
|
+
},
|
|
3375
|
+
null,
|
|
3376
|
+
2
|
|
3377
|
+
),
|
|
3378
|
+
scaffold: JSON.stringify(
|
|
3379
|
+
{
|
|
3380
|
+
version: "1.0",
|
|
3381
|
+
generatedBy: "stackwright-pro-scaffold-otter",
|
|
3382
|
+
// REQUIRED: appRouterFiles is a required key — NOT 'files' (see swp-k3mb)
|
|
3383
|
+
appRouterFiles: [
|
|
3384
|
+
"app/layout.tsx",
|
|
3385
|
+
"app/_components/page-client.tsx",
|
|
3386
|
+
"app/page.tsx",
|
|
3387
|
+
"app/[...slug]/page.tsx",
|
|
3388
|
+
"app/_components/providers.tsx",
|
|
3389
|
+
"app/not-found.tsx"
|
|
3390
|
+
],
|
|
3391
|
+
title: "<title from stackwright.yml>",
|
|
3392
|
+
hasCollectionEndpoints: false,
|
|
3393
|
+
hasAuthConfig: true
|
|
3394
|
+
},
|
|
3395
|
+
null,
|
|
3396
|
+
2
|
|
3397
|
+
),
|
|
3398
|
+
api: JSON.stringify(
|
|
3399
|
+
{
|
|
3400
|
+
version: "1.0",
|
|
3401
|
+
generatedBy: "stackwright-pro-api-otter",
|
|
3402
|
+
entities: [
|
|
3403
|
+
{
|
|
3404
|
+
name: "Shipment",
|
|
3405
|
+
endpoint: "/shipments",
|
|
3406
|
+
method: "GET",
|
|
3407
|
+
revalidate: 60,
|
|
3408
|
+
mutationType: null
|
|
3409
|
+
}
|
|
3410
|
+
],
|
|
3411
|
+
skipped: [
|
|
3412
|
+
{
|
|
3413
|
+
spec: "ais-message-models.yaml",
|
|
3414
|
+
format: "asyncapi",
|
|
3415
|
+
reason: "AsyncAPI spec \u2014 WebSocket/event integration required (no REST endpoints)"
|
|
3416
|
+
}
|
|
3417
|
+
],
|
|
3418
|
+
warnings: [
|
|
3419
|
+
"Spec contains external $ref URLs \u2014 recommend bundle: true in stackwright.yml integration config"
|
|
3420
|
+
],
|
|
3421
|
+
auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
|
|
3422
|
+
baseUrl: "https://api.example.mil/v2",
|
|
3423
|
+
specPath: "./specs/api.yaml"
|
|
3424
|
+
},
|
|
3425
|
+
null,
|
|
3426
|
+
2
|
|
3427
|
+
),
|
|
3428
|
+
data: JSON.stringify(
|
|
3429
|
+
{
|
|
3430
|
+
version: "1.0",
|
|
3431
|
+
generatedBy: "stackwright-pro-data-otter",
|
|
3432
|
+
strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
|
|
3433
|
+
pulseMode: false,
|
|
3434
|
+
collections: [{ name: "equipment", revalidate: 60, pulse: false }],
|
|
3435
|
+
endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
|
|
3436
|
+
requiredPackages: { dependencies: {}, devPackages: {} }
|
|
3437
|
+
},
|
|
3438
|
+
null,
|
|
3439
|
+
2
|
|
3440
|
+
),
|
|
3441
|
+
geo: JSON.stringify(
|
|
3442
|
+
{
|
|
3443
|
+
version: "1.0",
|
|
3444
|
+
generatedBy: "stackwright-pro-geo-otter",
|
|
3445
|
+
geoCollections: [
|
|
3446
|
+
{
|
|
3447
|
+
collection: "vessels",
|
|
3448
|
+
latField: "latitude",
|
|
3449
|
+
lngField: "longitude",
|
|
3450
|
+
labelField: "vesselName",
|
|
3451
|
+
colorField: "navigationStatus"
|
|
3452
|
+
}
|
|
3453
|
+
],
|
|
3454
|
+
pages: [
|
|
3455
|
+
{
|
|
3456
|
+
slug: "fleet-tracker",
|
|
3457
|
+
type: "<tracker|zone|route|combined>",
|
|
3458
|
+
collections: ["vessels"],
|
|
3459
|
+
hasLayers: false
|
|
3460
|
+
}
|
|
3461
|
+
]
|
|
3462
|
+
},
|
|
3463
|
+
null,
|
|
3464
|
+
2
|
|
3465
|
+
),
|
|
3466
|
+
workflow: JSON.stringify(
|
|
3467
|
+
{
|
|
3468
|
+
version: "1.0",
|
|
3469
|
+
generatedBy: "stackwright-pro-workflow-otter",
|
|
3470
|
+
// Root key is `workflow` — NOT `workflowConfig` (see swp-k7cl)
|
|
3471
|
+
workflow: {
|
|
3472
|
+
id: "procurement-approval",
|
|
3473
|
+
route: "/procurement",
|
|
3474
|
+
files: ["workflows/procurement-approval.yml"],
|
|
3475
|
+
serviceDependencies: ["service:workflow-state"],
|
|
3476
|
+
warnings: []
|
|
3477
|
+
}
|
|
3478
|
+
},
|
|
3479
|
+
null,
|
|
3480
|
+
2
|
|
3481
|
+
),
|
|
3482
|
+
services: JSON.stringify(
|
|
3483
|
+
{
|
|
3484
|
+
version: "1.0",
|
|
3485
|
+
generatedBy: "stackwright-services-otter",
|
|
3486
|
+
flows: [
|
|
3487
|
+
{
|
|
3488
|
+
name: "at-risk-patients",
|
|
3489
|
+
trigger: "<http|event|schedule|queue>",
|
|
3490
|
+
steps: 3,
|
|
3491
|
+
outputSpec: "at-risk-patients.openapi.json"
|
|
3492
|
+
}
|
|
3493
|
+
],
|
|
3494
|
+
workflows: [
|
|
3495
|
+
{
|
|
3496
|
+
name: "evacuation-coordination",
|
|
3497
|
+
states: 4,
|
|
3498
|
+
transitions: 5
|
|
3499
|
+
}
|
|
3500
|
+
],
|
|
3501
|
+
openApiSpecs: ["at-risk-patients.openapi.json"],
|
|
3502
|
+
capabilitiesUsed: ["service.call", "collection.join", "collection.filter"]
|
|
3503
|
+
},
|
|
3504
|
+
null,
|
|
3505
|
+
2
|
|
3506
|
+
),
|
|
3507
|
+
pages: JSON.stringify(
|
|
3508
|
+
{
|
|
3509
|
+
version: "1.0",
|
|
3510
|
+
generatedBy: "stackwright-pro-page-otter",
|
|
3511
|
+
pages: [
|
|
3512
|
+
{
|
|
3513
|
+
slug: "catalog",
|
|
3514
|
+
type: "collection_listing",
|
|
3515
|
+
collection: "products",
|
|
3516
|
+
themeApplied: true,
|
|
3517
|
+
authRequired: false
|
|
3518
|
+
},
|
|
3519
|
+
{
|
|
3520
|
+
slug: "admin",
|
|
3521
|
+
type: "protected",
|
|
3522
|
+
collection: null,
|
|
3523
|
+
themeApplied: true,
|
|
3524
|
+
authRequired: true
|
|
1985
3525
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
3526
|
+
]
|
|
3527
|
+
},
|
|
3528
|
+
null,
|
|
3529
|
+
2
|
|
3530
|
+
),
|
|
3531
|
+
dashboard: JSON.stringify(
|
|
3532
|
+
{
|
|
3533
|
+
version: "1.0",
|
|
3534
|
+
generatedBy: "stackwright-pro-dashboard-otter",
|
|
3535
|
+
pages: [
|
|
3536
|
+
{
|
|
3537
|
+
slug: "dashboard",
|
|
3538
|
+
layout: "<grid|table|mixed>",
|
|
3539
|
+
collections: ["equipment", "supplies"],
|
|
3540
|
+
mode: "<ISR|Pulse>"
|
|
3541
|
+
}
|
|
3542
|
+
]
|
|
3543
|
+
},
|
|
3544
|
+
null,
|
|
3545
|
+
2
|
|
3546
|
+
),
|
|
3547
|
+
// type: 'pki' = CAC/DoD certificate auth | 'oidc' = enterprise SSO
|
|
3548
|
+
// For dev-only mock auth: use type: 'oidc' with devOnly: true (Zod strips devOnly — it's a convention only)
|
|
3549
|
+
auth: JSON.stringify(
|
|
3550
|
+
{
|
|
3551
|
+
version: "1.0",
|
|
3552
|
+
generatedBy: "stackwright-pro-auth-otter",
|
|
3553
|
+
authConfig: {
|
|
3554
|
+
type: "<pki|oidc>",
|
|
3555
|
+
// OIDC-only fields (omit for pki):
|
|
3556
|
+
provider: "<azure_ad|okta|cognito|auth0|authentik|keycloak|custom>",
|
|
3557
|
+
discoveryUrl: "<IdP OIDC discovery URL>",
|
|
3558
|
+
clientId: "<OIDC client ID>",
|
|
3559
|
+
clientSecret: "<OIDC client secret>",
|
|
3560
|
+
rbacRoles: ["ADMIN", "ANALYST"],
|
|
3561
|
+
rbacDefaultRole: "ANALYST",
|
|
3562
|
+
protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
|
|
3563
|
+
auditEnabled: true,
|
|
3564
|
+
auditRetentionDays: 90
|
|
3565
|
+
},
|
|
3566
|
+
// devScripts is OPTIONAL — only present when devOnly: true was used
|
|
3567
|
+
// Polish otter and foreman MUST check devScripts.written before referencing scripts
|
|
3568
|
+
devScripts: {
|
|
3569
|
+
written: true,
|
|
3570
|
+
scripts: { "dev:admin": "MOCK_USER=admin next dev" }
|
|
1990
3571
|
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
return { text: JSON.stringify({ error: true, phase, message }), isError: true };
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
var OFF_SCRIPT_PATTERNS = [
|
|
2015
|
-
{
|
|
2016
|
-
pattern: /```(?:ts|tsx|js|jsx|python|bash|sh|sql|ruby|go|rust|java|csharp|c\+\+)\b/,
|
|
2017
|
-
label: "code fence"
|
|
2018
|
-
},
|
|
2019
|
-
{ pattern: /\bimport\s+[\w{]/, label: "import statement" },
|
|
2020
|
-
{ pattern: /\bexport\s+(?:const|function|default|class)\b/, label: "export statement" },
|
|
2021
|
-
{ pattern: /\brequire\s*\(/, label: "require() call" },
|
|
2022
|
-
{ pattern: /\beval\s*\(/, label: "eval() call" },
|
|
2023
|
-
{ pattern: /^#!/m, label: "shebang" },
|
|
2024
|
-
{ pattern: /<script[\s>]/i, label: "script tag" },
|
|
2025
|
-
{ pattern: /\.(ts|tsx|js|jsx)\b.*\bfile\b/i, label: "code file reference" }
|
|
2026
|
-
];
|
|
2027
|
-
var PHASE_REQUIRED_KEYS = {
|
|
2028
|
-
designer: ["designLanguage", "themeTokenSeeds"],
|
|
2029
|
-
theme: ["tokens"],
|
|
2030
|
-
api: ["entities"],
|
|
2031
|
-
auth: ["version", "generatedBy"],
|
|
2032
|
-
data: ["version", "generatedBy"],
|
|
2033
|
-
pages: ["version", "generatedBy"],
|
|
2034
|
-
dashboard: ["version", "generatedBy"],
|
|
2035
|
-
workflow: ["version", "generatedBy"]
|
|
3572
|
+
},
|
|
3573
|
+
null,
|
|
3574
|
+
2
|
|
3575
|
+
),
|
|
3576
|
+
polish: JSON.stringify(
|
|
3577
|
+
{
|
|
3578
|
+
version: "1.0",
|
|
3579
|
+
generatedBy: "stackwright-pro-polish-otter",
|
|
3580
|
+
landingPage: { slug: "", rewritten: true },
|
|
3581
|
+
navigation: { itemCount: 5, items: ["/dashboard", "/equipment", "/workflows"] },
|
|
3582
|
+
gettingStarted: "<rewritten|redirected|not-found>",
|
|
3583
|
+
scaffoldCleanup: {
|
|
3584
|
+
deleted: ["content/posts/getting-started.yaml"],
|
|
3585
|
+
rewritten: ["app/not-found.tsx"],
|
|
3586
|
+
skipped: []
|
|
3587
|
+
}
|
|
3588
|
+
},
|
|
3589
|
+
null,
|
|
3590
|
+
2
|
|
3591
|
+
)
|
|
2036
3592
|
};
|
|
2037
3593
|
function handleValidateArtifact(input) {
|
|
2038
3594
|
const cwd = input._cwd ?? process.cwd();
|
|
2039
|
-
const { phase, responseText } = input;
|
|
2040
|
-
if (!
|
|
3595
|
+
const { phase, responseText, artifact: directArtifact } = input;
|
|
3596
|
+
if (!isValidPhase2(phase)) {
|
|
2041
3597
|
return {
|
|
2042
3598
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
2043
3599
|
isError: true
|
|
2044
3600
|
};
|
|
2045
3601
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
3602
|
+
let artifact;
|
|
3603
|
+
if (directArtifact) {
|
|
3604
|
+
artifact = directArtifact;
|
|
3605
|
+
} else {
|
|
3606
|
+
const text = responseText ?? "";
|
|
3607
|
+
for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
|
|
3608
|
+
if (pattern.test(text)) {
|
|
3609
|
+
const result = {
|
|
3610
|
+
valid: false,
|
|
3611
|
+
phase,
|
|
3612
|
+
violation: "off-script",
|
|
3613
|
+
retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
|
|
3614
|
+
};
|
|
3615
|
+
return { text: JSON.stringify(result), isError: false };
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
try {
|
|
3619
|
+
artifact = extractJsonFromResponse(text);
|
|
3620
|
+
} catch {
|
|
2048
3621
|
const result = {
|
|
2049
3622
|
valid: false,
|
|
2050
3623
|
phase,
|
|
2051
|
-
violation: "
|
|
2052
|
-
retryPrompt:
|
|
3624
|
+
violation: "invalid-json",
|
|
3625
|
+
retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
|
|
2053
3626
|
};
|
|
2054
3627
|
return { text: JSON.stringify(result), isError: false };
|
|
2055
3628
|
}
|
|
2056
3629
|
}
|
|
2057
|
-
let artifact;
|
|
2058
|
-
try {
|
|
2059
|
-
artifact = extractJsonFromResponse(responseText);
|
|
2060
|
-
} catch {
|
|
2061
|
-
const result = {
|
|
2062
|
-
valid: false,
|
|
2063
|
-
phase,
|
|
2064
|
-
violation: "invalid-json",
|
|
2065
|
-
retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
|
|
2066
|
-
};
|
|
2067
|
-
return { text: JSON.stringify(result), isError: false };
|
|
2068
|
-
}
|
|
2069
3630
|
if (!artifact.version || !artifact.generatedBy) {
|
|
2070
3631
|
const result = {
|
|
2071
3632
|
valid: false,
|
|
@@ -2086,12 +3647,68 @@ function handleValidateArtifact(input) {
|
|
|
2086
3647
|
};
|
|
2087
3648
|
return { text: JSON.stringify(result), isError: false };
|
|
2088
3649
|
}
|
|
3650
|
+
const PHASE_ZOD_VALIDATORS = {
|
|
3651
|
+
workflow: (artifact2) => {
|
|
3652
|
+
const workflowData = artifact2["workflow"];
|
|
3653
|
+
if (!workflowData) {
|
|
3654
|
+
if (artifact2["workflowConfig"]) {
|
|
3655
|
+
console.warn(
|
|
3656
|
+
'[pipeline] DEPRECATED: workflow artifact uses "workflowConfig" key; rename to "workflow"'
|
|
3657
|
+
);
|
|
3658
|
+
return { success: true };
|
|
3659
|
+
}
|
|
3660
|
+
return { success: true };
|
|
3661
|
+
}
|
|
3662
|
+
if (typeof workflowData === "object" && workflowData !== null && "workflow" in workflowData) {
|
|
3663
|
+
const result = import_types3.WorkflowFileSchema.safeParse(workflowData);
|
|
3664
|
+
if (!result.success) {
|
|
3665
|
+
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
3666
|
+
return { success: false, error: { message: issues } };
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
return { success: true };
|
|
3670
|
+
},
|
|
3671
|
+
auth: (artifact2) => {
|
|
3672
|
+
const authConfig = artifact2["authConfig"];
|
|
3673
|
+
if (!authConfig) return { success: true };
|
|
3674
|
+
const result = import_types3.authConfigSchema.safeParse(authConfig);
|
|
3675
|
+
if (!result.success) {
|
|
3676
|
+
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
3677
|
+
return { success: false, error: { message: issues } };
|
|
3678
|
+
}
|
|
3679
|
+
return { success: true };
|
|
3680
|
+
}
|
|
3681
|
+
};
|
|
3682
|
+
const zodValidator = PHASE_ZOD_VALIDATORS[phase];
|
|
3683
|
+
if (zodValidator) {
|
|
3684
|
+
const zodResult = zodValidator(artifact);
|
|
3685
|
+
if (!zodResult.success) {
|
|
3686
|
+
const result = {
|
|
3687
|
+
valid: false,
|
|
3688
|
+
phase,
|
|
3689
|
+
violation: "schema-mismatch",
|
|
3690
|
+
retryPrompt: `Your artifact failed schema validation: ${zodResult.error?.message}. Fix these fields and return the corrected JSON artifact.`
|
|
3691
|
+
};
|
|
3692
|
+
return { text: JSON.stringify(result), isError: false };
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
2089
3695
|
try {
|
|
2090
|
-
const artifactsDir = (0,
|
|
2091
|
-
(0,
|
|
3696
|
+
const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
|
|
3697
|
+
(0, import_fs5.mkdirSync)(artifactsDir, { recursive: true });
|
|
2092
3698
|
const artifactFile = PHASE_ARTIFACT[phase];
|
|
2093
|
-
const artifactPath = (0,
|
|
2094
|
-
|
|
3699
|
+
const artifactPath = (0, import_path5.join)(artifactsDir, artifactFile);
|
|
3700
|
+
const serialized = JSON.stringify(artifact, null, 2) + "\n";
|
|
3701
|
+
const artifactBytes = Buffer.from(serialized, "utf-8");
|
|
3702
|
+
safeWriteSync(artifactPath, serialized);
|
|
3703
|
+
let signed = false;
|
|
3704
|
+
try {
|
|
3705
|
+
const { privateKey } = loadPipelineKeys(cwd);
|
|
3706
|
+
const sig = signArtifact(artifactBytes, privateKey);
|
|
3707
|
+
const signerOtter = PHASE_TO_OTTER2[phase];
|
|
3708
|
+
saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
|
|
3709
|
+
signed = true;
|
|
3710
|
+
} catch {
|
|
3711
|
+
}
|
|
2095
3712
|
const state = readState(cwd);
|
|
2096
3713
|
if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
|
|
2097
3714
|
const ps = state.phases[phase];
|
|
@@ -2102,7 +3719,7 @@ function handleValidateArtifact(input) {
|
|
|
2102
3719
|
valid: true,
|
|
2103
3720
|
phase,
|
|
2104
3721
|
artifactPath,
|
|
2105
|
-
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
|
|
3722
|
+
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
|
|
2106
3723
|
};
|
|
2107
3724
|
return { text: JSON.stringify(result), isError: false };
|
|
2108
3725
|
} catch (err) {
|
|
@@ -2126,11 +3743,24 @@ function registerPipelineTools(server2) {
|
|
|
2126
3743
|
"stackwright_pro_set_pipeline_state",
|
|
2127
3744
|
`Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
|
|
2128
3745
|
{
|
|
2129
|
-
phase:
|
|
2130
|
-
field:
|
|
2131
|
-
value:
|
|
2132
|
-
|
|
2133
|
-
|
|
3746
|
+
phase: import_zod13.z.string().optional().describe('Phase to update, e.g. "designer"'),
|
|
3747
|
+
field: import_zod13.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
|
|
3748
|
+
value: boolCoerce(import_zod13.z.boolean().optional()).describe(
|
|
3749
|
+
'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
|
|
3750
|
+
),
|
|
3751
|
+
status: import_zod13.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
|
|
3752
|
+
incrementRetry: boolCoerce(import_zod13.z.boolean().optional()).describe(
|
|
3753
|
+
"Bump retryCount by 1 \u2014 must be a JSON boolean"
|
|
3754
|
+
),
|
|
3755
|
+
updates: jsonCoerce(
|
|
3756
|
+
import_zod13.z.array(
|
|
3757
|
+
import_zod13.z.object({
|
|
3758
|
+
phase: import_zod13.z.string(),
|
|
3759
|
+
field: import_zod13.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]),
|
|
3760
|
+
value: import_zod13.z.boolean()
|
|
3761
|
+
})
|
|
3762
|
+
).optional()
|
|
3763
|
+
).describe("Batch updates \u2014 apply multiple field changes in one atomic read\u2192modify\u2192write")
|
|
2134
3764
|
},
|
|
2135
3765
|
async (args) => res(
|
|
2136
3766
|
handleSetPipelineState({
|
|
@@ -2138,15 +3768,24 @@ function registerPipelineTools(server2) {
|
|
|
2138
3768
|
...args.field != null ? { field: args.field } : {},
|
|
2139
3769
|
...args.value != null ? { value: args.value } : {},
|
|
2140
3770
|
...args.status != null ? { status: args.status } : {},
|
|
2141
|
-
...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {}
|
|
3771
|
+
...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {},
|
|
3772
|
+
...args.updates != null ? { updates: args.updates } : {}
|
|
2142
3773
|
})
|
|
2143
3774
|
)
|
|
2144
3775
|
);
|
|
2145
3776
|
server2.tool(
|
|
2146
3777
|
"stackwright_pro_check_execution_ready",
|
|
2147
|
-
`Check all phases have answer files in .stackwright/answers/. ${DESC}`,
|
|
3778
|
+
`Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
|
|
3779
|
+
{
|
|
3780
|
+
phase: import_zod13.z.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
|
|
3781
|
+
},
|
|
3782
|
+
async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
|
|
3783
|
+
);
|
|
3784
|
+
server2.tool(
|
|
3785
|
+
"stackwright_pro_get_ready_phases",
|
|
3786
|
+
`Return phases whose dependencies are all satisfied (executed=true). Use to determine which phases can run next \u2014 enables wave-based execution. ${DESC}`,
|
|
2148
3787
|
{},
|
|
2149
|
-
async () => res(
|
|
3788
|
+
async () => res(handleGetReadyPhases())
|
|
2150
3789
|
);
|
|
2151
3790
|
server2.tool(
|
|
2152
3791
|
"stackwright_pro_list_artifacts",
|
|
@@ -2156,40 +3795,91 @@ function registerPipelineTools(server2) {
|
|
|
2156
3795
|
);
|
|
2157
3796
|
server2.tool(
|
|
2158
3797
|
"stackwright_pro_write_phase_questions",
|
|
2159
|
-
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
|
|
3798
|
+
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
|
|
2160
3799
|
{
|
|
2161
|
-
phase:
|
|
2162
|
-
responseText:
|
|
3800
|
+
phase: import_zod13.z.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
|
|
3801
|
+
responseText: import_zod13.z.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
|
|
3802
|
+
questions: jsonCoerce(import_zod13.z.array(import_zod13.z.any()).optional()).describe(
|
|
3803
|
+
"Questions array for direct specialist write"
|
|
3804
|
+
)
|
|
2163
3805
|
},
|
|
2164
|
-
async ({ phase, responseText }) =>
|
|
3806
|
+
async ({ phase, responseText, questions }) => {
|
|
3807
|
+
if (phase && questions && Array.isArray(questions)) {
|
|
3808
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
3809
|
+
return {
|
|
3810
|
+
content: [
|
|
3811
|
+
{ type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
|
|
3812
|
+
],
|
|
3813
|
+
isError: true
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3816
|
+
const questionsDir = (0, import_path5.join)(process.cwd(), ".stackwright", "questions");
|
|
3817
|
+
(0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
|
|
3818
|
+
const outPath = (0, import_path5.join)(questionsDir, `${phase}.json`);
|
|
3819
|
+
(0, import_fs5.writeFileSync)(
|
|
3820
|
+
outPath,
|
|
3821
|
+
JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
3822
|
+
);
|
|
3823
|
+
return {
|
|
3824
|
+
content: [
|
|
3825
|
+
{
|
|
3826
|
+
type: "text",
|
|
3827
|
+
text: JSON.stringify({
|
|
3828
|
+
written: true,
|
|
3829
|
+
phase,
|
|
3830
|
+
count: questions.length
|
|
3831
|
+
})
|
|
3832
|
+
}
|
|
3833
|
+
]
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
return res(
|
|
3837
|
+
handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
|
|
3838
|
+
);
|
|
3839
|
+
}
|
|
2165
3840
|
);
|
|
2166
3841
|
server2.tool(
|
|
2167
3842
|
"stackwright_pro_build_specialist_prompt",
|
|
2168
3843
|
`Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
|
|
2169
|
-
{ phase:
|
|
3844
|
+
{ phase: import_zod13.z.string().describe('Phase to build prompt for, e.g. "pages"') },
|
|
2170
3845
|
async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
|
|
2171
3846
|
);
|
|
2172
3847
|
server2.tool(
|
|
2173
3848
|
"stackwright_pro_validate_artifact",
|
|
2174
|
-
`Validate
|
|
3849
|
+
`Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
|
|
2175
3850
|
{
|
|
2176
|
-
phase:
|
|
2177
|
-
responseText:
|
|
3851
|
+
phase: import_zod13.z.string().describe('Phase that produced this artifact, e.g. "designer"'),
|
|
3852
|
+
responseText: import_zod13.z.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
|
|
3853
|
+
artifact: jsonCoerce(import_zod13.z.record(import_zod13.z.string(), import_zod13.z.unknown()).optional()).describe(
|
|
3854
|
+
"Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
|
|
3855
|
+
)
|
|
2178
3856
|
},
|
|
2179
|
-
async ({ phase, responseText }) =>
|
|
3857
|
+
async ({ phase, responseText, artifact }) => {
|
|
3858
|
+
if (artifact) {
|
|
3859
|
+
return res(
|
|
3860
|
+
handleValidateArtifact({ phase, artifact })
|
|
3861
|
+
);
|
|
3862
|
+
}
|
|
3863
|
+
return res(handleValidateArtifact({ phase, responseText: responseText ?? "" }));
|
|
3864
|
+
}
|
|
2180
3865
|
);
|
|
2181
3866
|
}
|
|
2182
3867
|
|
|
2183
3868
|
// src/tools/safe-write.ts
|
|
2184
|
-
var
|
|
2185
|
-
var
|
|
2186
|
-
var
|
|
3869
|
+
var import_zod14 = require("zod");
|
|
3870
|
+
var import_fs6 = require("fs");
|
|
3871
|
+
var import_path6 = require("path");
|
|
2187
3872
|
var OTTER_WRITE_ALLOWLISTS = {
|
|
2188
3873
|
"stackwright-pro-designer-otter": [
|
|
2189
3874
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
|
|
2190
3875
|
],
|
|
2191
3876
|
"stackwright-pro-theme-otter": [
|
|
2192
|
-
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Theme tokens artifact" }
|
|
3877
|
+
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Theme tokens artifact" },
|
|
3878
|
+
{
|
|
3879
|
+
prefix: "stackwright.theme.",
|
|
3880
|
+
suffix: ".yml",
|
|
3881
|
+
description: "Theme config YAML (merged at build time)"
|
|
3882
|
+
}
|
|
2193
3883
|
],
|
|
2194
3884
|
"stackwright-pro-auth-otter": [
|
|
2195
3885
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Auth config artifact" },
|
|
@@ -2199,6 +3889,16 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
2199
3889
|
prefix: ".env",
|
|
2200
3890
|
suffix: "",
|
|
2201
3891
|
description: "Dotenv files (.env, .env.local, .env.production, etc.)"
|
|
3892
|
+
},
|
|
3893
|
+
{
|
|
3894
|
+
prefix: "lib/mock-auth",
|
|
3895
|
+
suffix: ".ts",
|
|
3896
|
+
description: "Mock auth module (DEV_ONLY_MODE role updates)"
|
|
3897
|
+
},
|
|
3898
|
+
{
|
|
3899
|
+
prefix: "package.json",
|
|
3900
|
+
suffix: ".json",
|
|
3901
|
+
description: "Project package.json (DEV_ONLY_MODE script cleanup)"
|
|
2202
3902
|
}
|
|
2203
3903
|
],
|
|
2204
3904
|
"stackwright-pro-data-otter": [
|
|
@@ -2220,21 +3920,138 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
2220
3920
|
{ prefix: "workflows/", suffix: ".yaml", description: "Workflow definition" },
|
|
2221
3921
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" }
|
|
2222
3922
|
],
|
|
3923
|
+
"stackwright-pro-scaffold-otter": [
|
|
3924
|
+
{ prefix: "app/", suffix: ".tsx", description: "Next.js App Router shell files" },
|
|
3925
|
+
{
|
|
3926
|
+
prefix: ".stackwright/artifacts/",
|
|
3927
|
+
suffix: ".json",
|
|
3928
|
+
description: "Scaffold manifest artifact"
|
|
3929
|
+
}
|
|
3930
|
+
],
|
|
2223
3931
|
"stackwright-pro-api-otter": [
|
|
2224
3932
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "API config artifact" }
|
|
3933
|
+
],
|
|
3934
|
+
"stackwright-pro-geo-otter": [
|
|
3935
|
+
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Geo manifest artifact" },
|
|
3936
|
+
{ prefix: "pages/", suffix: "/content.yml", description: "Map page content" },
|
|
3937
|
+
{ prefix: "pages/", suffix: "/content.yaml", description: "Map page content" }
|
|
3938
|
+
],
|
|
3939
|
+
"stackwright-pro-polish-otter": [
|
|
3940
|
+
{
|
|
3941
|
+
prefix: "stackwright.yml",
|
|
3942
|
+
suffix: "",
|
|
3943
|
+
description: "Stackwright config (navigation updates)"
|
|
3944
|
+
},
|
|
3945
|
+
{
|
|
3946
|
+
prefix: "stackwright.navigation.",
|
|
3947
|
+
suffix: ".yml",
|
|
3948
|
+
description: "Navigation config YAML (merged at build time)"
|
|
3949
|
+
},
|
|
3950
|
+
{ prefix: "pages/", suffix: "/content.yml", description: "Landing page content" },
|
|
3951
|
+
{ prefix: "pages/", suffix: "/content.yaml", description: "Landing page content" },
|
|
3952
|
+
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Polish artifact" },
|
|
3953
|
+
{ prefix: "README.md", suffix: "", description: "Project README rewrite" }
|
|
3954
|
+
],
|
|
3955
|
+
// domain-expert-otter is a reader/answerer only — it writes via stackwright_pro_save_phase_answers,
|
|
3956
|
+
// not via safe_write. Entry required here because the MCP tool availability guard boilerplate
|
|
3957
|
+
// in its system prompt mentions stackwright_pro_safe_write (triggering the coherence test).
|
|
3958
|
+
"stackwright-pro-domain-expert-otter": [],
|
|
3959
|
+
"stackwright-services-otter": [
|
|
3960
|
+
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Services config artifact" },
|
|
3961
|
+
{ prefix: "services/", suffix: ".ts", description: "Service implementation files" },
|
|
3962
|
+
{ prefix: "services/", suffix: ".yaml", description: "Service flow definitions" },
|
|
3963
|
+
{ prefix: "services/", suffix: ".yml", description: "Service flow definitions" },
|
|
3964
|
+
{ prefix: "lib/seeds/", suffix: ".ts", description: "Seed data files" },
|
|
3965
|
+
{ prefix: "specs/", suffix: ".json", description: "Generated OpenAPI specs" },
|
|
3966
|
+
{ prefix: "specs/", suffix: ".yaml", description: "Generated OpenAPI specs" },
|
|
3967
|
+
{ prefix: "stackwright-generated/", suffix: ".json", description: "Services manifest" }
|
|
2225
3968
|
]
|
|
2226
3969
|
};
|
|
2227
3970
|
var PROTECTED_PATH_PREFIXES = [
|
|
2228
3971
|
".stackwright/pipeline-state.json",
|
|
3972
|
+
".stackwright/pipeline-keys.json",
|
|
3973
|
+
// ephemeral signing keys
|
|
3974
|
+
".stackwright/artifacts/signatures.json",
|
|
3975
|
+
// artifact signature manifest
|
|
2229
3976
|
".stackwright/questions/",
|
|
2230
|
-
".stackwright/answers/"
|
|
3977
|
+
".stackwright/answers/",
|
|
3978
|
+
".stackwright/page-registry.json"
|
|
3979
|
+
// page ownership — managed by safe_write internally
|
|
2231
3980
|
];
|
|
3981
|
+
var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
|
|
3982
|
+
var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
|
|
3983
|
+
var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
|
|
3984
|
+
var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
|
|
3985
|
+
function getMaxBytesForPath(filePath) {
|
|
3986
|
+
if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
|
|
3987
|
+
if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
|
|
3988
|
+
return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
|
|
3989
|
+
if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
|
|
3990
|
+
return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
|
|
3991
|
+
return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
|
|
3992
|
+
}
|
|
3993
|
+
var PAGE_REGISTRY_FILE = ".stackwright/page-registry.json";
|
|
3994
|
+
function extractPageSlug(filePath) {
|
|
3995
|
+
const match = (0, import_path6.normalize)(filePath).match(/^pages\/(.+?)\/content\.ya?ml$/);
|
|
3996
|
+
return match?.[1] ?? null;
|
|
3997
|
+
}
|
|
3998
|
+
function extractContentTypes(yamlContent) {
|
|
3999
|
+
const typePattern = /^\s*-?\s*type:\s*(\S+)/gm;
|
|
4000
|
+
const types = [];
|
|
4001
|
+
let m;
|
|
4002
|
+
while ((m = typePattern.exec(yamlContent)) !== null) {
|
|
4003
|
+
const typeName = m[1];
|
|
4004
|
+
if (typeName && !types.includes(typeName)) {
|
|
4005
|
+
types.push(typeName);
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
return types.length > 0 ? types : ["unknown"];
|
|
4009
|
+
}
|
|
4010
|
+
function readPageRegistry(cwd) {
|
|
4011
|
+
const regPath = (0, import_path6.join)(cwd, PAGE_REGISTRY_FILE);
|
|
4012
|
+
if (!(0, import_fs6.existsSync)(regPath)) return {};
|
|
4013
|
+
try {
|
|
4014
|
+
const stat = (0, import_fs6.lstatSync)(regPath);
|
|
4015
|
+
if (stat.isSymbolicLink()) return {};
|
|
4016
|
+
return JSON.parse((0, import_fs6.readFileSync)(regPath, "utf-8"));
|
|
4017
|
+
} catch {
|
|
4018
|
+
return {};
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
function writePageRegistry(cwd, registry) {
|
|
4022
|
+
const dir = (0, import_path6.join)(cwd, ".stackwright");
|
|
4023
|
+
(0, import_fs6.mkdirSync)(dir, { recursive: true });
|
|
4024
|
+
const regPath = (0, import_path6.join)(cwd, PAGE_REGISTRY_FILE);
|
|
4025
|
+
if ((0, import_fs6.existsSync)(regPath)) {
|
|
4026
|
+
const stat = (0, import_fs6.lstatSync)(regPath);
|
|
4027
|
+
if (stat.isSymbolicLink()) {
|
|
4028
|
+
throw new Error("Refusing to write page registry through symlink");
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
(0, import_fs6.writeFileSync)(regPath, JSON.stringify(registry, null, 2) + "\n", { encoding: "utf-8" });
|
|
4032
|
+
}
|
|
4033
|
+
function checkPageOwnership(cwd, slug, callerOtter) {
|
|
4034
|
+
const registry = readPageRegistry(cwd);
|
|
4035
|
+
const existing = registry[slug];
|
|
4036
|
+
if (!existing || existing.claimedBy === callerOtter) {
|
|
4037
|
+
return { allowed: true };
|
|
4038
|
+
}
|
|
4039
|
+
return {
|
|
4040
|
+
allowed: false,
|
|
4041
|
+
owner: existing.claimedBy,
|
|
4042
|
+
contentTypes: existing.contentTypes
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
var NEXTJS_BRACKET_SEGMENT_RE = /\[\[\.{3}[a-zA-Z_][a-zA-Z0-9_]*\]\]|\[\.{3}[a-zA-Z_][a-zA-Z0-9_]*\]|\[[a-zA-Z_][a-zA-Z0-9_]*\]/g;
|
|
4046
|
+
function stripNextjsBracketSegments(path3) {
|
|
4047
|
+
return path3.replace(NEXTJS_BRACKET_SEGMENT_RE, "");
|
|
4048
|
+
}
|
|
2232
4049
|
function checkPathAllowed(callerOtter, filePath) {
|
|
2233
|
-
const normalized = (0,
|
|
2234
|
-
if (normalized.includes("..")) {
|
|
4050
|
+
const normalized = (0, import_path6.normalize)(filePath);
|
|
4051
|
+
if (stripNextjsBracketSegments(normalized).includes("..")) {
|
|
2235
4052
|
return { allowed: false, error: 'Path traversal detected: ".." segments are not allowed' };
|
|
2236
4053
|
}
|
|
2237
|
-
if ((0,
|
|
4054
|
+
if ((0, import_path6.isAbsolute)(normalized)) {
|
|
2238
4055
|
return {
|
|
2239
4056
|
allowed: false,
|
|
2240
4057
|
error: "Absolute paths are not allowed \u2014 use paths relative to project root"
|
|
@@ -2270,6 +4087,16 @@ function checkPathAllowed(callerOtter, filePath) {
|
|
|
2270
4087
|
continue;
|
|
2271
4088
|
}
|
|
2272
4089
|
}
|
|
4090
|
+
if (rule.prefix === "lib/mock-auth" && rule.suffix === ".ts") {
|
|
4091
|
+
if (normalized !== "lib/mock-auth.ts") {
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
if (rule.prefix === "package.json" && rule.suffix === ".json") {
|
|
4096
|
+
if (normalized !== "package.json") {
|
|
4097
|
+
continue;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
2273
4100
|
return { allowed: true, rule: rule.description };
|
|
2274
4101
|
}
|
|
2275
4102
|
}
|
|
@@ -2354,11 +4181,23 @@ function handleSafeWrite(input) {
|
|
|
2354
4181
|
};
|
|
2355
4182
|
return { text: JSON.stringify(result), isError: true };
|
|
2356
4183
|
}
|
|
2357
|
-
const
|
|
2358
|
-
const
|
|
2359
|
-
if (
|
|
4184
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
4185
|
+
const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
|
|
4186
|
+
if (contentBytes > maxBytes) {
|
|
4187
|
+
const result = {
|
|
4188
|
+
success: false,
|
|
4189
|
+
error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
|
|
4190
|
+
callerOtter,
|
|
4191
|
+
attemptedPath: filePath,
|
|
4192
|
+
allowedPaths: []
|
|
4193
|
+
};
|
|
4194
|
+
return { text: JSON.stringify(result), isError: true };
|
|
4195
|
+
}
|
|
4196
|
+
const normalized = (0, import_path6.normalize)(filePath);
|
|
4197
|
+
const fullPath = (0, import_path6.join)(cwd, normalized);
|
|
4198
|
+
if ((0, import_fs6.existsSync)(fullPath)) {
|
|
2360
4199
|
try {
|
|
2361
|
-
const stat = (0,
|
|
4200
|
+
const stat = (0, import_fs6.lstatSync)(fullPath);
|
|
2362
4201
|
if (stat.isSymbolicLink()) {
|
|
2363
4202
|
const result = {
|
|
2364
4203
|
success: false,
|
|
@@ -2383,11 +4222,38 @@ function handleSafeWrite(input) {
|
|
|
2383
4222
|
};
|
|
2384
4223
|
return { text: JSON.stringify(result), isError: true };
|
|
2385
4224
|
}
|
|
4225
|
+
const pageSlug = extractPageSlug(normalized);
|
|
4226
|
+
if (pageSlug) {
|
|
4227
|
+
const ownership = checkPageOwnership(cwd, pageSlug, callerOtter);
|
|
4228
|
+
if (!ownership.allowed) {
|
|
4229
|
+
const result = {
|
|
4230
|
+
success: false,
|
|
4231
|
+
error: `Page slug "${pageSlug}" is already claimed by ${ownership.owner} (content types: ${ownership.contentTypes.join(", ")}). Use a different slug or coordinate with the owning otter.`,
|
|
4232
|
+
callerOtter,
|
|
4233
|
+
attemptedPath: filePath,
|
|
4234
|
+
allowedPaths: []
|
|
4235
|
+
};
|
|
4236
|
+
return { text: JSON.stringify(result), isError: true };
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
2386
4239
|
try {
|
|
2387
4240
|
if (createDirectories) {
|
|
2388
|
-
(0,
|
|
4241
|
+
(0, import_fs6.mkdirSync)((0, import_path6.dirname)(fullPath), { recursive: true });
|
|
4242
|
+
}
|
|
4243
|
+
(0, import_fs6.writeFileSync)(fullPath, content, { encoding: "utf-8" });
|
|
4244
|
+
if (pageSlug) {
|
|
4245
|
+
try {
|
|
4246
|
+
const registry = readPageRegistry(cwd);
|
|
4247
|
+
registry[pageSlug] = {
|
|
4248
|
+
slug: pageSlug,
|
|
4249
|
+
claimedBy: callerOtter,
|
|
4250
|
+
contentTypes: extractContentTypes(content),
|
|
4251
|
+
writtenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4252
|
+
};
|
|
4253
|
+
writePageRegistry(cwd, registry);
|
|
4254
|
+
} catch {
|
|
4255
|
+
}
|
|
2389
4256
|
}
|
|
2390
|
-
(0, import_fs5.writeFileSync)(fullPath, content, { encoding: "utf-8" });
|
|
2391
4257
|
const result = {
|
|
2392
4258
|
success: true,
|
|
2393
4259
|
path: normalized,
|
|
@@ -2413,10 +4279,12 @@ function registerSafeWriteTools(server2) {
|
|
|
2413
4279
|
"stackwright_pro_safe_write",
|
|
2414
4280
|
DESC,
|
|
2415
4281
|
{
|
|
2416
|
-
callerOtter:
|
|
2417
|
-
filePath:
|
|
2418
|
-
content:
|
|
2419
|
-
createDirectories:
|
|
4282
|
+
callerOtter: import_zod14.z.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
|
|
4283
|
+
filePath: import_zod14.z.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
|
|
4284
|
+
content: import_zod14.z.string().describe("File content to write"),
|
|
4285
|
+
createDirectories: boolCoerce(import_zod14.z.boolean().optional().default(true)).describe(
|
|
4286
|
+
"Create parent directories if they don't exist. Default: true"
|
|
4287
|
+
)
|
|
2420
4288
|
},
|
|
2421
4289
|
async ({ callerOtter, filePath, content, createDirectories }) => {
|
|
2422
4290
|
const result = handleSafeWrite({
|
|
@@ -2431,9 +4299,9 @@ function registerSafeWriteTools(server2) {
|
|
|
2431
4299
|
}
|
|
2432
4300
|
|
|
2433
4301
|
// src/tools/auth.ts
|
|
2434
|
-
var
|
|
2435
|
-
var
|
|
2436
|
-
var
|
|
4302
|
+
var import_zod15 = require("zod");
|
|
4303
|
+
var import_fs7 = require("fs");
|
|
4304
|
+
var import_path7 = require("path");
|
|
2437
4305
|
function buildHierarchy(roles) {
|
|
2438
4306
|
const h = {};
|
|
2439
4307
|
for (let i = 0; i < roles.length - 1; i++) {
|
|
@@ -2453,6 +4321,19 @@ function routesToYaml(routes, defaultRole, indent) {
|
|
|
2453
4321
|
return routes.map((r) => `${indent}- pattern: ${r}
|
|
2454
4322
|
${indent} requiredRole: ${defaultRole}`).join("\n");
|
|
2455
4323
|
}
|
|
4324
|
+
function detectNextMajorVersion(cwd) {
|
|
4325
|
+
try {
|
|
4326
|
+
const pkgPath = (0, import_path7.join)(cwd, "package.json");
|
|
4327
|
+
if (!(0, import_fs7.existsSync)(pkgPath)) return null;
|
|
4328
|
+
const pkg = JSON.parse((0, import_fs7.readFileSync)(pkgPath, "utf8"));
|
|
4329
|
+
const nextVersion = pkg.dependencies?.next ?? pkg.devDependencies?.next;
|
|
4330
|
+
if (!nextVersion) return null;
|
|
4331
|
+
const match = nextVersion.match(/(\d+)/);
|
|
4332
|
+
return match ? parseInt(match[1], 10) : null;
|
|
4333
|
+
} catch {
|
|
4334
|
+
return null;
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
2456
4337
|
function upsertAuthBlock(existing, authYaml) {
|
|
2457
4338
|
const lines = existing.split("\n");
|
|
2458
4339
|
let authStart = -1;
|
|
@@ -2468,11 +4349,96 @@ function upsertAuthBlock(existing, authYaml) {
|
|
|
2468
4349
|
if (authStart < 0) {
|
|
2469
4350
|
return existing.trimEnd() + "\n" + authYaml + "\n";
|
|
2470
4351
|
}
|
|
2471
|
-
const before = lines.slice(0, authStart);
|
|
2472
|
-
const after = lines.slice(authEnd);
|
|
2473
|
-
return [...before, ...authYaml.trimEnd().split("\n"), ...after.length ? after : []].join("\n");
|
|
4352
|
+
const before = lines.slice(0, authStart);
|
|
4353
|
+
const after = lines.slice(authEnd);
|
|
4354
|
+
return [...before, ...authYaml.trimEnd().split("\n"), ...after.length ? after : []].join("\n");
|
|
4355
|
+
}
|
|
4356
|
+
function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
|
|
4357
|
+
const rbacBlock = ` rbac: {
|
|
4358
|
+
roles: ${JSON.stringify(roles)},
|
|
4359
|
+
defaultRole: '${defaultRole}',
|
|
4360
|
+
hierarchy: ${JSON.stringify(hierarchy, null, 4)},
|
|
4361
|
+
},`;
|
|
4362
|
+
const auditBlock = ` audit: {
|
|
4363
|
+
enabled: ${auditEnabled},
|
|
4364
|
+
retentionDays: ${auditRetentionDays},
|
|
4365
|
+
},`;
|
|
4366
|
+
const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
|
|
4367
|
+
const configBlock = `export const config = {
|
|
4368
|
+
matcher: ${JSON.stringify(protectedRoutes)},
|
|
4369
|
+
};`;
|
|
4370
|
+
if (method === "cac") {
|
|
4371
|
+
const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
|
|
4372
|
+
const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
|
|
4373
|
+
const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
|
|
4374
|
+
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
4375
|
+
return `// middleware.ts \u2014 generated by @stackwright-pro/auth
|
|
4376
|
+
// \u26A0\uFE0F SECURITY REVIEW REQUIRED \u2014 CAC/PKI certificate validation
|
|
4377
|
+
// DoD security officer review required before production deployment.
|
|
4378
|
+
// Verify: CA bundle completeness, EDIPI lookup endpoint, OCSP accessibility.
|
|
4379
|
+
import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';
|
|
4380
|
+
|
|
4381
|
+
export const middleware = createProMiddleware({
|
|
4382
|
+
method: 'cac',
|
|
4383
|
+
cac: {
|
|
4384
|
+
caBundle: process.env.CAC_CA_BUNDLE ?? '${caBundle}',
|
|
4385
|
+
edipiLookup: '${edipiLookup}',
|
|
4386
|
+
ocspEndpoint: process.env.CAC_OCSP_ENDPOINT ?? '${ocspEndpoint}',
|
|
4387
|
+
certHeader: '${certHeader}',
|
|
4388
|
+
},
|
|
4389
|
+
${rbacBlock}
|
|
4390
|
+
${auditBlock}
|
|
4391
|
+
${routesBlock}
|
|
4392
|
+
});
|
|
4393
|
+
|
|
4394
|
+
${configBlock}
|
|
4395
|
+
`;
|
|
4396
|
+
}
|
|
4397
|
+
if (method === "oidc") {
|
|
4398
|
+
const scopes2 = params.oidcScopes ?? "openid profile email";
|
|
4399
|
+
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
4400
|
+
return `// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs
|
|
4401
|
+
import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';
|
|
4402
|
+
|
|
4403
|
+
export const middleware = createProMiddleware({
|
|
4404
|
+
method: 'oidc',
|
|
4405
|
+
oidc: {
|
|
4406
|
+
discoveryUrl: process.env.OIDC_DISCOVERY_URL!,
|
|
4407
|
+
clientId: process.env.OIDC_CLIENT_ID!,
|
|
4408
|
+
clientSecret: process.env.OIDC_CLIENT_SECRET!,
|
|
4409
|
+
scopes: '${scopes2}',
|
|
4410
|
+
roleClaim: '${roleClaim}',
|
|
4411
|
+
},
|
|
4412
|
+
${rbacBlock}
|
|
4413
|
+
${auditBlock}
|
|
4414
|
+
${routesBlock}
|
|
4415
|
+
});
|
|
4416
|
+
|
|
4417
|
+
${configBlock}
|
|
4418
|
+
`;
|
|
4419
|
+
}
|
|
4420
|
+
const scopes = params.oauth2Scopes ?? "read write";
|
|
4421
|
+
return `// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs
|
|
4422
|
+
import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';
|
|
4423
|
+
|
|
4424
|
+
export const middleware = createProMiddleware({
|
|
4425
|
+
method: 'oauth2',
|
|
4426
|
+
oauth2: {
|
|
4427
|
+
authorizationUrl: process.env.OAUTH2_AUTH_URL!,
|
|
4428
|
+
tokenUrl: process.env.OAUTH2_TOKEN_URL!,
|
|
4429
|
+
clientId: process.env.OAUTH2_CLIENT_ID!,
|
|
4430
|
+
clientSecret: process.env.OAUTH2_CLIENT_SECRET!,
|
|
4431
|
+
scopes: '${scopes}',
|
|
4432
|
+
},
|
|
4433
|
+
${rbacBlock}
|
|
4434
|
+
${auditBlock}
|
|
4435
|
+
${routesBlock}
|
|
4436
|
+
});
|
|
4437
|
+
|
|
4438
|
+
${configBlock}
|
|
4439
|
+
`;
|
|
2474
4440
|
}
|
|
2475
|
-
function
|
|
4441
|
+
function generateProxyContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
|
|
2476
4442
|
const rbacBlock = ` rbac: {
|
|
2477
4443
|
roles: ${JSON.stringify(roles)},
|
|
2478
4444
|
defaultRole: '${defaultRole}',
|
|
@@ -2491,13 +4457,13 @@ function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy
|
|
|
2491
4457
|
const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
|
|
2492
4458
|
const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
|
|
2493
4459
|
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
2494
|
-
return `//
|
|
2495
|
-
//
|
|
4460
|
+
return `// proxy.ts \u2014 generated by @stackwright-pro/auth (Next.js >=16)
|
|
4461
|
+
// SECURITY REVIEW REQUIRED \u2014 CAC/PKI certificate validation
|
|
2496
4462
|
// DoD security officer review required before production deployment.
|
|
2497
4463
|
// Verify: CA bundle completeness, EDIPI lookup endpoint, OCSP accessibility.
|
|
2498
|
-
import {
|
|
4464
|
+
import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';
|
|
2499
4465
|
|
|
2500
|
-
export const
|
|
4466
|
+
export const proxy = createProProxy({
|
|
2501
4467
|
method: 'cac',
|
|
2502
4468
|
cac: {
|
|
2503
4469
|
caBundle: process.env.CAC_CA_BUNDLE ?? '${caBundle}',
|
|
@@ -2516,10 +4482,10 @@ ${configBlock}
|
|
|
2516
4482
|
if (method === "oidc") {
|
|
2517
4483
|
const scopes2 = params.oidcScopes ?? "openid profile email";
|
|
2518
4484
|
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
2519
|
-
return `//
|
|
2520
|
-
import {
|
|
4485
|
+
return `// proxy.ts \u2014 generated by @stackwright-pro/auth (Next.js >=16)
|
|
4486
|
+
import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';
|
|
2521
4487
|
|
|
2522
|
-
export const
|
|
4488
|
+
export const proxy = createProProxy({
|
|
2523
4489
|
method: 'oidc',
|
|
2524
4490
|
oidc: {
|
|
2525
4491
|
discoveryUrl: process.env.OIDC_DISCOVERY_URL!,
|
|
@@ -2537,10 +4503,10 @@ ${configBlock}
|
|
|
2537
4503
|
`;
|
|
2538
4504
|
}
|
|
2539
4505
|
const scopes = params.oauth2Scopes ?? "read write";
|
|
2540
|
-
return `//
|
|
2541
|
-
import {
|
|
4506
|
+
return `// proxy.ts \u2014 generated by @stackwright-pro/auth (Next.js >=16)
|
|
4507
|
+
import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';
|
|
2542
4508
|
|
|
2543
|
-
export const
|
|
4509
|
+
export const proxy = createProProxy({
|
|
2544
4510
|
method: 'oauth2',
|
|
2545
4511
|
oauth2: {
|
|
2546
4512
|
authorizationUrl: process.env.OAUTH2_AUTH_URL!,
|
|
@@ -2583,7 +4549,7 @@ OAUTH2_CLIENT_ID=your-client-id
|
|
|
2583
4549
|
OAUTH2_CLIENT_SECRET=your-client-secret
|
|
2584
4550
|
`;
|
|
2585
4551
|
}
|
|
2586
|
-
function generateYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
|
|
4552
|
+
function generateYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
|
|
2587
4553
|
const rbacSection = ` rbac:
|
|
2588
4554
|
roles:
|
|
2589
4555
|
${rolesToYaml(roles, " ")}
|
|
@@ -2606,7 +4572,7 @@ ${routesToYaml(protectedRoutes, " ", defaultRole)}`.replace(/\n\s+,/g, "");
|
|
|
2606
4572
|
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
2607
4573
|
return `auth:
|
|
2608
4574
|
method: cac
|
|
2609
|
-
${providerLine} middleware:
|
|
4575
|
+
${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
|
|
2610
4576
|
cac:
|
|
2611
4577
|
caBundle: \${CAC_CA_BUNDLE}
|
|
2612
4578
|
edipiLookup: ${edipiLookup}
|
|
@@ -2623,7 +4589,7 @@ ${auditSection}
|
|
|
2623
4589
|
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
2624
4590
|
return `auth:
|
|
2625
4591
|
method: oidc
|
|
2626
|
-
${providerLine} middleware:
|
|
4592
|
+
${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
|
|
2627
4593
|
oidc:
|
|
2628
4594
|
discoveryUrl: \${OIDC_DISCOVERY_URL}
|
|
2629
4595
|
clientId: \${OIDC_CLIENT_ID}
|
|
@@ -2639,7 +4605,7 @@ ${auditSection}
|
|
|
2639
4605
|
const scopes = params.oauth2Scopes ?? "read write";
|
|
2640
4606
|
return `auth:
|
|
2641
4607
|
method: oauth2
|
|
2642
|
-
${providerLine} middleware:
|
|
4608
|
+
${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
|
|
2643
4609
|
oauth2:
|
|
2644
4610
|
authorizationUrl: \${OAUTH2_AUTH_URL}
|
|
2645
4611
|
tokenUrl: \${OAUTH2_TOKEN_URL}
|
|
@@ -2652,10 +4618,321 @@ ${routeLines}
|
|
|
2652
4618
|
${auditSection}
|
|
2653
4619
|
`;
|
|
2654
4620
|
}
|
|
4621
|
+
function generateDevOnlyMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
|
|
4622
|
+
const rbacBlock = ` rbac: {
|
|
4623
|
+
roles: ${JSON.stringify(roles)},
|
|
4624
|
+
defaultRole: '${defaultRole}',
|
|
4625
|
+
hierarchy: ${JSON.stringify(hierarchy, null, 4)},
|
|
4626
|
+
},`;
|
|
4627
|
+
const auditBlock = ` audit: {
|
|
4628
|
+
enabled: ${auditEnabled},
|
|
4629
|
+
retentionDays: ${auditRetentionDays},
|
|
4630
|
+
},`;
|
|
4631
|
+
const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
|
|
4632
|
+
const configBlock = `export const config = {
|
|
4633
|
+
matcher: ${JSON.stringify(protectedRoutes)},
|
|
4634
|
+
};`;
|
|
4635
|
+
const devHeader = [
|
|
4636
|
+
"// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs (dev-only mock)",
|
|
4637
|
+
"// DEV ONLY \u2014 uses mock authentication from lib/mock-auth.ts",
|
|
4638
|
+
"// Do NOT deploy to production.",
|
|
4639
|
+
"import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';",
|
|
4640
|
+
"import { mockAuthProvider } from './lib/mock-auth';"
|
|
4641
|
+
].join("\n");
|
|
4642
|
+
if (method === "cac") {
|
|
4643
|
+
const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
|
|
4644
|
+
const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
|
|
4645
|
+
const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
|
|
4646
|
+
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
4647
|
+
return `${devHeader}
|
|
4648
|
+
|
|
4649
|
+
export const middleware = createProMiddleware({
|
|
4650
|
+
method: 'cac',
|
|
4651
|
+
cac: {
|
|
4652
|
+
caBundle: '${caBundle}',
|
|
4653
|
+
edipiLookup: '${edipiLookup}',
|
|
4654
|
+
ocspEndpoint: '${ocspEndpoint}',
|
|
4655
|
+
certHeader: '${certHeader}',
|
|
4656
|
+
provider: mockAuthProvider,
|
|
4657
|
+
},
|
|
4658
|
+
${rbacBlock}
|
|
4659
|
+
${auditBlock}
|
|
4660
|
+
${routesBlock}
|
|
4661
|
+
});
|
|
4662
|
+
|
|
4663
|
+
${configBlock}
|
|
4664
|
+
`;
|
|
4665
|
+
}
|
|
4666
|
+
if (method === "oidc") {
|
|
4667
|
+
const scopes2 = params.oidcScopes ?? "openid profile email";
|
|
4668
|
+
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
4669
|
+
return `${devHeader}
|
|
4670
|
+
|
|
4671
|
+
export const middleware = createProMiddleware({
|
|
4672
|
+
method: 'oidc',
|
|
4673
|
+
oidc: {
|
|
4674
|
+
discoveryUrl: 'https://dev-mock-oidc/.well-known/openid-configuration',
|
|
4675
|
+
clientId: 'dev-mock-client',
|
|
4676
|
+
clientSecret: 'dev-mock-secret',
|
|
4677
|
+
scopes: '${scopes2}',
|
|
4678
|
+
roleClaim: '${roleClaim}',
|
|
4679
|
+
provider: mockAuthProvider,
|
|
4680
|
+
},
|
|
4681
|
+
${rbacBlock}
|
|
4682
|
+
${auditBlock}
|
|
4683
|
+
${routesBlock}
|
|
4684
|
+
});
|
|
4685
|
+
|
|
4686
|
+
${configBlock}
|
|
4687
|
+
`;
|
|
4688
|
+
}
|
|
4689
|
+
const scopes = params.oauth2Scopes ?? "read write";
|
|
4690
|
+
return `${devHeader}
|
|
4691
|
+
|
|
4692
|
+
export const middleware = createProMiddleware({
|
|
4693
|
+
method: 'oauth2',
|
|
4694
|
+
oauth2: {
|
|
4695
|
+
authorizationUrl: 'https://dev-mock-oauth2/authorize',
|
|
4696
|
+
tokenUrl: 'https://dev-mock-oauth2/token',
|
|
4697
|
+
clientId: 'dev-mock-client',
|
|
4698
|
+
clientSecret: 'dev-mock-secret',
|
|
4699
|
+
scopes: '${scopes}',
|
|
4700
|
+
provider: mockAuthProvider,
|
|
4701
|
+
},
|
|
4702
|
+
${rbacBlock}
|
|
4703
|
+
${auditBlock}
|
|
4704
|
+
${routesBlock}
|
|
4705
|
+
});
|
|
4706
|
+
|
|
4707
|
+
${configBlock}
|
|
4708
|
+
`;
|
|
4709
|
+
}
|
|
4710
|
+
function generateDevOnlyProxyContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
|
|
4711
|
+
const rbacBlock = ` rbac: {
|
|
4712
|
+
roles: ${JSON.stringify(roles)},
|
|
4713
|
+
defaultRole: '${defaultRole}',
|
|
4714
|
+
hierarchy: ${JSON.stringify(hierarchy, null, 4)},
|
|
4715
|
+
},`;
|
|
4716
|
+
const auditBlock = ` audit: {
|
|
4717
|
+
enabled: ${auditEnabled},
|
|
4718
|
+
retentionDays: ${auditRetentionDays},
|
|
4719
|
+
},`;
|
|
4720
|
+
const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
|
|
4721
|
+
const configBlock = `export const config = {
|
|
4722
|
+
matcher: ${JSON.stringify(protectedRoutes)},
|
|
4723
|
+
};`;
|
|
4724
|
+
const devHeader = [
|
|
4725
|
+
"// proxy.ts -- generated by @stackwright-pro/auth (Next.js >=16, dev-only mock)",
|
|
4726
|
+
"// DEV ONLY -- uses mock authentication from lib/mock-auth.ts",
|
|
4727
|
+
"// Do NOT deploy to production.",
|
|
4728
|
+
"import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';",
|
|
4729
|
+
"import { mockAuthProvider } from './lib/mock-auth';"
|
|
4730
|
+
].join("\n");
|
|
4731
|
+
if (method === "cac") {
|
|
4732
|
+
const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
|
|
4733
|
+
const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
|
|
4734
|
+
const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
|
|
4735
|
+
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
4736
|
+
return `${devHeader}
|
|
4737
|
+
|
|
4738
|
+
export const proxy = createProProxy({
|
|
4739
|
+
method: 'cac',
|
|
4740
|
+
cac: {
|
|
4741
|
+
caBundle: '${caBundle}',
|
|
4742
|
+
edipiLookup: '${edipiLookup}',
|
|
4743
|
+
ocspEndpoint: '${ocspEndpoint}',
|
|
4744
|
+
certHeader: '${certHeader}',
|
|
4745
|
+
provider: mockAuthProvider,
|
|
4746
|
+
},
|
|
4747
|
+
${rbacBlock}
|
|
4748
|
+
${auditBlock}
|
|
4749
|
+
${routesBlock}
|
|
4750
|
+
});
|
|
4751
|
+
|
|
4752
|
+
${configBlock}
|
|
4753
|
+
`;
|
|
4754
|
+
}
|
|
4755
|
+
if (method === "oidc") {
|
|
4756
|
+
const scopes2 = params.oidcScopes ?? "openid profile email";
|
|
4757
|
+
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
4758
|
+
return `${devHeader}
|
|
4759
|
+
|
|
4760
|
+
export const proxy = createProProxy({
|
|
4761
|
+
method: 'oidc',
|
|
4762
|
+
oidc: {
|
|
4763
|
+
discoveryUrl: 'https://dev-mock-oidc/.well-known/openid-configuration',
|
|
4764
|
+
clientId: 'dev-mock-client',
|
|
4765
|
+
clientSecret: 'dev-mock-secret',
|
|
4766
|
+
scopes: '${scopes2}',
|
|
4767
|
+
roleClaim: '${roleClaim}',
|
|
4768
|
+
provider: mockAuthProvider,
|
|
4769
|
+
},
|
|
4770
|
+
${rbacBlock}
|
|
4771
|
+
${auditBlock}
|
|
4772
|
+
${routesBlock}
|
|
4773
|
+
});
|
|
4774
|
+
|
|
4775
|
+
${configBlock}
|
|
4776
|
+
`;
|
|
4777
|
+
}
|
|
4778
|
+
const scopes = params.oauth2Scopes ?? "read write";
|
|
4779
|
+
return `${devHeader}
|
|
4780
|
+
|
|
4781
|
+
export const proxy = createProProxy({
|
|
4782
|
+
method: 'oauth2',
|
|
4783
|
+
oauth2: {
|
|
4784
|
+
authorizationUrl: 'https://dev-mock-oauth2/authorize',
|
|
4785
|
+
tokenUrl: 'https://dev-mock-oauth2/token',
|
|
4786
|
+
clientId: 'dev-mock-client',
|
|
4787
|
+
clientSecret: 'dev-mock-secret',
|
|
4788
|
+
scopes: '${scopes}',
|
|
4789
|
+
provider: mockAuthProvider,
|
|
4790
|
+
},
|
|
4791
|
+
${rbacBlock}
|
|
4792
|
+
${auditBlock}
|
|
4793
|
+
${routesBlock}
|
|
4794
|
+
});
|
|
4795
|
+
|
|
4796
|
+
${configBlock}
|
|
4797
|
+
`;
|
|
4798
|
+
}
|
|
4799
|
+
function generateDevOnlyYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
|
|
4800
|
+
const rbacSection = ` rbac:
|
|
4801
|
+
roles:
|
|
4802
|
+
${rolesToYaml(roles, " ")}
|
|
4803
|
+
defaultRole: ${defaultRole}
|
|
4804
|
+
hierarchy:
|
|
4805
|
+
${hierarchyToYaml(hierarchy, " ")}`;
|
|
4806
|
+
const auditSection = ` audit:
|
|
4807
|
+
enabled: ${auditEnabled}
|
|
4808
|
+
retentionDays: ${auditRetentionDays}`;
|
|
4809
|
+
const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
|
|
4810
|
+
requiredRole: ${defaultRole}`).join("\n");
|
|
4811
|
+
const providerLine = params.provider ? ` provider: ${params.provider}
|
|
4812
|
+
` : "";
|
|
4813
|
+
if (method === "cac") {
|
|
4814
|
+
const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
|
|
4815
|
+
const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
|
|
4816
|
+
const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
|
|
4817
|
+
const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
|
|
4818
|
+
return `auth:
|
|
4819
|
+
method: cac
|
|
4820
|
+
devOnly: true
|
|
4821
|
+
${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
|
|
4822
|
+
cac:
|
|
4823
|
+
caBundle: ${caBundle}
|
|
4824
|
+
edipiLookup: ${edipiLookup}
|
|
4825
|
+
ocspEndpoint: ${ocspEndpoint}
|
|
4826
|
+
certHeader: ${certHeader}
|
|
4827
|
+
${rbacSection}
|
|
4828
|
+
protectedRoutes:
|
|
4829
|
+
${routeLines}
|
|
4830
|
+
${auditSection}
|
|
4831
|
+
`;
|
|
4832
|
+
}
|
|
4833
|
+
if (method === "oidc") {
|
|
4834
|
+
const scopes2 = params.oidcScopes ?? "openid profile email";
|
|
4835
|
+
const roleClaim = params.oidcRoleClaim ?? "roles";
|
|
4836
|
+
return `auth:
|
|
4837
|
+
method: oidc
|
|
4838
|
+
devOnly: true
|
|
4839
|
+
${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
|
|
4840
|
+
oidc:
|
|
4841
|
+
discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
|
|
4842
|
+
clientId: dev-mock-client
|
|
4843
|
+
clientSecret: dev-mock-secret
|
|
4844
|
+
scopes: ${scopes2}
|
|
4845
|
+
roleClaim: ${roleClaim}
|
|
4846
|
+
${rbacSection}
|
|
4847
|
+
protectedRoutes:
|
|
4848
|
+
${routeLines}
|
|
4849
|
+
${auditSection}
|
|
4850
|
+
`;
|
|
4851
|
+
}
|
|
4852
|
+
const scopes = params.oauth2Scopes ?? "read write";
|
|
4853
|
+
return `auth:
|
|
4854
|
+
method: oauth2
|
|
4855
|
+
devOnly: true
|
|
4856
|
+
${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
|
|
4857
|
+
oauth2:
|
|
4858
|
+
authorizationUrl: https://dev-mock-oauth2/authorize
|
|
4859
|
+
tokenUrl: https://dev-mock-oauth2/token
|
|
4860
|
+
clientId: dev-mock-client
|
|
4861
|
+
clientSecret: dev-mock-secret
|
|
4862
|
+
scopes: ${scopes}
|
|
4863
|
+
${rbacSection}
|
|
4864
|
+
protectedRoutes:
|
|
4865
|
+
${routeLines}
|
|
4866
|
+
${auditSection}
|
|
4867
|
+
`;
|
|
4868
|
+
}
|
|
4869
|
+
function deriveDevKey(roleName) {
|
|
4870
|
+
const segment = roleName.split("_")[0];
|
|
4871
|
+
return (segment ?? roleName).toLowerCase();
|
|
4872
|
+
}
|
|
4873
|
+
function generateMockAuthContent(roles, mockUsers) {
|
|
4874
|
+
const entries = [];
|
|
4875
|
+
const scriptLines = [];
|
|
4876
|
+
for (let i = 0; i < roles.length; i++) {
|
|
4877
|
+
const role = roles[i];
|
|
4878
|
+
const devKey = deriveDevKey(role);
|
|
4879
|
+
const persona = mockUsers?.[i];
|
|
4880
|
+
const name = persona?.name ?? `Dev ${role}`;
|
|
4881
|
+
const email = persona?.email ?? `dev-${devKey}@example.mil`;
|
|
4882
|
+
const edipi = String(i + 1).padStart(10, "0");
|
|
4883
|
+
entries.push(` ${devKey}: {
|
|
4884
|
+
id: 'mock-${devKey}-${String(i + 1).padStart(3, "0")}',
|
|
4885
|
+
name: '${name}',
|
|
4886
|
+
email: '${email}',
|
|
4887
|
+
roles: ['${role}'],
|
|
4888
|
+
edipi: '${edipi}',
|
|
4889
|
+
}`);
|
|
4890
|
+
scriptLines.push(` * pnpm dev:${devKey} \u2014 ${name}`);
|
|
4891
|
+
}
|
|
4892
|
+
return `/**
|
|
4893
|
+
* Mock authentication for development mode.
|
|
4894
|
+
*
|
|
4895
|
+
* DEV_ONLY_MODE \u2014 no real auth provider. Select a persona via MOCK_USER env var
|
|
4896
|
+
* or use the convenience scripts in package.json:
|
|
4897
|
+
${scriptLines.join("\n")}
|
|
4898
|
+
*/
|
|
4899
|
+
|
|
4900
|
+
export const MOCK_USERS = {
|
|
4901
|
+
${entries.join(",\n")}
|
|
4902
|
+
} as const;
|
|
4903
|
+
|
|
4904
|
+
export type MockRole = keyof typeof MOCK_USERS;
|
|
4905
|
+
|
|
4906
|
+
export function getMockUser(role?: string) {
|
|
4907
|
+
const key = (role ?? process.env.MOCK_USER) as MockRole | undefined;
|
|
4908
|
+
if (!key) return null;
|
|
4909
|
+
return MOCK_USERS[key] ?? null;
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
export function mockAuthProvider() {
|
|
4913
|
+
return getMockUser();
|
|
4914
|
+
}
|
|
4915
|
+
`;
|
|
4916
|
+
}
|
|
4917
|
+
function updatePackageJsonScripts(existingJson, roles, mockUsers) {
|
|
4918
|
+
const pkg = JSON.parse(existingJson);
|
|
4919
|
+
const scripts = pkg.scripts ?? {};
|
|
4920
|
+
delete scripts["dev:admin"];
|
|
4921
|
+
delete scripts["dev:analyst"];
|
|
4922
|
+
delete scripts["dev:viewer"];
|
|
4923
|
+
for (let i = 0; i < roles.length; i++) {
|
|
4924
|
+
const devKey = deriveDevKey(roles[i]);
|
|
4925
|
+
scripts[`dev:${devKey}`] = `MOCK_USER=${devKey} next dev`;
|
|
4926
|
+
}
|
|
4927
|
+
pkg.scripts = scripts;
|
|
4928
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
4929
|
+
}
|
|
2655
4930
|
async function configureAuthHandler(params, cwd) {
|
|
2656
4931
|
const {
|
|
2657
4932
|
method,
|
|
2658
4933
|
provider,
|
|
4934
|
+
devOnly = false,
|
|
4935
|
+
mockUsers,
|
|
2659
4936
|
rbacRoles = ["SUPER_ADMIN", "ADMIN", "ANALYST"],
|
|
2660
4937
|
auditEnabled = true,
|
|
2661
4938
|
auditRetentionDays = 90,
|
|
@@ -2664,7 +4941,10 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2664
4941
|
const roles = rbacRoles;
|
|
2665
4942
|
const defaultRole = params.rbacDefaultRole ?? roles[roles.length - 1];
|
|
2666
4943
|
const hierarchy = buildHierarchy(roles);
|
|
2667
|
-
|
|
4944
|
+
const detectedVersion = params.nextMajorVersion ?? detectNextMajorVersion(cwd);
|
|
4945
|
+
const useProxy = (detectedVersion ?? 0) >= 16;
|
|
4946
|
+
const conventionFile = useProxy ? "proxy.ts" : "middleware.ts";
|
|
4947
|
+
if (method === "none" && !devOnly) {
|
|
2668
4948
|
return {
|
|
2669
4949
|
content: [
|
|
2670
4950
|
{
|
|
@@ -2677,16 +4957,46 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2677
4957
|
rbacDefaultRole: defaultRole,
|
|
2678
4958
|
protectedRoutesCount: protectedRoutes.length,
|
|
2679
4959
|
filesWritten: [],
|
|
2680
|
-
securityWarning: null
|
|
4960
|
+
securityWarning: null,
|
|
4961
|
+
autoUpgradeWarning: null
|
|
2681
4962
|
})
|
|
2682
4963
|
}
|
|
2683
4964
|
]
|
|
2684
4965
|
};
|
|
2685
4966
|
}
|
|
4967
|
+
const effectiveMethod = method === "none" ? "oidc" : method;
|
|
4968
|
+
const autoUpgradeWarning = method === "none" ? "method: 'none' + devOnly: true was automatically upgraded to method: 'oidc' + devOnly: true. In DEV_ONLY_MODE, pass method: 'oidc' + devOnly: true directly (swp-5tmy)." : null;
|
|
2686
4969
|
const filesWritten = [];
|
|
2687
4970
|
try {
|
|
2688
|
-
const
|
|
2689
|
-
|
|
4971
|
+
const authFileContent = devOnly ? useProxy ? generateDevOnlyProxyContent(
|
|
4972
|
+
effectiveMethod,
|
|
4973
|
+
params,
|
|
4974
|
+
roles,
|
|
4975
|
+
defaultRole,
|
|
4976
|
+
hierarchy,
|
|
4977
|
+
auditEnabled,
|
|
4978
|
+
auditRetentionDays,
|
|
4979
|
+
protectedRoutes
|
|
4980
|
+
) : generateDevOnlyMiddlewareContent(
|
|
4981
|
+
effectiveMethod,
|
|
4982
|
+
params,
|
|
4983
|
+
roles,
|
|
4984
|
+
defaultRole,
|
|
4985
|
+
hierarchy,
|
|
4986
|
+
auditEnabled,
|
|
4987
|
+
auditRetentionDays,
|
|
4988
|
+
protectedRoutes
|
|
4989
|
+
) : useProxy ? generateProxyContent(
|
|
4990
|
+
effectiveMethod,
|
|
4991
|
+
params,
|
|
4992
|
+
roles,
|
|
4993
|
+
defaultRole,
|
|
4994
|
+
hierarchy,
|
|
4995
|
+
auditEnabled,
|
|
4996
|
+
auditRetentionDays,
|
|
4997
|
+
protectedRoutes
|
|
4998
|
+
) : generateMiddlewareContent(
|
|
4999
|
+
effectiveMethod,
|
|
2690
5000
|
params,
|
|
2691
5001
|
roles,
|
|
2692
5002
|
defaultRole,
|
|
@@ -2695,59 +5005,121 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2695
5005
|
auditRetentionDays,
|
|
2696
5006
|
protectedRoutes
|
|
2697
5007
|
);
|
|
2698
|
-
(0,
|
|
2699
|
-
filesWritten.push(
|
|
5008
|
+
(0, import_fs7.writeFileSync)((0, import_path7.join)(cwd, conventionFile), authFileContent, "utf8");
|
|
5009
|
+
filesWritten.push(conventionFile);
|
|
2700
5010
|
} catch (err) {
|
|
2701
5011
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2702
5012
|
return {
|
|
2703
5013
|
content: [
|
|
2704
5014
|
{
|
|
2705
5015
|
type: "text",
|
|
2706
|
-
text: JSON.stringify({
|
|
5016
|
+
text: JSON.stringify({
|
|
5017
|
+
success: false,
|
|
5018
|
+
error: `Failed writing ${conventionFile}: ${msg}`
|
|
5019
|
+
})
|
|
2707
5020
|
}
|
|
2708
5021
|
],
|
|
2709
5022
|
isError: true
|
|
2710
5023
|
};
|
|
2711
5024
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
5025
|
+
if (!devOnly) {
|
|
5026
|
+
try {
|
|
5027
|
+
const envBlock = generateEnvBlock(effectiveMethod, params);
|
|
5028
|
+
const envPath = (0, import_path7.join)(cwd, ".env.example");
|
|
5029
|
+
if ((0, import_fs7.existsSync)(envPath)) {
|
|
5030
|
+
const existing = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
5031
|
+
(0, import_fs7.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
5032
|
+
} else {
|
|
5033
|
+
(0, import_fs7.writeFileSync)(envPath, envBlock, "utf8");
|
|
5034
|
+
}
|
|
5035
|
+
filesWritten.push(".env.example");
|
|
5036
|
+
} catch (err) {
|
|
5037
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5038
|
+
return {
|
|
5039
|
+
content: [
|
|
5040
|
+
{
|
|
5041
|
+
type: "text",
|
|
5042
|
+
text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
|
|
5043
|
+
}
|
|
5044
|
+
],
|
|
5045
|
+
isError: true
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
5049
|
+
if (devOnly) {
|
|
5050
|
+
try {
|
|
5051
|
+
const mockAuthDir = (0, import_path7.join)(cwd, "lib");
|
|
5052
|
+
(0, import_fs7.mkdirSync)(mockAuthDir, { recursive: true });
|
|
5053
|
+
const mockAuthContent = generateMockAuthContent(roles, mockUsers);
|
|
5054
|
+
(0, import_fs7.writeFileSync)((0, import_path7.join)(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
|
|
5055
|
+
filesWritten.push("lib/mock-auth.ts");
|
|
5056
|
+
} catch (err) {
|
|
5057
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5058
|
+
return {
|
|
5059
|
+
content: [
|
|
5060
|
+
{
|
|
5061
|
+
type: "text",
|
|
5062
|
+
text: JSON.stringify({
|
|
5063
|
+
success: false,
|
|
5064
|
+
error: `Failed writing lib/mock-auth.ts: ${msg}`
|
|
5065
|
+
})
|
|
5066
|
+
}
|
|
5067
|
+
],
|
|
5068
|
+
isError: true
|
|
5069
|
+
};
|
|
5070
|
+
}
|
|
5071
|
+
try {
|
|
5072
|
+
const pkgPath = (0, import_path7.join)(cwd, "package.json");
|
|
5073
|
+
if ((0, import_fs7.existsSync)(pkgPath)) {
|
|
5074
|
+
const existingPkg = (0, import_fs7.readFileSync)(pkgPath, "utf8");
|
|
5075
|
+
const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
|
|
5076
|
+
(0, import_fs7.writeFileSync)(pkgPath, updatedPkg, "utf8");
|
|
5077
|
+
filesWritten.push("package.json");
|
|
5078
|
+
}
|
|
5079
|
+
} catch (err) {
|
|
5080
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5081
|
+
return {
|
|
5082
|
+
content: [
|
|
5083
|
+
{
|
|
5084
|
+
type: "text",
|
|
5085
|
+
text: JSON.stringify({
|
|
5086
|
+
success: false,
|
|
5087
|
+
error: `Failed updating package.json: ${msg}`
|
|
5088
|
+
})
|
|
5089
|
+
}
|
|
5090
|
+
],
|
|
5091
|
+
isError: true
|
|
5092
|
+
};
|
|
2720
5093
|
}
|
|
2721
|
-
filesWritten.push(".env.example");
|
|
2722
|
-
} catch (err) {
|
|
2723
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2724
|
-
return {
|
|
2725
|
-
content: [
|
|
2726
|
-
{
|
|
2727
|
-
type: "text",
|
|
2728
|
-
text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
|
|
2729
|
-
}
|
|
2730
|
-
],
|
|
2731
|
-
isError: true
|
|
2732
|
-
};
|
|
2733
5094
|
}
|
|
2734
5095
|
try {
|
|
2735
|
-
const authYaml =
|
|
2736
|
-
|
|
5096
|
+
const authYaml = devOnly ? generateDevOnlyYamlBlock(
|
|
5097
|
+
effectiveMethod,
|
|
2737
5098
|
params,
|
|
2738
5099
|
roles,
|
|
2739
5100
|
defaultRole,
|
|
2740
5101
|
hierarchy,
|
|
2741
5102
|
auditEnabled,
|
|
2742
5103
|
auditRetentionDays,
|
|
2743
|
-
protectedRoutes
|
|
5104
|
+
protectedRoutes,
|
|
5105
|
+
useProxy
|
|
5106
|
+
) : generateYamlBlock(
|
|
5107
|
+
effectiveMethod,
|
|
5108
|
+
params,
|
|
5109
|
+
roles,
|
|
5110
|
+
defaultRole,
|
|
5111
|
+
hierarchy,
|
|
5112
|
+
auditEnabled,
|
|
5113
|
+
auditRetentionDays,
|
|
5114
|
+
protectedRoutes,
|
|
5115
|
+
useProxy
|
|
2744
5116
|
);
|
|
2745
|
-
const ymlPath = (0,
|
|
2746
|
-
if (!(0,
|
|
2747
|
-
(0,
|
|
5117
|
+
const ymlPath = (0, import_path7.join)(cwd, "stackwright.yml");
|
|
5118
|
+
if (!(0, import_fs7.existsSync)(ymlPath)) {
|
|
5119
|
+
(0, import_fs7.writeFileSync)(ymlPath, authYaml, "utf8");
|
|
2748
5120
|
} else {
|
|
2749
|
-
const existing = (0,
|
|
2750
|
-
(0,
|
|
5121
|
+
const existing = (0, import_fs7.readFileSync)(ymlPath, "utf8");
|
|
5122
|
+
(0, import_fs7.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
|
|
2751
5123
|
}
|
|
2752
5124
|
filesWritten.push("stackwright.yml");
|
|
2753
5125
|
} catch (err) {
|
|
@@ -2762,20 +5134,37 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2762
5134
|
isError: true
|
|
2763
5135
|
};
|
|
2764
5136
|
}
|
|
2765
|
-
const securityWarning =
|
|
5137
|
+
const securityWarning = effectiveMethod === "cac" ? "SECURITY REVIEW REQUIRED \u2014 CAC certificate chain must be verified before production deployment" : null;
|
|
2766
5138
|
return {
|
|
2767
5139
|
content: [
|
|
2768
5140
|
{
|
|
2769
5141
|
type: "text",
|
|
2770
5142
|
text: JSON.stringify({
|
|
2771
5143
|
success: true,
|
|
2772
|
-
method,
|
|
5144
|
+
method: effectiveMethod,
|
|
2773
5145
|
provider: provider ?? null,
|
|
2774
5146
|
rbacRoles: roles,
|
|
2775
5147
|
rbacDefaultRole: defaultRole,
|
|
2776
5148
|
protectedRoutesCount: protectedRoutes.length,
|
|
2777
5149
|
filesWritten,
|
|
2778
|
-
|
|
5150
|
+
convention: useProxy ? "proxy" : "middleware",
|
|
5151
|
+
nextMajorVersion: params.nextMajorVersion ?? null,
|
|
5152
|
+
securityWarning,
|
|
5153
|
+
autoUpgradeWarning,
|
|
5154
|
+
...devOnly ? {
|
|
5155
|
+
devScripts: {
|
|
5156
|
+
written: filesWritten.includes("package.json"),
|
|
5157
|
+
scripts: filesWritten.includes("package.json") ? Object.fromEntries(
|
|
5158
|
+
roles.map((r) => {
|
|
5159
|
+
const k = deriveDevKey(r);
|
|
5160
|
+
return [`dev:${k}`, `MOCK_USER=${k} next dev`];
|
|
5161
|
+
})
|
|
5162
|
+
) : {},
|
|
5163
|
+
...filesWritten.includes("package.json") ? {} : {
|
|
5164
|
+
warning: "package.json not found \u2014 dev scripts were not written. Run the auth phase again after scaffolding creates package.json."
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
} : {}
|
|
2779
5168
|
})
|
|
2780
5169
|
}
|
|
2781
5170
|
]
|
|
@@ -2786,35 +5175,38 @@ function registerAuthTools(server2) {
|
|
|
2786
5175
|
"stackwright_pro_configure_auth",
|
|
2787
5176
|
"Generate authentication middleware and configuration for a Next.js Stackwright application. Writes `middleware.ts` from a secure template, appends/updates the `auth:` section in `stackwright.yml`, and generates `.env.example` with required environment variables. \u26A0\uFE0F For CAC/PKI: generated `middleware.ts` carries a SECURITY REVIEW REQUIRED comment \u2014 certificate chain validation must be verified by a DoD security officer before production deployment. This is the ONLY approved path to generating `middleware.ts`. Never write TypeScript auth files directly.",
|
|
2788
5177
|
{
|
|
2789
|
-
method:
|
|
2790
|
-
provider:
|
|
5178
|
+
method: import_zod15.z.enum(["cac", "oidc", "oauth2", "none"]),
|
|
5179
|
+
provider: import_zod15.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
|
|
2791
5180
|
// CAC
|
|
2792
|
-
cacCaBundle:
|
|
2793
|
-
cacEdipiLookup:
|
|
2794
|
-
cacOcspEndpoint:
|
|
2795
|
-
cacCertHeader:
|
|
5181
|
+
cacCaBundle: import_zod15.z.string().optional(),
|
|
5182
|
+
cacEdipiLookup: import_zod15.z.string().optional(),
|
|
5183
|
+
cacOcspEndpoint: import_zod15.z.string().optional(),
|
|
5184
|
+
cacCertHeader: import_zod15.z.string().optional(),
|
|
2796
5185
|
// OIDC
|
|
2797
|
-
oidcDiscoveryUrl:
|
|
2798
|
-
oidcClientId:
|
|
2799
|
-
oidcClientSecret:
|
|
2800
|
-
oidcScopes:
|
|
2801
|
-
oidcRoleClaim:
|
|
5186
|
+
oidcDiscoveryUrl: import_zod15.z.string().optional(),
|
|
5187
|
+
oidcClientId: import_zod15.z.string().optional(),
|
|
5188
|
+
oidcClientSecret: import_zod15.z.string().optional(),
|
|
5189
|
+
oidcScopes: import_zod15.z.string().optional(),
|
|
5190
|
+
oidcRoleClaim: import_zod15.z.string().optional(),
|
|
2802
5191
|
// OAuth2
|
|
2803
|
-
oauth2AuthUrl:
|
|
2804
|
-
oauth2TokenUrl:
|
|
2805
|
-
oauth2ClientId:
|
|
2806
|
-
oauth2ClientSecret:
|
|
2807
|
-
oauth2Scopes:
|
|
5192
|
+
oauth2AuthUrl: import_zod15.z.string().optional(),
|
|
5193
|
+
oauth2TokenUrl: import_zod15.z.string().optional(),
|
|
5194
|
+
oauth2ClientId: import_zod15.z.string().optional(),
|
|
5195
|
+
oauth2ClientSecret: import_zod15.z.string().optional(),
|
|
5196
|
+
oauth2Scopes: import_zod15.z.string().optional(),
|
|
2808
5197
|
// RBAC
|
|
2809
|
-
rbacRoles:
|
|
2810
|
-
rbacDefaultRole:
|
|
5198
|
+
rbacRoles: jsonCoerce(import_zod15.z.array(import_zod15.z.string()).optional()),
|
|
5199
|
+
rbacDefaultRole: import_zod15.z.string().optional(),
|
|
2811
5200
|
// Audit
|
|
2812
|
-
auditEnabled:
|
|
2813
|
-
auditRetentionDays:
|
|
5201
|
+
auditEnabled: boolCoerce(import_zod15.z.boolean().optional()),
|
|
5202
|
+
auditRetentionDays: numCoerce(import_zod15.z.number().int().positive().optional()),
|
|
2814
5203
|
// Routes
|
|
2815
|
-
protectedRoutes:
|
|
5204
|
+
protectedRoutes: jsonCoerce(import_zod15.z.array(import_zod15.z.string()).optional()),
|
|
2816
5205
|
// Injection for tests
|
|
2817
|
-
_cwd:
|
|
5206
|
+
_cwd: import_zod15.z.string().optional(),
|
|
5207
|
+
devOnly: boolCoerce(import_zod15.z.boolean().optional()),
|
|
5208
|
+
mockUsers: jsonCoerce(import_zod15.z.array(import_zod15.z.object({ name: import_zod15.z.string(), email: import_zod15.z.string() })).optional()),
|
|
5209
|
+
nextMajorVersion: numCoerce(import_zod15.z.number().int().positive().optional())
|
|
2818
5210
|
},
|
|
2819
5211
|
async (params) => {
|
|
2820
5212
|
const cwd = params._cwd ?? process.cwd();
|
|
@@ -2824,45 +5216,65 @@ function registerAuthTools(server2) {
|
|
|
2824
5216
|
}
|
|
2825
5217
|
|
|
2826
5218
|
// src/integrity.ts
|
|
2827
|
-
var
|
|
2828
|
-
var
|
|
2829
|
-
var
|
|
5219
|
+
var import_crypto4 = require("crypto");
|
|
5220
|
+
var import_fs8 = require("fs");
|
|
5221
|
+
var import_path8 = require("path");
|
|
2830
5222
|
var _checksums = /* @__PURE__ */ new Map([
|
|
2831
5223
|
[
|
|
2832
5224
|
"stackwright-pro-api-otter.json",
|
|
2833
|
-
"
|
|
5225
|
+
"df79f4389a576c2885efa07b04f613c60eb8ebf4a8b1d4c7da5e4bb6ee4248dd"
|
|
2834
5226
|
],
|
|
2835
5227
|
[
|
|
2836
5228
|
"stackwright-pro-auth-otter.json",
|
|
2837
|
-
"
|
|
5229
|
+
"07134125ab4f8c82ee015f27587e4d84bdeefca5ec211d1563bcb25b8aa61eb3"
|
|
2838
5230
|
],
|
|
2839
5231
|
[
|
|
2840
5232
|
"stackwright-pro-dashboard-otter.json",
|
|
2841
|
-
"
|
|
5233
|
+
"160af221e04200f53709f8c3e249ca6dafb38a325b232fabd478b28f5492ab01"
|
|
2842
5234
|
],
|
|
2843
5235
|
[
|
|
2844
5236
|
"stackwright-pro-data-otter.json",
|
|
2845
|
-
"
|
|
5237
|
+
"709c8e49328908549fe87de181a5991e09c918ef24bbfe6948f9888f0c758c25"
|
|
2846
5238
|
],
|
|
2847
5239
|
[
|
|
2848
5240
|
"stackwright-pro-designer-otter.json",
|
|
2849
|
-
"
|
|
5241
|
+
"1364b2c235c07b0b798e9aab90a68604f60019a5508d41ba576977f173e20cd9"
|
|
5242
|
+
],
|
|
5243
|
+
[
|
|
5244
|
+
"stackwright-pro-domain-expert-otter.json",
|
|
5245
|
+
"1f21b8ff3450bdae29a4d31b31462ba22583995a448af3c11ab0a5071f46bd72"
|
|
2850
5246
|
],
|
|
2851
5247
|
[
|
|
2852
5248
|
"stackwright-pro-foreman-otter.json",
|
|
2853
|
-
"
|
|
5249
|
+
"438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1"
|
|
5250
|
+
],
|
|
5251
|
+
[
|
|
5252
|
+
"stackwright-pro-geo-otter.json",
|
|
5253
|
+
"2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f"
|
|
2854
5254
|
],
|
|
2855
5255
|
[
|
|
2856
5256
|
"stackwright-pro-page-otter.json",
|
|
2857
|
-
"
|
|
5257
|
+
"0057ea97f7c2e33a8673140f59a0237eb627d82c676af3fae4faa31033598e03"
|
|
5258
|
+
],
|
|
5259
|
+
[
|
|
5260
|
+
"stackwright-pro-polish-otter.json",
|
|
5261
|
+
"bd87327b9a9a62fabaee8837492ce943feae8bfc15e5d43b45ba0e84619daec3"
|
|
5262
|
+
],
|
|
5263
|
+
[
|
|
5264
|
+
"stackwright-pro-scaffold-otter.json",
|
|
5265
|
+
"91de5861f1406043d1d387388302fb1492211b81e2eea9777dec60e48f317f2a"
|
|
2858
5266
|
],
|
|
2859
5267
|
[
|
|
2860
5268
|
"stackwright-pro-theme-otter.json",
|
|
2861
|
-
"
|
|
5269
|
+
"fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944"
|
|
2862
5270
|
],
|
|
2863
5271
|
[
|
|
2864
5272
|
"stackwright-pro-workflow-otter.json",
|
|
2865
|
-
"
|
|
5273
|
+
"5f6209fadc1355580e2455a16386f06bd7d5814f047b2dd5b33800abf6a48ff8"
|
|
5274
|
+
],
|
|
5275
|
+
[
|
|
5276
|
+
"stackwright-services-otter.json",
|
|
5277
|
+
"c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261"
|
|
2866
5278
|
]
|
|
2867
5279
|
]);
|
|
2868
5280
|
Object.freeze(_checksums);
|
|
@@ -2877,21 +5289,21 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
|
|
|
2877
5289
|
}
|
|
2878
5290
|
var MAX_OTTER_BYTES = 1 * 1024 * 1024;
|
|
2879
5291
|
function computeSha256(data) {
|
|
2880
|
-
return (0,
|
|
5292
|
+
return (0, import_crypto4.createHash)("sha256").update(data).digest("hex");
|
|
2881
5293
|
}
|
|
2882
5294
|
function safeEqual(a, b) {
|
|
2883
5295
|
if (a.length !== b.length) return false;
|
|
2884
|
-
return (0,
|
|
5296
|
+
return (0, import_crypto4.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
2885
5297
|
}
|
|
2886
5298
|
function verifyOtterFile(filePath) {
|
|
2887
|
-
const filename = (0,
|
|
5299
|
+
const filename = (0, import_path8.basename)(filePath);
|
|
2888
5300
|
const expected = CANONICAL_CHECKSUMS.get(filename);
|
|
2889
5301
|
if (expected === void 0) {
|
|
2890
5302
|
return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
|
|
2891
5303
|
}
|
|
2892
5304
|
let stat;
|
|
2893
5305
|
try {
|
|
2894
|
-
stat = (0,
|
|
5306
|
+
stat = (0, import_fs8.lstatSync)(filePath);
|
|
2895
5307
|
} catch (err) {
|
|
2896
5308
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2897
5309
|
return { verified: false, filename, error: `Cannot stat file: ${msg}` };
|
|
@@ -2909,7 +5321,7 @@ function verifyOtterFile(filePath) {
|
|
|
2909
5321
|
}
|
|
2910
5322
|
let raw;
|
|
2911
5323
|
try {
|
|
2912
|
-
raw = (0,
|
|
5324
|
+
raw = (0, import_fs8.readFileSync)(filePath);
|
|
2913
5325
|
} catch (err) {
|
|
2914
5326
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2915
5327
|
return { verified: false, filename, error: `Cannot read file: ${msg}` };
|
|
@@ -2942,12 +5354,24 @@ function verifyOtterFile(filePath) {
|
|
|
2942
5354
|
return { verified: true, filename };
|
|
2943
5355
|
}
|
|
2944
5356
|
function verifyAllOtters(otterDir) {
|
|
5357
|
+
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
|
|
5358
|
+
return {
|
|
5359
|
+
verified: [],
|
|
5360
|
+
failed: [
|
|
5361
|
+
{
|
|
5362
|
+
filename: "<directory>",
|
|
5363
|
+
error: `Security: path traversal sequence detected in otter directory parameter`
|
|
5364
|
+
}
|
|
5365
|
+
],
|
|
5366
|
+
unknown: []
|
|
5367
|
+
};
|
|
5368
|
+
}
|
|
2945
5369
|
const verified = [];
|
|
2946
5370
|
const failed = [];
|
|
2947
5371
|
const unknown = [];
|
|
2948
5372
|
let entries;
|
|
2949
5373
|
try {
|
|
2950
|
-
entries = (0,
|
|
5374
|
+
entries = (0, import_fs8.readdirSync)(otterDir);
|
|
2951
5375
|
} catch (err) {
|
|
2952
5376
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2953
5377
|
return {
|
|
@@ -2958,9 +5382,9 @@ function verifyAllOtters(otterDir) {
|
|
|
2958
5382
|
}
|
|
2959
5383
|
const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
|
|
2960
5384
|
for (const filename of otterFiles) {
|
|
2961
|
-
const filePath = (0,
|
|
5385
|
+
const filePath = (0, import_path8.join)(otterDir, filename);
|
|
2962
5386
|
try {
|
|
2963
|
-
if ((0,
|
|
5387
|
+
if ((0, import_fs8.lstatSync)(filePath).isSymbolicLink()) {
|
|
2964
5388
|
failed.push({ filename, error: "Skipped: symlink" });
|
|
2965
5389
|
continue;
|
|
2966
5390
|
}
|
|
@@ -2986,15 +5410,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
|
|
|
2986
5410
|
function resolveOtterDir() {
|
|
2987
5411
|
const cwd = process.cwd();
|
|
2988
5412
|
for (const relative of DEFAULT_SEARCH_PATHS) {
|
|
2989
|
-
const candidate = (0,
|
|
5413
|
+
const candidate = (0, import_path8.join)(cwd, relative);
|
|
2990
5414
|
try {
|
|
2991
|
-
(0,
|
|
5415
|
+
(0, import_fs8.lstatSync)(candidate);
|
|
2992
5416
|
return candidate;
|
|
2993
5417
|
} catch {
|
|
2994
5418
|
}
|
|
2995
5419
|
}
|
|
2996
5420
|
return null;
|
|
2997
5421
|
}
|
|
5422
|
+
function emitIntegrityAuditEvent(params) {
|
|
5423
|
+
const record = JSON.stringify({
|
|
5424
|
+
level: "AUDIT",
|
|
5425
|
+
event: "INTEGRITY_FAIL",
|
|
5426
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5427
|
+
source: "stackwright_pro_verify_otter_integrity",
|
|
5428
|
+
otterDir: params.otterDir,
|
|
5429
|
+
failedCount: params.failed.length,
|
|
5430
|
+
unknownCount: params.unknown.length,
|
|
5431
|
+
failures: params.failed,
|
|
5432
|
+
unknown: params.unknown
|
|
5433
|
+
});
|
|
5434
|
+
process.stderr.write(`INTEGRITY_FAIL ${record}
|
|
5435
|
+
`);
|
|
5436
|
+
}
|
|
2998
5437
|
function registerIntegrityTools(server2) {
|
|
2999
5438
|
server2.tool(
|
|
3000
5439
|
"stackwright_pro_verify_otter_integrity",
|
|
@@ -3018,6 +5457,13 @@ function registerIntegrityTools(server2) {
|
|
|
3018
5457
|
}
|
|
3019
5458
|
const result = verifyAllOtters(resolved);
|
|
3020
5459
|
const allGood = result.failed.length === 0 && result.unknown.length === 0;
|
|
5460
|
+
if (!allGood) {
|
|
5461
|
+
emitIntegrityAuditEvent({
|
|
5462
|
+
otterDir: resolved,
|
|
5463
|
+
failed: result.failed,
|
|
5464
|
+
unknown: result.unknown
|
|
5465
|
+
});
|
|
5466
|
+
}
|
|
3021
5467
|
return {
|
|
3022
5468
|
content: [
|
|
3023
5469
|
{
|
|
@@ -3031,25 +5477,27 @@ function registerIntegrityTools(server2) {
|
|
|
3031
5477
|
verified: result.verified,
|
|
3032
5478
|
failed: result.failed,
|
|
3033
5479
|
unknown: result.unknown,
|
|
3034
|
-
|
|
5480
|
+
...allGood ? {} : {
|
|
5481
|
+
error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
|
|
5482
|
+
}
|
|
3035
5483
|
})
|
|
3036
5484
|
}
|
|
3037
5485
|
],
|
|
3038
|
-
isError:
|
|
5486
|
+
isError: !allGood
|
|
3039
5487
|
};
|
|
3040
5488
|
}
|
|
3041
5489
|
);
|
|
3042
5490
|
}
|
|
3043
5491
|
|
|
3044
5492
|
// src/tools/domain.ts
|
|
3045
|
-
var
|
|
3046
|
-
var
|
|
3047
|
-
var
|
|
5493
|
+
var import_zod16 = require("zod");
|
|
5494
|
+
var import_fs9 = require("fs");
|
|
5495
|
+
var import_path9 = require("path");
|
|
3048
5496
|
function handleListCollections(input) {
|
|
3049
5497
|
const cwd = input._cwd ?? process.cwd();
|
|
3050
5498
|
const sources = [
|
|
3051
5499
|
{
|
|
3052
|
-
path: (0,
|
|
5500
|
+
path: (0, import_path9.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
|
|
3053
5501
|
source: "data-config.json",
|
|
3054
5502
|
parse: (raw) => {
|
|
3055
5503
|
const parsed = JSON.parse(raw);
|
|
@@ -3060,15 +5508,15 @@ function handleListCollections(input) {
|
|
|
3060
5508
|
}
|
|
3061
5509
|
},
|
|
3062
5510
|
{
|
|
3063
|
-
path: (0,
|
|
5511
|
+
path: (0, import_path9.join)(cwd, "stackwright.yml"),
|
|
3064
5512
|
source: "stackwright.yml",
|
|
3065
5513
|
parse: extractCollectionsFromYaml
|
|
3066
5514
|
}
|
|
3067
5515
|
];
|
|
3068
5516
|
for (const { path: path3, source, parse } of sources) {
|
|
3069
|
-
if (!(0,
|
|
5517
|
+
if (!(0, import_fs9.existsSync)(path3)) continue;
|
|
3070
5518
|
try {
|
|
3071
|
-
const collections = parse((0,
|
|
5519
|
+
const collections = parse((0, import_fs9.readFileSync)(path3, "utf8"));
|
|
3072
5520
|
return {
|
|
3073
5521
|
text: JSON.stringify({ collections, source, collectionCount: collections.length }),
|
|
3074
5522
|
isError: false
|
|
@@ -3219,8 +5667,8 @@ function handleValidateWorkflow(input) {
|
|
|
3219
5667
|
if (input.workflow && Object.keys(input.workflow).length > 0) {
|
|
3220
5668
|
raw = input.workflow;
|
|
3221
5669
|
} else {
|
|
3222
|
-
const artifactPath = (0,
|
|
3223
|
-
if (!(0,
|
|
5670
|
+
const artifactPath = (0, import_path9.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
|
|
5671
|
+
if (!(0, import_fs9.existsSync)(artifactPath)) {
|
|
3224
5672
|
return fail([
|
|
3225
5673
|
{
|
|
3226
5674
|
code: "NO_WORKFLOW",
|
|
@@ -3229,7 +5677,7 @@ function handleValidateWorkflow(input) {
|
|
|
3229
5677
|
]);
|
|
3230
5678
|
}
|
|
3231
5679
|
try {
|
|
3232
|
-
raw = JSON.parse((0,
|
|
5680
|
+
raw = JSON.parse((0, import_fs9.readFileSync)(artifactPath, "utf8"));
|
|
3233
5681
|
} catch (err) {
|
|
3234
5682
|
return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
|
|
3235
5683
|
}
|
|
@@ -3428,7 +5876,7 @@ function registerDomainTools(server2) {
|
|
|
3428
5876
|
"stackwright_pro_resolve_data_strategy",
|
|
3429
5877
|
"Look up the data freshness strategy configuration from the user's answer. Returns mechanism, revalidation seconds, required packages, and the exact MCP tool call to make. Replaces the strategy table in the data-otter prompt.",
|
|
3430
5878
|
{
|
|
3431
|
-
strategy:
|
|
5879
|
+
strategy: import_zod16.z.string().describe(
|
|
3432
5880
|
'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
|
|
3433
5881
|
)
|
|
3434
5882
|
},
|
|
@@ -3438,7 +5886,7 @@ function registerDomainTools(server2) {
|
|
|
3438
5886
|
"stackwright_pro_validate_workflow",
|
|
3439
5887
|
"Validate a workflow definition against the Stackwright workflow schema. Checks step ID uniqueness, transition targets, terminal state existence, and service references. Call this after the workflow otter produces output.",
|
|
3440
5888
|
{
|
|
3441
|
-
workflow:
|
|
5889
|
+
workflow: jsonCoerce(import_zod16.z.record(import_zod16.z.string(), import_zod16.z.unknown()).optional()).describe(
|
|
3442
5890
|
"Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
|
|
3443
5891
|
)
|
|
3444
5892
|
},
|
|
@@ -3446,18 +5894,589 @@ function registerDomainTools(server2) {
|
|
|
3446
5894
|
);
|
|
3447
5895
|
}
|
|
3448
5896
|
|
|
5897
|
+
// src/tools/type-schemas.ts
|
|
5898
|
+
var import_zod17 = require("zod");
|
|
5899
|
+
function buildTypeSchemaSummary() {
|
|
5900
|
+
return {
|
|
5901
|
+
version: "1.0",
|
|
5902
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5903
|
+
domains: {
|
|
5904
|
+
workflow: {
|
|
5905
|
+
description: "Workflow DSL \u2014 step definitions, auth blocks, field types, conditions",
|
|
5906
|
+
schemas: [
|
|
5907
|
+
"WorkflowFileSchema",
|
|
5908
|
+
"WorkflowDefinitionSchema",
|
|
5909
|
+
"WorkflowStepSchema",
|
|
5910
|
+
"WorkflowStepTypeSchema",
|
|
5911
|
+
"WorkflowFieldSchema",
|
|
5912
|
+
"WorkflowActionSchema",
|
|
5913
|
+
"WorkflowAuthSchema",
|
|
5914
|
+
"WorkflowThemeSchema",
|
|
5915
|
+
"TransitionConditionSchema",
|
|
5916
|
+
"PersistenceSchema"
|
|
5917
|
+
],
|
|
5918
|
+
otter: "stackwright-pro-workflow-otter",
|
|
5919
|
+
artifactKey: "workflowConfig"
|
|
5920
|
+
},
|
|
5921
|
+
auth: {
|
|
5922
|
+
description: "Authentication providers \u2014 PKI/CAC, OIDC, RBAC configuration",
|
|
5923
|
+
schemas: [
|
|
5924
|
+
"authConfigSchema",
|
|
5925
|
+
"pkiConfigSchema",
|
|
5926
|
+
"oidcConfigSchema",
|
|
5927
|
+
"rbacConfigSchema",
|
|
5928
|
+
"componentAuthSchema",
|
|
5929
|
+
"authUserSchema",
|
|
5930
|
+
"authSessionSchema"
|
|
5931
|
+
],
|
|
5932
|
+
otter: "stackwright-pro-auth-otter",
|
|
5933
|
+
artifactKey: "authConfig"
|
|
5934
|
+
},
|
|
5935
|
+
openapi: {
|
|
5936
|
+
description: "OpenAPI spec integration \u2014 collection config, endpoint filters, actions",
|
|
5937
|
+
interfaces: [
|
|
5938
|
+
"OpenAPIConfig",
|
|
5939
|
+
"ActionConfig",
|
|
5940
|
+
"EndpointFilter",
|
|
5941
|
+
"ApprovedSpec",
|
|
5942
|
+
"PrebuildSecurityConfig",
|
|
5943
|
+
"SiteConfig",
|
|
5944
|
+
"ValidationResult"
|
|
5945
|
+
],
|
|
5946
|
+
otter: "stackwright-pro-api-otter",
|
|
5947
|
+
artifactKey: "apiConfig"
|
|
5948
|
+
},
|
|
5949
|
+
pulse: {
|
|
5950
|
+
description: "Real-time data polling \u2014 source-agnostic polling options and states",
|
|
5951
|
+
interfaces: ["PulseOptions", "PulseMeta", "PulseState"],
|
|
5952
|
+
note: "React-bound types (PulseProps, PulseIndicatorProps) remain in @stackwright-pro/pulse"
|
|
5953
|
+
},
|
|
5954
|
+
enterprise: {
|
|
5955
|
+
description: "Enterprise collection access \u2014 multi-tenant provider extension",
|
|
5956
|
+
interfaces: [
|
|
5957
|
+
"EnterpriseCollectionProvider",
|
|
5958
|
+
"CollectionProvider",
|
|
5959
|
+
"CollectionEntry",
|
|
5960
|
+
"CollectionListOptions",
|
|
5961
|
+
"CollectionListResult",
|
|
5962
|
+
"TenantFilter",
|
|
5963
|
+
"Collection"
|
|
5964
|
+
],
|
|
5965
|
+
note: "CollectionProvider, CollectionEntry, CollectionListOptions, and CollectionListResult are re-exported from @stackwright/types (^1.5.0). EnterpriseCollectionProvider, TenantFilter, and Collection are Pro-only extensions."
|
|
5966
|
+
}
|
|
5967
|
+
}
|
|
5968
|
+
};
|
|
5969
|
+
}
|
|
5970
|
+
function registerTypeSchemasTool(server2) {
|
|
5971
|
+
server2.tool(
|
|
5972
|
+
"stackwright_pro_get_type_schemas",
|
|
5973
|
+
"Returns a structured summary of all canonical @stackwright-pro/types schemas, organized by domain. Use this to determine which otter owns a given schema and what artifact key to expect.",
|
|
5974
|
+
{
|
|
5975
|
+
format: import_zod17.z.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
|
|
5976
|
+
},
|
|
5977
|
+
async ({ format }) => {
|
|
5978
|
+
const summary = buildTypeSchemaSummary();
|
|
5979
|
+
const output = format === "domains-only" ? Object.keys(summary.domains) : summary;
|
|
5980
|
+
return {
|
|
5981
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
5982
|
+
};
|
|
5983
|
+
}
|
|
5984
|
+
);
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
// src/tools/scaffold-cleanup.ts
|
|
5988
|
+
var import_fs10 = require("fs");
|
|
5989
|
+
var import_path10 = require("path");
|
|
5990
|
+
var SCAFFOLD_FILES_TO_DELETE = [
|
|
5991
|
+
"content/posts/getting-started.yaml",
|
|
5992
|
+
"content/posts/hello-world.yaml"
|
|
5993
|
+
];
|
|
5994
|
+
var GETTING_STARTED_SCAFFOLD_FINGERPRINTS = [
|
|
5995
|
+
"Welcome to your new Stackwright",
|
|
5996
|
+
"Petstore API",
|
|
5997
|
+
"Petstore"
|
|
5998
|
+
];
|
|
5999
|
+
var POST_FILE_EXTENSIONS = [".yaml", ".yml", ".md", ".mdx"];
|
|
6000
|
+
var POSTS_COLLECTION_FILE = "_collection.yaml";
|
|
6001
|
+
var SCAFFOLD_DIRS_TO_PRUNE = [
|
|
6002
|
+
"pages/getting-started",
|
|
6003
|
+
"content/posts"
|
|
6004
|
+
// Don't prune 'content' — user may have other content directories
|
|
6005
|
+
];
|
|
6006
|
+
var NOT_FOUND_PATH = "app/not-found.tsx";
|
|
6007
|
+
var FALLBACK_COLORS = {
|
|
6008
|
+
background: "#ffffff",
|
|
6009
|
+
foreground: "#171717",
|
|
6010
|
+
primary: "#525252",
|
|
6011
|
+
mutedForeground: "#737373"
|
|
6012
|
+
};
|
|
6013
|
+
function readThemeColors(cwd) {
|
|
6014
|
+
const tokenPath = (0, import_path10.join)(cwd, ".stackwright", "artifacts", "theme-tokens.json");
|
|
6015
|
+
if (!(0, import_fs10.existsSync)(tokenPath)) return { ...FALLBACK_COLORS };
|
|
6016
|
+
const stat = (0, import_fs10.lstatSync)(tokenPath);
|
|
6017
|
+
if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
|
|
6018
|
+
try {
|
|
6019
|
+
const raw = JSON.parse((0, import_fs10.readFileSync)(tokenPath, "utf-8"));
|
|
6020
|
+
const css = raw?.cssVariables ?? {};
|
|
6021
|
+
const toHsl = (hslValue) => {
|
|
6022
|
+
if (!hslValue || typeof hslValue !== "string") return null;
|
|
6023
|
+
return `hsl(${hslValue})`;
|
|
6024
|
+
};
|
|
6025
|
+
return {
|
|
6026
|
+
background: toHsl(css["--background"]) ?? FALLBACK_COLORS.background,
|
|
6027
|
+
foreground: toHsl(css["--foreground"]) ?? FALLBACK_COLORS.foreground,
|
|
6028
|
+
primary: toHsl(css["--primary"]) ?? FALLBACK_COLORS.primary,
|
|
6029
|
+
mutedForeground: toHsl(css["--muted-foreground"]) ?? FALLBACK_COLORS.mutedForeground
|
|
6030
|
+
};
|
|
6031
|
+
} catch {
|
|
6032
|
+
return { ...FALLBACK_COLORS };
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
function generateNotFoundPage(colors) {
|
|
6036
|
+
return `export default function NotFound() {
|
|
6037
|
+
return (
|
|
6038
|
+
<div
|
|
6039
|
+
style={{
|
|
6040
|
+
display: 'flex',
|
|
6041
|
+
flexDirection: 'column',
|
|
6042
|
+
alignItems: 'center',
|
|
6043
|
+
justifyContent: 'center',
|
|
6044
|
+
minHeight: '100vh',
|
|
6045
|
+
backgroundColor: '${colors.background}',
|
|
6046
|
+
color: '${colors.foreground}',
|
|
6047
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
6048
|
+
padding: '2rem',
|
|
6049
|
+
textAlign: 'center',
|
|
6050
|
+
}}
|
|
6051
|
+
>
|
|
6052
|
+
<h1 style={{ fontSize: '4rem', fontWeight: 700, margin: 0, color: '${colors.primary}' }}>
|
|
6053
|
+
404
|
|
6054
|
+
</h1>
|
|
6055
|
+
<p style={{ fontSize: '1.25rem', color: '${colors.mutedForeground}', marginTop: '0.5rem' }}>
|
|
6056
|
+
Page not found
|
|
6057
|
+
</p>
|
|
6058
|
+
<a
|
|
6059
|
+
href="/"
|
|
6060
|
+
style={{
|
|
6061
|
+
marginTop: '2rem',
|
|
6062
|
+
padding: '0.75rem 1.5rem',
|
|
6063
|
+
backgroundColor: '${colors.primary}',
|
|
6064
|
+
color: '${colors.background}',
|
|
6065
|
+
borderRadius: '0.5rem',
|
|
6066
|
+
textDecoration: 'none',
|
|
6067
|
+
fontSize: '0.875rem',
|
|
6068
|
+
fontWeight: 500,
|
|
6069
|
+
}}
|
|
6070
|
+
>
|
|
6071
|
+
Go Home
|
|
6072
|
+
</a>
|
|
6073
|
+
</div>
|
|
6074
|
+
);
|
|
6075
|
+
}
|
|
6076
|
+
`;
|
|
6077
|
+
}
|
|
6078
|
+
function isSafePath(fullPath) {
|
|
6079
|
+
if (!(0, import_fs10.existsSync)(fullPath)) return true;
|
|
6080
|
+
const stat = (0, import_fs10.lstatSync)(fullPath);
|
|
6081
|
+
return !stat.isSymbolicLink();
|
|
6082
|
+
}
|
|
6083
|
+
function isDirEmpty(dirPath) {
|
|
6084
|
+
if (!(0, import_fs10.existsSync)(dirPath)) return false;
|
|
6085
|
+
const stat = (0, import_fs10.lstatSync)(dirPath);
|
|
6086
|
+
if (!stat.isDirectory()) return false;
|
|
6087
|
+
return (0, import_fs10.readdirSync)(dirPath).length === 0;
|
|
6088
|
+
}
|
|
6089
|
+
function handleCleanupScaffold(_cwd) {
|
|
6090
|
+
const cwd = _cwd ?? process.cwd();
|
|
6091
|
+
const result = {
|
|
6092
|
+
deleted: [],
|
|
6093
|
+
rewritten: [],
|
|
6094
|
+
skipped: [],
|
|
6095
|
+
errors: [],
|
|
6096
|
+
prunedDirs: [],
|
|
6097
|
+
cleanupWarnings: []
|
|
6098
|
+
};
|
|
6099
|
+
for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
|
|
6100
|
+
const fullPath = (0, import_path10.join)(cwd, relPath);
|
|
6101
|
+
if (!(0, import_fs10.existsSync)(fullPath)) {
|
|
6102
|
+
result.skipped.push(relPath);
|
|
6103
|
+
continue;
|
|
6104
|
+
}
|
|
6105
|
+
if (!isSafePath(fullPath)) {
|
|
6106
|
+
result.errors.push(`Refusing to delete symlink: ${relPath}`);
|
|
6107
|
+
continue;
|
|
6108
|
+
}
|
|
6109
|
+
try {
|
|
6110
|
+
(0, import_fs10.unlinkSync)(fullPath);
|
|
6111
|
+
result.deleted.push(relPath);
|
|
6112
|
+
} catch (err) {
|
|
6113
|
+
result.errors.push(`Failed to delete ${relPath}: ${String(err)}`);
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
{
|
|
6117
|
+
const relPath = "pages/getting-started/content.yml";
|
|
6118
|
+
const fullPath = (0, import_path10.join)(cwd, relPath);
|
|
6119
|
+
if (!(0, import_fs10.existsSync)(fullPath)) {
|
|
6120
|
+
result.skipped.push(relPath);
|
|
6121
|
+
} else if (!isSafePath(fullPath)) {
|
|
6122
|
+
result.errors.push(`Refusing to delete symlink: ${relPath}`);
|
|
6123
|
+
} else {
|
|
6124
|
+
const content = (0, import_fs10.readFileSync)(fullPath, "utf-8");
|
|
6125
|
+
const isScaffoldDefault = GETTING_STARTED_SCAFFOLD_FINGERPRINTS.some(
|
|
6126
|
+
(marker) => content.includes(marker)
|
|
6127
|
+
);
|
|
6128
|
+
if (isScaffoldDefault) {
|
|
6129
|
+
try {
|
|
6130
|
+
(0, import_fs10.unlinkSync)(fullPath);
|
|
6131
|
+
result.deleted.push(relPath);
|
|
6132
|
+
} catch (err) {
|
|
6133
|
+
result.errors.push(`Failed to delete ${relPath}: ${String(err)}`);
|
|
6134
|
+
}
|
|
6135
|
+
} else {
|
|
6136
|
+
result.cleanupWarnings.push(
|
|
6137
|
+
`pages/getting-started/content.yml preserved \u2014 no scaffold fingerprint found (user-modified content)`
|
|
6138
|
+
);
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
{
|
|
6143
|
+
const relPath = `content/posts/${POSTS_COLLECTION_FILE}`;
|
|
6144
|
+
const fullPath = (0, import_path10.join)(cwd, relPath);
|
|
6145
|
+
const postsDir = (0, import_path10.join)(cwd, "content/posts");
|
|
6146
|
+
if (!(0, import_fs10.existsSync)(fullPath)) {
|
|
6147
|
+
result.skipped.push(relPath);
|
|
6148
|
+
} else if (!isSafePath(fullPath)) {
|
|
6149
|
+
result.errors.push(`Refusing to delete symlink: ${relPath}`);
|
|
6150
|
+
} else if (!(0, import_fs10.existsSync)(postsDir)) {
|
|
6151
|
+
result.skipped.push(relPath);
|
|
6152
|
+
} else {
|
|
6153
|
+
const userPostFiles = (0, import_fs10.readdirSync)(postsDir).filter(
|
|
6154
|
+
(f) => f !== POSTS_COLLECTION_FILE && POST_FILE_EXTENSIONS.some((ext) => f.endsWith(ext))
|
|
6155
|
+
);
|
|
6156
|
+
if (userPostFiles.length === 0) {
|
|
6157
|
+
try {
|
|
6158
|
+
(0, import_fs10.unlinkSync)(fullPath);
|
|
6159
|
+
result.deleted.push(relPath);
|
|
6160
|
+
} catch (err) {
|
|
6161
|
+
result.errors.push(`Failed to delete ${relPath}: ${String(err)}`);
|
|
6162
|
+
}
|
|
6163
|
+
} else {
|
|
6164
|
+
result.cleanupWarnings.push(
|
|
6165
|
+
`content/posts/_collection.yaml preserved \u2014 ${userPostFiles.length} user post file(s) exist: ${userPostFiles.slice(0, 3).join(", ")}`
|
|
6166
|
+
);
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
|
|
6171
|
+
const fullDir = (0, import_path10.join)(cwd, relDir);
|
|
6172
|
+
if (!(0, import_fs10.existsSync)(fullDir)) continue;
|
|
6173
|
+
if (!isSafePath(fullDir)) {
|
|
6174
|
+
result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
|
|
6175
|
+
continue;
|
|
6176
|
+
}
|
|
6177
|
+
if (isDirEmpty(fullDir)) {
|
|
6178
|
+
try {
|
|
6179
|
+
(0, import_fs10.rmdirSync)(fullDir);
|
|
6180
|
+
result.prunedDirs.push(relDir);
|
|
6181
|
+
} catch (err) {
|
|
6182
|
+
result.errors.push(`Failed to prune directory ${relDir}: ${String(err)}`);
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6186
|
+
{
|
|
6187
|
+
const fullPath = (0, import_path10.join)(cwd, NOT_FOUND_PATH);
|
|
6188
|
+
if (!(0, import_fs10.existsSync)(fullPath)) {
|
|
6189
|
+
result.skipped.push(NOT_FOUND_PATH);
|
|
6190
|
+
} else if (!isSafePath(fullPath)) {
|
|
6191
|
+
result.errors.push(`Refusing to rewrite symlink: ${NOT_FOUND_PATH}`);
|
|
6192
|
+
} else {
|
|
6193
|
+
try {
|
|
6194
|
+
const colors = readThemeColors(cwd);
|
|
6195
|
+
const content = generateNotFoundPage(colors);
|
|
6196
|
+
const dir = (0, import_path10.dirname)(fullPath);
|
|
6197
|
+
(0, import_fs10.mkdirSync)(dir, { recursive: true });
|
|
6198
|
+
(0, import_fs10.writeFileSync)(fullPath, content, "utf-8");
|
|
6199
|
+
result.rewritten.push(NOT_FOUND_PATH);
|
|
6200
|
+
} catch (err) {
|
|
6201
|
+
result.errors.push(`Failed to rewrite ${NOT_FOUND_PATH}: ${String(err)}`);
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
return {
|
|
6206
|
+
text: JSON.stringify(result),
|
|
6207
|
+
isError: false
|
|
6208
|
+
};
|
|
6209
|
+
}
|
|
6210
|
+
function registerScaffoldCleanupTools(server2) {
|
|
6211
|
+
const DESC = "Deterministic scaffold remnant cleanup \u2014 removes stale OSS scaffold files after raft run.";
|
|
6212
|
+
server2.tool(
|
|
6213
|
+
"stackwright_pro_cleanup_scaffold",
|
|
6214
|
+
`Remove/rewrite stale scaffold files (blog posts, hardcoded not-found page) after the raft pipeline completes. ${DESC}`,
|
|
6215
|
+
{},
|
|
6216
|
+
async () => {
|
|
6217
|
+
const r = handleCleanupScaffold();
|
|
6218
|
+
return {
|
|
6219
|
+
content: [{ type: "text", text: r.text }],
|
|
6220
|
+
isError: r.isError
|
|
6221
|
+
};
|
|
6222
|
+
}
|
|
6223
|
+
);
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6226
|
+
// src/tools/contrast.ts
|
|
6227
|
+
var import_zod18 = require("zod");
|
|
6228
|
+
function linearizeChannel(c255) {
|
|
6229
|
+
const c = c255 / 255;
|
|
6230
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
6231
|
+
}
|
|
6232
|
+
function relativeLuminance(r, g, b) {
|
|
6233
|
+
return 0.2126 * linearizeChannel(r) + 0.7152 * linearizeChannel(g) + 0.0722 * linearizeChannel(b);
|
|
6234
|
+
}
|
|
6235
|
+
function contrastRatioFromLuminance(l1, l2) {
|
|
6236
|
+
const lighter = Math.max(l1, l2);
|
|
6237
|
+
const darker = Math.min(l1, l2);
|
|
6238
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
6239
|
+
}
|
|
6240
|
+
function parseHex(hex) {
|
|
6241
|
+
const clean = hex.replace("#", "").trim().toLowerCase();
|
|
6242
|
+
if (clean.length === 3) {
|
|
6243
|
+
const r = parseInt(clean.charAt(0) + clean.charAt(0), 16);
|
|
6244
|
+
const g = parseInt(clean.charAt(1) + clean.charAt(1), 16);
|
|
6245
|
+
const b = parseInt(clean.charAt(2) + clean.charAt(2), 16);
|
|
6246
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
6247
|
+
return [r, g, b];
|
|
6248
|
+
}
|
|
6249
|
+
if (clean.length === 6) {
|
|
6250
|
+
const r = parseInt(clean.slice(0, 2), 16);
|
|
6251
|
+
const g = parseInt(clean.slice(2, 4), 16);
|
|
6252
|
+
const b = parseInt(clean.slice(4, 6), 16);
|
|
6253
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
|
|
6254
|
+
return [r, g, b];
|
|
6255
|
+
}
|
|
6256
|
+
return null;
|
|
6257
|
+
}
|
|
6258
|
+
function hslToRgb(h, s, l) {
|
|
6259
|
+
const hn = h / 360;
|
|
6260
|
+
const sn = s / 100;
|
|
6261
|
+
const ln = l / 100;
|
|
6262
|
+
const hue2rgb = (p, q, t) => {
|
|
6263
|
+
let tt = t;
|
|
6264
|
+
if (tt < 0) tt += 1;
|
|
6265
|
+
if (tt > 1) tt -= 1;
|
|
6266
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
6267
|
+
if (tt < 1 / 2) return q;
|
|
6268
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
6269
|
+
return p;
|
|
6270
|
+
};
|
|
6271
|
+
let r, g, b;
|
|
6272
|
+
if (sn === 0) {
|
|
6273
|
+
r = g = b = ln;
|
|
6274
|
+
} else {
|
|
6275
|
+
const q = ln < 0.5 ? ln * (1 + sn) : ln + sn - ln * sn;
|
|
6276
|
+
const p = 2 * ln - q;
|
|
6277
|
+
r = hue2rgb(p, q, hn + 1 / 3);
|
|
6278
|
+
g = hue2rgb(p, q, hn);
|
|
6279
|
+
b = hue2rgb(p, q, hn - 1 / 3);
|
|
6280
|
+
}
|
|
6281
|
+
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
|
6282
|
+
}
|
|
6283
|
+
function parseHsl(hsl) {
|
|
6284
|
+
let str = hsl.replace(/^hsl\s*\(\s*/i, "").replace(/\s*\)$/, "").trim();
|
|
6285
|
+
str = str.replace(/%/g, "");
|
|
6286
|
+
const parts = str.split(/[\s,]+/).filter(Boolean);
|
|
6287
|
+
if (parts.length !== 3) return null;
|
|
6288
|
+
const h = parseFloat(parts[0]);
|
|
6289
|
+
const s = parseFloat(parts[1]);
|
|
6290
|
+
const l = parseFloat(parts[2]);
|
|
6291
|
+
if (isNaN(h) || isNaN(s) || isNaN(l)) return null;
|
|
6292
|
+
return hslToRgb(h, s, l);
|
|
6293
|
+
}
|
|
6294
|
+
function parseColor(color) {
|
|
6295
|
+
const trimmed = color.trim();
|
|
6296
|
+
if (trimmed.startsWith("#")) return parseHex(trimmed);
|
|
6297
|
+
if (/^hsl\s*\(/i.test(trimmed)) return parseHsl(trimmed);
|
|
6298
|
+
if (/^\d[\d.]*[\s,]+\d[\d.]*%?[\s,]+\d[\d.]*%?$/.test(trimmed)) return parseHsl(trimmed);
|
|
6299
|
+
return null;
|
|
6300
|
+
}
|
|
6301
|
+
function rgbToHex(r, g, b) {
|
|
6302
|
+
return "#" + [r, g, b].map(
|
|
6303
|
+
(c) => Math.max(0, Math.min(255, Math.round(c))).toString(16).padStart(2, "0")
|
|
6304
|
+
).join("");
|
|
6305
|
+
}
|
|
6306
|
+
function handleCheckContrast(fg, bg) {
|
|
6307
|
+
const fgRgb = parseColor(fg);
|
|
6308
|
+
const bgRgb = parseColor(bg);
|
|
6309
|
+
if (!fgRgb) throw new Error(`Cannot parse foreground color: "${fg}"`);
|
|
6310
|
+
if (!bgRgb) throw new Error(`Cannot parse background color: "${bg}"`);
|
|
6311
|
+
const fgL = relativeLuminance(...fgRgb);
|
|
6312
|
+
const bgL = relativeLuminance(...bgRgb);
|
|
6313
|
+
const ratio = parseFloat(contrastRatioFromLuminance(fgL, bgL).toFixed(2));
|
|
6314
|
+
return {
|
|
6315
|
+
fgHex: rgbToHex(...fgRgb),
|
|
6316
|
+
bgHex: rgbToHex(...bgRgb),
|
|
6317
|
+
ratio,
|
|
6318
|
+
aa: ratio >= 4.5,
|
|
6319
|
+
aaLarge: ratio >= 3,
|
|
6320
|
+
aaa: ratio >= 7,
|
|
6321
|
+
aaaLarge: ratio >= 4.5
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
function handleDeriveAccessiblePalette(seed, targetRatio) {
|
|
6325
|
+
const seedRgb = parseColor(seed);
|
|
6326
|
+
if (!seedRgb) throw new Error(`Cannot parse seed color: "${seed}"`);
|
|
6327
|
+
const seedHex = rgbToHex(...seedRgb);
|
|
6328
|
+
const seedL = relativeLuminance(...seedRgb);
|
|
6329
|
+
const whiteRatio = parseFloat(contrastRatioFromLuminance(1, seedL).toFixed(2));
|
|
6330
|
+
const blackRatio = parseFloat(contrastRatioFromLuminance(0, seedL).toFixed(2));
|
|
6331
|
+
const whiteWorks = whiteRatio >= targetRatio;
|
|
6332
|
+
const blackWorks = blackRatio >= targetRatio;
|
|
6333
|
+
let strategy;
|
|
6334
|
+
let foreground;
|
|
6335
|
+
let ratio;
|
|
6336
|
+
let alternativeForeground;
|
|
6337
|
+
let alternativeRatio;
|
|
6338
|
+
if (blackWorks && whiteWorks) {
|
|
6339
|
+
if (blackRatio >= whiteRatio) {
|
|
6340
|
+
strategy = "black";
|
|
6341
|
+
foreground = "#000000";
|
|
6342
|
+
ratio = blackRatio;
|
|
6343
|
+
alternativeForeground = "#ffffff";
|
|
6344
|
+
alternativeRatio = whiteRatio;
|
|
6345
|
+
} else {
|
|
6346
|
+
strategy = "white";
|
|
6347
|
+
foreground = "#ffffff";
|
|
6348
|
+
ratio = whiteRatio;
|
|
6349
|
+
alternativeForeground = "#000000";
|
|
6350
|
+
alternativeRatio = blackRatio;
|
|
6351
|
+
}
|
|
6352
|
+
} else if (blackWorks) {
|
|
6353
|
+
strategy = "black";
|
|
6354
|
+
foreground = "#000000";
|
|
6355
|
+
ratio = blackRatio;
|
|
6356
|
+
alternativeForeground = "#ffffff";
|
|
6357
|
+
alternativeRatio = whiteRatio;
|
|
6358
|
+
} else if (whiteWorks) {
|
|
6359
|
+
strategy = "white";
|
|
6360
|
+
foreground = "#ffffff";
|
|
6361
|
+
ratio = whiteRatio;
|
|
6362
|
+
alternativeForeground = "#000000";
|
|
6363
|
+
alternativeRatio = blackRatio;
|
|
6364
|
+
} else {
|
|
6365
|
+
strategy = "unachievable";
|
|
6366
|
+
if (blackRatio >= whiteRatio) {
|
|
6367
|
+
foreground = "#000000";
|
|
6368
|
+
ratio = blackRatio;
|
|
6369
|
+
alternativeForeground = "#ffffff";
|
|
6370
|
+
alternativeRatio = whiteRatio;
|
|
6371
|
+
} else {
|
|
6372
|
+
foreground = "#ffffff";
|
|
6373
|
+
ratio = whiteRatio;
|
|
6374
|
+
alternativeForeground = "#000000";
|
|
6375
|
+
alternativeRatio = blackRatio;
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
return {
|
|
6379
|
+
seedHex,
|
|
6380
|
+
foreground,
|
|
6381
|
+
ratio,
|
|
6382
|
+
aa: ratio >= 4.5,
|
|
6383
|
+
aaLarge: ratio >= 3,
|
|
6384
|
+
aaa: ratio >= 7,
|
|
6385
|
+
meetsTarget: ratio >= targetRatio,
|
|
6386
|
+
strategy,
|
|
6387
|
+
alternativeForeground,
|
|
6388
|
+
alternativeRatio
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
var PASS = "[PASS]";
|
|
6392
|
+
var FAIL = "[FAIL]";
|
|
6393
|
+
var mark = (ok) => ok ? PASS : FAIL;
|
|
6394
|
+
function registerContrastTools(server2) {
|
|
6395
|
+
server2.tool(
|
|
6396
|
+
"stackwright_pro_check_contrast",
|
|
6397
|
+
'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%").',
|
|
6398
|
+
{
|
|
6399
|
+
fg: import_zod18.z.string().describe("Foreground (text) color \u2014 hex or HSL"),
|
|
6400
|
+
bg: import_zod18.z.string().describe("Background color \u2014 hex or HSL")
|
|
6401
|
+
},
|
|
6402
|
+
async ({ fg, bg }) => {
|
|
6403
|
+
let result;
|
|
6404
|
+
try {
|
|
6405
|
+
result = handleCheckContrast(fg, bg);
|
|
6406
|
+
} catch (err) {
|
|
6407
|
+
return {
|
|
6408
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
6409
|
+
};
|
|
6410
|
+
}
|
|
6411
|
+
const text = [
|
|
6412
|
+
`WCAG 2.1 Contrast Check`,
|
|
6413
|
+
``,
|
|
6414
|
+
` Foreground : ${result.fgHex}`,
|
|
6415
|
+
` Background : ${result.bgHex}`,
|
|
6416
|
+
` Ratio : ${result.ratio}:1`,
|
|
6417
|
+
``,
|
|
6418
|
+
` ${mark(result.aa)} AA \u2014 normal text (\u22654.5:1)`,
|
|
6419
|
+
` ${mark(result.aaLarge)} AA \u2014 large text (\u22653.0:1)`,
|
|
6420
|
+
` ${mark(result.aaa)} AAA \u2014 normal text (\u22657.0:1)`,
|
|
6421
|
+
` ${mark(result.aaaLarge)} AAA \u2014 large text (\u22654.5:1)`
|
|
6422
|
+
].join("\n");
|
|
6423
|
+
return { content: [{ type: "text", text }] };
|
|
6424
|
+
}
|
|
6425
|
+
);
|
|
6426
|
+
server2.tool(
|
|
6427
|
+
"stackwright_pro_derive_accessible_palette",
|
|
6428
|
+
'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%").',
|
|
6429
|
+
{
|
|
6430
|
+
seed: import_zod18.z.string().describe("Background (seed) color \u2014 hex or HSL"),
|
|
6431
|
+
targetRatio: numCoerce(import_zod18.z.number().default(4.5)).describe(
|
|
6432
|
+
"Minimum required contrast ratio (default 4.5 for WCAG AA)"
|
|
6433
|
+
)
|
|
6434
|
+
},
|
|
6435
|
+
async ({ seed, targetRatio }) => {
|
|
6436
|
+
let result;
|
|
6437
|
+
try {
|
|
6438
|
+
result = handleDeriveAccessiblePalette(seed, targetRatio ?? 4.5);
|
|
6439
|
+
} catch (err) {
|
|
6440
|
+
return {
|
|
6441
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
6442
|
+
};
|
|
6443
|
+
}
|
|
6444
|
+
const metLine = result.meetsTarget ? `${PASS} Meets target (${targetRatio}:1)` : `${FAIL} Target (${targetRatio}:1) UNACHIEVABLE with white or black \u2014 best available used`;
|
|
6445
|
+
const altNote = result.alternativeRatio >= (targetRatio ?? 4.5) ? `also passes (${result.alternativeRatio}:1)` : `fails at ${result.alternativeRatio}:1`;
|
|
6446
|
+
const text = [
|
|
6447
|
+
`Accessible Palette Derivation`,
|
|
6448
|
+
``,
|
|
6449
|
+
` Background (seed) : ${result.seedHex}`,
|
|
6450
|
+
` Foreground : ${result.foreground} (${result.strategy})`,
|
|
6451
|
+
` Contrast ratio : ${result.ratio}:1`,
|
|
6452
|
+
` ${metLine}`,
|
|
6453
|
+
``,
|
|
6454
|
+
` ${mark(result.aa)} AA \u2014 normal text (\u22654.5:1)`,
|
|
6455
|
+
` ${mark(result.aaLarge)} AA \u2014 large text (\u22653.0:1)`,
|
|
6456
|
+
` ${mark(result.aaa)} AAA \u2014 normal text (\u22657.0:1)`,
|
|
6457
|
+
``,
|
|
6458
|
+
` Alternative (${result.alternativeForeground}): ${altNote}`
|
|
6459
|
+
].join("\n");
|
|
6460
|
+
return { content: [{ type: "text", text }] };
|
|
6461
|
+
}
|
|
6462
|
+
);
|
|
6463
|
+
}
|
|
6464
|
+
|
|
3449
6465
|
// package.json
|
|
3450
6466
|
var package_default = {
|
|
3451
6467
|
dependencies: {
|
|
3452
6468
|
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
3453
6469
|
"@stackwright-pro/cli-data-explorer": "workspace:*",
|
|
3454
|
-
|
|
6470
|
+
"@stackwright-pro/types": "workspace:*",
|
|
6471
|
+
"@types/js-yaml": "^4.0.9",
|
|
6472
|
+
"js-yaml": "^4.2.0",
|
|
6473
|
+
zod: "^4.4.3"
|
|
3455
6474
|
},
|
|
3456
6475
|
devDependencies: {
|
|
3457
|
-
"@types/node": "
|
|
3458
|
-
tsup: "
|
|
3459
|
-
typescript: "
|
|
3460
|
-
vitest: "
|
|
6476
|
+
"@types/node": "catalog:",
|
|
6477
|
+
tsup: "catalog:",
|
|
6478
|
+
typescript: "catalog:",
|
|
6479
|
+
vitest: "catalog:"
|
|
3461
6480
|
},
|
|
3462
6481
|
scripts: {
|
|
3463
6482
|
prepublishOnly: "node scripts/verify-integrity-sync.js",
|
|
@@ -3468,10 +6487,13 @@ var package_default = {
|
|
|
3468
6487
|
"test:coverage": "vitest run --coverage"
|
|
3469
6488
|
},
|
|
3470
6489
|
name: "@stackwright-pro/mcp",
|
|
3471
|
-
version: "0.2.0-alpha.
|
|
6490
|
+
version: "0.2.0-alpha.90",
|
|
3472
6491
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
3473
|
-
license: "
|
|
6492
|
+
license: "SEE LICENSE IN LICENSE",
|
|
3474
6493
|
main: "./dist/server.js",
|
|
6494
|
+
bin: {
|
|
6495
|
+
"stackwright-pro-mcp": "./dist/server.js"
|
|
6496
|
+
},
|
|
3475
6497
|
module: "./dist/server.mjs",
|
|
3476
6498
|
types: "./dist/server.d.ts",
|
|
3477
6499
|
exports: {
|
|
@@ -3484,6 +6506,11 @@ var package_default = {
|
|
|
3484
6506
|
types: "./dist/integrity.d.ts",
|
|
3485
6507
|
import: "./dist/integrity.mjs",
|
|
3486
6508
|
require: "./dist/integrity.js"
|
|
6509
|
+
},
|
|
6510
|
+
"./type-schemas": {
|
|
6511
|
+
types: "./dist/tools/type-schemas.d.ts",
|
|
6512
|
+
import: "./dist/tools/type-schemas.mjs",
|
|
6513
|
+
require: "./dist/tools/type-schemas.js"
|
|
3487
6514
|
}
|
|
3488
6515
|
},
|
|
3489
6516
|
files: [
|
|
@@ -3511,9 +6538,24 @@ registerPipelineTools(server);
|
|
|
3511
6538
|
registerSafeWriteTools(server);
|
|
3512
6539
|
registerAuthTools(server);
|
|
3513
6540
|
registerIntegrityTools(server);
|
|
6541
|
+
registerArtifactSigningTools(server);
|
|
3514
6542
|
registerDomainTools(server);
|
|
6543
|
+
registerTypeSchemasTool(server);
|
|
6544
|
+
registerValidateYamlFragmentTool(server);
|
|
6545
|
+
registerGetSchemaTool(server);
|
|
6546
|
+
registerScaffoldCleanupTools(server);
|
|
6547
|
+
registerContrastTools(server);
|
|
3515
6548
|
async function main() {
|
|
3516
6549
|
const transport = new import_stdio.StdioServerTransport();
|
|
6550
|
+
try {
|
|
6551
|
+
const servicesRegisterPkg = "@stackwright-services/mcp/register";
|
|
6552
|
+
const mod = await import(servicesRegisterPkg);
|
|
6553
|
+
if (typeof mod.registerServicesTools === "function") {
|
|
6554
|
+
mod.registerServicesTools(server);
|
|
6555
|
+
console.error("Stackwright Services tools registered on pro MCP");
|
|
6556
|
+
}
|
|
6557
|
+
} catch {
|
|
6558
|
+
}
|
|
3517
6559
|
await server.connect(transport);
|
|
3518
6560
|
console.error("Stackwright Pro MCP server running on stdio");
|
|
3519
6561
|
}
|