@frontera-sdk/cli 1.50.82 → 1.50.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/api/platform-api.ts +4 -1
- package/src/blueprint/projection.ts +27 -1
- package/src/blueprint-types.ts +70 -7
- package/src/commands/blueprint/declarative.ts +21 -0
- package/src/commands/blueprint/generate-types.ts +6 -0
- package/src/commands/blueprint/get.ts +5 -1
- package/src/commands/blueprint/list.ts +7 -2
- package/src/templates/next-skills.ts +9 -3
- package/src/vendor/sdk-sources.json +18 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.84",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/claude-agent-sdk": "^0.3.251",
|
|
42
|
-
"@frontera-sdk/functions": "1.50.
|
|
43
|
-
"@frontera-sdk/core": "1.50.
|
|
42
|
+
"@frontera-sdk/functions": "1.50.84",
|
|
43
|
+
"@frontera-sdk/core": "1.50.84",
|
|
44
44
|
"ai": "^6.0.116",
|
|
45
45
|
"gray-matter": "^4.0.3",
|
|
46
46
|
"yaml": "^2.9.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@frontera-sdk/forge-contracts": "1.50.
|
|
49
|
+
"@frontera-sdk/forge-contracts": "1.50.84",
|
|
50
50
|
"@types/bun": "^1.3.14",
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
}
|
package/src/api/platform-api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { FronteraClient } from '@frontera-sdk/core/client'
|
|
|
3
3
|
import { organizationHeaders, selectedOrganization } from '../organization'
|
|
4
4
|
|
|
5
5
|
import { CliError } from '../errors'
|
|
6
|
+
import { BLUEPRINT_SCHEMA_CAPABILITIES } from '../blueprint-types'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The service's code for an HTTP status, for responses that carry no `code` of
|
|
@@ -108,7 +109,7 @@ export class PlatformApi {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
blueprintSchema() {
|
|
111
|
-
return this.get<unknown>(
|
|
112
|
+
return this.get<unknown>(`/v1/blueprint/schema?capabilities=${BLUEPRINT_SCHEMA_CAPABILITIES.join(',')}`)
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
blueprintObjectType(apiName: string) {
|
|
@@ -145,6 +146,8 @@ export class PlatformApi {
|
|
|
145
146
|
orderBy?: Array<{ property: string; dir: 'asc' | 'desc' }>
|
|
146
147
|
pageSize?: number
|
|
147
148
|
pageToken?: string
|
|
149
|
+
/** Distance ordering from a location; checked by the service. */
|
|
150
|
+
nearest?: unknown
|
|
148
151
|
}) {
|
|
149
152
|
return this.client.request<{
|
|
150
153
|
rows?: Array<Record<string, unknown>>
|
|
@@ -79,8 +79,11 @@ function propertyToFile(
|
|
|
79
79
|
objectApiName: string,
|
|
80
80
|
sharedApiNameById: Map<string, string>,
|
|
81
81
|
semanticApiNameById: Map<string, string>,
|
|
82
|
+
siblingApiNameById: Map<string, string>,
|
|
82
83
|
): Record<string, unknown> {
|
|
83
84
|
const body = rest(property, new Set(['id', 'sharedPropertyId', 'semanticTypeId']))
|
|
85
|
+
const location = locationToFile(property, objectApiName, siblingApiNameById)
|
|
86
|
+
if (location) body.calculation = location
|
|
84
87
|
const semanticType = resolveName(
|
|
85
88
|
property.semanticTypeId,
|
|
86
89
|
semanticApiNameById,
|
|
@@ -127,6 +130,29 @@ function resolveName(
|
|
|
127
130
|
return apiName
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
/**
|
|
134
|
+
* A location calculation (ADR 0013) names its latitude and longitude fields, as
|
|
135
|
+
* `primaryKey` and `title` do, rather than carrying their ids. Its only reader is
|
|
136
|
+
* the differ, which compares it against the live draft projected the same way, so
|
|
137
|
+
* no inverse is needed. Other calculations pass through unchanged.
|
|
138
|
+
*/
|
|
139
|
+
function locationToFile(
|
|
140
|
+
property: Record<string, unknown>,
|
|
141
|
+
objectApiName: string,
|
|
142
|
+
siblingApiNameById: Map<string, string>,
|
|
143
|
+
): Record<string, unknown> | undefined {
|
|
144
|
+
const calculation = property.calculation as
|
|
145
|
+
| { kind?: unknown; latitudePropertyId?: unknown; longitudePropertyId?: unknown }
|
|
146
|
+
| undefined
|
|
147
|
+
if (calculation?.kind !== 'location') return undefined
|
|
148
|
+
const subject = `Location "${String(property.apiName)}" on ${objectApiName}`
|
|
149
|
+
return {
|
|
150
|
+
kind: 'location',
|
|
151
|
+
latitude: resolveName(calculation.latitudePropertyId, siblingApiNameById, 'latitude field', subject),
|
|
152
|
+
longitude: resolveName(calculation.longitudePropertyId, siblingApiNameById, 'longitude field', subject),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
/**
|
|
131
157
|
* `governance.sourceMappings[0]` → the portable `backing:` a file writes.
|
|
132
158
|
*
|
|
@@ -194,7 +220,7 @@ function objectToFile(
|
|
|
194
220
|
primaryKey: byId.get(object.primaryKeyPropertyId) ?? object.primaryKeyPropertyId,
|
|
195
221
|
title: byId.get(object.titlePropertyId) ?? object.titlePropertyId,
|
|
196
222
|
properties: object.properties.map((property) =>
|
|
197
|
-
propertyToFile(property, object.apiName, sharedApiNameById, semanticApiNameById)),
|
|
223
|
+
propertyToFile(property, object.apiName, sharedApiNameById, semanticApiNameById, byId)),
|
|
198
224
|
}
|
|
199
225
|
}
|
|
200
226
|
|
package/src/blueprint-types.ts
CHANGED
|
@@ -9,8 +9,19 @@ export const DEFAULT_BLUEPRINT_TYPES_OUTPUT = 'src/generated/frontera-blueprint.
|
|
|
9
9
|
/** Mirrors the service's `DataType`. `media_reference` is first-class (spec
|
|
10
10
|
* §4/§12): it generates a named struct rather than `unknown`, and the schema
|
|
11
11
|
* publishes it as neither filterable nor sortable, so the generated
|
|
12
|
-
* `BlueprintRegistry` cannot offer a predicate the query layer refuses.
|
|
13
|
-
|
|
12
|
+
* `BlueprintRegistry` cannot offer a predicate the query layer refuses.
|
|
13
|
+
* `geopoint` is a location (ADR 0013) and generates `BlueprintGeoPoint`. */
|
|
14
|
+
export const BLUEPRINT_DATA_TYPES = [
|
|
15
|
+
'string', 'number', 'boolean', 'date', 'timestamp', 'json', 'media_reference', 'geopoint',
|
|
16
|
+
] as const
|
|
17
|
+
export type BlueprintDataType = (typeof BLUEPRINT_DATA_TYPES)[number]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Value kinds this CLI declares to `GET /v1/blueprint/schema`. The service
|
|
21
|
+
* leaves out fields of any kind a client does not declare, so an older CLI
|
|
22
|
+
* keeps working when a newer kind is published.
|
|
23
|
+
*/
|
|
24
|
+
export const BLUEPRINT_SCHEMA_CAPABILITIES = ['geopoint'] as const
|
|
14
25
|
export type BlueprintPropertyType = 'attribute' | 'measure' | 'time'
|
|
15
26
|
|
|
16
27
|
export interface BlueprintSchemaProperty {
|
|
@@ -37,7 +48,18 @@ export interface BlueprintSchemaResponse {
|
|
|
37
48
|
objectTypes: BlueprintSchemaObject[]
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
/** The schema as this CLI can use it: the fields it left out, as `Type.field`. */
|
|
52
|
+
export interface ParsedBlueprintSchema extends BlueprintSchemaResponse {
|
|
53
|
+
skipped: string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The stderr warning for fields a schema had that this CLI cannot type. */
|
|
57
|
+
export function skippedFieldsWarning(skipped: readonly string[]): string | null {
|
|
58
|
+
if (skipped.length === 0) return null
|
|
59
|
+
return `warning: skipped ${skipped.join(', ')}: their value type is newer than this CLI; upgrade the Frontera CLI to generate types for them`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DATA_TYPES: ReadonlySet<string> = new Set(BLUEPRINT_DATA_TYPES)
|
|
41
63
|
const PROPERTY_TYPES = new Set<BlueprintPropertyType>(['attribute', 'measure', 'time'])
|
|
42
64
|
|
|
43
65
|
function schemaContractError(detail: string): never {
|
|
@@ -47,7 +69,16 @@ function schemaContractError(detail: string): never {
|
|
|
47
69
|
})
|
|
48
70
|
}
|
|
49
71
|
|
|
50
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Validate the schema response.
|
|
74
|
+
*
|
|
75
|
+
* A property whose `dataType` this CLI does not know is left out and named in
|
|
76
|
+
* `skipped`, rather than failing the whole schema: one field of a newer kind
|
|
77
|
+
* anywhere in the organization would otherwise stop type generation for every
|
|
78
|
+
* App. It is still validated in every other respect, and anything malformed
|
|
79
|
+
* still fails. The input is not modified.
|
|
80
|
+
*/
|
|
81
|
+
export function parseBlueprintSchema(input: unknown): ParsedBlueprintSchema {
|
|
51
82
|
if (!input || typeof input !== 'object') return schemaContractError('expected an object')
|
|
52
83
|
const schema = input as Record<string, unknown>
|
|
53
84
|
if (schema.schemaVersion !== 1) {
|
|
@@ -58,7 +89,9 @@ export function parseBlueprintSchema(input: unknown): BlueprintSchemaResponse {
|
|
|
58
89
|
}
|
|
59
90
|
if (!Array.isArray(schema.objectTypes)) return schemaContractError('objectTypes must be an array')
|
|
60
91
|
|
|
92
|
+
const skipped: string[] = []
|
|
61
93
|
const objectNames = new Set<string>()
|
|
94
|
+
const objectTypes: BlueprintSchemaObject[] = []
|
|
62
95
|
for (const [objectIndex, candidate] of schema.objectTypes.entries()) {
|
|
63
96
|
if (!candidate || typeof candidate !== 'object') return schemaContractError(`objectTypes[${objectIndex}] must be an object`)
|
|
64
97
|
const objectType = candidate as Record<string, unknown>
|
|
@@ -75,6 +108,7 @@ export function parseBlueprintSchema(input: unknown): BlueprintSchemaResponse {
|
|
|
75
108
|
if (objectNames.has(objectType.apiName)) return schemaContractError(`duplicate object apiName ${objectType.apiName}`)
|
|
76
109
|
objectNames.add(objectType.apiName)
|
|
77
110
|
const propertyNames = new Set<string>()
|
|
111
|
+
const properties: BlueprintSchemaProperty[] = []
|
|
78
112
|
for (const [propertyIndex, propertyCandidate] of objectType.properties.entries()) {
|
|
79
113
|
if (!propertyCandidate || typeof propertyCandidate !== 'object') {
|
|
80
114
|
return schemaContractError(`objectTypes[${objectIndex}].properties[${propertyIndex}] must be an object`)
|
|
@@ -87,7 +121,7 @@ export function parseBlueprintSchema(input: unknown): BlueprintSchemaResponse {
|
|
|
87
121
|
typeof property.displayName !== 'string'
|
|
88
122
|
|| !(property.description === null || typeof property.description === 'string')
|
|
89
123
|
|| !PROPERTY_TYPES.has(property.propertyType as BlueprintPropertyType)
|
|
90
|
-
||
|
|
124
|
+
|| typeof property.dataType !== 'string'
|
|
91
125
|
|| typeof property.nullable !== 'boolean'
|
|
92
126
|
|| typeof property.filterable !== 'boolean'
|
|
93
127
|
|| typeof property.sortable !== 'boolean'
|
|
@@ -98,9 +132,15 @@ export function parseBlueprintSchema(input: unknown): BlueprintSchemaResponse {
|
|
|
98
132
|
return schemaContractError(`duplicate property apiName ${property.apiName} on ${objectType.apiName}`)
|
|
99
133
|
}
|
|
100
134
|
propertyNames.add(property.apiName)
|
|
135
|
+
if (!DATA_TYPES.has(property.dataType)) {
|
|
136
|
+
skipped.push(`${objectType.apiName}.${property.apiName}`)
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
properties.push(property as unknown as BlueprintSchemaProperty)
|
|
101
140
|
}
|
|
141
|
+
objectTypes.push({ ...(objectType as unknown as BlueprintSchemaObject), properties })
|
|
102
142
|
}
|
|
103
|
-
return
|
|
143
|
+
return { schemaVersion: 1, digest: schema.digest as `sha256:${string}`, objectTypes, skipped }
|
|
104
144
|
}
|
|
105
145
|
|
|
106
146
|
/**
|
|
@@ -126,6 +166,19 @@ const MEDIA_REFERENCE_TYPE_DECLARATION = [
|
|
|
126
166
|
'}',
|
|
127
167
|
]
|
|
128
168
|
|
|
169
|
+
/** A WGS 84 location, always named — never a positional pair (ADR 0013). */
|
|
170
|
+
const GEO_POINT_TYPE_NAME = 'BlueprintGeoPoint'
|
|
171
|
+
|
|
172
|
+
const GEO_POINT_TYPE_DECLARATION = [
|
|
173
|
+
'/** A WGS 84 location in decimal degrees: latitude -90..90, longitude -180..180.',
|
|
174
|
+
' * Null when the object has no valid location. It cannot be sorted or',
|
|
175
|
+
' * compared — ask `!== null` to test presence. */',
|
|
176
|
+
`export interface ${GEO_POINT_TYPE_NAME} {`,
|
|
177
|
+
' lat: number',
|
|
178
|
+
' lon: number',
|
|
179
|
+
'}',
|
|
180
|
+
]
|
|
181
|
+
|
|
129
182
|
const TYPE_BY_DATA_TYPE: Readonly<Record<BlueprintDataType, string>> = {
|
|
130
183
|
string: 'string',
|
|
131
184
|
number: 'number',
|
|
@@ -134,6 +187,7 @@ const TYPE_BY_DATA_TYPE: Readonly<Record<BlueprintDataType, string>> = {
|
|
|
134
187
|
timestamp: 'string',
|
|
135
188
|
json: 'unknown',
|
|
136
189
|
media_reference: MEDIA_REFERENCE_TYPE_NAME,
|
|
190
|
+
geopoint: GEO_POINT_TYPE_NAME,
|
|
137
191
|
}
|
|
138
192
|
|
|
139
193
|
function jsDoc(value: string | null): string[] {
|
|
@@ -232,7 +286,7 @@ export function writeBlueprintTypesFile(
|
|
|
232
286
|
return { path, changed: true }
|
|
233
287
|
}
|
|
234
288
|
|
|
235
|
-
export function generateBlueprintTypes(schema: BlueprintSchemaResponse): string {
|
|
289
|
+
export function generateBlueprintTypes(schema: BlueprintSchemaResponse & { skipped?: readonly string[] }): string {
|
|
236
290
|
if (schema.schemaVersion !== 1) {
|
|
237
291
|
throw new Error(`unsupported Blueprint schema version: ${String(schema.schemaVersion)}`)
|
|
238
292
|
}
|
|
@@ -244,9 +298,13 @@ export function generateBlueprintTypes(schema: BlueprintSchemaResponse): string
|
|
|
244
298
|
properties: [...objectType.properties].sort(compareApiName),
|
|
245
299
|
}))
|
|
246
300
|
|
|
301
|
+
const skipped = schema.skipped ?? []
|
|
247
302
|
const lines = [
|
|
248
303
|
BLUEPRINT_TYPES_MARKER,
|
|
249
304
|
`// Blueprint schema digest: ${schema.digest}`,
|
|
305
|
+
// The digest covers the whole schema, so a field left out is named here:
|
|
306
|
+
// the file must not claim to type what it does not.
|
|
307
|
+
...(skipped.length > 0 ? [`// Not generated (value type newer than this CLI): ${skipped.join(', ')}`] : []),
|
|
250
308
|
'',
|
|
251
309
|
"import type { BlueprintObjectSchema as __FronteraBlueprintObjectSchema } from '@frontera-sdk/blueprint/types'",
|
|
252
310
|
'',
|
|
@@ -260,6 +318,11 @@ export function generateBlueprintTypes(schema: BlueprintSchemaResponse): string
|
|
|
260
318
|
))) {
|
|
261
319
|
lines.push(...MEDIA_REFERENCE_TYPE_DECLARATION, '')
|
|
262
320
|
}
|
|
321
|
+
if (objects.some((objectType) => objectType.properties.some(
|
|
322
|
+
(property) => property.dataType === 'geopoint',
|
|
323
|
+
))) {
|
|
324
|
+
lines.push(...GEO_POINT_TYPE_DECLARATION, '')
|
|
325
|
+
}
|
|
263
326
|
|
|
264
327
|
for (const objectType of objects) {
|
|
265
328
|
lines.push(...jsDoc(objectType.description), `export interface ${objectType.apiName} {`)
|
|
@@ -600,6 +600,7 @@ export function assertApplicable(
|
|
|
600
600
|
// omits, and refuse the run naming a field the plan never called changed.
|
|
601
601
|
const unmappable = Object.keys(file.document).filter((field) =>
|
|
602
602
|
!applicable.has(field) && !satisfies(file.document[field], live[field], field))
|
|
603
|
+
if (kind === 'object-type') unmappable.push(...unmappablePropertyFields(file.document, live))
|
|
603
604
|
if (unmappable.length === 0) return
|
|
604
605
|
throw new CliError(
|
|
605
606
|
`${file.path}: ${unmappable.join(', ')} ${unmappable.length === 1 ? 'differs' : 'differ'} from the draft, `
|
|
@@ -615,6 +616,26 @@ export function assertApplicable(
|
|
|
615
616
|
)
|
|
616
617
|
}
|
|
617
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Property fields the field commands do not carry. A property's `calculation`
|
|
621
|
+
* (a location included) and its own `sensitivity` have no route from a file:
|
|
622
|
+
* applying one would report a classification or a formula that never changed.
|
|
623
|
+
* Named as `properties.<apiName>.<field>`.
|
|
624
|
+
*/
|
|
625
|
+
const UNAPPLICABLE_PROPERTY_FIELDS = ['calculation', 'sensitivity'] as const
|
|
626
|
+
|
|
627
|
+
function unmappablePropertyFields(document: Record<string, unknown>, live: Record<string, unknown>): string[] {
|
|
628
|
+
const declared = Array.isArray(document.properties) ? document.properties as Array<Record<string, unknown>> : []
|
|
629
|
+
const liveProperties = Array.isArray(live.properties) ? live.properties as Array<Record<string, unknown>> : []
|
|
630
|
+
const liveByName = new Map(liveProperties.map((property) => [String(property.apiName), property]))
|
|
631
|
+
return declared.flatMap((property) => {
|
|
632
|
+
const counterpart = liveByName.get(String(property.apiName)) ?? {}
|
|
633
|
+
return UNAPPLICABLE_PROPERTY_FIELDS
|
|
634
|
+
.filter((field) => property[field] !== undefined && !satisfies(property[field], counterpart[field], field))
|
|
635
|
+
.map((field) => `properties.${String(property.apiName)}.${field}`)
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
|
|
618
639
|
/**
|
|
619
640
|
* `primaryKey` and `title` — the two references a file states by property apiName.
|
|
620
641
|
*
|
|
@@ -4,6 +4,7 @@ import { PlatformApi } from '../../api/platform-api'
|
|
|
4
4
|
import {
|
|
5
5
|
generateBlueprintTypes,
|
|
6
6
|
parseBlueprintSchema,
|
|
7
|
+
skippedFieldsWarning,
|
|
7
8
|
writeBlueprintTypesFile,
|
|
8
9
|
} from '../../blueprint-types'
|
|
9
10
|
import type { Command } from '../types'
|
|
@@ -31,6 +32,8 @@ export const blueprintGenerateTypes: Command = {
|
|
|
31
32
|
const project = ctx.project!
|
|
32
33
|
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
33
34
|
const schema = parseBlueprintSchema(await api.blueprintSchema())
|
|
35
|
+
const skippedWarning = skippedFieldsWarning(schema.skipped)
|
|
36
|
+
if (skippedWarning) ctx.output.note(skippedWarning)
|
|
34
37
|
const contents = generateBlueprintTypes(schema)
|
|
35
38
|
const written = writeBlueprintTypesFile(
|
|
36
39
|
project.root,
|
|
@@ -50,6 +53,9 @@ export const blueprintGenerateTypes: Command = {
|
|
|
50
53
|
propertyCount,
|
|
51
54
|
changed: written.changed,
|
|
52
55
|
checked,
|
|
56
|
+
// Fields left out because their value type is newer than this CLI,
|
|
57
|
+
// also named in the generated file's header.
|
|
58
|
+
skipped: schema.skipped,
|
|
53
59
|
},
|
|
54
60
|
text: checked
|
|
55
61
|
? `Blueprint types are current at ${path} (${schema.objectTypes.length} objects, ${propertyCount} properties).`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PlatformApi } from '../../api/platform-api'
|
|
2
|
-
import { parseBlueprintSchema, type BlueprintSchemaObject } from '../../blueprint-types'
|
|
2
|
+
import { parseBlueprintSchema, skippedFieldsWarning, type BlueprintSchemaObject } from '../../blueprint-types'
|
|
3
3
|
import { CliError, UsageError } from '../../errors'
|
|
4
4
|
import type { Command } from '../types'
|
|
5
5
|
|
|
@@ -139,6 +139,8 @@ export const blueprintGet: Command = {
|
|
|
139
139
|
|
|
140
140
|
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
141
141
|
const schema = parseBlueprintSchema(await api.blueprintSchema())
|
|
142
|
+
const skippedWarning = skippedFieldsWarning(schema.skipped)
|
|
143
|
+
if (skippedWarning) ctx.output.note(skippedWarning)
|
|
142
144
|
const apiName = resolveApiName(schema.objectTypes, typed)
|
|
143
145
|
const type = schema.objectTypes.find((objectType) => objectType.apiName === apiName) as BlueprintSchemaObject
|
|
144
146
|
const grantedNames = new Set(schema.objectTypes.map((objectType) => objectType.apiName))
|
|
@@ -261,6 +263,8 @@ export const blueprintGet: Command = {
|
|
|
261
263
|
...(relations.ok ? {} : { relationsUnavailable: relations.unavailable }),
|
|
262
264
|
metrics: own.ok ? own.rows : null,
|
|
263
265
|
...(own.ok ? {} : { metricsUnavailable: own.unavailable }),
|
|
266
|
+
// Fields of this type the CLI left out (a newer value type), as `Type.field`.
|
|
267
|
+
skipped: schema.skipped.filter((name) => name.startsWith(`${apiName}.`)),
|
|
264
268
|
},
|
|
265
269
|
text: lines.join('\n'),
|
|
266
270
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PlatformApi } from '../../api/platform-api'
|
|
2
|
-
import { parseBlueprintSchema } from '../../blueprint-types'
|
|
2
|
+
import { parseBlueprintSchema, skippedFieldsWarning } from '../../blueprint-types'
|
|
3
3
|
import { table } from '../../table'
|
|
4
4
|
import type { Command } from '../types'
|
|
5
5
|
|
|
@@ -32,7 +32,12 @@ export const blueprintList: Command = {
|
|
|
32
32
|
|
|
33
33
|
async run(ctx) {
|
|
34
34
|
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
35
|
-
const
|
|
35
|
+
const schema = parseBlueprintSchema(await api.blueprintSchema())
|
|
36
|
+
// `data` stays the type list its JSON consumers read; a field left out
|
|
37
|
+
// is only a warning here, since a type list shows no fields.
|
|
38
|
+
const skippedWarning = skippedFieldsWarning(schema.skipped)
|
|
39
|
+
if (skippedWarning) ctx.output.note(skippedWarning)
|
|
40
|
+
const types = schema.objectTypes
|
|
36
41
|
|
|
37
42
|
return {
|
|
38
43
|
data: types,
|
|
@@ -323,6 +323,10 @@ const session: Skill = {
|
|
|
323
323
|
' for presentation (chrome density, external links), never for data access.',
|
|
324
324
|
'- Treat `init.token` as opaque and short-lived. Do not cache it, put it in state, log it, send it',
|
|
325
325
|
' anywhere, or retry an authorization failure with a different credential.',
|
|
326
|
+
'- Who opened the App is `useViewer()` — `{ userId, name, email }` or `null`. Use it for "you",',
|
|
327
|
+
' "assigned to me" and owner defaults; never ask a person to type who they are. It is for',
|
|
328
|
+
' display only: never use it to decide what the person may do. It is `null` under a workspace',
|
|
329
|
+
' key, for an App hosted outside Frontera, and on an older host — only then ask.',
|
|
326
330
|
'- Local development is `frontera app dev`. It mints a short-lived App token, keeps the stored CLI',
|
|
327
331
|
' key in the broker, and owns `NEXT_PUBLIC_FRONTERA_DEV_SESSION_ENDPOINT` — never hand-write that',
|
|
328
332
|
' variable. `bun run dev` alone starts Next with no session: fine for layout, useless for data.',
|
|
@@ -330,13 +334,14 @@ const session: Skill = {
|
|
|
330
334
|
'```tsx',
|
|
331
335
|
"'use client'",
|
|
332
336
|
'',
|
|
333
|
-
"import { useFronteraApp } from '@frontera-sdk/core/react'",
|
|
337
|
+
"import { useFronteraApp, useViewer } from '@frontera-sdk/core/react'",
|
|
334
338
|
'',
|
|
335
339
|
'export function AppShell({ children }: { children: React.ReactNode }) {',
|
|
336
340
|
' const { mode, init } = useFronteraApp()',
|
|
341
|
+
' const viewer = useViewer() // { userId, name, email } | null',
|
|
337
342
|
' // mode: "embedded" | "standalone" | "local"',
|
|
338
|
-
' // init: { appId, version, orgId, workspaceId, theme, state, path? }',
|
|
339
|
-
' return <section data-mode={mode}>{children}</section>',
|
|
343
|
+
' // init: { appId, version, orgId, workspaceId, theme, state, path?, viewer? }',
|
|
344
|
+
' return <section data-mode={mode} aria-label={viewer?.name ?? undefined}>{children}</section>',
|
|
340
345
|
'}',
|
|
341
346
|
'```',
|
|
342
347
|
'',
|
|
@@ -353,6 +358,7 @@ const session: Skill = {
|
|
|
353
358
|
'- Writing `NEXT_PUBLIC_FRONTERA_DEV_SESSION_ENDPOINT` by hand into `.env`.',
|
|
354
359
|
'- Branching data access on `mode`.',
|
|
355
360
|
'- Rendering the app before the session resolves, or adding a "not ready" guard inside a feature.',
|
|
361
|
+
'- An "enter your email" field when `useViewer()` already says who the person is.',
|
|
356
362
|
'',
|
|
357
363
|
'## Verification',
|
|
358
364
|
'',
|