This commit is contained in:
ivaquero 2026-09-23 10:05:59 +08:00
parent 814d76c547
commit 993776f937
10 changed files with 245 additions and 72 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
.workbuddy
*.pyc

View File

@ -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`:

View File

@ -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

View File

@ -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

View File

@ -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 ---

View File

@ -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

View File

@ -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)