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.
- package/README.md +18 -17
- package/esm/deno.js +1 -1
- package/esm/proxy/cache/index.d.ts +41 -0
- package/esm/proxy/cache/index.d.ts.map +1 -0
- package/esm/proxy/cache/index.js +75 -0
- package/esm/proxy/cache/memory-cache.d.ts +18 -0
- package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
- package/esm/proxy/cache/memory-cache.js +100 -0
- package/esm/proxy/cache/redis-cache.d.ts +27 -0
- package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
- package/esm/proxy/cache/redis-cache.js +183 -0
- package/esm/proxy/cache/resilient-cache.d.ts +44 -0
- package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
- package/esm/proxy/cache/resilient-cache.js +178 -0
- package/esm/proxy/cache/types.d.ts +65 -0
- package/esm/proxy/cache/types.d.ts.map +1 -0
- package/esm/proxy/cache/types.js +7 -0
- package/esm/proxy/handler.d.ts +81 -0
- package/esm/proxy/handler.d.ts.map +1 -0
- package/esm/proxy/handler.js +417 -0
- package/esm/proxy/logger.d.ts +29 -0
- package/esm/proxy/logger.d.ts.map +1 -0
- package/esm/proxy/logger.js +258 -0
- package/esm/proxy/oauth-client.d.ts +15 -0
- package/esm/proxy/oauth-client.d.ts.map +1 -0
- package/esm/proxy/oauth-client.js +52 -0
- package/esm/proxy/token-manager.d.ts +59 -0
- package/esm/proxy/token-manager.d.ts.map +1 -0
- package/esm/proxy/token-manager.js +125 -0
- package/esm/proxy/tracing.d.ts +39 -0
- package/esm/proxy/tracing.d.ts.map +1 -0
- package/esm/proxy/tracing.js +194 -0
- package/esm/src/cache/backend.d.ts +2 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +2 -0
- package/esm/src/cache/cache-key-builder.d.ts +0 -4
- package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
- package/esm/src/cache/cache-key-builder.js +0 -6
- package/esm/src/cache/multi-tier.d.ts +0 -29
- package/esm/src/cache/multi-tier.d.ts.map +1 -1
- package/esm/src/cache/multi-tier.js +0 -26
- package/esm/src/cli/app/actions.d.ts +26 -0
- package/esm/src/cli/app/actions.d.ts.map +1 -0
- package/esm/src/cli/app/actions.js +152 -0
- package/esm/src/cli/app/components/inline-input.d.ts +35 -0
- package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
- package/esm/src/cli/app/components/inline-input.js +220 -0
- package/esm/src/cli/app/components/list-select.d.ts +69 -0
- package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
- package/esm/src/cli/app/components/list-select.js +137 -0
- package/esm/src/cli/app/index.d.ts +45 -0
- package/esm/src/cli/app/index.d.ts.map +1 -0
- package/esm/src/cli/app/index.js +1252 -0
- package/esm/src/cli/app/state.d.ts +122 -0
- package/esm/src/cli/app/state.d.ts.map +1 -0
- package/esm/src/cli/app/state.js +232 -0
- package/esm/src/cli/app/views/dashboard.d.ts +19 -0
- package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
- package/esm/src/cli/app/views/dashboard.js +178 -0
- package/esm/src/cli/commands/dev.js +2 -2
- package/esm/src/cli/commands/new.js +1 -1
- package/esm/src/cli/index/command-router.d.ts.map +1 -1
- package/esm/src/cli/index/command-router.js +9 -39
- package/esm/src/cli/index/start-handler.d.ts +3 -0
- package/esm/src/cli/index/start-handler.d.ts.map +1 -0
- package/esm/src/cli/index/start-handler.js +145 -0
- package/esm/src/cli/mcp/index.d.ts +11 -0
- package/esm/src/cli/mcp/index.d.ts.map +1 -0
- package/esm/src/cli/mcp/index.js +10 -0
- package/esm/src/cli/ui/tui.js +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +34 -13
- package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
- package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
- package/esm/src/server/context/cache-invalidation.js +4 -0
- package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
- package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +139 -64
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/package.json +2 -1
- package/src/deno.js +1 -1
- package/src/proxy/cache/index.ts +93 -0
- package/src/proxy/cache/memory-cache.ts +120 -0
- package/src/proxy/cache/redis-cache.ts +203 -0
- package/src/proxy/cache/resilient-cache.ts +205 -0
- package/src/proxy/cache/types.ts +72 -0
- package/src/proxy/handler.ts +593 -0
- package/src/proxy/logger.ts +329 -0
- package/src/proxy/oauth-client.ts +91 -0
- package/src/proxy/token-manager.ts +174 -0
- package/src/proxy/tracing.ts +237 -0
- package/src/src/cache/backend.ts +3 -0
- package/src/src/cache/cache-key-builder.ts +0 -9
- package/src/src/cache/multi-tier.ts +0 -41
- package/src/src/cli/app/actions.ts +190 -0
- package/src/src/cli/app/components/inline-input.ts +255 -0
- package/src/src/cli/app/components/list-select.ts +215 -0
- package/src/src/cli/app/index.ts +1471 -0
- package/src/src/cli/app/state.ts +385 -0
- package/src/src/cli/app/views/dashboard.ts +212 -0
- package/src/src/cli/commands/dev.ts +2 -2
- package/src/src/cli/commands/new.ts +1 -1
- package/src/src/cli/index/command-router.ts +9 -40
- package/src/src/cli/index/start-handler.ts +195 -0
- package/src/src/cli/mcp/index.ts +11 -0
- package/src/src/cli/ui/tui.ts +1 -1
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +38 -14
- package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
- package/src/src/server/context/cache-invalidation.ts +4 -0
- package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
- package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
- package/src/src/transforms/esm/http-cache.ts +148 -73
- 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";
|