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

@jinshuju/cli

v0.7.0

Published

Command line interface for Jinshuju Open API v1

Readme

@jinshuju/cli

npm

Command-line interface for the Jinshuju Open API v1.

Install

npm install -g @jinshuju/cli

Two equivalent entry points are installed:

jinshuju --help
jsj --help

Working on the CLI itself

npm install
npm link          # jinshuju / jsj now resolve to your checkout

If npm link leaves you with permission denied, dist/cli-bin.js has lost its execute bit — npm run build restores it.

Authentication

Two kinds of credential are supported: an interactive browser login (OAuth), and an access token (personal or account). Both go in through auth login:

jinshuju auth login                          # browser login
jinshuju auth login --access-token xxx       # access token, checked before it is stored

Either one is written to the config file (~/.jinshuju/config.json, mode 600) and replaces whatever credential was stored before. jinshuju auth logout forgets it (revoking it first, for a browser session).

For CI and scripts, an environment variable works without touching the file:

export JINSHUJU_ACCESS_TOKEN=xxx

The precedence is JINSHUJU_ACCESS_TOKEN, then the stored credential. A token set for this run always beats a stored login: acting as last week's login instead is a surprise nobody wants. jinshuju auth status reports which credential is in use and where it came from:

$ jinshuju auth status
Authenticated with an access token (from env).

config set holds only host, auth_host and client_id; credentials are not config.

Creating forms

Field types use the API v1 type names. Do not supply api_code when creating a field — the server assigns it.

jinshuju form create --json @form.json --scene registry --layout card --folder Fd2xK8
jinshuju form create --json @exam.json --type exam        # the payload may carry exam_setting
jinshuju form edit Kp7mQ2 --json '{"exam_setting":{"limited_time":45}}'

jinshuju form create --json '{
  "name": "Event signup",
  "fields": [
    { "type": "TextField", "label": "Name", "required": true },
    { "type": "MobileField", "label": "Mobile", "required": true }
  ]
}'

--type exam and --type evaluation select both the scene and the settings block that belongs to it. That settings block has an endpoint of its own, so a payload carrying it becomes two requests — and the general form update does not recognise the key at all (it reads only name, description, setting, fields and field_rules), so without the dispatch exam_setting would be dropped in silence.

The settings block is always sent first. It is the half that can be rejected — "this is not an exam form" — and sending it first is what guarantees a rejection leaves every other change unsent.

When the form takes entries

setting travels as is, on create and on edit. Its close rules decide when the form is open; they coexist, and form get reads them back in the shape they were written. On edit a rule replaces what was there (time_periods is the whole list) and {"enabled": false} removes it.

# between two instants; either end may be left out
jinshuju form edit Kp7mQ2 --json '{"setting":{"by_time_range_close_rule":{"start_time":"2026-10-01T09:00:00+08:00","end_time":"2026-10-07T18:00:00+08:00"}}}'
# 11:00-12:40 and 17:00-18:30, every day
jinshuju form edit Kp7mQ2 --json '{"setting":{"daily_repeat_close_rule":{"time_periods":[
  {"start_time":{"hour":11,"minute":0},"end_time":{"hour":12,"minute":40}},
  {"start_time":{"hour":17,"minute":0},"end_time":{"hour":18,"minute":30}}]}}}'
jinshuju form edit Kp7mQ2 --json '{"setting":{"daily_repeat_close_rule":{"enabled":false}}}'
jinshuju form edit Kp7mQ2 --json '{"setting":{"by_entries_close_rule":{"limit":100}}}'   # closes after 100 entries
jinshuju form edit Kp7mQ2 --json '{"setting":{"manually_close_rule":{"closed":true}}}'   # stops it now

show_form_before_open and show_close_count_down only hold with a time-range or daily rule; without one the save resets them to false.

Reading data

entry, view, field and comment are all top-level resources; the container is given by --form or --table (mutually exclusive, both mapping to the API's form_token).

jinshuju entry list --form Kp7mQ2
jinshuju entry list --table Vn4xR8
jinshuju entry get 1 --form Kp7mQ2
jinshuju entry list --form Kp7mQ2 --view aB3dE9

Filtering, sorting and paging:

jinshuju entry list --form Kp7mQ2 --filter 'field_3 gte 80'
jinshuju entry list --form Kp7mQ2 --filter 'created_at within_last 30d' --filter 'field_9 not_null'
jinshuju entry list --form Kp7mQ2 --sort created_at:desc
jinshuju entry list --form Kp7mQ2 --limit 10
jinshuju entry list --form Kp7mQ2 --all

--limit can only ask for less: a listing's default page size is also its maximum (50 in most cases), and anything larger is capped.

--all reads at most 200 pages. Past that it stops with an error naming the cursor; pass it to --next (with --all again) to read on.

--filter is repeatable and the conditions are AND-combined. For conditions it cannot express, use --filters <json|@file>. Cursors are opaque strings — pass the next value from the previous response back verbatim.

Analysis

There is no need to pull rows out and count them yourself. Counts, aggregates and profiles are computed server-side, and the size of the response depends only on how many metrics you asked for and how many groups came back.

jinshuju entry count --form Kp7mQ2 --filter 'field_3 gte 80'
jinshuju entry count --form Kp7mQ2 --form Vn4xR8            # repeatable, up to 10 containers

jinshuju entry aggregate --form Kp7mQ2 --metric avg:field_3
jinshuju entry aggregate --form Kp7mQ2 --metric count:field_1 --by created_at:month --limit 12

jinshuju entry summary --form Kp7mQ2
jinshuju entry summary --form Kp7mQ2 --fields field_3,field_7 --no-overview

--metric <func>:<field> is repeatable, 1–20 of them; --by <field>[:day|week|month] takes at most 2, and a date dimension must name a bucket. Which functions a field accepts is the field's own answer — read analytics.agg_funcs from form get.

A multi-container count takes no --keyword, and its --filter is limited to created_at, updated_at and creator_id: the same api_code names a different field in every container, so comparing across them would mean nothing.

Creating entries

Payload keys are field api_codes, not field labels. --json accepts inline JSON, @file, and - for stdin.

jinshuju entry create --form Kp7mQ2 --json '{
  "field_1": "Alice",
  "field_2": "13800138000"
}'

jinshuju entry create --form Kp7mQ2 --json @entry.json
cat entry.json | jinshuju entry create --form Kp7mQ2 --json -

Creating, updating or getting one entry answers with a url: where that entry opens in the Jinshuju data page, for whoever wrote it to check it there. A --batch write answers without one.

Writing

Apart from the settings-block dispatch described above, every write command issues exactly one request, and payload keys are field api_codes.

jinshuju folder create Ledgers --kind table       # folders are form or table; a table cannot go in a form folder
jinshuju form edit Kp7mQ2 --json '{"name":"2026 signups"}'
jinshuju form copy Kp7mQ2 --name Copy
jinshuju form move Kp7mQ2 --folder Fd2xK8         # without --folder, moves it out of its folder
jinshuju form theme set Kp7mQ2 --primary-color "#1F6FEB"
jinshuju table create --json @table.json --folder Nf7mDC
jinshuju table edit Vn4xR8 --json '{"name":"2026 ledger"}'

Adding, changing and removing fields all land on a single PATCH of the container:

jinshuju field add --form Kp7mQ2 --json '{"type":"TextField","label":"Notes"}'
jinshuju field update --form Kp7mQ2 field_3 --json '{"required":true}'
jinshuju field update-choices --form Kp7mQ2 field_7 --json '{"add":[{"label":"Third"}]}'
jinshuju field remove --form Kp7mQ2 field_9 --yes

Data and views:

jinshuju entry create --form Kp7mQ2 --batch @entries.json
jinshuju entry update --form Kp7mQ2 12 --json '{"field_2":99}'                 # merge
jinshuju entry update --form Kp7mQ2 12 --replace --json '{"field_1":"Bob"}'    # replace; omitted fields are cleared
jinshuju entry update --form Kp7mQ2 --batch @rows.json                         # [{serial_number, entry}]
jinshuju entry delete --form Kp7mQ2 12 --yes

jinshuju view create --form Kp7mQ2 "High scores" --filter 'field_3 gte 80' --sort created_at:desc
jinshuju comment create --form Kp7mQ2 --entry 12 "Contacted, awaiting reply"
jinshuju opensearch edit Qy7nR3 --disable

Deletion always requires --yes. This CLI is non-interactive — stdin belongs to --json - — so confirmation is a flag: without it nothing is deleted, rather than a prompt nobody is there to answer.

Searching across containers

jinshuju entry search "Acme Corp"                     # every form and table you can read, up to 10
jinshuju entry search 13800138000 --form Kp7mQ2 --form Vn4xR8
jinshuju entry search repair --scope-filter 'entries_count gt 100'   # picks which forms to search, not which rows

jinshuju entry stats --from 2026-09-01                # how much each form received in the period
jinshuju entry stats --from 2026-09-01 --to 2026-09-07 --kind form --limit 10
jinshuju entry stats --from 2026-09-28 --by day       # the same, split into each day, in one call
jinshuju entry stats --from 2026-01-01 --by month     # or by week / month: at most 92 days, 53 weeks, 24 months

A form that could not be searched stays in the result with the reason, rather than being dropped as if it had matched nothing — "not searched" and "nothing there" are different answers.

entry stats and entry count measure different things: the former is how much arrived (an import counts on the day it ran, and deletions are not subtracted), the latter is how much is there now.

Your own submissions

jinshuju form list --mine                      # forms you filled in, not forms you own
jinshuju entry list --form Kp7mQ2 --mine       # what you submitted to this form
jinshuju entry search "Acme Corp" --mine        # search your own submissions

The scope is pinned to your own submissions: you cannot read anyone else's, and you need no permission on the form itself. Flags that only make sense from an owner's point of view (--sort, --view, --scope-filter and the like) are rejected when combined with --mine.

Working with files

Three commands take a file. The CLI uploads with its own credential; there is no ticket to fetch first.

jinshuju entry import --form Kp7mQ2 ./signups.xlsx --map field_1=Name --map field_2=Mobile
jinshuju entry import --table Vn4xR8 ./rows.csv --map field_1=1 --map field_2=2 --header-row 2 --unique field_1

jinshuju entry create --form Kp7mQ2 --json '{"field_1":"Alice"}' --attach field_5=./id-card.jpg
jinshuju form theme set Kp7mQ2 --wallpaper ./bg.png
jinshuju entry import --form Kp7mQ2 ./signups.xlsx --map field_1=Name --wait   # wait for it; non-zero exit on failure
jinshuju entry import-status --form Kp7mQ2 <job-id>                            # or look it up later

--map takes a field api_code on the left and a column name or column number on the right (a bare number is read as a position). Everything knowable up front is checked before the import starts — the file, the size your plan allows, the header row, the column mapping — so a rejected import has written nothing, and the error names the sheet's actual layout. Once accepted, the rows are written in the background: the command returning means started, not finished.

That is what --wait is for. It waits for the job to settle, reports how many rows were written, skipped and rejected, and exits non-zero on failure. It gives up after thirty minutes, naming the job so it can still be looked up. Without it a failed import is invisible — the command succeeds and not a single row is written. After the fact, entry import-status answers the same question from a job id.

Line-delimited JSON

--output jsonl answers one JSON object per line: a listing becomes its rows, anything else is one line. With --all the rows are written as each page arrives and nothing is held back, so a listing of any size streams through jq -c, a shell loop or a log shipper without first fitting in memory.

jinshuju entry list --form Kp7mQ2 --all --output jsonl | jq -c 'select(.field_3 > 80)'
jinshuju entry list --form Kp7mQ2 --all --output jsonl | while read -r row; do …; done

Errors are JSON on stderr under jsonl exactly as under json.

Progress

Long-running commands (--all paging, uploads, --wait) report progress. Progress is written to stderr, and only when stderr is a terminal, so --output json | jq receives exactly the same bytes it would without it: a pipe, or an agent on the other end, never sees a stray character.

Exit codes

The exit code says what kind of failure it was, so a script can branch without parsing anything:

| code | meaning | | ---- | ------------------------------------------------------------------------ | | 0 | done | | 1 | something unexpected; please report it | | 2 | usage: a flag, argument, input file or unknown command | | 3 | authentication: no credential, or one the server refused (401, 403) | | 4 | not found (404) | | 5 | refused by the API: validation, conflict, quota (other 4xx) | | 6 | the server failed (5xx) | | 7 | transport: the connection failed or timed out | | 8 | rate limited: wait and retry; the message and retry_after say how long |

With --output json, stderr carries the same as JSON, plus the status and body the server answered with:

{
  "error": {
    "kind": "not_found",
    "message": "form cannot be found",
    "status": 404,
    "body": { "error_description": "form cannot be found" }
  }
}

Timeouts and retries

Every request has a deadline of one minute; JINSHUJU_TIMEOUT_MS changes it. A request the server answers with 429 or 503 is tried again after backing off, honouring Retry-After when it is sent. A read is also retried after a gateway failure or a dropped connection. A write is not: a POST that timed out may well have landed, and sending it again would create the entry twice.

Before removing a field

jinshuju field check --form Kp7mQ2 field_3 field_7:choice_1
jinshuju field preview-convert --form Kp7mQ2 field_1 --to RadioButton

field check answers whether a field or a choice has data under it — removing it takes that data along, so it is worth asking first. preview-convert reports how much a type conversion would keep and how much it would clear.

Odds and ends

jinshuju form list --name signup --name survey   # several keywords match any, not one joined phrase
jinshuju entry list --form Kp7mQ2 --labels       # carry the field label with each value, saving a form read
jinshuju table move Vn4xR8 --folder Nf7mDC       # a table only goes into a kind=table folder
jinshuju table create --json @t.json --with-default-entries   # seed a few blank rows, as the web UI does

Token format

Form, table and view tokens are six characters of mixed-case letters and digits, for example Kp7mQ2, Vn4xR8, aB3dE9. Examples here and in --help use that shape throughout.

Development

How the code is organised, and why, is in ARCHITECTURE.md.

npm install
npm test
npm run check       # typecheck + oxlint + oxfmt --check, what CI runs
npm run fmt         # format in place with oxfmt