devvami 1.2.0 → 1.4.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
@@ -14,6 +14,70 @@
14
14
  * @property {string} [latestVersion] - Latest known CLI version
15
15
  * @property {'opencode'|'copilot'} [aiTool] - Preferred AI tool for running prompts
16
16
  * @property {string} [promptsDir] - Local directory for downloaded prompts (default: .prompts)
17
+ * @property {DotfilesConfig} [dotfiles] - Chezmoi dotfiles configuration
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} DotfilesConfig
22
+ * @property {boolean} enabled - Whether chezmoi dotfiles management is active
23
+ * @property {string} [repo] - Remote dotfiles repository URL
24
+ * @property {string[]} [customSensitivePatterns] - User-added sensitive path patterns
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} DotfileEntry
29
+ * @property {string} path - Target file path (e.g. "/home/user/.zshrc")
30
+ * @property {string} sourcePath - Source path in chezmoi state
31
+ * @property {boolean} encrypted - Whether the file is stored with encryption
32
+ * @property {'file'|'dir'|'symlink'} type - Entry type
33
+ */
34
+
35
+ /**
36
+ * @typedef {Object} DotfileRecommendation
37
+ * @property {string} path - File path to recommend (e.g. "~/.zshrc")
38
+ * @property {'shell'|'git'|'editor'|'package'|'security'} category - Display grouping
39
+ * @property {Platform[]} platforms - Platforms this file applies to
40
+ * @property {boolean} autoEncrypt - Whether to encrypt by default
41
+ * @property {string} description - Human-readable description
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} DotfilesSetupResult
46
+ * @property {Platform} platform - Detected platform
47
+ * @property {boolean} chezmoiInstalled - Whether chezmoi was found
48
+ * @property {boolean} encryptionConfigured - Whether age encryption is set up
49
+ * @property {string|null} sourceDir - Chezmoi source directory path
50
+ * @property {string|null} publicKey - Age public key
51
+ * @property {'success'|'skipped'|'failed'} status - Overall setup outcome
52
+ * @property {string} [message] - Human-readable outcome message
53
+ */
54
+
55
+ /**
56
+ * @typedef {Object} DotfilesStatusResult
57
+ * @property {Platform} platform - Detected platform
58
+ * @property {boolean} enabled - Whether dotfiles management is active
59
+ * @property {boolean} chezmoiInstalled - Whether chezmoi binary exists
60
+ * @property {boolean} encryptionConfigured - Whether age encryption is set up
61
+ * @property {string|null} repo - Remote dotfiles repo URL
62
+ * @property {string|null} sourceDir - Chezmoi source directory
63
+ * @property {DotfileEntry[]} files - All managed files
64
+ * @property {{ total: number, encrypted: number, plaintext: number }} summary - File counts
65
+ */
66
+
67
+ /**
68
+ * @typedef {Object} DotfilesAddResult
69
+ * @property {{ path: string, encrypted: boolean }[]} added - Successfully added files
70
+ * @property {{ path: string, reason: string }[]} skipped - Skipped files
71
+ * @property {{ path: string, reason: string }[]} rejected - Rejected files
72
+ */
73
+
74
+ /**
75
+ * @typedef {Object} DotfilesSyncResult
76
+ * @property {'push'|'pull'|'init-remote'|'skipped'} action - Sync action performed
77
+ * @property {string|null} repo - Remote repository URL
78
+ * @property {'success'|'skipped'|'failed'} status - Outcome
79
+ * @property {string} [message] - Human-readable outcome
80
+ * @property {string[]} [conflicts] - Conflicting file paths
17
81
  */
18
82
 
19
83
  /**
@@ -189,10 +253,72 @@
189
253
 
190
254
  /**
191
255
  * @typedef {Object} AWSCostEntry
192
- * @property {string} serviceName
193
- * @property {number} amount
194
- * @property {string} unit
195
- * @property {{ start: string, end: string }} period
256
+ * @property {string} serviceName - AWS service name (e.g. "Amazon EC2"), or tag value label for tag grouping
257
+ * @property {string} [tagValue] - Tag value when grouping by TAG or BOTH (e.g. "prod"); undefined for service-only grouping
258
+ * @property {number} amount - Cost amount (USD)
259
+ * @property {string} unit - Currency unit (always "USD")
260
+ * @property {{ start: string, end: string }} period - ISO date range (YYYY-MM-DD)
261
+ */
262
+
263
+ /**
264
+ * @typedef {'service' | 'tag' | 'both'} CostGroupMode
265
+ * The dimension used to group cost entries.
266
+ * - 'service': group by AWS service name (default, backward-compatible)
267
+ * - 'tag': group by a tag key's values (requires tagKey)
268
+ * - 'both': group by AWS service + tag value simultaneously
269
+ */
270
+
271
+ /**
272
+ * @typedef {Object} CostTrendPoint
273
+ * @property {string} date - ISO date (YYYY-MM-DD) for this data point
274
+ * @property {number} amount - Total cost for this day (USD)
275
+ * @property {string} [label] - Display label: serviceName for service/both grouping,
276
+ * tag value for tag grouping; omitted when not multi-series
277
+ */
278
+
279
+ /**
280
+ * @typedef {Object} CostTrendSeries
281
+ * @property {string} name - Series label (service name or tag value)
282
+ * @property {CostTrendPoint[]} points - Ordered daily data points (ascending by date)
283
+ */
284
+
285
+ /**
286
+ * @typedef {Object} LogGroup
287
+ * @property {string} name - Full log group name (e.g. "/aws/lambda/my-fn")
288
+ * @property {number} [storedBytes] - Total stored bytes (may be absent for empty groups)
289
+ * @property {number} [retentionDays] - Retention policy in days; undefined = never expire
290
+ * @property {string} [creationTime] - ISO8601 creation timestamp
291
+ */
292
+
293
+ /**
294
+ * @typedef {Object} LogEvent
295
+ * @property {string} eventId - Unique event ID assigned by CloudWatch
296
+ * @property {string} logStreamName - Stream within the log group (e.g. "2026/03/26/[$LATEST]abc")
297
+ * @property {number} timestamp - Event time as epoch milliseconds
298
+ * @property {string} message - Raw log message text
299
+ */
300
+
301
+ /**
302
+ * @typedef {Object} LogFilterResult
303
+ * @property {LogEvent[]} events - Matched log events (up to --limit)
304
+ * @property {boolean} truncated - True when the result was capped by --limit or AWS pagination
305
+ * @property {string} logGroupName - The log group that was queried
306
+ * @property {number} startTime - Query start as epoch milliseconds
307
+ * @property {number} endTime - Query end as epoch milliseconds
308
+ * @property {string} filterPattern - The pattern used ('' = no filter)
309
+ */
310
+
311
+ /**
312
+ * @typedef {Object} ChartBarData
313
+ * @property {string} name - Row label (service name, tag value, or "service / tag")
314
+ * @property {number} value - Cost amount (USD)
315
+ */
316
+
317
+ /**
318
+ * @typedef {Object} ChartSeries
319
+ * @property {string} name - Series label displayed in legend
320
+ * @property {number[]} values - Ordered numeric values (one per day, ~60 for 2 months)
321
+ * @property {string[]} labels - Date labels matching values array (YYYY-MM-DD)
196
322
  */
197
323
 
198
324
  /**
@@ -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
+ }