devvami 1.4.2 → 1.5.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 +7 -0
- package/oclif.manifest.json +129 -89
- package/package.json +2 -1
- package/src/commands/auth/login.js +20 -16
- package/src/commands/changelog.js +12 -12
- package/src/commands/costs/get.js +14 -24
- package/src/commands/costs/trend.js +13 -24
- package/src/commands/create/repo.js +72 -54
- package/src/commands/docs/list.js +29 -25
- package/src/commands/docs/projects.js +58 -24
- package/src/commands/docs/read.js +56 -39
- package/src/commands/docs/search.js +37 -25
- package/src/commands/doctor.js +37 -35
- package/src/commands/dotfiles/add.js +51 -39
- package/src/commands/dotfiles/setup.js +62 -33
- package/src/commands/dotfiles/status.js +18 -18
- package/src/commands/dotfiles/sync.js +62 -46
- package/src/commands/init.js +143 -132
- package/src/commands/logs/index.js +10 -16
- package/src/commands/open.js +12 -12
- package/src/commands/pipeline/logs.js +8 -11
- package/src/commands/pipeline/rerun.js +21 -16
- package/src/commands/pipeline/status.js +28 -24
- package/src/commands/pr/create.js +40 -27
- package/src/commands/pr/detail.js +9 -7
- package/src/commands/pr/review.js +18 -19
- package/src/commands/pr/status.js +27 -21
- package/src/commands/prompts/browse.js +15 -15
- package/src/commands/prompts/download.js +15 -16
- package/src/commands/prompts/install-speckit.js +11 -12
- package/src/commands/prompts/list.js +12 -12
- package/src/commands/prompts/run.js +16 -19
- package/src/commands/repo/list.js +57 -41
- package/src/commands/search.js +20 -18
- package/src/commands/security/setup.js +38 -34
- package/src/commands/sync-config-ai/index.js +143 -0
- package/src/commands/tasks/assigned.js +43 -33
- package/src/commands/tasks/list.js +43 -33
- package/src/commands/tasks/today.js +32 -30
- package/src/commands/upgrade.js +18 -17
- package/src/commands/vuln/detail.js +8 -8
- package/src/commands/vuln/scan.js +39 -20
- package/src/commands/vuln/search.js +23 -18
- package/src/commands/welcome.js +2 -2
- package/src/commands/whoami.js +19 -23
- package/src/formatters/ai-config.js +127 -0
- package/src/formatters/charts.js +6 -23
- package/src/formatters/cost.js +1 -7
- package/src/formatters/dotfiles.js +48 -19
- package/src/formatters/markdown.js +11 -6
- package/src/formatters/openapi.js +7 -9
- package/src/formatters/prompts.js +69 -78
- package/src/formatters/security.js +2 -2
- package/src/formatters/status.js +1 -1
- package/src/formatters/table.js +1 -3
- package/src/formatters/vuln.js +33 -20
- package/src/help.js +162 -164
- package/src/hooks/init.js +1 -3
- package/src/hooks/postrun.js +5 -7
- package/src/index.js +1 -1
- package/src/services/ai-config-store.js +318 -0
- package/src/services/ai-env-deployer.js +444 -0
- package/src/services/ai-env-scanner.js +242 -0
- package/src/services/audit-detector.js +2 -2
- package/src/services/audit-runner.js +40 -31
- package/src/services/auth.js +9 -9
- package/src/services/awesome-copilot.js +7 -4
- package/src/services/aws-costs.js +22 -22
- package/src/services/clickup.js +26 -26
- package/src/services/cloudwatch-logs.js +5 -9
- package/src/services/config.js +13 -13
- package/src/services/docs.js +19 -20
- package/src/services/dotfiles.js +149 -51
- package/src/services/github.js +22 -24
- package/src/services/nvd.js +21 -31
- package/src/services/platform.js +2 -2
- package/src/services/prompts.js +23 -35
- package/src/services/security.js +135 -61
- package/src/services/shell.js +4 -4
- package/src/services/skills-sh.js +3 -9
- package/src/services/speckit.js +4 -7
- package/src/services/version-check.js +10 -10
- package/src/types.js +85 -0
- package/src/utils/aws-vault.js +18 -41
- package/src/utils/banner.js +5 -7
- package/src/utils/errors.js +42 -46
- package/src/utils/frontmatter.js +4 -4
- package/src/utils/gradient.js +18 -16
- package/src/utils/open-browser.js +3 -3
- package/src/utils/tui/form.js +1006 -0
- package/src/utils/tui/modal.js +15 -14
- package/src/utils/tui/navigable-table.js +16 -16
- package/src/utils/tui/tab-tui.js +800 -0
- package/src/utils/typewriter.js +3 -3
- package/src/utils/welcome.js +18 -21
- package/src/validators/repo-name.js +2 -2
package/src/commands/init.js
CHANGED
|
@@ -1,34 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
4
|
+
import {confirm, input, select} from '@inquirer/prompts'
|
|
5
|
+
import {printWelcomeScreen} from '../utils/welcome.js'
|
|
6
|
+
import {typewriterLine} from '../utils/typewriter.js'
|
|
7
|
+
import {detectPlatform} from '../services/platform.js'
|
|
8
|
+
import {exec, which} from '../services/shell.js'
|
|
9
|
+
import {configExists, loadConfig, saveConfig, CONFIG_PATH} from '../services/config.js'
|
|
10
|
+
import {oauthFlow, storeToken, validateToken, getTeams} from '../services/clickup.js'
|
|
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'
|
|
16
16
|
|
|
17
|
-
static examples = [
|
|
18
|
-
'<%= config.bin %> init',
|
|
19
|
-
'<%= config.bin %> init --dry-run',
|
|
20
|
-
'<%= config.bin %> init --verbose',
|
|
21
|
-
]
|
|
17
|
+
static examples = ['<%= config.bin %> init', '<%= config.bin %> init --dry-run', '<%= config.bin %> init --verbose']
|
|
22
18
|
|
|
23
19
|
static enableJsonFlag = true
|
|
24
20
|
|
|
25
21
|
static flags = {
|
|
26
|
-
verbose: Flags.boolean({
|
|
27
|
-
'dry-run': Flags.boolean({
|
|
22
|
+
verbose: Flags.boolean({description: 'Mostra output dettagliato', default: false}),
|
|
23
|
+
'dry-run': Flags.boolean({description: 'Mostra cosa farebbe senza eseguire', default: false}),
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
async run() {
|
|
31
|
-
const {
|
|
27
|
+
const {flags} = await this.parse(Init)
|
|
32
28
|
const isDryRun = flags['dry-run']
|
|
33
29
|
const isJson = flags.json
|
|
34
30
|
|
|
@@ -38,75 +34,82 @@ export default class Init extends Command {
|
|
|
38
34
|
const steps = []
|
|
39
35
|
|
|
40
36
|
// 1. Check prerequisites
|
|
41
|
-
const spinner = isJson
|
|
37
|
+
const spinner = isJson
|
|
38
|
+
? null
|
|
39
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking prerequisites...')}).start()
|
|
42
40
|
const prerequisites = [
|
|
43
|
-
{
|
|
44
|
-
{
|
|
45
|
-
{
|
|
46
|
-
{
|
|
47
|
-
{
|
|
48
|
-
{
|
|
49
|
-
{
|
|
50
|
-
{
|
|
41
|
+
{name: 'Node.js', cmd: 'node', args: ['--version'], required: true},
|
|
42
|
+
{name: 'nvm', cmd: 'nvm', args: ['--version'], required: false},
|
|
43
|
+
{name: 'npm', cmd: 'npm', args: ['--version'], required: true},
|
|
44
|
+
{name: 'Git', cmd: 'git', args: ['--version'], required: true},
|
|
45
|
+
{name: 'gh CLI', cmd: 'gh', args: ['--version'], required: true},
|
|
46
|
+
{name: 'Docker', cmd: 'docker', args: ['--version'], required: false},
|
|
47
|
+
{name: 'AWS CLI', cmd: 'aws', args: ['--version'], required: false},
|
|
48
|
+
{name: 'aws-vault', cmd: 'aws-vault', args: ['--version'], required: false},
|
|
51
49
|
]
|
|
52
50
|
|
|
53
51
|
for (const prereq of prerequisites) {
|
|
54
52
|
const path = await which(prereq.cmd)
|
|
55
53
|
const status = path ? 'ok' : prereq.required ? 'fail' : 'warn'
|
|
56
|
-
steps.push({
|
|
54
|
+
steps.push({name: prereq.name, status, action: path ? 'found' : 'missing'})
|
|
57
55
|
if (flags.verbose && !isJson) this.log(` ${prereq.name}: ${path ?? 'not found'}`)
|
|
58
56
|
}
|
|
59
57
|
spinner?.succeed('Prerequisites checked')
|
|
60
58
|
|
|
61
59
|
// 2. Configure Git credential helper
|
|
62
|
-
const gitCredSpinner = isJson
|
|
60
|
+
const gitCredSpinner = isJson
|
|
61
|
+
? null
|
|
62
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring Git credential helper...')}).start()
|
|
63
63
|
if (!isDryRun) {
|
|
64
64
|
await exec('git', ['config', '--global', 'credential.helper', platform.credentialHelper])
|
|
65
65
|
}
|
|
66
|
-
steps.push({
|
|
66
|
+
steps.push({name: 'git-credential', status: 'ok', action: isDryRun ? 'would configure' : 'configured'})
|
|
67
67
|
gitCredSpinner?.succeed(`Git credential helper: ${platform.credentialHelper}`)
|
|
68
68
|
|
|
69
69
|
// 3. Configure aws-vault (interactive if not configured)
|
|
70
70
|
const awsVaultInstalled = await which('aws-vault')
|
|
71
71
|
if (awsVaultInstalled) {
|
|
72
|
-
steps.push({
|
|
72
|
+
steps.push({name: 'aws-vault', status: 'ok', action: 'found'})
|
|
73
73
|
} else {
|
|
74
|
-
steps.push({
|
|
74
|
+
steps.push({name: 'aws-vault', status: 'warn', action: 'not installed'})
|
|
75
75
|
if (!isJson) {
|
|
76
|
-
const installHint =
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
const installHint =
|
|
77
|
+
platform.platform === 'macos'
|
|
78
|
+
? 'brew install aws-vault'
|
|
79
|
+
: 'run `dvmi security setup` (Debian/Ubuntu/WSL2) or install aws-vault manually'
|
|
79
80
|
this.log(chalk.yellow(` aws-vault not found. Install: ${installHint}`))
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
// 4. Create/update config
|
|
84
|
-
const configSpinner = isJson
|
|
85
|
+
const configSpinner = isJson
|
|
86
|
+
? null
|
|
87
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating config...')}).start()
|
|
85
88
|
let config = await loadConfig()
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
if (!configExists() && !isDryRun && !isJson) {
|
|
91
|
+
// Stop the spinner before interactive prompts to avoid TTY contention on macOS
|
|
92
|
+
configSpinner?.stop()
|
|
93
|
+
const useOrg = await confirm({message: 'Do you use a GitHub organization? (y/n)', default: true})
|
|
94
|
+
let org = ''
|
|
95
|
+
if (useOrg) {
|
|
96
|
+
org = await input({message: 'GitHub org name:', default: config.org || ''})
|
|
97
|
+
}
|
|
98
|
+
const awsProfile = await input({message: 'AWS profile name:', default: config.awsProfile || 'default'})
|
|
99
|
+
const awsRegion = await input({message: 'AWS region:', default: config.awsRegion || 'eu-west-1'})
|
|
100
|
+
config = {...config, org, awsProfile, awsRegion, shell: platform.credentialHelper}
|
|
101
|
+
}
|
|
99
102
|
|
|
100
103
|
if (!isDryRun) {
|
|
101
104
|
await saveConfig(config)
|
|
102
105
|
}
|
|
103
|
-
steps.push({
|
|
106
|
+
steps.push({name: 'config', status: 'ok', action: isDryRun ? 'would create' : 'created'})
|
|
104
107
|
configSpinner?.succeed(`Config: ${CONFIG_PATH}`)
|
|
105
108
|
|
|
106
109
|
// 5. ClickUp wizard (T008: interactive, T009: dry-run, T010: json)
|
|
107
110
|
if (isDryRun) {
|
|
108
111
|
// T009: In dry-run mode report what would happen without any network calls
|
|
109
|
-
steps.push({
|
|
112
|
+
steps.push({name: 'clickup', status: 'would configure'})
|
|
110
113
|
} else if (isJson) {
|
|
111
114
|
// T010: In JSON mode skip wizard, report current ClickUp config status
|
|
112
115
|
config = await loadConfig()
|
|
@@ -119,11 +122,11 @@ export default class Init extends Command {
|
|
|
119
122
|
})
|
|
120
123
|
} else {
|
|
121
124
|
// T008: Full interactive wizard
|
|
122
|
-
const configureClickUp = await confirm({
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const configureClickUp = await confirm({message: 'Configure ClickUp integration?', default: true})
|
|
126
|
+
if (!configureClickUp) {
|
|
127
|
+
steps.push({name: 'clickup', status: 'skipped'})
|
|
128
|
+
this.log(chalk.dim(' Skipped. Run `dvmi init` again to configure ClickUp later.'))
|
|
129
|
+
} else {
|
|
127
130
|
// Determine auth method
|
|
128
131
|
const clientId = process.env.CLICKUP_CLIENT_ID
|
|
129
132
|
const clientSecret = process.env.CLICKUP_CLIENT_SECRET
|
|
@@ -133,8 +136,8 @@ export default class Init extends Command {
|
|
|
133
136
|
const choice = await select({
|
|
134
137
|
message: 'Select ClickUp authentication method:',
|
|
135
138
|
choices: [
|
|
136
|
-
{
|
|
137
|
-
{
|
|
139
|
+
{name: 'Personal API Token (paste from ClickUp Settings > Apps)', value: 'personal_token'},
|
|
140
|
+
{name: 'OAuth (opens browser)', value: 'oauth'},
|
|
138
141
|
],
|
|
139
142
|
})
|
|
140
143
|
authMethod = /** @type {'oauth'|'personal_token'} */ (choice)
|
|
@@ -152,12 +155,16 @@ export default class Init extends Command {
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
if (authMethod === 'personal_token') {
|
|
155
|
-
const token = await input({
|
|
158
|
+
const token = await input({message: 'Paste your ClickUp Personal API Token:'})
|
|
156
159
|
await storeToken(token)
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
// Validate token
|
|
160
|
-
const validateSpinner = ora({
|
|
163
|
+
const validateSpinner = ora({
|
|
164
|
+
spinner: 'arc',
|
|
165
|
+
color: false,
|
|
166
|
+
text: chalk.hex('#FF6B2B')('Validating ClickUp credentials...'),
|
|
167
|
+
}).start()
|
|
161
168
|
let tokenValid = false
|
|
162
169
|
try {
|
|
163
170
|
const result = await validateToken()
|
|
@@ -169,11 +176,11 @@ export default class Init extends Command {
|
|
|
169
176
|
|
|
170
177
|
if (!tokenValid) {
|
|
171
178
|
this.log(chalk.yellow(' Invalid token. Check your ClickUp Personal API Token and try again.'))
|
|
172
|
-
const retry = await confirm({
|
|
179
|
+
const retry = await confirm({message: 'Retry ClickUp configuration?', default: false})
|
|
173
180
|
if (!retry) {
|
|
174
|
-
steps.push({
|
|
181
|
+
steps.push({name: 'clickup', status: 'skipped'})
|
|
175
182
|
} else {
|
|
176
|
-
const token = await input({
|
|
183
|
+
const token = await input({message: 'Paste your ClickUp Personal API Token:'})
|
|
177
184
|
await storeToken(token)
|
|
178
185
|
tokenValid = (await validateToken()).valid
|
|
179
186
|
}
|
|
@@ -183,7 +190,11 @@ export default class Init extends Command {
|
|
|
183
190
|
// Fetch teams
|
|
184
191
|
let teamId = ''
|
|
185
192
|
let teamName = ''
|
|
186
|
-
const teamsSpinner = ora({
|
|
193
|
+
const teamsSpinner = ora({
|
|
194
|
+
spinner: 'arc',
|
|
195
|
+
color: false,
|
|
196
|
+
text: chalk.hex('#FF6B2B')('Fetching available teams...'),
|
|
197
|
+
}).start()
|
|
187
198
|
try {
|
|
188
199
|
const teams = await getTeams()
|
|
189
200
|
teamsSpinner.stop()
|
|
@@ -194,97 +205,97 @@ export default class Init extends Command {
|
|
|
194
205
|
} else if (teams.length > 1) {
|
|
195
206
|
const selected = await select({
|
|
196
207
|
message: 'Select your ClickUp team:',
|
|
197
|
-
choices: teams.map((t) => ({
|
|
208
|
+
choices: teams.map((t) => ({name: `${t.name} (${t.id})`, value: t.id})),
|
|
198
209
|
})
|
|
199
210
|
teamId = selected
|
|
200
211
|
teamName = teams.find((t) => t.id === selected)?.name ?? ''
|
|
201
212
|
} else {
|
|
202
|
-
teamId = await input({
|
|
213
|
+
teamId = await input({message: 'Enter ClickUp team ID:'})
|
|
203
214
|
}
|
|
204
215
|
} catch {
|
|
205
216
|
teamsSpinner.fail('Could not fetch teams')
|
|
206
|
-
teamId = await input({
|
|
217
|
+
teamId = await input({message: 'Enter ClickUp team ID (find in ClickUp Settings > Spaces):'})
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
// Save ClickUp config
|
|
210
221
|
config = await loadConfig()
|
|
211
|
-
config = {
|
|
222
|
+
config = {...config, clickup: {...config.clickup, teamId, teamName, authMethod}}
|
|
212
223
|
await saveConfig(config)
|
|
213
224
|
this.log(chalk.green('✓') + ' ClickUp configured successfully!')
|
|
214
|
-
steps.push({
|
|
225
|
+
steps.push({name: 'clickup', status: 'configured', teamId, teamName, authMethod})
|
|
215
226
|
}
|
|
216
227
|
}
|
|
217
228
|
}
|
|
218
229
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
230
|
+
// 6. AI tool selection
|
|
231
|
+
if (isDryRun) {
|
|
232
|
+
steps.push({name: 'ai-tool', status: 'would configure'})
|
|
233
|
+
} else if (isJson) {
|
|
234
|
+
config = await loadConfig()
|
|
235
|
+
steps.push({
|
|
236
|
+
name: 'ai-tool',
|
|
237
|
+
status: config.aiTool ? 'configured' : 'not_configured',
|
|
238
|
+
aiTool: config.aiTool,
|
|
239
|
+
})
|
|
240
|
+
} else {
|
|
241
|
+
const aiToolChoices = Object.keys(SUPPORTED_TOOLS).map((t) => ({name: t, value: t}))
|
|
242
|
+
aiToolChoices.push({name: 'none / skip', value: ''})
|
|
243
|
+
const aiTool = await select({
|
|
244
|
+
message: 'Select your preferred AI tool for `dvmi prompts run`:',
|
|
245
|
+
choices: aiToolChoices,
|
|
246
|
+
})
|
|
247
|
+
if (aiTool) {
|
|
248
|
+
config = {...config, aiTool}
|
|
249
|
+
await saveConfig(config)
|
|
250
|
+
this.log(chalk.green(`✓ AI tool set to: ${aiTool}`))
|
|
251
|
+
steps.push({name: 'ai-tool', status: 'configured', aiTool})
|
|
252
|
+
} else {
|
|
253
|
+
steps.push({name: 'ai-tool', status: 'skipped'})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
245
256
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
257
|
+
// 7. Chezmoi dotfiles setup
|
|
258
|
+
if (isDryRun) {
|
|
259
|
+
steps.push({name: 'dotfiles', status: 'would configure'})
|
|
260
|
+
} else if (isJson) {
|
|
261
|
+
config = await loadConfig()
|
|
262
|
+
steps.push({
|
|
263
|
+
name: 'dotfiles',
|
|
264
|
+
status: config.dotfiles?.enabled ? 'configured' : 'not_configured',
|
|
265
|
+
enabled: config.dotfiles?.enabled ?? false,
|
|
266
|
+
})
|
|
267
|
+
} else {
|
|
268
|
+
const chezmoiInstalled = await isChezmoiInstalled()
|
|
269
|
+
if (!chezmoiInstalled) {
|
|
270
|
+
steps.push({name: 'dotfiles', status: 'skipped', reason: 'chezmoi not installed'})
|
|
271
|
+
} else {
|
|
272
|
+
const setupDotfiles = await confirm({
|
|
273
|
+
message: 'Set up chezmoi dotfiles management with age encryption?',
|
|
274
|
+
default: false,
|
|
275
|
+
})
|
|
276
|
+
if (setupDotfiles) {
|
|
277
|
+
try {
|
|
278
|
+
const dotfilesResult = await setupChezmoiInline(platform.platform)
|
|
279
|
+
config = await loadConfig()
|
|
280
|
+
steps.push({name: 'dotfiles', status: dotfilesResult.status, sourceDir: dotfilesResult.sourceDir})
|
|
281
|
+
} catch (err) {
|
|
282
|
+
steps.push({name: 'dotfiles', status: 'failed', reason: err instanceof Error ? err.message : String(err)})
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
steps.push({name: 'dotfiles', status: 'skipped', hint: 'Run `dvmi dotfiles setup` anytime to enable'})
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
278
289
|
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
// 8. Shell completions
|
|
291
|
+
steps.push({name: 'shell-completions', status: 'ok', action: 'install via: dvmi autocomplete'})
|
|
281
292
|
|
|
282
|
-
const result = {
|
|
293
|
+
const result = {steps, configPath: CONFIG_PATH}
|
|
283
294
|
|
|
284
|
-
|
|
295
|
+
if (isJson) return result
|
|
285
296
|
|
|
286
|
-
|
|
287
|
-
|
|
297
|
+
await typewriterLine('✓ Setup complete!')
|
|
298
|
+
this.log(chalk.dim(' Run `dvmi doctor` to verify your environment'))
|
|
288
299
|
|
|
289
300
|
return result
|
|
290
301
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
2
|
import ora from 'ora'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import {search, input} from '@inquirer/prompts'
|
|
4
|
+
import {listLogGroups, filterLogEvents, sinceToEpochMs} from '../../services/cloudwatch-logs.js'
|
|
5
|
+
import {loadConfig} from '../../services/config.js'
|
|
6
|
+
import {DvmiError} from '../../utils/errors.js'
|
|
7
7
|
|
|
8
8
|
const SINCE_OPTIONS = ['1h', '24h', '7d']
|
|
9
9
|
|
|
@@ -44,7 +44,7 @@ export default class Logs extends Command {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async run() {
|
|
47
|
-
const {
|
|
47
|
+
const {flags} = await this.parse(Logs)
|
|
48
48
|
const isJson = flags.json
|
|
49
49
|
|
|
50
50
|
// Validate --limit
|
|
@@ -86,9 +86,7 @@ export default class Logs extends Command {
|
|
|
86
86
|
message: 'Select a log group',
|
|
87
87
|
source: async (input) => {
|
|
88
88
|
const term = (input ?? '').toLowerCase()
|
|
89
|
-
return groups
|
|
90
|
-
.filter((g) => g.name.toLowerCase().includes(term))
|
|
91
|
-
.map((g) => ({ name: g.name, value: g.name }))
|
|
89
|
+
return groups.filter((g) => g.name.toLowerCase().includes(term)).map((g) => ({name: g.name, value: g.name}))
|
|
92
90
|
},
|
|
93
91
|
})
|
|
94
92
|
|
|
@@ -102,7 +100,7 @@ export default class Logs extends Command {
|
|
|
102
100
|
}
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
const {
|
|
103
|
+
const {startTime, endTime} = sinceToEpochMs(/** @type {'1h'|'24h'|'7d'} */ (flags.since))
|
|
106
104
|
|
|
107
105
|
const fetchSpinner = isJson ? null : ora('Fetching log events...').start()
|
|
108
106
|
|
|
@@ -171,14 +169,10 @@ export default class Logs extends Command {
|
|
|
171
169
|
_handleAwsError(err, _region, _logGroupName) {
|
|
172
170
|
const msg = String(err)
|
|
173
171
|
if (msg.includes('AccessDenied') || msg.includes('UnauthorizedAccess')) {
|
|
174
|
-
this.error(
|
|
175
|
-
'Access denied. Ensure your role has logs:DescribeLogGroups and logs:FilterLogEvents permissions.',
|
|
176
|
-
)
|
|
172
|
+
this.error('Access denied. Ensure your role has logs:DescribeLogGroups and logs:FilterLogEvents permissions.')
|
|
177
173
|
}
|
|
178
174
|
if (msg.includes('ResourceNotFoundException')) {
|
|
179
|
-
this.error(
|
|
180
|
-
`Log group not found. Check the name and confirm you are using the correct region (--region).`,
|
|
181
|
-
)
|
|
175
|
+
this.error(`Log group not found. Check the name and confirm you are using the correct region (--region).`)
|
|
182
176
|
}
|
|
183
177
|
if (msg.includes('InvalidParameterException')) {
|
|
184
178
|
this.error('Invalid filter pattern or parameter. Check the pattern syntax and time range.')
|
package/src/commands/open.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Args} from '@oclif/core'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import {exec} from '../services/shell.js'
|
|
4
|
+
import {openBrowser} from '../utils/open-browser.js'
|
|
5
|
+
import {loadConfig} from '../services/config.js'
|
|
6
6
|
|
|
7
7
|
const VALID_TARGETS = ['repo', 'pr', 'actions', 'aws']
|
|
8
8
|
|
|
@@ -20,11 +20,11 @@ export default class Open extends Command {
|
|
|
20
20
|
static enableJsonFlag = true
|
|
21
21
|
|
|
22
22
|
static args = {
|
|
23
|
-
target: Args.string({
|
|
23
|
+
target: Args.string({description: 'Target: repo, pr, actions, aws', required: true}),
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async run() {
|
|
27
|
-
const {
|
|
27
|
+
const {args, flags} = await this.parse(Open)
|
|
28
28
|
const isJson = flags.json
|
|
29
29
|
|
|
30
30
|
if (!VALID_TARGETS.includes(args.target)) {
|
|
@@ -55,15 +55,15 @@ export default class Open extends Command {
|
|
|
55
55
|
const branch = branchResult.stdout
|
|
56
56
|
// Try to find open PR for current branch
|
|
57
57
|
const prResult = await exec('gh', ['pr', 'view', '--json', 'url', '-H', branch])
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
if (prResult.exitCode === 0) {
|
|
59
|
+
url = JSON.parse(prResult.stdout).url
|
|
60
|
+
} else {
|
|
61
|
+
this.error(`No PR found for branch "${branch}". Create one with \`dvmi pr create\``)
|
|
62
|
+
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const result = {
|
|
66
|
+
const result = {target: args.target, url, opened: !isJson}
|
|
67
67
|
|
|
68
68
|
if (isJson) return result
|
|
69
69
|
|
|
@@ -1,26 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {Command, Args, Flags} from '@oclif/core'
|
|
2
|
+
import {exec} from '../../services/shell.js'
|
|
3
3
|
|
|
4
4
|
export default class PipelineLogs extends Command {
|
|
5
5
|
static description = 'Log di un workflow run specifico'
|
|
6
6
|
|
|
7
|
-
static examples = [
|
|
8
|
-
'<%= config.bin %> pipeline logs 12345',
|
|
9
|
-
'<%= config.bin %> pipeline logs 12345 --job test',
|
|
10
|
-
]
|
|
7
|
+
static examples = ['<%= config.bin %> pipeline logs 12345', '<%= config.bin %> pipeline logs 12345 --job test']
|
|
11
8
|
|
|
12
9
|
static enableJsonFlag = true
|
|
13
10
|
|
|
14
11
|
static args = {
|
|
15
|
-
'run-id': Args.integer({
|
|
12
|
+
'run-id': Args.integer({description: 'ID del workflow run', required: true}),
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
static flags = {
|
|
19
|
-
job: Flags.string({
|
|
16
|
+
job: Flags.string({description: 'Filtra per job name'}),
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
async run() {
|
|
23
|
-
const {
|
|
20
|
+
const {args, flags} = await this.parse(PipelineLogs)
|
|
24
21
|
const isJson = flags.json
|
|
25
22
|
|
|
26
23
|
const ghArgs = ['run', 'view', String(args['run-id']), '--log']
|
|
@@ -32,10 +29,10 @@ export default class PipelineLogs extends Command {
|
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
if (isJson) {
|
|
35
|
-
return {
|
|
32
|
+
return {runId: args['run-id'], log: result.stdout}
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
this.log(result.stdout)
|
|
39
|
-
return {
|
|
36
|
+
return {runId: args['run-id'], log: result.stdout}
|
|
40
37
|
}
|
|
41
38
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import {confirm} from '@inquirer/prompts'
|
|
5
|
+
import {listWorkflowRuns, rerunWorkflow} from '../../services/github.js'
|
|
6
|
+
import {exec} from '../../services/shell.js'
|
|
7
7
|
|
|
8
8
|
export default class PipelineRerun extends Command {
|
|
9
|
-
static description =
|
|
9
|
+
static description = "Rilancia l'ultimo workflow fallito"
|
|
10
10
|
|
|
11
11
|
static examples = [
|
|
12
12
|
'<%= config.bin %> pipeline rerun',
|
|
@@ -17,12 +17,12 @@ export default class PipelineRerun extends Command {
|
|
|
17
17
|
static enableJsonFlag = true
|
|
18
18
|
|
|
19
19
|
static flags = {
|
|
20
|
-
'run-id': Flags.integer({
|
|
21
|
-
'failed-only': Flags.boolean({
|
|
20
|
+
'run-id': Flags.integer({description: 'ID specifico del run'}),
|
|
21
|
+
'failed-only': Flags.boolean({description: 'Rilancia solo i job falliti', default: false}),
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
async run() {
|
|
25
|
-
const {
|
|
25
|
+
const {flags} = await this.parse(PipelineRerun)
|
|
26
26
|
const isJson = flags.json
|
|
27
27
|
|
|
28
28
|
const remoteResult = await exec('git', ['remote', 'get-url', 'origin'])
|
|
@@ -34,7 +34,7 @@ export default class PipelineRerun extends Command {
|
|
|
34
34
|
let runId = flags['run-id']
|
|
35
35
|
|
|
36
36
|
if (!runId) {
|
|
37
|
-
const runs = await listWorkflowRuns(owner, repo, {
|
|
37
|
+
const runs = await listWorkflowRuns(owner, repo, {limit: 10})
|
|
38
38
|
const failed = runs.find((r) => r.conclusion === 'failure')
|
|
39
39
|
if (!failed) {
|
|
40
40
|
this.log(chalk.green('No failed runs found.'))
|
|
@@ -47,19 +47,24 @@ export default class PipelineRerun extends Command {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (!isJson) {
|
|
50
|
-
const ok = await confirm({
|
|
51
|
-
if (!ok) {
|
|
50
|
+
const ok = await confirm({message: `Rerun workflow #${runId}?`})
|
|
51
|
+
if (!ok) {
|
|
52
|
+
this.log('Aborted.')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
const spinner = isJson
|
|
57
|
+
const spinner = isJson
|
|
58
|
+
? null
|
|
59
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Relaunching workflow...')}).start()
|
|
55
60
|
await rerunWorkflow(owner, repo, runId, flags['failed-only'])
|
|
56
61
|
spinner?.succeed(`Workflow #${runId} rerun started`)
|
|
57
62
|
|
|
58
|
-
const result = {
|
|
63
|
+
const result = {rerun: {id: runId, failedOnly: flags['failed-only'], status: 'queued'}}
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
if (!isJson) {
|
|
66
|
+
this.log(chalk.dim('Track with `dvmi pipeline status`'))
|
|
67
|
+
}
|
|
63
68
|
|
|
64
69
|
return result
|
|
65
70
|
}
|