@linxin666/dsh-pet 0.1.15 → 0.1.16

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.
Files changed (74) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +73 -38
  3. package/README.zh.md +91 -55
  4. package/assets/whale/pet.json +12 -1
  5. package/cordis.patch.yml +4 -4
  6. package/lib/client.js +270 -334
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +641 -87
  9. package/lib/types/client/PetDockEntry.d.ts +14 -12
  10. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  11. package/lib/types/client/PetDockEntry.js +13 -8
  12. package/lib/types/client/PetSettingsCard.d.ts +22 -9
  13. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PetSettingsCard.js +42 -7
  15. package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
  16. package/lib/types/client/PetSprite.d.ts.map +1 -0
  17. package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
  18. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/PluginSettingsCard.js +3 -1
  20. package/lib/types/client/index.d.ts +11 -8
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/client/index.js +30 -12
  23. package/lib/types/client/locales.d.ts +10 -10
  24. package/lib/types/client/locales.js +10 -10
  25. package/lib/types/client/pet-store.d.ts +5 -0
  26. package/lib/types/client/pet-store.d.ts.map +1 -1
  27. package/lib/types/client/pet-store.js +4 -0
  28. package/lib/types/client/settings-form.d.ts +62 -0
  29. package/lib/types/client/settings-form.d.ts.map +1 -1
  30. package/lib/types/client/settings-form.js +61 -12
  31. package/lib/types/client/spritesheet.d.ts +14 -48
  32. package/lib/types/client/spritesheet.d.ts.map +1 -1
  33. package/lib/types/client/spritesheet.js +13 -81
  34. package/lib/types/index.d.ts +24 -11
  35. package/lib/types/index.d.ts.map +1 -1
  36. package/lib/types/index.js +53 -33
  37. package/lib/types/ledger.d.ts +4 -2
  38. package/lib/types/ledger.d.ts.map +1 -1
  39. package/lib/types/ledger.js +11 -4
  40. package/lib/types/persist.d.ts +10 -3
  41. package/lib/types/persist.d.ts.map +1 -1
  42. package/lib/types/persist.js +32 -5
  43. package/lib/types/registry.d.ts +158 -0
  44. package/lib/types/registry.d.ts.map +1 -0
  45. package/lib/types/registry.js +276 -0
  46. package/lib/types/routes.d.ts +10 -10
  47. package/lib/types/routes.d.ts.map +1 -1
  48. package/lib/types/routes.js +169 -51
  49. package/lib/types/service.d.ts +46 -13
  50. package/lib/types/service.d.ts.map +1 -1
  51. package/lib/types/service.js +73 -16
  52. package/package.json +2 -2
  53. package/src/client/PetDockEntry.tsx +34 -26
  54. package/src/client/PetSettingsCard.tsx +67 -22
  55. package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
  56. package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
  57. package/src/client/PluginSettingsCard.tsx +7 -1
  58. package/src/client/index.ts +36 -14
  59. package/src/client/locales.ts +10 -10
  60. package/src/client/pet-store.ts +9 -0
  61. package/src/client/settings-card.module.css +3 -0
  62. package/src/client/settings-form.ts +114 -12
  63. package/src/client/spritesheet.ts +16 -92
  64. package/src/index.test.ts +23 -0
  65. package/src/index.ts +89 -45
  66. package/src/ledger.test.ts +5 -3
  67. package/src/ledger.ts +11 -4
  68. package/src/persist.test.ts +52 -3
  69. package/src/persist.ts +42 -8
  70. package/src/registry.test.ts +132 -0
  71. package/src/registry.ts +400 -0
  72. package/src/routes.ts +166 -53
  73. package/src/service.ts +102 -20
  74. package/lib/types/client/WhalePet.d.ts.map +0 -1
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Pet registry — the multi-pet contract. One pet is a directory holding a
3
+ * 'pet.json' manifest plus an atlas image; nothing else is required, and no
4
+ * host or client code changes when a pet is added. The registry scans three
5
+ * sources, later sources overriding earlier ones on an id collision:
6
+ *
7
+ * 1. the package's own 'assets' subdirectories (built-in pets);
8
+ * 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets);
9
+ * 3. 'PetConfig.pets' manifests composed by the embedding application
10
+ * (highest precedence).
11
+ *
12
+ * The manifest follows the Codex/hatch-pet contract (8 columns x 9 rows of
13
+ * 192x208 cells, the 9-state row order below). Legacy whale-girl manifests
14
+ * that only carry 'frames' keep working: geometry, per-row frame counts and
15
+ * per-track rhythm all fall back to the hatch-pet contract defaults, and the
16
+ * whale-girl manifest overrides its own durations.
17
+ * @module @linxin666/dsh-pet/registry
18
+ */
19
+ import type { PetAnimation } from './state.ts';
20
+ /** Fixed row order of the 9-state animation contract. */
21
+ export declare const PET_ROW_ORDER: readonly PetAnimation[];
22
+ /** Atlas cell size in px. */
23
+ export interface PetCell {
24
+ width: number;
25
+ height: number;
26
+ }
27
+ /** Atlas cell size in px (Codex/hatch-pet contract). */
28
+ export declare const DEFAULT_PET_CELL: PetCell;
29
+ /** Columns per row (max frames per track). */
30
+ export declare const DEFAULT_PET_COLUMNS = 8;
31
+ /** Rows in the atlas (fixed by the animation contract). */
32
+ export declare const DEFAULT_PET_ROW_COUNT = 9;
33
+ /**
34
+ * Per-row used-column counts from the hatch-pet contract table. Manifests
35
+ * that carry no 'frames' field (the Codex custom-pet shape) resolve here.
36
+ */
37
+ export declare const DEFAULT_FRAME_COUNTS: readonly number[];
38
+ /** Absolute package root, resolved from a module URL (lib/ or src/). */
39
+ export declare function petPackageRoot(importMetaUrl: string): string;
40
+ /** Resolve the hatch-pet custom pets directory (CODEX_HOME or ~/.codex). */
41
+ export declare function codexPetsDir(env?: NodeJS.ProcessEnv, home?: string): string;
42
+ /** One animation track as served to the browser half. */
43
+ export interface PetTrackDef {
44
+ /** Frame indices (columns) played in order. */
45
+ frames: number[];
46
+ /** Per-frame duration in ms; same length as frames. */
47
+ durations: number[];
48
+ /** Whether the track loops; a non-looping track holds its last frame. */
49
+ loop: boolean;
50
+ /** Track to switch to after a non-looping track finishes. */
51
+ fallback?: PetAnimation;
52
+ }
53
+ /** Default per-track rhythm (hatch-pet contract table). */
54
+ export declare const DEFAULT_TRACK_PATTERNS: Record<PetAnimation, {
55
+ durations: number[];
56
+ loop: boolean;
57
+ fallback?: PetAnimation;
58
+ }>;
59
+ /** Manifest shape a pet directory (or 'PetConfig.pets' entry) declares. */
60
+ export interface PetManifest {
61
+ /** Unique pet id, lowercase kebab-case. */
62
+ id: string;
63
+ /** Human-readable display name (settings selector, panel header). */
64
+ displayName: string;
65
+ /** One-line description. */
66
+ description?: string;
67
+ /** Atlas path relative to the manifest's directory. */
68
+ spritesheetPath: string;
69
+ /** Atlas cell size; defaults to the Codex contract 192x208. */
70
+ cell?: {
71
+ width?: number;
72
+ height?: number;
73
+ };
74
+ /** Columns per row; defaults to 8. */
75
+ columns?: number;
76
+ /**
77
+ * Per-row frame counts (9 entries, row order above). Manifests that omit
78
+ * it resolve the hatch-pet contract table.
79
+ */
80
+ frames?: number[];
81
+ /** Optional per-track rhythm overrides; omitted tracks use the defaults. */
82
+ tracks?: Partial<Record<PetAnimation, PetTrackOverride>>;
83
+ }
84
+ /** Per-track rhythm overrides a manifest may carry. */
85
+ export interface PetTrackOverride {
86
+ /** Per-frame durations in ms (cycled to the row's frame count). */
87
+ durations?: number[];
88
+ /** Whether the track loops. */
89
+ loop?: boolean;
90
+ /** Track to switch to after a non-looping track finishes. */
91
+ fallback?: PetAnimation;
92
+ }
93
+ /** A normalized pet as served to the browser half. */
94
+ export interface PetDefinition {
95
+ id: string;
96
+ displayName: string;
97
+ description: string;
98
+ /** Atlas cell size in px. */
99
+ cell: PetCell;
100
+ /** Columns per row. */
101
+ columns: number;
102
+ /** Per-row frame counts (length 9, row order above). */
103
+ rows: number[];
104
+ /** Fully resolved animation tracks (frames + durations + loop/fallback). */
105
+ tracks: Record<PetAnimation, PetTrackDef>;
106
+ /** Browser URL of the atlas (served by the host asset route). */
107
+ atlasUrl: string;
108
+ /** Browser URL of the manifest (served by the host asset route). */
109
+ manifestUrl: string;
110
+ }
111
+ /** A resolved pet plus its host-side file location. */
112
+ export interface PetEntry extends PetDefinition {
113
+ /** Absolute directory holding the manifest and atlas. */
114
+ dir: string;
115
+ /** Atlas path relative to 'dir' (declared by the manifest). */
116
+ spritesheetPath: string;
117
+ }
118
+ /** Registry load result: resolved entries plus load warnings. */
119
+ export interface PetRegistry {
120
+ entries: PetEntry[];
121
+ warnings: string[];
122
+ byId(id: string): PetEntry | undefined;
123
+ /** The pet an installation falls back to when the selection is unknown. */
124
+ defaultEntry(): PetEntry;
125
+ }
126
+ /** Registry sources. */
127
+ export interface PetRegistryOptions {
128
+ /** Absolute package root whose 'assets/*' hold built-in pets. */
129
+ packageRoot: string;
130
+ /** Asset route prefix the browser URLs are built under. */
131
+ assetPrefix?: string;
132
+ /** Custom pet directory (defaults to '${CODEX_HOME:-~/.codex}/pets'). */
133
+ petsDir?: string;
134
+ /** Extra manifest entries composed by the embedding application. */
135
+ extra?: readonly PetManifest[];
136
+ }
137
+ /**
138
+ * Normalize one parsed manifest into a renderable pet entry, or undefined
139
+ * (with a warning recorded) when the manifest violates the contract.
140
+ */
141
+ export declare function resolvePetManifest(raw: unknown, dir: string, options?: {
142
+ assetPrefix?: string;
143
+ warnings?: string[];
144
+ }): PetEntry | undefined;
145
+ /**
146
+ * Load the pet registry: built-in 'assets/*' first, then the hatch-pet
147
+ * custom pets directory, then composed 'extra' manifests (each later source
148
+ * overrides an earlier one on id collision). The registry never throws on a
149
+ * bad manifest: it skips it and records a warning.
150
+ */
151
+ export declare function loadPetRegistry(options: PetRegistryOptions): PetRegistry;
152
+ /** Strip host-only fields, leaving the client-visible definition. */
153
+ export declare function petEntryView(entry: PetEntry): PetDefinition;
154
+ /** The absolute file a pet's atlas resolves to (host asset route). */
155
+ export declare function petAtlasFile(entry: PetEntry): string;
156
+ /** The directory basename of one entry (legacy asset URL alias, e.g. whale). */
157
+ export declare function petDirAlias(entry: PetEntry): string;
158
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Pet registry — the multi-pet contract. One pet is a directory holding a
3
+ * 'pet.json' manifest plus an atlas image; nothing else is required, and no
4
+ * host or client code changes when a pet is added. The registry scans three
5
+ * sources, later sources overriding earlier ones on an id collision:
6
+ *
7
+ * 1. the package's own 'assets' subdirectories (built-in pets);
8
+ * 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets);
9
+ * 3. 'PetConfig.pets' manifests composed by the embedding application
10
+ * (highest precedence).
11
+ *
12
+ * The manifest follows the Codex/hatch-pet contract (8 columns x 9 rows of
13
+ * 192x208 cells, the 9-state row order below). Legacy whale-girl manifests
14
+ * that only carry 'frames' keep working: geometry, per-row frame counts and
15
+ * per-track rhythm all fall back to the hatch-pet contract defaults, and the
16
+ * whale-girl manifest overrides its own durations.
17
+ * @module @linxin666/dsh-pet/registry
18
+ */
19
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ /** Fixed row order of the 9-state animation contract. */
24
+ export const PET_ROW_ORDER = [
25
+ 'idle',
26
+ 'running-right',
27
+ 'running-left',
28
+ 'waving',
29
+ 'jumping',
30
+ 'failed',
31
+ 'waiting',
32
+ 'running',
33
+ 'review',
34
+ ];
35
+ /** Atlas cell size in px (Codex/hatch-pet contract). */
36
+ export const DEFAULT_PET_CELL = { width: 192, height: 208 };
37
+ /** Columns per row (max frames per track). */
38
+ export const DEFAULT_PET_COLUMNS = 8;
39
+ /** Rows in the atlas (fixed by the animation contract). */
40
+ export const DEFAULT_PET_ROW_COUNT = 9;
41
+ /**
42
+ * Per-row used-column counts from the hatch-pet contract table. Manifests
43
+ * that carry no 'frames' field (the Codex custom-pet shape) resolve here.
44
+ */
45
+ export const DEFAULT_FRAME_COUNTS = [6, 8, 8, 4, 5, 8, 6, 6, 6];
46
+ /** Absolute package root, resolved from a module URL (lib/ or src/). */
47
+ export function petPackageRoot(importMetaUrl) {
48
+ return fileURLToPath(new URL('../', importMetaUrl));
49
+ }
50
+ /** Resolve the hatch-pet custom pets directory (CODEX_HOME or ~/.codex). */
51
+ export function codexPetsDir(env = process.env, home = homedir()) {
52
+ const raw = env.CODEX_HOME !== undefined && env.CODEX_HOME.trim() !== ''
53
+ ? env.CODEX_HOME.trim()
54
+ : join(home, '.codex');
55
+ const expanded = raw === '~'
56
+ ? home
57
+ : (raw.startsWith('~/') || raw.startsWith('~\\')) ? join(home, raw.slice(2)) : raw;
58
+ return join(expanded, 'pets');
59
+ }
60
+ /** Finite non-negative integer guard, else the fallback. */
61
+ function finiteInt(value, fallback, max) {
62
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= max
63
+ ? value
64
+ : fallback;
65
+ }
66
+ /** Build the browser URL of one pet asset. */
67
+ function assetUrl(prefix, id, file) {
68
+ const path = file.split('/').filter(segment => segment !== '').join('/');
69
+ return prefix + '/' + encodeURIComponent(id) + '/' + path;
70
+ }
71
+ /** Default per-track rhythm (hatch-pet contract table). */
72
+ export const DEFAULT_TRACK_PATTERNS = {
73
+ idle: { durations: [280, 110, 110, 140, 140, 320], loop: true },
74
+ 'running-right': { durations: [120, 120, 120, 120, 120, 120, 120, 220], loop: true },
75
+ 'running-left': { durations: [120, 120, 120, 120, 120, 120, 120, 220], loop: true },
76
+ waving: { durations: [140, 140, 140, 280], loop: true },
77
+ jumping: { durations: [140, 140, 140, 140, 280], loop: false, fallback: 'idle' },
78
+ failed: { durations: [140, 140, 140, 140, 140, 140, 140, 240], loop: false, fallback: 'idle' },
79
+ waiting: { durations: [150, 150, 150, 150, 150, 260], loop: true },
80
+ running: { durations: [120, 120, 120, 120, 120, 220], loop: true },
81
+ review: { durations: [150, 150, 150, 150, 150, 280], loop: true },
82
+ };
83
+ /** Stable id charset: keeps asset URLs plain and filesystem-safe. */
84
+ const PET_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
85
+ /** Safe path-segment charset for atlas files. */
86
+ const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
87
+ const PET_NAME_MAX_LENGTH = 80;
88
+ /**
89
+ * Normalize one parsed manifest into a renderable pet entry, or undefined
90
+ * (with a warning recorded) when the manifest violates the contract.
91
+ */
92
+ export function resolvePetManifest(raw, dir, options = {}) {
93
+ const { assetPrefix = '/pet', warnings = [] } = options;
94
+ const warn = (message) => { warnings.push(message); };
95
+ if (typeof raw !== 'object' || raw === null) {
96
+ warn('manifest is not an object');
97
+ return undefined;
98
+ }
99
+ const source = raw;
100
+ const id = typeof source.id === 'string' ? source.id.trim() : '';
101
+ if (!PET_ID_PATTERN.test(id)) {
102
+ warn('manifest id ' + JSON.stringify(String(source.id)) + ' is not a lowercase kebab id');
103
+ return undefined;
104
+ }
105
+ const displayName = typeof source.displayName === 'string' && source.displayName.trim() !== ''
106
+ ? source.displayName.trim().slice(0, PET_NAME_MAX_LENGTH)
107
+ : id;
108
+ const description = typeof source.description === 'string'
109
+ ? source.description.trim()
110
+ : '';
111
+ const spritesheet = typeof source.spritesheetPath === 'string' && source.spritesheetPath.trim() !== ''
112
+ ? source.spritesheetPath.trim()
113
+ : 'spritesheet.webp';
114
+ const spritesheetPath = spritesheet.split('/').filter(segment => segment !== '');
115
+ if (spritesheetPath.length === 0
116
+ || isAbsolute(spritesheet)
117
+ || spritesheet.includes('\\')
118
+ || spritesheetPath.some(segment => segment === '..' || !PATH_SEGMENT_PATTERN.test(segment))) {
119
+ warn('manifest spritesheetPath ' + JSON.stringify(spritesheet) + ' is not a safe relative path');
120
+ return undefined;
121
+ }
122
+ const rawCell = (typeof source.cell === 'object' && source.cell !== null ? source.cell : {});
123
+ const cell = {
124
+ width: finiteInt(rawCell.width, DEFAULT_PET_CELL.width, 2048),
125
+ height: finiteInt(rawCell.height, DEFAULT_PET_CELL.height, 2048),
126
+ };
127
+ const columns = finiteInt(source.columns, DEFAULT_PET_COLUMNS, 32);
128
+ const rows = DEFAULT_FRAME_COUNTS.map((fallback, index) => {
129
+ const value = Array.isArray(source.frames) ? source.frames[index] : undefined;
130
+ return finiteInt(value, fallback, columns);
131
+ });
132
+ const trackOverrides = (typeof source.tracks === 'object' && source.tracks !== null ? source.tracks : {});
133
+ const tracks = {};
134
+ for (const [row, animation] of PET_ROW_ORDER.entries()) {
135
+ const pattern = DEFAULT_TRACK_PATTERNS[animation];
136
+ const override = trackOverrides[animation];
137
+ const durations = Array.isArray(override?.durations) && override.durations.length > 0
138
+ ? override.durations.filter((value) => typeof value === 'number' && Number.isFinite(value) && value > 0)
139
+ : pattern.durations;
140
+ if (durations.length === 0) {
141
+ warn('manifest ' + id + ': track ' + animation + ' carries no usable durations');
142
+ return undefined;
143
+ }
144
+ const frameCount = Math.max(1, Math.min(rows[row], columns));
145
+ const sized = durations.length >= frameCount
146
+ ? durations.slice(0, frameCount)
147
+ : Array.from({ length: frameCount }, (_, index) => durations[index % durations.length]);
148
+ tracks[animation] = {
149
+ frames: Array.from({ length: frameCount }, (_, index) => index),
150
+ durations: sized,
151
+ loop: typeof override?.loop === 'boolean' ? override.loop : pattern.loop,
152
+ ...(override?.fallback === undefined
153
+ ? pattern.fallback === undefined ? {} : { fallback: pattern.fallback }
154
+ : PET_ROW_ORDER.includes(override.fallback)
155
+ ? { fallback: override.fallback }
156
+ : pattern.fallback === undefined ? {} : { fallback: pattern.fallback }),
157
+ };
158
+ }
159
+ return {
160
+ id,
161
+ displayName,
162
+ description,
163
+ cell,
164
+ columns,
165
+ rows,
166
+ tracks,
167
+ atlasUrl: assetUrl(assetPrefix, id, spritesheet),
168
+ manifestUrl: assetUrl(assetPrefix, id, 'pet.json'),
169
+ dir,
170
+ spritesheetPath: spritesheetPath.join('/'),
171
+ };
172
+ }
173
+ /** Scan one directory of pet folders; entries come back in name order. */
174
+ function scanPetDir(dir, options) {
175
+ if (!existsSync(dir))
176
+ return [];
177
+ let names = [];
178
+ try {
179
+ names = readdirSync(dir).filter(name => !name.startsWith('.'));
180
+ }
181
+ catch {
182
+ return [];
183
+ }
184
+ names.sort();
185
+ const entries = [];
186
+ for (const name of names) {
187
+ const manifestFile = join(dir, name, 'pet.json');
188
+ if (!existsSync(manifestFile))
189
+ continue;
190
+ const parsed = readPetJson(manifestFile, options.warnings);
191
+ if (parsed === undefined)
192
+ continue;
193
+ const entry = resolvePetManifest(parsed, join(dir, name), options);
194
+ if (entry !== undefined)
195
+ entries.push(entry);
196
+ }
197
+ return entries;
198
+ }
199
+ /** Read and parse one manifest file; undefined (warning recorded) on failure. */
200
+ function readPetJson(file, warnings) {
201
+ try {
202
+ return JSON.parse(readFileSync(file, 'utf8'));
203
+ }
204
+ catch (error) {
205
+ warnings?.push('skipping ' + file + ': ' + (error instanceof Error ? error.message : String(error)));
206
+ return undefined;
207
+ }
208
+ }
209
+ /**
210
+ * Load the pet registry: built-in 'assets/*' first, then the hatch-pet
211
+ * custom pets directory, then composed 'extra' manifests (each later source
212
+ * overrides an earlier one on id collision). The registry never throws on a
213
+ * bad manifest: it skips it and records a warning.
214
+ */
215
+ export function loadPetRegistry(options) {
216
+ const { packageRoot, assetPrefix = '/pet' } = options;
217
+ const warnings = [];
218
+ const byId = new Map();
219
+ const builtinIds = new Set();
220
+ for (const entry of scanPetDir(join(packageRoot, 'assets'), { assetPrefix, warnings })) {
221
+ if (byId.has(entry.id)) {
222
+ warnings.push('duplicate built-in pet id ' + entry.id + '; the first one wins');
223
+ continue;
224
+ }
225
+ byId.set(entry.id, entry);
226
+ builtinIds.add(entry.id);
227
+ }
228
+ const petsDir = options.petsDir ?? codexPetsDir();
229
+ if (petsDir !== '') {
230
+ for (const entry of scanPetDir(petsDir, { assetPrefix, warnings })) {
231
+ if (byId.has(entry.id))
232
+ warnings.push('custom pet ' + entry.id + ' overrides the built-in one');
233
+ byId.set(entry.id, entry);
234
+ }
235
+ }
236
+ for (const manifest of options.extra ?? []) {
237
+ const dir = manifest.spritesheetPath === undefined || isAbsolute(manifest.spritesheetPath)
238
+ ? join(packageRoot, 'assets', 'extra')
239
+ : dirname(resolve(packageRoot, manifest.spritesheetPath));
240
+ const entry = resolvePetManifest(manifest, dir, { assetPrefix, warnings });
241
+ if (entry === undefined)
242
+ continue;
243
+ if (byId.has(entry.id))
244
+ warnings.push('composed pet ' + entry.id + ' overrides an earlier registration');
245
+ byId.set(entry.id, entry);
246
+ }
247
+ const entries = [...byId.values()];
248
+ return {
249
+ entries,
250
+ warnings,
251
+ byId: (id) => byId.get(id),
252
+ defaultEntry: () => entries.find(entry => builtinIds.has(entry.id)) ?? entries[0],
253
+ };
254
+ }
255
+ /** Strip host-only fields, leaving the client-visible definition. */
256
+ export function petEntryView(entry) {
257
+ return {
258
+ id: entry.id,
259
+ displayName: entry.displayName,
260
+ description: entry.description,
261
+ cell: entry.cell,
262
+ columns: entry.columns,
263
+ rows: entry.rows,
264
+ tracks: entry.tracks,
265
+ atlasUrl: entry.atlasUrl,
266
+ manifestUrl: entry.manifestUrl,
267
+ };
268
+ }
269
+ /** The absolute file a pet's atlas resolves to (host asset route). */
270
+ export function petAtlasFile(entry) {
271
+ return join(entry.dir, entry.spritesheetPath);
272
+ }
273
+ /** The directory basename of one entry (legacy asset URL alias, e.g. whale). */
274
+ export function petDirAlias(entry) {
275
+ return basename(entry.dir);
276
+ }
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * Pet HTTP routes — the browser half talks to the host through plain
3
- * same-origin JSON endpoints (`/api/pet/*`) and loads the whale-girl atlas
4
- * from `/pet/whale/*`. The `/plugins/` endpoint only serves client bundles
5
- * and RPC domains are platform-registered, so the pet serves its own API
6
- * and media — the same pattern as dsh-remote-web-ui's `/api/pair` family.
3
+ * same-origin JSON endpoints ('/api/pet/*') and loads pet assets from
4
+ * '/pet/<id>/*'. The '/plugins/' endpoint only serves client bundles and RPC
5
+ * domains are platform-registered, so the pet serves its own API and media —
6
+ * the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
7
+ * is one prefix registration serving every registry entry (manifest, atlas,
8
+ * optional previews), so adding a pet never touches route wiring.
7
9
  * @module @linxin666/dsh-pet/routes
8
10
  */
9
11
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
10
12
  import type { PetService } from './service.ts';
11
13
  /** Browser-facing base path of the pet API. */
12
14
  export declare const PET_API_PREFIX = "/api/pet";
13
- /** Browser-facing base path of the pet asset routes. */
14
- export declare const PET_ASSET_PREFIX = "/pet/whale";
15
- /** Absolute package root, resolved from this module's own location (lib/). */
16
- export declare function petPackageRoot(importMetaUrl: string): string;
17
- /** Build the full route family (API + assets) for one service + package root. */
15
+ /** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
16
+ export declare const PET_ASSET_PREFIX = "/pet";
17
+ /** Build the full route family (API + assets) for one service. */
18
18
  export declare function makePetRoutes(deps: {
19
19
  service: PetService;
20
- packageRoot: string;
21
20
  }): WebRoute[];
21
+ export { petPackageRoot } from './registry.ts';
22
22
  //# sourceMappingURL=routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,eAAe,CAAA;AAQ5C,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE5D;AAkFD,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,QAAQ,EAAE,CAuD5F"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAI9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAuNtC,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAA;CAAE,GAAG,QAAQ,EAAE,CAwCvE;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}