devvami 1.1.0 → 1.1.2
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/oclif.manifest.json +1 -1
- package/package.json +2 -1
- package/src/commands/init.js +2 -0
- package/src/commands/open.js +1 -1
- package/src/commands/upgrade.js +5 -0
- package/src/services/clickup.js +15 -1
- package/src/services/config.js +2 -1
- package/src/services/prompts.js +20 -1
- package/src/services/speckit.js +4 -1
package/oclif.manifest.json
CHANGED
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.1.
|
|
4
|
+
"version": "1.1.2",
|
|
5
5
|
"author": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"commit": "git-cz",
|
|
129
129
|
"release": "semantic-release",
|
|
130
130
|
"release:dry-run": "semantic-release --dry-run",
|
|
131
|
+
"version:sync": "node scripts/sync-version.js",
|
|
131
132
|
"lint": "eslint src/ tests/",
|
|
132
133
|
"lint:fix": "eslint src/ tests/ --fix",
|
|
133
134
|
"format": "prettier --write src/ tests/",
|
package/src/commands/init.js
CHANGED
|
@@ -80,6 +80,8 @@ export default class Init extends Command {
|
|
|
80
80
|
let config = await loadConfig()
|
|
81
81
|
|
|
82
82
|
if (!configExists() && !isDryRun && !isJson) {
|
|
83
|
+
// Stop the spinner before interactive prompts to avoid TTY contention on macOS
|
|
84
|
+
configSpinner?.stop()
|
|
83
85
|
const useOrg = await confirm({ message: 'Do you use a GitHub organization? (y/n)', default: true })
|
|
84
86
|
let org = ''
|
|
85
87
|
if (useOrg) {
|
package/src/commands/open.js
CHANGED
package/src/commands/upgrade.js
CHANGED
|
@@ -22,6 +22,11 @@ export default class Upgrade extends Command {
|
|
|
22
22
|
const { hasUpdate, current, latest } = await checkForUpdate({ force: true })
|
|
23
23
|
spinner?.stop()
|
|
24
24
|
|
|
25
|
+
// Guard against malformed version strings from the GitHub Releases API
|
|
26
|
+
if (latest && !/^v?\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(latest)) {
|
|
27
|
+
this.error(`Invalid version received from releases API: "${latest}" — update aborted`)
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
if (!hasUpdate) {
|
|
26
31
|
const msg = `You're already on the latest version (${current})`
|
|
27
32
|
if (isJson) return { currentVersion: current, latestVersion: latest, updated: false }
|
package/src/services/clickup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
2
3
|
import { openBrowser } from '../utils/open-browser.js'
|
|
3
4
|
import { loadConfig } from './config.js'
|
|
4
5
|
|
|
@@ -45,6 +46,10 @@ export async function storeToken(token) {
|
|
|
45
46
|
await keytar.setPassword('devvami', TOKEN_KEY, token)
|
|
46
47
|
} catch {
|
|
47
48
|
// Fallback: store in config (less secure)
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
'Warning: keytar unavailable. ClickUp token will be stored in plaintext.\n' +
|
|
51
|
+
'Run `dvmi auth logout` after this session on shared machines.\n',
|
|
52
|
+
)
|
|
48
53
|
const config = await loadConfig()
|
|
49
54
|
await saveConfig({ ...config, clickup: { ...config.clickup, token } })
|
|
50
55
|
}
|
|
@@ -57,11 +62,20 @@ export async function storeToken(token) {
|
|
|
57
62
|
* @returns {Promise<string>} Access token
|
|
58
63
|
*/
|
|
59
64
|
export async function oauthFlow(clientId, clientSecret) {
|
|
65
|
+
const csrfState = randomBytes(16).toString('hex')
|
|
60
66
|
return new Promise((resolve, reject) => {
|
|
61
67
|
const server = http.createServer(async (req, res) => {
|
|
62
68
|
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
63
69
|
const code = url.searchParams.get('code')
|
|
70
|
+
const returnedState = url.searchParams.get('state')
|
|
64
71
|
if (!code) return
|
|
72
|
+
if (!returnedState || returnedState !== csrfState) {
|
|
73
|
+
res.writeHead(400)
|
|
74
|
+
res.end('State mismatch — possible CSRF attack.')
|
|
75
|
+
server.close()
|
|
76
|
+
reject(new Error('OAuth state mismatch — possible CSRF attack'))
|
|
77
|
+
return
|
|
78
|
+
}
|
|
65
79
|
res.end('Authorization successful! You can close this tab.')
|
|
66
80
|
server.close()
|
|
67
81
|
try {
|
|
@@ -80,7 +94,7 @@ export async function oauthFlow(clientId, clientSecret) {
|
|
|
80
94
|
server.listen(0, async () => {
|
|
81
95
|
const addr = /** @type {import('node:net').AddressInfo} */ (server.address())
|
|
82
96
|
const callbackUrl = `http://localhost:${addr.port}/callback`
|
|
83
|
-
const authUrl = `https://app.clickup.com/api?client_id=${clientId}&redirect_uri=${encodeURIComponent(callbackUrl)}`
|
|
97
|
+
const authUrl = `https://app.clickup.com/api?client_id=${clientId}&redirect_uri=${encodeURIComponent(callbackUrl)}&state=${csrfState}`
|
|
84
98
|
await openBrowser(authUrl)
|
|
85
99
|
})
|
|
86
100
|
server.on('error', reject)
|
package/src/services/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
1
|
+
import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
@@ -47,6 +47,7 @@ export async function saveConfig(config, configPath = CONFIG_PATH) {
|
|
|
47
47
|
await mkdir(dir, { recursive: true })
|
|
48
48
|
}
|
|
49
49
|
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8')
|
|
50
|
+
await chmod(configPath, 0o600)
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
package/src/services/prompts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, writeFile, readFile, access } from 'node:fs/promises'
|
|
2
|
-
import { join, dirname } from 'node:path'
|
|
2
|
+
import { join, dirname, resolve, sep } from 'node:path'
|
|
3
3
|
import { execa } from 'execa'
|
|
4
4
|
import { createOctokit } from './github.js'
|
|
5
5
|
import { which } from './shell.js'
|
|
@@ -204,6 +204,15 @@ export async function fetchPromptByPath(relativePath) {
|
|
|
204
204
|
export async function downloadPrompt(relativePath, localDir, opts = {}) {
|
|
205
205
|
const destPath = join(localDir, relativePath)
|
|
206
206
|
|
|
207
|
+
// Prevent path traversal: destPath must remain within localDir
|
|
208
|
+
const safeBase = resolve(localDir) + sep
|
|
209
|
+
if (!resolve(destPath).startsWith(safeBase)) {
|
|
210
|
+
throw new DvmiError(
|
|
211
|
+
`Invalid prompt path: "${relativePath}"`,
|
|
212
|
+
'Path must stay within the prompts directory',
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
207
216
|
// Fast-path: skip without a network round-trip if file exists and no overwrite
|
|
208
217
|
if (!opts.overwrite) {
|
|
209
218
|
try {
|
|
@@ -245,6 +254,16 @@ export async function downloadPrompt(relativePath, localDir, opts = {}) {
|
|
|
245
254
|
*/
|
|
246
255
|
export async function resolveLocalPrompt(relativePath, localDir) {
|
|
247
256
|
const fullPath = join(localDir, relativePath)
|
|
257
|
+
|
|
258
|
+
// Prevent path traversal: fullPath must remain within localDir
|
|
259
|
+
const safeBase = resolve(localDir) + sep
|
|
260
|
+
if (!resolve(fullPath).startsWith(safeBase)) {
|
|
261
|
+
throw new DvmiError(
|
|
262
|
+
`Invalid prompt path: "${relativePath}"`,
|
|
263
|
+
'Path must stay within the prompts directory',
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
let raw
|
|
249
268
|
try {
|
|
250
269
|
raw = await readFile(fullPath, 'utf8')
|
package/src/services/speckit.js
CHANGED
|
@@ -2,7 +2,10 @@ import { execa } from 'execa'
|
|
|
2
2
|
import { which, exec } from './shell.js'
|
|
3
3
|
import { DvmiError } from '../utils/errors.js'
|
|
4
4
|
|
|
5
|
-
/** GitHub spec-kit package source for uv
|
|
5
|
+
/** GitHub spec-kit package source for uv.
|
|
6
|
+
* TODO: pin to a specific tagged release (e.g. #v1.x.x) once one is available upstream.
|
|
7
|
+
* Tracking: https://github.com/github/spec-kit/releases
|
|
8
|
+
*/
|
|
6
9
|
const SPECKIT_FROM = 'git+https://github.com/github/spec-kit.git'
|
|
7
10
|
|
|
8
11
|
/**
|