@frontera-sdk/cli 0.1.0 → 1.43.5
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 -2
- package/src/api/apps-api.ts +13 -1
- package/src/api/automation-api.ts +129 -1
- package/src/api/blueprint-authoring-api.ts +574 -0
- package/src/api/dataset-api.ts +199 -0
- package/src/api/platform-api.ts +300 -0
- package/src/automation-template.ts +224 -0
- package/src/blueprint/compile.ts +371 -0
- package/src/blueprint/dataset-revision.ts +33 -0
- package/src/blueprint/diff.ts +223 -0
- package/src/blueprint/model.ts +227 -0
- package/src/blueprint/projection.ts +254 -0
- package/src/blueprint/render.ts +73 -0
- package/src/blueprint/scaffold.ts +79 -0
- package/src/blueprint/tree.ts +121 -0
- package/src/commands/agent/index-commands.ts +87 -1
- package/src/commands/app/deploy.ts +43 -3
- package/src/commands/app/init.ts +23 -1
- package/src/commands/app/pull.ts +12 -35
- package/src/commands/automation/index-commands.ts +42 -1
- package/src/commands/automation/init.ts +52 -0
- package/src/commands/automation/project-root.ts +58 -0
- package/src/commands/automation/pull.ts +124 -0
- package/src/commands/automation/run.ts +271 -0
- package/src/commands/blueprint/authoring.ts +410 -0
- package/src/commands/blueprint/bind.ts +228 -0
- package/src/commands/blueprint/declarative.ts +1052 -0
- package/src/commands/blueprint/grants.ts +164 -0
- package/src/commands/dataset/index-commands.ts +431 -0
- package/src/commands/knowledge/index-commands.ts +278 -27
- package/src/commands/knowledge/upload-batch.ts +146 -0
- package/src/commands/knowledge/upload-plan.ts +127 -0
- package/src/commands/login.ts +49 -11
- package/src/commands/pack/index-commands.ts +373 -0
- package/src/commands/registry.ts +19 -2
- package/src/commands/secret/index-commands.ts +195 -0
- package/src/commands/skill/bundle-commands.ts +327 -0
- package/src/commands/skill/index-commands.ts +36 -42
- package/src/commands/skill/resolve.ts +34 -0
- package/src/dev-env.ts +114 -0
- package/src/flag-help.ts +34 -0
- package/src/harness.ts +30 -3
- package/src/main.ts +10 -3
- package/src/render-evidence.ts +152 -0
- package/src/template.ts +4 -0
- package/src/untar.ts +44 -0
- package/src/vendor/sdk-sources.json +13 -11
- package/src/commands/blueprint/reserved.ts +0 -40
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { BlueprintAuthoringApi } from '../../api/blueprint-authoring-api'
|
|
5
|
+
import { pickCurrentRevision } from '../../blueprint/dataset-revision'
|
|
6
|
+
import { readTree, writeTree } from '../../blueprint/tree'
|
|
7
|
+
import { CliError } from '../../errors'
|
|
8
|
+
import type { Command, CommandContext } from '../types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bind an object type to a Dataset revision.
|
|
12
|
+
*
|
|
13
|
+
* Two things about the route this uses are worth stating, because both were chosen
|
|
14
|
+
* against a plausible alternative:
|
|
15
|
+
*
|
|
16
|
+
* It is the `update_source_binding` DRAFT COMMAND, not
|
|
17
|
+
* `POST /object-types/:apiName/dataset-binding`. That route writes the ACTIVE
|
|
18
|
+
* catalog; the draft is a separate document, and authoring is draft-side end to
|
|
19
|
+
* end. Binding through the active route inside an authoring session would bind a
|
|
20
|
+
* different thing to everything around it.
|
|
21
|
+
*
|
|
22
|
+
* The mapping is reviewed, never guessed. The service refuses a property left
|
|
23
|
+
* unmapped rather than matching on name (ADR 0008), so `--plan` writes a skeleton
|
|
24
|
+
* with every live property in it and name matches pre-filled as SUGGESTIONS. The
|
|
25
|
+
* guess happens where a person can see and edit it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
function api(ctx: CommandContext): BlueprintAuthoringApi {
|
|
29
|
+
return new BlueprintAuthoringApi(ctx.apiUrl, ctx.token)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MappingFile {
|
|
33
|
+
dataset: string
|
|
34
|
+
mapping: Record<string, string | { column: string; field?: string | null } | null>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function datasetRevision(client: BlueprintAuthoringApi, name: string) {
|
|
38
|
+
const datasets = await client.listDatasets()
|
|
39
|
+
const dataset = datasets.find((candidate) => candidate.name === name)
|
|
40
|
+
if (!dataset?.id) {
|
|
41
|
+
throw new CliError(`No dataset named "${name}" in this organization.`, {
|
|
42
|
+
code: 'NOT_FOUND',
|
|
43
|
+
hint: datasets.length === 0
|
|
44
|
+
// An empty list and a refusal look identical to a caller. Naming the
|
|
45
|
+
// capability is the difference between a five-minute fix and an afternoon.
|
|
46
|
+
? 'No datasets are visible at all — the key may have been minted without dataset:read.'
|
|
47
|
+
: `Visible datasets: ${datasets.map((entry) => entry.name).filter(Boolean).join(', ')}`,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
const revisions = await client.datasetRevisions(dataset.id)
|
|
51
|
+
// The revision the DATASET calls current, not the highest-numbered one. After a
|
|
52
|
+
// rollback those are different revisions, and pinning by sort bound the type to a
|
|
53
|
+
// contract the organization had already stepped off.
|
|
54
|
+
const current = pickCurrentRevision(dataset, revisions)
|
|
55
|
+
if (!current?.id) {
|
|
56
|
+
throw new CliError(`Dataset "${name}" has no published revision.`, {
|
|
57
|
+
code: 'FAILURE',
|
|
58
|
+
hint: 'Publish a revision of the dataset, then bind.',
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
return current
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const blueprintBind: Command = {
|
|
65
|
+
meta: {
|
|
66
|
+
noun: 'blueprint',
|
|
67
|
+
verb: 'bind',
|
|
68
|
+
args: [{ name: 'objectType', required: true, description: 'The object type’s API name' }],
|
|
69
|
+
flags: {
|
|
70
|
+
dir: 'string',
|
|
71
|
+
dataset: 'string',
|
|
72
|
+
plan: 'string',
|
|
73
|
+
file: 'string',
|
|
74
|
+
'accept-contract-change': 'boolean',
|
|
75
|
+
},
|
|
76
|
+
aliases: { f: 'file' },
|
|
77
|
+
summary: 'Bind an object type to a Dataset revision, through a reviewed mapping',
|
|
78
|
+
examples: [
|
|
79
|
+
'frontera blueprint bind Customer --dataset customers --plan ./map.json',
|
|
80
|
+
'frontera blueprint bind Customer --dataset customers --file ./map.json',
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
async run(ctx) {
|
|
84
|
+
const apiName = ctx.positional[0]
|
|
85
|
+
if (!apiName) {
|
|
86
|
+
throw new CliError('An object type apiName is required.', {
|
|
87
|
+
code: 'USAGE',
|
|
88
|
+
hint: 'frontera blueprint bind <objectType> --dataset <name> --plan <file>',
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
const datasetName = ctx.flags.dataset as string | undefined
|
|
92
|
+
if (!datasetName) {
|
|
93
|
+
throw new CliError('A dataset is required: pass --dataset <name>.', {
|
|
94
|
+
code: 'USAGE',
|
|
95
|
+
hint: 'frontera blueprint bind Customer --dataset customers --plan ./mapping.yaml',
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const client = api(ctx)
|
|
100
|
+
const detail = await client.getObjectTypeDetail(apiName)
|
|
101
|
+
if (!detail?.id) {
|
|
102
|
+
throw new CliError(`No object type "${apiName}" on the draft.`, {
|
|
103
|
+
code: 'NOT_FOUND',
|
|
104
|
+
hint: 'frontera blueprint catalog',
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
const properties = detail.properties
|
|
108
|
+
const revision = await datasetRevision(client, datasetName)
|
|
109
|
+
|
|
110
|
+
const planPath = ctx.flags.plan as string | undefined
|
|
111
|
+
if (planPath) {
|
|
112
|
+
const columns = (revision.columns ?? []).map((column) => column.name).filter(Boolean)
|
|
113
|
+
const suggestions: MappingFile['mapping'] = {}
|
|
114
|
+
for (const property of properties) {
|
|
115
|
+
const name = property.apiName
|
|
116
|
+
if (!name) continue
|
|
117
|
+
// A suggestion, and marked as one by being editable rather than applied: an
|
|
118
|
+
// exact name match is the only guess offered, and `null` says plainly that
|
|
119
|
+
// the tool has nothing to suggest rather than inventing something.
|
|
120
|
+
suggestions[name] = columns.includes(name) ? name : null
|
|
121
|
+
}
|
|
122
|
+
const body: MappingFile = { dataset: datasetName, mapping: suggestions }
|
|
123
|
+
const target = resolve(ctx.cwd, planPath)
|
|
124
|
+
writeFileSync(target, `${JSON.stringify(body, null, 2)}\n`)
|
|
125
|
+
const unmapped = Object.values(suggestions).filter((value) => value === null).length
|
|
126
|
+
return {
|
|
127
|
+
data: { path: target, unmapped, columns },
|
|
128
|
+
text: `Wrote a mapping skeleton to ${target}.\n`
|
|
129
|
+
+ `${properties.length} propert${properties.length === 1 ? 'y' : 'ies'}, `
|
|
130
|
+
+ `${unmapped} still to map. Available columns: ${columns.join(', ')}`,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const filePath = ctx.flags.file as string | undefined
|
|
135
|
+
if (!filePath) {
|
|
136
|
+
throw new CliError('A mapping is required: pass --plan <file> to draft one, then --file <file>.', {
|
|
137
|
+
code: 'USAGE',
|
|
138
|
+
hint: `frontera blueprint bind ${apiName} --dataset ${datasetName} --plan ./map.json`,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
const mapping = JSON.parse(readFileSync(resolve(ctx.cwd, filePath), 'utf8')) as MappingFile
|
|
142
|
+
const idByApiName = new Map(properties.map((property) => [property.apiName, property.id]))
|
|
143
|
+
|
|
144
|
+
const columnMappings: Array<{ propertyId: string; column: string; field?: string | null }> = []
|
|
145
|
+
const missing: string[] = []
|
|
146
|
+
for (const property of properties) {
|
|
147
|
+
const name = property.apiName
|
|
148
|
+
if (!name) continue
|
|
149
|
+
const entry = mapping.mapping?.[name]
|
|
150
|
+
if (entry === undefined || entry === null) {
|
|
151
|
+
missing.push(name)
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
const resolved = typeof entry === 'string' ? { column: entry } : entry
|
|
155
|
+
const propertyId = idByApiName.get(name)
|
|
156
|
+
if (!propertyId) {
|
|
157
|
+
// `!` sent `propertyId: undefined` and let the service answer for it. The
|
|
158
|
+
// property came from the same read as the mapping, so this means the draft
|
|
159
|
+
// moved mid-command.
|
|
160
|
+
throw new CliError(`Property "${name}" is no longer on "${apiName}".`, {
|
|
161
|
+
code: 'CONFLICT',
|
|
162
|
+
hint: 'Re-run `frontera blueprint bind … --plan` against the current draft.',
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
columnMappings.push({
|
|
166
|
+
propertyId,
|
|
167
|
+
column: resolved.column,
|
|
168
|
+
...(resolved.field === undefined || resolved.field === null ? {} : { field: resolved.field }),
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
if (missing.length > 0) {
|
|
172
|
+
// Refused here rather than at the service: the same refusal, one round trip
|
|
173
|
+
// earlier, and it can name every unmapped property at once instead of the
|
|
174
|
+
// first one the server happens to reach.
|
|
175
|
+
throw new CliError(
|
|
176
|
+
`${missing.length} propert${missing.length === 1 ? 'y is' : 'ies are'} unmapped: ${missing.join(', ')}.`,
|
|
177
|
+
{
|
|
178
|
+
code: 'USAGE',
|
|
179
|
+
hint: 'Every property names the column it reads. There is no name-match fallback.',
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result = await client.draftCommand({
|
|
185
|
+
kind: 'update_source_binding',
|
|
186
|
+
objectId: detail.id,
|
|
187
|
+
binding: {
|
|
188
|
+
kind: 'dataset',
|
|
189
|
+
datasetRevisionId: revision.id,
|
|
190
|
+
columnMappings,
|
|
191
|
+
...(ctx.flags['accept-contract-change'] === true ? { acceptContractChange: true } : {}),
|
|
192
|
+
},
|
|
193
|
+
}, await client.revision())
|
|
194
|
+
|
|
195
|
+
// The tree is told what the draft now says. `backing` is excluded from the diff
|
|
196
|
+
// — a rebind is this command's act, not `apply`'s — so nothing else would ever
|
|
197
|
+
// reconcile the file, and it would sit stale until the next `pull`.
|
|
198
|
+
const treeRoot = resolve(ctx.cwd, (ctx.flags.dir as string) ?? 'blueprint')
|
|
199
|
+
let treeUpdated = false
|
|
200
|
+
if (existsSync(join(treeRoot, 'object-types'))) {
|
|
201
|
+
const file = readTree(treeRoot).find(
|
|
202
|
+
(candidate) => candidate.kind === 'object-type' && candidate.apiName === apiName,
|
|
203
|
+
)
|
|
204
|
+
if (file) {
|
|
205
|
+
const written: Record<string, unknown> = {}
|
|
206
|
+
for (const entry of columnMappings) {
|
|
207
|
+
const name = properties.find((property) => property.id === entry.propertyId)?.apiName
|
|
208
|
+
if (!name) continue
|
|
209
|
+
written[name] = entry.field === undefined || entry.field === null
|
|
210
|
+
? entry.column
|
|
211
|
+
: { column: entry.column, field: entry.field }
|
|
212
|
+
}
|
|
213
|
+
writeTree(treeRoot, [{
|
|
214
|
+
...file,
|
|
215
|
+
document: { ...file.document, backing: { dataset: datasetName, mapping: written } },
|
|
216
|
+
}])
|
|
217
|
+
treeUpdated = true
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
data: { ...result, treeUpdated },
|
|
223
|
+
text: `Bound "${apiName}" to dataset "${datasetName}" `
|
|
224
|
+
+ `(revision ${revision.revision ?? revision.id}), ${columnMappings.length} properties mapped.`
|
|
225
|
+
+ (treeUpdated ? ` Updated ${apiName}.yaml to match.` : ''),
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
}
|