Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add create-gensx package #66

Merged
merged 8 commits into from
Jan 7, 2025
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/lint-pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
permissions:
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
main:
name: Validate PR title
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ permissions:
checks: write
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/test-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ permissions:
checks: write
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ permissions:
checks: write
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -34,9 +38,11 @@ jobs:

- name: Run tests 🧪
run: pnpm test
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

- name: Report GenSX Coverage 📊
if: github.event_name == 'pull_request' && matrix.node-version == '20.x'
if: github.event_name == 'pull_request' && matrix.node-version == '20.x' && contains(github.event.pull_request.changed_files, 'packages/gensx')
uses: davelosert/vitest-coverage-report-action@v2
with:
working-directory: packages/gensx
Expand Down
3 changes: 3 additions & 0 deletions packages/create-gensx/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
node_modules
.turbo
7 changes: 7 additions & 0 deletions packages/create-gensx/.eslintrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
extends: ["../../.eslintrc.json"],
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: import.meta.dirname,
},
};
1 change: 1 addition & 0 deletions packages/create-gensx/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.tsbuildinfo
5 changes: 5 additions & 0 deletions packages/create-gensx/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 0.0.0 (Initial Release)

- Initial release of @gensx/openai
50 changes: 50 additions & 0 deletions packages/create-gensx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# create-gensx

Create a new [GenSX](https://gensx.dev) project with one command.

## Usage

```bash
# Using npm
npm create gensx@latest my-app

# Using npx
npx create-gensx@latest my-app

# Using yarn
yarn create gensx my-app

# Using pnpm
pnpm create gensx my-app
```

### Options

```bash
# Use a specific template (default: ts)
npm create gensx@latest my-app --template ts

# Force creation in non-empty directory
npm create gensx@latest my-app --force
```

## Templates

### TypeScript (ts)

A TypeScript-based project configured with:

- TypeScript configuration for GenSX
- Basic project structure
- Development server with hot reload
- Build setup

## Development

```bash
# Build the package
pnpm run build

# Run tests
pnpm test
```
28 changes: 28 additions & 0 deletions packages/create-gensx/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "create-gensx",
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=18.0.0"
},
"bin": {
"create-gensx": "./dist/cli.js"
},
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"dev": "tsc --watch",
"test": "vitest"
},
"dependencies": {
"commander": "^11.1.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@vitest/coverage-istanbul": "^2.1.8",
"typescript": "^5.7.2",
"vite": "^6.0.7",
"vite-plugin-dts": "^3.0.0",
"vitest": "^2.1.8"
}
}
34 changes: 34 additions & 0 deletions packages/create-gensx/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { fileURLToPath } from "url";

import { Command } from "commander";

import { createGensxProject, CreateOptions } from "./index.js";

export async function runCLI() {
const program = new Command();

program
.name("create-gensx")
.description("Create a new GenSX project")
.argument("<project-directory>", "Directory to create the project in")
.option("-t, --template <name>", "Template to use", "ts")
.option("-f, --force", "Overwrite existing files", false)
.action(async (projectPath: string, options: CreateOptions) => {
try {
await createGensxProject(projectPath, options);
} catch (error) {
console.error("Error:", (error as Error).message);
process.exit(1);
}
});

await program.parseAsync();
}

// Only run CLI when this file is being executed directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
runCLI().catch((error) => {
console.error("Error:", error);
process.exit(1);
});
}
143 changes: 143 additions & 0 deletions packages/create-gensx/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { exec as execCallback } from "child_process";
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";

const exec = promisify(execCallback);

const TEMPLATE_MAP: Record<string, string> = {
ts: "typescript",
};

interface Template {
name: string;
description: string;
dependencies: string[];
devDependencies: string[];
runCommand: string;
}

async function loadTemplate(templateName: string): Promise<Template> {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const templatePath = path.join(
__dirname,
"templates",
TEMPLATE_MAP[templateName] || templateName,
);
const templateConfigPath = path.join(templatePath, "template.json");

try {
const configContent = await readFile(templateConfigPath, "utf-8");
const template = JSON.parse(configContent) as Template;
return template;
} catch (_error) {
throw new Error(`Template "${templateName}" not found or invalid.`);
}
}

async function listTemplates(): Promise<string[]> {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const templatesPath = path.join(__dirname, "templates");
try {
const templates = await readdir(templatesPath);
// Map template directories back to their flag values
return Object.entries(TEMPLATE_MAP)
.filter(([_, dir]) => templates.includes(dir))
.map(([flag]) => flag);
} catch {
return [];
}
}

async function copyTemplateFiles(templateName: string, targetPath: string) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const templatePath = path.join(
__dirname,
"templates",
TEMPLATE_MAP[templateName] || templateName,
);

async function copyDir(currentPath: string, targetBase: string) {
const entries = await readdir(currentPath, { withFileTypes: true });

for (const entry of entries) {
const sourcePath = path.join(currentPath, entry.name);
const targetFilePath = path
.join(targetBase, path.relative(templatePath, sourcePath))
.replace(/\.template$/, "");

if (entry.name === "template.json") continue;

if (entry.isDirectory()) {
await mkdir(targetFilePath, { recursive: true });
await copyDir(sourcePath, targetBase);
} else {
const content = await readFile(sourcePath, "utf-8");
await mkdir(path.dirname(targetFilePath), { recursive: true });
await writeFile(targetFilePath, content);
}
}
}

await copyDir(templatePath, targetPath);
}

export interface CreateOptions {
template: string;
force: boolean;
}

export async function createGensxProject(
projectPath: string,
options: CreateOptions,
) {
const { template: templateName, force } = options;

// Validate template exists
const templates = await listTemplates();
if (!templates.includes(templateName)) {
throw new Error(
`Template "${templateName}" not found. Available templates: ${templates.join(", ")}`,
);
}

const template = await loadTemplate(templateName);
const absoluteProjectPath = path.resolve(process.cwd(), projectPath);

// Create project directory
await mkdir(absoluteProjectPath, { recursive: true });

// check if the directory is empty
const files = await readdir(absoluteProjectPath);
if (files.length > 0 && !force) {
throw new Error(
`Directory "${absoluteProjectPath}" is not empty. Use --force to overwrite existing files.`,
);
}

// Copy template files
await copyTemplateFiles(templateName, absoluteProjectPath);

// Initialize npm project and install dependencies
process.chdir(absoluteProjectPath);
await exec("npm init -y");

if (template.dependencies.length > 0) {
await exec(`npm install ${template.dependencies.join(" ")}`);
}

if (template.devDependencies.length > 0) {
await exec(`npm install -D ${template.devDependencies.join(" ")}`);
}

console.log(`
Successfully created GenSX project in ${absoluteProjectPath}

To get started:
${projectPath !== "." ? `cd ${projectPath}` : ""}
${template.runCommand}

Edit src/index.tsx to start building your GenSX application.
`);
}
19 changes: 19 additions & 0 deletions packages/create-gensx/src/templates/typescript/README.md.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# GenSX Project

Starter project for [GenSX](https://gensx.dev), created using `npx create-gensx`.

## Getting Started

1. Install dependencies:

```bash
npm install
```

2. Start the development server:

```bash
npm run dev
```

3. Edit `src/index.tsx` to start building your GenSX application.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"watch": ["src"],
"ext": ".ts,.tsx",
"exec": "tsx ./src/index.tsx"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "my-gensx-app",
"private": true,
"version": "0.0.1",
"type": "module",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"dev": "nodemon",
"start": "tsx ./src/index.tsx",
"build": "tsc"
}
}
Loading
Loading