infrawise 0.12.4 → 0.12.6

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/README.md CHANGED
@@ -149,7 +149,7 @@ Add to your editor's MCP config:
149
149
 
150
150
  | Tool | What it provides |
151
151
  | ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
152
- | `get_infra_overview` | Complete snapshot — all services, counts, and high-severity findings |
152
+ | `get_infra_overview` | Complete snapshot — all services, counts, high-severity findings, and a `configured` flag |
153
153
  | `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
154
154
  | `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes |
155
155
  | `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
@@ -46,6 +46,82 @@ function buildMarkdownReport(findings, projectName) {
46
46
  function mkSpinner(text) {
47
47
  return ora({ text: chalk.dim(text), color: 'cyan' }).start();
48
48
  }
49
+ // Runs one extractor behind a spinner: succeeds with a count summary, or warns and
50
+ // returns undefined on failure so a single service never aborts the whole analysis.
51
+ async function extract(enabled, spinnerText, label, fn, summarize) {
52
+ if (!enabled)
53
+ return undefined;
54
+ const s = mkSpinner(spinnerText);
55
+ try {
56
+ const result = await fn();
57
+ s.succeed(chalk.green(label) + chalk.dim(` ${summarize(result)}`));
58
+ return result;
59
+ }
60
+ catch (err) {
61
+ s.warn(chalk.yellow(`${label} skipped`) +
62
+ chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
63
+ return undefined;
64
+ }
65
+ }
66
+ // Single source of truth for which analyzers run given the config. Used by both the
67
+ // full `runAnalyze` and the cache-backed `runCodeRefresh` so the two never drift.
68
+ function buildAnalyzers(config, iacDriftAnalyzer, iacLambdas) {
69
+ const pipelineAnalyzer = new PipelineAnalyzer();
70
+ pipelineAnalyzer.setIaCLambdas(iacLambdas);
71
+ return [
72
+ ...(config.dynamodb?.enabled === true
73
+ ? [
74
+ new FullTableScanAnalyzer(),
75
+ new MissingGSIAnalyzer(),
76
+ new HotPartitionAnalyzer(config.analysis?.hotPartitionThreshold, config.analysis?.hotPartitionThresholds),
77
+ ]
78
+ : []),
79
+ ...(config.postgres?.enabled
80
+ ? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
81
+ : []),
82
+ ...(config.mysql?.enabled
83
+ ? [new MissingMySQLIndexAnalyzer(), new MySQLFullTableScanAnalyzer()]
84
+ : []),
85
+ ...(config.mongodb?.enabled
86
+ ? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
87
+ : []),
88
+ ...(config.sqs?.enabled === true
89
+ ? [
90
+ new MissingDLQAnalyzer(),
91
+ new UnencryptedQueueAnalyzer(),
92
+ new LargeQueueBacklogAnalyzer(),
93
+ new VisibilityTimeoutMismatchAnalyzer(),
94
+ ]
95
+ : []),
96
+ ...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
97
+ ...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
98
+ ...(config.lambda?.enabled === true
99
+ ? [
100
+ new LambdaDefaultMemoryAnalyzer(),
101
+ new LambdaHighTimeoutAnalyzer(),
102
+ new LambdaMissingTriggerDLQAnalyzer(),
103
+ ]
104
+ : []),
105
+ ...(config.rds?.enabled === true
106
+ ? [
107
+ new RDSPubliclyAccessibleAnalyzer(),
108
+ new RDSNoBackupAnalyzer(),
109
+ new RDSUnencryptedAnalyzer(),
110
+ new RDSNoDeletionProtectionAnalyzer(),
111
+ new RDSNoMultiAZAnalyzer(),
112
+ ]
113
+ : []),
114
+ ...(config.s3?.enabled === true
115
+ ? [
116
+ new S3PublicAccessAnalyzer(),
117
+ new S3MissingVersioningAnalyzer(),
118
+ new S3UnencryptedAnalyzer(),
119
+ ]
120
+ : []),
121
+ ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
122
+ pipelineAnalyzer,
123
+ ];
124
+ }
49
125
  export async function runAnalyze(options = {}) {
50
126
  printHeader('Running Analysis');
51
127
  let config;
@@ -64,204 +140,29 @@ export async function runAnalyze(options = {}) {
64
140
  region: config.aws?.region,
65
141
  profile: config.aws?.profile,
66
142
  };
67
- const dynamoMeta = [];
68
- const postgresMeta = [];
69
- const mysqlMeta = [];
70
- const mongoMeta = [];
71
143
  const servicesMeta = {};
72
- // ── DynamoDB ────────────────────────────────────────────────────────────────
73
- if (config.dynamodb?.enabled === true) {
74
- const s = mkSpinner('Extracting DynamoDB tables...');
75
- try {
76
- const result = await extractDynamoMetadata(config);
77
- dynamoMeta.push(...result);
78
- s.succeed(chalk.green('DynamoDB') + chalk.dim(` ${result.length} table(s)`));
79
- }
80
- catch (err) {
81
- s.warn(chalk.yellow('DynamoDB skipped') +
82
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
83
- }
84
- }
85
- // ── PostgreSQL ──────────────────────────────────────────────────────────────
86
- if (config.postgres?.enabled && config.postgres.connectionString) {
87
- const s = mkSpinner('Extracting PostgreSQL schema...');
88
- try {
89
- const result = await extractPostgresMetadata(config.postgres.connectionString);
90
- postgresMeta.push(...result);
91
- s.succeed(chalk.green('PostgreSQL') + chalk.dim(` ${result.length} table(s)`));
92
- }
93
- catch (err) {
94
- s.warn(chalk.yellow('PostgreSQL skipped') +
95
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
96
- }
97
- }
98
- // ── MySQL ───────────────────────────────────────────────────────────────────
99
- if (config.mysql?.enabled && config.mysql.connectionString) {
100
- const s = mkSpinner('Extracting MySQL schema...');
101
- try {
102
- const result = await extractMySQLMetadata(config.mysql.connectionString);
103
- mysqlMeta.push(...result);
104
- s.succeed(chalk.green('MySQL') + chalk.dim(` ${result.length} table(s)`));
105
- }
106
- catch (err) {
107
- s.warn(chalk.yellow('MySQL skipped') +
108
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
109
- }
110
- }
111
- // ── MongoDB ─────────────────────────────────────────────────────────────────
112
- if (config.mongodb?.enabled && config.mongodb.connectionString) {
113
- const s = mkSpinner('Extracting MongoDB schema...');
114
- try {
115
- const result = await extractMongoMetadata(config.mongodb.connectionString, config.mongodb.databases);
116
- mongoMeta.push(...result);
117
- s.succeed(chalk.green('MongoDB') + chalk.dim(` ${result.length} collection(s)`));
118
- }
119
- catch (err) {
120
- s.warn(chalk.yellow('MongoDB skipped') +
121
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
122
- }
123
- }
124
- // ── SQS ─────────────────────────────────────────────────────────────────────
125
- if (config.sqs?.enabled === true) {
126
- const s = mkSpinner('Extracting SQS queues...');
127
- try {
128
- const result = await extractSQSMetadata(awsCfg);
129
- servicesMeta.sqs = result;
130
- s.succeed(chalk.green('SQS') + chalk.dim(` ${result.length} queue(s)`));
131
- }
132
- catch (err) {
133
- s.warn(chalk.yellow('SQS skipped') +
134
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
135
- }
136
- }
137
- // ── SNS ─────────────────────────────────────────────────────────────────────
138
- if (config.sns?.enabled === true) {
139
- const s = mkSpinner('Extracting SNS topics...');
140
- try {
141
- const result = await extractSNSMetadata(awsCfg);
142
- servicesMeta.sns = result;
143
- s.succeed(chalk.green('SNS') + chalk.dim(` ${result.length} topic(s)`));
144
- }
145
- catch (err) {
146
- s.warn(chalk.yellow('SNS skipped') +
147
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
148
- }
149
- }
150
- // ── SSM Parameter Store ──────────────────────────────────────────────────────
151
- if (config.ssm?.enabled === true) {
152
- const s = mkSpinner('Extracting SSM parameters...');
153
- try {
154
- const result = await extractSSMMetadata({ ...awsCfg, paths: config.ssm?.paths });
155
- servicesMeta.ssm = result;
156
- s.succeed(chalk.green('SSM') +
157
- chalk.dim(` ${result.length} parameter(s) `) +
158
- chalk.dim('(metadata only, no values)'));
159
- }
160
- catch (err) {
161
- s.warn(chalk.yellow('SSM skipped') +
162
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
163
- }
164
- }
165
- // ── Secrets Manager ──────────────────────────────────────────────────────────
166
- if (config.secretsManager?.enabled === true) {
167
- const s = mkSpinner('Extracting Secrets Manager metadata...');
168
- try {
169
- const result = await extractSecretsMetadata(awsCfg);
170
- servicesMeta.secrets = result;
171
- s.succeed(chalk.green('Secrets Manager') +
172
- chalk.dim(` ${result.length} secret(s) `) +
173
- chalk.dim('(names/rotation only, no values)'));
174
- }
175
- catch (err) {
176
- s.warn(chalk.yellow('Secrets Manager skipped') +
177
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
178
- }
179
- }
180
- // ── Lambda ───────────────────────────────────────────────────────────────────
181
- if (config.lambda?.enabled === true) {
182
- const s = mkSpinner('Extracting Lambda functions...');
183
- try {
184
- const result = await extractLambdaMetadata(awsCfg, config.lambda?.includeFunctions);
185
- servicesMeta.lambda = result;
186
- s.succeed(chalk.green('Lambda') + chalk.dim(` ${result.length} function(s)`));
187
- }
188
- catch (err) {
189
- s.warn(chalk.yellow('Lambda skipped') +
190
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
191
- }
192
- }
193
- // ── EventBridge ──────────────────────────────────────────────────────────────
194
- if (config.eventbridge?.enabled === true) {
195
- const s = mkSpinner('Extracting EventBridge rules...');
196
- try {
197
- const result = await extractEventBridgeMetadata(awsCfg);
198
- servicesMeta.eventbridge = result;
199
- s.succeed(chalk.green('EventBridge') + chalk.dim(` ${result.length} rule(s)`));
200
- }
201
- catch (err) {
202
- s.warn(chalk.yellow('EventBridge skipped') +
203
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
204
- }
205
- }
206
- // ── RDS ──────────────────────────────────────────────────────────────────────
207
- if (config.rds?.enabled === true) {
208
- const s = mkSpinner('Extracting RDS instances...');
209
- try {
210
- const result = await extractRDSMetadata(awsCfg);
211
- servicesMeta.rds = result;
212
- s.succeed(chalk.green('RDS') + chalk.dim(` ${result.length} instance(s)`));
213
- }
214
- catch (err) {
215
- s.warn(chalk.yellow('RDS skipped') +
216
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
217
- }
218
- }
219
- // ── S3 ────────────────────────────────────────────────────────────────────────
220
- if (config.s3?.enabled === true) {
221
- const s = mkSpinner('Extracting S3 buckets...');
222
- try {
223
- const result = await extractS3Metadata(awsCfg);
224
- servicesMeta.s3 = result;
225
- s.succeed(chalk.green('S3') + chalk.dim(` ${result.length} bucket(s)`));
226
- }
227
- catch (err) {
228
- s.warn(chalk.yellow('S3 skipped') +
229
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
230
- }
231
- }
232
- // ── API Gateway ───────────────────────────────────────────────────────────────
233
- if (config.apiGateway?.enabled === true) {
234
- const s = mkSpinner('Extracting API Gateway routes...');
235
- try {
236
- const result = await extractAPIGatewayMetadata(awsCfg);
237
- servicesMeta.apiGateway = result;
238
- const routeCount = result.reduce((sum, api) => sum + api.routes.length, 0);
239
- s.succeed(chalk.green('API Gateway') + chalk.dim(` ${result.length} API(s), ${routeCount} route(s)`));
240
- }
241
- catch (err) {
242
- s.warn(chalk.yellow('API Gateway skipped') +
243
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
244
- }
245
- }
246
- // ── CloudWatch Logs ──────────────────────────────────────────────────────────
247
- if (config.cloudwatchLogs?.enabled) {
248
- const s = mkSpinner('Sampling CloudWatch Logs (errors only, max 50 groups)...');
249
- try {
250
- const result = await extractLogsMetadata({
251
- ...awsCfg,
252
- logGroupPrefixes: config.cloudwatchLogs.logGroupPrefixes,
253
- windowHours: config.cloudwatchLogs.windowHours,
254
- });
255
- servicesMeta.logs = result;
256
- const errorGroups = result.filter((lg) => lg.errorCount > 0).length;
257
- s.succeed(chalk.green('CloudWatch Logs') +
258
- chalk.dim(` ${result.length} group(s), ${errorGroups} with errors`));
259
- }
260
- catch (err) {
261
- s.warn(chalk.yellow('CloudWatch Logs skipped') +
262
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
263
- }
264
- }
144
+ const dynamoMeta = (await extract(config.dynamodb?.enabled === true, 'Extracting DynamoDB tables...', 'DynamoDB', () => extractDynamoMetadata(config), (r) => `${r.length} table(s)`)) ?? [];
145
+ const pgConn = config.postgres?.enabled ? config.postgres.connectionString : undefined;
146
+ const postgresMeta = (await extract(!!pgConn, 'Extracting PostgreSQL schema...', 'PostgreSQL', () => extractPostgresMetadata(pgConn ?? ''), (r) => `${r.length} table(s)`)) ?? [];
147
+ const mysqlConn = config.mysql?.enabled ? config.mysql.connectionString : undefined;
148
+ const mysqlMeta = (await extract(!!mysqlConn, 'Extracting MySQL schema...', 'MySQL', () => extractMySQLMetadata(mysqlConn ?? ''), (r) => `${r.length} table(s)`)) ?? [];
149
+ const mongoConn = config.mongodb?.enabled ? config.mongodb.connectionString : undefined;
150
+ const mongoMeta = (await extract(!!mongoConn, 'Extracting MongoDB schema...', 'MongoDB', () => extractMongoMetadata(mongoConn ?? '', config.mongodb?.databases), (r) => `${r.length} collection(s)`)) ?? [];
151
+ servicesMeta.sqs = await extract(config.sqs?.enabled === true, 'Extracting SQS queues...', 'SQS', () => extractSQSMetadata(awsCfg), (r) => `${r.length} queue(s)`);
152
+ servicesMeta.sns = await extract(config.sns?.enabled === true, 'Extracting SNS topics...', 'SNS', () => extractSNSMetadata(awsCfg), (r) => `${r.length} topic(s)`);
153
+ servicesMeta.ssm = await extract(config.ssm?.enabled === true, 'Extracting SSM parameters...', 'SSM', () => extractSSMMetadata({ ...awsCfg, paths: config.ssm?.paths }), (r) => `${r.length} parameter(s) (metadata only, no values)`);
154
+ servicesMeta.secrets = await extract(config.secretsManager?.enabled === true, 'Extracting Secrets Manager metadata...', 'Secrets Manager', () => extractSecretsMetadata(awsCfg), (r) => `${r.length} secret(s) (names/rotation only, no values)`);
155
+ servicesMeta.lambda = await extract(config.lambda?.enabled === true, 'Extracting Lambda functions...', 'Lambda', () => extractLambdaMetadata(awsCfg, config.lambda?.includeFunctions), (r) => `${r.length} function(s)`);
156
+ servicesMeta.eventbridge = await extract(config.eventbridge?.enabled === true, 'Extracting EventBridge rules...', 'EventBridge', () => extractEventBridgeMetadata(awsCfg), (r) => `${r.length} rule(s)`);
157
+ servicesMeta.rds = await extract(config.rds?.enabled === true, 'Extracting RDS instances...', 'RDS', () => extractRDSMetadata(awsCfg), (r) => `${r.length} instance(s)`);
158
+ servicesMeta.s3 = await extract(config.s3?.enabled === true, 'Extracting S3 buckets...', 'S3', () => extractS3Metadata(awsCfg), (r) => `${r.length} bucket(s)`);
159
+ servicesMeta.apiGateway = await extract(config.apiGateway?.enabled === true, 'Extracting API Gateway routes...', 'API Gateway', () => extractAPIGatewayMetadata(awsCfg), (r) => `${r.length} API(s), ${r.reduce((sum, api) => sum + api.routes.length, 0)} route(s)`);
160
+ const cwLogs = config.cloudwatchLogs;
161
+ servicesMeta.logs = await extract(cwLogs?.enabled, 'Sampling CloudWatch Logs (errors only, max 50 groups)...', 'CloudWatch Logs', () => extractLogsMetadata({
162
+ ...awsCfg,
163
+ logGroupPrefixes: cwLogs?.logGroupPrefixes,
164
+ windowHours: cwLogs?.windowHours,
165
+ }), (r) => `${r.length} group(s), ${r.filter((lg) => lg.errorCount > 0).length} with errors`);
265
166
  // ── IaC schema (Terraform / CloudFormation / CDK) ────────────────────────────
266
167
  let iacDriftAnalyzer;
267
168
  let iacLambdas = [];
@@ -316,63 +217,7 @@ export async function runAnalyze(options = {}) {
316
217
  let findings;
317
218
  {
318
219
  const s = mkSpinner('Running analyzers...');
319
- const hotPartitionThreshold = config.analysis?.hotPartitionThreshold;
320
- const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
321
- const pipelineAnalyzer = new PipelineAnalyzer();
322
- pipelineAnalyzer.setIaCLambdas(iacLambdas);
323
- const analyzers = [
324
- ...(config.dynamodb?.enabled === true
325
- ? [
326
- new FullTableScanAnalyzer(),
327
- new MissingGSIAnalyzer(),
328
- new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
329
- ]
330
- : []),
331
- ...(config.postgres?.enabled
332
- ? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
333
- : []),
334
- ...(config.mysql?.enabled
335
- ? [new MissingMySQLIndexAnalyzer(), new MySQLFullTableScanAnalyzer()]
336
- : []),
337
- ...(config.mongodb?.enabled
338
- ? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
339
- : []),
340
- ...(config.sqs?.enabled === true
341
- ? [
342
- new MissingDLQAnalyzer(),
343
- new UnencryptedQueueAnalyzer(),
344
- new LargeQueueBacklogAnalyzer(),
345
- new VisibilityTimeoutMismatchAnalyzer(),
346
- ]
347
- : []),
348
- ...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
349
- ...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
350
- ...(config.lambda?.enabled === true
351
- ? [
352
- new LambdaDefaultMemoryAnalyzer(),
353
- new LambdaHighTimeoutAnalyzer(),
354
- new LambdaMissingTriggerDLQAnalyzer(),
355
- ]
356
- : []),
357
- ...(config.rds?.enabled === true
358
- ? [
359
- new RDSPubliclyAccessibleAnalyzer(),
360
- new RDSNoBackupAnalyzer(),
361
- new RDSUnencryptedAnalyzer(),
362
- new RDSNoDeletionProtectionAnalyzer(),
363
- new RDSNoMultiAZAnalyzer(),
364
- ]
365
- : []),
366
- ...(config.s3?.enabled === true
367
- ? [
368
- new S3PublicAccessAnalyzer(),
369
- new S3MissingVersioningAnalyzer(),
370
- new S3UnencryptedAnalyzer(),
371
- ]
372
- : []),
373
- ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
374
- pipelineAnalyzer,
375
- ];
220
+ const analyzers = buildAnalyzers(config, iacDriftAnalyzer, iacLambdas);
376
221
  findings = await runAllAnalyzers(graph, analyzers);
377
222
  s.succeed(chalk.green('Analysis complete') + chalk.dim(` ${findings.length} finding(s)`));
378
223
  }
@@ -450,63 +295,7 @@ export async function runCodeRefresh(repoPath, config) {
450
295
  operations = [];
451
296
  }
452
297
  const graph = buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta, mongoMeta, servicesMeta);
453
- const hotPartitionThreshold = config.analysis?.hotPartitionThreshold;
454
- const hotPartitionThresholds = config.analysis?.hotPartitionThresholds;
455
- const pipelineAnalyzer = new PipelineAnalyzer();
456
- pipelineAnalyzer.setIaCLambdas(iacLambdas);
457
- const analyzers = [
458
- ...(config.dynamodb?.enabled === true
459
- ? [
460
- new FullTableScanAnalyzer(),
461
- new MissingGSIAnalyzer(),
462
- new HotPartitionAnalyzer(hotPartitionThreshold, hotPartitionThresholds),
463
- ]
464
- : []),
465
- ...(config.postgres?.enabled
466
- ? [new MissingIndexAnalyzer(), new NplusOneAnalyzer(), new LargeSelectAnalyzer()]
467
- : []),
468
- ...(config.mysql?.enabled
469
- ? [new MissingMySQLIndexAnalyzer(), new MySQLFullTableScanAnalyzer()]
470
- : []),
471
- ...(config.mongodb?.enabled
472
- ? [new MissingMongoIndexAnalyzer(), new MongoCollectionScanAnalyzer()]
473
- : []),
474
- ...(config.sqs?.enabled === true
475
- ? [
476
- new MissingDLQAnalyzer(),
477
- new UnencryptedQueueAnalyzer(),
478
- new LargeQueueBacklogAnalyzer(),
479
- new VisibilityTimeoutMismatchAnalyzer(),
480
- ]
481
- : []),
482
- ...(config.secretsManager?.enabled === true ? [new MissingSecretRotationAnalyzer()] : []),
483
- ...(config.cloudwatchLogs?.enabled ? [new MissingLogRetentionAnalyzer()] : []),
484
- ...(config.lambda?.enabled === true
485
- ? [
486
- new LambdaDefaultMemoryAnalyzer(),
487
- new LambdaHighTimeoutAnalyzer(),
488
- new LambdaMissingTriggerDLQAnalyzer(),
489
- ]
490
- : []),
491
- ...(config.rds?.enabled === true
492
- ? [
493
- new RDSPubliclyAccessibleAnalyzer(),
494
- new RDSNoBackupAnalyzer(),
495
- new RDSUnencryptedAnalyzer(),
496
- new RDSNoDeletionProtectionAnalyzer(),
497
- new RDSNoMultiAZAnalyzer(),
498
- ]
499
- : []),
500
- ...(config.s3?.enabled === true
501
- ? [
502
- new S3PublicAccessAnalyzer(),
503
- new S3MissingVersioningAnalyzer(),
504
- new S3UnencryptedAnalyzer(),
505
- ]
506
- : []),
507
- ...(iacDriftAnalyzer ? [iacDriftAnalyzer] : []),
508
- pipelineAnalyzer,
509
- ];
298
+ const analyzers = buildAnalyzers(config, iacDriftAnalyzer, iacLambdas);
510
299
  const findings = await runAllAnalyzers(graph, analyzers);
511
300
  writeCache('graph', graph);
512
301
  writeCache('findings', findings);
@@ -1,9 +1,192 @@
1
- import { runDev } from './dev.js';
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { loadConfig, readCache, setCacheDir } from '../../core/index.js';
6
+ import { createServer, setGraphState, setConfigured } from '../../server/index.js';
7
+ import { log, printHeader } from '../utils.js';
8
+ import { runAnalyze, runCodeRefresh } from './analyze.js';
2
9
  import { runStdio } from './stdio.js';
10
+ const BOX_W = 52;
11
+ const TOOL_MAP = [
12
+ { name: 'get_infra_overview' },
13
+ { name: 'get_graph_summary' },
14
+ { name: 'analyze_function' },
15
+ { name: 'suggest_gsi', service: 'dynamodb' },
16
+ { name: 'postgres_index_suggestions', service: 'postgres' },
17
+ { name: 'suggest_mongo_index', service: 'mongodb' },
18
+ { name: 'mysql_index_suggestions', service: 'mysql' },
19
+ { name: 'get_queue_details', service: 'sqs' },
20
+ { name: 'get_topic_details', service: 'sns' },
21
+ { name: 'get_secrets_overview', service: 'secretsManager' },
22
+ { name: 'get_parameter_overview', service: 'ssm' },
23
+ { name: 'get_lambda_overview', service: 'lambda' },
24
+ { name: 'get_eventbridge_details', service: 'eventbridge' },
25
+ { name: 'get_s3_overview', service: 's3' },
26
+ { name: 'get_api_routes', service: 'apiGateway' },
27
+ { name: 'get_log_errors', service: 'cloudwatchLogs' },
28
+ ];
29
+ // With no config every registered tool is shown as active (the server exposes
30
+ // all of them); a config narrows the list to its enabled services.
31
+ function isEnabled(cfg, service) {
32
+ if (!service || !cfg)
33
+ return true;
34
+ const svc = cfg[service];
35
+ return svc?.enabled === true;
36
+ }
37
+ function boxLine(visibleContent, coloredContent) {
38
+ const padding = ' '.repeat(Math.max(0, BOX_W - visibleContent.length));
39
+ console.log(chalk.dim(' │') + coloredContent + padding + chalk.dim('│'));
40
+ }
41
+ function boxDivider() {
42
+ console.log(chalk.dim(' ├────────────────────────────────────────────────────┤'));
43
+ }
44
+ function groupTools(tools) {
45
+ const lines = [];
46
+ let i = 0;
47
+ while (i < tools.length) {
48
+ const a = tools[i];
49
+ const b = tools[i + 1];
50
+ if (b && ` ${a} · ${b}`.length <= BOX_W) {
51
+ lines.push(`${a} · ${b}`);
52
+ i += 2;
53
+ }
54
+ else {
55
+ lines.push(a);
56
+ i++;
57
+ }
58
+ }
59
+ return lines;
60
+ }
3
61
  export async function runServe(options = {}) {
4
62
  if (options.stdio) {
5
63
  await runStdio(options.config);
6
64
  return;
7
65
  }
8
- await runDev({ config: options.config, port: options.port });
66
+ const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
67
+ printHeader('MCP Server');
68
+ setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
69
+ // A hosted MCP runtime may launch the server with no infrawise.yaml. Start
70
+ // anyway with an empty graph so the host can connect and list tools.
71
+ let config;
72
+ try {
73
+ config = loadConfig(options.config);
74
+ log.success('Config loaded', options.config ?? 'infrawise.yaml');
75
+ }
76
+ catch (err) {
77
+ log.warn(`Starting with empty graph (no config loaded): ${err instanceof Error ? err.message : String(err)}`);
78
+ }
79
+ setConfigured(config !== undefined);
80
+ const repoPath = process.cwd();
81
+ // Auto-analyze if no cache
82
+ const cachedGraph = readCache('graph');
83
+ const cachedFindings = readCache('findings');
84
+ if (cachedGraph && cachedFindings) {
85
+ log.success('Cached analysis loaded', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
86
+ setGraphState(cachedGraph, cachedFindings);
87
+ }
88
+ else if (config) {
89
+ log.warn('No cache found — running analysis now...');
90
+ console.log('');
91
+ await runAnalyze({ repo: repoPath, config: options.config });
92
+ const freshGraph = readCache('graph') ?? { nodes: [], edges: [] };
93
+ const freshFindings = readCache('findings') ?? [];
94
+ setGraphState(freshGraph, freshFindings);
95
+ }
96
+ else {
97
+ setGraphState({ nodes: [], edges: [] }, []);
98
+ }
99
+ console.log('');
100
+ // Start server
101
+ const spin = ora({ text: chalk.dim('Starting server...'), color: 'cyan' }).start();
102
+ const { start } = createServer(port);
103
+ await start();
104
+ spin.succeed(chalk.green('Server running'));
105
+ // Compute active/inactive tools from config
106
+ const activeTools = TOOL_MAP.filter((t) => isEnabled(config, t.service)).map((t) => t.name);
107
+ const inactiveTools = TOOL_MAP.filter((t) => !isEnabled(config, t.service)).map((t) => t.name);
108
+ // URL rows
109
+ const mcpUrl = `http://localhost:${port}/mcp`;
110
+ const healthUrl = `http://localhost:${port}/health`;
111
+ // Print box
112
+ console.log('');
113
+ console.log(chalk.dim(' ┌────────────────────────────────────────────────────┐'));
114
+ boxLine(' MCP Server', chalk.bold(' MCP Server'));
115
+ boxDivider();
116
+ boxLine(` POST ${mcpUrl}`, ` ${chalk.dim('POST')} ${chalk.cyan(mcpUrl)}`);
117
+ boxLine(` GET ${healthUrl}`, ` ${chalk.dim('GET')} ${chalk.cyan(healthUrl)}`);
118
+ boxDivider();
119
+ const activeLabel = ` Tools (${activeTools.length} active${inactiveTools.length > 0 ? ` · ${inactiveTools.length} off` : ''})`;
120
+ boxLine(activeLabel, chalk.dim(activeLabel));
121
+ for (const line of groupTools(activeTools)) {
122
+ boxLine(` ${line}`, ` ${line}`);
123
+ }
124
+ if (inactiveTools.length > 0) {
125
+ boxDivider();
126
+ boxLine(' Off (enable in infrawise.yaml):', chalk.dim(' Off (enable in infrawise.yaml):'));
127
+ for (const line of groupTools(inactiveTools)) {
128
+ boxLine(` ${line}`, chalk.dim(` ${line}`));
129
+ }
130
+ }
131
+ console.log(chalk.dim(' └────────────────────────────────────────────────────┘'));
132
+ console.log('');
133
+ console.log(chalk.dim(' Add via CLI:'));
134
+ console.log(chalk.dim(` claude mcp add --transport http infrawise ${mcpUrl}`));
135
+ console.log('');
136
+ console.log(chalk.dim(' Watching for file changes... Press Ctrl+C to stop\n'));
137
+ // File watch — re-run code analysis on save (needs a config to drive analyzers)
138
+ if (config) {
139
+ const cfg = config;
140
+ let debounceTimer = null;
141
+ let refreshing = false;
142
+ const configFile = path.resolve(options.config ?? 'infrawise.yaml');
143
+ try {
144
+ fs.watch(repoPath, { recursive: true }, (_, filename) => {
145
+ if (!filename)
146
+ return;
147
+ const abs = path.join(repoPath, filename);
148
+ if (abs === configFile) {
149
+ console.log(chalk.dim('\n infrawise.yaml changed — restart to apply config changes\n'));
150
+ return;
151
+ }
152
+ const ext = path.extname(filename);
153
+ if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
154
+ return;
155
+ if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
156
+ return;
157
+ if (debounceTimer)
158
+ clearTimeout(debounceTimer);
159
+ debounceTimer = setTimeout(async () => {
160
+ if (refreshing)
161
+ return;
162
+ refreshing = true;
163
+ const spin = ora({
164
+ text: chalk.dim('Refreshing code analysis...'),
165
+ color: 'cyan',
166
+ }).start();
167
+ try {
168
+ const { graph, findings } = await runCodeRefresh(repoPath, cfg);
169
+ setGraphState(graph, findings);
170
+ spin.succeed(chalk.green('Analysis refreshed') +
171
+ chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
172
+ }
173
+ catch (err) {
174
+ spin.warn(chalk.yellow('Refresh failed') +
175
+ chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
176
+ }
177
+ finally {
178
+ refreshing = false;
179
+ }
180
+ }, 2000);
181
+ });
182
+ }
183
+ catch {
184
+ // fs.watch may not support recursive on all platforms — silently skip
185
+ }
186
+ }
187
+ process.on('SIGINT', () => {
188
+ console.log(chalk.dim('\n Shutting down...\n'));
189
+ process.exit(0);
190
+ });
191
+ await new Promise(() => { });
9
192
  }
@@ -1,72 +1,81 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { loadConfig, formatError, readCache, setCacheDir } from '../../core/index.js';
5
- import { createMcpServer, setGraphState } from '../../server/index.js';
4
+ import { loadConfig, readCache, setCacheDir } from '../../core/index.js';
5
+ import { createMcpServer, setGraphState, setConfigured } from '../../server/index.js';
6
6
  import { runAnalyze, runCodeRefresh } from './analyze.js';
7
7
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
8
8
  export async function runStdio(configPath) {
9
+ const resolvedConfigPath = configPath ?? 'infrawise.yaml';
10
+ setCacheDir(path.dirname(path.resolve(resolvedConfigPath)));
11
+ // A hosted MCP runtime (e.g. Glama) launches the server with no infrawise.yaml.
12
+ // Start anyway with an empty graph so the host can connect and list tools;
13
+ // analysis populates once a config is present.
9
14
  let config;
10
15
  try {
11
16
  config = loadConfig(configPath);
12
17
  }
13
18
  catch (err) {
14
- process.stderr.write(formatError(err) + '\n');
15
- process.exit(1);
19
+ process.stderr.write(`infrawise: starting with empty graph (no config loaded: ${err instanceof Error ? err.message : String(err)})\n`);
16
20
  }
17
- const resolvedConfigPath = configPath ?? 'infrawise.yaml';
18
- setCacheDir(path.dirname(path.resolve(resolvedConfigPath)));
21
+ setConfigured(config !== undefined);
19
22
  const cachedGraph = readCache('graph', CACHE_TTL_MS);
20
23
  const cachedFindings = readCache('findings', CACHE_TTL_MS);
21
24
  if (cachedGraph && cachedFindings) {
22
- setGraphState(cachedGraph, cachedFindings, config);
25
+ setGraphState(cachedGraph, cachedFindings);
23
26
  }
24
- else {
27
+ else if (config) {
25
28
  await runAnalyze({ config: configPath });
26
29
  const graph = readCache('graph', CACHE_TTL_MS) ?? { nodes: [], edges: [] };
27
30
  const findings = readCache('findings', CACHE_TTL_MS) ?? [];
28
- setGraphState(graph, findings, config);
31
+ setGraphState(graph, findings);
29
32
  }
30
- // File watching — re-run code analysis on save (no AWS calls, instant)
31
- // stderr is safe in stdio transport; stdout is reserved for MCP JSON-RPC
32
- const repoPath = process.cwd();
33
- const configFile = path.resolve(resolvedConfigPath);
34
- let debounceTimer = null;
35
- let refreshing = false;
36
- try {
37
- fs.watch(repoPath, { recursive: true }, (_, filename) => {
38
- if (!filename)
39
- return;
40
- const abs = path.join(repoPath, filename);
41
- if (abs === configFile)
42
- return;
43
- const ext = path.extname(filename);
44
- if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
45
- return;
46
- if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
47
- return;
48
- if (debounceTimer)
49
- clearTimeout(debounceTimer);
50
- debounceTimer = setTimeout(async () => {
51
- if (refreshing)
52
- return;
53
- refreshing = true;
54
- try {
55
- const { graph, findings } = await runCodeRefresh(repoPath, config);
56
- setGraphState(graph, findings, config);
57
- process.stderr.write(`infrawise: code graph refreshed (${graph.nodes.length} nodes · ${findings.length} finding(s))\n`);
58
- }
59
- catch {
60
- // don't break the MCP connection on watcher errors
61
- }
62
- finally {
63
- refreshing = false;
64
- }
65
- }, 2000);
66
- });
33
+ else {
34
+ setGraphState({ nodes: [], edges: [] }, []);
67
35
  }
68
- catch {
69
- // fs.watch recursive not supported on all platforms silently skip
36
+ // File watching drives runCodeRefresh, which needs a config — skip it without one.
37
+ // stderr is safe in stdio transport; stdout is reserved for MCP JSON-RPC.
38
+ if (config) {
39
+ const cfg = config;
40
+ const repoPath = process.cwd();
41
+ const configFile = path.resolve(resolvedConfigPath);
42
+ let debounceTimer = null;
43
+ let refreshing = false;
44
+ try {
45
+ fs.watch(repoPath, { recursive: true }, (_, filename) => {
46
+ if (!filename)
47
+ return;
48
+ const abs = path.join(repoPath, filename);
49
+ if (abs === configFile)
50
+ return;
51
+ const ext = path.extname(filename);
52
+ if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
53
+ return;
54
+ if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
55
+ return;
56
+ if (debounceTimer)
57
+ clearTimeout(debounceTimer);
58
+ debounceTimer = setTimeout(async () => {
59
+ if (refreshing)
60
+ return;
61
+ refreshing = true;
62
+ try {
63
+ const { graph, findings } = await runCodeRefresh(repoPath, cfg);
64
+ setGraphState(graph, findings);
65
+ process.stderr.write(`infrawise: code graph refreshed (${graph.nodes.length} nodes · ${findings.length} finding(s))\n`);
66
+ }
67
+ catch {
68
+ // don't break the MCP connection on watcher errors
69
+ }
70
+ finally {
71
+ refreshing = false;
72
+ }
73
+ }, 2000);
74
+ });
75
+ }
76
+ catch {
77
+ // fs.watch recursive not supported on all platforms — silently skip
78
+ }
70
79
  }
71
80
  const mcp = createMcpServer();
72
81
  const transport = new StdioServerTransport();
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as yaml from 'js-yaml';
5
+ import { InfrawiseError } from './errors.js';
5
6
  export const InfrawiseConfigSchema = z.object({
6
7
  project: z.string().min(1, 'Project name is required'),
7
8
  aws: z
@@ -74,11 +75,9 @@ export const InfrawiseConfigSchema = z.object({
74
75
  })
75
76
  .optional(),
76
77
  });
77
- export class ConfigError extends Error {
78
- details;
78
+ export class ConfigError extends InfrawiseError {
79
79
  constructor(message, details) {
80
- super(message);
81
- this.details = details;
80
+ super(message, details, 'infrawise start');
82
81
  this.name = 'ConfigError';
83
82
  }
84
83
  }
@@ -183,13 +182,18 @@ export function generateDefaultConfig(projectName, options) {
183
182
  secretsManager: { enabled: options?.secretsManager?.enabled ?? true },
184
183
  lambda: { enabled: options?.lambda?.enabled ?? true },
185
184
  eventbridge: { enabled: options?.eventbridge?.enabled ?? true },
186
- rds: { enabled: options?.rds?.enabled ?? true },
185
+ rds: { enabled: options?.rds?.enabled ?? false },
186
+ s3: { enabled: options?.s3?.enabled ?? false },
187
+ apiGateway: { enabled: options?.apiGateway?.enabled ?? false },
187
188
  cloudwatchLogs: {
188
189
  enabled: options?.cloudwatchLogs?.enabled ?? false,
189
190
  logGroupPrefixes: options?.cloudwatchLogs?.logGroupPrefixes ?? [],
190
191
  windowHours: options?.cloudwatchLogs?.windowHours ?? 24,
191
192
  },
192
- analysis: { sampleSize: options?.analysis?.sampleSize ?? 100 },
193
+ analysis: {
194
+ sampleSize: options?.analysis?.sampleSize ?? 100,
195
+ hotPartitionThreshold: options?.analysis?.hotPartitionThreshold ?? 5,
196
+ },
193
197
  };
194
198
  return yaml.dump(config, { lineWidth: 120 });
195
199
  }
@@ -22,17 +22,6 @@ export class InfrawiseError extends Error {
22
22
  return lines.join('\n');
23
23
  }
24
24
  }
25
- export class AWSConnectionError extends InfrawiseError {
26
- constructor(details) {
27
- super('Unable to connect to AWS.', [
28
- 'Invalid or missing AWS credentials',
29
- 'Incorrect AWS profile specified',
30
- 'Network connectivity issues',
31
- details ?? 'Unexpected AWS error',
32
- ], 'infrawise doctor');
33
- this.name = 'AWSConnectionError';
34
- }
35
- }
36
25
  export class DynamoDBError extends InfrawiseError {
37
26
  constructor(details) {
38
27
  super('Unable to access DynamoDB.', [
@@ -66,16 +55,6 @@ export class RepositoryScanError extends InfrawiseError {
66
55
  this.name = 'RepositoryScanError';
67
56
  }
68
57
  }
69
- export class ConfigError extends InfrawiseError {
70
- constructor(details) {
71
- super('Invalid or missing configuration.', [
72
- 'infrawise.yaml not found in current directory',
73
- 'Missing required fields in configuration',
74
- details ?? 'Unexpected config error',
75
- ], 'infrawise start');
76
- this.name = 'ConfigError';
77
- }
78
- }
79
58
  export function formatError(err) {
80
59
  if (err instanceof InfrawiseError) {
81
60
  return err.format();
@@ -1,4 +1,4 @@
1
- export { loadConfig, generateDefaultConfig, InfrawiseConfigSchema, ConfigError as ConfigValidationError, } from './config.js';
1
+ export { loadConfig, generateDefaultConfig, InfrawiseConfigSchema, ConfigError } from './config.js';
2
2
  export { logger } from './logger.js';
3
- export { InfrawiseError, AWSConnectionError, DynamoDBError, PostgresConnectionError, RepositoryScanError, ConfigError, formatError, } from './errors.js';
3
+ export { InfrawiseError, DynamoDBError, PostgresConnectionError, RepositoryScanError, formatError, } from './errors.js';
4
4
  export { writeCache, readCache, clearCache, setCacheDir } from './cache.js';
@@ -12,10 +12,17 @@ import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecre
12
12
  // ── State ────────────────────────────────────────────────────────────────────
13
13
  let currentGraph = { nodes: [], edges: [] };
14
14
  let currentFindings = [];
15
- export function setGraphState(graph, findings, _config) {
15
+ // False when the server booted without an infrawise.yaml (e.g. a hosted MCP
16
+ // runtime). Used to return a "run locally" hint instead of a bare empty graph.
17
+ let configured = true;
18
+ export function setGraphState(graph, findings) {
16
19
  currentGraph = graph;
17
20
  currentFindings = findings;
18
21
  }
22
+ export function setConfigured(value) {
23
+ configured = value;
24
+ }
25
+ const NOT_CONFIGURED_HINT = 'No infrastructure loaded. infrawise reads your live infra locally — run `npx infrawise start` in your project (with AWS credentials and an infrawise.yaml) so these tools return your real DynamoDB/RDS/SQS/Lambda/etc. context. A remotely hosted instance has no access to your cloud account or code, so it returns empty results by design.';
19
26
  // ── Helpers ──────────────────────────────────────────────────────────────────
20
27
  function toText(data) {
21
28
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
@@ -31,7 +38,7 @@ function logged(name, fn) {
31
38
  export function createMcpServer() {
32
39
  const mcp = new McpServer({ name: 'infrawise', version });
33
40
  mcp.registerTool('get_infra_overview', {
34
- description: 'Returns a compact infrastructure snapshot: service counts, all databases, queues, topics, secrets, lambdas, and high-severity findings. Call this first at the start of any database or infrastructure task to understand what services are in scope. Prefer this over get_graph_summary for quick orientation; use get_graph_summary only when you need every node, edge, and finding in full.',
41
+ description: 'Returns a compact infrastructure snapshot: service counts, all databases, queues, topics, secrets, lambdas, and high-severity findings. Call this first at the start of any database or infrastructure task to understand what services are in scope. Prefer this over get_graph_summary for quick orientation; use get_graph_summary only when you need every node, edge, and finding in full. Also returns a `configured` flag — when false, the server has no infrawise.yaml loaded (e.g. a remotely hosted instance) and all tools return empty results; a `setupHint` explains how to run infrawise locally.',
35
42
  inputSchema: z.object({}),
36
43
  }, logged('get_infra_overview', async () => {
37
44
  const tables = getTableNodes(currentGraph);
@@ -44,6 +51,8 @@ export function createMcpServer() {
44
51
  const functions = getFunctionNodes(currentGraph);
45
52
  const buckets = getBucketNodes(currentGraph);
46
53
  return toText({
54
+ configured,
55
+ ...(configured ? {} : { setupHint: NOT_CONFIGURED_HINT }),
47
56
  summary: {
48
57
  tables: tables.length,
49
58
  functions: functions.length,
@@ -460,6 +469,7 @@ export function createServer(port = 3000) {
460
469
  'get_lambda_overview',
461
470
  'get_eventbridge_details',
462
471
  'get_s3_overview',
472
+ 'get_api_routes',
463
473
  'get_log_errors',
464
474
  ],
465
475
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "mcpName": "io.github.Sidd27/infrawise",
5
5
  "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
6
6
  "keywords": [
@@ -1,172 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import chalk from 'chalk';
4
- import ora from 'ora';
5
- import { loadConfig, formatError, readCache, setCacheDir } from '../../core/index.js';
6
- import { createServer, setGraphState } from '../../server/index.js';
7
- import { log, printHeader } from '../utils.js';
8
- import { runAnalyze, runCodeRefresh } from './analyze.js';
9
- const BOX_W = 52;
10
- const TOOL_MAP = [
11
- { name: 'get_infra_overview' },
12
- { name: 'get_graph_summary' },
13
- { name: 'analyze_function' },
14
- { name: 'suggest_gsi', service: 'dynamodb' },
15
- { name: 'postgres_index_suggestions', service: 'postgres' },
16
- { name: 'suggest_mongo_index', service: 'mongodb' },
17
- { name: 'mysql_index_suggestions', service: 'mysql' },
18
- { name: 'get_queue_details', service: 'sqs' },
19
- { name: 'get_topic_details', service: 'sns' },
20
- { name: 'get_secrets_overview', service: 'secretsManager' },
21
- { name: 'get_parameter_overview', service: 'ssm' },
22
- { name: 'get_lambda_overview', service: 'lambda' },
23
- { name: 'get_eventbridge_details', service: 'eventbridge' },
24
- { name: 'get_log_errors', service: 'cloudwatchLogs' },
25
- ];
26
- function isEnabled(cfg, service) {
27
- if (!service)
28
- return true;
29
- const svc = cfg[service];
30
- return svc?.enabled === true;
31
- }
32
- function boxLine(visibleContent, coloredContent) {
33
- const padding = ' '.repeat(Math.max(0, BOX_W - visibleContent.length));
34
- console.log(chalk.dim(' │') + coloredContent + padding + chalk.dim('│'));
35
- }
36
- function boxDivider() {
37
- console.log(chalk.dim(' ├────────────────────────────────────────────────────┤'));
38
- }
39
- function groupTools(tools) {
40
- const lines = [];
41
- let i = 0;
42
- while (i < tools.length) {
43
- const a = tools[i];
44
- const b = tools[i + 1];
45
- if (b && ` ${a} · ${b}`.length <= BOX_W) {
46
- lines.push(`${a} · ${b}`);
47
- i += 2;
48
- }
49
- else {
50
- lines.push(a);
51
- i++;
52
- }
53
- }
54
- return lines;
55
- }
56
- export async function runDev(options = {}) {
57
- const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
58
- printHeader('MCP Server');
59
- let config;
60
- try {
61
- config = loadConfig(options.config);
62
- setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
63
- log.success('Config loaded', options.config ?? 'infrawise.yaml');
64
- }
65
- catch (err) {
66
- console.error(formatError(err));
67
- process.exit(1);
68
- }
69
- const repoPath = process.cwd();
70
- // Auto-analyze if no cache
71
- const cachedGraph = readCache('graph');
72
- const cachedFindings = readCache('findings');
73
- if (cachedGraph && cachedFindings) {
74
- log.success('Cached analysis loaded', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
75
- setGraphState(cachedGraph, cachedFindings, config);
76
- }
77
- else {
78
- log.warn('No cache found — running analysis now...');
79
- console.log('');
80
- await runAnalyze({ repo: repoPath, config: options.config });
81
- const freshGraph = readCache('graph') ?? { nodes: [], edges: [] };
82
- const freshFindings = readCache('findings') ?? [];
83
- setGraphState(freshGraph, freshFindings, config);
84
- }
85
- console.log('');
86
- // Start server
87
- const spin = ora({ text: chalk.dim('Starting server...'), color: 'cyan' }).start();
88
- const { start } = createServer(port);
89
- await start();
90
- spin.succeed(chalk.green('Server running'));
91
- // Compute active/inactive tools from config
92
- const activeTools = TOOL_MAP.filter((t) => isEnabled(config, t.service)).map((t) => t.name);
93
- const inactiveTools = TOOL_MAP.filter((t) => !isEnabled(config, t.service)).map((t) => t.name);
94
- // URL rows
95
- const mcpUrl = `http://localhost:${port}/mcp`;
96
- const healthUrl = `http://localhost:${port}/health`;
97
- // Print box
98
- console.log('');
99
- console.log(chalk.dim(' ┌────────────────────────────────────────────────────┐'));
100
- boxLine(' MCP Server', chalk.bold(' MCP Server'));
101
- boxDivider();
102
- boxLine(` POST ${mcpUrl}`, ` ${chalk.dim('POST')} ${chalk.cyan(mcpUrl)}`);
103
- boxLine(` GET ${healthUrl}`, ` ${chalk.dim('GET')} ${chalk.cyan(healthUrl)}`);
104
- boxDivider();
105
- const activeLabel = ` Tools (${activeTools.length} active${inactiveTools.length > 0 ? ` · ${inactiveTools.length} off` : ''})`;
106
- boxLine(activeLabel, chalk.dim(activeLabel));
107
- for (const line of groupTools(activeTools)) {
108
- boxLine(` ${line}`, ` ${line}`);
109
- }
110
- if (inactiveTools.length > 0) {
111
- boxDivider();
112
- boxLine(' Off (enable in infrawise.yaml):', chalk.dim(' Off (enable in infrawise.yaml):'));
113
- for (const line of groupTools(inactiveTools)) {
114
- boxLine(` ${line}`, chalk.dim(` ${line}`));
115
- }
116
- }
117
- console.log(chalk.dim(' └────────────────────────────────────────────────────┘'));
118
- console.log('');
119
- console.log(chalk.dim(' Add via CLI:'));
120
- console.log(chalk.dim(` claude mcp add --transport http infrawise ${mcpUrl}`));
121
- console.log('');
122
- console.log(chalk.dim(' Watching for file changes... Press Ctrl+C to stop\n'));
123
- // File watch — re-run code analysis on save, skip slow AWS/DB extraction
124
- let debounceTimer = null;
125
- let refreshing = false;
126
- const configFile = path.resolve(options.config ?? 'infrawise.yaml');
127
- try {
128
- fs.watch(repoPath, { recursive: true }, (_, filename) => {
129
- if (!filename)
130
- return;
131
- const abs = path.join(repoPath, filename);
132
- if (abs === configFile) {
133
- console.log(chalk.dim('\n infrawise.yaml changed — restart to apply config changes\n'));
134
- return;
135
- }
136
- const ext = path.extname(filename);
137
- if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
138
- return;
139
- if (filename.includes('node_modules') || filename.startsWith('.infrawise'))
140
- return;
141
- if (debounceTimer)
142
- clearTimeout(debounceTimer);
143
- debounceTimer = setTimeout(async () => {
144
- if (refreshing)
145
- return;
146
- refreshing = true;
147
- const spin = ora({ text: chalk.dim('Refreshing code analysis...'), color: 'cyan' }).start();
148
- try {
149
- const { graph, findings } = await runCodeRefresh(repoPath, config);
150
- setGraphState(graph, findings, config);
151
- spin.succeed(chalk.green('Analysis refreshed') +
152
- chalk.dim(` ${graph.nodes.length} nodes · ${findings.length} finding(s)`));
153
- }
154
- catch (err) {
155
- spin.warn(chalk.yellow('Refresh failed') +
156
- chalk.dim(` ${err instanceof Error ? err.message : String(err)}`));
157
- }
158
- finally {
159
- refreshing = false;
160
- }
161
- }, 2000);
162
- });
163
- }
164
- catch {
165
- // fs.watch may not support recursive on all platforms — silently skip
166
- }
167
- process.on('SIGINT', () => {
168
- console.log(chalk.dim('\n Shutting down...\n'));
169
- process.exit(0);
170
- });
171
- await new Promise(() => { });
172
- }