devvami 1.4.2 → 1.5.1
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 +72 -0
- package/oclif.manifest.json +275 -235
- 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 +257 -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 +215 -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 +349 -0
- package/src/services/ai-env-deployer.js +650 -0
- package/src/services/ai-env-scanner.js +983 -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 +117 -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 +1184 -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 +1089 -0
- package/src/utils/typewriter.js +3 -3
- package/src/utils/welcome.js +18 -21
- package/src/validators/repo-name.js +2 -2
package/src/services/docs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import {createOctokit} from './github.js'
|
|
2
|
+
import {exec} from './shell.js'
|
|
3
|
+
import {isOpenApi, isAsyncApi} from '../formatters/openapi.js'
|
|
4
|
+
import {load} from 'js-yaml'
|
|
5
5
|
|
|
6
6
|
/** @import { DocumentEntry, RepoDocsIndex, SearchMatch, DetectedRepo } from '../types.js' */
|
|
7
7
|
|
|
@@ -18,7 +18,7 @@ export async function detectCurrentRepo() {
|
|
|
18
18
|
if (!match) {
|
|
19
19
|
throw new Error('Could not detect GitHub repository from git remote. Use --repo to specify a repository.')
|
|
20
20
|
}
|
|
21
|
-
return {
|
|
21
|
+
return {owner: match[1], repo: match[2]}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -27,22 +27,22 @@ export async function detectCurrentRepo() {
|
|
|
27
27
|
* @returns {DocumentEntry|null}
|
|
28
28
|
*/
|
|
29
29
|
function classifyEntry(entry) {
|
|
30
|
-
const {
|
|
30
|
+
const {size} = entry
|
|
31
31
|
const path = entry.path
|
|
32
32
|
if (size === 0) return null
|
|
33
33
|
const name = path.split('/').pop() ?? path
|
|
34
34
|
|
|
35
35
|
if (/^readme\.(md|rst|txt)$/i.test(path)) {
|
|
36
|
-
return {
|
|
36
|
+
return {name, path, type: 'readme', size}
|
|
37
37
|
}
|
|
38
38
|
if (/(openapi|swagger)\.(ya?ml|json)$/i.test(path)) {
|
|
39
|
-
return {
|
|
39
|
+
return {name, path, type: 'swagger', size}
|
|
40
40
|
}
|
|
41
41
|
if (/asyncapi\.(ya?ml|json)$/i.test(path)) {
|
|
42
|
-
return {
|
|
42
|
+
return {name, path, type: 'asyncapi', size}
|
|
43
43
|
}
|
|
44
44
|
if (path.startsWith('docs/') && /\.(md|rst|txt)$/.test(path)) {
|
|
45
|
-
return {
|
|
45
|
+
return {name, path, type: 'doc', size}
|
|
46
46
|
}
|
|
47
47
|
return null
|
|
48
48
|
}
|
|
@@ -54,7 +54,7 @@ function classifyEntry(entry) {
|
|
|
54
54
|
* @returns {number}
|
|
55
55
|
*/
|
|
56
56
|
function sortEntries(a, b) {
|
|
57
|
-
const order = {
|
|
57
|
+
const order = {readme: 0, swagger: 1, asyncapi: 2, doc: 3}
|
|
58
58
|
const diff = order[a.type] - order[b.type]
|
|
59
59
|
return diff !== 0 ? diff : a.path.localeCompare(b.path)
|
|
60
60
|
}
|
|
@@ -69,18 +69,18 @@ export async function listDocs(owner, repo) {
|
|
|
69
69
|
const octokit = await createOctokit()
|
|
70
70
|
|
|
71
71
|
// 1. Get default branch
|
|
72
|
-
const {
|
|
72
|
+
const {data: repoData} = await octokit.rest.repos.get({owner, repo})
|
|
73
73
|
const defaultBranch = repoData.default_branch
|
|
74
74
|
|
|
75
75
|
// 2. Get HEAD SHA
|
|
76
|
-
const {
|
|
76
|
+
const {data: ref} = await octokit.rest.git.getRef({
|
|
77
77
|
owner,
|
|
78
78
|
repo,
|
|
79
79
|
ref: `heads/${defaultBranch}`,
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
// 3. Fetch full recursive tree
|
|
83
|
-
const {
|
|
83
|
+
const {data: tree} = await octokit.rest.git.getTree({
|
|
84
84
|
owner,
|
|
85
85
|
repo,
|
|
86
86
|
tree_sha: ref.object.sha,
|
|
@@ -91,7 +91,7 @@ export async function listDocs(owner, repo) {
|
|
|
91
91
|
const entries = []
|
|
92
92
|
for (const e of tree.tree) {
|
|
93
93
|
if (e.type !== 'blob') continue
|
|
94
|
-
const entry = classifyEntry({
|
|
94
|
+
const entry = classifyEntry({path: e.path ?? '', size: e.size ?? 0})
|
|
95
95
|
if (entry) entries.push(entry)
|
|
96
96
|
}
|
|
97
97
|
return entries.sort(sortEntries)
|
|
@@ -106,7 +106,7 @@ export async function listDocs(owner, repo) {
|
|
|
106
106
|
*/
|
|
107
107
|
export async function readFile(owner, repo, path) {
|
|
108
108
|
const octokit = await createOctokit()
|
|
109
|
-
const {
|
|
109
|
+
const {data} = await octokit.rest.repos.getContent({owner, repo, path})
|
|
110
110
|
if (Array.isArray(data) || data.type !== 'file') {
|
|
111
111
|
throw new Error(`"${path}" is not a file.`)
|
|
112
112
|
}
|
|
@@ -198,9 +198,7 @@ export function detectApiSpecType(path, content) {
|
|
|
198
198
|
if (/asyncapi\.(ya?ml|json)$/i.test(path)) return 'asyncapi'
|
|
199
199
|
// Try to detect from content
|
|
200
200
|
try {
|
|
201
|
-
const doc = /^\s*\{/.test(content.trim())
|
|
202
|
-
? JSON.parse(content)
|
|
203
|
-
: load(content)
|
|
201
|
+
const doc = /^\s*\{/.test(content.trim()) ? JSON.parse(content) : load(content)
|
|
204
202
|
if (doc && typeof doc === 'object') {
|
|
205
203
|
if (isOpenApi(/** @type {Record<string, unknown>} */ (doc))) return 'swagger'
|
|
206
204
|
if (isAsyncApi(/** @type {Record<string, unknown>} */ (doc))) return 'asyncapi'
|
|
@@ -208,7 +206,8 @@ export function detectApiSpecType(path, content) {
|
|
|
208
206
|
} catch (err) {
|
|
209
207
|
// File content is not valid YAML/JSON — not an API spec, return null.
|
|
210
208
|
// Log at debug level for troubleshooting without exposing parse errors to users.
|
|
211
|
-
if (process.env.DVMI_DEBUG)
|
|
209
|
+
if (process.env.DVMI_DEBUG)
|
|
210
|
+
process.stderr.write(`[detectApiSpecType] parse failed: ${/** @type {Error} */ (err).message}\n`)
|
|
212
211
|
}
|
|
213
212
|
return null
|
|
214
213
|
}
|
package/src/services/dotfiles.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import {homedir} from 'node:os'
|
|
2
|
+
import {existsSync} from 'node:fs'
|
|
3
|
+
import {join} from 'node:path'
|
|
4
|
+
import {which, exec, execOrThrow} from './shell.js'
|
|
5
|
+
import {loadConfig, saveConfig} from './config.js'
|
|
6
6
|
|
|
7
7
|
/** @import { Platform, DotfileEntry, DotfileRecommendation, DotfilesSetupResult, DotfilesAddResult, SetupStep, StepResult, CLIConfig } from '../types.js' */
|
|
8
8
|
|
|
@@ -31,26 +31,116 @@ export const SENSITIVE_PATTERNS = [
|
|
|
31
31
|
*/
|
|
32
32
|
export const DEFAULT_FILE_LIST = [
|
|
33
33
|
// Shell
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
{
|
|
35
|
+
path: '~/.zshrc',
|
|
36
|
+
category: 'shell',
|
|
37
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
38
|
+
autoEncrypt: false,
|
|
39
|
+
description: 'Zsh configuration',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
path: '~/.bashrc',
|
|
43
|
+
category: 'shell',
|
|
44
|
+
platforms: ['linux', 'wsl2'],
|
|
45
|
+
autoEncrypt: false,
|
|
46
|
+
description: 'Bash configuration',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: '~/.bash_profile',
|
|
50
|
+
category: 'shell',
|
|
51
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
52
|
+
autoEncrypt: false,
|
|
53
|
+
description: 'Bash profile',
|
|
54
|
+
},
|
|
55
|
+
{path: '~/.zprofile', category: 'shell', platforms: ['macos'], autoEncrypt: false, description: 'Zsh login profile'},
|
|
56
|
+
{
|
|
57
|
+
path: '~/.config/fish/config.fish',
|
|
58
|
+
category: 'shell',
|
|
59
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
60
|
+
autoEncrypt: false,
|
|
61
|
+
description: 'Fish shell configuration',
|
|
62
|
+
},
|
|
39
63
|
// Git
|
|
40
|
-
{
|
|
41
|
-
|
|
64
|
+
{
|
|
65
|
+
path: '~/.gitconfig',
|
|
66
|
+
category: 'git',
|
|
67
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
68
|
+
autoEncrypt: false,
|
|
69
|
+
description: 'Git global config',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: '~/.gitignore_global',
|
|
73
|
+
category: 'git',
|
|
74
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
75
|
+
autoEncrypt: false,
|
|
76
|
+
description: 'Global gitignore patterns',
|
|
77
|
+
},
|
|
42
78
|
// Editor
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
79
|
+
{
|
|
80
|
+
path: '~/.vimrc',
|
|
81
|
+
category: 'editor',
|
|
82
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
83
|
+
autoEncrypt: false,
|
|
84
|
+
description: 'Vim configuration',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: '~/.config/nvim/init.vim',
|
|
88
|
+
category: 'editor',
|
|
89
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
90
|
+
autoEncrypt: false,
|
|
91
|
+
description: 'Neovim configuration',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
path: '~/.config/nvim/init.lua',
|
|
95
|
+
category: 'editor',
|
|
96
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
97
|
+
autoEncrypt: false,
|
|
98
|
+
description: 'Neovim Lua configuration',
|
|
99
|
+
},
|
|
46
100
|
// Package / macOS-specific
|
|
47
|
-
{
|
|
48
|
-
|
|
101
|
+
{
|
|
102
|
+
path: '~/.Brewfile',
|
|
103
|
+
category: 'package',
|
|
104
|
+
platforms: ['macos'],
|
|
105
|
+
autoEncrypt: false,
|
|
106
|
+
description: 'Homebrew bundle file',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: '~/.config/nvim',
|
|
110
|
+
category: 'editor',
|
|
111
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
112
|
+
autoEncrypt: false,
|
|
113
|
+
description: 'Neovim config directory',
|
|
114
|
+
},
|
|
49
115
|
// Security
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
116
|
+
{
|
|
117
|
+
path: '~/.ssh/config',
|
|
118
|
+
category: 'security',
|
|
119
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
120
|
+
autoEncrypt: true,
|
|
121
|
+
description: 'SSH client configuration (auto-encrypted)',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
path: '~/.ssh/id_ed25519',
|
|
125
|
+
category: 'security',
|
|
126
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
127
|
+
autoEncrypt: true,
|
|
128
|
+
description: 'SSH private key (auto-encrypted)',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
path: '~/.ssh/id_rsa',
|
|
132
|
+
category: 'security',
|
|
133
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
134
|
+
autoEncrypt: true,
|
|
135
|
+
description: 'SSH RSA private key (auto-encrypted)',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
path: '~/.gnupg/pubring.kbx',
|
|
139
|
+
category: 'security',
|
|
140
|
+
platforms: ['macos', 'linux', 'wsl2'],
|
|
141
|
+
autoEncrypt: true,
|
|
142
|
+
description: 'GPG public keyring (auto-encrypted)',
|
|
143
|
+
},
|
|
54
144
|
]
|
|
55
145
|
|
|
56
146
|
// ---------------------------------------------------------------------------
|
|
@@ -149,11 +239,12 @@ function globToRegex(pattern) {
|
|
|
149
239
|
const expanded = expandTilde(pattern)
|
|
150
240
|
// Split on `**` to handle double-star separately
|
|
151
241
|
const parts = expanded.split('**')
|
|
152
|
-
const escaped = parts.map(
|
|
153
|
-
part
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
242
|
+
const escaped = parts.map(
|
|
243
|
+
(part) =>
|
|
244
|
+
part
|
|
245
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars
|
|
246
|
+
.replace(/\*/g, '[^/]*') // single * → any non-separator
|
|
247
|
+
.replace(/\?/g, '[^/]'), // ? → any single non-separator char
|
|
157
248
|
)
|
|
158
249
|
const src = escaped.join('.*') // ** → match anything including /
|
|
159
250
|
return new RegExp(`^${src}$`, 'i')
|
|
@@ -246,14 +337,15 @@ export function buildSetupSteps(platform, options = {}) {
|
|
|
246
337
|
run: async () => {
|
|
247
338
|
const installed = await isChezmoiInstalled()
|
|
248
339
|
if (!installed) {
|
|
249
|
-
const hint =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
340
|
+
const hint =
|
|
341
|
+
platform === 'macos'
|
|
342
|
+
? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
|
|
343
|
+
: 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
|
|
344
|
+
return {status: 'failed', hint}
|
|
253
345
|
}
|
|
254
346
|
const result = await exec('chezmoi', ['--version'])
|
|
255
347
|
const version = (result.stdout || result.stderr).trim()
|
|
256
|
-
return {
|
|
348
|
+
return {status: 'success', message: `chezmoi ${version}`}
|
|
257
349
|
},
|
|
258
350
|
})
|
|
259
351
|
|
|
@@ -267,13 +359,13 @@ export function buildSetupSteps(platform, options = {}) {
|
|
|
267
359
|
run: async () => {
|
|
268
360
|
const config = options.existingConfig !== undefined ? options.existingConfig : await getChezmoiConfig()
|
|
269
361
|
if (!config) {
|
|
270
|
-
return {
|
|
362
|
+
return {status: 'success', message: 'No existing configuration — fresh setup'}
|
|
271
363
|
}
|
|
272
364
|
const hasEncryption = config.encryption?.tool === 'age' || !!config.age?.identity
|
|
273
365
|
if (hasEncryption) {
|
|
274
|
-
return {
|
|
366
|
+
return {status: 'skipped', message: 'Age encryption already configured'}
|
|
275
367
|
}
|
|
276
|
-
return {
|
|
368
|
+
return {status: 'success', message: 'Existing config found without encryption — will add age'}
|
|
277
369
|
},
|
|
278
370
|
})
|
|
279
371
|
|
|
@@ -287,18 +379,18 @@ export function buildSetupSteps(platform, options = {}) {
|
|
|
287
379
|
run: async () => {
|
|
288
380
|
// Skip if key already exists
|
|
289
381
|
if (existsSync(ageKeyPath)) {
|
|
290
|
-
return {
|
|
382
|
+
return {status: 'skipped', message: `Age key already exists at ${ageKeyPath}`}
|
|
291
383
|
}
|
|
292
384
|
try {
|
|
293
385
|
// chezmoi uses `age-keygen` via its own embedded command
|
|
294
386
|
await execOrThrow('chezmoi', ['age', 'keygen', '-o', ageKeyPath])
|
|
295
|
-
return {
|
|
387
|
+
return {status: 'success', message: `Age key generated at ${ageKeyPath}`}
|
|
296
388
|
} catch {
|
|
297
389
|
// Fallback: try standalone age-keygen
|
|
298
390
|
try {
|
|
299
391
|
// age-keygen writes public key to stderr, private key to file
|
|
300
392
|
await execOrThrow('age-keygen', ['-o', ageKeyPath])
|
|
301
|
-
return {
|
|
393
|
+
return {status: 'success', message: `Age key generated at ${ageKeyPath}`}
|
|
302
394
|
} catch {
|
|
303
395
|
return {
|
|
304
396
|
status: 'failed',
|
|
@@ -337,11 +429,14 @@ export function buildSetupSteps(platform, options = {}) {
|
|
|
337
429
|
.filter((l) => l !== undefined)
|
|
338
430
|
.join('\n')
|
|
339
431
|
|
|
340
|
-
const {
|
|
341
|
-
await mkdir(chezmoiConfigDir, {
|
|
432
|
+
const {writeFile, mkdir} = await import('node:fs/promises')
|
|
433
|
+
await mkdir(chezmoiConfigDir, {recursive: true})
|
|
342
434
|
await writeFile(configPath, tomlContent, 'utf8')
|
|
343
435
|
|
|
344
|
-
return {
|
|
436
|
+
return {
|
|
437
|
+
status: 'success',
|
|
438
|
+
message: `chezmoi.toml written with age encryption${publicKey ? ` (public key: ${publicKey.slice(0, 16)}...)` : ''}`,
|
|
439
|
+
}
|
|
345
440
|
} catch (err) {
|
|
346
441
|
return {
|
|
347
442
|
status: 'failed',
|
|
@@ -363,14 +458,14 @@ export function buildSetupSteps(platform, options = {}) {
|
|
|
363
458
|
await execOrThrow('chezmoi', ['init'])
|
|
364
459
|
const configResult = await getChezmoiConfig()
|
|
365
460
|
const sourceDir = configResult?.sourceDir ?? configResult?.sourcePath ?? null
|
|
366
|
-
return {
|
|
461
|
+
return {status: 'success', message: sourceDir ? `Source dir: ${sourceDir}` : 'chezmoi initialised'}
|
|
367
462
|
} catch {
|
|
368
463
|
// init may fail if already initialised — that's ok
|
|
369
464
|
const configResult = await getChezmoiConfig()
|
|
370
465
|
if (configResult) {
|
|
371
|
-
return {
|
|
466
|
+
return {status: 'skipped', message: 'chezmoi already initialised'}
|
|
372
467
|
}
|
|
373
|
-
return {
|
|
468
|
+
return {status: 'failed', hint: 'Run `chezmoi doctor` to diagnose init failure'}
|
|
374
469
|
}
|
|
375
470
|
},
|
|
376
471
|
})
|
|
@@ -385,9 +480,9 @@ export function buildSetupSteps(platform, options = {}) {
|
|
|
385
480
|
run: async () => {
|
|
386
481
|
try {
|
|
387
482
|
const config = await loadConfig()
|
|
388
|
-
config.dotfiles = {
|
|
483
|
+
config.dotfiles = {...config.dotfiles, enabled: true}
|
|
389
484
|
await saveConfig(config)
|
|
390
|
-
return {
|
|
485
|
+
return {status: 'success', message: 'dvmi config updated: dotfiles.enabled = true'}
|
|
391
486
|
} catch (err) {
|
|
392
487
|
return {
|
|
393
488
|
status: 'failed',
|
|
@@ -456,8 +551,8 @@ export async function setupChezmoiInline(platform) {
|
|
|
456
551
|
.filter((l) => l !== undefined)
|
|
457
552
|
.join('\n')
|
|
458
553
|
|
|
459
|
-
const {
|
|
460
|
-
await mkdir(chezmoiConfigDir, {
|
|
554
|
+
const {writeFile, mkdir} = await import('node:fs/promises')
|
|
555
|
+
await mkdir(chezmoiConfigDir, {recursive: true})
|
|
461
556
|
await writeFile(configPath, tomlContent, 'utf8')
|
|
462
557
|
|
|
463
558
|
// Init chezmoi
|
|
@@ -469,7 +564,7 @@ export async function setupChezmoiInline(platform) {
|
|
|
469
564
|
|
|
470
565
|
// Save dvmi config
|
|
471
566
|
const dvmiConfig = await loadConfig()
|
|
472
|
-
dvmiConfig.dotfiles = {
|
|
567
|
+
dvmiConfig.dotfiles = {...(dvmiConfig.dotfiles ?? {}), enabled: true}
|
|
473
568
|
await saveConfig(dvmiConfig)
|
|
474
569
|
|
|
475
570
|
return {
|
|
@@ -519,18 +614,21 @@ export function buildAddSteps(files, platform) {
|
|
|
519
614
|
run: async () => {
|
|
520
615
|
// V-001: file must exist
|
|
521
616
|
if (!existsSync(absPath)) {
|
|
522
|
-
return {
|
|
617
|
+
return {status: 'skipped', message: `${file.path}: file not found`}
|
|
523
618
|
}
|
|
524
619
|
// V-002: WSL2 Windows path rejection
|
|
525
620
|
if (platform === 'wsl2' && isWSLWindowsPath(absPath)) {
|
|
526
|
-
return {
|
|
621
|
+
return {
|
|
622
|
+
status: 'failed',
|
|
623
|
+
hint: `${file.path}: Windows filesystem paths not supported on WSL2. Use Linux-native paths (~/) instead.`,
|
|
624
|
+
}
|
|
527
625
|
}
|
|
528
626
|
try {
|
|
529
627
|
const args = ['add']
|
|
530
628
|
if (file.encrypt) args.push('--encrypt')
|
|
531
629
|
args.push(absPath)
|
|
532
630
|
await execOrThrow('chezmoi', args)
|
|
533
|
-
return {
|
|
631
|
+
return {status: 'success', message: `${file.path} added${file.encrypt ? ' (encrypted)' : ''}`}
|
|
534
632
|
} catch {
|
|
535
633
|
return {
|
|
536
634
|
status: 'failed',
|
package/src/services/github.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import {Octokit} from 'octokit'
|
|
2
|
+
import {exec} from './shell.js'
|
|
3
|
+
import {AuthError} from '../utils/errors.js'
|
|
4
4
|
|
|
5
5
|
/** @import { Template, Repository, PullRequest, PRComment, QAStep, PRDetail, PipelineRun } from '../types.js' */
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ async function getToken() {
|
|
|
21
21
|
export async function createOctokit() {
|
|
22
22
|
const token = await getToken()
|
|
23
23
|
const baseUrl = process.env.GITHUB_API_URL ?? 'https://api.github.com'
|
|
24
|
-
return new Octokit({
|
|
24
|
+
return new Octokit({auth: token, baseUrl})
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -47,9 +47,7 @@ export async function listRepos(org, filters = {}) {
|
|
|
47
47
|
isPrivate: r.private,
|
|
48
48
|
}))
|
|
49
49
|
if (filters.language) {
|
|
50
|
-
results = results.filter(
|
|
51
|
-
(r) => r.language?.toLowerCase() === filters.language?.toLowerCase(),
|
|
52
|
-
)
|
|
50
|
+
results = results.filter((r) => r.language?.toLowerCase() === filters.language?.toLowerCase())
|
|
53
51
|
}
|
|
54
52
|
if (filters.topic) {
|
|
55
53
|
results = results.filter((r) => r.topics.includes(filters.topic ?? ''))
|
|
@@ -87,7 +85,7 @@ export async function listTemplates(org) {
|
|
|
87
85
|
*/
|
|
88
86
|
export async function createFromTemplate(opts) {
|
|
89
87
|
const octokit = await createOctokit()
|
|
90
|
-
const {
|
|
88
|
+
const {data} = await octokit.rest.repos.createUsingTemplate({
|
|
91
89
|
template_owner: opts.templateOwner,
|
|
92
90
|
template_repo: opts.templateRepo,
|
|
93
91
|
name: opts.name,
|
|
@@ -96,7 +94,7 @@ export async function createFromTemplate(opts) {
|
|
|
96
94
|
private: opts.isPrivate,
|
|
97
95
|
include_all_branches: false,
|
|
98
96
|
})
|
|
99
|
-
return {
|
|
97
|
+
return {name: data.name, htmlUrl: data.html_url, cloneUrl: data.clone_url}
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
/**
|
|
@@ -113,7 +111,7 @@ export async function setBranchProtection(owner, repo) {
|
|
|
113
111
|
branch: 'main',
|
|
114
112
|
required_status_checks: null,
|
|
115
113
|
enforce_admins: false,
|
|
116
|
-
required_pull_request_reviews: {
|
|
114
|
+
required_pull_request_reviews: {required_approving_review_count: 0},
|
|
117
115
|
restrictions: null,
|
|
118
116
|
allow_force_pushes: false,
|
|
119
117
|
allow_deletions: false,
|
|
@@ -128,8 +126,8 @@ export async function setBranchProtection(owner, repo) {
|
|
|
128
126
|
*/
|
|
129
127
|
export async function enableDependabot(owner, repo) {
|
|
130
128
|
const octokit = await createOctokit()
|
|
131
|
-
await octokit.rest.repos.enableAutomatedSecurityFixes({
|
|
132
|
-
await octokit.rest.repos.enableVulnerabilityAlerts({
|
|
129
|
+
await octokit.rest.repos.enableAutomatedSecurityFixes({owner, repo})
|
|
130
|
+
await octokit.rest.repos.enableVulnerabilityAlerts({owner, repo})
|
|
133
131
|
}
|
|
134
132
|
|
|
135
133
|
/**
|
|
@@ -139,7 +137,7 @@ export async function enableDependabot(owner, repo) {
|
|
|
139
137
|
*/
|
|
140
138
|
export async function createPR(opts) {
|
|
141
139
|
const octokit = await createOctokit()
|
|
142
|
-
const {
|
|
140
|
+
const {data} = await octokit.rest.pulls.create({
|
|
143
141
|
owner: opts.owner,
|
|
144
142
|
repo: opts.repo,
|
|
145
143
|
title: opts.title,
|
|
@@ -164,7 +162,7 @@ export async function createPR(opts) {
|
|
|
164
162
|
reviewers: opts.reviewers,
|
|
165
163
|
})
|
|
166
164
|
}
|
|
167
|
-
return {
|
|
165
|
+
return {number: data.number, htmlUrl: data.html_url}
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
/**
|
|
@@ -174,7 +172,7 @@ export async function createPR(opts) {
|
|
|
174
172
|
*/
|
|
175
173
|
export async function listMyPRs(org) {
|
|
176
174
|
const octokit = await createOctokit()
|
|
177
|
-
const {
|
|
175
|
+
const {data: user} = await octokit.rest.users.getAuthenticated()
|
|
178
176
|
const login = user.login
|
|
179
177
|
|
|
180
178
|
const [authoredRes, reviewingRes] = await Promise.all([
|
|
@@ -227,9 +225,9 @@ export async function listWorkflowRuns(owner, repo, filters = {}) {
|
|
|
227
225
|
owner,
|
|
228
226
|
repo,
|
|
229
227
|
per_page: filters.limit ?? 10,
|
|
230
|
-
...(filters.branch ? {
|
|
228
|
+
...(filters.branch ? {branch: filters.branch} : {}),
|
|
231
229
|
}
|
|
232
|
-
const {
|
|
230
|
+
const {data} = await octokit.rest.actions.listWorkflowRunsForRepo(params)
|
|
233
231
|
return data.workflow_runs.map((run) => {
|
|
234
232
|
const start = new Date(run.created_at)
|
|
235
233
|
const end = run.updated_at ? new Date(run.updated_at) : new Date()
|
|
@@ -259,9 +257,9 @@ export async function listWorkflowRuns(owner, repo, filters = {}) {
|
|
|
259
257
|
export async function rerunWorkflow(owner, repo, runId, failedOnly = false) {
|
|
260
258
|
const octokit = await createOctokit()
|
|
261
259
|
if (failedOnly) {
|
|
262
|
-
await octokit.rest.actions.reRunWorkflowFailedJobs({
|
|
260
|
+
await octokit.rest.actions.reRunWorkflowFailedJobs({owner, repo, run_id: runId})
|
|
263
261
|
} else {
|
|
264
|
-
await octokit.rest.actions.reRunWorkflow({
|
|
262
|
+
await octokit.rest.actions.reRunWorkflow({owner, repo, run_id: runId})
|
|
265
263
|
}
|
|
266
264
|
}
|
|
267
265
|
|
|
@@ -277,7 +275,7 @@ export async function searchCode(org, query, opts = {}) {
|
|
|
277
275
|
let q = `${query} org:${org}`
|
|
278
276
|
if (opts.language) q += ` language:${opts.language}`
|
|
279
277
|
if (opts.repo) q += ` repo:${org}/${opts.repo}`
|
|
280
|
-
const {
|
|
278
|
+
const {data} = await octokit.rest.search.code({q, per_page: opts.limit ?? 20})
|
|
281
279
|
return data.items.map((item) => ({
|
|
282
280
|
repo: item.repository.name,
|
|
283
281
|
file: item.path,
|
|
@@ -297,7 +295,7 @@ export function extractQASteps(body) {
|
|
|
297
295
|
for (const line of body.split('\n')) {
|
|
298
296
|
const match = line.match(/^\s*-\s*\[([xX ])\]\s+(.+)/)
|
|
299
297
|
if (match) {
|
|
300
|
-
steps.push({
|
|
298
|
+
steps.push({text: match[2].trim(), checked: match[1].toLowerCase() === 'x'})
|
|
301
299
|
}
|
|
302
300
|
}
|
|
303
301
|
return steps
|
|
@@ -331,9 +329,9 @@ export async function getPRDetail(owner, repo, prNumber) {
|
|
|
331
329
|
const octokit = await createOctokit()
|
|
332
330
|
|
|
333
331
|
const [prRes, commentsRes, reviewsRes] = await Promise.all([
|
|
334
|
-
octokit.rest.pulls.get({
|
|
335
|
-
octokit.rest.issues.listComments({
|
|
336
|
-
octokit.rest.pulls.listReviews({
|
|
332
|
+
octokit.rest.pulls.get({owner, repo, pull_number: prNumber}),
|
|
333
|
+
octokit.rest.issues.listComments({owner, repo, issue_number: prNumber, per_page: 100}),
|
|
334
|
+
octokit.rest.pulls.listReviews({owner, repo, pull_number: prNumber, per_page: 100}),
|
|
337
335
|
])
|
|
338
336
|
|
|
339
337
|
const pr = prRes.data
|