sql-harness 0.3.4__tar.gz → 0.3.5__tar.gz
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.
- {sql_harness-0.3.4 → sql_harness-0.3.5}/PKG-INFO +5 -1
- {sql_harness-0.3.4 → sql_harness-0.3.5}/SKILL.md +1 -1
- {sql_harness-0.3.4 → sql_harness-0.3.5}/docs/AGENTS.md +2 -1
- {sql_harness-0.3.4 → sql_harness-0.3.5}/docs/README.md +3 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/pyproject.toml +6 -1
- sql_harness-0.3.5/src/sql_harness/_web/app.css +191 -0
- sql_harness-0.3.5/src/sql_harness/_web/app.js +318 -0
- sql_harness-0.3.5/src/sql_harness/_web/index.html +42 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/cli.py +80 -0
- sql_harness-0.3.5/src/sql_harness/webapp.py +723 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_cli.py +1 -1
- sql_harness-0.3.5/tests/unit/test_webapp.py +547 -0
- sql_harness-0.3.5/uv.lock +554 -0
- sql_harness-0.3.4/uv.lock +0 -531
- {sql_harness-0.3.4 → sql_harness-0.3.5}/.env.example +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/.gitignore +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/AGENTS.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/agent-workspace/README.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/agent-workspace/agent_helpers.example.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/agent-workspace/agent_helpers.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/agent-workspace/zones/example_pg/scripts/sh_demo_crud.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/agent-workspace/zones/example_pg/skills/sh_demo-schema.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/agent-workspace/zones/meta/skills/pool.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/agent-workspace/zones/meta/skills/workspace.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/docs/connections-toml-example.toml +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/docs/headless-notes.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/docs/install.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/docs/maintainer.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/docs/standalone-install.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/aggregations-advanced.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/aggregations.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/encoding-and-charset.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/joins-advanced.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/joins.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/large-result-sets.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/migrations.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/mysql/json-columns.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/pooling.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/btree-indexes.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/indexes-and-explain.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/json-columns.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/migrations.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/plan-reading.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/slow-queries-joins.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/specialized-indexes.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/postgres/table-optimization.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/recursive-ctes-advanced.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/recursive-ctes.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/save-run-cycle.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/schema-introspection.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/ssh/auth-and-tunnels.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/ssh/docker-via-ssh.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/ssh/ssh.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/timeouts-and-cancellation.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/transactions.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/window-functions-advanced.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/window-functions.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/interaction-skills/zone-skill-auto-surface.md +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/sql-harness +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/__init__.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/agent_loader.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/analytics.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/config.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/drivers/__init__.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/drivers/mysql.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/drivers/postgres.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/drivers/readonly.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/drivers/redis.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/drivers/sqlite.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/drivers/ssh.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/helpers.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/manager.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/output.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/paths.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/src/sql_harness/run.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/__init__.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/conftest.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/integration/__init__.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/integration/test_mysql.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/integration/test_pg_optimization.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/integration/test_pgexercises_practice.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/integration/test_postgres.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/integration/test_ssh.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/__init__.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_agent_loader.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_analytics.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_config.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_helpers.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_manager.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_output.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_paths.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_readonly.py +0 -0
- {sql_harness-0.3.4 → sql_harness-0.3.5}/tests/unit/test_ssh.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sql-harness
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace.
|
|
5
5
|
Project-URL: Source, https://github.com/zhaoliuxue/much_bigpy/tree/master/lab/sql_harness
|
|
6
6
|
Project-URL: Issues, https://github.com/zhaoliuxue/much_bigpy/issues
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Topic :: Database
|
|
16
16
|
Classifier: Topic :: System :: Shells
|
|
17
17
|
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: markdown-it-py<4,>=3
|
|
18
19
|
Requires-Dist: paramiko<4,>=3.4
|
|
19
20
|
Requires-Dist: psycopg[binary]<4,>=3.2
|
|
20
21
|
Requires-Dist: pymysql<2,>=1.1
|
|
@@ -49,6 +50,9 @@ print(query("SELECT version()"))
|
|
|
49
50
|
print(list_tables())
|
|
50
51
|
print(describe("pg_class"))
|
|
51
52
|
PY
|
|
53
|
+
|
|
54
|
+
# 6. Browse + edit the docs (SKILL.md, interaction-skills/, your zone skills)
|
|
55
|
+
uv run sql-harness web
|
|
52
56
|
```
|
|
53
57
|
|
|
54
58
|
## Architecture (~1k lines across 8 core files)
|
|
@@ -42,7 +42,7 @@ PY
|
|
|
42
42
|
- **First call requires `use_workspace(name)`** — no implicit default.
|
|
43
43
|
- Set `BH_SQL_ZONE_SKILLS=1` once so `use_workspace()` auto-surfaces that zone's per-DSN skill docs (see `interaction-skills/zone-skill-auto-surface.md`).
|
|
44
44
|
- For task-specific helpers, drop them into `agent-workspace/zones/<conn>/helpers.py` and they merge into the heredoc namespace (per-zone, overriding the shared base).
|
|
45
|
-
- Other subcommands: `init` (scaffold connections.toml + install preset zone skills), `doctor` (SELECT 1 on every connection), `edit`/`remove`/`show`, `workspace {list,use,close,show}`, `save`/`run`/`scripts`, `skill` (bare: emit packaged SKILL.md; `install`/`list`/`show`), `paths`, `open`, `stats`, `version`, `ssh {exec,upload,download,run-script,info}`.
|
|
45
|
+
- Other subcommands: `init` (scaffold connections.toml + install preset zone skills), `doctor` (SELECT 1 on every connection), `edit`/`remove`/`show`, `workspace {list,use,close,show}`, `save`/`run`/`scripts`, `skill` (bare: emit packaged SKILL.md; `install`/`list`/`show`), `paths`, `open`, `web` (browse + edit the whole doc library in a local web UI — shipped docs, runtime zones, saved scripts), `stats`, `version`, `ssh {exec,upload,download,run-script,info}`.
|
|
46
46
|
- Zone-scoped subcommands (`save`, `run`, `scripts`, `skill list/show`, `ssh`) need `-c <conn>` or `$BH_SQL_ACTIVE_CONNECTION`.
|
|
47
47
|
- **Analytics (ON by default)**: every CLI call and every doc read (`apply_skill`, `skill`, `skill show`) is counted into `$BH_SQL_HOME/analytics.json`; view with `sql-harness stats`. Every executed command (CLI argv or heredoc body) plus failures is appended to `$BH_SQL_HOME/sql-harness.log` (NDJSON, one record per line). Disable both with `BH_SQL_ANALYTICS=0|off|false|no`. Extension point: `from sql_harness.analytics import on, CLI_INVOKED, DOC_READ, COMMAND_EXECUTED` to attach custom listeners (observer pattern).
|
|
48
48
|
|
|
@@ -18,8 +18,9 @@ Core code lives in `lab/sql_harness/src/sql_harness/`:
|
|
|
18
18
|
- `agent_loader.py` — loads `$BH_SQL_AGENT_WORKSPACE/agent_helpers.py`
|
|
19
19
|
- `analytics.py` — event bus (observer) + JSON stats + NDJSON execution log: CLI call counts, doc-read tracking, command/error history; `BH_SQL_ANALYTICS` (default ON), view with `sql-harness stats`
|
|
20
20
|
- `output.py` — terminal output policy: UTF-8 stream setup, long-output spill to `$BH_SQL_HOME/tmp/output/`, concise error rendering; `BH_SQL_TRACEBACK=1` forces raw tracebacks
|
|
21
|
+
- `webapp.py` — `sql-harness web`: serves the doc library (shipped docs + runtime zones + saved scripts) over loopback with an in-page editor. `resolve_doc()` is the write boundary — read its docstring before touching path handling
|
|
21
22
|
- `run.py` — heredoc entry; delegates to CLI otherwise
|
|
22
|
-
- `cli.py` — argparse subparser CLI (
|
|
23
|
+
- `cli.py` — argparse subparser CLI (19 subcommands)
|
|
23
24
|
|
|
24
25
|
`SKILL.md` tells agents how to use the harness.
|
|
25
26
|
`docs/install.md` tells agents how to install and configure it.
|
|
@@ -26,6 +26,9 @@ print(query("SELECT version()"))
|
|
|
26
26
|
print(list_tables())
|
|
27
27
|
print(describe("pg_class"))
|
|
28
28
|
PY
|
|
29
|
+
|
|
30
|
+
# 6. Browse + edit the docs (SKILL.md, interaction-skills/, your zone skills)
|
|
31
|
+
uv run sql-harness web
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
## Architecture (~1k lines across 8 core files)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sql-harness"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.5"
|
|
4
4
|
description = "Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace."
|
|
5
5
|
readme = "docs/README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
|
24
24
|
"pymysql>=1.1,<2",
|
|
25
25
|
"sqlalchemy>=2.0,<3",
|
|
26
26
|
"paramiko>=3.4,<4",
|
|
27
|
+
"markdown-it-py>=3,<4",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.scripts]
|
|
@@ -38,6 +39,10 @@ sql-harness = "sql_harness.run:main"
|
|
|
38
39
|
requires = ["hatchling"]
|
|
39
40
|
build-backend = "hatchling.build"
|
|
40
41
|
|
|
42
|
+
[[tool.uv.index]]
|
|
43
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
44
|
+
default = true
|
|
45
|
+
|
|
41
46
|
[tool.hatch.build.targets.wheel]
|
|
42
47
|
# Mirrors browser-harness's setuptools src-layout. The package source is
|
|
43
48
|
# under `src/sql_harness/`; the package name (top-level import) is `sql_harness`.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/* sql-harness doc browser — docs-site styling, no framework. */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #ffffff;
|
|
5
|
+
--bg-soft: #f6f7f9;
|
|
6
|
+
--bg-side: #fafbfc;
|
|
7
|
+
--fg: #1f2328;
|
|
8
|
+
--fg-soft: #59636e;
|
|
9
|
+
--fg-faint: #818b98;
|
|
10
|
+
--line: #d8dee4;
|
|
11
|
+
--line-soft: #eaeef2;
|
|
12
|
+
--accent: #0969da;
|
|
13
|
+
--accent-soft: #ddf4ff;
|
|
14
|
+
--danger: #cf222e;
|
|
15
|
+
--ok: #1a7f37;
|
|
16
|
+
--code-bg: #f6f8fa;
|
|
17
|
+
--radius: 6px;
|
|
18
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
19
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:root {
|
|
24
|
+
--bg: #0d1117; --bg-soft: #161b22; --bg-side: #0f141b;
|
|
25
|
+
--fg: #e6edf3; --fg-soft: #9198a1; --fg-faint: #6e7681;
|
|
26
|
+
--line: #30363d; --line-soft: #21262d;
|
|
27
|
+
--accent: #4493f8; --accent-soft: #121d2f;
|
|
28
|
+
--danger: #f85149; --ok: #3fb950; --code-bg: #161b22;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
* { box-sizing: border-box; }
|
|
33
|
+
|
|
34
|
+
html, body {
|
|
35
|
+
margin: 0; height: 100%;
|
|
36
|
+
background: var(--bg); color: var(--fg);
|
|
37
|
+
font-family: var(--sans); font-size: 15px; line-height: 1.65;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.app { display: flex; height: 100vh; }
|
|
41
|
+
|
|
42
|
+
/* --- sidebar --- */
|
|
43
|
+
.sidebar {
|
|
44
|
+
width: 310px; flex: 0 0 310px; height: 100vh;
|
|
45
|
+
display: flex; flex-direction: column;
|
|
46
|
+
background: var(--bg-side); border-right: 1px solid var(--line);
|
|
47
|
+
}
|
|
48
|
+
.brand {
|
|
49
|
+
display: flex; align-items: baseline; gap: 8px;
|
|
50
|
+
padding: 16px 18px 12px; font-weight: 600;
|
|
51
|
+
}
|
|
52
|
+
.brand-name { font-size: 15px; letter-spacing: -0.01em; }
|
|
53
|
+
.brand-ver { font-size: 12px; color: var(--fg-faint); font-weight: 400; }
|
|
54
|
+
|
|
55
|
+
.filter {
|
|
56
|
+
margin: 0 12px 10px; padding: 7px 10px;
|
|
57
|
+
border: 1px solid var(--line); border-radius: var(--radius);
|
|
58
|
+
background: var(--bg); color: var(--fg); font-size: 13px; font-family: inherit;
|
|
59
|
+
}
|
|
60
|
+
.filter:focus { outline: none; border-color: var(--accent); }
|
|
61
|
+
|
|
62
|
+
.nav { flex: 1; overflow-y: auto; padding: 0 8px 24px; }
|
|
63
|
+
.group-title {
|
|
64
|
+
padding: 14px 10px 6px; font-size: 11px; font-weight: 600;
|
|
65
|
+
text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-faint);
|
|
66
|
+
}
|
|
67
|
+
.root-title {
|
|
68
|
+
display: flex; align-items: center; gap: 6px;
|
|
69
|
+
padding: 5px 10px; font-size: 13px; font-weight: 600; color: var(--fg-soft);
|
|
70
|
+
}
|
|
71
|
+
.badge {
|
|
72
|
+
font-size: 10px; font-weight: 500; padding: 1px 5px; border-radius: 20px;
|
|
73
|
+
background: var(--accent-soft); color: var(--accent);
|
|
74
|
+
}
|
|
75
|
+
.count { margin-left: auto; font-size: 11px; color: var(--fg-faint); font-weight: 400; }
|
|
76
|
+
|
|
77
|
+
.nav details { margin: 0; }
|
|
78
|
+
.nav summary {
|
|
79
|
+
cursor: pointer; list-style: none; padding: 3px 10px 3px 22px;
|
|
80
|
+
font-size: 13px; color: var(--fg-soft); border-radius: var(--radius);
|
|
81
|
+
}
|
|
82
|
+
.nav summary::-webkit-details-marker { display: none; }
|
|
83
|
+
.nav summary::before { content: "▸ "; color: var(--fg-faint); font-size: 10px; }
|
|
84
|
+
.nav details[open] > summary::before { content: "▾ "; }
|
|
85
|
+
.nav summary:hover { background: var(--bg-soft); }
|
|
86
|
+
|
|
87
|
+
.nav a {
|
|
88
|
+
display: block; padding: 3px 10px 3px 34px;
|
|
89
|
+
font-size: 13px; color: var(--fg-soft); text-decoration: none;
|
|
90
|
+
border-radius: var(--radius); overflow: hidden;
|
|
91
|
+
text-overflow: ellipsis; white-space: nowrap;
|
|
92
|
+
}
|
|
93
|
+
.nav a:hover { background: var(--bg-soft); color: var(--fg); }
|
|
94
|
+
.nav a.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
|
|
95
|
+
.nav .empty { padding: 2px 10px 2px 34px; font-size: 12px; color: var(--fg-faint); }
|
|
96
|
+
|
|
97
|
+
.sidebar-foot {
|
|
98
|
+
padding: 10px 18px; border-top: 1px solid var(--line-soft);
|
|
99
|
+
font-size: 12px; color: var(--fg-faint); display: flex; align-items: center; gap: 6px;
|
|
100
|
+
}
|
|
101
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
|
102
|
+
.dot.ro { background: var(--fg-faint); }
|
|
103
|
+
|
|
104
|
+
/* --- content --- */
|
|
105
|
+
.content { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
106
|
+
|
|
107
|
+
.doc-head {
|
|
108
|
+
display: flex; align-items: center; gap: 12px;
|
|
109
|
+
padding: 12px 32px; border-bottom: 1px solid var(--line-soft);
|
|
110
|
+
position: sticky; top: 0; background: var(--bg); z-index: 5;
|
|
111
|
+
}
|
|
112
|
+
.crumb {
|
|
113
|
+
font-family: var(--mono); font-size: 12px; color: var(--fg-faint);
|
|
114
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
|
|
115
|
+
}
|
|
116
|
+
.actions { display: flex; gap: 8px; flex: 0 0 auto; }
|
|
117
|
+
.actions button {
|
|
118
|
+
padding: 5px 12px; font-size: 13px; font-family: inherit;
|
|
119
|
+
border: 1px solid var(--line); border-radius: var(--radius);
|
|
120
|
+
background: var(--bg-soft); color: var(--fg); cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
.actions button:hover { border-color: var(--fg-faint); }
|
|
123
|
+
.actions button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
124
|
+
.actions button.primary:hover { filter: brightness(1.08); }
|
|
125
|
+
.actions button:disabled { opacity: 0.5; cursor: default; }
|
|
126
|
+
|
|
127
|
+
.markdown, .editor { flex: 1; overflow-y: auto; }
|
|
128
|
+
.markdown { padding: 28px 32px 80px; max-width: 860px; }
|
|
129
|
+
.editor {
|
|
130
|
+
margin: 0; padding: 20px 24px; border: none; resize: none;
|
|
131
|
+
background: var(--code-bg); color: var(--fg);
|
|
132
|
+
font-family: var(--mono); font-size: 13px; line-height: 1.6;
|
|
133
|
+
}
|
|
134
|
+
.editor:focus { outline: none; }
|
|
135
|
+
|
|
136
|
+
/* --- metadata panel --- */
|
|
137
|
+
.meta {
|
|
138
|
+
margin: 20px 32px -8px; padding: 12px 16px;
|
|
139
|
+
border: 1px solid var(--line); border-left: 3px solid var(--accent);
|
|
140
|
+
border-radius: var(--radius); background: var(--bg-soft);
|
|
141
|
+
font-size: 13px; max-width: 860px;
|
|
142
|
+
}
|
|
143
|
+
.meta dl { margin: 0; display: grid; grid-template-columns: 90px 1fr; gap: 4px 12px; }
|
|
144
|
+
.meta dt { color: var(--fg-faint); font-family: var(--mono); font-size: 12px; }
|
|
145
|
+
.meta dd { margin: 0; color: var(--fg-soft); word-break: break-word; }
|
|
146
|
+
|
|
147
|
+
/* --- status line --- */
|
|
148
|
+
.status {
|
|
149
|
+
padding: 8px 32px; font-size: 13px; border-top: 1px solid var(--line-soft);
|
|
150
|
+
background: var(--bg-soft);
|
|
151
|
+
}
|
|
152
|
+
.status.err { color: var(--danger); }
|
|
153
|
+
.status.ok { color: var(--ok); }
|
|
154
|
+
|
|
155
|
+
/* --- markdown --- */
|
|
156
|
+
.markdown h1, .markdown h2, .markdown h3, .markdown h4 {
|
|
157
|
+
line-height: 1.3; margin: 1.6em 0 0.6em; font-weight: 600; letter-spacing: -0.01em;
|
|
158
|
+
}
|
|
159
|
+
.markdown h1 { font-size: 1.9em; margin-top: 0; padding-bottom: 0.3em; border-bottom: 1px solid var(--line-soft); }
|
|
160
|
+
.markdown h2 { font-size: 1.4em; padding-bottom: 0.25em; border-bottom: 1px solid var(--line-soft); }
|
|
161
|
+
.markdown h3 { font-size: 1.15em; }
|
|
162
|
+
.markdown p, .markdown ul, .markdown ol { margin: 0.85em 0; }
|
|
163
|
+
.markdown ul, .markdown ol { padding-left: 1.6em; }
|
|
164
|
+
.markdown li { margin: 0.25em 0; }
|
|
165
|
+
.markdown a { color: var(--accent); text-decoration: none; }
|
|
166
|
+
.markdown a:hover { text-decoration: underline; }
|
|
167
|
+
.markdown code {
|
|
168
|
+
font-family: var(--mono); font-size: 0.87em;
|
|
169
|
+
background: var(--code-bg); padding: 0.15em 0.4em; border-radius: 4px;
|
|
170
|
+
}
|
|
171
|
+
.markdown pre {
|
|
172
|
+
background: var(--code-bg); padding: 14px 16px; border-radius: var(--radius);
|
|
173
|
+
overflow-x: auto; border: 1px solid var(--line-soft);
|
|
174
|
+
}
|
|
175
|
+
.markdown pre code { background: none; padding: 0; font-size: 12.5px; line-height: 1.55; }
|
|
176
|
+
.markdown blockquote {
|
|
177
|
+
margin: 0.9em 0; padding: 0 1em; color: var(--fg-soft);
|
|
178
|
+
border-left: 3px solid var(--line);
|
|
179
|
+
}
|
|
180
|
+
.markdown table {
|
|
181
|
+
border-collapse: collapse; margin: 1em 0; display: block; overflow-x: auto; max-width: 100%;
|
|
182
|
+
}
|
|
183
|
+
.markdown th, .markdown td { border: 1px solid var(--line); padding: 6px 12px; text-align: left; }
|
|
184
|
+
.markdown th { background: var(--bg-soft); font-weight: 600; }
|
|
185
|
+
.markdown hr { border: none; border-top: 1px solid var(--line-soft); margin: 2em 0; }
|
|
186
|
+
|
|
187
|
+
@media (max-width: 820px) {
|
|
188
|
+
.app { flex-direction: column; }
|
|
189
|
+
.sidebar { width: 100%; flex: 0 0 45vh; height: 45vh; border-right: none; border-bottom: 1px solid var(--line); }
|
|
190
|
+
.markdown, .doc-head, .status, .meta { padding-left: 16px; padding-right: 16px; margin-left: 0; margin-right: 0; }
|
|
191
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/* sql-harness doc browser. Vanilla JS, hash routing, no build step. */
|
|
2
|
+
|
|
3
|
+
const $ = (id) => document.getElementById(id);
|
|
4
|
+
|
|
5
|
+
const GROUP_LABELS = { source: "Shipped docs", runtime: "Runtime workspace" };
|
|
6
|
+
|
|
7
|
+
const state = {
|
|
8
|
+
roots: [],
|
|
9
|
+
current: null, // { root, path, sha256, raw, editable }
|
|
10
|
+
editing: false,
|
|
11
|
+
dirty: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/* --- helpers ------------------------------------------------------------- */
|
|
15
|
+
|
|
16
|
+
function setStatus(msg, kind) {
|
|
17
|
+
const el = $("status");
|
|
18
|
+
if (!msg) { el.hidden = true; el.textContent = ""; return; }
|
|
19
|
+
el.hidden = false;
|
|
20
|
+
el.textContent = msg;
|
|
21
|
+
el.className = "status" + (kind ? " " + kind : "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function api(path, options) {
|
|
25
|
+
const res = await fetch(path, options);
|
|
26
|
+
let body = null;
|
|
27
|
+
try { body = await res.json(); } catch (_) { /* non-JSON error page */ }
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const err = new Error((body && body.error) || res.status + " " + res.statusText);
|
|
30
|
+
err.status = res.status;
|
|
31
|
+
err.body = body;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return body;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* --- sidebar ------------------------------------------------------------- */
|
|
38
|
+
|
|
39
|
+
function renderNav() {
|
|
40
|
+
const filter = $("filter").value.trim().toLowerCase();
|
|
41
|
+
const nav = $("nav");
|
|
42
|
+
nav.textContent = "";
|
|
43
|
+
|
|
44
|
+
const groups = {};
|
|
45
|
+
for (const root of state.roots) (groups[root.kind] ||= []).push(root);
|
|
46
|
+
|
|
47
|
+
for (const [kind, roots] of Object.entries(groups)) {
|
|
48
|
+
const title = document.createElement("div");
|
|
49
|
+
title.className = "group-title";
|
|
50
|
+
title.textContent = GROUP_LABELS[kind] || kind;
|
|
51
|
+
nav.appendChild(title);
|
|
52
|
+
|
|
53
|
+
for (const root of roots) {
|
|
54
|
+
const head = document.createElement("div");
|
|
55
|
+
head.className = "root-title";
|
|
56
|
+
const name = document.createElement("span");
|
|
57
|
+
name.textContent = root.label;
|
|
58
|
+
head.appendChild(name);
|
|
59
|
+
if (root.connection) {
|
|
60
|
+
const badge = document.createElement("span");
|
|
61
|
+
badge.className = "badge";
|
|
62
|
+
badge.textContent = root.connection;
|
|
63
|
+
head.appendChild(badge);
|
|
64
|
+
}
|
|
65
|
+
if (!root.editable) {
|
|
66
|
+
const ro = document.createElement("span");
|
|
67
|
+
ro.className = "badge";
|
|
68
|
+
ro.textContent = "只读";
|
|
69
|
+
head.appendChild(ro);
|
|
70
|
+
}
|
|
71
|
+
const count = document.createElement("span");
|
|
72
|
+
count.className = "count";
|
|
73
|
+
count.textContent = root.file_count;
|
|
74
|
+
head.appendChild(count);
|
|
75
|
+
nav.appendChild(head);
|
|
76
|
+
|
|
77
|
+
nav.appendChild(renderNodes(root, root.tree, ""));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!nav.children.length) {
|
|
81
|
+
const empty = document.createElement("div");
|
|
82
|
+
empty.className = "empty";
|
|
83
|
+
empty.textContent = "没有文档";
|
|
84
|
+
nav.appendChild(empty);
|
|
85
|
+
}
|
|
86
|
+
void filter;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderNodes(root, nodes, prefix) {
|
|
90
|
+
const frag = document.createDocumentFragment();
|
|
91
|
+
nodes.sort((a, b) => (a.type === "file") - (b.type === "file") || a.name.localeCompare(b.name));
|
|
92
|
+
|
|
93
|
+
for (const node of nodes) {
|
|
94
|
+
const path = prefix ? prefix + "/" + node.name : node.name;
|
|
95
|
+
if (node.type === "file") {
|
|
96
|
+
const a = document.createElement("a");
|
|
97
|
+
a.href = "#/" + encodeURIComponent(root.id) + "/" + encodeURI(path.split("/").map(encodeURIComponent).join("/"));
|
|
98
|
+
a.textContent = node.name;
|
|
99
|
+
a.dataset.key = root.id + ":" + path;
|
|
100
|
+
if (node.kind === "other") a.title = "不支持预览的文件类型";
|
|
101
|
+
frag.appendChild(a);
|
|
102
|
+
} else if (node.type === "dir") {
|
|
103
|
+
const det = document.createElement("details");
|
|
104
|
+
const sum = document.createElement("summary");
|
|
105
|
+
sum.textContent = node.name;
|
|
106
|
+
det.appendChild(sum);
|
|
107
|
+
det.appendChild(renderNodes(root, node.children || [], path));
|
|
108
|
+
frag.appendChild(det);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return frag;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function markActive() {
|
|
115
|
+
const key = state.current ? state.current.root + ":" + state.current.path : "";
|
|
116
|
+
document.querySelectorAll("#nav a").forEach((a) => {
|
|
117
|
+
const on = a.dataset.key === key;
|
|
118
|
+
a.classList.toggle("active", on);
|
|
119
|
+
if (on) {
|
|
120
|
+
a.closest("details")?.setAttribute("open", "");
|
|
121
|
+
a.scrollIntoView({ block: "nearest" });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* --- document ------------------------------------------------------------ */
|
|
127
|
+
|
|
128
|
+
function setEditing(on) {
|
|
129
|
+
state.editing = on;
|
|
130
|
+
$("editor").hidden = !on;
|
|
131
|
+
$("doc").hidden = on;
|
|
132
|
+
$("meta").hidden = on || $("meta").dataset.empty === "1";
|
|
133
|
+
$("btn-edit").hidden = on || !(state.current && state.current.editable);
|
|
134
|
+
$("btn-save").hidden = !on;
|
|
135
|
+
$("btn-cancel").hidden = !on;
|
|
136
|
+
$("btn-save").disabled = !state.dirty;
|
|
137
|
+
if (on) $("editor").focus();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderMeta(meta) {
|
|
141
|
+
const el = $("meta");
|
|
142
|
+
const keys = Object.keys(meta || {}).filter((k) => k !== "_raw");
|
|
143
|
+
el.textContent = "";
|
|
144
|
+
if (!keys.length) {
|
|
145
|
+
el.hidden = true;
|
|
146
|
+
el.dataset.empty = "1";
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const dl = document.createElement("dl");
|
|
150
|
+
for (const key of keys) {
|
|
151
|
+
const dt = document.createElement("dt");
|
|
152
|
+
dt.textContent = key;
|
|
153
|
+
const dd = document.createElement("dd");
|
|
154
|
+
dd.textContent = meta[key];
|
|
155
|
+
dl.append(dt, dd);
|
|
156
|
+
}
|
|
157
|
+
el.appendChild(dl);
|
|
158
|
+
el.dataset.empty = "0";
|
|
159
|
+
el.hidden = state.editing;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function openDoc(rootId, path) {
|
|
163
|
+
if (state.dirty && !confirm("有未保存的修改,确定要离开吗?")) return;
|
|
164
|
+
setStatus("载入中…");
|
|
165
|
+
try {
|
|
166
|
+
const doc = await api("/api/doc?root=" + encodeURIComponent(rootId) + "&path=" + encodeURIComponent(path));
|
|
167
|
+
state.current = doc;
|
|
168
|
+
state.dirty = false;
|
|
169
|
+
|
|
170
|
+
$("crumb").textContent = doc.root + " / " + doc.path + " · " + doc.size + " B";
|
|
171
|
+
$("editor").value = doc.raw;
|
|
172
|
+
|
|
173
|
+
if (doc.html === null) {
|
|
174
|
+
$("doc").innerHTML = "";
|
|
175
|
+
const p = document.createElement("p");
|
|
176
|
+
p.textContent = "该文件类型不支持预览(" + doc.kind + ")。";
|
|
177
|
+
$("doc").appendChild(p);
|
|
178
|
+
} else {
|
|
179
|
+
$("doc").innerHTML = doc.html;
|
|
180
|
+
}
|
|
181
|
+
renderMeta(doc.frontmatter);
|
|
182
|
+
setEditing(false);
|
|
183
|
+
markActive();
|
|
184
|
+
setStatus("");
|
|
185
|
+
} catch (err) {
|
|
186
|
+
setStatus("打开失败:" + err.message, "err");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function save() {
|
|
191
|
+
if (!state.current) return;
|
|
192
|
+
const content = $("editor").value;
|
|
193
|
+
$("btn-save").disabled = true;
|
|
194
|
+
setStatus("保存中…");
|
|
195
|
+
try {
|
|
196
|
+
const res = await api("/api/doc", {
|
|
197
|
+
method: "PUT",
|
|
198
|
+
headers: { "Content-Type": "application/json" },
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
root: state.current.root,
|
|
201
|
+
path: state.current.path,
|
|
202
|
+
content: content,
|
|
203
|
+
base_sha256: state.current.sha256,
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
state.dirty = false;
|
|
207
|
+
setStatus("已保存 " + res.size + " B", "ok");
|
|
208
|
+
await refreshTree();
|
|
209
|
+
await openDoc(state.current.root, state.current.path);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
$("btn-save").disabled = false;
|
|
212
|
+
if (err.status === 409) {
|
|
213
|
+
const overwrite = confirm(
|
|
214
|
+
"这个文件在磁盘上已经被改过了(可能是 agent 写的)。\n\n" +
|
|
215
|
+
"确定 = 用我编辑器里的内容覆盖它\n取消 = 丢弃我的修改,重新载入磁盘版本"
|
|
216
|
+
);
|
|
217
|
+
if (overwrite) {
|
|
218
|
+
state.current.sha256 = err.body.sha256;
|
|
219
|
+
return save();
|
|
220
|
+
}
|
|
221
|
+
await refreshTree();
|
|
222
|
+
await openDoc(state.current.root, state.current.path);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
setStatus("保存失败:" + err.message, "err");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* --- boot ---------------------------------------------------------------- */
|
|
230
|
+
|
|
231
|
+
async function refreshTree() {
|
|
232
|
+
const data = await api("/api/tree");
|
|
233
|
+
state.roots = data.roots;
|
|
234
|
+
$("version").textContent = "v" + data.version;
|
|
235
|
+
renderNav();
|
|
236
|
+
markActive();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function applyHash() {
|
|
240
|
+
const hash = decodeURIComponent(location.hash.replace(/^#\/?/, ""));
|
|
241
|
+
if (!hash) return;
|
|
242
|
+
const idx = hash.indexOf("/");
|
|
243
|
+
if (idx < 0) return;
|
|
244
|
+
const rootId = hash.slice(0, idx);
|
|
245
|
+
const path = hash.slice(idx + 1).split("/").map(decodeURIComponent).join("/");
|
|
246
|
+
openDoc(rootId, path);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function boot() {
|
|
250
|
+
$("btn-refresh").addEventListener("click", async () => {
|
|
251
|
+
await refreshTree();
|
|
252
|
+
setStatus("文件树已刷新", "ok");
|
|
253
|
+
});
|
|
254
|
+
$("btn-edit").addEventListener("click", () => setEditing(true));
|
|
255
|
+
$("btn-cancel").addEventListener("click", () => {
|
|
256
|
+
if (state.dirty && !confirm("放弃未保存的修改?")) return;
|
|
257
|
+
state.dirty = false;
|
|
258
|
+
$("editor").value = state.current ? state.current.raw : "";
|
|
259
|
+
setEditing(false);
|
|
260
|
+
setStatus("");
|
|
261
|
+
});
|
|
262
|
+
$("btn-save").addEventListener("click", save);
|
|
263
|
+
$("editor").addEventListener("input", () => {
|
|
264
|
+
state.dirty = $("editor").value !== (state.current ? state.current.raw : "");
|
|
265
|
+
$("btn-save").disabled = !state.dirty;
|
|
266
|
+
});
|
|
267
|
+
$("filter").addEventListener("input", () => {
|
|
268
|
+
const q = $("filter").value.trim().toLowerCase();
|
|
269
|
+
document.querySelectorAll("#nav a").forEach((a) => {
|
|
270
|
+
a.style.display = !q || a.textContent.toLowerCase().includes(q) ? "" : "none";
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
window.addEventListener("hashchange", applyHash);
|
|
274
|
+
window.addEventListener("beforeunload", (e) => {
|
|
275
|
+
if (state.dirty) { e.preventDefault(); e.returnValue = ""; }
|
|
276
|
+
});
|
|
277
|
+
document.addEventListener("keydown", (e) => {
|
|
278
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
if (state.editing && state.dirty) save();
|
|
281
|
+
} else if (e.key === "Escape" && state.editing) {
|
|
282
|
+
$("btn-cancel").click();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
api("/api/health").then((h) => {
|
|
287
|
+
$("ro-label").textContent = h.read_only ? "只读模式" : "可编辑";
|
|
288
|
+
if (h.read_only) $("ro-dot").classList.add("ro");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
refreshTree()
|
|
292
|
+
.then(() => {
|
|
293
|
+
if (location.hash) applyHash();
|
|
294
|
+
else if (state.roots.length) {
|
|
295
|
+
// Open something real rather than an empty pane.
|
|
296
|
+
const first = state.roots.find((r) => r.file_count > 0);
|
|
297
|
+
const pick = firstFile(first);
|
|
298
|
+
if (pick && first) location.hash = "#/" + encodeURIComponent(first.id) + "/" + pick;
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
.catch((err) => setStatus("初始化失败:" + err.message, "err"));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function firstFile(root) {
|
|
305
|
+
if (!root) return null;
|
|
306
|
+
const walk = (nodes, prefix) => {
|
|
307
|
+
for (const n of nodes) {
|
|
308
|
+
const path = prefix ? prefix + "/" + n.name : n.name;
|
|
309
|
+
if (n.type === "file") return path;
|
|
310
|
+
const deeper = n.children && walk(n.children, path);
|
|
311
|
+
if (deeper) return deeper;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
};
|
|
315
|
+
return walk(root.tree || [], "");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
document.addEventListener("DOMContentLoaded", boot);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>sql-harness docs</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/app.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="app">
|
|
11
|
+
<aside class="sidebar">
|
|
12
|
+
<div class="brand">
|
|
13
|
+
<span class="brand-name">sql-harness</span>
|
|
14
|
+
<span class="brand-ver" id="version"></span>
|
|
15
|
+
</div>
|
|
16
|
+
<input id="filter" class="filter" type="search" placeholder="过滤文档…" autocomplete="off">
|
|
17
|
+
<nav id="nav" class="nav"></nav>
|
|
18
|
+
<div class="sidebar-foot">
|
|
19
|
+
<span class="dot" id="ro-dot"></span><span id="ro-label">可编辑</span>
|
|
20
|
+
</div>
|
|
21
|
+
</aside>
|
|
22
|
+
|
|
23
|
+
<main class="content">
|
|
24
|
+
<header class="doc-head">
|
|
25
|
+
<div class="crumb" id="crumb">选择左侧的文档</div>
|
|
26
|
+
<div class="actions">
|
|
27
|
+
<button id="btn-refresh" type="button" title="重新读取文件树">刷新</button>
|
|
28
|
+
<button id="btn-edit" type="button" hidden>编辑</button>
|
|
29
|
+
<button id="btn-save" type="button" class="primary" hidden>保存</button>
|
|
30
|
+
<button id="btn-cancel" type="button" hidden>取消</button>
|
|
31
|
+
</div>
|
|
32
|
+
</header>
|
|
33
|
+
|
|
34
|
+
<div id="meta" class="meta" hidden></div>
|
|
35
|
+
<article id="doc" class="markdown"></article>
|
|
36
|
+
<textarea id="editor" class="editor" hidden spellcheck="false"></textarea>
|
|
37
|
+
<div id="status" class="status" hidden></div>
|
|
38
|
+
</main>
|
|
39
|
+
</div>
|
|
40
|
+
<script src="/static/app.js"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|