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/security.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import {homedir} from 'node:os'
|
|
2
|
+
import {join} from 'node:path'
|
|
3
|
+
import {readFile, appendFile} from 'node:fs/promises'
|
|
4
|
+
import {existsSync} from 'node:fs'
|
|
5
|
+
import {which, exec, execOrThrow} from './shell.js'
|
|
6
6
|
|
|
7
7
|
/** @import { Platform, PlatformInfo, SecurityTool, SecurityToolStatus, SetupStep, StepResult, GpgKey } from '../types.js' */
|
|
8
8
|
|
|
@@ -74,14 +74,20 @@ export async function checkToolStatus(platform) {
|
|
|
74
74
|
|
|
75
75
|
for (const tool of TOOL_DEFINITIONS) {
|
|
76
76
|
if (!tool.platforms.includes(platform)) {
|
|
77
|
-
results.push({
|
|
77
|
+
results.push({id: tool.id, displayName: tool.displayName, status: 'n/a', version: null, hint: null})
|
|
78
78
|
continue
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if (tool.id === 'aws-vault') {
|
|
82
82
|
const path = await which('aws-vault')
|
|
83
83
|
if (!path) {
|
|
84
|
-
results.push({
|
|
84
|
+
results.push({
|
|
85
|
+
id: tool.id,
|
|
86
|
+
displayName: tool.displayName,
|
|
87
|
+
status: 'not-installed',
|
|
88
|
+
version: null,
|
|
89
|
+
hint: 'Install aws-vault',
|
|
90
|
+
})
|
|
85
91
|
continue
|
|
86
92
|
}
|
|
87
93
|
const versionResult = await exec('aws-vault', ['--version'])
|
|
@@ -90,30 +96,60 @@ export async function checkToolStatus(platform) {
|
|
|
90
96
|
if (platform !== 'macos') {
|
|
91
97
|
const backend = process.env.AWS_VAULT_BACKEND
|
|
92
98
|
if (backend !== 'pass') {
|
|
93
|
-
results.push({
|
|
99
|
+
results.push({
|
|
100
|
+
id: tool.id,
|
|
101
|
+
displayName: tool.displayName,
|
|
102
|
+
status: 'misconfigured',
|
|
103
|
+
version: version || null,
|
|
104
|
+
hint: 'Add export AWS_VAULT_BACKEND=pass to your shell profile',
|
|
105
|
+
})
|
|
94
106
|
continue
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
|
-
results.push({
|
|
109
|
+
results.push({
|
|
110
|
+
id: tool.id,
|
|
111
|
+
displayName: tool.displayName,
|
|
112
|
+
status: 'installed',
|
|
113
|
+
version: version || null,
|
|
114
|
+
hint: null,
|
|
115
|
+
})
|
|
98
116
|
continue
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
if (tool.id === 'gpg') {
|
|
102
120
|
const path = await which('gpg')
|
|
103
121
|
if (!path) {
|
|
104
|
-
results.push({
|
|
122
|
+
results.push({
|
|
123
|
+
id: tool.id,
|
|
124
|
+
displayName: tool.displayName,
|
|
125
|
+
status: 'not-installed',
|
|
126
|
+
version: null,
|
|
127
|
+
hint: 'Install gnupg via your package manager',
|
|
128
|
+
})
|
|
105
129
|
continue
|
|
106
130
|
}
|
|
107
131
|
const versionResult = await exec('gpg', ['--version'])
|
|
108
132
|
const match = versionResult.stdout.match(/gpg \(GnuPG\)\s+([\d.]+)/)
|
|
109
|
-
results.push({
|
|
133
|
+
results.push({
|
|
134
|
+
id: tool.id,
|
|
135
|
+
displayName: tool.displayName,
|
|
136
|
+
status: 'installed',
|
|
137
|
+
version: match ? match[1] : null,
|
|
138
|
+
hint: null,
|
|
139
|
+
})
|
|
110
140
|
continue
|
|
111
141
|
}
|
|
112
142
|
|
|
113
143
|
if (tool.id === 'pass') {
|
|
114
144
|
const path = await which('pass')
|
|
115
145
|
if (!path) {
|
|
116
|
-
results.push({
|
|
146
|
+
results.push({
|
|
147
|
+
id: tool.id,
|
|
148
|
+
displayName: tool.displayName,
|
|
149
|
+
status: 'not-installed',
|
|
150
|
+
version: null,
|
|
151
|
+
hint: 'Install pass via your package manager',
|
|
152
|
+
})
|
|
117
153
|
continue
|
|
118
154
|
}
|
|
119
155
|
const versionResult = await exec('pass', ['--version'])
|
|
@@ -121,19 +157,37 @@ export async function checkToolStatus(platform) {
|
|
|
121
157
|
// Check if pass is initialized
|
|
122
158
|
const lsResult = await exec('pass', ['ls'])
|
|
123
159
|
if (lsResult.exitCode !== 0) {
|
|
124
|
-
results.push({
|
|
160
|
+
results.push({
|
|
161
|
+
id: tool.id,
|
|
162
|
+
displayName: tool.displayName,
|
|
163
|
+
status: 'misconfigured',
|
|
164
|
+
version: match ? match[1] : null,
|
|
165
|
+
hint: 'Initialize pass with: pass init <gpg-key-id>',
|
|
166
|
+
})
|
|
125
167
|
continue
|
|
126
168
|
}
|
|
127
|
-
results.push({
|
|
169
|
+
results.push({
|
|
170
|
+
id: tool.id,
|
|
171
|
+
displayName: tool.displayName,
|
|
172
|
+
status: 'installed',
|
|
173
|
+
version: match ? match[1] : null,
|
|
174
|
+
hint: null,
|
|
175
|
+
})
|
|
128
176
|
continue
|
|
129
177
|
}
|
|
130
178
|
|
|
131
179
|
if (tool.id === 'osxkeychain') {
|
|
132
180
|
const result = await exec('git', ['config', '--global', 'credential.helper'])
|
|
133
181
|
if (result.stdout === 'osxkeychain') {
|
|
134
|
-
results.push({
|
|
182
|
+
results.push({id: tool.id, displayName: tool.displayName, status: 'installed', version: null, hint: null})
|
|
135
183
|
} else {
|
|
136
|
-
results.push({
|
|
184
|
+
results.push({
|
|
185
|
+
id: tool.id,
|
|
186
|
+
displayName: tool.displayName,
|
|
187
|
+
status: 'not-installed',
|
|
188
|
+
version: null,
|
|
189
|
+
hint: 'Run: git config --global credential.helper osxkeychain',
|
|
190
|
+
})
|
|
137
191
|
}
|
|
138
192
|
continue
|
|
139
193
|
}
|
|
@@ -141,17 +195,29 @@ export async function checkToolStatus(platform) {
|
|
|
141
195
|
if (tool.id === 'gcm') {
|
|
142
196
|
const path = await which('git-credential-manager')
|
|
143
197
|
if (!path) {
|
|
144
|
-
results.push({
|
|
198
|
+
results.push({
|
|
199
|
+
id: tool.id,
|
|
200
|
+
displayName: tool.displayName,
|
|
201
|
+
status: 'not-installed',
|
|
202
|
+
version: null,
|
|
203
|
+
hint: 'Install Git Credential Manager',
|
|
204
|
+
})
|
|
145
205
|
continue
|
|
146
206
|
}
|
|
147
207
|
const versionResult = await exec('git-credential-manager', ['--version'])
|
|
148
208
|
const version = versionResult.stdout.trim() || null
|
|
149
209
|
const storeResult = await exec('git', ['config', '--global', 'credential.credentialStore'])
|
|
150
210
|
if (storeResult.stdout !== 'gpg') {
|
|
151
|
-
results.push({
|
|
211
|
+
results.push({
|
|
212
|
+
id: tool.id,
|
|
213
|
+
displayName: tool.displayName,
|
|
214
|
+
status: 'misconfigured',
|
|
215
|
+
version,
|
|
216
|
+
hint: 'Run: git config --global credential.credentialStore gpg',
|
|
217
|
+
})
|
|
152
218
|
continue
|
|
153
219
|
}
|
|
154
|
-
results.push({
|
|
220
|
+
results.push({id: tool.id, displayName: tool.displayName, status: 'installed', version, hint: null})
|
|
155
221
|
continue
|
|
156
222
|
}
|
|
157
223
|
}
|
|
@@ -267,7 +333,7 @@ export function deriveOverallStatus(tools) {
|
|
|
267
333
|
* @returns {SetupStep[]}
|
|
268
334
|
*/
|
|
269
335
|
export function buildSteps(platformInfo, selection, context = {}) {
|
|
270
|
-
const {
|
|
336
|
+
const {platform} = platformInfo
|
|
271
337
|
const includeAws = selection === 'aws' || selection === 'both'
|
|
272
338
|
const includeGit = selection === 'git' || selection === 'both'
|
|
273
339
|
|
|
@@ -291,7 +357,7 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
291
357
|
hintUrl: 'https://brew.sh',
|
|
292
358
|
}
|
|
293
359
|
}
|
|
294
|
-
return {
|
|
360
|
+
return {status: 'success', message: 'Homebrew is available'}
|
|
295
361
|
},
|
|
296
362
|
})
|
|
297
363
|
|
|
@@ -303,10 +369,10 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
303
369
|
requiresConfirmation: true,
|
|
304
370
|
run: async () => {
|
|
305
371
|
const existing = await which('aws-vault')
|
|
306
|
-
if (existing) return {
|
|
372
|
+
if (existing) return {status: 'skipped', message: 'aws-vault already installed'}
|
|
307
373
|
try {
|
|
308
374
|
await execOrThrow('brew', ['install', 'aws-vault'])
|
|
309
|
-
return {
|
|
375
|
+
return {status: 'success', message: 'aws-vault installed via Homebrew'}
|
|
310
376
|
} catch {
|
|
311
377
|
return {
|
|
312
378
|
status: 'failed',
|
|
@@ -326,10 +392,10 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
326
392
|
run: async () => {
|
|
327
393
|
const result = await exec('aws-vault', ['--version'])
|
|
328
394
|
if (result.exitCode !== 0) {
|
|
329
|
-
return {
|
|
395
|
+
return {status: 'failed', hint: 'aws-vault not found in PATH after install'}
|
|
330
396
|
}
|
|
331
397
|
const version = (result.stdout || result.stderr).trim()
|
|
332
|
-
return {
|
|
398
|
+
return {status: 'success', message: `aws-vault ${version}`}
|
|
333
399
|
},
|
|
334
400
|
})
|
|
335
401
|
}
|
|
@@ -344,9 +410,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
344
410
|
run: async () => {
|
|
345
411
|
try {
|
|
346
412
|
await execOrThrow('git', ['config', '--global', 'credential.helper', 'osxkeychain'])
|
|
347
|
-
return {
|
|
413
|
+
return {status: 'success', message: 'Git credential helper set to osxkeychain'}
|
|
348
414
|
} catch {
|
|
349
|
-
return {
|
|
415
|
+
return {status: 'failed', hint: 'Run manually: git config --global credential.helper osxkeychain'}
|
|
350
416
|
}
|
|
351
417
|
},
|
|
352
418
|
})
|
|
@@ -360,9 +426,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
360
426
|
run: async () => {
|
|
361
427
|
const result = await exec('git', ['config', '--global', 'credential.helper'])
|
|
362
428
|
if (result.stdout !== 'osxkeychain') {
|
|
363
|
-
return {
|
|
429
|
+
return {status: 'failed', hint: 'credential.helper is not set to osxkeychain'}
|
|
364
430
|
}
|
|
365
|
-
return {
|
|
431
|
+
return {status: 'success', message: 'osxkeychain is configured'}
|
|
366
432
|
},
|
|
367
433
|
})
|
|
368
434
|
}
|
|
@@ -378,11 +444,11 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
378
444
|
run: async () => {
|
|
379
445
|
const path = await which('gpg')
|
|
380
446
|
if (!path) {
|
|
381
|
-
return {
|
|
447
|
+
return {status: 'failed', hint: 'GPG not found — will be installed in the next step'}
|
|
382
448
|
}
|
|
383
449
|
const result = await exec('gpg', ['--version'])
|
|
384
450
|
const match = result.stdout.match(/gpg \(GnuPG\)\s+([\d.]+)/)
|
|
385
|
-
return {
|
|
451
|
+
return {status: 'success', message: `GPG ${match ? match[1] : 'found'}`}
|
|
386
452
|
},
|
|
387
453
|
})
|
|
388
454
|
|
|
@@ -395,12 +461,12 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
395
461
|
skippable: true,
|
|
396
462
|
run: async () => {
|
|
397
463
|
const path = await which('gpg')
|
|
398
|
-
if (path) return {
|
|
464
|
+
if (path) return {status: 'skipped', message: 'GPG already installed'}
|
|
399
465
|
try {
|
|
400
466
|
await execOrThrow('sudo', ['apt-get', 'install', '-y', 'gnupg'])
|
|
401
|
-
return {
|
|
467
|
+
return {status: 'success', message: 'GPG installed'}
|
|
402
468
|
} catch {
|
|
403
|
-
return {
|
|
469
|
+
return {status: 'failed', hint: 'Run manually: sudo apt-get install -y gnupg'}
|
|
404
470
|
}
|
|
405
471
|
},
|
|
406
472
|
})
|
|
@@ -414,7 +480,7 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
414
480
|
gpgInteractive: true,
|
|
415
481
|
run: async () => {
|
|
416
482
|
const gpgId = context.gpgId
|
|
417
|
-
if (gpgId) return {
|
|
483
|
+
if (gpgId) return {status: 'skipped', message: `Using existing GPG key ${gpgId}`}
|
|
418
484
|
// When gpgInteractive=true, the command layer stops the spinner and spawns
|
|
419
485
|
// gpg --full-generate-key with stdio:inherit so the user sets a strong passphrase.
|
|
420
486
|
// We never generate a key with an empty passphrase — that would leave the private
|
|
@@ -436,12 +502,12 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
436
502
|
requiresConfirmation: true,
|
|
437
503
|
run: async () => {
|
|
438
504
|
const path = await which('pass')
|
|
439
|
-
if (path) return {
|
|
505
|
+
if (path) return {status: 'skipped', message: 'pass already installed'}
|
|
440
506
|
try {
|
|
441
507
|
await execOrThrow('sudo', ['apt-get', 'install', '-y', 'pass'])
|
|
442
|
-
return {
|
|
508
|
+
return {status: 'success', message: 'pass installed'}
|
|
443
509
|
} catch {
|
|
444
|
-
return {
|
|
510
|
+
return {status: 'failed', hint: 'Run manually: sudo apt-get install -y pass'}
|
|
445
511
|
}
|
|
446
512
|
},
|
|
447
513
|
})
|
|
@@ -456,17 +522,17 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
456
522
|
// Skip if pass is already initialized
|
|
457
523
|
const lsResult = await exec('pass', ['ls'])
|
|
458
524
|
if (lsResult.exitCode === 0) {
|
|
459
|
-
return {
|
|
525
|
+
return {status: 'skipped', message: 'pass store already initialized'}
|
|
460
526
|
}
|
|
461
527
|
const gpgId = context.gpgId
|
|
462
528
|
if (!gpgId) {
|
|
463
|
-
return {
|
|
529
|
+
return {status: 'failed', hint: 'No GPG key ID available — complete the create-gpg-key step first'}
|
|
464
530
|
}
|
|
465
531
|
try {
|
|
466
532
|
await execOrThrow('pass', ['init', gpgId])
|
|
467
|
-
return {
|
|
533
|
+
return {status: 'success', message: `pass initialized with key ${gpgId}`}
|
|
468
534
|
} catch {
|
|
469
|
-
return {
|
|
535
|
+
return {status: 'failed', hint: `Run manually: pass init ${gpgId}`}
|
|
470
536
|
}
|
|
471
537
|
},
|
|
472
538
|
})
|
|
@@ -479,12 +545,16 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
479
545
|
requiresConfirmation: true,
|
|
480
546
|
run: async () => {
|
|
481
547
|
const existing = await which('aws-vault')
|
|
482
|
-
if (existing) return {
|
|
548
|
+
if (existing) return {status: 'skipped', message: 'aws-vault already installed'}
|
|
483
549
|
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64'
|
|
484
550
|
const url = `https://github.com/99designs/aws-vault/releases/latest/download/aws-vault-linux-${arch}`
|
|
485
551
|
try {
|
|
486
|
-
await execOrThrow('sudo', [
|
|
487
|
-
|
|
552
|
+
await execOrThrow('sudo', [
|
|
553
|
+
'sh',
|
|
554
|
+
'-c',
|
|
555
|
+
`curl -sSL '${url}' -o /usr/local/bin/aws-vault && chmod +x /usr/local/bin/aws-vault`,
|
|
556
|
+
])
|
|
557
|
+
return {status: 'success', message: 'aws-vault installed to /usr/local/bin/aws-vault'}
|
|
488
558
|
} catch {
|
|
489
559
|
return {
|
|
490
560
|
status: 'failed',
|
|
@@ -505,9 +575,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
505
575
|
try {
|
|
506
576
|
await appendToShellProfile('export AWS_VAULT_BACKEND=pass')
|
|
507
577
|
await appendToShellProfile('export GPG_TTY=$(tty)')
|
|
508
|
-
return {
|
|
578
|
+
return {status: 'success', message: 'AWS_VAULT_BACKEND=pass and GPG_TTY added to shell profile'}
|
|
509
579
|
} catch {
|
|
510
|
-
return {
|
|
580
|
+
return {status: 'failed', hint: 'Add manually to ~/.bashrc or ~/.zshrc: export AWS_VAULT_BACKEND=pass'}
|
|
511
581
|
}
|
|
512
582
|
},
|
|
513
583
|
})
|
|
@@ -521,10 +591,10 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
521
591
|
run: async () => {
|
|
522
592
|
const result = await exec('aws-vault', ['--version'])
|
|
523
593
|
if (result.exitCode !== 0) {
|
|
524
|
-
return {
|
|
594
|
+
return {status: 'failed', hint: 'aws-vault not found in PATH after install'}
|
|
525
595
|
}
|
|
526
596
|
const version = (result.stdout || result.stderr).trim()
|
|
527
|
-
return {
|
|
597
|
+
return {status: 'success', message: `aws-vault ${version}`}
|
|
528
598
|
},
|
|
529
599
|
})
|
|
530
600
|
}
|
|
@@ -541,9 +611,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
541
611
|
run: async () => {
|
|
542
612
|
const bridgePath = '/mnt/c/Program Files/Git/mingw64/bin/git-credential-manager.exe'
|
|
543
613
|
if (existsSync(bridgePath)) {
|
|
544
|
-
return {
|
|
614
|
+
return {status: 'success', message: 'Windows GCM bridge found — using Windows Credential Manager'}
|
|
545
615
|
}
|
|
546
|
-
return {
|
|
616
|
+
return {status: 'skipped', message: 'Windows GCM not found — will install native Linux GCM'}
|
|
547
617
|
},
|
|
548
618
|
})
|
|
549
619
|
}
|
|
@@ -556,21 +626,25 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
556
626
|
requiresConfirmation: true,
|
|
557
627
|
run: async () => {
|
|
558
628
|
const existing = await which('git-credential-manager')
|
|
559
|
-
if (existing) return {
|
|
629
|
+
if (existing) return {status: 'skipped', message: 'Git Credential Manager already installed'}
|
|
560
630
|
try {
|
|
561
|
-
const latestResult = await exec('sh', [
|
|
631
|
+
const latestResult = await exec('sh', [
|
|
632
|
+
'-c',
|
|
633
|
+
"curl -sSL https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest | grep 'browser_download_url.*gcm.*linux.*amd64.*deb' | head -1 | cut -d '\"' -f 4",
|
|
634
|
+
])
|
|
562
635
|
const debUrl = latestResult.stdout.trim()
|
|
563
636
|
if (!debUrl) throw new Error('Could not find GCM deb package URL')
|
|
564
637
|
// Security: validate debUrl is a legitimate GitHub release asset URL before using it
|
|
565
638
|
// This prevents command injection if the GitHub API response were tampered with (MITM / supply-chain)
|
|
566
|
-
const SAFE_DEB_URL =
|
|
639
|
+
const SAFE_DEB_URL =
|
|
640
|
+
/^https:\/\/github\.com\/git-ecosystem\/git-credential-manager\/releases\/download\/[a-zA-Z0-9._\-/]+\.deb$/
|
|
567
641
|
if (!SAFE_DEB_URL.test(debUrl)) {
|
|
568
642
|
throw new Error(`Unexpected GCM package URL format: "${debUrl}"`)
|
|
569
643
|
}
|
|
570
644
|
// Use array args — no shell interpolation of the URL
|
|
571
645
|
await execOrThrow('curl', ['-sSL', debUrl, '-o', '/tmp/gcm.deb'])
|
|
572
646
|
await execOrThrow('sudo', ['dpkg', '-i', '/tmp/gcm.deb'])
|
|
573
|
-
return {
|
|
647
|
+
return {status: 'success', message: 'Git Credential Manager installed'}
|
|
574
648
|
} catch {
|
|
575
649
|
return {
|
|
576
650
|
status: 'failed',
|
|
@@ -590,9 +664,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
590
664
|
run: async () => {
|
|
591
665
|
try {
|
|
592
666
|
await execOrThrow('git-credential-manager', ['configure'])
|
|
593
|
-
return {
|
|
667
|
+
return {status: 'success', message: 'Git Credential Manager configured'}
|
|
594
668
|
} catch {
|
|
595
|
-
return {
|
|
669
|
+
return {status: 'failed', hint: 'Run manually: git-credential-manager configure'}
|
|
596
670
|
}
|
|
597
671
|
},
|
|
598
672
|
})
|
|
@@ -606,9 +680,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
606
680
|
run: async () => {
|
|
607
681
|
try {
|
|
608
682
|
await execOrThrow('git', ['config', '--global', 'credential.credentialStore', 'gpg'])
|
|
609
|
-
return {
|
|
683
|
+
return {status: 'success', message: 'GCM credential store set to gpg'}
|
|
610
684
|
} catch {
|
|
611
|
-
return {
|
|
685
|
+
return {status: 'failed', hint: 'Run manually: git config --global credential.credentialStore gpg'}
|
|
612
686
|
}
|
|
613
687
|
},
|
|
614
688
|
})
|
|
@@ -622,9 +696,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
|
|
|
622
696
|
run: async () => {
|
|
623
697
|
const result = await exec('git-credential-manager', ['--version'])
|
|
624
698
|
if (result.exitCode !== 0) {
|
|
625
|
-
return {
|
|
699
|
+
return {status: 'failed', hint: 'git-credential-manager not found in PATH'}
|
|
626
700
|
}
|
|
627
|
-
return {
|
|
701
|
+
return {status: 'success', message: `GCM ${result.stdout.trim()}`}
|
|
628
702
|
},
|
|
629
703
|
})
|
|
630
704
|
}
|
package/src/services/shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {execa} from 'execa'
|
|
2
2
|
|
|
3
3
|
/** @import { ExecResult } from '../types.js' */
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ import { execa } from 'execa'
|
|
|
10
10
|
* @returns {Promise<ExecResult>}
|
|
11
11
|
*/
|
|
12
12
|
export async function exec(command, args = [], opts = {}) {
|
|
13
|
-
const result = await execa(command, args, {
|
|
13
|
+
const result = await execa(command, args, {reject: false, ...opts})
|
|
14
14
|
return {
|
|
15
15
|
stdout: result.stdout?.trim() ?? '',
|
|
16
16
|
stderr: result.stderr?.trim() ?? '',
|
|
@@ -24,7 +24,7 @@ export async function exec(command, args = [], opts = {}) {
|
|
|
24
24
|
* @returns {Promise<string|null>} Resolved path or null if not found
|
|
25
25
|
*/
|
|
26
26
|
export async function which(binary) {
|
|
27
|
-
const result = await execa('which', [binary], {
|
|
27
|
+
const result = await execa('which', [binary], {reject: false})
|
|
28
28
|
if (result.exitCode !== 0 || !result.stdout) return null
|
|
29
29
|
return result.stdout.trim()
|
|
30
30
|
}
|
|
@@ -37,6 +37,6 @@ export async function which(binary) {
|
|
|
37
37
|
* @returns {Promise<string>}
|
|
38
38
|
*/
|
|
39
39
|
export async function execOrThrow(command, args = [], opts = {}) {
|
|
40
|
-
const result = await execa(command, args, {
|
|
40
|
+
const result = await execa(command, args, {reject: true, ...opts})
|
|
41
41
|
return result.stdout?.trim() ?? ''
|
|
42
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {DvmiError} from '../utils/errors.js'
|
|
2
2
|
|
|
3
3
|
/** @import { Skill } from '../types.js' */
|
|
4
4
|
|
|
@@ -37,17 +37,11 @@ export async function searchSkills(query, limit = 50) {
|
|
|
37
37
|
try {
|
|
38
38
|
res = await fetch(url.toString())
|
|
39
39
|
} catch {
|
|
40
|
-
throw new DvmiError(
|
|
41
|
-
'Unable to reach skills.sh API',
|
|
42
|
-
'Check your internet connection and try again',
|
|
43
|
-
)
|
|
40
|
+
throw new DvmiError('Unable to reach skills.sh API', 'Check your internet connection and try again')
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
if (!res.ok) {
|
|
47
|
-
throw new DvmiError(
|
|
48
|
-
`skills.sh API returned ${res.status}`,
|
|
49
|
-
'Try again later or visit https://skills.sh',
|
|
50
|
-
)
|
|
44
|
+
throw new DvmiError(`skills.sh API returned ${res.status}`, 'Try again later or visit https://skills.sh')
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
/** @type {unknown} */
|
package/src/services/speckit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import {execa} from 'execa'
|
|
2
|
+
import {which, exec} from './shell.js'
|
|
3
|
+
import {DvmiError} from '../utils/errors.js'
|
|
4
4
|
|
|
5
5
|
/** GitHub spec-kit package source for uv.
|
|
6
6
|
* TODO: pin to a specific tagged release (e.g. #v1.x.x) once one is available upstream.
|
|
@@ -68,9 +68,6 @@ export async function runSpecifyInit(cwd, opts = {}) {
|
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
if (result.exitCode !== 0) {
|
|
71
|
-
throw new DvmiError(
|
|
72
|
-
'`specify init` exited with a non-zero code',
|
|
73
|
-
'Check the output above for details',
|
|
74
|
-
)
|
|
71
|
+
throw new DvmiError('`specify init` exited with a non-zero code', 'Check the output above for details')
|
|
75
72
|
}
|
|
76
73
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import {readFile} from 'node:fs/promises'
|
|
2
|
+
import {join} from 'node:path'
|
|
3
|
+
import {fileURLToPath} from 'node:url'
|
|
4
|
+
import {loadConfig, saveConfig} from './config.js'
|
|
5
|
+
import {exec} from './shell.js'
|
|
6
6
|
|
|
7
7
|
const PKG_PATH = join(fileURLToPath(import.meta.url), '..', '..', '..', 'package.json')
|
|
8
8
|
const REPO = 'devvami/devvami'
|
|
@@ -22,7 +22,7 @@ export async function getCurrentVersion() {
|
|
|
22
22
|
* @param {{ force?: boolean }} [opts]
|
|
23
23
|
* @returns {Promise<string|null>}
|
|
24
24
|
*/
|
|
25
|
-
export async function getLatestVersion({
|
|
25
|
+
export async function getLatestVersion({force = false} = {}) {
|
|
26
26
|
const config = await loadConfig()
|
|
27
27
|
const now = Date.now()
|
|
28
28
|
const lastCheck = config.lastVersionCheck ? new Date(config.lastVersionCheck).getTime() : 0
|
|
@@ -38,7 +38,7 @@ export async function getLatestVersion({ force = false } = {}) {
|
|
|
38
38
|
// Il tag è nel formato "v1.0.0" — rimuove il prefisso "v"
|
|
39
39
|
const latest = result.stdout.trim().replace(/^v/, '') || null
|
|
40
40
|
if (latest) {
|
|
41
|
-
await saveConfig({
|
|
41
|
+
await saveConfig({...config, latestVersion: latest, lastVersionCheck: new Date().toISOString()})
|
|
42
42
|
}
|
|
43
43
|
return latest
|
|
44
44
|
} catch {
|
|
@@ -51,8 +51,8 @@ export async function getLatestVersion({ force = false } = {}) {
|
|
|
51
51
|
* @param {{ force?: boolean }} [opts]
|
|
52
52
|
* @returns {Promise<{ hasUpdate: boolean, current: string, latest: string|null }>}
|
|
53
53
|
*/
|
|
54
|
-
export async function checkForUpdate({
|
|
55
|
-
const [current, latest] = await Promise.all([getCurrentVersion(), getLatestVersion({
|
|
54
|
+
export async function checkForUpdate({force = false} = {}) {
|
|
55
|
+
const [current, latest] = await Promise.all([getCurrentVersion(), getLatestVersion({force})])
|
|
56
56
|
const hasUpdate = Boolean(latest && latest !== current)
|
|
57
|
-
return {
|
|
57
|
+
return {hasUpdate, current, latest}
|
|
58
58
|
}
|