返回 CodeWhale
metadata.ts
1 /**
2 * A preset's display metadata: the name and description a picker shows.
3 *
4 * It lives in its own file because the composition is a top-level list of
5 * plugin rows — YAML cannot carry sibling keys beside it, and faking a
6 * metadata row would hand the Loader something to load. Keeping it separate
7 * also keeps the composition exactly what its name says: a Cordis file the
8 * loader owns and the cordis preset can author.
9 *
10 * The file carries display text ONLY. `id` is the directory name and `trust`
11 * comes from the root a preset was discovered under, so neither is writable
12 * here — otherwise a locally authored preset could claim to be a shipped one.
13 *
14 * Every read failure degrades to no metadata. A preset whose display text is
15 * missing, malformed, or unreadable still mounts: presentation is not a
16 * capability, and a broken name must never become an agent that cannot start.
17 * @module @deepseek-ai/dsh-agent-presets/metadata
18 */
19
20 import { join } from 'node:path'
21 import yaml from 'js-yaml'
22
23 /** The optional display-metadata file beside a preset's composition. */
24 export const METADATA_FILE = 'preset.yml'
25
26 /** Display text a preset may publish about itself. */
27 export interface PresetMetadata {
28 /** Human-facing name; falls back to the preset id when absent. */
29 readonly name?: string
30 /** One sentence on what this preset is for. */
31 readonly description?: string
32 /**
33 * Position within its group; lower comes first. A preset that declares
34 * none sorts after every preset that does, then by id — so the shipped set
35 * can read in capability order while authored ones stay alphabetical.
36 */
37 readonly order?: number
38 }
39
40 /** A non-empty trimmed string, or undefined for anything else. */
41 function text(value: unknown): string | undefined {
42 if (typeof value !== 'string') return undefined
43 const trimmed = value.trim()
44 return trimmed === '' ? undefined : trimmed
45 }
46
47 /**
48 * Read one preset directory's display metadata.
49 *
50 * Absent, unparsable, and wrongly-shaped files are all the same answer —
51 * empty metadata — because the caller renders a picker, not a diagnostic.
52 * @param directory - the preset directory.
53 * @returns the display text the preset published, possibly empty.
54 */
55 export async function readPresetMetadata(directory: string, readFile: (path: string, encoding: string) => Promise<string>): Promise<PresetMetadata> {
56 let raw: string
57 try {
58 raw = await readFile(join(directory, METADATA_FILE), 'utf8')
59 } catch {
60 // Absent is the common case: metadata is optional and most presets,
61 // including every one authored by duplicating another, carry none.
62 return {}
63 }
64 let parsed: unknown
65 try {
66 parsed = yaml.load(raw)
67 } catch {
68 // Malformed display text is not worth failing discovery over; the picker
69 // falls back to the id, and the composition still mounts.
70 return {}
71 }
72 if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return {}
73 const record = parsed as Record<string, unknown>
74 const name = text(record.name)
75 const description = text(record.description)
76 const order = typeof record.order === 'number' && Number.isFinite(record.order)
77 ? record.order
78 : undefined
79 return {
80 ...name === undefined ? {} : { name },
81 ...description === undefined ? {} : { description },
82 ...order === undefined ? {} : { order },
83 }
84 }
85
86 /**
87 * Render display metadata as the file's contents.
88 *
89 * Absent fields are omitted rather than written empty, so a preset with no
90 * description does not ship a key that reads as an intentional blank.
91 * @param metadata - the display text to store.
92 * @returns the YAML document, or undefined when there is nothing to store.
93 */
94 export function renderPresetMetadata(metadata: PresetMetadata): string | undefined {
95 const name = text(metadata.name)
96 const description = text(metadata.description)
97 const { order } = metadata
98 if (name === undefined && description === undefined && order === undefined) return undefined
99 return yaml.dump({
100 ...name === undefined ? {} : { name },
101 ...description === undefined ? {} : { description },
102 ...order === undefined ? {} : { order },
103 }, { lineWidth: -1 })
104 }
105
105 lines TYPESCRIPT