@utopia-studio-design/design-system-cli 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/utopia-ds.mjs CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  listComponents, listDocs, listMotionProfiles, listTemplates, listThemes, mcpLaunch, repositoryDoctor, search,
8
8
  } from '../lib/api.mjs'
9
9
  import { createTemplateSubmissionUrl, validateTemplateSubmission } from '../lib/template-submission.mjs'
10
+ import { installCliTelemetry } from '../lib/telemetry.mjs'
10
11
 
11
12
  const args = process.argv.slice(2)
12
13
  const command = args.find((arg) => !arg.startsWith('--')) ?? 'help'
@@ -15,6 +16,14 @@ const optionsWithValues = new Set(['--copy', '--theme'])
15
16
  const values = commandIndex >= 0 ? positionalValues(args.slice(commandIndex + 1)) : []
16
17
  const json = args.includes('--json')
17
18
  const dense = args.includes('--dense')
19
+ const cliVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
20
+ let telemetryErrorCode
21
+
22
+ installCliTelemetry({
23
+ command,
24
+ packageVersion: cliVersion,
25
+ getErrorCode: () => telemetryErrorCode,
26
+ })
18
27
 
19
28
  function positionalValues(input) {
20
29
  const positionals = []
@@ -36,6 +45,7 @@ function output(type, data, format = null) {
36
45
  }
37
46
 
38
47
  function fail(message, code = 'ERR_INPUT', suggestions = []) {
48
+ telemetryErrorCode = code
39
49
  if (json) console.error(JSON.stringify({ apiVersion: 1, error: message, code, suggestions }, null, 2))
40
50
  else console.error(message)
41
51
  process.exitCode = 1
@@ -74,6 +74,7 @@ Arabic display sizing should follow the Latin display scale at about 95%, rather
74
74
  - Runtime recipes use four engine-neutral intents: `feedback`, `page`, `surface`, and `layout`.
75
75
  - Components consume `--motion-duration-*` and `--motion-ease-*` roles rather than hardcoded milliseconds or easing curves.
76
76
  - `MotionProvider` sets the theme profile and runtime adapter for a subtree. Motion-aware components expose `motion?: boolean` for a local override.
77
+ - `MotionProvider asChild` applies that policy to exactly one semantic subtree root without adding a wrapper. The consumer root must accept `className`, `style`, `data-*`, and its children.
77
78
  - `motion={false}` and `prefers-reduced-motion: reduce` disable decorative movement while preserving state changes and accessibility.
78
79
  - Directional motion follows logical inline start/end and mirrors in RTL when direction carries meaning.
79
80
  - Icon motion follows the action: a bell swings from its top, download moves downward, and copy snaps once. Do not apply a generic bounce.
@@ -0,0 +1,165 @@
1
+ # Ceramic Observability
2
+
3
+ Ceramic observability has two explicit modes. It is not a hidden backdoor:
4
+
5
+ - `operational` starts when a host configures and enables a collector. It
6
+ records only minimized error diagnostics, uses an in-memory page-session ID,
7
+ ignores actor/account IDs and custom properties, and does not use telemetry
8
+ cookies, `sessionStorage`, `localStorage`, usage events, or page-view events.
9
+ It needs no consent click, but hosts must publish notice, assess their lawful
10
+ basis, and provide an immediate opt-out.
11
+ - `consented` records the allow-listed usage plan and optional opaque cohorts
12
+ only after explicit consent.
13
+
14
+ ## Decisions this data supports
15
+
16
+ - Which Ceramic versions, components, and public composition paths are in use?
17
+ - Which anonymous app/account cohort is encountering a repeated error?
18
+ - Which route, viewport class, locale, direction, and release are affected?
19
+ - Did a CLI command succeed or fail, without collecting its arguments or path?
20
+ - Did a fixed error fingerprint stop recurring after a release?
21
+
22
+ ## Data minimization contract
23
+
24
+ Consented Ceramic telemetry may collect:
25
+
26
+ - event name and timestamp;
27
+ - opaque app, account, actor, session, and event IDs;
28
+ - component and operation names;
29
+ - package version and application release;
30
+ - route template supplied by the host;
31
+ - locale, direction, and viewport bucket;
32
+ - stable error code, error class, and non-reversible fingerprint;
33
+ - allow-listed short boolean, numeric, or non-email string properties.
34
+
35
+ Ceramic does not collect:
36
+
37
+ - names, email addresses, raw database IDs, or authentication identifiers;
38
+ - DOM, form values, user copy, component props, command arguments, or cwd;
39
+ - error messages or raw stacks;
40
+ - IP addresses in retained records;
41
+ - session replay, screenshots, keystrokes, or network payloads.
42
+
43
+ Operational mode accepts only `component_error` and `runtime_error`, and removes
44
+ `accountId`, `actorId`, custom properties, and persistent telemetry storage even
45
+ if the host passes them.
46
+
47
+ In consented mode, pass pre-hashed or otherwise opaque `accountId` and `actorId` values. The SDK
48
+ rejects identifiers with whitespace or email syntax. Route resolvers should
49
+ return route templates such as `/projects/:projectId`, not raw URLs containing
50
+ customer identifiers.
51
+
52
+ ## Start the private collector and dashboard
53
+
54
+ Create two different random secrets:
55
+
56
+ ```sh
57
+ export CERAMIC_TELEMETRY_INGEST_KEY="$(openssl rand -hex 32)"
58
+ export CERAMIC_TELEMETRY_ADMIN_TOKEN="$(openssl rand -hex 32)"
59
+ export CERAMIC_TELEMETRY_ALLOWED_ORIGINS="http://localhost:5173"
60
+ npm run observability:start
61
+ ```
62
+
63
+ Open `http://127.0.0.1:4318` and enter the admin token. The collector binds to
64
+ loopback by default, retains data for 30 days, and stores newline-delimited JSON
65
+ under `.ceramic/observability/`, which is ignored by Git.
66
+
67
+ The dashboard shows aggregate event, error, app, opaque actor/account,
68
+ component, release, and fingerprint counts plus the latest sanitized events.
69
+ The admin can export the aggregate response or delete all retained events.
70
+
71
+ For a shared deployment, place the collector behind TLS, SSO or a private
72
+ network, persistent encrypted storage, rate limiting, and backups appropriate
73
+ to the retention policy. Do not expose the Node reference collector directly
74
+ to the public internet. The admin token must never be put in a `VITE_*`
75
+ variable or browser bundle.
76
+
77
+ ## Configure a React/browser consumer
78
+
79
+ ```tsx
80
+ import {
81
+ configureCeramicTelemetry,
82
+ installCeramicGlobalErrorTracking,
83
+ trackCeramicEvent,
84
+ } from '@utopia-studio-design/design-system/Telemetry'
85
+
86
+ configureCeramicTelemetry({
87
+ enabled: diagnosticsPreference !== 'disabled',
88
+ consent: 'unknown',
89
+ mode: 'operational',
90
+ appId: 'renacore-dashboard',
91
+ endpoint: 'https://observability.example.com',
92
+ ingestKey: publicWriteOnlyIngestKey,
93
+ packageVersion: '0.6.0',
94
+ release: appRelease,
95
+ routeResolver: () => currentRouteTemplate,
96
+ sampleRate: 0.1,
97
+ })
98
+
99
+ const removeGlobalTracking = installCeramicGlobalErrorTracking()
100
+ ```
101
+
102
+ Global error listeners are never installed automatically by the package. The
103
+ host owns notice, lawful-basis assessment, opt-out, route normalization,
104
+ installation, and cleanup. Errors are sampled at 100%. Repeated reports with the same
105
+ fingerprint inside two seconds are deduplicated so a component boundary and a
106
+ global listener do not create duplicate incidents.
107
+
108
+ Ceramic's validated `asChild` paths report `ERR_COMPOSITION_CHILD` through the
109
+ configured client before throwing their explanatory development error. With no
110
+ configured and enabled client, this call is a no-op.
111
+
112
+ To enable usage events, obtain separate explicit consent and configure
113
+ `mode: "consented"`, `consent: "granted"`, and any opaque cohort IDs.
114
+
115
+ ## Configure CLI diagnostics
116
+
117
+ CLI telemetry is also disabled by default and never sends command arguments,
118
+ queries, target directories, generated file names, or cwd.
119
+
120
+ ```sh
121
+ export CERAMIC_TELEMETRY_ENABLED=true
122
+ export CERAMIC_TELEMETRY_MODE=operational
123
+ export CERAMIC_TELEMETRY_ENDPOINT=https://observability.example.com
124
+ export CERAMIC_TELEMETRY_INGEST_KEY=replace-with-write-only-key
125
+ export CERAMIC_TELEMETRY_APP_ID=renacore-dashboard
126
+ npx utopia-ds doctor --json
127
+ ```
128
+
129
+ Operational CLI mode sends failed completion only, without actor/account IDs.
130
+ Consented mode additionally requires `CERAMIC_TELEMETRY_MODE=consented` and
131
+ `CERAMIC_TELEMETRY_CONSENT=granted`, and may send successful completion and
132
+ opaque cohorts. Network failure never changes CLI output or exit status.
133
+
134
+ ## Event plan
135
+
136
+ | Event | Trigger | Decision fields |
137
+ | --- | --- | --- |
138
+ | `app_started` | Consented SDK initialization | app, release, package version |
139
+ | `component_used` | Explicit host instrumentation | component, operation, route |
140
+ | `component_error` | Ceramic component contract failure | component, operation, error code/fingerprint |
141
+ | `runtime_error` | Explicit boundary/global error capture | route, release, error code/fingerprint |
142
+ | `cli_command_completed` | Operational failure or consented CLI process exit | command, success, version |
143
+
144
+ ## Public notice and controls
145
+
146
+ The Ceramic documentation site publishes `#/terms` and `#/privacy`. Its privacy
147
+ page includes a browser-level operational-diagnostics opt-out and honors Global
148
+ Privacy Control and Do Not Track. Consumer applications must publish their own
149
+ notice and control because they determine the collector, purpose, retention,
150
+ recipients, and jurisdiction.
151
+
152
+ ## Operations and deletion
153
+
154
+ - Default retention: 30 days; configure 1–365 days.
155
+ - `GET /health`: unauthenticated liveness only.
156
+ - `POST /v1/events`: write-only ingest-key authentication.
157
+ - `GET /v1/summary`: admin Bearer authentication.
158
+ - `DELETE /v1/events`: admin Bearer authentication and full retained-data deletion.
159
+ - `DELETE /v1/events?actorId=<opaque-id>` or `?accountId=<opaque-id>`:
160
+ selective deletion for a consent withdrawal or data-subject request.
161
+ - Rotate ingest and admin secrets independently.
162
+ - Treat the ingest key as public/write-only when used in browsers.
163
+ - Treat the admin token as a server-side secret.
164
+ - Publish a privacy notice before enabling identifiable account or actor
165
+ cohorts in a production consumer.
@@ -42,6 +42,30 @@ export function SaveAction() {
42
42
  }
43
43
  ```
44
44
 
45
+ For navigation, keep the anchor or framework link as the single interactive
46
+ element:
47
+
48
+ ```tsx
49
+ import Link from 'next/link'
50
+ import { Button } from '@utopia-studio-design/design-system/Button'
51
+
52
+ <Button asChild variant="outline" size="sm">
53
+ <a href="/docs">Docs</a>
54
+ </Button>
55
+
56
+ <Button asChild startContent={<FolderIcon />}>
57
+ <Link href="/settings">Settings</Link>
58
+ </Button>
59
+ ```
60
+
61
+ `asChild` accepts exactly one non-Fragment React element. The child must forward
62
+ `ref`, `className`, events, `children`, and accessibility props to one
63
+ interactive element. Anchor and Link props such as `href`, `target`, and
64
+ `aria-*` are preserved. When `disabled` or `loading` is true, Ceramic does not
65
+ pass the invalid `disabled` attribute to a link: it applies
66
+ `aria-disabled="true"`, `tabIndex={-1}`, and blocks click activation while
67
+ keeping the original `href`.
68
+
45
69
  ## Set Motion Policy
46
70
 
47
71
  Ceramic maps BeUI-inspired interaction patterns to semantic roles: `press`, `page`, `expand`, `reveal`, and `icon`. Set the application default once and override only when a local workflow needs to be static.
@@ -96,6 +120,14 @@ The MCP server exposes the same API as the CLI. A generated `.mcp.json` uses:
96
120
 
97
121
  Available tools include search, component/template/theme/doc discovery, and doctor. An MCP client must never receive capabilities that the CLI cannot expose.
98
122
 
123
+ ## Optional diagnostics
124
+
125
+ Ceramic telemetry is disabled by default. If a product has an approved
126
+ diagnostics consent flow, configure the write-only SDK and private dashboard
127
+ using `npx utopia-ds docs observability --dense`. Never collect names, emails,
128
+ DOM content, component props, command arguments, filesystem paths, error
129
+ messages, or raw stacks.
130
+
99
131
  ## Agent Decision Loop
100
132
 
101
133
  1. Run `manifest --json` to discover supported operations.
@@ -671,7 +671,7 @@
671
671
  "sourcePath": "packages/design-system/src/components/Button.tsx",
672
672
  "shadcnFoundation": [
673
673
  "button",
674
- "Slot",
674
+ "single-element asChild composition",
675
675
  "cva"
676
676
  ],
677
677
  "fallbackToShadcn": "button",
@@ -709,6 +709,7 @@
709
709
  "New button colors",
710
710
  "Gradient states",
711
711
  "Unlabeled icon-only buttons",
712
+ "Fragments or multiple root children inside Button asChild",
712
713
  "Left/right-only layout rules",
713
714
  "Utopia brand primitives inside reusable component logic",
714
715
  "Motion that bypasses MotionProvider or prefers-reduced-motion"
@@ -718,7 +719,10 @@
718
719
  "motion": "Optional boolean. Defaults to the MotionProvider policy and disables this component pattern when false.",
719
720
  "contentAlign": "Use 'start' for leading-aligned labels, 'center' for the default action layout, or 'between' to pin endContent to the logical inline end.",
720
721
  "startContent": "Optional leading icon or content slot with package-owned alignment.",
721
- "endContent": "Optional trailing content slot. With contentAlign='between', it remains pinned while the label truncates."
722
+ "endContent": "Optional trailing content slot. With contentAlign='between', it remains pinned while the label truncates.",
723
+ "asChild": "Use with exactly one non-Fragment anchor or framework Link that forwards ref, className, events, children, and accessibility props to one interactive element. Button preserves the child href, target, aria attributes, className, and ref while composing its internal slots inside that element.",
724
+ "disabled": "Native buttons receive disabled. With asChild, disabled links keep href, receive aria-disabled=true and tabIndex=-1, and block click activation without receiving a disabled attribute.",
725
+ "loading": "Shows the spinner/loadingText and sets aria-busy. Native buttons are disabled; asChild links use the same aria-disabled, tabIndex, and activation-blocking contract as disabled."
722
726
  }
723
727
  }
724
728
  },
@@ -776,8 +780,9 @@
776
780
  "label",
777
781
  "children",
778
782
  "ButtonGroupSeparator.orientation",
779
- "ButtonGroupText.asChild"
783
+ "ButtonGroupText.asChild"
780
784
  ],
785
+ "composition": "ButtonGroupText.asChild accepts exactly one text-compatible consumer element and preserves it as the semantic root while applying the group text class and props.",
781
786
  "shadcnReferencePatterns": [
782
787
  "button group",
783
788
  "button group with dropdown menu",
@@ -4671,6 +4676,7 @@
4671
4676
  "shadcnFoundation": [
4672
4677
  "toggle",
4673
4678
  "Radix Toggle",
4679
+ "Slottable single-button asChild composition",
4674
4680
  "cva"
4675
4681
  ],
4676
4682
  "fallbackToShadcn": "shadcn/ui toggle",
@@ -4697,6 +4703,7 @@
4697
4703
  "neverInvent": [
4698
4704
  "New pressed colors",
4699
4705
  "Checkbox behavior",
4706
+ "Fragments, multiple roots, anchors, or navigation links inside ToggleButton asChild",
4700
4707
  "Left/right-only state names",
4701
4708
  "Utopia brand primitives inside reusable component logic"
4702
4709
  ],
@@ -4711,9 +4718,11 @@
4711
4718
  "isIconOnly",
4712
4719
  "isDisabled",
4713
4720
  "isLoading",
4721
+ "asChild",
4714
4722
  "variant",
4715
4723
  "size"
4716
4724
  ],
4725
+ "composition": "ToggleButton.asChild requires exactly one non-Fragment button-compatible component that forwards ref, className, events, children, disabled, and accessibility props to one button. Ceramic keeps spinner, icon, pressed icon, and label slots inside that root.",
4717
4726
  "arabicFriendly": [
4718
4727
  "Use logical start/end language for labels and adjacent content.",
4719
4728
  "In RTL, keep persistent pressed state behavior identical; only directional layout and directional icons may mirror.",
@@ -0,0 +1,74 @@
1
+ function safeId(value) {
2
+ return typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(value)
3
+ ? value
4
+ : undefined
5
+ }
6
+
7
+ function randomId() {
8
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`
9
+ }
10
+
11
+ const SAFE_COMMANDS = new Set([
12
+ 'component',
13
+ 'doctor',
14
+ 'docs',
15
+ 'help',
16
+ 'init',
17
+ 'manifest',
18
+ 'motion',
19
+ 'search',
20
+ 'template',
21
+ 'theme',
22
+ ])
23
+
24
+ export function installCliTelemetry({ command, packageVersion, getErrorCode }) {
25
+ const endpoint = process.env.CERAMIC_TELEMETRY_ENDPOINT?.replace(/\/$/, '')
26
+ const ingestKey = process.env.CERAMIC_TELEMETRY_INGEST_KEY
27
+ const appId = safeId(process.env.CERAMIC_TELEMETRY_APP_ID)
28
+ const consented = process.env.CERAMIC_TELEMETRY_MODE === 'consented'
29
+ && process.env.CERAMIC_TELEMETRY_CONSENT === 'granted'
30
+ const enabled = process.env.CERAMIC_TELEMETRY_ENABLED === 'true'
31
+ && endpoint
32
+ && ingestKey
33
+ && appId
34
+ && command !== 'mcp'
35
+ if (!enabled) return
36
+
37
+ let sent = false
38
+ process.on('beforeExit', async () => {
39
+ if (sent) return
40
+ sent = true
41
+ const errorCode = getErrorCode()
42
+ const success = !errorCode && (process.exitCode ?? 0) === 0
43
+ if (!consented && success) return
44
+ const event = {
45
+ schemaVersion: 1,
46
+ id: randomId(),
47
+ occurredAt: new Date().toISOString(),
48
+ name: 'cli_command_completed',
49
+ appId,
50
+ source: 'cli',
51
+ sessionId: randomId(),
52
+ accountId: consented ? safeId(process.env.CERAMIC_TELEMETRY_ACCOUNT_ID) : undefined,
53
+ actorId: consented ? safeId(process.env.CERAMIC_TELEMETRY_ACTOR_ID) : undefined,
54
+ errorCode,
55
+ operation: SAFE_COMMANDS.has(command) ? command : 'unknown',
56
+ packageVersion,
57
+ release: process.env.CERAMIC_RELEASE?.slice(0, 128),
58
+ properties: consented ? { success } : undefined,
59
+ }
60
+ try {
61
+ await fetch(`${endpoint}/v1/events`, {
62
+ method: 'POST',
63
+ headers: {
64
+ 'content-type': 'application/json',
65
+ 'x-ceramic-ingest-key': ingestKey,
66
+ },
67
+ body: JSON.stringify(event),
68
+ signal: AbortSignal.timeout(750),
69
+ })
70
+ } catch {
71
+ // Telemetry is best effort and must never change CLI behavior.
72
+ }
73
+ })
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utopia-studio-design/design-system-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "AI-readable CLI and MCP server for Ceramic Design System",
5
5
  "type": "module",
6
6
  "license": "MIT",