feat: add --template flag to agent-native create#145
Conversation
…GitHub Adds `agent-native create <name> --template <name>` support. Non-default templates are downloaded on demand from the GitHub source tarball (no npm package bloat). Resolves workspace:* deps to the latest published version via npm dist-tags. Also fixes useDbSync import in templates to use @agent-native/core/client (where it has always lived) instead of the root export which isn't in the current published build. Adds @tiptap/core as an explicit dep in the mail template so pnpm doesn't hoist the v2 copy from @agent-native/core. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
❌ Deploy Preview for agent-native-fw failed. Why did it fail? →
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
agent-native-mail | 35358f9 | Apr 03 2026, 11:46 PM |
packages/core/src/cli/create.ts
Outdated
| const __dirname = path.dirname(__filename); | ||
|
|
||
| const GITHUB_REPO = "BuilderIO/agent-native"; | ||
| const KNOWN_TEMPLATES = [ |
There was a problem hiding this comment.
would be great for it to discover this if possible so we dont have to hardcode it
There was a problem hiding this comment.
agree. @builderio-bot can you suggest 3 different methods for how we might dynamically pick up an array of known templates? anything to avoid the hardcoding...
There was a problem hiding this comment.
Here are 3 approaches to dynamically discover available templates instead of hardcoding KNOWN_TEMPLATES:
Option 1: GitHub Contents API
Call the GitHub REST API to list the templates/ directory before downloading. It's lightweight (no tarball), already available, and always reflects the current repo state:
async function fetchAvailableTemplates(version: string): Promise<string[]> {
// Try the tagged version first, fall back to main
const refs = [`v${version}`, "main"];
for (const ref of refs) {
try {
const data = await fetchJson(
`https://api.github.com/repos/${GITHUB_REPO}/contents/templates?ref=${ref}`
);
return data
.filter((e: { type: string }) => e.type === "dir")
.map((e: { name: string }) => e.name);
} catch { /* try next */ }
}
return KNOWN_TEMPLATES; // static fallback
}Pros: Zero extra download, version-aware, requires no maintenance. Cons: Adds one API round-trip; rate-limited at 60 req/hr unauthenticated.
Option 2: Bundled templates.json manifest in the npm package
Maintain a small src/templates/templates.json that's published with the package and updated whenever a template is added. Read it at runtime with no network call:
// templates.json: { "templates": ["mail", "calendar", ...] }
const manifest = JSON.parse(
fs.readFileSync(path.join(packageRoot, "src/templates/templates.json"), "utf-8")
);
const templates: string[] = manifest.templates;Also fetchable as a fallback from GitHub raw:
https://raw.githubusercontent.com/${GITHUB_REPO}/main/packages/core/src/templates/templates.json
Pros: Offline-capable, zero latency, no rate limits. Cons: Requires a one-line commit whenever a template is added (same effort as updating KNOWN_TEMPLATES, but at least it's a single source of truth).
Option 3: Inspect the tarball on download
Since the tarball is already downloaded when creating a non-default template, drop the pre-validation step and instead list templates/ directories from the extracted archive. Templates are discovered at use time:
// After extraction, list available templates from the tarball itself
const [repoDir] = fs.readdirSync(extractDir);
const templatesRoot = path.join(extractDir, repoDir, "templates");
const available = fs.readdirSync(templatesRoot, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name);
if (!available.includes(template)) {
console.error(`Unknown template "${template}". Available: ${available.join(", ")}`);
process.exit(1);
}Pros: Perfect accuracy, zero extra network calls, no manifest to maintain. Cons: Validation happens after the tarball downloads (~seconds), so a typo isn't caught until then. Could combine with Option 1 for early validation.
Recommendation: Option 1 (GitHub Contents API) with a static fallback is probably the best fit here — it's dynamic, requires zero maintenance, and the tarball download is already the slow step anyway, so one tiny API call is negligible.
There was a problem hiding this comment.
@builderio-bot Okay, go ahead and implement option one.
There was a problem hiding this comment.
Done. Here's a summary of what changed in packages/core/src/cli/create.ts:
- Removed the hardcoded
KNOWN_TEMPLATESarray - Added
fetchAvailableTemplates(version)— callshttps://api.github.com/repos/BuilderIO/agent-native/contents/templates?ref=<ref>, tries the versioned tag first thenmain, and returns directory names as the live template list - Added
fetchJson(url)helper (reuses the existinghttpsimport) with theUser-AgentandAccept: application/vnd.github+jsonheaders the API expects - Kept
FALLBACK_TEMPLATESas a static safety net for offline/rate-limited scenarios — same values as before, just no longer the primary source - Passed the resolved list into
downloadAndExtractTemplateso the "not found in repo" error message also reflects the live list
The fetchAvailableTemplates call only fires when a non-default template is requested, so the default scaffolding path is unaffected and adds zero latency.
|
tested the new non-hardcode path locally and it works |
There was a problem hiding this comment.
Builder has reviewed your changes and found 3 potential issues.
Review Details
PR #145 — --template flag for agent-native create
This PR adds on-demand template scaffolding via a --template flag, downloading GitHub tarballs, extracting the requested template subdirectory, and rewriting workspace:* deps to real semver ranges. It also consolidates useDbSync/ClientOnly/DefaultSpinner imports into @agent-native/core/client across multiple templates. Risk: Standard.
The overall approach is sound — using the GitHub tarball endpoint is a reasonable strategy for keeping the npm package lean, and the fallback chain (versioned tag → main) is sensible. The rewriteWorkspaceDeps npm registry query is a clean solution. However, several confirmed bugs need to be addressed before merge.
Key Findings
🔴 Argument parsing bug — template value consumed as app name
args.find((a) => !a.startsWith("--")) cannot distinguish the positional <name> from the value following --template. With agent-native create --template mail my-app, nameArg becomes "mail" instead of "my-app". Additionally, --template with no trailing value silently falls back to the bundled default rather than reporting an error.
🟡 process.exit(1) inside try block skips finally cleanup
Three error branches inside downloadAndExtractTemplate call process.exit(1) directly. Node.js does not run finally blocks after process.exit(), so failed downloads leave temp directories behind in os.tmpdir() on every error path.
🟡 Write stream not closed on error paths in downloadFile
fs.createWriteStream(dest) is created up front, but the redirect-with-no-Location, non-200, and network-error rejection paths never call file.close(). On Windows this prevents the finally cleanup (even if fixed) from deleting the temp directory.
🟡 Unbounded redirect recursion in downloadFile
The inner get() function follows 301/302 redirects by calling itself with no depth counter. A redirect loop will exhaust sockets/memory and hang the CLI indefinitely.
Code review by Builder.io
|
|
||
| case "create": { | ||
| import("./create.js").then((m) => m.createApp(args[0])); | ||
| const nameArg = args.find((a) => !a.startsWith("--")); |
There was a problem hiding this comment.
🔴 nameArg parsing consumes template value when --template precedes positional name
args.find((a) => !a.startsWith("--")) returns the first non-flag token — with agent-native create --template mail my-app this yields "mail" as the app name, not "my-app". Also, when --template is the last arg, args[templateIdx + 1] is undefined, which silently falls back to "default" instead of reporting an error. Fix: skip the value following recognized flag names when searching for the positional, and guard for a missing --template value explicitly.
React with 👍 or 👎 to help me improve.
| if (!downloaded) { | ||
| console.error( | ||
| "Failed to download template tarball from GitHub. Check your internet connection.", | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Extract the full tarball | ||
| execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`, { stdio: "pipe" }); | ||
|
|
||
| // The tarball root is BuilderIO-agent-native-<sha>/ — find it | ||
| const [repoDir] = fs.readdirSync(extractDir); | ||
| if (!repoDir) { | ||
| console.error("Tarball appears empty."); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const templateSrc = path.join(extractDir, repoDir, "templates", template); | ||
| if (!fs.existsSync(templateSrc)) { | ||
| console.error( | ||
| `Template "${template}" was not found in the repository. Available templates: ${availableTemplates.join(", ")}`, | ||
| ); | ||
| process.exit(1); |
There was a problem hiding this comment.
🟡 process.exit(1) inside try block bypasses finally temp-dir cleanup
Node.js does not run finally blocks after process.exit(). Three error paths inside downloadAndExtractTemplate (!downloaded, empty tarball, template not found) call process.exit(1) directly inside the try, so fs.rmSync(tmpDir, …) in the finally is never reached. Fix: throw an Error on each of these paths instead and call process.exit(1) from the caller after the finally has run.
React with 👍 or 👎 to help me improve.
| function downloadFile(url: string, dest: string): Promise<void> { | ||
| return new Promise((resolve, reject) => { | ||
| const file = fs.createWriteStream(dest); | ||
|
|
||
| function get(u: string): void { | ||
| https | ||
| .get(u, (res) => { | ||
| if (res.statusCode === 301 || res.statusCode === 302) { | ||
| const location = res.headers.location; | ||
| if (!location) { | ||
| reject(new Error("Redirect with no Location header")); | ||
| return; | ||
| } | ||
| get(location); | ||
| return; | ||
| } | ||
| if (res.statusCode !== 200) { | ||
| reject(new Error(`HTTP ${res.statusCode} for ${u}`)); | ||
| return; | ||
| } | ||
| res.pipe(file); | ||
| file.on("finish", () => file.close(() => resolve())); | ||
| }) | ||
| .on("error", reject); | ||
| } | ||
|
|
||
| get(url); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🟡 Write stream never closed on error paths in downloadFile — resource/cleanup leak
The file WriteStream is created once but only closed in the happy-path finish handler. The three rejection paths (missing Location header, non-200 status, network error) call reject() without calling file.close(). On Windows, the open handle prevents fs.rmSync from deleting the temp dir even in the finally block. Fix: introduce a fail(err) helper that calls file.close(() => reject(err)), and call res.resume() when discarding redirect/error responses.
React with 👍 or 👎 to help me improve.
There was a problem hiding this comment.
Browser testing: 6/6 passed
Test Results: 6/6 passed ✅
✅ TC-01: App loads without blank screen or critical errors (succeeded)
URLs tested: http://localhost:8089/
Evidence: 2 screenshots captured
✅ TC-02: No JavaScript console errors related to imports (useDbSync, ClientOnly, DefaultSpinner) (succeeded)
Console errors: None related to imports. Console shows: Vite connected, React DevTools suggestion, minor a11y form field warnings, and 404 for favicon (pre-existing issues)."
URLs tested: http://localhost:8089/
Evidence: 2 screenshots captured
✅ TC-03: Agent chat panel (if present) renders without errors (succeeded)
URLs tested: http://localhost:8089/
Evidence: 1 screenshot captured
✅ TC-04: Page reload works cleanly and returns same state (succeeded)
URLs tested: http://localhost:8089/
Evidence: 1 screenshot captured
✅ TC-05: Navigation between visible sections works without errors (succeeded)
URLs tested: http://localhost:8089/
Evidence: 1 screenshot captured
✅ TC-06: useDbSync hook initializes without crashing (succeeded)
Console errors: None related to useDbSync. Console shows successful Vite connection and normal dev mode warnings only."
URLs tested: http://localhost:8089/
Evidence: 2 screenshots captured
Details
PR #145 regression test PASSED. All 6 test cases succeeded. The import consolidation changes in template root.tsx files (combining multiple imports from @agent-native/core/client into single import statements) had zero impact on app functionality. All templates load cleanly without JavaScript errors related to the consolidated imports.
Summary
Adds `agent-native create --template ` to scaffold from any of the repo's full-featured templates without bloating the npm package.
Usage
```bash
npx @agent-native/core create my-app --template mail
```
Test plan
🤖 Generated with Claude Code