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, getMcpToken } from '../../auth/auth-store.js';
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
- const mcpToken = getMcpToken();
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
- const uploadedScreenshots = [];
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(uploadUrl, {
74
- method: 'POST',
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, getMcpToken } from '../../auth/auth-store.js';
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
- const mcpToken = getMcpToken();
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
- // Upload via Supabase Storage Object API (binary body) with retry
58
- const uploadUrl = `${storageBaseUrl}/object/growth-videos/${storagePath}`;
59
- await fetchWithRetry(uploadUrl, {
60
- method: 'POST',
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' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.33.1",
3
+ "version": "0.33.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"