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
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This monorepo contains:
- `--dir, -d` - Specify demo directory (default: ~/.cache/atxp/demo)

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

Expand Down Expand Up @@ -66,12 +66,15 @@ npx atxp demo --refresh

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

# Create with specific framework
# Create with Express framework (default)
npx atxp create my-app --framework express

# Create with Cloudflare Workers framework
npx atxp create my-app --framework cloudflare

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

Expand All @@ -84,7 +87,9 @@ npm create atxp my-app
# Set up the project
cd my-app
npm install
npm start
npm start # For Express projects
# or
npm start # For Cloudflare projects (runs vite dev)
```

## Development
Expand Down
12 changes: 8 additions & 4 deletions packages/atxp/src/create-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ describe('createProject', () => {

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

const expressFramework: Framework = 'express';
expect(expressFramework).toBe('express');

const cloudflareFramework: Framework = 'cloudflare';
expect(cloudflareFramework).toBe('cloudflare');

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

Expand Down
77 changes: 61 additions & 16 deletions packages/atxp/src/create-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import chalk from 'chalk';
import { spawn } from 'child_process';
import inquirer from 'inquirer';

export type Framework = 'express';
export type Framework = 'express' | 'cloudflare';

// Utility function to check if git is available
async function isGitAvailable(): Promise<boolean> {
Expand Down Expand Up @@ -141,6 +141,10 @@ 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)'
},
cloudflare: {
url: 'https://github.com/atxp-dev/atxp-cloudflare-chat-template',
humanText: 'Cloudflare (Cloudflare Chat agent with ATXP integration)'
}
// Future frameworks can be added here
// vercel: {
Expand Down Expand Up @@ -177,6 +181,34 @@ async function findEnvExample(projectPath: string): Promise<string | null> {
return null;
}

// Find .dev.vars.example file in project directory or one level of subdirectories
async function findDevVarsExample(projectPath: string): Promise<string | null> {
// Check root directory first
const rootDevVars = path.join(projectPath, '.dev.vars.example');
if (await fs.pathExists(rootDevVars)) {
return rootDevVars;
}

// Check subdirectories (max depth 1)
try {
const entries = await fs.readdir(projectPath, { withFileTypes: true });

for (const entry of entries) {
if (entry.isDirectory()) {
const subDirDevVars = path.join(projectPath, entry.name, '.dev.vars.example');
if (await fs.pathExists(subDirDevVars)) {
return subDirDevVars;
}
}
}
} catch {
// If we can't read the directory, just return null
console.warn(chalk.yellow('Could not search for .dev.vars.example files'));
}

return null;
}

export async function createProject(appName: string, framework: Framework, gitOption?: 'git' | 'no-git'): Promise<void> {
try {
// Validate app name
Expand Down Expand Up @@ -220,22 +252,27 @@ export async function createProject(appName: string, framework: Framework, gitOp
// Clone template from GitHub
await cloneTemplate(framework, projectPath);

// Copy .env file from env.example if it exists and configure it interactively
// Search for env.example in project root and one level of subdirectories
// Copy configuration file from template if it exists and configure it interactively
// Search for .dev.vars.example or env.example in project root and one level of subdirectories
// Prioritize .dev.vars.example (used by Cloudflare) over env.example
const devVarsExamplePath = await findDevVarsExample(projectPath);
const envExamplePath = await findEnvExample(projectPath);
let createdEnvPath: string | null = null;
let createdConfigPath: string | null = null;

if (envExamplePath) {
const envDir = path.dirname(envExamplePath);
const envPath = path.join(envDir, '.env');
const configSource = devVarsExamplePath || envExamplePath;
if (configSource) {
const configDir = path.dirname(configSource);
const isDevVars = configSource === devVarsExamplePath;
const configPath = path.join(configDir, isDevVars ? '.dev.vars' : '.env');
const configFileName = isDevVars ? '.dev.vars' : '.env';

await fs.copy(envExamplePath, envPath);
console.log(chalk.green('Environment file created from template'));
await fs.copy(configSource, configPath);
console.log(chalk.green(`${configFileName} file created from template`));

// Configure environment variables interactively
await configureEnvironmentVariables(envPath);
await configureEnvironmentVariables(configPath);

createdEnvPath = envPath;
createdConfigPath = configPath;
}

// Update package.json with project name
Expand Down Expand Up @@ -266,12 +303,20 @@ export async function createProject(appName: string, framework: Framework, gitOp
console.log(chalk.green('\nProject created successfully!'));
console.log(chalk.blue('\nNext steps:'));
console.log(chalk.white(` cd ${appName}`));
console.log(chalk.white(' npm run install-all'));

// Different installation instructions based on framework
if (framework === 'express') {
console.log(chalk.white(' npm run install-all'));
} else {
console.log(chalk.white(' npm install'));
}

console.log(chalk.white(' npm start'));

// Only show env reminder if there is an .env file that exists
if (createdEnvPath && await fs.pathExists(createdEnvPath)) {
console.log(chalk.yellow('\nRemember to configure your environment variables in the .env file!'));

// Only show config file reminder if a config file was created and exists
if (createdConfigPath && await fs.pathExists(createdConfigPath)) {
const configFileName = path.basename(createdConfigPath);
console.log(chalk.yellow(`\nRemember to configure your environment variables in the ${configFileName} file!`));
}

} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion packages/atxp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async function main() {
const framework: Framework = createOptions.framework || 'express';

// Validate framework
const validFrameworks: Framework[] = ['express'];
const validFrameworks: Framework[] = ['express', 'cloudflare'];
if (createOptions.framework && !validFrameworks.includes(createOptions.framework)) {
console.error(`Error: Unknown framework "${createOptions.framework}". Available frameworks: ${validFrameworks.join(', ')}`);
process.exit(1);
Expand Down