From c2a6fded0d6e095d0d66ded7c6f7349e55e4bb7b Mon Sep 17 00:00:00 2001 From: Rabi Shanker Guha Date: Mon, 6 Apr 2026 18:36:17 +0530 Subject: [PATCH 1/2] Add a flag to bootstrap a new project from an example instead of blank template --- .gitignore | 1 + packages/openui-cli/package.json | 11 +- .../openui-cli/scripts/gen-examples-list.mjs | 28 +++++ .../src/commands/create-chat-app.ts | 111 ++++++++++++++---- packages/openui-cli/src/index.ts | 37 ++++-- packages/openui-cli/src/lib/fetch-example.ts | 78 ++++++++++++ pnpm-lock.yaml | 54 +++++---- 7 files changed, 260 insertions(+), 60 deletions(-) create mode 100644 packages/openui-cli/scripts/gen-examples-list.mjs create mode 100644 packages/openui-cli/src/lib/fetch-example.ts diff --git a/.gitignore b/.gitignore index f1718dadf..99c9c66bf 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ dist .svelte-kit *.tsbuildinfo typings +packages/openui-cli/src/generated/ # Logs *storybook.log diff --git a/packages/openui-cli/package.json b/packages/openui-cli/package.json index b1b4ba98e..0b7017de0 100644 --- a/packages/openui-cli/package.json +++ b/packages/openui-cli/package.json @@ -1,6 +1,6 @@ { "name": "@openuidev/cli", - "version": "0.0.6", + "version": "0.0.7", "description": "CLI for OpenUI — scaffold generative UI chat apps and generate LLM system prompts from component libraries", "bin": { "openui": "dist/index.js" @@ -12,7 +12,8 @@ "scripts": { "build:cli": "tsc -p .", "build:templates": "rm -rf dist/templates/openui-chat && mkdir -p dist/templates && cp -R src/templates/openui-chat dist/templates/openui-chat", - "build": "pnpm run build:cli && pnpm run build:templates", + "build:examples-list": "node scripts/gen-examples-list.mjs", + "build": "pnpm run build:examples-list && pnpm run build:cli && pnpm run build:templates", "build:exec": "node dist/index.js", "lint:check": "eslint ./src --ignore-pattern 'src/templates/**'", "lint:fix": "eslint ./src --fix --ignore-pattern 'src/templates/**'", @@ -22,7 +23,8 @@ "ci": "pnpm run lint:check && pnpm run format:check" }, "devDependencies": { - "@types/node": "^22.15.32" + "@types/node": "^22.15.32", + "@types/tar": "^7.0.87" }, "keywords": [ "openui", @@ -52,6 +54,7 @@ "@inquirer/core": "^11.1.5", "@inquirer/prompts": "^8.3.0", "commander": "^14.0.3", - "esbuild": "^0.25.10" + "esbuild": "^0.25.10", + "tar": "^7.5.13" } } diff --git a/packages/openui-cli/scripts/gen-examples-list.mjs b/packages/openui-cli/scripts/gen-examples-list.mjs new file mode 100644 index 000000000..a3e44867c --- /dev/null +++ b/packages/openui-cli/scripts/gen-examples-list.mjs @@ -0,0 +1,28 @@ +import { readdirSync, mkdirSync, writeFileSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const examplesDir = resolve(__dirname, "../../../examples"); +const outDir = resolve(__dirname, "../src/generated"); +const outFile = resolve(outDir, "known-examples.ts"); + +const examples = readdirSync(examplesDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort(); + +mkdirSync(outDir, { recursive: true }); + +writeFileSync( + outFile, + [ + "// Auto-generated by scripts/gen-examples-list.mjs — do not edit manually", + `export const KNOWN_EXAMPLES = ${JSON.stringify(examples, null, 2)} as const;`, + `export type ExampleName = (typeof KNOWN_EXAMPLES)[number];`, + "", + ].join("\n"), +); + +console.info(`Generated known-examples.ts with ${examples.length} examples: ${examples.join(", ")}`); diff --git a/packages/openui-cli/src/commands/create-chat-app.ts b/packages/openui-cli/src/commands/create-chat-app.ts index 556b340e6..736af78d7 100644 --- a/packages/openui-cli/src/commands/create-chat-app.ts +++ b/packages/openui-cli/src/commands/create-chat-app.ts @@ -2,12 +2,15 @@ import { execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; +import { KNOWN_EXAMPLES } from "../generated/known-examples"; import { detectPackageManager } from "../lib/detect-package-manager"; +import { fetchExample } from "../lib/fetch-example"; import { runSkillInstall, shouldInstallSkill } from "../lib/install-skill"; import { resolveArgs } from "../lib/resolve-args"; export interface CreateChatAppOptions { name?: string; + example?: string; skill?: boolean; noInteractive?: boolean; } @@ -21,6 +24,30 @@ function shouldCopyTemplatePath(templateDir: string, src: string): boolean { } export async function runCreateChatApp(options: CreateChatAppOptions): Promise { + let useExample: boolean; + + if (options.example) { + useExample = true; + } else if (!options.noInteractive) { + try { + const { select } = await import("@inquirer/prompts"); + useExample = (await select({ + message: "Start from:", + choices: [ + { value: false, name: "Clean template (default)" }, + { value: true, name: "Example from the repo" }, + ], + default: false, + })) as boolean; + } catch (err) { + const { ExitPromptError } = await import("@inquirer/core"); + if (err instanceof ExitPromptError) process.exit(0); + throw err; + } + } else { + useExample = false; + } + const args = await resolveArgs( { name: options.name @@ -29,11 +56,37 @@ export async function runCreateChatApp(options: CreateChatAppOptions): Promise ({ value: e })), + }, + required: true as const, + }, + } + : {}), }, !options.noInteractive, ); - const { name } = args as { name: string }; + const { name } = args as { name: string; example?: string }; + const resolvedExample = useExample ? (args as { example: string }).example : undefined; + + if ( + resolvedExample && + !KNOWN_EXAMPLES.includes(resolvedExample as (typeof KNOWN_EXAMPLES)[number]) + ) { + console.error( + `Error: Unknown example "${resolvedExample}".\n\nAvailable examples:\n${KNOWN_EXAMPLES.map((e) => ` ${e}`).join("\n")}`, + ); + process.exit(1); + } + const targetDir = path.resolve(process.cwd(), name); if (fs.existsSync(targetDir)) { @@ -43,39 +96,45 @@ export async function runCreateChatApp(options: CreateChatAppOptions): Promise shouldCopyTemplatePath(templateDir, src), - }); + fs.cpSync(templateDir, targetDir, { + recursive: true, + filter: (src) => shouldCopyTemplatePath(templateDir, src), + }); + } const pkgPath = path.join(targetDir, "package.json"); - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { - name: string; - dependencies?: Record; - devDependencies?: Record; - }; - pkg.name = name; - for (const section of ["dependencies", "devDependencies"] as const) { - for (const key of Object.keys(pkg[section] ?? {})) { - if (pkg[section]![key] === "workspace:*") { - pkg[section]![key] = "latest"; + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { + name: string; + dependencies?: Record; + devDependencies?: Record; + }; + pkg.name = name; + for (const section of ["dependencies", "devDependencies"] as const) { + for (const key of Object.keys(pkg[section] ?? {})) { + if (pkg[section]![key] === "workspace:*") { + pkg[section]![key] = "latest"; + } } } + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); } - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); const installCmd = runner === "pnpm dlx" diff --git a/packages/openui-cli/src/index.ts b/packages/openui-cli/src/index.ts index d0e642203..80071e248 100644 --- a/packages/openui-cli/src/index.ts +++ b/packages/openui-cli/src/index.ts @@ -14,16 +14,39 @@ program .command("create") .description("Scaffold a new Next.js app with OpenUI Chat") .option("-n, --name ", "Project name") + .option( + "-e, --example ", + "Scaffold from a repo example instead of the default template (e.g. heroui-chat). Use --list-examples to see all options.", + ) + .option("--list-examples", "List all available examples and exit") .option("--skill", "Install the OpenUI agent skill for AI coding assistants") .option("--no-skill", "Skip installing the OpenUI agent skill") .option("--no-interactive", "Fail with error if required args are missing") - .action(async (options: { name?: string; skill?: boolean; interactive: boolean }) => { - await runCreateChatApp({ - name: options.name, - skill: options.skill, - noInteractive: !options.interactive, - }); - }); + .action( + async (options: { + name?: string; + example?: string; + listExamples?: boolean; + skill?: boolean; + interactive: boolean; + }) => { + if (options.listExamples) { + const { KNOWN_EXAMPLES } = await import("./generated/known-examples.js"); + console.info("Available examples:\n"); + for (const ex of KNOWN_EXAMPLES) { + console.info(` ${ex}`); + } + process.exit(0); + } + + await runCreateChatApp({ + name: options.name, + example: options.example, + skill: options.skill, + noInteractive: !options.interactive, + }); + }, + ); program .command("generate") diff --git a/packages/openui-cli/src/lib/fetch-example.ts b/packages/openui-cli/src/lib/fetch-example.ts new file mode 100644 index 000000000..aa91bd869 --- /dev/null +++ b/packages/openui-cli/src/lib/fetch-example.ts @@ -0,0 +1,78 @@ +import * as fs from "fs"; +import * as https from "https"; + +import * as tar from "tar"; +import * as zlib from "zlib"; + +const REPO_OWNER = "thesysdev"; +const REPO_NAME = "openui"; +const BRANCH = "main"; + +function getTarballUrl(): string { + return `https://codeload.github.com/${REPO_OWNER}/${REPO_NAME}/tar.gz/refs/heads/${BRANCH}`; +} + +function getExamplePrefix(exampleName: string): string { + return `${REPO_NAME}-${BRANCH}/examples/${exampleName}/`; +} + +function fetchWithRedirects(url: string): Promise { + return new Promise((resolve, reject) => { + const request = (currentUrl: string) => { + https + .get(currentUrl, { headers: { "User-Agent": "openui-cli" } }, (res) => { + if ( + res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + request(res.headers.location); + } else { + resolve(res); + } + }) + .on("error", reject); + }; + request(url); + }); +} + +export async function fetchExample(exampleName: string, targetDir: string): Promise { + const tarballUrl = getTarballUrl(); + const prefix = getExamplePrefix(exampleName); + const stripComponents = prefix.split("/").length - 1; + + console.info(`\nDownloading example "${exampleName}" from GitHub...\n`); + + const res = await fetchWithRedirects(tarballUrl); + + if (res.statusCode && res.statusCode >= 400) { + throw new Error(`Failed to download tarball: HTTP ${res.statusCode}`); + } + + fs.mkdirSync(targetDir, { recursive: true }); + + await new Promise((resolve, reject) => { + res + .pipe(zlib.createGunzip()) + .pipe( + tar.extract({ + cwd: targetDir, + strip: stripComponents, + filter: (entryPath) => entryPath.startsWith(prefix), + }), + ) + .on("finish", resolve) + .on("error", reject); + }); + + const entries = fs.readdirSync(targetDir); + if (entries.length === 0) { + fs.rmSync(targetDir, { recursive: true, force: true }); + throw new Error( + `Example "${exampleName}" was not found in the repository.\n` + + `Run \`openui create --list-examples\` to see available examples.`, + ); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b92f491d1..de4dfcd95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,7 +198,7 @@ importers: version: 0.0.45 '@ag-ui/mastra': specifier: ^1.0.1 - version: 1.0.1(d5bfccb2283c9e6cfb0a090aaed772b1) + version: 1.0.1(lffsbgredezch2lrpoqa3jpknu) '@mastra/core': specifier: 1.15.0 version: 1.15.0(@cfworker/json-schema@4.1.1)(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(openapi-types@12.1.3)(zod@3.25.76) @@ -915,10 +915,16 @@ importers: esbuild: specifier: ^0.25.10 version: 0.25.12 + tar: + specifier: ^7.5.13 + version: 7.5.13 devDependencies: '@types/node': specifier: ^22.15.32 version: 22.15.32 + '@types/tar': + specifier: ^7.0.87 + version: 7.0.87 packages/react-email: dependencies: @@ -7462,6 +7468,10 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/tar@7.0.87': + resolution: {integrity: sha512-3IxNBV8LeY5oi2ZFpvAhOtW1+mHswkzM7BuisVrwJgPv67GBO2rkLPQlEKtzfHuLdhDDczhkCZeT+RuizMay4A==} + deprecated: This is a stub types definition. tar provides its own type definitions, so you do not need this installed. + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -11892,10 +11902,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -13990,8 +13996,8 @@ packages: tar-stream@3.1.8: resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - tar@7.5.11: - resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} + tar@7.5.13: + resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} teex@1.0.1: @@ -15305,7 +15311,7 @@ snapshots: - react - react-dom - '@ag-ui/mastra@1.0.1(d5bfccb2283c9e6cfb0a090aaed772b1)': + '@ag-ui/mastra@1.0.1(lffsbgredezch2lrpoqa3jpknu)': dependencies: '@ag-ui/client': 0.0.49 '@ag-ui/core': 0.0.45 @@ -16911,7 +16917,7 @@ snapshots: source-map-support: 0.5.21 stacktrace-parser: 0.1.11 structured-headers: 0.4.1 - tar: 7.5.11 + tar: 7.5.13 terminal-link: 2.1.1 undici: 6.24.1 wrap-ansi: 7.0.0 @@ -17563,7 +17569,7 @@ snapshots: '@isaacs/fs-minipass@4.0.1': dependencies: - minipass: 7.1.2 + minipass: 7.1.3 '@isaacs/ttlcache@1.4.1': {} @@ -17722,7 +17728,7 @@ snapshots: node-fetch: 2.7.0 nopt: 8.1.0 semver: 7.7.4 - tar: 7.5.11 + tar: 7.5.13 transitivePeerDependencies: - encoding - supports-color @@ -22971,6 +22977,10 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/tar@7.0.87': + dependencies: + tar: 7.5.13 + '@types/trusted-types@2.0.7': {} '@types/unist@2.0.11': {} @@ -25414,7 +25424,7 @@ snapshots: '@next/eslint-plugin-next': 16.1.6 eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) @@ -25441,7 +25451,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -25456,14 +25466,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -25478,7 +25488,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -26338,7 +26348,7 @@ snapshots: foreground-child: 3.3.1 jackspeak: 3.4.3 minimatch: 9.0.5 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -28563,13 +28573,11 @@ snapshots: minimist@1.2.8: {} - minipass@7.1.2: {} - minipass@7.1.3: {} minizlib@3.1.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 mkdirp@1.0.4: {} @@ -29407,7 +29415,7 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 7.1.3 path-scurry@2.0.2: dependencies: @@ -31582,11 +31590,11 @@ snapshots: - bare-buffer - react-native-b4a - tar@7.5.11: + tar@7.5.13: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 - minipass: 7.1.2 + minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 From 3f9e210606697870a1ce72956e9a9abb2842c558 Mon Sep 17 00:00:00 2001 From: Rabi Shanker Guha Date: Mon, 6 Apr 2026 18:42:24 +0530 Subject: [PATCH 2/2] Update docs --- docs/content/docs/api-reference/cli.mdx | 15 ++++++++++++--- packages/openui-cli/README.md | 9 +++++++-- packages/openui-cli/src/lib/fetch-example.ts | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/content/docs/api-reference/cli.mdx b/docs/content/docs/api-reference/cli.mdx index 3d1d250a1..462012ecc 100644 --- a/docs/content/docs/api-reference/cli.mdx +++ b/docs/content/docs/api-reference/cli.mdx @@ -31,15 +31,17 @@ openui create [options] | Flag | Description | |---|---| | `-n, --name ` | Project name (directory to create) | +| `-e, --example ` | Scaffold from a repo example instead of the default template (e.g. `heroui-chat`) | +| `--list-examples` | Print all available examples and exit | | `--skill` | Install the OpenUI agent skill for AI coding assistants | | `--no-skill` | Skip installing the OpenUI agent skill | | `--no-interactive` | Fail instead of prompting for missing input | -When run interactively (default), the CLI prompts for any missing options. Pass `--no-interactive` in CI or scripted environments to surface missing required flags as errors instead. +When run interactively (default), the CLI first asks whether to start from the clean template or an example from the repo. Pass `--no-interactive` in CI or scripted environments to surface missing required flags as errors instead. **What it does** -1. Copies the bundled `openui-chat` Next.js template into `/` +1. Copies the bundled `openui-chat` Next.js template into `/`, or downloads the chosen example from GitHub 2. Rewrites `workspace:*` dependency versions to `latest` 3. Auto-detects your package manager (npm, pnpm, yarn, bun) 4. Installs dependencies @@ -56,13 +58,20 @@ Pass `--skill` or `--no-skill` to skip the prompt. In `--no-interactive` mode th **Examples** ```bash -# Interactive — prompts for project name and skill installation +# Interactive — prompts for template vs example, then project name and skill installation openui create # Non-interactive openui create --name my-app openui create --no-interactive --name my-app +# Start from a repo example +openui create --example heroui-chat +openui create --example heroui-chat --name my-app + +# List all available examples +openui create --list-examples + # Explicitly install or skip the agent skill openui create --name my-app --skill openui create --name my-app --no-skill diff --git a/packages/openui-cli/README.md b/packages/openui-cli/README.md index 8bfeea375..5ea73adf9 100644 --- a/packages/openui-cli/README.md +++ b/packages/openui-cli/README.md @@ -53,12 +53,15 @@ openui create [options] Options: - `-n, --name `: Project name +- `-e, --example `: Scaffold from a repo example instead of the default template (e.g. `heroui-chat`) +- `--list-examples`: Print all available examples and exit - `--no-interactive`: Fail instead of prompting for missing required input What it does: +- prompts whether to start from the clean template or a repo example (interactive mode only) - prompts for the project name if you do not pass `--name` -- copies the bundled `openui-chat` template into a new directory +- copies the bundled `openui-chat` template, or downloads the chosen example from GitHub - rewrites `workspace:*` dependencies in the generated `package.json` to `latest` - installs dependencies automatically using the detected package manager @@ -66,7 +69,9 @@ Examples: ```bash openui create -openui create +openui create --example heroui-chat +openui create --example heroui-chat --name my-app +openui create --list-examples openui create --no-interactive ``` diff --git a/packages/openui-cli/src/lib/fetch-example.ts b/packages/openui-cli/src/lib/fetch-example.ts index aa91bd869..84b2b61e1 100644 --- a/packages/openui-cli/src/lib/fetch-example.ts +++ b/packages/openui-cli/src/lib/fetch-example.ts @@ -72,7 +72,7 @@ export async function fetchExample(exampleName: string, targetDir: string): Prom fs.rmSync(targetDir, { recursive: true, force: true }); throw new Error( `Example "${exampleName}" was not found in the repository.\n` + - `Run \`openui create --list-examples\` to see available examples.`, + `Run \`npx @openuidev/cli create --list-examples\` to see available examples.`, ); } }