diff --git a/.agents/skills/build-cogmap/SKILL.md b/.agents/skills/build-cogmap/SKILL.md index 2268e04..c7e0498 100644 --- a/.agents/skills/build-cogmap/SKILL.md +++ b/.agents/skills/build-cogmap/SKILL.md @@ -117,6 +117,12 @@ Flags: - `--from-onex` — best-effort re-extract `.onex` → clean text first (~66%). - `--skip-resolve`, `--skip-synth` — force-skip those gates. - `--no-open` — don't auto-open the finished HTML (also via `COGMAP_NO_OPEN=1`). +- `--publish-github-pages` — after a successful build, publish the visualization + to a `gh-pages` branch and configure GitHub Pages when `gh` is authenticated. + Optional controls: `--publish-remote ` (default `origin`), + `--publish-branch ` (default `gh-pages`), `--publish-path ` for a + subdirectory, `--publish-no-push` for a local dry run, and + `--publish-no-enable-pages` to skip Pages API configuration. **Default recommendation:** `--with-resolve --with-synth` so a real content change yields fully-merged concepts and fresh insights. Read `action.json` after every @@ -202,6 +208,13 @@ finished `output/knowledge-base-viz.html` is **auto-opened in the default browse user to **hard-refresh the browser (Ctrl+Shift+R)** — for large corpora the embedded page can be 1 MB+ and can't be reloaded from the agent side. +If the user asks to share or publish the map publicly, re-run the final refresh +with `--publish-github-pages` from a GitHub-backed repository. The publisher copies +`knowledge-base-viz.html` to `index.html` on the Pages branch, includes the data +JSON and a small manifest, pushes the branch, and prints the Pages URL when it can +parse the GitHub remote. If automatic Pages enablement fails, report the printed +warning and the branch it pushed so the user can enable Pages manually. + ## Notes - First run **seeds the workspace** from the bundled demo (`assets/demo/`) and diff --git a/.agents/skills/build-cogmap/scripts/publish_github_pages.py b/.agents/skills/build-cogmap/scripts/publish_github_pages.py new file mode 100644 index 0000000..8bfe287 --- /dev/null +++ b/.agents/skills/build-cogmap/scripts/publish_github_pages.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Publish a built CogMap visualization to a GitHub Pages branch.""" +import argparse +import json +import os +import pathlib +import re +import shutil +import subprocess +import sys +import tempfile +from datetime import datetime, timezone + + +DEFAULT_BRANCH = "gh-pages" + + +def run(cmd, cwd=None, check=True): + result = subprocess.run( + cmd, + cwd=str(cwd) if cwd else None, + capture_output=True, + text=True, + encoding="utf-8", + ) + if check and result.returncode != 0: + detail = (result.stdout + result.stderr).strip() + raise RuntimeError("{} failed{}".format(" ".join(cmd), f":\n{detail}" if detail else "")) + return result + + +def parse_github_remote(url): + """Return (owner, repo) for common GitHub HTTPS/SSH remote URL forms.""" + patterns = ( + r"^https://github\.com/(?P[^/]+)/(?P[^/]+?)(?:\.git)?/?$", + r"^git@github\.com:(?P[^/]+)/(?P[^/]+?)(?:\.git)?$", + r"^ssh://git@github\.com/(?P[^/]+)/(?P[^/]+?)(?:\.git)?/?$", + ) + for pattern in patterns: + match = re.match(pattern, url.strip()) + if match: + return match.group("owner"), match.group("repo") + return None, None + + +def pages_url(owner, repo, site_path="."): + if not owner or not repo: + return None + base = f"https://{owner}.github.io/" if repo.lower() == f"{owner.lower()}.github.io" else f"https://{owner}.github.io/{repo}/" + clean = site_path.strip().strip("/").strip(".") + return base if not clean else base + clean + "/" + + +def copy_payload(output_dir, target_dir): + output_dir = pathlib.Path(output_dir) + target_dir = pathlib.Path(target_dir) + html = output_dir / "knowledge-base-viz.html" + data = output_dir / "knowledge-base-viz-data.json" + if not html.exists(): + raise FileNotFoundError(f"missing built visualization: {html}") + + target_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(html, target_dir / "index.html") + if data.exists(): + shutil.copy2(data, target_dir / data.name) + + (target_dir / ".nojekyll").write_text("", encoding="utf-8") + manifest = { + "name": "CogMap", + "published_at": datetime.now(timezone.utc).isoformat(), + "entrypoint": "index.html", + "source_html": html.name, + "data_file": data.name if data.exists() else None, + } + (target_dir / "cogmap-manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8") + + +def ensure_git_identity(worktree): + name = run(["git", "config", "user.name"], cwd=worktree, check=False).stdout.strip() + email = run(["git", "config", "user.email"], cwd=worktree, check=False).stdout.strip() + if not name: + run(["git", "config", "user.name", "CogMap Publisher"], cwd=worktree) + if not email: + run(["git", "config", "user.email", "cogmap-publisher@users.noreply.github.com"], cwd=worktree) + + +def ref_exists(repo_root, ref): + return run(["git", "rev-parse", "--verify", "--quiet", ref], cwd=repo_root, check=False).returncode == 0 + + +def add_pages_worktree(repo_root, worktree, remote, branch): + run(["git", "fetch", remote, branch, "--depth=1"], cwd=repo_root, check=False) + remote_ref = f"refs/remotes/{remote}/{branch}" + local_ref = f"refs/heads/{branch}" + if ref_exists(repo_root, remote_ref): + run(["git", "worktree", "add", "-B", branch, str(worktree), f"{remote}/{branch}"], cwd=repo_root) + return + if ref_exists(repo_root, local_ref): + run(["git", "worktree", "add", str(worktree), branch], cwd=repo_root) + return + + run(["git", "worktree", "add", "--detach", str(worktree), "HEAD"], cwd=repo_root) + run(["git", "checkout", "--orphan", branch], cwd=worktree) + run(["git", "rm", "-rf", "."], cwd=worktree, check=False) + for child in worktree.iterdir(): + if child.name == ".git": + continue + if child.is_dir(): + shutil.rmtree(child) + else: + child.unlink() + + +def enable_pages(owner, repo, branch): + if not owner or not repo: + return "Skipped Pages enablement: remote is not a github.com repository." + if shutil.which("gh") is None: + return "Skipped Pages enablement: GitHub CLI (`gh`) is not installed." + + endpoint = f"repos/{owner}/{repo}/pages" + get_result = run(["gh", "api", endpoint], check=False) + verb = "PUT" if get_result.returncode == 0 else "POST" + result = run( + [ + "gh", + "api", + "-X", + verb, + endpoint, + "-f", + f"source[branch]={branch}", + "-f", + "source[path]=/", + ], + check=False, + ) + if result.returncode != 0: + detail = (result.stdout + result.stderr).strip() + return "Pages branch was pushed, but automatic Pages enablement failed: {}".format(detail or "unknown error") + return "GitHub Pages is configured to serve from {}/.".format(branch) + + +def publish(args): + repo_root = pathlib.Path(args.repo_root or ".").resolve() + if not (repo_root / ".git").exists(): + repo_root = pathlib.Path(run(["git", "rev-parse", "--show-toplevel"], cwd=repo_root).stdout.strip()) + + remote_url = run(["git", "remote", "get-url", args.remote], cwd=repo_root, check=False).stdout.strip() + owner, repo = parse_github_remote(remote_url) if remote_url else (None, None) + tmp = pathlib.Path(tempfile.mkdtemp(prefix="cogmap-pages-")) + worktree = tmp / "worktree" + committed = False + try: + add_pages_worktree(repo_root, worktree, args.remote, args.branch) + target = worktree if args.site_path in ("", ".", "/") else worktree / args.site_path.strip("/\\") + copy_payload(args.output, target) + run(["git", "add", "-A"], cwd=worktree) + if run(["git", "diff", "--cached", "--quiet"], cwd=worktree, check=False).returncode == 0: + print("GitHub Pages branch already matches the current CogMap output.") + else: + ensure_git_identity(worktree) + run(["git", "commit", "-m", "Publish CogMap visualization"], cwd=worktree) + committed = True + print("Committed CogMap output to {}.".format(args.branch)) + + if args.no_push: + print("Published locally to branch {} (--no-push).".format(args.branch)) + else: + if not remote_url: + raise RuntimeError(f"remote `{args.remote}` was not found") + run(["git", "push", args.remote, args.branch], cwd=worktree) + print("Pushed CogMap output to {}/{}.".format(args.remote, args.branch)) + if not args.no_enable_pages: + print(enable_pages(owner, repo, args.branch)) + + url = pages_url(owner, repo, args.site_path) + if url: + print("CogMap Pages URL: {}".format(url)) + elif args.no_push: + print("No Pages URL available until the branch is pushed to a github.com remote.") + return committed + finally: + run(["git", "worktree", "remove", "--force", str(worktree)], cwd=repo_root, check=False) + shutil.rmtree(tmp, ignore_errors=True) + + +def parse_args(argv): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True, help="Directory containing knowledge-base-viz.html.") + parser.add_argument("--repo-root", default=".", help="Git repository to publish from.") + parser.add_argument("--remote", default="origin", help="Git remote to push to.") + parser.add_argument("--branch", default=DEFAULT_BRANCH, help="Pages branch to create/update.") + parser.add_argument("--site-path", default=".", help="Optional subdirectory within the Pages branch.") + parser.add_argument("--no-push", action="store_true", help="Commit locally but do not push.") + parser.add_argument("--no-enable-pages", action="store_true", help="Do not configure Pages with gh.") + return parser.parse_args(argv) + + +def main(argv=None): + try: + publish(parse_args(argv or sys.argv[1:])) + return 0 + except Exception as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/build-cogmap/scripts/refresh.py b/.agents/skills/build-cogmap/scripts/refresh.py index 0e2ab83..05f9960 100644 --- a/.agents/skills/build-cogmap/scripts/refresh.py +++ b/.agents/skills/build-cogmap/scripts/refresh.py @@ -17,6 +17,8 @@ --with-synth re-run insight synthesis when the graph changed --skip-resolve reuse existing resolution even if stale (new concepts -> singletons) --skip-synth reuse existing insights even if stale + --publish-github-pages + publish the finished HTML to a GitHub Pages branch """ import json, re, hashlib, pathlib, subprocess, sys, os, time, shutil sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) @@ -34,6 +36,18 @@ PY = sys.executable ARGS = set(sys.argv[1:]) + +def arg_value(name, default): + flag = f'--{name}' + prefix = flag + '=' + argv = sys.argv[1:] + for i, value in enumerate(argv): + if value == flag and i + 1 < len(argv): + return argv[i + 1] + if value.startswith(prefix): + return value[len(prefix):] + return default + def sha1s(s): return hashlib.sha1(s.encode('utf-8', 'ignore')).hexdigest() def safe_rmtree(p): @@ -98,6 +112,28 @@ def open_result(html): print('Open it manually: {}'.format(html)) +def publish_github_pages(): + cmd = [PY, str(PIPELINE / 'publish_github_pages.py'), + '--output', str(OUTPUT), + '--repo-root', str(pathlib.Path.cwd()), + '--remote', arg_value('publish-remote', 'origin'), + '--branch', arg_value('publish-branch', 'gh-pages'), + '--site-path', arg_value('publish-path', '.')] + if '--publish-no-push' in ARGS: + cmd.append('--no-push') + if '--publish-no-enable-pages' in ARGS: + cmd.append('--no-enable-pages') + env = dict(os.environ); env['PYTHONIOENCODING'] = 'utf-8' + r = subprocess.run(cmd, env=env, capture_output=True, text=True, encoding='utf-8') + if r.stdout.strip(): + print(r.stdout.strip()) + if r.returncode != 0: + if r.stderr.strip(): + print(r.stderr.strip()) + print('--- publish_github_pages.py FAILED ---') + sys.exit(r.returncode) + + def load_state(): if STATEF.exists(): return json.loads(STATEF.read_text(encoding='utf-8')) @@ -403,6 +439,9 @@ def main(): save_state(state) print('\nDONE. chunks={} tracked-extracted={}'.format(len(current), len(state['extracted_ids']))) print('Hard-refresh the browser (Ctrl+Shift+R) to see the update.') + if '--publish-github-pages' in ARGS: + print('publish: GitHub Pages...') + publish_github_pages() open_result(OUTPUT / 'knowledge-base-viz.html') if __name__ == '__main__': diff --git a/README.md b/README.md index 1340df4..d34760e 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ evolving landscape of thought. - Rebuilds incrementally, re-processing only changed chunks where possible. - Ships with a GitHub Copilot / Claude Code skill for agent-assisted refreshes. - Outputs a portable HTML artifact with embedded data and no server requirement. +- Can publish the generated artifact to GitHub Pages for easy sharing. ## See it in action @@ -112,6 +113,11 @@ pipeline, gives the coding agent structured actions to perform, and then resumes That agent-in-the-loop loop is what lets CogMap rebuild without requiring a separate LLM API key. +To share the finished map from a GitHub-backed repository, ask the agent to +publish it to GitHub Pages. The skill runs `refresh.py --publish-github-pages`, +copies the visualization to `index.html` on a `gh-pages` branch, pushes it, and +prints the Pages URL. + ## Installation paths ### GitHub Copilot CLI diff --git a/cogmap-app/README.md b/cogmap-app/README.md index 142aea3..1fb6d0a 100644 --- a/cogmap-app/README.md +++ b/cogmap-app/README.md @@ -43,6 +43,7 @@ cogmap-app/ Claude Code plugin root │ ├─ v3_aggregate.py merge extractions → raw concepts │ ├─ v3_assemble.py build the graph data JSON │ ├─ build_v2.py render the self-contained HTML + │ ├─ publish_github_pages.py optional: publish output to GitHub Pages │ └─ extract_onex.py optional: OneNote .onex → clean text (~66%) └─ assets/demo/ bundled demo corpus + prebuilt artifacts ├─ sources/sample-notes.txt synthetic demo corpus @@ -86,6 +87,9 @@ CogMap is a **coding agent skill**, not a standalone app you run by hand: it writes them into `cogmap/sources/`, removes the demo corpus, and refreshes. 4. Open `cogmap/output/knowledge-base-viz.html` when the agent reports completion (the agent prints the exact workspace path on every run). +5. To share the result from a GitHub-backed repo, ask the agent to publish it to + GitHub Pages. The skill runs `refresh.py --publish-github-pages`, updates the + `gh-pages` branch with `index.html`, and prints the Pages URL. `refresh.py` diffs the notes, re-extracts **only the chunks that changed**, re-clusters **only new concepts**, re-synthesizes insights **only if the graph diff --git a/cogmap-app/skills/build-cogmap/SKILL.md b/cogmap-app/skills/build-cogmap/SKILL.md index 2268e04..c7e0498 100644 --- a/cogmap-app/skills/build-cogmap/SKILL.md +++ b/cogmap-app/skills/build-cogmap/SKILL.md @@ -117,6 +117,12 @@ Flags: - `--from-onex` — best-effort re-extract `.onex` → clean text first (~66%). - `--skip-resolve`, `--skip-synth` — force-skip those gates. - `--no-open` — don't auto-open the finished HTML (also via `COGMAP_NO_OPEN=1`). +- `--publish-github-pages` — after a successful build, publish the visualization + to a `gh-pages` branch and configure GitHub Pages when `gh` is authenticated. + Optional controls: `--publish-remote ` (default `origin`), + `--publish-branch ` (default `gh-pages`), `--publish-path ` for a + subdirectory, `--publish-no-push` for a local dry run, and + `--publish-no-enable-pages` to skip Pages API configuration. **Default recommendation:** `--with-resolve --with-synth` so a real content change yields fully-merged concepts and fresh insights. Read `action.json` after every @@ -202,6 +208,13 @@ finished `output/knowledge-base-viz.html` is **auto-opened in the default browse user to **hard-refresh the browser (Ctrl+Shift+R)** — for large corpora the embedded page can be 1 MB+ and can't be reloaded from the agent side. +If the user asks to share or publish the map publicly, re-run the final refresh +with `--publish-github-pages` from a GitHub-backed repository. The publisher copies +`knowledge-base-viz.html` to `index.html` on the Pages branch, includes the data +JSON and a small manifest, pushes the branch, and prints the Pages URL when it can +parse the GitHub remote. If automatic Pages enablement fails, report the printed +warning and the branch it pushed so the user can enable Pages manually. + ## Notes - First run **seeds the workspace** from the bundled demo (`assets/demo/`) and diff --git a/cogmap-app/skills/build-cogmap/scripts/publish_github_pages.py b/cogmap-app/skills/build-cogmap/scripts/publish_github_pages.py new file mode 100644 index 0000000..8bfe287 --- /dev/null +++ b/cogmap-app/skills/build-cogmap/scripts/publish_github_pages.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Publish a built CogMap visualization to a GitHub Pages branch.""" +import argparse +import json +import os +import pathlib +import re +import shutil +import subprocess +import sys +import tempfile +from datetime import datetime, timezone + + +DEFAULT_BRANCH = "gh-pages" + + +def run(cmd, cwd=None, check=True): + result = subprocess.run( + cmd, + cwd=str(cwd) if cwd else None, + capture_output=True, + text=True, + encoding="utf-8", + ) + if check and result.returncode != 0: + detail = (result.stdout + result.stderr).strip() + raise RuntimeError("{} failed{}".format(" ".join(cmd), f":\n{detail}" if detail else "")) + return result + + +def parse_github_remote(url): + """Return (owner, repo) for common GitHub HTTPS/SSH remote URL forms.""" + patterns = ( + r"^https://github\.com/(?P[^/]+)/(?P[^/]+?)(?:\.git)?/?$", + r"^git@github\.com:(?P[^/]+)/(?P[^/]+?)(?:\.git)?$", + r"^ssh://git@github\.com/(?P[^/]+)/(?P[^/]+?)(?:\.git)?/?$", + ) + for pattern in patterns: + match = re.match(pattern, url.strip()) + if match: + return match.group("owner"), match.group("repo") + return None, None + + +def pages_url(owner, repo, site_path="."): + if not owner or not repo: + return None + base = f"https://{owner}.github.io/" if repo.lower() == f"{owner.lower()}.github.io" else f"https://{owner}.github.io/{repo}/" + clean = site_path.strip().strip("/").strip(".") + return base if not clean else base + clean + "/" + + +def copy_payload(output_dir, target_dir): + output_dir = pathlib.Path(output_dir) + target_dir = pathlib.Path(target_dir) + html = output_dir / "knowledge-base-viz.html" + data = output_dir / "knowledge-base-viz-data.json" + if not html.exists(): + raise FileNotFoundError(f"missing built visualization: {html}") + + target_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(html, target_dir / "index.html") + if data.exists(): + shutil.copy2(data, target_dir / data.name) + + (target_dir / ".nojekyll").write_text("", encoding="utf-8") + manifest = { + "name": "CogMap", + "published_at": datetime.now(timezone.utc).isoformat(), + "entrypoint": "index.html", + "source_html": html.name, + "data_file": data.name if data.exists() else None, + } + (target_dir / "cogmap-manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8") + + +def ensure_git_identity(worktree): + name = run(["git", "config", "user.name"], cwd=worktree, check=False).stdout.strip() + email = run(["git", "config", "user.email"], cwd=worktree, check=False).stdout.strip() + if not name: + run(["git", "config", "user.name", "CogMap Publisher"], cwd=worktree) + if not email: + run(["git", "config", "user.email", "cogmap-publisher@users.noreply.github.com"], cwd=worktree) + + +def ref_exists(repo_root, ref): + return run(["git", "rev-parse", "--verify", "--quiet", ref], cwd=repo_root, check=False).returncode == 0 + + +def add_pages_worktree(repo_root, worktree, remote, branch): + run(["git", "fetch", remote, branch, "--depth=1"], cwd=repo_root, check=False) + remote_ref = f"refs/remotes/{remote}/{branch}" + local_ref = f"refs/heads/{branch}" + if ref_exists(repo_root, remote_ref): + run(["git", "worktree", "add", "-B", branch, str(worktree), f"{remote}/{branch}"], cwd=repo_root) + return + if ref_exists(repo_root, local_ref): + run(["git", "worktree", "add", str(worktree), branch], cwd=repo_root) + return + + run(["git", "worktree", "add", "--detach", str(worktree), "HEAD"], cwd=repo_root) + run(["git", "checkout", "--orphan", branch], cwd=worktree) + run(["git", "rm", "-rf", "."], cwd=worktree, check=False) + for child in worktree.iterdir(): + if child.name == ".git": + continue + if child.is_dir(): + shutil.rmtree(child) + else: + child.unlink() + + +def enable_pages(owner, repo, branch): + if not owner or not repo: + return "Skipped Pages enablement: remote is not a github.com repository." + if shutil.which("gh") is None: + return "Skipped Pages enablement: GitHub CLI (`gh`) is not installed." + + endpoint = f"repos/{owner}/{repo}/pages" + get_result = run(["gh", "api", endpoint], check=False) + verb = "PUT" if get_result.returncode == 0 else "POST" + result = run( + [ + "gh", + "api", + "-X", + verb, + endpoint, + "-f", + f"source[branch]={branch}", + "-f", + "source[path]=/", + ], + check=False, + ) + if result.returncode != 0: + detail = (result.stdout + result.stderr).strip() + return "Pages branch was pushed, but automatic Pages enablement failed: {}".format(detail or "unknown error") + return "GitHub Pages is configured to serve from {}/.".format(branch) + + +def publish(args): + repo_root = pathlib.Path(args.repo_root or ".").resolve() + if not (repo_root / ".git").exists(): + repo_root = pathlib.Path(run(["git", "rev-parse", "--show-toplevel"], cwd=repo_root).stdout.strip()) + + remote_url = run(["git", "remote", "get-url", args.remote], cwd=repo_root, check=False).stdout.strip() + owner, repo = parse_github_remote(remote_url) if remote_url else (None, None) + tmp = pathlib.Path(tempfile.mkdtemp(prefix="cogmap-pages-")) + worktree = tmp / "worktree" + committed = False + try: + add_pages_worktree(repo_root, worktree, args.remote, args.branch) + target = worktree if args.site_path in ("", ".", "/") else worktree / args.site_path.strip("/\\") + copy_payload(args.output, target) + run(["git", "add", "-A"], cwd=worktree) + if run(["git", "diff", "--cached", "--quiet"], cwd=worktree, check=False).returncode == 0: + print("GitHub Pages branch already matches the current CogMap output.") + else: + ensure_git_identity(worktree) + run(["git", "commit", "-m", "Publish CogMap visualization"], cwd=worktree) + committed = True + print("Committed CogMap output to {}.".format(args.branch)) + + if args.no_push: + print("Published locally to branch {} (--no-push).".format(args.branch)) + else: + if not remote_url: + raise RuntimeError(f"remote `{args.remote}` was not found") + run(["git", "push", args.remote, args.branch], cwd=worktree) + print("Pushed CogMap output to {}/{}.".format(args.remote, args.branch)) + if not args.no_enable_pages: + print(enable_pages(owner, repo, args.branch)) + + url = pages_url(owner, repo, args.site_path) + if url: + print("CogMap Pages URL: {}".format(url)) + elif args.no_push: + print("No Pages URL available until the branch is pushed to a github.com remote.") + return committed + finally: + run(["git", "worktree", "remove", "--force", str(worktree)], cwd=repo_root, check=False) + shutil.rmtree(tmp, ignore_errors=True) + + +def parse_args(argv): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True, help="Directory containing knowledge-base-viz.html.") + parser.add_argument("--repo-root", default=".", help="Git repository to publish from.") + parser.add_argument("--remote", default="origin", help="Git remote to push to.") + parser.add_argument("--branch", default=DEFAULT_BRANCH, help="Pages branch to create/update.") + parser.add_argument("--site-path", default=".", help="Optional subdirectory within the Pages branch.") + parser.add_argument("--no-push", action="store_true", help="Commit locally but do not push.") + parser.add_argument("--no-enable-pages", action="store_true", help="Do not configure Pages with gh.") + return parser.parse_args(argv) + + +def main(argv=None): + try: + publish(parse_args(argv or sys.argv[1:])) + return 0 + except Exception as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cogmap-app/skills/build-cogmap/scripts/refresh.py b/cogmap-app/skills/build-cogmap/scripts/refresh.py index 0e2ab83..05f9960 100644 --- a/cogmap-app/skills/build-cogmap/scripts/refresh.py +++ b/cogmap-app/skills/build-cogmap/scripts/refresh.py @@ -17,6 +17,8 @@ --with-synth re-run insight synthesis when the graph changed --skip-resolve reuse existing resolution even if stale (new concepts -> singletons) --skip-synth reuse existing insights even if stale + --publish-github-pages + publish the finished HTML to a GitHub Pages branch """ import json, re, hashlib, pathlib, subprocess, sys, os, time, shutil sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) @@ -34,6 +36,18 @@ PY = sys.executable ARGS = set(sys.argv[1:]) + +def arg_value(name, default): + flag = f'--{name}' + prefix = flag + '=' + argv = sys.argv[1:] + for i, value in enumerate(argv): + if value == flag and i + 1 < len(argv): + return argv[i + 1] + if value.startswith(prefix): + return value[len(prefix):] + return default + def sha1s(s): return hashlib.sha1(s.encode('utf-8', 'ignore')).hexdigest() def safe_rmtree(p): @@ -98,6 +112,28 @@ def open_result(html): print('Open it manually: {}'.format(html)) +def publish_github_pages(): + cmd = [PY, str(PIPELINE / 'publish_github_pages.py'), + '--output', str(OUTPUT), + '--repo-root', str(pathlib.Path.cwd()), + '--remote', arg_value('publish-remote', 'origin'), + '--branch', arg_value('publish-branch', 'gh-pages'), + '--site-path', arg_value('publish-path', '.')] + if '--publish-no-push' in ARGS: + cmd.append('--no-push') + if '--publish-no-enable-pages' in ARGS: + cmd.append('--no-enable-pages') + env = dict(os.environ); env['PYTHONIOENCODING'] = 'utf-8' + r = subprocess.run(cmd, env=env, capture_output=True, text=True, encoding='utf-8') + if r.stdout.strip(): + print(r.stdout.strip()) + if r.returncode != 0: + if r.stderr.strip(): + print(r.stderr.strip()) + print('--- publish_github_pages.py FAILED ---') + sys.exit(r.returncode) + + def load_state(): if STATEF.exists(): return json.loads(STATEF.read_text(encoding='utf-8')) @@ -403,6 +439,9 @@ def main(): save_state(state) print('\nDONE. chunks={} tracked-extracted={}'.format(len(current), len(state['extracted_ids']))) print('Hard-refresh the browser (Ctrl+Shift+R) to see the update.') + if '--publish-github-pages' in ARGS: + print('publish: GitHub Pages...') + publish_github_pages() open_result(OUTPUT / 'knowledge-base-viz.html') if __name__ == '__main__': diff --git a/tests/test_cogmap_pipeline.py b/tests/test_cogmap_pipeline.py index 65c8ade..6a38bdc 100644 --- a/tests/test_cogmap_pipeline.py +++ b/tests/test_cogmap_pipeline.py @@ -4,6 +4,7 @@ import sys import tempfile import unittest +import importlib.util from pathlib import Path @@ -11,6 +12,12 @@ SKILL_DIR = REPO_ROOT / "cogmap-app" / "skills" / "build-cogmap" SCRIPTS_DIR = SKILL_DIR / "scripts" +_publish_spec = importlib.util.spec_from_file_location( + "publish_github_pages", SCRIPTS_DIR / "publish_github_pages.py" +) +publish_github_pages = importlib.util.module_from_spec(_publish_spec) +_publish_spec.loader.exec_module(publish_github_pages) + def run_script(script, workspace, cwd=None, *args): env = dict(os.environ) @@ -74,6 +81,61 @@ def test_demo_refresh_renders_html(self): self.assertGreater(data["metadata"]["counts"]["chunks"], 0) self.assertIn("CogMap", html.read_text(encoding="utf-8")) + def test_publish_remote_parsing_and_url(self): + cases = [ + ("https://github.com/octo/demo.git", ("octo", "demo")), + ("git@github.com:octo/demo.git", ("octo", "demo")), + ("ssh://git@github.com/octo/demo.git", ("octo", "demo")), + ] + for remote, expected in cases: + with self.subTest(remote=remote): + self.assertEqual(publish_github_pages.parse_github_remote(remote), expected) + self.assertEqual(publish_github_pages.pages_url("octo", "demo"), "https://octo.github.io/demo/") + self.assertEqual(publish_github_pages.pages_url("octo", "octo.github.io"), "https://octo.github.io/") + + def test_publish_github_pages_no_push_creates_branch_payload(self): + with tempfile.TemporaryDirectory() as td: + repo = Path(td) / "repo" + output = repo / "cogmap" / "output" + output.mkdir(parents=True) + (output / "knowledge-base-viz.html").write_text("CogMap", encoding="utf-8") + (output / "knowledge-base-viz-data.json").write_text('{"metadata": {}}', encoding="utf-8") + + subprocess.run(["git", "init", "-q", str(repo)], check=True) + subprocess.run(["git", "config", "user.name", "Test User"], cwd=repo, check=True) + subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo, check=True) + (repo / "README.md").write_text("test\n", encoding="utf-8") + subprocess.run(["git", "add", "README.md"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=repo, check=True) + + result = subprocess.run( + [ + sys.executable, + str(SCRIPTS_DIR / "publish_github_pages.py"), + "--repo-root", + str(repo), + "--output", + str(output), + "--no-push", + "--no-enable-pages", + ], + capture_output=True, + text=True, + encoding="utf-8", + ) + + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("Published locally to branch gh-pages", result.stdout) + html = subprocess.run( + ["git", "show", "gh-pages:index.html"], + cwd=repo, + check=True, + capture_output=True, + text=True, + encoding="utf-8", + ).stdout + self.assertEqual(html, "CogMap") + if __name__ == "__main__": unittest.main()