devvami 1.4.1 → 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 +41 -1
- 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 +95 -21
- 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 +25 -17
- 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
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags, Args} from '@oclif/core'
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
11
|
|
|
12
12
|
/** @import { DotfilesSyncResult } from '../../types.js' */
|
|
13
13
|
|
|
@@ -26,18 +26,18 @@ export default class DotfilesSync extends Command {
|
|
|
26
26
|
static enableJsonFlag = true
|
|
27
27
|
|
|
28
28
|
static flags = {
|
|
29
|
-
help: Flags.help({
|
|
30
|
-
push: Flags.boolean({
|
|
31
|
-
pull: Flags.boolean({
|
|
32
|
-
'dry-run': Flags.boolean({
|
|
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
33
|
}
|
|
34
34
|
|
|
35
35
|
static args = {
|
|
36
|
-
repo: Args.string({
|
|
36
|
+
repo: Args.string({description: 'Remote repository URL (for initial remote setup)', required: false}),
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async run() {
|
|
40
|
-
const {
|
|
40
|
+
const {flags, args} = await this.parse(DotfilesSync)
|
|
41
41
|
const isJson = flags.json
|
|
42
42
|
const isPush = flags.push
|
|
43
43
|
const isPull = flags.pull
|
|
@@ -56,30 +56,27 @@ export default class DotfilesSync extends Command {
|
|
|
56
56
|
const isCI = process.env.CI === 'true'
|
|
57
57
|
const isNonInteractive = !process.stdout.isTTY
|
|
58
58
|
if ((isCI || isNonInteractive) && !isJson) {
|
|
59
|
-
this.error(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
59
|
+
this.error('This command requires an interactive terminal (TTY). Run with --json for a non-interactive sync.', {
|
|
60
|
+
exit: 1,
|
|
61
|
+
})
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
const config = await loadConfig()
|
|
66
65
|
if (!config.dotfiles?.enabled) {
|
|
67
|
-
throw new DvmiError(
|
|
68
|
-
'Chezmoi dotfiles management is not configured',
|
|
69
|
-
'Run `dvmi dotfiles setup` first',
|
|
70
|
-
)
|
|
66
|
+
throw new DvmiError('Chezmoi dotfiles management is not configured', 'Run `dvmi dotfiles setup` first')
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
const chezmoiInstalled = await isChezmoiInstalled()
|
|
74
70
|
if (!chezmoiInstalled) {
|
|
75
71
|
const platformInfo = await detectPlatform()
|
|
76
|
-
const hint =
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const hint =
|
|
73
|
+
platformInfo.platform === 'macos'
|
|
74
|
+
? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
|
|
75
|
+
: 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
|
|
79
76
|
throw new DvmiError('chezmoi is not installed', hint)
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
const remote = config.dotfiles?.repo ?? await getChezmoiRemote()
|
|
79
|
+
const remote = config.dotfiles?.repo ?? (await getChezmoiRemote())
|
|
83
80
|
|
|
84
81
|
// --json mode: attempt push/pull or report status
|
|
85
82
|
if (isJson) {
|
|
@@ -105,7 +102,13 @@ export default class DotfilesSync extends Command {
|
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
/** @type {DotfilesSyncResult} */
|
|
108
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
action: 'skipped',
|
|
107
|
+
repo: effectiveRemote ?? null,
|
|
108
|
+
status: 'skipped',
|
|
109
|
+
message: 'No action specified',
|
|
110
|
+
conflicts: [],
|
|
111
|
+
}
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
// ---------------------------------------------------------------------------
|
|
@@ -140,22 +143,29 @@ export default class DotfilesSync extends Command {
|
|
|
140
143
|
const action = await select({
|
|
141
144
|
message: 'What would you like to do?',
|
|
142
145
|
choices: [
|
|
143
|
-
{
|
|
144
|
-
{
|
|
145
|
-
{
|
|
146
|
+
{name: 'Push local changes to remote', value: 'push'},
|
|
147
|
+
{name: 'Pull remote changes and apply', value: 'pull'},
|
|
148
|
+
{name: 'Cancel', value: 'cancel'},
|
|
146
149
|
],
|
|
147
150
|
})
|
|
148
151
|
|
|
149
152
|
if (action === 'cancel') {
|
|
150
153
|
/** @type {DotfilesSyncResult} */
|
|
151
|
-
const cancelResult = {
|
|
154
|
+
const cancelResult = {
|
|
155
|
+
action: 'skipped',
|
|
156
|
+
repo: effectiveRemote ?? null,
|
|
157
|
+
status: 'skipped',
|
|
158
|
+
message: 'Cancelled by user',
|
|
159
|
+
conflicts: [],
|
|
160
|
+
}
|
|
152
161
|
this.log(formatDotfilesSync(cancelResult))
|
|
153
162
|
return cancelResult
|
|
154
163
|
}
|
|
155
164
|
|
|
156
|
-
const result =
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
const result =
|
|
166
|
+
action === 'push'
|
|
167
|
+
? await this._push(effectiveRemote, isDryRun, false)
|
|
168
|
+
: await this._pull(effectiveRemote, isDryRun, false)
|
|
159
169
|
|
|
160
170
|
this.log(formatDotfilesSync(result))
|
|
161
171
|
return result
|
|
@@ -174,25 +184,25 @@ export default class DotfilesSync extends Command {
|
|
|
174
184
|
const choice = await select({
|
|
175
185
|
message: 'Connect to an existing dotfiles repository or create a new one?',
|
|
176
186
|
choices: [
|
|
177
|
-
{
|
|
178
|
-
{
|
|
187
|
+
{name: 'Connect to existing repository', value: 'existing'},
|
|
188
|
+
{name: 'Create new repository on GitHub', value: 'new'},
|
|
179
189
|
],
|
|
180
190
|
})
|
|
181
191
|
|
|
182
192
|
let repoUrl = ''
|
|
183
193
|
|
|
184
194
|
if (choice === 'existing') {
|
|
185
|
-
repoUrl = await input({
|
|
195
|
+
repoUrl = await input({message: 'Repository URL (SSH or HTTPS):'})
|
|
186
196
|
} else {
|
|
187
|
-
const repoName = await input({
|
|
188
|
-
const isPrivate = await confirm({
|
|
197
|
+
const repoName = await input({message: 'Repository name:', default: 'dotfiles'})
|
|
198
|
+
const isPrivate = await confirm({message: 'Make repository private?', default: true})
|
|
189
199
|
|
|
190
200
|
if (!isDryRun) {
|
|
191
201
|
try {
|
|
192
202
|
const visFlag = isPrivate ? '--private' : '--public'
|
|
193
203
|
await execOrThrow('gh', ['repo', 'create', repoName, visFlag, '--confirm'])
|
|
194
204
|
// Get the SSH URL from the created repo
|
|
195
|
-
const {
|
|
205
|
+
const {exec} = await import('../../services/shell.js')
|
|
196
206
|
const result = await exec('gh', ['repo', 'view', repoName, '--json', 'sshUrl', '--jq', '.sshUrl'])
|
|
197
207
|
repoUrl = result.stdout.trim() || `git@github.com:${repoName}.git`
|
|
198
208
|
} catch {
|
|
@@ -211,7 +221,7 @@ export default class DotfilesSync extends Command {
|
|
|
211
221
|
await execOrThrow('chezmoi', ['git', '--', 'remote', 'add', 'origin', repoUrl])
|
|
212
222
|
await execOrThrow('chezmoi', ['git', '--', 'push', '-u', 'origin', 'main'])
|
|
213
223
|
// Save repo to dvmi config
|
|
214
|
-
config.dotfiles = {
|
|
224
|
+
config.dotfiles = {...(config.dotfiles ?? {enabled: true}), repo: repoUrl}
|
|
215
225
|
await saveConfig(config)
|
|
216
226
|
} catch (err) {
|
|
217
227
|
/** @type {DotfilesSyncResult} */
|
|
@@ -232,7 +242,9 @@ export default class DotfilesSync extends Command {
|
|
|
232
242
|
action: 'init-remote',
|
|
233
243
|
repo: repoUrl,
|
|
234
244
|
status: isDryRun ? 'skipped' : 'success',
|
|
235
|
-
message: isDryRun
|
|
245
|
+
message: isDryRun
|
|
246
|
+
? `Would configure remote: ${repoUrl}`
|
|
247
|
+
: 'Remote repository configured and initial push completed',
|
|
236
248
|
conflicts: [],
|
|
237
249
|
}
|
|
238
250
|
this.log(formatDotfilesSync(result))
|
|
@@ -271,7 +283,9 @@ export default class DotfilesSync extends Command {
|
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
285
|
|
|
274
|
-
const spinner = isJson
|
|
286
|
+
const spinner = isJson
|
|
287
|
+
? null
|
|
288
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pushing to remote...')}).start()
|
|
275
289
|
|
|
276
290
|
try {
|
|
277
291
|
// Stage all changes
|
|
@@ -282,7 +296,7 @@ export default class DotfilesSync extends Command {
|
|
|
282
296
|
await execOrThrow('chezmoi', ['git', '--', 'push', 'origin', 'HEAD'])
|
|
283
297
|
spinner?.succeed(chalk.green('Pushed to remote'))
|
|
284
298
|
|
|
285
|
-
return {
|
|
299
|
+
return {action: 'push', repo: remote, status: 'success', message: 'Changes pushed to remote', conflicts: []}
|
|
286
300
|
} catch (err) {
|
|
287
301
|
spinner?.fail(chalk.red('Push failed'))
|
|
288
302
|
return {
|
|
@@ -327,7 +341,9 @@ export default class DotfilesSync extends Command {
|
|
|
327
341
|
}
|
|
328
342
|
}
|
|
329
343
|
|
|
330
|
-
const spinner = isJson
|
|
344
|
+
const spinner = isJson
|
|
345
|
+
? null
|
|
346
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pulling from remote...')}).start()
|
|
331
347
|
|
|
332
348
|
try {
|
|
333
349
|
// Check if chezmoi init was done with this remote (first-time pull)
|
|
@@ -360,7 +376,7 @@ export default class DotfilesSync extends Command {
|
|
|
360
376
|
}
|
|
361
377
|
}
|
|
362
378
|
|
|
363
|
-
return {
|
|
379
|
+
return {action: 'pull', repo: remote, status: 'success', message: 'Remote changes applied', conflicts: []}
|
|
364
380
|
} catch (err) {
|
|
365
381
|
spinner?.fail(chalk.red('Pull failed'))
|
|
366
382
|
return {
|
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
|
}
|