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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
7 changes: 7 additions & 0 deletions .changeset/clerk-1-0-0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
"clerk": major
---

Release Clerk CLI 1.0 as the first stable `1.x` line.

This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI.
5 changes: 0 additions & 5 deletions .changeset/release-process-test.md

This file was deleted.

27 changes: 27 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
# Clerk CLI 1.0

Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool.

This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself.

Core workflows available in Clerk CLI `1.0.0` include:

- `clerk init` to bootstrap Clerk into supported projects and starters
- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association
- `clerk env pull` for pulling Clerk environment variables into local env files
- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management
- `clerk api` and `clerk apps` for direct platform access from the terminal
- `clerk doctor` for integration diagnostics and guided fixes
- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows

Install Clerk CLI:

```sh
# Homebrew (macOS / Linux)
brew install clerk/stable/clerk

# npm
npm install -g clerk
```

See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release.
4 changes: 0 additions & 4 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -224,7 +224,3 @@ clerk update
$ clerk update --yes Update without confirmation prompt
$ clerk update --all Update every clerk install on PATH
```

## Open Questions

- How do we keep types in sync with PLAPI?
5 changes: 4 additions & 1 deletion docs/releasing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether

On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`).

Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`.

Configuration:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)
Expand DownExpand Up@@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st
- Copies the compiled binary from the build artifacts
3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner)
4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API
5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes.

The releaser accepts these flags:

Expand DownExpand Up@@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk`
| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define |
| `install.sh` | Shell install script — downloads binary from GitHub Releases |
| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm |
| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes |
| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) |
| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets |
| `scripts/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) |
Expand Down
49 changes: 49 additions & 0 deletions scripts/lib/release-notes.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getStableReleaseCreateArgs } from "./release-notes.ts";

describe("getStableReleaseCreateArgs", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "release-notes-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("uses plain generated notes when no intro file exists", async () => {
expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});

test("prepends version-specific notes when the intro file exists", async () => {
const notesPath = join(tempDir, "v1.0.0.md");
await Bun.write(notesPath, "Intro line\n\n- Highlight");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: [
"gh",
"release",
"create",
"v1.0.0",
"--generate-notes",
"--notes",
"Intro line\n\n- Highlight",
],
notesPath,
});
});

test("ignores empty intro files", async () => {
await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n");

expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({
args: ["gh", "release", "create", "v1.0.0", "--generate-notes"],
});
});
});
28 changes: 28 additions & 0 deletions scripts/lib/release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
import { join } from "node:path";

const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes");

export type StableReleaseCreateArgs = {
args: string[];
notesPath?: string;
};

export async function getStableReleaseCreateArgs(
version: string,
notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR,
): Promise<StableReleaseCreateArgs> {
const tagName = `v${version}`;
const args = ["gh", "release", "create", tagName, "--generate-notes"];
const notesPath = join(notesDir, `v${version}.md`);

if (!(await Bun.file(notesPath).exists())) {
return { args };
}

const notes = (await Bun.file(notesPath).text()).trim();
if (!notes) {
return { args };
}

return { args: [...args, "--notes", notes], notesPath };
}
7 changes: 6 additions & 1 deletion scripts/releaser.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import { join } from "node:path";
import { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts";
import { run, isPublished, publish } from "./lib/npm.ts";
import { getStableReleaseCreateArgs } from "./lib/release-notes.ts";

const DIST_DIR = join(import.meta.dir, "../dist/platform-packages");
const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts");
Expand DownExpand Up@@ -136,7 +137,11 @@ if (!tag && !dryRun) {
console.log(`GitHub Release for ${tagName} already exists, skipping.`);
} else {
console.log(`Creating GitHub Release for ${tagName}...`);
await run(["gh", "release", "create", tagName, "--generate-notes"]);
const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version);
if (notesPath) {
console.log(`Prepending release intro from ${notesPath}...`);
}
await run(releaseArgs);
}
}

Expand Down
Loading