Skip to content

feat: add --template flag to agent-native create#145

Open
zuchka wants to merge 3 commits intomainfrom
feat/template-flag
Open

feat: add --template flag to agent-native create#145
zuchka wants to merge 3 commits intomainfrom
feat/template-flag

Conversation

@zuchka
Copy link
Copy Markdown
Contributor

@zuchka zuchka commented Apr 3, 2026

Summary

Adds `agent-native create --template ` to scaffold from any of the repo's full-featured templates without bloating the npm package.

  • `--template mail` (or `calendar`, `forms`, `slides`, `analytics`, `content`, `issues`, `recruiting`, `starter`, `videos`) downloads the GitHub source tarball for the matching version tag, falls back to `main`, extracts the template subdirectory, and runs the same post-processing as the default template (placeholder replacement, gitignore rename, agent symlinks)
  • `workspace:*` deps in the scaffolded `package.json` are rewritten to real semver ranges by querying npm dist-tags — picks the highest base version across all tags so dev/next releases are preferred over an older stable
  • No new npm dependencies — uses Node built-ins (`https`, `os`) and system `tar`
  • Also fixes `useDbSync`, `ClientOnly`, `DefaultSpinner` imports across templates to use `@agent-native/core/client` (where they've always lived) rather than the root export, which isn't present 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` over the v3 the template needs

Usage

```bash
npx @agent-native/core create my-app --template mail
```

Test plan

  • `npx @agent-native/core@dev create my-app` — default template unchanged
  • `npx @agent-native/core@dev create my-app --template mail` — downloads and scaffolds mail template
  • `cd my-app && pnpm install` — no `workspace:*` or missing version errors
  • `pnpm dev` — no `useDbSync` / `DefaultSpinner` export errors, app loads
  • `npx @agent-native/core@dev create my-app --template invalid` — prints available templates list and exits cleanly

🤖 Generated with Claude Code

…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>
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 3, 2026

Deploy Preview for agent-native-fw failed. Why did it fail? →

Name Link
🔨 Latest commit 35358f9
🔍 Latest deploy log https://app.netlify.com/projects/agent-native-fw/deploys/69d0514000317600080234b1

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 3, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
agent-native-mail 35358f9 Apr 03 2026, 11:46 PM

const __dirname = path.dirname(__filename);

const GITHUB_REPO = "BuilderIO/agent-native";
const KNOWN_TEMPLATES = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would be great for it to discover this if possible so we dont have to hardcode it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.


Edit in Builder  Preview

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@builderio-bot Okay, go ahead and implement option one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done. Here's a summary of what changed in packages/core/src/cli/create.ts:

  • Removed the hardcoded KNOWN_TEMPLATES array
  • Added fetchAvailableTemplates(version) — calls https://api.github.com/repos/BuilderIO/agent-native/contents/templates?ref=<ref>, tries the versioned tag first then main, and returns directory names as the live template list
  • Added fetchJson(url) helper (reuses the existing https import) with the User-Agent and Accept: application/vnd.github+json headers the API expects
  • Kept FALLBACK_TEMPLATES as a static safety net for offline/rate-limited scenarios — same values as before, just no longer the primary source
  • Passed the resolved list into downloadAndExtractTemplate so 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.


Edit in Builder  Preview

@zuchka zuchka marked this pull request as ready for review April 4, 2026 00:31
@zuchka
Copy link
Copy Markdown
Contributor Author

zuchka commented Apr 4, 2026

tested the new non-hardcode path locally and it works

Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

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

Comment on lines 168 to +170

case "create": {
import("./create.js").then((m) => m.createApp(args[0]));
const nameArg = args.find((a) => !a.startsWith("--"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 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.

Comment on lines +208 to +230
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Comment on lines +272 to +300
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);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Visual Verification

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants