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).
This commit is contained in:
93
packages/scaffold/telemetry/src/anonymous-id.ts
Normal file
93
packages/scaffold/telemetry/src/anonymous-id.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user