@goodandready/dsh-subscriptions 0.7.8 → 0.7.9

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.
@@ -123,6 +123,11 @@ export function formatPacks(packs) {
123
123
  return JSON.stringify((Array.isArray(packs) ? packs : []).map(sanitizePack), null, 2) + '\\n'
124
124
  }
125
125
 
126
+ export function formatSnapshotStatus(payload) {
127
+ const stats = (payload && payload.stats) || {}
128
+ return 'snapshots: ' + (stats.count || 0) + ' bytes: ' + (stats.bytes || 0) + ' path: ' + (stats.path || '-')
129
+ }
130
+
126
131
  export function formatSnapshots(payload) {
127
132
  const rows = (payload && payload.snapshots) || []
128
133
  if (!rows.length) return 'no vault snapshots'
@@ -217,6 +222,7 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
217
222
  if (options.command === 'vault' && !options.json) {
218
223
  try {
219
224
  const payload = JSON.parse(text || '{}')
225
+ if (options.vaultAction === 'status') return { text: formatSnapshotStatus(payload), contentType: 'text/plain' }
220
226
  if (options.vaultAction === 'snapshots' || !options.vaultAction) return { text: formatSnapshots(payload), contentType: 'text/plain' }
221
227
  if (options.vaultAction === 'snapshot') return { text: 'created ' + (payload.snapshot && payload.snapshot.id), contentType: 'text/plain' }
222
228
  if (options.vaultAction === 'restore') return { text: 'restored ' + (payload.slotCount || 0) + ' slots', contentType: 'text/plain' }
@@ -237,7 +243,7 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
237
243
  async function main() {
238
244
  const result = await run()
239
245
  if (result.help) {
240
- process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|vault|doctor> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n packs\n packs add <file.json>\n vault snapshots | vault snapshot [label] | vault restore <id>\n doctor [--json]\n')
246
+ process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|vault|doctor> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n packs\n packs add <file.json>\n vault snapshots | vault snapshot [label] | vault restore <id> | vault status\n doctor [--json]\n')
241
247
  return
242
248
  }
243
249
  process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
package/lib/client.js CHANGED
@@ -3713,11 +3713,11 @@ window.__ModuleLoader__.load({
3713
3713
 
3714
3714
  // #736: vault snapshot list with create/restore. Payload stays server-side.
3715
3715
  function VaultSnapshotsPanel() {
3716
- const [state, setState] = React.useState({ snapshots: [], message: '', error: null, busy: false })
3716
+ const [state, setState] = React.useState({ snapshots: [], stats: null, message: '', error: null, busy: false })
3717
3717
  const pull = () => {
3718
3718
  fetch('/dsh-subscriptions/vault/snapshots', { cache: 'no-store' })
3719
3719
  .then((res) => res.json())
3720
- .then((data) => setState((st) => Object.assign({}, st, { snapshots: data.snapshots || [], error: null })))
3720
+ .then((data) => setState((st) => Object.assign({}, st, { snapshots: data.snapshots || [], stats: data.stats || null, error: null })))
3721
3721
  .catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
3722
3722
  }
3723
3723
  React.useEffect(() => { pull() }, [])
@@ -3745,6 +3745,7 @@ window.__ModuleLoader__.load({
3745
3745
  return h('div', { className: 'dsub-section-card' },
3746
3746
  h('div', { className: 'dsub-section-title' }, 'Vault snapshots', h('button', { type: 'button', className: 'dsub-mini', onClick: create, disabled: state.busy }, state.busy ? '…' : 'Create snapshot')),
3747
3747
  h('div', { className: 'dsub-section-desc' }, 'Encrypted slots snapshots. A pre-pull snapshot is created automatically. Payload stays on the host.'),
3748
+ state.stats ? h('div', { className: 'dsub-dim' }, state.stats.count + ' snapshots · ' + state.stats.bytes + ' bytes · ' + state.stats.path) : null,
3748
3749
  state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
3749
3750
  h('span', { className: 'dsub-h' }, snap.id),
3750
3751
  h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
@@ -15,7 +15,7 @@ export function registerVaultRoutes(ctx, state) {
15
15
  return
16
16
  }
17
17
  if (req.method === 'GET') {
18
- writeJson(res, 200, { ok: true, snapshots: defaultSnapshotStore.list() })
18
+ writeJson(res, 200, { ok: true, snapshots: defaultSnapshotStore.list(), stats: defaultSnapshotStore.stats() })
19
19
  return
20
20
  }
21
21
  if (req.method !== 'POST') {
@@ -1,7 +1,25 @@
1
- // #734: bounded in-memory snapshot ring. Only metadata leaves list(); the
2
- // encrypted payload is available through get(id) for an explicit restore.
1
+ // #739: durable, bounded snapshot ring. Encrypted payloads are written with
2
+ // 0600 permissions through a temp file + rename; list() only returns metadata.
3
+ import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { homedir } from 'node:os'
6
+
7
+ const FILE_NAME = 'vault-snapshots.json'
8
+ export const DEFAULT_MAX_BYTES = 2 * 1024 * 1024
9
+
10
+ function bytesOf(items) {
11
+ return Buffer.byteLength(JSON.stringify(items))
12
+ }
3
13
  let seq = 0
4
14
 
15
+ export function resolveSnapshotDir(customDir) {
16
+ if (customDir) return customDir
17
+ if (process.env.DSH_SNAPSHOT_DIR) return process.env.DSH_SNAPSHOT_DIR
18
+ if (process.env.DSH_STORAGE_DIR) return process.env.DSH_STORAGE_DIR
19
+ if (process.env.DSH_HOME) return join(process.env.DSH_HOME, 'storages', 'dsh-subscriptions')
20
+ return join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
21
+ }
22
+
5
23
  function meta(snapshot) {
6
24
  return {
7
25
  id: snapshot.id,
@@ -13,9 +31,36 @@ function meta(snapshot) {
13
31
  }
14
32
  }
15
33
 
16
- export function createSnapshotStore({ max = 10, now = () => Date.now() } = {}) {
17
- let items = []
34
+ function load(file) {
35
+ try {
36
+ const rows = JSON.parse(readFileSync(file, 'utf8'))
37
+ if (!Array.isArray(rows)) return []
38
+ return rows.filter((row) => row && typeof row === 'object' && typeof row.vault === 'string' && row.id)
39
+ } catch {
40
+ return []
41
+ }
42
+ }
43
+
44
+ function persist(dir, file, items) {
45
+ try {
46
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
47
+ const tmp = file + '.tmp-' + process.pid + '-' + Date.now()
48
+ writeFileSync(tmp, JSON.stringify(items), { mode: 0o600 })
49
+ renameSync(tmp, file)
50
+ try { chmodSync(file, 0o600) } catch { /* best-effort */ }
51
+ return true
52
+ } catch {
53
+ return false
54
+ }
55
+ }
56
+
57
+ export function createSnapshotStore({ max = 10, maxBytes = DEFAULT_MAX_BYTES, now = () => Date.now(), dir, persistToDisk = true } = {}) {
58
+ const snapshotDir = resolveSnapshotDir(dir)
59
+ const file = join(snapshotDir, FILE_NAME)
60
+ let items = persistToDisk ? load(file) : []
61
+ const save = () => { if (persistToDisk) persist(snapshotDir, file, items) }
18
62
  return {
63
+ path: file,
19
64
  add(snapshot, label = '') {
20
65
  const at = now()
21
66
  const entry = {
@@ -26,6 +71,8 @@ export function createSnapshotStore({ max = 10, now = () => Date.now() } = {}) {
26
71
  }
27
72
  items.unshift(entry)
28
73
  if (items.length > Math.max(1, max)) items = items.slice(0, Math.max(1, max))
74
+ while (items.length > 1 && bytesOf(items) > Math.max(1, maxBytes)) items.pop()
75
+ save()
29
76
  return meta(entry)
30
77
  },
31
78
  list() {
@@ -36,6 +83,12 @@ export function createSnapshotStore({ max = 10, now = () => Date.now() } = {}) {
36
83
  },
37
84
  clear() {
38
85
  items = []
86
+ save()
87
+ },
88
+ stats() {
89
+ let bytes = bytesOf(items)
90
+ try { bytes = Math.max(bytes, statSync(file).size) } catch { /* file may not exist yet */ }
91
+ return { count: items.length, bytes, path: file, maxBytes }
39
92
  },
40
93
  }
41
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
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",