From 2a4f674b26fa3fc0a55649b06a97d68972c3496f Mon Sep 17 00:00:00 2001 From: Charan Date: Sun, 30 Aug 2026 14:58:33 +0530 Subject: [PATCH 1/3] ci: automate npm publishing via trusted publishing (OIDC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Releases for @inkform/framework and @inkform/cli were a manual `npm publish` from a maintainer's laptop. This replaces that with a tag-driven workflow. Authentication is npm Trusted Publishing: GitHub mints a short-lived OIDC token scoped to this exact workflow file, and npm accepts it because each package's trusted-publisher settings name this repo and this filename. No NPM_TOKEN is stored in the repository, and every release carries a provenance attestation automatically. Push `framework-v` or `cli-v` to release that package. The job re-runs the full ci.yml gate against the tagged commit (a green PR check is not proof the tag is green), refuses a tag whose version disagrees with package.json, and refuses a version already on npm rather than surfacing npm's bare 403. workflow_dispatch rehearses the whole thing in dry-run. Hardening notes, since the repo is public: - No pull_request trigger. A workflow holding id-token: write must never run fork-supplied code. - permissions are contents:read + id-token:write, nothing more. - Every ${{ }} value is passed through env: and read as a shell variable. GitHub splices expansions in as raw text before bash parses the file, so an inline expansion in a run block is a script-injection sink. - The job runs in an `npm-publish` environment, so adding required reviewers there gates every publish on a human approval independently of npm. setup-node ships npm 10 with Node 22; trusted publishing needs >= 11.5.1, so the workflow upgrades npm explicitly — removing that step yields a confusing ENEEDAUTH. PUBLISHING.md is rewritten for this flow; it had gone stale claiming @inkform/framework had never been published. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012XWzjtTYZRNgD1YVM6pJ8u --- .github/workflows/release.yml | 160 ++++++++++++++++++++++++++ CONTRIBUTING.md | 15 +++ packages/framework/PUBLISHING.md | 185 +++++++++++++++++-------------- 3 files changed, 276 insertions(+), 84 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b4785ce --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,160 @@ +name: Release + +# Publishes @inkform/framework or @inkform/cli to public npm. +# +# Authentication is npm Trusted Publishing (OIDC) — there is NO npm token +# stored in this repository's secrets. GitHub mints a short-lived OIDC token +# for this specific workflow file, and npm accepts it only because the +# package's "Trusted publisher" settings on npmjs.com name this repo AND +# this exact filename (.github/workflows/release.yml). Renaming this file +# breaks publishing until the npm-side config is updated to match — that is +# the security property, not an accident. Publishes made this way also carry +# a provenance attestation automatically (the "Built and signed on GitHub +# Actions" badge on npm), with no --provenance flag needed. +# +# Trigger: push an annotated tag naming the package and its version. +# +# framework-v0.5.0 → publishes packages/framework +# cli-v0.5.0 → publishes packages/cli +# +# The tag's version MUST equal the version already committed in that +# package's package.json — the job refuses to guess. Bump, commit, push, +# THEN tag. workflow_dispatch runs the whole thing in --dry-run mode by +# default so you can rehearse a release without publishing anything. + +on: + push: + tags: + - 'framework-v*' + - 'cli-v*' + workflow_dispatch: + inputs: + package: + description: 'Which package to release' + required: true + type: choice + options: [framework, cli] + dry_run: + description: 'Dry run (pack and validate, publish nothing)' + required: true + type: boolean + default: true + +permissions: + contents: read + id-token: write # required: this is what lets npm verify the OIDC claim + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + + # Second gate, independent of npm. Add required reviewers to the + # "npm-publish" environment in Settings → Environments and every publish + # pauses for a human approval, so a tag push alone can never ship. + # Referencing an environment that doesn't exist yet is harmless. + environment: npm-publish + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + registry-url: 'https://registry.npmjs.org' + + # setup-node ships npm 10.x with Node 22; trusted publishing needs + # npm >= 11.5.1. Without this the publish falls back to looking for an + # auth token and fails with a confusing ENEEDAUTH. + - name: Use an npm that supports trusted publishing + run: | + npm install -g npm@latest + npm --version + + # Every ${{ }} value below is passed through `env:` and read as a + # shell variable, never expanded into the script body. GitHub splices + # expansions in as raw text before bash ever sees the file, so an + # inline `'${{ github.ref_name }}'` in a run block is a script-injection + # sink — a tag name containing a quote and a semicolon would execute. + # Only maintainers can push tags here, but a workflow holding npm + # publish rights shouldn't rely on that as its only defense. + - name: Resolve target package + id: target + env: + EVENT: ${{ github.event_name }} + INPUT_PACKAGE: ${{ inputs.package }} + REF: ${{ github.ref_name }} + run: | + set -euo pipefail + if [ "$EVENT" = 'workflow_dispatch' ]; then + SLUG="$INPUT_PACKAGE" + TAG_VERSION='' + else + SLUG="${REF%%-v*}" + TAG_VERSION="${REF#*-v}" + fi + + case "$SLUG" in + framework) DIR='packages/framework' ;; + cli) DIR='packages/cli' ;; + *) echo "::error::Unrecognized release target '$SLUG'"; exit 1 ;; + esac + + NAME=$(node -p "require('./$DIR/package.json').name") + VERSION=$(node -p "require('./$DIR/package.json').version") + + if [ -n "$TAG_VERSION" ] && [ "$TAG_VERSION" != "$VERSION" ]; then + echo "::error::Tag says v$TAG_VERSION but $DIR/package.json says $VERSION. Commit the version bump before tagging." + exit 1 + fi + + echo "dir=$DIR" >> "$GITHUB_OUTPUT" + echo "name=$NAME" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Releasing $NAME@$VERSION from $DIR" + + # npm rejects a re-publish of an existing version with a bare 403. + # Failing here instead says what actually went wrong. + - name: Refuse to republish an existing version + env: + NAME: ${{ steps.target.outputs.name }} + VERSION: ${{ steps.target.outputs.version }} + run: | + set -euo pipefail + if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then + echo "::error::$NAME@$VERSION is already on npm. Bump the version — published versions are immutable." + exit 1 + fi + echo "$NAME@$VERSION is unpublished. Proceeding." + + - run: npm ci + + # Same gates as ci.yml, re-run here on the exact commit being shipped. + # A green PR check is not proof that the tagged commit is green. + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run build + - run: npm audit --audit-level=high + + - name: Preview tarball contents + env: + DIR: ${{ steps.target.outputs.dir }} + run: npm pack --dry-run --workspace "$DIR" + + - name: Publish to npm + if: ${{ github.event_name == 'push' || !inputs.dry_run }} + env: + DIR: ${{ steps.target.outputs.dir }} + run: npm publish --workspace "$DIR" + + - name: Dry run only — nothing published + if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} + env: + NAME: ${{ steps.target.outputs.name }} + VERSION: ${{ steps.target.outputs.version }} + run: echo "Dry run complete for $NAME@$VERSION. Re-run with dry_run unchecked, or push a tag, to publish." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a04d50..f3df7a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,7 @@ documentation theme that lands in the CLI's theme picker. - [Contributing to the CLI](#contributing-to-the-cli) - [Pull request process](#pull-request-process) - [Conventions](#conventions) +- [Releases (maintainers)](#releases-maintainers) - [Source mirror (maintainers)](#source-mirror-maintainers) --- @@ -388,6 +389,20 @@ chore: bump @inkform/framework to 0.4.1 --- +## Releases (maintainers) + +`@inkform/framework` and `@inkform/cli` are published to npm by +`.github/workflows/release.yml`, triggered by pushing a version tag +(`framework-v0.5.0`, `cli-v0.5.0`). It re-runs the full CI gate against the +tagged commit, then publishes with npm Trusted Publishing — a short-lived +OIDC token, no npm secret stored in this repository, and a provenance +attestation on every release. + +Nobody publishes from a laptop. Full procedure and the one-time npm/GitHub +setup: [`packages/framework/PUBLISHING.md`](packages/framework/PUBLISHING.md). + +--- + ## Source mirror (maintainers) This public repo is kept in sync with a private working monorepo via diff --git a/packages/framework/PUBLISHING.md b/packages/framework/PUBLISHING.md index 2e4f135..ea0cf69 100644 --- a/packages/framework/PUBLISHING.md +++ b/packages/framework/PUBLISHING.md @@ -1,84 +1,116 @@ # Publishing — `@inkform/framework` + `@inkform/cli` -Two packages in this monorepo are actually published to public npm under the -`@inkform` org (org already exists on npmjs.com): - -- **`packages/framework`** → `@inkform/framework` (MIT, `private: false`, - `publishConfig.access: public`). Ships TypeScript source directly; - consumers add `transpilePackages: ['@inkform/framework']` (no build step). -- **`packages/cli`** → `@inkform/cli` (MIT, `private: false`), bin command - `inkform-docs`. Scaffolds new projects by fetching a template directly from - GitHub (`github:inkform-dev/framework/templates/` via `giget`) — it - does **not** fetch the 5 themes or 2 examples as npm packages. Those - (`templates/*`, `examples/*`) are `private: true` and stay that way; they - are not meant to be installed from npm at all, only copied by the CLI or - cloned directly. - -This is the only publish blocker left from earlier passes (tracked as "the -temporary dependency bridge" — every consumer today declares -`"@inkform/framework": "npm:@freewrite-cms/framework@^0.2.0"`, an alias to -the last real published snapshot, since `@inkform/framework` itself has never -been published). Publishing closes that out permanently. - -## Prerequisites (one-time, needs your own npm login — not something an -## agent session can do; publishing to public npm is a real, hard-to-reverse -## action) +Two packages in this monorepo are published to public npm under the +`@inkform` org: -```bash -npm whoami # confirm you're logged in as an @inkform org member -# if not: -npm login -``` +- **`packages/framework`** → [`@inkform/framework`](https://www.npmjs.com/package/@inkform/framework) + (MIT, `publishConfig.access: public`). Ships TypeScript source directly; + consumers add `transpilePackages: ['@inkform/framework']`. No build step, + so there is nothing to compile before publishing. +- **`packages/cli`** → [`@inkform/cli`](https://www.npmjs.com/package/@inkform/cli), + bin command `inkform-docs`. Scaffolds projects by fetching a template + from GitHub (`github:inkform-dev/framework/templates/` via `giget`), + not from npm. -## Release — do framework first, then cli (cli doesn't depend on framework -## at publish time, but framework going out first means anyone testing cli -## against a fresh `npm install` immediately gets a real, resolvable -## `@inkform/framework`) +`templates/*` and `examples/*` are `private: true` and stay that way — they +are copied by the CLI or cloned directly, never installed from npm. -```bash -cd packages/framework -npm version # bumps package.json, no git tag pushed automatically -npm publish -``` +Releases run through +[`.github/workflows/release.yml`](../../.github/workflows/release.yml). +**Nobody publishes from a laptop, and no npm token exists anywhere in this +repo.** -```bash -cd ../cli -npm version -npm publish -``` +--- -Both commands run from a clean `git status` (commit first) so the published -`package.json` version matches what's in git. +## How authentication works (npm Trusted Publishing) -## After publishing — retire the dependency-alias bridge +The release workflow authenticates to npm with a short-lived OIDC token that +GitHub mints for that workflow run. npm accepts it because each package's +"Trusted publisher" settings on npmjs.com name this repository *and this +exact workflow filename*. There is no `NPM_TOKEN` secret to leak, rotate, or +scope, and a fork cannot publish: a fork's OIDC claim carries the fork's own +repository name and npm rejects it. -Every consumer currently pins the OLD published snapshot under the new name: +Two consequences worth knowing before you touch anything: -```json -"@inkform/framework": "npm:@freewrite-cms/framework@^0.2.0" -``` +- **Renaming or moving `.github/workflows/release.yml` breaks publishing** + until the filename is updated on npmjs.com to match. That coupling is the + security property. +- Every publish made this way carries a **provenance attestation** — the + "Built and signed on GitHub Actions" badge on the npm page, linking the + tarball back to the exact commit and workflow run that produced it. -Once the real `@inkform/framework` is live on npm, change this in every -`package.json` that has it (all 5 themes, both examples, and — in the -**separate** `cms/` repo — `apps/blog`, `apps/docs`, and -`packages/templates/{blog-only,docs-only,unified}`) to a plain version range -matching whatever you just published: +--- -```json -"@inkform/framework": "^0.3.0" -``` +## Cutting a release -Then, in each repo: +Framework first, then CLI. They version independently; there is no +requirement that their numbers match. -```bash -npm install # re-resolves the lockfile against the real package -npm run build # confirm nothing broke -``` +1. **Bump the version** in `packages//package.json`. Published + versions are immutable, so this must be a version that has never been + published — the workflow checks and refuses otherwise. +2. **Update `CHANGELOG.md`** at the repo root (it tracks + `packages/framework`'s version). +3. **Commit and push to `main`.** The tag must point at a commit that is + actually on the branch. +4. **Tag and push the tag:** + + ```bash + git tag -a framework-v0.5.0 -m "@inkform/framework 0.5.0" + git push origin framework-v0.5.0 + ``` + + The prefix selects the package: `framework-v*` → `packages/framework`, + `cli-v*` → `packages/cli`. The version in the tag must equal the version + in that package's `package.json`; the workflow refuses to guess. + +5. **Approve the deployment** if the `npm-publish` environment has required + reviewers configured (recommended — see below). + +The workflow re-runs the full CI gate (`lint`, `typecheck`, `test`, `build`, +`npm audit --audit-level=high`) against the tagged commit before publishing. +A green PR check is not proof the tagged commit is green. + +### Rehearsing without publishing + +Actions → Release → *Run workflow* → pick the package, leave **Dry run** +checked. Everything runs including `npm pack --dry-run`, and the publish step +is skipped. Useful for confirming the tarball contents after changing +`files` or `exports`. -This is a mechanical find-and-replace across ~10 `package.json` files plus a -lockfile regeneration in each of the two repos (`framework/` and `cms/`) — -safe to do in one pass once the npm publish itself has happened, since it's -just pointing at the real thing instead of the alias. +--- + +## One-time setup + +Already done once per package, recorded here for whoever has to redo it: + +**On npmjs.com** — package page → Settings → Trusted Publisher → GitHub Actions: + +| Field | Value | +| --- | --- | +| Organization or user | `inkform-dev` | +| Repository | `framework` | +| Workflow filename | `release.yml` | +| Environment name | `npm-publish` | + +Then, on the same settings page, set publishing access to **"Require +two-factor authentication and disallow tokens."** That kills classic +automation tokens as a publish path entirely; trusted publishing is +unaffected by it. + +**On GitHub** — Settings → Environments → `npm-publish` → add yourself as a +required reviewer. This is a second, independent gate: even someone who can +push a tag cannot ship without a human approving the run. + +--- + +## Versioning + +Versions are bumped by hand, and `CHANGELOG.md` is written by hand. If that +becomes a chore across more than these two packages, +[Changesets](https://github.com/changesets/changesets) automates both — but +it earns its keep at four or five packages, not two. ## Optional: ship compiled JS instead of source @@ -87,25 +119,10 @@ To let consumers skip `transpilePackages`, add a build step and point ```bash npm i -D tsup -# package.json # "scripts": { "build": "tsup src/*.ts src/*.tsx --format esm --dts --external next,react,react-dom" } # "files": ["dist"], exports → ./dist/*.js ``` -Not required — TS-source-direct + `transpilePackages` works fine and is what -every template/example already does. Only worth it if a consumer outside -this monorepo's own conventions complains about build times or wants to -avoid the `transpilePackages` requirement. - -## Versioning across the monorepo - -For coordinated releases of `@inkform/framework` + `@inkform/cli`, -[Changesets](https://github.com/changesets/changesets) is recommended: - -```bash -npm i -D @changesets/cli && npx changeset init -# per change: npx changeset → npx changeset version → npx changeset publish -``` - -See the repository `CONTRIBUTING.md` for the dev workflow and the source-mirror -arrangement. +Not required — TS-source-direct works fine and is what every template and +example already does. Only worth it if a consumer outside this monorepo's +conventions wants to avoid `transpilePackages`. From b3c74af3bf2285bdb6a56e4e3ab292c3c48a496e Mon Sep 17 00:00:00 2001 From: Charan Date: Sun, 30 Aug 2026 16:54:40 +0530 Subject: [PATCH 2/3] fix(framework): restore the ./reactions subpath export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit df21f0b ("Unify secondary top nav styling and add mobile scroll hints") dropped `"./reactions"` from the exports map while adding the two nav entries. Nothing else in that commit touches reactions, and the commit message doesn't mention it — it was collateral, not a decision. The effect is a documented public export that resolves to nothing: - `src/reactions.tsx` is unchanged and still ships inside the tarball (`files: ["src"]`), it's just unreachable. - `packages/framework/README.md` lists `./reactions` among the bring-your-own-backend widgets, and that README ships in the tarball too. - `examples/inkform-docs` — the dogfooded docs site — documents `import { Reactions } from '@inkform/framework/reactions'` in both guides/interactive.mdx and reference/framework-api.mdx. So the published docs instruct an import the published package can't satisfy. Restored in its original position between ./analytics-script and the widget group it belongs to. This also keeps the next release purely additive: every export present in 0.4.0 is present now, with ./markdown, ./page-actions, ./secondary-top-nav and ./scrollable-top-nav added. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012XWzjtTYZRNgD1YVM6pJ8u --- packages/framework/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/framework/package.json b/packages/framework/package.json index 931da9e..f885631 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -57,6 +57,7 @@ "./theme-toggle": "./src/theme-toggle.tsx", "./subscribe-form": "./src/subscribe-form.tsx", "./analytics-script": "./src/analytics-script.tsx", + "./reactions": "./src/reactions.tsx", "./secondary-top-nav": "./src/secondary-top-nav.tsx", "./scrollable-top-nav": "./src/scrollable-top-nav.tsx", "./comments": "./src/comments.tsx" From e86a3d65fb42038cf4576790cbd9fb1a1f16c50a Mon Sep 17 00:00:00 2001 From: Charan Date: Sun, 30 Aug 2026 17:00:30 +0530 Subject: [PATCH 3/3] chore(release): prepare @inkform/framework 0.5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm's latest is 0.4.0, published 2026-07-22. Everything merged since — the Markdown-per-URL routes, the structural MDX-to-Markdown converter, page actions, the expanded AI tool menu, the unified secondary nav — has had no path to npm, because main still declared 0.4.0. Same version number, different code: `npm publish` returns 403 against an immutable version. Bumps packages/framework to 0.5.0 and writes the CHANGELOG entry. Purely additive over 0.4.0 — with ./reactions restored in the previous commit, every export present in 0.4.0 is still present. Also fixes the dependency range that made scaffolding hand users stale code. Every template and example declared "@inkform/framework": "^0.3.0". For a 0.x package that range is >=0.3.0 <0.4.0, so it stopped matching the moment the framework went to 0.4.0. Two consequences, both live today: - `npx @inkform/cli init` && `npm install` resolved to 0.3.0. New users got a framework with no native API reference renderer, no MCP server, no AI ask-box and no llms.txt — all of which shipped in 0.4.0. The CLI rewrites a scaffolded project's name and version but never touched this range. - Inside the monorepo the local packages/framework no longer satisfied what the workspaces asked for, so npm fetched a real 0.3.0 from the registry into all six workspaces' own node_modules, shadowing the live source. Those entries were in the lockfile; removing them accounts for the 139 deleted lines here. That second one is what scripts/prune-workspace-shadows.mjs has been deleting on every postinstall since it was written. Its header blamed npm for "occasionally" materializing physical copies; the cause was this range all along, so the note is corrected to point at the real trigger. The script stays — the failure mode is silent and confusing — but it now prunes nothing. Ranges move to ^0.5.0. archive/templates/* is deliberately left at ^0.3.0: those are archived, don't build, and take security bumps only. PUBLISHING.md gains the ordering caveat this creates — templates declare a real npm range and the CLI scaffolds from GitHub main without pinning a ref, so between merging a release PR and the tag publishing, a scaffold asks for a version npm doesn't have yet. Verified on this tree: npm ci, lint, typecheck, test (96/96), build, and npm audit --audit-level=high all pass; the prune script now reports nothing; every workspace resolves @inkform/framework to packages/framework at 0.5.0; npm pack --dry-run produces inkform-framework-0.5.0.tgz, 88 files. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012XWzjtTYZRNgD1YVM6pJ8u --- CHANGELOG.md | 57 +++++++++++ examples/inkform-docs/package.json | 2 +- examples/markdown-docs/package.json | 2 +- examples/pokeapi-docs/package.json | 2 +- package-lock.json | 146 ++-------------------------- packages/framework/PUBLISHING.md | 9 ++ packages/framework/package.json | 2 +- scripts/prune-workspace-shadows.mjs | 27 +++-- templates/canopy/package.json | 2 +- templates/galley/package.json | 2 +- templates/shadcn/package.json | 2 +- 11 files changed, 99 insertions(+), 154 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1215b00..74113c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,63 @@ All notable changes to this project are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions track `packages/framework`'s own `package.json`. +## [0.5.0] — 2026-08-30 + +### Added + +- **Every page is served as Markdown at its own URL** — append `.md` to any + docs page (or content-negotiate) and get the source back as clean Markdown. + Makes the whole site directly consumable by agents and LLM tooling without + scraping rendered HTML. +- **`@inkform/framework/markdown`** — a structural MDX-to-Markdown converter + that preserves link URLs and component labels instead of flattening them + away, plus `@inkform/framework/page-actions` (``): a per-page + *Copy Markdown* / *Open in…* control rendered above the page title. +- **Expanded AI tool menu** — a two-column menu driven by a single data + registry (`ai-tools.ts`) rather than hardcoded links: ChatGPT, Claude, + Google (AI Overview), and copy-the-command entries for Claude Code, + OpenCode, Codex, and Antigravity. Monochrome brand icons throughout. +- **`@inkform/framework/secondary-top-nav` and `/scrollable-top-nav`** — + unified secondary navigation with mobile scroll hints and de-duplicated + anchors/navbar links. + +### Changed + +- Copy actions give real feedback — copied-state on the button, with a + confetti flourish on success (tokenized colors, no hardcoded hex). +- Glyph and clipboard helpers deduplicated into shared modules. + +### Fixed + +- The VS Code MCP install link pointed at the wrong handler. +- The ChatGPT share link now uses the `prompt` parameter. +- Mobile *Open* menu is capped at `80vw` instead of overflowing the viewport. +- The left column of the AI menu now shares the right column's gutter off the + divider. +- **`@inkform/framework/reactions` resolves again.** The subpath export was + dropped from the exports map in 0.4.0's development while + `src/reactions.tsx` kept shipping, so the export documented in the package + README and in the guides resolved to nothing. Every export present in 0.4.0 + is present in 0.5.0 — this release is purely additive. +- **Scaffolded projects get the current framework.** Every template and + example declared `"@inkform/framework": "^0.3.0"`. For a 0.x package that + range means `>=0.3.0 <0.4.0`, so `npx @inkform/cli init` followed by + `npm install` resolved to 0.3.0 — no native API reference renderer, no MCP + server, no AI ask-box, no `llms.txt`. The CLI rewrites a scaffolded + project's `name` and `version` but never touched this range. Now `^0.5.0`. +- **Workspace shadowing fixed at the root.** The same stale range meant the + local `packages/framework` no longer satisfied what the templates and + examples asked for, so npm fetched a real 0.3.0 from the registry into each + of the six workspaces' own `node_modules` — shadowing the live source. This + is what `scripts/prune-workspace-shadows.mjs` had been deleting on every + `postinstall`; the lockfile is 139 lines lighter without those entries. The + script stays as a safety net, with its root-cause note corrected. + +### Security + +- 4 lockfile advisories patched (3 high, 1 moderate); archived templates + bumped to Next 16.2.12, clearing 54 Dependabot alerts. + ## [0.4.0] — 2026-07-21 ### Added diff --git a/examples/inkform-docs/package.json b/examples/inkform-docs/package.json index 95f8d01..71a0a6d 100644 --- a/examples/inkform-docs/package.json +++ b/examples/inkform-docs/package.json @@ -12,7 +12,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", diff --git a/examples/markdown-docs/package.json b/examples/markdown-docs/package.json index 4a959a3..a3868a5 100644 --- a/examples/markdown-docs/package.json +++ b/examples/markdown-docs/package.json @@ -12,7 +12,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", diff --git a/examples/pokeapi-docs/package.json b/examples/pokeapi-docs/package.json index 25a733d..af4ab37 100644 --- a/examples/pokeapi-docs/package.json +++ b/examples/pokeapi-docs/package.json @@ -13,7 +13,7 @@ "test:e2e": "playwright test" }, "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", diff --git a/package-lock.json b/package-lock.json index c243a64..58251c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "version": "0.2.0", "license": "MIT", "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", @@ -40,34 +40,12 @@ "typescript": "^5" } }, - "examples/inkform-docs/node_modules/@inkform/framework": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@inkform/framework/-/framework-0.3.0.tgz", - "integrity": "sha512-BRLtjNPS5UT06IToODYWHm1nSg6MYJSDMXe6qWnSNqPexFCDlLDbbeq5BreDNHR+ASJGQ2Q9t3ckukT+0tVu0A==", - "license": "MIT", - "dependencies": { - "gray-matter": "^4.0.3", - "next-mdx-remote": "^6.0.0", - "rehype-pretty-code": "^0.14.1", - "rehype-slug": "^6.0.0", - "remark-directive": "^3.0.0", - "remark-gfm": "^4.0.1", - "shiki": "^1.24.0", - "unist-util-visit": "^5.0.0", - "yaml": "^2.6.0" - }, - "peerDependencies": { - "next": ">=16", - "react": ">=19", - "react-dom": ">=19" - } - }, "examples/markdown-docs": { "name": "@inkform/example-markdown", "version": "0.2.0", "license": "MIT", "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", @@ -81,34 +59,12 @@ "typescript": "^5" } }, - "examples/markdown-docs/node_modules/@inkform/framework": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@inkform/framework/-/framework-0.3.0.tgz", - "integrity": "sha512-BRLtjNPS5UT06IToODYWHm1nSg6MYJSDMXe6qWnSNqPexFCDlLDbbeq5BreDNHR+ASJGQ2Q9t3ckukT+0tVu0A==", - "license": "MIT", - "dependencies": { - "gray-matter": "^4.0.3", - "next-mdx-remote": "^6.0.0", - "rehype-pretty-code": "^0.14.1", - "rehype-slug": "^6.0.0", - "remark-directive": "^3.0.0", - "remark-gfm": "^4.0.1", - "shiki": "^1.24.0", - "unist-util-visit": "^5.0.0", - "yaml": "^2.6.0" - }, - "peerDependencies": { - "next": ">=16", - "react": ">=19", - "react-dom": ">=19" - } - }, "examples/pokeapi-docs": { "name": "@inkform/example-pokeapi", "version": "0.2.0", "license": "MIT", "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", @@ -123,28 +79,6 @@ "typescript": "^5" } }, - "examples/pokeapi-docs/node_modules/@inkform/framework": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@inkform/framework/-/framework-0.3.0.tgz", - "integrity": "sha512-BRLtjNPS5UT06IToODYWHm1nSg6MYJSDMXe6qWnSNqPexFCDlLDbbeq5BreDNHR+ASJGQ2Q9t3ckukT+0tVu0A==", - "license": "MIT", - "dependencies": { - "gray-matter": "^4.0.3", - "next-mdx-remote": "^6.0.0", - "rehype-pretty-code": "^0.14.1", - "rehype-slug": "^6.0.0", - "remark-directive": "^3.0.0", - "remark-gfm": "^4.0.1", - "shiki": "^1.24.0", - "unist-util-visit": "^5.0.0", - "yaml": "^2.6.0" - }, - "peerDependencies": { - "next": ">=16", - "react": ">=19", - "react-dom": ">=19" - } - }, "node_modules/@ai-sdk/anthropic": { "version": "4.0.21", "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-4.0.21.tgz", @@ -7720,7 +7654,7 @@ }, "packages/framework": { "name": "@inkform/framework", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { "@ai-sdk/anthropic": "^4.0.16", @@ -7764,7 +7698,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", @@ -7778,34 +7712,12 @@ "typescript": "^5" } }, - "templates/canopy/node_modules/@inkform/framework": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@inkform/framework/-/framework-0.3.0.tgz", - "integrity": "sha512-BRLtjNPS5UT06IToODYWHm1nSg6MYJSDMXe6qWnSNqPexFCDlLDbbeq5BreDNHR+ASJGQ2Q9t3ckukT+0tVu0A==", - "license": "MIT", - "dependencies": { - "gray-matter": "^4.0.3", - "next-mdx-remote": "^6.0.0", - "rehype-pretty-code": "^0.14.1", - "rehype-slug": "^6.0.0", - "remark-directive": "^3.0.0", - "remark-gfm": "^4.0.1", - "shiki": "^1.24.0", - "unist-util-visit": "^5.0.0", - "yaml": "^2.6.0" - }, - "peerDependencies": { - "next": ">=16", - "react": ">=19", - "react-dom": ">=19" - } - }, "templates/galley": { "name": "@inkform/theme-galley", "version": "0.1.0", "license": "MIT", "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", @@ -7819,34 +7731,12 @@ "typescript": "^5" } }, - "templates/galley/node_modules/@inkform/framework": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@inkform/framework/-/framework-0.3.0.tgz", - "integrity": "sha512-BRLtjNPS5UT06IToODYWHm1nSg6MYJSDMXe6qWnSNqPexFCDlLDbbeq5BreDNHR+ASJGQ2Q9t3ckukT+0tVu0A==", - "license": "MIT", - "dependencies": { - "gray-matter": "^4.0.3", - "next-mdx-remote": "^6.0.0", - "rehype-pretty-code": "^0.14.1", - "rehype-slug": "^6.0.0", - "remark-directive": "^3.0.0", - "remark-gfm": "^4.0.1", - "shiki": "^1.24.0", - "unist-util-visit": "^5.0.0", - "yaml": "^2.6.0" - }, - "peerDependencies": { - "next": ">=16", - "react": ">=19", - "react-dom": ">=19" - } - }, "templates/shadcn": { "name": "@inkform/theme-shadcn", "version": "0.1.0", "license": "MIT", "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", @@ -7859,28 +7749,6 @@ "pagefind": "^1.5.2", "typescript": "^5" } - }, - "templates/shadcn/node_modules/@inkform/framework": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@inkform/framework/-/framework-0.3.0.tgz", - "integrity": "sha512-BRLtjNPS5UT06IToODYWHm1nSg6MYJSDMXe6qWnSNqPexFCDlLDbbeq5BreDNHR+ASJGQ2Q9t3ckukT+0tVu0A==", - "license": "MIT", - "dependencies": { - "gray-matter": "^4.0.3", - "next-mdx-remote": "^6.0.0", - "rehype-pretty-code": "^0.14.1", - "rehype-slug": "^6.0.0", - "remark-directive": "^3.0.0", - "remark-gfm": "^4.0.1", - "shiki": "^1.24.0", - "unist-util-visit": "^5.0.0", - "yaml": "^2.6.0" - }, - "peerDependencies": { - "next": ">=16", - "react": ">=19", - "react-dom": ">=19" - } } } } diff --git a/packages/framework/PUBLISHING.md b/packages/framework/PUBLISHING.md index ea0cf69..cfbe4a7 100644 --- a/packages/framework/PUBLISHING.md +++ b/packages/framework/PUBLISHING.md @@ -68,6 +68,15 @@ requirement that their numbers match. 5. **Approve the deployment** if the `npm-publish` environment has required reviewers configured (recommended — see below). +> **Tag promptly after merging a release PR.** `templates/*` declare a real +> npm range (`"@inkform/framework": "^"`), and the CLI scaffolds +> straight from GitHub `main` via giget — it does not pin a ref. Between +> merging a version bump and the tag actually publishing, `npx @inkform/cli +> init` hands users a `package.json` asking for a version npm doesn't have +> yet, and their `npm install` fails. The window is however long the +> `npm-publish` approval sits unattended, so don't merge a release PR you +> aren't around to approve. + The workflow re-runs the full CI gate (`lint`, `typecheck`, `test`, `build`, `npm audit --audit-level=high`) against the tagged commit before publishing. A green PR check is not proof the tagged commit is green. diff --git a/packages/framework/package.json b/packages/framework/package.json index f885631..75160b0 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -1,6 +1,6 @@ { "name": "@inkform/framework", - "version": "0.4.0", + "version": "0.5.0", "description": "Standalone Next.js + MDX framework for documentation, API reference (OpenAPI), blog, and changelog sites. The rendering engine behind the inkform docs themes — usable on its own.", "license": "MIT", "author": "Charan", diff --git a/scripts/prune-workspace-shadows.mjs b/scripts/prune-workspace-shadows.mjs index e36f536..d91f260 100644 --- a/scripts/prune-workspace-shadows.mjs +++ b/scripts/prune-workspace-shadows.mjs @@ -1,13 +1,24 @@ #!/usr/bin/env node /** - * npm workspaces occasionally materializes a real, physical copy of a - * `@inkform/*` internal package inside a consuming workspace's own - * node_modules (e.g. `examples/pokeapi-docs/node_modules/@inkform/framework`) - * instead of relying on the root-level symlink to `packages/framework`. When - * that happens, Node's module resolution finds the nested copy FIRST — which - * can be an arbitrarily stale snapshot (observed: frozen at an old version, - * missing exports and fields added since) — silently shadowing the real, - * live workspace source and breaking typecheck/build in confusing ways. + * Safety net against a `@inkform/*` internal package being materialized as a + * real, physical copy inside a consuming workspace's own node_modules (e.g. + * `examples/pokeapi-docs/node_modules/@inkform/framework`) instead of + * resolving through the root-level symlink to `packages/framework`. When that + * happens, Node's module resolution finds the nested copy FIRST — an + * arbitrarily stale snapshot, missing exports and fields added since — + * silently shadowing the live workspace source and breaking typecheck/build + * in confusing ways. + * + * This is NOT an npm quirk, which is what this comment used to claim. The + * cause was a version range: every template and example declared + * `"@inkform/framework": "^0.3.0"` while `packages/framework` had moved to + * 0.4.0. For a 0.x package `^0.3.0` means `>=0.3.0 <0.4.0`, so the local + * workspace no longer satisfied it and npm correctly went to the registry + * for a real 0.3.0 — in all six workspaces, recorded in the lockfile. Those + * ranges now track the current major, so nothing should be pruned. Kept + * because the failure is silent and confusing when it does happen; if this + * script starts reporting again, suspect a range that has drifted out of + * step with `packages/framework`'s version rather than npm. * * Every internal `@inkform/*` package is workspace-local; there is never a * reason to keep a nested copy. Runs automatically via `postinstall` so diff --git a/templates/canopy/package.json b/templates/canopy/package.json index 0da6283..ca5b141 100644 --- a/templates/canopy/package.json +++ b/templates/canopy/package.json @@ -12,7 +12,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", diff --git a/templates/galley/package.json b/templates/galley/package.json index 3da1c60..b12c8a9 100644 --- a/templates/galley/package.json +++ b/templates/galley/package.json @@ -12,7 +12,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1", diff --git a/templates/shadcn/package.json b/templates/shadcn/package.json index cf882a8..7bd9f3f 100644 --- a/templates/shadcn/package.json +++ b/templates/shadcn/package.json @@ -12,7 +12,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@inkform/framework": "^0.3.0", + "@inkform/framework": "^0.5.0", "lucide-react": "^0.483.0", "next": "16.2.12", "react": "19.2.1",