ds-bot 0.1.0 → 0.1.1

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.
@@ -7,10 +7,10 @@ import { join } from 'node:path'
7
7
 
8
8
  const PKG = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
9
9
  export const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/'
10
- const MIRROR_REGISTRY = 'https://registry.npmmirror.com/'
10
+ export const MIRROR_REGISTRY = 'https://registry.npmmirror.com/'
11
11
  // Every window that opens asks; within this long the last answer stands.
12
- const CHECK_TTL_MS = 10 * 60_000
13
- const FETCH_TIMEOUT_MS = 8000
12
+ export const CHECK_TTL_MS = 10 * 60_000
13
+ export const FETCH_TIMEOUT_MS = 8000
14
14
  // The first check waits until the host has settled its own start.
15
15
  const STARTUP_DELAY_MS = 15_000
16
16
  const VERSION = /^v?(\d+\.\d+\.\d+(?:-[\w.]+)?)(?:\+[\w.]+)?$/
@@ -60,10 +60,47 @@ export function sourceOf(spec) {
60
60
  }
61
61
 
62
62
  const normalizeRegistry = url => `${String(url).trim().replace(/\/+$/, '')}/`
63
- const encodeName = name => (name.startsWith('@') ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name))
64
- // The project page of the releases, for the "what's new" link.
63
+ export const encodeName = name => (name.startsWith('@') ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name))
64
+
65
+ export const fetchJson = async (url, headers = {}) => {
66
+ const response = await fetch(url, { headers: { accept: 'application/json', ...headers }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
67
+ if (!response.ok) throw new Error(`${url} answered ${response.status}`)
68
+ return response.json()
69
+ }
70
+
71
+ // The registries the plugin manager installs from, in its order: its configured
72
+ // first one (else pnpm's own), then the fallbacks, unless that first one is private.
73
+ export async function registryPlan(config, manager) {
74
+ if (typeof config.updateRegistry === 'string' && config.updateRegistry !== '') return [normalizeRegistry(config.updateRegistry)]
75
+ const configured = await Promise.resolve(manager?.registries?.()).catch(() => null)
76
+ if (!configured) return [OFFICIAL_REGISTRY, MIRROR_REGISTRY]
77
+ const fallbacks = (configured.fallbackRegistries ?? []).map(normalizeRegistry)
78
+ const own = configured.resolved ? normalizeRegistry(configured.resolved) : null
79
+ const first = configured.registry ? normalizeRegistry(configured.registry) : own ?? OFFICIAL_REGISTRY
80
+ const isPublic = first === OFFICIAL_REGISTRY || fallbacks.includes(first)
81
+ return isPublic ? [...new Set([first, ...fallbacks])] : [first]
82
+ }
83
+
84
+ // The plugin manager's `source` for this package; DSH releases before it reported
85
+ // one leave the profile's own dependency entry to say the same.
86
+ export async function installSpecOf(ctx) {
87
+ const manager = ctx.get?.('pluginManager')
88
+ const bundles = await Promise.resolve(manager?.listBundles?.()).catch(() => null)
89
+ const own = Array.isArray(bundles) ? bundles.find(bundle => bundle.name === PKG.name) : undefined
90
+ if (typeof own?.source === 'string') return own.source
91
+ const dir = ctx.get?.('profileContext')?.dir
92
+ if (typeof dir !== 'string') return undefined
93
+ try {
94
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies?.[PKG.name]
95
+ } catch { return undefined }
96
+ }
97
+
98
+ // The public repository the releases live in, for tags and the "what's new" link.
99
+ export const repoSlug = () =>
100
+ /github\.com[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/i.exec(String(PKG.repository?.url ?? PKG.repository ?? ''))?.[1] ?? null
101
+
65
102
  const releasesUrl = () => {
66
- const repo = /github\.com[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/i.exec(String(PKG.repository?.url ?? PKG.repository ?? ''))?.[1]
103
+ const repo = repoSlug()
67
104
  return repo ? `https://github.com/${repo}/releases` : (PKG.homepage ?? null)
68
105
  }
69
106
 
@@ -86,27 +123,8 @@ export function install(rt) {
86
123
  available: status.phase !== 'installed' && target !== null && status.latest !== null && newer(status.latest, status.current),
87
124
  })
88
125
 
89
- const fetchJson = async (url, headers = {}) => {
90
- const response = await fetch(url, { headers: { accept: 'application/json', ...headers }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
91
- if (!response.ok) throw new Error(`${url} answered ${response.status}`)
92
- return response.json()
93
- }
94
-
95
- // The registries the plugin manager installs from, in its order: its configured
96
- // first one (else pnpm's own), then the fallbacks, unless that first one is private.
97
- async function registryPlan() {
98
- if (typeof config.updateRegistry === 'string' && config.updateRegistry !== '') return [normalizeRegistry(config.updateRegistry)]
99
- const configured = await Promise.resolve(manager()?.registries?.()).catch(() => null)
100
- if (!configured) return [OFFICIAL_REGISTRY, MIRROR_REGISTRY]
101
- const fallbacks = (configured.fallbackRegistries ?? []).map(normalizeRegistry)
102
- const own = configured.resolved ? normalizeRegistry(configured.resolved) : null
103
- const first = configured.registry ? normalizeRegistry(configured.registry) : own ?? OFFICIAL_REGISTRY
104
- const isPublic = first === OFFICIAL_REGISTRY || fallbacks.includes(first)
105
- return isPublic ? [...new Set([first, ...fallbacks])] : [first]
106
- }
107
-
108
126
  async function newestFromRegistry() {
109
- for (const registry of await registryPlan()) {
127
+ for (const registry of await registryPlan(config, manager())) {
110
128
  try {
111
129
  const version = cleanVersion((await fetchJson(`${registry}${encodeName(PKG.name)}/latest`)).version)
112
130
  if (version) return { version, spec: `${PKG.name}@${version}`, registry }
@@ -124,21 +142,8 @@ export function install(rt) {
124
142
  return { version, spec: `github:${source.owner}/${source.repo}#${fragment}` }
125
143
  }
126
144
 
127
- // The plugin manager's `source` for this package; DSH releases before it reported
128
- // one leave the profile's own dependency entry to say the same.
129
- async function installSpec() {
130
- const bundles = await Promise.resolve(manager()?.listBundles?.()).catch(() => null)
131
- const own = Array.isArray(bundles) ? bundles.find(bundle => bundle.name === PKG.name) : undefined
132
- if (typeof own?.source === 'string') return own.source
133
- const dir = ctx.get?.('profileContext')?.dir
134
- if (typeof dir !== 'string') return undefined
135
- try {
136
- return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies?.[PKG.name]
137
- } catch { return undefined }
138
- }
139
-
140
145
  async function runCheck() {
141
- const source = sourceOf(await installSpec())
146
+ const source = sourceOf(await installSpecOf(ctx))
142
147
  status.source = source.kind
143
148
  let found = null
144
149
  try {
package/src/host/usage.js CHANGED
@@ -320,7 +320,18 @@ export function install(rt) {
320
320
  ...Object.values(room.segments ?? {}).flat().map(segment => segment.sessionId),
321
321
  ]))
322
322
 
323
- async function scanSession(ledger, sessionId, asOfSeq, sealable, signal) {
323
+ // DSH reads a subagent Session only through its parent's address; the parent
324
+ // need not exist any more. One with no parent on record cannot be read.
325
+ const addressOf = item => (item.origin !== 'subagent'
326
+ ? { kind: 'session', sessionId: item.sessionId }
327
+ : item.parentSessionId === undefined
328
+ ? undefined
329
+ : { kind: 'subagent', parentSessionId: item.parentSessionId, childSessionId: item.sessionId, mode: 'unknown' })
330
+ // Sessions that already failed to read, so each one warns once, not every scan.
331
+ const unreadable = new Set()
332
+
333
+ async function scanSession(ledger, address, asOfSeq, sealable, signal) {
334
+ const sessionId = address.kind === 'session' ? address.sessionId : address.childSessionId
324
335
  const mark = ledger.mark(sessionId)
325
336
  if (mark?.sealed) return
326
337
  let through = asOfSeq
@@ -338,7 +349,7 @@ export function install(rt) {
338
349
  let beforeSeq
339
350
  for (let reached = false; !reached;) {
340
351
  const page = await ctx.sessionController.page({
341
- address: { kind: 'session', sessionId },
352
+ address,
342
353
  throughSeq: through,
343
354
  ...(beforeSeq === undefined ? {} : { beforeSeq }),
344
355
  maxMessages: PAGE_MESSAGES,
@@ -397,8 +408,19 @@ export function install(rt) {
397
408
  const targets = new Map()
398
409
  for (const item of listed) if (item.blank !== true) targets.set(item.sessionId, item.projections?.asOfSeq)
399
410
  for (const sessionId of owners.keys()) if (!targets.has(sessionId)) targets.set(sessionId, undefined)
400
- await eachLimited([...targets], SCAN_PARALLEL, ([sessionId, asOfSeq]) => scanSession(ledger, sessionId, asOfSeq, ended.has(sessionId), signal)
401
- .catch(error => rt.warn('ds-bot: usage could not read %s: %s', sessionId, error)))
411
+ const addresses = new Map(listed.map(item => [item.sessionId, addressOf(item)]))
412
+ await eachLimited([...targets], SCAN_PARALLEL, async ([sessionId, asOfSeq]) => {
413
+ const address = addresses.has(sessionId) ? addresses.get(sessionId) : { kind: 'session', sessionId }
414
+ if (address === undefined) return
415
+ try {
416
+ await scanSession(ledger, address, asOfSeq, ended.has(sessionId), signal)
417
+ unreadable.delete(sessionId)
418
+ } catch (error) {
419
+ if (unreadable.has(sessionId)) return
420
+ unreadable.add(sessionId)
421
+ rt.warn('ds-bot: usage could not read %s: %s', sessionId, error)
422
+ }
423
+ })
402
424
  })().finally(() => {
403
425
  scannedAt = Date.now()
404
426
  scanning = undefined