docs: update documentation and project structure
- Update AGENTS.md, README.md, and docs/* with revisions - Refactor lib/acme.py and lib/state.py - Add tests for acme module - Remove install.sh and restart-services.sh (moved to scripts/) - Normalize vendor files (acme.sh, htm.js)
This commit is contained in:
+55
-4
@@ -248,7 +248,7 @@ def list_certs() -> list[dict]:
|
||||
A list of dicts, one per certificate, with keys matching
|
||||
the cert-info schema (domain, ca, cert_path, etc.).
|
||||
"""
|
||||
raw = _run_acme(["--list"])
|
||||
raw = _run_acme(["--list", "--listraw"])
|
||||
certs: list[dict] = []
|
||||
|
||||
entries = _parse_list_output(raw)
|
||||
@@ -472,7 +472,7 @@ def deploy(domain: str) -> None:
|
||||
|
||||
def _split_line(line: str, separator: str | None) -> list[str]:
|
||||
"""Split a line by *separator*, falling back to whitespace for column output."""
|
||||
if separator in line:
|
||||
if separator is not None and separator in line:
|
||||
return line.split(separator)
|
||||
return line.split()
|
||||
|
||||
@@ -503,8 +503,8 @@ def _parse_list_output(raw: str) -> list[dict]:
|
||||
headers = _split_line(header_line, "\t")
|
||||
separator = "\t"
|
||||
else:
|
||||
headers = _split_line(header_line, None) # whitespace
|
||||
separator = None
|
||||
# Column-aligned: use position-based parsing via helper
|
||||
return _parse_column_aligned(header_line, lines[1:])
|
||||
|
||||
if "Main_Domain" not in headers:
|
||||
raise ValueError(
|
||||
@@ -526,6 +526,57 @@ def _parse_list_output(raw: str) -> list[dict]:
|
||||
return entries
|
||||
|
||||
|
||||
def _find_header_positions(header_line: str):
|
||||
"""Find start positions of each header word in a column-aligned header."""
|
||||
names: list[str] = []
|
||||
starts: list[int] = []
|
||||
i = 0
|
||||
while i < len(header_line):
|
||||
while i < len(header_line) and header_line[i] == " ":
|
||||
i += 1
|
||||
j = i
|
||||
while j < len(header_line) and header_line[j] != " ":
|
||||
j += 1
|
||||
if j > i:
|
||||
names.append(header_line[i:j])
|
||||
starts.append(i)
|
||||
i = j
|
||||
return names, starts
|
||||
|
||||
|
||||
def _parse_column_aligned(header_line: str, data_lines: list[str]) -> list[dict]:
|
||||
"""Parse column-aligned output using header positions to locate fields.
|
||||
|
||||
Unlike simple whitespace splitting, this preserves empty fields by using
|
||||
character positions rather than token counts. Empty columns (e.g. missing
|
||||
Profile or SAN_Domains) are correctly handled.
|
||||
"""
|
||||
names, starts = _find_header_positions(header_line)
|
||||
|
||||
if "Main_Domain" not in names:
|
||||
raise ValueError(
|
||||
f"acme.sh --list output is not in expected format: {header_line!r}"
|
||||
)
|
||||
|
||||
# Column ends: midpoint before next header starts (or end of line for last)
|
||||
ends: list[int] = len(starts) * [len(header_line)]
|
||||
for i in range(len(starts) - 1):
|
||||
ends[i] = (starts[i] + starts[i + 1]) // 2
|
||||
|
||||
entries: list[dict] = []
|
||||
for line in data_lines:
|
||||
line = line.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
entry: dict[str, str] = {}
|
||||
for k, name in enumerate(names):
|
||||
s, e = starts[k], ends[k]
|
||||
cell = line[s:e] if len(line) > s else ""
|
||||
entry[name.lower()] = cell.strip().strip('"')
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
def _days_until(date_str: str) -> int | None:
|
||||
"""Parse an ISO date string and return days until that date from now."""
|
||||
if not date_str:
|
||||
|
||||
Reference in New Issue
Block a user