@frontera-sdk/cli 1.52.39 → 1.52.41
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/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +49 -5
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/draft-changes.ts +1 -1
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +332 -32
- package/src/commands/blueprint/editable.ts +3 -3
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +876 -89
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +321 -14
- package/src/commands/local/sync.ts +103 -6
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +54 -10
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/claude-commands.ts +11 -6
- package/src/forge/claude-skills.ts +4 -4
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +113 -2
- package/src/kit.ts +1 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +212 -22
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +181 -8
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +347 -36
- package/src/local/local-dev-sync.ts +141 -12
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-documents.ts +1 -1
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-lessons.ts +1 -1
- package/src/local/local-library.ts +1 -1
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +206 -8
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +143 -18
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +41 -11
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +3 -3
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-merge.ts +1 -1
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +19 -8
- package/src/local/local-sync-push.ts +15 -12
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +349 -38
- package/src/local/local-sync-snapshot.ts +18 -13
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/scopes.ts +1 -1
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +17 -14
|
@@ -40,7 +40,7 @@ export function nextEditableSelection(
|
|
|
40
40
|
const base = selection.set ?? [...current]
|
|
41
41
|
const removed = new Set(selection.remove ?? [])
|
|
42
42
|
const next = new Set([...base, ...(selection.add ?? [])].filter((name) => !removed.has(name)))
|
|
43
|
-
return [...next].sort()
|
|
43
|
+
return [...next].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function api(ctx: CommandContext): BlueprintAuthoringApi {
|
|
@@ -93,7 +93,7 @@ export const blueprintEditable: Command = {
|
|
|
93
93
|
// the way out. Mapping them through the property ids again read every one
|
|
94
94
|
// as unknown and reported an empty set — which would have let `--add` post
|
|
95
95
|
// a set containing only the addition, silently dropping the rest.
|
|
96
|
-
const current = [...detail.editableProperties].sort()
|
|
96
|
+
const current = [...detail.editableProperties].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
|
|
97
97
|
|
|
98
98
|
if (!hasSet && add.length === 0 && remove.length === 0) {
|
|
99
99
|
return {
|
|
@@ -119,7 +119,7 @@ export const blueprintEditable: Command = {
|
|
|
119
119
|
`${unknown.join(', ')} ${unknown.length === 1 ? 'is not a property' : 'are not properties'} of "${apiName}".`,
|
|
120
120
|
{
|
|
121
121
|
code: 'USAGE',
|
|
122
|
-
hint: `Properties: ${[...known].sort().join(', ')}`,
|
|
122
|
+
hint: `Properties: ${[...known].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0)).join(', ')}`,
|
|
123
123
|
},
|
|
124
124
|
)
|
|
125
125
|
}
|
|
@@ -124,7 +124,7 @@ export const blueprintGet: Command = {
|
|
|
124
124
|
},
|
|
125
125
|
],
|
|
126
126
|
flags: {},
|
|
127
|
-
summary: 'Show one object type: its properties, relations and metrics',
|
|
127
|
+
summary: 'Show one object type: its id, properties, relations and metrics',
|
|
128
128
|
examples: ['frontera blueprint get Shipment', 'frontera blueprint get shipment --json'],
|
|
129
129
|
},
|
|
130
130
|
|
|
@@ -221,6 +221,8 @@ export const blueprintGet: Command = {
|
|
|
221
221
|
const heading = [apiName, type.displayName].filter(Boolean)
|
|
222
222
|
const lines = [
|
|
223
223
|
heading[1] && heading[1] !== heading[0] ? `${heading[0]} — ${heading[1]}` : String(heading[0]),
|
|
224
|
+
// The runtime's id: what a hard-coded id in an App, a workflow or a test has to equal.
|
|
225
|
+
` id ${selfId || (types.ok ? '(not in the object-type catalog)' : `(unavailable: ${types.unavailable})`)}`,
|
|
224
226
|
...(type.description ? ['', type.description] : []),
|
|
225
227
|
'',
|
|
226
228
|
`Properties (${props.length})`,
|
|
@@ -254,7 +256,9 @@ export const blueprintGet: Command = {
|
|
|
254
256
|
|
|
255
257
|
return {
|
|
256
258
|
data: {
|
|
257
|
-
|
|
259
|
+
// `id` from the organization catalog; null when it could not be read or does not list the type.
|
|
260
|
+
objectType: { ...type, id: selfId || null },
|
|
261
|
+
id: selfId || null,
|
|
258
262
|
properties: props,
|
|
259
263
|
// `null` rather than `[]` for an unreadable section, and a sibling
|
|
260
264
|
// field saying why. A consumer that treats absence as emptiness now has
|
|
@@ -25,9 +25,11 @@ async function resolveSubject(
|
|
|
25
25
|
const byId = candidates.find((candidate) => candidate.id === nameOrId)
|
|
26
26
|
if (byId?.id) return { id: byId.id, name: byId.name ?? byId.id }
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// An Agent by its slug too (`agentId`, `dispatcher`), as every other command names it; a
|
|
29
|
+
// workspace by its slug. A slug names one, so it wins over a display name that matches.
|
|
30
|
+
const slugOf = (candidate: { slug?: string; agentId?: string }) => (subject === 'workspace' ? candidate.slug : candidate.agentId ?? candidate.slug)
|
|
31
|
+
const bySlug = candidates.filter((candidate) => slugOf(candidate as { slug?: string; agentId?: string }) === nameOrId)
|
|
32
|
+
const matches = bySlug.length ? bySlug : candidates.filter((candidate) => candidate.name === nameOrId)
|
|
31
33
|
const only = matches.length === 1 ? matches[0] : undefined
|
|
32
34
|
if (only?.id) return { id: only.id, name: only.name ?? only.id }
|
|
33
35
|
if (matches.length > 1) {
|
|
@@ -42,11 +44,47 @@ async function resolveSubject(
|
|
|
42
44
|
throw new CliError(`No ${subject} named "${nameOrId}" in this organization.`, {
|
|
43
45
|
code: 'NOT_FOUND',
|
|
44
46
|
hint: candidates.length
|
|
45
|
-
? `Known ${subject}s: ${candidates.map((candidate) => candidate.name).filter(Boolean).join(', ')}`
|
|
47
|
+
? `Known ${subject}s: ${candidates.map((candidate) => { const slug = slugOf(candidate as { slug?: string; agentId?: string }); return slug && slug !== candidate.name ? `${slug} (${candidate.name})` : candidate.name }).filter(Boolean).join(', ')}`
|
|
46
48
|
: `No ${subject}s are visible to this credential.`,
|
|
47
49
|
})
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* `--permissions`, as the grant's whole list: `read` always, `read_sensitive`
|
|
54
|
+
* the only other one. A workspace's only: an Agent's clearance is its
|
|
55
|
+
* bindings.json, and a revoke takes the whole grant.
|
|
56
|
+
*/
|
|
57
|
+
export function grantPermissions(raw: string | boolean | undefined, subject: string, revoke: boolean): string[] | undefined {
|
|
58
|
+
if (raw === undefined) return undefined
|
|
59
|
+
if (typeof raw !== 'string' || !raw.trim()) throw new CliError('--permissions takes a list: read,read_sensitive', { code: 'USAGE', hint: 'frontera blueprint grant workspace <name> <apiName…> --permissions read,read_sensitive' })
|
|
60
|
+
if (subject !== 'workspace') throw new CliError('--permissions is for a workspace grant', { code: 'USAGE', hint: 'an Agent\'s clearance over sensitive fields is `read_sensitive` in its bindings.json' })
|
|
61
|
+
if (revoke) throw new CliError('--revoke takes the whole grant; --permissions sets what a grant gives', { code: 'USAGE', hint: 'to keep only read, grant again with --permissions read' })
|
|
62
|
+
const given = raw.split(',').map((p) => p.trim()).filter(Boolean)
|
|
63
|
+
const unknown = given.filter((p) => p !== 'read' && p !== 'read_sensitive')
|
|
64
|
+
if (unknown.length) throw new CliError(`${unknown.join(', ')} ${unknown.length === 1 ? 'is not a permission' : 'are not permissions'} a workspace grant takes`, { code: 'USAGE', hint: 'the permissions are read and read_sensitive' })
|
|
65
|
+
return ['read', ...(given.includes('read_sensitive') ? ['read_sensitive'] : [])]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What a grant without `--permissions` sends, per object type: each grant
|
|
70
|
+
* replaces the whole permission list, so `read` plus whatever the grant
|
|
71
|
+
* already holds (`act`, `read_sensitive`), never `read` alone over a grant
|
|
72
|
+
* that had more. `kept` names what was kept, by object type.
|
|
73
|
+
*/
|
|
74
|
+
export function keptPermissions(apiNames: string[], current: Array<{ objectTypeApiName?: string; permissions?: string[] }>): { send: Map<string, string[] | undefined>; kept: Record<string, string[]> } {
|
|
75
|
+
const send = new Map<string, string[] | undefined>()
|
|
76
|
+
const kept: Record<string, string[]> = {}
|
|
77
|
+
for (const apiName of apiNames) {
|
|
78
|
+
const held = current.find((g) => g.objectTypeApiName === apiName)?.permissions ?? []
|
|
79
|
+
const extras = [...new Set(held.filter((p) => p !== 'read'))].sort()
|
|
80
|
+
if (extras.length) {
|
|
81
|
+
send.set(apiName, ['read', ...extras])
|
|
82
|
+
kept[apiName] = extras
|
|
83
|
+
} else send.set(apiName, undefined)
|
|
84
|
+
}
|
|
85
|
+
return { send, kept }
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
export const blueprintGrant: Command = {
|
|
51
89
|
meta: {
|
|
52
90
|
noun: 'blueprint',
|
|
@@ -56,10 +94,15 @@ export const blueprintGrant: Command = {
|
|
|
56
94
|
{ name: 'name', required: false, description: 'Workspace or agent, by name or id' },
|
|
57
95
|
{ name: 'apiName...', required: false, description: 'One or more object type API names' },
|
|
58
96
|
],
|
|
59
|
-
flags: { revoke: 'boolean' },
|
|
97
|
+
flags: { revoke: 'boolean', permissions: 'string' },
|
|
60
98
|
summary: 'Grant or revoke an object type for a workspace or an agent',
|
|
99
|
+
details: [
|
|
100
|
+
'`--permissions read,read_sensitive` gives a workspace clearance over the type\'s confidential and restricted fields, for the workflows, Functions and Apps that read through it (`read` is always included; `--permissions` sets the grant\'s whole list). An Agent\'s clearance is its bindings.json. In a local project, blueprint/workspace-grants.yaml declares it and `frontera local dev` applies it.',
|
|
101
|
+
'Without `--permissions`, a grant keeps what it already holds: read, plus the grant\'s `read_sensitive` or an Agent\'s `act` (submit Actions), and the output says what it kept. To take one away: `--permissions read` for a workspace; for an Agent, revoke the grant and grant it again.',
|
|
102
|
+
],
|
|
61
103
|
examples: [
|
|
62
104
|
'frontera blueprint grant workspace Ops Customer Policy',
|
|
105
|
+
'frontera blueprint grant workspace Ops Customer --permissions read_sensitive',
|
|
63
106
|
'frontera blueprint grant agent "Claims bot" Customer --revoke',
|
|
64
107
|
'frontera blueprint grant list',
|
|
65
108
|
],
|
|
@@ -145,15 +188,20 @@ export const blueprintGrant: Command = {
|
|
|
145
188
|
})
|
|
146
189
|
}
|
|
147
190
|
|
|
148
|
-
const resolved = await resolveSubject(client, subject, name)
|
|
149
191
|
const revoke = ctx.flags.revoke === true
|
|
192
|
+
const permissions = grantPermissions(ctx.flags.permissions, subject, revoke)
|
|
193
|
+
const resolved = await resolveSubject(client, subject, name)
|
|
194
|
+
// Each grant replaces the whole list: without --permissions, what each grant holds is read first and kept.
|
|
195
|
+
const { send, kept } = revoke || permissions
|
|
196
|
+
? { send: new Map<string, string[] | undefined>(apiNames.map((n) => [n, permissions])), kept: {} as Record<string, string[]> }
|
|
197
|
+
: keptPermissions(apiNames, subject === 'workspace' ? await client.workspaceGrants(resolved.id) : await client.agentGrants(resolved.id))
|
|
150
198
|
for (const apiName of apiNames) {
|
|
151
199
|
try {
|
|
152
200
|
if (subject === 'workspace') {
|
|
153
201
|
if (revoke) await client.revokeWorkspace(resolved.id, apiName)
|
|
154
|
-
else await client.grantWorkspace(resolved.id, apiName)
|
|
202
|
+
else await client.grantWorkspace(resolved.id, apiName, send.get(apiName))
|
|
155
203
|
} else if (revoke) await client.revokeAgent(resolved.id, apiName)
|
|
156
|
-
else await client.grantAgent(resolved.id, apiName)
|
|
204
|
+
else await client.grantAgent(resolved.id, apiName, send.get(apiName))
|
|
157
205
|
} catch (err) {
|
|
158
206
|
/**
|
|
159
207
|
* Grants address the PUBLISHED catalog, and the draft is a different
|
|
@@ -166,6 +214,10 @@ export const blueprintGrant: Command = {
|
|
|
166
214
|
* which step — a comprehension audit read the bare 404 as the whole
|
|
167
215
|
* pipeline being broken.
|
|
168
216
|
*/
|
|
217
|
+
// Grants are a Blueprint Steward's to give, `read_sensitive` above all: said as that.
|
|
218
|
+
if (err instanceof CliError && err.code === 'FORBIDDEN' && permissions?.includes('read_sensitive')) {
|
|
219
|
+
throw new CliError(`${err.message} Granting ${apiName} with read_sensitive needs a Blueprint Steward.`, { code: err.code, hint: 'run it with the credential of a Blueprint Steward of this organization', cause: err })
|
|
220
|
+
}
|
|
169
221
|
const notFound = err instanceof CliError && err.code === 'NOT_FOUND'
|
|
170
222
|
if (!notFound) throw err
|
|
171
223
|
const onDraft = await client.getObjectType(apiName).then((t) => Boolean(t?.id)).catch(() => false)
|
|
@@ -182,10 +234,12 @@ export const blueprintGrant: Command = {
|
|
|
182
234
|
}
|
|
183
235
|
}
|
|
184
236
|
|
|
237
|
+
const keptText = Object.entries(kept).map(([apiName, extras]) => `${extras.join(' and ')} on ${apiName}`).join('; ')
|
|
185
238
|
return {
|
|
186
|
-
data: { subject, id: resolved.id, name: resolved.name, apiNames, revoked: revoke },
|
|
239
|
+
data: { subject, id: resolved.id, name: resolved.name, apiNames, revoked: revoke, ...(permissions ? { permissions } : {}), ...(keptText ? { kept } : {}) },
|
|
187
240
|
text: `${revoke ? 'Revoked' : 'Granted'} ${apiNames.join(', ')} `
|
|
188
|
-
+ `${revoke ? 'from' : 'to'} ${subject} "${resolved.name}"
|
|
241
|
+
+ `${revoke ? 'from' : 'to'} ${subject} "${resolved.name}"${permissions ? ` (${permissions.join(', ')})` : ''}.`
|
|
242
|
+
+ (keptText ? ` Kept what the grant${Object.keys(kept).length === 1 ? '' : 's'} already held: ${keptText} (\`--permissions\` sets the whole list).` : ''),
|
|
189
243
|
}
|
|
190
244
|
},
|
|
191
245
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { PlatformApi } from '../../api/platform-api'
|
|
2
|
+
import { METRIC_GRAINS, metricQueryBody, metricRows, resolveRangeWindow, timeseriesOf, type RangeWindow } from '../../blueprint/metric-query'
|
|
3
|
+
import { UsageError } from '../../errors'
|
|
4
|
+
import { table } from '../../table'
|
|
5
|
+
import { flagBool, flagString, type Command } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A metric's numbers, through the same route an App's `useMetric` and an
|
|
9
|
+
* Agent's `blueprint_query_metric` read: so a metric just authored can be
|
|
10
|
+
* checked from the CLI, as `blueprint query` checks an object type's rows.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const list = (raw: string | undefined) => (raw === undefined ? undefined : raw.split(',').map((s) => s.trim()).filter(Boolean))
|
|
14
|
+
|
|
15
|
+
function cell(value: unknown): string {
|
|
16
|
+
if (value === null || value === undefined) return ''
|
|
17
|
+
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(4)))
|
|
18
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
19
|
+
return String(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const blueprintMetric: Command = {
|
|
23
|
+
meta: {
|
|
24
|
+
noun: 'blueprint',
|
|
25
|
+
verb: 'metric',
|
|
26
|
+
// Reads the slice granted to ONE workspace, as `blueprint query` does.
|
|
27
|
+
scope: 'workspace' as const,
|
|
28
|
+
args: [
|
|
29
|
+
{ name: 'query', required: true, description: 'query (the one action)' },
|
|
30
|
+
{ name: 'apiName', required: true, description: 'the metric, from `frontera blueprint list` or blueprint/metrics/<apiName>.yaml' },
|
|
31
|
+
],
|
|
32
|
+
flags: { measure: 'string', 'group-by': 'string', grain: 'string', range: 'string', start: 'string', end: 'string', compare: 'boolean' },
|
|
33
|
+
summary: 'Query the numbers of a Blueprint metric: by grain, over a time window, grouped by its dimensions',
|
|
34
|
+
details: [
|
|
35
|
+
'Every measure the metric defines, unless `--measure a,b` names some. `--group-by d1,d2` breaks the numbers down by the metric\'s dimensions.',
|
|
36
|
+
`\`--grain\` (${METRIC_GRAINS.join(', ')}) gives a series: the metric's timeseries dimension is added to the group-by. \`--range P7D\` is a window as an ISO-8601 duration, counted back from the newest value of the metric's timeseries in the data, not from now (so a load that runs behind does not empty it); the answer says the window it covered. \`--start\` and \`--end\` give a fixed one (a date reads as its midnight UTC; the end is exclusive). \`--compare\` adds the window before: <measure>_prev, <measure>_delta and <measure>_pct.`,
|
|
37
|
+
'Reads what the workspace is granted, as `blueprint query` does: an unknown or ungranted metric is NOT_FOUND. A metric the runtime is still building says so; run it again shortly.',
|
|
38
|
+
],
|
|
39
|
+
examples: [
|
|
40
|
+
'frontera blueprint metric query workOrderBacklog',
|
|
41
|
+
'frontera blueprint metric query workOrderBacklog --group-by state,priority',
|
|
42
|
+
'frontera blueprint metric query workOrdersOpened --grain day --range P7D',
|
|
43
|
+
'frontera blueprint metric query workOrdersOpened --range P7D --compare',
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async run(ctx) {
|
|
48
|
+
const [first, second, ...rest] = ctx.positional
|
|
49
|
+
// `metric <apiName>` reads as `metric query <apiName>`.
|
|
50
|
+
const apiName = first === 'query' ? second : first
|
|
51
|
+
const extra = first === 'query' ? rest : [second, ...rest].filter((x) => x !== undefined)
|
|
52
|
+
if (!apiName) throw new UsageError('missing <apiName>', 'frontera blueprint list — then `frontera blueprint metric query <apiName>`')
|
|
53
|
+
if (extra.length) throw new UsageError(`unexpected ${extra.join(' ')}`, 'name one metric: `frontera blueprint metric query <apiName> [--group-by …]`')
|
|
54
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
55
|
+
const grain = flagString(ctx, 'grain')
|
|
56
|
+
// The timeseries dimension, read only when a series is asked for.
|
|
57
|
+
let timeseries: string | null | undefined
|
|
58
|
+
let metricRow: unknown
|
|
59
|
+
if (grain !== undefined) {
|
|
60
|
+
metricRow = await api.blueprintMetric(apiName).catch(async () => {
|
|
61
|
+
const all = (await api.blueprintMetrics().catch(() => [])) as Array<{ apiName?: string }>
|
|
62
|
+
return all.find((m) => m?.apiName === apiName) ?? null
|
|
63
|
+
})
|
|
64
|
+
timeseries = metricRow ? timeseriesOf(metricRow) : undefined
|
|
65
|
+
}
|
|
66
|
+
let body
|
|
67
|
+
try {
|
|
68
|
+
body = metricQueryBody({
|
|
69
|
+
measures: list(flagString(ctx, 'measure')),
|
|
70
|
+
dimensions: list(flagString(ctx, 'group-by')),
|
|
71
|
+
grain,
|
|
72
|
+
range: flagString(ctx, 'range'),
|
|
73
|
+
from: flagString(ctx, 'start'),
|
|
74
|
+
to: flagString(ctx, 'end'),
|
|
75
|
+
compare: flagBool(ctx, 'compare') || undefined,
|
|
76
|
+
}, timeseries)
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new UsageError((err as Error).message.replace(/^(grain|range|compare)\b/, '--$1').replace(/^from\b/, '--start').replace(/^to\b/, '--end').replace(/give a range \(P7D\) or from and to/, 'give --range (P7D) or --start and --end').replace(/as a range \(P7D\) or as from and to/, 'as --range (P7D) or as --start and --end'), 'e.g. `frontera blueprint metric query <apiName> --grain day --range P7D`')
|
|
79
|
+
}
|
|
80
|
+
const result = await api.blueprintMetricQuery(apiName, body)
|
|
81
|
+
const rows = metricRows(result)
|
|
82
|
+
// A `--range` window ends at the newest record, not at now: say which window it was.
|
|
83
|
+
const isoDuration = body.timeRange?.isoDuration
|
|
84
|
+
const window: RangeWindow | undefined = isoDuration
|
|
85
|
+
? await resolveRangeWindow(isoDuration, {
|
|
86
|
+
metric: async () => metricRow ?? api.blueprintMetric(apiName),
|
|
87
|
+
objectTypes: () => api.blueprintObjectTypes(),
|
|
88
|
+
aggregate: (aggregate) => api.blueprintAggregate(aggregate),
|
|
89
|
+
})
|
|
90
|
+
: undefined
|
|
91
|
+
const data = window ? (Array.isArray(result) ? { data: result, window } : { ...result, window }) : result
|
|
92
|
+
const compared = !Array.isArray(result) ? (result?.compareWindow as { start?: string; end?: string } | undefined) : undefined
|
|
93
|
+
const windowLine = (window ? `\n Window: ${window.note}` : '')
|
|
94
|
+
+ (compared?.start && compared.end ? `\n Compared with: ${compared.start} up to ${compared.end} (the _prev, _delta and _pct columns).` : '')
|
|
95
|
+
if (!rows.length) {
|
|
96
|
+
return {
|
|
97
|
+
data,
|
|
98
|
+
text: `No rows for ${apiName}${body.timeRange ? ' in that window' : ''}.${windowLine}\n The metric's object type may have no records yet; \`frontera blueprint query <ObjectType> --limit 5\` shows whether it has.`,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const columns = [...new Set(rows.flatMap((r) => Object.keys(r)))]
|
|
102
|
+
const truncated = !Array.isArray(result) && result?.truncated ? '\n Truncated: narrow the window or group by fewer dimensions.' : ''
|
|
103
|
+
return {
|
|
104
|
+
data,
|
|
105
|
+
text: `${table(columns, rows.map((r) => columns.map((c) => cell(r[c]))))}\n\n${rows.length} row${rows.length === 1 ? '' : 's'}.${windowLine}${truncated}`,
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { PlatformApi } from '../../api/platform-api'
|
|
2
|
+
import { findObjectSet, normalizeObjectSets, projectObjectSetNames, type SavedObjectSet } from '../../blueprint/object-sets'
|
|
3
|
+
import { CliError } from '../../errors'
|
|
4
|
+
import { findSolutionRoot } from '../../local/local-project'
|
|
5
|
+
import { table } from '../../table'
|
|
6
|
+
import type { Command, CommandContext } from '../types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The saved object sets this credential can read: name, id, object type.
|
|
10
|
+
*
|
|
11
|
+
* A set is saved from a project file (`blueprint/object-sets/<name>.yaml`) by
|
|
12
|
+
* `local dev`, or in the Console, and a file or an App names it. An App's
|
|
13
|
+
* code keeps the id, a workflow file or a query the name: this is where both
|
|
14
|
+
* are read.
|
|
15
|
+
*/
|
|
16
|
+
export async function savedObjectSets(ctx: Pick<CommandContext, 'apiUrl' | 'token' | 'workspaceId'> & { cwd?: string }): Promise<SavedObjectSet[]> {
|
|
17
|
+
// In a project, a set a file saves is referenced by that file's name, as `status` names it.
|
|
18
|
+
const files = ctx.cwd ? projectObjectSetNames(projectRoot(ctx.cwd)) : new Map<string, string>()
|
|
19
|
+
return normalizeObjectSets(await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).blueprintObjectSets(), files)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The project's root when the working directory is in one with object set files; else null. */
|
|
23
|
+
function projectRoot(cwd: string): string | null {
|
|
24
|
+
try {
|
|
25
|
+
return findSolutionRoot(cwd)
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The set a flag names, by id, name or reference; a refusal that lists the sets otherwise. */
|
|
32
|
+
export async function resolveObjectSet(ctx: Pick<CommandContext, 'apiUrl' | 'token' | 'workspaceId' | 'cwd'>, wanted: string, sets?: SavedObjectSet[]): Promise<SavedObjectSet> {
|
|
33
|
+
const all = sets ?? (await savedObjectSets(ctx))
|
|
34
|
+
const found = findObjectSet(all, wanted, projectObjectSetNames(projectRoot(ctx.cwd)))
|
|
35
|
+
if ('set' in found) return found.set
|
|
36
|
+
const listed = (found.ambiguous ?? all).slice(0, 8).map((s) => `${s.name} (${s.ref}, ${s.id})`)
|
|
37
|
+
throw new CliError(found.problem, {
|
|
38
|
+
code: found.ambiguous ? 'AMBIGUOUS_OBJECT_SET' : 'UNKNOWN_OBJECT_SET',
|
|
39
|
+
hint: all.length
|
|
40
|
+
? `${found.ambiguous ? 'pass its id; they are' : 'the sets here are'}: ${listed.join('; ')}${all.length > 8 && !found.ambiguous ? ` and ${all.length - 8} more` : ''} — \`frontera blueprint object-sets\` lists them`
|
|
41
|
+
: 'no saved object set is readable here: add blueprint/object-sets/<name>.yaml and run `frontera local dev`, or save one in the Console',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const blueprintObjectSetsList: Command = {
|
|
46
|
+
meta: {
|
|
47
|
+
noun: 'blueprint',
|
|
48
|
+
verb: 'object-sets',
|
|
49
|
+
scope: 'workspace' as const,
|
|
50
|
+
args: [],
|
|
51
|
+
flags: {},
|
|
52
|
+
summary: 'List the saved object sets: name, id, object type and the reference a file names it by',
|
|
53
|
+
details: [
|
|
54
|
+
'Pass a set to `frontera blueprint query --object-set <name|ref|id>` to read its rows. A workflow file names it as `objectSet/<ref>` or by its name; an App keeps its id.',
|
|
55
|
+
],
|
|
56
|
+
examples: ['frontera blueprint object-sets', 'frontera blueprint object-sets --json'],
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async run(ctx) {
|
|
60
|
+
const sets = await savedObjectSets(ctx)
|
|
61
|
+
return {
|
|
62
|
+
data: sets,
|
|
63
|
+
text: sets.length
|
|
64
|
+
? table(
|
|
65
|
+
['name', 'id', 'object type', 'kind', 'reference'],
|
|
66
|
+
sets.map((s) => [s.name, s.id, s.objectType, s.kind, s.ref]),
|
|
67
|
+
)
|
|
68
|
+
: 'No saved object sets are readable with this credential. Add blueprint/object-sets/<name>.yaml and run `frontera local dev`, or save one in the Console.',
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}
|