@frontera-sdk/cli 1.44.1 → 1.45.0
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 +65 -1
- package/package.json +4 -3
- package/src/api/automation-api.ts +15 -0
- package/src/api/dataset-api.ts +99 -0
- package/src/api/governed-action-api.ts +80 -0
- package/src/api/platform-api.ts +293 -0
- package/src/auth-verify.ts +105 -0
- package/src/binding-registry.ts +87 -0
- package/src/commands/action/deploy.ts +1 -0
- package/src/commands/action/grant.ts +1 -0
- package/src/commands/action/index-commands.ts +8 -0
- package/src/commands/action/prepare.ts +1 -0
- package/src/commands/action/requests.ts +111 -0
- package/src/commands/action/review.ts +1 -0
- package/src/commands/agent/index-commands.ts +189 -7
- package/src/commands/app/init.ts +1 -1
- package/src/commands/app/pull.ts +1 -1
- package/src/commands/auth/add.ts +145 -0
- package/src/commands/auth/current.ts +82 -0
- package/src/commands/auth/index-commands.ts +16 -0
- package/src/commands/auth/list.ts +71 -0
- package/src/commands/auth/remove.ts +80 -0
- package/src/commands/auth/use.ts +84 -0
- package/src/commands/auth/verify.ts +93 -0
- package/src/commands/automation/run.ts +41 -2
- package/src/commands/blueprint/query.ts +294 -0
- package/src/commands/capability/index-commands.ts +334 -0
- package/src/commands/dataset/index-commands.ts +103 -14
- package/src/commands/kit/doctor.ts +101 -0
- package/src/commands/kit/index-commands.ts +7 -0
- package/src/commands/kit/shared.ts +52 -0
- package/src/commands/kit/status.ts +92 -0
- package/src/commands/kit/sync.ts +106 -0
- package/src/commands/kit/vendor.ts +120 -0
- package/src/commands/knowledge/index-commands.ts +165 -0
- package/src/commands/login.ts +64 -84
- package/src/commands/plugin/index-commands.ts +284 -21
- package/src/commands/registry.ts +104 -1
- package/src/commands/setup.ts +248 -0
- package/src/commands/source/index-commands.ts +446 -0
- package/src/commands/types.ts +14 -0
- package/src/config.ts +197 -100
- package/src/credential-store.ts +273 -0
- package/src/dev-env.ts +3 -3
- package/src/exit.ts +29 -2
- package/src/flag-help.ts +65 -3
- package/src/fs-atomic.ts +44 -0
- package/src/harness.ts +155 -4
- package/src/kit.ts +419 -0
- package/src/main.ts +13 -1
- package/src/paths.ts +43 -0
- package/src/profile-migration.ts +101 -0
- package/src/profiles.ts +240 -0
- package/src/project-context.ts +178 -0
- package/src/prompt.ts +23 -0
- package/src/templates/next-app-files.ts +4 -1
- package/src/vendor/kit-assets.json +31 -0
- package/src/vendor/sdk-sources.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
1
3
|
import { PlatformApi } from '../../api/platform-api'
|
|
4
|
+
import { UsageError } from '../../errors'
|
|
2
5
|
import { table } from '../../table'
|
|
3
|
-
import type
|
|
6
|
+
import { flagString, type Command } from '../types'
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* `/v1/apps` returns `{ install, catalog, … }` per row, not a flattened
|
|
@@ -78,35 +81,295 @@ const catalog: Command = {
|
|
|
78
81
|
},
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
const get: Command = {
|
|
85
|
+
meta: {
|
|
86
|
+
noun: 'plugin',
|
|
87
|
+
verb: 'get',
|
|
88
|
+
args: [{ name: 'plugin', required: true, description: 'install id, from `frontera plugin list`' }],
|
|
89
|
+
flags: {},
|
|
90
|
+
summary: 'Show one installed plugin — its configuration and connection state',
|
|
91
|
+
examples: ['frontera plugin get <installId>', 'frontera plugin get <installId> --json'],
|
|
92
|
+
},
|
|
93
|
+
async run(ctx) {
|
|
94
|
+
const installId = ctx.positional[0]
|
|
95
|
+
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list')
|
|
96
|
+
|
|
97
|
+
const install = await new PlatformApi(ctx.apiUrl, ctx.token).plugin(installId)
|
|
98
|
+
return {
|
|
99
|
+
data: install,
|
|
100
|
+
text: table(
|
|
101
|
+
['field', 'value'],
|
|
102
|
+
Object.entries(install).map(([k, v]) => [
|
|
103
|
+
k,
|
|
104
|
+
v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v),
|
|
105
|
+
]),
|
|
106
|
+
[undefined, 70],
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const add: Command = {
|
|
113
|
+
meta: {
|
|
114
|
+
noun: 'plugin',
|
|
115
|
+
verb: 'add',
|
|
116
|
+
args: [{ name: 'kind', required: true, description: 'catalog kind, from `frontera plugin catalog`' }],
|
|
117
|
+
flags: { name: 'string', file: 'string' },
|
|
118
|
+
aliases: { f: 'file' },
|
|
119
|
+
summary: 'Install a plugin from the catalog',
|
|
120
|
+
examples: [
|
|
121
|
+
'frontera plugin add slack --name "Support Slack"',
|
|
122
|
+
'frontera plugin add postgres --file ./config.json',
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
async run(ctx) {
|
|
126
|
+
const kind = ctx.positional[0]
|
|
127
|
+
if (!kind) throw new UsageError('missing <kind>', 'frontera plugin catalog — then pass a kind')
|
|
128
|
+
|
|
129
|
+
// Configuration by file, never by flag: a connector config carries
|
|
130
|
+
// credentials, and a flag lands in shell history and the process list.
|
|
131
|
+
// Same rule `secret set` states for itself.
|
|
132
|
+
const file = flagString(ctx, 'file')
|
|
133
|
+
let config: Record<string, unknown> | undefined
|
|
134
|
+
if (file) {
|
|
135
|
+
const raw = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8')
|
|
136
|
+
try {
|
|
137
|
+
config = JSON.parse(raw) as Record<string, unknown>
|
|
138
|
+
} catch (e) {
|
|
139
|
+
throw new UsageError(
|
|
140
|
+
`${file === '-' ? 'stdin' : file} is not valid JSON: ${(e as Error).message}`,
|
|
141
|
+
'the file holds the connector config object for this plugin kind',
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token).createPluginInstall({
|
|
147
|
+
kind,
|
|
148
|
+
...(flagString(ctx, 'name') ? { installName: flagString(ctx, 'name')! } : {}),
|
|
149
|
+
...(config ? { config } : {}),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// An OAuth-backed install answers with a URL and is inert until a person
|
|
153
|
+
// opens it. Reporting "installed" without saying so would leave a caller
|
|
154
|
+
// wiring capabilities onto something that cannot connect.
|
|
155
|
+
const authUrl = (result as { authUrl?: string }).authUrl
|
|
156
|
+
return {
|
|
157
|
+
data: result,
|
|
158
|
+
text: authUrl
|
|
159
|
+
? `Installed ${kind}, and it is NOT usable yet.\n`
|
|
160
|
+
+ ` Open this to authorize it:\n ${authUrl}\n`
|
|
161
|
+
+ ' Then `frontera plugin verify <installId>`.'
|
|
162
|
+
: `Installed ${kind}.\n`
|
|
163
|
+
+ ' `frontera plugin verify <installId>` — confirm it can reach the far end.\n'
|
|
164
|
+
+ ' `frontera capability available <installId>` — what it now offers.',
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const configure: Command = {
|
|
170
|
+
meta: {
|
|
171
|
+
noun: 'plugin',
|
|
172
|
+
verb: 'configure',
|
|
173
|
+
args: [{ name: 'plugin', required: true, description: 'install id, from `frontera plugin list`' }],
|
|
174
|
+
flags: { name: 'string', file: 'string' },
|
|
175
|
+
aliases: { f: 'file' },
|
|
176
|
+
summary: 'Rename an install, or replace its stored configuration',
|
|
177
|
+
examples: [
|
|
178
|
+
'frontera plugin configure <installId> --name "Support Slack"',
|
|
179
|
+
'frontera plugin configure <installId> --file ./config.json',
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
async run(ctx) {
|
|
183
|
+
const installId = ctx.positional[0]
|
|
184
|
+
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list')
|
|
185
|
+
|
|
186
|
+
const name = flagString(ctx, 'name')
|
|
187
|
+
const file = flagString(ctx, 'file')
|
|
188
|
+
if (!name && !file) {
|
|
189
|
+
throw new UsageError(
|
|
190
|
+
'nothing to change',
|
|
191
|
+
'pass --name to rename, or --file to replace the configuration',
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let config: Record<string, unknown> | undefined
|
|
196
|
+
if (file) {
|
|
197
|
+
const raw = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8')
|
|
198
|
+
try {
|
|
199
|
+
config = JSON.parse(raw) as Record<string, unknown>
|
|
200
|
+
} catch (e) {
|
|
201
|
+
throw new UsageError(
|
|
202
|
+
`${file === '-' ? 'stdin' : file} is not valid JSON: ${(e as Error).message}`,
|
|
203
|
+
'the file holds the connector config object — `frontera plugin get <installId>` shows the current one',
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token).updatePluginInstall(installId, {
|
|
209
|
+
...(name ? { installName: name } : {}),
|
|
210
|
+
...(config ? { config } : {}),
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
data: result,
|
|
215
|
+
text: `Updated ${installId}.`
|
|
216
|
+
+ (config ? '\n Configuration replaced — `frontera plugin verify` to confirm it still connects.' : ''),
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Policy is separate from configuration on purpose.
|
|
223
|
+
*
|
|
224
|
+
* `configure` changes how the install reaches the far end. This changes what
|
|
225
|
+
* the platform will let it do once it gets there — including `read_only`,
|
|
226
|
+
* which refuses every Action capability grant across every agent bound to it.
|
|
227
|
+
* Granting a capability against a read-only install fails with a conflict that
|
|
228
|
+
* no retry fixes, and this is the verb that fixes it.
|
|
229
|
+
*/
|
|
230
|
+
const policy: Command = {
|
|
231
|
+
meta: {
|
|
232
|
+
noun: 'plugin',
|
|
233
|
+
verb: 'policy',
|
|
234
|
+
args: [{ name: 'plugin', required: true, description: 'install id, from `frontera plugin list`' }],
|
|
235
|
+
flags: { enable: 'boolean', disable: 'boolean', actions: 'string', 'new-capability': 'string' },
|
|
236
|
+
summary: 'Set an install’s policy — enabled, Action policy, new-capability default',
|
|
237
|
+
examples: [
|
|
238
|
+
'frontera plugin policy <installId> --actions allow_all',
|
|
239
|
+
'frontera plugin policy <installId> --disable',
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
async run(ctx) {
|
|
243
|
+
const installId = ctx.positional[0]
|
|
244
|
+
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list')
|
|
245
|
+
|
|
246
|
+
const enable = ctx.flags.enable === true
|
|
247
|
+
const disable = ctx.flags.disable === true
|
|
248
|
+
if (enable && disable) {
|
|
249
|
+
throw new UsageError('--enable and --disable contradict each other', 'pass one of them')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const actions = flagString(ctx, 'actions')
|
|
253
|
+
const ACTION_POLICIES = ['allow_all', 'read_only', 'custom'] as const
|
|
254
|
+
if (actions && !ACTION_POLICIES.includes(actions as never)) {
|
|
255
|
+
throw new UsageError(
|
|
256
|
+
`--actions must be one of ${ACTION_POLICIES.join(', ')}, not "${actions}"`,
|
|
257
|
+
'read_only refuses every Action capability grant on this install',
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const newCapability = flagString(ctx, 'new-capability')
|
|
262
|
+
if (newCapability && newCapability !== 'enabled' && newCapability !== 'disabled') {
|
|
263
|
+
throw new UsageError(
|
|
264
|
+
`--new-capability must be enabled or disabled, not "${newCapability}"`,
|
|
265
|
+
'it decides what a capability discovered later defaults to; disabled is the safe choice',
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!enable && !disable && !actions && !newCapability) {
|
|
270
|
+
throw new UsageError('nothing to change', 'pass --enable, --disable, --actions or --new-capability')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token).patchPluginInstall(installId, {
|
|
274
|
+
...(enable ? { enabled: true } : {}),
|
|
275
|
+
...(disable ? { enabled: false } : {}),
|
|
276
|
+
...(actions ? { actionPolicy: actions as 'allow_all' | 'read_only' | 'custom' } : {}),
|
|
277
|
+
...(newCapability ? { newCapabilityDefault: newCapability as 'enabled' | 'disabled' } : {}),
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
return { data: result, text: `Policy updated on ${installId}.` }
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const verify: Command = {
|
|
285
|
+
meta: {
|
|
286
|
+
noun: 'plugin',
|
|
287
|
+
verb: 'verify',
|
|
288
|
+
args: [{ name: 'plugin', required: true, description: 'install id, from `frontera plugin list`' }],
|
|
289
|
+
flags: {},
|
|
290
|
+
summary: 'Check that an install can reach the far end',
|
|
291
|
+
examples: ['frontera plugin verify <installId>'],
|
|
292
|
+
},
|
|
293
|
+
async run(ctx) {
|
|
294
|
+
const installId = ctx.positional[0]
|
|
295
|
+
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list')
|
|
296
|
+
|
|
297
|
+
// A failed check still answers 200 — the verdict is in the body, the same
|
|
298
|
+
// shape `dataset test-source` established. Exiting non-zero here would
|
|
299
|
+
// make an unreachable plugin indistinguishable from an unreachable API.
|
|
300
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token).verifyPluginInstall(installId)
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
data: result,
|
|
304
|
+
text: table(
|
|
305
|
+
['field', 'value'],
|
|
306
|
+
Object.entries(result).map(([k, v]) => [
|
|
307
|
+
k,
|
|
308
|
+
v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v),
|
|
309
|
+
]),
|
|
310
|
+
[undefined, 70],
|
|
311
|
+
),
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const remove: Command = {
|
|
317
|
+
meta: {
|
|
318
|
+
noun: 'plugin',
|
|
319
|
+
verb: 'remove',
|
|
320
|
+
args: [{ name: 'plugin', required: true, description: 'install id, from `frontera plugin list`' }],
|
|
321
|
+
flags: {},
|
|
322
|
+
summary: 'Uninstall a plugin from this workspace',
|
|
323
|
+
examples: ['frontera plugin remove <installId>'],
|
|
324
|
+
},
|
|
325
|
+
async run(ctx) {
|
|
326
|
+
const installId = ctx.positional[0]
|
|
327
|
+
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list')
|
|
328
|
+
|
|
329
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token).deletePluginInstall(installId)
|
|
330
|
+
return {
|
|
331
|
+
data: result,
|
|
332
|
+
text: `Uninstalled ${installId}.\n`
|
|
333
|
+
+ ' Every agent bound to it loses the capabilities it carried.',
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
|
|
81
338
|
/**
|
|
82
|
-
* `connect`
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
339
|
+
* `connect` stays reserved, alone among the five.
|
|
340
|
+
*
|
|
341
|
+
* It needs the headless consent flow — print the authorization URL, poll,
|
|
342
|
+
* `--no-wait` to hand off — because the Computer has no browser, and shipping
|
|
343
|
+
* it without that produces a command that hangs in exactly the environment it
|
|
344
|
+
* exists for. `plugin add` prints the URL when an install needs one, which
|
|
345
|
+
* covers the start of the handshake; finishing it is what remains.
|
|
86
346
|
*/
|
|
87
|
-
const RESERVED = '
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
['add', 'Install a plugin from the catalog'],
|
|
92
|
-
['configure', 'Set a plugin’s configuration or credentials'],
|
|
93
|
-
['connect', 'Complete an OAuth connection'],
|
|
94
|
-
['verify', 'Check a plugin’s connection'],
|
|
95
|
-
['remove', 'Uninstall a plugin'],
|
|
96
|
-
] as const
|
|
97
|
-
).map(([verb, summary]) => ({
|
|
347
|
+
const RESERVED = 'completing an OAuth handshake needs a browser the CLI does not have; '
|
|
348
|
+
+ '`frontera plugin add` prints the authorization URL to open'
|
|
349
|
+
|
|
350
|
+
const connect: Command = {
|
|
98
351
|
meta: {
|
|
99
352
|
noun: 'plugin',
|
|
100
|
-
verb,
|
|
353
|
+
verb: 'connect',
|
|
101
354
|
args: [],
|
|
102
355
|
flags: {},
|
|
103
|
-
summary,
|
|
104
|
-
examples: [
|
|
356
|
+
summary: 'Complete an OAuth connection',
|
|
357
|
+
examples: ['frontera plugin connect <installId>'],
|
|
105
358
|
reserved: RESERVED,
|
|
106
359
|
},
|
|
107
360
|
async run(): Promise<never> {
|
|
108
361
|
throw new Error(RESERVED)
|
|
109
362
|
},
|
|
110
|
-
}
|
|
363
|
+
}
|
|
111
364
|
|
|
112
|
-
export const pluginCommands: Command[] = [
|
|
365
|
+
export const pluginCommands: Command[] = [
|
|
366
|
+
list,
|
|
367
|
+
get,
|
|
368
|
+
catalog,
|
|
369
|
+
add,
|
|
370
|
+
configure,
|
|
371
|
+
policy,
|
|
372
|
+
verify,
|
|
373
|
+
remove,
|
|
374
|
+
connect,
|
|
375
|
+
]
|
package/src/commands/registry.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { appSdk } from './app/sdk'
|
|
|
16
16
|
import { appDev } from './app/dev'
|
|
17
17
|
import { blueprintList } from './blueprint/list'
|
|
18
18
|
import { blueprintGet } from './blueprint/get'
|
|
19
|
+
import { blueprintInstance, blueprintQuery } from './blueprint/query'
|
|
19
20
|
import { blueprintGenerateTypes } from './blueprint/generate-types'
|
|
20
21
|
import { blueprintAuthoringCommands } from './blueprint/authoring'
|
|
21
22
|
import { blueprintDeclarativeCommands } from './blueprint/declarative'
|
|
@@ -26,7 +27,9 @@ import { actionCommands } from './action/index-commands'
|
|
|
26
27
|
import { agentCommands } from './agent/index-commands'
|
|
27
28
|
import { skillCommands } from './skill/index-commands'
|
|
28
29
|
import { pluginCommands } from './plugin/index-commands'
|
|
30
|
+
import { capabilityCommands } from './capability/index-commands'
|
|
29
31
|
import { datasetCommands } from './dataset/index-commands'
|
|
32
|
+
import { sourceCommands } from './source/index-commands'
|
|
30
33
|
import { knowledgeCommands } from './knowledge/index-commands'
|
|
31
34
|
import { packCommands } from './pack/index-commands'
|
|
32
35
|
import { secretCommands } from './secret/index-commands'
|
|
@@ -34,6 +37,9 @@ import { automationCommands } from './automation/index-commands'
|
|
|
34
37
|
import { completionCommand } from './completion'
|
|
35
38
|
import { initCommand } from './init'
|
|
36
39
|
import { loginCommand } from './login'
|
|
40
|
+
import { setupCommand } from './setup'
|
|
41
|
+
import { authCommands } from './auth/index-commands'
|
|
42
|
+
import { kitCommands } from './kit/index-commands'
|
|
37
43
|
|
|
38
44
|
/**
|
|
39
45
|
* Flags every command accepts. Declared once so a caller never has to wonder
|
|
@@ -45,6 +51,10 @@ export const GLOBAL_FLAGS: FlagSpec = {
|
|
|
45
51
|
yes: 'boolean',
|
|
46
52
|
help: 'boolean',
|
|
47
53
|
'api-url': 'string',
|
|
54
|
+
// Global because the override has to work on EVERY command, not only the
|
|
55
|
+
// auth ones — a one-command escape hatch is useless if it exists on six
|
|
56
|
+
// commands and the seventh is the one being diagnosed.
|
|
57
|
+
profile: 'string',
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
/** Additionally accepted by any command that resolves a project. */
|
|
@@ -52,9 +62,13 @@ export const PROJECT_FLAGS: FlagSpec = { dir: 'string' }
|
|
|
52
62
|
|
|
53
63
|
export const COMMANDS: readonly Command[] = [
|
|
54
64
|
initCommand,
|
|
65
|
+
setupCommand,
|
|
55
66
|
loginCommand,
|
|
56
67
|
completionCommand,
|
|
57
68
|
|
|
69
|
+
...authCommands,
|
|
70
|
+
...kitCommands,
|
|
71
|
+
|
|
58
72
|
appInit,
|
|
59
73
|
appList,
|
|
60
74
|
appAdd,
|
|
@@ -69,7 +83,9 @@ export const COMMANDS: readonly Command[] = [
|
|
|
69
83
|
...agentCommands,
|
|
70
84
|
...skillCommands,
|
|
71
85
|
...pluginCommands,
|
|
86
|
+
...capabilityCommands,
|
|
72
87
|
...datasetCommands,
|
|
88
|
+
...sourceCommands,
|
|
73
89
|
...knowledgeCommands,
|
|
74
90
|
...packCommands,
|
|
75
91
|
...secretCommands,
|
|
@@ -77,6 +93,10 @@ export const COMMANDS: readonly Command[] = [
|
|
|
77
93
|
|
|
78
94
|
blueprintList,
|
|
79
95
|
blueprintGet,
|
|
96
|
+
// Reads, next to the other two reads: the authoring verbs below all report
|
|
97
|
+
// success without proving anything came back, and these are what prove it.
|
|
98
|
+
blueprintQuery,
|
|
99
|
+
blueprintInstance,
|
|
80
100
|
blueprintGenerateTypes,
|
|
81
101
|
// Authoring is no longer reserved: the organization API key (`sk-org-`) reaches the
|
|
82
102
|
// organization-level shared draft, which is what these verbs were waiting on.
|
|
@@ -112,18 +132,68 @@ const NOUN_SUMMARY: Readonly<Record<string, string>> = {
|
|
|
112
132
|
skill: 'Workspace skills an agent loads at runtime',
|
|
113
133
|
plugin: 'Integrations and MCP servers connected to this workspace',
|
|
114
134
|
dataset: 'Source datasets a Blueprint object type can bind to',
|
|
135
|
+
source: 'Connected Sources — where a dataset’s rows come from',
|
|
115
136
|
knowledge: 'Knowledge bases and the sources inside them',
|
|
116
137
|
pack: 'Reusable skill bundles — author once, install per workspace',
|
|
117
138
|
secret: 'Workspace secrets — named here, never printed back',
|
|
118
139
|
automation: 'Automations — TypeScript deployed here, run on a schedule',
|
|
119
140
|
blueprint: 'The shared model of the organization — what an app can read',
|
|
120
141
|
action: 'Governed Actions — arm the write path a published Action runs',
|
|
142
|
+
capability: 'What an agent may DO — plugin operations granted to it',
|
|
143
|
+
auth: 'Credential profiles — one per API key, selected by directory',
|
|
144
|
+
kit: 'Optional project-local Frontera skills for Codex and Claude Code',
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
export function nounSummary(noun: string): string {
|
|
124
148
|
return NOUN_SUMMARY[noun] ?? ''
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Where the CLI ends, and what to do instead.
|
|
153
|
+
*
|
|
154
|
+
* A `reserved` verb tells a caller that a command is planned but unbuilt.
|
|
155
|
+
* Nothing tells them about the surface that has no verb AT ALL — and that is
|
|
156
|
+
* most of the platform: channels, workspaces, sheets and the evaluation suite
|
|
157
|
+
* are all API surfaces the CLI deliberately does not carry. A caller asked to
|
|
158
|
+
* bind a channel reads "unknown command: channel", concludes it mistyped, and
|
|
159
|
+
* either guesses a synonym or greps the binary. Both waste a turn and neither
|
|
160
|
+
* ends at the answer.
|
|
161
|
+
*
|
|
162
|
+
* Keyed by the noun a caller would REACH FOR, not by the one that exists —
|
|
163
|
+
* these entries exist to be hit by a wrong guess. Every value names where the
|
|
164
|
+
* capability actually lives, because "not supported" without a destination is
|
|
165
|
+
* the same dead end in a politer voice.
|
|
166
|
+
*
|
|
167
|
+
* A test asserts no key here collides with a real noun: the day one of these
|
|
168
|
+
* ships, its entry has to go, or the CLI would disown a command it has.
|
|
169
|
+
*/
|
|
170
|
+
export const UNSUPPORTED: Readonly<Record<string, string>> = {
|
|
171
|
+
channel:
|
|
172
|
+
'channels have no CLI surface — create one and bind it to an agent in the Console, '
|
|
173
|
+
+ 'under the agent’s Channels tab',
|
|
174
|
+
workspace:
|
|
175
|
+
'workspace and member management is Console-only. `frontera login` selects which workspace '
|
|
176
|
+
+ 'a key acts on; it cannot create one',
|
|
177
|
+
key: 'API keys are minted in the Console, never by the CLI — a credential that mints credentials '
|
|
178
|
+
+ 'is the escalation that separation exists to prevent',
|
|
179
|
+
sheet: 'sheets have no CLI surface and none is planned',
|
|
180
|
+
task: 'task and run history is Console-only; `frontera automation runs` covers automation runs',
|
|
181
|
+
eval: 'the evaluation suite (datasets, scorers, judges, queues) is Console-only',
|
|
182
|
+
usage: 'usage, cost and audit reporting is Console-only',
|
|
183
|
+
site: 'published sites are managed in the Console; `frontera app` covers Frontera Apps',
|
|
184
|
+
object:
|
|
185
|
+
'the legacy object surface is superseded by Blueprint — use `frontera blueprint` instead',
|
|
186
|
+
artifact:
|
|
187
|
+
'artifacts belong to the chat product — download or export one from the Console, '
|
|
188
|
+
+ 'from the session that produced it',
|
|
189
|
+
user: 'users, roles and provider credentials are Console-only',
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The message for a noun the CLI deliberately does not carry, if it is one. */
|
|
193
|
+
export function unsupportedNoun(noun: string): string | null {
|
|
194
|
+
return UNSUPPORTED[noun] ?? null
|
|
195
|
+
}
|
|
196
|
+
|
|
127
197
|
/** Short forms every command understands, plus the command's own. */
|
|
128
198
|
export function aliasesFor(meta: CommandMeta): Readonly<Record<string, string>> {
|
|
129
199
|
return { h: 'help', ...(meta.aliases ?? {}) }
|
|
@@ -162,7 +232,11 @@ const GLOBAL_SECTION = [
|
|
|
162
232
|
heading('Environment'),
|
|
163
233
|
...definitions([
|
|
164
234
|
['FRONTERA_API_URL', 'Frontera API origin'],
|
|
165
|
-
|
|
235
|
+
// Upstream's key-kind detail, kept — profiles change where a key is stored,
|
|
236
|
+
// not which kinds are valid.
|
|
237
|
+
['FRONTERA_TOKEN', 'sk-ws-… or sk-org-… — requires FRONTERA_API_URL or --api-url'],
|
|
238
|
+
['FRONTERA_PROFILE', 'profile to use, overriding the directory selection'],
|
|
239
|
+
['FRONTERA_SECRET_STORE', 'where keys are stored: keychain (default on macOS) or file'],
|
|
166
240
|
['NO_COLOR', 'set to disable colour'],
|
|
167
241
|
]),
|
|
168
242
|
]
|
|
@@ -276,6 +350,20 @@ export function renderCommandHelp(meta: CommandMeta): string {
|
|
|
276
350
|
lines.push('', ...wrap(`Not available yet: ${meta.reserved}`, width).map(dim))
|
|
277
351
|
}
|
|
278
352
|
|
|
353
|
+
// Stated only for the exception. Printing "works with an API key" on the
|
|
354
|
+
// thirty commands where that is true trains a reader to skip the line, and
|
|
355
|
+
// the one command where it is false is the one that has to be read.
|
|
356
|
+
if (meta.authLane === 'session') {
|
|
357
|
+
lines.push(
|
|
358
|
+
'',
|
|
359
|
+
...wrap(
|
|
360
|
+
'Needs a session credential: run `frontera login`. This command’s endpoints '
|
|
361
|
+
+ 'refuse sk-ws- and sk-org- keys, and answer 401 rather than a permission error.',
|
|
362
|
+
width,
|
|
363
|
+
).map(dim),
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
279
367
|
if (meta.args.length > 0) {
|
|
280
368
|
lines.push('', heading('Arguments'))
|
|
281
369
|
lines.push(
|
|
@@ -338,6 +426,11 @@ export interface CommandDescriptor {
|
|
|
338
426
|
examples: string[]
|
|
339
427
|
needsProject: boolean
|
|
340
428
|
requiresCredential: boolean
|
|
429
|
+
/**
|
|
430
|
+
* `api-key` — reachable with `sk-ws-`/`sk-org-` or a session.
|
|
431
|
+
* `session` — reachable ONLY with a session token from `frontera login`.
|
|
432
|
+
*/
|
|
433
|
+
authLane: 'api-key' | 'session'
|
|
341
434
|
}
|
|
342
435
|
|
|
343
436
|
export function describeCommand(meta: CommandMeta): CommandDescriptor {
|
|
@@ -366,6 +459,7 @@ export function describeCommand(meta: CommandMeta): CommandDescriptor {
|
|
|
366
459
|
examples: [...meta.examples],
|
|
367
460
|
needsProject: Boolean(meta.needsProject),
|
|
368
461
|
requiresCredential: !meta.offline,
|
|
462
|
+
authLane: meta.authLane ?? 'api-key',
|
|
369
463
|
}
|
|
370
464
|
}
|
|
371
465
|
|
|
@@ -414,6 +508,15 @@ function nearest(typed: string, candidates: string[]): string | null {
|
|
|
414
508
|
export function unknownCommand(noun: string, verb: string | undefined): UsageError {
|
|
415
509
|
const known = nouns()
|
|
416
510
|
if (!known.includes(noun)) {
|
|
511
|
+
// A deliberate boundary answers before the typo check does. `frontera
|
|
512
|
+
// channel bind` is not a misspelling of anything, and offering the nearest
|
|
513
|
+
// real noun would send a caller to an unrelated command with a plausible
|
|
514
|
+
// name rather than to the Console, where the capability is.
|
|
515
|
+
const boundary = unsupportedNoun(noun)
|
|
516
|
+
if (boundary) {
|
|
517
|
+
return new UsageError(`\`${noun}\` is not a Frontera CLI command`, boundary)
|
|
518
|
+
}
|
|
519
|
+
|
|
417
520
|
const guess = nearest(noun, known)
|
|
418
521
|
return new UsageError(
|
|
419
522
|
`unknown command: ${noun}`,
|