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
55 changes: 46 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: CI

on:
workflow_dispatch:
pull_request:
branches: [main]
push:
Expand All @@ -12,37 +13,73 @@ concurrency:

jobs:
check:
name: Build, typecheck & test (Node ${{ matrix.node }})
name: Build, typecheck & test (${{ matrix.os }}, Node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node: [24]
include:
# fastest feedback on PRs; full matrix only for pushes to main
# Keep PR feedback on Linux; also cover macOS on pushes to main.
os: ${{ fromJSON(github.event_name == 'push' && '["ubuntu-latest", "macos-latest"]' || '["ubuntu-latest"]') }}
node: [24, 26]
exclude:
- os: macos-latest
node: 24
if: github.event_name == 'push'
node: 26
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
# Playwright 1.59.1's browser ZIP extraction stalls under Node 26.
node-version: 24

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
bun-version: 1.4.1

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Install browser for integration tests
working-directory: plugins/terminal-browser-plugin
timeout-minutes: 10
run: bunx playwright-core install --with-deps chromium

- name: Select Node version for build and tests
if: matrix.node != 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}

- name: Build all plugins
run: bun run build

- name: Legacy model-config bundle is up to date
run: |
git diff --exit-code -- plugins/zcode-model-config-plugin/dist
test -z "$(git ls-files --others --exclude-standard -- plugins/zcode-model-config-plugin/dist)"
test -f plugins/zcode-model-config-plugin/dist/mcp/server.js
test -z "$(git ls-files -- plugins/terminal-browser-plugin/dist)"

- name: Typecheck
run: bun run typecheck

- name: Test
run: bun test

- name: Package terminal-browser
working-directory: plugins/terminal-browser-plugin
run: bun run package

- name: Verify extracted release with real Chromium
working-directory: plugins/terminal-browser-plugin
run: bun run verify:package

- name: Upload verified release candidate
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: terminal-browser-${{ matrix.os }}-node${{ matrix.node }}
path: .cache/releases/
include-hidden-files: true
if-no-files-found: error
retention-days: 14
119 changes: 119 additions & 0 deletions .github/workflows/release-terminal-browser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Release terminal-browser

on:
workflow_dispatch:
inputs:
version:
description: Plugin version to publish (must match plugin.json)
required: true
type: string

permissions:
contents: write
pull-requests: write
actions: write

concurrency:
group: release-terminal-browser
cancel-in-progress: false

jobs:
release:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.1

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build and typecheck
run: |
bun run build
bun run typecheck

- name: Install test browser
working-directory: plugins/terminal-browser-plugin
run: bunx playwright-core install --with-deps chromium

- name: Test source and create release ZIP
run: |
bun test
bun run --filter '@zcode-plugins/terminal-browser-plugin' package

- name: Verify release ZIP
working-directory: plugins/terminal-browser-plugin
run: bun run verify:package

- name: Check requested version
id: release
env:
REQUESTED_VERSION: ${{ inputs.version }}
run: |
bun -e 'const r = await Bun.file(".cache/releases/release.json").json(); if (r.version !== process.env.REQUESTED_VERSION) throw new Error("Requested version does not match plugin.json"); console.log(Object.entries({tag:r.tag,file:r.file,version:r.version}).map(([key,value])=>key+"="+value).join("\n"))' >> "$GITHUB_OUTPUT"

- name: Publish versioned assets
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
RELEASE_FILE: ${{ steps.release.outputs.file }}
run: |
if ! gh release view "$RELEASE_TAG" > /dev/null 2>&1; then
gh release create "$RELEASE_TAG" \
".cache/releases/$RELEASE_FILE" \
".cache/releases/$RELEASE_FILE.sha256" \
".cache/releases/release.json" \
--target "$GITHUB_SHA" --title "$RELEASE_TAG" \
--notes-file plugins/terminal-browser-plugin/RELEASE.md \
--latest=false
fi

- name: Verify download and prepare marketplace update
env:
RELEASE_FILE: ${{ steps.release.outputs.file }}
run: |
bun plugins/terminal-browser-plugin/scripts/release.ts promote \
".cache/releases/$RELEASE_FILE" --repository "$GITHUB_REPOSITORY"

- name: Propose marketplace update
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: |
if git diff --quiet -- .claude-plugin/marketplace.json; then
exit 0
fi
release_branch="release/terminal-browser-$RELEASE_VERSION"
if test -n "$(gh pr list --head "$release_branch" --state open --json number --jq '.[0].number // empty')"; then
gh workflow run ci.yml --ref "$release_branch"
exit 0
fi
if git ls-remote --exit-code --heads origin "$release_branch" > /dev/null; then
git fetch origin "$release_branch"
git show FETCH_HEAD:.claude-plugin/marketplace.json > .cache/marketplace-published.json
cmp .claude-plugin/marketplace.json .cache/marketplace-published.json
else
git switch -c "$release_branch"
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add .claude-plugin/marketplace.json
git commit -m "chore: publish terminal-browser $RELEASE_VERSION in marketplace"
git push origin "$release_branch"
fi
gh pr create --base main --head "$release_branch" \
--title "Publish terminal-browser $RELEASE_VERSION" \
--body-file plugins/terminal-browser-plugin/RELEASE.md
# GITHUB_TOKEN-created PRs do not trigger pull_request workflows.
gh workflow run ci.yml --ref "$release_branch"
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ Desktop.ini
# Dependencies
node_modules/

# Build output
# Only the legacy directory-source plugin keeps its bundle in Git.
# terminal-browser ships its runtime in versioned release ZIPs.
dist/
!plugins/zcode-model-config-plugin/dist/
!plugins/zcode-model-config-plugin/dist/**
*.tsbuildinfo

# Bun
Expand Down
52 changes: 39 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Read this before creating or modifying a plugin.
│ ├── commands/*.md # slash commands
│ ├── skills/<name>/SKILL.md # agent skills
│ ├── src/ # TypeScript sources
│ └── dist/ # build output — COMMITTED (the runtime runs this)
│ └── dist/ # runtime build output; see distribution rules below
├── package.json # bun workspace root ("workspaces": ["plugins/*"])
└── tsconfig.base.json # shared strict TS config (ES2023, Bundler resolution)
```
Expand All @@ -23,7 +23,7 @@ Two manifest layers, do not confuse them:

| File | Read by | Purpose |
| --- | --- | --- |
| `.claude-plugin/marketplace.json` (repo root) | `zcode plugins marketplace add` | Declares the marketplace name (the `@marketplace` half of plugin ids) and the list of plugins with their source directories. |
| `.claude-plugin/marketplace.json` (repo root) | `zcode plugins marketplace add` | Declares the marketplace name and plugins with directory or verified release ZIP sources. |
| `plugins/<dir>/.zcode-plugin/plugin.json` | plugin runtime at load time | Declares the plugin name, version, MCP servers, commands and skills directories. |

The CLI looks for the marketplace manifest only at
Expand Down Expand Up @@ -77,6 +77,11 @@ Minimum required fields: `name` (the plugin id used in `<plugin>@<marketplace>`)

### 4. Register in the marketplace manifest

For a directory source, commit the complete runtime. For a Release ZIP source,
publish and verify the archive before adding the catalog entry. terminal-browser
uses the release workflow described below; its unpublished listing metadata
lives in `plugins/terminal-browser-plugin/marketplace-entry.json`.

Add an entry to `plugins` in `.claude-plugin/marketplace.json`:

```json
Expand All @@ -103,10 +108,9 @@ Pattern used by both existing plugins (`src/mcp/server.ts`):
- `@modelcontextprotocol/sdk` `McpServer` + `StdioServerTransport`, tools
registered with `server.registerTool(name, { description, inputSchema }, handler)`
using zod schemas.
- Keep the bundle **self-contained**: bundle all npm dependencies (tsdown does
this by default; with the vite-plus-core node build, mark `@modelcontextprotocol/sdk/*`,
`zod`, and `node:*` as external — those packages are installed into the
plugin's `node_modules` in the cache). The point is: nothing outside
- Keep the bundle **self-contained**: bundle npm dependencies or include their
required runtime files in the release ZIP. The CLI does not run npm install
or build steps. Only Node built-ins can be assumed available. Nothing outside
`dist/`, `node_modules/`, and the manifest needs to exist on a user machine.
- If the plugin ships a web UI, inline it: build the UI to a single HTML/JS/CSS
string module (see `plugins/zcode-model-config-plugin/scripts/build.ts` and
Expand Down Expand Up @@ -160,6 +164,18 @@ zcode plugins validate <name>@zcode-plugins # after installing once (see below

Use the local zcode-cli checkout, not the global install:

For terminal-browser, use its isolated ZIP verification and development catalog:

```bash
bun run package:terminal-browser
bun plugins/terminal-browser-plugin/scripts/verify-package.ts --cli ../zcode-cli/bin/zcode.js
bun run marketplace:dev
node ../zcode-cli/bin/zcode.js plugins marketplace add "$PWD/.cache/marketplace" --yes
node ../zcode-cli/bin/zcode.js plugins install terminal-browser@zcode-plugins-dev --yes
```

For legacy directory-source plugins:

```bash
# from this repo root; ../zcode-cli must exist
node ../zcode-cli/bin/zcode.js plugins marketplace update zcode-plugins
Expand All @@ -173,6 +189,15 @@ session to load it — running sessions keep old MCP server processes.

## Modifying an existing plugin

terminal-browser: rebuild, run `package` and `verify:package`, and use
`marketplace:dev` for local installation under `zcode-plugins-dev`. Bump its
package/manifest versions together before release. After merging to `main`,
the `release-terminal-browser.yml` workflow publishes a verified versioned ZIP
and opens a marketplace update PR. Never insert an unavailable release URL or
overwrite existing release assets. The workflow must be allowed to create PRs.

For legacy directory-source plugins:

After editing plugin source, the full update flow is three steps (markdown-only
changes to `commands/` or `skills/` skip step 1):

Expand All @@ -187,13 +212,14 @@ Then start a new zcode session.

## Conventions and gotchas

- `dist/` is git-ignored for normal projects but plugin bundles are committed
so consumers never need Bun or a build step. Generated web assets
(`plugins/*/src/ui/assets.ts`) stay ignored. Exception: if a bundle grows
past ~1 MB with no dependency install story, reconsider what gets inlined.
- Version fields live in **three** places that must move together:
`.zcode-plugin/plugin.json`, the plugin `package.json`, and the marketplace
manifest entry. The install cache path is keyed by the plugin.json version.
- terminal-browser's `dist/` and release ZIPs are ignored; CI builds and verifies
them. zcode-model-config retains its committed bundle and directory source.
Generated web assets (`plugins/*/src/ui/assets.ts`) stay ignored.
- `.zcode-plugin/plugin.json` and the plugin `package.json` versions move
together. For directory sources the marketplace version moves with them;
for ZIP sources it changes only after publication and checksum verification.
The install cache path is keyed by the plugin.json version. Commands and skills
inside a ZIP also require a new release to reach consumers.
- Plugin ids are `<name>@zcode-plugins` — the marketplace half comes from the
top-level `name` in `.claude-plugin/marketplace.json` (`zcode-plugins`).
- `bun run --filter '*' build` at the root is the supported fan-out; `bun
Expand Down
Loading
Loading