from __future__ import annotations
import hashlib
import json
import re
import subprocess
from datetime import datetime, timezone
from pathlib import Path
import yaml
GENERATOR_VERSION = "2"
EXTS_PRI = ["sh", "zsh", "py", "js", "pl", "lua", "exs", "rb"]
SKIP_RUNTIME = frozenset({"script.meta.yaml", "README.md"})
def primary_exe(d: Path) -> Path:
name = d.name
for ext in EXTS_PRI:
cand = d / f"{name}.{ext}"
if cand.is_file():
return cand
globs: list[Path] = []
for pattern in ("*.sh", "*.zsh", "*.py", "*.js"):
globs.extend(d.glob(pattern))
if not globs:
raise FileNotFoundError(f"no runnable in {d}")
return sorted(globs, key=lambda p: str(p))[0]
def read_script_meta(script_dir: Path) -> dict:
meta_path = script_dir / "script.meta.yaml"
if not meta_path.is_file():
return {}
data = yaml.safe_load(meta_path.read_text(encoding="utf-8", errors="replace"))
return data if isinstance(data, dict) else {}
def first_non_header_line(text: str) -> str:
for line in text.splitlines():
s = line.strip()
if not s or s.startswith("#"):
continue
return s.replace("\t", " ")[:240]
return ""
def readme_description(readme: Path) -> str:
text = readme.read_text(encoding="utf-8", errors="replace")
match = re.search(r"^##\s+Description\s*\n+(.+?)(?:\n##|\Z)", text, re.MULTILINE | re.DOTALL)
if match:
for line in match.group(1).strip().splitlines():
line = line.strip()
if line and not line.startswith("#"):
return line.replace("\t", " ")[:240]
return first_non_header_line(text)
def script_header_description(exe: Path) -> str:
for line in exe.read_text(encoding="utf-8", errors="replace").splitlines()[:40]:
s = line.strip()
if not s.startswith("#") or s.startswith("#!"):
continue
s = s.lstrip("#").strip()
if not s or s.lower().startswith("(c)"):
continue
if re.match(r"^(version|author|usage):", s, re.I):
continue
if len(s) >= 12:
return s[:240]
return ""
def read_about(script_dir: Path, script_name: str) -> str:
meta = read_script_meta(script_dir)
for key in ("description", "about", "summary"):
val = meta.get(key)
if isinstance(val, str) and val.strip():
return val.strip()[:500]
readme = script_dir / "README.md"
if readme.is_file():
about = readme_description(readme).strip()
if about:
return about
try:
about = script_header_description(primary_exe(script_dir)).strip()
if about:
return about
except OSError:
pass
return f"{script_name} utility script"
def pick_category(name: str, script_dir: Path) -> str:
meta = read_script_meta(script_dir)
cat = meta.get("category")
if isinstance(cat, str) and cat.strip():
return cat.strip().lower()
n = name.lower()
patterns: list[tuple[str, str]] = [
("android", r"^(android|adb|emulator|gradle).*|.*android.*"),
("git", r"^(git|gd|jj|bcommit|autogit|gitsync|gitflux|gitfile|gitfilehistory|resquash|repick|mergetest|merge-project|create-ticket|add-submodule).*|.*git.*"),
("docker", r"^docker.*|.*docker.*"),
("network", r"^(net|ip|scan|ports|wget|curl|duck|duckduckgo|web|url|myip).*|.*(lan|port|http|https|dns|ssh).*"),
("media", r"^(img|image|png|pdf|comic|chafa|qr|qrencoder|gif|video|vid).*|.*(png|pdf|gif|qr|video|comic).*"),
("tmux", r"^(tmux|agent-session).*|.*tmux.*"),
("time", r"^(time|pomodoro|pause|days_until|halloween|xmas|countdown|timeclock).*|.*countdown.*"),
("files", r"^(file|files|flatten|fullpath|basename|nospaces|notabs|rm_|remove_|folder_|compare|diffall|chunk_|filesplit|cat-).*|.*(dir|dirs|files).*"),
("text", r"^(to(camel|snake)case|simplify-prose|dictionary|quote|dequote|append_keyword|emoji_).*|.*(case|latex|prose|text).*"),
("system", r"^(sys|cputemp|lsdaemons|killport|install-deb|machinetype|upkeep|server|sdk|task|trackusage).*|.*(systemd|journal|cpu|daemon|process).*"),
]
for cat_name, pat in patterns:
if re.match(pat, n):
return cat_name
return "misc"
def heredoc_delim(tag: str, content: str) -> str:
base = "JAN_EOF_" + re.sub(r"[^A-Za-z0-9_]", "_", tag)[:48]
suffix = hashlib.sha256(content.encode("utf-8", errors="ignore")).hexdigest()[:12]
delim = f"{base}_{suffix}"
while delim in content:
suffix = hashlib.sha256((suffix + "x").encode("utf-8")).hexdigest()[:12]
delim = f"{base}_{suffix}"
return delim
def read_file_text(path: Path) -> str:
raw = path.read_text(encoding="utf-8", errors="replace")
if not raw.endswith("\n"):
raw += "\n"
return raw
def collect_runtime_files(script_dir: Path) -> list[tuple[str, str]]:
out: list[tuple[str, str]] = []
for p in sorted(script_dir.iterdir()):
if not p.is_file() or p.name in SKIP_RUNTIME:
continue
out.append((p.name, read_file_text(p)))
return out
def resolve_dependencies(meta: dict, scripts_root: Path) -> list[str]:
deps: list[str] = []
raw = meta.get("dependencies")
if isinstance(raw, list):
for d in raw:
name = str(d).strip()
if name and (scripts_root / name).is_dir():
deps.append(name)
return deps
def inline_runner_bash(
script_name: str,
script_dir: Path,
scripts_root: Path,
dependencies: list[str],
) -> str:
primary = primary_exe(script_dir)
primary_name = primary.name
local_files = collect_runtime_files(script_dir)
lines = [
"set -euo pipefail",
'tmpdir="$(mktemp -d)"',
'trap \'rm -rf "$tmpdir"\' EXIT',
]
for fname, raw in local_files:
delim = heredoc_delim(f"{script_name}_{fname}", raw)
lines.append(f'cat >"$tmpdir/{fname}" <<\'{delim}\'')
lines.append(raw.rstrip("\n"))
lines.append(delim)
lines.append(f'chmod +x "$tmpdir/{fname}" 2>/dev/null || true')
if dependencies:
lines.append('mkdir -p "$tmpdir/bin"')
for dep in dependencies:
dep_dir = scripts_root / dep
dep_exe = primary_exe(dep_dir)
dep_raw = read_file_text(dep_exe)
delim = heredoc_delim(f"dep_{script_name}_{dep}", dep_raw)
lines.append(f'cat >"$tmpdir/bin/{dep}" <<\'{delim}\'')
lines.append(dep_raw.rstrip("\n"))
lines.append(delim)
lines.append(f'chmod +x "$tmpdir/bin/{dep}"')
lines.append('export PATH="$tmpdir/bin:$PATH"')
lines.append('cd "$tmpdir"')
lines.append(f'exec "$tmpdir/{primary_name}" "$@"')
return "\n".join(lines) + "\n"
def build_help_text(
script_name: str,
about: str,
meta: dict,
dependencies: list[str],
) -> str:
parts = [script_name, "", about, ""]
requires = meta.get("requires")
if isinstance(requires, list) and requires:
parts.append("Requires on host: " + ", ".join(str(r) for r in requires if str(r).strip()))
parts.append("")
if dependencies:
parts.append("Bundled dependencies (on PATH when run): " + ", ".join(dependencies))
parts.append("")
env = meta.get("env")
if isinstance(env, dict) and env:
parts.append("Environment:")
for k, v in env.items():
parts.append(f" {k}={v}")
parts.append("")
return "\n".join(parts).rstrip() + "\n"
def script_node(
script_name: str,
about: str,
script_dir: Path,
scripts_root: Path,
) -> dict:
meta = read_script_meta(script_dir)
dependencies = resolve_dependencies(meta, scripts_root)
help_text = build_help_text(script_name, about, meta, dependencies)
run_script = inline_runner_bash(script_name, script_dir, scripts_root, dependencies)
node: dict = {
"about": about,
"path": f"../scripts/source/{script_name}",
"commands": {
"help": {
"about": "Describe this script.",
"exec": {"argv": ["bash", "-lc", f"cat <<'EOF'\n{help_text}\nEOF\n"]},
},
"run": {
"about": "Run the script (inlined); forwards args.",
"exec": {"argv": ["bash", "-lc", run_script], "passthrough": True},
},
},
}
if dependencies:
node["dependencies"] = dependencies
requires = meta.get("requires")
if isinstance(requires, list) and requires:
node["requires"] = [str(r) for r in requires if str(r).strip()]
env = meta.get("env")
if isinstance(env, dict) and env:
node["env"] = {str(k): str(v) for k, v in env.items() if str(k).strip()}
return node
def git_sha(repo: Path) -> str | None:
try:
out = subprocess.run(
["git", "-C", str(repo), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
return out.stdout.strip() or None
except (OSError, subprocess.CalledProcessError):
return None
def write_manifest(out_dir: Path, repo: Path, category_files: list[str], script_count: int) -> None:
manifest = {
"generator_version": GENERATOR_VERSION,
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"script_count": script_count,
"categories": category_files,
"git_sha": git_sha(repo),
"files": {},
}
for path in sorted(out_dir.glob("*.yaml")):
data = path.read_bytes()
manifest["files"][path.name] = hashlib.sha256(data).hexdigest()
(out_dir / "manifest.json").write_text(
json.dumps(manifest, indent=2) + "\n",
encoding="utf-8",
)
def main() -> None:
repo = Path(__file__).resolve().parents[2]
scripts_root = repo / "scripts" / "source"
out_dir = repo / "jan-cli" / "generated" / "scripts"
out_dir.mkdir(parents=True, exist_ok=True)
outfile = out_dir / "scripts.yaml"
script_dirs = sorted(
[p for p in scripts_root.iterdir() if p.is_dir() and (p / "script.meta.yaml").is_file()],
key=lambda p: (p.name.lower(), p.name),
)
cats: dict[str, dict] = {}
for d in script_dirs:
name = d.name
about = read_about(d, name)
cat = pick_category(name, d)
cats.setdefault(cat, {})
cats[cat][name] = script_node(name, about, d, scripts_root)
category_order = [
"git",
"android",
"docker",
"network",
"media",
"files",
"text",
"tmux",
"time",
"system",
"misc",
]
ordered_cats = [(c, cats[c]) for c in category_order if c in cats]
header = (
"# AUTO-GENERATED — run: python3 jan-cli/scripts/generate_scripts_jan_spec.py\n"
"# Categorized scripts with directory-inlined `run` and standard `help`.\n"
)
category_names: list[str] = []
for cat, scripts in ordered_cats:
cat_path = out_dir / f"{cat}.yaml"
category_names.append(f"{cat}.yaml")
cat_node = {
"about": f"{cat} utilities",
"commands": {k: scripts[k] for k in sorted(scripts.keys(), key=lambda s: s.lower())},
}
cat_path.write_text(
header + yaml.safe_dump(cat_node, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
scripts_subtree: dict = {
"about": "Personal utilities from the monorepo `scripts/source` tree (auto-generated).",
"commands": {cat: {"include": f"{cat}.yaml"} for cat, _ in ordered_cats},
}
outfile.write_text(
header + yaml.safe_dump(scripts_subtree, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
write_manifest(out_dir, repo, category_names, len(script_dirs))
print(f"wrote {outfile}, {len(ordered_cats)} categories, {len(script_dirs)} scripts")
if __name__ == "__main__":
main()