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

@devxeazy/cron-cli

v0.1.0

Published

One table for everything scheduled on a Linux box: user crontab, /etc/crontab, /etc/cron.d, run-parts and systemd timers — plus a preflight that says why a job will fail before it fires.

Downloads

191

Readme

cron-cli

Everything scheduled on this machine. One table.

cron-cli is a scheduled-task inventory for Linux. It reads the user crontab, /etc/crontab, /etc/cron.d, the cron.{hourly,daily,weekly,monthly} run-parts directories and systemd timers — system and user — and prints them as one systemctl list-timers-shaped table. It also tells you which of those entries fire and then do nothing.

S      LEFT     PASSED RESULT   NAME                           COMMAND
●  1min 53s   8min ago ok       sysstat-collect.timer          /usr/lib/sysstat/sa1 1 1
✖  1min 53s   3min ago exit 1   session-cleanup.timer          /usr/local/libexec/session-cleanup/prune-expired-session…
≡  6min 53s          - -        sysstat:6                      debian-sa1 1 1
●  8min 53s          - -        crontab:19                     cd / && run-parts --report /etc/cron.hourly
●  5h 21min     6h ago ok       apt-daily.timer                /usr/lib/apt/apt.systemd.daily update
≡  6h 50min          - -        sysstat:9                      debian-sa1 60 2
●  6h 51min    17h ago ok       dpkg-db-backup.timer           dpkg/dpkg-db-backup
●  6h 51min    17h ago ok       logrotate.timer                logrotate /etc/logrotate.conf
●  6h 58min    17h ago ok       sysstat-summary.timer          /usr/lib/sysstat/sa2 -A
≡  10h 1min          - -        e2scrub_all:2                  SERVICE_MODE=1 /sbin/e2scrub_all -A -r
≡ 12h 49min    11h ago ok       launchpadlib-cache-clean.timer find /home/user/.launchpadlib/api.launchpad.net/cache -ty…
● 13h 12min    10h ago ok       apt-daily-upgrade.timer        /usr/lib/apt/apt.systemd.daily install
● 13h 16min          - -        crontab:20                     cd / && run-parts --report /etc/cron.daily
≡ 13h 16min          - -        cron.daily/apt-compat          /etc/cron.daily/apt-compat
≡ 13h 16min          - -        cron.daily/dpkg                /etc/cron.daily/dpkg
≡ 13h 16min          - -        cron.daily/logrotate           /etc/cron.daily/logrotate
≡ 13h 16min          - -        cron.daily/man-db              /etc/cron.daily/man-db
≡ 13h 16min          - -        cron.daily/sysstat             /etc/cron.daily/sysstat
●  17h 7min    14h ago ok       man-db.timer                   install -d -o man -g man -m 0755 /var/cache/man ; mandb …
● 22h 39min     1h ago ok       update-notifier-download.timer /usr/lib/update-notifier/package-data-downloader
● 22h 49min     1h ago ok       systemd-tmpfiles-clean.timer   systemd-tmpfiles --clean
●     2d 2h          - -        monarx-update:1                apt-get update -qq && apt-get install -y -qq monarx-agen…
●     2d 6h          - -        dev:1                          backup.sh --to /srv/backups
●     3d 2h     8d ago ok       update-notifier-motd.timer     /usr/lib/ubuntu-release-upgrader/release-upgrade-motd
●    4d 10h     2d ago ok       e2scrub_all.timer              /sbin/e2scrub_all
≡    4d 10h          - -        e2scrub_all:1                  SERVICE_MODE=1 /usr/lib/x86_64-linux-gnu/e2fsprogs/e2scr…
●    4d 13h          - -        crontab:21                     cd / && run-parts --report /etc/cron.weekly
≡    4d 13h          - -        cron.weekly/man-db             /etc/cron.weekly/man-db
●     5d 6h     1d ago ok       fstrim.timer                   /sbin/fstrim --listed-in /etc/fstab:/proc/self/mountinfo…
●    8d 13h          - -        crontab:22                     cd / && run-parts --report /etc/cron.monthly

30 scheduled entries from 2 sources · 971.6ms
● will run  ✖ last run failed  ≡ fires, does nothing
! /var/spool/cron/crontabs is mode 1730 root:crontab; other users' crontabs cannot be listed or even counted. Not zero …

Real cron-cli list --width=120 output from a stock Ubuntu 24.04 box. Two private rows were replaced with generic equivalents (session-cleanup.timer, dev:1); every distro row is untouched.

crontab -l on that machine prints one line. cron-cli prints thirty. Twenty-nine of the things scheduled on that host are invisible to the command most people reach for.

And eleven of the thirty fire and do nothing. They are the ≡ rows. Ten of them are cron entries whose scripts open with a /run/systemd/system guard — /etc/cron.daily/logrotate runs every day, tests whether systemd is booted, and exits 0 without rotating anything, because logrotate.timer already did. The eleventh is a user timer whose service reports ConditionResult=no. Edit /etc/cron.daily/logrotate and nothing will happen, forever, and nothing on the machine will tell you why. cron-cli does, with the file and line number of the guard it found:

$ cron-cli --json | grep -B3 '/etc/cron.daily/logrotate:4'
      "effect": {
        "kind": "no-op",
        "by": "systemd-guard",
        "evidence_ref": "/etc/cron.daily/logrotate:4",

1. Why the table looks like that

| Column | What it means | | --- | --- | | S | ● will run · ≡ fires, does nothing · ✖ last run failed · ○ disabled · ? state unreadable | | NEXT / LEFT | Next firing, computed from the Vixie expression or taken from systemd. ~ marks a randomized delay. | | LAST / PASSED | Last firing. Populated for systemd timers. See §7. | | RESULT | ok, exit N, or a signal name. systemd timers only, for now. | | NAME | <file>:<line> for crontab entries, cron.daily/<script> for run-parts, the unit name for timers. |

Columns are dropped in a fixed order as the terminal narrows, so the table stays readable at 80 columns and uses the space it has at 200. --width=N pins it, --ascii replaces the glyphs, NO_COLOR is honoured.

There are never bare dashes for unknown values. Every empty cell carries a reason — not recorded, not visible, pre-boot, unreadable, no occurrence, BAD SCHEDULE — because "we do not know" and "it never ran" are different facts and a tool that renders both as - is lying to you.

2. What this is not

If you want a cron TUI with features, use cronboard. 1,454 stars, in homebrew/core as cronboard 1.1.1, last pushed 2026-09-19, Apache-2.0. It creates, edits and deletes jobs interactively, manages remote hosts over SSH, and it is better at all of that than cron-cli will be, because that is its product and not this one's. You would have found it in thirty seconds anyway; saying so first is the only honest move.

cron-cli is a different shape:

  • It unifies cron and systemd timers. cronboard manages crontabs. On a modern Debian or Ubuntu host the crontab is a minority of what is scheduled — 1 of 30 on the box above — and the interesting failure (a cron.daily script superseded by a timer) is only visible when you read both and cross-reference them. The only other project that tried this, aligoren/sta, has 3 stars and its last commit was in April 2026.
  • It has a preflight. cron-cli check says why a job will fail before it fires. See §5.
  • It installs as nothing. Zero runtime dependencies, Node builtins only, one node:-prefixed import graph that CI proves on every push. No daemon, no database, no network, no config file.
  • It is designed to be read by a program. --json is a stable, versioned envelope and the exit codes are a published contract. See §8.

What it deliberately does not do: create, edit or delete jobs inside a TUI; connect over SSH; alert. Those are cronboard's, and they are not on the roadmap.

3. Install

Requires Node.js >= 20.19 and Linux. No other runtime dependency — the files allowlist in package.json ships bin/, src/ and skills/, and nothing else.

npm install -g @devxeazy/cron-cli
cron-cli list

Or without installing anything:

npx @devxeazy/cron-cli list

Or from a release, with no npm at all. install.sh verifies SHA256SUMS before it writes a single byte, installs atomically into ~/.local, refuses to overwrite a file it did not create, and never edits a shell startup file:

curl -fLO https://github.com/devxeazy/cron-cli/releases/download/v0.1.0/install.sh
curl -fLO https://github.com/devxeazy/cron-cli/releases/download/v0.1.0/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
sh install.sh

install.sh --uninstall removes exactly what it installed and nothing else. install.sh --help lists --prefix, --force, --from-source and --version.

Or from a checkout, which is how the outputs in this README were produced:

git clone https://github.com/devxeazy/cron-cli.git
cd cron-cli
node bin/cron-cli.mjs list

On Ubuntu, ~/.local/bin is already added to PATH by the default ~/.profile — but only if the directory existed when the shell started. If cron-cli is not found after installing, open a new login shell before editing anything. install.sh detects this and tells you which case you are in; it will not append to .bashrc on your behalf.

4. The verbs

list — the inventory

cron-cli list                 # the table above
cron-cli list --fast          # next-run only; skips the batched systemctl show
cron-cli list --width=120     # pin the width
cron-cli list --json          # the full document (see docs/json-schema.md)

check — the preflight

Every entry, every finding, exit 2 if anything is broken. Real output from the same box:

$ cron-cli check

crontab:19  cd / && run-parts --report /etc/cron.hourly
  ✖ CRON013 output goes nowhere: no redirection, MAILTO unset and no MTA installed

dev:1  /usr/local/bin/backup.sh --to /srv/backups
  ✖ CRON013 output goes nowhere: no redirection, MAILTO unset and no MTA installed
  ▲ CRON025 */N in the day-of-month field is not "every N days": cron expands it to a fixed day set that restarts each month, so it fires on consecutive days at ~7 month boundaries a year (6 in a leap year).

! /var/spool/cron/crontabs is mode 1730 root:crontab; other users' crontabs cannot be listed or even counted. Not zero -- unknown. Re-run with sudo to include them.

! no MTA on this host (/usr/sbin/sendmail absent), so cron generates every job's output and then discards it. Failures are silent at any privilege level.

explain — what a schedule really means

The humanizer describes field sets and never infers a period across a field boundary, which is how the usual implementation turns @weekly into "daily at 00:00 on Sun":

$ cron-cli explain "@weekly"
@weekly   at 00:00 on Sun
  next  2026-09-27T00:00:00.000Z
  next  2026-10-04T00:00:00.000Z
  next  2026-10-11T00:00:00.000Z
  next  2026-10-18T00:00:00.000Z
  next  2026-10-25T00:00:00.000Z

It refuses to phrase a lie. */10 in the day-of-month field is not "every ten days", so it is not described as one — it is enumerated, and the collapse at the month boundary is called out:

$ cron-cli explain "0 0 */10 * *"
0 0 */10 * *   at 00:00 on day 1, 11, 21, 31 of the month
  next  2026-10-01T00:00:00.000Z
  next  2026-10-11T00:00:00.000Z
  next  2026-10-21T00:00:00.000Z
  next  2026-10-31T00:00:00.000Z
  next  2026-11-01T00:00:00.000Z
  ▲ CRON025 */N in the day-of-month field is not "every N days": cron expands it to a fixed set of days that restarts each month, so it fires on consecutive days at month boundaries.

And the single most misunderstood rule in cron gets said out loud, with the man page:

$ cron-cli explain "0 0 13 * 5"
0 0 13 * 5   at 00:00 on day 13 of the month OR on Fri (whichever matches first)
  next  2026-09-25T00:00:00.000Z
  ...
  ▲ CRON015 day-of-month AND day-of-week are both restricted, so cron runs when EITHER matches (Vixie OR), not both. man 5 crontab: "if both fields are restricted, the command will be run when either field matches the current time".

validate — syntax plus the semantic lint

Syntax comes from crontab -n - itself, through stdin. The semantic layer is ours:

$ cron-cli validate '0 3 * * * date +%F > /tmp/stamp'
syntax  ok
  ▲ CRON007 unescaped % truncates the command and sends the rest to stdin. Write \% or put the command in a script. Suggested: date +\%F > /tmp/stamp

env and run <id> --dry-run — the runtime cron will actually give a job

$ cron-cli env
SHELL      /bin/sh
PATH       /usr/bin:/bin
HOME       /home/user
LOGNAME    dev
USER       dev
cwd        /home/user
timezone   UTC
note       cron sources no shell startup file. Your ~/.bashrc, ~/.profile and any PATH you export there do not exist here.

run <id> --dry-run prints the exact argv and environment for one managed job, as JSON with --json. It does not execute anything in v0.1; see the roadmap.

add, remove, enable, disable, undo — the write path

Everything that changes a crontab goes through one funnel, into one managed block, and text outside that block is copied byte for byte. --dry-run prints the unified diff and exits 7 without installing:

$ cron-cli add --file ./demo.crontab --schedule "*/10 * * * *" \
    --command "/usr/local/bin/reindex.sh" --name reindex --dry-run
--- ./demo.crontab
+++ ./demo.crontab (proposed)
  # my own comment, never touched
  0 0 */3 * * /usr/local/bin/backup.sh
+ 
+ # >>> cron-cli:begin (managed by cron-cli; do not hand-edit inside) <<<
+ # cron-cli:job id=cc-3c4bd847f4 name=reindex owner=dev created=2026-09-22T17:04:12.196Z hash=sha256:978a1ae082e2
+ */10 * * * * /usr/bin/flock -n -E 0 /home/user/.local/state/cron-cli/locks/reindex.lock /usr/local/bin/reindex.sh >> /home/user/.local/state/cron-cli/logs/reindex.log 2>&1
+ # >>> cron-cli:end <<<

(--dry-run: nothing was installed)

The line it writes is not the line you gave it, and that is the point: an absolute path, a flock -n -E 0 wrapper because the interval is under fifteen minutes, and a destination for the output. Three of the most common cron failures, closed by construction.

Every real write re-reads the crontab inside a lock, refuses on an etag mismatch (--if-match), takes a timestamped backup first, and verifies the read-back afterwards. cron-cli undo restores the backup; undo --list shows them.

It also refuses work. --force does not override a block:

$ cron-cli add --schedule "@daily" --command 'rm -rf $BUILD_DIR'
⛔ SAFE001 target `$BUILD_DIR` is a bare variable — if it is unset, /bin/sh expands it to nothing and `rm -r` runs against the working directory, which under cron is $HOME
cron-cli: refusing to schedule this command (--force does not override a block)

--file <path> and CRON_CLI_CRONTAB_FILE point every read and write at an ordinary file instead of the real crontab. Use them in tests, in CI, and in anything an agent drives.

ui — the same table, interactively

cron-cli ui

Arrow keys and PgUp/PgDn to move, enter for a job's detail, / to filter, r to rescan, ? for help, q to quit. It is a reader. Creating, editing and deleting jobs are not bound to any key, and non-user-crontab sources say so when you try (cron.d is read-only in v0.1 — only the user crontab is writable). That is deliberate: in the tool this replaced, unhandled keys in one view fell through to handlers that rewrote the crontab.

With no arguments and a TTY on both ends, cron-cli starts here; piped or with --json, it runs list instead.

5. The preflight

This is the reason the project exists.

$ claude --version
2.1.278 (Claude Code)

$ env -i PATH=/usr/bin:/bin HOME=/home/user /bin/sh -c 'claude --version'
/bin/sh: 1: claude: not found

Exit 0 in your shell. Exit 127 under cron. Nothing on the machine will tell you, because this host has no MTA, so cron generates the "command not found" message and discards it. This is the number one cause of "it works when I run it myself", and cron-cli catches it before you install the job:

$ cron-cli validate "0 3 * * * claude --version"
syntax  ok
  ▲ CRON013 no output redirection: cron mails the output, and with no MTA it is discarded
  ▲ CRON001 `claude` not on cron's PATH (it is at /home/user/.local/bin/claude, which is not on cron's PATH=/usr/bin:/bin)

The checks, all of them bought with a reproduction against a real cron daemon:

| Code | What it catches | | --- | --- | | CRON001 | First token does not resolve on cron's PATH. Tells you where the binary actually is. | | CRON007 | Relative command path — cron's cwd is $HOME, not where you tested it. Also unescaped %. | | CRON010 | [[ ]], set -o pipefail, <(...), <<< under SHELL=/bin/sh, which is dash. Exit 127 or 2. | | CRON011 | &>, {1..3}, $RANDOM, echo -e under dash. These exit 0 and do the wrong thing silently. | | CRON012 | Unescaped %: cron truncates the command there and sends the rest to stdin, pre-shell. Quoting does not help. | | CRON013 | Output goes nowhere: no redirection, MAILTO unset, no MTA. Not a failure — the reason every other failure is invisible. | | CRON015 | Day-of-month and day-of-week both restricted: Vixie runs on either, not both. | | CRON025 | */N in day-of-month is not a period; the set restarts each month. |

cron-cli check resolves cron's real PATH rather than assuming one. On a host where cron.service runs /usr/sbin/cron -f -P, -P means the daemon does not set PATH and children inherit systemd's manager environment — so the effective PATH is /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin, not the compiled-in /usr/bin:/bin. A checker that hardcodes the classic value invents a false positive for every binary in /usr/sbin. check reports the resolved value and how it derived it. See docs/what-cron-cli-cannot-see.md.

6. Exit codes

They are a published contract. Codes may be added; they are never renumbered.

| Code | Name | Meaning | | --- | --- | --- | | 0 | ok | Succeeded, nothing to report. | | 1 | warnings | Succeeded, findings or skipped sources to report. | | 2 | usage | Bad invocation — unknown verb, unknown flag, missing argument. | | 3 | notFound | No managed job with that id or name; no backup to restore. | | 4 | conflict | --if-match mismatch, or the crontab moved mid-write. Nothing was installed. | | 5 | invalid | Unparseable schedule, or crontab -n rejected the content. | | 6 | unsupported | No crontab(1), or the crontab could not be read at all. | | 7 | wouldChange | --dry-run and the write would have changed something. | | 8 | locked | Another cron-cli write is in progress. | | 9 | refused | A safety tier blocked it. --force does not override a block. |

cron-cli check is the one verb with an overload: in human mode it exits 2 when any finding has severity error, which collides with usage. In --json mode the same state exits 1. Branch on the JSON, not on check's exit code — the full table and the caveat are in docs/exit-codes.md.

7. What cron-cli cannot see

An inventory that overstates its own coverage is worse than no inventory. The full accounting, with the evidence, is in docs. The two that matter:

cron records invocation and never outcome. LAST and PASSED populate for systemd timers, because systemd keeps LastTriggerUSec and Result. They do not populate for cron jobs, because cron does not store the exit status of a job anywhere — not in the spool, not in a state file, not in syslog. The syslog line says the command started. This was checked at every privilege level, including with sudo: 491 CRON lines in the log for this host, zero exit codes among them. So cron-cli prints not recorded rather than inventing a green tick. cron-cli wrap, in v0.2, is the only way this can change, because the only place an exit status exists is inside the job's own process.

Other users' crontabs are unreadable, and that is not the same as absent.

$ stat -c '%A %U:%G %a' /var/spool/cron/crontabs
drwx-wx--T root:crontab 1730

$ ls /var/spool/cron/crontabs
ls: cannot open directory '/var/spool/cron/crontabs': Permission denied

Mode 1730 means an unprivileged user cannot list the directory, so cron-cli cannot even count the crontabs it is missing. It says so, in a warning on every run, instead of quietly reporting a total that looks complete:

/var/spool/cron/crontabs is mode 1730 root:crontab; other users' crontabs cannot be listed or even counted. Not zero -- unknown. Re-run with sudo to include them.

Also not yet covered: anacron and at (v0.3), launchd/macOS (v1.0), Kubernetes CronJobs and application-level schedulers such as Celery, Sidekiq or BullMQ (never — different problem).

8. For AI agents

cron-cli was built to be driven by a program as much as by a person.

  • --json on every read verb emits one versioned object on stdout and nothing else: schema_version, generated_at, host, scan (depth, plan, per-enricher timings, which sources were skipped and why), jobs[], notes[], and an explicit sort block. Sorting is by UTF-16 code unit, never localeCompare, so two runs in two locales diff clean. Documented field by field in docs/json-schema.md.
  • Unknowns are typed. Every next_run, last_run and last_result carries a machine-readable reason and an evidence string. There is no branch in the codebase that can produce an unexplained null.
  • Exit codes are a contract — docs/exit-codes.md.
  • Writes are reversible and refusable. --dry-run returns the diff, --if-match gives you compare-and-swap, every write leaves a backup, and the confirm safety tier exists precisely so an agent must stop and ask a human rather than pass --yes itself.
  • An agent skill ships in the repo under skills/cron-cli/ and in the npm tarball. It is the same surface, written for a tool-using model.

Always point agent-driven writes at --file <path> or CRON_CLI_CRONTAB_FILE unless a human has explicitly asked for the real crontab.

Por que

O crontab -l desta máquina mostra 1 entrada; o cron-cli mostra 30, e avisa que 11 delas disparam e não fazem nada porque um timer do systemd já fez o trabalho. A pergunta "o que está agendado aqui?" não tem uma resposta única no Linux — ela está espalhada entre o crontab do usuário, /etc/cron.d, os diretórios run-parts e os timers do systemd — e é exatamente essa resposta, mais o motivo pelo qual um job vai falhar antes de ele falhar, que esta ferramenta entrega. Documentação em inglês; o resto do projeto também. Este parágrafo é o resumo, não um espelho traduzido.

Help and contribute

Run cron-cli list on a box you know well and tell us which row is wrong — that is the most useful bug report this project can get. Support says where to ask. See Contributing, the roadmap, and the Code of Conduct. Report vulnerabilities privately through the security policy.

Docs: exit codes · JSON schema · writing an adapter · what cron-cli cannot see

License

MIT.