@embeddables/cli 0.7.19 → 0.8.1

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 (256) hide show
  1. package/.prompts/embeddables-cli.md +7 -4
  2. package/dist/auth/index.d.ts +43 -0
  3. package/dist/auth/index.d.ts.map +1 -0
  4. package/dist/auth/index.js +102 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +210 -0
  8. package/dist/command-history.d.ts +13 -0
  9. package/dist/command-history.d.ts.map +1 -0
  10. package/dist/command-history.js +34 -0
  11. package/dist/commands/branch.d.ts +4 -0
  12. package/dist/commands/branch.d.ts.map +1 -0
  13. package/dist/commands/branch.js +67 -0
  14. package/dist/commands/build-workbench.d.ts +5 -0
  15. package/dist/commands/build-workbench.d.ts.map +1 -0
  16. package/dist/commands/build-workbench.js +116 -0
  17. package/dist/commands/build.d.ts +8 -0
  18. package/dist/commands/build.d.ts.map +1 -0
  19. package/dist/commands/build.js +60 -0
  20. package/dist/commands/builder-open.d.ts +4 -0
  21. package/dist/commands/builder-open.d.ts.map +1 -0
  22. package/dist/commands/builder-open.js +74 -0
  23. package/dist/commands/dev.d.ts +12 -0
  24. package/dist/commands/dev.d.ts.map +1 -0
  25. package/dist/commands/dev.js +226 -0
  26. package/dist/commands/diff.d.ts +76 -0
  27. package/dist/commands/diff.d.ts.map +1 -0
  28. package/dist/commands/diff.js +653 -0
  29. package/dist/commands/experiments-connect.d.ts +6 -0
  30. package/dist/commands/experiments-connect.d.ts.map +1 -0
  31. package/dist/commands/experiments-connect.js +140 -0
  32. package/dist/commands/feedback.d.ts +29 -0
  33. package/dist/commands/feedback.d.ts.map +1 -0
  34. package/dist/commands/feedback.js +267 -0
  35. package/dist/commands/init.d.ts +5 -0
  36. package/dist/commands/init.d.ts.map +1 -0
  37. package/dist/commands/init.js +384 -0
  38. package/dist/commands/inspect.d.ts +9 -0
  39. package/dist/commands/inspect.d.ts.map +1 -0
  40. package/dist/commands/inspect.js +293 -0
  41. package/dist/commands/login.d.ts +2 -0
  42. package/dist/commands/login.d.ts.map +1 -0
  43. package/dist/commands/login.js +117 -0
  44. package/dist/commands/logout.d.ts +2 -0
  45. package/dist/commands/logout.d.ts.map +1 -0
  46. package/dist/commands/logout.js +19 -0
  47. package/dist/commands/pull.d.ts +16 -0
  48. package/dist/commands/pull.d.ts.map +1 -0
  49. package/dist/commands/pull.js +395 -0
  50. package/dist/commands/save.d.ts +30 -0
  51. package/dist/commands/save.d.ts.map +1 -0
  52. package/dist/commands/save.js +597 -0
  53. package/dist/commands/upgrade.d.ts +2 -0
  54. package/dist/commands/upgrade.d.ts.map +1 -0
  55. package/dist/commands/upgrade.js +50 -0
  56. package/dist/compiler/errors.d.ts +20 -0
  57. package/dist/compiler/errors.d.ts.map +1 -0
  58. package/dist/compiler/errors.js +35 -0
  59. package/dist/compiler/evalStatic.d.ts +3 -0
  60. package/dist/compiler/evalStatic.d.ts.map +1 -0
  61. package/dist/compiler/evalStatic.js +57 -0
  62. package/dist/compiler/flatten.js +1 -0
  63. package/dist/compiler/helpers/duplicateIds.d.ts +9 -0
  64. package/dist/compiler/helpers/duplicateIds.d.ts.map +1 -0
  65. package/dist/compiler/helpers/duplicateIds.js +71 -0
  66. package/dist/compiler/helpers/numericLeadingKeys.d.ts +8 -0
  67. package/dist/compiler/helpers/numericLeadingKeys.d.ts.map +1 -0
  68. package/dist/compiler/helpers/numericLeadingKeys.js +17 -0
  69. package/dist/compiler/index.d.ts +18 -0
  70. package/dist/compiler/index.d.ts.map +1 -0
  71. package/dist/compiler/index.js +1272 -0
  72. package/dist/compiler/parsePage.d.ts +15 -0
  73. package/dist/compiler/parsePage.d.ts.map +1 -0
  74. package/dist/compiler/parsePage.js +654 -0
  75. package/dist/compiler/registry.d.ts +4 -0
  76. package/dist/compiler/registry.d.ts.map +1 -0
  77. package/dist/compiler/registry.js +44 -0
  78. package/dist/compiler/reverse.d.ts +23 -0
  79. package/dist/compiler/reverse.d.ts.map +1 -0
  80. package/dist/compiler/reverse.js +1920 -0
  81. package/dist/compiler/types.d.ts +21 -0
  82. package/dist/compiler/types.d.ts.map +1 -0
  83. package/dist/compiler/types.js +1 -0
  84. package/dist/components/index.d.ts +21 -0
  85. package/dist/components/index.d.ts.map +1 -0
  86. package/dist/components/index.js +21 -0
  87. package/dist/components/primitives/BaseComponent.d.ts +33 -0
  88. package/dist/components/primitives/BaseComponent.d.ts.map +1 -0
  89. package/dist/components/primitives/BaseComponent.js +26 -0
  90. package/dist/components/primitives/BookMeeting.d.ts +18 -0
  91. package/dist/components/primitives/BookMeeting.d.ts.map +1 -0
  92. package/dist/components/primitives/BookMeeting.js +5 -0
  93. package/dist/components/primitives/Chart.d.ts +41 -0
  94. package/dist/components/primitives/Chart.d.ts.map +1 -0
  95. package/dist/components/primitives/Chart.js +5 -0
  96. package/dist/components/primitives/Container.d.ts +8 -0
  97. package/dist/components/primitives/Container.d.ts.map +1 -0
  98. package/dist/components/primitives/Container.js +5 -0
  99. package/dist/components/primitives/CustomButton.d.ts +37 -0
  100. package/dist/components/primitives/CustomButton.d.ts.map +1 -0
  101. package/dist/components/primitives/CustomButton.js +10 -0
  102. package/dist/components/primitives/CustomHTML.d.ts +8 -0
  103. package/dist/components/primitives/CustomHTML.d.ts.map +1 -0
  104. package/dist/components/primitives/CustomHTML.js +5 -0
  105. package/dist/components/primitives/FileUpload.d.ts +18 -0
  106. package/dist/components/primitives/FileUpload.d.ts.map +1 -0
  107. package/dist/components/primitives/FileUpload.js +16 -0
  108. package/dist/components/primitives/InputBox.d.ts +34 -0
  109. package/dist/components/primitives/InputBox.d.ts.map +1 -0
  110. package/dist/components/primitives/InputBox.js +25 -0
  111. package/dist/components/primitives/Lottie.d.ts +11 -0
  112. package/dist/components/primitives/Lottie.d.ts.map +1 -0
  113. package/dist/components/primitives/Lottie.js +5 -0
  114. package/dist/components/primitives/MediaEmbed.d.ts +13 -0
  115. package/dist/components/primitives/MediaEmbed.d.ts.map +1 -0
  116. package/dist/components/primitives/MediaEmbed.js +6 -0
  117. package/dist/components/primitives/MediaImage.d.ts +8 -0
  118. package/dist/components/primitives/MediaImage.d.ts.map +1 -0
  119. package/dist/components/primitives/MediaImage.js +5 -0
  120. package/dist/components/primitives/OptionSelector.d.ts +38 -0
  121. package/dist/components/primitives/OptionSelector.d.ts.map +1 -0
  122. package/dist/components/primitives/OptionSelector.js +8 -0
  123. package/dist/components/primitives/PaypalCheckout.d.ts +25 -0
  124. package/dist/components/primitives/PaypalCheckout.d.ts.map +1 -0
  125. package/dist/components/primitives/PaypalCheckout.js +5 -0
  126. package/dist/components/primitives/PlainText.d.ts +6 -0
  127. package/dist/components/primitives/PlainText.d.ts.map +1 -0
  128. package/dist/components/primitives/PlainText.js +5 -0
  129. package/dist/components/primitives/ProgressBar.d.ts +15 -0
  130. package/dist/components/primitives/ProgressBar.d.ts.map +1 -0
  131. package/dist/components/primitives/ProgressBar.js +5 -0
  132. package/dist/components/primitives/RichText.d.ts +6 -0
  133. package/dist/components/primitives/RichText.d.ts.map +1 -0
  134. package/dist/components/primitives/RichText.js +5 -0
  135. package/dist/components/primitives/RichTextMarkdown.d.ts +6 -0
  136. package/dist/components/primitives/RichTextMarkdown.d.ts.map +1 -0
  137. package/dist/components/primitives/RichTextMarkdown.js +5 -0
  138. package/dist/components/primitives/Rive.d.ts +16 -0
  139. package/dist/components/primitives/Rive.d.ts.map +1 -0
  140. package/dist/components/primitives/Rive.js +8 -0
  141. package/dist/components/primitives/StripeCheckout.d.ts +52 -0
  142. package/dist/components/primitives/StripeCheckout.d.ts.map +1 -0
  143. package/dist/components/primitives/StripeCheckout.js +5 -0
  144. package/dist/components/primitives/StripeCheckout2.d.ts +30 -0
  145. package/dist/components/primitives/StripeCheckout2.d.ts.map +1 -0
  146. package/dist/components/primitives/StripeCheckout2.js +7 -0
  147. package/dist/config/index.d.ts +23 -0
  148. package/dist/config/index.d.ts.map +1 -0
  149. package/dist/config/index.js +42 -0
  150. package/dist/constants.d.ts +9 -0
  151. package/dist/constants.d.ts.map +1 -0
  152. package/dist/constants.js +9 -0
  153. package/dist/helpers/TEMP helpers file.d.ts +1 -0
  154. package/dist/helpers/TEMP helpers file.d.ts.map +1 -0
  155. package/dist/helpers/TEMP helpers file.js +1 -0
  156. package/dist/helpers/dates.d.ts +5 -0
  157. package/dist/helpers/dates.d.ts.map +1 -0
  158. package/dist/helpers/dates.js +7 -0
  159. package/dist/helpers/json.d.ts +47 -0
  160. package/dist/helpers/json.d.ts.map +1 -0
  161. package/dist/helpers/json.js +622 -0
  162. package/dist/helpers/prompt.d.ts +15 -0
  163. package/dist/helpers/prompt.d.ts.map +1 -0
  164. package/dist/helpers/prompt.js +35 -0
  165. package/dist/helpers/utils.d.ts +13 -0
  166. package/dist/helpers/utils.d.ts.map +1 -0
  167. package/dist/helpers/utils.js +28 -0
  168. package/dist/logger.d.ts +11 -0
  169. package/dist/logger.d.ts.map +1 -0
  170. package/dist/logger.js +21 -0
  171. package/dist/patches/prompts-escape.d.ts +14 -0
  172. package/dist/patches/prompts-escape.d.ts.map +1 -0
  173. package/dist/patches/prompts-escape.js +23 -0
  174. package/dist/prompts/branches.d.ts +20 -0
  175. package/dist/prompts/branches.d.ts.map +1 -0
  176. package/dist/prompts/branches.js +86 -0
  177. package/dist/prompts/embeddables.d.ts +43 -0
  178. package/dist/prompts/embeddables.d.ts.map +1 -0
  179. package/dist/prompts/embeddables.js +200 -0
  180. package/dist/prompts/experiments.d.ts +28 -0
  181. package/dist/prompts/experiments.d.ts.map +1 -0
  182. package/dist/prompts/experiments.js +89 -0
  183. package/dist/prompts/index.d.ts +11 -0
  184. package/dist/prompts/index.d.ts.map +1 -0
  185. package/dist/prompts/index.js +6 -0
  186. package/dist/prompts/projects.d.ts +22 -0
  187. package/dist/prompts/projects.d.ts.map +1 -0
  188. package/dist/prompts/projects.js +92 -0
  189. package/dist/prompts/versions.d.ts +18 -0
  190. package/dist/prompts/versions.d.ts.map +1 -0
  191. package/dist/prompts/versions.js +95 -0
  192. package/dist/proxy/injectApiInterceptor.d.ts +6 -0
  193. package/dist/proxy/injectApiInterceptor.d.ts.map +1 -0
  194. package/dist/proxy/injectApiInterceptor.js +66 -0
  195. package/dist/proxy/injectReload.d.ts +2 -0
  196. package/dist/proxy/injectReload.d.ts.map +1 -0
  197. package/dist/proxy/injectReload.js +14 -0
  198. package/dist/proxy/injectWorkbench.d.ts +5 -0
  199. package/dist/proxy/injectWorkbench.d.ts.map +1 -0
  200. package/dist/proxy/injectWorkbench.js +22 -0
  201. package/dist/proxy/server.d.ts +11 -0
  202. package/dist/proxy/server.d.ts.map +1 -0
  203. package/dist/proxy/server.js +304 -0
  204. package/dist/proxy/sse.d.ts +5 -0
  205. package/dist/proxy/sse.d.ts.map +1 -0
  206. package/dist/proxy/sse.js +17 -0
  207. package/dist/sentry-context.d.ts +48 -0
  208. package/dist/sentry-context.d.ts.map +1 -0
  209. package/dist/sentry-context.js +156 -0
  210. package/dist/stdout.d.ts +61 -0
  211. package/dist/stdout.d.ts.map +1 -0
  212. package/dist/stdout.js +163 -0
  213. package/dist/types-builder.d.ts +800 -0
  214. package/dist/types-builder.d.ts.map +1 -0
  215. package/dist/types-builder.js +20 -0
  216. package/dist/workbench/ActionsPanel.d.ts +6 -0
  217. package/dist/workbench/ActionsPanel.d.ts.map +1 -0
  218. package/dist/workbench/ActionsPanel.js +47 -0
  219. package/dist/workbench/AutofillPanel.d.ts +6 -0
  220. package/dist/workbench/AutofillPanel.d.ts.map +1 -0
  221. package/dist/workbench/AutofillPanel.js +543 -0
  222. package/dist/workbench/ComputedFieldsPanel.d.ts +6 -0
  223. package/dist/workbench/ComputedFieldsPanel.d.ts.map +1 -0
  224. package/dist/workbench/ComputedFieldsPanel.js +31 -0
  225. package/dist/workbench/ExperimentsPanel.d.ts +6 -0
  226. package/dist/workbench/ExperimentsPanel.d.ts.map +1 -0
  227. package/dist/workbench/ExperimentsPanel.js +182 -0
  228. package/dist/workbench/FieldEditorPanel.d.ts +9 -0
  229. package/dist/workbench/FieldEditorPanel.d.ts.map +1 -0
  230. package/dist/workbench/FieldEditorPanel.js +650 -0
  231. package/dist/workbench/InspectorPanel.d.ts +6 -0
  232. package/dist/workbench/InspectorPanel.d.ts.map +1 -0
  233. package/dist/workbench/InspectorPanel.js +341 -0
  234. package/dist/workbench/PageNavigator.d.ts +6 -0
  235. package/dist/workbench/PageNavigator.d.ts.map +1 -0
  236. package/dist/workbench/PageNavigator.js +123 -0
  237. package/dist/workbench/SchemaPanel.d.ts +6 -0
  238. package/dist/workbench/SchemaPanel.d.ts.map +1 -0
  239. package/dist/workbench/SchemaPanel.js +222 -0
  240. package/dist/workbench/UserDataPanel.d.ts +6 -0
  241. package/dist/workbench/UserDataPanel.d.ts.map +1 -0
  242. package/dist/workbench/UserDataPanel.js +350 -0
  243. package/dist/workbench/WorkbenchApp.d.ts +7 -0
  244. package/dist/workbench/WorkbenchApp.d.ts.map +1 -0
  245. package/dist/workbench/WorkbenchApp.js +193 -0
  246. package/dist/workbench/cloudflare-worker/README.md +31 -0
  247. package/dist/workbench/cloudflare-worker/public/workbench.css +1614 -0
  248. package/dist/workbench/cloudflare-worker/public/workbench.js +77 -0
  249. package/dist/workbench/cloudflare-worker/worker.js +40 -0
  250. package/dist/workbench/cloudflare-worker/wrangler.toml +10 -0
  251. package/dist/workbench/index.d.ts +10 -0
  252. package/dist/workbench/index.d.ts.map +1 -0
  253. package/dist/workbench/index.js +44 -0
  254. package/dist/workbench/workbench.css +1614 -0
  255. package/dist/workbench/workbench.js +77 -0
  256. package/package.json +4 -1
@@ -0,0 +1,92 @@
1
+ import pc from 'picocolors';
2
+ import { getAuthenticatedSupabaseClient } from '../auth/index.js';
3
+ import { prompt } from '../helpers/prompt.js';
4
+ import * as stdout from '../stdout.js';
5
+ /**
6
+ * Fetch all non-archived projects from Supabase
7
+ */
8
+ export async function fetchProjects() {
9
+ const supabase = await getAuthenticatedSupabaseClient();
10
+ if (!supabase) {
11
+ return [];
12
+ }
13
+ try {
14
+ const { data, error } = await supabase
15
+ .from('projects')
16
+ .select(`
17
+ id,
18
+ title,
19
+ group_id,
20
+ groups (
21
+ title
22
+ )
23
+ `)
24
+ .not('archived', 'is', 'true')
25
+ .order('title', { ascending: true });
26
+ if (error) {
27
+ stdout.warn(pc.yellow(`Could not fetch projects: ${error.message}`));
28
+ return [];
29
+ }
30
+ const projects = (data || []).map((row) => ({
31
+ id: row.id,
32
+ title: row.title || null,
33
+ org_id: row.group_id || null,
34
+ org_title:
35
+ // For some reason, I can't get Supabase typings to return this as an object
36
+ // (not an array of objects) so I have to convert it manually
37
+ row.groups?.title || null,
38
+ }));
39
+ projects.sort((a, b) => {
40
+ if (a.title && !b.title)
41
+ return -1;
42
+ if (!a.title && b.title)
43
+ return 1;
44
+ const aLabel = a.title || a.id;
45
+ const bLabel = b.title || b.id;
46
+ return aLabel.localeCompare(bLabel, undefined, { sensitivity: 'base' });
47
+ });
48
+ return projects;
49
+ }
50
+ catch (err) {
51
+ stdout.warn(pc.yellow(`Could not fetch projects: ${err}`));
52
+ return [];
53
+ }
54
+ }
55
+ /**
56
+ * Prompt the user to select a project from the list
57
+ * Returns null if no projects found, user cancels, or user selects "skip"
58
+ */
59
+ export async function promptForProject(options = {}) {
60
+ const { allowSkip = false, message = 'Select a project:' } = options;
61
+ const projects = await fetchProjects();
62
+ if (projects.length === 0) {
63
+ stdout.print(pc.yellow('No projects found.'));
64
+ return null;
65
+ }
66
+ const choices = projects.map((p) => ({
67
+ title: p.title || p.id,
68
+ description: p.org_title ? `${p.org_title} (${p.id})` : p.id,
69
+ value: p.id,
70
+ }));
71
+ if (allowSkip) {
72
+ choices.push({
73
+ title: pc.dim('Skip for now'),
74
+ description: 'You can set this later',
75
+ value: '__skip__',
76
+ });
77
+ }
78
+ const response = await prompt({
79
+ type: 'autocomplete',
80
+ name: 'id',
81
+ message,
82
+ choices,
83
+ suggest: (input, choices) => Promise.resolve(choices.filter((c) => c.value === '__skip__' ||
84
+ (c.title?.toLowerCase().includes(input.toLowerCase()) ?? false) ||
85
+ String(c.value).toLowerCase().includes(input.toLowerCase()) ||
86
+ c.description?.toLowerCase().includes(input.toLowerCase()))),
87
+ });
88
+ if (!response.id || response.id === '__skip__') {
89
+ return null;
90
+ }
91
+ return projects.find((p) => p.id === response.id) || null;
92
+ }
@@ -0,0 +1,18 @@
1
+ export interface VersionInfo {
2
+ version_number: number;
3
+ created_at: string | null;
4
+ }
5
+ /**
6
+ * Fetch up to 100 most recent version numbers for an embeddable (flow).
7
+ * Uses flow_versions for the given flow_id and branch (null = main).
8
+ * Returns unique version numbers in descending order.
9
+ */
10
+ export declare function fetchRecentVersions(flowId: string, branchId: string | null): Promise<VersionInfo[]>;
11
+ /**
12
+ * Prompt the user to select a version from the list.
13
+ * Returns the selected version number, or null for "Latest".
14
+ */
15
+ export declare function promptForVersion(versions: VersionInfo[], options?: {
16
+ message?: string;
17
+ }): Promise<number | null>;
18
+ //# sourceMappingURL=versions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versions.d.ts","sourceRoot":"","sources":["../../src/prompts/versions.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAID;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,WAAW,EAAE,CAAC,CA8CxB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2CxB"}
@@ -0,0 +1,95 @@
1
+ import pc from 'picocolors';
2
+ import { getAuthenticatedSupabaseClient } from '../auth/index.js';
3
+ import { prompt } from '../helpers/prompt.js';
4
+ import * as stdout from '../stdout.js';
5
+ import { formatDate } from '../helpers/dates.js';
6
+ const RECENT_VERSIONS_LIMIT = 100;
7
+ /**
8
+ * Fetch up to 100 most recent version numbers for an embeddable (flow).
9
+ * Uses flow_versions for the given flow_id and branch (null = main).
10
+ * Returns unique version numbers in descending order.
11
+ */
12
+ export async function fetchRecentVersions(flowId, branchId) {
13
+ const supabase = await getAuthenticatedSupabaseClient();
14
+ if (!supabase) {
15
+ return [];
16
+ }
17
+ try {
18
+ let query = supabase
19
+ .from('flow_versions')
20
+ .select('version_number, created_at')
21
+ .eq('flow_id', flowId)
22
+ .order('version_number', { ascending: false })
23
+ .limit(RECENT_VERSIONS_LIMIT * 2); // fetch extra then dedupe
24
+ if (branchId === null) {
25
+ query = query.is('branch_id', null);
26
+ }
27
+ else {
28
+ query = query.eq('branch_id', branchId);
29
+ }
30
+ const { data, error } = await query;
31
+ if (error) {
32
+ stdout.warn(pc.yellow(`Could not fetch versions: ${error.message}`));
33
+ return [];
34
+ }
35
+ // Dedupe by version_number, preserving order (most recent first)
36
+ const seen = new Set();
37
+ const versions = [];
38
+ for (const row of data || []) {
39
+ const v = row.version_number;
40
+ if (typeof v !== 'number' || seen.has(v))
41
+ continue;
42
+ seen.add(v);
43
+ versions.push({
44
+ version_number: v,
45
+ created_at: row.created_at ?? null,
46
+ });
47
+ if (versions.length >= RECENT_VERSIONS_LIMIT)
48
+ break;
49
+ }
50
+ return versions;
51
+ }
52
+ catch (err) {
53
+ stdout.warn(pc.yellow(`Could not fetch versions: ${err}`));
54
+ return [];
55
+ }
56
+ }
57
+ /**
58
+ * Prompt the user to select a version from the list.
59
+ * Returns the selected version number, or null for "Latest".
60
+ */
61
+ export async function promptForVersion(versions, options = {}) {
62
+ const choices = [];
63
+ if (versions.length === 0) {
64
+ choices.push({
65
+ title: pc.bold('Latest'),
66
+ value: 'latest',
67
+ description: 'Current published version',
68
+ });
69
+ }
70
+ else {
71
+ for (let i = 0; i < versions.length; i++) {
72
+ const v = versions[i];
73
+ const date = v.created_at ? formatDate(v.created_at) : '';
74
+ const isLatest = i === 0;
75
+ choices.push({
76
+ title: isLatest ? `Version ${v.version_number} (Latest)` : `Version ${v.version_number}`,
77
+ value: isLatest ? 'latest' : v.version_number,
78
+ description: date || undefined,
79
+ });
80
+ }
81
+ }
82
+ const response = await prompt({
83
+ type: 'autocomplete',
84
+ name: 'version',
85
+ message: options.message ?? 'Select a version to pull:',
86
+ choices,
87
+ suggest: (input, choicesList) => Promise.resolve(choicesList.filter((c) => c.value === 'latest' ||
88
+ String(c.value).includes(input) ||
89
+ (typeof c.title === 'string' && c.title.toLowerCase().includes(input.toLowerCase())))),
90
+ });
91
+ if (response.version === 'latest' || response.version === undefined) {
92
+ return null;
93
+ }
94
+ return typeof response.version === 'number' ? response.version : null;
95
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Injects a script that intercepts fetch/XHR calls to the remote engine
3
+ * and rewrites them to go through the local proxy instead.
4
+ */
5
+ export declare function injectApiInterceptor(html: string, remoteOrigin: string): string;
6
+ //# sourceMappingURL=injectApiInterceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectApiInterceptor.d.ts","sourceRoot":"","sources":["../../src/proxy/injectApiInterceptor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAiE/E"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Injects a script that intercepts fetch/XHR calls to the remote engine
3
+ * and rewrites them to go through the local proxy instead.
4
+ */
5
+ export function injectApiInterceptor(html, remoteOrigin) {
6
+ // This script needs to run BEFORE any other scripts, so inject into <head>
7
+ const lower = html.toLowerCase();
8
+ const headCloseIndex = lower.indexOf('</head>');
9
+ // The interceptor script - patches fetch and XMLHttpRequest
10
+ const interceptorScript = `<script>(function(){
11
+ var remoteOrigin = ${JSON.stringify(remoteOrigin)};
12
+ var localOrigin = window.location.origin;
13
+
14
+ // Helper to rewrite URLs
15
+ function rewriteUrl(url) {
16
+ if (typeof url === 'string' && url.startsWith(remoteOrigin)) {
17
+ var newUrl = url.replace(remoteOrigin, localOrigin);
18
+ console.log('[DevProxy] Intercepted:', url, '->', newUrl);
19
+ return newUrl;
20
+ }
21
+ if (url instanceof URL && url.origin === remoteOrigin) {
22
+ var newUrl = new URL(url.pathname + url.search + url.hash, localOrigin);
23
+ console.log('[DevProxy] Intercepted:', url.href, '->', newUrl.href);
24
+ return newUrl;
25
+ }
26
+ return url;
27
+ }
28
+
29
+ // Patch fetch
30
+ var originalFetch = window.fetch;
31
+ window.fetch = function(input, init) {
32
+ if (typeof input === 'string') {
33
+ input = rewriteUrl(input);
34
+ } else if (input instanceof Request) {
35
+ var newUrl = rewriteUrl(input.url);
36
+ if (newUrl !== input.url) {
37
+ input = new Request(newUrl, input);
38
+ }
39
+ } else if (input instanceof URL) {
40
+ input = rewriteUrl(input);
41
+ }
42
+ return originalFetch.call(this, input, init);
43
+ };
44
+
45
+ // Patch XMLHttpRequest
46
+ var originalXHROpen = XMLHttpRequest.prototype.open;
47
+ XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
48
+ url = rewriteUrl(url);
49
+ return originalXHROpen.call(this, method, url, async, user, password);
50
+ };
51
+
52
+ console.log('[DevProxy] API interceptor installed, rewriting', remoteOrigin, 'to', localOrigin);
53
+ })();</script>`;
54
+ const injection = `\n${interceptorScript}\n`;
55
+ if (headCloseIndex === -1) {
56
+ // No </head> tag found; try to inject after <head> or at the start
57
+ const headOpenIndex = lower.indexOf('<head>');
58
+ if (headOpenIndex !== -1) {
59
+ const insertPos = headOpenIndex + '<head>'.length;
60
+ return html.slice(0, insertPos) + injection + html.slice(insertPos);
61
+ }
62
+ // No head at all, prepend to document
63
+ return injection + html;
64
+ }
65
+ return html.slice(0, headCloseIndex) + injection + html.slice(headCloseIndex);
66
+ }
@@ -0,0 +1,2 @@
1
+ export declare function injectReloadScript(html: string): string;
2
+ //# sourceMappingURL=injectReload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectReload.d.ts","sourceRoot":"","sources":["../../src/proxy/injectReload.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBvD"}
@@ -0,0 +1,14 @@
1
+ export function injectReloadScript(html) {
2
+ const lower = html.toLowerCase();
3
+ const bodyCloseIndex = lower.lastIndexOf('</body>');
4
+ const reloadScript = '<script>(function(){' +
5
+ 'var es = new EventSource("/__embeddables_reload");' +
6
+ 'es.addEventListener("reload", function() { window.location.reload(); });' +
7
+ '})();</script>';
8
+ const injection = `\n${reloadScript}\n`;
9
+ if (bodyCloseIndex === -1) {
10
+ // No </body> tag found; append at the end
11
+ return html + injection;
12
+ }
13
+ return html.slice(0, bodyCloseIndex) + injection + html.slice(bodyCloseIndex);
14
+ }
@@ -0,0 +1,5 @@
1
+ export declare function injectWorkbenchHtml(html: string, opts: {
2
+ embeddableId: string;
3
+ workbenchOrigin?: string;
4
+ }): string;
5
+ //# sourceMappingURL=injectWorkbench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectWorkbench.d.ts","sourceRoot":"","sources":["../../src/proxy/injectWorkbench.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,MAAM,CA8BR"}
@@ -0,0 +1,22 @@
1
+ export function injectWorkbenchHtml(html, opts) {
2
+ const lower = html.toLowerCase();
3
+ const bodyCloseIndex = lower.lastIndexOf('</body>');
4
+ const isLocalDev = !opts.workbenchOrigin;
5
+ const container = '<div id="__embeddables_workbench_root"></div>';
6
+ const configScript = `<script>(function(){` +
7
+ 'window.__EMBEDDABLES_WORKBENCH = window.__EMBEDDABLES_WORKBENCH || {};' +
8
+ `window.__EMBEDDABLES_WORKBENCH.embeddableId = ${JSON.stringify(opts.embeddableId)};` +
9
+ `window.__EMBEDDABLES_WORKBENCH.localDev = ${JSON.stringify(isLocalDev)};` +
10
+ '})();</script>';
11
+ // When a remote origin is provided, load from the CDN; otherwise use the local proxy path.
12
+ const jsUrl = opts.workbenchOrigin
13
+ ? `${opts.workbenchOrigin}/workbench.js`
14
+ : '/__embeddables_workbench.js';
15
+ const scriptTag = `<script src="${jsUrl}" onload="if(typeof window.__embeddables_bootstrap_workbench==='function'){window.__embeddables_bootstrap_workbench({embeddableId:window.__EMBEDDABLES_WORKBENCH.embeddableId,localDev:window.__EMBEDDABLES_WORKBENCH.localDev});}"></script>`;
16
+ const injection = `\n${container}\n${configScript}\n${scriptTag}\n`;
17
+ if (bodyCloseIndex === -1) {
18
+ // No </body> tag found; append at the end
19
+ return html + injection;
20
+ }
21
+ return html.slice(0, bodyCloseIndex) + injection + html.slice(bodyCloseIndex);
22
+ }
@@ -0,0 +1,11 @@
1
+ export declare function startProxyServer(opts: {
2
+ port: number;
3
+ engineOrigin: string;
4
+ overrideRoute: string;
5
+ generatedJsonPath: string;
6
+ embeddableId: string;
7
+ watchWorkbench?: boolean;
8
+ }): Promise<{
9
+ broadcastReload: () => void;
10
+ }>;
11
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAoCA,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;;GA+TA"}
@@ -0,0 +1,304 @@
1
+ import express from 'express';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createProxyMiddleware } from 'http-proxy-middleware';
6
+ import * as esbuild from 'esbuild';
7
+ import chokidar from 'chokidar';
8
+ import postcss from 'postcss';
9
+ import tailwindcss from '@tailwindcss/postcss';
10
+ import autoprefixer from 'autoprefixer';
11
+ import { attachSse } from './sse.js';
12
+ import { injectWorkbenchHtml } from './injectWorkbench.js';
13
+ import { injectReloadScript } from './injectReload.js';
14
+ import { injectApiInterceptor } from './injectApiInterceptor.js';
15
+ import { WORKBENCH_CDN_ORIGIN } from '../constants.js';
16
+ import * as stdout from '../stdout.js';
17
+ // Resolve the CLI package root so workbench paths work regardless of cwd.
18
+ // This file lives at src/proxy/server.ts (or dist/proxy/server.js when compiled),
19
+ // so the package root is two directories up.
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const CLI_ROOT = path.resolve(__dirname, '..', '..');
22
+ /**
23
+ * Detect whether the CLI is running from a local development checkout
24
+ * (e.g. via `npm link` or `tsx`) rather than a regular `npm install -g`.
25
+ *
26
+ * Heuristic: the `src/workbench/` source directory only exists in the repo
27
+ * checkout — it is excluded from the published package (`files` in package.json
28
+ * only ships `bin/` and `dist/`).
29
+ */
30
+ function isLocalDev() {
31
+ const workbenchSrcDir = path.join(CLI_ROOT, 'src', 'workbench', 'index.tsx');
32
+ return fs.existsSync(workbenchSrcDir);
33
+ }
34
+ export async function startProxyServer(opts) {
35
+ const app = express();
36
+ const localDev = isLocalDev();
37
+ // ---------- Local-dev mode: build workbench from source ----------
38
+ // Resolve workbench paths relative to the CLI package root, not process.cwd()
39
+ const workbenchEntry = path.join(CLI_ROOT, 'src', 'workbench', 'index.tsx');
40
+ const workbenchDir = path.join(CLI_ROOT, 'src', 'workbench');
41
+ const workbenchCssEntry = path.join(CLI_ROOT, 'src', 'workbench', 'workbench.css');
42
+ let workbenchBundle = null;
43
+ let workbenchCss = null;
44
+ // The origin used when injecting the workbench <script>/<link> tags.
45
+ // undefined → local proxy paths (/__embeddables_workbench.{js,css})
46
+ // string → remote CDN origin (e.g. https://embeddables-workbench.heysavvy.workers.dev)
47
+ const workbenchOrigin = localDev ? undefined : WORKBENCH_CDN_ORIGIN;
48
+ if (localDev) {
49
+ stdout.print('[Workbench] Local dev mode — building workbench from source');
50
+ }
51
+ else {
52
+ stdout.print(`[Workbench] Installed mode — loading workbench from CDN (${WORKBENCH_CDN_ORIGIN})`);
53
+ }
54
+ const buildWorkbench = async () => {
55
+ try {
56
+ const result = await esbuild.build({
57
+ entryPoints: [workbenchEntry],
58
+ bundle: true,
59
+ format: 'iife',
60
+ globalName: '__EmbeddablesWorkbenchBundle',
61
+ platform: 'browser',
62
+ write: false,
63
+ sourcemap: 'inline',
64
+ });
65
+ const outputFile = result.outputFiles?.[0];
66
+ if (outputFile) {
67
+ workbenchBundle = outputFile.contents;
68
+ }
69
+ else {
70
+ workbenchBundle = null;
71
+ }
72
+ const rawCss = fs.readFileSync(workbenchCssEntry, 'utf8');
73
+ const cssResult = await postcss([tailwindcss(), autoprefixer]).process(rawCss, {
74
+ from: workbenchCssEntry,
75
+ });
76
+ workbenchCss = cssResult.css;
77
+ if (workbenchBundle) {
78
+ stdout.print('[Workbench] Bundle built successfully');
79
+ return true;
80
+ }
81
+ stdout.warn('[Workbench] Bundle build completed but no output file found');
82
+ return false;
83
+ }
84
+ catch (err) {
85
+ stdout.warn(`[Workbench] Failed to build Workbench bundle. Workbench UI will be unavailable. ${err instanceof Error ? err.message : String(err)}`);
86
+ workbenchCss = null;
87
+ return false;
88
+ }
89
+ };
90
+ // Only build from source in local-dev mode
91
+ if (localDev) {
92
+ await buildWorkbench();
93
+ }
94
+ // Add JSON body parsing middleware
95
+ app.use(express.json());
96
+ app.use(express.urlencoded({ extended: true }));
97
+ const { broadcastReload } = attachSse(app);
98
+ // Watch workbench files if enabled (only meaningful in local-dev mode)
99
+ if (localDev && opts.watchWorkbench) {
100
+ const workbenchGlob = path.join(workbenchDir, '**/*');
101
+ const watcher = chokidar.watch(workbenchGlob, {
102
+ ignoreInitial: true,
103
+ });
104
+ let rebuildTimeout;
105
+ watcher.on('all', async () => {
106
+ clearTimeout(rebuildTimeout);
107
+ rebuildTimeout = setTimeout(async () => {
108
+ stdout.print('[Workbench] Files changed, rebuilding bundle...');
109
+ const success = await buildWorkbench();
110
+ if (success) {
111
+ broadcastReload();
112
+ stdout.print('[Workbench] Bundle rebuilt and reload broadcast');
113
+ }
114
+ }, 100);
115
+ });
116
+ stdout.print(`[Workbench] Watching ${workbenchGlob} for changes`);
117
+ }
118
+ // Serve the Workbench bundle as a static asset (local-dev mode only)
119
+ if (localDev) {
120
+ app.get('/__embeddables_workbench.js', (_req, res) => {
121
+ if (!workbenchBundle) {
122
+ res
123
+ .status(503)
124
+ .type('text/plain')
125
+ .send('Workbench bundle is not available. Check dev server logs.');
126
+ return;
127
+ }
128
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
129
+ res.setHeader('Cache-Control', 'no-store');
130
+ res.send(Buffer.from(workbenchBundle));
131
+ });
132
+ app.get('/__embeddables_workbench.css', (_req, res) => {
133
+ if (!workbenchCss) {
134
+ res
135
+ .status(503)
136
+ .type('text/plain')
137
+ .send('Workbench CSS is not available. Check dev server logs.');
138
+ return;
139
+ }
140
+ res.setHeader('Content-Type', 'text/css; charset=utf-8');
141
+ res.setHeader('Cache-Control', 'no-store');
142
+ res.send(workbenchCss);
143
+ });
144
+ }
145
+ // Intercept GET requests to overrideRoute: convert to POST and add generated JSON to body
146
+ app.get(opts.overrideRoute, async (req, res) => {
147
+ stdout.print(`[/init] Intercepted GET ${opts.overrideRoute}, query: ${JSON.stringify(req.query)}`);
148
+ try {
149
+ // Load the generated JSON
150
+ const raw = fs.readFileSync(opts.generatedJsonPath, 'utf8');
151
+ const generatedJson = JSON.parse(raw);
152
+ generatedJson.id = opts.embeddableId;
153
+ stdout.print(`[/init] Loaded local JSON, pages: ${generatedJson.pages?.length ?? 0}`);
154
+ // When the flow has content_sources (CMS), the remote engine needs flow metadata
155
+ // (project_id) to load them. Without an engine that supports dev + CMS, the request
156
+ // fails. So we omit content_sources so the page loads; CMS-backed content will be
157
+ // empty in dev.
158
+ const hasContentSources = Array.isArray(generatedJson.content_sources) && generatedJson.content_sources.length > 0;
159
+ const flowToSend = hasContentSources
160
+ ? (() => {
161
+ const { content_sources: _, ...rest } = generatedJson;
162
+ return rest;
163
+ })()
164
+ : generatedJson;
165
+ if (hasContentSources) {
166
+ stdout.print(`[/init] Omitting content_sources so dev loads (CMS content will be empty in preview).`);
167
+ }
168
+ // Create POST request body with the JSON
169
+ const postBody = {
170
+ json: {
171
+ flow: flowToSend,
172
+ flowId: generatedJson.id,
173
+ groupId: 'group_test',
174
+ },
175
+ };
176
+ // Build the target URL with query parameters
177
+ const queryString = new URLSearchParams(req.query).toString();
178
+ const targetUrl = `${opts.engineOrigin}/init${queryString ? `?${queryString}` : ''}`;
179
+ stdout.print(`[/init] Forwarding to engine: POST ${targetUrl}`);
180
+ // Forward headers from original request (excluding problematic ones)
181
+ const headers = {};
182
+ for (const [key, value] of Object.entries(req.headers)) {
183
+ if (!['host', 'connection', 'content-length'].includes(key.toLowerCase())) {
184
+ if (typeof value === 'string') {
185
+ headers[key] = value;
186
+ }
187
+ else if (Array.isArray(value) && value.length > 0) {
188
+ headers[key] = value[0];
189
+ }
190
+ }
191
+ }
192
+ headers['Content-Type'] = 'application/json';
193
+ // Forward the modified request to the engine as POST
194
+ const engineResponse = await fetch(targetUrl, {
195
+ method: 'POST',
196
+ headers,
197
+ body: JSON.stringify(postBody),
198
+ });
199
+ // Parse and forward the response
200
+ const responseBody = await engineResponse.json();
201
+ stdout.print(`[/init] Engine response: status=${engineResponse.status}, hasFlow=${!!responseBody?.flow}, flowPages=${responseBody?.flow?.pages?.length ?? 'N/A'}`);
202
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
203
+ res.setHeader('Cache-Control', 'no-store');
204
+ return res.status(engineResponse.status).send(JSON.stringify(responseBody));
205
+ }
206
+ catch (err) {
207
+ stdout.error(`Failed to modify /init request: ${err instanceof Error ? err.message : String(err)}`);
208
+ if (!res.headersSent) {
209
+ res.status(500).json({
210
+ error: 'Failed to modify /init request',
211
+ details: err?.message ?? String(err),
212
+ path: opts.generatedJsonPath,
213
+ });
214
+ }
215
+ }
216
+ });
217
+ // For GET / from localhost: fetch HTML from engine, inject Workbench, then send.
218
+ // This avoids selfHandleResponse/responseInterceptor which were causing the proxy to hang.
219
+ app.get('/', async (req, res, next) => {
220
+ const host = (req.headers.host ?? '').toString();
221
+ const isLocalhost = host.startsWith('localhost') || host.startsWith('127.0.0.1');
222
+ stdout.print(`[Workbench] GET / handler: host=${host}, isLocalhost=${isLocalhost}, url=${req.url}`);
223
+ if (!isLocalhost) {
224
+ stdout.print('[Workbench] Not localhost, skipping injection');
225
+ return next();
226
+ }
227
+ const targetUrl = `${opts.engineOrigin}${req.url}`;
228
+ stdout.print(`[Workbench] Fetching from engine: ${targetUrl}`);
229
+ try {
230
+ const headers = {};
231
+ for (const [key, value] of Object.entries(req.headers)) {
232
+ if (!['host', 'connection'].includes(key.toLowerCase())) {
233
+ if (typeof value === 'string')
234
+ headers[key] = value;
235
+ else if (Array.isArray(value) && value[0])
236
+ headers[key] = value[0];
237
+ }
238
+ }
239
+ const engineRes = await fetch(targetUrl, { method: 'GET', headers });
240
+ const contentType = engineRes.headers.get('content-type') ?? '';
241
+ stdout.print(`[Workbench] Engine response: status=${engineRes.status}, content-type=${contentType}`);
242
+ if (!contentType.includes('text/html')) {
243
+ stdout.print('[Workbench] Not HTML, proxying normally');
244
+ return next();
245
+ }
246
+ const html = await engineRes.text();
247
+ stdout.print(`[Workbench] Got HTML (${html.length} chars), injecting Workbench...`);
248
+ let modifiedHtml = html;
249
+ // If proxying to a remote engine, inject API interceptor to rewrite absolute URLs
250
+ const isRemoteEngine = !opts.engineOrigin.includes('localhost') && !opts.engineOrigin.includes('127.0.0.1');
251
+ if (isRemoteEngine) {
252
+ modifiedHtml = injectApiInterceptor(modifiedHtml, opts.engineOrigin);
253
+ stdout.print(`[Workbench] Injected API interceptor for ${opts.engineOrigin}`);
254
+ }
255
+ modifiedHtml = injectReloadScript(modifiedHtml);
256
+ const includeWorkbench = req.query?.workbench !== 'false';
257
+ if (includeWorkbench) {
258
+ modifiedHtml = injectWorkbenchHtml(modifiedHtml, {
259
+ embeddableId: opts.embeddableId,
260
+ workbenchOrigin,
261
+ });
262
+ }
263
+ else {
264
+ console.log('[Workbench] Skipping workbench (workbench=false in URL)');
265
+ }
266
+ stdout.print(`[Workbench] Injection complete (${modifiedHtml.length} chars), sending response`);
267
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
268
+ res.setHeader('Cache-Control', engineRes.headers.get('cache-control') ?? 'no-store');
269
+ res.status(engineRes.status).send(modifiedHtml);
270
+ }
271
+ catch (err) {
272
+ stdout.error(`[Workbench] Error fetching/injecting HTML: ${err instanceof Error ? err.message : String(err)}`);
273
+ next();
274
+ }
275
+ });
276
+ // Proxy everything else to the engine (no response interception)
277
+ app.use('/', createProxyMiddleware({
278
+ target: opts.engineOrigin,
279
+ changeOrigin: true,
280
+ ws: true,
281
+ on: {
282
+ proxyReq: (proxyReq, req) => {
283
+ stdout.print(`[Proxy] ${req.method} ${req.url} → ${opts.engineOrigin}${req.url}`);
284
+ },
285
+ error: (err, req, res) => {
286
+ // ECONNRESET/ECONNABORTED often happen when the client navigates away or the
287
+ // upstream closes the connection; treat as non-fatal and log at debug level.
288
+ const code = err?.code ?? err?.errno;
289
+ const isConnectionClosed = code === 'ECONNRESET' || code === 'ECONNABORTED' || code === -54;
290
+ if (isConnectionClosed) {
291
+ stdout.print(`[Proxy] Connection closed (client or upstream): ${err.message}`);
292
+ }
293
+ else {
294
+ stdout.error(`[Proxy] Error proxying request: ${err instanceof Error ? err.message : String(err)}`);
295
+ }
296
+ if (!res.headersSent) {
297
+ res.status(500).send('Proxy error');
298
+ }
299
+ },
300
+ },
301
+ }));
302
+ app.listen(opts.port);
303
+ return { broadcastReload };
304
+ }