From 7f8c907ac41f89a5a2cd1ff53cba6ca38bf70c9a Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Tue, 9 Dec 2025 15:01:12 -0800 Subject: [PATCH 1/8] add AI template recommendation prompt to init command --- src/commands/app/init.js | 103 +++++++++++++++++++++++++++-- src/lib/template-recommendation.js | 51 ++++++++++++++ 2 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 src/lib/template-recommendation.js diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 5a525596..497505f4 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -24,6 +24,7 @@ const { importConsoleConfig } = require('../../lib/import') const { loadAndValidateConfigFile } = require('../../lib/import-helper') const { Octokit } = require('@octokit/rest') const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:init', { provider: 'debug' }) +const { getAIRecommendation } = require('../../lib/template-recommendation') const DEFAULT_WORKSPACE = 'Stage' @@ -107,9 +108,22 @@ class InitCommand extends TemplatesCommand { await this.withQuickstart(flags.repo, flags['github-pat']) } else { // 2. prompt for templates to be installed - - // TODO: Modify this to be a prompt for natural language here -mg - const templates = await this.getTemplatesForFlags(flags) + + // Ask user: Do you want to use natural language? + const { useNL } = await inquirer.prompt([{ + type: 'confirm', + name: 'useNL', + message: 'Do you want to describe your needs in natural language? (AI will recommend templates)', + default: false + }]) + + let templates + if (useNL) { + templates = await this.getTemplatesWithAI(flags) // NEW AI FUNCTION + } else { + templates = await this.getTemplatesForFlags(flags) // EXISTING TABLE FLOW + } + // If no templates selected, init a standalone app if (templates.length <= 0) { flags['standalone-app'] = true @@ -154,9 +168,21 @@ class InitCommand extends TemplatesCommand { let templates if (!flags.repo) { // 5. get list of templates to install - - // TODO: Modify this to be a prompt for natural language here -mg - templates = await this.getTemplatesForFlags(flags, orgSupportedServices) + + // Ask user: Do you want to use natural language? + const { useNL } = await inquirer.prompt([{ + type: 'confirm', + name: 'useNL', + message: 'Do you want to describe your needs in natural language? (AI will recommend templates)', + default: false + }]) + + if (useNL) { + templates = await this.getTemplatesWithAI(flags, orgSupportedServices) // NEW AI FUNCTION + } else { + templates = await this.getTemplatesForFlags(flags, orgSupportedServices) // EXISTING TABLE FLOW + } + // If no templates selected, init a standalone app if (templates.length <= 0) { flags['standalone-app'] = true @@ -187,6 +213,71 @@ class InitCommand extends TemplatesCommand { this.log(chalk.blue(chalk.bold(`Project initialized for Workspace ${workspace.name}, you can run 'aio app use -w ' to switch workspace.`))) } + async getTemplatesWithAI (flags, orgSupportedServices = null) { + // Step 1: Ask user to describe their needs in natural language + const { userPrompt } = await inquirer.prompt([ + { + type: 'input', + name: 'userPrompt', + message: 'Describe what you want to build (e.g., "I need a CRUD API" or "I want an event-driven app"):', + validate: (input) => { + if (!input || input.trim() === '') { + return 'Please provide a description of what you want to build.' + } + return true + } + } + ]) + + const spinner = ora() + spinner.start(`Analyzing your request: "${userPrompt}"`) + + try { + // Step 2: Call backend API via lib + const template = await getAIRecommendation(userPrompt) + + spinner.succeed('Found matching template!') + + // Step 3: No template was returned + if (!template || !template.name) { + this.log(chalk.yellow('\nNo template matched your description. Falling back to table selection...')) + return this.getTemplatesForFlags(flags, orgSupportedServices) + } + + // Step 4: Display AI recommendation + this.log(chalk.bold('\n Recommendation:')) + this.log(chalk.dim(` Based on "${userPrompt}"\n`)) + this.log(` ${chalk.bold(template.name)}`) + if (template.description) { + this.log(` ${chalk.dim(template.description)}`) + } + this.log('') // Empty line + + // Step 5: Confirm with user + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Do you want to install this recommended template?', + default: false + } + ]) + + if (confirm) { + return [template.name] + } else { + this.log(chalk.yellow('\nFalling back to traditional template selection...')) + return this.getTemplatesForFlags(flags, orgSupportedServices) + } + + } catch (error) { + spinner.fail('Could not get AI recommendation') + aioLogger.error('AI API error:', error) + this.log(chalk.yellow(`\nError: ${error.message}. Falling back to table selection...`)) + return this.getTemplatesForFlags(flags, orgSupportedServices) + } + } + async getTemplatesForFlags (flags, orgSupportedServices = null) { if (flags.template) { return flags.template diff --git a/src/lib/template-recommendation.js b/src/lib/template-recommendation.js new file mode 100644 index 00000000..5a073864 --- /dev/null +++ b/src/lib/template-recommendation.js @@ -0,0 +1,51 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:template-recommendation', { provider: 'debug' }) + +/** + * Calls the template recommendation API to get AI-based template suggestions + * + * @param {string} prompt - User's natural language description of what they want to build + * @param {string} [apiUrl] - Optional API URL (defaults to env var or hardcoded URL) + * @returns {Promise} Template recommendation from the API + * @throws {Error} If API call fails + */ +async function getAIRecommendation (prompt, apiUrl) { + const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || 'https://268550-garageweektest.adobeio-static.net/api/v1/web/recommend-template' + + aioLogger.debug(`Calling template recommendation API: ${url}`) + aioLogger.debug(`Prompt: ${prompt}`) + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ prompt }) + }) + + if (!response.ok) { + const errorText = await response.text() + aioLogger.error(`API returned status ${response.status}: ${errorText}`) + throw new Error(`API returned status ${response.status}`) + } + + const data = await response.json() + aioLogger.debug(`API response: ${JSON.stringify(data)}`) + + return data.body || data +} + +module.exports = { + getAIRecommendation +} + From 4e6f23aa80dc71dcd4b568aa17ae1e442af9d6b3 Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Tue, 9 Dec 2025 15:15:14 -0800 Subject: [PATCH 2/8] removed comments --- src/lib/template-recommendation.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/lib/template-recommendation.js b/src/lib/template-recommendation.js index 5a073864..34d72291 100644 --- a/src/lib/template-recommendation.js +++ b/src/lib/template-recommendation.js @@ -1,14 +1,3 @@ -/* -Copyright 2024 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:template-recommendation', { provider: 'debug' }) /** From 527cae9231653f4066ff69979aaefef9f73ffe21 Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Wed, 10 Dec 2025 12:33:28 -0800 Subject: [PATCH 3/8] chore: update API endpoint to use deployed backend --- src/lib/template-recommendation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/template-recommendation.js b/src/lib/template-recommendation.js index 34d72291..a3c1d474 100644 --- a/src/lib/template-recommendation.js +++ b/src/lib/template-recommendation.js @@ -9,7 +9,7 @@ const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin- * @throws {Error} If API call fails */ async function getAIRecommendation (prompt, apiUrl) { - const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || 'https://268550-garageweektest.adobeio-static.net/api/v1/web/recommend-template' + const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || 'https://development-918-aiappinit-stage.adobeioruntime.net/api/v1/web/recommend-api/recommend-template' aioLogger.debug(`Calling template recommendation API: ${url}`) aioLogger.debug(`Prompt: ${prompt}`) From c0c1a4d906d5a8ec23e99bef3ca3cfe168c8fc6f Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Thu, 11 Dec 2025 13:59:19 -0800 Subject: [PATCH 4/8] Updated logs --- src/commands/app/init.js | 13 +++++++------ src/lib/defaults.js | 4 +++- src/lib/template-recommendation.js | 17 +++++++++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 497505f4..476e2c54 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -240,12 +240,13 @@ class InitCommand extends TemplatesCommand { // Step 3: No template was returned if (!template || !template.name) { - this.log(chalk.yellow('\nNo template matched your description. Falling back to table selection...')) + spinner.info('AI could not find a matching template') + this.log(chalk.cyan('\n💡 No specific match found. Please explore templates from the options below:\n')) return this.getTemplatesForFlags(flags, orgSupportedServices) } // Step 4: Display AI recommendation - this.log(chalk.bold('\n Recommendation:')) + this.log(chalk.bold('\n🤖 AI Recommendation:')) this.log(chalk.dim(` Based on "${userPrompt}"\n`)) this.log(` ${chalk.bold(template.name)}`) if (template.description) { @@ -266,14 +267,14 @@ class InitCommand extends TemplatesCommand { if (confirm) { return [template.name] } else { - this.log(chalk.yellow('\nFalling back to traditional template selection...')) + this.log(chalk.cyan('\n💡 Please explore templates from the options below:\n')) return this.getTemplatesForFlags(flags, orgSupportedServices) } - + } catch (error) { - spinner.fail('Could not get AI recommendation') + spinner.info('AI recommendation unavailable') aioLogger.error('AI API error:', error) - this.log(chalk.yellow(`\nError: ${error.message}. Falling back to table selection...`)) + this.log(chalk.cyan('\n💡 AI could not recommend a template. Please explore templates from the options below:\n')) return this.getTemplatesForFlags(flags, orgSupportedServices) } } diff --git a/src/lib/defaults.js b/src/lib/defaults.js index c24d5440..789c832a 100644 --- a/src/lib/defaults.js +++ b/src/lib/defaults.js @@ -40,5 +40,7 @@ module.exports = { EXTENSIONS_CONFIG_KEY: 'extensions', // Adding tracking file constants LAST_BUILT_ACTIONS_FILENAME: 'last-built-actions.json', - LAST_DEPLOYED_ACTIONS_FILENAME: 'last-deployed-actions.json' + LAST_DEPLOYED_ACTIONS_FILENAME: 'last-deployed-actions.json', + // Template recommendation API + defaultTemplateRecommendationApiUrl: 'https://development-918-aiappinit-stage.adobeioruntime.net/api/v1/web/recommend-api/recommend-template' } diff --git a/src/lib/template-recommendation.js b/src/lib/template-recommendation.js index a3c1d474..1b785271 100644 --- a/src/lib/template-recommendation.js +++ b/src/lib/template-recommendation.js @@ -1,15 +1,28 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:template-recommendation', { provider: 'debug' }) +const { defaultTemplateRecommendationApiUrl } = require('./defaults') /** * Calls the template recommendation API to get AI-based template suggestions * * @param {string} prompt - User's natural language description of what they want to build - * @param {string} [apiUrl] - Optional API URL (defaults to env var or hardcoded URL) + * @param {string} [apiUrl] - Optional API URL (defaults to env var TEMPLATE_RECOMMENDATION_API or default URL from defaults.js) * @returns {Promise} Template recommendation from the API * @throws {Error} If API call fails */ async function getAIRecommendation (prompt, apiUrl) { - const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || 'https://development-918-aiappinit-stage.adobeioruntime.net/api/v1/web/recommend-api/recommend-template' + const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || defaultTemplateRecommendationApiUrl aioLogger.debug(`Calling template recommendation API: ${url}`) aioLogger.debug(`Prompt: ${prompt}`) From 0db83726834cc207e9f639510e683f27b227ea85 Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Fri, 12 Dec 2025 09:48:33 -0800 Subject: [PATCH 5/8] Updated console logs --- src/commands/app/init.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 476e2c54..97309f13 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -236,16 +236,17 @@ class InitCommand extends TemplatesCommand { // Step 2: Call backend API via lib const template = await getAIRecommendation(userPrompt) - spinner.succeed('Found matching template!') + // Step 3: No template was returned if (!template || !template.name) { - spinner.info('AI could not find a matching template') - this.log(chalk.cyan('\n💡 No specific match found. Please explore templates from the options below:\n')) + spinner.stop() + this.log(chalk.cyan('\n💡 AI could not find a matching template. Please explore templates from the options below:\n')) return this.getTemplatesForFlags(flags, orgSupportedServices) } // Step 4: Display AI recommendation + spinner.succeed('Found matching template!') this.log(chalk.bold('\n🤖 AI Recommendation:')) this.log(chalk.dim(` Based on "${userPrompt}"\n`)) this.log(` ${chalk.bold(template.name)}`) @@ -272,9 +273,9 @@ class InitCommand extends TemplatesCommand { } } catch (error) { - spinner.info('AI recommendation unavailable') + spinner.stop() aioLogger.error('AI API error:', error) - this.log(chalk.cyan('\n💡 AI could not recommend a template. Please explore templates from the options below:\n')) + this.log(chalk.cyan('\n💡 AI recommendation unavailable. Please explore templates from the options below:\n')) return this.getTemplatesForFlags(flags, orgSupportedServices) } } From d79fef3635ed4ec6e853b17446e08b5271200b3e Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Tue, 6 Jan 2026 12:11:28 -0800 Subject: [PATCH 6/8] Added Flag chat for AI prompt --- README.md | 13 ++ src/commands/app/init.js | 48 +++----- src/lib/template-recommendation.js | 4 - test/commands/app/init.test.js | 149 +++++++++++++++++++++++ test/lib/template-recommendation.test.js | 136 +++++++++++++++++++++ 5 files changed, 315 insertions(+), 35 deletions(-) create mode 100644 test/lib/template-recommendation.test.js diff --git a/README.md b/README.md index 9a8d0f94..a2e3e699 100644 --- a/README.md +++ b/README.md @@ -541,11 +541,13 @@ USAGE $ aio app init [PATH] [-v] [--version] [--install] [-y] [--login] [-e ... | -t ... | --repo ] [--standalone-app | | ] [--template-options ] [-o | -i | ] [-p | | ] [-w | | ] [--confirm-new-workspace] [--use-jwt] [--github-pat ] [--linter none|basic|adobe-recommended] + [-c] ARGUMENTS [PATH] [default: .] Path to the app directory FLAGS + -c, --chat Use AI chat mode for natural language template recommendations -e, --extension=... Extension point(s) to implement -i, --import= Import an Adobe I/O Developer Console configuration file -o, --org= Specify the Adobe Developer Console Org to init from (orgId, or orgCode) @@ -570,6 +572,17 @@ FLAGS DESCRIPTION Create a new Adobe I/O App + +EXAMPLES + # Initialize with traditional template selection + $ aio app init + + # Initialize with AI-powered chat mode + $ aio app init --chat + $ aio app init -c + + # Initialize from a quickstart repository + $ aio app init --repo adobe/appbuilder-quickstarts/progressive-web-app ``` _See code: [src/commands/app/init.js](https://github.com/adobe/aio-cli-plugin-app/blob/14.3.1/src/commands/app/init.js)_ diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 97309f13..623cdcd5 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -108,22 +108,14 @@ class InitCommand extends TemplatesCommand { await this.withQuickstart(flags.repo, flags['github-pat']) } else { // 2. prompt for templates to be installed - - // Ask user: Do you want to use natural language? - const { useNL } = await inquirer.prompt([{ - type: 'confirm', - name: 'useNL', - message: 'Do you want to describe your needs in natural language? (AI will recommend templates)', - default: false - }]) - let templates - if (useNL) { - templates = await this.getTemplatesWithAI(flags) // NEW AI FUNCTION + if (flags.chat) { + // Use AI-powered natural language recommendations + templates = await this.getTemplatesWithAI(flags) } else { - templates = await this.getTemplatesForFlags(flags) // EXISTING TABLE FLOW + // Use traditional template selection table + templates = await this.getTemplatesForFlags(flags) } - // If no templates selected, init a standalone app if (templates.length <= 0) { flags['standalone-app'] = true @@ -168,21 +160,13 @@ class InitCommand extends TemplatesCommand { let templates if (!flags.repo) { // 5. get list of templates to install - - // Ask user: Do you want to use natural language? - const { useNL } = await inquirer.prompt([{ - type: 'confirm', - name: 'useNL', - message: 'Do you want to describe your needs in natural language? (AI will recommend templates)', - default: false - }]) - - if (useNL) { - templates = await this.getTemplatesWithAI(flags, orgSupportedServices) // NEW AI FUNCTION + if (flags.chat) { + // Use AI-powered natural language recommendations + templates = await this.getTemplatesWithAI(flags, orgSupportedServices) } else { - templates = await this.getTemplatesForFlags(flags, orgSupportedServices) // EXISTING TABLE FLOW + // Use traditional template selection table + templates = await this.getTemplatesForFlags(flags, orgSupportedServices) } - // If no templates selected, init a standalone app if (templates.length <= 0) { flags['standalone-app'] = true @@ -231,13 +215,10 @@ class InitCommand extends TemplatesCommand { const spinner = ora() spinner.start(`Analyzing your request: "${userPrompt}"`) - try { // Step 2: Call backend API via lib const template = await getAIRecommendation(userPrompt) - - - + // Step 3: No template was returned if (!template || !template.name) { spinner.stop() @@ -271,7 +252,6 @@ class InitCommand extends TemplatesCommand { this.log(chalk.cyan('\n💡 Please explore templates from the options below:\n')) return this.getTemplatesForFlags(flags, orgSupportedServices) } - } catch (error) { spinner.stop() aioLogger.error('AI API error:', error) @@ -604,6 +584,12 @@ InitCommand.flags = { description: 'Specify the linter to use for the project', options: ['none', 'basic', 'adobe-recommended'], default: 'basic' + }), + chat: Flags.boolean({ + description: 'Use AI chat mode for natural language template recommendations', + char: 'c', + default: false, + exclusive: ['repo', 'template', 'import'] }) } diff --git a/src/lib/template-recommendation.js b/src/lib/template-recommendation.js index 1b785271..75248511 100644 --- a/src/lib/template-recommendation.js +++ b/src/lib/template-recommendation.js @@ -15,7 +15,6 @@ const { defaultTemplateRecommendationApiUrl } = require('./defaults') /** * Calls the template recommendation API to get AI-based template suggestions - * * @param {string} prompt - User's natural language description of what they want to build * @param {string} [apiUrl] - Optional API URL (defaults to env var TEMPLATE_RECOMMENDATION_API or default URL from defaults.js) * @returns {Promise} Template recommendation from the API @@ -23,7 +22,6 @@ const { defaultTemplateRecommendationApiUrl } = require('./defaults') */ async function getAIRecommendation (prompt, apiUrl) { const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || defaultTemplateRecommendationApiUrl - aioLogger.debug(`Calling template recommendation API: ${url}`) aioLogger.debug(`Prompt: ${prompt}`) @@ -43,11 +41,9 @@ async function getAIRecommendation (prompt, apiUrl) { const data = await response.json() aioLogger.debug(`API response: ${JSON.stringify(data)}`) - return data.body || data } module.exports = { getAIRecommendation } - diff --git a/test/commands/app/init.test.js b/test/commands/app/init.test.js index aa9abd98..c4fdde27 100644 --- a/test/commands/app/init.test.js +++ b/test/commands/app/init.test.js @@ -27,6 +27,9 @@ jest.mock('inquirer', () => ({ prompt: jest.fn(), createPromptModule: jest.fn() })) +jest.mock('../../../src/lib/template-recommendation', () => ({ + getAIRecommendation: jest.fn() +})) // mock ora jest.mock('ora', () => { @@ -240,6 +243,12 @@ describe('Command Prototype', () => { expect(TheCommand.flags['confirm-new-workspace'].type).toBe('boolean') expect(TheCommand.flags['confirm-new-workspace'].default).toBe(true) + + expect(TheCommand.flags.chat).toBeDefined() + expect(TheCommand.flags.chat.type).toBe('boolean') + expect(TheCommand.flags.chat.char).toBe('c') + expect(TheCommand.flags.chat.default).toBe(false) + expect(TheCommand.flags.chat.exclusive).toEqual(['repo', 'template', 'import']) }) test('args', async () => { @@ -510,6 +519,146 @@ describe('--no-login', () => { command.argv = ['--yes', '--no-login', '--linter=invalid'] await expect(command.run()).rejects.toThrow('Expected --linter=invalid to be one of: none, basic, adobe-recommended\nSee more help with --help') }) + + test('--chat --no-login (AI mode)', async () => { + const installOptions = { + useDefaultValues: false, + installNpm: true, + installConfig: false, + templates: ['@adobe/generator-app-excshell'] + } + command.getTemplatesWithAI = jest.fn().mockResolvedValue(['@adobe/generator-app-excshell']) + command.runCodeGenerators = jest.fn() + + command.argv = ['--chat', '--no-login'] + await command.run() + + expect(command.getTemplatesWithAI).toHaveBeenCalledWith(expect.objectContaining({ + chat: true, + login: false, + install: true, + linter: 'basic' + })) + expect(command.installTemplates).toHaveBeenCalledWith(installOptions) + expect(LibConsoleCLI.init).not.toHaveBeenCalled() + }) + + test('--chat cannot be used with --template', async () => { + command.argv = ['--chat', '--template', '@adobe/my-template', '--no-login'] + await expect(command.run()).rejects.toThrow() + }) + + test('--chat cannot be used with --repo', async () => { + command.argv = ['--chat', '--repo', 'adobe/appbuilder-quickstarts/qr-code', '--no-login'] + await expect(command.run()).rejects.toThrow() + }) + + test('--chat with login (covers line 168)', async () => { + const { getAIRecommendation } = require('../../../src/lib/template-recommendation') + getAIRecommendation.mockResolvedValue({ name: '@adobe/generator-app-excshell' }) + inquirer.prompt.mockResolvedValueOnce({ userPrompt: 'I want a web app' }) + .mockResolvedValueOnce({ confirm: true }) + + command.argv = ['--chat'] // with login (default) + await command.run() + + expect(getAIRecommendation).toHaveBeenCalledWith('I want a web app') + expect(command.installTemplates).toHaveBeenCalledWith(expect.objectContaining({ + useDefaultValues: false, + installNpm: true, + templates: ['@adobe/generator-app-excshell'] + })) + expect(LibConsoleCLI.init).toHaveBeenCalled() + }) +}) + +describe('getTemplatesWithAI', () => { + let getAIRecommendation + + beforeEach(() => { + const templateRecommendation = require('../../../src/lib/template-recommendation') + getAIRecommendation = templateRecommendation.getAIRecommendation + jest.clearAllMocks() + }) + + test('should return template when user accepts AI recommendation', async () => { + getAIRecommendation.mockResolvedValue({ + name: '@adobe/generator-app-excshell', + description: 'Experience Cloud SPA' + }) + inquirer.prompt + .mockResolvedValueOnce({ userPrompt: 'I want a web app' }) + .mockResolvedValueOnce({ confirm: true }) + + const result = await command.getTemplatesWithAI({}) + + expect(result).toEqual(['@adobe/generator-app-excshell']) + expect(getAIRecommendation).toHaveBeenCalledWith('I want a web app') + }) + + test('should fallback to getTemplatesForFlags when user declines recommendation', async () => { + getAIRecommendation.mockResolvedValue({ + name: '@adobe/test-template', + description: 'Test template' + }) + inquirer.prompt + .mockResolvedValueOnce({ userPrompt: 'test prompt' }) + .mockResolvedValueOnce({ confirm: false }) + command.getTemplatesForFlags = jest.fn().mockResolvedValue(['@adobe/fallback-template']) + + const result = await command.getTemplatesWithAI({}, null) + + expect(result).toEqual(['@adobe/fallback-template']) + expect(command.getTemplatesForFlags).toHaveBeenCalledWith({}, null) + }) + + test('should fallback to getTemplatesForFlags when AI returns null', async () => { + getAIRecommendation.mockResolvedValue(null) + inquirer.prompt.mockResolvedValueOnce({ userPrompt: 'nonsense prompt' }) + command.getTemplatesForFlags = jest.fn().mockResolvedValue(['@adobe/fallback']) + + const result = await command.getTemplatesWithAI({}, null) + + expect(result).toEqual(['@adobe/fallback']) + expect(command.getTemplatesForFlags).toHaveBeenCalled() + }) + + test('should fallback to getTemplatesForFlags when AI returns template without name', async () => { + getAIRecommendation.mockResolvedValue({ description: 'no name field' }) + inquirer.prompt.mockResolvedValueOnce({ userPrompt: 'test' }) + command.getTemplatesForFlags = jest.fn().mockResolvedValue(['@adobe/fallback']) + + const result = await command.getTemplatesWithAI({}) + + expect(result).toEqual(['@adobe/fallback']) + expect(command.getTemplatesForFlags).toHaveBeenCalled() + }) + + test('should fallback to getTemplatesForFlags on API error', async () => { + getAIRecommendation.mockRejectedValue(new Error('API Error')) + inquirer.prompt.mockResolvedValueOnce({ userPrompt: 'test' }) + command.getTemplatesForFlags = jest.fn().mockResolvedValue(['@adobe/fallback']) + + const result = await command.getTemplatesWithAI({}) + + expect(result).toEqual(['@adobe/fallback']) + expect(command.getTemplatesForFlags).toHaveBeenCalled() + }) + + test('should validate empty prompt and reject empty input', async () => { + let capturedValidator + inquirer.prompt.mockImplementationOnce(async (questions) => { + capturedValidator = questions[0].validate + return { userPrompt: 'valid input' } + }).mockResolvedValueOnce({ confirm: true }) + getAIRecommendation.mockResolvedValue({ name: '@adobe/test' }) + await command.getTemplatesWithAI({}) + + // Test the validator that was captured + expect(capturedValidator('')).toBe('Please provide a description of what you want to build.') + expect(capturedValidator(' ')).toBe('Please provide a description of what you want to build.') + expect(capturedValidator('valid input')).toBe(true) + }) }) describe('--login', () => { diff --git a/test/lib/template-recommendation.test.js b/test/lib/template-recommendation.test.js new file mode 100644 index 00000000..dc39e8e0 --- /dev/null +++ b/test/lib/template-recommendation.test.js @@ -0,0 +1,136 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +// Unmock the module (in case it's mocked by other tests like init.test.js) +jest.unmock('../../src/lib/template-recommendation') + +// Unmock the module in case init.test.js has mocked it +jest.unmock('../../src/lib/template-recommendation') + +// Mock fetch before requiring the module +global.fetch = jest.fn() + +const { getAIRecommendation } = require('../../src/lib/template-recommendation') + +describe('template-recommendation', () => { + beforeEach(() => { + jest.clearAllMocks() + delete process.env.TEMPLATE_RECOMMENDATION_API + }) + + describe('getAIRecommendation', () => { + test('should return template from API response', async () => { + global.fetch.mockResolvedValue({ + ok: true, + json: async () => ({ template: '@adobe/generator-app-excshell', description: 'Test' }) + }) + + const result = await getAIRecommendation('I want a web app') + + expect(result).toEqual({ template: '@adobe/generator-app-excshell', description: 'Test' }) + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('recommend-template'), + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt: 'I want a web app' }) + }) + ) + }) + + test('should return data.body when response has body wrapper', async () => { + global.fetch.mockResolvedValue({ + ok: true, + json: async () => ({ + body: { template: '@adobe/test-template' } + }) + }) + + const result = await getAIRecommendation('test prompt') + + expect(result).toEqual({ template: '@adobe/test-template' }) + }) + + test('should use environment variable URL when set', async () => { + process.env.TEMPLATE_RECOMMENDATION_API = 'https://custom-env.url/api' + global.fetch.mockResolvedValue({ + ok: true, + json: async () => ({ template: 'test' }) + }) + + await getAIRecommendation('test') + + expect(global.fetch).toHaveBeenCalledWith( + 'https://custom-env.url/api', + expect.any(Object) + ) + }) + + test('should use provided apiUrl parameter over env var', async () => { + process.env.TEMPLATE_RECOMMENDATION_API = 'https://env.url/api' + global.fetch.mockResolvedValue({ + ok: true, + json: async () => ({ template: 'test' }) + }) + + await getAIRecommendation('test', 'https://param.url/api') + + expect(global.fetch).toHaveBeenCalledWith( + 'https://param.url/api', + expect.any(Object) + ) + }) + + test('should throw error when API returns non-ok response', async () => { + global.fetch.mockResolvedValue({ + ok: false, + status: 500, + text: async () => 'Internal Server Error' + }) + + await expect(getAIRecommendation('test')).rejects.toThrow('API returned status 500') + }) + + test('should throw error on network failure', async () => { + global.fetch.mockRejectedValue(new Error('Network error')) + + await expect(getAIRecommendation('test')).rejects.toThrow('Network error') + }) + + test('should handle empty response body', async () => { + global.fetch.mockResolvedValue({ + ok: true, + json: async () => ({}) + }) + + const result = await getAIRecommendation('test') + + expect(result).toEqual({}) + }) + + test('should pass prompt in request body', async () => { + global.fetch.mockResolvedValue({ + ok: true, + json: async () => ({ template: 'test' }) + }) + + await getAIRecommendation('my custom prompt') + + expect(global.fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: JSON.stringify({ prompt: 'my custom prompt' }) + }) + ) + }) + }) +}) From d60d345218a21d0e7c1156888d2660d7be6c3082 Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Tue, 6 Jan 2026 12:12:19 -0800 Subject: [PATCH 7/8] Added Flag chat for AI prompt --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index a2e3e699..216cd605 100644 --- a/README.md +++ b/README.md @@ -572,17 +572,6 @@ FLAGS DESCRIPTION Create a new Adobe I/O App - -EXAMPLES - # Initialize with traditional template selection - $ aio app init - - # Initialize with AI-powered chat mode - $ aio app init --chat - $ aio app init -c - - # Initialize from a quickstart repository - $ aio app init --repo adobe/appbuilder-quickstarts/progressive-web-app ``` _See code: [src/commands/app/init.js](https://github.com/adobe/aio-cli-plugin-app/blob/14.3.1/src/commands/app/init.js)_ From 92fe742e6877d8645bfab31605a744fb7f72cbe1 Mon Sep 17 00:00:00 2001 From: Sri Manaswi Chirumamilla Date: Wed, 7 Jan 2026 14:07:28 -0800 Subject: [PATCH 8/8] Add logic to select between env --- src/lib/defaults.js | 12 ++++++++++-- src/lib/template-recommendation.js | 20 +++++++++++++------- test/lib/template-recommendation.test.js | 7 +++---- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/lib/defaults.js b/src/lib/defaults.js index 789c832a..45f3ec87 100644 --- a/src/lib/defaults.js +++ b/src/lib/defaults.js @@ -11,6 +11,14 @@ governing permissions and limitations under the License. // defaults & constants +const { PROD_ENV, STAGE_ENV } = require('@adobe/aio-lib-env') + +// Template recommendation API endpoints (same pattern as aio-lib-state) +const TEMPLATE_RECOMMENDATION_API_ENDPOINTS = { + [PROD_ENV]: 'https://development-918-aiappinit.adobeioruntime.net/api/v1/web/recommend-api/recommend-template', + [STAGE_ENV]: 'https://development-918-aiappinit-stage.adobeioruntime.net/api/v1/web/recommend-api/recommend-template' +} + module.exports = { defaultAppHostname: 'adobeio-static.net', stageAppHostname: 'dev.runtime.adobe.io', @@ -41,6 +49,6 @@ module.exports = { // Adding tracking file constants LAST_BUILT_ACTIONS_FILENAME: 'last-built-actions.json', LAST_DEPLOYED_ACTIONS_FILENAME: 'last-deployed-actions.json', - // Template recommendation API - defaultTemplateRecommendationApiUrl: 'https://development-918-aiappinit-stage.adobeioruntime.net/api/v1/web/recommend-api/recommend-template' + // Template recommendation API endpoints + TEMPLATE_RECOMMENDATION_API_ENDPOINTS } diff --git a/src/lib/template-recommendation.js b/src/lib/template-recommendation.js index 75248511..3ffd848f 100644 --- a/src/lib/template-recommendation.js +++ b/src/lib/template-recommendation.js @@ -11,27 +11,33 @@ governing permissions and limitations under the License. */ const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:template-recommendation', { provider: 'debug' }) -const { defaultTemplateRecommendationApiUrl } = require('./defaults') +const { TEMPLATE_RECOMMENDATION_API_ENDPOINTS } = require('./defaults') +const { getCliEnv } = require('@adobe/aio-lib-env') /** * Calls the template recommendation API to get AI-based template suggestions * @param {string} prompt - User's natural language description of what they want to build - * @param {string} [apiUrl] - Optional API URL (defaults to env var TEMPLATE_RECOMMENDATION_API or default URL from defaults.js) + * @param {string} [apiUrl] - Optional API URL (defaults to env var TEMPLATE_RECOMMENDATION_API or environment-based URL) * @returns {Promise} Template recommendation from the API * @throws {Error} If API call fails */ async function getAIRecommendation (prompt, apiUrl) { - const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || defaultTemplateRecommendationApiUrl - aioLogger.debug(`Calling template recommendation API: ${url}`) + // Select URL based on environment (same pattern as aio-lib-state) + const env = getCliEnv() + const url = apiUrl || process.env.TEMPLATE_RECOMMENDATION_API || TEMPLATE_RECOMMENDATION_API_ENDPOINTS[env] + aioLogger.debug(`Calling template recommendation API: ${url} (env: ${env})`) aioLogger.debug(`Prompt: ${prompt}`) - const response = await fetch(url, { + const payload = { prompt } + const options = { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ prompt }) - }) + body: JSON.stringify(payload) + } + + const response = await fetch(url, options) if (!response.ok) { const errorText = await response.text() diff --git a/test/lib/template-recommendation.test.js b/test/lib/template-recommendation.test.js index dc39e8e0..24ba76bf 100644 --- a/test/lib/template-recommendation.test.js +++ b/test/lib/template-recommendation.test.js @@ -13,9 +13,6 @@ governing permissions and limitations under the License. // Unmock the module (in case it's mocked by other tests like init.test.js) jest.unmock('../../src/lib/template-recommendation') -// Unmock the module in case init.test.js has mocked it -jest.unmock('../../src/lib/template-recommendation') - // Mock fetch before requiring the module global.fetch = jest.fn() @@ -41,7 +38,9 @@ describe('template-recommendation', () => { expect.stringContaining('recommend-template'), expect.objectContaining({ method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify({ prompt: 'I want a web app' }) }) )