@kirrosh/zond 0.20.0 → 0.22.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/CHANGELOG.md +110 -3
- package/README.md +26 -15
- package/package.json +10 -6
- package/src/cli/commands/catalog.ts +62 -0
- package/src/cli/commands/ci-init.ts +12 -6
- package/src/cli/commands/completions.ts +176 -0
- package/src/cli/commands/db.ts +2 -1
- package/src/cli/commands/generate.ts +18 -2
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +79 -0
- package/src/cli/commands/init/skills.ts +45 -0
- package/src/cli/commands/init/templates/agents.md +73 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/scenarios.md +97 -0
- package/src/cli/commands/init/templates/skills/zond.md +184 -0
- package/src/cli/commands/init/templates/zond-config.yml +15 -0
- package/src/cli/commands/init.ts +124 -31
- package/src/cli/commands/probe-methods.ts +108 -0
- package/src/cli/commands/probe-validation.ts +124 -0
- package/src/cli/commands/run.ts +99 -10
- package/src/cli/commands/serve.ts +52 -19
- package/src/cli/commands/sync.ts +28 -1
- package/src/cli/commands/update.ts +1 -1
- package/src/cli/commands/use.ts +57 -0
- package/src/cli/index.ts +21 -591
- package/src/cli/program.ts +655 -0
- package/src/cli/version.ts +3 -0
- package/src/core/context/current.ts +35 -0
- package/src/core/diagnostics/db-analysis.ts +11 -2
- package/src/core/diagnostics/render-md.ts +112 -0
- package/src/core/generator/catalog-builder.ts +179 -0
- package/src/core/generator/chunker.ts +14 -2
- package/src/core/generator/data-factory.ts +50 -19
- package/src/core/generator/guide-builder.ts +1 -1
- package/src/core/generator/index.ts +2 -0
- package/src/core/generator/openapi-reader.ts +18 -0
- package/src/core/generator/serializer.ts +11 -2
- package/src/core/generator/suite-generator.ts +106 -7
- package/src/core/meta/types.ts +0 -2
- package/src/core/parser/schema.ts +3 -1
- package/src/core/parser/types.ts +10 -1
- package/src/core/parser/variables.ts +90 -2
- package/src/core/parser/yaml-parser.ts +50 -1
- package/src/core/probe/method-probe.ts +197 -0
- package/src/core/probe/negative-probe.ts +657 -0
- package/src/core/reporter/console.ts +29 -3
- package/src/core/reporter/index.ts +2 -2
- package/src/core/reporter/json.ts +5 -2
- package/src/core/runner/assertions.ts +4 -1
- package/src/core/runner/executor.ts +132 -37
- package/src/core/runner/http-client.ts +40 -5
- package/src/core/runner/rate-limiter.ts +131 -0
- package/src/core/setup-api.ts +4 -1
- package/src/core/workspace/root.ts +94 -0
- package/src/db/schema.ts +4 -1
- package/src/web/routes/api.ts +80 -0
- package/src/web/routes/dashboard.ts +15 -0
- package/src/web/static/style.css +290 -0
- package/src/web/views/explorer-tab.ts +402 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Files / directories that mark a workspace root. Order matters — earlier
|
|
7
|
+
* markers win when more than one is present in the same directory.
|
|
8
|
+
*
|
|
9
|
+
* zond.config.yml — explicit project config (T12)
|
|
10
|
+
* .zond/ — `zond init --here` subdir convention (T19)
|
|
11
|
+
* zond.db — flat layout from `zond init`
|
|
12
|
+
* apis/ — flat layout (collections directory)
|
|
13
|
+
*/
|
|
14
|
+
export const WORKSPACE_MARKERS = ["zond.config.yml", ".zond", "zond.db", "apis"] as const;
|
|
15
|
+
export type WorkspaceMarker = (typeof WORKSPACE_MARKERS)[number];
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceInfo {
|
|
18
|
+
/** Absolute path to the workspace root. */
|
|
19
|
+
root: string;
|
|
20
|
+
/** Marker that triggered detection, or "" when fallback (cwd) was used. */
|
|
21
|
+
marker: WorkspaceMarker | "";
|
|
22
|
+
/** True when no marker was found and we fell back to `cwd`. */
|
|
23
|
+
fromFallback: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let warned = false;
|
|
27
|
+
|
|
28
|
+
function hasMarker(dir: string): WorkspaceMarker | null {
|
|
29
|
+
for (const m of WORKSPACE_MARKERS) {
|
|
30
|
+
const p = join(dir, m);
|
|
31
|
+
if (!existsSync(p)) continue;
|
|
32
|
+
// .zond and apis must be directories; zond.config.yml and zond.db must be files
|
|
33
|
+
try {
|
|
34
|
+
const st = statSync(p);
|
|
35
|
+
if (m === ".zond" || m === "apis") {
|
|
36
|
+
if (st.isDirectory()) return m;
|
|
37
|
+
} else if (st.isFile()) {
|
|
38
|
+
return m;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
/* race / permissions — treat as no marker */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Walk-up from `cwd` (default `process.cwd()`) to the nearest workspace
|
|
49
|
+
* marker. The walk stops at `os.homedir()` to avoid accidentally picking up
|
|
50
|
+
* `~/apis` or `~/zond.db` when the user runs zond from somewhere unrelated.
|
|
51
|
+
*
|
|
52
|
+
* When no marker is found, returns `{ root: cwd, fromFallback: true }` and
|
|
53
|
+
* prints a one-time stderr warning so the user knows zond is operating in
|
|
54
|
+
* cwd-mode.
|
|
55
|
+
*/
|
|
56
|
+
export function findWorkspaceRoot(cwd?: string): WorkspaceInfo {
|
|
57
|
+
const start = resolve(cwd ?? process.cwd());
|
|
58
|
+
const stop = resolve(homedir());
|
|
59
|
+
|
|
60
|
+
let dir = start;
|
|
61
|
+
// Walk strictly while above (or equal to) HOME's length, but include HOME
|
|
62
|
+
// itself as a candidate only when start is inside HOME. If start is outside
|
|
63
|
+
// HOME (e.g. /tmp), walk all the way to "/".
|
|
64
|
+
const insideHome = start === stop || start.startsWith(stop + "/") || start.startsWith(stop + "\\");
|
|
65
|
+
|
|
66
|
+
while (true) {
|
|
67
|
+
const marker = hasMarker(dir);
|
|
68
|
+
if (marker) return { root: dir, marker, fromFallback: false };
|
|
69
|
+
|
|
70
|
+
const parent = dirname(dir);
|
|
71
|
+
if (parent === dir) break; // filesystem root
|
|
72
|
+
if (insideHome && dir === stop) break; // do not climb past HOME
|
|
73
|
+
dir = parent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!warned) {
|
|
77
|
+
warned = true;
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
`[zond] no workspace marker found from ${start}; using cwd. ` +
|
|
80
|
+
`Run 'zond init' or create zond.config.yml to anchor the workspace.\n`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return { root: start, marker: "", fromFallback: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Resolve `relative` against the workspace root (auto-detected from `cwd`). */
|
|
87
|
+
export function resolveWorkspacePath(relative: string, cwd?: string): string {
|
|
88
|
+
return resolve(findWorkspaceRoot(cwd).root, relative);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Test helper: reset the one-shot warning latch. */
|
|
92
|
+
export function _resetWorkspaceWarning(): void {
|
|
93
|
+
warned = false;
|
|
94
|
+
}
|
package/src/db/schema.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
|
+
import { findWorkspaceRoot } from "../core/workspace/root.ts";
|
|
4
5
|
|
|
5
6
|
let _db: Database | null = null;
|
|
6
7
|
let _dbPath: string | null = null;
|
|
7
8
|
|
|
8
9
|
export function getDb(dbPath?: string): Database {
|
|
9
|
-
const path = dbPath
|
|
10
|
+
const path = dbPath
|
|
11
|
+
? resolve(dbPath)
|
|
12
|
+
: (_dbPath ?? resolve(findWorkspaceRoot().root, "zond.db"));
|
|
10
13
|
|
|
11
14
|
// If cached connection exists, verify the file still exists
|
|
12
15
|
if (_db && _dbPath === path && existsSync(path)) return _db;
|
package/src/web/routes/api.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
RunDetailSchema,
|
|
13
13
|
RunIdParam,
|
|
14
14
|
} from "../schemas.ts";
|
|
15
|
+
import { renderProxyResponse, renderProxyError } from "../views/explorer-tab.ts";
|
|
15
16
|
|
|
16
17
|
const api = new OpenAPIHono();
|
|
17
18
|
|
|
@@ -114,6 +115,85 @@ api.openapi(runRoute, async (c) => {
|
|
|
114
115
|
}
|
|
115
116
|
});
|
|
116
117
|
|
|
118
|
+
// ──────────────────────────────────────────────
|
|
119
|
+
// POST /api/proxy — Explorer proxy for HTTP requests
|
|
120
|
+
// ──────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
api.post("/api/proxy", async (c) => {
|
|
123
|
+
const form = await c.req.parseBody();
|
|
124
|
+
const baseUrl = (form["base_url"] as string) ?? "";
|
|
125
|
+
const method = ((form["method"] as string) ?? "GET").toUpperCase();
|
|
126
|
+
let path = (form["path"] as string) ?? "/";
|
|
127
|
+
const body = (form["body"] as string) || undefined;
|
|
128
|
+
const contentType = (form["content_type"] as string) || undefined;
|
|
129
|
+
|
|
130
|
+
if (!baseUrl) {
|
|
131
|
+
return c.html(renderProxyError("Base URL is required", 0));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Substitute path parameters
|
|
135
|
+
for (const [key, value] of Object.entries(form)) {
|
|
136
|
+
if (typeof key === "string" && key.startsWith("param_path_") && value) {
|
|
137
|
+
const paramName = key.slice("param_path_".length);
|
|
138
|
+
path = path.replace(`{${paramName}}`, encodeURIComponent(value as string));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Build query string
|
|
143
|
+
const queryParams = new URLSearchParams();
|
|
144
|
+
for (const [key, value] of Object.entries(form)) {
|
|
145
|
+
if (typeof key === "string" && key.startsWith("param_query_") && value) {
|
|
146
|
+
queryParams.set(key.slice("param_query_".length), value as string);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Build headers
|
|
151
|
+
const headers: Record<string, string> = {};
|
|
152
|
+
for (const [key, value] of Object.entries(form)) {
|
|
153
|
+
if (typeof key === "string" && key.startsWith("param_header_") && value) {
|
|
154
|
+
headers[key.slice("param_header_".length)] = value as string;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Custom headers
|
|
158
|
+
for (let i = 0; i < 50; i++) {
|
|
159
|
+
const k = form[`custom_header_key_${i}`] as string | undefined;
|
|
160
|
+
const v = form[`custom_header_value_${i}`] as string | undefined;
|
|
161
|
+
if (!k && !v) break;
|
|
162
|
+
if (k && v) headers[k] = v;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (contentType && body) {
|
|
166
|
+
headers["Content-Type"] = contentType;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Build URL
|
|
170
|
+
let url: URL;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(path, baseUrl.endsWith("/") ? baseUrl : baseUrl + "/");
|
|
173
|
+
queryParams.forEach((v, k) => url.searchParams.set(k, v));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return c.html(renderProxyError(`Invalid URL: ${baseUrl}${path} — ${(err as Error).message}`, 0));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const startTime = performance.now();
|
|
179
|
+
try {
|
|
180
|
+
const resp = await fetch(url.toString(), {
|
|
181
|
+
method,
|
|
182
|
+
headers,
|
|
183
|
+
body: ["GET", "HEAD"].includes(method) ? undefined : (body || undefined),
|
|
184
|
+
});
|
|
185
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
186
|
+
const respBody = await resp.text();
|
|
187
|
+
const respHeaders: Record<string, string> = {};
|
|
188
|
+
resp.headers.forEach((v, k) => { respHeaders[k] = v; });
|
|
189
|
+
|
|
190
|
+
return c.html(renderProxyResponse(resp.status, respHeaders, respBody, elapsed));
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
193
|
+
return c.html(renderProxyError((err as Error).message, elapsed));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
117
197
|
// ──────────────────────────────────────────────
|
|
118
198
|
// Export helpers
|
|
119
199
|
// ──────────────────────────────────────────────
|
|
@@ -5,6 +5,7 @@ import { renderHealthStrip } from "../views/health-strip.ts";
|
|
|
5
5
|
import { renderEndpointsTab } from "../views/endpoints-tab.ts";
|
|
6
6
|
import { renderSuitesTab } from "../views/suites-tab.ts";
|
|
7
7
|
import { renderRunsTab, renderRunDetail } from "../views/runs-tab.ts";
|
|
8
|
+
import { renderExplorerTab } from "../views/explorer-tab.ts";
|
|
8
9
|
import { buildCollectionState, invalidateCollectionCache } from "../data/collection-state.ts";
|
|
9
10
|
import {
|
|
10
11
|
listCollections,
|
|
@@ -81,6 +82,16 @@ dashboard.get("/panels/endpoints", async (c) => {
|
|
|
81
82
|
return c.html(renderEndpointsTab(state, filters));
|
|
82
83
|
});
|
|
83
84
|
|
|
85
|
+
dashboard.get("/panels/explorer", async (c) => {
|
|
86
|
+
const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
|
|
87
|
+
if (isNaN(collectionId)) return c.html("");
|
|
88
|
+
|
|
89
|
+
const collection = getCollectionById(collectionId);
|
|
90
|
+
if (!collection) return c.html("");
|
|
91
|
+
|
|
92
|
+
return c.html(await renderExplorerTab(collection));
|
|
93
|
+
});
|
|
94
|
+
|
|
84
95
|
dashboard.get("/panels/suites", async (c) => {
|
|
85
96
|
const collectionId = parseInt(c.req.query("collection_id") ?? "", 10);
|
|
86
97
|
if (isNaN(collectionId)) return c.html("");
|
|
@@ -243,6 +254,10 @@ async function renderCollectionContent(collection: CollectionRecord): Promise<st
|
|
|
243
254
|
hx-get="/panels/runs-tab?collection_id=${collection.id}"
|
|
244
255
|
hx-target="#tab-content" hx-swap="innerHTML"
|
|
245
256
|
onclick="activateTab(this)">Runs <span class="tab-count">${runCount}</span></button>
|
|
257
|
+
<button class="tab-btn" data-tab="explorer"
|
|
258
|
+
hx-get="/panels/explorer?collection_id=${collection.id}"
|
|
259
|
+
hx-target="#tab-content" hx-swap="innerHTML"
|
|
260
|
+
onclick="activateTab(this)">Explorer</button>
|
|
246
261
|
</div>`;
|
|
247
262
|
|
|
248
263
|
// Default tab content (endpoints)
|
package/src/web/static/style.css
CHANGED
|
@@ -847,6 +847,294 @@ h3 { font-size: 1.05rem; margin: 1rem 0 0.25rem; }
|
|
|
847
847
|
100% { background: transparent; }
|
|
848
848
|
}
|
|
849
849
|
|
|
850
|
+
/* ═══════════════════════════════════════════════════
|
|
851
|
+
Explorer Tab
|
|
852
|
+
═══════════════════════════════════════════════════ */
|
|
853
|
+
|
|
854
|
+
.explorer-base-url {
|
|
855
|
+
display: flex;
|
|
856
|
+
align-items: center;
|
|
857
|
+
gap: 0.5rem;
|
|
858
|
+
padding: 0.75rem 1rem;
|
|
859
|
+
background: var(--bg-secondary);
|
|
860
|
+
border: 1px solid var(--border);
|
|
861
|
+
border-radius: var(--radius);
|
|
862
|
+
margin-bottom: 1rem;
|
|
863
|
+
}
|
|
864
|
+
.explorer-base-url .explorer-label {
|
|
865
|
+
font-size: 0.8rem;
|
|
866
|
+
font-weight: 600;
|
|
867
|
+
color: var(--text-dim);
|
|
868
|
+
white-space: nowrap;
|
|
869
|
+
}
|
|
870
|
+
.explorer-input {
|
|
871
|
+
background: var(--bg-inset);
|
|
872
|
+
border: 1px solid var(--border);
|
|
873
|
+
border-radius: var(--radius-sm);
|
|
874
|
+
color: var(--text);
|
|
875
|
+
font-family: var(--font-mono);
|
|
876
|
+
font-size: 0.85rem;
|
|
877
|
+
padding: 0.4rem 0.6rem;
|
|
878
|
+
flex: 1;
|
|
879
|
+
min-width: 0;
|
|
880
|
+
}
|
|
881
|
+
.explorer-input:focus {
|
|
882
|
+
outline: none;
|
|
883
|
+
border-color: var(--accent);
|
|
884
|
+
}
|
|
885
|
+
.explorer-input-sm { flex: unset; width: auto; }
|
|
886
|
+
|
|
887
|
+
/* Groups */
|
|
888
|
+
.explorer-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
889
|
+
.explorer-group {
|
|
890
|
+
border: 1px solid var(--border);
|
|
891
|
+
border-radius: var(--radius);
|
|
892
|
+
overflow: hidden;
|
|
893
|
+
}
|
|
894
|
+
.explorer-group-title {
|
|
895
|
+
font-size: 0.85rem;
|
|
896
|
+
font-weight: 600;
|
|
897
|
+
padding: 0.6rem 1rem;
|
|
898
|
+
background: var(--bg-secondary);
|
|
899
|
+
cursor: pointer;
|
|
900
|
+
display: flex;
|
|
901
|
+
align-items: center;
|
|
902
|
+
gap: 0.5rem;
|
|
903
|
+
}
|
|
904
|
+
.explorer-group-title .tab-count {
|
|
905
|
+
font-size: 0.75rem;
|
|
906
|
+
background: var(--bg-hover);
|
|
907
|
+
padding: 0.1rem 0.5rem;
|
|
908
|
+
border-radius: var(--radius-pill);
|
|
909
|
+
color: var(--text-dim);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* Endpoint row */
|
|
913
|
+
.explorer-endpoint {
|
|
914
|
+
display: flex;
|
|
915
|
+
align-items: center;
|
|
916
|
+
gap: 0.5rem;
|
|
917
|
+
padding: 0.5rem 1rem;
|
|
918
|
+
cursor: pointer;
|
|
919
|
+
border-top: 1px solid var(--border-subtle);
|
|
920
|
+
transition: background 0.15s;
|
|
921
|
+
}
|
|
922
|
+
.explorer-endpoint:hover { background: var(--bg-hover); }
|
|
923
|
+
.explorer-endpoint-path {
|
|
924
|
+
font-family: var(--font-mono);
|
|
925
|
+
font-size: 0.85rem;
|
|
926
|
+
font-weight: 500;
|
|
927
|
+
}
|
|
928
|
+
.explorer-endpoint-summary {
|
|
929
|
+
color: var(--text-dim);
|
|
930
|
+
font-size: 0.8rem;
|
|
931
|
+
margin-left: auto;
|
|
932
|
+
white-space: nowrap;
|
|
933
|
+
overflow: hidden;
|
|
934
|
+
text-overflow: ellipsis;
|
|
935
|
+
}
|
|
936
|
+
.explorer-auth-hint {
|
|
937
|
+
font-size: 0.65rem;
|
|
938
|
+
font-weight: 600;
|
|
939
|
+
padding: 0.1rem 0.35rem;
|
|
940
|
+
border-radius: var(--radius-sm);
|
|
941
|
+
background: var(--warn-dim);
|
|
942
|
+
color: var(--warn);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/* Detail / form */
|
|
946
|
+
.explorer-detail {
|
|
947
|
+
padding: 1rem;
|
|
948
|
+
background: var(--bg-raised);
|
|
949
|
+
border-top: 1px solid var(--border);
|
|
950
|
+
}
|
|
951
|
+
.explorer-section {
|
|
952
|
+
margin-bottom: 1rem;
|
|
953
|
+
}
|
|
954
|
+
.explorer-section-title {
|
|
955
|
+
font-size: 0.8rem;
|
|
956
|
+
font-weight: 600;
|
|
957
|
+
color: var(--text-dim);
|
|
958
|
+
margin-bottom: 0.5rem;
|
|
959
|
+
display: flex;
|
|
960
|
+
align-items: center;
|
|
961
|
+
gap: 0.5rem;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/* Parameters */
|
|
965
|
+
.explorer-param-row {
|
|
966
|
+
display: grid;
|
|
967
|
+
grid-template-columns: 140px 50px 90px 1fr;
|
|
968
|
+
gap: 0.5rem;
|
|
969
|
+
align-items: center;
|
|
970
|
+
margin-bottom: 0.35rem;
|
|
971
|
+
font-size: 0.85rem;
|
|
972
|
+
}
|
|
973
|
+
.explorer-param-name {
|
|
974
|
+
font-family: var(--font-mono);
|
|
975
|
+
font-weight: 500;
|
|
976
|
+
font-size: 0.8rem;
|
|
977
|
+
}
|
|
978
|
+
.explorer-param-location {
|
|
979
|
+
font-size: 0.7rem;
|
|
980
|
+
color: var(--text-muted);
|
|
981
|
+
background: var(--bg-inset);
|
|
982
|
+
padding: 0.1rem 0.3rem;
|
|
983
|
+
border-radius: var(--radius-sm);
|
|
984
|
+
text-align: center;
|
|
985
|
+
}
|
|
986
|
+
.explorer-param-type {
|
|
987
|
+
font-size: 0.75rem;
|
|
988
|
+
color: var(--text-dim);
|
|
989
|
+
font-family: var(--font-mono);
|
|
990
|
+
}
|
|
991
|
+
.explorer-required {
|
|
992
|
+
color: var(--fail);
|
|
993
|
+
font-weight: 700;
|
|
994
|
+
margin-left: 0.15rem;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* Body editor */
|
|
998
|
+
.explorer-body-editor {
|
|
999
|
+
width: 100%;
|
|
1000
|
+
min-height: 120px;
|
|
1001
|
+
background: var(--bg-inset);
|
|
1002
|
+
border: 1px solid var(--border);
|
|
1003
|
+
border-radius: var(--radius-sm);
|
|
1004
|
+
color: var(--text);
|
|
1005
|
+
font-family: var(--font-mono);
|
|
1006
|
+
font-size: 0.85rem;
|
|
1007
|
+
padding: 0.5rem;
|
|
1008
|
+
resize: vertical;
|
|
1009
|
+
tab-size: 2;
|
|
1010
|
+
}
|
|
1011
|
+
.explorer-body-editor:focus { outline: none; border-color: var(--accent); }
|
|
1012
|
+
.explorer-content-type {
|
|
1013
|
+
font-size: 0.75rem;
|
|
1014
|
+
padding: 0.15rem 0.35rem;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/* Custom headers */
|
|
1018
|
+
.explorer-headers-list {
|
|
1019
|
+
display: flex;
|
|
1020
|
+
flex-direction: column;
|
|
1021
|
+
gap: 0.35rem;
|
|
1022
|
+
}
|
|
1023
|
+
.explorer-header-pair {
|
|
1024
|
+
display: flex;
|
|
1025
|
+
gap: 0.35rem;
|
|
1026
|
+
align-items: center;
|
|
1027
|
+
}
|
|
1028
|
+
.explorer-header-pair input { flex: 1; }
|
|
1029
|
+
.explorer-remove-btn {
|
|
1030
|
+
background: none;
|
|
1031
|
+
border: 1px solid var(--border);
|
|
1032
|
+
border-radius: var(--radius-sm);
|
|
1033
|
+
color: var(--text-dim);
|
|
1034
|
+
cursor: pointer;
|
|
1035
|
+
font-size: 0.75rem;
|
|
1036
|
+
padding: 0.25rem 0.5rem;
|
|
1037
|
+
line-height: 1;
|
|
1038
|
+
}
|
|
1039
|
+
.explorer-remove-btn:hover { color: var(--fail); border-color: var(--fail); }
|
|
1040
|
+
.explorer-add-header-btn {
|
|
1041
|
+
background: none;
|
|
1042
|
+
border: none;
|
|
1043
|
+
color: var(--accent);
|
|
1044
|
+
cursor: pointer;
|
|
1045
|
+
font-size: 0.8rem;
|
|
1046
|
+
padding: 0.25rem 0;
|
|
1047
|
+
margin-top: 0.25rem;
|
|
1048
|
+
}
|
|
1049
|
+
.explorer-add-header-btn:hover { text-decoration: underline; }
|
|
1050
|
+
|
|
1051
|
+
/* Actions */
|
|
1052
|
+
.explorer-actions {
|
|
1053
|
+
display: flex;
|
|
1054
|
+
align-items: center;
|
|
1055
|
+
gap: 0.75rem;
|
|
1056
|
+
margin-top: 0.5rem;
|
|
1057
|
+
}
|
|
1058
|
+
.explorer-send-btn {
|
|
1059
|
+
background: var(--accent);
|
|
1060
|
+
color: #fff;
|
|
1061
|
+
border: none;
|
|
1062
|
+
border-radius: var(--radius);
|
|
1063
|
+
padding: 0.5rem 1.5rem;
|
|
1064
|
+
font-weight: 600;
|
|
1065
|
+
font-size: 0.85rem;
|
|
1066
|
+
cursor: pointer;
|
|
1067
|
+
transition: background 0.15s;
|
|
1068
|
+
}
|
|
1069
|
+
.explorer-send-btn:hover { background: var(--accent-hover); }
|
|
1070
|
+
.explorer-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1071
|
+
.explorer-spinner { color: var(--text-dim); font-size: 0.85rem; }
|
|
1072
|
+
|
|
1073
|
+
/* Response */
|
|
1074
|
+
.explorer-response-container { margin-top: 1rem; }
|
|
1075
|
+
.explorer-response {
|
|
1076
|
+
border: 1px solid var(--border);
|
|
1077
|
+
border-radius: var(--radius);
|
|
1078
|
+
overflow: hidden;
|
|
1079
|
+
}
|
|
1080
|
+
.explorer-response-error { border-color: var(--fail); }
|
|
1081
|
+
.response-meta {
|
|
1082
|
+
display: flex;
|
|
1083
|
+
align-items: center;
|
|
1084
|
+
gap: 1rem;
|
|
1085
|
+
padding: 0.5rem 0.75rem;
|
|
1086
|
+
background: var(--bg-secondary);
|
|
1087
|
+
border-bottom: 1px solid var(--border);
|
|
1088
|
+
font-size: 0.85rem;
|
|
1089
|
+
}
|
|
1090
|
+
.response-status {
|
|
1091
|
+
font-weight: 700;
|
|
1092
|
+
font-family: var(--font-mono);
|
|
1093
|
+
}
|
|
1094
|
+
.response-status.status-2xx { color: var(--pass); }
|
|
1095
|
+
.response-status.status-3xx { color: var(--info); }
|
|
1096
|
+
.response-status.status-4xx { color: var(--warn); }
|
|
1097
|
+
.response-status.status-5xx { color: var(--fail); }
|
|
1098
|
+
.response-time { color: var(--text-dim); font-size: 0.8rem; }
|
|
1099
|
+
.response-size { color: var(--text-muted); font-size: 0.8rem; }
|
|
1100
|
+
.response-headers { padding: 0.5rem 0.75rem; }
|
|
1101
|
+
.response-headers summary {
|
|
1102
|
+
font-size: 0.8rem;
|
|
1103
|
+
color: var(--text-dim);
|
|
1104
|
+
cursor: pointer;
|
|
1105
|
+
}
|
|
1106
|
+
.response-headers-pre {
|
|
1107
|
+
font-size: 0.8rem;
|
|
1108
|
+
margin-top: 0.25rem;
|
|
1109
|
+
white-space: pre-wrap;
|
|
1110
|
+
word-break: break-all;
|
|
1111
|
+
}
|
|
1112
|
+
.response-body {
|
|
1113
|
+
padding: 0.75rem;
|
|
1114
|
+
background: var(--bg-inset);
|
|
1115
|
+
}
|
|
1116
|
+
.response-body pre {
|
|
1117
|
+
font-family: var(--font-mono);
|
|
1118
|
+
font-size: 0.8rem;
|
|
1119
|
+
white-space: pre-wrap;
|
|
1120
|
+
word-break: break-all;
|
|
1121
|
+
max-height: 500px;
|
|
1122
|
+
overflow-y: auto;
|
|
1123
|
+
margin: 0;
|
|
1124
|
+
}
|
|
1125
|
+
.response-error-msg {
|
|
1126
|
+
padding: 0.75rem;
|
|
1127
|
+
color: var(--fail);
|
|
1128
|
+
font-size: 0.85rem;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/* JSON syntax highlighting */
|
|
1132
|
+
.json-key { color: var(--accent); }
|
|
1133
|
+
.json-string { color: var(--pass); }
|
|
1134
|
+
.json-number { color: var(--warn); }
|
|
1135
|
+
.json-boolean { color: var(--method-patch); }
|
|
1136
|
+
.json-null { color: var(--text-dim); font-style: italic; }
|
|
1137
|
+
|
|
850
1138
|
/* ── Responsive ── */
|
|
851
1139
|
@media (max-width: 768px) {
|
|
852
1140
|
.health-strip { grid-template-columns: 1fr; gap: 1rem; }
|
|
@@ -855,4 +1143,6 @@ h3 { font-size: 1.05rem; margin: 1rem 0 0.25rem; }
|
|
|
855
1143
|
.run-row { grid-template-columns: 48px 1fr 60px; }
|
|
856
1144
|
.run-time, .run-duration { display: none; }
|
|
857
1145
|
.runs-header { display: none; }
|
|
1146
|
+
.explorer-param-row { grid-template-columns: 1fr; }
|
|
1147
|
+
.explorer-endpoint-summary { display: none; }
|
|
858
1148
|
}
|