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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { detectPlatform } from '../../services/platform.js'
|
|
5
|
+
import { isChezmoiInstalled, getChezmoiConfig, getManagedFiles, getChezmoiRemote } from '../../services/dotfiles.js'
|
|
6
|
+
import { loadConfig } from '../../services/config.js'
|
|
7
|
+
import { formatDotfilesStatus } from '../../formatters/dotfiles.js'
|
|
8
|
+
import { DvmiError } from '../../utils/errors.js'
|
|
9
|
+
|
|
10
|
+
/** @import { DotfilesStatusResult } from '../../types.js' */
|
|
11
|
+
|
|
12
|
+
export default class DotfilesStatus extends Command {
|
|
13
|
+
static description = 'Show chezmoi dotfiles status: managed files, encryption state, and sync health'
|
|
14
|
+
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> dotfiles status',
|
|
17
|
+
'<%= config.bin %> dotfiles status --json',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
static enableJsonFlag = true
|
|
21
|
+
|
|
22
|
+
static flags = {
|
|
23
|
+
help: Flags.help({ char: 'h' }),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(DotfilesStatus)
|
|
28
|
+
const isJson = flags.json
|
|
29
|
+
|
|
30
|
+
const platformInfo = await detectPlatform()
|
|
31
|
+
const { platform } = platformInfo
|
|
32
|
+
|
|
33
|
+
const config = await loadConfig()
|
|
34
|
+
const enabled = config.dotfiles?.enabled === true
|
|
35
|
+
|
|
36
|
+
// Check chezmoi installation (even for not-configured state)
|
|
37
|
+
const chezmoiInstalled = await isChezmoiInstalled()
|
|
38
|
+
|
|
39
|
+
// Not configured state — valid, not an error
|
|
40
|
+
if (!enabled) {
|
|
41
|
+
/** @type {DotfilesStatusResult} */
|
|
42
|
+
const notConfiguredResult = {
|
|
43
|
+
platform,
|
|
44
|
+
enabled: false,
|
|
45
|
+
chezmoiInstalled,
|
|
46
|
+
encryptionConfigured: false,
|
|
47
|
+
repo: null,
|
|
48
|
+
sourceDir: null,
|
|
49
|
+
files: [],
|
|
50
|
+
summary: { total: 0, encrypted: 0, plaintext: 0 },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isJson) return notConfiguredResult
|
|
54
|
+
this.log(formatDotfilesStatus(notConfiguredResult))
|
|
55
|
+
return notConfiguredResult
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!chezmoiInstalled) {
|
|
59
|
+
const hint = platform === 'macos'
|
|
60
|
+
? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
|
|
61
|
+
: 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
|
|
62
|
+
throw new DvmiError('chezmoi is not installed', hint)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Gather data
|
|
66
|
+
const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Gathering dotfiles status...') }).start()
|
|
67
|
+
|
|
68
|
+
const [chezmoiConfig, files, remote] = await Promise.all([
|
|
69
|
+
getChezmoiConfig(),
|
|
70
|
+
getManagedFiles(),
|
|
71
|
+
getChezmoiRemote(),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
spinner?.stop()
|
|
75
|
+
|
|
76
|
+
const encryptionConfigured = chezmoiConfig?.encryption?.tool === 'age' || !!chezmoiConfig?.age?.identity
|
|
77
|
+
const sourceDir = chezmoiConfig?.sourceDir ?? chezmoiConfig?.sourcePath ?? null
|
|
78
|
+
const repo = config.dotfiles?.repo ?? remote
|
|
79
|
+
|
|
80
|
+
const encryptedCount = files.filter((f) => f.encrypted).length
|
|
81
|
+
const plaintextCount = files.length - encryptedCount
|
|
82
|
+
|
|
83
|
+
/** @type {DotfilesStatusResult} */
|
|
84
|
+
const result = {
|
|
85
|
+
platform,
|
|
86
|
+
enabled: true,
|
|
87
|
+
chezmoiInstalled: true,
|
|
88
|
+
encryptionConfigured,
|
|
89
|
+
repo: repo ?? null,
|
|
90
|
+
sourceDir,
|
|
91
|
+
files,
|
|
92
|
+
summary: {
|
|
93
|
+
total: files.length,
|
|
94
|
+
encrypted: encryptedCount,
|
|
95
|
+
plaintext: plaintextCount,
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isJson) return result
|
|
100
|
+
this.log(formatDotfilesStatus(result))
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { Command, Flags, Args } from '@oclif/core'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { confirm, input, select } from '@inquirer/prompts'
|
|
5
|
+
import { detectPlatform } from '../../services/platform.js'
|
|
6
|
+
import { isChezmoiInstalled, getChezmoiRemote, hasLocalChanges } from '../../services/dotfiles.js'
|
|
7
|
+
import { loadConfig, saveConfig } from '../../services/config.js'
|
|
8
|
+
import { exec, execOrThrow } from '../../services/shell.js'
|
|
9
|
+
import { formatDotfilesSync } from '../../formatters/dotfiles.js'
|
|
10
|
+
import { DvmiError } from '../../utils/errors.js'
|
|
11
|
+
|
|
12
|
+
/** @import { DotfilesSyncResult } from '../../types.js' */
|
|
13
|
+
|
|
14
|
+
export default class DotfilesSync extends Command {
|
|
15
|
+
static description = 'Sync dotfiles with remote repository: push local changes or pull from remote'
|
|
16
|
+
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> dotfiles sync',
|
|
19
|
+
'<%= config.bin %> dotfiles sync --push',
|
|
20
|
+
'<%= config.bin %> dotfiles sync --pull',
|
|
21
|
+
'<%= config.bin %> dotfiles sync --pull git@github.com:user/dotfiles.git',
|
|
22
|
+
'<%= config.bin %> dotfiles sync --dry-run --push',
|
|
23
|
+
'<%= config.bin %> dotfiles sync --json',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
static enableJsonFlag = true
|
|
27
|
+
|
|
28
|
+
static flags = {
|
|
29
|
+
help: Flags.help({ char: 'h' }),
|
|
30
|
+
push: Flags.boolean({ description: 'Push local changes to remote', default: false }),
|
|
31
|
+
pull: Flags.boolean({ description: 'Pull remote changes and apply', default: false }),
|
|
32
|
+
'dry-run': Flags.boolean({ description: 'Show what would change without applying', default: false }),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static args = {
|
|
36
|
+
repo: Args.string({ description: 'Remote repository URL (for initial remote setup)', required: false }),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async run() {
|
|
40
|
+
const { flags, args } = await this.parse(DotfilesSync)
|
|
41
|
+
const isJson = flags.json
|
|
42
|
+
const isPush = flags.push
|
|
43
|
+
const isPull = flags.pull
|
|
44
|
+
const isDryRun = flags['dry-run']
|
|
45
|
+
const repoArg = args.repo
|
|
46
|
+
|
|
47
|
+
// Flag validation: --push and --pull are mutually exclusive
|
|
48
|
+
if (isPush && isPull) {
|
|
49
|
+
throw new DvmiError(
|
|
50
|
+
'Cannot use --push and --pull together — they are mutually exclusive.',
|
|
51
|
+
'Use --push to upload local changes or --pull to download remote changes.',
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Non-interactive guard
|
|
56
|
+
const isCI = process.env.CI === 'true'
|
|
57
|
+
const isNonInteractive = !process.stdout.isTTY
|
|
58
|
+
if ((isCI || isNonInteractive) && !isJson) {
|
|
59
|
+
this.error(
|
|
60
|
+
'This command requires an interactive terminal (TTY). Run with --json for a non-interactive sync.',
|
|
61
|
+
{ exit: 1 },
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const config = await loadConfig()
|
|
66
|
+
if (!config.dotfiles?.enabled) {
|
|
67
|
+
throw new DvmiError(
|
|
68
|
+
'Chezmoi dotfiles management is not configured',
|
|
69
|
+
'Run `dvmi dotfiles setup` first',
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const chezmoiInstalled = await isChezmoiInstalled()
|
|
74
|
+
if (!chezmoiInstalled) {
|
|
75
|
+
const platformInfo = await detectPlatform()
|
|
76
|
+
const hint = platformInfo.platform === 'macos'
|
|
77
|
+
? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
|
|
78
|
+
: 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
|
|
79
|
+
throw new DvmiError('chezmoi is not installed', hint)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const remote = config.dotfiles?.repo ?? await getChezmoiRemote()
|
|
83
|
+
|
|
84
|
+
// --json mode: attempt push/pull or report status
|
|
85
|
+
if (isJson) {
|
|
86
|
+
if (!remote && !repoArg) {
|
|
87
|
+
/** @type {DotfilesSyncResult} */
|
|
88
|
+
return {
|
|
89
|
+
action: 'skipped',
|
|
90
|
+
repo: null,
|
|
91
|
+
status: 'skipped',
|
|
92
|
+
message: 'No remote repository configured. Run `dvmi dotfiles sync <repo-url>` to set up remote.',
|
|
93
|
+
conflicts: [],
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const effectiveRemote = repoArg ?? remote
|
|
98
|
+
|
|
99
|
+
if (isPull) {
|
|
100
|
+
return await this._pull(effectiveRemote, isDryRun, isJson)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isPush || remote) {
|
|
104
|
+
return await this._push(effectiveRemote, isDryRun, isJson)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @type {DotfilesSyncResult} */
|
|
108
|
+
return { action: 'skipped', repo: effectiveRemote ?? null, status: 'skipped', message: 'No action specified', conflicts: [] }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Interactive mode
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
// No remote — initial setup flow
|
|
116
|
+
if (!remote && !repoArg) {
|
|
117
|
+
return await this._setupRemote(config, isDryRun)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const effectiveRemote = repoArg ?? remote
|
|
121
|
+
const localChanges = await hasLocalChanges()
|
|
122
|
+
|
|
123
|
+
if (isPush) {
|
|
124
|
+
const result = await this._push(effectiveRemote, isDryRun, false)
|
|
125
|
+
this.log(formatDotfilesSync(result))
|
|
126
|
+
return result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isPull) {
|
|
130
|
+
const result = await this._pull(effectiveRemote, isDryRun, false)
|
|
131
|
+
this.log(formatDotfilesSync(result))
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Interactive menu
|
|
136
|
+
this.log(chalk.bold(`\n Remote: ${chalk.cyan(effectiveRemote)}`))
|
|
137
|
+
this.log(chalk.white(` Local changes: ${localChanges ? chalk.yellow('yes') : chalk.dim('none')}`))
|
|
138
|
+
this.log('')
|
|
139
|
+
|
|
140
|
+
const action = await select({
|
|
141
|
+
message: 'What would you like to do?',
|
|
142
|
+
choices: [
|
|
143
|
+
{ name: 'Push local changes to remote', value: 'push' },
|
|
144
|
+
{ name: 'Pull remote changes and apply', value: 'pull' },
|
|
145
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
146
|
+
],
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (action === 'cancel') {
|
|
150
|
+
/** @type {DotfilesSyncResult} */
|
|
151
|
+
const cancelResult = { action: 'skipped', repo: effectiveRemote ?? null, status: 'skipped', message: 'Cancelled by user', conflicts: [] }
|
|
152
|
+
this.log(formatDotfilesSync(cancelResult))
|
|
153
|
+
return cancelResult
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = action === 'push'
|
|
157
|
+
? await this._push(effectiveRemote, isDryRun, false)
|
|
158
|
+
: await this._pull(effectiveRemote, isDryRun, false)
|
|
159
|
+
|
|
160
|
+
this.log(formatDotfilesSync(result))
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// _setupRemote — initial remote connection
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {import('../../types.js').CLIConfig} config
|
|
170
|
+
* @param {boolean} isDryRun
|
|
171
|
+
* @returns {Promise<DotfilesSyncResult>}
|
|
172
|
+
*/
|
|
173
|
+
async _setupRemote(config, isDryRun) {
|
|
174
|
+
const choice = await select({
|
|
175
|
+
message: 'Connect to an existing dotfiles repository or create a new one?',
|
|
176
|
+
choices: [
|
|
177
|
+
{ name: 'Connect to existing repository', value: 'existing' },
|
|
178
|
+
{ name: 'Create new repository on GitHub', value: 'new' },
|
|
179
|
+
],
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
let repoUrl = ''
|
|
183
|
+
|
|
184
|
+
if (choice === 'existing') {
|
|
185
|
+
repoUrl = await input({ message: 'Repository URL (SSH or HTTPS):' })
|
|
186
|
+
} else {
|
|
187
|
+
const repoName = await input({ message: 'Repository name:', default: 'dotfiles' })
|
|
188
|
+
const isPrivate = await confirm({ message: 'Make repository private?', default: true })
|
|
189
|
+
|
|
190
|
+
if (!isDryRun) {
|
|
191
|
+
try {
|
|
192
|
+
const visFlag = isPrivate ? '--private' : '--public'
|
|
193
|
+
await execOrThrow('gh', ['repo', 'create', repoName, visFlag, '--confirm'])
|
|
194
|
+
// Get the SSH URL from the created repo
|
|
195
|
+
const { exec } = await import('../../services/shell.js')
|
|
196
|
+
const result = await exec('gh', ['repo', 'view', repoName, '--json', 'sshUrl', '--jq', '.sshUrl'])
|
|
197
|
+
repoUrl = result.stdout.trim() || `git@github.com:${repoName}.git`
|
|
198
|
+
} catch {
|
|
199
|
+
throw new DvmiError(
|
|
200
|
+
'Failed to create repository on GitHub',
|
|
201
|
+
'Verify your GitHub authentication: `gh auth status`',
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
repoUrl = `git@github.com:<user>/${repoName}.git`
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!isDryRun) {
|
|
210
|
+
try {
|
|
211
|
+
await execOrThrow('chezmoi', ['git', '--', 'remote', 'add', 'origin', repoUrl])
|
|
212
|
+
await execOrThrow('chezmoi', ['git', '--', 'push', '-u', 'origin', 'main'])
|
|
213
|
+
// Save repo to dvmi config
|
|
214
|
+
config.dotfiles = { ...(config.dotfiles ?? { enabled: true }), repo: repoUrl }
|
|
215
|
+
await saveConfig(config)
|
|
216
|
+
} catch (err) {
|
|
217
|
+
/** @type {DotfilesSyncResult} */
|
|
218
|
+
const failResult = {
|
|
219
|
+
action: 'init-remote',
|
|
220
|
+
repo: repoUrl,
|
|
221
|
+
status: 'failed',
|
|
222
|
+
message: err instanceof Error ? err.message : String(err),
|
|
223
|
+
conflicts: [],
|
|
224
|
+
}
|
|
225
|
+
this.log(formatDotfilesSync(failResult))
|
|
226
|
+
return failResult
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @type {DotfilesSyncResult} */
|
|
231
|
+
const result = {
|
|
232
|
+
action: 'init-remote',
|
|
233
|
+
repo: repoUrl,
|
|
234
|
+
status: isDryRun ? 'skipped' : 'success',
|
|
235
|
+
message: isDryRun ? `Would configure remote: ${repoUrl}` : 'Remote repository configured and initial push completed',
|
|
236
|
+
conflicts: [],
|
|
237
|
+
}
|
|
238
|
+
this.log(formatDotfilesSync(result))
|
|
239
|
+
return result
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// _push
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {string|null|undefined} remote
|
|
248
|
+
* @param {boolean} isDryRun
|
|
249
|
+
* @param {boolean} isJson
|
|
250
|
+
* @returns {Promise<DotfilesSyncResult>}
|
|
251
|
+
*/
|
|
252
|
+
async _push(remote, isDryRun, isJson) {
|
|
253
|
+
if (!remote) {
|
|
254
|
+
return {
|
|
255
|
+
action: 'push',
|
|
256
|
+
repo: null,
|
|
257
|
+
status: 'failed',
|
|
258
|
+
message: 'No remote repository configured. Run `dvmi dotfiles sync <repo-url>` to set up remote.',
|
|
259
|
+
conflicts: [],
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isDryRun) {
|
|
264
|
+
const diffResult = await exec('chezmoi', ['git', '--', 'diff', '--cached'])
|
|
265
|
+
return {
|
|
266
|
+
action: 'push',
|
|
267
|
+
repo: remote,
|
|
268
|
+
status: 'skipped',
|
|
269
|
+
message: diffResult.stdout.trim() || 'No staged changes to push',
|
|
270
|
+
conflicts: [],
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pushing to remote...') }).start()
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Stage all changes
|
|
278
|
+
await execOrThrow('chezmoi', ['git', '--', 'add', '-A'])
|
|
279
|
+
// Commit
|
|
280
|
+
await exec('chezmoi', ['git', '--', 'commit', '-m', 'chore: update dotfiles'])
|
|
281
|
+
// Push
|
|
282
|
+
await execOrThrow('chezmoi', ['git', '--', 'push', 'origin', 'HEAD'])
|
|
283
|
+
spinner?.succeed(chalk.green('Pushed to remote'))
|
|
284
|
+
|
|
285
|
+
return { action: 'push', repo: remote, status: 'success', message: 'Changes pushed to remote', conflicts: [] }
|
|
286
|
+
} catch (err) {
|
|
287
|
+
spinner?.fail(chalk.red('Push failed'))
|
|
288
|
+
return {
|
|
289
|
+
action: 'push',
|
|
290
|
+
repo: remote,
|
|
291
|
+
status: 'failed',
|
|
292
|
+
message: err instanceof Error ? err.message : String(err),
|
|
293
|
+
conflicts: [],
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// _pull (US4 + US5)
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {string|null|undefined} remote
|
|
304
|
+
* @param {boolean} isDryRun
|
|
305
|
+
* @param {boolean} isJson
|
|
306
|
+
* @returns {Promise<DotfilesSyncResult>}
|
|
307
|
+
*/
|
|
308
|
+
async _pull(remote, isDryRun, isJson) {
|
|
309
|
+
if (!remote) {
|
|
310
|
+
return {
|
|
311
|
+
action: 'pull',
|
|
312
|
+
repo: null,
|
|
313
|
+
status: 'failed',
|
|
314
|
+
message: 'No remote repository configured. Run `dvmi dotfiles sync <repo-url>` to set up remote.',
|
|
315
|
+
conflicts: [],
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (isDryRun) {
|
|
320
|
+
const dryResult = await exec('chezmoi', ['apply', '--dry-run', '--verbose'])
|
|
321
|
+
return {
|
|
322
|
+
action: 'pull',
|
|
323
|
+
repo: remote,
|
|
324
|
+
status: 'skipped',
|
|
325
|
+
message: dryResult.stdout.trim() || 'Would apply remote changes',
|
|
326
|
+
conflicts: [],
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pulling from remote...') }).start()
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
// Check if chezmoi init was done with this remote (first-time pull)
|
|
334
|
+
const currentRemote = await getChezmoiRemote()
|
|
335
|
+
if (!currentRemote) {
|
|
336
|
+
// First time: chezmoi init <repo>
|
|
337
|
+
await execOrThrow('chezmoi', ['init', remote])
|
|
338
|
+
} else {
|
|
339
|
+
// Subsequent: git pull + apply
|
|
340
|
+
await execOrThrow('chezmoi', ['git', '--', 'pull', '--rebase', 'origin', 'HEAD'])
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Apply changes
|
|
344
|
+
const applyResult = await exec('chezmoi', ['apply', '--verbose'])
|
|
345
|
+
spinner?.succeed(chalk.green('Applied remote changes'))
|
|
346
|
+
|
|
347
|
+
// Check for conflicts in apply output
|
|
348
|
+
const conflictLines = (applyResult.stdout + applyResult.stderr)
|
|
349
|
+
.split('\n')
|
|
350
|
+
.filter((l) => l.toLowerCase().includes('conflict'))
|
|
351
|
+
.map((l) => l.trim())
|
|
352
|
+
|
|
353
|
+
if (conflictLines.length > 0) {
|
|
354
|
+
return {
|
|
355
|
+
action: 'pull',
|
|
356
|
+
repo: remote,
|
|
357
|
+
status: 'failed',
|
|
358
|
+
message: `Merge conflicts detected in ${conflictLines.length} file(s)`,
|
|
359
|
+
conflicts: conflictLines,
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { action: 'pull', repo: remote, status: 'success', message: 'Remote changes applied', conflicts: [] }
|
|
364
|
+
} catch (err) {
|
|
365
|
+
spinner?.fail(chalk.red('Pull failed'))
|
|
366
|
+
return {
|
|
367
|
+
action: 'pull',
|
|
368
|
+
repo: remote,
|
|
369
|
+
status: 'failed',
|
|
370
|
+
message: err instanceof Error ? err.message : String(err),
|
|
371
|
+
conflicts: [],
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -8,8 +8,8 @@ import { detectPlatform } from '../services/platform.js'
|
|
|
8
8
|
import { exec, which } from '../services/shell.js'
|
|
9
9
|
import { configExists, loadConfig, saveConfig, CONFIG_PATH } from '../services/config.js'
|
|
10
10
|
import { oauthFlow, storeToken, validateToken, getTeams } from '../services/clickup.js'
|
|
11
|
-
|
|
12
11
|
import { SUPPORTED_TOOLS } from '../services/prompts.js'
|
|
12
|
+
import { isChezmoiInstalled, setupChezmoiInline } from '../services/dotfiles.js'
|
|
13
13
|
|
|
14
14
|
export default class Init extends Command {
|
|
15
15
|
static description = 'Setup completo ambiente di sviluppo locale'
|
|
@@ -72,7 +72,12 @@ export default class Init extends Command {
|
|
|
72
72
|
steps.push({ name: 'aws-vault', status: 'ok', action: 'found' })
|
|
73
73
|
} else {
|
|
74
74
|
steps.push({ name: 'aws-vault', status: 'warn', action: 'not installed' })
|
|
75
|
-
if (!isJson)
|
|
75
|
+
if (!isJson) {
|
|
76
|
+
const installHint = platform.platform === 'macos'
|
|
77
|
+
? 'brew install aws-vault'
|
|
78
|
+
: 'run `dvmi security setup` (Debian/Ubuntu/WSL2) or install aws-vault manually'
|
|
79
|
+
this.log(chalk.yellow(` aws-vault not found. Install: ${installHint}`))
|
|
80
|
+
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
// 4. Create/update config
|
|
@@ -238,7 +243,40 @@ export default class Init extends Command {
|
|
|
238
243
|
}
|
|
239
244
|
}
|
|
240
245
|
|
|
241
|
-
// 7.
|
|
246
|
+
// 7. Chezmoi dotfiles setup
|
|
247
|
+
if (isDryRun) {
|
|
248
|
+
steps.push({ name: 'dotfiles', status: 'would configure' })
|
|
249
|
+
} else if (isJson) {
|
|
250
|
+
config = await loadConfig()
|
|
251
|
+
steps.push({
|
|
252
|
+
name: 'dotfiles',
|
|
253
|
+
status: config.dotfiles?.enabled ? 'configured' : 'not_configured',
|
|
254
|
+
enabled: config.dotfiles?.enabled ?? false,
|
|
255
|
+
})
|
|
256
|
+
} else {
|
|
257
|
+
const chezmoiInstalled = await isChezmoiInstalled()
|
|
258
|
+
if (!chezmoiInstalled) {
|
|
259
|
+
steps.push({ name: 'dotfiles', status: 'skipped', reason: 'chezmoi not installed' })
|
|
260
|
+
} else {
|
|
261
|
+
const setupDotfiles = await confirm({
|
|
262
|
+
message: 'Set up chezmoi dotfiles management with age encryption?',
|
|
263
|
+
default: false,
|
|
264
|
+
})
|
|
265
|
+
if (setupDotfiles) {
|
|
266
|
+
try {
|
|
267
|
+
const dotfilesResult = await setupChezmoiInline(platform.platform)
|
|
268
|
+
config = await loadConfig()
|
|
269
|
+
steps.push({ name: 'dotfiles', status: dotfilesResult.status, sourceDir: dotfilesResult.sourceDir })
|
|
270
|
+
} catch (err) {
|
|
271
|
+
steps.push({ name: 'dotfiles', status: 'failed', reason: err instanceof Error ? err.message : String(err) })
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
steps.push({ name: 'dotfiles', status: 'skipped', hint: 'Run `dvmi dotfiles setup` anytime to enable' })
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 8. Shell completions
|
|
242
280
|
steps.push({ name: 'shell-completions', status: 'ok', action: 'install via: dvmi autocomplete' })
|
|
243
281
|
|
|
244
282
|
const result = { steps, configPath: CONFIG_PATH }
|