@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.
Files changed (48) hide show
  1. package/package.json +4 -2
  2. package/src/api/apps-api.ts +13 -1
  3. package/src/api/automation-api.ts +129 -1
  4. package/src/api/blueprint-authoring-api.ts +574 -0
  5. package/src/api/dataset-api.ts +199 -0
  6. package/src/api/platform-api.ts +300 -0
  7. package/src/automation-template.ts +224 -0
  8. package/src/blueprint/compile.ts +371 -0
  9. package/src/blueprint/dataset-revision.ts +33 -0
  10. package/src/blueprint/diff.ts +223 -0
  11. package/src/blueprint/model.ts +227 -0
  12. package/src/blueprint/projection.ts +254 -0
  13. package/src/blueprint/render.ts +73 -0
  14. package/src/blueprint/scaffold.ts +79 -0
  15. package/src/blueprint/tree.ts +121 -0
  16. package/src/commands/agent/index-commands.ts +87 -1
  17. package/src/commands/app/deploy.ts +43 -3
  18. package/src/commands/app/init.ts +23 -1
  19. package/src/commands/app/pull.ts +12 -35
  20. package/src/commands/automation/index-commands.ts +42 -1
  21. package/src/commands/automation/init.ts +52 -0
  22. package/src/commands/automation/project-root.ts +58 -0
  23. package/src/commands/automation/pull.ts +124 -0
  24. package/src/commands/automation/run.ts +271 -0
  25. package/src/commands/blueprint/authoring.ts +410 -0
  26. package/src/commands/blueprint/bind.ts +228 -0
  27. package/src/commands/blueprint/declarative.ts +1052 -0
  28. package/src/commands/blueprint/grants.ts +164 -0
  29. package/src/commands/dataset/index-commands.ts +431 -0
  30. package/src/commands/knowledge/index-commands.ts +278 -27
  31. package/src/commands/knowledge/upload-batch.ts +146 -0
  32. package/src/commands/knowledge/upload-plan.ts +127 -0
  33. package/src/commands/login.ts +49 -11
  34. package/src/commands/pack/index-commands.ts +373 -0
  35. package/src/commands/registry.ts +19 -2
  36. package/src/commands/secret/index-commands.ts +195 -0
  37. package/src/commands/skill/bundle-commands.ts +327 -0
  38. package/src/commands/skill/index-commands.ts +36 -42
  39. package/src/commands/skill/resolve.ts +34 -0
  40. package/src/dev-env.ts +114 -0
  41. package/src/flag-help.ts +34 -0
  42. package/src/harness.ts +30 -3
  43. package/src/main.ts +10 -3
  44. package/src/render-evidence.ts +152 -0
  45. package/src/template.ts +4 -0
  46. package/src/untar.ts +44 -0
  47. package/src/vendor/sdk-sources.json +13 -11
  48. 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
+ }