@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
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { getBaseUrl, getApiToken } from './config.js';
|
|
3
|
+
import { logDebug, logError } from './logger.js';
|
|
4
|
+
class ApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
responseBody;
|
|
7
|
+
constructor(message, status, responseBody) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.responseBody = responseBody;
|
|
11
|
+
this.name = 'ApiError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function getAuthHeaders(contentType = 'application/json') {
|
|
15
|
+
const token = getApiToken();
|
|
16
|
+
const headers = {};
|
|
17
|
+
if (contentType) {
|
|
18
|
+
headers['Content-Type'] = contentType;
|
|
19
|
+
}
|
|
20
|
+
if (token) {
|
|
21
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
22
|
+
}
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
async function apiCall(method, path, body) {
|
|
26
|
+
const baseUrl = getBaseUrl();
|
|
27
|
+
const url = `${baseUrl}${path}`;
|
|
28
|
+
const started = Date.now();
|
|
29
|
+
logDebug('API request', {
|
|
30
|
+
method,
|
|
31
|
+
url,
|
|
32
|
+
hasBody: body != null,
|
|
33
|
+
});
|
|
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;
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
logError('API error response', {
|
|
60
|
+
method,
|
|
61
|
+
url,
|
|
62
|
+
status: response.status,
|
|
63
|
+
durationMs,
|
|
64
|
+
body: data,
|
|
65
|
+
});
|
|
66
|
+
throw new ApiError(data.error || `HTTP ${response.status}`, response.status, data);
|
|
67
|
+
}
|
|
68
|
+
logDebug('API success', { method, url, status: response.status, durationMs });
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
async function apiFormCall(path, formData) {
|
|
72
|
+
const baseUrl = getBaseUrl();
|
|
73
|
+
const headers = getAuthHeaders('');
|
|
74
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers,
|
|
77
|
+
body: formData,
|
|
78
|
+
});
|
|
79
|
+
const data = await response.json().catch(() => ({}));
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new ApiError(data.error || `HTTP ${response.status}`, response.status, data);
|
|
82
|
+
}
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
export async function validateVidscript(vidscript, options = {}) {
|
|
86
|
+
return apiCall('POST', '/api/cli/validate', {
|
|
87
|
+
vidscript,
|
|
88
|
+
language: options.language || 'vidscript',
|
|
89
|
+
generateAssets: options.generateAssets || undefined,
|
|
90
|
+
moduleMap: options.moduleMap,
|
|
91
|
+
workingDirectory: options.workingDirectory,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export async function validateRender(renderId, options = {}) {
|
|
95
|
+
return apiCall('POST', '/api/cli/validate-render', {
|
|
96
|
+
renderId,
|
|
97
|
+
cost: options.cost,
|
|
98
|
+
frames: options.frames,
|
|
99
|
+
budgetUsd: options.budgetUsd,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export async function submitRender(vidscript, resolution, templateId, options = {}) {
|
|
103
|
+
return apiCall('POST', '/api/cli/render', {
|
|
104
|
+
vidscript,
|
|
105
|
+
resolution,
|
|
106
|
+
templateId,
|
|
107
|
+
language: options.language || 'vidscript',
|
|
108
|
+
moduleMap: options.moduleMap,
|
|
109
|
+
workingDirectory: options.workingDirectory,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export async function getRenderStatus(renderId) {
|
|
113
|
+
return apiCall('GET', `/api/cli/status?id=${renderId}`);
|
|
114
|
+
}
|
|
115
|
+
export async function downloadRender(renderId) {
|
|
116
|
+
const baseUrl = getBaseUrl();
|
|
117
|
+
const response = await fetch(`${baseUrl}/api/cli/render/download?id=${renderId}`, {
|
|
118
|
+
method: 'GET',
|
|
119
|
+
headers: getAuthHeaders(''),
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
const data = await response.json().catch(() => ({}));
|
|
123
|
+
throw new ApiError(data.error || `HTTP ${response.status}`, response.status, data);
|
|
124
|
+
}
|
|
125
|
+
const disposition = response.headers.get('content-disposition') || '';
|
|
126
|
+
const filenameMatch = disposition.match(/filename="([^"]+)"/);
|
|
127
|
+
const filename = filenameMatch?.[1] || `${renderId}.mp4`;
|
|
128
|
+
return {
|
|
129
|
+
buffer: Buffer.from(await response.arrayBuffer()),
|
|
130
|
+
filename,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export async function uploadProject(formData) {
|
|
134
|
+
return apiFormCall('/api/cli/projects', formData);
|
|
135
|
+
}
|
|
136
|
+
export async function uploadCliAssets(formData) {
|
|
137
|
+
return apiFormCall('/api/cli/assets', formData);
|
|
138
|
+
}
|
|
139
|
+
export async function listProjects() {
|
|
140
|
+
return apiCall('GET', '/api/cli/projects');
|
|
141
|
+
}
|
|
142
|
+
export async function getAssetCache(options = {}) {
|
|
143
|
+
const params = new URLSearchParams();
|
|
144
|
+
if (options.plugin)
|
|
145
|
+
params.set('plugin', options.plugin);
|
|
146
|
+
if (options.limit)
|
|
147
|
+
params.set('limit', String(options.limit));
|
|
148
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
149
|
+
return apiCall('GET', `/api/cli/asset-cache${suffix}`);
|
|
150
|
+
}
|
|
151
|
+
export async function checkAssetCache(cacheKeys) {
|
|
152
|
+
return apiCall('POST', '/api/cli/asset-cache/check', { cacheKeys });
|
|
153
|
+
}
|
|
154
|
+
export async function initiateDeviceAuth(deviceName) {
|
|
155
|
+
return apiCall('POST', '/api/cli/auth/device', { deviceName });
|
|
156
|
+
}
|
|
157
|
+
export async function pollDeviceAuth(deviceCode) {
|
|
158
|
+
return apiCall('GET', `/api/cli/auth/device?device_code=${deviceCode}`);
|
|
159
|
+
}
|
|
160
|
+
export { ApiError };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CliConfig {
|
|
2
|
+
apiToken?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
defaultResolution?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function readConfig(): CliConfig;
|
|
7
|
+
export declare function writeConfig(config: CliConfig): void;
|
|
8
|
+
export declare function getBaseUrl(): string;
|
|
9
|
+
export declare function getApiToken(): string | undefined;
|
|
10
|
+
export declare function isAuthenticated(): boolean;
|
|
11
|
+
export declare function getConfigDir(): string;
|
|
12
|
+
export declare function getCacheDir(): string;
|
|
13
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAQD,wBAAgB,UAAU,IAAI,SAAS,CAUtC;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,QAG5C;AAOD,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAED,wBAAgB,WAAW,IAAI,MAAM,GAAG,SAAS,CAGhD;AAED,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,WAAW,IAAI,MAAM,CAKpC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.scenerok');
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
6
|
+
const CACHE_DIR = join(CONFIG_DIR, 'cache');
|
|
7
|
+
const PRODUCTION_API_ORIGIN = ['https://', 'scenerok', '.com'].join(''); // pragma: allowlist secret
|
|
8
|
+
function ensureConfigDir() {
|
|
9
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
10
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function readConfig() {
|
|
14
|
+
ensureConfigDir();
|
|
15
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function writeConfig(config) {
|
|
26
|
+
ensureConfigDir();
|
|
27
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
28
|
+
}
|
|
29
|
+
function cliBaseUrlFromEnv() {
|
|
30
|
+
const key = ['SCENEROK', '_', 'BASE', '_', 'URL'].join('');
|
|
31
|
+
return process.env[key]?.replace(/\/$/, '');
|
|
32
|
+
}
|
|
33
|
+
export function getBaseUrl() {
|
|
34
|
+
const config = readConfig();
|
|
35
|
+
return config.baseUrl?.replace(/\/$/, '') || cliBaseUrlFromEnv() || PRODUCTION_API_ORIGIN;
|
|
36
|
+
}
|
|
37
|
+
export function getApiToken() {
|
|
38
|
+
const config = readConfig();
|
|
39
|
+
return config.apiToken;
|
|
40
|
+
}
|
|
41
|
+
export function isAuthenticated() {
|
|
42
|
+
return !!getApiToken();
|
|
43
|
+
}
|
|
44
|
+
export function getConfigDir() {
|
|
45
|
+
ensureConfigDir();
|
|
46
|
+
return CONFIG_DIR;
|
|
47
|
+
}
|
|
48
|
+
export function getCacheDir() {
|
|
49
|
+
if (!existsSync(CACHE_DIR)) {
|
|
50
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
return CACHE_DIR;
|
|
53
|
+
}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"language.d.ts","sourceRoot":"","sources":["../../src/lib/language.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;AAE1E,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAK3E"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
export function detectAuthoringLanguage(filePath) {
|
|
3
|
+
const ext = extname(filePath).toLowerCase();
|
|
4
|
+
if (ext === '.ts' || ext === '.tsx')
|
|
5
|
+
return 'typescript';
|
|
6
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs')
|
|
7
|
+
return 'javascript';
|
|
8
|
+
return 'vidscript';
|
|
9
|
+
}
|
|
@@ -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,29 @@
|
|
|
1
|
+
export interface PreparedProject {
|
|
2
|
+
rootDir: string;
|
|
3
|
+
entryFile: string;
|
|
4
|
+
originalVidscript: string;
|
|
5
|
+
rewrittenVidscript: string;
|
|
6
|
+
assets: Array<{
|
|
7
|
+
localPath: string;
|
|
8
|
+
absolutePath: string;
|
|
9
|
+
size: number;
|
|
10
|
+
sha256: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export declare function getMimeType(filePath: string): string;
|
|
14
|
+
export declare function listAssetFiles(assetDir: string): string[];
|
|
15
|
+
export declare function prepareProject(entryFile: string, assetDirs?: string[]): PreparedProject;
|
|
16
|
+
export declare function appendProjectToForm(formData: FormData, project: PreparedProject): Promise<void>;
|
|
17
|
+
export declare function buildAssetToken(localPath: string): string;
|
|
18
|
+
export declare function rewritePreparedVidscriptWithAssetUrls(vidscript: string, mappings: Array<{
|
|
19
|
+
localPath: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}>): string;
|
|
22
|
+
export declare function writeAssetManifest(project: PreparedProject, mappings: Array<{
|
|
23
|
+
localPath: string;
|
|
24
|
+
url: string;
|
|
25
|
+
id?: number;
|
|
26
|
+
mimeType?: string | null;
|
|
27
|
+
fileSize?: number;
|
|
28
|
+
}>): string;
|
|
29
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/lib/project.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,KAAK,CAAC;QACZ,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;CACJ;AAcD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAepD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAoBzD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,GAAG,eAAe,CAiE3F;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrG;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,qCAAqC,CACnD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAClD,MAAM,CAMR;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAC5G,MAAM,CAwBR"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
const INPUT_RE = /^(\s*input\s+[A-Za-z_][A-Za-z0-9_]*\s*=\s*)(["'])([^"']+)(\2)/gm;
|
|
5
|
+
const ASSET_IMPORT_RE = /^(\s*)import\s+([A-Za-z_][A-Za-z0-9_]*)\s+from\s+(["'])([^"']+)\3\s*;?\s*$/gm;
|
|
6
|
+
const HTTP_RE = /^https?:\/\//i;
|
|
7
|
+
const MEDIA_EXT_RE = /\.(mp4|mov|m4v|webm|mp3|wav|m4a|aac|ogg|png|jpe?g|webp|gif|avif|svg)(?:[?#].*)?$/i;
|
|
8
|
+
function isRenderableLocalPath(value) {
|
|
9
|
+
return Boolean(value)
|
|
10
|
+
&& !HTTP_RE.test(value)
|
|
11
|
+
&& !value.startsWith('scenerok://')
|
|
12
|
+
&& !value.startsWith('/uploads/')
|
|
13
|
+
&& !value.startsWith('data:');
|
|
14
|
+
}
|
|
15
|
+
function isAssetImportPath(value) {
|
|
16
|
+
return isRenderableLocalPath(value) && MEDIA_EXT_RE.test(value);
|
|
17
|
+
}
|
|
18
|
+
export function getMimeType(filePath) {
|
|
19
|
+
const ext = filePath.toLowerCase().split('.').pop();
|
|
20
|
+
switch (ext) {
|
|
21
|
+
case 'mp4': return 'video/mp4';
|
|
22
|
+
case 'mov': return 'video/quicktime';
|
|
23
|
+
case 'webm': return 'video/webm';
|
|
24
|
+
case 'mp3': return 'audio/mpeg';
|
|
25
|
+
case 'wav': return 'audio/wav';
|
|
26
|
+
case 'png': return 'image/png';
|
|
27
|
+
case 'jpg':
|
|
28
|
+
case 'jpeg': return 'image/jpeg';
|
|
29
|
+
case 'webp': return 'image/webp';
|
|
30
|
+
case 'gif': return 'image/gif';
|
|
31
|
+
default: return 'application/octet-stream';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function listAssetFiles(assetDir) {
|
|
35
|
+
const results = [];
|
|
36
|
+
function walk(dir) {
|
|
37
|
+
for (const entry of readdirSync(dir)) {
|
|
38
|
+
const absolute = join(dir, entry);
|
|
39
|
+
const stats = statSync(absolute);
|
|
40
|
+
if (stats.isDirectory()) {
|
|
41
|
+
walk(absolute);
|
|
42
|
+
}
|
|
43
|
+
else if (stats.isFile()) {
|
|
44
|
+
results.push(absolute);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (existsSync(assetDir)) {
|
|
49
|
+
walk(assetDir);
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
export function prepareProject(entryFile, assetDirs = []) {
|
|
54
|
+
const absoluteEntry = resolve(entryFile);
|
|
55
|
+
if (!existsSync(absoluteEntry)) {
|
|
56
|
+
throw new Error(`VidScript file not found: ${entryFile}`);
|
|
57
|
+
}
|
|
58
|
+
const rootDir = dirname(absoluteEntry);
|
|
59
|
+
const originalVidscript = readFileSync(absoluteEntry, 'utf-8');
|
|
60
|
+
const assetsByPath = new Map();
|
|
61
|
+
let rewrittenVidscript = originalVidscript.replace(INPUT_RE, (match, prefix, quote, rawPath, suffix) => {
|
|
62
|
+
if (!isRenderableLocalPath(rawPath)) {
|
|
63
|
+
return match;
|
|
64
|
+
}
|
|
65
|
+
const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(rootDir, rawPath);
|
|
66
|
+
if (!existsSync(absolutePath)) {
|
|
67
|
+
return match;
|
|
68
|
+
}
|
|
69
|
+
const localPath = relative(rootDir, absolutePath).replace(/\\/g, '/');
|
|
70
|
+
assetsByPath.set(localPath, absolutePath);
|
|
71
|
+
return `${prefix}${quote}{{asset:${localPath}}}${suffix}`;
|
|
72
|
+
});
|
|
73
|
+
rewrittenVidscript = rewrittenVidscript.replace(ASSET_IMPORT_RE, (match, indent, localName, _quote, rawPath) => {
|
|
74
|
+
if (!isAssetImportPath(rawPath)) {
|
|
75
|
+
return match;
|
|
76
|
+
}
|
|
77
|
+
const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(rootDir, rawPath);
|
|
78
|
+
if (!existsSync(absolutePath)) {
|
|
79
|
+
return match;
|
|
80
|
+
}
|
|
81
|
+
const localPath = relative(rootDir, absolutePath).replace(/\\/g, '/');
|
|
82
|
+
assetsByPath.set(localPath, absolutePath);
|
|
83
|
+
return `${indent}input ${localName} = "{{asset:${localPath}}}"`;
|
|
84
|
+
});
|
|
85
|
+
for (const dir of assetDirs) {
|
|
86
|
+
const absoluteDir = resolve(dir);
|
|
87
|
+
for (const absolutePath of listAssetFiles(absoluteDir)) {
|
|
88
|
+
const localPath = relative(rootDir, absolutePath).replace(/\\/g, '/');
|
|
89
|
+
assetsByPath.set(localPath, absolutePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const [localPath] of assetsByPath) {
|
|
93
|
+
const token = `{{asset:${localPath}}}`;
|
|
94
|
+
rewrittenVidscript = rewrittenVidscript.replaceAll(token, `__SCENEROK_ASSET_${Buffer.from(localPath).toString('base64url')}__`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
rootDir,
|
|
98
|
+
entryFile: basename(absoluteEntry),
|
|
99
|
+
originalVidscript,
|
|
100
|
+
rewrittenVidscript,
|
|
101
|
+
assets: Array.from(assetsByPath.entries()).map(([localPath, absolutePath]) => ({
|
|
102
|
+
localPath,
|
|
103
|
+
absolutePath,
|
|
104
|
+
size: statSync(absolutePath).size,
|
|
105
|
+
sha256: createHash('sha256').update(readFileSync(absolutePath)).digest('hex'),
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export async function appendProjectToForm(formData, project) {
|
|
110
|
+
formData.set('localRoot', project.rootDir);
|
|
111
|
+
formData.set('entryFile', project.entryFile);
|
|
112
|
+
formData.set('originalVidscript', project.originalVidscript);
|
|
113
|
+
let serverVidscript = project.rewrittenVidscript;
|
|
114
|
+
for (const asset of project.assets) {
|
|
115
|
+
const bytes = readFileSync(asset.absolutePath);
|
|
116
|
+
const blob = new Blob([new Uint8Array(bytes)], { type: getMimeType(asset.absolutePath) });
|
|
117
|
+
formData.append('asset', blob, basename(asset.absolutePath));
|
|
118
|
+
formData.append('assetPath', asset.localPath);
|
|
119
|
+
}
|
|
120
|
+
formData.set('vidscript', serverVidscript);
|
|
121
|
+
}
|
|
122
|
+
export function buildAssetToken(localPath) {
|
|
123
|
+
return `__SCENEROK_ASSET_${Buffer.from(localPath).toString('base64url')}__`;
|
|
124
|
+
}
|
|
125
|
+
export function rewritePreparedVidscriptWithAssetUrls(vidscript, mappings) {
|
|
126
|
+
let next = vidscript;
|
|
127
|
+
for (const mapping of mappings) {
|
|
128
|
+
next = next.replaceAll(buildAssetToken(mapping.localPath), mapping.url);
|
|
129
|
+
}
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
export function writeAssetManifest(project, mappings) {
|
|
133
|
+
const manifestDir = join(project.rootDir, '.scenerok');
|
|
134
|
+
mkdirSync(manifestDir, { recursive: true });
|
|
135
|
+
const manifestPath = join(manifestDir, 'assets.manifest.json');
|
|
136
|
+
const byLocalPath = new Map(project.assets.map((asset) => [asset.localPath, asset]));
|
|
137
|
+
const manifest = {
|
|
138
|
+
version: 1,
|
|
139
|
+
entryFile: project.entryFile,
|
|
140
|
+
generatedAt: new Date().toISOString(),
|
|
141
|
+
assets: mappings.map((mapping) => {
|
|
142
|
+
const localAsset = byLocalPath.get(mapping.localPath);
|
|
143
|
+
return {
|
|
144
|
+
localPath: mapping.localPath,
|
|
145
|
+
absolutePath: localAsset?.absolutePath,
|
|
146
|
+
size: localAsset?.size ?? mapping.fileSize,
|
|
147
|
+
sha256: localAsset?.sha256,
|
|
148
|
+
assetId: mapping.id,
|
|
149
|
+
mimeType: mapping.mimeType,
|
|
150
|
+
url: mapping.url,
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
155
|
+
return manifestPath;
|
|
156
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect companion `@scenerok/sdk` scene files referenced from VidScript
|
|
3
|
+
* so the CLI can upload them as a moduleMap for remote compile.
|
|
4
|
+
*/
|
|
5
|
+
export declare function buildSdkSceneModuleMap(vidscript: string, baseDir: string): Record<string, string>;
|
|
6
|
+
export declare function resolveEntryWorkingDirectory(filePath: string): string;
|
|
7
|
+
//# sourceMappingURL=sdk-scene-modules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-scene-modules.d.ts","sourceRoot":"","sources":["../../src/lib/sdk-scene-modules.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAeH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAexB;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect companion `@scenerok/sdk` scene files referenced from VidScript
|
|
3
|
+
* so the CLI can upload them as a moduleMap for remote compile.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
7
|
+
const SDK_SCENE_EXT_RE = /\.(tsx?|jsx?|mjs|cjs)$/i;
|
|
8
|
+
const SDK_IMPORT_RE = /^\s*import\s+[A-Za-z_][A-Za-z0-9_]*\s+from\s+(["'])([^"']+)\1\s*;?\s*$/gm;
|
|
9
|
+
function isSdkSceneSpecifier(specifier) {
|
|
10
|
+
if (!specifier || specifier.startsWith('@'))
|
|
11
|
+
return false;
|
|
12
|
+
const pathOnly = specifier.split('?')[0].split('#')[0];
|
|
13
|
+
return SDK_SCENE_EXT_RE.test(pathOnly);
|
|
14
|
+
}
|
|
15
|
+
export function buildSdkSceneModuleMap(vidscript, baseDir) {
|
|
16
|
+
const moduleMap = {};
|
|
17
|
+
SDK_IMPORT_RE.lastIndex = 0;
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = SDK_IMPORT_RE.exec(vidscript)) !== null) {
|
|
20
|
+
const specifier = match[2];
|
|
21
|
+
if (!isSdkSceneSpecifier(specifier))
|
|
22
|
+
continue;
|
|
23
|
+
if (!(specifier.startsWith('./') || specifier.startsWith('../') || isAbsolute(specifier))) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const absolute = isAbsolute(specifier) ? specifier : resolve(baseDir, specifier);
|
|
27
|
+
if (!existsSync(absolute))
|
|
28
|
+
continue;
|
|
29
|
+
moduleMap[specifier] = readFileSync(absolute, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
return moduleMap;
|
|
32
|
+
}
|
|
33
|
+
export function resolveEntryWorkingDirectory(filePath) {
|
|
34
|
+
return dirname(resolve(filePath));
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const DEFAULT_MANIFEST_URL = "https://scenerok.com/.well-known/agent-skills/manifest.json";
|
|
2
|
+
export interface SkillEntry {
|
|
3
|
+
hash: string;
|
|
4
|
+
files: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SkillsManifest {
|
|
7
|
+
source: string;
|
|
8
|
+
skills: Record<string, SkillEntry>;
|
|
9
|
+
}
|
|
10
|
+
export type SkillStatus = 'current' | 'outdated' | 'missing';
|
|
11
|
+
export interface SkillDiff {
|
|
12
|
+
name: string;
|
|
13
|
+
status: SkillStatus;
|
|
14
|
+
installedHash?: string;
|
|
15
|
+
latestHash?: string;
|
|
16
|
+
filename: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SkillsCheckResult {
|
|
19
|
+
location: string | null;
|
|
20
|
+
updateAvailable: boolean;
|
|
21
|
+
summary: {
|
|
22
|
+
current: number;
|
|
23
|
+
outdated: number;
|
|
24
|
+
missing: number;
|
|
25
|
+
};
|
|
26
|
+
skills: SkillDiff[];
|
|
27
|
+
}
|
|
28
|
+
/** Map manifest slug to on-disk filename. */
|
|
29
|
+
export declare function slugToFilename(slug: string): string;
|
|
30
|
+
export declare function hashInstalledFile(filePath: string): SkillEntry | null;
|
|
31
|
+
export declare function hashInstalled(skillDir: string, manifest: SkillsManifest): Record<string, SkillEntry>;
|
|
32
|
+
export declare function diffSkills(installed: Record<string, SkillEntry>, latest: SkillsManifest): Omit<SkillsCheckResult, 'location'>;
|
|
33
|
+
export declare function fetchManifest(url?: string): Promise<SkillsManifest>;
|
|
34
|
+
export declare function fetchSkillFile(slug: string, baseUrl?: string): Promise<string>;
|
|
35
|
+
export declare function locateSkillDir(platform?: string): string | null;
|
|
36
|
+
export declare function checkSkills(opts?: {
|
|
37
|
+
dir?: string;
|
|
38
|
+
manifestUrl?: string;
|
|
39
|
+
}): Promise<SkillsCheckResult>;
|
|
40
|
+
/** List extra local files not in manifest (legacy bundled files). */
|
|
41
|
+
export declare function listExtraLocalFiles(skillDir: string, manifest: SkillsManifest): string[];
|
|
42
|
+
//# sourceMappingURL=skillsManifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillsManifest.d.ts","sourceRoot":"","sources":["../../src/utils/skillsManifest.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,oBAAoB,gEAC8B,CAAC;AAEhE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AAWD,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAKrE;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAQpG;AAED,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,EACrC,MAAM,EAAE,cAAc,GACrB,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,CA8BrC;AAED,wBAAsB,aAAa,CAAC,GAAG,SAAuB,GAAG,OAAO,CAAC,cAAc,CAAC,CAQvF;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOpF;AAED,wBAAgB,cAAc,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiB/D;AAED,wBAAsB,WAAW,CAAC,IAAI,GAAE;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAMlC;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,EAAE,CAKxF"}
|