veryfront 0.1.90 → 0.1.91

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 (36) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/jobs/schemas.d.ts +30 -30
  3. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts +3 -1
  4. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts.map +1 -1
  5. package/esm/src/platform/adapters/veryfront-api-client/client.js +6 -0
  6. package/esm/src/platform/adapters/veryfront-api-client/index.d.ts +2 -2
  7. package/esm/src/platform/adapters/veryfront-api-client/index.d.ts.map +1 -1
  8. package/esm/src/platform/adapters/veryfront-api-client/index.js +1 -1
  9. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts +24 -0
  10. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
  11. package/esm/src/platform/adapters/veryfront-api-client/operations.js +65 -3
  12. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +28 -0
  13. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts.map +1 -1
  14. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +13 -0
  15. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts +1 -1
  16. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts.map +1 -1
  17. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.js +1 -1
  18. package/esm/src/sandbox/index.d.ts +1 -1
  19. package/esm/src/sandbox/index.d.ts.map +1 -1
  20. package/esm/src/sandbox/index.js +1 -1
  21. package/esm/src/sandbox/sandbox.d.ts +58 -0
  22. package/esm/src/sandbox/sandbox.d.ts.map +1 -1
  23. package/esm/src/sandbox/sandbox.js +111 -0
  24. package/esm/src/server/handlers/dev/styles-css.handler.d.ts +5 -0
  25. package/esm/src/server/handlers/dev/styles-css.handler.d.ts.map +1 -1
  26. package/esm/src/server/handlers/dev/styles-css.handler.js +121 -1
  27. package/package.json +1 -1
  28. package/src/deno.js +1 -1
  29. package/src/src/platform/adapters/veryfront-api-client/client.ts +17 -0
  30. package/src/src/platform/adapters/veryfront-api-client/index.ts +6 -0
  31. package/src/src/platform/adapters/veryfront-api-client/operations.ts +110 -3
  32. package/src/src/platform/adapters/veryfront-api-client/schemas/api.schema.ts +16 -0
  33. package/src/src/platform/adapters/veryfront-api-client/schemas/index.ts +2 -0
  34. package/src/src/sandbox/index.ts +13 -1
  35. package/src/src/sandbox/sandbox.ts +183 -0
  36. package/src/src/server/handlers/dev/styles-css.handler.ts +179 -1
@@ -41,6 +41,60 @@ export interface ExecStreamEvent {
41
41
  exitCode?: number;
42
42
  }
43
43
 
44
+ /** Status of an async command job. */
45
+ export type CommandJobStatus = "running" | "completed" | "failed" | "canceled";
46
+
47
+ /** Heartbeat health status for a command job. */
48
+ export type CommandJobHeartbeatStatus = "disabled" | "healthy" | "degraded";
49
+
50
+ /** An async command job running in a sandbox. */
51
+ export interface CommandJob {
52
+ id: string;
53
+ status: CommandJobStatus;
54
+ exitCode: number | null;
55
+ signal: string | null;
56
+ startedAt: string;
57
+ finishedAt: string | null;
58
+ heartbeatStatus: CommandJobHeartbeatStatus;
59
+ lastHeartbeatAt: string | null;
60
+ lastHeartbeatError: string | null;
61
+ heartbeatFailureCount: number;
62
+ }
63
+
64
+ /** A command job with its captured output. */
65
+ export interface CommandJobOutput extends CommandJob {
66
+ stdout: string;
67
+ stderr: string;
68
+ stdoutTruncated: boolean;
69
+ stderrTruncated: boolean;
70
+ }
71
+
72
+ /** A sandbox session summary returned by list. */
73
+ export interface SandboxSession {
74
+ id: string;
75
+ shortId: string;
76
+ endpoint: string;
77
+ status: string;
78
+ createdAt: string;
79
+ }
80
+
81
+ /** Options for listing sandbox sessions. */
82
+ export interface SandboxListOptions extends SandboxOptions {
83
+ cursor?: string;
84
+ limit?: number;
85
+ }
86
+
87
+ /** Paginated result of sandbox sessions. */
88
+ export interface SandboxListResult {
89
+ data: SandboxSession[];
90
+ pageInfo: {
91
+ self: string | null;
92
+ first: null;
93
+ next: string | null;
94
+ prev: string | null;
95
+ };
96
+ }
97
+
44
98
  /** Client for isolated ephemeral compute environments with command execution and file I/O. */
45
99
  export class Sandbox {
46
100
  private constructor(
@@ -117,6 +171,47 @@ export class Sandbox {
117
171
  return new Sandbox(endpoint, id, authToken, apiUrl);
118
172
  }
119
173
 
174
+ /** List sandbox sessions with optional pagination. */
175
+ static async list(options: SandboxListOptions = {}): Promise<SandboxListResult> {
176
+ const apiUrl = Sandbox.resolveApiUrl(options);
177
+ const authToken = Sandbox.resolveAuthToken(options);
178
+
179
+ const params = new URLSearchParams();
180
+ if (options.cursor) params.set("cursor", options.cursor);
181
+ if (options.limit !== undefined) params.set("limit", String(options.limit));
182
+
183
+ const query = params.toString();
184
+ const url = `${apiUrl}/sandbox-sessions${query ? `?${query}` : ""}`;
185
+
186
+ const res = await dntShim.fetch(url, {
187
+ headers: { Authorization: `Bearer ${authToken}` },
188
+ });
189
+
190
+ if (!res.ok) {
191
+ throw REQUEST_ERROR.create({
192
+ detail: `Failed to list sandboxes: ${res.status} ${await res.text()}`,
193
+ });
194
+ }
195
+
196
+ const json = await res.json();
197
+
198
+ return {
199
+ data: json.data.map((s: Record<string, unknown>) => ({
200
+ id: s.id,
201
+ shortId: s.short_id,
202
+ endpoint: s.endpoint,
203
+ status: s.status,
204
+ createdAt: s.created_at,
205
+ })),
206
+ pageInfo: {
207
+ self: json.page_info?.self ?? null,
208
+ first: null,
209
+ next: json.page_info?.next ?? null,
210
+ prev: json.page_info?.prev ?? null,
211
+ },
212
+ };
213
+ }
214
+
120
215
  private static async waitForReady(
121
216
  apiUrl: string,
122
217
  id: string,
@@ -247,6 +342,94 @@ export class Sandbox {
247
342
  }
248
343
  }
249
344
 
345
+ /** Start an async command job in the sandbox. */
346
+ async startCommandJob(command: string): Promise<CommandJob> {
347
+ const res = await dntShim.fetch(`${this.endpoint}/exec/jobs`, {
348
+ method: "POST",
349
+ headers: {
350
+ Authorization: `Bearer ${this.authToken}`,
351
+ "Content-Type": "application/json",
352
+ },
353
+ body: JSON.stringify({ command }),
354
+ });
355
+
356
+ if (!res.ok) {
357
+ throw REQUEST_ERROR.create({
358
+ detail: `Start command job failed: ${res.status} ${await res.text()}`,
359
+ });
360
+ }
361
+
362
+ return Sandbox.mapCommandJob(await res.json());
363
+ }
364
+
365
+ /** Get the status of an async command job. */
366
+ async getCommandJob(jobId: string): Promise<CommandJob> {
367
+ const res = await dntShim.fetch(`${this.endpoint}/exec/jobs/${jobId}`, {
368
+ headers: { Authorization: `Bearer ${this.authToken}` },
369
+ });
370
+
371
+ if (!res.ok) {
372
+ throw REQUEST_ERROR.create({
373
+ detail: `Get command job failed: ${res.status} ${await res.text()}`,
374
+ });
375
+ }
376
+
377
+ return Sandbox.mapCommandJob(await res.json());
378
+ }
379
+
380
+ /** Get the output of an async command job. */
381
+ async getCommandJobOutput(jobId: string): Promise<CommandJobOutput> {
382
+ const res = await dntShim.fetch(`${this.endpoint}/exec/jobs/${jobId}/output`, {
383
+ headers: { Authorization: `Bearer ${this.authToken}` },
384
+ });
385
+
386
+ if (!res.ok) {
387
+ throw REQUEST_ERROR.create({
388
+ detail: `Get command job output failed: ${res.status} ${await res.text()}`,
389
+ });
390
+ }
391
+
392
+ const json = await res.json();
393
+ return {
394
+ ...Sandbox.mapCommandJob(json),
395
+ stdout: json.stdout,
396
+ stderr: json.stderr,
397
+ stdoutTruncated: json.stdout_truncated,
398
+ stderrTruncated: json.stderr_truncated,
399
+ };
400
+ }
401
+
402
+ /** Cancel an async command job. */
403
+ async cancelCommandJob(jobId: string): Promise<CommandJob> {
404
+ const res = await dntShim.fetch(`${this.endpoint}/exec/jobs/${jobId}/cancel`, {
405
+ method: "POST",
406
+ headers: { Authorization: `Bearer ${this.authToken}` },
407
+ });
408
+
409
+ if (!res.ok) {
410
+ throw REQUEST_ERROR.create({
411
+ detail: `Cancel command job failed: ${res.status} ${await res.text()}`,
412
+ });
413
+ }
414
+
415
+ return Sandbox.mapCommandJob(await res.json());
416
+ }
417
+
418
+ private static mapCommandJob(json: Record<string, unknown>): CommandJob {
419
+ return {
420
+ id: json.id as string,
421
+ status: json.status as CommandJobStatus,
422
+ exitCode: json.exit_code as number | null,
423
+ signal: json.signal as string | null,
424
+ startedAt: json.started_at as string,
425
+ finishedAt: json.finished_at as string | null,
426
+ heartbeatStatus: json.heartbeat_status as CommandJobHeartbeatStatus,
427
+ lastHeartbeatAt: json.last_heartbeat_at as string | null,
428
+ lastHeartbeatError: json.last_heartbeat_error as string | null,
429
+ heartbeatFailureCount: json.heartbeat_failure_count as number,
430
+ };
431
+ }
432
+
250
433
  /** Send a heartbeat to prevent idle timeout. */
251
434
  async heartbeat(): Promise<void> {
252
435
  await dntShim.fetch(`${this.apiUrl}/sandbox-sessions/${this.sessionId}/heartbeat`, {
@@ -11,22 +11,33 @@ import { BaseHandler } from "../response/base.js";
11
11
  import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
12
12
  import { HTTP_OK, PRIORITY_HIGH_DEV } from "../../../utils/constants/index.js";
13
13
  import { joinPath } from "../../../utils/path-utils.js";
14
- import { formatCSSError, getProjectCSS } from "../../../html/styles-builder/tailwind-compiler.js";
14
+ import {
15
+ formatCSSError,
16
+ getCSSByHashAsync,
17
+ getProjectCSS,
18
+ regenerateCSSByHash,
19
+ } from "../../../html/styles-builder/tailwind-compiler.js";
15
20
  import { DEFAULT_STYLESHEET } from "../../../html/styles-builder/css-hash-cache.js";
16
21
  import { resolveStyleContentVersion } from "../../../html/styles-builder/content-version.js";
17
22
  import {
18
23
  createPreparedProjectCSSContext,
24
+ type PreparedProjectCSSRequestContext,
19
25
  storePreparedProjectCSS,
20
26
  tryGetPreparedProjectCSS,
21
27
  } from "../../../html/styles-builder/prepared-project-css-cache.js";
22
28
  import { createStyleScopeProfile } from "../../../html/styles-builder/style-scope-profile.js";
23
29
  import { serverLogger } from "../../../utils/index.js";
24
30
  import type { ResolvedContentContext } from "../../../platform/adapters/fs/veryfront/types.js";
31
+ import type {
32
+ ResolveStyleArtifactInput,
33
+ VeryfrontApiClient,
34
+ } from "../../../platform/adapters/veryfront-api-client/index.js";
25
35
  import { extractProjectCandidates } from "./styles-candidate-scanner.js";
26
36
 
27
37
  const logger = serverLogger.component("styles-css-handler");
28
38
 
29
39
  type GeneratedStylesResult = Awaited<ReturnType<typeof getProjectCSS>>;
40
+ type StyleArtifactSelectorContext = Omit<ResolveStyleArtifactInput, "styleProfileHash">;
30
41
 
31
42
  export class StylesCSSHandler extends BaseHandler {
32
43
  metadata: HandlerMetadata = {
@@ -78,6 +89,25 @@ export class StylesCSSHandler extends BaseHandler {
78
89
  }
79
90
  }
80
91
 
92
+ const remotePrepared = await this.tryResolveRemotePreparedCSS(
93
+ ctx,
94
+ projectScope,
95
+ styleProfile.hash,
96
+ contentContext,
97
+ preparedContext,
98
+ );
99
+ if (remotePrepared) {
100
+ logger.debug("Prepared CSS resolved via style artifact metadata", {
101
+ projectScope,
102
+ styleProfileHash: styleProfile.hash,
103
+ cssHash: remotePrepared.hash,
104
+ });
105
+
106
+ return this.respond(
107
+ responseBuilder.withContentType("text/css; charset=utf-8", remotePrepared.css, HTTP_OK),
108
+ );
109
+ }
110
+
81
111
  let candidates: Set<string>;
82
112
  try {
83
113
  candidates = await extractProjectCandidates(ctx);
@@ -148,6 +178,15 @@ body::before {
148
178
  });
149
179
  }
150
180
 
181
+ if ("hash" in result) {
182
+ await this.registerPreparedCSSArtifact(
183
+ ctx,
184
+ styleProfile.hash,
185
+ contentContext,
186
+ result.hash,
187
+ );
188
+ }
189
+
151
190
  return this.respond(
152
191
  responseBuilder.withContentType("text/css; charset=utf-8", result.css, HTTP_OK),
153
192
  );
@@ -212,6 +251,17 @@ body::before {
212
251
  return typeof fsAdapter.getContentContext === "function" ? fsAdapter.getContentContext() : null;
213
252
  }
214
253
 
254
+ private getVeryfrontApiClient(ctx: HandlerContext): VeryfrontApiClient | null {
255
+ const wrappedFs = ctx.adapter.fs as { getUnderlyingAdapter?: () => unknown };
256
+ if (typeof wrappedFs.getUnderlyingAdapter !== "function") return null;
257
+
258
+ const fsAdapter = wrappedFs.getUnderlyingAdapter() as {
259
+ getClient?: () => VeryfrontApiClient;
260
+ };
261
+
262
+ return typeof fsAdapter.getClient === "function" ? fsAdapter.getClient() : null;
263
+ }
264
+
215
265
  private createPreparedCSSContext(
216
266
  projectScope: string | undefined,
217
267
  rawCss: string,
@@ -237,4 +287,132 @@ body::before {
237
287
  },
238
288
  );
239
289
  }
290
+
291
+ private resolveStyleArtifactSelector(
292
+ contentContext: ResolvedContentContext | null,
293
+ ctx: HandlerContext,
294
+ ): StyleArtifactSelectorContext | null {
295
+ if (contentContext?.sourceType === "branch" && contentContext.branch) {
296
+ return {
297
+ branch: contentContext.branch,
298
+ };
299
+ }
300
+
301
+ if (contentContext?.sourceType === "environment" && contentContext.environmentName) {
302
+ return {
303
+ environmentName: contentContext.environmentName,
304
+ };
305
+ }
306
+
307
+ if (contentContext?.sourceType === "release" && contentContext.releaseId) {
308
+ return {
309
+ releaseId: contentContext.releaseId,
310
+ };
311
+ }
312
+
313
+ if (ctx.parsedDomain?.branch) {
314
+ return {
315
+ branch: ctx.parsedDomain.branch,
316
+ };
317
+ }
318
+
319
+ if (ctx.environmentName) {
320
+ return {
321
+ environmentName: ctx.environmentName,
322
+ };
323
+ }
324
+
325
+ if (ctx.releaseId) {
326
+ return {
327
+ releaseId: ctx.releaseId,
328
+ };
329
+ }
330
+
331
+ return null;
332
+ }
333
+
334
+ private async tryResolveRemotePreparedCSS(
335
+ ctx: HandlerContext,
336
+ projectScope: string | undefined,
337
+ styleProfileHash: string,
338
+ contentContext: ResolvedContentContext | null,
339
+ preparedContext?: PreparedProjectCSSRequestContext,
340
+ ): Promise<{ css: string; hash: string } | undefined> {
341
+ if (!projectScope) return undefined;
342
+
343
+ const selector = this.resolveStyleArtifactSelector(contentContext, ctx);
344
+ if (!selector) return undefined;
345
+
346
+ const client = this.getVeryfrontApiClient(ctx);
347
+ if (!client) return undefined;
348
+
349
+ try {
350
+ const resolved = await client.resolveStyleArtifact({
351
+ ...selector,
352
+ styleProfileHash,
353
+ });
354
+
355
+ if (resolved.status !== "ready" || !resolved.artifactHash) {
356
+ return undefined;
357
+ }
358
+
359
+ const css = await this.getPreparedCSSByHash(resolved.artifactHash, projectScope);
360
+ if (!css) return undefined;
361
+
362
+ if (preparedContext) {
363
+ await storePreparedProjectCSS(preparedContext, {
364
+ css,
365
+ hash: resolved.artifactHash,
366
+ });
367
+ }
368
+
369
+ return {
370
+ css,
371
+ hash: resolved.artifactHash,
372
+ };
373
+ } catch (error) {
374
+ logger.debug("Failed to resolve prepared CSS via style artifact metadata", {
375
+ projectScope,
376
+ styleProfileHash,
377
+ error: error instanceof Error ? error.message : String(error),
378
+ });
379
+ return undefined;
380
+ }
381
+ }
382
+
383
+ private async getPreparedCSSByHash(
384
+ cssHash: string,
385
+ projectScope: string,
386
+ ): Promise<string | undefined> {
387
+ const cached = await getCSSByHashAsync(cssHash);
388
+ if (cached) return cached;
389
+ return regenerateCSSByHash(cssHash, projectScope);
390
+ }
391
+
392
+ private async registerPreparedCSSArtifact(
393
+ ctx: HandlerContext,
394
+ styleProfileHash: string,
395
+ contentContext: ResolvedContentContext | null,
396
+ cssHash: string,
397
+ ): Promise<void> {
398
+ const selector = this.resolveStyleArtifactSelector(contentContext, ctx);
399
+ if (!selector) return;
400
+
401
+ const client = this.getVeryfrontApiClient(ctx);
402
+ if (!client) return;
403
+
404
+ try {
405
+ await client.upsertStyleArtifact({
406
+ ...selector,
407
+ styleProfileHash,
408
+ artifactHash: cssHash,
409
+ });
410
+ } catch (error) {
411
+ logger.debug("Failed to register prepared CSS artifact", {
412
+ cssHash,
413
+ styleProfileHash,
414
+ error: error instanceof Error ? error.message : String(error),
415
+ });
416
+ }
417
+ }
240
418
  }