Skip to content
Open
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
5 changes: 2 additions & 3 deletions .release-it.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/release-it@19/schema/release-it.json",
"$schema": "https://unpkg.com/release-it@21/schema/release-it.json",
"git": {
"commitMessage": "chore: release v${version}",
"tagName": "v${version}",
Expand All @@ -14,8 +14,7 @@
"releaseName": "v${version}"
},
"npm": {
"publish": true,
"publishArgs": ["--access", "public"]
"publish": false
},
"hooks": {
"before:init": ["bash scripts/check-changelog.sh", "bun run verify"],
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

### Added
- `rd update` updates the CLI the way it was installed, with `--check` to report only (exit 0 up to date, 1 update available, 2 unknown) and `--force` to reinstall even when current. `rd update <id>` still updates a bookmark.

### Changed
- `rd --version` now prints the commit as well: `rd X.Y.Z (sha7 YYYY-MM-DD)`, with `-dirty` for a build from uncommitted changes, so it is obvious which commit is installed.

## [0.3.1] - 2026-07-15

### Fixed
Expand Down
140 changes: 33 additions & 107 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,47 @@ Built for power users, automation workflows, and AI agent integration.

## Requirements

- Node.js 20+
- Node.js 22+

## Installation

```bash
npm install -g raindrop-cli
npm install -g raindrop-cli # or: pnpm add -g raindrop-cli, bun add -g raindrop-cli
npx raindrop-cli --help # run without installing
```

Or use without installing:
```bash
npx raindrop-cli --help
```
Requires Node.js 22 or later.

This installs `rd` as the primary command. An `rdcli` alias is also available if `rd` conflicts with another tool:

- **Windows**: `rd` is a built-in command (remove directory)
- **oh-my-zsh**: `rd` is aliased to `rmdir` — add `unalias rd` to your `~/.zshrc` or use `rdcli`

### From source

```bash
git clone https://github.com/crcatala/raindrop-cli.git
cd raindrop-cli
bun install
bun run build
bun link # puts `rd` on your PATH, pointing at this clone
rd --version # rd X.Y.Z (sha7 YYYY-MM-DD): the commit you built
```

Rebuild after pulling (`bun run build`); the link keeps pointing at the clone. If `rd` is
also installed globally, remove that copy so the linked build is the one on `PATH`
(`command -v rd` should resolve inside the clone).

### Updating

```bash
rd update --check # exit 0: up to date, 1: update available, 2: unknown
rd update # updates the same way it was installed
```

`rd update` pulls and rebuilds a source checkout (it refuses when the tree is dirty or
not on `main`), and runs `npm install -g raindrop-cli@latest` for a global npm install.

## Quick Start

### 1. Get Your API Token
Expand Down Expand Up @@ -351,107 +374,10 @@ src/

## Releasing

For maintainers publishing a new version to npm. We use [release-it](https://github.com/release-it/release-it) to automate the release process.

### Prerequisites

- Push access to the repository
- npm account with publish rights to `raindrop-cli` (`npm whoami` to verify)
- ggshield installed and authenticated (`ggshield auth status`)
- `GITHUB_TOKEN` environment variable set (for creating GitHub releases)

#### Getting a GitHub Token

1. Go to [GitHub Settings → Tokens](https://github.com/settings/tokens?type=beta) (fine-grained)
2. Click **Generate new token**
3. Set expiration and select **Only select repositories** → `raindrop-cli`
4. Under **Repository permissions**, set **Contents** to **Read and write**
5. Generate and export: `export GITHUB_TOKEN=github_pat_xxxx`

Or use a [classic token](https://github.com/settings/tokens/new) with the `repo` scope (broader access).

### Before You Release

Generate a changelog draft from commits since the latest release:

```bash
bun run release:prep
```

The helper prints commit history, a changed-files summary, and a prompt you can use to draft changelog entries. Review the result and update `CHANGELOG.md` under the `[Unreleased]` section:

```markdown
## [Unreleased]

### Added
- New feature X

### Fixed
- Bug Y
```

The helper does not modify the changelog. The release process will automatically move your `[Unreleased]` entries to the new version section.

### Release Process

```bash
# 1. Ensure you're on main with latest changes
git checkout main
git pull

# 2. Preview the local release steps (recommended first time)
bun run release:dry

# 3. Run the release
bun run release
```

`release:dry` deliberately disables the npm and GitHub integrations, so it cannot publish or open GitHub's release form. Run `release` for the full process.

The interactive prompt will ask you to select the version bump (patch/minor/major). Then release-it will:

1. **Verify** — runs tests, lint, typecheck, format
2. **Bump version** — updates `package.json`
3. **Update changelog** — moves "Unreleased" items to new version section with date
4. **Package smoke test** — rebuilds `dist/`, packs it, and executes the installed `rd` CLI
5. **Commit** — creates "chore: release vX.Y.Z" commit
6. **Tag** — creates `vX.Y.Z` git tag
7. **Secret scan** — ggshield scans a freshly rebuilt `dist/` for leaked secrets
8. **Publish to npm** — rebuilds immediately before packing and uploads the artifact
9. **Push** — pushes commit and tag to GitHub
10. **GitHub Release** — creates release with changelog notes

### Manual Steps (if needed)

If you need to skip certain steps or recover from a partial release:

```bash
# Skip npm publish (e.g., if it succeeded but git push failed)
bun run release -- --no-npm

# Skip GitHub release
bun run release -- --no-github

# Specific version (skip prompt)
bun run release -- 1.2.3

# Pre-release
bun run release -- --preRelease=beta
```

### Verify After Release

```bash
npm info raindrop-cli
npx raindrop-cli@latest --version
```

### Notes

- **Immutable** — published npm versions cannot be changed, only deprecated
- **Clean working directory required** — commit or stash changes before releasing
- **Main branch only** — releases must be from the `main` branch
- **Secrets** — publish will fail if ggshield detects secrets in the bundle
For maintainers publishing a new version to npm. Releases are cut by hand from a clean
`main` with [release-it](https://github.com/release-it/release-it), wrapped by
`scripts/release.mjs`; the full process, prerequisites and recovery steps are in
[RELEASING.md](RELEASING.md).

## Contributing

Expand Down
98 changes: 98 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Releasing

Releases are cut by hand from a clean `main` with
[release-it](https://github.com/release-it/release-it), wrapped by
`scripts/release.mjs`. One command bumps the version, moves the changelog's
Unreleased section into the new version, commits, tags `vX.Y.Z`, pushes,
creates the GitHub Release, and then publishes `raindrop-cli` to npm from the
tagged commit.

## Prerequisites

- Push access to `main`, and a GitHub token for the release (`GITHUB_TOKEN`,
or release-it opens the GitHub release form in the browser instead):

```bash
export GITHUB_TOKEN=github_pat_xxxx
```

A fine-grained token needs **Contents: Read and write** on
`crcatala/raindrop-cli`; a classic token needs the `repo` scope.
- An npm login with publish rights to `raindrop-cli`: `npm whoami`, else `npm login`.
- `ggshield` installed and authenticated (`ggshield auth status`). `npm publish`
runs `prepublishOnly`, which rebuilds and then scans the source tree and
`dist/` for secrets, so a publish fails without it.

## Before releasing

1. Start from an up-to-date `main`:

```bash
git checkout main
git pull --ff-only
```

2. Draft the changelog:

```bash
bun run release:prep
```

Paste the output into an LLM, review the result, and put it under
`## [Unreleased]` in `CHANGELOG.md` (Added, Changed, Fixed, Removed,
Security; user-facing changes only). Commit it to `main` and push.

## Release

```bash
bun run release:dry # preview; changes nothing and skips the hooks
bun run release # asks for the version; or: bun run release -- patch|minor|major
```

In order, `bun run release`:

1. refuses to start when not logged in to npm or when `HEAD` is already a release;
2. checks that Unreleased has entries and runs `bun run verify`;
3. bumps the version and the changelog, then runs `bun run test:package` (packs
the tarball, installs it, checks `--help` and `--version` for every command);
4. commits `chore: release vX.Y.Z`, tags `vX.Y.Z`, pushes both;
5. creates the GitHub Release;
6. publishes to npm from the tagged commit (npm may ask for a one-time password).
Its `prepublishOnly` hook rebuilds and runs the `ggshield` secret scans
before the tarball is uploaded.

A prerelease is published under a dist-tag named after its identifier, so
`latest` keeps pointing at the last full release:

```bash
bun run release -- --preRelease=beta # 1.3.0-beta.0, published as raindrop-cli@beta
bun run release -- --preRelease=beta --npm.tag=next # same version, under "next"
```

## When a step fails

- **Before the commit** (steps 1–3): nothing was changed on the remote. Fix the
cause and run `bun run release` again; `git checkout -- .` discards a
half-made bump.
- **GitHub Release** (step 5): the tag is pushed. Create the release from the
tag with that version's CHANGELOG section as notes
(`gh release create vX.Y.Z --title vX.Y.Z --notes-file <section>`), then run
`bun run release:publish`.
- **npm publish** (step 6): the tag is pushed; only the publish is left. Run
`bun run release:publish` from the tagged commit (it picks the prerelease
dist-tag again; `bun run release:publish -- --tag <tag>` chooses another).

## Verify

```bash
npm view raindrop-cli version
npx raindrop-cli@latest --version # rd X.Y.Z (sha7 YYYY-MM-DD)
```

## Notes

- **Immutable** — published npm versions cannot be changed, only deprecated.
- **Clean working directory required** — commit or stash changes before releasing.
- **Main branch only** — releases must be from the `main` branch.
- **Secrets** — the publish fails if `ggshield` detects secrets in the source
tree or the freshly built `dist/`.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
"!dist/**/*.test.d.ts",
"!dist/**/*.test.d.ts.map",
"!dist/test-utils",
"dist/build-info.json",
"README.md",
"LICENSE"
],
"scripts": {
"clean": "node -e \"for (const path of ['dist', 'tsconfig.tsbuildinfo']) require('node:fs').rmSync(path, { recursive: true, force: true })\"",
"build": "bun run clean && bun build src/cli.ts --outdir dist --target node --format esm && chmod +x dist/cli.js && bun run build:dts",
"build": "bun run clean && bun build src/cli.ts --outdir dist --target node --format esm && chmod +x dist/cli.js && bun run build:dts && node scripts/build-info.mjs",
"build:dts": "tsc --emitDeclarationOnly --declaration --outDir dist",
"dev": "bun src/index.ts",
"test": "bash scripts/check.sh test",
Expand All @@ -46,9 +47,10 @@
"verify:verbose": "VERBOSE=1 bun scripts/verify.ts",
"prepack": "bun run build",
"prepublishOnly": "bun run build && ggshield secret scan path . -r --use-gitignore -y && ggshield secret scan path dist -r -y",
"release": "release-it",
"release:dry": "release-it --dry-run --no-npm --no-github",
"release:prep": "./scripts/prep-release.sh"
"release": "node scripts/release.mjs release",
"release:dry": "node scripts/release.mjs release --dry-run",
"release:publish": "node scripts/release.mjs publish",
"release:prep": "bash scripts/prep-release.sh"
},
"keywords": [
"raindrop",
Expand Down
29 changes: 29 additions & 0 deletions scripts/build-info.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env node
// release-standard: scripts/build-info.mjs r1 (do not edit; see RELEASING.md)
// Usage: node scripts/build-info.mjs [output] (default: dist/build-info.json)
// Stamps the version, commit, commit date and dirty state into the build. Run it at the end of `build`.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

const output = process.argv[2] ?? 'dist/build-info.json';
const git = (...args) => {
try {
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
} catch {
return undefined;
}
};

const { version } = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const info = { version };
const commit = git('rev-parse', 'HEAD');
if (commit) {
info.commit = commit;
info.date = git('show', '-s', '--format=%cs', 'HEAD');
// Ignored files (the build output itself) do not count.
info.dirty = git('status', '--porcelain') !== '';
}

fs.mkdirSync(path.dirname(output), { recursive: true });
fs.writeFileSync(output, `${JSON.stringify(info, null, 2)}\n`);
31 changes: 14 additions & 17 deletions scripts/check-changelog.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
#!/bin/bash
# Validates that CHANGELOG.md has entries in the [Unreleased] section before releasing.
#!/usr/bin/env bash
# release-standard: scripts/check-changelog.sh r1 (do not edit; see RELEASING.md)
# Fail a release when CHANGELOG.md has no unreleased user-facing entries.
set -euo pipefail

set -e

# Extract content between [Unreleased] and next version header
content=$(awk '/^## \[Unreleased\]/{found=1;next} /^## \[/{exit} found{print}' CHANGELOG.md)
file="${1:-CHANGELOG.md}"
if [[ ! -f "$file" ]]; then
echo "Error: $file not found." >&2
exit 1
fi

# Check if there's any actual content (### headers like Added, Fixed, etc.)
if ! echo "$content" | grep -q '^### '; then
echo "Error: No entries in [Unreleased] section of CHANGELOG.md"
echo ""
echo "Add your changes before releasing:"
echo " ### Added"
echo " - New feature description"
echo ""
echo " ### Fixed"
echo " - Bug fix description"
content=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{exit} found{print}' "$file")
if ! awk '/^### /{section=1; next} section && /^[-*+] /{found=1} END{exit !found}' <<<"$content"; then
echo "Error: $file has no unreleased user-facing entries." >&2
echo 'Run the release prep script, then add a section under "## [Unreleased]", such as "### Added" followed by "- New feature".' >&2
exit 1
fi

echo "✓ Changelog has unreleased entries"
echo 'Changelog has unreleased entries.'
Loading
Loading