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.
@@ -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
- this.log(this.#buildRootLayout())
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') +