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,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {execa} from 'execa'
|
|
2
|
+
import {dirname} from 'node:path'
|
|
3
3
|
|
|
4
4
|
/** @import { PackageEcosystem, VulnerabilityFinding } from '../types.js' */
|
|
5
5
|
|
|
@@ -141,8 +141,9 @@ function parsePipAudit(data, ecosystem) {
|
|
|
141
141
|
if (!Array.isArray(dep.vulns) || dep.vulns.length === 0) continue
|
|
142
142
|
for (const vuln of dep.vulns) {
|
|
143
143
|
// Determine best ID: prefer CVE
|
|
144
|
-
const cveId = vuln.id?.startsWith('CVE-')
|
|
145
|
-
|
|
144
|
+
const cveId = vuln.id?.startsWith('CVE-')
|
|
145
|
+
? vuln.id
|
|
146
|
+
: ((vuln.aliases ?? []).find((a) => a.startsWith('CVE-')) ?? null)
|
|
146
147
|
|
|
147
148
|
findings.push({
|
|
148
149
|
package: dep.name,
|
|
@@ -151,9 +152,8 @@ function parsePipAudit(data, ecosystem) {
|
|
|
151
152
|
cveId,
|
|
152
153
|
advisoryUrl: null,
|
|
153
154
|
title: vuln.description ?? null,
|
|
154
|
-
patchedVersions:
|
|
155
|
-
? `>=${vuln.fix_versions[0]}`
|
|
156
|
-
: null,
|
|
155
|
+
patchedVersions:
|
|
156
|
+
Array.isArray(vuln.fix_versions) && vuln.fix_versions.length > 0 ? `>=${vuln.fix_versions[0]}` : null,
|
|
157
157
|
ecosystem,
|
|
158
158
|
isDirect: null,
|
|
159
159
|
})
|
|
@@ -177,9 +177,7 @@ function parseCargoAudit(data, ecosystem) {
|
|
|
177
177
|
const advisory = item.advisory ?? {}
|
|
178
178
|
const pkg = item.package ?? {}
|
|
179
179
|
|
|
180
|
-
const cveId = Array.isArray(advisory.aliases)
|
|
181
|
-
? (advisory.aliases.find((a) => /^CVE-/i.test(a)) ?? null)
|
|
182
|
-
: null
|
|
180
|
+
const cveId = Array.isArray(advisory.aliases) ? (advisory.aliases.find((a) => /^CVE-/i.test(a)) ?? null) : null
|
|
183
181
|
|
|
184
182
|
// CVSS vector string — extract base score from it? Too complex; mark Unknown for now
|
|
185
183
|
findings.push({
|
|
@@ -275,55 +273,56 @@ export async function runAudit(ecosystem) {
|
|
|
275
273
|
})
|
|
276
274
|
} catch (err) {
|
|
277
275
|
// Binary not found — tool not installed
|
|
278
|
-
const errMsg =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
const errMsg =
|
|
277
|
+
/** @type {any} */ (err).code === 'ENOENT'
|
|
278
|
+
? `"${cmd}" is not installed. Install it to scan ${ecosystem.name} dependencies.`
|
|
279
|
+
: String(err)
|
|
280
|
+
return {findings: [], error: errMsg}
|
|
282
281
|
}
|
|
283
282
|
|
|
284
283
|
const output = result.stdout ?? result.all ?? ''
|
|
285
284
|
|
|
286
285
|
if (!output.trim()) {
|
|
287
286
|
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
288
|
-
return {
|
|
287
|
+
return {findings: [], error: `${cmd} exited with code ${result.exitCode}: ${result.stderr ?? ''}`}
|
|
289
288
|
}
|
|
290
|
-
return {
|
|
289
|
+
return {findings: [], error: null}
|
|
291
290
|
}
|
|
292
291
|
|
|
293
292
|
try {
|
|
294
293
|
switch (ecosystem.name) {
|
|
295
294
|
case 'npm': {
|
|
296
295
|
const data = JSON.parse(output)
|
|
297
|
-
return {
|
|
296
|
+
return {findings: parseNpmAudit(data, ecosystem.name), error: null}
|
|
298
297
|
}
|
|
299
298
|
case 'pnpm': {
|
|
300
299
|
const data = JSON.parse(output)
|
|
301
|
-
return {
|
|
300
|
+
return {findings: parsePnpmAudit(data, ecosystem.name), error: null}
|
|
302
301
|
}
|
|
303
302
|
case 'yarn': {
|
|
304
|
-
return {
|
|
303
|
+
return {findings: parseYarnAudit(output, ecosystem.name), error: null}
|
|
305
304
|
}
|
|
306
305
|
case 'pip': {
|
|
307
306
|
const data = JSON.parse(output)
|
|
308
|
-
return {
|
|
307
|
+
return {findings: parsePipAudit(data, ecosystem.name), error: null}
|
|
309
308
|
}
|
|
310
309
|
case 'cargo': {
|
|
311
310
|
const data = JSON.parse(output)
|
|
312
|
-
return {
|
|
311
|
+
return {findings: parseCargoAudit(data, ecosystem.name), error: null}
|
|
313
312
|
}
|
|
314
313
|
case 'bundler': {
|
|
315
314
|
const data = JSON.parse(output)
|
|
316
|
-
return {
|
|
315
|
+
return {findings: parseBundlerAudit(data, ecosystem.name), error: null}
|
|
317
316
|
}
|
|
318
317
|
case 'composer': {
|
|
319
318
|
const data = JSON.parse(output)
|
|
320
|
-
return {
|
|
319
|
+
return {findings: parseComposerAudit(data, ecosystem.name), error: null}
|
|
321
320
|
}
|
|
322
321
|
default:
|
|
323
|
-
return {
|
|
322
|
+
return {findings: [], error: `Unknown ecosystem: ${ecosystem.name}`}
|
|
324
323
|
}
|
|
325
324
|
} catch (parseErr) {
|
|
326
|
-
return {
|
|
325
|
+
return {findings: [], error: `Failed to parse ${ecosystem.name} audit output: ${parseErr.message}`}
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
328
|
|
|
@@ -333,15 +332,25 @@ export async function runAudit(ecosystem) {
|
|
|
333
332
|
* @returns {import('../types.js').ScanSummary}
|
|
334
333
|
*/
|
|
335
334
|
export function summarizeFindings(findings) {
|
|
336
|
-
const summary = {
|
|
335
|
+
const summary = {critical: 0, high: 0, medium: 0, low: 0, unknown: 0, total: 0}
|
|
337
336
|
for (const f of findings) {
|
|
338
337
|
summary.total++
|
|
339
338
|
switch (f.severity) {
|
|
340
|
-
case 'Critical':
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
case '
|
|
344
|
-
|
|
339
|
+
case 'Critical':
|
|
340
|
+
summary.critical++
|
|
341
|
+
break
|
|
342
|
+
case 'High':
|
|
343
|
+
summary.high++
|
|
344
|
+
break
|
|
345
|
+
case 'Medium':
|
|
346
|
+
summary.medium++
|
|
347
|
+
break
|
|
348
|
+
case 'Low':
|
|
349
|
+
summary.low++
|
|
350
|
+
break
|
|
351
|
+
default:
|
|
352
|
+
summary.unknown++
|
|
353
|
+
break
|
|
345
354
|
}
|
|
346
355
|
}
|
|
347
356
|
return summary
|
package/src/services/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {exec} from './shell.js'
|
|
2
|
+
import {loadConfig} from './config.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {Object} AuthStatus
|
|
@@ -17,11 +17,11 @@ import { loadConfig } from './config.js'
|
|
|
17
17
|
export async function checkGitHubAuth() {
|
|
18
18
|
const result = await exec('gh', ['auth', 'status'])
|
|
19
19
|
if (result.exitCode !== 0) {
|
|
20
|
-
return {
|
|
20
|
+
return {authenticated: false, error: result.stderr}
|
|
21
21
|
}
|
|
22
22
|
// Extract username from output like "Logged in to github.com as username"
|
|
23
23
|
const match = result.stderr.match(/Logged in to .+ as (\S+)/)
|
|
24
|
-
return {
|
|
24
|
+
return {authenticated: true, username: match?.[1] ?? 'unknown'}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -31,7 +31,7 @@ export async function checkGitHubAuth() {
|
|
|
31
31
|
export async function loginGitHub() {
|
|
32
32
|
const result = await exec('gh', ['auth', 'login', '--web'])
|
|
33
33
|
if (result.exitCode !== 0) {
|
|
34
|
-
return {
|
|
34
|
+
return {authenticated: false, error: result.stderr}
|
|
35
35
|
}
|
|
36
36
|
return checkGitHubAuth()
|
|
37
37
|
}
|
|
@@ -42,7 +42,7 @@ export async function loginGitHub() {
|
|
|
42
42
|
*/
|
|
43
43
|
export async function checkAWSAuth() {
|
|
44
44
|
const config = await loadConfig()
|
|
45
|
-
if (!config.awsProfile) return {
|
|
45
|
+
if (!config.awsProfile) return {authenticated: false, error: 'No AWS profile configured'}
|
|
46
46
|
|
|
47
47
|
const result = await exec('aws-vault', [
|
|
48
48
|
'exec',
|
|
@@ -55,7 +55,7 @@ export async function checkAWSAuth() {
|
|
|
55
55
|
'json',
|
|
56
56
|
])
|
|
57
57
|
if (result.exitCode !== 0) {
|
|
58
|
-
return {
|
|
58
|
+
return {authenticated: false, error: result.stderr || 'Session expired'}
|
|
59
59
|
}
|
|
60
60
|
try {
|
|
61
61
|
const identity = JSON.parse(result.stdout)
|
|
@@ -65,7 +65,7 @@ export async function checkAWSAuth() {
|
|
|
65
65
|
role: identity.Arn?.split('/').at(-1),
|
|
66
66
|
}
|
|
67
67
|
} catch {
|
|
68
|
-
return {
|
|
68
|
+
return {authenticated: false, error: 'Could not parse AWS identity'}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -77,7 +77,7 @@ export async function checkAWSAuth() {
|
|
|
77
77
|
export async function loginAWS(profile) {
|
|
78
78
|
const result = await exec('aws-vault', ['login', profile])
|
|
79
79
|
if (result.exitCode !== 0) {
|
|
80
|
-
return {
|
|
80
|
+
return {authenticated: false, error: result.stderr}
|
|
81
81
|
}
|
|
82
82
|
return checkAWSAuth()
|
|
83
83
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {createOctokit} from './github.js'
|
|
2
|
+
import {DvmiError} from '../utils/errors.js'
|
|
3
3
|
|
|
4
4
|
/** @import { AwesomeEntry } from '../types.js' */
|
|
5
5
|
|
|
@@ -10,7 +10,7 @@ import { DvmiError } from '../utils/errors.js'
|
|
|
10
10
|
*/
|
|
11
11
|
export const AWESOME_CATEGORIES = ['agents', 'instructions', 'skills', 'plugins', 'hooks', 'workflows']
|
|
12
12
|
|
|
13
|
-
const AWESOME_REPO = {
|
|
13
|
+
const AWESOME_REPO = {owner: 'github', repo: 'awesome-copilot'}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Parse a GitHub-flavoured markdown table into AwesomeEntry objects.
|
|
@@ -47,7 +47,10 @@ export function parseMarkdownTable(md, category) {
|
|
|
47
47
|
if (/^[\*_]?name[\*_]?$/i.test(rawName)) continue
|
|
48
48
|
|
|
49
49
|
// Strip badge images: [](url) → keep nothing; [] → keep nothing
|
|
50
|
-
const noBadge = rawName
|
|
50
|
+
const noBadge = rawName
|
|
51
|
+
.replace(/\[!\[.*?\]\(.*?\)\]\(.*?\)/g, '')
|
|
52
|
+
.replace(/!\[.*?\]\(.*?\)/g, '')
|
|
53
|
+
.trim()
|
|
51
54
|
|
|
52
55
|
// Extract [text](url) link
|
|
53
56
|
const linkMatch = noBadge.match(/\[([^\]]+)\]\(([^)]+)\)/)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {CostExplorerClient, GetCostAndUsageCommand} from '@aws-sdk/client-cost-explorer'
|
|
2
2
|
|
|
3
3
|
/** @import { AWSCostEntry, CostGroupMode, CostTrendSeries } from '../types.js' */
|
|
4
4
|
|
|
@@ -14,18 +14,18 @@ function getPeriodDates(period) {
|
|
|
14
14
|
if (period === 'last-month') {
|
|
15
15
|
const start = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
|
16
16
|
const end = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
17
|
-
return {
|
|
17
|
+
return {start: fmt(start), end: fmt(end)}
|
|
18
18
|
}
|
|
19
19
|
if (period === 'last-week') {
|
|
20
20
|
const end = new Date(now)
|
|
21
21
|
end.setDate(now.getDate() - now.getDay())
|
|
22
22
|
const start = new Date(end)
|
|
23
23
|
start.setDate(end.getDate() - 7)
|
|
24
|
-
return {
|
|
24
|
+
return {start: fmt(start), end: fmt(end)}
|
|
25
25
|
}
|
|
26
26
|
// mtd
|
|
27
27
|
const start = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
28
|
-
return {
|
|
28
|
+
return {start: fmt(start), end: fmt(now)}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -36,7 +36,7 @@ export function getTwoMonthPeriod() {
|
|
|
36
36
|
const now = new Date()
|
|
37
37
|
const fmt = (d) => d.toISOString().split('T')[0]
|
|
38
38
|
const start = new Date(now.getFullYear(), now.getMonth() - 2, 1)
|
|
39
|
-
return {
|
|
39
|
+
return {start: fmt(start), end: fmt(now)}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -62,15 +62,15 @@ function stripTagPrefix(rawKey) {
|
|
|
62
62
|
*/
|
|
63
63
|
function buildGroupBy(groupBy, tagKey) {
|
|
64
64
|
if (groupBy === 'service') {
|
|
65
|
-
return [{
|
|
65
|
+
return [{Type: 'DIMENSION', Key: 'SERVICE'}]
|
|
66
66
|
}
|
|
67
67
|
if (groupBy === 'tag') {
|
|
68
|
-
return [{
|
|
68
|
+
return [{Type: 'TAG', Key: tagKey ?? ''}]
|
|
69
69
|
}
|
|
70
70
|
// both
|
|
71
71
|
return [
|
|
72
|
-
{
|
|
73
|
-
{
|
|
72
|
+
{Type: 'DIMENSION', Key: 'SERVICE'},
|
|
73
|
+
{Type: 'TAG', Key: tagKey ?? ''},
|
|
74
74
|
]
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -85,22 +85,22 @@ function buildGroupBy(groupBy, tagKey) {
|
|
|
85
85
|
*/
|
|
86
86
|
export async function getServiceCosts(serviceName, tags, period = 'last-month', groupBy = 'service', tagKey) {
|
|
87
87
|
// Cost Explorer always uses us-east-1
|
|
88
|
-
const client = new CostExplorerClient({
|
|
89
|
-
const {
|
|
88
|
+
const client = new CostExplorerClient({region: 'us-east-1'})
|
|
89
|
+
const {start, end} = getPeriodDates(period)
|
|
90
90
|
|
|
91
91
|
// Build tag filter from project tags
|
|
92
92
|
const tagEntries = Object.entries(tags)
|
|
93
93
|
const filter =
|
|
94
94
|
tagEntries.length === 1
|
|
95
|
-
? {
|
|
95
|
+
? {Tags: {Key: tagEntries[0][0], Values: [tagEntries[0][1]]}}
|
|
96
96
|
: {
|
|
97
97
|
And: tagEntries.map(([k, v]) => ({
|
|
98
|
-
Tags: {
|
|
98
|
+
Tags: {Key: k, Values: [v]},
|
|
99
99
|
})),
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const command = new GetCostAndUsageCommand({
|
|
103
|
-
TimePeriod: {
|
|
103
|
+
TimePeriod: {Start: start, End: end},
|
|
104
104
|
Granularity: 'MONTHLY',
|
|
105
105
|
Metrics: ['UnblendedCost'],
|
|
106
106
|
Filter: filter,
|
|
@@ -120,7 +120,7 @@ export async function getServiceCosts(serviceName, tags, period = 'last-month',
|
|
|
120
120
|
serviceName: groupBy === 'tag' ? stripTagPrefix(keys[0] ?? '') : (keys[0] ?? 'Unknown'),
|
|
121
121
|
amount,
|
|
122
122
|
unit: group.Metrics?.UnblendedCost?.Unit ?? 'USD',
|
|
123
|
-
period: {
|
|
123
|
+
period: {start, end},
|
|
124
124
|
}
|
|
125
125
|
if (groupBy === 'both') {
|
|
126
126
|
entry.tagValue = stripTagPrefix(keys[1] ?? '')
|
|
@@ -132,7 +132,7 @@ export async function getServiceCosts(serviceName, tags, period = 'last-month',
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
return {
|
|
135
|
+
return {entries, period: {start, end}}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
@@ -143,8 +143,8 @@ export async function getServiceCosts(serviceName, tags, period = 'last-month',
|
|
|
143
143
|
* @returns {Promise<CostTrendSeries[]>}
|
|
144
144
|
*/
|
|
145
145
|
export async function getTrendCosts(groupBy = 'service', tagKey) {
|
|
146
|
-
const client = new CostExplorerClient({
|
|
147
|
-
const {
|
|
146
|
+
const client = new CostExplorerClient({region: 'us-east-1'})
|
|
147
|
+
const {start, end} = getTwoMonthPeriod()
|
|
148
148
|
|
|
149
149
|
/** @type {Map<string, Map<string, number>>} seriesName → date → amount */
|
|
150
150
|
const seriesMap = new Map()
|
|
@@ -152,11 +152,11 @@ export async function getTrendCosts(groupBy = 'service', tagKey) {
|
|
|
152
152
|
let nextPageToken = undefined
|
|
153
153
|
do {
|
|
154
154
|
const command = new GetCostAndUsageCommand({
|
|
155
|
-
TimePeriod: {
|
|
155
|
+
TimePeriod: {Start: start, End: end},
|
|
156
156
|
Granularity: 'DAILY',
|
|
157
157
|
Metrics: ['UnblendedCost'],
|
|
158
158
|
GroupBy: buildGroupBy(groupBy, tagKey),
|
|
159
|
-
...(nextPageToken ? {
|
|
159
|
+
...(nextPageToken ? {NextPageToken: nextPageToken} : {}),
|
|
160
160
|
})
|
|
161
161
|
|
|
162
162
|
const result = await client.send(command)
|
|
@@ -194,9 +194,9 @@ export async function getTrendCosts(groupBy = 'service', tagKey) {
|
|
|
194
194
|
for (const [name, dateMap] of seriesMap) {
|
|
195
195
|
const points = Array.from(dateMap.entries())
|
|
196
196
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
197
|
-
.map(([date, amount]) => ({
|
|
197
|
+
.map(([date, amount]) => ({date, amount}))
|
|
198
198
|
if (points.some((p) => p.amount > 0)) {
|
|
199
|
-
series.push({
|
|
199
|
+
series.push({name, points})
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
package/src/services/clickup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import {randomBytes} from 'node:crypto'
|
|
3
|
+
import {openBrowser} from '../utils/open-browser.js'
|
|
4
|
+
import {loadConfig, saveConfig} from './config.js'
|
|
5
5
|
|
|
6
6
|
/** @import { ClickUpTask } from '../types.js' */
|
|
7
7
|
|
|
@@ -25,10 +25,10 @@ function localDateString(date) {
|
|
|
25
25
|
async function getToken() {
|
|
26
26
|
// Allow tests / CI to inject a token via environment variable
|
|
27
27
|
if (process.env.CLICKUP_TOKEN) return process.env.CLICKUP_TOKEN
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
try {
|
|
29
|
+
const {default: keytar} = await import('keytar')
|
|
30
|
+
return keytar.getPassword('devvami', TOKEN_KEY)
|
|
31
|
+
} catch {
|
|
32
32
|
// keytar not available (e.g. WSL2 without D-Bus) — fallback to config
|
|
33
33
|
const config = await loadConfig()
|
|
34
34
|
return config.clickup?.token ?? null
|
|
@@ -41,17 +41,17 @@ async function getToken() {
|
|
|
41
41
|
* @returns {Promise<void>}
|
|
42
42
|
*/
|
|
43
43
|
export async function storeToken(token) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
try {
|
|
45
|
+
const {default: keytar} = await import('keytar')
|
|
46
|
+
await keytar.setPassword('devvami', TOKEN_KEY, token)
|
|
47
|
+
} catch {
|
|
48
48
|
// Fallback: store in config (less secure)
|
|
49
49
|
process.stderr.write(
|
|
50
50
|
'Warning: keytar unavailable. ClickUp token will be stored in plaintext.\n' +
|
|
51
|
-
|
|
51
|
+
'Run `dvmi auth logout` after this session on shared machines.\n',
|
|
52
52
|
)
|
|
53
53
|
const config = await loadConfig()
|
|
54
|
-
await saveConfig({
|
|
54
|
+
await saveConfig({...config, clickup: {...config.clickup, token}})
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -81,8 +81,8 @@ export async function oauthFlow(clientId, clientSecret) {
|
|
|
81
81
|
try {
|
|
82
82
|
const resp = await fetch(`${API_BASE}/oauth/token`, {
|
|
83
83
|
method: 'POST',
|
|
84
|
-
headers: {
|
|
85
|
-
body: JSON.stringify({
|
|
84
|
+
headers: {'Content-Type': 'application/json'},
|
|
85
|
+
body: JSON.stringify({client_id: clientId, client_secret: clientSecret, code}),
|
|
86
86
|
})
|
|
87
87
|
const data = /** @type {any} */ (await resp.json())
|
|
88
88
|
await storeToken(data.access_token)
|
|
@@ -108,12 +108,12 @@ export async function oauthFlow(clientId, clientSecret) {
|
|
|
108
108
|
* @param {number} [retries]
|
|
109
109
|
* @returns {Promise<unknown>}
|
|
110
110
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
async function clickupFetch(path, retries = 0) {
|
|
112
|
+
const MAX_RETRIES = 5
|
|
113
|
+
const token = await getToken()
|
|
114
|
+
if (!token) throw new Error('ClickUp not authenticated. Run `dvmi init` to authorize.')
|
|
115
115
|
const resp = await fetch(`${API_BASE}${path}`, {
|
|
116
|
-
headers: {
|
|
116
|
+
headers: {Authorization: token},
|
|
117
117
|
})
|
|
118
118
|
if (resp.status === 429) {
|
|
119
119
|
if (retries >= MAX_RETRIES) {
|
|
@@ -136,7 +136,7 @@ export async function oauthFlow(clientId, clientSecret) {
|
|
|
136
136
|
*/
|
|
137
137
|
export async function getUser() {
|
|
138
138
|
const data = /** @type {any} */ (await clickupFetch('/user'))
|
|
139
|
-
return {
|
|
139
|
+
return {id: String(data.user.id), username: data.user.username}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
@@ -201,8 +201,8 @@ export async function getTasksToday(teamId) {
|
|
|
201
201
|
const endOfTodayMs = new Date().setHours(23, 59, 59, 999)
|
|
202
202
|
|
|
203
203
|
const [overdueTasks, inProgressTasks] = await Promise.all([
|
|
204
|
-
getTasks(teamId, {
|
|
205
|
-
getTasks(teamId, {
|
|
204
|
+
getTasks(teamId, {due_date_lt: endOfTodayMs}),
|
|
205
|
+
getTasks(teamId, {status: 'in progress'}),
|
|
206
206
|
])
|
|
207
207
|
|
|
208
208
|
// De-duplicate by task ID (a task may appear in both result sets)
|
|
@@ -288,11 +288,11 @@ export async function isAuthenticated() {
|
|
|
288
288
|
export async function validateToken() {
|
|
289
289
|
try {
|
|
290
290
|
const data = /** @type {any} */ (await clickupFetch('/user'))
|
|
291
|
-
return {
|
|
291
|
+
return {valid: true, user: {id: data.user.id, username: data.user.username}}
|
|
292
292
|
} catch (err) {
|
|
293
293
|
// 401 or no token → not valid
|
|
294
294
|
if (err instanceof Error && (err.message.includes('401') || err.message.includes('not authenticated'))) {
|
|
295
|
-
return {
|
|
295
|
+
return {valid: false}
|
|
296
296
|
}
|
|
297
297
|
throw err
|
|
298
298
|
}
|
|
@@ -304,5 +304,5 @@ export async function validateToken() {
|
|
|
304
304
|
*/
|
|
305
305
|
export async function getTeams() {
|
|
306
306
|
const data = /** @type {any} */ (await clickupFetch('/team'))
|
|
307
|
-
return (data.teams ?? []).map((t) => ({
|
|
307
|
+
return (data.teams ?? []).map((t) => ({id: String(t.id), name: t.name}))
|
|
308
308
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CloudWatchLogsClient,
|
|
3
|
-
paginateDescribeLogGroups,
|
|
4
|
-
FilterLogEventsCommand,
|
|
5
|
-
} from '@aws-sdk/client-cloudwatch-logs'
|
|
1
|
+
import {CloudWatchLogsClient, paginateDescribeLogGroups, FilterLogEventsCommand} from '@aws-sdk/client-cloudwatch-logs'
|
|
6
2
|
|
|
7
3
|
/** @import { LogGroup, LogEvent, LogFilterResult } from '../types.js' */
|
|
8
4
|
|
|
@@ -20,7 +16,7 @@ export function sinceToEpochMs(since) {
|
|
|
20
16
|
}
|
|
21
17
|
const offset = MS[since]
|
|
22
18
|
if (!offset) throw new Error(`Invalid since value: ${since}. Must be one of: 1h, 24h, 7d`)
|
|
23
|
-
return {
|
|
19
|
+
return {startTime: now - offset, endTime: now}
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
/**
|
|
@@ -29,11 +25,11 @@ export function sinceToEpochMs(since) {
|
|
|
29
25
|
* @returns {Promise<LogGroup[]>}
|
|
30
26
|
*/
|
|
31
27
|
export async function listLogGroups(region = 'eu-west-1') {
|
|
32
|
-
const client = new CloudWatchLogsClient({
|
|
28
|
+
const client = new CloudWatchLogsClient({region})
|
|
33
29
|
/** @type {LogGroup[]} */
|
|
34
30
|
const groups = []
|
|
35
31
|
|
|
36
|
-
const paginator = paginateDescribeLogGroups({
|
|
32
|
+
const paginator = paginateDescribeLogGroups({client}, {})
|
|
37
33
|
for await (const page of paginator) {
|
|
38
34
|
for (const lg of page.logGroups ?? []) {
|
|
39
35
|
groups.push({
|
|
@@ -59,7 +55,7 @@ export async function listLogGroups(region = 'eu-west-1') {
|
|
|
59
55
|
* @returns {Promise<LogFilterResult>}
|
|
60
56
|
*/
|
|
61
57
|
export async function filterLogEvents(logGroupName, filterPattern, startTime, endTime, limit, region = 'eu-west-1') {
|
|
62
|
-
const client = new CloudWatchLogsClient({
|
|
58
|
+
const client = new CloudWatchLogsClient({region})
|
|
63
59
|
|
|
64
60
|
const command = new FilterLogEventsCommand({
|
|
65
61
|
logGroupName,
|
package/src/services/config.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import {readFile, writeFile, mkdir, chmod} from 'node:fs/promises'
|
|
2
|
+
import {existsSync, readFileSync} from 'node:fs'
|
|
3
|
+
import {join} from 'node:path'
|
|
4
|
+
import {homedir} from 'node:os'
|
|
5
5
|
|
|
6
6
|
/** @import { CLIConfig } from '../types.js' */
|
|
7
7
|
|
|
8
8
|
const CONFIG_DIR = process.env.XDG_CONFIG_HOME
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
? join(process.env.XDG_CONFIG_HOME, 'dvmi')
|
|
10
|
+
: join(homedir(), '.config', 'dvmi')
|
|
11
11
|
|
|
12
12
|
export const CONFIG_PATH = join(CONFIG_DIR, 'config.json')
|
|
13
13
|
|
|
@@ -26,12 +26,12 @@ const DEFAULTS = {
|
|
|
26
26
|
* @returns {Promise<CLIConfig>}
|
|
27
27
|
*/
|
|
28
28
|
export async function loadConfig(configPath = process.env.DVMI_CONFIG_PATH ?? CONFIG_PATH) {
|
|
29
|
-
if (!existsSync(configPath)) return {
|
|
29
|
+
if (!existsSync(configPath)) return {...DEFAULTS}
|
|
30
30
|
try {
|
|
31
31
|
const raw = await readFile(configPath, 'utf8')
|
|
32
|
-
return {
|
|
32
|
+
return {...DEFAULTS, ...JSON.parse(raw)}
|
|
33
33
|
} catch {
|
|
34
|
-
return {
|
|
34
|
+
return {...DEFAULTS}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -44,7 +44,7 @@ export async function loadConfig(configPath = process.env.DVMI_CONFIG_PATH ?? CO
|
|
|
44
44
|
export async function saveConfig(config, configPath = CONFIG_PATH) {
|
|
45
45
|
const dir = configPath.replace(/\/[^/]+$/, '')
|
|
46
46
|
if (!existsSync(dir)) {
|
|
47
|
-
await mkdir(dir, {
|
|
47
|
+
await mkdir(dir, {recursive: true})
|
|
48
48
|
}
|
|
49
49
|
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8')
|
|
50
50
|
await chmod(configPath, 0o600)
|
|
@@ -66,11 +66,11 @@ export function configExists(configPath = CONFIG_PATH) {
|
|
|
66
66
|
* @returns {CLIConfig}
|
|
67
67
|
*/
|
|
68
68
|
export function loadConfigSync(configPath = process.env.DVMI_CONFIG_PATH ?? CONFIG_PATH) {
|
|
69
|
-
if (!existsSync(configPath)) return {
|
|
69
|
+
if (!existsSync(configPath)) return {...DEFAULTS}
|
|
70
70
|
try {
|
|
71
71
|
const raw = readFileSync(configPath, 'utf8')
|
|
72
|
-
return {
|
|
72
|
+
return {...DEFAULTS, ...JSON.parse(raw)}
|
|
73
73
|
} catch {
|
|
74
|
-
return {
|
|
74
|
+
return {...DEFAULTS}
|
|
75
75
|
}
|
|
76
76
|
}
|