@jsonpages/cli 3.0.44 → 3.0.46

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.
@@ -41,6 +41,7 @@ import { addSectionConfig } from '@/lib/addSectionConfig';
41
41
  import { getHydratedData } from '@/lib/draftStorage';
42
42
  import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
43
43
  import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
44
+ import { SaveDrawer } from '@/components/SaveDrawer';
44
45
 
45
46
  import siteData from '@/data/config/site.json';
46
47
  import themeData from '@/data/config/theme.json';
@@ -69,6 +70,7 @@ function getInitialData() {
69
70
  function App() {
70
71
  const [{ pages, siteConfig }] = useState(getInitialData);
71
72
  const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
73
+ const [saveDrawer, setSaveDrawer] = useState<{ state: ProjectState; slug: string } | null>(null);
72
74
 
73
75
  useEffect(() => {
74
76
  // In Cloud mode, listing assets might be different or disabled for MVP
@@ -78,11 +80,7 @@ function App() {
78
80
  .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
79
81
  .catch(() => setAssetsManifest([]));
80
82
  }, []);
81
-
82
- console.log("🔍 DEBUG ENV:", {
83
- URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
84
- KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
85
- });
83
+
86
84
  const config: JsonPagesConfig = {
87
85
  tenantId: TENANT_ID,
88
86
  registry: ComponentRegistry as JsonPagesConfig['registry'],
@@ -96,29 +94,9 @@ console.log("🔍 DEBUG ENV:", {
96
94
  persistence: {
97
95
  async saveToFile(state: ProjectState, slug: string): Promise<void> {
98
96
 
99
- // ☁️ SCENARIO A: CLOUD BRIDGE (Production)
97
+ // ☁️ SCENARIO A: CLOUD BRIDGE (Production) — open SaveDrawer, stream save-stream
100
98
  if (CLOUD_API_URL && CLOUD_API_KEY) {
101
- console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
102
-
103
- const res = await fetch(`${CLOUD_API_URL}/save`, {
104
- method: 'POST',
105
- headers: {
106
- 'Content-Type': 'application/json',
107
- 'Authorization': `Bearer ${CLOUD_API_KEY}`
108
- },
109
- body: JSON.stringify({
110
- // Mapping logical slug to physical path in repo
111
- path: `src/data/pages/${slug}.json`,
112
- // We save the page config specifically
113
- content: state.page,
114
- message: `Content update for ${slug} via Visual Editor`
115
- }),
116
- });
117
-
118
- if (!res.ok) {
119
- const err = await res.json().catch(() => ({}));
120
- throw new Error(err.error || `Cloud save failed: ${res.status}`);
121
- }
99
+ setSaveDrawer({ state, slug });
122
100
  return;
123
101
  }
124
102
 
@@ -164,7 +142,20 @@ console.log("🔍 DEBUG ENV:", {
164
142
  },
165
143
  };
166
144
 
167
- return <JsonPagesEngine config={config} />;
145
+ return (
146
+ <>
147
+ <JsonPagesEngine config={config} />
148
+ {saveDrawer && CLOUD_API_URL && CLOUD_API_KEY && (
149
+ <SaveDrawer
150
+ cloudUrl={CLOUD_API_URL}
151
+ apiKey={CLOUD_API_KEY}
152
+ state={saveDrawer.state}
153
+ slug={saveDrawer.slug}
154
+ onClose={() => setSaveDrawer(null)}
155
+ />
156
+ )}
157
+ </>
158
+ );
168
159
  }
169
160
 
170
161
  export default App;
@@ -301,6 +292,279 @@ export const NotFound: React.FC = () => {
301
292
 
302
293
 
303
294
 
295
+ END_OF_FILE_CONTENT
296
+ mkdir -p "src/components/SaveDrawer"
297
+ echo "Creating src/components/SaveDrawer/SaveDrawer.tsx..."
298
+ cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/SaveDrawer.tsx"
299
+ /**
300
+ * SaveDrawer — Cloud save flow with real SSE (commit → push → build → live).
301
+ * Opens when user saves in Cloud mode; consumes POST save-stream and shows
302
+ * the four steps. On done: "Refresh page" + "Close" only (no redirect).
303
+ */
304
+ import { useState, useEffect, useCallback } from 'react';
305
+ import type { ProjectState } from '@jsonpages/core';
306
+
307
+ const SAVE_STEPS = [
308
+ { id: 'commit' as const, label: 'Commit' },
309
+ { id: 'push' as const, label: 'Push' },
310
+ { id: 'build' as const, label: 'Build' },
311
+ { id: 'live' as const, label: 'Live' },
312
+ ];
313
+
314
+ function parseSSE(buffer: string): { event: string; data: string }[] {
315
+ const lines = buffer.split('\n');
316
+ const out: { event: string; data: string }[] = [];
317
+ let event = 'message';
318
+ let data = '';
319
+ for (const line of lines) {
320
+ if (line.startsWith('event:')) event = line.slice(6).trim();
321
+ else if (line.startsWith('data:')) data = line.slice(5).trim();
322
+ else if (line === '' && data) {
323
+ out.push({ event, data });
324
+ data = '';
325
+ event = 'message';
326
+ }
327
+ }
328
+ if (data) out.push({ event, data });
329
+ return out;
330
+ }
331
+
332
+ export interface SaveDrawerProps {
333
+ cloudUrl: string;
334
+ apiKey: string;
335
+ state: ProjectState;
336
+ slug: string;
337
+ onClose: () => void;
338
+ }
339
+
340
+ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawerProps) {
341
+ const [completedSteps, setCompletedSteps] = useState<string[]>([]);
342
+ const [activeStepId, setActiveStepId] = useState<string | null>('commit');
343
+ const [logs, setLogs] = useState<Record<string, string[]>>({});
344
+ const [phase, setPhase] = useState<'running' | 'done' | 'error'>('running');
345
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
346
+
347
+ const addLog = useCallback((stepId: string, message: string) => {
348
+ setLogs((prev) => ({ ...prev, [stepId]: [...(prev[stepId] || []), message] }));
349
+ }, []);
350
+
351
+ useEffect(() => {
352
+ let cancelled = false;
353
+ let gotDone = false;
354
+
355
+ const run = async () => {
356
+ const url = `${cloudUrl.replace(/\/$/, '')}/save-stream`;
357
+ const res = await fetch(url, {
358
+ method: 'POST',
359
+ headers: {
360
+ 'Content-Type': 'application/json',
361
+ Authorization: `Bearer ${apiKey}`,
362
+ },
363
+ body: JSON.stringify({
364
+ path: `src/data/pages/${slug}.json`,
365
+ content: state.page,
366
+ message: `Content update for ${slug} via Visual Editor`,
367
+ }),
368
+ });
369
+
370
+ if (!res.ok || !res.body) {
371
+ const data = await res.json().catch(() => ({}));
372
+ setErrorMessage((data as { error?: string }).error || `Request failed: ${res.status}`);
373
+ setPhase('error');
374
+ return;
375
+ }
376
+
377
+ const reader = res.body.getReader();
378
+ const dec = new TextDecoder();
379
+ let buffer = '';
380
+
381
+ while (true) {
382
+ const { value, done: streamDone } = await reader.read();
383
+ if (streamDone || cancelled) break;
384
+ buffer += dec.decode(value, { stream: true });
385
+ const parts = buffer.split('\n\n');
386
+ buffer = parts.pop() || '';
387
+
388
+ for (const part of parts) {
389
+ const parsed = parseSSE(part + '\n\n');
390
+ for (const { event: ev, data: raw } of parsed) {
391
+ if (cancelled) return;
392
+ try {
393
+ const data = JSON.parse(raw) as Record<string, unknown>;
394
+ if (ev === 'step') {
395
+ const id = data.id as string | undefined;
396
+ const status = data.status as string | undefined;
397
+ if (id && status === 'running') setActiveStepId(id);
398
+ if (id && status === 'done') {
399
+ setCompletedSteps((prev) => [...prev, id].filter(Boolean));
400
+ setActiveStepId(null);
401
+ }
402
+ } else if (ev === 'log' && data.stepId && data.message) {
403
+ addLog(data.stepId as string, data.message as string);
404
+ } else if (ev === 'error') {
405
+ setErrorMessage((data.message as string) || 'Error');
406
+ setPhase('error');
407
+ return;
408
+ } else if (ev === 'done') {
409
+ gotDone = true;
410
+ setCompletedSteps((prev) => [...new Set([...prev, 'commit', 'push', 'build', 'live'])]);
411
+ setActiveStepId(null);
412
+ setPhase('done');
413
+ return;
414
+ }
415
+ } catch {
416
+ // ignore parse errors
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ if (!cancelled && !gotDone) {
423
+ setErrorMessage('Connection closed before completion');
424
+ setPhase('error');
425
+ }
426
+ };
427
+
428
+ run();
429
+ return () => {
430
+ cancelled = true;
431
+ };
432
+ }, [cloudUrl, apiKey, state.page, slug, addLog]);
433
+
434
+ const progressPct =
435
+ phase === 'done' ? 100 : SAVE_STEPS.length > 0 ? Math.round((completedSteps.length / SAVE_STEPS.length) * 100) : 0;
436
+
437
+ const stepState = (id: string) => {
438
+ if (completedSteps.includes(id)) return 'done';
439
+ if (activeStepId === id) return 'active';
440
+ return 'pending';
441
+ };
442
+
443
+ return (
444
+ <>
445
+ <div
446
+ className="fixed inset-0 z-40 bg-black/40 backdrop-blur-[2px]"
447
+ onClick={phase === 'done' || phase === 'error' ? onClose : undefined}
448
+ aria-hidden
449
+ />
450
+ <div
451
+ role="status"
452
+ aria-live="polite"
453
+ className="fixed inset-x-0 bottom-0 z-50 flex justify-center pb-6 px-4"
454
+ aria-label={phase === 'done' ? 'Save completed' : phase === 'error' ? 'Save failed' : 'Saving'}
455
+ >
456
+ <div
457
+ className="relative w-full max-w-2xl overflow-hidden rounded-2xl border border-white/[0.07] bg-[hsl(222,18%,7%)] shadow-lg"
458
+ onClick={(e) => e.stopPropagation()}
459
+ >
460
+ <div className="relative z-10 px-6 pt-6 pb-5">
461
+ <div className="flex items-center justify-between mb-4">
462
+ <h2 className="text-lg font-semibold text-white">
463
+ {phase === 'done' ? 'Content is live' : phase === 'error' ? 'Save failed' : 'Saving to Cloud'}
464
+ </h2>
465
+ {phase === 'running' && (
466
+ <span className="text-xs font-mono text-white/50">{completedSteps.length} / {SAVE_STEPS.length}</span>
467
+ )}
468
+ </div>
469
+
470
+ {phase === 'error' && errorMessage && (
471
+ <p className="text-sm text-red-400 mb-4">{errorMessage}</p>
472
+ )}
473
+
474
+ <div className="flex items-center gap-2 mb-4">
475
+ {SAVE_STEPS.map((step, i) => (
476
+ <div key={step.id} className="flex items-center flex-1">
477
+ <div
478
+ className={`flex items-center justify-center w-8 h-8 rounded-full border-2 shrink-0 ${
479
+ stepState(step.id) === 'done'
480
+ ? 'bg-emerald-500/20 border-emerald-500'
481
+ : stepState(step.id) === 'active'
482
+ ? 'bg-blue-500/20 border-blue-500'
483
+ : 'bg-white/10 border-white/20'
484
+ }`}
485
+ >
486
+ {stepState(step.id) === 'done' ? (
487
+ <span className="text-emerald-400" aria-hidden>✓</span>
488
+ ) : stepState(step.id) === 'active' ? (
489
+ <span className="w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" aria-hidden />
490
+ ) : (
491
+ <span className="text-xs text-white/40">{i + 1}</span>
492
+ )}
493
+ </div>
494
+ <span
495
+ className={`ml-2 text-xs font-medium truncate ${
496
+ stepState(step.id) !== 'pending' ? 'text-white' : 'text-white/40'
497
+ }`}
498
+ >
499
+ {step.label}
500
+ </span>
501
+ {i < SAVE_STEPS.length - 1 && (
502
+ <div
503
+ className={`flex-1 h-0.5 mx-2 min-w-[8px] ${
504
+ stepState(step.id) === 'done' ? 'bg-emerald-500/50' : 'bg-white/10'
505
+ }`}
506
+ />
507
+ )}
508
+ </div>
509
+ ))}
510
+ </div>
511
+
512
+ <div className="h-1 bg-white/10 rounded-full overflow-hidden mb-4">
513
+ <div
514
+ className="h-full bg-emerald-500 rounded-full transition-all duration-300"
515
+ style={{ width: `${progressPct}%` }}
516
+ />
517
+ </div>
518
+
519
+ {Object.keys(logs).length > 0 && (
520
+ <div className="mb-4 p-2 rounded bg-black/30 border border-white/10 font-mono text-xs text-white/60 space-y-0.5 max-h-20 overflow-y-auto">
521
+ {(logs[activeStepId || ''] || []).slice(-3).map((line, i) => (
522
+ <div key={i}>$ {line}</div>
523
+ ))}
524
+ </div>
525
+ )}
526
+
527
+ <div className="flex items-center gap-2 justify-end">
528
+ {phase === 'done' && (
529
+ <>
530
+ <button
531
+ type="button"
532
+ onClick={() => window.location.reload()}
533
+ className="px-4 py-2 rounded-lg bg-emerald-500 text-zinc-900 font-medium text-sm hover:bg-emerald-400"
534
+ >
535
+ Refresh page
536
+ </button>
537
+ <button
538
+ type="button"
539
+ onClick={onClose}
540
+ className="px-4 py-2 rounded-lg border border-white/20 text-white/90 text-sm hover:bg-white/10"
541
+ >
542
+ Close
543
+ </button>
544
+ </>
545
+ )}
546
+ {phase === 'error' && (
547
+ <button
548
+ type="button"
549
+ onClick={onClose}
550
+ className="px-4 py-2 rounded-lg border border-white/20 text-white/90 text-sm hover:bg-white/10"
551
+ >
552
+ Close
553
+ </button>
554
+ )}
555
+ </div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+ </>
560
+ );
561
+ }
562
+
563
+ END_OF_FILE_CONTENT
564
+ echo "Creating src/components/SaveDrawer/index.ts..."
565
+ cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/index.ts"
566
+ export { SaveDrawer } from './SaveDrawer';
567
+
304
568
  END_OF_FILE_CONTENT
305
569
  mkdir -p "src/components/arch-layers"
306
570
  echo "Creating src/components/arch-layers/View.tsx..."
@@ -0,0 +1 @@
1
+ {"rewrites":[{"source":"/(.*)","destination":"/index.html"}],"headers":[{"source":"/assets/(.*)","headers":[{"key":"Cache-Control","value":"public, max-age=31536000, immutable"}]}]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonpages/cli",
3
- "version": "3.0.44",
3
+ "version": "3.0.46",
4
4
  "description": "The Sovereign CLI Engine for JsonPages.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "src",
11
- "assets/src_tenant_alpha.sh"
11
+ "assets/src_tenant_alpha.sh",
12
+ "assets/vercel.json"
12
13
  ],
13
14
  "author": "JsonPages Team",
14
15
  "license": "MIT",
package/src/index.js CHANGED
@@ -335,6 +335,12 @@ async function injectInfraFiles(targetDir, name) {
335
335
  }
336
336
  };
337
337
  await fs.writeJson(path.join(targetDir, 'components.json'), shadcnConfig, { spaces: 2 });
338
+
339
+ // Vercel deploy config (SPA rewrites + asset cache)
340
+ const vercelJsonPath = path.resolve(__dirname, '../assets/vercel.json');
341
+ if (fs.existsSync(vercelJsonPath)) {
342
+ await fs.copy(vercelJsonPath, path.join(targetDir, 'vercel.json'));
343
+ }
338
344
  }
339
345
 
340
346
  program.parse();