mysystem-cli 1.0.0
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/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +194 -0
- package/dist/commands/destroy.d.ts +1 -0
- package/dist/commands/destroy.js +92 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +255 -0
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.js +76 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +64 -0
- package/dist/utils/detector.d.ts +8 -0
- package/dist/utils/detector.js +111 -0
- package/dist/utils/installer.d.ts +1 -0
- package/dist/utils/installer.js +122 -0
- package/package.json +34 -0
- package/src/commands/audit.ts +176 -0
- package/src/commands/destroy.ts +65 -0
- package/src/commands/init.ts +252 -0
- package/src/commands/logs.ts +48 -0
- package/src/index.ts +66 -0
- package/src/utils/detector.ts +89 -0
- package/src/utils/installer.ts +89 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as readline from 'readline/promises';
|
|
4
|
+
import { detectProject } from '../utils/detector';
|
|
5
|
+
|
|
6
|
+
export async function runInit(projectRoot: string) {
|
|
7
|
+
console.log('\n🔍 Scanning project codebase...');
|
|
8
|
+
const detected = detectProject(projectRoot);
|
|
9
|
+
|
|
10
|
+
console.log(`Detected application type: \x1b[36m${detected.type}\x1b[0m`);
|
|
11
|
+
console.log(`Default container port: \x1b[36m${detected.port}\x1b[0m`);
|
|
12
|
+
console.log(`Requires database: \x1b[36m${detected.hasDatabase ? 'Yes' : 'No'}\x1b[0m`);
|
|
13
|
+
console.log(`Requires Redis: \x1b[36m${detected.hasRedis ? 'Yes' : 'No'}\x1b[0m\n`);
|
|
14
|
+
|
|
15
|
+
// Prompt the user for overrides using native readline
|
|
16
|
+
const rl = readline.createInterface({
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const appNameInput = await rl.question(`Enter application name [${detected.name}]: `);
|
|
23
|
+
const appName = appNameInput.trim() || detected.name;
|
|
24
|
+
|
|
25
|
+
const awsRegionInput = await rl.question(`Enter AWS region [us-east-1]: `);
|
|
26
|
+
const awsRegion = awsRegionInput.trim() || 'us-east-1';
|
|
27
|
+
|
|
28
|
+
console.log('Select your AWS hosting tier:');
|
|
29
|
+
console.log(' \x1b[36m1. Production\x1b[0m [ECS Fargate + RDS + ALB + WAF] (~$17/mo free-tier, ~$51/mo standard)');
|
|
30
|
+
console.log(' \x1b[36m2. Hobbyist\x1b[0m [Single EC2 + Docker Compose + Postgres] ($0/mo free-tier, ~$3.20/mo standard)');
|
|
31
|
+
const tierInput = await rl.question('Choose tier [1]: ');
|
|
32
|
+
const isProductionTier = tierInput.trim() !== '2';
|
|
33
|
+
|
|
34
|
+
let enableDatabase = detected.hasDatabase;
|
|
35
|
+
let enableRedis = detected.hasRedis;
|
|
36
|
+
let enableRdsProxy = false;
|
|
37
|
+
|
|
38
|
+
if (isProductionTier) {
|
|
39
|
+
const enableDbInput = await rl.question(`Enable RDS PostgreSQL database? (y/n) [${detected.hasDatabase ? 'y' : 'n'}]: `);
|
|
40
|
+
enableDatabase = enableDbInput.trim() ? enableDbInput.trim().toLowerCase() === 'y' : detected.hasDatabase;
|
|
41
|
+
|
|
42
|
+
const enableRedisInput = await rl.question(`Enable ElastiCache Redis? (y/n) [${detected.hasRedis ? 'y' : 'n'}]: `);
|
|
43
|
+
enableRedis = enableRedisInput.trim() ? enableRedisInput.trim().toLowerCase() === 'y' : detected.hasRedis;
|
|
44
|
+
|
|
45
|
+
if (enableDatabase) {
|
|
46
|
+
const enableProxyInput = await rl.question(`Enable RDS Proxy (PgBouncer connection pooler)? (y/n) [n]: `);
|
|
47
|
+
enableRdsProxy = enableProxyInput.trim().toLowerCase() === 'y';
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// Hobby tier runs Postgres inside Docker Compose on the same instance (flat cost)
|
|
51
|
+
enableDatabase = true;
|
|
52
|
+
enableRedis = false;
|
|
53
|
+
enableRdsProxy = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const billingEmailInput = await rl.question(`Enter email for AWS budget alerts (press Enter to skip): `);
|
|
57
|
+
const billingEmail = billingEmailInput.trim();
|
|
58
|
+
|
|
59
|
+
const customDomainInput = await rl.question(`Enable custom domain & HTTPS SSL certificate? (y/n) [n]: `);
|
|
60
|
+
const enableCustomDomain = customDomainInput.trim().toLowerCase() === 'y';
|
|
61
|
+
|
|
62
|
+
let domainName = '';
|
|
63
|
+
let dnsProvider = 'external';
|
|
64
|
+
if (enableCustomDomain) {
|
|
65
|
+
const domainInput = await rl.question(`Enter custom domain (e.g., app.myproduct.com): `);
|
|
66
|
+
domainName = domainInput.trim();
|
|
67
|
+
|
|
68
|
+
const providerInput = await rl.question(`Is your domain DNS managed on AWS Route 53? (y/n) [n]: `);
|
|
69
|
+
dnsProvider = providerInput.trim().toLowerCase() === 'y' ? 'route53' : 'external';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const enableSentryInput = await rl.question(`Enable Sentry Error Tracking? (y/n) [n]: `);
|
|
73
|
+
const enableSentry = enableSentryInput.trim().toLowerCase() === 'y';
|
|
74
|
+
let sentryDsn = '';
|
|
75
|
+
if (enableSentry) {
|
|
76
|
+
const dsnInput = await rl.question(`Enter Sentry DSN (press Enter to skip and configure later): `);
|
|
77
|
+
sentryDsn = dsnInput.trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
rl.close();
|
|
81
|
+
|
|
82
|
+
console.log('\n🚀 Generating deployment assets...');
|
|
83
|
+
|
|
84
|
+
// Find templates directory relative to CLI source or dist
|
|
85
|
+
let templatesDir = '';
|
|
86
|
+
const pathsToCheck = [
|
|
87
|
+
path.join(__dirname, '../../templates'), // Packaged location relative to dist/commands/
|
|
88
|
+
path.join(__dirname, '../../../templates'), // Local dev relative to src/commands/
|
|
89
|
+
path.join(__dirname, '../../../../templates'), // Local dev relative to packages/cli/dist/commands
|
|
90
|
+
path.join(__dirname, '../../../../../templates'), // Fallback
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (const p of pathsToCheck) {
|
|
94
|
+
if (fs.existsSync(p)) {
|
|
95
|
+
templatesDir = p;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(templatesDir)) {
|
|
101
|
+
throw new Error(`Templates directory not found. Looked in: ${templatesDir}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1. Copy Dockerfile
|
|
105
|
+
let dockerfileTemplate = '';
|
|
106
|
+
switch (detected.type) {
|
|
107
|
+
case 'nextjs':
|
|
108
|
+
dockerfileTemplate = 'nextjs.Dockerfile';
|
|
109
|
+
break;
|
|
110
|
+
case 'react-vite':
|
|
111
|
+
dockerfileTemplate = 'react.Dockerfile';
|
|
112
|
+
break;
|
|
113
|
+
case 'fastapi':
|
|
114
|
+
dockerfileTemplate = 'fastapi.Dockerfile';
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
dockerfileTemplate = 'node.Dockerfile';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const srcDockerfile = path.join(templatesDir, 'docker', dockerfileTemplate);
|
|
121
|
+
const destDockerfile = path.join(projectRoot, 'Dockerfile');
|
|
122
|
+
if (fs.existsSync(srcDockerfile)) {
|
|
123
|
+
fs.copyFileSync(srcDockerfile, destDockerfile);
|
|
124
|
+
console.log(' ✅ Created Dockerfile');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Create GitHub Actions folder & copy deploy.yml and destroy.yml
|
|
128
|
+
const githubWorkflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
129
|
+
fs.mkdirSync(githubWorkflowDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
const srcDeployWorkflow = path.join(templatesDir, 'github', isProductionTier ? 'deploy.yml' : 'deploy-ec2.yml');
|
|
132
|
+
const destDeployWorkflow = path.join(githubWorkflowDir, 'mysystem-deploy.yml');
|
|
133
|
+
if (fs.existsSync(srcDeployWorkflow)) {
|
|
134
|
+
let workflowContent = fs.readFileSync(srcDeployWorkflow, 'utf8');
|
|
135
|
+
workflowContent = workflowContent.replace(/aws-region: us-east-1/g, `aws-region: ${awsRegion}`);
|
|
136
|
+
fs.writeFileSync(destDeployWorkflow, workflowContent, 'utf8');
|
|
137
|
+
console.log(' ✅ Created .github/workflows/mysystem-deploy.yml');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const srcDestroyWorkflow = path.join(templatesDir, 'github', 'destroy.yml');
|
|
141
|
+
const destDestroyWorkflow = path.join(githubWorkflowDir, 'mysystem-destroy.yml');
|
|
142
|
+
if (fs.existsSync(srcDestroyWorkflow)) {
|
|
143
|
+
let workflowContent = fs.readFileSync(srcDestroyWorkflow, 'utf8');
|
|
144
|
+
workflowContent = workflowContent.replace(/aws-region: us-east-1/g, `aws-region: ${awsRegion}`);
|
|
145
|
+
fs.writeFileSync(destDestroyWorkflow, workflowContent, 'utf8');
|
|
146
|
+
console.log(' ✅ Created .github/workflows/mysystem-destroy.yml');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3. Create Terraform directory & copy templates
|
|
150
|
+
const terraformDir = path.join(projectRoot, 'terraform');
|
|
151
|
+
fs.mkdirSync(terraformDir, { recursive: true });
|
|
152
|
+
|
|
153
|
+
const srcTerraformDir = path.join(templatesDir, isProductionTier ? 'terraform' : 'terraform-ec2');
|
|
154
|
+
if (fs.existsSync(srcTerraformDir)) {
|
|
155
|
+
const files = fs.readdirSync(srcTerraformDir);
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
const filePath = path.join(srcTerraformDir, file);
|
|
158
|
+
if (fs.statSync(filePath).isFile() && file !== 'bootstrap-oidc.yaml') {
|
|
159
|
+
fs.copyFileSync(filePath, path.join(terraformDir, file));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log(' ✅ Created Terraform modules in /terraform');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 4. Write terraform.tfvars
|
|
166
|
+
const tfvarsContent = `aws_region = "${awsRegion}"
|
|
167
|
+
app_name = "${appName}"
|
|
168
|
+
container_port = ${detected.port}
|
|
169
|
+
enable_database = ${enableDatabase}
|
|
170
|
+
enable_redis = ${enableRedis}
|
|
171
|
+
enable_rds_proxy = ${enableRdsProxy}
|
|
172
|
+
billing_email = "${billingEmail}"
|
|
173
|
+
enable_custom_domain = ${enableCustomDomain}
|
|
174
|
+
domain_name = "${domainName}"
|
|
175
|
+
dns_provider = "${dnsProvider}"
|
|
176
|
+
sentry_dsn = "${sentryDsn}"
|
|
177
|
+
`;
|
|
178
|
+
fs.writeFileSync(path.join(terraformDir, 'terraform.tfvars'), tfvarsContent, 'utf8');
|
|
179
|
+
console.log(' ✅ Created terraform/terraform.tfvars');
|
|
180
|
+
|
|
181
|
+
// 5. Copy AGENTS.md
|
|
182
|
+
let srcAgentsMd = '';
|
|
183
|
+
const agentsPathsToCheck = [
|
|
184
|
+
path.join(__dirname, '../../AGENTS.md'),
|
|
185
|
+
path.join(__dirname, '../../../AGENTS.md'),
|
|
186
|
+
path.join(__dirname, '../../../../AGENTS.md'),
|
|
187
|
+
path.join(__dirname, '../../../../../AGENTS.md'),
|
|
188
|
+
];
|
|
189
|
+
for (const p of agentsPathsToCheck) {
|
|
190
|
+
if (fs.existsSync(p)) {
|
|
191
|
+
srcAgentsMd = p;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const destAgentsMd = path.join(projectRoot, 'AGENTS.md');
|
|
196
|
+
if (fs.existsSync(srcAgentsMd)) {
|
|
197
|
+
fs.copyFileSync(srcAgentsMd, destAgentsMd);
|
|
198
|
+
console.log(' ✅ Created AGENTS.md');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 6. Write project configuration mysystem.json
|
|
202
|
+
const config = {
|
|
203
|
+
name: appName,
|
|
204
|
+
region: awsRegion,
|
|
205
|
+
port: detected.port,
|
|
206
|
+
tier: isProductionTier ? 'production' : 'hobbyist',
|
|
207
|
+
database: enableDatabase,
|
|
208
|
+
redis: enableRedis,
|
|
209
|
+
rdsProxy: enableRdsProxy,
|
|
210
|
+
billingEmail: billingEmail,
|
|
211
|
+
customDomain: enableCustomDomain,
|
|
212
|
+
domainName: domainName,
|
|
213
|
+
dnsProvider: dnsProvider,
|
|
214
|
+
sentryDsn: sentryDsn,
|
|
215
|
+
type: detected.type,
|
|
216
|
+
initializedAt: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
fs.writeFileSync(path.join(projectRoot, 'mysystem.json'), JSON.stringify(config, null, 2), 'utf8');
|
|
219
|
+
console.log(' ✅ Created mysystem.json');
|
|
220
|
+
|
|
221
|
+
// Output Setup Guidance
|
|
222
|
+
const cfUrl = `https://console.aws.amazon.com/cloudformation/home?region=${awsRegion}#/stacks/create/review?templateURL=https://raw.githubusercontent.com/ai-production-standard/mysystem/main/templates/terraform/bootstrap-oidc.yaml&stackName=mysystem-oidc-${appName}`;
|
|
223
|
+
|
|
224
|
+
console.log('\n\x1b[32m✨ MySystem deployment files initialized successfully!\x1b[0m\n');
|
|
225
|
+
console.log('\x1b[1m🚀 NEXT STEPS TO DEPLOY:\x1b[0m\n');
|
|
226
|
+
console.log('\x1b[33mStep 1: Connect AWS to GitHub (One-Time Setup)\x1b[0m');
|
|
227
|
+
console.log('--------------------------------------------');
|
|
228
|
+
console.log('Click this link to configure a secure OIDC Trust stack in AWS:');
|
|
229
|
+
console.log(`\x1b[36m${cfUrl}\x1b[0m\n`);
|
|
230
|
+
console.log('Fill in parameters:');
|
|
231
|
+
console.log(' - GitHubOrg: Your GitHub Org or username (case-sensitive)');
|
|
232
|
+
console.log(' - GitHubRepo: Your GitHub repository name (case-sensitive)');
|
|
233
|
+
console.log('\nClick "Create Stack". Once complete, copy the "RoleARN" from outputs.');
|
|
234
|
+
|
|
235
|
+
console.log('\n\x1b[33mStep 2: Save Role ARN as a GitHub Secret\x1b[0m');
|
|
236
|
+
console.log('----------------------------------------');
|
|
237
|
+
console.log('Go to your GitHub repository -> Settings -> Secrets and variables -> Actions.');
|
|
238
|
+
console.log('Add a new secret:');
|
|
239
|
+
console.log(' - Name: \x1b[1mAWS_ROLE_ARN\x1b[0m');
|
|
240
|
+
console.log(' - Value: \x1b[32m<copied-role-arn>\x1b[0m');
|
|
241
|
+
|
|
242
|
+
console.log('\n\x1b[33mStep 3: Tell your AI coding agent to push and deploy!\x1b[0m');
|
|
243
|
+
console.log('----------------------------------------------------');
|
|
244
|
+
console.log('Simply type in your AI chat:');
|
|
245
|
+
console.log(' \x1b[32m"I have set up the AWS secrets. Push changes to deploy."\x1b[0m');
|
|
246
|
+
console.log('\nThe agent will commit, push, and initiate the GitHub Actions pipeline.\n');
|
|
247
|
+
|
|
248
|
+
} catch (e: any) {
|
|
249
|
+
rl.close();
|
|
250
|
+
console.error(`\x1b[31mError during initialization: ${e.message}\x1b[0m`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
import { ensureAwsCli } from '../utils/installer';
|
|
6
|
+
|
|
7
|
+
export async function runLogs(projectRoot: string) {
|
|
8
|
+
const configPath = path.join(projectRoot, 'mysystem.json');
|
|
9
|
+
if (!fs.existsSync(configPath)) {
|
|
10
|
+
console.error('\x1b[31mError: MySystem is not initialized in this directory.\x1b[0m');
|
|
11
|
+
console.error('Run `npx mysystem init` first.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
16
|
+
const logGroupName = `/ecs/${config.name}`;
|
|
17
|
+
const region = config.region;
|
|
18
|
+
|
|
19
|
+
// 1. Ensure AWS CLI is installed
|
|
20
|
+
const hasAwsCli = await ensureAwsCli();
|
|
21
|
+
if (!hasAwsCli) {
|
|
22
|
+
console.log('\nAlternatively, you can view logs in your browser via the AWS CloudWatch Console:');
|
|
23
|
+
console.log(`👉 \x1b[36mhttps://console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:log-groups/log-group/%252Fecs%252F${config.name}\x1b[0m\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`\n☁️ Streaming logs for \x1b[36m${config.name}\x1b[0m [Log Group: ${logGroupName}] in \x1b[32m${region}\x1b[0m...`);
|
|
28
|
+
console.log('Press \x1b[33mCtrl+C\x1b[0m to stop streaming.\n');
|
|
29
|
+
|
|
30
|
+
// Spawn AWS CLI log tailing command
|
|
31
|
+
const awsLog = spawn('aws', ['logs', 'tail', logGroupName, '--follow', '--region', region], {
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
shell: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
awsLog.on('close', (code) => {
|
|
37
|
+
if (code !== 0 && code !== null) {
|
|
38
|
+
console.error(`\n\x1b[31m❌ AWS CLI log tailing exited with code ${code}.\x1b[0m`);
|
|
39
|
+
console.log('Make sure your AWS credentials are configured locally by running `aws configure`.');
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Handle Ctrl+C gracefully
|
|
44
|
+
process.on('SIGINT', () => {
|
|
45
|
+
awsLog.kill();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runInit } from './commands/init';
|
|
4
|
+
import { runAudit } from './commands/audit';
|
|
5
|
+
import { runDestroy } from './commands/destroy';
|
|
6
|
+
import { runLogs } from './commands/logs';
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const command = args[0];
|
|
10
|
+
|
|
11
|
+
function printHelp() {
|
|
12
|
+
console.log(`
|
|
13
|
+
\x1b[1mMySystem CLI - Production Deployment for Vibecoders\x1b[0m
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx mysystem-cli <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
\x1b[36minit\x1b[0m Initialize AWS Terraform configs, Dockerfiles, and GitHub workflows.
|
|
20
|
+
\x1b[36maudit\x1b[0m Audit local files for production-readiness, security, and compliance.
|
|
21
|
+
\x1b[36mlogs\x1b[0m Stream container logs from AWS CloudWatch directly to your terminal.
|
|
22
|
+
\x1b[36mdestroy\x1b[0m Teardown all AWS infrastructure resources for this application.
|
|
23
|
+
\x1b[36mhelp\x1b[0m Print this help menu.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
$ npx mysystem-cli init
|
|
27
|
+
$ npx mysystem-cli audit
|
|
28
|
+
$ npx mysystem-cli logs
|
|
29
|
+
$ npx mysystem-cli destroy
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function main() {
|
|
34
|
+
switch (command) {
|
|
35
|
+
case 'init':
|
|
36
|
+
await runInit(process.cwd());
|
|
37
|
+
break;
|
|
38
|
+
case 'audit':
|
|
39
|
+
runAudit(process.cwd());
|
|
40
|
+
break;
|
|
41
|
+
case 'logs':
|
|
42
|
+
await runLogs(process.cwd());
|
|
43
|
+
break;
|
|
44
|
+
case 'destroy':
|
|
45
|
+
await runDestroy(process.cwd());
|
|
46
|
+
break;
|
|
47
|
+
case 'help':
|
|
48
|
+
case '--help':
|
|
49
|
+
case '-h':
|
|
50
|
+
printHelp();
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
if (!command) {
|
|
54
|
+
printHelp();
|
|
55
|
+
} else {
|
|
56
|
+
console.error(`\x1b[31mUnknown command: ${command}\x1b[0m`);
|
|
57
|
+
printHelp();
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main().catch(err => {
|
|
64
|
+
console.error('\x1b[31mFatal error occurred:\x1b[0m', err);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ProjectInfo {
|
|
5
|
+
type: 'nextjs' | 'react-vite' | 'node' | 'fastapi' | 'unknown';
|
|
6
|
+
port: number;
|
|
7
|
+
hasDatabase: boolean;
|
|
8
|
+
hasRedis: boolean;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function detectProject(projectRoot: string): ProjectInfo {
|
|
13
|
+
const info: ProjectInfo = {
|
|
14
|
+
type: 'unknown',
|
|
15
|
+
port: 3000,
|
|
16
|
+
hasDatabase: false,
|
|
17
|
+
hasRedis: false,
|
|
18
|
+
name: path.basename(projectRoot) || 'mysystem-app',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// 1. Read package.json if it exists
|
|
22
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
23
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
26
|
+
if (packageJson.name) {
|
|
27
|
+
info.name = packageJson.name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const deps = {
|
|
31
|
+
...packageJson.dependencies,
|
|
32
|
+
...packageJson.devDependencies,
|
|
33
|
+
};
|
|
34
|
+
|
|
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))) {
|
|
38
|
+
info.hasDatabase = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Detect Redis dependencies
|
|
42
|
+
const redisDeps = ['redis', 'ioredis', 'bull', 'bullmq', 'handy-redis', 'keyv'];
|
|
43
|
+
if (Object.keys(deps).some(dep => redisDeps.includes(dep))) {
|
|
44
|
+
info.hasRedis = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
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;
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Ignore JSON parse errors and continue
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Read requirements.txt or main.py if Python
|
|
68
|
+
const reqTxtPath = path.join(projectRoot, 'requirements.txt');
|
|
69
|
+
const mainPyPath = path.join(projectRoot, 'main.py');
|
|
70
|
+
if (fs.existsSync(reqTxtPath) || fs.existsSync(mainPyPath)) {
|
|
71
|
+
info.type = 'fastapi';
|
|
72
|
+
info.port = 8000;
|
|
73
|
+
|
|
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))) {
|
|
78
|
+
info.hasDatabase = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const redisTerms = ['redis', 'django-redis', 'celery'];
|
|
82
|
+
if (redisTerms.some(term => reqs.toLowerCase().includes(term))) {
|
|
83
|
+
info.hasRedis = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return info;
|
|
89
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'child_process';
|
|
2
|
+
import * as readline from 'readline/promises';
|
|
3
|
+
|
|
4
|
+
export async function ensureAwsCli(): Promise<boolean> {
|
|
5
|
+
// 1. Check if AWS CLI is already installed
|
|
6
|
+
try {
|
|
7
|
+
execSync('aws --version', { stdio: 'ignore' });
|
|
8
|
+
return true;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
// AWS CLI not installed
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log('\n\x1b[33m⚠️ AWS CLI is not installed on your system.\x1b[0m');
|
|
14
|
+
console.log('The AWS CLI is required to stream container logs and manage local credentials.');
|
|
15
|
+
|
|
16
|
+
const rl = readline.createInterface({
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const answer = await rl.question('\nWould you like MySystem to install the AWS CLI automatically? (y/n) [y]: ');
|
|
22
|
+
rl.close();
|
|
23
|
+
|
|
24
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const platform = process.platform;
|
|
29
|
+
console.log(`\n⚙️ Installing AWS CLI for \x1b[36m${platform}\x1b[0m...`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (platform === 'win32') {
|
|
33
|
+
// Windows: Use winget (native Windows Package Manager)
|
|
34
|
+
console.log('Running winget installer...');
|
|
35
|
+
const res = spawnSync('winget', ['install', '--id', 'Amazon.AWSCLI', '--silent', '--accept-source-agreements', '--accept-package-agreements'], {
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
shell: true,
|
|
38
|
+
});
|
|
39
|
+
if (res.status === 0) {
|
|
40
|
+
console.log('\x1b[32m✅ AWS CLI installed successfully! You may need to restart your terminal for changes to take effect.\x1b[0m');
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
} else if (platform === 'darwin') {
|
|
44
|
+
// macOS: Try homebrew first
|
|
45
|
+
let hasBrew = false;
|
|
46
|
+
try {
|
|
47
|
+
execSync('brew --version', { stdio: 'ignore' });
|
|
48
|
+
hasBrew = true;
|
|
49
|
+
} catch (e) {}
|
|
50
|
+
|
|
51
|
+
if (hasBrew) {
|
|
52
|
+
console.log('Running: brew install awscli...');
|
|
53
|
+
const res = spawnSync('brew', ['install', 'awscli'], { stdio: 'inherit' });
|
|
54
|
+
if (res.status === 0) {
|
|
55
|
+
console.log('\x1b[32m✅ AWS CLI installed successfully via Homebrew!\x1b[0m');
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Fallback: Download pkg installer
|
|
60
|
+
console.log('Downloading AWS CLI macOS package...');
|
|
61
|
+
execSync('curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "/tmp/AWSCLIV2.pkg"', { stdio: 'inherit' });
|
|
62
|
+
console.log('Installing package (requires sudo privileges)...');
|
|
63
|
+
const res = spawnSync('sudo', ['installer', '-pkg', '/tmp/AWSCLIV2.pkg', '-target', '/'], { stdio: 'inherit' });
|
|
64
|
+
if (res.status === 0) {
|
|
65
|
+
console.log('\x1b[32m✅ AWS CLI installed successfully!\x1b[0m');
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else if (platform === 'linux') {
|
|
70
|
+
// Linux install
|
|
71
|
+
console.log('Downloading AWS CLI Linux package...');
|
|
72
|
+
execSync('curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"', { stdio: 'inherit' });
|
|
73
|
+
execSync('unzip -q /tmp/awscliv2.zip -d /tmp', { stdio: 'inherit' });
|
|
74
|
+
console.log('Installing package (requires sudo privileges)...');
|
|
75
|
+
const res = spawnSync('sudo', ['/tmp/aws/install', '--update'], { stdio: 'inherit' });
|
|
76
|
+
if (res.status === 0) {
|
|
77
|
+
console.log('\x1b[32m✅ AWS CLI installed successfully!\x1b[0m');
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (err: any) {
|
|
82
|
+
console.error(`\x1b[31mInstallation failed: ${err.message}\x1b[0m`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('\n\x1b[31m❌ Automatic installation failed.\x1b[0m');
|
|
86
|
+
console.log('Please install the AWS CLI manually:');
|
|
87
|
+
console.log('👉 \x1b[36mhttps://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html\x1b[0m\n');
|
|
88
|
+
return false;
|
|
89
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|