@goodandready/dsh-clinebot 0.3.10 → 0.3.11

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/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.11] - 2026-09-16
9
+
10
+ ### Added
11
+ - **One-Click In-App Update UI & Live Status Banner** (Gitea Issue #39): Rendered interactive update status bar in `PluginCard` / `SettingsPage`. Displays current installed version, live npm registry check indicator, and up-to-date status badge. Automatically reveals warning badge and 'Update Now' button (`update.btn`) when a newer version is released, triggering safe POST `/dsh-clinebot/update` with `x-dsh-plugin-update: 1` header and completion guidance.
12
+ - **Client Test Coverage Expansion**: Added component render assertions for one-click update UI elements and updated test state table to 41 passing unit tests.
13
+
8
14
  ## [0.3.10] - 2026-09-16
9
15
 
10
16
  ### Changed
package/lib/client.js CHANGED
@@ -94,6 +94,8 @@ const en = {
94
94
  'update.updating': 'Updating…',
95
95
  'update.done': 'Successfully updated to v{version}! Please restart DSH.',
96
96
  'update.failed': 'Update failed: {error}',
97
+ 'update.checking': 'Checking for updates…',
98
+ 'update.up_to_date': 'Up to date',
97
99
  }
98
100
 
99
101
  const zh = {
@@ -181,6 +183,8 @@ const en = {
181
183
  'update.updating': '正在更新…',
182
184
  'update.done': '更新成功至 v{version}!请重启 DSH 服务生效。',
183
185
  'update.failed': '更新失败:{error}',
186
+ 'update.checking': '正在检查更新…',
187
+ 'update.up_to_date': '已是最新版本',
184
188
  }
185
189
 
186
190
  function makeT(dict, fallback) {
@@ -418,6 +422,18 @@ const en = {
418
422
  const [msg, setMsg] = React.useState('')
419
423
  const [smokeResult, setSmokeResult] = React.useState(null)
420
424
 
425
+ // Plugin in-app updater state
426
+ const [updateState, setUpdateState] = React.useState({
427
+ checking: false,
428
+ updating: false,
429
+ currentVersion: '0.3.10',
430
+ latestVersion: '',
431
+ updateAvailable: false,
432
+ canAutoUpdate: true,
433
+ error: '',
434
+ notice: '',
435
+ })
436
+
421
437
  // Key input state
422
438
  const [apiKeyInput, setApiKeyInput] = React.useState('')
423
439
  const [showKey, setShowKey] = React.useState(false)
@@ -439,6 +455,58 @@ const en = {
439
455
  load().catch((e) => setErr(String(e.message || e)))
440
456
  }, [load])
441
457
 
458
+ const checkUpdate = React.useCallback(async () => {
459
+ setUpdateState((s) => ({ ...s, checking: true, error: '' }))
460
+ try {
461
+ const res = await fetch(`${ROUTE_PREFIX}/update`)
462
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
463
+ const data = await res.json().catch(() => ({}))
464
+ setUpdateState((s) => ({
465
+ ...s,
466
+ checking: false,
467
+ currentVersion: data.currentVersion || s.currentVersion,
468
+ latestVersion: data.latestVersion || '',
469
+ updateAvailable: !!data.updateAvailable,
470
+ canAutoUpdate: data.canAutoUpdate !== false,
471
+ }))
472
+ } catch (_) {
473
+ setUpdateState((s) => ({ ...s, checking: false }))
474
+ }
475
+ }, [])
476
+
477
+ React.useEffect(() => {
478
+ checkUpdate()
479
+ }, [checkUpdate])
480
+
481
+ async function handleTriggerUpdate() {
482
+ if (updateState.updating) return
483
+ setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
484
+ try {
485
+ const res = await fetch(`${ROUTE_PREFIX}/update`, {
486
+ method: 'POST',
487
+ headers: { 'x-dsh-plugin-update': '1' },
488
+ })
489
+ const data = await res.json().catch(() => ({}))
490
+ if (!res.ok || data.ok === false || data.error) {
491
+ throw new Error(data.error || `HTTP ${res.status}`)
492
+ }
493
+ const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion
494
+ setUpdateState((s) => ({
495
+ ...s,
496
+ updating: false,
497
+ updateAvailable: false,
498
+ currentVersion: newVer,
499
+ notice: t('update.done', { version: newVer }),
500
+ }))
501
+ } catch (e) {
502
+ setUpdateState((s) => ({
503
+ ...s,
504
+ updating: false,
505
+ error: t('update.failed', { error: e.message || String(e) }),
506
+ }))
507
+ }
508
+ }
509
+
442
510
  React.useEffect(() => {
443
511
  if (snapshotStatus === 'ready' && snapshot?.value && typeof snapshot.value === 'object') {
444
512
  setDraft((prev) => (prev ? { ...prev, ...snapshot.value } : snapshot.value))
@@ -772,6 +840,60 @@ const en = {
772
840
  )
773
841
  ),
774
842
 
843
+ // In-app Update Bar
844
+ React.createElement(
845
+ 'div',
846
+ {
847
+ className: 'cb-row',
848
+ style: {
849
+ padding: '10px 14px',
850
+ borderRadius: '8px',
851
+ border: '1px solid var(--dsw-alias-border-l2)',
852
+ background: 'var(--dsw-alias-bg-layer-2)',
853
+ justifyContent: 'space-between',
854
+ }
855
+ },
856
+ React.createElement(
857
+ 'div',
858
+ { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' } },
859
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
860
+ `v${updateState.currentVersion}`
861
+ ),
862
+ updateState.checking
863
+ ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
864
+ t('update.checking')
865
+ )
866
+ : updateState.updateAvailable
867
+ ? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
868
+ t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
869
+ )
870
+ : React.createElement('span', { className: 'cb-badge cb-badge-ok' },
871
+ '✓ ' + t('update.up_to_date')
872
+ )
873
+ ),
874
+ updateState.updateAvailable
875
+ ? React.createElement(
876
+ 'button',
877
+ {
878
+ type: 'button',
879
+ className: 'cb-btn cb-btn-primary',
880
+ disabled: updateState.updating,
881
+ onClick: handleTriggerUpdate,
882
+ style: { padding: '5px 12px', fontSize: '12px' }
883
+ },
884
+ updateState.updating ? t('update.updating') : t('update.btn')
885
+ )
886
+ : null
887
+ ),
888
+
889
+ // Update notice & error banners
890
+ updateState.notice
891
+ ? React.createElement('div', { className: 'cb-alert-ok' }, '✓ ' + updateState.notice)
892
+ : null,
893
+ updateState.error
894
+ ? React.createElement('div', { className: 'cb-alert-err' }, updateState.error)
895
+ : null,
896
+
775
897
  // Notifications
776
898
  err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
777
899
  msg ? React.createElement('div', { className: 'cb-alert-ok' }, msg) : null,
@@ -814,7 +936,7 @@ const en = {
814
936
  {
815
937
  type: 'button',
816
938
  className: 'cb-btn cb-btn-primary',
817
- disabled: !!busy || !apiKeyInput.trim(),
939
+ disabled: !!busy || !String(apiKeyInput || '').trim(),
818
940
  onClick: handleSaveKey,
819
941
  },
820
942
  busy === 'save-key' ? t('key.saving') : t('key.save')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
5
5
  "license": "MIT",
6
6
  "type": "module",