minutework 0.1.40 → 0.1.41
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/EXTERNAL_ALPHA.md +17 -1
- package/README.md +21 -1
- package/assets/claude-local/skills/README.md +5 -0
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +15 -0
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +25 -9
- package/assets/claude-local/skills/shell-architecture/SKILL.md +15 -0
- package/assets/claude-local/skills/vuilder-public-site-authoring/SKILL.md +10 -4
- package/assets/claude-local/skills/vuilder-workspace-architecture/SKILL.md +78 -0
- package/assets/templates/vuilder-public-site/.env.example +11 -0
- package/assets/templates/vuilder-public-site/README.md +15 -0
- package/assets/templates/vuilder-public-site/eslint.config.mjs +6 -0
- package/assets/templates/vuilder-public-site/next-env.d.ts +4 -0
- package/assets/templates/vuilder-public-site/next.config.mjs +28 -0
- package/assets/templates/vuilder-public-site/package.json +39 -0
- package/assets/templates/vuilder-public-site/postcss.config.mjs +5 -0
- package/assets/templates/vuilder-public-site/src/app/api/onboarding/start/route.ts +19 -0
- package/assets/templates/vuilder-public-site/src/app/blog/[slug]/page.tsx +25 -0
- package/assets/templates/vuilder-public-site/src/app/blog/page.tsx +26 -0
- package/assets/templates/vuilder-public-site/src/app/globals.css +103 -0
- package/assets/templates/vuilder-public-site/src/app/layout.tsx +18 -0
- package/assets/templates/vuilder-public-site/src/app/onboarding/[[...step]]/page.tsx +39 -0
- package/assets/templates/vuilder-public-site/src/app/page.tsx +43 -0
- package/assets/templates/vuilder-public-site/src/lib/env.server.ts +31 -0
- package/assets/templates/vuilder-public-site/src/lib/platform.server.ts +47 -0
- package/assets/templates/vuilder-public-site/src/lib/public-dj.server.ts +86 -0
- package/assets/templates/vuilder-public-site/src/lib/public-dj.test.ts +8 -0
- package/assets/templates/vuilder-public-site/src/lib/routes.test.ts +13 -0
- package/assets/templates/vuilder-public-site/src/lib/routes.ts +12 -0
- package/assets/templates/vuilder-public-site/template.json +21 -0
- package/assets/templates/vuilder-public-site/tools/template/validate-template.mjs +44 -0
- package/assets/templates/vuilder-public-site/tsconfig.json +23 -0
- package/assets/templates/vuilder-public-site/vitest.config.ts +13 -0
- package/assets/templates/vuilder-shell/.env.example +8 -0
- package/assets/templates/vuilder-shell/.storybook/main.ts +19 -0
- package/assets/templates/vuilder-shell/.storybook/preview.tsx +38 -0
- package/assets/templates/vuilder-shell/README.md +49 -0
- package/assets/templates/vuilder-shell/components.json +21 -0
- package/assets/templates/vuilder-shell/eslint.config.mjs +41 -0
- package/assets/templates/vuilder-shell/next-env.d.ts +6 -0
- package/assets/templates/vuilder-shell/next.config.mjs +33 -0
- package/assets/templates/vuilder-shell/package.json +61 -0
- package/assets/templates/vuilder-shell/postcss.config.mjs +8 -0
- package/assets/templates/vuilder-shell/public/.gitkeep +1 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.test.ts +105 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.ts +63 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.test.ts +63 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.ts +24 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/session/route.test.ts +70 -0
- package/assets/templates/vuilder-shell/src/app/api/auth/session/route.ts +27 -0
- package/assets/templates/vuilder-shell/src/app/app/layout.tsx +17 -0
- package/assets/templates/vuilder-shell/src/app/app/page.tsx +30 -0
- package/assets/templates/vuilder-shell/src/app/blog/[slug]/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/blog/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/docs/[...slug]/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/docs/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/globals.css +70 -0
- package/assets/templates/vuilder-shell/src/app/layout.tsx +69 -0
- package/assets/templates/vuilder-shell/src/app/login/route.test.ts +33 -0
- package/assets/templates/vuilder-shell/src/app/login/route.ts +21 -0
- package/assets/templates/vuilder-shell/src/app/page.tsx +16 -0
- package/assets/templates/vuilder-shell/src/app/pricing/page.tsx +15 -0
- package/assets/templates/vuilder-shell/src/app/providers.tsx +25 -0
- package/assets/templates/vuilder-shell/src/app/robots.ts +21 -0
- package/assets/templates/vuilder-shell/src/app/sitemap.ts +33 -0
- package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/connect/page.tsx +31 -0
- package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/page.tsx +54 -0
- package/assets/templates/vuilder-shell/src/components/ui/button.tsx +59 -0
- package/assets/templates/vuilder-shell/src/components/ui/input.tsx +21 -0
- package/assets/templates/vuilder-shell/src/design-system/docs/governance.mdx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.stories.tsx +48 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.tsx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.stories.tsx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.tsx +35 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
- package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/button.stories.tsx +37 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/button.ts +1 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/input.stories.tsx +26 -0
- package/assets/templates/vuilder-shell/src/design-system/primitives/input.ts +1 -0
- package/assets/templates/vuilder-shell/src/design-system/recipes/chrome.ts +28 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/foundation.css +31 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/index.css +3 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.json +85 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.ts +87 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/semantic.css +105 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/theme.css +59 -0
- package/assets/templates/vuilder-shell/src/design-system/tokens/tokens.stories.tsx +71 -0
- package/assets/templates/vuilder-shell/src/features/dashboard/components/tenant-dashboard.tsx +134 -0
- package/assets/templates/vuilder-shell/src/features/public-shell/components/static-public-page.tsx +58 -0
- package/assets/templates/vuilder-shell/src/features/shell/components/authenticated-app-layout-shell.tsx +84 -0
- package/assets/templates/vuilder-shell/src/features/shell/components/private-app-shell.tsx +22 -0
- package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-connect-screen.tsx +89 -0
- package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-workspace-screen.tsx +49 -0
- package/assets/templates/vuilder-shell/src/lib/app-routes.test.ts +37 -0
- package/assets/templates/vuilder-shell/src/lib/app-routes.ts +86 -0
- package/assets/templates/vuilder-shell/src/lib/auth-routes.server.test.ts +26 -0
- package/assets/templates/vuilder-shell/src/lib/auth-routes.server.ts +53 -0
- package/assets/templates/vuilder-shell/src/lib/http/same-origin.test.ts +23 -0
- package/assets/templates/vuilder-shell/src/lib/http/same-origin.ts +18 -0
- package/assets/templates/vuilder-shell/src/lib/platform/client.server.test.ts +201 -0
- package/assets/templates/vuilder-shell/src/lib/platform/client.server.ts +540 -0
- package/assets/templates/vuilder-shell/src/lib/platform/contracts.ts +190 -0
- package/assets/templates/vuilder-shell/src/lib/platform/endpoints.server.ts +29 -0
- package/assets/templates/vuilder-shell/src/lib/platform/env.server.ts +82 -0
- package/assets/templates/vuilder-shell/src/lib/platform/route-response.ts +33 -0
- package/assets/templates/vuilder-shell/src/lib/platform/session.server.ts +145 -0
- package/assets/templates/vuilder-shell/src/lib/public-site.test.ts +20 -0
- package/assets/templates/vuilder-shell/src/lib/public-site.ts +48 -0
- package/assets/templates/vuilder-shell/src/lib/theme-config.ts +10 -0
- package/assets/templates/vuilder-shell/src/lib/theme.tsx +159 -0
- package/assets/templates/vuilder-shell/src/lib/utils.ts +6 -0
- package/assets/templates/vuilder-shell/template.json +28 -0
- package/assets/templates/vuilder-shell/template.schema.json +171 -0
- package/assets/templates/vuilder-shell/test/server-only-stub.ts +1 -0
- package/assets/templates/vuilder-shell/tools/design-system/build-token-manifest.mjs +3 -0
- package/assets/templates/vuilder-shell/tools/design-system/check-imports.mjs +9 -0
- package/assets/templates/vuilder-shell/tools/design-system/check-stories.mjs +9 -0
- package/assets/templates/vuilder-shell/tools/design-system/check-values.mjs +9 -0
- package/assets/templates/vuilder-shell/tools/design-system/checks.mjs +238 -0
- package/assets/templates/vuilder-shell/tools/design-system/eslint-plugin-design-system.mjs +184 -0
- package/assets/templates/vuilder-shell/tools/design-system/playwright.config.mjs +34 -0
- package/assets/templates/vuilder-shell/tools/design-system/run-checks.mjs +22 -0
- package/assets/templates/vuilder-shell/tools/design-system/shared.mjs +166 -0
- package/assets/templates/vuilder-shell/tools/design-system/visual.spec.ts +41 -0
- package/assets/templates/vuilder-shell/tools/template/validate-route-contract.mjs +373 -0
- package/assets/templates/vuilder-shell/tools/template/validate-template.mjs +45 -0
- package/assets/templates/vuilder-shell/tools/template/with-public-site-fixture.mjs +45 -0
- package/assets/templates/vuilder-shell/tsconfig.json +42 -0
- package/assets/templates/vuilder-shell/vitest.config.ts +23 -0
- package/dist/auth.js +66 -14
- package/dist/auth.js.map +1 -1
- package/dist/deploy-state.d.ts +1 -0
- package/dist/deploy-state.js.map +1 -1
- package/dist/deploy.js +18 -4
- package/dist/deploy.js.map +1 -1
- package/dist/developer-client.d.ts +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/init-prompt.js +21 -13
- package/dist/init-prompt.js.map +1 -1
- package/dist/init.d.ts +3 -1
- package/dist/init.js +103 -12
- package/dist/init.js.map +1 -1
- package/dist/orchestrator-context.js +17 -5
- package/dist/orchestrator-context.js.map +1 -1
- package/dist/orchestrator-state.d.ts +2 -2
- package/dist/orchestrator-state.js.map +1 -1
- package/dist/publish.js +12 -2
- package/dist/publish.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/state.js +9 -0
- package/dist/state.js.map +1 -1
- package/package.json +3 -3
- package/vendor/workspace-mcp/context.d.ts +3 -1
- package/vendor/workspace-mcp/context.js +134 -21
- package/vendor/workspace-mcp/context.js.map +1 -1
- package/vendor/workspace-mcp/types.d.ts +72 -7
- package/vendor/workspace-mcp/types.js +8 -4
- package/vendor/workspace-mcp/types.js.map +1 -1
- package/assets/templates/fastapi-sidecar/poetry.lock +0 -757
- package/assets/templates/next-tenant-app/package-lock.json +0 -9682
- package/assets/templates/next-tenant-app/pnpm-lock.yaml +0 -6062
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { extname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
7
|
+
|
|
8
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
9
|
+
".ts",
|
|
10
|
+
".tsx",
|
|
11
|
+
".js",
|
|
12
|
+
".jsx",
|
|
13
|
+
".mjs",
|
|
14
|
+
".cjs",
|
|
15
|
+
".css",
|
|
16
|
+
".mdx",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ROOTS = ["src", "tools/design-system", ".storybook"];
|
|
20
|
+
|
|
21
|
+
export const rawColorPattern = /#(?:[\da-fA-F]{3,8})\b|(?:rgb|hsl|oklch)a?\([^)]*\)/g;
|
|
22
|
+
export const arbitraryVisualUtilityPattern =
|
|
23
|
+
/\b(?:bg|text|border|ring|shadow|from|to|via|fill|stroke)-\[[^\]]+\]/g;
|
|
24
|
+
export const literalStyleVisualPattern =
|
|
25
|
+
/style=\{\{[^}]*\b(?:color|background(?:Color)?|border(?:Color)?|boxShadow|fill|stroke)\s*:\s*['"`](?!var\(--)[^'"`]+['"`]/gs;
|
|
26
|
+
|
|
27
|
+
function toPosix(filePath) {
|
|
28
|
+
return filePath.split(sep).join("/");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizePath(filePath) {
|
|
32
|
+
return toPosix(relative(repoRoot, resolve(repoRoot, filePath)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function absolutePath(filePath) {
|
|
36
|
+
return resolve(repoRoot, filePath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function walkDirectory(directoryPath) {
|
|
40
|
+
const entries = readdirSync(directoryPath, { withFileTypes: true });
|
|
41
|
+
const files = [];
|
|
42
|
+
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const absoluteEntryPath = join(directoryPath, entry.name);
|
|
45
|
+
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
files.push(...walkDirectory(absoluteEntryPath));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (SOURCE_EXTENSIONS.has(extname(entry.name))) {
|
|
52
|
+
files.push(normalizePath(absoluteEntryPath));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getSourceFiles() {
|
|
60
|
+
const files = new Set();
|
|
61
|
+
|
|
62
|
+
for (const root of DEFAULT_ROOTS) {
|
|
63
|
+
const absoluteRoot = absolutePath(root);
|
|
64
|
+
if (!existsSync(absoluteRoot) || !statSync(absoluteRoot).isDirectory()) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const filePath of walkDirectory(absoluteRoot)) {
|
|
69
|
+
files.add(filePath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [...files].sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getStagedFiles() {
|
|
77
|
+
try {
|
|
78
|
+
const output = execFileSync(
|
|
79
|
+
"git",
|
|
80
|
+
["diff", "--name-only", "--cached", "--diff-filter=ACMR"],
|
|
81
|
+
{ cwd: repoRoot, encoding: "utf8" },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return output
|
|
85
|
+
.split("\n")
|
|
86
|
+
.map((line) => line.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.map(normalizePath);
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function parseCliArgs(argv = process.argv.slice(2)) {
|
|
95
|
+
const options = {
|
|
96
|
+
staged: false,
|
|
97
|
+
files: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
101
|
+
const arg = argv[index];
|
|
102
|
+
if (arg === "--staged") {
|
|
103
|
+
options.staged = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arg === "--files") {
|
|
108
|
+
options.files.push(...argv.slice(index + 1));
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
options.files.push(arg);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return options;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function resolveTargetFiles({ staged = false, files = [] } = {}) {
|
|
119
|
+
const candidates =
|
|
120
|
+
files.length > 0 ? files : staged ? getStagedFiles() : getSourceFiles();
|
|
121
|
+
const normalizedCandidates = candidates
|
|
122
|
+
.map(normalizePath)
|
|
123
|
+
.filter((candidate) => SOURCE_EXTENSIONS.has(extname(candidate)));
|
|
124
|
+
|
|
125
|
+
if (normalizedCandidates.length > 0) {
|
|
126
|
+
return [...new Set(normalizedCandidates)].sort();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return getSourceFiles();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function isTokenFile(filePath) {
|
|
133
|
+
const normalizedPath = normalizePath(filePath);
|
|
134
|
+
return (
|
|
135
|
+
normalizedPath.startsWith("src/design-system/tokens/") &&
|
|
136
|
+
(normalizedPath.endsWith(".css") ||
|
|
137
|
+
normalizedPath.endsWith("/manifest.ts") ||
|
|
138
|
+
normalizedPath.endsWith("/manifest.json"))
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function isFeatureSurfaceFile(filePath) {
|
|
143
|
+
const normalizedPath = normalizePath(filePath);
|
|
144
|
+
return (
|
|
145
|
+
normalizedPath.startsWith("src/app/") ||
|
|
146
|
+
normalizedPath.startsWith("src/features/")
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isGovernedVisualFile(filePath) {
|
|
151
|
+
const normalizedPath = normalizePath(filePath);
|
|
152
|
+
return (
|
|
153
|
+
normalizedPath.startsWith("src/app/") ||
|
|
154
|
+
normalizedPath.startsWith("src/features/") ||
|
|
155
|
+
normalizedPath.startsWith("src/design-system/")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isLegacyUiImport(specifier) {
|
|
160
|
+
return specifier.startsWith("@/components/ui/");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function matchesPattern(pattern, value) {
|
|
164
|
+
pattern.lastIndex = 0;
|
|
165
|
+
return pattern.test(value);
|
|
166
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
const storyCases = [
|
|
4
|
+
{
|
|
5
|
+
id: "design-system-primitives-button--primary",
|
|
6
|
+
name: "button-primary.png",
|
|
7
|
+
viewport: { width: 480, height: 220 },
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "design-system-primitives-input--default",
|
|
11
|
+
name: "input-default.png",
|
|
12
|
+
viewport: { width: 520, height: 220 },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "design-system-patterns-panel-frame--raised",
|
|
16
|
+
name: "panel-frame-raised.png",
|
|
17
|
+
viewport: { width: 560, height: 340 },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "design-system-patterns-status-badge--all-tones",
|
|
21
|
+
name: "status-badge-tones.png",
|
|
22
|
+
viewport: { width: 520, height: 220 },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "design-system-patterns-theme-mode-toggle--default",
|
|
26
|
+
name: "theme-mode-toggle-default.png",
|
|
27
|
+
viewport: { width: 420, height: 260 },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const storyCase of storyCases) {
|
|
32
|
+
test(storyCase.id, async ({ page }) => {
|
|
33
|
+
await page.setViewportSize(storyCase.viewport);
|
|
34
|
+
await page.goto(`/iframe.html?id=${storyCase.id}&viewMode=story`);
|
|
35
|
+
await page.addStyleTag({
|
|
36
|
+
content:
|
|
37
|
+
"*{animation:none !important;transition:none !important;caret-color:transparent !important;}",
|
|
38
|
+
});
|
|
39
|
+
await expect(page.locator("#storybook-root")).toHaveScreenshot(storyCase.name);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const templateRoot = path.resolve(__dirname, "..", "..");
|
|
7
|
+
|
|
8
|
+
const requiredPaths = [
|
|
9
|
+
"src/app/w/[workspace_slug]/page.tsx",
|
|
10
|
+
"src/app/w/[workspace_slug]/connect/page.tsx",
|
|
11
|
+
"src/app/app/page.tsx",
|
|
12
|
+
"src/app/api/auth/accept-shell-session/route.ts",
|
|
13
|
+
"src/app/api/auth/session/route.ts",
|
|
14
|
+
"src/app/api/auth/logout/route.ts",
|
|
15
|
+
"src/app/login/route.ts",
|
|
16
|
+
"src/features/vuilder-shell/components/vuilder-workspace-screen.tsx",
|
|
17
|
+
"src/features/vuilder-shell/components/vuilder-connect-screen.tsx",
|
|
18
|
+
"src/lib/auth-routes.server.ts",
|
|
19
|
+
"src/lib/app-routes.ts",
|
|
20
|
+
"src/lib/platform/client.server.ts",
|
|
21
|
+
"src/lib/platform/contracts.ts",
|
|
22
|
+
"src/lib/platform/session.server.ts",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const forbiddenPaths = [
|
|
26
|
+
"src/app/api/auth/login/route.ts",
|
|
27
|
+
"src/features/auth/components/login-screen.tsx",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const missingPaths = requiredPaths.filter(
|
|
31
|
+
(relativePath) => !existsSync(path.join(templateRoot, relativePath)),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (missingPaths.length > 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Vuilder shell route contract is incomplete.\n${missingPaths
|
|
37
|
+
.map((relativePath) => `- ${relativePath}`)
|
|
38
|
+
.join("\n")}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const presentForbiddenPaths = forbiddenPaths.filter((relativePath) =>
|
|
43
|
+
existsSync(path.join(templateRoot, relativePath)),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (presentForbiddenPaths.length > 0) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`vuilder-shell must hand credential auth to central SSO, not local password routes.\n${presentForbiddenPaths
|
|
49
|
+
.map((relativePath) => `- ${relativePath}`)
|
|
50
|
+
.join("\n")}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sourcePaths = collectNamedFiles(
|
|
55
|
+
path.join(templateRoot, "src"),
|
|
56
|
+
new Set(["ts", "tsx"]),
|
|
57
|
+
).filter((absolutePath) => !absolutePath.includes(".test."));
|
|
58
|
+
const webAuthReferences = sourcePaths.filter((absolutePath) =>
|
|
59
|
+
readFileSync(absolutePath, "utf8").includes("@minutework/web-auth"),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (webAuthReferences.length > 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`vuilder-shell must use platform member SSO, not tenant customer web-auth.\n${webAuthReferences
|
|
65
|
+
.map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
|
|
66
|
+
.join("\n")}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const credentialAuthReferences = sourcePaths.filter((absolutePath) => {
|
|
71
|
+
const source = readFileSync(absolutePath, "utf8");
|
|
72
|
+
return (
|
|
73
|
+
source.includes("loginWithPassword") ||
|
|
74
|
+
source.includes("loginRequestSchema") ||
|
|
75
|
+
source.includes("platformAuthEndpoints.login") ||
|
|
76
|
+
source.includes("/api/auth/login")
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (credentialAuthReferences.length > 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`vuilder-shell must redirect credential auth through central SSO.\n${credentialAuthReferences
|
|
83
|
+
.map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
|
|
84
|
+
.join("\n")}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rawPlatformCredentialReferences = sourcePaths.filter((absolutePath) => {
|
|
89
|
+
const relativePath = path.relative(templateRoot, absolutePath);
|
|
90
|
+
const source = readFileSync(absolutePath, "utf8");
|
|
91
|
+
if (relativePath === path.join("src", "lib", "platform", "session.server.ts")) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
"mw_platform_session",
|
|
96
|
+
"mw_platform_csrf",
|
|
97
|
+
"sessionid=",
|
|
98
|
+
"csrftoken",
|
|
99
|
+
"X-CSRFToken",
|
|
100
|
+
"platformAuthEndpoints.currentSession",
|
|
101
|
+
"platformAuthEndpoints.logout",
|
|
102
|
+
"logoutPlatformSession",
|
|
103
|
+
"ensureCsrfToken",
|
|
104
|
+
].some((pattern) => source.includes(pattern));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (rawPlatformCredentialReferences.length > 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`vuilder-shell must consume the tenant-bound shell token, not raw platform session credentials.\n${rawPlatformCredentialReferences
|
|
110
|
+
.map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
|
|
111
|
+
.join("\n")}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sharedShellCookieDomainReferences = sourcePaths.filter((absolutePath) => {
|
|
116
|
+
const source = readFileSync(absolutePath, "utf8");
|
|
117
|
+
return source.includes("MW_VUILDER_SHELL_COOKIE_DOMAIN");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (sharedShellCookieDomainReferences.length > 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`vuilder-shell must set shell sessions as host-only cookies on the branded shell origin.\n${sharedShellCookieDomainReferences
|
|
123
|
+
.map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
|
|
124
|
+
.join("\n")}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const platformClientSource = readFileSync(
|
|
129
|
+
path.join(templateRoot, "src", "lib", "platform", "client.server.ts"),
|
|
130
|
+
"utf8",
|
|
131
|
+
);
|
|
132
|
+
for (const requiredSnippet of [
|
|
133
|
+
"platformAuthEndpoints.shellTokenExchange",
|
|
134
|
+
"platformAuthEndpoints.shellTokenContext",
|
|
135
|
+
"platformAuthEndpoints.shellTokenRevoke",
|
|
136
|
+
"exchangeVuilderShellSessionHandoffCode",
|
|
137
|
+
"revokeVuilderShellSession",
|
|
138
|
+
"X-Vuilder-Shell-Session",
|
|
139
|
+
"loadWorkspaceShellSession",
|
|
140
|
+
"shellContextCanViewWorkspace",
|
|
141
|
+
"shellContextCanOpenWorkspace",
|
|
142
|
+
]) {
|
|
143
|
+
if (!platformClientSource.includes(requiredSnippet)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`src/lib/platform/client.server.ts must resolve server-authored shell token context (${requiredSnippet}).`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const workspaceRoutesRoot = path.join(templateRoot, "src", "app", "w");
|
|
151
|
+
const workspaceRouteHandlerPaths = collectNamedFiles(
|
|
152
|
+
workspaceRoutesRoot,
|
|
153
|
+
new Set(["route.ts", "route.tsx"]),
|
|
154
|
+
);
|
|
155
|
+
if (workspaceRouteHandlerPaths.length > 0) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Route handlers under /w bypass the platform shell/session contract.\n${workspaceRouteHandlerPaths
|
|
158
|
+
.map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
|
|
159
|
+
.join("\n")}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const routesSource = readFileSync(path.join(templateRoot, "src", "lib", "app-routes.ts"), "utf8");
|
|
164
|
+
for (const requiredExport of [
|
|
165
|
+
"workspaceShell",
|
|
166
|
+
"workspaceConnect",
|
|
167
|
+
"loginForWorkspace",
|
|
168
|
+
"loginForWorkspaceConnect",
|
|
169
|
+
]) {
|
|
170
|
+
if (!routesSource.includes(requiredExport)) {
|
|
171
|
+
throw new Error(`src/lib/app-routes.ts must expose ${requiredExport}.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const loginRouteSource = readFileSync(
|
|
176
|
+
path.join(templateRoot, "src", "app", "login", "route.ts"),
|
|
177
|
+
"utf8",
|
|
178
|
+
);
|
|
179
|
+
if (
|
|
180
|
+
!loginRouteSource.includes("buildCentralSsoLoginUrl") ||
|
|
181
|
+
!loginRouteSource.includes("createVuilderShellHandoffState") ||
|
|
182
|
+
!loginRouteSource.includes("applyVuilderShellHandoffStateToResponse") ||
|
|
183
|
+
!loginRouteSource.includes("NextResponse.redirect")
|
|
184
|
+
) {
|
|
185
|
+
throw new Error("/login must set shell handoff state before central SSO redirect.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const workspacePageSource = readFileSync(
|
|
189
|
+
path.join(templateRoot, "src", "app", "w", "[workspace_slug]", "page.tsx"),
|
|
190
|
+
"utf8",
|
|
191
|
+
);
|
|
192
|
+
if (!workspacePageSource.includes("VuilderWorkspaceScreen")) {
|
|
193
|
+
throw new Error("/w/[workspace_slug] must render VuilderWorkspaceScreen.");
|
|
194
|
+
}
|
|
195
|
+
for (const requiredSnippet of [
|
|
196
|
+
"loadWorkspaceShellSession",
|
|
197
|
+
"workspaceCanView",
|
|
198
|
+
"workspaceCanOpen",
|
|
199
|
+
"appRoutes.loginForWorkspace(workspaceSlug)",
|
|
200
|
+
"appRoutes.workspaceConnect(workspaceSlug)",
|
|
201
|
+
]) {
|
|
202
|
+
if (!workspacePageSource.includes(requiredSnippet)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`/w/[workspace_slug] must preserve the platform-authored shell context and requested workspace (${requiredSnippet}).`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const connectPageSource = readFileSync(
|
|
210
|
+
path.join(templateRoot, "src", "app", "w", "[workspace_slug]", "connect", "page.tsx"),
|
|
211
|
+
"utf8",
|
|
212
|
+
);
|
|
213
|
+
if (!connectPageSource.includes("VuilderConnectScreen")) {
|
|
214
|
+
throw new Error("/w/[workspace_slug]/connect must render VuilderConnectScreen.");
|
|
215
|
+
}
|
|
216
|
+
for (const requiredSnippet of [
|
|
217
|
+
"loadWorkspaceShellSession",
|
|
218
|
+
"workspaceCanView",
|
|
219
|
+
"notFound()",
|
|
220
|
+
]) {
|
|
221
|
+
if (!connectPageSource.includes(requiredSnippet)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`/w/[workspace_slug]/connect must fail closed for authenticated shell-context mismatch (${requiredSnippet}).`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const appPageSource = readFileSync(
|
|
229
|
+
path.join(templateRoot, "src", "app", "app", "page.tsx"),
|
|
230
|
+
"utf8",
|
|
231
|
+
);
|
|
232
|
+
for (const requiredSnippet of [
|
|
233
|
+
"loadCurrentWorkspaceShellSession",
|
|
234
|
+
"workspaceCanView",
|
|
235
|
+
"workspaceCanOpen",
|
|
236
|
+
"notFound()",
|
|
237
|
+
"appRoutes.workspaceConnect(membership.workspace_slug)",
|
|
238
|
+
"redirect(appRoutes.workspaceShell(membership.workspace_slug))",
|
|
239
|
+
]) {
|
|
240
|
+
if (!appPageSource.includes(requiredSnippet)) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`/app must apply the same platform-authored shell context gate before redirecting (${requiredSnippet}).`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (appPageSource.includes("PrivateAppShell") || appPageSource.includes("TenantDashboard")) {
|
|
247
|
+
throw new Error("/app must not render private workspace UI outside the /w gate.");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const acceptShellSessionRouteSource = readFileSync(
|
|
251
|
+
path.join(
|
|
252
|
+
templateRoot,
|
|
253
|
+
"src",
|
|
254
|
+
"app",
|
|
255
|
+
"api",
|
|
256
|
+
"auth",
|
|
257
|
+
"accept-shell-session",
|
|
258
|
+
"route.ts",
|
|
259
|
+
),
|
|
260
|
+
"utf8",
|
|
261
|
+
);
|
|
262
|
+
for (const requiredSnippet of [
|
|
263
|
+
"exchangeVuilderShellSessionHandoffCode",
|
|
264
|
+
"applyVuilderShellSessionToResponse",
|
|
265
|
+
"readVuilderShellHandoffState",
|
|
266
|
+
"clearVuilderShellHandoffStateFromResponse",
|
|
267
|
+
"code",
|
|
268
|
+
"state",
|
|
269
|
+
"return_to",
|
|
270
|
+
"NextResponse.redirect",
|
|
271
|
+
]) {
|
|
272
|
+
if (!acceptShellSessionRouteSource.includes(requiredSnippet)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`/api/auth/accept-shell-session must consume SSO handoff on the shell origin (${requiredSnippet}).`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (acceptShellSessionRouteSource.includes("clearPlatformSessionFromResponse")) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
"/api/auth/accept-shell-session must not clear an existing shell session when a forged handoff fails.",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const sessionRouteSource = readFileSync(
|
|
285
|
+
path.join(templateRoot, "src", "app", "api", "auth", "session", "route.ts"),
|
|
286
|
+
"utf8",
|
|
287
|
+
);
|
|
288
|
+
const sessionNoStoreHeaderCount = Array.from(
|
|
289
|
+
sessionRouteSource.matchAll(/Cache-Control["'],\s*["']no-store/g),
|
|
290
|
+
).length;
|
|
291
|
+
if (sessionNoStoreHeaderCount < 2) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"/api/auth/session must set Cache-Control: no-store on success and error responses.",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const logoutRouteSource = readFileSync(
|
|
298
|
+
path.join(templateRoot, "src", "app", "api", "auth", "logout", "route.ts"),
|
|
299
|
+
"utf8",
|
|
300
|
+
);
|
|
301
|
+
for (const requiredSnippet of [
|
|
302
|
+
"hasTrustedBrowserOrigin",
|
|
303
|
+
"readPlatformAuthState",
|
|
304
|
+
"revokeVuilderShellSession",
|
|
305
|
+
"clearPlatformSessionFromResponse",
|
|
306
|
+
]) {
|
|
307
|
+
if (!logoutRouteSource.includes(requiredSnippet)) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`/api/auth/logout must preserve the shell-token logout contract (${requiredSnippet}).`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const logoutPostHandlerIndex = logoutRouteSource.indexOf("export async function POST");
|
|
314
|
+
const logoutPostHandlerSource =
|
|
315
|
+
logoutPostHandlerIndex >= 0 ? logoutRouteSource.slice(logoutPostHandlerIndex) : "";
|
|
316
|
+
const originGuardIndex = logoutPostHandlerSource.indexOf("hasTrustedBrowserOrigin");
|
|
317
|
+
const revokeIndex = logoutPostHandlerSource.indexOf("revokeVuilderShellSession");
|
|
318
|
+
const clearCookieIndex = logoutPostHandlerSource.indexOf("clearPlatformSessionFromResponse");
|
|
319
|
+
if (
|
|
320
|
+
originGuardIndex < 0 ||
|
|
321
|
+
revokeIndex < 0 ||
|
|
322
|
+
clearCookieIndex < 0 ||
|
|
323
|
+
originGuardIndex > revokeIndex ||
|
|
324
|
+
revokeIndex > clearCookieIndex
|
|
325
|
+
) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"/api/auth/logout must check the browser origin, revoke the shell token, then clear shell cookies.",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const connectScreenSource = readFileSync(
|
|
332
|
+
path.join(
|
|
333
|
+
templateRoot,
|
|
334
|
+
"src",
|
|
335
|
+
"features",
|
|
336
|
+
"vuilder-shell",
|
|
337
|
+
"components",
|
|
338
|
+
"vuilder-connect-screen.tsx",
|
|
339
|
+
),
|
|
340
|
+
"utf8",
|
|
341
|
+
);
|
|
342
|
+
if (!connectScreenSource.includes("appRoutes.loginForWorkspaceConnect(workspaceSlug)")) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"VuilderConnectScreen must preserve workspace context when linking to login.",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log("vuilder shell route contract is valid");
|
|
349
|
+
|
|
350
|
+
function collectNamedFiles(directory, names) {
|
|
351
|
+
const entries = readdirSync(directory, { withFileTypes: true });
|
|
352
|
+
const files = [];
|
|
353
|
+
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
const absolutePath = path.join(directory, entry.name);
|
|
356
|
+
if (entry.isDirectory()) {
|
|
357
|
+
files.push(...collectNamedFiles(absolutePath, names));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (
|
|
361
|
+
!entry.isFile() ||
|
|
362
|
+
(!names.has(entry.name) && !names.has(path.extname(entry.name).slice(1)))
|
|
363
|
+
) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (!statSync(absolutePath).isFile()) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
files.push(absolutePath);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return files;
|
|
373
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const templateRoot = path.resolve(__dirname, "..", "..");
|
|
9
|
+
const bundledSchemaPath = path.join(templateRoot, "template.schema.json");
|
|
10
|
+
const sharedSchemaPath = path.resolve(templateRoot, "..", "template.schema.json");
|
|
11
|
+
const manifestPath = path.join(templateRoot, "template.json");
|
|
12
|
+
|
|
13
|
+
const bundledSchemaSource = readFileSync(bundledSchemaPath, "utf8");
|
|
14
|
+
const sharedSchemaSource = existsSync(sharedSchemaPath)
|
|
15
|
+
? readFileSync(sharedSchemaPath, "utf8")
|
|
16
|
+
: null;
|
|
17
|
+
|
|
18
|
+
function normalizeJson(source) {
|
|
19
|
+
return JSON.stringify(JSON.parse(source));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
sharedSchemaSource &&
|
|
24
|
+
normalizeJson(sharedSchemaSource) !== normalizeJson(bundledSchemaSource)
|
|
25
|
+
) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Bundled template schema is out of sync with the shared schema at ${sharedSchemaPath}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const schema = JSON.parse(sharedSchemaSource ?? bundledSchemaSource);
|
|
32
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
33
|
+
|
|
34
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
35
|
+
const validate = ajv.compile(schema);
|
|
36
|
+
|
|
37
|
+
if (!validate(manifest)) {
|
|
38
|
+
const details = (validate.errors ?? [])
|
|
39
|
+
.map((error) => `${error.instancePath || "/"} ${error.message ?? "invalid"}`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
|
|
42
|
+
throw new Error(`Template manifest validation failed.\n${details}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log("template.json is valid");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const [command, ...args] = process.argv.slice(2);
|
|
4
|
+
const storybookHeapMb = process.env.MW_STORYBOOK_NODE_HEAP_MB ?? "768";
|
|
5
|
+
|
|
6
|
+
if (!command) {
|
|
7
|
+
console.error("Usage: node tools/template/with-public-site-fixture.mjs <command> [args...]");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const childEnv = {
|
|
12
|
+
...process.env,
|
|
13
|
+
NEXT_PUBLIC_MW_APP_ID: process.env.NEXT_PUBLIC_MW_APP_ID ?? "vuilder.vertical",
|
|
14
|
+
MW_TEMPLATE_APP_NAME: process.env.MW_TEMPLATE_APP_NAME ?? "Vuilder Shell",
|
|
15
|
+
MW_PUBLIC_BASE_URL: process.env.MW_PUBLIC_BASE_URL ?? "https://shell.example.com",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
command === "storybook" &&
|
|
20
|
+
!/\bmax-old-space-size=/.test(childEnv.NODE_OPTIONS ?? "")
|
|
21
|
+
) {
|
|
22
|
+
childEnv.NODE_OPTIONS = [childEnv.NODE_OPTIONS, `--max-old-space-size=${storybookHeapMb}`]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join(" ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const child = spawn(command, args, {
|
|
28
|
+
env: childEnv,
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
child.once("error", (error) => {
|
|
33
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
child.once("exit", (code, signal) => {
|
|
38
|
+
if (signal) {
|
|
39
|
+
console.error(`Validation fixture command exited via signal ${signal}.`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.exitCode = code ?? 1;
|
|
45
|
+
});
|