devvami 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.js CHANGED
@@ -189,10 +189,72 @@
189
189
 
190
190
  /**
191
191
  * @typedef {Object} AWSCostEntry
192
- * @property {string} serviceName
193
- * @property {number} amount
194
- * @property {string} unit
195
- * @property {{ start: string, end: string }} period
192
+ * @property {string} serviceName - AWS service name (e.g. "Amazon EC2"), or tag value label for tag grouping
193
+ * @property {string} [tagValue] - Tag value when grouping by TAG or BOTH (e.g. "prod"); undefined for service-only grouping
194
+ * @property {number} amount - Cost amount (USD)
195
+ * @property {string} unit - Currency unit (always "USD")
196
+ * @property {{ start: string, end: string }} period - ISO date range (YYYY-MM-DD)
197
+ */
198
+
199
+ /**
200
+ * @typedef {'service' | 'tag' | 'both'} CostGroupMode
201
+ * The dimension used to group cost entries.
202
+ * - 'service': group by AWS service name (default, backward-compatible)
203
+ * - 'tag': group by a tag key's values (requires tagKey)
204
+ * - 'both': group by AWS service + tag value simultaneously
205
+ */
206
+
207
+ /**
208
+ * @typedef {Object} CostTrendPoint
209
+ * @property {string} date - ISO date (YYYY-MM-DD) for this data point
210
+ * @property {number} amount - Total cost for this day (USD)
211
+ * @property {string} [label] - Display label: serviceName for service/both grouping,
212
+ * tag value for tag grouping; omitted when not multi-series
213
+ */
214
+
215
+ /**
216
+ * @typedef {Object} CostTrendSeries
217
+ * @property {string} name - Series label (service name or tag value)
218
+ * @property {CostTrendPoint[]} points - Ordered daily data points (ascending by date)
219
+ */
220
+
221
+ /**
222
+ * @typedef {Object} LogGroup
223
+ * @property {string} name - Full log group name (e.g. "/aws/lambda/my-fn")
224
+ * @property {number} [storedBytes] - Total stored bytes (may be absent for empty groups)
225
+ * @property {number} [retentionDays] - Retention policy in days; undefined = never expire
226
+ * @property {string} [creationTime] - ISO8601 creation timestamp
227
+ */
228
+
229
+ /**
230
+ * @typedef {Object} LogEvent
231
+ * @property {string} eventId - Unique event ID assigned by CloudWatch
232
+ * @property {string} logStreamName - Stream within the log group (e.g. "2026/03/26/[$LATEST]abc")
233
+ * @property {number} timestamp - Event time as epoch milliseconds
234
+ * @property {string} message - Raw log message text
235
+ */
236
+
237
+ /**
238
+ * @typedef {Object} LogFilterResult
239
+ * @property {LogEvent[]} events - Matched log events (up to --limit)
240
+ * @property {boolean} truncated - True when the result was capped by --limit or AWS pagination
241
+ * @property {string} logGroupName - The log group that was queried
242
+ * @property {number} startTime - Query start as epoch milliseconds
243
+ * @property {number} endTime - Query end as epoch milliseconds
244
+ * @property {string} filterPattern - The pattern used ('' = no filter)
245
+ */
246
+
247
+ /**
248
+ * @typedef {Object} ChartBarData
249
+ * @property {string} name - Row label (service name, tag value, or "service / tag")
250
+ * @property {number} value - Cost amount (USD)
251
+ */
252
+
253
+ /**
254
+ * @typedef {Object} ChartSeries
255
+ * @property {string} name - Series label displayed in legend
256
+ * @property {number[]} values - Ordered numeric values (one per day, ~60 for 2 months)
257
+ * @property {string[]} labels - Date labels matching values array (YYYY-MM-DD)
196
258
  */
197
259
 
198
260
  /**
@@ -260,3 +322,69 @@
260
322
  * @property {string} owner - GitHub owner (org or user)
261
323
  * @property {string} repo - Repository name
262
324
  */
325
+
326
+ /**
327
+ * @typedef {Object} SecurityTool
328
+ * @property {string} id - Unique identifier (e.g., "aws-vault", "pass", "gpg", "gcm", "osxkeychain")
329
+ * @property {string} displayName - Human-readable name for UI output
330
+ * @property {'aws'|'git'|'dependency'} role - What the tool protects; 'dependency' = required by another tool
331
+ * @property {'not-installed'|'installed'|'misconfigured'|'skipped'|'n/a'} status - Status after check phase
332
+ * @property {Platform[]} platforms - Platforms where this tool applies
333
+ * @property {string|null} version - Detected version string, if available
334
+ * @property {string|null} hint - Actionable message when status is 'misconfigured' or 'not-installed'
335
+ */
336
+
337
+ /**
338
+ * @typedef {Object} SetupStep
339
+ * @property {string} id - Unique step identifier (e.g., "install-aws-vault", "init-pass")
340
+ * @property {string} label - Human-readable description shown to the developer
341
+ * @property {string} toolId - The SecurityTool this step belongs to
342
+ * @property {'check'|'install'|'configure'|'verify'} type - Step category
343
+ * @property {() => Promise<StepResult>} run - Async function that executes the step
344
+ * @property {boolean} requiresConfirmation - True for 'install' and 'configure' steps
345
+ * @property {boolean} [skippable] - True if the developer can skip this step without breaking subsequent steps
346
+ * @property {boolean} [gpgInteractive] - True if the step spawns GPG interactively (requires stdio:inherit)
347
+ */
348
+
349
+ /**
350
+ * @typedef {Object} StepResult
351
+ * @property {'success'|'skipped'|'failed'} status
352
+ * @property {string} [message] - Human-readable outcome message
353
+ * @property {string} [hint] - Actionable recovery suggestion shown only when status is 'failed'
354
+ * @property {string} [hintUrl] - Documentation URL to include with the hint
355
+ */
356
+
357
+ /**
358
+ * @typedef {Object} SetupSession
359
+ * @property {Platform} platform - Detected platform for this run
360
+ * @property {'aws'|'git'|'both'} selection - What the developer chose to set up
361
+ * @property {SetupStep[]} steps - Ordered list of steps for this session
362
+ * @property {Map<string, StepResult>} results - Map of stepId → StepResult
363
+ * @property {'in-progress'|'completed'|'failed'|'cancelled'} overallStatus - Aggregate status
364
+ */
365
+
366
+ /**
367
+ * @typedef {Object} GpgKey
368
+ * @property {string} id - Long key ID (16-character hex)
369
+ * @property {string} fingerprint - Full 40-character fingerprint
370
+ * @property {string} name - Associated name from the key's UID
371
+ * @property {string} email - Associated email from the key's UID
372
+ * @property {string|null} expiry - Expiry date as ISO8601 string, or null if no expiry
373
+ */
374
+
375
+ /**
376
+ * @typedef {Object} SecurityToolStatus
377
+ * @property {string} id - Tool id
378
+ * @property {string} displayName - Human-readable name
379
+ * @property {'not-installed'|'installed'|'misconfigured'|'n/a'} status - Current status
380
+ * @property {string|null} version - Detected version, if any
381
+ * @property {string|null} hint - Recovery hint if misconfigured
382
+ */
383
+
384
+ /**
385
+ * @typedef {Object} SecuritySetupJsonResult
386
+ * @property {Platform} platform - Detected platform
387
+ * @property {'aws'|'git'|'both'|null} selection - Selection made (null for --json health check)
388
+ * @property {SecurityToolStatus[]} tools - Status of each applicable tool
389
+ * @property {'success'|'partial'|'not-configured'} overallStatus - Aggregate status
390
+ */
@@ -0,0 +1,144 @@
1
+ import { loadConfigSync } from '../services/config.js'
2
+ import { execa } from 'execa'
3
+
4
+ /**
5
+ * Returns the aws-vault exec prefix to prepend to AWS CLI commands.
6
+ *
7
+ * Detection order:
8
+ * 1. process.env.AWS_VAULT — set by `aws-vault exec` at runtime in the child process
9
+ * 2. config.awsProfile — if an already-loaded config object is passed (avoids sync I/O)
10
+ * 3. loadConfigSync() — synchronous fallback for static getters where async is unavailable
11
+ *
12
+ * @param {{ awsProfile?: string } | null} [config] - Already-loaded config (optional).
13
+ * Pass this when the caller has already loaded config asynchronously to avoid a redundant sync read.
14
+ * @returns {string} e.g. `"aws-vault exec myprofile -- "` or `""`
15
+ *
16
+ * @example
17
+ * // Inside an async run() method where config is already loaded:
18
+ * const prefix = awsVaultPrefix(config)
19
+ * this.error(`No credentials. Use: ${prefix}dvmi costs get`)
20
+ *
21
+ * @example
22
+ * // Inside a static getter (no async available):
23
+ * static get examples() {
24
+ * const prefix = awsVaultPrefix()
25
+ * return [`${prefix}<%= config.bin %> costs get my-service`]
26
+ * }
27
+ */
28
+ export function awsVaultPrefix(config = null) {
29
+ // 1. Runtime env var — set by aws-vault exec in the subprocess environment
30
+ if (process.env.AWS_VAULT) return `aws-vault exec ${process.env.AWS_VAULT} -- `
31
+
32
+ // 2. Already-loaded config passed by the caller
33
+ if (config?.awsProfile) return `aws-vault exec ${config.awsProfile} -- `
34
+
35
+ // 3. Synchronous config read — fallback for static getters
36
+ const synced = loadConfigSync()
37
+ if (synced.awsProfile) return `aws-vault exec ${synced.awsProfile} -- `
38
+
39
+ return ''
40
+ }
41
+
42
+ /**
43
+ * Returns true when the current process already has AWS credentials in env.
44
+ * @returns {boolean}
45
+ */
46
+ export function hasAwsCredentialEnv() {
47
+ return Boolean(
48
+ process.env.AWS_ACCESS_KEY_ID ||
49
+ process.env.AWS_SESSION_TOKEN,
50
+ )
51
+ }
52
+
53
+ /**
54
+ * Returns true when this process is already running inside aws-vault exec.
55
+ * @returns {boolean}
56
+ */
57
+ export function isAwsVaultSession() {
58
+ return Boolean(process.env.AWS_VAULT)
59
+ }
60
+
61
+ /**
62
+ * Re-execute the current dvmi command under aws-vault and mirror stdio.
63
+ * Returns null when re-exec should not run.
64
+ *
65
+ * Guard conditions:
66
+ * - awsProfile must be configured
67
+ * - command must not already be inside aws-vault
68
+ * - process must not already have AWS credentials in env
69
+ * - re-exec must not have already happened in this process chain
70
+ *
71
+ * @param {{ awsProfile?: string } | null} [config]
72
+ * @returns {Promise<number | null>} child exit code or null when skipped
73
+ */
74
+ export async function reexecCurrentCommandWithAwsVault(config = null) {
75
+ const profile = config?.awsProfile ?? loadConfigSync().awsProfile
76
+ if (!profile) return null
77
+ if (isAwsVaultSession()) return null
78
+ if (hasAwsCredentialEnv()) return null
79
+ if (process.env.DVMI_AWS_VAULT_REEXEC === '1') return null
80
+
81
+ try {
82
+ const child = await execa(
83
+ 'aws-vault',
84
+ [
85
+ 'exec',
86
+ profile,
87
+ '--',
88
+ process.execPath,
89
+ ...process.argv.slice(1),
90
+ ],
91
+ {
92
+ reject: false,
93
+ stdio: 'inherit',
94
+ env: {
95
+ ...process.env,
96
+ DVMI_AWS_VAULT_REEXEC: '1',
97
+ },
98
+ },
99
+ )
100
+
101
+ return child.exitCode ?? 1
102
+ } catch {
103
+ // aws-vault missing or failed to spawn; fallback to normal execution path
104
+ return null
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Re-execute the current dvmi command under aws-vault using an explicit profile.
110
+ * This bypasses auto-detection guards and is intended for interactive recovery flows.
111
+ *
112
+ * @param {string} profile
113
+ * @param {Record<string, string>} [extraEnv]
114
+ * @returns {Promise<number | null>} child exit code or null when skipped/failed to spawn
115
+ */
116
+ export async function reexecCurrentCommandWithAwsVaultProfile(profile, extraEnv = {}) {
117
+ if (!profile) return null
118
+
119
+ try {
120
+ const child = await execa(
121
+ 'aws-vault',
122
+ [
123
+ 'exec',
124
+ profile,
125
+ '--',
126
+ process.execPath,
127
+ ...process.argv.slice(1),
128
+ ],
129
+ {
130
+ reject: false,
131
+ stdio: 'inherit',
132
+ env: {
133
+ ...process.env,
134
+ DVMI_AWS_VAULT_REEXEC: '1',
135
+ ...extraEnv,
136
+ },
137
+ },
138
+ )
139
+
140
+ return child.exitCode ?? 1
141
+ } catch {
142
+ return null
143
+ }
144
+ }
@@ -0,0 +1,173 @@
1
+ import chalk from 'chalk'
2
+ import { printBanner } from './banner.js'
3
+ import { isColorEnabled } from './gradient.js'
4
+ import { typewriterLine } from './typewriter.js'
5
+
6
+ // ─── Constants ────────────────────────────────────────────────────────────────
7
+
8
+ const STAGGER = 150
9
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms))
10
+ const out = (line) => process.stdout.write(line + '\n')
11
+ const nl = () => process.stdout.write('\n')
12
+
13
+ // ─── Color palette ────────────────────────────────────────────────────────────
14
+
15
+ const p = isColorEnabled
16
+ ? {
17
+ sep: (t) => chalk.hex('#4A9EFF').dim(t),
18
+ cyan: (t) => chalk.hex('#00D4FF').bold(t),
19
+ green: (t) => chalk.hex('#00FF88').bold(t),
20
+ pink: (t) => chalk.hex('#FF3399').bold(t),
21
+ gold: (t) => chalk.hex('#FFD700').bold(t),
22
+ orange: (t) => chalk.hex('#FF6B2B').bold(t),
23
+ blue: (t) => chalk.hex('#4A9EFF')(t),
24
+ white: (t) => chalk.white(t),
25
+ dim: (t) => chalk.dim(t),
26
+ }
27
+ : Object.fromEntries(
28
+ ['sep', 'cyan', 'green', 'pink', 'gold', 'orange', 'blue', 'white', 'dim'].map((k) => [
29
+ k,
30
+ (t) => t,
31
+ ]),
32
+ )
33
+
34
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Build a ruler-style section header.
38
+ * Example: " 🔐 SUPPLY CHAIN SECURITY ──────────────────────────────"
39
+ * No right-side border: dashes trail right, no alignment required.
40
+ *
41
+ * @param {string} icon
42
+ * @param {string} label
43
+ * @param {(t: string) => string} colorFn
44
+ * @returns {string}
45
+ */
46
+ function ruler(icon, label, colorFn) {
47
+ return colorFn(` ${icon} ${label} `) + p.sep('─'.repeat(40))
48
+ }
49
+
50
+ // ─── Welcome screen ───────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Print the full cyberpunk dvmi welcome screen.
54
+ * Shows the animated DVMI logo followed by a styled mission dashboard.
55
+ * Falls back to plain text in non-TTY / NO_COLOR / CI environments.
56
+ *
57
+ * @param {string} [version=''] - CLI version string (e.g. '2.1.0')
58
+ * @returns {Promise<void>}
59
+ */
60
+ export async function printWelcomeScreen(version = '') {
61
+ // ── 1. Animated DVMI logo ──────────────────────────────────────────────────
62
+ await printBanner()
63
+
64
+ // ── 2. Badge line (typewriter with brand gradient) ─────────────────────────
65
+ const versionTag = version ? `v${version} · ` : ''
66
+ await typewriterLine(` ◆ Developer Mission Interface · ${versionTag}Node >= 24`)
67
+
68
+ // ── 3. Connection established ──────────────────────────────────────────────
69
+ await delay(STAGGER)
70
+ nl()
71
+ out(p.sep(' ' + '─'.repeat(72)))
72
+ nl()
73
+ out(p.cyan(' LINK ESTABLISHED'))
74
+ nl()
75
+ out(p.white(' dvmi consolidates the operational surface of modern software delivery into'))
76
+ out(p.white(' one deterministic CLI experience. Instead of context switching across browser'))
77
+ out(p.white(' tabs, dashboards, and disconnected tools, you run critical workflows from a'))
78
+ out(p.white(' single terminal interface: consistent output, predictable behavior, full control.'))
79
+
80
+ // ── 4. Mission profile ─────────────────────────────────────────────────────
81
+ await delay(STAGGER)
82
+ nl()
83
+ out(ruler('⚙️ ', 'MISSION PROFILE :: WHAT THIS CLI DOES', p.cyan))
84
+ nl()
85
+ const mission = [
86
+ 'discover and manage repositories across your GitHub organization',
87
+ 'handle pull requests with faster review and decision flow',
88
+ 'monitor CI/CD pipelines, inspect failures, rerun with intent',
89
+ 'query and read technical documentation without leaving the shell',
90
+ 'track execution priorities through task-oriented commands',
91
+ 'inspect cloud costs early, before budget drift becomes an incident',
92
+ ]
93
+ for (const line of mission) {
94
+ out(p.dim(' - ') + p.white(line))
95
+ }
96
+
97
+ // ── 5. Supply chain security ───────────────────────────────────────────────
98
+ await delay(STAGGER)
99
+ nl()
100
+ out(ruler('🔐', "SUPPLY CHAIN SECURITY :: VERIFY, DON'T GUESS", p.green))
101
+ nl()
102
+ out(p.dim(' dvmi takes a security-first but pragmatic approach to software delivery:'))
103
+ nl()
104
+ const security = [
105
+ 'artifact integrity and provenance-aware delivery workflow',
106
+ 'dependency visibility with an SBOM mindset (SPDX / CycloneDX)',
107
+ 'continuous hygiene on dependency risk and secret exposure',
108
+ 'credential management via OS-native secure storage (keychain)',
109
+ 'less improvised shell procedures, more repeatable safe operations',
110
+ ]
111
+ for (const line of security) {
112
+ out(p.green(' ▸ ') + p.white(line))
113
+ }
114
+ nl()
115
+ out(p.dim(' Objective: reduce the risk surface without slowing down delivery.'))
116
+
117
+ // ── 6. DevEx high-velocity ─────────────────────────────────────────────────
118
+ await delay(STAGGER)
119
+ nl()
120
+ out(ruler('⚡', 'DEVEX HIGH-VELOCITY :: STAY IN FLOW', p.pink))
121
+ nl()
122
+ out(p.dim(' dvmi is designed to lower cognitive cost and keep you in flow:'))
123
+ nl()
124
+ const devex = [
125
+ 'less context switching between tools and dashboards',
126
+ 'less time spent hunting down "where did this break"',
127
+ 'faster "what is blocked / what is next" decision loops',
128
+ 'scriptable, composable output for automation and team workflows',
129
+ ]
130
+ for (const line of devex) {
131
+ out(p.pink(' ▸ ') + p.white(line))
132
+ }
133
+ nl()
134
+ out(p.dim(' No noise added. Operational signal only.'))
135
+
136
+ // ── 7. Delivery reliability ────────────────────────────────────────────────
137
+ await delay(STAGGER)
138
+ nl()
139
+ out(ruler('📡', 'DELIVERY RELIABILITY :: SHIP WITH CONTROL', p.orange))
140
+ nl()
141
+ out(p.white(' From PR readiness to pipeline health to release confidence,'))
142
+ out(p.white(' dvmi moves teams from reactive debugging to proactive control.'))
143
+ nl()
144
+ out(p.white(' Reliability is treated as a habit, not a phase.'))
145
+
146
+ // ── 8. Boot sequence ───────────────────────────────────────────────────────
147
+ await delay(STAGGER)
148
+ nl()
149
+ out(ruler('🚀', 'BOOT SEQUENCE', p.gold))
150
+ nl()
151
+
152
+ /** @type {Array<[string, string]>} */
153
+ const commands = [
154
+ ['dvmi init', 'configure your workspace'],
155
+ ['dvmi auth login', 'connect GitHub & ClickUp'],
156
+ ['dvmi pr status', 'open pull requests'],
157
+ ['dvmi pipeline status', 'CI/CD health check'],
158
+ ['dvmi tasks today', 'focus mode: what to ship today'],
159
+ ['dvmi costs get', 'AWS bill reality check'],
160
+ ['dvmi doctor', 'diagnose config issues'],
161
+ ]
162
+ for (const [cmd, comment] of commands) {
163
+ out(' ' + p.blue('$ ' + cmd.padEnd(24)) + p.dim('# ' + comment))
164
+ }
165
+
166
+ // ── 9. Closing ─────────────────────────────────────────────────────────────
167
+ await delay(STAGGER)
168
+ nl()
169
+ out(p.sep(' ' + '─'.repeat(72)))
170
+ nl()
171
+ out(p.dim(' DVMI PROTOCOL: ') + p.cyan('Ship fast. Verify everything.'))
172
+ nl()
173
+ }