@builder.io/ai-utils 0.107.0 → 0.108.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@builder.io/ai-utils",
3
- "version": "0.107.0",
3
+ "version": "0.108.0",
4
4
  "description": "Builder.io AI utils",
5
5
  "files": [
6
6
  "src"
package/src/codegen.d.ts CHANGED
@@ -140,6 +140,7 @@ export declare const GlobSearchToolInputSchema: z.ZodObject<{
140
140
  export type GlobSearchToolInput = z.infer<typeof GlobSearchToolInputSchema>;
141
141
  export declare const GrepSearchToolInputSchema: z.ZodObject<{
142
142
  query: z.ZodString;
143
+ node_modules_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
143
144
  include_glob: z.ZodOptional<z.ZodNullable<z.ZodString>>;
144
145
  exclude_glob: z.ZodOptional<z.ZodNullable<z.ZodString>>;
145
146
  case_sensitive: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -1515,6 +1516,7 @@ export declare const CodeGenToolMapSchema: z.ZodObject<{
1515
1516
  }, z.core.$strip>;
1516
1517
  Grep: z.ZodObject<{
1517
1518
  query: z.ZodString;
1519
+ node_modules_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1518
1520
  include_glob: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1519
1521
  exclude_glob: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1520
1522
  case_sensitive: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
package/src/codegen.js CHANGED
@@ -143,7 +143,9 @@ export function readToolImageMediaType(filePath) {
143
143
  }
144
144
  export const GlobSearchToolInputSchema = z
145
145
  .object({
146
- pattern: z.string().meta({ description: "The pattern to search for" }),
146
+ pattern: z.string().meta({
147
+ description: "The pattern to search for. node_modules and dist folders are skipped unless the pattern starts with a path inside node_modules (e.g. 'node_modules/@tabler/icons-react/**/*.d.ts').",
148
+ }),
147
149
  })
148
150
  .meta({ title: "GlobSearchToolInput" });
149
151
  export const GrepSearchToolInputSchema = z
@@ -151,6 +153,9 @@ export const GrepSearchToolInputSchema = z
151
153
  query: z.string().meta({
152
154
  description: 'A string literal or a JavaScript-flavored regex to search for in all project files. Only escape special characters such as {} and () in string literals to avoid confusion with regex patterns (e.g. "import \\{ foo \\}"). Avoid over-escaping as that will cause errors. Do NOT use inline regex flags like (?i) or (?s) — they are not supported. The search is case-insensitive by default; use the case_sensitive parameter to control this.',
153
155
  }),
156
+ node_modules_path: z.string().nullish().meta({
157
+ description: "Searches an installed package instead of the project. node_modules and dist folders are skipped by default; set this to a directory or file inside node_modules (e.g. 'node_modules/@tabler/icons-react') to search that package, including its dist folder and .d.ts files.",
158
+ }),
154
159
  include_glob: z.string().nullish().meta({
155
160
  description: "An optional single glob pattern to include only specific files (e.g. '**/*.(ts|tsx)' for all TypeScript files)",
156
161
  }),
package/src/projects.d.ts CHANGED
@@ -472,7 +472,13 @@ export declare const ProjectSkillSchema: z.ZodUnion<readonly [z.ZodObject<{
472
472
  url: z.ZodString;
473
473
  }, z.core.$strip>]>;
474
474
  export type ProjectSkill = z.infer<typeof ProjectSkillSchema>;
475
- export type FusionExecutionEnvironment = "containerized" | "container-less" | "cloud" | "cloud-v2";
475
+ export declare const FusionExecutionEnvironmentSchema: z.ZodEnum<{
476
+ cloud: "cloud";
477
+ "cloud-v2": "cloud-v2";
478
+ "container-less": "container-less";
479
+ containerized: "containerized";
480
+ }>;
481
+ export type FusionExecutionEnvironment = z.infer<typeof FusionExecutionEnvironmentSchema>;
476
482
  export type AgentType = "setup-project" | "project-configuration" | "org-agent" | "code-review-orchestrator" | "design-system-indexer" | "repo-indexer" | "builder-publish-integration";
477
483
  export interface PartialBranchData {
478
484
  name?: string;
@@ -725,6 +731,19 @@ export interface NewBranch extends BranchSharedData, RequiredBranchSharedData {
725
731
  */
726
732
  export type Branch = NewBranch | LegacyBranch;
727
733
  export declare const checkIsNewBranch: (branch: Branch) => branch is NewBranch;
734
+ /** Identifies a single branch by its project and branch name. */
735
+ export declare const BranchIdentifierSchema: z.ZodObject<{
736
+ projectId: z.ZodString;
737
+ branchName: z.ZodString;
738
+ }, z.core.$strip>;
739
+ export type BranchIdentifier = z.infer<typeof BranchIdentifierSchema>;
740
+ /** Query params for `DELETE /projects/branches`. */
741
+ export declare const DeleteBranchOptionsSchema: z.ZodObject<{
742
+ projectId: z.ZodString;
743
+ branchName: z.ZodString;
744
+ hard: z.ZodDefault<z.ZodCodec<z.ZodString, z.ZodBoolean>>;
745
+ }, z.core.$strip>;
746
+ export type DeleteBranchOptions = z.input<typeof DeleteBranchOptionsSchema>;
728
747
  /**
729
748
  * Request options for `GET /projects/branches/list`. Owner/org scope comes from
730
749
  * the authenticated request context; `ownerId` here is a superuser-only override.
@@ -829,6 +848,11 @@ export declare const ShapeOptionSchema: z.ZodObject<{
829
848
  designSystemName: z.ZodString;
830
849
  }, z.core.$strip>], "kind">>>;
831
850
  }, z.core.$strip>>;
851
+ kind: z.ZodOptional<z.ZodEnum<{
852
+ delegate: "delegate";
853
+ "figma-link": "figma-link";
854
+ upload: "upload";
855
+ }>>;
832
856
  }, z.core.$strip>;
833
857
  export type ShapeOption = z.infer<typeof ShapeOptionSchema>;
834
858
  export declare const ShapeQuestionSchema: z.ZodObject<{
@@ -853,14 +877,32 @@ export declare const ShapeQuestionSchema: z.ZodObject<{
853
877
  designSystemName: z.ZodString;
854
878
  }, z.core.$strip>], "kind">>>;
855
879
  }, z.core.$strip>>;
880
+ kind: z.ZodOptional<z.ZodEnum<{
881
+ delegate: "delegate";
882
+ "figma-link": "figma-link";
883
+ upload: "upload";
884
+ }>>;
856
885
  }, z.core.$strip>>;
857
886
  guessOptionId: z.ZodOptional<z.ZodString>;
858
887
  }, z.core.$strip>;
859
888
  export type ShapeQuestion = z.infer<typeof ShapeQuestionSchema>;
889
+ /**
890
+ * The link in canonical form, or undefined when it is not an https link to a
891
+ * file on figma.com. Deliberately strict: the result is written into the
892
+ * user's repo and handed to the agent as a requirement.
893
+ */
894
+ export declare function parseFigmaFileUrl(value: string): string | undefined;
895
+ /**
896
+ * The first Figma file link in `text`, canonicalized by `parseFigmaFileUrl`.
897
+ * Bare and `http:` mentions count as https; trailing punctuation is dropped.
898
+ */
899
+ export declare function findFigmaFileUrl(text: string): string | undefined;
900
+ export declare function mentionsFigmaFileUrl(text: string): boolean;
860
901
  export declare const ShapeAnswerSchema: z.ZodObject<{
861
902
  questionId: z.ZodString;
862
903
  optionId: z.ZodOptional<z.ZodString>;
863
904
  designSystemId: z.ZodOptional<z.ZodString>;
905
+ figmaUrl: z.ZodOptional<z.ZodString>;
864
906
  designSystemName: z.ZodOptional<z.ZodString>;
865
907
  skipped: z.ZodBoolean;
866
908
  delegated: z.ZodBoolean;
@@ -878,6 +920,7 @@ export declare const ShapeAnswerInputSchema: z.ZodObject<{
878
920
  questionId: z.ZodString;
879
921
  optionId: z.ZodOptional<z.ZodString>;
880
922
  designSystemId: z.ZodOptional<z.ZodString>;
923
+ figmaUrl: z.ZodOptional<z.ZodString>;
881
924
  }, z.core.$strip>;
882
925
  export type ShapeAnswerInput = z.infer<typeof ShapeAnswerInputSchema>;
883
926
  export declare const GetShapeQuestionnaireOptionsSchema: z.ZodObject<{
@@ -893,6 +936,7 @@ export declare const SubmitShapeAnswersOptionsSchema: z.ZodObject<{
893
936
  questionId: z.ZodString;
894
937
  optionId: z.ZodOptional<z.ZodString>;
895
938
  designSystemId: z.ZodOptional<z.ZodString>;
939
+ figmaUrl: z.ZodOptional<z.ZodString>;
896
940
  }, z.core.$strip>>;
897
941
  }, z.core.$strip>;
898
942
  export type SubmitShapeAnswersOptions = z.infer<typeof SubmitShapeAnswersOptionsSchema>;
@@ -968,6 +1012,11 @@ export declare const ShapeSessionSchema: z.ZodObject<{
968
1012
  designSystemName: z.ZodString;
969
1013
  }, z.core.$strip>], "kind">>>;
970
1014
  }, z.core.$strip>>;
1015
+ kind: z.ZodOptional<z.ZodEnum<{
1016
+ delegate: "delegate";
1017
+ "figma-link": "figma-link";
1018
+ upload: "upload";
1019
+ }>>;
971
1020
  }, z.core.$strip>>;
972
1021
  guessOptionId: z.ZodOptional<z.ZodString>;
973
1022
  }, z.core.$strip>>;
@@ -976,6 +1025,7 @@ export declare const ShapeSessionSchema: z.ZodObject<{
976
1025
  questionId: z.ZodString;
977
1026
  optionId: z.ZodOptional<z.ZodString>;
978
1027
  designSystemId: z.ZodOptional<z.ZodString>;
1028
+ figmaUrl: z.ZodOptional<z.ZodString>;
979
1029
  designSystemName: z.ZodOptional<z.ZodString>;
980
1030
  skipped: z.ZodBoolean;
981
1031
  delegated: z.ZodBoolean;
@@ -1041,6 +1091,11 @@ export declare const ShapeSessionDocSchema: z.ZodObject<{
1041
1091
  designSystemName: z.ZodString;
1042
1092
  }, z.core.$strip>], "kind">>>;
1043
1093
  }, z.core.$strip>>;
1094
+ kind: z.ZodOptional<z.ZodEnum<{
1095
+ delegate: "delegate";
1096
+ "figma-link": "figma-link";
1097
+ upload: "upload";
1098
+ }>>;
1044
1099
  }, z.core.$strip>>;
1045
1100
  guessOptionId: z.ZodOptional<z.ZodString>;
1046
1101
  }, z.core.$strip>>;
@@ -1049,6 +1104,7 @@ export declare const ShapeSessionDocSchema: z.ZodObject<{
1049
1104
  questionId: z.ZodString;
1050
1105
  optionId: z.ZodOptional<z.ZodString>;
1051
1106
  designSystemId: z.ZodOptional<z.ZodString>;
1107
+ figmaUrl: z.ZodOptional<z.ZodString>;
1052
1108
  designSystemName: z.ZodOptional<z.ZodString>;
1053
1109
  skipped: z.ZodBoolean;
1054
1110
  delegated: z.ZodBoolean;
@@ -3936,6 +3992,24 @@ export declare const EnsureMainBranchExistsOptionsSchema: z.ZodObject<{
3936
3992
  branchName: z.ZodString;
3937
3993
  }, z.core.$strip>;
3938
3994
  export type EnsureMainBranchExistsOptions = z.infer<typeof EnsureMainBranchExistsOptionsSchema>;
3995
+ /** Request body for `POST /projects/ensure-fusion-config`. */
3996
+ export declare const EnsureFusionConfigParamsSchema: z.ZodObject<{
3997
+ projectId: z.ZodString;
3998
+ branchName: z.ZodString;
3999
+ includeBackupMetadata: z.ZodOptional<z.ZodBoolean>;
4000
+ volumeMountPath: z.ZodOptional<z.ZodString>;
4001
+ isLocal: z.ZodOptional<z.ZodBoolean>;
4002
+ nativeApp: z.ZodOptional<z.ZodBoolean>;
4003
+ miseLocationPath: z.ZodOptional<z.ZodString>;
4004
+ _useNI: z.ZodOptional<z.ZodBoolean>;
4005
+ fusionEnvironment: z.ZodOptional<z.ZodEnum<{
4006
+ cloud: "cloud";
4007
+ "cloud-v2": "cloud-v2";
4008
+ "container-less": "container-less";
4009
+ containerized: "containerized";
4010
+ }>>;
4011
+ }, z.core.$strip>;
4012
+ export type EnsureFusionConfigParams = z.infer<typeof EnsureFusionConfigParamsSchema>;
3939
4013
  /**
3940
4014
  * `settings.environmentVariables` is stored with **plaintext** secret values;
3941
4015
  * secrets are masked before this shape leaves ai-services, same as
package/src/projects.js CHANGED
@@ -200,9 +200,27 @@ export const ProjectSkillSchema = z
200
200
  ProjectSkillUrlSchema,
201
201
  ])
202
202
  .meta({ title: "ProjectSkill" });
203
+ export const FusionExecutionEnvironmentSchema = z.enum([
204
+ "containerized",
205
+ "container-less",
206
+ "cloud",
207
+ "cloud-v2",
208
+ ]);
203
209
  export const checkIsNewBranch = (branch) => {
204
210
  return "projectId" in branch;
205
211
  };
212
+ /** Identifies a single branch by its project and branch name. */
213
+ export const BranchIdentifierSchema = z.object({
214
+ projectId: z.string().min(1),
215
+ branchName: z.string().min(1),
216
+ });
217
+ /** Query params for `DELETE /projects/branches`. */
218
+ export const DeleteBranchOptionsSchema = z.object({
219
+ ...BranchIdentifierSchema.shape,
220
+ hard: z.stringbool().default(false).meta({
221
+ description: 'Permanently delete the branch instead of soft-deleting it ("true" / "false").',
222
+ }),
223
+ });
206
224
  /**
207
225
  * Request options for `GET /projects/branches/list`. Owner/org scope comes from
208
226
  * the authenticated request context; `ownerId` here is a superuser-only override.
@@ -310,6 +328,11 @@ export const ShapeOptionSchema = z.object({
310
328
  id: z.string(),
311
329
  label: z.string(),
312
330
  effect: ShapeOptionEffectSchema.optional(),
331
+ /**
332
+ * How the client renders an option that is not a plain choice. Absent means
333
+ * a choice when `effect` is set, delegation otherwise.
334
+ */
335
+ kind: z.enum(["upload", "delegate", "figma-link"]).optional(),
313
336
  });
314
337
  export const ShapeQuestionSchema = z.object({
315
338
  id: z.string(),
@@ -318,10 +341,57 @@ export const ShapeQuestionSchema = z.object({
318
341
  options: z.array(ShapeOptionSchema),
319
342
  guessOptionId: z.string().optional(),
320
343
  });
344
+ /** Figma paths that name a file, which is what the Builder Design tools read. */
345
+ const FIGMA_FILE_PATH = /^\/(?:design|file|proto|board|slides|make)\/[A-Za-z0-9]+/;
346
+ /**
347
+ * The link in canonical form, or undefined when it is not an https link to a
348
+ * file on figma.com. Deliberately strict: the result is written into the
349
+ * user's repo and handed to the agent as a requirement.
350
+ */
351
+ export function parseFigmaFileUrl(value) {
352
+ let url;
353
+ try {
354
+ url = new URL(value.trim());
355
+ }
356
+ catch (_a) {
357
+ return undefined;
358
+ }
359
+ const isFigmaHost = url.hostname === "figma.com" || url.hostname === "www.figma.com";
360
+ if (url.protocol !== "https:" ||
361
+ !isFigmaHost ||
362
+ url.port ||
363
+ url.username ||
364
+ url.password ||
365
+ !FIGMA_FILE_PATH.test(url.pathname)) {
366
+ return undefined;
367
+ }
368
+ return url.href;
369
+ }
370
+ /**
371
+ * Lenient on purpose: users paste bare links, so any mention counts. Not one
372
+ * inside another URL's path or after an `@`, which would name a different host.
373
+ */
374
+ const FIGMA_FILE_MENTION = /(?:^|[^a-z0-9.\/@-])(?:https?:\/\/)?((?:www\.)?figma\.com\/(?:design|file|proto|board|slides|make)\/[a-z0-9]+[^\s<>()[\]{}"'`]*)/i;
375
+ /**
376
+ * The first Figma file link in `text`, canonicalized by `parseFigmaFileUrl`.
377
+ * Bare and `http:` mentions count as https; trailing punctuation is dropped.
378
+ */
379
+ export function findFigmaFileUrl(text) {
380
+ var _a;
381
+ const mention = (_a = FIGMA_FILE_MENTION.exec(text)) === null || _a === void 0 ? void 0 : _a[1];
382
+ if (!mention)
383
+ return undefined;
384
+ return parseFigmaFileUrl(`https://${mention.replace(/[.,;:!?]+$/, "")}`);
385
+ }
386
+ export function mentionsFigmaFileUrl(text) {
387
+ return findFigmaFileUrl(text) !== undefined;
388
+ }
321
389
  export const ShapeAnswerSchema = z.object({
322
390
  questionId: z.string(),
323
391
  optionId: z.string().optional(),
324
392
  designSystemId: z.string().optional(),
393
+ /** Set only for the Figma link option, canonicalized by `parseFigmaFileUrl`. */
394
+ figmaUrl: z.string().optional(),
325
395
  /**
326
396
  * Server-resolved from `designSystemId` against the caller's accessible
327
397
  * design systems. `settings.designSystems` is matched by name downstream, so
@@ -338,6 +408,7 @@ const ShapeAnswerInputBaseSchema = ShapeAnswerSchema.pick({
338
408
  questionId: true,
339
409
  optionId: true,
340
410
  designSystemId: true,
411
+ figmaUrl: true,
341
412
  skipped: true,
342
413
  delegated: true,
343
414
  });
@@ -354,6 +425,14 @@ export const ShapeAnswerInputSchema = z.object({
354
425
  .trim()
355
426
  .min(1)
356
427
  .optional(),
428
+ figmaUrl: ShapeAnswerInputBaseSchema.shape.figmaUrl
429
+ .unwrap()
430
+ .trim()
431
+ .max(2048)
432
+ .refine((value) => parseFigmaFileUrl(value) !== undefined, {
433
+ message: "Must be a link to a Figma file",
434
+ })
435
+ .optional(),
357
436
  });
358
437
  export const GetShapeQuestionnaireOptionsSchema = z.object({
359
438
  projectId: z.string().trim().min(1),
@@ -2214,6 +2293,32 @@ export const EnsureMainBranchExistsOptionsSchema = z.object({
2214
2293
  description: "Branch whose head ref seeds the main branch when it doesn't exist yet. No-op for non-internalHost projects.",
2215
2294
  }),
2216
2295
  });
2296
+ /** Request body for `POST /projects/ensure-fusion-config`. */
2297
+ export const EnsureFusionConfigParamsSchema = z.object({
2298
+ projectId: z.string().min(1),
2299
+ branchName: z.string().min(1),
2300
+ includeBackupMetadata: z.boolean().optional().meta({
2301
+ description: "Whether to include backup metadata in the fusion config. This involves checking backup data and generating download URLs. Defaults to false.",
2302
+ }),
2303
+ volumeMountPath: z.string().optional().meta({
2304
+ description: 'Volume mount path for the container. Defaults to "/app" if not specified.',
2305
+ }),
2306
+ isLocal: z.boolean().optional().meta({
2307
+ description: "Flag for local execution mode. When true, adjusts paths for local execution (e.g., ./workspace instead of /app/workspace).",
2308
+ }),
2309
+ nativeApp: z.boolean().optional().meta({
2310
+ description: "Flag for native app projects. When true, adds nativeApp: true to fusion config.",
2311
+ }),
2312
+ miseLocationPath: z.string().optional().meta({
2313
+ description: "Path to mise installation. Provided by client from desktop app system info.",
2314
+ }),
2315
+ _useNI: z.boolean().optional().meta({
2316
+ description: "Flag to use NI for package management. When true, uses NI for package management. Defaults to true.",
2317
+ }),
2318
+ fusionEnvironment: FusionExecutionEnvironmentSchema.optional().meta({
2319
+ description: "Fusion environment type override. When not provided, the environment is resolved from branch/project settings.",
2320
+ }),
2321
+ });
2217
2322
  /**
2218
2323
  * `settings.environmentVariables` is stored with **plaintext** secret values;
2219
2324
  * secrets are masked before this shape leaves ai-services, same as
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { DeleteBranchOptionsSchema, EnsureFusionConfigParamsSchema, } from "./projects.js";
3
+ describe("DeleteBranchOptionsSchema", () => {
4
+ const ids = { projectId: "p1", branchName: "main" };
5
+ it("defaults hard to false", () => {
6
+ expect(DeleteBranchOptionsSchema.parse(ids)).toEqual({
7
+ ...ids,
8
+ hard: false,
9
+ });
10
+ });
11
+ it("parses hard from its query-string form", () => {
12
+ expect(DeleteBranchOptionsSchema.parse({ ...ids, hard: "true" }).hard).toBe(true);
13
+ expect(DeleteBranchOptionsSchema.parse({ ...ids, hard: "false" }).hard).toBe(false);
14
+ });
15
+ it("rejects a hard value that isn't a boolean string", () => {
16
+ expect(DeleteBranchOptionsSchema.safeParse({ ...ids, hard: "maybe" }).success).toBe(false);
17
+ });
18
+ it("requires a non-empty projectId and branchName", () => {
19
+ expect(DeleteBranchOptionsSchema.safeParse({}).success).toBe(false);
20
+ expect(DeleteBranchOptionsSchema.safeParse({ projectId: "", branchName: "main" })
21
+ .success).toBe(false);
22
+ });
23
+ });
24
+ describe("EnsureFusionConfigParamsSchema", () => {
25
+ const ids = { projectId: "p1", branchName: "main" };
26
+ it("accepts just the identifiers", () => {
27
+ expect(EnsureFusionConfigParamsSchema.parse(ids)).toEqual(ids);
28
+ });
29
+ it("requires a non-empty projectId and branchName", () => {
30
+ expect(EnsureFusionConfigParamsSchema.safeParse({ ...ids, projectId: "" })
31
+ .success).toBe(false);
32
+ expect(EnsureFusionConfigParamsSchema.safeParse({ projectId: "p1" }).success).toBe(false);
33
+ });
34
+ it("rejects an unknown fusionEnvironment", () => {
35
+ expect(EnsureFusionConfigParamsSchema.safeParse({
36
+ ...ids,
37
+ fusionEnvironment: "mainframe",
38
+ }).success).toBe(false);
39
+ });
40
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { AGENT_NATIVE_STARTER_REPO, getAgentNativeRootPackageJsonWriteError, getManagedDatabasePackageJsonError, getProjectState, GetShapeQuestionnaireOptionsSchema, InitialPromptSchema, isProjectDeleted, matchesAgentNativeStarter, ProjectHostingExternalSchema, ProjectHostingSchema, ProjectSettingsExternalSchema, ProjectTemplateOverridesSchema, ShapeSessionSchema, SubmitShapeAnswersOptionsSchema, } from "./projects";
2
+ import { AGENT_NATIVE_STARTER_REPO, getAgentNativeRootPackageJsonWriteError, getManagedDatabasePackageJsonError, getProjectState, GetShapeQuestionnaireOptionsSchema, InitialPromptSchema, isProjectDeleted, matchesAgentNativeStarter, findFigmaFileUrl, mentionsFigmaFileUrl, parseFigmaFileUrl, ProjectHostingExternalSchema, ProjectHostingSchema, ProjectSettingsExternalSchema, ProjectTemplateOverridesSchema, ShapeSessionSchema, SubmitShapeAnswersOptionsSchema, } from "./projects";
3
3
  describe("project hosting schemas", () => {
4
4
  it("accepts the canonical published branch route", () => {
5
5
  expect(ProjectHostingSchema.parse({ publishedBranchId: "branch-live" })).toEqual({ publishedBranchId: "branch-live" });
@@ -112,6 +112,92 @@ describe("guided onboarding project settings", () => {
112
112
  answers: [],
113
113
  })).toThrow();
114
114
  });
115
+ it("accepts a Figma file link on an answer and rejects anything else", () => {
116
+ var _a, _b;
117
+ const submit = (figmaUrl) => SubmitShapeAnswersOptionsSchema.safeParse({
118
+ projectId: "project-1",
119
+ questionnaireId: "questionnaire-1",
120
+ answers: [
121
+ {
122
+ questionId: "visual-style",
123
+ optionId: "figma-link",
124
+ figmaUrl,
125
+ skipped: false,
126
+ delegated: false,
127
+ },
128
+ ],
129
+ });
130
+ expect((_b = (_a = submit(" https://www.figma.com/design/AbC123/App ").data) === null || _a === void 0 ? void 0 : _a.answers[0]) === null || _b === void 0 ? void 0 : _b.figmaUrl).toBe("https://www.figma.com/design/AbC123/App");
131
+ expect(submit("https://example.com/design/AbC123").success).toBe(false);
132
+ expect(submit("").success).toBe(false);
133
+ });
134
+ });
135
+ describe("parseFigmaFileUrl", () => {
136
+ it.each([
137
+ "https://www.figma.com/design/AbC123/My-App?node-id=1-2",
138
+ "https://figma.com/file/AbC123/Old-Style",
139
+ " https://www.figma.com/proto/AbC123/Flow ",
140
+ ])("accepts %s", (value) => {
141
+ expect(parseFigmaFileUrl(value)).toBe(new URL(value.trim()).href);
142
+ });
143
+ it.each([
144
+ "",
145
+ "not a url",
146
+ "http://www.figma.com/design/AbC123/My-App",
147
+ "https://evil.com/figma.com/design/AbC123",
148
+ "https://www.figma.com.evil.com/design/AbC123",
149
+ "https://www.figma.com/community/file/123",
150
+ "https://www.figma.com/design/",
151
+ "https://user:pass@www.figma.com/design/AbC123",
152
+ "https://www.figma.com:8443/design/AbC123",
153
+ "javascript:alert(1)",
154
+ ])("rejects %j", (value) => {
155
+ expect(parseFigmaFileUrl(value)).toBeUndefined();
156
+ });
157
+ });
158
+ describe("mentionsFigmaFileUrl", () => {
159
+ it.each([
160
+ "Build this https://www.figma.com/design/AbC123/My-App please",
161
+ "from figma.com/file/AbC123",
162
+ "(https://figma.com/proto/AbC123)",
163
+ ])("finds a link in %j", (text) => {
164
+ expect(mentionsFigmaFileUrl(text)).toBe(true);
165
+ });
166
+ it.each([
167
+ "Make it look like our Figma mockups",
168
+ "https://notfigma.com/design/AbC123",
169
+ "https://www.figma.com/community",
170
+ "https://evil.com/figma.com/design/AbC123",
171
+ "https://evil.com/?next=//figma.com/design/AbC123",
172
+ "user@figma.com/design/AbC123",
173
+ ])("finds nothing in %j", (text) => {
174
+ expect(mentionsFigmaFileUrl(text)).toBe(false);
175
+ });
176
+ });
177
+ describe("findFigmaFileUrl", () => {
178
+ it.each([
179
+ [
180
+ "Build this https://www.figma.com/design/AbC123/App?node-id=1-2.",
181
+ "https://www.figma.com/design/AbC123/App?node-id=1-2",
182
+ ],
183
+ [
184
+ "from figma.com/file/AbC123/Old, thanks",
185
+ "https://figma.com/file/AbC123/Old",
186
+ ],
187
+ [
188
+ "(http://www.figma.com/proto/AbC123/Flow)",
189
+ "https://www.figma.com/proto/AbC123/Flow",
190
+ ],
191
+ [
192
+ "first https://www.figma.com/design/One/A then https://www.figma.com/design/Two/B",
193
+ "https://www.figma.com/design/One/A",
194
+ ],
195
+ ])("reads the link out of %j", (text, expected) => {
196
+ expect(findFigmaFileUrl(text)).toBe(expected);
197
+ });
198
+ it("finds nothing without a file link", () => {
199
+ expect(findFigmaFileUrl("Make it like our Figma mockups")).toBeUndefined();
200
+ });
115
201
  });
116
202
  describe("matchesAgentNativeStarter", () => {
117
203
  it("matches when repoName is the starter", () => {