@frontera-sdk/cli 1.45.1 → 1.45.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/api/platform-api.ts +16 -2
- package/src/commands/agent/index-commands.ts +4 -4
- package/src/commands/app/dev.ts +1 -1
- package/src/commands/app/list.ts +1 -1
- package/src/commands/auth/current.ts +18 -1
- package/src/commands/blueprint/generate-types.ts +1 -1
- package/src/commands/blueprint/get.ts +1 -1
- package/src/commands/blueprint/list.ts +1 -1
- package/src/commands/blueprint/query.ts +2 -2
- package/src/commands/capability/index-commands.ts +7 -7
- package/src/commands/knowledge/index-commands.ts +10 -10
- package/src/commands/pack/index-commands.ts +8 -8
- package/src/commands/plugin/index-commands.ts +8 -8
- package/src/commands/registry.ts +7 -3
- package/src/commands/secret/index-commands.ts +3 -3
- package/src/commands/skill/bundle-commands.ts +3 -3
- package/src/commands/skill/index-commands.ts +3 -3
- package/src/commands/types.ts +5 -0
- package/src/commands/workspace/index-commands.ts +76 -0
- package/src/exit.ts +10 -1
- package/src/flag-help.ts +4 -0
- package/src/main.ts +56 -0
- package/src/project-context.ts +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.45.
|
|
3
|
+
"version": "1.45.3",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"build:release": "bun run scripts/build-release.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@frontera-sdk/automation": "1.45.
|
|
42
|
-
"@frontera-sdk/core": "1.45.
|
|
41
|
+
"@frontera-sdk/automation": "1.45.2",
|
|
42
|
+
"@frontera-sdk/core": "1.45.2",
|
|
43
43
|
"gray-matter": "^4.0.3",
|
|
44
44
|
"yaml": "^2.9.0"
|
|
45
45
|
},
|
package/src/api/platform-api.ts
CHANGED
|
@@ -38,10 +38,22 @@ export interface AgentDraft {
|
|
|
38
38
|
export class PlatformApi {
|
|
39
39
|
private readonly client: FronteraClient
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* `workspaceId` is for an ORGANIZATION key, which spans a list of workspaces
|
|
43
|
+
* and is refused a workspace-scoped route until one is named — the service
|
|
44
|
+
* will not widen an unaddressed request to org scope. The SDK turns it into
|
|
45
|
+
* `x-workspace-id`.
|
|
46
|
+
*
|
|
47
|
+
* Not taken from the profile's `workspaceId`: that field is descriptive by
|
|
48
|
+
* design (`profiles.ts`), recorded at verify time and never re-derived, so
|
|
49
|
+
* treating it as authority would send a stale id as fact. This comes from
|
|
50
|
+
* `--workspace`, which the caller states now.
|
|
51
|
+
*/
|
|
52
|
+
constructor(apiBaseUrl: string, token: string, workspaceId?: string) {
|
|
42
53
|
this.client = new FronteraClient({
|
|
43
54
|
apiBaseUrl,
|
|
44
55
|
credential: { kind: 'apiKey', key: token },
|
|
56
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
45
57
|
})
|
|
46
58
|
}
|
|
47
59
|
|
|
@@ -278,7 +290,9 @@ export class PlatformApi {
|
|
|
278
290
|
{
|
|
279
291
|
code: body?.code ?? statusCode(res.status),
|
|
280
292
|
...(res.status === 401
|
|
281
|
-
? { hint: '
|
|
293
|
+
? { hint: 'run `frontera auth current` to see which credential this resolves to — a '
|
|
294
|
+
+ 'workspace key (sk-ws-…) serves agents, skills, plugins and knowledge; an '
|
|
295
|
+
+ 'organization key (sk-org-…) serves Blueprint authoring, datasets and Sources' }
|
|
282
296
|
: {}),
|
|
283
297
|
},
|
|
284
298
|
)
|
|
@@ -8,7 +8,7 @@ import { renderComposition, type Lookups } from './compose'
|
|
|
8
8
|
import { resolveAgentRef, type AgentRow } from './resolve'
|
|
9
9
|
|
|
10
10
|
function api(ctx: CommandContext): PlatformApi {
|
|
11
|
-
return new PlatformApi(ctx.apiUrl, ctx.token)
|
|
11
|
+
return new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
async function target(ctx: CommandContext, verb: string): Promise<{ api: PlatformApi; id: string; ref: string }> {
|
|
@@ -510,7 +510,7 @@ const skillAttach: Command = {
|
|
|
510
510
|
if (!ref) throw new UsageError('missing <agent>', 'frontera agent list')
|
|
511
511
|
if (!skillId) throw new UsageError('missing <skill>', 'frontera skill list — then pass an id')
|
|
512
512
|
|
|
513
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
513
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
514
514
|
const result = await api.attachAgentSkill(await resolveAgentRef(api, ref), skillId)
|
|
515
515
|
|
|
516
516
|
return {
|
|
@@ -538,7 +538,7 @@ const skillDetach: Command = {
|
|
|
538
538
|
if (!ref) throw new UsageError('missing <agent>', 'frontera agent list')
|
|
539
539
|
if (!skillId) throw new UsageError('missing <skill>', `frontera agent get ${ref} — lists what it loads`)
|
|
540
540
|
|
|
541
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
541
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
542
542
|
const result = await api.detachAgentSkill(await resolveAgentRef(api, ref), skillId)
|
|
543
543
|
|
|
544
544
|
return { data: result, text: `Detached ${skillId} from ${ref}. The skill itself is untouched.` }
|
|
@@ -581,7 +581,7 @@ const revert: Command = {
|
|
|
581
581
|
throw new UsageError('missing <version>', `frontera agent versions ${ref} — then pass a version id`)
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
584
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
585
585
|
const result = await api.revertAgent(await resolveAgentRef(api, ref), versionId, {
|
|
586
586
|
// The old snapshot can name a knowledge base that has since been
|
|
587
587
|
// deleted. Refusing by default is right — silently reverting to an agent
|
package/src/commands/app/dev.ts
CHANGED
|
@@ -77,7 +77,7 @@ export const appDev: Command = {
|
|
|
77
77
|
|
|
78
78
|
let broker: ReturnType<typeof createDevSessionBroker> | null = null
|
|
79
79
|
if (framework === 'next') {
|
|
80
|
-
const identity = await new PlatformApi(ctx.apiUrl, ctx.token).whoami()
|
|
80
|
+
const identity = await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).whoami()
|
|
81
81
|
if (!identity.workspaceId || !identity.orgId) {
|
|
82
82
|
throw new UsageError(
|
|
83
83
|
'App development requires a workspace-scoped credential',
|
package/src/commands/app/list.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const appList: Command = {
|
|
|
29
29
|
},
|
|
30
30
|
|
|
31
31
|
async run(ctx) {
|
|
32
|
-
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token).platformApps()) as AppRow[]
|
|
32
|
+
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).platformApps()) as AppRow[]
|
|
33
33
|
|
|
34
34
|
return {
|
|
35
35
|
data: rows,
|
|
@@ -52,8 +52,20 @@ export const authCurrent: Command = {
|
|
|
52
52
|
profile: selection.profile,
|
|
53
53
|
profileSource: selection.source,
|
|
54
54
|
...(selection.path ? { profileSourcePath: selection.path } : {}),
|
|
55
|
-
|
|
55
|
+
// The EFFECTIVE origin, not the profile's.
|
|
56
|
+
//
|
|
57
|
+
// Reporting `metadata.apiUrl` while `apiUrlSource` said "environment" was
|
|
58
|
+
// a contradiction in one object: the field named where the value came
|
|
59
|
+
// from and then showed a different value. This command exists to answer
|
|
60
|
+
// "which deployment am I about to write to", so it is the one place that
|
|
61
|
+
// must not be optimistic — an agent reading `acme` and writing to
|
|
62
|
+
// `globex` is the failure the whole profile model is built to prevent.
|
|
63
|
+
apiUrl: suppliedOrigin ?? metadata.apiUrl,
|
|
56
64
|
apiUrlSource: suppliedOrigin ? (flagString(ctx, 'api-url') ? 'flag' : 'environment') : 'profile',
|
|
65
|
+
// Kept alongside so a mismatch is visible rather than merely absent.
|
|
66
|
+
...(suppliedOrigin && suppliedOrigin !== metadata.apiUrl
|
|
67
|
+
? { profileApiUrl: metadata.apiUrl }
|
|
68
|
+
: {}),
|
|
57
69
|
credentialSource: envToken ? 'environment' : (located?.source ?? null),
|
|
58
70
|
credentialKind: metadata.credentialKind,
|
|
59
71
|
workspaceId: metadata.workspaceId,
|
|
@@ -76,6 +88,11 @@ export const authCurrent: Command = {
|
|
|
76
88
|
if (envToken) {
|
|
77
89
|
lines.push(' FRONTERA_TOKEN is set and takes precedence over this profile’s stored key')
|
|
78
90
|
}
|
|
91
|
+
if (suppliedOrigin && suppliedOrigin !== metadata.apiUrl) {
|
|
92
|
+
lines.push(
|
|
93
|
+
` the origin above is overridden — this profile's own origin is ${metadata.apiUrl}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
79
96
|
|
|
80
97
|
return { data, text: lines.join('\n') }
|
|
81
98
|
},
|
|
@@ -26,7 +26,7 @@ export const blueprintGenerateTypes: Command = {
|
|
|
26
26
|
|
|
27
27
|
async run(ctx) {
|
|
28
28
|
const project = ctx.project!
|
|
29
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
29
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
30
30
|
const schema = parseBlueprintSchema(await api.blueprintSchema())
|
|
31
31
|
const contents = generateBlueprintTypes(schema)
|
|
32
32
|
const written = writeBlueprintTypesFile(
|
|
@@ -83,7 +83,7 @@ export const blueprintGet: Command = {
|
|
|
83
83
|
)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
86
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
87
87
|
const schema = parseBlueprintSchema(await api.blueprintSchema())
|
|
88
88
|
const apiName = resolveApiName(schema.objectTypes, typed)
|
|
89
89
|
const type = schema.objectTypes.find((objectType) => objectType.apiName === apiName) as BlueprintSchemaObject
|
|
@@ -28,7 +28,7 @@ export const blueprintList: Command = {
|
|
|
28
28
|
},
|
|
29
29
|
|
|
30
30
|
async run(ctx) {
|
|
31
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
31
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
32
32
|
const types = parseBlueprintSchema(await api.blueprintSchema()).objectTypes
|
|
33
33
|
|
|
34
34
|
return {
|
|
@@ -19,8 +19,8 @@ import { flagString, type Command } from '../types'
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/** Constructed per call: the credential is per-invocation, not per-process. */
|
|
22
|
-
function api(ctx: { apiUrl: string; token: string }): PlatformApi {
|
|
23
|
-
return new PlatformApi(ctx.apiUrl, ctx.token)
|
|
22
|
+
function api(ctx: { apiUrl: string; token: string; workspaceId?: string }): PlatformApi {
|
|
23
|
+
return new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** `status:open` and `amount:>:1000` — the two forms worth typing by hand. */
|
|
@@ -74,7 +74,7 @@ const list: Command = {
|
|
|
74
74
|
const ref = ctx.positional[0]
|
|
75
75
|
if (!ref) throw new UsageError('missing <agent>', 'frontera agent list — then pass an id or slug')
|
|
76
76
|
|
|
77
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
77
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
78
78
|
const agentId = await resolveAgentRef(api, ref)
|
|
79
79
|
const rows = (await api.agentCapabilities(agentId)) as CapabilityRow[]
|
|
80
80
|
|
|
@@ -119,7 +119,7 @@ const available: Command = {
|
|
|
119
119
|
const installId = ctx.positional[0]
|
|
120
120
|
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list — then pass an install id')
|
|
121
121
|
|
|
122
|
-
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token)
|
|
122
|
+
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
123
123
|
.pluginCapabilities(installId)) as CapabilityRow[]
|
|
124
124
|
|
|
125
125
|
return {
|
|
@@ -158,7 +158,7 @@ const grant: Command = {
|
|
|
158
158
|
)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
161
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
162
162
|
const agentId = await resolveAgentRef(api, ref)
|
|
163
163
|
const result = await api.grantAgentCapability(agentId, capabilityId)
|
|
164
164
|
|
|
@@ -196,7 +196,7 @@ const revoke: Command = {
|
|
|
196
196
|
throw new UsageError('missing <capability>', `frontera capability list ${ref}`)
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
199
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
200
200
|
const agentId = await resolveAgentRef(api, ref)
|
|
201
201
|
const result = await api.revokeAgentCapability(agentId, capabilityId)
|
|
202
202
|
|
|
@@ -223,7 +223,7 @@ const installs: Command = {
|
|
|
223
223
|
const ref = ctx.positional[0]
|
|
224
224
|
if (!ref) throw new UsageError('missing <agent>', 'frontera agent list — then pass an id or slug')
|
|
225
225
|
|
|
226
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
226
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
227
227
|
const rows = (await api.agentInstalls(await resolveAgentRef(api, ref))) as Array<{
|
|
228
228
|
install?: { installName?: string }
|
|
229
229
|
catalog?: { name?: string; kind?: string }
|
|
@@ -281,7 +281,7 @@ const bind: Command = {
|
|
|
281
281
|
// switching later does not migrate anything already connected.
|
|
282
282
|
const accountMode = ctx.flags['end-user'] === true ? 'end_user' : 'agent_owned'
|
|
283
283
|
|
|
284
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
284
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
285
285
|
const agentId = await resolveAgentRef(api, ref)
|
|
286
286
|
const result = await api.bindAgentInstall(agentId, installId, accountMode)
|
|
287
287
|
|
|
@@ -312,7 +312,7 @@ const unbind: Command = {
|
|
|
312
312
|
if (!ref) throw new UsageError('missing <agent>', 'frontera agent list — then pass an id or slug')
|
|
313
313
|
if (!installId) throw new UsageError('missing <plugin>', `frontera capability installs ${ref}`)
|
|
314
314
|
|
|
315
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
315
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
316
316
|
const agentId = await resolveAgentRef(api, ref)
|
|
317
317
|
const result = await api.unbindAgentInstall(agentId, installId).catch((e: unknown) => {
|
|
318
318
|
throw new CliError((e as Error).message, {
|
|
@@ -51,7 +51,7 @@ const list: Command = {
|
|
|
51
51
|
examples: ['frontera knowledge list', 'frontera knowledge list --json'],
|
|
52
52
|
},
|
|
53
53
|
async run(ctx) {
|
|
54
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
54
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
55
55
|
// The route takes the workspace explicitly, and a key holder cannot see
|
|
56
56
|
// its own workspace id from anywhere else — hence /v1/whoami.
|
|
57
57
|
const workspaceId = await requireWorkspaceId(api)
|
|
@@ -86,7 +86,7 @@ const sources: Command = {
|
|
|
86
86
|
const ref = ctx.positional[0]
|
|
87
87
|
if (!ref) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
88
88
|
|
|
89
|
-
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
89
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
90
90
|
const id = await resolveKnowledgeRef(client, ref)
|
|
91
91
|
const rows = (await client.knowledgeSources(id)) as Array<Record<string, unknown>>
|
|
92
92
|
|
|
@@ -124,7 +124,7 @@ const get: Command = {
|
|
|
124
124
|
const ref = ctx.positional[0]
|
|
125
125
|
if (!ref) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
126
126
|
|
|
127
|
-
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
127
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
128
128
|
const base = await client.knowledgeBase(await resolveKnowledgeRef(client, ref))
|
|
129
129
|
|
|
130
130
|
return {
|
|
@@ -188,7 +188,7 @@ const search: Command = {
|
|
|
188
188
|
throw new UsageError(`--threshold must be a number, not "${thresholdRaw}"`, 'try --threshold 0.7')
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
191
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
192
192
|
const baseId = await resolveKnowledgeRef(client, ref)
|
|
193
193
|
const result = await client.knowledgeSearch(baseId, {
|
|
194
194
|
query,
|
|
@@ -258,7 +258,7 @@ const retry: Command = {
|
|
|
258
258
|
throw new UsageError('missing <source>', `frontera knowledge sources ${ref} — the id column`)
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
261
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
262
262
|
const baseId = await resolveKnowledgeRef(client, ref)
|
|
263
263
|
const result = await client.retryKnowledgeSource(baseId, sourceId)
|
|
264
264
|
|
|
@@ -291,7 +291,7 @@ const create: Command = {
|
|
|
291
291
|
throw new UsageError('missing <name>', 'frontera knowledge create <name> [--description <text>]')
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
294
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
295
295
|
const workspaceId = await requireWorkspaceId(api)
|
|
296
296
|
|
|
297
297
|
// Checked here rather than left to the unique constraint: the service
|
|
@@ -379,7 +379,7 @@ const upload: Command = {
|
|
|
379
379
|
})
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
382
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
383
383
|
const baseId = await resolveKnowledgeRef(api, ref)
|
|
384
384
|
const outcomes = await uploadAll(api, baseId, plan.files, strategy)
|
|
385
385
|
const summary = summarizeUploads(ref, baseId, outcomes, plan)
|
|
@@ -415,7 +415,7 @@ const attach: Command = {
|
|
|
415
415
|
if (!baseRef) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
416
416
|
if (!agentRef) throw new UsageError('missing <agent>', 'frontera agent list — then pass a slug or id')
|
|
417
417
|
|
|
418
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
418
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
419
419
|
const baseId = await resolveKnowledgeRef(api, baseRef)
|
|
420
420
|
const agentId = await resolveAgentRef(api, agentRef)
|
|
421
421
|
await api.attachKnowledgeAgent(baseId, agentId)
|
|
@@ -450,7 +450,7 @@ const detach: Command = {
|
|
|
450
450
|
if (!baseRef) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
451
451
|
if (!agentRef) throw new UsageError('missing <agent>', 'frontera agent list — then pass a slug or id')
|
|
452
452
|
|
|
453
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
453
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
454
454
|
const baseId = await resolveKnowledgeRef(api, baseRef)
|
|
455
455
|
const agentId = await resolveAgentRef(api, agentRef)
|
|
456
456
|
await api.detachKnowledgeAgent(baseId, agentId)
|
|
@@ -477,7 +477,7 @@ const agents: Command = {
|
|
|
477
477
|
const ref = ctx.positional[0]
|
|
478
478
|
if (!ref) throw new UsageError('missing <base>', 'frontera knowledge list — then pass a name or id')
|
|
479
479
|
|
|
480
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
480
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
481
481
|
const baseId = await resolveKnowledgeRef(api, ref)
|
|
482
482
|
const rows = (await api.knowledgeAttachments(baseId)) as Array<Record<string, unknown>>
|
|
483
483
|
|
|
@@ -65,7 +65,7 @@ const list: Command = {
|
|
|
65
65
|
examples: ['frontera pack list', 'frontera pack list --json'],
|
|
66
66
|
},
|
|
67
67
|
async run(ctx) {
|
|
68
|
-
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token).packs()) as PackListEntry[]
|
|
68
|
+
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).packs()) as PackListEntry[]
|
|
69
69
|
|
|
70
70
|
return {
|
|
71
71
|
data: rows,
|
|
@@ -101,7 +101,7 @@ const get: Command = {
|
|
|
101
101
|
const ref = ctx.positional[0]
|
|
102
102
|
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
103
103
|
|
|
104
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
104
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
105
105
|
const detail = (await api.pack(await resolvePackRef(api, ref))) as {
|
|
106
106
|
pack?: PackRow
|
|
107
107
|
install?: unknown
|
|
@@ -177,7 +177,7 @@ const push: Command = {
|
|
|
177
177
|
throw new UsageError('the manifest has no `name`', 'a pack is addressed by name everywhere else')
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
180
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
181
181
|
const rows = (await api.packs()) as PackListEntry[]
|
|
182
182
|
const existing = rows.find(
|
|
183
183
|
(r) => (r.pack?.name ?? '').toLowerCase() === manifest.name!.toLowerCase(),
|
|
@@ -222,7 +222,7 @@ const install: Command = {
|
|
|
222
222
|
const ref = ctx.positional[0]
|
|
223
223
|
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
224
224
|
|
|
225
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
225
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
226
226
|
const id = await resolvePackRef(api, ref)
|
|
227
227
|
const result = (await api.installPack(id)) as { skills?: unknown[] }
|
|
228
228
|
|
|
@@ -252,7 +252,7 @@ const uninstall: Command = {
|
|
|
252
252
|
const ref = ctx.positional[0]
|
|
253
253
|
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
254
254
|
|
|
255
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
255
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
256
256
|
const id = await resolvePackRef(api, ref)
|
|
257
257
|
const removeApps = flagBool(ctx, 'remove-apps')
|
|
258
258
|
const result = await api.uninstallPack(id, removeApps)
|
|
@@ -285,7 +285,7 @@ const apply: Command = {
|
|
|
285
285
|
if (!packRef) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
286
286
|
if (!agentRef) throw new UsageError('missing <agent>', 'frontera agent list — then pass a slug or id')
|
|
287
287
|
|
|
288
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
288
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
289
289
|
const packId = await resolvePackRef(api, packRef)
|
|
290
290
|
const agentId = await resolveAgentRef(api, agentRef)
|
|
291
291
|
const result = await api.applyPackToAgent(packId, agentId)
|
|
@@ -323,7 +323,7 @@ const detach: Command = {
|
|
|
323
323
|
if (!packRef) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
324
324
|
if (!agentRef) throw new UsageError('missing <agent>', 'frontera agent list — then pass a slug or id')
|
|
325
325
|
|
|
326
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
326
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
327
327
|
const packId = await resolvePackRef(api, packRef)
|
|
328
328
|
const agentId = await resolveAgentRef(api, agentRef)
|
|
329
329
|
await api.removePackFromAgent(packId, agentId)
|
|
@@ -348,7 +348,7 @@ const remove: Command = {
|
|
|
348
348
|
const ref = ctx.positional[0]
|
|
349
349
|
if (!ref) throw new UsageError('missing <pack>', 'frontera pack list — then pass a name or id')
|
|
350
350
|
|
|
351
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
351
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
352
352
|
const id = await resolvePackRef(api, ref)
|
|
353
353
|
await api.deletePack(id)
|
|
354
354
|
|
|
@@ -34,7 +34,7 @@ const list: Command = {
|
|
|
34
34
|
examples: ['frontera plugin list', 'frontera plugin list --json'],
|
|
35
35
|
},
|
|
36
36
|
async run(ctx) {
|
|
37
|
-
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token).pluginInstalls()) as InstallRow[]
|
|
37
|
+
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).pluginInstalls()) as InstallRow[]
|
|
38
38
|
return {
|
|
39
39
|
data: rows,
|
|
40
40
|
text:
|
|
@@ -63,7 +63,7 @@ const catalog: Command = {
|
|
|
63
63
|
examples: ['frontera plugin catalog'],
|
|
64
64
|
},
|
|
65
65
|
async run(ctx) {
|
|
66
|
-
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token).pluginCatalog()) as Array<{
|
|
66
|
+
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).pluginCatalog()) as Array<{
|
|
67
67
|
kind?: string
|
|
68
68
|
displayName?: string
|
|
69
69
|
authType?: string
|
|
@@ -94,7 +94,7 @@ const get: Command = {
|
|
|
94
94
|
const installId = ctx.positional[0]
|
|
95
95
|
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list')
|
|
96
96
|
|
|
97
|
-
const install = await new PlatformApi(ctx.apiUrl, ctx.token).plugin(installId)
|
|
97
|
+
const install = await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).plugin(installId)
|
|
98
98
|
return {
|
|
99
99
|
data: install,
|
|
100
100
|
text: table(
|
|
@@ -143,7 +143,7 @@ const add: Command = {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const result = await new PlatformApi(ctx.apiUrl, ctx.token).createPluginInstall({
|
|
146
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).createPluginInstall({
|
|
147
147
|
kind,
|
|
148
148
|
...(flagString(ctx, 'name') ? { installName: flagString(ctx, 'name')! } : {}),
|
|
149
149
|
...(config ? { config } : {}),
|
|
@@ -205,7 +205,7 @@ const configure: Command = {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
const result = await new PlatformApi(ctx.apiUrl, ctx.token).updatePluginInstall(installId, {
|
|
208
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).updatePluginInstall(installId, {
|
|
209
209
|
...(name ? { installName: name } : {}),
|
|
210
210
|
...(config ? { config } : {}),
|
|
211
211
|
})
|
|
@@ -270,7 +270,7 @@ const policy: Command = {
|
|
|
270
270
|
throw new UsageError('nothing to change', 'pass --enable, --disable, --actions or --new-capability')
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
const result = await new PlatformApi(ctx.apiUrl, ctx.token).patchPluginInstall(installId, {
|
|
273
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).patchPluginInstall(installId, {
|
|
274
274
|
...(enable ? { enabled: true } : {}),
|
|
275
275
|
...(disable ? { enabled: false } : {}),
|
|
276
276
|
...(actions ? { actionPolicy: actions as 'allow_all' | 'read_only' | 'custom' } : {}),
|
|
@@ -297,7 +297,7 @@ const verify: Command = {
|
|
|
297
297
|
// A failed check still answers 200 — the verdict is in the body, the same
|
|
298
298
|
// shape `dataset test-source` established. Exiting non-zero here would
|
|
299
299
|
// make an unreachable plugin indistinguishable from an unreachable API.
|
|
300
|
-
const result = await new PlatformApi(ctx.apiUrl, ctx.token).verifyPluginInstall(installId)
|
|
300
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).verifyPluginInstall(installId)
|
|
301
301
|
|
|
302
302
|
return {
|
|
303
303
|
data: result,
|
|
@@ -326,7 +326,7 @@ const remove: Command = {
|
|
|
326
326
|
const installId = ctx.positional[0]
|
|
327
327
|
if (!installId) throw new UsageError('missing <plugin>', 'frontera plugin list')
|
|
328
328
|
|
|
329
|
-
const result = await new PlatformApi(ctx.apiUrl, ctx.token).deletePluginInstall(installId)
|
|
329
|
+
const result = await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).deletePluginInstall(installId)
|
|
330
330
|
return {
|
|
331
331
|
data: result,
|
|
332
332
|
text: `Uninstalled ${installId}.\n`
|
package/src/commands/registry.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { loginCommand } from './login'
|
|
|
40
40
|
import { setupCommand } from './setup'
|
|
41
41
|
import { authCommands } from './auth/index-commands'
|
|
42
42
|
import { kitCommands } from './kit/index-commands'
|
|
43
|
+
import { workspaceCommands } from './workspace/index-commands'
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Flags every command accepts. Declared once so a caller never has to wonder
|
|
@@ -55,6 +56,10 @@ export const GLOBAL_FLAGS: FlagSpec = {
|
|
|
55
56
|
// auth ones — a one-command escape hatch is useless if it exists on six
|
|
56
57
|
// commands and the seventh is the one being diagnosed.
|
|
57
58
|
profile: 'string',
|
|
59
|
+
// Organization keys only. A workspace key already names its workspace; an
|
|
60
|
+
// organization key spans a list, and the service refuses to guess — leaving
|
|
61
|
+
// an unaddressed request at org scope is WIDER than what was asked for.
|
|
62
|
+
workspace: 'string',
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/** Additionally accepted by any command that resolves a project. */
|
|
@@ -68,6 +73,7 @@ export const COMMANDS: readonly Command[] = [
|
|
|
68
73
|
|
|
69
74
|
...authCommands,
|
|
70
75
|
...kitCommands,
|
|
76
|
+
...workspaceCommands,
|
|
71
77
|
|
|
72
78
|
appInit,
|
|
73
79
|
appList,
|
|
@@ -142,6 +148,7 @@ const NOUN_SUMMARY: Readonly<Record<string, string>> = {
|
|
|
142
148
|
capability: 'What an agent may DO — plugin operations granted to it',
|
|
143
149
|
auth: 'Credential profiles — one per API key, selected by directory',
|
|
144
150
|
kit: 'Optional project-local Frontera skills for Codex and Claude Code',
|
|
151
|
+
workspace: 'Workspaces here — the ids --workspace takes',
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
export function nounSummary(noun: string): string {
|
|
@@ -171,9 +178,6 @@ export const UNSUPPORTED: Readonly<Record<string, string>> = {
|
|
|
171
178
|
channel:
|
|
172
179
|
'channels have no CLI surface — create one and bind it to an agent in the Console, '
|
|
173
180
|
+ '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
181
|
key: 'API keys are minted in the Console, never by the CLI — a credential that mints credentials '
|
|
178
182
|
+ 'is the escalation that separation exists to prevent',
|
|
179
183
|
sheet: 'sheets have no CLI surface and none is planned',
|
|
@@ -51,7 +51,7 @@ const list: Command = {
|
|
|
51
51
|
examples: ['frontera secret list', 'frontera secret list --json'],
|
|
52
52
|
},
|
|
53
53
|
async run(ctx) {
|
|
54
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
54
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
55
55
|
const rows = (await api.workspaceSecrets(await requireWorkspaceId(api))) as SecretRow[]
|
|
56
56
|
|
|
57
57
|
return {
|
|
@@ -129,7 +129,7 @@ const set: Command = {
|
|
|
129
129
|
})
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
132
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
133
133
|
const workspaceId = await requireWorkspaceId(api)
|
|
134
134
|
const description = flagString(ctx, 'description')
|
|
135
135
|
|
|
@@ -179,7 +179,7 @@ const remove: Command = {
|
|
|
179
179
|
const name = ctx.positional[0]
|
|
180
180
|
if (!name) throw new UsageError('missing <name>', 'frontera secret list — then pass a name')
|
|
181
181
|
|
|
182
|
-
const api = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
182
|
+
const api = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
183
183
|
await api.deleteWorkspaceSecret(await requireWorkspaceId(api), name)
|
|
184
184
|
|
|
185
185
|
// No `--force`. The service refuses while anything still resolves the
|
|
@@ -125,7 +125,7 @@ const pull: Command = {
|
|
|
125
125
|
const ref = ctx.positional[0]
|
|
126
126
|
if (!ref) throw new UsageError('missing <skill>', 'frontera skill pull <skill> [--dir <path>]')
|
|
127
127
|
|
|
128
|
-
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
128
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
129
129
|
const id = await resolveSkillRef(client, ref)
|
|
130
130
|
const doc = (await client.workspaceSkill(id)) as SkillDoc
|
|
131
131
|
if (!doc.name) throw new CliError('skill has no name', { code: 'INTERNAL_ERROR', hint: 'run `frontera skill list` and pull by id' })
|
|
@@ -320,8 +320,8 @@ const push: Command = {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
// One PlatformApi per run is enough; a tiny helper keeps push readable.
|
|
323
|
-
function client(ctx: { apiUrl: string; token: string }): PlatformApi {
|
|
324
|
-
return new PlatformApi(ctx.apiUrl, ctx.token)
|
|
323
|
+
function client(ctx: { apiUrl: string; token: string; workspaceId?: string }): PlatformApi {
|
|
324
|
+
return new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
export const skillBundleCommands: Command[] = [pull, push]
|
|
@@ -29,7 +29,7 @@ const list: Command = {
|
|
|
29
29
|
examples: ['frontera skill list', 'frontera skill list --json'],
|
|
30
30
|
},
|
|
31
31
|
async run(ctx) {
|
|
32
|
-
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token).workspaceSkills()) as SkillRow[]
|
|
32
|
+
const rows = (await new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId).workspaceSkills()) as SkillRow[]
|
|
33
33
|
return {
|
|
34
34
|
data: rows,
|
|
35
35
|
text:
|
|
@@ -59,7 +59,7 @@ const get: Command = {
|
|
|
59
59
|
const ref = ctx.positional[0]
|
|
60
60
|
if (!ref) throw new UsageError('missing <skill>', 'frontera skill list — then pass a name or id')
|
|
61
61
|
|
|
62
|
-
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
62
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
63
63
|
// Name or id, like every other noun. `skill list` leads with the id but a
|
|
64
64
|
// reader reaches for the name, and an id-only argument made this the one
|
|
65
65
|
// command that refused the obvious input.
|
|
@@ -118,7 +118,7 @@ const remove: Command = {
|
|
|
118
118
|
const ref = ctx.positional[0]
|
|
119
119
|
if (!ref) throw new UsageError('missing <skill>', 'frontera skill list — then pass a name or id')
|
|
120
120
|
|
|
121
|
-
const client = new PlatformApi(ctx.apiUrl, ctx.token)
|
|
121
|
+
const client = new PlatformApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
|
|
122
122
|
const id = await resolveSkillRef(client, ref)
|
|
123
123
|
await client.deleteWorkspaceSkill(id)
|
|
124
124
|
|
package/src/commands/types.ts
CHANGED
|
@@ -63,6 +63,11 @@ export interface CommandContext {
|
|
|
63
63
|
output: Output
|
|
64
64
|
/** Present when `meta.needsProject`. */
|
|
65
65
|
project: AppProject | null
|
|
66
|
+
/**
|
|
67
|
+
* `--workspace`, for an organization key that must name which workspace it is
|
|
68
|
+
* acting in. Undefined for a workspace key, which carries its own.
|
|
69
|
+
*/
|
|
70
|
+
workspaceId?: string
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
/**
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BlueprintAuthoringApi } from '../../api/blueprint-authoring-api'
|
|
2
|
+
import { table } from '../../table'
|
|
3
|
+
import type { Command } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Workspaces, read-only.
|
|
7
|
+
*
|
|
8
|
+
* This noun exists for one reason: `--workspace <id>` is how an organization
|
|
9
|
+
* key names where it is acting, and nothing in the CLI could tell you what to
|
|
10
|
+
* put there. The id had to come from the Console or a database, which made a
|
|
11
|
+
* flag that exists to unblock organization keys unusable without leaving the
|
|
12
|
+
* terminal.
|
|
13
|
+
*
|
|
14
|
+
* The read is organization-scoped (`organization: ['update']`), which is what
|
|
15
|
+
* an organization key carries — so the credential that needs the answer is the
|
|
16
|
+
* one that can ask. A workspace key already knows its own workspace and does
|
|
17
|
+
* not need this.
|
|
18
|
+
*
|
|
19
|
+
* Managing workspaces stays out. Creating, deleting and membership are
|
|
20
|
+
* Console-only, and the verbs below are declared rather than omitted so that
|
|
21
|
+
* reaching for one says why instead of "unknown verb".
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const list: Command = {
|
|
25
|
+
meta: {
|
|
26
|
+
noun: 'workspace',
|
|
27
|
+
verb: 'list',
|
|
28
|
+
args: [],
|
|
29
|
+
flags: {},
|
|
30
|
+
summary: 'List workspaces in this organization, with the ids --workspace takes',
|
|
31
|
+
examples: ['frontera workspace list', 'frontera workspace list --json'],
|
|
32
|
+
},
|
|
33
|
+
async run(ctx) {
|
|
34
|
+
const rows = await new BlueprintAuthoringApi(ctx.apiUrl, ctx.token).orgWorkspaces()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
data: rows,
|
|
38
|
+
text: rows.length === 0
|
|
39
|
+
? 'No workspaces visible to this credential.\n'
|
|
40
|
+
+ ' This read needs an organization key (sk-org-…) carrying organization:update.'
|
|
41
|
+
: // ID first: it is the value `--workspace` takes, and the reason to
|
|
42
|
+
// run this at all. Name and slug are for recognising which is which.
|
|
43
|
+
table(
|
|
44
|
+
['id', 'name', 'slug'],
|
|
45
|
+
rows.map((w) => [w.id ?? '?', w.name ?? '', w.slug ?? '']),
|
|
46
|
+
[undefined, 34, undefined],
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const RESERVED = 'the CLI reads workspaces, it does not manage them — create, delete and '
|
|
53
|
+
+ 'membership are Console-only'
|
|
54
|
+
|
|
55
|
+
const reserved: Command[] = (
|
|
56
|
+
[
|
|
57
|
+
['create', 'Create a workspace'],
|
|
58
|
+
['delete', 'Delete a workspace'],
|
|
59
|
+
['members', 'List or change who belongs to a workspace'],
|
|
60
|
+
] as const
|
|
61
|
+
).map(([verb, summary]) => ({
|
|
62
|
+
meta: {
|
|
63
|
+
noun: 'workspace',
|
|
64
|
+
verb,
|
|
65
|
+
args: [],
|
|
66
|
+
flags: {},
|
|
67
|
+
summary,
|
|
68
|
+
examples: [`frontera workspace ${verb}`],
|
|
69
|
+
reserved: RESERVED,
|
|
70
|
+
},
|
|
71
|
+
async run(): Promise<never> {
|
|
72
|
+
throw new Error(RESERVED)
|
|
73
|
+
},
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
export const workspaceCommands: Command[] = [list, ...reserved]
|
package/src/exit.ts
CHANGED
|
@@ -44,6 +44,10 @@ const USAGE_CODES = new Set([
|
|
|
44
44
|
'PROJECT_CONTEXT_NOT_FOUND',
|
|
45
45
|
'PROJECT_CONTEXT_CORRUPT',
|
|
46
46
|
'PROJECT_CONTEXT_UNTRUSTED',
|
|
47
|
+
// The directory cannot be written to. Fixed by binding somewhere else or by
|
|
48
|
+
// not binding at all — never by retrying, which is why it sits with USAGE
|
|
49
|
+
// rather than FAILURE.
|
|
50
|
+
'PROJECT_CONTEXT_UNWRITABLE',
|
|
47
51
|
// Authoring kit. Both are resolved by running a named command, not by retry.
|
|
48
52
|
'KIT_VERSION_MISMATCH',
|
|
49
53
|
'GENERATED_FILE_CONFLICT',
|
|
@@ -82,7 +86,12 @@ const SERVICE_HINTS: Record<string, string> = {
|
|
|
82
86
|
// which case they are in throw a CliError with a specific hint instead.
|
|
83
87
|
CONFLICT: 'the message above names what conflicted — resolve that, then retry',
|
|
84
88
|
VALIDATION_STALE: 're-fetch the resource and re-run validation against the current revision',
|
|
85
|
-
|
|
89
|
+
// Names neither kind. Both are valid here — a workspace key for agents,
|
|
90
|
+
// skills, plugins and knowledge; an organization key for Blueprint authoring,
|
|
91
|
+
// datasets and Sources — and naming only one sent holders of the other
|
|
92
|
+
// looking for a credential they already had.
|
|
93
|
+
UNAUTHORIZED: 'run `frontera auth current` to see which profile this resolves to, '
|
|
94
|
+
+ 'then `frontera auth verify` — or set FRONTERA_TOKEN',
|
|
86
95
|
FORBIDDEN: 'this credential lacks permission for that resource — ask an admin, or use a different key',
|
|
87
96
|
NOT_FOUND: 'list the resource first to confirm the id',
|
|
88
97
|
RATE_LIMITED: 'wait and retry',
|
package/src/flag-help.ts
CHANGED
|
@@ -43,6 +43,9 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
43
43
|
help: 'show this help',
|
|
44
44
|
'api-url': "API origin to use — must match the selected profile's origin",
|
|
45
45
|
profile: 'credential profile to use for this command, overriding the directory selection',
|
|
46
|
+
workspace:
|
|
47
|
+
"workspace to act in, for an organization key (sk-org-…) — it must be on that key's own "
|
|
48
|
+
+ 'list. A workspace key already names one and ignores this',
|
|
46
49
|
output: 'project-relative .ts path for generated output',
|
|
47
50
|
check: 'verify generated output is current without writing it',
|
|
48
51
|
|
|
@@ -152,6 +155,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
152
155
|
*/
|
|
153
156
|
const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
154
157
|
'api-url': 'origin',
|
|
158
|
+
workspace: 'id',
|
|
155
159
|
profile: 'name',
|
|
156
160
|
output: 'path',
|
|
157
161
|
dataset: 'name',
|
package/src/main.ts
CHANGED
|
@@ -184,6 +184,61 @@ async function main(): Promise<number> {
|
|
|
184
184
|
{ cwd },
|
|
185
185
|
)
|
|
186
186
|
|
|
187
|
+
const workspaceFlag = typeof flags.workspace === 'string' ? flags.workspace.trim() : undefined
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Nouns whose commands build a `PlatformApi`, which is the only client that
|
|
191
|
+
* carries `x-workspace-id`.
|
|
192
|
+
*
|
|
193
|
+
* `--workspace` is global — same reasoning as `--profile`: an escape hatch
|
|
194
|
+
* that exists on six commands and not the seventh is useless when the
|
|
195
|
+
* seventh is the one being diagnosed. But global must not mean "silently
|
|
196
|
+
* ignored where it cannot work": passing it to `dataset rows` sent nothing
|
|
197
|
+
* and reported nothing, which is the plausible-wrong-answer shape this CLI
|
|
198
|
+
* refuses everywhere else.
|
|
199
|
+
*
|
|
200
|
+
* Listed rather than derived because the mapping is a fact about which
|
|
201
|
+
* client a command constructs, and a wrong entry should be a failing test
|
|
202
|
+
* rather than a silent no-op.
|
|
203
|
+
*/
|
|
204
|
+
const WORKSPACE_ADDRESSABLE = new Set([
|
|
205
|
+
'agent', 'app', 'blueprint', 'capability', 'knowledge',
|
|
206
|
+
'pack', 'plugin', 'secret', 'skill',
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
if (workspaceFlag && !WORKSPACE_ADDRESSABLE.has(command.meta.noun)) {
|
|
210
|
+
throw new UsageError(
|
|
211
|
+
`--workspace does nothing on \`${command.meta.noun}\``,
|
|
212
|
+
'these commands are organization-scoped, or need no credential at all. '
|
|
213
|
+
+ '--workspace applies to: ' + [...WORKSPACE_ADDRESSABLE].sort().join(', '),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// An organization key with no workspace named runs at ORG scope, and a
|
|
218
|
+
// workspace-scoped read then answers with an empty list rather than a
|
|
219
|
+
// refusal — `agent list` said "No agents in this workspace" for a workspace
|
|
220
|
+
// holding thirty. A plausible wrong answer is worse than an error, so say
|
|
221
|
+
// so before the command runs.
|
|
222
|
+
//
|
|
223
|
+
// Exempt where org scope is the point: those nouns are correct as-is, and
|
|
224
|
+
// `workspace list` is the FIRST thing such a caller runs to find an id.
|
|
225
|
+
// Listed rather than inferred — there is no scope field on a command, and
|
|
226
|
+
// adding one to silence a note would be the wrong trade.
|
|
227
|
+
const ORG_SCOPED_NOUNS = new Set(['blueprint', 'dataset', 'source', 'workspace', 'auth', 'kit'])
|
|
228
|
+
if (
|
|
229
|
+
!command.meta.offline
|
|
230
|
+
&& !workspaceFlag
|
|
231
|
+
&& !ORG_SCOPED_NOUNS.has(command.meta.noun)
|
|
232
|
+
&& credential.token.startsWith('sk-org-')
|
|
233
|
+
) {
|
|
234
|
+
output.note(
|
|
235
|
+
'note: organization key with no --workspace. Organization-scoped commands '
|
|
236
|
+
+ '(blueprint authoring, dataset, source) work as-is; workspace-scoped ones '
|
|
237
|
+
+ '(agent, skill, plugin, capability, knowledge, pack, secret) will read nothing. '
|
|
238
|
+
+ '`frontera workspace list` shows the ids.',
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
187
242
|
const result = await command.run({
|
|
188
243
|
cwd,
|
|
189
244
|
apiUrl: credential.apiUrl,
|
|
@@ -192,6 +247,7 @@ async function main(): Promise<number> {
|
|
|
192
247
|
flags,
|
|
193
248
|
output,
|
|
194
249
|
project,
|
|
250
|
+
...(workspaceFlag ? { workspaceId: workspaceFlag } : {}),
|
|
195
251
|
})
|
|
196
252
|
|
|
197
253
|
output.render(result.data, () => result.text)
|
package/src/project-context.ts
CHANGED
|
@@ -139,7 +139,28 @@ export function resolveBindingRoot(
|
|
|
139
139
|
export function writeContext(root: string, profile: string, env: Env = process.env): string {
|
|
140
140
|
const path = contextPath(root)
|
|
141
141
|
const body: ProjectContext = { schemaVersion: 1, profile }
|
|
142
|
-
|
|
142
|
+
try {
|
|
143
|
+
writeFileAtomic(path, `${JSON.stringify(body, null, 2)}\n`, 0o600)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// A directory nobody owns is an ordinary place to be standing — `/Users` on
|
|
146
|
+
// macOS, `/` in a container, a read-only mount in CI. The raw ENOENT/EACCES
|
|
147
|
+
// escaped as `error [EACCES]: EACCES: permission denied, mkdir …`: a code
|
|
148
|
+
// outside the CLI's own set, no hint, and mapped to exit 1, which tells a
|
|
149
|
+
// caller to retry something no retry can fix.
|
|
150
|
+
//
|
|
151
|
+
// Named alternatives rather than just the refusal, because binding is not
|
|
152
|
+
// the only way to select a profile — and in a directory like this it is the
|
|
153
|
+
// wrong one anyway.
|
|
154
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
155
|
+
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS' || code === 'ENOSPC') {
|
|
156
|
+
throw new CliError(`cannot write a profile binding into ${root}`, {
|
|
157
|
+
code: 'PROJECT_CONTEXT_UNWRITABLE',
|
|
158
|
+
hint: 'cd to a directory you own and run it there, or bind a different one with '
|
|
159
|
+
+ '`--dir <path>` — or skip binding entirely with `--profile <name>` or FRONTERA_PROFILE',
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
throw err
|
|
163
|
+
}
|
|
143
164
|
recordBinding(root, profile, env)
|
|
144
165
|
return path
|
|
145
166
|
}
|