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.
- package/AGENTS.md +66 -0
- package/copy-templates.js +47 -0
- package/dist/commands/init.js +15 -2
- package/dist/utils/detector.js +84 -32
- package/package.json +2 -2
- package/src/commands/init.ts +15 -2
- package/src/utils/detector.ts +86 -29
- package/templates/docker/fastapi.Dockerfile +32 -0
- package/templates/docker/nextjs.Dockerfile +45 -0
- package/templates/docker/node.Dockerfile +32 -0
- package/templates/docker/react.Dockerfile +38 -0
- package/templates/github/deploy-ec2.yml +163 -0
- package/templates/github/deploy.yml +94 -0
- package/templates/github/destroy.yml +43 -0
- package/templates/terraform/alb.tf +69 -0
- package/templates/terraform/bootstrap-oidc.yaml +69 -0
- package/templates/terraform/budget.tf +40 -0
- package/templates/terraform/db.tf +46 -0
- package/templates/terraform/dns.tf +89 -0
- package/templates/terraform/ecs.tf +110 -0
- package/templates/terraform/outputs.tf +14 -0
- package/templates/terraform/provider.tf +21 -0
- package/templates/terraform/rds_proxy.tf +157 -0
- package/templates/terraform/redis.tf +69 -0
- package/templates/terraform/security.tf +156 -0
- package/templates/terraform/variables.tf +46 -0
- package/templates/terraform/vpc.tf +68 -0
- package/templates/terraform/waf.tf +97 -0
- package/templates/terraform-ec2/budget.tf +40 -0
- package/templates/terraform-ec2/dns.tf +85 -0
- package/templates/terraform-ec2/ec2.tf +124 -0
- package/templates/terraform-ec2/ecr.tf +10 -0
- package/templates/terraform-ec2/outputs.tf +19 -0
- package/templates/terraform-ec2/provider.tf +22 -0
- package/templates/terraform-ec2/variables.tf +58 -0
- 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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
57
|
-
const
|
|
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)');
|
package/dist/utils/detector.js
CHANGED
|
@@ -44,7 +44,22 @@ function detectProject(projectRoot) {
|
|
|
44
44
|
hasRedis: false,
|
|
45
45
|
name: path.basename(projectRoot) || 'mysystem-app',
|
|
46
46
|
};
|
|
47
|
-
// 1.
|
|
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 (
|
|
60
|
-
const dbDeps = [
|
|
61
|
-
|
|
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 (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
109
|
+
// Ignore JSON parse errors
|
|
90
110
|
}
|
|
91
111
|
}
|
|
92
|
-
//
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
info.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 =>
|
|
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.
|
|
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": [
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
const
|
|
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)');
|
package/src/utils/detector.ts
CHANGED
|
@@ -18,7 +18,24 @@ export function detectProject(projectRoot: string): ProjectInfo {
|
|
|
18
18
|
name: path.basename(projectRoot) || 'mysystem-app',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
// 1.
|
|
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 (
|
|
36
|
-
const dbDeps = [
|
|
37
|
-
|
|
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 (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
85
|
+
// Ignore JSON parse errors
|
|
64
86
|
}
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
//
|
|
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
|
-
|
|
71
|
-
info.type = 'fastapi';
|
|
72
|
-
info.port = 8000;
|
|
92
|
+
const pyProjectToml = path.join(projectRoot, 'pyproject.toml');
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 =>
|
|
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;"]
|