@see-ms/converter 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -4,11 +4,13 @@
4
4
  import { Command } from "commander";
5
5
  import pc4 from "picocolors";
6
6
  import * as readline2 from "readline";
7
+ import fs12 from "fs-extra";
8
+ import path14 from "path";
7
9
 
8
10
  // src/converter.ts
9
11
  import pc3 from "picocolors";
10
- import path11 from "path";
11
- import fs9 from "fs-extra";
12
+ import path12 from "path";
13
+ import fs10 from "fs-extra";
12
14
 
13
15
  // src/filesystem.ts
14
16
  import fs from "fs-extra";
@@ -326,39 +328,423 @@ ${styles}`, "utf-8");
326
328
  ${styles}`, "utf-8");
327
329
  }
328
330
  }
331
+ async function addStrapiUrlToConfig(outputDir, strapiUrl = "http://localhost:1337") {
332
+ const configPath = path3.join(outputDir, "nuxt.config.ts");
333
+ const configExists = await fs2.pathExists(configPath);
334
+ if (!configExists) {
335
+ throw new Error("nuxt.config.ts not found in output directory");
336
+ }
337
+ let config = await fs2.readFile(configPath, "utf-8");
338
+ if (config.includes("runtimeConfig:")) {
339
+ if (config.includes("public:")) {
340
+ config = config.replace(
341
+ /public:\s*\{/,
342
+ `public: {
343
+ strapiUrl: process.env.STRAPI_URL || '${strapiUrl}',`
344
+ );
345
+ } else {
346
+ config = config.replace(
347
+ /runtimeConfig:\s*\{/,
348
+ `runtimeConfig: {
349
+ public: {
350
+ strapiUrl: process.env.STRAPI_URL || '${strapiUrl}'
351
+ },`
352
+ );
353
+ }
354
+ } else {
355
+ config = config.replace(
356
+ /export default defineNuxtConfig\(\{/,
357
+ `export default defineNuxtConfig({
358
+ runtimeConfig: {
359
+ public: {
360
+ strapiUrl: process.env.STRAPI_URL || '${strapiUrl}'
361
+ }
362
+ },`
363
+ );
364
+ }
365
+ await fs2.writeFile(configPath, config, "utf-8");
366
+ }
329
367
 
330
368
  // src/editor-integration.ts
331
369
  import fs3 from "fs-extra";
332
370
  import path4 from "path";
371
+ async function createEditorContentComposable(outputDir) {
372
+ const composablesDir = path4.join(outputDir, "composables");
373
+ await fs3.ensureDir(composablesDir);
374
+ const composableContent = `/**
375
+ * Global state for editor content in preview mode
376
+ * This allows the editor overlay to update content reactively
377
+ */
378
+
379
+ // Global reactive state
380
+ const editorState = reactive<{
381
+ isPreviewMode: boolean;
382
+ currentPage: string | null;
383
+ content: Record<string, Record<string, any>>; // page -> field -> value
384
+ hasChanges: Record<string, boolean>; // page -> hasChanges
385
+ }>({
386
+ isPreviewMode: false,
387
+ currentPage: null,
388
+ content: {},
389
+ hasChanges: {},
390
+ });
391
+
392
+ export function useEditorContent(pageName?: string) {
393
+ const route = useRoute();
394
+
395
+ // Check if we're in preview mode
396
+ const isPreviewMode = computed(() => route.query.preview === 'true');
397
+
398
+ // Update global state
399
+ if (import.meta.client) {
400
+ editorState.isPreviewMode = isPreviewMode.value;
401
+ if (pageName) {
402
+ editorState.currentPage = pageName;
403
+ }
404
+ }
405
+
406
+ // Get content for specific page
407
+ const getPageContent = (page: string) => {
408
+ return editorState.content[page] || {};
409
+ };
410
+
411
+ // Update a field's value
412
+ const updateField = (page: string, fieldName: string, value: any) => {
413
+ if (!editorState.content[page]) {
414
+ editorState.content[page] = {};
415
+ }
416
+ editorState.content[page][fieldName] = value;
417
+ editorState.hasChanges[page] = true;
418
+ };
419
+
420
+ // Clear all changes for a page
421
+ const clearPageChanges = (page: string) => {
422
+ delete editorState.content[page];
423
+ editorState.hasChanges[page] = false;
424
+ };
425
+
426
+ // Initialize page content from Strapi data
427
+ const initializePageContent = (page: string, content: Record<string, any>) => {
428
+ if (!editorState.content[page]) {
429
+ editorState.content[page] = { ...content };
430
+ }
431
+ };
432
+
433
+ // Get content for current page (reactive)
434
+ const content = computed(() => {
435
+ const page = pageName || editorState.currentPage;
436
+ if (!page) return {};
437
+ return editorState.content[page] || {};
438
+ });
439
+
440
+ // Check if page has unsaved changes
441
+ const hasChanges = computed(() => {
442
+ const page = pageName || editorState.currentPage;
443
+ if (!page) return false;
444
+ return editorState.hasChanges[page] || false;
445
+ });
446
+
447
+ // Get all pages with changes
448
+ const pagesWithChanges = computed(() => {
449
+ return Object.keys(editorState.hasChanges).filter(
450
+ (page) => editorState.hasChanges[page]
451
+ );
452
+ });
453
+
454
+ // Expose state for window object (for editor overlay to access)
455
+ if (import.meta.client) {
456
+ (window as any).__editorState = editorState;
457
+ }
458
+
459
+ return {
460
+ isPreviewMode,
461
+ content,
462
+ hasChanges,
463
+ pagesWithChanges,
464
+ getPageContent,
465
+ updateField,
466
+ clearPageChanges,
467
+ initializePageContent,
468
+ };
469
+ }
470
+ `;
471
+ const composablePath = path4.join(composablesDir, "useEditorContent.ts");
472
+ await fs3.writeFile(composablePath, composableContent, "utf-8");
473
+ }
474
+ async function createStrapiContentComposable(outputDir) {
475
+ const composablesDir = path4.join(outputDir, "composables");
476
+ await fs3.ensureDir(composablesDir);
477
+ const composableContent = `/**
478
+ * Composable to fetch content from Strapi based on CMS manifest
479
+ * Integrates with editor state for preview mode
480
+ */
481
+
482
+ export function useStrapiContent(pageName: string) {
483
+ const config = useRuntimeConfig();
484
+ const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
485
+ const editorContent = useEditorContent(pageName);
486
+
487
+ // Helper to transform Strapi image objects to URL strings
488
+ const transformStrapiImages = (data: any, baseUrl: string): any => {
489
+ if (!data || typeof data !== 'object') return data;
490
+
491
+ const transformed: any = {};
492
+
493
+ for (const [key, value] of Object.entries(data)) {
494
+ if (value && typeof value === 'object') {
495
+ // Check if it's a Strapi media object
496
+ if ('url' in value && ('mime' in value || 'formats' in value)) {
497
+ // It's an image - extract the URL
498
+ transformed[key] = value.url.startsWith('http')
499
+ ? value.url
500
+ : \`\${baseUrl}\${value.url}\`;
501
+ } else if (Array.isArray(value)) {
502
+ // Handle arrays (collections of images)
503
+ transformed[key] = value.map((item) =>
504
+ item && typeof item === 'object' && 'url' in item
505
+ ? item.url.startsWith('http')
506
+ ? item.url
507
+ : \`\${baseUrl}\${item.url}\`
508
+ : item
509
+ );
510
+ } else {
511
+ // Recursively transform nested objects
512
+ transformed[key] = transformStrapiImages(value, baseUrl);
513
+ }
514
+ } else {
515
+ transformed[key] = value;
516
+ }
517
+ }
518
+
519
+ return transformed;
520
+ };
521
+
522
+ // Fetch content from Strapi with populated media fields
523
+ const { data: strapiData } = useFetch<any>(
524
+ \`\${strapiUrl}/api/\${pageName}\`,
525
+ {
526
+ key: \`strapi-\${pageName}\`,
527
+ query: {
528
+ populate: '*', // Strapi v5: Populate all fields including images
529
+ },
530
+ transform: (response) => {
531
+ // Strapi v5 returns data in response.data
532
+ const data = response?.data || response;
533
+
534
+ // Transform image fields from Strapi objects to URL strings
535
+ if (data && typeof data === 'object') {
536
+ return transformStrapiImages(data, strapiUrl);
537
+ }
538
+
539
+ return data;
540
+ },
541
+ }
542
+ );
543
+
544
+ // Initialize editor state with Strapi data when fetched
545
+ // This runs in both normal AND preview mode to ensure initial content is available
546
+ watch(
547
+ strapiData,
548
+ (newData) => {
549
+ if (newData) {
550
+ // Always initialize from Strapi on first load
551
+ // Drafts will override this when they load in the editor
552
+ editorContent.initializePageContent(pageName, newData);
553
+ }
554
+ },
555
+ { immediate: true }
556
+ );
557
+
558
+ // In preview mode: use editor state
559
+ // In normal mode: use Strapi data (and sync to editor state)
560
+ const content = computed(() => {
561
+ if (editorContent.isPreviewMode.value) {
562
+ // Use editor state in preview mode
563
+ return editorContent.getPageContent(pageName);
564
+ } else {
565
+ // Use Strapi data in normal mode
566
+ return strapiData.value || editorContent.getPageContent(pageName);
567
+ }
568
+ });
569
+
570
+ return {
571
+ content,
572
+ };
573
+ }
574
+ `;
575
+ const composablePath = path4.join(composablesDir, "useStrapiContent.ts");
576
+ await fs3.writeFile(composablePath, composableContent, "utf-8");
577
+ }
333
578
  async function createEditorPlugin(outputDir) {
334
579
  const pluginsDir = path4.join(outputDir, "plugins");
335
580
  await fs3.ensureDir(pluginsDir);
336
581
  const pluginContent = `/**
337
582
  * CMS Editor Overlay Plugin
338
- * Loads the inline editor when ?preview=true
583
+ * Loads the inline editor when ?preview=true with full state management
584
+ */
585
+
586
+ /**
587
+ * Disable Lenis smooth scroll to allow native scrolling in edit mode
339
588
  */
589
+ function disableLenisInEditMode() {
590
+ try {
591
+ // Check for Lenis in common locations
592
+ const lenisInstances = [
593
+ (window as any).lenis,
594
+ (window as any).__lenis,
595
+ document.querySelector('.lenis'),
596
+ ];
597
+
598
+ for (const lenis of lenisInstances) {
599
+ if (lenis && typeof lenis.destroy === 'function') {
600
+ lenis.destroy();
601
+ return;
602
+ }
603
+ }
340
604
 
341
- export default defineNuxtPlugin(() => {
605
+ // Check for Vue Lenis component instances
606
+ const lenisElements = document.querySelectorAll('[data-lenis], .lenis');
607
+ if (lenisElements.length > 0) {
608
+ // Try to find and destroy via data attributes or component instances
609
+ lenisElements.forEach((el: any) => {
610
+ if (el.__lenis && typeof el.__lenis.destroy === 'function') {
611
+ el.__lenis.destroy();
612
+ }
613
+ });
614
+ }
615
+ } catch (error) {
616
+ // Silently fail - Lenis may not be present
617
+ }
618
+ }
619
+
620
+ export default defineNuxtPlugin(async (nuxtApp) => {
342
621
  // Only run on client side
343
622
  if (process.server) return;
344
-
345
- // Check for preview mode
346
- const params = new URLSearchParams(window.location.search);
347
-
348
- if (params.get('preview') === 'true') {
349
- // Dynamically import the editor
350
- import('@see-ms/editor-overlay').then(({ initEditor, createToolbar }) => {
351
- const editor = initEditor({
352
- apiEndpoint: '/api/cms/save',
353
- richText: true,
354
- });
355
-
356
- editor.enable();
357
-
358
- const toolbar = createToolbar(editor);
359
- document.body.appendChild(toolbar);
360
- });
623
+
624
+ // Import editor overlay modules
625
+ const {
626
+ initEditor,
627
+ createAuthManager,
628
+ showLoginModal,
629
+ createDraftStorage,
630
+ createURLStateManager,
631
+ createManifestLoader,
632
+ createNavigationGuard,
633
+ getCurrentPageFromRoute,
634
+ } = await import('@see-ms/editor-overlay');
635
+
636
+ // Initialize URL state manager
637
+ const urlState = createURLStateManager();
638
+ const state = urlState.getState();
639
+
640
+ // Only proceed if in preview mode
641
+ if (!state.preview) return;
642
+
643
+ // Get Strapi URL from runtime config
644
+ const config = useRuntimeConfig();
645
+ const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
646
+
647
+ // Initialize components
648
+ const authManager = createAuthManager({
649
+ strapiUrl,
650
+ storageKey: 'cms_editor_token',
651
+ });
652
+
653
+ const draftStorage = createDraftStorage();
654
+ const manifestLoader = createManifestLoader();
655
+
656
+ // Load manifest
657
+ try {
658
+ await manifestLoader.load();
659
+ } catch (error) {
660
+ console.error('[CMS Editor] Failed to load manifest:', error);
661
+ return;
361
662
  }
663
+
664
+ // Get current page from route
665
+ let currentPage = getCurrentPageFromRoute();
666
+ if (!currentPage) {
667
+ currentPage = manifestLoader.getPageFromRoute(window.location.pathname);
668
+ }
669
+
670
+ if (!currentPage) {
671
+ console.error('[CMS Editor] Could not determine current page');
672
+ return;
673
+ }
674
+
675
+ // URL state only manages preview mode (page is derived from route)
676
+ urlState.setState({ preview: true });
677
+
678
+ // Auth flow
679
+ let token = authManager.getToken();
680
+ if (!token || !await authManager.verifyToken(token)) {
681
+ try {
682
+ token = await showLoginModal(authManager);
683
+ } catch (error) {
684
+ // Login cancelled - exit preview mode
685
+ console.log('[CMS Editor] Login cancelled');
686
+ urlState.clearPreviewMode();
687
+ return;
688
+ }
689
+ }
690
+
691
+ // Disable Lenis smooth scroll in edit mode (allows native scrolling)
692
+ disableLenisInEditMode();
693
+
694
+ // Initialize navigation guard
695
+ const navigationGuard = createNavigationGuard({
696
+ showToast: true,
697
+ toastMessage: 'Navigation disabled in edit mode',
698
+ });
699
+ navigationGuard.enable();
700
+
701
+ // Initialize editor with full context
702
+ const editor = initEditor({
703
+ apiEndpoint: '/api/cms/save',
704
+ authToken: token,
705
+ richText: true,
706
+ manifestLoader,
707
+ draftStorage,
708
+ currentPage,
709
+ });
710
+
711
+ // Enable editor (will auto-load drafts)
712
+ await editor.enable();
713
+
714
+ // Create toolbar with navigation
715
+ const { createToolbar } = await import('@see-ms/editor-overlay');
716
+ const toolbar = await createToolbar(editor, {
717
+ draftStorage,
718
+ urlState,
719
+ navigationGuard,
720
+ manifestLoader,
721
+ currentPage,
722
+ });
723
+ document.body.appendChild(toolbar);
724
+
725
+ // Watch for route changes
726
+ const router = useRouter();
727
+ router.afterEach(async (to) => {
728
+ const newPage = manifestLoader.getPageFromRoute(to.path);
729
+ if (newPage && newPage !== currentPage) {
730
+ currentPage = newPage;
731
+ await editor.setPage(newPage);
732
+
733
+ // Update toolbar if it has an update method
734
+ if (typeof (toolbar as any).updateCurrentPage === 'function') {
735
+ await (toolbar as any).updateCurrentPage(newPage);
736
+ }
737
+ }
738
+ });
739
+
740
+ // Cleanup on navigation away from preview mode
741
+ nuxtApp.hook('page:finish', () => {
742
+ const currentState = urlState.getState();
743
+ if (!currentState.preview) {
744
+ navigationGuard.disable();
745
+ editor.destroy();
746
+ }
747
+ });
362
748
  });
363
749
  `;
364
750
  const pluginPath = path4.join(pluginsDir, "cms-editor.client.ts");
@@ -380,29 +766,595 @@ async function createSaveEndpoint(outputDir) {
380
766
  await fs3.ensureDir(serverDir);
381
767
  const endpointContent = `/**
382
768
  * API endpoint for saving CMS changes
769
+ * Handles draft and final saving to Strapi
383
770
  */
384
771
 
772
+ import fs from 'fs';
773
+ import path from 'path';
774
+
385
775
  export default defineEventHandler(async (event) => {
386
- const body = await readBody(event);
387
-
388
- // TODO: Implement actual saving to Strapi
389
- // For now, just log the changes
390
- console.log('CMS changes:', body);
391
-
392
- // In production, this would:
393
- // 1. Validate the changes
394
- // 2. Send to Strapi API
395
- // 3. Return success/error
396
-
397
- return {
398
- success: true,
399
- message: 'Changes saved (demo mode)',
400
- };
776
+ // Get Strapi URL from runtime config
777
+ const config = useRuntimeConfig();
778
+ const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
779
+
780
+ // Extract Authorization header
781
+ const authHeader = getHeader(event, 'authorization');
782
+
783
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
784
+ throw createError({
785
+ statusCode: 401,
786
+ statusMessage: 'Unauthorized: Missing or invalid authorization header',
787
+ });
788
+ }
789
+
790
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
791
+
792
+ // Verify token with Strapi and determine if it's an admin or user token
793
+ let userResponse: any;
794
+ let isAdminToken = false;
795
+
796
+ try {
797
+ // Try admin token verification first
798
+ try {
799
+ userResponse = await $fetch(\`\${strapiUrl}/admin/users/me\`, {
800
+ headers: {
801
+ Authorization: \`Bearer \${token}\`,
802
+ },
803
+ });
804
+ isAdminToken = true;
805
+ } catch (adminError) {
806
+ // Fallback to regular user token verification
807
+ userResponse = await $fetch(\`\${strapiUrl}/api/users/me\`, {
808
+ headers: {
809
+ Authorization: \`Bearer \${token}\`,
810
+ },
811
+ });
812
+ isAdminToken = false;
813
+ }
814
+
815
+ // Get the request body
816
+ const body = await readBody(event);
817
+ const { page, fields, isDraft = true } = body;
818
+
819
+ if (!page || !fields) {
820
+ throw createError({
821
+ statusCode: 400,
822
+ statusMessage: 'Bad Request: Missing page or fields',
823
+ });
824
+ }
825
+
826
+ // Load manifest to understand field mappings
827
+ const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
828
+ let manifest;
829
+ try {
830
+ const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
831
+ manifest = JSON.parse(manifestContent);
832
+ } catch (error) {
833
+ console.error('Failed to load manifest:', error);
834
+ throw createError({
835
+ statusCode: 500,
836
+ statusMessage: 'Failed to load CMS manifest',
837
+ });
838
+ }
839
+
840
+ // Get page configuration from manifest
841
+ const pageConfig = manifest.pages[page];
842
+ if (!pageConfig) {
843
+ throw createError({
844
+ statusCode: 404,
845
+ statusMessage: \`Page "\${page}" not found in manifest\`,
846
+ });
847
+ }
848
+
849
+ // Transform fields to Strapi format
850
+ const strapiData: Record<string, any> = {};
851
+ for (const [fieldName, value] of Object.entries(fields)) {
852
+ const fieldConfig = pageConfig.fields[fieldName];
853
+ if (!fieldConfig) {
854
+ console.warn(\`Field "\${fieldName}" not found in manifest for page "\${page}"\`);
855
+ continue;
856
+ }
857
+
858
+ // Handle different field types
859
+ if (fieldConfig.type === 'image') {
860
+ // TODO: Handle image uploads - for now just store the value
861
+ strapiData[fieldName] = value;
862
+ } else {
863
+ strapiData[fieldName] = value;
864
+ }
865
+ }
866
+
867
+ // Update Strapi v5 content - use different endpoints for admin vs user tokens
868
+ if (isAdminToken) {
869
+ // Admin tokens use the content-manager API (Strapi v5)
870
+ const contentEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
871
+
872
+ // Step 1: Update the content
873
+ await $fetch(contentEndpoint, {
874
+ method: 'PUT',
875
+ headers: {
876
+ 'Authorization': \`Bearer \${token}\`,
877
+ 'Content-Type': 'application/json',
878
+ },
879
+ body: strapiData,
880
+ });
881
+
882
+ // Step 2: Publish if not a draft (Strapi v5)
883
+ if (!isDraft) {
884
+ const publishEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}/actions/publish\`;
885
+ await $fetch(publishEndpoint, {
886
+ method: 'POST',
887
+ headers: {
888
+ 'Authorization': \`Bearer \${token}\`,
889
+ 'Content-Type': 'application/json',
890
+ },
891
+ body: {},
892
+ });
893
+ }
894
+ } else {
895
+ // User tokens use the regular REST API
896
+ const strapiEndpoint = \`\${strapiUrl}/api/\${page}\`;
897
+
898
+ await $fetch(strapiEndpoint, {
899
+ method: 'PUT',
900
+ headers: {
901
+ 'Authorization': \`Bearer \${token}\`,
902
+ 'Content-Type': 'application/json',
903
+ },
904
+ body: {
905
+ data: strapiData,
906
+ },
907
+ });
908
+
909
+ // Publish if not a draft (Strapi v5)
910
+ if (!isDraft) {
911
+ const publishEndpoint = \`\${strapiUrl}/api/\${page}/publish\`;
912
+ await $fetch(publishEndpoint, {
913
+ method: 'POST',
914
+ headers: {
915
+ 'Authorization': \`Bearer \${token}\`,
916
+ 'Content-Type': 'application/json',
917
+ },
918
+ body: {},
919
+ });
920
+ }
921
+ }
922
+
923
+ console.log(\`[CMS Save] Updated "\${page}" in Strapi (draft: \${isDraft})\`);
924
+
925
+ return {
926
+ success: true,
927
+ message: 'Changes saved successfully',
928
+ page,
929
+ isDraft,
930
+ user: {
931
+ id: userResponse.id,
932
+ username: userResponse.username || userResponse.firstname || 'Unknown',
933
+ },
934
+ };
935
+ } catch (error: any) {
936
+ console.error('[CMS Save] Error:', error);
937
+
938
+ // Token verification failed
939
+ if (error.statusCode === 401 || error.status === 401) {
940
+ throw createError({
941
+ statusCode: 401,
942
+ statusMessage: 'Unauthorized: Invalid or expired token',
943
+ });
944
+ }
945
+
946
+ // Strapi error
947
+ if (error.statusCode || error.status) {
948
+ throw createError({
949
+ statusCode: error.statusCode || error.status,
950
+ statusMessage: error.statusMessage || error.message || 'Failed to save to Strapi',
951
+ });
952
+ }
953
+
954
+ // Generic error
955
+ throw createError({
956
+ statusCode: 500,
957
+ statusMessage: 'Internal server error while saving changes',
958
+ });
959
+ }
401
960
  });
402
961
  `;
403
962
  const endpointPath = path4.join(serverDir, "save.post.ts");
404
963
  await fs3.writeFile(endpointPath, endpointContent, "utf-8");
405
964
  }
965
+ async function createStrapiBootstrap(outputDir) {
966
+ const strapiBootstrapDir = path4.join(outputDir, "strapi-bootstrap");
967
+ await fs3.ensureDir(strapiBootstrapDir);
968
+ const bootstrapContent = `/**
969
+ * Strapi Bootstrap File
970
+ * Auto-enables public read permissions for all single types
971
+ *
972
+ * Place this file in your Strapi project at: src/index.ts
973
+ */
974
+
975
+ export default {
976
+ /**
977
+ * Bootstrap function runs when Strapi starts
978
+ */
979
+ async bootstrap({ strapi }: { strapi: any }) {
980
+ try {
981
+ console.log('[Bootstrap] Configuring public permissions for CMS...');
982
+
983
+ // Get the public role
984
+ const publicRole = await strapi
985
+ .query('plugin::users-permissions.role')
986
+ .findOne({ where: { type: 'public' } });
987
+
988
+ if (!publicRole) {
989
+ console.error('[Bootstrap] Public role not found');
990
+ return;
991
+ }
992
+
993
+ // Get all content types
994
+ const contentTypes = Object.keys(strapi.contentTypes).filter(
995
+ (uid) => uid.startsWith('api::')
996
+ );
997
+
998
+ // Enable find and findOne for each content type
999
+ const permissions = await strapi
1000
+ .query('plugin::users-permissions.permission')
1001
+ .findMany({
1002
+ where: {
1003
+ role: publicRole.id,
1004
+ },
1005
+ });
1006
+
1007
+ let updatedCount = 0;
1008
+
1009
+ for (const contentType of contentTypes) {
1010
+ const [, apiName] = contentType.split('::');
1011
+ const [controllerName] = apiName.split('.');
1012
+
1013
+ // Find or create find permission
1014
+ const findPermission = permissions.find(
1015
+ (p: any) =>
1016
+ p.action === \`api::\${apiName}.find\` ||
1017
+ p.action === 'find' && p.controller === controllerName
1018
+ );
1019
+
1020
+ const findOnePermission = permissions.find(
1021
+ (p: any) =>
1022
+ p.action === \`api::\${apiName}.findOne\` ||
1023
+ p.action === 'findOne' && p.controller === controllerName
1024
+ );
1025
+
1026
+ // Enable find
1027
+ if (findPermission && !findPermission.enabled) {
1028
+ await strapi
1029
+ .query('plugin::users-permissions.permission')
1030
+ .update({
1031
+ where: { id: findPermission.id },
1032
+ data: { enabled: true },
1033
+ });
1034
+ updatedCount++;
1035
+ }
1036
+
1037
+ // Enable findOne
1038
+ if (findOnePermission && !findOnePermission.enabled) {
1039
+ await strapi
1040
+ .query('plugin::users-permissions.permission')
1041
+ .update({
1042
+ where: { id: findOnePermission.id },
1043
+ data: { enabled: true },
1044
+ });
1045
+ updatedCount++;
1046
+ }
1047
+
1048
+ // If permissions don't exist, create them
1049
+ if (!findPermission) {
1050
+ await strapi.query('plugin::users-permissions.permission').create({
1051
+ data: {
1052
+ action: \`api::\${apiName}.find\`,
1053
+ role: publicRole.id,
1054
+ enabled: true,
1055
+ },
1056
+ });
1057
+ updatedCount++;
1058
+ }
1059
+
1060
+ if (!findOnePermission) {
1061
+ await strapi.query('plugin::users-permissions.permission').create({
1062
+ data: {
1063
+ action: \`api::\${apiName}.findOne\`,
1064
+ role: publicRole.id,
1065
+ enabled: true,
1066
+ },
1067
+ });
1068
+ updatedCount++;
1069
+ }
1070
+ }
1071
+
1072
+ console.log(
1073
+ \`[Bootstrap] \u2705 Enabled \${updatedCount} public permissions for \${contentTypes.length} content types\`
1074
+ );
1075
+ } catch (error) {
1076
+ console.error('[Bootstrap] Error enabling public permissions:', error);
1077
+ }
1078
+ },
1079
+ };
1080
+ `;
1081
+ const bootstrapPath = path4.join(strapiBootstrapDir, "index.ts");
1082
+ await fs3.writeFile(bootstrapPath, bootstrapContent, "utf-8");
1083
+ const readmeContent = `# Strapi Bootstrap File
1084
+
1085
+ This file automatically enables public read permissions for all CMS content types when Strapi starts.
1086
+
1087
+ ## Installation
1088
+
1089
+ 1. Copy the \`index.ts\` file to your Strapi project:
1090
+ \`\`\`bash
1091
+ cp strapi-bootstrap/index.ts <your-strapi-project>/src/index.ts
1092
+ \`\`\`
1093
+
1094
+ 2. Restart Strapi:
1095
+ \`\`\`bash
1096
+ cd <your-strapi-project>
1097
+ npm run develop
1098
+ \`\`\`
1099
+
1100
+ 3. Check the console logs - you should see:
1101
+ \`\`\`
1102
+ [Bootstrap] \u2705 Enabled X public permissions for Y content types
1103
+ \`\`\`
1104
+
1105
+ ## What It Does
1106
+
1107
+ - Runs automatically when Strapi starts
1108
+ - Finds the "Public" role
1109
+ - Enables \`find\` and \`findOne\` permissions for all API content types
1110
+ - Allows unauthenticated users to read published content
1111
+ - Fixes 403 Forbidden errors from \`useStrapiContent\`
1112
+
1113
+ ## Manual Alternative
1114
+
1115
+ If you prefer to set permissions manually:
1116
+
1117
+ 1. Open Strapi admin: http://localhost:1337/admin
1118
+ 2. Go to: Settings \u2192 Users & Permissions Plugin \u2192 Roles \u2192 Public
1119
+ 3. For each content type, check:
1120
+ - \u2705 find
1121
+ - \u2705 findOne
1122
+ 4. Click Save
1123
+
1124
+ ## Notes
1125
+
1126
+ - Only enables READ permissions (find, findOne)
1127
+ - Does NOT enable write permissions (create, update, delete)
1128
+ - Only affects the "Public" role (unauthenticated users)
1129
+ - Safe to run multiple times (idempotent)
1130
+ `;
1131
+ const readmePath = path4.join(strapiBootstrapDir, "README.md");
1132
+ await fs3.writeFile(readmePath, readmeContent, "utf-8");
1133
+ console.log(" \u2713 Generated Strapi bootstrap file");
1134
+ }
1135
+ async function createPublishEndpoint(outputDir) {
1136
+ const serverDir = path4.join(outputDir, "server", "api", "cms");
1137
+ await fs3.ensureDir(serverDir);
1138
+ const endpointContent = `/**
1139
+ * API endpoint for batch publishing CMS changes
1140
+ * Publishes all drafts at once
1141
+ */
1142
+
1143
+ import fs from 'fs';
1144
+ import path from 'path';
1145
+
1146
+ export default defineEventHandler(async (event) => {
1147
+ // Get Strapi URL from runtime config
1148
+ const config = useRuntimeConfig();
1149
+ const strapiUrl = config.public.strapiUrl || 'http://localhost:1337';
1150
+
1151
+ // Extract Authorization header
1152
+ const authHeader = getHeader(event, 'authorization');
1153
+
1154
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
1155
+ throw createError({
1156
+ statusCode: 401,
1157
+ statusMessage: 'Unauthorized: Missing or invalid authorization header',
1158
+ });
1159
+ }
1160
+
1161
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
1162
+
1163
+ // Verify token with Strapi and determine if it's an admin or user token
1164
+ let userResponse: any;
1165
+ let isAdminToken = false;
1166
+
1167
+ try {
1168
+ // Try admin token verification first
1169
+ try {
1170
+ userResponse = await $fetch(\`\${strapiUrl}/admin/users/me\`, {
1171
+ headers: {
1172
+ Authorization: \`Bearer \${token}\`,
1173
+ },
1174
+ });
1175
+ isAdminToken = true;
1176
+ } catch (adminError) {
1177
+ // Fallback to regular user token verification
1178
+ userResponse = await $fetch(\`\${strapiUrl}/api/users/me\`, {
1179
+ headers: {
1180
+ Authorization: \`Bearer \${token}\`,
1181
+ },
1182
+ });
1183
+ isAdminToken = false;
1184
+ }
1185
+
1186
+ // Get the request body
1187
+ const body = await readBody(event);
1188
+ const { pages } = body;
1189
+
1190
+ if (!pages || !Array.isArray(pages)) {
1191
+ throw createError({
1192
+ statusCode: 400,
1193
+ statusMessage: 'Bad Request: Missing or invalid pages array',
1194
+ });
1195
+ }
1196
+
1197
+ // Load manifest to understand field mappings
1198
+ const manifestPath = path.join(process.cwd(), 'cms-manifest.json');
1199
+ let manifest;
1200
+ try {
1201
+ const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
1202
+ manifest = JSON.parse(manifestContent);
1203
+ } catch (error) {
1204
+ console.error('Failed to load manifest:', error);
1205
+ throw createError({
1206
+ statusCode: 500,
1207
+ statusMessage: 'Failed to load CMS manifest',
1208
+ });
1209
+ }
1210
+
1211
+ // Process all pages - call Strapi directly
1212
+ const results = await Promise.allSettled(
1213
+ pages.map(async ({ page, fields }) => {
1214
+ try {
1215
+ // Get page configuration from manifest
1216
+ const pageConfig = manifest.pages[page];
1217
+ if (!pageConfig) {
1218
+ throw new Error(\`Page "\${page}" not found in manifest\`);
1219
+ }
1220
+
1221
+ // Transform fields to Strapi format
1222
+ const strapiData: Record<string, any> = {};
1223
+ for (const [fieldName, value] of Object.entries(fields)) {
1224
+ const fieldConfig = pageConfig.fields[fieldName];
1225
+ if (!fieldConfig) {
1226
+ console.warn(\`Field "\${fieldName}" not found in manifest for page "\${page}"\`);
1227
+ continue;
1228
+ }
1229
+
1230
+ // Handle different field types
1231
+ if (fieldConfig.type === 'image') {
1232
+ // TODO: Handle image uploads - for now just store the value
1233
+ strapiData[fieldName] = value;
1234
+ } else {
1235
+ strapiData[fieldName] = value;
1236
+ }
1237
+ }
1238
+
1239
+ // Update Strapi v5 content - use different endpoints for admin vs user tokens
1240
+ if (isAdminToken) {
1241
+ // Admin tokens use the content-manager API (Strapi v5)
1242
+ const contentEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}\`;
1243
+
1244
+ // Step 1: Update the content
1245
+ await $fetch(contentEndpoint, {
1246
+ method: 'PUT',
1247
+ headers: {
1248
+ 'Authorization': \`Bearer \${token}\`,
1249
+ 'Content-Type': 'application/json',
1250
+ },
1251
+ body: strapiData,
1252
+ });
1253
+
1254
+ // Step 2: Publish the content (Strapi v5)
1255
+ const publishEndpoint = \`\${strapiUrl}/content-manager/single-types/api::\${page}.\${page}/actions/publish\`;
1256
+ await $fetch(publishEndpoint, {
1257
+ method: 'POST',
1258
+ headers: {
1259
+ 'Authorization': \`Bearer \${token}\`,
1260
+ 'Content-Type': 'application/json',
1261
+ },
1262
+ body: {},
1263
+ });
1264
+ } else {
1265
+ // User tokens use the regular REST API
1266
+ const strapiEndpoint = \`\${strapiUrl}/api/\${page}\`;
1267
+
1268
+ await $fetch(strapiEndpoint, {
1269
+ method: 'PUT',
1270
+ headers: {
1271
+ 'Authorization': \`Bearer \${token}\`,
1272
+ 'Content-Type': 'application/json',
1273
+ },
1274
+ body: {
1275
+ data: strapiData,
1276
+ },
1277
+ });
1278
+
1279
+ // Publish using the publish endpoint (Strapi v5)
1280
+ const publishEndpoint = \`\${strapiUrl}/api/\${page}/publish\`;
1281
+ await $fetch(publishEndpoint, {
1282
+ method: 'POST',
1283
+ headers: {
1284
+ 'Authorization': \`Bearer \${token}\`,
1285
+ 'Content-Type': 'application/json',
1286
+ },
1287
+ body: {},
1288
+ });
1289
+ }
1290
+
1291
+ console.log(\`[CMS Publish] Published "\${page}" to Strapi\`);
1292
+ return { page, success: true };
1293
+ } catch (error: any) {
1294
+ console.error(\`[CMS Publish] Failed to publish "\${page}":\`, error);
1295
+ return {
1296
+ page,
1297
+ success: false,
1298
+ error: error.message || 'Unknown error',
1299
+ };
1300
+ }
1301
+ })
1302
+ );
1303
+
1304
+ // Separate successful and failed publications
1305
+ const successful: string[] = [];
1306
+ const failed: Array<{ page: string; error: string }> = [];
1307
+
1308
+ results.forEach((result, index) => {
1309
+ if (result.status === 'fulfilled' && result.value.success) {
1310
+ successful.push(result.value.page);
1311
+ } else if (result.status === 'fulfilled' && !result.value.success) {
1312
+ failed.push({
1313
+ page: result.value.page,
1314
+ error: result.value.error || 'Unknown error',
1315
+ });
1316
+ } else if (result.status === 'rejected') {
1317
+ failed.push({
1318
+ page: pages[index].page,
1319
+ error: result.reason?.message || 'Unknown error',
1320
+ });
1321
+ }
1322
+ });
1323
+
1324
+ console.log(\`[CMS Publish] Published \${successful.length} pages, \${failed.length} failed\`);
1325
+
1326
+ return {
1327
+ success: failed.length === 0,
1328
+ message: \`Published \${successful.length} of \${pages.length} pages\`,
1329
+ successful,
1330
+ failed,
1331
+ user: {
1332
+ id: userResponse.id,
1333
+ username: userResponse.username || userResponse.firstname || 'Unknown',
1334
+ },
1335
+ };
1336
+ } catch (error: any) {
1337
+ console.error('[CMS Publish] Error:', error);
1338
+
1339
+ // Token verification failed
1340
+ if (error.statusCode === 401 || error.status === 401) {
1341
+ throw createError({
1342
+ statusCode: 401,
1343
+ statusMessage: 'Unauthorized: Invalid or expired token',
1344
+ });
1345
+ }
1346
+
1347
+ // Generic error
1348
+ throw createError({
1349
+ statusCode: 500,
1350
+ statusMessage: 'Internal server error while publishing changes',
1351
+ });
1352
+ }
1353
+ });
1354
+ `;
1355
+ const endpointPath = path4.join(serverDir, "publish.post.ts");
1356
+ await fs3.writeFile(endpointPath, endpointContent, "utf-8");
1357
+ }
406
1358
 
407
1359
  // src/boilerplate.ts
408
1360
  import fs4 from "fs-extra";
@@ -742,8 +1694,119 @@ async function generateManifest(pagesDir) {
742
1694
  return manifest;
743
1695
  }
744
1696
  async function writeManifest(outputDir, manifest) {
1697
+ const manifestContent = JSON.stringify(manifest, null, 2);
745
1698
  const manifestPath = path7.join(outputDir, "cms-manifest.json");
746
- await fs6.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
1699
+ await fs6.writeFile(manifestPath, manifestContent, "utf-8");
1700
+ const publicDir = path7.join(outputDir, "public");
1701
+ await fs6.ensureDir(publicDir);
1702
+ const publicManifestPath = path7.join(publicDir, "cms-manifest.json");
1703
+ await fs6.writeFile(publicManifestPath, manifestContent, "utf-8");
1704
+ }
1705
+
1706
+ // src/vue-transformer.ts
1707
+ import * as cheerio3 from "cheerio";
1708
+ import fs7 from "fs-extra";
1709
+ import path8 from "path";
1710
+ function replaceWithBinding(_$, $el, fieldName, type) {
1711
+ if (type === "image") {
1712
+ const $img = $el.find("img").first();
1713
+ if ($img.length) {
1714
+ $img.attr(":src", `content.${fieldName}`);
1715
+ $img.removeAttr("src");
1716
+ }
1717
+ } else if (type === "rich") {
1718
+ $el.attr("v-html", `content.${fieldName}`);
1719
+ $el.empty();
1720
+ } else {
1721
+ $el.empty();
1722
+ $el.text(`{{ content.${fieldName} }}`);
1723
+ }
1724
+ }
1725
+ function transformCollection($, collectionName, collection) {
1726
+ const $items = $(collection.selector);
1727
+ if ($items.length === 0) return;
1728
+ const $first = $items.first();
1729
+ $first.attr("v-for", `(item, index) in content.${collectionName}`);
1730
+ $first.attr(":key", "index");
1731
+ Object.entries(collection.fields).forEach(([fieldName, selector]) => {
1732
+ const $fieldEl = $first.find(selector);
1733
+ if ($fieldEl.length) {
1734
+ if (fieldName === "image") {
1735
+ const $img = $fieldEl.find("img").first();
1736
+ if ($img.length) {
1737
+ $img.attr(":src", "item.image");
1738
+ $img.removeAttr("src");
1739
+ }
1740
+ } else if (fieldName === "link") {
1741
+ $fieldEl.attr(":to", "item.link");
1742
+ $fieldEl.removeAttr("to");
1743
+ $fieldEl.removeAttr("href");
1744
+ } else {
1745
+ $fieldEl.empty();
1746
+ $fieldEl.text(`{{ item.${fieldName} }}`);
1747
+ }
1748
+ }
1749
+ });
1750
+ $items.slice(1).remove();
1751
+ }
1752
+ async function transformVueToReactive(vueFilePath, pageName, manifest) {
1753
+ const pageManifest = manifest.pages[pageName];
1754
+ if (!pageManifest) return;
1755
+ const vueContent = await fs7.readFile(vueFilePath, "utf-8");
1756
+ if (vueContent.includes("useStrapiContent")) {
1757
+ console.log(` Skipping ${pageName} - already transformed`);
1758
+ return;
1759
+ }
1760
+ const templateMatch = vueContent.match(/<template>([\s\S]*?)<\/template>/);
1761
+ if (!templateMatch) return;
1762
+ const templateContent = templateMatch[1];
1763
+ const $ = cheerio3.load(templateContent, { xmlMode: false });
1764
+ if (pageManifest.collections) {
1765
+ Object.entries(pageManifest.collections).forEach(([collectionName, collection]) => {
1766
+ transformCollection($, collectionName, collection);
1767
+ });
1768
+ }
1769
+ if (pageManifest.fields) {
1770
+ Object.entries(pageManifest.fields).forEach(([fieldName, field]) => {
1771
+ const $elements = $(field.selector);
1772
+ $elements.each((_, el) => {
1773
+ const $el = $(el);
1774
+ replaceWithBinding($, $el, fieldName, field.type);
1775
+ });
1776
+ });
1777
+ }
1778
+ let transformedTemplate = $.html();
1779
+ const bodyMatch = transformedTemplate.match(/<body>([\s\S]*)<\/body>/);
1780
+ if (bodyMatch) {
1781
+ transformedTemplate = bodyMatch[1];
1782
+ }
1783
+ transformedTemplate = transformedTemplate.replace(/<\/?html[^>]*>/gi, "").replace(/<head><\/head>/gi, "").trim();
1784
+ const wrapperDivMatch = transformedTemplate.match(/^<div>\s*([\s\S]*?)\s*<\/div>$/);
1785
+ if (wrapperDivMatch) {
1786
+ transformedTemplate = wrapperDivMatch[1].trim();
1787
+ }
1788
+ const scriptSetup = `<script setup lang="ts">
1789
+ // Auto-generated reactive content from Strapi
1790
+ const { content } = useStrapiContent('${pageName}');
1791
+ </script>`;
1792
+ const finalTemplate = transformedTemplate.split("\n").map((line) => " " + line).join("\n");
1793
+ const newVueContent = `${scriptSetup}
1794
+
1795
+ <template>
1796
+ ${finalTemplate}
1797
+ </template>
1798
+ `;
1799
+ await fs7.writeFile(vueFilePath, newVueContent, "utf-8");
1800
+ }
1801
+ async function transformAllVuePages(pagesDir, manifest) {
1802
+ const vueFiles = await fs7.readdir(pagesDir);
1803
+ for (const file of vueFiles) {
1804
+ if (file.endsWith(".vue")) {
1805
+ const pageName = file.replace(".vue", "");
1806
+ const vueFilePath = path8.join(pagesDir, file);
1807
+ await transformVueToReactive(vueFilePath, pageName, manifest);
1808
+ }
1809
+ }
747
1810
  }
748
1811
 
749
1812
  // src/transformer.ts
@@ -849,13 +1912,13 @@ function manifestToSchemas(manifest) {
849
1912
  }
850
1913
 
851
1914
  // src/schema-writer.ts
852
- import fs7 from "fs-extra";
853
- import path8 from "path";
1915
+ import fs8 from "fs-extra";
1916
+ import path9 from "path";
854
1917
  async function writeStrapiSchema(outputDir, name, schema) {
855
- const schemasDir = path8.join(outputDir, "cms-schemas");
856
- await fs7.ensureDir(schemasDir);
857
- const schemaPath = path8.join(schemasDir, `${name}.json`);
858
- await fs7.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
1918
+ const schemasDir = path9.join(outputDir, "cms-schemas");
1919
+ await fs8.ensureDir(schemasDir);
1920
+ const schemaPath = path9.join(schemasDir, `${name}.json`);
1921
+ await fs8.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
859
1922
  }
860
1923
  async function writeAllSchemas(outputDir, schemas) {
861
1924
  for (const [name, schema] of Object.entries(schemas)) {
@@ -863,7 +1926,7 @@ async function writeAllSchemas(outputDir, schemas) {
863
1926
  }
864
1927
  }
865
1928
  async function createStrapiReadme(outputDir) {
866
- const readmePath = path8.join(outputDir, "cms-schemas", "README.md");
1929
+ const readmePath = path9.join(outputDir, "cms-schemas", "README.md");
867
1930
  const content = `# CMS Schemas
868
1931
 
869
1932
  Auto-generated Strapi content type schemas from your Webflow export.
@@ -933,14 +1996,14 @@ const { data } = await $fetch('http://localhost:1337/api/index')
933
1996
  const { data } = await $fetch('http://localhost:1337/api/portfolio-cards')
934
1997
  \`\`\`
935
1998
  `;
936
- await fs7.writeFile(readmePath, content, "utf-8");
1999
+ await fs8.writeFile(readmePath, content, "utf-8");
937
2000
  }
938
2001
 
939
2002
  // src/content-extractor.ts
940
- import * as cheerio3 from "cheerio";
941
- import path9 from "path";
2003
+ import * as cheerio4 from "cheerio";
2004
+ import path10 from "path";
942
2005
  function extractContentFromHTML(html, _pageName, pageManifest) {
943
- const $ = cheerio3.load(html);
2006
+ const $ = cheerio4.load(html);
944
2007
  const content = {
945
2008
  fields: {},
946
2009
  collections: {}
@@ -1009,7 +2072,7 @@ function extractAllContent(htmlFiles, manifest) {
1009
2072
  function normalizeImagePath(imageSrc) {
1010
2073
  if (!imageSrc) return "";
1011
2074
  if (imageSrc.startsWith("/")) return imageSrc;
1012
- const filename = path9.basename(imageSrc);
2075
+ const filename = path10.basename(imageSrc);
1013
2076
  if (imageSrc.includes("images/")) {
1014
2077
  return `/images/${filename}`;
1015
2078
  }
@@ -1048,16 +2111,16 @@ function formatForStrapi(extracted) {
1048
2111
  }
1049
2112
 
1050
2113
  // src/seed-writer.ts
1051
- import fs8 from "fs-extra";
1052
- import path10 from "path";
2114
+ import fs9 from "fs-extra";
2115
+ import path11 from "path";
1053
2116
  async function writeSeedData(outputDir, seedData) {
1054
- const seedDir = path10.join(outputDir, "cms-seed");
1055
- await fs8.ensureDir(seedDir);
1056
- const seedPath = path10.join(seedDir, "seed-data.json");
1057
- await fs8.writeJson(seedPath, seedData, { spaces: 2 });
2117
+ const seedDir = path11.join(outputDir, "cms-seed");
2118
+ await fs9.ensureDir(seedDir);
2119
+ const seedPath = path11.join(seedDir, "seed-data.json");
2120
+ await fs9.writeJson(seedPath, seedData, { spaces: 2 });
1058
2121
  }
1059
2122
  async function createSeedReadme(outputDir) {
1060
- const readmePath = path10.join(outputDir, "cms-seed", "README.md");
2123
+ const readmePath = path11.join(outputDir, "cms-seed", "README.md");
1061
2124
  const content = `# CMS Seed Data
1062
2125
 
1063
2126
  Auto-extracted content from your Webflow export, ready to seed into Strapi.
@@ -1113,7 +2176,7 @@ When seeding Strapi, these images will be uploaded to Strapi's media library.
1113
2176
  2. Set up your Strapi instance with the schemas from \`cms-schemas/\`
1114
2177
  3. Use this seed data to populate your CMS
1115
2178
  `;
1116
- await fs8.writeFile(readmePath, content, "utf-8");
2179
+ await fs9.writeFile(readmePath, content, "utf-8");
1117
2180
  }
1118
2181
 
1119
2182
  // src/converter.ts
@@ -1124,7 +2187,7 @@ async function convertWebflowExport(options) {
1124
2187
  console.log(pc3.dim(`Output: ${outputDir}`));
1125
2188
  try {
1126
2189
  await setupBoilerplate(boilerplate, outputDir);
1127
- const inputExists = await fs9.pathExists(inputDir);
2190
+ const inputExists = await fs10.pathExists(inputDir);
1128
2191
  if (!inputExists) {
1129
2192
  throw new Error(`Input directory not found: ${inputDir}`);
1130
2193
  }
@@ -1166,7 +2229,7 @@ ${parsed.embeddedStyles}
1166
2229
  }
1167
2230
  await formatVueFiles(outputDir);
1168
2231
  console.log(pc3.blue("\n\u{1F50D} Analyzing pages for CMS fields..."));
1169
- const pagesDir = path11.join(outputDir, "pages");
2232
+ const pagesDir = path12.join(outputDir, "pages");
1170
2233
  const manifest = await generateManifest(pagesDir);
1171
2234
  await writeManifest(outputDir, manifest);
1172
2235
  const totalFields = Object.values(manifest.pages).reduce(
@@ -1180,6 +2243,9 @@ ${parsed.embeddedStyles}
1180
2243
  console.log(pc3.green(` \u2713 Detected ${totalFields} fields across ${Object.keys(manifest.pages).length} pages`));
1181
2244
  console.log(pc3.green(` \u2713 Detected ${totalCollections} collections`));
1182
2245
  console.log(pc3.green(" \u2713 Generated cms-manifest.json"));
2246
+ console.log(pc3.blue("\n\u26A1 Transforming Vue files to reactive templates..."));
2247
+ await transformAllVuePages(pagesDir, manifest);
2248
+ console.log(pc3.green(` \u2713 Transformed ${Object.keys(manifest.pages).length} pages to use Vue template syntax`));
1183
2249
  console.log(pc3.blue("\n\u{1F4DD} Extracting content from HTML..."));
1184
2250
  console.log(pc3.dim(` HTML map has ${htmlContentMap.size} entries`));
1185
2251
  console.log(pc3.dim(` Manifest has ${Object.keys(manifest.pages).length} pages`));
@@ -1219,19 +2285,31 @@ ${parsed.embeddedStyles}
1219
2285
  }
1220
2286
  console.log(pc3.blue("\n\u{1F3A8} Setting up editor overlay..."));
1221
2287
  await createEditorPlugin(outputDir);
2288
+ await createEditorContentComposable(outputDir);
2289
+ await createStrapiContentComposable(outputDir);
1222
2290
  await addEditorDependency(outputDir);
1223
2291
  await createSaveEndpoint(outputDir);
2292
+ await createPublishEndpoint(outputDir);
2293
+ await createStrapiBootstrap(outputDir);
2294
+ await addStrapiUrlToConfig(outputDir);
1224
2295
  console.log(pc3.green(" \u2713 Editor plugin created"));
2296
+ console.log(pc3.green(" \u2713 Editor content composable created"));
2297
+ console.log(pc3.green(" \u2713 Strapi content composable created"));
1225
2298
  console.log(pc3.green(" \u2713 Editor dependency added"));
1226
2299
  console.log(pc3.green(" \u2713 Save endpoint created"));
2300
+ console.log(pc3.green(" \u2713 Publish endpoint created"));
2301
+ console.log(pc3.green(" \u2713 Strapi bootstrap file generated"));
2302
+ console.log(pc3.green(" \u2713 Strapi config added"));
1227
2303
  console.log(pc3.green("\n\u2705 Conversion completed successfully!"));
1228
2304
  console.log(pc3.cyan("\n\u{1F4CB} Next steps:"));
1229
2305
  console.log(pc3.dim(` 1. cd ${outputDir}`));
1230
2306
  console.log(pc3.dim(" 2. Review cms-manifest.json and cms-seed/seed-data.json"));
1231
2307
  console.log(pc3.dim(" 3. Set up Strapi and install schemas from cms-schemas/"));
1232
- console.log(pc3.dim(" 4. Seed Strapi with data from cms-seed/"));
1233
- console.log(pc3.dim(" 5. pnpm install && pnpm dev"));
1234
- console.log(pc3.dim(" 6. Visit http://localhost:3000?preview=true to edit inline!"));
2308
+ console.log(pc3.dim(" 4. Copy strapi-bootstrap/index.ts to your Strapi project at src/index.ts"));
2309
+ console.log(pc3.dim(" (This auto-enables public read permissions on Strapi startup)"));
2310
+ console.log(pc3.dim(" 5. Seed Strapi with data from cms-seed/"));
2311
+ console.log(pc3.dim(" 6. pnpm install && pnpm dev"));
2312
+ console.log(pc3.dim(" 7. Visit http://localhost:3000?preview=true to edit inline!"));
1235
2313
  } catch (error) {
1236
2314
  console.error(pc3.red("\n\u274C Conversion failed:"));
1237
2315
  console.error(pc3.red(error instanceof Error ? error.message : String(error)));
@@ -1240,17 +2318,59 @@ ${parsed.embeddedStyles}
1240
2318
  }
1241
2319
 
1242
2320
  // src/strapi-setup.ts
1243
- import fs10 from "fs-extra";
1244
- import path12 from "path";
2321
+ import fs11 from "fs-extra";
2322
+ import path13 from "path";
1245
2323
  import { glob as glob2 } from "glob";
1246
2324
  import * as readline from "readline";
2325
+ var ENV_FILE = ".env";
2326
+ async function loadConfig(projectDir) {
2327
+ const envPath = path13.join(projectDir, ENV_FILE);
2328
+ if (await fs11.pathExists(envPath)) {
2329
+ try {
2330
+ const content = await fs11.readFile(envPath, "utf-8");
2331
+ const config = {};
2332
+ for (const line of content.split("\n")) {
2333
+ const trimmed = line.trim();
2334
+ if (!trimmed || trimmed.startsWith("#")) continue;
2335
+ const [key, ...valueParts] = trimmed.split("=");
2336
+ const value = valueParts.join("=").trim();
2337
+ if (key === "STRAPI_API_TOKEN") {
2338
+ config.apiToken = value;
2339
+ } else if (key === "STRAPI_URL") {
2340
+ config.strapiUrl = value;
2341
+ }
2342
+ }
2343
+ return config;
2344
+ } catch {
2345
+ return {};
2346
+ }
2347
+ }
2348
+ return {};
2349
+ }
2350
+ async function saveConfig(projectDir, config) {
2351
+ const envPath = path13.join(projectDir, ENV_FILE);
2352
+ let content = "";
2353
+ if (await fs11.pathExists(envPath)) {
2354
+ content = await fs11.readFile(envPath, "utf-8");
2355
+ content = content.split("\n").filter((line) => !line.startsWith("STRAPI_API_TOKEN=") && !line.startsWith("STRAPI_URL=")).join("\n");
2356
+ if (content && !content.endsWith("\n")) {
2357
+ content += "\n";
2358
+ }
2359
+ }
2360
+ if (config.strapiUrl) {
2361
+ content += `STRAPI_URL=${config.strapiUrl}
2362
+ `;
2363
+ }
2364
+ if (config.apiToken) {
2365
+ content += `STRAPI_API_TOKEN=${config.apiToken}
2366
+ `;
2367
+ }
2368
+ await fs11.writeFile(envPath, content);
2369
+ }
1247
2370
  async function completeSetup(options) {
1248
- const {
1249
- projectDir,
1250
- strapiDir,
1251
- strapiUrl = "http://localhost:1337",
1252
- apiToken
1253
- } = options;
2371
+ const { projectDir, strapiDir, strapiUrl: optionUrl, apiToken: optionToken, ignoreSavedToken } = options;
2372
+ const savedConfig = await loadConfig(projectDir);
2373
+ const strapiUrl = optionUrl || savedConfig.strapiUrl || "http://localhost:1337";
1254
2374
  console.log("\u{1F680} Starting complete Strapi setup...\n");
1255
2375
  console.log("\u{1F4E6} Step 1: Installing schemas...");
1256
2376
  await installSchemas(projectDir, strapiDir);
@@ -1267,16 +2387,23 @@ async function completeSetup(options) {
1267
2387
  process.exit(1);
1268
2388
  }
1269
2389
  console.log("\u2713 Connected to Strapi\n");
1270
- let token = apiToken;
1271
- if (!token) {
2390
+ let token = optionToken || (!ignoreSavedToken ? savedConfig.apiToken : void 0);
2391
+ if (token && !ignoreSavedToken) {
2392
+ console.log("\u{1F511} Step 4: Using saved API token");
2393
+ } else if (token && optionToken) {
2394
+ console.log("\u{1F511} Step 4: Using provided API token");
2395
+ } else {
1272
2396
  console.log("\u{1F511} Step 4: API Token needed");
1273
2397
  console.log(" 1. Open Strapi admin: http://localhost:1337/admin");
1274
2398
  console.log(" 2. Go to Settings > API Tokens > Create new API Token");
1275
- console.log(
1276
- ' 3. Name: "Seed Script", Type: "Full access", Duration: "Unlimited"'
1277
- );
2399
+ console.log(' 3. Name: "Seed Script", Type: "Full access", Duration: "Unlimited"');
1278
2400
  console.log(" 4. Copy the token and paste it here:\n");
1279
2401
  token = await promptForToken();
2402
+ const saveToken = await promptYesNo(" Save token for future use?");
2403
+ if (saveToken) {
2404
+ await saveConfig(projectDir, { ...savedConfig, apiToken: token, strapiUrl });
2405
+ console.log(" \u2713 Token saved to .env");
2406
+ }
1280
2407
  console.log("");
1281
2408
  }
1282
2409
  console.log("\u{1F4F8} Step 5: Uploading images...");
@@ -1293,19 +2420,19 @@ async function completeSetup(options) {
1293
2420
  console.log(" 3. Connect your Nuxt app to Strapi API");
1294
2421
  }
1295
2422
  async function installSchemas(projectDir, strapiDir) {
1296
- if (!await fs10.pathExists(strapiDir)) {
2423
+ if (!await fs11.pathExists(strapiDir)) {
1297
2424
  console.error(` \u2717 Strapi directory not found: ${strapiDir}`);
1298
- console.error(` Resolved to: ${path12.resolve(strapiDir)}`);
2425
+ console.error(` Resolved to: ${path13.resolve(strapiDir)}`);
1299
2426
  throw new Error(`Strapi directory not found: ${strapiDir}`);
1300
2427
  }
1301
- const packageJsonPath = path12.join(strapiDir, "package.json");
1302
- if (await fs10.pathExists(packageJsonPath)) {
1303
- const pkg = await fs10.readJson(packageJsonPath);
2428
+ const packageJsonPath = path13.join(strapiDir, "package.json");
2429
+ if (await fs11.pathExists(packageJsonPath)) {
2430
+ const pkg = await fs11.readJson(packageJsonPath);
1304
2431
  if (!pkg.dependencies?.["@strapi/strapi"]) {
1305
2432
  console.warn(` \u26A0\uFE0F Warning: ${strapiDir} may not be a Strapi project`);
1306
2433
  }
1307
2434
  }
1308
- const schemaDir = path12.join(projectDir, "cms-schemas");
2435
+ const schemaDir = path13.join(projectDir, "cms-schemas");
1309
2436
  const schemaFiles = await glob2("*.json", {
1310
2437
  cwd: schemaDir,
1311
2438
  absolute: false
@@ -1316,42 +2443,42 @@ async function installSchemas(projectDir, strapiDir) {
1316
2443
  }
1317
2444
  console.log(` Found ${schemaFiles.length} schema file(s)`);
1318
2445
  for (const file of schemaFiles) {
1319
- const schemaPath = path12.join(schemaDir, file);
1320
- const schema = await fs10.readJson(schemaPath);
1321
- const singularName = schema.info?.singularName || path12.basename(file, ".json");
2446
+ const schemaPath = path13.join(schemaDir, file);
2447
+ const schema = await fs11.readJson(schemaPath);
2448
+ const singularName = schema.info?.singularName || path13.basename(file, ".json");
1322
2449
  console.log(` Installing ${singularName}...`);
1323
2450
  try {
1324
- const apiPath = path12.join(strapiDir, "src", "api", singularName);
1325
- const contentTypesPath = path12.join(
2451
+ const apiPath = path13.join(strapiDir, "src", "api", singularName);
2452
+ const contentTypesPath = path13.join(
1326
2453
  apiPath,
1327
2454
  "content-types",
1328
2455
  singularName
1329
2456
  );
1330
- const targetPath = path12.join(contentTypesPath, "schema.json");
1331
- await fs10.ensureDir(contentTypesPath);
1332
- await fs10.ensureDir(path12.join(apiPath, "routes"));
1333
- await fs10.ensureDir(path12.join(apiPath, "controllers"));
1334
- await fs10.ensureDir(path12.join(apiPath, "services"));
1335
- await fs10.writeJson(targetPath, schema, { spaces: 2 });
2457
+ const targetPath = path13.join(contentTypesPath, "schema.json");
2458
+ await fs11.ensureDir(contentTypesPath);
2459
+ await fs11.ensureDir(path13.join(apiPath, "routes"));
2460
+ await fs11.ensureDir(path13.join(apiPath, "controllers"));
2461
+ await fs11.ensureDir(path13.join(apiPath, "services"));
2462
+ await fs11.writeJson(targetPath, schema, { spaces: 2 });
1336
2463
  const routeContent = `import { factories } from '@strapi/strapi';
1337
2464
  export default factories.createCoreRouter('api::${singularName}.${singularName}');
1338
2465
  `;
1339
- await fs10.writeFile(
1340
- path12.join(apiPath, "routes", `${singularName}.ts`),
2466
+ await fs11.writeFile(
2467
+ path13.join(apiPath, "routes", `${singularName}.ts`),
1341
2468
  routeContent
1342
2469
  );
1343
2470
  const controllerContent = `import { factories } from '@strapi/strapi';
1344
2471
  export default factories.createCoreController('api::${singularName}.${singularName}');
1345
2472
  `;
1346
- await fs10.writeFile(
1347
- path12.join(apiPath, "controllers", `${singularName}.ts`),
2473
+ await fs11.writeFile(
2474
+ path13.join(apiPath, "controllers", `${singularName}.ts`),
1348
2475
  controllerContent
1349
2476
  );
1350
2477
  const serviceContent = `import { factories } from '@strapi/strapi';
1351
2478
  export default factories.createCoreService('api::${singularName}.${singularName}');
1352
2479
  `;
1353
- await fs10.writeFile(
1354
- path12.join(apiPath, "services", `${singularName}.ts`),
2480
+ await fs11.writeFile(
2481
+ path13.join(apiPath, "services", `${singularName}.ts`),
1355
2482
  serviceContent
1356
2483
  );
1357
2484
  } catch (error) {
@@ -1391,10 +2518,51 @@ async function promptForToken() {
1391
2518
  });
1392
2519
  });
1393
2520
  }
2521
+ async function promptYesNo(question) {
2522
+ const rl = createReadline();
2523
+ return new Promise((resolve) => {
2524
+ rl.question(`${question} (y/n): `, (answer) => {
2525
+ rl.close();
2526
+ resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
2527
+ });
2528
+ });
2529
+ }
2530
+ async function getExistingMedia(strapiUrl, apiToken) {
2531
+ const existingMedia = /* @__PURE__ */ new Map();
2532
+ try {
2533
+ let page = 1;
2534
+ const pageSize = 100;
2535
+ let hasMore = true;
2536
+ while (hasMore) {
2537
+ const response = await fetch(
2538
+ `${strapiUrl}/api/upload/files?pagination[page]=${page}&pagination[pageSize]=${pageSize}`,
2539
+ {
2540
+ headers: {
2541
+ Authorization: `Bearer ${apiToken}`
2542
+ }
2543
+ }
2544
+ );
2545
+ if (!response.ok) {
2546
+ break;
2547
+ }
2548
+ const data = await response.json();
2549
+ const files = Array.isArray(data) ? data : data.results || [];
2550
+ for (const file of files) {
2551
+ if (file.name) {
2552
+ existingMedia.set(file.name, file.id);
2553
+ }
2554
+ }
2555
+ hasMore = files.length === pageSize;
2556
+ page++;
2557
+ }
2558
+ } catch (error) {
2559
+ }
2560
+ return existingMedia;
2561
+ }
1394
2562
  async function uploadAllImages(projectDir, strapiUrl, apiToken) {
1395
2563
  const mediaMap = /* @__PURE__ */ new Map();
1396
- const imagesDir = path12.join(projectDir, "public", "assets", "images");
1397
- if (!await fs10.pathExists(imagesDir)) {
2564
+ const imagesDir = path13.join(projectDir, "public", "assets", "images");
2565
+ if (!await fs11.pathExists(imagesDir)) {
1398
2566
  console.log(" No images directory found");
1399
2567
  return mediaMap;
1400
2568
  }
@@ -1402,26 +2570,35 @@ async function uploadAllImages(projectDir, strapiUrl, apiToken) {
1402
2570
  cwd: imagesDir,
1403
2571
  absolute: false
1404
2572
  });
1405
- console.log(` Uploading ${imageFiles.length} images...`);
2573
+ console.log(` Checking for existing media...`);
2574
+ const existingMedia = await getExistingMedia(strapiUrl, apiToken);
2575
+ let uploadedCount = 0;
2576
+ let skippedCount = 0;
2577
+ console.log(` Processing ${imageFiles.length} images...`);
1406
2578
  for (const imageFile of imageFiles) {
1407
- const imagePath = path12.join(imagesDir, imageFile);
1408
- const mediaId = await uploadImage(
1409
- imagePath,
1410
- imageFile,
1411
- strapiUrl,
1412
- apiToken
1413
- );
2579
+ const fileName = path13.basename(imageFile);
2580
+ const existingId = existingMedia.get(fileName);
2581
+ if (existingId) {
2582
+ mediaMap.set(`/images/${imageFile}`, existingId);
2583
+ mediaMap.set(imageFile, existingId);
2584
+ skippedCount++;
2585
+ continue;
2586
+ }
2587
+ const imagePath = path13.join(imagesDir, imageFile);
2588
+ const mediaId = await uploadImage(imagePath, imageFile, strapiUrl, apiToken);
1414
2589
  if (mediaId) {
1415
2590
  mediaMap.set(`/images/${imageFile}`, mediaId);
1416
2591
  mediaMap.set(imageFile, mediaId);
2592
+ uploadedCount++;
1417
2593
  console.log(` \u2713 ${imageFile}`);
1418
2594
  }
1419
2595
  }
2596
+ console.log(` Uploaded: ${uploadedCount}, Skipped (existing): ${skippedCount}`);
1420
2597
  return mediaMap;
1421
2598
  }
1422
2599
  async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
1423
2600
  try {
1424
- const fileBuffer = await fs10.readFile(filePath);
2601
+ const fileBuffer = await fs11.readFile(filePath);
1425
2602
  const mimeType = getMimeType(fileName);
1426
2603
  const blob = new Blob([fileBuffer], { type: mimeType });
1427
2604
  const formData = new globalThis.FormData();
@@ -1448,7 +2625,7 @@ async function uploadImage(filePath, fileName, strapiUrl, apiToken) {
1448
2625
  }
1449
2626
  }
1450
2627
  function getMimeType(fileName) {
1451
- const ext = path12.extname(fileName).toLowerCase();
2628
+ const ext = path13.extname(fileName).toLowerCase();
1452
2629
  const mimeTypes = {
1453
2630
  ".jpg": "image/jpeg",
1454
2631
  ".jpeg": "image/jpeg",
@@ -1460,18 +2637,18 @@ function getMimeType(fileName) {
1460
2637
  return mimeTypes[ext] || "application/octet-stream";
1461
2638
  }
1462
2639
  async function seedContent(projectDir, strapiUrl, apiToken, mediaMap) {
1463
- const seedPath = path12.join(projectDir, "cms-seed", "seed-data.json");
1464
- if (!await fs10.pathExists(seedPath)) {
2640
+ const seedPath = path13.join(projectDir, "cms-seed", "seed-data.json");
2641
+ if (!await fs11.pathExists(seedPath)) {
1465
2642
  console.log(" No seed data found");
1466
2643
  return;
1467
2644
  }
1468
- const seedData = await fs10.readJson(seedPath);
1469
- const schemasDir = path12.join(projectDir, "cms-schemas");
2645
+ const seedData = await fs11.readJson(seedPath);
2646
+ const schemasDir = path13.join(projectDir, "cms-schemas");
1470
2647
  const schemas = /* @__PURE__ */ new Map();
1471
2648
  const schemaFiles = await glob2("*.json", { cwd: schemasDir });
1472
2649
  for (const file of schemaFiles) {
1473
- const schema = await fs10.readJson(path12.join(schemasDir, file));
1474
- const name = path12.basename(file, ".json");
2650
+ const schema = await fs11.readJson(path13.join(schemasDir, file));
2651
+ const name = path13.basename(file, ".json");
1475
2652
  schemas.set(name, schema);
1476
2653
  }
1477
2654
  let successCount = 0;
@@ -1627,7 +2804,14 @@ async function confirm(question) {
1627
2804
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
1628
2805
  }
1629
2806
  program.name("cms").description("SeeMS - Webflow to CMS converter").version("0.1.2");
1630
- program.command("convert").description("Convert Webflow export to Nuxt 3 project").argument("<input>", "Path to Webflow export directory").argument("<output>", "Path to output Nuxt project directory").option("-b, --boilerplate <source>", "Boilerplate source (GitHub URL or local path)").option("-o, --overrides <path>", "Path to overrides JSON file").option("--generate-schemas", "Generate CMS schemas immediately").option("--cms <type>", "CMS backend type (strapi|contentful|sanity)", "strapi").option("--no-interactive", "Skip interactive prompts").action(async (input, output, options) => {
2807
+ program.command("convert").description("Convert Webflow export to Nuxt 3 project").argument("<input>", "Path to Webflow export directory").argument("<output>", "Path to output Nuxt project directory").option(
2808
+ "-b, --boilerplate <source>",
2809
+ "Boilerplate source (GitHub URL or local path)"
2810
+ ).option("-o, --overrides <path>", "Path to overrides JSON file").option("--generate-schemas", "Generate CMS schemas immediately").option(
2811
+ "--cms <type>",
2812
+ "CMS backend type (strapi|contentful|sanity)",
2813
+ "strapi"
2814
+ ).option("--no-interactive", "Skip interactive prompts").action(async (input, output, options) => {
1631
2815
  try {
1632
2816
  await convertWebflowExport({
1633
2817
  inputDir: input,
@@ -1644,7 +2828,9 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
1644
2828
  );
1645
2829
  if (shouldSetup) {
1646
2830
  const strapiDir = await prompt(
1647
- pc4.cyan("\u{1F4C1} Enter path to your Strapi directory (e.g., ./strapi-dev): ")
2831
+ pc4.cyan(
2832
+ "\u{1F4C1} Enter path to your Strapi directory (e.g., ./strapi-dev): "
2833
+ )
1648
2834
  );
1649
2835
  if (strapiDir) {
1650
2836
  console.log("");
@@ -1658,13 +2844,17 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
1658
2844
  } catch (error) {
1659
2845
  console.error(pc4.red("\n\u274C Strapi setup failed"));
1660
2846
  console.error(pc4.dim("You can run setup manually later with:"));
1661
- console.error(pc4.dim(` cms setup-strapi ${output} ${strapiDir}`));
2847
+ console.error(
2848
+ pc4.dim(` cms setup-strapi ${output} ${strapiDir}`)
2849
+ );
1662
2850
  }
1663
2851
  }
1664
2852
  } else {
1665
2853
  console.log("");
1666
2854
  console.log(pc4.dim("\u{1F4A1} You can setup Strapi later with:"));
1667
- console.log(pc4.dim(` cms setup-strapi ${output} <strapi-directory>`));
2855
+ console.log(
2856
+ pc4.dim(` cms setup-strapi ${output} <strapi-directory>`)
2857
+ );
1668
2858
  }
1669
2859
  }
1670
2860
  } catch (error) {
@@ -1672,13 +2862,14 @@ program.command("convert").description("Convert Webflow export to Nuxt 3 project
1672
2862
  process.exit(1);
1673
2863
  }
1674
2864
  });
1675
- program.command("setup-strapi").description("Setup Strapi with schemas and seed data").argument("<project-dir>", "Path to converted project directory").argument("<strapi-dir>", "Path to Strapi directory").option("--url <url>", "Strapi URL", "http://localhost:1337").option("--token <token>", "Strapi API token (optional)").action(async (projectDir, strapiDir, options) => {
2865
+ program.command("setup-strapi").description("Setup Strapi with schemas and seed data").argument("<project-dir>", "Path to converted project directory").argument("<strapi-dir>", "Path to Strapi directory").option("--url <url>", "Strapi URL", "http://localhost:1337").option("--token <token>", "Strapi API token (optional)").option("--new-token", "Ignore saved token and prompt for a new one").action(async (projectDir, strapiDir, options) => {
1676
2866
  try {
1677
2867
  await completeSetup({
1678
2868
  projectDir,
1679
2869
  strapiDir,
1680
2870
  strapiUrl: options.url,
1681
- apiToken: options.token
2871
+ apiToken: options.token,
2872
+ ignoreSavedToken: options.newToken
1682
2873
  });
1683
2874
  } catch (error) {
1684
2875
  console.error(pc4.red("Strapi setup failed"));
@@ -1686,10 +2877,43 @@ program.command("setup-strapi").description("Setup Strapi with schemas and seed
1686
2877
  process.exit(1);
1687
2878
  }
1688
2879
  });
1689
- program.command("generate").description("Generate CMS schemas from manifest").argument("<manifest>", "Path to cms-manifest.json").option("-t, --type <cms>", "CMS type (strapi|contentful|sanity)", "strapi").option("-o, --output <dir>", "Output directory for schemas").action(async (manifest, _options) => {
1690
- console.log(pc4.cyan("\u{1F5C2}\uFE0F SeeMS Schema Generator"));
1691
- console.log(pc4.dim(`Generating schemas from: ${manifest}`));
1692
- console.log(pc4.yellow("\u26A0\uFE0F Schema generation logic to be implemented"));
2880
+ program.command("generate").description("Generate CMS schemas from manifest").argument("<manifest>", "Path to cms-manifest.json").option("-t, --type <cms>", "CMS type (strapi|contentful|sanity)", "strapi").option("-o, --output <dir>", "Output directory for schemas").action(async (manifestPath, options) => {
2881
+ try {
2882
+ console.log(pc4.cyan("\u{1F5C2}\uFE0F SeeMS Schema Generator"));
2883
+ console.log(pc4.dim(`Reading manifest from: ${manifestPath}`));
2884
+ const manifestExists = await fs12.pathExists(manifestPath);
2885
+ if (!manifestExists) {
2886
+ throw new Error(`Manifest file not found: ${manifestPath}`);
2887
+ }
2888
+ const manifestContent = await fs12.readFile(manifestPath, "utf-8");
2889
+ const manifest = JSON.parse(manifestContent);
2890
+ console.log(pc4.green(` \u2713 Manifest loaded successfully`));
2891
+ const outputDir = options.output || path14.dirname(manifestPath);
2892
+ if (options.type !== "strapi") {
2893
+ console.log(
2894
+ pc4.yellow(
2895
+ `\u26A0\uFE0F Only Strapi is currently supported. Using Strapi schema format.`
2896
+ )
2897
+ );
2898
+ }
2899
+ console.log(pc4.blue("\n\u{1F4CB} Generating Strapi schemas..."));
2900
+ const schemas = manifestToSchemas(manifest);
2901
+ await writeAllSchemas(outputDir, schemas);
2902
+ await createStrapiReadme(outputDir);
2903
+ console.log(
2904
+ pc4.green(
2905
+ ` \u2713 Generated ${Object.keys(schemas).length} Strapi content types`
2906
+ )
2907
+ );
2908
+ console.log(pc4.dim(` \u2713 Schemas written to: ${path14.join(outputDir, "cms-schemas")}/`));
2909
+ console.log(pc4.green("\n\u2705 Schema generation completed successfully!"));
2910
+ } catch (error) {
2911
+ console.error(pc4.red("\n\u274C Schema generation failed:"));
2912
+ console.error(
2913
+ pc4.red(error instanceof Error ? error.message : String(error))
2914
+ );
2915
+ process.exit(1);
2916
+ }
1693
2917
  });
1694
2918
  program.parse();
1695
2919
  //# sourceMappingURL=cli.mjs.map