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 CHANGED
@@ -55,35 +55,32 @@ npm install -g infrawise
55
55
  or use without installing:
56
56
 
57
57
  ```bash
58
- npx infrawise init
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 init
67
+ infrawise start --claude
70
68
  ```
71
69
 
72
- Detects your AWS profile and region, asks a few questions, writes `infrawise.yaml`. That's the only file it creates in your project.
73
-
74
- **2. Validate everything is connected**
70
+ That's it. Infrawise will:
75
71
 
76
- ```bash
77
- infrawise doctor
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
- **3. Run analysis**
77
+ **Every time after:**
81
78
 
82
79
  ```bash
83
- infrawise analyze
80
+ claude # no infrawise command needed — editor manages the connection
84
81
  ```
85
82
 
86
- Or skip this step `infrawise dev` auto-runs analysis if no cache exists.
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
- ### Step 1: Start the MCP server
104
+ ### Claude Code (recommended)
108
105
 
109
106
  ```bash
110
- infrawise dev
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
- ✔ Config loaded infrawise.yaml
115
- Cached analysis loaded 42 nodes · 18 edges · 7 finding(s)
116
- ✔ Server running
117
-
118
- ┌────────────────────────────────────────────────────┐
119
- │ MCP Server │
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
- ### Step 2: Add to your editor settings
126
+ Writes to `~/.codeium/windsurf/mcp_config.json` and opens Windsurf.
133
127
 
134
- **Claude Code** edit `.claude/settings.json` in your repo (project-level) or `~/.claude/settings.json` (global):
128
+ ### Any editor (no flag)
135
129
 
136
- ```json
137
- {
138
- "mcpServers": {
139
- "infrawise": {
140
- "url": "http://localhost:3000/mcp"
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
- To let Claude Code manage the server lifecycle automatically:
144
+ Add to your editor's MCP config:
147
145
 
148
146
  ```json
149
147
  {
150
148
  "mcpServers": {
151
149
  "infrawise": {
152
- "command": "infrawise",
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 | What it does |
186
- | ------------------- | ---------------------------------------------------------------------------- |
187
- | `infrawise init` | Detect AWS + repo, generate `infrawise.yaml` |
188
- | `infrawise auth` | Select or switch AWS profile |
189
- | `infrawise analyze` | Scan repo + AWS, build graph, print findings |
190
- | `infrawise dev` | Start MCP server — auto-analyzes if no cache, watches files for live refresh |
191
- | `infrawise stdio` | Start MCP server on stdio transport (for Claude Desktop) |
192
- | `infrawise doctor` | Validate AWS access, DB connectivity, and config |
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
- const displayFindings = minSeverity > 0
369
- ? findings.filter((f) => (SEVERITY_ORDER[f.severity] ?? 0) >= minSeverity)
370
- : findings;
371
- console.log('');
372
- if (displayFindings.length === 0) {
373
- const msg = minSeverity > 0
374
- ? `No ${options.severity} (or higher) severity issues found.`
375
- : 'Your infrastructure looks clean.';
376
- console.log(` ${chalk.green.bold(' No issues found!')} ${chalk.dim(msg)}`);
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
- if (options.output) {
388
- const report = buildMarkdownReport(displayFindings, config.project);
389
- const outPath = path.resolve(options.output);
390
- fs.writeFileSync(outPath, report, 'utf8');
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 core = await inquirer.prompt([
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: 'awsProfile',
34
- message: 'AWS profile:',
36
+ name: 'provider',
37
+ message: 'Infrastructure:',
35
38
  choices: [
36
- new inquirer.Separator('── no profile ──'),
37
- { name: 'Environment variables (CI/CD, real AWS)', value: '__env__' },
38
- { name: 'LocalStack (local development)', value: '__localstack__' },
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
- console.log('\n ' + chalk.bold('AWS Services'));
104
- console.log(chalk.dim(' Infrawise will introspect these services using the credentials configured above.'));
105
- const services = await inquirer.prompt([
106
- {
107
- type: 'confirm',
108
- name: 'dynamoEnabled',
109
- message: 'Introspect DynamoDB?',
110
- default: true,
111
- },
112
- {
113
- type: 'input',
114
- name: 'dynamoTables',
115
- message: 'DynamoDB tables to include:',
116
- default: '',
117
- suffix: chalk.dim(' (comma-separated, blank = all)'),
118
- when: (a) => a.dynamoEnabled,
119
- },
120
- {
121
- type: 'confirm',
122
- name: 'sqsEnabled',
123
- message: 'Introspect SQS queues?',
124
- default: true,
125
- },
126
- {
127
- type: 'confirm',
128
- name: 'snsEnabled',
129
- message: 'Introspect SNS topics?',
130
- default: true,
131
- },
132
- {
133
- type: 'confirm',
134
- name: 'ssmEnabled',
135
- message: 'Introspect SSM Parameter Store? (metadata only, no values)',
136
- default: true,
137
- },
138
- {
139
- type: 'input',
140
- name: 'ssmPaths',
141
- message: 'SSM path prefixes to filter:',
142
- default: '',
143
- suffix: chalk.dim(' (comma-separated, blank = all e.g. /myapp/prod)'),
144
- when: (a) => a.ssmEnabled,
145
- },
146
- {
147
- type: 'confirm',
148
- name: 'secretsEnabled',
149
- message: 'Introspect Secrets Manager? (names & rotation only, no values)',
150
- default: true,
151
- },
152
- {
153
- type: 'confirm',
154
- name: 'lambdaEnabled',
155
- message: 'Introspect Lambda functions?',
156
- default: true,
157
- },
158
- {
159
- type: 'confirm',
160
- name: 'eventbridgeEnabled',
161
- message: 'Introspect EventBridge rules?',
162
- default: true,
163
- },
164
- {
165
- type: 'confirm',
166
- name: 'rdsEnabled',
167
- message: 'Introspect RDS instances?',
168
- default: true,
169
- },
170
- {
171
- type: 'confirm',
172
- name: 'logsEnabled',
173
- message: 'Sample CloudWatch Logs? (error patterns only, no raw logs)',
174
- default: false,
175
- },
176
- {
177
- type: 'input',
178
- name: 'logGroupPrefixes',
179
- message: 'CloudWatch log group prefixes:',
180
- default: '',
181
- suffix: chalk.dim(' (comma-separated, blank = all)'),
182
- when: (a) => a.logsEnabled,
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 || 'http://localhost:4566' : undefined;
259
+ const resolvedEndpoint = isLocalStack ? (core.endpoint ?? 'http://localhost:4566') : undefined;
208
260
  const configContent = generateDefaultConfig(core.project, {
209
- aws: { profile: resolvedProfile, region: core.region, endpoint: resolvedEndpoint },
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
- console.log('');
240
- console.log(chalk.bold(' Next steps:'));
241
- log.info(`Run ${chalk.cyan('infrawise doctor')} to validate your setup`);
242
- log.info(`Run ${chalk.cyan('infrawise analyze')} to scan your infrastructure`);
243
- log.info(`Run ${chalk.cyan('infrawise dev')} to start the MCP server`);
244
- console.log('');
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 cachedGraph = readCache('graph');
15
- const cachedFindings = readCache('findings');
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, discover DynamoDB tables, and generate infrawise.yaml')
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 Fastify MCP server on localhost:3000')
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 (for Claude Desktop and Glama)')
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.9.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": [