infrawise 0.11.0 → 0.12.1
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 +49 -26
- 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 +0 -16
- package/dist/cli/commands/analyze.js +0 -1
- 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 +33 -5
- package/dist/core/errors.js +1 -1
- package/package.json +3 -7
- package/dist/cli/commands/auth.js +0 -54
package/README.md
CHANGED
|
@@ -69,10 +69,10 @@ infrawise start --claude
|
|
|
69
69
|
|
|
70
70
|
That's it. Infrawise will:
|
|
71
71
|
|
|
72
|
-
1.
|
|
72
|
+
1. Probe your environment and generate `infrawise.yaml` (first time only — asks which AWS profile to use only if you have several)
|
|
73
73
|
2. Scan your AWS services, databases, and codebase
|
|
74
74
|
3. Write `.mcp.json` so your editor auto-connects on every future launch
|
|
75
|
-
4. Open Claude Code with all
|
|
75
|
+
4. Open Claude Code with all 16 MCP tools ready
|
|
76
76
|
|
|
77
77
|
**Every time after:**
|
|
78
78
|
|
|
@@ -80,7 +80,7 @@ That's it. Infrawise will:
|
|
|
80
80
|
claude # no infrawise command needed — editor manages the connection
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Analysis is cached for 24 hours. When the cache is stale, `infrawise stdio` (spawned automatically by your editor) refreshes it at session start. File changes are detected within the session and the code graph is updated automatically.
|
|
83
|
+
Analysis is cached for 24 hours. When the cache is stale, `infrawise serve --stdio` (spawned automatically by your editor) refreshes it at session start. File changes are detected within the session and the code graph is updated automatically.
|
|
84
84
|
|
|
85
85
|
```
|
|
86
86
|
Findings (3 total)
|
|
@@ -107,7 +107,7 @@ Findings (3 total)
|
|
|
107
107
|
infrawise start --claude
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
Writes `.mcp.json` to your project root and opens Claude Code. Claude Code reads `.mcp.json` automatically on every launch and manages the `infrawise stdio` process — no server to start, no ports to configure.
|
|
110
|
+
Writes `.mcp.json` to your project root and opens Claude Code. Claude Code reads `.mcp.json` automatically on every launch and manages the `infrawise serve --stdio` process — no server to start, no ports to configure.
|
|
111
111
|
|
|
112
112
|
### Cursor
|
|
113
113
|
|
|
@@ -115,7 +115,7 @@ Writes `.mcp.json` to your project root and opens Claude Code. Claude Code reads
|
|
|
115
115
|
infrawise start --cursor
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
Writes `.cursor/mcp.json` and opens Cursor. All
|
|
118
|
+
Writes `.cursor/mcp.json` and opens Cursor. All 16 infrawise tools are available in Cursor's MCP panel.
|
|
119
119
|
|
|
120
120
|
### Any editor (no flag)
|
|
121
121
|
|
|
@@ -123,14 +123,14 @@ Writes `.cursor/mcp.json` and opens Cursor. All 15 infrawise tools are available
|
|
|
123
123
|
infrawise start
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
Writes `.mcp.json` and exits. Open whichever editor you prefer — point it at `infrawise stdio --config /path/to/infrawise.yaml` as an MCP server command.
|
|
126
|
+
Writes `.mcp.json` and exits. Open whichever editor you prefer — point it at `infrawise serve --stdio --config /path/to/infrawise.yaml` as an MCP server command.
|
|
127
127
|
|
|
128
128
|
### HTTP transport (alternative)
|
|
129
129
|
|
|
130
130
|
If your editor or workflow requires an HTTP MCP endpoint instead of stdio:
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
|
-
infrawise
|
|
133
|
+
infrawise serve # starts server at http://localhost:3000/mcp
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
Add to your editor's MCP config:
|
|
@@ -170,17 +170,17 @@ Add to your editor's MCP config:
|
|
|
170
170
|
|
|
171
171
|
## CLI reference
|
|
172
172
|
|
|
173
|
-
| Command
|
|
174
|
-
|
|
|
175
|
-
| `infrawise start`
|
|
176
|
-
| `infrawise start --claude`
|
|
177
|
-
| `infrawise start --cursor`
|
|
178
|
-
| `infrawise
|
|
179
|
-
| `infrawise
|
|
180
|
-
| `infrawise analyze`
|
|
181
|
-
| `infrawise
|
|
182
|
-
| `infrawise
|
|
183
|
-
| `infrawise doctor`
|
|
173
|
+
| Command | What it does |
|
|
174
|
+
| ----------------------------- | --------------------------------------------------------------------------------- |
|
|
175
|
+
| `infrawise start` | **Primary command** — probe env, generate config, analyze, write editor MCP config |
|
|
176
|
+
| `infrawise start --claude` | Same as above, then opens Claude Code |
|
|
177
|
+
| `infrawise start --cursor` | Same as above, then opens Cursor |
|
|
178
|
+
| `infrawise start --interactive` | Run the guided setup wizard instead of auto-discovery |
|
|
179
|
+
| `infrawise start --rediscover` | Delete `infrawise.yaml` + `.infrawise/`, then re-probe and re-analyze |
|
|
180
|
+
| `infrawise analyze` | Force a full re-scan — useful after major infrastructure changes |
|
|
181
|
+
| `infrawise check` | CI gate — analyze and exit non-zero when findings reach the threshold severity |
|
|
182
|
+
| `infrawise serve` | Start the MCP server — HTTP by default, or `--stdio` for editor integration |
|
|
183
|
+
| `infrawise doctor` | Diagnostic escape hatch — validate AWS/DB access, config, and repo scan |
|
|
184
184
|
|
|
185
185
|
### `infrawise analyze` options
|
|
186
186
|
|
|
@@ -190,7 +190,7 @@ Add to your editor's MCP config:
|
|
|
190
190
|
| `-r, --repo <path>` | Repository to scan (default: current directory) |
|
|
191
191
|
| `--no-cache` | Skip reading/writing the cache |
|
|
192
192
|
| `-o, --output <path>` | Save findings as a markdown report, e.g. `report.md` |
|
|
193
|
-
| `--severity <level>` | Only show findings at or above this level: `high` \| `medium` \| `low`
|
|
193
|
+
| `--severity <level>` | Only show findings at or above this level: `high` \| `medium` \| `low` |
|
|
194
194
|
|
|
195
195
|
```bash
|
|
196
196
|
# Export a shareable findings report
|
|
@@ -203,11 +203,37 @@ infrawise analyze --severity high
|
|
|
203
203
|
infrawise analyze --severity high --output report.md
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
+
### `infrawise check` options (CI/CD)
|
|
207
|
+
|
|
208
|
+
`check` runs a fresh analysis and sets a non-zero exit code when blocking findings exist, so it can gate a pipeline without an AI editor.
|
|
209
|
+
|
|
210
|
+
| Flag | Description |
|
|
211
|
+
| --------------------- | ---------------------------------------------------------------------- |
|
|
212
|
+
| `-c, --config <path>` | Path to `infrawise.yaml` (default: `infrawise.yaml`) |
|
|
213
|
+
| `-r, --repo <path>` | Repository to scan (default: current directory) |
|
|
214
|
+
| `--fail-on <level>` | Severity that fails the build: `high` (default) \| `medium` \| `low` |
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Block a deploy if any high-severity finding exists (exit 1)
|
|
218
|
+
infrawise check
|
|
219
|
+
|
|
220
|
+
# Stricter gate — fail on medium and above
|
|
221
|
+
infrawise check --fail-on medium
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `infrawise serve` options
|
|
225
|
+
|
|
226
|
+
| Flag | Description |
|
|
227
|
+
| --------------------- | ---------------------------------------------------------------------- |
|
|
228
|
+
| `-c, --config <path>` | Path to `infrawise.yaml` (default: `infrawise.yaml`) |
|
|
229
|
+
| `--stdio` | Use stdio transport (for editors via `.mcp.json`) instead of HTTP |
|
|
230
|
+
| `-p, --port <number>` | Port to listen on, HTTP only (default: `3000`) |
|
|
231
|
+
|
|
206
232
|
---
|
|
207
233
|
|
|
208
234
|
## Configuration
|
|
209
235
|
|
|
210
|
-
`infrawise.yaml` is generated by `infrawise start` (or `infrawise
|
|
236
|
+
`infrawise.yaml` is generated by `infrawise start` (or `infrawise start --interactive` for the guided wizard) and lives in your repo root. Every service must be explicitly `enabled: true` — infrawise never connects to anything not listed in config.
|
|
211
237
|
|
|
212
238
|
Connection strings support `${ENV_VAR}` substitution so passwords never need to be committed:
|
|
213
239
|
|
|
@@ -272,9 +298,6 @@ rds:
|
|
|
272
298
|
s3:
|
|
273
299
|
enabled: false
|
|
274
300
|
|
|
275
|
-
kafka:
|
|
276
|
-
enabled: false
|
|
277
|
-
|
|
278
301
|
apiGateway:
|
|
279
302
|
enabled: false
|
|
280
303
|
|
|
@@ -343,9 +366,9 @@ Works from AWS APIs, database schema introspection, and IaC files — no depende
|
|
|
343
366
|
| MongoDB schema | Collections, indexes |
|
|
344
367
|
| SQS | Missing DLQs, unencrypted queues, large backlogs, FIFO detection, visibility timeout vs Lambda timeout mismatch |
|
|
345
368
|
| SNS | Subscription filter policies — required message attributes per subscription |
|
|
346
|
-
| Kafka (kafkajs)
|
|
369
|
+
| Apache Kafka (kafkajs) | Producer/consumer topic mapping from code — any broker (self-hosted, Confluent, Redpanda, MSK); distinct from the MSK Lambda trigger |
|
|
347
370
|
| Secrets Manager | Missing secret rotation |
|
|
348
|
-
| Lambda | Default memory (128 MB), high timeouts, triggers (SQS/DynamoDB/Kinesis/EventBridge/S3), missing DLQ on trigger source |
|
|
371
|
+
| Lambda | Default memory (128 MB), high timeouts, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), missing DLQ on trigger source |
|
|
349
372
|
| S3 | Public access blocking (verify), missing versioning, missing encryption |
|
|
350
373
|
| EventBridge | Rules, schedules, event patterns, target Lambda functions |
|
|
351
374
|
| API Gateway | REST, HTTP, and WebSocket APIs — routes, methods, Lambda integrations |
|
|
@@ -363,7 +386,7 @@ Uses [ts-morph](https://ts-morph.com/) AST analysis to detect which functions ca
|
|
|
363
386
|
| Missing GSI | Medium | Queries on attributes without a matching GSI |
|
|
364
387
|
| Hot Partition | Medium | 5+ distinct code paths hitting the same table |
|
|
365
388
|
| Missing Index (PostgreSQL) | Medium | Tables queried without indexes |
|
|
366
|
-
| N+1 Query |
|
|
389
|
+
| N+1 Query | High | Repeated query patterns from ORM loops |
|
|
367
390
|
| Large SELECT | Low | `SELECT *` usage |
|
|
368
391
|
| Missing MySQL Index | Medium | MySQL tables queried without indexes |
|
|
369
392
|
| MySQL Full Table Scan | High | Full table scan patterns in MySQL queries |
|
|
@@ -4,10 +4,7 @@ import { DynamoDBError, logger } from '../../core/index.js';
|
|
|
4
4
|
function createDynamoClient(config) {
|
|
5
5
|
const region = config.aws?.region ?? 'us-east-1';
|
|
6
6
|
const profile = config.aws?.profile;
|
|
7
|
-
const endpoint = config.aws?.endpoint;
|
|
8
7
|
const clientConfig = { region };
|
|
9
|
-
if (endpoint)
|
|
10
|
-
clientConfig.endpoint = endpoint;
|
|
11
8
|
if (profile)
|
|
12
9
|
clientConfig.credentials = fromIni({ profile });
|
|
13
10
|
return new DynamoDBClient(clientConfig);
|
|
@@ -7,8 +7,6 @@ const MAX_EVENTS_PER_GROUP = 50;
|
|
|
7
7
|
function clientConfig(cfg) {
|
|
8
8
|
const region = cfg.region ?? 'us-east-1';
|
|
9
9
|
const base = { region };
|
|
10
|
-
if (cfg.endpoint)
|
|
11
|
-
base.endpoint = cfg.endpoint;
|
|
12
10
|
if (cfg.profile)
|
|
13
11
|
base.credentials = fromIni({ profile: cfg.profile });
|
|
14
12
|
return base;
|
package/dist/adapters/aws/s3.js
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
import { S3Client, ListBucketsCommand, GetBucketNotificationConfigurationCommand, GetBucketVersioningCommand, GetBucketEncryptionCommand, GetPublicAccessBlockCommand, } from '@aws-sdk/client-s3';
|
|
2
2
|
import { fromIni } from '@aws-sdk/credential-providers';
|
|
3
3
|
import { logger } from '../../core/index.js';
|
|
4
|
-
function validateEndpoint(endpoint) {
|
|
5
|
-
let url;
|
|
6
|
-
try {
|
|
7
|
-
url = new URL(endpoint);
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
throw new Error(`Invalid aws.endpoint URL: "${endpoint}"`);
|
|
11
|
-
}
|
|
12
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
13
|
-
throw new Error(`aws.endpoint must use http:// or https://, got "${url.protocol}//"`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
4
|
function clientConfig(cfg) {
|
|
17
5
|
const region = cfg.region ?? 'us-east-1';
|
|
18
6
|
const base = { region };
|
|
19
|
-
if (cfg.endpoint) {
|
|
20
|
-
validateEndpoint(cfg.endpoint);
|
|
21
|
-
base.endpoint = cfg.endpoint;
|
|
22
|
-
}
|
|
23
7
|
if (cfg.profile)
|
|
24
8
|
base.credentials = fromIni({ profile: cfg.profile });
|
|
25
9
|
return base;
|
|
@@ -9,25 +9,9 @@ import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '
|
|
|
9
9
|
import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
|
|
10
10
|
import { fromIni } from '@aws-sdk/credential-providers';
|
|
11
11
|
import { logger } from '../../core/index.js';
|
|
12
|
-
function validateEndpoint(endpoint) {
|
|
13
|
-
let url;
|
|
14
|
-
try {
|
|
15
|
-
url = new URL(endpoint);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
throw new Error(`Invalid aws.endpoint URL: "${endpoint}"`);
|
|
19
|
-
}
|
|
20
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
21
|
-
throw new Error(`aws.endpoint must use http:// or https://, got "${url.protocol}//"`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
12
|
function clientConfig(cfg) {
|
|
25
13
|
const region = cfg.region ?? 'us-east-1';
|
|
26
14
|
const base = { region };
|
|
27
|
-
if (cfg.endpoint) {
|
|
28
|
-
validateEndpoint(cfg.endpoint);
|
|
29
|
-
base.endpoint = cfg.endpoint;
|
|
30
|
-
}
|
|
31
15
|
if (cfg.profile)
|
|
32
16
|
base.credentials = fromIni({ profile: cfg.profile });
|
|
33
17
|
return base;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readCache, setCacheDir } from '../../core/index.js';
|
|
4
|
+
import { printFinding, log, printHeader } from '../utils.js';
|
|
5
|
+
import { runAnalyze } from './analyze.js';
|
|
6
|
+
const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
|
|
7
|
+
export async function runCheck(options = {}) {
|
|
8
|
+
const failOn = options.failOn ?? 'high';
|
|
9
|
+
const threshold = SEVERITY_ORDER[failOn] ?? 3;
|
|
10
|
+
printHeader('Infrawise Check');
|
|
11
|
+
// Always extract fresh — CI must not gate on a stale graph.
|
|
12
|
+
await runAnalyze({ config: options.config, repo: options.repo, silent: true });
|
|
13
|
+
setCacheDir(path.dirname(path.resolve(options.config ?? 'infrawise.yaml')));
|
|
14
|
+
const findings = readCache('findings') ?? [];
|
|
15
|
+
const violations = findings.filter((f) => (SEVERITY_ORDER[f.severity] ?? 0) >= threshold);
|
|
16
|
+
console.log('');
|
|
17
|
+
if (violations.length === 0) {
|
|
18
|
+
log.success('Check passed', `no ${failOn}+ findings (${findings.length} total below threshold)`);
|
|
19
|
+
console.log('');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(chalk.bold(' Blocking findings') + chalk.dim(` ${violations.length} at or above ${failOn}`));
|
|
23
|
+
violations.forEach((f, i) => printFinding(f, i));
|
|
24
|
+
console.log('');
|
|
25
|
+
log.fail(`Check failed`, `${violations.length} ${failOn}+ finding(s) must be resolved before deploy`);
|
|
26
|
+
console.log('');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { probePort, scanDotEnv } from '../probe.js';
|
|
8
|
+
import { readAWSProfiles, detectAWSRegion, log } from '../utils.js';
|
|
9
|
+
import { runInit } from '../interactive-setup.js';
|
|
10
|
+
const DB_PORTS = { postgres: 5432, mysql: 3306, mongodb: 27017 };
|
|
11
|
+
const ENV_KEYS = {
|
|
12
|
+
postgres: ['DATABASE_URL', 'POSTGRES_URL', 'POSTGRESQL_URL', 'POSTGRES_CONNECTION_STRING'],
|
|
13
|
+
mysql: ['MYSQL_URL', 'MYSQL_CONNECTION_STRING'],
|
|
14
|
+
mongodb: ['MONGO_URI', 'MONGODB_URI', 'MONGO_URL', 'MONGODB_URL'],
|
|
15
|
+
};
|
|
16
|
+
export async function runDiscover(options = {}) {
|
|
17
|
+
if (options.interactive) {
|
|
18
|
+
await runInit();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
log.dim('Probing environment...');
|
|
23
|
+
const [pgDetected, mysqlDetected, mongoDetected] = await Promise.all([
|
|
24
|
+
probePort('localhost', DB_PORTS.postgres, 300),
|
|
25
|
+
probePort('localhost', DB_PORTS.mysql, 300),
|
|
26
|
+
probePort('localhost', DB_PORTS.mongodb, 300),
|
|
27
|
+
]);
|
|
28
|
+
if (pgDetected)
|
|
29
|
+
log.success('Postgres detected', `port ${DB_PORTS.postgres}`);
|
|
30
|
+
if (mysqlDetected)
|
|
31
|
+
log.success('MySQL detected', `port ${DB_PORTS.mysql}`);
|
|
32
|
+
if (mongoDetected)
|
|
33
|
+
log.success('MongoDB detected', `port ${DB_PORTS.mongodb}`);
|
|
34
|
+
const envVars = scanDotEnv(cwd);
|
|
35
|
+
const secrets = extractDbSecrets(envVars);
|
|
36
|
+
const iacDetected = detectIaC(cwd);
|
|
37
|
+
const hasAWSConfig = process.env.AWS_ACCESS_KEY_ID !== undefined ||
|
|
38
|
+
fs.existsSync(path.join(os.homedir(), '.aws', 'credentials')) ||
|
|
39
|
+
fs.existsSync(path.join(os.homedir(), '.aws', 'config'));
|
|
40
|
+
if (!hasAWSConfig && !pgDetected && !mysqlDetected && !mongoDetected) {
|
|
41
|
+
console.error(chalk.red('\n Nothing detected.'));
|
|
42
|
+
console.error(chalk.dim(' Run `aws configure` to set up AWS credentials, or start a local database.\n'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
let selectedProfile = '';
|
|
46
|
+
if (process.env.AWS_PROFILE) {
|
|
47
|
+
selectedProfile = process.env.AWS_PROFILE;
|
|
48
|
+
log.success('AWS profile', `${selectedProfile} (from AWS_PROFILE)`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const profiles = readAWSProfiles();
|
|
52
|
+
if (profiles.length === 1) {
|
|
53
|
+
selectedProfile = profiles[0];
|
|
54
|
+
log.success('AWS profile', profiles[0]);
|
|
55
|
+
}
|
|
56
|
+
else if (profiles.length > 1) {
|
|
57
|
+
console.log('');
|
|
58
|
+
const answer = await inquirer.prompt([
|
|
59
|
+
{
|
|
60
|
+
type: 'select',
|
|
61
|
+
name: 'profile',
|
|
62
|
+
message: 'Select AWS profile:',
|
|
63
|
+
choices: profiles,
|
|
64
|
+
default: profiles[0],
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
selectedProfile = answer.profile;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const region = detectAWSRegion(selectedProfile);
|
|
71
|
+
ensureInfrawiseDir(cwd);
|
|
72
|
+
writeYaml(cwd, {
|
|
73
|
+
profile: selectedProfile,
|
|
74
|
+
region,
|
|
75
|
+
pgDetected,
|
|
76
|
+
mysqlDetected,
|
|
77
|
+
mongoDetected,
|
|
78
|
+
iacDetected,
|
|
79
|
+
});
|
|
80
|
+
const anyDbDetected = pgDetected || mysqlDetected || mongoDetected;
|
|
81
|
+
if (anyDbDetected) {
|
|
82
|
+
writeSecrets(cwd, secrets);
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
|
85
|
+
log.success('Generated infrawise.yaml');
|
|
86
|
+
if (pgDetected && !secrets.postgres) {
|
|
87
|
+
log.warn('Postgres detected — add connection string to .infrawise/secrets.yaml');
|
|
88
|
+
}
|
|
89
|
+
if (mysqlDetected && !secrets.mysql) {
|
|
90
|
+
log.warn('MySQL detected — add connection string to .infrawise/secrets.yaml');
|
|
91
|
+
}
|
|
92
|
+
if (mongoDetected && !secrets.mongodb) {
|
|
93
|
+
log.warn('MongoDB detected — add connection string to .infrawise/secrets.yaml');
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
}
|
|
97
|
+
function extractDbSecrets(env) {
|
|
98
|
+
const secrets = {};
|
|
99
|
+
for (const key of ENV_KEYS.postgres) {
|
|
100
|
+
if (env[key]) {
|
|
101
|
+
secrets.postgres = env[key];
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const key of ENV_KEYS.mysql) {
|
|
106
|
+
if (env[key]) {
|
|
107
|
+
secrets.mysql = env[key];
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const key of ENV_KEYS.mongodb) {
|
|
112
|
+
if (env[key]) {
|
|
113
|
+
secrets.mongodb = env[key];
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return secrets;
|
|
118
|
+
}
|
|
119
|
+
function detectIaC(cwd) {
|
|
120
|
+
return scanDirForIaC(cwd, 3);
|
|
121
|
+
}
|
|
122
|
+
function scanDirForIaC(dir, depth) {
|
|
123
|
+
if (depth < 0)
|
|
124
|
+
return false;
|
|
125
|
+
try {
|
|
126
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
129
|
+
continue;
|
|
130
|
+
if (entry.isFile()) {
|
|
131
|
+
const n = entry.name;
|
|
132
|
+
if (n.endsWith('.tf') ||
|
|
133
|
+
n === 'cdk.json' ||
|
|
134
|
+
n === 'template.yaml' ||
|
|
135
|
+
n === 'template.yml') {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (entry.isDirectory() && depth > 0) {
|
|
140
|
+
if (scanDirForIaC(path.join(dir, entry.name), depth - 1))
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore permission errors
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function ensureInfrawiseDir(cwd) {
|
|
151
|
+
const infrawiseDir = path.join(cwd, '.infrawise');
|
|
152
|
+
if (!fs.existsSync(infrawiseDir)) {
|
|
153
|
+
fs.mkdirSync(infrawiseDir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
156
|
+
const entry = '.infrawise/';
|
|
157
|
+
if (fs.existsSync(gitignorePath)) {
|
|
158
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
159
|
+
if (!content.includes(entry)) {
|
|
160
|
+
const prefix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
|
|
161
|
+
fs.appendFileSync(gitignorePath, `${prefix}${entry}\n`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
fs.writeFileSync(gitignorePath, `${entry}\n`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function writeYaml(cwd, opts) {
|
|
169
|
+
const config = {
|
|
170
|
+
project: path.basename(cwd),
|
|
171
|
+
aws: {
|
|
172
|
+
profile: opts.profile,
|
|
173
|
+
region: opts.region,
|
|
174
|
+
},
|
|
175
|
+
dynamodb: { enabled: true, includeTables: [] },
|
|
176
|
+
postgres: { enabled: opts.pgDetected, connectionString: '' },
|
|
177
|
+
mysql: { enabled: opts.mysqlDetected, connectionString: '' },
|
|
178
|
+
mongodb: { enabled: opts.mongoDetected, connectionString: '', databases: [] },
|
|
179
|
+
terraform: { enabled: opts.iacDetected },
|
|
180
|
+
sqs: { enabled: true },
|
|
181
|
+
sns: { enabled: true },
|
|
182
|
+
ssm: { enabled: true, paths: [] },
|
|
183
|
+
secretsManager: { enabled: true },
|
|
184
|
+
lambda: { enabled: true },
|
|
185
|
+
eventbridge: { enabled: true },
|
|
186
|
+
rds: { enabled: false },
|
|
187
|
+
s3: { enabled: true },
|
|
188
|
+
apiGateway: { enabled: true },
|
|
189
|
+
cloudwatchLogs: { enabled: false, logGroupPrefixes: [], windowHours: 24 },
|
|
190
|
+
analysis: { sampleSize: 100 },
|
|
191
|
+
};
|
|
192
|
+
fs.writeFileSync(path.join(cwd, 'infrawise.yaml'), yaml.dump(config, { lineWidth: 120 }), 'utf-8');
|
|
193
|
+
}
|
|
194
|
+
function writeSecrets(cwd, secrets) {
|
|
195
|
+
const data = {
|
|
196
|
+
postgres: { connectionString: secrets.postgres ?? '' },
|
|
197
|
+
mysql: { connectionString: secrets.mysql ?? '' },
|
|
198
|
+
mongodb: { connectionString: secrets.mongodb ?? '' },
|
|
199
|
+
};
|
|
200
|
+
fs.writeFileSync(path.join(cwd, '.infrawise', 'secrets.yaml'), yaml.dump(data, { lineWidth: 120 }), 'utf-8');
|
|
201
|
+
}
|
|
@@ -44,7 +44,7 @@ export async function runDoctor(options = {}) {
|
|
|
44
44
|
name: 'Config file',
|
|
45
45
|
status: exists ? 'pass' : 'fail',
|
|
46
46
|
message: exists ? configPath : `Not found at ${configPath}`,
|
|
47
|
-
detail: exists ? undefined : 'Run: infrawise
|
|
47
|
+
detail: exists ? undefined : 'Run: infrawise start',
|
|
48
48
|
};
|
|
49
49
|
}));
|
|
50
50
|
// Config valid
|
|
@@ -80,7 +80,6 @@ export async function runDoctor(options = {}) {
|
|
|
80
80
|
const awsCfg = {
|
|
81
81
|
region: config?.aws?.region,
|
|
82
82
|
profile: config?.aws?.profile,
|
|
83
|
-
endpoint: config?.aws?.endpoint,
|
|
84
83
|
};
|
|
85
84
|
// DynamoDB
|
|
86
85
|
results.push(await runCheck('Testing DynamoDB access...', async () => {
|
|
@@ -4,7 +4,7 @@ import { spawn } from 'child_process';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { readCache, setCacheDir, formatError, loadConfig } from '../../core/index.js';
|
|
6
6
|
import { log, printHeader } from '../utils.js';
|
|
7
|
-
import {
|
|
7
|
+
import { runDiscover } from './discover.js';
|
|
8
8
|
import { runAnalyze } from './analyze.js';
|
|
9
9
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
10
|
function writeMcpJson(configAbsPath) {
|
|
@@ -12,7 +12,7 @@ function writeMcpJson(configAbsPath) {
|
|
|
12
12
|
mcpServers: {
|
|
13
13
|
infrawise: {
|
|
14
14
|
command: 'infrawise',
|
|
15
|
-
args: ['stdio', '--config', configAbsPath],
|
|
15
|
+
args: ['serve', '--stdio', '--config', configAbsPath],
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
};
|
|
@@ -27,7 +27,7 @@ function writeCursorMcp(configAbsPath) {
|
|
|
27
27
|
mcpServers: {
|
|
28
28
|
infrawise: {
|
|
29
29
|
command: 'infrawise',
|
|
30
|
-
args: ['stdio', '--config', configAbsPath],
|
|
30
|
+
args: ['serve', '--stdio', '--config', configAbsPath],
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
};
|
|
@@ -54,7 +54,7 @@ function launchEditor(editor) {
|
|
|
54
54
|
const cmd = editor;
|
|
55
55
|
const args = ['.'];
|
|
56
56
|
console.log('');
|
|
57
|
-
console.log(chalk.dim(
|
|
57
|
+
console.log(chalk.dim(` Opening ${cmd}...`));
|
|
58
58
|
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
59
59
|
child.on('error', (err) => {
|
|
60
60
|
if (err.code === 'ENOENT') {
|
|
@@ -71,23 +71,35 @@ export async function runStart(options = {}) {
|
|
|
71
71
|
const cwd = process.cwd();
|
|
72
72
|
const configPath = options.config ?? 'infrawise.yaml';
|
|
73
73
|
const configAbsPath = path.resolve(cwd, configPath);
|
|
74
|
-
//
|
|
74
|
+
// --rediscover: wipe yaml + entire .infrawise dir, then re-run discovery and analysis
|
|
75
|
+
if (options.rediscover) {
|
|
76
|
+
if (fs.existsSync(configAbsPath)) {
|
|
77
|
+
fs.unlinkSync(configAbsPath);
|
|
78
|
+
}
|
|
79
|
+
const infrawiseDir = path.join(path.dirname(configAbsPath), '.infrawise');
|
|
80
|
+
if (fs.existsSync(infrawiseDir)) {
|
|
81
|
+
fs.rmSync(infrawiseDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
log.warn('Cleared config and .infrawise — rediscovering...');
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
|
86
|
+
// Generate config if missing
|
|
75
87
|
if (!fs.existsSync(configAbsPath)) {
|
|
76
|
-
log.warn('No infrawise.yaml found —
|
|
88
|
+
log.warn('No infrawise.yaml found — probing environment...');
|
|
77
89
|
console.log('');
|
|
78
|
-
await
|
|
90
|
+
await runDiscover({ interactive: options.interactive });
|
|
79
91
|
console.log('');
|
|
80
92
|
}
|
|
81
|
-
// Validate config
|
|
93
|
+
// Validate config
|
|
82
94
|
try {
|
|
83
|
-
loadConfig(
|
|
95
|
+
loadConfig(configAbsPath);
|
|
84
96
|
}
|
|
85
97
|
catch (err) {
|
|
86
98
|
console.error(formatError(err));
|
|
87
99
|
process.exit(1);
|
|
88
100
|
}
|
|
89
101
|
setCacheDir(path.dirname(configAbsPath));
|
|
90
|
-
//
|
|
102
|
+
// Check cache, re-analyze if stale
|
|
91
103
|
const cachedGraph = readCache('graph', CACHE_TTL_MS);
|
|
92
104
|
const cachedFindings = readCache('findings', CACHE_TTL_MS);
|
|
93
105
|
if (cachedGraph && cachedFindings) {
|
|
@@ -100,12 +112,12 @@ export async function runStart(options = {}) {
|
|
|
100
112
|
await runAnalyze({ config: options.config, silent: true });
|
|
101
113
|
console.log('');
|
|
102
114
|
}
|
|
103
|
-
//
|
|
115
|
+
// Write editor MCP config files
|
|
104
116
|
console.log('');
|
|
105
117
|
writeMcpJson(configAbsPath);
|
|
106
118
|
if (options.cursor)
|
|
107
119
|
writeCursorMcp(configAbsPath);
|
|
108
|
-
//
|
|
120
|
+
// Launch editor or print instructions
|
|
109
121
|
const editor = options.claude ? 'claude' : options.cursor ? 'cursor' : null;
|
|
110
122
|
if (!editor) {
|
|
111
123
|
console.log('');
|
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,6 @@ 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
|
-
kafka: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
60
58
|
apiGateway: z.object({ enabled: z.boolean().optional().default(false) }).optional(),
|
|
61
59
|
cloudwatchLogs: z
|
|
62
60
|
.object({
|
|
@@ -84,13 +82,32 @@ export class ConfigError extends Error {
|
|
|
84
82
|
this.name = 'ConfigError';
|
|
85
83
|
}
|
|
86
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
|
+
}
|
|
87
104
|
export function loadConfig(configPath) {
|
|
88
105
|
const resolvedPath = configPath
|
|
89
106
|
? path.resolve(configPath)
|
|
90
107
|
: path.resolve(process.cwd(), 'infrawise.yaml');
|
|
91
108
|
if (!fs.existsSync(resolvedPath)) {
|
|
92
109
|
throw new ConfigError(`Configuration file not found at: ${resolvedPath}`, [
|
|
93
|
-
'Run `infrawise
|
|
110
|
+
'Run `infrawise start` to generate a configuration file',
|
|
94
111
|
`Or specify a path with --config <path>`,
|
|
95
112
|
]);
|
|
96
113
|
}
|
|
@@ -118,7 +135,19 @@ export function loadConfig(configPath) {
|
|
|
118
135
|
const details = result.error.issues.map((e) => ` - ${e.path.join('.')}: ${e.message}`);
|
|
119
136
|
throw new ConfigError('Configuration validation failed', details);
|
|
120
137
|
}
|
|
121
|
-
|
|
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;
|
|
122
151
|
}
|
|
123
152
|
export function generateDefaultConfig(projectName, options) {
|
|
124
153
|
const config = {
|
|
@@ -126,7 +155,6 @@ export function generateDefaultConfig(projectName, options) {
|
|
|
126
155
|
aws: {
|
|
127
156
|
profile: options?.aws?.profile ?? '',
|
|
128
157
|
region: options?.aws?.region ?? 'us-east-1',
|
|
129
|
-
...(options?.aws?.endpoint ? { endpoint: options.aws.endpoint } : {}),
|
|
130
158
|
},
|
|
131
159
|
dynamodb: {
|
|
132
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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": [
|
|
@@ -65,12 +65,12 @@
|
|
|
65
65
|
"build-arch": "node scripts/build-arch.mjs",
|
|
66
66
|
"generate-diagrams": "node scripts/build-arch.mjs",
|
|
67
67
|
"record": "bash docs/demo/record.sh",
|
|
68
|
-
"dev": "node dist/cli/index.js
|
|
68
|
+
"dev": "node dist/cli/index.js serve",
|
|
69
69
|
"release": "node scripts/release.js",
|
|
70
70
|
"prepare": "simple-git-hooks"
|
|
71
71
|
},
|
|
72
72
|
"simple-git-hooks": {
|
|
73
|
-
"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"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@aws-sdk/client-api-gateway": "^3.1068.0",
|
|
@@ -103,20 +103,16 @@
|
|
|
103
103
|
"zod": "^4.4.3"
|
|
104
104
|
},
|
|
105
105
|
"devDependencies": {
|
|
106
|
-
"@mermaid-js/mermaid-cli": "^11.15.0",
|
|
107
|
-
"@types/inquirer": "^9.0.9",
|
|
108
106
|
"@types/js-yaml": "^4.0.9",
|
|
109
107
|
"@types/node": "^25.8.0",
|
|
110
108
|
"@types/pg": "^8.11.0",
|
|
111
109
|
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
|
112
110
|
"@typescript-eslint/parser": "^8.59.3",
|
|
113
111
|
"@vitest/coverage-v8": "^4.1.6",
|
|
114
|
-
"@xmldom/xmldom": "^0.9.10",
|
|
115
112
|
"eslint": "^10.4.0",
|
|
116
113
|
"prettier": "^3.8.3",
|
|
117
114
|
"simple-git-hooks": "^2.13.1",
|
|
118
115
|
"typescript": "^6.0.3",
|
|
119
|
-
"vite": "^8.0.13",
|
|
120
116
|
"vitest": "^4.1.6"
|
|
121
117
|
},
|
|
122
118
|
"files": [
|
|
@@ -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
|
-
}
|