devvami 1.3.0 → 1.4.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.
@@ -813,6 +813,216 @@
813
813
  "search.js"
814
814
  ]
815
815
  },
816
+ "dotfiles:add": {
817
+ "aliases": [],
818
+ "args": {
819
+ "files": {
820
+ "description": "File paths to add",
821
+ "name": "files",
822
+ "required": false
823
+ }
824
+ },
825
+ "description": "Add dotfiles to chezmoi management with automatic encryption for sensitive files",
826
+ "examples": [
827
+ "<%= config.bin %> dotfiles add",
828
+ "<%= config.bin %> dotfiles add ~/.zshrc",
829
+ "<%= config.bin %> dotfiles add ~/.zshrc ~/.gitconfig",
830
+ "<%= config.bin %> dotfiles add ~/.ssh/id_ed25519 --encrypt",
831
+ "<%= config.bin %> dotfiles add --json ~/.zshrc"
832
+ ],
833
+ "flags": {
834
+ "json": {
835
+ "description": "Format output as json.",
836
+ "helpGroup": "GLOBAL",
837
+ "name": "json",
838
+ "allowNo": false,
839
+ "type": "boolean"
840
+ },
841
+ "help": {
842
+ "char": "h",
843
+ "description": "Show CLI help.",
844
+ "name": "help",
845
+ "allowNo": false,
846
+ "type": "boolean"
847
+ },
848
+ "encrypt": {
849
+ "char": "e",
850
+ "description": "Force encryption for all files being added",
851
+ "name": "encrypt",
852
+ "allowNo": false,
853
+ "type": "boolean"
854
+ },
855
+ "no-encrypt": {
856
+ "description": "Disable auto-encryption (add all as plaintext)",
857
+ "name": "no-encrypt",
858
+ "allowNo": false,
859
+ "type": "boolean"
860
+ }
861
+ },
862
+ "hasDynamicHelp": false,
863
+ "hiddenAliases": [],
864
+ "id": "dotfiles:add",
865
+ "pluginAlias": "devvami",
866
+ "pluginName": "devvami",
867
+ "pluginType": "core",
868
+ "strict": false,
869
+ "enableJsonFlag": true,
870
+ "isESM": true,
871
+ "relativePath": [
872
+ "src",
873
+ "commands",
874
+ "dotfiles",
875
+ "add.js"
876
+ ]
877
+ },
878
+ "dotfiles:setup": {
879
+ "aliases": [],
880
+ "args": {},
881
+ "description": "Interactive wizard to configure chezmoi with age encryption for dotfile management",
882
+ "examples": [
883
+ "<%= config.bin %> dotfiles setup",
884
+ "<%= config.bin %> dotfiles setup --json"
885
+ ],
886
+ "flags": {
887
+ "json": {
888
+ "description": "Format output as json.",
889
+ "helpGroup": "GLOBAL",
890
+ "name": "json",
891
+ "allowNo": false,
892
+ "type": "boolean"
893
+ },
894
+ "help": {
895
+ "char": "h",
896
+ "description": "Show CLI help.",
897
+ "name": "help",
898
+ "allowNo": false,
899
+ "type": "boolean"
900
+ }
901
+ },
902
+ "hasDynamicHelp": false,
903
+ "hiddenAliases": [],
904
+ "id": "dotfiles:setup",
905
+ "pluginAlias": "devvami",
906
+ "pluginName": "devvami",
907
+ "pluginType": "core",
908
+ "strict": true,
909
+ "enableJsonFlag": true,
910
+ "isESM": true,
911
+ "relativePath": [
912
+ "src",
913
+ "commands",
914
+ "dotfiles",
915
+ "setup.js"
916
+ ]
917
+ },
918
+ "dotfiles:status": {
919
+ "aliases": [],
920
+ "args": {},
921
+ "description": "Show chezmoi dotfiles status: managed files, encryption state, and sync health",
922
+ "examples": [
923
+ "<%= config.bin %> dotfiles status",
924
+ "<%= config.bin %> dotfiles status --json"
925
+ ],
926
+ "flags": {
927
+ "json": {
928
+ "description": "Format output as json.",
929
+ "helpGroup": "GLOBAL",
930
+ "name": "json",
931
+ "allowNo": false,
932
+ "type": "boolean"
933
+ },
934
+ "help": {
935
+ "char": "h",
936
+ "description": "Show CLI help.",
937
+ "name": "help",
938
+ "allowNo": false,
939
+ "type": "boolean"
940
+ }
941
+ },
942
+ "hasDynamicHelp": false,
943
+ "hiddenAliases": [],
944
+ "id": "dotfiles:status",
945
+ "pluginAlias": "devvami",
946
+ "pluginName": "devvami",
947
+ "pluginType": "core",
948
+ "strict": true,
949
+ "enableJsonFlag": true,
950
+ "isESM": true,
951
+ "relativePath": [
952
+ "src",
953
+ "commands",
954
+ "dotfiles",
955
+ "status.js"
956
+ ]
957
+ },
958
+ "dotfiles:sync": {
959
+ "aliases": [],
960
+ "args": {
961
+ "repo": {
962
+ "description": "Remote repository URL (for initial remote setup)",
963
+ "name": "repo",
964
+ "required": false
965
+ }
966
+ },
967
+ "description": "Sync dotfiles with remote repository: push local changes or pull from remote",
968
+ "examples": [
969
+ "<%= config.bin %> dotfiles sync",
970
+ "<%= config.bin %> dotfiles sync --push",
971
+ "<%= config.bin %> dotfiles sync --pull",
972
+ "<%= config.bin %> dotfiles sync --pull git@github.com:user/dotfiles.git",
973
+ "<%= config.bin %> dotfiles sync --dry-run --push",
974
+ "<%= config.bin %> dotfiles sync --json"
975
+ ],
976
+ "flags": {
977
+ "json": {
978
+ "description": "Format output as json.",
979
+ "helpGroup": "GLOBAL",
980
+ "name": "json",
981
+ "allowNo": false,
982
+ "type": "boolean"
983
+ },
984
+ "help": {
985
+ "char": "h",
986
+ "description": "Show CLI help.",
987
+ "name": "help",
988
+ "allowNo": false,
989
+ "type": "boolean"
990
+ },
991
+ "push": {
992
+ "description": "Push local changes to remote",
993
+ "name": "push",
994
+ "allowNo": false,
995
+ "type": "boolean"
996
+ },
997
+ "pull": {
998
+ "description": "Pull remote changes and apply",
999
+ "name": "pull",
1000
+ "allowNo": false,
1001
+ "type": "boolean"
1002
+ },
1003
+ "dry-run": {
1004
+ "description": "Show what would change without applying",
1005
+ "name": "dry-run",
1006
+ "allowNo": false,
1007
+ "type": "boolean"
1008
+ }
1009
+ },
1010
+ "hasDynamicHelp": false,
1011
+ "hiddenAliases": [],
1012
+ "id": "dotfiles:sync",
1013
+ "pluginAlias": "devvami",
1014
+ "pluginName": "devvami",
1015
+ "pluginType": "core",
1016
+ "strict": true,
1017
+ "enableJsonFlag": true,
1018
+ "isESM": true,
1019
+ "relativePath": [
1020
+ "src",
1021
+ "commands",
1022
+ "dotfiles",
1023
+ "sync.js"
1024
+ ]
1025
+ },
816
1026
  "logs": {
817
1027
  "aliases": [],
818
1028
  "args": {},
@@ -1722,7 +1932,193 @@
1722
1932
  "tasks",
1723
1933
  "today.js"
1724
1934
  ]
1935
+ },
1936
+ "vuln:detail": {
1937
+ "aliases": [],
1938
+ "args": {
1939
+ "cveId": {
1940
+ "description": "CVE identifier (e.g. CVE-2021-44228)",
1941
+ "name": "cveId",
1942
+ "required": true
1943
+ }
1944
+ },
1945
+ "description": "View full details for a specific CVE",
1946
+ "examples": [
1947
+ "<%= config.bin %> vuln detail CVE-2021-44228",
1948
+ "<%= config.bin %> vuln detail CVE-2021-44228 --open",
1949
+ "<%= config.bin %> vuln detail CVE-2021-44228 --json"
1950
+ ],
1951
+ "flags": {
1952
+ "json": {
1953
+ "description": "Format output as json.",
1954
+ "helpGroup": "GLOBAL",
1955
+ "name": "json",
1956
+ "allowNo": false,
1957
+ "type": "boolean"
1958
+ },
1959
+ "open": {
1960
+ "char": "o",
1961
+ "description": "Open the first reference URL in the default browser",
1962
+ "name": "open",
1963
+ "allowNo": false,
1964
+ "type": "boolean"
1965
+ }
1966
+ },
1967
+ "hasDynamicHelp": false,
1968
+ "hiddenAliases": [],
1969
+ "id": "vuln:detail",
1970
+ "pluginAlias": "devvami",
1971
+ "pluginName": "devvami",
1972
+ "pluginType": "core",
1973
+ "strict": true,
1974
+ "enableJsonFlag": true,
1975
+ "isESM": true,
1976
+ "relativePath": [
1977
+ "src",
1978
+ "commands",
1979
+ "vuln",
1980
+ "detail.js"
1981
+ ]
1982
+ },
1983
+ "vuln:scan": {
1984
+ "aliases": [],
1985
+ "args": {},
1986
+ "description": "Scan the current directory for known vulnerabilities in dependencies",
1987
+ "examples": [
1988
+ "<%= config.bin %> vuln scan",
1989
+ "<%= config.bin %> vuln scan --severity high",
1990
+ "<%= config.bin %> vuln scan --no-fail",
1991
+ "<%= config.bin %> vuln scan --report vuln-report.md",
1992
+ "<%= config.bin %> vuln scan --json"
1993
+ ],
1994
+ "flags": {
1995
+ "json": {
1996
+ "description": "Format output as json.",
1997
+ "helpGroup": "GLOBAL",
1998
+ "name": "json",
1999
+ "allowNo": false,
2000
+ "type": "boolean"
2001
+ },
2002
+ "severity": {
2003
+ "char": "s",
2004
+ "description": "Minimum severity filter",
2005
+ "name": "severity",
2006
+ "hasDynamicHelp": false,
2007
+ "multiple": false,
2008
+ "options": [
2009
+ "low",
2010
+ "medium",
2011
+ "high",
2012
+ "critical"
2013
+ ],
2014
+ "type": "option"
2015
+ },
2016
+ "no-fail": {
2017
+ "description": "Exit with code 0 even when vulnerabilities are found",
2018
+ "name": "no-fail",
2019
+ "allowNo": false,
2020
+ "type": "boolean"
2021
+ },
2022
+ "report": {
2023
+ "char": "r",
2024
+ "description": "Export vulnerability report to file path (Markdown format)",
2025
+ "name": "report",
2026
+ "hasDynamicHelp": false,
2027
+ "multiple": false,
2028
+ "type": "option"
2029
+ }
2030
+ },
2031
+ "hasDynamicHelp": false,
2032
+ "hiddenAliases": [],
2033
+ "id": "vuln:scan",
2034
+ "pluginAlias": "devvami",
2035
+ "pluginName": "devvami",
2036
+ "pluginType": "core",
2037
+ "strict": true,
2038
+ "enableJsonFlag": true,
2039
+ "isESM": true,
2040
+ "relativePath": [
2041
+ "src",
2042
+ "commands",
2043
+ "vuln",
2044
+ "scan.js"
2045
+ ]
2046
+ },
2047
+ "vuln:search": {
2048
+ "aliases": [],
2049
+ "args": {
2050
+ "keyword": {
2051
+ "description": "Product, library, or keyword to search for (optional — omit to see all recent CVEs)",
2052
+ "name": "keyword",
2053
+ "required": false
2054
+ }
2055
+ },
2056
+ "description": "Search for recent CVEs by keyword (omit keyword to see all recent CVEs)",
2057
+ "examples": [
2058
+ "<%= config.bin %> vuln search openssl",
2059
+ "<%= config.bin %> vuln search openssl --days 30",
2060
+ "<%= config.bin %> vuln search log4j --severity critical",
2061
+ "<%= config.bin %> vuln search nginx --limit 10 --json",
2062
+ "<%= config.bin %> vuln search",
2063
+ "<%= config.bin %> vuln search --days 7 --severity high"
2064
+ ],
2065
+ "flags": {
2066
+ "json": {
2067
+ "description": "Format output as json.",
2068
+ "helpGroup": "GLOBAL",
2069
+ "name": "json",
2070
+ "allowNo": false,
2071
+ "type": "boolean"
2072
+ },
2073
+ "days": {
2074
+ "char": "d",
2075
+ "description": "Time window in days (search CVEs published within last N days)",
2076
+ "name": "days",
2077
+ "default": 14,
2078
+ "hasDynamicHelp": false,
2079
+ "multiple": false,
2080
+ "type": "option"
2081
+ },
2082
+ "severity": {
2083
+ "char": "s",
2084
+ "description": "Minimum severity filter",
2085
+ "name": "severity",
2086
+ "hasDynamicHelp": false,
2087
+ "multiple": false,
2088
+ "options": [
2089
+ "low",
2090
+ "medium",
2091
+ "high",
2092
+ "critical"
2093
+ ],
2094
+ "type": "option"
2095
+ },
2096
+ "limit": {
2097
+ "char": "l",
2098
+ "description": "Maximum number of results to display",
2099
+ "name": "limit",
2100
+ "default": 20,
2101
+ "hasDynamicHelp": false,
2102
+ "multiple": false,
2103
+ "type": "option"
2104
+ }
2105
+ },
2106
+ "hasDynamicHelp": false,
2107
+ "hiddenAliases": [],
2108
+ "id": "vuln:search",
2109
+ "pluginAlias": "devvami",
2110
+ "pluginName": "devvami",
2111
+ "pluginType": "core",
2112
+ "strict": true,
2113
+ "enableJsonFlag": true,
2114
+ "isESM": true,
2115
+ "relativePath": [
2116
+ "src",
2117
+ "commands",
2118
+ "vuln",
2119
+ "search.js"
2120
+ ]
1725
2121
  }
1726
2122
  },
1727
- "version": "1.3.0"
2123
+ "version": "1.4.1"
1728
2124
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devvami",
3
3
  "description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
4
- "version": "1.3.0",
4
+ "version": "1.4.1",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
@@ -0,0 +1,249 @@
1
+ import { Command, Flags, Args } from '@oclif/core'
2
+ import ora from 'ora'
3
+ import chalk from 'chalk'
4
+ import { checkbox, confirm, input } from '@inquirer/prompts'
5
+ import { detectPlatform } from '../../services/platform.js'
6
+ import {
7
+ isChezmoiInstalled,
8
+ getManagedFiles,
9
+ getDefaultFileList,
10
+ getSensitivePatterns,
11
+ isPathSensitive,
12
+ isWSLWindowsPath,
13
+ } from '../../services/dotfiles.js'
14
+ import { loadConfig } from '../../services/config.js'
15
+ import { execOrThrow } from '../../services/shell.js'
16
+ import { formatDotfilesAdd } from '../../formatters/dotfiles.js'
17
+ import { DvmiError } from '../../utils/errors.js'
18
+ import { homedir } from 'node:os'
19
+ import { join } from 'node:path'
20
+ import { existsSync } from 'node:fs'
21
+
22
+ /** @import { DotfilesAddResult } from '../../types.js' */
23
+
24
+ /**
25
+ * Expand tilde to home directory.
26
+ * @param {string} p
27
+ * @returns {string}
28
+ */
29
+ function expandTilde(p) {
30
+ if (p.startsWith('~/') || p === '~') {
31
+ return join(homedir(), p.slice(2))
32
+ }
33
+ return p
34
+ }
35
+
36
+ export default class DotfilesAdd extends Command {
37
+ static description = 'Add dotfiles to chezmoi management with automatic encryption for sensitive files'
38
+
39
+ static examples = [
40
+ '<%= config.bin %> dotfiles add',
41
+ '<%= config.bin %> dotfiles add ~/.zshrc',
42
+ '<%= config.bin %> dotfiles add ~/.zshrc ~/.gitconfig',
43
+ '<%= config.bin %> dotfiles add ~/.ssh/id_ed25519 --encrypt',
44
+ '<%= config.bin %> dotfiles add --json ~/.zshrc',
45
+ ]
46
+
47
+ static enableJsonFlag = true
48
+
49
+ static flags = {
50
+ help: Flags.help({ char: 'h' }),
51
+ encrypt: Flags.boolean({ char: 'e', description: 'Force encryption for all files being added', default: false }),
52
+ 'no-encrypt': Flags.boolean({ description: 'Disable auto-encryption (add all as plaintext)', default: false }),
53
+ }
54
+
55
+ static args = {
56
+ files: Args.string({ description: 'File paths to add', required: false }),
57
+ }
58
+
59
+ // oclif does not support variadic args natively via Args.string for multiple values;
60
+ // we'll parse extra args from this.argv
61
+ static strict = false
62
+
63
+ async run() {
64
+ const { flags } = await this.parse(DotfilesAdd)
65
+ const isJson = flags.json
66
+ const forceEncrypt = flags.encrypt
67
+ const forceNoEncrypt = flags['no-encrypt']
68
+
69
+ // Collect file args from argv (strict=false allows extra positional args)
70
+ const rawArgs = this.argv.filter((a) => !a.startsWith('-'))
71
+ const fileArgs = rawArgs
72
+
73
+ // Pre-checks
74
+ const config = await loadConfig()
75
+ if (!config.dotfiles?.enabled) {
76
+ throw new DvmiError(
77
+ 'Chezmoi dotfiles management is not configured',
78
+ 'Run `dvmi dotfiles setup` first',
79
+ )
80
+ }
81
+
82
+ const chezmoiInstalled = await isChezmoiInstalled()
83
+ if (!chezmoiInstalled) {
84
+ const platformInfo = await detectPlatform()
85
+ const hint = platformInfo.platform === 'macos'
86
+ ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
87
+ : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
88
+ throw new DvmiError('chezmoi is not installed', hint)
89
+ }
90
+
91
+ const platformInfo = await detectPlatform()
92
+ const { platform } = platformInfo
93
+ const sensitivePatterns = getSensitivePatterns(config)
94
+
95
+ // Get already-managed files for V-007 check
96
+ const managedFiles = await getManagedFiles()
97
+ const managedPaths = new Set(managedFiles.map((f) => f.path))
98
+
99
+ /** @type {DotfilesAddResult} */
100
+ const result = { added: [], skipped: [], rejected: [] }
101
+
102
+ if (fileArgs.length > 0) {
103
+ // Direct mode — files provided as arguments
104
+ for (const rawPath of fileArgs) {
105
+ const absPath = expandTilde(rawPath)
106
+ const displayPath = rawPath
107
+
108
+ // V-002: WSL2 Windows path rejection
109
+ if (platform === 'wsl2' && isWSLWindowsPath(absPath)) {
110
+ result.rejected.push({ path: displayPath, reason: 'Windows filesystem paths not supported on WSL2. Use Linux-native paths (~/) instead.' })
111
+ continue
112
+ }
113
+
114
+ // V-001: file must exist
115
+ if (!existsSync(absPath)) {
116
+ result.skipped.push({ path: displayPath, reason: 'File not found' })
117
+ continue
118
+ }
119
+
120
+ // V-007: not already managed
121
+ if (managedPaths.has(absPath)) {
122
+ result.skipped.push({ path: displayPath, reason: 'Already managed by chezmoi' })
123
+ continue
124
+ }
125
+
126
+ // Determine encryption
127
+ let encrypt = false
128
+ if (forceEncrypt) {
129
+ encrypt = true
130
+ } else if (forceNoEncrypt) {
131
+ encrypt = false
132
+ } else {
133
+ encrypt = isPathSensitive(rawPath, sensitivePatterns)
134
+ }
135
+
136
+ try {
137
+ const args = ['add']
138
+ if (encrypt) args.push('--encrypt')
139
+ args.push(absPath)
140
+ await execOrThrow('chezmoi', args)
141
+ result.added.push({ path: displayPath, encrypted: encrypt })
142
+ } catch {
143
+ result.skipped.push({ path: displayPath, reason: `Failed to add to chezmoi. Run \`chezmoi doctor\` to verify your setup.` })
144
+ }
145
+ }
146
+
147
+ if (isJson) return result
148
+ this.log(formatDotfilesAdd(result))
149
+ return result
150
+ }
151
+
152
+ // Interactive mode — no file args
153
+ if (isJson) {
154
+ // In --json with no files: return empty result
155
+ return result
156
+ }
157
+
158
+ // Non-interactive guard for interactive mode
159
+ const isCI = process.env.CI === 'true'
160
+ const isNonInteractive = !process.stdout.isTTY
161
+ if (isCI || isNonInteractive) {
162
+ this.error(
163
+ 'This command requires an interactive terminal (TTY) when no files are specified. Provide file paths as arguments or run with --json.',
164
+ { exit: 1 },
165
+ )
166
+ }
167
+
168
+ const spinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Loading recommended files...') }).start()
169
+ const recommended = getDefaultFileList(platform)
170
+ spinner.stop()
171
+
172
+ // Filter and build choices
173
+ const choices = recommended.map((rec) => {
174
+ const absPath = expandTilde(rec.path)
175
+ const exists = existsSync(absPath)
176
+ const alreadyManaged = managedPaths.has(absPath)
177
+ const sensitive = rec.autoEncrypt || isPathSensitive(rec.path, sensitivePatterns)
178
+ const encTag = sensitive ? chalk.dim(' (auto-encrypted)') : ''
179
+ const statusTag = !exists ? chalk.dim(' (not found)') : alreadyManaged ? chalk.dim(' (already tracked)') : ''
180
+ return {
181
+ name: `${rec.path}${encTag}${statusTag} — ${rec.description}`,
182
+ value: rec.path,
183
+ checked: exists && !alreadyManaged,
184
+ disabled: alreadyManaged ? 'already tracked' : false,
185
+ }
186
+ })
187
+
188
+ const selected = await checkbox({
189
+ message: 'Select files to add to chezmoi:',
190
+ choices,
191
+ })
192
+
193
+ // Offer custom file
194
+ const addCustom = await confirm({ message: 'Add a custom file path?', default: false })
195
+ if (addCustom) {
196
+ const customPath = await input({ message: 'Enter file path:' })
197
+ if (customPath.trim()) selected.push(customPath.trim())
198
+ }
199
+
200
+ if (selected.length === 0) {
201
+ this.log(chalk.dim(' No files selected.'))
202
+ return result
203
+ }
204
+
205
+ const addSpinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Adding files to chezmoi...') }).start()
206
+ addSpinner.stop()
207
+
208
+ for (const rawPath of selected) {
209
+ const absPath = expandTilde(rawPath)
210
+
211
+ if (platform === 'wsl2' && isWSLWindowsPath(absPath)) {
212
+ result.rejected.push({ path: rawPath, reason: 'Windows filesystem paths not supported on WSL2' })
213
+ continue
214
+ }
215
+
216
+ if (!existsSync(absPath)) {
217
+ result.skipped.push({ path: rawPath, reason: 'File not found' })
218
+ continue
219
+ }
220
+
221
+ if (managedPaths.has(absPath)) {
222
+ result.skipped.push({ path: rawPath, reason: 'Already managed by chezmoi' })
223
+ continue
224
+ }
225
+
226
+ let encrypt = false
227
+ if (forceEncrypt) {
228
+ encrypt = true
229
+ } else if (forceNoEncrypt) {
230
+ encrypt = false
231
+ } else {
232
+ encrypt = isPathSensitive(rawPath, sensitivePatterns)
233
+ }
234
+
235
+ try {
236
+ const args = ['add']
237
+ if (encrypt) args.push('--encrypt')
238
+ args.push(absPath)
239
+ await execOrThrow('chezmoi', args)
240
+ result.added.push({ path: rawPath, encrypted: encrypt })
241
+ } catch {
242
+ result.skipped.push({ path: rawPath, reason: `Failed to add. Run \`chezmoi doctor\` to verify your setup.` })
243
+ }
244
+ }
245
+
246
+ this.log(formatDotfilesAdd(result))
247
+ return result
248
+ }
249
+ }