@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
|
@@ -5,7 +5,7 @@ import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
|
5
5
|
import { parse as parseYaml } from 'yaml'
|
|
6
6
|
|
|
7
7
|
import type { Portability } from './local-portability'
|
|
8
|
-
import { conflictsId, deepEqual, mergeItemFiles, type Choice, type Side } from './local-sync-merge'
|
|
8
|
+
import { conflictsId, deepEqual, mergeItemFiles, sameContent, type Choice, type Side } from './local-sync-merge'
|
|
9
9
|
import { itemTitle } from './local-sync-names'
|
|
10
10
|
import { toNames, type Directory, type Unresolved } from './local-sync-refs'
|
|
11
11
|
import { comparableText, isPluginFile, withLocalSecrets } from './local-secrets'
|
|
@@ -358,6 +358,32 @@ export function baseFiles(entry: LockEntry | undefined, read: ((path: string) =>
|
|
|
358
358
|
return out
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/** Whether two sets of an item's files say the same, file by file: a missing file only matches a missing one. */
|
|
362
|
+
function sameFiles(a: Record<string, string | null>, b: Record<string, string | null>): boolean {
|
|
363
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])].every((path) => {
|
|
364
|
+
const x = a[path] ?? null
|
|
365
|
+
const y = b[path] ?? null
|
|
366
|
+
return x === y || (x !== null && y !== null && sameContent(path, x, y))
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Every base file is here, as the last pull wrote it: without one, a deletion on one side reads as an addition on the other. */
|
|
371
|
+
const wholeBase = (base: Record<string, string | null>) => Object.keys(base).length > 0 && Object.values(base).every((v) => v !== null)
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* The status again, from what the files say rather than their exact text,
|
|
375
|
+
* when every base file is here to compare with: a side whose files
|
|
376
|
+
* differ from the base only in key order or formatting (the platform writes
|
|
377
|
+
* a document's keys its own way from one read to the next) has not moved.
|
|
378
|
+
*/
|
|
379
|
+
function settledStatus(found: ItemStatus, base: Record<string, string | null>, local: Record<string, string | null>, remote: Record<string, string | null>): ItemStatus {
|
|
380
|
+
if (found !== 'conflict' && found !== 'edited' && found !== 'changed') return found
|
|
381
|
+
if (!wholeBase(base)) return found
|
|
382
|
+
const localMoved = !sameFiles(base, local)
|
|
383
|
+
const remoteMoved = !sameFiles(base, remote)
|
|
384
|
+
return remoteMoved ? (localMoved ? 'conflict' : 'changed') : localMoved ? 'edited' : 'same'
|
|
385
|
+
}
|
|
386
|
+
|
|
361
387
|
/** The working files at these paths, null where absent. */
|
|
362
388
|
export function localFiles(root: string, paths: string[]): Record<string, string | null> {
|
|
363
389
|
return Object.fromEntries(paths.map((p) => [p, readLocal(root, p)]))
|
|
@@ -414,18 +440,30 @@ export function computePlan(root: string, remote: RemoteItem[], lock: SyncLock |
|
|
|
414
440
|
// An Agent's file that neither the base nor the workspace has but the folder does (its first tools, in a new
|
|
415
441
|
// `bindings.json`) is a change here too, as push reads it: otherwise status says nothing while push proposes it.
|
|
416
442
|
const added = item.kind === 'agent' ? AGENT_FILES.map((f) => `agents/${item.name}/${f}`).filter((p) => !paths.includes(p) && existsSync(join(root, p))) : []
|
|
417
|
-
|
|
443
|
+
// Saying the same as the workspace is up to date, whatever order the platform wrote the keys in.
|
|
444
|
+
const matches = files.every((f) => f.local === f.remote || (f.local !== null && f.remote !== null && sameContent(f.path, f.local, f.remote))) && !added.length
|
|
418
445
|
// The files as they stand over the base's paths, plus any path only the workspace has that is
|
|
419
446
|
// already in the folder: a file the pull would overwrite counts as a change here.
|
|
420
447
|
const basePaths = entry ? [...Object.keys(entry.files), ...files.filter((f) => !(f.path in entry.files) && f.local !== null).map((f) => f.path), ...added] : Object.keys(item.files)
|
|
421
|
-
const
|
|
448
|
+
const hashed = statusFor(entry?.remote ?? null, localHash(root, basePaths, opts.base, item.files), itemHash(item.files), matches) ?? 'same'
|
|
449
|
+
const base = baseFiles(entry, opts.base)
|
|
450
|
+
let found = entry && !entry.conflict && !added.length ? settledStatus(hashed, base, Object.fromEntries(files.map((f) => [f.path, f.local])), Object.fromEntries(files.map((f) => [f.path, f.remote]))) : hashed
|
|
422
451
|
// Pulled with conflicts and not resolved: it stays so until `resolve`, whatever the files say.
|
|
423
|
-
const status: ItemStatus = entry?.conflict && found !== 'same' ? 'unresolved' : found
|
|
424
452
|
let merge: PlanItem['merge']
|
|
425
|
-
if (
|
|
453
|
+
if (found === 'conflict' || (entry?.conflict && found !== 'same')) {
|
|
426
454
|
const local = localFiles(root, paths)
|
|
427
|
-
|
|
455
|
+
const result = mergeItemFiles(base, local, item.files)
|
|
456
|
+
merge = { conflicts: result.conflicts }
|
|
457
|
+
// A clean merge that leaves every file as it is here: what the workspace changed is already in the files
|
|
458
|
+
// (proposed from here, or a set only reordered there). Nothing comes in; the change is this side's alone.
|
|
459
|
+
// Only against the whole base: without it the merge keeps what the workspace deleted, and the item stays a
|
|
460
|
+
// conflict, so the base is looked for again and a push waits for the pull.
|
|
461
|
+
if (found === 'conflict' && entry && !entry.conflict && wholeBase(base) && !result.conflicts && result.files.every((f) => f.merged === (local[f.path] ?? null))) {
|
|
462
|
+
found = 'edited'
|
|
463
|
+
merge = undefined
|
|
464
|
+
}
|
|
428
465
|
}
|
|
466
|
+
const status: ItemStatus = entry?.conflict && found !== 'same' ? 'unresolved' : found
|
|
429
467
|
const namesOnly = status === 'changed' && opts.directory ? differsOnlyInNames(localFiles(root, paths), item.files, opts.directory) : false
|
|
430
468
|
plan.push({
|
|
431
469
|
ref: item.ref,
|
|
@@ -65,7 +65,8 @@ export function fixFor(check: Check): string | null {
|
|
|
65
65
|
if (check.code === 'stale_base' || check.code === 'exists' || check.code === 'gone') return `pull it first (\`frontera local sync pull ${ref}\`), then propose`
|
|
66
66
|
if (check.code === 'unpublished_draft') return 'publish or discard its draft on the platform, then fetch and propose'
|
|
67
67
|
if (check.code === 'blueprint_backing') return 'make the type’s backing name a dataset the organization has, or add its datasets/<name>.json to this request, and map each field to one of its columns'
|
|
68
|
-
if (check.code === 'blueprint_draft') return 'publish
|
|
68
|
+
if (check.code === 'blueprint_draft') return 'publish the organization’s Blueprint on the platform first, then propose'
|
|
69
|
+
if (check.code === 'blueprint_draft_conflict') return 'publish or discard that change in the Blueprint on the platform, or leave this item out (`frontera local sync push <kind/name>…` proposes only the items it names), then propose'
|
|
69
70
|
const model = /the model (\S+) is not in this workspace/.exec(check.message)
|
|
70
71
|
if (model) return `choose the workspace model it goes as (\`frontera local sync map-model ${model[1]} <workspace model>\`)`
|
|
71
72
|
if (check.code === 'unresolved') return 'propose what it names too, or create that in the workspace first'
|
|
@@ -17,7 +17,7 @@ import { importedFiles } from './local-function-shared'
|
|
|
17
17
|
import { isIgnoredName } from './local-dev-sync'
|
|
18
18
|
import { comparableText, withoutSecrets } from './local-secrets'
|
|
19
19
|
import { projectModels } from './local-sync-models'
|
|
20
|
-
import { deepEqual } from './local-sync-merge'
|
|
20
|
+
import { deepEqual, sameContent } from './local-sync-merge'
|
|
21
21
|
import { portableNotes, sameDocumentText, toLocal, toWorkspace } from './local-sync-portable'
|
|
22
22
|
import { itemTitle } from './local-sync-names'
|
|
23
23
|
import { isScaffoldSample } from './local-scaffold'
|
|
@@ -443,12 +443,18 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
443
443
|
// A Plugin whose only difference is this computer's secrets (a header set here) is the same, as status reads it.
|
|
444
444
|
const pluginSame = kind === 'plugin' && !!theirs && Object.keys(files).length > 0 && Object.keys(files).every((p) => theirs.files[p] !== undefined && comparableText(p, readLocal(root, p) ?? '', theirs.files[p]!) === theirs.files[p])
|
|
445
445
|
const textSame = !entry || itemHash(files) === entry.remote || !theirs || itemHash(theirs.files) !== entry.remote || pluginSame
|
|
446
|
-
|
|
446
|
+
// Up to date as status reads it: the files say what the workspace's say, keys in another order or not.
|
|
447
|
+
const saysSame = status === 'same' && !!theirs && Object.keys(files).length === Object.keys(theirs.files).length && Object.entries(files).every(([path, text]) => theirs.files[path] !== undefined && sameContent(path, text, theirs.files[path]!))
|
|
448
|
+
if (status === 'same' && (textSame || saysSame) && !extra) continue
|
|
447
449
|
if (status === 'same' && Object.keys(files).length === 0) continue
|
|
448
450
|
const isNew = !entry && !theirs
|
|
449
|
-
|
|
451
|
+
// The workspace has not moved since the last pull (the plan reads what its files say, key order aside), or what it
|
|
452
|
+
// changed is already in the files here: its version now is the base.
|
|
453
|
+
const unmoved = !!theirs && !!entry && (itemHash(theirs.files) === entry.remote || p?.status === 'edited')
|
|
454
|
+
const baseFiles: Record<string, string> = !isNew && unmoved ? theirs!.files : {}
|
|
450
455
|
const diffPaths = [...new Set([...Object.keys(baseFiles), ...Object.keys(files)])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
|
|
451
|
-
|
|
456
|
+
// A file that says the same as the base, keys in another order, is no change to show.
|
|
457
|
+
const diff: PushFile[] = diffPaths.map((path) => ({ path, base: baseFiles[path] ?? null, local: files[path] ?? null })).filter((f) => f.base !== f.local && !(f.base !== null && f.local !== null && sameContent(f.path, f.base, f.local)))
|
|
452
458
|
const item: PushItem = {
|
|
453
459
|
ref,
|
|
454
460
|
kind,
|
|
@@ -513,7 +519,7 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
513
519
|
} else if (item.proposable && !isNew) {
|
|
514
520
|
// The base is what the workspace holds now (the plan says only this side moved): its current
|
|
515
521
|
// revision, which for a Blueprint file is the Blueprint's.
|
|
516
|
-
item.baseRevision = (kind === 'blueprint' || kind === 'evaluation' ? snapshot?.groups[kind] : undefined) ?? (
|
|
522
|
+
item.baseRevision = (kind === 'blueprint' || kind === 'evaluation' ? snapshot?.groups[kind] : undefined) ?? (unmoved ? theirs!.revision : undefined) ?? entry?.revision ?? null
|
|
517
523
|
} else if (item.proposable && kind === 'blueprint') {
|
|
518
524
|
item.baseRevision = snapshot?.groups.blueprint ?? null
|
|
519
525
|
}
|
|
@@ -805,12 +811,10 @@ export function sameDocument(path: string, a: string, b: string): boolean {
|
|
|
805
811
|
if (/^agents\/[^/]+\/evals\/[^/]+\.ya?ml$/.test(path)) return deepEqual(evaluationCases(a), evaluationCases(b))
|
|
806
812
|
// A knowledge base's quality checks are a set: the platform lists them in its own order.
|
|
807
813
|
if (path.endsWith(`/${QUALITY_CHECKS_FILE}`)) return deepEqual(checkSet(a), checkSet(b))
|
|
808
|
-
if (/\.json$/i.test(path)) return deepEqual(JSON.parse(a), JSON.parse(b))
|
|
809
|
-
if (/\.ya?ml$/i.test(path)) return deepEqual(parseYaml(a), parseYaml(b))
|
|
810
814
|
} catch {
|
|
811
815
|
return false
|
|
812
816
|
}
|
|
813
|
-
return
|
|
817
|
+
return sameContent(path, a, b)
|
|
814
818
|
}
|
|
815
819
|
|
|
816
820
|
/** Quality checks as a set: by question, each one's lists in order of their words. */
|
|
@@ -1017,7 +1021,8 @@ export interface ApplyOutcome {
|
|
|
1017
1021
|
ok: boolean
|
|
1018
1022
|
steps: ApplyStep[]
|
|
1019
1023
|
failure?: { key: string; ref: string; itemIds: string[]; message: string }
|
|
1020
|
-
|
|
1024
|
+
/** `note`: what a put-back left other than as it was. */
|
|
1025
|
+
compensation: Array<{ label: string; type: string; status: 'put-back' | 'failed' | 'irreversible'; error?: string; note?: string }>
|
|
1021
1026
|
}
|
|
1022
1027
|
|
|
1023
1028
|
/** Apply's outcome as the service sent it, read defensively: a field it lacks reads as empty. */
|
|
@@ -1042,7 +1047,13 @@ export function parseOutcome(raw: unknown): ApplyOutcome {
|
|
|
1042
1047
|
if (!c || typeof c !== 'object') return []
|
|
1043
1048
|
const r = c as Record<string, unknown>
|
|
1044
1049
|
const status = r.status === 'failed' || r.status === 'irreversible' ? r.status : 'put-back'
|
|
1045
|
-
return [{
|
|
1050
|
+
return [{
|
|
1051
|
+
label: String(r.label ?? ''),
|
|
1052
|
+
type: String(r.type ?? ''),
|
|
1053
|
+
status: status as 'put-back' | 'failed' | 'irreversible',
|
|
1054
|
+
...(typeof r.error === 'string' ? { error: r.error } : {}),
|
|
1055
|
+
...(typeof r.note === 'string' ? { note: r.note } : {}),
|
|
1056
|
+
}]
|
|
1046
1057
|
})
|
|
1047
1058
|
return {
|
|
1048
1059
|
ok: o.ok === true,
|
|
@@ -1074,7 +1085,7 @@ export function describeOutcome(outcome: ApplyOutcome, titles: Record<string, st
|
|
|
1074
1085
|
`Not applied: ${failed ? `${failedName(outcome, titles)} failed: ${failed.message}` : 'a step failed'}`,
|
|
1075
1086
|
...lines,
|
|
1076
1087
|
...(outcome.compensation.length
|
|
1077
|
-
? ['', 'What was put back:', ...outcome.compensation.map((c) => ` ${c.label}: ${BACK_WORD[c.status]}${c.error ? ` (${c.error})` : ''}`)]
|
|
1088
|
+
? ['', 'What was put back:', ...outcome.compensation.map((c) => ` ${c.label}: ${BACK_WORD[c.status]}${c.error ? ` (${c.error})` : ''}${c.note ? `. ${c.note}` : ''}`)]
|
|
1078
1089
|
: ['', 'Nothing had changed yet, so nothing needed putting back.']),
|
|
1079
1090
|
]
|
|
1080
1091
|
}
|
|
@@ -42,9 +42,11 @@ import { governedActionIds, invert, itemRef, referencesIn, refName as refNameOf,
|
|
|
42
42
|
export interface SyncSource {
|
|
43
43
|
/** The workspace key's client. */
|
|
44
44
|
workspace: SyncClient
|
|
45
|
-
/** The organization
|
|
45
|
+
/** The organization's reader, when there is one: the person signed in, or the organization key. */
|
|
46
46
|
organization?: SyncClient
|
|
47
47
|
workspaceId: string
|
|
48
|
+
/** The person signed in, when they read nothing at organization grain (no Blueprint Steward) and no key reads for them. */
|
|
49
|
+
notSteward?: string
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** Envelopes come single (`{data}`) or double (`{data:{data}}`); lists are what is inside. */
|
|
@@ -1268,7 +1270,8 @@ export function sliceDraft(draft: Bundle, granted: ReadonlySet<string>): Bundle
|
|
|
1268
1270
|
const ids = new Set(objects.map((o) => o.id))
|
|
1269
1271
|
const actions = (def.actions ?? []).filter((a) => {
|
|
1270
1272
|
const changes = (Array.isArray(a.businessOutcomes) ? a.businessOutcomes : []).flatMap((o: Record<string, any>) => (Array.isArray(o?.expectedChanges) ? o.expectedChanges : [])) as Array<Record<string, any>>
|
|
1271
|
-
|
|
1273
|
+
const records = (Array.isArray(a.records) ? a.records : []) as Array<Record<string, any>>
|
|
1274
|
+
return [a.subject?.objectTypeId, ...records.map((r) => r?.objectTypeId), ...changes.map((c) => c?.objectTypeId)].filter((v): v is string => typeof v === 'string').every((id) => ids.has(id))
|
|
1272
1275
|
})
|
|
1273
1276
|
return {
|
|
1274
1277
|
...def,
|
|
@@ -1312,7 +1315,7 @@ async function readBlueprint(
|
|
|
1312
1315
|
notes.push('This platform cannot cut the Blueprint to the workspace yet; the organization key read the whole model')
|
|
1313
1316
|
return readBlueprint(source, 'organization', warnings, notes, sets, opts)
|
|
1314
1317
|
} else {
|
|
1315
|
-
warnings.push('Blueprint: this platform cannot export a workspace’s Blueprint yet;
|
|
1318
|
+
warnings.push('Blueprint: this platform cannot export a workspace’s Blueprint yet; sign in for the project’s workspace to pull it')
|
|
1316
1319
|
return []
|
|
1317
1320
|
}
|
|
1318
1321
|
if (opts.draft && !source.organization) notes.push('Reading the Blueprint’s unpublished draft takes an organization key; its published release was read')
|
|
@@ -1384,7 +1387,8 @@ async function withActions(org: SyncClient, sliced: Bundle, warnings: string[],
|
|
|
1384
1387
|
const outside: string[] = []
|
|
1385
1388
|
for (const action of published) {
|
|
1386
1389
|
const changes = (Array.isArray(action.businessOutcomes) ? action.businessOutcomes : []).flatMap((o: Record<string, any>) => (Array.isArray(o?.expectedChanges) ? o.expectedChanges : [])) as Array<Record<string, any>>
|
|
1387
|
-
const
|
|
1390
|
+
const records = (Array.isArray(action.records) ? action.records : []) as Array<Record<string, any>>
|
|
1391
|
+
const touched = [action.subject?.objectTypeId, ...records.map((r) => r?.objectTypeId), ...changes.map((c) => c?.objectTypeId)].filter((v): v is string => typeof v === 'string')
|
|
1388
1392
|
if (touched.every((id) => ids.has(id))) kept.push(action)
|
|
1389
1393
|
else outside.push(String(action.apiName))
|
|
1390
1394
|
}
|
|
@@ -1742,9 +1746,10 @@ export async function readWorkspace(source: SyncSource, opts: ReadOptions = {}):
|
|
|
1742
1746
|
it.notes.push(`Hard-codes the workspace’s object type ids, which the local runtime doesn’t share: ${hits.map((h) => `${h.file}:${h.line} (${h.objectType})`).join(', ')}. \`local dev\` deploys it here with this runtime’s ids in their place and leaves the files as they are; take the id from the query result (\`subjectOf\`, or \`result.objectTypeId\`) so the code holds on any runtime`)
|
|
1743
1747
|
}
|
|
1744
1748
|
if (!source.organization && items.some((i) => i.unresolved.length)) {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1749
|
+
const [what, shown] = directory.model?.size ? ['Governed Actions are', 'an Action step'] : ['Models and governed Actions are', 'an LLM step’s model or an Action step']
|
|
1750
|
+
notes.push(source.notSteward
|
|
1751
|
+
? `${what} named only for a Blueprint Steward signed in for the project’s workspace (organization-wide); ${source.notSteward}, signed in here, is not one, so ${shown} shows as an id`
|
|
1752
|
+
: `${what} named only while you’re signed in for the project’s workspace (organization-wide); ${shown} shows as an id until you sign in`)
|
|
1748
1753
|
}
|
|
1749
1754
|
return { items, warnings, notes, directory, models: lists.models ?? [] }
|
|
1750
1755
|
}
|
|
@@ -1817,7 +1822,7 @@ export function noteActions(items: RemoteItem[], directory?: Directory): void {
|
|
|
1817
1822
|
continue
|
|
1818
1823
|
}
|
|
1819
1824
|
const what = many ? `${missing.length} governed Actions` : `governed Action${missing.length === 1 ? '' : 's'} ${list}`
|
|
1820
|
-
it.notes.push(`${subject.charAt(0).toUpperCase()}${subject.slice(1)} runs ${what}:
|
|
1825
|
+
it.notes.push(`${subject.charAt(0).toUpperCase()}${subject.slice(1)} runs ${what}: sign in for the project’s workspace to bring ${missing.length === 1 ? 'it' : 'them'} with the Blueprint${many ? `: ${list}` : ''}`)
|
|
1821
1826
|
}
|
|
1822
1827
|
}
|
|
1823
1828
|
|
|
@@ -112,7 +112,7 @@ interface ActionFile {
|
|
|
112
112
|
apiName: string
|
|
113
113
|
subject?: { objectType?: string }
|
|
114
114
|
inputs?: Array<{ apiName?: string; valueType?: { kind?: string; enum?: unknown[] } }>
|
|
115
|
-
businessOutcomes?: Array<{ expectedChanges?: Array<{ kind?: string; property?: string; value?: { kind?: string; field?: string; value?: unknown } }> }>
|
|
115
|
+
businessOutcomes?: Array<{ expectedChanges?: Array<{ kind?: string; objectType?: string; property?: string; value?: { kind?: string; field?: string; value?: unknown } }> }>
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
@@ -612,7 +612,7 @@ export function actionEnums(actions: ActionFile[]): Map<string, string[]> {
|
|
|
612
612
|
for (const outcome of a.businessOutcomes ?? []) {
|
|
613
613
|
for (const change of outcome.expectedChanges ?? []) {
|
|
614
614
|
if (change.kind !== 'property_set' || !change.property) continue
|
|
615
|
-
const key = `${type}.${change.property}`
|
|
615
|
+
const key = `${change.objectType ?? type}.${change.property}`
|
|
616
616
|
const values = change.value?.kind === 'input' && change.value.field ? asStrings(inputs.get(change.value.field)?.valueType?.enum) : change.value?.kind === 'literal' && change.value.value !== undefined ? [String(change.value.value)] : null
|
|
617
617
|
if (values) out.set(key, [...new Set([...(out.get(key) ?? []), ...values])])
|
|
618
618
|
}
|
package/src/main.ts
CHANGED
|
@@ -245,8 +245,13 @@ async function main(): Promise<number> {
|
|
|
245
245
|
{ cwd, ...(piped ? { env: { ...process.env, FRONTERA_TOKEN: piped } } : {}) },
|
|
246
246
|
)
|
|
247
247
|
// A command that acts as a person takes the browser sign-in (`auth login --browser`) over the
|
|
248
|
-
// profile's key, which it would refuse;
|
|
249
|
-
|
|
248
|
+
// profile's key, which it would refuse; so does an organization command over a workspace key
|
|
249
|
+
// (a project's target profile with no organization key), which it would refuse too. Nothing
|
|
250
|
+
// else does. A raw FRONTERA_TOKEN still wins.
|
|
251
|
+
const keyRefused = command.meta.authLane === 'session'
|
|
252
|
+
? credential.token.startsWith('sk-')
|
|
253
|
+
: credential.token.startsWith('sk-ws-') && commandScope(command.meta) === 'organization' && command.meta.noun !== 'auth' && command.meta.noun !== 'kit'
|
|
254
|
+
if (keyRefused && !process.env.FRONTERA_TOKEN?.trim() && 'profile' in credential && credential.profile) {
|
|
250
255
|
const signedIn = await readDeviceSession(credentialStore(), credential.profile, credential.apiUrl)
|
|
251
256
|
if (signedIn) {
|
|
252
257
|
credential = { ...credential, token: signedIn.token }
|