@notis_ai/cli 0.2.0-beta.16.1 → 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.
Files changed (39) hide show
  1. package/README.md +4 -6
  2. package/package.json +1 -6
  3. package/src/command-specs/apps.js +10 -77
  4. package/src/runtime/app-platform.js +69 -309
  5. package/src/runtime/app-preview-server.js +96 -136
  6. package/template/app/globals.css +0 -3
  7. package/template/app/layout.tsx +0 -7
  8. package/template/app/page.tsx +0 -55
  9. package/template/components/ui/badge.tsx +0 -28
  10. package/template/components/ui/button.tsx +0 -53
  11. package/template/components/ui/card.tsx +0 -56
  12. package/template/components.json +0 -20
  13. package/template/lib/utils.ts +0 -6
  14. package/template/notis.config.ts +0 -18
  15. package/template/package.json +0 -32
  16. package/template/packages/notis-sdk/package.json +0 -26
  17. package/template/packages/notis-sdk/src/config.ts +0 -48
  18. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  19. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  20. package/template/packages/notis-sdk/src/hooks/useBackend.ts +0 -41
  21. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  22. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  23. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  24. package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
  25. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
  26. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  27. package/template/packages/notis-sdk/src/hooks/useTools.ts +0 -56
  28. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  29. package/template/packages/notis-sdk/src/index.ts +0 -47
  30. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  31. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  32. package/template/packages/notis-sdk/src/styles.css +0 -123
  33. package/template/packages/notis-sdk/src/ui.ts +0 -15
  34. package/template/packages/notis-sdk/src/vite.ts +0 -54
  35. package/template/packages/notis-sdk/tsconfig.json +0 -15
  36. package/template/postcss.config.mjs +0 -8
  37. package/template/tailwind.config.ts +0 -58
  38. package/template/tsconfig.json +0 -22
  39. package/template/vite.config.ts +0 -10
@@ -3,23 +3,22 @@
3
3
  *
4
4
  * Handles project scaffolding, validation, building, and linking. This is the
5
5
  * CLI-side counterpart to @notis/sdk -- it reads notis.config.ts, runs the
6
- * Vite build, and packages the bundle for deployment.
6
+ * Next.js build, and packages the artifact for deployment.
7
7
  */
8
8
 
9
9
  import { spawn } from 'node:child_process';
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, readdirSync } from 'node:fs';
11
- import { createRequire } from 'node:module';
12
11
  import { dirname, join, resolve } from 'node:path';
13
12
  import { fileURLToPath } from 'node:url';
13
+ import { createHash } from 'node:crypto';
14
14
 
15
15
  import { usageError } from './errors.js';
16
16
 
17
17
  const NOTIS_DIR = '.notis';
18
18
  const STATE_FILE = join(NOTIS_DIR, 'state.json');
19
19
  const OUTPUT_DIR = join(NOTIS_DIR, 'output');
20
- const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
20
+ const SITE_DIR = join(OUTPUT_DIR, 'site');
21
21
  const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
22
- let appConfigImportNonce = 0;
23
22
 
24
23
  // ---------------------------------------------------------------------------
25
24
  // Project directory resolution
@@ -35,29 +34,9 @@ export function resolveProjectDir(inputDir = '.') {
35
34
 
36
35
  /**
37
36
  * Load and parse notis.config.ts from a project directory. Uses a simple
38
- * approach: we read the file, transpile with the project's TypeScript compiler
39
- * when available, then evaluate as ESM.
37
+ * approach: we read the file, transpile with a basic regex to strip types,
38
+ * and evaluate. For production use this should use tsx or similar.
40
39
  */
41
- function transpileTsConfigSource(source, configPath) {
42
- const requireFromConfig = createRequire(`file://${configPath}`);
43
- try {
44
- const ts = requireFromConfig('typescript');
45
- const transpiled = ts.transpileModule(source, {
46
- compilerOptions: {
47
- module: ts.ModuleKind.ESNext,
48
- target: ts.ScriptTarget.ES2020,
49
- },
50
- fileName: configPath,
51
- });
52
- return transpiled.outputText;
53
- } catch {
54
- return source
55
- .replace(/import\s+type\s+[\s\S]*?from\s+['"][^'"]*['"]\s*;?/g, '')
56
- .replace(/import\s*{[\s\S]*?\bdefineNotisApp\b[\s\S]*?}\s+from\s+['"]@notis\/sdk\/config['"]\s*;?/g, '')
57
- .replace(/defineNotisApp\s*\(/, '(');
58
- }
59
- }
60
-
61
40
  export async function loadAppConfig(projectDir) {
62
41
  const configPaths = ['notis.config.ts', 'notis.config.js', 'notis.config.mjs'];
63
42
  let configPath = null;
@@ -75,24 +54,24 @@ export async function loadAppConfig(projectDir) {
75
54
  }
76
55
 
77
56
  // For .js/.mjs files, import directly. For .ts, we need transpilation.
57
+ // In the CLI context, we use a simple approach: read the file, strip TS
58
+ // syntax, and eval. This avoids requiring tsx as a dependency.
78
59
  if (configPath.endsWith('.ts')) {
79
60
  const source = readFileSync(configPath, 'utf-8');
80
- const jsSource = transpileTsConfigSource(source, configPath);
61
+ // Strip import type annotations and type declarations
62
+ const jsSource = source
63
+ .replace(/import\s+type\s+{[^}]*}\s+from\s+['"][^'"]*['"]\s*;?/g, '')
64
+ .replace(/import\s+{[^}]*}\s+from\s+['"][^'"]*['"]\s*;?/g, '')
65
+ .replace(/export\s+default\s+/, 'export default ')
66
+ .replace(/defineNotisApp\s*\(/, '(');
81
67
 
82
- const tmpPath = join(dirname(configPath), '._notis_config_tmp.mjs');
68
+ // Write a temporary .mjs file and import it
69
+ const tmpPath = join(projectDir, '.notis', '_config_tmp.mjs');
83
70
  mkdirSync(dirname(tmpPath), { recursive: true });
84
71
  writeFileSync(tmpPath, jsSource);
85
72
  try {
86
- const mod = await import(`file://${tmpPath}?v=${appConfigImportNonce++}`);
73
+ const mod = await import(`file://${tmpPath}`);
87
74
  return mod.default || mod;
88
- } catch (error) {
89
- if (error instanceof SyntaxError) {
90
- throw usageError(
91
- `Failed to parse ${configPath}. Install project dependencies (including typescript) ` +
92
- 'or convert the config to plain JavaScript.',
93
- );
94
- }
95
- throw error;
96
75
  } finally {
97
76
  try {
98
77
  const { unlinkSync } = await import('node:fs');
@@ -116,10 +95,10 @@ export function detectProjectProblems(projectDir) {
116
95
  problems.push('Missing package.json');
117
96
  }
118
97
 
119
- const hasViteConfig = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']
98
+ const hasNextConfig = ['next.config.ts', 'next.config.mjs', 'next.config.js']
120
99
  .some((name) => existsSync(join(projectDir, name)));
121
- if (!hasViteConfig) {
122
- problems.push('Missing vite.config file');
100
+ if (!hasNextConfig) {
101
+ problems.push('Missing next.config file');
123
102
  }
124
103
 
125
104
  if (!existsSync(join(projectDir, 'app'))) {
@@ -149,7 +128,7 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
149
128
  }
150
129
 
151
130
  if (appConfig && (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)) {
152
- warnings.push('No database references declared in notis.config.ts.');
131
+ warnings.push('No databases declared in notis.config.ts.');
153
132
  }
154
133
 
155
134
  return warnings;
@@ -177,31 +156,6 @@ export async function runProjectScript({ projectDir, scriptName, env = {}, stdio
177
156
  });
178
157
  }
179
158
 
180
- /**
181
- * Derive an export name from a route path.
182
- * '/' -> 'index', '/inbox' -> 'inbox', '/my-tasks' -> 'myTasks'
183
- */
184
- function exportNameFromPath(routePath) {
185
- if (routePath === '/') return 'index';
186
- const slug = routePath.replace(/^\//, '').replace(/\//g, '-');
187
- const identifier = slug
188
- .split(/[^A-Za-z0-9_$]+/)
189
- .filter(Boolean)
190
- .map((part, index) => {
191
- const lower = part.toLowerCase();
192
- if (index === 0) return lower;
193
- return lower.charAt(0).toUpperCase() + lower.slice(1);
194
- })
195
- .join('');
196
- if (!identifier) return 'route';
197
- return /^[A-Za-z_$]/.test(identifier) ? identifier : `r${identifier}`;
198
- }
199
-
200
- function slugFromPath(routePath) {
201
- if (routePath === '/') return 'index';
202
- return routePath.replace(/^\//, '').replace(/\//g, '-');
203
- }
204
-
205
159
  /**
206
160
  * Auto-detect routes from the app/ directory by scanning for page.tsx files.
207
161
  */
@@ -220,13 +174,13 @@ function autoDetectRoutes(projectDir) {
220
174
  scan(join(dir, entry.name), `${pathPrefix}/${entry.name}`);
221
175
  } else if (entry.name === 'page.tsx' || entry.name === 'page.jsx' || entry.name === 'page.js') {
222
176
  const routePath = pathPrefix || '/';
223
- const slug = slugFromPath(routePath);
177
+ const slug = routePath === '/' ? 'index' : routePath.replace(/^\//, '').replace(/\//g, '-');
224
178
  routes.push({
225
179
  path: routePath,
226
180
  slug,
227
181
  name: slug === 'index' ? 'Home' : slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
228
182
  default: routePath === '/',
229
- export_name: exportNameFromPath(routePath),
183
+ entry_html: routePath === '/' ? 'index.html' : `${routePath.replace(/^\//, '')}/index.html`,
230
184
  });
231
185
  }
232
186
  }
@@ -236,109 +190,89 @@ function autoDetectRoutes(projectDir) {
236
190
  return routes;
237
191
  }
238
192
 
239
- function resolveConfiguredRoutes(appConfig, projectDir) {
240
- const configuredRoutes = Array.isArray(appConfig.routes) ? appConfig.routes : [];
241
- const routes = configuredRoutes.map((route) => ({
242
- ...route,
243
- slug: route.slug || slugFromPath(route.path),
244
- export_name: route.exportName || route.export_name || exportNameFromPath(route.path),
245
- }));
246
- return routes.length > 0 ? routes : autoDetectRoutes(projectDir);
193
+ function slugFromPath(routePath) {
194
+ if (routePath === '/') return 'index';
195
+ return routePath.replace(/^\//, '').replace(/\//g, '-');
247
196
  }
248
197
 
249
- /**
250
- * Generate the _entry.tsx file that re-exports each route's page component.
251
- */
252
- function generateEntryFile(projectDir, routes) {
253
- const entryDir = join(projectDir, NOTIS_DIR);
254
- mkdirSync(entryDir, { recursive: true });
255
-
256
- // Also re-export the layout if it exists
257
- const lines = [];
258
- const layoutPath = join(projectDir, 'app', 'layout.tsx');
259
- if (existsSync(layoutPath)) {
260
- lines.push(`export { default as __AppShell } from '../app/layout';`);
261
- }
262
-
263
- for (const route of routes) {
264
- const pagePath = route.path === '/' ? '../app/page' : `../app${route.path}/page`;
265
- lines.push(`export { default as ${route.export_name} } from '${pagePath}';`);
266
- }
267
-
268
- const entryPath = join(entryDir, '_entry.tsx');
269
- writeFileSync(entryPath, lines.join('\n') + '\n');
270
- return entryPath;
198
+ function entryHtmlFromPath(routePath) {
199
+ if (routePath === '/') return 'index.html';
200
+ return `${routePath.replace(/^\//, '')}/index.html`;
271
201
  }
272
202
 
273
203
  /**
274
204
  * Generate the manifest from app config and build output.
275
205
  */
276
206
  function generateManifest(appConfig, projectDir) {
277
- const routes = resolveConfiguredRoutes(appConfig, projectDir).map((route) => {
278
- const entry = {
279
- path: route.path,
280
- slug: route.slug || slugFromPath(route.path),
281
- name: route.name,
282
- icon: route.icon || null,
283
- default: route.default || false,
284
- export_name: route.exportName || route.export_name || exportNameFromPath(route.path),
285
- collection: route.collection || null,
286
- };
287
- if (route.tool_access) {
288
- entry.tool_access = route.tool_access;
289
- }
290
- return entry;
291
- });
207
+ const routes = (appConfig.routes || autoDetectRoutes(projectDir)).map((route) => ({
208
+ path: route.path,
209
+ slug: route.slug || slugFromPath(route.path),
210
+ name: route.name,
211
+ icon: route.icon || null,
212
+ default: route.default || false,
213
+ entry_html: route.entry_html || entryHtmlFromPath(route.path),
214
+ collection: route.collection || null,
215
+ }));
292
216
 
293
- const databases = appConfig.databases || [];
217
+ const databases = (appConfig.databases || []).map((db) => ({
218
+ slug: db.slug,
219
+ title: db.title,
220
+ description: db.description || null,
221
+ icon: db.icon || null,
222
+ properties: (db.properties || []).map((prop) => ({
223
+ name: prop.name,
224
+ type: prop.type,
225
+ description: prop.description || null,
226
+ options: prop.options
227
+ ? prop.options.map((opt) => typeof opt === 'string' ? { name: opt } : opt)
228
+ : null,
229
+ })),
230
+ }));
294
231
 
295
232
  return {
296
233
  version: 1,
297
- spec_version: 3,
234
+ spec_version: 2,
298
235
  app: {
299
236
  name: appConfig.name,
300
237
  description: appConfig.description || null,
301
238
  icon: appConfig.icon || null,
302
239
  },
303
240
  routes,
304
- bundle: {
305
- js: 'bundle/app.js',
306
- css: 'bundle/app.css',
307
- },
308
241
  databases,
309
242
  tools: appConfig.tools || [],
310
243
  };
311
244
  }
312
245
 
313
246
  /**
314
- * Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
247
+ * Build the app artifact: run `next build`, then package into .notis/output/.
315
248
  */
316
249
  export async function buildArtifact(projectDir) {
317
250
  const appConfig = await loadAppConfig(projectDir);
318
251
 
319
- // Auto-detect or use configured routes
320
- const detectedRoutes = resolveConfiguredRoutes(appConfig, projectDir);
321
-
322
- // Generate the entry file that re-exports all route page components
323
- generateEntryFile(projectDir, detectedRoutes);
324
-
325
- // Run Vite build
252
+ // Run the Next.js build
326
253
  await runProjectScript({
327
254
  projectDir,
328
255
  scriptName: 'build',
256
+ env: {
257
+ NOTIS_BUILD: '1',
258
+ NOTIS_APP_BASE_PATH: '/__NOTIS_APP_BASE__',
259
+ NOTIS_ASSET_PREFIX: '/__NOTIS_ASSET_BASE__',
260
+ },
329
261
  });
330
262
 
331
- // Verify output
332
- const bundleDir = join(projectDir, BUNDLE_DIR);
333
- const jsPath = join(bundleDir, 'app.js');
334
- if (!existsSync(jsPath)) {
335
- throw usageError('Vite build did not produce .notis/output/bundle/app.js. Check your vite.config.ts.');
263
+ // Copy the static export into .notis/output/site/
264
+ const nextOutDir = join(projectDir, 'out');
265
+ if (!existsSync(nextOutDir)) {
266
+ throw usageError('Next.js build did not produce an `out/` directory. Ensure `output: "export"` is set (withNotis does this automatically).');
336
267
  }
337
268
 
269
+ const outputSiteDir = join(projectDir, SITE_DIR);
270
+ mkdirSync(outputSiteDir, { recursive: true });
271
+ cpSync(nextOutDir, outputSiteDir, { recursive: true });
272
+
338
273
  // Generate manifest
339
274
  const manifest = generateManifest(appConfig, projectDir);
340
275
  const manifestPath = join(projectDir, MANIFEST_FILE);
341
- mkdirSync(dirname(manifestPath), { recursive: true });
342
276
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
343
277
 
344
278
  return { manifest, outputDir: join(projectDir, OUTPUT_DIR) };
@@ -388,11 +322,11 @@ export function requireLinkedAppId(projectDir, explicitAppId) {
388
322
  export function scaffoldProject({ projectDir, appName }) {
389
323
  const templateDir = resolve(
390
324
  dirname(fileURLToPath(import.meta.url)),
391
- '../../template',
325
+ '../../../../packages/notis-sdk/template',
392
326
  );
393
327
 
394
328
  if (!existsSync(templateDir)) {
395
- throw usageError(`SDK template not found at ${templateDir}. Ensure @notis_ai/cli is installed correctly.`);
329
+ throw usageError(`SDK template not found at ${templateDir}. Ensure @notis/sdk is installed.`);
396
330
  }
397
331
 
398
332
  mkdirSync(projectDir, { recursive: true });
@@ -448,177 +382,3 @@ export function collectArtifactFiles(projectDir) {
448
382
  walk(outputDir, '');
449
383
  return files;
450
384
  }
451
-
452
- // ---------------------------------------------------------------------------
453
- // Direct deploy (bypasses backend server)
454
- // ---------------------------------------------------------------------------
455
-
456
- /**
457
- * Resolve Supabase credentials from the server/.env file in the repo workspace.
458
- * Falls back to environment variables.
459
- */
460
- function resolveSupabaseCredentials() {
461
- const envPaths = [
462
- resolve(process.cwd(), 'server/.env'),
463
- resolve(process.cwd(), '../server/.env'),
464
- resolve(process.cwd(), '../../server/.env'),
465
- ];
466
-
467
- let supabaseUrl = process.env.SUPABASE_URL;
468
- let supabaseSubdomain = process.env.SUPABASE_SUBDOMAIN;
469
- let supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY;
470
-
471
- for (const envPath of envPaths) {
472
- if (existsSync(envPath)) {
473
- const content = readFileSync(envPath, 'utf-8');
474
- for (const line of content.split('\n')) {
475
- const trimmed = line.trim();
476
- if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
477
- const eqIdx = trimmed.indexOf('=');
478
- const key = trimmed.slice(0, eqIdx).trim();
479
- let value = trimmed.slice(eqIdx + 1).trim();
480
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
481
- value = value.slice(1, -1);
482
- }
483
- if (key === 'SUPABASE_URL' && !supabaseUrl) supabaseUrl = value;
484
- if (key === 'SUPABASE_SUBDOMAIN' && !supabaseSubdomain) supabaseSubdomain = value;
485
- if ((key === 'SUPABASE_SERVICE_ROLE_KEY' || key === 'SUPABASE_SERVICE_KEY') && !supabaseKey) supabaseKey = value;
486
- }
487
- break;
488
- }
489
- }
490
-
491
- // Derive URL from subdomain if needed
492
- if (!supabaseUrl && supabaseSubdomain) {
493
- supabaseUrl = `https://${supabaseSubdomain}.supabase.co`;
494
- }
495
-
496
- if (!supabaseUrl || !supabaseKey) {
497
- throw usageError(
498
- 'Cannot resolve Supabase credentials for direct deploy. ' +
499
- 'Ensure server/.env exists with SUPABASE_SUBDOMAIN (or SUPABASE_URL) and SUPABASE_SERVICE_KEY, ' +
500
- 'or set them as environment variables.',
501
- );
502
- }
503
-
504
- return { supabaseUrl, supabaseKey };
505
- }
506
-
507
- /**
508
- * Upload a file to Supabase Storage with upsert.
509
- */
510
- async function uploadToStorage(supabaseUrl, supabaseKey, bucket, storagePath, content, contentType) {
511
- const url = `${supabaseUrl}/storage/v1/object/${bucket}/${storagePath}`;
512
- const response = await fetch(url, {
513
- method: 'POST',
514
- headers: {
515
- 'Authorization': `Bearer ${supabaseKey}`,
516
- 'Content-Type': contentType,
517
- 'x-upsert': 'true',
518
- },
519
- body: content,
520
- });
521
-
522
- if (!response.ok) {
523
- const text = await response.text().catch(() => '');
524
- throw new Error(`Storage upload failed (${response.status}): ${text}`);
525
- }
526
- }
527
-
528
- /**
529
- * Get the current app manifest version from the apps table.
530
- */
531
- async function getAppCurrentVersion(supabaseUrl, supabaseKey, appId) {
532
- const encodedAppId = encodeURIComponent(appId);
533
- const url = `${supabaseUrl}/rest/v1/apps?id=eq.${encodedAppId}&select=manifest`;
534
- const response = await fetch(url, {
535
- headers: {
536
- 'Authorization': `Bearer ${supabaseKey}`,
537
- 'apikey': supabaseKey,
538
- },
539
- });
540
- if (!response.ok) throw new Error(`Failed to get app version: ${response.status}`);
541
- const rows = await response.json();
542
- if (!rows.length) throw usageError(`App ${appId} not found.`);
543
- const manifest = rows[0].manifest || {};
544
- return { currentVersion: manifest.version || 0, manifest };
545
- }
546
-
547
- /**
548
- * Update the app manifest in the apps table.
549
- */
550
- async function updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, manifest) {
551
- const encodedAppId = encodeURIComponent(appId);
552
- const url = `${supabaseUrl}/rest/v1/apps?id=eq.${encodedAppId}`;
553
- const response = await fetch(url, {
554
- method: 'PATCH',
555
- headers: {
556
- 'Authorization': `Bearer ${supabaseKey}`,
557
- 'apikey': supabaseKey,
558
- 'Content-Type': 'application/json',
559
- 'Prefer': 'return=minimal',
560
- },
561
- body: JSON.stringify({
562
- manifest: { ...manifest, version: newVersion },
563
- updated_at: new Date().toISOString(),
564
- }),
565
- });
566
- if (!response.ok) {
567
- const text = await response.text().catch(() => '');
568
- throw new Error(`Failed to update app version: ${response.status} ${text}`);
569
- }
570
- }
571
-
572
- const CONTENT_TYPE_MAP = {
573
- '.js': 'application/javascript',
574
- '.css': 'text/css',
575
- '.json': 'application/json',
576
- '.html': 'text/html',
577
- };
578
-
579
- /**
580
- * Deploy app bundle directly to Supabase storage, bypassing the backend server.
581
- */
582
- export async function directDeploy(projectDir, appId) {
583
- const { supabaseUrl, supabaseKey } = resolveSupabaseCredentials();
584
- const manifest = readManifest(projectDir);
585
-
586
- // Get current version and increment
587
- const { currentVersion } = await getAppCurrentVersion(supabaseUrl, supabaseKey, appId);
588
- const newVersion = currentVersion + 1;
589
-
590
- // Upload all files from the output directory
591
- const outputDir = join(projectDir, OUTPUT_DIR);
592
- const bucket = 'app-code';
593
-
594
- async function uploadDir(dir, prefix) {
595
- const entries = readdirSync(dir, { withFileTypes: true });
596
- for (const entry of entries) {
597
- const fullPath = join(dir, entry.name);
598
- const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
599
- if (entry.isDirectory()) {
600
- await uploadDir(fullPath, relPath);
601
- } else {
602
- const ext = entry.name.includes('.') ? '.' + entry.name.split('.').pop() : '';
603
- const contentType = CONTENT_TYPE_MAP[ext] || 'application/octet-stream';
604
- const content = readFileSync(fullPath);
605
- const storagePath = `${appId}/v${newVersion}/${relPath}`;
606
- await uploadToStorage(supabaseUrl, supabaseKey, bucket, storagePath, content, contentType);
607
- }
608
- }
609
- }
610
-
611
- await uploadDir(outputDir, '');
612
-
613
- // Update the app record -- include storage_prefix so the portal can resolve bundle URLs
614
- const storagePrefix = `${appId}/v${newVersion}/`;
615
- const deployManifest = {
616
- ...manifest,
617
- version: newVersion,
618
- storage_bucket: bucket,
619
- storage_prefix: storagePrefix,
620
- };
621
- await updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, deployManifest);
622
-
623
- return { version: newVersion };
624
- }