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,110 @@
|
|
|
1
|
+
<%- include("partials/head", { title: command ? "Edit Command" : "New Command" }) %>
|
|
2
|
+
|
|
3
|
+
<div class="max-w-2xl">
|
|
4
|
+
<h1 class="text-2xl font-bold mb-6"><%= command ? "Edit Command" : "New Command" %></h1>
|
|
5
|
+
|
|
6
|
+
<div id="cmd-editor">
|
|
7
|
+
<form @submit.prevent="save" class="flex flex-col gap-4">
|
|
8
|
+
|
|
9
|
+
<div class="grid grid-cols-3 gap-4">
|
|
10
|
+
<div class="form-control">
|
|
11
|
+
<label class="label"><span class="label-text">Namespace</span></label>
|
|
12
|
+
<input v-model="form.namespace" class="input input-bordered input-sm" required placeholder="e.g. referential">
|
|
13
|
+
</div>
|
|
14
|
+
<div class="form-control">
|
|
15
|
+
<label class="label"><span class="label-text">Resource</span></label>
|
|
16
|
+
<input v-model="form.resource" class="input input-bordered input-sm" required placeholder="e.g. users">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-control">
|
|
19
|
+
<label class="label"><span class="label-text">Action</span></label>
|
|
20
|
+
<input v-model="form.action" class="input input-bordered input-sm" required placeholder="e.g. fetch">
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="form-control">
|
|
25
|
+
<label class="label"><span class="label-text">Description</span></label>
|
|
26
|
+
<input v-model="form.description" class="input input-bordered input-sm" placeholder="What does this command do?">
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="form-control">
|
|
30
|
+
<label class="label"><span class="label-text">Adapter</span></label>
|
|
31
|
+
<select v-model="form.adapter" class="select select-bordered select-sm">
|
|
32
|
+
<option value="http">http</option>
|
|
33
|
+
<option value="openapi">openapi</option>
|
|
34
|
+
<option value="mcp">mcp</option>
|
|
35
|
+
<option value="shell">shell</option>
|
|
36
|
+
<option value="custom">custom</option>
|
|
37
|
+
</select>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="form-control">
|
|
41
|
+
<label class="label"><span class="label-text">Adapter Config (JSON)</span></label>
|
|
42
|
+
<textarea v-model="adapterConfigStr" class="textarea textarea-bordered textarea-sm font-mono" rows="4" placeholder='{"method":"GET","url":"https://..."}'></textarea>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Arguments -->
|
|
46
|
+
<div class="form-control">
|
|
47
|
+
<label class="label"><span class="label-text">Arguments</span></label>
|
|
48
|
+
<div v-for="(arg, i) in form.args" :key="i" class="flex gap-2 mb-2 items-center">
|
|
49
|
+
<input v-model="arg.name" class="input input-bordered input-xs flex-1" placeholder="name">
|
|
50
|
+
<select v-model="arg.type" class="select select-bordered select-xs">
|
|
51
|
+
<option value="string">string</option>
|
|
52
|
+
<option value="number">number</option>
|
|
53
|
+
<option value="boolean">boolean</option>
|
|
54
|
+
</select>
|
|
55
|
+
<label class="cursor-pointer flex items-center gap-1">
|
|
56
|
+
<input type="checkbox" v-model="arg.required" class="checkbox checkbox-xs">
|
|
57
|
+
<span class="text-xs">req</span>
|
|
58
|
+
</label>
|
|
59
|
+
<button type="button" @click="form.args.splice(i,1)" class="btn btn-xs btn-ghost text-error">✕</button>
|
|
60
|
+
</div>
|
|
61
|
+
<button type="button" @click="form.args.push({name:'',type:'string',required:false})" class="btn btn-xs btn-outline btn-primary w-fit">+ Add Arg</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="flex gap-2 mt-4">
|
|
65
|
+
<button type="submit" class="btn btn-primary btn-sm">{{ editId ? 'Update' : 'Create' }}</button>
|
|
66
|
+
<a href="/api/commands" class="btn btn-ghost btn-sm">Cancel</a>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
</form>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<script>
|
|
74
|
+
const editData = <%- JSON.stringify(command) %>;
|
|
75
|
+
|
|
76
|
+
Vue.createApp({
|
|
77
|
+
data() {
|
|
78
|
+
return {
|
|
79
|
+
editId: editData ? editData._id : null,
|
|
80
|
+
form: {
|
|
81
|
+
namespace: editData ? editData.namespace : '',
|
|
82
|
+
resource: editData ? editData.resource : '',
|
|
83
|
+
action: editData ? editData.action : '',
|
|
84
|
+
description: editData ? editData.description : '',
|
|
85
|
+
adapter: editData ? editData.adapter : 'http',
|
|
86
|
+
args: editData && editData.args ? editData.args : []
|
|
87
|
+
},
|
|
88
|
+
adapterConfigStr: editData && editData.adapterConfig ? JSON.stringify(editData.adapterConfig, null, 2) : '{}'
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
methods: {
|
|
92
|
+
async save() {
|
|
93
|
+
const body = {
|
|
94
|
+
...this.form,
|
|
95
|
+
adapterConfig: JSON.parse(this.adapterConfigStr || '{}')
|
|
96
|
+
}
|
|
97
|
+
const url = this.editId ? '/api/commands/' + this.editId : '/api/commands'
|
|
98
|
+
const method = this.editId ? 'PUT' : 'POST'
|
|
99
|
+
await fetch(url, {
|
|
100
|
+
method,
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify(body)
|
|
103
|
+
})
|
|
104
|
+
window.location.href = '/api/commands'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}).mount('#cmd-editor')
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<%- include("partials/foot") %>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<%- include("partials/head", { title: "Commands" }) %>
|
|
2
|
+
|
|
3
|
+
<div class="flex justify-between items-center mb-6">
|
|
4
|
+
<h1 class="text-2xl font-bold">Commands</h1>
|
|
5
|
+
<a href="/api/commands/new" class="btn btn-primary btn-sm">+ Create Command</a>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="overflow-x-auto">
|
|
9
|
+
<table class="table table-zebra w-full">
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th>Namespace</th>
|
|
13
|
+
<th>Resource</th>
|
|
14
|
+
<th>Action</th>
|
|
15
|
+
<th>Adapter</th>
|
|
16
|
+
<th>Description</th>
|
|
17
|
+
<th>Actions</th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% if (commands.length === 0) { %>
|
|
22
|
+
<tr><td colspan="6" class="text-center opacity-60">No commands yet. Create one to get started.</td></tr>
|
|
23
|
+
<% } %>
|
|
24
|
+
<% commands.forEach(c => { %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td><span class="badge badge-outline"><%= c.namespace %></span></td>
|
|
27
|
+
<td><%= c.resource %></td>
|
|
28
|
+
<td><code><%= c.action %></code></td>
|
|
29
|
+
<td><span class="badge badge-ghost badge-sm"><%= c.adapter %></span></td>
|
|
30
|
+
<td class="max-w-xs truncate opacity-70"><%= c.description || '-' %></td>
|
|
31
|
+
<td class="flex gap-1">
|
|
32
|
+
<a href="/api/commands/<%= c._id %>/edit" class="btn btn-xs btn-outline">Edit</a>
|
|
33
|
+
<button onclick="deleteCommand('<%= c._id %>')" class="btn btn-xs btn-error btn-outline">Delete</button>
|
|
34
|
+
</td>
|
|
35
|
+
</tr>
|
|
36
|
+
<% }) %>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
async function deleteCommand(id) {
|
|
43
|
+
if (!confirm('Delete this command?')) return
|
|
44
|
+
await fetch('/api/commands/' + id, { method: 'DELETE' })
|
|
45
|
+
location.reload()
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<%- include("partials/foot") %>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<%- include("partials/head", { title: "Jobs" }) %>
|
|
2
|
+
|
|
3
|
+
<div id="jobs-app">
|
|
4
|
+
<div class="flex justify-between items-center mb-6">
|
|
5
|
+
<h1 class="text-2xl font-bold">Execution History</h1>
|
|
6
|
+
<button @click="loadStats" class="btn btn-ghost btn-sm">📊 Stats</button>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- Stats panel -->
|
|
10
|
+
<div v-if="stats" class="grid grid-cols-4 gap-4 mb-6">
|
|
11
|
+
<div class="stat bg-base-300 rounded-lg p-4">
|
|
12
|
+
<div class="stat-title">Total</div>
|
|
13
|
+
<div class="stat-value text-primary">{{ stats.total }}</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="stat bg-base-300 rounded-lg p-4">
|
|
16
|
+
<div class="stat-title">Success</div>
|
|
17
|
+
<div class="stat-value text-success">{{ stats.success }}</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="stat bg-base-300 rounded-lg p-4">
|
|
20
|
+
<div class="stat-title">Failed</div>
|
|
21
|
+
<div class="stat-value text-error">{{ stats.failed }}</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="stat bg-base-300 rounded-lg p-4">
|
|
24
|
+
<div class="stat-title">Failure Rate</div>
|
|
25
|
+
<div class="stat-value">{{ stats.failure_rate }}</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="overflow-x-auto">
|
|
30
|
+
<table class="table table-zebra w-full table-sm">
|
|
31
|
+
<thead>
|
|
32
|
+
<tr>
|
|
33
|
+
<th>Command</th>
|
|
34
|
+
<th>Status</th>
|
|
35
|
+
<th>Duration</th>
|
|
36
|
+
<th>Timestamp</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
<tr v-if="jobs.length===0"><td colspan="4" class="text-center opacity-60">No executions recorded yet.</td></tr>
|
|
41
|
+
<tr v-for="j in jobs" :key="j._id">
|
|
42
|
+
<td><code>{{ j.command }}</code></td>
|
|
43
|
+
<td>
|
|
44
|
+
<span class="badge badge-sm" :class="j.status==='success' ? 'badge-success' : 'badge-error'">
|
|
45
|
+
{{ j.status }}
|
|
46
|
+
</span>
|
|
47
|
+
</td>
|
|
48
|
+
<td>{{ j.duration_ms }}ms</td>
|
|
49
|
+
<td class="opacity-70">{{ new Date(j.timestamp).toLocaleString() }}</td>
|
|
50
|
+
</tr>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
const initialJobs = <%- JSON.stringify(jobs) %>;
|
|
58
|
+
|
|
59
|
+
Vue.createApp({
|
|
60
|
+
data() {
|
|
61
|
+
return { jobs: initialJobs, stats: null }
|
|
62
|
+
},
|
|
63
|
+
methods: {
|
|
64
|
+
async loadStats() {
|
|
65
|
+
const r = await fetch('/api/jobs/stats')
|
|
66
|
+
this.stats = await r.json()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}).mount('#jobs-app')
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<%- include("partials/foot") %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>SUPERCLI — Command Control Plane</title>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.2/dist/full.min.css" rel="stylesheet">
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
body { min-height: 100vh; }
|
|
12
|
+
.nav-active { background: oklch(var(--p)/0.15); color: oklch(var(--p)); }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body class="bg-base-200">
|
|
16
|
+
|
|
17
|
+
<div class="flex min-h-screen">
|
|
18
|
+
<!-- Sidebar -->
|
|
19
|
+
<aside class="w-64 bg-base-300 p-4 flex flex-col gap-2 border-r border-base-content/10">
|
|
20
|
+
<a href="/api/commands" class="text-xl font-bold px-3 py-2 mb-4 flex items-center gap-2">
|
|
21
|
+
<span class="text-primary">⚡</span> SUPERCLI
|
|
22
|
+
</a>
|
|
23
|
+
<a href="/api/commands" class="btn btn-ghost justify-start <%= typeof commands !== 'undefined' ? 'nav-active' : '' %>">
|
|
24
|
+
📋 Commands
|
|
25
|
+
</a>
|
|
26
|
+
<a href="/api/specs" class="btn btn-ghost justify-start <%= typeof specs !== 'undefined' ? 'nav-active' : '' %>">
|
|
27
|
+
📄 OpenAPI Specs
|
|
28
|
+
</a>
|
|
29
|
+
<a href="/api/mcp" class="btn btn-ghost justify-start <%= typeof servers !== 'undefined' ? 'nav-active' : '' %>">
|
|
30
|
+
🔌 MCP Servers
|
|
31
|
+
</a>
|
|
32
|
+
</aside>
|
|
33
|
+
|
|
34
|
+
<!-- Main content -->
|
|
35
|
+
<main class="flex-1 p-8">
|
|
36
|
+
<%- body %>
|
|
37
|
+
</main>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<script src="/static/app.js"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<%- include("partials/head", { title: "MCP Servers" }) %>
|
|
2
|
+
|
|
3
|
+
<div id="mcp-app">
|
|
4
|
+
<div class="flex justify-between items-center mb-6">
|
|
5
|
+
<h1 class="text-2xl font-bold">MCP Servers</h1>
|
|
6
|
+
<button @click="showForm=!showForm" class="btn btn-primary btn-sm">+ Add Server</button>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- Add form -->
|
|
10
|
+
<div v-if="showForm" class="card bg-base-300 p-4 mb-6">
|
|
11
|
+
<form @submit.prevent="create" class="flex gap-4 items-end">
|
|
12
|
+
<div class="form-control flex-1">
|
|
13
|
+
<label class="label"><span class="label-text">Name</span></label>
|
|
14
|
+
<input v-model="form.name" class="input input-bordered input-sm" required placeholder="e.g. ai-tools">
|
|
15
|
+
</div>
|
|
16
|
+
<div class="form-control flex-1">
|
|
17
|
+
<label class="label"><span class="label-text">URL</span></label>
|
|
18
|
+
<input v-model="form.url" class="input input-bordered input-sm" required placeholder="https://mcp.example.com">
|
|
19
|
+
</div>
|
|
20
|
+
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
|
21
|
+
<button type="button" @click="showForm=false" class="btn btn-ghost btn-sm">Cancel</button>
|
|
22
|
+
</form>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="overflow-x-auto">
|
|
26
|
+
<table class="table table-zebra w-full">
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Name</th>
|
|
30
|
+
<th>URL</th>
|
|
31
|
+
<th>Actions</th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
<tr v-if="servers.length===0"><td colspan="3" class="text-center opacity-60">No MCP servers registered.</td></tr>
|
|
36
|
+
<tr v-for="s in servers" :key="s._id">
|
|
37
|
+
<td><code>{{ s.name }}</code></td>
|
|
38
|
+
<td class="max-w-xs truncate opacity-70">{{ s.url }}</td>
|
|
39
|
+
<td>
|
|
40
|
+
<button @click="remove(s._id)" class="btn btn-xs btn-error btn-outline">Delete</button>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
const initialServers = <%- JSON.stringify(servers) %>;
|
|
50
|
+
|
|
51
|
+
Vue.createApp({
|
|
52
|
+
data() {
|
|
53
|
+
return {
|
|
54
|
+
servers: initialServers,
|
|
55
|
+
showForm: false,
|
|
56
|
+
form: { name: '', url: '' }
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
methods: {
|
|
60
|
+
async create() {
|
|
61
|
+
const r = await fetch('/api/mcp', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify(this.form)
|
|
65
|
+
})
|
|
66
|
+
const doc = await r.json()
|
|
67
|
+
this.servers.push(doc)
|
|
68
|
+
this.form = { name: '', url: '' }
|
|
69
|
+
this.showForm = false
|
|
70
|
+
},
|
|
71
|
+
async remove(id) {
|
|
72
|
+
if (!confirm('Delete this server?')) return
|
|
73
|
+
await fetch('/api/mcp/' + id, { method: 'DELETE' })
|
|
74
|
+
this.servers = this.servers.filter(s => s._id !== id)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}).mount('#mcp-app')
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<%- include("partials/foot") %>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>SUPERCLI — <%= typeof title !== 'undefined' ? title : 'Control Plane' %></title>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.2/dist/full.min.css" rel="stylesheet">
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
.nav-active { background: oklch(var(--p)/0.15); color: oklch(var(--p)); }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body class="bg-base-200 min-h-screen">
|
|
15
|
+
<div class="flex min-h-screen">
|
|
16
|
+
<!-- Sidebar -->
|
|
17
|
+
<aside class="w-64 bg-base-300 p-4 flex flex-col gap-2 border-r border-base-content/10">
|
|
18
|
+
<a href="/api/commands" class="text-xl font-bold px-3 py-2 mb-4 flex items-center gap-2">
|
|
19
|
+
<span class="text-primary">⚡</span> SUPERCLI
|
|
20
|
+
</a>
|
|
21
|
+
<a href="/api/commands" class="btn btn-ghost justify-start" id="nav-commands">📋 Commands</a>
|
|
22
|
+
<a href="/api/specs" class="btn btn-ghost justify-start" id="nav-specs">📄 OpenAPI Specs</a>
|
|
23
|
+
<a href="/api/mcp" class="btn btn-ghost justify-start" id="nav-mcp">🔌 MCP Servers</a>
|
|
24
|
+
<a href="/api/jobs" class="btn btn-ghost justify-start" id="nav-jobs">📊 Jobs</a>
|
|
25
|
+
</aside>
|
|
26
|
+
<!-- Main content -->
|
|
27
|
+
<main class="flex-1 p-8">
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<%- include("partials/head", { title: "OpenAPI Specs" }) %>
|
|
2
|
+
|
|
3
|
+
<div id="specs-app">
|
|
4
|
+
<div class="flex justify-between items-center mb-6">
|
|
5
|
+
<h1 class="text-2xl font-bold">OpenAPI Specs</h1>
|
|
6
|
+
<button @click="showForm=!showForm" class="btn btn-primary btn-sm">+ Add Spec</button>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- Add form -->
|
|
10
|
+
<div v-if="showForm" class="card bg-base-300 p-4 mb-6">
|
|
11
|
+
<form @submit.prevent="create" class="flex gap-4 items-end">
|
|
12
|
+
<div class="form-control flex-1">
|
|
13
|
+
<label class="label"><span class="label-text">Name</span></label>
|
|
14
|
+
<input v-model="form.name" class="input input-bordered input-sm" required placeholder="e.g. referential-api">
|
|
15
|
+
</div>
|
|
16
|
+
<div class="form-control flex-1">
|
|
17
|
+
<label class="label"><span class="label-text">URL</span></label>
|
|
18
|
+
<input v-model="form.url" class="input input-bordered input-sm" required placeholder="https://api.example.com/openapi.json">
|
|
19
|
+
</div>
|
|
20
|
+
<div class="form-control">
|
|
21
|
+
<label class="label"><span class="label-text">Auth</span></label>
|
|
22
|
+
<select v-model="form.auth" class="select select-bordered select-sm">
|
|
23
|
+
<option value="none">none</option>
|
|
24
|
+
<option value="oauth">oauth</option>
|
|
25
|
+
<option value="apiKey">apiKey</option>
|
|
26
|
+
<option value="jwt">jwt</option>
|
|
27
|
+
</select>
|
|
28
|
+
</div>
|
|
29
|
+
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
|
30
|
+
<button type="button" @click="showForm=false" class="btn btn-ghost btn-sm">Cancel</button>
|
|
31
|
+
</form>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="overflow-x-auto">
|
|
35
|
+
<table class="table table-zebra w-full">
|
|
36
|
+
<thead>
|
|
37
|
+
<tr>
|
|
38
|
+
<th>Name</th>
|
|
39
|
+
<th>URL</th>
|
|
40
|
+
<th>Auth</th>
|
|
41
|
+
<th>Actions</th>
|
|
42
|
+
</tr>
|
|
43
|
+
</thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
<tr v-if="specs.length===0"><td colspan="4" class="text-center opacity-60">No specs registered.</td></tr>
|
|
46
|
+
<tr v-for="s in specs" :key="s._id">
|
|
47
|
+
<td><code>{{ s.name }}</code></td>
|
|
48
|
+
<td class="max-w-xs truncate opacity-70">{{ s.url }}</td>
|
|
49
|
+
<td><span class="badge badge-ghost badge-sm">{{ s.auth }}</span></td>
|
|
50
|
+
<td>
|
|
51
|
+
<button @click="remove(s._id)" class="btn btn-xs btn-error btn-outline">Delete</button>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<script>
|
|
60
|
+
const initialSpecs = <%- JSON.stringify(specs) %>;
|
|
61
|
+
|
|
62
|
+
Vue.createApp({
|
|
63
|
+
data() {
|
|
64
|
+
return {
|
|
65
|
+
specs: initialSpecs,
|
|
66
|
+
showForm: false,
|
|
67
|
+
form: { name: '', url: '', auth: 'none' }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
methods: {
|
|
71
|
+
async create() {
|
|
72
|
+
const r = await fetch('/api/specs', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify(this.form)
|
|
76
|
+
})
|
|
77
|
+
const doc = await r.json()
|
|
78
|
+
this.specs.push(doc)
|
|
79
|
+
this.form = { name: '', url: '', auth: 'none' }
|
|
80
|
+
this.showForm = false
|
|
81
|
+
},
|
|
82
|
+
async remove(id) {
|
|
83
|
+
if (!confirm('Delete this spec?')) return
|
|
84
|
+
await fetch('/api/specs/' + id, { method: 'DELETE' })
|
|
85
|
+
this.specs = this.specs.filter(s => s._id !== id)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}).mount('#specs-app')
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<%- include("partials/foot") %>
|