diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9544e01 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + pull_request: + branches: [dev, main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test suite + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint and format checks + run: bun run check + + - name: Run unit tests + run: bun test --pass-with-no-tests + + - name: Generator sync check + run: | + bun run build + if ! git diff --exit-code -- themes/; then + echo "::error::Generated theme artifacts are out of sync with palette/. Run 'bun run build' and commit the results." + git diff -- themes/ + exit 1 + fi diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 0000000..b5804fc --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,71 @@ +name: Release PR + +on: + workflow_dispatch: + inputs: + version: + description: "Next version (e.g. 1.2.0)" + required: true + type: string + +jobs: + open-release-pr: + name: Bump version and open release PR + runs-on: ubuntu-latest + steps: + - name: Validate version format + run: | + if ! [[ "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Version must be X.Y.Z (no pre-release suffixes). Got: ${{ inputs.version }}" + exit 1 + fi + + - uses: actions/checkout@v4 + with: + ref: dev + fetch-depth: 0 + token: ${{ secrets.RELEASE_PR_TOKEN }} + + - name: Confirm version is strictly greater than current + run: | + CURRENT=$(node -p "require('./package.json').version") + NEXT="${{ inputs.version }}" + HIGHEST=$(printf '%s\n%s\n' "$CURRENT" "$NEXT" | sort -V | tail -n1) + if [ "$CURRENT" = "$NEXT" ] || [ "$HIGHEST" != "$NEXT" ]; then + echo "::error::New version ($NEXT) must be strictly greater than current ($CURRENT)" + exit 1 + fi + echo "Bumping $CURRENT -> $NEXT" + + - uses: oven-sh/setup-bun@v2 + + - name: Bump version in all manifests + run: bun scripts/bump-version.ts "${{ inputs.version }}" + + - name: Commit and push bump + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "chore: bump version to ${{ inputs.version }}" + git push origin dev + + - name: Open release PR + env: + GH_TOKEN: ${{ secrets.RELEASE_PR_TOKEN }} + run: | + PR_BODY_FILE=$(mktemp) + cat > "$PR_BODY_FILE" <<'EOF' + + ## Release notes + + _Replace this paragraph with a short narrative for the release. If left unchanged, GitHub's auto-generated notes will be used instead._ + EOF + + PR_URL=$(gh pr create \ + --base main \ + --head dev \ + --title "Release v${{ inputs.version }}" \ + --body-file "$PR_BODY_FILE") + + echo "Opened release PR: $PR_URL" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..29608fd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,217 @@ +name: Release + +on: + push: + branches: [main] + +jobs: + tag-and-release: + name: Tag and create GitHub release + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + is_release: ${{ steps.tag-check.outputs.is_release }} + tag: ${{ steps.version.outputs.tag }} + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Read version from package.json + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "Version on main: $VERSION" + + - name: Check if tag already exists (idempotency) + id: tag-check + run: | + if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then + echo "Tag ${{ steps.version.outputs.tag }} already exists — skipping release." + echo "is_release=false" >> "$GITHUB_OUTPUT" + else + echo "is_release=true" >> "$GITHUB_OUTPUT" + fi + + - name: Build release notes + if: steps.tag-check.outputs.is_release == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_BODY=$(gh pr list \ + --state merged \ + --base main \ + --limit 1 \ + --json body \ + --jq '.[0].body // ""') + + if [ -n "$PR_BODY" ] && ! echo "$PR_BODY" | grep -q "Replace this paragraph"; then + echo "Using release PR body for notes." + printf '%s\n' "$PR_BODY" > /tmp/release-notes.md + else + echo "Falling back to auto-generated notes." + PREVIOUS_TAG=$(git tag --sort=-creatordate \ + | grep -v "^${{ steps.version.outputs.tag }}$" \ + | head -n1) + if [ -n "$PREVIOUS_TAG" ]; then + gh api "repos/${{ github.repository }}/releases/generate-notes" \ + -f tag_name="${{ steps.version.outputs.tag }}" \ + -f previous_tag_name="$PREVIOUS_TAG" \ + --jq '.body' > /tmp/release-notes.md + else + gh api "repos/${{ github.repository }}/releases/generate-notes" \ + -f tag_name="${{ steps.version.outputs.tag }}" \ + --jq '.body' > /tmp/release-notes.md + fi + fi + + echo "--- Release notes preview ---" + cat /tmp/release-notes.md + echo "--- end preview ---" + + - name: Create GitHub release + if: steps.tag-check.outputs.is_release == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.tag }}" \ + --title "Synthpunk ${{ steps.version.outputs.tag }}" \ + --notes-file /tmp/release-notes.md + + - name: Create and push tag + if: steps.tag-check.outputs.is_release == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag "${{ steps.version.outputs.tag }}" + git push origin "${{ steps.version.outputs.tag }}" + + build-artifacts: + name: Build and upload release artifacts + needs: tag-and-release + if: needs.tag-and-release.outputs.is_release == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.tag-and-release.outputs.tag }} + + - uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build themes + run: bun run build + + - name: Run tests + run: bun test --pass-with-no-tests + + - name: Upload Group 2 files to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.tag-and-release.outputs.tag }} + run: | + gh release upload "$TAG" \ + themes/starship/starship.toml \ + themes/wezterm/synthpunk-pastel-dark.toml \ + themes/wezterm/synthpunk-pastel-light.toml \ + themes/wezterm/synthpunk-neon-dark.toml \ + themes/wezterm/synthpunk-neon-light.toml + + - name: Build VS Code VSIX + run: | + cd themes/vscode + npx --yes @vscode/vsce package -o "../../synthpunk-${{ needs.tag-and-release.outputs.tag }}.vsix" + + - name: Upload VSIX as workflow artifact + uses: actions/upload-artifact@v4 + with: + name: synthpunk-vsix + path: synthpunk-${{ needs.tag-and-release.outputs.tag }}.vsix + + publish-vscode: + name: Publish to VS Code Marketplace + needs: build-artifacts + if: vars.PUBLISH_VSCODE == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: synthpunk-vsix + + - name: Publish to VS Code Marketplace + run: npx --yes @vscode/vsce publish --package-path synthpunk-*.vsix -p ${{ secrets.VS_MARKETPLACE_TOKEN }} + + publish-openvsx: + name: Publish to OpenVSX + needs: build-artifacts + if: vars.PUBLISH_OPENVSX == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: synthpunk-vsix + + - name: Publish to OpenVSX + run: npx --yes ovsx publish synthpunk-*.vsix -p ${{ secrets.OVSX_TOKEN }} + + publish-zed: + name: Publish Zed extension + needs: [build-artifacts, tag-and-release] + if: vars.PUBLISH_ZED == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.tag-and-release.outputs.tag }} + + - name: Publish Zed extension + working-directory: themes/zed + run: | + # The zed CLI may not be available on CI runners. If this fails, + # see RELEASE.md for manual publishing instructions. + zed extension publish + + publish-neovim: + name: Publish to synthpunk.nvim repo + needs: [build-artifacts, tag-and-release] + if: vars.PUBLISH_NVIM == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.tag-and-release.outputs.tag }} + fetch-depth: 0 + + - name: Clone synthpunk.nvim + env: + SSH_DEPLOY_KEY: ${{ secrets.SYNTHPUNK_NVIM_DEPLOY_KEY }} + run: | + mkdir -p ~/.ssh + echo "$SSH_DEPLOY_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan github.com >> ~/.ssh/known_hosts + GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key" git clone git@github.com:slowdini/synthpunk.nvim.git /tmp/synthpunk.nvim + + - name: Copy neovim files and push + env: + TAG: ${{ needs.tag-and-release.outputs.tag }} + GIT_SSH_COMMAND: ssh -i ~/.ssh/deploy_key + run: | + cd /tmp/synthpunk.nvim + find . -maxdepth 1 -not -name '.git' -not -name '.' -exec rm -rf {} + + cp -r "$GITHUB_WORKSPACE/themes/neovim/"* . + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "chore: release $TAG" + git tag "$TAG" + git push origin main + git push origin "$TAG" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..466bfa4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +# AGENTS.md + +## Project structure + +Synthpunk is a synthwave-inspired color scheme that ships as themes for multiple tools. All theme artifacts are generated from a single palette source. + +- `palette/` — source-of-truth color palette data (the only place to edit colors) +- `generator/` — TypeScript theme-generation engine (run via `bun run build`) +- `themes/` — generated artifacts, one subdirectory per target: + - `vscode/` — VS Code extension package + - `zed/` — Zed extension package + - `neovim/` — Neovim colorscheme (Lua) + - `wezterm/` — WezTerm color scheme files (TOML) + - `starship/` — Starship prompt config (TOML) +- `scripts/` — release tooling (version bumping) +- `tests/` — cross-target tests (version lockstep) +- `assets/` — preview images for the README +- `schemas/` — JSON schemas for palette files + +## Commands + +```sh +bun install # install dependencies +bun run build # regenerate all theme artifacts from palette/ +bun test # run all tests +bun run check # biome lint/format + tsc typecheck +bun run format # auto-format with biome +``` + +## Key rules + +- **Never edit `themes/**` directly.** These files are generated. Edit `palette/` and `generator/`, then run `bun run build`. CI enforces that committed artifacts match generator output. +- **Version lockstep.** The version in `package.json`, `themes/vscode/package.json`, and `themes/zed/extension.toml` must always match. Use `bun scripts/bump-version.ts ` to bump all at once. The lockstep is enforced by `tests/lockstep.test.ts`. + +## Commit conventions + +Use Conventional Commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`. + +## Release process + +See [`RELEASE.md`](RELEASE.md) for the full release operations guide. The short version: + +1. Feature PRs merge into `dev` after CI passes. +2. Trigger the "Release PR" workflow with the next version — it bumps manifests and opens a `dev → main` PR. +3. Merge the release PR into `main` — this auto-tags, creates the GitHub release, and publishes to all configured channels. +4. Merge `main` back into `dev`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c48dcb6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Synthpunk contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..86a15f7 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Synthpunk + +High-energy synthwave-inspired color themes for VS Code, Zed, Neovim, WezTerm, and Starship. Four variants: Pastel Dark/Light and Neon Dark/Light. + +![Synthpunk Pastel Dark](assets/pastel-dark.png) + +## Installation + +### VS Code + +Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=synthpunk.synthpunk) (search "Synthpunk"), then select your theme via `Preferences: Color Theme`. + +For local development, see [`themes/vscode/README.md`](themes/vscode/README.md). + +### Zed + +Install from the Zed extension registry (search "Synthpunk"), then select your theme from the theme picker. + +For local development, see [`themes/zed/README.md`](themes/zed/README.md). + +### Neovim + +Install via [Lazy.nvim](https://github.com/folke/lazy.nvim): + +```lua +return { + "slowdini/synthpunk.nvim", + lazy = false, + priority = 1000, + config = function() + vim.cmd("colorscheme synthpunk-pastel-dark") + end, +} +``` + +See [`themes/neovim/README.md`](themes/neovim/README.md) for manual installation and variant details. + +### WezTerm + +```sh +curl -fsSL https://github.com/slowdini/synthpunk/releases/latest/download/synthpunk-pastel-dark.toml -o ~/.config/wezterm/colors/synthpunk-pastel-dark.toml +``` + +Then set `config.color_scheme = 'Synthpunk Pastel Dark'` in your `wezterm.lua`. See [`themes/wezterm/README.md`](themes/wezterm/README.md) for all variants. + +### Starship + +```sh +curl -fsSL https://github.com/slowdini/synthpunk/releases/latest/download/starship.toml -o ~/.config/starship.toml +``` + +The default variant is `synthpunk_pastel_dark`. To switch, change the `palette =` line to `synthpunk_pastel_light`, `synthpunk_neon_dark`, or `synthpunk_neon_light`. See [`themes/starship/README.md`](themes/starship/README.md) for details. + +## Development + +```sh +bun install # install dependencies +bun run build # regenerate all theme artifacts from palette/ +bun test # run tests +bun run check # lint + typecheck +bun run format # auto-format +``` + +All theme files under `themes/` are generated from `palette/` by the generator. Never edit them directly — edit `palette/` and `generator/`, then run `bun run build`. + +## Releasing + +Releases are cut from `dev` and tagged from `main`: + +1. Merge feature PRs into `dev` after CI passes. +2. When ready to ship, trigger the **Release PR** workflow with the next version number. It bumps every manifest via `scripts/bump-version.ts`, commits to `dev`, and opens a `dev → main` PR. +3. Review the release PR and merge. +4. Merging to `main` automatically tags `vX.Y.Z`, creates the GitHub release, uploads the Starship and WezTerm config files as release assets, builds the VS Code VSIX, and fans out publish jobs (VS Code Marketplace, OpenVSX, Zed, synthpunk.nvim) — each gated on a configured secret. +5. After a release, merge `main` back into `dev` to keep them in sync. + +See [`RELEASE.md`](RELEASE.md) for manual publishing steps and prerequisite setup. + +## License + +MIT diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..01b8356 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,92 @@ +# Release Operations + +This document catalogues the manual steps and prerequisites for the release workflow. The automated flow lives in `.github/workflows/`. Each publish channel is gated on a repository variable — when the variable is `true` and the corresponding secret is configured, publishing is automatic. Otherwise the channel is skipped and the steps below serve as the manual fallback. + +## Prerequisites (one-time setup) + +### GitHub + +- **`RELEASE_PR_TOKEN`** secret: a Personal Access Token with repo contents-write. Used by `release-pr.yml` to push the bump commit to `dev` and open the release PR. The default `GITHUB_TOKEN` is avoided because PRs opened with it don't trigger CI. +- **`dev`** branch as the default branch. Feature work lands on `dev`; only release PRs go `dev → main`. +- Branch protection on `main` (PR + required CI) and `dev` (required CI on PRs). + +### VS Code Marketplace + +- A publisher account named `synthpunk` on the [VS Code Marketplace](https://marketplace.visualstudio.com/manage). +- **`VS_MARKETPLACE_TOKEN`** secret: a Personal Access Token from the Marketplace publisher dashboard. +- **`PUBLISH_VSCODE`** variable set to `true` to enable auto-publishing. + +### OpenVSX + +- An account on [OpenVSX](https://open-vsx.org). +- **`OVSX_TOKEN`** secret: an access token from your OpenVSX settings. +- **`PUBLISH_OPENVSX`** variable set to `true` to enable auto-publishing. + +### Zed + +- A Zed account with extension publishing access. +- **`ZED_PUBLISH_TOKEN`** secret: the publish token (if the `zed extension publish` CLI supports it — confirm the exact mechanism). +- **`PUBLISH_ZED`** variable set to `true` to enable auto-publishing. + +### Neovim (synthpunk.nvim) + +- An empty `slowdini/synthpunk.nvim` repository on GitHub. +- **`SYNTHPUNK_NVIM_DEPLOY_KEY`** secret: an SSH deploy key with write access to `slowdini/synthpunk.nvim`. +- **`PUBLISH_NVIM`** variable set to `true` to enable auto-publishing. + +## Manual publishing (when a channel is not automated) + +If a publish variable is not set (or the secret is missing), the corresponding job is skipped. Run these commands manually after the GitHub release is created: + +### VS Code Marketplace + +```sh +git checkout vX.Y.Z +cd themes/vscode +npx @vscode/vsce publish -p +``` + +### OpenVSX + +```sh +git checkout vX.Y.Z +cd themes/vscode +npx @vscode/vsce package -o synthpunk-vX.Y.Z.vsix +npx ovsx publish synthpunk-vX.Y.Z.vsix -p +``` + +### Zed + +```sh +git checkout vX.Y.Z +cd themes/zed +zed extension publish +``` + +### Neovim (synthpunk.nvim) + +```sh +git checkout vX.Y.Z +git clone git@github.com:slowdini/synthpunk.nvim.git /tmp/synthpunk.nvim +cd /tmp/synthpunk.nvim +rm -rf -- */ +cp -r /path/to/synthpunk/themes/neovim/* . +git add -A +git commit -m "chore: release vX.Y.Z" +git tag vX.Y.Z +git push origin main +git push origin vX.Y.Z +``` + +## Release flow summary + +1. **Trigger**: Actions → "Release PR" workflow → Run with version `X.Y.Z`. +2. **Bump**: `release-pr.yml` bumps all manifests, commits to `dev`, opens `dev → main` PR. +3. **Review**: Edit the PR body with release notes, merge into `main`. +4. **Auto-release**: `release.yml` on push to `main`: + - Tags `vX.Y.Z`, creates GitHub Release (notes from PR body or auto-generated). + - Builds all themes, uploads Starship + WezTerm files to the release. + - Builds VS Code VSIX, publishes to Marketplace/OpenVSX if configured. + - Publishes Zed extension if configured. + - Splits `themes/neovim/` to `slowdini/synthpunk.nvim` if configured. +5. **Back-sync**: Merge `main` back into `dev`. diff --git a/biome.json b/biome.json index c1277a3..f6e1f23 100644 --- a/biome.json +++ b/biome.json @@ -23,6 +23,9 @@ "quoteStyle": "double" } }, + "json": { + "formatter": { "enabled": true } + }, "assist": { "enabled": true, "actions": { diff --git a/package.json b/package.json index 62b496d..ecc6a09 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,10 @@ { - "name": "add-linting-formatting-setup", + "name": "synthpunk", + "version": "0.1.1", "module": "index.ts", "type": "module", "private": true, + "license": "MIT", "devDependencies": { "@biomejs/biome": "2.4.15", "@types/bun": "latest", @@ -12,7 +14,7 @@ "typescript": "^6.0.3" }, "scripts": { - "build": "bun run generator/index.ts", + "build": "bun run generator/index.ts && biome format --write themes/vscode/themes/*.json themes/zed/themes/*.json", "test": "bun test", "format": "biome check --write", "check": "biome check && tsc --noEmit", diff --git a/scripts/bump-version.test.ts b/scripts/bump-version.test.ts new file mode 100644 index 0000000..75843cd --- /dev/null +++ b/scripts/bump-version.test.ts @@ -0,0 +1,104 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { bumpFiles } from "./bump-version"; + +const FIXTURE_ROOT = join(tmpdir(), `synthpunk-bump-test-${process.pid}`); + +beforeAll(() => { + mkdirSync(FIXTURE_ROOT, { recursive: true }); +}); + +afterAll(() => { + rmSync(FIXTURE_ROOT, { recursive: true, force: true }); +}); + +describe("bump-version bumpFiles", () => { + test("bumps JSON version field and emits biome-canonical formatting (short arrays stay one line)", () => { + // Use a non-package.json filename: biome has special handling for + // package.json that preserves expanded arrays. + const file = join(FIXTURE_ROOT, "manifest.json"); + writeFileSync( + file, + [ + "{", + ' "name": "demo",', + ' "version": "0.0.1",', + ' "keywords": [', + ' "synthwave",', + ' "vaporwave"', + " ]", + "}", + "", + ].join("\n"), + ); + + const updated = bumpFiles([file], "1.2.3"); + expect(updated).toEqual([file]); + + const content = readFileSync(file, "utf8"); + const parsed = JSON.parse(content) as { + version: string; + keywords: string[]; + }; + + expect(parsed.version).toBe("1.2.3"); + expect(parsed.keywords).toEqual(["synthwave", "vaporwave"]); + expect(content).toContain('"keywords": ["synthwave", "vaporwave"]'); + expect(content).toMatch(/\n$/); + }); + + test("bumps TOML version line in extension.toml", () => { + const file = join(FIXTURE_ROOT, "extension.toml"); + writeFileSync( + file, + [ + 'id = "synthpunk"', + 'name = "Synthpunk"', + 'version = "0.1.0"', + "schema_version = 1", + 'repository = "https://github.com/slowdini/synthpunk"', + "", + ].join("\n"), + ); + + const updated = bumpFiles([file], "2.0.0"); + expect(updated).toEqual([file]); + + const content = readFileSync(file, "utf8"); + expect(content).toContain('version = "2.0.0"'); + expect(content).not.toContain('version = "0.1.0"'); + expect(content).toContain('id = "synthpunk"'); + expect(content).toContain("schema_version = 1"); + }); + + test("skips JSON files without a version field", () => { + const noVersion = join(FIXTURE_ROOT, "no-version.json"); + writeFileSync(noVersion, `${JSON.stringify({ name: "x" }, null, 2)}\n`); + + const updated = bumpFiles([noVersion], "3.0.0"); + expect(updated).toEqual([]); + }); + + test("handles mixed JSON and TOML files in one call", () => { + const jsonFile = join(FIXTURE_ROOT, "mixed.json"); + writeFileSync( + jsonFile, + `${JSON.stringify({ name: "demo", version: "0.0.1" }, null, 2)}\n`, + ); + const tomlFile = join(FIXTURE_ROOT, "mixed.toml"); + writeFileSync(tomlFile, `id = "demo"\nversion = "0.0.1"\n`); + + const updated = bumpFiles([jsonFile, tomlFile], "4.5.6"); + expect(updated).toEqual([jsonFile, tomlFile]); + + const jsonParsed = JSON.parse(readFileSync(jsonFile, "utf8")) as { + version: string; + }; + expect(jsonParsed.version).toBe("4.5.6"); + + const tomlContent = readFileSync(tomlFile, "utf8"); + expect(tomlContent).toContain('version = "4.5.6"'); + }); +}); diff --git a/scripts/bump-version.ts b/scripts/bump-version.ts new file mode 100644 index 0000000..4d9a525 --- /dev/null +++ b/scripts/bump-version.ts @@ -0,0 +1,91 @@ +#!/usr/bin/env bun +import { readFileSync, writeFileSync } from "node:fs"; +import { VERSION_LOCKED_MANIFESTS } from "./manifest-files"; + +/** + * Rewrites the `version` field in each manifest, then runs JSON files through + * biome so the output is byte-for-byte what `biome check` produces. Without the + * biome pass, `JSON.stringify(_, null, 2)` explodes short arrays + * one-element-per-line while biome collapses them — so every bump would + * reintroduce a formatting diff the pre-commit hook then fights. TOML files are + * updated in-place with a targeted regex (biome does not format TOML). Returns + * the list of files that were actually updated. + */ +export function bumpFiles(files: readonly string[], version: string): string[] { + const updatedFiles: string[] = []; + const jsonFiles: string[] = []; + + for (const file of files) { + if (file.endsWith(".toml")) { + if (bumpToml(file, version)) { + updatedFiles.push(file); + console.log(`Bumped ${file}`); + } else { + console.log(`Skipped ${file} (no version field)`); + } + } else { + if (bumpJson(file, version)) { + updatedFiles.push(file); + jsonFiles.push(file); + console.log(`Bumped ${file}`); + } else { + console.log(`Skipped ${file} (no version field)`); + } + } + } + + if (jsonFiles.length > 0) { + formatWithBiome(jsonFiles); + } + + return updatedFiles; +} + +function bumpJson(file: string, version: string): boolean { + const content = JSON.parse(readFileSync(file, "utf8")); + if (content.version === undefined) { + return false; + } + content.version = version; + writeFileSync(file, `${JSON.stringify(content, null, 2)}\n`); + return true; +} + +function bumpToml(file: string, version: string): boolean { + const content = readFileSync(file, "utf8"); + const versionPattern = /^version = ".*"/m; + if (!versionPattern.test(content)) { + return false; + } + const updated = content.replace(versionPattern, `version = "${version}"`); + writeFileSync(file, updated); + return true; +} + +/** + * Normalizes the given JSON files with the project's biome so a version bump + * never leaves a file in a state `biome check` would want to reformat. Fails + * loudly: a silent skip would reintroduce the formatting drift this exists to + * prevent. + */ +function formatWithBiome(files: string[]): void { + const result = Bun.spawnSync( + ["bunx", "@biomejs/biome", "format", "--write", ...files], + { stdout: "pipe", stderr: "pipe" }, + ); + if (result.exitCode !== 0) { + console.error(result.stderr.toString()); + throw new Error( + `biome formatting failed (exit ${result.exitCode}); manifests may be left in a non-canonical format`, + ); + } +} + +if (import.meta.main) { + const version = process.argv[2]; + if (!version || !/^\d+\.\d+\.\d+/.test(version)) { + console.error("Usage: bun scripts/bump-version.ts "); + process.exit(1); + } + bumpFiles(VERSION_LOCKED_MANIFESTS, version); +} diff --git a/scripts/manifest-files.ts b/scripts/manifest-files.ts new file mode 100644 index 0000000..80a7c63 --- /dev/null +++ b/scripts/manifest-files.ts @@ -0,0 +1,9 @@ +// Single source of truth for the manifests kept in version lockstep. +// Consumed by scripts/bump-version.ts (to rewrite each version) and by +// tests/lockstep.test.ts (to assert each matches package.json). +// Paths are relative to the repository root. +export const VERSION_LOCKED_MANIFESTS = [ + "package.json", + "themes/vscode/package.json", + "themes/zed/extension.toml", +] as const; diff --git a/tests/lockstep.test.ts b/tests/lockstep.test.ts new file mode 100644 index 0000000..8f0beb0 --- /dev/null +++ b/tests/lockstep.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { VERSION_LOCKED_MANIFESTS } from "../scripts/manifest-files"; + +const REPO_ROOT = join(import.meta.dir, ".."); + +function readVersion(relPath: string): string | undefined { + const content = readFileSync(join(REPO_ROOT, relPath), "utf8"); + + if (relPath.endsWith(".toml")) { + const match = content.match(/^version = "(.*)"/m); + return match?.[1]; + } + + const parsed = JSON.parse(content) as { version?: string }; + return parsed.version; +} + +describe("version lockstep", () => { + const packageVersion = readVersion("package.json") as string; + + test.each([ + ...VERSION_LOCKED_MANIFESTS, + ])("%s version matches package.json (%s)", (relPath) => { + const version = readVersion(relPath); + expect(version).toBeDefined(); + expect(version).toBe(packageVersion); + }); +}); diff --git a/themes/neovim/README.md b/themes/neovim/README.md index 31e1972..9f0160f 100644 --- a/themes/neovim/README.md +++ b/themes/neovim/README.md @@ -11,25 +11,11 @@ A Neovim colorscheme generated from the [Synthpunk](https://github.com/slowdini/ ## Installation -### Manual (no plugin manager) - -```bash -git clone https://github.com/slowdini/synthpunk /tmp/synthpunk -cp -r /tmp/synthpunk/themes/neovim/* ~/.config/nvim/ -``` - -Then in your Neovim config: - -```lua -vim.cmd("colorscheme synthpunk-pastel-dark") -``` - ### Lazy.nvim ```lua return { - "slowdini/synthpunk", - dir = "~/path/to/synthpunk/themes/neovim", + "slowdini/synthpunk.nvim", lazy = false, priority = 1000, config = function() @@ -38,6 +24,19 @@ return { } ``` +### Manual (no plugin manager) + +```bash +git clone https://github.com/slowdini/synthpunk.nvim /tmp/synthpunk.nvim +cp -r /tmp/synthpunk.nvim/* ~/.config/nvim/ +``` + +Then in your Neovim config: + +```lua +vim.cmd("colorscheme synthpunk-pastel-dark") +``` + ## Requirements - Neovim >= 0.8 (for `vim.api.nvim_set_hl`) diff --git a/themes/starship/README.md b/themes/starship/README.md index bc59392..e2ebb7b 100644 --- a/themes/starship/README.md +++ b/themes/starship/README.md @@ -18,8 +18,4 @@ A [Nerd Font](https://www.nerdfonts.com/) must be installed and enabled in your ## Regenerating -Run from the project root: - -```sh -cd generator && bun run src/index.ts -``` \ No newline at end of file +Run `bun run build` from the repo root. \ No newline at end of file diff --git a/themes/vscode/.vscodeignore b/themes/vscode/.vscodeignore new file mode 100644 index 0000000..0fdcffb --- /dev/null +++ b/themes/vscode/.vscodeignore @@ -0,0 +1,3 @@ +**/.DS_Store +**/*.test.ts +README.md diff --git a/themes/vscode/README.md b/themes/vscode/README.md index 9dafe8c..8cfb59b 100644 --- a/themes/vscode/README.md +++ b/themes/vscode/README.md @@ -24,8 +24,4 @@ To test this extension locally in VSCode: ## Theme Generation -Themes are generated from palette source files. Do not edit theme files directly — run: - -```bash -cd generator && bun run src/index.ts -``` \ No newline at end of file +Themes are generated from palette source files. Do not edit theme files directly — run `bun run build` from the repo root. \ No newline at end of file diff --git a/themes/vscode/package.json b/themes/vscode/package.json index 216cec0..0285687 100644 --- a/themes/vscode/package.json +++ b/themes/vscode/package.json @@ -2,9 +2,13 @@ "name": "synthpunk", "displayName": "Synthpunk", "description": "High-energy synthwave-inspired color themes for VSCode", - "version": "0.1.0", + "version": "0.1.1", "publisher": "synthpunk", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/slowdini/synthpunk" + }, "engines": { "vscode": "^1.80.0" }, diff --git a/themes/wezterm/README.md b/themes/wezterm/README.md index 8205965..e572bcb 100644 --- a/themes/wezterm/README.md +++ b/themes/wezterm/README.md @@ -25,8 +25,4 @@ Available color schemes: ## Regenerating -Run from the project root: - -```sh -cd generator && bun run src/index.ts -``` \ No newline at end of file +Run `bun run build` from the repo root. \ No newline at end of file diff --git a/themes/zed/extension.toml b/themes/zed/extension.toml index fd26c76..ed90a08 100644 --- a/themes/zed/extension.toml +++ b/themes/zed/extension.toml @@ -1,7 +1,7 @@ -id = "synthpunk-pastel-theme" -name = "Synthpunk Pastel" -version = "0.1.0" +id = "synthpunk" +name = "Synthpunk" +version = "0.1.1" schema_version = 1 authors = ["Synthpunk"] -description = "A pastel synthwave-inspired color theme for Zed" -repository = "https://github.com/maxhaarhaus/synthpunk" +description = "Synthwave-inspired color themes (pastel + neon, dark + light) for Zed" +repository = "https://github.com/slowdini/synthpunk"