@birdapi/velinstyle 1.2.2 → 1.3.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.de.md +18 -4
- package/README.md +42 -7
- package/cli/cli-manifest.json +441 -174
- package/cli/docgen/extract-cli.js +2 -1
- package/cli/docs-generate.js +9 -0
- package/cli/experience.js +173 -0
- package/cli/index.js +1294 -417
- package/cli/motion.js +738 -0
- package/cli/production/extract.js +46 -6
- package/cli/production/report.js +104 -14
- package/cli/production/run.js +44 -1
- package/cli/security.js +190 -0
- package/cli/skills.js +53 -10
- package/cli/transparency.js +140 -41
- package/cli/workflow.js +32 -17
- package/components/velin-theme-toggle.js +15 -1
- package/core/a11y/component-contracts.json +391 -274
- package/core/attributes/registry.js +1 -1
- package/core/motion/analyze.js +38 -0
- package/core/motion/blueprint.js +51 -0
- package/core/motion/composer.js +114 -0
- package/core/motion/data/registry.json +352 -0
- package/core/motion/effects.js +17 -1
- package/core/motion/index.js +112 -9
- package/core/motion/optimize.js +44 -0
- package/core/motion/policy.js +77 -0
- package/core/motion/registry.js +131 -0
- package/core/motion/review.js +59 -0
- package/core/motion/scan.js +150 -0
- package/core/motion/timeline.js +68 -0
- package/core/motion/transitions.js +49 -0
- package/core/motion/triggers.js +168 -0
- package/core/security/checks/fs.js +92 -0
- package/core/security/detect.js +35 -0
- package/core/security/engine.js +116 -0
- package/core/security/index.js +18 -0
- package/core/security/registry.js +85 -0
- package/core/security/report/report.js +113 -0
- package/core/security/rules/ci/index.js +146 -0
- package/core/security/rules/consumer/index.js +174 -0
- package/core/security/rules/deps/index.js +182 -0
- package/core/security/rules/index.js +24 -0
- package/core/security/rules/publish/index.js +230 -0
- package/core/security/rules/repo/index.js +143 -0
- package/core/security/rules/secrets/index.js +133 -0
- package/core/security/score/dependency-health.js +53 -0
- package/core/security/score/release-health.js +66 -0
- package/core/security/score/score.js +102 -0
- package/core/security/timeline/timeline.js +76 -0
- package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
- package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
- package/dist/chunks/chunk-MYKUI364.js +116 -0
- package/dist/chunks/chunk-NEVBPD5T.js +116 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
- package/dist/llms.txt +2 -2
- package/dist/search-index.json +166 -2
- package/dist/velin-agent.json +156 -32
- package/dist/velinstyle-components.iife.js +862 -17
- package/dist/velinstyle-components.js +862 -17
- package/dist/velinstyle-components.min.js +126 -126
- package/dist/velinstyle.css +42 -7
- package/dist/velinstyle.min.css +1 -1
- package/package.json +158 -143
- package/packages/velinstyle-cli-core/package.json +11 -0
- package/packages/velinstyle-cli-core/src/config.js +84 -0
- package/packages/velinstyle-cli-core/src/contract.js +56 -0
- package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
- package/packages/velinstyle-cli-core/src/index.js +18 -0
- package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
- package/packages/velinstyle-cli-core/src/runner.js +129 -0
- package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
- package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
- package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
- package/packages/velinstyle-cli-registry/package.json +13 -0
- package/packages/velinstyle-cli-registry/src/index.js +88 -0
- package/packages/velinstyle-cli-renderer/package.json +8 -0
- package/packages/velinstyle-cli-renderer/src/index.js +286 -0
- package/packages/velinstyle-cli-theme/package.json +8 -0
- package/packages/velinstyle-cli-theme/src/index.js +84 -0
- package/packages/velinstyle-cli-ui/package.json +8 -0
- package/packages/velinstyle-cli-ui/src/index.js +34 -0
- package/packages/velinstyle-motion/README.md +81 -0
- package/packages/velinstyle-motion/package.json +37 -0
- package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
- package/packages/velinstyle-motion/src/ai/provider.js +59 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
- package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
- package/packages/velinstyle-motion/src/config.js +257 -0
- package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
- package/packages/velinstyle-motion/src/index.js +304 -0
- package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
- package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
- package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
- package/packages/velinstyle-motion/src/local/effects.js +139 -0
- package/packages/velinstyle-motion/src/local/engine.js +96 -0
- package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
- package/packages/velinstyle-motion/src/registry/presets.js +25 -0
- package/packages/velinstyle-motion/src/registry/presets.json +85 -0
- package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
- package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
- package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
- package/packages/velinstyle-motion/src/validate-image.js +91 -0
- package/packages/velinstyle-skills/catalog.json +1 -1
- package/packages/velinstyle-skills/registry.json +1 -142
- package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
- package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
- package/src/tokens/motion.css +7 -0
- package/src/utilities/scroll-animation.css +66 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @birdapi/velinstyle-motion
|
|
2
|
+
|
|
3
|
+
**Velin Motion Clip Pipeline** — image → short motion clip.
|
|
4
|
+
|
|
5
|
+
- **Local** (default): CSS / Web Animations HTML clips — free, offline, reproducible
|
|
6
|
+
- **AI** (optional): pluggable providers — first partner: **PixVerse** (credits + API key)
|
|
7
|
+
|
|
8
|
+
This package is separate from the DOM Motion Design System (`@birdapi/velinstyle/motion` / `core/motion`).
|
|
9
|
+
|
|
10
|
+
Ship with **`@birdapi/velinstyle@1.3.0`** (export `@birdapi/velinstyle/motion-clip`).
|
|
11
|
+
|
|
12
|
+
## CLI (via VelinStyle)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
velinstyle motion providers
|
|
16
|
+
velinstyle motion setup pixverse
|
|
17
|
+
velinstyle motion create atelier.webp
|
|
18
|
+
velinstyle motion create hero.png --preset cinematic --yes
|
|
19
|
+
velinstyle motion create logo.png --ai --yes # needs PIXVERSE_API_KEY; dry: VELIN_MOTION_DRY_RUN=1
|
|
20
|
+
velinstyle motion atelier ./assets --count 10 --preset atelier --yes
|
|
21
|
+
velinstyle motion transition a.webp b.webp --yes
|
|
22
|
+
velinstyle motion sequence 01.webp 02.webp 03.webp --provider pixverse --yes
|
|
23
|
+
velinstyle motion usage
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## PixVerse partner
|
|
27
|
+
|
|
28
|
+
PixVerse is an **external** AI video platform. VelinStyle does not sell credits and does not host PixVerse.
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
|--|--|
|
|
32
|
+
| Setup | `velinstyle motion setup pixverse` — affiliate-first (open / copy partner link · enter API key · abort) |
|
|
33
|
+
| Signup | [VelinStyle partner link](https://motivaiprivatelimited.sjv.io/c/7581758/3811144/49478) (Impact) |
|
|
34
|
+
| API docs | https://docs.platform.pixverse.ai/ |
|
|
35
|
+
| Site guide | https://velinstyle.info/docs/guides/pixverse.html |
|
|
36
|
+
|
|
37
|
+
There is **no** bare `platform.pixverse.ai` signup bypass in the CLI. Tracker blockers may block the affiliate open — use **copy partner link** then.
|
|
38
|
+
|
|
39
|
+
## Config
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
.velin/motion/config.json
|
|
43
|
+
.velin/motion/.env # PIXVERSE_API_KEY=… (gitignored via .velin/)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"provider": "pixverse",
|
|
49
|
+
"api_key": "${PIXVERSE_API_KEY}",
|
|
50
|
+
"model": "v6",
|
|
51
|
+
"quality": "720p",
|
|
52
|
+
"default_duration": 5,
|
|
53
|
+
"output_dir": "./motion-output",
|
|
54
|
+
"pixverse": { "setup_completed": false }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Never store plaintext API keys in `config.json`. Prefer `PIXVERSE_API_KEY` env or `.velin/motion/.env`. Keys are never logged, printed in `--json`, or written to job manifests.
|
|
59
|
+
|
|
60
|
+
`--yes` + AI without a key → hard error (CI-safe). Affiliate browser opens only during explicit setup / first-time gate when `setup_completed` is false.
|
|
61
|
+
|
|
62
|
+
**Image inputs:** non-empty `.webp` / `.png` / `.jpg` / `.jpeg` with valid raster magic bytes.
|
|
63
|
+
|
|
64
|
+
## Programmatic
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import { createMotionClip, listClipPresets } from '@birdapi/velinstyle-motion';
|
|
68
|
+
|
|
69
|
+
await createMotionClip({
|
|
70
|
+
image: 'atelier.webp',
|
|
71
|
+
preset: 'soft-zoom',
|
|
72
|
+
yes: true,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Providers
|
|
77
|
+
|
|
78
|
+
Add another backend under `src/ai/providers/` and `registerAiProvider(...)`.
|
|
79
|
+
PixVerse implements `imageToVideo`, `transition`, `multiTransition` (capabilities-driven).
|
|
80
|
+
|
|
81
|
+
Framework guide: [`docs/guides/velin-motion-clips.md`](../../docs/guides/velin-motion-clips.md)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@birdapi/velinstyle-motion",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Velin Motion Clip Pipeline — local image→clip engine + pluggable AI providers (PixVerse first)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./presets": "./src/registry/presets.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/SkyliteDesign/velinstyle.git",
|
|
18
|
+
"directory": "packages/velinstyle-motion"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://velinstyle.info/docs/guides/velin-motion-clips.html",
|
|
21
|
+
"author": "SkyliteDesign",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"velinstyle",
|
|
29
|
+
"motion",
|
|
30
|
+
"animation",
|
|
31
|
+
"image-to-video",
|
|
32
|
+
"pixverse"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mini-prompt → structured motion profile (not raw PixVerse prompts).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} MotionProfile
|
|
7
|
+
* @property {string} direction
|
|
8
|
+
* @property {string[]} phases
|
|
9
|
+
* @property {string} intensity
|
|
10
|
+
* @property {string} [aiPrompt]
|
|
11
|
+
* @property {string} [label]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SUGGESTIONS = {
|
|
15
|
+
'rechts rein': [
|
|
16
|
+
{ label: 'Rechts → Mitte', direction: 'right-in', phases: ['enter', 'hold'], intensity: 'soft' },
|
|
17
|
+
{ label: 'Rechts → Mitte → Links', direction: 'right-in-left-out', phases: ['enter', 'hold', 'exit'], intensity: 'soft' },
|
|
18
|
+
{ label: 'Rechts → Mitte → Explosion', direction: 'right-in', phases: ['enter', 'hold', 'explosion'], intensity: 'strong' },
|
|
19
|
+
{ label: 'Rechts → Mitte → Partikel', direction: 'right-in', phases: ['enter', 'particles'], intensity: 'medium' },
|
|
20
|
+
],
|
|
21
|
+
'links rein': [
|
|
22
|
+
{ label: 'Links → Mitte', direction: 'left-in', phases: ['enter', 'hold'], intensity: 'soft' },
|
|
23
|
+
{ label: 'Links → Mitte → Rechts', direction: 'left-in-right-out', phases: ['enter', 'hold', 'exit'], intensity: 'soft' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} text
|
|
29
|
+
* @returns {MotionProfile[]}
|
|
30
|
+
*/
|
|
31
|
+
export function suggestFromMiniPrompt(text) {
|
|
32
|
+
const t = (text || '').trim().toLowerCase();
|
|
33
|
+
if (!t) return [];
|
|
34
|
+
|
|
35
|
+
for (const [key, list] of Object.entries(SUGGESTIONS)) {
|
|
36
|
+
if (t.includes(key) || key.includes(t)) return list.map(enrichAiPrompt);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (/dreh|rotat/.test(t) && /explod/.test(t)) {
|
|
40
|
+
return [
|
|
41
|
+
enrichAiPrompt({
|
|
42
|
+
label: 'Drehen + Explosion',
|
|
43
|
+
direction: 'center',
|
|
44
|
+
phases: ['rotate', 'explosion'],
|
|
45
|
+
intensity: 'strong',
|
|
46
|
+
}),
|
|
47
|
+
enrichAiPrompt({
|
|
48
|
+
label: 'Drehen + Partikel',
|
|
49
|
+
direction: 'center',
|
|
50
|
+
phases: ['rotate', 'particles'],
|
|
51
|
+
intensity: 'medium',
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (/zoom|näher|nah/.test(t)) {
|
|
57
|
+
return [
|
|
58
|
+
enrichAiPrompt({ label: 'Soft Zoom', direction: 'in', phases: ['zoom'], intensity: 'soft' }),
|
|
59
|
+
enrichAiPrompt({ label: 'Cinematic Zoom + Pan', direction: 'in', phases: ['zoom', 'pan'], intensity: 'medium' }),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (/partikel|particle/.test(t)) {
|
|
64
|
+
return [
|
|
65
|
+
enrichAiPrompt({ label: 'Particle Reveal', direction: 'center', phases: ['particles', 'fade'], intensity: 'medium' }),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generic single profile from free text
|
|
70
|
+
return [
|
|
71
|
+
enrichAiPrompt({
|
|
72
|
+
label: `Profil: ${text.trim().slice(0, 48)}`,
|
|
73
|
+
direction: /rechts|right/.test(t) ? 'right-in' : /links|left/.test(t) ? 'left-in' : 'center',
|
|
74
|
+
phases: inferPhases(t),
|
|
75
|
+
intensity: /explod|stark|strong/.test(t) ? 'strong' : 'soft',
|
|
76
|
+
raw: text.trim(),
|
|
77
|
+
}),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function inferPhases(t) {
|
|
82
|
+
const phases = [];
|
|
83
|
+
if (/rein|enter|einblend/.test(t)) phases.push('enter');
|
|
84
|
+
if (/zoom/.test(t)) phases.push('zoom');
|
|
85
|
+
if (/pan|schwenk/.test(t)) phases.push('pan');
|
|
86
|
+
if (/dreh|rotat/.test(t)) phases.push('rotate');
|
|
87
|
+
if (/partikel|particle/.test(t)) phases.push('particles');
|
|
88
|
+
if (/explod/.test(t)) phases.push('explosion');
|
|
89
|
+
if (/raus|ausblend|exit|links$/.test(t)) phases.push('exit');
|
|
90
|
+
if (!phases.length) phases.push('zoom', 'hold');
|
|
91
|
+
return phases;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function enrichAiPrompt(profile) {
|
|
95
|
+
const bits = [];
|
|
96
|
+
if (profile.phases?.includes('enter')) bits.push('subject enters frame smoothly');
|
|
97
|
+
if (profile.direction?.includes('right')) bits.push('motion from the right toward center');
|
|
98
|
+
if (profile.direction?.includes('left')) bits.push('motion from the left toward center');
|
|
99
|
+
if (profile.phases?.includes('zoom')) bits.push('gentle zoom');
|
|
100
|
+
if (profile.phases?.includes('pan')) bits.push('subtle camera pan');
|
|
101
|
+
if (profile.phases?.includes('rotate')) bits.push('slight rotation');
|
|
102
|
+
if (profile.phases?.includes('particles') || profile.phases?.includes('explosion')) {
|
|
103
|
+
bits.push('energetic particle burst, keep subject readable');
|
|
104
|
+
}
|
|
105
|
+
if (profile.phases?.includes('exit')) bits.push('exit opposite side with soft fade');
|
|
106
|
+
bits.push('cinematic, keep composition stable, no text overlay');
|
|
107
|
+
return {
|
|
108
|
+
...profile,
|
|
109
|
+
aiPrompt: bits.join(', '),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Translate profile to PixVerse-oriented prompt string.
|
|
115
|
+
* @param {MotionProfile} profile
|
|
116
|
+
*/
|
|
117
|
+
export function profileToAiPrompt(profile) {
|
|
118
|
+
return profile.aiPrompt || enrichAiPrompt(profile).aiPrompt;
|
|
119
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AiProvider interface — pluggable image/video backends.
|
|
3
|
+
* Capabilities are declared per provider; CLI queries them dynamically.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} AiProvider
|
|
8
|
+
* @property {string} id
|
|
9
|
+
* @property {string} title
|
|
10
|
+
* @property {string[]} models
|
|
11
|
+
* @property {Set<string>|string[]} capabilities
|
|
12
|
+
* @property {(opts: object) => Promise<object>} imageToVideo
|
|
13
|
+
* @property {(opts: object) => Promise<object>} [transition]
|
|
14
|
+
* @property {(opts: object) => Promise<object>} [multiTransition]
|
|
15
|
+
* @property {(opts: object) => Promise<object>} [extend]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @type {Map<string, AiProvider>} */
|
|
19
|
+
const PROVIDERS = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {AiProvider} provider
|
|
23
|
+
*/
|
|
24
|
+
export function registerAiProvider(provider) {
|
|
25
|
+
if (!provider?.id) throw new Error('registerAiProvider: id required');
|
|
26
|
+
PROVIDERS.set(provider.id, provider);
|
|
27
|
+
return provider.id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getAiProvider(id) {
|
|
31
|
+
return PROVIDERS.get(id) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function listAiProviders() {
|
|
35
|
+
return [...PROVIDERS.values()].map((p) => ({
|
|
36
|
+
id: p.id,
|
|
37
|
+
title: p.title,
|
|
38
|
+
models: p.models,
|
|
39
|
+
capabilities: [...(p.capabilities || [])],
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function providerHas(provider, capability) {
|
|
44
|
+
const caps = provider.capabilities;
|
|
45
|
+
if (!caps) return false;
|
|
46
|
+
if (caps instanceof Set) return caps.has(capability);
|
|
47
|
+
return caps.includes(capability);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ensure built-in providers are loaded once.
|
|
52
|
+
*/
|
|
53
|
+
let loaded = false;
|
|
54
|
+
export async function loadBuiltinAiProviders() {
|
|
55
|
+
if (loaded) return;
|
|
56
|
+
loaded = true;
|
|
57
|
+
const { createPixVerseProvider } = await import('./providers/pixverse.js');
|
|
58
|
+
registerAiProvider(createPixVerseProvider());
|
|
59
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PixVerse provider setup — affiliate-first onboarding (Impact partner link).
|
|
3
|
+
* Never offer bare platform.pixverse.ai as a signup bypass.
|
|
4
|
+
* Browser opens only on explicit request. Never log API keys.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { spawn, spawnSync } from 'child_process';
|
|
8
|
+
import { createInterface } from 'readline';
|
|
9
|
+
import {
|
|
10
|
+
ensureMotionDirs,
|
|
11
|
+
hasPixVerseApiKey,
|
|
12
|
+
isPixVerseSetupCompleted,
|
|
13
|
+
loadMotionConfig,
|
|
14
|
+
loadMotionDotEnv,
|
|
15
|
+
markPixVerseSetupCompleted,
|
|
16
|
+
savePixVerseApiKey,
|
|
17
|
+
safeErrorMessage,
|
|
18
|
+
} from '../../config.js';
|
|
19
|
+
|
|
20
|
+
/** Impact affiliate — only URL VelinStyle opens / copies for signup. */
|
|
21
|
+
export const PIXVERSE_AFFILIATE_URL =
|
|
22
|
+
'https://motivaiprivatelimited.sjv.io/c/7581758/3811144/49478';
|
|
23
|
+
|
|
24
|
+
/** @deprecated Kept for API docs references only — never use for signup attribution. */
|
|
25
|
+
export const PIXVERSE_DIRECT_URL = 'https://platform.pixverse.ai/';
|
|
26
|
+
|
|
27
|
+
export const PIXVERSE_DOCS_URL = 'https://docs.platform.pixverse.ai/';
|
|
28
|
+
|
|
29
|
+
export const PIXVERSE_KEY_MISSING_HINT =
|
|
30
|
+
'PIXVERSE_API_KEY missing — run: velinstyle motion setup pixverse';
|
|
31
|
+
|
|
32
|
+
export function formatPixVerseTrackerHint() {
|
|
33
|
+
return [
|
|
34
|
+
'Hinweis:',
|
|
35
|
+
'Wenn dein Browser den Tracking-Link blockiert, deaktiviere',
|
|
36
|
+
'für diesen Vorgang deinen Tracking-Blocker oder öffne den',
|
|
37
|
+
'kopierten Partnerlink in einem Browser ohne entsprechende Blockierung.',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Main visible CLI / docs block for PixVerse setup.
|
|
43
|
+
* @param {{ affiliateUrl?: string }} [opts]
|
|
44
|
+
*/
|
|
45
|
+
export function formatPixVerseSignupHelp(opts = {}) {
|
|
46
|
+
const affiliate = opts.affiliateUrl || PIXVERSE_AFFILIATE_URL;
|
|
47
|
+
return [
|
|
48
|
+
'PixVerse Setup',
|
|
49
|
+
'',
|
|
50
|
+
'VelinStyle verwendet PixVerse als AI-Provider.',
|
|
51
|
+
'',
|
|
52
|
+
'Um PixVerse über VelinStyle einzurichten, öffnet VelinStyle',
|
|
53
|
+
'deinen offiziellen PixVerse-Partnerlink.',
|
|
54
|
+
'',
|
|
55
|
+
affiliate,
|
|
56
|
+
'',
|
|
57
|
+
'[1] PixVerse über VelinStyle öffnen',
|
|
58
|
+
'[2] Partnerlink kopieren',
|
|
59
|
+
'[3] API-Key eingeben',
|
|
60
|
+
'[4] Abbrechen',
|
|
61
|
+
'',
|
|
62
|
+
formatPixVerseTrackerHint(),
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @deprecated Use formatPixVerseSignupHelp / formatPixVerseOpenFailedHelp */
|
|
67
|
+
export function formatPixVerseOpenAccountHelp(opts = {}) {
|
|
68
|
+
const affiliate = opts.affiliateUrl || PIXVERSE_AFFILIATE_URL;
|
|
69
|
+
return [
|
|
70
|
+
'PixVerse-Partnerlink:',
|
|
71
|
+
affiliate,
|
|
72
|
+
'',
|
|
73
|
+
formatPixVerseTrackerHint(),
|
|
74
|
+
'',
|
|
75
|
+
'Danach gib deinen API-Key hier ein:',
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatPixVerseOpenFailedHelp(opts = {}) {
|
|
80
|
+
const affiliate = opts.affiliateUrl || PIXVERSE_AFFILIATE_URL;
|
|
81
|
+
return [
|
|
82
|
+
'Der PixVerse-Partnerlink konnte in diesem Browser nicht',
|
|
83
|
+
'geöffnet werden.',
|
|
84
|
+
'',
|
|
85
|
+
'Grund:',
|
|
86
|
+
'Ein Tracking- oder Cookie-Blocker verhindert vermutlich',
|
|
87
|
+
'die Attribution.',
|
|
88
|
+
'',
|
|
89
|
+
'Partnerlink:',
|
|
90
|
+
affiliate,
|
|
91
|
+
'',
|
|
92
|
+
'[1] Erneut versuchen',
|
|
93
|
+
'[2] Link kopieren',
|
|
94
|
+
'[3] Abbrechen',
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Open PixVerse signup — always the Impact affiliate URL (never bare platform.pixverse.ai).
|
|
100
|
+
* @returns {{ ok: boolean, url: string, error?: string }}
|
|
101
|
+
*/
|
|
102
|
+
export function openPixVerseSignup(url = PIXVERSE_AFFILIATE_URL, opts = {}) {
|
|
103
|
+
const target =
|
|
104
|
+
typeof url === 'string' && url.includes('motivaiprivatelimited.sjv.io')
|
|
105
|
+
? url
|
|
106
|
+
: PIXVERSE_AFFILIATE_URL;
|
|
107
|
+
const spawnImpl = opts.spawnImpl || spawn;
|
|
108
|
+
const platform = process.platform;
|
|
109
|
+
try {
|
|
110
|
+
let child;
|
|
111
|
+
if (platform === 'win32') {
|
|
112
|
+
child = spawnImpl('cmd', ['/c', 'start', '', target], { detached: true, stdio: 'ignore' });
|
|
113
|
+
} else if (platform === 'darwin') {
|
|
114
|
+
child = spawnImpl('open', [target], { detached: true, stdio: 'ignore' });
|
|
115
|
+
} else {
|
|
116
|
+
child = spawnImpl('xdg-open', [target], { detached: true, stdio: 'ignore' });
|
|
117
|
+
}
|
|
118
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
119
|
+
if (opts.forceFail) {
|
|
120
|
+
return { ok: false, url: target, error: 'forced open failure' };
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, url: target };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return { ok: false, url: target, error: safeErrorMessage(err) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Assert helper for CLI/docs — always prefer affiliate over bare platform URL. */
|
|
129
|
+
export function pixVerseSignupUrl(url) {
|
|
130
|
+
if (typeof url === 'string' && url.includes('motivaiprivatelimited.sjv.io')) return url;
|
|
131
|
+
return PIXVERSE_AFFILIATE_URL;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Copy the official partner link to the clipboard (affiliate only).
|
|
136
|
+
* @returns {{ ok: boolean, url: string, method?: string }}
|
|
137
|
+
*/
|
|
138
|
+
export function copyPixVersePartnerLink(opts = {}) {
|
|
139
|
+
const url = opts.url || PIXVERSE_AFFILIATE_URL;
|
|
140
|
+
const target = pixVerseSignupUrl(url);
|
|
141
|
+
const platform = process.platform;
|
|
142
|
+
try {
|
|
143
|
+
if (opts.copyImpl) {
|
|
144
|
+
opts.copyImpl(target);
|
|
145
|
+
return { ok: true, url: target, method: 'custom' };
|
|
146
|
+
}
|
|
147
|
+
if (platform === 'win32') {
|
|
148
|
+
const r = spawnSync(
|
|
149
|
+
'powershell',
|
|
150
|
+
['-NoProfile', '-Command', `Set-Clipboard -Value @'\n${target}\n'@`],
|
|
151
|
+
{ windowsHide: true, encoding: 'utf8' },
|
|
152
|
+
);
|
|
153
|
+
if (r.error || (r.status !== 0 && r.status != null)) {
|
|
154
|
+
return { ok: false, url: target };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, url: target, method: 'Set-Clipboard' };
|
|
157
|
+
}
|
|
158
|
+
if (platform === 'darwin') {
|
|
159
|
+
const r = spawnSync('pbcopy', [], { input: target, encoding: 'utf8' });
|
|
160
|
+
if (r.error || (r.status !== 0 && r.status != null)) {
|
|
161
|
+
return { ok: false, url: target };
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, url: target, method: 'pbcopy' };
|
|
164
|
+
}
|
|
165
|
+
let r = spawnSync('wl-copy', [], { input: target, encoding: 'utf8' });
|
|
166
|
+
if (!r.error && (r.status === 0 || r.status == null)) {
|
|
167
|
+
return { ok: true, url: target, method: 'wl-copy' };
|
|
168
|
+
}
|
|
169
|
+
r = spawnSync('xclip', ['-selection', 'clipboard'], { input: target, encoding: 'utf8' });
|
|
170
|
+
if (!r.error && (r.status === 0 || r.status == null)) {
|
|
171
|
+
return { ok: true, url: target, method: 'xclip' };
|
|
172
|
+
}
|
|
173
|
+
return { ok: false, url: target };
|
|
174
|
+
} catch {
|
|
175
|
+
return { ok: false, url: target };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ask(rl, q) {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
rl.question(q, resolve);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writePartnerCopied(result) {
|
|
186
|
+
if (result.ok) {
|
|
187
|
+
process.stdout.write(`\nPartnerlink kopiert:\n${result.url}\n`);
|
|
188
|
+
} else {
|
|
189
|
+
process.stdout.write(
|
|
190
|
+
`\nPartnerlink (manuell kopieren):\n${result.url}\n`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
process.stdout.write(`\n${formatPixVerseTrackerHint()}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Open partner link; on technical failure offer retry / copy / abort (no direct bypass).
|
|
198
|
+
* @returns {Promise<'opened'|'copied'|'aborted'>}
|
|
199
|
+
*/
|
|
200
|
+
export async function runPixVersePartnerOpenFlow(opts = {}) {
|
|
201
|
+
const rl = opts.rl;
|
|
202
|
+
if (!rl) throw new Error('runPixVersePartnerOpenFlow requires opts.rl');
|
|
203
|
+
const openImpl = opts.openImpl || openPixVerseSignup;
|
|
204
|
+
const copyImpl = opts.copyImpl;
|
|
205
|
+
|
|
206
|
+
async function attemptOpen() {
|
|
207
|
+
const result = openImpl(PIXVERSE_AFFILIATE_URL, {
|
|
208
|
+
spawnImpl: opts.spawnImpl,
|
|
209
|
+
forceFail: opts.forceFail,
|
|
210
|
+
});
|
|
211
|
+
const normalized =
|
|
212
|
+
result && typeof result === 'object' && 'url' in result
|
|
213
|
+
? result
|
|
214
|
+
: { ok: true, url: typeof result === 'string' ? result : PIXVERSE_AFFILIATE_URL };
|
|
215
|
+
|
|
216
|
+
if (normalized.ok) {
|
|
217
|
+
process.stdout.write(`\nPartnerlink geöffnet:\n${normalized.url}\n`);
|
|
218
|
+
process.stdout.write(`\n${formatPixVerseTrackerHint()}\n`);
|
|
219
|
+
process.stdout.write(`Docs: ${PIXVERSE_DOCS_URL}\n`);
|
|
220
|
+
return 'opened';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
process.stdout.write(`\n${formatPixVerseOpenFailedHelp({ affiliateUrl: normalized.url })}\n`);
|
|
224
|
+
const choice = (await ask(rl, 'Auswahl [1/2/3]: ')).trim();
|
|
225
|
+
if (choice === '1' || choice === '') {
|
|
226
|
+
return attemptOpen();
|
|
227
|
+
}
|
|
228
|
+
if (choice === '2') {
|
|
229
|
+
writePartnerCopied(copyPixVersePartnerLink({ copyImpl }));
|
|
230
|
+
return 'copied';
|
|
231
|
+
}
|
|
232
|
+
return 'aborted';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return attemptOpen();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {object} [opts]
|
|
240
|
+
* @param {string} [opts.cwd]
|
|
241
|
+
* @param {import('readline').Interface} [opts.rl]
|
|
242
|
+
* @param {boolean} [opts.save]
|
|
243
|
+
*/
|
|
244
|
+
export async function promptAndSaveApiKey(opts = {}) {
|
|
245
|
+
const cwd = opts.cwd || process.cwd();
|
|
246
|
+
ensureMotionDirs(cwd);
|
|
247
|
+
const rl = opts.rl || createInterface({ input: process.stdin, output: process.stdout });
|
|
248
|
+
const owned = !opts.rl;
|
|
249
|
+
try {
|
|
250
|
+
process.stdout.write('\nPixVerse API-Key:\n');
|
|
251
|
+
const raw = await ask(rl, '> ');
|
|
252
|
+
const key = String(raw || '').trim();
|
|
253
|
+
if (!key) throw new Error('Kein API-Key eingegeben');
|
|
254
|
+
|
|
255
|
+
let doSave = opts.save;
|
|
256
|
+
if (doSave == null) {
|
|
257
|
+
const ans = await ask(rl, 'API-Key speichern? [J/n] ');
|
|
258
|
+
const a = (ans || 'j').trim().toLowerCase();
|
|
259
|
+
doSave = a === '' || a === 'j' || a === 'y' || a === 'ja' || a === 'yes';
|
|
260
|
+
}
|
|
261
|
+
if (doSave) {
|
|
262
|
+
savePixVerseApiKey(key, cwd);
|
|
263
|
+
} else {
|
|
264
|
+
process.env.PIXVERSE_API_KEY = key;
|
|
265
|
+
}
|
|
266
|
+
return { saved: Boolean(doSave), present: true };
|
|
267
|
+
} finally {
|
|
268
|
+
if (owned) rl.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Lightweight connection check — never includes the key in errors/results.
|
|
274
|
+
* @param {object} [opts]
|
|
275
|
+
*/
|
|
276
|
+
export async function testPixVerseConnection(opts = {}) {
|
|
277
|
+
const cwd = opts.cwd || process.cwd();
|
|
278
|
+
loadMotionDotEnv(cwd);
|
|
279
|
+
const key = process.env.PIXVERSE_API_KEY || loadMotionConfig(cwd).api_key;
|
|
280
|
+
if (!key) {
|
|
281
|
+
return { ok: false, reason: PIXVERSE_KEY_MISSING_HINT };
|
|
282
|
+
}
|
|
283
|
+
if (opts.skipNetwork || process.env.VELIN_MOTION_DRY_RUN === '1') {
|
|
284
|
+
return { ok: true, dryCheck: true, apiKeyPresent: true };
|
|
285
|
+
}
|
|
286
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
287
|
+
if (!fetchImpl) {
|
|
288
|
+
return { ok: true, dryCheck: true, apiKeyPresent: true, note: 'fetch unavailable — key format accepted' };
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const res = await fetchImpl('https://app-api.pixverse.ai/openapi/v2/video/list', {
|
|
292
|
+
method: 'GET',
|
|
293
|
+
headers: {
|
|
294
|
+
'API-KEY': key,
|
|
295
|
+
'Ai-trace-id': randomUUID(),
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
if (res.status === 401 || res.status === 403) {
|
|
299
|
+
return { ok: false, reason: 'PixVerse rejected the API key (HTTP auth error)' };
|
|
300
|
+
}
|
|
301
|
+
return { ok: true, apiKeyPresent: true, httpStatus: res.status };
|
|
302
|
+
} catch (err) {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
reason: `Network error during PixVerse check: ${safeErrorMessage(err)}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Interactive PixVerse setup — affiliate-first menu (open / copy / key / abort).
|
|
312
|
+
* @param {object} [opts]
|
|
313
|
+
*/
|
|
314
|
+
export async function runPixVerseSetup(opts = {}) {
|
|
315
|
+
const cwd = opts.cwd || process.cwd();
|
|
316
|
+
ensureMotionDirs(cwd);
|
|
317
|
+
loadMotionDotEnv(cwd);
|
|
318
|
+
|
|
319
|
+
if (opts.yes) {
|
|
320
|
+
if (!hasPixVerseApiKey(undefined, cwd)) {
|
|
321
|
+
throw new Error(PIXVERSE_KEY_MISSING_HINT);
|
|
322
|
+
}
|
|
323
|
+
markPixVerseSetupCompleted(cwd);
|
|
324
|
+
const test = await testPixVerseConnection({
|
|
325
|
+
cwd,
|
|
326
|
+
skipNetwork: opts.skipNetwork !== false,
|
|
327
|
+
fetchImpl: opts.fetchImpl,
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
provider: 'pixverse',
|
|
331
|
+
setup_completed: true,
|
|
332
|
+
apiKeyPresent: true,
|
|
333
|
+
test,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!process.stdin.isTTY) {
|
|
338
|
+
throw new Error(PIXVERSE_KEY_MISSING_HINT);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
process.stdout.write(`\nVelinStyle Motion\n\n${formatPixVerseSignupHelp()}\n\n`);
|
|
345
|
+
const choice = (await ask(rl, 'Auswahl [1/2/3/4]: ')).trim();
|
|
346
|
+
|
|
347
|
+
let openedBrowser = false;
|
|
348
|
+
if (choice === '4' || choice.toLowerCase() === 'q' || choice.toLowerCase() === 'c') {
|
|
349
|
+
throw new Error('Abgebrochen');
|
|
350
|
+
}
|
|
351
|
+
if (choice === '1' || choice === '') {
|
|
352
|
+
const outcome = await runPixVersePartnerOpenFlow({
|
|
353
|
+
rl,
|
|
354
|
+
openImpl: opts.openImpl,
|
|
355
|
+
spawnImpl: opts.spawnImpl,
|
|
356
|
+
copyImpl: opts.copyImpl,
|
|
357
|
+
forceFail: opts.forceFail,
|
|
358
|
+
});
|
|
359
|
+
if (outcome === 'aborted') throw new Error('Abgebrochen');
|
|
360
|
+
openedBrowser = outcome === 'opened';
|
|
361
|
+
process.stdout.write(
|
|
362
|
+
'\nRichte den API-Zugang im PixVerse-Dashboard ein, dann füge den Key hier ein.\n',
|
|
363
|
+
);
|
|
364
|
+
} else if (choice === '2') {
|
|
365
|
+
writePartnerCopied(copyPixVersePartnerLink({ copyImpl: opts.copyImpl }));
|
|
366
|
+
process.stdout.write(
|
|
367
|
+
'\nÖffne den kopierten Partnerlink, richte den API-Zugang ein, dann Key hier einfügen.\n',
|
|
368
|
+
);
|
|
369
|
+
} else if (choice === '3') {
|
|
370
|
+
process.stdout.write(
|
|
371
|
+
'\nAPI-Key aus dem PixVerse-Dashboard einfügen (Env / .velin/motion/.env).\n',
|
|
372
|
+
);
|
|
373
|
+
} else {
|
|
374
|
+
throw new Error('Abgebrochen');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await promptAndSaveApiKey({ cwd, rl });
|
|
378
|
+
|
|
379
|
+
const test = await testPixVerseConnection({
|
|
380
|
+
cwd,
|
|
381
|
+
skipNetwork: opts.skipNetwork,
|
|
382
|
+
fetchImpl: opts.fetchImpl,
|
|
383
|
+
});
|
|
384
|
+
if (!test.ok && !test.dryCheck) {
|
|
385
|
+
process.stdout.write(`\nVerbindung: ${test.reason}\n`);
|
|
386
|
+
process.stdout.write('(Key wurde gespeichert — du kannst später erneut testen.)\n');
|
|
387
|
+
} else {
|
|
388
|
+
process.stdout.write('\nVerbindung: OK (oder Dry-Check)\n');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
markPixVerseSetupCompleted(cwd);
|
|
392
|
+
return {
|
|
393
|
+
provider: 'pixverse',
|
|
394
|
+
setup_completed: true,
|
|
395
|
+
apiKeyPresent: true,
|
|
396
|
+
openedBrowser,
|
|
397
|
+
test: { ok: test.ok, dryCheck: test.dryCheck, apiKeyPresent: true },
|
|
398
|
+
};
|
|
399
|
+
} finally {
|
|
400
|
+
rl.close();
|
|
401
|
+
}
|
|
402
|
+
}
|