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 @@
|
|
|
1
|
+
export declare function runAudit(projectRoot: string): void;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runAudit = runAudit;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
function runAudit(projectRoot) {
|
|
40
|
+
console.log('\n🔍 Auditing project for production readiness...\n');
|
|
41
|
+
const items = [];
|
|
42
|
+
// 1. Check Dockerfile
|
|
43
|
+
const dockerfilePath = path.join(projectRoot, 'Dockerfile');
|
|
44
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
45
|
+
items.push({
|
|
46
|
+
name: 'Dockerfile Exists',
|
|
47
|
+
passed: false,
|
|
48
|
+
type: 'error',
|
|
49
|
+
message: 'No Dockerfile found. Run `npx mysystem init` to generate one.',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
items.push({
|
|
54
|
+
name: 'Dockerfile Exists',
|
|
55
|
+
passed: true,
|
|
56
|
+
type: 'error',
|
|
57
|
+
message: 'Dockerfile found.',
|
|
58
|
+
});
|
|
59
|
+
const dockerfileContent = fs.readFileSync(dockerfilePath, 'utf8');
|
|
60
|
+
// Check for non-root USER
|
|
61
|
+
const hasUser = dockerfileContent.includes('USER ');
|
|
62
|
+
items.push({
|
|
63
|
+
name: 'Secure Container User (Non-Root)',
|
|
64
|
+
passed: hasUser,
|
|
65
|
+
type: 'error',
|
|
66
|
+
message: hasUser
|
|
67
|
+
? 'Dockerfile specifies a non-root USER.'
|
|
68
|
+
: 'Dockerfile executes as root. Add a USER instruction for security container hardening.',
|
|
69
|
+
});
|
|
70
|
+
// Check for healthcheck
|
|
71
|
+
const hasHealthcheck = dockerfileContent.includes('HEALTHCHECK ');
|
|
72
|
+
items.push({
|
|
73
|
+
name: 'Container Health Check',
|
|
74
|
+
passed: hasHealthcheck,
|
|
75
|
+
type: 'warning',
|
|
76
|
+
message: hasHealthcheck
|
|
77
|
+
? 'Dockerfile specifies a HEALTHCHECK instruction.'
|
|
78
|
+
: 'No HEALTHCHECK found in Dockerfile. ECS needs health checks to detect failing tasks.',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// 2. Check CI/CD Workflows
|
|
82
|
+
const workflowPath = path.join(projectRoot, '.github', 'workflows', 'mysystem-deploy.yml');
|
|
83
|
+
const hasWorkflow = fs.existsSync(workflowPath);
|
|
84
|
+
items.push({
|
|
85
|
+
name: 'GitHub Actions CI/CD Pipeline',
|
|
86
|
+
passed: hasWorkflow,
|
|
87
|
+
type: 'error',
|
|
88
|
+
message: hasWorkflow
|
|
89
|
+
? 'GitHub Actions deploy workflow configured.'
|
|
90
|
+
: 'Deployment workflow missing. Make sure `.github/workflows/mysystem-deploy.yml` exists.',
|
|
91
|
+
});
|
|
92
|
+
// 3. Check Terraform Infrastructure as Code
|
|
93
|
+
const terraformPath = path.join(projectRoot, 'terraform');
|
|
94
|
+
const hasTerraform = fs.existsSync(terraformPath) && fs.readdirSync(terraformPath).some(file => file.endsWith('.tf'));
|
|
95
|
+
items.push({
|
|
96
|
+
name: 'Infrastructure as Code (Terraform)',
|
|
97
|
+
passed: hasTerraform,
|
|
98
|
+
type: 'error',
|
|
99
|
+
message: hasTerraform
|
|
100
|
+
? 'Terraform modules found in `/terraform` directory.'
|
|
101
|
+
: 'Terraform configuration files not found in `/terraform`. Run `npx mysystem init`.',
|
|
102
|
+
});
|
|
103
|
+
// 4. Check AGENTS.md rulebook
|
|
104
|
+
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
105
|
+
const hasAgents = fs.existsSync(agentsPath);
|
|
106
|
+
items.push({
|
|
107
|
+
name: 'AI Agent Guidelines (AGENTS.md)',
|
|
108
|
+
passed: hasAgents,
|
|
109
|
+
type: 'warning',
|
|
110
|
+
message: hasAgents
|
|
111
|
+
? 'AGENTS.md rules file configured in root.'
|
|
112
|
+
: 'No AGENTS.md rules found. AI agents won\'t have constraints for production-readiness.',
|
|
113
|
+
});
|
|
114
|
+
// 5. Secret Scanning (Simple checks for common API Keys/Credentials)
|
|
115
|
+
let foundSecrets = false;
|
|
116
|
+
const filesToScan = scanDirForCodeFiles(projectRoot);
|
|
117
|
+
for (const file of filesToScan) {
|
|
118
|
+
// Skip node_modules, git, terraform, dist, etc.
|
|
119
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
120
|
+
const hasSmdPattern = /aws_access_key_id\s*=\s*['"][A-Z0-9]{20}['"]/gi.test(content) ||
|
|
121
|
+
/aws_secret_access_key\s*=\s*['"][A-Za-z0-9/+=]{40}['"]/gi.test(content) ||
|
|
122
|
+
/db_password\s*=\s*['"][^'"]{6,}['"]/gi.test(content);
|
|
123
|
+
if (hasSmdPattern) {
|
|
124
|
+
foundSecrets = true;
|
|
125
|
+
console.log(`\x1b[31m⚠️ Potential secret/credential leak in file: ${path.relative(projectRoot, file)}\x1b[0m`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
items.push({
|
|
129
|
+
name: 'No Hardcoded Secrets',
|
|
130
|
+
passed: !foundSecrets,
|
|
131
|
+
type: 'error',
|
|
132
|
+
message: !foundSecrets
|
|
133
|
+
? 'No plain-text credentials found in repository files.'
|
|
134
|
+
: 'Potential plain-text credentials detected in codebase. Clean secrets and use environment variables.',
|
|
135
|
+
});
|
|
136
|
+
// Output Audit Dashboard
|
|
137
|
+
console.log('----------------------------------------------------');
|
|
138
|
+
console.log('\x1b[1mAUDIT REPORT RESULTS\x1b[0m');
|
|
139
|
+
console.log('----------------------------------------------------');
|
|
140
|
+
let passedCount = 0;
|
|
141
|
+
let errorCount = 0;
|
|
142
|
+
let warningCount = 0;
|
|
143
|
+
for (const item of items) {
|
|
144
|
+
if (item.passed) {
|
|
145
|
+
console.log(` ✅ \x1b[32m[PASS]\x1b[0m \x1b[1m${item.name}\x1b[0m`);
|
|
146
|
+
console.log(` ${item.message}`);
|
|
147
|
+
passedCount++;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const color = item.type === 'error' ? '\x1b[31m[FAIL]\x1b[0m' : '\x1b[33m[WARN]\x1b[0m';
|
|
151
|
+
console.log(` ❌ ${color} \x1b[1m${item.name}\x1b[0m`);
|
|
152
|
+
console.log(` ${item.message}`);
|
|
153
|
+
if (item.type === 'error')
|
|
154
|
+
errorCount++;
|
|
155
|
+
else
|
|
156
|
+
warningCount++;
|
|
157
|
+
}
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
|
160
|
+
const score = Math.round((passedCount / items.length) * 100);
|
|
161
|
+
console.log('----------------------------------------------------');
|
|
162
|
+
console.log(`Readiness Score: \x1b[1m${score === 100 ? '\x1b[32m' : score >= 70 ? '\x1b[33m' : '\x1b[31m'}${score}%\x1b[0m`);
|
|
163
|
+
console.log(`Summary: ${passedCount} passed | ${errorCount} errors | ${warningCount} warnings`);
|
|
164
|
+
console.log('----------------------------------------------------');
|
|
165
|
+
if (errorCount > 0) {
|
|
166
|
+
console.log('\x1b[31m❌ Fix all errors before deploying to production.\x1b[0m\n');
|
|
167
|
+
}
|
|
168
|
+
else if (warningCount > 0) {
|
|
169
|
+
console.log('\x1b[33m⚠️ Resolving warnings is recommended for full production readiness.\x1b[0m\n');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log('\x1b[32m🚀 Your application is 100% production ready! Ready to deploy to AWS.\x1b[0m\n');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function scanDirForCodeFiles(dir, fileList = []) {
|
|
176
|
+
const files = fs.readdirSync(dir);
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
const filePath = path.join(dir, file);
|
|
179
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
180
|
+
// Exclude build, node, git, terraform folders
|
|
181
|
+
if (['node_modules', '.git', 'terraform', 'dist', 'build', '.next', 'out'].includes(file)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
scanDirForCodeFiles(filePath, fileList);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Only scan code files
|
|
188
|
+
if (/\.(js|ts|tsx|jsx|json|py|env|tfvars)$/.test(file) && file !== 'package-lock.json') {
|
|
189
|
+
fileList.push(filePath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return fileList;
|
|
194
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDestroy(projectRoot: string): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runDestroy = runDestroy;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const readline = __importStar(require("readline/promises"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
async function runDestroy(projectRoot) {
|
|
42
|
+
const configPath = path.join(projectRoot, 'mysystem.json');
|
|
43
|
+
if (!fs.existsSync(configPath)) {
|
|
44
|
+
console.error('\x1b[31mError: MySystem is not initialized in this directory.\x1b[0m');
|
|
45
|
+
console.error('Run `npx mysystem init` first.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
49
|
+
console.log(`\n\x1b[31m⚠️ WARNING: You are about to DESTROY all AWS infrastructure for "${config.name}".\x1b[0m`);
|
|
50
|
+
console.log('This will delete the database (RDS), cache (Redis), server (Fargate), and load balancers.');
|
|
51
|
+
console.log('\x1b[1mALL DATA WILL BE PERMANENTLY LOST.\x1b[0m\n');
|
|
52
|
+
const rl = readline.createInterface({
|
|
53
|
+
input: process.stdin,
|
|
54
|
+
output: process.stdout,
|
|
55
|
+
});
|
|
56
|
+
const confirm = await rl.question('Are you absolutely sure? Type the application name to confirm: ');
|
|
57
|
+
rl.close();
|
|
58
|
+
if (confirm.trim() !== config.name) {
|
|
59
|
+
console.log('\n❌ Confirmation failed. Destruction cancelled.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log('\n🔥 Initiating infrastructure destruction. Please wait...');
|
|
63
|
+
const tfDir = path.join(projectRoot, 'terraform');
|
|
64
|
+
if (!fs.existsSync(tfDir)) {
|
|
65
|
+
console.error('\x1b[31mError: terraform directory not found.\x1b[0m');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Execute terraform destroy
|
|
69
|
+
// Set stdio: 'inherit' to stream Terraform output directly to user's terminal
|
|
70
|
+
const tf = (0, child_process_1.spawn)('terraform', ['destroy', '-auto-approve'], {
|
|
71
|
+
cwd: tfDir,
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
shell: true,
|
|
74
|
+
});
|
|
75
|
+
tf.on('close', (code) => {
|
|
76
|
+
if (code === 0) {
|
|
77
|
+
console.log('\n\x1b[32m✅ Successfully destroyed all AWS resources for this application.\x1b[0m');
|
|
78
|
+
console.log('Your AWS billing for this project has been stopped.');
|
|
79
|
+
// Remove local generated files if they want, or keep configuration
|
|
80
|
+
try {
|
|
81
|
+
if (fs.existsSync(path.join(projectRoot, 'mysystem.json'))) {
|
|
82
|
+
fs.unlinkSync(path.join(projectRoot, 'mysystem.json'));
|
|
83
|
+
}
|
|
84
|
+
console.log('Deleted local mysystem.json configuration.');
|
|
85
|
+
}
|
|
86
|
+
catch (e) { }
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.error(`\n\x1b[31m❌ Terraform destroy failed with exit code ${code}.\x1b[0m`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runInit(projectRoot: string): Promise<void>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runInit = runInit;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const readline = __importStar(require("readline/promises"));
|
|
40
|
+
const detector_1 = require("../utils/detector");
|
|
41
|
+
async function runInit(projectRoot) {
|
|
42
|
+
console.log('\n🔍 Scanning project codebase...');
|
|
43
|
+
const detected = (0, detector_1.detectProject)(projectRoot);
|
|
44
|
+
console.log(`Detected application type: \x1b[36m${detected.type}\x1b[0m`);
|
|
45
|
+
console.log(`Default container port: \x1b[36m${detected.port}\x1b[0m`);
|
|
46
|
+
console.log(`Requires database: \x1b[36m${detected.hasDatabase ? 'Yes' : 'No'}\x1b[0m`);
|
|
47
|
+
console.log(`Requires Redis: \x1b[36m${detected.hasRedis ? 'Yes' : 'No'}\x1b[0m\n`);
|
|
48
|
+
// Prompt the user for overrides using native readline
|
|
49
|
+
const rl = readline.createInterface({
|
|
50
|
+
input: process.stdin,
|
|
51
|
+
output: process.stdout,
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const appNameInput = await rl.question(`Enter application name [${detected.name}]: `);
|
|
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';
|
|
58
|
+
console.log('Select your AWS hosting tier:');
|
|
59
|
+
console.log(' \x1b[36m1. Production\x1b[0m [ECS Fargate + RDS + ALB + WAF] (~$17/mo free-tier, ~$51/mo standard)');
|
|
60
|
+
console.log(' \x1b[36m2. Hobbyist\x1b[0m [Single EC2 + Docker Compose + Postgres] ($0/mo free-tier, ~$3.20/mo standard)');
|
|
61
|
+
const tierInput = await rl.question('Choose tier [1]: ');
|
|
62
|
+
const isProductionTier = tierInput.trim() !== '2';
|
|
63
|
+
let enableDatabase = detected.hasDatabase;
|
|
64
|
+
let enableRedis = detected.hasRedis;
|
|
65
|
+
let enableRdsProxy = false;
|
|
66
|
+
if (isProductionTier) {
|
|
67
|
+
const enableDbInput = await rl.question(`Enable RDS PostgreSQL database? (y/n) [${detected.hasDatabase ? 'y' : 'n'}]: `);
|
|
68
|
+
enableDatabase = enableDbInput.trim() ? enableDbInput.trim().toLowerCase() === 'y' : detected.hasDatabase;
|
|
69
|
+
const enableRedisInput = await rl.question(`Enable ElastiCache Redis? (y/n) [${detected.hasRedis ? 'y' : 'n'}]: `);
|
|
70
|
+
enableRedis = enableRedisInput.trim() ? enableRedisInput.trim().toLowerCase() === 'y' : detected.hasRedis;
|
|
71
|
+
if (enableDatabase) {
|
|
72
|
+
const enableProxyInput = await rl.question(`Enable RDS Proxy (PgBouncer connection pooler)? (y/n) [n]: `);
|
|
73
|
+
enableRdsProxy = enableProxyInput.trim().toLowerCase() === 'y';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Hobby tier runs Postgres inside Docker Compose on the same instance (flat cost)
|
|
78
|
+
enableDatabase = true;
|
|
79
|
+
enableRedis = false;
|
|
80
|
+
enableRdsProxy = false;
|
|
81
|
+
}
|
|
82
|
+
const billingEmailInput = await rl.question(`Enter email for AWS budget alerts (press Enter to skip): `);
|
|
83
|
+
const billingEmail = billingEmailInput.trim();
|
|
84
|
+
const customDomainInput = await rl.question(`Enable custom domain & HTTPS SSL certificate? (y/n) [n]: `);
|
|
85
|
+
const enableCustomDomain = customDomainInput.trim().toLowerCase() === 'y';
|
|
86
|
+
let domainName = '';
|
|
87
|
+
let dnsProvider = 'external';
|
|
88
|
+
if (enableCustomDomain) {
|
|
89
|
+
const domainInput = await rl.question(`Enter custom domain (e.g., app.myproduct.com): `);
|
|
90
|
+
domainName = domainInput.trim();
|
|
91
|
+
const providerInput = await rl.question(`Is your domain DNS managed on AWS Route 53? (y/n) [n]: `);
|
|
92
|
+
dnsProvider = providerInput.trim().toLowerCase() === 'y' ? 'route53' : 'external';
|
|
93
|
+
}
|
|
94
|
+
const enableSentryInput = await rl.question(`Enable Sentry Error Tracking? (y/n) [n]: `);
|
|
95
|
+
const enableSentry = enableSentryInput.trim().toLowerCase() === 'y';
|
|
96
|
+
let sentryDsn = '';
|
|
97
|
+
if (enableSentry) {
|
|
98
|
+
const dsnInput = await rl.question(`Enter Sentry DSN (press Enter to skip and configure later): `);
|
|
99
|
+
sentryDsn = dsnInput.trim();
|
|
100
|
+
}
|
|
101
|
+
rl.close();
|
|
102
|
+
console.log('\n🚀 Generating deployment assets...');
|
|
103
|
+
// Find templates directory relative to CLI source or dist
|
|
104
|
+
let templatesDir = '';
|
|
105
|
+
const pathsToCheck = [
|
|
106
|
+
path.join(__dirname, '../../templates'), // Packaged location relative to dist/commands/
|
|
107
|
+
path.join(__dirname, '../../../templates'), // Local dev relative to src/commands/
|
|
108
|
+
path.join(__dirname, '../../../../templates'), // Local dev relative to packages/cli/dist/commands
|
|
109
|
+
path.join(__dirname, '../../../../../templates'), // Fallback
|
|
110
|
+
];
|
|
111
|
+
for (const p of pathsToCheck) {
|
|
112
|
+
if (fs.existsSync(p)) {
|
|
113
|
+
templatesDir = p;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!fs.existsSync(templatesDir)) {
|
|
118
|
+
throw new Error(`Templates directory not found. Looked in: ${templatesDir}`);
|
|
119
|
+
}
|
|
120
|
+
// 1. Copy Dockerfile
|
|
121
|
+
let dockerfileTemplate = '';
|
|
122
|
+
switch (detected.type) {
|
|
123
|
+
case 'nextjs':
|
|
124
|
+
dockerfileTemplate = 'nextjs.Dockerfile';
|
|
125
|
+
break;
|
|
126
|
+
case 'react-vite':
|
|
127
|
+
dockerfileTemplate = 'react.Dockerfile';
|
|
128
|
+
break;
|
|
129
|
+
case 'fastapi':
|
|
130
|
+
dockerfileTemplate = 'fastapi.Dockerfile';
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
dockerfileTemplate = 'node.Dockerfile';
|
|
134
|
+
}
|
|
135
|
+
const srcDockerfile = path.join(templatesDir, 'docker', dockerfileTemplate);
|
|
136
|
+
const destDockerfile = path.join(projectRoot, 'Dockerfile');
|
|
137
|
+
if (fs.existsSync(srcDockerfile)) {
|
|
138
|
+
fs.copyFileSync(srcDockerfile, destDockerfile);
|
|
139
|
+
console.log(' ✅ Created Dockerfile');
|
|
140
|
+
}
|
|
141
|
+
// 2. Create GitHub Actions folder & copy deploy.yml and destroy.yml
|
|
142
|
+
const githubWorkflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
143
|
+
fs.mkdirSync(githubWorkflowDir, { recursive: true });
|
|
144
|
+
const srcDeployWorkflow = path.join(templatesDir, 'github', isProductionTier ? 'deploy.yml' : 'deploy-ec2.yml');
|
|
145
|
+
const destDeployWorkflow = path.join(githubWorkflowDir, 'mysystem-deploy.yml');
|
|
146
|
+
if (fs.existsSync(srcDeployWorkflow)) {
|
|
147
|
+
let workflowContent = fs.readFileSync(srcDeployWorkflow, 'utf8');
|
|
148
|
+
workflowContent = workflowContent.replace(/aws-region: us-east-1/g, `aws-region: ${awsRegion}`);
|
|
149
|
+
fs.writeFileSync(destDeployWorkflow, workflowContent, 'utf8');
|
|
150
|
+
console.log(' ✅ Created .github/workflows/mysystem-deploy.yml');
|
|
151
|
+
}
|
|
152
|
+
const srcDestroyWorkflow = path.join(templatesDir, 'github', 'destroy.yml');
|
|
153
|
+
const destDestroyWorkflow = path.join(githubWorkflowDir, 'mysystem-destroy.yml');
|
|
154
|
+
if (fs.existsSync(srcDestroyWorkflow)) {
|
|
155
|
+
let workflowContent = fs.readFileSync(srcDestroyWorkflow, 'utf8');
|
|
156
|
+
workflowContent = workflowContent.replace(/aws-region: us-east-1/g, `aws-region: ${awsRegion}`);
|
|
157
|
+
fs.writeFileSync(destDestroyWorkflow, workflowContent, 'utf8');
|
|
158
|
+
console.log(' ✅ Created .github/workflows/mysystem-destroy.yml');
|
|
159
|
+
}
|
|
160
|
+
// 3. Create Terraform directory & copy templates
|
|
161
|
+
const terraformDir = path.join(projectRoot, 'terraform');
|
|
162
|
+
fs.mkdirSync(terraformDir, { recursive: true });
|
|
163
|
+
const srcTerraformDir = path.join(templatesDir, isProductionTier ? 'terraform' : 'terraform-ec2');
|
|
164
|
+
if (fs.existsSync(srcTerraformDir)) {
|
|
165
|
+
const files = fs.readdirSync(srcTerraformDir);
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
const filePath = path.join(srcTerraformDir, file);
|
|
168
|
+
if (fs.statSync(filePath).isFile() && file !== 'bootstrap-oidc.yaml') {
|
|
169
|
+
fs.copyFileSync(filePath, path.join(terraformDir, file));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
console.log(' ✅ Created Terraform modules in /terraform');
|
|
173
|
+
}
|
|
174
|
+
// 4. Write terraform.tfvars
|
|
175
|
+
const tfvarsContent = `aws_region = "${awsRegion}"
|
|
176
|
+
app_name = "${appName}"
|
|
177
|
+
container_port = ${detected.port}
|
|
178
|
+
enable_database = ${enableDatabase}
|
|
179
|
+
enable_redis = ${enableRedis}
|
|
180
|
+
enable_rds_proxy = ${enableRdsProxy}
|
|
181
|
+
billing_email = "${billingEmail}"
|
|
182
|
+
enable_custom_domain = ${enableCustomDomain}
|
|
183
|
+
domain_name = "${domainName}"
|
|
184
|
+
dns_provider = "${dnsProvider}"
|
|
185
|
+
sentry_dsn = "${sentryDsn}"
|
|
186
|
+
`;
|
|
187
|
+
fs.writeFileSync(path.join(terraformDir, 'terraform.tfvars'), tfvarsContent, 'utf8');
|
|
188
|
+
console.log(' ✅ Created terraform/terraform.tfvars');
|
|
189
|
+
// 5. Copy AGENTS.md
|
|
190
|
+
let srcAgentsMd = '';
|
|
191
|
+
const agentsPathsToCheck = [
|
|
192
|
+
path.join(__dirname, '../../AGENTS.md'),
|
|
193
|
+
path.join(__dirname, '../../../AGENTS.md'),
|
|
194
|
+
path.join(__dirname, '../../../../AGENTS.md'),
|
|
195
|
+
path.join(__dirname, '../../../../../AGENTS.md'),
|
|
196
|
+
];
|
|
197
|
+
for (const p of agentsPathsToCheck) {
|
|
198
|
+
if (fs.existsSync(p)) {
|
|
199
|
+
srcAgentsMd = p;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const destAgentsMd = path.join(projectRoot, 'AGENTS.md');
|
|
204
|
+
if (fs.existsSync(srcAgentsMd)) {
|
|
205
|
+
fs.copyFileSync(srcAgentsMd, destAgentsMd);
|
|
206
|
+
console.log(' ✅ Created AGENTS.md');
|
|
207
|
+
}
|
|
208
|
+
// 6. Write project configuration mysystem.json
|
|
209
|
+
const config = {
|
|
210
|
+
name: appName,
|
|
211
|
+
region: awsRegion,
|
|
212
|
+
port: detected.port,
|
|
213
|
+
tier: isProductionTier ? 'production' : 'hobbyist',
|
|
214
|
+
database: enableDatabase,
|
|
215
|
+
redis: enableRedis,
|
|
216
|
+
rdsProxy: enableRdsProxy,
|
|
217
|
+
billingEmail: billingEmail,
|
|
218
|
+
customDomain: enableCustomDomain,
|
|
219
|
+
domainName: domainName,
|
|
220
|
+
dnsProvider: dnsProvider,
|
|
221
|
+
sentryDsn: sentryDsn,
|
|
222
|
+
type: detected.type,
|
|
223
|
+
initializedAt: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
fs.writeFileSync(path.join(projectRoot, 'mysystem.json'), JSON.stringify(config, null, 2), 'utf8');
|
|
226
|
+
console.log(' ✅ Created mysystem.json');
|
|
227
|
+
// Output Setup Guidance
|
|
228
|
+
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}`;
|
|
229
|
+
console.log('\n\x1b[32m✨ MySystem deployment files initialized successfully!\x1b[0m\n');
|
|
230
|
+
console.log('\x1b[1m🚀 NEXT STEPS TO DEPLOY:\x1b[0m\n');
|
|
231
|
+
console.log('\x1b[33mStep 1: Connect AWS to GitHub (One-Time Setup)\x1b[0m');
|
|
232
|
+
console.log('--------------------------------------------');
|
|
233
|
+
console.log('Click this link to configure a secure OIDC Trust stack in AWS:');
|
|
234
|
+
console.log(`\x1b[36m${cfUrl}\x1b[0m\n`);
|
|
235
|
+
console.log('Fill in parameters:');
|
|
236
|
+
console.log(' - GitHubOrg: Your GitHub Org or username (case-sensitive)');
|
|
237
|
+
console.log(' - GitHubRepo: Your GitHub repository name (case-sensitive)');
|
|
238
|
+
console.log('\nClick "Create Stack". Once complete, copy the "RoleARN" from outputs.');
|
|
239
|
+
console.log('\n\x1b[33mStep 2: Save Role ARN as a GitHub Secret\x1b[0m');
|
|
240
|
+
console.log('----------------------------------------');
|
|
241
|
+
console.log('Go to your GitHub repository -> Settings -> Secrets and variables -> Actions.');
|
|
242
|
+
console.log('Add a new secret:');
|
|
243
|
+
console.log(' - Name: \x1b[1mAWS_ROLE_ARN\x1b[0m');
|
|
244
|
+
console.log(' - Value: \x1b[32m<copied-role-arn>\x1b[0m');
|
|
245
|
+
console.log('\n\x1b[33mStep 3: Tell your AI coding agent to push and deploy!\x1b[0m');
|
|
246
|
+
console.log('----------------------------------------------------');
|
|
247
|
+
console.log('Simply type in your AI chat:');
|
|
248
|
+
console.log(' \x1b[32m"I have set up the AWS secrets. Push changes to deploy."\x1b[0m');
|
|
249
|
+
console.log('\nThe agent will commit, push, and initiate the GitHub Actions pipeline.\n');
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
rl.close();
|
|
253
|
+
console.error(`\x1b[31mError during initialization: ${e.message}\x1b[0m`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runLogs(projectRoot: string): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runLogs = runLogs;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const installer_1 = require("../utils/installer");
|
|
41
|
+
async function runLogs(projectRoot) {
|
|
42
|
+
const configPath = path.join(projectRoot, 'mysystem.json');
|
|
43
|
+
if (!fs.existsSync(configPath)) {
|
|
44
|
+
console.error('\x1b[31mError: MySystem is not initialized in this directory.\x1b[0m');
|
|
45
|
+
console.error('Run `npx mysystem init` first.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
49
|
+
const logGroupName = `/ecs/${config.name}`;
|
|
50
|
+
const region = config.region;
|
|
51
|
+
// 1. Ensure AWS CLI is installed
|
|
52
|
+
const hasAwsCli = await (0, installer_1.ensureAwsCli)();
|
|
53
|
+
if (!hasAwsCli) {
|
|
54
|
+
console.log('\nAlternatively, you can view logs in your browser via the AWS CloudWatch Console:');
|
|
55
|
+
console.log(`👉 \x1b[36mhttps://console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:log-groups/log-group/%252Fecs%252F${config.name}\x1b[0m\n`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
console.log(`\n☁️ Streaming logs for \x1b[36m${config.name}\x1b[0m [Log Group: ${logGroupName}] in \x1b[32m${region}\x1b[0m...`);
|
|
59
|
+
console.log('Press \x1b[33mCtrl+C\x1b[0m to stop streaming.\n');
|
|
60
|
+
// Spawn AWS CLI log tailing command
|
|
61
|
+
const awsLog = (0, child_process_1.spawn)('aws', ['logs', 'tail', logGroupName, '--follow', '--region', region], {
|
|
62
|
+
stdio: 'inherit',
|
|
63
|
+
shell: true,
|
|
64
|
+
});
|
|
65
|
+
awsLog.on('close', (code) => {
|
|
66
|
+
if (code !== 0 && code !== null) {
|
|
67
|
+
console.error(`\n\x1b[31m❌ AWS CLI log tailing exited with code ${code}.\x1b[0m`);
|
|
68
|
+
console.log('Make sure your AWS credentials are configured locally by running `aws configure`.');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// Handle Ctrl+C gracefully
|
|
72
|
+
process.on('SIGINT', () => {
|
|
73
|
+
awsLog.kill();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
});
|
|
76
|
+
}
|
package/dist/index.d.ts
ADDED