@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.
- package/README.md +204 -0
- package/dist/brand.d.ts +39 -0
- package/dist/brand.js +63 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +249 -0
- package/dist/packager.d.ts +16 -0
- package/dist/packager.js +40 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +30 -0
- package/dist/prompts/examples-interactive.d.ts +339 -0
- package/dist/prompts/examples-interactive.js +139 -0
- package/dist/prompts/examples-mascot.d.ts +765 -0
- package/dist/prompts/examples-mascot.js +319 -0
- package/dist/prompts/examples.d.ts +238 -0
- package/dist/prompts/examples.js +168 -0
- package/dist/prompts/index.d.ts +17 -0
- package/dist/prompts/index.js +21 -0
- package/dist/prompts/system-interactive.d.ts +2 -0
- package/dist/prompts/system-interactive.js +93 -0
- package/dist/prompts/system.d.ts +3 -0
- package/dist/prompts/system.js +94 -0
- package/dist/prompts/tokens.d.ts +6 -0
- package/dist/prompts/tokens.js +28 -0
- package/dist/providers/anthropic.d.ts +2 -0
- package/dist/providers/anthropic.js +25 -0
- package/dist/providers/claude.d.ts +2 -0
- package/dist/providers/claude.js +47 -0
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +60 -0
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +62 -0
- package/dist/state-machine-validator.d.ts +6 -0
- package/dist/state-machine-validator.js +182 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.js +89 -0
- package/dist/validator.d.ts +8 -0
- package/dist/validator.js +195 -0
- package/examples/interactive-button.lottie +0 -0
- package/examples/mascot.json +760 -0
- package/examples/mascot.lottie +0 -0
- package/examples/pulse.json +75 -0
- package/examples/waveform.json +179 -0
- package/package.json +54 -0
- package/preview/template-interactive.html +223 -0
- 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
|
+
[](https://www.npmjs.com/package/kin3o)
|
|
8
|
+
[](https://github.com/affromero/kin3o/actions/workflows/ci.yml)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
[](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
|
package/dist/brand.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/index.d.ts
ADDED
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
|
+
}>;
|
package/dist/packager.js
ADDED
|
@@ -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
|
+
}
|
package/dist/preview.js
ADDED
|
@@ -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
|
+
}
|