@hesed/webui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +67 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/webui.d.ts +12 -0
- package/dist/commands/webui.js +52 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/executor.d.ts +9 -0
- package/dist/lib/executor.js +42 -0
- package/dist/lib/introspect.d.ts +41 -0
- package/dist/lib/introspect.js +44 -0
- package/dist/lib/server.d.ts +17 -0
- package/dist/lib/server.js +98 -0
- package/oclif.manifest.json +54 -0
- package/package.json +88 -0
- package/web/.next/BUILD_ID +1 -0
- package/web/.next/app-build-manifest.json +26 -0
- package/web/.next/app-path-routes-manifest.json +4 -0
- package/web/.next/build-manifest.json +33 -0
- package/web/.next/cache/.previewinfo +1 -0
- package/web/.next/cache/.rscinfo +1 -0
- package/web/.next/cache/.tsbuildinfo +1 -0
- package/web/.next/cache/eslint/.cache_1ki824o +1 -0
- package/web/.next/cache/next-devtools-config.json +1 -0
- package/web/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/web/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/web/.next/cache/webpack/client-development/index.pack.gz +0 -0
- package/web/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/web/.next/cache/webpack/client-production/0.pack +0 -0
- package/web/.next/cache/webpack/client-production/1.pack +0 -0
- package/web/.next/cache/webpack/client-production/2.pack +0 -0
- package/web/.next/cache/webpack/client-production/3.pack +0 -0
- package/web/.next/cache/webpack/client-production/index.pack +0 -0
- package/web/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/web/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/web/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/web/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/web/.next/cache/webpack/server-production/0.pack +0 -0
- package/web/.next/cache/webpack/server-production/1.pack +0 -0
- package/web/.next/cache/webpack/server-production/index.pack +0 -0
- package/web/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/web/.next/diagnostics/build-diagnostics.json +6 -0
- package/web/.next/diagnostics/framework.json +1 -0
- package/web/.next/export-marker.json +6 -0
- package/web/.next/images-manifest.json +58 -0
- package/web/.next/next-minimal-server.js.nft.json +1 -0
- package/web/.next/next-server.js.nft.json +1 -0
- package/web/.next/package.json +1 -0
- package/web/.next/prerender-manifest.json +61 -0
- package/web/.next/react-loadable-manifest.json +1 -0
- package/web/.next/required-server-files.json +320 -0
- package/web/.next/routes-manifest.json +53 -0
- package/web/.next/server/app/_not-found/page.js +2 -0
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/_not-found.html +1 -0
- package/web/.next/server/app/_not-found.meta +8 -0
- package/web/.next/server/app/_not-found.rsc +16 -0
- package/web/.next/server/app/index.html +1 -0
- package/web/.next/server/app/index.meta +7 -0
- package/web/.next/server/app/index.rsc +20 -0
- package/web/.next/server/app/page.js +2 -0
- package/web/.next/server/app/page.js.nft.json +1 -0
- package/web/.next/server/app/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app-paths-manifest.json +4 -0
- package/web/.next/server/chunks/157.js +6 -0
- package/web/.next/server/chunks/799.js +30 -0
- package/web/.next/server/functions-config-manifest.json +4 -0
- package/web/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/web/.next/server/middleware-build-manifest.js +1 -0
- package/web/.next/server/middleware-manifest.json +6 -0
- package/web/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/web/.next/server/next-font-manifest.js +1 -0
- package/web/.next/server/next-font-manifest.json +1 -0
- package/web/.next/server/pages/404.html +1 -0
- package/web/.next/server/pages/500.html +1 -0
- package/web/.next/server/pages/_app.js +1 -0
- package/web/.next/server/pages/_app.js.nft.json +1 -0
- package/web/.next/server/pages/_document.js +1 -0
- package/web/.next/server/pages/_document.js.nft.json +1 -0
- package/web/.next/server/pages/_error.js +19 -0
- package/web/.next/server/pages/_error.js.nft.json +1 -0
- package/web/.next/server/pages-manifest.json +6 -0
- package/web/.next/server/server-reference-manifest.js +1 -0
- package/web/.next/server/server-reference-manifest.json +1 -0
- package/web/.next/server/webpack-runtime.js +1 -0
- package/web/.next/static/LEwu5WvMMaoVEbVj3-KGq/_buildManifest.js +1 -0
- package/web/.next/static/LEwu5WvMMaoVEbVj3-KGq/_ssgManifest.js +1 -0
- package/web/.next/static/chunks/131-a68a87dd22cef82b.js +1 -0
- package/web/.next/static/chunks/app/_not-found/page-784ddbf5f1a9343a.js +1 -0
- package/web/.next/static/chunks/app/layout-4e0fe443358078fc.js +1 -0
- package/web/.next/static/chunks/app/page-d098bd39f1fcec43.js +1 -0
- package/web/.next/static/chunks/c7879cf7-b5ab1053c1d9a2e7.js +1 -0
- package/web/.next/static/chunks/framework-1934959d81242241.js +1 -0
- package/web/.next/static/chunks/main-a8814cf406d931cd.js +1 -0
- package/web/.next/static/chunks/main-app-a3b742ef05fa17a3.js +1 -0
- package/web/.next/static/chunks/pages/_app-930c2e1cfe9b86bc.js +1 -0
- package/web/.next/static/chunks/pages/_error-2a950c2742f550d2.js +1 -0
- package/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/web/.next/static/chunks/webpack-3046868c425865e4.js +1 -0
- package/web/.next/static/css/48007c73fe98a161.css +1 -0
- package/web/.next/trace +2 -0
- package/web/.next/types/app/layout.ts +84 -0
- package/web/.next/types/app/page.ts +84 -0
- package/web/.next/types/cache-life.d.ts +141 -0
- package/web/.next/types/package.json +1 -0
- package/web/.next/types/routes.d.ts +57 -0
- package/web/.next/types/validator.ts +61 -0
- package/web/app/globals.css +339 -0
- package/web/app/layout.tsx +20 -0
- package/web/app/page.tsx +57 -0
- package/web/components/command-detail.tsx +160 -0
- package/web/components/command-list.tsx +67 -0
- package/web/components/theme-provider.tsx +12 -0
- package/web/components/theme-toggle.tsx +62 -0
- package/web/lib/types.ts +77 -0
- package/web/next.config.mjs +14 -0
- package/web/tsconfig.json +23 -0
package/web/app/page.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {useEffect, useState} from 'react'
|
|
4
|
+
|
|
5
|
+
import type {CommandsResponse} from '../lib/types'
|
|
6
|
+
|
|
7
|
+
import {CommandDetail} from '../components/command-detail'
|
|
8
|
+
import {CommandList} from '../components/command-list'
|
|
9
|
+
|
|
10
|
+
export default function Page() {
|
|
11
|
+
const [data, setData] = useState<CommandsResponse | null>(null)
|
|
12
|
+
const [error, setError] = useState<null | string>(null)
|
|
13
|
+
const [query, setQuery] = useState('')
|
|
14
|
+
const [selectedId, setSelectedId] = useState<string>()
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
fetch('/api/commands')
|
|
18
|
+
.then((res) => res.json())
|
|
19
|
+
.then((json: CommandsResponse) => setData(json))
|
|
20
|
+
.catch((error_) => setError(String(error_)))
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
if (error) {
|
|
24
|
+
return <div className="empty">Failed to load commands: {error}</div>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!data) {
|
|
28
|
+
return <div className="empty">Loading commands…</div>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const selected = data.commands.find((c) => c.id === selectedId)
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="app">
|
|
35
|
+
<CommandList
|
|
36
|
+
bin={data.bin}
|
|
37
|
+
commands={data.commands}
|
|
38
|
+
onSelect={setSelectedId}
|
|
39
|
+
query={query}
|
|
40
|
+
selectedId={selectedId}
|
|
41
|
+
setQuery={setQuery}
|
|
42
|
+
version={data.version}
|
|
43
|
+
/>
|
|
44
|
+
<main className="main">
|
|
45
|
+
{selected ? (
|
|
46
|
+
<CommandDetail bin={data.bin} command={selected} />
|
|
47
|
+
) : (
|
|
48
|
+
<div className="empty">
|
|
49
|
+
Select a command from the left to view its options and run it.
|
|
50
|
+
<br />
|
|
51
|
+
{data.commands.length} commands available.
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
</main>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {useMemo, useState} from 'react'
|
|
4
|
+
|
|
5
|
+
import {buildArgv, type CommandMeta, type RunResult} from '../lib/types'
|
|
6
|
+
|
|
7
|
+
export function CommandDetail({bin, command}: {bin: string; command: CommandMeta}) {
|
|
8
|
+
const [argValues, setArgValues] = useState<Record<string, string>>({})
|
|
9
|
+
const [flagValues, setFlagValues] = useState<Record<string, boolean | string>>({})
|
|
10
|
+
const [result, setResult] = useState<null | RunResult>(null)
|
|
11
|
+
const [running, setRunning] = useState(false)
|
|
12
|
+
|
|
13
|
+
// Reset form state whenever the selected command changes.
|
|
14
|
+
const argv = useMemo(() => buildArgv(command, argValues, flagValues), [command, argValues, flagValues])
|
|
15
|
+
const preview = `${bin || 'sdkck'} ${command.id}${argv.length > 0 ? ' ' + argv.join(' ') : ''}`
|
|
16
|
+
|
|
17
|
+
async function run() {
|
|
18
|
+
setRunning(true)
|
|
19
|
+
setResult(null)
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch('/api/run', {
|
|
22
|
+
body: JSON.stringify({argv, id: command.id}),
|
|
23
|
+
headers: {'content-type': 'application/json'},
|
|
24
|
+
method: 'POST',
|
|
25
|
+
})
|
|
26
|
+
setResult((await res.json()) as RunResult)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
setResult({durationMs: 0, error: String(error), output: String(error), success: false})
|
|
29
|
+
} finally {
|
|
30
|
+
setRunning(false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="detail" key={command.id}>
|
|
36
|
+
<h1>
|
|
37
|
+
{command.id}
|
|
38
|
+
{command.pluginName && <span className="badge">{command.pluginName}</span>}
|
|
39
|
+
</h1>
|
|
40
|
+
{(command.description || command.summary) && <p className="desc">{command.description ?? command.summary}</p>}
|
|
41
|
+
|
|
42
|
+
{command.args.length > 0 && (
|
|
43
|
+
<>
|
|
44
|
+
<div className="section-title">Arguments</div>
|
|
45
|
+
{command.args.map((arg) => (
|
|
46
|
+
<div className="field" key={arg.name}>
|
|
47
|
+
<label htmlFor={`arg-${arg.name}`}>
|
|
48
|
+
{arg.name}
|
|
49
|
+
{arg.required && <span className="req">*</span>}
|
|
50
|
+
</label>
|
|
51
|
+
{arg.description && <div className="hint">{arg.description}</div>}
|
|
52
|
+
{arg.options ? (
|
|
53
|
+
<select
|
|
54
|
+
id={`arg-${arg.name}`}
|
|
55
|
+
onChange={(e) => setArgValues((v) => ({...v, [arg.name]: e.target.value}))}
|
|
56
|
+
value={argValues[arg.name] ?? ''}
|
|
57
|
+
>
|
|
58
|
+
<option value="">— choose —</option>
|
|
59
|
+
{arg.options.map((opt) => (
|
|
60
|
+
<option key={opt} value={opt}>
|
|
61
|
+
{opt}
|
|
62
|
+
</option>
|
|
63
|
+
))}
|
|
64
|
+
</select>
|
|
65
|
+
) : (
|
|
66
|
+
<input
|
|
67
|
+
id={`arg-${arg.name}`}
|
|
68
|
+
onChange={(e) => setArgValues((v) => ({...v, [arg.name]: e.target.value}))}
|
|
69
|
+
placeholder={arg.default ? String(arg.default) : ''}
|
|
70
|
+
type="text"
|
|
71
|
+
value={argValues[arg.name] ?? ''}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{command.flags.length > 0 && (
|
|
80
|
+
<>
|
|
81
|
+
<div className="section-title">Flags</div>
|
|
82
|
+
{command.flags.map((flag) =>
|
|
83
|
+
flag.type === 'boolean' ? (
|
|
84
|
+
<div className="field checkbox-row" key={flag.name}>
|
|
85
|
+
<input
|
|
86
|
+
checked={flagValues[flag.name] === true}
|
|
87
|
+
id={`flag-${flag.name}`}
|
|
88
|
+
onChange={(e) => setFlagValues((v) => ({...v, [flag.name]: e.target.checked}))}
|
|
89
|
+
type="checkbox"
|
|
90
|
+
/>
|
|
91
|
+
<label htmlFor={`flag-${flag.name}`}>
|
|
92
|
+
--{flag.name}
|
|
93
|
+
{flag.description ? (
|
|
94
|
+
<span className="hint" style={{display: 'inline', marginLeft: 8}}>
|
|
95
|
+
{flag.description}
|
|
96
|
+
</span>
|
|
97
|
+
) : null}
|
|
98
|
+
</label>
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="field" key={flag.name}>
|
|
102
|
+
<label htmlFor={`flag-${flag.name}`}>
|
|
103
|
+
--{flag.name}
|
|
104
|
+
{flag.char ? ` (-${flag.char})` : ''}
|
|
105
|
+
{flag.required && <span className="req">*</span>}
|
|
106
|
+
{flag.multiple && <span className="badge">multiple</span>}
|
|
107
|
+
</label>
|
|
108
|
+
{flag.description && <div className="hint">{flag.description}</div>}
|
|
109
|
+
{flag.options ? (
|
|
110
|
+
<select
|
|
111
|
+
id={`flag-${flag.name}`}
|
|
112
|
+
onChange={(e) => setFlagValues((v) => ({...v, [flag.name]: e.target.value}))}
|
|
113
|
+
value={(flagValues[flag.name] as string) ?? ''}
|
|
114
|
+
>
|
|
115
|
+
<option value="">— choose —</option>
|
|
116
|
+
{flag.options.map((opt) => (
|
|
117
|
+
<option key={opt} value={opt}>
|
|
118
|
+
{opt}
|
|
119
|
+
</option>
|
|
120
|
+
))}
|
|
121
|
+
</select>
|
|
122
|
+
) : (
|
|
123
|
+
<input
|
|
124
|
+
id={`flag-${flag.name}`}
|
|
125
|
+
onChange={(e) => setFlagValues((v) => ({...v, [flag.name]: e.target.value}))}
|
|
126
|
+
placeholder={
|
|
127
|
+
flag.default === undefined
|
|
128
|
+
? flag.multiple
|
|
129
|
+
? 'comma,separated,values'
|
|
130
|
+
: ''
|
|
131
|
+
: String(flag.default)
|
|
132
|
+
}
|
|
133
|
+
type="text"
|
|
134
|
+
value={(flagValues[flag.name] as string) ?? ''}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
),
|
|
139
|
+
)}
|
|
140
|
+
</>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<div className="section-title">Command</div>
|
|
144
|
+
<div className="preview">$ {preview}</div>
|
|
145
|
+
|
|
146
|
+
<button className="run-btn" disabled={running} onClick={run} type="button">
|
|
147
|
+
{running ? 'Running…' : 'Run command'}
|
|
148
|
+
</button>
|
|
149
|
+
|
|
150
|
+
{result && (
|
|
151
|
+
<div className="output">
|
|
152
|
+
<div className={`status ${result.success ? 'ok' : 'err'}`}>
|
|
153
|
+
{result.success ? '✓ Success' : '✗ Failed'} · {result.durationMs}ms
|
|
154
|
+
</div>
|
|
155
|
+
<pre>{result.output || '(no output)'}</pre>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type {CommandMeta} from '../lib/types'
|
|
4
|
+
|
|
5
|
+
import {ThemeToggle} from './theme-toggle'
|
|
6
|
+
|
|
7
|
+
export function CommandList({
|
|
8
|
+
bin,
|
|
9
|
+
commands,
|
|
10
|
+
onSelect,
|
|
11
|
+
query,
|
|
12
|
+
selectedId,
|
|
13
|
+
setQuery,
|
|
14
|
+
version,
|
|
15
|
+
}: {
|
|
16
|
+
bin: string
|
|
17
|
+
commands: CommandMeta[]
|
|
18
|
+
onSelect: (id: string) => void
|
|
19
|
+
query: string
|
|
20
|
+
selectedId?: string
|
|
21
|
+
setQuery: (q: string) => void
|
|
22
|
+
version: string
|
|
23
|
+
}) {
|
|
24
|
+
const filtered = commands.filter((cmd) => {
|
|
25
|
+
if (!query) return true
|
|
26
|
+
const haystack = `${cmd.id} ${cmd.summary ?? ''} ${cmd.description ?? ''}`.toLowerCase()
|
|
27
|
+
return query
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.split(/\s+/)
|
|
30
|
+
.every((term) => haystack.includes(term))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<aside className="sidebar">
|
|
35
|
+
<div className="sidebar-header">
|
|
36
|
+
<div className="brand-row">
|
|
37
|
+
<div className="brand">
|
|
38
|
+
{bin || 'sdkck'} <small>web UI · v{version}</small>
|
|
39
|
+
</div>
|
|
40
|
+
<ThemeToggle />
|
|
41
|
+
</div>
|
|
42
|
+
<input
|
|
43
|
+
aria-label="Filter commands"
|
|
44
|
+
className="search"
|
|
45
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
46
|
+
placeholder="Filter commands…"
|
|
47
|
+
type="text"
|
|
48
|
+
value={query}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
<nav className="command-list">
|
|
52
|
+
{filtered.length === 0 && <div className="empty">No commands match.</div>}
|
|
53
|
+
{filtered.map((cmd) => (
|
|
54
|
+
<button
|
|
55
|
+
className={`command-item${cmd.id === selectedId ? ' active' : ''}`}
|
|
56
|
+
key={cmd.id}
|
|
57
|
+
onClick={() => onSelect(cmd.id)}
|
|
58
|
+
type="button"
|
|
59
|
+
>
|
|
60
|
+
<span className="cmd-id">{cmd.id}</span>
|
|
61
|
+
<span className="cmd-summary">{cmd.summary ?? cmd.description ?? ''}</span>
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</nav>
|
|
65
|
+
</aside>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {ThemeProvider as NextThemesProvider} from 'next-themes'
|
|
4
|
+
import {type ReactNode} from 'react'
|
|
5
|
+
|
|
6
|
+
export function ThemeProvider({children}: {children: ReactNode}) {
|
|
7
|
+
return (
|
|
8
|
+
<NextThemesProvider attribute="class" defaultTheme="dark" disableTransitionOnChange enableSystem>
|
|
9
|
+
{children}
|
|
10
|
+
</NextThemesProvider>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {useTheme} from 'next-themes'
|
|
4
|
+
import {useEffect, useState} from 'react'
|
|
5
|
+
|
|
6
|
+
export function ThemeToggle() {
|
|
7
|
+
const {setTheme, theme} = useTheme()
|
|
8
|
+
const [mounted, setMounted] = useState(false)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
setMounted(true)
|
|
12
|
+
}, [])
|
|
13
|
+
|
|
14
|
+
if (!mounted) {
|
|
15
|
+
return <div className="theme-toggle-placeholder" />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<button
|
|
20
|
+
aria-label="Toggle theme"
|
|
21
|
+
className="theme-toggle"
|
|
22
|
+
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
23
|
+
type="button"
|
|
24
|
+
>
|
|
25
|
+
{theme === 'dark' ? (
|
|
26
|
+
<svg
|
|
27
|
+
fill="none"
|
|
28
|
+
height="15"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
strokeLinejoin="round"
|
|
32
|
+
strokeWidth="2"
|
|
33
|
+
viewBox="0 0 24 24"
|
|
34
|
+
width="15"
|
|
35
|
+
>
|
|
36
|
+
<circle cx="12" cy="12" r="4" />
|
|
37
|
+
<path d="M12 2v2" />
|
|
38
|
+
<path d="M12 20v2" />
|
|
39
|
+
<path d="m4.93 4.93 1.41 1.41" />
|
|
40
|
+
<path d="m17.66 17.66 1.41 1.41" />
|
|
41
|
+
<path d="M2 12h2" />
|
|
42
|
+
<path d="M20 12h2" />
|
|
43
|
+
<path d="m6.34 17.66-1.41 1.41" />
|
|
44
|
+
<path d="m19.07 4.93-1.41 1.41" />
|
|
45
|
+
</svg>
|
|
46
|
+
) : (
|
|
47
|
+
<svg
|
|
48
|
+
fill="none"
|
|
49
|
+
height="15"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
strokeLinecap="round"
|
|
52
|
+
strokeLinejoin="round"
|
|
53
|
+
strokeWidth="2"
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
width="15"
|
|
56
|
+
>
|
|
57
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
58
|
+
</svg>
|
|
59
|
+
)}
|
|
60
|
+
</button>
|
|
61
|
+
)
|
|
62
|
+
}
|
package/web/lib/types.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface FlagMeta {
|
|
2
|
+
char?: string
|
|
3
|
+
default?: unknown
|
|
4
|
+
description?: string
|
|
5
|
+
multiple: boolean
|
|
6
|
+
name: string
|
|
7
|
+
options?: string[]
|
|
8
|
+
required: boolean
|
|
9
|
+
type: 'boolean' | 'option'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ArgMeta {
|
|
13
|
+
default?: unknown
|
|
14
|
+
description?: string
|
|
15
|
+
name: string
|
|
16
|
+
options?: string[]
|
|
17
|
+
required: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CommandMeta {
|
|
21
|
+
aliases: string[]
|
|
22
|
+
args: ArgMeta[]
|
|
23
|
+
description?: string
|
|
24
|
+
flags: FlagMeta[]
|
|
25
|
+
id: string
|
|
26
|
+
pluginName?: string
|
|
27
|
+
pluginType?: string
|
|
28
|
+
summary?: string
|
|
29
|
+
usage?: string | string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CommandsResponse {
|
|
33
|
+
bin: string
|
|
34
|
+
commands: CommandMeta[]
|
|
35
|
+
version: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RunResult {
|
|
39
|
+
durationMs: number
|
|
40
|
+
error?: string
|
|
41
|
+
output: string
|
|
42
|
+
success: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Build an oclif-style argv array from form values for a command. */
|
|
46
|
+
export function buildArgv(
|
|
47
|
+
command: CommandMeta,
|
|
48
|
+
argValues: Record<string, string>,
|
|
49
|
+
flagValues: Record<string, boolean | string>,
|
|
50
|
+
): string[] {
|
|
51
|
+
const argv: string[] = []
|
|
52
|
+
|
|
53
|
+
for (const arg of command.args) {
|
|
54
|
+
const value = argValues[arg.name]
|
|
55
|
+
if (value !== undefined && value !== '') argv.push(value)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const flag of command.flags) {
|
|
59
|
+
const value = flagValues[flag.name]
|
|
60
|
+
if (flag.type === 'boolean') {
|
|
61
|
+
if (value === true) argv.push(`--${flag.name}`)
|
|
62
|
+
} else if (typeof value === 'string' && value !== '') {
|
|
63
|
+
if (flag.multiple) {
|
|
64
|
+
for (const part of value
|
|
65
|
+
.split(',')
|
|
66
|
+
.map((s) => s.trim())
|
|
67
|
+
.filter(Boolean)) {
|
|
68
|
+
argv.push(`--${flag.name}`, part)
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
argv.push(`--${flag.name}`, value)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return argv
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {dirname} from 'node:path'
|
|
2
|
+
import {fileURLToPath} from 'node:url'
|
|
3
|
+
|
|
4
|
+
/** @type {import('next').NextConfig} */
|
|
5
|
+
const nextConfig = {
|
|
6
|
+
// The plugin ships its own lockfile alongside the host repo's; pin the trace
|
|
7
|
+
// root to this directory so Next doesn't guess the wrong workspace root.
|
|
8
|
+
outputFileTracingRoot: dirname(fileURLToPath(import.meta.url)),
|
|
9
|
+
// The app is always served behind the plugin's custom Node server, which owns
|
|
10
|
+
// the /api/* routes; Next only renders the UI.
|
|
11
|
+
reactStrictMode: true,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default nextConfig
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{"name": "next"}],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|