devvami 1.3.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/oclif.manifest.json +211 -1
- package/package.json +1 -1
- 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 +35 -2
- package/src/formatters/dotfiles.js +259 -0
- package/src/help.js +47 -2
- package/src/services/dotfiles.js +573 -0
- package/src/types.js +64 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
/** @import { DotfilesSetupResult, DotfilesStatusResult, DotfilesAddResult, DotfilesSyncResult, DotfileEntry } from '../types.js' */
|
|
4
|
+
|
|
5
|
+
const BORDER = chalk.dim('─'.repeat(60))
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// formatDotfilesSetup (T020)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format the output of `dvmi dotfiles setup` completion.
|
|
13
|
+
* @param {DotfilesSetupResult} result
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function formatDotfilesSetup(result) {
|
|
17
|
+
const lines = [
|
|
18
|
+
'',
|
|
19
|
+
BORDER,
|
|
20
|
+
chalk.bold(' Dotfiles Setup — Summary'),
|
|
21
|
+
BORDER,
|
|
22
|
+
'',
|
|
23
|
+
chalk.bold(` Platform: ${chalk.cyan(result.platform)}`),
|
|
24
|
+
chalk.bold(` Status: ${result.status === 'success' ? chalk.green('success') : result.status === 'skipped' ? chalk.dim('skipped') : chalk.red('failed')}`),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
if (result.sourceDir) {
|
|
28
|
+
lines.push(chalk.white(` Source dir: ${chalk.cyan(result.sourceDir)}`))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (result.publicKey) {
|
|
32
|
+
lines.push('')
|
|
33
|
+
lines.push(chalk.white(` Age public key:`))
|
|
34
|
+
lines.push(chalk.cyan(` ${result.publicKey}`))
|
|
35
|
+
lines.push('')
|
|
36
|
+
lines.push(chalk.yellow(' IMPORTANT: Back up your age key!'))
|
|
37
|
+
lines.push(chalk.dim(` Key file: ~/.config/chezmoi/key.txt`))
|
|
38
|
+
lines.push(chalk.dim(' Without this key you cannot decrypt your dotfiles on a new machine.'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (result.status === 'success') {
|
|
42
|
+
lines.push('')
|
|
43
|
+
lines.push(chalk.bold.green(' Chezmoi configured with age encryption!'))
|
|
44
|
+
lines.push(chalk.dim(' Run `dvmi dotfiles add` to start tracking files'))
|
|
45
|
+
} else if (result.status === 'failed') {
|
|
46
|
+
lines.push('')
|
|
47
|
+
lines.push(chalk.bold.red(' Setup failed.'))
|
|
48
|
+
if (result.message) lines.push(chalk.dim(` → ${result.message}`))
|
|
49
|
+
} else if (result.status === 'skipped') {
|
|
50
|
+
lines.push('')
|
|
51
|
+
if (result.message) lines.push(chalk.dim(` ${result.message}`))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
lines.push(BORDER)
|
|
55
|
+
return lines.join('\n')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// formatDotfilesSummary (T012)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format the file count summary line.
|
|
64
|
+
* @param {{ total: number, encrypted: number, plaintext: number }} summary
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function formatDotfilesSummary(summary) {
|
|
68
|
+
return `${summary.total} total: ${summary.plaintext} plaintext, ${summary.encrypted} encrypted`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// formatDotfilesStatus (T012 / T032)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Infer a display category from a file path.
|
|
77
|
+
* @param {string} filePath
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function inferCategory(filePath) {
|
|
81
|
+
const lower = filePath.toLowerCase()
|
|
82
|
+
if (lower.includes('.ssh') || lower.includes('.gnupg') || lower.includes('gpg') || lower.includes('secret') || lower.includes('credential') || lower.includes('token') || lower.includes('password')) return 'Security'
|
|
83
|
+
if (lower.includes('.gitconfig') || lower.includes('.gitignore') || lower.includes('.git')) return 'Git'
|
|
84
|
+
if (lower.includes('zshrc') || lower.includes('bashrc') || lower.includes('bash_profile') || lower.includes('zprofile') || lower.includes('fish')) return 'Shell'
|
|
85
|
+
if (lower.includes('vim') || lower.includes('nvim') || lower.includes('emacs') || lower.includes('vscode') || lower.includes('cursor')) return 'Editor'
|
|
86
|
+
if (lower.includes('brew') || lower.includes('npm') || lower.includes('yarn') || lower.includes('pip') || lower.includes('gem')) return 'Package'
|
|
87
|
+
return 'Other'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format the full `dvmi dotfiles status` interactive output.
|
|
92
|
+
* @param {DotfilesStatusResult} result
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function formatDotfilesStatus(result) {
|
|
96
|
+
const lines = [
|
|
97
|
+
'',
|
|
98
|
+
BORDER,
|
|
99
|
+
chalk.bold(' Dotfiles Status'),
|
|
100
|
+
BORDER,
|
|
101
|
+
'',
|
|
102
|
+
chalk.white(` Platform: ${chalk.cyan(result.platform)}`),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
if (result.sourceDir) {
|
|
106
|
+
lines.push(chalk.white(` Source dir: ${chalk.cyan(result.sourceDir)}`))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const encLabel = result.encryptionConfigured ? chalk.green('age (configured)') : chalk.dim('not configured')
|
|
110
|
+
lines.push(chalk.white(` Encryption: ${encLabel}`))
|
|
111
|
+
|
|
112
|
+
if (result.repo) {
|
|
113
|
+
lines.push(chalk.white(` Remote: ${chalk.cyan(result.repo)}`))
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(chalk.white(` Remote: ${chalk.dim('not configured')}`))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!result.enabled) {
|
|
119
|
+
lines.push('')
|
|
120
|
+
lines.push(chalk.dim(' Dotfiles management not configured.'))
|
|
121
|
+
lines.push(chalk.dim(' Run `dvmi dotfiles setup` to get started.'))
|
|
122
|
+
lines.push(BORDER)
|
|
123
|
+
return lines.join('\n')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Group files by category
|
|
127
|
+
/** @type {Record<string, DotfileEntry[]>} */
|
|
128
|
+
const grouped = {}
|
|
129
|
+
for (const file of result.files) {
|
|
130
|
+
const category = inferCategory(file.path)
|
|
131
|
+
if (!grouped[category]) grouped[category] = []
|
|
132
|
+
grouped[category].push(file)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const summaryLine = formatDotfilesSummary(result.summary)
|
|
136
|
+
lines.push('')
|
|
137
|
+
lines.push(chalk.bold(` Managed Files (${summaryLine})`))
|
|
138
|
+
lines.push(BORDER)
|
|
139
|
+
|
|
140
|
+
for (const [category, files] of Object.entries(grouped)) {
|
|
141
|
+
const catLabel = category === 'Security' ? ` ${category} 🔒` : ` ${category}`
|
|
142
|
+
lines.push('')
|
|
143
|
+
lines.push(chalk.bold(catLabel))
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const encTag = file.encrypted ? chalk.dim(' encrypted') : ''
|
|
146
|
+
lines.push(` ${file.path}${encTag}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (result.files.length === 0) {
|
|
151
|
+
lines.push('')
|
|
152
|
+
lines.push(chalk.dim(' No files managed yet. Run `dvmi dotfiles add` to start tracking files.'))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lines.push('')
|
|
156
|
+
lines.push(BORDER)
|
|
157
|
+
return lines.join('\n')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// formatDotfilesAdd (T027)
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Format the output of `dvmi dotfiles add` completion.
|
|
166
|
+
* @param {DotfilesAddResult} result
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function formatDotfilesAdd(result) {
|
|
170
|
+
const lines = [
|
|
171
|
+
'',
|
|
172
|
+
BORDER,
|
|
173
|
+
chalk.bold(' Dotfiles Add — Summary'),
|
|
174
|
+
BORDER,
|
|
175
|
+
'',
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
if (result.added.length > 0) {
|
|
179
|
+
lines.push(chalk.bold(` Added (${result.added.length}):`))
|
|
180
|
+
for (const item of result.added) {
|
|
181
|
+
const encTag = item.encrypted ? chalk.dim(' [encrypted]') : ''
|
|
182
|
+
lines.push(chalk.green(` ✔ ${item.path}${encTag}`))
|
|
183
|
+
}
|
|
184
|
+
lines.push('')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (result.skipped.length > 0) {
|
|
188
|
+
lines.push(chalk.bold(` Skipped (${result.skipped.length}):`))
|
|
189
|
+
for (const item of result.skipped) {
|
|
190
|
+
lines.push(chalk.dim(` ─ ${item.path} ${item.reason}`))
|
|
191
|
+
}
|
|
192
|
+
lines.push('')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (result.rejected.length > 0) {
|
|
196
|
+
lines.push(chalk.bold(` Rejected (${result.rejected.length}):`))
|
|
197
|
+
for (const item of result.rejected) {
|
|
198
|
+
lines.push(chalk.red(` ✗ ${item.path} ${item.reason}`))
|
|
199
|
+
}
|
|
200
|
+
lines.push('')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (result.added.length === 0 && result.skipped.length === 0 && result.rejected.length === 0) {
|
|
204
|
+
lines.push(chalk.dim(' No files processed.'))
|
|
205
|
+
lines.push('')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
lines.push(BORDER)
|
|
209
|
+
return lines.join('\n')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// formatDotfilesSync (T039 / T044)
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Format the output of `dvmi dotfiles sync` completion.
|
|
218
|
+
* @param {DotfilesSyncResult} result
|
|
219
|
+
* @returns {string}
|
|
220
|
+
*/
|
|
221
|
+
export function formatDotfilesSync(result) {
|
|
222
|
+
const actionLabel = {
|
|
223
|
+
push: 'Push',
|
|
224
|
+
pull: 'Pull',
|
|
225
|
+
'init-remote': 'Remote Setup',
|
|
226
|
+
skipped: 'Skipped',
|
|
227
|
+
}[result.action] ?? result.action
|
|
228
|
+
|
|
229
|
+
const lines = [
|
|
230
|
+
'',
|
|
231
|
+
BORDER,
|
|
232
|
+
chalk.bold(` Dotfiles Sync — ${actionLabel}`),
|
|
233
|
+
BORDER,
|
|
234
|
+
'',
|
|
235
|
+
chalk.white(` Action: ${chalk.cyan(actionLabel)}`),
|
|
236
|
+
chalk.white(` Status: ${result.status === 'success' ? chalk.green('success') : result.status === 'skipped' ? chalk.dim('skipped') : chalk.red('failed')}`),
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
if (result.repo) {
|
|
240
|
+
lines.push(chalk.white(` Remote: ${chalk.cyan(result.repo)}`))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (result.message) {
|
|
244
|
+
lines.push('')
|
|
245
|
+
lines.push(chalk.white(` ${result.message}`))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (result.conflicts && result.conflicts.length > 0) {
|
|
249
|
+
lines.push('')
|
|
250
|
+
lines.push(chalk.bold.red(` Conflicts (${result.conflicts.length}):`))
|
|
251
|
+
for (const conflict of result.conflicts) {
|
|
252
|
+
lines.push(chalk.red(` ✗ ${conflict}`))
|
|
253
|
+
}
|
|
254
|
+
lines.push(chalk.dim(' Resolve conflicts manually and run `chezmoi apply` to continue.'))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
lines.push(BORDER)
|
|
258
|
+
return lines.join('\n')
|
|
259
|
+
}
|
package/src/help.js
CHANGED
|
@@ -87,6 +87,15 @@ const CATEGORIES = [
|
|
|
87
87
|
{ id: 'security:setup', hint: '[--json]' },
|
|
88
88
|
],
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
title: 'Dotfiles & Cifratura',
|
|
92
|
+
cmds: [
|
|
93
|
+
{ id: 'dotfiles:setup', hint: '[--json]' },
|
|
94
|
+
{ id: 'dotfiles:add', hint: '[FILES...] [--encrypt]' },
|
|
95
|
+
{ id: 'dotfiles:status', hint: '[--json]' },
|
|
96
|
+
{ id: 'dotfiles:sync', hint: '[--push] [--pull] [--dry-run]' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
90
99
|
{
|
|
91
100
|
title: 'Setup & Ambiente',
|
|
92
101
|
cmds: [
|
|
@@ -119,7 +128,20 @@ export default class CustomHelp extends Help {
|
|
|
119
128
|
async showRootHelp() {
|
|
120
129
|
// Animated logo — identical to `dvmi init` (no-ops in CI/non-TTY)
|
|
121
130
|
await printBanner()
|
|
122
|
-
|
|
131
|
+
|
|
132
|
+
// Version check: uses cached result (populated by init hook) — 800 ms timeout
|
|
133
|
+
let versionInfo = null
|
|
134
|
+
try {
|
|
135
|
+
const { checkForUpdate } = await import('./services/version-check.js')
|
|
136
|
+
versionInfo = await Promise.race([
|
|
137
|
+
checkForUpdate(),
|
|
138
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 800)),
|
|
139
|
+
])
|
|
140
|
+
} catch {
|
|
141
|
+
// never block help output
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.log(this.#buildRootLayout(versionInfo))
|
|
123
145
|
}
|
|
124
146
|
|
|
125
147
|
/**
|
|
@@ -151,9 +173,10 @@ export default class CustomHelp extends Help {
|
|
|
151
173
|
|
|
152
174
|
/**
|
|
153
175
|
* Build the full categorized root help layout.
|
|
176
|
+
* @param {{ hasUpdate: boolean, current: string, latest: string|null }|null} [versionInfo]
|
|
154
177
|
* @returns {string}
|
|
155
178
|
*/
|
|
156
|
-
#buildRootLayout() {
|
|
179
|
+
#buildRootLayout(versionInfo = null) {
|
|
157
180
|
/** @type {Map<string, import('@oclif/core').Command.Cached>} */
|
|
158
181
|
const cmdMap = new Map(this.config.commands.map((c) => [c.id, c]))
|
|
159
182
|
|
|
@@ -181,6 +204,10 @@ export default class CustomHelp extends Help {
|
|
|
181
204
|
{ cmd: 'dvmi logs --group /aws/lambda/my-fn --filter "ERROR"', note: 'Filtra eventi ERROR su un log group' },
|
|
182
205
|
{ cmd: 'dvmi security setup --json', note: 'Controlla lo stato degli strumenti di sicurezza' },
|
|
183
206
|
{ cmd: 'dvmi security setup', note: 'Wizard interattivo: installa aws-vault e GCM' },
|
|
207
|
+
{ cmd: 'dvmi dotfiles setup', note: 'Configura chezmoi con cifratura age' },
|
|
208
|
+
{ cmd: 'dvmi dotfiles add ~/.zshrc ~/.gitconfig', note: 'Aggiungi dotfile a chezmoi' },
|
|
209
|
+
{ cmd: 'dvmi dotfiles status --json', note: 'Stato dotfile gestiti (JSON)' },
|
|
210
|
+
{ cmd: 'dvmi dotfiles sync --push', note: 'Push dotfile al repository remoto' },
|
|
184
211
|
{ cmd: 'dvmi welcome', note: 'Dashboard missione dvmi con intro animata' },
|
|
185
212
|
]
|
|
186
213
|
|
|
@@ -248,6 +275,24 @@ export default class CustomHelp extends Help {
|
|
|
248
275
|
}
|
|
249
276
|
|
|
250
277
|
lines.push('')
|
|
278
|
+
|
|
279
|
+
// ── Versione + update notice ───────────────────────────────────────────
|
|
280
|
+
const current = versionInfo?.current ?? this.config.version
|
|
281
|
+
const versionStr = isColorEnabled
|
|
282
|
+
? chalk.dim('version ') + chalk.hex(DIM_BLUE)(current)
|
|
283
|
+
: `version ${current}`
|
|
284
|
+
|
|
285
|
+
if (versionInfo?.hasUpdate && versionInfo.latest) {
|
|
286
|
+
const updateStr = isColorEnabled
|
|
287
|
+
? chalk.yellow('update disponibile: ') +
|
|
288
|
+
chalk.dim(current) + chalk.yellow(' → ') + chalk.green(versionInfo.latest) +
|
|
289
|
+
chalk.dim(' (esegui ') + chalk.hex(LIGHT_ORANGE)('dvmi upgrade') + chalk.dim(')')
|
|
290
|
+
: `update disponibile: ${current} → ${versionInfo.latest} (esegui dvmi upgrade)`
|
|
291
|
+
lines.push(' ' + versionStr + chalk.dim(' · ') + updateStr)
|
|
292
|
+
} else {
|
|
293
|
+
lines.push(' ' + versionStr)
|
|
294
|
+
}
|
|
295
|
+
|
|
251
296
|
lines.push(
|
|
252
297
|
' ' + chalk.dim('Approfondisci:') + ' ' +
|
|
253
298
|
chalk.hex(DIM_BLUE)('dvmi <COMANDO> --help') +
|