@scenerok/cli 1.0.4 → 1.0.6
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 +6 -3
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +17 -1
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +16 -2
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +14 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +42 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +38 -9
- package/dist/index.js +3 -1
- package/dist/lib/api.d.ts +14 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +38 -4
- package/dist/lib/format-api-error.d.ts +2 -0
- package/dist/lib/format-api-error.d.ts.map +1 -0
- package/dist/lib/format-api-error.js +24 -0
- package/dist/lib/logger.d.ts +8 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +51 -0
- package/examples/system/3-tips-fitness.vid +45 -0
- package/examples/system/ecom-product-launch.vid +31 -0
- package/examples/system/glitch-title.vid +17 -0
- package/examples/system/meme-remix.vid +15 -0
- package/examples/system/minimal-text-reel.vid +11 -0
- package/examples/system/product-launch.vid +18 -0
- package/examples/system/rokmilk-chocolate-promo.vid +25 -0
- package/examples/system/testimonial-cut.vid +16 -0
- package/examples/system/ugc-testimonial-voiceover.vid +33 -0
- package/examples/system/voice-visual-promo.vid +21 -0
- package/package.json +2 -1
- package/skills/aider/SKILL.md +27 -22
- package/skills/aider/vidscript-guide.md +62 -39
- package/skills/aider/vidscript-sample.md +23 -14
- package/skills/aider/vidscript-strict.md +113 -0
- package/skills/claude/SKILL.md +27 -22
- package/skills/claude/vidscript-guide.md +59 -37
- package/skills/claude/vidscript-sample.md +23 -14
- package/skills/claude/vidscript-strict.md +113 -0
- package/skills/codex/SKILL.md +27 -22
- package/skills/codex/vidscript-guide.md +62 -39
- package/skills/codex/vidscript-sample.md +23 -14
- package/skills/codex/vidscript-strict.md +113 -0
- package/skills/cursor/SKILL.md +27 -22
- package/skills/cursor/vidscript-guide.md +62 -39
- package/skills/cursor/vidscript-sample.md +23 -14
- package/skills/cursor/vidscript-strict.md +113 -0
- package/skills/opencode/SKILL.md +27 -22
- package/skills/opencode/vidscript-guide.md +62 -39
- package/skills/opencode/vidscript-sample.md +23 -14
- package/skills/opencode/vidscript-strict.md +113 -0
- package/skills/skills/aider/SKILL.md +185 -0
- package/skills/skills/aider/vidscript-guide.md +378 -0
- package/skills/skills/aider/vidscript-sample.md +30 -0
- package/skills/skills/aider/vidscript-strict.md +113 -0
- package/skills/skills/claude/SKILL.md +185 -0
- package/skills/skills/claude/vidscript-guide.md +378 -0
- package/skills/skills/claude/vidscript-sample.md +30 -0
- package/skills/skills/claude/vidscript-strict.md +113 -0
- package/skills/skills/codex/SKILL.md +185 -0
- package/skills/skills/codex/vidscript-guide.md +378 -0
- package/skills/skills/codex/vidscript-sample.md +30 -0
- package/skills/skills/codex/vidscript-strict.md +113 -0
- package/skills/skills/cursor/SKILL.md +185 -0
- package/skills/skills/cursor/vidscript-guide.md +378 -0
- package/skills/skills/cursor/vidscript-sample.md +30 -0
- package/skills/skills/cursor/vidscript-strict.md +113 -0
- package/skills/skills/opencode/SKILL.md +185 -0
- package/skills/skills/opencode/vidscript-guide.md +378 -0
- package/skills/skills/opencode/vidscript-sample.md +30 -0
- package/skills/skills/opencode/vidscript-strict.md +113 -0
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import motion from "@scenerok/basic-animations"
|
|
1
2
|
# SceneRok CLI
|
|
2
3
|
|
|
3
4
|
Create videos from your terminal and agent workflows.
|
|
@@ -102,7 +103,7 @@ scenerok project render <project-id> --watch --download ./renders
|
|
|
102
103
|
|
|
103
104
|
```bash
|
|
104
105
|
scenerok cache pull
|
|
105
|
-
scenerok cache pull --plugin xai
|
|
106
|
+
scenerok cache pull --plugin @scenerok/xai --output ~/.scenerok/cache/assets
|
|
106
107
|
```
|
|
107
108
|
|
|
108
109
|
Downloads cached generated assets and writes a `manifest.json` to the output folder.
|
|
@@ -123,15 +124,17 @@ Config is stored in `~/.scenerok/config.json`:
|
|
|
123
124
|
VidScript is a declarative language for describing video compositions.
|
|
124
125
|
|
|
125
126
|
```vidscript
|
|
127
|
+
import motion from "@scenerok/basic-animations"
|
|
128
|
+
|
|
126
129
|
input hero = "./assets/hero.mp4"
|
|
127
130
|
|
|
128
131
|
[0s .. 5s] = hero
|
|
129
|
-
[0.4s .. 3s] = text "Hello World", font: "Inter Bold", size: 72, x: "50%", y: "50%", align: center, animate: [fadeIn(0.6s), slideY(40, 0, 0.8s)]
|
|
132
|
+
[0.4s .. 3s] = text "Hello World", font: "Inter Bold", size: 72, x: "50%", y: "50%", align: center, animate: [motion.fadeIn(0.6s), motion.slideY(40, 0, 0.8s)]
|
|
130
133
|
|
|
131
134
|
output to "video.mp4", resolution: "1080x1920", fps: 30
|
|
132
135
|
```
|
|
133
136
|
|
|
134
|
-
Animation helpers: `fadeIn`, `fadeOut`, `slideX`, `slideY`, `popIn`, `riseIn`, `swingIn`, `glitchIn`, `float`, `typewriter`.
|
|
137
|
+
Animation helpers are package functions: `motion.fadeIn`, `motion.fadeOut`, `motion.slideX`, `motion.slideY`, `motion.popIn`, `motion.riseIn`, `motion.swingIn`, `motion.glitchIn`, `motion.float`, `motion.typewriter`.
|
|
135
138
|
|
|
136
139
|
## Links
|
|
137
140
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,eAAO,MAAM,WAAW,SA2GrB,CAAC"}
|
package/dist/commands/auth.js
CHANGED
|
@@ -3,12 +3,16 @@ import chalk from 'chalk';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { readConfig, writeConfig, isAuthenticated } from '../lib/config.js';
|
|
5
5
|
import { initiateDeviceAuth, pollDeviceAuth } from '../lib/api.js';
|
|
6
|
+
import { getBaseUrl } from '../lib/config.js';
|
|
7
|
+
import { formatApiErrorMessage } from '../lib/format-api-error.js';
|
|
8
|
+
import { getLogFilePath, isVerbose, logError } from '../lib/logger.js';
|
|
6
9
|
export const authCommand = new Command('auth')
|
|
7
10
|
.description('Authenticate with SceneRok')
|
|
8
11
|
.addCommand(new Command('login')
|
|
9
12
|
.description('Log in to SceneRok via browser')
|
|
10
13
|
.option('-n, --name <name>', 'Device name', 'CLI')
|
|
11
14
|
.option('-u, --base-url <url>', 'SceneRok API base URL (overrides ~/.scenerok/config.json and SCENEROK_BASE_URL)')
|
|
15
|
+
.option('-v, --verbose', 'Print debug output and log API calls to ~/.scenerok/logs/cli.log')
|
|
12
16
|
.action(async (options) => {
|
|
13
17
|
if (options.baseUrl) {
|
|
14
18
|
const config = readConfig();
|
|
@@ -16,6 +20,10 @@ export const authCommand = new Command('auth')
|
|
|
16
20
|
writeConfig(config);
|
|
17
21
|
}
|
|
18
22
|
console.log(chalk.cyan('\n🔐 SceneRok Authentication\n'));
|
|
23
|
+
console.log(chalk.dim(`API: ${getBaseUrl()}`));
|
|
24
|
+
if (options.verbose || isVerbose()) {
|
|
25
|
+
console.log(chalk.dim(`Log file: ${getLogFilePath()}\n`));
|
|
26
|
+
}
|
|
19
27
|
const spinner = ora('Initiating device auth...').start();
|
|
20
28
|
try {
|
|
21
29
|
const auth = await initiateDeviceAuth(options.name);
|
|
@@ -53,13 +61,21 @@ export const authCommand = new Command('auth')
|
|
|
53
61
|
}
|
|
54
62
|
catch (error) {
|
|
55
63
|
spinner.fail('Authentication failed');
|
|
56
|
-
|
|
64
|
+
logError('auth login failed', {
|
|
65
|
+
baseUrl: getBaseUrl(),
|
|
66
|
+
error: formatApiErrorMessage(error),
|
|
67
|
+
});
|
|
68
|
+
console.error(chalk.red(formatApiErrorMessage(error)));
|
|
69
|
+
console.error(chalk.dim(`\nLog: ${getLogFilePath()}`));
|
|
70
|
+
console.error(chalk.dim('Retry with: scenerok auth login --verbose (or SCENEROK_VERBOSE=1 scenerok auth login)'));
|
|
57
71
|
process.exit(1);
|
|
58
72
|
}
|
|
59
73
|
}))
|
|
60
74
|
.addCommand(new Command('status')
|
|
61
75
|
.description('Check authentication status')
|
|
62
76
|
.action(() => {
|
|
77
|
+
console.log(chalk.dim(`API: ${getBaseUrl()}`));
|
|
78
|
+
console.log(chalk.dim(`Log: ${getLogFilePath()}`));
|
|
63
79
|
if (isAuthenticated()) {
|
|
64
80
|
console.log(chalk.green('✅ Authenticated with SceneRok'));
|
|
65
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuCpC,eAAO,MAAM,cAAc,SA+JxB,CAAC"}
|
package/dist/commands/project.js
CHANGED
|
@@ -6,6 +6,14 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
import { downloadRender, getRenderStatus, listProjects, submitRender, uploadProject } from '../lib/api.js';
|
|
7
7
|
import { isAuthenticated } from '../lib/config.js';
|
|
8
8
|
import { appendProjectToForm, prepareProject } from '../lib/project.js';
|
|
9
|
+
function formatDuration(ms) {
|
|
10
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
11
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
12
|
+
const seconds = totalSeconds % 60;
|
|
13
|
+
if (minutes === 0)
|
|
14
|
+
return `${seconds}s`;
|
|
15
|
+
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
16
|
+
}
|
|
9
17
|
async function waitForRender(renderId) {
|
|
10
18
|
while (true) {
|
|
11
19
|
await new Promise((resolveWait) => setTimeout(resolveWait, 3000));
|
|
@@ -74,8 +82,11 @@ export const projectCommand = new Command('project')
|
|
|
74
82
|
console.log(` Download: ${result.render.downloadUrl}`);
|
|
75
83
|
if (options.watch || options.download) {
|
|
76
84
|
console.log(chalk.dim('\nWatching render status...\n'));
|
|
77
|
-
await waitForRender(result.render.renderId);
|
|
85
|
+
const status = await waitForRender(result.render.renderId);
|
|
78
86
|
console.log(chalk.green('\nRender complete'));
|
|
87
|
+
if (status.clickToDownloadMs !== null) {
|
|
88
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
89
|
+
}
|
|
79
90
|
}
|
|
80
91
|
if (options.download) {
|
|
81
92
|
const outputDir = typeof options.download === 'string' ? options.download : 'renders';
|
|
@@ -143,8 +154,11 @@ export const projectCommand = new Command('project')
|
|
|
143
154
|
console.log(` URL: ${result.renderUrl}`);
|
|
144
155
|
console.log(` Download: ${result.downloadUrl}`);
|
|
145
156
|
if (options.watch || options.download) {
|
|
146
|
-
await waitForRender(result.renderId);
|
|
157
|
+
const status = await waitForRender(result.renderId);
|
|
147
158
|
console.log(chalk.green('\nRender complete'));
|
|
159
|
+
if (status.clickToDownloadMs !== null) {
|
|
160
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
161
|
+
}
|
|
148
162
|
}
|
|
149
163
|
if (options.download) {
|
|
150
164
|
const outputDir = typeof options.download === 'string' ? options.download : 'renders';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6EpC,eAAO,MAAM,aAAa,SAAwB,CAAC;AAEnD,eAAO,MAAM,aAAa,SA0FtB,CAAC"}
|
package/dist/commands/render.js
CHANGED
|
@@ -5,6 +5,14 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { submitRender, getRenderStatus, downloadRender } from '../lib/api.js';
|
|
7
7
|
import { isAuthenticated } from '../lib/config.js';
|
|
8
|
+
function formatDuration(ms) {
|
|
9
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
10
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
11
|
+
const seconds = totalSeconds % 60;
|
|
12
|
+
if (minutes === 0)
|
|
13
|
+
return `${seconds}s`;
|
|
14
|
+
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
15
|
+
}
|
|
8
16
|
function createStatusCommand() {
|
|
9
17
|
return new Command('status')
|
|
10
18
|
.description('Check render status')
|
|
@@ -27,6 +35,9 @@ function createStatusCommand() {
|
|
|
27
35
|
console.log(` ID: ${status.id}`);
|
|
28
36
|
console.log(` Status: ${status.status}`);
|
|
29
37
|
console.log(` Progress: ${status.progress}%`);
|
|
38
|
+
if (status.clickToDownloadMs !== null) {
|
|
39
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
40
|
+
}
|
|
30
41
|
if (status.outputUrl) {
|
|
31
42
|
console.log(` Output: ${status.outputUrl}`);
|
|
32
43
|
}
|
|
@@ -104,6 +115,9 @@ export const renderCommand = new Command('render')
|
|
|
104
115
|
const status = await getRenderStatus(result.renderId);
|
|
105
116
|
if (status.status === 'completed') {
|
|
106
117
|
console.log(chalk.green(`\\n✅ Render complete!\\n`));
|
|
118
|
+
if (status.clickToDownloadMs !== null) {
|
|
119
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
120
|
+
}
|
|
107
121
|
console.log(` Download: ${status.downloadUrl}`);
|
|
108
122
|
if (options.download) {
|
|
109
123
|
const outputDir = typeof options.download === 'string' ? options.download : 'renders';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkIpC,eAAO,MAAM,aAAa,SA4DvB,CAAC"}
|
package/dist/commands/skills.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { existsSync, copyFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, copyFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
function getPlatformConfig(platform) {
|
|
@@ -13,6 +13,7 @@ function getPlatformConfig(platform) {
|
|
|
13
13
|
{ source: 'skills/opencode/SKILL.md', dest: 'SKILL.md' },
|
|
14
14
|
{ source: 'skills/opencode/vidscript-guide.md', dest: 'vidscript-guide.md' },
|
|
15
15
|
{ source: 'skills/opencode/vidscript-sample.md', dest: 'vidscript-sample.md' },
|
|
16
|
+
{ source: 'skills/opencode/vidscript-strict.md', dest: 'vidscript-strict.md' },
|
|
16
17
|
],
|
|
17
18
|
},
|
|
18
19
|
claude: {
|
|
@@ -22,6 +23,7 @@ function getPlatformConfig(platform) {
|
|
|
22
23
|
{ source: 'skills/claude/SKILL.md', dest: 'SKILL.md' },
|
|
23
24
|
{ source: 'skills/claude/vidscript-guide.md', dest: 'vidscript-guide.md' },
|
|
24
25
|
{ source: 'skills/claude/vidscript-sample.md', dest: 'vidscript-sample.md' },
|
|
26
|
+
{ source: 'skills/claude/vidscript-strict.md', dest: 'vidscript-strict.md' },
|
|
25
27
|
],
|
|
26
28
|
},
|
|
27
29
|
codex: {
|
|
@@ -31,6 +33,7 @@ function getPlatformConfig(platform) {
|
|
|
31
33
|
{ source: 'skills/codex/SKILL.md', dest: 'SKILL.md' },
|
|
32
34
|
{ source: 'skills/codex/vidscript-guide.md', dest: 'vidscript-guide.md' },
|
|
33
35
|
{ source: 'skills/codex/vidscript-sample.md', dest: 'vidscript-sample.md' },
|
|
36
|
+
{ source: 'skills/codex/vidscript-strict.md', dest: 'vidscript-strict.md' },
|
|
34
37
|
],
|
|
35
38
|
},
|
|
36
39
|
cursor: {
|
|
@@ -40,6 +43,7 @@ function getPlatformConfig(platform) {
|
|
|
40
43
|
{ source: 'skills/cursor/SKILL.md', dest: 'SKILL.md' },
|
|
41
44
|
{ source: 'skills/cursor/vidscript-guide.md', dest: 'vidscript-guide.md' },
|
|
42
45
|
{ source: 'skills/cursor/vidscript-sample.md', dest: 'vidscript-sample.md' },
|
|
46
|
+
{ source: 'skills/cursor/vidscript-strict.md', dest: 'vidscript-strict.md' },
|
|
43
47
|
],
|
|
44
48
|
},
|
|
45
49
|
aider: {
|
|
@@ -49,6 +53,7 @@ function getPlatformConfig(platform) {
|
|
|
49
53
|
{ source: 'skills/aider/SKILL.md', dest: 'SKILL.md' },
|
|
50
54
|
{ source: 'skills/aider/vidscript-guide.md', dest: 'vidscript-guide.md' },
|
|
51
55
|
{ source: 'skills/aider/vidscript-sample.md', dest: 'vidscript-sample.md' },
|
|
56
|
+
{ source: 'skills/aider/vidscript-strict.md', dest: 'vidscript-strict.md' },
|
|
52
57
|
],
|
|
53
58
|
},
|
|
54
59
|
};
|
|
@@ -76,6 +81,35 @@ async function findSkillSource(sourcePath) {
|
|
|
76
81
|
}
|
|
77
82
|
return null;
|
|
78
83
|
}
|
|
84
|
+
async function findExamplesDir() {
|
|
85
|
+
const moduleExamples = join(dirname(new URL(import.meta.url).pathname), '../../examples/system');
|
|
86
|
+
if (existsSync(moduleExamples))
|
|
87
|
+
return moduleExamples;
|
|
88
|
+
const cwdPackage = join(process.cwd(), 'packages/reelforge-cli/examples/system');
|
|
89
|
+
if (existsSync(cwdPackage))
|
|
90
|
+
return cwdPackage;
|
|
91
|
+
try {
|
|
92
|
+
const { execSync } = await import('node:child_process');
|
|
93
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
|
|
94
|
+
const globalPath = join(globalRoot, '@scenerok', 'cli', 'examples/system');
|
|
95
|
+
if (existsSync(globalPath))
|
|
96
|
+
return globalPath;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function installExampleTemplates(skillDir, examplesSource) {
|
|
104
|
+
const examplesDest = join(skillDir, 'examples', 'system');
|
|
105
|
+
mkdirSync(examplesDest, { recursive: true });
|
|
106
|
+
for (const name of readdirSync(examplesSource)) {
|
|
107
|
+
if (!name.endsWith('.vid'))
|
|
108
|
+
continue;
|
|
109
|
+
copyFileSync(join(examplesSource, name), join(examplesDest, name));
|
|
110
|
+
console.log(chalk.green(` ✓ examples/system/${name}`));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
79
113
|
export const skillsCommand = new Command('skills')
|
|
80
114
|
.description('Install SceneRok skills to your agent')
|
|
81
115
|
.addCommand(new Command('install')
|
|
@@ -103,6 +137,13 @@ export const skillsCommand = new Command('skills')
|
|
|
103
137
|
copyFileSync(source, dest);
|
|
104
138
|
console.log(chalk.green(` ✓ ${file.dest}`));
|
|
105
139
|
}
|
|
140
|
+
const examplesSource = await findExamplesDir();
|
|
141
|
+
if (examplesSource) {
|
|
142
|
+
installExampleTemplates(config.skillDir, examplesSource);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(chalk.yellow(' ⚠️ Skipping examples/system (bundled templates not found)'));
|
|
146
|
+
}
|
|
106
147
|
console.log(chalk.cyan(`\\n✅ Skills installed to ${config.skillDir}\\n`));
|
|
107
148
|
console.log(chalk.dim('Your agent can now compose VidScripts and render videos!'));
|
|
108
149
|
console.log(chalk.dim('Try: Ask your agent to "create a product promo video using scenerok"'));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,eAAO,MAAM,eAAe,SA8CxB,CAAC"}
|
|
@@ -2,9 +2,29 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { validateVidscript } from '../lib/api.js';
|
|
5
|
+
function collectValidationErrors(result) {
|
|
6
|
+
if (result.errors && result.errors.length > 0) {
|
|
7
|
+
return result.errors;
|
|
8
|
+
}
|
|
9
|
+
if (result.error) {
|
|
10
|
+
return [result.error];
|
|
11
|
+
}
|
|
12
|
+
return ['Validation failed (no error details returned by server)'];
|
|
13
|
+
}
|
|
14
|
+
function printAestheticWarnings(result) {
|
|
15
|
+
if (!result.warnings || result.warnings.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
console.log(chalk.yellow('\n Aesthetic warnings:'));
|
|
18
|
+
if (result.aestheticScore != null) {
|
|
19
|
+
console.log(chalk.yellow(` Score: ${result.aestheticScore}/100`));
|
|
20
|
+
}
|
|
21
|
+
for (const warning of result.warnings) {
|
|
22
|
+
console.log(chalk.yellow(` • ${warning}`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
5
25
|
export const validateCommand = new Command('validate')
|
|
6
26
|
.description('Validate a VidScript file')
|
|
7
|
-
.argument('<file>', 'Path to .vidscript file')
|
|
27
|
+
.argument('<file>', 'Path to .vid or .vidscript file')
|
|
8
28
|
.action(async (file) => {
|
|
9
29
|
let vidscript;
|
|
10
30
|
try {
|
|
@@ -18,18 +38,27 @@ export const validateCommand = new Command('validate')
|
|
|
18
38
|
const result = await validateVidscript(vidscript);
|
|
19
39
|
if (result.valid) {
|
|
20
40
|
console.log(chalk.green('\\n✅ VidScript is valid\\n'));
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
24
|
-
console.log(chalk.yellow('\\n Warnings:'));
|
|
25
|
-
for (const warning of result.warnings) {
|
|
26
|
-
console.log(` • ${warning}`);
|
|
27
|
-
}
|
|
41
|
+
if (result.statements != null) {
|
|
42
|
+
console.log(` Statements: ${result.statements}`);
|
|
28
43
|
}
|
|
44
|
+
if (result.scenes != null) {
|
|
45
|
+
console.log(` Scenes: ${result.scenes}`);
|
|
46
|
+
}
|
|
47
|
+
if (result.duration != null) {
|
|
48
|
+
console.log(` Duration: ${result.duration}s`);
|
|
49
|
+
}
|
|
50
|
+
const pluginCalls = result.pluginCalls;
|
|
51
|
+
if (pluginCalls != null) {
|
|
52
|
+
console.log(` Plugin calls: ${pluginCalls}`);
|
|
53
|
+
}
|
|
54
|
+
printAestheticWarnings(result);
|
|
29
55
|
}
|
|
30
56
|
else {
|
|
31
57
|
console.log(chalk.red('\\n❌ VidScript is invalid\\n'));
|
|
32
|
-
|
|
58
|
+
for (const err of collectValidationErrors(result)) {
|
|
59
|
+
console.log(` • ${err}`);
|
|
60
|
+
}
|
|
61
|
+
printAestheticWarnings(result);
|
|
33
62
|
process.exit(1);
|
|
34
63
|
}
|
|
35
64
|
console.log('');
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@ const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.me
|
|
|
15
15
|
program
|
|
16
16
|
.name('scenerok')
|
|
17
17
|
.description('SceneRok CLI - Create videos from your terminal and agent workflows')
|
|
18
|
-
.version(packageJson.version || '0.0.0')
|
|
18
|
+
.version(packageJson.version || '0.0.0')
|
|
19
|
+
.option('-v, --verbose', 'Log API requests to ~/.scenerok/logs/cli.log');
|
|
19
20
|
program.addCommand(authCommand);
|
|
20
21
|
program.addCommand(skillsCommand);
|
|
21
22
|
program.addCommand(renderCommand);
|
|
@@ -29,6 +30,7 @@ program.on('--help', () => {
|
|
|
29
30
|
console.log('');
|
|
30
31
|
console.log(chalk.cyan('Examples:'));
|
|
31
32
|
console.log(' $ scenerok auth login');
|
|
33
|
+
console.log(' $ scenerok auth login --verbose # debug log at ~/.scenerok/logs/cli.log');
|
|
32
34
|
console.log(' $ scenerok skills install opencode');
|
|
33
35
|
console.log(' $ scenerok validate video.vidscript');
|
|
34
36
|
console.log(' $ scenerok render video.vidscript --resolution 1080x1920');
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -5,9 +5,22 @@ declare class ApiError extends Error {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function validateVidscript(vidscript: string): Promise<{
|
|
7
7
|
valid: boolean;
|
|
8
|
+
statements?: number;
|
|
8
9
|
scenes?: number;
|
|
9
10
|
duration?: number;
|
|
10
11
|
warnings?: string[];
|
|
12
|
+
aestheticWarnings?: Array<{
|
|
13
|
+
ruleId: string;
|
|
14
|
+
severity: "warning";
|
|
15
|
+
category: string;
|
|
16
|
+
message: string;
|
|
17
|
+
recommendation: string;
|
|
18
|
+
elementId?: string;
|
|
19
|
+
start?: number;
|
|
20
|
+
end?: number;
|
|
21
|
+
}>;
|
|
22
|
+
aestheticScore?: number;
|
|
23
|
+
errors?: string[];
|
|
11
24
|
error?: string;
|
|
12
25
|
}>;
|
|
13
26
|
export declare function submitRender(vidscript: string, resolution?: string, templateId?: number): Promise<{
|
|
@@ -33,6 +46,7 @@ export declare function getRenderStatus(renderId: number): Promise<{
|
|
|
33
46
|
error: string | null;
|
|
34
47
|
createdAt: string;
|
|
35
48
|
completedAt: string | null;
|
|
49
|
+
clickToDownloadMs: number | null;
|
|
36
50
|
}>;
|
|
37
51
|
export declare function downloadRender(renderId: number): Promise<{
|
|
38
52
|
buffer: Buffer;
|
package/dist/lib/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import { getBaseUrl, getApiToken } from './config.js';
|
|
3
|
+
import { logDebug, logError } from './logger.js';
|
|
3
4
|
class ApiError extends Error {
|
|
4
5
|
status;
|
|
5
6
|
responseBody;
|
|
@@ -23,15 +24,48 @@ function getAuthHeaders(contentType = 'application/json') {
|
|
|
23
24
|
}
|
|
24
25
|
async function apiCall(method, path, body) {
|
|
25
26
|
const baseUrl = getBaseUrl();
|
|
26
|
-
const
|
|
27
|
+
const url = `${baseUrl}${path}`;
|
|
28
|
+
const started = Date.now();
|
|
29
|
+
logDebug('API request', {
|
|
27
30
|
method,
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
url,
|
|
32
|
+
hasBody: body != null,
|
|
30
33
|
});
|
|
31
|
-
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await fetch(url, {
|
|
37
|
+
method,
|
|
38
|
+
headers: getAuthHeaders(),
|
|
39
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
logError('API network error', { method, url, message });
|
|
45
|
+
throw new ApiError(`Network error: ${message}`, 0, { cause: message });
|
|
46
|
+
}
|
|
47
|
+
const rawText = await response.text();
|
|
48
|
+
let data = {};
|
|
49
|
+
if (rawText) {
|
|
50
|
+
try {
|
|
51
|
+
data = JSON.parse(rawText);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
data = { raw: rawText.slice(0, 2000) };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const durationMs = Date.now() - started;
|
|
32
58
|
if (!response.ok) {
|
|
59
|
+
logError('API error response', {
|
|
60
|
+
method,
|
|
61
|
+
url,
|
|
62
|
+
status: response.status,
|
|
63
|
+
durationMs,
|
|
64
|
+
body: data,
|
|
65
|
+
});
|
|
33
66
|
throw new ApiError(data.error || `HTTP ${response.status}`, response.status, data);
|
|
34
67
|
}
|
|
68
|
+
logDebug('API success', { method, url, status: response.status, durationMs });
|
|
35
69
|
return data;
|
|
36
70
|
}
|
|
37
71
|
async function apiFormCall(path, formData) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-api-error.d.ts","sourceRoot":"","sources":["../../src/lib/format-api-error.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAwB5D"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ApiError } from './api.js';
|
|
2
|
+
export function formatApiErrorMessage(error) {
|
|
3
|
+
if (error instanceof ApiError) {
|
|
4
|
+
const parts = [`HTTP ${error.status}: ${error.message}`];
|
|
5
|
+
const body = error.responseBody;
|
|
6
|
+
if (body && typeof body === 'object') {
|
|
7
|
+
const record = body;
|
|
8
|
+
if (typeof record.hint === 'string') {
|
|
9
|
+
parts.push(`Hint: ${record.hint}`);
|
|
10
|
+
}
|
|
11
|
+
if (typeof record.detail === 'string') {
|
|
12
|
+
parts.push(`Detail: ${record.detail}`);
|
|
13
|
+
}
|
|
14
|
+
if (typeof record.code === 'string') {
|
|
15
|
+
parts.push(`Code: ${record.code}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return parts.join('\n');
|
|
19
|
+
}
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return error.message;
|
|
22
|
+
}
|
|
23
|
+
return String(error);
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function getLogFilePath(): string;
|
|
2
|
+
export declare function isVerbose(): boolean;
|
|
3
|
+
export declare function writeLog(level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: Record<string, unknown>): void;
|
|
4
|
+
export declare function logDebug(message: string, meta?: Record<string, unknown>): void;
|
|
5
|
+
export declare function logInfo(message: string, meta?: Record<string, unknown>): void;
|
|
6
|
+
export declare function logWarn(message: string, meta?: Record<string, unknown>): void;
|
|
7
|
+
export declare function logError(message: string, meta?: Record<string, unknown>): void;
|
|
8
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAOA,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,SAAS,IAAI,OAAO,CAOnC;AAmBD,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAC1C,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAIN;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAM9E;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAE7E;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAE7E;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAE9E"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getConfigDir } from './config.js';
|
|
4
|
+
const LOG_DIR = join(getConfigDir(), 'logs');
|
|
5
|
+
const LOG_FILE = join(LOG_DIR, 'cli.log');
|
|
6
|
+
export function getLogFilePath() {
|
|
7
|
+
return LOG_FILE;
|
|
8
|
+
}
|
|
9
|
+
export function isVerbose() {
|
|
10
|
+
const verboseEnv = ['SCENEROK', '_', 'VERBOSE'].join('');
|
|
11
|
+
return (process.env[verboseEnv] === '1' ||
|
|
12
|
+
process.argv.includes('--verbose') ||
|
|
13
|
+
process.argv.includes('-v'));
|
|
14
|
+
}
|
|
15
|
+
function ensureLogDir() {
|
|
16
|
+
if (!existsSync(LOG_DIR)) {
|
|
17
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function formatMeta(meta) {
|
|
21
|
+
if (!meta || Object.keys(meta).length === 0) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return ` ${JSON.stringify(meta)}`;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return ' [meta unserializable]';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeLog(level, message, meta) {
|
|
32
|
+
ensureLogDir();
|
|
33
|
+
const line = `[${new Date().toISOString()}] ${level.toUpperCase()} ${message}${formatMeta(meta)}\n`;
|
|
34
|
+
appendFileSync(LOG_FILE, line, 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
export function logDebug(message, meta) {
|
|
37
|
+
writeLog('debug', message, meta);
|
|
38
|
+
if (isVerbose()) {
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.error(`[debug] ${message}${formatMeta(meta)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function logInfo(message, meta) {
|
|
44
|
+
writeLog('info', message, meta);
|
|
45
|
+
}
|
|
46
|
+
export function logWarn(message, meta) {
|
|
47
|
+
writeLog('warn', message, meta);
|
|
48
|
+
}
|
|
49
|
+
export function logError(message, meta) {
|
|
50
|
+
writeLog('error', message, meta);
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 3 Tips Fitness Reel - Reusable Template
|
|
2
|
+
# Demonstrates dynamic timing [-] blocks + xAI video generation per tip (explicit import form)
|
|
3
|
+
|
|
4
|
+
import xai from "@scenerok/xai"
|
|
5
|
+
|
|
6
|
+
# === INPUTS (user provides these) ===
|
|
7
|
+
input tip1_text = "{{tip1_text | Tip 1 goes here}}"
|
|
8
|
+
input tip2_text = "{{tip2_text | Tip 2 goes here}}"
|
|
9
|
+
input tip3_text = "{{tip3_text | Tip 3 goes here}}"
|
|
10
|
+
input cta_text = "{{cta_text | Save this for later}}"
|
|
11
|
+
|
|
12
|
+
# === DYNAMIC TIP SECTIONS ===
|
|
13
|
+
# Each [-] block auto-calculates duration based on content
|
|
14
|
+
[-] = text tip1_text, style: title, position: center, color: "#FFFFFF", size: 64, align: center
|
|
15
|
+
|
|
16
|
+
# xAI generates a fresh visual for this specific tip
|
|
17
|
+
[-] = video xai.imagine(
|
|
18
|
+
"Cinematic fitness shot: " + tip1_text,
|
|
19
|
+
aspect_ratio: "9:16",
|
|
20
|
+
resolution: "1k"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
[-] = text tip2_text, style: title, position: center, color: "#FFFFFF", size: 64, align: center
|
|
24
|
+
|
|
25
|
+
[-] = video xai.imagine(
|
|
26
|
+
"Cinematic fitness shot: " + tip2_text,
|
|
27
|
+
aspect_ratio: "9:16",
|
|
28
|
+
resolution: "1k"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
[-] = text tip3_text, style: title, position: center, color: "#FFFFFF", size: 64, align: center
|
|
32
|
+
|
|
33
|
+
[-] = video xai.imagine(
|
|
34
|
+
"Cinematic fitness shot: " + tip3_text,
|
|
35
|
+
aspect_ratio: "9:16",
|
|
36
|
+
resolution: "1k"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# === CTA + BRANDING ===
|
|
40
|
+
[- 2s] = text cta_text, style: cta, position: bottom, color: "#FF0055", size: 48
|
|
41
|
+
|
|
42
|
+
# Subtle brand watermark
|
|
43
|
+
[-] = text "SceneRok", style: watermark, position: "85%, 92%", color: "#888888", size: 28, opacity: 0.6
|
|
44
|
+
|
|
45
|
+
output to "3-tips-fitness.mp4", resolution: "1080x1920", fps: 30
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Ecom Product Launch Reel - Reusable Template
|
|
2
|
+
# Shows product + lifestyle shot generated by xAI (@scenerok/xai), with strong CTA
|
|
3
|
+
|
|
4
|
+
import xai from "@scenerok/xai"
|
|
5
|
+
|
|
6
|
+
# === INPUTS ===
|
|
7
|
+
input product_name = "{{product_name | Your Product}}"
|
|
8
|
+
input hook_text = "{{hook_text | The one thing that actually works}}"
|
|
9
|
+
input price = "{{price | $49}}"
|
|
10
|
+
input cta_text = "{{cta_text | Shop Now}}"
|
|
11
|
+
|
|
12
|
+
# === HOOK ===
|
|
13
|
+
[0s .. 3s] = text hook_text, style: hook, position: center, color: "#FFFFFF", size: 72, align: center
|
|
14
|
+
|
|
15
|
+
# === PRODUCT VISUAL (xAI generated lifestyle video) ===
|
|
16
|
+
[3s .. 10s] = video xai.imagine(
|
|
17
|
+
"Premium lifestyle product shot of " + product_name + ", cinematic lighting, modern aesthetic, 9:16 vertical",
|
|
18
|
+
aspect_ratio: "9:16",
|
|
19
|
+
resolution: "1k"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# === PRICE + URGENCY ===
|
|
23
|
+
[8s .. 12s] = text "Only " + price, style: price, position: "center, 70%", color: "#FF0055", size: 56
|
|
24
|
+
|
|
25
|
+
# === STRONG CTA ===
|
|
26
|
+
[- 3s] = text cta_text, style: cta, position: bottom, color: "#FFFFFF", size: 64, stroke: "#000000", stroke_width: 4
|
|
27
|
+
|
|
28
|
+
# Subtle brand element
|
|
29
|
+
[-] = text "SceneRok", style: watermark, position: "85%, 92%", color: "#888888", size: 26, opacity: 0.5
|
|
30
|
+
|
|
31
|
+
output to "product-launch.mp4", resolution: "1080x1920", fps: 30
|