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/README.md +36 -1
- package/oclif.manifest.json +380 -7
- package/package.json +2 -1
- package/src/commands/costs/get.js +112 -18
- package/src/commands/costs/trend.js +165 -0
- package/src/commands/dotfiles/add.js +249 -0
- package/src/commands/dotfiles/setup.js +190 -0
- package/src/commands/dotfiles/status.js +103 -0
- package/src/commands/dotfiles/sync.js +375 -0
- package/src/commands/init.js +41 -3
- package/src/commands/logs/index.js +190 -0
- package/src/formatters/charts.js +205 -0
- package/src/formatters/cost.js +18 -5
- package/src/formatters/dotfiles.js +259 -0
- package/src/help.js +85 -28
- package/src/services/aws-costs.js +130 -6
- package/src/services/cloudwatch-logs.js +92 -0
- package/src/services/config.js +17 -1
- package/src/services/dotfiles.js +573 -0
- package/src/types.js +130 -4
- package/src/utils/aws-vault.js +144 -0
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 {
|
|
194
|
-
* @property {
|
|
195
|
-
* @property {
|
|
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
|
+
}
|