@natjswenson/devlog 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/SKILL.md +72 -7
- package/lib/config_ops.mjs +1 -0
- package/lib/core.mjs +13 -0
- package/lib/publish_entry.mjs +15 -1
- package/lib/scan.mjs +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@natjswenson/devlog` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.13.0 (2026-07-29) — a publish run now proves the entry is live
|
|
6
|
+
|
|
7
|
+
Publishing reported success on a post that was never reachable. The content, cover
|
|
8
|
+
and manifest all landed correctly, the push succeeded, every command returned `ok`,
|
|
9
|
+
and the entry 404'd for an hour because the site's own project registry had never
|
|
10
|
+
been told the project existed. A push is not a route, and this release stops the
|
|
11
|
+
skill treating the two as the same thing.
|
|
12
|
+
|
|
13
|
+
- **Added: `publish-entry` returns `firstEntryForProject`.** True when the call
|
|
14
|
+
created a project's first live entry, which is exactly when a rendering site needs
|
|
15
|
+
a new registry row. A project holding only tombstones still counts as first, since
|
|
16
|
+
a retired entry never implied a route. Previously nothing distinguished a project's
|
|
17
|
+
first publish from its fiftieth, so the one moment registration is required looked
|
|
18
|
+
identical to every run where it isn't.
|
|
19
|
+
- **Added: `siteUrl` config field** (settable, optional, trailing slash stripped,
|
|
20
|
+
http(s) only) plus `siteUrl` in the scan output. The repo name is not the site name
|
|
21
|
+
— a repo named `example.io` is commonly served at `example.com` — so a publish run
|
|
22
|
+
had no way to reach the page it had just published.
|
|
23
|
+
- **Added: SKILL.md Generate step 5b (register a project the site has never
|
|
24
|
+
rendered).** Find the site's registry, add the project, and *build the clone to
|
|
25
|
+
confirm the route exists* before pushing. Also warns that registry position can
|
|
26
|
+
decide feed order: a site sorting by date alone falls back to load order for
|
|
27
|
+
same-date entries, so a project appended last can render its newest entry last.
|
|
28
|
+
- **Added: SKILL.md Generate step 6 (verify the entry is live).** Poll the published
|
|
29
|
+
URL until it returns 200 before reporting success; on a persistent 404, check the
|
|
30
|
+
registry first, then the host's build log, and report it as **not live**. With no
|
|
31
|
+
`siteUrl` configured, the run must say the entry is pushed but unverified rather
|
|
32
|
+
than call it published.
|
|
33
|
+
- **Changed: the confirmation block leads with the verified live URL**, with the repo
|
|
34
|
+
blob URL demoted to secondary, and prints the failure on the `Live:` line rather
|
|
35
|
+
than a URL that may 404.
|
|
36
|
+
|
|
5
37
|
## 0.12.0 (2026-07-28) — five composition slots; stop defaulting to before/after
|
|
6
38
|
|
|
7
39
|
Minor rather than patch: this adds three new named composition slots, so covers
|
package/SKILL.md
CHANGED
|
@@ -41,12 +41,19 @@ Map the user's request onto the CLI — never hand-edit `config.json`:
|
|
|
41
41
|
| Show config | `npx -y @natjswenson/devlog@latest config --json` |
|
|
42
42
|
| Add a project | `npx -y @natjswenson/devlog@latest add-project --yes --path <abs-path> [--key K] [--remote O/R] [--label L] [--tag-prefix P] [--path-filter F] [--private]` |
|
|
43
43
|
| Remove a project | `npx -y @natjswenson/devlog@latest remove-project <key> --yes` |
|
|
44
|
-
| Change a setting | `npx -y @natjswenson/devlog@latest set <field> <value>` (settable: `targetRepo`, `branch`, `targetDir`, `gitAuthor`, `githubUser`, `voicePath`, `deepDive.minSources`, `deepDive.topicDomains`) |
|
|
44
|
+
| Change a setting | `npx -y @natjswenson/devlog@latest set <field> <value>` (settable: `targetRepo`, `branch`, `targetDir`, `gitAuthor`, `githubUser`, `siteUrl`, `voicePath`, `deepDive.minSources`, `deepDive.topicDomains`) |
|
|
45
45
|
|
|
46
46
|
`targetDir` is the subdirectory of `targetRepo` that holds the devlog content tree
|
|
47
47
|
(e.g. `content/devlog` when the target is the site repo itself); unset/empty means the
|
|
48
48
|
repo root. Set it with `set targetDir content/devlog`, clear it with `set targetDir ''`.
|
|
49
49
|
|
|
50
|
+
`siteUrl` is the public base URL that repo is *served* at, with no trailing slash — used
|
|
51
|
+
to verify a published entry actually renders (Generate step 6). **The repo name is not the
|
|
52
|
+
site name**: a repo called `example.io` is commonly served at `example.com`, so take this
|
|
53
|
+
from a working page, not from the repo. Without it a run can push but cannot confirm
|
|
54
|
+
anything went live. Set with `set siteUrl 'https://example.com'`, clear with
|
|
55
|
+
`set siteUrl ''`.
|
|
56
|
+
|
|
50
57
|
For **add-project**: resolve the path first (the repo the user named, or the cwd), then
|
|
51
58
|
detect what the CLI will use — key = directory basename, remote = `git -C '<path>' remote
|
|
52
59
|
get-url origin`. In a monorepo, suggest a `--path-filter` (the project's subdir) and a
|
|
@@ -412,7 +419,65 @@ rm -rf '<abs-tmp>'
|
|
|
412
419
|
|
|
413
420
|
If the push fails, report the error and stop — do not retry automatically.
|
|
414
421
|
|
|
415
|
-
### Step
|
|
422
|
+
### Step 5b: Register a project the site has never rendered before
|
|
423
|
+
|
|
424
|
+
`publish-entry` returns **`firstEntryForProject: true`** when the call created that
|
|
425
|
+
project's first live entry. **A push is not a route.** A site that renders the devlog
|
|
426
|
+
almost always keeps its own registry deciding *which* content directories become pages;
|
|
427
|
+
content on disk that the registry doesn't list is built right past, and the entry 404s
|
|
428
|
+
while every command in this skill reports success. Publishing a project's first entry is
|
|
429
|
+
therefore a two-part job, and the CLI can only do the first part.
|
|
430
|
+
|
|
431
|
+
When `firstEntryForProject` is true, **before the push**:
|
|
432
|
+
|
|
433
|
+
1. Find the registry in the clone. It is a source file, not content — grep the site's
|
|
434
|
+
`src/` for the existing project keys (`grep -rn '<a-known-project-key>' <clone>/src`).
|
|
435
|
+
In an Astro/Next-style site it is typically a `PROJECTS` array the entry loader maps
|
|
436
|
+
over.
|
|
437
|
+
2. Add the new project, matching the shape of the neighbouring rows exactly.
|
|
438
|
+
3. **Build the site in the clone and confirm the route exists** (`npm ci && npx astro
|
|
439
|
+
build`, then check the output directory contains the new `<project>/<version>` path).
|
|
440
|
+
A registry edit that doesn't produce a route is not done.
|
|
441
|
+
4. Run the site's own test suite if it has one, and commit the registry change together
|
|
442
|
+
with the content in the same push.
|
|
443
|
+
|
|
444
|
+
If you cannot find a registry, say so plainly rather than assuming there isn't one; the
|
|
445
|
+
verification in Step 6 is what actually settles it.
|
|
446
|
+
|
|
447
|
+
**Registry position can decide feed order, not just visibility.** If the site sorts
|
|
448
|
+
entries by date alone, same-date entries fall back to their load order, which is registry
|
|
449
|
+
order — so a project appended to the end can render its newest entry last. After the build
|
|
450
|
+
in step 3, check that the new entry appears where its date and publish numeral say it
|
|
451
|
+
should, and report it if not.
|
|
452
|
+
|
|
453
|
+
### Step 6: Verify the entry is actually live
|
|
454
|
+
|
|
455
|
+
**Do not report a publish as successful until the published URL returns 200.** The push
|
|
456
|
+
succeeding, the manifest updating, and the site serving the post are three different
|
|
457
|
+
things, and this skill has previously reported success on all of the first two while the
|
|
458
|
+
post 404'd.
|
|
459
|
+
|
|
460
|
+
When `siteUrl` is set in config (it is echoed in the scan output):
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
curl -s -o /dev/null -w '%{http_code}' -L '<siteUrl>/devlog/<project>/<version>/'
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Poll every ~45s for up to ~5 minutes; deploys are not instant. Then:
|
|
467
|
+
|
|
468
|
+
- **200** → done, report the live URL as the primary link.
|
|
469
|
+
- **Still 404 after the window** → the deploy failed or the entry is not routed. Check
|
|
470
|
+
`firstEntryForProject`/the registry from Step 5b first, since that is the most common
|
|
471
|
+
cause, then the host's build log. Report it as **not live**, with what you checked.
|
|
472
|
+
- **`siteUrl` unset** → say the entry is pushed but unverified, give the repo URL, and
|
|
473
|
+
offer to set it: `npx -y @natjswenson/devlog@latest set siteUrl 'https://example.com'`.
|
|
474
|
+
Never describe an unverified push as published.
|
|
475
|
+
|
|
476
|
+
The `/devlog/<project>/<version>/` path is this skill's default convention. If the site
|
|
477
|
+
routes differently, take the pattern from a URL of an already-published entry rather than
|
|
478
|
+
assuming this one.
|
|
479
|
+
|
|
480
|
+
### Step 7: Confirm
|
|
416
481
|
|
|
417
482
|
```
|
|
418
483
|
Release dev log entries published
|
|
@@ -420,13 +485,13 @@ Release dev log entries published
|
|
|
420
485
|
Project: <key>
|
|
421
486
|
Releases: <version>, ...
|
|
422
487
|
Judged weaknesses: <residuals from Step 4, or "none">
|
|
423
|
-
|
|
488
|
+
Live: <siteUrl>/devlog/<key>/<version>/ (verified 200)
|
|
489
|
+
Source: https://github.com/<targetRepo>/blob/<branch>/<targetDir-prefix><key>/<version>.md
|
|
424
490
|
```
|
|
425
491
|
|
|
426
|
-
(`<targetDir-prefix>` is `<targetDir>/` when set, empty otherwise.)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
next deploy.
|
|
492
|
+
(`<targetDir-prefix>` is `<targetDir>/` when set, empty otherwise.) Report the **verified
|
|
493
|
+
live URL** as the primary link; the repo blob URL is the secondary one. If Step 6 could
|
|
494
|
+
not verify, say so on the `Live:` line instead of printing a URL that may 404.
|
|
430
495
|
|
|
431
496
|
## Security rules
|
|
432
497
|
|
package/lib/config_ops.mjs
CHANGED
|
@@ -34,6 +34,7 @@ const SETTERS = {
|
|
|
34
34
|
targetDir: (c, v) => (v === '' ? omit(c, 'targetDir') : { ...c, targetDir: v }),
|
|
35
35
|
gitAuthor: (c, v) => ({ ...c, gitAuthor: v }),
|
|
36
36
|
githubUser: (c, v) => ({ ...c, githubUser: v }),
|
|
37
|
+
siteUrl: (c, v) => (v === '' ? omit(c, 'siteUrl') : { ...c, siteUrl: v.replace(/\/+$/, '') }),
|
|
37
38
|
voicePath: (c, v) => (v === '' ? omit(c, 'voicePath') : { ...c, voicePath: expandHome(v) }),
|
|
38
39
|
'deepDive.minSources': (c, v) => {
|
|
39
40
|
const n = Number(v);
|
package/lib/core.mjs
CHANGED
|
@@ -125,6 +125,19 @@ export function validateConfig(config) {
|
|
|
125
125
|
throw new Error(`targetDir must be a relative path like "content/devlog" (no leading/trailing slash, no '..'): got ${JSON.stringify(config.targetDir)}`);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
if ('siteUrl' in config) {
|
|
129
|
+
// Optional: the public base URL the target repo is served at, used only to
|
|
130
|
+
// confirm a published entry actually renders. The repo name is not the site
|
|
131
|
+
// name (a repo called `example.io` can be served at `example.com`), and
|
|
132
|
+
// pushing content is not the same as the site serving it, so this is the
|
|
133
|
+
// only way a publish run can verify itself rather than assume.
|
|
134
|
+
// Interpolated into a shell curl, hence the tight charset.
|
|
135
|
+
if (typeof config.siteUrl !== 'string'
|
|
136
|
+
|| !/^https?:\/\/[A-Za-z0-9.-]+(:\d+)?(\/[A-Za-z0-9._~/-]*)?$/.test(config.siteUrl)
|
|
137
|
+
|| config.siteUrl.endsWith('/')) {
|
|
138
|
+
throw new Error(`siteUrl must be an http(s) URL with no trailing slash, e.g. "https://example.com": got ${JSON.stringify(config.siteUrl)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
128
141
|
if ('voicePath' in config) {
|
|
129
142
|
// Optional: directory holding the voice profile used to write entries. Read by
|
|
130
143
|
// the skill with the Read tool only — never shell-interpolated — so the only
|
package/lib/publish_entry.mjs
CHANGED
|
@@ -221,6 +221,14 @@ export function publishEntry({ cloneDir, project, version, entryPath, coverImage
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
const file = `${version}.md`;
|
|
224
|
+
// Whether this project had any live entry before this call. A site that
|
|
225
|
+
// renders the devlog typically keeps its own registry of which projects to
|
|
226
|
+
// render (content on disk is not the same as a route), and that registry is
|
|
227
|
+
// the thing a first-ever publish silently misses: the entry lands, the build
|
|
228
|
+
// walks past it, and the post 404s while every check reports success.
|
|
229
|
+
// Reported back so the caller can require registration exactly once per
|
|
230
|
+
// project, instead of relying on whoever is publishing to remember.
|
|
231
|
+
const hadLiveEntries = manifest.entries.some((e) => e && !e.removed);
|
|
224
232
|
// Idempotent: legacy manifests may already reference this file/version even
|
|
225
233
|
// when the .md was missing — never duplicate an index row.
|
|
226
234
|
const already = manifest.entries.some((e) => e && (e.file === file || e.version === version));
|
|
@@ -247,7 +255,13 @@ export function publishEntry({ cloneDir, project, version, entryPath, coverImage
|
|
|
247
255
|
manifestUpdated = true;
|
|
248
256
|
}
|
|
249
257
|
|
|
250
|
-
return {
|
|
258
|
+
return {
|
|
259
|
+
written: destPath,
|
|
260
|
+
manifestUpdated,
|
|
261
|
+
coverWritten: !!coverFile,
|
|
262
|
+
no,
|
|
263
|
+
firstEntryForProject: manifestUpdated && !hadLiveEntries,
|
|
264
|
+
};
|
|
251
265
|
}
|
|
252
266
|
|
|
253
267
|
// Editorially retire a release: after an entry is manually moved, consolidated,
|
package/lib/scan.mjs
CHANGED
|
@@ -288,6 +288,11 @@ export function scanAll(config, { projectKey = null, fetch = true, getExisting =
|
|
|
288
288
|
// skill appends it to the publish clone path (`--clone <clone>/<targetDir>`).
|
|
289
289
|
targetDir: config.targetDir || '',
|
|
290
290
|
branch,
|
|
291
|
+
// Public base URL the target repo is served at, when configured — the skill
|
|
292
|
+
// uses it to confirm a published entry actually renders. null means the run
|
|
293
|
+
// can push but cannot verify, which the skill has to say out loud rather
|
|
294
|
+
// than report an unverified publish as a success.
|
|
295
|
+
siteUrl: config.siteUrl || null,
|
|
291
296
|
deepDive: resolveDeepDive(config),
|
|
292
297
|
voicePath: config.voicePath || null,
|
|
293
298
|
projects: results,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@natjswenson/devlog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Release dev log generator \u2014 Claude Code skill + preview app for publishing version-release dev logs, written in your voice, to your site",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nate Swenson",
|