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
7 changes: 3 additions & 4 deletions .release-it.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/release-it@20/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,11 +14,10 @@
"releaseName": "v${version}"
},
"npm": {
"publish": true,
"publishArgs": ["--access", "public"]
"publish": false
},
"hooks": {
"before:init": ["bun run verify"],
"before:init": ["bash scripts/check-changelog.sh", "bun run verify"],
"after:bump": ["bun run test:package"]
},
"plugins": {
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `tt update` (with `--check` and `--force`) updates the CLI the way it was installed: a clone is pulled
with `git pull --ff-only`, then `bun install` and `bun run build`; a global npm, pnpm or bun install is
reinstalled from the registry.
- `tt version [--json]` reports the version, full commit, commit date, dirty state, install method and
install path.
- Install documentation now covers running from source and updating.

### Changed

- `tt --version` / `tt -V` now prints `tt X.Y.Z (sha7 YYYY-MM-DD)`, with `-dirty` for builds made from a
working tree with uncommitted changes, instead of the bare version number. The same line is printed by
a registry install and by a local build.

## [0.1.3] - 2026-07-15

### Added
Expand Down
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,44 @@

A command-line interface for the [TickTick](https://ticktick.com) task management app, optimized for AI agents.

## Installation
## Install

Requires Node.js 22.12+ (commander v15 is ESM-only and requires Node ≥22.12 for `require(esm)`):
Requires Node.js `^22.21.0 || >=24.0.0` (commander v15 is ESM-only and needs Node ≥22.12 for `require(esm)`):

```bash
npm install -g @crcatala/ticktick-cli
npm install -g @crcatala/ticktick-cli # or: pnpm add -g @crcatala/ticktick-cli, bun add -g @crcatala/ticktick-cli
npx @crcatala/ticktick-cli --help # run without installing
```

The package provides two equivalent commands: `tt` and `ttcli`. `tt` is used throughout this guide.

<details>
<summary><strong>Build from Source</strong></summary>
### From source

Requires Node.js 22.12+ and [Bun](https://bun.sh):
Requires Node.js `^22.21.0 || >=24.0.0` and [Bun](https://bun.sh) for the build:

```bash
git clone https://github.com/crcatala/ticktick-cli.git
cd ticktick-cli
bun install
bun run build
node dist/index.js --help
npm link # puts `tt` and `ttcli` on your PATH, pointing at this clone
tt --version # tt X.Y.Z (sha7 YYYY-MM-DD): the commit you built
```

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

### Updating

```bash
tt update --check # exit 0: up to date, 1: update available, 2: could not tell
tt update # updates the same way it was installed
```

`tt update` follows the install method: a clone is pulled with `git pull --ff-only`, then
`bun install` and `bun run build`; a global npm, pnpm or bun install is reinstalled from the
registry. `tt version` shows which commit, install method and path are in use.

## Quick Start

Expand Down Expand Up @@ -199,6 +213,8 @@ fi
| `tt user stats` | Show usage statistics |
| `tt sync` | Fetch full state snapshot |
| `tt trash empty` | Permanently delete all trashed items |
| `tt update` | Update `tt` the way it was installed |
| `tt version` | Show version, commit, install method and install path |

Task references accept exact IDs, unambiguous ID prefixes, and case-insensitive exact titles. Project references also accept unambiguous case-insensitive name prefixes. If multiple items match, use a full ID or add `--project PROJECT` to task lookup commands that support it.

Expand Down
105 changes: 61 additions & 44 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,108 @@
# Releasing

This project uses [release-it](https://github.com/release-it/release-it) for manual releases from a maintainer machine. A release builds and validates the npm package, updates the changelog and version, publishes to npm, pushes a signed-off release commit and tag, and creates a GitHub Release.
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 `@crcatala/ticktick-cli` to npm from the
tagged commit.

## Prerequisites
The package is public and installs the `tt` and `ttcli` commands.

- Push access to `crcatala/ticktick-cli`
- An npm account with publish access to the `@crcatala` scope (`npm whoami`)
- A GitHub token available as `GITHUB_TOKEN` with repository **Contents: read and write** permission, so release-it can create the GitHub Release
- Node.js 22.21+ (release-it v21 requirement) or 24+, and Bun
- A clean checkout on `main`
## Prerequisites

The package is public and is published as `@crcatala/ticktick-cli`. It installs the `tt` and `ttcli` commands.
- Push access to `main`, and a GitHub token for the release (`GITHUB_TOKEN`
with repository **Contents: read and write**, or release-it opens the GitHub
release form in the browser instead).
- An npm login with publish rights to the `@crcatala` scope: `npm whoami`,
else `npm login`.
- Node.js `^22.21.0 || >=24.0.0` (release-it v21 requirement) and Bun.
- A clean checkout on `main`.

## Before releasing

1. Pull the current main branch:
1. Start from an up-to-date `main`:

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

2. Prepare and update the changelog. The helper lists changes since the last tag:
2. Draft the changelog:

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

Add user-facing entries beneath `## [Unreleased]` in `CHANGELOG.md`, using [Keep a Changelog](https://keepachangelog.com/) sections such as Added, Changed, Fixed, Removed, or Security.
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.

3. Run the complete local verification suite:
3. Run the full local verification suite, which is also what the release
preflight runs:

```bash
bun run verify
```

This runs tests, linting, type checking, the JavaScript build, and an install smoke test of the exact `npm pack` tarball.
This runs the tests, linting, type checking, the JavaScript build (stamped
with the commit by `scripts/build-info.mjs`), and an install smoke test of
the exact `npm pack` tarball (`tt` and `ttcli` must answer `--help` and print
the standard `--version` line).

## Release

Preview the local release steps first:

```bash
bun run release:dry
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
```

`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.

Then release interactively:
`release:dry` runs the same preflight as `release`, so it also needs an npm
login (`npm whoami`). To preview the release-it steps without one, run
release-it directly:

```bash
export GITHUB_TOKEN=github_pat_... # if not already configured
bun run release
npx release-it --dry-run --ci --no-npm --no-github
```

Release-it prompts for the version bump and then:
In order, `bun run release`:

1. validates the clean `main` checkout and runs `bun run verify`;
2. bumps `package.json` and moves the changelog's Unreleased entries into the new version;
3. rebuilds `dist/`, packs it, and smoke-tests the installed `tt` and `ttcli` commands;
4. commits and tags `vX.Y.Z`;
5. rebuilds immediately before publishing `@crcatala/ticktick-cli` publicly to npm;
6. pushes the commit and tag; and
7. creates a GitHub Release using the changelog notes.
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).

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

```bash
# Skip npm publishing if it already succeeded in a partial release
bun run release -- --no-npm
bun run release -- --preRelease=beta # 1.3.0-beta.0, published as @crcatala/ticktick-cli@beta
bun run release -- --preRelease=beta --npm.tag=next # same version, under "next"
```

# Skip GitHub Release creation
bun run release -- --no-github
## When a step fails

# Release a specific version without the bump prompt
bun run release -- 0.1.1
- **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).

# Publish a prerelease
bun run release -- --preRelease=alpha
```
Published npm versions are immutable. If a release has a defect, publish a
corrective version rather than replacing the existing one.

## Verify the release
## Verify

```bash
npm view @crcatala/ticktick-cli
npx @crcatala/ticktick-cli@latest --version
npm view @crcatala/ticktick-cli version
npx @crcatala/ticktick-cli@latest --version # tt X.Y.Z (sha7 YYYY-MM-DD)
npx @crcatala/ticktick-cli@latest --help
```

Published npm versions are immutable. If a release has a defect, publish a corrective version rather than replacing the existing one.
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@
"prepublishOnly": "bun run verify",
"dev": "bun run src/index.ts",
"clean": "node -e \"for (const path of ['dist', 'tsconfig.tsbuildinfo']) require('node:fs').rmSync(path, { recursive: true, force: true })\"",
"build": "bun run clean && tsc --build",
"test": "bun --raw-test",
"test:watch": "bun --raw-test --watch",
"test:coverage": "bun --raw-test --coverage",
"build": "bun run clean && tsc --build && node scripts/build-info.mjs",
"test": "bun test --isolate",
"test:watch": "bun test --isolate --watch",
"test:coverage": "bun test --isolate --coverage",
"test:live": "RUN_LIVE_TESTS=1 bun test --preload ./scripts/live-test-preflight.ts tests/integration/live-api.test.ts tests/integration/reference-resolution.live.test.ts --timeout 60000",
"typecheck": "tsc --noEmit",
"lint": "oxlint src/",
"schema:capture": "bun run scripts/schema-capture.ts",
"test:package": "bash scripts/test-package.sh",
"verify": "bun run test && bun run lint && bun run typecheck && bun run build && bun run test:package",
"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": [
"ticktick",
Expand All @@ -53,6 +54,7 @@
},
"files": [
"dist/**/*.js",
"dist/build-info.json",
"README.md",
"LICENSE"
],
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`);
19 changes: 19 additions & 0 deletions scripts/check-changelog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/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

file="${1:-CHANGELOG.md}"
if [[ ! -f "$file" ]]; then
echo "Error: $file not found." >&2
exit 1
fi

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.'
Loading