@afromero/kin3o 0.1.0 → 0.2.1

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,68 @@
1
+ export interface LottieFilesAnimation {
2
+ id: number;
3
+ name: string;
4
+ slug: string;
5
+ uuid: string;
6
+ description: string | null;
7
+ url: string | null;
8
+ jsonUrl: string | null;
9
+ lottieUrl: string | null;
10
+ gifUrl: string | null;
11
+ imageUrl: string | null;
12
+ likesCount: number;
13
+ downloads: number | null;
14
+ createdBy: {
15
+ username: string;
16
+ } | null;
17
+ createdAt: string;
18
+ lottieFileSize: number | null;
19
+ frameRate: number | null;
20
+ }
21
+ export interface SearchResult {
22
+ totalCount: number;
23
+ animations: LottieFilesAnimation[];
24
+ pageInfo: {
25
+ hasNextPage: boolean;
26
+ endCursor: string | null;
27
+ };
28
+ }
29
+ export declare function searchAnimations(query: string, opts?: {
30
+ first?: number;
31
+ after?: string;
32
+ }): Promise<SearchResult>;
33
+ export declare function featuredAnimations(first?: number): Promise<SearchResult>;
34
+ export declare function popularAnimations(first?: number): Promise<SearchResult>;
35
+ export declare function recentAnimations(first?: number): Promise<SearchResult>;
36
+ export declare function fetchAnimationJson(jsonUrl: string): Promise<object>;
37
+ export declare function fetchAnimationByUuid(uuid: string): Promise<{
38
+ json: object;
39
+ meta: LottieFilesAnimation;
40
+ }>;
41
+ export interface ResolvedTarget {
42
+ json: object;
43
+ meta: LottieFilesAnimation | null;
44
+ lottieBuffer: Buffer | null;
45
+ }
46
+ export declare function resolveTarget(target: string, lottie?: boolean): Promise<ResolvedTarget>;
47
+ export declare function createLoginToken(appKey: string): Promise<{
48
+ loginUrl: string;
49
+ token: string;
50
+ }>;
51
+ export declare function pollForAccessToken(token: string, timeoutMs?: number): Promise<{
52
+ accessToken: string;
53
+ expiresAt: string;
54
+ }>;
55
+ export declare function createUploadRequest(authToken: string, filename: string, type: 'LOTTIE' | 'DOT_LOTTIE'): Promise<{
56
+ requestId: string;
57
+ presignedUrl: string;
58
+ }>;
59
+ export declare function uploadFile(presignedUrl: string, buffer: Buffer): Promise<void>;
60
+ export declare function publishAnimation(authToken: string, input: {
61
+ name: string;
62
+ requestId: string;
63
+ tags: string[];
64
+ description?: string;
65
+ }): Promise<{
66
+ id: number;
67
+ url: string;
68
+ }>;
@@ -0,0 +1,267 @@
1
+ const GRAPHQL_ENDPOINT = 'https://graphql.lottiefiles.com/';
2
+ const ANIM_FIELDS_FRAGMENT = `
3
+ fragment AnimFields on PublicAnimation {
4
+ id name slug uuid description url
5
+ jsonUrl lottieUrl gifUrl imageUrl
6
+ likesCount downloads
7
+ createdBy { username }
8
+ createdAt lottieFileSize frameRate
9
+ }
10
+ `;
11
+ async function gqlQuery(query, variables, token) {
12
+ const headers = { 'Content-Type': 'application/json' };
13
+ if (token)
14
+ headers['Authorization'] = `Bearer ${token}`;
15
+ let response;
16
+ try {
17
+ response = await fetch(GRAPHQL_ENDPOINT, {
18
+ method: 'POST',
19
+ headers,
20
+ body: JSON.stringify({ query, variables }),
21
+ });
22
+ }
23
+ catch {
24
+ throw new Error('Network error: unable to reach LottieFiles API');
25
+ }
26
+ if (!response.ok) {
27
+ throw new Error(`LottieFiles API returned HTTP ${response.status}`);
28
+ }
29
+ const body = (await response.json());
30
+ if (body.errors && body.errors.length > 0) {
31
+ const msg = body.errors.map(e => e.message).join('; ');
32
+ throw new Error(`LottieFiles API error: ${msg}`);
33
+ }
34
+ if (!body.data) {
35
+ throw new Error('LottieFiles API returned no data');
36
+ }
37
+ return body.data;
38
+ }
39
+ function parseEdges(edges) {
40
+ if (!edges)
41
+ return [];
42
+ return edges.map(e => e.node);
43
+ }
44
+ export async function searchAnimations(query, opts) {
45
+ const first = opts?.first ?? 20;
46
+ const variables = { query, first };
47
+ if (opts?.after)
48
+ variables['after'] = opts.after;
49
+ const data = await gqlQuery(`query($query: String!, $first: Int, $after: String) {
50
+ searchPublicAnimations(query: $query, first: $first, after: $after) {
51
+ totalCount
52
+ edges { node { ...AnimFields } }
53
+ pageInfo { hasNextPage endCursor }
54
+ }
55
+ }
56
+ ${ANIM_FIELDS_FRAGMENT}`, variables);
57
+ const result = data.searchPublicAnimations;
58
+ return {
59
+ totalCount: result.totalCount,
60
+ animations: parseEdges(result.edges),
61
+ pageInfo: result.pageInfo,
62
+ };
63
+ }
64
+ export async function featuredAnimations(first = 20) {
65
+ const data = await gqlQuery(`query($first: Int) {
66
+ featuredPublicAnimations(first: $first) {
67
+ totalCount
68
+ edges { node { ...AnimFields } }
69
+ pageInfo { hasNextPage endCursor }
70
+ }
71
+ }
72
+ ${ANIM_FIELDS_FRAGMENT}`, { first });
73
+ const result = data.featuredPublicAnimations;
74
+ return {
75
+ totalCount: result.totalCount,
76
+ animations: parseEdges(result.edges),
77
+ pageInfo: result.pageInfo,
78
+ };
79
+ }
80
+ export async function popularAnimations(first = 20) {
81
+ const data = await gqlQuery(`query($first: Int) {
82
+ popularPublicAnimations(first: $first) {
83
+ totalCount
84
+ edges { node { ...AnimFields } }
85
+ pageInfo { hasNextPage endCursor }
86
+ }
87
+ }
88
+ ${ANIM_FIELDS_FRAGMENT}`, { first });
89
+ const result = data.popularPublicAnimations;
90
+ return {
91
+ totalCount: result.totalCount,
92
+ animations: parseEdges(result.edges),
93
+ pageInfo: result.pageInfo,
94
+ };
95
+ }
96
+ export async function recentAnimations(first = 20) {
97
+ const data = await gqlQuery(`query($first: Int) {
98
+ recentPublicAnimations(first: $first) {
99
+ totalCount
100
+ edges { node { ...AnimFields } }
101
+ pageInfo { hasNextPage endCursor }
102
+ }
103
+ }
104
+ ${ANIM_FIELDS_FRAGMENT}`, { first });
105
+ const result = data.recentPublicAnimations;
106
+ return {
107
+ totalCount: result.totalCount,
108
+ animations: parseEdges(result.edges),
109
+ pageInfo: result.pageInfo,
110
+ };
111
+ }
112
+ export async function fetchAnimationJson(jsonUrl) {
113
+ let response;
114
+ try {
115
+ response = await fetch(jsonUrl);
116
+ }
117
+ catch {
118
+ throw new Error(`Network error: unable to fetch animation from ${jsonUrl}`);
119
+ }
120
+ if (!response.ok) {
121
+ throw new Error(`Failed to fetch animation: HTTP ${response.status}`);
122
+ }
123
+ return (await response.json());
124
+ }
125
+ export async function fetchAnimationByUuid(uuid) {
126
+ const data = await gqlQuery(`query($hash: String!) {
127
+ publicAnimationByHash(hash: $hash) { ...AnimFields }
128
+ }
129
+ ${ANIM_FIELDS_FRAGMENT}`, { hash: uuid });
130
+ const anim = data.publicAnimationByHash;
131
+ if (!anim) {
132
+ throw new Error(`Animation not found: ${uuid}`);
133
+ }
134
+ if (!anim.jsonUrl) {
135
+ throw new Error(`Animation "${anim.name}" has no JSON URL available`);
136
+ }
137
+ const json = await fetchAnimationJson(anim.jsonUrl);
138
+ return { json, meta: anim };
139
+ }
140
+ export async function resolveTarget(target, lottie = false) {
141
+ if (target.startsWith('http')) {
142
+ const url = new URL(target);
143
+ // LottieFiles page URL → extract hash from last segment after last hyphen
144
+ if (url.hostname === 'lottiefiles.com' || url.hostname.endsWith('.lottiefiles.com')) {
145
+ if (url.hostname.startsWith('assets') || url.hostname === 'lottie.host') {
146
+ // Direct CDN URL
147
+ if (lottie) {
148
+ const buffer = await fetchLottieBuffer(target);
149
+ return { json: {}, meta: null, lottieBuffer: buffer };
150
+ }
151
+ const json = await fetchAnimationJson(target);
152
+ return { json, meta: null, lottieBuffer: null };
153
+ }
154
+ // Page URL — extract hash from path
155
+ const segments = url.pathname.split('/').filter(Boolean);
156
+ const lastSegment = segments[segments.length - 1];
157
+ if (!lastSegment)
158
+ throw new Error('Could not extract animation hash from URL');
159
+ const parts = lastSegment.split('-');
160
+ const hash = parts[parts.length - 1];
161
+ if (!hash)
162
+ throw new Error('Could not extract animation hash from URL');
163
+ if (lottie) {
164
+ const { meta } = await fetchAnimationByUuid(hash);
165
+ if (!meta.lottieUrl)
166
+ throw new Error(`Animation "${meta.name}" has no .lottie URL available`);
167
+ const buffer = await fetchLottieBuffer(meta.lottieUrl);
168
+ return { json: {}, meta, lottieBuffer: buffer };
169
+ }
170
+ const { json, meta } = await fetchAnimationByUuid(hash);
171
+ return { json, meta, lottieBuffer: null };
172
+ }
173
+ // Direct CDN URL for other hosts (lottie.host, etc.)
174
+ if (url.hostname === 'lottie.host') {
175
+ if (lottie) {
176
+ const buffer = await fetchLottieBuffer(target);
177
+ return { json: {}, meta: null, lottieBuffer: buffer };
178
+ }
179
+ const json = await fetchAnimationJson(target);
180
+ return { json, meta: null, lottieBuffer: null };
181
+ }
182
+ // Generic URL — try fetching directly
183
+ if (lottie) {
184
+ const buffer = await fetchLottieBuffer(target);
185
+ return { json: {}, meta: null, lottieBuffer: buffer };
186
+ }
187
+ const json = await fetchAnimationJson(target);
188
+ return { json, meta: null, lottieBuffer: null };
189
+ }
190
+ // UUID/hash pattern
191
+ if (/^[a-zA-Z0-9]{8,}$/.test(target)) {
192
+ if (lottie) {
193
+ const { meta } = await fetchAnimationByUuid(target);
194
+ if (!meta.lottieUrl)
195
+ throw new Error(`Animation "${meta.name}" has no .lottie URL available`);
196
+ const buffer = await fetchLottieBuffer(meta.lottieUrl);
197
+ return { json: {}, meta, lottieBuffer: buffer };
198
+ }
199
+ const { json, meta } = await fetchAnimationByUuid(target);
200
+ return { json, meta, lottieBuffer: null };
201
+ }
202
+ throw new Error('Unrecognized target. Provide a UUID, LottieFiles URL, or CDN URL.');
203
+ }
204
+ async function fetchLottieBuffer(url) {
205
+ let response;
206
+ try {
207
+ response = await fetch(url);
208
+ }
209
+ catch {
210
+ throw new Error(`Network error: unable to fetch .lottie from ${url}`);
211
+ }
212
+ if (!response.ok) {
213
+ throw new Error(`Failed to fetch .lottie: HTTP ${response.status}`);
214
+ }
215
+ const arrayBuffer = await response.arrayBuffer();
216
+ return Buffer.from(arrayBuffer);
217
+ }
218
+ export async function createLoginToken(appKey) {
219
+ const data = await gqlQuery(`mutation($appKey: String!) {
220
+ createLoginToken(input: { appKey: $appKey }) { token loginUrl }
221
+ }`, { appKey });
222
+ return data.createLoginToken;
223
+ }
224
+ export async function pollForAccessToken(token, timeoutMs = 120_000) {
225
+ const start = Date.now();
226
+ while (Date.now() - start < timeoutMs) {
227
+ await new Promise(r => setTimeout(r, 3000));
228
+ try {
229
+ const result = await gqlQuery('mutation($token: String!) { tokenLogin(token: $token) { accessToken expiresAt } }', { token });
230
+ if (result.tokenLogin.accessToken)
231
+ return result.tokenLogin;
232
+ }
233
+ catch {
234
+ // User hasn't completed login yet — keep polling
235
+ }
236
+ }
237
+ throw new Error('Login timed out. Please try again.');
238
+ }
239
+ export async function createUploadRequest(authToken, filename, type) {
240
+ const data = await gqlQuery(`mutation($input: PublicAnimationUploadRequestCreateInput!) {
241
+ publicAnimationUploadRequestCreate(input: $input) { requestId presignedUrl }
242
+ }`, { input: { filename, type } }, authToken);
243
+ return data.publicAnimationUploadRequestCreate;
244
+ }
245
+ export async function uploadFile(presignedUrl, buffer) {
246
+ let response;
247
+ try {
248
+ const body = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
249
+ response = await fetch(presignedUrl, {
250
+ method: 'PUT',
251
+ body,
252
+ headers: { 'Content-Type': 'application/octet-stream' },
253
+ });
254
+ }
255
+ catch {
256
+ throw new Error('Network error: unable to upload file');
257
+ }
258
+ if (!response.ok) {
259
+ throw new Error(`Upload failed: HTTP ${response.status}`);
260
+ }
261
+ }
262
+ export async function publishAnimation(authToken, input) {
263
+ const data = await gqlQuery(`mutation($input: PublicAnimationCreateInput!) {
264
+ publicAnimationCreate(input: $input) { id url }
265
+ }`, { input }, authToken);
266
+ return data.publicAnimationCreate;
267
+ }
@@ -8,9 +8,9 @@
8
8
  * Each mode has its own few-shot examples tuned for that output format.
9
9
  */
10
10
  export { LOTTIE_FORMAT_REFERENCE } from './system.js';
11
- export { buildSystemPrompt } from './system.js';
11
+ export { buildSystemPrompt, buildRefinementUserPrompt } from './system.js';
12
12
  export { PULSING_CIRCLE, WAVEFORM_BARS } from './examples.js';
13
- export { buildInteractiveSystemPrompt } from './system-interactive.js';
13
+ export { buildInteractiveSystemPrompt, buildInteractiveRefinementUserPrompt } from './system-interactive.js';
14
14
  export { INTERACTIVE_BUTTON } from './examples-interactive.js';
15
15
  export { MASCOT_STATIC, MASCOT_INTERACTIVE } from './examples-mascot.js';
16
16
  export { loadDesignTokens, SOTTO_TOKENS } from './tokens.js';
@@ -10,10 +10,10 @@
10
10
  // ── Shared ──
11
11
  export { LOTTIE_FORMAT_REFERENCE } from './system.js';
12
12
  // ── Static mode ──
13
- export { buildSystemPrompt } from './system.js';
13
+ export { buildSystemPrompt, buildRefinementUserPrompt } from './system.js';
14
14
  export { PULSING_CIRCLE, WAVEFORM_BARS } from './examples.js';
15
15
  // ── Interactive mode ──
16
- export { buildInteractiveSystemPrompt } from './system-interactive.js';
16
+ export { buildInteractiveSystemPrompt, buildInteractiveRefinementUserPrompt } from './system-interactive.js';
17
17
  export { INTERACTIVE_BUTTON } from './examples-interactive.js';
18
18
  // ── Mascot ──
19
19
  export { MASCOT_STATIC, MASCOT_INTERACTIVE } from './examples-mascot.js';
@@ -1,2 +1,4 @@
1
1
  import type { DesignTokens } from './tokens.js';
2
+ /** Build the user prompt for a refinement request (interactive mode) */
3
+ export declare function buildInteractiveRefinementUserPrompt(currentEnvelope: string, instruction: string): string;
2
4
  export declare function buildInteractiveSystemPrompt(tokens?: DesignTokens): string;
@@ -1,5 +1,15 @@
1
1
  import { LOTTIE_FORMAT_REFERENCE } from './system.js';
2
2
  import { INTERACTIVE_BUTTON } from './examples-interactive.js';
3
+ /** Build the user prompt for a refinement request (interactive mode) */
4
+ export function buildInteractiveRefinementUserPrompt(currentEnvelope, instruction) {
5
+ return `Here is the current interactive animation envelope (animations + state machine):
6
+
7
+ ${currentEnvelope}
8
+
9
+ Refine this interactive animation according to the following instruction: ${instruction}
10
+
11
+ Output ONLY the complete updated envelope JSON with all animations and the state machine. Preserve the overall structure and only modify what the instruction requires. Do not add explanation or commentary.`;
12
+ }
3
13
  export function buildInteractiveSystemPrompt(tokens) {
4
14
  const sections = [];
5
15
  // 1. Role + output rules
@@ -1,3 +1,5 @@
1
1
  import type { DesignTokens } from './tokens.js';
2
2
  export declare const LOTTIE_FORMAT_REFERENCE = "\nLOTTIE FORMAT REFERENCE:\n\nTop-level required fields:\n- \"v\": string (use \"5.5.2\")\n- \"fr\": number (use 60 for 60fps)\n- \"ip\": 0 (in-point, always 0)\n- \"op\": number (out-point, end frame \u2014 e.g. 120 for 2s at 60fps)\n- \"w\": number (width in pixels)\n- \"h\": number (height in pixels)\n- \"ddd\": 0 (no 3D)\n- \"assets\": [] (empty array)\n- \"layers\": array of layer objects\n\nLayer (shape layer, ty=4):\n- \"ty\": 4 (shape layer)\n- \"ind\": number (unique index)\n- \"nm\": string (layer name)\n- \"ip\": 0, \"op\": same as top-level op\n- \"st\": 0 (start time)\n- \"ddd\": 0\n- \"ks\": transform object\n- \"shapes\": array of shape objects\n- \"bm\": 0 (blend mode, normal)\n\nTransform \"ks\" object:\n- \"a\": anchor point (VECTOR property)\n- \"p\": position (VECTOR property)\n- \"s\": scale (VECTOR property, [100,100] = 100%)\n- \"r\": rotation (SCALAR property, degrees)\n- \"o\": opacity (SCALAR property, 0-100)\n\nPROPERTY TYPES \u2014 this distinction is critical:\n- VECTOR property (position, scale, anchor, size): {\"a\":0,\"k\":[x,y]} or animated {\"a\":1,\"k\":[keyframes]}\n- SCALAR property (rotation, opacity, roundness, stroke width): {\"a\":0,\"k\":0} or animated {\"a\":1,\"k\":[keyframes]}\n- COLOR property: {\"a\":0,\"k\":[r,g,b,1]} with 0-1 floats (NOT 0-255)\n\nKeyframe format:\n{\"t\":frame,\"s\":[values],\"o\":{\"x\":[n],\"y\":[n]},\"i\":{\"x\":[n],\"y\":[n]}}\n- \"t\": frame number\n- \"s\": start values (array for vector, array with single value for scalar)\n- \"o\": out-tangent (ease out), \"i\": in-tangent (ease in)\n- Last keyframe needs only \"t\" and \"s\" (no tangents)\n\nEasing (ease-in-out): \"o\":{\"x\":[0.42],\"y\":[0]}, \"i\":{\"x\":[0.58],\"y\":[1]}\n\nShape types:\n- \"el\": ellipse \u2014 \"p\" (center, vector), \"s\" (size, vector)\n- \"rc\": rectangle \u2014 \"p\" (center, vector), \"s\" (size, vector), \"r\" (roundness, scalar)\n- \"sh\": path \u2014 \"ks\" with bezier {\"c\":bool, \"v\":[[x,y],...], \"i\":[[dx,dy],...], \"o\":[[dx,dy],...]}\n- \"fl\": fill \u2014 \"c\" (color), \"o\" (opacity, scalar), \"r\" (fill rule, 1=nonzero)\n- \"st\": stroke \u2014 \"c\" (color), \"o\" (opacity), \"w\" (width, scalar), \"lc\" (line cap, 2=round), \"lj\" (line join, 2=round)\n- \"gr\": group \u2014 \"it\" array of shapes + MUST end with \"tr\" (group transform)\n- \"tr\": group transform \u2014 same fields as layer transform \"ks\"\n- \"tm\": trim path \u2014 \"s\" (start%, scalar), \"e\" (end%, scalar), \"o\" (offset, scalar)\n\nCRITICAL: Groups MUST have \"tr\" (transform) as the LAST item in their \"it\" array.";
3
+ /** Build the user prompt for a refinement request (static mode) */
4
+ export declare function buildRefinementUserPrompt(currentJson: string, instruction: string): string;
3
5
  export declare function buildSystemPrompt(tokens?: DesignTokens): string;
@@ -56,6 +56,16 @@ Shape types:
56
56
  - "tm": trim path — "s" (start%, scalar), "e" (end%, scalar), "o" (offset, scalar)
57
57
 
58
58
  CRITICAL: Groups MUST have "tr" (transform) as the LAST item in their "it" array.`;
59
+ /** Build the user prompt for a refinement request (static mode) */
60
+ export function buildRefinementUserPrompt(currentJson, instruction) {
61
+ return `Here is the current Lottie animation JSON:
62
+
63
+ ${currentJson}
64
+
65
+ Refine this animation according to the following instruction: ${instruction}
66
+
67
+ Output ONLY the complete updated Lottie JSON. Preserve the overall structure and only modify what the instruction requires. Do not add explanation or commentary.`;
68
+ }
59
69
  export function buildSystemPrompt(tokens) {
60
70
  const sections = [];
61
71
  // 1. Role + output rules
package/dist/utils.d.ts CHANGED
@@ -18,3 +18,5 @@ export declare function hexToLottieColor(hex: string): [number, number, number,
18
18
  export declare function slugify(text: string, maxLength?: number): string;
19
19
  /** Ensure the output directory exists */
20
20
  export declare function ensureOutputDir(dir?: string): string;
21
+ /** Compute the next versioned output path for a refinement */
22
+ export declare function nextVersionedPath(inputPath: string, outputDir: string): string;
package/dist/utils.js CHANGED
@@ -1,5 +1,5 @@
1
- import { mkdirSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { mkdirSync, readdirSync } from 'node:fs';
2
+ import { join, parse as parsePath } from 'node:path';
3
3
  /** Strip benign CLI warnings (e.g. PATH update failures) from stderr */
4
4
  export function filterCliStderr(stderr) {
5
5
  return stderr
@@ -87,3 +87,28 @@ export function ensureOutputDir(dir = 'output') {
87
87
  mkdirSync(outputPath, { recursive: true });
88
88
  return outputPath;
89
89
  }
90
+ /** Compute the next versioned output path for a refinement */
91
+ export function nextVersionedPath(inputPath, outputDir) {
92
+ const { name, ext } = parsePath(inputPath);
93
+ // Strip existing -vN suffix to get base name
94
+ const baseName = name.replace(/-v\d+$/, '');
95
+ // Scan output dir for existing versioned files
96
+ const versionPattern = new RegExp(`^${escapeRegex(baseName)}-v(\\d+)\\${ext}$`);
97
+ let maxVersion = 1;
98
+ try {
99
+ for (const file of readdirSync(outputDir)) {
100
+ const match = file.match(versionPattern);
101
+ if (match) {
102
+ maxVersion = Math.max(maxVersion, parseInt(match[1], 10));
103
+ }
104
+ }
105
+ }
106
+ catch {
107
+ // Output dir may not exist yet — that's fine, start at v2
108
+ }
109
+ const nextVersion = maxVersion + 1;
110
+ return join(outputDir, `${baseName}-v${nextVersion}${ext}`);
111
+ }
112
+ function escapeRegex(str) {
113
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114
+ }
package/dist/view.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface ViewOptions {
2
+ port?: number;
3
+ openBrowser?: boolean;
4
+ }
5
+ export interface ViewServer {
6
+ url: string;
7
+ close(): void;
8
+ }
9
+ export declare function startViewServer(filePath: string, options?: ViewOptions): Promise<ViewServer>;
package/dist/view.js ADDED
@@ -0,0 +1,99 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync, watch } from 'node:fs';
3
+ import { join, extname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import open from 'open';
6
+ const templateDir = join(fileURLToPath(new URL('.', import.meta.url)), '..', 'preview');
7
+ const SSE_SCRIPT = `<script>
8
+ (function(){
9
+ var es = new EventSource('/events');
10
+ es.onmessage = function(){ location.reload(); };
11
+ es.onerror = function(){ es.close(); };
12
+ })();
13
+ </script>`;
14
+ function renderHtml(filePath) {
15
+ const ext = extname(filePath).toLowerCase();
16
+ let template;
17
+ let html;
18
+ if (ext === '.lottie') {
19
+ template = readFileSync(join(templateDir, 'template-interactive.html'), 'utf-8');
20
+ const base64 = readFileSync(filePath).toString('base64');
21
+ html = template.replace('__DOTLOTTIE_DATA_BASE64__', base64);
22
+ }
23
+ else {
24
+ template = readFileSync(join(templateDir, 'template.html'), 'utf-8');
25
+ const json = readFileSync(filePath, 'utf-8');
26
+ html = template.replace('__ANIMATION_DATA__', json);
27
+ }
28
+ return html.replace('</body>', SSE_SCRIPT + '</body>');
29
+ }
30
+ export function startViewServer(filePath, options) {
31
+ const clients = new Set();
32
+ let debounceTimer = null;
33
+ let watcher = null;
34
+ const server = createServer((req, res) => {
35
+ if (req.url === '/' || req.url === '') {
36
+ try {
37
+ const html = renderHtml(filePath);
38
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
39
+ res.end(html);
40
+ }
41
+ catch (err) {
42
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
43
+ res.end(`Error: ${err instanceof Error ? err.message : String(err)}`);
44
+ }
45
+ return;
46
+ }
47
+ if (req.url === '/events') {
48
+ res.writeHead(200, {
49
+ 'Content-Type': 'text/event-stream',
50
+ 'Cache-Control': 'no-cache',
51
+ 'Connection': 'keep-alive',
52
+ });
53
+ res.write('\n');
54
+ clients.add(res);
55
+ req.on('close', () => clients.delete(res));
56
+ return;
57
+ }
58
+ if (req.url === '/favicon.ico') {
59
+ res.writeHead(204);
60
+ res.end();
61
+ return;
62
+ }
63
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
64
+ res.end('Not found');
65
+ });
66
+ // Watch for file changes
67
+ watcher = watch(filePath, () => {
68
+ if (debounceTimer)
69
+ clearTimeout(debounceTimer);
70
+ debounceTimer = setTimeout(() => {
71
+ for (const client of clients) {
72
+ client.write('data: reload\n\n');
73
+ }
74
+ }, 100);
75
+ });
76
+ const requestedPort = options?.port ?? 0;
77
+ return new Promise((resolve) => {
78
+ server.listen(requestedPort, () => {
79
+ const port = server.address().port;
80
+ const url = `http://localhost:${port}`;
81
+ if (options?.openBrowser !== false) {
82
+ open(url);
83
+ }
84
+ resolve({
85
+ url,
86
+ close() {
87
+ if (debounceTimer)
88
+ clearTimeout(debounceTimer);
89
+ watcher?.close();
90
+ for (const client of clients) {
91
+ client.end();
92
+ }
93
+ clients.clear();
94
+ server.close();
95
+ },
96
+ });
97
+ });
98
+ });
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afromero/kin3o",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "AI-powered Lottie animation generator — text to motion from your terminal",
5
5
  "type": "module",
6
6
  "license": "MIT",