veryfront 0.1.89 → 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 (87) hide show
  1. package/esm/cli/commands/knowledge/command.d.ts +3 -0
  2. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  3. package/esm/cli/commands/knowledge/command.js +27 -2
  4. package/esm/deno.js +1 -1
  5. package/esm/src/html/styles-builder/candidate-extractor.d.ts +5 -1
  6. package/esm/src/html/styles-builder/candidate-extractor.d.ts.map +1 -1
  7. package/esm/src/html/styles-builder/candidate-extractor.js +6 -1
  8. package/esm/src/html/styles-builder/content-version.d.ts +9 -0
  9. package/esm/src/html/styles-builder/content-version.d.ts.map +1 -0
  10. package/esm/src/html/styles-builder/content-version.js +15 -0
  11. package/esm/src/html/styles-builder/css-pregeneration.d.ts +11 -0
  12. package/esm/src/html/styles-builder/css-pregeneration.d.ts.map +1 -1
  13. package/esm/src/html/styles-builder/css-pregeneration.js +15 -10
  14. package/esm/src/html/styles-builder/prepared-project-css-cache.d.ts +31 -0
  15. package/esm/src/html/styles-builder/prepared-project-css-cache.d.ts.map +1 -0
  16. package/esm/src/html/styles-builder/prepared-project-css-cache.js +165 -0
  17. package/esm/src/html/styles-builder/style-scope-profile.d.ts +14 -0
  18. package/esm/src/html/styles-builder/style-scope-profile.d.ts.map +1 -0
  19. package/esm/src/html/styles-builder/style-scope-profile.js +121 -0
  20. package/esm/src/jobs/schemas.d.ts +30 -30
  21. package/esm/src/platform/adapters/fs/factory.d.ts.map +1 -1
  22. package/esm/src/platform/adapters/fs/factory.js +2 -21
  23. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
  24. package/esm/src/platform/adapters/fs/veryfront/adapter.js +44 -19
  25. package/esm/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.d.ts +3 -0
  26. package/esm/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.d.ts.map +1 -0
  27. package/esm/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.js +25 -0
  28. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.d.ts.map +1 -1
  29. package/esm/src/platform/adapters/fs/veryfront/proxy-manager.js +2 -15
  30. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +4 -0
  31. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  32. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +2 -0
  33. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts +3 -1
  34. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts.map +1 -1
  35. package/esm/src/platform/adapters/veryfront-api-client/client.js +6 -0
  36. package/esm/src/platform/adapters/veryfront-api-client/index.d.ts +2 -2
  37. package/esm/src/platform/adapters/veryfront-api-client/index.d.ts.map +1 -1
  38. package/esm/src/platform/adapters/veryfront-api-client/index.js +1 -1
  39. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts +24 -0
  40. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
  41. package/esm/src/platform/adapters/veryfront-api-client/operations.js +65 -3
  42. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +28 -0
  43. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts.map +1 -1
  44. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +13 -0
  45. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts +1 -1
  46. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts.map +1 -1
  47. package/esm/src/platform/adapters/veryfront-api-client/schemas/index.js +1 -1
  48. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts +3 -0
  49. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -1
  50. package/esm/src/rendering/orchestrator/css-candidate-manifest.js +10 -5
  51. package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
  52. package/esm/src/rendering/orchestrator/html.js +8 -0
  53. package/esm/src/sandbox/index.d.ts +1 -1
  54. package/esm/src/sandbox/index.d.ts.map +1 -1
  55. package/esm/src/sandbox/index.js +1 -1
  56. package/esm/src/sandbox/sandbox.d.ts +58 -0
  57. package/esm/src/sandbox/sandbox.d.ts.map +1 -1
  58. package/esm/src/sandbox/sandbox.js +111 -0
  59. package/esm/src/server/handlers/dev/styles-candidate-scanner.d.ts.map +1 -1
  60. package/esm/src/server/handlers/dev/styles-candidate-scanner.js +14 -16
  61. package/esm/src/server/handlers/dev/styles-css.handler.d.ts +7 -0
  62. package/esm/src/server/handlers/dev/styles-css.handler.d.ts.map +1 -1
  63. package/esm/src/server/handlers/dev/styles-css.handler.js +175 -8
  64. package/package.json +1 -1
  65. package/src/cli/commands/knowledge/command.ts +30 -2
  66. package/src/deno.js +1 -1
  67. package/src/src/html/styles-builder/candidate-extractor.ts +13 -0
  68. package/src/src/html/styles-builder/content-version.ts +20 -0
  69. package/src/src/html/styles-builder/css-pregeneration.ts +49 -12
  70. package/src/src/html/styles-builder/prepared-project-css-cache.ts +228 -0
  71. package/src/src/html/styles-builder/style-scope-profile.ts +164 -0
  72. package/src/src/platform/adapters/fs/factory.ts +2 -27
  73. package/src/src/platform/adapters/fs/veryfront/adapter.ts +49 -20
  74. package/src/src/platform/adapters/fs/veryfront/default-invalidation-callbacks.ts +35 -0
  75. package/src/src/platform/adapters/fs/veryfront/proxy-manager.ts +4 -21
  76. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +3 -0
  77. package/src/src/platform/adapters/veryfront-api-client/client.ts +17 -0
  78. package/src/src/platform/adapters/veryfront-api-client/index.ts +6 -0
  79. package/src/src/platform/adapters/veryfront-api-client/operations.ts +110 -3
  80. package/src/src/platform/adapters/veryfront-api-client/schemas/api.schema.ts +16 -0
  81. package/src/src/platform/adapters/veryfront-api-client/schemas/index.ts +2 -0
  82. package/src/src/rendering/orchestrator/css-candidate-manifest.ts +28 -6
  83. package/src/src/rendering/orchestrator/html.ts +11 -0
  84. package/src/src/sandbox/index.ts +13 -1
  85. package/src/src/sandbox/sandbox.ts +183 -0
  86. package/src/src/server/handlers/dev/styles-candidate-scanner.ts +18 -15
  87. package/src/src/server/handlers/dev/styles-css.handler.ts +262 -12
@@ -9,6 +9,10 @@ export {
9
9
  type FileDetail,
10
10
  type FileListResult,
11
11
  type ListFilesOptions,
12
+ type ProjectStyleArtifactResolution,
13
+ type ResolveStyleArtifactInput,
14
+ type StyleArtifactSelector,
15
+ type UpsertStyleArtifactInput,
12
16
  VeryfrontAPIOperations,
13
17
  } from "./operations.js";
14
18
  export { type RequestOptions, requestWithRetry, type RetryConfig } from "./retry-handler.js";
@@ -39,4 +43,6 @@ export {
39
43
  ProjectSchema,
40
44
  ReleaseFileDetailSchema,
41
45
  ReleaseFileListItemSchema,
46
+ type StyleArtifactResolveResponse,
47
+ StyleArtifactResolveResponseSchema,
42
48
  } from "./schemas/index.js";
@@ -1,6 +1,6 @@
1
1
  import { logger as baseLogger } from "../../../utils/index.js";
2
2
  import { z } from "zod";
3
- import { requestWithRetry, type RetryConfig } from "./retry-handler.js";
3
+ import { type RequestOptions, requestWithRetry, type RetryConfig } from "./retry-handler.js";
4
4
  import { API_CLIENT_ERROR } from "./types.js";
5
5
  import {
6
6
  BranchFileDetailSchema,
@@ -15,6 +15,7 @@ import {
15
15
  type ProjectFile,
16
16
  ProjectSchema,
17
17
  ReleaseFileDetailSchema,
18
+ StyleArtifactResolveResponseSchema,
18
19
  } from "./schemas/index.js";
19
20
  import { withSpan } from "../../../observability/tracing/otlp-setup.js";
20
21
  import { SpanNames } from "../../../observability/tracing/span-names.js";
@@ -53,6 +54,32 @@ export interface FileDetail {
53
54
  release_version?: string | null;
54
55
  }
55
56
 
57
+ export interface StyleArtifactSelector {
58
+ branch?: string;
59
+ environmentName?: string;
60
+ releaseId?: string;
61
+ }
62
+
63
+ export interface ResolveStyleArtifactInput extends StyleArtifactSelector {
64
+ styleProfileHash: string;
65
+ }
66
+
67
+ export interface UpsertStyleArtifactInput extends ResolveStyleArtifactInput {
68
+ artifactHash: string;
69
+ assetPath?: string;
70
+ contentType?: string;
71
+ etag?: string;
72
+ }
73
+
74
+ export interface ProjectStyleArtifactResolution {
75
+ status: "ready" | "missing";
76
+ artifactHash?: string;
77
+ assetPath?: string;
78
+ etag?: string;
79
+ contentType?: string;
80
+ updatedAt?: string;
81
+ }
82
+
56
83
  function buildListParams(options: ListFilesOptions): URLSearchParams {
57
84
  const { cursor, limit = DEFAULT_PAGE_LIMIT, pattern, sortBy = "updated_at", sortOrder = "desc" } =
58
85
  options;
@@ -81,6 +108,30 @@ function mapProjectFile<T extends ProjectFile>(file: T): ProjectFile {
81
108
  };
82
109
  }
83
110
 
111
+ function buildStyleArtifactParams(input: ResolveStyleArtifactInput): URLSearchParams {
112
+ const params = new URLSearchParams({
113
+ style_profile_hash: input.styleProfileHash,
114
+ });
115
+
116
+ if (input.branch) params.set("branch", input.branch);
117
+ if (input.environmentName) params.set("environment_name", input.environmentName);
118
+ if (input.releaseId) params.set("release_id", input.releaseId);
119
+
120
+ return params;
121
+ }
122
+
123
+ function mapStyleArtifactResolution(raw: unknown): ProjectStyleArtifactResolution {
124
+ const response = StyleArtifactResolveResponseSchema.parse(raw);
125
+ return {
126
+ status: response.status,
127
+ artifactHash: response.artifact_hash,
128
+ assetPath: response.asset_path,
129
+ etag: response.etag,
130
+ contentType: response.content_type,
131
+ updatedAt: response.updated_at,
132
+ };
133
+ }
134
+
84
135
  async function listAllFiles(
85
136
  list: (cursor?: string) => Promise<FileListResult>,
86
137
  ): Promise<ProjectFile[]> {
@@ -423,11 +474,67 @@ export class VeryfrontAPIOperations {
423
474
  );
424
475
  }
425
476
 
426
- private request(endpoint: string): Promise<unknown> {
477
+ async resolveStyleArtifact(
478
+ projectRef: string,
479
+ input: ResolveStyleArtifactInput,
480
+ ): Promise<ProjectStyleArtifactResolution> {
481
+ const params = buildStyleArtifactParams(input);
482
+ const url = `/projects/${encodeURIComponent(projectRef)}/style-artifacts/current?${params}`;
483
+ logger.debug("resolveStyleArtifact", {
484
+ projectRef,
485
+ branch: input.branch,
486
+ environmentName: input.environmentName,
487
+ releaseId: input.releaseId,
488
+ styleProfileHash: input.styleProfileHash,
489
+ });
490
+
491
+ return mapStyleArtifactResolution(await this.request(url));
492
+ }
493
+
494
+ async upsertStyleArtifact(
495
+ projectRef: string,
496
+ input: UpsertStyleArtifactInput,
497
+ ): Promise<ProjectStyleArtifactResolution> {
498
+ const url = `/projects/${encodeURIComponent(projectRef)}/style-artifacts/current`;
499
+ logger.debug("upsertStyleArtifact", {
500
+ projectRef,
501
+ branch: input.branch,
502
+ environmentName: input.environmentName,
503
+ releaseId: input.releaseId,
504
+ styleProfileHash: input.styleProfileHash,
505
+ artifactHash: input.artifactHash,
506
+ });
507
+
508
+ return mapStyleArtifactResolution(
509
+ await this.request(url, {
510
+ method: "PUT",
511
+ headers: {
512
+ "Content-Type": "application/json",
513
+ },
514
+ body: JSON.stringify({
515
+ style_profile_hash: input.styleProfileHash,
516
+ branch: input.branch,
517
+ environment_name: input.environmentName,
518
+ release_id: input.releaseId,
519
+ artifact_hash: input.artifactHash,
520
+ asset_path: input.assetPath,
521
+ content_type: input.contentType,
522
+ etag: input.etag,
523
+ }),
524
+ }),
525
+ );
526
+ }
527
+
528
+ private request(endpoint: string, options: RequestOptions = {}): Promise<unknown> {
427
529
  return withSpan(
428
530
  SpanNames.API_REQUEST,
429
531
  () =>
430
- requestWithRetry(`${this.apiBaseUrl}${endpoint}`, this.tokenProvider(), this.retryConfig),
532
+ requestWithRetry(
533
+ `${this.apiBaseUrl}${endpoint}`,
534
+ this.tokenProvider(),
535
+ this.retryConfig,
536
+ options,
537
+ ),
431
538
  { "api.endpoint": endpoint, "api.base_url": this.apiBaseUrl },
432
539
  );
433
540
  }
@@ -147,6 +147,15 @@ export const LookupDomainResponseSchema = z.object({
147
147
  release_id: z.string().uuid().nullable(),
148
148
  });
149
149
 
150
+ export const StyleArtifactResolveResponseSchema = z.object({
151
+ status: z.enum(["ready", "missing"]),
152
+ artifact_hash: z.string().optional(),
153
+ asset_path: z.string().optional(),
154
+ etag: z.string().optional(),
155
+ content_type: z.string().optional(),
156
+ updated_at: z.string().optional(),
157
+ });
158
+
150
159
  export type Project = z.infer<typeof ProjectSchema>;
151
160
  export type ProjectFile = z.infer<typeof ProjectFileSchema>;
152
161
  export type PageInfo = z.infer<typeof PageInfoSchema>;
@@ -165,6 +174,7 @@ export type ListReleaseFilesResponse = z.infer<typeof ListReleaseFilesResponseSc
165
174
  export type ReleaseFileDetail = z.infer<typeof ReleaseFileDetailSchema>;
166
175
 
167
176
  export type LookupDomainResponse = z.infer<typeof LookupDomainResponseSchema>;
177
+ export type StyleArtifactResolveResponse = z.infer<typeof StyleArtifactResolveResponseSchema>;
168
178
 
169
179
  export const API_ENDPOINTS = {
170
180
  listProjects: {
@@ -212,4 +222,10 @@ export const API_ENDPOINTS = {
212
222
  path: "/projects/{domain}",
213
223
  description: "Look up project by custom domain (resolved via project_reference)",
214
224
  },
225
+ resolveStyleArtifact: {
226
+ method: "GET" as const,
227
+ path: "/projects/{projectRef}/style-artifacts/current",
228
+ description:
229
+ "Resolve metadata for the latest ready style artifact for a branch, environment, or release selector",
230
+ },
215
231
  } as const;
@@ -35,4 +35,6 @@ export {
35
35
  ReleaseFileDetailSchema,
36
36
  type ReleaseFileListItem,
37
37
  ReleaseFileListItemSchema,
38
+ type StyleArtifactResolveResponse,
39
+ StyleArtifactResolveResponseSchema,
38
40
  } from "./api.schema.js";
@@ -1,4 +1,8 @@
1
1
  import { extractCandidates } from "../../html/styles-builder/tailwind-compiler.js";
2
+ import {
3
+ filterFilesForStyleScope,
4
+ type StyleScopeProfile,
5
+ } from "../../html/styles-builder/style-scope-profile.js";
2
6
  import { getRouteModulePaths } from "../../modules/manifest/route-module-manifest.js";
3
7
  import { rendererLogger } from "../../utils/index.js";
4
8
 
@@ -17,6 +21,7 @@ interface RouteCandidateOptions {
17
21
  projectScope: string;
18
22
  projectVersion: string;
19
23
  projectDir: string;
24
+ styleProfile?: StyleScopeProfile;
20
25
  routeKey: string;
21
26
  routeFilePaths: string[];
22
27
  files: SourceFileLike[];
@@ -27,6 +32,7 @@ interface ProjectCandidateOptions {
27
32
  projectScope: string;
28
33
  projectVersion: string;
29
34
  projectDir: string;
35
+ styleProfile?: StyleScopeProfile;
30
36
  files: SourceFileLike[];
31
37
  developmentMode: boolean;
32
38
  }
@@ -51,8 +57,12 @@ function toRelativeProjectPath(path: string, projectDir: string): string {
51
57
  return normalized.replace(/^\/+/, "");
52
58
  }
53
59
 
54
- function buildManifestCacheKey(projectScope: string, projectVersion: string): string {
55
- return `${projectScope}:${projectVersion}`;
60
+ function buildManifestCacheKey(
61
+ projectScope: string,
62
+ projectVersion: string,
63
+ styleProfileHash?: string,
64
+ ): string {
65
+ return `${projectScope}:${projectVersion}:${styleProfileHash ?? "default"}`;
56
66
  }
57
67
 
58
68
  function shouldRebuildManifest(
@@ -101,13 +111,20 @@ function buildCandidateManifest(files: SourceFileLike[], projectDir: string): Ca
101
111
  function getOrBuildManifest(
102
112
  options: Pick<
103
113
  ProjectCandidateOptions,
104
- "projectScope" | "projectVersion" | "projectDir" | "files" | "developmentMode"
114
+ "projectScope" | "projectVersion" | "projectDir" | "files" | "developmentMode" | "styleProfile"
105
115
  >,
106
116
  ): CandidateManifest {
107
- const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
117
+ const manifestKey = buildManifestCacheKey(
118
+ options.projectScope,
119
+ options.projectVersion,
120
+ options.styleProfile?.hash,
121
+ );
108
122
  const existingManifest = manifestCache.get(manifestKey);
123
+ const scopedFiles = options.styleProfile
124
+ ? filterFilesForStyleScope(options.files, options.styleProfile, options.projectDir)
125
+ : options.files;
109
126
  const manifest = shouldRebuildManifest(existingManifest, options.developmentMode)
110
- ? buildCandidateManifest(options.files, options.projectDir)
127
+ ? buildCandidateManifest(scopedFiles, options.projectDir)
111
128
  : existingManifest!;
112
129
 
113
130
  if (manifest !== existingManifest) {
@@ -139,7 +156,11 @@ function addCandidatesForPath(
139
156
  * Resolve route-scoped Tailwind candidates from a precomputed per-project manifest.
140
157
  */
141
158
  export function getRouteCandidates(options: RouteCandidateOptions): Set<string> {
142
- const manifestKey = buildManifestCacheKey(options.projectScope, options.projectVersion);
159
+ const manifestKey = buildManifestCacheKey(
160
+ options.projectScope,
161
+ options.projectVersion,
162
+ options.styleProfile?.hash,
163
+ );
143
164
  const manifest = getOrBuildManifest(options);
144
165
  const routeCacheKey = `${manifestKey}:${options.routeKey}`;
145
166
  const cachedRoute = routeCandidateCache.get(routeCacheKey);
@@ -167,6 +188,7 @@ export function getRouteCandidates(options: RouteCandidateOptions): Set<string>
167
188
  logger.debug("Resolved route candidates", {
168
189
  projectScope: options.projectScope,
169
190
  projectVersion: options.projectVersion,
191
+ styleProfileHash: options.styleProfile?.hash,
170
192
  route: options.routeKey,
171
193
  count: routeCandidates.size,
172
194
  });
@@ -29,6 +29,9 @@ import {
29
29
  rewriteCssModuleContent,
30
30
  } from "../../transforms/css-modules/naming.js";
31
31
  import { getRouteCandidates } from "./css-candidate-manifest.js";
32
+ import { resolveStyleContentVersion } from "../../html/styles-builder/content-version.js";
33
+ import { createStyleScopeProfile } from "../../html/styles-builder/style-scope-profile.js";
34
+ import type { ResolvedContentContext } from "../../platform/adapters/fs/veryfront/types.js";
32
35
 
33
36
  const logger = rendererLogger.component("html-generator");
34
37
 
@@ -453,9 +456,16 @@ export class HTMLGenerator {
453
456
  if (typeof wrappedFs.getUnderlyingAdapter !== "function") return undefined;
454
457
 
455
458
  const fsAdapter = wrappedFs.getUnderlyingAdapter() as {
459
+ getContentContext?: () => ResolvedContentContext | null;
456
460
  getProjectData?: () => { updated_at?: string } | undefined;
457
461
  };
458
462
 
463
+ const contentContext = typeof fsAdapter.getContentContext === "function"
464
+ ? fsAdapter.getContentContext()
465
+ : null;
466
+
467
+ if (contentContext) return resolveStyleContentVersion(contentContext);
468
+
459
469
  return fsAdapter.getProjectData?.()?.updated_at;
460
470
  }
461
471
 
@@ -505,6 +515,7 @@ export class HTMLGenerator {
505
515
  projectScope,
506
516
  projectVersion,
507
517
  projectDir: this.config.projectDir,
518
+ styleProfile: createStyleScopeProfile(this.config.config),
508
519
  routeKey,
509
520
  routeFilePaths,
510
521
  files,
@@ -19,4 +19,16 @@
19
19
  import "../../_dnt.polyfills.js";
20
20
 
21
21
 
22
- export { type ExecResult, type ExecStreamEvent, Sandbox, type SandboxOptions } from "./sandbox.js";
22
+ export {
23
+ type CommandJob,
24
+ type CommandJobHeartbeatStatus,
25
+ type CommandJobOutput,
26
+ type CommandJobStatus,
27
+ type ExecResult,
28
+ type ExecStreamEvent,
29
+ Sandbox,
30
+ type SandboxListOptions,
31
+ type SandboxListResult,
32
+ type SandboxOptions,
33
+ type SandboxSession,
34
+ } from "./sandbox.js";
@@ -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`, {
@@ -9,6 +9,12 @@
9
9
  */
10
10
 
11
11
  import { extractCandidates } from "../../../html/styles-builder/tailwind-compiler.js";
12
+ import { resolveStyleContentVersion } from "../../../html/styles-builder/content-version.js";
13
+ import {
14
+ createStyleScopeProfile,
15
+ shouldIncludeStylePath,
16
+ shouldTraverseStyleDirectory,
17
+ } from "../../../html/styles-builder/style-scope-profile.js";
12
18
  import { serverLogger } from "../../../utils/index.js";
13
19
  import { createFileSystem } from "../../../platform/compat/fs.js";
14
20
  import { join } from "../../../platform/compat/path/index.js";
@@ -20,7 +26,6 @@ import { FRAMEWORK_CANDIDATES } from "./framework-candidates.generated.js";
20
26
  const logger = serverLogger.component("styles-candidate-scanner");
21
27
 
22
28
  const SOURCE_EXTENSIONS = [".tsx", ".jsx", ".mdx", ".ts", ".js"];
23
- const SKIP_DIRS = new Set(["node_modules", ".cache", ".git", "dist", "build", ".vscode"]);
24
29
 
25
30
  /** De-duplicated set of framework candidates, computed once at import time. */
26
31
  const frameworkCandidates = new Set<string>(FRAMEWORK_CANDIDATES);
@@ -32,18 +37,6 @@ interface SourceFileProvider {
32
37
  getContentContext?: () => ResolvedContentContext | null;
33
38
  }
34
39
 
35
- function resolveProjectVersion(
36
- ctx: HandlerContext,
37
- contentContext: ResolvedContentContext | null,
38
- ): string {
39
- if (contentContext?.releaseId) return `release:${contentContext.releaseId}`;
40
- if (contentContext?.branch) return `branch:${contentContext.branch}`;
41
- if (contentContext?.environmentName) return `environment:${contentContext.environmentName}`;
42
- if (ctx.releaseId) return `release:${ctx.releaseId}`;
43
- if (ctx.parsedDomain?.branch) return `branch:${ctx.parsedDomain.branch}`;
44
- return "live";
45
- }
46
-
47
40
  /**
48
41
  * Extract Tailwind CSS candidate class names from all project source files.
49
42
  *
@@ -52,6 +45,7 @@ function resolveProjectVersion(
52
45
  * method is available (local dev mode).
53
46
  */
54
47
  export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set<string>> {
48
+ const styleProfile = createStyleScopeProfile(ctx.config);
55
49
  const wrappedFs = ctx.adapter.fs as { getUnderlyingAdapter?: () => unknown };
56
50
 
57
51
  if (typeof wrappedFs.getUnderlyingAdapter !== "function") {
@@ -83,8 +77,13 @@ export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set
83
77
  for (
84
78
  const cls of getProjectCandidates({
85
79
  projectScope: ctx.projectSlug ?? contentContext?.projectSlug ?? ctx.projectDir,
86
- projectVersion: resolveProjectVersion(ctx, contentContext),
80
+ projectVersion: resolveStyleContentVersion(contentContext, {
81
+ releaseId: ctx.releaseId,
82
+ branch: ctx.parsedDomain?.branch,
83
+ environmentName: ctx.environmentName,
84
+ }),
87
85
  projectDir: ctx.projectDir,
86
+ styleProfile,
88
87
  files,
89
88
  developmentMode: contentContext?.sourceType === "branch",
90
89
  })
@@ -100,6 +99,7 @@ export async function extractProjectCandidates(ctx: HandlerContext): Promise<Set
100
99
  * Used in local development mode where projects are read directly from disk.
101
100
  */
102
101
  async function scanLocalFiles(projectDir: string, ctx: HandlerContext): Promise<Set<string>> {
102
+ const styleProfile = createStyleScopeProfile(ctx.config);
103
103
  const candidates = new Set<string>(frameworkCandidates);
104
104
  const fs = createFileSystem();
105
105
 
@@ -116,11 +116,14 @@ async function scanLocalFiles(projectDir: string, ctx: HandlerContext): Promise<
116
116
  const fullPath = join(dir, entry.name);
117
117
 
118
118
  if (entry.isDirectory) {
119
- if (!SKIP_DIRS.has(entry.name)) await scanDir(fullPath);
119
+ if (shouldTraverseStyleDirectory(styleProfile, fullPath, projectDir)) {
120
+ await scanDir(fullPath);
121
+ }
120
122
  continue;
121
123
  }
122
124
 
123
125
  if (!entry.isFile) continue;
126
+ if (!shouldIncludeStylePath(styleProfile, fullPath, projectDir)) continue;
124
127
  if (!SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) continue;
125
128
 
126
129
  try {