Skip to content

Add program.json + Support for Agent-Friendly CLI Input via stdio @@W-18855311@@#2662

Merged
bendvc merged 13 commits intodevelopfrom
bendvc/W-18855311_create-app-stdio
Jun 27, 2025
Merged

Add program.json + Support for Agent-Friendly CLI Input via stdio @@W-18855311@@#2662
bendvc merged 13 commits intodevelopfrom
bendvc/W-18855311_create-app-stdio

Conversation

@bendvc
Copy link
Contributor

@bendvc bendvc commented Jun 26, 2025

Description

This PR introduces several improvements to the create-mobify-app script that make it easier to automate and integrate with AI agents or other tooling. Key updates include:

  • Introduced program.json: A new JSON-based manifest that defines the behavior and structure of the CLI tool. This serves as a machine-readable contract for tools like the MCP server.
  • Added support for stdio input: Enables passing answers via stdin, allowing agents to gather and pipe input directly into the CLI—great for non-interactive environments and automation.
  • Refactored internal CLI logic to read from program.json, making the tool’s structure more declarative and extensible.
  • Updated data structures and templates to align with this new configuration-driven model.
  • Added --displayProgram argument: A new script action to display the schemas and data that this script uses, we ingest this from out MCP server.

These changes pave the way for more flexible, agent-driven CLI workflows while keeping the experience familiar for human users.


Types of Changes

  • New feature (non-breaking change that adds functionality)
  • Bug fix
  • Documentation update
  • Breaking change
  • Other changes

How to Test-Drive This PR

  1. Pull the branch and run the CLI tool manually to ensure the prompts still behave as expected.

  2. Pipe a JSON payload of answers into the CLI via stdin to verify agent-style automation works:

    echo '{"project.extend":true,"project.name":"demo-storefront","project.commerce.instanceUrl":"https://zzte-053.dx.commercecloud.salesforce.com","project.commerce.clientId":"1d763261-6522-4913-9d52-5d947d3b94c4","project.commerce.isSlasPrivate":false,"project.commerce.siteId":"RefArch","project.commerce.organizationId":"f_ecom_zzte_053","project.commerce.shortCode":"kv7kzm78","general.presetOrTemplateId":"retail-react-app"}' | node ./scripts/create-mobify-app --stdio
  3. Check that updates to the generated app files (e.g., templates) reflect the expected changes.

  4. Review program.json and confirm it correctly describes available questions and options.


Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes

Accessibility Compliance

  • There are no changes to UI

- Create `program.json` to define project operation (this is consumed by the MCP server)
- Allow stdio input for answers. This allows integration with agents where they can gather answers and pipe them to the cli for project generation.
- Updates to data structure and asset templates.
- Use program.json in the script to configure the internal commander app
@bendvc bendvc requested a review from a team as a code owner June 26, 2025 17:29
@bendvc bendvc added the skip changelog Skip the "Changelog Check" GitHub Actions step even if the Changelog.md files are not updated label Jun 26, 2025
@cc-prodsec
Copy link
Collaborator

cc-prodsec commented Jun 26, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

license/snyk check is complete. No issues have been found. (View Details)

bendvc added 3 commits June 26, 2025 11:00
Tried to use the package name as the template id, but this doesn't work with a lot of our current logic. So I reverted it.
Comment on lines +475 to +476
// NOTE: This is a little weird, but we'll set this value to the template id and treat is as such from this point forward..
answers.general.presetOrTemplateId = selectedPreset.templateId
Copy link
Contributor

@vmarta vmarta Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few comments in this file where you said this may be weird.. how about merging both presets and templates into a single list?

AI shows what that could look like:

const ALL_TEMPLATES = [
    // Convert presets to templates with default answers
    ...RAW_PRESETS.map((preset) => {
        const baseTemplate = RAW_TEMPLATES.find((t) => t.id === preset.templateId)
        if (!baseTemplate) {
            throw new Error(
                `Preset "${preset.id}" references unknown template "${preset.templateId}"`
            )
        }

        return {
            ...baseTemplate, // Template structure (questions, assets, etc.)
            id: preset.id, // Use preset ID as the unified ID
            name: preset.name, // Use preset name
            description: preset.description,
            shortDescription: preset.shortDescription,
            isPreset: true, // Flag to identify presets
            templateId: preset.templateId, // Keep reference to base template
            defaultAnswers: preset.answers, // Pre-filled answers
            private: preset.private
        }
    }),
    // Regular templates (no default answers)
    ...RAW_TEMPLATES.map((template) => ({
        ...template,
        isPreset: false,
        templateId: template.id, // Template references itself
        defaultAnswers: {} // No pre-filled answers
    }))
] 

Here's the full file: https://gist.github.com/vmarta/2273ea2dd094f1fab1df4f6eafe9ac82

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see there they are going with this, but I'm not sure it is in fact any less weird. I say that because this notion of "defaultAnswers". Initially if you had no idea about presets you would say that defaultAnswers would be used to show the user what the answer would be if they skipped the input or maybe use it as an example, but it would still ask you the question.

But I'm assuming what we would have to do here is say, if the template is actually a preset, then when we ask the questions, use the defaultAnswers as provided answers so they won't get asked in the first place.

And at first glance having a prop on a template called isPreset is still kinda weird, that data structure probably isn't a template but we use a variable called all_templates which serves to make us believe we are working with all templates.

How strongly do you feel about making this logic perfect before merging?

Comment on lines +454 to +460
try {
const input = await readStdin()
answers = merge(answers, expandObject(JSON.parse(input)))
} catch (err) {
console.error('Failed to read from stdin:', err.message)
process.exit(1)
}
Copy link
Contributor

@adamraya adamraya Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we split the code into separate steps to let users know which step fails by providing more specific error messages?

Also, should we check if the input includes all required fields?

Something like:

try {
        const input = await readStdin()
        if (!input.trim()) {
            throw new Error('No input received. Please pipe valid JSON to stdin.')
        }
        
        const parsedInput = JSON.parse(input)
        
        if (!parsedInput['general.presetOrTemplateId']) {
            throw new Error('Missing required field: "general.presetOrTemplateId"')
        }
        answers = merge(answers, expandObject(parsedInput))
    } catch (err) {
        if (err instanceof SyntaxError) {
            console.error('Invalid JSON format in stdin input')
        } else {
            console.error('Failed to process stdin input:', err.message)
        }
        process.exit(1)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a good called out. I can organize the code a little bit more to better show errors. But I'm not too sure about the answer validation in it's entirety... Maybe we can go with what you have here and validate that there is a presetORTemplateId.. the other answers are template specific and maybe we can create a follow up ticket to implement the stdio input better.

Comment on lines +507 to +508
? (input) => new RegExp(validator.regex, 'i').test(input) || validator.message
: undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we catch errors in case the validator.regex is invalid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future I think there will be a schema validation step that could be used to do something like this. Right now this is all a design time and if there is an error it's because the pwa-kit developer added something wrong to the program json and it's not something that would effect external developers that are using the script.

}

// Extract the source
console.log('Extracting base template from package or npm: ', tarPath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we intentionally leaving this console.log? Should we print this behind a verbose or debug flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Thanks for spotting that. I have removed it.

vmarta
vmarta previously approved these changes Jun 27, 2025
Copy link
Contributor

@vmarta vmarta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A minor comment.. other than that, PR looks good to me 👍

* @param {Object} answers - The current answers object to merge into.
* @returns {Promise<Object>} - The merged answers object.
*/
const getAnswersFromStdin = async (answers) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: I feel the function name doesn't imply that there's going to be a merging of the initial answers and the ones from stdin. So when I see line 518 answers = await getAnswersFromStdin(answers), it made me think why we need to pass in answers.

How about doing the answers merging outside of this function? The error handling is focusing on json and stdin anyways, so it looks to me like the merging can be done outside.

} catch (err) {
console.error('Failed to import program.json:', err.message)
}
process.exit(1)
Copy link
Contributor

@adamraya adamraya Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using process.exit(1) for a successful operation? Should we use process.exit(0) instead? Or move it inside the catch block?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 is success from what the ide is telling me.

const OUTPUT_DIR_FLAG_ACTIVE = !!outputDir
const presetId = preset || process.env.GENERATOR_PRESET

// Exit if the preset provided is not valid.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is this comment copied and pasted? Should it instead say something like: "Display program schema and exit if requested"?

adamraya
adamraya previously approved these changes Jun 27, 2025
@bendvc bendvc dismissed stale reviews from adamraya and vmarta via 9df1e94 June 27, 2025 21:22
adamraya
adamraya previously approved these changes Jun 27, 2025
vmarta
vmarta previously approved these changes Jun 27, 2025
@bendvc bendvc dismissed stale reviews from vmarta and adamraya via b1c0f39 June 27, 2025 21:37
@bendvc bendvc merged commit 9e0adf8 into develop Jun 27, 2025
66 of 67 checks passed
@bendvc bendvc deleted the bendvc/W-18855311_create-app-stdio branch June 27, 2025 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip changelog Skip the "Changelog Check" GitHub Actions step even if the Changelog.md files are not updated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants