@goodandready/dsh-subscriptions 0.6.30 → 0.6.32

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 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 --config.minimumReleaseAge=0`).
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 --config.minimumReleaseAge=0`).
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 --config.minimumReleaseAge=0`)。
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/index.js CHANGED
@@ -65,7 +65,7 @@ const OK_HTML = `<!doctype html>
65
65
  </html>`
66
66
 
67
67
  export function apply(ctx, rawConfig) {
68
- const logger = ctx.logger ? ctx.logger('subscriptions') : undefined
68
+ const logger = typeof ctx.logger === 'function' ? ctx.logger('subscriptions') : (ctx.logger || ctx.log)
69
69
  let settingsApi
70
70
 
71
71
  let cachedConfig = Config(rawConfig || {})
@@ -185,7 +185,7 @@ 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 historyDir = ctx?.historyDir || ctx?.storageDir || resolveHistoryDir()
188
+ const historyDir = resolveHistoryDir()
189
189
  const history = new HistoryStore(historyDir, 7 * 24 * 60 * 60 * 1000, 1000)
190
190
  if (typeof ctx?.on === 'function') {
191
191
  ctx.on('dispose', () => history.dispose())
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 async function installExactPackage(target, packageSpec, options) {
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
- '--config.minimumReleaseAge=0', packageSpec,
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
- child.kill()
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
- }, UPDATE_TIMEOUT_MS)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.30",
3
+ "version": "0.6.32",
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",