@tour-kit/studio 0.1.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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/dist/index.js +1743 -0
  3. package/package.json +35 -0
package/dist/index.js ADDED
@@ -0,0 +1,1743 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { defineCommand as defineCommand6, runMain } from "citty";
5
+
6
+ // src/commands/add.ts
7
+ import { existsSync as existsSync2 } from "fs";
8
+ import { mkdir, readFile, writeFile } from "fs/promises";
9
+ import { dirname, join as join2, relative } from "path";
10
+
11
+ // ../recipe/src/announcement.ts
12
+ import { z as z2 } from "zod";
13
+
14
+ // ../recipe/src/primitives.ts
15
+ import { z } from "zod";
16
+ var recipeId = z.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/);
17
+ var semver = z.string().regex(/^\d+\.\d+\.\d+(?:-[\w.]+)?$/);
18
+ var dateString = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
19
+ var timeString = z.string().regex(/^\d{2}:\d{2}$/);
20
+ var placement = z.enum([
21
+ "top",
22
+ "right",
23
+ "bottom",
24
+ "left",
25
+ "top-start",
26
+ "top-end",
27
+ "right-start",
28
+ "right-end",
29
+ "bottom-start",
30
+ "bottom-end",
31
+ "left-start",
32
+ "left-end"
33
+ ]);
34
+ var mediaType = z.enum([
35
+ "auto",
36
+ "youtube",
37
+ "vimeo",
38
+ "loom",
39
+ "wistia",
40
+ "video",
41
+ "gif",
42
+ "lottie",
43
+ "image"
44
+ ]);
45
+ var aspectRatio = z.enum([
46
+ "16/9",
47
+ "4/3",
48
+ "1/1",
49
+ "9/16",
50
+ "21/9",
51
+ "auto"
52
+ ]);
53
+ var urlOrRootPath = z.string().min(1).refine(
54
+ (s) => s.startsWith("/") || z.url().safeParse(s).success,
55
+ "media src must be an absolute URL or root-relative path"
56
+ );
57
+ var media = z.object({
58
+ src: urlOrRootPath,
59
+ type: mediaType.optional(),
60
+ poster: z.string().optional(),
61
+ aspectRatio: aspectRatio.optional(),
62
+ alt: z.string().optional(),
63
+ title: z.string().optional(),
64
+ autoplay: z.boolean().optional(),
65
+ loop: z.boolean().optional(),
66
+ muted: z.boolean().optional()
67
+ });
68
+
69
+ // ../recipe/src/announcement.ts
70
+ var announcementAction = z2.object({
71
+ label: z2.string(),
72
+ href: z2.string().optional(),
73
+ variant: z2.enum(["primary", "secondary", "ghost", "link"]).optional(),
74
+ dismissOnClick: z2.boolean().optional()
75
+ });
76
+ var announcementMedia = media.omit({ title: true });
77
+ var spotlightOptions = z2.object({
78
+ targetSelector: z2.string(),
79
+ placement: z2.enum(["top", "right", "bottom", "left"]).optional(),
80
+ offset: z2.number().optional(),
81
+ showOverlay: z2.boolean().optional(),
82
+ overlayOpacity: z2.number().optional(),
83
+ closeOnOverlayClick: z2.boolean().optional()
84
+ });
85
+ var announcementComponent = z2.object({
86
+ kind: z2.literal("announcement"),
87
+ id: z2.string(),
88
+ variant: z2.enum(["modal", "slideout", "banner", "toast", "spotlight"]),
89
+ priority: z2.enum(["low", "normal", "high", "critical"]).optional(),
90
+ title: z2.string().optional(),
91
+ description: z2.string().optional(),
92
+ media: announcementMedia.optional(),
93
+ primaryAction: announcementAction.optional(),
94
+ secondaryAction: announcementAction.optional(),
95
+ schedule: z2.string().optional(),
96
+ autoShow: z2.boolean().optional(),
97
+ spotlightOptions: spotlightOptions.optional()
98
+ });
99
+
100
+ // ../recipe/src/branch.ts
101
+ import { z as z3 } from "zod";
102
+ var branchTarget = z3.lazy(
103
+ () => z3.union([
104
+ z3.string(),
105
+ z3.number().int(),
106
+ z3.enum(["next", "prev", "complete", "skip", "restart"]),
107
+ z3.object({
108
+ tour: z3.string(),
109
+ step: z3.union([z3.string(), z3.number().int()]).optional()
110
+ }),
111
+ z3.object({ skip: z3.number().int() }),
112
+ // biome-ignore lint/suspicious/noThenProperty: `then` mirrors @tour-kit/core's BranchWait wire-format field; renaming would break the projection
113
+ z3.object({ wait: z3.number().int(), then: branchTarget.optional() }),
114
+ z3.null()
115
+ ])
116
+ );
117
+
118
+ // ../recipe/src/checklist.ts
119
+ import { z as z4 } from "zod";
120
+ var checklistTask = z4.object({
121
+ id: z4.string(),
122
+ title: z4.string(),
123
+ description: z4.string().optional(),
124
+ media: media.optional(),
125
+ icon: z4.string().optional(),
126
+ action: z4.discriminatedUnion("type", [
127
+ z4.object({
128
+ type: z4.literal("navigate"),
129
+ url: z4.string(),
130
+ external: z4.boolean().optional()
131
+ }),
132
+ z4.object({ type: z4.literal("tour"), tourId: z4.string() }),
133
+ z4.object({ type: z4.literal("modal"), modalId: z4.string() })
134
+ ]).optional(),
135
+ dependsOn: z4.array(z4.string()).optional(),
136
+ completedWhen: z4.union([
137
+ z4.object({ tourCompleted: z4.string() }),
138
+ z4.object({ tourStarted: z4.string() }),
139
+ z4.object({ type: z4.literal("urlVisit"), urlPattern: z4.string() })
140
+ ]).optional(),
141
+ manualComplete: z4.boolean().optional()
142
+ });
143
+ var checklistComponent = z4.object({
144
+ kind: z4.literal("checklist"),
145
+ id: z4.string(),
146
+ title: z4.string(),
147
+ description: z4.string().optional(),
148
+ icon: z4.string().optional(),
149
+ tasks: z4.array(checklistTask).min(1),
150
+ dismissible: z4.boolean().optional(),
151
+ hideOnComplete: z4.boolean().optional()
152
+ });
153
+
154
+ // ../recipe/src/components.ts
155
+ import { z as z6 } from "zod";
156
+
157
+ // ../recipe/src/selector.ts
158
+ import { z as z5 } from "zod";
159
+ var selectorStrategy = z5.enum([
160
+ "data-tour-id",
161
+ "data-testid",
162
+ "role-name",
163
+ "css-path"
164
+ ]);
165
+ var selectorMeta = z5.object({
166
+ strategy: selectorStrategy,
167
+ grade: z5.enum(["stable", "fragile"]),
168
+ brittleness: z5.number().int().min(0).max(100).optional()
169
+ });
170
+ var elementDescriptorSchema = z5.object({
171
+ tag: z5.string(),
172
+ attributes: z5.record(z5.string(), z5.string()),
173
+ role: z5.string().optional(),
174
+ accessibleName: z5.string().optional(),
175
+ cssPath: z5.string(),
176
+ cssPathDepth: z5.number().int().min(0),
177
+ usesNthChild: z5.boolean()
178
+ });
179
+
180
+ // ../recipe/src/components.ts
181
+ var tourStep = z6.discriminatedUnion("kind", [
182
+ z6.object({
183
+ kind: z6.literal("visible"),
184
+ id: z6.string(),
185
+ target: z6.string(),
186
+ selectorMeta: selectorMeta.optional(),
187
+ title: z6.string().optional(),
188
+ content: z6.string(),
189
+ media: media.optional(),
190
+ placement: placement.optional(),
191
+ offset: z6.tuple([z6.number(), z6.number()]).optional(),
192
+ showNavigation: z6.boolean().optional(),
193
+ showClose: z6.boolean().optional(),
194
+ showProgress: z6.boolean().optional(),
195
+ advanceOn: z6.object({
196
+ event: z6.enum(["click", "input", "custom"]),
197
+ selector: z6.string().optional()
198
+ }).optional(),
199
+ onNext: branchTarget.optional(),
200
+ onPrev: branchTarget.optional(),
201
+ onAction: z6.record(z6.string(), branchTarget).optional()
202
+ }),
203
+ z6.object({ kind: z6.literal("hidden"), id: z6.string() })
204
+ ]);
205
+ var tourComponent = z6.object({
206
+ kind: z6.literal("tour"),
207
+ id: z6.string(),
208
+ name: z6.string().optional(),
209
+ steps: z6.array(tourStep).min(1),
210
+ autoStart: z6.boolean().optional(),
211
+ startAt: z6.number().int().optional(),
212
+ keyboard: z6.boolean().optional(),
213
+ spotlight: z6.boolean().optional()
214
+ });
215
+ var hintComponent = z6.object({
216
+ kind: z6.literal("hint"),
217
+ id: z6.string(),
218
+ target: z6.string(),
219
+ selectorMeta: selectorMeta.optional(),
220
+ title: z6.string().optional(),
221
+ content: z6.string(),
222
+ position: z6.enum(["top-left", "top-right", "bottom-left", "bottom-right", "center"]).optional(),
223
+ tooltipPlacement: placement.optional(),
224
+ pulse: z6.boolean().optional(),
225
+ autoShow: z6.boolean().optional(),
226
+ persist: z6.boolean().optional(),
227
+ media: media.optional()
228
+ });
229
+
230
+ // ../recipe/src/recipe.ts
231
+ import { z as z11 } from "zod";
232
+
233
+ // ../recipe/src/scaffold.ts
234
+ import { z as z7 } from "zod";
235
+ var providerName = z7.enum(["LicenseProvider", "TourKitProvider"]);
236
+ var scaffold = z7.object({
237
+ files: z7.array(z7.object({ path: z7.string(), purpose: z7.string().optional() })).default([]),
238
+ providerWrap: z7.object({
239
+ target: z7.literal("rootLayout"),
240
+ providers: z7.array(providerName).min(1)
241
+ }),
242
+ envVars: z7.array(z7.literal("TOUR_KIT_LICENSE_KEY")).default([]),
243
+ hooks: z7.array(z7.object({ ref: z7.string(), signature: z7.string() })).default([])
244
+ });
245
+
246
+ // ../recipe/src/schedule.ts
247
+ import { z as z8 } from "zod";
248
+ var dayOfWeek = z8.number().int().min(0).max(6);
249
+ var scheduleDef = z8.object({
250
+ enabled: z8.boolean().optional(),
251
+ startAt: dateString.optional(),
252
+ endAt: dateString.optional(),
253
+ daysOfWeek: z8.array(dayOfWeek).optional(),
254
+ timeOfDay: z8.object({ start: timeString, end: timeString }).optional(),
255
+ useUserTimezone: z8.boolean().optional(),
256
+ timezone: z8.string().optional(),
257
+ recurring: z8.object({
258
+ type: z8.enum(["daily", "weekly", "monthly", "yearly"]),
259
+ interval: z8.number().int().optional(),
260
+ daysOfWeek: z8.array(dayOfWeek).optional(),
261
+ dayOfMonth: z8.number().int().min(1).max(31).optional(),
262
+ month: z8.number().int().min(1).max(12).optional(),
263
+ maxOccurrences: z8.number().int().optional(),
264
+ endDate: dateString.optional()
265
+ }).optional(),
266
+ blackouts: z8.array(
267
+ z8.object({
268
+ id: z8.string(),
269
+ start: dateString,
270
+ end: dateString,
271
+ reason: z8.string().optional()
272
+ })
273
+ ).optional()
274
+ });
275
+
276
+ // ../recipe/src/survey.ts
277
+ import { z as z9 } from "zod";
278
+ var ratingScale = z9.object({
279
+ min: z9.number(),
280
+ max: z9.number(),
281
+ step: z9.number().positive().optional(),
282
+ labels: z9.object({ min: z9.string().optional(), max: z9.string().optional() }).optional(),
283
+ style: z9.enum(["numeric", "stars", "emoji"]).optional()
284
+ });
285
+ var selectOption = z9.object({
286
+ value: z9.string(),
287
+ label: z9.string(),
288
+ disabled: z9.boolean().optional()
289
+ });
290
+ var skipLogicOps = [
291
+ "eq",
292
+ "neq",
293
+ "lt",
294
+ "lte",
295
+ "gt",
296
+ "gte",
297
+ "includes"
298
+ ];
299
+ var skipLogicRule = z9.object({
300
+ questionId: z9.string(),
301
+ op: z9.enum(skipLogicOps),
302
+ value: z9.union([z9.string(), z9.number(), z9.boolean()]),
303
+ skipTo: z9.string()
304
+ });
305
+ var COMPARISON_OPS = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
306
+ var surveyQuestion = z9.object({
307
+ id: z9.string().min(1),
308
+ type: z9.enum([
309
+ "rating",
310
+ "text",
311
+ "textarea",
312
+ "single-select",
313
+ "multi-select",
314
+ "boolean"
315
+ ]),
316
+ text: z9.string().min(1),
317
+ description: z9.string().optional(),
318
+ media: media.optional(),
319
+ required: z9.boolean().optional(),
320
+ placeholder: z9.string().optional(),
321
+ ratingScale: ratingScale.optional(),
322
+ preset: z9.enum(["thumbs", "stars"]).optional(),
323
+ options: z9.array(selectOption).min(1).optional(),
324
+ maxLength: z9.number().int().positive().optional(),
325
+ skipLogic: z9.array(skipLogicRule).optional()
326
+ }).superRefine((q, ctx) => {
327
+ const isSelect = q.type === "single-select" || q.type === "multi-select";
328
+ const isText = q.type === "text" || q.type === "textarea";
329
+ if (q.options && !isSelect)
330
+ ctx.addIssue({
331
+ code: "custom",
332
+ path: ["options"],
333
+ message: "options are only valid on select questions"
334
+ });
335
+ if (isSelect && !q.options)
336
+ ctx.addIssue({
337
+ code: "custom",
338
+ path: ["options"],
339
+ message: "select questions require options"
340
+ });
341
+ if (q.ratingScale && q.type !== "rating")
342
+ ctx.addIssue({
343
+ code: "custom",
344
+ path: ["ratingScale"],
345
+ message: "ratingScale is only valid on rating questions"
346
+ });
347
+ if (q.preset && q.type !== "rating")
348
+ ctx.addIssue({
349
+ code: "custom",
350
+ path: ["preset"],
351
+ message: "preset is only valid on rating questions"
352
+ });
353
+ if (q.maxLength !== void 0 && !isText)
354
+ ctx.addIssue({
355
+ code: "custom",
356
+ path: ["maxLength"],
357
+ message: "maxLength is only valid on text/textarea questions"
358
+ });
359
+ if (q.placeholder !== void 0 && !isText)
360
+ ctx.addIssue({
361
+ code: "custom",
362
+ path: ["placeholder"],
363
+ message: "placeholder is only valid on text/textarea questions"
364
+ });
365
+ if (q.ratingScale && q.ratingScale.min >= q.ratingScale.max)
366
+ ctx.addIssue({
367
+ code: "custom",
368
+ path: ["ratingScale", "min"],
369
+ message: "ratingScale.min must be less than max"
370
+ });
371
+ q.skipLogic?.forEach((rule, i) => {
372
+ if (COMPARISON_OPS.has(rule.op) && typeof rule.value !== "number")
373
+ ctx.addIssue({
374
+ code: "custom",
375
+ path: ["skipLogic", i, "value"],
376
+ message: `op "${rule.op}" requires a numeric value`
377
+ });
378
+ if (rule.op === "includes" && typeof rule.value !== "string")
379
+ ctx.addIssue({
380
+ code: "custom",
381
+ path: ["skipLogic", i, "value"],
382
+ message: 'op "includes" requires a string value'
383
+ });
384
+ });
385
+ });
386
+ var surveyQuestions = z9.array(surveyQuestion).min(1).superRefine((qs, ctx) => {
387
+ const ids = qs.map((q) => q.id);
388
+ const seen = /* @__PURE__ */ new Set();
389
+ ids.forEach((id, i) => {
390
+ if (seen.has(id))
391
+ ctx.addIssue({
392
+ code: "custom",
393
+ path: [i, "id"],
394
+ message: `duplicate question id "${id}"`
395
+ });
396
+ seen.add(id);
397
+ });
398
+ qs.forEach((q, qi) => {
399
+ q.skipLogic?.forEach((rule, ri) => {
400
+ if (!seen.has(rule.questionId))
401
+ ctx.addIssue({
402
+ code: "custom",
403
+ path: [qi, "skipLogic", ri, "questionId"],
404
+ message: `unknown question "${rule.questionId}"`
405
+ });
406
+ if (!seen.has(rule.skipTo))
407
+ ctx.addIssue({
408
+ code: "custom",
409
+ path: [qi, "skipLogic", ri, "skipTo"],
410
+ message: `unknown question "${rule.skipTo}"`
411
+ });
412
+ else if (ids.indexOf(rule.skipTo) <= qi)
413
+ ctx.addIssue({
414
+ code: "custom",
415
+ path: [qi, "skipLogic", ri, "skipTo"],
416
+ message: "skipTo must reference a LATER question (loop guard)"
417
+ });
418
+ });
419
+ });
420
+ });
421
+ var frequencyRule = z9.union([
422
+ z9.enum(["once", "session", "always"]),
423
+ z9.object({ type: z9.literal("times"), count: z9.number().int().positive() }),
424
+ z9.object({ type: z9.literal("interval"), days: z9.number().positive() })
425
+ ]);
426
+ var modalOptions = z9.object({
427
+ size: z9.enum(["sm", "md", "lg"]).optional(),
428
+ closeOnOverlayClick: z9.boolean().optional(),
429
+ closeOnEscape: z9.boolean().optional(),
430
+ showCloseButton: z9.boolean().optional()
431
+ });
432
+ var slideoutOptions = z9.object({
433
+ position: z9.enum(["left", "right"]).optional(),
434
+ size: z9.enum(["sm", "md", "lg"]).optional(),
435
+ closeOnOverlayClick: z9.boolean().optional(),
436
+ closeOnEscape: z9.boolean().optional(),
437
+ showCloseButton: z9.boolean().optional()
438
+ });
439
+ var bannerOptions = z9.object({
440
+ position: z9.enum(["top", "bottom"]).optional(),
441
+ sticky: z9.boolean().optional(),
442
+ dismissable: z9.boolean().optional(),
443
+ intent: z9.enum(["info", "feedback"]).optional()
444
+ });
445
+ var popoverOptions = z9.object({
446
+ position: z9.enum(["bottom-right", "bottom-left", "top-right", "top-left"]).optional(),
447
+ offset: z9.number().optional(),
448
+ showCloseButton: z9.boolean().optional()
449
+ });
450
+ var surveyComponent = z9.object({
451
+ kind: z9.literal("survey"),
452
+ id: z9.string(),
453
+ type: z9.enum(["nps", "csat", "ces", "custom"]),
454
+ displayMode: z9.enum(["popover", "modal", "slideout", "banner", "inline"]),
455
+ priority: z9.enum(["critical", "high", "normal", "low"]).optional(),
456
+ title: z9.string().optional(),
457
+ description: z9.string().optional(),
458
+ questions: surveyQuestions,
459
+ frequency: frequencyRule.optional(),
460
+ schedule: z9.string().optional(),
461
+ globalCooldownDays: z9.number().nonnegative().optional(),
462
+ samplingRate: z9.number().min(0).max(1).optional(),
463
+ maxSnoozeCount: z9.number().int().nonnegative().optional(),
464
+ snoozeDelayDays: z9.number().nonnegative().optional(),
465
+ maxPerSession: z9.number().int().positive().optional(),
466
+ modalOptions: modalOptions.optional(),
467
+ slideoutOptions: slideoutOptions.optional(),
468
+ bannerOptions: bannerOptions.optional(),
469
+ popoverOptions: popoverOptions.optional()
470
+ });
471
+
472
+ // ../recipe/src/theme.ts
473
+ import { z as z10 } from "zod";
474
+ var themeTokenKey = z10.string().regex(
475
+ /^--[a-z][a-z0-9-]{0,30}$/,
476
+ "theme token keys must be --kebab-case CSS custom properties"
477
+ );
478
+ var themeTokenValue = z10.string().regex(
479
+ /^[a-zA-Z0-9#.,()%\s/-]{1,64}$/,
480
+ "theme token values may only contain color/length characters (1\u201364 chars)"
481
+ );
482
+ var themeTokens = z10.record(themeTokenKey, themeTokenValue);
483
+ var themeMatcher = z10.object({ kind: z10.literal("system") });
484
+ var themeVariation = z10.object({
485
+ id: z10.string().min(1),
486
+ when: themeMatcher,
487
+ theme: themeTokens
488
+ });
489
+ var recipeTheme = z10.object({
490
+ variations: z10.array(themeVariation).length(1)
491
+ });
492
+ var TOKEN_BRIDGE = {
493
+ "--primary": "--tour-primary",
494
+ "--primary-foreground": "--tour-primary-fg",
495
+ "--popover": "--tour-card-bg",
496
+ "--popover-foreground": "--tour-card-fg",
497
+ "--muted-foreground": "--tour-muted-fg",
498
+ "--border": "--tour-card-border",
499
+ "--ring": "--tour-ring",
500
+ "--radius": "--tour-card-radius"
501
+ };
502
+ var SCOPED_SELECTORS = ["[data-tour-step]"];
503
+
504
+ // ../recipe/src/recipe.ts
505
+ var SUPPORTED_DEPS = {
506
+ "@tour-kit/core": "1.0.6",
507
+ "@tour-kit/react": "1.0.6",
508
+ "@tour-kit/hints": "1.0.6",
509
+ "@tour-kit/media": "0.13.2",
510
+ "@tour-kit/checklists": "0.13.9",
511
+ "@tour-kit/announcements": "4.1.6",
512
+ "@tour-kit/scheduling": "0.11.9",
513
+ "@tour-kit/surveys": "3.0.9",
514
+ "@tour-kit/license": "1.3.5"
515
+ };
516
+ var RUNTIME_PEERS = {
517
+ "@tour-kit/react": {
518
+ // 0.11.9 = the exact analytics peer of react/hints/checklists/
519
+ // announcements/scheduling at the pinned versions (the 0.11.9 train —
520
+ // see the SUPPORTED_DEPS coherence note). surveys@3.0.9 has no
521
+ // analytics peer of its own.
522
+ "@tour-kit/analytics": "0.11.9",
523
+ // media@0.13.x still ranges `^3.5.0` — the Phase 25 QA-proven pin stands.
524
+ "@lottiefiles/react-lottie-player": "3.5.4"
525
+ }
526
+ };
527
+ function runtimePeerSpecs(packages) {
528
+ const specs = /* @__PURE__ */ new Set();
529
+ for (const pkg of packages) {
530
+ const peers = RUNTIME_PEERS[pkg];
531
+ if (!peers) continue;
532
+ for (const [name, version] of Object.entries(peers))
533
+ specs.add(`${name}@${version}`);
534
+ }
535
+ return [...specs].sort();
536
+ }
537
+ var PRO_PACKAGES = [
538
+ "@tour-kit/media",
539
+ "@tour-kit/announcements",
540
+ "@tour-kit/checklists",
541
+ "@tour-kit/license",
542
+ "@tour-kit/scheduling",
543
+ "@tour-kit/surveys"
544
+ ];
545
+ var tkPackage = z11.enum(
546
+ Object.keys(SUPPORTED_DEPS)
547
+ );
548
+ var recipeComponent = z11.discriminatedUnion("kind", [
549
+ tourComponent,
550
+ hintComponent,
551
+ checklistComponent,
552
+ announcementComponent,
553
+ surveyComponent
554
+ ]);
555
+ var isSupportedPackage = (p) => p in SUPPORTED_DEPS;
556
+ var usesMedia = (c) => {
557
+ switch (c.kind) {
558
+ case "tour":
559
+ return c.steps.some((s) => s.kind === "visible" && s.media !== void 0);
560
+ case "checklist":
561
+ return c.tasks.some((t) => t.media !== void 0);
562
+ case "survey":
563
+ return c.questions.some((q) => q.media !== void 0);
564
+ case "hint":
565
+ case "announcement":
566
+ return c.media !== void 0;
567
+ }
568
+ };
569
+ var requiredPackages = (c) => {
570
+ const req = /* @__PURE__ */ new Set(["@tour-kit/react"]);
571
+ if (c.kind === "tour") req.add("@tour-kit/core");
572
+ else if (c.kind === "hint") req.add("@tour-kit/hints");
573
+ else if (c.kind === "checklist") req.add("@tour-kit/checklists");
574
+ else if (c.kind === "announcement") req.add("@tour-kit/announcements");
575
+ else if (c.kind === "survey") req.add("@tour-kit/surveys");
576
+ if (usesMedia(c)) req.add("@tour-kit/media");
577
+ if ((c.kind === "announcement" || c.kind === "survey") && c.schedule)
578
+ req.add("@tour-kit/scheduling");
579
+ return [...req];
580
+ };
581
+ var recipeSchemaV1 = z11.object({
582
+ $schema: z11.string().optional(),
583
+ schemaVersion: z11.literal(1),
584
+ id: recipeId,
585
+ version: semver,
586
+ name: z11.string().min(1),
587
+ tier: z11.enum(["free", "pro"]),
588
+ packages: z11.array(tkPackage).min(1),
589
+ components: z11.array(recipeComponent).min(1),
590
+ schedules: z11.record(z11.string(), scheduleDef).optional(),
591
+ // Recipe-global theme (Phase 32): one base ThemeVariation projected from
592
+ // @tour-kit/react — see ./theme for the executable-subset rules.
593
+ theme: recipeTheme.optional(),
594
+ scaffold,
595
+ deps: z11.record(z11.string(), z11.string()),
596
+ meta: z11.object({ createdAt: z11.iso.datetime(), studioVersion: z11.string() })
597
+ }).superRefine((r, ctx) => {
598
+ const derived = r.packages.some(
599
+ (p) => PRO_PACKAGES.includes(p)
600
+ ) ? "pro" : "free";
601
+ if (r.tier !== derived)
602
+ ctx.addIssue({
603
+ code: "custom",
604
+ path: ["tier"],
605
+ message: `tier must be "${derived}"`
606
+ });
607
+ for (const p of r.packages) {
608
+ const pinned = r.deps[p];
609
+ if (pinned !== SUPPORTED_DEPS[p])
610
+ ctx.addIssue({
611
+ code: "custom",
612
+ path: ["deps", p],
613
+ message: `package dep must be pinned to ${SUPPORTED_DEPS[p]}`
614
+ });
615
+ }
616
+ for (const p of Object.keys(r.deps)) {
617
+ if (!isSupportedPackage(p) || !r.packages.includes(p))
618
+ ctx.addIssue({
619
+ code: "custom",
620
+ path: ["deps", p],
621
+ message: "deps may only contain selected, supported @tour-kit packages"
622
+ });
623
+ }
624
+ const providers = r.scaffold.providerWrap.providers;
625
+ if (derived === "pro" && (!r.packages.includes("@tour-kit/license") || providers[0] !== "LicenseProvider" || providers[1] !== "TourKitProvider" || !r.scaffold.envVars.includes("TOUR_KIT_LICENSE_KEY")))
626
+ ctx.addIssue({
627
+ code: "custom",
628
+ path: ["scaffold"],
629
+ message: "pro recipe must install @tour-kit/license, wrap both providers, and expose TOUR_KIT_LICENSE_KEY"
630
+ });
631
+ if (derived === "free" && !providers.includes("TourKitProvider"))
632
+ ctx.addIssue({
633
+ code: "custom",
634
+ path: ["scaffold", "providerWrap"],
635
+ message: "free recipe must inject TourKitProvider"
636
+ });
637
+ for (const c of r.components)
638
+ if ((c.kind === "announcement" || c.kind === "survey") && c.schedule && !r.schedules?.[c.schedule])
639
+ ctx.addIssue({
640
+ code: "custom",
641
+ path: ["components"],
642
+ message: `unknown schedule "${c.schedule}"`
643
+ });
644
+ const selected = new Set(r.packages);
645
+ r.components.forEach((c, i) => {
646
+ const missing = requiredPackages(c).filter((p) => !selected.has(p));
647
+ if (missing.length > 0)
648
+ ctx.addIssue({
649
+ code: "custom",
650
+ path: ["components", i],
651
+ message: `"${c.kind}" component requires package(s) not in the recipe: ${missing.join(", ")}`
652
+ });
653
+ });
654
+ if (r.schedules && Object.keys(r.schedules).length > 0 && !selected.has("@tour-kit/scheduling"))
655
+ ctx.addIssue({
656
+ code: "custom",
657
+ path: ["schedules"],
658
+ message: "schedules require @tour-kit/scheduling"
659
+ });
660
+ });
661
+
662
+ // src/commands/add.ts
663
+ import { defineCommand } from "citty";
664
+ import { consola as consola3 } from "consola";
665
+ import { execa } from "execa";
666
+
667
+ // src/lib/detect.ts
668
+ import { existsSync } from "fs";
669
+ import { join } from "path";
670
+ import { readPackageJSON } from "pkg-types";
671
+
672
+ // src/lib/result.ts
673
+ import { consola } from "consola";
674
+ import { ZodError } from "zod";
675
+ var CliError = class extends Error {
676
+ constructor(message, exitCode) {
677
+ super(message);
678
+ this.exitCode = exitCode;
679
+ this.name = "CliError";
680
+ }
681
+ exitCode;
682
+ };
683
+ function emitResult(result, json) {
684
+ if (json) {
685
+ process.stdout.write(JSON.stringify(result));
686
+ return;
687
+ }
688
+ consola.box(
689
+ [
690
+ result.ok ? "Plan ready" : "Plan failed",
691
+ `framework: ${result.framework ?? "unknown (detection lands in a later phase)"}`,
692
+ `packages: ${result.packages.join(", ") || "(none)"}`,
693
+ `runtime peers: ${result.runtimePeers.join(", ") || "(none)"}`,
694
+ `would write: ${result.filesWritten.join(", ") || "(none)"}`,
695
+ `provider patched: ${result.providerPatched}`
696
+ ].join("\n")
697
+ );
698
+ }
699
+ function toExitCode(err) {
700
+ if (err instanceof CliError)
701
+ return { code: err.exitCode, message: err.message };
702
+ if (err instanceof ZodError) return { code: 2, message: "invalid recipe" };
703
+ return { code: 1, message: err instanceof Error ? err.message : String(err) };
704
+ }
705
+ async function runOrExit(fn) {
706
+ try {
707
+ await fn();
708
+ } catch (err) {
709
+ const { code, message } = toExitCode(err);
710
+ consola.error(message);
711
+ process.exit(code);
712
+ }
713
+ }
714
+
715
+ // src/lib/detect.ts
716
+ var LICENSE_ENV = {
717
+ "next-app": {
718
+ name: "NEXT_PUBLIC_TOUR_KIT_LICENSE_KEY",
719
+ expression: "process.env.NEXT_PUBLIC_TOUR_KIT_LICENSE_KEY"
720
+ },
721
+ "next-pages": {
722
+ name: "NEXT_PUBLIC_TOUR_KIT_LICENSE_KEY",
723
+ expression: "process.env.NEXT_PUBLIC_TOUR_KIT_LICENSE_KEY"
724
+ },
725
+ "vite-react": {
726
+ name: "VITE_TOUR_KIT_LICENSE_KEY",
727
+ expression: "import.meta.env.VITE_TOUR_KIT_LICENSE_KEY"
728
+ },
729
+ remix: {
730
+ name: "PUBLIC_TOUR_KIT_LICENSE_KEY",
731
+ expression: "window.ENV?.PUBLIC_TOUR_KIT_LICENSE_KEY"
732
+ },
733
+ astro: {
734
+ name: "PUBLIC_TOUR_KIT_LICENSE_KEY",
735
+ expression: "import.meta.env.PUBLIC_TOUR_KIT_LICENSE_KEY"
736
+ },
737
+ cra: {
738
+ name: "REACT_APP_TOUR_KIT_LICENSE_KEY",
739
+ expression: "process.env.REACT_APP_TOUR_KIT_LICENSE_KEY"
740
+ }
741
+ };
742
+ var ENTRY = {
743
+ "next-app": "app/layout.tsx",
744
+ "next-pages": "pages/_app.tsx",
745
+ "vite-react": "src/main.tsx",
746
+ remix: "app/root.tsx",
747
+ astro: "src/layouts/Layout.astro",
748
+ cra: "src/index.tsx"
749
+ };
750
+ async function detectProject(cwd) {
751
+ const pkg = await readPackageJSON(cwd);
752
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
753
+ const packageManager = existsSync(
754
+ join(cwd, "pnpm-lock.yaml")
755
+ ) ? "pnpm" : existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")) ? "bun" : existsSync(join(cwd, "yarn.lock")) ? "yarn" : "npm";
756
+ const framework = deps.next && existsSync(join(cwd, "app")) ? "next-app" : deps.next ? "next-pages" : deps["@remix-run/react"] ? "remix" : deps.astro ? "astro" : deps.vite ? "vite-react" : deps["react-scripts"] ? "cra" : null;
757
+ if (!framework) throw new CliError("unsupported framework", 2);
758
+ return {
759
+ framework,
760
+ packageManager,
761
+ entryFile: join(cwd, ENTRY[framework]),
762
+ formatter: existsSync(join(cwd, "biome.json")) ? "biome" : existsSync(join(cwd, ".prettierrc")) ? "prettier" : null,
763
+ typescript: existsSync(join(cwd, "tsconfig.json")),
764
+ licenseEnv: LICENSE_ENV[framework]
765
+ };
766
+ }
767
+
768
+ // src/lib/header.ts
769
+ function header(id, version) {
770
+ return `// Generated by User Tour Kit Studio \xB7 recipe: ${id}@${version}
771
+ // Edit freely. Sync with: npx @tour-kit/studio pull ${id}`;
772
+ }
773
+
774
+ // src/lib/expand.ts
775
+ var pad = (level) => " ".repeat(level);
776
+ var isIdentifier = (k) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k);
777
+ var key = (k) => isIdentifier(k) ? k : str(k);
778
+ function str(s) {
779
+ if (s.includes('"') && !s.includes("'"))
780
+ return `'${s.replace(/\\/g, "\\\\").replace(/\n/g, "\\n")}'`;
781
+ return JSON.stringify(s);
782
+ }
783
+ var isPrimitive = (v) => v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
784
+ var RawCode = class {
785
+ constructor(code) {
786
+ this.code = code;
787
+ }
788
+ code;
789
+ };
790
+ function serialize(value, level) {
791
+ if (value === null) return "null";
792
+ if (value instanceof RawCode) return value.code;
793
+ if (typeof value === "string") return str(value);
794
+ if (typeof value === "number" || typeof value === "boolean")
795
+ return String(value);
796
+ if (Array.isArray(value)) {
797
+ if (value.length === 0) return "[]";
798
+ if (value.every(isPrimitive))
799
+ return `[${value.map((v) => serialize(v, level)).join(", ")}]`;
800
+ const items = value.map((v) => `${pad(level + 1)}${serialize(v, level + 1)}`).join(",\n");
801
+ return `[
802
+ ${items}
803
+ ${pad(level)}]`;
804
+ }
805
+ if (typeof value === "object") {
806
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
807
+ if (entries.length === 0) return "{}";
808
+ const body = entries.map(([k, v]) => `${pad(level + 1)}${key(k)}: ${serialize(v, level + 1)}`).join(",\n");
809
+ return `{
810
+ ${body}
811
+ ${pad(level)}}`;
812
+ }
813
+ throw new CliError(
814
+ `expand: cannot serialize value of type ${typeof value}`,
815
+ 2
816
+ );
817
+ }
818
+ var sanitize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
819
+ var hookName = (ref) => `hook_${sanitize(ref)}`;
820
+ function wrap(kind, id, cfg, note = "", type) {
821
+ const decl = `${kind}_${sanitize(id)}${type ? `: ${type}` : ""}`;
822
+ return `// ${kind}: ${id}${note}
823
+ export const ${decl} = ${serialize(cfg, 0)};`;
824
+ }
825
+ var ANNOUNCEMENT_COMPONENT = {
826
+ modal: "AnnouncementModal",
827
+ slideout: "AnnouncementSlideout",
828
+ banner: "AnnouncementBanner",
829
+ toast: "AnnouncementToast",
830
+ spotlight: "AnnouncementSpotlight"
831
+ };
832
+ var SURVEY_COMPONENT = {
833
+ popover: "SurveyPopover",
834
+ modal: "SurveyModal",
835
+ slideout: "SurveySlideout",
836
+ banner: "SurveyBanner",
837
+ inline: "SurveyInline"
838
+ };
839
+ function conditionSource(rule) {
840
+ const { op, value } = rule;
841
+ switch (op) {
842
+ case "eq":
843
+ case "neq": {
844
+ const cmp2 = typeof value === "number" ? `Number(answer) === ${value}` : typeof value === "string" ? `String(answer) === ${str(value)}` : `answer === ${value}`;
845
+ return op === "neq" ? `(answer) => !(${cmp2})` : `(answer) => ${cmp2}`;
846
+ }
847
+ case "lt":
848
+ return `(answer) => Number(answer) < ${Number(value)}`;
849
+ case "lte":
850
+ return `(answer) => Number(answer) <= ${Number(value)}`;
851
+ case "gt":
852
+ return `(answer) => Number(answer) > ${Number(value)}`;
853
+ case "gte":
854
+ return `(answer) => Number(answer) >= ${Number(value)}`;
855
+ case "includes":
856
+ return `(answer) => Array.isArray(answer) && answer.includes(${str(String(value))})`;
857
+ }
858
+ }
859
+ function imports(recipe) {
860
+ const kinds = new Set(recipe.components.map((c) => c.kind));
861
+ const value = [];
862
+ const types = [];
863
+ if (kinds.has("survey"))
864
+ value.push(`import { useEffect, useRef } from "react";`);
865
+ const reactNames = [];
866
+ if (recipe.theme) reactNames.push("ThemeProvider");
867
+ if (kinds.has("tour")) reactNames.push("Tour", "TourStep");
868
+ if (reactNames.length > 0)
869
+ value.push(
870
+ `import { ${reactNames.sort().join(", ")} } from "@tour-kit/react";`
871
+ );
872
+ if (kinds.has("hint"))
873
+ value.push(`import { Hint, HintsProvider } from "@tour-kit/hints";`);
874
+ if (kinds.has("checklist")) {
875
+ value.push(
876
+ `import { Checklist, ChecklistProvider } from "@tour-kit/checklists";`
877
+ );
878
+ types.push(`import type { ChecklistConfig } from "@tour-kit/checklists";`);
879
+ }
880
+ if (kinds.has("announcement")) {
881
+ const variants = [
882
+ ...new Set(
883
+ recipe.components.filter((c) => c.kind === "announcement").map((c) => ANNOUNCEMENT_COMPONENT[c.variant])
884
+ )
885
+ ];
886
+ const named = ["AnnouncementsProvider", ...variants].sort();
887
+ value.push(
888
+ `import { ${named.join(", ")} } from "@tour-kit/announcements";`
889
+ );
890
+ types.push(
891
+ `import type { AnnouncementConfig } from "@tour-kit/announcements";`
892
+ );
893
+ }
894
+ if (kinds.has("survey")) {
895
+ const displays = [
896
+ ...new Set(
897
+ recipe.components.filter((c) => c.kind === "survey").map((c) => SURVEY_COMPONENT[c.displayMode])
898
+ )
899
+ ];
900
+ const named = [
901
+ "QuestionBoolean",
902
+ "QuestionMedia",
903
+ "QuestionRating",
904
+ "QuestionSelect",
905
+ "QuestionText",
906
+ "SurveyProgress",
907
+ "SurveysProvider",
908
+ "useSurvey",
909
+ ...displays
910
+ ].sort();
911
+ value.push(`import { ${named.join(", ")} } from "@tour-kit/surveys";`);
912
+ types.push(
913
+ `import type { AnswerValue, SurveyConfig } from "@tour-kit/surveys";`
914
+ );
915
+ }
916
+ if (recipe.theme)
917
+ types.push(`import type { ThemeVariation } from "@tour-kit/react";`);
918
+ return [...value, ...types].join("\n");
919
+ }
920
+ var cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
921
+ var cssBlock = (sel, decls) => decls.length === 0 ? "" : `${sel} { ${decls.map(([k, v]) => `${k}: ${v};`).join(" ")} }`;
922
+ function themeCssFor(tokens) {
923
+ const entries = Object.entries(tokens);
924
+ const bridged = entries.flatMap(([k, v]) => {
925
+ const target = TOKEN_BRIDGE[k];
926
+ return target === void 0 ? [] : [[target, v]];
927
+ }).sort(([a], [b]) => cmp(a, b));
928
+ const all = [...entries].sort(([a], [b]) => cmp(a, b));
929
+ return [
930
+ cssBlock(":root", bridged),
931
+ ...SCOPED_SELECTORS.map((sel) => cssBlock(sel, all))
932
+ ].filter((b) => b !== "").join("\n");
933
+ }
934
+ function emitTheme(theme) {
935
+ const css = themeCssFor(theme.variations[0]?.theme ?? {});
936
+ return [
937
+ "// theme: recipe-global ThemeProvider variations + the portal-bridging <style> css",
938
+ `export const THEME_VARIATIONS: ThemeVariation[] = ${serialize(theme.variations, 0)};`,
939
+ `export const THEME_CSS = ${str(css)};`
940
+ ].join("\n");
941
+ }
942
+ var jsxAttr = (name, value) => ` ${name}={${serialize(value, 0)}}`;
943
+ var STEP_FIELDS = [
944
+ "target",
945
+ "content",
946
+ "title",
947
+ "placement",
948
+ "showNavigation",
949
+ "showClose",
950
+ "showProgress"
951
+ ];
952
+ function emitTourStep(c, s) {
953
+ const attrs = STEP_FIELDS.filter((k) => s[k] !== void 0).map((k) => jsxAttr(k, s[k])).join("");
954
+ return ` <TourStep key={${serialize(`${c.id}:${s.id}`, 0)}} id={${serialize(s.id, 0)}}${attrs} />`;
955
+ }
956
+ var TOUR_FIELDS = ["autoStart", "startAt"];
957
+ function emitTour(c) {
958
+ const open = TOUR_FIELDS.filter((k) => c[k] !== void 0).map((k) => jsxAttr(k, c[k])).join("");
959
+ const steps = c.steps.filter((s) => s.kind === "visible").map((s) => emitTourStep(c, s)).join("\n");
960
+ return ` <Tour key={${serialize(c.id, 0)}} id={${serialize(c.id, 0)}}${open}>
961
+ ${steps}
962
+ </Tour>`;
963
+ }
964
+ var HINT_FIELDS = [
965
+ "target",
966
+ "content",
967
+ "title",
968
+ "position",
969
+ "tooltipPlacement",
970
+ "pulse",
971
+ "autoShow",
972
+ "persist"
973
+ ];
974
+ function emitHint(c) {
975
+ const attrs = HINT_FIELDS.filter((k) => c[k] !== void 0).map((k) => jsxAttr(k, c[k])).join("");
976
+ return ` <Hint key={${serialize(c.id, 0)}} id={${serialize(c.id, 0)}}${attrs} />`;
977
+ }
978
+ function emitChecklistEl(c) {
979
+ return ` <Checklist key={${serialize(c.id, 0)}} checklistId={${serialize(c.id, 0)}} showProgress />`;
980
+ }
981
+ function emitAnnouncementEl(c) {
982
+ const Comp = ANNOUNCEMENT_COMPONENT[c.variant];
983
+ return ` <${Comp} key={${serialize(c.id, 0)}} id={${serialize(c.id, 0)}} useConfig />`;
984
+ }
985
+ function emitSurveyEl(c) {
986
+ const Comp = SURVEY_COMPONENT[c.displayMode];
987
+ const id = serialize(c.id, 0);
988
+ const lines = [
989
+ ` <SurveyAutoShow key={${serialize(`${c.id}:auto-show`, 0)}} surveyId={${id}} />`
990
+ ];
991
+ if (c.displayMode === "popover") {
992
+ lines.push(
993
+ ` {/* TODO: move the data-survey-popover-anchor attribute onto the element this survey should attach to */}`,
994
+ ` <span data-survey-popover-anchor={${id}}>Feedback</span>`
995
+ );
996
+ }
997
+ if (c.displayMode === "inline")
998
+ lines.push(
999
+ ` {/* SurveyInline renders in-flow exactly where it is mounted \u2014 move it where the survey belongs */}`
1000
+ );
1001
+ lines.push(
1002
+ ` <${Comp} key={${id}} surveyId={${id}}>
1003
+ <SurveyBody surveyId={${id}} />
1004
+ </${Comp}>`
1005
+ );
1006
+ return lines.join("\n");
1007
+ }
1008
+ var emitElement = (c) => {
1009
+ switch (c.kind) {
1010
+ case "tour":
1011
+ return emitTour(c);
1012
+ case "hint":
1013
+ return emitHint(c);
1014
+ case "checklist":
1015
+ return emitChecklistEl(c);
1016
+ case "announcement":
1017
+ return emitAnnouncementEl(c);
1018
+ case "survey":
1019
+ return emitSurveyEl(c);
1020
+ }
1021
+ };
1022
+ var indentBlock = (block, levels) => levels <= 0 ? block : block.split("\n").map((l) => l === "" ? l : `${pad(levels)}${l}`).join("\n");
1023
+ function emitMount(recipe) {
1024
+ const inner = recipe.components.map(emitElement).join("\n");
1025
+ const checklistConsts = recipe.components.filter((c) => c.kind === "checklist").map((c) => `checklist_${sanitize(c.id)}`);
1026
+ const announcementConsts = recipe.components.filter((c) => c.kind === "announcement").map((c) => `announcement_${sanitize(c.id)}`);
1027
+ const surveyConsts = recipe.components.filter((c) => c.kind === "survey").map((c) => `survey_${sanitize(c.id)}`);
1028
+ const providers = [];
1029
+ if (recipe.components.some((c) => c.kind === "hint"))
1030
+ providers.push({ open: "<HintsProvider>", close: "</HintsProvider>" });
1031
+ if (checklistConsts.length > 0)
1032
+ providers.push({
1033
+ open: `<ChecklistProvider checklists={[${checklistConsts.join(", ")}]} persistence={{ enabled: false }}>`,
1034
+ close: "</ChecklistProvider>"
1035
+ });
1036
+ if (announcementConsts.length > 0)
1037
+ providers.push({
1038
+ open: `<AnnouncementsProvider announcements={[${announcementConsts.join(", ")}]}>`,
1039
+ close: "</AnnouncementsProvider>"
1040
+ });
1041
+ if (surveyConsts.length > 0)
1042
+ providers.push({
1043
+ open: `<SurveysProvider surveys={[${surveyConsts.join(", ")}]}>`,
1044
+ close: "</SurveysProvider>"
1045
+ });
1046
+ let body;
1047
+ if (providers.length === 0) {
1048
+ body = ` <>
1049
+ ${inner}
1050
+ </>`;
1051
+ } else {
1052
+ const elements = indentBlock(inner, providers.length - 1);
1053
+ const opens = providers.map((p, i) => `${pad(2 + i)}${p.open}`).join("\n");
1054
+ const closes = providers.map((p, i) => `${pad(2 + i)}${p.close}`).reverse().join("\n");
1055
+ body = `${opens}
1056
+ ${elements}
1057
+ ${closes}`;
1058
+ }
1059
+ if (recipe.theme) {
1060
+ body = `${pad(2)}<ThemeProvider variations={THEME_VARIATIONS}>
1061
+ ${pad(3)}<style>{THEME_CSS}</style>
1062
+ ${indentBlock(body, 1)}
1063
+ ${pad(2)}</ThemeProvider>`;
1064
+ }
1065
+ return `export default function RecipeMount() {
1066
+ return (
1067
+ ${body}
1068
+ );
1069
+ }`;
1070
+ }
1071
+ var STRIP = /* @__PURE__ */ new Set(["selectorMeta"]);
1072
+ function clean(value) {
1073
+ if (value instanceof RawCode) return value;
1074
+ if (Array.isArray(value)) return value.map(clean);
1075
+ if (value !== null && typeof value === "object")
1076
+ return Object.fromEntries(
1077
+ Object.entries(value).filter(([k]) => !STRIP.has(k)).map(([k, v]) => [k, clean(v)])
1078
+ );
1079
+ return value;
1080
+ }
1081
+ function emitComponent(c, r) {
1082
+ switch (c.kind) {
1083
+ case "tour":
1084
+ case "hint": {
1085
+ const { kind, ...rest } = c;
1086
+ return wrap(kind, c.id, clean(rest));
1087
+ }
1088
+ case "checklist": {
1089
+ const { kind, ...rest } = c;
1090
+ return wrap(kind, c.id, clean(rest), "", "ChecklistConfig");
1091
+ }
1092
+ case "announcement": {
1093
+ const { kind, schedule, ...rest } = c;
1094
+ const cfg = schedule ? { ...rest, schedule: r.schedules?.[schedule] } : rest;
1095
+ return wrap(
1096
+ kind,
1097
+ c.id,
1098
+ clean(cfg),
1099
+ schedule ? ` (schedule: ${schedule})` : "",
1100
+ "AnnouncementConfig"
1101
+ );
1102
+ }
1103
+ case "survey": {
1104
+ const { kind, schedule, questions, ...rest } = c;
1105
+ const sdkQuestions = questions.map((q) => {
1106
+ const { skipLogic, ...qRest } = q;
1107
+ return skipLogic ? {
1108
+ ...qRest,
1109
+ skipLogic: skipLogic.map((rule) => ({
1110
+ questionId: rule.questionId,
1111
+ condition: new RawCode(conditionSource(rule)),
1112
+ skipTo: rule.skipTo
1113
+ }))
1114
+ } : qRest;
1115
+ });
1116
+ const cfg = {
1117
+ ...rest,
1118
+ questions: sdkQuestions,
1119
+ ...schedule ? { schedule: r.schedules?.[schedule] } : {}
1120
+ };
1121
+ const hasSkipLogic = questions.some(
1122
+ (q) => (q.skipLogic?.length ?? 0) > 0
1123
+ );
1124
+ const note = `${schedule ? ` (schedule: ${schedule})` : ""}${hasSkipLogic ? "\n// skip-logic is EVALUATED by the generated SurveyBody below (resolveSkipTarget,\n// applied on Next): @tour-kit/surveys 3.0.9 types SkipLogic but does not evaluate it\n// itself \u2014 the config below is the published contract, this file is the executor." : ""}`;
1125
+ return wrap(kind, c.id, clean(cfg), note, "SurveyConfig");
1126
+ }
1127
+ default: {
1128
+ const _never = c;
1129
+ throw new CliError(
1130
+ `expand: unhandled component kind ${JSON.stringify(_never)}`,
1131
+ 2
1132
+ );
1133
+ }
1134
+ }
1135
+ }
1136
+ var SURVEY_HELPERS = `// Where Next lands from question \`current\`: the first matching rule wins (in
1137
+ // authored array order); an unanswered tested question never matches; no match
1138
+ // advances one step. Skip targets are always later than the carrying question
1139
+ // (the Studio validates this), so the seek is forward-only. Back is
1140
+ // deliberately linear \u2014 after a skip it revisits the skipped question.
1141
+ function resolveSkipTarget(
1142
+ questions: SurveyConfig["questions"],
1143
+ current: number,
1144
+ responses: ReadonlyMap<string, AnswerValue>,
1145
+ ): number {
1146
+ const rules = questions[current]?.skipLogic ?? [];
1147
+ const hit = rules.find((rule) => {
1148
+ const answer = responses.get(rule.questionId);
1149
+ return answer !== undefined && rule.condition(answer);
1150
+ });
1151
+ if (!hit) return current + 1;
1152
+ const target = questions.findIndex((q) => q.id === hit.skipTo);
1153
+ return target > current ? target : current + 1;
1154
+ }
1155
+
1156
+ // Renders the active question of a survey: the Survey* display components are
1157
+ // chrome-only wrappers, so the question body must be composed explicitly.
1158
+ function SurveyBody({ surveyId }: { surveyId: string }) {
1159
+ const survey = useSurvey(surveyId);
1160
+ const config = survey.config;
1161
+ const state = survey.state;
1162
+ const question = config?.questions[state?.currentStep ?? 0];
1163
+ if (!config || !state || !question) return null;
1164
+ const label = typeof question.text === "string" ? question.text : question.id;
1165
+ const answer = state.responses.get(question.id);
1166
+ const isLast = state.currentStep >= config.questions.length - 1;
1167
+ // The SDK advances one question per nextQuestion() call (no goTo), so Next
1168
+ // seeks the skip target by stepping forward to it.
1169
+ const advance = () => {
1170
+ const target = resolveSkipTarget(
1171
+ config.questions,
1172
+ state.currentStep,
1173
+ state.responses,
1174
+ );
1175
+ for (let k = state.currentStep; k < target; k++) survey.nextQuestion();
1176
+ };
1177
+ return (
1178
+ <div className="flex flex-col gap-4">
1179
+ <SurveyProgress current={state.currentStep + 1} total={config.questions.length} />
1180
+ <QuestionMedia question={question} />
1181
+ {/* The Question* controls carry the prompt as aria-label only \u2014 render it
1182
+ visibly here (the SDK's turnkey modals do the same). */}
1183
+ <p className="text-sm font-medium">{label}</p>
1184
+ {typeof question.description === "string" && (
1185
+ <p className="text-sm text-muted-foreground">{question.description}</p>
1186
+ )}
1187
+ {question.type === "rating" && (
1188
+ <QuestionRating
1189
+ id={question.id}
1190
+ label={label}
1191
+ // ratingScale wins over preset/min/max/style defaults and carries
1192
+ // step (QuestionRatingProps has no step prop; the 3.0.9 renderer
1193
+ // does not consume step yet \u2014 forward-compat).
1194
+ ratingScale={question.ratingScale}
1195
+ preset={question.preset}
1196
+ value={typeof answer === "number" ? answer : null}
1197
+ onChange={(v) => survey.answer(question.id, v)}
1198
+ isRequired={question.required}
1199
+ />
1200
+ )}
1201
+ {(question.type === "text" || question.type === "textarea") && (
1202
+ <QuestionText
1203
+ id={question.id}
1204
+ mode={question.type}
1205
+ label={label}
1206
+ placeholder={typeof question.placeholder === "string" ? question.placeholder : undefined}
1207
+ maxLength={question.maxLength}
1208
+ value={typeof answer === "string" ? answer : ""}
1209
+ onChange={(v) => survey.answer(question.id, v)}
1210
+ isRequired={question.required}
1211
+ />
1212
+ )}
1213
+ {(question.type === "single-select" || question.type === "multi-select") && (
1214
+ <QuestionSelect
1215
+ id={question.id}
1216
+ mode={question.type === "multi-select" ? "multi" : "single"}
1217
+ label={label}
1218
+ options={question.options ?? []}
1219
+ value={typeof answer === "string" || Array.isArray(answer) ? answer : undefined}
1220
+ onChange={(v) => survey.answer(question.id, v)}
1221
+ isRequired={question.required}
1222
+ />
1223
+ )}
1224
+ {question.type === "boolean" && (
1225
+ <QuestionBoolean
1226
+ id={question.id}
1227
+ label={label}
1228
+ value={typeof answer === "boolean" ? answer : null}
1229
+ onChange={(v) => survey.answer(question.id, v)}
1230
+ isRequired={question.required}
1231
+ />
1232
+ )}
1233
+ <div className="flex justify-end gap-2">
1234
+ {state.currentStep > 0 && (
1235
+ <button type="button" onClick={() => survey.prevQuestion()}>
1236
+ Back
1237
+ </button>
1238
+ )}
1239
+ <button
1240
+ type="button"
1241
+ onClick={() => (isLast ? survey.complete() : advance())}
1242
+ >
1243
+ {isLast ? "Done" : "Next"}
1244
+ </button>
1245
+ </div>
1246
+ </div>
1247
+ );
1248
+ }
1249
+
1250
+ // Registration alone leaves a survey hidden \u2014 show it ONCE when the provider's
1251
+ // gates (schedule, frequency, fatigue, sampling) allow.
1252
+ function SurveyAutoShow({ surveyId }: { surveyId: string }) {
1253
+ const survey = useSurvey(surveyId);
1254
+ const shownRef = useRef(false);
1255
+ useEffect(() => {
1256
+ if (survey.canShow && !shownRef.current) {
1257
+ shownRef.current = true;
1258
+ survey.show();
1259
+ }
1260
+ }, [survey]);
1261
+ return null;
1262
+ }`;
1263
+ function emitHooks(recipe) {
1264
+ const stubs = recipe.scaffold.hooks.map(
1265
+ (h) => `// ref: ${h.ref}
1266
+ export const ${hookName(h.ref)}: ${h.signature} = () => {
1267
+ throw new Error("TODO: implement hook for ${h.ref}");
1268
+ };`
1269
+ ).join("\n\n");
1270
+ return `${header(recipe.id, recipe.version)}
1271
+
1272
+ ${stubs}
1273
+ `;
1274
+ }
1275
+ function expand(recipe) {
1276
+ const body = recipe.components.map((c) => emitComponent(c, recipe)).join("\n\n");
1277
+ const helpers = recipe.components.some((c) => c.kind === "survey") ? `
1278
+
1279
+ ${SURVEY_HELPERS}` : "";
1280
+ const themeConsts = recipe.theme ? `
1281
+
1282
+ ${emitTheme(recipe.theme)}` : "";
1283
+ const contents = `${header(recipe.id, recipe.version)}
1284
+ "use client";
1285
+ ${imports(recipe)}
1286
+
1287
+ ${body}${helpers}${themeConsts}
1288
+
1289
+ ${emitMount(recipe)}
1290
+ `;
1291
+ const files = [{ path: `tours/${recipe.id}.tsx`, contents }];
1292
+ if (recipe.scaffold.hooks.length > 0)
1293
+ files.push({
1294
+ path: `tours/${recipe.id}.hooks.ts`,
1295
+ contents: emitHooks(recipe)
1296
+ });
1297
+ return files;
1298
+ }
1299
+
1300
+ // src/lib/fetch-recipe.ts
1301
+ import { z as z12 } from "zod";
1302
+ var SUPPORTED = 1;
1303
+ var schemaVersionProbe = z12.object({
1304
+ schemaVersion: z12.number().int().optional()
1305
+ });
1306
+ async function fetchRecipe(id, base = "https://usertourkit.com/builder") {
1307
+ const res = await fetch(`${base}/api/recipes/${id}`);
1308
+ if (res.status === 404) throw new CliError("unknown recipe", 1);
1309
+ const json = await res.json();
1310
+ const probe = schemaVersionProbe.safeParse(json);
1311
+ if (probe.success && probe.data.schemaVersion && probe.data.schemaVersion > SUPPORTED)
1312
+ throw new CliError(
1313
+ "recipe schemaVersion newer than this CLI \u2014 upgrade @tour-kit/studio",
1314
+ 2
1315
+ );
1316
+ return recipeSchemaV1.parse(json);
1317
+ }
1318
+
1319
+ // src/lib/patch.ts
1320
+ import { execFile } from "child_process";
1321
+ import { promisify } from "util";
1322
+ import { consola as consola2 } from "consola";
1323
+ import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
1324
+ var execFileAsync = promisify(execFile);
1325
+ function providerStack(tier, envExpr, module) {
1326
+ const tour = {
1327
+ name: "TourKitProvider",
1328
+ module,
1329
+ open: "<TourKitProvider>",
1330
+ close: "</TourKitProvider>"
1331
+ };
1332
+ if (tier === "free") return [tour];
1333
+ return [
1334
+ {
1335
+ name: "LicenseProvider",
1336
+ module,
1337
+ open: `<LicenseProvider licenseKey={${envExpr} ?? ""}>`,
1338
+ close: "</LicenseProvider>"
1339
+ },
1340
+ tour
1341
+ ];
1342
+ }
1343
+ var wrap2 = (providers, inner) => providers.reduceRight((acc, p) => `${p.open}${acc}${p.close}`, inner);
1344
+ function findBase(sf) {
1345
+ let found;
1346
+ sf.forEachDescendant((node, traversal) => {
1347
+ if (Node.isJsxExpression(node)) {
1348
+ const expr = node.getExpression();
1349
+ if (expr && Node.isIdentifier(expr) && expr.getText() === "children") {
1350
+ found = node;
1351
+ traversal.stop();
1352
+ }
1353
+ return;
1354
+ }
1355
+ if (Node.isJsxSelfClosingElement(node)) {
1356
+ if (node.getTagNameNode().getText() === "App") {
1357
+ found = node;
1358
+ traversal.stop();
1359
+ }
1360
+ }
1361
+ });
1362
+ return found;
1363
+ }
1364
+ function analyze(sf, names) {
1365
+ const base = findBase(sf);
1366
+ if (!base) return void 0;
1367
+ let target = base;
1368
+ const innerToOuter = [];
1369
+ let parent = base.getParent();
1370
+ while (parent && Node.isJsxElement(parent)) {
1371
+ const tag = parent.getOpeningElement().getTagNameNode().getText();
1372
+ if (!names.includes(tag)) break;
1373
+ innerToOuter.push(tag);
1374
+ target = parent;
1375
+ parent = parent.getParent();
1376
+ }
1377
+ return { target, present: innerToOuter.reverse() };
1378
+ }
1379
+ function patchRootLayout(entryFile, providers, mount) {
1380
+ const project = new Project({
1381
+ manipulationSettings: {
1382
+ indentationText: IndentationText.TwoSpaces,
1383
+ quoteKind: QuoteKind.Double
1384
+ }
1385
+ });
1386
+ const sf = project.addSourceFileAtPath(entryFile);
1387
+ const names = providers.map((p) => p.name);
1388
+ const importedNames = new Set(
1389
+ sf.getImportDeclarations().flatMap((d) => d.getNamedImports().map((n) => n.getName()))
1390
+ );
1391
+ for (const p of providers) {
1392
+ if (importedNames.has(p.name)) continue;
1393
+ const imp = sf.getImportDeclaration(p.module) ?? sf.addImportDeclaration({ namedImports: [], moduleSpecifier: p.module });
1394
+ imp.addNamedImport({ name: p.name });
1395
+ importedNames.add(p.name);
1396
+ }
1397
+ if (mount && !sf.getImportDeclaration(mount.specifier))
1398
+ sf.addImportDeclaration({
1399
+ defaultImport: mount.component,
1400
+ moduleSpecifier: mount.specifier
1401
+ });
1402
+ const located = analyze(sf, names);
1403
+ if (!located)
1404
+ throw new CliError(
1405
+ "could not locate the root children expression in entry file",
1406
+ 3
1407
+ );
1408
+ const base = findBase(sf);
1409
+ const missing = providers.filter((p) => !located.present.includes(p.name));
1410
+ const hasMount = mount ? sf.getFullText().includes(`<${mount.component}`) : true;
1411
+ let inner = located.target.getText();
1412
+ if (mount && !hasMount && base) {
1413
+ const baseText = base.getText();
1414
+ inner = inner.replace(baseText, `<${mount.component} />${baseText}`);
1415
+ }
1416
+ if (missing.length > 0 || mount && !hasMount)
1417
+ located.target.replaceWithText(wrap2(missing, inner));
1418
+ return sf.getFullText();
1419
+ }
1420
+ async function assertEntryClean(entryFile, cwd, force) {
1421
+ if (force) return;
1422
+ try {
1423
+ const { stdout } = await execFileAsync(
1424
+ "git",
1425
+ ["status", "--porcelain", "--", entryFile],
1426
+ { cwd }
1427
+ );
1428
+ if (stdout.trim() !== "")
1429
+ throw new CliError(
1430
+ "entry file has uncommitted changes; commit/stash or re-run with --force",
1431
+ 3
1432
+ );
1433
+ } catch (err) {
1434
+ if (err instanceof CliError) throw err;
1435
+ consola2.warn(
1436
+ `could not check git status for ${entryFile}; proceeding without the dirty-file guard`
1437
+ );
1438
+ }
1439
+ }
1440
+
1441
+ // src/commands/add.ts
1442
+ var installSpecs = (r) => r.packages.map((p) => `${p}@${r.deps[p]}`);
1443
+ var allInstallSpecs = (r) => [
1444
+ ...installSpecs(r),
1445
+ ...runtimePeerSpecs(r.packages)
1446
+ ];
1447
+ var installArgs = (pm, specs) => pm === "npm" ? ["install", "--save-exact", "--legacy-peer-deps", ...specs] : pm === "yarn" ? ["add", "--exact", ...specs] : pm === "bun" ? ["add", "--exact", ...specs] : ["add", "--save-exact", ...specs];
1448
+ var toursSpecifier = (recipe, cwd, entryFile, suffix) => {
1449
+ const rel = relative(
1450
+ dirname(entryFile),
1451
+ join2(cwd, "tours", `${recipe.id}${suffix}`)
1452
+ ).split(/[/\\]/).join("/");
1453
+ return rel.startsWith(".") ? rel : `./${rel}`;
1454
+ };
1455
+ var mountSpec = (recipe, cwd, entryFile) => ({
1456
+ component: `RecipeMount_${recipe.id.replace(/[^A-Za-z0-9]+/g, "_")}`,
1457
+ specifier: toursSpecifier(recipe, cwd, entryFile, "")
1458
+ });
1459
+ var providersSpecifier = (recipe, cwd, entryFile) => toursSpecifier(recipe, cwd, entryFile, ".providers");
1460
+ var providersShim = (tier) => {
1461
+ const lines = ['"use client";'];
1462
+ if (tier === "pro")
1463
+ lines.push('export { LicenseProvider } from "@tour-kit/license";');
1464
+ lines.push('export { TourKitProvider } from "@tour-kit/react";');
1465
+ return `${lines.join("\n")}
1466
+ `;
1467
+ };
1468
+ var assertPatchSupported = (framework) => {
1469
+ if (framework !== "next-app" && framework !== "vite-react")
1470
+ throw new CliError(
1471
+ `provider patch for ${framework} is not supported in v0.1`,
1472
+ 2
1473
+ );
1474
+ };
1475
+ async function ensureLicenseEnv(cwd, name) {
1476
+ const line = `${name}=`;
1477
+ for (const file of [".env.local", ".env.example"]) {
1478
+ const abs = join2(cwd, file);
1479
+ const existing = existsSync2(abs) ? await readFile(abs, "utf8") : "";
1480
+ const present = existing.split(/\r?\n/).some((l) => l.trimStart().startsWith(line));
1481
+ if (present) continue;
1482
+ const sep = existing === "" || existing.endsWith("\n") ? "" : "\n";
1483
+ await writeFile(abs, `${existing}${sep}${line}
1484
+ `);
1485
+ }
1486
+ }
1487
+ async function runFormatter(project, cwd, files, json) {
1488
+ if (!project.formatter) return;
1489
+ const args = project.formatter === "biome" ? ["format", "--write", ...files] : ["--write", ...files];
1490
+ try {
1491
+ await execa(project.formatter, args, { cwd, preferLocal: true });
1492
+ } catch {
1493
+ if (!json)
1494
+ consola3.warn(
1495
+ `formatter "${project.formatter}" failed; files written unformatted`
1496
+ );
1497
+ }
1498
+ }
1499
+ async function runAdd(args) {
1500
+ const json = Boolean(args.json);
1501
+ const cwd = args.cwd ?? process.cwd();
1502
+ const recipe = await fetchRecipe(args.recipe, args.base);
1503
+ const project = await detectProject(cwd);
1504
+ assertPatchSupported(project.framework);
1505
+ const providers = providerStack(
1506
+ recipe.tier,
1507
+ project.licenseEnv.expression,
1508
+ providersSpecifier(recipe, cwd, project.entryFile)
1509
+ );
1510
+ const shim = {
1511
+ path: `tours/${recipe.id}.providers.ts`,
1512
+ contents: providersShim(recipe.tier)
1513
+ };
1514
+ const files = [...expand(recipe), shim];
1515
+ const patched = patchRootLayout(
1516
+ project.entryFile,
1517
+ providers,
1518
+ mountSpec(recipe, cwd, project.entryFile)
1519
+ );
1520
+ const peers = runtimePeerSpecs(recipe.packages);
1521
+ const specs = allInstallSpecs(recipe);
1522
+ if (args["dry-run"]) {
1523
+ const result2 = {
1524
+ ok: true,
1525
+ framework: project.framework,
1526
+ packages: recipe.packages,
1527
+ runtimePeers: peers,
1528
+ filesWritten: files.map((f) => f.path),
1529
+ providerPatched: patched !== await readFile(project.entryFile, "utf8")
1530
+ };
1531
+ if (json) {
1532
+ emitResult(result2, true);
1533
+ } else {
1534
+ const current = await readFile(project.entryFile, "utf8");
1535
+ consola3.box(
1536
+ [
1537
+ "Dry run \u2014 nothing written",
1538
+ `install: ${project.packageManager} ${installArgs(project.packageManager, specs).join(" ")}`,
1539
+ `peers: ${peers.join(", ") || "(none)"}`,
1540
+ `files: ${files.map((f) => f.path).join(", ")}`,
1541
+ `entry: ${project.entryFile}${patched === current ? " (already patched)" : " (would patch providers)"}`
1542
+ ].join("\n")
1543
+ );
1544
+ }
1545
+ return;
1546
+ }
1547
+ await assertEntryClean(project.entryFile, cwd, Boolean(args.force));
1548
+ if (!args["no-install"]) {
1549
+ try {
1550
+ await execa(
1551
+ project.packageManager,
1552
+ installArgs(project.packageManager, specs),
1553
+ { cwd }
1554
+ );
1555
+ } catch (err) {
1556
+ throw new CliError(
1557
+ `install failed: ${err instanceof Error ? err.message : String(err)}`,
1558
+ 1
1559
+ );
1560
+ }
1561
+ }
1562
+ const filesWritten = [];
1563
+ for (const f of files) {
1564
+ const abs = join2(cwd, f.path);
1565
+ if (existsSync2(abs) && !args.overwrite) {
1566
+ if (!json)
1567
+ consola3.warn(`skipped existing ${f.path} (use --overwrite to replace)`);
1568
+ continue;
1569
+ }
1570
+ await mkdir(dirname(abs), { recursive: true });
1571
+ await writeFile(abs, f.contents);
1572
+ filesWritten.push(f.path);
1573
+ }
1574
+ await writeFile(project.entryFile, patched);
1575
+ await runFormatter(
1576
+ project,
1577
+ cwd,
1578
+ [project.entryFile, ...filesWritten.map((p) => join2(cwd, p))],
1579
+ json
1580
+ );
1581
+ if (recipe.tier === "pro")
1582
+ await ensureLicenseEnv(cwd, project.licenseEnv.name);
1583
+ const result = {
1584
+ ok: true,
1585
+ framework: project.framework,
1586
+ packages: recipe.packages,
1587
+ runtimePeers: peers,
1588
+ filesWritten,
1589
+ providerPatched: true
1590
+ };
1591
+ if (json) {
1592
+ emitResult(result, true);
1593
+ return;
1594
+ }
1595
+ consola3.success(`Detected ${project.framework} (${project.packageManager})`);
1596
+ if (!args["no-install"]) {
1597
+ consola3.success(`Installed ${recipe.packages.join(", ") || "(none)"}`);
1598
+ if (peers.length > 0)
1599
+ consola3.success(`Installed runtime peers ${peers.join(", ")}`);
1600
+ }
1601
+ consola3.success(`Scaffolded ${filesWritten.join(", ") || "(no new files)"}`);
1602
+ consola3.success(`Patched providers into ${project.entryFile}`);
1603
+ }
1604
+ var add = defineCommand({
1605
+ meta: {
1606
+ name: "add",
1607
+ description: "Install a recipe into this project"
1608
+ },
1609
+ args: {
1610
+ recipe: {
1611
+ type: "positional",
1612
+ description: "Recipe id to add",
1613
+ required: true
1614
+ },
1615
+ cwd: { type: "string", description: "Project root (defaults to .)" },
1616
+ base: {
1617
+ type: "string",
1618
+ description: "Recipe API base URL (defaults to the Studio API)"
1619
+ },
1620
+ "dry-run": {
1621
+ type: "boolean",
1622
+ description: "Print the plan, change nothing"
1623
+ },
1624
+ yes: { type: "boolean", description: "Skip confirmation prompts" },
1625
+ "no-install": {
1626
+ type: "boolean",
1627
+ description: "Scaffold without installing"
1628
+ },
1629
+ overwrite: {
1630
+ type: "boolean",
1631
+ description: "Overwrite an existing scaffold file"
1632
+ },
1633
+ force: {
1634
+ type: "boolean",
1635
+ description: "Patch even a git-dirty entry file"
1636
+ },
1637
+ json: { type: "boolean", description: "Emit one machine-readable result" }
1638
+ },
1639
+ run: ({ args }) => runOrExit(() => runAdd(args))
1640
+ });
1641
+
1642
+ // src/commands/harden.ts
1643
+ import { defineCommand as defineCommand2 } from "citty";
1644
+ import { consola as consola4 } from "consola";
1645
+ var harden = defineCommand2({
1646
+ meta: {
1647
+ name: "harden",
1648
+ description: "Inject data-tour-id attributes on referenced elements (coming in a later phase)"
1649
+ },
1650
+ args: {
1651
+ recipe: {
1652
+ type: "positional",
1653
+ description: "Recipe id(s) to harden",
1654
+ required: false
1655
+ },
1656
+ cwd: { type: "string", description: "Project root (defaults to .)" },
1657
+ json: { type: "boolean", description: "Emit machine-readable JSON" }
1658
+ },
1659
+ run: () => {
1660
+ consola4.info(
1661
+ "`tourkit harden` is not implemented in v0.1 \u2014 coming in a later phase."
1662
+ );
1663
+ }
1664
+ });
1665
+
1666
+ // src/commands/init.ts
1667
+ import { defineCommand as defineCommand3 } from "citty";
1668
+ import { consola as consola5 } from "consola";
1669
+ var init = defineCommand3({
1670
+ meta: {
1671
+ name: "init",
1672
+ description: "Create a tourkit.config.json manifest (coming in a later phase)"
1673
+ },
1674
+ args: {
1675
+ cwd: { type: "string", description: "Project root (defaults to .)" },
1676
+ json: { type: "boolean", description: "Emit machine-readable JSON" }
1677
+ },
1678
+ run: () => {
1679
+ consola5.info(
1680
+ "`tourkit init` is not implemented in v0.1 \u2014 coming in a later phase."
1681
+ );
1682
+ }
1683
+ });
1684
+
1685
+ // src/commands/pull.ts
1686
+ import { defineCommand as defineCommand4 } from "citty";
1687
+ import { consola as consola6 } from "consola";
1688
+ var pull = defineCommand4({
1689
+ meta: {
1690
+ name: "pull",
1691
+ description: "Re-sync a recipe (coming in a later phase)"
1692
+ },
1693
+ args: {
1694
+ recipe: {
1695
+ type: "positional",
1696
+ description: "Recipe id to pull",
1697
+ required: true
1698
+ },
1699
+ cwd: { type: "string", description: "Project root (defaults to .)" },
1700
+ json: { type: "boolean", description: "Emit machine-readable JSON" }
1701
+ },
1702
+ run: () => {
1703
+ consola6.info(
1704
+ "`tourkit pull` is not implemented in v0.1 \u2014 coming in a later phase."
1705
+ );
1706
+ }
1707
+ });
1708
+
1709
+ // src/commands/verify.ts
1710
+ import { defineCommand as defineCommand5 } from "citty";
1711
+ import { consola as consola7 } from "consola";
1712
+ var verify = defineCommand5({
1713
+ meta: {
1714
+ name: "verify",
1715
+ description: "Check tour selectors against a running app (coming in a later phase)"
1716
+ },
1717
+ args: {
1718
+ recipe: {
1719
+ type: "positional",
1720
+ description: "Recipe id(s) to verify",
1721
+ required: false
1722
+ },
1723
+ cwd: { type: "string", description: "Project root (defaults to .)" },
1724
+ json: { type: "boolean", description: "Emit machine-readable JSON" }
1725
+ },
1726
+ run: () => {
1727
+ consola7.info(
1728
+ "`tourkit verify` is not implemented in v0.1 \u2014 coming in a later phase."
1729
+ );
1730
+ }
1731
+ });
1732
+
1733
+ // src/index.ts
1734
+ runMain(
1735
+ defineCommand6({
1736
+ meta: {
1737
+ name: "tourkit",
1738
+ version: "0.1.0",
1739
+ description: "User Tour Kit Studio CLI"
1740
+ },
1741
+ subCommands: { add, pull, verify, harden, init }
1742
+ })
1743
+ );