infrawise 0.10.3 → 0.12.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/README.md +62 -28
- package/dist/adapters/aws/dynamodb.js +0 -3
- package/dist/adapters/aws/logs.js +0 -2
- package/dist/adapters/aws/s3.js +0 -16
- package/dist/adapters/aws/services.js +106 -16
- package/dist/adapters/iac/terraform.js +2 -0
- package/dist/analyzers/aws-services.js +36 -0
- package/dist/analyzers/dynamodb.js +9 -5
- package/dist/analyzers/index.js +2 -1
- package/dist/analyzers/linkers.js +106 -0
- package/dist/analyzers/pipeline.js +166 -0
- package/dist/cli/commands/analyze.js +47 -6
- package/dist/cli/commands/check.js +28 -0
- package/dist/cli/commands/discover.js +201 -0
- package/dist/cli/commands/doctor.js +1 -2
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/start.js +24 -12
- package/dist/cli/index.js +32 -26
- package/dist/cli/{commands/init.js → interactive-setup.js} +6 -27
- package/dist/cli/probe.js +44 -0
- package/dist/cli/utils.js +23 -32
- package/dist/core/config.js +39 -5
- package/dist/core/errors.js +1 -1
- package/dist/graph/index.js +21 -0
- package/dist/server/index.js +22 -2
- package/package.json +10 -20
- package/dist/cli/commands/auth.js +0 -54
package/dist/cli/index.js
CHANGED
|
@@ -3,10 +3,9 @@ import { readFileSync } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { printBanner } from './utils.js';
|
|
6
|
-
import { runInit } from './commands/init.js';
|
|
7
|
-
import { runAuth } from './commands/auth.js';
|
|
8
6
|
import { runAnalyze } from './commands/analyze.js';
|
|
9
|
-
import {
|
|
7
|
+
import { runCheck } from './commands/check.js';
|
|
8
|
+
import { runServe } from './commands/serve.js';
|
|
10
9
|
import { runDoctor } from './commands/doctor.js';
|
|
11
10
|
import { runStdio } from './commands/stdio.js';
|
|
12
11
|
import { runStart } from './commands/start.js';
|
|
@@ -18,33 +17,22 @@ program
|
|
|
18
17
|
.version(version);
|
|
19
18
|
program
|
|
20
19
|
.command('start')
|
|
21
|
-
.description('
|
|
20
|
+
.description('Probe environment, generate config, analyze, and connect your editor')
|
|
22
21
|
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
23
22
|
.option('--claude', 'Write .mcp.json and open Claude Code')
|
|
24
23
|
.option('--cursor', 'Write .cursor/mcp.json and open Cursor')
|
|
24
|
+
.option('--interactive', 'Run interactive setup wizard instead of auto-discovery')
|
|
25
|
+
.option('--rediscover', 'Delete existing infrawise.yaml and re-probe the environment')
|
|
25
26
|
.action(async (options) => {
|
|
26
27
|
printBanner();
|
|
27
28
|
await runStart({
|
|
28
29
|
config: options.config !== 'infrawise.yaml' ? options.config : undefined,
|
|
29
30
|
claude: options.claude,
|
|
30
31
|
cursor: options.cursor,
|
|
32
|
+
interactive: options.interactive,
|
|
33
|
+
rediscover: options.rediscover,
|
|
31
34
|
});
|
|
32
35
|
});
|
|
33
|
-
program
|
|
34
|
-
.command('init')
|
|
35
|
-
.description('Detect AWS profile/region, ask setup questions, and generate infrawise.yaml')
|
|
36
|
-
.option('--force', 'Overwrite existing infrawise.yaml')
|
|
37
|
-
.action(async (options) => {
|
|
38
|
-
printBanner();
|
|
39
|
-
await runInit({ force: options.force });
|
|
40
|
-
});
|
|
41
|
-
program
|
|
42
|
-
.command('auth')
|
|
43
|
-
.description('Validate and select AWS profile from ~/.aws/credentials')
|
|
44
|
-
.action(async () => {
|
|
45
|
-
printBanner();
|
|
46
|
-
await runAuth();
|
|
47
|
-
});
|
|
48
36
|
program
|
|
49
37
|
.command('analyze')
|
|
50
38
|
.description('Load config, run extractors, build graph, and run all analyzers')
|
|
@@ -64,27 +52,45 @@ program
|
|
|
64
52
|
});
|
|
65
53
|
});
|
|
66
54
|
program
|
|
67
|
-
.command('
|
|
68
|
-
.description('
|
|
55
|
+
.command('check')
|
|
56
|
+
.description('CI gate: analyze and exit non-zero if findings reach the threshold severity')
|
|
69
57
|
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
70
|
-
.option('-
|
|
58
|
+
.option('-r, --repo <path>', 'Path to repository to scan', process.cwd())
|
|
59
|
+
.option('--fail-on <level>', 'Severity that fails the build: high | medium | low', 'high')
|
|
71
60
|
.action(async (options) => {
|
|
72
61
|
printBanner();
|
|
73
|
-
await
|
|
62
|
+
await runCheck({
|
|
63
|
+
config: options.config !== 'infrawise.yaml' ? options.config : undefined,
|
|
64
|
+
repo: options.repo,
|
|
65
|
+
failOn: options.failOn,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
program
|
|
69
|
+
.command('serve')
|
|
70
|
+
.description('Start the MCP server — HTTP by default, or stdio for editor integration')
|
|
71
|
+
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
72
|
+
.option('--stdio', 'Use stdio transport (for editors via .mcp.json) instead of HTTP')
|
|
73
|
+
.option('-p, --port <number>', 'Port to listen on (HTTP only)', '3000')
|
|
74
|
+
.action(async (options) => {
|
|
75
|
+
if (!options.stdio)
|
|
76
|
+
printBanner();
|
|
77
|
+
await runServe({
|
|
74
78
|
config: options.config !== 'infrawise.yaml' ? options.config : undefined,
|
|
79
|
+
stdio: options.stdio,
|
|
75
80
|
port: parseInt(options.port, 10),
|
|
76
81
|
});
|
|
77
82
|
});
|
|
83
|
+
// Hidden backcompat alias: editors launched from a .mcp.json generated before
|
|
84
|
+
// `serve` existed still invoke `infrawise stdio`. Kept out of --help.
|
|
78
85
|
program
|
|
79
|
-
.command('stdio')
|
|
80
|
-
.description('Start MCP server on stdio transport — used by editors via .mcp.json (auto-managed)')
|
|
86
|
+
.command('stdio', { hidden: true })
|
|
81
87
|
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
82
88
|
.action(async (options) => {
|
|
83
89
|
await runStdio(options.config !== 'infrawise.yaml' ? options.config : undefined);
|
|
84
90
|
});
|
|
85
91
|
program
|
|
86
92
|
.command('doctor')
|
|
87
|
-
.description('
|
|
93
|
+
.description('Diagnostic escape hatch: validate AWS/DB access, config, and repo scan')
|
|
88
94
|
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
89
95
|
.action(async (options) => {
|
|
90
96
|
printBanner();
|
|
@@ -2,8 +2,8 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
-
import { generateDefaultConfig } from '
|
|
6
|
-
import { readAWSProfiles, detectAWSRegion,
|
|
5
|
+
import { generateDefaultConfig } from '../core/index.js';
|
|
6
|
+
import { readAWSProfiles, detectAWSRegion, log, printHeader } from './utils.js';
|
|
7
7
|
export async function runInit(options = {}) {
|
|
8
8
|
const cwd = process.cwd();
|
|
9
9
|
const configPath = path.join(cwd, 'infrawise.yaml');
|
|
@@ -12,12 +12,10 @@ export async function runInit(options = {}) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
printHeader('Initialize Infrawise');
|
|
15
|
-
const repoType = detectRepoType(cwd);
|
|
16
15
|
const repoName = path.basename(cwd);
|
|
17
16
|
const profiles = readAWSProfiles();
|
|
18
17
|
const detectedRegion = detectAWSRegion();
|
|
19
18
|
log.success(`Repository detected`, repoName);
|
|
20
|
-
log.success(`Type`, repoType);
|
|
21
19
|
log.success(`AWS profiles found`, String(profiles.length));
|
|
22
20
|
console.log('');
|
|
23
21
|
// ── Core settings ──────────────────────────────────────────────────────────
|
|
@@ -37,7 +35,6 @@ export async function runInit(options = {}) {
|
|
|
37
35
|
message: 'Infrastructure:',
|
|
38
36
|
choices: [
|
|
39
37
|
{ name: 'AWS', value: 'aws' },
|
|
40
|
-
{ name: 'LocalStack (mimics AWS locally)', value: 'localstack' },
|
|
41
38
|
{ name: 'Local (no cloud — databases, queues, self-hosted services)', value: 'local' },
|
|
42
39
|
],
|
|
43
40
|
},
|
|
@@ -59,12 +56,8 @@ export async function runInit(options = {}) {
|
|
|
59
56
|
]);
|
|
60
57
|
awsProfile = answer.awsProfile;
|
|
61
58
|
}
|
|
62
|
-
|
|
63
|
-
awsProfile = '__localstack__';
|
|
64
|
-
}
|
|
65
|
-
// ── Step 3: region + endpoint ─────────────────────────────────────────────
|
|
59
|
+
// ── Step 3: region ────────────────────────────────────────────────────────
|
|
66
60
|
let region = detectedRegion;
|
|
67
|
-
let endpoint;
|
|
68
61
|
if (provider !== 'local') {
|
|
69
62
|
const regionAnswer = await inquirer.prompt([
|
|
70
63
|
{
|
|
@@ -75,19 +68,8 @@ export async function runInit(options = {}) {
|
|
|
75
68
|
},
|
|
76
69
|
]);
|
|
77
70
|
region = regionAnswer.region;
|
|
78
|
-
if (provider === 'localstack') {
|
|
79
|
-
const endpointAnswer = await inquirer.prompt([
|
|
80
|
-
{
|
|
81
|
-
type: 'input',
|
|
82
|
-
name: 'endpoint',
|
|
83
|
-
message: 'LocalStack endpoint:',
|
|
84
|
-
default: 'http://localhost:4566',
|
|
85
|
-
},
|
|
86
|
-
]);
|
|
87
|
-
endpoint = endpointAnswer.endpoint;
|
|
88
|
-
}
|
|
89
71
|
}
|
|
90
|
-
const core = { project, awsProfile, region
|
|
72
|
+
const core = { project, awsProfile, region };
|
|
91
73
|
// ── Databases ──────────────────────────────────────────────────────────────
|
|
92
74
|
console.log('\n ' + chalk.bold('Databases'));
|
|
93
75
|
console.log(chalk.dim(' Self-hosted databases (PostgreSQL, MySQL, MongoDB).'));
|
|
@@ -253,15 +235,12 @@ export async function runInit(options = {}) {
|
|
|
253
235
|
.map((p) => p.trim())
|
|
254
236
|
.filter(Boolean)
|
|
255
237
|
: [];
|
|
256
|
-
const isLocalStack = core.awsProfile === '__localstack__';
|
|
257
238
|
const isEnvVars = core.awsProfile === '__env__';
|
|
258
|
-
const resolvedProfile =
|
|
259
|
-
const resolvedEndpoint = isLocalStack ? (core.endpoint ?? 'http://localhost:4566') : undefined;
|
|
239
|
+
const resolvedProfile = isEnvVars ? '' : core.awsProfile;
|
|
260
240
|
const configContent = generateDefaultConfig(core.project, {
|
|
261
241
|
aws: {
|
|
262
242
|
profile: resolvedProfile,
|
|
263
243
|
region: core.region ?? detectedRegion,
|
|
264
|
-
endpoint: resolvedEndpoint,
|
|
265
244
|
},
|
|
266
245
|
dynamodb: { enabled: services.dynamoEnabled, includeTables },
|
|
267
246
|
postgres: {
|
|
@@ -296,7 +275,7 @@ export async function runInit(options = {}) {
|
|
|
296
275
|
console.log('');
|
|
297
276
|
console.log(chalk.bold(' Next steps:'));
|
|
298
277
|
log.info(`Run ${chalk.cyan('infrawise start')} to analyze and connect your editor`);
|
|
299
|
-
log.info(`Run ${chalk.cyan('infrawise doctor')}
|
|
278
|
+
log.info(`Run ${chalk.cyan('infrawise doctor')} if extraction comes up empty`);
|
|
300
279
|
console.log('');
|
|
301
280
|
}
|
|
302
281
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export function probePort(host, port, timeoutMs = 300) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const socket = new net.Socket();
|
|
7
|
+
const cleanup = (result) => {
|
|
8
|
+
socket.destroy();
|
|
9
|
+
resolve(result);
|
|
10
|
+
};
|
|
11
|
+
socket.setTimeout(timeoutMs);
|
|
12
|
+
socket.on('connect', () => cleanup(true));
|
|
13
|
+
socket.on('timeout', () => cleanup(false));
|
|
14
|
+
socket.on('error', () => cleanup(false));
|
|
15
|
+
socket.connect(port, host);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function scanDotEnv(cwd) {
|
|
19
|
+
const envPath = path.join(cwd, '.env');
|
|
20
|
+
if (!fs.existsSync(envPath))
|
|
21
|
+
return {};
|
|
22
|
+
const result = {};
|
|
23
|
+
try {
|
|
24
|
+
const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
28
|
+
continue;
|
|
29
|
+
const eqIdx = trimmed.indexOf('=');
|
|
30
|
+
if (eqIdx === -1)
|
|
31
|
+
continue;
|
|
32
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
33
|
+
const value = trimmed
|
|
34
|
+
.slice(eqIdx + 1)
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/^["']|["']$/g, '');
|
|
37
|
+
result[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// silent — non-critical
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
package/dist/cli/utils.js
CHANGED
|
@@ -1,49 +1,40 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
// ─── AWS helpers ─────────────────────────────────────────────────────────────
|
|
6
6
|
export function readAWSProfiles() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (name.startsWith('profile '))
|
|
18
|
-
name = name.slice(8);
|
|
19
|
-
profiles.add(name);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
7
|
+
try {
|
|
8
|
+
const out = execSync('aws configure list-profiles', {
|
|
9
|
+
encoding: 'utf-8',
|
|
10
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
11
|
+
});
|
|
12
|
+
const profiles = out.trim().split('\n').filter(Boolean);
|
|
13
|
+
return profiles.length > 0 ? profiles : ['default'];
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return ['default'];
|
|
22
17
|
}
|
|
23
|
-
parseFile(credentialsPath);
|
|
24
|
-
parseFile(configPath);
|
|
25
|
-
return profiles.size > 0 ? [...profiles] : ['default'];
|
|
26
18
|
}
|
|
27
|
-
export function detectAWSRegion() {
|
|
19
|
+
export function detectAWSRegion(profile) {
|
|
28
20
|
if (process.env.AWS_DEFAULT_REGION)
|
|
29
21
|
return process.env.AWS_DEFAULT_REGION;
|
|
30
22
|
if (process.env.AWS_REGION)
|
|
31
23
|
return process.env.AWS_REGION;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
const target = profile ?? process.env.AWS_PROFILE ?? 'default';
|
|
25
|
+
try {
|
|
26
|
+
const region = execSync(`aws configure get region --profile ${target}`, {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
29
|
+
}).trim();
|
|
30
|
+
if (region)
|
|
31
|
+
return region;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// profile may not have region configured
|
|
37
35
|
}
|
|
38
36
|
return 'us-east-1';
|
|
39
37
|
}
|
|
40
|
-
export function detectRepoType(repoPath) {
|
|
41
|
-
if (fs.existsSync(path.join(repoPath, 'tsconfig.json')))
|
|
42
|
-
return 'typescript';
|
|
43
|
-
if (fs.existsSync(path.join(repoPath, 'package.json')))
|
|
44
|
-
return 'javascript';
|
|
45
|
-
return 'unknown';
|
|
46
|
-
}
|
|
47
38
|
// ─── Banner ──────────────────────────────────────────────────────────────────
|
|
48
39
|
function readVersion() {
|
|
49
40
|
try {
|
package/dist/core/config.js
CHANGED
|
@@ -8,7 +8,6 @@ export const InfrawiseConfigSchema = z.object({
|
|
|
8
8
|
.object({
|
|
9
9
|
profile: z.string().optional().default(''),
|
|
10
10
|
region: z.string().optional().default('us-east-1'),
|
|
11
|
-
endpoint: z.string().optional(),
|
|
12
11
|
})
|
|
13
12
|
.optional()
|
|
14
13
|
.default({ profile: 'default', region: 'us-east-1' }),
|
|
@@ -56,7 +55,7 @@ export const InfrawiseConfigSchema = z.object({
|
|
|
56
55
|
eventbridge: z.object({ enabled: z.boolean().optional().default(true) }).optional(),
|
|
57
56
|
rds: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
58
57
|
s3: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
59
|
-
|
|
58
|
+
apiGateway: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
60
59
|
cloudwatchLogs: z
|
|
61
60
|
.object({
|
|
62
61
|
enabled: z.boolean().optional().default(false),
|
|
@@ -67,6 +66,11 @@ export const InfrawiseConfigSchema = z.object({
|
|
|
67
66
|
analysis: z
|
|
68
67
|
.object({
|
|
69
68
|
sampleSize: z.number().int().positive().optional().default(100),
|
|
69
|
+
hotPartitionThreshold: z.number().int().positive().optional().default(5),
|
|
70
|
+
hotPartitionThresholds: z
|
|
71
|
+
.record(z.string(), z.number().int().positive())
|
|
72
|
+
.optional()
|
|
73
|
+
.default({}),
|
|
70
74
|
})
|
|
71
75
|
.optional(),
|
|
72
76
|
});
|
|
@@ -78,13 +82,32 @@ export class ConfigError extends Error {
|
|
|
78
82
|
this.name = 'ConfigError';
|
|
79
83
|
}
|
|
80
84
|
}
|
|
85
|
+
const SecretsSchema = z.object({
|
|
86
|
+
postgres: z.object({ connectionString: z.string() }).optional(),
|
|
87
|
+
mysql: z.object({ connectionString: z.string() }).optional(),
|
|
88
|
+
mongodb: z.object({ connectionString: z.string() }).optional(),
|
|
89
|
+
});
|
|
90
|
+
export function loadSecrets(configDir) {
|
|
91
|
+
const secretsPath = path.join(configDir, '.infrawise', 'secrets.yaml');
|
|
92
|
+
if (!fs.existsSync(secretsPath))
|
|
93
|
+
return {};
|
|
94
|
+
try {
|
|
95
|
+
const raw = fs.readFileSync(secretsPath, 'utf-8');
|
|
96
|
+
const parsed = yaml.load(raw);
|
|
97
|
+
const result = SecretsSchema.safeParse(parsed);
|
|
98
|
+
return result.success ? result.data : {};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
81
104
|
export function loadConfig(configPath) {
|
|
82
105
|
const resolvedPath = configPath
|
|
83
106
|
? path.resolve(configPath)
|
|
84
107
|
: path.resolve(process.cwd(), 'infrawise.yaml');
|
|
85
108
|
if (!fs.existsSync(resolvedPath)) {
|
|
86
109
|
throw new ConfigError(`Configuration file not found at: ${resolvedPath}`, [
|
|
87
|
-
'Run `infrawise
|
|
110
|
+
'Run `infrawise start` to generate a configuration file',
|
|
88
111
|
`Or specify a path with --config <path>`,
|
|
89
112
|
]);
|
|
90
113
|
}
|
|
@@ -112,7 +135,19 @@ export function loadConfig(configPath) {
|
|
|
112
135
|
const details = result.error.issues.map((e) => ` - ${e.path.join('.')}: ${e.message}`);
|
|
113
136
|
throw new ConfigError('Configuration validation failed', details);
|
|
114
137
|
}
|
|
115
|
-
|
|
138
|
+
const config = result.data;
|
|
139
|
+
const configDir = path.dirname(resolvedPath);
|
|
140
|
+
const secrets = loadSecrets(configDir);
|
|
141
|
+
if (secrets.postgres?.connectionString && config.postgres) {
|
|
142
|
+
config.postgres.connectionString = secrets.postgres.connectionString;
|
|
143
|
+
}
|
|
144
|
+
if (secrets.mysql?.connectionString && config.mysql) {
|
|
145
|
+
config.mysql.connectionString = secrets.mysql.connectionString;
|
|
146
|
+
}
|
|
147
|
+
if (secrets.mongodb?.connectionString && config.mongodb) {
|
|
148
|
+
config.mongodb.connectionString = secrets.mongodb.connectionString;
|
|
149
|
+
}
|
|
150
|
+
return config;
|
|
116
151
|
}
|
|
117
152
|
export function generateDefaultConfig(projectName, options) {
|
|
118
153
|
const config = {
|
|
@@ -120,7 +155,6 @@ export function generateDefaultConfig(projectName, options) {
|
|
|
120
155
|
aws: {
|
|
121
156
|
profile: options?.aws?.profile ?? '',
|
|
122
157
|
region: options?.aws?.region ?? 'us-east-1',
|
|
123
|
-
...(options?.aws?.endpoint ? { endpoint: options.aws.endpoint } : {}),
|
|
124
158
|
},
|
|
125
159
|
dynamodb: {
|
|
126
160
|
enabled: options?.dynamodb?.enabled ?? true,
|
package/dist/core/errors.js
CHANGED
|
@@ -72,7 +72,7 @@ export class ConfigError extends InfrawiseError {
|
|
|
72
72
|
'infrawise.yaml not found in current directory',
|
|
73
73
|
'Missing required fields in configuration',
|
|
74
74
|
details ?? 'Unexpected config error',
|
|
75
|
-
], 'infrawise
|
|
75
|
+
], 'infrawise start');
|
|
76
76
|
this.name = 'ConfigError';
|
|
77
77
|
}
|
|
78
78
|
}
|
package/dist/graph/index.js
CHANGED
|
@@ -73,10 +73,28 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
73
73
|
provider: 'aws',
|
|
74
74
|
hasDLQ: q.hasDLQ,
|
|
75
75
|
encrypted: q.encrypted,
|
|
76
|
+
isFifo: q.isFifo,
|
|
77
|
+
visibilityTimeoutSec: q.visibilityTimeoutSec,
|
|
76
78
|
approximateMessages: q.approximateMessages,
|
|
77
79
|
retentionDays: q.retentionDays,
|
|
78
80
|
});
|
|
79
81
|
}
|
|
82
|
+
for (const api of servicesMeta.apiGateway ?? []) {
|
|
83
|
+
addNode({
|
|
84
|
+
id: `api:aws:${api.id}`,
|
|
85
|
+
type: 'api',
|
|
86
|
+
name: api.name,
|
|
87
|
+
provider: 'aws',
|
|
88
|
+
apiType: api.type,
|
|
89
|
+
routes: api.routes,
|
|
90
|
+
});
|
|
91
|
+
for (const route of api.routes) {
|
|
92
|
+
if (route.lambdaName) {
|
|
93
|
+
const lambdaId = `lambda:aws:${route.lambdaName}`;
|
|
94
|
+
edges.push({ from: `api:aws:${api.id}`, to: lambdaId, type: 'triggers' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
80
98
|
for (const t of servicesMeta.sns ?? []) {
|
|
81
99
|
addNode({
|
|
82
100
|
id: `topic:aws:${t.name}`,
|
|
@@ -402,3 +420,6 @@ export function getEdgeFrequency(graph) {
|
|
|
402
420
|
}
|
|
403
421
|
return freq;
|
|
404
422
|
}
|
|
423
|
+
export function getAPINodes(graph) {
|
|
424
|
+
return graph.nodes.filter((n) => n.type === 'api');
|
|
425
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { z } from 'zod';
|
|
|
8
8
|
import { logger } from '../core/index.js';
|
|
9
9
|
const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
|
|
10
10
|
import { summarizeFindings } from '../analyzers/index.js';
|
|
11
|
-
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getBucketNodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
11
|
+
import { getTableNodes, getFunctionNodes, getQueueNodes, getTopicNodes, getSecretNodes, getParameterNodes, getLogGroupNodes, getLambdaNodes, getEventBridgeRuleNodes, getBucketNodes, getAPINodes, getScanEdges, getOutgoingEdges, } from '../graph/index.js';
|
|
12
12
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
13
13
|
let currentGraph = { nodes: [], edges: [] };
|
|
14
14
|
let currentFindings = [];
|
|
@@ -244,7 +244,7 @@ export function createMcpServer() {
|
|
|
244
244
|
});
|
|
245
245
|
}));
|
|
246
246
|
mcp.registerTool('get_queue_details', {
|
|
247
|
-
description: 'Returns all SQS queues with DLQ presence, encryption status, approximate message count, and retention days. Call this when reviewing messaging architecture, investigating a message backlog,
|
|
247
|
+
description: 'Returns all SQS queues with DLQ presence, encryption status, FIFO type (isFifo), visibility timeout, approximate message count, and retention days. When isFifo is true, all SendMessage calls must include a MessageGroupId. Call this when reviewing messaging architecture, investigating a message backlog, checking DLQ coverage, or verifying visibility timeout is set correctly relative to Lambda timeout (should be 6× the Lambda timeout). Use get_infra_overview for a quick queue count only.',
|
|
248
248
|
inputSchema: z.object({}),
|
|
249
249
|
}, logged('get_queue_details', async () => {
|
|
250
250
|
const queues = getQueueNodes(currentGraph);
|
|
@@ -256,6 +256,8 @@ export function createMcpServer() {
|
|
|
256
256
|
provider: q.provider,
|
|
257
257
|
hasDLQ: q.hasDLQ,
|
|
258
258
|
encrypted: q.encrypted,
|
|
259
|
+
isFifo: q.isFifo ?? false,
|
|
260
|
+
visibilityTimeoutSec: q.visibilityTimeoutSec,
|
|
259
261
|
approximateMessages: q.approximateMessages,
|
|
260
262
|
retentionDays: q.retentionDays,
|
|
261
263
|
findings: queueFindings
|
|
@@ -385,6 +387,24 @@ export function createMcpServer() {
|
|
|
385
387
|
})),
|
|
386
388
|
});
|
|
387
389
|
}));
|
|
390
|
+
mcp.registerTool('get_api_routes', {
|
|
391
|
+
description: 'Returns all API Gateway APIs (REST, HTTP, WebSocket) with their routes, HTTP methods, paths, and the Lambda function each route invokes. Call this before writing any API handler to understand which Lambda handles a route, or when reviewing API surface area and Lambda integration coverage.',
|
|
392
|
+
inputSchema: z.object({}),
|
|
393
|
+
}, logged('get_api_routes', async () => {
|
|
394
|
+
const apis = getAPINodes(currentGraph);
|
|
395
|
+
return toText({
|
|
396
|
+
total: apis.length,
|
|
397
|
+
apis: apis.map((api) => ({
|
|
398
|
+
name: api.name,
|
|
399
|
+
type: api.apiType,
|
|
400
|
+
routes: (api.routes ?? []).map((r) => ({
|
|
401
|
+
method: r.method,
|
|
402
|
+
path: r.path,
|
|
403
|
+
lambda: r.lambdaName ?? null,
|
|
404
|
+
})),
|
|
405
|
+
})),
|
|
406
|
+
});
|
|
407
|
+
}));
|
|
388
408
|
mcp.registerTool('get_log_errors', {
|
|
389
409
|
description: 'Returns recent error pattern summaries from CloudWatch log groups: pattern counts and frequencies grouped by log group. Raw log messages are never returned. Use the optional logGroup filter to scope to one group by name substring. Call this when investigating errors or identifying log groups with no retention policy.',
|
|
390
410
|
inputSchema: z.object({
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"mcpName": "io.github.Sidd27/infrawise",
|
|
5
|
-
"description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
|
|
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": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"mcp-server",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"cli",
|
|
33
33
|
"terraform",
|
|
34
34
|
"cloudformation",
|
|
35
|
-
"cdk"
|
|
35
|
+
"cdk",
|
|
36
|
+
"api-gateway",
|
|
37
|
+
"sqs-fifo"
|
|
36
38
|
],
|
|
37
39
|
"agentskills": {
|
|
38
40
|
"infrawise": "./AGENTS.md"
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
"node": ">=24.0.0",
|
|
53
55
|
"pnpm": ">=9.0.0"
|
|
54
56
|
},
|
|
55
|
-
"packageManager": "pnpm@
|
|
57
|
+
"packageManager": "pnpm@11.6.0",
|
|
56
58
|
"scripts": {
|
|
57
59
|
"build": "tsc --noEmit false --outDir dist",
|
|
58
60
|
"test": "vitest run --passWithNoTests",
|
|
@@ -63,14 +65,16 @@
|
|
|
63
65
|
"build-arch": "node scripts/build-arch.mjs",
|
|
64
66
|
"generate-diagrams": "node scripts/build-arch.mjs",
|
|
65
67
|
"record": "bash docs/demo/record.sh",
|
|
66
|
-
"dev": "node dist/cli/index.js
|
|
68
|
+
"dev": "node dist/cli/index.js serve",
|
|
67
69
|
"release": "node scripts/release.js",
|
|
68
70
|
"prepare": "simple-git-hooks"
|
|
69
71
|
},
|
|
70
72
|
"simple-git-hooks": {
|
|
71
|
-
"pre-commit": "pnpm format && git add
|
|
73
|
+
"pre-commit": "pnpm format && git add $(git diff --cached --name-only --diff-filter=ACMR) && pnpm lint && pnpm typecheck && pnpm test"
|
|
72
74
|
},
|
|
73
75
|
"dependencies": {
|
|
76
|
+
"@aws-sdk/client-api-gateway": "^3.1068.0",
|
|
77
|
+
"@aws-sdk/client-apigatewayv2": "^3.1068.0",
|
|
74
78
|
"@aws-sdk/client-cloudwatch-logs": "^3.1048.0",
|
|
75
79
|
"@aws-sdk/client-dynamodb": "^3.1048.0",
|
|
76
80
|
"@aws-sdk/client-eventbridge": "^3.1051.0",
|
|
@@ -99,20 +103,16 @@
|
|
|
99
103
|
"zod": "^4.4.3"
|
|
100
104
|
},
|
|
101
105
|
"devDependencies": {
|
|
102
|
-
"@mermaid-js/mermaid-cli": "^11.15.0",
|
|
103
|
-
"@types/inquirer": "^9.0.9",
|
|
104
106
|
"@types/js-yaml": "^4.0.9",
|
|
105
107
|
"@types/node": "^25.8.0",
|
|
106
108
|
"@types/pg": "^8.11.0",
|
|
107
109
|
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
|
108
110
|
"@typescript-eslint/parser": "^8.59.3",
|
|
109
111
|
"@vitest/coverage-v8": "^4.1.6",
|
|
110
|
-
"@xmldom/xmldom": "^0.9.10",
|
|
111
112
|
"eslint": "^10.4.0",
|
|
112
113
|
"prettier": "^3.8.3",
|
|
113
114
|
"simple-git-hooks": "^2.13.1",
|
|
114
115
|
"typescript": "^6.0.3",
|
|
115
|
-
"vite": "^8.0.13",
|
|
116
116
|
"vitest": "^4.1.6"
|
|
117
117
|
},
|
|
118
118
|
"files": [
|
|
@@ -121,15 +121,5 @@
|
|
|
121
121
|
],
|
|
122
122
|
"publishConfig": {
|
|
123
123
|
"access": "public"
|
|
124
|
-
},
|
|
125
|
-
"pnpm": {
|
|
126
|
-
"overrides": {
|
|
127
|
-
"qs": ">=6.15.2",
|
|
128
|
-
"vite": "^7"
|
|
129
|
-
},
|
|
130
|
-
"allowedBuildScripts": [
|
|
131
|
-
"puppeteer",
|
|
132
|
-
"simple-git-hooks"
|
|
133
|
-
]
|
|
134
124
|
}
|
|
135
125
|
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import { readAWSProfiles, log, printHeader } from '../utils.js';
|
|
5
|
-
import { validateDynamoAccess } from '../../adapters/aws/dynamodb.js';
|
|
6
|
-
export async function runAuth() {
|
|
7
|
-
printHeader('AWS Authentication');
|
|
8
|
-
const profiles = readAWSProfiles();
|
|
9
|
-
if (profiles.length === 0) {
|
|
10
|
-
log.fail('No AWS profiles found');
|
|
11
|
-
console.log('');
|
|
12
|
-
log.info('Run ' + chalk.cyan('aws configure') + ' to set up credentials');
|
|
13
|
-
log.info('Or manually edit ' + chalk.dim('~/.aws/credentials'));
|
|
14
|
-
console.log('');
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
log.success(`Found ${profiles.length} profile(s)`);
|
|
18
|
-
console.log('');
|
|
19
|
-
const { selectedProfile } = await inquirer.prompt([
|
|
20
|
-
{
|
|
21
|
-
type: 'list',
|
|
22
|
-
name: 'selectedProfile',
|
|
23
|
-
message: 'Select a profile to validate:',
|
|
24
|
-
choices: profiles,
|
|
25
|
-
},
|
|
26
|
-
]);
|
|
27
|
-
console.log('');
|
|
28
|
-
const spin = ora({
|
|
29
|
-
text: chalk.dim(`Validating "${selectedProfile}"...`),
|
|
30
|
-
color: 'cyan',
|
|
31
|
-
}).start();
|
|
32
|
-
const testConfig = {
|
|
33
|
-
project: 'auth-test',
|
|
34
|
-
aws: { profile: selectedProfile, region: 'us-east-1' },
|
|
35
|
-
};
|
|
36
|
-
const isValid = await validateDynamoAccess(testConfig);
|
|
37
|
-
if (isValid) {
|
|
38
|
-
spin.succeed(chalk.green(`Profile "${chalk.bold(selectedProfile)}" is valid`));
|
|
39
|
-
console.log('');
|
|
40
|
-
console.log(chalk.dim(' Update your infrawise.yaml:'));
|
|
41
|
-
console.log(chalk.cyan(` aws:\n profile: ${selectedProfile}`));
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
spin.fail(chalk.red(`Profile "${chalk.bold(selectedProfile)}" cannot access DynamoDB`));
|
|
45
|
-
console.log('');
|
|
46
|
-
log.warn('Possible causes:');
|
|
47
|
-
log.dim('Missing IAM permissions — need dynamodb:ListTables, dynamodb:DescribeTable');
|
|
48
|
-
log.dim('Expired SSO — run: aws sso login');
|
|
49
|
-
log.dim('Wrong region — check your AWS config');
|
|
50
|
-
console.log('');
|
|
51
|
-
log.info(`Run ${chalk.cyan('infrawise doctor')} for a full diagnostic`);
|
|
52
|
-
}
|
|
53
|
-
console.log('');
|
|
54
|
-
}
|