Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dist
.svelte-kit
*.tsbuildinfo
typings
packages/openui-cli/src/generated/

# Logs
*storybook.log
Expand Down
15 changes: 12 additions & 3 deletions docs/content/docs/api-reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ openui create [options]
| Flag | Description |
|---|---|
| `-n, --name <string>` | Project name (directory to create) |
| `-e, --example <name>` | 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 `<name>/`
1. Copies the bundled `openui-chat` Next.js template into `<name>/`, 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
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions packages/openui-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,25 @@ openui create [options]
Options:

- `-n, --name <string>`: Project name
- `-e, --example <name>`: 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

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

Expand Down
11 changes: 7 additions & 4 deletions packages/openui-cli/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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/**'",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
}
}
28 changes: 28 additions & 0 deletions packages/openui-cli/scripts/gen-examples-list.mjs
Original file line number Diff line number Diff line change
@@ -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(", ")}`);
111 changes: 85 additions & 26 deletions packages/openui-cli/src/commands/create-chat-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -21,6 +24,30 @@ function shouldCopyTemplatePath(templateDir: string, src: string): boolean {
}

export async function runCreateChatApp(options: CreateChatAppOptions): Promise<void> {
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
Expand All @@ -29,11 +56,37 @@ export async function runCreateChatApp(options: CreateChatAppOptions): Promise<v
prompt: { type: "input", message: "Project name?" },
required: true,
},
...(useExample
? {
example: options.example
? { value: options.example }
: {
prompt: {
type: "select",
message: "Which example?",
choices: KNOWN_EXAMPLES.map((e) => ({ 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)) {
Expand All @@ -43,39 +96,45 @@ export async function runCreateChatApp(options: CreateChatAppOptions): Promise<v

const runner = detectPackageManager();

const templateDir = path.join(__dirname, "..", "templates", "openui-chat");
if (!fs.existsSync(templateDir)) {
console.error("Error: Template not found. Please rebuild the CLI with `pnpm build`.");
process.exit(1);
}
if (resolvedExample) {
await fetchExample(resolvedExample, targetDir);
} else {
const templateDir = path.join(__dirname, "..", "templates", "openui-chat");
if (!fs.existsSync(templateDir)) {
console.error("Error: Template not found. Please rebuild the CLI with `pnpm build`.");
process.exit(1);
}

console.info(`\nScaffolding OpenUI Chat app into "${name}"...\n`);
console.info(`\nScaffolding OpenUI Chat app into "${name}"...\n`);

const nestedTemplateDir = path.join(templateDir, "openui-chat");
if (fs.existsSync(nestedTemplateDir)) {
console.warn("Warning: Ignoring nested template directory left by a previous CLI build.");
}
const nestedTemplateDir = path.join(templateDir, "openui-chat");
if (fs.existsSync(nestedTemplateDir)) {
console.warn("Warning: Ignoring nested template directory left by a previous CLI build.");
}

fs.cpSync(templateDir, targetDir, {
recursive: true,
filter: (src) => 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<string, string>;
devDependencies?: Record<string, string>;
};
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<string, string>;
devDependencies?: Record<string, string>;
};
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";
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.

can we update this to use latest version number fetched from npm?

}
}
}
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
}
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");

const installCmd =
runner === "pnpm dlx"
Expand Down
37 changes: 30 additions & 7 deletions packages/openui-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,39 @@ program
.command("create")
.description("Scaffold a new Next.js app with OpenUI Chat")
.option("-n, --name <string>", "Project name")
.option(
"-e, --example <name>",
"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")
Expand Down
Loading
Loading