Add program.json + Support for Agent-Friendly CLI Input via stdio @@W-18855311@@#2662
Add program.json + Support for Agent-Friendly CLI Input via stdio @@W-18855311@@#2662
program.json + Support for Agent-Friendly CLI Input via stdio @@W-18855311@@#2662Conversation
- 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
🎉 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) |
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.
| // 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
Related to change in the schema for the template
| 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) | ||
| } |
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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.
| ? (input) => new RegExp(validator.regex, 'i').test(input) || validator.message | ||
| : undefined |
There was a problem hiding this comment.
Should we catch errors in case the validator.regex is invalid?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Are we intentionally leaving this console.log? Should we print this behind a verbose or debug flag?
There was a problem hiding this comment.
No. Thanks for spotting that. I have removed it.
vmarta
left a comment
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
nit: Is this comment copied and pasted? Should it instead say something like: "Display program schema and exit if requested"?
Description
This PR introduces several improvements to the
create-mobify-appscript that make it easier to automate and integrate with AI agents or other tooling. Key updates include: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.stdioinput: Enables passing answers via stdin, allowing agents to gather and pipe input directly into the CLI—great for non-interactive environments and automation.program.json, making the tool’s structure more declarative and extensible.These changes pave the way for more flexible, agent-driven CLI workflows while keeping the experience familiar for human users.
Types of Changes
How to Test-Drive This PR
Pull the branch and run the CLI tool manually to ensure the prompts still behave as expected.
Pipe a JSON payload of answers into the CLI via stdin to verify agent-style automation works:
Check that updates to the generated app files (e.g., templates) reflect the expected changes.
Review
program.jsonand confirm it correctly describes available questions and options.Checklists
General
CHANGELOG.mdupdated with a short description of changesAccessibility Compliance