@cyclonedx/cdxgen 12.3.0 → 12.3.2
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 +15 -5
- package/bin/audit.js +7 -0
- package/bin/cdxgen.js +241 -81
- package/bin/repl.js +138 -0
- package/data/rules/ai-agent-governance.yaml +249 -0
- package/data/rules/dependency-sources.yaml +41 -0
- package/data/rules/mcp-servers.yaml +304 -0
- package/data/rules/package-integrity.yaml +123 -0
- package/lib/audit/index.js +353 -29
- package/lib/audit/index.poku.js +247 -7
- package/lib/audit/reporters.js +26 -0
- package/lib/audit/scoring.js +262 -13
- package/lib/audit/scoring.poku.js +179 -0
- package/lib/audit/targets.js +391 -2
- package/lib/audit/targets.poku.js +416 -3
- package/lib/cli/index.js +588 -45
- package/lib/cli/index.poku.js +735 -1
- package/lib/evinser/evinser.js +8 -5
- package/lib/helpers/agentFormulationParser.js +318 -0
- package/lib/helpers/aiInventory.js +262 -0
- package/lib/helpers/aiInventory.poku.js +111 -0
- package/lib/helpers/analyzer.js +1769 -0
- package/lib/helpers/analyzer.poku.js +284 -3
- package/lib/helpers/auditCategories.js +76 -0
- package/lib/helpers/ciParsers/githubActions.js +140 -16
- package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
- package/lib/helpers/communityAiConfigParser.js +672 -0
- package/lib/helpers/communityAiConfigParser.poku.js +63 -0
- package/lib/helpers/depsUtils.js +108 -0
- package/lib/helpers/depsUtils.poku.js +72 -1
- package/lib/helpers/display.js +325 -3
- package/lib/helpers/display.poku.js +301 -0
- package/lib/helpers/formulationParsers.js +28 -0
- package/lib/helpers/formulationParsers.poku.js +504 -1
- package/lib/helpers/jsonLike.js +102 -0
- package/lib/helpers/jsonLike.poku.js +34 -0
- package/lib/helpers/mcp.js +248 -0
- package/lib/helpers/mcp.poku.js +101 -0
- package/lib/helpers/mcpConfigParser.js +656 -0
- package/lib/helpers/mcpConfigParser.poku.js +126 -0
- package/lib/helpers/mcpDiscovery.js +84 -0
- package/lib/helpers/mcpDiscovery.poku.js +21 -0
- package/lib/helpers/protobom.js +3 -3
- package/lib/helpers/provenanceUtils.js +29 -4
- package/lib/helpers/provenanceUtils.poku.js +29 -3
- package/lib/helpers/registryProvenance.js +210 -0
- package/lib/helpers/registryProvenance.poku.js +144 -0
- package/lib/helpers/rustFormulationParser.js +330 -0
- package/lib/helpers/source.js +21 -2
- package/lib/helpers/source.poku.js +38 -0
- package/lib/helpers/utils.js +1331 -83
- package/lib/helpers/utils.poku.js +599 -188
- package/lib/helpers/vsixutils.js +12 -4
- package/lib/helpers/vsixutils.poku.js +34 -0
- package/lib/managers/binary.js +36 -12
- package/lib/managers/binary.poku.js +68 -0
- package/lib/managers/docker.js +59 -9
- package/lib/managers/docker.poku.js +61 -0
- package/lib/managers/piptree.js +12 -7
- package/lib/managers/piptree.poku.js +44 -0
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +15 -0
- package/lib/stages/postgen/auditBom.js +20 -6
- package/lib/stages/postgen/auditBom.poku.js +694 -1
- package/lib/stages/postgen/postgen.js +262 -11
- package/lib/stages/postgen/postgen.poku.js +306 -2
- package/lib/stages/postgen/ruleEngine.js +49 -1
- package/lib/stages/postgen/spdxConverter.poku.js +70 -0
- package/lib/stages/pregen/pregen.js +6 -4
- package/package.json +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts +12 -0
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -8
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/aiInventory.d.ts +23 -0
- package/types/lib/helpers/aiInventory.d.ts.map +1 -0
- package/types/lib/helpers/analyzer.d.ts +10 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/auditCategories.d.ts +12 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +8 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +17 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/jsonLike.d.ts +4 -0
- package/types/lib/helpers/jsonLike.d.ts.map +1 -0
- package/types/lib/helpers/mcp.d.ts +29 -0
- package/types/lib/helpers/mcp.d.ts.map +1 -0
- package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
- package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +5 -3
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
- package/types/lib/helpers/registryProvenance.d.ts +9 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
- package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
- package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +31 -1
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
@@ -41,4 +41,507 @@ describe("addFormulationSection()", () => {
|
|
|
41
41
|
rmSync(tmpDir, { force: true, recursive: true });
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
it("adds AI agent instruction components and inferred MCP endpoints", () => {
|
|
46
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-formulation-"));
|
|
47
|
+
mkdirSync(path.join(tmpDir, ".github"), { recursive: true });
|
|
48
|
+
writeFileSync(
|
|
49
|
+
path.join(tmpDir, "AGENTS.md"),
|
|
50
|
+
[
|
|
51
|
+
"# Agent policy",
|
|
52
|
+
"",
|
|
53
|
+
"Connect to http://localhost:3000/mcp for local testing.",
|
|
54
|
+
"Connect to https://demo.ngrok-free.app/mcp for remote validation.",
|
|
55
|
+
"Use @acme/mcp-server if you need a wrapper.",
|
|
56
|
+
"Bearer sk_test_agent_token_value",
|
|
57
|
+
"<!-- hidden \u200B prompt -->",
|
|
58
|
+
].join("\n"),
|
|
59
|
+
);
|
|
60
|
+
writeFileSync(
|
|
61
|
+
path.join(tmpDir, ".github", "copilot-instructions.md"),
|
|
62
|
+
"Use Anthropic and Gemini for model routing.",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = addFormulationSection(tmpDir, { specVersion: 1.7 });
|
|
67
|
+
const formulation = result.formulation[0];
|
|
68
|
+
const agentComponent = formulation.components.find(
|
|
69
|
+
(component) =>
|
|
70
|
+
getProp(component, "cdx:agent:inventorySource") === "agent-file",
|
|
71
|
+
);
|
|
72
|
+
assert.ok(agentComponent, "expected AI agent formulation component");
|
|
73
|
+
assert.strictEqual(
|
|
74
|
+
getProp(agentComponent, "cdx:file:hasHiddenUnicode"),
|
|
75
|
+
"true",
|
|
76
|
+
);
|
|
77
|
+
assert.strictEqual(
|
|
78
|
+
getProp(agentComponent, "cdx:agent:hasPublicMcpEndpoint"),
|
|
79
|
+
"true",
|
|
80
|
+
);
|
|
81
|
+
assert.strictEqual(
|
|
82
|
+
getProp(agentComponent, "cdx:agent:hasTunnelReference"),
|
|
83
|
+
"true",
|
|
84
|
+
);
|
|
85
|
+
assert.strictEqual(
|
|
86
|
+
getProp(agentComponent, "cdx:agent:hasNonOfficialMcpReference"),
|
|
87
|
+
"true",
|
|
88
|
+
);
|
|
89
|
+
assert.strictEqual(
|
|
90
|
+
getProp(agentComponent, "cdx:agent:credentialExposure"),
|
|
91
|
+
"true",
|
|
92
|
+
);
|
|
93
|
+
assert.match(getProp(agentComponent, "cdx:agent:hiddenMcpUrls"), /ngrok/);
|
|
94
|
+
assert.ok(
|
|
95
|
+
formulation.services?.some(
|
|
96
|
+
(service) =>
|
|
97
|
+
getProp(service, "cdx:mcp:inventorySource") === "agent-file" &&
|
|
98
|
+
service.endpoints?.some((endpoint) => endpoint.includes("/mcp")),
|
|
99
|
+
),
|
|
100
|
+
"expected inferred MCP service from AI agent file",
|
|
101
|
+
);
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("adds MCP config components and configured services from client config files", () => {
|
|
108
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-formulation-"));
|
|
109
|
+
mkdirSync(path.join(tmpDir, ".vscode"), { recursive: true });
|
|
110
|
+
writeFileSync(
|
|
111
|
+
path.join(tmpDir, ".vscode", "mcp.json"),
|
|
112
|
+
JSON.stringify(
|
|
113
|
+
{
|
|
114
|
+
mcpServers: {
|
|
115
|
+
localFs: {
|
|
116
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
117
|
+
command: "npx",
|
|
118
|
+
env: {
|
|
119
|
+
OPENAI_API_KEY: "$OPENAI_API_KEY",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
remoteGateway: {
|
|
123
|
+
auth: {
|
|
124
|
+
registration_endpoint: "https://auth.example.com/register",
|
|
125
|
+
},
|
|
126
|
+
client_id: "shared-static-client",
|
|
127
|
+
endpoint: "https://demo.ngrok-free.app/mcp",
|
|
128
|
+
headers: {
|
|
129
|
+
Authorization: "Bearer sk_test_config_token_value",
|
|
130
|
+
},
|
|
131
|
+
passthroughToken: true,
|
|
132
|
+
transport: "http",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
null,
|
|
137
|
+
2,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const result = addFormulationSection(tmpDir, { specVersion: 1.7 });
|
|
143
|
+
const formulation = result.formulation[0];
|
|
144
|
+
const configComponent = formulation.components.find(
|
|
145
|
+
(component) => getProp(component, "cdx:file:kind") === "mcp-config",
|
|
146
|
+
);
|
|
147
|
+
assert.ok(configComponent, "expected MCP config formulation component");
|
|
148
|
+
assert.strictEqual(
|
|
149
|
+
getProp(configComponent, "cdx:mcp:configuredServiceCount"),
|
|
150
|
+
"2",
|
|
151
|
+
);
|
|
152
|
+
assert.strictEqual(
|
|
153
|
+
getProp(configComponent, "cdx:mcp:credentialExposure"),
|
|
154
|
+
"true",
|
|
155
|
+
);
|
|
156
|
+
const remoteService = formulation.services?.find(
|
|
157
|
+
(service) => service.name === "remoteGateway",
|
|
158
|
+
);
|
|
159
|
+
assert.ok(remoteService, "expected remote configured MCP service");
|
|
160
|
+
assert.strictEqual(
|
|
161
|
+
getProp(remoteService, "cdx:mcp:inventorySource"),
|
|
162
|
+
"config-file",
|
|
163
|
+
);
|
|
164
|
+
assert.strictEqual(
|
|
165
|
+
getProp(remoteService, "cdx:mcp:security:confusedDeputyRisk"),
|
|
166
|
+
"high",
|
|
167
|
+
);
|
|
168
|
+
assert.strictEqual(
|
|
169
|
+
getProp(remoteService, "cdx:mcp:security:tokenPassthroughRisk"),
|
|
170
|
+
"high",
|
|
171
|
+
);
|
|
172
|
+
assert.strictEqual(
|
|
173
|
+
getProp(remoteService, "cdx:mcp:credentialExposure"),
|
|
174
|
+
"true",
|
|
175
|
+
);
|
|
176
|
+
assert.strictEqual(
|
|
177
|
+
getProp(remoteService, "cdx:mcp:auth:supportsDCR"),
|
|
178
|
+
"true",
|
|
179
|
+
);
|
|
180
|
+
} finally {
|
|
181
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("discovers community provider agents, tools, skills, and config-derived services", () => {
|
|
186
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-formulation-"));
|
|
187
|
+
mkdirSync(path.join(tmpDir, ".opencode", "agents"), { recursive: true });
|
|
188
|
+
mkdirSync(path.join(tmpDir, ".opencode", "tools"), { recursive: true });
|
|
189
|
+
mkdirSync(path.join(tmpDir, ".opencode", "skills", "git-release"), {
|
|
190
|
+
recursive: true,
|
|
191
|
+
});
|
|
192
|
+
mkdirSync(path.join(tmpDir, ".nanocoder", "agents"), { recursive: true });
|
|
193
|
+
mkdirSync(path.join(tmpDir, ".nanocoder", "commands"), {
|
|
194
|
+
recursive: true,
|
|
195
|
+
});
|
|
196
|
+
mkdirSync(path.join(tmpDir, "config"), { recursive: true });
|
|
197
|
+
writeFileSync(
|
|
198
|
+
path.join(tmpDir, "opencode.jsonc"),
|
|
199
|
+
`{
|
|
200
|
+
// opencode project config
|
|
201
|
+
"agent": {
|
|
202
|
+
"reviewer": {
|
|
203
|
+
"description": "Review changes for security issues",
|
|
204
|
+
"mode": "subagent",
|
|
205
|
+
"model": "anthropic/claude-sonnet-4-20250514"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"mcp": {
|
|
209
|
+
"remoteDocs": {
|
|
210
|
+
"type": "remote",
|
|
211
|
+
"url": "https://docs.example.com/mcp",
|
|
212
|
+
"oauth": {}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}`,
|
|
216
|
+
);
|
|
217
|
+
writeFileSync(
|
|
218
|
+
path.join(tmpDir, ".mcp.json"),
|
|
219
|
+
JSON.stringify({
|
|
220
|
+
mcpServers: {
|
|
221
|
+
nanocoderFs: {
|
|
222
|
+
command: "npx",
|
|
223
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "./src"],
|
|
224
|
+
env: {
|
|
225
|
+
GITHUB_TOKEN: "$GITHUB_TOKEN",
|
|
226
|
+
},
|
|
227
|
+
transport: "stdio",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
writeFileSync(
|
|
233
|
+
path.join(tmpDir, ".opencode", "agents", "review.md"),
|
|
234
|
+
[
|
|
235
|
+
"---",
|
|
236
|
+
"description: Reviews code for bugs and quality",
|
|
237
|
+
"mode: subagent",
|
|
238
|
+
"model: anthropic/claude-sonnet-4-20250514",
|
|
239
|
+
"---",
|
|
240
|
+
"Focus on code review findings.",
|
|
241
|
+
].join("\n"),
|
|
242
|
+
);
|
|
243
|
+
writeFileSync(
|
|
244
|
+
path.join(tmpDir, ".opencode", "tools", "database.ts"),
|
|
245
|
+
[
|
|
246
|
+
'import { tool } from "@opencode-ai/plugin";',
|
|
247
|
+
"",
|
|
248
|
+
"export default tool({",
|
|
249
|
+
' description: "Query the project database",',
|
|
250
|
+
" args: {},",
|
|
251
|
+
" async execute() {",
|
|
252
|
+
' return "ok";',
|
|
253
|
+
" },",
|
|
254
|
+
"});",
|
|
255
|
+
].join("\n"),
|
|
256
|
+
);
|
|
257
|
+
writeFileSync(
|
|
258
|
+
path.join(tmpDir, ".opencode", "skills", "git-release", "SKILL.md"),
|
|
259
|
+
[
|
|
260
|
+
"---",
|
|
261
|
+
"name: git-release",
|
|
262
|
+
"description: Prepare consistent releases",
|
|
263
|
+
"license: MIT",
|
|
264
|
+
"compatibility: opencode",
|
|
265
|
+
"---",
|
|
266
|
+
"Use this skill when preparing a release.",
|
|
267
|
+
].join("\n"),
|
|
268
|
+
);
|
|
269
|
+
writeFileSync(
|
|
270
|
+
path.join(tmpDir, ".nanocoder", "agents", "researcher.md"),
|
|
271
|
+
[
|
|
272
|
+
"---",
|
|
273
|
+
"name: researcher",
|
|
274
|
+
"description: Researches code and docs",
|
|
275
|
+
"model: inherit",
|
|
276
|
+
"tools:",
|
|
277
|
+
" - read_file",
|
|
278
|
+
" - search_file_contents",
|
|
279
|
+
"---",
|
|
280
|
+
"Search first, then summarize.",
|
|
281
|
+
].join("\n"),
|
|
282
|
+
);
|
|
283
|
+
writeFileSync(
|
|
284
|
+
path.join(tmpDir, ".nanocoder", "commands", "fix.md"),
|
|
285
|
+
[
|
|
286
|
+
"---",
|
|
287
|
+
"description: Apply the standard fix workflow",
|
|
288
|
+
"category: engineering",
|
|
289
|
+
"tags: [bugfix, workflow]",
|
|
290
|
+
"triggers: [fix bug, repair issue]",
|
|
291
|
+
"---",
|
|
292
|
+
"1. Reproduce the issue",
|
|
293
|
+
"2. Fix it",
|
|
294
|
+
].join("\n"),
|
|
295
|
+
);
|
|
296
|
+
writeFileSync(
|
|
297
|
+
path.join(tmpDir, "langgraph.json"),
|
|
298
|
+
JSON.stringify({
|
|
299
|
+
dependencies: ["langchain_openai", "./graphs"],
|
|
300
|
+
env: ".env",
|
|
301
|
+
graphs: {
|
|
302
|
+
planner: "./graphs/planner.py:graph",
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
writeFileSync(
|
|
307
|
+
path.join(tmpDir, "config", "agents.yaml"),
|
|
308
|
+
[
|
|
309
|
+
"researcher:",
|
|
310
|
+
' role: "Researcher"',
|
|
311
|
+
' goal: "Find the best answer"',
|
|
312
|
+
' backstory: "Helpful teammate"',
|
|
313
|
+
" tools:",
|
|
314
|
+
" - search_docs",
|
|
315
|
+
].join("\n"),
|
|
316
|
+
);
|
|
317
|
+
writeFileSync(
|
|
318
|
+
path.join(tmpDir, "agents.py"),
|
|
319
|
+
[
|
|
320
|
+
"from crewai import Agent",
|
|
321
|
+
"",
|
|
322
|
+
"class CustomAgents:",
|
|
323
|
+
" def researcher(self):",
|
|
324
|
+
" return Agent(",
|
|
325
|
+
' role="Researcher",',
|
|
326
|
+
' goal="Find answers",',
|
|
327
|
+
' backstory="Helpful teammate",',
|
|
328
|
+
" )",
|
|
329
|
+
].join("\n"),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const result = addFormulationSection(tmpDir, { specVersion: 1.7 });
|
|
334
|
+
const formulation = result.formulation[0];
|
|
335
|
+
assert.ok(
|
|
336
|
+
formulation.components.some(
|
|
337
|
+
(component) =>
|
|
338
|
+
getProp(component, "cdx:agent:framework") === "opencode" &&
|
|
339
|
+
getProp(component, "cdx:file:kind") === "agent-definition",
|
|
340
|
+
),
|
|
341
|
+
"expected OpenCode agent component",
|
|
342
|
+
);
|
|
343
|
+
assert.ok(
|
|
344
|
+
formulation.components.some(
|
|
345
|
+
(component) =>
|
|
346
|
+
getProp(component, "cdx:file:kind") === "skill-file" &&
|
|
347
|
+
getProp(component, "cdx:skill:name") === "git-release",
|
|
348
|
+
),
|
|
349
|
+
"expected skill file component",
|
|
350
|
+
);
|
|
351
|
+
assert.ok(
|
|
352
|
+
formulation.components.some(
|
|
353
|
+
(component) =>
|
|
354
|
+
getProp(component, "cdx:agent:framework") === "nanocoder" &&
|
|
355
|
+
getProp(component, "cdx:file:kind") === "custom-command",
|
|
356
|
+
),
|
|
357
|
+
"expected Nanocoder custom command component",
|
|
358
|
+
);
|
|
359
|
+
assert.ok(
|
|
360
|
+
formulation.components.some(
|
|
361
|
+
(component) =>
|
|
362
|
+
getProp(component, "cdx:agent:framework") === "langgraph" &&
|
|
363
|
+
getProp(component, "cdx:agent:role") === "langgraph-graph",
|
|
364
|
+
),
|
|
365
|
+
"expected LangGraph graph component",
|
|
366
|
+
);
|
|
367
|
+
assert.ok(
|
|
368
|
+
formulation.components.some(
|
|
369
|
+
(component) =>
|
|
370
|
+
getProp(component, "cdx:agent:framework") === "crewai" &&
|
|
371
|
+
getProp(component, "cdx:agent:role") === "crew-agent",
|
|
372
|
+
),
|
|
373
|
+
"expected CrewAI agent component",
|
|
374
|
+
);
|
|
375
|
+
assert.ok(
|
|
376
|
+
formulation.services?.some(
|
|
377
|
+
(service) =>
|
|
378
|
+
service.name === "remoteDocs" &&
|
|
379
|
+
getProp(service, "cdx:mcp:inventorySource") === "config-file",
|
|
380
|
+
),
|
|
381
|
+
"expected OpenCode MCP service",
|
|
382
|
+
);
|
|
383
|
+
assert.ok(
|
|
384
|
+
formulation.services?.some(
|
|
385
|
+
(service) =>
|
|
386
|
+
service.name === "planner" &&
|
|
387
|
+
getProp(service, "cdx:agent:framework") === "langgraph",
|
|
388
|
+
),
|
|
389
|
+
"expected LangGraph service",
|
|
390
|
+
);
|
|
391
|
+
assert.ok(
|
|
392
|
+
formulation.services?.some(
|
|
393
|
+
(service) =>
|
|
394
|
+
service.name === "nanocoderFs" &&
|
|
395
|
+
getProp(service, "cdx:mcp:configFormat") === "dot-mcp-json",
|
|
396
|
+
),
|
|
397
|
+
"expected .mcp.json service",
|
|
398
|
+
);
|
|
399
|
+
} finally {
|
|
400
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("adds Cargo and maturin formulation components for Rust build context", () => {
|
|
405
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-formulation-"));
|
|
406
|
+
writeFileSync(
|
|
407
|
+
path.join(tmpDir, "Cargo.toml"),
|
|
408
|
+
`[package]
|
|
409
|
+
name = "cargo-demo"
|
|
410
|
+
version = "1.0.0"
|
|
411
|
+
build = "build.rs"
|
|
412
|
+
rust-version = "1.78"
|
|
413
|
+
|
|
414
|
+
[build-dependencies]
|
|
415
|
+
cc = "1.0.0"
|
|
416
|
+
openssl-sys = "0.9.0"
|
|
417
|
+
|
|
418
|
+
[profile.release]
|
|
419
|
+
lto = true
|
|
420
|
+
`,
|
|
421
|
+
);
|
|
422
|
+
writeFileSync(
|
|
423
|
+
path.join(tmpDir, "build.rs"),
|
|
424
|
+
[
|
|
425
|
+
'println!("cargo:rustc-link-lib=ssl");',
|
|
426
|
+
'std::process::Command::new("cc");',
|
|
427
|
+
'std::fs::write("generated.rs", "");',
|
|
428
|
+
].join("\n"),
|
|
429
|
+
);
|
|
430
|
+
writeFileSync(
|
|
431
|
+
path.join(tmpDir, "pyproject.toml"),
|
|
432
|
+
`[build-system]
|
|
433
|
+
requires = ["maturin>=1.0,<2.0"]
|
|
434
|
+
build-backend = "maturin"
|
|
435
|
+
|
|
436
|
+
[project]
|
|
437
|
+
name = "maturin-demo"
|
|
438
|
+
|
|
439
|
+
[tool.maturin]
|
|
440
|
+
bindings = "pyo3"
|
|
441
|
+
module-name = "maturin_demo._native"
|
|
442
|
+
features = ["pyo3/extension-module"]
|
|
443
|
+
`,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const result = addFormulationSection(tmpDir, { specVersion: 1.7 });
|
|
448
|
+
const formulation = result.formulation[0];
|
|
449
|
+
const cargoComponent = formulation.components.find(
|
|
450
|
+
(component) => getProp(component, "cdx:rust:buildTool") === "cargo",
|
|
451
|
+
);
|
|
452
|
+
const maturinComponent = formulation.components.find(
|
|
453
|
+
(component) => getProp(component, "cdx:rust:buildTool") === "maturin",
|
|
454
|
+
);
|
|
455
|
+
assert.ok(cargoComponent, "expected cargo formulation component");
|
|
456
|
+
assert.strictEqual(
|
|
457
|
+
getProp(cargoComponent, "cdx:cargo:hasNativeBuild"),
|
|
458
|
+
"true",
|
|
459
|
+
);
|
|
460
|
+
assert.strictEqual(
|
|
461
|
+
getProp(cargoComponent, "cdx:cargo:nativeBuildIndicators"),
|
|
462
|
+
"cc, openssl-sys",
|
|
463
|
+
);
|
|
464
|
+
assert.strictEqual(
|
|
465
|
+
getProp(cargoComponent, "cdx:cargo:hasBuildScript"),
|
|
466
|
+
"true",
|
|
467
|
+
);
|
|
468
|
+
assert.match(
|
|
469
|
+
getProp(cargoComponent, "cdx:cargo:buildScriptCapabilities"),
|
|
470
|
+
/process-execution/,
|
|
471
|
+
);
|
|
472
|
+
assert.match(
|
|
473
|
+
getProp(cargoComponent, "cdx:cargo:buildScriptCapabilities"),
|
|
474
|
+
/linker-directives/,
|
|
475
|
+
);
|
|
476
|
+
assert.match(
|
|
477
|
+
getProp(cargoComponent, "cdx:cargo:buildScriptCapabilities"),
|
|
478
|
+
/file-generation/,
|
|
479
|
+
);
|
|
480
|
+
assert.strictEqual(
|
|
481
|
+
getProp(cargoComponent, "cdx:cargo:rustVersion"),
|
|
482
|
+
"1.78",
|
|
483
|
+
);
|
|
484
|
+
assert.strictEqual(
|
|
485
|
+
getProp(cargoComponent, "cdx:cargo:releaseProfiles"),
|
|
486
|
+
"release",
|
|
487
|
+
);
|
|
488
|
+
assert.ok(maturinComponent, "expected maturin formulation component");
|
|
489
|
+
assert.strictEqual(
|
|
490
|
+
getProp(maturinComponent, "cdx:maturin:buildBackend"),
|
|
491
|
+
"maturin",
|
|
492
|
+
);
|
|
493
|
+
assert.strictEqual(
|
|
494
|
+
getProp(maturinComponent, "cdx:maturin:bindings"),
|
|
495
|
+
"pyo3",
|
|
496
|
+
);
|
|
497
|
+
assert.strictEqual(
|
|
498
|
+
getProp(maturinComponent, "cdx:maturin:moduleName"),
|
|
499
|
+
"maturin_demo._native",
|
|
500
|
+
);
|
|
501
|
+
} finally {
|
|
502
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("adds virtual-workspace formulation metadata for Cargo workspaces", () => {
|
|
507
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-formulation-"));
|
|
508
|
+
const memberDir = path.join(tmpDir, "crates", "member-a");
|
|
509
|
+
mkdirSync(memberDir, { recursive: true });
|
|
510
|
+
writeFileSync(
|
|
511
|
+
path.join(tmpDir, "Cargo.toml"),
|
|
512
|
+
`[workspace]
|
|
513
|
+
members = ["crates/*"]
|
|
514
|
+
`,
|
|
515
|
+
);
|
|
516
|
+
writeFileSync(
|
|
517
|
+
path.join(memberDir, "Cargo.toml"),
|
|
518
|
+
`[package]
|
|
519
|
+
name = "member-a"
|
|
520
|
+
version = "1.0.0"
|
|
521
|
+
`,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const result = addFormulationSection(tmpDir, { specVersion: 1.7 });
|
|
526
|
+
const formulation = result.formulation[0];
|
|
527
|
+
const workspaceComponent = formulation.components.find(
|
|
528
|
+
(component) =>
|
|
529
|
+
getProp(component, "cdx:cargo:manifestMode") === "workspace",
|
|
530
|
+
);
|
|
531
|
+
assert.ok(
|
|
532
|
+
workspaceComponent,
|
|
533
|
+
"expected cargo workspace formulation component",
|
|
534
|
+
);
|
|
535
|
+
assert.strictEqual(
|
|
536
|
+
getProp(workspaceComponent, "cdx:cargo:hasWorkspaceMembers"),
|
|
537
|
+
"true",
|
|
538
|
+
);
|
|
539
|
+
assert.strictEqual(
|
|
540
|
+
getProp(workspaceComponent, "cdx:cargo:workspaceMembers"),
|
|
541
|
+
"crates/*",
|
|
542
|
+
);
|
|
543
|
+
} finally {
|
|
544
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
44
547
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether the quote at `index` is escaped by an odd-length run of
|
|
3
|
+
* backslashes immediately preceding it.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} raw Raw JSON-like text being scanned
|
|
6
|
+
* @param {number} index Index of the quote character to evaluate
|
|
7
|
+
* @returns {boolean} `true` when the quote is escaped and should not terminate
|
|
8
|
+
* the current string literal
|
|
9
|
+
*/
|
|
10
|
+
function isEscapedQuote(raw, index) {
|
|
11
|
+
let backslashCount = 0;
|
|
12
|
+
let lookBehind = index - 1;
|
|
13
|
+
while (lookBehind >= 0 && raw[lookBehind] === "\\") {
|
|
14
|
+
backslashCount += 1;
|
|
15
|
+
lookBehind -= 1;
|
|
16
|
+
}
|
|
17
|
+
return backslashCount % 2 === 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stripJsonComments(raw) {
|
|
21
|
+
let output = "";
|
|
22
|
+
let inString = false;
|
|
23
|
+
let stringQuote = "";
|
|
24
|
+
for (let index = 0; index < raw.length; index++) {
|
|
25
|
+
const char = raw[index];
|
|
26
|
+
const nextChar = raw[index + 1];
|
|
27
|
+
if (inString) {
|
|
28
|
+
output += char;
|
|
29
|
+
if (char === stringQuote && !isEscapedQuote(raw, index)) {
|
|
30
|
+
inString = false;
|
|
31
|
+
stringQuote = "";
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (char === '"' || char === "'") {
|
|
36
|
+
inString = true;
|
|
37
|
+
stringQuote = char;
|
|
38
|
+
output += char;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (char === "/" && nextChar === "/") {
|
|
42
|
+
while (index < raw.length && raw[index] !== "\n") {
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
if (index < raw.length) {
|
|
46
|
+
output += raw[index];
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === "/" && nextChar === "*") {
|
|
51
|
+
index += 2;
|
|
52
|
+
while (
|
|
53
|
+
index < raw.length &&
|
|
54
|
+
!(raw[index] === "*" && raw[index + 1] === "/")
|
|
55
|
+
) {
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
index += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
output += char;
|
|
62
|
+
}
|
|
63
|
+
return output;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stripJsonTrailingCommas(raw) {
|
|
67
|
+
let output = "";
|
|
68
|
+
let inString = false;
|
|
69
|
+
let stringQuote = "";
|
|
70
|
+
for (let index = 0; index < raw.length; index++) {
|
|
71
|
+
const char = raw[index];
|
|
72
|
+
if (inString) {
|
|
73
|
+
output += char;
|
|
74
|
+
if (char === stringQuote && !isEscapedQuote(raw, index)) {
|
|
75
|
+
inString = false;
|
|
76
|
+
stringQuote = "";
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (char === '"' || char === "'") {
|
|
81
|
+
inString = true;
|
|
82
|
+
stringQuote = char;
|
|
83
|
+
output += char;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === ",") {
|
|
87
|
+
let lookAhead = index + 1;
|
|
88
|
+
while (lookAhead < raw.length && /\s/u.test(raw[lookAhead])) {
|
|
89
|
+
lookAhead += 1;
|
|
90
|
+
}
|
|
91
|
+
if (raw[lookAhead] === "}" || raw[lookAhead] === "]") {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
output += char;
|
|
96
|
+
}
|
|
97
|
+
return output;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseJsonLike(raw) {
|
|
101
|
+
return JSON.parse(stripJsonTrailingCommas(stripJsonComments(raw)));
|
|
102
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { parseJsonLike, stripJsonComments } from "./jsonLike.js";
|
|
4
|
+
|
|
5
|
+
describe("jsonLike", () => {
|
|
6
|
+
it("preserves escaped quotes while stripping comments", () => {
|
|
7
|
+
const parsedMessage = 'escaped quote: " // not a comment';
|
|
8
|
+
const rawMessage = String.raw`escaped quote: \" // not a comment`;
|
|
9
|
+
const raw = String.raw`{
|
|
10
|
+
"message": "escaped quote: \" // not a comment",
|
|
11
|
+
// trailing comment
|
|
12
|
+
"enabled": true
|
|
13
|
+
}`;
|
|
14
|
+
const stripped = stripJsonComments(raw);
|
|
15
|
+
assert.ok(stripped.includes(rawMessage));
|
|
16
|
+
assert.ok(!stripped.includes("trailing comment"));
|
|
17
|
+
assert.deepStrictEqual(parseJsonLike(raw), {
|
|
18
|
+
enabled: true,
|
|
19
|
+
message: parsedMessage,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("preserves comment markers after escaped backslashes inside strings", () => {
|
|
24
|
+
const raw = `{
|
|
25
|
+
"path": "C:\\\\\\\\temp\\\\\\\\file // keep",
|
|
26
|
+
/* block comment */
|
|
27
|
+
"count": 1
|
|
28
|
+
}`;
|
|
29
|
+
assert.deepStrictEqual(parseJsonLike(raw), {
|
|
30
|
+
count: 1,
|
|
31
|
+
path: "C:\\\\temp\\\\file // keep",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|