@frontera-sdk/cli 1.53.8 → 1.53.10
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/governed-action-api.ts +9 -3
- package/src/blueprint/action-rules.ts +175 -9
- package/src/blueprint/diff.ts +27 -8
- package/src/blueprint/ontology-edit-plan.ts +155 -4
- package/src/blueprint/org-name-check.ts +10 -8
- package/src/blueprint/projection.ts +13 -3
- package/src/blueprint/property-intents.ts +248 -0
- package/src/blueprint/publish-plan.ts +40 -4
- package/src/blueprint/render.ts +4 -0
- package/src/blueprint/tree.ts +22 -0
- package/src/commands/action/deploy.ts +31 -24
- package/src/commands/action/retire.ts +9 -1
- package/src/commands/action/submit.ts +21 -5
- package/src/commands/auth/login.ts +2 -2
- package/src/commands/blueprint/authoring.ts +22 -4
- package/src/commands/blueprint/declarative.ts +140 -32
- package/src/commands/knowledge/index-commands.ts +1 -1
- package/src/commands/local/check.ts +6 -4
- package/src/commands/local/index-commands.ts +8 -2
- package/src/commands/local/sync-request.ts +252 -66
- package/src/commands/local/sync.ts +29 -24
- package/src/device-login.ts +6 -1
- package/src/local/local-actions.ts +13 -1
- package/src/local/local-applied.ts +1 -1
- package/src/local/local-blueprint-schemas.ts +40 -6
- package/src/local/local-dev-references.ts +4 -3
- package/src/local/local-jobs.ts +28 -0
- package/src/local/local-portability.ts +1 -1
- package/src/local/local-sync-link.ts +62 -4
- package/src/local/local-sync-merge.ts +37 -1
- package/src/local/local-sync-plan.ts +44 -6
- package/src/local/local-sync-precheck.ts +2 -1
- package/src/local/local-sync-push.ts +22 -11
- package/src/local/local-sync-remote.ts +13 -8
- package/src/local/local-synthetic-data.ts +2 -2
- package/src/main.ts +7 -2
- package/src/vendor/sdk-sources.json +2 -2
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
migrationInstructions,
|
|
10
10
|
newActionSteps,
|
|
11
11
|
changedActionSteps,
|
|
12
|
+
needsActionSubjects,
|
|
12
13
|
publishPlan,
|
|
13
14
|
} from '../../blueprint/publish-plan'
|
|
14
15
|
import { signInStep } from '../../device-login'
|
|
@@ -375,7 +376,11 @@ export const blueprintPublish: Command = {
|
|
|
375
376
|
report = await client.validate()
|
|
376
377
|
}
|
|
377
378
|
const signIn = await signInStep({ profile: ctx.profile, apiUrl: ctx.apiUrl })
|
|
378
|
-
|
|
379
|
+
// An Action switched off by a change to its object type, not to itself, is found by its type.
|
|
380
|
+
const subjects = async (latest: ValidationReport) => (needsActionSubjects(latest)
|
|
381
|
+
? new Map(((await client.draft()).definition.actions ?? []).map((action) => [action.id, String((action.subject as { objectTypeId?: unknown } | undefined)?.objectTypeId ?? '')]))
|
|
382
|
+
: undefined)
|
|
383
|
+
let plan = publishPlan(report, signIn, profile, await subjects(report))
|
|
379
384
|
|
|
380
385
|
// What `action prepare` records, for each Action that has no decision yet.
|
|
381
386
|
const prepared: string[] = []
|
|
@@ -385,7 +390,8 @@ export const blueprintPublish: Command = {
|
|
|
385
390
|
await actions.recordDeploymentDecision(name, `Published with the Frontera CLI ("${label}"): ships switched off until a person deploys it and turns it on.`)
|
|
386
391
|
prepared.push(name)
|
|
387
392
|
}
|
|
388
|
-
|
|
393
|
+
const revalidated = await client.validate()
|
|
394
|
+
plan = publishPlan(revalidated, signIn, profile, await subjects(revalidated))
|
|
389
395
|
}
|
|
390
396
|
const preparedLines = prepared.map((name) => `Recorded that Action ${name} ships switched off: a person deploys it and turns it on after publish.`)
|
|
391
397
|
|
|
@@ -437,7 +443,12 @@ export const blueprintPublish: Command = {
|
|
|
437
443
|
return { data: { release: released, prepared, newActions: [] }, text: `Release "${activeReleaseLabel}" is already active; nothing was published.` }
|
|
438
444
|
}
|
|
439
445
|
const newActions = plan.newActions.map((action) => ({ ...action, nextSteps: newActionSteps(action, signIn, profile) }))
|
|
440
|
-
|
|
446
|
+
// An Action listed for its type's change is one this publish recorded switched off; one a person had switched off stays as they left it.
|
|
447
|
+
const listed = (action: { apiName: string; objectType?: string }) => !action.objectType || prepared.includes(action.apiName)
|
|
448
|
+
const changedActions = plan.changedSwitchedOff
|
|
449
|
+
.filter(listed)
|
|
450
|
+
.map((action) => ({ apiName: action.apiName, ...(action.objectType ? { objectType: action.objectType } : {}), nextSteps: changedActionSteps(action, signIn, profile) }))
|
|
451
|
+
const leftSwitchedOff = plan.changedSwitchedOff.filter((action) => !listed(action))
|
|
441
452
|
const numbered = (steps: Array<{ step: string; command?: string }>) => steps.flatMap((step, index) => (step.command ? [` ${index + 1}. ${step.step}:`, ` ${step.command}`] : [` ${index + 1}. ${step.step}.`]))
|
|
442
453
|
return {
|
|
443
454
|
data: { release: released, prepared, newActions, changedActions },
|
|
@@ -451,9 +462,16 @@ export const blueprintPublish: Command = {
|
|
|
451
462
|
]),
|
|
452
463
|
...changedActions.flatMap((action) => [
|
|
453
464
|
'',
|
|
454
|
-
|
|
465
|
+
action.objectType
|
|
466
|
+
? `Action ${action.apiName} acts on ${action.objectType}, which this release changes, and is published switched off; its old write path does not run it. To turn it on again:`
|
|
467
|
+
: `Action ${action.apiName} changed and is published switched off; its old write path does not run the new version. To turn it on again:`,
|
|
455
468
|
...numbered(action.nextSteps),
|
|
456
469
|
]),
|
|
470
|
+
...leftSwitchedOff.flatMap((action) => [
|
|
471
|
+
'',
|
|
472
|
+
`Action ${action.apiName} acts on ${action.objectType}, which this release changes, and was switched off before this publish, so it stays switched off. `
|
|
473
|
+
+ 'To run it on this release, `frontera action list` shows what it needs.',
|
|
474
|
+
]),
|
|
457
475
|
].join('\n'),
|
|
458
476
|
}
|
|
459
477
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { BlueprintAuthoringApi } from '../../api/blueprint-authoring-api'
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type DefinitionBundle,
|
|
49
49
|
} from '../../blueprint/model'
|
|
50
50
|
import { toFiles } from '../../blueprint/projection'
|
|
51
|
+
import { namesOf, objectSetNamesOf, readPropertyIntents } from '../../blueprint/property-intents'
|
|
51
52
|
import { sourceSystemHint } from '../../local/local-blueprint-errors'
|
|
52
53
|
import { inheritedFromPull, unchangedSincePull } from '../../local/local-check-inherited'
|
|
53
54
|
import { localProject } from '../../local/local-project'
|
|
@@ -63,7 +64,7 @@ import {
|
|
|
63
64
|
type ScaffoldOptions,
|
|
64
65
|
type SemanticTypeTemplate,
|
|
65
66
|
} from '../../blueprint/scaffold'
|
|
66
|
-
import { pathFor, readTree, removeFromTree, writeTree } from '../../blueprint/tree'
|
|
67
|
+
import { pathFor, readObjectSets, readTree, removeFromTree, serialize, writeTree } from '../../blueprint/tree'
|
|
67
68
|
import { CliError } from '../../errors'
|
|
68
69
|
import type { Command, CommandContext } from '../types'
|
|
69
70
|
|
|
@@ -361,9 +362,9 @@ async function computePlan(ctx: CommandContext): Promise<{
|
|
|
361
362
|
hint: 'frontera blueprint pull, or frontera blueprint new object-type <ApiName>',
|
|
362
363
|
})
|
|
363
364
|
}
|
|
364
|
-
const
|
|
365
|
+
const authored = readTree(directory)
|
|
365
366
|
// A measure's filters the draft would refuse: said by `plan`, and before `apply` writes anything.
|
|
366
|
-
const metricProblems = metricRuleProblems(
|
|
367
|
+
const metricProblems = metricRuleProblems(authored)
|
|
367
368
|
if (metricProblems.length) {
|
|
368
369
|
throw new CliError(`${metricProblems.length} metric problem${metricProblems.length === 1 ? '' : 's'} the draft would refuse, so nothing was planned or applied:\n${metricProblems.map((p) => ` ✕ ${p}`).join('\n')}`, {
|
|
369
370
|
code: 'VALIDATION_ERROR',
|
|
@@ -372,8 +373,11 @@ async function computePlan(ctx: CommandContext): Promise<{
|
|
|
372
373
|
})
|
|
373
374
|
}
|
|
374
375
|
const { definition, revision } = await client.draft()
|
|
376
|
+
// Compared against the draft as it reads once the files' renames are made, so a
|
|
377
|
+
// renamed property, and every file naming it, plans as the same property.
|
|
378
|
+
const { files, live, ...intents } = readPropertyIntents(authored, definition, readObjectSets(directory))
|
|
375
379
|
const datasets = await readDatasetIndex(client, files, definition)
|
|
376
|
-
const plan = withheldPrunes(diff(files,
|
|
380
|
+
const plan = withheldPrunes(diff(files, live, revision, datasets, intents), directory)
|
|
377
381
|
|
|
378
382
|
// A type moving to another Dataset is no longer held to the old one's contract.
|
|
379
383
|
const rebound = new Set(plan.rebind.map((change) => change.objectApiName))
|
|
@@ -635,6 +639,7 @@ export async function applyOperation(
|
|
|
635
639
|
)
|
|
636
640
|
current = nextRevision(updated, revision)
|
|
637
641
|
}
|
|
642
|
+
current = await applyPropertyRenames(client, operation.apiName, operation.renames ?? [], current)
|
|
638
643
|
current = await applyObjectKeys(client, operation.apiName, file, operation.liveDocument, current)
|
|
639
644
|
current = await applyPropertyPlan(
|
|
640
645
|
client, operation.apiName, file, operation.liveDocument, current, ids,
|
|
@@ -1012,10 +1017,8 @@ export function objectTypeMetadataChanges(
|
|
|
1012
1017
|
* planning tool that says it did something it did not is the one failure worth
|
|
1013
1018
|
* refusing outright, so anything this cannot map now aborts BEFORE the first write.
|
|
1014
1019
|
*
|
|
1015
|
-
* Mapped: add, remove, and a change to any field the file can state.
|
|
1016
|
-
*
|
|
1017
|
-
* reason an object-type rename is (`rename` exists for that, and there is no
|
|
1018
|
-
* per-property equivalent).
|
|
1020
|
+
* Mapped: add, remove, and a change to any field the file can state. A rename is
|
|
1021
|
+
* made before this runs (`applyPropertyRenames`), so here it is the same property.
|
|
1019
1022
|
*/
|
|
1020
1023
|
interface PropertyPlan {
|
|
1021
1024
|
adds: Array<Record<string, unknown>>
|
|
@@ -1145,6 +1148,34 @@ function planProperties(
|
|
|
1145
1148
|
return plan
|
|
1146
1149
|
}
|
|
1147
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* A property that keeps its id and takes the name the file gives it (`renamedFrom`).
|
|
1153
|
+
* Before the keys and the field commands, which name properties as the file does.
|
|
1154
|
+
*/
|
|
1155
|
+
async function applyPropertyRenames(
|
|
1156
|
+
client: BlueprintAuthoringApi,
|
|
1157
|
+
apiName: string,
|
|
1158
|
+
renames: ReadonlyArray<{ from: string; to: string }>,
|
|
1159
|
+
revision: number,
|
|
1160
|
+
): Promise<number> {
|
|
1161
|
+
if (renames.length === 0) return revision
|
|
1162
|
+
const detail = await client.getObjectTypeDetail(apiName)
|
|
1163
|
+
let current = revision
|
|
1164
|
+
for (const { from, to } of renames) {
|
|
1165
|
+
const fieldId = detail?.properties.find((property) => property.apiName === from)?.id
|
|
1166
|
+
if (!detail?.id || !fieldId) {
|
|
1167
|
+
throw new CliError(`"${apiName}.${from}" is no longer on the draft.`, {
|
|
1168
|
+
code: 'CONFLICT',
|
|
1169
|
+
hint: 'frontera blueprint plan — the draft moved under this run.',
|
|
1170
|
+
})
|
|
1171
|
+
}
|
|
1172
|
+
current = (await client.draftCommand({
|
|
1173
|
+
kind: 'update_field_schema', objectId: detail.id, fieldId, patch: { apiName: to },
|
|
1174
|
+
}, current)).revision
|
|
1175
|
+
}
|
|
1176
|
+
return current
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1148
1179
|
async function applyPropertyPlan(
|
|
1149
1180
|
client: BlueprintAuthoringApi,
|
|
1150
1181
|
apiName: string,
|
|
@@ -1188,24 +1219,6 @@ async function applyPropertyPlan(
|
|
|
1188
1219
|
}
|
|
1189
1220
|
}
|
|
1190
1221
|
|
|
1191
|
-
// A property that leaves AND one that arrives in the same edit is the shape a
|
|
1192
|
-
// rename takes, and it is indistinguishable from a genuine replacement — the two
|
|
1193
|
-
// have opposite consequences for the data behind the field. The earlier guard only
|
|
1194
|
-
// caught it when `backing.mapping` lacked the new name; update the mapping too and
|
|
1195
|
-
// it silently ran remove_field + add_field, which is the data drop the refusal was
|
|
1196
|
-
// supposed to prevent.
|
|
1197
|
-
if (plan.adds.length > 0 && plan.removes.length > 0) {
|
|
1198
|
-
throw new CliError(
|
|
1199
|
-
`${file.path}: this edit removes ${plan.removes.map((entry) => entry.apiName).join(', ')} and adds `
|
|
1200
|
-
+ `${plan.adds.map((entry) => String(entry.apiName)).join(', ')} at once.`,
|
|
1201
|
-
{
|
|
1202
|
-
code: 'USAGE',
|
|
1203
|
-
hint: 'If that is a rename, there is no per-property rename command — do the removal and the '
|
|
1204
|
-
+ 'addition in separate applies, so the data consequence is a decision rather than a side effect.',
|
|
1205
|
-
},
|
|
1206
|
-
)
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
1222
|
let current = revision
|
|
1210
1223
|
for (const property of plan.adds) {
|
|
1211
1224
|
const name = String(property.apiName)
|
|
@@ -1376,6 +1389,13 @@ export const blueprintApply: Command = {
|
|
|
1376
1389
|
const prune = ctx.flags.prune === true
|
|
1377
1390
|
const { plan, files, definition, drift, actionProblems, inheritedWrites } = await computePlan(ctx)
|
|
1378
1391
|
assertActionsApplicable(actionProblems, plan.operations)
|
|
1392
|
+
if (plan.propertyRefusals?.length) {
|
|
1393
|
+
// Said as a failed write says it, so `local dev` leaves that type off and applies the rest (`refusedArtifact`).
|
|
1394
|
+
throw new CliError(`Nothing was applied.\nStopped at: update object-type ${plan.propertyRefusals[0]!.objectApiName}\n${plan.propertyRefusals.map((refusal) => refusal.message).join('\n')}`, {
|
|
1395
|
+
code: 'USAGE',
|
|
1396
|
+
hint: plan.propertyRefusals.map((refusal) => refusal.next).join('\n'),
|
|
1397
|
+
})
|
|
1398
|
+
}
|
|
1379
1399
|
const actionWarnings = renderActionProblems({ refused: [], undeployable: actionProblems.undeployable, refusedActions: [], ...(actionProblems.inherited ? { inherited: actionProblems.inherited } : {}) })
|
|
1380
1400
|
|
|
1381
1401
|
// BEFORE the first write, not before the prune loop: the operations land first,
|
|
@@ -1858,15 +1878,16 @@ export const blueprintRename: Command = {
|
|
|
1858
1878
|
noun: 'blueprint',
|
|
1859
1879
|
verb: 'rename',
|
|
1860
1880
|
args: [
|
|
1861
|
-
{ name: 'kind', required: true, description: 'object-type' },
|
|
1862
|
-
{ name: 'from', required: true, description: 'Current apiName' },
|
|
1881
|
+
{ name: 'kind', required: true, description: 'object-type or property' },
|
|
1882
|
+
{ name: 'from', required: true, description: 'Current apiName (a property: its object type, then its current apiName)' },
|
|
1863
1883
|
{ name: 'to', required: true, description: 'New apiName' },
|
|
1864
1884
|
],
|
|
1865
1885
|
flags: { dir: 'string' },
|
|
1866
|
-
summary: 'Rename an object type, and
|
|
1867
|
-
examples: ['frontera blueprint rename object-type Client Customer'],
|
|
1886
|
+
summary: 'Rename an object type or a property, and the files that name it, as one act',
|
|
1887
|
+
examples: ['frontera blueprint rename object-type Client Customer', 'frontera blueprint rename property Ticket assignee owner'],
|
|
1868
1888
|
},
|
|
1869
1889
|
async run(ctx) {
|
|
1890
|
+
if (ctx.positional[0] === 'property') return renameProperty(ctx)
|
|
1870
1891
|
const [kind, from, to] = ctx.positional
|
|
1871
1892
|
if (kind === 'object-type' && to && !API_NAME_RULE['object-type'].test(to)) {
|
|
1872
1893
|
throw new CliError(`"${to}" is not a valid object-type apiName.`, {
|
|
@@ -1875,7 +1896,7 @@ export const blueprintRename: Command = {
|
|
|
1875
1896
|
})
|
|
1876
1897
|
}
|
|
1877
1898
|
if (kind !== 'object-type') {
|
|
1878
|
-
throw new CliError('Only object types can be renamed in place.', {
|
|
1899
|
+
throw new CliError('Only object types and properties can be renamed in place.', {
|
|
1879
1900
|
code: 'USAGE',
|
|
1880
1901
|
// Being specific about the alternative rather than just refusing: for the
|
|
1881
1902
|
// other kinds a delete-and-create is genuinely equivalent, because nothing
|
|
@@ -1929,6 +1950,93 @@ export const blueprintRename: Command = {
|
|
|
1929
1950
|
},
|
|
1930
1951
|
}
|
|
1931
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* `rename property <ObjectType> <from> <to>`: the same property under a new name,
|
|
1955
|
+
* on the draft and in the files. The property keeps its id, so its column, its
|
|
1956
|
+
* edited values and every Action, link and metric that names it stay with it, and
|
|
1957
|
+
* the files that name it, saved object sets included, are changed to the new name. The type file keeps
|
|
1958
|
+
* `renamedFrom: <from>` on it, which is how an apply or a change request against
|
|
1959
|
+
* another draft that still has the old name renames it the same way.
|
|
1960
|
+
*/
|
|
1961
|
+
async function renameProperty(ctx: CommandContext) {
|
|
1962
|
+
const [, objectApiName, from, to] = ctx.positional
|
|
1963
|
+
if (!objectApiName || !from || !to) {
|
|
1964
|
+
throw new CliError('A property rename names its object type, its current apiName and its new one.', {
|
|
1965
|
+
code: 'USAGE',
|
|
1966
|
+
hint: 'frontera blueprint rename property <ObjectType> <from> <to>',
|
|
1967
|
+
})
|
|
1968
|
+
}
|
|
1969
|
+
if (!/^[a-z][A-Za-z0-9]{0,99}$/.test(to)) {
|
|
1970
|
+
throw new CliError(`"${to}" is not a valid property apiName.`, { code: 'USAGE', hint: 'camelCase: a lowercase letter, then letters and digits, up to 100.' })
|
|
1971
|
+
}
|
|
1972
|
+
const client = api(ctx)
|
|
1973
|
+
const detail = await client.getObjectTypeDetail(objectApiName)
|
|
1974
|
+
if (!detail?.id) {
|
|
1975
|
+
throw new CliError(`No object type "${objectApiName}" on the draft.`, { code: 'NOT_FOUND', hint: 'frontera blueprint catalog' })
|
|
1976
|
+
}
|
|
1977
|
+
const names = new Set(detail.properties.map((property) => property.apiName))
|
|
1978
|
+
const field = detail.properties.find((property) => property.apiName === from)
|
|
1979
|
+
if (field && names.has(to)) {
|
|
1980
|
+
throw new CliError(`${objectApiName} has both ${from} and ${to}, so ${from} cannot take the name.`, {
|
|
1981
|
+
code: 'CONFLICT',
|
|
1982
|
+
hint: `remove ${to} first (its own apply), or rename ${from} to another name`,
|
|
1983
|
+
})
|
|
1984
|
+
}
|
|
1985
|
+
if (!field && !names.has(to)) {
|
|
1986
|
+
throw new CliError(`${objectApiName} has no property "${from}" on the draft.`, { code: 'NOT_FOUND', hint: `frontera blueprint get object-type ${objectApiName}` })
|
|
1987
|
+
}
|
|
1988
|
+
if (field?.id) {
|
|
1989
|
+
await client.draftCommand(
|
|
1990
|
+
{ kind: 'update_field_schema', objectId: detail.id, fieldId: field.id, patch: { apiName: to } },
|
|
1991
|
+
await client.revision(),
|
|
1992
|
+
)
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const directory = root(ctx)
|
|
1996
|
+
const files = existsSync(directory) ? readTree(directory) : []
|
|
1997
|
+
const changed = new Map<string, AuthoredFile>()
|
|
1998
|
+
const changedSets: string[] = []
|
|
1999
|
+
const typeFile = files.find((file) => file.kind === 'object-type' && file.apiName === objectApiName)
|
|
2000
|
+
if (typeFile) {
|
|
2001
|
+
const properties = Array.isArray(typeFile.document.properties) ? typeFile.document.properties as Array<Record<string, unknown>> : []
|
|
2002
|
+
const target = properties.find((property) => property.apiName === to) ?? properties.find((property) => property.apiName === from)
|
|
2003
|
+
if (target && (target.apiName !== to || target.renamedFrom !== from)) {
|
|
2004
|
+
target.apiName = to
|
|
2005
|
+
target.renamedFrom = from
|
|
2006
|
+
changed.set(typeFile.path, typeFile)
|
|
2007
|
+
}
|
|
2008
|
+
// A property the file still names `from` is a new one taking the old name back,
|
|
2009
|
+
// and what the files name `from` then means it.
|
|
2010
|
+
if (!properties.some((property) => property.apiName === from)) {
|
|
2011
|
+
for (const file of files) {
|
|
2012
|
+
const places = namesOf(file, objectApiName, from)
|
|
2013
|
+
for (const rename of places) rename(to)
|
|
2014
|
+
if (places.length) changed.set(file.path, file)
|
|
2015
|
+
}
|
|
2016
|
+
const sets = readObjectSets(directory)
|
|
2017
|
+
for (const set of sets) {
|
|
2018
|
+
const places = objectSetNamesOf(set, files, sets, objectApiName, from)
|
|
2019
|
+
for (const rename of places) rename(to)
|
|
2020
|
+
if (places.length) {
|
|
2021
|
+
writeFileSync(join(directory, set.path), serialize(set.path, set.document))
|
|
2022
|
+
changedSets.push(set.path)
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (changed.size) writeTree(directory, [...changed.values()])
|
|
2028
|
+
const changedPaths = [...changed.keys(), ...changedSets]
|
|
2029
|
+
const where = changedPaths.length ? ` Changed ${changedPaths.join(', ')}.` : ''
|
|
2030
|
+
return {
|
|
2031
|
+
data: { objectType: objectApiName, from, to, renamedOnDraft: Boolean(field), changedFiles: changedPaths },
|
|
2032
|
+
text: (field
|
|
2033
|
+
? `Renamed ${objectApiName}.${from} to ${to} on the draft. It keeps its id, so its column, its edited values and what names it stay with it.`
|
|
2034
|
+
: `${objectApiName}.${from} is already ${to} on the draft.`)
|
|
2035
|
+
+ where
|
|
2036
|
+
+ '\nNext: `frontera blueprint apply --yes` applies the rest of the files, then `frontera blueprint publish` releases it.',
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1932
2040
|
export const blueprintDeclarativeCommands: Command[] = [
|
|
1933
2041
|
blueprintPull,
|
|
1934
2042
|
blueprintPlan,
|
|
@@ -34,7 +34,7 @@ interface KnowledgeRow {
|
|
|
34
34
|
*/
|
|
35
35
|
async function knowledgeAppNote(api: PlatformApi): Promise<string> {
|
|
36
36
|
return (await api.knowledgeAppRuns())
|
|
37
|
-
? ' A knowledge base from the Knowledge app is not one of these: it runs on the platform only, not on a local runtime. '
|
|
37
|
+
? ' A knowledge base from the Knowledge app is not one of these: it runs on the platform only, with its search and AI answers, not on a local runtime. '
|
|
38
38
|
+ 'Choose it for an Agent on the platform, under the Agent\'s Knowledge page, and test that Agent there.'
|
|
39
39
|
: ''
|
|
40
40
|
}
|
|
@@ -28,6 +28,7 @@ import { findWorkflowTestFiles, recordWorkflowTestRun, runWorkflowTests, workflo
|
|
|
28
28
|
import { vendoredSdkLine, vendoredSdkState } from '../../local/local-vendored-sdk'
|
|
29
29
|
import { withFix } from '../../local/fix-words'
|
|
30
30
|
import { runtimeKeyCustodianProblem } from '../../local/local-key-custodian'
|
|
31
|
+
import { runtimeJobsProblem } from '../../local/local-jobs'
|
|
31
32
|
import { ensureAppPackages } from '../../project-bootstrap'
|
|
32
33
|
import { flagBool, flagString, type Command, type CommandContext } from '../types'
|
|
33
34
|
|
|
@@ -395,10 +396,11 @@ async function check(ctx: CommandContext) {
|
|
|
395
396
|
}
|
|
396
397
|
if (step === 'start') {
|
|
397
398
|
if (running(project)) {
|
|
398
|
-
// Running, with keys it can't read
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
399
|
+
// Running, with keys it can't read (every later step would fail on a secret or an Action record),
|
|
400
|
+
// or with a jobs engine that delivers nothing (every later step that writes waits on it).
|
|
401
|
+
const service = `http://127.0.0.1:${readState(project)!.ports.service}`
|
|
402
|
+
const problem = (await runtimeKeyCustodianProblem(service)) ?? (await runtimeJobsProblem(service))
|
|
403
|
+
if (problem) record({ step, status: 'failed', note: problem, ms: 0 })
|
|
402
404
|
else record({ step, status: 'skipped', note: 'the runtime is already running', ms: 0 })
|
|
403
405
|
} else await command(step, root, self(['local', 'start']), 'frontera local start')
|
|
404
406
|
continue
|
|
@@ -40,7 +40,7 @@ import { agentFilesProblem, caseConcurrency, caseCount, caseRepeat, DEFAULT_PASS
|
|
|
40
40
|
import { compareWithRun, comparisonWords, listTestRuns, readTestRun, runOnModel, saveTestRun, stuckWords } from '../../local/local-test-models'
|
|
41
41
|
import { modelSubstitutions, skippedCases, skippedWords } from '../../local/local-test-scope'
|
|
42
42
|
import { devServerLine, listDevServers, stopDevServers } from '../../mcp/mcp-apps'
|
|
43
|
-
import { restartJobs } from '../../local/local-jobs'
|
|
43
|
+
import { JOBS_STALL_FIX, restartJobs, runtimeJobsProblem } from '../../local/local-jobs'
|
|
44
44
|
import { conversationTranscript } from '../../mcp/mcp-conversations'
|
|
45
45
|
import { runtimeLink } from '../../mcp/mcp-runtime'
|
|
46
46
|
import { caseTraces, evidenceLine, failingCaseEvidence } from '../../local/local-case-evidence'
|
|
@@ -470,7 +470,11 @@ async function status(ctx: CommandContext) {
|
|
|
470
470
|
// What the runtime holds with no file in the project (an Agent whose folder was deleted): said with its fix.
|
|
471
471
|
const orphans = (state.phase === 'ready' && isAlive(state.supervisorPid) && state.credentials ? await projectOrphans(project, `http://127.0.0.1:${state.ports.service}`, state.credentials).catch(() => null) : null) ?? []
|
|
472
472
|
// Encryption keys the runtime can't read (data keys a hosted OpenBao locked): every secret and governed Action record fails until a reset.
|
|
473
|
-
|
|
473
|
+
// And governed work the jobs engine has left waiting: its port can answer while its calls into the service fail, so its line above says running.
|
|
474
|
+
// Read together: each waits up to 3 s on a service that doesn't answer.
|
|
475
|
+
const [keyProblem, jobsProblem] = state.phase === 'ready' && isAlive(state.supervisorPid)
|
|
476
|
+
? await Promise.all([runtimeKeyCustodianProblem(`http://127.0.0.1:${state.ports.service}`), runtimeJobsProblem(`http://127.0.0.1:${state.ports.service}`)])
|
|
477
|
+
: [null, null]
|
|
474
478
|
// Who last changed the runtime or the files, one line per session, and the run holding the runtime (X9).
|
|
475
479
|
const writers = recentWriters(project.stateDir)
|
|
476
480
|
const lock = readLock(project.stateDir)
|
|
@@ -495,12 +499,14 @@ async function status(ctx: CommandContext) {
|
|
|
495
499
|
// On the runtime, not in the files: each with what fixes it (`local dev` archives an Agent or workflow whose file was deleted).
|
|
496
500
|
...(orphans.length ? { notInProject: orphans } : {}),
|
|
497
501
|
...(keyProblem ? { keyCustodian: { problem: keyProblem, fix: KEY_CUSTODIAN_FIX } } : {}),
|
|
502
|
+
...(jobsProblem ? { jobsDelivery: { problem: jobsProblem, fix: JOBS_STALL_FIX } } : {}),
|
|
498
503
|
writers,
|
|
499
504
|
...(lock ? { lock: { command: lock.command, session: lock.session, harness: lock.harness, since: lock.since } } : {}),
|
|
500
505
|
},
|
|
501
506
|
text: [
|
|
502
507
|
renderStatus({ ...state, components }, project, undefined, live, setIds, personas, secrets, liveApps ?? null, down),
|
|
503
508
|
...(keyProblem ? ['', 'Encryption keys', ` ✕ ${keyProblem}`] : []),
|
|
509
|
+
...(jobsProblem ? ['', 'Jobs', ` ✕ ${jobsProblem}`] : []),
|
|
504
510
|
...(stuck.length ? ['', 'Knowledge', ...stuck.map((k) => ` warning: ${k.problem}`)] : []),
|
|
505
511
|
...(orphans.length ? ['', 'On the runtime, not in the project', ...orphans.map((o) => ` warning: ${orphanLine(o)}`)] : []),
|
|
506
512
|
...(devServers.length ? ['', 'App dev servers', ...devServers.map((s) => ` ${devServerLine(s)}`)] : []),
|