mirror of
https://github.com/ivaquero/scoopet.git
synced 2026-11-08 01:14:16 +08:00
update
This commit is contained in:
parent
814d76c547
commit
993776f937
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
.workbuddy
|
||||
|
||||
*.pyc
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: scoop-manifest
|
||||
version: 1.4.0
|
||||
name: scoop-extras-cn
|
||||
version: 1.5.0
|
||||
description: >
|
||||
Generate, update and lint Extras-CN Scoop bucket app manifests (bucket/*.json).
|
||||
Three trigger commands: generate builds a skeleton from one of 16 built-in
|
||||
@ -10,16 +10,17 @@ description: >
|
||||
probes upstream for the latest release (batch sweep supported); lint runs 22
|
||||
rules against this repo's CI and .editorconfig conventions and repairs
|
||||
formatting with --fix-format.
|
||||
The write target is $env:Scoop/buckets/extras-cn.
|
||||
Triggers: generate manifest, new manifest, update manifest, lint manifest,
|
||||
scoop manifest, bucket manifest, checkver, autoupdate, hash verification,
|
||||
version bump, Excavator, Scoop bucket maintenance, lint bucket, 生成 manifest,
|
||||
更新 manifest, 检查 manifest.
|
||||
display_name: "Scoop Manifest Forge (Extras-CN)"
|
||||
scoop manifest, scoop-extras-cn, bucket manifest, checkver, autoupdate, hash
|
||||
verification, version bump, Excavator, Scoop bucket maintenance, lint bucket,
|
||||
生成 manifest, 更新 manifest, 检查 manifest.
|
||||
display_name: "Scoop Extras CN Manifest Forge"
|
||||
visibility: "public"
|
||||
agent_created: true
|
||||
---
|
||||
|
||||
# Scoop Manifest Forge — Extras-CN edition
|
||||
# Scoop Extras CN Manifest Forge
|
||||
|
||||
Turn "upstream shipped something new" or "upstream shipped a new version" into a
|
||||
single command. All three trigger commands -- **generate / update / lint** --
|
||||
@ -27,16 +28,22 @@ share the recipe catalog and the rule engine. Python standard library only, and
|
||||
everything runs offline except `--checkver`, `--fetch-hash` and `--rehash`.
|
||||
|
||||
This is the **Extras-CN** build of the skill, adapted to
|
||||
`github.com/Scoopforge/Extras-CN` and the `C:\Scoop\buckets\extras-cn` checkout.
|
||||
It was ported from the Extras-Plus build; section 8 lists what differs.
|
||||
`github.com/Scoopforge/Extras-CN` and the copy of that bucket Scoop has
|
||||
installed. It was ported from the Extras-Plus build; section 8 lists the
|
||||
differences.
|
||||
|
||||
**Target**: `$env:Scoop/buckets/extras-cn`, the copy of this bucket Scoop has
|
||||
installed. The path is read from the environment at run time and is never
|
||||
expanded in this package, so the globally installed skill works on any machine
|
||||
and from any cwd. `--repo <path>` overrides it.
|
||||
|
||||
Package layout:
|
||||
|
||||
- `scripts/sm_lib.py` shared layer: paths, serialization, the 15 builders,
|
||||
checkver, rule engine, README table sync
|
||||
- `scripts/sm_lib.py` shared layer: paths (including the `$env:Scoop` lookup),
|
||||
serialization, the 15 builders, checkver, rule engine, README table sync
|
||||
- `scripts/scoop_manifest.py` the three-command CLI
|
||||
- `scripts/sm_selftest.py` self-check: recipes <-> builders, docs <-> code,
|
||||
repo round-trip, lint baseline
|
||||
- `scripts/sm_selftest.py` self-check: recipes <-> builders, bucket resolution,
|
||||
docs <-> code, repo round-trip, lint baseline
|
||||
- `references/manifest-fields.md` manifest field reference (this repo's rules)
|
||||
- `references/recipes.md` when each of the 16 recipes applies, and what it emits
|
||||
- `references/lint-rules.md` the 22 rules and how to fix each one
|
||||
@ -51,12 +58,18 @@ python scripts/scoop_manifest.py <command> [options]
|
||||
python scripts/sm_selftest.py
|
||||
```
|
||||
|
||||
Globally installed at
|
||||
`$env:USERPROFILE/.workbuddy/skills/scoop-extras-cn`, a junction onto this
|
||||
repo's `skills/scoop-extras-cn`, so the repo stays the single source of truth.
|
||||
Every example below is relative to the package root.
|
||||
|
||||
Managed interpreter on this machine:
|
||||
`C:\Users\msain\.workbuddy\binaries\python\versions\3.13.12\python.exe`.
|
||||
`$env:USERPROFILE/.workbuddy/binaries/python/versions/3.13.12/python.exe`.
|
||||
|
||||
## 1. Hard constraints
|
||||
|
||||
- **Output**: `<repo>/bucket/<app>.json`, optionally plus a README summary row.
|
||||
- **Output**: `$env:Scoop/buckets/extras-cn/bucket/<app>.json` by default,
|
||||
optionally plus a README summary row in the same repo.
|
||||
Never write to `bin/`, `scripts/` or `.github/` -- those belong to Scoop's
|
||||
official scripts and to this repo's CI. `scripts/AppsUtils.psm1` is this
|
||||
repo's own module and is imported by manifests, never generated by this skill.
|
||||
@ -66,8 +79,10 @@ Managed interpreter on this machine:
|
||||
- **Self-check before writing**: the result goes through the rule engine first,
|
||||
and error-level findings block the write (`--force` overrides).
|
||||
- **`--repo` goes before or after the subcommand** and in practice is rarely
|
||||
needed: without it the script walks up from the cwd for a directory holding
|
||||
both `bucket/` and `README.md`. From `C:\Scoop\buckets\extras-cn` it just works.
|
||||
needed: without it the script prefers `$env:Scoop/buckets/extras-cn`, and only
|
||||
then walks up from the cwd for a directory holding both `bucket/` and
|
||||
`README.md`. A `--repo` that points somewhere other than a bucket root is
|
||||
rejected rather than trusted.
|
||||
- **Never add a `.json` file to this package.** The bucket CI hands Scoop's
|
||||
manifest gate every path a commit changes that matches its `*.json` include
|
||||
pattern -- a `-like` match on the repo-relative path, anywhere in the tree (the
|
||||
@ -91,9 +106,11 @@ Managed interpreter on this machine:
|
||||
| **update** | `upd` | Edit fields / bump version + rewrite URLs / recompute hashes / probe upstream | `--name`, `--all`, `--set`, `--unset`, `--version`, `--rehash`, `--checkver [--apply]` |
|
||||
| **lint** | `check` | Run the 22 rules, repair formatting | `--name`, `--json`, `--strict`, `--fix-format`, `--rules` |
|
||||
|
||||
Shared option `--repo <bucket repo root>`: accepted before or after the
|
||||
subcommand; without it the script walks up from the cwd looking for a directory
|
||||
holding both `bucket/` and `README.md`.
|
||||
Shared option `--repo <bucket repo root>` overrides the target. It is accepted
|
||||
before or after the subcommand. Without it the script takes
|
||||
`$env:Scoop/buckets/extras-cn` whenever that is a bucket repo, and otherwise
|
||||
walks up from the cwd looking for a directory holding both `bucket/` and
|
||||
`README.md`.
|
||||
|
||||
## 3. generate
|
||||
|
||||
@ -236,15 +253,20 @@ emitted; and any change under `bin/`, `scripts/` or `.github/`.
|
||||
```bash
|
||||
python scripts/sm_selftest.py # full self-check (offline)
|
||||
python scripts/sm_selftest.py --verbose # print every detail
|
||||
python scripts/sm_selftest.py --repo C:\Scoop\buckets\extras-cn
|
||||
```
|
||||
|
||||
The self-check has 6 groups: recipe catalog shape -> recipe <-> builder coverage
|
||||
both ways -> virtual rendering of all 16 recipes -> repo serialization round-trip
|
||||
-> README table round-trip and row-insert idempotence -> docs <-> code
|
||||
consistency (`lint-rules.md` matches `RULES` word for word, `recipes.md` maps
|
||||
one-to-one onto `recipes.jsonc`, and `SKILL.md`'s `name` equals the directory
|
||||
name).
|
||||
The default target is the same bucket the skill writes to, so
|
||||
`$env:Scoop/buckets/extras-cn` needs no `--repo`; pass one only to point at a
|
||||
different checkout.
|
||||
|
||||
The self-check has 7 groups: recipe catalog shape -> bucket resolution
|
||||
(`$env:Scoop/buckets/extras-cn` is the default target, `--repo` overrides it,
|
||||
and no hard-coded Scoop root appears anywhere in the package) -> recipe <->
|
||||
builder coverage both ways -> virtual rendering of all 16 recipes -> repo
|
||||
serialization round-trip -> README table round-trip and row-insert idempotence
|
||||
-> docs <-> code consistency (`lint-rules.md` matches `RULES` word for word,
|
||||
`recipes.md` maps one-to-one onto `recipes.jsonc`, and `SKILL.md`'s `name`
|
||||
equals the directory name).
|
||||
|
||||
**Adding a recipe** (4 steps, and the self-check catches omissions): add an entry
|
||||
to the `recipes` array in `recipes.jsonc` (`id` / `label` / `when` /
|
||||
@ -257,11 +279,13 @@ section to `recipes.md` -> run `sm_selftest.py`.
|
||||
`lint-rules.md` together, keeping the wording identical.
|
||||
|
||||
**Editing docs**: all four markdown files pass `rumdl check` at its default
|
||||
(width <= 80 columns); finish with `rumdl fmt`.
|
||||
(width <= 80 columns); finish with `rumdl fmt --no-cache`, because plain
|
||||
`rumdl fmt` drops a `.rumdl_cache/` into whatever directory it runs in.
|
||||
|
||||
## 8. What differs from the Extras-Plus build
|
||||
|
||||
This package was ported from `extras-plus/skills/scoop-manifest`. The recipe
|
||||
This package was ported from the Extras-Plus build of this skill
|
||||
(`$env:Scoop/buckets/extras-plus/skills/scoop-extras-plus`). The recipe
|
||||
catalog, the builders and the canonical key order are shared verbatim; the
|
||||
differences are the ones this bucket actually forces.
|
||||
|
||||
@ -282,6 +306,8 @@ differences are the ones this bucket actually forces.
|
||||
| README sync errors | section must be in `summary_sections` | validated against the sections **actually parsed out of the README**, falling back to `summary_sections` only when no README is present |
|
||||
| W105 gate | literal `## ⭐️ Summary` heading | "the README has summary tables", so it runs here instead of being dead code |
|
||||
| Extra repo surface | — | `scripts/AppsUtils.psm1`, imported by 3 manifests; never generated or edited by this skill |
|
||||
| Skill name | `scoop-extras-plus` | `scoop-extras-cn` |
|
||||
| Bucket target | `--repo` -> `$env:Scoop/buckets/extras-plus` -> cwd | same, spelled `$env:Scoop/buckets/extras-cn` |
|
||||
|
||||
Two upstream-rule fixes were carried into this build because they are latent bugs
|
||||
rather than repo-specific choices, and both are described in `lint-rules.md`:
|
||||
@ -20,7 +20,7 @@ larger share of local apps than the upstream proportions would suggest.
|
||||
|
||||
## 1. Method
|
||||
|
||||
- Corpus: `C:\Scoop\buckets\extras\bucket` — the upstream
|
||||
- Corpus: `$env:Scoop/buckets/extras/bucket` — the upstream
|
||||
`ScoopInstaller/Extras` bucket, **2389** `*.json` manifests, against the 88 in
|
||||
this repo.
|
||||
- Offline and read-only: every file is `json.loads`-ed and tallied with
|
||||
@ -153,11 +153,13 @@ python scripts/scoop_manifest.py lint --json # machine-readable
|
||||
python scripts/scoop_manifest.py lint --strict # warnings fail too
|
||||
python scripts/scoop_manifest.py lint --fix-format # formatting only
|
||||
python scripts/scoop_manifest.py lint --rules # print the rule catalog
|
||||
python scripts/scoop_manifest.py lint --repo C:\Scoop\buckets\extras-cn
|
||||
python scripts/scoop_manifest.py lint --repo $env:Scoop/buckets/extras-cn
|
||||
```
|
||||
|
||||
`--repo` is accepted before or after the subcommand; from inside
|
||||
`C:\Scoop\buckets\extras-cn` it is not needed at all.
|
||||
`--repo` is accepted before or after the subcommand, but is rarely needed: the
|
||||
default target is `$env:Scoop/buckets/extras-cn`, the copy of this bucket Scoop
|
||||
has installed. The example above is PowerShell spelling of the very path the
|
||||
script already picks.
|
||||
|
||||
## Divergence from the Extras-Plus build
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
"""scoop-manifest skill CLI: three trigger commands -- generate / update / lint.
|
||||
"""scoop-extras-cn skill CLI: three trigger commands -- generate / update / lint.
|
||||
|
||||
python scripts/scoop_manifest.py generate # generate: build a manifest from a recipe and fill it in
|
||||
python scripts/scoop_manifest.py update # update: edit fields / bump version / rehash / probe upstream
|
||||
python scripts/scoop_manifest.py lint # lint: check bucket/ against this repo's CI standard
|
||||
|
||||
The target is `$env:Scoop/buckets/extras-cn` unless `--repo` says otherwise, so the
|
||||
globally installed skill writes into the bucket Scoop itself reads, from any cwd.
|
||||
All three subcommands accept short aliases: gen, upd, check.
|
||||
"""
|
||||
|
||||
@ -50,6 +52,12 @@ def _key_value_pairs(items: list[str], flag: str) -> OrderedDict:
|
||||
return pairs
|
||||
|
||||
|
||||
REPO_HELP = (
|
||||
"bucket repo root (default: $env:Scoop/buckets/extras-cn, else the "
|
||||
"nearest ancestor holding bucket/ and README.md)"
|
||||
)
|
||||
|
||||
|
||||
def add_repo_argument(parser: argparse.ArgumentParser, main_parser=None) -> None:
|
||||
"""Declare --repo so it works before *and* after the subcommand.
|
||||
|
||||
@ -59,11 +67,14 @@ def add_repo_argument(parser: argparse.ArgumentParser, main_parser=None) -> None
|
||||
it in both places and merging the two values in repo_root_from keeps both
|
||||
spellings usable. The subcommand declares it with `default=argparse.SUPPRESS`
|
||||
so that omitting it does not overwrite an earlier top-level value with None.
|
||||
|
||||
The two spellings are separate calls rather than one `**kwargs` dict:
|
||||
unpacking a dict into add_argument defeats its overloads, and ty rejects it.
|
||||
"""
|
||||
kwargs = {"help": "bucket repo root (walks upwards by default)"}
|
||||
if main_parser is not None:
|
||||
kwargs["default"] = argparse.SUPPRESS
|
||||
parser.add_argument("--repo", **kwargs)
|
||||
parser.add_argument("--repo", default=argparse.SUPPRESS, help=REPO_HELP)
|
||||
else:
|
||||
parser.add_argument("--repo", help=REPO_HELP)
|
||||
|
||||
|
||||
def repo_root_from(args) -> Path:
|
||||
@ -803,7 +814,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
" lint: python scripts/scoop_manifest.py lint --fix-format\n"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--repo", help="bucket repo root (walks upwards by default)")
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
help="bucket repo root (default: $env:Scoop/buckets/extras-cn, else the "
|
||||
"nearest ancestor holding bucket/ and README.md)",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# --- generate ---
|
||||
@ -1,7 +1,7 @@
|
||||
"""Shared library for the scoop-manifest skill (Python standard library only).
|
||||
"""Shared library for the scoop-extras-cn skill (Python standard library only).
|
||||
|
||||
Layers:
|
||||
paths skill_root / assets_dir / find_repo_root / bucket_dir
|
||||
paths skill_root / assets_dir / scoop_bucket_root / find_repo_root / bucket_dir
|
||||
serialize load_manifest / dumps_manifest / write_manifest (4-space indent + CRLF + trailing newline + canonical key order)
|
||||
recipes load_recipes / recipe_by_id / build_manifest (assets/recipes.jsonc is the single source of truth)
|
||||
checkver detect_latest (github / url+regex / url+jsonpath+regex+replace)
|
||||
@ -18,16 +18,18 @@ import contextlib
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
USER_AGENT = "scoop-manifest-skill/1.0 (+https://github.com/Scoopforge/Extras-CN)"
|
||||
USER_AGENT = "scoop-extras-cn-skill/1.0 (+https://github.com/Scoopforge/Extras-CN)"
|
||||
|
||||
# This bucket targets Chinese users, so a description written in Chinese is the
|
||||
# repo convention rather than a defect: 57 of the 88 manifests here do it. The
|
||||
@ -40,6 +42,7 @@ def has_cjk(text: str) -> bool:
|
||||
"""True when the text contains Chinese (or CJK) characters."""
|
||||
return bool(CJK_RE.search(text))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 0. Exceptions and runtime
|
||||
# --------------------------------------------------------------------------
|
||||
@ -80,15 +83,57 @@ def references_dir() -> Path:
|
||||
return skill_root() / "references"
|
||||
|
||||
|
||||
def find_repo_root(start: Path | None = None) -> Path:
|
||||
"""Walk upwards for a directory holding both bucket/ and README.md."""
|
||||
# The skill is bound to one bucket, and to the copy of it Scoop itself has
|
||||
# installed. The location is read from the environment at run time -- the docs
|
||||
# spell it `$env:Scoop/buckets/extras-cn`, and a literal drive path never
|
||||
# appears in this package, so the globally installed skill works on any machine
|
||||
# and from any cwd.
|
||||
SCOOP_ENV_VAR = "Scoop"
|
||||
BUCKET_NAME = "extras-cn"
|
||||
BUCKETS_SUBDIR = "buckets"
|
||||
|
||||
|
||||
def is_bucket_root(path: Path) -> bool:
|
||||
"""True when *path* looks like a bucket repo: bucket/ next to README.md."""
|
||||
return (path / "bucket").is_dir() and (path / "README.md").is_file()
|
||||
|
||||
|
||||
def scoop_bucket_root() -> Path | None:
|
||||
"""`$env:Scoop/buckets/extras-cn`, or None when Scoop is not installed."""
|
||||
home = os.environ.get(SCOOP_ENV_VAR, "").strip()
|
||||
if not home:
|
||||
return None
|
||||
return Path(home) / BUCKETS_SUBDIR / BUCKET_NAME
|
||||
|
||||
|
||||
def find_repo_root(explicit: Path | None = None, start: Path | None = None) -> Path:
|
||||
"""Resolve the bucket repo to operate on.
|
||||
|
||||
Precedence: an explicit `--repo` path, then `$env:Scoop/buckets/extras-cn`,
|
||||
so the globally installed skill writes into the bucket Scoop actually reads
|
||||
whatever the cwd is, then the nearest ancestor of *start* (the cwd by
|
||||
default) holding both bucket/ and README.md, which covers a plain checkout
|
||||
on a machine without the Scoop environment.
|
||||
"""
|
||||
if explicit is not None:
|
||||
candidate = Path(explicit)
|
||||
if not is_bucket_root(candidate):
|
||||
raise SmError(
|
||||
"--repo is not a bucket root (needs both bucket/ and README.md): "
|
||||
f"{candidate}"
|
||||
)
|
||||
return candidate.resolve()
|
||||
installed = scoop_bucket_root()
|
||||
if installed is not None and is_bucket_root(installed):
|
||||
return installed.resolve()
|
||||
base = Path(start).resolve() if start is not None else Path.cwd().resolve()
|
||||
for cand in [base, *base.parents]:
|
||||
if (cand / "bucket").is_dir() and (cand / "README.md").is_file():
|
||||
return cand
|
||||
for candidate in [base, *base.parents]:
|
||||
if is_bucket_root(candidate):
|
||||
return candidate
|
||||
raise SmError(
|
||||
"bucket repo root not found (needs both bucket/ and README.md); "
|
||||
"pass --repo, or run from inside the repo."
|
||||
"bucket repo not found: no ancestor of the cwd holds both bucket/ and "
|
||||
f"README.md, and {installed or SCOOP_ENV_VAR} is not such a directory. "
|
||||
"Set the Scoop environment variable, pass --repo, or run from inside the repo."
|
||||
)
|
||||
|
||||
|
||||
@ -1322,6 +1367,8 @@ def is_dangerous_snippet(script: str) -> str | None:
|
||||
continue
|
||||
return label
|
||||
return None
|
||||
|
||||
|
||||
HTTP_ALLOW_HOSTS = ("mirror.ctan.org",)
|
||||
REQUIRED_FIELDS = [
|
||||
"version",
|
||||
@ -1796,9 +1843,7 @@ def lint_manifest_text(
|
||||
occurrences = len(re.findall(rf"\[{re.escape(name)}\]\(", readme_text))
|
||||
if occurrences == 0:
|
||||
documented = readme_app_names(readme_text)
|
||||
cased = next(
|
||||
(n for n in documented if n.lower() == name.lower()), None
|
||||
)
|
||||
cased = next((n for n in documented if n.lower() == name.lower()), None)
|
||||
close = difflib.get_close_matches(name, documented, n=1, cutoff=0.75)
|
||||
if cased is not None:
|
||||
hint = (
|
||||
@ -1976,9 +2021,7 @@ def parse_summary(readme_text: str) -> list[SummaryTable]:
|
||||
separator = _split_row(block[1])
|
||||
widths = [len(cell) for cell in separator]
|
||||
rows = [_split_row(row) for row in block[2:]]
|
||||
tables.append(
|
||||
SummaryTable(section, header, rows, widths, app_col)
|
||||
)
|
||||
tables.append(SummaryTable(section, header, rows, widths, app_col))
|
||||
continue
|
||||
index += 1
|
||||
return tables
|
||||
@ -2001,7 +2044,7 @@ def _heading_matches(line: str, section: str) -> bool:
|
||||
return re.sub(r"^#+\s*", "", line).strip() == section
|
||||
|
||||
|
||||
def _locate_summary_table(lines: list[str], section: str) -> tuple[int, int] | None:
|
||||
def _locate_summary_table(lines: Sequence[str], section: str) -> tuple[int, int] | None:
|
||||
"""Return (header row, end row) of the summary table under a section heading.
|
||||
|
||||
The table may be nested under further sub-headings, so the search descends
|
||||
@ -1,4 +1,4 @@
|
||||
"""scoop-manifest skill self-check: offline, validates the skill package and the repo baseline.
|
||||
"""scoop-extras-cn skill self-check: offline, validates the skill package and the repo baseline.
|
||||
|
||||
python scripts/sm_selftest.py # full self-check
|
||||
python scripts/sm_selftest.py --verbose # print every detail
|
||||
@ -8,8 +8,10 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
@ -111,10 +113,18 @@ def check_recipes(check: Checker) -> None:
|
||||
bool(recipes), "recipe catalog is not empty", f"{len(recipes)} recipes"
|
||||
)
|
||||
|
||||
# Dot-directories are tool caches (`__pycache__`, and rumdl's `.rumdl_cache`,
|
||||
# which ships its own .gitignore). Git never stages them, so they cannot reach
|
||||
# the CI gate, which only ever sees tracked paths. Skipping them keeps the
|
||||
# documented `rumdl fmt` step from tripping this guard.
|
||||
stray_json = sorted(
|
||||
path.relative_to(L.skill_root()).as_posix()
|
||||
for path in L.skill_root().rglob("*")
|
||||
if path.is_file() and path.suffix == ".json"
|
||||
if path.is_file()
|
||||
and path.suffix == ".json"
|
||||
and not any(
|
||||
part.startswith(".") for part in path.relative_to(L.skill_root()).parts[:-1]
|
||||
)
|
||||
)
|
||||
check.expect(
|
||||
not stray_json,
|
||||
@ -162,8 +172,74 @@ def check_recipes(check: Checker) -> None:
|
||||
)
|
||||
|
||||
|
||||
# A drive-letter path that ends up inside a `Scoop` directory: the package must
|
||||
# spell the bucket target as `$env:Scoop/...` and let the shell or the Python
|
||||
# process supply the value, so the skill keeps working on a machine whose Scoop
|
||||
# lives somewhere else.
|
||||
HARDCODED_SCOOP_ROOT = re.compile(r"(?<![A-Za-z])[A-Za-z]:[\\/](?![\\/])[^\s]*[Ss]coop")
|
||||
|
||||
|
||||
def check_paths(check: Checker) -> None:
|
||||
print("\n[2] bucket resolution ($env:Scoop, no hard-coded root)")
|
||||
root = L.skill_root()
|
||||
text_files = [
|
||||
root / "SKILL.md",
|
||||
root / "assets" / L.CATALOG_NAME,
|
||||
*sorted((root / "references").glob("*.md")),
|
||||
*sorted((root / "scripts").glob("*.py")),
|
||||
]
|
||||
hard: list[str] = []
|
||||
for path in text_files:
|
||||
for match in HARDCODED_SCOOP_ROOT.finditer(path.read_text(encoding="utf-8")):
|
||||
hard.append(f"{path.name}: {match.group(0)}")
|
||||
check.expect(
|
||||
not hard,
|
||||
"no hard-coded Scoop root in the package ($env:Scoop is read at run time)",
|
||||
"; ".join(hard) if hard else f"{len(text_files)} files scanned",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
installed = Path(tmp) / L.BUCKETS_SUBDIR / L.BUCKET_NAME
|
||||
(installed / "bucket").mkdir(parents=True)
|
||||
(installed / "README.md").write_text("# stub\n", encoding="utf-8")
|
||||
saved = os.environ.get(L.SCOOP_ENV_VAR)
|
||||
os.environ[L.SCOOP_ENV_VAR] = tmp
|
||||
try:
|
||||
resolved = L.find_repo_root()
|
||||
check.expect(
|
||||
resolved == installed.resolve(),
|
||||
"$env:Scoop/buckets/extras-cn is the default target",
|
||||
str(resolved),
|
||||
)
|
||||
outside = root.parent.parent
|
||||
if L.is_bucket_root(outside):
|
||||
check.expect(
|
||||
L.find_repo_root(outside) == outside.resolve(),
|
||||
"--repo overrides the environment",
|
||||
str(outside),
|
||||
)
|
||||
else:
|
||||
check.warn(
|
||||
"--repo override not exercised: the package is not inside a bucket repo",
|
||||
str(outside),
|
||||
)
|
||||
try:
|
||||
L.find_repo_root(Path(tmp))
|
||||
rejected = False
|
||||
except L.SmError:
|
||||
rejected = True
|
||||
check.expect(
|
||||
rejected, "--repo rejects a directory that is not a bucket root"
|
||||
)
|
||||
finally:
|
||||
if saved is None:
|
||||
os.environ.pop(L.SCOOP_ENV_VAR, None)
|
||||
else:
|
||||
os.environ[L.SCOOP_ENV_VAR] = saved
|
||||
|
||||
|
||||
def check_render(check: Checker) -> None:
|
||||
print("\n[2] recipe rendering (offline, virtual parameters)")
|
||||
print("\n[3] recipe rendering (offline, virtual parameters)")
|
||||
catalog = L.load_recipes()
|
||||
for recipe in catalog["recipes"]:
|
||||
spec = dict(DUMMY)
|
||||
@ -201,7 +277,7 @@ def check_render(check: Checker) -> None:
|
||||
def check_repo(check: Checker, repo: Path) -> None:
|
||||
bucket = L.bucket_dir(repo)
|
||||
files = sorted(bucket.glob("*.json"))
|
||||
print(f"\n[3] repo round-trip consistency ({repo.name})")
|
||||
print(f"\n[5] repo round-trip consistency ({repo.name})")
|
||||
if not files:
|
||||
check.fail("no manifests under bucket/")
|
||||
return
|
||||
@ -221,7 +297,7 @@ def check_repo(check: Checker, repo: Path) -> None:
|
||||
else:
|
||||
check.ok(f"{len(files)} manifests round-trip byte-identically")
|
||||
|
||||
print("\n[5] README summary table round-trip (this repo)")
|
||||
print("\n[6] README summary table round-trip (this repo)")
|
||||
readme_path = repo / "README.md"
|
||||
if not readme_path.is_file():
|
||||
check.warn("no README.md at the repo root, skipping")
|
||||
@ -262,7 +338,7 @@ def check_repo(check: Checker, repo: Path) -> None:
|
||||
|
||||
|
||||
def check_lint_baseline(check: Checker, repo: Path) -> None:
|
||||
print("\n[6] full lint baseline")
|
||||
print("\n[7] full lint baseline")
|
||||
readme = (
|
||||
(repo / "README.md").read_text(encoding="utf-8")
|
||||
if (repo / "README.md").is_file()
|
||||
@ -295,7 +371,7 @@ def check_lint_baseline(check: Checker, repo: Path) -> None:
|
||||
|
||||
|
||||
def check_docs(check: Checker) -> None:
|
||||
print("\n[7] docs <-> code consistency")
|
||||
print("\n[8] docs <-> code consistency")
|
||||
refs = L.references_dir()
|
||||
catalog = L.load_recipes()
|
||||
recipe_ids = [r["id"] for r in catalog["recipes"]]
|
||||
@ -433,9 +509,11 @@ def check_readme_parser(check: Checker) -> None:
|
||||
f"got {[t.section for t in plus_tables]}",
|
||||
)
|
||||
check.expect(
|
||||
plus_tables
|
||||
and plus_tables[0].app_col == 0
|
||||
and L.readme_app_names(plus) == ["alpha", "beta"],
|
||||
bool(
|
||||
plus_tables
|
||||
and plus_tables[0].app_col == 0
|
||||
and L.readme_app_names(plus) == ["alpha", "beta"]
|
||||
),
|
||||
"Extras-Plus app names come from the linked cell",
|
||||
)
|
||||
|
||||
@ -448,7 +526,7 @@ def check_readme_parser(check: Checker) -> None:
|
||||
f"got {[t.section for t in cn_tables]}",
|
||||
)
|
||||
check.expect(
|
||||
bool(cn_tables) and cn_tables[0].app_col == 1,
|
||||
bool(cn_tables and cn_tables[0].app_col == 1),
|
||||
"Extras-CN app names come from column 1, not column 0",
|
||||
)
|
||||
check.expect(
|
||||
@ -478,13 +556,18 @@ def check_readme_parser(check: Checker) -> None:
|
||||
)
|
||||
|
||||
# Insertion must respect the shape of the target table.
|
||||
inserted, _msg = L.insert_summary_row(cn, "\u5916\u8bed\u5b66\u4e60", "zzz", "https://z")
|
||||
inserted, _msg = L.insert_summary_row(
|
||||
cn, "\u5916\u8bed\u5b66\u4e60", "zzz", "https://z"
|
||||
)
|
||||
check.expect(
|
||||
"[zzz](https://z)" in inserted and inserted.index("[zzz](https://z)")
|
||||
"[zzz](https://z)" in inserted
|
||||
and inserted.index("[zzz](https://z)")
|
||||
> inserted.index("[aboboo](http://aboboo.com)"),
|
||||
"a new row is appended in a 4-column CJK table",
|
||||
)
|
||||
mirror, _msg2 = L.insert_summary_row(cn, "\u5f00\u6e90\u955c\u50cf", "Zzz-cn", "https://z")
|
||||
mirror, _msg2 = L.insert_summary_row(
|
||||
cn, "\u5f00\u6e90\u955c\u50cf", "Zzz-cn", "https://z"
|
||||
)
|
||||
check.expect(
|
||||
"Zzz-cn" in mirror and "[Zzz-cn](" not in mirror,
|
||||
"the mirror table keeps plain text (no markdown link)",
|
||||
@ -493,17 +576,20 @@ def check_readme_parser(check: Checker) -> None:
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="sm_selftest.py", description="scoop-manifest skill self-check"
|
||||
prog="sm_selftest.py", description="scoop-extras-cn skill self-check"
|
||||
)
|
||||
parser.add_argument("--verbose", action="store_true")
|
||||
parser.add_argument("--repo")
|
||||
parser.add_argument(
|
||||
"--repo", help="bucket repo root (default: $env:Scoop/buckets/extras-cn)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
check = Checker(args.verbose)
|
||||
print("scoop-manifest skill self-check")
|
||||
print("scoop-extras-cn skill self-check")
|
||||
print(f"skill package: {L.skill_root()}")
|
||||
|
||||
check_recipes(check)
|
||||
check_paths(check)
|
||||
check_render(check)
|
||||
check_readme_parser(check)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user