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

@datarails-ai/dr-appspace-cli

v0.12.3

Published

Datarails App Space CLI — build, try, push and publish App Space apps

Readme

dr-appspace — the Datarails App Space CLI

One binary, one job: App Space — build, try, push and publish App Space apps. Nothing else of the Datarails system is exposed here; for everything else, use dr (dr-cli). (The read-only dr-cli command groups this package used to carry were removed — combining CLI capabilities is handled elsewhere now.)

npm install -g @datarails-ai/dr-appspace-cli --registry https://datarails.jfrog.io/artifactory/api/npm/dr-cli-virtual
dr-appspace login -s DEV
dr-appspace app-space whoami -s DEV

What's inside

  • Auth — identical to dr-cli (the code is a port of dr-cli's auth stack): OAuth device-flow login, sessions in the OS keychain (service dr-appspace-cli, keyed by (server, email)), automatic JWT refresh with the full 401 recovery ladder, and the credential-file store for keychain-less hosts (CI, containers, managed agents): when ~/.dr/session.json (or $DR_CREDENTIALS_FILE) exists it wins over the keychain. The file contract is byte-compatible with dr-cli's ({access, refresh, server, email}), so one provisioning step signs in both CLIs — the same dr_cli_auth.json mount ai-service already provides to managed agents works here unchanged.
  • app-space commands (ported from dr-dev-cli app-space, re-based onto this auth): whoami, list, get, logs, try, push, publish, unpublish — plus scaffold, preview, test, contract, compile and eject.

A handler that takes a file

try and test can put a file in front of a handler, the way the page does: upload it through the app's upload route, then send the signed reference as {"<field>": {"upload": "…"}}.

dr-appspace app-space try upload_invoice --file pdf                   # the built-in sample invoice PDF
dr-appspace app-space try upload_invoice --file pdf=./acme-0417.pdf   # a file of yours
dr-appspace app-space try upload_invoice --file pdf --payload @./payload.json

--file <field>[=<path>] is repeatable; a field that is not a file column of any table in data_model.json gets a warning, not a refusal. --payload @<path> reads the whole payload from a JSON file. Paths are relative to the working directory. The sample invoice (INV-0417, Acme Office Supplies Ltd, subtotal 165.00, tax 33.00, total 198.00) is a one-page ASCII PDF generated in src/commands/appspace/_samplePdf.ts.

test hands the same sample PDF to every form's file field, and a tests.json case may upload files before its dry-run:

{"handlers": {"upload_invoice": [
  {"name": "reads the sample invoice", "payload": {}, "files": {"pdf": "sample"},
   "shape": {"row": {"id": "string", "status": "string"}}}
]}}

A files value is "sample" (reserved: the built-in sample invoice) or a path inside the app folder; a path whose real location is outside the folder, through a symlink too, is refused. Every file is read before any is uploaded. Each upload stores a real file, as a browser upload does, so a handler that hands it to an agent runs that agent on every test.

compile knows a chart component: a root element that draws the rows one list, inbox or custom handler answers (source), as a line, bar or area (kind), with x on the category axis, the one numeric column y, an optional series column (one line per distinct value) and an optional format (currency, number, percent; absent, a decimal y reads as currency). Over a list or an inbox its columns are checked against the table; over a custom handler --check prints Not checked: chart columns of …, and app-space test checks what the handler answers: a chart-empty finding when the source answered no rows (ungated for a custom handler; over a list or an inbox, only when seed.json says it should have rows), no rows array, or rows that carry no x/series key or no number under y — naming the keys it did return (names only; no value leaves the run). Like every finding it fails the run only under --strict. A page holding a chart loads [email protected] and dr-fmt (the compiler's FMT, 2.0.0 since P1b) from the shelf before the runtime. An older CLI's get drops the element.

A dashboard can be assembled rather than typed. compile --draft <spec-draft.json> builds contract.json from the dashboard's skeleton (--skeleton <file>, or the draft's own skeleton key) and one SPEC draft per page — each card's props and SPEC row, plus its page, reads, datasets and capabilities — and then compiles it like any other. The app's name and description (the line under the name on its card, which kit rule R1.5 requires) are the skeleton's app, as written. The rest of the frame is the CLI's: the first page is ui/index.html at /, every other ui/<name>.html at /<name>.html, the skeleton's sections and card order, a page not built yet carries one card saying it comes next, and — only when the draft states it as page.window — the window every page opens on (mount.defaults.filters.period), which preview and the post-push load apply too. A draft or contract where every card opts out of the period bar is refused (assemble-period-override / period-override). With a contract.json already in the folder a draft replaces only its own page — except that a draft carrying page.window also moves every page's opening window, because one widget.js serves them all. --check assembles in memory and writes nothing; a draft it cannot place is assemble-*, exit 1. --app-id <id> names the app a first pass is for, and --expect <card>=<figure> holds a card to the figure the user confirmed once the package runs (exit 6 when it does not).

A dataset series row may be drawn through its own transform — "transform": "transforms/<name>.py", the App Builder's transform(rows, ctx, params), handed the dataset's whole monthly history. A draft carries the file as "transforms": {"transforms/<name>.py": "<text>"}; it is written beside contract.json (not under --check) and compiled into the handler, and a later draft's text replaces it and says so — unless a card that is not the draft's is drawn through that file, which is refused. A skeleton page may carry controls ({id, label, param, kind: "slider"|"number", min, max, step, default}) and a card params: the control is written once a row on the page declares its param, the card redraws with it, and a row whose own rule is not the control's is refused. preview draws such a card in two round trips: the server hands over the rows the transform would get, the transform runs here with python3 -I — 20 s at most, what it prints shown under the card rather than read as its answer — and the server draws what it returned, so the figure printed is the card's. Only a transform the previewed file's drafts carry runs here, never a file in the folder: get restores there what the live app carries, whoever pushed it. compile's lint and the server's own ast check run first, so preview refuses what the page would; neither is a sandbox, and the run starts with no environment beyond PATH, HOME, the locale and a Python version manager's own variables.

app-space ship [dir] --draft spec-draft-cfo.json --app-id app-7k2m3f --expect revenue=870,000

ship is one pass of a dashboard build as one command — compile --push over the assembled contract, R5 over every card, the --expect check — and after a run that passed it prints the appspace_app_update block a build agent's reply ends with, from the revision the push printed (compile --update-block prints it too). After any failure it prints none.

compile also knows the two ways a file becomes the author's rather than the compiler's, besides a custom handler:

  • a custom element — {"type": "custom", "props": {"module": "components/<name>.js", "source": "<handler>", "args": {…}}} — drawn by ui/components/<name>.js, which calls drApp.define("components/<name>.js", {render(el, data, ui) {…}}) ([email protected]; ui carries args, fmt, call, refresh, chart and theme). args must be an object; module is required.
  • an owned page — pages[i].owned: true — whose markup is the file at its path, carrying <meta name="dr-owned" content="true">. An optional pages[i].role gates its nav entry, as a compiled page's screens do.

Both are compiler INPUTS, like a custom handler: they must be in the folder, compile never writes them (not even under --force), records them under inputs in .appspace/compile.json, never removes one as orphaned, and --check carries them into its scratch package. A ui/components/*.js no custom element names is refused like a stray handler — a push would ship it. app-space test reports custom-undefined when a module never called drApp.define for itself, custom-failed (with the module and the error) when its component threw while drawing and the runtime drew its <module> failed: <message> panel, and reads no layout rule into an owned page. A custom element whose declares.shows names a number nothing inside it projects once it drew (no [data-dr-field] for it) is custom-shows-unprojected — a counted finding, so --strict fails on it: a number no grader can read is not a measured one.

A card the contract gives no props.label is card-untitled, one per element: the runtime titles it anyway — after its field, its id, or its module, y column, source, entity (a form reads "New") — so a builder who never named it ships a card called "Whatif":

[card-untitled]  ui/cockpit.html → ui/cockpit.html#10: custom "whatif" has no label, so its card is titled "Whatif" after its id. Fix: give it props.label (what a reader should call this card). (contract.md §3.4 Pages)

It reads the contract alone, for what a page draws as its own — its roots and what sits in a region or a tab panel: custom, chart, widget, stat, list, queue, form, and the parts that draw their label (dialog, number_input, select, date_picker; a typed props.props.label, or a dialog's props.props.title, names them). An element's card.title names any of them (runtime 1.4.0's CardFrame draws it); a widget's props.title does not — the runtime never reads it. A field, a nav, a region, a tab panel, tabs, data_grid and variance_table draw no title of their own and are never reported. Apps only; counted, so --strict fails on it.

A page may declare its own params (pages[i].params, the seven kinds [email protected]'s param store reads) and a source take them by name in honors. Every source handler of such an app carries PARAMS — every page's declarations — into sources.serve(…, TRANSFORMS, PARAMS), so a transform's params holds only values the page declared; an app no page of which declares one compiles exactly as before. A dimensions:<Name> honour fills the source's filterable. compile refuses, with a pointer: param_custom_multi / param_custom_period (a custom name of kind multi or period — only scenario, dimensions:<Name> and period take those), param_kind_value (a default, min/max/step, values or max_selected its kind cannot hold), param_conflict (one name, two rules) and param_unknown_honor (a source honours a name no page declares).

The look layer (runtime 1.4.0) is optional and per key: a band may state a subtitle, a layout (stack | grid | kpi_strip | split | sidebar | hero), a surface (none | tinted | inverse) and a sidebar's aside (start | end); an element a card ({variant: plain | outlined | elevated | accent, title, subtitle}) and an emphasis (low | normal | high). app.theme ({file: "ui/theme.css", tokens, preset?, density?}) compiles to ui/theme.css on body.dr-app-page in the --dr-app-theme: full form — the --dr-* tokens and the shadcn triples computed from them by the runtime's own map (contract/themeMap.gen.mjs, generated from tokens.mjs) — linked after every shelf stylesheet; preset and density become each page's data-dr-theme / data-dr-density where the page states none. A theme compile cannot write is theme_shape.

app-space test holds every declared param to two counted findings, read statically from the embedded contract and the pushed modules (contract/levers.mjs, the App kit's A3.17/A3.18): control-unbound — no control on the page writes it (no part in param mode, no custom whose module calls ui.setParam("<name>", …) or whose declares.produces names it), so the page only ever shows its default — and param-unread — no source a component on the page reads honours it, directly or through what it is composed of, and no custom module reads it off ui.params: moving it changes nothing. A page with a module the run could not read, or a ui.setParam whose name is not a literal, is not reported on. The App kit reports A3.18 as a violation and A3.17 as a warning until dr-components ships param-mode controls.

It then RUNS each lever. On every page that declares params, once the page has loaded (and before any form is submitted), app-space test moves one param at a time through the page's own URL — #p= + encodeURIComponent(JSON) of that one param, then hashchange, which runtime 1.4.0's param store reads — and compares what the page draws with what it drew at the defaults, NUMBERS only: every data-dr-value a part or custom projects that reads as a number (never a control's own value, nor a value whose field is the moved param), the figures in a stat's text (only when the stat projects no data-dr-value; its dates and periods — 2026-01, 01/2026, Aug 2026, Q1 2026 — dropped, and any figure equal to the value the param was at or moved to dropped as an echo), and the numbers in each chart's series data as handed to echarts, gaps dropped. Labels never count — a chart's categories, its series names, a month or a name in a cell or a stat — so a start month that only relabels the months, every value the same, is static. A number moves to its max (else double its default, else default + step), a month one month on, a toggle flips, a select takes another value, a multi another subset. A param whose first value moved no number is moved once more — the other end of its range (else default + step), a month one month back, another select value or subset — since a lever can be inert at one end. Only a param neither value moved is lever-static, counted: ui/index.html: moving param "hires" (5 → 20, and 5 → 0) changed no number on the page — its sources honour it, but nothing drawn depends on it. Fix: Check the transform reads params.get("hires") (transforms/plan.py) …. --json carries levers: [{page, param, from, to, second?, moved: [uid…]}] (plus inconclusive for a lever it could not judge). The moved reads go to the pushed app's real handlers, so the transforms run with the moved params; a lever this run could not judge — --skip-ui, a page that did not load, a move no source that honours the param re-read after (a missed hashchange, a debounce past the grace — a refetch of some other source does not count), a read that failed or was still running with the param moved, a page that drew no number at its defaults, a budget that ran out before the second value — is a Not checked line naming the param, never a pass and never lever-static. Cost: one settle per declared param, a second for each that moved nothing at its first value, plus one to move back, on the page already mounted (the first render of each page only).

Roles are declared least senior first and the app's creator holds the last one. compile refuses a list that reads the other way (role-order: the default not first, or a creator's role that cannot call a handler the default can), and app-space test reports role-sees-nothing when the app gates components to roles and none of them to the role you hold — the page you open is then only the ungated rest, although every render as the other roles passes.

Before its findings, app-space test says how much of the pushed app is the registry's and how much the builder's own code:

parts: 5 registry, 0 custom UI (0 lines), 0 custom handlers (0 lines), 2 transforms (30 lines) — 0% custom

Custom UI is each custom element (with its modules' lines), a custom handler each custom-role handler (with its lines), and a transform each file under transforms/ that data.sources names (once, however many sources share it, by its non-blank lines — read out of the generated source handlers, which carry it). The percent is custom UI's share of the components and nothing else. --json carries the same counts as parts, the transforms as parts.transforms: {count, lines}. A dashboard's line names no transforms.

app-space eject <target> [--dir <dir>] writes the starting file:

dr-appspace app-space eject handler:list_my_invoices      # handlers/<name>.py as compiled, role "custom"
dr-appspace app-space eject page:ui/queue.html            # the compiled page, marked owned; its elements leave the contract
dr-appspace app-space eject element:ui/overview.html#13   # a chart or a stat becomes a custom element + its module

It compiles in memory, writes only the ejected file and contract.json, refuses a file already at that path unless it holds exactly what the compiler would write, and refuses an eject after which the folder would not compile (a page holding the only form a by handler is called from, a handler a picker needs as a list). page: keeps the page's nav gate as pages[i].role; an ejected handler's docstring says the file is yours. The next compile carries the file as yours, and the one after writes nothing.

Which kit made the app: push --kit

push can name the builder kit that made the package, so the platform's contract record (app_contract.kit_version) says which kit built each app rather than the extractor's stamp, dashboard-kit/contract@1, which every kit shares:

dr-appspace app-space push --kit appspace-app-kit@12
DR_APPSPACE_KIT=appspace-app-kit@12 dr-appspace app-space push

The value travels as the push body's optional kit field. --kit wins over DR_APPSPACE_KIT; with neither set (or either left blank) the field is left out, and the platform records the contract's generated_by as before. compile --push, ship and compile --check build the same body, so they honour the variable too; they have no --kit flag of their own. The platform checks the shape: <name>@<number>, the name in a-z, 0-9 and -, 64 characters at most. Anything else is a 422 naming that shape. The field is a column on the record and nothing else: the contract document, the compiled pages and the dashboard are the same with or without it. The builder flows set DR_APPSPACE_KIT in their sandboxes.

Push before get: get replaces handlers/ and ui/ with what the platform holds, and refuses (unless --force) while the compile record lists files as yours that the pushed package does not hold as written.

This package ships no skill and no generator. The guidance an agent reads lives in dr-assets-service, attached to the agent by id — see the repo's CLAUDE.md. Nothing here installs, prints or bundles a SKILL.md, and no command calls a model: the builders write contract.json themselves and run app-space compile.

Development

This package lives inside the dr-app-space repo but is independent of the Python service: its own package.json, pnpm lockfile, and CI (.github/workflows/cli-checks.yml).

cd cli
pnpm install
pnpm dev -- --help     # run from source
pnpm typecheck && pnpm test
pnpm build             # dist/cli.js

Releasing

.github/workflows/cli-release.yml releases automatically on every push to main that touches cli/ (patch bump); manual dispatch remains for minor/major or an explicit version. Flow: tests → resolve version → build → publish to the dr-cli-public JFrog repo with --ignore-scripts → push a cli-v<semver> tag (best-effort, traceability only). Versioning is stateless because main's ruleset forbids direct pushes: no bump commit is ever made — the next version is derived from the latest one already on the registry, and package.json's version only seeds the very first publish. Consumers install through dr-cli-virtual, which fronts dr-cli-public + public npm with anonymous read — the same registry managed agents already use for dr-cli, so no new JFrog infrastructure is needed.

Squat protection: like dr-cli, the scoped name means federation with public npm is safe; if an unscoped placeholder is ever wanted on npmjs, publish it manually (0.0.1-locked), as was done for @datarails-ai/dr-cli.

Managed agents (later)

To expose this CLI on a managed agent the ai-service side needs the same wiring dr-cli got under DR-50929 (cliAgentDefinition.ts and friends): an install preamble (npm install -g @datarails-ai/dr-appspace-cli --registry .../dr-cli-virtual), the mounted credential file (this CLI reads the exact same ~/.dr/session.json contract), and a token-refresh custom tool. Nothing in this package blocks that — the credential-file store is the piece that makes it possible, and it ships here from day one.