@stndrds/cli 1.0.0-alpha.192 → 1.0.0-alpha.194

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.
@@ -0,0 +1,513 @@
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`).
package/dist/bin.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import chalk5 from "chalk";
4
+ import chalk6 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/client.ts
@@ -26,10 +26,11 @@ function buildUrl(base, path, params) {
26
26
  return url.toString();
27
27
  }
28
28
  function createClient(config) {
29
- const { apiUrl, apiKey } = config;
29
+ const { apiUrl, apiKey, tenantId } = config;
30
30
  const headers = {
31
31
  Authorization: `Bearer ${apiKey}`,
32
- "Content-Type": "application/json"
32
+ "Content-Type": "application/json",
33
+ ...tenantId ? { "X-Tenant-ID": tenantId } : {}
33
34
  };
34
35
  async function request(method, path, options) {
35
36
  const url = buildUrl(apiUrl, path, options?.params);
@@ -91,7 +92,7 @@ function getClientFromCommand(cmd) {
91
92
  if (!(root.apiUrl && root.apiKey)) {
92
93
  throw new Error('No Standards instance configured. Run "standards login" or pass --api-key.');
93
94
  }
94
- return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey });
95
+ return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey, tenantId: root.tenant });
95
96
  }
96
97
 
97
98
  // src/commands/auth.ts
@@ -505,6 +506,11 @@ function registerRootCommands(program) {
505
506
  }
506
507
 
507
508
  // src/commands/schema.ts
509
+ import { createHash } from "crypto";
510
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
511
+ import { join as join2 } from "path";
512
+ import { fileURLToPath } from "url";
513
+ import chalk5 from "chalk";
508
514
  function registerSchemaCommand(program) {
509
515
  const schema = program.command("schema").description("Inspect schema objects and attributes");
510
516
  schema.command("list").description("List all schema objects").action(async (_opts, cmd) => {
@@ -517,6 +523,197 @@ function registerSchemaCommand(program) {
517
523
  const result = await client.get(`/schema/objects/${objectName}`);
518
524
  formatOutput(result, getFormat(cmd));
519
525
  });
526
+ schema.command("diff").description("Show runtime drift between DB and code schema").option("--object <name>", "Scope to a single object").option("--quiet", "Exit code only, no output").action(async (opts, cmd) => {
527
+ const format = getFormat(cmd);
528
+ const result = await fetchDrift(getClientFromCommand(cmd), opts.object);
529
+ if (format === "json" && cmd.getOptionValueSourceWithGlobals("format") !== "default") {
530
+ formatOutput(result, "json");
531
+ } else if (!opts.quiet) {
532
+ printDiffOutput(result);
533
+ }
534
+ process.exit(result.hasUnexpectedDrift ? 1 : 0);
535
+ });
536
+ schema.command("pull").description("Generate an AI-ready prompt to resolve schema drift").option("--object <name>", "Scope to a single object").option("--no-save", "Print to stdout only, do not write to .standards/").option("--output <file>", "Write prompt to a specific file path").action(async (opts, cmd) => {
537
+ const result = await fetchDrift(getClientFromCommand(cmd), opts.object);
538
+ const totalDrift = result.summary.totalCustomObjects + result.summary.totalCustomAttributes;
539
+ if (totalDrift === 0) {
540
+ console.info(chalk5.green("\u2713 No drift detected \u2014 nothing to pull."));
541
+ process.exit(0);
542
+ }
543
+ const globalOpts = cmd.optsWithGlobals();
544
+ const prompt = buildPullPrompt(result, globalOpts.apiUrl ?? "unknown");
545
+ const shouldSave = opts.save !== false;
546
+ if (opts.output) {
547
+ writeFileSync(opts.output, prompt, "utf-8");
548
+ console.info(chalk5.green(`\u2713 Prompt written to ${opts.output}`));
549
+ return;
550
+ }
551
+ if (shouldSave) {
552
+ const standardsDir = resolveStandardsDir();
553
+ const pullsDir = join2(standardsDir, "pulls");
554
+ const diffDir = join2(standardsDir, "diff");
555
+ mkdirSync(pullsDir, { recursive: true });
556
+ mkdirSync(diffDir, { recursive: true });
557
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
558
+ const hash = createHash("sha1").update(JSON.stringify(result)).digest("hex").slice(0, 6);
559
+ const filename = join2(pullsDir, `${date}-${hash}.md`);
560
+ writeFileSync(filename, prompt, "utf-8");
561
+ writeFileSync(join2(diffDir, "latest.json"), JSON.stringify(result, null, 2), "utf-8");
562
+ console.info(chalk5.green(`\u2713 Pull prompt written to ${filename}`));
563
+ console.info(chalk5.dim(" Copy-paste its contents into your LLM to resolve the drift."));
564
+ } else {
565
+ process.stdout.write(prompt);
566
+ }
567
+ });
568
+ }
569
+ var fetchDrift = (client, object) => client.get("/schema/drift", object ? { object } : {});
570
+ var relationTag = (attr) => attr.isRelation ? " [relation]" : "";
571
+ var attrInline = (attr) => {
572
+ const suffix = attr.tolerated ? ", tolerated" : `${relationTag(attr)}, label: "${attr.label}"`;
573
+ return ` \xB7 ${attr.name} (${attr.type}${suffix})`;
574
+ };
575
+ function printDiffOutput(result) {
576
+ const lines = [];
577
+ for (const obj of result.customObjects) {
578
+ lines.push(chalk5.yellow(`\u26A0 ${obj.name} (${obj.label}) \u2014 custom object, not in code`));
579
+ lines.push(chalk5.dim(' \u2192 Run "standards pull" to promote or leave.'));
580
+ }
581
+ for (const entry of result.systemObjectDrift) {
582
+ const unexpected = entry.sealed ? entry.customAttributes.filter((a) => !a.tolerated) : [];
583
+ const tolerated = entry.customAttributes.filter((a) => a.tolerated);
584
+ if (entry.sealed && unexpected.length > 0) {
585
+ lines.push(
586
+ chalk5.red(
587
+ `\u2717 ${entry.objectName} \u2014 ${unexpected.length} unexpected custom attribute(s) (sealed object)`
588
+ )
589
+ );
590
+ for (const attr of unexpected) lines.push(chalk5.red(attrInline(attr)));
591
+ lines.push(chalk5.dim(' \u2192 Run "standards pull" to promote or tolerate these attributes.'));
592
+ if (tolerated.length > 0) {
593
+ lines.push(chalk5.dim(` Also tolerated: ${tolerated.length} attribute(s)`));
594
+ for (const attr of tolerated) lines.push(chalk5.dim(attrInline(attr)));
595
+ }
596
+ continue;
597
+ }
598
+ const okAttrs = entry.sealed ? tolerated : entry.customAttributes;
599
+ if (okAttrs.length === 0) continue;
600
+ const okSuffix = entry.sealed ? "tolerated custom attribute(s) (sealed)" : "custom attribute(s) (extensible)";
601
+ lines.push(chalk5.green(`\u2713 ${entry.objectName} \u2014 ${okAttrs.length} ${okSuffix}`));
602
+ for (const attr of okAttrs) lines.push(chalk5.dim(attrInline(attr)));
603
+ }
604
+ if (lines.length === 0) {
605
+ console.info(chalk5.green("\u2713 No drift detected."));
606
+ return;
607
+ }
608
+ console.info(lines.join("\n"));
609
+ if (result.hasUnexpectedDrift) {
610
+ console.info(
611
+ chalk5.red(
612
+ `
613
+ \u2717 Unexpected drift: ${result.summary.totalUnexpected} attribute(s) on sealed object(s).`
614
+ )
615
+ );
616
+ } else {
617
+ console.info(chalk5.green("\n\u2713 No unexpected drift."));
618
+ }
619
+ }
620
+ function resolveStandardsDir() {
621
+ let dir = process.cwd();
622
+ while (true) {
623
+ if (existsSync(join2(dir, "package.json"))) {
624
+ return join2(dir, ".standards");
625
+ }
626
+ const parent = join2(dir, "..");
627
+ if (parent === dir) break;
628
+ dir = parent;
629
+ }
630
+ return join2(process.cwd(), ".standards");
631
+ }
632
+ function buildPullPrompt(result, instanceUrl) {
633
+ const now = (/* @__PURE__ */ new Date()).toISOString();
634
+ const builderRef = loadBuilderReference();
635
+ const formatAttr = (attr) => {
636
+ const toleratedNote = attr.tolerated ? " *(already tolerated)*" : "";
637
+ return ` - ${attr.name}${relationTag(attr)}${toleratedNote} \u2014 type: ${attr.type}, label: "${attr.label}"`;
638
+ };
639
+ const lines = [
640
+ "# Standards Schema Pull",
641
+ "generated-with: standards-cli",
642
+ `generated-at: ${now}`,
643
+ `instance: ${instanceUrl}`,
644
+ "",
645
+ "---",
646
+ "",
647
+ "## Standards Schema Builder \u2014 Reference",
648
+ "",
649
+ builderRef,
650
+ "",
651
+ "---",
652
+ "",
653
+ "## Runtime Drift",
654
+ "",
655
+ "The following attributes and objects exist in the database but are not declared",
656
+ "in the code schema. For each one, the user will decide what to do.",
657
+ ""
658
+ ];
659
+ if (result.customObjects.length > 0) {
660
+ lines.push("### Custom Objects (not declared in code)");
661
+ lines.push("");
662
+ for (const obj of result.customObjects) {
663
+ lines.push(`**${obj.name}** \u2014 label: "${obj.label}"`);
664
+ for (const attr of obj.attributes) lines.push(formatAttr(attr));
665
+ lines.push("");
666
+ }
667
+ }
668
+ if (result.systemObjectDrift.length > 0) {
669
+ lines.push("### Custom Attributes on Existing Objects");
670
+ lines.push("");
671
+ for (const entry of result.systemObjectDrift) {
672
+ const sealedNote = entry.sealed ? " **(sealed \u2014 CI fails until resolved)**" : " (extensible)";
673
+ lines.push(`**${entry.objectName}**${sealedNote} \u2014 label: "${entry.objectLabel}"`);
674
+ for (const attr of entry.customAttributes) lines.push(formatAttr(attr));
675
+ lines.push("");
676
+ }
677
+ }
678
+ lines.push("---");
679
+ lines.push("");
680
+ lines.push("## Your Task");
681
+ lines.push("");
682
+ lines.push("1. **Explore the codebase** to find the schema files where each object is declared");
683
+ lines.push(' (look for `object("name")` calls importing from `@stndrds/schema`)');
684
+ lines.push("");
685
+ lines.push("2. **Present each attribute and custom object to the user**, one object at a time.");
686
+ lines.push(" For each, explain what it is and ask the user to choose:");
687
+ lines.push(
688
+ " - **Promote** \u2192 will add `.attribute(...)` to the object builder (becomes system on next boot)"
689
+ );
690
+ lines.push(
691
+ ' - **Tolerate** \u2192 will add to `.tolerate(["name"])` (required on sealed objects to fix CI)'
692
+ );
693
+ lines.push(" - **Leave** \u2192 do nothing (will keep appearing in future `standards diff` runs)");
694
+ lines.push("");
695
+ lines.push("3. **Collect all decisions** before writing any code. Once the user has decided");
696
+ lines.push(" for every attribute, **apply all changes in one pass**.");
697
+ lines.push("");
698
+ lines.push("4. **Verify** by running: `standards diff`");
699
+ lines.push("");
700
+ lines.push("Refer to the Standards Schema Builder Reference above for the exact API.");
701
+ lines.push(
702
+ "For relation attributes, use the `relation()` builder with bilateral config if needed."
703
+ );
704
+ return lines.join("\n");
705
+ }
706
+ function loadBuilderReference() {
707
+ const candidates = [
708
+ join2(fileURLToPath(new URL(".", import.meta.url)), "../assets/schema-builder-reference.md"),
709
+ join2(process.cwd(), "packages/cli/src/assets/schema-builder-reference.md")
710
+ ];
711
+ for (const candidate of candidates) {
712
+ if (existsSync(candidate)) {
713
+ return readFileSync(candidate, "utf-8");
714
+ }
715
+ }
716
+ return "[Standards Schema Builder Reference \u2014 see packages/cli/src/assets/schema-builder-reference.md]";
520
717
  }
521
718
 
522
719
  // src/program.ts
@@ -527,7 +724,7 @@ function isPublicCommand(actionCommand) {
527
724
  }
528
725
  function createProgram() {
529
726
  const program = new Command();
530
- program.name("standards").description("CLI to interact with Standards API").version("1.0.0-alpha.139").option("--format <format>", "output format (json, table, csv)", "json").option("--api-url <url>", "API base URL").option("--api-key <key>", "API key for authentication").option("--instance <name>", "Standards instance profile to use");
727
+ program.name("standards").description("CLI to interact with Standards API").version("1.0.0-alpha.139").option("--format <format>", "output format (json, table, csv)", "json").option("--api-url <url>", "API base URL").option("--api-key <key>", "API key for authentication").option("--instance <name>", "Standards instance profile to use").option("--tenant <id>", "Tenant ID (multi-tenant setups, defaults to primary tenant)");
531
728
  registerRootCommands(program);
532
729
  registerRecordsCommand(program);
533
730
  registerSchemaCommand(program);
@@ -544,9 +741,10 @@ function createProgram() {
544
741
  program.setOptionValue("apiUrl", resolved.apiUrl);
545
742
  program.setOptionValue("apiKey", resolved.apiKey);
546
743
  program.setOptionValue("profileName", resolved.profileName);
744
+ program.setOptionValue("tenant", raw.tenant);
547
745
  if (!(resolved.apiKey || isPublicCommand(actionCommand))) {
548
746
  console.error(
549
- chalk5.red(
747
+ chalk6.red(
550
748
  `\u2717 Error: No Standards instance configured. Run "standards login" or pass --api-key.`
551
749
  )
552
750
  );
@@ -560,12 +758,12 @@ async function runProgram(argv = process.argv) {
560
758
  await program.parseAsync(argv).catch((error) => {
561
759
  if (error instanceof ApiClientError) {
562
760
  if (error.statusCode > 0) {
563
- console.error(chalk5.red(`\u2717 Error (${error.statusCode}): ${error.message}`));
761
+ console.error(chalk6.red(`\u2717 Error (${error.statusCode}): ${error.message}`));
564
762
  } else {
565
- console.error(chalk5.red(`\u2717 Error: ${error.message}`));
763
+ console.error(chalk6.red(`\u2717 Error: ${error.message}`));
566
764
  }
567
765
  } else if (error instanceof Error) {
568
- console.error(chalk5.red(`\u2717 Error: ${error.message}`));
766
+ console.error(chalk6.red(`\u2717 Error: ${error.message}`));
569
767
  }
570
768
  process.exit(1);
571
769
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stndrds/cli",
3
- "version": "1.0.0-alpha.192",
3
+ "version": "1.0.0-alpha.194",
4
4
  "description": "CLI tool to interact with Standards API",
5
5
  "type": "module",
6
6
  "bin": {