` closed, no bare ``, no unclosed tags. The template handles the shell; your body has to hold up its end. diff --git a/plugins/tools/.claude-plugin/plugin.json b/plugins/tools/.claude-plugin/plugin.json new file mode 100644 index 0000000..99ee3c9 --- /dev/null +++ b/plugins/tools/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://anthropic.com/claude-code/plugin.schema.json", + "name": "tools", + "description": "Install and manage the cli-tools command set: put every command on PATH, keep it current, and wire up the moshcode pit aliases.", + "version": "0.1.0", + "author": { + "name": "profullstack", + "url": "https://profullstack.com" + }, + "homepage": "https://github.com/profullstack/cli-tools#install", + "license": "MIT", + "keywords": ["cli", "install", "path", "aliases", "moshcode"] +} diff --git a/plugins/tools/README.md b/plugins/tools/README.md new file mode 100644 index 0000000..b2bb886 --- /dev/null +++ b/plugins/tools/README.md @@ -0,0 +1,37 @@ +# tools + +Install and manage the `cli-tools` command set. + +`/tools:install` puts every command on `PATH` and wires up the moshcode pit +aliases. `/tools:list` says which checkout they run from and what actually +landed. + +## Install + +```bash +moshcode plugin marketplace add profullstack/cli-tools +moshcode plugin install tools@cli-tools +``` + +Or install the commands directly, without the plugin: + +```bash +curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/install.sh | sh +cli-tools aliases --install +``` + +## The thing worth knowing + +The installed commands are **symlinks into a working tree**, not a copied build. +Whatever branch the checkout sits on is the code that runs, so a merged PR does +not update the installed command and a checkout parked on an old branch produces +stale output with no warning. `cli-tools where` names the checkout; `cli-tools +update` pulls and relinks it, refusing to move a dirty or diverged tree rather +than discarding work. + +The commands are real executables rather than shell functions because a file +works from every caller — an interactive shell, `zsh -c`, a systemd unit, a CI +step — without anything having been sourced first. The pit aliases this installs +are a shorter word for a longer invocation, never what makes a command +reachable, and none of them shares a name with a command: a function beats +`PATH`, so a wrapper of the same name would silently shadow the file. diff --git a/plugins/tools/commands/install.md b/plugins/tools/commands/install.md new file mode 100644 index 0000000..936b1a4 --- /dev/null +++ b/plugins/tools/commands/install.md @@ -0,0 +1,110 @@ +--- +description: Install the cli-tools command set onto PATH, and wire up the pit aliases. +allowed-tools: Bash(cli-tools:*), Bash(curl:*), Bash(sh:*), Bash(moshcode:*), Read +--- + +## Task + +Put every `cli-tools` command on `PATH` and make it reachable from the moshcode +pit. + +If `cli-tools` is not installed yet, one line does the whole thing: + +```bash +curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/install.sh | sh +``` + +Or, if moshcode is already on the box: + +```bash +moshcode install cli-tools +``` + +Then wire up the pit aliases: + +```bash +cli-tools aliases --install +``` + +## What lands where + +The installer clones to `~/.local/share/cli-tools` (override with +`CLI_TOOLS_HOME`), installs dependencies, and symlinks every command into +`~/.local/bin` (override with `CLI_TOOLS_PREFIX`): + +| Command | What it does | +| --- | --- | +| `blog-post` | Publish to a plain-HTML blog without breaking the feed | +| `cli-tools` | This dispatcher | +| `domainfree` | Which of these domains you can actually register | +| `domainjson` | whois-style, JSON-first name lookup | +| `gh-prs` | Every open PR across the owners you name | +| `gh-prs-fix-all` | Repair the open scan PRs that are broken because of us | +| `gh-prs-merge` | Squash-merge the PRs that are genuinely ready | +| `tcfeed` | Find repositories worth scanning, scan them, print a shortlist | + +Check what took: + +```bash +cli-tools list # a * marks each command found on PATH +``` + +## If it says a command is not on PATH + +Two causes, and `cli-tools list` tells them apart from the rest of the output. + +**`~/.local/bin` is not on `PATH`.** The installer warns about this at the end. +Add it to your shell profile: + +```bash +export PATH="$HOME/.local/bin:$PATH" +``` + +**The name is already taken by another checkout.** A symlink pointing at a +different clone is left alone, because taking it over silently would change +which code runs. `cli-tools link --force` takes over a *symlink*; a real file of +that name is refused either way. + +## Aliases are a convenience, not the mechanism + +`cli-tools aliases --install` merges these into `~/.moshcode/aliases.json`: + +| Alias | Expands to | +| --- | --- | +| `/blog` | `blog-post` | +| `/free` | `domainfree` | +| `/merge` | `gh-prs-merge --apply` | +| `/prs` | `gh-prs` | +| `/whois` | `domainjson` | + +An alias you already bound is never overwritten — the collision is reported and +yours is kept. The pit re-reads the file on every lookup, so an open pit picks +them up with no restart. Arguments append rather than substitute, so +`/merge --limit 5` works. + +None of these shares a name with a command, deliberately. A shell function beats +`PATH`, so an alias named after the file it wraps silently shadows it and the +two drift apart. Keep them thin for the same reason `/merge` carries only +`--apply`: `gh-prs-merge` already repairs by default under `--apply`, and baking +`--fix` in as well is what once made `/merge --fix` expand to +`--apply --fix --fix`. + +The commands are reachable from the pit whether or not you install any of this. +They are real executables on `PATH` because a file works from every caller — an +interactive shell, `zsh -c`, a systemd unit, a CI step — without anything having +been sourced first. The aliases only buy you a shorter word. + +## Keeping it current + +```bash +cli-tools update # git pull, reinstall dependencies, relink +``` + +`update` refuses to move a dirty or diverged checkout rather than discarding +work. If it stops, sort the checkout out at `cli-tools where` and retry. + +Note that the installed command runs **whatever branch the checkout is on** — +these are symlinks into a working tree, not a copied build. A checkout parked on +an old branch silently runs old code, so `cli-tools where` and a `git branch +--show-current` there are the first two things to check when a command behaves +like a version you do not recognise. diff --git a/plugins/tools/commands/list.md b/plugins/tools/commands/list.md new file mode 100644 index 0000000..8e616c7 --- /dev/null +++ b/plugins/tools/commands/list.md @@ -0,0 +1,36 @@ +--- +description: What cli-tools installs, which checkout it runs from, and what is on PATH. +allowed-tools: Bash(cli-tools:*), Read +--- + +## Task + +Report the state of the installed command set. + +```bash +cli-tools list # a * marks each command found on PATH +cli-tools list --json # the same, machine-readable +cli-tools where # the checkout the commands run from +``` + +## Reading it + +The first line is the checkout. That is the answer to most surprises here, +because these commands are **symlinks into a working tree**, not a copied +build: the command on `PATH` runs whatever branch that checkout happens to be +on. A tree parked on a stale branch silently produces stale output, and a flag +added by a merged PR answers `unknown option` until someone pulls. + +So when a command behaves like a version you do not recognise, check the +checkout before reading its source: + +```bash +cli-tools where +git -C "$(cli-tools where)" branch --show-current +git -C "$(cli-tools where)" log --oneline HEAD..origin/master +``` + +`cli-tools update` fixes the common case. + +A command with no `*` is not on `PATH` — see `/tools:install`, which covers both +causes. diff --git a/src/blog-config.ts b/src/blog-config.ts new file mode 100644 index 0000000..507da39 --- /dev/null +++ b/src/blog-config.ts @@ -0,0 +1,139 @@ +import { readFile } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +/** + * Who the blog belongs to, and which third-party ids its pages carry. + * + * None of this is a property of the *tool*, so none of it is baked into it. A + * byline, a Mastodon handle and an analytics site id are the author's, and a + * checkout that carried someone else's would publish their name on your posts + * and meter your pageviews and ad impressions into their account. So the + * defaults here are empty, every field is optional, and a post rendered without + * config is a clean post rather than a broken one: no byline, no identity + * links, and — the part that matters — no third-party scripts at all, which is + * the only configuration that is fully smolweb-valid. + */ + +export interface BlogLink { + label: string; + href: string; + /** Emitted as the anchor's `rel`. Defaults to `me`, which is what makes these verifiable. */ + rel?: string; +} + +export interface BlogConfig { + /** Site name, appended to each post's `
Find me: ${anchors.join(' ·\n')}
\n`; +} /** * Render a post file. @@ -128,21 +158,43 @@ export const AD_UNIT = [ * Deliberately smolweb-valid, which is stricter than "valid HTML": an explicit * ``, `` and ``; `` * rather than a bare ``, because every `` needs a `content` - * attribute; and every `` closed. The one exception is {@link TRACKER}, the - * external analytics tag, which smolweb's no-third-party-script rule forbids. + * attribute; and every `
` closed. The one exception is {@link tracker}, the + * external analytics tag, which smolweb's no-third-party-script rule forbids — + * and which is absent unless a site id is configured. + * + * Everything identifying the author comes from {@link BlogConfig}. Rendered + * with the default config the post carries no byline, no identity links and no + * third-party scripts, so a checkout cannot publish somebody else's name or + * meter traffic into an account it inherited from the repository. */ -export function renderPost({ title, description, date, body = '' }: NewPost): string { +export function renderPost( + { title, description, date, body = '' }: NewPost, + config: BlogConfig = EMPTY_CONFIG, +): string { const day = date.slice(0, 10); const heading = typogrify(title); const content = body.trim() || '
…
'; + // esc rather than typogrify: the site name is emitted identically in the + //${day}, by ${typogrify(config.author)}.
` + : `${day}
`; + const disclosure = config.disclosure + ? `\n\n${typogrify(config.disclosure)}
` + : ''; + const footer = adUnit(config); + return ` -${day}, by Anthony “chovy” Ettinger.
- -How this was written: drafted with an AI assistant from my own notes, -then edited by me.
+${byline}${disclosure}