@afromero/kin3o 0.1.0

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 (45) hide show
  1. package/README.md +204 -0
  2. package/dist/brand.d.ts +39 -0
  3. package/dist/brand.js +63 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +249 -0
  6. package/dist/packager.d.ts +16 -0
  7. package/dist/packager.js +40 -0
  8. package/dist/preview.d.ts +2 -0
  9. package/dist/preview.js +30 -0
  10. package/dist/prompts/examples-interactive.d.ts +339 -0
  11. package/dist/prompts/examples-interactive.js +139 -0
  12. package/dist/prompts/examples-mascot.d.ts +765 -0
  13. package/dist/prompts/examples-mascot.js +319 -0
  14. package/dist/prompts/examples.d.ts +238 -0
  15. package/dist/prompts/examples.js +168 -0
  16. package/dist/prompts/index.d.ts +17 -0
  17. package/dist/prompts/index.js +21 -0
  18. package/dist/prompts/system-interactive.d.ts +2 -0
  19. package/dist/prompts/system-interactive.js +93 -0
  20. package/dist/prompts/system.d.ts +3 -0
  21. package/dist/prompts/system.js +94 -0
  22. package/dist/prompts/tokens.d.ts +6 -0
  23. package/dist/prompts/tokens.js +28 -0
  24. package/dist/providers/anthropic.d.ts +2 -0
  25. package/dist/providers/anthropic.js +25 -0
  26. package/dist/providers/claude.d.ts +2 -0
  27. package/dist/providers/claude.js +47 -0
  28. package/dist/providers/codex.d.ts +2 -0
  29. package/dist/providers/codex.js +60 -0
  30. package/dist/providers/registry.d.ts +18 -0
  31. package/dist/providers/registry.js +62 -0
  32. package/dist/state-machine-validator.d.ts +6 -0
  33. package/dist/state-machine-validator.js +182 -0
  34. package/dist/utils.d.ts +20 -0
  35. package/dist/utils.js +89 -0
  36. package/dist/validator.d.ts +8 -0
  37. package/dist/validator.js +195 -0
  38. package/examples/interactive-button.lottie +0 -0
  39. package/examples/mascot.json +760 -0
  40. package/examples/mascot.lottie +0 -0
  41. package/examples/pulse.json +75 -0
  42. package/examples/waveform.json +179 -0
  43. package/package.json +54 -0
  44. package/preview/template-interactive.html +223 -0
  45. package/preview/template.html +133 -0
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ <div align="center">
2
+
3
+ # kin3o
4
+
5
+ **Text to Motion. From your terminal.**
6
+
7
+ [![npm](https://img.shields.io/npm/v/kin3o)](https://www.npmjs.com/package/kin3o)
8
+ [![CI](https://github.com/affromero/kin3o/actions/workflows/ci.yml/badge.svg)](https://github.com/affromero/kin3o/actions/workflows/ci.yml)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
11
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/affromero/kin3o/pulls)
12
+
13
+ AI-powered Lottie animation generator. Turns natural language prompts into valid, playable Lottie JSON and interactive dotLottie state machines using your existing Claude or Codex subscription.
14
+
15
+ </div>
16
+
17
+ ## Why?
18
+
19
+ Every motion design tool (Rive, LottieFiles, Hera) sandboxes its AI inside a walled-garden editor, charges per-generation credits, and requires designs we don't have. kin3o spawns `claude --print` or `codex exec` as subprocesses to use existing Max/Pro subscriptions at zero marginal cost, with full context control via system prompts.
20
+
21
+ ## Competitors
22
+
23
+ | Feature | **kin3o** | [LottieFiles](https://lottiefiles.com) | [LottieGen](https://lottiegenai.webflow.io/) | [Recraft](https://www.recraft.ai) | [Lottielab](https://www.lottielab.com) |
24
+ |---------|-----------|-----------------|---------------|-------------|---------------|
25
+ | Text → Lottie JSON | **Yes** | Yes (Motion Copilot) | Yes | Yes (via export) | No |
26
+ | Text → State machines | **Yes (dotLottie)** | No | No | No | No |
27
+ | CLI | **Yes** | No | No | No | No |
28
+ | Web editor | **CLI-first** | Yes | Yes | Yes | Yes |
29
+ | Open source | **Yes** | No | No | No | No |
30
+ | Uses your own AI sub | **Yes** | No | No | No | No |
31
+ | Custom design tokens | **Yes** | No | No | No | No |
32
+ | Animation library | **Compatible with [LottieFiles](https://lottiefiles.com)** | Yes (massive) | No | Yes | Yes |
33
+ | State machines | **Yes (dotLottie)** | Yes | No | No | Yes |
34
+ | Team collaboration | **Git-native** | Yes | No | Yes | Yes |
35
+ | Programmatic access | **Yes (CLI + stdout)** | Yes (REST API) | No | Yes (REST API) | Yes (REST API) |
36
+ | **Price** | **Free (OSS)** | Free / $19.99+/mo | Waitlist (TBD) | Free (50/day) / $10+/mo | Free / up to $99/mo |
37
+
38
+ > Prices as of March 2026.
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ # Install globally
44
+ npm install -g kin3o
45
+
46
+ # Generate a static animation
47
+ kin3o generate "loading spinner with 3 dots"
48
+
49
+ # Generate an interactive state machine
50
+ kin3o generate "toggle switch with on/off states" --interactive
51
+ ```
52
+
53
+ Or use without installing:
54
+
55
+ ```bash
56
+ npx kin3o generate "pulsing circle"
57
+ ```
58
+
59
+ ## CLI Usage
60
+
61
+ ```bash
62
+ # Static animations (.json)
63
+ kin3o generate "pulsing circle that breathes"
64
+ kin3o generate "5-bar audio waveform" --provider claude-code --model sonnet
65
+ kin3o generate "notification bell" --no-preview --output bell.json
66
+ kin3o generate "loading dots" --tokens sotto
67
+
68
+ # Interactive state machines (.lottie)
69
+ kin3o generate "toggle switch with on/off states" --interactive
70
+ kin3o generate "like button with hover and click" --interactive
71
+
72
+ # Preview
73
+ kin3o preview output/animation.json
74
+ kin3o preview output/animation.lottie
75
+
76
+ # Validate
77
+ kin3o validate output/animation.json
78
+ kin3o validate output/animation.lottie
79
+
80
+ # List available AI providers
81
+ kin3o providers
82
+ ```
83
+
84
+ ### Options
85
+
86
+ | Flag | Description |
87
+ |------|-------------|
88
+ | `-p, --provider <name>` | AI provider (`claude-code`, `codex`, `anthropic`) |
89
+ | `-m, --model <name>` | Model (`sonnet`, `opus`, `haiku`, `codex`) |
90
+ | `-o, --output <path>` | Output filename |
91
+ | `--no-preview` | Skip browser preview |
92
+ | `-i, --interactive` | Generate interactive state machine (`.lottie` output) |
93
+ | `-t, --tokens <path>` | Design tokens JSON or `sotto` preset |
94
+
95
+ ## Using Generated Animations
96
+
97
+ ### Static animations (`.json`)
98
+
99
+ ```tsx
100
+ // React (lottie-react)
101
+ import Lottie from 'lottie-react';
102
+ import animationData from './pulsing-circle.json';
103
+
104
+ <Lottie animationData={animationData} loop autoplay />
105
+ ```
106
+
107
+ ```html
108
+ <!-- Vanilla JS (lottie-web) -->
109
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js"></script>
110
+ <div id="anim"></div>
111
+ <script>
112
+ lottie.loadAnimation({
113
+ container: document.getElementById('anim'),
114
+ path: './pulsing-circle.json',
115
+ loop: true,
116
+ autoplay: true,
117
+ });
118
+ </script>
119
+ ```
120
+
121
+ ```swift
122
+ // iOS (lottie-ios)
123
+ let animationView = LottieAnimationView(name: "pulsing-circle")
124
+ animationView.loopMode = .loop
125
+ animationView.play()
126
+ ```
127
+
128
+ ### Interactive state machines (`.lottie`)
129
+
130
+ ```html
131
+ <!-- dotlottie-web — hover, click, and tap state transitions -->
132
+ <script type="module">
133
+ import { DotLottie } from 'https://esm.sh/@lottiefiles/dotlottie-web';
134
+ const dotLottie = new DotLottie({
135
+ canvas: document.querySelector('canvas'),
136
+ src: './toggle-switch.lottie',
137
+ autoplay: true,
138
+ });
139
+ </script>
140
+ ```
141
+
142
+ ## Why Lottie over Rive?
143
+
144
+ ### For AI generation
145
+
146
+ | Criteria | Lottie | Rive |
147
+ |----------|--------|------|
148
+ | Format | JSON (text, diffable) | Binary (.riv) |
149
+ | LLM can generate? | Yes (structured JSON) | No (binary, no public writer) |
150
+ | Web runtime | lottie-web (165KB) | @rive-app/canvas (200KB+) |
151
+ | React integration | lottie-react | @rive-app/react-canvas |
152
+ | Ecosystem | Massive (LottieFiles, AE) | Growing (Rive editor) |
153
+ | **Verdict for AI gen** | **Winner** | Binary format = blocker |
154
+
155
+ ### Animation capabilities
156
+
157
+ | Capability | Lottie | Rive |
158
+ |-----------|--------|------|
159
+ | Shape animation (scale, rotate, position, opacity) | ✓ | ✓ |
160
+ | Path morphing, trim paths | ✓ | ✓ |
161
+ | Color transitions | ✓ | ✓ |
162
+ | Multi-layer compositions | ✓ | ✓ |
163
+ | Masking, mattes | ✓ | ✓ |
164
+ | Easing curves | ✓ | ✓ |
165
+ | State machines (hover, click, drag → states) | ✓ (dotLottie) | ✓ |
166
+ | Skeletal/bone animation | ✗ | ✓ |
167
+ | Mesh deformation | ✗ | ✓ |
168
+ | Runtime input (eyes follow cursor, sliders) | ✗ | ✓ |
169
+
170
+ Lottie covers ~90% of real-world animation needs. The 10% Rive wins on — skeletal animation, mesh deformation — requires an interactive editor to wire up, not something generatable from a text prompt. State machines are now covered by kin3o via dotLottie.
171
+
172
+ ## Architecture
173
+
174
+ ```
175
+ Static: prompt → generate() → extractJson() → validateLottie() → autoFix() → .json → preview
176
+ Interactive: prompt → generate() → extractInteractiveJson() → validate animations + state machine → .lottie → preview
177
+ ```
178
+
179
+ ### Prompt System
180
+
181
+ All prompts live in `src/prompts/` with a barrel export at `src/prompts/index.ts`:
182
+
183
+ | Module | Purpose |
184
+ |--------|---------|
185
+ | `system.ts` | Static Lottie generation prompt + `LOTTIE_FORMAT_REFERENCE` |
186
+ | `system-interactive.ts` | Interactive state machine prompt (imports shared ref) |
187
+ | `examples.ts` | Few-shot: pulsing circle, waveform bars |
188
+ | `examples-interactive.ts` | Few-shot: interactive button (idle/hover/pressed) |
189
+ | `examples-mascot.ts` | kin3o mascot/logo (static + interactive) |
190
+ | `tokens.ts` | Design token loader (hex → Lottie RGBA) |
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ npm install
196
+ npm run typecheck # Type check
197
+ npm run test # Run tests (node --test)
198
+ npm run ci # typecheck + test
199
+ npm run build # Compile to dist/
200
+ ```
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,39 @@
1
+ /**
2
+ * kin3o brand tokens — single source of truth for colors, fonts, and styling.
3
+ * Used by landing page generator, preview template, and design token presets.
4
+ */
5
+ export declare const brand: {
6
+ readonly name: "kin3o";
7
+ readonly tagline: "Text to Motion";
8
+ readonly description: "AI-powered Lottie animation generator";
9
+ readonly colors: {
10
+ readonly accent: "#A78BFA";
11
+ readonly accentBright: "#C4B5FD";
12
+ readonly accentDeep: "#7C3AED";
13
+ readonly warm: "#E2B96F";
14
+ readonly warmMuted: "rgba(226, 185, 111, 0.7)";
15
+ readonly bg: "#08080A";
16
+ readonly bgElevated: "#0E0E12";
17
+ readonly bgCard: "#131318";
18
+ readonly bgCardHover: "#1A1A21";
19
+ readonly border: "rgba(255, 255, 255, 0.05)";
20
+ readonly borderHover: "rgba(255, 255, 255, 0.1)";
21
+ readonly borderAccent: "rgba(167, 139, 250, 0.15)";
22
+ readonly text: "#EDEDF0";
23
+ readonly textSecondary: "#9494A0";
24
+ readonly textMuted: "#4E4E5A";
25
+ readonly success: "#34D399";
26
+ readonly error: "#EF4444";
27
+ };
28
+ readonly gradients: {
29
+ readonly text: "linear-gradient(135deg, #C4B5FD 0%, #E2B96F 100%)";
30
+ readonly glow: "radial-gradient(circle, rgba(124, 58, 237, 0.07) 0%, rgba(167, 139, 250, 0.03) 40%, transparent 70%)";
31
+ };
32
+ readonly fonts: {
33
+ readonly heading: "'DM Serif Display', serif";
34
+ readonly body: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif";
35
+ readonly mono: "'JetBrains Mono', monospace";
36
+ };
37
+ /** Generate CSS custom properties block from brand tokens */
38
+ readonly toCssVars: () => string;
39
+ };
package/dist/brand.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * kin3o brand tokens — single source of truth for colors, fonts, and styling.
3
+ * Used by landing page generator, preview template, and design token presets.
4
+ */
5
+ export const brand = {
6
+ name: 'kin3o',
7
+ tagline: 'Text to Motion',
8
+ description: 'AI-powered Lottie animation generator',
9
+ colors: {
10
+ accent: '#A78BFA', // Soft violet — primary brand color
11
+ accentBright: '#C4B5FD', // Lighter violet — hover states, highlights
12
+ accentDeep: '#7C3AED', // Deep violet — buttons, CTAs
13
+ warm: '#E2B96F', // Champagne gold — secondary accent
14
+ warmMuted: 'rgba(226, 185, 111, 0.7)',
15
+ bg: '#08080A', // Near-black background
16
+ bgElevated: '#0E0E12', // Slightly raised surfaces
17
+ bgCard: '#131318', // Card backgrounds
18
+ bgCardHover: '#1A1A21', // Card hover state
19
+ border: 'rgba(255, 255, 255, 0.05)',
20
+ borderHover: 'rgba(255, 255, 255, 0.1)',
21
+ borderAccent: 'rgba(167, 139, 250, 0.15)',
22
+ text: '#EDEDF0', // Primary text
23
+ textSecondary: '#9494A0', // Secondary/body text
24
+ textMuted: '#4E4E5A', // Muted/disabled text
25
+ success: '#34D399', // Validation passed
26
+ error: '#EF4444', // Validation failed
27
+ },
28
+ gradients: {
29
+ text: 'linear-gradient(135deg, #C4B5FD 0%, #E2B96F 100%)',
30
+ glow: 'radial-gradient(circle, rgba(124, 58, 237, 0.07) 0%, rgba(167, 139, 250, 0.03) 40%, transparent 70%)',
31
+ },
32
+ fonts: {
33
+ heading: "'DM Serif Display', serif",
34
+ body: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
35
+ mono: "'JetBrains Mono', monospace",
36
+ },
37
+ /** Generate CSS custom properties block from brand tokens */
38
+ toCssVars() {
39
+ return `
40
+ :root {
41
+ --accent: ${this.colors.accent};
42
+ --accent-bright: ${this.colors.accentBright};
43
+ --accent-deep: ${this.colors.accentDeep};
44
+ --accent-glow: rgba(167, 139, 250, 0.12);
45
+ --accent-glow-strong: rgba(167, 139, 250, 0.25);
46
+ --warm: ${this.colors.warm};
47
+ --warm-muted: ${this.colors.warmMuted};
48
+ --bg: ${this.colors.bg};
49
+ --bg-elevated: ${this.colors.bgElevated};
50
+ --bg-card: ${this.colors.bgCard};
51
+ --bg-card-hover: ${this.colors.bgCardHover};
52
+ --border: ${this.colors.border};
53
+ --border-hover: ${this.colors.borderHover};
54
+ --border-accent: ${this.colors.borderAccent};
55
+ --text: ${this.colors.text};
56
+ --text-secondary: ${this.colors.textSecondary};
57
+ --text-muted: ${this.colors.textMuted};
58
+ --code-bg: #111116;
59
+ --success: ${this.colors.success};
60
+ --gradient-text: ${this.gradients.text};
61
+ }`.trim();
62
+ },
63
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { Command } from 'commander';
6
+ import { PROVIDERS, detectAvailableProviders, getDefaultProvider } from './providers/registry.js';
7
+ import { buildSystemPrompt, buildInteractiveSystemPrompt, loadDesignTokens } from './prompts/index.js';
8
+ import { validateLottie, autoFix } from './validator.js';
9
+ import { validateStateMachine } from './state-machine-validator.js';
10
+ import { openPreview, openDotLottiePreview } from './preview.js';
11
+ import { writeDotLottie, readDotLottie } from './packager.js';
12
+ import { extractJson, extractInteractiveJson, slugify, ensureOutputDir } from './utils.js';
13
+ const program = new Command();
14
+ program
15
+ .name('kin3o')
16
+ .description('AI-powered Lottie animation generator')
17
+ .version('0.1.0');
18
+ program
19
+ .command('generate <prompt>')
20
+ .description('Generate a Lottie animation from a natural language prompt')
21
+ .option('-p, --provider <provider>', 'AI provider to use')
22
+ .option('-m, --model <model>', 'Model to use')
23
+ .option('-o, --output <path>', 'Output file path')
24
+ .option('--no-preview', 'Skip opening preview in browser')
25
+ .option('-t, --tokens <path>', 'Path to design tokens JSON (or "sotto" for built-in)')
26
+ .option('-i, --interactive', 'Generate interactive state machine (.lottie output)', false)
27
+ .action(async (prompt, options) => {
28
+ const mode = options.interactive ? 'interactive' : 'static';
29
+ console.log(`\nkin3o — Generating ${mode} animation...`);
30
+ // 1. Detect provider
31
+ let providerKey = options.provider;
32
+ if (!providerKey) {
33
+ providerKey = await getDefaultProvider() ?? undefined;
34
+ if (!providerKey) {
35
+ console.error(' ✗ No AI providers available. Install Claude Code, Codex, or set ANTHROPIC_API_KEY.');
36
+ process.exit(1);
37
+ }
38
+ }
39
+ const provider = PROVIDERS[providerKey];
40
+ if (!provider) {
41
+ console.error(` ✗ Unknown provider "${providerKey}". Run "kin3o providers" to see available options.`);
42
+ process.exit(1);
43
+ }
44
+ const model = options.model ?? provider.defaultModel;
45
+ console.log(` Provider: ${provider.displayName} (${model})`);
46
+ console.log(` Prompt: "${prompt}"\n`);
47
+ // 2. Load design tokens
48
+ const tokens = options.tokens ? loadDesignTokens(options.tokens) : undefined;
49
+ // 3. Build system prompt
50
+ const systemPrompt = options.interactive
51
+ ? buildInteractiveSystemPrompt(tokens)
52
+ : buildSystemPrompt(tokens);
53
+ // 4. Generate
54
+ try {
55
+ const result = await provider.generate(model, systemPrompt, prompt);
56
+ console.log(` ✓ Generated in ${(result.durationMs / 1000).toFixed(1)}s`);
57
+ if (options.interactive) {
58
+ await handleInteractiveOutput(result.content, prompt, options);
59
+ }
60
+ else {
61
+ await handleStaticOutput(result.content, prompt, options);
62
+ }
63
+ console.log('');
64
+ }
65
+ catch (err) {
66
+ console.error(` ✗ Generation failed: ${err instanceof Error ? err.message : String(err)}`);
67
+ process.exit(1);
68
+ }
69
+ });
70
+ async function handleStaticOutput(content, prompt, options) {
71
+ const jsonStr = extractJson(content);
72
+ const lottieJson = JSON.parse(jsonStr);
73
+ const validation = validateLottie(lottieJson);
74
+ if (!validation.valid) {
75
+ console.error(' ✗ Invalid Lottie JSON:');
76
+ validation.errors.forEach(e => console.error(` - ${e}`));
77
+ process.exit(1);
78
+ }
79
+ let finalJson = lottieJson;
80
+ if (validation.warnings.length > 0) {
81
+ finalJson = autoFix(lottieJson);
82
+ const fixes = validation.warnings.length;
83
+ console.log(` ✓ Valid Lottie JSON (${fixes} auto-fix${fixes > 1 ? 'es' : ''}: ${validation.warnings.join(', ')})`);
84
+ }
85
+ else {
86
+ console.log(' ✓ Valid Lottie JSON');
87
+ }
88
+ const outputDir = ensureOutputDir();
89
+ const slug = slugify(prompt);
90
+ const timestamp = Math.floor(Date.now() / 1000);
91
+ const filename = options.output ?? `${slug}-${timestamp}.json`;
92
+ const outputPath = join(outputDir, filename);
93
+ writeFileSync(outputPath, JSON.stringify(finalJson, null, 2), 'utf-8');
94
+ console.log(` ✓ Written to ${outputPath}`);
95
+ if (options.preview) {
96
+ const previewPath = await openPreview(finalJson);
97
+ console.log(` ✓ Preview opened: ${previewPath}`);
98
+ }
99
+ }
100
+ async function handleInteractiveOutput(content, prompt, options) {
101
+ const envelope = extractInteractiveJson(content);
102
+ const animationIds = Object.keys(envelope.animations);
103
+ console.log(` ✓ Extracted ${animationIds.length} animations: ${animationIds.join(', ')}`);
104
+ // Validate each animation
105
+ let totalFixes = 0;
106
+ const fixedAnimations = {};
107
+ for (const [id, anim] of Object.entries(envelope.animations)) {
108
+ const validation = validateLottie(anim);
109
+ if (!validation.valid) {
110
+ console.error(` ✗ Animation "${id}" invalid:`);
111
+ validation.errors.forEach(e => console.error(` - ${e}`));
112
+ process.exit(1);
113
+ }
114
+ if (validation.warnings.length > 0) {
115
+ fixedAnimations[id] = autoFix(anim);
116
+ totalFixes += validation.warnings.length;
117
+ }
118
+ else {
119
+ fixedAnimations[id] = anim;
120
+ }
121
+ }
122
+ if (totalFixes > 0) {
123
+ console.log(` ✓ All animations valid (${totalFixes} auto-fix${totalFixes > 1 ? 'es' : ''})`);
124
+ }
125
+ else {
126
+ console.log(' ✓ All animations valid');
127
+ }
128
+ // Validate state machine
129
+ const smValidation = validateStateMachine(envelope.stateMachine, animationIds);
130
+ if (!smValidation.valid) {
131
+ console.error(' ✗ Invalid state machine:');
132
+ smValidation.errors.forEach(e => console.error(` - ${e}`));
133
+ process.exit(1);
134
+ }
135
+ if (smValidation.warnings.length > 0) {
136
+ smValidation.warnings.forEach(w => console.log(` ⚠ ${w}`));
137
+ }
138
+ console.log(' ✓ State machine valid');
139
+ // Package .lottie
140
+ const outputDir = ensureOutputDir();
141
+ const slug = slugify(prompt);
142
+ const timestamp = Math.floor(Date.now() / 1000);
143
+ const filename = options.output ?? `${slug}-${timestamp}.lottie`;
144
+ const outputPath = join(outputDir, filename);
145
+ await writeDotLottie(outputPath, {
146
+ animations: Object.entries(fixedAnimations).map(([id, data]) => ({ id, data })),
147
+ stateMachine: { id: 'state-machine', data: envelope.stateMachine },
148
+ });
149
+ console.log(` ✓ Written to ${outputPath}`);
150
+ // Preview
151
+ if (options.preview) {
152
+ const buffer = readFileSync(outputPath);
153
+ const previewPath = await openDotLottiePreview(buffer);
154
+ console.log(` ✓ Preview opened: ${previewPath}`);
155
+ }
156
+ }
157
+ program
158
+ .command('preview <file>')
159
+ .description('Preview a Lottie JSON or .lottie file in the browser')
160
+ .action(async (file) => {
161
+ try {
162
+ if (file.endsWith('.lottie')) {
163
+ const buffer = readFileSync(file);
164
+ const previewPath = await openDotLottiePreview(buffer);
165
+ console.log(`Preview opened: ${previewPath}`);
166
+ }
167
+ else {
168
+ const raw = readFileSync(file, 'utf-8');
169
+ const json = JSON.parse(raw);
170
+ const previewPath = await openPreview(json);
171
+ console.log(`Preview opened: ${previewPath}`);
172
+ }
173
+ }
174
+ catch (err) {
175
+ console.error(`Failed to preview: ${err instanceof Error ? err.message : String(err)}`);
176
+ process.exit(1);
177
+ }
178
+ });
179
+ program
180
+ .command('providers')
181
+ .description('List available AI providers')
182
+ .action(async () => {
183
+ const available = await detectAvailableProviders();
184
+ console.log('\nAvailable AI Providers:\n');
185
+ for (const [key, config] of Object.entries(PROVIDERS)) {
186
+ const status = available.includes(key) ? '✓' : '✗';
187
+ console.log(` ${status} ${config.displayName} (${key})`);
188
+ console.log(` Models: ${config.models.join(', ')}`);
189
+ }
190
+ console.log('');
191
+ });
192
+ program
193
+ .command('validate <file>')
194
+ .description('Validate a Lottie JSON or .lottie file')
195
+ .action(async (file) => {
196
+ try {
197
+ if (file.endsWith('.lottie')) {
198
+ const { animations, stateMachine } = await readDotLottie(file);
199
+ const animationIds = Object.keys(animations);
200
+ let allValid = true;
201
+ for (const [id, anim] of Object.entries(animations)) {
202
+ const result = validateLottie(anim);
203
+ if (result.valid) {
204
+ console.log(`✓ Animation "${id}": valid`);
205
+ }
206
+ else {
207
+ console.log(`✗ Animation "${id}": invalid`);
208
+ result.errors.forEach(e => console.log(` Error: ${e}`));
209
+ allValid = false;
210
+ }
211
+ result.warnings.forEach(w => console.log(` Warning: ${w}`));
212
+ }
213
+ if (stateMachine) {
214
+ const smResult = validateStateMachine(stateMachine, animationIds);
215
+ if (smResult.valid) {
216
+ console.log('✓ State machine: valid');
217
+ }
218
+ else {
219
+ console.log('✗ State machine: invalid');
220
+ smResult.errors.forEach(e => console.log(` Error: ${e}`));
221
+ allValid = false;
222
+ }
223
+ smResult.warnings.forEach(w => console.log(` Warning: ${w}`));
224
+ }
225
+ process.exit(allValid ? 0 : 1);
226
+ }
227
+ else {
228
+ const raw = readFileSync(file, 'utf-8');
229
+ const json = JSON.parse(raw);
230
+ const result = validateLottie(json);
231
+ if (result.valid) {
232
+ console.log(`✓ Valid Lottie JSON: ${file}`);
233
+ }
234
+ else {
235
+ console.log(`✗ Invalid Lottie JSON: ${file}`);
236
+ result.errors.forEach(e => console.log(` Error: ${e}`));
237
+ }
238
+ if (result.warnings.length > 0) {
239
+ result.warnings.forEach(w => console.log(` Warning: ${w}`));
240
+ }
241
+ process.exit(result.valid ? 0 : 1);
242
+ }
243
+ }
244
+ catch (err) {
245
+ console.error(`Failed to validate: ${err instanceof Error ? err.message : String(err)}`);
246
+ process.exit(1);
247
+ }
248
+ });
249
+ program.parse();
@@ -0,0 +1,16 @@
1
+ export interface PackageOptions {
2
+ animations: Array<{
3
+ id: string;
4
+ data: object;
5
+ }>;
6
+ stateMachine?: {
7
+ id: string;
8
+ data: object;
9
+ };
10
+ }
11
+ export declare function packageDotLottie(options: PackageOptions): Promise<Buffer>;
12
+ export declare function writeDotLottie(outputPath: string, options: PackageOptions): Promise<void>;
13
+ export declare function readDotLottie(filePath: string): Promise<{
14
+ animations: Record<string, object>;
15
+ stateMachine?: object;
16
+ }>;
@@ -0,0 +1,40 @@
1
+ import { writeFileSync, readFileSync } from 'node:fs';
2
+ import { DotLottie, getAnimations, getStateMachines } from '@dotlottie/dotlottie-js';
3
+ export async function packageDotLottie(options) {
4
+ const dotlottie = new DotLottie();
5
+ for (const anim of options.animations) {
6
+ dotlottie.addAnimation({
7
+ id: anim.id,
8
+ data: anim.data,
9
+ });
10
+ }
11
+ if (options.stateMachine) {
12
+ dotlottie.addStateMachine({
13
+ id: options.stateMachine.id,
14
+ data: options.stateMachine.data,
15
+ });
16
+ }
17
+ const arrayBuffer = await dotlottie.toArrayBuffer();
18
+ return Buffer.from(arrayBuffer);
19
+ }
20
+ export async function writeDotLottie(outputPath, options) {
21
+ const buffer = await packageDotLottie(options);
22
+ writeFileSync(outputPath, buffer);
23
+ }
24
+ export async function readDotLottie(filePath) {
25
+ const fileBuffer = readFileSync(filePath);
26
+ const uint8 = new Uint8Array(fileBuffer.buffer, fileBuffer.byteOffset, fileBuffer.byteLength);
27
+ const animations = {};
28
+ const animRecord = await getAnimations(uint8);
29
+ for (const [id, data] of Object.entries(animRecord)) {
30
+ animations[id] = data;
31
+ }
32
+ let stateMachine;
33
+ const smRecord = await getStateMachines(uint8);
34
+ const smEntries = Object.entries(smRecord);
35
+ if (smEntries.length > 0) {
36
+ const [, smStr] = smEntries[0];
37
+ stateMachine = JSON.parse(smStr);
38
+ }
39
+ return { animations, stateMachine };
40
+ }
@@ -0,0 +1,2 @@
1
+ export declare function openPreview(lottieJson: object, outputPath?: string): Promise<string>;
2
+ export declare function openDotLottiePreview(dotlottieBuffer: Buffer): Promise<string>;
@@ -0,0 +1,30 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import open from 'open';
5
+ import { ensureOutputDir } from './utils.js';
6
+ const templateDir = join(fileURLToPath(new URL('.', import.meta.url)), '..', 'preview');
7
+ export async function openPreview(lottieJson, outputPath) {
8
+ const template = readFileSync(join(templateDir, 'template.html'), 'utf-8');
9
+ const html = template.replace('__ANIMATION_DATA__', JSON.stringify(lottieJson));
10
+ const outputDir = ensureOutputDir();
11
+ const filename = outputPath ?? `preview-${Date.now()}.html`;
12
+ const fullPath = join(outputDir, filename);
13
+ writeFileSync(fullPath, html, 'utf-8');
14
+ await open(fullPath);
15
+ return fullPath;
16
+ }
17
+ export async function openDotLottiePreview(dotlottieBuffer) {
18
+ const templatePath = join(templateDir, 'template-interactive.html');
19
+ if (!existsSync(templatePath)) {
20
+ throw new Error(`Interactive preview template not found at ${templatePath}`);
21
+ }
22
+ const template = readFileSync(templatePath, 'utf-8');
23
+ const base64 = dotlottieBuffer.toString('base64');
24
+ const html = template.replace('__DOTLOTTIE_DATA_BASE64__', base64);
25
+ const outputDir = ensureOutputDir();
26
+ const fullPath = join(outputDir, `preview-interactive-${Date.now()}.html`);
27
+ writeFileSync(fullPath, html, 'utf-8');
28
+ await open(fullPath);
29
+ return fullPath;
30
+ }