@goodandready/dsh-subscriptions 0.6.29 → 0.6.31
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 +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +9 -7
- package/lib/crypto.js +35 -8
- package/lib/history.js +10 -4
- package/lib/import-auth.js +2 -21
- package/lib/index.js +3 -2
- package/lib/updater.js +96 -6
- package/lib/vault.js +3 -3
- package/package.json +2 -2
- package/lib/importers/coding-agents.js +0 -37
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ Supports Anthropic's adaptive thinking (`thinking: { type: "adaptive" }`) and re
|
|
|
262
262
|
|
|
263
263
|
## 🚀 One-Click Plugin Updater & Stability Hardening (Added in v0.6.9)
|
|
264
264
|
|
|
265
|
-
- **Host One-Click Updater**: Automatic in-place updater mounted at `/dsh-subscriptions/update`, checking the npm registry for newer releases and executing single-flight installation via the host DSH CLI (`dsh plugin add
|
|
265
|
+
- **Host One-Click Updater**: Automatic in-place updater mounted at `/dsh-subscriptions/update`, checking the npm registry for newer releases and executing single-flight installation via the host DSH CLI (`dsh plugin add`).
|
|
266
266
|
- **Security & Origin Protection**: Updater endpoints enforce strict loopback address checks (supporting IPv4 `127.0.0.1`, IPv6 `::1`, `localhost`), `x-dsh-plugin-update` header verification, and same-origin validation to prevent unauthorized updates.
|
|
267
267
|
- **Header Badge & UI Action**: The settings header bar displays current plugin version, an update warning badge when a new release is detected, and an instant "Update" button with live restart countdown.
|
|
268
268
|
- **Network Timeout Hardening**: Quota balance and smoke test probes now enforce guaranteed 15-second abort timeouts (`AbortSignal.timeout(15_000)`), preventing hanging sockets during vendor outages.
|
package/README.ru.md
CHANGED
|
@@ -235,7 +235,7 @@ dsh-subscriptions:
|
|
|
235
235
|
|
|
236
236
|
## 🚀 Обновление в один клик и повышение стабильности (Добавлено в v0.6.9)
|
|
237
237
|
|
|
238
|
-
- **Встроенный апдейтер в один клик**: Фоновая проверка новых версий в npm registry и обновление на месте по маршруту `/dsh-subscriptions/update` через хостовый CLI DSH (`dsh plugin add
|
|
238
|
+
- **Встроенный апдейтер в один клик**: Фоновая проверка новых версий в npm registry и обновление на месте по маршруту `/dsh-subscriptions/update` через хостовый CLI DSH (`dsh plugin add`).
|
|
239
239
|
- **Безопасность и защита эндпоинта**: Строгая верификация loopback-адресов (поддержка IPv4 `127.0.0.1`, IPv6 `::1`, `localhost`), валидация заголовка `x-dsh-plugin-update` и защита от CSRF/cross-origin запросов.
|
|
240
240
|
- **Индикатор в шапке и кнопка обновления**: В строке состояния панели отображается текущая версия, бейдж наличия обновления и кнопка быстрого обновления («Update → vX.Y.Z») с уведомлением о перезапуске службы.
|
|
241
241
|
- **Сетевые таймауты и устойчивость**: Ограничение времени ожидания запросов баланса и smoke-тестов до 15 секунд (`AbortSignal.timeout(15_000)`), предотвращающее зависание сокетов при недоступности внешних API.
|
package/README.zh.md
CHANGED
|
@@ -157,7 +157,7 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
|
|
|
157
157
|
|
|
158
158
|
## 🚀 一键插件自更新与稳定性强化 (v0.6.9 新增)
|
|
159
159
|
|
|
160
|
-
- **宿主端一键更新**: 挂载于 `/dsh-subscriptions/update` 的就地更新机制,自动对比 npm registry 最新版本并通过宿主 DSH CLI 执行单飞安装 (`dsh plugin add
|
|
160
|
+
- **宿主端一键更新**: 挂载于 `/dsh-subscriptions/update` 的就地更新机制,自动对比 npm registry 最新版本并通过宿主 DSH CLI 执行单飞安装 (`dsh plugin add`)。
|
|
161
161
|
- **安全与来源防护**: 更新接口严格校验环回地址(支持 IPv4 `127.0.0.1`、IPv6 `::1`、`localhost`)、`x-dsh-plugin-update` 请求头及同源策略,阻断未授权跨站调用。
|
|
162
162
|
- **状态栏徽章与更新按钮**: 设置面板顶部状态栏显示当前插件版本,有新版本时提示告警徽章并提供一键更新按钮与重启提示。
|
|
163
163
|
- **网络超时熔断**: 配额检测与冒烟测试增加 15 秒超时信号保护 (`AbortSignal.timeout(15_000)`),防止外部服务商接口故障导致请求挂起。
|
package/lib/client.js
CHANGED
|
@@ -299,12 +299,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
299
299
|
grok: {
|
|
300
300
|
title: "xAI Grok",
|
|
301
301
|
stepsEn: [
|
|
302
|
-
"1. Run grok
|
|
302
|
+
"1. Run grok CLI login on the server.",
|
|
303
303
|
"2. Click \"📥 From CLI\" (~/.grok/auth.json).",
|
|
304
304
|
"3. Or paste your xAI API Key and click \"Save token\".",
|
|
305
305
|
],
|
|
306
306
|
stepsZh: [
|
|
307
|
-
"1. 在服务器上运行 grok
|
|
307
|
+
"1. 在服务器上运行 grok CLI 登录。",
|
|
308
308
|
"2. 点击 \"📥 从 CLI 导入\" (~/.grok/auth.json)。",
|
|
309
309
|
"3. 或将 xAI API 密钥粘贴到输入框并点击 \"保存令牌\"。",
|
|
310
310
|
],
|
|
@@ -529,8 +529,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
529
529
|
"backupImportTitle": "Import Encrypted Backup",
|
|
530
530
|
"backupExportAction": "Export",
|
|
531
531
|
"backupImportAction": "Import",
|
|
532
|
-
"backupPassphrase": "Vault Passphrase (min
|
|
533
|
-
"backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM
|
|
532
|
+
"backupPassphrase": "Vault Passphrase (min 12 chars)",
|
|
533
|
+
"backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM. ⚠️ Contains sensitive OAuth refresh tokens; use a strong password (min 12 chars).",
|
|
534
|
+
"vaultTokenWarning": "⚠️ Contains sensitive OAuth refresh tokens",
|
|
534
535
|
"backupImportHint": "Restore encrypted subscription accounts and credentials from a vault payload.",
|
|
535
536
|
"backupPayloadPlaceholder": "Paste DSHE... encrypted string here",
|
|
536
537
|
"backupExportOk": "Backup exported successfully",
|
|
@@ -727,8 +728,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
727
728
|
"backupImportTitle": "导入加密备份",
|
|
728
729
|
"backupExportAction": "导出",
|
|
729
730
|
"backupImportAction": "导入",
|
|
730
|
-
"backupPassphrase": "凭据库密码(至少
|
|
731
|
-
"backupExportHint": "使用 AES-256-GCM
|
|
731
|
+
"backupPassphrase": "凭据库密码(至少 12 位)",
|
|
732
|
+
"backupExportHint": "使用 AES-256-GCM 安全加密导出所有订阅账户与配置。⚠️ 包含敏感的 OAuth 刷新令牌,请使用至少 12 位的强密码保护。",
|
|
733
|
+
"vaultTokenWarning": "⚠️ 包含敏感的 OAuth 刷新令牌",
|
|
732
734
|
"backupImportHint": "从已加密的备份内容中导入并恢复订阅账户与凭据。",
|
|
733
735
|
"backupPayloadPlaceholder": "在此粘贴 DSHE... 加密内容",
|
|
734
736
|
"backupExportOk": "备份导出成功",
|
|
@@ -1676,7 +1678,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1676
1678
|
backupModal === 'export' ? React.createElement('button', {
|
|
1677
1679
|
type: 'button',
|
|
1678
1680
|
className: 'dsub-mini',
|
|
1679
|
-
disabled: backupLoading || !backupPass || backupPass.length <
|
|
1681
|
+
disabled: backupLoading || !backupPass || backupPass.length < 12,
|
|
1680
1682
|
onClick: doExportBackup,
|
|
1681
1683
|
}, backupLoading ? '…' : t('backupExportAction')) : React.createElement('button', {
|
|
1682
1684
|
type: 'button',
|
package/lib/crypto.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { scryptSync, pbkdf2Sync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
|
|
2
2
|
|
|
3
|
-
// AES-256-GCM with PBKDF2 (
|
|
4
|
-
//
|
|
3
|
+
// AES-256-GCM with PBKDF2 (600,000 iterations, SHA-256) (DSHE2:),
|
|
4
|
+
// hardened scrypt (N=131072) (DSHE3:), or legacy scrypt (DSHE1:).
|
|
5
|
+
// Backward-compatible across all key derivation algorithms.
|
|
5
6
|
export function encryptWithPassphrase(plainText, passphrase, { algorithm = "scrypt" } = {}) {
|
|
6
7
|
const salt = randomBytes(16)
|
|
7
8
|
const isPbkdf2 = algorithm === "pbkdf2"
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const isHardenedScrypt = algorithm === "scrypt-v2" || algorithm === "scrypt-hardened"
|
|
10
|
+
let key
|
|
11
|
+
let prefix
|
|
12
|
+
|
|
13
|
+
if (isPbkdf2) {
|
|
14
|
+
key = pbkdf2Sync(String(passphrase), salt, 600000, 32, "sha256")
|
|
15
|
+
prefix = "DSHE2:"
|
|
16
|
+
} else if (isHardenedScrypt) {
|
|
17
|
+
key = scryptSync(String(passphrase), salt, 32, { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 })
|
|
18
|
+
prefix = "DSHE3:"
|
|
19
|
+
} else {
|
|
20
|
+
key = scryptSync(String(passphrase), salt, 32)
|
|
21
|
+
prefix = "DSHE1:"
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
const iv = randomBytes(12)
|
|
12
25
|
const cipher = createCipheriv("aes-256-gcm", key, iv)
|
|
13
26
|
const encrypted = Buffer.concat([cipher.update(String(plainText), "utf8"), cipher.final()])
|
|
14
27
|
const tag = cipher.getAuthTag()
|
|
15
28
|
// format: magic + salt + iv + tag + data
|
|
16
|
-
const prefix = isPbkdf2 ? "DSHE2:" : "DSHE1:"
|
|
17
29
|
return prefix + Buffer.concat([salt, iv, tag, encrypted]).toString("base64")
|
|
18
30
|
}
|
|
19
31
|
|
|
@@ -24,13 +36,28 @@ export function decryptWithPassphrase(payload, passphrase) {
|
|
|
24
36
|
let iv
|
|
25
37
|
let tag
|
|
26
38
|
let data
|
|
27
|
-
if (raw.startsWith("
|
|
39
|
+
if (raw.startsWith("DSHE3:")) {
|
|
40
|
+
const buf = Buffer.from(raw.slice(6), "base64")
|
|
41
|
+
salt = buf.subarray(0, 16)
|
|
42
|
+
iv = buf.subarray(16, 28)
|
|
43
|
+
tag = buf.subarray(28, 44)
|
|
44
|
+
data = buf.subarray(44)
|
|
45
|
+
key = scryptSync(String(passphrase), salt, 32, { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 })
|
|
46
|
+
} else if (raw.startsWith("DSHE2:")) {
|
|
28
47
|
const buf = Buffer.from(raw.slice(6), "base64")
|
|
29
48
|
salt = buf.subarray(0, 16)
|
|
30
49
|
iv = buf.subarray(16, 28)
|
|
31
50
|
tag = buf.subarray(28, 44)
|
|
32
51
|
data = buf.subarray(44)
|
|
33
|
-
|
|
52
|
+
// Try 600,000 iterations first (#414), fallback to legacy 100,000 iterations
|
|
53
|
+
try {
|
|
54
|
+
key = pbkdf2Sync(String(passphrase), salt, 600000, 32, "sha256")
|
|
55
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv)
|
|
56
|
+
decipher.setAuthTag(tag)
|
|
57
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8")
|
|
58
|
+
} catch {
|
|
59
|
+
key = pbkdf2Sync(String(passphrase), salt, 100000, 32, "sha256")
|
|
60
|
+
}
|
|
34
61
|
} else if (raw.startsWith("DSHE1:")) {
|
|
35
62
|
const buf = Buffer.from(raw.slice(6), "base64")
|
|
36
63
|
salt = buf.subarray(0, 16)
|
package/lib/history.js
CHANGED
|
@@ -6,15 +6,21 @@ import { homedir } from 'node:os'
|
|
|
6
6
|
// ~/.dsh/storages/dsh-subscriptions/history.json (JSON array, newest first).
|
|
7
7
|
// All IO is best-effort: a write failure must never crash the harness.
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function resolveHistoryDir(customDir) {
|
|
10
|
+
if (customDir) return customDir
|
|
11
|
+
if (process.env.DSH_HISTORY_DIR) return process.env.DSH_HISTORY_DIR
|
|
12
|
+
if (process.env.DSH_STORAGE_DIR) return process.env.DSH_STORAGE_DIR
|
|
13
|
+
if (process.env.DSH_HOME) return join(process.env.DSH_HOME, 'storages', 'dsh-subscriptions')
|
|
14
|
+
return join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
export class HistoryStore {
|
|
12
|
-
constructor(dir =
|
|
13
|
-
this.dir = dir
|
|
18
|
+
constructor(dir = resolveHistoryDir(), ttlMs = 7 * 24 * 60 * 60 * 1000, debounceMs = 0, maxEntries = 1000) {
|
|
19
|
+
this.dir = resolveHistoryDir(dir)
|
|
14
20
|
this.ttlMs = ttlMs
|
|
15
21
|
this.debounceMs = debounceMs
|
|
16
22
|
this.maxEntries = maxEntries
|
|
17
|
-
this.path = join(dir, 'history.json')
|
|
23
|
+
this.path = join(this.dir, 'history.json')
|
|
18
24
|
this.rows = []
|
|
19
25
|
this._persistTimer = null
|
|
20
26
|
this._load()
|
package/lib/import-auth.js
CHANGED
|
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { readClaudeCredentials } from './vendors/claude-cli.js'
|
|
5
|
-
import { discoverCodingAgentConfigs } from './importers/coding-agents.js'
|
|
6
5
|
|
|
7
6
|
// Test seam: a fixture root overrides the real home for the duration of a
|
|
8
7
|
// public call. Undefined (the default) keeps production behaviour.
|
|
@@ -46,10 +45,9 @@ async function discoverLocalCliSessionsInner() {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
// 2. Grok CLI
|
|
48
|
+
// 2. Grok CLI
|
|
50
49
|
const grokPaths = [
|
|
51
50
|
homeFile('.grok', 'auth.json'),
|
|
52
|
-
homeFile('.hermes', 'auth.json'),
|
|
53
51
|
]
|
|
54
52
|
for (const p of grokPaths) {
|
|
55
53
|
const grok = await readJson(p)
|
|
@@ -198,24 +196,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
198
196
|
}
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
const agentConfigs = await discoverCodingAgentConfigs()
|
|
203
|
-
if (agentConfigs.aider) {
|
|
204
|
-
detected.aider = {
|
|
205
|
-
provider: 'aider',
|
|
206
|
-
path: agentConfigs.aider.path,
|
|
207
|
-
email: 'Aider Configured Agent',
|
|
208
|
-
hasRefreshToken: false,
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (agentConfigs.roocode) {
|
|
212
|
-
detected.roocode = {
|
|
213
|
-
provider: 'roocode',
|
|
214
|
-
path: agentConfigs.roocode.path,
|
|
215
|
-
email: 'Roo-Code Configured Agent',
|
|
216
|
-
hasRefreshToken: false,
|
|
217
|
-
}
|
|
218
|
-
}
|
|
199
|
+
|
|
219
200
|
|
|
220
201
|
return detected
|
|
221
202
|
}
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.j
|
|
|
16
16
|
import { createResetCreditService } from './reset-credits.js'
|
|
17
17
|
import { maskEmail, maskLabel } from './mask.js'
|
|
18
18
|
import { proxyFetch } from './proxy.js'
|
|
19
|
-
import { HistoryStore,
|
|
19
|
+
import { HistoryStore, resolveHistoryDir } from './history.js'
|
|
20
20
|
import { createDiagnosticsReport } from './diagnostics.js'
|
|
21
21
|
import { createAccountsView } from './accounts-view.js'
|
|
22
22
|
import { createAdapterManager } from './adapter-manager.js'
|
|
@@ -185,7 +185,8 @@ export function apply(ctx, rawConfig) {
|
|
|
185
185
|
bestEffort('emit limit-notice', () => ctx.emit && ctx.emit('subscriptions.limit-notice', { provider, ref, window: win.id, usedPercent: win.usedPercent, threshold }), undefined, logger)
|
|
186
186
|
},
|
|
187
187
|
})
|
|
188
|
-
const
|
|
188
|
+
const historyDir = ctx?.historyDir || ctx?.storageDir || resolveHistoryDir()
|
|
189
|
+
const history = new HistoryStore(historyDir, 7 * 24 * 60 * 60 * 1000, 1000)
|
|
189
190
|
if (typeof ctx?.on === 'function') {
|
|
190
191
|
ctx.on('dispose', () => history.dispose())
|
|
191
192
|
}
|
package/lib/updater.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { existsSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
5
|
import { basename, dirname, isAbsolute, resolve } from 'node:path'
|
|
@@ -162,12 +162,82 @@ export async function getUpdaterStatus(options, target = getRuntime()) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
export
|
|
165
|
+
export function readLockPid(lockPath) {
|
|
166
|
+
try {
|
|
167
|
+
if (!existsSync(lockPath)) return undefined
|
|
168
|
+
const stat = statSync(lockPath)
|
|
169
|
+
if (stat.isDirectory()) return undefined
|
|
170
|
+
const content = readFileSync(lockPath, "utf8").trim()
|
|
171
|
+
if (!content) return undefined
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(content)
|
|
174
|
+
if (parsed && typeof parsed.pid === "number") return parsed.pid
|
|
175
|
+
} catch {}
|
|
176
|
+
const num = parseInt(content, 10)
|
|
177
|
+
if (!isNaN(num) && num > 0) return num
|
|
178
|
+
const match = /\b(\d{1,8})\b/.exec(content)
|
|
179
|
+
if (match) return Number(match[1])
|
|
180
|
+
return undefined
|
|
181
|
+
} catch {
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function isPidAlive(pid) {
|
|
187
|
+
if (typeof pid !== "number" || isNaN(pid) || pid <= 0) return false
|
|
188
|
+
try {
|
|
189
|
+
process.kill(pid, 0)
|
|
190
|
+
return true
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return err?.code === "EPERM"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function checkAndCleanLock(profileDir, childPid) {
|
|
197
|
+
const lockPath = resolve(profileDir, "package.json.lock")
|
|
198
|
+
if (!existsSync(lockPath)) return { locked: false }
|
|
199
|
+
const pid = readLockPid(lockPath)
|
|
200
|
+
if (pid !== undefined) {
|
|
201
|
+
if (isPidAlive(pid)) {
|
|
202
|
+
if (childPid !== undefined && pid === childPid) {
|
|
203
|
+
try {
|
|
204
|
+
rmSync(lockPath, { recursive: true, force: true })
|
|
205
|
+
return { locked: false, cleaned: true }
|
|
206
|
+
} catch {
|
|
207
|
+
return { locked: true, pid }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { locked: true, pid }
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
rmSync(lockPath, { recursive: true, force: true })
|
|
214
|
+
return { locked: false, cleaned: true }
|
|
215
|
+
} catch {
|
|
216
|
+
return { locked: true, pid }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (childPid !== undefined) {
|
|
220
|
+
try {
|
|
221
|
+
rmSync(lockPath, { recursive: true, force: true })
|
|
222
|
+
return { locked: false, cleaned: true }
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
return { locked: true }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function installExactPackage(target, packageSpec, options = {}) {
|
|
166
229
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
|
|
230
|
+
const lockStatus = checkAndCleanLock(target.profileDir)
|
|
231
|
+
if (lockStatus.locked) {
|
|
232
|
+
const error = new Error('Another plugin installation is currently in progress.')
|
|
233
|
+
error.status = 409
|
|
234
|
+
error.code = 'ELOCKED'
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
167
237
|
await new Promise((resolvePromise, reject) => {
|
|
168
238
|
const child = spawn(process.execPath, [
|
|
169
239
|
target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
|
|
170
|
-
|
|
240
|
+
packageSpec,
|
|
171
241
|
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
172
242
|
], {
|
|
173
243
|
cwd: target.profileDir,
|
|
@@ -178,13 +248,24 @@ export async function installExactPackage(target, packageSpec, options) {
|
|
|
178
248
|
let detail = ''
|
|
179
249
|
child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
|
|
180
250
|
child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
|
|
251
|
+
let timedOut = false
|
|
252
|
+
const timeoutMs = options.timeoutMs ?? UPDATE_TIMEOUT_MS
|
|
181
253
|
const timer = setTimeout(() => {
|
|
182
|
-
|
|
254
|
+
timedOut = true
|
|
255
|
+
try { child.kill('SIGTERM') } catch {}
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
try { child.kill('SIGKILL') } catch {}
|
|
258
|
+
checkAndCleanLock(target.profileDir, child.pid)
|
|
259
|
+
}, 500).unref?.()
|
|
183
260
|
reject(new Error('Update timed out; use the normal DSH update flow.'))
|
|
184
|
-
},
|
|
261
|
+
}, timeoutMs)
|
|
185
262
|
child.once('error', error => { clearTimeout(timer); reject(error) })
|
|
186
|
-
child.once('exit', code => {
|
|
263
|
+
child.once('exit', (code) => {
|
|
187
264
|
clearTimeout(timer)
|
|
265
|
+
if (timedOut) {
|
|
266
|
+
checkAndCleanLock(target.profileDir, child.pid)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
188
269
|
if (code === 0) resolvePromise()
|
|
189
270
|
else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
|
|
190
271
|
})
|
|
@@ -218,6 +299,11 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
218
299
|
writeJson(response, 409, { ok: false, error: { code: 'busy', message: 'This plugin is already updating.' } })
|
|
219
300
|
return
|
|
220
301
|
}
|
|
302
|
+
const profileLock = checkAndCleanLock(target.profileDir)
|
|
303
|
+
if (profileLock.locked) {
|
|
304
|
+
writeJson(response, 409, { ok: false, error: { code: 'busy', message: 'Another plugin installation is currently in progress in this profile.' } })
|
|
305
|
+
return
|
|
306
|
+
}
|
|
221
307
|
installing = true
|
|
222
308
|
try {
|
|
223
309
|
const before = await getUpdaterStatus(options, target)
|
|
@@ -240,6 +326,10 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
240
326
|
installing = false
|
|
241
327
|
}
|
|
242
328
|
} catch (error) {
|
|
329
|
+
if (error?.status === 409 || error?.code === 'ELOCKED') {
|
|
330
|
+
writeJson(response, 409, { ok: false, error: { code: 'busy', message: error.message || 'Another plugin installation is currently in progress.' } })
|
|
331
|
+
return
|
|
332
|
+
}
|
|
243
333
|
ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`)
|
|
244
334
|
writeJson(response, 503, { ok: false, error: { code: 'update_failed', message: String(error?.message || error) } })
|
|
245
335
|
}
|
package/lib/vault.js
CHANGED
|
@@ -11,8 +11,8 @@ export const VAULT_FORMAT = 'dsh-subscriptions-vault-v1'
|
|
|
11
11
|
*/
|
|
12
12
|
export function exportVault({ slots = [], blobs = {}, exportedBy = 'dsh-subscriptions', algorithm = 'scrypt' }, passphrase) {
|
|
13
13
|
const pass = String(passphrase || '').trim()
|
|
14
|
-
if (!pass || pass.length <
|
|
15
|
-
throw new Error('passphrase must be at least
|
|
14
|
+
if (!pass || pass.length < 12) {
|
|
15
|
+
throw new Error('passphrase must be at least 12 characters')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const payload = {
|
|
@@ -43,7 +43,7 @@ export function importVault(encryptedPayload, passphrase) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const raw = String(encryptedPayload || '').trim()
|
|
46
|
-
if (!raw.startsWith('DSHE1:') && !raw.startsWith('DSHE2:')) {
|
|
46
|
+
if (!raw.startsWith('DSHE1:') && !raw.startsWith('DSHE2:') && !raw.startsWith('DSHE3:')) {
|
|
47
47
|
throw new Error('invalid vault format: missing DSHE1 header')
|
|
48
48
|
}
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.31",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"test": "eslint lib/ test/ && node --test test/*.test.mjs",
|
|
40
|
+
"test": "eslint lib/ test/ && node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
41
41
|
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
42
42
|
},
|
|
43
43
|
"dsh": {
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
export async function discoverCodingAgentConfigs() {
|
|
6
|
-
const configs = {}
|
|
7
|
-
|
|
8
|
-
// 1. Aider (.aider.conf.yml)
|
|
9
|
-
try {
|
|
10
|
-
const p = join(homedir(), '.aider.conf.yml')
|
|
11
|
-
const text = await readFile(p, 'utf8')
|
|
12
|
-
configs.aider = {
|
|
13
|
-
name: 'Aider Config',
|
|
14
|
-
path: p,
|
|
15
|
-
found: true,
|
|
16
|
-
raw: text
|
|
17
|
-
}
|
|
18
|
-
} catch {
|
|
19
|
-
// not present
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 2. Roo-Code / Roo-Cline settings
|
|
23
|
-
try {
|
|
24
|
-
const p = join(homedir(), '.roo-code', 'settings.json')
|
|
25
|
-
const text = await readFile(p, 'utf8')
|
|
26
|
-
configs.roocode = {
|
|
27
|
-
name: 'Roo-Code Config',
|
|
28
|
-
path: p,
|
|
29
|
-
found: true,
|
|
30
|
-
raw: text
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
// not present
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return configs
|
|
37
|
-
}
|