@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.
- package/bin/dsh-subscriptions.mjs +7 -1
- package/lib/client.js +3 -2
- package/lib/routes/vault.js +1 -1
- package/lib/vault-snapshots.js +57 -4
- package/package.json +1 -1
|
@@ -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
|
|
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),
|
package/lib/routes/vault.js
CHANGED
|
@@ -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') {
|
package/lib/vault-snapshots.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
// #
|
|
2
|
-
//
|
|
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
|
-
|
|
17
|
-
|
|
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.
|
|
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",
|