@luv1211/dsh-pet 0.1.1-rc.2

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.
@@ -0,0 +1,19 @@
1
+ //#region lib/types/invariant.js
2
+ /** Package-owned invariant companion. @module @luv1211/dsh-pet/invariant */
3
+ const PACKAGE_NAME = "@luv1211/dsh-pet";
4
+ /** Cordis companion plugin name. */
5
+ const name = "pet-invariant";
6
+ /** Service required before the companion can reserve package ownership. */
7
+ const inject = ["invariants"];
8
+ /**
9
+ * No runtime invariant: the preference document persists through the
10
+ * settings seam (schema-validated there), and activity records are
11
+ * presentation state the service derives and owns alone, so a companion
12
+ * would restate the service rather than check a relation another package
13
+ * can violate.
14
+ */
15
+ const install = () => {};
16
+ /** Register this package's invariant companion. */
17
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
18
+ //#endregion
19
+ export { apply, inject, name };
@@ -0,0 +1,29 @@
1
+ /** Host activity projection adapter consumed by the pet domain. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { PetActivitySource, PetHostActivityRecord } from './types.ts';
4
+ /**
5
+ * Owns detached host activity records and publishes whole replacements. When
6
+ * constructed with a Context, it supplies the existing session lifecycle as
7
+ * the default host producer; a richer host projection can instead be
8
+ * provided to PetService through the `petActivity` service key.
9
+ */
10
+ export declare class PetActivityProjection implements PetActivitySource {
11
+ private readonly records;
12
+ private readonly listeners;
13
+ /**
14
+ * @param ctx - optional host context for the default session producer.
15
+ */
16
+ constructor(ctx?: Context);
17
+ /** Read a detached activity projection. */
18
+ getSnapshot(): readonly PetHostActivityRecord[];
19
+ /** Subscribe to whole detached projection replacements. */
20
+ subscribe(listener: (records: readonly PetHostActivityRecord[]) => void): () => void;
21
+ /** Publish one host-owned replacement after its producer commits.
22
+ * @param records - detached records replacing the current projection.
23
+ */
24
+ publish(records: readonly PetHostActivityRecord[]): void;
25
+ private observe;
26
+ private forget;
27
+ private notify;
28
+ }
29
+ //# sourceMappingURL=activity.d.ts.map
@@ -0,0 +1,63 @@
1
+ /** Host activity projection adapter consumed by the pet domain. */
2
+ /**
3
+ * Owns detached host activity records and publishes whole replacements. When
4
+ * constructed with a Context, it supplies the existing session lifecycle as
5
+ * the default host producer; a richer host projection can instead be
6
+ * provided to PetService through the `petActivity` service key.
7
+ */
8
+ export class PetActivityProjection {
9
+ records = new Map();
10
+ listeners = new Set();
11
+ /**
12
+ * @param ctx - optional host context for the default session producer.
13
+ */
14
+ constructor(ctx) {
15
+ if (ctx === undefined)
16
+ return;
17
+ ctx.on('session/event', (session, event) => { this.observe(session, event); }, { global: true });
18
+ ctx.on('session/disposed', (session) => { this.forget(session); }, { global: true });
19
+ }
20
+ /** Read a detached activity projection. */
21
+ getSnapshot() {
22
+ return [...this.records.values()].map(record => ({ ...record }));
23
+ }
24
+ /** Subscribe to whole detached projection replacements. */
25
+ subscribe(listener) {
26
+ this.listeners.add(listener);
27
+ return () => { this.listeners.delete(listener); };
28
+ }
29
+ /** Publish one host-owned replacement after its producer commits.
30
+ * @param records - detached records replacing the current projection.
31
+ */
32
+ publish(records) {
33
+ this.records.clear();
34
+ for (const record of records)
35
+ this.records.set(String(record.sessionId), { ...record });
36
+ this.notify();
37
+ }
38
+ observe(session, event) {
39
+ if (event.type !== 'turn/start' && event.type !== 'turn/end')
40
+ return;
41
+ const record = event.type === 'turn/start'
42
+ ? { sessionId: session.id, title: String(session.id), status: 'running', since: event.time, completed: false }
43
+ : {
44
+ sessionId: session.id,
45
+ title: String(session.id),
46
+ status: event.data.reason.kind === 'blocked' || event.data.reason.kind === 'error' ? 'blocked' : 'idle',
47
+ since: event.time,
48
+ completed: event.data.reason.kind !== 'blocked' && event.data.reason.kind !== 'error',
49
+ };
50
+ this.records.set(String(session.id), record);
51
+ this.notify();
52
+ }
53
+ forget(session) {
54
+ if (this.records.delete(String(session.id)))
55
+ this.notify();
56
+ }
57
+ notify() {
58
+ const snapshot = this.getSnapshot();
59
+ for (const listener of this.listeners)
60
+ listener(snapshot);
61
+ }
62
+ }
63
+ //# sourceMappingURL=activity.js.map
@@ -0,0 +1,67 @@
1
+ /** Host-side compatible pet catalog and transactional user-package storage. */
2
+ import { type PetPackageValidationOptions } from './runtime.ts';
3
+ import type { PetCatalog, PetDescriptor } from './types.ts';
4
+ /** Configured filesystem limits and the one DSH-owned user package root. */
5
+ export interface PetCatalogOptions extends PetPackageValidationOptions {
6
+ /** Explicit DSH home used when petRoot is absent. */
7
+ readonly dshHome?: string;
8
+ /** One user-managed package root; defaults to `<dshHome>/pets`. */
9
+ readonly petRoot?: string;
10
+ /** Maximum wall time for one complete user-image decode. */
11
+ readonly decodeTimeoutMs?: number;
12
+ }
13
+ /** One validated built-in or user package with detached client metadata. */
14
+ export declare class PetCatalogStore {
15
+ /** Absolute DSH-owned directory for user-installed pet packages. */
16
+ readonly petRoot: string;
17
+ private readonly options;
18
+ private readonly records;
19
+ private readonly listeners;
20
+ /**
21
+ * Load the embedded package and the configured DSH user root.
22
+ * @param options - package root and validation limits.
23
+ */
24
+ constructor(options?: PetCatalogOptions);
25
+ /** Read a detached deterministic catalog.
26
+ * @returns a stable descriptor list without filesystem references.
27
+ */
28
+ getCatalog(): PetCatalog;
29
+ /** Return a detached validated sprite for one catalog-owned id.
30
+ * @param id - catalog id to resolve.
31
+ * @returns a copied sprite byte array, or `undefined` for an unknown id.
32
+ */
33
+ getAsset(id: string): Uint8Array | undefined;
34
+ /** Subscribe to catalog publication and return its disposer.
35
+ * @param listener - callback receiving each detached catalog.
36
+ * @returns a disposer that removes the listener.
37
+ */
38
+ subscribe(listener: (catalog: PetCatalog) => void): () => void;
39
+ /** Check that an id names a loaded package.
40
+ * @param id - catalog id to test.
41
+ * @returns whether the id is currently loaded.
42
+ */
43
+ has(id: string): boolean;
44
+ /** Create the configured user root before a host action opens it. */
45
+ ensureRoot(): void;
46
+ /**
47
+ * Atomically publish one validated package under the user root.
48
+ * @param manifestBytes - UTF-8 compatible pet.json bytes.
49
+ * @param spritesheetBytes - validated WebP bytes.
50
+ * @returns the newly published descriptor.
51
+ */
52
+ importPackage(manifestBytes: Uint8Array, spritesheetBytes: Uint8Array): PetDescriptor;
53
+ /**
54
+ * Atomically replace one existing user package's content in place.
55
+ * @param manifestBytes - UTF-8 compatible pet.json bytes whose id names a loaded user package.
56
+ * @param spritesheetBytes - validated WebP bytes.
57
+ * @returns the freshly published descriptor.
58
+ */
59
+ replacePackage(manifestBytes: Uint8Array, spritesheetBytes: Uint8Array): PetDescriptor;
60
+ /** Dispose all update listeners owned by the catalog service. */
61
+ dispose(): void;
62
+ /** Delete the `.tmp` residue of earlier interrupted imports or replacements of one id. */
63
+ private sweepTemporaryDirectories;
64
+ /** Reload validated package records from the embedded assets and the user root, then publish one detached catalog. */
65
+ reload(): void;
66
+ }
67
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1,258 @@
1
+ /** Host-side compatible pet catalog and transactional user-package storage. */
2
+ import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
6
+ import { DEFAULT_PET_ID, petSpritesheetPath, validatePetPackageFiles, } from "./runtime.js";
7
+ import { decodeWebpDimensions } from "./host-image.js";
8
+ /** One validated built-in or user package with detached client metadata. */
9
+ export class PetCatalogStore {
10
+ /** Absolute DSH-owned directory for user-installed pet packages. */
11
+ petRoot;
12
+ options;
13
+ records = new Map();
14
+ listeners = new Set();
15
+ /**
16
+ * Load the embedded package and the configured DSH user root.
17
+ * @param options - package root and validation limits.
18
+ */
19
+ constructor(options = {}) {
20
+ this.options = { ...options };
21
+ this.petRoot = resolve(options.petRoot ?? join(resolveDshHome(options.dshHome), 'pets'));
22
+ this.reload();
23
+ }
24
+ /** Read a detached deterministic catalog.
25
+ * @returns a stable descriptor list without filesystem references.
26
+ */
27
+ getCatalog() {
28
+ return Object.freeze({ pets: Object.freeze([...this.records.values()].map(record => ({ ...record.descriptor }))) });
29
+ }
30
+ /** Return a detached validated sprite for one catalog-owned id.
31
+ * @param id - catalog id to resolve.
32
+ * @returns a copied sprite byte array, or `undefined` for an unknown id.
33
+ */
34
+ getAsset(id) {
35
+ const record = this.records.get(id);
36
+ return record === undefined ? undefined : new Uint8Array(record.spriteBytes);
37
+ }
38
+ /** Subscribe to catalog publication and return its disposer.
39
+ * @param listener - callback receiving each detached catalog.
40
+ * @returns a disposer that removes the listener.
41
+ */
42
+ subscribe(listener) {
43
+ this.listeners.add(listener);
44
+ return () => { this.listeners.delete(listener); };
45
+ }
46
+ /** Check that an id names a loaded package.
47
+ * @param id - catalog id to test.
48
+ * @returns whether the id is currently loaded.
49
+ */
50
+ has(id) {
51
+ return this.records.has(id);
52
+ }
53
+ /** Create the configured user root before a host action opens it. */
54
+ ensureRoot() {
55
+ mkdirSync(this.petRoot, { recursive: true });
56
+ }
57
+ /**
58
+ * Atomically publish one validated package under the user root.
59
+ * @param manifestBytes - UTF-8 compatible pet.json bytes.
60
+ * @param spritesheetBytes - validated WebP bytes.
61
+ * @returns the newly published descriptor.
62
+ */
63
+ importPackage(manifestBytes, spritesheetBytes) {
64
+ const firstPass = validatePetPackageFiles(manifestBytes, spritesheetBytes, { ...this.options, assetUrl: '', source: 'user' });
65
+ decodeWebpDimensions(spritesheetBytes, this.options.decodeTimeoutMs ?? 10_000);
66
+ if (firstPass.descriptor.id === DEFAULT_PET_ID)
67
+ throw new Error(`pet id ${DEFAULT_PET_ID} is reserved for the built-in package`);
68
+ if (this.records.has(firstPass.descriptor.id))
69
+ throw new Error(`pet id ${firstPass.descriptor.id} is already registered`);
70
+ mkdirSync(this.petRoot, { recursive: true });
71
+ const target = join(this.petRoot, firstPass.descriptor.id);
72
+ if (existsSync(target))
73
+ throw new Error(`pet package directory ${firstPass.descriptor.id} already exists`);
74
+ const temporary = join(this.petRoot, `.${firstPass.descriptor.id}.${randomUUID()}.tmp`);
75
+ try {
76
+ mkdirSync(temporary);
77
+ writeFileSync(join(temporary, 'pet.json'), manifestBytes, { flag: 'wx' });
78
+ const spriteTarget = join(temporary, firstPass.spritesheetPath.replaceAll('\\', '/'));
79
+ mkdirSync(dirname(spriteTarget), { recursive: true });
80
+ writeFileSync(spriteTarget, spritesheetBytes, { flag: 'wx' });
81
+ renameSync(temporary, target);
82
+ }
83
+ catch (error) {
84
+ rmSync(temporary, { recursive: true, force: true });
85
+ throw error;
86
+ }
87
+ this.reload();
88
+ const published = this.records.get(firstPass.descriptor.id)?.descriptor;
89
+ if (published === undefined)
90
+ throw new Error(`pet package ${firstPass.descriptor.id} was not published after atomic rename`);
91
+ return { ...published };
92
+ }
93
+ /**
94
+ * Atomically replace one existing user package's content in place.
95
+ * @param manifestBytes - UTF-8 compatible pet.json bytes whose id names a loaded user package.
96
+ * @param spritesheetBytes - validated WebP bytes.
97
+ * @returns the freshly published descriptor.
98
+ */
99
+ replacePackage(manifestBytes, spritesheetBytes) {
100
+ const firstPass = validatePetPackageFiles(manifestBytes, spritesheetBytes, { ...this.options, assetUrl: '', source: 'user' });
101
+ decodeWebpDimensions(spritesheetBytes, this.options.decodeTimeoutMs ?? 10_000);
102
+ const id = firstPass.descriptor.id;
103
+ if (this.records.get(id)?.source !== 'user')
104
+ throw new TypeError(`pet package ${id} is not an updatable user package`);
105
+ mkdirSync(this.petRoot, { recursive: true });
106
+ this.sweepTemporaryDirectories(id);
107
+ const target = join(this.petRoot, id);
108
+ // renameSync cannot overwrite a non-empty directory (EPERM on Windows,
109
+ // ENOTEMPTY on POSIX), so the swap is fixed at three renames. The target
110
+ // is briefly absent between the first two; every interruption outside
111
+ // that adjacent pair leaves either the complete old or complete new
112
+ // content, and a crash inside it leaves only same-id `.tmp` residue.
113
+ const staged = join(this.petRoot, `.${id}.${randomUUID()}.tmp`);
114
+ const aside = join(this.petRoot, `.${id}.${randomUUID()}.tmp`);
115
+ try {
116
+ mkdirSync(staged);
117
+ writeFileSync(join(staged, 'pet.json'), manifestBytes, { flag: 'wx' });
118
+ const spriteTarget = join(staged, firstPass.spritesheetPath.replaceAll('\\', '/'));
119
+ mkdirSync(dirname(spriteTarget), { recursive: true });
120
+ writeFileSync(spriteTarget, spritesheetBytes, { flag: 'wx' });
121
+ renameSync(target, aside);
122
+ try {
123
+ renameSync(staged, target);
124
+ }
125
+ catch (error) {
126
+ renameSync(aside, target);
127
+ throw error;
128
+ }
129
+ }
130
+ catch (error) {
131
+ rmSync(staged, { recursive: true, force: true });
132
+ throw error;
133
+ }
134
+ rmSync(aside, { recursive: true, force: true });
135
+ this.reload();
136
+ const published = this.records.get(id)?.descriptor;
137
+ if (published === undefined)
138
+ throw new Error(`pet package ${id} was not published after replacement`);
139
+ return { ...published };
140
+ }
141
+ /** Dispose all update listeners owned by the catalog service. */
142
+ dispose() {
143
+ this.listeners.clear();
144
+ this.records.clear();
145
+ }
146
+ /** Delete the `.tmp` residue of earlier interrupted imports or replacements of one id. */
147
+ sweepTemporaryDirectories(id) {
148
+ for (const entry of readdirSync(this.petRoot, { withFileTypes: true })) {
149
+ if (entry.name.startsWith(`.${id}.`) && entry.name.endsWith('.tmp')) {
150
+ rmSync(join(this.petRoot, entry.name), { recursive: true, force: true });
151
+ }
152
+ }
153
+ }
154
+ /** Reload validated package records from the embedded assets and the user root, then publish one detached catalog. */
155
+ reload() {
156
+ const next = new Map();
157
+ const builtin = readValidatedPackage(new URL('../assets/deepseek-whale/pet.json', import.meta.url), this.options, 'builtin');
158
+ next.set(DEFAULT_PET_ID, builtin);
159
+ if (existsSync(this.petRoot) && isDirectory(this.petRoot)) {
160
+ for (const entry of readdirSync(this.petRoot, { withFileTypes: true })) {
161
+ if (!entry.isDirectory() || entry.isSymbolicLink() || entry.name.startsWith('.'))
162
+ continue;
163
+ const directory = join(this.petRoot, entry.name);
164
+ if (entry.name === DEFAULT_PET_ID)
165
+ continue;
166
+ try {
167
+ const record = readValidatedPackage(join(directory, 'pet.json'), this.options, 'user');
168
+ if (next.has(record.descriptor.id))
169
+ throw new Error(`pet id ${record.descriptor.id} is duplicated`);
170
+ next.set(record.descriptor.id, record);
171
+ }
172
+ catch {
173
+ // A malformed user package is excluded from the catalog. The next
174
+ // explicit catalog read remains deterministic and never exposes its path.
175
+ }
176
+ }
177
+ }
178
+ this.records.clear();
179
+ for (const id of [DEFAULT_PET_ID, ...[...next.keys()].filter(key => key !== DEFAULT_PET_ID).sort()]) {
180
+ const record = next.get(id);
181
+ if (record !== undefined)
182
+ this.records.set(id, record);
183
+ }
184
+ const catalog = this.getCatalog();
185
+ for (const listener of this.listeners)
186
+ listener(catalog);
187
+ }
188
+ }
189
+ function readValidatedPackage(manifest, options, source) {
190
+ const maxManifestBytes = options.maxManifestBytes ?? 16 * 1024;
191
+ const maxSpriteBytes = options.maxSpriteBytes ?? 16 * 1024 * 1024;
192
+ const manifestBytes = readRegularFile(manifest, 'pet manifest', maxManifestBytes);
193
+ const relativeSprite = petSpritesheetPath(manifestBytes);
194
+ const spriteLocation = manifest instanceof URL
195
+ ? new URL(relativeSprite.replaceAll('\\', '/'), manifest)
196
+ : join(dirname(manifest), relativeSprite.replaceAll('\\', '/'));
197
+ if (typeof manifest === 'string' && typeof spriteLocation === 'string')
198
+ assertContainedFile(dirname(manifest), spriteLocation);
199
+ const spriteBytes = readRegularFile(spriteLocation, 'pet spritesheet', maxSpriteBytes);
200
+ const validated = validatePetPackageFiles(manifestBytes, spriteBytes, {
201
+ ...options,
202
+ assetUrl: '',
203
+ source,
204
+ });
205
+ if (source === 'user')
206
+ decodeWebpDimensions(spriteBytes, options.decodeTimeoutMs ?? 10_000);
207
+ const id = validated.descriptor.id;
208
+ const descriptor = Object.freeze({ ...validated.descriptor, assetUrl: `/__dsh/pet/assets/${id}/spritesheet.webp` });
209
+ if (descriptor.id !== id)
210
+ throw new Error(`pet package id ${descriptor.id} does not match its catalog key ${id}`);
211
+ return { descriptor, spriteBytes, source };
212
+ }
213
+ function readRegularFile(path, label, maxBytes) {
214
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
215
+ throw new Error(`${label} byte limit is invalid`);
216
+ const stat = lstatSync(path);
217
+ if (!stat.isFile() || stat.isSymbolicLink())
218
+ throw new Error(`${label} must be a regular file`);
219
+ if (stat.size > maxBytes)
220
+ throw new Error(`${label} exceeds the configured byte limit`);
221
+ const handle = openSync(path, 'r');
222
+ try {
223
+ const openedStat = fstatSync(handle);
224
+ if (!openedStat.isFile() || openedStat.size > maxBytes)
225
+ throw new Error(`${label} exceeds the configured byte limit`);
226
+ const bytes = new Uint8Array(openedStat.size + 1);
227
+ let offset = 0;
228
+ while (offset < bytes.byteLength) {
229
+ const count = readSync(handle, bytes, offset, bytes.byteLength - offset, null);
230
+ if (count === 0)
231
+ break;
232
+ offset += count;
233
+ }
234
+ if (offset !== openedStat.size)
235
+ throw new Error(`${label} changed while it was being read`);
236
+ return bytes.subarray(0, offset);
237
+ }
238
+ finally {
239
+ closeSync(handle);
240
+ }
241
+ }
242
+ function assertContainedFile(packageRoot, candidate) {
243
+ const canonicalRoot = realpathSync(packageRoot);
244
+ const canonicalCandidate = realpathSync(candidate);
245
+ const fromRoot = relative(canonicalRoot, canonicalCandidate);
246
+ if (fromRoot.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || fromRoot === '..' || isAbsolute(fromRoot)) {
247
+ throw new Error('pet spritesheet must stay inside its package directory');
248
+ }
249
+ }
250
+ function isDirectory(path) {
251
+ try {
252
+ return lstatSync(path).isDirectory();
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ }
258
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Client-namespace projection of the pet domain: a pure re-export of the
3
+ * package's types outlet. Client code imports ONLY the client namespace
4
+ * (repo discipline), so `./client` projects the same single-source content
5
+ * `./types` serves to host consumers — zero duplication.
6
+ * @module @luv1211/dsh-pet/client
7
+ */
8
+ export type * from './types.ts';
9
+ export type * from './renderer.ts';
10
+ export type { PetAnimationState, PetDragDirection, PetLookTarget, PetPresentation, PetPresentationInput } from './runtime.ts';
11
+ export { DEFAULT_PET_ID, DEFAULT_PET_SIZE_PX, MAX_PET_SIZE_PX, MIN_PET_SIZE_PX, PET_PREFERENCE_VERSION, PET_COMPAT_ATLAS, PetValidationError, comparePetActivities, defaultPetPreference, isDragMovement, petStatusForHostActivity, petWidthForSize, resolvePetPreference, selectLookDirection, selectPetPresentation, validatePetSize, validatePetPackage, } from './runtime.ts';
12
+ export { petSpriteAvatar, petSpriteFrame } from './renderer.ts';
13
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Client-namespace projection of the pet domain: a pure re-export of the
3
+ * package's types outlet. Client code imports ONLY the client namespace
4
+ * (repo discipline), so `./client` projects the same single-source content
5
+ * `./types` serves to host consumers — zero duplication.
6
+ * @module @luv1211/dsh-pet/client
7
+ */
8
+ export { DEFAULT_PET_ID, DEFAULT_PET_SIZE_PX, MAX_PET_SIZE_PX, MIN_PET_SIZE_PX, PET_PREFERENCE_VERSION, PET_COMPAT_ATLAS, PetValidationError, comparePetActivities, defaultPetPreference, isDragMovement, petStatusForHostActivity, petWidthForSize, resolvePetPreference, selectLookDirection, selectPetPresentation, validatePetSize, validatePetPackage, } from "./runtime.js";
9
+ export { petSpriteAvatar, petSpriteFrame } from "./renderer.js";
10
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,11 @@
1
+ /** Host-only complete WebP decoding for package publication. */
2
+ /** Decode every pixel in a bounded WebP inside an isolated process.
3
+ * @param bytes - already byte-limited candidate WebP data.
4
+ * @param timeoutMs - positive host-configured decode deadline.
5
+ * @returns dimensions reported by the successful decoder.
6
+ */
7
+ export declare function decodeWebpDimensions(bytes: Uint8Array, timeoutMs: number): {
8
+ readonly width: number;
9
+ readonly height: number;
10
+ };
11
+ //# sourceMappingURL=host-image.d.ts.map
@@ -0,0 +1,48 @@
1
+ /** Host-only complete WebP decoding for package publication. */
2
+ import { spawnSync } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess';
6
+ import { PetValidationError } from "./runtime.js";
7
+ const SHARP_ENTRY_URL = pathToFileURL(createRequire(import.meta.url).resolve('sharp')).href;
8
+ const DECODE_SCRIPT = `
9
+ import fs from 'node:fs';
10
+ import sharp from ${JSON.stringify(SHARP_ENTRY_URL)};
11
+ const input = fs.readFileSync(0);
12
+ try {
13
+ const result = await sharp(input).raw().toBuffer({ resolveWithObject: true });
14
+ process.stdout.write(JSON.stringify({ width: result.info.width, height: result.info.height }));
15
+ } catch (error) {
16
+ process.stderr.write(error instanceof Error ? error.message : String(error));
17
+ process.exitCode = 1;
18
+ }
19
+ `;
20
+ /** Decode every pixel in a bounded WebP inside an isolated process.
21
+ * @param bytes - already byte-limited candidate WebP data.
22
+ * @param timeoutMs - positive host-configured decode deadline.
23
+ * @returns dimensions reported by the successful decoder.
24
+ */
25
+ export function decodeWebpDimensions(bytes, timeoutMs) {
26
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
27
+ throw new PetValidationError('pet spritesheet decode timeout must be positive');
28
+ const result = spawnSync(process.execPath, ['--input-type=module', '--eval', DECODE_SCRIPT], {
29
+ input: bytes,
30
+ encoding: 'utf8',
31
+ env: scrubbedParentEnv(),
32
+ maxBuffer: 64 * 1024,
33
+ timeout: timeoutMs,
34
+ windowsHide: true,
35
+ });
36
+ if (result.status !== 0)
37
+ throw new PetValidationError('pet spritesheet must be a decodable WebP image');
38
+ try {
39
+ const dimensions = JSON.parse(result.stdout);
40
+ if (!Number.isSafeInteger(dimensions.width) || !Number.isSafeInteger(dimensions.height))
41
+ throw new Error('invalid decoder result');
42
+ return { width: dimensions.width, height: dimensions.height };
43
+ }
44
+ catch {
45
+ throw new PetValidationError('pet spritesheet decoder returned invalid dimensions');
46
+ }
47
+ }
48
+ //# sourceMappingURL=host-image.js.map
@@ -0,0 +1,17 @@
1
+ /** Host-native pet package and folder operations assembled from existing host seams. */
2
+ import type { DirectoryPickerCapability } from '@deepseek-ai/dsh-host-directory-picker';
3
+ import type { PetNativeActions } from './types.ts';
4
+ /** Injectable filesystem/open hooks used by the host adapter tests. */
5
+ export interface PetNativeActionInternals {
6
+ /** Read one selected package file. */
7
+ readFile?: (path: string) => Promise<Uint8Array>;
8
+ /** Open one host path with its default application. */
9
+ openPath?: (path: string) => Promise<void>;
10
+ }
11
+ /** Assemble pet actions only when the composed directory picker is native.
12
+ * @param capability - composed directory picker capability.
13
+ * @param internals - optional filesystem and host-open test seams.
14
+ * @returns native pet actions, or `undefined` for browser-only pickers.
15
+ */
16
+ export declare function createPetNativeActions(capability: DirectoryPickerCapability | undefined, internals?: PetNativeActionInternals): PetNativeActions | undefined;
17
+ //# sourceMappingURL=host-native.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** Host-native pet package and folder operations assembled from existing host seams. */
2
+ import { lstat, readFile } from 'node:fs/promises';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { openNativePath } from "./path-opener.js";
5
+ import { petSpritesheetPath } from "./runtime.js";
6
+ /** Assemble pet actions only when the composed directory picker is native.
7
+ * @param capability - composed directory picker capability.
8
+ * @param internals - optional filesystem and host-open test seams.
9
+ * @returns native pet actions, or `undefined` for browser-only pickers.
10
+ */
11
+ export function createPetNativeActions(capability, internals = {}) {
12
+ if (capability?.kind !== 'native')
13
+ return undefined;
14
+ const read = internals.readFile ?? (async (path) => new Uint8Array(await readFile(path)));
15
+ const open = internals.openPath ?? (path => openNativePath(path, new AbortController().signal));
16
+ return {
17
+ async pickPetPackage() {
18
+ const selected = await capability.pick(new AbortController().signal);
19
+ if (selected === null)
20
+ return null;
21
+ const directory = await packageDirectory(selected);
22
+ const manifestBytes = await read(join(directory, 'pet.json'));
23
+ const spritesheetPath = petSpritesheetPath(manifestBytes);
24
+ return {
25
+ manifestBytes,
26
+ spritesheetBytes: await read(join(directory, spritesheetPath.replaceAll('\\', '/'))),
27
+ };
28
+ },
29
+ openPetFolder: open,
30
+ };
31
+ }
32
+ /** Resolve a picker result to one package directory and reject links or unrelated files. */
33
+ async function packageDirectory(selected) {
34
+ const target = resolve(selected);
35
+ const targetStat = await lstat(target);
36
+ if (targetStat.isDirectory() && !targetStat.isSymbolicLink())
37
+ return target;
38
+ if (!targetStat.isFile() || targetStat.isSymbolicLink())
39
+ throw new Error('pet package selection must be a directory or package file');
40
+ const name = basename(target);
41
+ if (name !== 'pet.json' && name !== 'spritesheet.webp') {
42
+ throw new Error('pet package selection must name pet.json or spritesheet.webp');
43
+ }
44
+ const directory = dirname(target);
45
+ const directoryStat = await lstat(directory);
46
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
47
+ throw new Error('pet package directory is invalid');
48
+ return directory;
49
+ }
50
+ //# sourceMappingURL=host-native.js.map