Skip to content

Commit f37c3b7

Browse files
authored
feat: add workspace add-api and list-apis commands (#254)
Adds non-interactive CLI commands for discovering API services and subscribing them to a Workspace, completing the fully-scripted App Builder project bootstrap workflow. New commands: - `aio console api list` — list API services available to the Organization. Flags services that require a product profile. - `aio console workspace api list` — list API services currently subscribed to a Workspace, including their product profiles. - `aio console workspace api add` — subscribe one or more API services to a Workspace by service code, using OAuth S2S credentials. Supports product profiles via repeatable `--license-config '<sdkCode>=<profileNameOrId>[,...]'`. Profiles match by id or name; when a service requires profiles but none are provided, the error lists the available profiles. All commands resolve project/workspace by name and support `--json` / `--yml` output. Together with `aio console project create` and `aio console workspace create`, this enables fully non-interactive project setup, e.g.: aio console org select $ORG_ID aio console project create -n myproject aio console workspace create --projectName myproject --name dev aio console api list --json aio console workspace api add \ --projectName myproject --workspaceName dev \ --service-code AdobeAnalyticsSDK \ --license-config 'AdobeAnalyticsSDK=Analytics - Adobe IO DEMO' 55 new tests across 5 suites; 100% coverage on all new files. Closes ACNA-4538
1 parent e01ac4b commit f37c3b7

10 files changed

Lines changed: 1376 additions & 0 deletions

File tree

src/commands/console/api/index.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2026 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
Unless required by applicable law or agreed to in writing, software distributed under
7+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8+
OF ANY KIND, either express or implied. See the License for the specific language
9+
governing permissions and limitations under the License.
10+
*/
11+
12+
const { Help } = require('@oclif/core')
13+
const ConsoleCommand = require('../')
14+
15+
class IndexCommand extends ConsoleCommand {
16+
async run () {
17+
const help = new Help(this.config)
18+
await help.showHelp(['console:api', '--help'])
19+
}
20+
}
21+
22+
IndexCommand.description = 'Manage API services available to your Organization'
23+
24+
module.exports = IndexCommand

src/commands/console/api/list.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2026 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
const { Flags } = require('@oclif/core')
13+
const ConsoleCommand = require('../index')
14+
const aioConsoleLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-console:api:list', { provider: 'debug' })
15+
16+
class ListCommand extends ConsoleCommand {
17+
async run () {
18+
const { flags } = await this.parse(ListCommand)
19+
20+
const orgId = flags.orgId || this.getConfig('org.id')
21+
if (!orgId) {
22+
this.log('You have not selected an Organization. Please select one first.')
23+
this.printConsoleConfig()
24+
this.exit(1)
25+
}
26+
27+
await this.initSdk()
28+
29+
try {
30+
const enabledServices = await this.consoleCLI.getEnabledServicesForOrg(orgId)
31+
aioConsoleLogger.debug(`Enabled services: ${JSON.stringify(enabledServices.map(s => s.code))}`)
32+
33+
if (flags.json) {
34+
this.printJson(enabledServices)
35+
} else if (flags.yml) {
36+
this.printYaml(enabledServices)
37+
} else {
38+
if (enabledServices.length === 0) {
39+
this.log('No enabled API services found for this Organization.')
40+
return []
41+
}
42+
this.log(`Enabled API services for the Organization (${enabledServices.length}):`)
43+
this.log('')
44+
for (const service of enabledServices) {
45+
const hasProfiles = Boolean(service.properties && service.properties.licenseConfigs && service.properties.licenseConfigs.length > 0)
46+
this.log(` ${service.code}`)
47+
this.log(` Name: ${service.name}`)
48+
if (hasProfiles) {
49+
this.log(' Requires product profile: yes')
50+
}
51+
this.log('')
52+
}
53+
}
54+
55+
return enabledServices
56+
} catch (err) {
57+
aioConsoleLogger.debug(err)
58+
this.error(err.message)
59+
} finally {
60+
this.cleanOutput()
61+
}
62+
}
63+
}
64+
65+
ListCommand.description = 'List API services available to the Organization'
66+
67+
ListCommand.flags = {
68+
...ConsoleCommand.flags,
69+
orgId: Flags.string({
70+
description: 'Organization id'
71+
}),
72+
json: Flags.boolean({
73+
description: 'Output json',
74+
char: 'j',
75+
exclusive: ['yml']
76+
}),
77+
yml: Flags.boolean({
78+
description: 'Output yml',
79+
char: 'y',
80+
exclusive: ['json']
81+
})
82+
}
83+
84+
ListCommand.aliases = [
85+
'console:api:ls'
86+
]
87+
88+
module.exports = ListCommand
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
Copyright 2026 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
const { Flags } = require('@oclif/core')
13+
const ConsoleCommand = require('../../index')
14+
const aioConsoleLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-console:workspace:api:add', { provider: 'debug' })
15+
const LibConsoleCLI = require('@adobe/aio-cli-lib-console')
16+
17+
/**
18+
* Parse --license-config flag values into a map of sdkCode -> array of profile
19+
* identifiers (matched later against profile name or id).
20+
*
21+
* Format: "<sdkCode>=<nameOrId>[,<nameOrId>...]"
22+
*
23+
* @param {string[]} values raw flag values
24+
* @returns {Object<string, string[]>} map of sdkCode to list of profile identifiers
25+
*/
26+
function parseLicenseConfigFlags (values) {
27+
const result = {}
28+
for (const raw of values) {
29+
const eq = raw.indexOf('=')
30+
if (eq <= 0) {
31+
throw new Error(`Invalid --license-config value '${raw}'. Expected format: '<sdkCode>=<profileNameOrId>[,<profileNameOrId>...]'.`)
32+
}
33+
const sdkCode = raw.slice(0, eq).trim()
34+
const rest = raw.slice(eq + 1)
35+
const profiles = rest.split(',').map(s => s.trim()).filter(Boolean)
36+
if (!sdkCode || profiles.length === 0) {
37+
throw new Error(`Invalid --license-config value '${raw}'. Expected format: '<sdkCode>=<profileNameOrId>[,<profileNameOrId>...]'.`)
38+
}
39+
if (!result[sdkCode]) {
40+
result[sdkCode] = []
41+
}
42+
result[sdkCode].push(...profiles)
43+
}
44+
return result
45+
}
46+
47+
/**
48+
* Match requested profile identifiers against a service's available
49+
* licenseConfigs by either id or name.
50+
*
51+
* @param {Array<{id: string, name: string, productId: string}>} available
52+
* @param {string[]} requested profile names or ids
53+
* @param {string} sdkCode service code for error messages
54+
* @returns {Array} selected licenseConfig objects
55+
*/
56+
function resolveLicenseConfigs (available, requested, sdkCode) {
57+
const selected = []
58+
const notFound = []
59+
for (const id of requested) {
60+
const match = available.find(lc => lc.id === id || lc.name === id)
61+
if (match) {
62+
selected.push(match)
63+
} else {
64+
notFound.push(id)
65+
}
66+
}
67+
if (notFound.length > 0) {
68+
const availableNames = available.map(lc => lc.name).join(', ')
69+
throw new Error(
70+
`Product profile(s) not found for service ${sdkCode}: ${notFound.join(', ')}. ` +
71+
`Available profiles: ${availableNames}.`
72+
)
73+
}
74+
return selected
75+
}
76+
77+
class AddCommand extends ConsoleCommand {
78+
async run () {
79+
const { flags } = await this.parse(AddCommand)
80+
81+
const orgId = flags.orgId || this.getConfig('org.id')
82+
if (!orgId) {
83+
this.log('You have not selected an Organization. Please select one first.')
84+
this.printConsoleConfig()
85+
this.exit(1)
86+
}
87+
88+
await this.initSdk()
89+
90+
try {
91+
const projects = await this.consoleCLI.getProjects(orgId)
92+
const project = projects.find(p => p.name === flags.projectName)
93+
if (!project) {
94+
this.error(`Project ${flags.projectName} not found in the Organization.`)
95+
}
96+
97+
const workspaces = await this.consoleCLI.getWorkspaces(orgId, project.id)
98+
const workspace = workspaces.find(ws => ws.name === flags.workspaceName)
99+
if (!workspace) {
100+
this.error(`Workspace ${flags.workspaceName} not found in Project ${flags.projectName}.`)
101+
}
102+
103+
const requestedCodes = flags['service-code'].split(',').map(s => s.trim()).filter(Boolean)
104+
if (requestedCodes.length === 0) {
105+
this.error('At least one service code must be provided.')
106+
}
107+
108+
const licenseConfigMap = parseLicenseConfigFlags(flags['license-config'] || [])
109+
110+
const enabledServices = await this.consoleCLI.getEnabledServicesForOrg(orgId)
111+
aioConsoleLogger.debug(`Enabled services: ${JSON.stringify(enabledServices.map(s => s.code))}`)
112+
113+
const serviceProperties = []
114+
const notFound = []
115+
const missingProfiles = []
116+
for (const code of requestedCodes) {
117+
const service = enabledServices.find(s => s.code === code)
118+
if (!service) {
119+
notFound.push(code)
120+
continue
121+
}
122+
123+
const availableProfiles = (service.properties && service.properties.licenseConfigs) || null
124+
let licenseConfigs = null
125+
if (availableProfiles && availableProfiles.length > 0) {
126+
const requestedProfiles = licenseConfigMap[code]
127+
if (!requestedProfiles || requestedProfiles.length === 0) {
128+
missingProfiles.push({ code, available: availableProfiles })
129+
continue
130+
}
131+
licenseConfigs = resolveLicenseConfigs(availableProfiles, requestedProfiles, code)
132+
}
133+
134+
serviceProperties.push({
135+
name: service.name,
136+
sdkCode: service.code,
137+
roles: (service.properties && service.properties.roles) || null,
138+
licenseConfigs
139+
})
140+
}
141+
142+
if (notFound.length > 0) {
143+
this.error(`Service code(s) not found or not enabled in the Organization: ${notFound.join(', ')}`)
144+
}
145+
146+
if (missingProfiles.length > 0) {
147+
const lines = missingProfiles.map(({ code, available }) => {
148+
const names = available.map(lc => lc.name).join(', ')
149+
return ` ${code}: ${names}`
150+
}).join('\n')
151+
this.error(
152+
'The following service(s) require one or more product profiles. ' +
153+
'Pass them with --license-config \'<sdkCode>=<profileNameOrId>[,...]\':\n' + lines
154+
)
155+
}
156+
157+
const result = await this.consoleCLI.subscribeToServicesWithCredentialType({
158+
orgId,
159+
project,
160+
workspace,
161+
serviceProperties,
162+
credentialType: LibConsoleCLI.OAUTH_SERVER_TO_SERVER_CREDENTIAL
163+
})
164+
165+
if (flags.json) {
166+
this.printJson(result)
167+
} else if (flags.yml) {
168+
this.printYaml(result)
169+
} else {
170+
this.log(`Successfully added API(s) ${requestedCodes.join(', ')} to Workspace ${workspace.name}.`)
171+
}
172+
173+
return result
174+
} catch (err) {
175+
aioConsoleLogger.debug(err)
176+
this.error(err.message)
177+
} finally {
178+
this.cleanOutput()
179+
}
180+
}
181+
}
182+
183+
AddCommand.description = 'Add API service(s) to a Workspace'
184+
185+
AddCommand.flags = {
186+
...ConsoleCommand.flags,
187+
orgId: Flags.string({
188+
description: 'Organization id'
189+
}),
190+
projectName: Flags.string({
191+
description: 'Name of the project containing the workspace',
192+
required: true
193+
}),
194+
workspaceName: Flags.string({
195+
description: 'Name of the workspace to add the API to',
196+
required: true
197+
}),
198+
'service-code': Flags.string({
199+
description: 'Comma-separated list of API service codes to add (e.g. AssetComputeSDK,AdobeAnalyticsSDK)',
200+
required: true
201+
}),
202+
'license-config': Flags.string({
203+
description: 'Product profile(s) for a service, format: \'<sdkCode>=<profileNameOrId>[,<profileNameOrId>...]\'. Repeat for multiple services.',
204+
multiple: true
205+
}),
206+
json: Flags.boolean({
207+
description: 'Output json',
208+
char: 'j',
209+
exclusive: ['yml']
210+
}),
211+
yml: Flags.boolean({
212+
description: 'Output yml',
213+
char: 'y',
214+
exclusive: ['json']
215+
})
216+
}
217+
218+
AddCommand.aliases = [
219+
'console:ws:api:add'
220+
]
221+
222+
module.exports = AddCommand
223+
module.exports.parseLicenseConfigFlags = parseLicenseConfigFlags
224+
module.exports.resolveLicenseConfigs = resolveLicenseConfigs
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Copyright 2026 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
Unless required by applicable law or agreed to in writing, software distributed under
7+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8+
OF ANY KIND, either express or implied. See the License for the specific language
9+
governing permissions and limitations under the License.
10+
*/
11+
12+
const { Help } = require('@oclif/core')
13+
const ConsoleCommand = require('../../')
14+
15+
class IndexCommand extends ConsoleCommand {
16+
async run () {
17+
const help = new Help(this.config)
18+
await help.showHelp(['console:workspace:api', '--help'])
19+
}
20+
}
21+
22+
IndexCommand.description = 'Manage API services subscribed to a Workspace'
23+
24+
IndexCommand.aliases = ['console:ws:api']
25+
26+
module.exports = IndexCommand

0 commit comments

Comments
 (0)