veryfront 0.0.82 → 0.0.84

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 (126) hide show
  1. package/README.md +18 -17
  2. package/esm/deno.js +1 -1
  3. package/esm/proxy/cache/index.d.ts +41 -0
  4. package/esm/proxy/cache/index.d.ts.map +1 -0
  5. package/esm/proxy/cache/index.js +75 -0
  6. package/esm/proxy/cache/memory-cache.d.ts +18 -0
  7. package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
  8. package/esm/proxy/cache/memory-cache.js +100 -0
  9. package/esm/proxy/cache/redis-cache.d.ts +27 -0
  10. package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
  11. package/esm/proxy/cache/redis-cache.js +183 -0
  12. package/esm/proxy/cache/resilient-cache.d.ts +44 -0
  13. package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
  14. package/esm/proxy/cache/resilient-cache.js +178 -0
  15. package/esm/proxy/cache/types.d.ts +65 -0
  16. package/esm/proxy/cache/types.d.ts.map +1 -0
  17. package/esm/proxy/cache/types.js +7 -0
  18. package/esm/proxy/handler.d.ts +81 -0
  19. package/esm/proxy/handler.d.ts.map +1 -0
  20. package/esm/proxy/handler.js +417 -0
  21. package/esm/proxy/logger.d.ts +29 -0
  22. package/esm/proxy/logger.d.ts.map +1 -0
  23. package/esm/proxy/logger.js +258 -0
  24. package/esm/proxy/oauth-client.d.ts +15 -0
  25. package/esm/proxy/oauth-client.d.ts.map +1 -0
  26. package/esm/proxy/oauth-client.js +52 -0
  27. package/esm/proxy/token-manager.d.ts +59 -0
  28. package/esm/proxy/token-manager.d.ts.map +1 -0
  29. package/esm/proxy/token-manager.js +125 -0
  30. package/esm/proxy/tracing.d.ts +39 -0
  31. package/esm/proxy/tracing.d.ts.map +1 -0
  32. package/esm/proxy/tracing.js +194 -0
  33. package/esm/src/cache/backend.d.ts +2 -0
  34. package/esm/src/cache/backend.d.ts.map +1 -1
  35. package/esm/src/cache/backend.js +2 -0
  36. package/esm/src/cache/cache-key-builder.d.ts +0 -4
  37. package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
  38. package/esm/src/cache/cache-key-builder.js +0 -6
  39. package/esm/src/cache/multi-tier.d.ts +0 -29
  40. package/esm/src/cache/multi-tier.d.ts.map +1 -1
  41. package/esm/src/cache/multi-tier.js +0 -26
  42. package/esm/src/cli/app/actions.d.ts +26 -0
  43. package/esm/src/cli/app/actions.d.ts.map +1 -0
  44. package/esm/src/cli/app/actions.js +152 -0
  45. package/esm/src/cli/app/components/inline-input.d.ts +35 -0
  46. package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
  47. package/esm/src/cli/app/components/inline-input.js +220 -0
  48. package/esm/src/cli/app/components/list-select.d.ts +69 -0
  49. package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
  50. package/esm/src/cli/app/components/list-select.js +137 -0
  51. package/esm/src/cli/app/index.d.ts +45 -0
  52. package/esm/src/cli/app/index.d.ts.map +1 -0
  53. package/esm/src/cli/app/index.js +1252 -0
  54. package/esm/src/cli/app/state.d.ts +122 -0
  55. package/esm/src/cli/app/state.d.ts.map +1 -0
  56. package/esm/src/cli/app/state.js +232 -0
  57. package/esm/src/cli/app/views/dashboard.d.ts +19 -0
  58. package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
  59. package/esm/src/cli/app/views/dashboard.js +178 -0
  60. package/esm/src/cli/commands/dev.js +2 -2
  61. package/esm/src/cli/commands/new.js +1 -1
  62. package/esm/src/cli/index/command-router.d.ts.map +1 -1
  63. package/esm/src/cli/index/command-router.js +9 -39
  64. package/esm/src/cli/index/start-handler.d.ts +3 -0
  65. package/esm/src/cli/index/start-handler.d.ts.map +1 -0
  66. package/esm/src/cli/index/start-handler.js +145 -0
  67. package/esm/src/cli/mcp/index.d.ts +11 -0
  68. package/esm/src/cli/mcp/index.d.ts.map +1 -0
  69. package/esm/src/cli/mcp/index.js +10 -0
  70. package/esm/src/cli/ui/tui.js +1 -1
  71. package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
  72. package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
  73. package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
  74. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
  75. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
  76. package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
  77. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  78. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
  79. package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
  80. package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
  81. package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
  82. package/esm/src/server/context/cache-invalidation.js +4 -0
  83. package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
  84. package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
  85. package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
  86. package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
  87. package/esm/src/transforms/esm/http-cache.js +139 -64
  88. package/esm/src/utils/index.d.ts +1 -1
  89. package/esm/src/utils/index.d.ts.map +1 -1
  90. package/esm/src/utils/index.js +1 -1
  91. package/package.json +2 -1
  92. package/src/deno.js +1 -1
  93. package/src/proxy/cache/index.ts +93 -0
  94. package/src/proxy/cache/memory-cache.ts +120 -0
  95. package/src/proxy/cache/redis-cache.ts +203 -0
  96. package/src/proxy/cache/resilient-cache.ts +205 -0
  97. package/src/proxy/cache/types.ts +72 -0
  98. package/src/proxy/handler.ts +593 -0
  99. package/src/proxy/logger.ts +329 -0
  100. package/src/proxy/oauth-client.ts +91 -0
  101. package/src/proxy/token-manager.ts +174 -0
  102. package/src/proxy/tracing.ts +237 -0
  103. package/src/src/cache/backend.ts +3 -0
  104. package/src/src/cache/cache-key-builder.ts +0 -9
  105. package/src/src/cache/multi-tier.ts +0 -41
  106. package/src/src/cli/app/actions.ts +190 -0
  107. package/src/src/cli/app/components/inline-input.ts +255 -0
  108. package/src/src/cli/app/components/list-select.ts +215 -0
  109. package/src/src/cli/app/index.ts +1471 -0
  110. package/src/src/cli/app/state.ts +385 -0
  111. package/src/src/cli/app/views/dashboard.ts +212 -0
  112. package/src/src/cli/commands/dev.ts +2 -2
  113. package/src/src/cli/commands/new.ts +1 -1
  114. package/src/src/cli/index/command-router.ts +9 -40
  115. package/src/src/cli/index/start-handler.ts +195 -0
  116. package/src/src/cli/mcp/index.ts +11 -0
  117. package/src/src/cli/ui/tui.ts +1 -1
  118. package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
  119. package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
  120. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
  121. package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
  122. package/src/src/server/context/cache-invalidation.ts +4 -0
  123. package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
  124. package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
  125. package/src/src/transforms/esm/http-cache.ts +148 -73
  126. package/src/src/utils/index.ts +0 -1
@@ -0,0 +1,1252 @@
1
+ /**
2
+ * CLI App Shell
3
+ *
4
+ * Interactive app-like CLI experience with dashboard, project navigation,
5
+ * and MCP integration for coding agents.
6
+ * Uses cross-runtime platform abstractions for terminal I/O.
7
+ */
8
+ import * as dntShim from "../../../_dnt.shims.js";
9
+ import { cwd, exit, isInteractive, isStdoutTTY, writeStdout, } from "../../platform/compat/process.js";
10
+ import { join } from "../../platform/compat/path/index.js";
11
+ import { getRuntimeEnv } from "../../config/runtime-env.js";
12
+ import { getStdinReader, setRawMode } from "../../platform/compat/stdin.js";
13
+ import { cursor, screen, SPINNER_FRAMES } from "../ui/ansi.js";
14
+ import { brand, dim, success } from "../ui/colors.js";
15
+ import { moveDown, moveUp, selectByNumber } from "./components/list-select.js";
16
+ import { renderDashboard, renderEmptyState } from "./views/dashboard.js";
17
+ import { openInBrowser, openInIDE, openInStudio, openMCPSettings } from "./actions.js";
18
+ import { initCommand } from "../commands/init/init-command.js";
19
+ import { addLog, createInitialState, endInput, getActiveSelection, goBack, navigateTo, scrollLogs, setActiveList, setExamples, setProjects, setTemplates, startInput, toggleLogsExpanded, updateActiveList, updateInputValue, updateMCP, updateRemote, updateServer, } from "./state.js";
20
+ import { handleInputKey, renderInput, renderLogs } from "./components/inline-input.js";
21
+ import { login, logout, validateToken } from "../auth/login.js";
22
+ import { readToken } from "../auth/token-store.js";
23
+ import { openBrowser } from "../auth/browser.js";
24
+ import { fetchRemoteProjects } from "../sync/index.js";
25
+ import { pullCommand } from "../commands/pull.js";
26
+ import { pushCommand } from "../commands/push.js";
27
+ async function copyDirectory(src, dest) {
28
+ const fs = await import("../../platform/compat/fs.js");
29
+ const pathMod = await import("../../platform/compat/path/index.js");
30
+ const filesystem = fs.createFileSystem();
31
+ await filesystem.mkdir(dest, { recursive: true });
32
+ for await (const entry of filesystem.readDir(src)) {
33
+ const srcPath = pathMod.join(src, entry.name);
34
+ const destPath = pathMod.join(dest, entry.name);
35
+ if (entry.isDirectory) {
36
+ await copyDirectory(srcPath, destPath);
37
+ }
38
+ else {
39
+ const content = await filesystem.readFile(srcPath);
40
+ await filesystem.writeFile(destPath, content);
41
+ }
42
+ }
43
+ }
44
+ function generateRandomSlug() {
45
+ const adjectives = [
46
+ // colors & gems
47
+ "amber",
48
+ "azure",
49
+ "coral",
50
+ "crimson",
51
+ "cyan",
52
+ "golden",
53
+ "indigo",
54
+ "ivory",
55
+ "jade",
56
+ "magenta",
57
+ "maroon",
58
+ "olive",
59
+ "onyx",
60
+ "opal",
61
+ "pearl",
62
+ "ruby",
63
+ "scarlet",
64
+ "silver",
65
+ "teal",
66
+ "topaz",
67
+ "turquoise",
68
+ "violet",
69
+ // nature
70
+ "alpine",
71
+ "arctic",
72
+ "autumn",
73
+ "coastal",
74
+ "crystal",
75
+ "desert",
76
+ "floral",
77
+ "forest",
78
+ "frozen",
79
+ "lunar",
80
+ "misty",
81
+ "mossy",
82
+ "ocean",
83
+ "polar",
84
+ "rainy",
85
+ "snowy",
86
+ "solar",
87
+ "spring",
88
+ "stormy",
89
+ "sunny",
90
+ "tidal",
91
+ "tropic",
92
+ "windy",
93
+ // qualities
94
+ "agile",
95
+ "bold",
96
+ "brave",
97
+ "bright",
98
+ "calm",
99
+ "clever",
100
+ "cosmic",
101
+ "daring",
102
+ "eager",
103
+ "epic",
104
+ "fierce",
105
+ "gentle",
106
+ "grand",
107
+ "keen",
108
+ "kind",
109
+ "lively",
110
+ "mystic",
111
+ "nimble",
112
+ "noble",
113
+ "proud",
114
+ "quiet",
115
+ "rapid",
116
+ "serene",
117
+ "silent",
118
+ "steady",
119
+ "swift",
120
+ "vivid",
121
+ "wild",
122
+ "wise",
123
+ "witty",
124
+ "zen",
125
+ ];
126
+ const nouns = [
127
+ // water
128
+ "bay",
129
+ "brook",
130
+ "canal",
131
+ "cascade",
132
+ "coast",
133
+ "creek",
134
+ "delta",
135
+ "falls",
136
+ "fjord",
137
+ "gulf",
138
+ "harbor",
139
+ "lagoon",
140
+ "lake",
141
+ "marsh",
142
+ "ocean",
143
+ "pond",
144
+ "rapids",
145
+ "reef",
146
+ "river",
147
+ "shore",
148
+ "spring",
149
+ "strait",
150
+ "stream",
151
+ "tide",
152
+ "wave",
153
+ // land
154
+ "bluff",
155
+ "canyon",
156
+ "cave",
157
+ "cliff",
158
+ "crater",
159
+ "desert",
160
+ "dune",
161
+ "field",
162
+ "glade",
163
+ "gorge",
164
+ "grove",
165
+ "hill",
166
+ "isle",
167
+ "mesa",
168
+ "oasis",
169
+ "pass",
170
+ "peak",
171
+ "plain",
172
+ "plateau",
173
+ "ridge",
174
+ "rock",
175
+ "slope",
176
+ "stone",
177
+ "summit",
178
+ "trail",
179
+ "valley",
180
+ "volcano",
181
+ // sky & space
182
+ "aurora",
183
+ "cloud",
184
+ "comet",
185
+ "cosmos",
186
+ "dawn",
187
+ "dusk",
188
+ "eclipse",
189
+ "ember",
190
+ "flare",
191
+ "frost",
192
+ "galaxy",
193
+ "glow",
194
+ "haze",
195
+ "horizon",
196
+ "meteor",
197
+ "mist",
198
+ "moon",
199
+ "nebula",
200
+ "nova",
201
+ "orbit",
202
+ "prism",
203
+ "pulse",
204
+ "quasar",
205
+ "ray",
206
+ "shadow",
207
+ "sky",
208
+ "spark",
209
+ "star",
210
+ "storm",
211
+ "sun",
212
+ "thunder",
213
+ "twilight",
214
+ "vapor",
215
+ "wind",
216
+ "zenith",
217
+ ];
218
+ const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
219
+ const noun = nouns[Math.floor(Math.random() * nouns.length)];
220
+ return `${adj}-${noun}`;
221
+ }
222
+ /**
223
+ * Create the CLI app
224
+ */
225
+ export function createApp(config) {
226
+ let state = createInitialState();
227
+ let running = false;
228
+ let spinnerFrame = 0;
229
+ let spinnerInterval = null;
230
+ // Force non-interactive if headless flag is set (for coding agents)
231
+ const isInteractiveMode = !config.headless && isInteractive() && isStdoutTTY();
232
+ state = setProjects(Array.from(config.projects.entries()).map(([slug, path]) => ({ slug, path })))(state);
233
+ if (config.examples) {
234
+ state = setExamples(Array.from(config.examples.entries()).map(([slug, path]) => ({ slug, path })))(state);
235
+ }
236
+ state = setTemplates([
237
+ { id: "minimal", name: "Minimal", description: "Bare-bones starter with just the essentials" },
238
+ { id: "app", name: "App", description: "Full-featured app with routing and layouts" },
239
+ { id: "ai", name: "AI", description: "AI-powered app with chat and agents" },
240
+ { id: "blog", name: "Blog", description: "MDX-powered blog with syntax highlighting" },
241
+ { id: "docs", name: "Docs", description: "Documentation site with search" },
242
+ ])(state);
243
+ state = updateServer({
244
+ port: config.port,
245
+ url: `http://veryfront.me:${config.port}`,
246
+ })(state);
247
+ state = updateMCP({
248
+ enabled: config.mcpPort !== undefined,
249
+ transport: config.mcpPort ? "http" : null,
250
+ httpPort: config.mcpPort,
251
+ })(state);
252
+ // Check for existing auth (async, updates state when ready)
253
+ void (async () => {
254
+ try {
255
+ const token = await readToken();
256
+ if (token) {
257
+ const user = await validateToken(token);
258
+ if (user) {
259
+ const result = await fetchRemoteProjects();
260
+ state = updateRemote({
261
+ user,
262
+ projects: result.projects.map((p) => ({ id: p.id, name: p.name, slug: p.slug })),
263
+ })(state);
264
+ }
265
+ }
266
+ }
267
+ catch {
268
+ // Auth check failed - non-fatal
269
+ }
270
+ })();
271
+ const write = (text) => writeStdout(text);
272
+ function renderTemplatesView() {
273
+ const lines = [
274
+ "",
275
+ ` ${brand("Templates")}`,
276
+ "",
277
+ ` ${dim("Create a new project from a template:")}`,
278
+ "",
279
+ ];
280
+ state.templates.items.forEach((item, i) => {
281
+ const selected = i === state.templates.selectedIndex;
282
+ const prefix = selected ? brand("›") : " ";
283
+ const label = selected ? brand(item.label) : item.label;
284
+ lines.push(` ${prefix} ${label} ${dim(item.description || "")}`);
285
+ });
286
+ lines.push("");
287
+ lines.push(` ${dim("Press")} ${brand("Enter")} ${dim("to create •")} ${brand("Esc")} ${dim("to go back")}`);
288
+ lines.push("");
289
+ return lines.join("\n");
290
+ }
291
+ function renderExamplesView() {
292
+ const lines = [
293
+ "",
294
+ ` ${brand("Examples")}`,
295
+ "",
296
+ ` ${dim("Create a new project from an example:")}`,
297
+ "",
298
+ ];
299
+ state.examples.items.forEach((item, i) => {
300
+ const selected = i === state.examples.selectedIndex;
301
+ const prefix = selected ? brand("›") : " ";
302
+ const label = selected ? brand(item.label) : item.label;
303
+ lines.push(` ${prefix} ${label} ${dim(item.description || "")}`);
304
+ });
305
+ lines.push("");
306
+ lines.push(` ${dim("Press")} ${brand("Enter")} ${dim("to create •")} ${brand("Esc")} ${dim("to go back")}`);
307
+ lines.push("");
308
+ return lines.join("\n");
309
+ }
310
+ function renderHelpView() {
311
+ const lines = [
312
+ "",
313
+ ` ${brand("Keyboard Shortcuts")}`,
314
+ "",
315
+ ` ${dim("Navigation")}`,
316
+ ` ${brand("↑↓")} ${dim("or")} ${brand("jk")} Navigate list`,
317
+ ` ${brand("Tab")} Switch sections`,
318
+ ` ${brand("1-9")} Quick select item`,
319
+ ` ${brand("Enter")} Select / Open in browser`,
320
+ ` ${brand("Esc")} Go back`,
321
+ "",
322
+ ` ${dim("Actions")}`,
323
+ ` ${brand("o")} Open in browser`,
324
+ ` ${brand("s")} Open in Studio`,
325
+ ` ${brand("i")} Open in IDE`,
326
+ ` ${brand("p")} Pull from remote`,
327
+ ` ${brand("u")} Push to remote`,
328
+ "",
329
+ ` ${dim("Auth")}`,
330
+ ` ${brand("a")} Login`,
331
+ ` ${brand("x")} Logout`,
332
+ "",
333
+ ` ${dim("Views")}`,
334
+ ` ${brand("n")} New project`,
335
+ ` ${brand("l")} Toggle logs`,
336
+ ` ${brand("?")} Help (this screen)`,
337
+ "",
338
+ ` ${dim("Other")}`,
339
+ ` ${brand("q")} Quit`,
340
+ "",
341
+ ];
342
+ if (state.mcp.enabled) {
343
+ lines.push(` ${brand("MCP Server")}`);
344
+ lines.push("");
345
+ lines.push(` ${dim("Add to your")} ${brand("~/.claude/settings.json")}${dim(":")}`);
346
+ lines.push("");
347
+ lines.push(` ${dim('"mcpServers": {')}`);
348
+ lines.push(` ${dim(' "veryfront": {')}`);
349
+ lines.push(` ${dim(' "type": "url",')}`);
350
+ lines.push(` ${dim(` "url": "http://veryfront.me:${state.mcp.httpPort}/mcp"`)}`);
351
+ lines.push(` ${dim(" }")}`);
352
+ lines.push(` ${dim("}")}`);
353
+ lines.push("");
354
+ lines.push(` ${brand("m")} ${dim("Open settings.json in IDE")}`);
355
+ lines.push("");
356
+ lines.push(` ${dim("Tools:")} vf_list_routes, vf_scaffold, vf_get_errors, vf_get_logs`);
357
+ lines.push("");
358
+ }
359
+ lines.push(` ${dim("Press")} ${brand("Esc")} ${dim("to go back")}`);
360
+ lines.push("");
361
+ return lines.join("\n");
362
+ }
363
+ function renderNewProjectView() {
364
+ const options = [
365
+ { label: "From template", desc: "Start with a pre-built template" },
366
+ { label: "From example", desc: "Copy an example project" },
367
+ { label: "From scratch", desc: "Empty project" },
368
+ ];
369
+ const lines = [
370
+ "",
371
+ ` ${brand("New Project")}`,
372
+ "",
373
+ ` ${dim("Choose how to start:")}`,
374
+ "",
375
+ ];
376
+ options.forEach((opt, i) => {
377
+ const isFocused = i === state.newProjectIndex;
378
+ const cursor = isFocused ? brand("›") : " ";
379
+ const num = isFocused ? brand(`[${i + 1}]`) : dim(`[${i + 1}]`);
380
+ const label = isFocused ? opt.label : dim(opt.label);
381
+ const desc = dim(opt.desc);
382
+ lines.push(` ${cursor} ${num} ${label} ${desc}`);
383
+ });
384
+ lines.push("", ` ${dim("↑↓ nav enter select esc back")}`, "");
385
+ return lines.join("\n");
386
+ }
387
+ function renderAuthView() {
388
+ const providers = ["Google", "GitHub", "Microsoft"];
389
+ const lines = [
390
+ "",
391
+ ` ${brand("Login to Veryfront")}`,
392
+ "",
393
+ ` ${dim("Choose authentication provider:")}`,
394
+ "",
395
+ ];
396
+ providers.forEach((p, i) => {
397
+ const isFocused = i === state.authProviderIndex;
398
+ const cursor = isFocused ? brand("›") : " ";
399
+ const num = isFocused ? brand(`[${i + 1}]`) : dim(`[${i + 1}]`);
400
+ const label = isFocused ? p : dim(p);
401
+ lines.push(`${cursor} ${num} ${label}`);
402
+ });
403
+ lines.push("", ` ${dim("↑↓ nav enter select esc back")}`, "");
404
+ return lines.join("\n");
405
+ }
406
+ function render() {
407
+ let content;
408
+ switch (state.view) {
409
+ case "dashboard":
410
+ content = state.projects.items.length > 0 || state.examples.items.length > 0
411
+ ? renderDashboard(state)
412
+ : renderEmptyState();
413
+ break;
414
+ case "new-project":
415
+ content = renderNewProjectView();
416
+ break;
417
+ case "templates":
418
+ content = renderTemplatesView();
419
+ break;
420
+ case "examples":
421
+ content = renderExamplesView();
422
+ break;
423
+ case "auth":
424
+ content = renderAuthView();
425
+ break;
426
+ case "help":
427
+ content = renderHelpView();
428
+ break;
429
+ default:
430
+ content = renderDashboard(state);
431
+ }
432
+ const parts = [content];
433
+ if (state.logs.length > 0) {
434
+ const logsHeader = state.logsExpanded ? "▼ Logs" : "▶ Logs";
435
+ parts.push("");
436
+ parts.push(` ${dim("─".repeat(60))}`);
437
+ parts.push(` ${dim(logsHeader)} ${dim(`(${state.logs.length})`)} ${dim("l")} ${dim("toggle")} ${state.logsExpanded ? `${dim("↑↓")} ${dim("scroll")}` : ""}`);
438
+ parts.push(renderLogs(state.logs, {
439
+ maxLines: state.logsExpanded ? 15 : 3,
440
+ scroll: state.logScroll,
441
+ expanded: state.logsExpanded,
442
+ }));
443
+ }
444
+ if (state.input.active) {
445
+ parts.push("");
446
+ parts.push(` ${dim("─".repeat(60))}`);
447
+ parts.push(renderInput(state.input));
448
+ }
449
+ if (!isInteractiveMode)
450
+ return;
451
+ write(cursor.moveTo(1, 1) + screen.clearDown);
452
+ write(parts.join("\n"));
453
+ }
454
+ function update(updater) {
455
+ state = updater(state);
456
+ if (isInteractiveMode)
457
+ render();
458
+ }
459
+ function startSpinner() {
460
+ if (spinnerInterval)
461
+ return;
462
+ spinnerInterval = dntShim.setInterval(() => {
463
+ spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
464
+ render();
465
+ }, 80);
466
+ }
467
+ function stopSpinner() {
468
+ if (!spinnerInterval)
469
+ return;
470
+ clearInterval(spinnerInterval);
471
+ spinnerInterval = null;
472
+ }
473
+ async function handleInput() {
474
+ // Skip interactive input if not a TTY (e.g., running in background or CI)
475
+ if (!isInteractive())
476
+ return;
477
+ setRawMode(true);
478
+ const reader = getStdinReader();
479
+ const decoder = new TextDecoder();
480
+ try {
481
+ while (running) {
482
+ const { value, done } = await reader.read();
483
+ if (done)
484
+ break;
485
+ await handleKey(decoder.decode(value));
486
+ }
487
+ }
488
+ finally {
489
+ reader.releaseLock();
490
+ try {
491
+ setRawMode(false);
492
+ }
493
+ catch {
494
+ // Ignore if stdin is already closed
495
+ }
496
+ }
497
+ }
498
+ async function handleKey(key) {
499
+ if (state.input.active) {
500
+ const result = handleInputKey(key, state.input.value, state.input.cursorPos);
501
+ if ("action" in result) {
502
+ if (result.action === "submit" && state.input.onSubmit) {
503
+ const value = state.input.value;
504
+ const onSubmit = state.input.onSubmit;
505
+ state = endInput()(state);
506
+ render();
507
+ await onSubmit(value);
508
+ return;
509
+ }
510
+ if (result.action === "cancel") {
511
+ const onCancel = state.input.onCancel;
512
+ state = endInput()(state);
513
+ render();
514
+ onCancel?.();
515
+ return;
516
+ }
517
+ return;
518
+ }
519
+ state = updateInputValue(result.value, result.cursorPos)(state);
520
+ render();
521
+ return;
522
+ }
523
+ if (key === "\x03" || (key === "q" && state.view === "dashboard")) {
524
+ stop();
525
+ exit(0);
526
+ }
527
+ // Handle Escape key (but not escape sequences like arrow keys)
528
+ // Escape alone is \x1b, arrow keys are \x1b[A, \x1b[B, etc.
529
+ if (key === "\x1b") {
530
+ if (state.view !== "dashboard")
531
+ update(goBack());
532
+ return;
533
+ }
534
+ if (state.view === "templates") {
535
+ handleTemplatesKey(key);
536
+ return;
537
+ }
538
+ if (state.view === "examples") {
539
+ handleExamplesKey(key);
540
+ return;
541
+ }
542
+ if (state.view === "new-project") {
543
+ handleNewProjectKey(key);
544
+ return;
545
+ }
546
+ if (state.view === "auth") {
547
+ handleAuthKey(key);
548
+ return;
549
+ }
550
+ if (state.view === "help") {
551
+ update(goBack());
552
+ return;
553
+ }
554
+ // Toggle logs expanded with 'l'
555
+ if (key === "l" || key === "L") {
556
+ update(toggleLogsExpanded());
557
+ return;
558
+ }
559
+ // When logs are expanded, arrow keys scroll logs instead of list
560
+ if (state.logsExpanded && state.logs.length > 0) {
561
+ if (key === "\x1b[A" || key === "k") {
562
+ update(scrollLogs("up"));
563
+ return;
564
+ }
565
+ if (key === "\x1b[B" || key === "j") {
566
+ update(scrollLogs("down"));
567
+ return;
568
+ }
569
+ }
570
+ if (key === "\x1b[A" || key === "k") {
571
+ if (state.activeList === "remoteProjects") {
572
+ const total = state.remote.projects.length;
573
+ const visibleCount = 5;
574
+ const newIndex = state.remote.focusedIndex > 0 ? state.remote.focusedIndex - 1 : total - 1;
575
+ // Adjust scroll offset
576
+ let scrollOffset = state.remote.scrollOffset;
577
+ if (newIndex < scrollOffset) {
578
+ scrollOffset = newIndex;
579
+ }
580
+ else if (newIndex === total - 1) {
581
+ // Wrapped to bottom
582
+ scrollOffset = Math.max(0, total - visibleCount);
583
+ }
584
+ update(updateRemote({ focusedIndex: newIndex, scrollOffset }));
585
+ }
586
+ else {
587
+ update(updateActiveList((list) => moveUp(list)));
588
+ }
589
+ return;
590
+ }
591
+ if (key === "\x1b[B" || key === "j") {
592
+ if (state.activeList === "remoteProjects") {
593
+ const total = state.remote.projects.length;
594
+ const visibleCount = 5;
595
+ const newIndex = state.remote.focusedIndex < total - 1 ? state.remote.focusedIndex + 1 : 0;
596
+ // Adjust scroll offset
597
+ let scrollOffset = state.remote.scrollOffset;
598
+ if (newIndex === 0) {
599
+ // Wrapped to top
600
+ scrollOffset = 0;
601
+ }
602
+ else if (newIndex >= scrollOffset + visibleCount) {
603
+ scrollOffset = newIndex - visibleCount + 1;
604
+ }
605
+ update(updateRemote({ focusedIndex: newIndex, scrollOffset }));
606
+ }
607
+ else {
608
+ update(updateActiveList((list) => moveDown(list, 5)));
609
+ }
610
+ return;
611
+ }
612
+ if (key === "\t") {
613
+ const hasProjects = state.projects.items.length > 0;
614
+ const hasExamples = state.examples.items.length > 0;
615
+ const hasRemoteProjects = state.remote.user && state.remote.projects.length > 0;
616
+ // Build list of available sections in display order
617
+ const sections = [];
618
+ if (hasProjects)
619
+ sections.push("projects");
620
+ if (hasRemoteProjects)
621
+ sections.push("remoteProjects");
622
+ if (hasExamples)
623
+ sections.push("examples");
624
+ if (sections.length > 1) {
625
+ const currentIndex = sections.indexOf(state.activeList);
626
+ const nextIndex = (currentIndex + 1) % sections.length;
627
+ const nextSection = sections[nextIndex];
628
+ if (nextSection)
629
+ update(setActiveList(nextSection));
630
+ }
631
+ return;
632
+ }
633
+ // Number keys for remote project - update focusedIndex (Enter triggers pull)
634
+ if (key >= "1" && key <= "9" && state.activeList === "remoteProjects") {
635
+ const num = parseInt(key, 10);
636
+ const total = state.remote.projects.length;
637
+ if (num <= total) {
638
+ const newIndex = num - 1;
639
+ const visibleCount = 5;
640
+ let scrollOffset = state.remote.scrollOffset;
641
+ if (newIndex < scrollOffset) {
642
+ scrollOffset = newIndex;
643
+ }
644
+ else if (newIndex >= scrollOffset + visibleCount) {
645
+ scrollOffset = newIndex - visibleCount + 1;
646
+ }
647
+ update(updateRemote({ focusedIndex: newIndex, scrollOffset }));
648
+ }
649
+ return;
650
+ }
651
+ // Letter keys for remote project items 10+ (a=10, b=11, etc.)
652
+ // Exclude p (pull), u (push), j/k (vim nav), o/s/i (open actions) shortcuts
653
+ if (key >= "a" && key <= "z" && key !== "j" && key !== "k" && key !== "p" && key !== "u" &&
654
+ key !== "o" && key !== "s" && key !== "i" &&
655
+ state.activeList === "remoteProjects") {
656
+ const num = key.charCodeAt(0) - 96 + 9; // a=10, b=11, etc.
657
+ const total = state.remote.projects.length;
658
+ if (num <= total) {
659
+ const newIndex = num - 1;
660
+ const visibleCount = 5;
661
+ let scrollOffset = state.remote.scrollOffset;
662
+ if (newIndex < scrollOffset) {
663
+ scrollOffset = newIndex;
664
+ }
665
+ else if (newIndex >= scrollOffset + visibleCount) {
666
+ scrollOffset = newIndex - visibleCount + 1;
667
+ }
668
+ update(updateRemote({ focusedIndex: newIndex, scrollOffset }));
669
+ }
670
+ return;
671
+ }
672
+ // Auth: login
673
+ if (key === "a" && !state.remote.user) {
674
+ update(navigateTo("auth"));
675
+ return;
676
+ }
677
+ // Auth: logout
678
+ if (key === "x" && state.remote.user) {
679
+ await logout();
680
+ update(updateRemote({ user: null, projects: [], focusedIndex: 0, scrollOffset: 0 }));
681
+ update(addLog("info", "Logged out"));
682
+ return;
683
+ }
684
+ // Number keys select from active list (1-9) - skip for remoteProjects (handled above)
685
+ if (key >= "1" && key <= "9" && state.activeList !== "remoteProjects") {
686
+ const num = parseInt(key, 10);
687
+ const activeList = state[state.activeList];
688
+ if (num <= activeList.items.length) {
689
+ state = { ...state, [state.activeList]: selectByNumber(activeList, num) };
690
+ render();
691
+ const selected = activeList.items[num - 1];
692
+ if (selected?.data)
693
+ await openInBrowser(selected.data, state.server.port);
694
+ return;
695
+ }
696
+ }
697
+ // Letter keys only work when examples focused (a=1, b=2, etc.)
698
+ // Exclude j/k (vim nav), p/u (pull/push) shortcuts
699
+ if (key >= "a" && key <= "z" && key !== "j" && key !== "k" && key !== "p" && key !== "u" &&
700
+ state.activeList === "examples") {
701
+ const num = key.charCodeAt(0) - 96; // a=1, b=2, ...
702
+ if (num <= state.examples.items.length) {
703
+ state = { ...state, examples: selectByNumber(state.examples, num) };
704
+ render();
705
+ const selected = state.examples.items[num - 1];
706
+ if (selected?.data)
707
+ await openInBrowser(selected.data, state.server.port);
708
+ return;
709
+ }
710
+ }
711
+ if (key === "\r" || key === "\n") {
712
+ // Enter on remote projects: pull
713
+ if (state.activeList === "remoteProjects") {
714
+ const focused = state.remote.projects[state.remote.focusedIndex];
715
+ if (focused) {
716
+ const projectDir = join(cwd(), "projects", focused.slug);
717
+ update(addLog("info", `Pulling to projects/${focused.slug}/...`));
718
+ render();
719
+ try {
720
+ await pullCommand({
721
+ projectSlug: focused.slug,
722
+ projectDir,
723
+ force: true,
724
+ quiet: true,
725
+ });
726
+ update(addLog("info", `Pulled to projects/${focused.slug}/`));
727
+ }
728
+ catch (err) {
729
+ update(addLog("error", `Pull failed: ${err instanceof Error ? err.message : String(err)}`));
730
+ }
731
+ render();
732
+ }
733
+ return;
734
+ }
735
+ // Enter on local projects/examples: open in browser
736
+ const selected = getActiveSelection(state);
737
+ if (selected?.data)
738
+ await openInBrowser(selected.data, state.server.port);
739
+ return;
740
+ }
741
+ if (key === "o") {
742
+ // Open focused remote project in local dev server
743
+ if (state.activeList === "remoteProjects") {
744
+ const focused = state.remote.projects[state.remote.focusedIndex];
745
+ if (focused) {
746
+ const url = `http://${focused.slug}.veryfront.me:${state.server.port}`;
747
+ await openBrowser(url);
748
+ }
749
+ return;
750
+ }
751
+ // Otherwise open local project in browser
752
+ const selected = getActiveSelection(state);
753
+ if (selected?.data)
754
+ await openInBrowser(selected.data, state.server.port);
755
+ return;
756
+ }
757
+ if (key === "s") {
758
+ // Open focused remote project in Studio
759
+ if (state.activeList === "remoteProjects") {
760
+ const focused = state.remote.projects[state.remote.focusedIndex];
761
+ if (focused) {
762
+ const url = `https://veryfront.com/projects/${focused.slug}`;
763
+ await openBrowser(url);
764
+ }
765
+ return;
766
+ }
767
+ // Otherwise open local project in Studio
768
+ const selected = getActiveSelection(state);
769
+ if (selected?.data)
770
+ await openInStudio(selected.data);
771
+ return;
772
+ }
773
+ if (key === "i") {
774
+ // Open focused remote project's local directory in IDE
775
+ if (state.activeList === "remoteProjects") {
776
+ const focused = state.remote.projects[state.remote.focusedIndex];
777
+ if (focused) {
778
+ const projectDir = join(cwd(), "projects", focused.slug);
779
+ await openInIDE({ slug: focused.slug, path: projectDir, type: "local" });
780
+ }
781
+ return;
782
+ }
783
+ // Otherwise open local project in IDE
784
+ const selected = getActiveSelection(state);
785
+ if (selected?.data)
786
+ await openInIDE(selected.data);
787
+ return;
788
+ }
789
+ if (key === "n") {
790
+ state = { ...state, newProjectIndex: 0 };
791
+ update(navigateTo("new-project"));
792
+ return;
793
+ }
794
+ if (key === "?") {
795
+ update(navigateTo("help"));
796
+ return;
797
+ }
798
+ if (key === "m" && state.mcp.enabled) {
799
+ const result = await openMCPSettings();
800
+ update(addLog(result.success ? "info" : "error", result.message ||
801
+ (result.success ? "Opened MCP settings" : "Failed to open MCP settings")));
802
+ return;
803
+ }
804
+ // Pull focused remote project
805
+ if (key === "p" && state.activeList === "remoteProjects") {
806
+ const focused = state.remote.projects[state.remote.focusedIndex];
807
+ if (focused) {
808
+ const projectDir = join(cwd(), "projects", focused.slug);
809
+ update(addLog("info", `Pulling to projects/${focused.slug}/...`));
810
+ render();
811
+ try {
812
+ await pullCommand({
813
+ projectSlug: focused.slug,
814
+ projectDir,
815
+ force: true,
816
+ quiet: true,
817
+ });
818
+ update(addLog("info", `Pulled to projects/${focused.slug}/`));
819
+ }
820
+ catch (err) {
821
+ update(addLog("error", `Pull failed: ${err instanceof Error ? err.message : String(err)}`));
822
+ }
823
+ render();
824
+ }
825
+ return;
826
+ }
827
+ // Pull local project from remote (sync)
828
+ if (key === "p" && state.activeList === "projects") {
829
+ const selected = state.projects.items[state.projects.selectedIndex];
830
+ if (selected?.data) {
831
+ const { slug, path: projectDir } = selected.data;
832
+ update(addLog("info", `Pulling ${slug}...`));
833
+ render();
834
+ try {
835
+ await pullCommand({ projectSlug: slug, projectDir, force: true, quiet: true });
836
+ update(addLog("info", `Pulled ${slug}`));
837
+ }
838
+ catch (err) {
839
+ update(addLog("error", `Pull failed: ${err instanceof Error ? err.message : String(err)}`));
840
+ }
841
+ render();
842
+ }
843
+ return;
844
+ }
845
+ // Push local project
846
+ if (key === "u" && state.activeList === "projects") {
847
+ const selected = state.projects.items[state.projects.selectedIndex];
848
+ if (selected?.data) {
849
+ const { slug, path: projectDir } = selected.data;
850
+ update(addLog("info", `Pushing ${slug}...`));
851
+ render();
852
+ try {
853
+ await pushCommand({ projectSlug: slug, projectDir, force: true, quiet: true });
854
+ update(addLog("info", `Pushed ${slug} — merge in Studio`));
855
+ }
856
+ catch (err) {
857
+ update(addLog("error", `Push failed: ${err instanceof Error ? err.message : String(err)}`));
858
+ }
859
+ render();
860
+ }
861
+ return;
862
+ }
863
+ }
864
+ async function createProject(projectName, template) {
865
+ try {
866
+ state = addLog("info", `Creating project...`)(state);
867
+ render();
868
+ const token = await readToken();
869
+ if (!token) {
870
+ state = addLog("error", "Not authenticated. Press 'a' to login.")(state);
871
+ return;
872
+ }
873
+ // Normalize slug: lowercase, alphanumeric and hyphens only
874
+ const normalizedSlug = projectName.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
875
+ // Use slug as name (capitalize first letter of each word)
876
+ const name = normalizedSlug
877
+ .split("-")
878
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
879
+ .join(" ");
880
+ const apiUrl = getRuntimeEnv().apiUrl || "https://api.veryfront.com";
881
+ const response = await dntShim.fetch(`${apiUrl}/projects`, {
882
+ method: "POST",
883
+ headers: {
884
+ Authorization: `Bearer ${token}`,
885
+ "Content-Type": "application/json",
886
+ Accept: "application/json",
887
+ },
888
+ body: JSON.stringify({ slug: normalizedSlug, name }),
889
+ });
890
+ if (!response.ok) {
891
+ const error = await response.json().catch(() => ({}));
892
+ const msg = error.message || `HTTP ${response.status}`;
893
+ throw new Error(msg);
894
+ }
895
+ const { slug } = await response.json();
896
+ const projectPath = `${cwd()}/projects/${slug}`;
897
+ await initCommand({
898
+ name: `projects/${slug}`,
899
+ template,
900
+ skipInstall: true,
901
+ skipEnvPrompt: true,
902
+ quiet: true,
903
+ });
904
+ const currentProjects = state.projects.items.map((item) => ({
905
+ slug: item.data.slug,
906
+ path: item.data.path,
907
+ }));
908
+ currentProjects.push({ slug, path: projectPath });
909
+ state = setProjects(currentProjects)(state);
910
+ // Refresh remote projects list to include the new project
911
+ const result = await fetchRemoteProjects();
912
+ state = updateRemote({
913
+ projects: result.projects.map((p) => ({ id: p.id, name: p.name, slug: p.slug })),
914
+ })(state);
915
+ state = addLog("info", `Created ${slug}`)(state);
916
+ }
917
+ catch (error) {
918
+ state = addLog("error", `Failed: ${error}`)(state);
919
+ }
920
+ }
921
+ function promptForProjectName(template, onCancel) {
922
+ const suggested = generateRandomSlug();
923
+ state = startInput("Project name", async (name) => {
924
+ if (name.trim())
925
+ await createProject(name.trim(), template);
926
+ state = navigateTo("dashboard")(state);
927
+ render();
928
+ }, onCancel, suggested)(state);
929
+ render();
930
+ }
931
+ function handleTemplatesKey(key) {
932
+ if (key === "\x1b[A" || key === "k") {
933
+ state = { ...state, templates: moveUp(state.templates) };
934
+ render();
935
+ return;
936
+ }
937
+ if (key === "\x1b[B" || key === "j") {
938
+ state = { ...state, templates: moveDown(state.templates, state.templates.items.length) };
939
+ render();
940
+ return;
941
+ }
942
+ if (key === "\r" || key === "\n") {
943
+ const selected = state.templates.items[state.templates.selectedIndex];
944
+ if (selected)
945
+ promptForProjectName(selected.id, () => render());
946
+ }
947
+ }
948
+ function handleExamplesKey(key) {
949
+ if (key === "\x1b[A" || key === "k") {
950
+ state = { ...state, examples: moveUp(state.examples) };
951
+ render();
952
+ return;
953
+ }
954
+ if (key === "\x1b[B" || key === "j") {
955
+ state = { ...state, examples: moveDown(state.examples, state.examples.items.length) };
956
+ render();
957
+ return;
958
+ }
959
+ if (key === "\r" || key === "\n") {
960
+ const selected = state.examples.items[state.examples.selectedIndex];
961
+ if (selected?.data) {
962
+ promptForExampleProject(selected.data, () => render());
963
+ }
964
+ }
965
+ }
966
+ function promptForExampleProject(example, onCancel) {
967
+ const suggested = generateRandomSlug();
968
+ state = startInput("Project name", async (name) => {
969
+ if (name.trim())
970
+ await createProjectFromExample(name.trim(), example);
971
+ state = navigateTo("dashboard")(state);
972
+ render();
973
+ }, onCancel, suggested)(state);
974
+ render();
975
+ }
976
+ async function createProjectFromExample(projectName, example) {
977
+ try {
978
+ state = addLog("info", `Creating project from ${example.slug}...`)(state);
979
+ render();
980
+ const token = await readToken();
981
+ if (!token) {
982
+ state = addLog("error", "Not authenticated. Press 'a' to login.")(state);
983
+ return;
984
+ }
985
+ // Normalize slug
986
+ const normalizedSlug = projectName.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
987
+ const name = normalizedSlug
988
+ .split("-")
989
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
990
+ .join(" ");
991
+ // Create project in API
992
+ const apiUrl = getRuntimeEnv().apiUrl || "https://api.veryfront.com";
993
+ const response = await dntShim.fetch(`${apiUrl}/projects`, {
994
+ method: "POST",
995
+ headers: {
996
+ Authorization: `Bearer ${token}`,
997
+ "Content-Type": "application/json",
998
+ Accept: "application/json",
999
+ },
1000
+ body: JSON.stringify({ slug: normalizedSlug, name }),
1001
+ });
1002
+ if (!response.ok) {
1003
+ const error = await response.json().catch(() => ({}));
1004
+ const msg = error.message || `HTTP ${response.status}`;
1005
+ throw new Error(msg);
1006
+ }
1007
+ const { slug } = await response.json();
1008
+ const projectPath = `${cwd()}/projects/${slug}`;
1009
+ // Copy example files to new project
1010
+ await copyDirectory(example.path, projectPath);
1011
+ // Update local projects list
1012
+ const currentProjects = state.projects.items.map((item) => ({
1013
+ slug: item.data.slug,
1014
+ path: item.data.path,
1015
+ }));
1016
+ currentProjects.push({ slug, path: projectPath });
1017
+ state = setProjects(currentProjects)(state);
1018
+ // Refresh remote projects
1019
+ const result = await fetchRemoteProjects();
1020
+ state = updateRemote({
1021
+ projects: result.projects.map((p) => ({ id: p.id, name: p.name, slug: p.slug })),
1022
+ })(state);
1023
+ state = addLog("info", `Created ${slug} from ${example.slug}`)(state);
1024
+ }
1025
+ catch (error) {
1026
+ state = addLog("error", `Failed: ${error}`)(state);
1027
+ }
1028
+ }
1029
+ function handleNewProjectKey(key) {
1030
+ // Arrow navigation
1031
+ if (key === "\x1b[A" || key === "k") {
1032
+ state = {
1033
+ ...state,
1034
+ newProjectIndex: state.newProjectIndex > 0 ? state.newProjectIndex - 1 : 2,
1035
+ };
1036
+ render();
1037
+ return;
1038
+ }
1039
+ if (key === "\x1b[B" || key === "j") {
1040
+ state = {
1041
+ ...state,
1042
+ newProjectIndex: state.newProjectIndex < 2 ? state.newProjectIndex + 1 : 0,
1043
+ };
1044
+ render();
1045
+ return;
1046
+ }
1047
+ // Number keys to select directly
1048
+ if (key >= "1" && key <= "3") {
1049
+ state = { ...state, newProjectIndex: parseInt(key, 10) - 1 };
1050
+ render();
1051
+ // Fall through to execute the selection
1052
+ }
1053
+ // Enter to confirm selection (or after number key press)
1054
+ if (key === "\r" || key === "\n" || (key >= "1" && key <= "3")) {
1055
+ switch (state.newProjectIndex) {
1056
+ case 0:
1057
+ update(navigateTo("templates"));
1058
+ break;
1059
+ case 1:
1060
+ update(navigateTo("examples"));
1061
+ break;
1062
+ case 2:
1063
+ promptForProjectName("minimal", () => render());
1064
+ break;
1065
+ }
1066
+ }
1067
+ }
1068
+ function handleAuthKey(key) {
1069
+ const providerList = [
1070
+ "google",
1071
+ "github",
1072
+ "microsoft",
1073
+ ];
1074
+ // Arrow navigation
1075
+ if (key === "\x1b[A" || key === "k") {
1076
+ state = {
1077
+ ...state,
1078
+ authProviderIndex: state.authProviderIndex > 0 ? state.authProviderIndex - 1 : 2,
1079
+ };
1080
+ render();
1081
+ return;
1082
+ }
1083
+ if (key === "\x1b[B" || key === "j") {
1084
+ state = {
1085
+ ...state,
1086
+ authProviderIndex: state.authProviderIndex < 2 ? state.authProviderIndex + 1 : 0,
1087
+ };
1088
+ render();
1089
+ return;
1090
+ }
1091
+ // Number keys to select directly
1092
+ if (key >= "1" && key <= "3") {
1093
+ state = { ...state, authProviderIndex: parseInt(key, 10) - 1 };
1094
+ render();
1095
+ return;
1096
+ }
1097
+ // Enter to confirm selection
1098
+ if (key === "\r" || key === "\n") {
1099
+ const provider = providerList[state.authProviderIndex];
1100
+ update(addLog("info", `Opening browser for ${provider} login...`));
1101
+ update(navigateTo("dashboard"));
1102
+ // Run login in background to keep TUI responsive
1103
+ void (async () => {
1104
+ const user = await login(provider);
1105
+ if (user) {
1106
+ const result = await fetchRemoteProjects();
1107
+ update(updateRemote({
1108
+ user,
1109
+ projects: result.projects.map((p) => ({ id: p.id, name: p.name, slug: p.slug })),
1110
+ }));
1111
+ update(addLog("info", `Logged in as ${user.email}`));
1112
+ }
1113
+ render();
1114
+ })();
1115
+ }
1116
+ }
1117
+ function start() {
1118
+ running = true;
1119
+ if (!isInteractiveMode) {
1120
+ console.log(`Server running on http://veryfront.me:${config.port}`);
1121
+ if (config.mcpPort)
1122
+ console.log(`MCP available at http://veryfront.me:${config.mcpPort}/mcp`);
1123
+ return;
1124
+ }
1125
+ write(screen.altOn + cursor.hide);
1126
+ render();
1127
+ handleInput();
1128
+ if (!state.server.running)
1129
+ startSpinner();
1130
+ }
1131
+ function stop() {
1132
+ running = false;
1133
+ stopSpinner();
1134
+ if (isInteractiveMode)
1135
+ write(cursor.show + screen.altOff);
1136
+ }
1137
+ return {
1138
+ start,
1139
+ stop,
1140
+ update,
1141
+ getState: () => state,
1142
+ render,
1143
+ setServerReady: () => {
1144
+ stopSpinner();
1145
+ update(updateServer({ running: true }));
1146
+ },
1147
+ addError: () => {
1148
+ update(updateServer({ errors: state.server.errors + 1 }));
1149
+ },
1150
+ clearErrors: () => {
1151
+ update(updateServer({ errors: 0, warnings: 0 }));
1152
+ },
1153
+ log: (level, message) => {
1154
+ update(addLog(level, message));
1155
+ },
1156
+ interceptConsole: () => {
1157
+ if (!isInteractiveMode)
1158
+ return () => { };
1159
+ const orig = {
1160
+ log: console.log,
1161
+ error: console.error,
1162
+ warn: console.warn,
1163
+ info: console.info,
1164
+ debug: console.debug,
1165
+ };
1166
+ // Parse request log format: " GET /path 200 45ms project:env:release"
1167
+ const parseRequestLog = (msg) => {
1168
+ // Match: whitespace + METHOD + path + status + duration + optional project:env:release
1169
+ const match = msg.match(/^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\S+)\s+(\d{3})\s+(\d+)ms(?:\s+(\S+))?/);
1170
+ if (!match)
1171
+ return undefined;
1172
+ const [, method, path, status, duration, context] = match;
1173
+ const meta = {
1174
+ method,
1175
+ path,
1176
+ status: parseInt(status, 10),
1177
+ durationMs: parseInt(duration, 10),
1178
+ };
1179
+ if (context) {
1180
+ // Parse project:env:release or project:env
1181
+ const parts = context.split(":");
1182
+ if (parts[0])
1183
+ meta.project = parts[0];
1184
+ if (parts[1])
1185
+ meta.env = parts[1];
1186
+ if (parts[2])
1187
+ meta.releaseId = parts[2];
1188
+ }
1189
+ return meta;
1190
+ };
1191
+ // Regex to strip ANSI escape codes (ESC [ ... m)
1192
+ // deno-lint-ignore no-control-regex
1193
+ const ansiPattern = /\x1b\[[0-9;]*m/g;
1194
+ const capture = (level) => (...args) => {
1195
+ const msg = args
1196
+ .map((a) => (typeof a === "string" ? a : JSON.stringify(a)))
1197
+ .join(" ")
1198
+ .replace(ansiPattern, "");
1199
+ if (msg.trim()) {
1200
+ const meta = parseRequestLog(msg);
1201
+ state = addLog(level, msg, meta)(state);
1202
+ render();
1203
+ }
1204
+ };
1205
+ console.log = capture("info");
1206
+ console.error = capture("error");
1207
+ console.warn = capture("warn");
1208
+ console.info = capture("info");
1209
+ console.debug = capture("debug");
1210
+ return () => Object.assign(console, orig);
1211
+ },
1212
+ };
1213
+ }
1214
+ /**
1215
+ * Show startup animation
1216
+ */
1217
+ export async function showStartup(steps) {
1218
+ const write = (text) => writeStdout(text);
1219
+ write(screen.altOn + cursor.hide);
1220
+ for (let i = 0; i < steps.length; i++) {
1221
+ const step = steps[i];
1222
+ const completed = steps.slice(0, i).map((s) => ` ${success("✓")} ${dim(s)}`);
1223
+ const current = ` ${brand("●")} ${step}`;
1224
+ const pending = steps.slice(i + 1).map((s) => ` ${dim("○")} ${dim(s)}`);
1225
+ const content = [
1226
+ "",
1227
+ ` ${brand("Veryfront")} ${dim("starting...")}`,
1228
+ "",
1229
+ ...completed,
1230
+ current,
1231
+ ...pending,
1232
+ "",
1233
+ ].join("\n");
1234
+ write(cursor.moveTo(1, 1) + screen.clearDown + content);
1235
+ await new Promise((r) => dntShim.setTimeout(r, 200));
1236
+ }
1237
+ const allComplete = steps.map((s) => ` ${success("✓")} ${dim(s)}`);
1238
+ const finalContent = [
1239
+ "",
1240
+ ` ${brand("Veryfront")} ${success("ready")}`,
1241
+ "",
1242
+ ...allComplete,
1243
+ "",
1244
+ ].join("\n");
1245
+ write(cursor.moveTo(1, 1) + screen.clearDown + finalContent);
1246
+ await new Promise((r) => dntShim.setTimeout(r, 300));
1247
+ // Don't exit alternate screen - let app.start() continue in it
1248
+ // This prevents a flash when transitioning to the dashboard
1249
+ }
1250
+ export * from "./state.js";
1251
+ export * from "./actions.js";
1252
+ export * from "./components/list-select.js";