docs: migrate workflow to DooPlex-local execution
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
# Felhom scripts — Changelog
|
||||
|
||||
## install_skills.py — cross-platform (POSIX symlink / Windows junction) (2026-07-19)
|
||||
|
||||
Claude Code now runs on DooPlex (Debian 13), where `mklink /J` does not exist — the script would
|
||||
have fallen through to COPY mode on every run, silently breaking the "repo edits are live
|
||||
immediately" property that makes `felhom.eu/skills/` the source of truth.
|
||||
|
||||
- Link creation is now platform-dispatched behind `os.name == "nt"`: `os.symlink(...,
|
||||
target_is_directory=True)` on POSIX, the existing `mklink /J` on Windows. Copy-mode fallback,
|
||||
idempotency, and the re-run warning are unchanged.
|
||||
- **`remove_existing()` had a real hazard on POSIX**: `os.rmdir()` fails on a symlink-to-directory,
|
||||
which would have fallen through to `shutil.rmtree()` — and rmtree following a symlink into
|
||||
`skills/` would have deleted the repo's own skill sources. It now unlinks symlinks explicitly
|
||||
before any rmdir/rmtree path is reached. Existence checks use `os.path.lexists()` so a broken
|
||||
link is seen and replaced rather than ignored.
|
||||
- Human-facing output says "symlink" or "junction" per platform.
|
||||
|
||||
## build-felhom-iso.sh v1.22.0 — the boot screen is ours, and it offers exactly one thing (R-38) (2026-07-19)
|
||||
|
||||
**A boot menu is a product surface, and ours was Proxmox's.** Every ISO is now repacked after
|
||||
|
||||
+43
-17
@@ -1,12 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Install the Felhom Claude Code skills from <repo>/skills/ into ~/.claude/skills/.
|
||||
|
||||
Usage: python scripts/install_skills.py (run from the felhom.eu repo root or anywhere)
|
||||
Usage: python3 scripts/install_skills.py (run from the felhom.eu repo root or anywhere)
|
||||
|
||||
Personal skills (~/.claude/skills/) apply across all projects — the right scope for the 4-repo
|
||||
E:\\git workspace. Preferred install is a Windows junction (mklink /J) so edits in the repo are live
|
||||
immediately; if junction creation fails or isn't followed, falls back to a full COPY — in copy mode
|
||||
you must RE-RUN this script after editing skills/. Idempotent: safe to re-run any time.
|
||||
felhom workspace. Preferred install is a LINK so edits in the repo are live immediately: a POSIX
|
||||
symlink on Linux/macOS (the DooPlex environment), a junction (mklink /J) on Windows. If the link
|
||||
cannot be created or isn't followed, falls back to a full COPY — in copy mode you must RE-RUN this
|
||||
script after editing skills/. Idempotent: safe to re-run any time.
|
||||
"""
|
||||
import os, shutil, subprocess, sys
|
||||
|
||||
@@ -18,7 +19,11 @@ fails = 0
|
||||
copy_mode_used = False
|
||||
|
||||
|
||||
def is_working_junction(target, src):
|
||||
IS_WINDOWS = os.name == "nt"
|
||||
LINK_KIND = "junction" if IS_WINDOWS else "symlink"
|
||||
|
||||
|
||||
def is_working_link(target, src):
|
||||
"""True if target resolves into src (junction/symlink already correct)."""
|
||||
try:
|
||||
return os.path.isfile(os.path.join(target, "SKILL.md")) and \
|
||||
@@ -28,35 +33,56 @@ def is_working_junction(target, src):
|
||||
|
||||
|
||||
def remove_existing(target):
|
||||
# a junction must be removed with rmdir semantics (never recurse INTO it), a copy with rmtree
|
||||
# a link must be removed WITHOUT recursing INTO it (that would delete the repo's skills);
|
||||
# a real copied directory needs rmtree
|
||||
if os.path.islink(target):
|
||||
os.unlink(target) # POSIX symlink (also a Windows symlink-to-dir)
|
||||
return
|
||||
try:
|
||||
os.rmdir(target) # works for junctions and empty dirs
|
||||
os.rmdir(target) # works for Windows junctions and empty dirs
|
||||
except OSError:
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
|
||||
|
||||
def make_link(target, src):
|
||||
"""Create the platform's live link. Returns (ok, detail-for-humans)."""
|
||||
if IS_WINDOWS:
|
||||
r = subprocess.run(["cmd", "/c", "mklink", "/J", target, src],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
return False, (r.stderr or r.stdout).strip() or "mklink failed"
|
||||
return True, ""
|
||||
try:
|
||||
os.symlink(src, target, target_is_directory=True)
|
||||
return True, ""
|
||||
except OSError as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def install(name):
|
||||
global fails, copy_mode_used
|
||||
src = os.path.join(SRC, name)
|
||||
target = os.path.join(DST, name)
|
||||
if os.path.exists(target):
|
||||
if is_working_junction(target, src):
|
||||
print("OK %-22s junction (already installed, live-linked to repo)" % name)
|
||||
if os.path.lexists(target):
|
||||
if is_working_link(target, src):
|
||||
print("OK %-22s %s (already installed, live-linked to repo)" % (name, LINK_KIND))
|
||||
return
|
||||
remove_existing(target)
|
||||
# try junction first
|
||||
r = subprocess.run(["cmd", "/c", "mklink", "/J", target, src],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode == 0 and is_working_junction(target, src):
|
||||
print("OK %-22s junction -> %s" % (name, src))
|
||||
ok, detail = make_link(target, src)
|
||||
if ok and is_working_link(target, src):
|
||||
print("OK %-22s %s -> %s" % (name, LINK_KIND, src))
|
||||
return
|
||||
if not ok:
|
||||
reason = detail
|
||||
else:
|
||||
reason = "not followed"
|
||||
# fall back to copy
|
||||
if os.path.exists(target):
|
||||
if os.path.lexists(target):
|
||||
remove_existing(target)
|
||||
try:
|
||||
shutil.copytree(src, target)
|
||||
copy_mode_used = True
|
||||
print("OK %-22s COPY (junction failed: %s)" % (name, (r.stderr or r.stdout).strip() or "not followed"))
|
||||
print("OK %-22s COPY (%s failed: %s)" % (name, LINK_KIND, reason))
|
||||
except OSError as e:
|
||||
fails += 1
|
||||
print("FAIL %-22s %s" % (name, e))
|
||||
|
||||
Reference in New Issue
Block a user