superacli 1.0.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/.env.example +14 -0
- package/README.md +173 -0
- package/cli/adapters/http.js +72 -0
- package/cli/adapters/mcp.js +193 -0
- package/cli/adapters/openapi.js +160 -0
- package/cli/ask.js +208 -0
- package/cli/config.js +133 -0
- package/cli/executor.js +117 -0
- package/cli/help-json.js +46 -0
- package/cli/mcp-local.js +72 -0
- package/cli/plan-runtime.js +32 -0
- package/cli/planner.js +67 -0
- package/cli/skills.js +240 -0
- package/cli/supercli.js +704 -0
- package/docs/features/adapters.md +25 -0
- package/docs/features/agent-friendly.md +28 -0
- package/docs/features/ask.md +32 -0
- package/docs/features/config-sync.md +22 -0
- package/docs/features/execution-plans.md +25 -0
- package/docs/features/observability.md +22 -0
- package/docs/features/skills.md +25 -0
- package/docs/features/storage.md +25 -0
- package/docs/features/workflows.md +33 -0
- package/docs/initial/AGENTS_FRIENDLY_TOOLS.md +553 -0
- package/docs/initial/agent-friendly.md +447 -0
- package/docs/initial/architecture.md +436 -0
- package/docs/initial/built-in-mcp-server.md +64 -0
- package/docs/initial/command-plan.md +532 -0
- package/docs/initial/core-features-2.md +428 -0
- package/docs/initial/core-features.md +366 -0
- package/docs/initial/dag.md +20 -0
- package/docs/initial/description.txt +9 -0
- package/docs/initial/idea.txt +564 -0
- package/docs/initial/initial-spec-details.md +726 -0
- package/docs/initial/initial-spec.md +731 -0
- package/docs/initial/mcp-local-mode.md +53 -0
- package/docs/initial/mcp-sse-mode.md +54 -0
- package/docs/initial/skills-support.md +246 -0
- package/docs/initial/storage-adapter-example.md +155 -0
- package/docs/initial/supercli-vs-gwc.md +109 -0
- package/examples/mcp-sse/install-demo.js +86 -0
- package/examples/mcp-sse/server.js +81 -0
- package/examples/mcp-stdio/install-demo.js +78 -0
- package/examples/mcp-stdio/server.js +50 -0
- package/package.json +21 -0
- package/server/app.js +59 -0
- package/server/public/app.js +18 -0
- package/server/routes/ask.js +92 -0
- package/server/routes/commands.js +126 -0
- package/server/routes/config.js +58 -0
- package/server/routes/jobs.js +122 -0
- package/server/routes/mcp.js +79 -0
- package/server/routes/plans.js +134 -0
- package/server/routes/specs.js +79 -0
- package/server/services/configService.js +88 -0
- package/server/storage/adapter.js +32 -0
- package/server/storage/file.js +64 -0
- package/server/storage/mongo.js +55 -0
- package/server/views/command-edit.ejs +110 -0
- package/server/views/commands.ejs +49 -0
- package/server/views/jobs.ejs +72 -0
- package/server/views/layout.ejs +42 -0
- package/server/views/mcp.ejs +80 -0
- package/server/views/partials/foot.ejs +5 -0
- package/server/views/partials/head.ejs +27 -0
- package/server/views/specs.ejs +91 -0
- package/tests/test-cli.js +367 -0
- package/tests/test-mcp.js +189 -0
- package/tests/test-openapi.js +101 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SUPERCLI CLI Integration Tests
|
|
4
|
+
* Tests all built-in commands, output modes, and error handling.
|
|
5
|
+
* Requires: server running on SUPERCLI_SERVER (default http://localhost:3000)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const SERVER = process.env.SUPERCLI_SERVER || "http://127.0.0.1:3000";
|
|
12
|
+
const CLI = path.join(__dirname, "..", "cli", "supercli.js");
|
|
13
|
+
const SERVER_AVAILABLE = (() => {
|
|
14
|
+
try {
|
|
15
|
+
execSync(`curl -s --max-time 2 ${SERVER}/api/config > /dev/null`, {
|
|
16
|
+
stdio: "ignore",
|
|
17
|
+
});
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
const run = (args, opts = {}) => {
|
|
24
|
+
try {
|
|
25
|
+
const result = execSync(`SUPERCLI_SERVER=${SERVER} node ${CLI} ${args}`, {
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
timeout: 15000,
|
|
28
|
+
env: { ...process.env, SUPERCLI_SERVER: SERVER },
|
|
29
|
+
...opts,
|
|
30
|
+
});
|
|
31
|
+
return { ok: true, output: result.trim() };
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
output: (err.stdout || "").trim(),
|
|
36
|
+
stderr: (err.stderr || "").trim(),
|
|
37
|
+
code: err.status,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const runNoServer = (args, opts = {}) => {
|
|
43
|
+
try {
|
|
44
|
+
const env = { ...process.env };
|
|
45
|
+
delete env.SUPERCLI_SERVER;
|
|
46
|
+
const result = execSync(`node ${CLI} ${args}`, {
|
|
47
|
+
encoding: "utf-8",
|
|
48
|
+
timeout: 15000,
|
|
49
|
+
env,
|
|
50
|
+
...opts,
|
|
51
|
+
});
|
|
52
|
+
return { ok: true, output: result.trim() };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
output: (err.stdout || "").trim(),
|
|
57
|
+
stderr: (err.stderr || "").trim(),
|
|
58
|
+
code: err.status,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const parse = (r) => JSON.parse(r.output);
|
|
64
|
+
|
|
65
|
+
let passed = 0;
|
|
66
|
+
let failed = 0;
|
|
67
|
+
|
|
68
|
+
function test(name, fn) {
|
|
69
|
+
try {
|
|
70
|
+
fn();
|
|
71
|
+
passed++;
|
|
72
|
+
console.log(` ✅ ${name}`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
failed++;
|
|
75
|
+
console.error(` ❌ ${name}: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assert(cond, msg) {
|
|
80
|
+
if (!cond) throw new Error(msg || "Assertion failed");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log("\n⚡ SUPERCLI CLI Tests\n");
|
|
84
|
+
|
|
85
|
+
// ── Seed test data ──
|
|
86
|
+
console.log(" Seeding test data...");
|
|
87
|
+
try {
|
|
88
|
+
execSync(
|
|
89
|
+
`curl -s -X POST ${SERVER}/api/commands -H 'Content-Type: application/json' -d '${JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
namespace: "test",
|
|
92
|
+
resource: "items",
|
|
93
|
+
action: "list",
|
|
94
|
+
adapter: "http",
|
|
95
|
+
adapterConfig: {
|
|
96
|
+
method: "GET",
|
|
97
|
+
url: "https://jsonplaceholder.typicode.com/posts?_limit=2",
|
|
98
|
+
},
|
|
99
|
+
args: [],
|
|
100
|
+
description: "List test items",
|
|
101
|
+
},
|
|
102
|
+
)}'`,
|
|
103
|
+
{ encoding: "utf-8" },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
execSync(
|
|
107
|
+
`curl -s -X POST ${SERVER}/api/commands -H 'Content-Type: application/json' -d '${JSON.stringify(
|
|
108
|
+
{
|
|
109
|
+
namespace: "test",
|
|
110
|
+
resource: "items",
|
|
111
|
+
action: "get",
|
|
112
|
+
adapter: "http",
|
|
113
|
+
adapterConfig: {
|
|
114
|
+
method: "GET",
|
|
115
|
+
url: "https://jsonplaceholder.typicode.com/posts/{id}",
|
|
116
|
+
},
|
|
117
|
+
args: [{ name: "id", type: "string", required: true }],
|
|
118
|
+
description: "Get test item by ID",
|
|
119
|
+
},
|
|
120
|
+
)}'`,
|
|
121
|
+
{ encoding: "utf-8" },
|
|
122
|
+
);
|
|
123
|
+
console.log(" Seeded.\n");
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.log(" (Seed skipped — data may already exist)\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── help ──
|
|
129
|
+
test("help --json returns namespaces", () => {
|
|
130
|
+
const r = run("help --json");
|
|
131
|
+
assert(r.ok, "help should succeed");
|
|
132
|
+
const d = parse(r);
|
|
133
|
+
assert(d.version === "1.0", "version should be 1.0");
|
|
134
|
+
assert(Array.isArray(d.namespaces), "should have namespaces array");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── --help-json ──
|
|
138
|
+
test("--help-json returns capability discovery", () => {
|
|
139
|
+
run("sync --json");
|
|
140
|
+
const r = run("--help-json");
|
|
141
|
+
assert(r.ok, "help-json should succeed");
|
|
142
|
+
const d = parse(r);
|
|
143
|
+
assert(d.name === "supercli", "name should be supercli");
|
|
144
|
+
assert(d.exit_codes, "should have exit_codes");
|
|
145
|
+
assert(d.flags, "should have flags");
|
|
146
|
+
assert(d.total_commands > 0, "should have commands");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("--help-json hides sync when SUPERCLI_SERVER is not set", () => {
|
|
150
|
+
const r = runNoServer("--help-json");
|
|
151
|
+
assert(r.ok, "help-json without server should succeed");
|
|
152
|
+
const d = JSON.parse(r.output);
|
|
153
|
+
assert(
|
|
154
|
+
!d.commands.sync,
|
|
155
|
+
"sync should not be exposed without SUPERCLI_SERVER",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("sync is unavailable when SUPERCLI_SERVER is not set", () => {
|
|
160
|
+
const r = runNoServer("sync --json");
|
|
161
|
+
assert(!r.ok, "sync should fail without SUPERCLI_SERVER");
|
|
162
|
+
assert(r.code === 92, "sync should be treated as unknown command");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("plan works in local mode without SUPERCLI_SERVER", () => {
|
|
166
|
+
run("sync --json");
|
|
167
|
+
const r = runNoServer("plan test items list --json");
|
|
168
|
+
assert(r.ok, "local plan should succeed");
|
|
169
|
+
const d = JSON.parse(r.output);
|
|
170
|
+
assert(d.execution_mode === "local", "execution_mode should be local");
|
|
171
|
+
assert(d.persisted === false, "persisted should be false");
|
|
172
|
+
assert(
|
|
173
|
+
Array.isArray(d.steps) && d.steps.length > 0,
|
|
174
|
+
"should include plan steps",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("local mcp registry can add/list/remove without SUPERCLI_SERVER", () => {
|
|
179
|
+
const add = runNoServer(
|
|
180
|
+
"mcp add local-demo --url http://127.0.0.1:7777 --json",
|
|
181
|
+
);
|
|
182
|
+
assert(add.ok, "mcp add should succeed");
|
|
183
|
+
|
|
184
|
+
const list = runNoServer("mcp list --json");
|
|
185
|
+
assert(list.ok, "mcp list should succeed");
|
|
186
|
+
const listData = JSON.parse(list.output);
|
|
187
|
+
assert(
|
|
188
|
+
Array.isArray(listData.mcp_servers),
|
|
189
|
+
"list should include mcp_servers",
|
|
190
|
+
);
|
|
191
|
+
assert(
|
|
192
|
+
listData.mcp_servers.find((s) => s.name === "local-demo"),
|
|
193
|
+
"list should include local-demo",
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const remove = runNoServer("mcp remove local-demo --json");
|
|
197
|
+
assert(remove.ok, "mcp remove should succeed");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── config show ──
|
|
201
|
+
test("config show returns cache info", () => {
|
|
202
|
+
// First sync
|
|
203
|
+
run("sync --json");
|
|
204
|
+
const r = run("config show --json");
|
|
205
|
+
assert(r.ok, "config show should succeed");
|
|
206
|
+
const d = parse(r);
|
|
207
|
+
assert(d.version, "should have version");
|
|
208
|
+
assert(d.cacheFile, "should have cacheFile");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("sync command refreshes local config", () => {
|
|
212
|
+
if (!SERVER_AVAILABLE) return;
|
|
213
|
+
const r = run("sync --json");
|
|
214
|
+
assert(r.ok, "sync should succeed");
|
|
215
|
+
const d = parse(r);
|
|
216
|
+
assert(d.ok === true, "sync should return ok");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ── commands ──
|
|
220
|
+
test("commands --json lists all commands", () => {
|
|
221
|
+
const r = run("commands --json");
|
|
222
|
+
assert(r.ok, "commands should succeed");
|
|
223
|
+
const d = parse(r);
|
|
224
|
+
assert(Array.isArray(d.commands), "should have commands array");
|
|
225
|
+
assert(d.commands.length > 0, "should have at least 1 command");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ── skills list ──
|
|
229
|
+
test("skills list --json returns minimal metadata", () => {
|
|
230
|
+
const r = run("skills list --json");
|
|
231
|
+
assert(r.ok, "skills list should succeed");
|
|
232
|
+
const d = parse(r);
|
|
233
|
+
assert(Array.isArray(d.skills), "should return skills array");
|
|
234
|
+
assert(d.skills.length > 0, "should include at least one skill");
|
|
235
|
+
const first = d.skills[0];
|
|
236
|
+
assert(
|
|
237
|
+
Object.keys(first).sort().join(",") === "description,name",
|
|
238
|
+
"skill metadata should only include name and description",
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── skills teach ──
|
|
243
|
+
test("skills teach defaults to skill.md output", () => {
|
|
244
|
+
const r = run("skills teach");
|
|
245
|
+
assert(r.ok, "skills teach should succeed");
|
|
246
|
+
assert(r.output.startsWith("---"), "should start with frontmatter");
|
|
247
|
+
assert(
|
|
248
|
+
r.output.includes('skill_name: "teach_skills_usage"'),
|
|
249
|
+
"should include teach skill name",
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ── skills get ──
|
|
254
|
+
test("skills get defaults to skill.md output", () => {
|
|
255
|
+
const r = run("skills get test.items.list");
|
|
256
|
+
assert(r.ok, "skills get should succeed");
|
|
257
|
+
assert(r.output.startsWith("---"), "should start with frontmatter");
|
|
258
|
+
assert(
|
|
259
|
+
r.output.includes('command: "test items list"'),
|
|
260
|
+
"should include command field",
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("skills get can include DAG section", () => {
|
|
265
|
+
const r = run("skills get test.items.list --show-dag");
|
|
266
|
+
assert(r.ok, "skills get with dag should succeed");
|
|
267
|
+
assert(r.output.includes("dag:"), "should include dag section");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ── namespace listing ──
|
|
271
|
+
test("namespace listing returns resources", () => {
|
|
272
|
+
const r = run("test --json");
|
|
273
|
+
assert(r.ok, "namespace should succeed");
|
|
274
|
+
const d = parse(r);
|
|
275
|
+
assert(d.namespace === "test", "namespace should be test");
|
|
276
|
+
assert(Array.isArray(d.resources), "should have resources");
|
|
277
|
+
assert(d.resources.includes("items"), "should include items");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ── resource listing ──
|
|
281
|
+
test("resource listing returns actions", () => {
|
|
282
|
+
const r = run("test items --json");
|
|
283
|
+
assert(r.ok, "resource should succeed");
|
|
284
|
+
const d = parse(r);
|
|
285
|
+
assert(d.resource === "items", "resource should be items");
|
|
286
|
+
assert(Array.isArray(d.actions), "should have actions");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ── inspect ──
|
|
290
|
+
test("inspect returns command schema", () => {
|
|
291
|
+
const r = run("inspect test items get --json");
|
|
292
|
+
assert(r.ok, "inspect should succeed");
|
|
293
|
+
const d = parse(r);
|
|
294
|
+
assert(d.command === "test.items.get", "command should match");
|
|
295
|
+
assert(d.input_schema, "should have input_schema");
|
|
296
|
+
assert(d.input_schema.required.includes("id"), "id should be required");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ── --schema ──
|
|
300
|
+
test("--schema returns input/output schema", () => {
|
|
301
|
+
const r = run("test items get --schema");
|
|
302
|
+
assert(r.ok, "schema should succeed");
|
|
303
|
+
const d = parse(r);
|
|
304
|
+
assert(d.input_schema, "should have input_schema");
|
|
305
|
+
assert(d.output_schema, "should have output_schema");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ── execute command ──
|
|
309
|
+
test("execute command returns data envelope", () => {
|
|
310
|
+
const r = run("test items list --json");
|
|
311
|
+
assert(r.ok, "execute should succeed");
|
|
312
|
+
const d = parse(r);
|
|
313
|
+
assert(d.version === "1.0", "version should be 1.0");
|
|
314
|
+
assert(d.command === "test.items.list", "command should match");
|
|
315
|
+
assert(d.duration_ms >= 0, "should have duration");
|
|
316
|
+
assert(Array.isArray(d.data), "data should be array");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ── execute with path param ──
|
|
320
|
+
test("execute with required arg substitutes path param", () => {
|
|
321
|
+
const r = run("test items get --id 1 --json");
|
|
322
|
+
assert(r.ok, "execute should succeed");
|
|
323
|
+
const d = parse(r);
|
|
324
|
+
assert(d.data.id === 1, "should return item with id 1");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ── --compact flag ──
|
|
328
|
+
test("--compact returns compressed keys", () => {
|
|
329
|
+
const r = run("test items list --compact");
|
|
330
|
+
assert(r.ok, "compact should succeed");
|
|
331
|
+
const d = parse(r);
|
|
332
|
+
assert(d.v === "1.0", "version key should be v");
|
|
333
|
+
assert(d.c === "test.items.list", "command key should be c");
|
|
334
|
+
assert(d.d, "data key should be d");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── validation error ──
|
|
338
|
+
test("missing required arg returns validation error", () => {
|
|
339
|
+
const r = run("test items get --json");
|
|
340
|
+
assert(!r.ok, "should fail");
|
|
341
|
+
assert(r.code === 82, "exit code should be 82");
|
|
342
|
+
const d = JSON.parse(r.output);
|
|
343
|
+
assert(d.error.type === "validation_error", "should be validation_error");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ── not found error ──
|
|
347
|
+
test("unknown command returns resource_not_found", () => {
|
|
348
|
+
const r = run("nonexistent foo bar --json");
|
|
349
|
+
assert(!r.ok, "should fail");
|
|
350
|
+
assert(r.code === 92, "exit code should be 92");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ── plan command ──
|
|
354
|
+
test("plan creates execution plan", () => {
|
|
355
|
+
if (!SERVER_AVAILABLE) return;
|
|
356
|
+
const r = run("plan test items list --json");
|
|
357
|
+
assert(r.ok, "plan should succeed");
|
|
358
|
+
const d = parse(r);
|
|
359
|
+
assert(d.plan_id, "should have plan_id");
|
|
360
|
+
assert(Array.isArray(d.steps), "should have steps");
|
|
361
|
+
assert(d.steps.length >= 3, "should have at least 3 steps");
|
|
362
|
+
assert(d.risk_level, "should have risk_level");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ── Results ──
|
|
366
|
+
console.log(`\n Results: ${passed} passed, ${failed} failed\n`);
|
|
367
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Adapter Integration Test
|
|
4
|
+
* Spins up a mock MCP server, registers it, creates command, executes via CLI.
|
|
5
|
+
* Requires: server running on SUPERCLI_SERVER
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const http = require("http");
|
|
9
|
+
const { exec } = require("child_process");
|
|
10
|
+
const util = require("util");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
|
|
13
|
+
const execAsync = util.promisify(exec);
|
|
14
|
+
const SERVER = process.env.SUPERCLI_SERVER || "http://127.0.0.1:3000";
|
|
15
|
+
const CLI = path.join(__dirname, "..", "cli", "supercli.js");
|
|
16
|
+
const MCP_PORT = 4567;
|
|
17
|
+
|
|
18
|
+
let passed = 0,
|
|
19
|
+
failed = 0;
|
|
20
|
+
async function test(name, fn) {
|
|
21
|
+
try {
|
|
22
|
+
await fn();
|
|
23
|
+
passed++;
|
|
24
|
+
console.log(` ✅ ${name}`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
failed++;
|
|
27
|
+
console.error(` ❌ ${name}: ${err.message}`);
|
|
28
|
+
console.error(err.stack);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function assert(cond, msg) {
|
|
32
|
+
if (!cond) throw new Error(msg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function api(method, urlPath, body) {
|
|
36
|
+
const bodyStr = body ? JSON.stringify(body).replace(/'/g, "'\\''") : "";
|
|
37
|
+
const cmd = body
|
|
38
|
+
? `curl -s -X ${method} '${SERVER}${urlPath}' -H 'Content-Type: application/json' -d '${bodyStr}'`
|
|
39
|
+
: `curl -s -X ${method} '${SERVER}${urlPath}'`;
|
|
40
|
+
const { stdout } = await execAsync(cmd, { encoding: "utf-8" });
|
|
41
|
+
return JSON.parse(stdout);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function cli(args) {
|
|
45
|
+
try {
|
|
46
|
+
const { stdout } = await execAsync(
|
|
47
|
+
`SUPERCLI_SERVER=${SERVER} node ${CLI} ${args}`,
|
|
48
|
+
{
|
|
49
|
+
encoding: "utf-8",
|
|
50
|
+
timeout: 15000,
|
|
51
|
+
env: { ...process.env, SUPERCLI_SERVER: SERVER },
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
return { ok: true, data: JSON.parse(stdout.trim()) };
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
output: (e.stdout || "").trim(),
|
|
59
|
+
code: e.code || e.status,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function cliLocal(args) {
|
|
65
|
+
try {
|
|
66
|
+
const env = { ...process.env };
|
|
67
|
+
delete env.SUPERCLI_SERVER;
|
|
68
|
+
const { stdout } = await execAsync(`node ${CLI} ${args}`, {
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
timeout: 15000,
|
|
71
|
+
env,
|
|
72
|
+
});
|
|
73
|
+
return { ok: true, data: JSON.parse(stdout.trim()) };
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
output: (e.stdout || "").trim(),
|
|
78
|
+
code: e.code || e.status,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function run() {
|
|
84
|
+
console.log("\n⚡ MCP Adapter Tests\n");
|
|
85
|
+
|
|
86
|
+
// ── Start mock MCP server ──
|
|
87
|
+
let mockServer;
|
|
88
|
+
await test("start mock MCP server", async () => {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
mockServer = http.createServer((req, res) => {
|
|
91
|
+
if (req.method === "POST" && req.url === "/tool") {
|
|
92
|
+
let body = "";
|
|
93
|
+
req.on("data", (c) => (body += c));
|
|
94
|
+
req.on("end", () => {
|
|
95
|
+
const { tool, input } = JSON.parse(body);
|
|
96
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
97
|
+
res.end(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
tool,
|
|
100
|
+
result: `Processed by ${tool}`,
|
|
101
|
+
input_received: input,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
res.writeHead(404);
|
|
107
|
+
res.end();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
mockServer.listen(MCP_PORT, resolve);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await test("register MCP server", async () => {
|
|
115
|
+
const r = await api("POST", "/api/mcp", {
|
|
116
|
+
name: "test-mcp",
|
|
117
|
+
url: `http://127.0.0.1:${MCP_PORT}`,
|
|
118
|
+
});
|
|
119
|
+
assert(r.name === "test-mcp" || r.error, "should register");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await test("create MCP command", async () => {
|
|
123
|
+
const r = await api("POST", "/api/commands", {
|
|
124
|
+
namespace: "ai",
|
|
125
|
+
resource: "text",
|
|
126
|
+
action: "summarize",
|
|
127
|
+
adapter: "mcp",
|
|
128
|
+
adapterConfig: { server: "test-mcp", tool: "summarize" },
|
|
129
|
+
args: [{ name: "text", type: "string", required: true }],
|
|
130
|
+
description: "Summarize text via MCP",
|
|
131
|
+
});
|
|
132
|
+
assert(r.namespace === "ai", "should create command");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await test("sync CLI config", async () => {
|
|
136
|
+
const r = await cli("sync --json");
|
|
137
|
+
assert(r.ok, "sync should succeed");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await test("inspect MCP command", async () => {
|
|
141
|
+
const r = await cli("inspect ai text summarize --json");
|
|
142
|
+
assert(r.ok, "inspect should succeed");
|
|
143
|
+
assert(r.data.adapter === "mcp", "adapter should be mcp");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await test("execute MCP command", async () => {
|
|
147
|
+
const r = await cli("ai text summarize --text helloworld --json");
|
|
148
|
+
assert(
|
|
149
|
+
r.ok,
|
|
150
|
+
"execute should succeed. output: " +
|
|
151
|
+
(r.output || "none") +
|
|
152
|
+
" code: " +
|
|
153
|
+
r.code,
|
|
154
|
+
);
|
|
155
|
+
assert(r.data.command === "ai.text.summarize", "command should match");
|
|
156
|
+
assert(r.data.data.tool === "summarize", "tool should match");
|
|
157
|
+
assert(r.data.data.result, "should have result");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await test("execute MCP command in local mode", async () => {
|
|
161
|
+
const r = await cliLocal("ai text summarize --text localmode --json");
|
|
162
|
+
assert(
|
|
163
|
+
r.ok,
|
|
164
|
+
"local execute should succeed. output: " +
|
|
165
|
+
(r.output || "none") +
|
|
166
|
+
" code: " +
|
|
167
|
+
r.code,
|
|
168
|
+
);
|
|
169
|
+
assert(r.data.command === "ai.text.summarize", "command should match");
|
|
170
|
+
assert(r.data.data.tool === "summarize", "tool should match");
|
|
171
|
+
assert(r.data.data.result, "should have result");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await test("list MCP servers", async () => {
|
|
175
|
+
const r = await api("GET", "/api/mcp?format=json");
|
|
176
|
+
assert(Array.isArray(r), "should return array");
|
|
177
|
+
assert(
|
|
178
|
+
r.find((s) => s.name === "test-mcp"),
|
|
179
|
+
"should find test-mcp",
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (mockServer) mockServer.close();
|
|
184
|
+
|
|
185
|
+
console.log(`\n Results: ${passed} passed, ${failed} failed\n`);
|
|
186
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
run();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenAPI Adapter Integration Test
|
|
4
|
+
* Registers an OpenAPI spec, creates a command, executes via CLI.
|
|
5
|
+
* Requires: server running on SUPERCLI_SERVER
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const SERVER = process.env.SUPERCLI_SERVER || "http://127.0.0.1:3000";
|
|
12
|
+
const CLI = path.join(__dirname, "..", "cli", "supercli.js");
|
|
13
|
+
|
|
14
|
+
let passed = 0,
|
|
15
|
+
failed = 0;
|
|
16
|
+
function test(name, fn) {
|
|
17
|
+
try {
|
|
18
|
+
fn();
|
|
19
|
+
passed++;
|
|
20
|
+
console.log(` ✅ ${name}`);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
failed++;
|
|
23
|
+
console.error(` ❌ ${name}: ${err.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function assert(cond, msg) {
|
|
27
|
+
if (!cond) throw new Error(msg);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function api(method, urlPath, body) {
|
|
31
|
+
const bodyStr = body ? JSON.stringify(body).replace(/'/g, "'\\''") : "";
|
|
32
|
+
const cmd = body
|
|
33
|
+
? `curl -s -X ${method} '${SERVER}${urlPath}' -H 'Content-Type: application/json' -d '${bodyStr}'`
|
|
34
|
+
: `curl -s -X ${method} '${SERVER}${urlPath}'`;
|
|
35
|
+
return JSON.parse(execSync(cmd, { encoding: "utf-8" }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cli(args) {
|
|
39
|
+
try {
|
|
40
|
+
const out = execSync(`SUPERCLI_SERVER=${SERVER} node ${CLI} ${args}`, {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
timeout: 15000,
|
|
43
|
+
env: { ...process.env, SUPERCLI_SERVER: SERVER },
|
|
44
|
+
});
|
|
45
|
+
return { ok: true, data: JSON.parse(out.trim()) };
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { ok: false, output: (e.stdout || "").trim(), code: e.status };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log("\n⚡ OpenAPI Adapter Tests\n");
|
|
52
|
+
|
|
53
|
+
test("register OpenAPI spec", () => {
|
|
54
|
+
const r = api("POST", "/api/specs", {
|
|
55
|
+
name: "jsonplaceholder",
|
|
56
|
+
url: "https://jsonplaceholder.typicode.com",
|
|
57
|
+
auth: "none",
|
|
58
|
+
});
|
|
59
|
+
assert(r.name === "jsonplaceholder" || r.error, "should register spec");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("create command bound to spec", () => {
|
|
63
|
+
const r = api("POST", "/api/commands", {
|
|
64
|
+
namespace: "oapi",
|
|
65
|
+
resource: "todos",
|
|
66
|
+
action: "list",
|
|
67
|
+
adapter: "http",
|
|
68
|
+
adapterConfig: {
|
|
69
|
+
method: "GET",
|
|
70
|
+
url: "https://jsonplaceholder.typicode.com/todos?_limit=3",
|
|
71
|
+
},
|
|
72
|
+
args: [],
|
|
73
|
+
description: "List todos via OpenAPI-style command",
|
|
74
|
+
});
|
|
75
|
+
assert(r.namespace === "oapi", "should create command");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("CLI config sync", () => {
|
|
79
|
+
const r = cli("sync --json");
|
|
80
|
+
assert(r.ok, "sync should succeed");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("execute openapi-bound command", () => {
|
|
84
|
+
const r = cli("oapi todos list --json");
|
|
85
|
+
assert(r.ok, "execute should succeed");
|
|
86
|
+
assert(r.data.command === "oapi.todos.list", "command should match");
|
|
87
|
+
assert(Array.isArray(r.data.data), "should return array");
|
|
88
|
+
assert(r.data.data.length === 3, "should return 3 items");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("list specs via API", () => {
|
|
92
|
+
const r = api("GET", "/api/specs?format=json");
|
|
93
|
+
assert(Array.isArray(r), "should return array");
|
|
94
|
+
assert(
|
|
95
|
+
r.find((s) => s.name === "jsonplaceholder"),
|
|
96
|
+
"should find registered spec",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
console.log(`\n Results: ${passed} passed, ${failed} failed\n`);
|
|
101
|
+
process.exit(failed > 0 ? 1 : 0);
|