edsger 0.33.3 → 0.33.5
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/dist/auth/env-store.d.ts +41 -0
- package/dist/auth/env-store.js +124 -0
- package/dist/commands/config/index.d.ts +16 -0
- package/dist/commands/config/index.js +60 -0
- package/dist/commands/growth-analysis/index.js +14 -0
- package/dist/index.js +37 -2
- package/dist/phases/app-store-generation/screenshot-composer.js +11 -8
- package/dist/phases/growth-analysis/index.js +31 -43
- package/dist/services/video/device-frames.d.ts +4 -0
- package/dist/services/video/device-frames.js +13 -13
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env Store - Persistent environment variable storage for the Edsger CLI
|
|
3
|
+
*
|
|
4
|
+
* Stores user-configured environment variables in ~/.edsger/env.json
|
|
5
|
+
* These are loaded into process.env at CLI startup, with lower priority
|
|
6
|
+
* than real environment variables.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Load all stored env vars from ~/.edsger/env.json
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadEnvStore(): Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Set a single environment variable
|
|
14
|
+
*/
|
|
15
|
+
export declare function setEnvVar(key: string, value: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Get a single environment variable from the store
|
|
18
|
+
*/
|
|
19
|
+
export declare function getEnvVar(key: string): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Remove a single environment variable from the store
|
|
22
|
+
*/
|
|
23
|
+
export declare function unsetEnvVar(key: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* List all stored environment variables.
|
|
26
|
+
* Secret values are masked unless showSecrets is true.
|
|
27
|
+
*/
|
|
28
|
+
export declare function listEnvVars(showSecrets?: boolean): Array<{
|
|
29
|
+
key: string;
|
|
30
|
+
value: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Apply stored env vars to process.env.
|
|
34
|
+
* Only sets vars that are NOT already defined in process.env
|
|
35
|
+
* (real env vars take precedence).
|
|
36
|
+
*/
|
|
37
|
+
export declare function applyEnvStore(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Get the env file path (for display purposes)
|
|
40
|
+
*/
|
|
41
|
+
export declare function getEnvFilePath(): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env Store - Persistent environment variable storage for the Edsger CLI
|
|
3
|
+
*
|
|
4
|
+
* Stores user-configured environment variables in ~/.edsger/env.json
|
|
5
|
+
* These are loaded into process.env at CLI startup, with lower priority
|
|
6
|
+
* than real environment variables.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
const EDSGER_DIR = join(homedir(), '.edsger');
|
|
12
|
+
const ENV_FILE = join(EDSGER_DIR, 'env.json');
|
|
13
|
+
/** Known keys that contain secrets and should be masked in display */
|
|
14
|
+
const SECRET_KEYS = new Set([
|
|
15
|
+
'ELEVENLABS_API_KEY',
|
|
16
|
+
'DEEPGRAM_API_KEY',
|
|
17
|
+
'OPENAI_API_KEY',
|
|
18
|
+
'CLAUDE_API_KEY',
|
|
19
|
+
'GEMINI_API_KEY',
|
|
20
|
+
'EDSGER_MCP_TOKEN',
|
|
21
|
+
'GITHUB_TOKEN',
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Ensure the ~/.edsger directory exists
|
|
25
|
+
*/
|
|
26
|
+
function ensureDir() {
|
|
27
|
+
if (!existsSync(EDSGER_DIR)) {
|
|
28
|
+
mkdirSync(EDSGER_DIR, { recursive: true, mode: 0o700 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load all stored env vars from ~/.edsger/env.json
|
|
33
|
+
*/
|
|
34
|
+
export function loadEnvStore() {
|
|
35
|
+
try {
|
|
36
|
+
if (!existsSync(ENV_FILE)) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const content = readFileSync(ENV_FILE, 'utf-8');
|
|
40
|
+
return JSON.parse(content);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Save env vars to ~/.edsger/env.json
|
|
48
|
+
*/
|
|
49
|
+
function saveEnvStore(env) {
|
|
50
|
+
ensureDir();
|
|
51
|
+
writeFileSync(ENV_FILE, JSON.stringify(env, null, 2), 'utf-8');
|
|
52
|
+
try {
|
|
53
|
+
chmodSync(ENV_FILE, 0o600);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// chmod may fail on Windows
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Set a single environment variable
|
|
61
|
+
*/
|
|
62
|
+
export function setEnvVar(key, value) {
|
|
63
|
+
const env = loadEnvStore();
|
|
64
|
+
env[key] = value;
|
|
65
|
+
saveEnvStore(env);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a single environment variable from the store
|
|
69
|
+
*/
|
|
70
|
+
export function getEnvVar(key) {
|
|
71
|
+
const env = loadEnvStore();
|
|
72
|
+
return env[key];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Remove a single environment variable from the store
|
|
76
|
+
*/
|
|
77
|
+
export function unsetEnvVar(key) {
|
|
78
|
+
const env = loadEnvStore();
|
|
79
|
+
if (!(key in env)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
delete env[key];
|
|
83
|
+
saveEnvStore(env);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* List all stored environment variables.
|
|
88
|
+
* Secret values are masked unless showSecrets is true.
|
|
89
|
+
*/
|
|
90
|
+
export function listEnvVars(showSecrets = false) {
|
|
91
|
+
const env = loadEnvStore();
|
|
92
|
+
return Object.entries(env).map(([key, value]) => ({
|
|
93
|
+
key,
|
|
94
|
+
value: !showSecrets && SECRET_KEYS.has(key) ? maskValue(value) : value,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Apply stored env vars to process.env.
|
|
99
|
+
* Only sets vars that are NOT already defined in process.env
|
|
100
|
+
* (real env vars take precedence).
|
|
101
|
+
*/
|
|
102
|
+
export function applyEnvStore() {
|
|
103
|
+
const env = loadEnvStore();
|
|
104
|
+
for (const [key, value] of Object.entries(env)) {
|
|
105
|
+
if (process.env[key] === undefined) {
|
|
106
|
+
process.env[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get the env file path (for display purposes)
|
|
112
|
+
*/
|
|
113
|
+
export function getEnvFilePath() {
|
|
114
|
+
return ENV_FILE;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Mask a secret value for display, showing first 4 and last 4 chars
|
|
118
|
+
*/
|
|
119
|
+
function maskValue(value) {
|
|
120
|
+
if (value.length <= 12) {
|
|
121
|
+
return '****';
|
|
122
|
+
}
|
|
123
|
+
return `${value.substring(0, 4)}...${value.substring(value.length - 4)}`;
|
|
124
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edsger config set KEY=VALUE
|
|
3
|
+
*/
|
|
4
|
+
export declare function runConfigSet(pair: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* edsger config get KEY
|
|
7
|
+
*/
|
|
8
|
+
export declare function runConfigGet(key: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* edsger config unset KEY
|
|
11
|
+
*/
|
|
12
|
+
export declare function runConfigUnset(key: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* edsger config list
|
|
15
|
+
*/
|
|
16
|
+
export declare function runConfigList(): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { logInfo, logSuccess, logError, logWarning, logRaw } from '../../utils/logger.js';
|
|
2
|
+
import { setEnvVar, getEnvVar, unsetEnvVar, listEnvVars, getEnvFilePath, } from '../../auth/env-store.js';
|
|
3
|
+
/**
|
|
4
|
+
* edsger config set KEY=VALUE
|
|
5
|
+
*/
|
|
6
|
+
export function runConfigSet(pair) {
|
|
7
|
+
const eqIndex = pair.indexOf('=');
|
|
8
|
+
if (eqIndex === -1) {
|
|
9
|
+
logError('Invalid format. Use: edsger config set KEY=VALUE');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const key = pair.substring(0, eqIndex).trim();
|
|
13
|
+
const value = pair.substring(eqIndex + 1).trim();
|
|
14
|
+
if (!key) {
|
|
15
|
+
logError('Key cannot be empty.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
setEnvVar(key, value);
|
|
19
|
+
logSuccess(`${key} saved to ${getEnvFilePath()}`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* edsger config get KEY
|
|
23
|
+
*/
|
|
24
|
+
export function runConfigGet(key) {
|
|
25
|
+
const value = getEnvVar(key);
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
logWarning(`${key} is not set.`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
logRaw(value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* edsger config unset KEY
|
|
35
|
+
*/
|
|
36
|
+
export function runConfigUnset(key) {
|
|
37
|
+
const removed = unsetEnvVar(key);
|
|
38
|
+
if (removed) {
|
|
39
|
+
logSuccess(`${key} removed.`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
logWarning(`${key} was not set.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* edsger config list
|
|
47
|
+
*/
|
|
48
|
+
export function runConfigList() {
|
|
49
|
+
const vars = listEnvVars();
|
|
50
|
+
if (vars.length === 0) {
|
|
51
|
+
logInfo('No environment variables configured.');
|
|
52
|
+
logInfo(`Run \`edsger config set KEY=VALUE\` to add one.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
logInfo(`Stored environment variables (${getEnvFilePath()}):`);
|
|
56
|
+
logRaw('');
|
|
57
|
+
for (const { key, value } of vars) {
|
|
58
|
+
logRaw(` ${key}=${value}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logInfo, logError, logSuccess } from '../../utils/logger.js';
|
|
2
2
|
import { validateConfiguration } from '../../utils/validation.js';
|
|
3
3
|
import { analyseGrowth, } from '../../phases/growth-analysis/index.js';
|
|
4
|
+
import { checkVideoDependencies } from '../../services/video/index.js';
|
|
4
5
|
/**
|
|
5
6
|
* Run AI-powered growth analysis for a product.
|
|
6
7
|
* Analyzes the product, reviews previous campaigns, and generates
|
|
@@ -11,6 +12,19 @@ export const runGrowthAnalysis = async (options) => {
|
|
|
11
12
|
if (!productId) {
|
|
12
13
|
throw new Error('Product ID is required for growth analysis');
|
|
13
14
|
}
|
|
15
|
+
// Pre-flight checks: ensure TTS key and video dependencies are available
|
|
16
|
+
if (!process.env.ELEVENLABS_API_KEY && !process.env.DEEPGRAM_API_KEY) {
|
|
17
|
+
throw new Error('TTS API key required for growth analysis.\n' +
|
|
18
|
+
'Set one of the following environment variables:\n' +
|
|
19
|
+
' ELEVENLABS_API_KEY (primary)\n' +
|
|
20
|
+
' DEEPGRAM_API_KEY (alternative)\n\n' +
|
|
21
|
+
'Get a key at https://elevenlabs.io or https://deepgram.com');
|
|
22
|
+
}
|
|
23
|
+
const missingDeps = await checkVideoDependencies();
|
|
24
|
+
if (missingDeps.length > 0) {
|
|
25
|
+
throw new Error('Missing video dependencies:\n' +
|
|
26
|
+
missingDeps.map((d) => ` - ${d}`).join('\n'));
|
|
27
|
+
}
|
|
14
28
|
const config = validateConfiguration(options);
|
|
15
29
|
logInfo(`Starting growth analysis for product: ${productId}`);
|
|
16
30
|
try {
|
package/dist/index.js
CHANGED
|
@@ -15,14 +15,19 @@ import { runAnalyzeLogs } from './commands/analyze-logs/index.js';
|
|
|
15
15
|
import { runAgentWorkflow } from './commands/agent-workflow/index.js';
|
|
16
16
|
import { runTaskWorker } from './commands/task-worker/index.js';
|
|
17
17
|
import { runLogin, runLogout, runStatus } from './auth/login.js';
|
|
18
|
+
import { applyEnvStore } from './auth/env-store.js';
|
|
19
|
+
import { runConfigSet, runConfigGet, runConfigUnset, runConfigList, } from './commands/config/index.js';
|
|
18
20
|
// Get package.json version dynamically
|
|
19
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
22
|
const __dirname = dirname(__filename);
|
|
21
23
|
const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
22
24
|
const version = packageJson.version;
|
|
23
|
-
// Load environment variables
|
|
24
|
-
//
|
|
25
|
+
// Load environment variables (each only sets vars not already defined):
|
|
26
|
+
// 1. Real environment variables (highest priority - already in process.env)
|
|
27
|
+
// 2. .env file in cwd (medium priority - loaded first)
|
|
28
|
+
// 3. ~/.edsger/env.json (lowest priority - loaded last)
|
|
25
29
|
dotenvConfig({ path: join(process.cwd(), '.env') });
|
|
30
|
+
applyEnvStore();
|
|
26
31
|
const program = new Command();
|
|
27
32
|
program
|
|
28
33
|
.name('edsger')
|
|
@@ -74,6 +79,36 @@ program
|
|
|
74
79
|
}
|
|
75
80
|
});
|
|
76
81
|
// ============================================================
|
|
82
|
+
// Subcommand: edsger config set/get/list/unset
|
|
83
|
+
// ============================================================
|
|
84
|
+
const configCmd = program
|
|
85
|
+
.command('config')
|
|
86
|
+
.description('Manage CLI environment variables (stored in ~/.edsger/env.json)');
|
|
87
|
+
configCmd
|
|
88
|
+
.command('set <key=value>')
|
|
89
|
+
.description('Set an environment variable (e.g., edsger config set ELEVENLABS_API_KEY=sk-xxx)')
|
|
90
|
+
.action((pair) => {
|
|
91
|
+
runConfigSet(pair);
|
|
92
|
+
});
|
|
93
|
+
configCmd
|
|
94
|
+
.command('get <key>')
|
|
95
|
+
.description('Get the value of a stored environment variable')
|
|
96
|
+
.action((key) => {
|
|
97
|
+
runConfigGet(key);
|
|
98
|
+
});
|
|
99
|
+
configCmd
|
|
100
|
+
.command('unset <key>')
|
|
101
|
+
.description('Remove a stored environment variable')
|
|
102
|
+
.action((key) => {
|
|
103
|
+
runConfigUnset(key);
|
|
104
|
+
});
|
|
105
|
+
configCmd
|
|
106
|
+
.command('list')
|
|
107
|
+
.description('List all stored environment variables')
|
|
108
|
+
.action(() => {
|
|
109
|
+
runConfigList();
|
|
110
|
+
});
|
|
111
|
+
// ============================================================
|
|
77
112
|
// Subcommand: edsger growth <productId>
|
|
78
113
|
// ============================================================
|
|
79
114
|
program
|
|
@@ -88,12 +88,10 @@ function createCompositionHtml(framedScreenshotDataUrl, spec, targetWidth, targe
|
|
|
88
88
|
align-items: flex-start;
|
|
89
89
|
justify-content: center;
|
|
90
90
|
overflow: hidden;
|
|
91
|
-
padding: 0 ${Math.round(targetWidth * 0.
|
|
91
|
+
padding: 0 ${Math.round(targetWidth * 0.02)}px;
|
|
92
92
|
}
|
|
93
93
|
.device-area img {
|
|
94
|
-
|
|
95
|
-
max-height: 100%;
|
|
96
|
-
object-fit: contain;
|
|
94
|
+
width: 92%;
|
|
97
95
|
}
|
|
98
96
|
</style>
|
|
99
97
|
</head>
|
|
@@ -151,11 +149,11 @@ function createCompositionHtml(framedScreenshotDataUrl, spec, targetWidth, targe
|
|
|
151
149
|
align-items: center;
|
|
152
150
|
justify-content: center;
|
|
153
151
|
overflow: hidden;
|
|
154
|
-
padding: ${Math.round(targetHeight * 0.
|
|
152
|
+
padding: ${Math.round(targetHeight * 0.03)}px;
|
|
155
153
|
}
|
|
156
154
|
.device-area img {
|
|
157
155
|
max-width: 100%;
|
|
158
|
-
max-height:
|
|
156
|
+
max-height: 95%;
|
|
159
157
|
object-fit: contain;
|
|
160
158
|
}
|
|
161
159
|
</style>
|
|
@@ -236,13 +234,17 @@ export async function generateStoreScreenshots(specs, options) {
|
|
|
236
234
|
continue;
|
|
237
235
|
}
|
|
238
236
|
// Step 2: Wrap in device frame
|
|
237
|
+
// Use tight canvas dimensions so the device fills most of the frame image.
|
|
238
|
+
// This prevents excess padding that would make the device appear small in composition.
|
|
239
239
|
const rawDataUrl = `data:image/png;base64,${rawScreenshotBuffer.toString('base64')}`;
|
|
240
|
+
const frameWidth = deviceFrame === 'iphone' ? 500 : 1600;
|
|
241
|
+
const frameHeight = deviceFrame === 'iphone' ? 1050 : 1000;
|
|
240
242
|
const frameHtml = generateDeviceFrameHtml(rawDataUrl, deviceFrame, {
|
|
241
243
|
background: 'transparent',
|
|
242
244
|
shadow: true,
|
|
245
|
+
canvasWidth: frameWidth,
|
|
246
|
+
canvasHeight: frameHeight,
|
|
243
247
|
});
|
|
244
|
-
const frameWidth = deviceFrame === 'iphone' ? 800 : 1600;
|
|
245
|
-
const frameHeight = deviceFrame === 'iphone' ? 1200 : 1000;
|
|
246
248
|
let framedBuffer;
|
|
247
249
|
try {
|
|
248
250
|
const frameContext = await browser.newContext({
|
|
@@ -255,6 +257,7 @@ export async function generateStoreScreenshots(specs, options) {
|
|
|
255
257
|
framedBuffer = await framePage.screenshot({
|
|
256
258
|
type: 'png',
|
|
257
259
|
fullPage: false,
|
|
260
|
+
omitBackground: true,
|
|
258
261
|
});
|
|
259
262
|
await frameContext.close();
|
|
260
263
|
}
|
|
@@ -5,7 +5,7 @@ import { executeGrowthAnalysisQuery } from './agent.js';
|
|
|
5
5
|
import { saveGrowthAnalysis } from '../../api/growth.js';
|
|
6
6
|
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
7
7
|
import { ensureWorkspaceDir, cloneFeatureRepo, } from '../../workspace/workspace-manager.js';
|
|
8
|
-
import { generateGrowthVideo,
|
|
8
|
+
import { generateGrowthVideo, } from '../../services/video/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* Extract video plans from AI-generated content suggestions
|
|
11
11
|
*/
|
|
@@ -98,56 +98,44 @@ export const analyseGrowth = async (options, config) => {
|
|
|
98
98
|
const videoResults = new Map();
|
|
99
99
|
if (videoPlans.length > 0) {
|
|
100
100
|
logInfo(`\nFound ${videoPlans.length} content suggestion(s) with video plans`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
const analysisId = savedAnalysis?.id || productId;
|
|
102
|
+
// Log all planned videos
|
|
103
|
+
for (const { index, plan } of videoPlans) {
|
|
104
|
+
const suggestion = contentSuggestions[index];
|
|
105
|
+
logInfo(`\nVideo planned for: "${suggestion.title}" [${suggestion.channel}]`);
|
|
106
|
+
logInfo(` Reason: ${plan.videoReason}`);
|
|
107
|
+
logInfo(` Scenes: ${plan.scenes.length}`);
|
|
106
108
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
// Generate videos with concurrency limit (max 2 simultaneous)
|
|
110
|
+
const MAX_CONCURRENT_VIDEOS = 2;
|
|
111
|
+
logInfo(`\nGenerating ${videoPlans.length} video(s) (max ${MAX_CONCURRENT_VIDEOS} concurrent)...`);
|
|
112
|
+
const settled = await runWithConcurrencyLimit(videoPlans.map(({ index, plan }) => () => generateGrowthVideo(plan, {
|
|
113
|
+
productId,
|
|
114
|
+
analysisId,
|
|
115
|
+
verbose,
|
|
116
|
+
}).then((result) => ({ index, result }))), MAX_CONCURRENT_VIDEOS);
|
|
117
|
+
for (const outcome of settled) {
|
|
118
|
+
if (outcome.status === 'fulfilled') {
|
|
119
|
+
const { index, result } = outcome.value;
|
|
115
120
|
const suggestion = contentSuggestions[index];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const MAX_CONCURRENT_VIDEOS = 2;
|
|
122
|
-
logInfo(`\nGenerating ${videoPlans.length} video(s) (max ${MAX_CONCURRENT_VIDEOS} concurrent)...`);
|
|
123
|
-
const settled = await runWithConcurrencyLimit(videoPlans.map(({ index, plan }) => () => generateGrowthVideo(plan, {
|
|
124
|
-
productId,
|
|
125
|
-
analysisId,
|
|
126
|
-
verbose,
|
|
127
|
-
}).then((result) => ({ index, result }))), MAX_CONCURRENT_VIDEOS);
|
|
128
|
-
for (const outcome of settled) {
|
|
129
|
-
if (outcome.status === 'fulfilled') {
|
|
130
|
-
const { index, result } = outcome.value;
|
|
131
|
-
const suggestion = contentSuggestions[index];
|
|
132
|
-
videoResults.set(index, result);
|
|
133
|
-
if (result.success) {
|
|
134
|
-
logSuccess(`Video generated for "${suggestion.title}": ${result.duration?.toFixed(1)}s, ${result.sceneCount} scenes`);
|
|
135
|
-
if (result.localPath) {
|
|
136
|
-
logInfo(` Local: ${result.localPath}`);
|
|
137
|
-
}
|
|
138
|
-
if (result.videoUrl) {
|
|
139
|
-
logInfo(` URL: ${result.videoUrl}`);
|
|
140
|
-
}
|
|
121
|
+
videoResults.set(index, result);
|
|
122
|
+
if (result.success) {
|
|
123
|
+
logSuccess(`Video generated for "${suggestion.title}": ${result.duration?.toFixed(1)}s, ${result.sceneCount} scenes`);
|
|
124
|
+
if (result.localPath) {
|
|
125
|
+
logInfo(` Local: ${result.localPath}`);
|
|
141
126
|
}
|
|
142
|
-
|
|
143
|
-
|
|
127
|
+
if (result.videoUrl) {
|
|
128
|
+
logInfo(` URL: ${result.videoUrl}`);
|
|
144
129
|
}
|
|
145
130
|
}
|
|
146
131
|
else {
|
|
147
|
-
|
|
148
|
-
logError(`Video generation error: ${outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)}`);
|
|
132
|
+
logWarning(`Video generation failed for "${suggestion.title}": ${result.error}`);
|
|
149
133
|
}
|
|
150
134
|
}
|
|
135
|
+
else {
|
|
136
|
+
// Find the index from the error — log it generically
|
|
137
|
+
logError(`Video generation error: ${outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)}`);
|
|
138
|
+
}
|
|
151
139
|
}
|
|
152
140
|
}
|
|
153
141
|
// Build final content suggestions with video results
|
|
@@ -27,4 +27,8 @@ export declare function generateDeviceFrameHtml(screenshotDataUrl: string, devic
|
|
|
27
27
|
browserTitle?: string;
|
|
28
28
|
/** URL to show in the browser address bar (browser frame only) */
|
|
29
29
|
browserUrl?: string;
|
|
30
|
+
/** Custom canvas width (defaults vary by device type) */
|
|
31
|
+
canvasWidth?: number;
|
|
32
|
+
/** Custom canvas height (defaults vary by device type) */
|
|
33
|
+
canvasHeight?: number;
|
|
30
34
|
}): string;
|
|
@@ -17,27 +17,27 @@ export const DEVICE_PRESETS = {
|
|
|
17
17
|
* @param options - additional customization options
|
|
18
18
|
*/
|
|
19
19
|
export function generateDeviceFrameHtml(screenshotDataUrl, deviceType, options) {
|
|
20
|
-
const { background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', shadow = true, browserTitle = 'Edsger', browserUrl = 'app.edsger.ai', } = options || {};
|
|
20
|
+
const { background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', shadow = true, browserTitle = 'Edsger', browserUrl = 'app.edsger.ai', canvasWidth, canvasHeight, } = options || {};
|
|
21
21
|
switch (deviceType) {
|
|
22
22
|
case 'macbook':
|
|
23
|
-
return generateMacBookFrame(screenshotDataUrl, background, shadow);
|
|
23
|
+
return generateMacBookFrame(screenshotDataUrl, background, shadow, canvasWidth ?? 1600, canvasHeight ?? 1000);
|
|
24
24
|
case 'iphone':
|
|
25
|
-
return generateIPhoneFrame(screenshotDataUrl, background, shadow);
|
|
25
|
+
return generateIPhoneFrame(screenshotDataUrl, background, shadow, canvasWidth ?? 800, canvasHeight ?? 1200);
|
|
26
26
|
case 'browser':
|
|
27
|
-
return generateBrowserFrame(screenshotDataUrl, background, shadow, browserTitle, browserUrl);
|
|
27
|
+
return generateBrowserFrame(screenshotDataUrl, background, shadow, browserTitle, browserUrl, canvasWidth ?? 1600, canvasHeight ?? 1000);
|
|
28
28
|
case 'none':
|
|
29
29
|
return generatePlainFrame(screenshotDataUrl, background);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
function generateMacBookFrame(screenshotDataUrl, background, shadow) {
|
|
32
|
+
function generateMacBookFrame(screenshotDataUrl, background, shadow, canvasWidth, canvasHeight) {
|
|
33
33
|
return `<!DOCTYPE html>
|
|
34
34
|
<html>
|
|
35
35
|
<head>
|
|
36
36
|
<style>
|
|
37
37
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
38
38
|
body {
|
|
39
|
-
width:
|
|
40
|
-
height:
|
|
39
|
+
width: ${canvasWidth}px;
|
|
40
|
+
height: ${canvasHeight}px;
|
|
41
41
|
display: flex;
|
|
42
42
|
align-items: center;
|
|
43
43
|
justify-content: center;
|
|
@@ -118,15 +118,15 @@ function generateMacBookFrame(screenshotDataUrl, background, shadow) {
|
|
|
118
118
|
</body>
|
|
119
119
|
</html>`;
|
|
120
120
|
}
|
|
121
|
-
function generateIPhoneFrame(screenshotDataUrl, background, shadow) {
|
|
121
|
+
function generateIPhoneFrame(screenshotDataUrl, background, shadow, canvasWidth, canvasHeight) {
|
|
122
122
|
return `<!DOCTYPE html>
|
|
123
123
|
<html>
|
|
124
124
|
<head>
|
|
125
125
|
<style>
|
|
126
126
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
127
127
|
body {
|
|
128
|
-
width:
|
|
129
|
-
height:
|
|
128
|
+
width: ${canvasWidth}px;
|
|
129
|
+
height: ${canvasHeight}px;
|
|
130
130
|
display: flex;
|
|
131
131
|
align-items: center;
|
|
132
132
|
justify-content: center;
|
|
@@ -234,15 +234,15 @@ function generateIPhoneFrame(screenshotDataUrl, background, shadow) {
|
|
|
234
234
|
</body>
|
|
235
235
|
</html>`;
|
|
236
236
|
}
|
|
237
|
-
function generateBrowserFrame(screenshotDataUrl, background, shadow, title, url) {
|
|
237
|
+
function generateBrowserFrame(screenshotDataUrl, background, shadow, title, url, canvasWidth, canvasHeight) {
|
|
238
238
|
return `<!DOCTYPE html>
|
|
239
239
|
<html>
|
|
240
240
|
<head>
|
|
241
241
|
<style>
|
|
242
242
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
243
243
|
body {
|
|
244
|
-
width:
|
|
245
|
-
height:
|
|
244
|
+
width: ${canvasWidth}px;
|
|
245
|
+
height: ${canvasHeight}px;
|
|
246
246
|
display: flex;
|
|
247
247
|
align-items: center;
|
|
248
248
|
justify-content: center;
|