@webstir-io/webstir 0.1.1 → 0.1.3
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/README.md +13 -0
- package/assets/deployment/docker/.dockerignore +7 -0
- package/assets/deployment/docker/Dockerfile +17 -0
- package/assets/deployment/docker/README.md +44 -0
- package/assets/deployment/docker/example.env +3 -0
- package/assets/features/client_nav/client_nav.ts +369 -264
- package/assets/features/client_nav/document_navigation.ts +344 -0
- package/assets/features/client_nav/form_enhancement.ts +275 -0
- package/assets/templates/api/src/backend/index.ts +71 -10
- package/assets/templates/api/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/backend/index.ts +71 -10
- package/assets/templates/full/src/backend/module.ts +515 -0
- package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
- package/assets/templates/full/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
- package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
- package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
- package/package.json +31 -13
- package/scripts/check-feature-projections.mjs +87 -0
- package/scripts/check-full-demo-sync.mjs +89 -0
- package/scripts/check-package-install.mjs +537 -0
- package/scripts/check-standalone-install.mjs +221 -0
- package/scripts/pack-standalone.mjs +52 -28
- package/scripts/publish.sh +9 -0
- package/scripts/run-tests.mjs +103 -0
- package/scripts/sync-assets.mjs +175 -17
- package/src/add-backend-compat.ts +628 -0
- package/src/add-backend.ts +155 -27
- package/src/add.ts +111 -4
- package/src/agent.ts +393 -0
- package/src/api-watch.ts +7 -4
- package/src/backend-inspect.ts +70 -2
- package/src/backend-runtime.ts +22 -14
- package/src/build.ts +1 -3
- package/src/bun-generated-frontend-watch.ts +209 -0
- package/src/bun-globals.d.ts +23 -0
- package/src/bun-spa-document.ts +310 -0
- package/src/bun-spa-routes.ts +159 -0
- package/src/bun-spa-watch.ts +29 -0
- package/src/bun-ssg-watch.ts +304 -0
- package/src/cli.ts +381 -50
- package/src/compile-tests.ts +37 -29
- package/src/dev-server.ts +215 -144
- package/src/doctor.ts +164 -0
- package/src/enable-assets.ts +18 -1
- package/src/enable.ts +133 -41
- package/src/execute.ts +30 -4
- package/src/external-workspace.ts +178 -0
- package/src/format.ts +296 -17
- package/src/frontend-inspect.ts +32 -0
- package/src/frontend-watch.ts +27 -102
- package/src/full-watch.ts +13 -18
- package/src/index.ts +7 -0
- package/src/init-assets.ts +41 -11
- package/src/init.ts +85 -71
- package/src/inspect.ts +112 -0
- package/src/mcp/run-cli-json.ts +46 -0
- package/src/mcp/server.ts +307 -0
- package/src/operations.ts +176 -0
- package/src/providers.ts +20 -18
- package/src/refresh.ts +29 -3
- package/src/repair.ts +110 -43
- package/src/runtime-filter.ts +41 -0
- package/src/runtime.ts +1 -1
- package/src/smoke.ts +48 -16
- package/src/test.ts +54 -16
- package/src/testing-runtime.ts +273 -0
- package/src/types.ts +1 -4
- package/src/watch-events.ts +46 -17
- package/src/watch.ts +25 -14
- package/src/workspace-lock.ts +207 -0
- package/src/workspace-watcher.ts +10 -6
- package/src/workspace.ts +4 -2
- package/src/watch-daemon-client.ts +0 -171
package/src/inspect.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { BackendInspectResult } from './backend-inspect.ts';
|
|
2
|
+
import type { DoctorResult } from './doctor.ts';
|
|
3
|
+
import type { FrontendInspectResult } from './frontend-inspect.ts';
|
|
4
|
+
import type { WorkspaceDescriptor } from './types.ts';
|
|
5
|
+
|
|
6
|
+
import { runBackendInspect } from './backend-inspect.ts';
|
|
7
|
+
import { runDoctor } from './doctor.ts';
|
|
8
|
+
import { runFrontendInspect } from './frontend-inspect.ts';
|
|
9
|
+
|
|
10
|
+
export interface RunInspectOptions {
|
|
11
|
+
readonly workspaceRoot: string;
|
|
12
|
+
readonly env?: Record<string, string | undefined>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface InspectStepResult {
|
|
16
|
+
readonly id: 'doctor' | 'frontend-inspect' | 'backend-inspect';
|
|
17
|
+
readonly status: 'completed' | 'skipped' | 'failed';
|
|
18
|
+
readonly summary: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface InspectResult {
|
|
22
|
+
readonly workspace: WorkspaceDescriptor;
|
|
23
|
+
readonly success: boolean;
|
|
24
|
+
readonly steps: readonly InspectStepResult[];
|
|
25
|
+
readonly doctor: DoctorResult;
|
|
26
|
+
readonly frontend?: FrontendInspectResult['frontend'];
|
|
27
|
+
readonly backend?: BackendInspectResult;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runInspect(options: RunInspectOptions): Promise<InspectResult> {
|
|
31
|
+
const steps: InspectStepResult[] = [];
|
|
32
|
+
const doctor = await runDoctor({
|
|
33
|
+
workspaceRoot: options.workspaceRoot,
|
|
34
|
+
env: options.env,
|
|
35
|
+
});
|
|
36
|
+
steps.push({
|
|
37
|
+
id: 'doctor',
|
|
38
|
+
status: doctor.healthy ? 'completed' : 'failed',
|
|
39
|
+
summary: doctor.healthy
|
|
40
|
+
? 'Workspace diagnosis completed without issues.'
|
|
41
|
+
: `Workspace diagnosis found ${doctor.issues.length} issue(s).`,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let frontendFailed = false;
|
|
45
|
+
let frontend: FrontendInspectResult['frontend'] | undefined;
|
|
46
|
+
if (doctor.workspace.mode === 'api') {
|
|
47
|
+
steps.push({
|
|
48
|
+
id: 'frontend-inspect',
|
|
49
|
+
status: 'skipped',
|
|
50
|
+
summary: 'Skipped for api workspaces.',
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
try {
|
|
54
|
+
const result = await runFrontendInspect({
|
|
55
|
+
workspaceRoot: options.workspaceRoot,
|
|
56
|
+
});
|
|
57
|
+
frontend = result.frontend;
|
|
58
|
+
steps.push({
|
|
59
|
+
id: 'frontend-inspect',
|
|
60
|
+
status: 'completed',
|
|
61
|
+
summary: `${frontend.pages.length} page(s), app shell ${frontend.appShell.exists ? 'present' : 'missing'}.`,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
frontendFailed = true;
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
steps.push({
|
|
67
|
+
id: 'frontend-inspect',
|
|
68
|
+
status: 'failed',
|
|
69
|
+
summary: `Frontend inspection failed: ${message}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let backendFailed = false;
|
|
75
|
+
let backend: BackendInspectResult | undefined;
|
|
76
|
+
if (doctor.workspace.mode === 'api' || doctor.workspace.mode === 'full') {
|
|
77
|
+
try {
|
|
78
|
+
backend = await runBackendInspect({
|
|
79
|
+
workspaceRoot: options.workspaceRoot,
|
|
80
|
+
env: options.env,
|
|
81
|
+
});
|
|
82
|
+
steps.push({
|
|
83
|
+
id: 'backend-inspect',
|
|
84
|
+
status: 'completed',
|
|
85
|
+
summary: `${backend.manifest.routes?.length ?? 0} route(s), ${backend.manifest.jobs?.length ?? 0} job(s), module ${backend.manifest.name}@${backend.manifest.version}.`,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
backendFailed = true;
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
steps.push({
|
|
91
|
+
id: 'backend-inspect',
|
|
92
|
+
status: 'failed',
|
|
93
|
+
summary: `Backend inspection failed: ${message}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
steps.push({
|
|
98
|
+
id: 'backend-inspect',
|
|
99
|
+
status: 'skipped',
|
|
100
|
+
summary: `Skipped for ${doctor.workspace.mode} workspaces.`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
workspace: doctor.workspace,
|
|
106
|
+
success: doctor.healthy && !frontendFailed && !backendFailed,
|
|
107
|
+
steps,
|
|
108
|
+
doctor,
|
|
109
|
+
...(frontend ? { frontend } : {}),
|
|
110
|
+
...(backend ? { backend } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { runCli } from '../cli.ts';
|
|
2
|
+
|
|
3
|
+
interface CliStream {
|
|
4
|
+
write(message: string): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface CliIo {
|
|
8
|
+
readonly stdout: CliStream;
|
|
9
|
+
readonly stderr: CliStream;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface JsonCliResult {
|
|
13
|
+
readonly exitCode: number;
|
|
14
|
+
readonly stdout: string;
|
|
15
|
+
readonly stderr: string;
|
|
16
|
+
readonly data?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runCliJson(args: readonly string[]): Promise<JsonCliResult> {
|
|
20
|
+
const stdout: string[] = [];
|
|
21
|
+
const stderr: string[] = [];
|
|
22
|
+
const io: CliIo = {
|
|
23
|
+
stdout: {
|
|
24
|
+
write(message) {
|
|
25
|
+
stdout.push(message);
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
stderr: {
|
|
29
|
+
write(message) {
|
|
30
|
+
stderr.push(message);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const exitCode = await runCli(args, io);
|
|
36
|
+
const stdoutText = stdout.join('');
|
|
37
|
+
const stderrText = stderr.join('');
|
|
38
|
+
const trimmed = stdoutText.trim();
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
exitCode,
|
|
42
|
+
stdout: stdoutText,
|
|
43
|
+
stderr: stderrText,
|
|
44
|
+
...(trimmed.length > 0 ? { data: JSON.parse(trimmed) as Record<string, unknown> } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
import { runAgentScaffoldJob, runAgentScaffoldPage, runAgentScaffoldRoute } from '../agent.ts';
|
|
8
|
+
import { runUpdateRouteContract } from '../add-backend.ts';
|
|
9
|
+
import { runBackendInspect } from '../backend-inspect.ts';
|
|
10
|
+
import { runCliJson, type JsonCliResult } from './run-cli-json.ts';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkg = require('../../package.json') as { version?: string };
|
|
14
|
+
const httpMethodSchema = z.enum(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']);
|
|
15
|
+
const routeInteractionSchema = z.enum(['navigation', 'mutation']);
|
|
16
|
+
const sessionModeSchema = z.enum(['optional', 'required']);
|
|
17
|
+
const fragmentModeSchema = z.enum(['replace', 'append', 'prepend']);
|
|
18
|
+
const schemaReferenceSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
kind: z.enum(['zod', 'json-schema', 'ts-rest']).optional(),
|
|
21
|
+
name: z.string().min(1),
|
|
22
|
+
source: z.string().min(1).optional(),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
|
|
26
|
+
const workspaceSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
workspace: z.string().min(1),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
|
|
32
|
+
const scaffoldPageSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
workspace: z.string().min(1),
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
|
|
39
|
+
const scaffoldRouteSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
workspace: z.string().min(1),
|
|
42
|
+
name: z.string().min(1),
|
|
43
|
+
method: httpMethodSchema.optional(),
|
|
44
|
+
path: z.string().min(1).optional(),
|
|
45
|
+
summary: z.string().min(1).optional(),
|
|
46
|
+
description: z.string().min(1).optional(),
|
|
47
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
48
|
+
interaction: routeInteractionSchema.optional(),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
|
|
52
|
+
const updateRouteContractSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
workspace: z.string().min(1),
|
|
55
|
+
method: httpMethodSchema,
|
|
56
|
+
path: z.string().min(1),
|
|
57
|
+
session: sessionModeSchema.nullable().optional(),
|
|
58
|
+
sessionWrite: z.boolean().nullable().optional(),
|
|
59
|
+
formUrlEncoded: z.boolean().nullable().optional(),
|
|
60
|
+
csrf: z.boolean().nullable().optional(),
|
|
61
|
+
fragmentTarget: z.string().min(1).nullable().optional(),
|
|
62
|
+
fragmentSelector: z.string().min(1).nullable().optional(),
|
|
63
|
+
fragmentMode: fragmentModeSchema.nullable().optional(),
|
|
64
|
+
paramsSchema: schemaReferenceSchema.nullable().optional(),
|
|
65
|
+
querySchema: schemaReferenceSchema.nullable().optional(),
|
|
66
|
+
bodySchema: schemaReferenceSchema.nullable().optional(),
|
|
67
|
+
headersSchema: schemaReferenceSchema.nullable().optional(),
|
|
68
|
+
responseSchema: schemaReferenceSchema.nullable().optional(),
|
|
69
|
+
responseStatus: z.number().int().min(100).max(599).nullable().optional(),
|
|
70
|
+
responseHeadersSchema: schemaReferenceSchema.nullable().optional(),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
|
|
74
|
+
const scaffoldJobSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
workspace: z.string().min(1),
|
|
77
|
+
name: z.string().min(1),
|
|
78
|
+
schedule: z.string().min(1).optional(),
|
|
79
|
+
description: z.string().min(1).optional(),
|
|
80
|
+
priority: z.union([z.number().int(), z.string().min(1)]).optional(),
|
|
81
|
+
})
|
|
82
|
+
.strict();
|
|
83
|
+
|
|
84
|
+
export function createMcpServer(): McpServer {
|
|
85
|
+
const server = new McpServer({
|
|
86
|
+
name: 'webstir',
|
|
87
|
+
version: getPackageVersion(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
server.registerTool(
|
|
91
|
+
'list_operations',
|
|
92
|
+
{
|
|
93
|
+
title: 'List Operations',
|
|
94
|
+
description: 'List stable Webstir operations and their machine-readable metadata.',
|
|
95
|
+
},
|
|
96
|
+
async () => toToolResult(await runCliJson(['operations', '--json'])),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
server.registerTool(
|
|
100
|
+
'inspect_workspace',
|
|
101
|
+
{
|
|
102
|
+
title: 'Inspect Workspace',
|
|
103
|
+
description: 'Run unified Webstir inspection for the selected workspace.',
|
|
104
|
+
inputSchema: workspaceSchema,
|
|
105
|
+
},
|
|
106
|
+
async ({ workspace }) =>
|
|
107
|
+
toToolResult(await runCliJson(['inspect', '--json', '--workspace', workspace])),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
server.registerTool(
|
|
111
|
+
'validate_workspace',
|
|
112
|
+
{
|
|
113
|
+
title: 'Validate Workspace',
|
|
114
|
+
description: 'Run doctor and tests for the selected workspace.',
|
|
115
|
+
inputSchema: workspaceSchema,
|
|
116
|
+
},
|
|
117
|
+
async ({ workspace }) =>
|
|
118
|
+
toToolResult(await runCliJson(['agent', 'validate', '--json', '--workspace', workspace])),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
server.registerTool(
|
|
122
|
+
'repair_workspace',
|
|
123
|
+
{
|
|
124
|
+
title: 'Repair Workspace',
|
|
125
|
+
description: 'Apply scaffold-managed repair actions and re-check workspace health.',
|
|
126
|
+
inputSchema: workspaceSchema,
|
|
127
|
+
},
|
|
128
|
+
async ({ workspace }) =>
|
|
129
|
+
toToolResult(await runCliJson(['agent', 'repair', '--json', '--workspace', workspace])),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
server.registerTool(
|
|
133
|
+
'repair_dry_run',
|
|
134
|
+
{
|
|
135
|
+
title: 'Repair Dry Run',
|
|
136
|
+
description: 'Report scaffold-managed repair actions without mutating the workspace.',
|
|
137
|
+
inputSchema: workspaceSchema,
|
|
138
|
+
},
|
|
139
|
+
async ({ workspace }) =>
|
|
140
|
+
toToolResult(await runCliJson(['repair', '--dry-run', '--json', '--workspace', workspace])),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
server.registerTool(
|
|
144
|
+
'scaffold_page',
|
|
145
|
+
{
|
|
146
|
+
title: 'Scaffold Page',
|
|
147
|
+
description: 'Create a frontend page through the stable Webstir scaffold flow.',
|
|
148
|
+
inputSchema: scaffoldPageSchema,
|
|
149
|
+
},
|
|
150
|
+
async (input) =>
|
|
151
|
+
toStructuredToolResult({
|
|
152
|
+
command: 'agent',
|
|
153
|
+
...(await runAgentScaffoldPage({
|
|
154
|
+
workspaceRoot: input.workspace,
|
|
155
|
+
pageName: input.name,
|
|
156
|
+
})),
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
server.registerTool(
|
|
161
|
+
'scaffold_route',
|
|
162
|
+
{
|
|
163
|
+
title: 'Scaffold Route',
|
|
164
|
+
description: 'Create a backend route through the stable Webstir scaffold flow.',
|
|
165
|
+
inputSchema: scaffoldRouteSchema,
|
|
166
|
+
},
|
|
167
|
+
async (input) =>
|
|
168
|
+
toStructuredToolResult({
|
|
169
|
+
command: 'agent',
|
|
170
|
+
...(await runAgentScaffoldRoute({
|
|
171
|
+
workspaceRoot: input.workspace,
|
|
172
|
+
name: input.name,
|
|
173
|
+
method: input.method,
|
|
174
|
+
path: input.path,
|
|
175
|
+
summary: input.summary,
|
|
176
|
+
description: input.description,
|
|
177
|
+
tags: input.tags,
|
|
178
|
+
interaction: input.interaction,
|
|
179
|
+
})),
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
server.registerTool(
|
|
184
|
+
'update_route_contract',
|
|
185
|
+
{
|
|
186
|
+
title: 'Update Route Contract',
|
|
187
|
+
description:
|
|
188
|
+
'Enrich an existing backend route with session, form, fragment, and request or response contract metadata.',
|
|
189
|
+
inputSchema: updateRouteContractSchema,
|
|
190
|
+
},
|
|
191
|
+
async (input) => {
|
|
192
|
+
const scaffold = await runUpdateRouteContract({
|
|
193
|
+
workspaceRoot: input.workspace,
|
|
194
|
+
method: input.method,
|
|
195
|
+
path: input.path,
|
|
196
|
+
sessionMode: input.session,
|
|
197
|
+
sessionWrite: input.sessionWrite,
|
|
198
|
+
formUrlEncoded: input.formUrlEncoded,
|
|
199
|
+
formCsrf: input.csrf,
|
|
200
|
+
fragmentTarget: input.fragmentTarget,
|
|
201
|
+
fragmentSelector: input.fragmentSelector,
|
|
202
|
+
fragmentMode: input.fragmentMode,
|
|
203
|
+
paramsSchema: formatNullableSchemaReference(input.paramsSchema),
|
|
204
|
+
querySchema: formatNullableSchemaReference(input.querySchema),
|
|
205
|
+
bodySchema: formatNullableSchemaReference(input.bodySchema),
|
|
206
|
+
headersSchema: formatNullableSchemaReference(input.headersSchema),
|
|
207
|
+
responseSchema: formatNullableSchemaReference(input.responseSchema),
|
|
208
|
+
responseStatus: input.responseStatus,
|
|
209
|
+
responseHeadersSchema: formatNullableSchemaReference(input.responseHeadersSchema),
|
|
210
|
+
});
|
|
211
|
+
const inspect = await runBackendInspect({
|
|
212
|
+
workspaceRoot: input.workspace,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return toStructuredToolResult({
|
|
216
|
+
command: 'update-route-contract',
|
|
217
|
+
success: true,
|
|
218
|
+
scaffold,
|
|
219
|
+
inspect,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
server.registerTool(
|
|
225
|
+
'scaffold_job',
|
|
226
|
+
{
|
|
227
|
+
title: 'Scaffold Job',
|
|
228
|
+
description: 'Create a backend job through the stable Webstir scaffold flow.',
|
|
229
|
+
inputSchema: scaffoldJobSchema,
|
|
230
|
+
},
|
|
231
|
+
async (input) =>
|
|
232
|
+
toStructuredToolResult({
|
|
233
|
+
command: 'agent',
|
|
234
|
+
...(await runAgentScaffoldJob({
|
|
235
|
+
workspaceRoot: input.workspace,
|
|
236
|
+
name: input.name,
|
|
237
|
+
schedule: input.schedule,
|
|
238
|
+
description: input.description,
|
|
239
|
+
priority: input.priority,
|
|
240
|
+
})),
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return server;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function runMcpServer(): Promise<void> {
|
|
248
|
+
const server = createMcpServer();
|
|
249
|
+
const transport = new StdioServerTransport();
|
|
250
|
+
await server.connect(transport);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getPackageVersion(): string {
|
|
254
|
+
if (typeof pkg.version === 'string' && pkg.version.trim().length > 0) {
|
|
255
|
+
return pkg.version;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error('Missing version in orchestrator package.json for MCP server.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatSchemaReference(
|
|
262
|
+
value: z.infer<typeof schemaReferenceSchema> | undefined,
|
|
263
|
+
): string | undefined {
|
|
264
|
+
if (!value) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return `${value.kind ?? 'zod'}:${value.name}${value.source ? `@${value.source}` : ''}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function formatNullableSchemaReference(
|
|
272
|
+
value: z.infer<typeof schemaReferenceSchema> | null | undefined,
|
|
273
|
+
): string | null | undefined {
|
|
274
|
+
if (value === null) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return formatSchemaReference(value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function toStructuredToolResult(data: Record<string, unknown>): {
|
|
281
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
282
|
+
structuredContent: Record<string, unknown>;
|
|
283
|
+
isError?: boolean;
|
|
284
|
+
} {
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
287
|
+
structuredContent: data,
|
|
288
|
+
...(data.success === false ? { isError: true } : {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function toToolResult(result: JsonCliResult): {
|
|
293
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
294
|
+
structuredContent?: Record<string, unknown>;
|
|
295
|
+
isError?: boolean;
|
|
296
|
+
} {
|
|
297
|
+
const text =
|
|
298
|
+
result.data !== undefined
|
|
299
|
+
? JSON.stringify(result.data, null, 2)
|
|
300
|
+
: [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n\n');
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text }],
|
|
304
|
+
...(result.data !== undefined ? { structuredContent: result.data } : {}),
|
|
305
|
+
...(result.exitCode !== 0 ? { isError: true } : {}),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { WorkspaceMode } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export interface WebstirOperationDescriptor {
|
|
4
|
+
readonly id:
|
|
5
|
+
| 'init'
|
|
6
|
+
| 'refresh'
|
|
7
|
+
| 'inspect'
|
|
8
|
+
| 'doctor'
|
|
9
|
+
| 'repair'
|
|
10
|
+
| 'enable'
|
|
11
|
+
| 'add-page'
|
|
12
|
+
| 'add-test'
|
|
13
|
+
| 'add-route'
|
|
14
|
+
| 'add-job'
|
|
15
|
+
| 'frontend-inspect'
|
|
16
|
+
| 'backend-inspect'
|
|
17
|
+
| 'build'
|
|
18
|
+
| 'publish'
|
|
19
|
+
| 'watch'
|
|
20
|
+
| 'test'
|
|
21
|
+
| 'smoke';
|
|
22
|
+
readonly summary: string;
|
|
23
|
+
readonly requiresWorkspace: boolean;
|
|
24
|
+
readonly mutatesWorkspace: boolean;
|
|
25
|
+
readonly supportsJson: boolean;
|
|
26
|
+
readonly stableForMcp: boolean;
|
|
27
|
+
readonly workspaceModes?: readonly WorkspaceMode[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const OPERATIONS: readonly WebstirOperationDescriptor[] = [
|
|
31
|
+
{
|
|
32
|
+
id: 'init',
|
|
33
|
+
summary: 'Scaffold a new workspace for the supported Webstir modes.',
|
|
34
|
+
requiresWorkspace: false,
|
|
35
|
+
mutatesWorkspace: true,
|
|
36
|
+
supportsJson: false,
|
|
37
|
+
stableForMcp: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'refresh',
|
|
41
|
+
summary: 'Reset and re-scaffold an existing workspace directory.',
|
|
42
|
+
requiresWorkspace: true,
|
|
43
|
+
mutatesWorkspace: true,
|
|
44
|
+
supportsJson: false,
|
|
45
|
+
stableForMcp: false,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'inspect',
|
|
49
|
+
summary: 'Diagnose the workspace and surface stable frontend and backend contract data.',
|
|
50
|
+
requiresWorkspace: true,
|
|
51
|
+
mutatesWorkspace: false,
|
|
52
|
+
supportsJson: true,
|
|
53
|
+
stableForMcp: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'doctor',
|
|
57
|
+
summary: 'Diagnose scaffold drift and backend manifest health.',
|
|
58
|
+
requiresWorkspace: true,
|
|
59
|
+
mutatesWorkspace: false,
|
|
60
|
+
supportsJson: true,
|
|
61
|
+
stableForMcp: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'repair',
|
|
65
|
+
summary: 'Restore missing scaffold-managed files and wiring.',
|
|
66
|
+
requiresWorkspace: true,
|
|
67
|
+
mutatesWorkspace: true,
|
|
68
|
+
supportsJson: true,
|
|
69
|
+
stableForMcp: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'enable',
|
|
73
|
+
summary: 'Opt into an optional framework feature.',
|
|
74
|
+
requiresWorkspace: true,
|
|
75
|
+
mutatesWorkspace: true,
|
|
76
|
+
supportsJson: false,
|
|
77
|
+
stableForMcp: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'add-page',
|
|
81
|
+
summary: 'Scaffold a frontend document page.',
|
|
82
|
+
requiresWorkspace: true,
|
|
83
|
+
mutatesWorkspace: true,
|
|
84
|
+
supportsJson: false,
|
|
85
|
+
stableForMcp: true,
|
|
86
|
+
workspaceModes: ['spa', 'ssg', 'full'],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'add-test',
|
|
90
|
+
summary: 'Scaffold a frontend or backend test file.',
|
|
91
|
+
requiresWorkspace: true,
|
|
92
|
+
mutatesWorkspace: true,
|
|
93
|
+
supportsJson: false,
|
|
94
|
+
stableForMcp: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'add-route',
|
|
98
|
+
summary: 'Record a backend route contract in the module manifest.',
|
|
99
|
+
requiresWorkspace: true,
|
|
100
|
+
mutatesWorkspace: true,
|
|
101
|
+
supportsJson: false,
|
|
102
|
+
stableForMcp: true,
|
|
103
|
+
workspaceModes: ['api', 'full'],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'add-job',
|
|
107
|
+
summary: 'Scaffold a backend job and record it in the module manifest.',
|
|
108
|
+
requiresWorkspace: true,
|
|
109
|
+
mutatesWorkspace: true,
|
|
110
|
+
supportsJson: false,
|
|
111
|
+
stableForMcp: true,
|
|
112
|
+
workspaceModes: ['api', 'full'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'frontend-inspect',
|
|
116
|
+
summary: 'Read stable frontend workspace facts such as pages, app shell, and feature flags.',
|
|
117
|
+
requiresWorkspace: true,
|
|
118
|
+
mutatesWorkspace: false,
|
|
119
|
+
supportsJson: true,
|
|
120
|
+
stableForMcp: true,
|
|
121
|
+
workspaceModes: ['spa', 'ssg', 'full'],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'backend-inspect',
|
|
125
|
+
summary: 'Build the backend and emit manifest metadata for routes and jobs.',
|
|
126
|
+
requiresWorkspace: true,
|
|
127
|
+
mutatesWorkspace: false,
|
|
128
|
+
supportsJson: true,
|
|
129
|
+
stableForMcp: true,
|
|
130
|
+
workspaceModes: ['api', 'full'],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'build',
|
|
134
|
+
summary: 'Build the current workspace through the canonical providers.',
|
|
135
|
+
requiresWorkspace: true,
|
|
136
|
+
mutatesWorkspace: false,
|
|
137
|
+
supportsJson: false,
|
|
138
|
+
stableForMcp: true,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'publish',
|
|
142
|
+
summary: 'Produce publish-ready artifacts for the current workspace.',
|
|
143
|
+
requiresWorkspace: true,
|
|
144
|
+
mutatesWorkspace: false,
|
|
145
|
+
supportsJson: false,
|
|
146
|
+
stableForMcp: true,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'watch',
|
|
150
|
+
summary: 'Run the long-lived development loop.',
|
|
151
|
+
requiresWorkspace: true,
|
|
152
|
+
mutatesWorkspace: false,
|
|
153
|
+
supportsJson: false,
|
|
154
|
+
stableForMcp: false,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'test',
|
|
158
|
+
summary: 'Build and run frontend and or backend tests for the workspace.',
|
|
159
|
+
requiresWorkspace: true,
|
|
160
|
+
mutatesWorkspace: false,
|
|
161
|
+
supportsJson: false,
|
|
162
|
+
stableForMcp: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'smoke',
|
|
166
|
+
summary: 'Run the bounded end-to-end verification flow.',
|
|
167
|
+
requiresWorkspace: false,
|
|
168
|
+
mutatesWorkspace: false,
|
|
169
|
+
supportsJson: false,
|
|
170
|
+
stableForMcp: true,
|
|
171
|
+
},
|
|
172
|
+
] as const;
|
|
173
|
+
|
|
174
|
+
export function listOperations(): readonly WebstirOperationDescriptor[] {
|
|
175
|
+
return OPERATIONS;
|
|
176
|
+
}
|
package/src/providers.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { access } from 'node:fs/promises';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
3
|
|
|
5
4
|
import type { BuildProvider, BuildTargetKind } from './types.ts';
|
|
6
5
|
import { monorepoRoot } from './paths.ts';
|
|
@@ -11,11 +10,13 @@ let localPackageBuildPromise: Promise<void> | null = null;
|
|
|
11
10
|
export async function loadProvider(kind: BuildTargetKind): Promise<BuildProvider> {
|
|
12
11
|
await ensureLocalPackageArtifacts();
|
|
13
12
|
if (kind === 'frontend') {
|
|
14
|
-
const mod = await import('@webstir-io/webstir-frontend') as {
|
|
13
|
+
const mod = (await import('@webstir-io/webstir-frontend')) as {
|
|
14
|
+
frontendProvider: BuildProvider;
|
|
15
|
+
};
|
|
15
16
|
return mod.frontendProvider;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const mod = await import('@webstir-io/webstir-backend') as { backendProvider: BuildProvider };
|
|
19
|
+
const mod = (await import('@webstir-io/webstir-backend')) as { backendProvider: BuildProvider };
|
|
19
20
|
return mod.backendProvider;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -68,21 +69,22 @@ async function runRuntimeCommand(args: readonly string[]): Promise<void> {
|
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
try {
|
|
73
|
+
await Bun.$.cwd(cwd)`${resolveRuntimeCommand()} ${args}`;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const exitCode =
|
|
76
|
+
typeof error === 'object' &&
|
|
77
|
+
error !== null &&
|
|
78
|
+
'exitCode' in error &&
|
|
79
|
+
typeof error.exitCode === 'number'
|
|
80
|
+
? error.exitCode
|
|
81
|
+
: null;
|
|
82
|
+
if (exitCode === null) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
throw new Error(`Command failed with exit code ${exitCode}: ${args.join(' ')}`, {
|
|
87
|
+
cause: error,
|
|
86
88
|
});
|
|
87
|
-
}
|
|
89
|
+
}
|
|
88
90
|
}
|