mysystem-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AGENTS.md +66 -0
  2. package/copy-templates.js +47 -0
  3. package/dist/commands/init.js +15 -2
  4. package/dist/utils/detector.js +84 -32
  5. package/package.json +2 -2
  6. package/src/commands/init.ts +15 -2
  7. package/src/utils/detector.ts +86 -29
  8. package/templates/docker/fastapi.Dockerfile +32 -0
  9. package/templates/docker/nextjs.Dockerfile +45 -0
  10. package/templates/docker/node.Dockerfile +32 -0
  11. package/templates/docker/react.Dockerfile +38 -0
  12. package/templates/github/deploy-ec2.yml +163 -0
  13. package/templates/github/deploy.yml +94 -0
  14. package/templates/github/destroy.yml +43 -0
  15. package/templates/terraform/alb.tf +69 -0
  16. package/templates/terraform/bootstrap-oidc.yaml +69 -0
  17. package/templates/terraform/budget.tf +40 -0
  18. package/templates/terraform/db.tf +46 -0
  19. package/templates/terraform/dns.tf +89 -0
  20. package/templates/terraform/ecs.tf +110 -0
  21. package/templates/terraform/outputs.tf +14 -0
  22. package/templates/terraform/provider.tf +21 -0
  23. package/templates/terraform/rds_proxy.tf +157 -0
  24. package/templates/terraform/redis.tf +69 -0
  25. package/templates/terraform/security.tf +156 -0
  26. package/templates/terraform/variables.tf +46 -0
  27. package/templates/terraform/vpc.tf +68 -0
  28. package/templates/terraform/waf.tf +97 -0
  29. package/templates/terraform-ec2/budget.tf +40 -0
  30. package/templates/terraform-ec2/dns.tf +85 -0
  31. package/templates/terraform-ec2/ec2.tf +124 -0
  32. package/templates/terraform-ec2/ecr.tf +10 -0
  33. package/templates/terraform-ec2/outputs.tf +19 -0
  34. package/templates/terraform-ec2/provider.tf +22 -0
  35. package/templates/terraform-ec2/variables.tf +58 -0
  36. package/templates/terraform-ec2/vpc.tf +50 -0
package/AGENTS.md ADDED
@@ -0,0 +1,66 @@
1
+ # MySystem Agent Guidelines (AGENTS.md)
2
+
3
+ This file contains critical production-readiness instructions for AI coding agents (Cursor, Claude Code, Lovable, Bolt, Antigravity, etc.).
4
+
5
+ > [!IMPORTANT]
6
+ > **To the AI Agent**: You MUST read and follow these rules when editing or creating code in this codebase.
7
+
8
+ ---
9
+
10
+ ## 1. Core Production Architecture Rules
11
+
12
+ ### 1.1 Port & Port Binding
13
+ * Always bind the application web server to the port specified in the `PORT` environment variable.
14
+ * If `PORT` is not defined, fall back to a standard default (e.g., `3000` or `8080`), but never hardcode the listen port without checking `process.env.PORT` (Node) or `os.environ.get("PORT")` (Python).
15
+
16
+ ### 1.2 Health Check Endpoint
17
+ * The application must expose a GET `/health` or `/healthz` endpoint.
18
+ * It must return a `200 OK` response (preferably JSON, e.g., `{"status": "healthy"}`) with no authentication required.
19
+ * The AWS Load Balancer uses this endpoint to verify if the container is running. If it fails, the container will be terminated and replaced.
20
+
21
+ ### 1.3 Statelessness & File Storage
22
+ * Do NOT write persistent files to the local container filesystem. The container instances are ephemeral and can be destroyed at any time.
23
+ * For file uploads, use an external object storage service (e.g., AWS S3).
24
+ * For temporary processing, use the system `/tmp` directory, but expect it to disappear.
25
+
26
+ ### 1.4 Environment Variables & Secrets
27
+ * Never hardcode API keys, database credentials, passwords, or secrets.
28
+ * Always read configurations from environment variables.
29
+ * Generate a sample `.env.example` file when adding new environment variables.
30
+
31
+ ### 1.5 Database Migrations
32
+ * Ensure database migrations are safe to run concurrently or executed as a single-run step during deployment (never inside the web server's startup command if scaling to multiple container instances).
33
+ * Design database changes to be backward-compatible (e.g., add new columns as nullable, deprecate columns in a two-stage deployment).
34
+
35
+ ### 1.6 Logging & Error Tracking (Sentry & Pino)
36
+ * **Structured Logging**: For Node.js/TypeScript applications, format production logs in JSON using `pino` (or standard JSON formatters in Python/Go) for easy querying in CloudWatch. Avoid plain-text print statements in production.
37
+ * **Error Tracking**: If the `SENTRY_DSN` environment variable is present:
38
+ 1. Install the appropriate Sentry SDK (e.g., `@sentry/nextjs` for Next.js, `@sentry/node` for Node, or `sentry-sdk` for Python).
39
+ 2. Initialize the SDK in the application entrypoint.
40
+ 3. Ensure unhandled exceptions and runtime errors are automatically captured and reported to Sentry.
41
+
42
+ ### 1.7 EC2 Instance Sizing (Hobbyist Tier)
43
+ If the project is configured to use the **Hobbyist Tier** (single EC2 + Docker Compose), you must select and manage the `instance_type` parameter in `terraform/terraform.tfvars` according to the application footprint:
44
+ * **Micro-services / Static Frontends (Vite/React)**: Use `t4g.nano` (ARM64, 0.5GB RAM, ~$3.20/month) for maximum savings.
45
+ * **Standard Frameworks (Express, Next.js, FastAPI + Postgres)**: Use `t3.micro` (x86, 1GB RAM, **AWS Free Tier eligible**).
46
+ * **Medium Apps (App + Postgres + Redis or memory-intensive runtimes)**: Use `t3.small` (2GB RAM, ~$16/month) to prevent Out-Of-Memory (OOM) crashes.
47
+ * **Large Monoliths**: Scale to `t3.medium` (4GB RAM, ~$32/month) or larger when active traffic warrants it.
48
+ * *Note: When deploying on t4g (ARM) instances, ensure the Dockerfile is compiled for arm64.*
49
+
50
+ ---
51
+
52
+ ## 2. Docker & Container Rules
53
+ * Do not modify the `Dockerfile` in a way that breaks multi-stage optimization.
54
+ * Keep container images small by using minimal base images (e.g., alpine or slim variants).
55
+ * Always run the container application under a non-root user (e.g., `node` in Node.js) for security.
56
+
57
+ ---
58
+
59
+ ## 3. Automated Git Push Workflow (Vibecoder Deployment)
60
+ If the user tells you: **"I have set up the OIDC role / GitHub secrets"** or **"Deploy the app now"**:
61
+
62
+ 1. **Verify Git Status**: Run `git status` via terminal to verify the changes.
63
+ 2. **Stage files**: Run `git add .` to stage the changes.
64
+ 3. **Commit**: Commit with a clean message: `git commit -m "chore: configure MySystem deployment assets"`
65
+ 4. **Push**: Identify the active branch (usually `main` or `master`) and push it: `git push origin main`
66
+ 5. **Confirm**: Let the user know the push was successful and the GitHub Actions deployment pipeline has been triggered.
@@ -0,0 +1,47 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const rootDir = path.join(__dirname, '../..');
5
+ const cliDir = __dirname;
6
+
7
+ const srcTemplates = path.join(rootDir, 'templates');
8
+ const destTemplates = path.join(cliDir, 'templates');
9
+
10
+ const srcAgents = path.join(rootDir, 'AGENTS.md');
11
+ const destAgents = path.join(cliDir, 'AGENTS.md');
12
+
13
+ // Helper to recursively copy directories
14
+ function copyDirSync(src, dest) {
15
+ fs.mkdirSync(dest, { recursive: true });
16
+ const entries = fs.readdirSync(src, { withFileTypes: true });
17
+
18
+ for (let entry of entries) {
19
+ const srcPath = path.join(src, entry.name);
20
+ const destPath = path.join(dest, entry.name);
21
+
22
+ if (entry.isDirectory()) {
23
+ copyDirSync(srcPath, destPath);
24
+ } else {
25
+ fs.copyFileSync(srcPath, destPath);
26
+ }
27
+ }
28
+ }
29
+
30
+ console.log('Copying templates to CLI package...');
31
+ if (fs.existsSync(srcTemplates)) {
32
+ if (fs.existsSync(destTemplates)) {
33
+ fs.rmSync(destTemplates, { recursive: true, force: true });
34
+ }
35
+ copyDirSync(srcTemplates, destTemplates);
36
+ console.log(' ✅ Templates copied.');
37
+ } else {
38
+ console.error(' ❌ Source templates folder not found at:', srcTemplates);
39
+ }
40
+
41
+ console.log('Copying AGENTS.md to CLI package...');
42
+ if (fs.existsSync(srcAgents)) {
43
+ fs.copyFileSync(srcAgents, destAgents);
44
+ console.log(' ✅ AGENTS.md copied.');
45
+ } else {
46
+ console.error(' ❌ Source AGENTS.md not found at:', srcAgents);
47
+ }
@@ -53,8 +53,21 @@ async function runInit(projectRoot) {
53
53
  try {
54
54
  const appNameInput = await rl.question(`Enter application name [${detected.name}]: `);
55
55
  const appName = appNameInput.trim() || detected.name;
56
- const awsRegionInput = await rl.question(`Enter AWS region [us-east-1]: `);
57
- const awsRegion = awsRegionInput.trim() || 'us-east-1';
56
+ let awsRegion = 'us-east-1';
57
+ const regionRegex = /^[a-z]{2,}-[a-z]+-[0-9]+$/;
58
+ while (true) {
59
+ const awsRegionInput = await rl.question(`Enter AWS region [us-east-1]: `);
60
+ const val = awsRegionInput.trim();
61
+ if (!val) {
62
+ awsRegion = 'us-east-1';
63
+ break;
64
+ }
65
+ if (regionRegex.test(val)) {
66
+ awsRegion = val;
67
+ break;
68
+ }
69
+ console.log('\x1b[31mInvalid AWS region format. Example: us-east-1, eu-west-1, ap-south-1. Please try again.\x1b[0m');
70
+ }
58
71
  console.log('Select your AWS hosting tier:');
59
72
  console.log(' \x1b[36m1. Production\x1b[0m [ECS Fargate + RDS + ALB + WAF] (~$17/mo free-tier, ~$51/mo standard)');
60
73
  console.log(' \x1b[36m2. Hobbyist\x1b[0m [Single EC2 + Docker Compose + Postgres] ($0/mo free-tier, ~$3.20/mo standard)');
@@ -44,7 +44,22 @@ function detectProject(projectRoot) {
44
44
  hasRedis: false,
45
45
  name: path.basename(projectRoot) || 'mysystem-app',
46
46
  };
47
- // 1. Read package.json if it exists
47
+ // 1. Scan for framework config files directly first
48
+ const hasNextConfig = ['next.config.js', 'next.config.mjs', 'next.config.ts'].some(f => fs.existsSync(path.join(projectRoot, f)));
49
+ const hasViteConfig = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'].some(f => fs.existsSync(path.join(projectRoot, f)));
50
+ const hasPrismaSchema = fs.existsSync(path.join(projectRoot, 'prisma', 'schema.prisma')) || fs.existsSync(path.join(projectRoot, 'schema.prisma'));
51
+ if (hasNextConfig) {
52
+ info.type = 'nextjs';
53
+ info.port = 3000;
54
+ }
55
+ else if (hasViteConfig) {
56
+ info.type = 'react-vite';
57
+ info.port = 80; // Served via Nginx in production container
58
+ }
59
+ if (hasPrismaSchema) {
60
+ info.hasDatabase = true;
61
+ }
62
+ // 2. Read package.json if it exists
48
63
  const packageJsonPath = path.join(projectRoot, 'package.json');
49
64
  if (fs.existsSync(packageJsonPath)) {
50
65
  try {
@@ -56,55 +71,92 @@ function detectProject(projectRoot) {
56
71
  ...packageJson.dependencies,
57
72
  ...packageJson.devDependencies,
58
73
  };
59
- // Detect database dependencies (pg, prisma, typeorm, sequelize, knex, sqlite3, mysql2)
60
- const dbDeps = ['pg', 'postgres', 'prisma', 'typeorm', 'sequelize', 'knex', 'sqlite3', 'mysql2'];
61
- if (Object.keys(deps).some(dep => dbDeps.includes(dep))) {
74
+ // Detect database dependencies (including MongoDB, SQLite, Postgres, MySQL, MariaDB, SQL Server, Drizzle, etc.)
75
+ const dbDeps = [
76
+ 'pg', 'postgres', 'prisma', 'typeorm', 'sequelize', 'knex', 'sqlite3',
77
+ 'mysql2', 'mongodb', 'mongoose', 'mssql', 'mariadb', 'pg-promise',
78
+ 'drizzle-orm', 'better-sqlite3'
79
+ ];
80
+ if (Object.keys(deps).some(dep => dbDeps.includes(dep) || dep.startsWith('@prisma/'))) {
62
81
  info.hasDatabase = true;
63
82
  }
64
83
  // Detect Redis dependencies
65
- const redisDeps = ['redis', 'ioredis', 'bull', 'bullmq', 'handy-redis', 'keyv'];
84
+ const redisDeps = ['redis', 'ioredis', 'bull', 'bullmq', 'handy-redis', 'keyv', 'redis-om'];
66
85
  if (Object.keys(deps).some(dep => redisDeps.includes(dep))) {
67
86
  info.hasRedis = true;
68
87
  }
69
- // Framework detection
70
- if (deps['next']) {
71
- info.type = 'nextjs';
72
- info.port = 3000;
73
- }
74
- else if (deps['vite'] || deps['react']) {
75
- // A Vite React app (or standard React)
76
- info.type = 'react-vite';
77
- info.port = 80; // React SPAs get served on port 80 via Nginx in production
78
- }
79
- else if (deps['express'] || deps['koa'] || deps['fastify'] || deps['nest']) {
80
- info.type = 'node';
81
- info.port = 3000;
82
- }
83
- else {
84
- info.type = 'node';
85
- info.port = 3000;
88
+ // Fallback Framework detection if config files weren't matched
89
+ if (info.type === 'unknown') {
90
+ if (deps['next']) {
91
+ info.type = 'nextjs';
92
+ info.port = 3000;
93
+ }
94
+ else if (deps['vite'] || deps['react']) {
95
+ info.type = 'react-vite';
96
+ info.port = 80;
97
+ }
98
+ else if (deps['express'] || deps['koa'] || deps['fastify'] || deps['nest'] || deps['hapi']) {
99
+ info.type = 'node';
100
+ info.port = 3000;
101
+ }
102
+ else {
103
+ info.type = 'node';
104
+ info.port = 3000;
105
+ }
86
106
  }
87
107
  }
88
108
  catch (e) {
89
- // Ignore JSON parse errors and continue
109
+ // Ignore JSON parse errors
90
110
  }
91
111
  }
92
- // 2. Read requirements.txt or main.py if Python
112
+ // 3. Read requirements.txt or main.py if Python
93
113
  const reqTxtPath = path.join(projectRoot, 'requirements.txt');
94
114
  const mainPyPath = path.join(projectRoot, 'main.py');
95
- if (fs.existsSync(reqTxtPath) || fs.existsSync(mainPyPath)) {
96
- info.type = 'fastapi';
97
- info.port = 8000;
98
- if (fs.existsSync(reqTxtPath)) {
99
- const reqs = fs.readFileSync(reqTxtPath, 'utf8');
100
- const dbTerms = ['postgresql', 'psycopg2', 'sqlalchemy', 'tortoise-orm', 'peewee', 'asyncpg'];
101
- if (dbTerms.some(term => reqs.toLowerCase().includes(term))) {
115
+ const pyProjectToml = path.join(projectRoot, 'pyproject.toml');
116
+ if (fs.existsSync(reqTxtPath) || fs.existsSync(mainPyPath) || fs.existsSync(pyProjectToml)) {
117
+ if (info.type === 'unknown') {
118
+ info.type = 'fastapi';
119
+ info.port = 8000;
120
+ }
121
+ const checkPythonDeps = (content) => {
122
+ const dbTerms = ['postgresql', 'psycopg2', 'sqlalchemy', 'tortoise-orm', 'peewee', 'asyncpg', 'pymongo', 'mongoengine', 'mysqlclient'];
123
+ if (dbTerms.some(term => content.toLowerCase().includes(term))) {
102
124
  info.hasDatabase = true;
103
125
  }
104
126
  const redisTerms = ['redis', 'django-redis', 'celery'];
105
- if (redisTerms.some(term => reqs.toLowerCase().includes(term))) {
127
+ if (redisTerms.some(term => content.toLowerCase().includes(term))) {
106
128
  info.hasRedis = true;
107
129
  }
130
+ };
131
+ if (fs.existsSync(reqTxtPath)) {
132
+ checkPythonDeps(fs.readFileSync(reqTxtPath, 'utf8'));
133
+ }
134
+ if (fs.existsSync(pyProjectToml)) {
135
+ checkPythonDeps(fs.readFileSync(pyProjectToml, 'utf8'));
136
+ }
137
+ }
138
+ // 4. Scan .env, .env.example, .env.local for database/redis keywords (CRITICAL fallback)
139
+ const envFiles = ['.env', '.env.example', '.env.local', '.env.development', '.env.production'];
140
+ for (const file of envFiles) {
141
+ const filePath = path.join(projectRoot, file);
142
+ if (fs.existsSync(filePath)) {
143
+ try {
144
+ const content = fs.readFileSync(filePath, 'utf8');
145
+ const dbKeywords = [
146
+ 'DATABASE_URL', 'DATABASE_URI', 'POSTGRES_', 'MONGODB_URI', 'MONGO_URI',
147
+ 'DB_HOST', 'DB_PASSWORD', 'DB_CONNECTION', 'MYSQL_URL', 'DATABASE_NAME'
148
+ ];
149
+ const redisKeywords = ['REDIS_URL', 'REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD'];
150
+ if (dbKeywords.some(kw => content.includes(kw))) {
151
+ info.hasDatabase = true;
152
+ }
153
+ if (redisKeywords.some(kw => content.includes(kw))) {
154
+ info.hasRedis = true;
155
+ }
156
+ }
157
+ catch {
158
+ // Ignore file read errors
159
+ }
108
160
  }
109
161
  }
110
162
  return info;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysystem-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Zero-config deployment standard and CLI for AI-generated applications on AWS Fargate",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "mysystem-cli": "dist/index.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc",
11
+ "build": "tsc && node copy-templates.js",
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
14
  "keywords": [
@@ -22,8 +22,21 @@ export async function runInit(projectRoot: string) {
22
22
  const appNameInput = await rl.question(`Enter application name [${detected.name}]: `);
23
23
  const appName = appNameInput.trim() || detected.name;
24
24
 
25
- const awsRegionInput = await rl.question(`Enter AWS region [us-east-1]: `);
26
- const awsRegion = awsRegionInput.trim() || 'us-east-1';
25
+ let awsRegion = 'us-east-1';
26
+ const regionRegex = /^[a-z]{2,}-[a-z]+-[0-9]+$/;
27
+ while (true) {
28
+ const awsRegionInput = await rl.question(`Enter AWS region [us-east-1]: `);
29
+ const val = awsRegionInput.trim();
30
+ if (!val) {
31
+ awsRegion = 'us-east-1';
32
+ break;
33
+ }
34
+ if (regionRegex.test(val)) {
35
+ awsRegion = val;
36
+ break;
37
+ }
38
+ console.log('\x1b[31mInvalid AWS region format. Example: us-east-1, eu-west-1, ap-south-1. Please try again.\x1b[0m');
39
+ }
27
40
 
28
41
  console.log('Select your AWS hosting tier:');
29
42
  console.log(' \x1b[36m1. Production\x1b[0m [ECS Fargate + RDS + ALB + WAF] (~$17/mo free-tier, ~$51/mo standard)');
@@ -18,7 +18,24 @@ export function detectProject(projectRoot: string): ProjectInfo {
18
18
  name: path.basename(projectRoot) || 'mysystem-app',
19
19
  };
20
20
 
21
- // 1. Read package.json if it exists
21
+ // 1. Scan for framework config files directly first
22
+ const hasNextConfig = ['next.config.js', 'next.config.mjs', 'next.config.ts'].some(f => fs.existsSync(path.join(projectRoot, f)));
23
+ const hasViteConfig = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'].some(f => fs.existsSync(path.join(projectRoot, f)));
24
+ const hasPrismaSchema = fs.existsSync(path.join(projectRoot, 'prisma', 'schema.prisma')) || fs.existsSync(path.join(projectRoot, 'schema.prisma'));
25
+
26
+ if (hasNextConfig) {
27
+ info.type = 'nextjs';
28
+ info.port = 3000;
29
+ } else if (hasViteConfig) {
30
+ info.type = 'react-vite';
31
+ info.port = 80; // Served via Nginx in production container
32
+ }
33
+
34
+ if (hasPrismaSchema) {
35
+ info.hasDatabase = true;
36
+ }
37
+
38
+ // 2. Read package.json if it exists
22
39
  const packageJsonPath = path.join(projectRoot, 'package.json');
23
40
  if (fs.existsSync(packageJsonPath)) {
24
41
  try {
@@ -32,56 +49,96 @@ export function detectProject(projectRoot: string): ProjectInfo {
32
49
  ...packageJson.devDependencies,
33
50
  };
34
51
 
35
- // Detect database dependencies (pg, prisma, typeorm, sequelize, knex, sqlite3, mysql2)
36
- const dbDeps = ['pg', 'postgres', 'prisma', 'typeorm', 'sequelize', 'knex', 'sqlite3', 'mysql2'];
37
- if (Object.keys(deps).some(dep => dbDeps.includes(dep))) {
52
+ // Detect database dependencies (including MongoDB, SQLite, Postgres, MySQL, MariaDB, SQL Server, Drizzle, etc.)
53
+ const dbDeps = [
54
+ 'pg', 'postgres', 'prisma', 'typeorm', 'sequelize', 'knex', 'sqlite3',
55
+ 'mysql2', 'mongodb', 'mongoose', 'mssql', 'mariadb', 'pg-promise',
56
+ 'drizzle-orm', 'better-sqlite3'
57
+ ];
58
+ if (Object.keys(deps).some(dep => dbDeps.includes(dep) || dep.startsWith('@prisma/'))) {
38
59
  info.hasDatabase = true;
39
60
  }
40
61
 
41
62
  // Detect Redis dependencies
42
- const redisDeps = ['redis', 'ioredis', 'bull', 'bullmq', 'handy-redis', 'keyv'];
63
+ const redisDeps = ['redis', 'ioredis', 'bull', 'bullmq', 'handy-redis', 'keyv', 'redis-om'];
43
64
  if (Object.keys(deps).some(dep => redisDeps.includes(dep))) {
44
65
  info.hasRedis = true;
45
66
  }
46
67
 
47
- // Framework detection
48
- if (deps['next']) {
49
- info.type = 'nextjs';
50
- info.port = 3000;
51
- } else if (deps['vite'] || deps['react']) {
52
- // A Vite React app (or standard React)
53
- info.type = 'react-vite';
54
- info.port = 80; // React SPAs get served on port 80 via Nginx in production
55
- } else if (deps['express'] || deps['koa'] || deps['fastify'] || deps['nest']) {
56
- info.type = 'node';
57
- info.port = 3000;
58
- } else {
59
- info.type = 'node';
60
- info.port = 3000;
68
+ // Fallback Framework detection if config files weren't matched
69
+ if (info.type === 'unknown') {
70
+ if (deps['next']) {
71
+ info.type = 'nextjs';
72
+ info.port = 3000;
73
+ } else if (deps['vite'] || deps['react']) {
74
+ info.type = 'react-vite';
75
+ info.port = 80;
76
+ } else if (deps['express'] || deps['koa'] || deps['fastify'] || deps['nest'] || deps['hapi']) {
77
+ info.type = 'node';
78
+ info.port = 3000;
79
+ } else {
80
+ info.type = 'node';
81
+ info.port = 3000;
82
+ }
61
83
  }
62
84
  } catch (e) {
63
- // Ignore JSON parse errors and continue
85
+ // Ignore JSON parse errors
64
86
  }
65
87
  }
66
88
 
67
- // 2. Read requirements.txt or main.py if Python
89
+ // 3. Read requirements.txt or main.py if Python
68
90
  const reqTxtPath = path.join(projectRoot, 'requirements.txt');
69
91
  const mainPyPath = path.join(projectRoot, 'main.py');
70
- if (fs.existsSync(reqTxtPath) || fs.existsSync(mainPyPath)) {
71
- info.type = 'fastapi';
72
- info.port = 8000;
92
+ const pyProjectToml = path.join(projectRoot, 'pyproject.toml');
73
93
 
74
- if (fs.existsSync(reqTxtPath)) {
75
- const reqs = fs.readFileSync(reqTxtPath, 'utf8');
76
- const dbTerms = ['postgresql', 'psycopg2', 'sqlalchemy', 'tortoise-orm', 'peewee', 'asyncpg'];
77
- if (dbTerms.some(term => reqs.toLowerCase().includes(term))) {
94
+ if (fs.existsSync(reqTxtPath) || fs.existsSync(mainPyPath) || fs.existsSync(pyProjectToml)) {
95
+ if (info.type === 'unknown') {
96
+ info.type = 'fastapi';
97
+ info.port = 8000;
98
+ }
99
+
100
+ const checkPythonDeps = (content: string) => {
101
+ const dbTerms = ['postgresql', 'psycopg2', 'sqlalchemy', 'tortoise-orm', 'peewee', 'asyncpg', 'pymongo', 'mongoengine', 'mysqlclient'];
102
+ if (dbTerms.some(term => content.toLowerCase().includes(term))) {
78
103
  info.hasDatabase = true;
79
104
  }
80
105
 
81
106
  const redisTerms = ['redis', 'django-redis', 'celery'];
82
- if (redisTerms.some(term => reqs.toLowerCase().includes(term))) {
107
+ if (redisTerms.some(term => content.toLowerCase().includes(term))) {
83
108
  info.hasRedis = true;
84
109
  }
110
+ };
111
+
112
+ if (fs.existsSync(reqTxtPath)) {
113
+ checkPythonDeps(fs.readFileSync(reqTxtPath, 'utf8'));
114
+ }
115
+ if (fs.existsSync(pyProjectToml)) {
116
+ checkPythonDeps(fs.readFileSync(pyProjectToml, 'utf8'));
117
+ }
118
+ }
119
+
120
+ // 4. Scan .env, .env.example, .env.local for database/redis keywords (CRITICAL fallback)
121
+ const envFiles = ['.env', '.env.example', '.env.local', '.env.development', '.env.production'];
122
+ for (const file of envFiles) {
123
+ const filePath = path.join(projectRoot, file);
124
+ if (fs.existsSync(filePath)) {
125
+ try {
126
+ const content = fs.readFileSync(filePath, 'utf8');
127
+ const dbKeywords = [
128
+ 'DATABASE_URL', 'DATABASE_URI', 'POSTGRES_', 'MONGODB_URI', 'MONGO_URI',
129
+ 'DB_HOST', 'DB_PASSWORD', 'DB_CONNECTION', 'MYSQL_URL', 'DATABASE_NAME'
130
+ ];
131
+ const redisKeywords = ['REDIS_URL', 'REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD'];
132
+
133
+ if (dbKeywords.some(kw => content.includes(kw))) {
134
+ info.hasDatabase = true;
135
+ }
136
+ if (redisKeywords.some(kw => content.includes(kw))) {
137
+ info.hasRedis = true;
138
+ }
139
+ } catch {
140
+ // Ignore file read errors
141
+ }
85
142
  }
86
143
  }
87
144
 
@@ -0,0 +1,32 @@
1
+ # Multi-stage build for Python FastAPI Applications
2
+
3
+ # --- Build Stage ---
4
+ FROM python:3.11-slim AS builder
5
+ WORKDIR /app
6
+ RUN python -m venv /opt/venv
7
+ ENV PATH="/opt/venv/bin:$PATH"
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # --- Production Stage ---
12
+ FROM python:3.11-slim AS runner
13
+ WORKDIR /app
14
+ COPY --from=builder /opt/venv /opt/venv
15
+ ENV PATH="/opt/venv/bin:$PATH"
16
+ ENV PYTHONDONTWRITEBYTECODE=1
17
+ ENV PYTHONUNBUFFERED=1
18
+
19
+ COPY . .
20
+
21
+ # Run as non-privileged user
22
+ RUN adduser --disabled-password --gecos "" appuser && chown -R appuser:appuser /app
23
+ USER appuser
24
+
25
+ # Healthcheck
26
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
27
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:' + str(os.environ.get('PORT', 8000)) + '/health', timeout=5)" || exit 1
28
+
29
+ EXPOSE 8000
30
+ ENV PORT=8000
31
+
32
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1,45 @@
1
+ # Multi-stage build for Next.js (using Standalone output)
2
+
3
+ # --- Dependencies Stage ---
4
+ FROM node:20-alpine AS deps
5
+ RUN apk add --no-cache libc6-compat
6
+ WORKDIR /app
7
+ COPY package*.json ./
8
+ RUN npm ci
9
+
10
+ # --- Build Stage ---
11
+ FROM node:20-alpine AS builder
12
+ WORKDIR /app
13
+ COPY --from=deps /app/node_modules ./node_modules
14
+ COPY . .
15
+ # Set Next.js telemetry disable
16
+ ENV NEXT_TELEMETRY_DISABLED=1
17
+ RUN npm run build
18
+
19
+ # --- Production Stage ---
20
+ FROM node:20-alpine AS runner
21
+ WORKDIR /app
22
+
23
+ ENV NODE_ENV=production
24
+ ENV NEXT_TELEMETRY_DISABLED=1
25
+
26
+ # Create non-root user
27
+ RUN addgroup --system --gid 1001 nodejs
28
+ RUN adduser --system --uid 1001 nextjs
29
+
30
+ # Copy standalone build and static assets
31
+ COPY --from=builder /app/public ./public
32
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
33
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
34
+
35
+ USER nextjs
36
+
37
+ EXPOSE 3000
38
+ ENV PORT=3000
39
+ ENV HOSTNAME="0.0.0.0"
40
+
41
+ # Healthcheck
42
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
43
+ CMD node -e "fetch('http://localhost:3000/api/health').then(r => r.status === 200 ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
44
+
45
+ CMD ["node", "server.js"]
@@ -0,0 +1,32 @@
1
+ # Multi-stage build for Node.js Applications
2
+
3
+ # --- Build Stage ---
4
+ FROM node:20-alpine AS builder
5
+ WORKDIR /app
6
+ COPY package*.json ./
7
+ RUN npm ci
8
+ COPY . .
9
+ RUN npm run build --if-present
10
+
11
+ # --- Production Stage ---
12
+ FROM node:20-alpine AS runner
13
+ WORKDIR /app
14
+ ENV NODE_ENV=production
15
+ COPY package*.json ./
16
+ RUN npm ci --only=production
17
+ COPY --from=builder /app/dist ./dist --keep-directory-structure --if-present
18
+ COPY --from=builder /app/build ./build --keep-directory-structure --if-present
19
+ COPY --from=builder /app/server.js ./server.js --if-present
20
+ COPY --from=builder /app/index.js ./index.js --if-present
21
+
22
+ # Set safe, non-root user
23
+ USER node
24
+
25
+ # Health check setup
26
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
27
+ CMD node -e "fetch('http://localhost:' + (process.env.PORT || 3000) + '/health').then(r => r.status === 200 ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
28
+
29
+ EXPOSE 3000
30
+ ENV PORT=3000
31
+
32
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,38 @@
1
+ # Multi-stage build for React/Vite Single Page Applications (served via Nginx)
2
+
3
+ # --- Build Stage ---
4
+ FROM node:20-alpine AS builder
5
+ WORKDIR /app
6
+ COPY package*.json ./
7
+ RUN npm ci
8
+ COPY . .
9
+ RUN npm run build
10
+
11
+ # --- Production Stage ---
12
+ FROM nginx:alpine AS runner
13
+ COPY --from=builder /app/dist /usr/share/nginx/html
14
+
15
+ # Custom Nginx configuration to support SPA routing (redirect all fallback routes to index.html)
16
+ RUN echo $'\n\
17
+ server {\n\
18
+ listen 80;\n\
19
+ location / {\n\
20
+ root /usr/share/nginx/html;\n\
21
+ index index.html index.htm;\n\
22
+ try_files $uri $uri/ /index.html;\n\
23
+ }\n\
24
+ location /health {\n\
25
+ access_log off;\n\
26
+ add_header Content-Type text/plain;\n\
27
+ return 200 "healthy\\n";\n\
28
+ }\n\
29
+ }' > /etc/nginx/conf.d/default.conf
30
+
31
+ # Healthcheck
32
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
33
+ CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
34
+
35
+ EXPOSE 80
36
+ ENV PORT=80
37
+
38
+ CMD ["nginx", "-g", "daemon off;"]