@datarails-ai/dr-appspace-cli 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +346 -0
  2. package/dist/cli.js +29461 -0
  3. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # dr-appspace — the Datarails App Space CLI
2
+
3
+ One binary, one job: **App Space** — build, try, push and publish App Space
4
+ apps. Nothing else of the Datarails system is exposed here; for everything
5
+ else, use `dr` (dr-cli). (The read-only dr-cli command groups this package
6
+ used to carry were removed — combining CLI capabilities is handled
7
+ elsewhere now.)
8
+
9
+ ```
10
+ npm install -g @datarails-ai/dr-appspace-cli --registry https://datarails.jfrog.io/artifactory/api/npm/dr-cli-virtual
11
+ dr-appspace login -s DEV
12
+ dr-appspace app-space whoami -s DEV
13
+ ```
14
+
15
+ ## What's inside
16
+
17
+ - **Auth — identical to dr-cli** (the code is a port of dr-cli's auth stack):
18
+ OAuth device-flow `login`, sessions in the OS keychain (service
19
+ `dr-appspace-cli`, keyed by `(server, email)`), automatic JWT refresh with
20
+ the full 401 recovery ladder, and the **credential-file store** for
21
+ keychain-less hosts (CI, containers, managed agents): when
22
+ `~/.dr/session.json` (or `$DR_CREDENTIALS_FILE`) exists it wins over the
23
+ keychain. The file contract is byte-compatible with dr-cli's
24
+ (`{access, refresh, server, email}`), so one provisioning step signs in
25
+ both CLIs — the same `dr_cli_auth.json` mount ai-service already provides
26
+ to managed agents works here unchanged.
27
+ - **`app-space` commands** (ported from `dr-dev-cli app-space`, re-based onto
28
+ this auth): `whoami`, `list`, `get`, `logs`, `try`, `push`, `publish`,
29
+ `unpublish` — plus `scaffold`, `preview`, `test`, `contract`, `compile` and
30
+ `eject`.
31
+
32
+ ### A handler that takes a file
33
+
34
+ `try` and `test` can put a file in front of a handler, the way the page does:
35
+ upload it through the app's upload route, then send the signed reference as
36
+ `{"<field>": {"upload": "…"}}`.
37
+
38
+ ```
39
+ dr-appspace app-space try upload_invoice --file pdf # the built-in sample invoice PDF
40
+ dr-appspace app-space try upload_invoice --file pdf=./acme-0417.pdf # a file of yours
41
+ dr-appspace app-space try upload_invoice --file pdf --payload @./payload.json
42
+ ```
43
+
44
+ `--file <field>[=<path>]` is repeatable; a field that is not a `file` column
45
+ of any table in `data_model.json` gets a warning, not a refusal. `--payload
46
+ @<path>` reads the whole payload from a JSON file. Paths are relative to the
47
+ working directory. The sample invoice (`INV-0417`, Acme Office Supplies Ltd,
48
+ subtotal 165.00, tax 33.00, total 198.00) is a one-page ASCII PDF generated in
49
+ `src/commands/appspace/_samplePdf.ts`.
50
+
51
+ `test` hands the same sample PDF to every form's file field, and a
52
+ `tests.json` case may upload files before its dry-run:
53
+
54
+ ```json
55
+ {"handlers": {"upload_invoice": [
56
+ {"name": "reads the sample invoice", "payload": {}, "files": {"pdf": "sample"},
57
+ "shape": {"row": {"id": "string", "status": "string"}}}
58
+ ]}}
59
+ ```
60
+
61
+ A `files` value is `"sample"` (reserved: the built-in sample invoice) or a
62
+ path inside the app folder; a path whose real location is outside the
63
+ folder, through a symlink too, is refused. Every file is read before any is
64
+ uploaded. Each upload stores a real file, as a browser upload does, so a
65
+ handler that hands it to an agent runs that agent on every `test`.
66
+
67
+ `compile` knows a `chart` component: a root element that draws the rows one
68
+ `list`, `inbox` or `custom` handler answers (`source`), as a `line`, `bar` or
69
+ `area` (`kind`), with `x` on the category axis, the one numeric column `y`,
70
+ an optional `series` column (one line per distinct value) and an optional
71
+ `format` (`currency`, `number`, `percent`; absent, a decimal `y` reads as
72
+ currency). Over a `list` or an `inbox` its columns are checked against the
73
+ table; over a `custom` handler `--check` prints `Not checked: chart columns
74
+ of …`, and `app-space test` checks what the handler answers: a `chart-empty`
75
+ finding when the source answered no rows (ungated for a custom handler; over
76
+ a list or an inbox, only when `seed.json` says it should have rows), no
77
+ `rows` array, or rows that carry no `x`/`series` key or no number under `y` —
78
+ naming the keys it did return (names only; no value leaves the run). Like
79
+ every finding it fails the run only under `--strict`. A page holding a
80
+ chart loads `echarts@5.6.0` and `dr-fmt@1.0.0` from the shelf before
81
+ `dr-app-runtime@1.1.5`. An older CLI's `get` drops the element.
82
+
83
+ A dashboard can be **assembled** rather than typed. `compile --draft
84
+ <spec-draft.json>` builds `contract.json` from the dashboard's skeleton
85
+ (`--skeleton <file>`, or the draft's own `skeleton` key) and one SPEC draft
86
+ per page — each card's `props` and SPEC `row`, plus its `page`, `reads`,
87
+ `datasets` and `capabilities` — and then compiles it like any other. The
88
+ frame is the CLI's: the first page is `ui/index.html` at `/`, every other
89
+ `ui/<name>.html` at `/<name>.html`, the skeleton's sections and card order,
90
+ a page not built yet carries one card saying it comes next, and — only
91
+ when the draft states it as `page.window` — the window every page opens on
92
+ (`mount.defaults.filters.period`), which `preview` and the post-push load
93
+ apply too. A draft or contract where every card opts out of the period bar
94
+ is refused (`assemble-period-override` / `period-override`). With a `contract.json` already in the folder a draft replaces only its
95
+ own page — except that a draft carrying `page.window` also moves every
96
+ page's opening window, because one `widget.js` serves them all. `--check` assembles in memory and writes nothing; a draft it cannot
97
+ place is `assemble-*`, exit 1. `--app-id <id>` names the app a first pass is
98
+ for, and `--expect <card>=<figure>` holds a card to the figure the user
99
+ confirmed once the package runs (exit 6 when it does not).
100
+
101
+ ```
102
+ app-space ship [dir] --draft spec-draft-cfo.json --app-id app-7k2m3f --expect revenue=870,000
103
+ ```
104
+
105
+ `ship` is one pass of a dashboard build as one command — `compile --push`
106
+ over the assembled contract, R5 over every card, the `--expect` check — and
107
+ after a run that passed it prints the `appspace_app_update` block a build
108
+ agent's reply ends with, from the revision the push printed (`compile
109
+ --update-block` prints it too). After any failure it prints none.
110
+
111
+ `compile` also knows the two ways a file becomes the author's rather than
112
+ the compiler's, besides a `custom` handler:
113
+
114
+ - a **`custom` element** — `{"type": "custom", "props": {"module":
115
+ "components/<name>.js", "source": "<handler>", "args": {…}}}` — drawn by
116
+ `ui/components/<name>.js`, which calls `drApp.define("components/<name>.js",
117
+ {render(el, data, ui) {…}})` (`dr-app-runtime@1.1.6`; `ui` carries `args`,
118
+ `fmt`, `call`, `refresh`, `chart` and `theme`). `args` must be an object;
119
+ `module` is required.
120
+ - an **owned page** — `pages[i].owned: true` — whose markup is the file at its
121
+ path, carrying `<meta name="dr-owned" content="true">`. An optional
122
+ `pages[i].role` gates its nav entry, as a compiled page's screens do.
123
+
124
+ Both are compiler INPUTS, like a custom handler: they must be in the folder,
125
+ `compile` never writes them (not even under `--force`), records them under
126
+ `inputs` in `.appspace/compile.json`, never removes one as orphaned, and
127
+ `--check` carries them into its scratch package. A `ui/components/*.js` no
128
+ custom element names is refused like a stray handler — a push would ship it.
129
+ `app-space test` reports `custom-undefined` when a module never called
130
+ `drApp.define` for itself, `custom-failed` (with the module and the error)
131
+ when its component threw while drawing and the runtime drew its
132
+ `<module> failed: <message>` panel, and reads no layout rule into an owned
133
+ page. A custom element whose `declares.shows` names a number nothing inside
134
+ it projects once it drew (no `[data-dr-field]` for it) is
135
+ `custom-shows-unprojected` — a counted finding, so `--strict` fails on it: a
136
+ number no grader can read is not a measured one.
137
+
138
+ A card the contract gives no `props.label` is `card-untitled`, one per
139
+ element: the runtime titles it anyway — after its field, its id, or its
140
+ module, y column, source, entity (a form reads "New") — so a builder who
141
+ never named it ships a card called "Whatif":
142
+
143
+ ```
144
+ [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)
145
+ ```
146
+
147
+ It reads the contract alone, for what a page draws as its own — its roots
148
+ and what sits in a region or a tab panel: `custom`, `chart`, `widget`,
149
+ `stat`, `list`, `queue`, `form`, and the parts that draw their label
150
+ (`dialog`, `number_input`, `select`, `date_picker`; a typed
151
+ `props.props.label`, or a dialog's `props.props.title`, names them). An
152
+ element's `card.title` names any of them (runtime 1.4.0's CardFrame draws
153
+ it); a widget's `props.title` does not — the runtime never reads it. A
154
+ field, a nav, a region, a tab panel, `tabs`, `data_grid` and
155
+ `variance_table` draw no title of their own and are never reported. Apps
156
+ only; counted, so `--strict` fails on it.
157
+
158
+ A page may declare its own params (`pages[i].params`, the seven kinds
159
+ `dr-app-runtime@1.4.0`'s param store reads) and a source take them by name
160
+ in `honors`. Every source handler of such an app carries `PARAMS` — every
161
+ page's declarations — into `sources.serve(…, TRANSFORMS, PARAMS)`, so a
162
+ transform's `params` holds only values the page declared; an app no page of
163
+ which declares one compiles exactly as before. A `dimensions:<Name>` honour
164
+ fills the source's `filterable`. `compile` refuses, with a pointer:
165
+ `param_custom_multi` / `param_custom_period` (a custom name of kind `multi`
166
+ or `period` — only `scenario`, `dimensions:<Name>` and `period` take those),
167
+ `param_kind_value` (a default, `min`/`max`/`step`, `values` or
168
+ `max_selected` its kind cannot hold), `param_conflict` (one name, two rules)
169
+ and `param_unknown_honor` (a source honours a name no page declares).
170
+
171
+ The look layer (runtime 1.4.0) is optional and per key: a band may state a
172
+ `subtitle`, a `layout` (`stack | grid | kpi_strip | split | sidebar |
173
+ hero`), a `surface` (`none | tinted | inverse`) and a sidebar's `aside`
174
+ (`start | end`); an element a `card` (`{variant: plain | outlined | elevated
175
+ | accent, title, subtitle}`) and an `emphasis` (`low | normal | high`).
176
+ `app.theme` (`{file: "ui/theme.css", tokens, preset?, density?}`) compiles
177
+ to `ui/theme.css` on `body.dr-app-page` in the `--dr-app-theme: full` form —
178
+ the `--dr-*` tokens and the shadcn triples computed from them by the
179
+ runtime's own map (`contract/themeMap.gen.mjs`, generated from
180
+ `tokens.mjs`) — linked after every shelf stylesheet; `preset` and `density`
181
+ become each page's `data-dr-theme` / `data-dr-density` where the page states
182
+ none. A theme `compile` cannot write is `theme_shape`.
183
+
184
+ `app-space test` holds every declared param to two counted findings, read
185
+ statically from the embedded contract and the pushed modules
186
+ (`contract/levers.mjs`, the App kit's A3.17/A3.18): `control-unbound` — no
187
+ control on the page writes it (no part in param mode, no custom whose module
188
+ calls `ui.setParam("<name>", …)` or whose `declares.produces` names it), so the
189
+ page only ever shows its default — and `param-unread` — no source a component
190
+ on the page reads honours it, directly or through what it is composed of, and
191
+ no custom module reads it off `ui.params`: moving it changes nothing. A page
192
+ with a module the run could not read, or a `ui.setParam` whose name is not a
193
+ literal, is not reported on. The App kit reports A3.18 as a violation and
194
+ A3.17 as a warning until dr-components ships param-mode controls.
195
+
196
+ It then RUNS each lever. On every page that declares params, once the page
197
+ has loaded (and before any form is submitted), `app-space test` moves one
198
+ param at a time through the page's own URL — `#p=` +
199
+ `encodeURIComponent(JSON)` of that one param, then `hashchange`, which
200
+ runtime 1.4.0's param store reads — and compares what the page draws with
201
+ what it drew at the defaults, NUMBERS only: every `data-dr-value` a part or
202
+ custom projects that reads as a number (never a control's own value, nor a
203
+ value whose field is the moved param), the figures in a stat's text (only
204
+ when the stat projects no `data-dr-value`; its dates and periods — `2026-01`,
205
+ `01/2026`, `Aug 2026`, `Q1 2026` — dropped, and any figure equal to the value
206
+ the param was at or moved to dropped as an echo), and the numbers in each
207
+ chart's series `data` as handed to echarts, gaps dropped. Labels never count
208
+ — a chart's categories, its series names, a month or a name in a cell or a
209
+ stat — so a start month that only relabels the months, every value the same,
210
+ is static. A number moves to its `max` (else double its default,
211
+ else default + `step`), a month one month on, a toggle flips, a select takes
212
+ another value, a multi another subset. A param whose first value moved no
213
+ number is moved once more — the other end of its range (else default +
214
+ `step`), a month one month back, another select value or subset — since a
215
+ lever can be inert at one end. Only a param neither value moved is
216
+ `lever-static`, counted: `ui/index.html: moving param "hires" (5 → 20, and
217
+ 5 → 0) changed no number on the page — its sources honour it, but nothing
218
+ drawn depends on it. Fix: Check the transform reads params.get("hires")
219
+ (transforms/plan.py) …`. `--json` carries `levers: [{page, param, from, to,
220
+ second?, moved: [uid…]}]` (plus `inconclusive` for a lever it could not
221
+ judge). The moved reads go to the pushed app's real handlers, so the
222
+ transforms run with the moved params; a lever this run could not judge —
223
+ `--skip-ui`, a page that did not load, a move no source that honours the
224
+ param re-read after (a missed `hashchange`, a debounce past the grace — a
225
+ refetch of some other source does not count), a read that failed or was
226
+ still running with the param moved, a page that drew no number at its
227
+ defaults, a budget that ran out before the second value — is a `Not checked` line naming the param, never a pass and never
228
+ `lever-static`. Cost: one settle per declared param,
229
+ a second for each that moved nothing at its first value, plus one to move
230
+ back, on the page already mounted (the first render of each page only).
231
+
232
+ Roles are declared least senior first and the app's creator holds the last
233
+ one. `compile` refuses a list that reads the other way (`role-order`: the
234
+ default not first, or a creator's role that cannot call a handler the default
235
+ can), and `app-space test` reports `role-sees-nothing` when the app gates
236
+ components to roles and none of them to the role you hold — the page you open
237
+ is then only the ungated rest, although every render as the other roles
238
+ passes.
239
+
240
+ Before its findings, `app-space test` says how much of the pushed app is the
241
+ registry's and how much the builder's own code:
242
+
243
+ ```
244
+ parts: 5 registry, 0 custom UI (0 lines), 0 custom handlers (0 lines), 2 transforms (30 lines) — 0% custom
245
+ ```
246
+
247
+ Custom UI is each `custom` element (with its modules' lines), a custom handler
248
+ each `custom`-role handler (with its lines), and a transform each file under
249
+ `transforms/` that `data.sources` names (once, however many sources share it,
250
+ by its non-blank lines — read out of the generated source handlers, which carry
251
+ it). The percent is custom UI's share of the components and nothing else.
252
+ `--json` carries the same counts as `parts`, the transforms as
253
+ `parts.transforms: {count, lines}`. A dashboard's line names no transforms.
254
+
255
+ `app-space eject <target> [--dir <dir>]` writes the starting file:
256
+
257
+ ```
258
+ dr-appspace app-space eject handler:list_my_invoices # handlers/<name>.py as compiled, role "custom"
259
+ dr-appspace app-space eject page:ui/queue.html # the compiled page, marked owned; its elements leave the contract
260
+ dr-appspace app-space eject element:ui/overview.html#13 # a chart or a stat becomes a custom element + its module
261
+ ```
262
+
263
+ It compiles in memory, writes only the ejected file and `contract.json`,
264
+ refuses a file already at that path unless it holds exactly what the
265
+ compiler would write, and refuses an eject after which the folder would not
266
+ compile (a page holding the only form a `by` handler is called from, a
267
+ handler a picker needs as a list). `page:` keeps the page's nav gate as
268
+ `pages[i].role`; an ejected handler's docstring says the file is yours.
269
+ The next `compile` carries the file as yours, and the one after writes
270
+ nothing.
271
+
272
+ ### Which kit made the app: `push --kit`
273
+
274
+ `push` can name the builder kit that made the package, so the platform's
275
+ contract record (`app_contract.kit_version`) says which kit built each app
276
+ rather than the extractor's stamp, `dashboard-kit/contract@1`, which every
277
+ kit shares:
278
+
279
+ ```
280
+ dr-appspace app-space push --kit appspace-app-kit@12
281
+ DR_APPSPACE_KIT=appspace-app-kit@12 dr-appspace app-space push
282
+ ```
283
+
284
+ The value travels as the push body's optional `kit` field. `--kit` wins over
285
+ `DR_APPSPACE_KIT`; with neither set (or either left blank) the field is left
286
+ out, and the platform records the contract's `generated_by` as before.
287
+ `compile --push`, `ship` and `compile --check` build the same body, so they
288
+ honour the variable too; they have no `--kit` flag of their own. The platform
289
+ checks the shape: `<name>@<number>`, the name in `a-z`, `0-9` and `-`, 64
290
+ characters at most. Anything else is a 422 naming that shape. The field is a
291
+ column on the record and nothing else: the contract document, the compiled
292
+ pages and the dashboard are the same with or without it. The builder flows
293
+ set `DR_APPSPACE_KIT` in their sandboxes.
294
+
295
+ **Push before `get`**: `get` replaces `handlers/` and `ui/` with what the
296
+ platform holds, and refuses (unless `--force`) while the compile record lists
297
+ files as yours that the pushed package does not hold as written.
298
+
299
+ This package ships **no skill and no generator**. The guidance an agent reads
300
+ lives in dr-assets-service, attached to the agent by id — see the repo's
301
+ `CLAUDE.md`. Nothing here installs, prints or bundles a `SKILL.md`, and no
302
+ command calls a model: the builders write `contract.json` themselves and run
303
+ `app-space compile`.
304
+
305
+ ## Development
306
+
307
+ This package lives inside the dr-app-space repo but is independent of the
308
+ Python service: its own `package.json`, pnpm lockfile, and CI
309
+ (`.github/workflows/cli-checks.yml`).
310
+
311
+ ```
312
+ cd cli
313
+ pnpm install
314
+ pnpm dev -- --help # run from source
315
+ pnpm typecheck && pnpm test
316
+ pnpm build # dist/cli.js
317
+ ```
318
+
319
+ ## Releasing
320
+
321
+ `.github/workflows/cli-release.yml` releases automatically on every push to
322
+ main that touches `cli/` (patch bump); manual dispatch remains for
323
+ minor/major or an explicit version. Flow: tests → resolve version → build →
324
+ publish to the `dr-cli-public` JFrog repo with `--ignore-scripts` → push a
325
+ `cli-v<semver>` tag (best-effort, traceability only). Versioning is
326
+ stateless because main's ruleset forbids direct pushes: no bump commit is
327
+ ever made — the next version is derived from the latest one already on the
328
+ registry, and `package.json`'s version only seeds the very first publish.
329
+ Consumers
330
+ install through `dr-cli-virtual`, which fronts `dr-cli-public` + public npm
331
+ with anonymous read — the same registry managed agents already use for
332
+ dr-cli, so no new JFrog infrastructure is needed.
333
+
334
+ Squat protection: like dr-cli, the scoped name means federation with public
335
+ npm is safe; if an unscoped placeholder is ever wanted on npmjs, publish it
336
+ manually (`0.0.1-locked`), as was done for `@datarails-ai/dr-cli`.
337
+
338
+ ## Managed agents (later)
339
+
340
+ To expose this CLI on a managed agent the ai-service side needs the same
341
+ wiring dr-cli got under DR-50929 (`cliAgentDefinition.ts` and friends):
342
+ an install preamble (`npm install -g @datarails-ai/dr-appspace-cli --registry
343
+ .../dr-cli-virtual`), the mounted credential file (this CLI reads the exact
344
+ same `~/.dr/session.json` contract), and a token-refresh custom tool. Nothing
345
+ in this package blocks that — the credential-file store is the piece that
346
+ makes it possible, and it ships here from day one.