superacli 1.1.20 → 1.1.21

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.
Files changed (125) hide show
  1. package/cli/adapter-schema.js +1 -6
  2. package/cli/config.js +95 -0
  3. package/cli/executor.js +90 -11
  4. package/cli/server-command.js +395 -0
  5. package/cli/supercli.js +82 -1
  6. package/docs/adapters.md +159 -0
  7. package/docs/changelog-2026-04.html +15 -0
  8. package/docs/index.html +314 -98
  9. package/docs/meta-plugins.json +24424 -1247
  10. package/docs/plugins-examples.md +8 -8
  11. package/docs/plugins-how-to.md +0 -13
  12. package/docs/plugins.html +165 -61
  13. package/docs/server.md +50 -0
  14. package/graphify-out/GRAPH_REPORT.md +141 -136
  15. package/graphify-out/cache/1821936911d6eb3130499b6b45b8e6d760d6c05328131e115ac9825a5e3061cd.json +1 -0
  16. package/graphify-out/cache/20b0e1261b41286d639e7b039c8143682001981f607e26eb2a492a06f12c233a.json +1 -0
  17. package/graphify-out/cache/21dc011c08a0813ac33c0520c9cf131d8922e6007df7d9b51df740974fee5fb3.json +1 -0
  18. package/graphify-out/cache/22d3d43c24fdff310ab8445bd442fbcf4714872c6950f15b90777e5a8358dcd9.json +1 -0
  19. package/graphify-out/cache/2b8a3fbc23afe7bbc1a5ecb60d4074f39afdf6cb42120922176c280f4cc4ab4b.json +1 -0
  20. package/graphify-out/cache/37418b8b152d9c436d75361969c39bc74a76260ab7209ceabf455f80452f1654.json +1 -0
  21. package/graphify-out/cache/3b9aae57e4326f5862f8153c13b7d809a34c422996993e7b491c5ea3a66b5e04.json +1 -0
  22. package/graphify-out/cache/448f8e0c5e77cfb15fa625ed40ee0dc1622d9814d24905fe4c12c4d175131dc9.json +1 -0
  23. package/graphify-out/cache/47dd42009ceac28b7a7fec5586e565e7bc850c80be96b8329e1ed7c49efbe34f.json +1 -0
  24. package/graphify-out/cache/492712949d38efbe312c303fe717e5c93e1ecc8888e1ceb855750afc48caffdb.json +1 -0
  25. package/graphify-out/cache/494fd6af80b6a1bb7a2a5641c6ad11e2fbe2a67b5a2b3ff57fd04bb49ccad1c4.json +1 -0
  26. package/graphify-out/cache/4b31b0de656deff083119780df64d17205c45e72233987e1c495f2121c4923fd.json +1 -0
  27. package/graphify-out/cache/5a16fa8cdd687a39661321adb39186ce8cf924a79ef936e42b365380b5e3543e.json +1 -0
  28. package/graphify-out/cache/644fcb129e595cfdb4e3a9757ed9658230347f5ed0652be45a894528a2b6585b.json +1 -0
  29. package/graphify-out/cache/6b8762db2c5f51408cf49d915f3e44bcc25c7ebb32abc635307fdf2a672b370c.json +1 -0
  30. package/graphify-out/cache/6db88050060880e56dd26ec58f29e3b56a409ace48e31bc75f54cab7dc74e409.json +1 -0
  31. package/graphify-out/cache/7932d1b6a9751b2cb2f4dc6b4e5f9fd7dcc2cee82d7866ec3d655482010533c0.json +1 -0
  32. package/graphify-out/cache/8140a76ff5b70dfdd0d020aeffff27f87af39da5b237c35baa938fcc4b288ba5.json +1 -0
  33. package/graphify-out/cache/8876638878758733a412bd4ca4cbd22863322689fbba9f037d92abfa877ea3ab.json +1 -0
  34. package/graphify-out/cache/90445359c448f8135207ddcd378c8391b9274460064cf998f1ea2ad6f2173703.json +1 -0
  35. package/graphify-out/cache/b01f675bb3942d2e8f359d11c38d52f28487544f54cdef518c60b7d8dfd2b383.json +1 -0
  36. package/graphify-out/cache/bc353b40b348d54c730d8498af675f57998b14ae796b4f952cdc200147008f13.json +1 -0
  37. package/graphify-out/cache/c38b2e784b1c441121b422db0c79b4911fb525d2a3cfad10d3fd561a07900bd5.json +1 -0
  38. package/graphify-out/cache/ce9df9bb2d36ecf310a53fb34526168bdf25773edf0826c1f55accebfa3c012d.json +1 -0
  39. package/graphify-out/cache/ceaeb0f99851d313b29cb1440240db1cf835de425b9b0c1e58fa3035355c2b2b.json +1 -0
  40. package/graphify-out/cache/d8f3e78931a41cd0f367fdab1d11b634bbffb2c90280c75a80a1a53a6b2d6bc7.json +1 -0
  41. package/graphify-out/cache/f574e4ce3bcb1250ca88410519fb4fd26c5b6cd786039410880fbd6e606ab23f.json +1 -0
  42. package/graphify-out/cache/faa65636d5f779b8b1c790dde11cc6470b48125bf42a12a77651827fd1653fd7.json +1 -0
  43. package/graphify-out/graph.json +4173 -2115
  44. package/ideal-plugins.csv +124 -0
  45. package/package.json +3 -2
  46. package/plugin-scores.csv +999 -0
  47. package/plugins/beads/plugin.json +2 -2
  48. package/plugins/biome/install-guidance.json +11 -0
  49. package/plugins/biome/meta.json +13 -0
  50. package/plugins/biome/plugin.json +83 -0
  51. package/plugins/biome/skills/quickstart/SKILL.md +25 -0
  52. package/plugins/bore/install-guidance.json +11 -0
  53. package/plugins/bore/meta.json +11 -0
  54. package/plugins/bore/plugin.json +65 -0
  55. package/plugins/bore/skills/quickstart/SKILL.md +25 -0
  56. package/plugins/ccf-deadlines/install-guidance.json +11 -0
  57. package/plugins/ccf-deadlines/meta.json +11 -0
  58. package/plugins/ccf-deadlines/plugin.json +28 -0
  59. package/plugins/ccf-deadlines/skills/quickstart/SKILL.md +25 -0
  60. package/plugins/code2prompt/install-guidance.json +11 -0
  61. package/plugins/code2prompt/meta.json +12 -0
  62. package/plugins/code2prompt/plugin.json +46 -0
  63. package/plugins/code2prompt/skills/quickstart/SKILL.md +25 -0
  64. package/plugins/commiat/install-guidance.json +10 -0
  65. package/plugins/commiat/plugin.json +2 -2
  66. package/plugins/dua-cli/install-guidance.json +11 -0
  67. package/plugins/dua-cli/meta.json +11 -0
  68. package/plugins/dua-cli/plugin.json +57 -0
  69. package/plugins/dua-cli/skills/quickstart/SKILL.md +25 -0
  70. package/plugins/forever/install-guidance.json +11 -0
  71. package/plugins/forever/meta.json +11 -0
  72. package/plugins/forever/plugin.json +75 -0
  73. package/plugins/forever/skills/quickstart/SKILL.md +25 -0
  74. package/plugins/gitmoji-cli/install-guidance.json +11 -0
  75. package/plugins/gitmoji-cli/meta.json +11 -0
  76. package/plugins/gitmoji-cli/plugin.json +49 -0
  77. package/plugins/gitmoji-cli/skills/quickstart/SKILL.md +25 -0
  78. package/plugins/google-maps-scraper/plugin.json +1 -1
  79. package/plugins/gwc/install-guidance.json +10 -0
  80. package/plugins/gwc/plugin.json +2 -2
  81. package/plugins/quarto/install-guidance.json +11 -0
  82. package/plugins/quarto/meta.json +11 -0
  83. package/plugins/quarto/plugin.json +61 -0
  84. package/plugins/quarto/skills/quickstart/SKILL.md +25 -0
  85. package/plugins/readme-md-generator/install-guidance.json +11 -0
  86. package/plugins/readme-md-generator/meta.json +11 -0
  87. package/plugins/readme-md-generator/plugin.json +28 -0
  88. package/plugins/readme-md-generator/skills/quickstart/SKILL.md +25 -0
  89. package/plugins/twarc/install-guidance.json +11 -0
  90. package/plugins/twarc/meta.json +13 -0
  91. package/plugins/twarc/plugin.json +165 -0
  92. package/plugins/twarc/skills/quickstart/SKILL.md +61 -0
  93. package/plugins/vale/install-guidance.json +11 -0
  94. package/plugins/vale/meta.json +12 -0
  95. package/plugins/vale/plugin.json +50 -0
  96. package/plugins/vale/skills/quickstart/SKILL.md +25 -0
  97. package/plugins/volta/install-guidance.json +11 -0
  98. package/plugins/volta/meta.json +12 -0
  99. package/plugins/volta/plugin.json +75 -0
  100. package/plugins/volta/skills/quickstart/SKILL.md +25 -0
  101. package/plugins/wgcf/install-guidance.json +11 -0
  102. package/plugins/wgcf/meta.json +12 -0
  103. package/plugins/wgcf/plugin.json +45 -0
  104. package/plugins/wgcf/skills/quickstart/SKILL.md +25 -0
  105. package/scripts/analyze-plugins.js +130 -0
  106. package/scripts/enrich-meta-plugins.js +67 -0
  107. package/server/app.js +23 -1
  108. package/server/routes/adapters.js +356 -0
  109. package/server/routes/dashboard.js +113 -0
  110. package/server/routes/jobs.js +26 -0
  111. package/server/services/adaptersService.js +284 -0
  112. package/server/services/pluginsService.js +4 -0
  113. package/server/views/adapter-edit.ejs +226 -0
  114. package/server/views/adapter-packages.ejs +191 -0
  115. package/server/views/adapters.ejs +112 -0
  116. package/server/views/command-edit.ejs +48 -21
  117. package/server/views/commands.ejs +25 -22
  118. package/server/views/dashboard.ejs +196 -0
  119. package/server/views/layout.ejs +94 -14
  120. package/server/views/mcp.ejs +38 -35
  121. package/server/views/partials/head.ejs +88 -12
  122. package/server/views/plugins.ejs +9 -0
  123. package/server/views/specs.ejs +33 -30
  124. package/cli/adapters/builtin.js +0 -43
  125. package/index.html +0 -384
@@ -0,0 +1,284 @@
1
+ const { getStorage } = require("../storage/adapter")
2
+ const { bumpVersion } = require("./configService")
3
+
4
+ const ADAPTER_PREFIX = "adapter:"
5
+ const ADAPTER_PACKAGES_PREFIX = "adapter_packages:"
6
+
7
+ function invalid(message) {
8
+ return Object.assign(new Error(message), {
9
+ code: 85,
10
+ type: "invalid_argument",
11
+ recoverable: false,
12
+ })
13
+ }
14
+
15
+ function adapterKey(name) {
16
+ return `${ADAPTER_PREFIX}${name}`
17
+ }
18
+
19
+ function adapterPackagesKey(name) {
20
+ return `${ADAPTER_PACKAGES_PREFIX}${name}`
21
+ }
22
+
23
+ function sanitizeAdapter(adapter) {
24
+ if (!adapter || typeof adapter !== "object") {
25
+ throw invalid("adapter must be an object")
26
+ }
27
+
28
+ const name = String(adapter.name || "").trim()
29
+ if (!name) throw invalid("adapter.name is required")
30
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
31
+ throw invalid("adapter.name must start with letter and contain only letters, numbers, underscores, hyphens")
32
+ }
33
+
34
+ const description = String(adapter.description || "")
35
+ const source = String(adapter.source || "").trim()
36
+ if (!source) throw invalid("adapter.source is required")
37
+
38
+ const executionContext = ["server", "cli"].includes(adapter.execution_context)
39
+ ? adapter.execution_context
40
+ : "server"
41
+
42
+ const dependencies = Array.isArray(adapter.dependencies)
43
+ ? adapter.dependencies.filter(d => typeof d === "string")
44
+ : []
45
+
46
+ const timeoutMs = Number(adapter.timeout_ms) || 30000
47
+ const memoryLimitMb = Number(adapter.memory_limit_mb) || 128
48
+ const allowNetwork = adapter.allow_network === true
49
+
50
+ return {
51
+ name,
52
+ description,
53
+ execution_context: executionContext,
54
+ source,
55
+ dependencies,
56
+ timeout_ms: timeoutMs,
57
+ memory_limit_mb: memoryLimitMb,
58
+ allow_network: allowNetwork,
59
+ }
60
+ }
61
+
62
+ async function listAdapters() {
63
+ const storage = getStorage()
64
+ const keys = await storage.listKeys(ADAPTER_PREFIX)
65
+ const records = await Promise.all(keys.map(k => storage.get(k)))
66
+ return records
67
+ .filter(Boolean)
68
+ .sort((a, b) => String(a.name || "").localeCompare(String(b.name || "")))
69
+ .map(toPublicAdapter)
70
+ }
71
+
72
+ async function getAdapter(name) {
73
+ const storage = getStorage()
74
+ const record = await storage.get(adapterKey(name))
75
+ if (!record) return null
76
+ return toPublicAdapter(record)
77
+ }
78
+
79
+ async function getAdapterSource(name) {
80
+ const storage = getStorage()
81
+ const record = await storage.get(adapterKey(name))
82
+ if (!record) return null
83
+ return record.source || null
84
+ }
85
+
86
+ function toPublicAdapter(record) {
87
+ return {
88
+ name: record.name,
89
+ description: record.description || "",
90
+ execution_context: record.execution_context || "server",
91
+ dependencies: record.dependencies || [],
92
+ timeout_ms: record.timeout_ms || 30000,
93
+ memory_limit_mb: record.memory_limit_mb || 128,
94
+ allow_network: record.allow_network === true,
95
+ created_at: record.created_at,
96
+ updated_at: record.updated_at,
97
+ }
98
+ }
99
+
100
+ async function createAdapter(payload) {
101
+ const sanitized = sanitizeAdapter(payload)
102
+ const now = new Date().toISOString()
103
+
104
+ const record = {
105
+ ...sanitized,
106
+ created_at: now,
107
+ updated_at: now,
108
+ }
109
+
110
+ const storage = getStorage()
111
+ await storage.set(adapterKey(sanitized.name), record)
112
+ await bumpVersion()
113
+ return toPublicAdapter(record)
114
+ }
115
+
116
+ async function updateAdapter(name, payload) {
117
+ const storage = getStorage()
118
+ const key = adapterKey(name)
119
+ const current = await storage.get(key)
120
+
121
+ if (!current) {
122
+ throw Object.assign(new Error(`Adapter '${name}' not found`), {
123
+ code: 92,
124
+ type: "resource_not_found",
125
+ recoverable: false,
126
+ })
127
+ }
128
+
129
+ const sanitized = sanitizeAdapter({ ...payload, name })
130
+ const now = new Date().toISOString()
131
+
132
+ const record = {
133
+ ...sanitized,
134
+ created_at: current.created_at || now,
135
+ updated_at: now,
136
+ }
137
+
138
+ await storage.set(key, record)
139
+ await bumpVersion()
140
+ return toPublicAdapter(record)
141
+ }
142
+
143
+ async function deleteAdapter(name) {
144
+ const storage = getStorage()
145
+ const key = adapterKey(name)
146
+ const current = await storage.get(key)
147
+ if (!current) return false
148
+
149
+ await storage.delete(key)
150
+ await storage.delete(adapterPackagesKey(name))
151
+ await bumpVersion()
152
+ return true
153
+ }
154
+
155
+ async function getAdapterPackages(name) {
156
+ const storage = getStorage()
157
+ const packages = await storage.get(adapterPackagesKey(name))
158
+ return packages || []
159
+ }
160
+
161
+ async function addAdapterPackage(name, packageName, version = "latest") {
162
+ const { execSync } = require("child_process")
163
+ const path = require("path")
164
+ const fs = require("fs")
165
+
166
+ const storage = getStorage()
167
+ const key = adapterPackagesKey(name)
168
+ const current = await storage.get(key) || []
169
+
170
+ const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`
171
+
172
+ if (!current.includes(packageSpec)) {
173
+ // Create adapter directory if it doesn't exist
174
+ const adapterDir = path.join(process.cwd(), "supercli_storage", "adapters", name)
175
+ fs.mkdirSync(adapterDir, { recursive: true })
176
+
177
+ // Initialize package.json if it doesn't exist
178
+ const packageJsonPath = path.join(adapterDir, "package.json")
179
+ if (!fs.existsSync(packageJsonPath)) {
180
+ fs.writeFileSync(packageJsonPath, JSON.stringify({ name, version: "1.0.0" }, null, 2))
181
+ }
182
+
183
+ current.push(packageSpec)
184
+ await storage.set(key, current)
185
+
186
+ try {
187
+ // Install only in adapter-specific directory
188
+ execSync(`npm install ${packageSpec}`, {
189
+ cwd: adapterDir,
190
+ stdio: "inherit",
191
+ })
192
+ } catch (err) {
193
+ // Rollback if install fails
194
+ const filtered = current.filter(p => p !== packageSpec)
195
+ await storage.set(key, filtered)
196
+ throw Object.assign(new Error(`Failed to install package ${packageSpec}`), {
197
+ code: 105,
198
+ type: "integration_error",
199
+ recoverable: false
200
+ })
201
+ }
202
+ }
203
+
204
+ return current
205
+ }
206
+
207
+ async function removeAdapterPackage(name, packageName) {
208
+ const { execSync } = require("child_process")
209
+ const path = require("path")
210
+ const fs = require("fs")
211
+
212
+ const storage = getStorage()
213
+ const key = adapterPackagesKey(name)
214
+ const current = await storage.get(key) || []
215
+
216
+ const filtered = current.filter(p => !p.startsWith(`${packageName}@`) && p !== packageName)
217
+
218
+ if (filtered.length !== current.length) {
219
+ await storage.set(key, filtered)
220
+
221
+ // Uninstall from adapter-specific directory only
222
+ const adapterDir = path.join(process.cwd(), "supercli_storage", "adapters", name)
223
+ if (fs.existsSync(adapterDir)) {
224
+ try {
225
+ execSync(`npm uninstall ${packageName}`, {
226
+ cwd: adapterDir,
227
+ stdio: "inherit",
228
+ })
229
+ } catch (err) {
230
+ // Don't fail if uninstall fails, just log
231
+ console.error(`Failed to uninstall ${packageName} from adapter directory:`, err.message)
232
+ }
233
+ }
234
+ }
235
+
236
+ return filtered
237
+ }
238
+
239
+ async function setAdapterPackages(name, packages) {
240
+ const { execSync } = require("child_process")
241
+ const path = require("path")
242
+ const fs = require("fs")
243
+
244
+ const storage = getStorage()
245
+ const key = adapterPackagesKey(name)
246
+
247
+ // Get current packages to determine which ones to uninstall
248
+ const current = await storage.get(key) || []
249
+ const newPackages = Array.isArray(packages) ? packages : []
250
+
251
+ // Find packages that are being removed
252
+ const packagesToRemove = current.filter(p => !newPackages.includes(p))
253
+
254
+ // Uninstall removed packages from adapter directory
255
+ const adapterDir = path.join(process.cwd(), "supercli_storage", "adapters", name)
256
+ if (packagesToRemove.length > 0 && fs.existsSync(adapterDir)) {
257
+ for (const pkg of packagesToRemove) {
258
+ const packageName = pkg.split('@')[0]
259
+ try {
260
+ execSync(`npm uninstall ${packageName}`, {
261
+ cwd: adapterDir,
262
+ stdio: "inherit",
263
+ })
264
+ } catch (err) {
265
+ console.error(`Failed to uninstall ${packageName} during prune:`, err.message)
266
+ }
267
+ }
268
+ }
269
+
270
+ await storage.set(key, newPackages)
271
+ }
272
+
273
+ module.exports = {
274
+ listAdapters,
275
+ getAdapter,
276
+ getAdapterSource,
277
+ createAdapter,
278
+ updateAdapter,
279
+ deleteAdapter,
280
+ getAdapterPackages,
281
+ addAdapterPackage,
282
+ removeAdapterPackage,
283
+ setAdapterPackages,
284
+ }
@@ -17,6 +17,7 @@ function defaultSettings() {
17
17
  return {
18
18
  max_zip_mb: 10,
19
19
  default_hooks_policy: "deny",
20
+ admin_mode_enabled: false,
20
21
  }
21
22
  }
22
23
 
@@ -115,6 +116,9 @@ async function updateSettings(patch = {}) {
115
116
  if (patch.default_hooks_policy !== undefined) {
116
117
  next.default_hooks_policy = sanitizeHooksPolicy(patch.default_hooks_policy, next.default_hooks_policy)
117
118
  }
119
+ if (patch.admin_mode_enabled !== undefined) {
120
+ next.admin_mode_enabled = patch.admin_mode_enabled === true || patch.admin_mode_enabled === "true"
121
+ }
118
122
  const storage = getStorage()
119
123
  await storage.set(SETTINGS_KEY, next)
120
124
  return next
@@ -0,0 +1,226 @@
1
+ <%- include("partials/head", { title: adapter ? "Edit Adapter" : "New Adapter" }) %>
2
+
3
+ <div class="max-w-4xl">
4
+ <div class="flex justify-between items-center mb-6">
5
+ <div>
6
+ <p class="text-xs uppercase tracking-[0.2em] text-[#787774] mb-1">Management</p>
7
+ <h1 class="text-2xl editorial-heading text-[#111111]"><%= adapter ? "Edit Adapter" : "New Adapter" %></h1>
8
+ </div>
9
+ <div class="flex gap-2">
10
+ <% if (adapter) { %>
11
+ <a href="/api/adapters/<%= adapter.name %>/packages" class="btn">Packages</a>
12
+ <% } %>
13
+ <a href="/api/adapters" class="btn">Back</a>
14
+ </div>
15
+ </div>
16
+
17
+ <div id="adapter-editor">
18
+ <div v-if="message.text" :class="['alert mb-4', message.type === 'error' ? 'alert-error' : 'alert-success']">
19
+ <span>{{ message.text }}</span>
20
+ </div>
21
+
22
+ <form @submit.prevent="save" class="flex flex-col gap-4">
23
+ <div class="form-control">
24
+ <label class="label"><span class="label-text">Name</span></label>
25
+ <input v-model="form.name" class="input input-bordered input-sm" required
26
+ placeholder="e.g. postgres" :disabled="!!editName"
27
+ pattern="[a-zA-Z][a-zA-Z0-9_-]*"
28
+ title="Must start with letter, contain only letters, numbers, underscores, hyphens">
29
+ <span class="text-xs text-[#787774] mt-1">Unique identifier for this adapter</span>
30
+ </div>
31
+
32
+ <div class="form-control">
33
+ <label class="label"><span class="label-text">Description</span></label>
34
+ <input v-model="form.description" class="input input-bordered input-sm"
35
+ placeholder="What does this adapter do?">
36
+ </div>
37
+
38
+ <div class="form-control">
39
+ <label class="label"><span class="label-text">Execution Context</span></label>
40
+ <div class="flex gap-4 mt-2">
41
+ <label class="flex items-center gap-2 cursor-pointer">
42
+ <input type="radio" v-model="form.execution_context" value="server" class="radio">
43
+ <span>Server (runs on server, CLI receives result)</span>
44
+ </label>
45
+ <label class="flex items-center gap-2 cursor-pointer">
46
+ <input type="radio" v-model="form.execution_context" value="cli" class="radio">
47
+ <span>CLI (syncs to CLI, runs locally)</span>
48
+ </label>
49
+ </div>
50
+ <span class="text-xs text-[#787774] mt-1">
51
+ {{ form.execution_context === 'server' ?
52
+ 'Adapter runs on the server. Use for accessing server-side resources like internal databases.' :
53
+ 'Adapter code is synced to CLI and runs locally. Use for accessing user machine resources.' }}
54
+ </span>
55
+ </div>
56
+
57
+ <div class="grid grid-cols-3 gap-4">
58
+ <div class="form-control">
59
+ <label class="label"><span class="label-text">Timeout (ms)</span></label>
60
+ <input v-model.number="form.timeout_ms" type="number" class="input input-bordered input-sm"
61
+ min="1000" max="300000" step="1000">
62
+ </div>
63
+ <div class="form-control">
64
+ <label class="label"><span class="label-text">Memory Limit (MB)</span></label>
65
+ <input v-model.number="form.memory_limit_mb" type="number" class="input input-bordered input-sm"
66
+ min="16" max="512" step="16">
67
+ </div>
68
+ <div class="form-control">
69
+ <label class="label"><span class="label-text">Allow Network</span></label>
70
+ <input v-model="form.allow_network" type="checkbox" class="checkbox">
71
+ </div>
72
+ </div>
73
+
74
+ <div class="form-control">
75
+ <label class="label flex justify-between">
76
+ <span>Source Code</span>
77
+ <span class="text-xs text-[#787774]">Must export an 'execute' function</span>
78
+ </label>
79
+ <div ref="editor" class="border border-[#EAEAEA] rounded" style="height: 400px;"></div>
80
+ <span class="text-xs text-[#787774] mt-1">
81
+ The adapter must export an 'execute' function: <code>function execute(cmd, flags, context)</code>
82
+ </span>
83
+ </div>
84
+
85
+ <div class="flex gap-2 mt-4">
86
+ <button type="submit" class="btn btn-primary">Save</button>
87
+ <button type="button" @click="testAdapter" class="btn" :disabled="form.execution_context === 'cli'">
88
+ {{ form.execution_context === 'cli' ? 'Test (CLI only)' : 'Test' }}
89
+ </button>
90
+ <a href="/api/adapters" class="btn">Cancel</a>
91
+ </div>
92
+ </form>
93
+
94
+ <div v-if="testResult" class="mt-6 p-4 rounded bg-[#F7F6F3]">
95
+ <h3 class="text-sm font-medium mb-2">Test Result</h3>
96
+ <div v-if="testResult.ok" class="text-green-600 text-sm">
97
+ ✓ Success ({{ testResult.duration_ms }}ms)
98
+ </div>
99
+ <div v-else class="text-red-600 text-sm">
100
+ ✗ Failed ({{ testResult.duration_ms }}ms): {{ testResult.error }}
101
+ </div>
102
+ <pre v-if="testResult.result" class="mt-2 p-2 bg-white rounded text-xs overflow-auto">{{ JSON.stringify(testResult.result, null, 2) }}</pre>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
108
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/eclipse.min.css">
109
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
110
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
111
+
112
+ <script>
113
+ const defaultSource = `function execute(cmd, flags, context) {
114
+ // cmd: { namespace, resource, action, adapter, adapterConfig, args }
115
+ // flags: { key: value } - command line arguments
116
+ // context: { config, ... } - execution context
117
+
118
+ // Example: return data
119
+ return {
120
+ data: {
121
+ message: "Hello from custom adapter",
122
+ flags: flags
123
+ }
124
+ }
125
+ }
126
+
127
+ module.exports = { execute }
128
+ `
129
+
130
+ const editData = <%- JSON.stringify(adapter || null) %>
131
+ const editName = '<%= adapter ? adapter.name : "" %>'
132
+
133
+ Vue.createApp({
134
+ data() {
135
+ return {
136
+ editName,
137
+ form: {
138
+ name: editData?.name || '',
139
+ description: editData?.description || '',
140
+ execution_context: editData?.execution_context || 'server',
141
+ timeout_ms: editData?.timeout_ms || 30000,
142
+ memory_limit_mb: editData?.memory_limit_mb || 128,
143
+ allow_network: editData?.allow_network || false,
144
+ },
145
+ source: editData?.source || defaultSource,
146
+ message: { text: '', type: '' },
147
+ testResult: null,
148
+ editor: null,
149
+ }
150
+ },
151
+ mounted() {
152
+ this.editor = CodeMirror(this.$refs.editor, {
153
+ value: this.source,
154
+ mode: 'javascript',
155
+ theme: 'eclipse',
156
+ lineNumbers: true,
157
+ indentUnit: 2,
158
+ tabSize: 2,
159
+ lineWrapping: true,
160
+ viewportMargin: Infinity,
161
+ })
162
+ this.editor.on('change', () => {
163
+ this.source = this.editor.getValue()
164
+ })
165
+ },
166
+ methods: {
167
+ async save() {
168
+ this.message.text = ''
169
+ this.testResult = null
170
+
171
+ const payload = {
172
+ ...this.form,
173
+ source: this.source,
174
+ }
175
+
176
+ try {
177
+ const url = this.editName ? `/api/adapters/${this.editName}` : '/api/adapters'
178
+ const method = this.editName ? 'PUT' : 'POST'
179
+
180
+ const res = await fetch(url, {
181
+ method,
182
+ headers: { 'Content-Type': 'application/json' },
183
+ body: JSON.stringify(payload)
184
+ })
185
+
186
+ const data = await res.json()
187
+
188
+ if (!res.ok) {
189
+ throw new Error(data.error || 'Save failed')
190
+ }
191
+
192
+ this.message = { text: 'Adapter saved successfully', type: 'success' }
193
+
194
+ if (!this.editName) {
195
+ setTimeout(() => {
196
+ window.location.href = `/api/adapters/${data.adapter.name}/edit`
197
+ }, 500)
198
+ }
199
+ } catch (err) {
200
+ this.message = { text: err.message, type: 'error' }
201
+ }
202
+ },
203
+
204
+ async testAdapter() {
205
+ if (this.form.execution_context === 'cli') return
206
+
207
+ this.testResult = null
208
+ this.message.text = ''
209
+
210
+ try {
211
+ const res = await fetch(`/api/adapters/${this.editName}/test`, {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify({ flags: { test: true } })
215
+ })
216
+
217
+ this.testResult = await res.json()
218
+ } catch (err) {
219
+ this.testResult = { ok: false, error: err.message, duration_ms: 0 }
220
+ }
221
+ }
222
+ }
223
+ }).mount('#adapter-editor')
224
+ </script>
225
+
226
+ <%- include("partials/foot") %>