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,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry OTLP tracing for proxy.
|
|
3
|
+
* Env: OTEL_TRACES_ENABLED, OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS
|
|
4
|
+
*/
|
|
5
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import type { Context, Span, Tracer } from "@opentelemetry/api";
|
|
9
|
+
|
|
10
|
+
// Inline cross-runtime getEnv to avoid dependency on src/platform/compat (not copied in Docker)
|
|
11
|
+
function getEnv(key: string): string | undefined {
|
|
12
|
+
// Deno
|
|
13
|
+
if (typeof dntShim.Deno !== "undefined" && dntShim.Deno.env?.get) {
|
|
14
|
+
return dntShim.Deno.env.get(key);
|
|
15
|
+
}
|
|
16
|
+
// Node.js / Bun
|
|
17
|
+
const nodeProcess = (dntShim.dntGlobalThis as { process?: { env?: Record<string, string> } }).process;
|
|
18
|
+
return nodeProcess?.env?.[key];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let initialized = false;
|
|
22
|
+
let tracerProvider: { shutdown: () => Promise<void> } | null = null;
|
|
23
|
+
let tracer: Tracer | null = null;
|
|
24
|
+
|
|
25
|
+
interface OTLPConfig {
|
|
26
|
+
serviceName: string;
|
|
27
|
+
endpoint: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseHeaders(headerString: string | undefined): Record<string, string> {
|
|
33
|
+
if (!headerString) return {};
|
|
34
|
+
const headers: Record<string, string> = {};
|
|
35
|
+
for (const part of headerString.split(",")) {
|
|
36
|
+
const [key, ...valueParts] = part.split("=");
|
|
37
|
+
if (key && valueParts.length > 0) {
|
|
38
|
+
headers[key.trim()] = valueParts.join("=").trim();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return headers;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getConfig(): OTLPConfig {
|
|
45
|
+
return {
|
|
46
|
+
enabled: getEnv("OTEL_TRACES_ENABLED") === "true",
|
|
47
|
+
serviceName: getEnv("OTEL_SERVICE_NAME") || "veryfront-proxy",
|
|
48
|
+
endpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT") || "",
|
|
49
|
+
headers: parseHeaders(getEnv("OTEL_EXPORTER_OTLP_HEADERS")),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let traceApi: typeof import("@opentelemetry/api") | null = null;
|
|
54
|
+
let propagationApi: typeof import("@opentelemetry/core") | null = null;
|
|
55
|
+
|
|
56
|
+
async function loadApis(): Promise<void> {
|
|
57
|
+
if (traceApi) return;
|
|
58
|
+
traceApi = await import("@opentelemetry/api");
|
|
59
|
+
propagationApi = await import("@opentelemetry/core");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function initializeOTLP(): Promise<void> {
|
|
63
|
+
if (initialized) return;
|
|
64
|
+
|
|
65
|
+
const config = getConfig();
|
|
66
|
+
|
|
67
|
+
if (!config.enabled) {
|
|
68
|
+
initialized = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!config.endpoint) {
|
|
73
|
+
console.warn("[otel] No endpoint configured");
|
|
74
|
+
initialized = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const { trace } = await import("@opentelemetry/api");
|
|
80
|
+
const { BasicTracerProvider, BatchSpanProcessor } = await import(
|
|
81
|
+
"@opentelemetry/sdk-trace-base"
|
|
82
|
+
);
|
|
83
|
+
const { OTLPTraceExporter } = await import("@opentelemetry/exporter-trace-otlp-http");
|
|
84
|
+
const { Resource } = await import("@opentelemetry/resources");
|
|
85
|
+
const { ATTR_SERVICE_NAME } = await import("@opentelemetry/semantic-conventions");
|
|
86
|
+
|
|
87
|
+
const resource = new Resource({ [ATTR_SERVICE_NAME]: config.serviceName });
|
|
88
|
+
const exporter = new OTLPTraceExporter({
|
|
89
|
+
url: `${config.endpoint}/v1/traces`,
|
|
90
|
+
headers: config.headers,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const provider = new BasicTracerProvider({ resource });
|
|
94
|
+
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
|
|
95
|
+
provider.register();
|
|
96
|
+
|
|
97
|
+
tracerProvider = provider;
|
|
98
|
+
tracer = trace.getTracer(config.serviceName);
|
|
99
|
+
initialized = true;
|
|
100
|
+
|
|
101
|
+
await loadApis();
|
|
102
|
+
|
|
103
|
+
console.log("[otel] Initialized", {
|
|
104
|
+
serviceName: config.serviceName,
|
|
105
|
+
endpoint: config.endpoint,
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error("[otel] Init failed", { error });
|
|
109
|
+
initialized = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function shutdownOTLP(): Promise<void> {
|
|
114
|
+
if (tracerProvider) {
|
|
115
|
+
try {
|
|
116
|
+
await tracerProvider.shutdown();
|
|
117
|
+
console.log("[otel] Shutdown complete");
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.warn("[otel] Shutdown error", { error });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isOTLPEnabled(): boolean {
|
|
125
|
+
return initialized && tracerProvider !== null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function extractContext(headers: dntShim.Headers): Context | undefined {
|
|
129
|
+
if (!traceApi || !propagationApi) return undefined;
|
|
130
|
+
const carrier: Record<string, string> = {};
|
|
131
|
+
headers.forEach((v, k) => (carrier[k.toLowerCase()] = v));
|
|
132
|
+
return new propagationApi.W3CTraceContextPropagator().extract(
|
|
133
|
+
traceApi.context.active(),
|
|
134
|
+
carrier,
|
|
135
|
+
traceApi.defaultTextMapGetter,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function injectContext(headers: dntShim.Headers): void {
|
|
140
|
+
if (!traceApi || !propagationApi) return;
|
|
141
|
+
const carrier: Record<string, string> = {};
|
|
142
|
+
new propagationApi.W3CTraceContextPropagator().inject(
|
|
143
|
+
traceApi.context.active(),
|
|
144
|
+
carrier,
|
|
145
|
+
traceApi.defaultTextMapSetter,
|
|
146
|
+
);
|
|
147
|
+
Object.entries(carrier).forEach(([k, v]) => headers.set(k, v));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function startServerSpan(
|
|
151
|
+
method: string,
|
|
152
|
+
path: string,
|
|
153
|
+
parentContext?: Context,
|
|
154
|
+
): { span: Span; context: Context } | null {
|
|
155
|
+
if (!traceApi || !tracer) return null;
|
|
156
|
+
const ctx = parentContext || traceApi.context.active();
|
|
157
|
+
const span = tracer.startSpan(`${method} ${path}`, { kind: traceApi.SpanKind.SERVER }, ctx);
|
|
158
|
+
return { span, context: traceApi.trace.setSpan(ctx, span) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function endSpan(span: Span | undefined, statusCode: number, error?: Error): void {
|
|
162
|
+
if (!span || !traceApi) return;
|
|
163
|
+
span.setAttribute("http.status_code", statusCode);
|
|
164
|
+
if (error) {
|
|
165
|
+
span.setStatus({ code: traceApi.SpanStatusCode.ERROR, message: error.message });
|
|
166
|
+
span.recordException(error);
|
|
167
|
+
} else if (statusCode >= 400) {
|
|
168
|
+
span.setStatus({ code: traceApi.SpanStatusCode.ERROR });
|
|
169
|
+
}
|
|
170
|
+
span.end();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function withContext<T>(spanContext: Context, fn: () => Promise<T>): Promise<T> {
|
|
174
|
+
if (!traceApi) return fn();
|
|
175
|
+
return traceApi.context.with(spanContext, fn);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getTraceContext(): { traceId?: string; spanId?: string } {
|
|
179
|
+
if (!traceApi) return {};
|
|
180
|
+
const span = traceApi.trace.getSpan(traceApi.context.active());
|
|
181
|
+
if (!span) return {};
|
|
182
|
+
const ctx = span.spanContext();
|
|
183
|
+
return { traceId: ctx.traceId, spanId: ctx.spanId };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Span names for proxy tracing.
|
|
188
|
+
*/
|
|
189
|
+
export const ProxySpanNames = {
|
|
190
|
+
PROXY_REQUEST: "proxy.request",
|
|
191
|
+
PROXY_PROCESS: "proxy.process",
|
|
192
|
+
PROXY_TOKEN_FETCH: "proxy.token_fetch",
|
|
193
|
+
PROXY_DOMAIN_LOOKUP: "proxy.domain_lookup",
|
|
194
|
+
OAUTH_TOKEN_REQUEST: "oauth.token_request",
|
|
195
|
+
HTTP_CLIENT_FETCH: "http.client.fetch",
|
|
196
|
+
} as const;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Execute an async function within a tracing span.
|
|
200
|
+
* If tracing is disabled, executes the function directly.
|
|
201
|
+
*/
|
|
202
|
+
export async function withSpan<T>(
|
|
203
|
+
name: string,
|
|
204
|
+
fn: () => Promise<T>,
|
|
205
|
+
attributes?: Record<string, string | number | boolean>,
|
|
206
|
+
): Promise<T> {
|
|
207
|
+
if (!traceApi || !tracer) {
|
|
208
|
+
return await fn();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const parentContext = traceApi.context.active();
|
|
212
|
+
const span = tracer.startSpan(name, {
|
|
213
|
+
kind: traceApi.SpanKind.INTERNAL,
|
|
214
|
+
attributes,
|
|
215
|
+
}, parentContext);
|
|
216
|
+
|
|
217
|
+
const spanContext = traceApi.trace.setSpan(parentContext, span);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = await traceApi.context.with(spanContext, fn);
|
|
221
|
+
span.setStatus({ code: traceApi.SpanStatusCode.OK });
|
|
222
|
+
return result;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
span.setStatus({
|
|
225
|
+
code: traceApi.SpanStatusCode.ERROR,
|
|
226
|
+
message: error instanceof Error ? error.message : String(error),
|
|
227
|
+
});
|
|
228
|
+
if (error instanceof Error) {
|
|
229
|
+
span.recordException(error);
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
} finally {
|
|
233
|
+
span.end();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export { initializeOTLP as initializeOTLPWithApis };
|
package/src/src/cache/backend.ts
CHANGED
|
@@ -671,6 +671,9 @@ export const CacheBackends = {
|
|
|
671
671
|
httpModule: () =>
|
|
672
672
|
createCacheBackend({ keyPrefix: "http-module", circuitBreakerName: "api-cache-http" }),
|
|
673
673
|
|
|
674
|
+
/** SSR module cache for React loader (cross-pod sharing). */
|
|
675
|
+
ssrModule: () => createCacheBackend({ keyPrefix: "ssr-module" }),
|
|
676
|
+
|
|
674
677
|
/** Project CSS cache for Tailwind CSS output (cross-pod sharing). */
|
|
675
678
|
projectCSS: () => createCacheBackend({ keyPrefix: "project-css" }),
|
|
676
679
|
};
|
|
@@ -119,12 +119,3 @@ export function extractCacheKeyContext(handlerCtx: HandlerContext): CacheKeyCont
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export type { MultiProjectRequestContextType as MultiProjectRequestContext };
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @deprecated Use tryGetCacheKeyContext() which auto-detects context
|
|
125
|
-
*/
|
|
126
|
-
export function extractCacheKeyContextFromRequestContext(
|
|
127
|
-
reqCtx: MultiProjectRequestContextType,
|
|
128
|
-
): CacheKeyContext {
|
|
129
|
-
return extractCacheKeyContextFromMultiProjectContext(reqCtx);
|
|
130
|
-
}
|
|
@@ -460,44 +460,3 @@ export class MultiTierCache<T = string> {
|
|
|
460
460
|
return new Map(results);
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Create a memory-backed cache tier from a CacheBackend.
|
|
466
|
-
*/
|
|
467
|
-
export function createMemoryTier(backend: {
|
|
468
|
-
get(key: string): Promise<string | null>;
|
|
469
|
-
set(key: string, value: string, ttlSeconds?: number): Promise<void>;
|
|
470
|
-
del?(key: string): Promise<void>;
|
|
471
|
-
getBatch?(keys: string[]): Promise<Map<string, string | null>>;
|
|
472
|
-
setBatch?(entries: Array<{ key: string; value: string; ttl?: number }>): Promise<void>;
|
|
473
|
-
}): CacheTier<string> {
|
|
474
|
-
return {
|
|
475
|
-
name: "memory",
|
|
476
|
-
get: (key) => backend.get(key),
|
|
477
|
-
set: (key, value, ttl) => backend.set(key, value, ttl),
|
|
478
|
-
delete: backend.del?.bind(backend),
|
|
479
|
-
getBatch: backend.getBatch?.bind(backend),
|
|
480
|
-
setBatch: backend.setBatch?.bind(backend),
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Create a distributed cache tier from a CacheBackend.
|
|
486
|
-
*/
|
|
487
|
-
export function createDistributedTier(backend: {
|
|
488
|
-
readonly type: string;
|
|
489
|
-
get(key: string): Promise<string | null>;
|
|
490
|
-
set(key: string, value: string, ttlSeconds?: number): Promise<void>;
|
|
491
|
-
del?(key: string): Promise<void>;
|
|
492
|
-
getBatch?(keys: string[]): Promise<Map<string, string | null>>;
|
|
493
|
-
setBatch?(entries: Array<{ key: string; value: string; ttl?: number }>): Promise<void>;
|
|
494
|
-
}): CacheTier<string> {
|
|
495
|
-
return {
|
|
496
|
-
name: `distributed-${backend.type}`,
|
|
497
|
-
get: (key) => backend.get(key),
|
|
498
|
-
set: (key, value, ttl) => backend.set(key, value, ttl),
|
|
499
|
-
delete: backend.del?.bind(backend),
|
|
500
|
-
getBatch: backend.getBatch?.bind(backend),
|
|
501
|
-
setBatch: backend.setBatch?.bind(backend),
|
|
502
|
-
};
|
|
503
|
-
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI App Actions
|
|
3
|
+
*
|
|
4
|
+
* Handlers for opening projects in browser, Studio, and IDE.
|
|
5
|
+
* Uses cross-runtime platform abstractions for filesystem and command execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { openBrowser } from "../auth/browser.js";
|
|
9
|
+
import { createFileSystem } from "../../platform/compat/fs.js";
|
|
10
|
+
import { getOsType, runCommand } from "../../platform/compat/process.js";
|
|
11
|
+
import { join } from "../../platform/compat/path/index.js";
|
|
12
|
+
import type { ProjectInfo } from "./state.js";
|
|
13
|
+
import { getRuntimeEnv, type RuntimeEnv } from "../../config/runtime-env.js";
|
|
14
|
+
|
|
15
|
+
export type IDE = "cursor" | "code" | "zed" | "idea" | "webstorm";
|
|
16
|
+
|
|
17
|
+
export interface ActionResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
message?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** IDE command-line executables */
|
|
23
|
+
const IDE_COMMANDS: Record<IDE, string> = {
|
|
24
|
+
cursor: "cursor",
|
|
25
|
+
code: "code",
|
|
26
|
+
zed: "zed",
|
|
27
|
+
idea: "idea",
|
|
28
|
+
webstorm: "webstorm",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** IDE display names */
|
|
32
|
+
const IDE_NAMES: Record<IDE, string> = {
|
|
33
|
+
cursor: "Cursor",
|
|
34
|
+
code: "VS Code",
|
|
35
|
+
zed: "Zed",
|
|
36
|
+
idea: "IntelliJ IDEA",
|
|
37
|
+
webstorm: "WebStorm",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** IDE detection order (preferred first) */
|
|
41
|
+
const IDE_DETECTION_ORDER: IDE[] = ["cursor", "code", "zed", "idea", "webstorm"];
|
|
42
|
+
|
|
43
|
+
/** Cache directories to clear relative to project path */
|
|
44
|
+
const PROJECT_CACHE_DIRS = [".cache", "node_modules/.cache"];
|
|
45
|
+
|
|
46
|
+
function formatError(error: unknown): string {
|
|
47
|
+
return error instanceof Error ? error.message : String(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function commandExists(cmd: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const whichCmd = getOsType() === "windows" ? "where" : "which";
|
|
53
|
+
const result = await runCommand(whichCmd, { args: [cmd] });
|
|
54
|
+
return result.success;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runCommandLocal(cmd: string, args: string[]): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
const result = await runCommand(cmd, { args });
|
|
63
|
+
return result.success;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function openInBrowser(project: ProjectInfo, port: number): Promise<ActionResult> {
|
|
70
|
+
const url = `http://${project.slug}.veryfront.me:${port}`;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await openBrowser(url);
|
|
74
|
+
return { success: true, message: `Opened ${url}` };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { success: false, message: `Failed to open browser: ${formatError(error)}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function openInStudio(project: ProjectInfo): Promise<ActionResult> {
|
|
81
|
+
const url = `https://veryfront.com/projects/${project.slug}`;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await openBrowser(url);
|
|
85
|
+
return { success: true, message: `Opened Studio for ${project.slug}` };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { success: false, message: `Failed to open Studio: ${formatError(error)}` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function detectIDEs(): Promise<IDE[]> {
|
|
92
|
+
const available: IDE[] = [];
|
|
93
|
+
|
|
94
|
+
for (const ide of IDE_DETECTION_ORDER) {
|
|
95
|
+
if (await commandExists(IDE_COMMANDS[ide])) {
|
|
96
|
+
available.push(ide);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return available;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getPreferredIDE(): Promise<IDE | null> {
|
|
104
|
+
const ides = await detectIDEs();
|
|
105
|
+
return ides[0] ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function openPathInIDE(path: string, ide?: IDE): Promise<ActionResult> {
|
|
109
|
+
const targetIDE = ide ?? (await getPreferredIDE());
|
|
110
|
+
|
|
111
|
+
if (!targetIDE) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
message: "No supported IDE found. Install VS Code, Cursor, or Zed.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cmd = IDE_COMMANDS[targetIDE];
|
|
119
|
+
const name = IDE_NAMES[targetIDE];
|
|
120
|
+
|
|
121
|
+
if (await runCommandLocal(cmd, [path])) {
|
|
122
|
+
return { success: true, message: `Opened in ${name}` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { success: false, message: `Failed to open ${name}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function openInIDE(project: ProjectInfo, ide?: IDE): Promise<ActionResult> {
|
|
129
|
+
const result = await openPathInIDE(project.path, ide);
|
|
130
|
+
if (!result.success) return result;
|
|
131
|
+
|
|
132
|
+
const ideName = result.message?.split(" in ")[1];
|
|
133
|
+
return { success: true, message: `Opened ${project.slug} in ${ideName}` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function openFileInIDE(filePath: string, ide?: IDE): Promise<ActionResult> {
|
|
137
|
+
return openPathInIDE(filePath, ide);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function clearProjectCache(project: ProjectInfo): Promise<ActionResult> {
|
|
141
|
+
const fs = createFileSystem();
|
|
142
|
+
let cleared = 0;
|
|
143
|
+
|
|
144
|
+
for (const relativeDir of PROJECT_CACHE_DIRS) {
|
|
145
|
+
const dir = join(project.path, relativeDir);
|
|
146
|
+
try {
|
|
147
|
+
await fs.remove(dir, { recursive: true });
|
|
148
|
+
cleared++;
|
|
149
|
+
} catch {
|
|
150
|
+
// Directory doesn't exist
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const message = cleared > 0 ? `Cleared ${cleared} cache directories` : "No caches to clear";
|
|
155
|
+
return { success: true, message };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function openMCPSettings(env: RuntimeEnv = getRuntimeEnv()): Promise<ActionResult> {
|
|
159
|
+
const home = env.homeDir || "";
|
|
160
|
+
const claudeDir = join(home, ".claude");
|
|
161
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
162
|
+
const fs = createFileSystem();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
166
|
+
} catch {
|
|
167
|
+
// Already exists
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!(await fs.exists(settingsPath))) {
|
|
171
|
+
const defaultSettings = { mcpServers: {} };
|
|
172
|
+
await fs.writeTextFile(settingsPath, JSON.stringify(defaultSettings, null, 2));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return openFileInIDE(settingsPath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function quickOpen(
|
|
179
|
+
projects: Array<{ slug: string; path: string }>,
|
|
180
|
+
num: number,
|
|
181
|
+
port: number,
|
|
182
|
+
): Promise<ActionResult> {
|
|
183
|
+
const index = num - 1;
|
|
184
|
+
if (index < 0 || index >= projects.length) {
|
|
185
|
+
return Promise.resolve({ success: false, message: `No project at position ${num}` });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const project = projects[index]!;
|
|
189
|
+
return openInBrowser({ slug: project.slug, path: project.path, type: "local" }, port);
|
|
190
|
+
}
|