edsger 0.33.1 → 0.33.2
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.
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage API – fetches signed upload URLs from the MCP server
|
|
3
|
+
* so the CLI can upload directly to Supabase Storage without a JWT.
|
|
4
|
+
*/
|
|
5
|
+
export interface SignedUploadUrl {
|
|
6
|
+
path: string;
|
|
7
|
+
signed_url: string;
|
|
8
|
+
token: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Request signed upload URLs for a batch of storage paths.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getSignedUploadUrls(bucket: string, paths: string[], verbose?: boolean): Promise<SignedUploadUrl[]>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage API – fetches signed upload URLs from the MCP server
|
|
3
|
+
* so the CLI can upload directly to Supabase Storage without a JWT.
|
|
4
|
+
*/
|
|
5
|
+
import { callMcpEndpoint } from './mcp-client.js';
|
|
6
|
+
import { logError } from '../utils/logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* Request signed upload URLs for a batch of storage paths.
|
|
9
|
+
*/
|
|
10
|
+
export async function getSignedUploadUrls(bucket, paths, verbose) {
|
|
11
|
+
try {
|
|
12
|
+
const result = (await callMcpEndpoint('storage/create_signed_upload_urls', { bucket, paths }));
|
|
13
|
+
const text = result.content?.[0]?.text || '{"urls":[],"errors":[]}';
|
|
14
|
+
const parsed = JSON.parse(text);
|
|
15
|
+
if (parsed.errors?.length) {
|
|
16
|
+
for (const err of parsed.errors) {
|
|
17
|
+
logError(`Signed URL error for ${err.path}: ${err.error}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return parsed.urls;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
logError(`Failed to get signed upload URLs: ${error instanceof Error ? error.message : String(error)}`);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { logInfo, logError, logSuccess } from '../../utils/logger.js';
|
|
2
|
-
import { validateConfiguration } from '../../utils/validation.js';
|
|
2
|
+
import { validateConfiguration, validateRequirements, } from '../../utils/validation.js';
|
|
3
|
+
import { isPlaywrightAvailable } from '../../services/video/screenshot-generator.js';
|
|
3
4
|
import { generateAppStoreAssets, } from '../../phases/app-store-generation/index.js';
|
|
4
5
|
/**
|
|
5
6
|
* Run AI-powered app store asset generation for a product.
|
|
@@ -11,6 +12,14 @@ export const runAppStoreGeneration = async (options) => {
|
|
|
11
12
|
throw new Error('Product ID is required for app store generation');
|
|
12
13
|
}
|
|
13
14
|
const config = validateConfiguration(options);
|
|
15
|
+
// Pre-flight check: ensure Playwright is installed when screenshots are needed
|
|
16
|
+
if (!options.listingsOnly) {
|
|
17
|
+
await validateRequirements(isPlaywrightAvailable, 'Playwright is required for screenshot generation but is not installed.\n\n' +
|
|
18
|
+
' Install it by running:\n\n' +
|
|
19
|
+
' npm install playwright\n' +
|
|
20
|
+
' npx playwright install chromium\n\n' +
|
|
21
|
+
' Then re-run this command. Use --listings-only to skip screenshots.');
|
|
22
|
+
}
|
|
14
23
|
const targetStore = options.store === 'apple'
|
|
15
24
|
? 'apple'
|
|
16
25
|
: options.store === 'google'
|
|
@@ -8,9 +8,10 @@ import { readFile, copyFile, mkdir } from 'fs/promises';
|
|
|
8
8
|
import { join, basename } from 'path';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
11
|
-
import { getMcpServerUrl
|
|
11
|
+
import { getMcpServerUrl } from '../../auth/auth-store.js';
|
|
12
12
|
import { fetchWithRetry } from '../../services/video/retry.js';
|
|
13
13
|
import { saveAppStoreScreenshots, } from '../../api/app-store.js';
|
|
14
|
+
import { getSignedUploadUrls } from '../../api/storage.js';
|
|
14
15
|
/**
|
|
15
16
|
* Get local backup directory for app store assets
|
|
16
17
|
*/
|
|
@@ -39,8 +40,7 @@ export async function uploadAppStoreScreenshots(screenshots, options) {
|
|
|
39
40
|
const localDir = join(getLocalAssetDir(), productId);
|
|
40
41
|
await mkdir(localDir, { recursive: true });
|
|
41
42
|
const storageBaseUrl = getStorageBaseUrl();
|
|
42
|
-
|
|
43
|
-
if (!storageBaseUrl || !mcpToken) {
|
|
43
|
+
if (!storageBaseUrl) {
|
|
44
44
|
logError('Not authenticated. Run `edsger login` to configure storage access.');
|
|
45
45
|
// Still save locally even if we can't upload
|
|
46
46
|
const localResults = [];
|
|
@@ -56,26 +56,48 @@ export async function uploadAppStoreScreenshots(screenshots, options) {
|
|
|
56
56
|
}
|
|
57
57
|
return localResults;
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
// Save local backups and build storage paths
|
|
60
|
+
const storagePaths = [];
|
|
60
61
|
for (const ss of screenshots) {
|
|
62
|
+
const localFileName = basename(ss.local_path);
|
|
63
|
+
const localPath = join(localDir, localFileName);
|
|
64
|
+
await copyFile(ss.local_path, localPath);
|
|
65
|
+
storagePaths.push(`${productId}/${localFileName}`);
|
|
66
|
+
}
|
|
67
|
+
// Fetch signed upload URLs in a single batch request
|
|
68
|
+
const signedUrls = await getSignedUploadUrls('app-store-assets', storagePaths, verbose);
|
|
69
|
+
const signedUrlMap = new Map(signedUrls.map((u) => [u.path, u]));
|
|
70
|
+
const uploadedScreenshots = [];
|
|
71
|
+
for (let i = 0; i < screenshots.length; i++) {
|
|
72
|
+
const ss = screenshots[i];
|
|
73
|
+
const storagePath = storagePaths[i];
|
|
74
|
+
const signed = signedUrlMap.get(storagePath);
|
|
75
|
+
if (!signed) {
|
|
76
|
+
logError(`No signed URL for ${storagePath}, skipping upload`);
|
|
77
|
+
uploadedScreenshots.push({
|
|
78
|
+
device_type: ss.device_type,
|
|
79
|
+
display_order: ss.display_order,
|
|
80
|
+
storage_url: '',
|
|
81
|
+
storage_path: '',
|
|
82
|
+
width: ss.width,
|
|
83
|
+
height: ss.height,
|
|
84
|
+
caption: ss.caption,
|
|
85
|
+
background_gradient: ss.background_gradient,
|
|
86
|
+
device_frame: ss.device_frame,
|
|
87
|
+
locale: ss.locale,
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
61
91
|
try {
|
|
62
|
-
// Local backup
|
|
63
92
|
const localFileName = basename(ss.local_path);
|
|
64
|
-
const localPath = join(localDir, localFileName);
|
|
65
|
-
await copyFile(ss.local_path, localPath);
|
|
66
|
-
// Upload to Supabase Storage
|
|
67
|
-
const storagePath = `${productId}/${localFileName}`;
|
|
68
|
-
const uploadUrl = `${storageBaseUrl}/object/app-store-assets/${storagePath}`;
|
|
69
93
|
const fileBuffer = await readFile(ss.local_path);
|
|
70
94
|
if (verbose) {
|
|
71
95
|
logInfo(`Uploading ${localFileName} (${(fileBuffer.length / 1024).toFixed(0)}KB)...`);
|
|
72
96
|
}
|
|
73
|
-
await fetchWithRetry(
|
|
74
|
-
method: '
|
|
97
|
+
await fetchWithRetry(signed.signed_url, {
|
|
98
|
+
method: 'PUT',
|
|
75
99
|
headers: {
|
|
76
|
-
Authorization: `Bearer ${mcpToken}`,
|
|
77
100
|
'Content-Type': 'image/png',
|
|
78
|
-
'x-upsert': 'true',
|
|
79
101
|
},
|
|
80
102
|
body: new Uint8Array(fileBuffer),
|
|
81
103
|
}, { label: `Upload ${localFileName}` });
|
|
@@ -95,7 +117,6 @@ export async function uploadAppStoreScreenshots(screenshots, options) {
|
|
|
95
117
|
}
|
|
96
118
|
catch (error) {
|
|
97
119
|
logError(`Failed to upload ${basename(ss.local_path)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
-
// Still include with empty storage URL
|
|
99
120
|
uploadedScreenshots.push({
|
|
100
121
|
device_type: ss.device_type,
|
|
101
122
|
display_order: ss.display_order,
|
|
@@ -6,9 +6,10 @@ import { readFile, copyFile, mkdir } from 'fs/promises';
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { logInfo, logError } from '../../utils/logger.js';
|
|
9
|
-
import { getMcpServerUrl
|
|
9
|
+
import { getMcpServerUrl } from '../../auth/auth-store.js';
|
|
10
10
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
11
11
|
import { fetchWithRetry } from './retry.js';
|
|
12
|
+
import { getSignedUploadUrls } from '../../api/storage.js';
|
|
12
13
|
/**
|
|
13
14
|
* Get the local backup directory for growth videos
|
|
14
15
|
*/
|
|
@@ -46,22 +47,23 @@ export async function uploadGrowthVideo(videoPath, productId, analysisId, verbos
|
|
|
46
47
|
const storagePath = `${productId}/${analysisId}/${localFileName}`;
|
|
47
48
|
try {
|
|
48
49
|
const storageBaseUrl = getStorageBaseUrl();
|
|
49
|
-
|
|
50
|
-
if (!storageBaseUrl || !mcpToken) {
|
|
50
|
+
if (!storageBaseUrl) {
|
|
51
51
|
throw new Error('Not authenticated. Run `edsger login` to configure storage access.');
|
|
52
52
|
}
|
|
53
53
|
const videoBuffer = await readFile(videoPath);
|
|
54
54
|
if (verbose) {
|
|
55
55
|
logInfo(`Uploading video to Supabase Storage (${(videoBuffer.length / 1024 / 1024).toFixed(1)}MB)...`);
|
|
56
56
|
}
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// Get a signed upload URL from the MCP server
|
|
58
|
+
const signedUrls = await getSignedUploadUrls('growth-videos', [storagePath], verbose);
|
|
59
|
+
if (!signedUrls.length) {
|
|
60
|
+
throw new Error('Failed to get signed upload URL for video');
|
|
61
|
+
}
|
|
62
|
+
// Upload using the signed URL (PUT, no auth header needed)
|
|
63
|
+
await fetchWithRetry(signedUrls[0].signed_url, {
|
|
64
|
+
method: 'PUT',
|
|
61
65
|
headers: {
|
|
62
|
-
Authorization: `Bearer ${mcpToken}`,
|
|
63
66
|
'Content-Type': 'video/mp4',
|
|
64
|
-
'x-upsert': 'true',
|
|
65
67
|
},
|
|
66
68
|
body: new Uint8Array(videoBuffer),
|
|
67
69
|
}, { label: 'Supabase Storage upload' });
|