@scenerok/cli 2.2.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 +173 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +94 -0
- package/dist/commands/cache.d.ts +3 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +65 -0
- package/dist/commands/lint-video.d.ts +21 -0
- package/dist/commands/lint-video.d.ts.map +1 -0
- package/dist/commands/lint-video.js +122 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +173 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +242 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +208 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +254 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/lib/api.d.ts +268 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +160 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +53 -0
- 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/language.d.ts +3 -0
- package/dist/lib/language.d.ts.map +1 -0
- package/dist/lib/language.js +9 -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/dist/lib/project.d.ts +29 -0
- package/dist/lib/project.d.ts.map +1 -0
- package/dist/lib/project.js +156 -0
- package/dist/lib/sdk-scene-modules.d.ts +7 -0
- package/dist/lib/sdk-scene-modules.d.ts.map +1 -0
- package/dist/lib/sdk-scene-modules.js +35 -0
- package/dist/utils/skillsManifest.d.ts +42 -0
- package/dist/utils/skillsManifest.d.ts.map +1 -0
- package/dist/utils/skillsManifest.js +122 -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 +51 -0
- package/skills/remote/ai-models.md +65 -0
- package/skills/remote/animations-catalog.md +89 -0
- package/skills/remote/animations-picker.md +77 -0
- package/skills/remote/brand-setup.md +69 -0
- package/skills/remote/catalog-index.json +1810 -0
- package/skills/remote/environments.md +73 -0
- package/skills/remote/intent-router.md +81 -0
- package/skills/remote/media-stdlib.md +66 -0
- package/skills/remote/music.md +69 -0
- package/skills/remote/sdk-guide.md +201 -0
- package/skills/remote/shaders-filters.md +97 -0
- package/skills/remote/text-styles.md +108 -0
- package/skills/remote/transitions-catalog.md +55 -0
- package/skills/remote/tts.md +82 -0
- package/skills/remote/vidscript-guide.md +574 -0
- package/skills/remote/vidscript-sample.md +28 -0
- package/skills/remote/vidscript-strict.md +251 -0
- package/skills/remote/workflow.md +72 -0
- package/skills/shared/SKILL.md +157 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { downloadRender, getRenderStatus, listProjects, submitRender, uploadProject } from '../lib/api.js';
|
|
7
|
+
import { isAuthenticated } from '../lib/config.js';
|
|
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
|
+
}
|
|
17
|
+
async function waitForRender(renderId) {
|
|
18
|
+
while (true) {
|
|
19
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 3000));
|
|
20
|
+
const status = await getRenderStatus(renderId);
|
|
21
|
+
if (status.status === 'completed')
|
|
22
|
+
return status;
|
|
23
|
+
if (status.status === 'failed') {
|
|
24
|
+
throw new Error(status.error || 'Render failed');
|
|
25
|
+
}
|
|
26
|
+
process.stdout.write(`\r ${status.status}... ${status.progress}%`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function downloadRenderTo(renderId, outputDir) {
|
|
30
|
+
const result = await downloadRender(renderId);
|
|
31
|
+
mkdirSync(outputDir, { recursive: true });
|
|
32
|
+
const outputPath = join(outputDir, result.filename);
|
|
33
|
+
writeFileSync(outputPath, result.buffer);
|
|
34
|
+
return outputPath;
|
|
35
|
+
}
|
|
36
|
+
export const projectCommand = new Command('project')
|
|
37
|
+
.description('Upload and render SceneRok projects')
|
|
38
|
+
.addCommand(new Command('upload')
|
|
39
|
+
.description('Upload a local VidScript project and its assets')
|
|
40
|
+
.argument('<file>', 'Path to the project .vidscript file')
|
|
41
|
+
.option('-t, --title <title>', 'Project title')
|
|
42
|
+
.option('-d, --description <description>', 'Project description')
|
|
43
|
+
.option('-a, --assets <dir...>', 'Additional asset directories to upload')
|
|
44
|
+
.option('--tags <tags>', 'Comma-separated project tags')
|
|
45
|
+
.option('-r, --render', 'Create a render after uploading')
|
|
46
|
+
.option('--resolution <resolution>', 'Render resolution if --render is used')
|
|
47
|
+
.option('-w, --watch', 'Watch the render until complete')
|
|
48
|
+
.option('--download [dir]', 'Download completed render to a directory', false)
|
|
49
|
+
.action(async (file, options) => {
|
|
50
|
+
if (!isAuthenticated()) {
|
|
51
|
+
console.log(chalk.yellow('Not authenticated'));
|
|
52
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const spinner = ora('Preparing project...').start();
|
|
56
|
+
try {
|
|
57
|
+
const project = prepareProject(file, options.assets || []);
|
|
58
|
+
const formData = new FormData();
|
|
59
|
+
formData.set('title', options.title || project.entryFile.replace(/\.(vid|vidscript)$/i, ''));
|
|
60
|
+
if (options.description)
|
|
61
|
+
formData.set('description', options.description);
|
|
62
|
+
if (options.tags)
|
|
63
|
+
formData.set('tags', options.tags);
|
|
64
|
+
if (options.render)
|
|
65
|
+
formData.set('render', 'true');
|
|
66
|
+
if (options.resolution)
|
|
67
|
+
formData.set('resolution', options.resolution);
|
|
68
|
+
await appendProjectToForm(formData, project);
|
|
69
|
+
spinner.text = `Uploading project (${project.assets.length} asset${project.assets.length === 1 ? '' : 's'})...`;
|
|
70
|
+
const result = await uploadProject(formData);
|
|
71
|
+
spinner.succeed(`Project uploaded (#${result.project.id})`);
|
|
72
|
+
console.log(chalk.cyan('\nProject\n'));
|
|
73
|
+
console.log(` ID: ${result.project.id}`);
|
|
74
|
+
console.log(` Title: ${result.project.title}`);
|
|
75
|
+
console.log(` URL: ${result.project.projectUrl}`);
|
|
76
|
+
console.log(` Assets: ${result.assets.length}`);
|
|
77
|
+
if (result.render) {
|
|
78
|
+
console.log(chalk.cyan('\nRender\n'));
|
|
79
|
+
console.log(` ID: ${result.render.renderId}`);
|
|
80
|
+
console.log(` Status: ${result.render.status}`);
|
|
81
|
+
console.log(` URL: ${result.render.renderUrl}`);
|
|
82
|
+
console.log(` Download: ${result.render.downloadUrl}`);
|
|
83
|
+
if (options.watch || options.download) {
|
|
84
|
+
console.log(chalk.dim('\nWatching render status...\n'));
|
|
85
|
+
const status = await waitForRender(result.render.renderId);
|
|
86
|
+
console.log(chalk.green('\nRender complete'));
|
|
87
|
+
if (status.clickToDownloadMs !== null) {
|
|
88
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (options.download) {
|
|
92
|
+
const outputDir = typeof options.download === 'string' ? options.download : 'renders';
|
|
93
|
+
const outputPath = await downloadRenderTo(result.render.renderId, resolve(outputDir));
|
|
94
|
+
console.log(chalk.green(`Downloaded: ${outputPath}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
spinner.fail('Project upload failed');
|
|
101
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}))
|
|
105
|
+
.addCommand(new Command('list')
|
|
106
|
+
.description('List projects uploaded from CLI/agents')
|
|
107
|
+
.action(async () => {
|
|
108
|
+
if (!isAuthenticated()) {
|
|
109
|
+
console.log(chalk.yellow('Not authenticated'));
|
|
110
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = await listProjects();
|
|
115
|
+
console.log(chalk.cyan('\nProjects\n'));
|
|
116
|
+
for (const project of result.projects) {
|
|
117
|
+
console.log(` #${project.id} ${project.title} (${project.status})`);
|
|
118
|
+
}
|
|
119
|
+
if (result.projects.length === 0) {
|
|
120
|
+
console.log(chalk.dim(' No CLI projects yet.'));
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}))
|
|
129
|
+
.addCommand(new Command('render')
|
|
130
|
+
.description('Render an uploaded project')
|
|
131
|
+
.argument('<project-id>', 'Project ID')
|
|
132
|
+
.option('-r, --resolution <resolution>', 'Output resolution')
|
|
133
|
+
.option('-w, --watch', 'Watch render status until complete')
|
|
134
|
+
.option('--download [dir]', 'Download completed render to a directory', false)
|
|
135
|
+
.action(async (projectId, options) => {
|
|
136
|
+
if (!isAuthenticated()) {
|
|
137
|
+
console.log(chalk.yellow('Not authenticated'));
|
|
138
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const id = Number.parseInt(projectId, 10);
|
|
142
|
+
if (Number.isNaN(id)) {
|
|
143
|
+
console.log(chalk.red('Invalid project ID'));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const projects = await listProjects();
|
|
148
|
+
const project = projects.projects.find((item) => item.id === id);
|
|
149
|
+
if (!project) {
|
|
150
|
+
throw new Error(`Project not found: ${id}`);
|
|
151
|
+
}
|
|
152
|
+
const result = await submitRender('', options.resolution, id);
|
|
153
|
+
console.log(chalk.green(`Render submitted (#${result.renderId})`));
|
|
154
|
+
console.log(` URL: ${result.renderUrl}`);
|
|
155
|
+
console.log(` Download: ${result.downloadUrl}`);
|
|
156
|
+
if (options.watch || options.download) {
|
|
157
|
+
const status = await waitForRender(result.renderId);
|
|
158
|
+
console.log(chalk.green('\nRender complete'));
|
|
159
|
+
if (status.clickToDownloadMs !== null) {
|
|
160
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (options.download) {
|
|
164
|
+
const outputDir = typeof options.download === 'string' ? options.download : 'renders';
|
|
165
|
+
const outputPath = await downloadRenderTo(result.renderId, resolve(outputDir));
|
|
166
|
+
console.log(chalk.green(`Downloaded: ${outputPath}`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuFpC,eAAO,MAAM,aAAa,SAAwB,CAAC;AAEnD,eAAO,MAAM,aAAa,SAmLtB,CAAC"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { submitRender, getRenderStatus, downloadRender, uploadCliAssets } from '../lib/api.js';
|
|
7
|
+
import { isAuthenticated } from '../lib/config.js';
|
|
8
|
+
import { LINT_COST_CHOICES, runVideoLint } from './lint-video.js';
|
|
9
|
+
import { prepareProject, getMimeType, rewritePreparedVidscriptWithAssetUrls, writeAssetManifest, } from '../lib/project.js';
|
|
10
|
+
import { detectAuthoringLanguage } from '../lib/language.js';
|
|
11
|
+
import { buildSdkSceneModuleMap } from '../lib/sdk-scene-modules.js';
|
|
12
|
+
function formatDuration(ms) {
|
|
13
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
14
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
15
|
+
const seconds = totalSeconds % 60;
|
|
16
|
+
if (minutes === 0)
|
|
17
|
+
return `${seconds}s`;
|
|
18
|
+
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
19
|
+
}
|
|
20
|
+
function createStatusCommand() {
|
|
21
|
+
return new Command('status')
|
|
22
|
+
.description('Check render status')
|
|
23
|
+
.argument('<id>', 'Render job ID')
|
|
24
|
+
.option('--download [dir]', 'Download completed render to a directory', false)
|
|
25
|
+
.action(async (id, options) => {
|
|
26
|
+
if (!isAuthenticated()) {
|
|
27
|
+
console.log(chalk.yellow('⚠️ Not authenticated'));
|
|
28
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const renderId = Number.parseInt(id, 10);
|
|
32
|
+
if (Number.isNaN(renderId)) {
|
|
33
|
+
console.log(chalk.red('❌ Invalid render ID'));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const status = await getRenderStatus(renderId);
|
|
38
|
+
console.log(chalk.cyan('\\n📹 Render Status\\n'));
|
|
39
|
+
console.log(` ID: ${status.id}`);
|
|
40
|
+
console.log(` Status: ${status.status}`);
|
|
41
|
+
console.log(` Progress: ${status.progress}%`);
|
|
42
|
+
if (status.clickToDownloadMs !== null) {
|
|
43
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
44
|
+
}
|
|
45
|
+
if (status.outputUrl) {
|
|
46
|
+
console.log(` Output: ${status.outputUrl}`);
|
|
47
|
+
}
|
|
48
|
+
if (status.downloadUrl) {
|
|
49
|
+
console.log(` Download: ${status.downloadUrl}`);
|
|
50
|
+
}
|
|
51
|
+
if (options.download) {
|
|
52
|
+
if (status.status !== 'completed') {
|
|
53
|
+
throw new Error('Render is not completed yet');
|
|
54
|
+
}
|
|
55
|
+
const outputDir = typeof options.download === 'string' ? options.download : 'renders';
|
|
56
|
+
const downloaded = await downloadRender(renderId);
|
|
57
|
+
mkdirSync(resolve(outputDir), { recursive: true });
|
|
58
|
+
const outputPath = join(resolve(outputDir), downloaded.filename);
|
|
59
|
+
writeFileSync(outputPath, downloaded.buffer);
|
|
60
|
+
console.log(` Saved: ${outputPath}`);
|
|
61
|
+
}
|
|
62
|
+
if (status.error) {
|
|
63
|
+
console.log(chalk.red(` Error: ${status.error}`));
|
|
64
|
+
}
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export const statusCommand = createStatusCommand();
|
|
74
|
+
export const renderCommand = new Command('render')
|
|
75
|
+
.description('Submit a VidScript (.vid) or @scenerok/sdk (.ts/.js) file for rendering')
|
|
76
|
+
.argument('<file>', 'Path to .vid, .vidscript, .ts, or .js file')
|
|
77
|
+
.option('-r, --resolution <resolution>', 'Output resolution (e.g. 1080x1920)')
|
|
78
|
+
.option('-p, --project-id <id>', 'Attach render to an uploaded project ID')
|
|
79
|
+
.option('-w, --watch', 'Watch render status until complete')
|
|
80
|
+
.option('--download [dir]', 'Download completed render to a directory', false)
|
|
81
|
+
.option('--validate [cost]', `After rendering, validate the output MP4 with ffprobe + LLM frame review (billed). Cost tier: ${LINT_COST_CHOICES.join(', ')} (default medium). Implies --watch.`, false)
|
|
82
|
+
.option('--validation-budget <usd>', 'Hard USD cap on the MP4 LLM review when using --validate')
|
|
83
|
+
.action(async (file, options) => {
|
|
84
|
+
if (!isAuthenticated()) {
|
|
85
|
+
console.log(chalk.yellow('⚠️ Not authenticated'));
|
|
86
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const language = detectAuthoringLanguage(file);
|
|
90
|
+
let vidscript;
|
|
91
|
+
let preparedProject = null;
|
|
92
|
+
if (language !== 'vidscript') {
|
|
93
|
+
try {
|
|
94
|
+
vidscript = readFileSync(file, 'utf-8');
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
console.log(chalk.red(`❌ Could not read file: ${file}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
try {
|
|
103
|
+
preparedProject = prepareProject(file);
|
|
104
|
+
vidscript = preparedProject.rewrittenVidscript;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
try {
|
|
108
|
+
vidscript = readFileSync(file, 'utf-8');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
console.log(chalk.red(`❌ Could not read file: ${file}`));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (preparedProject && preparedProject.assets.length > 0) {
|
|
117
|
+
const assetSpinner = ora(`Uploading ${preparedProject.assets.length} local asset${preparedProject.assets.length === 1 ? '' : 's'}...`).start();
|
|
118
|
+
try {
|
|
119
|
+
const formData = new FormData();
|
|
120
|
+
for (const asset of preparedProject.assets) {
|
|
121
|
+
const bytes = readFileSync(asset.absolutePath);
|
|
122
|
+
const blob = new Blob([new Uint8Array(bytes)], { type: getMimeType(asset.absolutePath) });
|
|
123
|
+
formData.append('asset', blob, basename(asset.absolutePath));
|
|
124
|
+
formData.append('assetPath', asset.localPath);
|
|
125
|
+
}
|
|
126
|
+
const uploaded = await uploadCliAssets(formData);
|
|
127
|
+
const mappings = uploaded.assets.map((asset, index) => ({
|
|
128
|
+
localPath: preparedProject.assets[index].localPath,
|
|
129
|
+
id: asset.id,
|
|
130
|
+
url: asset.url,
|
|
131
|
+
mimeType: asset.mimeType,
|
|
132
|
+
fileSize: asset.fileSize,
|
|
133
|
+
}));
|
|
134
|
+
vidscript = rewritePreparedVidscriptWithAssetUrls(vidscript, mappings);
|
|
135
|
+
const manifestPath = writeAssetManifest(preparedProject, mappings);
|
|
136
|
+
assetSpinner.succeed(`Uploaded ${uploaded.assets.length} asset${uploaded.assets.length === 1 ? '' : 's'}`);
|
|
137
|
+
console.log(chalk.dim(` Manifest: ${manifestPath}`));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
assetSpinner.fail('Asset upload failed');
|
|
141
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (preparedProject) {
|
|
146
|
+
vidscript = preparedProject.originalVidscript;
|
|
147
|
+
}
|
|
148
|
+
if (vidscript.includes('__SCENEROK_ASSET_')) {
|
|
149
|
+
console.log(chalk.red('❌ Some local assets could not be uploaded or mapped.'));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const workingDirectory = preparedProject?.rootDir ?? dirname(resolve(file));
|
|
153
|
+
const moduleMap = language === 'vidscript'
|
|
154
|
+
? buildSdkSceneModuleMap(preparedProject?.originalVidscript ?? vidscript, workingDirectory)
|
|
155
|
+
: undefined;
|
|
156
|
+
const moduleOptions = moduleMap && Object.keys(moduleMap).length > 0
|
|
157
|
+
? { moduleMap, workingDirectory }
|
|
158
|
+
: { workingDirectory };
|
|
159
|
+
const spinner = ora('Submitting render job...').start();
|
|
160
|
+
try {
|
|
161
|
+
const projectId = options.projectId ? Number.parseInt(options.projectId, 10) : undefined;
|
|
162
|
+
if (options.projectId && Number.isNaN(projectId)) {
|
|
163
|
+
throw new Error('Invalid project ID');
|
|
164
|
+
}
|
|
165
|
+
const result = await submitRender(vidscript, options.resolution, projectId, { language, ...moduleOptions });
|
|
166
|
+
spinner.succeed(`Render job submitted (#${result.renderId})`);
|
|
167
|
+
console.log(chalk.cyan('\\n📹 Render Job\\n'));
|
|
168
|
+
console.log(` ID: ${result.renderId}`);
|
|
169
|
+
console.log(` Status: ${result.status}`);
|
|
170
|
+
console.log(` Resolution: ${result.resolution}`);
|
|
171
|
+
console.log(` Output: ${result.outputFilename}`);
|
|
172
|
+
console.log(` Mode: ${result.dispatchMode}`);
|
|
173
|
+
console.log(` Render URL: ${result.renderUrl}`);
|
|
174
|
+
console.log(` Download: ${result.downloadUrl}`);
|
|
175
|
+
if (result.logUrl) {
|
|
176
|
+
console.log(` Log: ${result.logUrl}`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
179
|
+
const validateCost = typeof options.validate === 'string' ? options.validate : options.validate ? 'medium' : undefined;
|
|
180
|
+
if (validateCost && !LINT_COST_CHOICES.includes(validateCost)) {
|
|
181
|
+
console.log(chalk.red(`❌ Invalid --validate cost "${validateCost}" — use ${LINT_COST_CHOICES.join(', ')}`));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
let validationBudgetUsd;
|
|
185
|
+
if (options.validationBudget !== undefined) {
|
|
186
|
+
validationBudgetUsd = Number.parseFloat(options.validationBudget);
|
|
187
|
+
if (Number.isNaN(validationBudgetUsd) || validationBudgetUsd <= 0) {
|
|
188
|
+
console.log(chalk.red('❌ --validation-budget must be a positive USD amount'));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (options.watch || options.download || validateCost) {
|
|
193
|
+
console.log(chalk.dim('Watching render status...\\n'));
|
|
194
|
+
while (true) {
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
196
|
+
const status = await getRenderStatus(result.renderId);
|
|
197
|
+
if (status.status === 'completed') {
|
|
198
|
+
console.log(chalk.green(`\\n✅ Render complete!\\n`));
|
|
199
|
+
if (status.clickToDownloadMs !== null) {
|
|
200
|
+
console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
|
|
201
|
+
}
|
|
202
|
+
console.log(` Download: ${status.downloadUrl}`);
|
|
203
|
+
if (options.download) {
|
|
204
|
+
const outputDir = typeof options.download === 'string' ? options.download : 'renders';
|
|
205
|
+
const downloaded = await downloadRender(result.renderId);
|
|
206
|
+
mkdirSync(resolve(outputDir), { recursive: true });
|
|
207
|
+
const outputPath = join(resolve(outputDir), downloaded.filename);
|
|
208
|
+
writeFileSync(outputPath, downloaded.buffer);
|
|
209
|
+
console.log(` Saved: ${outputPath}`);
|
|
210
|
+
}
|
|
211
|
+
console.log('');
|
|
212
|
+
if (validateCost) {
|
|
213
|
+
const passed = await runVideoLint(result.renderId, {
|
|
214
|
+
cost: validateCost,
|
|
215
|
+
budgetUsd: validationBudgetUsd,
|
|
216
|
+
});
|
|
217
|
+
if (!passed)
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
if (status.status === 'failed') {
|
|
223
|
+
console.log(chalk.red(`\\n❌ Render failed: ${status.error}\\n`));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
process.stdout.write(`\r ${status.status}... ${status.progress}%`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(chalk.dim(`Check status: scenerok status ${result.renderId}`));
|
|
231
|
+
console.log(chalk.dim(`Open render: ${result.renderUrl}`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
spinner.fail('Render submission failed');
|
|
236
|
+
if (error instanceof Error) {
|
|
237
|
+
console.error(chalk.red(error.message));
|
|
238
|
+
}
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
renderCommand.addCommand(createStatusCommand());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgGpC,eAAO,MAAM,aAAa,SAuKvB,CAAC"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { existsSync, copyFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { checkSkills, fetchManifest, fetchSkillFile, slugToFilename, } from '../utils/skillsManifest.js';
|
|
7
|
+
function getPlatformConfig(platform) {
|
|
8
|
+
const configs = {
|
|
9
|
+
opencode: {
|
|
10
|
+
name: 'OpenCode',
|
|
11
|
+
skillDir: join(homedir(), '.agents/skills/scenerok'),
|
|
12
|
+
},
|
|
13
|
+
claude: {
|
|
14
|
+
name: 'Claude Code',
|
|
15
|
+
skillDir: join(homedir(), '.claude/skills/scenerok'),
|
|
16
|
+
},
|
|
17
|
+
codex: {
|
|
18
|
+
name: 'Codex',
|
|
19
|
+
skillDir: join(homedir(), '.codex/skills/scenerok'),
|
|
20
|
+
},
|
|
21
|
+
cursor: {
|
|
22
|
+
name: 'Cursor',
|
|
23
|
+
skillDir: join(homedir(), '.cursor/skills/scenerok'),
|
|
24
|
+
},
|
|
25
|
+
aider: {
|
|
26
|
+
name: 'Aider',
|
|
27
|
+
skillDir: join(homedir(), '.aider/skills/scenerok'),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
return configs[platform] || null;
|
|
31
|
+
}
|
|
32
|
+
const ROUTER_SKILL = { source: 'skills/shared/SKILL.md', dest: 'SKILL.md' };
|
|
33
|
+
async function findSkillSource(sourcePath) {
|
|
34
|
+
const modulePath = join(dirname(new URL(import.meta.url).pathname), '../..', sourcePath);
|
|
35
|
+
if (existsSync(modulePath))
|
|
36
|
+
return modulePath;
|
|
37
|
+
const cwdPath = join(process.cwd(), sourcePath);
|
|
38
|
+
if (existsSync(cwdPath))
|
|
39
|
+
return cwdPath;
|
|
40
|
+
try {
|
|
41
|
+
const { execSync } = await import('node:child_process');
|
|
42
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
|
|
43
|
+
const globalPath = join(globalRoot, '@scenerok', 'cli', sourcePath);
|
|
44
|
+
if (existsSync(globalPath))
|
|
45
|
+
return globalPath;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function printSkillDiff(skills) {
|
|
53
|
+
for (const s of skills) {
|
|
54
|
+
const icon = s.status === 'current' ? chalk.green('✓') :
|
|
55
|
+
s.status === 'outdated' ? chalk.yellow('↻') :
|
|
56
|
+
chalk.red('✗');
|
|
57
|
+
console.log(` ${icon} ${s.filename} (${s.status})`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function syncSkills(skillDir, slugs) {
|
|
61
|
+
const manifest = await fetchManifest();
|
|
62
|
+
const toSync = slugs ?? Object.keys(manifest.skills);
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const slug of toSync.sort()) {
|
|
65
|
+
const content = await fetchSkillFile(slug);
|
|
66
|
+
const dest = join(skillDir, slugToFilename(slug));
|
|
67
|
+
writeFileSync(dest, content, 'utf8');
|
|
68
|
+
console.log(chalk.green(` ✓ ${slugToFilename(slug)}`));
|
|
69
|
+
count++;
|
|
70
|
+
}
|
|
71
|
+
return count;
|
|
72
|
+
}
|
|
73
|
+
export const skillsCommand = new Command('skills')
|
|
74
|
+
.description('Install and sync SceneRok agent skills')
|
|
75
|
+
.addCommand(new Command('install')
|
|
76
|
+
.description('Install the router skill for a platform')
|
|
77
|
+
.argument('<platform>', 'Agent platform: opencode, claude, codex, cursor, aider')
|
|
78
|
+
.option('--sync', 'Also download the full remote skill bundle')
|
|
79
|
+
.action(async (platform, opts) => {
|
|
80
|
+
const config = getPlatformConfig(platform);
|
|
81
|
+
if (!config) {
|
|
82
|
+
console.log(chalk.red(`❌ Unknown platform: ${platform}`));
|
|
83
|
+
console.log(chalk.dim('Supported platforms: opencode, claude, codex, cursor, aider'));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
console.log(chalk.cyan(`\n📦 Installing SceneRok router skill for ${config.name}...\n`));
|
|
87
|
+
if (!existsSync(config.skillDir)) {
|
|
88
|
+
mkdirSync(config.skillDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
const source = await findSkillSource(ROUTER_SKILL.source);
|
|
91
|
+
const dest = join(config.skillDir, ROUTER_SKILL.dest);
|
|
92
|
+
if (!source) {
|
|
93
|
+
console.log(chalk.red(` ✗ Router skill not found (${ROUTER_SKILL.source})`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
copyFileSync(source, dest);
|
|
97
|
+
console.log(chalk.green(` ✓ ${ROUTER_SKILL.dest} (router)`));
|
|
98
|
+
if (opts.sync) {
|
|
99
|
+
console.log(chalk.cyan('\n⬇️ Syncing remote skill bundle...\n'));
|
|
100
|
+
await syncSkills(config.skillDir);
|
|
101
|
+
}
|
|
102
|
+
console.log(chalk.cyan(`\n✅ Router skill installed to ${config.skillDir}\n`));
|
|
103
|
+
console.log(chalk.dim('Agents fetch latest docs from https://scenerok.com/.well-known/agent-skills/'));
|
|
104
|
+
console.log(chalk.dim('Offline fallback: scenerok skills sync'));
|
|
105
|
+
}))
|
|
106
|
+
.addCommand(new Command('sync')
|
|
107
|
+
.description('Download the full remote skill bundle locally')
|
|
108
|
+
.option('--dir <path>', 'Skill directory (auto-detected if omitted)')
|
|
109
|
+
.option('--platform <platform>', 'Platform to resolve skill dir')
|
|
110
|
+
.action(async (opts) => {
|
|
111
|
+
const skillDir = opts.dir ??
|
|
112
|
+
(opts.platform ? getPlatformConfig(opts.platform)?.skillDir : undefined) ??
|
|
113
|
+
(await import('../utils/skillsManifest.js')).locateSkillDir();
|
|
114
|
+
if (!skillDir) {
|
|
115
|
+
console.log(chalk.red('❌ No skill directory found. Run: scenerok skills install <platform>'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (!existsSync(skillDir))
|
|
119
|
+
mkdirSync(skillDir, { recursive: true });
|
|
120
|
+
console.log(chalk.cyan(`\n⬇️ Syncing remote skills to ${skillDir}...\n`));
|
|
121
|
+
const count = await syncSkills(skillDir);
|
|
122
|
+
console.log(chalk.cyan(`\n✅ Synced ${count} files\n`));
|
|
123
|
+
}))
|
|
124
|
+
.addCommand(new Command('check')
|
|
125
|
+
.description('Check whether local skills match the remote manifest')
|
|
126
|
+
.option('--json', 'Machine-readable output')
|
|
127
|
+
.option('--dir <path>', 'Skill directory to check')
|
|
128
|
+
.action(async (opts) => {
|
|
129
|
+
try {
|
|
130
|
+
const result = await checkSkills({ dir: opts.dir });
|
|
131
|
+
if (opts.json) {
|
|
132
|
+
console.log(JSON.stringify(result, null, 2));
|
|
133
|
+
process.exit(result.updateAvailable ? 1 : 0);
|
|
134
|
+
}
|
|
135
|
+
console.log(chalk.cyan('\n📋 SceneRok skills check\n'));
|
|
136
|
+
if (result.location) {
|
|
137
|
+
console.log(chalk.dim(`Location: ${result.location}\n`));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log(chalk.yellow('No local install found. Run: scenerok skills install <platform>\n'));
|
|
141
|
+
}
|
|
142
|
+
printSkillDiff(result.skills);
|
|
143
|
+
const { current, outdated, missing } = result.summary;
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(chalk.dim(`${current} current · ${outdated} outdated · ${missing} missing`));
|
|
146
|
+
if (result.updateAvailable) {
|
|
147
|
+
console.log(chalk.yellow('\nUpdate available: scenerok skills update\n'));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
console.log(chalk.green('\n✅ Skills are up to date\n'));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.log(chalk.red(`❌ ${err.message}`));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}))
|
|
157
|
+
.addCommand(new Command('update')
|
|
158
|
+
.description('Refresh outdated or missing local skill files')
|
|
159
|
+
.option('--dir <path>', 'Skill directory')
|
|
160
|
+
.action(async (opts) => {
|
|
161
|
+
const { locateSkillDir } = await import('../utils/skillsManifest.js');
|
|
162
|
+
const skillDir = opts.dir ?? locateSkillDir();
|
|
163
|
+
if (!skillDir) {
|
|
164
|
+
console.log(chalk.red('❌ No skill directory found. Run: scenerok skills install <platform>'));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const result = await checkSkills({ dir: skillDir });
|
|
168
|
+
const toUpdate = result.skills
|
|
169
|
+
.filter((s) => s.status !== 'current')
|
|
170
|
+
.map((s) => s.name);
|
|
171
|
+
if (toUpdate.length === 0) {
|
|
172
|
+
console.log(chalk.green('\n✅ Skills already up to date\n'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log(chalk.cyan(`\n⬇️ Updating ${toUpdate.length} skill file(s)...\n`));
|
|
176
|
+
await syncSkills(skillDir, toUpdate);
|
|
177
|
+
console.log(chalk.cyan('\n✅ Update complete\n'));
|
|
178
|
+
}))
|
|
179
|
+
.addCommand(new Command('list')
|
|
180
|
+
.description('List remote skills and local install status')
|
|
181
|
+
.option('--dir <path>', 'Skill directory')
|
|
182
|
+
.action(async (opts) => {
|
|
183
|
+
try {
|
|
184
|
+
const manifest = await fetchManifest();
|
|
185
|
+
const result = await checkSkills({ dir: opts.dir });
|
|
186
|
+
console.log(chalk.cyan('\n📦 SceneRok remote skills\n'));
|
|
187
|
+
printSkillDiff(result.skills);
|
|
188
|
+
console.log(chalk.dim(`\nManifest: ${Object.keys(manifest.skills).length} skills · source: ${manifest.source}`));
|
|
189
|
+
console.log(chalk.dim('Install router: scenerok skills install <platform>'));
|
|
190
|
+
console.log(chalk.dim('Sync all: scenerok skills sync\n'));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.log(chalk.red(`❌ ${err.message}`));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}))
|
|
197
|
+
.addCommand(new Command('platforms')
|
|
198
|
+
.description('List supported agent platforms')
|
|
199
|
+
.action(() => {
|
|
200
|
+
console.log(chalk.cyan('\n📦 Supported SceneRok skill platforms\n'));
|
|
201
|
+
console.log(' opencode - OpenCode agent (.agents/skills/)');
|
|
202
|
+
console.log(' claude - Claude Code (.claude/skills/)');
|
|
203
|
+
console.log(' codex - OpenAI Codex (.codex/skills/)');
|
|
204
|
+
console.log(' cursor - Cursor editor (.cursor/skills/)');
|
|
205
|
+
console.log(' aider - Aider (.aider/skills/)');
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(chalk.dim('Install: scenerok skills install <platform> [--sync]'));
|
|
208
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4HpC,eAAO,MAAM,eAAe,SAoLxB,CAAC"}
|