veryfront 0.0.83 → 0.0.86

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 (41) hide show
  1. package/README.md +21 -60
  2. package/esm/deno.js +1 -1
  3. package/esm/src/cli/app/components/list-select.js +2 -2
  4. package/esm/src/cli/app/index.d.ts +1 -1
  5. package/esm/src/cli/app/index.d.ts.map +1 -1
  6. package/esm/src/cli/app/index.js +26 -33
  7. package/esm/src/cli/app/state.d.ts +3 -0
  8. package/esm/src/cli/app/state.d.ts.map +1 -1
  9. package/esm/src/cli/app/state.js +4 -0
  10. package/esm/src/cli/app/views/dashboard.d.ts.map +1 -1
  11. package/esm/src/cli/app/views/dashboard.js +46 -58
  12. package/esm/src/cli/app/views/startup.d.ts +39 -0
  13. package/esm/src/cli/app/views/startup.d.ts.map +1 -0
  14. package/esm/src/cli/app/views/startup.js +103 -0
  15. package/esm/src/cli/commands/dev.js +2 -2
  16. package/esm/src/cli/commands/new.js +1 -1
  17. package/esm/src/cli/ui/colors.d.ts +8 -0
  18. package/esm/src/cli/ui/colors.d.ts.map +1 -1
  19. package/esm/src/cli/ui/colors.js +34 -0
  20. package/esm/src/cli/ui/dot-matrix.d.ts +8 -0
  21. package/esm/src/cli/ui/dot-matrix.d.ts.map +1 -1
  22. package/esm/src/cli/ui/dot-matrix.js +67 -2
  23. package/esm/src/cli/ui/tui.js +1 -1
  24. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  25. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +28 -6
  26. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  27. package/esm/src/transforms/esm/http-cache.js +25 -17
  28. package/package.json +1 -1
  29. package/src/deno.js +1 -1
  30. package/src/src/cli/app/components/list-select.ts +2 -2
  31. package/src/src/cli/app/index.ts +34 -35
  32. package/src/src/cli/app/state.ts +7 -0
  33. package/src/src/cli/app/views/dashboard.ts +47 -61
  34. package/src/src/cli/app/views/startup.ts +132 -0
  35. package/src/src/cli/commands/dev.ts +2 -2
  36. package/src/src/cli/commands/new.ts +1 -1
  37. package/src/src/cli/ui/colors.ts +37 -0
  38. package/src/src/cli/ui/dot-matrix.ts +77 -1
  39. package/src/src/cli/ui/tui.ts +1 -1
  40. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +43 -30
  41. package/src/src/transforms/esm/http-cache.ts +26 -17
@@ -157,8 +157,8 @@ export function devCommand(options) {
157
157
  const serverUrl = `http://veryfront.me:${finalPort}`;
158
158
  console.log();
159
159
  console.log(banner({
160
- title: "Veryfront",
161
- subtitle: "is now running",
160
+ title: "Veryfront Code",
161
+ subtitle: "is running",
162
162
  info: {
163
163
  url: serverUrl,
164
164
  ...(projectSlug ? { project: projectSlug } : {}),
@@ -102,7 +102,7 @@ export async function newCommand(name, options = {}, env = getRuntimeEnv()) {
102
102
  exitProcess(1);
103
103
  return;
104
104
  }
105
- const tui = createTui({ title: "Veryfront", showLogs: true });
105
+ const tui = createTui({ title: "Veryfront Code", showLogs: true });
106
106
  const restore = interceptConsole(tui);
107
107
  const localUrl = `http://${name}.veryfront.me:${port}`;
108
108
  const prodUrl = `https://${slug}.veryfront.com`;
@@ -20,4 +20,12 @@ export declare const successBold: (text: string) => string;
20
20
  export declare const errorBold: (text: string) => string;
21
21
  export { RESET as reset };
22
22
  export declare function animatedMatrix(frame: number): string;
23
+ /**
24
+ * Apply shimmer effect to text - creates a wave of brightness moving across
25
+ * @param text The text to shimmer
26
+ * @param frame Current animation frame (increments over time)
27
+ * @param waveWidth Width of the bright wave (default: 3 characters)
28
+ * @returns Text with shimmer effect applied
29
+ */
30
+ export declare function shimmer(text: string, frame: number, waveWidth?: number): string;
23
31
  //# sourceMappingURL=colors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../../../src/src/cli/ui/colors.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,KAAK,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7D,wBAAgB,aAAa,IAAI,UAAU,CA+B1C;AAED,wBAAgB,cAAc,IAAI,OAAO,CAExC;AASD,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAgED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGzD;AAMD,eAAO,MAAM,KAAK,SAJsC,MAAM,WAIxB,CAAC;AACvC,eAAO,MAAM,OAAO,SAJsC,MAAM,WAItB,CAAC;AAE3C,eAAO,MAAM,OAAO,SAPoC,MAAM,WAOvB,CAAC;AACxC,eAAO,MAAM,KAAK,SARsC,MAAM,WAQzB,CAAC;AACtC,eAAO,MAAM,OAAO,SAToC,MAAM,WASvB,CAAC;AACxC,eAAO,MAAM,KAAK,SAVsC,MAAM,WAUvB,CAAC;AAExC,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,WAA+B,CAAC;AACjE,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,WAA+B,CAAC;AAChE,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,WAA+B,CAAC;AACnE,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,WAA+B,CAAC;AAEtE,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,WAAsB,CAAC;AAC7D,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,WAAwB,CAAC;AACjE,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,WAAsB,CAAC;AAE7D,OAAO,EAAE,KAAK,IAAI,KAAK,EAAE,CAAC;AAS1B,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD"}
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../../../src/src/cli/ui/colors.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,KAAK,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7D,wBAAgB,aAAa,IAAI,UAAU,CA+B1C;AAED,wBAAgB,cAAc,IAAI,OAAO,CAExC;AASD,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAgED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGzD;AAMD,eAAO,MAAM,KAAK,SAJsC,MAAM,WAIxB,CAAC;AACvC,eAAO,MAAM,OAAO,SAJsC,MAAM,WAItB,CAAC;AAE3C,eAAO,MAAM,OAAO,SAPoC,MAAM,WAOvB,CAAC;AACxC,eAAO,MAAM,KAAK,SARsC,MAAM,WAQzB,CAAC;AACtC,eAAO,MAAM,OAAO,SAToC,MAAM,WASvB,CAAC;AACxC,eAAO,MAAM,KAAK,SAVsC,MAAM,WAUvB,CAAC;AAExC,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,WAA+B,CAAC;AACjE,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM,WAA+B,CAAC;AAChE,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,WAA+B,CAAC;AACnE,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,WAA+B,CAAC;AAEtE,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,WAAsB,CAAC;AAC7D,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,WAAwB,CAAC;AACjE,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,WAAsB,CAAC;AAE7D,OAAO,EAAE,KAAK,IAAI,KAAK,EAAE,CAAC;AAS1B,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,SAAI,GAAG,MAAM,CA4B1E"}
@@ -132,3 +132,37 @@ export function animatedMatrix(frame) {
132
132
  const state = MATRIX_STATES[frame % MATRIX_STATES.length] ?? ["●", "○", "○"];
133
133
  return state.map((dot) => (dot === "●" ? brand(dot) : muted(dot))).join("");
134
134
  }
135
+ /**
136
+ * Apply shimmer effect to text - creates a wave of brightness moving across
137
+ * @param text The text to shimmer
138
+ * @param frame Current animation frame (increments over time)
139
+ * @param waveWidth Width of the bright wave (default: 3 characters)
140
+ * @returns Text with shimmer effect applied
141
+ */
142
+ export function shimmer(text, frame, waveWidth = 3) {
143
+ // Brand orange gradient: bright → normal → dim
144
+ const bright = (char) => applyColor(char, 255, 180, 140, false); // Brighter orange
145
+ const normal = (char) => applyColor(char, 252, 143, 93, false); // Brand orange
146
+ const dimmed = (char) => applyColor(char, 180, 100, 65, false); // Dimmer orange
147
+ const len = text.length;
148
+ const wavePos = frame % (len + waveWidth * 2);
149
+ let result = "";
150
+ for (let i = 0; i < len; i++) {
151
+ const char = text[i];
152
+ const distFromWave = i - (wavePos - waveWidth);
153
+ if (distFromWave >= 0 && distFromWave < waveWidth) {
154
+ // In the bright wave
155
+ const intensity = 1 - Math.abs(distFromWave - waveWidth / 2) / (waveWidth / 2);
156
+ if (intensity > 0.6) {
157
+ result += bright(char);
158
+ }
159
+ else {
160
+ result += normal(char);
161
+ }
162
+ }
163
+ else {
164
+ result += dimmed(char);
165
+ }
166
+ }
167
+ return result;
168
+ }
@@ -48,6 +48,14 @@ export declare function getAgentFace(options?: DotMatrixOptions): string;
48
48
  * Text lines appear to the right of the face, vertically centered
49
49
  */
50
50
  export declare function getAgentFaceWithText(textLines: string[], options?: DotMatrixOptions): string;
51
+ /**
52
+ * Get the agent face with spinning blade animation
53
+ * Orange and purple dots rotate around the logo
54
+ * @param textLines Text to show next to the face
55
+ * @param frame Animation frame (0-15 for full rotation)
56
+ * @param options Dot matrix options
57
+ */
58
+ export declare function getSpinningAgentFace(textLines: string[], frame: number, options?: DotMatrixOptions): string;
51
59
  /**
52
60
  * Animated dot matrix display with spinner support
53
61
  */
@@ -1 +1 @@
1
- {"version":3,"file":"dot-matrix.d.ts","sourceRoot":"","sources":["../../../../src/src/cli/ui/dot-matrix.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dot-matrix.d.ts","sourceRoot":"","sources":["../../../../src/src/cli/ui/dot-matrix.ts"],"names":[],"mappings":"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"}
@@ -52,6 +52,13 @@ const COMPACT_OPTIONS = {
52
52
  spacing: " ",
53
53
  };
54
54
  const RESET = "\x1b[0m";
55
+ // Colors for spinning animation - shades of orange
56
+ const SPIN_COLORS = {
57
+ bright: "\x1b[38;2;255;165;120m", // Bright orange (leading edge, toned down)
58
+ orange: "\x1b[38;2;252;143;93m", // Brand orange
59
+ mid: "\x1b[38;2;200;110;70m", // Mid orange (trailing)
60
+ dim: "\x1b[38;2;140;80;50m", // Dim orange/brown
61
+ };
55
62
  function resolveOptions(options) {
56
63
  if (options.compact)
57
64
  return { ...DEFAULT_OPTIONS, ...COMPACT_OPTIONS, ...options };
@@ -65,8 +72,56 @@ function renderPattern(pattern, opts) {
65
72
  return opts.prefix + dots.join(opts.spacing);
66
73
  });
67
74
  }
68
- function renderPatternWithText(pattern, textLines, opts) {
69
- const faceLines = renderPattern(pattern, opts);
75
+ /**
76
+ * Render the pattern with a spinning blade effect
77
+ * The blade sweeps across the lit dots, creating orange → purple gradient
78
+ */
79
+ function renderSpinningPattern(pattern, frame, opts) {
80
+ const centerRow = 3;
81
+ const centerCol = 3;
82
+ const totalFrames = 16; // Full rotation in 16 frames
83
+ const bladeAngle = ((frame % totalFrames) / totalFrames) * Math.PI * 2;
84
+ return pattern.map((row, rowIdx) => {
85
+ const dots = row.map((dot, colIdx) => {
86
+ if (dot !== 1) {
87
+ return `${opts.offColor}${opts.offChar}${RESET}`;
88
+ }
89
+ // Calculate angle from center to this dot
90
+ const dy = rowIdx - centerRow;
91
+ const dx = colIdx - centerCol;
92
+ let dotAngle = Math.atan2(dy, dx);
93
+ if (dotAngle < 0)
94
+ dotAngle += Math.PI * 2;
95
+ // Calculate angular distance from the blade
96
+ let angleDiff = dotAngle - bladeAngle;
97
+ if (angleDiff < 0)
98
+ angleDiff += Math.PI * 2;
99
+ if (angleDiff > Math.PI)
100
+ angleDiff = Math.PI * 2 - angleDiff;
101
+ // Color based on angular distance from blade
102
+ const normalizedDiff = angleDiff / Math.PI; // 0 = at blade, 1 = opposite
103
+ let color;
104
+ if (normalizedDiff < 0.15) {
105
+ color = SPIN_COLORS.bright; // Leading edge - brightest orange
106
+ }
107
+ else if (normalizedDiff < 0.35) {
108
+ color = SPIN_COLORS.orange; // Near blade - brand orange
109
+ }
110
+ else if (normalizedDiff < 0.6) {
111
+ color = SPIN_COLORS.mid; // Trailing - mid orange
112
+ }
113
+ else {
114
+ color = SPIN_COLORS.dim; // Far from blade - dim orange
115
+ }
116
+ return `${color}${opts.litChar}${RESET}`;
117
+ });
118
+ return opts.prefix + dots.join(opts.spacing);
119
+ });
120
+ }
121
+ function renderPatternWithText(pattern, textLines, opts, spinFrame) {
122
+ const faceLines = spinFrame !== undefined
123
+ ? renderSpinningPattern(pattern, spinFrame, opts)
124
+ : renderPattern(pattern, opts);
70
125
  const faceHeight = faceLines.length;
71
126
  const startLine = Math.floor((faceHeight - textLines.length) / 2);
72
127
  const result = faceLines.map((line, i) => {
@@ -120,6 +175,16 @@ export function getAgentFace(options = {}) {
120
175
  export function getAgentFaceWithText(textLines, options = {}) {
121
176
  return renderPatternWithText(AGENT_FACE, textLines, resolveOptions(options));
122
177
  }
178
+ /**
179
+ * Get the agent face with spinning blade animation
180
+ * Orange and purple dots rotate around the logo
181
+ * @param textLines Text to show next to the face
182
+ * @param frame Animation frame (0-15 for full rotation)
183
+ * @param options Dot matrix options
184
+ */
185
+ export function getSpinningAgentFace(textLines, frame, options = {}) {
186
+ return renderPatternWithText(AGENT_FACE, textLines, resolveOptions(options), frame);
187
+ }
123
188
  /**
124
189
  * Animated dot matrix display with spinner support
125
190
  */
@@ -106,7 +106,7 @@ function stopSpinner() {
106
106
  spinnerInterval = null;
107
107
  }
108
108
  export function createTui(cfg = {}) {
109
- config = { title: "Veryfront", showLogs: true, ...cfg };
109
+ config = { title: "Veryfront Code", showLogs: true, ...cfg };
110
110
  state = {
111
111
  status: "Initializing...",
112
112
  statusType: "loading",
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/modules/react-loader/ssr-module-loader/loader.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAsCpC,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAgC3E;;;;;GAKG;AACH,qBAAa,eAAe;IAId,OAAO,CAAC,OAAO;IAH3B,OAAO,CAAC,EAAE,CAAsB;IAChC,OAAO,CAAC,mBAAmB,CAAuB;gBAE9B,OAAO,EAAE,sBAAsB;IAEnD;;OAEG;IACH,UAAU,CACR,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAyFxD,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,wBAAwB;IAwBhC,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,yBAAyB;IAuBjC,OAAO,CAAC,kBAAkB;IAK1B;;OAEG;YACW,2BAA2B;IAkGzC,OAAO,CAAC,yBAAyB;YAiBnB,2BAA2B;IA6RzC;;;OAGG;YACW,mBAAmB;IA2CjC,OAAO,CAAC,yBAAyB;IAYjC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA4B3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B,OAAO,CAAC,wBAAwB;IAehC,OAAO,CAAC,2BAA2B;IAiBnC,OAAO,CAAC,2BAA2B;IAWnC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA4B3B;;OAEG;IACH,OAAO,CAAC,aAAa;YAIP,uBAAuB;IAyCrC;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAUhB;;;OAGG;YACW,gBAAgB;YAgBhB,WAAW;YAeX,YAAY;CAiC3B"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/modules/react-loader/ssr-module-loader/loader.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAsCpC,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAgC3E;;;;;GAKG;AACH,qBAAa,eAAe;IAId,OAAO,CAAC,OAAO;IAH3B,OAAO,CAAC,EAAE,CAAsB;IAChC,OAAO,CAAC,mBAAmB,CAAuB;gBAE9B,OAAO,EAAE,sBAAsB;IAEnD;;OAEG;IACH,UAAU,CACR,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAsGxD,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,wBAAwB;IAwBhC,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,yBAAyB;IAuBjC,OAAO,CAAC,kBAAkB;IAK1B;;OAEG;YACW,2BAA2B;IAkGzC,OAAO,CAAC,yBAAyB;YAiBnB,2BAA2B;IA6RzC;;;OAGG;YACW,mBAAmB;IA2CjC,OAAO,CAAC,yBAAyB;IAYjC;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA4B3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B,OAAO,CAAC,wBAAwB;IAehC,OAAO,CAAC,2BAA2B;IAiBnC,OAAO,CAAC,2BAA2B;IAWnC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA4B3B;;OAEG;IACH,OAAO,CAAC,aAAa;YAIP,uBAAuB;IAyCrC;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAUhB;;;OAGG;YACW,gBAAgB;YAgBhB,WAAW;YAeX,YAAY;CAiC3B"}
@@ -97,18 +97,31 @@ export class SSRModuleLoader {
97
97
  const bundleMatch = errorMsg.match(/veryfront-http-bundle\/http-([a-f0-9]+)\.mjs/);
98
98
  if (bundleMatch) {
99
99
  const hash = bundleMatch[1];
100
- logger.warn("[SSR-MODULE-LOADER] Import failed due to missing HTTP bundle, attempting recovery", {
100
+ const cacheDir = getHttpBundleCacheDir();
101
+ logger.error("[SSR-MODULE-LOADER] Missing HTTP bundle after ensureHttpBundlesExist", {
101
102
  file: filePath.slice(-40),
102
103
  hash,
104
+ tempPath: cacheEntry.tempPath,
105
+ contentHash: cacheEntry.contentHash,
106
+ cacheDir,
107
+ expectedPath: `${cacheDir}/http-${hash}.mjs`,
103
108
  });
104
109
  const { recoverHttpBundleByHash } = await import("../../../transforms/esm/http-cache.js");
105
- const cacheDir = getHttpBundleCacheDir();
106
110
  const recovered = await recoverHttpBundleByHash(hash, cacheDir);
107
111
  if (recovered) {
108
- logger.info("[SSR-MODULE-LOADER] HTTP bundle recovered, retrying import", { hash });
112
+ logger.info("[SSR-MODULE-LOADER] HTTP bundle recovered, retrying import", {
113
+ hash,
114
+ file: filePath.slice(-40),
115
+ });
109
116
  mod = await import(`file://${cacheEntry.tempPath}?v=${cacheEntry.contentHash}&retry=1`);
110
117
  }
111
118
  else {
119
+ logger.error("[SSR-MODULE-LOADER] HTTP bundle recovery failed", {
120
+ hash,
121
+ file: filePath.slice(-40),
122
+ cacheDir,
123
+ hint: "Bundle may have expired from Redis (24h TTL) while transform was still cached",
124
+ });
112
125
  throw importError;
113
126
  }
114
127
  }
@@ -313,9 +326,12 @@ export class SSRModuleLoader {
313
326
  const cacheDir = getHttpBundleCacheDir();
314
327
  const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
315
328
  if (failed.length > 0) {
316
- logger.warn("[SSR-MODULE-LOADER] In-memory cached module has unrecoverable HTTP bundles, re-transforming", {
329
+ logger.warn("[SSR-MODULE-LOADER] Unrecoverable HTTP bundles, re-transforming", {
317
330
  file: filePath.slice(-40),
318
331
  failed,
332
+ totalBundles: bundlePaths.length,
333
+ cacheDir,
334
+ source: "memory-cache",
319
335
  });
320
336
  globalModuleCache.delete(contentCacheKey);
321
337
  globalModuleCache.delete(filePathCacheKey);
@@ -354,9 +370,12 @@ export class SSRModuleLoader {
354
370
  const cacheDir = getHttpBundleCacheDir();
355
371
  const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
356
372
  if (failed.length > 0) {
357
- logger.warn("[SSR-MODULE-LOADER] Redis cached code has unrecoverable HTTP bundles, re-transforming", {
373
+ logger.warn("[SSR-MODULE-LOADER] Unrecoverable HTTP bundles, re-transforming", {
358
374
  file: filePath.slice(-40),
359
375
  failed,
376
+ totalBundles: bundlePaths.length,
377
+ cacheDir,
378
+ source: "redis-cache",
360
379
  });
361
380
  httpBundlesOk = false;
362
381
  }
@@ -454,9 +473,12 @@ export class SSRModuleLoader {
454
473
  const cacheDir = getHttpBundleCacheDir();
455
474
  const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
456
475
  if (failed.length > 0) {
457
- logger.warn("[SSR-MODULE-LOADER] Some HTTP bundles could not be recovered", {
476
+ logger.warn("[SSR-MODULE-LOADER] Unrecoverable HTTP bundles", {
458
477
  file: filePath.slice(-40),
459
478
  failed,
479
+ totalBundles: bundlePaths.length,
480
+ cacheDir,
481
+ source: "fresh-transform",
460
482
  });
461
483
  }
462
484
  }
@@ -1 +1 @@
1
- {"version":3,"file":"http-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/http-cache.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAmBzE,KAAK,YAAY,GAAG;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,eAAe,CAAC;IAC3B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAyUF;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOvF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8D9F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,EAAE,CAAC,CAiInB"}
1
+ {"version":3,"file":"http-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/http-cache.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAgBzE,KAAK,YAAY,GAAG;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,eAAe,CAAC;IAC3B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAqVF;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOvF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8D9F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,EAAE,CAAC,CAiInB"}
@@ -23,10 +23,13 @@ import { CacheBackends, createDistributedCacheAccessor } from "../../cache/backe
23
23
  import { HTTP_MODULE_CACHE_MAX_ENTRIES, HTTP_MODULE_DISTRIBUTED_TTL_SEC, } from "../../utils/constants/cache.js";
24
24
  /** Lazy-loaded distributed cache backend for cross-pod sharing */
25
25
  const getDistributedCache = createDistributedCacheAccessor(() => CacheBackends.httpModule(), "HTTP-CACHE");
26
- /** TTL for cached modules in distributed cache (uses centralized config) */
27
- const DISTRIBUTED_CACHE_TTL_SECONDS = HTTP_MODULE_DISTRIBUTED_TTL_SEC;
28
26
  const cachedPaths = new LRUCache({ maxEntries: HTTP_MODULE_CACHE_MAX_ENTRIES });
29
27
  const processingStack = new Set();
28
+ /** Tracks last TTL refresh per hash. Refresh every 4h to keep 20h+ remaining (24h total). */
29
+ const lastDistributedRefresh = new LRUCache({
30
+ maxEntries: HTTP_MODULE_CACHE_MAX_ENTRIES,
31
+ });
32
+ const DISTRIBUTED_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
30
33
  function ensureAbsoluteDir(path) {
31
34
  return isAbsolute(path) ? path : join(cwd(), path);
32
35
  }
@@ -151,23 +154,28 @@ async function cacheHttpModule(url, options) {
151
154
  const fs = createFileSystem();
152
155
  if (await exists(cachePath)) {
153
156
  cachedPaths.set(cacheKey, cachePath);
154
- // Synchronously ensure code:{hash} exists in distributed cache for cross-pod recovery
155
- // This backfills bundles created before code:{hash} storage was added
156
- // Synchronous to guarantee data is stored before other pods need it
157
+ // Refresh distributed cache TTL so bundles outlive transforms that reference them.
158
+ // Without this, bundles expire (24h) while SSR transforms (6h) are still valid.
157
159
  const distributed = await getDistributedCache();
158
160
  if (distributed) {
159
161
  const hash = simpleHash(normalizedUrl);
160
- try {
161
- const hasCode = await distributed.get(`code:${hash}`);
162
- if (!hasCode) {
162
+ const hashStr = String(hash);
163
+ const now = Date.now();
164
+ const lastRefresh = lastDistributedRefresh.get(hashStr);
165
+ const needsRefresh = !lastRefresh || (now - lastRefresh > DISTRIBUTED_REFRESH_INTERVAL_MS);
166
+ if (needsRefresh) {
167
+ try {
163
168
  const code = await fs.readTextFile(cachePath);
164
- await distributed.set(`code:${hash}`, code, DISTRIBUTED_CACHE_TTL_SECONDS);
165
- logger.info("[HTTP-CACHE] Backfilled code:{hash} for existing bundle", { hash });
169
+ await Promise.all([
170
+ distributed.set(`code:${hash}`, code, HTTP_MODULE_DISTRIBUTED_TTL_SEC),
171
+ distributed.set(`hash:${hash}`, normalizedUrl, HTTP_MODULE_DISTRIBUTED_TTL_SEC),
172
+ ]);
173
+ lastDistributedRefresh.set(hashStr, now);
174
+ logger.debug("[HTTP-CACHE] Refreshed distributed cache TTL", { hash });
175
+ }
176
+ catch (error) {
177
+ logger.debug("[HTTP-CACHE] Distributed cache refresh failed", { hash, error });
166
178
  }
167
- }
168
- catch (error) {
169
- // Log but don't fail - backfill is best-effort
170
- logger.debug("[HTTP-CACHE] Backfill failed, continuing", { hash, error });
171
179
  }
172
180
  }
173
181
  return cachePath;
@@ -232,9 +240,9 @@ async function cacheHttpModule(url, options) {
232
240
  const hash = simpleHash(normalizedUrl);
233
241
  try {
234
242
  await Promise.all([
235
- distributed.set(normalizedUrl, code, DISTRIBUTED_CACHE_TTL_SECONDS),
236
- distributed.set(`code:${hash}`, code, DISTRIBUTED_CACHE_TTL_SECONDS),
237
- distributed.set(`hash:${hash}`, normalizedUrl, DISTRIBUTED_CACHE_TTL_SECONDS),
243
+ distributed.set(normalizedUrl, code, HTTP_MODULE_DISTRIBUTED_TTL_SEC),
244
+ distributed.set(`code:${hash}`, code, HTTP_MODULE_DISTRIBUTED_TTL_SEC),
245
+ distributed.set(`hash:${hash}`, normalizedUrl, HTTP_MODULE_DISTRIBUTED_TTL_SEC),
238
246
  ]);
239
247
  }
240
248
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.0.83",
3
+ "version": "0.0.86",
4
4
  "description": "Zero-config React meta-framework for building agentic AI applications",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.0.83",
3
+ "version": "0.0.86",
4
4
  "nodeModulesDir": "auto",
5
5
  "exclude": [
6
6
  "npm/",
@@ -196,8 +196,8 @@ export function renderList<T>(
196
196
  }
197
197
  }
198
198
 
199
- if (start > 0) lines.unshift(` ${dim("more above")}`);
200
- if (end < state.items.length) lines.push(` ${dim("more below")}`);
199
+ if (start > 0) lines.unshift(` ${dim("↑")} ${dim("more above")}`);
200
+ if (end < state.items.length) lines.push(` ${dim("↓")} ${dim("more below")}`);
201
201
 
202
202
  return lines.join("\n");
203
203
  }
@@ -19,9 +19,16 @@ import { join } from "../../platform/compat/path/index.js";
19
19
  import { getRuntimeEnv } from "../../config/runtime-env.js";
20
20
  import { getStdinReader, setRawMode } from "../../platform/compat/stdin.js";
21
21
  import { cursor, screen, SPINNER_FRAMES } from "../ui/ansi.js";
22
- import { brand, dim, success } from "../ui/colors.js";
22
+ import { brand, dim } from "../ui/colors.js";
23
+ import { getTerminalWidth } from "../ui/layout.js";
23
24
  import { moveDown, moveUp, selectByNumber } from "./components/list-select.js";
24
25
  import { renderDashboard, renderEmptyState } from "./views/dashboard.js";
26
+ import {
27
+ createStartupState,
28
+ incrementFrame,
29
+ renderStartup,
30
+ setStepActive,
31
+ } from "./views/startup.js";
25
32
  import { openInBrowser, openInIDE, openInStudio, openMCPSettings } from "./actions.js";
26
33
  import { initCommand } from "../commands/init/init-command.js";
27
34
  import type { InitTemplate } from "../commands/init/types.js";
@@ -42,6 +49,7 @@ import {
42
49
  setTemplates,
43
50
  startInput,
44
51
  type StateUpdater,
52
+ toggleHelp,
45
53
  toggleLogsExpanded,
46
54
  updateActiveList,
47
55
  updateInputValue,
@@ -545,10 +553,13 @@ export function createApp(config: AppConfig): App {
545
553
 
546
554
  const parts: string[] = [content];
547
555
 
556
+ // Divider width matches the box in dashboard
557
+ const dividerWidth = Math.min(getTerminalWidth() - 4, 80);
558
+
548
559
  if (state.logs.length > 0) {
549
560
  const logsHeader = state.logsExpanded ? "▼ Logs" : "▶ Logs";
550
561
  parts.push("");
551
- parts.push(` ${dim("─".repeat(60))}`);
562
+ parts.push(dim("─".repeat(dividerWidth)));
552
563
  parts.push(
553
564
  ` ${dim(logsHeader)} ${dim(`(${state.logs.length})`)} ${dim("l")} ${dim("toggle")} ${
554
565
  state.logsExpanded ? `${dim("↑↓")} ${dim("scroll")}` : ""
@@ -563,14 +574,14 @@ export function createApp(config: AppConfig): App {
563
574
 
564
575
  if (state.input.active) {
565
576
  parts.push("");
566
- parts.push(` ${dim("─".repeat(60))}`);
577
+ parts.push(dim("─".repeat(dividerWidth)));
567
578
  parts.push(renderInput(state.input));
568
579
  }
569
580
 
570
581
  if (!isInteractiveMode) return;
571
582
 
572
583
  write(cursor.moveTo(1, 1) + screen.clearDown);
573
- write(parts.join("\n"));
584
+ write("\n" + parts.join("\n"));
574
585
  }
575
586
 
576
587
  function update(updater: StateUpdater): void {
@@ -933,7 +944,7 @@ export function createApp(config: AppConfig): App {
933
944
  }
934
945
 
935
946
  if (key === "?") {
936
- update(navigateTo("help"));
947
+ update(toggleHelp());
937
948
  return;
938
949
  }
939
950
 
@@ -1422,47 +1433,35 @@ export function createApp(config: AppConfig): App {
1422
1433
  }
1423
1434
 
1424
1435
  /**
1425
- * Show startup animation
1436
+ * Show startup animation with boxed view and shimmer effect
1426
1437
  */
1427
1438
  export async function showStartup(steps: string[]): Promise<void> {
1428
1439
  const write = (text: string): void => writeStdout(text);
1429
1440
 
1430
1441
  write(screen.altOn + cursor.hide);
1431
1442
 
1432
- for (let i = 0; i < steps.length; i++) {
1433
- const step = steps[i]!;
1434
- const completed = steps.slice(0, i).map((s) => ` ${success("✓")} ${dim(s)}`);
1435
- const current = ` ${brand("●")} ${step}`;
1436
- const pending = steps.slice(i + 1).map((s) => ` ${dim("○")} ${dim(s)}`);
1437
-
1438
- const content = [
1439
- "",
1440
- ` ${brand("Veryfront")} ${dim("starting...")}`,
1441
- "",
1442
- ...completed,
1443
- current,
1444
- ...pending,
1445
- "",
1446
- ].join("\n");
1443
+ let startupState = createStartupState(steps);
1447
1444
 
1448
- write(cursor.moveTo(1, 1) + screen.clearDown + content);
1449
- await new Promise((r) => dntShim.setTimeout(r, 200));
1445
+ // Show each step with spinning avatar animation
1446
+ for (let i = 0; i < steps.length; i++) {
1447
+ startupState = setStepActive(startupState, i);
1448
+
1449
+ // Animate spinning avatar (16 frames at 60ms = ~1s per step for full rotation)
1450
+ const framesPerStep = 16;
1451
+ for (let f = 0; f < framesPerStep; f++) {
1452
+ write(cursor.moveTo(1, 1) + screen.clearDown + "\n" + renderStartup(startupState));
1453
+ startupState = incrementFrame(startupState);
1454
+ await new Promise((r) => dntShim.setTimeout(r, 60));
1455
+ }
1450
1456
  }
1451
1457
 
1452
- const allComplete = steps.map((s) => ` ${success("✓")} ${dim(s)}`);
1453
- const finalContent = [
1454
- "",
1455
- ` ${brand("Veryfront")} ${success("ready")}`,
1456
- "",
1457
- ...allComplete,
1458
- "",
1459
- ].join("\n");
1460
-
1461
- write(cursor.moveTo(1, 1) + screen.clearDown + finalContent);
1462
- await new Promise((r) => dntShim.setTimeout(r, 300));
1458
+ // Mark all steps done - logo fills up and holds before transitioning
1459
+ startupState = setStepActive(startupState, steps.length);
1460
+ write(cursor.moveTo(1, 1) + screen.clearDown + "\n" + renderStartup(startupState));
1461
+ await new Promise((r) => dntShim.setTimeout(r, 400));
1463
1462
 
1464
1463
  // Don't exit alternate screen - let app.start() continue in it
1465
- // This prevents a flash when transitioning to the dashboard
1464
+ // Dashboard takes over directly from here
1466
1465
  }
1467
1466
 
1468
1467
  export type { AppState } from "./state.js";
@@ -103,6 +103,8 @@ export interface AppState {
103
103
  authProviderIndex: number;
104
104
  /** New project option index (0=template, 1=example, 2=scratch) */
105
105
  newProjectIndex: number;
106
+ /** Show expanded help */
107
+ showHelp: boolean;
106
108
  }
107
109
 
108
110
  export function createInitialState(): AppState {
@@ -153,6 +155,7 @@ export function createInitialState(): AppState {
153
155
  logScroll: 0,
154
156
  authProviderIndex: 0,
155
157
  newProjectIndex: 0,
158
+ showHelp: false,
156
159
  };
157
160
  }
158
161
 
@@ -339,6 +342,10 @@ export function toggleLogsExpanded(): StateUpdater {
339
342
  });
340
343
  }
341
344
 
345
+ export function toggleHelp(): StateUpdater {
346
+ return (state) => ({ ...state, showHelp: !state.showHelp });
347
+ }
348
+
342
349
  export function scrollLogs(direction: "up" | "down"): StateUpdater {
343
350
  return (state) => {
344
351
  if (!state.logsExpanded) return state;