Skip to content

Commands

m4bwav edited this page Oct 4, 2026 · 1 revision

wikiwright.py is the helper the skill runs. It lives at skills/wikiwright/scripts/wikiwright.py, is one file, needs Python 3.9 or later and imports only the standard library. Every transcript on this page ran from a fresh clone of master (a809eff, tag v0.9.0) on 2026-10-03, on Windows 11 with Python 3.14.6 and again with 3.9.25. Paths inside the scratch clone print as <clone>. The rules behind check, outputs, snippets and live are on What the helper checks.

Usage

Run with no subcommand, it prints its usage line:

$ python skills/wikiwright/scripts/wikiwright.py
usage: wikiwright.py [-h] [--version]
                     {preflight,check,live,outputs,snippets,unbs,diffout,cachecheck,releasecheck,registry,scaffold} ...
wikiwright.py: error: the following arguments are required: cmd
$ echo $?
2

Python 3.9's argparse wraps the ... onto a third line and says "optional arguments" where 3.14 says "options"; nothing else differed. --version prints 0.9.0, and --help after any subcommand prints its options.

Subcommand What it does Network
preflight wiki feature, wiki repository, placeholder, clone github.com through gh and git
check lint a wiki working copy none
outputs every output a page shows is in the saved verification output none
snippets every code block a page shows is in the verification program none
live the published pages answer and render github.com (or the other host)
unbs replace backslash placeholders with backslashes none
diffout compare two runs of a verification script, section by section none
scaffold write the verification script, filled in and cut to the sections needed none
registry a compact survey of an npm or NuGet package npmjs.org or nuget.org
cachecheck the installed plugin against the source, by SHA-256 none
releasecheck version fields, changelog entry and a test run before tagging none

Exit codes and streams

Code Meaning
0 clean
1 findings: errors from a check, differences from diffout, a failed page from live
2 a usage or environment error: a missing folder, a bad argument, a file that would be overwritten

The helper prints its findings and its errors on stdout. Only argparse's usage errors (an unknown subcommand or option, a missing argument) go to stderr. An unknown subcommand:

$ python skills/wikiwright/scripts/wikiwright.py publish
usage: wikiwright.py [-h] [--version]
                     {preflight,check,live,outputs,snippets,unbs,diffout,cachecheck,releasecheck,registry,scaffold} ...
wikiwright.py: error: argument cmd: invalid choice: 'publish' (choose from 'preflight', 'check', 'live', 'outputs', 'snippets', 'unbs', 'diffout', 'cachecheck', 'releasecheck', 'registry', 'scaffold')
$ echo $?
2

With stderr thrown away, the usage error disappears and the check error does not:

$ python skills/wikiwright/scripts/wikiwright.py publish 2>/dev/null
$ echo $?
2
$ python skills/wikiwright/scripts/wikiwright.py check nowhere 2>/dev/null
error: nowhere is not a directory
$ echo $?
2

preflight

preflight REPO [--enable] [--wait SEC] [--clone DIR] [--kind KIND] [--seed]

REPO is OWNER/REPO, a remote URL or a clone with an origin. It needs gh logged in.

Option What it does
--enable switches the wiki feature on when it is off (gh repo edit --enable-wiki)
--wait SEC keeps re-checking git ls-remote for this many seconds (60 after --enable)
--clone DIR clones the wiki repository into DIR (or fetches, when DIR already holds it) and says whether it is a placeholder
--kind KIND github, gitea, forgejo, gitlab or azure, when neither the host name nor its API tells
--seed Gitea and Forgejo only: create the wiki with a first page through the API, with a token in WIKIWRIGHT_TOKEN

It ends with a STATE: line. On everwrite's wiki, which has pages:

$ python skills/wikiwright/scripts/wikiwright.py preflight m4bwav/everwrite
repo: m4bwav/everwrite  visibility: PUBLIC  archived: False  wiki feature: True
ls-remote: a86ebc1 HEAD
ls-remote: a86ebc1 refs/heads/master
branch: master (push here; only the default branch goes live)
STATE: exists (branch master; clone it with --clone DIR to see whether it is a placeholder)
$ python skills/wikiwright/scripts/wikiwright.py preflight m4bwav/everwrite --clone everwrite.wiki
repo: m4bwav/everwrite  visibility: PUBLIC  archived: False  wiki feature: True
ls-remote: a86ebc1 HEAD
ls-remote: a86ebc1 refs/heads/master
branch: master (push here; only the default branch goes live)
clone: everwrite.wiki
commits: 2  files: 11 (Commands.md, Development.md, FAQ.md, Getting-Started.md, Home.md, How-The-Skill-Works.md, Recipes.md, Versions-and-Upgrading.md, What-The-Checker-Flags.md, _Footer.md, _Sidebar.md)
STATE: has-pages (existing content: read every page first; update, never overwrite)

The states on GitHub:

State Exit Meaning
exists 0 the wiki repository is there; add --clone to see what it holds
placeholder 0 one commit holding only a short Home.md: write the pages in the clone and push, no force needed
has-pages 0 someone wrote pages: read them all and update in place
feature-off 1 the wiki feature is off; run again with --enable
no-wiki-repo 1 no repository yet; someone must save a first page at https://github.com/OWNER/REPO/wiki/_new (no API can create it)
archived 1 an archived repository's wiki cannot be edited
other-host 2 the remote is not a host the helper knows; pass --kind

placeholder was the state of this wiki's own repository before these pages were pushed. feature-off, no-wiki-repo and archived are read from the source and were not run for this wiki. Something that is not a repository at all:

$ python skills/wikiwright/scripts/wikiwright.py preflight nowhere
error: nowhere is not OWNER/REPO, a remote URL or a clone with an origin
$ echo $?
2

check

check DIR [--version X] [--partial] [--host KIND]

Option What it does
--version X the footer must name X and a date; Home.md should mention X (a warning)
--partial a draft of some pages: no sidebar or footer required
--host KIND the host's rules for navigation files, wikilinks, anchors and file names (default github)

A draft with most of the common mistakes. draft-wiki/Home.md:

# Home

Widget 1.2.0 sorts words. Read [[Getting Started]] first.

See [the API](API-Reference.md), [Recipes](Recipes) and [the options](Getting-Started#options).

## Sorting Words Quickly

Getting-Started.md is one line, "Install it with pip.", saved with a CRLF ending; _Sidebar.md links only Home, and _Footer.md says "This wiki describes widget 1.2.0." with no date.

$ python skills/wikiwright/scripts/wikiwright.py check draft-wiki --version 1.2.0
Getting-Started.md:0: error: 1 carriage returns (wiki pages are LF only)
Home.md:1: error: the first heading repeats the page title the host prints from the file name; start with the first paragraph: Home
Home.md:3: error: wikilink; use [Text](Page-Name)
Home.md:5: error: link to API-Reference.md: drop the .md (wiki links are page names)
Home.md:5: error: link to missing page API-Reference
Home.md:5: error: link to missing page Recipes
Home.md:5: error: no heading for #options on Getting-Started
_Footer.md:1: error: names no date (YYYY-MM-DD)
Home.md:7: warn: heading looks like Title Case; use sentence case: Sorting Words Quickly
_Sidebar.md:0: warn: does not link Getting-Started
check: 2 pages, 8 errors, 2 warnings
$ echo $?
1

The page count leaves out the sidebar and footer. A draft of two pages with no sidebar or footer passes only with --partial:

$ python skills/wikiwright/scripts/wikiwright.py check draft
_Sidebar.md:0: error: missing (a draft of a few pages: --partial)
_Footer.md:0: error: missing (a draft of a few pages: --partial)
check: 2 pages, 2 errors, 0 warnings
$ python skills/wikiwright/scripts/wikiwright.py check draft --partial
check: 2 pages, 0 errors, 0 warnings
$ echo $?
0

A version the footer does not name is an error; on Home it is a warning:

$ python skills/wikiwright/scripts/wikiwright.py check tests/fixtures/good-wiki --version 1.3.0
_Footer.md:1: error: does not name version 1.3.0
Home.md:0: warn: does not mention version 1.3.0
check: 3 pages, 1 errors, 1 warnings
$ echo $?
1

The same sample wiki under GitLab's rules, then Forgejo's:

$ python skills/wikiwright/scripts/wikiwright.py check tests/fixtures/good-wiki --host gitlab
_sidebar.md:0: error: missing (a draft of a few pages: --partial)
_Sidebar.md:0: error: GitLab reads _sidebar.md (lower case) and ignores this file
_Footer.md:0: warn: gitlab shows no footer; put the version line on Home
check: 3 pages, 2 errors, 1 warnings
$ python skills/wikiwright/scripts/wikiwright.py check tests/fixtures/good-wiki --host forgejo
check: 3 pages, 0 errors, 0 warnings

A folder that does not exist is exit 2:

$ python skills/wikiwright/scripts/wikiwright.py check nowhere
error: nowhere is not a directory
$ echo $?
2

outputs

outputs DIR VERIFY... [--address URL] [--node N]

Option What it does
VERIFY... one or more saved outputs of the verification script (the main run and the oldest Node's, for example)
--address URL the address pages show in place of the fixture's http://127.0.0.1:<port> (default https://example.com; '' for none)
--node N the Node major every output ran on, instead of reading each output's Node vN line

With the page pages/Home.md below and a saved output whose ## home-sort section prints ['apple', 'fig', 'pear']:

Widget sorts words.

```python
words = ["pear", "fig", "apple"]
print(sorted(words))
```

It prints:

```text
['apple', 'fig', 'pear']
```

Install it first.

```sh
pip install widget
```
$ python skills/wikiwright/scripts/wikiwright.py outputs pages wiki-verify.out.txt
outputs: 1 pages, 1 outputs checked, 0 missing, 0 skipped
$ echo $?
0

The run where the page's output was edited by hand is on Home. A page with a code block and nothing outputs reads as output fails:

$ python skills/wikiwright/scripts/wikiwright.py outputs pages wiki-verify.out.txt
outputs: 1 pages, 0 outputs checked, 0 missing, 0 skipped
error: 1 code blocks and no output recognised; tag each output fence ```text, or mark a page that shows none with <!-- outputs: skip (reason) -->
$ echo $?
1

--address: a page showing https://example.com/ answered 200 against a saved output that printed http://127.0.0.1:4321/ answered 200. The default reads the fixture address as https://example.com; --address '' compares as written:

$ python skills/wikiwright/scripts/wikiwright.py outputs pages wiki-verify.out.txt
outputs: 1 pages, 1 outputs checked, 0 missing, 0 skipped
$ python skills/wikiwright/scripts/wikiwright.py outputs pages wiki-verify.out.txt --address ''
Fetch.md:3: error: output block not in the verify output: https://example.com/ answered 200
outputs: 1 pages, 1 outputs checked, 1 missing, 0 skipped
$ echo $?
1

snippets

snippets DIR PROGRAM...

The same pages/Home.md, with a verification program wiki-verify.py that holds its code:

# wiki-verify.py: runs every example on the pages
words = ["pear", "fig", "apple"]
print(sorted(words))
$ python skills/wikiwright/scripts/wikiwright.py snippets pages wiki-verify.py
Home.md:16: command, not checked: pip install widget
snippets: 1 pages, 1 blocks checked, 0 missing, 0 skipped, 1 commands
$ echo $?
0

The pip install block only runs a command, so it is counted and not checked. A page whose code differs from the program is on Home.

live

live REPO DIR [--kind KIND] [--no-anchors]

DIR is the wiki working copy, which gives the page list and the sidebar and footer text to look for. Run after a push. On everwrite's wiki, cloned by the preflight above:

$ python skills/wikiwright/scripts/wikiwright.py live m4bwav/everwrite everwrite.wiki
ok   200 Commands
ok   200 Development
ok   200 FAQ
ok   200 Getting-Started
ok   301 Home -> https://github.com/m4bwav/everwrite/wiki
ok   200 How-The-Skill-Works
ok   200 Recipes
ok   200 Versions-and-Upgrading
ok   200 What-The-Checker-Flags
ok   200 wiki root
ok   footer renders (1 of 1 probes found)
ok   sidebar renders (12 of 12 probes found)
ok   anchors: 11 links to 7 pages, 0 broken, 0 extra fetches
live: 9 pages, 0 failures
$ echo $?
0

GitHub can take a few seconds to serve a page after a push; run it again once before calling a failure.

unbs

unbs FILE...

Replaces every backslash placeholder (<BS>) with a backslash, in place, and says how many it replaced. With pages/Regex.md holding "Match digits with" and the placeholder before d+:

$ python skills/wikiwright/scripts/wikiwright.py unbs pages/Regex.md
pages/Regex.md: 1 replaced
unbs: 1 placeholders replaced in 1 files
$ cat pages/Regex.md
Match digits with `\d+`.

It replaces every occurrence, including one that names the placeholder in prose, so never run it on a file that explains the placeholder.

diffout

diffout OLD NEW [--skip REGEX] [--mask REGEX] [--context N] [--save FILE] [--keep-paths] [--keep-node]

Splits two outputs of a verification script at their ## label lines and compares them section by section. Line endings, trailing spaces and local ports (127.0.0.1:<digits>, localhost:<digits>) are normalised first, so a new fixture port is not a change.

Option What it does
--skip REGEX leaves out sections whose label matches (repeatable)
--mask REGEX replaces matching text with <masked> (repeatable)
--context N unchanged lines around each change (default 1)
--save FILE writes the normalised new output, with the new output's folder, the temp folder and the home folder as <scratch>, <temp> and <home>
--keep-paths leaves local paths as they are
--keep-node compares shell node: vN lines too; by default they are counted per output and masked

With an old output whose sections are installed (Python 3.9.25), home-sort and server (listening on port 50123), and a new one with Python 3.14.6, port 61007 and an extra home-reverse section:

$ python skills/wikiwright/scripts/wikiwright.py diffout old.txt new.txt
*** changed: ## installed
  @@ -1 +1 @@
  -Python 3.9.25
  +Python 3.14.6
+++ added: ## home-reverse
  + ['pear', 'fig', 'apple']
diffout: 4 sections, 2 same, 1 changed, 1 added, 0 removed, 0 skipped
$ echo $?
1
$ python skills/wikiwright/scripts/wikiwright.py diffout old.txt new.txt --skip installed --save saved.txt
+++ added: ## home-reverse
  + ['pear', 'fig', 'apple']
saved the normalised new output to saved.txt
diffout: 4 sections, 2 same, 0 changed, 1 added, 0 removed, 1 skipped
$ cat saved.txt
## installed
Python 3.14.6
## home-sort
['apple', 'fig', 'pear']
## server
listening on 127.0.0.1:<port>
## home-reverse
['pear', 'fig', 'apple']

Exit 1 means the outputs differ, not that something broke. An output whose shell cases ran on two Node versions is also exit 1, with a warning.

scaffold

scaffold npm PACKAGE VERSION [--bin] [--requests] [--by-host] [--files] [--golden OLD_VERSION] [-o FILE] [--force]

scaffold nuget ID VERSION --namespace NS --type T [--children TFMS] [--requests] [--fsharp] [--tool ID:COMMAND] [-o FILE] [--force]

Writes the verification script from the skill's template with the package and version filled in and only the sections the package needs. It reads no registry: it does not check that the package or version exists.

Option Template What it keeps
--bin npm cli(), term() and the help case, for a package with a bin
--requests both npm: the local fixture server; NuGet: the stand-in proxy and its gate (implies --children)
--by-host npm serving the fixture under real host names (implies --requests); copies host-fixture.mjs beside the script
--files npm the files-on-disk section; copies file-tree.mjs
--golden OLD_VERSION npm the replay of a golden capture of an old version
--namespace NS, --type T NuGet required: the namespace the examples use and a public type of the package
--children TFMS NuGet each page example as a whole program on net10.0 and these frameworks (net48,net8.0)
--fsharp NuGet F# through dotnet fsi
--tool ID:COMMAND NuGet installing a dotnet tool into scratch and its transcripts
-o FILE both the file to write (default ./wiki-verify.mjs or ./wiki-verify.cs)
--force both overwrite the script and kits when they exist

For get-title-at-url, an npm package with a bin:

$ python skills/wikiwright/scripts/wikiwright.py scaffold npm get-title-at-url 3.0.0 --bin -o verify/wiki-verify.mjs
wrote <clone>\verify\wiki-verify.mjs: 14,608 bytes (the template is 25,516)
kept: core, bin; dropped: requests, by-host, files, golden, example
package.json: wrote {"private": true}
next:
  - npm install --prefix <clone>\verify get-title-at-url@3.0.0, and typescript for TypeScript snippets (typescript6@npm:typescript@6 for a second compiler)
  - under '// ----- the cases': one snippet() per code block on the pages, labelled by page
  - run: node wiki-verify.mjs > wiki-verify.out.txt, then again with OLDEST_NODE=<the oldest major in engines>
$ ls verify
package.json
wiki-verify.mjs
$ python skills/wikiwright/scripts/wikiwright.py scaffold npm get-title-at-url 3.0.0 --bin -o verify/wiki-verify.mjs
error: <clone>\verify\wiki-verify.mjs exists (pass --force to overwrite it)
$ echo $?
2

The paths print with backslashes because the run was on Windows. For RandomNameGeneratorLibrary, a NuGet library, with whole-program examples on .NET Framework 4.8 as well:

$ python skills/wikiwright/scripts/wikiwright.py scaffold nuget RandomNameGeneratorLibrary 2.3.0 --namespace RandomNameGeneratorLibrary --type PersonNameGenerator --children net48 -o verify/wiki-verify.cs
wrote <clone>\verify\wiki-verify.cs: 22,827 bytes (the template is 42,477)
kept: core, children; dropped: requests, fsharp, tool
children: net10.0, net48 (net10.0 always; add a config per end of a dependency range, new("net48", ["Autofac@9.3.4"]))
next:
  - run from a scratch folder outside any project, with TEMP and TMP set to it (C:/ paths from Git Bash): dotnet build wiki-verify.cs && dotnet run --no-build wiki-verify.cs > wiki-verify.out.txt
  - `dependencies`: the assembly names of the dependencies whose versions the pages name
  - `examples`: one whole program per C# block on the pages, as the page shows it, labelled by page

A flag for the other template is refused:

$ python skills/wikiwright/scripts/wikiwright.py scaffold npm get-title-at-url 3.0.0 --fsharp
error: --fsharp: not for scaffold npm
$ echo $?
2

The scripts it writes were not run for this wiki.

registry

registry NAME [--nuget | --npm] [--version X] [--limit N] [--no-nupkg] [--json]

One fact per line about a published package, in place of the raw registry JSON. Without a flag, a name with capitals is read from NuGet, a scoped name from npm, and a lower-case name from npm first, then NuGet.

Option What it does
--nuget, --npm read only that registry
--version X describe that version's package instead of the latest
--limit N show the newest N versions (default 40; 0 shows all)
--no-nupkg NuGet: skip downloading the .nupkg, so no file listing
--json the full survey as JSON

Download counts change daily; these are from the run on 2026-10-03 (the helper stamps the time in UTC):

$ python skills/wikiwright/scripts/wikiwright.py registry get-title-at-url --limit 3
npmjs.org get-title-at-url (a lower-case name is read from npm first; --nuget reads nuget.org)
license: MIT; maintainers: markrogers
repository: git+https://github.com/m4bwav/get-title-at-url.git
homepage: https://github.com/m4bwav/get-title-at-url#readme
description: Get the title of the web page at a URL. Zero dependencies, TypeScript, ESM and CommonJS, Node 20+.
dist-tags: latest 3.0.0
versions: 33 (0 deprecated), latest 3.0.0
  ... 30 older version(s) not shown (--limit 0 shows all); the first, 1.0.0, on 2016-05-06
  2022-11-29  2.0.0
  2026-09-25  3.0.0-beta.1, 3.0.0
3.0.0 engines: node >=20
3.0.0 type module; main ./dist/index.cjs; types ./dist/index.d.cts; exports ., ./package.json; bin get-title-at-url
3.0.0 dependencies: none
3.0.0 unpacked 140555 bytes, 11 files; provenance yes
downloads last week: 544 (2026-09-25 to 2026-10-01)
downloads last week by version: 3.0.0 193, 2.0.0 115, 1.1.8 26, 3.0.0-beta.1 25, 1.1.6 8, 1.1.5 3, 1.1.7 2, 1.1.0 1, and 5 more version(s) with 5; 20 version(s) none
read 2026-10-04 03:24 UTC: 3 requests, 17352 bytes on the wire (76825 decoded)
$ python skills/wikiwright/scripts/wikiwright.py registry RandomNameGeneratorLibrary --limit 3 --no-nupkg
nuget.org RandomNameGeneratorLibrary (Random Name Generator)
authors: Mark Rogers; owners: rogersm0; license: MIT
project: https://github.com/m4bwav/DotNetRandomNameGenerator
description: Generates random people and place names drawn from freely available US census data, and the names of real stars from the IAU star-name list and the Bright Star, Henry Draper and Hipparcos catalogues....
tags: random, name, generator, census, person, place, star, astronomy, fake, test-data
versions: 16 (4 unlisted), latest 2.3.0; total downloads 3894453
  ... 13 older version(s) not shown (--limit 0 shows all); the first, 1.0.0, on an unknown date
  2.2.0         2026-09-27  listed  97 downloads
  2.3.0-beta.1  2026-09-28  listed  40 downloads
  2.3.0         2026-09-28  listed  120 downloads
2.3.0 dependencies net10.0: none
2.3.0 dependencies .NETStandard2.0: none
read 2026-10-04 03:25 UTC: 2 requests, 3743 bytes on the wire (36660 decoded)

Without --no-nupkg it also downloads the package and lists its lib/ folders, README, icon, size and repository commit. --json was not run for this wiki.

cachecheck

cachecheck [--source DIR] [--cache DIR]

Compares every tracked file under skills/ and .claude-plugin/ in the source with the installed copy, by SHA-256. The source defaults to the repository holding the script, the cache to wikiwright's install path in ~/.claude/plugins/installed_plugins.json. A copy with one changed file:

$ mkdir cache && cp -r .claude-plugin skills cache/
$ echo changed >> cache/skills/wikiwright/SKILL.md
$ python skills/wikiwright/scripts/wikiwright.py cachecheck --cache cache
source: <clone>
cache:  cache
differs: skills/wikiwright/SKILL.md
cachecheck: 32 files, 31 equal, 1 differ, 0 missing, 0 only in the cache
       reinstall: claude plugin uninstall wikiwright@<marketplace>, then claude plugin install wikiwright@<marketplace> (update keeps a stale copy while the version is unchanged, L-012)
$ echo $?
1

releasecheck

releasecheck X.Y.Z [--root DIR]

For maintainers, before tagging a release: the version in .claude-plugin/plugin.json, evergreen.json, VERSION in wikiwright.py and metadata.version in SKILL.md, a CHANGELOG entry naming the version, a TESTS.md run that the last tag did not have, and a free tag. On master today, where 0.9.0 is already tagged:

$ python skills/wikiwright/scripts/wikiwright.py releasecheck 0.9.0
ok   .claude-plugin/plugin.json version: 0.9.0
ok   evergreen.json version: 0.9.0
ok   wikiwright.py VERSION: 0.9.0
ok   SKILL.md metadata.version: 0.9.0
ok   CHANGELOG entry naming 0.9.0: C-20261001-7 · 2026-10-01 · Release 0.9.0: the ninth and tenth runs, referen
FAIL TESTS run since v0.9.0: none
FAIL tag v0.9.0 exists already
releasecheck 0.9.0: 2 problem(s)
$ echo $?
1
$ python skills/wikiwright/scripts/wikiwright.py releasecheck 1.0.0
FAIL .claude-plugin/plugin.json version: 0.9.0
FAIL evergreen.json version: 0.9.0
FAIL wikiwright.py VERSION: 0.9.0
FAIL SKILL.md metadata.version: 0.9.0
FAIL CHANGELOG entry naming 1.0.0
FAIL TESTS run since v0.9.0: none
releasecheck 1.0.0: 6 problem(s)

Clone this wiki locally