@stndrds/cli 1.0.0-alpha.258 → 1.0.0-alpha.259

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stndrds/cli",
3
- "version": "1.0.0-alpha.258",
3
+ "version": "1.0.0-alpha.259",
4
4
  "description": "CLI tool to interact with Standards API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,9 +10,12 @@
10
10
  "dist"
11
11
  ],
12
12
  "dependencies": {
13
+ "@clack/prompts": "^1.7.0",
13
14
  "chalk": "^5.4.1",
14
15
  "cli-table3": "^0.6.5",
15
- "commander": "^13.1.0"
16
+ "commander": "^13.1.0",
17
+ "esbuild": "^0.25.0",
18
+ "@stndrds/schema": "1.0.0-alpha.259"
16
19
  },
17
20
  "devDependencies": {
18
21
  "@types/node": "^25.6.0",
@@ -1,513 +0,0 @@
1
- # Standards Schema Builder — Reference
2
-
3
- ## Overview
4
-
5
- Standards uses a fluent builder API to define your data schema in TypeScript.
6
- All objects and attributes declared in code are **system** (immutable at runtime).
7
- Users can extend your schema at runtime by adding custom attributes via the UI.
8
-
9
- ## Importing
10
-
11
- ```typescript
12
- import {
13
- object,
14
- text, richtext,
15
- number, currency,
16
- checkbox,
17
- date,
18
- phone,
19
- select, multiselect, status,
20
- relation,
21
- user,
22
- file, document,
23
- location,
24
- formula, rollup,
25
- } from "@stndrds/schema";
26
- ```
27
-
28
- ## `object()` — Define an Object (table)
29
-
30
- ```typescript
31
- const DEAL = object({ name: "deals", label: "Deal" })
32
- .pluralLabel("Deals")
33
- .description("Sales opportunities")
34
- .icon("briefcase")
35
- .order(10) // sidebar display order
36
- .labelExpression("{{ name }}") // REQUIRED — how records display
37
- .attribute(text({ name: "name", label: "Name" }).required())
38
- .attribute(number({ name: "amount", label: "Amount" }))
39
- .build();
40
- ```
41
-
42
- Object names must be **kebab-case** (e.g. `"deals"`, `"order-items"`). Max 63 characters.
43
-
44
- The `.build()` call is required at the end. The returned value is the `ObjectDefinition`.
45
-
46
- ### `.sealed()` — Prevent custom attributes
47
-
48
- Marks this object as closed: `standards diff` will error if any user-created
49
- custom attribute is found in the DB that is not listed in `.tolerate()`.
50
-
51
- ```typescript
52
- object({ name: "invoice", label: "Invoice" })
53
- .sealed()
54
- .labelExpression("{{ ref }}")
55
- .attribute(number({ name: "amount", label: "Amount" }))
56
- .build();
57
- ```
58
-
59
- ### `.tolerate(["name"])` — Acknowledge known custom attributes
60
-
61
- On a **sealed** object: these names won't cause a CI failure.
62
- On an **extensible** object: purely documentary.
63
-
64
- ```typescript
65
- object({ name: "invoice", label: "Invoice" })
66
- .sealed()
67
- .tolerate(["legacy_ref", "old_notes"])
68
- .labelExpression("{{ ref }}")
69
- .attribute(number({ name: "amount", label: "Amount" }))
70
- .build();
71
- ```
72
-
73
- ### `.labelExpression()` — Display template (REQUIRED)
74
-
75
- Every object must declare a label expression. Uses `{{ attribute }}` syntax.
76
- Pipes are supported: `{{ name | UPPER }}`, `{{ name | capitalize }}`, `{{ name | trim }}`, `{{ name | LOWER }}`.
77
-
78
- ```typescript
79
- .labelExpression("{{ firstName }} {{ lastName }}")
80
- .labelExpression("{{ name | UPPER }}")
81
- .labelExpression("{{ ref }} — {{ title }}")
82
- ```
83
-
84
- ### Other object methods
85
-
86
- | Method | Purpose |
87
- |---|---|
88
- | `.pluralLabel("Deals")` | Plural form shown in collections |
89
- | `.description("...")` | Shown in admin UI tooltips |
90
- | `.icon("briefcase")` | Icon name from `@stndrds/constants` |
91
- | `.order(10)` | Sidebar sort order (lower = first) |
92
-
93
- ---
94
-
95
- ## Attribute Types
96
-
97
- All attribute builders accept `{ name, label }` as first argument.
98
- `name` must be **camelCase** or **snake_case**, unique within the object.
99
-
100
- ### `text()` — Plain text
101
-
102
- ```typescript
103
- text({ name: "name", label: "Name" })
104
- .required()
105
- .minLength(1)
106
- .maxLength(255)
107
- .pattern("^[A-Z].*") // custom regex
108
- .email() // shortcut: validates as email
109
- .url() // shortcut: validates as URL
110
- .slug() // shortcut: validates as slug (a-z0-9-)
111
- .placeholder("Enter name...")
112
- ```
113
-
114
- Use `.multiline()` for longer plain text:
115
-
116
- ```typescript
117
- text({ name: "notes", label: "Notes" })
118
- .multiline()
119
- .optional()
120
- .placeholder("Add notes...")
121
- ```
122
-
123
- ### `richtext()` — Rich text (Tiptap editor)
124
-
125
- ```typescript
126
- richtext({ name: "description", label: "Description" }).required()
127
- ```
128
-
129
- ### `number()` — Numeric value
130
-
131
- ```typescript
132
- number({ name: "amount", label: "Amount" })
133
- .decimal(2) // decimal with 2 places (default unit="decimal")
134
- .integer() // whole numbers only (unit="integer", decimals=0)
135
- .percentage() // percentage display (unit="percentage")
136
- .min(0)
137
- .max(100)
138
- .required()
139
- ```
140
-
141
- Use `.renderAs("rating")` for rating UI backed by a number:
142
-
143
- ```typescript
144
- number({ name: "score", label: "Score" })
145
- .renderAs("rating")
146
- .max(5)
147
- ```
148
-
149
- ### `currency()` — Monetary amount with currency code
150
-
151
- ```typescript
152
- currency({ name: "price", label: "Price" })
153
- .defaultCurrency("EUR")
154
- .allowedCurrencies(["EUR", "USD", "GBP"])
155
- .allowNegative()
156
- .required()
157
- ```
158
-
159
- ### `checkbox()` — Boolean toggle
160
-
161
- ```typescript
162
- checkbox({ name: "isVerified", label: "Verified" })
163
- .defaultValue(false)
164
- ```
165
-
166
- ### `date()` — Date picker
167
-
168
- ```typescript
169
- date({ name: "closeDate", label: "Close Date" })
170
- .format("short") // "short" | "long" | "full" | "relative"
171
- .minDate("2024-01-01")
172
- .maxDate("2030-12-31")
173
- .required()
174
- ```
175
-
176
- ### `phone()` — Phone number with country code
177
-
178
- ```typescript
179
- phone({ name: "mobile", label: "Mobile" })
180
- .defaultCountry("FRA") // ISO 3166-1 alpha-3 country code
181
- .required()
182
- ```
183
-
184
- ### `select()` — Single-choice dropdown
185
-
186
- ```typescript
187
- select({ name: "status", label: "Status" })
188
- .options([
189
- { label: "Lead", value: "lead", color: "blue" },
190
- { label: "Client", value: "client", color: "green" },
191
- ])
192
- .required()
193
- ```
194
-
195
- Options shape: `{ label: string, value: string, color?: string, description?: string }`.
196
- Colors: `"gray"` | `"red"` | `"orange"` | `"yellow"` | `"green"` | `"blue"` | `"purple"`.
197
-
198
- Use `.option({ ... })` to append a single option instead of replacing all.
199
-
200
- ### `multiselect()` — Multi-choice dropdown
201
-
202
- Same API as `select()`, but the value is an array of selected option values.
203
-
204
- ```typescript
205
- multiselect({ name: "tags", label: "Tags" })
206
- .options([
207
- { label: "Urgent", value: "urgent", color: "red" },
208
- { label: "Pending", value: "pending", color: "orange" },
209
- ])
210
- ```
211
-
212
- ### `status()` — Status with workflow groups
213
-
214
- Like `select()` but options support a `group` field for visual grouping.
215
-
216
- ```typescript
217
- status({ name: "stage", label: "Stage" })
218
- .options([
219
- { label: "To Do", value: "todo", color: "gray", group: "idle" },
220
- { label: "In Progress", value: "in_progress", color: "blue", group: "in_progress" },
221
- { label: "Done", value: "done", color: "green", group: "finished" },
222
- ])
223
- .required()
224
- ```
225
-
226
- Option groups: `"idle"` | `"in_progress"` | `"finished"`. The `group` field is optional.
227
-
228
- ### `user()` — User reference
229
-
230
- ```typescript
231
- user({ name: "owner", label: "Owner" })
232
- .multiple() // allow multiple actors
233
- .types(["user", "agent"]) // allow users, agents, or both
234
- .required()
235
- ```
236
-
237
- ### `file()` — File upload
238
-
239
- ```typescript
240
- file({ name: "attachment", label: "Attachment" })
241
- .multiple() // allow multiple files
242
- .maxFiles(5)
243
- .maxSize(10 * 1024 * 1024) // 10 MB in bytes
244
- .allowedTypes(["image/png", "image/jpeg", "application/pdf"])
245
- .required()
246
- ```
247
-
248
- ### `document()` — Structured document with slots and edge properties
249
-
250
- Documents wrap files with agent-compatible processing and edge-level metadata.
251
-
252
- ```typescript
253
- document({ name: "contracts", label: "Contracts" })
254
- .slots([{ name: "file", label: "File", acceptedMimeTypes: ["application/pdf"] }])
255
- .qualifyWith(
256
- date({ name: "signedDate", label: "Signed Date" }),
257
- select({ name: "status", label: "Status" }).options([
258
- { label: "Pending", value: "pending", color: "orange" },
259
- { label: "Validated", value: "validated", color: "green" },
260
- ])
261
- )
262
- ```
263
-
264
- For multi-sided documents (e.g. identity card), define slots:
265
-
266
- ```typescript
267
- document({ name: "identityDoc", label: "Identity Document" })
268
- .slots([
269
- {
270
- name: "recto",
271
- label: "Front",
272
- required: true,
273
- acceptedMimeTypes: ["image/*", "application/pdf"],
274
- maxSizeBytes: 10 * 1024 * 1024,
275
- },
276
- {
277
- name: "verso",
278
- label: "Back",
279
- required: false,
280
- acceptedMimeTypes: ["image/*", "application/pdf"],
281
- maxSizeBytes: 10 * 1024 * 1024,
282
- },
283
- ])
284
- ```
285
-
286
- ### `location()` — Address / geographic location
287
-
288
- ```typescript
289
- location({ name: "address", label: "Address" })
290
- .granularity("full") // "full" | "address" | "city" | "state" | "country"
291
- .defaultCountry("FRA")
292
- .allowedCountries(["FRA", "BEL", "CHE"])
293
- .required()
294
- ```
295
-
296
- ### `relation()` — Record reference (single or many)
297
-
298
- ```typescript
299
- // Single relation (default cardinality: "one")
300
- relation({ name: "company", label: "Company" })
301
- .to("companies")
302
- .required()
303
-
304
- // Multi relation
305
- relation({ name: "contacts", label: "Contacts" })
306
- .to("contacts")
307
- .many()
308
-
309
- // Polymorphic (multiple targets)
310
- relation({ name: "linked", label: "Linked" })
311
- .to("companies")
312
- .to("contacts")
313
- .many()
314
-
315
- // Universal (link to any object)
316
- relation({ name: "reference", label: "Reference" })
317
- .toAny()
318
- .many()
319
- ```
320
-
321
- **Qualified relations** — add metadata on each edge:
322
-
323
- ```typescript
324
- relation({ name: "shareholders", label: "Shareholders" })
325
- .to("contacts")
326
- .many()
327
- .qualifyWith(
328
- select({ name: "role", label: "Role" }).options([
329
- { label: "Founder", value: "founder", color: "blue" },
330
- { label: "Investor", value: "investor", color: "green" },
331
- ]).required(),
332
- number({ name: "shares", label: "Shares %" }).min(0).max(100),
333
- )
334
- ```
335
-
336
- **Bilateral relations** — automatically sync the reverse side:
337
-
338
- ```typescript
339
- relation({ name: "relationships", label: "Relationships" })
340
- .to("contacts")
341
- .many()
342
- .bilateral({ object: "contacts", attribute: "relationships" })
343
- .qualifyWith(
344
- select({ name: "type", label: "Type" }).options([...]).required()
345
- )
346
- ```
347
-
348
- Multi-relation constraints:
349
-
350
- ```typescript
351
- .maxItems(10) // maximum number of linked records
352
- ```
353
-
354
- ### `formula()` — Computed read-only value
355
-
356
- Formula attributes cannot be required (they are always read-only).
357
-
358
- ```typescript
359
- formula({ name: "total", label: "Total" })
360
- .expression("price * quantity")
361
- .returns("number") // "text" | "number" | "boolean" | "date"
362
- .decimals(2)
363
-
364
- formula({ name: "fullName", label: "Full Name" })
365
- .expression("CONCAT(firstName, ' ', lastName)")
366
- .returns("text")
367
- ```
368
-
369
- ### `rollup()` — Aggregation from related records
370
-
371
- Rollup attributes cannot be required (they are always read-only).
372
-
373
- ```typescript
374
- // Sum a numeric field across related records
375
- rollup({ name: "totalRevenue", label: "Total Revenue" })
376
- .from("orders") // name of the relation attribute on THIS object
377
- .aggregate("amount") // attribute name on the RELATED object
378
- .using("sum")
379
- .decimals(2)
380
-
381
- // Count related records
382
- rollup({ name: "orderCount", label: "Order Count" })
383
- .from("orders")
384
- .aggregate("id")
385
- .using("count")
386
- ```
387
-
388
- Available `.using()` functions:
389
- - Numeric only: `"sum"` | `"avg"`
390
- - Date only: `"earliest"` | `"latest"`
391
- - Universal: `"count"` | `"countValues"` | `"countUniqueValues"` | `"countEmpty"` | `"percentEmpty"` | `"percentNotEmpty"` | `"original"`
392
-
393
- The `"original"` function returns raw values as an array rendered as the target type.
394
- When using `"original"` on a select/status/multiselect, also call `.targetType()` and `.targetOptions()`:
395
-
396
- ```typescript
397
- rollup({ name: "dealStages", label: "Deal Stages" })
398
- .from("deals")
399
- .aggregate("stage")
400
- .using("original")
401
- .targetType("status")
402
- .targetOptions([
403
- { label: "Open", value: "open", color: "blue" },
404
- { label: "Won", value: "won", color: "green" },
405
- { label: "Lost", value: "lost", color: "red" },
406
- ])
407
- ```
408
-
409
- ---
410
-
411
- ## Common Attribute Options (all types)
412
-
413
- ```typescript
414
- .required() // value is mandatory
415
- .optional() // value is optional (default)
416
- .hidden() // hidden from default UI views
417
- .description("Tooltip help text") // shown as tooltip in UI
418
- .icon("star") // IconName from @stndrds/constants
419
- .placeholder("Enter a value...") // input placeholder text
420
- .order(5) // display order within the form
421
- .defaultValue(...) // pre-fill new records
422
- ```
423
-
424
- ---
425
-
426
- ## Promoting a Custom Attribute
427
-
428
- When a user has created a custom attribute and you want to make it system:
429
-
430
- **Before (user-created in DB, not in code):**
431
- ```
432
- drift: deal.urgence (text, label: "Urgence")
433
- ```
434
-
435
- **After (promoted to system in code):**
436
- ```typescript
437
- const DEAL = object({ name: "deals", label: "Deal" })
438
- .labelExpression("{{ name }}")
439
- .attribute(text({ name: "name", label: "Name" }).required())
440
- .attribute(text({ name: "urgence", label: "Urgence" })) // ← add this
441
- .build();
442
- ```
443
-
444
- On next boot, the existing custom attribute is automatically promoted to system.
445
-
446
- ---
447
-
448
- ## Tolerating a Custom Attribute (sealed objects only)
449
-
450
- When you seal an object but want to acknowledge an existing custom attribute:
451
-
452
- ```typescript
453
- const INVOICE = object({ name: "invoice", label: "Invoice" })
454
- .sealed()
455
- .tolerate(["legacy_ref"]) // ← known custom attr, won't fail CI
456
- .labelExpression("{{ ref }}")
457
- .attribute(text({ name: "ref", label: "Reference" }).required())
458
- .build();
459
- ```
460
-
461
- ---
462
-
463
- ## Migrations
464
-
465
- When you rename or change the type of an attribute, declare a migration.
466
- Versions must start at 2 and be sequential (2, 3, 4, ...).
467
-
468
- ```typescript
469
- object({ name: "deals", label: "Deal" })
470
- .migration(2, (m) => m.renameAttribute("old_name", "new_name"))
471
- .migration(3, (m) => m.changeType("score", "text", "number", { transform: (v) => v }))
472
- .migration(4, (m) => m.removeAttribute("deprecated_field"))
473
- .labelExpression("{{ new_name }}")
474
- .attribute(text({ name: "new_name", label: "Name" }).required())
475
- .build();
476
- ```
477
-
478
- Available migration operations:
479
- - `m.renameAttribute(from, to)` — rename an attribute
480
- - `m.changeType(name, fromType, toType, options?)` — change attribute type
481
- - `m.removeAttribute(name)` — permanently remove an attribute
482
- - `m.addAttribute(attribute)` — add a new attribute via migration
483
- - `m.updateConfig(name, config)` — patch attribute config (label, options, etc.)
484
-
485
- ---
486
-
487
- ## Type Inference
488
-
489
- Extract TypeScript types from your schema builders for use in frontend code:
490
-
491
- ```typescript
492
- // ✅ Correct: infer types from the builder (before .build())
493
- const dealBuilder = object({ name: "deals", label: "Deal" })
494
- .labelExpression("{{ name }}")
495
- .attribute(text({ name: "name", label: "Name" }).required())
496
- .attribute(number({ name: "amount", label: "Amount" }));
497
-
498
- type DealRecord = typeof dealBuilder.$infer.record; // full record with id, timestamps
499
- type DealCreate = typeof dealBuilder.$infer.create; // input for creation
500
- type DealUpdate = typeof dealBuilder.$infer.update; // input for partial update
501
-
502
- // Export the compiled definition separately
503
- export const DEAL = dealBuilder.build();
504
- ```
505
-
506
- You can also use `ExtractRecord` from `@stndrds/schema`:
507
-
508
- ```typescript
509
- import { ExtractRecord } from "@stndrds/schema";
510
- type DealRecord = ExtractRecord<typeof dealBuilder>; // same as $infer.record
511
- ```
512
-
513
- Note: `ExtractRecord` works on the **builder** (`typeof dealBuilder`), not the compiled definition (`typeof DEAL`).