Files
deepseek-harness/packages/scaffold/telemetry/src/anonymous-id.ts
Tianyi Cui 3fc35c91ff refactor(packages): dissolve ui/ and rename sdk/ to scaffold/
git mv per the regrouping RFC: the five human-collaboration seams and
tui join packages/interaction/, app-boot becomes packages/boot/, and
jsonrpc joins the renamed scaffold/ (formerly sdk/) as its server half
beside client/protocol/create-sdk/helper/scripts/telemetry, whose
folders drop the legacy sdk- prefix. Three new group README triplets
replace the ui/ and sdk/ ones; tsconfig references/paths/globs,
knip keys, vitest globs, gate scripts, catalogs, docs, and the
lockfile follow. Adds the four settled FIXME rename markers
(dsh-sdk-server, dsh-sdk-telemetry, dsh-sdk-helper, dsh-sdk-scripts).

The scaffold folders diverge from their npm names until those renames
land, so tsconfig.base.json maps the three affected names explicitly
beside the group wildcard. Also repairs two pre-existing stale-path
classes the strengthened sweep surfaced: docs/web-styling.md's retired
web-ui host package and type-model spec fixture-literal joins.

app-boot's three Loader-composition specs time out at the default 5s
under full-suite parallel load on this filesystem (pre-existing;
pass isolated with --testTimeout=30000); interaction/scaffold/boot
suites otherwise green (687 passed).
2026-08-09 01:21:12 +08:00

94 lines
3.8 KiB
TypeScript

/**
* Per-harness-home anonymous telemetry id.
*
* The id is a random UUID persisted directly in the harness home resolved by
* {@link resolveDshHome} (`$DSH_HOME` > `~/.dsh`), and never derived from the
* git remote, repository URL, or any other identifying source (a derived id
* would make "anonymous" a fiction). The id is scoped to the harness home, not
* the machine: every command sharing one `$DSH_HOME` reuses the same id, so the
* default `~/.dsh` counts per-OS-user home directories, while a relocated
* `$DSH_HOME` moves the id with the rest of the harness data — the single-root
* convention this package shares, not a telemetry-specific policy.
*
* @module @deepseek-ai/dsh-telemetry/anonymous-id
*/
import { randomUUID } from 'node:crypto'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import type { Branded } from '@deepseek-ai/dsh-brand'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
/** A harness-home-scoped anonymous telemetry id (random UUID v4). */
export type AnonymousId = Branded<'AnonymousId'>
/** Default file, inside the harness home, storing the anonymous id. */
export const ANONYMOUS_ID_FILE_NAME = 'telemetry.json'
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
/** Ambient seams for locating and generating the id; every field has a default. */
export interface AnonymousIdOptions {
/** Environment consulted for `DSH_HOME`; defaults to `process.env`. */
env?: NodeJS.ProcessEnv
/** UUID generator; defaults to `crypto.randomUUID` (test seam). */
randomUUID?: () => string
}
/**
* Resolve the single-root harness home that stores the anonymous id.
* Delegates to {@link resolveDshHome} so telemetry shares the harness's one
* home-resolution policy (`DSH_HOME` > `~/.dsh`) instead of maintaining a
* second config-directory convention.
* @param options - environment seam.
* @returns absolute harness home path.
*/
export function globalConfigDir(options: AnonymousIdOptions = {}): string {
return resolveDshHome(undefined, options.env ?? process.env)
}
/** Read a valid persisted id from the store, or `undefined` when absent/corrupt. */
async function readPersistedId(file: string): Promise<AnonymousId | undefined> {
let text: string
try {
text = await readFile(file, 'utf8')
} catch {
// Absent or unreadable: the caller mints and persists a fresh id.
return undefined
}
let parsed: unknown
try {
parsed = JSON.parse(text)
} catch {
// Corrupt JSON: the caller overwrites the store with a fresh id.
return undefined
}
if (parsed !== null && typeof parsed === 'object') {
const value = (parsed as Record<string, unknown>).anonymousId
if (typeof value === 'string' && UUID_PATTERN.test(value)) return value as AnonymousId
}
return undefined
}
/**
* Return the harness home's anonymous id, creating and persisting one on first use.
* Persistence is best-effort: a write failure still returns a usable id for the
* current run so telemetry is never blocked by config-dir permissions.
* @param options - config-location and UUID-generation seams.
* @returns the stable per-harness-home anonymous id.
*/
export async function getOrCreateAnonymousId(options: AnonymousIdOptions = {}): Promise<AnonymousId> {
const file = join(globalConfigDir(options), ANONYMOUS_ID_FILE_NAME)
const existing = await readPersistedId(file)
if (existing !== undefined) return existing
const generate = options.randomUUID ?? randomUUID
const created = generate() as AnonymousId
try {
await mkdir(dirname(file), { recursive: true })
await writeFile(file, `${JSON.stringify({ anonymousId: created }, null, 2)}\n`, 'utf8')
} catch {
// Best-effort persistence: return the fresh id even when the store is unwritable.
}
return created
}