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.
Files changed (96) hide show
  1. package/README.md +7 -0
  2. package/oclif.manifest.json +129 -89
  3. package/package.json +2 -1
  4. package/src/commands/auth/login.js +20 -16
  5. package/src/commands/changelog.js +12 -12
  6. package/src/commands/costs/get.js +14 -24
  7. package/src/commands/costs/trend.js +13 -24
  8. package/src/commands/create/repo.js +72 -54
  9. package/src/commands/docs/list.js +29 -25
  10. package/src/commands/docs/projects.js +58 -24
  11. package/src/commands/docs/read.js +56 -39
  12. package/src/commands/docs/search.js +37 -25
  13. package/src/commands/doctor.js +37 -35
  14. package/src/commands/dotfiles/add.js +51 -39
  15. package/src/commands/dotfiles/setup.js +62 -33
  16. package/src/commands/dotfiles/status.js +18 -18
  17. package/src/commands/dotfiles/sync.js +62 -46
  18. package/src/commands/init.js +143 -132
  19. package/src/commands/logs/index.js +10 -16
  20. package/src/commands/open.js +12 -12
  21. package/src/commands/pipeline/logs.js +8 -11
  22. package/src/commands/pipeline/rerun.js +21 -16
  23. package/src/commands/pipeline/status.js +28 -24
  24. package/src/commands/pr/create.js +40 -27
  25. package/src/commands/pr/detail.js +9 -7
  26. package/src/commands/pr/review.js +18 -19
  27. package/src/commands/pr/status.js +27 -21
  28. package/src/commands/prompts/browse.js +15 -15
  29. package/src/commands/prompts/download.js +15 -16
  30. package/src/commands/prompts/install-speckit.js +11 -12
  31. package/src/commands/prompts/list.js +12 -12
  32. package/src/commands/prompts/run.js +16 -19
  33. package/src/commands/repo/list.js +57 -41
  34. package/src/commands/search.js +20 -18
  35. package/src/commands/security/setup.js +38 -34
  36. package/src/commands/sync-config-ai/index.js +143 -0
  37. package/src/commands/tasks/assigned.js +43 -33
  38. package/src/commands/tasks/list.js +43 -33
  39. package/src/commands/tasks/today.js +32 -30
  40. package/src/commands/upgrade.js +18 -17
  41. package/src/commands/vuln/detail.js +8 -8
  42. package/src/commands/vuln/scan.js +39 -20
  43. package/src/commands/vuln/search.js +23 -18
  44. package/src/commands/welcome.js +2 -2
  45. package/src/commands/whoami.js +19 -23
  46. package/src/formatters/ai-config.js +127 -0
  47. package/src/formatters/charts.js +6 -23
  48. package/src/formatters/cost.js +1 -7
  49. package/src/formatters/dotfiles.js +48 -19
  50. package/src/formatters/markdown.js +11 -6
  51. package/src/formatters/openapi.js +7 -9
  52. package/src/formatters/prompts.js +69 -78
  53. package/src/formatters/security.js +2 -2
  54. package/src/formatters/status.js +1 -1
  55. package/src/formatters/table.js +1 -3
  56. package/src/formatters/vuln.js +33 -20
  57. package/src/help.js +162 -164
  58. package/src/hooks/init.js +1 -3
  59. package/src/hooks/postrun.js +5 -7
  60. package/src/index.js +1 -1
  61. package/src/services/ai-config-store.js +318 -0
  62. package/src/services/ai-env-deployer.js +444 -0
  63. package/src/services/ai-env-scanner.js +242 -0
  64. package/src/services/audit-detector.js +2 -2
  65. package/src/services/audit-runner.js +40 -31
  66. package/src/services/auth.js +9 -9
  67. package/src/services/awesome-copilot.js +7 -4
  68. package/src/services/aws-costs.js +22 -22
  69. package/src/services/clickup.js +26 -26
  70. package/src/services/cloudwatch-logs.js +5 -9
  71. package/src/services/config.js +13 -13
  72. package/src/services/docs.js +19 -20
  73. package/src/services/dotfiles.js +149 -51
  74. package/src/services/github.js +22 -24
  75. package/src/services/nvd.js +21 -31
  76. package/src/services/platform.js +2 -2
  77. package/src/services/prompts.js +23 -35
  78. package/src/services/security.js +135 -61
  79. package/src/services/shell.js +4 -4
  80. package/src/services/skills-sh.js +3 -9
  81. package/src/services/speckit.js +4 -7
  82. package/src/services/version-check.js +10 -10
  83. package/src/types.js +85 -0
  84. package/src/utils/aws-vault.js +18 -41
  85. package/src/utils/banner.js +5 -7
  86. package/src/utils/errors.js +42 -46
  87. package/src/utils/frontmatter.js +4 -4
  88. package/src/utils/gradient.js +18 -16
  89. package/src/utils/open-browser.js +3 -3
  90. package/src/utils/tui/form.js +1006 -0
  91. package/src/utils/tui/modal.js +15 -14
  92. package/src/utils/tui/navigable-table.js +16 -16
  93. package/src/utils/tui/tab-tui.js +800 -0
  94. package/src/utils/typewriter.js +3 -3
  95. package/src/utils/welcome.js +18 -21
  96. package/src/validators/repo-name.js +2 -2
@@ -1,5 +1,5 @@
1
- import { execa } from 'execa'
2
- import { dirname } from 'node:path'
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-') ? vuln.id
145
- : (vuln.aliases ?? []).find((a) => a.startsWith('CVE-')) ?? null
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: Array.isArray(vuln.fix_versions) && vuln.fix_versions.length > 0
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 = /** @type {any} */ (err).code === 'ENOENT'
279
- ? `"${cmd}" is not installed. Install it to scan ${ecosystem.name} dependencies.`
280
- : String(err)
281
- return { findings: [], error: errMsg }
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 { findings: [], error: `${cmd} exited with code ${result.exitCode}: ${result.stderr ?? ''}` }
287
+ return {findings: [], error: `${cmd} exited with code ${result.exitCode}: ${result.stderr ?? ''}`}
289
288
  }
290
- return { findings: [], error: null }
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 { findings: parseNpmAudit(data, ecosystem.name), error: null }
296
+ return {findings: parseNpmAudit(data, ecosystem.name), error: null}
298
297
  }
299
298
  case 'pnpm': {
300
299
  const data = JSON.parse(output)
301
- return { findings: parsePnpmAudit(data, ecosystem.name), error: null }
300
+ return {findings: parsePnpmAudit(data, ecosystem.name), error: null}
302
301
  }
303
302
  case 'yarn': {
304
- return { findings: parseYarnAudit(output, ecosystem.name), error: null }
303
+ return {findings: parseYarnAudit(output, ecosystem.name), error: null}
305
304
  }
306
305
  case 'pip': {
307
306
  const data = JSON.parse(output)
308
- return { findings: parsePipAudit(data, ecosystem.name), error: null }
307
+ return {findings: parsePipAudit(data, ecosystem.name), error: null}
309
308
  }
310
309
  case 'cargo': {
311
310
  const data = JSON.parse(output)
312
- return { findings: parseCargoAudit(data, ecosystem.name), error: null }
311
+ return {findings: parseCargoAudit(data, ecosystem.name), error: null}
313
312
  }
314
313
  case 'bundler': {
315
314
  const data = JSON.parse(output)
316
- return { findings: parseBundlerAudit(data, ecosystem.name), error: null }
315
+ return {findings: parseBundlerAudit(data, ecosystem.name), error: null}
317
316
  }
318
317
  case 'composer': {
319
318
  const data = JSON.parse(output)
320
- return { findings: parseComposerAudit(data, ecosystem.name), error: null }
319
+ return {findings: parseComposerAudit(data, ecosystem.name), error: null}
321
320
  }
322
321
  default:
323
- return { findings: [], error: `Unknown ecosystem: ${ecosystem.name}` }
322
+ return {findings: [], error: `Unknown ecosystem: ${ecosystem.name}`}
324
323
  }
325
324
  } catch (parseErr) {
326
- return { findings: [], error: `Failed to parse ${ecosystem.name} audit output: ${parseErr.message}` }
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 = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0, total: 0 }
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': summary.critical++; break
341
- case 'High': summary.high++; break
342
- case 'Medium': summary.medium++; break
343
- case 'Low': summary.low++; break
344
- default: summary.unknown++; break
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
@@ -1,5 +1,5 @@
1
- import { exec } from './shell.js'
2
- import { loadConfig } from './config.js'
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 { authenticated: false, error: result.stderr }
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 { authenticated: true, username: match?.[1] ?? 'unknown' }
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 { authenticated: false, error: result.stderr }
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 { authenticated: false, error: 'No AWS profile configured' }
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 { authenticated: false, error: result.stderr || 'Session expired' }
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 { authenticated: false, error: 'Could not parse AWS identity' }
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 { authenticated: false, error: result.stderr }
80
+ return {authenticated: false, error: result.stderr}
81
81
  }
82
82
  return checkAWSAuth()
83
83
  }
@@ -1,5 +1,5 @@
1
- import { createOctokit } from './github.js'
2
- import { DvmiError } from '../utils/errors.js'
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 = { owner: 'github', repo: 'awesome-copilot' }
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: [![alt](img)](url) → keep nothing; [![alt](img)] → keep nothing
50
- const noBadge = rawName.replace(/\[!\[.*?\]\(.*?\)\]\(.*?\)/g, '').replace(/!\[.*?\]\(.*?\)/g, '').trim()
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 { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer'
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 { start: fmt(start), end: fmt(end) }
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 { start: fmt(start), end: fmt(end) }
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 { start: fmt(start), end: fmt(now) }
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 { start: fmt(start), end: fmt(now) }
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 [{ Type: 'DIMENSION', Key: 'SERVICE' }]
65
+ return [{Type: 'DIMENSION', Key: 'SERVICE'}]
66
66
  }
67
67
  if (groupBy === 'tag') {
68
- return [{ Type: 'TAG', Key: tagKey ?? '' }]
68
+ return [{Type: 'TAG', Key: tagKey ?? ''}]
69
69
  }
70
70
  // both
71
71
  return [
72
- { Type: 'DIMENSION', Key: 'SERVICE' },
73
- { Type: 'TAG', Key: tagKey ?? '' },
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({ region: 'us-east-1' })
89
- const { start, end } = getPeriodDates(period)
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
- ? { Tags: { Key: tagEntries[0][0], Values: [tagEntries[0][1]] } }
95
+ ? {Tags: {Key: tagEntries[0][0], Values: [tagEntries[0][1]]}}
96
96
  : {
97
97
  And: tagEntries.map(([k, v]) => ({
98
- Tags: { Key: k, Values: [v] },
98
+ Tags: {Key: k, Values: [v]},
99
99
  })),
100
100
  }
101
101
 
102
102
  const command = new GetCostAndUsageCommand({
103
- TimePeriod: { Start: start, End: end },
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: { start, end },
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 { entries, period: { start, end } }
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({ region: 'us-east-1' })
147
- const { start, end } = getTwoMonthPeriod()
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: { Start: start, End: end },
155
+ TimePeriod: {Start: start, End: end},
156
156
  Granularity: 'DAILY',
157
157
  Metrics: ['UnblendedCost'],
158
158
  GroupBy: buildGroupBy(groupBy, tagKey),
159
- ...(nextPageToken ? { NextPageToken: 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]) => ({ date, amount }))
197
+ .map(([date, amount]) => ({date, amount}))
198
198
  if (points.some((p) => p.amount > 0)) {
199
- series.push({ name, points })
199
+ series.push({name, points})
200
200
  }
201
201
  }
202
202
 
@@ -1,7 +1,7 @@
1
1
  import http from 'node:http'
2
- import { randomBytes } from 'node:crypto'
3
- import { openBrowser } from '../utils/open-browser.js'
4
- import { loadConfig, saveConfig } from './config.js'
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
- try {
29
- const { default: keytar } = await import('keytar')
30
- return keytar.getPassword('devvami', TOKEN_KEY)
31
- } catch {
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
- try {
45
- const { default: keytar } = await import('keytar')
46
- await keytar.setPassword('devvami', TOKEN_KEY, token)
47
- } catch {
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
- 'Run `dvmi auth logout` after this session on shared machines.\n',
51
+ 'Run `dvmi auth logout` after this session on shared machines.\n',
52
52
  )
53
53
  const config = await loadConfig()
54
- await saveConfig({ ...config, clickup: { ...config.clickup, token } })
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: { 'Content-Type': 'application/json' },
85
- body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code }),
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
- 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.')
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: { Authorization: token },
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 { id: String(data.user.id), username: data.user.username }
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, { due_date_lt: endOfTodayMs }),
205
- getTasks(teamId, { status: 'in progress' }),
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 { valid: true, user: { id: data.user.id, username: data.user.username } }
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 { valid: false }
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) => ({ id: String(t.id), name: t.name }))
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 { startTime: now - offset, endTime: now }
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({ region })
28
+ const client = new CloudWatchLogsClient({region})
33
29
  /** @type {LogGroup[]} */
34
30
  const groups = []
35
31
 
36
- const paginator = paginateDescribeLogGroups({ client }, {})
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({ region })
58
+ const client = new CloudWatchLogsClient({region})
63
59
 
64
60
  const command = new FilterLogEventsCommand({
65
61
  logGroupName,
@@ -1,13 +1,13 @@
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'
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
- ? join(process.env.XDG_CONFIG_HOME, 'dvmi')
10
- : join(homedir(), '.config', 'dvmi')
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 { ...DEFAULTS }
29
+ if (!existsSync(configPath)) return {...DEFAULTS}
30
30
  try {
31
31
  const raw = await readFile(configPath, 'utf8')
32
- return { ...DEFAULTS, ...JSON.parse(raw) }
32
+ return {...DEFAULTS, ...JSON.parse(raw)}
33
33
  } catch {
34
- return { ...DEFAULTS }
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, { recursive: true })
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 { ...DEFAULTS }
69
+ if (!existsSync(configPath)) return {...DEFAULTS}
70
70
  try {
71
71
  const raw = readFileSync(configPath, 'utf8')
72
- return { ...DEFAULTS, ...JSON.parse(raw) }
72
+ return {...DEFAULTS, ...JSON.parse(raw)}
73
73
  } catch {
74
- return { ...DEFAULTS }
74
+ return {...DEFAULTS}
75
75
  }
76
76
  }