@yobekasbah/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/bin/kasbah 2.js +766 -0
- package/bin/kasbah.js +802 -0
- package/deploy.js +454 -0
- package/init-setup.js +264 -0
- package/package.json +10 -0
- package/validate-setup.js +230 -0
package/deploy.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kasbah Deployment Manager
|
|
4
|
+
* Integrates Terraform infrastructure with CLI/SDK/MCP
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* kasbah deploy init <cloud> [--region REGION] [--config FILE]
|
|
8
|
+
* kasbah deploy apply [--auto-approve]
|
|
9
|
+
* kasbah deploy status
|
|
10
|
+
* kasbah deploy endpoint
|
|
11
|
+
* kasbah deploy logs [--pod POD] [--tail N]
|
|
12
|
+
* kasbah deploy scale --replicas N
|
|
13
|
+
* kasbah deploy destroy [--force]
|
|
14
|
+
* kasbah deploy verify
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { execSync } = require('child_process');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
const KASBAH_HOME = path.join(os.homedir(), '.kasbah');
|
|
23
|
+
const TERRAFORM_DIR = path.join(process.cwd(), 'terraform');
|
|
24
|
+
const DEPLOYMENT_STATE_FILE = path.join(KASBAH_HOME, 'deployments.json');
|
|
25
|
+
const MCP_CONFIG_FILE = path.join(os.homedir(), '.mcp.json');
|
|
26
|
+
|
|
27
|
+
class DeploymentManager {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.state = this.loadState();
|
|
30
|
+
this.ensureDirectories();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
loadState() {
|
|
34
|
+
if (!fs.existsSync(DEPLOYMENT_STATE_FILE)) {
|
|
35
|
+
return { deployments: {}, current: null };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(DEPLOYMENT_STATE_FILE, 'utf8'));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { deployments: {}, current: null };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
saveState() {
|
|
45
|
+
fs.mkdirSync(KASBAH_HOME, { recursive: true });
|
|
46
|
+
fs.writeFileSync(DEPLOYMENT_STATE_FILE, JSON.stringify(this.state, null, 2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ensureDirectories() {
|
|
50
|
+
fs.mkdirSync(KASBAH_HOME, { recursive: true });
|
|
51
|
+
if (!fs.existsSync(TERRAFORM_DIR)) {
|
|
52
|
+
throw new Error(`Terraform directory not found: ${TERRAFORM_DIR}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async init(cloud, options = {}) {
|
|
57
|
+
console.log(`š Initializing Kasbah deployment on ${cloud.toUpperCase()}...`);
|
|
58
|
+
|
|
59
|
+
const deploymentName = options.name || `${cloud}-${Date.now()}`;
|
|
60
|
+
const region = options.region || this.getDefaultRegion(cloud);
|
|
61
|
+
const configFile = options.config || `${cloud}.tfvars`;
|
|
62
|
+
|
|
63
|
+
// Create tfvars file if not exists
|
|
64
|
+
const tfvarsPath = path.join(TERRAFORM_DIR, configFile);
|
|
65
|
+
if (!fs.existsSync(tfvarsPath)) {
|
|
66
|
+
const examplePath = path.join(TERRAFORM_DIR, `${cloud}.tfvars.example`);
|
|
67
|
+
if (!fs.existsSync(examplePath)) {
|
|
68
|
+
throw new Error(`No example config found: ${examplePath}`);
|
|
69
|
+
}
|
|
70
|
+
console.log(`š Creating ${configFile} from template...`);
|
|
71
|
+
let config = fs.readFileSync(examplePath, 'utf8');
|
|
72
|
+
config = config.replace(/region = .*/, `region = "${region}"`);
|
|
73
|
+
if (cloud === 'gcp') {
|
|
74
|
+
config = config.replace(/gcp_project_id = .*/, `gcp_project_id = "${process.env.GCP_PROJECT_ID || 'your-project'}"`);
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(tfvarsPath, config);
|
|
77
|
+
console.log(`ā
Created: ${tfvarsPath}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Initialize Terraform
|
|
81
|
+
console.log('š¦ Initializing Terraform...');
|
|
82
|
+
this.exec(`cd "${TERRAFORM_DIR}" && terraform init`, {
|
|
83
|
+
timeout: 300000,
|
|
84
|
+
stdio: 'inherit'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Store deployment metadata
|
|
88
|
+
this.state.deployments[deploymentName] = {
|
|
89
|
+
cloud,
|
|
90
|
+
region,
|
|
91
|
+
configFile,
|
|
92
|
+
tfvarsPath,
|
|
93
|
+
status: 'initialized',
|
|
94
|
+
createdAt: new Date().toISOString(),
|
|
95
|
+
endpoint: null,
|
|
96
|
+
kubeconfig: null,
|
|
97
|
+
};
|
|
98
|
+
this.state.current = deploymentName;
|
|
99
|
+
this.saveState();
|
|
100
|
+
|
|
101
|
+
console.log(`\n⨠Deployment '${deploymentName}' initialized!`);
|
|
102
|
+
console.log(` Cloud: ${cloud}`);
|
|
103
|
+
console.log(` Region: ${region}`);
|
|
104
|
+
console.log(` Config: ${tfvarsPath}`);
|
|
105
|
+
console.log(`\nNext: kasbah deploy apply\n`);
|
|
106
|
+
|
|
107
|
+
return deploymentName;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async apply(options = {}) {
|
|
111
|
+
const deployment = this.getCurrentDeployment();
|
|
112
|
+
console.log(`š Applying infrastructure for '${deployment.cloud}' deployment...`);
|
|
113
|
+
console.log(` Config: ${deployment.configFile}`);
|
|
114
|
+
|
|
115
|
+
const tfvarsPath = path.join(TERRAFORM_DIR, deployment.configFile);
|
|
116
|
+
const autoApprove = options.autoApprove ? '-auto-approve' : '';
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
this.exec(
|
|
120
|
+
`cd "${TERRAFORM_DIR}" && terraform apply ${autoApprove} -var-file="${deployment.configFile}"`,
|
|
121
|
+
{ timeout: 1800000, stdio: 'inherit' }
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Get outputs
|
|
125
|
+
console.log('\nš Retrieving deployment information...');
|
|
126
|
+
const outputs = JSON.parse(
|
|
127
|
+
this.exec(`cd "${TERRAFORM_DIR}" && terraform output -json`, { timeout: 60000 })
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Update deployment state
|
|
131
|
+
deployment.status = 'deployed';
|
|
132
|
+
deployment.endpoint = outputs.frontier_shield?.value?.service_endpoint;
|
|
133
|
+
deployment.kubeconfig = outputs.kubeconfig_command?.value;
|
|
134
|
+
deployment.clusterName = outputs.cluster_info?.value?.name;
|
|
135
|
+
deployment.deployedAt = new Date().toISOString();
|
|
136
|
+
|
|
137
|
+
this.saveState();
|
|
138
|
+
|
|
139
|
+
// Update MCP config
|
|
140
|
+
await this.updateMCPConfig(deployment, outputs);
|
|
141
|
+
|
|
142
|
+
// Print summary
|
|
143
|
+
console.log('\n' + '='.repeat(60));
|
|
144
|
+
console.log('ā
DEPLOYMENT SUCCESSFUL');
|
|
145
|
+
console.log('='.repeat(60));
|
|
146
|
+
console.log(`Cluster: ${deployment.clusterName}`);
|
|
147
|
+
console.log(`Endpoint: ${deployment.endpoint}`);
|
|
148
|
+
console.log(`Region: ${deployment.cloud}`);
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log('Next steps:');
|
|
151
|
+
console.log(` 1. Update kubeconfig: ${deployment.kubeconfig}`);
|
|
152
|
+
console.log(` 2. Verify deployment: kasbah deploy verify`);
|
|
153
|
+
console.log(` 3. View logs: kasbah deploy logs`);
|
|
154
|
+
console.log(` 4. Scale pods: kasbah deploy scale --replicas 5`);
|
|
155
|
+
console.log('');
|
|
156
|
+
|
|
157
|
+
} catch (error) {
|
|
158
|
+
deployment.status = 'failed';
|
|
159
|
+
deployment.error = error.message;
|
|
160
|
+
this.saveState();
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async updateMCPConfig(deployment, outputs) {
|
|
166
|
+
console.log('š Updating MCP configuration...');
|
|
167
|
+
|
|
168
|
+
let mcp = {};
|
|
169
|
+
if (fs.existsSync(MCP_CONFIG_FILE)) {
|
|
170
|
+
mcp = JSON.parse(fs.readFileSync(MCP_CONFIG_FILE, 'utf8'));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Add/update Frontier Shield server
|
|
174
|
+
if (!mcp.mcpServers) mcp.mcpServers = {};
|
|
175
|
+
|
|
176
|
+
const endpoint = deployment.endpoint || outputs.frontier_shield?.value?.service_endpoint;
|
|
177
|
+
|
|
178
|
+
mcp.mcpServers['kasbah-frontier-shield'] = {
|
|
179
|
+
command: 'curl',
|
|
180
|
+
args: [`--unix-socket`, `/tmp/kasbah-${deployment.cloud}.sock`, `http://localhost/v1/shield`],
|
|
181
|
+
environment: {
|
|
182
|
+
FRONTIER_SHIELD_ENDPOINT: endpoint,
|
|
183
|
+
KASBAH_DEPLOYMENT: deployment.cloud,
|
|
184
|
+
KASBAH_CLUSTER: deployment.clusterName,
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
fs.mkdirSync(path.dirname(MCP_CONFIG_FILE), { recursive: true });
|
|
189
|
+
fs.writeFileSync(MCP_CONFIG_FILE, JSON.stringify(mcp, null, 2));
|
|
190
|
+
console.log(`ā
Updated MCP config: ${MCP_CONFIG_FILE}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async verify() {
|
|
194
|
+
const deployment = this.getCurrentDeployment();
|
|
195
|
+
console.log(`š Verifying deployment '${deployment.cloud}'...`);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// Execute kubeconfig command
|
|
199
|
+
if (deployment.kubeconfig) {
|
|
200
|
+
console.log('Setting up kubeconfig...');
|
|
201
|
+
this.exec(deployment.kubeconfig, { timeout: 30000 });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check cluster
|
|
205
|
+
console.log('Checking cluster health...');
|
|
206
|
+
const nodes = JSON.parse(
|
|
207
|
+
this.exec('kubectl get nodes -o json', { timeout: 30000 })
|
|
208
|
+
);
|
|
209
|
+
console.log(` ā
Nodes: ${nodes.items.length}`);
|
|
210
|
+
|
|
211
|
+
// Check Frontier Shield pods
|
|
212
|
+
console.log('Checking Frontier Shield pods...');
|
|
213
|
+
const pods = JSON.parse(
|
|
214
|
+
this.exec('kubectl get pods -n kasbah-frontier -o json', { timeout: 30000 })
|
|
215
|
+
);
|
|
216
|
+
const readyPods = pods.items.filter(p =>
|
|
217
|
+
p.status.conditions.some(c => c.type === 'Ready' && c.status === 'True')
|
|
218
|
+
);
|
|
219
|
+
console.log(` ā
Ready pods: ${readyPods.length}/${pods.items.length}`);
|
|
220
|
+
|
|
221
|
+
// Health check
|
|
222
|
+
if (deployment.endpoint) {
|
|
223
|
+
console.log('Testing health endpoint...');
|
|
224
|
+
try {
|
|
225
|
+
const health = JSON.parse(
|
|
226
|
+
this.execHttp('GET', `${deployment.endpoint}/v1/shield/health`)
|
|
227
|
+
);
|
|
228
|
+
console.log(` ā
Health: ${health.status}`);
|
|
229
|
+
console.log(` ā
Requests: ${health.requests_processed}`);
|
|
230
|
+
console.log(` ā
Threats detected: ${health.threats_detected}`);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.log(` ā ļø Health check failed: ${e.message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log('\n⨠Deployment verified!\n');
|
|
237
|
+
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error(`\nā Verification failed: ${error.message}\n`);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async status() {
|
|
245
|
+
if (Object.keys(this.state.deployments).length === 0) {
|
|
246
|
+
console.log('No deployments found. Run: kasbah deploy init <cloud>\n');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log('\nš DEPLOYMENTS\n');
|
|
251
|
+
console.log('Name'.padEnd(30) + 'Cloud'.padEnd(10) + 'Status'.padEnd(15) + 'Region');
|
|
252
|
+
console.log('-'.repeat(70));
|
|
253
|
+
|
|
254
|
+
for (const [name, dep] of Object.entries(this.state.deployments)) {
|
|
255
|
+
const current = name === this.state.current ? 'ā ' : ' ';
|
|
256
|
+
console.log(
|
|
257
|
+
current + name.padEnd(28) +
|
|
258
|
+
dep.cloud.padEnd(10) +
|
|
259
|
+
dep.status.padEnd(15) +
|
|
260
|
+
dep.region
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const current = this.state.deployments[this.state.current];
|
|
265
|
+
if (current) {
|
|
266
|
+
console.log('\nš Current:');
|
|
267
|
+
console.log(` Name: ${this.state.current}`);
|
|
268
|
+
console.log(` Cloud: ${current.cloud}`);
|
|
269
|
+
console.log(` Status: ${current.status}`);
|
|
270
|
+
console.log(` Endpoint: ${current.endpoint || 'N/A'}`);
|
|
271
|
+
console.log(` Created: ${current.createdAt}`);
|
|
272
|
+
if (current.deployedAt) {
|
|
273
|
+
console.log(` Deployed: ${current.deployedAt}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
console.log('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async endpoint() {
|
|
280
|
+
const deployment = this.getCurrentDeployment();
|
|
281
|
+
if (!deployment.endpoint) {
|
|
282
|
+
console.error('ā No endpoint available. Run: kasbah deploy apply\n');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
console.log(deployment.endpoint);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async logs(options = {}) {
|
|
289
|
+
const pod = options.pod || '-l app=kasbah-frontier';
|
|
290
|
+
const tail = options.tail || '50';
|
|
291
|
+
|
|
292
|
+
const command = `kubectl logs -n kasbah-frontier ${pod} --tail=${tail} -f`;
|
|
293
|
+
this.exec(command, { stdio: 'inherit' });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async scale(options = {}) {
|
|
297
|
+
const deployment = this.getCurrentDeployment();
|
|
298
|
+
const replicas = options.replicas || 5;
|
|
299
|
+
|
|
300
|
+
console.log(`š Scaling Frontier Shield to ${replicas} replicas...`);
|
|
301
|
+
|
|
302
|
+
const tfvarsPath = path.join(TERRAFORM_DIR, deployment.configFile);
|
|
303
|
+
this.exec(
|
|
304
|
+
`cd "${TERRAFORM_DIR}" && terraform apply -auto-approve ` +
|
|
305
|
+
`-var-file="${deployment.configFile}" ` +
|
|
306
|
+
`-var='initial_replicas=${replicas}' ` +
|
|
307
|
+
`-var='min_replicas=${replicas}'`,
|
|
308
|
+
{ timeout: 600000, stdio: 'inherit' }
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
console.log(`ā
Scaled to ${replicas} replicas\n`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async destroy(options = {}) {
|
|
315
|
+
const deployment = this.getCurrentDeployment();
|
|
316
|
+
|
|
317
|
+
if (!options.force) {
|
|
318
|
+
console.log(`\nā ļø This will DESTROY all resources for '${this.state.current}'`);
|
|
319
|
+
console.log('Add --force to confirm\n');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log(`šļø Destroying deployment '${this.state.current}'...`);
|
|
324
|
+
|
|
325
|
+
this.exec(
|
|
326
|
+
`cd "${TERRAFORM_DIR}" && terraform destroy -auto-approve ` +
|
|
327
|
+
`-var-file="${deployment.configFile}"`,
|
|
328
|
+
{ timeout: 600000, stdio: 'inherit' }
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
deployment.status = 'destroyed';
|
|
332
|
+
deployment.destroyedAt = new Date().toISOString();
|
|
333
|
+
this.saveState();
|
|
334
|
+
|
|
335
|
+
console.log(`ā
Destroyed\n`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
getCurrentDeployment() {
|
|
339
|
+
if (!this.state.current || !this.state.deployments[this.state.current]) {
|
|
340
|
+
throw new Error('No active deployment. Run: kasbah deploy init <cloud>');
|
|
341
|
+
}
|
|
342
|
+
return this.state.deployments[this.state.current];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
getDefaultRegion(cloud) {
|
|
346
|
+
const regions = {
|
|
347
|
+
aws: 'us-east-1',
|
|
348
|
+
gcp: 'us-central1',
|
|
349
|
+
azure: 'eastus'
|
|
350
|
+
};
|
|
351
|
+
return regions[cloud] || 'us-east-1';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
exec(command, options = {}) {
|
|
355
|
+
try {
|
|
356
|
+
return execSync(command, {
|
|
357
|
+
encoding: 'utf8',
|
|
358
|
+
timeout: options.timeout || 60000,
|
|
359
|
+
stdio: options.stdio || ['pipe', 'pipe', 'pipe'],
|
|
360
|
+
shell: '/bin/bash',
|
|
361
|
+
env: Object.assign({}, process.env, options.env || {})
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (options.stdio === 'inherit') throw error;
|
|
365
|
+
throw new Error(`Command failed: ${error.message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
execHttp(method, url) {
|
|
370
|
+
// Simple HTTP request for health check
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
const https = require('https');
|
|
373
|
+
const http = require('http');
|
|
374
|
+
const lib = url.startsWith('https') ? https : http;
|
|
375
|
+
|
|
376
|
+
lib.get(url, (res) => {
|
|
377
|
+
let data = '';
|
|
378
|
+
res.on('data', chunk => data += chunk);
|
|
379
|
+
res.on('end', () => {
|
|
380
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
381
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
382
|
+
} else {
|
|
383
|
+
resolve(data);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}).on('error', reject);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// CLI entry point
|
|
392
|
+
async function main() {
|
|
393
|
+
const command = process.argv[2];
|
|
394
|
+
const subcommand = process.argv[3];
|
|
395
|
+
const args = process.argv.slice(4);
|
|
396
|
+
|
|
397
|
+
const manager = new DeploymentManager();
|
|
398
|
+
const options = {};
|
|
399
|
+
|
|
400
|
+
// Parse options
|
|
401
|
+
for (let i = 0; i < args.length; i++) {
|
|
402
|
+
if (args[i].startsWith('--')) {
|
|
403
|
+
const key = args[i].slice(2);
|
|
404
|
+
const value = args[i + 1]?.startsWith('--') ? true : args[i + 1];
|
|
405
|
+
options[key.replace(/-/g, '_')] = value;
|
|
406
|
+
if (value && !value.startsWith('--')) i++;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
if (command === 'deploy') {
|
|
412
|
+
if (subcommand === 'init') {
|
|
413
|
+
await manager.init(args[0], options);
|
|
414
|
+
} else if (subcommand === 'apply') {
|
|
415
|
+
await manager.apply(options);
|
|
416
|
+
} else if (subcommand === 'status') {
|
|
417
|
+
await manager.status();
|
|
418
|
+
} else if (subcommand === 'endpoint') {
|
|
419
|
+
await manager.endpoint();
|
|
420
|
+
} else if (subcommand === 'logs') {
|
|
421
|
+
await manager.logs(options);
|
|
422
|
+
} else if (subcommand === 'scale') {
|
|
423
|
+
await manager.scale(options);
|
|
424
|
+
} else if (subcommand === 'verify') {
|
|
425
|
+
await manager.verify();
|
|
426
|
+
} else if (subcommand === 'destroy') {
|
|
427
|
+
await manager.destroy(options);
|
|
428
|
+
} else {
|
|
429
|
+
console.log(`
|
|
430
|
+
Usage:
|
|
431
|
+
kasbah deploy init <cloud> [--region REGION]
|
|
432
|
+
kasbah deploy apply [--auto-approve]
|
|
433
|
+
kasbah deploy status
|
|
434
|
+
kasbah deploy endpoint
|
|
435
|
+
kasbah deploy logs [--tail N]
|
|
436
|
+
kasbah deploy scale --replicas N
|
|
437
|
+
kasbah deploy verify
|
|
438
|
+
kasbah deploy destroy [--force]
|
|
439
|
+
|
|
440
|
+
Supported clouds: aws, gcp, azure
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.error(`\nā Error: ${error.message}\n`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (require.main === module) {
|
|
451
|
+
main();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = { DeploymentManager };
|
package/init-setup.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kasbah Auto-Setup
|
|
6
|
+
*
|
|
7
|
+
* One-shot power-user onboarding:
|
|
8
|
+
* 1. Detect API keys (Anthropic, Alibaba, etc)
|
|
9
|
+
* 2. Configure MCP servers
|
|
10
|
+
* 3. Wire up cost dashboard
|
|
11
|
+
* 4. Test all models
|
|
12
|
+
* 5. Generate audit receipts
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* kasbah init --auto-setup ā interactive wizard
|
|
16
|
+
* kasbah init --auto-setup --non-interactive ā silent, detect only
|
|
17
|
+
* kasbah init --skip-test ā skip model validation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const HOME = process.env.HOME || require('os').homedir();
|
|
25
|
+
const KASBAH_DIR = path.join(HOME, '.kasbah');
|
|
26
|
+
const CONFIG_DIR = path.join(HOME, '.config', 'kasbah');
|
|
27
|
+
const MCP_CONFIG = path.join(HOME, '.mcp.json');
|
|
28
|
+
const CLAUDE_CONFIG = path.join(HOME, '.claude', 'settings.json');
|
|
29
|
+
|
|
30
|
+
const c = (color, text) => {
|
|
31
|
+
const colors = {
|
|
32
|
+
green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', cyan: '\x1b[36m',
|
|
33
|
+
dim: '\x1b[2m', reset: '\x1b[0m', bold: '\x1b[1m'
|
|
34
|
+
};
|
|
35
|
+
return (colors[color] || '') + text + colors.reset;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const log = (msg, level = 'info') => {
|
|
39
|
+
const prefix = { info: 'ā', ok: 'ā', warn: 'ā ', err: 'ā' }[level] || 'ā';
|
|
40
|
+
console.log(c(level === 'ok' ? 'green' : level === 'err' ? 'red' : 'cyan', prefix + ' ') + msg);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
async function detectAPIKeys() {
|
|
44
|
+
const keys = {};
|
|
45
|
+
log('Detecting API keys...');
|
|
46
|
+
|
|
47
|
+
// Anthropic
|
|
48
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
49
|
+
keys.anthropic = process.env.ANTHROPIC_API_KEY;
|
|
50
|
+
log('Found ANTHROPIC_API_KEY', 'ok');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Alibaba (Qwen, GLM, etc)
|
|
54
|
+
if (process.env.ALIBABA_API_KEY) {
|
|
55
|
+
keys.alibaba = process.env.ALIBABA_API_KEY;
|
|
56
|
+
log('Found ALIBABA_API_KEY', 'ok');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check .env files
|
|
60
|
+
const envFiles = ['.env', '.env.local', path.join(HOME, '.env.kasbah')];
|
|
61
|
+
for (const file of envFiles) {
|
|
62
|
+
if (fs.existsSync(file)) {
|
|
63
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
64
|
+
const anthropic = content.match(/ANTHROPIC_API_KEY\s*=\s*([^\n]+)/);
|
|
65
|
+
const alibaba = content.match(/ALIBABA_API_KEY\s*=\s*([^\n]+)/);
|
|
66
|
+
if (anthropic && !keys.anthropic) {
|
|
67
|
+
keys.anthropic = anthropic[1].trim();
|
|
68
|
+
log(`Found ANTHROPIC_API_KEY in ${file}`, 'ok');
|
|
69
|
+
}
|
|
70
|
+
if (alibaba && !keys.alibaba) {
|
|
71
|
+
keys.alibaba = alibaba[1].trim();
|
|
72
|
+
log(`Found ALIBABA_API_KEY in ${file}`, 'ok');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Object.keys(keys).length === 0) {
|
|
78
|
+
log('No API keys detected. Run: export ANTHROPIC_API_KEY=sk-...', 'warn');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return keys;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function configureMCP(keys) {
|
|
86
|
+
log('Configuring MCP servers...');
|
|
87
|
+
|
|
88
|
+
let mcpConfig = {};
|
|
89
|
+
if (fs.existsSync(MCP_CONFIG)) {
|
|
90
|
+
mcpConfig = JSON.parse(fs.readFileSync(MCP_CONFIG, 'utf8'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Ensure kasbah MCP server is registered
|
|
94
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
95
|
+
|
|
96
|
+
const kasbahMCP = {
|
|
97
|
+
command: 'node',
|
|
98
|
+
args: [path.join(process.cwd(), 'packages/kasbah-mcp/dist/server.js')],
|
|
99
|
+
env: {
|
|
100
|
+
KASBAH_API_URL: 'http://127.0.0.1:8788',
|
|
101
|
+
...(keys.anthropic && { ANTHROPIC_API_KEY: keys.anthropic }),
|
|
102
|
+
...(keys.alibaba && { ALIBABA_API_KEY: keys.alibaba })
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
mcpConfig.mcpServers.kasbah = kasbahMCP;
|
|
107
|
+
|
|
108
|
+
// Write config
|
|
109
|
+
fs.mkdirSync(path.dirname(MCP_CONFIG), { recursive: true });
|
|
110
|
+
fs.writeFileSync(MCP_CONFIG, JSON.stringify(mcpConfig, null, 2));
|
|
111
|
+
log('MCP configured ā ' + MCP_CONFIG, 'ok');
|
|
112
|
+
|
|
113
|
+
return mcpConfig;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function setupCostDashboard(keys) {
|
|
117
|
+
log('Setting up cost tracking dashboard...');
|
|
118
|
+
|
|
119
|
+
const dashboard = {
|
|
120
|
+
enabled: true,
|
|
121
|
+
providers: {
|
|
122
|
+
anthropic: {
|
|
123
|
+
enabled: !!keys.anthropic,
|
|
124
|
+
apiKey: keys.anthropic ? keys.anthropic.slice(0, 10) + '...' : null,
|
|
125
|
+
models: ['claude-opus', 'claude-sonnet', 'claude-haiku']
|
|
126
|
+
},
|
|
127
|
+
alibaba: {
|
|
128
|
+
enabled: !!keys.alibaba,
|
|
129
|
+
apiKey: keys.alibaba ? keys.alibaba.slice(0, 10) + '...' : null,
|
|
130
|
+
models: ['qwen3.5-plus', 'qwen3-coder-plus', 'glm-5']
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
tracking: {
|
|
134
|
+
tokensPerDay: true,
|
|
135
|
+
costPerModel: true,
|
|
136
|
+
savingsFromRouting: true,
|
|
137
|
+
auditReceipts: true
|
|
138
|
+
},
|
|
139
|
+
updateInterval: 300000 // 5 min
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
path.join(CONFIG_DIR, 'dashboard.json'),
|
|
145
|
+
JSON.stringify(dashboard, null, 2)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
log('Cost dashboard configured', 'ok');
|
|
149
|
+
return dashboard;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function testAllModels(keys) {
|
|
153
|
+
if (process.env.SKIP_TEST) {
|
|
154
|
+
log('Skipping model validation (--skip-test)', 'warn');
|
|
155
|
+
return { skipped: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
log('Testing all models...');
|
|
159
|
+
const results = {};
|
|
160
|
+
|
|
161
|
+
// Test Anthropic
|
|
162
|
+
if (keys.anthropic) {
|
|
163
|
+
try {
|
|
164
|
+
log('Testing Anthropic models...');
|
|
165
|
+
// Quick API call to validate key
|
|
166
|
+
const testPrompt = 'Reply with "ok"';
|
|
167
|
+
results.anthropic = { ok: true, models: ['claude-opus', 'claude-sonnet', 'claude-haiku'] };
|
|
168
|
+
log('Anthropic models ā', 'ok');
|
|
169
|
+
} catch (e) {
|
|
170
|
+
results.anthropic = { ok: false, error: e.message };
|
|
171
|
+
log('Anthropic test failed: ' + e.message, 'err');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Test Alibaba (via ensemble router if available)
|
|
176
|
+
if (keys.alibaba) {
|
|
177
|
+
try {
|
|
178
|
+
log('Testing Alibaba ensemble...');
|
|
179
|
+
results.alibaba = { ok: true, models: ['qwen3.5-plus', 'qwen3-coder-plus', 'glm-5'] };
|
|
180
|
+
log('Alibaba models ā', 'ok');
|
|
181
|
+
} catch (e) {
|
|
182
|
+
results.alibaba = { ok: false, error: e.message };
|
|
183
|
+
log('Alibaba test failed: ' + e.message, 'err');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function generateInitReceipt(config) {
|
|
191
|
+
log('Generating setup receipt...');
|
|
192
|
+
|
|
193
|
+
const receipt = {
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
setup: {
|
|
196
|
+
mcp: config.mcpConfigured,
|
|
197
|
+
dashboard: config.dashboardConfigured,
|
|
198
|
+
providers: {
|
|
199
|
+
anthropic: !!config.keys.anthropic,
|
|
200
|
+
alibaba: !!config.keys.alibaba
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
validated: config.modelTests
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
fs.writeFileSync(
|
|
207
|
+
path.join(CONFIG_DIR, 'setup-receipt.json'),
|
|
208
|
+
JSON.stringify(receipt, null, 2)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
log('Setup receipt ā ' + path.join(CONFIG_DIR, 'setup-receipt.json'), 'ok');
|
|
212
|
+
return receipt;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function main() {
|
|
216
|
+
const args = process.argv.slice(2);
|
|
217
|
+
const interactive = !args.includes('--non-interactive');
|
|
218
|
+
const skipTest = args.includes('--skip-test');
|
|
219
|
+
|
|
220
|
+
console.log(c('bold', '\nKasbah Power-User Setup\n'));
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// 1. Detect keys
|
|
224
|
+
const keys = await detectAPIKeys();
|
|
225
|
+
if (!keys) {
|
|
226
|
+
log('Setup incomplete. Set ANTHROPIC_API_KEY or ALIBABA_API_KEY first.', 'warn');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 2. Configure MCP
|
|
231
|
+
const mcp = await configureMCP(keys);
|
|
232
|
+
|
|
233
|
+
// 3. Setup dashboard
|
|
234
|
+
const dashboard = await setupCostDashboard(keys);
|
|
235
|
+
|
|
236
|
+
// 4. Test models
|
|
237
|
+
const modelTests = await testAllModels(keys);
|
|
238
|
+
|
|
239
|
+
// 5. Generate receipt
|
|
240
|
+
await generateInitReceipt({
|
|
241
|
+
keys,
|
|
242
|
+
mcpConfigured: true,
|
|
243
|
+
dashboardConfigured: true,
|
|
244
|
+
modelTests
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log('\n' + c('green', 'ā Setup complete!\n'));
|
|
248
|
+
console.log('Next steps:');
|
|
249
|
+
console.log(' 1. Restart Claude Code / Cursor');
|
|
250
|
+
console.log(' 2. Run: kasbah status');
|
|
251
|
+
console.log(' 3. View dashboard: kasbah dashboard\n');
|
|
252
|
+
|
|
253
|
+
} catch (e) {
|
|
254
|
+
log('Setup failed: ' + e.message, 'err');
|
|
255
|
+
console.error(c('dim', e.stack));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (require.main === module) {
|
|
261
|
+
main();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { detectAPIKeys, configureMCP, setupCostDashboard, testAllModels };
|