npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@apytel/youtrack-cli

v0.6.0

Published

Thin YouTrack CLI for humans and AI agents — one dependency, compact output

Readme

youtrack-cli

A thin YouTrack CLI built for AI agents and humans at the same time.

Every command below runs against a fake YouTrack in CI with no secrets, and the browser OAuth flow and every write path have been exercised against a live instance.

Getting started

npm install -g @apytel/youtrack-cli
yt login                    # browser OAuth; the credential goes to the OS keychain
yt ls

For CI, containers and headless Linux, skip yt login entirely:

export YT_URL=https://example.youtrack.cloud
export YT_TOKEN=perm:…

yt login --status says which of the two is in use. yt login --token stores a permanent token in the keychain instead of running the browser flow.

The browser flow needs Allow automatic OAuth client registration via CIMD enabled on the instance — Access Management > OAuth Clients. It is off by default. Without it yt login explains the switch and falls back to a token.

What it can do

--json on any command returns YouTrack's raw response, unmodified. yt help prints the same list; exit codes are 0 ok, 1 auth, 2 not found, 3 command rejected, 4 usage.

Authentication

yt login [--url URL]              browser OAuth; the credential goes to the OS keychain
yt login --token                  store a permanent token instead, for CI and headless hosts
yt login --status                 which instance, and which of the two credentials is in use
yt logout                         clear the stored credential

Issues

yt ls [query] [--fields F] [--top N] [--all]   YouTrack query syntax; state included
yt view <id> [--comments]
yt new <project> <summary> [-d text | -f file]     -d and -f omitted: read stdin
yt edit <id> [-s summary] [-d text | -f file]      summary and/or description
yt comment <id> <text>
yt attach <id> <file...>
yt attach --download <id> [name]    save them back into the current directory

yt attach --download closes the loop the upload half leaves open: an agent in a later session can read what an earlier one attached. Without a name it saves every attachment of the issue and prints the file names; with one it saves that attachment alone. An issue with nothing attached, or a name that is not there, exits 2 rather than writing nothing and claiming success.

-d, -f and stdin are three spellings of the same argument, so a multi-line body never has to survive shell quoting:

yt new DEMO "Rework the export" -f draft.md
git log -1 --format=%B | yt edit DEMO-42

yt ls shows 50 issues. When more matched, it says so on stderr — stdout stays exactly the rows, so yt ls | wc -l and yt ls --json | jq are unaffected by the warning, and nothing is silently cut without a word. --top N widens the window; --all pages through every match and prints no warning at all (with --json, one flat array). --all and --top together are a usage error.

Every other list — yt view --comments, yt art ls, yt board ls, yt project team — is exhaustive without asking, and so is every name lookup behind the scenes. A truncated list a decision is made from is not a short answer, it is a wrong one.

Every other mutation

Mutations go through YouTrack's own command language, so custom fields, tags, links, priorities and milestones work without this tool knowing they exist:

yt cmd <id...> "<command>" [--as user] [--dry-run]
yt cmd DEMO-42 "state In Progress assignee me Fix versions 2026.1"
yt cmd DEMO-1 DEMO-2 "tag urgent"        several issues at once
yt cmd DEMO-42 "subtask of DEMO-7"       hierarchy, links, anything the field accepts
yt cmd DEMO-42 "state Fixed" --as agent-bot     attribute the work to another user
yt cmd DEMO-42 "state Frozen" --dry-run  validate syntax *and* values, apply nothing

yt cmd --help carries the worked examples.

Knowledge base

yt art ls [--project P] [--grep text]   an indented tree, ordered as YouTrack orders it
yt art view <id>
yt art new <project> <title> [-f file | -c text] [--parent <id>]
yt art edit <id> [-s title] [-f file | -c text]

--project is served by the per-project endpoint, so the result is complete rather than whatever fits in one page of a global list. --grep matches article titles case-insensitively and filters client-side — the REST API exposes no server-side article search.

Project configuration

yt state ls <project> [--all]           the State value set; --all shows archived too
yt state add <project> <name> [--resolved] [--archived] [--after "Other"]
yt state edit <project> <name> [--rename X] [--resolved|--no-resolved]
                                        [--archived|--no-archived]
yt state order <project> "Open,In Progress,Fixed"
yt type ls|add|edit|order <project> ...          the same, for issue types
yt field ls <project> <field> [--all]   the value set of any field, named as an argument
yt field add <project> <field> <name> [--description X] [--after "Other"]
yt field edit <project> <field> <name> [--rename X] [--description X]
yt field order <project> <field> "First,Second,Third"
                                        a version field also takes --release-date
                                        2026-09-01 and --released|--no-released
yt board ls
yt board new <project> <name> [--columns "A,B,C"]
yt board add <project...> <board>       put projects on a board that already exists
yt board rm <project...> <board>        take them off again
yt sprint ls <board> [--all]            the board's sprints; --all lists the closed ones too
yt sprint new <board> <name>            open a sprint
yt sprint close <board>                 archive the current one
yt sprint current <board>               the name of the current sprint, and nothing else

The current sprint is the last open one, not the one today's date falls into: a sprint is closed by hand whenever the work is done, so its dates are decoration and a schedule-based answer would be a guess. yt sprint current prints the name alone, so it feeds straight back in — yt cmd DEMO-42 "add Board Delivery $(yt sprint current Delivery)" — and exits 2 when the last sprint has been closed and no new one opened.

A sprint belongs to its board, not to a project. A board covering several projects has one sprint across all of them; two boards over the same project have two unrelated sprint lists, even where the names match. The one exception is a board configured to take its sprints from a sync field — there the sprints mirror that field's values and every board on the same field shows them, so yt sprint new names those boards instead of leaving the reach unsaid. Stock boards have no sync field and it says nothing. A name already on the board is refused rather than opened twice, and a board with sprints switched off answers the API with one permanent active sprint — yt refuses it with exit 3 instead of passing that fiction on.

yt board add|rm names the board, never an id — yt board ls prints no id to copy. Board names are not unique in YouTrack, so a name matching several boards lists them and changes nothing. Both are idempotent, and rm refuses to leave a board with no projects at all. Board columns reference state values by name, so adding a project whose states do not cover every column still succeeds, and the columns that stay empty for it are named on stderr.

state, type and field are one command under three names: the first two fix the field they work on, field takes it as an argument and so reaches any field backed by a value set — Subsystem, Fix versions, Fixed in build, or a State this instance calls something else. Values can be added, renamed, reordered, archived, described, and — for states — marked as resolving the issue.

yt field add DEMO Subsystem "CLI" --description "the command line client"
yt field order DEMO Subsystem "CLI,Docs"
yt field add DEMO "Fix versions" 1.0.0 --release-date 2026-09-01
yt field edit DEMO "Fix versions" 1.0.0 --released

--description is taken by every field, because it lives on the value type they all share. --released and --release-date belong to version values alone and are refused elsewhere, the way --resolved is refused outside State. There is no --field flag: yt field ls DEMO Status is how an instance that renamed its State field is reached, and passing the old flag says so.

Deleting is deliberately absent: archiving hides a value without touching the issues that already carry it.

A value set can belong to several projects at once — the stock Type field usually does — so yt type add names the other projects a new value just appeared in.

Setting an instance up

yt project new <shortName> <name> [-d description] [--leader login] [--org name]
yt project team <shortName>             the project team, login and full name
yt project assign <shortName> <login...>   add users to the project team
yt org new <name> [-d description]      an organization to hold projects
yt user new <login> [--name "Full Name"] [--email [email protected]]

--leader resolves a login to a database id; without it the leader is the authenticated user. --org is optional — a project without an organization is complete and usable — and a project created this way comes up with the full default field set, so yt state ls and yt board new work on it immediately.

yt project team lists everyone who reaches the project, whether directly or through a group; yt project assign adds direct members. YouTrack replaces the member list wholesale on write, so the current members are read and posted back with the new ones — assigning someone twice changes nothing.

yt user new generates the password and writes it to stderr, once. It is never accepted as a flag: argv is visible in ps and lands in shell history.

Design

  • CONTEXT.md — domain glossary
  • ADR-0001 — why mutations use the command language instead of typed subcommands
  • ADR-0002 — why there is no fallback file for the token
  • ADR-0003 — why the CLI used to register its own OAuth client (superseded)
  • ADR-0004 — why the client id is a URL we host, and how that removed the token bootstrap
  • ADR-0005 — why yt state manages a value set and not the transitions between them
  • ADR-0006 — why yt user new generates the password and prints it on stderr

License

MIT