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 CHANGED
@@ -69,10 +69,10 @@ infrawise start --claude
69
69
 
70
70
  That's it. Infrawise will:
71
71
 
72
- 1. Ask a few questions and generate `infrawise.yaml` (first time only)
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 15 MCP tools ready
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 15 infrawise tools are available in Cursor's MCP panel.
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 dev # starts server at http://localhost:3000/mcp
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 | What it does |
174
- | ---------------------------- | --------------------------------------------------------------------------------- |
175
- | `infrawise start` | **Primary command** — init + analyze + write editor MCP config, then exit |
176
- | `infrawise start --claude` | Same as above, then opens Claude Code |
177
- | `infrawise start --cursor` | Same as above, then opens Cursor |
178
- | `infrawise init` | Generate `infrawise.yaml` only (no analysis, no editor config) |
179
- | `infrawise auth` | Select or switch AWS profile |
180
- | `infrawise analyze` | Force a full re-scan — useful after major infrastructure changes |
181
- | `infrawise dev` | HTTP transport alternative starts server at `localhost:3000/mcp` |
182
- | `infrawise stdio` | Stdio transportauto-managed by editors via `.mcp.json` (rarely run directly) |
183
- | `infrawise doctor` | Validate AWS access, DB connectivity, and config |
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 gateanalyze 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` \| `verify` |
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 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.
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) | Producer/consumer topic mapping from code |
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 | Medium | Repeated query patterns from ORM loops |
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;
@@ -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;
@@ -63,7 +63,6 @@ export async function runAnalyze(options = {}) {
63
63
  const awsCfg = {
64
64
  region: config.aws?.region,
65
65
  profile: config.aws?.profile,
66
- endpoint: config.aws?.endpoint,
67
66
  };
68
67
  const dynamoMeta = [];
69
68
  const postgresMeta = [];
@@ -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 init',
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 () => {
@@ -0,0 +1,9 @@
1
+ import { runDev } from './dev.js';
2
+ import { runStdio } from './stdio.js';
3
+ export async function runServe(options = {}) {
4
+ if (options.stdio) {
5
+ await runStdio(options.config);
6
+ return;
7
+ }
8
+ await runDev({ config: options.config, port: options.port });
9
+ }
@@ -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 { runInit } from './init.js';
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(' Opening Cursor...'));
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
- // Step 1 create infrawise.yaml if missing
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 — running setup...');
88
+ log.warn('No infrawise.yaml found — probing environment...');
77
89
  console.log('');
78
- await runInit({ quiet: true });
90
+ await runDiscover({ interactive: options.interactive });
79
91
  console.log('');
80
92
  }
81
- // Validate config is loadable before proceeding
93
+ // Validate config
82
94
  try {
83
- loadConfig(configPath);
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
- // Step 2 — check cache with 24h TTL, re-analyze if stale or missing
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
- // Step 3 — write editor MCP config files
115
+ // Write editor MCP config files
104
116
  console.log('');
105
117
  writeMcpJson(configAbsPath);
106
118
  if (options.cursor)
107
119
  writeCursorMcp(configAbsPath);
108
- // Step 4 — launch editor or print instructions
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 { runDev } from './commands/dev.js';
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('Set up infrawise and connect your editor — runs init + analyze + writes editor MCP config')
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('dev')
68
- .description('Start MCP server over HTTP at localhost:3000 (alternative to stdio-based start)')
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('-p, --port <number>', 'Port to listen on', '3000')
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 runDev({
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('Validate AWS access, postgres connectivity, config, and repo scan')
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 '../../core/index.js';
6
- import { readAWSProfiles, detectAWSRegion, detectRepoType, log, printHeader } from '../utils.js';
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
- else if (provider === 'localstack') {
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, endpoint };
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 = isLocalStack || isEnvVars ? '' : core.awsProfile;
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')} to validate your setup`);
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 * as os from 'os';
3
+ import { execSync } from 'child_process';
4
4
  import chalk from 'chalk';
5
5
  // ─── AWS helpers ─────────────────────────────────────────────────────────────
6
6
  export function readAWSProfiles() {
7
- const credentialsPath = path.join(os.homedir(), '.aws', 'credentials');
8
- const configPath = path.join(os.homedir(), '.aws', 'config');
9
- const profiles = new Set();
10
- function parseFile(filePath) {
11
- if (!fs.existsSync(filePath))
12
- return;
13
- for (const line of fs.readFileSync(filePath, 'utf-8').split('\n')) {
14
- const match = line.match(/^\[(.+)\]$/);
15
- if (match?.[1]) {
16
- let name = match[1];
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 configPath = path.join(os.homedir(), '.aws', 'config');
33
- if (fs.existsSync(configPath)) {
34
- const match = fs.readFileSync(configPath, 'utf-8').match(/region\s*=\s*(.+)/);
35
- if (match?.[1])
36
- return match[1].trim();
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 {
@@ -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 init` to generate a configuration file',
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
- return result.data;
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,
@@ -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 init');
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.11.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 dev",
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 src/ && pnpm lint && pnpm typecheck && pnpm test"
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
- }