@drafthq/draft 3.2.1 → 3.3.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +28 -0
- package/README.md +20 -2
- package/cli/src/hosts/cursor.js +35 -5
- package/cli/src/installer.js +12 -0
- package/cli/src/lib/cursor-registry.js +122 -0
- package/cli/src/lib/plugin-manifest.js +20 -0
- package/core/methodology.md +1 -1
- package/core/templates/okf/ai-context-index.md +48 -0
- package/core/templates/okf/concept.md +54 -0
- package/core/templates/okf/index.md +40 -0
- package/core/templates/okf/section-index.md +25 -0
- package/integrations/agents/AGENTS.md +452 -2
- package/integrations/copilot/.github/copilot-instructions.md +452 -2
- package/package.json +3 -2
- package/scripts/lib.sh +9 -0
- package/scripts/tools/graph-preflight.sh +259 -0
- package/scripts/tools/okf-render-views.sh +373 -0
- package/scripts/tools/okf-validate.sh +204 -0
- package/skills/init/SKILL.md +24 -1
- package/skills/init/references/okf-emitter.md +223 -0
- package/integrations/copilot/.github/copilot-instructions.md.7iDz8X +0 -91
- package/integrations/copilot/.github/copilot-instructions.md.DoBdtd +0 -91
- package/integrations/copilot/.github/copilot-instructions.md.McGoBW +0 -122
- package/integrations/copilot/.github/copilot-instructions.md.VsPyLB +0 -91
- package/integrations/copilot/.github/copilot-instructions.md.XAVr7D +0 -91
- package/integrations/copilot/.github/copilot-instructions.md.YoFVFa +0 -91
- package/integrations/copilot/.github/copilot-instructions.md.a9DeW0 +0 -91
- package/integrations/copilot/.github/copilot-instructions.md.oxQs3B +0 -91
- package/integrations/copilot/.github/copilot-instructions.md.ww33Ly +0 -91
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# graph-preflight.sh — read-only go/no-go check before indexing a repo with the
|
|
3
|
+
# Draft knowledge-graph engine (codebase-memory-mcp).
|
|
4
|
+
#
|
|
5
|
+
# Indexes NOTHING. Walks git metadata + engine status only. Safe to run anywhere.
|
|
6
|
+
# Companion preflight for `scripts/tools/graph-init.sh` / `/draft:init --graph-only`.
|
|
7
|
+
#
|
|
8
|
+
# Usage: scripts/tools/graph-preflight.sh [--json] [REPO_PATH] (default repo: cwd)
|
|
9
|
+
# --json emit a machine-readable report on stdout (no human output)
|
|
10
|
+
# Exit: 0 = GO / GO-with-caution, 1 = NO-GO (blocking), 2 = bad invocation.
|
|
11
|
+
#
|
|
12
|
+
# Deliberately uses guard idioms (`|| true`, `|| echo 0`) rather than aborting:
|
|
13
|
+
# the report accumulates blockers/warnings and prints a verdict even under -e.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
SELF_REPO="$(cd "$TOOLS_DIR/../.." && pwd)"
|
|
18
|
+
# shellcheck source=_lib.sh
|
|
19
|
+
source "$TOOLS_DIR/_lib.sh"
|
|
20
|
+
|
|
21
|
+
usage() {
|
|
22
|
+
cat <<'EOF'
|
|
23
|
+
graph-preflight.sh — read-only go/no-go check before knowledge-graph indexing.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
scripts/tools/graph-preflight.sh [--json] [REPO_PATH] (default repo: cwd)
|
|
27
|
+
|
|
28
|
+
Flags:
|
|
29
|
+
--json Emit a machine-readable report on stdout (no human output).
|
|
30
|
+
--help Show this help.
|
|
31
|
+
|
|
32
|
+
Indexes nothing — walks git metadata + engine status only.
|
|
33
|
+
Exit codes: 0 GO / GO-with-caution, 1 NO-GO (blocking), 2 bad invocation.
|
|
34
|
+
EOF
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# --- args ---
|
|
38
|
+
JSON_MODE=0
|
|
39
|
+
REPO=""
|
|
40
|
+
while [[ $# -gt 0 ]]; do
|
|
41
|
+
case "$1" in
|
|
42
|
+
--json) JSON_MODE=1; shift;;
|
|
43
|
+
-h|--help) usage; exit 0;;
|
|
44
|
+
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;;
|
|
45
|
+
*) if [[ -z "$REPO" ]]; then REPO="$1"; else echo "Unexpected arg: $1" >&2; exit 2; fi; shift;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
REPO="${REPO:-.}"
|
|
49
|
+
[[ -d "$REPO" ]] || { echo "ERROR: '$REPO' is not a directory" >&2; exit 2; }
|
|
50
|
+
REPO_ABS="$(cd "$REPO" && pwd)"
|
|
51
|
+
|
|
52
|
+
# --- formatting (color only on a tty, and never in --json) ---
|
|
53
|
+
if [[ -t 1 && "$JSON_MODE" -eq 0 ]]; then B=$'\e[1m'; G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; D=$'\e[0m'; else B=""; G=""; Y=""; R=""; D=""; fi
|
|
54
|
+
hr() { [[ "$JSON_MODE" -eq 0 ]] && printf '%s\n' "------------------------------------------------------------"; return 0; }
|
|
55
|
+
sec() { [[ "$JSON_MODE" -eq 0 ]] && { echo; printf '%s== %s ==%s\n' "$B" "$1" "$D"; }; return 0; }
|
|
56
|
+
ok() { [[ "$JSON_MODE" -eq 0 ]] && printf ' %s[ OK ]%s %s\n' "$G" "$D" "$1"; return 0; }
|
|
57
|
+
info() { [[ "$JSON_MODE" -eq 0 ]] && printf ' %s\n' "$1"; return 0; }
|
|
58
|
+
warn() { WARNINGS=$((WARNINGS+1)); WARN_J="${WARN_J:+$WARN_J,}\"$(json_escape "$1")\""; [[ "$JSON_MODE" -eq 0 ]] && printf ' %s[WARN]%s %s\n' "$Y" "$D" "$1"; return 0; }
|
|
59
|
+
fail() { BLOCKERS=$((BLOCKERS+1)); FAIL_J="${FAIL_J:+$FAIL_J,}\"$(json_escape "$1")\""; [[ "$JSON_MODE" -eq 0 ]] && printf ' %s[FAIL]%s %s\n' "$R" "$D" "$1"; return 0; }
|
|
60
|
+
|
|
61
|
+
WARNINGS=0; BLOCKERS=0; WARN_J=""; FAIL_J=""
|
|
62
|
+
|
|
63
|
+
# --- helpers ---
|
|
64
|
+
count_files() { { git -C "$REPO_ABS" ls-files -- "$@" 2>/dev/null || true; } | wc -l | tr -d ' '; }
|
|
65
|
+
count_loc() {
|
|
66
|
+
[[ -n "$(git -C "$REPO_ABS" ls-files -- "$@" 2>/dev/null | head -1)" ]] || { echo 0; return; }
|
|
67
|
+
# cat must run from the repo root so tracked paths resolve.
|
|
68
|
+
( cd "$REPO_ABS" && git ls-files -z -- "$@" 2>/dev/null | xargs -0 cat 2>/dev/null ) | wc -l | tr -d ' ' || true
|
|
69
|
+
}
|
|
70
|
+
human() { awk -v n="$1" 'BEGIN{ v=n; split("K M B",u); if(v<1000){printf "%d",v;exit}
|
|
71
|
+
for(i=1;i<=3;i++){v/=1000; if(v<1000){printf "%.1f%s",v,u[i];exit}}}'; }
|
|
72
|
+
|
|
73
|
+
# --- collected fields (defaults so --json is always well-formed) ---
|
|
74
|
+
IS_GIT=false; AT_ROOT=false; GIT_TOP=""; COMMIT="none"
|
|
75
|
+
TRACKED=0; ALLDISK=0; TOTAL_LOC=0; CCGO_LOC=0; LANG_J=""
|
|
76
|
+
VEND_J=""; ENGINE=""; VER=""; LIMIT=""; ENGINE_FOUND=false
|
|
77
|
+
RAM_GB=""; FREE_GB=""
|
|
78
|
+
|
|
79
|
+
hr
|
|
80
|
+
[[ "$JSON_MODE" -eq 0 ]] && printf '%sDraft graph pre-flight%s — %s\n' "$B" "$D" "$REPO_ABS" || true
|
|
81
|
+
hr
|
|
82
|
+
|
|
83
|
+
# ============================================================
|
|
84
|
+
sec "1. Git boundary"
|
|
85
|
+
# ============================================================
|
|
86
|
+
GIT_TOP="$(git -C "$REPO_ABS" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
87
|
+
if [[ -z "$GIT_TOP" ]]; then
|
|
88
|
+
fail "Not inside a git repo. The engine would raw-walk the filesystem (no .gitignore filter)."
|
|
89
|
+
info "Point this at a real git repo root, never a parent container dir."
|
|
90
|
+
GIT_OK=0
|
|
91
|
+
else
|
|
92
|
+
GIT_OK=1; IS_GIT=true
|
|
93
|
+
if [[ "$GIT_TOP" != "$REPO_ABS" ]]; then
|
|
94
|
+
warn "Not at the git root. Git top is: $GIT_TOP"
|
|
95
|
+
info "Run /draft:init --graph-only at the git root for whole-repo coverage."
|
|
96
|
+
else
|
|
97
|
+
AT_ROOT=true
|
|
98
|
+
ok "Git root: $GIT_TOP"
|
|
99
|
+
fi
|
|
100
|
+
COMMIT="$(git -C "$REPO_ABS" rev-parse --short HEAD 2>/dev/null || echo none)"
|
|
101
|
+
info "HEAD: $COMMIT"
|
|
102
|
+
[[ -f "$GIT_TOP/.gitmodules" ]] && warn "Submodules present — engine indexes the superproject's tracked tree; submodule contents may need separate indexing." || true
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# ============================================================
|
|
106
|
+
sec "2. Index scope (git-tracked = what actually gets indexed)"
|
|
107
|
+
# ============================================================
|
|
108
|
+
if [[ "$GIT_OK" -eq 1 ]]; then
|
|
109
|
+
TRACKED="$(count_files)"
|
|
110
|
+
ALLDISK="$({ find "$REPO_ABS" -type d -name .git -prune -o -type f -print 2>/dev/null || true; } | wc -l | tr -d ' ')"
|
|
111
|
+
ok "Git-tracked files: $TRACKED (on disk: $ALLDISK — the difference is gitignored and SKIPPED)"
|
|
112
|
+
|
|
113
|
+
[[ "$JSON_MODE" -eq 0 ]] && { echo; printf ' %-14s %10s %12s\n' "language" "files" "lines"; printf ' %-14s %10s %12s\n' "--------" "-----" "-----"; } || true
|
|
114
|
+
declare -A GLOBS=(
|
|
115
|
+
[C/C++]='*.c *.cc *.cpp *.cxx *.h *.hpp *.hh *.hxx'
|
|
116
|
+
[Go]='*.go'
|
|
117
|
+
[Python]='*.py'
|
|
118
|
+
[TS/JS]='*.ts *.tsx *.js *.jsx *.mjs'
|
|
119
|
+
[Rust]='*.rs'
|
|
120
|
+
[Java]='*.java'
|
|
121
|
+
)
|
|
122
|
+
for lang in "C/C++" Go Python TS/JS Rust Java; do
|
|
123
|
+
# shellcheck disable=SC2086
|
|
124
|
+
read -ra g <<< "${GLOBS[$lang]}"
|
|
125
|
+
f="$(count_files "${g[@]}")"
|
|
126
|
+
[[ "$f" -eq 0 ]] && continue
|
|
127
|
+
l="$(count_loc "${g[@]}")"
|
|
128
|
+
[[ "$JSON_MODE" -eq 0 ]] && printf ' %-14s %10s %12s\n' "$lang" "$f" "$l" || true
|
|
129
|
+
LANG_J="${LANG_J:+$LANG_J,}{\"lang\":\"$(json_escape "$lang")\",\"files\":$f,\"lines\":$l}"
|
|
130
|
+
TOTAL_LOC=$((TOTAL_LOC + l))
|
|
131
|
+
[[ "$lang" == "C/C++" || "$lang" == "Go" ]] && CCGO_LOC=$((CCGO_LOC + l)) || true
|
|
132
|
+
done
|
|
133
|
+
[[ "$JSON_MODE" -eq 0 ]] && printf ' %-14s %10s %12s\n' "TOTAL" "$TRACKED" "$TOTAL_LOC" || true
|
|
134
|
+
info "Source LOC total: $(human "$TOTAL_LOC") | C/C++/Go: $(human "$CCGO_LOC")"
|
|
135
|
+
else
|
|
136
|
+
warn "Skipped — no git repo."
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# ============================================================
|
|
140
|
+
sec "3. Committed vendor/generated trees (these WILL be indexed)"
|
|
141
|
+
# ============================================================
|
|
142
|
+
if [[ "$GIT_OK" -eq 1 ]]; then
|
|
143
|
+
# Match vendor/generated *directories* (token followed by /) and protobuf-generated
|
|
144
|
+
# file suffixes — not filenames that merely contain "gen"/"generate".
|
|
145
|
+
VEND="$(git -C "$REPO_ABS" ls-files 2>/dev/null \
|
|
146
|
+
| grep -iE '(^|/)(third_party|thirdparty|vendor|external|deps|generated)/|\.pb\.(cc|h|go)$|_pb2\.py$' \
|
|
147
|
+
| sed -E 's#(^.*/(third_party|thirdparty|vendor|external|deps|generated))/.*#\1/#' \
|
|
148
|
+
| sort -u | head -40 || true)"
|
|
149
|
+
if [[ -n "$VEND" ]]; then
|
|
150
|
+
warn "Committed vendor/generated paths found — gitignore to exclude, or accept index inflation:"
|
|
151
|
+
while IFS= read -r p; do
|
|
152
|
+
[[ -z "$p" ]] && continue
|
|
153
|
+
info "$p"
|
|
154
|
+
VEND_J="${VEND_J:+$VEND_J,}\"$(json_escape "$p")\""
|
|
155
|
+
done <<< "$VEND"
|
|
156
|
+
else
|
|
157
|
+
ok "No obvious committed vendor/generated trees."
|
|
158
|
+
fi
|
|
159
|
+
else
|
|
160
|
+
warn "Skipped — no git repo."
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# ============================================================
|
|
164
|
+
sec "4. Engine availability"
|
|
165
|
+
# ============================================================
|
|
166
|
+
if find_memory_bin "$REPO_ABS" "$SELF_REPO"; then
|
|
167
|
+
ENGINE="$MEMORY_BIN"
|
|
168
|
+
ENGINE_FOUND=true
|
|
169
|
+
VER="$("$ENGINE" --version 2>/dev/null | head -1 || echo '?')"
|
|
170
|
+
ok "Engine: $ENGINE ($VER)"
|
|
171
|
+
LIMIT="$("$ENGINE" config list 2>/dev/null | awk '/auto_index_limit/{print $3}' || true)"
|
|
172
|
+
[[ -n "$LIMIT" ]] && info "auto_index_limit: $LIMIT (governs AUTO-index only; explicit index_repository should bypass)" || true
|
|
173
|
+
if [[ "$GIT_OK" -eq 1 && -n "${LIMIT:-}" && "$TRACKED" -gt "$LIMIT" ]]; then
|
|
174
|
+
warn "Tracked files ($TRACKED) > auto_index_limit ($LIMIT) — confirm the explicit index isn't truncated near $LIMIT."
|
|
175
|
+
fi
|
|
176
|
+
else
|
|
177
|
+
ENGINE=""
|
|
178
|
+
fail "Engine 'codebase-memory-mcp' not found (checked \$DRAFT_MEMORY_BIN, PATH, ~/.cache/draft/bin/, vendored bin/<arch>/)."
|
|
179
|
+
info "Install: scripts/fetch-memory-engine.sh (or put the binary on PATH)"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# ============================================================
|
|
183
|
+
sec "5. Machine headroom"
|
|
184
|
+
# ============================================================
|
|
185
|
+
if [[ -r /proc/meminfo ]]; then
|
|
186
|
+
RAM_GB="$(awk '/MemTotal/{printf "%d", $2/1024/1024}' /proc/meminfo)"
|
|
187
|
+
ok "Total RAM: ${RAM_GB} GB (engine self-budgets ~half)"
|
|
188
|
+
elif command -v sysctl >/dev/null 2>&1; then
|
|
189
|
+
RAM_GB="$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1024 / 1024 / 1024 ))"
|
|
190
|
+
ok "Total RAM: ${RAM_GB} GB"
|
|
191
|
+
else
|
|
192
|
+
warn "Could not read total RAM."
|
|
193
|
+
fi
|
|
194
|
+
CACHE_DIR="$HOME/.cache"; mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
|
195
|
+
FREE_K="$(df -Pk "$CACHE_DIR" 2>/dev/null | awk 'NR==2{print $4}' || true)"
|
|
196
|
+
if [[ -n "${FREE_K:-}" ]]; then
|
|
197
|
+
FREE_GB=$((FREE_K / 1024 / 1024))
|
|
198
|
+
if [[ "$FREE_GB" -lt 10 ]]; then warn "$CACHE_DIR free: ${FREE_GB} GB (low — index lives here)"; else ok "$CACHE_DIR free: ${FREE_GB} GB"; fi
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# ============================================================
|
|
202
|
+
# Scale heuristic for first-pass time expectation.
|
|
203
|
+
if [[ "$CCGO_LOC" -ge 5000000 || "$TOTAL_LOC" -ge 5000000 ]]; then
|
|
204
|
+
warn "Large codebase ($(human "$TOTAL_LOC") LOC) — expect a long first-pass index (likely hours). Run backgrounded; incremental thereafter."
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
# --- verdict ---
|
|
208
|
+
if [[ "$BLOCKERS" -gt 0 ]]; then VERDICT="NO_GO"; VEXIT=1
|
|
209
|
+
elif [[ "$WARNINGS" -gt 0 ]]; then VERDICT="GO_WITH_CAUTION"; VEXIT=0
|
|
210
|
+
else VERDICT="GO"; VEXIT=0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# ============================================================
|
|
214
|
+
# Output
|
|
215
|
+
# ============================================================
|
|
216
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
217
|
+
printf '{\n'
|
|
218
|
+
printf ' "repo": "%s",\n' "$(json_escape "$REPO_ABS")"
|
|
219
|
+
printf ' "is_git_repo": %s,\n' "$IS_GIT"
|
|
220
|
+
printf ' "git_root": %s,\n' "$([[ -n "$GIT_TOP" ]] && printf '"%s"' "$(json_escape "$GIT_TOP")" || printf 'null')"
|
|
221
|
+
printf ' "at_git_root": %s,\n' "$AT_ROOT"
|
|
222
|
+
printf ' "head": "%s",\n' "$(json_escape "$COMMIT")"
|
|
223
|
+
printf ' "tracked_files": %s,\n' "$TRACKED"
|
|
224
|
+
printf ' "files_on_disk": %s,\n' "$ALLDISK"
|
|
225
|
+
printf ' "languages": [%s],\n' "$LANG_J"
|
|
226
|
+
printf ' "total_source_loc": %s,\n' "$TOTAL_LOC"
|
|
227
|
+
printf ' "ccgo_loc": %s,\n' "$CCGO_LOC"
|
|
228
|
+
printf ' "committed_vendor_paths": [%s],\n' "$VEND_J"
|
|
229
|
+
printf ' "engine": {"found": %s, "path": %s, "version": %s, "auto_index_limit": %s},\n' \
|
|
230
|
+
"$ENGINE_FOUND" \
|
|
231
|
+
"$([[ -n "$ENGINE" ]] && printf '"%s"' "$(json_escape "$ENGINE")" || printf 'null')" \
|
|
232
|
+
"$([[ -n "$VER" ]] && printf '"%s"' "$(json_escape "$VER")" || printf 'null')" \
|
|
233
|
+
"${LIMIT:-null}"
|
|
234
|
+
printf ' "machine": {"ram_gb": %s, "cache_free_gb": %s},\n' "${RAM_GB:-null}" "${FREE_GB:-null}"
|
|
235
|
+
printf ' "warnings": [%s],\n' "$WARN_J"
|
|
236
|
+
printf ' "blockers": [%s],\n' "$FAIL_J"
|
|
237
|
+
printf ' "verdict": "%s",\n' "$VERDICT"
|
|
238
|
+
printf ' "exit_code": %s\n' "$VEXIT"
|
|
239
|
+
printf '}\n'
|
|
240
|
+
exit "$VEXIT"
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
sec "Verdict"
|
|
244
|
+
echo
|
|
245
|
+
case "$VERDICT" in
|
|
246
|
+
NO_GO) printf '%s NO-GO %s — %d blocker(s), %d warning(s). Resolve blockers above first.\n' "$R" "$D" "$BLOCKERS" "$WARNINGS";;
|
|
247
|
+
GO_WITH_CAUTION) printf '%s GO (with caution) %s — %d warning(s). Review them, then proceed.\n' "$Y" "$D" "$WARNINGS";;
|
|
248
|
+
GO) printf '%s GO %s — clear to index.\n' "$G" "$D";;
|
|
249
|
+
esac
|
|
250
|
+
|
|
251
|
+
cat <<EOF
|
|
252
|
+
|
|
253
|
+
Next step (when ready, from the git root):
|
|
254
|
+
scripts/tools/graph-init.sh --scope . --json & # or: /draft:init --graph-only
|
|
255
|
+
${ENGINE:-codebase-memory-mcp} cli list_projects '{}'
|
|
256
|
+
${ENGINE:-codebase-memory-mcp} cli index_status '{"project":"<name>"}'
|
|
257
|
+
EOF
|
|
258
|
+
hr
|
|
259
|
+
exit "$VEXIT"
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# okf-render-views.sh — render the demoted views from an OKF taxonomy bundle.
|
|
3
|
+
#
|
|
4
|
+
# The wiki/ bundle is the source of truth. This produces the two derived,
|
|
5
|
+
# human-facing views deterministically (so they never drift from the bundle and
|
|
6
|
+
# carry zero extra maintenance):
|
|
7
|
+
# 1. architecture.md — a single linear concatenation of every concept page,
|
|
8
|
+
# frontmatter stripped, in canonical section order, with a banner + TOC.
|
|
9
|
+
# This is the onboarding "read one doc" view (demoted, not deleted).
|
|
10
|
+
# 2. Concept Map — a routing table injected between the
|
|
11
|
+
# <!-- CONCEPT-MAP:START --> / <!-- CONCEPT-MAP:END --> markers in
|
|
12
|
+
# wiki/index.md (and optionally another index-root file).
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# okf-render-views.sh <BUNDLE_DIR> --arch-out <FILE> [--concept-map-into <FILE>]
|
|
16
|
+
#
|
|
17
|
+
# BUNDLE_DIR is the wiki/ directory. Exit 0 ok, 1 error, 2 bundle not found.
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
# shellcheck source=scripts/tools/_lib.sh
|
|
22
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
23
|
+
|
|
24
|
+
BUNDLE=""
|
|
25
|
+
ARCH_OUT=""
|
|
26
|
+
WEB_OUT=""
|
|
27
|
+
CMAP_INTO=()
|
|
28
|
+
|
|
29
|
+
usage() {
|
|
30
|
+
cat <<'EOF'
|
|
31
|
+
okf-render-views.sh — render architecture.md + Concept Map + HTML viewer from an OKF bundle.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
okf-render-views.sh <BUNDLE_DIR> [--arch-out FILE] [--concept-map-into FILE]... [--web FILE]
|
|
35
|
+
|
|
36
|
+
Flags:
|
|
37
|
+
--arch-out FILE Write the rendered linear architecture.md here.
|
|
38
|
+
--concept-map-into FILE Inject the Concept Map between the CONCEPT-MAP markers
|
|
39
|
+
in FILE (repeatable: e.g. wiki/index.md and ai-context.md).
|
|
40
|
+
--web FILE Write a self-contained, offline HTML viewer (single file:
|
|
41
|
+
all pages inlined, built-in markdown renderer, sidebar +
|
|
42
|
+
search). Double-click to open — no server, no internet.
|
|
43
|
+
--help Show this help.
|
|
44
|
+
|
|
45
|
+
Requires jq (already a Draft prereq) for --web. Exit 0 ok, 1 error, 2 bundle not found.
|
|
46
|
+
EOF
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
while [[ $# -gt 0 ]]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
--arch-out) ARCH_OUT="$2"; shift 2;;
|
|
52
|
+
--concept-map-into) CMAP_INTO+=("$2"); shift 2;;
|
|
53
|
+
--web) WEB_OUT="$2"; shift 2;;
|
|
54
|
+
--help|-h) usage; exit 0;;
|
|
55
|
+
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
56
|
+
*)
|
|
57
|
+
if [[ -z "$BUNDLE" ]]; then BUNDLE="$1"; else echo "Unexpected arg: $1" >&2; exit 1; fi
|
|
58
|
+
shift
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
done
|
|
62
|
+
|
|
63
|
+
[[ -n "$BUNDLE" ]] || { usage >&2; exit 1; }
|
|
64
|
+
[[ -d "$BUNDLE" ]] || { echo "ERROR: bundle directory not found: $BUNDLE" >&2; exit 2; }
|
|
65
|
+
BUNDLE="${BUNDLE%/}"
|
|
66
|
+
|
|
67
|
+
# Canonical section order for the linear render. Sections not present are skipped.
|
|
68
|
+
SECTIONS=(overview systems features reference entrypoints)
|
|
69
|
+
|
|
70
|
+
# Emit bundle-relative page paths in canonical order: for each section, its
|
|
71
|
+
# index.md first, then the rest alphabetically. Pages outside these sections
|
|
72
|
+
# (e.g. log.md, the bundle root index.md) are excluded from the linear view.
|
|
73
|
+
ordered_pages() {
|
|
74
|
+
local sec dir f
|
|
75
|
+
for sec in "${SECTIONS[@]}"; do
|
|
76
|
+
dir="$BUNDLE/$sec"
|
|
77
|
+
[[ -d "$dir" ]] || continue
|
|
78
|
+
[[ -f "$dir/index.md" ]] && echo "$sec/index.md"
|
|
79
|
+
while IFS= read -r f; do
|
|
80
|
+
[[ "$(basename "$f")" == "index.md" ]] && continue
|
|
81
|
+
echo "$sec/${f##*/}"
|
|
82
|
+
done < <(find "$dir" -maxdepth 1 -type f -name '*.md' | sort)
|
|
83
|
+
done
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
|
|
87
|
+
strip_frontmatter() {
|
|
88
|
+
awk '
|
|
89
|
+
NR==1 && /^---$/ { fm=1; next }
|
|
90
|
+
fm && /^---$/ { fm=0; next }
|
|
91
|
+
!fm { print }
|
|
92
|
+
' "$1"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# --- 1. Render architecture.md ---
|
|
96
|
+
render_architecture() {
|
|
97
|
+
local out="$1"
|
|
98
|
+
local tmp; tmp="$(mktemp)"
|
|
99
|
+
{
|
|
100
|
+
echo "---"
|
|
101
|
+
echo "generated_by: \"draft:init (okf-render-views.sh)\""
|
|
102
|
+
echo "view: rendered"
|
|
103
|
+
echo "source_of_truth: \"wiki/\""
|
|
104
|
+
echo "---"
|
|
105
|
+
echo ""
|
|
106
|
+
echo "# Architecture (Rendered View)"
|
|
107
|
+
echo ""
|
|
108
|
+
echo "> **Generated** from the \`wiki/\` OKF bundle — do not edit by hand."
|
|
109
|
+
echo "> The bundle is the source of truth; this is the single-document linear"
|
|
110
|
+
echo "> view for onboarding. Regenerate with \`okf-render-views.sh\`."
|
|
111
|
+
echo ""
|
|
112
|
+
echo "## Contents"
|
|
113
|
+
echo ""
|
|
114
|
+
# TOC from page titles.
|
|
115
|
+
local rel title sec last_sec=""
|
|
116
|
+
while IFS= read -r rel; do
|
|
117
|
+
[[ -z "$rel" ]] && continue
|
|
118
|
+
sec="${rel%%/*}"
|
|
119
|
+
if [[ "$sec" != "$last_sec" ]]; then
|
|
120
|
+
echo "- **${sec}/**"
|
|
121
|
+
last_sec="$sec"
|
|
122
|
+
fi
|
|
123
|
+
title="$(get_yaml_field "$BUNDLE/$rel" title)"
|
|
124
|
+
[[ -n "$title" ]] || title="$rel"
|
|
125
|
+
local anchor; anchor="$(printf '%s' "$title" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')"
|
|
126
|
+
anchor="${anchor#-}"; anchor="${anchor%-}"
|
|
127
|
+
echo " - [${title}](#${anchor})"
|
|
128
|
+
done < <(ordered_pages)
|
|
129
|
+
echo ""
|
|
130
|
+
# Body: each page, frontmatter stripped.
|
|
131
|
+
while IFS= read -r rel; do
|
|
132
|
+
[[ -z "$rel" ]] && continue
|
|
133
|
+
echo ""
|
|
134
|
+
echo "---"
|
|
135
|
+
echo ""
|
|
136
|
+
strip_frontmatter "$BUNDLE/$rel"
|
|
137
|
+
done < <(ordered_pages)
|
|
138
|
+
} >"$tmp"
|
|
139
|
+
mv "$tmp" "$out"
|
|
140
|
+
echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# --- 2. Build the Concept Map table (stdout) ---
|
|
144
|
+
build_concept_map() {
|
|
145
|
+
echo "| Concept | Type | Open it when… |"
|
|
146
|
+
echo "|---------|------|---------------|"
|
|
147
|
+
local rel type title desc
|
|
148
|
+
while IFS= read -r -d '' page; do
|
|
149
|
+
rel="${page#"$BUNDLE/"}"
|
|
150
|
+
[[ "$(basename "$rel")" == "index.md" ]] && continue
|
|
151
|
+
type="$(get_yaml_field "$page" type)"
|
|
152
|
+
[[ -n "$type" ]] || continue
|
|
153
|
+
title="$(get_yaml_field "$page" title)"
|
|
154
|
+
[[ -n "$title" ]] || title="$rel"
|
|
155
|
+
# description may be a folded (>) block — take the first non-empty body line.
|
|
156
|
+
desc="$(awk '
|
|
157
|
+
NR==1&&/^---$/{fm=1;next} fm&&/^---$/{exit}
|
|
158
|
+
fm && /^description:/ { collect=1; sub(/^description:[[:space:]]*>?[[:space:]]*/,""); if($0!=""){print; exit} next }
|
|
159
|
+
fm && collect { sub(/^[[:space:]]+/,""); if($0!=""){print; exit} }
|
|
160
|
+
' "$page")"
|
|
161
|
+
echo "| [${title}](${rel}) | ${type} | ${desc} |"
|
|
162
|
+
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Inject the Concept Map between markers in a target file (path may be relative
|
|
166
|
+
# to BUNDLE: links in the map are bundle-relative, so the target should resolve
|
|
167
|
+
# them — wiki/index.md works directly; an index root above wiki/ should prefix).
|
|
168
|
+
inject_concept_map() {
|
|
169
|
+
local target="$1" map="$2"
|
|
170
|
+
[[ -f "$target" ]] || { echo "WARN: concept-map target not found: $target" >&2; return 0; }
|
|
171
|
+
if ! grep -q 'CONCEPT-MAP:START' "$target" || ! grep -q 'CONCEPT-MAP:END' "$target"; then
|
|
172
|
+
echo "WARN: $target has no CONCEPT-MAP markers — skipping injection" >&2
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
local tmp; tmp="$(mktemp)"
|
|
176
|
+
awk -v mapfile="$map" '
|
|
177
|
+
/<!-- CONCEPT-MAP:START -->/ { print; while ((getline line < mapfile) > 0) print line; close(mapfile); skip=1; next }
|
|
178
|
+
/<!-- CONCEPT-MAP:END -->/ { skip=0 }
|
|
179
|
+
!skip { print }
|
|
180
|
+
' "$target" >"$tmp"
|
|
181
|
+
mv "$tmp" "$target"
|
|
182
|
+
echo "injected Concept Map → $target"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# --- 3. Render a self-contained offline HTML viewer (single file) ---
|
|
186
|
+
# All pages are inlined as JSON; a small built-in markdown renderer draws them in
|
|
187
|
+
# the browser. No server, no internet, no CDN. jq encodes page content safely
|
|
188
|
+
# (and we neutralize any literal </ so embedded "</script>" can't break parsing).
|
|
189
|
+
render_web() {
|
|
190
|
+
local out="$1"
|
|
191
|
+
command -v jq >/dev/null 2>&1 || { echo "ERROR: --web requires jq" >&2; return 1; }
|
|
192
|
+
local tmp; tmp="$(mktemp)"
|
|
193
|
+
|
|
194
|
+
cat >"$tmp" <<'HTML_HEAD'
|
|
195
|
+
<!doctype html>
|
|
196
|
+
<html lang="en">
|
|
197
|
+
<head>
|
|
198
|
+
<meta charset="utf-8">
|
|
199
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
200
|
+
<title>Knowledge Bundle</title>
|
|
201
|
+
<style>
|
|
202
|
+
:root { --bg:#0f1115; --panel:#161a22; --ink:#d7dce5; --muted:#8a93a6; --accent:#6ea8fe; --border:#262c38; --code:#1b2030; }
|
|
203
|
+
* { box-sizing: border-box; }
|
|
204
|
+
body { margin:0; font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; color:var(--ink); background:var(--bg); }
|
|
205
|
+
#app { display:flex; min-height:100vh; }
|
|
206
|
+
#side { width:300px; flex:0 0 300px; background:var(--panel); border-right:1px solid var(--border); height:100vh; overflow:auto; position:sticky; top:0; padding:14px; }
|
|
207
|
+
#side h1 { font-size:14px; margin:0 0 10px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
|
|
208
|
+
#search { width:100%; padding:8px 10px; margin-bottom:12px; background:var(--code); border:1px solid var(--border); border-radius:6px; color:var(--ink); }
|
|
209
|
+
.sec { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:14px 0 4px; }
|
|
210
|
+
.nav a { display:block; padding:4px 8px; color:var(--ink); text-decoration:none; border-radius:5px; font-size:13.5px; }
|
|
211
|
+
.nav a:hover { background:var(--code); }
|
|
212
|
+
.nav a.active { background:var(--accent); color:#0b0e14; }
|
|
213
|
+
.nav a .ty { float:right; font-size:10px; color:var(--muted); }
|
|
214
|
+
.nav a.active .ty { color:#0b0e14; }
|
|
215
|
+
#main { flex:1; max-width:900px; padding:32px 44px; }
|
|
216
|
+
#content h1,#content h2,#content h3 { line-height:1.25; }
|
|
217
|
+
#content h1 { font-size:28px; border-bottom:1px solid var(--border); padding-bottom:8px; }
|
|
218
|
+
#content a { color:var(--accent); }
|
|
219
|
+
#content code { background:var(--code); padding:2px 5px; border-radius:4px; font-size:90%; }
|
|
220
|
+
#content pre { background:var(--code); border:1px solid var(--border); border-radius:8px; padding:12px 14px; overflow:auto; }
|
|
221
|
+
#content pre code { background:none; padding:0; }
|
|
222
|
+
#content pre.mermaid-src { border-left:3px solid var(--accent); }
|
|
223
|
+
#content pre.mermaid-src::before { content:"⬡ Mermaid diagram (source)"; display:block; color:var(--muted); font-size:11px; margin-bottom:6px; }
|
|
224
|
+
#content table { border-collapse:collapse; width:100%; margin:14px 0; font-size:13.5px; }
|
|
225
|
+
#content th,#content td { border:1px solid var(--border); padding:6px 9px; text-align:left; vertical-align:top; }
|
|
226
|
+
#content th { background:var(--code); }
|
|
227
|
+
#content blockquote { border-left:3px solid var(--border); margin:12px 0; padding:2px 14px; color:var(--muted); }
|
|
228
|
+
#content hr { border:none; border-top:1px solid var(--border); margin:22px 0; }
|
|
229
|
+
.crumb { color:var(--muted); font-size:12px; margin-bottom:8px; }
|
|
230
|
+
</style>
|
|
231
|
+
</head>
|
|
232
|
+
<body>
|
|
233
|
+
<div id="app">
|
|
234
|
+
<nav id="side">
|
|
235
|
+
<h1>Knowledge Bundle</h1>
|
|
236
|
+
<input id="search" placeholder="Search…" autocomplete="off">
|
|
237
|
+
<div id="nav" class="nav"></div>
|
|
238
|
+
</nav>
|
|
239
|
+
<main id="main"><div id="content"></div></main>
|
|
240
|
+
</div>
|
|
241
|
+
<script>
|
|
242
|
+
HTML_HEAD
|
|
243
|
+
|
|
244
|
+
# Inline page data: PAGES[rel] = {title, type, md}, plus ORDER (index first).
|
|
245
|
+
{
|
|
246
|
+
echo "const PAGES = {"
|
|
247
|
+
while IFS= read -r -d '' page; do
|
|
248
|
+
local rel title type
|
|
249
|
+
rel="${page#"$BUNDLE/"}"
|
|
250
|
+
title="$(get_yaml_field "$page" title)"; [[ -n "$title" ]] || title="$rel"
|
|
251
|
+
type="$(get_yaml_field "$page" type)"
|
|
252
|
+
printf '%s: {"title": %s, "type": %s, "md": %s},\n' \
|
|
253
|
+
"$(jq -Rn --arg v "$rel" '$v')" \
|
|
254
|
+
"$(jq -Rn --arg v "$title" '$v')" \
|
|
255
|
+
"$(jq -Rn --arg v "$type" '$v')" \
|
|
256
|
+
"$(strip_frontmatter "$page" | jq -Rs . | sed 's#</#<\\/#g')"
|
|
257
|
+
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
|
|
258
|
+
echo "};"
|
|
259
|
+
# ORDER: bundle root index.md first, then everything else sorted.
|
|
260
|
+
echo "const ORDER = Object.keys(PAGES).sort(function(a,b){"
|
|
261
|
+
echo " if(a==='index.md') return -1; if(b==='index.md') return 1;"
|
|
262
|
+
echo " return a<b?-1:a>b?1:0; });"
|
|
263
|
+
} >>"$tmp"
|
|
264
|
+
|
|
265
|
+
cat >>"$tmp" <<'HTML_TAIL'
|
|
266
|
+
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
267
|
+
function resolve(base, href){
|
|
268
|
+
if(/^[a-z]+:\/\//.test(href)||href[0]==='#') return href;
|
|
269
|
+
var dir = base.indexOf('/')<0 ? '' : base.replace(/\/[^/]*$/,'');
|
|
270
|
+
var parts = (dir? dir.split('/'):[]).concat(href.split('/')), out=[];
|
|
271
|
+
for(var i=0;i<parts.length;i++){ var p=parts[i];
|
|
272
|
+
if(p==='..') out.pop(); else if(p!=='.'&&p!=='') out.push(p); }
|
|
273
|
+
return out.join('/');
|
|
274
|
+
}
|
|
275
|
+
function inline(s, base){
|
|
276
|
+
s = s.replace(/`([^`]+)`/g, function(m,c){return '<code>'+esc(c)+'</code>';});
|
|
277
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function(m,t,u){
|
|
278
|
+
if(/^[a-z]+:\/\//.test(u)) return '<a href="'+u+'" target="_blank" rel="noopener">'+t+'</a>';
|
|
279
|
+
var key=resolve(base,u);
|
|
280
|
+
if(PAGES[key]) return '<a href="#'+key+'" data-nav="'+key+'">'+t+'</a>';
|
|
281
|
+
return '<span title="'+esc(u)+'">'+t+'</span>';
|
|
282
|
+
});
|
|
283
|
+
s = s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
|
|
284
|
+
s = s.replace(/(^|[^*])\*([^*\n]+)\*/g,'$1<em>$2</em>');
|
|
285
|
+
return s;
|
|
286
|
+
}
|
|
287
|
+
function render(md, base){
|
|
288
|
+
// Pull fenced code blocks out first so their contents aren't block-parsed.
|
|
289
|
+
var blocks=[], src=md.replace(/```(\w*)\n([\s\S]*?)```/g,function(m,lang,body){
|
|
290
|
+
var cls = lang==='mermaid' ? ' class="mermaid-src"' : '';
|
|
291
|
+
blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>');
|
|
292
|
+
return 'BLOCK'+(blocks.length-1)+'';
|
|
293
|
+
});
|
|
294
|
+
var lines=src.split('\n'), out='', i=0, list='', tbl=[];
|
|
295
|
+
function closeList(){ if(list){ out+='</'+list+'>'; list=''; } }
|
|
296
|
+
function flushTbl(){
|
|
297
|
+
if(!tbl.length) return;
|
|
298
|
+
var rows=tbl.filter(function(r){return !/^\s*\|?[\s:|-]+\|?\s*$/.test(r);});
|
|
299
|
+
out+='<table>';
|
|
300
|
+
rows.forEach(function(r,ri){
|
|
301
|
+
var cells=r.replace(/^\||\|$/g,'').split('|');
|
|
302
|
+
out+='<tr>'+cells.map(function(c){var t=ri===0?'th':'td';return '<'+t+'>'+inline(c.trim(),base)+'</'+t+'>';}).join('')+'</tr>';
|
|
303
|
+
});
|
|
304
|
+
out+='</table>'; tbl=[];
|
|
305
|
+
}
|
|
306
|
+
for(;i<lines.length;i++){
|
|
307
|
+
var ln=lines[i];
|
|
308
|
+
if(/^\s*\|.*\|\s*$/.test(ln)){ closeList(); tbl.push(ln); continue; } else flushTbl();
|
|
309
|
+
var h=ln.match(/^(#{1,6})\s+(.*)$/);
|
|
310
|
+
if(h){ closeList(); out+='<h'+h[1].length+'>'+inline(esc(h[2]),base)+'</h'+h[1].length+'>'; continue; }
|
|
311
|
+
if(/^\s*---\s*$/.test(ln)){ closeList(); out+='<hr>'; continue; }
|
|
312
|
+
if(/^\s*>\s?/.test(ln)){ closeList(); out+='<blockquote>'+inline(esc(ln.replace(/^\s*>\s?/,'')),base)+'</blockquote>'; continue; }
|
|
313
|
+
var li=ln.match(/^\s*([-*]|\d+\.)\s+(.*)$/);
|
|
314
|
+
if(li){ var want=/^\d/.test(li[1])?'ol':'ul'; if(list!==want){ closeList(); list=want; out+='<'+want+'>'; } out+='<li>'+inline(esc(li[2]),base)+'</li>'; continue; }
|
|
315
|
+
var b=ln.match(/^BLOCK(\d+)$/);
|
|
316
|
+
if(b){ closeList(); out+=blocks[+b[1]]; continue; }
|
|
317
|
+
if(/^\s*$/.test(ln)){ closeList(); continue; }
|
|
318
|
+
closeList(); out+='<p>'+inline(esc(ln),base)+'</p>';
|
|
319
|
+
}
|
|
320
|
+
flushTbl(); closeList();
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
var navEl=document.getElementById('nav'), contentEl=document.getElementById('content');
|
|
324
|
+
function section(k){ return k.indexOf('/')<0 ? '(root)' : k.split('/')[0]; }
|
|
325
|
+
function buildNav(filter){
|
|
326
|
+
navEl.innerHTML=''; var lastSec=null;
|
|
327
|
+
ORDER.forEach(function(k){
|
|
328
|
+
var p=PAGES[k];
|
|
329
|
+
if(filter && (p.title+' '+p.md).toLowerCase().indexOf(filter)<0) return;
|
|
330
|
+
var sec=section(k);
|
|
331
|
+
if(sec!==lastSec){ var s=document.createElement('div'); s.className='sec'; s.textContent=sec; navEl.appendChild(s); lastSec=sec; }
|
|
332
|
+
var a=document.createElement('a'); a.href='#'+k; a.dataset.nav=k;
|
|
333
|
+
a.innerHTML=esc(p.title)+(p.type?'<span class="ty">'+esc(p.type)+'</span>':'');
|
|
334
|
+
navEl.appendChild(a);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function show(k){
|
|
338
|
+
var p=PAGES[k]; if(!p){ k=ORDER[0]; p=PAGES[k]; }
|
|
339
|
+
contentEl.innerHTML='<div class="crumb">'+esc(k)+'</div>'+render(p.md,k);
|
|
340
|
+
document.querySelectorAll('#nav a').forEach(function(a){ a.classList.toggle('active', a.dataset.nav===k); });
|
|
341
|
+
if(location.hash.slice(1)!==k) history.replaceState(null,'','#'+k);
|
|
342
|
+
contentEl.parentElement.scrollTop=0; window.scrollTo(0,0);
|
|
343
|
+
}
|
|
344
|
+
document.addEventListener('click',function(e){ var a=e.target.closest('[data-nav]'); if(a){ e.preventDefault(); show(a.dataset.nav); } });
|
|
345
|
+
document.getElementById('search').addEventListener('input',function(e){ buildNav(e.target.value.toLowerCase().trim()); });
|
|
346
|
+
window.addEventListener('hashchange',function(){ var k=decodeURIComponent(location.hash.slice(1)); if(PAGES[k]) show(k); });
|
|
347
|
+
buildNav('');
|
|
348
|
+
show(decodeURIComponent(location.hash.slice(1)) || ORDER[0]);
|
|
349
|
+
</script>
|
|
350
|
+
</body>
|
|
351
|
+
</html>
|
|
352
|
+
HTML_TAIL
|
|
353
|
+
|
|
354
|
+
mkdir -p "$(dirname "$out")"
|
|
355
|
+
mv "$tmp" "$out"
|
|
356
|
+
echo "rendered offline HTML viewer → $out ($(find "$BUNDLE" -type f -name '*.md' | grep -c .) pages)"
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
[[ -n "$ARCH_OUT" ]] && render_architecture "$ARCH_OUT"
|
|
360
|
+
|
|
361
|
+
if [[ ${#CMAP_INTO[@]} -gt 0 ]]; then
|
|
362
|
+
MAP_TMP="$(mktemp)"
|
|
363
|
+
build_concept_map >"$MAP_TMP"
|
|
364
|
+
for tgt in "${CMAP_INTO[@]}"; do
|
|
365
|
+
inject_concept_map "$tgt" "$MAP_TMP"
|
|
366
|
+
done
|
|
367
|
+
rm -f "$MAP_TMP"
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
[[ -n "$WEB_OUT" ]] && render_web "$WEB_OUT"
|
|
371
|
+
|
|
372
|
+
[[ -n "$ARCH_OUT" || -n "$WEB_OUT" || ${#CMAP_INTO[@]} -gt 0 ]] || { echo "ERROR: nothing to do (pass --arch-out, --web, and/or --concept-map-into)" >&2; exit 1; }
|
|
373
|
+
exit 0
|