bazaar.it 0.1.0 → 0.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 +489 -3
- package/bin/baz.js +6 -1
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +109 -0
- package/dist/commands/capabilities.d.ts +2 -0
- package/dist/commands/capabilities.js +44 -0
- package/dist/commands/context.d.ts +13 -0
- package/dist/commands/context.js +498 -0
- package/dist/commands/export.d.ts +2 -0
- package/dist/commands/export.js +360 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +180 -0
- package/dist/commands/loop.d.ts +2 -0
- package/dist/commands/loop.js +538 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +143 -0
- package/dist/commands/media.d.ts +2 -0
- package/dist/commands/media.js +362 -0
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.js +786 -0
- package/dist/commands/prompt.d.ts +2 -0
- package/dist/commands/prompt.js +529 -0
- package/dist/commands/recipe.d.ts +15 -0
- package/dist/commands/recipe.js +607 -0
- package/dist/commands/review.d.ts +17 -0
- package/dist/commands/review.js +345 -0
- package/dist/commands/scenes.d.ts +2 -0
- package/dist/commands/scenes.js +481 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +226 -0
- package/dist/commands/state.d.ts +2 -0
- package/dist/commands/state.js +171 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +219 -0
- package/dist/commands/template.d.ts +2 -0
- package/dist/commands/template.js +123 -0
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.js +150 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +124 -0
- package/dist/lib/api.d.ts +186 -0
- package/dist/lib/api.js +717 -0
- package/dist/lib/banner.d.ts +12 -0
- package/dist/lib/banner.js +69 -0
- package/dist/lib/config.d.ts +33 -0
- package/dist/lib/config.js +99 -0
- package/dist/lib/output.d.ts +52 -0
- package/dist/lib/output.js +162 -0
- package/dist/lib/project-state.d.ts +52 -0
- package/dist/lib/project-state.js +178 -0
- package/dist/lib/sse.d.ts +168 -0
- package/dist/lib/sse.js +227 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +3 -0
- package/dist/repl.d.ts +4 -0
- package/dist/repl.js +764 -0
- package/package.json +39 -5
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display the startup banner
|
|
3
|
+
*/
|
|
4
|
+
export declare function showBanner(version: string, configInfo?: {
|
|
5
|
+
hasApiKey: boolean;
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
activeProject?: string;
|
|
8
|
+
}): void;
|
|
9
|
+
/**
|
|
10
|
+
* Show a compact banner for subcommands
|
|
11
|
+
*/
|
|
12
|
+
export declare function showCompactBanner(): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
// Big chunky ASCII art for "BAZ"
|
|
3
|
+
const BAZ_ASCII = `
|
|
4
|
+
██████╗ █████╗ ███████╗
|
|
5
|
+
██╔══██╗██╔══██╗╚══███╔╝
|
|
6
|
+
██████╔╝███████║ ███╔╝
|
|
7
|
+
██╔══██╗██╔══██║ ███╔╝
|
|
8
|
+
██████╔╝██║ ██║███████╗
|
|
9
|
+
╚═════╝ ╚═╝ ╚═╝╚══════╝
|
|
10
|
+
`.trim();
|
|
11
|
+
// Gradient colors (purple -> magenta -> cyan)
|
|
12
|
+
const gradientColors = [
|
|
13
|
+
'#a855f7', // purple
|
|
14
|
+
'#c084fc', // light purple
|
|
15
|
+
'#e879f9', // magenta
|
|
16
|
+
'#f0abfc', // pink
|
|
17
|
+
'#67e8f9', // cyan
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Apply gradient colors to each line of ASCII art
|
|
21
|
+
*/
|
|
22
|
+
function applyGradient(text) {
|
|
23
|
+
const lines = text.split('\n');
|
|
24
|
+
return lines.map((line, lineIndex) => {
|
|
25
|
+
// Color varies by line for vertical gradient effect
|
|
26
|
+
const colorIndex = Math.min(lineIndex, gradientColors.length - 1);
|
|
27
|
+
const color = gradientColors[colorIndex];
|
|
28
|
+
return chalk.hex(color)(line);
|
|
29
|
+
}).join('\n');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Display the startup banner
|
|
33
|
+
*/
|
|
34
|
+
export function showBanner(version, configInfo) {
|
|
35
|
+
console.log();
|
|
36
|
+
console.log(applyGradient(BAZ_ASCII));
|
|
37
|
+
console.log();
|
|
38
|
+
console.log(chalk.gray(' Bazaar CLI'), chalk.white(`v${version}`), chalk.gray('·'), chalk.gray('AI-Powered Video Generation'));
|
|
39
|
+
// Show config status if available
|
|
40
|
+
if (configInfo) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (configInfo.hasApiKey) {
|
|
43
|
+
parts.push(chalk.green('✓') + chalk.gray(' authenticated'));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
parts.push(chalk.yellow('○') + chalk.gray(' not authenticated'));
|
|
47
|
+
}
|
|
48
|
+
if (configInfo.activeProject) {
|
|
49
|
+
// Truncate long UUIDs to first 8 chars
|
|
50
|
+
const shortId = configInfo.activeProject.length > 12
|
|
51
|
+
? configInfo.activeProject.slice(0, 8)
|
|
52
|
+
: configInfo.activeProject;
|
|
53
|
+
parts.push(chalk.cyan('▸') + chalk.gray(` project: ${shortId}`));
|
|
54
|
+
}
|
|
55
|
+
if (configInfo.apiUrl !== 'https://bazaar.it') {
|
|
56
|
+
parts.push(chalk.gray(`→ ${configInfo.apiUrl}`));
|
|
57
|
+
}
|
|
58
|
+
if (parts.length > 0) {
|
|
59
|
+
console.log(chalk.gray(' ') + parts.join(chalk.gray(' | ')));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Show a compact banner for subcommands
|
|
66
|
+
*/
|
|
67
|
+
export function showCompactBanner() {
|
|
68
|
+
console.log(chalk.hex('#a855f7')('▸'), chalk.bold('baz'), chalk.gray('—'), chalk.gray('bazaar.it'));
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
activeProjectId?: string;
|
|
5
|
+
defaults?: {
|
|
6
|
+
mode?: 'agent' | 'agent-max' | 'multi-scene';
|
|
7
|
+
outputFormat?: 'pretty' | 'json';
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Load config from file, environment, and CLI options
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadConfig(options?: {
|
|
14
|
+
configPath?: string;
|
|
15
|
+
apiUrl?: string;
|
|
16
|
+
projectId?: string;
|
|
17
|
+
}): Config;
|
|
18
|
+
/**
|
|
19
|
+
* Save config to file
|
|
20
|
+
*/
|
|
21
|
+
export declare function saveConfig(updates: Partial<Config>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Get active project ID or throw if not set
|
|
24
|
+
*/
|
|
25
|
+
export declare function getProjectId(config: Config, override?: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Check if config has valid API key
|
|
28
|
+
*/
|
|
29
|
+
export declare function hasAuth(config: Config): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Get config file path for display
|
|
32
|
+
*/
|
|
33
|
+
export declare function getConfigPath(): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
apiUrl: 'https://bazaar.it',
|
|
6
|
+
defaults: {
|
|
7
|
+
mode: 'agent',
|
|
8
|
+
outputFormat: 'pretty',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.bazaar');
|
|
12
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
13
|
+
/**
|
|
14
|
+
* Load config from file, environment, and CLI options
|
|
15
|
+
*/
|
|
16
|
+
export function loadConfig(options) {
|
|
17
|
+
// Start with defaults
|
|
18
|
+
let config = { ...DEFAULT_CONFIG };
|
|
19
|
+
// Load from file
|
|
20
|
+
const configPath = options?.configPath || CONFIG_FILE;
|
|
21
|
+
if (existsSync(configPath)) {
|
|
22
|
+
try {
|
|
23
|
+
const fileContent = readFileSync(configPath, 'utf-8');
|
|
24
|
+
const fileConfig = JSON.parse(fileContent);
|
|
25
|
+
config = { ...config, ...fileConfig };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
// Ignore parse errors, use defaults
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Override with environment variables
|
|
32
|
+
if (process.env.BAZ_API_KEY) {
|
|
33
|
+
config.apiKey = process.env.BAZ_API_KEY;
|
|
34
|
+
}
|
|
35
|
+
if (process.env.BAZ_API_URL) {
|
|
36
|
+
config.apiUrl = process.env.BAZ_API_URL;
|
|
37
|
+
}
|
|
38
|
+
if (process.env.BAZ_PROJECT_ID) {
|
|
39
|
+
config.activeProjectId = process.env.BAZ_PROJECT_ID;
|
|
40
|
+
}
|
|
41
|
+
if (process.env.BAZ_JSON === '1') {
|
|
42
|
+
config.defaults = { ...config.defaults, outputFormat: 'json' };
|
|
43
|
+
}
|
|
44
|
+
// Override with CLI options
|
|
45
|
+
if (options?.apiUrl) {
|
|
46
|
+
config.apiUrl = options.apiUrl;
|
|
47
|
+
}
|
|
48
|
+
if (options?.projectId) {
|
|
49
|
+
config.activeProjectId = options.projectId;
|
|
50
|
+
}
|
|
51
|
+
return config;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save config to file
|
|
55
|
+
*/
|
|
56
|
+
export function saveConfig(updates) {
|
|
57
|
+
// Ensure directory exists
|
|
58
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
59
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
// Load existing config
|
|
62
|
+
let config = { ...DEFAULT_CONFIG };
|
|
63
|
+
if (existsSync(CONFIG_FILE)) {
|
|
64
|
+
try {
|
|
65
|
+
const fileContent = readFileSync(CONFIG_FILE, 'utf-8');
|
|
66
|
+
config = { ...config, ...JSON.parse(fileContent) };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Ignore
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Apply updates
|
|
73
|
+
config = { ...config, ...updates };
|
|
74
|
+
// Write back
|
|
75
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get active project ID or throw if not set
|
|
79
|
+
*/
|
|
80
|
+
export function getProjectId(config, override) {
|
|
81
|
+
const projectId = override || config.activeProjectId;
|
|
82
|
+
if (!projectId) {
|
|
83
|
+
throw new Error('No active project. Run: baz project use <id>\n' +
|
|
84
|
+
'Or pass --project-id <id>');
|
|
85
|
+
}
|
|
86
|
+
return projectId;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if config has valid API key
|
|
90
|
+
*/
|
|
91
|
+
export function hasAuth(config) {
|
|
92
|
+
return Boolean(config.apiKey);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get config file path for display
|
|
96
|
+
*/
|
|
97
|
+
export function getConfigPath() {
|
|
98
|
+
return CONFIG_FILE;
|
|
99
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface OutputOptions {
|
|
2
|
+
json?: boolean;
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
compact?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Output data in JSON or pretty format
|
|
8
|
+
* When compact=true, outputs minified JSON (saves tokens for agents)
|
|
9
|
+
*/
|
|
10
|
+
export declare function output(data: unknown, options: OutputOptions): void;
|
|
11
|
+
/**
|
|
12
|
+
* Output NDJSON line (for streaming)
|
|
13
|
+
* Always compact - no pretty printing for streams
|
|
14
|
+
*/
|
|
15
|
+
export declare function outputNDJSON(data: unknown): void;
|
|
16
|
+
/**
|
|
17
|
+
* Pretty print any data structure
|
|
18
|
+
*/
|
|
19
|
+
export declare function prettyPrint(data: unknown): void;
|
|
20
|
+
/**
|
|
21
|
+
* Print success message
|
|
22
|
+
*/
|
|
23
|
+
export declare function success(message: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Print error message
|
|
26
|
+
*/
|
|
27
|
+
export declare function error(message: string, suggestion?: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Print warning message
|
|
30
|
+
*/
|
|
31
|
+
export declare function warn(message: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Print info message
|
|
34
|
+
*/
|
|
35
|
+
export declare function info(message: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Print a table
|
|
38
|
+
*/
|
|
39
|
+
export declare function table(headers: string[], rows: string[][]): void;
|
|
40
|
+
/**
|
|
41
|
+
* Format duration in mm:ss
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatDuration(seconds: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Format relative time (e.g., "2 hours ago")
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Prompt the user for confirmation before a destructive action.
|
|
49
|
+
* In non-interactive (piped/agent) mode, returns false — callers must use --force.
|
|
50
|
+
*/
|
|
51
|
+
export declare function confirmAction(message: string): Promise<boolean>;
|
|
52
|
+
export declare function formatRelativeTime(date: Date | string): string;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
/**
|
|
4
|
+
* Output data in JSON or pretty format
|
|
5
|
+
* When compact=true, outputs minified JSON (saves tokens for agents)
|
|
6
|
+
*/
|
|
7
|
+
export function output(data, options) {
|
|
8
|
+
if (options.json) {
|
|
9
|
+
// Compact mode: no whitespace (token-efficient for agents)
|
|
10
|
+
// Normal JSON mode: pretty-printed with 2-space indent
|
|
11
|
+
const indent = options.compact ? undefined : 2;
|
|
12
|
+
console.log(JSON.stringify(data, null, indent));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
prettyPrint(data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Output NDJSON line (for streaming)
|
|
20
|
+
* Always compact - no pretty printing for streams
|
|
21
|
+
*/
|
|
22
|
+
export function outputNDJSON(data) {
|
|
23
|
+
console.log(JSON.stringify(data));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pretty print any data structure
|
|
27
|
+
*/
|
|
28
|
+
export function prettyPrint(data) {
|
|
29
|
+
if (data === null || data === undefined) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (typeof data === 'string') {
|
|
33
|
+
console.log(data);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(data)) {
|
|
37
|
+
data.forEach((item, i) => {
|
|
38
|
+
console.log(`${chalk.gray(`${i + 1}.`)} ${formatValue(item)}`);
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (typeof data === 'object') {
|
|
43
|
+
for (const [key, value] of Object.entries(data)) {
|
|
44
|
+
console.log(`${chalk.cyan(key)}: ${formatValue(value)}`);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(String(data));
|
|
49
|
+
}
|
|
50
|
+
function formatValue(value) {
|
|
51
|
+
if (value === null || value === undefined) {
|
|
52
|
+
return chalk.gray('null');
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'boolean') {
|
|
55
|
+
return value ? chalk.green('✓') : chalk.red('✗');
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'number') {
|
|
58
|
+
return chalk.yellow(String(value));
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'string') {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return `[${value.length} items]`;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'object') {
|
|
67
|
+
return JSON.stringify(value);
|
|
68
|
+
}
|
|
69
|
+
return String(value);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Print success message
|
|
73
|
+
*/
|
|
74
|
+
export function success(message) {
|
|
75
|
+
console.log(chalk.green('✓'), message);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Print error message
|
|
79
|
+
*/
|
|
80
|
+
export function error(message, suggestion) {
|
|
81
|
+
console.error(chalk.red('Error:'), message);
|
|
82
|
+
if (suggestion) {
|
|
83
|
+
console.error(chalk.gray('Suggestion:'), suggestion);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Print warning message
|
|
88
|
+
*/
|
|
89
|
+
export function warn(message) {
|
|
90
|
+
console.warn(chalk.yellow('⚠'), message);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Print info message
|
|
94
|
+
*/
|
|
95
|
+
export function info(message) {
|
|
96
|
+
console.log(chalk.blue('ℹ'), message);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Print a table
|
|
100
|
+
*/
|
|
101
|
+
export function table(headers, rows) {
|
|
102
|
+
// Calculate column widths
|
|
103
|
+
const widths = headers.map((h, i) => {
|
|
104
|
+
const colValues = [h, ...rows.map(r => r[i] || '')];
|
|
105
|
+
return Math.max(...colValues.map(v => v.length));
|
|
106
|
+
});
|
|
107
|
+
// Print header
|
|
108
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(' ');
|
|
109
|
+
console.log(chalk.bold(headerLine));
|
|
110
|
+
console.log(chalk.gray('─'.repeat(headerLine.length)));
|
|
111
|
+
// Print rows
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
const rowLine = row.map((cell, i) => (cell || '').padEnd(widths[i])).join(' ');
|
|
114
|
+
console.log(rowLine);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Format duration in mm:ss
|
|
119
|
+
*/
|
|
120
|
+
export function formatDuration(seconds) {
|
|
121
|
+
const mins = Math.floor(seconds / 60);
|
|
122
|
+
const secs = Math.floor(seconds % 60);
|
|
123
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Format relative time (e.g., "2 hours ago")
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* Prompt the user for confirmation before a destructive action.
|
|
130
|
+
* In non-interactive (piped/agent) mode, returns false — callers must use --force.
|
|
131
|
+
*/
|
|
132
|
+
export async function confirmAction(message) {
|
|
133
|
+
if (!process.stdin.isTTY) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
rl.question(`${chalk.yellow('⚠')} ${message} ${chalk.gray('[y/N]')} `, (answer) => {
|
|
139
|
+
rl.close();
|
|
140
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
export function formatRelativeTime(date) {
|
|
145
|
+
const now = new Date();
|
|
146
|
+
const then = new Date(date);
|
|
147
|
+
const diffMs = now.getTime() - then.getTime();
|
|
148
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
149
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
150
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
151
|
+
if (diffMins < 1)
|
|
152
|
+
return 'just now';
|
|
153
|
+
if (diffMins < 60)
|
|
154
|
+
return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
|
|
155
|
+
if (diffHours < 24)
|
|
156
|
+
return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
157
|
+
if (diffDays === 1)
|
|
158
|
+
return 'yesterday';
|
|
159
|
+
if (diffDays < 7)
|
|
160
|
+
return `${diffDays} days ago`;
|
|
161
|
+
return then.toLocaleDateString();
|
|
162
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface NormalizedScene {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
order: number;
|
|
5
|
+
track: number;
|
|
6
|
+
startFrame: number;
|
|
7
|
+
durationFrames: number;
|
|
8
|
+
endFrame: number;
|
|
9
|
+
hasCode: boolean;
|
|
10
|
+
hasCompilationError: boolean;
|
|
11
|
+
compilationError?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LayoutIssue {
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
sceneId?: string;
|
|
17
|
+
track?: number;
|
|
18
|
+
startFrame?: number;
|
|
19
|
+
endFrame?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ProjectValidationResult {
|
|
22
|
+
valid: boolean;
|
|
23
|
+
errors: LayoutIssue[];
|
|
24
|
+
warnings: LayoutIssue[];
|
|
25
|
+
summary: {
|
|
26
|
+
sceneCount: number;
|
|
27
|
+
trackCount: number;
|
|
28
|
+
totalFrames: number;
|
|
29
|
+
totalDurationSeconds: number;
|
|
30
|
+
compilationErrorCount: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
type ProjectMeta = {
|
|
34
|
+
format?: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
} | undefined;
|
|
38
|
+
export declare function normalizeScenes(rawScenes: unknown[], options?: {
|
|
39
|
+
codeOmitted?: boolean;
|
|
40
|
+
}): NormalizedScene[];
|
|
41
|
+
export declare function validateProjectStructure(scenes: NormalizedScene[], options?: {
|
|
42
|
+
fps?: number;
|
|
43
|
+
meta?: ProjectMeta;
|
|
44
|
+
}): ProjectValidationResult;
|
|
45
|
+
export declare function buildSearchCorpus(scenes: NormalizedScene[], options?: {
|
|
46
|
+
projectTitle?: string;
|
|
47
|
+
projectFormat?: string;
|
|
48
|
+
recipePrompts?: string[];
|
|
49
|
+
voiceovers?: string[];
|
|
50
|
+
sceneCodeById?: Record<string, string | undefined>;
|
|
51
|
+
}): string;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const FORMAT_DIMENSIONS = {
|
|
2
|
+
landscape: { width: 1920, height: 1080 },
|
|
3
|
+
portrait: { width: 1080, height: 1920 },
|
|
4
|
+
square: { width: 1080, height: 1080 },
|
|
5
|
+
};
|
|
6
|
+
export function normalizeScenes(rawScenes, options) {
|
|
7
|
+
return rawScenes.map((scene, index) => {
|
|
8
|
+
const id = String(scene.id ?? `scene-${index + 1}`);
|
|
9
|
+
const name = String(scene.name ?? scene.data?.name ?? `Scene ${index + 1}`);
|
|
10
|
+
const startFrame = Number(scene.start ?? scene.props?.start ?? scene.startFrame ?? 0);
|
|
11
|
+
const durationFrames = Number(scene.duration ?? scene.durationInFrames ?? 0);
|
|
12
|
+
const order = Number(scene.order ?? index);
|
|
13
|
+
const track = Number(scene.track ?? 0);
|
|
14
|
+
// When code is omitted from the response, tsxCode will be undefined --
|
|
15
|
+
// but the schema is NOT NULL, so every scene always has code.
|
|
16
|
+
const hasCode = options?.codeOmitted
|
|
17
|
+
? true
|
|
18
|
+
: Boolean(scene.tsxCode || scene.code || scene.data?.code);
|
|
19
|
+
const compilationError = scene.compilationError ?? scene.compilation_error;
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
name,
|
|
23
|
+
order,
|
|
24
|
+
track,
|
|
25
|
+
startFrame,
|
|
26
|
+
durationFrames,
|
|
27
|
+
endFrame: startFrame + durationFrames,
|
|
28
|
+
hasCode,
|
|
29
|
+
hasCompilationError: Boolean(compilationError),
|
|
30
|
+
compilationError: typeof compilationError === "string" ? compilationError : undefined,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function validateProjectStructure(scenes, options) {
|
|
35
|
+
const fps = options?.fps ?? 30;
|
|
36
|
+
const errors = [];
|
|
37
|
+
const warnings = [];
|
|
38
|
+
if (scenes.length === 0) {
|
|
39
|
+
warnings.push({
|
|
40
|
+
code: "NO_SCENES",
|
|
41
|
+
message: "Project has no scenes.",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (const scene of scenes) {
|
|
45
|
+
if (!Number.isFinite(scene.startFrame) || scene.startFrame < 0) {
|
|
46
|
+
errors.push({
|
|
47
|
+
code: "INVALID_START",
|
|
48
|
+
message: `Scene "${scene.name}" has invalid start frame (${scene.startFrame}).`,
|
|
49
|
+
sceneId: scene.id,
|
|
50
|
+
track: scene.track,
|
|
51
|
+
startFrame: scene.startFrame,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isFinite(scene.durationFrames) || scene.durationFrames <= 0) {
|
|
55
|
+
errors.push({
|
|
56
|
+
code: "INVALID_DURATION",
|
|
57
|
+
message: `Scene "${scene.name}" has invalid duration (${scene.durationFrames}).`,
|
|
58
|
+
sceneId: scene.id,
|
|
59
|
+
track: scene.track,
|
|
60
|
+
startFrame: scene.startFrame,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (!scene.hasCode) {
|
|
64
|
+
warnings.push({
|
|
65
|
+
code: "MISSING_CODE",
|
|
66
|
+
message: `Scene "${scene.name}" has no TSX code.`,
|
|
67
|
+
sceneId: scene.id,
|
|
68
|
+
track: scene.track,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (scene.hasCompilationError) {
|
|
72
|
+
errors.push({
|
|
73
|
+
code: "COMPILATION_ERROR",
|
|
74
|
+
message: `Scene "${scene.name}" has a compilation error.`,
|
|
75
|
+
sceneId: scene.id,
|
|
76
|
+
track: scene.track,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Overlap detection per track.
|
|
81
|
+
const scenesByTrack = new Map();
|
|
82
|
+
for (const scene of scenes) {
|
|
83
|
+
if (!scenesByTrack.has(scene.track))
|
|
84
|
+
scenesByTrack.set(scene.track, []);
|
|
85
|
+
scenesByTrack.get(scene.track).push(scene);
|
|
86
|
+
}
|
|
87
|
+
for (const [track, trackScenes] of scenesByTrack.entries()) {
|
|
88
|
+
const ordered = [...trackScenes].sort((a, b) => a.startFrame - b.startFrame || a.order - b.order);
|
|
89
|
+
for (let i = 1; i < ordered.length; i += 1) {
|
|
90
|
+
const prev = ordered[i - 1];
|
|
91
|
+
const curr = ordered[i];
|
|
92
|
+
if (curr.startFrame < prev.endFrame) {
|
|
93
|
+
errors.push({
|
|
94
|
+
code: "OVERLAP",
|
|
95
|
+
message: `Scenes "${prev.name}" and "${curr.name}" overlap on track ${track}.`,
|
|
96
|
+
sceneId: curr.id,
|
|
97
|
+
track,
|
|
98
|
+
startFrame: curr.startFrame,
|
|
99
|
+
endFrame: prev.endFrame,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (track === 0 && curr.startFrame > prev.endFrame) {
|
|
103
|
+
warnings.push({
|
|
104
|
+
code: "GAP",
|
|
105
|
+
message: `Gap detected on track 0 between "${prev.name}" and "${curr.name}".`,
|
|
106
|
+
sceneId: curr.id,
|
|
107
|
+
track,
|
|
108
|
+
startFrame: prev.endFrame,
|
|
109
|
+
endFrame: curr.startFrame,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (track === 0 && ordered.length > 0 && ordered[0].startFrame > 0) {
|
|
114
|
+
warnings.push({
|
|
115
|
+
code: "LEADING_GAP",
|
|
116
|
+
message: `Track 0 starts at frame ${ordered[0].startFrame} instead of 0.`,
|
|
117
|
+
sceneId: ordered[0].id,
|
|
118
|
+
track,
|
|
119
|
+
startFrame: 0,
|
|
120
|
+
endFrame: ordered[0].startFrame,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const format = options?.meta?.format;
|
|
125
|
+
const width = options?.meta?.width;
|
|
126
|
+
const height = options?.meta?.height;
|
|
127
|
+
if (format && FORMAT_DIMENSIONS[format]) {
|
|
128
|
+
const expected = FORMAT_DIMENSIONS[format];
|
|
129
|
+
if (width && height && (width !== expected.width || height !== expected.height)) {
|
|
130
|
+
warnings.push({
|
|
131
|
+
code: "FORMAT_DIMENSION_MISMATCH",
|
|
132
|
+
message: `Project format "${format}" expects ${expected.width}x${expected.height}, got ${width}x${height}.`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (format) {
|
|
137
|
+
warnings.push({
|
|
138
|
+
code: "UNKNOWN_FORMAT",
|
|
139
|
+
message: `Project format "${format}" is not recognized.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const totalFrames = scenes.reduce((max, scene) => Math.max(max, scene.endFrame), 0);
|
|
143
|
+
const trackCount = scenesByTrack.size || 0;
|
|
144
|
+
const compilationErrorCount = scenes.filter((scene) => scene.hasCompilationError).length;
|
|
145
|
+
return {
|
|
146
|
+
valid: errors.length === 0,
|
|
147
|
+
errors,
|
|
148
|
+
warnings,
|
|
149
|
+
summary: {
|
|
150
|
+
sceneCount: scenes.length,
|
|
151
|
+
trackCount,
|
|
152
|
+
totalFrames,
|
|
153
|
+
totalDurationSeconds: totalFrames / fps,
|
|
154
|
+
compilationErrorCount,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function buildSearchCorpus(scenes, options) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
if (options?.projectTitle)
|
|
161
|
+
parts.push(options.projectTitle);
|
|
162
|
+
if (options?.projectFormat)
|
|
163
|
+
parts.push(`format:${options.projectFormat}`);
|
|
164
|
+
for (const scene of scenes) {
|
|
165
|
+
parts.push(scene.name);
|
|
166
|
+
parts.push(`track:${scene.track}`);
|
|
167
|
+
if (options?.sceneCodeById?.[scene.id]) {
|
|
168
|
+
parts.push(options.sceneCodeById[scene.id] || "");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const prompt of options?.recipePrompts ?? []) {
|
|
172
|
+
parts.push(prompt);
|
|
173
|
+
}
|
|
174
|
+
for (const voiceover of options?.voiceovers ?? []) {
|
|
175
|
+
parts.push(voiceover);
|
|
176
|
+
}
|
|
177
|
+
return parts.join("\n").toLowerCase();
|
|
178
|
+
}
|