@frontera-sdk/cli 1.53.9 → 1.53.11
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 +5 -5
- package/src/api/platform-api.ts +43 -0
- package/src/blueprint/compile.ts +35 -2
- package/src/blueprint/dataset-revision.ts +4 -3
- package/src/blueprint/diff.ts +4 -1
- package/src/commands/blueprint/declarative.ts +106 -44
- package/src/commands/local/steps.ts +4 -4
- package/src/commands/local/sync-request.ts +229 -152
- package/src/commands/local/sync.ts +308 -44
- package/src/commands/registry.ts +3 -0
- package/src/commands/voice/index-commands.ts +363 -0
- package/src/flag-help.ts +19 -0
- package/src/forge/touches.ts +2 -0
- package/src/local/local-blueprint-schemas.ts +6 -2
- package/src/local/local-person-steps.ts +17 -16
- package/src/local/local-portability.ts +5 -3
- package/src/local/local-schemas.ts +41 -0
- package/src/local/local-sync-link.ts +255 -55
- package/src/local/local-sync-plan.ts +1 -1
- package/src/local/local-sync-precheck.ts +16 -2
- package/src/local/local-synthetic-data.ts +12 -7
- package/src/mcp/tools/project-tools.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.53.
|
|
3
|
+
"version": "1.53.11",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/claude-agent-sdk": "^0.3.251",
|
|
42
42
|
"@faker-js/faker": "10.6.0",
|
|
43
|
-
"@frontera-sdk/core": "1.53.
|
|
44
|
-
"@frontera-sdk/functions": "1.53.
|
|
45
|
-
"ai": "^6.0.
|
|
43
|
+
"@frontera-sdk/core": "1.53.11",
|
|
44
|
+
"@frontera-sdk/functions": "1.53.11",
|
|
45
|
+
"ai": "^6.0.319",
|
|
46
46
|
"axe-core": "4.12.1",
|
|
47
47
|
"gray-matter": "^4.0.3",
|
|
48
48
|
"playwright-core": "1.63.0",
|
|
49
49
|
"yaml": "^2.9.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@frontera-sdk/forge-contracts": "1.53.
|
|
52
|
+
"@frontera-sdk/forge-contracts": "1.53.11",
|
|
53
53
|
"@types/bun": "^1.3.14",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
}
|
package/src/api/platform-api.ts
CHANGED
|
@@ -965,6 +965,49 @@ export class PlatformApi {
|
|
|
965
965
|
)
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
+
// ── Voice phone numbers ───────────────────────────────────────────────────
|
|
969
|
+
|
|
970
|
+
/** The numbers an Agent answers, and whether the deployment has a phone system. Served by the Voice application. */
|
|
971
|
+
voiceNumbers(agentId: string) {
|
|
972
|
+
return this.get<{ numbers: unknown[]; available: boolean }>(`/v1/voice-calls/agents/${encodeURIComponent(agentId)}/numbers`)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
addVoiceNumber(agentId: string, body: { number?: string; source: 'whatsapp'; channelId?: string | null; callHours?: unknown }) {
|
|
976
|
+
return this.client.request<{ number: unknown }>(`/v1/voice-calls/agents/${encodeURIComponent(agentId)}/numbers`, { method: 'POST', body })
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
applyVoiceNumber(numberId: string) {
|
|
980
|
+
return this.client.request<{ number: unknown }>(`/v1/voice-calls/numbers/${encodeURIComponent(numberId)}/apply`, { method: 'POST' })
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
setVoiceNumberEnabled(numberId: string, enabled: boolean) {
|
|
984
|
+
return this.client.request<{ number: unknown }>(`/v1/voice-calls/numbers/${encodeURIComponent(numberId)}/enabled`, { method: 'PUT', body: { enabled } })
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Changes a number's call hours (null: any time) and applies them again. */
|
|
988
|
+
setVoiceNumberHours(numberId: string, callHours: unknown) {
|
|
989
|
+
return this.client.request<{ number: unknown }>(`/v1/voice-calls/numbers/${encodeURIComponent(numberId)}/hours`, { method: 'PUT', body: { callHours } })
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** Starts a call from one of the Agent's numbers. A refusal is a 409 the caller reads. */
|
|
993
|
+
startVoiceCall(agentId: string, body: { numberId: string; to: string }) {
|
|
994
|
+
return this.client.request<{ callId: string; status: string }>(`/v1/voice-calls/agents/${encodeURIComponent(agentId)}/calls`, { method: 'POST', body })
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
voiceCallPermission(numberId: string, to: string) {
|
|
998
|
+
return this.get<{ permission: { status: string; expiresAt?: string; canCall: boolean; canRequest: boolean } }>(
|
|
999
|
+
`/v1/voice-calls/numbers/${encodeURIComponent(numberId)}/call-permission?to=${encodeURIComponent(to)}`,
|
|
1000
|
+
)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
requestVoiceCallPermission(numberId: string, body: { to: string; message?: string }) {
|
|
1004
|
+
return this.client.request<{ requested: boolean }>(`/v1/voice-calls/numbers/${encodeURIComponent(numberId)}/call-permission-requests`, { method: 'POST', body })
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
removeVoiceNumber(numberId: string) {
|
|
1008
|
+
return this.client.request<unknown>(`/v1/voice-calls/numbers/${encodeURIComponent(numberId)}`, { method: 'DELETE' })
|
|
1009
|
+
}
|
|
1010
|
+
|
|
968
1011
|
// ── Workspace secrets ─────────────────────────────────────────────────────
|
|
969
1012
|
|
|
970
1013
|
/**
|
package/src/blueprint/compile.ts
CHANGED
|
@@ -72,6 +72,36 @@ export function readBacking(file: AuthoredFile): AuthoredBacking | undefined {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** A file's property that reads no column: only Actions write its values. */
|
|
76
|
+
export function isEditOnly(property: unknown): boolean {
|
|
77
|
+
return (property as { editOnly?: unknown } | null)?.editOnly === true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The file shape of every edit-only property, refused before any write. It reads no
|
|
82
|
+
* column, so `backing.mapping` names none for it; and only an Action can give it a
|
|
83
|
+
* value, so a file that states `backing.editable` lists it there.
|
|
84
|
+
*/
|
|
85
|
+
export function assertEditOnlyShape(file: AuthoredFile): void {
|
|
86
|
+
const backing = readBacking(file)
|
|
87
|
+
const declared = Array.isArray(file.document.properties) ? file.document.properties as Array<Record<string, unknown>> : []
|
|
88
|
+
for (const property of declared.filter(isEditOnly)) {
|
|
89
|
+
const name = String(property.apiName)
|
|
90
|
+
if (backing?.mapping?.[name] !== undefined) {
|
|
91
|
+
throw new CliError(`${file.path}: property "${name}" is edit-only and reads no column, but backing.mapping names one for it.`, {
|
|
92
|
+
code: 'USAGE',
|
|
93
|
+
hint: `Remove "${name}" from backing.mapping, or drop \`editOnly: true\` to read that column.`,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
if (backing?.editable && !backing.editable.includes(name)) {
|
|
97
|
+
throw new CliError(`${file.path}: property "${name}" is edit-only, so only an Action can give it a value, and backing.editable does not list it.`, {
|
|
98
|
+
code: 'USAGE',
|
|
99
|
+
hint: `Add "${name}" to backing.editable.`,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
75
105
|
/**
|
|
76
106
|
* `sharedField: accountCode` → the uuid the wire takes. THREE-VALUED, because the
|
|
77
107
|
* authored key has three states and the wire has the same three:
|
|
@@ -211,10 +241,13 @@ export async function objectTypeCreateBody(
|
|
|
211
241
|
})
|
|
212
242
|
}
|
|
213
243
|
|
|
244
|
+
assertEditOnlyShape(file)
|
|
214
245
|
const { revisionId } = await resolveDataset(backing.dataset)
|
|
215
246
|
// A calculated field (a location from latitude and longitude) reads no column, and the
|
|
216
|
-
// create route has no channel for it: it is added after the type, by `add_field`.
|
|
217
|
-
|
|
247
|
+
// create route has no channel for it: it is added after the type, by `add_field`. So
|
|
248
|
+
// is an edit-only one, which reads no column either.
|
|
249
|
+
const properties = (Array.isArray(document.properties) ? document.properties : [])
|
|
250
|
+
.filter((entry) => !(entry as { calculation?: unknown }).calculation && !isEditOnly(entry))
|
|
218
251
|
|
|
219
252
|
return {
|
|
220
253
|
apiName: requireString(document, 'apiName', file.path),
|
|
@@ -48,15 +48,16 @@ export interface ColumnMapping {
|
|
|
48
48
|
* Shared by `bind` and by `apply`'s rebind, which send the same draft command.
|
|
49
49
|
*/
|
|
50
50
|
export function columnMappingsFrom(
|
|
51
|
-
properties: ReadonlyArray<{ id?: string; apiName?: string; calculation?: unknown }>,
|
|
51
|
+
properties: ReadonlyArray<{ id?: string; apiName?: string; calculation?: unknown; editOnly?: unknown }>,
|
|
52
52
|
mapping: Readonly<Record<string, MappingEntry | null | undefined>>,
|
|
53
53
|
): { columnMappings: ColumnMapping[]; unmapped: string[] } {
|
|
54
54
|
const columnMappings: ColumnMapping[] = []
|
|
55
55
|
const unmapped: string[] = []
|
|
56
56
|
for (const property of properties) {
|
|
57
57
|
const name = property.apiName
|
|
58
|
-
// A calculated field (a location from latitude and longitude) reads no column
|
|
59
|
-
|
|
58
|
+
// A calculated field (a location from latitude and longitude) reads no column, nor
|
|
59
|
+
// does an edit-only one: its values are the ones Actions write.
|
|
60
|
+
if (!name || property.calculation || property.editOnly) continue
|
|
60
61
|
const entry = mapping[name]
|
|
61
62
|
if (entry === undefined || entry === null) {
|
|
62
63
|
unmapped.push(name)
|
package/src/blueprint/diff.ts
CHANGED
|
@@ -282,12 +282,15 @@ function describeUpdate(local: Record<string, unknown>, live: Record<string, unk
|
|
|
282
282
|
const replacements = (intents?.replacements ?? []).filter((entry) => entry.objectApiName === objectApiName)
|
|
283
283
|
const added = after.filter((name) => !before.has(name) && !replacements.some((entry) => entry.added === name))
|
|
284
284
|
const removed = [...before].filter((name) => !after.includes(name) && !replacements.some((entry) => entry.removed === name))
|
|
285
|
+
// An edit-only field is named as one: it reads no column, so its values start empty.
|
|
286
|
+
const editOnly = new Set((Array.isArray(local.properties) ? local.properties as Array<Record<string, unknown>> : [])
|
|
287
|
+
.filter((property) => property.editOnly === true).map((property) => String(property.apiName)))
|
|
285
288
|
const parts: string[] = [
|
|
286
289
|
...(intents?.renames ?? []).filter((entry) => entry.objectApiName === objectApiName)
|
|
287
290
|
.map((entry) => `rename ${entry.from} → ${entry.to} (its values, edits and references stay)`),
|
|
288
291
|
...replacements.map((entry) => `replace ${entry.removed} with ${entry.added} (${entry.removed}'s values are not carried over)`),
|
|
289
292
|
]
|
|
290
|
-
if (added.length) parts.push(`+${added.length} propert${added.length === 1 ? 'y' : 'ies'} (${added.join(', ')})`)
|
|
293
|
+
if (added.length) parts.push(`+${added.length} propert${added.length === 1 ? 'y' : 'ies'} (${added.map((name) => editOnly.has(name) ? `${name}: edit-only, no column` : name).join(', ')})`)
|
|
291
294
|
if (removed.length) parts.push(`-${removed.length} propert${removed.length === 1 ? 'y' : 'ies'} (${removed.join(', ')})`)
|
|
292
295
|
return parts.length ? parts.join(', ') : 'fields changed'
|
|
293
296
|
}
|
|
@@ -3,6 +3,8 @@ import { join, resolve } from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
import { BlueprintAuthoringApi } from '../../api/blueprint-authoring-api'
|
|
5
5
|
import {
|
|
6
|
+
assertEditOnlyShape,
|
|
7
|
+
isEditOnly,
|
|
6
8
|
linkTypeCreateBody,
|
|
7
9
|
linkTypeStructure,
|
|
8
10
|
objectTypeCreateBody,
|
|
@@ -619,7 +621,8 @@ export async function applyOperation(
|
|
|
619
621
|
await objectTypeCreateBody(file, revision, resolveDataset, ids),
|
|
620
622
|
revision,
|
|
621
623
|
)
|
|
622
|
-
|
|
624
|
+
const withCalculated = await applyCalculatedFields(client, operation.apiName, file, nextRevision(created, revision))
|
|
625
|
+
return applyEditOnlyFields(client, operation.apiName, file, withCalculated, ids)
|
|
623
626
|
}
|
|
624
627
|
// Refused BEFORE the first write, not after some of them. Everything this
|
|
625
628
|
// executor can express is enumerated below; a difference outside that set has no
|
|
@@ -780,6 +783,7 @@ export function assertApplicable(
|
|
|
780
783
|
live: Record<string, unknown> | undefined,
|
|
781
784
|
): void {
|
|
782
785
|
if (!live) return
|
|
786
|
+
if (kind === 'object-type') assertEditOnlyKept(file, live)
|
|
783
787
|
const applicable = APPLICABLE_FIELDS[kind]
|
|
784
788
|
// The SAME predicate the plan used to decide this artifact changed. An equality
|
|
785
789
|
// comparison here would disagree with it about key order and about the fields a file
|
|
@@ -915,6 +919,98 @@ async function applyCalculatedFields(client: BlueprintAuthoringApi, apiName: str
|
|
|
915
919
|
return current
|
|
916
920
|
}
|
|
917
921
|
|
|
922
|
+
/**
|
|
923
|
+
* A field is edit-only from the moment it is added, or never: the values Actions
|
|
924
|
+
* wrote and the values a column holds are not the same values, and no command moves
|
|
925
|
+
* one into the other. A file and a draft that disagree about it are refused by name.
|
|
926
|
+
*/
|
|
927
|
+
function assertEditOnlyKept(file: AuthoredFile, live: Record<string, unknown>): void {
|
|
928
|
+
const declared = Array.isArray(file.document.properties) ? file.document.properties as Array<Record<string, unknown>> : []
|
|
929
|
+
const liveByName = new Map((Array.isArray(live.properties) ? live.properties as Array<Record<string, unknown>> : [])
|
|
930
|
+
.map((property) => [String(property.apiName), property]))
|
|
931
|
+
for (const property of declared) {
|
|
932
|
+
const counterpart = liveByName.get(String(property.apiName))
|
|
933
|
+
if (!counterpart || isEditOnly(property) === isEditOnly(counterpart)) continue
|
|
934
|
+
throw new CliError(
|
|
935
|
+
`${file.path}: property "${String(property.apiName)}" ${isEditOnly(property) ? 'is edit-only in the file but reads a column on the draft' : 'is edit-only on the draft but not in the file'}.`,
|
|
936
|
+
{
|
|
937
|
+
code: 'USAGE',
|
|
938
|
+
hint: 'A field is edit-only from when it is added, or never. Add a new field with the other choice, '
|
|
939
|
+
+ 'and remove this one once nothing needs its values.',
|
|
940
|
+
},
|
|
941
|
+
)
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* The `add_field` a file's property is added with: its column, or none for an
|
|
947
|
+
* edit-only one, which the service makes editable in the same command.
|
|
948
|
+
*/
|
|
949
|
+
function addFieldCommand(
|
|
950
|
+
objectId: string,
|
|
951
|
+
file: AuthoredFile,
|
|
952
|
+
property: Record<string, unknown>,
|
|
953
|
+
mapped: string | { column: string; field?: string | null } | undefined,
|
|
954
|
+
ids: AuthoringIds,
|
|
955
|
+
): Record<string, unknown> {
|
|
956
|
+
const name = String(property.apiName)
|
|
957
|
+
const column = mapped === undefined ? '' : typeof mapped === 'string' ? mapped : mapped.column
|
|
958
|
+
const sourceField = mapped === undefined || typeof mapped === 'string' ? undefined : mapped.field
|
|
959
|
+
// Resolved here rather than at plan time: a shared field created earlier in
|
|
960
|
+
// this same run only entered the map when its create answered.
|
|
961
|
+
const sharedPropertyId = resolveSharedProperty(file, name, property.sharedField, ids.sharedPropertyIdByApiName)
|
|
962
|
+
const semanticTypeId = resolveSemanticType(file, `field "${name}"`, property.semanticType, ids.semanticTypeIdByApiName)
|
|
963
|
+
return {
|
|
964
|
+
kind: 'add_field',
|
|
965
|
+
objectId,
|
|
966
|
+
field: {
|
|
967
|
+
// The one identifier the CLI generates, and it never reaches a file: the
|
|
968
|
+
// command addresses a field that does not exist yet, so there is nothing on
|
|
969
|
+
// the draft to resolve a name against. It lives for one request.
|
|
970
|
+
id: crypto.randomUUID(),
|
|
971
|
+
apiName: name,
|
|
972
|
+
displayName: String(property.displayName ?? name),
|
|
973
|
+
propertyType: declaredPropertyType(property, ids.sharedPropertyTypeByApiName),
|
|
974
|
+
valueType: String(property.valueType ?? 'string'),
|
|
975
|
+
sourceColumn: column,
|
|
976
|
+
...(sourceField === undefined || sourceField === null ? {} : { sourceField }),
|
|
977
|
+
...(isEditOnly(property) ? { editOnly: true } : {}),
|
|
978
|
+
required: Boolean(property.required ?? false),
|
|
979
|
+
unique: Boolean(property.unique ?? false),
|
|
980
|
+
status: 'active',
|
|
981
|
+
visibility: 'normal',
|
|
982
|
+
// A field being ADDED has no reference to detach, so an authored `null`
|
|
983
|
+
// is simply "no shared field" — `add_field`'s uuid is not nullable, and a
|
|
984
|
+
// detach command on a field that is being created makes no sense anyway.
|
|
985
|
+
...(typeof sharedPropertyId === 'string' ? { sharedPropertyId } : {}),
|
|
986
|
+
// Same collapse: `add_field`'s uuid is not nullable, and a field being
|
|
987
|
+
// created has no meaning to detach from.
|
|
988
|
+
...(typeof semanticTypeId === 'string' ? { semanticTypeId } : {}),
|
|
989
|
+
// Its own classification, when the file gives one other than the object type's.
|
|
990
|
+
...fieldSensitivityOf(property, objectTypeSensitivity(file.document)),
|
|
991
|
+
},
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* A created type's edit-only fields: the create route takes only fields that read a
|
|
997
|
+
* column, so each is added after it, as a calculated one is.
|
|
998
|
+
*/
|
|
999
|
+
async function applyEditOnlyFields(client: BlueprintAuthoringApi, apiName: string, file: AuthoredFile, revision: number, ids: AuthoringIds): Promise<number> {
|
|
1000
|
+
const declared = Array.isArray(file.document.properties) ? (file.document.properties as Array<Record<string, unknown>>).filter(isEditOnly) : []
|
|
1001
|
+
if (!declared.length) return revision
|
|
1002
|
+
const detail = await client.getObjectTypeDetail(apiName)
|
|
1003
|
+
if (!detail?.id) return revision
|
|
1004
|
+
const present = new Set(detail.properties.map((property) => String(property.apiName)))
|
|
1005
|
+
let current = revision
|
|
1006
|
+
for (const property of declared) {
|
|
1007
|
+
if (present.has(String(property.apiName))) continue
|
|
1008
|
+
current = (await client.draftCommand(addFieldCommand(detail.id, file, property, undefined, ids), current)
|
|
1009
|
+
.catch((err) => { throw stewardRefusal(err, file, String(property.apiName), fieldSensitivityOf(property, objectTypeSensitivity(file.document)).sensitivity !== undefined) })).revision
|
|
1010
|
+
}
|
|
1011
|
+
return current
|
|
1012
|
+
}
|
|
1013
|
+
|
|
918
1014
|
/**
|
|
919
1015
|
* `primaryKey` and `title` — the two references a file states by property apiName.
|
|
920
1016
|
*
|
|
@@ -1204,16 +1300,19 @@ async function applyPropertyPlan(
|
|
|
1204
1300
|
: []
|
|
1205
1301
|
const plan = planProperties(file, liveProperties, backing, idByApiName, objectTypeSensitivity(liveDocument ?? {}))
|
|
1206
1302
|
|
|
1303
|
+
assertEditOnlyShape(file)
|
|
1207
1304
|
// Every add needs a column, and the service offers no name-match fallback. Checked
|
|
1208
|
-
// for ALL of them before the first write, so a partial run cannot start.
|
|
1305
|
+
// for ALL of them before the first write, so a partial run cannot start. An
|
|
1306
|
+
// edit-only field reads none.
|
|
1209
1307
|
for (const property of plan.adds) {
|
|
1210
1308
|
const name = String(property.apiName)
|
|
1211
|
-
if (backing?.mapping?.[name] === undefined) {
|
|
1309
|
+
if (!isEditOnly(property) && backing?.mapping?.[name] === undefined) {
|
|
1212
1310
|
throw new CliError(
|
|
1213
1311
|
`${file.path}: new property "${name}" has no column in \`backing.mapping\`.`,
|
|
1214
1312
|
{
|
|
1215
1313
|
code: 'USAGE',
|
|
1216
|
-
hint: 'A property reads a column, and there is no name-match fallback. Add it to backing.mapping
|
|
1314
|
+
hint: 'A property reads a column, and there is no name-match fallback. Add it to backing.mapping, '
|
|
1315
|
+
+ 'or, for a field only Actions write, add `editOnly: true` to it.',
|
|
1217
1316
|
},
|
|
1218
1317
|
)
|
|
1219
1318
|
}
|
|
@@ -1222,46 +1321,9 @@ async function applyPropertyPlan(
|
|
|
1222
1321
|
let current = revision
|
|
1223
1322
|
for (const property of plan.adds) {
|
|
1224
1323
|
const name = String(property.apiName)
|
|
1225
|
-
const mapped = backing!.mapping[name]!
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
// Resolved here rather than at plan time: a shared field created earlier in
|
|
1229
|
-
// this same run only entered the map when its create answered.
|
|
1230
|
-
const sharedPropertyId = resolveSharedProperty(
|
|
1231
|
-
file, name, property.sharedField, ids.sharedPropertyIdByApiName,
|
|
1232
|
-
)
|
|
1233
|
-
const semanticTypeId = resolveSemanticType(
|
|
1234
|
-
file, `field "${name}"`, property.semanticType, ids.semanticTypeIdByApiName,
|
|
1235
|
-
)
|
|
1236
|
-
const result = await client.draftCommand({
|
|
1237
|
-
kind: 'add_field',
|
|
1238
|
-
objectId: detail.id,
|
|
1239
|
-
field: {
|
|
1240
|
-
// The one identifier the CLI generates, and it never reaches a file: the
|
|
1241
|
-
// command addresses a field that does not exist yet, so there is nothing on
|
|
1242
|
-
// the draft to resolve a name against. It lives for one request.
|
|
1243
|
-
id: crypto.randomUUID(),
|
|
1244
|
-
apiName: name,
|
|
1245
|
-
displayName: String(property.displayName ?? name),
|
|
1246
|
-
propertyType: declaredPropertyType(property, ids.sharedPropertyTypeByApiName),
|
|
1247
|
-
valueType: String(property.valueType ?? 'string'),
|
|
1248
|
-
sourceColumn: column,
|
|
1249
|
-
...(sourceField === undefined || sourceField === null ? {} : { sourceField }),
|
|
1250
|
-
required: Boolean(property.required ?? false),
|
|
1251
|
-
unique: Boolean(property.unique ?? false),
|
|
1252
|
-
status: 'active',
|
|
1253
|
-
visibility: 'normal',
|
|
1254
|
-
// A field being ADDED has no reference to detach, so an authored `null`
|
|
1255
|
-
// is simply "no shared field" — `add_field`'s uuid is not nullable, and a
|
|
1256
|
-
// detach command on a field that is being created makes no sense anyway.
|
|
1257
|
-
...(typeof sharedPropertyId === 'string' ? { sharedPropertyId } : {}),
|
|
1258
|
-
// Same collapse: `add_field`'s uuid is not nullable, and a field being
|
|
1259
|
-
// created has no meaning to detach from.
|
|
1260
|
-
...(typeof semanticTypeId === 'string' ? { semanticTypeId } : {}),
|
|
1261
|
-
// Its own classification, when the file gives one other than the object type's.
|
|
1262
|
-
...fieldSensitivityOf(property, objectTypeSensitivity(file.document)),
|
|
1263
|
-
},
|
|
1264
|
-
}, current).catch((err) => { throw stewardRefusal(err, file, name, fieldSensitivityOf(property, objectTypeSensitivity(file.document)).sensitivity !== undefined) })
|
|
1324
|
+
const mapped = isEditOnly(property) ? undefined : backing!.mapping[name]!
|
|
1325
|
+
const result = await client.draftCommand(addFieldCommand(detail.id, file, property, mapped, ids), current)
|
|
1326
|
+
.catch((err) => { throw stewardRefusal(err, file, name, fieldSensitivityOf(property, objectTypeSensitivity(file.document)).sensitivity !== undefined) })
|
|
1265
1327
|
current = result.revision
|
|
1266
1328
|
}
|
|
1267
1329
|
for (const change of plan.schemaChanges) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UsageError } from '../../errors'
|
|
2
2
|
import { FRONTERA_READ_ONLY_COMMANDS } from '../../forge/permission-rules'
|
|
3
|
-
import { addPersonStep, clearSettledSteps, CREDENTIAL_WORDS, CREDENTIALS,
|
|
3
|
+
import { addPersonStep, clearSettledSteps, CREDENTIAL_WORDS, CREDENTIALS, deliveryGroups, readPersonSteps, runStepChecks, settlePersonStep, staleProblem, stepTargets, type PersonStep, type StepTarget } from '../../local/local-person-steps'
|
|
4
4
|
import { runSelf } from '../../mcp/mcp-cli'
|
|
5
5
|
import { localProject } from '../../local/local-project'
|
|
6
6
|
import { flagBool, flagString, type Command } from '../types'
|
|
@@ -15,9 +15,9 @@ import { flagBool, flagString, type Command } from '../types'
|
|
|
15
15
|
|
|
16
16
|
const ACTIONS = ['list', 'add', 'done', 'dismiss', 'reopen', 'check', 'clear'] as const
|
|
17
17
|
|
|
18
|
-
function stepLines(steps: PersonStep[],
|
|
18
|
+
function stepLines(steps: PersonStep[], targets: StepTarget[]): string[] {
|
|
19
19
|
return steps.flatMap((s, n) => {
|
|
20
|
-
const stale = staleProblem(s,
|
|
20
|
+
const stale = staleProblem(s, targets)
|
|
21
21
|
return [
|
|
22
22
|
` ${n + 1}. ${s.command ?? `${s.changes} (in the product)`}${s.check ? ' · checks itself' : ''}`,
|
|
23
23
|
` ${s.why} Changes: ${s.changes} Needs ${CREDENTIAL_WORDS[s.credential] ?? s.credential}. (${s.id})`,
|
|
@@ -101,7 +101,7 @@ export const localSteps: Command = {
|
|
|
101
101
|
const groups = deliveryGroups(steps)
|
|
102
102
|
const settled = steps.filter((s) => s.status !== 'pending')
|
|
103
103
|
const lines = groups.length
|
|
104
|
-
? groups.flatMap((g) => [g.delivery ? `${g.delivery}:` : 'Other steps:', ...stepLines(g.steps,
|
|
104
|
+
? groups.flatMap((g) => [g.delivery ? `${g.delivery}:` : 'Other steps:', ...stepLines(g.steps, stepTargets(project.root)), ''])
|
|
105
105
|
: ['No steps wait on the person.', '']
|
|
106
106
|
if (flagBool(ctx, 'all') && settled.length) lines.push('Settled:', ...settled.map((s) => ` ${s.status === 'done' ? '✓' : '–'} ${s.command ?? s.changes} (${s.id})`))
|
|
107
107
|
return { data: { pending: groups, ...(flagBool(ctx, 'all') ? { settled } : {}) }, text: lines.join('\n').trimEnd() }
|