infrawise 0.9.0 → 0.10.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 +54 -55
- package/dist/cli/commands/analyze.js +28 -27
- package/dist/cli/commands/init.js +170 -113
- package/dist/cli/commands/start.js +154 -0
- package/dist/cli/commands/stdio.js +52 -6
- package/dist/cli/index.js +20 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,35 +55,32 @@ npm install -g infrawise
|
|
|
55
55
|
or use without installing:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
npx infrawise
|
|
58
|
+
npx infrawise start --claude
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
63
|
## Quick start
|
|
64
64
|
|
|
65
|
-
**1. Initialize in your repo**
|
|
66
|
-
|
|
67
65
|
```bash
|
|
68
66
|
cd your-project
|
|
69
|
-
infrawise
|
|
67
|
+
infrawise start --claude
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
**2. Validate everything is connected**
|
|
70
|
+
That's it. Infrawise will:
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
1. Ask a few questions and generate `infrawise.yaml` (first time only)
|
|
73
|
+
2. Scan your AWS services, databases, and codebase
|
|
74
|
+
3. Write `.mcp.json` so your editor auto-connects on every future launch
|
|
75
|
+
4. Open Claude Code with all 15 MCP tools ready
|
|
79
76
|
|
|
80
|
-
**
|
|
77
|
+
**Every time after:**
|
|
81
78
|
|
|
82
79
|
```bash
|
|
83
|
-
infrawise
|
|
80
|
+
claude # no infrawise command needed — editor manages the connection
|
|
84
81
|
```
|
|
85
82
|
|
|
86
|
-
|
|
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.
|
|
87
84
|
|
|
88
85
|
```
|
|
89
86
|
Findings (3 total)
|
|
@@ -104,60 +101,58 @@ Findings (3 total)
|
|
|
104
101
|
|
|
105
102
|
## Using with AI coding assistants
|
|
106
103
|
|
|
107
|
-
###
|
|
104
|
+
### Claude Code (recommended)
|
|
108
105
|
|
|
109
106
|
```bash
|
|
110
|
-
infrawise
|
|
107
|
+
infrawise start --claude
|
|
111
108
|
```
|
|
112
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.
|
|
111
|
+
|
|
112
|
+
### Cursor
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
infrawise start --cursor
|
|
113
116
|
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
│ POST http://localhost:3000/mcp │
|
|
122
|
-
│ GET http://localhost:3000/health │
|
|
123
|
-
├────────────────────────────────────────────────────┤
|
|
124
|
-
│ Tools (13 active) │
|
|
125
|
-
│ get_infra_overview · get_graph_summary │
|
|
126
|
-
│ ... │
|
|
127
|
-
└────────────────────────────────────────────────────┘
|
|
128
|
-
|
|
129
|
-
Watching for file changes... Press Ctrl+C to stop
|
|
117
|
+
|
|
118
|
+
Writes `.cursor/mcp.json` and opens Cursor. All 15 infrawise tools are available in Cursor's MCP panel.
|
|
119
|
+
|
|
120
|
+
### Windsurf
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
infrawise start --windsurf
|
|
130
124
|
```
|
|
131
125
|
|
|
132
|
-
|
|
126
|
+
Writes to `~/.codeium/windsurf/mcp_config.json` and opens Windsurf.
|
|
133
127
|
|
|
134
|
-
|
|
128
|
+
### Any editor (no flag)
|
|
135
129
|
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
130
|
+
```bash
|
|
131
|
+
infrawise start
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
### HTTP transport (alternative)
|
|
137
|
+
|
|
138
|
+
If your editor or workflow requires an HTTP MCP endpoint instead of stdio:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
infrawise dev # starts server at http://localhost:3000/mcp
|
|
144
142
|
```
|
|
145
143
|
|
|
146
|
-
|
|
144
|
+
Add to your editor's MCP config:
|
|
147
145
|
|
|
148
146
|
```json
|
|
149
147
|
{
|
|
150
148
|
"mcpServers": {
|
|
151
149
|
"infrawise": {
|
|
152
|
-
"
|
|
153
|
-
"args": ["dev"]
|
|
150
|
+
"url": "http://localhost:3000/mcp"
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
}
|
|
157
154
|
```
|
|
158
155
|
|
|
159
|
-
**Cursor** and **Windsurf** — add `http://localhost:3000/mcp` as an MCP server in editor settings.
|
|
160
|
-
|
|
161
156
|
### MCP tools
|
|
162
157
|
|
|
163
158
|
| Tool | What it provides |
|
|
@@ -182,14 +177,18 @@ To let Claude Code manage the server lifecycle automatically:
|
|
|
182
177
|
|
|
183
178
|
## CLI reference
|
|
184
179
|
|
|
185
|
-
| Command
|
|
186
|
-
|
|
|
187
|
-
| `infrawise
|
|
188
|
-
| `infrawise
|
|
189
|
-
| `infrawise
|
|
190
|
-
| `infrawise
|
|
191
|
-
| `infrawise
|
|
192
|
-
| `infrawise
|
|
180
|
+
| Command | What it does |
|
|
181
|
+
| ---------------------------- | --------------------------------------------------------------------------------- |
|
|
182
|
+
| `infrawise start` | **Primary command** — init + analyze + write editor MCP config, then exit |
|
|
183
|
+
| `infrawise start --claude` | Same as above, then opens Claude Code |
|
|
184
|
+
| `infrawise start --cursor` | Same as above, then opens Cursor |
|
|
185
|
+
| `infrawise start --windsurf` | Same as above, then opens Windsurf |
|
|
186
|
+
| `infrawise init` | Generate `infrawise.yaml` only (no analysis, no editor config) |
|
|
187
|
+
| `infrawise auth` | Select or switch AWS profile |
|
|
188
|
+
| `infrawise analyze` | Force a full re-scan — useful after major infrastructure changes |
|
|
189
|
+
| `infrawise dev` | HTTP transport alternative — starts server at `localhost:3000/mcp` |
|
|
190
|
+
| `infrawise stdio` | Stdio transport — auto-managed by editors via `.mcp.json` (rarely run directly) |
|
|
191
|
+
| `infrawise doctor` | Validate AWS access, DB connectivity, and config |
|
|
193
192
|
|
|
194
193
|
### `infrawise analyze` options
|
|
195
194
|
|
|
@@ -216,7 +215,7 @@ infrawise analyze --severity high --output report.md
|
|
|
216
215
|
|
|
217
216
|
## Configuration
|
|
218
217
|
|
|
219
|
-
`infrawise.yaml` is generated by `infrawise init` and lives in your repo root. Every service must be explicitly `enabled: true` — infrawise never connects to anything not listed in config.
|
|
218
|
+
`infrawise.yaml` is generated by `infrawise start` (or `infrawise init` to create the file only) and lives in your repo root. Every service must be explicitly `enabled: true` — infrawise never connects to anything not listed in config.
|
|
220
219
|
|
|
221
220
|
Connection strings support `${ENV_VAR}` substitution so passwords never need to be committed:
|
|
222
221
|
|
|
@@ -365,36 +365,37 @@ export async function runAnalyze(options = {}) {
|
|
|
365
365
|
});
|
|
366
366
|
}
|
|
367
367
|
// ── Output ────────────────────────────────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
else {
|
|
379
|
-
const filterNote = minSeverity > 0 ? chalk.dim(` (${options.severity}+ only)`) : '';
|
|
380
|
-
console.log(chalk.bold(` Findings`) + chalk.dim(` ${displayFindings.length} total`) + filterNote);
|
|
381
|
-
displayFindings.forEach((f, i) => printFinding(f, i));
|
|
382
|
-
printSummaryBox(displayFindings);
|
|
383
|
-
if (displayFindings.some((f) => f.severity === 'high')) {
|
|
384
|
-
console.log(`\n ${chalk.red.bold('Action required:')} ${chalk.red('High severity issues detected.')}`);
|
|
368
|
+
if (!options.silent) {
|
|
369
|
+
const displayFindings = minSeverity > 0
|
|
370
|
+
? findings.filter((f) => (SEVERITY_ORDER[f.severity] ?? 0) >= minSeverity)
|
|
371
|
+
: findings;
|
|
372
|
+
console.log('');
|
|
373
|
+
if (displayFindings.length === 0) {
|
|
374
|
+
const msg = minSeverity > 0
|
|
375
|
+
? `No ${options.severity} (or higher) severity issues found.`
|
|
376
|
+
: 'Your infrastructure looks clean.';
|
|
377
|
+
console.log(` ${chalk.green.bold('✓ No issues found!')} ${chalk.dim(msg)}`);
|
|
385
378
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
379
|
+
else {
|
|
380
|
+
const filterNote = minSeverity > 0 ? chalk.dim(` (${options.severity}+ only)`) : '';
|
|
381
|
+
console.log(chalk.bold(` Findings`) + chalk.dim(` ${displayFindings.length} total`) + filterNote);
|
|
382
|
+
displayFindings.forEach((f, i) => printFinding(f, i));
|
|
383
|
+
printSummaryBox(displayFindings);
|
|
384
|
+
if (displayFindings.some((f) => f.severity === 'high')) {
|
|
385
|
+
console.log(`\n ${chalk.red.bold('Action required:')} ${chalk.red('High severity issues detected.')}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (options.output) {
|
|
389
|
+
const report = buildMarkdownReport(displayFindings, config.project);
|
|
390
|
+
const outPath = path.resolve(options.output);
|
|
391
|
+
fs.writeFileSync(outPath, report, 'utf8');
|
|
392
|
+
console.log('');
|
|
393
|
+
log.success('Report saved', outPath);
|
|
394
|
+
}
|
|
395
|
+
console.log('');
|
|
396
|
+
log.dim(`Results cached in .infrawise/cache/`);
|
|
391
397
|
console.log('');
|
|
392
|
-
log.success('Report saved', outPath);
|
|
393
398
|
}
|
|
394
|
-
console.log('');
|
|
395
|
-
log.dim(`Results cached in .infrawise/cache/`);
|
|
396
|
-
log.info(`Run ${chalk.cyan('infrawise dev')} to explore via the MCP server`);
|
|
397
|
-
console.log('');
|
|
398
399
|
}
|
|
399
400
|
export async function runCodeRefresh(repoPath, config) {
|
|
400
401
|
const cached = readCache('meta', 60 * 60 * 1000);
|
|
@@ -21,40 +21,73 @@ export async function runInit(options = {}) {
|
|
|
21
21
|
log.success(`AWS profiles found`, String(profiles.length));
|
|
22
22
|
console.log('');
|
|
23
23
|
// ── Core settings ──────────────────────────────────────────────────────────
|
|
24
|
-
const
|
|
24
|
+
const { project } = await inquirer.prompt([
|
|
25
25
|
{
|
|
26
26
|
type: 'input',
|
|
27
27
|
name: 'project',
|
|
28
28
|
message: 'Project name:',
|
|
29
29
|
default: repoName,
|
|
30
30
|
},
|
|
31
|
+
]);
|
|
32
|
+
// ── Step 1: provider ───────────────────────────────────────────────────────
|
|
33
|
+
const { provider } = await inquirer.prompt([
|
|
31
34
|
{
|
|
32
35
|
type: 'select',
|
|
33
|
-
name: '
|
|
34
|
-
message: '
|
|
36
|
+
name: 'provider',
|
|
37
|
+
message: 'Infrastructure:',
|
|
35
38
|
choices: [
|
|
36
|
-
|
|
37
|
-
{ name: '
|
|
38
|
-
{ name: '
|
|
39
|
-
new inquirer.Separator('── named profiles ──'),
|
|
40
|
-
...profiles,
|
|
39
|
+
{ name: 'AWS', value: 'aws' },
|
|
40
|
+
{ name: 'LocalStack (mimics AWS locally)', value: 'localstack' },
|
|
41
|
+
{ name: 'Local (no cloud — databases, queues, self-hosted services)', value: 'local' },
|
|
41
42
|
],
|
|
42
|
-
default: profiles[0],
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
type: 'input',
|
|
46
|
-
name: 'region',
|
|
47
|
-
message: 'AWS region:',
|
|
48
|
-
default: detectedRegion,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
type: 'input',
|
|
52
|
-
name: 'endpoint',
|
|
53
|
-
message: 'LocalStack endpoint:',
|
|
54
|
-
default: 'http://localhost:4566',
|
|
55
|
-
when: (a) => a.awsProfile === '__localstack__',
|
|
56
43
|
},
|
|
57
44
|
]);
|
|
45
|
+
// ── Step 2: AWS profile (aws only) ────────────────────────────────────────
|
|
46
|
+
let awsProfile = '__env__';
|
|
47
|
+
if (provider === 'aws') {
|
|
48
|
+
const answer = await inquirer.prompt([
|
|
49
|
+
{
|
|
50
|
+
type: 'select',
|
|
51
|
+
name: 'awsProfile',
|
|
52
|
+
message: 'AWS profile:',
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: 'Environment variables (CI/CD)', value: '__env__' },
|
|
55
|
+
...(profiles.length ? [new inquirer.Separator('── named profiles ──'), ...profiles] : []),
|
|
56
|
+
],
|
|
57
|
+
default: profiles[0] ?? '__env__',
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
awsProfile = answer.awsProfile;
|
|
61
|
+
}
|
|
62
|
+
else if (provider === 'localstack') {
|
|
63
|
+
awsProfile = '__localstack__';
|
|
64
|
+
}
|
|
65
|
+
// ── Step 3: region + endpoint ─────────────────────────────────────────────
|
|
66
|
+
let region = detectedRegion;
|
|
67
|
+
let endpoint;
|
|
68
|
+
if (provider !== 'local') {
|
|
69
|
+
const regionAnswer = await inquirer.prompt([
|
|
70
|
+
{
|
|
71
|
+
type: 'input',
|
|
72
|
+
name: 'region',
|
|
73
|
+
message: 'AWS region:',
|
|
74
|
+
default: detectedRegion,
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
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
|
+
}
|
|
90
|
+
const core = { project, awsProfile, region, endpoint };
|
|
58
91
|
// ── Databases ──────────────────────────────────────────────────────────────
|
|
59
92
|
console.log('\n ' + chalk.bold('Databases'));
|
|
60
93
|
console.log(chalk.dim(' Self-hosted databases (PostgreSQL, MySQL, MongoDB).'));
|
|
@@ -99,89 +132,108 @@ export async function runInit(options = {}) {
|
|
|
99
132
|
when: (a) => a.mongoEnabled,
|
|
100
133
|
},
|
|
101
134
|
]);
|
|
102
|
-
// ── AWS services
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
135
|
+
// ── AWS services (skipped for local) ─────────────────────────────────────
|
|
136
|
+
let services;
|
|
137
|
+
if (provider === 'local') {
|
|
138
|
+
services = {
|
|
139
|
+
dynamoEnabled: false,
|
|
140
|
+
dynamoTables: '',
|
|
141
|
+
sqsEnabled: false,
|
|
142
|
+
snsEnabled: false,
|
|
143
|
+
ssmEnabled: false,
|
|
144
|
+
ssmPaths: '',
|
|
145
|
+
secretsEnabled: false,
|
|
146
|
+
lambdaEnabled: false,
|
|
147
|
+
eventbridgeEnabled: false,
|
|
148
|
+
rdsEnabled: false,
|
|
149
|
+
logsEnabled: false,
|
|
150
|
+
logGroupPrefixes: '',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log('\n ' + chalk.bold('AWS Services'));
|
|
155
|
+
console.log(chalk.dim(' Infrawise will introspect these services using the credentials configured above.'));
|
|
156
|
+
services = await inquirer.prompt([
|
|
157
|
+
{
|
|
158
|
+
type: 'confirm',
|
|
159
|
+
name: 'dynamoEnabled',
|
|
160
|
+
message: 'Introspect DynamoDB?',
|
|
161
|
+
default: true,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: 'input',
|
|
165
|
+
name: 'dynamoTables',
|
|
166
|
+
message: 'DynamoDB tables to include:',
|
|
167
|
+
default: '',
|
|
168
|
+
suffix: chalk.dim(' (comma-separated, blank = all)'),
|
|
169
|
+
when: (a) => a.dynamoEnabled,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: 'confirm',
|
|
173
|
+
name: 'sqsEnabled',
|
|
174
|
+
message: 'Introspect SQS queues?',
|
|
175
|
+
default: true,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: 'confirm',
|
|
179
|
+
name: 'snsEnabled',
|
|
180
|
+
message: 'Introspect SNS topics?',
|
|
181
|
+
default: true,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: 'confirm',
|
|
185
|
+
name: 'ssmEnabled',
|
|
186
|
+
message: 'Introspect SSM Parameter Store? (metadata only, no values)',
|
|
187
|
+
default: true,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'input',
|
|
191
|
+
name: 'ssmPaths',
|
|
192
|
+
message: 'SSM path prefixes to filter:',
|
|
193
|
+
default: '',
|
|
194
|
+
suffix: chalk.dim(' (comma-separated, blank = all e.g. /myapp/prod)'),
|
|
195
|
+
when: (a) => a.ssmEnabled,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'confirm',
|
|
199
|
+
name: 'secretsEnabled',
|
|
200
|
+
message: 'Introspect Secrets Manager? (names & rotation only, no values)',
|
|
201
|
+
default: true,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: 'confirm',
|
|
205
|
+
name: 'lambdaEnabled',
|
|
206
|
+
message: 'Introspect Lambda functions?',
|
|
207
|
+
default: true,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: 'confirm',
|
|
211
|
+
name: 'eventbridgeEnabled',
|
|
212
|
+
message: 'Introspect EventBridge rules?',
|
|
213
|
+
default: true,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
type: 'confirm',
|
|
217
|
+
name: 'rdsEnabled',
|
|
218
|
+
message: 'Introspect RDS instances?',
|
|
219
|
+
default: true,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'confirm',
|
|
223
|
+
name: 'logsEnabled',
|
|
224
|
+
message: 'Sample CloudWatch Logs? (error patterns only, no raw logs)',
|
|
225
|
+
default: false,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'input',
|
|
229
|
+
name: 'logGroupPrefixes',
|
|
230
|
+
message: 'CloudWatch log group prefixes:',
|
|
231
|
+
default: '',
|
|
232
|
+
suffix: chalk.dim(' (comma-separated, blank = all)'),
|
|
233
|
+
when: (a) => a.logsEnabled,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
} // end else (provider !== 'local')
|
|
185
237
|
// ── Build config ───────────────────────────────────────────────────────────
|
|
186
238
|
const includeTables = services.dynamoTables
|
|
187
239
|
? services.dynamoTables
|
|
@@ -204,9 +256,13 @@ export async function runInit(options = {}) {
|
|
|
204
256
|
const isLocalStack = core.awsProfile === '__localstack__';
|
|
205
257
|
const isEnvVars = core.awsProfile === '__env__';
|
|
206
258
|
const resolvedProfile = isLocalStack || isEnvVars ? '' : core.awsProfile;
|
|
207
|
-
const resolvedEndpoint = isLocalStack ? core.endpoint
|
|
259
|
+
const resolvedEndpoint = isLocalStack ? (core.endpoint ?? 'http://localhost:4566') : undefined;
|
|
208
260
|
const configContent = generateDefaultConfig(core.project, {
|
|
209
|
-
aws: {
|
|
261
|
+
aws: {
|
|
262
|
+
profile: resolvedProfile,
|
|
263
|
+
region: core.region ?? detectedRegion,
|
|
264
|
+
endpoint: resolvedEndpoint,
|
|
265
|
+
},
|
|
210
266
|
dynamodb: { enabled: services.dynamoEnabled, includeTables },
|
|
211
267
|
postgres: {
|
|
212
268
|
enabled: databases.pgEnabled,
|
|
@@ -236,10 +292,11 @@ export async function runInit(options = {}) {
|
|
|
236
292
|
fs.writeFileSync(configPath, configContent, 'utf-8');
|
|
237
293
|
console.log('');
|
|
238
294
|
log.success(`Created ${chalk.bold('infrawise.yaml')}`);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
295
|
+
if (!options.quiet) {
|
|
296
|
+
console.log('');
|
|
297
|
+
console.log(chalk.bold(' Next steps:'));
|
|
298
|
+
log.info(`Run ${chalk.cyan('infrawise start')} to analyze and connect your editor`);
|
|
299
|
+
log.info(`Run ${chalk.cyan('infrawise doctor')} to validate your setup`);
|
|
300
|
+
console.log('');
|
|
301
|
+
}
|
|
245
302
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { readCache, setCacheDir, formatError, loadConfig } from '../../core/index.js';
|
|
6
|
+
import { log, printHeader } from '../utils.js';
|
|
7
|
+
import { runInit } from './init.js';
|
|
8
|
+
import { runAnalyze } from './analyze.js';
|
|
9
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
function writeMcpJson(configAbsPath) {
|
|
11
|
+
const entry = {
|
|
12
|
+
mcpServers: {
|
|
13
|
+
infrawise: {
|
|
14
|
+
command: 'infrawise',
|
|
15
|
+
args: ['stdio', '--config', configAbsPath],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
fs.writeFileSync('.mcp.json', JSON.stringify(entry, null, 2), 'utf-8');
|
|
20
|
+
log.success('MCP config written', '.mcp.json');
|
|
21
|
+
}
|
|
22
|
+
function writeCursorMcp(configAbsPath) {
|
|
23
|
+
const dir = '.cursor';
|
|
24
|
+
if (!fs.existsSync(dir))
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
const entry = {
|
|
27
|
+
mcpServers: {
|
|
28
|
+
infrawise: {
|
|
29
|
+
command: 'infrawise',
|
|
30
|
+
args: ['stdio', '--config', configAbsPath],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
fs.writeFileSync(path.join(dir, 'mcp.json'), JSON.stringify(entry, null, 2), 'utf-8');
|
|
35
|
+
log.success('Cursor config written', '.cursor/mcp.json');
|
|
36
|
+
}
|
|
37
|
+
function writeWindsurfMcp(configAbsPath) {
|
|
38
|
+
const windsurfDir = path.join(process.env.HOME ?? '', '.codeium', 'windsurf');
|
|
39
|
+
if (!fs.existsSync(windsurfDir))
|
|
40
|
+
fs.mkdirSync(windsurfDir, { recursive: true });
|
|
41
|
+
const mcpConfigPath = path.join(windsurfDir, 'mcp_config.json');
|
|
42
|
+
let existing = {};
|
|
43
|
+
if (fs.existsSync(mcpConfigPath)) {
|
|
44
|
+
try {
|
|
45
|
+
existing = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// corrupt file — overwrite cleanly
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const servers = existing.mcpServers ?? {};
|
|
52
|
+
servers['infrawise'] = {
|
|
53
|
+
command: 'infrawise',
|
|
54
|
+
args: ['stdio', '--config', configAbsPath],
|
|
55
|
+
};
|
|
56
|
+
existing.mcpServers = servers;
|
|
57
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
58
|
+
log.success('Windsurf config written', mcpConfigPath);
|
|
59
|
+
}
|
|
60
|
+
function launchEditor(editor) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
if (editor === 'claude') {
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(chalk.dim(' Starting Claude Code...'));
|
|
65
|
+
console.log('');
|
|
66
|
+
const child = spawn('claude', [], { stdio: 'inherit' });
|
|
67
|
+
child.on('error', (err) => {
|
|
68
|
+
if (err.code === 'ENOENT') {
|
|
69
|
+
console.error(chalk.red('\n Error: claude CLI not found.'));
|
|
70
|
+
console.error(chalk.dim(' Install it from https://claude.ai/code\n'));
|
|
71
|
+
}
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
child.on('exit', () => resolve());
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const cmd = editor;
|
|
78
|
+
const args = ['.'];
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(chalk.dim(` Opening ${editor === 'cursor' ? 'Cursor' : 'Windsurf'}...`));
|
|
81
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
82
|
+
child.on('error', (err) => {
|
|
83
|
+
if (err.code === 'ENOENT') {
|
|
84
|
+
console.error(chalk.red(`\n Error: ${cmd} CLI not found. Open it manually.\n`));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
child.unref();
|
|
88
|
+
resolve();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export async function runStart(options = {}) {
|
|
93
|
+
printHeader('Infrawise');
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
const configPath = options.config ?? 'infrawise.yaml';
|
|
96
|
+
const configAbsPath = path.resolve(cwd, configPath);
|
|
97
|
+
// Step 1 — create infrawise.yaml if missing
|
|
98
|
+
if (!fs.existsSync(configAbsPath)) {
|
|
99
|
+
log.warn('No infrawise.yaml found — running setup...');
|
|
100
|
+
console.log('');
|
|
101
|
+
await runInit({ quiet: true });
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
// Validate config is loadable before proceeding
|
|
105
|
+
try {
|
|
106
|
+
loadConfig(configPath);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.error(formatError(err));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
setCacheDir(path.dirname(configAbsPath));
|
|
113
|
+
// Step 2 — check cache with 24h TTL, re-analyze if stale or missing
|
|
114
|
+
const cachedGraph = readCache('graph', CACHE_TTL_MS);
|
|
115
|
+
const cachedFindings = readCache('findings', CACHE_TTL_MS);
|
|
116
|
+
if (cachedGraph && cachedFindings) {
|
|
117
|
+
log.success('Analysis loaded from cache', `${cachedGraph.nodes.length} nodes · ${cachedGraph.edges.length} edges · ${cachedFindings.length} finding(s)`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const reason = (cachedGraph ?? cachedFindings) ? 'Cache is stale (>24h)' : 'No cache found';
|
|
121
|
+
log.warn(`${reason} — running analysis...`);
|
|
122
|
+
console.log('');
|
|
123
|
+
await runAnalyze({ config: options.config, silent: true });
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
126
|
+
// Step 3 — write editor MCP config files
|
|
127
|
+
console.log('');
|
|
128
|
+
writeMcpJson(configAbsPath);
|
|
129
|
+
if (options.cursor)
|
|
130
|
+
writeCursorMcp(configAbsPath);
|
|
131
|
+
if (options.windsurf)
|
|
132
|
+
writeWindsurfMcp(configAbsPath);
|
|
133
|
+
// Step 4 — launch editor or print instructions
|
|
134
|
+
const editor = options.claude
|
|
135
|
+
? 'claude'
|
|
136
|
+
: options.cursor
|
|
137
|
+
? 'cursor'
|
|
138
|
+
: options.windsurf
|
|
139
|
+
? 'windsurf'
|
|
140
|
+
: null;
|
|
141
|
+
if (!editor) {
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(chalk.bold(' Setup complete — open your editor to start.'));
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(chalk.dim(' Claude Code: claude'));
|
|
146
|
+
console.log(chalk.dim(' Cursor: cursor .'));
|
|
147
|
+
console.log(chalk.dim(' Windsurf: windsurf .'));
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(chalk.dim(' Next time just open your editor — no infrawise command needed.'));
|
|
150
|
+
console.log('');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
await launchEditor(editor);
|
|
154
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
2
|
-
import { loadConfig, formatError, readCache } from '../../core/index.js';
|
|
4
|
+
import { loadConfig, formatError, readCache, setCacheDir } from '../../core/index.js';
|
|
3
5
|
import { createMcpServer, setGraphState } from '../../server/index.js';
|
|
4
|
-
import { runAnalyze } from './analyze.js';
|
|
6
|
+
import { runAnalyze, runCodeRefresh } from './analyze.js';
|
|
7
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
5
8
|
export async function runStdio(configPath) {
|
|
6
9
|
let config;
|
|
7
10
|
try {
|
|
@@ -11,17 +14,60 @@ export async function runStdio(configPath) {
|
|
|
11
14
|
process.stderr.write(formatError(err) + '\n');
|
|
12
15
|
process.exit(1);
|
|
13
16
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
17
|
+
const resolvedConfigPath = configPath ?? 'infrawise.yaml';
|
|
18
|
+
setCacheDir(path.dirname(path.resolve(resolvedConfigPath)));
|
|
19
|
+
const cachedGraph = readCache('graph', CACHE_TTL_MS);
|
|
20
|
+
const cachedFindings = readCache('findings', CACHE_TTL_MS);
|
|
16
21
|
if (cachedGraph && cachedFindings) {
|
|
17
22
|
setGraphState(cachedGraph, cachedFindings, config);
|
|
18
23
|
}
|
|
19
24
|
else {
|
|
20
25
|
await runAnalyze({ config: configPath });
|
|
21
|
-
const graph = readCache('graph') ?? { nodes: [], edges: [] };
|
|
22
|
-
const findings = readCache('findings') ?? [];
|
|
26
|
+
const graph = readCache('graph', CACHE_TTL_MS) ?? { nodes: [], edges: [] };
|
|
27
|
+
const findings = readCache('findings', CACHE_TTL_MS) ?? [];
|
|
23
28
|
setGraphState(graph, findings, config);
|
|
24
29
|
}
|
|
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
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// fs.watch recursive not supported on all platforms — silently skip
|
|
70
|
+
}
|
|
25
71
|
const mcp = createMcpServer();
|
|
26
72
|
const transport = new StdioServerTransport();
|
|
27
73
|
await mcp.connect(transport);
|
package/dist/cli/index.js
CHANGED
|
@@ -9,15 +9,32 @@ import { runAnalyze } from './commands/analyze.js';
|
|
|
9
9
|
import { runDev } from './commands/dev.js';
|
|
10
10
|
import { runDoctor } from './commands/doctor.js';
|
|
11
11
|
import { runStdio } from './commands/stdio.js';
|
|
12
|
+
import { runStart } from './commands/start.js';
|
|
12
13
|
const { version } = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
|
|
13
14
|
const program = new Command();
|
|
14
15
|
program
|
|
15
16
|
.name('infrawise')
|
|
16
17
|
.description('CLI-first infrastructure intelligence platform — analyze your databases, AWS services, and IaC')
|
|
17
18
|
.version(version);
|
|
19
|
+
program
|
|
20
|
+
.command('start')
|
|
21
|
+
.description('Set up infrawise and connect your editor — runs init + analyze + writes editor MCP config')
|
|
22
|
+
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
23
|
+
.option('--claude', 'Write .mcp.json and open Claude Code')
|
|
24
|
+
.option('--cursor', 'Write .cursor/mcp.json and open Cursor')
|
|
25
|
+
.option('--windsurf', 'Write Windsurf MCP config and open Windsurf')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
printBanner();
|
|
28
|
+
await runStart({
|
|
29
|
+
config: options.config !== 'infrawise.yaml' ? options.config : undefined,
|
|
30
|
+
claude: options.claude,
|
|
31
|
+
cursor: options.cursor,
|
|
32
|
+
windsurf: options.windsurf,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
18
35
|
program
|
|
19
36
|
.command('init')
|
|
20
|
-
.description('Detect AWS profile/region,
|
|
37
|
+
.description('Detect AWS profile/region, ask setup questions, and generate infrawise.yaml')
|
|
21
38
|
.option('--force', 'Overwrite existing infrawise.yaml')
|
|
22
39
|
.action(async (options) => {
|
|
23
40
|
printBanner();
|
|
@@ -50,7 +67,7 @@ program
|
|
|
50
67
|
});
|
|
51
68
|
program
|
|
52
69
|
.command('dev')
|
|
53
|
-
.description('Start
|
|
70
|
+
.description('Start MCP server over HTTP at localhost:3000 (alternative to stdio-based start)')
|
|
54
71
|
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
55
72
|
.option('-p, --port <number>', 'Port to listen on', '3000')
|
|
56
73
|
.action(async (options) => {
|
|
@@ -62,7 +79,7 @@ program
|
|
|
62
79
|
});
|
|
63
80
|
program
|
|
64
81
|
.command('stdio')
|
|
65
|
-
.description('Start MCP server on stdio transport
|
|
82
|
+
.description('Start MCP server on stdio transport — used by editors via .mcp.json (auto-managed)')
|
|
66
83
|
.option('-c, --config <path>', 'Path to infrawise.yaml', 'infrawise.yaml')
|
|
67
84
|
.action(async (options) => {
|
|
68
85
|
await runStdio(options.config !== 'infrawise.yaml' ? options.config : undefined);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
|
|
6
6
|
"keywords": [
|