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,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
|
+
import {writeFile} from 'node:fs/promises'
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import {detectEcosystems, supportedEcosystemsMessage} from '../../services/audit-detector.js'
|
|
6
|
+
import {runAudit, summarizeFindings, filterBySeverity} from '../../services/audit-runner.js'
|
|
7
|
+
import {
|
|
8
|
+
formatFindingsTable,
|
|
9
|
+
formatScanSummary,
|
|
10
|
+
formatMarkdownReport,
|
|
11
|
+
truncate,
|
|
12
|
+
colorSeverity,
|
|
13
|
+
} from '../../formatters/vuln.js'
|
|
14
|
+
import {getCveDetail} from '../../services/nvd.js'
|
|
15
|
+
import {startInteractiveTable} from '../../utils/tui/navigable-table.js'
|
|
10
16
|
|
|
11
17
|
// Minimum terminal rows required to show the interactive TUI (same threshold as vuln search)
|
|
12
18
|
const MIN_TTY_ROWS = 6
|
|
@@ -49,9 +55,9 @@ export default class VulnScan extends Command {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
async run() {
|
|
52
|
-
const {
|
|
58
|
+
const {flags} = await this.parse(VulnScan)
|
|
53
59
|
const isJson = flags.json
|
|
54
|
-
const {
|
|
60
|
+
const {severity, 'no-fail': noFail, report} = flags
|
|
55
61
|
|
|
56
62
|
const projectPath = process.env.DVMI_SCAN_DIR ?? process.cwd()
|
|
57
63
|
const scanDate = new Date().toISOString()
|
|
@@ -66,8 +72,8 @@ export default class VulnScan extends Command {
|
|
|
66
72
|
scanDate,
|
|
67
73
|
ecosystems: [],
|
|
68
74
|
findings: [],
|
|
69
|
-
summary: {
|
|
70
|
-
errors: [{
|
|
75
|
+
summary: {critical: 0, high: 0, medium: 0, low: 0, unknown: 0, total: 0},
|
|
76
|
+
errors: [{ecosystem: 'none', message: 'No supported package manager detected.'}],
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -99,11 +105,11 @@ export default class VulnScan extends Command {
|
|
|
99
105
|
for (const eco of ecosystems) {
|
|
100
106
|
const spinner = isJson ? null : ora(` Scanning ${eco.name} dependencies...`).start()
|
|
101
107
|
|
|
102
|
-
const {
|
|
108
|
+
const {findings, error} = await runAudit(eco)
|
|
103
109
|
|
|
104
110
|
if (error) {
|
|
105
111
|
spinner?.fail(` Scanning ${eco.name} dependencies... failed`)
|
|
106
|
-
errors.push({
|
|
112
|
+
errors.push({ecosystem: eco.name, message: error})
|
|
107
113
|
} else {
|
|
108
114
|
spinner?.succeed(` Scanning ${eco.name} dependencies... done`)
|
|
109
115
|
allFindings.push(...findings)
|
|
@@ -170,25 +176,38 @@ export default class VulnScan extends Command {
|
|
|
170
176
|
|
|
171
177
|
/** @type {import('../../utils/tui/navigable-table.js').TableColumnDef[]} */
|
|
172
178
|
const columns = [
|
|
173
|
-
{
|
|
174
|
-
{
|
|
175
|
-
{
|
|
176
|
-
{
|
|
177
|
-
|
|
179
|
+
{header: 'Package', key: 'pkg', width: COL_WIDTHS.pkg},
|
|
180
|
+
{header: 'Version', key: 'version', width: COL_WIDTHS.version},
|
|
181
|
+
{header: 'Severity', key: 'severity', width: COL_WIDTHS.severity, colorize: (v) => colorSeverity(v)},
|
|
182
|
+
{
|
|
183
|
+
header: 'CVE',
|
|
184
|
+
key: 'cve',
|
|
185
|
+
width: COL_WIDTHS.cve,
|
|
186
|
+
colorize: (v) => (v !== '—' ? chalk.cyan(v) : chalk.gray(v)),
|
|
187
|
+
},
|
|
188
|
+
{header: 'Title', key: 'title', width: titleWidth},
|
|
178
189
|
]
|
|
179
190
|
|
|
180
191
|
await startInteractiveTable(rows, columns, heading, filteredFindings.length, getCveDetail)
|
|
181
192
|
} else {
|
|
182
193
|
// Non-TTY fallback: static table + summary (unchanged from pre-TUI behaviour)
|
|
183
194
|
if (filteredFindings.length > 0) {
|
|
184
|
-
this.log(
|
|
195
|
+
this.log(
|
|
196
|
+
chalk.bold(
|
|
197
|
+
` Findings (${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'})`,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
185
200
|
this.log('')
|
|
186
201
|
this.log(formatFindingsTable(filteredFindings))
|
|
187
202
|
this.log('')
|
|
188
203
|
this.log(chalk.bold(' Summary'))
|
|
189
204
|
this.log(formatScanSummary(summary))
|
|
190
205
|
this.log('')
|
|
191
|
-
this.log(
|
|
206
|
+
this.log(
|
|
207
|
+
chalk.yellow(
|
|
208
|
+
` ⚠ ${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'} found. Run \`dvmi vuln detail <CVE-ID>\` for details.`,
|
|
209
|
+
),
|
|
210
|
+
)
|
|
192
211
|
}
|
|
193
212
|
}
|
|
194
213
|
|
|
@@ -1,10 +1,10 @@
|
|
|
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 {
|
|
4
|
+
import {searchCves, getCveDetail} from '../../services/nvd.js'
|
|
5
|
+
import {formatCveSearchTable, colorSeverity, formatScore, formatDate, truncate} from '../../formatters/vuln.js'
|
|
6
|
+
import {startInteractiveTable} from '../../utils/tui/navigable-table.js'
|
|
7
|
+
import {ValidationError} from '../../utils/errors.js'
|
|
8
8
|
|
|
9
9
|
// Minimum terminal rows required to show the interactive TUI
|
|
10
10
|
const MIN_TTY_ROWS = 6
|
|
@@ -33,7 +33,10 @@ export default class VulnSearch extends Command {
|
|
|
33
33
|
static enableJsonFlag = true
|
|
34
34
|
|
|
35
35
|
static args = {
|
|
36
|
-
keyword: Args.string({
|
|
36
|
+
keyword: Args.string({
|
|
37
|
+
description: 'Product, library, or keyword to search for (optional — omit to see all recent CVEs)',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
static flags = {
|
|
@@ -55,11 +58,11 @@ export default class VulnSearch extends Command {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
async run() {
|
|
58
|
-
const {
|
|
61
|
+
const {args, flags} = await this.parse(VulnSearch)
|
|
59
62
|
const isJson = flags.json
|
|
60
63
|
|
|
61
|
-
const {
|
|
62
|
-
const {
|
|
64
|
+
const {keyword} = args
|
|
65
|
+
const {days, severity, limit} = flags
|
|
63
66
|
|
|
64
67
|
if (days < 1 || days > 120) {
|
|
65
68
|
throw new ValidationError(
|
|
@@ -75,13 +78,15 @@ export default class VulnSearch extends Command {
|
|
|
75
78
|
)
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
const spinner = isJson
|
|
81
|
+
const spinner = isJson
|
|
82
|
+
? null
|
|
83
|
+
: ora(keyword ? `Searching NVD for "${keyword}"...` : `Fetching recent CVEs (last ${days} days)...`).start()
|
|
79
84
|
|
|
80
85
|
try {
|
|
81
|
-
const {
|
|
86
|
+
const {results, totalResults} = await searchCves({keyword, days, severity, limit})
|
|
82
87
|
spinner?.stop()
|
|
83
88
|
|
|
84
|
-
const result = {
|
|
89
|
+
const result = {keyword: keyword ?? null, days, severity: severity ?? null, totalResults, results}
|
|
85
90
|
|
|
86
91
|
if (isJson) return result
|
|
87
92
|
|
|
@@ -108,12 +113,12 @@ export default class VulnSearch extends Command {
|
|
|
108
113
|
|
|
109
114
|
/** @type {import('../../utils/tui/navigable-table.js').TableColumnDef[]} */
|
|
110
115
|
const columns = [
|
|
111
|
-
{
|
|
112
|
-
{
|
|
113
|
-
{
|
|
114
|
-
{
|
|
115
|
-
{
|
|
116
|
-
{
|
|
116
|
+
{header: 'CVE ID', key: 'id', width: COL_WIDTHS.id, colorize: (v) => chalk.cyan(v)},
|
|
117
|
+
{header: 'Severity', key: 'severity', width: COL_WIDTHS.severity, colorize: (v) => colorSeverity(v)},
|
|
118
|
+
{header: 'Score', key: 'score', width: COL_WIDTHS.score},
|
|
119
|
+
{header: 'Published', key: 'published', width: COL_WIDTHS.published},
|
|
120
|
+
{header: 'Description', key: 'description', width: descWidth},
|
|
121
|
+
{header: 'Reference', key: 'reference', width: COL_WIDTHS.reference},
|
|
117
122
|
]
|
|
118
123
|
|
|
119
124
|
await startInteractiveTable(rows, columns, heading, totalResults, getCveDetail)
|
package/src/commands/welcome.js
CHANGED
package/src/commands/whoami.js
CHANGED
|
@@ -1,66 +1,62 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command} 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 {
|
|
4
|
+
import {createOctokit} from '../services/github.js'
|
|
5
|
+
import {checkAWSAuth} from '../services/auth.js'
|
|
6
|
+
import {getCurrentVersion} from '../services/version-check.js'
|
|
7
|
+
import {CONFIG_PATH, loadConfig} from '../services/config.js'
|
|
8
|
+
import {getUser, isAuthenticated} from '../services/clickup.js'
|
|
9
9
|
|
|
10
10
|
export default class Whoami extends Command {
|
|
11
11
|
static description = 'Mostra la tua identita su GitHub, AWS e ClickUp'
|
|
12
12
|
|
|
13
|
-
static examples = [
|
|
14
|
-
'<%= config.bin %> whoami',
|
|
15
|
-
'<%= config.bin %> whoami --json',
|
|
16
|
-
]
|
|
13
|
+
static examples = ['<%= config.bin %> whoami', '<%= config.bin %> whoami --json']
|
|
17
14
|
|
|
18
15
|
static enableJsonFlag = true
|
|
19
16
|
|
|
20
17
|
async run() {
|
|
21
|
-
const {
|
|
18
|
+
const {flags} = await this.parse(Whoami)
|
|
22
19
|
const isJson = flags.json
|
|
23
20
|
|
|
24
|
-
const spinner = isJson
|
|
21
|
+
const spinner = isJson
|
|
22
|
+
? null
|
|
23
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching identity...')}).start()
|
|
25
24
|
|
|
26
25
|
const [ghResult, awsResult, version, cuResult] = await Promise.allSettled([
|
|
27
26
|
(async () => {
|
|
28
27
|
const octokit = await createOctokit()
|
|
29
|
-
const {
|
|
30
|
-
return {
|
|
28
|
+
const {data: user} = await octokit.rest.users.getAuthenticated()
|
|
29
|
+
return {username: user.login, name: user.name ?? '', org: '', teams: []}
|
|
31
30
|
})(),
|
|
32
31
|
checkAWSAuth(),
|
|
33
32
|
getCurrentVersion(),
|
|
34
33
|
(async () => {
|
|
35
34
|
if (!(await isAuthenticated())) return null
|
|
36
35
|
const [user, config] = await Promise.all([getUser(), loadConfig()])
|
|
37
|
-
return {
|
|
36
|
+
return {username: user.username, teamName: config.clickup?.teamName ?? null}
|
|
38
37
|
})(),
|
|
39
38
|
])
|
|
40
39
|
|
|
41
40
|
spinner?.stop()
|
|
42
41
|
|
|
43
|
-
const github =
|
|
44
|
-
ghResult.status === 'fulfilled'
|
|
45
|
-
? ghResult.value
|
|
46
|
-
: { username: null, error: '[NOT AUTHENTICATED]' }
|
|
42
|
+
const github = ghResult.status === 'fulfilled' ? ghResult.value : {username: null, error: '[NOT AUTHENTICATED]'}
|
|
47
43
|
|
|
48
44
|
const aws =
|
|
49
45
|
awsResult.status === 'fulfilled' && awsResult.value.authenticated
|
|
50
|
-
? {
|
|
51
|
-
: {
|
|
46
|
+
? {accountId: awsResult.value.account, role: awsResult.value.role}
|
|
47
|
+
: {accountId: null, error: '[NOT AUTHENTICATED]'}
|
|
52
48
|
|
|
53
49
|
const clickup =
|
|
54
50
|
cuResult.status === 'fulfilled' && cuResult.value
|
|
55
51
|
? cuResult.value
|
|
56
|
-
: {
|
|
52
|
+
: {username: null, teamName: null, error: '[NOT AUTHENTICATED]'}
|
|
57
53
|
|
|
58
54
|
const cli = {
|
|
59
55
|
version: version.status === 'fulfilled' ? version.value : '?',
|
|
60
56
|
configPath: CONFIG_PATH,
|
|
61
57
|
}
|
|
62
58
|
|
|
63
|
-
const result = {
|
|
59
|
+
const result = {github, aws, clickup, cli}
|
|
64
60
|
|
|
65
61
|
if (isJson) return result
|
|
66
62
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
/** @import { DetectedEnvironment, CategoryEntry } from '../types.js' */
|
|
4
|
+
|
|
5
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Internal helpers
|
|
7
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pad a string to a fixed width, truncating with '…' if needed.
|
|
11
|
+
* @param {string} str
|
|
12
|
+
* @param {number} width
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function padCell(str, width) {
|
|
16
|
+
if (!str) str = ''
|
|
17
|
+
if (str.length > width) return str.slice(0, width - 1) + '…'
|
|
18
|
+
return str.padEnd(width)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Environments table formatter
|
|
23
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format a list of detected environments as a table string for display in the TUI.
|
|
27
|
+
* Columns: Environment (name), Status, Scope, MCPs, Commands, Skills, Agents
|
|
28
|
+
* @param {DetectedEnvironment[]} detectedEnvs
|
|
29
|
+
* @param {number} [termCols]
|
|
30
|
+
* @returns {string[]} Array of formatted lines (no ANSI clear/home)
|
|
31
|
+
*/
|
|
32
|
+
export function formatEnvironmentsTable(detectedEnvs, termCols = 120) {
|
|
33
|
+
const COL_ENV = 22
|
|
34
|
+
const COL_STATUS = 24
|
|
35
|
+
const COL_SCOPE = 8
|
|
36
|
+
const COL_COUNT = 9
|
|
37
|
+
|
|
38
|
+
const headerParts = [
|
|
39
|
+
chalk.bold.white(padCell('Environment', COL_ENV)),
|
|
40
|
+
chalk.bold.white(padCell('Status', COL_STATUS)),
|
|
41
|
+
chalk.bold.white(padCell('Scope', COL_SCOPE)),
|
|
42
|
+
chalk.bold.white(padCell('MCPs', COL_COUNT)),
|
|
43
|
+
chalk.bold.white(padCell('Commands', COL_COUNT)),
|
|
44
|
+
chalk.bold.white(padCell('Skills', COL_COUNT)),
|
|
45
|
+
chalk.bold.white(padCell('Agents', COL_COUNT)),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const dividerWidth = COL_ENV + COL_STATUS + COL_SCOPE + COL_COUNT * 4 + 6 * 2
|
|
49
|
+
const lines = []
|
|
50
|
+
lines.push(headerParts.join(' '))
|
|
51
|
+
lines.push(chalk.dim('─'.repeat(Math.min(termCols, dividerWidth))))
|
|
52
|
+
|
|
53
|
+
for (const env of detectedEnvs) {
|
|
54
|
+
const hasUnreadable = env.unreadable.length > 0
|
|
55
|
+
const statusText = hasUnreadable ? 'Detected (unreadable)' : 'Detected'
|
|
56
|
+
const statusStr = hasUnreadable
|
|
57
|
+
? chalk.yellow(padCell(statusText, COL_STATUS))
|
|
58
|
+
: chalk.green(padCell(statusText, COL_STATUS))
|
|
59
|
+
const scopeStr = padCell(env.scope ?? 'project', COL_SCOPE)
|
|
60
|
+
|
|
61
|
+
const mcpStr = padCell(String(env.counts.mcp), COL_COUNT)
|
|
62
|
+
const cmdStr = padCell(String(env.counts.command), COL_COUNT)
|
|
63
|
+
const skillStr = env.supportedCategories.includes('skill')
|
|
64
|
+
? padCell(String(env.counts.skill), COL_COUNT)
|
|
65
|
+
: padCell('—', COL_COUNT)
|
|
66
|
+
const agentStr = env.supportedCategories.includes('agent')
|
|
67
|
+
? padCell(String(env.counts.agent), COL_COUNT)
|
|
68
|
+
: padCell('—', COL_COUNT)
|
|
69
|
+
|
|
70
|
+
lines.push([padCell(env.name, COL_ENV), statusStr, scopeStr, mcpStr, cmdStr, skillStr, agentStr].join(' '))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Categories table formatter
|
|
78
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** @type {Record<string, string>} */
|
|
81
|
+
const ENV_SHORT_NAMES = {
|
|
82
|
+
'vscode-copilot': 'VSCode',
|
|
83
|
+
'claude-code': 'Claude',
|
|
84
|
+
opencode: 'OpenCode',
|
|
85
|
+
'gemini-cli': 'Gemini',
|
|
86
|
+
'copilot-cli': 'Copilot',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format a list of category entries as a table string for display in the TUI.
|
|
91
|
+
* Columns: Name, Type, Status, Environments
|
|
92
|
+
* @param {CategoryEntry[]} entries
|
|
93
|
+
* @param {number} [termCols]
|
|
94
|
+
* @returns {string[]} Array of formatted lines (no ANSI clear/home)
|
|
95
|
+
*/
|
|
96
|
+
export function formatCategoriesTable(entries, termCols = 120) {
|
|
97
|
+
const COL_NAME = 24
|
|
98
|
+
const COL_TYPE = 9
|
|
99
|
+
const COL_STATUS = 10
|
|
100
|
+
const COL_ENVS = 36
|
|
101
|
+
|
|
102
|
+
const headerParts = [
|
|
103
|
+
chalk.bold.white(padCell('Name', COL_NAME)),
|
|
104
|
+
chalk.bold.white(padCell('Type', COL_TYPE)),
|
|
105
|
+
chalk.bold.white(padCell('Status', COL_STATUS)),
|
|
106
|
+
chalk.bold.white(padCell('Environments', COL_ENVS)),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
const dividerWidth = COL_NAME + COL_TYPE + COL_STATUS + COL_ENVS + 3 * 2
|
|
110
|
+
const lines = []
|
|
111
|
+
lines.push(headerParts.join(' '))
|
|
112
|
+
lines.push(chalk.dim('─'.repeat(Math.min(termCols, dividerWidth))))
|
|
113
|
+
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const statusStr = entry.active
|
|
116
|
+
? chalk.green(padCell('Active', COL_STATUS))
|
|
117
|
+
: chalk.dim(padCell('Inactive', COL_STATUS))
|
|
118
|
+
|
|
119
|
+
const envNames = entry.environments.map((id) => ENV_SHORT_NAMES[id] ?? id).join(', ')
|
|
120
|
+
|
|
121
|
+
lines.push(
|
|
122
|
+
[padCell(entry.name, COL_NAME), padCell(entry.type, COL_TYPE), statusStr, padCell(envNames, COL_ENVS)].join(' '),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines
|
|
127
|
+
}
|
package/src/formatters/charts.js
CHANGED
|
@@ -3,16 +3,7 @@ import chalk from 'chalk'
|
|
|
3
3
|
/** @import { ChartSeries } from '../types.js' */
|
|
4
4
|
|
|
5
5
|
// Colour palette for multi-series charts (cycles if more than 8 series)
|
|
6
|
-
const PALETTE = [
|
|
7
|
-
chalk.cyan,
|
|
8
|
-
chalk.yellow,
|
|
9
|
-
chalk.green,
|
|
10
|
-
chalk.magenta,
|
|
11
|
-
chalk.blue,
|
|
12
|
-
chalk.red,
|
|
13
|
-
chalk.white,
|
|
14
|
-
chalk.gray,
|
|
15
|
-
]
|
|
6
|
+
const PALETTE = [chalk.cyan, chalk.yellow, chalk.green, chalk.magenta, chalk.blue, chalk.red, chalk.white, chalk.gray]
|
|
16
7
|
|
|
17
8
|
/**
|
|
18
9
|
* Get the terminal width, falling back to 80 columns.
|
|
@@ -54,9 +45,7 @@ export function barChart(series, options = {}) {
|
|
|
54
45
|
|
|
55
46
|
// Combine all series into per-label totals for scaling
|
|
56
47
|
const allLabels = series[0]?.labels ?? []
|
|
57
|
-
const totals = allLabels.map((_, i) =>
|
|
58
|
-
series.reduce((sum, s) => sum + (s.values[i] ?? 0), 0),
|
|
59
|
-
)
|
|
48
|
+
const totals = allLabels.map((_, i) => series.reduce((sum, s) => sum + (s.values[i] ?? 0), 0))
|
|
60
49
|
const maxTotal = Math.max(...totals, 0)
|
|
61
50
|
|
|
62
51
|
const lines = []
|
|
@@ -71,7 +60,7 @@ export function barChart(series, options = {}) {
|
|
|
71
60
|
|
|
72
61
|
// Build chart column by column (one char per day)
|
|
73
62
|
// We render it as a 2D grid: rows = height levels, cols = days
|
|
74
|
-
const grid = Array.from({
|
|
63
|
+
const grid = Array.from({length: BAR_HEIGHT}, () => Array(allLabels.length).fill(' '))
|
|
75
64
|
|
|
76
65
|
for (let col = 0; col < allLabels.length; col++) {
|
|
77
66
|
const total = totals[col]
|
|
@@ -110,9 +99,7 @@ export function barChart(series, options = {}) {
|
|
|
110
99
|
|
|
111
100
|
// X-axis date labels (sample every ~10 positions)
|
|
112
101
|
const step = Math.max(1, Math.ceil(allLabels.length / Math.floor(chartWidth / 10)))
|
|
113
|
-
const xLabels = allLabels
|
|
114
|
-
.filter((_, i) => i % step === 0)
|
|
115
|
-
.map((l) => l.slice(5)) // "MM-DD"
|
|
102
|
+
const xLabels = allLabels.filter((_, i) => i % step === 0).map((l) => l.slice(5)) // "MM-DD"
|
|
116
103
|
lines.push(' '.repeat(labelColWidth + 1) + xLabels.join(' '))
|
|
117
104
|
|
|
118
105
|
// Legend for multi-series
|
|
@@ -156,9 +143,7 @@ export function lineChart(series, options = {}) {
|
|
|
156
143
|
}
|
|
157
144
|
|
|
158
145
|
// Build a 2D canvas: rows = chartHeight, cols = chartWidth
|
|
159
|
-
const canvas = Array.from({
|
|
160
|
-
Array(chartWidth).fill(' '),
|
|
161
|
-
)
|
|
146
|
+
const canvas = Array.from({length: chartHeight}, () => Array(chartWidth).fill(' '))
|
|
162
147
|
|
|
163
148
|
const step = Math.max(1, Math.ceil(allLabels.length / chartWidth))
|
|
164
149
|
|
|
@@ -186,9 +171,7 @@ export function lineChart(series, options = {}) {
|
|
|
186
171
|
|
|
187
172
|
// X-axis date labels
|
|
188
173
|
const xStep = Math.max(1, Math.ceil(allLabels.length / Math.floor(chartWidth / 10)))
|
|
189
|
-
const xLabels = allLabels
|
|
190
|
-
.filter((_, i) => i % xStep === 0)
|
|
191
|
-
.map((l) => l.slice(5))
|
|
174
|
+
const xLabels = allLabels.filter((_, i) => i % xStep === 0).map((l) => l.slice(5))
|
|
192
175
|
lines.push(' '.repeat(labelColWidth + 1) + xLabels.join(' '))
|
|
193
176
|
|
|
194
177
|
// Legend
|
package/src/formatters/cost.js
CHANGED
|
@@ -57,11 +57,5 @@ export function formatCostTable(entries, label, groupBy = 'service') {
|
|
|
57
57
|
.map((e) => ` ${rowLabel(e, groupBy).padEnd(40)} ${formatCurrency(e.amount)}`)
|
|
58
58
|
.join('\n')
|
|
59
59
|
const divider = '─'.repeat(50)
|
|
60
|
-
return [
|
|
61
|
-
`Costs for: ${label}`,
|
|
62
|
-
divider,
|
|
63
|
-
rows,
|
|
64
|
-
divider,
|
|
65
|
-
` ${'Total'.padEnd(40)} ${formatCurrency(total)}`,
|
|
66
|
-
].join('\n')
|
|
60
|
+
return [`Costs for: ${label}`, divider, rows, divider, ` ${'Total'.padEnd(40)} ${formatCurrency(total)}`].join('\n')
|
|
67
61
|
}
|
|
@@ -21,7 +21,9 @@ export function formatDotfilesSetup(result) {
|
|
|
21
21
|
BORDER,
|
|
22
22
|
'',
|
|
23
23
|
chalk.bold(` Platform: ${chalk.cyan(result.platform)}`),
|
|
24
|
-
chalk.bold(
|
|
24
|
+
chalk.bold(
|
|
25
|
+
` Status: ${result.status === 'success' ? chalk.green('success') : result.status === 'skipped' ? chalk.dim('skipped') : chalk.red('failed')}`,
|
|
26
|
+
),
|
|
25
27
|
]
|
|
26
28
|
|
|
27
29
|
if (result.sourceDir) {
|
|
@@ -79,11 +81,41 @@ export function formatDotfilesSummary(summary) {
|
|
|
79
81
|
*/
|
|
80
82
|
function inferCategory(filePath) {
|
|
81
83
|
const lower = filePath.toLowerCase()
|
|
82
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
lower.includes('.ssh') ||
|
|
86
|
+
lower.includes('.gnupg') ||
|
|
87
|
+
lower.includes('gpg') ||
|
|
88
|
+
lower.includes('secret') ||
|
|
89
|
+
lower.includes('credential') ||
|
|
90
|
+
lower.includes('token') ||
|
|
91
|
+
lower.includes('password')
|
|
92
|
+
)
|
|
93
|
+
return 'Security'
|
|
83
94
|
if (lower.includes('.gitconfig') || lower.includes('.gitignore') || lower.includes('.git')) return 'Git'
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
if (
|
|
96
|
+
lower.includes('zshrc') ||
|
|
97
|
+
lower.includes('bashrc') ||
|
|
98
|
+
lower.includes('bash_profile') ||
|
|
99
|
+
lower.includes('zprofile') ||
|
|
100
|
+
lower.includes('fish')
|
|
101
|
+
)
|
|
102
|
+
return 'Shell'
|
|
103
|
+
if (
|
|
104
|
+
lower.includes('vim') ||
|
|
105
|
+
lower.includes('nvim') ||
|
|
106
|
+
lower.includes('emacs') ||
|
|
107
|
+
lower.includes('vscode') ||
|
|
108
|
+
lower.includes('cursor')
|
|
109
|
+
)
|
|
110
|
+
return 'Editor'
|
|
111
|
+
if (
|
|
112
|
+
lower.includes('brew') ||
|
|
113
|
+
lower.includes('npm') ||
|
|
114
|
+
lower.includes('yarn') ||
|
|
115
|
+
lower.includes('pip') ||
|
|
116
|
+
lower.includes('gem')
|
|
117
|
+
)
|
|
118
|
+
return 'Package'
|
|
87
119
|
return 'Other'
|
|
88
120
|
}
|
|
89
121
|
|
|
@@ -167,13 +199,7 @@ export function formatDotfilesStatus(result) {
|
|
|
167
199
|
* @returns {string}
|
|
168
200
|
*/
|
|
169
201
|
export function formatDotfilesAdd(result) {
|
|
170
|
-
const lines = [
|
|
171
|
-
'',
|
|
172
|
-
BORDER,
|
|
173
|
-
chalk.bold(' Dotfiles Add — Summary'),
|
|
174
|
-
BORDER,
|
|
175
|
-
'',
|
|
176
|
-
]
|
|
202
|
+
const lines = ['', BORDER, chalk.bold(' Dotfiles Add — Summary'), BORDER, '']
|
|
177
203
|
|
|
178
204
|
if (result.added.length > 0) {
|
|
179
205
|
lines.push(chalk.bold(` Added (${result.added.length}):`))
|
|
@@ -219,12 +245,13 @@ export function formatDotfilesAdd(result) {
|
|
|
219
245
|
* @returns {string}
|
|
220
246
|
*/
|
|
221
247
|
export function formatDotfilesSync(result) {
|
|
222
|
-
const actionLabel =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
const actionLabel =
|
|
249
|
+
{
|
|
250
|
+
push: 'Push',
|
|
251
|
+
pull: 'Pull',
|
|
252
|
+
'init-remote': 'Remote Setup',
|
|
253
|
+
skipped: 'Skipped',
|
|
254
|
+
}[result.action] ?? result.action
|
|
228
255
|
|
|
229
256
|
const lines = [
|
|
230
257
|
'',
|
|
@@ -233,7 +260,9 @@ export function formatDotfilesSync(result) {
|
|
|
233
260
|
BORDER,
|
|
234
261
|
'',
|
|
235
262
|
chalk.white(` Action: ${chalk.cyan(actionLabel)}`),
|
|
236
|
-
chalk.white(
|
|
263
|
+
chalk.white(
|
|
264
|
+
` Status: ${result.status === 'success' ? chalk.green('success') : result.status === 'skipped' ? chalk.dim('skipped') : chalk.red('failed')}`,
|
|
265
|
+
),
|
|
237
266
|
]
|
|
238
267
|
|
|
239
268
|
if (result.repo) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {marked} from 'marked'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import {
|
|
3
|
+
import {deflate} from 'pako'
|
|
4
4
|
|
|
5
5
|
// Custom terminal renderer — outputs ANSI-formatted text using chalk.
|
|
6
6
|
// marked-terminal@7 is incompatible with all currently released versions of marked
|
|
@@ -30,7 +30,12 @@ const terminalRenderer = {
|
|
|
30
30
|
return '\n' + lines.join('\n') + '\n\n'
|
|
31
31
|
},
|
|
32
32
|
blockquote(quote) {
|
|
33
|
-
return
|
|
33
|
+
return (
|
|
34
|
+
quote
|
|
35
|
+
.split('\n')
|
|
36
|
+
.map((l) => chalk.dim('│ ') + chalk.italic(l))
|
|
37
|
+
.join('\n') + '\n'
|
|
38
|
+
)
|
|
34
39
|
},
|
|
35
40
|
link(href, _title, text) {
|
|
36
41
|
return `${text} ${chalk.dim(`(${href})`)}`
|
|
@@ -61,7 +66,7 @@ const terminalRenderer = {
|
|
|
61
66
|
},
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
marked.use({
|
|
69
|
+
marked.use({renderer: terminalRenderer})
|
|
65
70
|
|
|
66
71
|
/**
|
|
67
72
|
* Render a markdown string as ANSI-formatted terminal output.
|
|
@@ -95,14 +100,14 @@ export function extractMermaidBlocks(content) {
|
|
|
95
100
|
export function toMermaidLiveUrl(diagramCode) {
|
|
96
101
|
const state = JSON.stringify({
|
|
97
102
|
code: diagramCode,
|
|
98
|
-
mermaid: JSON.stringify({
|
|
103
|
+
mermaid: JSON.stringify({theme: 'default'}),
|
|
99
104
|
updateDiagram: true,
|
|
100
105
|
grid: true,
|
|
101
106
|
panZoom: true,
|
|
102
107
|
rough: false,
|
|
103
108
|
})
|
|
104
109
|
const data = new TextEncoder().encode(state)
|
|
105
|
-
const compressed = deflate(data, {
|
|
110
|
+
const compressed = deflate(data, {level: 9})
|
|
106
111
|
const encoded = Buffer.from(compressed).toString('base64url')
|
|
107
112
|
return `https://mermaid.live/view#pako:${encoded}`
|
|
108
113
|
}
|