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
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import {select} from '@inquirer/prompts'
|
|
5
|
+
import {listPrompts} from '../../services/prompts.js'
|
|
6
|
+
import {formatPromptTable, formatPromptBody} from '../../formatters/prompts.js'
|
|
7
|
+
import {DvmiError} from '../../utils/errors.js'
|
|
8
8
|
|
|
9
9
|
/** @import { Prompt } from '../../types.js' */
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ export default class PromptsList extends Command {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async run() {
|
|
30
|
-
const {
|
|
30
|
+
const {flags} = await this.parse(PromptsList)
|
|
31
31
|
const isJson = flags.json
|
|
32
32
|
|
|
33
33
|
const spinner = isJson
|
|
@@ -45,7 +45,7 @@ export default class PromptsList extends Command {
|
|
|
45
45
|
} catch (err) {
|
|
46
46
|
spinner?.fail()
|
|
47
47
|
if (err instanceof DvmiError) {
|
|
48
|
-
this.error(err.message, {
|
|
48
|
+
this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
|
|
49
49
|
}
|
|
50
50
|
throw err
|
|
51
51
|
}
|
|
@@ -65,7 +65,7 @@ export default class PromptsList extends Command {
|
|
|
65
65
|
: prompts
|
|
66
66
|
|
|
67
67
|
if (isJson) {
|
|
68
|
-
return {
|
|
68
|
+
return {prompts: filtered, total: filtered.length}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
if (filtered.length === 0) {
|
|
@@ -73,7 +73,7 @@ export default class PromptsList extends Command {
|
|
|
73
73
|
? chalk.dim(`No prompts matching "${flags.filter}".`)
|
|
74
74
|
: chalk.yellow('No prompts found in the repository.')
|
|
75
75
|
this.log(msg)
|
|
76
|
-
return {
|
|
76
|
+
return {prompts: [], total: 0}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const filterInfo = query ? chalk.dim(` — filter: ${chalk.white(`"${flags.filter}"`)}`) : ''
|
|
@@ -87,8 +87,8 @@ export default class PromptsList extends Command {
|
|
|
87
87
|
|
|
88
88
|
// Interactive selection to view full prompt content
|
|
89
89
|
try {
|
|
90
|
-
const choices = filtered.map((p) => ({
|
|
91
|
-
choices.push({
|
|
90
|
+
const choices = filtered.map((p) => ({name: p.title, value: p}))
|
|
91
|
+
choices.push({name: chalk.dim('← Exit'), value: /** @type {Prompt} */ (null)})
|
|
92
92
|
|
|
93
93
|
const selected = await select({
|
|
94
94
|
message: 'Select a prompt to view its content (or Exit):',
|
|
@@ -102,6 +102,6 @@ export default class PromptsList extends Command {
|
|
|
102
102
|
// User pressed Ctrl+C — exit gracefully
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
return {
|
|
105
|
+
return {prompts: filtered, total: filtered.length}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Args, Flags} 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 {
|
|
4
|
+
import {select, confirm} from '@inquirer/prompts'
|
|
5
|
+
import {join} from 'node:path'
|
|
6
|
+
import {readdir} from 'node:fs/promises'
|
|
7
|
+
import {resolveLocalPrompt, invokeTool, SUPPORTED_TOOLS} from '../../services/prompts.js'
|
|
8
|
+
import {loadConfig} from '../../services/config.js'
|
|
9
|
+
import {DvmiError} from '../../utils/errors.js'
|
|
10
10
|
|
|
11
11
|
/** @import { AITool } from '../../types.js' */
|
|
12
12
|
|
|
@@ -23,7 +23,7 @@ async function walkPrompts(dir, base) {
|
|
|
23
23
|
const results = []
|
|
24
24
|
let entries
|
|
25
25
|
try {
|
|
26
|
-
entries = await readdir(dir, {
|
|
26
|
+
entries = await readdir(dir, {withFileTypes: true})
|
|
27
27
|
} catch {
|
|
28
28
|
return results
|
|
29
29
|
}
|
|
@@ -67,7 +67,7 @@ export default class PromptsRun extends Command {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
async run() {
|
|
70
|
-
const {
|
|
70
|
+
const {args, flags} = await this.parse(PromptsRun)
|
|
71
71
|
const isJson = flags.json
|
|
72
72
|
|
|
73
73
|
// Load config
|
|
@@ -78,8 +78,7 @@ export default class PromptsRun extends Command {
|
|
|
78
78
|
/* use defaults */
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const localDir =
|
|
82
|
-
process.env.DVMI_PROMPTS_DIR ?? config.promptsDir ?? join(process.cwd(), DEFAULT_PROMPTS_DIR)
|
|
81
|
+
const localDir = process.env.DVMI_PROMPTS_DIR ?? config.promptsDir ?? join(process.cwd(), DEFAULT_PROMPTS_DIR)
|
|
83
82
|
|
|
84
83
|
// Resolve tool: --tool flag > config.aiTool
|
|
85
84
|
const toolName = /** @type {AITool | undefined} */ (flags.tool ?? config.aiTool)
|
|
@@ -111,7 +110,7 @@ export default class PromptsRun extends Command {
|
|
|
111
110
|
prompt = await resolveLocalPrompt(args.path, localDir)
|
|
112
111
|
} catch (err) {
|
|
113
112
|
if (err instanceof DvmiError) {
|
|
114
|
-
this.error(err.message, {
|
|
113
|
+
this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
|
|
115
114
|
}
|
|
116
115
|
throw err
|
|
117
116
|
}
|
|
@@ -142,7 +141,7 @@ export default class PromptsRun extends Command {
|
|
|
142
141
|
|
|
143
142
|
relativePath = await select({
|
|
144
143
|
message: 'Select a local prompt to run:',
|
|
145
|
-
choices: localPaths.map((p) => ({
|
|
144
|
+
choices: localPaths.map((p) => ({name: p, value: p})),
|
|
146
145
|
})
|
|
147
146
|
}
|
|
148
147
|
|
|
@@ -168,7 +167,7 @@ export default class PromptsRun extends Command {
|
|
|
168
167
|
} catch (err) {
|
|
169
168
|
spinner.fail()
|
|
170
169
|
if (err instanceof DvmiError) {
|
|
171
|
-
this.error(err.message, {
|
|
170
|
+
this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
|
|
172
171
|
}
|
|
173
172
|
throw err
|
|
174
173
|
}
|
|
@@ -180,14 +179,12 @@ export default class PromptsRun extends Command {
|
|
|
180
179
|
// This protects against prompt injection from tampered local files (originally
|
|
181
180
|
// downloaded from remote repositories). Skipped in CI/non-interactive environments.
|
|
182
181
|
if (!process.env.CI && process.stdin.isTTY) {
|
|
183
|
-
const preview = prompt.body.length > 500
|
|
184
|
-
? prompt.body.slice(0, 500) + chalk.dim('\n…[truncated]')
|
|
185
|
-
: prompt.body
|
|
182
|
+
const preview = prompt.body.length > 500 ? prompt.body.slice(0, 500) + chalk.dim('\n…[truncated]') : prompt.body
|
|
186
183
|
this.log(chalk.yellow('Prompt preview:'))
|
|
187
184
|
this.log(chalk.dim('─'.repeat(50)))
|
|
188
185
|
this.log(chalk.dim(preview))
|
|
189
186
|
this.log(chalk.dim('─'.repeat(50)) + '\n')
|
|
190
|
-
const ok = await confirm({
|
|
187
|
+
const ok = await confirm({message: `Run this prompt with ${toolName}?`, default: true})
|
|
191
188
|
if (!ok) {
|
|
192
189
|
this.log(chalk.dim('Aborted.'))
|
|
193
190
|
return
|
|
@@ -199,7 +196,7 @@ export default class PromptsRun extends Command {
|
|
|
199
196
|
await invokeTool(toolName, prompt.body)
|
|
200
197
|
} catch (err) {
|
|
201
198
|
if (err instanceof DvmiError) {
|
|
202
|
-
this.error(err.message, {
|
|
199
|
+
this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
|
|
203
200
|
}
|
|
204
201
|
throw err
|
|
205
202
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import {listRepos} from '../../services/github.js'
|
|
5
|
+
import {loadConfig} from '../../services/config.js'
|
|
6
|
+
import {renderTable} from '../../formatters/table.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @param {string} lang
|
|
@@ -13,22 +13,22 @@ function langColor(lang) {
|
|
|
13
13
|
const map = {
|
|
14
14
|
javascript: chalk.yellow,
|
|
15
15
|
typescript: chalk.blue,
|
|
16
|
-
python:
|
|
17
|
-
java:
|
|
18
|
-
go:
|
|
19
|
-
ruby:
|
|
20
|
-
rust:
|
|
21
|
-
kotlin:
|
|
22
|
-
swift:
|
|
23
|
-
php:
|
|
24
|
-
shell:
|
|
16
|
+
python: chalk.green,
|
|
17
|
+
java: chalk.red,
|
|
18
|
+
go: chalk.cyan,
|
|
19
|
+
ruby: chalk.magenta,
|
|
20
|
+
rust: chalk.hex('#CE422B'),
|
|
21
|
+
kotlin: chalk.hex('#7F52FF'),
|
|
22
|
+
swift: chalk.hex('#F05138'),
|
|
23
|
+
php: chalk.hex('#777BB4'),
|
|
24
|
+
shell: chalk.greenBright,
|
|
25
25
|
}
|
|
26
26
|
const fn = map[lang.toLowerCase()]
|
|
27
27
|
return fn ? fn(lang) : chalk.dim(lang)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export default class RepoList extends Command {
|
|
31
|
-
static description =
|
|
31
|
+
static description = "Lista repository dell'organizzazione"
|
|
32
32
|
|
|
33
33
|
static examples = [
|
|
34
34
|
'<%= config.bin %> repo list',
|
|
@@ -41,13 +41,13 @@ export default class RepoList extends Command {
|
|
|
41
41
|
static enableJsonFlag = true
|
|
42
42
|
|
|
43
43
|
static flags = {
|
|
44
|
-
language: Flags.string({
|
|
45
|
-
topic:
|
|
46
|
-
search:
|
|
44
|
+
language: Flags.string({description: 'Filtra per linguaggio'}),
|
|
45
|
+
topic: Flags.string({description: 'Filtra per topic'}),
|
|
46
|
+
search: Flags.string({char: 's', description: 'Cerca in nome e descrizione (case-insensitive)'}),
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async run() {
|
|
50
|
-
const {
|
|
50
|
+
const {flags} = await this.parse(RepoList)
|
|
51
51
|
const isJson = flags.json
|
|
52
52
|
const config = await loadConfig()
|
|
53
53
|
|
|
@@ -55,7 +55,9 @@ export default class RepoList extends Command {
|
|
|
55
55
|
this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const spinner = isJson
|
|
58
|
+
const spinner = isJson
|
|
59
|
+
? null
|
|
60
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching repositories...')}).start()
|
|
59
61
|
const repos = await listRepos(config.org, {
|
|
60
62
|
language: flags.language,
|
|
61
63
|
topic: flags.topic,
|
|
@@ -65,49 +67,63 @@ export default class RepoList extends Command {
|
|
|
65
67
|
// Search filter (name + description)
|
|
66
68
|
const searchQuery = flags.search?.toLowerCase()
|
|
67
69
|
const filtered = searchQuery
|
|
68
|
-
? repos.filter(
|
|
69
|
-
r.name.toLowerCase().includes(searchQuery) ||
|
|
70
|
-
r.description.toLowerCase().includes(searchQuery),
|
|
70
|
+
? repos.filter(
|
|
71
|
+
(r) => r.name.toLowerCase().includes(searchQuery) || r.description.toLowerCase().includes(searchQuery),
|
|
71
72
|
)
|
|
72
73
|
: repos
|
|
73
74
|
|
|
74
|
-
if (isJson) return {
|
|
75
|
+
if (isJson) return {repositories: filtered, total: filtered.length}
|
|
75
76
|
|
|
76
77
|
if (repos.length === 0) {
|
|
77
78
|
this.log(chalk.yellow('No repositories found matching your filters.'))
|
|
78
|
-
return {
|
|
79
|
+
return {repositories: [], total: 0}
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
if (filtered.length === 0) {
|
|
82
83
|
this.log(chalk.dim(`No repositories matching "${flags.search}".`))
|
|
83
|
-
return {
|
|
84
|
+
return {repositories: [], total: 0}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// Build filter info line
|
|
87
88
|
const filterInfo = [
|
|
88
89
|
flags.language && chalk.dim(`language: ${chalk.white(flags.language)}`),
|
|
89
|
-
flags.topic
|
|
90
|
-
flags.search
|
|
91
|
-
]
|
|
90
|
+
flags.topic && chalk.dim(`topic: ${chalk.white(flags.topic)}`),
|
|
91
|
+
flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(chalk.dim(' · '))
|
|
92
95
|
|
|
93
96
|
this.log(
|
|
94
97
|
chalk.bold(`\nRepositories in ${config.org}`) +
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
(filterInfo ? chalk.dim(' — ') + filterInfo : '') +
|
|
99
|
+
chalk.dim(` (${filtered.length}${filtered.length < repos.length ? `/${repos.length}` : ''})`) +
|
|
100
|
+
'\n',
|
|
98
101
|
)
|
|
99
102
|
|
|
100
|
-
this.log(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
this.log(
|
|
104
|
+
renderTable(filtered, [
|
|
105
|
+
{header: 'Name', key: 'name', width: 40},
|
|
106
|
+
{
|
|
107
|
+
header: 'Language',
|
|
108
|
+
key: 'language',
|
|
109
|
+
width: 14,
|
|
110
|
+
format: (v) => v || '—',
|
|
111
|
+
colorize: (v) => (v === '—' ? chalk.dim(v) : langColor(v)),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
header: 'Last push',
|
|
115
|
+
key: 'pushedAt',
|
|
116
|
+
width: 12,
|
|
117
|
+
format: (v) => {
|
|
118
|
+
const d = new Date(String(v))
|
|
119
|
+
return isNaN(d.getTime()) ? '—' : d.toLocaleDateString()
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—')},
|
|
123
|
+
]),
|
|
124
|
+
)
|
|
109
125
|
|
|
110
126
|
this.log('')
|
|
111
|
-
return {
|
|
127
|
+
return {repositories: filtered, total: filtered.length}
|
|
112
128
|
}
|
|
113
129
|
}
|
package/src/commands/search.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Args, Flags} from '@oclif/core'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import {searchCode} from '../services/github.js'
|
|
5
|
+
import {loadConfig} from '../services/config.js'
|
|
6
|
+
import {renderTable} from '../formatters/table.js'
|
|
7
7
|
|
|
8
8
|
export default class Search extends Command {
|
|
9
|
-
static description =
|
|
9
|
+
static description = "Cerca codice nei repository dell'organizzazione"
|
|
10
10
|
|
|
11
11
|
static examples = [
|
|
12
12
|
'<%= config.bin %> search "getUserById"',
|
|
@@ -17,17 +17,17 @@ export default class Search extends Command {
|
|
|
17
17
|
static enableJsonFlag = true
|
|
18
18
|
|
|
19
19
|
static args = {
|
|
20
|
-
term: Args.string({
|
|
20
|
+
term: Args.string({description: 'Termine di ricerca', required: true}),
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
static flags = {
|
|
24
|
-
language: Flags.string({
|
|
25
|
-
repo: Flags.string({
|
|
26
|
-
limit: Flags.integer({
|
|
24
|
+
language: Flags.string({description: 'Filtra per linguaggio'}),
|
|
25
|
+
repo: Flags.string({description: 'Cerca in un repo specifico'}),
|
|
26
|
+
limit: Flags.integer({description: 'Max risultati', default: 20}),
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async run() {
|
|
30
|
-
const {
|
|
30
|
+
const {args, flags} = await this.parse(Search)
|
|
31
31
|
const isJson = flags.json
|
|
32
32
|
const config = await loadConfig()
|
|
33
33
|
|
|
@@ -43,20 +43,22 @@ export default class Search extends Command {
|
|
|
43
43
|
})
|
|
44
44
|
spinner?.stop()
|
|
45
45
|
|
|
46
|
-
if (isJson) return {
|
|
46
|
+
if (isJson) return {results, total: results.length}
|
|
47
47
|
|
|
48
48
|
if (results.length === 0) {
|
|
49
49
|
this.log(chalk.yellow(`No results found for "${args.term}" in the organization.`))
|
|
50
|
-
return {
|
|
50
|
+
return {results: [], total: 0}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
this.log(chalk.bold(`\n${results.length} result(s) for "${args.term}":\n`))
|
|
54
|
-
this.log(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
this.log(
|
|
55
|
+
renderTable(results, [
|
|
56
|
+
{header: 'Repo', key: 'repo', width: 25},
|
|
57
|
+
{header: 'File', key: 'file', width: 45},
|
|
58
|
+
{header: 'Match', key: 'match'},
|
|
59
|
+
]),
|
|
60
|
+
)
|
|
59
61
|
|
|
60
|
-
return {
|
|
62
|
+
return {results, total: results.length}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
|
+
import {confirm, select} from '@inquirer/prompts'
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import {execa} from 'execa'
|
|
6
|
+
import {detectPlatform} from '../../services/platform.js'
|
|
7
|
+
import {exec} from '../../services/shell.js'
|
|
8
|
+
import {buildSteps, checkToolStatus, listGpgKeys, deriveOverallStatus} from '../../services/security.js'
|
|
9
|
+
import {formatEducationalIntro, formatStepHeader, formatSecuritySummary} from '../../formatters/security.js'
|
|
10
10
|
/** @import { SetupSession, SetupStep, StepResult, PlatformInfo } from '../../types.js' */
|
|
11
11
|
|
|
12
12
|
export default class SecuritySetup extends Command {
|
|
13
|
-
static description =
|
|
13
|
+
static description =
|
|
14
|
+
'Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)'
|
|
14
15
|
|
|
15
|
-
static examples = [
|
|
16
|
-
'<%= config.bin %> security setup',
|
|
17
|
-
'<%= config.bin %> security setup --json',
|
|
18
|
-
]
|
|
16
|
+
static examples = ['<%= config.bin %> security setup', '<%= config.bin %> security setup --json']
|
|
19
17
|
|
|
20
18
|
static enableJsonFlag = true
|
|
21
19
|
|
|
22
20
|
static flags = {
|
|
23
|
-
help: Flags.help({
|
|
21
|
+
help: Flags.help({char: 'h'}),
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
async run() {
|
|
27
|
-
const {
|
|
25
|
+
const {flags} = await this.parse(SecuritySetup)
|
|
28
26
|
const isJson = flags.json
|
|
29
27
|
|
|
30
28
|
// FR-018: Detect non-interactive environments
|
|
@@ -34,22 +32,19 @@ export default class SecuritySetup extends Command {
|
|
|
34
32
|
if ((isCI || isNonInteractive) && !isJson) {
|
|
35
33
|
this.error(
|
|
36
34
|
'This command requires an interactive terminal (TTY). Run with --json for a non-interactive health check.',
|
|
37
|
-
{
|
|
35
|
+
{exit: 1},
|
|
38
36
|
)
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
// Detect platform
|
|
42
40
|
const platformInfo = await detectPlatform()
|
|
43
|
-
const {
|
|
41
|
+
const {platform} = platformInfo
|
|
44
42
|
|
|
45
43
|
// FR-019: Sudo pre-flight on Linux/WSL2
|
|
46
44
|
if (platform !== 'macos' && !isJson) {
|
|
47
45
|
const sudoCheck = await exec('sudo', ['-n', 'true'])
|
|
48
46
|
if (sudoCheck.exitCode !== 0) {
|
|
49
|
-
this.error(
|
|
50
|
-
'sudo access is required to install packages. Run `sudo -v` to authenticate and retry.',
|
|
51
|
-
{ exit: 1 },
|
|
52
|
-
)
|
|
47
|
+
this.error('sudo access is required to install packages. Run `sudo -v` to authenticate and retry.', {exit: 1})
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
50
|
|
|
@@ -68,7 +63,11 @@ export default class SecuritySetup extends Command {
|
|
|
68
63
|
// ---------------------------------------------------------------------------
|
|
69
64
|
// Pre-check: show current tool status
|
|
70
65
|
// ---------------------------------------------------------------------------
|
|
71
|
-
const spinner = ora({
|
|
66
|
+
const spinner = ora({
|
|
67
|
+
spinner: 'arc',
|
|
68
|
+
color: false,
|
|
69
|
+
text: chalk.hex('#FF6B2B')('Checking current tool status...'),
|
|
70
|
+
}).start()
|
|
72
71
|
const currentStatus = await checkToolStatus(platform)
|
|
73
72
|
spinner.stop()
|
|
74
73
|
|
|
@@ -100,7 +99,7 @@ export default class SecuritySetup extends Command {
|
|
|
100
99
|
})
|
|
101
100
|
if (!understood) {
|
|
102
101
|
this.log('Setup cancelled.')
|
|
103
|
-
return {
|
|
102
|
+
return {platform, selection: null, tools: currentStatus, overallStatus: deriveOverallStatus(currentStatus)}
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
// ---------------------------------------------------------------------------
|
|
@@ -109,9 +108,9 @@ export default class SecuritySetup extends Command {
|
|
|
109
108
|
const selectionValue = await select({
|
|
110
109
|
message: 'What would you like to set up?',
|
|
111
110
|
choices: [
|
|
112
|
-
{
|
|
113
|
-
{
|
|
114
|
-
{
|
|
111
|
+
{name: 'Both AWS and Git credentials (recommended)', value: 'both'},
|
|
112
|
+
{name: 'AWS credentials only (aws-vault)', value: 'aws'},
|
|
113
|
+
{name: 'Git credentials only (macOS Keychain / GCM)', value: 'git'},
|
|
115
114
|
],
|
|
116
115
|
})
|
|
117
116
|
|
|
@@ -131,7 +130,7 @@ export default class SecuritySetup extends Command {
|
|
|
131
130
|
name: `${k.name} <${k.email}> (${k.id})`,
|
|
132
131
|
value: k.id,
|
|
133
132
|
})),
|
|
134
|
-
{
|
|
133
|
+
{name: 'Create a new GPG key', value: '__new__'},
|
|
135
134
|
]
|
|
136
135
|
const chosen = await select({
|
|
137
136
|
message: 'Select a GPG key for pass and Git Credential Manager:',
|
|
@@ -145,7 +144,7 @@ export default class SecuritySetup extends Command {
|
|
|
145
144
|
// ---------------------------------------------------------------------------
|
|
146
145
|
// Build steps
|
|
147
146
|
// ---------------------------------------------------------------------------
|
|
148
|
-
const steps = buildSteps(platformInfo, selection, {
|
|
147
|
+
const steps = buildSteps(platformInfo, selection, {gpgId})
|
|
149
148
|
|
|
150
149
|
/** @type {SetupSession} */
|
|
151
150
|
const session = {
|
|
@@ -166,9 +165,9 @@ export default class SecuritySetup extends Command {
|
|
|
166
165
|
|
|
167
166
|
// FR-014: confirmation prompt before system-level changes
|
|
168
167
|
if (step.requiresConfirmation) {
|
|
169
|
-
const proceed = await confirm({
|
|
168
|
+
const proceed = await confirm({message: `Proceed with: ${step.label}?`, default: true})
|
|
170
169
|
if (!proceed) {
|
|
171
|
-
session.results.set(step.id, {
|
|
170
|
+
session.results.set(step.id, {status: 'skipped', message: 'Skipped by user'})
|
|
172
171
|
this.log(chalk.dim(' Skipped.'))
|
|
173
172
|
continue
|
|
174
173
|
}
|
|
@@ -179,17 +178,17 @@ export default class SecuritySetup extends Command {
|
|
|
179
178
|
this.log(chalk.cyan('\n GPG will now prompt you for a passphrase in your terminal.'))
|
|
180
179
|
this.log(chalk.dim(' Follow the interactive prompts to complete key generation.\n'))
|
|
181
180
|
try {
|
|
182
|
-
await execa('gpg', ['--full-generate-key'], {
|
|
181
|
+
await execa('gpg', ['--full-generate-key'], {stdio: 'inherit', reject: true})
|
|
183
182
|
// Refresh the gpgId from newly created key
|
|
184
183
|
const newKeys = await listGpgKeys()
|
|
185
184
|
if (newKeys.length > 0) {
|
|
186
185
|
gpgId = newKeys[0].id
|
|
187
186
|
// gpgId is now set — subsequent step closures capture it via the shared context object
|
|
188
187
|
}
|
|
189
|
-
session.results.set(step.id, {
|
|
188
|
+
session.results.set(step.id, {status: 'success', message: `GPG key created (${gpgId || 'new key'})`})
|
|
190
189
|
this.log(chalk.green(' ✔ GPG key created'))
|
|
191
190
|
} catch {
|
|
192
|
-
const result = {
|
|
191
|
+
const result = {status: /** @type {'failed'} */ ('failed'), hint: 'Run manually: gpg --full-generate-key'}
|
|
193
192
|
session.results.set(step.id, result)
|
|
194
193
|
this.log(chalk.red(' ✗ GPG key creation failed'))
|
|
195
194
|
this.log(chalk.dim(` → ${result.hint}`))
|
|
@@ -200,7 +199,7 @@ export default class SecuritySetup extends Command {
|
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
// Regular step with spinner
|
|
203
|
-
const stepSpinner = ora({
|
|
202
|
+
const stepSpinner = ora({spinner: 'arc', color: false, text: chalk.dim(step.label)}).start()
|
|
204
203
|
|
|
205
204
|
let result
|
|
206
205
|
try {
|
|
@@ -243,7 +242,12 @@ export default class SecuritySetup extends Command {
|
|
|
243
242
|
platform,
|
|
244
243
|
selection,
|
|
245
244
|
tools: currentStatus,
|
|
246
|
-
overallStatus:
|
|
245
|
+
overallStatus:
|
|
246
|
+
session.overallStatus === 'completed'
|
|
247
|
+
? 'success'
|
|
248
|
+
: session.overallStatus === 'failed'
|
|
249
|
+
? 'partial'
|
|
250
|
+
: 'not-configured',
|
|
247
251
|
}
|
|
248
252
|
}
|
|
249
253
|
}
|