@frontera-sdk/cli 1.44.0 → 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
|
@@ -111,6 +111,168 @@ const sources: Command = {
|
|
|
111
111
|
},
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
const get: Command = {
|
|
115
|
+
meta: {
|
|
116
|
+
noun: 'knowledge',
|
|
117
|
+
verb: 'get',
|
|
118
|
+
args: [{ name: 'base', required: true, description: 'knowledge base name or id, from `frontera knowledge list`' }],
|
|
119
|
+
flags: {},
|
|
120
|
+
summary: 'Show one knowledge base — its settings and ingestion counts',
|
|
121
|
+
examples: ['frontera knowledge get product-docs', 'frontera knowledge get product-docs --json'],
|
|
122
|
+
},
|
|
123
|
+
async run(ctx) {
|
|
124
|
+
const ref = ctx.positional[0]
|
|
125
|
+
if (!ref) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
126
|
+
|
|
127
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
128
|
+
const base = await client.knowledgeBase(await resolveKnowledgeRef(client, ref))
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
data: base,
|
|
132
|
+
// Whatever the route returns, flattened. Naming the fields here would
|
|
133
|
+
// print "?" for every one the service renames, and the embedding model
|
|
134
|
+
// and chunker are exactly the fields a caller comes to this verb to read.
|
|
135
|
+
text: table(
|
|
136
|
+
['field', 'value'],
|
|
137
|
+
Object.entries(base).map(([k, v]) => [
|
|
138
|
+
k,
|
|
139
|
+
v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v),
|
|
140
|
+
]),
|
|
141
|
+
[undefined, 70],
|
|
142
|
+
),
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Retrieval, which is the only thing that proves an upload landed.
|
|
149
|
+
*
|
|
150
|
+
* `knowledge upload` reports what it sent; `knowledge sources` reports the
|
|
151
|
+
* ingestion status per file. Neither answers the question a caller actually
|
|
152
|
+
* has — can the agent find this — because a source can reach `ready` with
|
|
153
|
+
* chunks that embed nothing useful. Searching is the check.
|
|
154
|
+
*/
|
|
155
|
+
const search: Command = {
|
|
156
|
+
meta: {
|
|
157
|
+
noun: 'knowledge',
|
|
158
|
+
verb: 'search',
|
|
159
|
+
args: [
|
|
160
|
+
{ name: 'base', required: true, description: 'knowledge base name or id, from `frontera knowledge list`' },
|
|
161
|
+
{ name: 'query', required: true, description: 'text to search for, as an agent would ask it' },
|
|
162
|
+
],
|
|
163
|
+
flags: { 'top-k': 'string', threshold: 'string' },
|
|
164
|
+
summary: 'Search a knowledge base and show the chunks that come back',
|
|
165
|
+
examples: [
|
|
166
|
+
'frontera knowledge search product-docs "how do refunds work"',
|
|
167
|
+
'frontera knowledge search product-docs "refund policy" --top-k 3 --json',
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
async run(ctx) {
|
|
171
|
+
const [ref, ...queryParts] = ctx.positional
|
|
172
|
+
if (!ref) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
173
|
+
// Joined rather than requiring quotes: an unquoted multi-word query is the
|
|
174
|
+
// normal typo, and refusing it teaches nothing a join cannot fix.
|
|
175
|
+
const query = queryParts.join(' ').trim()
|
|
176
|
+
if (!query) {
|
|
177
|
+
throw new UsageError('missing <query>', `frontera knowledge search ${ref} "what a user would ask"`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const topKRaw = flagString(ctx, 'top-k')
|
|
181
|
+
const topK = topKRaw === undefined ? undefined : Number(topKRaw)
|
|
182
|
+
if (topK !== undefined && (!Number.isInteger(topK) || topK < 1)) {
|
|
183
|
+
throw new UsageError(`--top-k must be a positive whole number, not "${topKRaw}"`, 'try --top-k 5')
|
|
184
|
+
}
|
|
185
|
+
const thresholdRaw = flagString(ctx, 'threshold')
|
|
186
|
+
const threshold = thresholdRaw === undefined ? undefined : Number(thresholdRaw)
|
|
187
|
+
if (threshold !== undefined && Number.isNaN(threshold)) {
|
|
188
|
+
throw new UsageError(`--threshold must be a number, not "${thresholdRaw}"`, 'try --threshold 0.7')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
192
|
+
const baseId = await resolveKnowledgeRef(client, ref)
|
|
193
|
+
const result = await client.knowledgeSearch(baseId, {
|
|
194
|
+
query,
|
|
195
|
+
...(topK === undefined ? {} : { topK }),
|
|
196
|
+
...(threshold === undefined ? {} : { threshold }),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const chunks = result.results
|
|
200
|
+
if (chunks.length === 0) {
|
|
201
|
+
// The overwhelmingly common cause, and the one a caller cannot see from
|
|
202
|
+
// here: ingestion is asynchronous, so a base uploaded a minute ago is
|
|
203
|
+
// empty rather than broken.
|
|
204
|
+
return {
|
|
205
|
+
data: result,
|
|
206
|
+
text:
|
|
207
|
+
'No chunks matched.\n'
|
|
208
|
+
+ ` If the upload was recent, check \`frontera knowledge sources ${ref}\` — a source\n`
|
|
209
|
+
+ ' still `processing` is not searchable yet.\n'
|
|
210
|
+
// Stated the right way round: --threshold is a distance cutoff, so a
|
|
211
|
+
// LOW value is the restrictive one and 0 can never match.
|
|
212
|
+
+ (threshold !== undefined
|
|
213
|
+
? ` --threshold ${threshold} is a maximum cosine distance — raise it to widen the search.\n`
|
|
214
|
+
: ' The default cutoff is 0.9; pass a higher --threshold to widen the search.\n')
|
|
215
|
+
+ ' Otherwise the corpus may simply not cover this.',
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
data: result,
|
|
221
|
+
text: table(
|
|
222
|
+
['score', 'source', 'chunk'],
|
|
223
|
+
chunks.map((c) => [
|
|
224
|
+
typeof c.score === 'number' ? c.score.toFixed(3) : String(c.score ?? ''),
|
|
225
|
+
String(c.fileName ?? c.sourceId ?? ''),
|
|
226
|
+
String(c.content ?? c.text ?? '').replace(/\s+/g, ' ').trim(),
|
|
227
|
+
]),
|
|
228
|
+
[8, 30, 90],
|
|
229
|
+
),
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Re-run ingestion for a source that failed.
|
|
236
|
+
*
|
|
237
|
+
* `upload` exits 0 on a partial batch by design — one unreadable file does not
|
|
238
|
+
* end the run — so a failed source is normal and recovery has to be normal
|
|
239
|
+
* too. Until this verb the only route was the Console, which meant an
|
|
240
|
+
* unattended `knowledge upload` could not repair itself.
|
|
241
|
+
*/
|
|
242
|
+
const retry: Command = {
|
|
243
|
+
meta: {
|
|
244
|
+
noun: 'knowledge',
|
|
245
|
+
verb: 'retry',
|
|
246
|
+
args: [
|
|
247
|
+
{ name: 'base', required: true, description: 'knowledge base name or id' },
|
|
248
|
+
{ name: 'source', required: true, description: 'source id, from `frontera knowledge sources`' },
|
|
249
|
+
],
|
|
250
|
+
flags: {},
|
|
251
|
+
summary: 'Re-run ingestion for one source that failed',
|
|
252
|
+
examples: ['frontera knowledge retry product-docs <sourceId>'],
|
|
253
|
+
},
|
|
254
|
+
async run(ctx) {
|
|
255
|
+
const [ref, sourceId] = ctx.positional
|
|
256
|
+
if (!ref) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
257
|
+
if (!sourceId) {
|
|
258
|
+
throw new UsageError('missing <source>', `frontera knowledge sources ${ref} — the id column`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
262
|
+
const baseId = await resolveKnowledgeRef(client, ref)
|
|
263
|
+
const result = await client.retryKnowledgeSource(baseId, sourceId)
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
data: result,
|
|
267
|
+
// Same asynchrony as upload: this queues the job, it does not finish it.
|
|
268
|
+
text:
|
|
269
|
+
`Re-queued ${sourceId}.\n`
|
|
270
|
+
+ ` Ingestion is asynchronous — \`frontera knowledge sources ${ref}\` shows the outcome.\n`
|
|
271
|
+
+ ' Retrying is additive and safe to repeat.',
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
114
276
|
const create: Command = {
|
|
115
277
|
meta: {
|
|
116
278
|
noun: 'knowledge',
|
|
@@ -381,7 +543,10 @@ const reserved: Command[] = (
|
|
|
381
543
|
|
|
382
544
|
export const knowledgeCommands: Command[] = [
|
|
383
545
|
list,
|
|
546
|
+
get,
|
|
384
547
|
sources,
|
|
548
|
+
search,
|
|
549
|
+
retry,
|
|
385
550
|
create,
|
|
386
551
|
upload,
|
|
387
552
|
attach,
|
package/src/commands/login.ts
CHANGED
|
@@ -1,41 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { verifyKey } from '../auth-verify'
|
|
2
|
+
import { ensureMigrated } from '../config'
|
|
3
|
+
import { credentialStore } from '../credential-store'
|
|
3
4
|
import { CliError, UsageError } from '../errors'
|
|
4
|
-
import { readSecretValue } from '../secrets'
|
|
5
5
|
import { canPrompt, promptSecret } from '../prompt'
|
|
6
|
-
import
|
|
6
|
+
import { assertApiUrl, fingerprint, getProfile, putProfile } from '../profiles'
|
|
7
|
+
import { readSecretValue } from '../secrets'
|
|
8
|
+
import { flagBool, type Command } from './types'
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* The old entry point, kept working.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* `login` predates profiles and stored one token per API origin, which is the
|
|
14
|
+
* limitation profiles exist to remove. It survives for two minor releases as an
|
|
15
|
+
* alias that creates or replaces the `default` profile, because a machine that
|
|
16
|
+
* upgrades mid-task should not discover its setup command is gone.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* workspace it just bound to.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Where the token comes from, in the order a caller would expect.
|
|
23
|
-
*
|
|
24
|
-
* Piped input wins over a prompt, so `echo $KEY | frontera login` works
|
|
25
|
-
* without a flag — the clig.dev convention, and the reason `--token-stdin`
|
|
26
|
-
* is now an explicit opt-in rather than a requirement.
|
|
27
|
-
*
|
|
28
|
-
* The prompt is reached only on an interactive terminal. An agent's stdin is
|
|
29
|
-
* a pipe or /dev/null, so it lands in the stdin branch or the error, never on
|
|
30
|
-
* a prompt that would hang. `--no-input` forces that even on a TTY.
|
|
18
|
+
* Replacement is still explicit: an existing `default` needs `--force`, exactly
|
|
19
|
+
* as `auth add` does. Silently rebinding `default` was the old behaviour and it
|
|
20
|
+
* is the behaviour that made two customers impossible to hold at once.
|
|
31
21
|
*/
|
|
22
|
+
const DEPRECATION = 'frontera login is deprecated — use `frontera auth add <profile> --api-url <origin>`'
|
|
23
|
+
|
|
32
24
|
async function readToken(flags: Record<string, string | boolean>): Promise<string> {
|
|
33
25
|
const noInput = flags['no-input'] === true
|
|
34
26
|
const explicitStdin = flags['token-stdin'] === true
|
|
35
27
|
|
|
36
28
|
if (explicitStdin || !canPrompt() || noInput) {
|
|
37
|
-
// Not a terminal: read whatever was piped in. Falls back to the
|
|
38
|
-
// environment so an already-exported key can simply be stored.
|
|
39
29
|
if (explicitStdin || !process.stdin.isTTY) {
|
|
40
30
|
const piped = (await readSecretValue('-')).trim()
|
|
41
31
|
if (piped) return piped
|
|
@@ -43,7 +33,11 @@ async function readToken(flags: Record<string, string | boolean>): Promise<strin
|
|
|
43
33
|
return (process.env.FRONTERA_TOKEN ?? '').trim()
|
|
44
34
|
}
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
// Names BOTH kinds, because the prompt is the only place most callers ever
|
|
37
|
+
// learn which are accepted. It read `Workspace key (sk-ws-…)` while the
|
|
38
|
+
// verification below has always taken an organization key too — so a holder
|
|
39
|
+
// of one had no way to discover that except by pasting it and being right.
|
|
40
|
+
return promptSecret('Frontera key (sk-ws-… or sk-org-…): ')
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
export const loginCommand: Command = {
|
|
@@ -51,91 +45,77 @@ export const loginCommand: Command = {
|
|
|
51
45
|
noun: 'login',
|
|
52
46
|
verb: '',
|
|
53
47
|
args: [],
|
|
54
|
-
flags: { 'token-stdin': 'boolean', 'no-input': 'boolean' },
|
|
55
|
-
|
|
48
|
+
flags: { 'token-stdin': 'boolean', 'no-input': 'boolean', force: 'boolean' },
|
|
49
|
+
// Keeps the upstream correction that this accepts an organization key too,
|
|
50
|
+
// inside the deprecation framing profiles introduce.
|
|
51
|
+
summary: 'Deprecated — store a workspace or organization key as `default`',
|
|
56
52
|
examples: [
|
|
57
|
-
'frontera login --api-url http://localhost:4000',
|
|
58
53
|
'echo $KEY | frontera login --api-url http://localhost:4000',
|
|
59
|
-
'frontera login --api-url
|
|
54
|
+
'frontera login --api-url http://localhost:4000 --force',
|
|
60
55
|
],
|
|
61
|
-
// It SUPPLIES a credential, so it must not require one to start.
|
|
62
56
|
offline: true,
|
|
63
57
|
},
|
|
64
58
|
|
|
65
59
|
async run(ctx) {
|
|
60
|
+
ctx.output.note(DEPRECATION)
|
|
61
|
+
|
|
66
62
|
if (!ctx.apiUrl) {
|
|
67
63
|
throw new UsageError(
|
|
68
64
|
'no API origin configured',
|
|
69
65
|
'set FRONTERA_API_URL, or pass --api-url <origin>',
|
|
70
66
|
)
|
|
71
67
|
}
|
|
68
|
+
assertApiUrl(ctx.apiUrl)
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
await ensureMigrated(process.env)
|
|
74
71
|
|
|
72
|
+
const existing = getProfile('default')
|
|
73
|
+
if (existing && !flagBool(ctx, 'force')) {
|
|
74
|
+
throw new CliError('the `default` profile already exists', {
|
|
75
|
+
code: 'USAGE',
|
|
76
|
+
hint: 're-run with --force to replace it, or name a new one with `frontera auth add <profile>`',
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const token = await readToken(ctx.flags)
|
|
75
81
|
if (!token) {
|
|
76
82
|
throw new UsageError(
|
|
77
83
|
'no token supplied',
|
|
78
84
|
'run `frontera login` in a terminal to be asked for one, or pipe it in: `echo $KEY | frontera login`',
|
|
79
85
|
)
|
|
80
86
|
}
|
|
81
|
-
// Two credential kinds reach this CLI, and they are not interchangeable:
|
|
82
|
-
//
|
|
83
|
-
// sk-ws- a WORKSPACE key — one workspace's slice of the platform.
|
|
84
|
-
// sk-org- an ORGANIZATION key — the organization-level Blueprint draft,
|
|
85
|
-
// which no workspace credential can reach, because that draft is
|
|
86
|
-
// shared by every workspace in the organization.
|
|
87
|
-
//
|
|
88
|
-
// Both are stored the same way; only the verification below differs, because an
|
|
89
|
-
// organization key belongs to no workspace and `whoami` has none to report.
|
|
90
|
-
const isWorkspaceKey = token.startsWith('sk-ws-')
|
|
91
|
-
const isOrgKey = token.startsWith('sk-org-')
|
|
92
|
-
if (!isWorkspaceKey && !isOrgKey) {
|
|
93
|
-
throw new UsageError(
|
|
94
|
-
'that does not look like a Frontera key',
|
|
95
|
-
'workspace keys start with sk-ws- (Workspace settings → API Keys); ' +
|
|
96
|
-
'organization keys start with sk-org- (Settings → API keys)',
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
87
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
me = await new PlatformApi(ctx.apiUrl, token).whoami().catch(() => null)
|
|
105
|
-
if (!me?.workspaceId) {
|
|
106
|
-
throw new CliError('the token was rejected by this API origin', {
|
|
107
|
-
code: 'UNAUTHORIZED',
|
|
108
|
-
hint: `check the key is enabled, and that --api-url is right (currently ${ctx.apiUrl})`,
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
} else {
|
|
112
|
-
// Verified against an organization-scoped read instead. A revoked or expired
|
|
113
|
-
// key answers 401 here, which is exactly what login should catch.
|
|
114
|
-
const lifecycle = await fetch(`${ctx.apiUrl}/v1/blueprint/lifecycle`, {
|
|
115
|
-
headers: { authorization: `Bearer ${token}` },
|
|
116
|
-
}).catch(() => null)
|
|
117
|
-
if (!lifecycle || lifecycle.status === 401 || lifecycle.status === 403) {
|
|
118
|
-
throw new CliError('the organization key was rejected by this API origin', {
|
|
119
|
-
code: 'UNAUTHORIZED',
|
|
120
|
-
hint: `check the key is active and carries organization:update, and that --api-url is right (currently ${ctx.apiUrl})`,
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
}
|
|
88
|
+
const verified = await verifyKey(ctx.apiUrl, token)
|
|
89
|
+
|
|
90
|
+
const store = credentialStore()
|
|
91
|
+
await store.set('default', token)
|
|
124
92
|
|
|
125
|
-
|
|
93
|
+
const now = new Date().toISOString()
|
|
94
|
+
putProfile('default', {
|
|
95
|
+
apiUrl: ctx.apiUrl,
|
|
96
|
+
credentialKind: verified.kind,
|
|
97
|
+
workspaceId: verified.workspaceId,
|
|
98
|
+
orgId: verified.orgId,
|
|
99
|
+
fingerprint: fingerprint(token),
|
|
100
|
+
createdAt: existing?.createdAt ?? now,
|
|
101
|
+
lastVerifiedAt: now,
|
|
102
|
+
})
|
|
126
103
|
|
|
127
104
|
return {
|
|
128
105
|
data: {
|
|
106
|
+
profile: 'default',
|
|
129
107
|
apiUrl: ctx.apiUrl,
|
|
130
|
-
kind:
|
|
131
|
-
workspaceId:
|
|
132
|
-
orgId:
|
|
108
|
+
kind: verified.kind,
|
|
109
|
+
workspaceId: verified.workspaceId,
|
|
110
|
+
orgId: verified.orgId,
|
|
111
|
+
deprecated: true,
|
|
133
112
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
113
|
+
text: [
|
|
114
|
+
verified.kind === 'organization'
|
|
115
|
+
? `Stored an organization key as profile default for ${ctx.apiUrl}\n it authors this organization's Blueprint`
|
|
116
|
+
: `Stored a key as profile default for ${ctx.apiUrl}\n workspace ${verified.workspaceId}`,
|
|
117
|
+
' select it for this directory with `frontera auth use default`',
|
|
118
|
+
].join('\n'),
|
|
139
119
|
}
|
|
140
120
|
},
|
|
141
121
|
}
|