@jsonpages/cli 3.0.47 → 3.0.49

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.
@@ -439,13 +439,14 @@ END_OF_FILE_CONTENT
439
439
  echo "Creating src/components/SaveDrawer/SaveDrawer.tsx..."
440
440
  cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/SaveDrawer.tsx"
441
441
  /**
442
- * SaveDrawer — Identical to prototype DopaDrawer.
442
+ * SaveDrawer — Identical to prototype DopaDrawer (dopa-save).
443
443
  * Bottom sheet for Cloud save; driven by real SSE (save-stream).
444
- * Done: "Refresh page" + "Chiudi". Error: "Annulla" + "Retry".
444
+ * Done: countdown 3s + "Chiusura in Xs" + Chiudi + Open site → auto-close. Error: Annulla + Retry.
445
445
  */
446
446
  import { useState } from 'react';
447
447
  import { cn } from '@/lib/utils';
448
448
  import { Separator } from '@/components/ui/separator';
449
+ import { Progress } from '@/components/ui/progress';
449
450
  import { DeployNode } from './DeployNode';
450
451
  import { DeployConnector } from './DeployConnector';
451
452
  import { Particles, BuildBars, SuccessBurst, ElapsedTimer } from './Visuals';
@@ -464,7 +465,18 @@ export interface SaveDrawerProps {
464
465
  export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawerProps) {
465
466
  const [burst, setBurst] = useState(false);
466
467
 
467
- const { phase, currentStep, stepState, doneSteps, progress, retry } = useSaveStream({
468
+ const {
469
+ phase,
470
+ currentStep,
471
+ stepState,
472
+ doneSteps,
473
+ progress,
474
+ deployUrl,
475
+ countdown,
476
+ logs,
477
+ errorMessage,
478
+ retry,
479
+ } = useSaveStream({
468
480
  cloudUrl,
469
481
  apiKey,
470
482
  state,
@@ -567,8 +579,12 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
567
579
  </div>
568
580
  ) : isError ? (
569
581
  <>
570
- <p className="text-base font-semibold text-white leading-snug">Deploy failed at build.</p>
571
- <p className="text-sm text-white/35 mt-0.5">Check your Vercel logs or retry below</p>
582
+ <p className="text-base font-semibold text-white leading-snug">
583
+ Deploy failed at build.
584
+ </p>
585
+ <p className="text-sm text-white/35 mt-0.5">
586
+ {errorMessage ?? 'Check your Vercel logs or retry below'}
587
+ </p>
572
588
  </>
573
589
  ) : currentStep ? (
574
590
  <>
@@ -580,7 +596,24 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
580
596
  </div>
581
597
 
582
598
  <div className="flex flex-col items-end gap-1.5 shrink-0 ml-6">
583
- {!isDone && <ElapsedTimer running={isRunning} />}
599
+ {isDone ? (
600
+ <div className="flex flex-col items-end gap-1.5 save-fade-up">
601
+ <span
602
+ className="font-mono text-xs text-emerald-400 font-semibold"
603
+ aria-live="polite"
604
+ >
605
+ Chiusura in {countdown}s
606
+ </span>
607
+ <div className="w-24 h-0.5 bg-white/10 rounded-full overflow-hidden">
608
+ <div
609
+ className="h-full bg-emerald-400 rounded-full countdown-bar"
610
+ style={{ boxShadow: '0 0 6px #34d39988' }}
611
+ />
612
+ </div>
613
+ </div>
614
+ ) : (
615
+ <ElapsedTimer running={isRunning} />
616
+ )}
584
617
  </div>
585
618
  </div>
586
619
 
@@ -608,12 +641,21 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
608
641
 
609
642
  <Separator className="mb-5 bg-white/[0.06]" />
610
643
 
644
+ {isRunning && currentStep && (logs[currentStep.id]?.length ?? 0) > 0 && (
645
+ <div className="mb-4 max-h-16 overflow-y-auto rounded-md bg-white/[0.04] px-3 py-2 font-mono text-[11px] text-white/50 leading-relaxed">
646
+ {logs[currentStep.id]?.map((line, i) => (
647
+ <div key={i}>{line}</div>
648
+ ))}
649
+ </div>
650
+ )}
651
+
611
652
  <div className="flex items-center gap-4">
612
- <div className="flex-1 h-[2px] bg-white/[0.06] rounded-full overflow-hidden">
613
- <div
614
- className="h-full rounded-full transition-all duration-300"
615
- style={{
616
- width: `${progress}%`,
653
+ <div className="flex-1">
654
+ <Progress
655
+ value={progress}
656
+ className="h-[2px] bg-white/[0.06]"
657
+ indicatorClassName="transition-none"
658
+ indicatorStyle={{
617
659
  background: `linear-gradient(90deg, ${DEPLOY_STEPS[0].color}, ${activeColor})`,
618
660
  }}
619
661
  />
@@ -624,17 +666,19 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
624
666
  <div className="flex gap-2 save-fade-up">
625
667
  <button
626
668
  type="button"
627
- onClick={() => window.location.reload()}
628
- className="px-3 py-1.5 rounded-md bg-emerald-400 text-zinc-900 text-sm font-semibold hover:bg-emerald-300"
669
+ onClick={onClose}
670
+ className="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium border border-white/20 bg-white/5 text-white/90 hover:bg-white/10 transition-colors"
629
671
  >
630
- Refresh page
672
+ Chiudi
631
673
  </button>
632
674
  <button
633
675
  type="button"
634
- onClick={onClose}
635
- className="px-3 py-1.5 rounded-md border border-white/20 text-white/90 text-sm hover:bg-white/10"
676
+ onClick={() =>
677
+ deployUrl ? window.open(deployUrl, '_blank') : window.location.reload()
678
+ }
679
+ className="h-9 rounded-md px-3 inline-flex items-center justify-center gap-1.5 text-sm font-semibold bg-emerald-400 text-zinc-900 hover:bg-emerald-300 transition-colors"
636
680
  >
637
- Chiudi
681
+ <span aria-hidden>↗</span> Open site
638
682
  </button>
639
683
  </div>
640
684
  )}
@@ -644,14 +688,14 @@ export function SaveDrawer({ cloudUrl, apiKey, state, slug, onClose }: SaveDrawe
644
688
  <button
645
689
  type="button"
646
690
  onClick={onClose}
647
- className="px-3 py-1.5 rounded-md text-white/80 text-sm hover:bg-white/10"
691
+ className="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium text-white/80 hover:bg-white/10 transition-colors"
648
692
  >
649
693
  Annulla
650
694
  </button>
651
695
  <button
652
696
  type="button"
653
697
  onClick={retry}
654
- className="px-3 py-1.5 rounded-md bg-red-500/90 text-white text-sm font-medium hover:bg-red-500"
698
+ className="h-9 rounded-md px-3 inline-flex items-center justify-center text-sm font-medium bg-red-600 text-white hover:bg-red-500 transition-colors"
655
699
  >
656
700
  Retry
657
701
  </button>
@@ -875,7 +919,9 @@ END_OF_FILE_CONTENT
875
919
  echo "Creating src/components/SaveDrawer/useSaveStream.ts..."
876
920
  cat << 'END_OF_FILE_CONTENT' > "src/components/SaveDrawer/useSaveStream.ts"
877
921
  /**
878
- * useSaveStream — Same API as prototype useSaveOrchestrator, but driven by real SSE from save-stream.
922
+ * useSaveStream — Same API as prototype useSaveOrchestrator, driven by SSE from save-stream.
923
+ * Event handling aligned with CreateTenantFlow (parseSSE, step/log/error/done, post-loop, try/catch).
924
+ * Payload is read from a ref at effect start so the effect does not depend on state.page (avoids re-run and stream kill).
879
925
  */
880
926
  import { useState, useCallback, useEffect, useRef } from 'react';
881
927
  import type { ProjectState } from '@jsonpages/core';
@@ -913,15 +959,25 @@ export function useSaveStream({
913
959
  apiKey,
914
960
  state,
915
961
  slug,
916
- onClose: _onClose,
962
+ onClose,
917
963
  }: UseSaveStreamOptions) {
918
964
  const [phase, setPhase] = useState<DeployPhase>('running');
919
965
  const [doneSteps, setDoneSteps] = useState<StepId[]>([]);
920
966
  const [activeStepId, setActiveStepId] = useState<StepId | null>('commit');
921
967
  const [progress, setProgress] = useState(0);
968
+ const [deployUrl, setDeployUrl] = useState<string | null>(null);
969
+ const [countdown, setCountdown] = useState(3);
970
+ const [logs, setLogs] = useState<Record<string, string[]>>({});
971
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
922
972
  const progressRef = useRef(0);
923
973
  const [runId, setRunId] = useState(0);
924
974
 
975
+ const payloadRef = useRef<{ path: string; content: unknown; message: string } | null>(null);
976
+
977
+ const addLog = useCallback((stepId: string, message: string) => {
978
+ setLogs((prev) => ({ ...prev, [stepId]: [...(prev[stepId] || []), message] }));
979
+ }, []);
980
+
925
981
  const stepState = useCallback(
926
982
  (i: number): StepState => {
927
983
  const step = DEPLOY_STEPS[i];
@@ -937,81 +993,116 @@ export function useSaveStream({
937
993
 
938
994
  useEffect(() => {
939
995
  let cancelled = false;
996
+ let gotDone = false;
940
997
  setPhase('running');
941
998
  setDoneSteps([]);
942
999
  setActiveStepId('commit');
943
1000
  setProgress(0);
1001
+ setDeployUrl(null);
1002
+ setCountdown(3);
1003
+ setLogs({});
1004
+ setErrorMessage(null);
944
1005
  progressRef.current = 0;
945
1006
 
946
- const url = `${cloudUrl.replace(/\/$/, '')}/save-stream`;
1007
+ payloadRef.current = {
1008
+ path: `src/data/pages/${slug}.json`,
1009
+ content: state.page,
1010
+ message: `Content update for ${slug} via Visual Editor`,
1011
+ };
1012
+
1013
+ const base = cloudUrl.replace(/\/$/, '');
1014
+ const url = base.includes('/api/v1')
1015
+ ? `${base}/save-stream`
1016
+ : `${base}/api/v1/save-stream`;
947
1017
 
948
1018
  const run = async () => {
949
- const res = await fetch(url, {
950
- method: 'POST',
951
- headers: {
952
- 'Content-Type': 'application/json',
953
- Authorization: `Bearer ${apiKey}`,
954
- },
955
- body: JSON.stringify({
956
- path: `src/data/pages/${slug}.json`,
957
- content: state.page,
958
- message: `Content update for ${slug} via Visual Editor`,
959
- }),
960
- });
1019
+ try {
1020
+ const payload = payloadRef.current;
1021
+ if (!payload || cancelled) return;
961
1022
 
962
- if (!res.ok || !res.body) {
963
- if (!cancelled) setPhase('error');
964
- return;
965
- }
1023
+ const res = await fetch(url, {
1024
+ method: 'POST',
1025
+ headers: {
1026
+ 'Content-Type': 'application/json',
1027
+ Authorization: `Bearer ${apiKey}`,
1028
+ },
1029
+ body: JSON.stringify(payload),
1030
+ });
966
1031
 
967
- const reader = res.body.getReader();
968
- const dec = new TextDecoder();
969
- let buffer = '';
1032
+ if (!res.ok || !res.body) {
1033
+ if (!cancelled) {
1034
+ setPhase('error');
1035
+ setErrorMessage(res.status ? `HTTP ${res.status}` : 'Request failed');
1036
+ }
1037
+ return;
1038
+ }
970
1039
 
971
- while (true) {
972
- const { value, done: streamDone } = await reader.read();
973
- if (streamDone || cancelled) break;
974
- buffer += dec.decode(value, { stream: true });
975
- const parts = buffer.split('\n\n');
976
- buffer = parts.pop() || '';
1040
+ const reader = res.body.getReader();
1041
+ const dec = new TextDecoder();
1042
+ let buffer = '';
977
1043
 
978
- for (const part of parts) {
979
- const parsed = parseSSE(part + '\n\n');
980
- for (const { event: ev, data: raw } of parsed) {
981
- if (cancelled) return;
982
- try {
983
- const data = JSON.parse(raw) as Record<string, unknown>;
984
- if (ev === 'step') {
985
- const id = data.id as StepId | undefined;
986
- const status = data.status as string | undefined;
987
- if (id && status === 'running') {
988
- setActiveStepId(id);
989
- const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
990
- if (idx >= 0) progressRef.current = Math.round((idx / DEPLOY_STEPS.length) * 100);
991
- setProgress(progressRef.current);
992
- }
993
- if (id && status === 'done') {
994
- setDoneSteps((prev) => [...prev, id].filter(Boolean));
1044
+ while (true) {
1045
+ const { value, done: streamDone } = await reader.read();
1046
+ if (streamDone || cancelled) break;
1047
+ buffer += dec.decode(value, { stream: true });
1048
+ const parts = buffer.split('\n\n');
1049
+ buffer = parts.pop() || '';
1050
+
1051
+ for (const part of parts) {
1052
+ const parsed = parseSSE(part + '\n\n');
1053
+ for (const { event: ev, data: raw } of parsed) {
1054
+ if (cancelled) return;
1055
+ try {
1056
+ const data = JSON.parse(raw) as Record<string, unknown>;
1057
+ if (ev === 'step') {
1058
+ const id = data.id as StepId | undefined;
1059
+ const status = data.status as string | undefined;
1060
+ if (id && status === 'running') {
1061
+ setActiveStepId(id);
1062
+ const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
1063
+ if (idx >= 0) progressRef.current = Math.round((idx / DEPLOY_STEPS.length) * 100);
1064
+ setProgress(progressRef.current);
1065
+ }
1066
+ if (id && status === 'done') {
1067
+ setDoneSteps((prev) => [...prev, id].filter(Boolean));
1068
+ setActiveStepId(null);
1069
+ const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
1070
+ progressRef.current = idx >= 0 ? Math.round(((idx + 1) / DEPLOY_STEPS.length) * 100) : 100;
1071
+ setProgress(progressRef.current);
1072
+ }
1073
+ } else if (ev === 'log' && data.stepId && data.message) {
1074
+ addLog(data.stepId as string, data.message as string);
1075
+ } else if (ev === 'error') {
1076
+ if (!cancelled) {
1077
+ setErrorMessage((data.message as string) || 'Errore');
1078
+ setPhase('error');
1079
+ }
1080
+ return;
1081
+ } else if (ev === 'done') {
1082
+ gotDone = true;
1083
+ setDeployUrl((data.deployUrl as string) ?? null);
1084
+ setProgress(100);
1085
+ setDoneSteps(['commit', 'push', 'build', 'live']);
995
1086
  setActiveStepId(null);
996
- const idx = DEPLOY_STEPS.findIndex((s) => s.id === id);
997
- progressRef.current = idx >= 0 ? Math.round(((idx + 1) / DEPLOY_STEPS.length) * 100) : 100;
998
- setProgress(progressRef.current);
1087
+ setPhase('done');
1088
+ return;
999
1089
  }
1000
- } else if (ev === 'error') {
1001
- setPhase('error');
1002
- return;
1003
- } else if (ev === 'done') {
1004
- setProgress(100);
1005
- setDoneSteps(['commit', 'push', 'build', 'live']);
1006
- setActiveStepId(null);
1007
- setPhase('done');
1008
- return;
1090
+ } catch {
1091
+ // ignore parse errors
1009
1092
  }
1010
- } catch {
1011
- // ignore
1012
1093
  }
1013
1094
  }
1014
1095
  }
1096
+
1097
+ if (!cancelled && !gotDone) {
1098
+ setErrorMessage('Operazione interrotta o build non completato.');
1099
+ setPhase('error');
1100
+ }
1101
+ } catch (e) {
1102
+ if (!cancelled) {
1103
+ setErrorMessage(e instanceof Error ? e.message : 'Errore');
1104
+ setPhase('error');
1105
+ }
1015
1106
  }
1016
1107
  };
1017
1108
 
@@ -1019,7 +1110,23 @@ export function useSaveStream({
1019
1110
  return () => {
1020
1111
  cancelled = true;
1021
1112
  };
1022
- }, [cloudUrl, apiKey, slug, state.page, runId]);
1113
+ }, [cloudUrl, apiKey, slug, runId, addLog]);
1114
+
1115
+ useEffect(() => {
1116
+ if (phase !== 'done') return;
1117
+ setCountdown(3);
1118
+ const iv = setInterval(() => {
1119
+ setCountdown((s) => {
1120
+ if (s <= 1) {
1121
+ clearInterval(iv);
1122
+ onClose();
1123
+ return 0;
1124
+ }
1125
+ return s - 1;
1126
+ });
1127
+ }, 1000);
1128
+ return () => clearInterval(iv);
1129
+ }, [phase, onClose]);
1023
1130
 
1024
1131
  const retry = useCallback(() => setRunId((r) => r + 1), []);
1025
1132
 
@@ -1029,6 +1136,10 @@ export function useSaveStream({
1029
1136
  stepState,
1030
1137
  doneSteps,
1031
1138
  progress,
1139
+ deployUrl,
1140
+ countdown,
1141
+ logs,
1142
+ errorMessage,
1032
1143
  retry,
1033
1144
  };
1034
1145
  }
@@ -3871,6 +3982,47 @@ export { Label }
3871
3982
 
3872
3983
 
3873
3984
 
3985
+ END_OF_FILE_CONTENT
3986
+ echo "Creating src/components/ui/progress.tsx..."
3987
+ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/progress.tsx"
3988
+ import * as React from 'react';
3989
+ import { cn } from '@/lib/utils';
3990
+
3991
+ interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
3992
+ value?: number | null;
3993
+ indicatorClassName?: string;
3994
+ indicatorStyle?: React.CSSProperties;
3995
+ }
3996
+
3997
+ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
3998
+ ({ className, value, indicatorClassName, indicatorStyle, ...props }, ref) => (
3999
+ <div
4000
+ ref={ref}
4001
+ role="progressbar"
4002
+ aria-valuenow={value ?? 0}
4003
+ aria-valuemin={0}
4004
+ aria-valuemax={100}
4005
+ className={cn('relative h-2 w-full overflow-hidden rounded-full bg-secondary', className)}
4006
+ {...props}
4007
+ >
4008
+ <div
4009
+ className={cn(
4010
+ 'h-full min-w-full bg-primary transition-all duration-700 ease-out',
4011
+ indicatorClassName
4012
+ )}
4013
+ style={{
4014
+ width: '100%',
4015
+ transform: `translateX(-${100 - (value ?? 0)}%)`,
4016
+ ...indicatorStyle,
4017
+ }}
4018
+ />
4019
+ </div>
4020
+ )
4021
+ );
4022
+ Progress.displayName = 'Progress';
4023
+
4024
+ export { Progress };
4025
+
3874
4026
  END_OF_FILE_CONTENT
3875
4027
  echo "Creating src/components/ui/select.tsx..."
3876
4028
  cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
@@ -5340,6 +5492,15 @@ html {
5340
5492
  to { stroke-dashoffset: 0; }
5341
5493
  }
5342
5494
 
5495
+ /* Countdown drain — 3s linear (dopa-save identical) */
5496
+ .countdown-bar {
5497
+ animation: countdown-drain 3s linear forwards;
5498
+ }
5499
+ @keyframes countdown-drain {
5500
+ from { width: 100%; }
5501
+ to { width: 0%; }
5502
+ }
5503
+
5343
5504
 
5344
5505
  END_OF_FILE_CONTENT
5345
5506
  mkdir -p "src/lib"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonpages/cli",
3
- "version": "3.0.47",
3
+ "version": "3.0.49",
4
4
  "description": "The Sovereign CLI Engine for JsonPages.",
5
5
  "type": "module",
6
6
  "bin": {