@crossdelta/pf-mcp 0.1.0
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 +389 -0
- package/dist/cli.cjs +2348 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2379 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +2489 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +722 -0
- package/dist/index.d.ts +722 -0
- package/dist/index.js +2460 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,2348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/server.ts
|
|
5
|
+
var import_facade12 = require("@crossdelta/platform-sdk/facade");
|
|
6
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
7
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
+
var import_types10 = require("@modelcontextprotocol/sdk/types.js");
|
|
9
|
+
|
|
10
|
+
// src/resources/contracts.ts
|
|
11
|
+
var import_facade = require("@crossdelta/platform-sdk/facade");
|
|
12
|
+
var CONTRACTS_URI = "pf://contracts";
|
|
13
|
+
var buildContractsMeta = (contracts) => ({
|
|
14
|
+
path: contracts.path,
|
|
15
|
+
packageName: contracts.packageName
|
|
16
|
+
});
|
|
17
|
+
var readContracts = async (workspaceRoot) => {
|
|
18
|
+
const context = await (0, import_facade.createContextFromWorkspace)(workspaceRoot);
|
|
19
|
+
const meta = buildContractsMeta(context.workspace.contracts);
|
|
20
|
+
return {
|
|
21
|
+
uri: CONTRACTS_URI,
|
|
22
|
+
mimeType: "application/json",
|
|
23
|
+
text: JSON.stringify(meta, null, 2)
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
var createContractsResource = (workspaceRoot) => ({
|
|
27
|
+
resource: {
|
|
28
|
+
uri: CONTRACTS_URI,
|
|
29
|
+
name: "Contracts",
|
|
30
|
+
description: "Contracts package configuration (path and package name).",
|
|
31
|
+
mimeType: "application/json"
|
|
32
|
+
},
|
|
33
|
+
read: async () => readContracts(workspaceRoot)
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// src/resources/generator-docs.ts
|
|
37
|
+
var import_promises = require("fs/promises");
|
|
38
|
+
var import_node_path = require("path");
|
|
39
|
+
var import_facade2 = require("@crossdelta/platform-sdk/facade");
|
|
40
|
+
var createGeneratorDocResource = (docName) => ({
|
|
41
|
+
resource: {
|
|
42
|
+
uri: `docs://generators/${docName}`,
|
|
43
|
+
name: `${docName} Generator Guidelines`,
|
|
44
|
+
description: `AI instructions for ${docName} generation`,
|
|
45
|
+
mimeType: "text/markdown"
|
|
46
|
+
},
|
|
47
|
+
async read() {
|
|
48
|
+
const docsDir = (0, import_facade2.getGeneratorDocsDir)();
|
|
49
|
+
const docPath = (0, import_node_path.join)(docsDir, `${docName}.md`);
|
|
50
|
+
try {
|
|
51
|
+
const content = await (0, import_promises.readFile)(docPath, "utf-8");
|
|
52
|
+
return {
|
|
53
|
+
uri: `docs://generators/${docName}`,
|
|
54
|
+
mimeType: "text/markdown",
|
|
55
|
+
text: content
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Failed to read generator doc '${docName}': ${error instanceof Error ? error.message : String(error)}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
var createGeneratorDocsListResource = () => ({
|
|
65
|
+
resource: {
|
|
66
|
+
uri: "docs://generators",
|
|
67
|
+
name: "Available Generator Documentation",
|
|
68
|
+
description: "List of all available generator guidelines",
|
|
69
|
+
mimeType: "application/json"
|
|
70
|
+
},
|
|
71
|
+
async read() {
|
|
72
|
+
const docs = await (0, import_facade2.listGeneratorDocs)();
|
|
73
|
+
const docsInfo = docs.map((name) => ({
|
|
74
|
+
name,
|
|
75
|
+
uri: `docs://generators/${name}`
|
|
76
|
+
}));
|
|
77
|
+
return {
|
|
78
|
+
uri: "docs://generators",
|
|
79
|
+
mimeType: "application/json",
|
|
80
|
+
text: JSON.stringify(docsInfo, null, 2)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
var createAllGeneratorDocResources = async () => {
|
|
85
|
+
const docNames = await (0, import_facade2.listGeneratorDocs)();
|
|
86
|
+
const docResources = docNames.map(createGeneratorDocResource);
|
|
87
|
+
const listResource = createGeneratorDocsListResource();
|
|
88
|
+
return [listResource, ...docResources];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/plan-cache.ts
|
|
92
|
+
var planCache = /* @__PURE__ */ new Map();
|
|
93
|
+
var reviewCache = /* @__PURE__ */ new Map();
|
|
94
|
+
var cachePlan = (plan) => {
|
|
95
|
+
planCache.set(plan.planHash, plan);
|
|
96
|
+
};
|
|
97
|
+
var cacheReview = (review) => {
|
|
98
|
+
reviewCache.set(review.planHash, review);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/resources/services.ts
|
|
102
|
+
var import_facade3 = require("@crossdelta/platform-sdk/facade");
|
|
103
|
+
var SERVICES_URI = "pf://services";
|
|
104
|
+
var readServices = async (workspaceRoot) => {
|
|
105
|
+
const context = await (0, import_facade3.createContextFromWorkspace)(workspaceRoot);
|
|
106
|
+
const meta = {
|
|
107
|
+
services: context.workspace.availableServices,
|
|
108
|
+
count: context.workspace.availableServices.length
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
uri: SERVICES_URI,
|
|
112
|
+
mimeType: "application/json",
|
|
113
|
+
text: JSON.stringify(meta, null, 2)
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
var createServicesResource = (workspaceRoot) => ({
|
|
117
|
+
resource: {
|
|
118
|
+
uri: SERVICES_URI,
|
|
119
|
+
name: "Services",
|
|
120
|
+
description: "List of discovered services in the workspace.",
|
|
121
|
+
mimeType: "application/json"
|
|
122
|
+
},
|
|
123
|
+
read: async () => readServices(workspaceRoot)
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// src/resources/templates.ts
|
|
127
|
+
var import_promises2 = require("fs/promises");
|
|
128
|
+
var import_node_path2 = require("path");
|
|
129
|
+
var getTemplatesDir = () => {
|
|
130
|
+
const devPath = (0, import_node_path2.join)(process.cwd(), "packages/platform-sdk/bin/templates");
|
|
131
|
+
try {
|
|
132
|
+
const fs = require("fs");
|
|
133
|
+
if (fs.existsSync(devPath)) {
|
|
134
|
+
return devPath;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
return (0, import_node_path2.join)(process.cwd(), "node_modules/@crossdelta/platform-sdk/bin/templates");
|
|
139
|
+
};
|
|
140
|
+
var getTemplateStructure = async (templateName) => {
|
|
141
|
+
const templateDir = (0, import_node_path2.join)(getTemplatesDir(), templateName);
|
|
142
|
+
const walk = async (dir, prefix = "") => {
|
|
143
|
+
try {
|
|
144
|
+
const entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
|
|
145
|
+
const results = await Promise.all(
|
|
146
|
+
entries.map(async (entry) => {
|
|
147
|
+
const fullPath = (0, import_node_path2.join)(dir, entry.name);
|
|
148
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
const subFiles = await walk(fullPath, relativePath);
|
|
151
|
+
return [`${relativePath}/`, ...subFiles];
|
|
152
|
+
}
|
|
153
|
+
const stats = await (0, import_promises2.stat)(fullPath);
|
|
154
|
+
return [`${relativePath} (${stats.size} bytes)`];
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
return results.flat();
|
|
158
|
+
} catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return await walk(templateDir);
|
|
163
|
+
};
|
|
164
|
+
var listTemplates = async () => {
|
|
165
|
+
const templatesDir = getTemplatesDir();
|
|
166
|
+
try {
|
|
167
|
+
const entries = await (0, import_promises2.readdir)(templatesDir, { withFileTypes: true });
|
|
168
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var listTemplateFiles = async (templateName) => {
|
|
174
|
+
const structure = await getTemplateStructure(templateName);
|
|
175
|
+
return structure.filter((entry) => !entry.endsWith("/")).map((entry) => entry.replace(/ \(\d+ bytes\)$/, ""));
|
|
176
|
+
};
|
|
177
|
+
var readTemplateFile = async (templateName, filePath) => {
|
|
178
|
+
const templateDir = (0, import_node_path2.join)(getTemplatesDir(), templateName);
|
|
179
|
+
const fullPath = (0, import_node_path2.join)(templateDir, filePath);
|
|
180
|
+
if (!fullPath.startsWith(templateDir)) {
|
|
181
|
+
throw new Error("Invalid file path");
|
|
182
|
+
}
|
|
183
|
+
return await (0, import_promises2.readFile)(fullPath, "utf-8");
|
|
184
|
+
};
|
|
185
|
+
var readTemplateStructure = async (templateName) => {
|
|
186
|
+
const structure = await getTemplateStructure(templateName);
|
|
187
|
+
return JSON.stringify(
|
|
188
|
+
{
|
|
189
|
+
template: templateName,
|
|
190
|
+
files: structure,
|
|
191
|
+
totalFiles: structure.length
|
|
192
|
+
},
|
|
193
|
+
null,
|
|
194
|
+
2
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
var createTemplatesListResource = () => ({
|
|
198
|
+
resource: {
|
|
199
|
+
uri: "templates://list",
|
|
200
|
+
name: "Available Service Templates",
|
|
201
|
+
description: "List of all available service templates (hono-microservice, nest-microservice, workspace)",
|
|
202
|
+
mimeType: "application/json"
|
|
203
|
+
},
|
|
204
|
+
async read() {
|
|
205
|
+
const templates = await listTemplates();
|
|
206
|
+
const templatesInfo = await Promise.all(
|
|
207
|
+
templates.map(async (name) => ({
|
|
208
|
+
name,
|
|
209
|
+
uri: `templates://${name}`,
|
|
210
|
+
description: `${name} template structure`
|
|
211
|
+
}))
|
|
212
|
+
);
|
|
213
|
+
return {
|
|
214
|
+
uri: "templates://list",
|
|
215
|
+
mimeType: "application/json",
|
|
216
|
+
text: JSON.stringify(
|
|
217
|
+
{
|
|
218
|
+
templates: templatesInfo,
|
|
219
|
+
count: templatesInfo.length
|
|
220
|
+
},
|
|
221
|
+
null,
|
|
222
|
+
2
|
|
223
|
+
)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
var createTemplateFileResourceTemplate = () => ({
|
|
228
|
+
uriTemplate: "templates://{template}/file/{path}",
|
|
229
|
+
name: "Template File",
|
|
230
|
+
description: "Read individual files from service templates",
|
|
231
|
+
mimeType: "text/plain"
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// src/resources/workspace-summary.ts
|
|
235
|
+
var import_facade4 = require("@crossdelta/platform-sdk/facade");
|
|
236
|
+
var WORKSPACE_SUMMARY_URI = "pf://workspace/summary";
|
|
237
|
+
var buildWorkspaceSummary = (context, pluginModules) => ({
|
|
238
|
+
workspaceRoot: context.workspace.workspaceRoot,
|
|
239
|
+
contracts: context.workspace.contracts,
|
|
240
|
+
availableServices: context.workspace.availableServices,
|
|
241
|
+
loadedPlugins: pluginModules
|
|
242
|
+
});
|
|
243
|
+
var readWorkspaceSummary = async (pluginModules = [], workspaceRoot) => {
|
|
244
|
+
const context = await (0, import_facade4.createContextFromWorkspace)(workspaceRoot);
|
|
245
|
+
const summary = buildWorkspaceSummary(context, pluginModules);
|
|
246
|
+
return {
|
|
247
|
+
uri: WORKSPACE_SUMMARY_URI,
|
|
248
|
+
mimeType: "application/json",
|
|
249
|
+
text: JSON.stringify(summary, null, 2)
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
var createWorkspaceSummaryResource = (pluginModules = [], workspaceRoot) => ({
|
|
253
|
+
resource: {
|
|
254
|
+
uri: WORKSPACE_SUMMARY_URI,
|
|
255
|
+
name: "Workspace Summary",
|
|
256
|
+
description: "Read-only workspace metadata including root path, contracts config, available services, and loaded plugins.",
|
|
257
|
+
mimeType: "application/json"
|
|
258
|
+
},
|
|
259
|
+
read: async () => readWorkspaceSummary(pluginModules, workspaceRoot)
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// src/response.ts
|
|
263
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
264
|
+
var formatArtifactsSummary = (artifacts) => {
|
|
265
|
+
if (artifacts.length === 0) return "";
|
|
266
|
+
const grouped = artifacts.reduce(
|
|
267
|
+
(acc, a) => {
|
|
268
|
+
const key = a.type;
|
|
269
|
+
if (!acc[key]) acc[key] = [];
|
|
270
|
+
acc[key].push(a);
|
|
271
|
+
return acc;
|
|
272
|
+
},
|
|
273
|
+
{}
|
|
274
|
+
);
|
|
275
|
+
const lines = [];
|
|
276
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
277
|
+
lines.push(`
|
|
278
|
+
${type.toUpperCase()} (${items.length}):`);
|
|
279
|
+
for (const item of items) {
|
|
280
|
+
lines.push(` - ${item.path}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
};
|
|
285
|
+
var formatChangesSummary = (changes) => {
|
|
286
|
+
if (changes.length === 0) return "";
|
|
287
|
+
const grouped = changes.reduce(
|
|
288
|
+
(acc, c) => {
|
|
289
|
+
const key = c.kind;
|
|
290
|
+
if (!acc[key]) acc[key] = [];
|
|
291
|
+
acc[key].push(c);
|
|
292
|
+
return acc;
|
|
293
|
+
},
|
|
294
|
+
{}
|
|
295
|
+
);
|
|
296
|
+
const lines = [];
|
|
297
|
+
for (const [kind, items] of Object.entries(grouped)) {
|
|
298
|
+
lines.push(`
|
|
299
|
+
${kind} (${items.length}):`);
|
|
300
|
+
for (const item of items) {
|
|
301
|
+
const path = "path" in item ? item.path : "";
|
|
302
|
+
lines.push(` - ${path || kind}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
};
|
|
307
|
+
var formatNextActionsSummary = (next) => {
|
|
308
|
+
if (!next || next.length === 0) return "";
|
|
309
|
+
return `
|
|
310
|
+
|
|
311
|
+
NEXT STEPS:
|
|
312
|
+
${next.map((n) => ` $ ${n.command}
|
|
313
|
+
${n.description}`).join("\n")}`;
|
|
314
|
+
};
|
|
315
|
+
var formatDiagnosticsSummary = (diagnostics) => {
|
|
316
|
+
if (diagnostics.length === 0) return "";
|
|
317
|
+
return `
|
|
318
|
+
|
|
319
|
+
DIAGNOSTICS:
|
|
320
|
+
${diagnostics.map((d) => ` [${d.level.toUpperCase()}] ${d.message}`).join("\n")}`;
|
|
321
|
+
};
|
|
322
|
+
var formatDependenciesSummary = (dependencies) => {
|
|
323
|
+
if (!dependencies || dependencies.length === 0) return "";
|
|
324
|
+
const lines = ["\n## Dependencies"];
|
|
325
|
+
for (const dep of dependencies) {
|
|
326
|
+
const version = dep.version || "latest";
|
|
327
|
+
const devTag = dep.dev ? " (dev)" : "";
|
|
328
|
+
lines.push(` - ${dep.name}@${version}${devTag}`);
|
|
329
|
+
}
|
|
330
|
+
return lines.join("\n");
|
|
331
|
+
};
|
|
332
|
+
var formatEnvVarsSummary = (envVars) => {
|
|
333
|
+
if (!envVars || envVars.length === 0) return "";
|
|
334
|
+
const lines = ["\n## Environment Variables"];
|
|
335
|
+
for (const ev of envVars) {
|
|
336
|
+
const reqTag = ev.required ? " (required)" : "";
|
|
337
|
+
lines.push(` - ${ev.key}${reqTag}: ${ev.description}`);
|
|
338
|
+
}
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
};
|
|
341
|
+
var formatFilesToGenerateSummary = (files) => {
|
|
342
|
+
if (!files || files.length === 0) return "";
|
|
343
|
+
const lines = ["\n## Files for AI to Generate"];
|
|
344
|
+
for (const file of files) {
|
|
345
|
+
const tags = [file.layer, file.kind].filter(Boolean).join("/");
|
|
346
|
+
lines.push(` - ${file.path} [${tags}]`);
|
|
347
|
+
lines.push(` Intent: ${file.intent}`);
|
|
348
|
+
}
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
};
|
|
351
|
+
var formatOperationResultAsMcpResponse = (result) => {
|
|
352
|
+
const extResult = result;
|
|
353
|
+
const textParts = [`# ${result.operation}`, "", result.ok ? "\u2713 Success" : "\u2717 Failed", "", result.summary];
|
|
354
|
+
if (result.artifacts.length > 0) {
|
|
355
|
+
textParts.push("\n## Artifacts");
|
|
356
|
+
textParts.push(formatArtifactsSummary(result.artifacts));
|
|
357
|
+
}
|
|
358
|
+
if (result.changes.length > 0) {
|
|
359
|
+
textParts.push("\n## Changes (Plan)");
|
|
360
|
+
textParts.push(formatChangesSummary(result.changes));
|
|
361
|
+
}
|
|
362
|
+
textParts.push(formatFilesToGenerateSummary(extResult.files));
|
|
363
|
+
textParts.push(formatDependenciesSummary(extResult.dependencies));
|
|
364
|
+
textParts.push(formatEnvVarsSummary(extResult.envVars));
|
|
365
|
+
if (extResult.postCommands && extResult.postCommands.length > 0) {
|
|
366
|
+
textParts.push("\n## Post Commands");
|
|
367
|
+
for (const cmd of extResult.postCommands) {
|
|
368
|
+
textParts.push(` $ ${cmd}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
textParts.push(formatDiagnosticsSummary(result.diagnostics));
|
|
372
|
+
textParts.push(formatNextActionsSummary(result.next));
|
|
373
|
+
textParts.push("\n\n---\n## Raw Result (JSON)");
|
|
374
|
+
textParts.push("```json");
|
|
375
|
+
textParts.push(JSON.stringify(result, null, 2));
|
|
376
|
+
textParts.push("```");
|
|
377
|
+
return {
|
|
378
|
+
content: [
|
|
379
|
+
{
|
|
380
|
+
type: "text",
|
|
381
|
+
text: textParts.join("\n")
|
|
382
|
+
}
|
|
383
|
+
],
|
|
384
|
+
structuredContent: result,
|
|
385
|
+
isError: !result.ok
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
var formatErrorAsMcpResponse = (error, operation) => {
|
|
389
|
+
return {
|
|
390
|
+
content: [
|
|
391
|
+
{
|
|
392
|
+
type: "text",
|
|
393
|
+
text: `# ${operation}
|
|
394
|
+
|
|
395
|
+
\u2717 Error: ${error.message}`
|
|
396
|
+
}
|
|
397
|
+
],
|
|
398
|
+
structuredContent: {
|
|
399
|
+
ok: false,
|
|
400
|
+
operation,
|
|
401
|
+
summary: error.message,
|
|
402
|
+
artifacts: [],
|
|
403
|
+
changes: [],
|
|
404
|
+
diagnostics: [{ level: "error", message: error.message }]
|
|
405
|
+
},
|
|
406
|
+
isError: true
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
var formatElicitationAsMcpResponse = (elicitEffect, operation) => {
|
|
410
|
+
const fieldNames = elicitEffect.fields.map((f) => f.name);
|
|
411
|
+
const fieldDetails = elicitEffect.fields.map((f) => {
|
|
412
|
+
const parts = [`- **${f.name}**`];
|
|
413
|
+
if (f.required) parts.push(" (required)");
|
|
414
|
+
parts.push(`: ${f.prompt}`);
|
|
415
|
+
if (f.enum) parts.push(` [${f.enum.join(", ")}]`);
|
|
416
|
+
if (f.description) parts.push(`
|
|
417
|
+
${f.description}`);
|
|
418
|
+
return parts.join("");
|
|
419
|
+
}).join("\n");
|
|
420
|
+
const textParts = [
|
|
421
|
+
`# ${operation}`,
|
|
422
|
+
"",
|
|
423
|
+
"\u23F3 Missing Required Inputs",
|
|
424
|
+
"",
|
|
425
|
+
elicitEffect.message,
|
|
426
|
+
"",
|
|
427
|
+
"## Please provide:",
|
|
428
|
+
fieldDetails,
|
|
429
|
+
"",
|
|
430
|
+
"---",
|
|
431
|
+
"Re-run the tool with these parameters."
|
|
432
|
+
];
|
|
433
|
+
return {
|
|
434
|
+
content: [
|
|
435
|
+
{
|
|
436
|
+
type: "text",
|
|
437
|
+
text: textParts.join("\n")
|
|
438
|
+
}
|
|
439
|
+
],
|
|
440
|
+
structuredContent: {
|
|
441
|
+
ok: false,
|
|
442
|
+
operation,
|
|
443
|
+
summary: elicitEffect.message,
|
|
444
|
+
artifacts: [],
|
|
445
|
+
changes: [],
|
|
446
|
+
diagnostics: [{ level: "info", message: "Awaiting user input" }],
|
|
447
|
+
elicitation: {
|
|
448
|
+
fields: elicitEffect.fields,
|
|
449
|
+
missingInputs: fieldNames
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
isError: false
|
|
453
|
+
// Not an error, just needs more input
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/tools/apply-changes.ts
|
|
458
|
+
var import_facade6 = require("@crossdelta/platform-sdk/facade");
|
|
459
|
+
|
|
460
|
+
// src/audit.ts
|
|
461
|
+
var import_node_fs = require("fs");
|
|
462
|
+
var import_node_path3 = require("path");
|
|
463
|
+
var AUDIT_DIR = ".pf/audit";
|
|
464
|
+
var generateAuditFilename = (planHash, appliedAt) => {
|
|
465
|
+
const safeTimestamp = appliedAt.replace(/:/g, "-").replace(/\./g, "-");
|
|
466
|
+
return `${planHash.slice(0, 16)}-${safeTimestamp}.json`;
|
|
467
|
+
};
|
|
468
|
+
var writeAuditArtifact = (audit, workspaceRoot) => {
|
|
469
|
+
const auditDir = (0, import_node_path3.join)(workspaceRoot, AUDIT_DIR);
|
|
470
|
+
const filename = generateAuditFilename(audit.planHash, audit.appliedAt);
|
|
471
|
+
const filePath = (0, import_node_path3.join)(auditDir, filename);
|
|
472
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path3.dirname)(filePath), { recursive: true });
|
|
473
|
+
(0, import_node_fs.writeFileSync)(filePath, JSON.stringify(audit, null, 2), "utf-8");
|
|
474
|
+
return filePath;
|
|
475
|
+
};
|
|
476
|
+
var createAuditArtifact = (planHash, policyVersion, effects, result, options = {}) => ({
|
|
477
|
+
planHash,
|
|
478
|
+
reviewToken: options.reviewToken,
|
|
479
|
+
policyVersion,
|
|
480
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
481
|
+
reviewedAt: options.reviewedAt,
|
|
482
|
+
dryRun: result.dryRun,
|
|
483
|
+
summary: result.summary,
|
|
484
|
+
counts: result.counts,
|
|
485
|
+
effects,
|
|
486
|
+
results: result.results
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// src/effects/executors/deps-add.ts
|
|
490
|
+
var import_node_fs2 = require("fs");
|
|
491
|
+
var import_node_path4 = require("path");
|
|
492
|
+
|
|
493
|
+
// src/types/plan.ts
|
|
494
|
+
var DEFAULT_DEPENDENCY_POLICY = {
|
|
495
|
+
denyPatterns: ["file:", "git+", "git://", "github:", "https://", "http://"],
|
|
496
|
+
pinningStrategy: "caret"
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// src/effects/types.ts
|
|
500
|
+
var ErrorCodes = {
|
|
501
|
+
PATH_OUTSIDE_ROOT: "PATH_OUTSIDE_ROOT",
|
|
502
|
+
REQUIRES_ELICITATION: "REQUIRES_ELICITATION",
|
|
503
|
+
UNSUPPORTED_EFFECT: "UNSUPPORTED_EFFECT",
|
|
504
|
+
WRITE_FAILED: "WRITE_FAILED",
|
|
505
|
+
ATOMIC_RENAME_FAILED: "ATOMIC_RENAME_FAILED",
|
|
506
|
+
PLAN_HASH_MISMATCH: "PLAN_HASH_MISMATCH",
|
|
507
|
+
// M10: Review gate error codes
|
|
508
|
+
REVIEW_REQUIRED: "REVIEW_REQUIRED",
|
|
509
|
+
REVIEW_TOKEN_INVALID: "REVIEW_TOKEN_INVALID",
|
|
510
|
+
REVIEW_TOKEN_MISMATCH: "REVIEW_TOKEN_MISMATCH",
|
|
511
|
+
POLICY_VERSION_MISMATCH: "POLICY_VERSION_MISMATCH",
|
|
512
|
+
// Policy validation error codes
|
|
513
|
+
POLICY_VIOLATION: "POLICY_VIOLATION",
|
|
514
|
+
FORBIDDEN_DIR: "FORBIDDEN_DIR",
|
|
515
|
+
CONSOLE_IN_DOMAIN: "CONSOLE_IN_DOMAIN",
|
|
516
|
+
PROCESS_ENV_IN_DOMAIN: "PROCESS_ENV_IN_DOMAIN",
|
|
517
|
+
FETCH_IN_DOMAIN: "FETCH_IN_DOMAIN"
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/effects/executors/deps-add.ts
|
|
521
|
+
var validateDependency = (name, version, policy) => {
|
|
522
|
+
for (const pattern of policy.denyPatterns) {
|
|
523
|
+
if (name.startsWith(pattern)) {
|
|
524
|
+
return { valid: false, reason: `Package name matches denied pattern: ${pattern}` };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (version) {
|
|
528
|
+
for (const pattern of policy.denyPatterns) {
|
|
529
|
+
if (version.startsWith(pattern)) {
|
|
530
|
+
return { valid: false, reason: `Version matches denied pattern: ${pattern}` };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return { valid: true };
|
|
535
|
+
};
|
|
536
|
+
var resolveVersion = (version, policy) => {
|
|
537
|
+
if (!version || version === "latest") {
|
|
538
|
+
return "*";
|
|
539
|
+
}
|
|
540
|
+
if (/^\d/.test(version)) {
|
|
541
|
+
switch (policy.pinningStrategy) {
|
|
542
|
+
case "exact":
|
|
543
|
+
return version;
|
|
544
|
+
case "tilde":
|
|
545
|
+
return `~${version}`;
|
|
546
|
+
default:
|
|
547
|
+
return `^${version}`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return version;
|
|
551
|
+
};
|
|
552
|
+
var createSkipResult = (effect, packageJsonPath, existingVersion, dryRun) => ({
|
|
553
|
+
effect,
|
|
554
|
+
success: true,
|
|
555
|
+
status: dryRun ? "would_skip" : "skipped",
|
|
556
|
+
message: dryRun ? `Would skip: ${effect.name}@${existingVersion} already exists` : `Dependency ${effect.name}@${existingVersion} already exists`,
|
|
557
|
+
path: packageJsonPath,
|
|
558
|
+
target: effect.name,
|
|
559
|
+
reason: "dependency exists"
|
|
560
|
+
});
|
|
561
|
+
var createAddResult = (effect, packageJsonPath, version, dryRun) => ({
|
|
562
|
+
effect,
|
|
563
|
+
success: true,
|
|
564
|
+
status: dryRun ? "would_create" : "created",
|
|
565
|
+
message: dryRun ? `Would add dependency: ${effect.name}@${version}` : `Added dependency: ${effect.name}@${version}`,
|
|
566
|
+
path: packageJsonPath,
|
|
567
|
+
target: effect.name
|
|
568
|
+
});
|
|
569
|
+
var createPolicyViolationResult = (effect, reason) => ({
|
|
570
|
+
effect,
|
|
571
|
+
success: false,
|
|
572
|
+
status: "error",
|
|
573
|
+
message: `Policy violation: ${reason}`,
|
|
574
|
+
target: effect.name,
|
|
575
|
+
code: ErrorCodes.POLICY_VIOLATION,
|
|
576
|
+
reason
|
|
577
|
+
});
|
|
578
|
+
var createErrorResult = (effect, packageJsonPath, error) => ({
|
|
579
|
+
effect,
|
|
580
|
+
success: false,
|
|
581
|
+
status: "error",
|
|
582
|
+
message: `Failed to add dependency ${effect.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
583
|
+
path: packageJsonPath,
|
|
584
|
+
target: effect.name,
|
|
585
|
+
code: ErrorCodes.WRITE_FAILED
|
|
586
|
+
});
|
|
587
|
+
var createMissingPackageJsonResult = (effect, packageJsonPath) => ({
|
|
588
|
+
effect,
|
|
589
|
+
success: false,
|
|
590
|
+
status: "error",
|
|
591
|
+
message: `package.json not found at ${packageJsonPath}`,
|
|
592
|
+
path: packageJsonPath,
|
|
593
|
+
target: effect.name,
|
|
594
|
+
code: ErrorCodes.WRITE_FAILED
|
|
595
|
+
});
|
|
596
|
+
var executeDepsAdd = (effect, workspaceRoot, options, policy = DEFAULT_DEPENDENCY_POLICY) => {
|
|
597
|
+
const { name, version, dev = false, targetDir } = effect;
|
|
598
|
+
const { dryRun = false } = options;
|
|
599
|
+
const baseDir = targetDir ? (0, import_node_path4.join)(workspaceRoot, targetDir) : workspaceRoot;
|
|
600
|
+
const packageJsonPath = (0, import_node_path4.join)(baseDir, "package.json");
|
|
601
|
+
const validation = validateDependency(name, version, policy);
|
|
602
|
+
if (!validation.valid) {
|
|
603
|
+
return createPolicyViolationResult(effect, validation.reason);
|
|
604
|
+
}
|
|
605
|
+
if (!(0, import_node_fs2.existsSync)(packageJsonPath)) {
|
|
606
|
+
return createMissingPackageJsonResult(effect, packageJsonPath);
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
const packageJson = JSON.parse((0, import_node_fs2.readFileSync)(packageJsonPath, "utf-8"));
|
|
610
|
+
const depsKey = dev ? "devDependencies" : "dependencies";
|
|
611
|
+
const deps = packageJson[depsKey] ?? {};
|
|
612
|
+
if (name in deps) {
|
|
613
|
+
return createSkipResult(effect, packageJsonPath, deps[name], dryRun);
|
|
614
|
+
}
|
|
615
|
+
const resolvedVersion = resolveVersion(version, policy);
|
|
616
|
+
if (!dryRun) {
|
|
617
|
+
packageJson[depsKey] = {
|
|
618
|
+
...deps,
|
|
619
|
+
[name]: resolvedVersion
|
|
620
|
+
};
|
|
621
|
+
packageJson[depsKey] = Object.keys(packageJson[depsKey]).sort().reduce(
|
|
622
|
+
(acc, key) => {
|
|
623
|
+
acc[key] = packageJson[depsKey][key];
|
|
624
|
+
return acc;
|
|
625
|
+
},
|
|
626
|
+
{}
|
|
627
|
+
);
|
|
628
|
+
(0, import_node_fs2.writeFileSync)(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
|
|
629
|
+
`, "utf-8");
|
|
630
|
+
}
|
|
631
|
+
return createAddResult(effect, packageJsonPath, resolvedVersion, dryRun);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
return createErrorResult(effect, packageJsonPath, error);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/effects/utils.ts
|
|
638
|
+
var import_node_crypto = require("crypto");
|
|
639
|
+
var import_node_fs3 = require("fs");
|
|
640
|
+
var import_node_path5 = require("path");
|
|
641
|
+
var normalizeLineEndings = (content) => content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
642
|
+
var resolveSafePath = (targetPath, workspaceRoot) => {
|
|
643
|
+
const resolvedRoot = (0, import_node_path5.resolve)(workspaceRoot);
|
|
644
|
+
const absolutePath = targetPath.startsWith("/") ? (0, import_node_path5.resolve)(targetPath) : (0, import_node_path5.resolve)(workspaceRoot, targetPath);
|
|
645
|
+
const isWithinRoot = absolutePath.startsWith(`${resolvedRoot}/`) || absolutePath === resolvedRoot;
|
|
646
|
+
return isWithinRoot ? { safe: true, absolutePath } : { safe: false, reason: `Path "${targetPath}" resolves outside workspace root` };
|
|
647
|
+
};
|
|
648
|
+
var readFileSafe = (absolutePath) => (0, import_node_fs3.existsSync)(absolutePath) ? (0, import_node_fs3.readFileSync)(absolutePath, "utf-8") : void 0;
|
|
649
|
+
var atomicWriteFile = (absolutePath, content) => {
|
|
650
|
+
const dir = (0, import_node_path5.dirname)(absolutePath);
|
|
651
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
652
|
+
const tempPath = (0, import_node_path5.join)(dir, `.tmp-${(0, import_node_crypto.randomUUID)()}`);
|
|
653
|
+
try {
|
|
654
|
+
(0, import_node_fs3.writeFileSync)(tempPath, content, "utf-8");
|
|
655
|
+
(0, import_node_fs3.renameSync)(tempPath, absolutePath);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
cleanupTempFile(tempPath);
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
var cleanupTempFile = (tempPath) => {
|
|
662
|
+
try {
|
|
663
|
+
if ((0, import_node_fs3.existsSync)(tempPath)) {
|
|
664
|
+
(0, import_node_fs3.unlinkSync)(tempPath);
|
|
665
|
+
}
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/effects/executors/env-add.ts
|
|
671
|
+
var createSkipResult2 = (effect, key, envPath, dryRun) => ({
|
|
672
|
+
effect,
|
|
673
|
+
success: true,
|
|
674
|
+
status: dryRun ? "would_skip" : "skipped",
|
|
675
|
+
message: dryRun ? `Would skip: ${key} already exists` : `Env var ${key} already exists`,
|
|
676
|
+
path: envPath,
|
|
677
|
+
target: key,
|
|
678
|
+
reason: "key exists"
|
|
679
|
+
});
|
|
680
|
+
var createAddResult2 = (effect, key, envPath, dryRun) => ({
|
|
681
|
+
effect,
|
|
682
|
+
success: true,
|
|
683
|
+
status: dryRun ? "would_create" : "created",
|
|
684
|
+
message: dryRun ? `Would add env var: ${key}` : `Added env var: ${key}`,
|
|
685
|
+
path: envPath,
|
|
686
|
+
target: key
|
|
687
|
+
});
|
|
688
|
+
var createErrorResult2 = (effect, key, envPath, error) => ({
|
|
689
|
+
effect,
|
|
690
|
+
success: false,
|
|
691
|
+
status: "error",
|
|
692
|
+
message: `Failed to add env var ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
|
693
|
+
path: envPath,
|
|
694
|
+
target: key,
|
|
695
|
+
code: ErrorCodes.WRITE_FAILED
|
|
696
|
+
});
|
|
697
|
+
var keyExists = (content, key) => {
|
|
698
|
+
const keyRegex = new RegExp(`^${key}=`, "m");
|
|
699
|
+
return keyRegex.test(content);
|
|
700
|
+
};
|
|
701
|
+
var buildNewContent = (existingContent, key, value) => {
|
|
702
|
+
const normalized = normalizeLineEndings(existingContent);
|
|
703
|
+
const needsNewline = normalized.length > 0 && !normalized.endsWith("\n");
|
|
704
|
+
const prefix = needsNewline ? "\n" : "";
|
|
705
|
+
return `${normalized}${prefix}${key}=${value}
|
|
706
|
+
`;
|
|
707
|
+
};
|
|
708
|
+
var executeEnvAdd = (effect, workspaceRoot, options) => {
|
|
709
|
+
const { key, value } = effect;
|
|
710
|
+
const { dryRun = false } = options;
|
|
711
|
+
const envPath = `${workspaceRoot}/.env.local`;
|
|
712
|
+
try {
|
|
713
|
+
const existingContent = readFileSafe(envPath) ?? "";
|
|
714
|
+
if (keyExists(existingContent, key)) {
|
|
715
|
+
return createSkipResult2(effect, key, envPath, dryRun);
|
|
716
|
+
}
|
|
717
|
+
if (!dryRun) {
|
|
718
|
+
const newContent = buildNewContent(existingContent, key, value);
|
|
719
|
+
atomicWriteFile(envPath, newContent);
|
|
720
|
+
}
|
|
721
|
+
return createAddResult2(effect, key, envPath, dryRun);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
return createErrorResult2(effect, key, envPath, error);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// src/plan-hash.ts
|
|
728
|
+
var import_node_crypto2 = require("crypto");
|
|
729
|
+
var stableStringify = (value) => {
|
|
730
|
+
if (value === null || value === void 0) {
|
|
731
|
+
return JSON.stringify(value);
|
|
732
|
+
}
|
|
733
|
+
if (Array.isArray(value)) {
|
|
734
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
735
|
+
}
|
|
736
|
+
if (typeof value === "object") {
|
|
737
|
+
const obj = value;
|
|
738
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
739
|
+
const pairs = sortedKeys.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`);
|
|
740
|
+
return `{${pairs.join(",")}}`;
|
|
741
|
+
}
|
|
742
|
+
return JSON.stringify(value);
|
|
743
|
+
};
|
|
744
|
+
var computePlanHash = (changes) => {
|
|
745
|
+
const normalized = stableStringify(changes);
|
|
746
|
+
return (0, import_node_crypto2.createHash)("sha256").update(normalized, "utf-8").digest("hex");
|
|
747
|
+
};
|
|
748
|
+
var computeContentHash = (content) => (0, import_node_crypto2.createHash)("sha256").update(content, "utf-8").digest("hex");
|
|
749
|
+
|
|
750
|
+
// src/effects/executors/file-write.ts
|
|
751
|
+
var createPathSafetyError = (effect, relativePath, reason) => ({
|
|
752
|
+
effect,
|
|
753
|
+
success: false,
|
|
754
|
+
status: "error",
|
|
755
|
+
message: reason,
|
|
756
|
+
target: relativePath,
|
|
757
|
+
code: ErrorCodes.PATH_OUTSIDE_ROOT
|
|
758
|
+
});
|
|
759
|
+
var createSkipResult3 = (effect, relativePath, absolutePath, dryRun, existingContent, newContent) => ({
|
|
760
|
+
effect,
|
|
761
|
+
success: true,
|
|
762
|
+
status: dryRun ? "would_skip" : "skipped",
|
|
763
|
+
message: dryRun ? `Would skip (unchanged): ${relativePath}` : `File unchanged: ${relativePath}`,
|
|
764
|
+
path: absolutePath,
|
|
765
|
+
target: relativePath,
|
|
766
|
+
reason: "content identical",
|
|
767
|
+
preview: {
|
|
768
|
+
oldBytes: existingContent.length,
|
|
769
|
+
newBytes: newContent.length,
|
|
770
|
+
oldHash: computeContentHash(existingContent),
|
|
771
|
+
newHash: computeContentHash(newContent),
|
|
772
|
+
wouldOverwrite: false,
|
|
773
|
+
existingLength: existingContent.length,
|
|
774
|
+
newLength: newContent.length
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
var createUpdateResult = (effect, relativePath, absolutePath, dryRun, existingContent, newContent) => ({
|
|
778
|
+
effect,
|
|
779
|
+
success: true,
|
|
780
|
+
status: dryRun ? "would_update" : "updated",
|
|
781
|
+
message: dryRun ? `Would update: ${relativePath}` : `Updated: ${relativePath}`,
|
|
782
|
+
path: absolutePath,
|
|
783
|
+
target: relativePath,
|
|
784
|
+
reason: "content differs",
|
|
785
|
+
preview: {
|
|
786
|
+
oldBytes: existingContent.length,
|
|
787
|
+
newBytes: newContent.length,
|
|
788
|
+
oldHash: computeContentHash(existingContent),
|
|
789
|
+
newHash: computeContentHash(newContent),
|
|
790
|
+
wouldOverwrite: true,
|
|
791
|
+
existingLength: existingContent.length,
|
|
792
|
+
newLength: newContent.length
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
var createCreateResult = (effect, relativePath, absolutePath, dryRun, newContent) => ({
|
|
796
|
+
effect,
|
|
797
|
+
success: true,
|
|
798
|
+
status: dryRun ? "would_create" : "created",
|
|
799
|
+
message: dryRun ? `Would create: ${relativePath}` : `Created: ${relativePath}`,
|
|
800
|
+
path: absolutePath,
|
|
801
|
+
target: relativePath,
|
|
802
|
+
preview: {
|
|
803
|
+
newBytes: newContent.length,
|
|
804
|
+
newHash: computeContentHash(newContent),
|
|
805
|
+
wouldOverwrite: false,
|
|
806
|
+
newLength: newContent.length
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
var createWriteError = (effect, relativePath, absolutePath, error) => ({
|
|
810
|
+
effect,
|
|
811
|
+
success: false,
|
|
812
|
+
status: "error",
|
|
813
|
+
message: `Failed to write ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
814
|
+
path: absolutePath,
|
|
815
|
+
target: relativePath,
|
|
816
|
+
code: ErrorCodes.WRITE_FAILED
|
|
817
|
+
});
|
|
818
|
+
var decideWriteAction = (absolutePath, normalizedContent) => {
|
|
819
|
+
const existing = readFileSafe(absolutePath);
|
|
820
|
+
if (existing === void 0) {
|
|
821
|
+
return { action: "create" };
|
|
822
|
+
}
|
|
823
|
+
const normalizedExisting = normalizeLineEndings(existing);
|
|
824
|
+
return normalizedExisting === normalizedContent ? { action: "skip", existingContent: normalizedExisting } : { action: "update", existingContent: normalizedExisting };
|
|
825
|
+
};
|
|
826
|
+
var executeFileWrite = (effect, workspaceRoot, options) => {
|
|
827
|
+
const { path: relativePath, content } = effect;
|
|
828
|
+
const { dryRun = false } = options;
|
|
829
|
+
const pathResult = resolveSafePath(relativePath, workspaceRoot);
|
|
830
|
+
if (!pathResult.safe) {
|
|
831
|
+
return createPathSafetyError(effect, relativePath, pathResult.reason);
|
|
832
|
+
}
|
|
833
|
+
const { absolutePath } = pathResult;
|
|
834
|
+
const normalizedContent = normalizeLineEndings(content);
|
|
835
|
+
try {
|
|
836
|
+
const decision = decideWriteAction(absolutePath, normalizedContent);
|
|
837
|
+
switch (decision.action) {
|
|
838
|
+
case "skip":
|
|
839
|
+
return createSkipResult3(effect, relativePath, absolutePath, dryRun, decision.existingContent, normalizedContent);
|
|
840
|
+
case "update":
|
|
841
|
+
if (!dryRun) {
|
|
842
|
+
atomicWriteFile(absolutePath, normalizedContent);
|
|
843
|
+
}
|
|
844
|
+
return createUpdateResult(
|
|
845
|
+
effect,
|
|
846
|
+
relativePath,
|
|
847
|
+
absolutePath,
|
|
848
|
+
dryRun,
|
|
849
|
+
decision.existingContent,
|
|
850
|
+
normalizedContent
|
|
851
|
+
);
|
|
852
|
+
case "create":
|
|
853
|
+
if (!dryRun) {
|
|
854
|
+
atomicWriteFile(absolutePath, normalizedContent);
|
|
855
|
+
}
|
|
856
|
+
return createCreateResult(effect, relativePath, absolutePath, dryRun, normalizedContent);
|
|
857
|
+
}
|
|
858
|
+
} catch (error) {
|
|
859
|
+
return createWriteError(effect, relativePath, absolutePath, error);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// src/effects/executors/infra-add.ts
|
|
864
|
+
var createPathSafetyError2 = (effect, relativePath, reason) => ({
|
|
865
|
+
effect,
|
|
866
|
+
success: false,
|
|
867
|
+
status: "error",
|
|
868
|
+
message: reason,
|
|
869
|
+
target: relativePath,
|
|
870
|
+
code: ErrorCodes.PATH_OUTSIDE_ROOT
|
|
871
|
+
});
|
|
872
|
+
var createSkipResult4 = (effect, serviceName, absolutePath, relativePath, dryRun) => ({
|
|
873
|
+
effect,
|
|
874
|
+
success: true,
|
|
875
|
+
status: dryRun ? "would_skip" : "skipped",
|
|
876
|
+
message: dryRun ? `Would skip (unchanged): ${serviceName}` : `Infra config unchanged: ${serviceName}`,
|
|
877
|
+
path: absolutePath,
|
|
878
|
+
target: relativePath,
|
|
879
|
+
reason: "content identical"
|
|
880
|
+
});
|
|
881
|
+
var createUpdateResult2 = (effect, serviceName, absolutePath, relativePath, dryRun) => ({
|
|
882
|
+
effect,
|
|
883
|
+
success: true,
|
|
884
|
+
status: dryRun ? "would_update" : "updated",
|
|
885
|
+
message: dryRun ? `Would update infra config: ${serviceName}` : `Updated infra config: ${serviceName}`,
|
|
886
|
+
path: absolutePath,
|
|
887
|
+
target: relativePath
|
|
888
|
+
});
|
|
889
|
+
var createCreateResult2 = (effect, serviceName, absolutePath, relativePath, dryRun) => ({
|
|
890
|
+
effect,
|
|
891
|
+
success: true,
|
|
892
|
+
status: dryRun ? "would_create" : "created",
|
|
893
|
+
message: dryRun ? `Would create infra config: ${serviceName}` : `Created infra config: ${serviceName}`,
|
|
894
|
+
path: absolutePath,
|
|
895
|
+
target: relativePath
|
|
896
|
+
});
|
|
897
|
+
var createErrorResult3 = (effect, serviceName, absolutePath, relativePath, error) => ({
|
|
898
|
+
effect,
|
|
899
|
+
success: false,
|
|
900
|
+
status: "error",
|
|
901
|
+
message: `Failed to create infra config for ${serviceName}: ${error instanceof Error ? error.message : String(error)}`,
|
|
902
|
+
path: absolutePath,
|
|
903
|
+
target: relativePath,
|
|
904
|
+
code: ErrorCodes.WRITE_FAILED
|
|
905
|
+
});
|
|
906
|
+
var generateInfraContent = (serviceName, port) => `import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
|
|
907
|
+
|
|
908
|
+
const config: K8sServiceConfig = {
|
|
909
|
+
name: '${serviceName}',
|
|
910
|
+
ports: ports().http(${port}).build(),
|
|
911
|
+
replicas: 1,
|
|
912
|
+
healthCheck: { httpPath: '/health' },
|
|
913
|
+
resources: {
|
|
914
|
+
requests: { cpu: '50m', memory: '64Mi' },
|
|
915
|
+
limits: { cpu: '150m', memory: '128Mi' },
|
|
916
|
+
},
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export default config
|
|
920
|
+
`;
|
|
921
|
+
var decideInfraAction = (absolutePath, normalizedContent) => {
|
|
922
|
+
const existing = readFileSafe(absolutePath);
|
|
923
|
+
if (existing === void 0) {
|
|
924
|
+
return { action: "create" };
|
|
925
|
+
}
|
|
926
|
+
const normalizedExisting = normalizeLineEndings(existing);
|
|
927
|
+
return normalizedExisting === normalizedContent ? { action: "skip" } : { action: "update" };
|
|
928
|
+
};
|
|
929
|
+
var executeInfraAdd = (effect, workspaceRoot, options) => {
|
|
930
|
+
const { serviceName, port } = effect;
|
|
931
|
+
const { dryRun = false } = options;
|
|
932
|
+
const relativePath = `infra/services/${serviceName}.ts`;
|
|
933
|
+
const pathResult = resolveSafePath(relativePath, workspaceRoot);
|
|
934
|
+
if (!pathResult.safe) {
|
|
935
|
+
return createPathSafetyError2(effect, relativePath, pathResult.reason);
|
|
936
|
+
}
|
|
937
|
+
const { absolutePath } = pathResult;
|
|
938
|
+
const content = generateInfraContent(serviceName, port);
|
|
939
|
+
const normalizedContent = normalizeLineEndings(content);
|
|
940
|
+
try {
|
|
941
|
+
const decision = decideInfraAction(absolutePath, normalizedContent);
|
|
942
|
+
switch (decision.action) {
|
|
943
|
+
case "skip":
|
|
944
|
+
return createSkipResult4(effect, serviceName, absolutePath, relativePath, dryRun);
|
|
945
|
+
case "update":
|
|
946
|
+
if (!dryRun) {
|
|
947
|
+
atomicWriteFile(absolutePath, content);
|
|
948
|
+
}
|
|
949
|
+
return createUpdateResult2(effect, serviceName, absolutePath, relativePath, dryRun);
|
|
950
|
+
case "create":
|
|
951
|
+
if (!dryRun) {
|
|
952
|
+
atomicWriteFile(absolutePath, content);
|
|
953
|
+
}
|
|
954
|
+
return createCreateResult2(effect, serviceName, absolutePath, relativePath, dryRun);
|
|
955
|
+
}
|
|
956
|
+
} catch (error) {
|
|
957
|
+
return createErrorResult3(effect, serviceName, absolutePath, relativePath, error);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
// src/effects/summary.ts
|
|
962
|
+
var statusCountMap = {
|
|
963
|
+
created: "created",
|
|
964
|
+
updated: "updated",
|
|
965
|
+
skipped: "skipped",
|
|
966
|
+
error: "error",
|
|
967
|
+
would_create: "wouldCreate",
|
|
968
|
+
would_update: "wouldUpdate",
|
|
969
|
+
would_skip: "wouldSkip"
|
|
970
|
+
};
|
|
971
|
+
var initialCounts = {
|
|
972
|
+
created: 0,
|
|
973
|
+
updated: 0,
|
|
974
|
+
skipped: 0,
|
|
975
|
+
error: 0,
|
|
976
|
+
wouldCreate: 0,
|
|
977
|
+
wouldUpdate: 0,
|
|
978
|
+
wouldSkip: 0
|
|
979
|
+
};
|
|
980
|
+
var initialKindCounts = { total: 0, created: 0, updated: 0, skipped: 0, error: 0 };
|
|
981
|
+
var createInitialCounts = () => ({ ...initialCounts });
|
|
982
|
+
var incrementCount = (counts, key) => ({
|
|
983
|
+
...counts,
|
|
984
|
+
[key]: counts[key] + 1
|
|
985
|
+
});
|
|
986
|
+
var aggregateCounts = (results) => results.reduce((acc, r) => {
|
|
987
|
+
const key = statusCountMap[r.status];
|
|
988
|
+
return key ? incrementCount(acc, key) : acc;
|
|
989
|
+
}, createInitialCounts());
|
|
990
|
+
var dryRunSummaryParts = (counts) => [
|
|
991
|
+
counts.wouldCreate > 0 ? `${counts.wouldCreate} would be created` : "",
|
|
992
|
+
counts.wouldUpdate > 0 ? `${counts.wouldUpdate} would be updated` : "",
|
|
993
|
+
counts.wouldSkip > 0 ? `${counts.wouldSkip} would be skipped` : ""
|
|
994
|
+
].filter(Boolean);
|
|
995
|
+
var normalSummaryParts = (counts) => [
|
|
996
|
+
counts.created > 0 ? `${counts.created} created` : "",
|
|
997
|
+
counts.updated > 0 ? `${counts.updated} updated` : "",
|
|
998
|
+
counts.skipped > 0 ? `${counts.skipped} skipped` : ""
|
|
999
|
+
].filter(Boolean);
|
|
1000
|
+
var buildSummary = (counts, dryRun) => {
|
|
1001
|
+
const modeParts = dryRun ? dryRunSummaryParts(counts) : normalSummaryParts(counts);
|
|
1002
|
+
const errorPart = counts.error > 0 ? [`${counts.error} errors`] : [];
|
|
1003
|
+
const parts = [...modeParts, ...errorPart];
|
|
1004
|
+
return parts.length > 0 ? parts.join(", ") : "No effects to apply";
|
|
1005
|
+
};
|
|
1006
|
+
var statusToKindKey = (status) => {
|
|
1007
|
+
switch (status) {
|
|
1008
|
+
case "created":
|
|
1009
|
+
case "would_create":
|
|
1010
|
+
return "created";
|
|
1011
|
+
case "updated":
|
|
1012
|
+
case "would_update":
|
|
1013
|
+
return "updated";
|
|
1014
|
+
case "skipped":
|
|
1015
|
+
case "would_skip":
|
|
1016
|
+
return "skipped";
|
|
1017
|
+
case "error":
|
|
1018
|
+
return "error";
|
|
1019
|
+
default:
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
var incrementKindCounts = (counts, status) => {
|
|
1024
|
+
const key = statusToKindKey(status);
|
|
1025
|
+
return key ? { ...counts, total: counts.total + 1, [key]: counts[key] + 1 } : { ...counts, total: counts.total + 1 };
|
|
1026
|
+
};
|
|
1027
|
+
var updateByKind = (byKind, kind, status) => ({
|
|
1028
|
+
...byKind,
|
|
1029
|
+
[kind]: incrementKindCounts(byKind[kind] ?? { ...initialKindCounts }, status)
|
|
1030
|
+
});
|
|
1031
|
+
var buildStructuredSummary = (results) => results.reduce(
|
|
1032
|
+
(acc, r) => ({
|
|
1033
|
+
byKind: updateByKind(acc.byKind, r.effect.kind, r.status),
|
|
1034
|
+
totalNewBytes: acc.totalNewBytes + (r.preview?.newBytes ?? 0),
|
|
1035
|
+
totalOldBytes: acc.totalOldBytes + (r.preview?.oldBytes ?? 0)
|
|
1036
|
+
}),
|
|
1037
|
+
{ byKind: {}, totalNewBytes: 0, totalOldBytes: 0 }
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
// src/effects/validation.ts
|
|
1041
|
+
var import_facade5 = require("@crossdelta/platform-sdk/facade");
|
|
1042
|
+
var extractPlannedFiles = (effects) => effects.filter((e) => e.kind === "file:write").map((e) => ({ path: e.path, content: e.content }));
|
|
1043
|
+
var detectFramework = (effects) => {
|
|
1044
|
+
const infraEffect = effects.find((e) => e.kind === "infra:add");
|
|
1045
|
+
if (!infraEffect) return void 0;
|
|
1046
|
+
const frameworkMap = {
|
|
1047
|
+
hono: "hono",
|
|
1048
|
+
nest: "nestjs",
|
|
1049
|
+
nestjs: "nestjs"
|
|
1050
|
+
};
|
|
1051
|
+
return frameworkMap[infraEffect.framework];
|
|
1052
|
+
};
|
|
1053
|
+
var createPolicyViolationResult2 = (violations, dryRun) => ({
|
|
1054
|
+
ok: false,
|
|
1055
|
+
summary: `Policy violations: ${violations.length} issue(s) found`,
|
|
1056
|
+
results: violations.map((v) => ({
|
|
1057
|
+
effect: { kind: "file:write", path: v.path, content: "" },
|
|
1058
|
+
success: false,
|
|
1059
|
+
status: "error",
|
|
1060
|
+
message: v.message,
|
|
1061
|
+
path: v.path,
|
|
1062
|
+
target: v.path,
|
|
1063
|
+
code: v.code
|
|
1064
|
+
})),
|
|
1065
|
+
counts: {
|
|
1066
|
+
created: 0,
|
|
1067
|
+
updated: 0,
|
|
1068
|
+
skipped: 0,
|
|
1069
|
+
error: violations.length,
|
|
1070
|
+
wouldCreate: 0,
|
|
1071
|
+
wouldUpdate: 0,
|
|
1072
|
+
wouldSkip: 0
|
|
1073
|
+
},
|
|
1074
|
+
dryRun,
|
|
1075
|
+
code: ErrorCodes.POLICY_VIOLATION,
|
|
1076
|
+
policyViolations: [...violations]
|
|
1077
|
+
});
|
|
1078
|
+
var validateEffects = (effects, framework, dryRun) => {
|
|
1079
|
+
if (!framework) {
|
|
1080
|
+
return { valid: true };
|
|
1081
|
+
}
|
|
1082
|
+
const plannedFiles = extractPlannedFiles(effects);
|
|
1083
|
+
const validationResult = (0, import_facade5.validateGeneratedFiles)(framework, plannedFiles);
|
|
1084
|
+
if (validationResult.valid) {
|
|
1085
|
+
return { valid: true };
|
|
1086
|
+
}
|
|
1087
|
+
return {
|
|
1088
|
+
valid: false,
|
|
1089
|
+
result: createPolicyViolationResult2(validationResult.violations, dryRun)
|
|
1090
|
+
};
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
// src/effects/index.ts
|
|
1094
|
+
var effectExecutors = {
|
|
1095
|
+
"file:write": executeFileWrite,
|
|
1096
|
+
"env:add": executeEnvAdd,
|
|
1097
|
+
"infra:add": executeInfraAdd,
|
|
1098
|
+
"deps:add": executeDepsAdd
|
|
1099
|
+
};
|
|
1100
|
+
var isElicitInputEffect = (effect) => effect.kind === "elicit:input";
|
|
1101
|
+
var createElicitationRejection = (elicitEffects, dryRun) => ({
|
|
1102
|
+
ok: false,
|
|
1103
|
+
summary: "Cannot apply: Plan requires user input (elicitation pending)",
|
|
1104
|
+
results: elicitEffects.map((effect) => ({
|
|
1105
|
+
effect,
|
|
1106
|
+
success: false,
|
|
1107
|
+
status: "error",
|
|
1108
|
+
message: "ElicitInputEffect cannot be applied - requires host interaction",
|
|
1109
|
+
code: ErrorCodes.REQUIRES_ELICITATION
|
|
1110
|
+
})),
|
|
1111
|
+
counts: {
|
|
1112
|
+
...createInitialCounts(),
|
|
1113
|
+
error: elicitEffects.length
|
|
1114
|
+
},
|
|
1115
|
+
dryRun
|
|
1116
|
+
});
|
|
1117
|
+
var createUnsupportedEffectResult = (effect) => ({
|
|
1118
|
+
effect,
|
|
1119
|
+
success: false,
|
|
1120
|
+
status: "error",
|
|
1121
|
+
message: `Unsupported effect kind: ${effect.kind}`,
|
|
1122
|
+
code: ErrorCodes.UNSUPPORTED_EFFECT
|
|
1123
|
+
});
|
|
1124
|
+
var applyEffects = (effects, workspaceRoot, options = {}) => {
|
|
1125
|
+
const { dryRun = false, framework: explicitFramework } = options;
|
|
1126
|
+
const elicitEffects = effects.filter(isElicitInputEffect);
|
|
1127
|
+
if (elicitEffects.length > 0) {
|
|
1128
|
+
return createElicitationRejection(elicitEffects, dryRun);
|
|
1129
|
+
}
|
|
1130
|
+
const framework = explicitFramework ?? detectFramework(effects);
|
|
1131
|
+
const validation = validateEffects(effects, framework, dryRun);
|
|
1132
|
+
if (!validation.valid) {
|
|
1133
|
+
return validation.result;
|
|
1134
|
+
}
|
|
1135
|
+
const executeEffect = (effect) => {
|
|
1136
|
+
const executor = effectExecutors[effect.kind];
|
|
1137
|
+
return executor ? executor(effect, workspaceRoot, options) : createUnsupportedEffectResult(effect);
|
|
1138
|
+
};
|
|
1139
|
+
const results = effects.map(executeEffect);
|
|
1140
|
+
const counts = aggregateCounts(results);
|
|
1141
|
+
return {
|
|
1142
|
+
ok: counts.error === 0,
|
|
1143
|
+
summary: buildSummary(counts, dryRun),
|
|
1144
|
+
results,
|
|
1145
|
+
counts,
|
|
1146
|
+
dryRun,
|
|
1147
|
+
structuredSummary: buildStructuredSummary(results)
|
|
1148
|
+
};
|
|
1149
|
+
};
|
|
1150
|
+
var isEffectSupported = (kind) => kind in effectExecutors;
|
|
1151
|
+
|
|
1152
|
+
// src/policy.ts
|
|
1153
|
+
var POLICY_VERSION = "2025-01-03";
|
|
1154
|
+
var generateReviewToken = (planHash, policyVersion = POLICY_VERSION) => `${planHash}.${policyVersion}`;
|
|
1155
|
+
var parseReviewToken = (token) => {
|
|
1156
|
+
const dotIndex = token.indexOf(".");
|
|
1157
|
+
if (dotIndex === -1) return null;
|
|
1158
|
+
const planHash = token.slice(0, dotIndex);
|
|
1159
|
+
const policyVersion = token.slice(dotIndex + 1);
|
|
1160
|
+
if (planHash.length !== 64 || !/^[a-f0-9]+$/i.test(planHash)) return null;
|
|
1161
|
+
if (!policyVersion || policyVersion.length === 0) return null;
|
|
1162
|
+
return { planHash, policyVersion };
|
|
1163
|
+
};
|
|
1164
|
+
var validateReviewToken = (token, expectedPlanHash, expectedPolicyVersion = POLICY_VERSION) => {
|
|
1165
|
+
const parsed = parseReviewToken(token);
|
|
1166
|
+
if (!parsed) {
|
|
1167
|
+
return {
|
|
1168
|
+
valid: false,
|
|
1169
|
+
code: "REVIEW_TOKEN_INVALID",
|
|
1170
|
+
message: "Invalid review token format (expected: planHash.policyVersion)"
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
if (parsed.planHash !== expectedPlanHash) {
|
|
1174
|
+
return {
|
|
1175
|
+
valid: false,
|
|
1176
|
+
code: "REVIEW_TOKEN_MISMATCH",
|
|
1177
|
+
message: `Plan hash mismatch: token has ${parsed.planHash.slice(0, 8)}..., expected ${expectedPlanHash.slice(0, 8)}...`,
|
|
1178
|
+
parsed
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
if (parsed.policyVersion !== expectedPolicyVersion) {
|
|
1182
|
+
return {
|
|
1183
|
+
valid: false,
|
|
1184
|
+
code: "POLICY_VERSION_MISMATCH",
|
|
1185
|
+
message: `Policy version mismatch: token has ${parsed.policyVersion}, expected ${expectedPolicyVersion}`,
|
|
1186
|
+
parsed
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
return { valid: true, parsed };
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
// src/tools/apply-changes.ts
|
|
1193
|
+
var validateChanges = (changes) => {
|
|
1194
|
+
if (!Array.isArray(changes)) return false;
|
|
1195
|
+
return changes.every((c) => typeof c === "object" && c !== null && "kind" in c && typeof c.kind === "string");
|
|
1196
|
+
};
|
|
1197
|
+
var createFailure = (summary, errorMsg, infoMsg, code) => ({
|
|
1198
|
+
ok: false,
|
|
1199
|
+
operation: "apply_changes",
|
|
1200
|
+
summary,
|
|
1201
|
+
artifacts: [],
|
|
1202
|
+
changes: [],
|
|
1203
|
+
diagnostics: [
|
|
1204
|
+
{ level: "error", message: errorMsg },
|
|
1205
|
+
...infoMsg ? [{ level: "info", message: infoMsg }] : []
|
|
1206
|
+
],
|
|
1207
|
+
data: code ? {
|
|
1208
|
+
ok: false,
|
|
1209
|
+
dryRun: false,
|
|
1210
|
+
summary,
|
|
1211
|
+
results: [],
|
|
1212
|
+
counts: { created: 0, updated: 0, skipped: 0, error: 1, wouldCreate: 0, wouldUpdate: 0, wouldSkip: 0 },
|
|
1213
|
+
code: ErrorCodes[code]
|
|
1214
|
+
} : void 0
|
|
1215
|
+
});
|
|
1216
|
+
var checkReviewGate = (requireReview, reviewToken, actualHash, policyVer) => {
|
|
1217
|
+
if (requireReview !== true) return null;
|
|
1218
|
+
if (typeof reviewToken !== "string") {
|
|
1219
|
+
return createFailure(
|
|
1220
|
+
"Review required but no reviewToken provided",
|
|
1221
|
+
"requireReview is enabled but reviewToken was not provided",
|
|
1222
|
+
"Run plan_review first to get a reviewToken, then pass it to apply_changes",
|
|
1223
|
+
"REVIEW_REQUIRED"
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
const validation = validateReviewToken(reviewToken, actualHash, policyVer);
|
|
1227
|
+
if (!validation.valid) {
|
|
1228
|
+
const info = validation.code === "POLICY_VERSION_MISMATCH" ? "Policy version has changed. Re-run plan_review to get a new reviewToken." : "Re-run plan_review to get a valid reviewToken for the current plan";
|
|
1229
|
+
return createFailure(
|
|
1230
|
+
`Review token validation failed: ${validation.message}`,
|
|
1231
|
+
validation.message ?? "Review token validation failed",
|
|
1232
|
+
info,
|
|
1233
|
+
validation.code
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
return null;
|
|
1237
|
+
};
|
|
1238
|
+
var checkPlanHash = (expectedHash, actualHash) => {
|
|
1239
|
+
if (typeof expectedHash !== "string") return null;
|
|
1240
|
+
if (actualHash === expectedHash) return null;
|
|
1241
|
+
return createFailure(
|
|
1242
|
+
"Plan hash mismatch: changes have been modified since review",
|
|
1243
|
+
`Expected hash: ${expectedHash}, got: ${actualHash}`,
|
|
1244
|
+
"Re-run plan_review to get the updated hash, or remove expectedPlanHash to skip verification",
|
|
1245
|
+
"PLAN_HASH_MISMATCH"
|
|
1246
|
+
);
|
|
1247
|
+
};
|
|
1248
|
+
var writeAuditAndGetInfo = (planHash, policyVersion, changes, result, reviewToken, workspaceRoot) => {
|
|
1249
|
+
if (!result.ok || result.dryRun) return null;
|
|
1250
|
+
const audit = createAuditArtifact(planHash, policyVersion, changes, result, {
|
|
1251
|
+
reviewToken: typeof reviewToken === "string" ? reviewToken : void 0
|
|
1252
|
+
});
|
|
1253
|
+
const auditArtifactPath = writeAuditArtifact(audit, workspaceRoot);
|
|
1254
|
+
return { auditArtifactPath, appliedAt: audit.appliedAt };
|
|
1255
|
+
};
|
|
1256
|
+
var execute = async (args2) => {
|
|
1257
|
+
const {
|
|
1258
|
+
changes,
|
|
1259
|
+
workspaceRoot: workspaceRootArg,
|
|
1260
|
+
expectedPlanHash,
|
|
1261
|
+
requireReview,
|
|
1262
|
+
reviewToken,
|
|
1263
|
+
expectedPolicyVersion
|
|
1264
|
+
} = args2;
|
|
1265
|
+
if (!changes) {
|
|
1266
|
+
return createFailure("Missing required input: changes", "changes array is required");
|
|
1267
|
+
}
|
|
1268
|
+
if (!validateChanges(changes)) {
|
|
1269
|
+
return createFailure(
|
|
1270
|
+
"Invalid changes format: expected array of effects with kind property",
|
|
1271
|
+
'Each change must have a "kind" property'
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
const actualPlanHash = computePlanHash(changes);
|
|
1275
|
+
const policyVersion = typeof expectedPolicyVersion === "string" ? expectedPolicyVersion : POLICY_VERSION;
|
|
1276
|
+
const reviewGateError = checkReviewGate(requireReview, reviewToken, actualPlanHash, policyVersion);
|
|
1277
|
+
if (reviewGateError) return reviewGateError;
|
|
1278
|
+
const hashMismatchError = checkPlanHash(expectedPlanHash, actualPlanHash);
|
|
1279
|
+
if (hashMismatchError) return hashMismatchError;
|
|
1280
|
+
const elicitEffects = changes.filter(import_facade6.isElicitInputEffect);
|
|
1281
|
+
if (elicitEffects.length > 0) {
|
|
1282
|
+
const fieldNames = elicitEffects.flatMap((e) => {
|
|
1283
|
+
if ("fields" in e && Array.isArray(e.fields)) {
|
|
1284
|
+
return e.fields.map((f) => f.name);
|
|
1285
|
+
}
|
|
1286
|
+
return [];
|
|
1287
|
+
}).join(", ");
|
|
1288
|
+
return {
|
|
1289
|
+
ok: false,
|
|
1290
|
+
operation: "apply_changes",
|
|
1291
|
+
summary: "Cannot apply: Plan requires user input",
|
|
1292
|
+
artifacts: [],
|
|
1293
|
+
changes: elicitEffects,
|
|
1294
|
+
diagnostics: [
|
|
1295
|
+
{
|
|
1296
|
+
level: "error",
|
|
1297
|
+
message: `Elicitation required for: ${fieldNames || "unknown fields"}`
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
level: "info",
|
|
1301
|
+
message: "Use generate_service with required inputs first, then apply the resulting changes"
|
|
1302
|
+
}
|
|
1303
|
+
]
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
const workspaceRoot = typeof workspaceRootArg === "string" ? workspaceRootArg : (0, import_facade6.findWorkspaceRoot)();
|
|
1307
|
+
if (!workspaceRoot) {
|
|
1308
|
+
return {
|
|
1309
|
+
ok: false,
|
|
1310
|
+
operation: "apply_changes",
|
|
1311
|
+
summary: "Could not determine workspace root",
|
|
1312
|
+
artifacts: [],
|
|
1313
|
+
changes: [],
|
|
1314
|
+
diagnostics: [
|
|
1315
|
+
{ level: "error", message: "Workspace root not found" },
|
|
1316
|
+
{ level: "info", message: "Provide workspaceRoot parameter or run from within a workspace" }
|
|
1317
|
+
]
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
const result = applyEffects(changes, workspaceRoot);
|
|
1321
|
+
const artifacts = result.results.filter((r) => r.success && r.path).map((r) => ({
|
|
1322
|
+
type: "file",
|
|
1323
|
+
path: r.path,
|
|
1324
|
+
description: r.message
|
|
1325
|
+
}));
|
|
1326
|
+
const diagnostics = result.results.map((r) => ({
|
|
1327
|
+
level: r.success ? "info" : "error",
|
|
1328
|
+
message: r.message,
|
|
1329
|
+
location: r.path
|
|
1330
|
+
}));
|
|
1331
|
+
const auditInfo = writeAuditAndGetInfo(actualPlanHash, policyVersion, changes, result, reviewToken, workspaceRoot);
|
|
1332
|
+
if (auditInfo) {
|
|
1333
|
+
artifacts.push({
|
|
1334
|
+
type: "file",
|
|
1335
|
+
path: auditInfo.auditArtifactPath,
|
|
1336
|
+
description: "Audit artifact"
|
|
1337
|
+
});
|
|
1338
|
+
diagnostics.push({
|
|
1339
|
+
level: "info",
|
|
1340
|
+
message: `Audit artifact written to: ${auditInfo.auditArtifactPath}`,
|
|
1341
|
+
location: auditInfo.auditArtifactPath
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
const extendedResult = {
|
|
1345
|
+
...result,
|
|
1346
|
+
planHash: actualPlanHash,
|
|
1347
|
+
policyVersion,
|
|
1348
|
+
appliedAt: auditInfo?.appliedAt,
|
|
1349
|
+
auditArtifactPath: auditInfo?.auditArtifactPath,
|
|
1350
|
+
reviewTokenUsed: typeof reviewToken === "string" ? reviewToken : void 0
|
|
1351
|
+
};
|
|
1352
|
+
return {
|
|
1353
|
+
ok: result.ok,
|
|
1354
|
+
operation: "apply_changes",
|
|
1355
|
+
summary: result.summary,
|
|
1356
|
+
artifacts,
|
|
1357
|
+
changes: [],
|
|
1358
|
+
// Effects already applied
|
|
1359
|
+
diagnostics,
|
|
1360
|
+
data: extendedResult
|
|
1361
|
+
};
|
|
1362
|
+
};
|
|
1363
|
+
var applyChangesTool = {
|
|
1364
|
+
name: "apply_changes",
|
|
1365
|
+
description: "Apply changes (effects) from a generate_service plan. Writes files, adds env vars, and creates infra configs. Non-interactive, idempotent - safe to run multiple times. Rejects plans that require user input (elicitation). Supports requireReview gate and expectedPlanHash for drift detection.",
|
|
1366
|
+
inputSchema: {
|
|
1367
|
+
type: "object",
|
|
1368
|
+
properties: {
|
|
1369
|
+
changes: {
|
|
1370
|
+
type: "array",
|
|
1371
|
+
description: 'Array of effects to apply. Get this from the "changes" field of a generate_service result.',
|
|
1372
|
+
items: {
|
|
1373
|
+
type: "object",
|
|
1374
|
+
properties: {
|
|
1375
|
+
kind: {
|
|
1376
|
+
type: "string",
|
|
1377
|
+
description: 'Effect type: "file:write", "env:add", "infra:add"'
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
required: ["kind"]
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
workspaceRoot: {
|
|
1384
|
+
type: "string",
|
|
1385
|
+
description: "Optional workspace root path. Auto-detected if omitted."
|
|
1386
|
+
},
|
|
1387
|
+
expectedPlanHash: {
|
|
1388
|
+
type: "string",
|
|
1389
|
+
description: "Optional: SHA-256 hash from generate_service or plan_review. If provided, apply_changes will verify the plan has not been modified before execution."
|
|
1390
|
+
},
|
|
1391
|
+
requireReview: {
|
|
1392
|
+
type: "boolean",
|
|
1393
|
+
description: "Optional: If true, requires a valid reviewToken from plan_review before applying. Enforces review workflow."
|
|
1394
|
+
},
|
|
1395
|
+
reviewToken: {
|
|
1396
|
+
type: "string",
|
|
1397
|
+
description: "Optional: Review token from plan_review (format: planHash.policyVersion). Required if requireReview is true."
|
|
1398
|
+
},
|
|
1399
|
+
expectedPolicyVersion: {
|
|
1400
|
+
type: "string",
|
|
1401
|
+
description: "Optional: Expected policy version for reviewToken validation. Defaults to current POLICY_VERSION."
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
required: ["changes"]
|
|
1405
|
+
},
|
|
1406
|
+
execute
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
// src/tools/plan-review.ts
|
|
1410
|
+
var import_facade7 = require("@crossdelta/platform-sdk/facade");
|
|
1411
|
+
var validateChanges2 = (changes) => {
|
|
1412
|
+
if (!Array.isArray(changes)) return false;
|
|
1413
|
+
return changes.every((c) => typeof c === "object" && c !== null && "kind" in c && typeof c.kind === "string");
|
|
1414
|
+
};
|
|
1415
|
+
var getEffectTarget = (effect) => {
|
|
1416
|
+
if ("path" in effect && typeof effect.path === "string") {
|
|
1417
|
+
return effect.path;
|
|
1418
|
+
}
|
|
1419
|
+
if ("key" in effect && typeof effect.key === "string") {
|
|
1420
|
+
return `.env.local:${effect.key}`;
|
|
1421
|
+
}
|
|
1422
|
+
if ("serviceName" in effect && typeof effect.serviceName === "string") {
|
|
1423
|
+
return `infra/services/${effect.serviceName}.ts`;
|
|
1424
|
+
}
|
|
1425
|
+
return void 0;
|
|
1426
|
+
};
|
|
1427
|
+
var checkElicitation = (effects) => {
|
|
1428
|
+
const elicitEffects = effects.filter(import_facade7.isElicitInputEffect);
|
|
1429
|
+
if (elicitEffects.length === 0) return [];
|
|
1430
|
+
const fieldNames = elicitEffects.flatMap((e) => {
|
|
1431
|
+
if ("fields" in e && Array.isArray(e.fields)) {
|
|
1432
|
+
return e.fields.map((f) => f.name);
|
|
1433
|
+
}
|
|
1434
|
+
return [];
|
|
1435
|
+
}).join(", ");
|
|
1436
|
+
return [
|
|
1437
|
+
{
|
|
1438
|
+
severity: "error",
|
|
1439
|
+
code: "REQUIRES_ELICITATION",
|
|
1440
|
+
message: `Plan requires user input for: ${fieldNames || "unknown fields"}`
|
|
1441
|
+
}
|
|
1442
|
+
];
|
|
1443
|
+
};
|
|
1444
|
+
var checkUnsupportedEffects = (effects) => effects.filter((e) => !isEffectSupported(e.kind) && !(0, import_facade7.isElicitInputEffect)(e)).map((e) => ({
|
|
1445
|
+
severity: "error",
|
|
1446
|
+
code: "UNSUPPORTED_EFFECT",
|
|
1447
|
+
message: `Unsupported effect kind: ${e.kind}`,
|
|
1448
|
+
target: getEffectTarget(e)
|
|
1449
|
+
}));
|
|
1450
|
+
var checkDuplicateTargets = (effects) => {
|
|
1451
|
+
const targets = effects.map(getEffectTarget).filter((t) => t !== void 0);
|
|
1452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1453
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1454
|
+
for (const target of targets) {
|
|
1455
|
+
if (seen.has(target)) {
|
|
1456
|
+
duplicates.add(target);
|
|
1457
|
+
}
|
|
1458
|
+
seen.add(target);
|
|
1459
|
+
}
|
|
1460
|
+
return Array.from(duplicates).map((target) => ({
|
|
1461
|
+
severity: "warning",
|
|
1462
|
+
code: "DUPLICATE_TARGET",
|
|
1463
|
+
message: `Multiple effects target the same path: ${target}`,
|
|
1464
|
+
target
|
|
1465
|
+
}));
|
|
1466
|
+
};
|
|
1467
|
+
var extractTargets = (effects) => effects.map((e) => {
|
|
1468
|
+
const path = getEffectTarget(e);
|
|
1469
|
+
return path ? { path, kind: e.kind } : null;
|
|
1470
|
+
}).filter((t) => t !== null);
|
|
1471
|
+
var execute2 = async (args2) => {
|
|
1472
|
+
const { changes, workspaceRoot: workspaceRootArg } = args2;
|
|
1473
|
+
if (!changes) {
|
|
1474
|
+
return {
|
|
1475
|
+
ok: false,
|
|
1476
|
+
operation: "plan_review",
|
|
1477
|
+
summary: "Missing required input: changes",
|
|
1478
|
+
artifacts: [],
|
|
1479
|
+
changes: [],
|
|
1480
|
+
diagnostics: [{ level: "error", message: "changes array is required" }]
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
if (!validateChanges2(changes)) {
|
|
1484
|
+
return {
|
|
1485
|
+
ok: false,
|
|
1486
|
+
operation: "plan_review",
|
|
1487
|
+
summary: "Invalid changes format: expected array of effects with kind property",
|
|
1488
|
+
artifacts: [],
|
|
1489
|
+
changes: [],
|
|
1490
|
+
diagnostics: [{ level: "error", message: 'Each change must have a "kind" property' }]
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const workspaceRoot = typeof workspaceRootArg === "string" ? workspaceRootArg : (0, import_facade7.findWorkspaceRoot)();
|
|
1494
|
+
if (!workspaceRoot) {
|
|
1495
|
+
return {
|
|
1496
|
+
ok: false,
|
|
1497
|
+
operation: "plan_review",
|
|
1498
|
+
summary: "Could not determine workspace root",
|
|
1499
|
+
artifacts: [],
|
|
1500
|
+
changes: [],
|
|
1501
|
+
diagnostics: [
|
|
1502
|
+
{ level: "error", message: "Workspace root not found" },
|
|
1503
|
+
{ level: "info", message: "Provide workspaceRoot parameter or run from within a workspace" }
|
|
1504
|
+
]
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
const issues = [
|
|
1508
|
+
...checkElicitation(changes),
|
|
1509
|
+
...checkUnsupportedEffects(changes),
|
|
1510
|
+
...checkDuplicateTargets(changes)
|
|
1511
|
+
];
|
|
1512
|
+
const dryRunResult = applyEffects(changes, workspaceRoot, { dryRun: true });
|
|
1513
|
+
for (const result of dryRunResult.results) {
|
|
1514
|
+
if (!result.success && result.code) {
|
|
1515
|
+
issues.push({
|
|
1516
|
+
severity: "error",
|
|
1517
|
+
code: result.code,
|
|
1518
|
+
message: result.message,
|
|
1519
|
+
target: result.target
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
1524
|
+
const targets = extractTargets(changes);
|
|
1525
|
+
const planHash = computePlanHash(changes);
|
|
1526
|
+
const reviewToken = generateReviewToken(planHash, POLICY_VERSION);
|
|
1527
|
+
const reviewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1528
|
+
const reviewResult = {
|
|
1529
|
+
valid: !hasErrors,
|
|
1530
|
+
canApply: !hasErrors && issues.every((i) => i.severity !== "error"),
|
|
1531
|
+
issues,
|
|
1532
|
+
preview: {
|
|
1533
|
+
wouldCreate: dryRunResult.counts.wouldCreate,
|
|
1534
|
+
wouldUpdate: dryRunResult.counts.wouldUpdate,
|
|
1535
|
+
wouldSkip: dryRunResult.counts.wouldSkip,
|
|
1536
|
+
errors: dryRunResult.counts.error
|
|
1537
|
+
},
|
|
1538
|
+
targets,
|
|
1539
|
+
planHash,
|
|
1540
|
+
policyVersion: POLICY_VERSION,
|
|
1541
|
+
reviewToken,
|
|
1542
|
+
reviewedAt
|
|
1543
|
+
};
|
|
1544
|
+
cacheReview({
|
|
1545
|
+
planHash,
|
|
1546
|
+
policyVersion: POLICY_VERSION,
|
|
1547
|
+
reviewToken,
|
|
1548
|
+
reviewedAt,
|
|
1549
|
+
valid: reviewResult.valid,
|
|
1550
|
+
canApply: reviewResult.canApply,
|
|
1551
|
+
preview: reviewResult.preview,
|
|
1552
|
+
issues: reviewResult.issues
|
|
1553
|
+
});
|
|
1554
|
+
const diagnostics = issues.map((issue) => ({
|
|
1555
|
+
level: issue.severity === "error" ? "error" : issue.severity === "warning" ? "warning" : "info",
|
|
1556
|
+
message: `[${issue.code}] ${issue.message}`,
|
|
1557
|
+
location: issue.target
|
|
1558
|
+
}));
|
|
1559
|
+
if (reviewResult.canApply) {
|
|
1560
|
+
diagnostics.push({
|
|
1561
|
+
level: "info",
|
|
1562
|
+
message: dryRunResult.summary,
|
|
1563
|
+
location: void 0
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
return {
|
|
1567
|
+
ok: reviewResult.valid,
|
|
1568
|
+
operation: "plan_review",
|
|
1569
|
+
summary: reviewResult.valid ? `Plan is valid: ${dryRunResult.summary}` : `Plan has ${issues.filter((i) => i.severity === "error").length} errors`,
|
|
1570
|
+
artifacts: [],
|
|
1571
|
+
changes: [],
|
|
1572
|
+
// No changes - this is review only
|
|
1573
|
+
diagnostics,
|
|
1574
|
+
data: reviewResult
|
|
1575
|
+
};
|
|
1576
|
+
};
|
|
1577
|
+
var planReviewTool = {
|
|
1578
|
+
name: "plan_review",
|
|
1579
|
+
description: "Review and validate a plan (effects array) without executing it. Checks for path safety issues, duplicate targets, unsupported effects, and elicitation requirements. Shows preview of what would be created/updated/skipped.",
|
|
1580
|
+
inputSchema: {
|
|
1581
|
+
type: "object",
|
|
1582
|
+
properties: {
|
|
1583
|
+
changes: {
|
|
1584
|
+
type: "array",
|
|
1585
|
+
description: 'Array of effects to review. Get this from the "changes" field of a generate_service result.',
|
|
1586
|
+
items: {
|
|
1587
|
+
type: "object",
|
|
1588
|
+
properties: {
|
|
1589
|
+
kind: {
|
|
1590
|
+
type: "string",
|
|
1591
|
+
description: 'Effect type: "file:write", "env:add", "infra:add"'
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
required: ["kind"]
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
workspaceRoot: {
|
|
1598
|
+
type: "string",
|
|
1599
|
+
description: "Optional workspace root path. Auto-detected if omitted."
|
|
1600
|
+
}
|
|
1601
|
+
},
|
|
1602
|
+
required: ["changes"]
|
|
1603
|
+
},
|
|
1604
|
+
execute: execute2
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
// src/tools/services-generate/index.ts
|
|
1608
|
+
var import_facade10 = require("@crossdelta/platform-sdk/facade");
|
|
1609
|
+
|
|
1610
|
+
// src/tools/services-generate/mode-explicit.ts
|
|
1611
|
+
var import_facade8 = require("@crossdelta/platform-sdk/facade");
|
|
1612
|
+
|
|
1613
|
+
// src/tools/services-generate/review.ts
|
|
1614
|
+
var runInlineReview = (changes, workspaceRoot) => {
|
|
1615
|
+
const issues = [];
|
|
1616
|
+
for (const effect of changes) {
|
|
1617
|
+
if (!isEffectSupported(effect.kind)) {
|
|
1618
|
+
issues.push({
|
|
1619
|
+
severity: "error",
|
|
1620
|
+
code: "UNSUPPORTED_EFFECT",
|
|
1621
|
+
message: `Unsupported effect kind: ${effect.kind}`
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const dryRunResult = applyEffects(changes, workspaceRoot, { dryRun: true });
|
|
1626
|
+
for (const result of dryRunResult.results) {
|
|
1627
|
+
if (!result.success && result.code) {
|
|
1628
|
+
issues.push({
|
|
1629
|
+
severity: "error",
|
|
1630
|
+
code: result.code,
|
|
1631
|
+
message: result.message
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
1636
|
+
return {
|
|
1637
|
+
valid: !hasErrors,
|
|
1638
|
+
canApply: !hasErrors,
|
|
1639
|
+
issues,
|
|
1640
|
+
preview: {
|
|
1641
|
+
wouldCreate: dryRunResult.counts.wouldCreate,
|
|
1642
|
+
wouldUpdate: dryRunResult.counts.wouldUpdate,
|
|
1643
|
+
wouldSkip: dryRunResult.counts.wouldSkip,
|
|
1644
|
+
errors: dryRunResult.counts.error
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
};
|
|
1648
|
+
var applyPlanEffects = (result, workspaceRoot) => {
|
|
1649
|
+
const applyResult = applyEffects(result.changes, workspaceRoot, { dryRun: false });
|
|
1650
|
+
return {
|
|
1651
|
+
...result,
|
|
1652
|
+
summary: `${result.summary} - ${applyResult.summary}`,
|
|
1653
|
+
diagnostics: [
|
|
1654
|
+
...result.diagnostics ?? [],
|
|
1655
|
+
...applyResult.results.filter((r) => r.message).map((r) => ({
|
|
1656
|
+
level: r.success ? "info" : "error",
|
|
1657
|
+
message: r.message,
|
|
1658
|
+
location: r.path
|
|
1659
|
+
}))
|
|
1660
|
+
]
|
|
1661
|
+
};
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// src/tools/services-generate/validation.ts
|
|
1665
|
+
var VALID_TEMPLATES = ["hono-bun", "nest"];
|
|
1666
|
+
var getMissingFields = (args2) => {
|
|
1667
|
+
const { template, name } = args2;
|
|
1668
|
+
const missing = [];
|
|
1669
|
+
if (!template || typeof template !== "string") {
|
|
1670
|
+
missing.push({
|
|
1671
|
+
name: "template",
|
|
1672
|
+
prompt: "Which service template would you like to use?",
|
|
1673
|
+
required: true,
|
|
1674
|
+
description: "Service template type for scaffolding",
|
|
1675
|
+
type: "string",
|
|
1676
|
+
enum: [...VALID_TEMPLATES]
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
if (!name || typeof name !== "string") {
|
|
1680
|
+
missing.push({
|
|
1681
|
+
name: "name",
|
|
1682
|
+
prompt: "What should the service be named?",
|
|
1683
|
+
required: true,
|
|
1684
|
+
description: 'Service name in kebab-case (e.g., "order-processing")',
|
|
1685
|
+
type: "string"
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
return missing;
|
|
1689
|
+
};
|
|
1690
|
+
var isValidTemplate = (template) => VALID_TEMPLATES.includes(template);
|
|
1691
|
+
var createElicitResult = (message, fields) => {
|
|
1692
|
+
const elicitEffect = {
|
|
1693
|
+
kind: "elicit:input",
|
|
1694
|
+
message,
|
|
1695
|
+
fields
|
|
1696
|
+
};
|
|
1697
|
+
return {
|
|
1698
|
+
ok: false,
|
|
1699
|
+
operation: "generate_service",
|
|
1700
|
+
summary: message,
|
|
1701
|
+
artifacts: [],
|
|
1702
|
+
changes: [elicitEffect],
|
|
1703
|
+
diagnostics: [{ level: "info", message: "Awaiting user input" }]
|
|
1704
|
+
};
|
|
1705
|
+
};
|
|
1706
|
+
var toMcpElicitFields = (fields) => fields.map((f) => ({
|
|
1707
|
+
name: f.path,
|
|
1708
|
+
prompt: f.prompt,
|
|
1709
|
+
required: f.required,
|
|
1710
|
+
description: f.prompt,
|
|
1711
|
+
type: f.type === "enum" ? "string" : "string",
|
|
1712
|
+
enum: f.options
|
|
1713
|
+
}));
|
|
1714
|
+
|
|
1715
|
+
// src/tools/services-generate/mode-explicit.ts
|
|
1716
|
+
var executeExplicitMode = (args2, workspaceRoot, _options) => {
|
|
1717
|
+
const { template, name, port, events, dryRun, autoReview } = args2;
|
|
1718
|
+
const missingFields = getMissingFields(args2);
|
|
1719
|
+
if (missingFields.length > 0) {
|
|
1720
|
+
return createElicitResult("Missing required inputs for service generation", missingFields);
|
|
1721
|
+
}
|
|
1722
|
+
if (!isValidTemplate(template)) {
|
|
1723
|
+
return createElicitResult(`Invalid template: ${template}. Please select a valid template.`, [
|
|
1724
|
+
{
|
|
1725
|
+
name: "template",
|
|
1726
|
+
prompt: "Which service template would you like to use?",
|
|
1727
|
+
required: true,
|
|
1728
|
+
description: "Service template type for scaffolding",
|
|
1729
|
+
type: "string",
|
|
1730
|
+
enum: ["hono-bun", "nest"]
|
|
1731
|
+
}
|
|
1732
|
+
]);
|
|
1733
|
+
}
|
|
1734
|
+
const eventsList = Array.isArray(events) ? events.filter((e) => typeof e === "string") : [];
|
|
1735
|
+
const result = (0, import_facade8.planServiceGeneration)({
|
|
1736
|
+
type: template,
|
|
1737
|
+
name,
|
|
1738
|
+
workspaceRoot,
|
|
1739
|
+
port: typeof port === "number" ? port : void 0,
|
|
1740
|
+
events: eventsList
|
|
1741
|
+
});
|
|
1742
|
+
if (result.ok && result.changes.length > 0) {
|
|
1743
|
+
const planHash = computePlanHash(result.changes);
|
|
1744
|
+
const extendedResult = {
|
|
1745
|
+
...result,
|
|
1746
|
+
planHash
|
|
1747
|
+
};
|
|
1748
|
+
cachePlan({
|
|
1749
|
+
planHash,
|
|
1750
|
+
effects: result.changes,
|
|
1751
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1752
|
+
source: "generate_service",
|
|
1753
|
+
metadata: {
|
|
1754
|
+
template,
|
|
1755
|
+
name,
|
|
1756
|
+
port: typeof port === "number" ? port : void 0
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
if (autoReview === true) {
|
|
1760
|
+
extendedResult.review = runInlineReview(result.changes, workspaceRoot);
|
|
1761
|
+
}
|
|
1762
|
+
if (dryRun === false) {
|
|
1763
|
+
return applyPlanEffects(extendedResult, workspaceRoot);
|
|
1764
|
+
}
|
|
1765
|
+
return extendedResult;
|
|
1766
|
+
}
|
|
1767
|
+
return result;
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
// src/tools/services-generate/mode-nl.ts
|
|
1771
|
+
var import_node_fs4 = require("fs");
|
|
1772
|
+
var import_node_path6 = require("path");
|
|
1773
|
+
var import_facade9 = require("@crossdelta/platform-sdk/facade");
|
|
1774
|
+
|
|
1775
|
+
// src/tools/services-generate/npm-verifier.ts
|
|
1776
|
+
var parsePackageJson = (content) => {
|
|
1777
|
+
try {
|
|
1778
|
+
return JSON.parse(content);
|
|
1779
|
+
} catch {
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
var extractNpmPackages = (pkg) => {
|
|
1784
|
+
const allDeps = {
|
|
1785
|
+
...pkg.dependencies ?? {},
|
|
1786
|
+
...pkg.devDependencies ?? {}
|
|
1787
|
+
};
|
|
1788
|
+
return Object.entries(allDeps).filter(([_, version]) => !version.startsWith("workspace:")).map(([name, version]) => ({ name, version }));
|
|
1789
|
+
};
|
|
1790
|
+
var normalizeVersion = (version) => version.replace(/^\^/, "");
|
|
1791
|
+
var createCorrection = (name, generatedVersion, actualVersion) => ({
|
|
1792
|
+
name,
|
|
1793
|
+
generatedVersion,
|
|
1794
|
+
actualVersion: actualVersion ?? generatedVersion,
|
|
1795
|
+
corrected: actualVersion !== null && normalizeVersion(generatedVersion) !== actualVersion
|
|
1796
|
+
});
|
|
1797
|
+
var applyCorrections = (pkg, corrections) => {
|
|
1798
|
+
const corrected = { ...pkg };
|
|
1799
|
+
for (const correction of corrections.filter((c) => c.corrected)) {
|
|
1800
|
+
if (corrected.dependencies?.[correction.name]) {
|
|
1801
|
+
corrected.dependencies = {
|
|
1802
|
+
...corrected.dependencies,
|
|
1803
|
+
[correction.name]: `^${correction.actualVersion}`
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (corrected.devDependencies?.[correction.name]) {
|
|
1807
|
+
corrected.devDependencies = {
|
|
1808
|
+
...corrected.devDependencies,
|
|
1809
|
+
[correction.name]: `^${correction.actualVersion}`
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return corrected;
|
|
1814
|
+
};
|
|
1815
|
+
var fetchNpmVersion = async (packageName) => {
|
|
1816
|
+
try {
|
|
1817
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
1818
|
+
if (!response.ok) return null;
|
|
1819
|
+
const data = await response.json();
|
|
1820
|
+
return data.version ?? null;
|
|
1821
|
+
} catch {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
var isFileWriteEffect = (effect) => effect.kind === "file:write";
|
|
1826
|
+
var verifyAndCorrectPackageJson = async (content) => {
|
|
1827
|
+
const pkg = parsePackageJson(content);
|
|
1828
|
+
if (!pkg) {
|
|
1829
|
+
return {
|
|
1830
|
+
correctedContent: content,
|
|
1831
|
+
result: { corrections: [], hasCorrections: false }
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
const packages = extractNpmPackages(pkg);
|
|
1835
|
+
if (packages.length === 0) {
|
|
1836
|
+
return {
|
|
1837
|
+
correctedContent: content,
|
|
1838
|
+
result: { corrections: [], hasCorrections: false }
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
const corrections = await Promise.all(
|
|
1842
|
+
packages.map(async ({ name, version }) => {
|
|
1843
|
+
const actualVersion = await fetchNpmVersion(name);
|
|
1844
|
+
return createCorrection(name, version, actualVersion);
|
|
1845
|
+
})
|
|
1846
|
+
);
|
|
1847
|
+
const hasCorrections = corrections.some((c) => c.corrected);
|
|
1848
|
+
const correctedPkg = hasCorrections ? applyCorrections(pkg, corrections) : pkg;
|
|
1849
|
+
return {
|
|
1850
|
+
correctedContent: JSON.stringify(correctedPkg, null, 2),
|
|
1851
|
+
result: {
|
|
1852
|
+
corrections: corrections.filter((c) => c.corrected),
|
|
1853
|
+
hasCorrections
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
};
|
|
1857
|
+
var verifyAndCorrectNpmVersions = async (changes) => {
|
|
1858
|
+
const pkgIndex = changes.findIndex((c) => isFileWriteEffect(c) && c.path.endsWith("package.json"));
|
|
1859
|
+
if (pkgIndex === -1) {
|
|
1860
|
+
return {
|
|
1861
|
+
correctedChanges: changes,
|
|
1862
|
+
result: { corrections: [], hasCorrections: false }
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
const pkgChange = changes[pkgIndex];
|
|
1866
|
+
const { correctedContent, result } = await verifyAndCorrectPackageJson(pkgChange.content);
|
|
1867
|
+
const correctedChanges = [...changes];
|
|
1868
|
+
const correctedFileEffect = {
|
|
1869
|
+
kind: "file:write",
|
|
1870
|
+
path: pkgChange.path,
|
|
1871
|
+
content: correctedContent
|
|
1872
|
+
};
|
|
1873
|
+
correctedChanges[pkgIndex] = correctedFileEffect;
|
|
1874
|
+
return { correctedChanges, result };
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
// src/tools/services-generate/mode-nl.ts
|
|
1878
|
+
var buildDiagnostics = (spec, serviceMeta, files, scaffoldCount, aiFilesCount, dependencies, envVars, templateType) => [
|
|
1879
|
+
{ level: "info", message: `Parsed from prompt: ${spec.serviceName}` },
|
|
1880
|
+
{ level: "info", message: `Template: ${templateType} (port ${serviceMeta.port})` },
|
|
1881
|
+
{ level: "info", message: `Scaffold files: ${scaffoldCount} (ready to apply)` },
|
|
1882
|
+
{ level: "info", message: `Files for AI: ${aiFilesCount} (${files.map((f) => f.intent).join(", ")})` },
|
|
1883
|
+
...dependencies.length > 0 ? [{ level: "info", message: `Dependencies: ${dependencies.map((d) => d.name).join(", ")}` }] : [],
|
|
1884
|
+
...envVars.length > 0 ? [{ level: "info", message: `Env vars: ${envVars.map((e) => e.key).join(", ")}` }] : []
|
|
1885
|
+
];
|
|
1886
|
+
var normalizeFiles = (files) => files.map((f) => ({
|
|
1887
|
+
path: f.path,
|
|
1888
|
+
intent: f.intent,
|
|
1889
|
+
inputs: f.inputs ?? {},
|
|
1890
|
+
layer: f.layer,
|
|
1891
|
+
kind: f.kind
|
|
1892
|
+
}));
|
|
1893
|
+
var extractEventsFromSpec = (spec) => spec.triggers.filter((t) => t.type === "event" && !!t.eventType).map((t) => t.eventType);
|
|
1894
|
+
var extractDependencies = (deps) => Object.entries(deps).map(([name, version]) => ({
|
|
1895
|
+
name,
|
|
1896
|
+
version,
|
|
1897
|
+
dev: false
|
|
1898
|
+
}));
|
|
1899
|
+
var buildServiceResult = (spec, serviceMeta, serviceDir, correctedChanges, metadata, dependencies, verificationCorrections, templateType, scaffoldArtifacts) => ({
|
|
1900
|
+
ok: true,
|
|
1901
|
+
operation: "generate_service",
|
|
1902
|
+
summary: `Planned ${spec.serviceName}: ${correctedChanges.length} scaffold files + ${metadata.files.length} files for AI to generate`,
|
|
1903
|
+
data: serviceMeta,
|
|
1904
|
+
artifacts: [
|
|
1905
|
+
...scaffoldArtifacts,
|
|
1906
|
+
{ type: "service", path: serviceDir, description: `Generated service ${spec.serviceName}` }
|
|
1907
|
+
],
|
|
1908
|
+
changes: correctedChanges,
|
|
1909
|
+
files: normalizeFiles(metadata.files),
|
|
1910
|
+
dependencies,
|
|
1911
|
+
envVars: metadata.envVars.map((ev) => ({
|
|
1912
|
+
key: ev.key,
|
|
1913
|
+
description: ev.description ?? "",
|
|
1914
|
+
required: ev.required ?? false
|
|
1915
|
+
})),
|
|
1916
|
+
postCommands: metadata.postCommands,
|
|
1917
|
+
diagnostics: [
|
|
1918
|
+
...buildDiagnostics(
|
|
1919
|
+
spec,
|
|
1920
|
+
serviceMeta,
|
|
1921
|
+
metadata.files,
|
|
1922
|
+
correctedChanges.length,
|
|
1923
|
+
metadata.files.length,
|
|
1924
|
+
dependencies,
|
|
1925
|
+
metadata.envVars,
|
|
1926
|
+
templateType
|
|
1927
|
+
),
|
|
1928
|
+
...verificationCorrections.map((c) => ({
|
|
1929
|
+
level: "info",
|
|
1930
|
+
message: `\u{1F4E6} Corrected ${c.name}: ${c.generatedVersion} \u2192 ^${c.actualVersion}`
|
|
1931
|
+
}))
|
|
1932
|
+
]
|
|
1933
|
+
});
|
|
1934
|
+
var executeNLMode = async (prompt, workspaceRoot, options) => {
|
|
1935
|
+
const pkgPath = (0, import_node_path6.join)(workspaceRoot, "package.json");
|
|
1936
|
+
const pkg = (0, import_node_fs4.existsSync)(pkgPath) ? JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8")) : null;
|
|
1937
|
+
const workspaceConfig = (0, import_facade9.loadCapabilitiesConfig)(pkg);
|
|
1938
|
+
const parseResult = await (0, import_facade9.parseServicePrompt)(prompt, { workspaceConfig });
|
|
1939
|
+
if (!parseResult.complete) {
|
|
1940
|
+
const fields = toMcpElicitFields(parseResult.fields);
|
|
1941
|
+
return createElicitResult("Additional information needed", fields);
|
|
1942
|
+
}
|
|
1943
|
+
const spec = parseResult.spec;
|
|
1944
|
+
const events = extractEventsFromSpec(spec);
|
|
1945
|
+
const serviceDir = `services/${spec.serviceName}`;
|
|
1946
|
+
const metadata = (0, import_facade9.lowerSpecToCapabilities)(spec, serviceDir);
|
|
1947
|
+
const templateType = options.template || "hono-bun";
|
|
1948
|
+
const scaffoldResult = (0, import_facade9.planServiceGeneration)({
|
|
1949
|
+
type: templateType,
|
|
1950
|
+
name: spec.serviceName,
|
|
1951
|
+
workspaceRoot,
|
|
1952
|
+
events,
|
|
1953
|
+
envVars: metadata.envVars
|
|
1954
|
+
});
|
|
1955
|
+
if (!scaffoldResult.ok) {
|
|
1956
|
+
return {
|
|
1957
|
+
ok: false,
|
|
1958
|
+
operation: "generate_service",
|
|
1959
|
+
summary: scaffoldResult.summary,
|
|
1960
|
+
artifacts: [],
|
|
1961
|
+
changes: [],
|
|
1962
|
+
diagnostics: [{ level: "error", message: scaffoldResult.summary }]
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
if (!scaffoldResult.data) {
|
|
1966
|
+
return {
|
|
1967
|
+
ok: false,
|
|
1968
|
+
operation: "generate_service",
|
|
1969
|
+
summary: "Service scaffold generation failed: no data returned",
|
|
1970
|
+
artifacts: [],
|
|
1971
|
+
changes: [],
|
|
1972
|
+
diagnostics: [{ level: "error", message: "No service metadata returned from scaffold" }]
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
const dependencies = extractDependencies(metadata.dependencies);
|
|
1976
|
+
const { correctedChanges, result: verificationResult } = await verifyAndCorrectNpmVersions(scaffoldResult.changes);
|
|
1977
|
+
const serviceMeta = scaffoldResult.data;
|
|
1978
|
+
const result = buildServiceResult(
|
|
1979
|
+
spec,
|
|
1980
|
+
serviceMeta,
|
|
1981
|
+
serviceDir,
|
|
1982
|
+
correctedChanges,
|
|
1983
|
+
metadata,
|
|
1984
|
+
dependencies,
|
|
1985
|
+
verificationResult.corrections,
|
|
1986
|
+
templateType,
|
|
1987
|
+
scaffoldResult.artifacts ?? []
|
|
1988
|
+
);
|
|
1989
|
+
if (result.changes.length > 0) {
|
|
1990
|
+
result.planHash = computePlanHash(result.changes);
|
|
1991
|
+
cachePlan({
|
|
1992
|
+
planHash: result.planHash,
|
|
1993
|
+
effects: result.changes,
|
|
1994
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1995
|
+
source: "generate_service",
|
|
1996
|
+
metadata: { prompt, serviceName: spec.serviceName, filesToGenerate: metadata.files.length }
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
if (options.autoReview === true) {
|
|
2000
|
+
result.review = runInlineReview(result.changes, workspaceRoot);
|
|
2001
|
+
}
|
|
2002
|
+
if (options.dryRun === false) {
|
|
2003
|
+
return applyPlanEffects(result, workspaceRoot);
|
|
2004
|
+
}
|
|
2005
|
+
return result;
|
|
2006
|
+
};
|
|
2007
|
+
|
|
2008
|
+
// src/tools/services-generate/index.ts
|
|
2009
|
+
var execute3 = async (args2) => {
|
|
2010
|
+
const { prompt, template, workspaceUri, dryRun, autoReview } = args2;
|
|
2011
|
+
const workspaceRoot = typeof workspaceUri === "string" ? workspaceUri : (0, import_facade10.findWorkspaceRoot)();
|
|
2012
|
+
if (typeof prompt === "string" && prompt.trim().length > 0) {
|
|
2013
|
+
return executeNLMode(prompt, workspaceRoot, {
|
|
2014
|
+
dryRun,
|
|
2015
|
+
autoReview,
|
|
2016
|
+
template
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
return executeExplicitMode(args2, workspaceRoot, { dryRun, autoReview });
|
|
2020
|
+
};
|
|
2021
|
+
var servicesGenerateTool = {
|
|
2022
|
+
name: "generate_service",
|
|
2023
|
+
description: 'Plan generation of a new microservice from a natural language prompt. Example: "Erstelle notification-service, h\xF6rt auf order.created, sendet Push via Pusher Beams". Returns a structured plan with files, config, and environment variables. By default runs in plan-only mode (dryRun=true). After showing the plan, ask if user wants to apply it.\n\n\u{1F4DA} IMPORTANT: Before generating services, read the generator documentation:\n1. First, read docs://generators to discover available documentation\n2. Then read the relevant docs (e.g., docs://generators/service for main guidelines)\n\nThese resources contain up-to-date package versions, naming conventions, and best practices. Always verify package versions on npm - do not hallucinate versions!',
|
|
2024
|
+
inputSchema: {
|
|
2025
|
+
type: "object",
|
|
2026
|
+
properties: {
|
|
2027
|
+
prompt: {
|
|
2028
|
+
type: "string",
|
|
2029
|
+
description: 'Natural language prompt describing the service. Example: "Erstelle notification-service, h\xF6rt auf order.created, sendet Push via Pusher Beams". When provided, uses capabilities pipeline for intelligent generation.'
|
|
2030
|
+
},
|
|
2031
|
+
template: {
|
|
2032
|
+
type: "string",
|
|
2033
|
+
enum: ["hono-bun", "nest"],
|
|
2034
|
+
description: "Service template type. Required when not using prompt mode."
|
|
2035
|
+
},
|
|
2036
|
+
name: {
|
|
2037
|
+
type: "string",
|
|
2038
|
+
description: "Service name in kebab-case. Required when not using prompt mode."
|
|
2039
|
+
},
|
|
2040
|
+
workspaceUri: {
|
|
2041
|
+
type: "string",
|
|
2042
|
+
description: "Optional workspace root path. Auto-detected if omitted."
|
|
2043
|
+
},
|
|
2044
|
+
port: {
|
|
2045
|
+
type: "number",
|
|
2046
|
+
description: "Optional explicit port number. Auto-allocated if omitted."
|
|
2047
|
+
},
|
|
2048
|
+
events: {
|
|
2049
|
+
type: "array",
|
|
2050
|
+
items: { type: "string" },
|
|
2051
|
+
description: 'Event types to consume (e.g., ["order.created"]). Only for explicit mode.'
|
|
2052
|
+
},
|
|
2053
|
+
dryRun: {
|
|
2054
|
+
type: "boolean",
|
|
2055
|
+
description: "Plan-only mode (default: true). When true, returns plan without writing files.",
|
|
2056
|
+
default: true
|
|
2057
|
+
},
|
|
2058
|
+
autoReview: {
|
|
2059
|
+
type: "boolean",
|
|
2060
|
+
description: "Include inline plan validation (default: false).",
|
|
2061
|
+
default: false
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2064
|
+
required: []
|
|
2065
|
+
},
|
|
2066
|
+
execute: execute3
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
// src/tools/workspace-scan.ts
|
|
2070
|
+
var import_facade11 = require("@crossdelta/platform-sdk/facade");
|
|
2071
|
+
var normalizeInput = (args2) => ({
|
|
2072
|
+
workspaceRoot: typeof args2.workspaceRoot === "string" ? args2.workspaceRoot : void 0
|
|
2073
|
+
});
|
|
2074
|
+
var buildScanMeta = (context, loadedPlugins) => ({
|
|
2075
|
+
workspaceRoot: context.workspace.workspaceRoot,
|
|
2076
|
+
contracts: context.workspace.contracts,
|
|
2077
|
+
availableServices: context.workspace.availableServices,
|
|
2078
|
+
loadedPlugins
|
|
2079
|
+
});
|
|
2080
|
+
var createScanResult = (meta) => ({
|
|
2081
|
+
ok: true,
|
|
2082
|
+
operation: "scan_workspace",
|
|
2083
|
+
summary: `Workspace scanned: ${meta.availableServices.length} services, contracts at ${meta.contracts.packageName}`,
|
|
2084
|
+
artifacts: [],
|
|
2085
|
+
changes: [],
|
|
2086
|
+
// Read-only: no changes
|
|
2087
|
+
diagnostics: [],
|
|
2088
|
+
data: meta
|
|
2089
|
+
});
|
|
2090
|
+
var createScanError = (error) => ({
|
|
2091
|
+
ok: false,
|
|
2092
|
+
operation: "scan_workspace",
|
|
2093
|
+
summary: `Failed to scan workspace: ${error.message}`,
|
|
2094
|
+
artifacts: [],
|
|
2095
|
+
changes: [],
|
|
2096
|
+
// Read-only: no changes
|
|
2097
|
+
diagnostics: [
|
|
2098
|
+
{
|
|
2099
|
+
level: "error",
|
|
2100
|
+
message: error.message
|
|
2101
|
+
}
|
|
2102
|
+
]
|
|
2103
|
+
});
|
|
2104
|
+
var execute4 = async (args2, pluginModules = []) => {
|
|
2105
|
+
try {
|
|
2106
|
+
const input = normalizeInput(args2);
|
|
2107
|
+
const context = await (0, import_facade11.createContextFromWorkspace)(input.workspaceRoot);
|
|
2108
|
+
const meta = buildScanMeta(context, pluginModules);
|
|
2109
|
+
return createScanResult(meta);
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
return createScanError(error instanceof Error ? error : new Error(String(error)));
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
var workspaceScanInputSchema = {
|
|
2115
|
+
type: "object",
|
|
2116
|
+
properties: {
|
|
2117
|
+
workspaceRoot: {
|
|
2118
|
+
type: "string",
|
|
2119
|
+
description: "Optional: Absolute path to workspace root. If omitted, auto-discovers from current directory."
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
required: []
|
|
2123
|
+
};
|
|
2124
|
+
var createWorkspaceScanTool = (pluginModules) => ({
|
|
2125
|
+
name: "scan_workspace",
|
|
2126
|
+
description: "Scan the current workspace and return a read-only snapshot including workspace root, contracts configuration, discovered services, and loaded plugins. Does not write any files.",
|
|
2127
|
+
inputSchema: workspaceScanInputSchema,
|
|
2128
|
+
execute: async (args2) => execute4(args2, pluginModules)
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
// src/server.ts
|
|
2132
|
+
var isOperationResult = (result) => {
|
|
2133
|
+
return typeof result === "object" && result !== null && "ok" in result && "operation" in result && "summary" in result;
|
|
2134
|
+
};
|
|
2135
|
+
var readTemplateFileResource = async (uri) => {
|
|
2136
|
+
const templateFileMatch = uri.match(/^templates:\/\/([^/]+)\/file\/(.+)$/);
|
|
2137
|
+
if (!templateFileMatch) return null;
|
|
2138
|
+
const [, templateName, filePath] = templateFileMatch;
|
|
2139
|
+
if (!templateName || !filePath) {
|
|
2140
|
+
throw new Error(`Invalid template file URI: ${uri}`);
|
|
2141
|
+
}
|
|
2142
|
+
try {
|
|
2143
|
+
const content = await readTemplateFile(templateName, filePath);
|
|
2144
|
+
return {
|
|
2145
|
+
contents: [
|
|
2146
|
+
{
|
|
2147
|
+
uri,
|
|
2148
|
+
mimeType: "text/plain",
|
|
2149
|
+
text: content
|
|
2150
|
+
}
|
|
2151
|
+
]
|
|
2152
|
+
};
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
throw new Error(`Failed to read template file: ${error instanceof Error ? error.message : String(error)}`);
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
var readTemplateStructureResource = async (uri) => {
|
|
2158
|
+
const templateStructureMatch = uri.match(/^templates:\/\/([^/]+)$/);
|
|
2159
|
+
if (!templateStructureMatch) return null;
|
|
2160
|
+
const [, templateName] = templateStructureMatch;
|
|
2161
|
+
if (!templateName || templateName === "list") {
|
|
2162
|
+
return null;
|
|
2163
|
+
}
|
|
2164
|
+
try {
|
|
2165
|
+
const content = await readTemplateStructure(templateName);
|
|
2166
|
+
return {
|
|
2167
|
+
contents: [
|
|
2168
|
+
{
|
|
2169
|
+
uri,
|
|
2170
|
+
mimeType: "application/json",
|
|
2171
|
+
text: content
|
|
2172
|
+
}
|
|
2173
|
+
]
|
|
2174
|
+
};
|
|
2175
|
+
} catch (error) {
|
|
2176
|
+
throw new Error(`Failed to read template structure: ${error instanceof Error ? error.message : String(error)}`);
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
var completeTemplateName = async () => {
|
|
2180
|
+
const templates = await listTemplates();
|
|
2181
|
+
return {
|
|
2182
|
+
completion: {
|
|
2183
|
+
values: templates,
|
|
2184
|
+
total: templates.length,
|
|
2185
|
+
hasMore: false
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
};
|
|
2189
|
+
var completeTemplatePath = async (uri) => {
|
|
2190
|
+
const templateMatch = uri.match(/templates:\/\/([^/]+)/);
|
|
2191
|
+
if (!templateMatch?.[1]) {
|
|
2192
|
+
return {
|
|
2193
|
+
completion: {
|
|
2194
|
+
values: [],
|
|
2195
|
+
total: 0,
|
|
2196
|
+
hasMore: false
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
const templateName = templateMatch[1];
|
|
2201
|
+
try {
|
|
2202
|
+
const files = await listTemplateFiles(templateName);
|
|
2203
|
+
return {
|
|
2204
|
+
completion: {
|
|
2205
|
+
values: files,
|
|
2206
|
+
total: files.length,
|
|
2207
|
+
hasMore: false
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
} catch {
|
|
2211
|
+
return {
|
|
2212
|
+
completion: {
|
|
2213
|
+
values: [],
|
|
2214
|
+
total: 0,
|
|
2215
|
+
hasMore: false
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
var normalizeToOperationResult = (result, toolName) => {
|
|
2221
|
+
if (isOperationResult(result)) {
|
|
2222
|
+
return result;
|
|
2223
|
+
}
|
|
2224
|
+
const legacyResult = result;
|
|
2225
|
+
return {
|
|
2226
|
+
ok: legacyResult.ok ?? true,
|
|
2227
|
+
operation: legacyResult.operation ?? toolName,
|
|
2228
|
+
summary: legacyResult.summary ?? (legacyResult.output ? Array.isArray(legacyResult.output) ? legacyResult.output.join("\n") : legacyResult.output : `Executed ${toolName}`),
|
|
2229
|
+
artifacts: [],
|
|
2230
|
+
changes: legacyResult.effects ?? [],
|
|
2231
|
+
diagnostics: []
|
|
2232
|
+
};
|
|
2233
|
+
};
|
|
2234
|
+
var buildTools = (pluginTools, pluginModules) => {
|
|
2235
|
+
const workspaceScanTool = createWorkspaceScanTool(pluginModules);
|
|
2236
|
+
return [servicesGenerateTool, planReviewTool, applyChangesTool, workspaceScanTool, ...pluginTools];
|
|
2237
|
+
};
|
|
2238
|
+
var buildResources = async (pluginModules, workspaceRoot) => {
|
|
2239
|
+
const generatorDocsResources = await createAllGeneratorDocResources();
|
|
2240
|
+
const templatesListResource = createTemplatesListResource();
|
|
2241
|
+
return [
|
|
2242
|
+
createWorkspaceSummaryResource(pluginModules, workspaceRoot),
|
|
2243
|
+
createServicesResource(workspaceRoot),
|
|
2244
|
+
createContractsResource(workspaceRoot),
|
|
2245
|
+
...generatorDocsResources,
|
|
2246
|
+
templatesListResource
|
|
2247
|
+
// Only discovery resource, not individual templates
|
|
2248
|
+
];
|
|
2249
|
+
};
|
|
2250
|
+
var registerHandlers = (server, tools, resources) => {
|
|
2251
|
+
server.setRequestHandler(import_types10.ListToolsRequestSchema, async () => ({
|
|
2252
|
+
tools: tools.map((tool) => ({
|
|
2253
|
+
name: tool.name,
|
|
2254
|
+
description: tool.description,
|
|
2255
|
+
inputSchema: tool.inputSchema
|
|
2256
|
+
}))
|
|
2257
|
+
}));
|
|
2258
|
+
server.setRequestHandler(import_types10.CallToolRequestSchema, async (request) => {
|
|
2259
|
+
const { name: toolName, arguments: args2 } = request.params;
|
|
2260
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
2261
|
+
if (!tool) {
|
|
2262
|
+
return formatErrorAsMcpResponse(new Error(`Unknown tool: ${toolName}`), toolName);
|
|
2263
|
+
}
|
|
2264
|
+
try {
|
|
2265
|
+
const result = await tool.execute(args2 ?? {});
|
|
2266
|
+
const normalizedResult = normalizeToOperationResult(result, toolName);
|
|
2267
|
+
const elicitEffect = normalizedResult.changes.find(import_facade12.isElicitInputEffect);
|
|
2268
|
+
if (elicitEffect) {
|
|
2269
|
+
return formatElicitationAsMcpResponse(elicitEffect, toolName);
|
|
2270
|
+
}
|
|
2271
|
+
return formatOperationResultAsMcpResponse(normalizedResult);
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
return formatErrorAsMcpResponse(error instanceof Error ? error : new Error(String(error)), toolName);
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
server.setRequestHandler(import_types10.ListResourcesRequestSchema, async () => ({
|
|
2277
|
+
resources: resources.map((r) => r.resource)
|
|
2278
|
+
}));
|
|
2279
|
+
server.setRequestHandler(import_types10.ListResourceTemplatesRequestSchema, async () => ({
|
|
2280
|
+
resourceTemplates: [createTemplateFileResourceTemplate()]
|
|
2281
|
+
}));
|
|
2282
|
+
server.setRequestHandler(import_types10.CompleteRequestSchema, async (request) => {
|
|
2283
|
+
const { ref, argument } = request.params;
|
|
2284
|
+
if (ref.type === "ref/resource" && ref.uri?.startsWith("templates://")) {
|
|
2285
|
+
if (argument.name === "template") {
|
|
2286
|
+
return await completeTemplateName();
|
|
2287
|
+
}
|
|
2288
|
+
if (argument.name === "path" && ref.uri) {
|
|
2289
|
+
return await completeTemplatePath(ref.uri);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return {
|
|
2293
|
+
completion: {
|
|
2294
|
+
values: [],
|
|
2295
|
+
total: 0,
|
|
2296
|
+
hasMore: false
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
});
|
|
2300
|
+
server.setRequestHandler(import_types10.ReadResourceRequestSchema, async (request) => {
|
|
2301
|
+
const { uri } = request.params;
|
|
2302
|
+
const templateFileResult = await readTemplateFileResource(uri);
|
|
2303
|
+
if (templateFileResult) return templateFileResult;
|
|
2304
|
+
const templateStructureResult = await readTemplateStructureResource(uri);
|
|
2305
|
+
if (templateStructureResult) return templateStructureResult;
|
|
2306
|
+
const resource = resources.find((r) => r.resource.uri === uri);
|
|
2307
|
+
if (!resource) {
|
|
2308
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
2309
|
+
}
|
|
2310
|
+
const content = await resource.read(uri);
|
|
2311
|
+
return { contents: [content] };
|
|
2312
|
+
});
|
|
2313
|
+
};
|
|
2314
|
+
var createMcpServerFromWorkspace = async (options) => {
|
|
2315
|
+
const { name, version, workspaceRoot, plugins: pluginModules, logger } = options;
|
|
2316
|
+
const context = await (0, import_facade12.createContextFromWorkspace)(workspaceRoot, logger);
|
|
2317
|
+
const loadedPlugins = await (0, import_facade12.loadPlugins)(pluginModules, context);
|
|
2318
|
+
const pluginTools = (0, import_facade12.collectExecutableToolSpecs)(loadedPlugins, context);
|
|
2319
|
+
const discoveredRoot = context.workspace.workspaceRoot;
|
|
2320
|
+
const tools = buildTools(pluginTools, pluginModules);
|
|
2321
|
+
const resources = await buildResources(pluginModules, discoveredRoot);
|
|
2322
|
+
const server = new import_server.Server({ name, version }, { capabilities: { tools: {}, resources: {}, completions: {} } });
|
|
2323
|
+
registerHandlers(server, tools, resources);
|
|
2324
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
2325
|
+
await server.connect(transport);
|
|
2326
|
+
logger?.info(
|
|
2327
|
+
`MCP server '${name}' started with ${tools.length} tools, ${resources.length} resources (auto-discovered workspace)`
|
|
2328
|
+
);
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
// src/cli.ts
|
|
2332
|
+
var args = process.argv.slice(2);
|
|
2333
|
+
if (!args.includes("--stdio")) {
|
|
2334
|
+
console.error("Usage: pf-mcp --stdio");
|
|
2335
|
+
console.error("");
|
|
2336
|
+
console.error("The --stdio flag is required for MCP server communication.");
|
|
2337
|
+
console.error("This server is designed to be used with VS Code Copilot Agent Mode.");
|
|
2338
|
+
process.exit(1);
|
|
2339
|
+
}
|
|
2340
|
+
createMcpServerFromWorkspace({
|
|
2341
|
+
name: "pf-mcp",
|
|
2342
|
+
version: "0.1.0",
|
|
2343
|
+
plugins: []
|
|
2344
|
+
}).catch((error) => {
|
|
2345
|
+
console.error("Failed to start MCP server:", error);
|
|
2346
|
+
process.exit(1);
|
|
2347
|
+
});
|
|
2348
|
+
//# sourceMappingURL=cli.cjs.map
|