Skip to content
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
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@ This monorepo contains:

- `npx atxp` - Show help and available commands
- `npx atxp demo` - Run the interactive demo application
- `npx atxp create` - Create a new ATXP project
- `npx atxp create <app-name>` - Create a new ATXP project
- `npx atxp help` - Display help information

### Demo Options
- `--verbose, -v` - Show detailed logs
- `--refresh` - Force refresh demo from GitHub
- `--port, -p` - Specify port number (default: 8016)
- `--port, -p` - Specify port number (default: 8017)
- `--dir, -d` - Specify demo directory (default: ~/.cache/atxp/demo)

### Create Options
- `--framework, -f` - Specify framework template (default: express)
- `--git` - Force git initialization
- `--no-git` - Skip git initialization

## Examples

### Get Started
Expand Down Expand Up @@ -61,10 +66,19 @@ npx atxp demo --refresh

### Create a New Project
```bash
# Create a new project
# Create a new project (auto-detects git)
npx atxp create my-app

# Alternative method
# Create with specific framework
npx atxp create my-app --framework express

# Skip git initialization
npx atxp create my-app --no-git

# Force git initialization
npx atxp create my-app --git

# Alternative method using npm create
npm create atxp my-app

# Set up the project
Expand Down
15 changes: 2 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 21 additions & 4 deletions packages/atxp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npx atxp
# Run the interactive demo
npx atxp demo

# Create a new project
# Create a new project (requires app name)
npx atxp create my-app
```

Expand All @@ -36,7 +36,7 @@ Runs the interactive ATXP demo application.
**Options:**
- `--verbose, -v` - Show detailed logs
- `--refresh` - Force refresh demo from GitHub
- `--port, -p` - Specify port number (default: 8016)
- `--port, -p` - Specify port number (default: 8017)
- `--dir, -d` - Specify demo directory (default: ~/.cache/atxp/demo)

**Examples:**
Expand All @@ -49,13 +49,30 @@ npx atxp demo --port 3000 --dir ./my-demo --verbose
npx atxp demo --refresh
```

### `npx atxp create [project-name]`
### `npx atxp create <app-name> [options]`
Creates a new ATXP project with the specified name.

**Options:**
- `--framework, -f` - Specify framework template (default: express)
- `--git` - Force git initialization
- `--no-git` - Skip git initialization

**Examples:**
```bash
# Basic usage (auto-detects git)
npx atxp create my-app
npx atxp create my-agent-project

# With specific framework
npx atxp create my-app --framework express

# Skip git initialization
npx atxp create my-app --no-git

# Force git initialization
npx atxp create my-app --git

# Alternative using npm create
npm create atxp my-app
```

### `npx atxp help`
Expand Down
45 changes: 39 additions & 6 deletions packages/atxp/src/create-project.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it, expect } from 'vitest';
import type { Framework } from './create-project.js';

describe('createProject', () => {
it('should validate project names correctly', () => {
describe('project name validation', () => {
const validateProjectName = (input: string) => {
if (!input.trim()) return 'Project name is required';
if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
Expand All @@ -10,10 +11,42 @@ describe('createProject', () => {
return true;
};

expect(validateProjectName('my-project')).toBe(true);
expect(validateProjectName('project_123')).toBe(true);
expect(validateProjectName('')).toBe('Project name is required');
expect(validateProjectName('invalid name')).toBe('Project name can only contain letters, numbers, hyphens, and underscores');
expect(validateProjectName('invalid@name')).toBe('Project name can only contain letters, numbers, hyphens, and underscores');
it('should accept valid project names', () => {
expect(validateProjectName('my-project')).toBe(true);
expect(validateProjectName('project_123')).toBe(true);
expect(validateProjectName('MyProject')).toBe(true);
expect(validateProjectName('project123')).toBe(true);
expect(validateProjectName('my-awesome_project-2024')).toBe(true);
});

it('should reject invalid project names', () => {
expect(validateProjectName('')).toBe('Project name is required');
expect(validateProjectName(' ')).toBe('Project name is required');
expect(validateProjectName('invalid name')).toBe('Project name can only contain letters, numbers, hyphens, and underscores');
expect(validateProjectName('invalid@name')).toBe('Project name can only contain letters, numbers, hyphens, and underscores');
expect(validateProjectName('invalid.name')).toBe('Project name can only contain letters, numbers, hyphens, and underscores');
expect(validateProjectName('invalid/name')).toBe('Project name can only contain letters, numbers, hyphens, and underscores');
});
});

describe('Framework type', () => {
it('should have correct framework types', () => {
const validFramework: Framework = 'express';
expect(validFramework).toBe('express');

// Test that the type system prevents invalid frameworks
// This is compile-time validation, but we can test the concept
const frameworks = ['express'] as const;
expect(frameworks).toContain('express');
});
});

describe('git options', () => {
it('should recognize valid git options', () => {
const validGitOptions = ['git', 'no-git'] as const;

expect(validGitOptions).toContain('git');
expect(validGitOptions).toContain('no-git');
});
});
});
106 changes: 52 additions & 54 deletions packages/atxp/src/create-project.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import inquirer from 'inquirer';
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import { spawn } from 'child_process';

interface ProjectAnswers {
projectName: string;
template: 'agent';
initGit: boolean;
export type Framework = 'express';

// Utility function to check if git is available
async function isGitAvailable(): Promise<boolean> {
try {
const { execSync } = await import('child_process');
execSync('git --version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}

interface PackageJson {
Expand All @@ -16,54 +22,50 @@ interface PackageJson {
}

// Template repositories
const TEMPLATES = {
agent: {
url: 'https://github.com/atxp-dev/atxp-express-example.git',
humanText: 'Agent Demo (Full-stack web agent)'
const TEMPLATES: Record<Framework, { url: string; humanText: string }> = {
express: {
url: 'https://github.com/atxp-dev/atxp-express-starter.git',
humanText: 'Express (Express.js starter template)'
}
// Future frameworks can be added here
// vercel: {
// url: 'https://github.com/atxp-dev/atxp-vercel-starter.git',
// humanText: 'Vercel (Vercel.js starter template)'
// }
};

export async function createProject(): Promise<void> {
export async function createProject(appName: string, framework: Framework, gitOption?: 'git' | 'no-git'): Promise<void> {
try {
// Get project details from user
const answers = await inquirer.prompt<ProjectAnswers>([
{
type: 'input',
name: 'projectName',
message: 'What is your project named?',
default: 'my-atxp-app',
validate: (input: string) => {
if (!input.trim()) return 'Project name is required';
if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
return 'Project name can only contain letters, numbers, hyphens, and underscores';
}
return true;
}
},
{
type: 'list',
name: 'template',
message: 'Choose a template:',
choices: Object.entries(TEMPLATES).map(([key, template]) => ({
name: template.humanText,
value: key
})),
default: 'agent'
},
{
type: 'confirm',
name: 'initGit',
message: 'Initialize git repository?',
default: true
}
]);
// Validate app name
if (!appName.trim()) {
console.error(chalk.red('Project name is required'));
process.exit(1);
}
if (!/^[a-zA-Z0-9-_]+$/.test(appName)) {
console.error(chalk.red('Project name can only contain letters, numbers, hyphens, and underscores'));
process.exit(1);
}

const { projectName, template, initGit } = answers;
const projectPath = path.resolve(process.cwd(), projectName);
// Determine git initialization preference
let initGit: boolean;
if (gitOption === 'git') {
initGit = true;
} else if (gitOption === 'no-git') {
initGit = false;
} else {
// Smart default: use git if available
initGit = await isGitAvailable();
if (initGit) {
console.log(chalk.blue('Git detected - will initialize git repository (use --no-git to skip)'));
} else {
console.log(chalk.yellow('Git not found - skipping git initialization (install git or use --git to force)'));
}
}
const projectPath = path.resolve(process.cwd(), appName);

// Check if directory already exists
if (await fs.pathExists(projectPath)) {
console.error(chalk.red(`Directory "${projectName}" already exists`));
console.error(chalk.red(`Directory "${appName}" already exists`));
process.exit(1);
}

Expand All @@ -73,7 +75,7 @@ export async function createProject(): Promise<void> {
await fs.ensureDir(projectPath);

// Clone template from GitHub
await cloneTemplate(template, projectPath);
await cloneTemplate(framework, projectPath);

// Copy .env file from env.example if it exists
const envExamplePath = path.join(projectPath, 'env.example');
Expand All @@ -89,7 +91,7 @@ export async function createProject(): Promise<void> {
const packageJsonPath = path.join(projectPath, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath) as PackageJson;
packageJson.name = projectName;
packageJson.name = appName;
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}

Expand All @@ -112,7 +114,7 @@ export async function createProject(): Promise<void> {

console.log(chalk.green('\nProject created successfully!'));
console.log(chalk.blue('\nNext steps:'));
console.log(chalk.white(` cd ${projectName}`));
console.log(chalk.white(` cd ${appName}`));
console.log(chalk.white(' npm install'));
console.log(chalk.white(' npm start'));
console.log(chalk.yellow('\nRemember to configure your environment variables in the .env file!'));
Expand All @@ -123,12 +125,8 @@ export async function createProject(): Promise<void> {
}
}

async function cloneTemplate(template: string, projectPath: string): Promise<void> {
const templateConfig = TEMPLATES[template as keyof typeof TEMPLATES];

if (!templateConfig) {
throw new Error(`Template "${template}" not found`);
}
async function cloneTemplate(framework: Framework, projectPath: string): Promise<void> {
const templateConfig = TEMPLATES[framework];

return new Promise((resolve, reject) => {
console.log(chalk.blue('Downloading template from GitHub...'));
Expand Down
19 changes: 15 additions & 4 deletions packages/atxp/src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ export function showHelp(): void {

console.log(chalk.bold('Usage:'));
console.log(' npx atxp <command> [options]');
console.log(' npx atxp create <app-name> [options]');
console.log(' npm create atxp <app-name> [options]');
console.log();

console.log(chalk.bold('Commands:'));
console.log(' ' + chalk.cyan('demo') + ' ' + 'Run the ATXP demo application');
console.log(' ' + chalk.cyan('create') + ' ' + 'Create a new ATXP project');
console.log(' ' + chalk.cyan('help') + ' ' + 'Show this help message');
console.log(' ' + chalk.cyan('demo') + ' ' + 'Run the ATXP demo application');
console.log(' ' + chalk.cyan('create') + ' ' + chalk.yellow('<app-name>') + ' ' + 'Create a new ATXP project');
console.log(' ' + chalk.cyan('help') + ' ' + 'Show this help message');
console.log();

console.log(chalk.bold('Demo Options:'));
Expand All @@ -23,6 +25,11 @@ export function showHelp(): void {
console.log(' ' + chalk.yellow('--dir, -d') + ' ' + 'Specify demo directory (default: ~/.cache/atxp/demo)');
console.log();

console.log(chalk.bold('Create Options:'));
console.log(' ' + chalk.yellow('--framework, -f') + ' ' + 'Specify framework template (default: express)');
console.log(' ' + chalk.yellow('--git') + ' ' + 'Force git initialization');
console.log(' ' + chalk.yellow('--no-git') + ' ' + 'Skip git initialization');

console.log(chalk.bold('Examples:'));
console.log(' npx atxp demo # Run the demo with defaults (frontend: 8016, backend: 8017)');
console.log(' npx atxp demo --verbose # Run demo with detailed logs');
Expand All @@ -31,7 +38,11 @@ export function showHelp(): void {
console.log(' npx atxp demo --dir ./my-demo # Use custom demo directory');
console.log(' npx atxp demo --frontend-port 4000 --backend-port 4001 # Custom ports');
console.log(' npx atxp demo --dir ./my-demo --frontend-port 4000 # Custom directory and port');
console.log(' npx atxp create # Create a new project');
console.log(' npx atxp create my-app # Create new project (auto-detect git)');
console.log(' npx atxp create my-app --framework express # Create with Express framework');
console.log(' npx atxp create my-app --no-git # Create without git initialization');
console.log(' npx atxp create my-app --git # Force git initialization');
console.log(' npm create atxp my-app # Create project using npm create');
console.log();

console.log(chalk.bold('Learn more:'));
Expand Down
Loading