@stndrds/cli 1.0.0-alpha.2 → 1.0.0-alpha.201

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
- // src/bin.ts
4
- import chalk3 from "chalk";
3
+ // src/program.ts
4
+ import chalk6 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/client.ts
@@ -13,7 +13,9 @@ var ApiClientError = class extends Error {
13
13
  }
14
14
  };
15
15
  function buildUrl(base, path, params) {
16
- const url = new URL(path, base);
16
+ const normalizedBase = base.endsWith("/") ? base : `${base}/`;
17
+ const normalizedPath = path.replace(/^\/+/, "");
18
+ const url = new URL(normalizedPath, normalizedBase);
17
19
  if (params) {
18
20
  for (const [key, value] of Object.entries(params)) {
19
21
  if (value !== void 0 && value !== null) {
@@ -24,10 +26,11 @@ function buildUrl(base, path, params) {
24
26
  return url.toString();
25
27
  }
26
28
  function createClient(config) {
27
- const { apiUrl, apiKey } = config;
29
+ const { apiUrl, apiKey, tenantId } = config;
28
30
  const headers = {
29
31
  Authorization: `Bearer ${apiKey}`,
30
- "Content-Type": "application/json"
32
+ "Content-Type": "application/json",
33
+ ...tenantId ? { "X-Tenant-ID": tenantId } : {}
31
34
  };
32
35
  async function request(method, path, options) {
33
36
  const url = buildUrl(apiUrl, path, options?.params);
@@ -75,10 +78,48 @@ function createClient(config) {
75
78
  }
76
79
 
77
80
  // src/commands/auth.ts
78
- import chalk2 from "chalk";
81
+ import chalk from "chalk";
82
+
83
+ // src/commands/common.ts
84
+ function getGlobalOptions(cmd) {
85
+ return cmd.optsWithGlobals();
86
+ }
87
+ function getFormat(cmd) {
88
+ return getGlobalOptions(cmd).format;
89
+ }
90
+ function getClientFromCommand(cmd) {
91
+ const root = getGlobalOptions(cmd);
92
+ if (!(root.apiUrl && root.apiKey)) {
93
+ throw new Error('No Standards instance configured. Run "standards login" or pass --api-key.');
94
+ }
95
+ return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey, tenantId: root.tenant });
96
+ }
97
+
98
+ // src/commands/auth.ts
99
+ function registerAuthCommand(program) {
100
+ const auth = program.command("auth").description("Inspect CLI authentication");
101
+ auth.command("whoami").description("Show current configuration and validate API key").action(async (_opts, cmd) => {
102
+ const root = getGlobalOptions(cmd);
103
+ const client = getClientFromCommand(cmd);
104
+ const write = (msg) => process.stdout.write(`${msg}
105
+ `);
106
+ write(chalk.bold("Standards CLI Configuration"));
107
+ write(` API URL: ${root.apiUrl}`);
108
+ write(` API Key: ${root.apiKey ? `${root.apiKey.slice(0, 8)}...` : chalk.red("not set")}`);
109
+ write("");
110
+ const keys = await client.get("/api-keys");
111
+ write(chalk.green("\u2713 API key is valid"));
112
+ if (Array.isArray(keys) && keys.length > 0) {
113
+ const activeKeys = keys.filter(
114
+ (key) => typeof key === "object" && key !== null && !("revokedAt" in key && key.revokedAt)
115
+ );
116
+ write(` Active keys: ${activeKeys.length}`);
117
+ }
118
+ });
119
+ }
79
120
 
80
121
  // src/output.ts
81
- import chalk from "chalk";
122
+ import chalk2 from "chalk";
82
123
  import Table from "cli-table3";
83
124
  var MAX_CELL_WIDTH = 50;
84
125
  function truncate(value, maxLength) {
@@ -107,7 +148,7 @@ function formatTable(items) {
107
148
  if (items.length === 0) return "No results.";
108
149
  const headers = Object.keys(items[0]);
109
150
  const table = new Table({
110
- head: headers.map((h) => chalk.bold(h)),
151
+ head: headers.map((h) => chalk2.bold(h)),
111
152
  style: { head: [], border: [] }
112
153
  });
113
154
  for (const item of items) {
@@ -137,224 +178,596 @@ function formatOutput(data, format) {
137
178
  `);
138
179
  }
139
180
 
140
- // src/commands/auth.ts
141
- function getClientFromCommand(cmd) {
142
- const root = cmd.optsWithGlobals();
143
- return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey });
144
- }
145
- function getFormat(cmd) {
146
- return cmd.optsWithGlobals().format;
147
- }
148
- function registerAuthCommand(program2) {
149
- const auth = program2.command("auth").description("Manage authentication and API keys");
150
- auth.command("whoami").description("Show current configuration and validate API key").action(async (_opts, cmd) => {
151
- const root = cmd.optsWithGlobals();
152
- const client = getClientFromCommand(cmd);
153
- const write = (msg) => process.stdout.write(`${msg}
154
- `);
155
- write(chalk2.bold("Standards CLI Configuration"));
156
- write(` API URL: ${root.apiUrl}`);
157
- write(` API Key: ${root.apiKey ? `${root.apiKey.slice(0, 8)}...` : chalk2.red("not set")}`);
158
- write("");
159
- const keys = await client.get("/api-keys");
160
- write(chalk2.green("\u2713 API key is valid"));
161
- if (Array.isArray(keys) && keys.length > 0) {
162
- write(` Active keys: ${keys.length}`);
163
- }
164
- });
165
- auth.command("create-key").description("Create a new API key").requiredOption("--name <name>", "name for the API key").option("--expires <date>", "expiration date (ISO 8601)").action(async (opts, cmd) => {
166
- const client = getClientFromCommand(cmd);
167
- const body = {
168
- name: opts.name,
169
- permissions: []
170
- };
171
- if (opts.expires) body.expiresAt = opts.expires;
172
- const result = await client.post("/api-keys", body);
173
- formatOutput(result, getFormat(cmd));
174
- });
175
- }
176
-
177
181
  // src/commands/documents.ts
178
- function getClientFromCommand2(cmd) {
179
- const root = cmd.optsWithGlobals();
180
- return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey });
181
- }
182
- function getFormat2(cmd) {
183
- return cmd.optsWithGlobals().format;
184
- }
185
- function registerDocumentsCommand(program2) {
186
- const documents = program2.command("documents").description("Manage documents");
187
- documents.command("list").description("List documents").option("--status <status>", "filter by processing status").option("--limit <n>", "max documents to return").option("--offset <n>", "number of documents to skip").action(async (opts, cmd) => {
188
- const client = getClientFromCommand2(cmd);
182
+ function registerDocumentsCommand(program) {
183
+ const documents = program.command("documents").description("Manage documents");
184
+ documents.command("list").description("List documents").option("--limit <n>", "max documents to return").option("--offset <n>", "number of documents to skip").action(async (opts, cmd) => {
185
+ const client = getClientFromCommand(cmd);
189
186
  const params = {};
190
- if (opts.status) params.processingStatus = opts.status;
191
187
  if (opts.limit) params.limit = opts.limit;
192
188
  if (opts.offset) params.offset = opts.offset;
193
189
  const result = await client.get("/documents", params);
194
- formatOutput(result, getFormat2(cmd));
190
+ formatOutput(result, getFormat(cmd));
195
191
  });
196
192
  documents.command("get").description("Get a document by ID").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
197
- const client = getClientFromCommand2(cmd);
193
+ const client = getClientFromCommand(cmd);
198
194
  const result = await client.get(`/documents/${id}`);
199
- formatOutput(result, getFormat2(cmd));
195
+ formatOutput(result, getFormat(cmd));
200
196
  });
201
197
  documents.command("create").description("Create a new document").requiredOption("--title <title>", "document title").option("--values <json>", "document values as JSON string").action(async (opts, cmd) => {
202
- const client = getClientFromCommand2(cmd);
198
+ const client = getClientFromCommand(cmd);
203
199
  const body = { title: opts.title };
204
200
  if (opts.values) body.values = JSON.parse(opts.values);
205
201
  const result = await client.post("/documents", body);
206
- formatOutput(result, getFormat2(cmd));
202
+ formatOutput(result, getFormat(cmd));
207
203
  });
208
204
  documents.command("update").description("Update a document").argument("<id>", "document ID").option("--title <title>", "new title").option("--values <json>", "new values as JSON string").action(async (id, opts, cmd) => {
209
- const client = getClientFromCommand2(cmd);
205
+ const client = getClientFromCommand(cmd);
210
206
  const body = {};
211
207
  if (opts.title) body.title = opts.title;
212
208
  if (opts.values) body.values = JSON.parse(opts.values);
213
209
  const result = await client.patch(`/documents/${id}`, body);
214
- formatOutput(result, getFormat2(cmd));
210
+ formatOutput(result, getFormat(cmd));
215
211
  });
216
212
  documents.command("delete").description("Delete a document").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
217
- const client = getClientFromCommand2(cmd);
213
+ const client = getClientFromCommand(cmd);
218
214
  await client.delete(`/documents/${id}`);
219
215
  process.stdout.write(`Document ${id} deleted.
220
216
  `);
221
217
  });
222
218
  documents.command("preview").description("Get preview file for a document").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
223
- const client = getClientFromCommand2(cmd);
219
+ const client = getClientFromCommand(cmd);
224
220
  const result = await client.get(`/documents/${id}/preview`);
225
- formatOutput(result, getFormat2(cmd));
221
+ formatOutput(result, getFormat(cmd));
226
222
  });
227
223
  documents.command("slots").description("List slots for a document").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
228
- const client = getClientFromCommand2(cmd);
224
+ const client = getClientFromCommand(cmd);
229
225
  const result = await client.get(`/documents/${id}/slots`);
230
- formatOutput(result, getFormat2(cmd));
226
+ formatOutput(result, getFormat(cmd));
231
227
  });
232
228
  documents.command("add-slot").description("Add a file slot to a document").argument("<id>", "document ID").requiredOption("--slot-name <name>", "slot name").requiredOption("--file-id <fileId>", "file ID").action(async (id, opts, cmd) => {
233
- const client = getClientFromCommand2(cmd);
229
+ const client = getClientFromCommand(cmd);
234
230
  const result = await client.post(`/documents/${id}/slots`, {
235
231
  slotName: opts.slotName,
236
232
  fileId: opts.fileId
237
233
  });
238
- formatOutput(result, getFormat2(cmd));
234
+ formatOutput(result, getFormat(cmd));
239
235
  });
240
236
  documents.command("remove-slot").description("Remove a slot from a document").argument("<id>", "document ID").argument("<slotId>", "slot ID").action(async (id, slotId, _opts, cmd) => {
241
- const client = getClientFromCommand2(cmd);
237
+ const client = getClientFromCommand(cmd);
242
238
  await client.delete(`/documents/${id}/slots/${slotId}`);
243
239
  process.stdout.write(`Slot ${slotId} removed.
244
240
  `);
245
241
  });
246
242
  }
247
243
 
248
- // src/commands/records.ts
249
- function getClientFromCommand3(cmd) {
250
- const root = cmd.optsWithGlobals();
251
- return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey });
252
- }
253
- function getFormat3(cmd) {
254
- return cmd.optsWithGlobals().format;
244
+ // src/commands/keys.ts
245
+ import chalk3 from "chalk";
246
+ function registerKeysCommand(program) {
247
+ const keys = program.command("keys").description("Manage Standards API keys");
248
+ keys.command("list").description("List API keys").action(async (_opts, cmd) => {
249
+ const client = getClientFromCommand(cmd);
250
+ const result = await client.get("/api-keys");
251
+ formatOutput(result, getFormat(cmd));
252
+ });
253
+ keys.command("create").description("Create a new API key").requiredOption("--name <name>", "name for the API key").option("--expires <date>", "expiration date (ISO 8601)").option("--yes", "create without confirmation").action(async (opts, cmd) => {
254
+ if (!opts.yes) {
255
+ throw new Error('Creating API keys is explicit. Re-run with "--yes" to confirm.');
256
+ }
257
+ const client = getClientFromCommand(cmd);
258
+ const body = {
259
+ name: opts.name,
260
+ permissions: []
261
+ };
262
+ if (opts.expires) body.expiresAt = opts.expires;
263
+ const result = await client.post("/api-keys", body);
264
+ formatOutput(result, getFormat(cmd));
265
+ });
266
+ keys.command("revoke").description("Revoke an API key").argument("<id>", "API key ID").option("--yes", "revoke without confirmation").action(async (id, opts, cmd) => {
267
+ if (!opts.yes) {
268
+ throw new Error('Revoking API keys is explicit. Re-run with "--yes" to confirm.');
269
+ }
270
+ const client = getClientFromCommand(cmd);
271
+ await client.delete(`/api-keys/${id}`);
272
+ process.stdout.write(`${chalk3.green("\u2713")} API key ${id} revoked.
273
+ `);
274
+ });
255
275
  }
276
+
277
+ // src/commands/records.ts
256
278
  function parseSortFlag(sort) {
257
279
  const [attribute, direction = "asc"] = sort.split(":");
258
- return JSON.stringify([{ attribute, direction }]);
280
+ return [{ attribute, direction }];
259
281
  }
260
- function registerRecordsCommand(program2) {
261
- const records = program2.command("records").description("Manage records (CRUD + search)");
282
+ function registerRecordsCommand(program) {
283
+ const records = program.command("records").description("Manage records (CRUD + search)");
262
284
  records.command("list").description("List records for an object").argument("<object>", "object name (e.g. contacts)").option("--limit <n>", "max records to return").option("--offset <n>", "number of records to skip").option("--sort <sort>", 'sort rule as "attribute:direction"').option("--filter <json>", "filter state as JSON string").action(async (objectName, opts, cmd) => {
263
- const client = getClientFromCommand3(cmd);
264
- const params = {};
265
- if (opts.limit) params.limit = opts.limit;
266
- if (opts.offset) params.offset = opts.offset;
267
- if (opts.sort) params.sorts = parseSortFlag(opts.sort);
268
- if (opts.filter) params.filters = opts.filter;
269
- const result = await client.get(`/records/${objectName}`, params);
270
- formatOutput(result, getFormat3(cmd));
285
+ const client = getClientFromCommand(cmd);
286
+ const body = {};
287
+ if (opts.limit) body.limit = Number(opts.limit);
288
+ if (opts.offset) body.offset = Number(opts.offset);
289
+ if (opts.sort) body.sorts = parseSortFlag(opts.sort);
290
+ if (opts.filter) body.filters = JSON.parse(opts.filter);
291
+ const result = await client.post(`/records/${objectName}/list`, body);
292
+ formatOutput(result, getFormat(cmd));
271
293
  });
272
294
  records.command("get").description("Get a record by ID").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
273
- const client = getClientFromCommand3(cmd);
295
+ const client = getClientFromCommand(cmd);
274
296
  const result = await client.get(`/records/${objectName}/${id}`);
275
- formatOutput(result, getFormat3(cmd));
297
+ formatOutput(result, getFormat(cmd));
276
298
  });
277
299
  records.command("create").description("Create a new record").argument("<object>", "object name").option("--data <json>", "record data as JSON string").action(async (objectName, opts, cmd) => {
278
300
  const data = opts.data ? JSON.parse(opts.data) : {};
279
- const client = getClientFromCommand3(cmd);
280
- const result = await client.post(`/records/${objectName}`, data);
281
- formatOutput(result, getFormat3(cmd));
301
+ const client = getClientFromCommand(cmd);
302
+ const result = await client.post(`/records/${objectName}`, { data });
303
+ formatOutput(result, getFormat(cmd));
282
304
  });
283
305
  records.command("update").description("Update a record").argument("<object>", "object name").argument("<id>", "record ID").option("--data <json>", "fields to update as JSON string").action(async (objectName, id, opts, cmd) => {
284
306
  const data = opts.data ? JSON.parse(opts.data) : {};
285
- const client = getClientFromCommand3(cmd);
307
+ const client = getClientFromCommand(cmd);
286
308
  const result = await client.put(`/records/${objectName}/${id}`, data);
287
- formatOutput(result, getFormat3(cmd));
309
+ formatOutput(result, getFormat(cmd));
288
310
  });
289
311
  records.command("delete").description("Delete a record").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
290
- const client = getClientFromCommand3(cmd);
312
+ const client = getClientFromCommand(cmd);
291
313
  await client.delete(`/records/${objectName}/${id}`);
292
314
  process.stdout.write(`Record ${id} deleted.
293
315
  `);
294
316
  });
295
317
  records.command("search").description("Full-text search records").argument("<object>", "object name").requiredOption("--query <q>", "search query string").option("--limit <n>", "max records to return").option("--offset <n>", "number of records to skip").option("--sort <sort>", 'sort rule as "attribute:direction"').option("--filter <json>", "filter state as JSON string").action(async (objectName, opts, cmd) => {
296
- const client = getClientFromCommand3(cmd);
297
- const params = { q: opts.query };
298
- if (opts.limit) params.limit = opts.limit;
299
- if (opts.offset) params.offset = opts.offset;
300
- if (opts.sort) params.sorts = parseSortFlag(opts.sort);
301
- if (opts.filter) params.filters = opts.filter;
302
- const result = await client.get(`/records/${objectName}/search`, params);
303
- formatOutput(result, getFormat3(cmd));
318
+ const client = getClientFromCommand(cmd);
319
+ const body = { q: opts.query };
320
+ if (opts.limit) body.limit = Number(opts.limit);
321
+ if (opts.offset) body.offset = Number(opts.offset);
322
+ if (opts.sort) body.sorts = parseSortFlag(opts.sort);
323
+ if (opts.filter) body.filters = JSON.parse(opts.filter);
324
+ const result = await client.post(`/records/${objectName}/search`, body);
325
+ formatOutput(result, getFormat(cmd));
304
326
  });
305
327
  }
306
328
 
307
- // src/commands/schema.ts
308
- function getClientFromCommand4(cmd) {
309
- const root = cmd.optsWithGlobals();
310
- return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey });
329
+ // src/commands/root.ts
330
+ import { stdin as input, stdout as output } from "process";
331
+ import { createInterface } from "readline/promises";
332
+ import chalk4 from "chalk";
333
+
334
+ // src/config.ts
335
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
336
+ import { homedir } from "os";
337
+ import { dirname, join } from "path";
338
+ var DEFAULT_API_URL = "http://localhost:4100/v1";
339
+ var ENV_PROFILE_NAME = "sandbox";
340
+ function getDefaultApiUrl() {
341
+ return DEFAULT_API_URL;
342
+ }
343
+ function getConfigPath() {
344
+ const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
345
+ return join(configDir, "config.json");
346
+ }
347
+ async function readConfig() {
348
+ try {
349
+ const raw = await readFile(getConfigPath(), "utf8");
350
+ const parsed = JSON.parse(raw);
351
+ return {
352
+ currentProfile: parsed.currentProfile,
353
+ profiles: parsed.profiles ?? {}
354
+ };
355
+ } catch (error) {
356
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
357
+ return { profiles: {} };
358
+ }
359
+ throw error;
360
+ }
361
+ }
362
+ async function writeConfig(config) {
363
+ const path = getConfigPath();
364
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
365
+ await writeFile(path, `${JSON.stringify(config, null, 2)}
366
+ `, { mode: 384 });
367
+ }
368
+ async function upsertProfile(input2) {
369
+ const config = await readConfig();
370
+ config.profiles[input2.name] = {
371
+ apiUrl: input2.apiUrl,
372
+ apiKey: input2.apiKey
373
+ };
374
+ config.currentProfile = input2.name;
375
+ await writeConfig(config);
311
376
  }
312
- function getFormat4(cmd) {
313
- return cmd.optsWithGlobals().format;
377
+ async function setCurrentProfile(name) {
378
+ const config = await readConfig();
379
+ if (!config.profiles[name]) {
380
+ throw new Error(`Unknown Standards instance "${name}"`);
381
+ }
382
+ config.currentProfile = name;
383
+ await writeConfig(config);
384
+ }
385
+ async function removeProfile(name) {
386
+ const config = await readConfig();
387
+ const profileName = name ?? config.currentProfile;
388
+ if (!profileName) return;
389
+ delete config.profiles[profileName];
390
+ if (config.currentProfile === profileName) {
391
+ config.currentProfile = void 0;
392
+ }
393
+ await writeConfig(config);
314
394
  }
315
- function registerSchemaCommand(program2) {
316
- const schema = program2.command("schema").description("Inspect schema objects and attributes");
395
+ async function listProfiles() {
396
+ const config = await readConfig();
397
+ const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
398
+ name,
399
+ apiUrl: profile.apiUrl,
400
+ current: config.currentProfile === name
401
+ }));
402
+ const envProfile = getEnvProfile();
403
+ if (envProfile) {
404
+ profiles.push({
405
+ name: ENV_PROFILE_NAME,
406
+ apiUrl: envProfile.apiUrl,
407
+ current: !config.currentProfile,
408
+ source: "env"
409
+ });
410
+ }
411
+ return profiles;
412
+ }
413
+ async function getActiveProfile() {
414
+ const config = await readConfig();
415
+ if (!config.currentProfile) {
416
+ const envProfile = getEnvProfile();
417
+ return envProfile ? { name: ENV_PROFILE_NAME, ...envProfile } : null;
418
+ }
419
+ const profile = config.profiles[config.currentProfile];
420
+ if (!profile) return null;
421
+ return { name: config.currentProfile, ...profile };
422
+ }
423
+ function getEnvProfile() {
424
+ if (!process.env.STANDARDS_API_KEY) return null;
425
+ return {
426
+ apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
427
+ apiKey: process.env.STANDARDS_API_KEY
428
+ };
429
+ }
430
+ async function resolveCliConfig(options) {
431
+ if (options.apiUrl || options.apiKey) {
432
+ return {
433
+ apiUrl: options.apiUrl ?? process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
434
+ apiKey: options.apiKey ?? process.env.STANDARDS_API_KEY
435
+ };
436
+ }
437
+ const config = await readConfig();
438
+ const profileName = options.instance ?? config.currentProfile;
439
+ if (profileName) {
440
+ const profile = config.profiles[profileName];
441
+ if (!profile) throw new Error(`Unknown Standards instance "${profileName}"`);
442
+ return {
443
+ apiUrl: profile.apiUrl,
444
+ apiKey: profile.apiKey,
445
+ profileName
446
+ };
447
+ }
448
+ return {
449
+ apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
450
+ apiKey: process.env.STANDARDS_API_KEY
451
+ };
452
+ }
453
+
454
+ // src/commands/root.ts
455
+ async function promptForMissingValue(label) {
456
+ const rl = createInterface({ input, output });
457
+ try {
458
+ const value = await rl.question(`${label}: `);
459
+ return value.trim();
460
+ } finally {
461
+ rl.close();
462
+ }
463
+ }
464
+ function registerRootCommands(program) {
465
+ program.command("login").description("Connect the CLI to a Standards instance").option("--url <url>", "Standards API URL", getDefaultApiUrl()).option("--key <key>", "API key to store for this instance").option("--name <name>", "local instance name", "local").action(async (opts) => {
466
+ const apiKey = opts.key ?? await promptForMissingValue("API key");
467
+ if (!apiKey) throw new Error("API key is required");
468
+ const client = createClient({ apiUrl: opts.url, apiKey });
469
+ await client.get("/api-keys");
470
+ await upsertProfile({ name: opts.name, apiUrl: opts.url, apiKey });
471
+ process.stdout.write(
472
+ `${chalk4.green("\u2713")} Standards instance "${opts.name}" saved and selected.
473
+ `
474
+ );
475
+ process.stdout.write(` API URL: ${opts.url}
476
+ `);
477
+ process.stdout.write(` API Key: ${apiKey.slice(0, 8)}...
478
+ `);
479
+ process.stdout.write(` Use it with: standards --instance ${opts.name} <command>
480
+ `);
481
+ });
482
+ program.command("use").description("Select the active Standards instance").argument("<name>", "instance name").action(async (name) => {
483
+ await setCurrentProfile(name);
484
+ process.stdout.write(`${chalk4.green("\u2713")} Standards instance "${name}" selected.
485
+ `);
486
+ });
487
+ program.command("instances").description("List configured Standards instances").action(async (_opts, cmd) => {
488
+ const profiles = await listProfiles();
489
+ formatOutput(profiles, getFormat(cmd));
490
+ });
491
+ program.command("current").description("Show the active Standards instance").action(async () => {
492
+ const profile = await getActiveProfile();
493
+ if (!profile) {
494
+ process.stdout.write(`No Standards instance selected. Run "standards login".
495
+ `);
496
+ return;
497
+ }
498
+ process.stdout.write(`${profile.name} ${profile.apiUrl}
499
+ `);
500
+ });
501
+ program.command("logout").description("Remove a Standards instance from local CLI config").argument("[name]", "instance name, defaults to current").action(async (name) => {
502
+ await removeProfile(name);
503
+ process.stdout.write(`${chalk4.green("\u2713")} Standards instance removed.
504
+ `);
505
+ });
506
+ }
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";
514
+ function registerSchemaCommand(program) {
515
+ const schema = program.command("schema").description("Inspect schema objects and attributes");
317
516
  schema.command("list").description("List all schema objects").action(async (_opts, cmd) => {
318
- const client = getClientFromCommand4(cmd);
517
+ const client = getClientFromCommand(cmd);
319
518
  const result = await client.get("/schema/objects");
320
- formatOutput(result, getFormat4(cmd));
519
+ formatOutput(result, getFormat(cmd));
321
520
  });
322
521
  schema.command("inspect").description("Inspect an object (attributes, types, views)").argument("<object>", "object name or ID").action(async (objectName, _opts, cmd) => {
323
- const client = getClientFromCommand4(cmd);
522
+ const client = getClientFromCommand(cmd);
324
523
  const result = await client.get(`/schema/objects/${objectName}`);
325
- formatOutput(result, getFormat4(cmd));
524
+ formatOutput(result, getFormat(cmd));
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
+ }
326
567
  });
327
568
  }
328
-
329
- // src/bin.ts
330
- var program = new Command();
331
- program.name("standards").description("CLI to interact with Standards API").version("1.0.0-alpha.1").option("--format <format>", "output format (json, table, csv)", "json").option(
332
- "--api-url <url>",
333
- "API base URL",
334
- process.env.STANDARDS_API_URL ?? "http://localhost:4100"
335
- ).option("--api-key <key>", "API key for authentication", process.env.STANDARDS_API_KEY);
336
- registerRecordsCommand(program);
337
- registerSchemaCommand(program);
338
- registerDocumentsCommand(program);
339
- registerAuthCommand(program);
340
- program.hook("preAction", (_thisCommand, _actionCommand) => {
341
- const opts = program.opts();
342
- if (!opts.apiKey) {
343
- console.error(
344
- chalk3.red("\u2717 Error: No API key configured. Set STANDARDS_API_KEY or use --api-key")
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
+ )
345
615
  );
346
- process.exit(1);
616
+ } else {
617
+ console.info(chalk5.green("\n\u2713 No unexpected drift."));
347
618
  }
348
- });
349
- program.parseAsync(process.argv).catch((error) => {
350
- if (error instanceof ApiClientError) {
351
- if (error.statusCode > 0) {
352
- console.error(chalk3.red(`\u2717 Error (${error.statusCode}): ${error.message}`));
353
- } else {
354
- console.error(chalk3.red(`\u2717 Error: ${error.message}`));
619
+ }
620
+ function resolveStandardsDir() {
621
+ let dir = process.cwd();
622
+ while (true) {
623
+ if (existsSync(join2(dir, "package.json"))) {
624
+ return join2(dir, ".standards");
355
625
  }
356
- } else if (error instanceof Error) {
357
- console.error(chalk3.red(`\u2717 Error: ${error.message}`));
626
+ const parent = join2(dir, "..");
627
+ if (parent === dir) break;
628
+ dir = parent;
358
629
  }
359
- process.exit(1);
360
- });
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]";
717
+ }
718
+
719
+ // src/program.ts
720
+ var PUBLIC_COMMANDS = /* @__PURE__ */ new Set(["login", "use", "instances", "current", "logout", "help"]);
721
+ function isPublicCommand(actionCommand) {
722
+ if (PUBLIC_COMMANDS.has(actionCommand.name())) return true;
723
+ return actionCommand.parent?.name() === "auth" && actionCommand.name() === "help";
724
+ }
725
+ function createProgram() {
726
+ const program = new Command();
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)");
728
+ registerRootCommands(program);
729
+ registerRecordsCommand(program);
730
+ registerSchemaCommand(program);
731
+ registerDocumentsCommand(program);
732
+ registerKeysCommand(program);
733
+ registerAuthCommand(program);
734
+ program.hook("preAction", async (_thisCommand, actionCommand) => {
735
+ const raw = program.opts();
736
+ const resolved = await resolveCliConfig({
737
+ apiUrl: raw.apiUrl,
738
+ apiKey: raw.apiKey,
739
+ instance: raw.instance
740
+ });
741
+ program.setOptionValue("apiUrl", resolved.apiUrl);
742
+ program.setOptionValue("apiKey", resolved.apiKey);
743
+ program.setOptionValue("profileName", resolved.profileName);
744
+ program.setOptionValue("tenant", raw.tenant);
745
+ if (!(resolved.apiKey || isPublicCommand(actionCommand))) {
746
+ console.error(
747
+ chalk6.red(
748
+ `\u2717 Error: No Standards instance configured. Run "standards login" or pass --api-key.`
749
+ )
750
+ );
751
+ process.exit(1);
752
+ }
753
+ });
754
+ return program;
755
+ }
756
+ async function runProgram(argv = process.argv) {
757
+ const program = createProgram();
758
+ await program.parseAsync(argv).catch((error) => {
759
+ if (error instanceof ApiClientError) {
760
+ if (error.statusCode > 0) {
761
+ console.error(chalk6.red(`\u2717 Error (${error.statusCode}): ${error.message}`));
762
+ } else {
763
+ console.error(chalk6.red(`\u2717 Error: ${error.message}`));
764
+ }
765
+ } else if (error instanceof Error) {
766
+ console.error(chalk6.red(`\u2717 Error: ${error.message}`));
767
+ }
768
+ process.exit(1);
769
+ });
770
+ }
771
+
772
+ // src/bin.ts
773
+ await runProgram();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stndrds/cli",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.201",
4
4
  "description": "CLI tool to interact with Standards API",
5
5
  "type": "module",
6
6
  "bin": {