Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
"url": "https://profullstack.com"
},
"plugins": [
{
"name": "tools",
"description": "Install and manage the cli-tools command set: put every command on PATH, keep the checkout current, and wire up the moshcode pit aliases.",
"source": "./plugins/tools",
"category": "productivity",
"author": {
"name": "profullstack",
"url": "https://profullstack.com"
},
"homepage": "https://github.com/profullstack/cli-tools#install",
"keywords": ["cli", "install", "path", "aliases", "moshcode"]
},
{
"name": "blog",
"description": "Write, check and publish posts on the plain-HTML blog: next post number, smolweb-valid template, index listing and feed regeneration, with a lint that catches the mistakes that silently break RSS.",
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
node_modules/
*.log
.DS_Store

# Your blog identity: byline, rel="me" links, analytics and ad account ids.
# Belongs in ~/.config/cli-tools/blog.json, never in the repository.
blog.config.json

# Local environment and credentials, in every form they usually turn up in.
.env
.env.*
!.env.example
*.pem
*.key
104 changes: 92 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ TypeScript, installed as executables on `PATH`.

| Command | What it does |
| --- | --- |
| [`cli-tools`](#install) | The dispatcher: list, update, link, and the pit aliases |
| [`gh-prs`](#gh-prs) | List every open PR across the owners you name |
| [`gh-prs-merge`](#gh-prs-merge) | Squash-merge the PRs that are genuinely ready |
| [`gh-prs-fix-all`](#gh-prs-fix-all) | Fix the open threatcrush-scan PRs that are broken because of us |
| [`tcfeed`](#tcfeed) | Find repositories worth scanning, scan them, print a shortlist |
| [`domainjson`](#domainjson) | whois-style, JSON-first name lookup |
| [`blog-post`](#blog-post) | Publish to the plain-HTML blog without breaking the feed |
| [`domainfree`](#domainfree) | Which of these domains you can actually register |
| [`blog-post`](#blog-post) | Publish to a plain-HTML blog without breaking the feed |

## Requirements

Expand All @@ -23,12 +25,39 @@ TypeScript, installed as executables on `PATH`.

## Install

```sh
curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/install.sh | sh
```

That clones to `~/.local/share/cli-tools`, installs dependencies, and symlinks
every command into `~/.local/bin`. `CLI_TOOLS_HOME` and `CLI_TOOLS_PREFIX`
override both. If a checkout already owns these command names, the installer
updates *that* one rather than cloning a second copy beside it.

With moshcode on the box, the same thing:

```sh
moshcode install cli-tools # then /cli-tools … in the pit
```

Check what landed, and wire up the pit aliases:

```sh
cli-tools list # a * marks each command found on PATH
cli-tools aliases --install # /blog /free /merge /prs /whois
cli-tools update # git pull, reinstall, relink
```

<details>
<summary>From a clone, for development</summary>

```sh
git clone git@github.com:profullstack/cli-tools.git ~/src/profullstack/cli-tools
cd ~/src/profullstack/cli-tools
pnpm install
pnpm link:bin
```
</details>

`link:bin` symlinks every `bin/*.ts` into `~/.local/bin` without the extension,
so `gh-prs-merge` is a real command. (Not named `link` — that is a pnpm builtin,
Expand Down Expand Up @@ -71,8 +100,8 @@ and URL become clickable.

```sh
gh-prs --orgs profullstack,moshcoder,h4kr,infernetprotocol
gh-prs --users ralyodio
gh-prs --orgs profullstack --users ralyodio --limit 50
gh-prs --users octocat
gh-prs --orgs profullstack --users octocat --limit 50
gh-prs --orgs profullstack --no-links # plain text, for piping
```

Expand Down Expand Up @@ -226,12 +255,49 @@ blog-post new "A title" --description "..." --body draft.html
blog-post check # posts that will break the feed
blog-post list # every post with its date
blog-post feed # regenerate feed.xml
blog-post config # where your identity is read from, and what is in effect
```

`new` picks the next `NNN-post.html`, renders the smolweb-valid template with
the AI-drafting acknowledgment, splices the entry into the hand-maintained
`index.html`, and runs the blog's own `build-feed.mjs`. Point it elsewhere with
`--dir` or `$BLOG_DIR`.
`new` picks the next `NNN-post.html`, renders the smolweb-valid template,
splices the entry into the hand-maintained `index.html`, and runs the blog's own
`build-feed.mjs`. Point it elsewhere with `--dir` or `$BLOG_DIR`.

#### Your identity is configuration, not code

Nothing about *you* is baked into this repository. The byline, the site name,
the `rel="me"` links and any analytics or ad ids come from a config file, and
with none present a post renders with no byline, no identity links and **no
third-party scripts at all** — which is the only fully smolweb-valid output.

Copy [`blog.config.example.json`](blog.config.example.json) to whichever of
these suits, most specific first:

| Path | Use it for |
| --- | --- |
| `$BLOG_CONFIG` | a one-off, or CI |
| `<blog dir>/blog.config.json` | a second blog with its own identity |
| `~/.config/cli-tools/blog.json` | your own blog — the usual answer |

```json
{
"siteTitle": "Your Blog",
"author": "Your Name",
"disclosure": "<strong>How this was written:</strong> drafted with an AI assistant, then edited by me.",
"links": [{ "label": "Mastodon", "href": "https://example.social/@you" }],
"trackerSiteId": null,
"adSlotId": null
}
```

`BLOG_SITE_TITLE`, `BLOG_AUTHOR`, `BLOG_DISCLOSURE`, `CRAWLPROOF_SITE_ID`,
`CRAWLPROOF_AD_SLOT` and `CRAWLPROOF_AD_FORMAT` override the file. `links` is
the only field with no environment equivalent.

`trackerSiteId` and `adSlotId` are **accounts, not settings**: leave them null
unless they are yours. A shared id would meter your readers' pageviews and your
ad impressions into somebody else's account, which is why they are not defaults.

Run `blog-post config` to see which file was picked up and what it resolved to.

What it refuses to do:

Expand All @@ -250,19 +316,33 @@ missing `<h1>`, and exits non-zero, so it works as a pre-publish gate.

## As a moshcode plugin

This repo is also a plugin marketplace, exposing `blog-post` as slash commands:
This repo is also a plugin marketplace:

```sh
moshcode plugin marketplace add profullstack/cli-tools
moshcode plugin install blog@cli-tools
moshcode plugin install tools@cli-tools # /tools:install, /tools:list
moshcode plugin install blog@cli-tools # /blog:post, :check, :list, :feed
moshcode plugin install domain@cli-tools # /domain:free, /domain:lookup
```

That adds `/blog:post`, `/blog:check`, `/blog:list` and `/blog:feed`. See
[plugins/blog](plugins/blog/README.md).
See [plugins/tools](plugins/tools/README.md), [plugins/blog](plugins/blog/README.md)
and [plugins/domain](plugins/domain/README.md).

`cli-tools` is also a moshcode workflow tool, so the whole set installs and
updates through moshcode itself:

```sh
moshcode install cli-tools # then /cli-tools list, /cli-tools update
```

## Aliases

Pit aliases live in `~/.moshcode/aliases.json`:
Pit aliases live in `~/.moshcode/aliases.json`. `cli-tools aliases --install`
writes a thin default set (`/blog`, `/free`, `/merge`, `/prs`, `/whois`),
merging rather than replacing — an alias you bound yourself is kept and the
collision is reported. `cli-tools aliases` prints them without writing anything.

To manage them by hand:

```
/alias set prs "gh-prs --orgs profullstack"
Expand Down
40 changes: 34 additions & 6 deletions bin/blog-post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@ import {
lint,
readPosts,
} from '../src/blog.ts';
import { configPaths, loadBlogConfig } from '../src/blog-config.ts';

const USAGE = `Usage:
blog-post new <title> --description <text> [--body file.html] [--date ISO]
blog-post check
blog-post list
blog-post feed
blog-post config

Commands:
new Write the next post, list it in index.html, rebuild the feed
check Report posts that will break the feed (non-zero exit if any)
list Every post with its date
feed Regenerate feed.xml
config Where the blog identity is read from, and what is in effect

Options:
--description TEXT Feed summary. Required by \`new\`.
Expand Down Expand Up @@ -126,12 +129,19 @@ export async function run(argv: readonly string[]): Promise<number> {
const bodyFile = values.get('--body');
const body = bodyFile ? await readFile(bodyFile, 'utf8') : '';

const { file, path } = await createPost(dir, {
title,
description,
date: isoSeconds(when),
body,
});
const config = await loadBlogConfig(dir);
if (!config.author) {
process.stderr.write(
'note: no blog config found, so this post has no byline and no identity links.\n' +
` Write one to ${configPaths(dir).at(-1)} — see \`blog-post config\`.\n`,
);
}

const { file, path } = await createPost(
dir,
{ title, description, date: isoSeconds(when), body },
config,
);

process.stdout.write(`created ${file}\n ${path}\n listed in index.html\n`);
return rebuildFeed(dir);
Expand All @@ -157,6 +167,24 @@ export async function run(argv: readonly string[]): Promise<number> {
return 0;
}

case 'config': {
const config = await loadBlogConfig(dir);
const paths = configPaths(dir);
process.stdout.write('Config is read from the first of these that exists:\n');
for (const path of paths) {
process.stdout.write(` ${existsSync(path) ? '*' : ' '} ${path}\n`);
}
process.stdout.write(`\nIn effect:\n${JSON.stringify(config, null, 2)}\n`);
if (!config.author) {
process.stdout.write(
'\nNothing is configured, so posts render with no byline, no identity links\n' +
'and no third-party scripts. Copy blog.config.example.json to\n' +
`${paths.at(-1)} and fill it in.\n`,
);
}
return 0;
}

case 'feed':
return rebuildFeed(dir);

Expand Down
Loading
Loading