gmoonc 0.0.17 → 0.0.19

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/README.md CHANGED
@@ -63,6 +63,21 @@ The logo is installed at `src/gmoonc/assets/gmoonc-logo.png`. You can replace it
63
63
 
64
64
  ## Changelog
65
65
 
66
+ ### 0.0.19
67
+ - Fix: Added Navigate import to GMooncAppLayout.tsx template
68
+ - Fix: Corrected route protection order - isLoading checked before isAuthenticated
69
+ - Fix: Automatic update of all session context imports to use GMooncSupabaseSessionProvider
70
+ - Fix: All components now use Supabase provider instead of mock context
71
+
72
+ ### 0.0.18
73
+ - Fix: env.ts no longer throws when .env is missing - only logs warning
74
+ - Fix: client.ts import corrected - hasSupabaseEnv imported from env.ts
75
+ - Fix: GMooncSupabaseSessionProvider imports corrected - hasSupabaseEnv from env.ts
76
+ - Fix: createMissingEnvClient improved - more robust mock client
77
+ - Fix: GMooncAppLayout route protection added - redirects to /login when not authenticated
78
+ - Fix: All imports in generated files now without .js suffix
79
+ - Fix: client.ts syntax error fixed - removed extra closing brace
80
+
66
81
  ### 0.0.17
67
82
  - Feature: New `supabase --vite` command to setup Supabase integration
68
83
  - Feature: Supabase Auth + RBAC integration with session provider
package/dist/index.cjs CHANGED
@@ -388,8 +388,8 @@ function resolveImportToFile(importPath, fromDir) {
388
388
  const extensions = [".tsx", ".ts", ".jsx", ".js"];
389
389
  const indexFiles = ["index.tsx", "index.ts", "index.jsx", "index.js"];
390
390
  if ((0, import_fs8.existsSync)(basePath)) {
391
- const { statSync: statSync2 } = require("fs");
392
- const stats = statSync2(basePath);
391
+ const { statSync: statSync3 } = require("fs");
392
+ const stats = statSync3(basePath);
393
393
  if (stats.isFile()) {
394
394
  return basePath;
395
395
  }
@@ -405,16 +405,16 @@ function resolveImportToFile(importPath, fromDir) {
405
405
  for (const ext of extensions) {
406
406
  const fullPath = basePath + ext;
407
407
  if ((0, import_fs8.existsSync)(fullPath)) {
408
- const { statSync: statSync2 } = require("fs");
409
- const stats = statSync2(fullPath);
408
+ const { statSync: statSync3 } = require("fs");
409
+ const stats = statSync3(fullPath);
410
410
  if (stats.isFile()) {
411
411
  return fullPath;
412
412
  }
413
413
  }
414
414
  }
415
415
  if ((0, import_fs8.existsSync)(basePath)) {
416
- const { statSync: statSync2 } = require("fs");
417
- const stats = statSync2(basePath);
416
+ const { statSync: statSync3 } = require("fs");
417
+ const stats = statSync3(basePath);
418
418
  if (stats.isDirectory()) {
419
419
  for (const indexFile of indexFiles) {
420
420
  const indexPath = (0, import_path5.join)(basePath, indexFile);
@@ -787,30 +787,35 @@ function generateEnvFile(projectDir, supabaseDir, platform) {
787
787
  * Supabase environment variables for Vite
788
788
  *
789
789
  * This file reads environment variables from import.meta.env (Vite)
790
- * and validates their presence.
790
+ * and validates their presence. Never throws - only logs warnings.
791
791
  */
792
792
 
793
793
  const supabaseUrlValue = import.meta.env.VITE_SUPABASE_URL;
794
794
  const supabaseAnonKeyValue = import.meta.env.VITE_SUPABASE_ANON_KEY;
795
795
 
796
- export const hasSupabaseEnv = Boolean(supabaseUrlValue?.trim() && supabaseAnonKeyValue?.trim());
796
+ // Safe trim with fallback to empty string
797
+ const url = typeof supabaseUrlValue === 'string' ? supabaseUrlValue.trim() : '';
798
+ const anonKey = typeof supabaseAnonKeyValue === 'string' ? supabaseAnonKeyValue.trim() : '';
797
799
 
798
- if (!hasSupabaseEnv) {
800
+ export const supabaseUrl = url;
801
+ export const supabaseAnonKey = anonKey;
802
+ export const hasSupabaseEnv = Boolean(url && anonKey);
803
+
804
+ // Log warning once if env vars are missing (only in browser, only in dev)
805
+ if (!hasSupabaseEnv && typeof window !== 'undefined' && import.meta.env.DEV) {
799
806
  const missingVars: string[] = [];
800
- if (!supabaseUrlValue?.trim()) missingVars.push('VITE_SUPABASE_URL');
801
- if (!supabaseAnonKeyValue?.trim()) missingVars.push('VITE_SUPABASE_ANON_KEY');
802
-
803
- const errorMessage = \`Missing Supabase environment variables: \${missingVars.join(', ')}.\\n\\nPlease:\\n1. Copy .env.example to .env\\n2. Fill VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY\\n3. Restart dev server\`;
804
-
805
- if (typeof window !== 'undefined') {
806
- console.error('\u274C', errorMessage);
807
- }
807
+ if (!url) missingVars.push('VITE_SUPABASE_URL');
808
+ if (!anonKey) missingVars.push('VITE_SUPABASE_ANON_KEY');
808
809
 
809
- throw new Error(errorMessage);
810
+ console.warn(
811
+ \`\u26A0\uFE0F Supabase environment variables missing: \${missingVars.join(', ')}.\\n\` +
812
+ \`Please:\\n\` +
813
+ \`1. Copy .env.example to .env\\n\` +
814
+ \`2. Fill VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY\\n\` +
815
+ \`3. Restart dev server\\n\` +
816
+ \`The app will continue without authentication until configured.\`
817
+ );
810
818
  }
811
-
812
- export const supabaseUrl = supabaseUrlValue!;
813
- export const supabaseAnonKey = supabaseAnonKeyValue!;
814
819
  `;
815
820
  writeFileSafe(envFilePath, content);
816
821
  logSuccess("Generated src/gmoonc/supabase/env.ts");
@@ -818,7 +823,7 @@ export const supabaseAnonKey = supabaseAnonKeyValue!;
818
823
  function generateClientFile(projectDir, supabaseDir) {
819
824
  const clientFilePath = (0, import_path6.join)(supabaseDir, "client.ts");
820
825
  const content = `import { createClient, type SupabaseClient } from '@supabase/supabase-js';
821
- import { supabaseUrl, supabaseAnonKey, hasSupabaseEnv } from './env.js';
826
+ import { supabaseUrl, supabaseAnonKey, hasSupabaseEnv } from './env';
822
827
 
823
828
  // Database types (minimal - extend as needed)
824
829
  export interface Database {
@@ -1118,37 +1123,58 @@ export interface Database {
1118
1123
  // Singleton pattern to avoid multiple instances
1119
1124
  let supabaseInstance: SupabaseClient<Database> | null = null;
1120
1125
 
1121
- const createMissingEnvClient = () => {
1126
+ let warningLogged = false;
1127
+
1128
+ const createMissingEnvClient = (): SupabaseClient<Database> => {
1122
1129
  const missingEnvMessage = 'Supabase environment variables are missing or empty.';
1123
1130
 
1131
+ // Log warning once
1132
+ if (!warningLogged && typeof window !== 'undefined') {
1133
+ console.warn('\u26A0\uFE0F Supabase env not configured. Authentication features will not work.');
1134
+ warningLogged = true;
1135
+ }
1136
+
1124
1137
  const notConfigured = () => Promise.reject(new Error(missingEnvMessage));
1138
+ const emptyResult = () => Promise.resolve({ data: null, error: new Error(missingEnvMessage) });
1139
+ const emptyArray = () => Promise.resolve({ data: [], error: new Error(missingEnvMessage) });
1125
1140
 
1126
1141
  return {
1127
1142
  auth: {
1128
- getSession: async () => ({ data: { session: null }, error: new Error(missingEnvMessage) }),
1143
+ getSession: async () => ({ data: { session: null }, error: null }),
1129
1144
  signInWithPassword: notConfigured,
1130
1145
  signUp: notConfigured,
1131
- signOut: notConfigured,
1146
+ signOut: async () => ({ error: null }),
1132
1147
  resend: notConfigured,
1133
- onAuthStateChange: () => ({ data: { subscription: { unsubscribe: () => {} } } }),
1148
+ onAuthStateChange: () => ({
1149
+ data: { subscription: { unsubscribe: () => {} } },
1150
+ error: null
1151
+ }),
1134
1152
  startAutoRefresh: () => Promise.resolve(),
1135
1153
  stopAutoRefresh: () => {},
1136
1154
  },
1137
- from: () => ({
1138
- select: () => notConfigured(),
1139
- eq: () => ({ single: () => notConfigured() }),
1155
+ from: (table: string) => ({
1156
+ select: () => ({
1157
+ eq: () => ({
1158
+ single: () => emptyResult(),
1159
+ maybeSingle: () => emptyResult(),
1160
+ }),
1161
+ order: () => ({
1162
+ eq: () => emptyArray(),
1163
+ }),
1140
1164
  }),
1141
- } as unknown as SupabaseClient<Database>;
1142
- };
1165
+ insert: () => notConfigured(),
1166
+ update: () => notConfigured(),
1167
+ delete: () => notConfigured(),
1168
+ upsert: () => notConfigured(),
1169
+ }),
1170
+ rpc: () => Promise.resolve({ data: null, error: new Error(missingEnvMessage) }),
1171
+ } as unknown as SupabaseClient<Database>;
1143
1172
  };
1144
1173
 
1145
1174
  export const supabase = (() => {
1146
1175
  if (supabaseInstance) return supabaseInstance;
1147
1176
 
1148
1177
  if (!hasSupabaseEnv) {
1149
- if (typeof window !== 'undefined' && import.meta.env.DEV) {
1150
- console.warn('\u26A0\uFE0F Supabase env vars missing. App will continue without authentication.');
1151
- }
1152
1178
  supabaseInstance = createMissingEnvClient();
1153
1179
  return supabaseInstance;
1154
1180
  }
@@ -1196,7 +1222,8 @@ function generateSessionProvider(projectDir, supabaseDir) {
1196
1222
  const content = `import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
1197
1223
  import { useNavigate } from 'react-router-dom';
1198
1224
  import { User } from '@supabase/supabase-js';
1199
- import { supabase, hasSupabaseEnv } from '../client.js';
1225
+ import { supabase } from '../client';
1226
+ import { hasSupabaseEnv } from '../env';
1200
1227
 
1201
1228
  export interface GMooncUser {
1202
1229
  id: string;
@@ -1418,8 +1445,8 @@ export function useGMooncSession(): GMooncSessionContextType {
1418
1445
  }
1419
1446
  function generateRbacHelpers(projectDir, supabaseDir) {
1420
1447
  const getMyProfilePath = (0, import_path6.join)(supabaseDir, "rbac/getMyProfile.ts");
1421
- const getMyProfileContent = `import { supabase } from '../client.js';
1422
- import type { Database } from '../client.js';
1448
+ const getMyProfileContent = `import { supabase } from '../client';
1449
+ import type { Database } from '../client';
1423
1450
 
1424
1451
  type Profile = Database['public']['Tables']['profiles']['Row'];
1425
1452
 
@@ -1456,7 +1483,7 @@ export async function getMyProfile(): Promise<Profile | null> {
1456
1483
  writeFileSafe(getMyProfilePath, getMyProfileContent);
1457
1484
  logSuccess("Generated src/gmoonc/supabase/rbac/getMyProfile.ts");
1458
1485
  const hasPermissionPath = (0, import_path6.join)(supabaseDir, "rbac/hasPermission.ts");
1459
- const hasPermissionContent = `import { supabase } from '../client.js';
1486
+ const hasPermissionContent = `import { supabase } from '../client';
1460
1487
 
1461
1488
  /**
1462
1489
  * Check if the current user has a specific permission
@@ -1545,7 +1572,7 @@ export function hasPermissionSync(
1545
1572
  writeFileSafe(hasPermissionPath, hasPermissionContent);
1546
1573
  logSuccess("Generated src/gmoonc/supabase/rbac/hasPermission.ts");
1547
1574
  const getUserPermissionsPath = (0, import_path6.join)(supabaseDir, "rbac/getUserPermissions.ts");
1548
- const getUserPermissionsContent = `import { supabase } from '../client.js';
1575
+ const getUserPermissionsContent = `import { supabase } from '../client';
1549
1576
 
1550
1577
  export interface UserPermission {
1551
1578
  module_name: string;
@@ -1589,10 +1616,10 @@ export async function getUserPermissions(): Promise<UserPermission[]> {
1589
1616
  writeFileSafe(getUserPermissionsPath, getUserPermissionsContent);
1590
1617
  logSuccess("Generated src/gmoonc/supabase/rbac/getUserPermissions.ts");
1591
1618
  const rbacIndexPath = (0, import_path6.join)(supabaseDir, "rbac/index.ts");
1592
- const rbacIndexContent = `export { getMyProfile } from './getMyProfile.js';
1593
- export { hasPermission, hasPermissionSync } from './hasPermission.js';
1594
- export { getUserPermissions } from './getUserPermissions.js';
1595
- export type { UserPermission } from './getUserPermissions.js';
1619
+ const rbacIndexContent = `export { getMyProfile } from './getMyProfile';
1620
+ export { hasPermission, hasPermissionSync } from './hasPermission';
1621
+ export { getUserPermissions } from './getUserPermissions';
1622
+ export type { UserPermission } from './getUserPermissions';
1596
1623
  `;
1597
1624
  writeFileSafe(rbacIndexPath, rbacIndexContent);
1598
1625
  logSuccess("Generated src/gmoonc/supabase/rbac/index.ts");
@@ -1638,18 +1665,143 @@ function patchExistingCode(projectDir, gmooncDir) {
1638
1665
  );
1639
1666
  content = content.replace(
1640
1667
  /import\s+{\s*GMooncSessionProvider[^}]*}\s+from\s+['"][^'"]*session\/GMooncSessionContext['"]/g,
1641
- "import { GMooncSupabaseSessionProvider } from '../supabase/auth/GMooncSupabaseSessionProvider'"
1668
+ "import { GMooncSupabaseSessionProvider, useGMooncSession } from '../supabase/auth/GMooncSupabaseSessionProvider'"
1642
1669
  );
1670
+ if (!content.includes("from 'react-router-dom'") || !content.includes("Navigate")) {
1671
+ content = content.replace(
1672
+ /import\s+{\s*([^}]*)\s*}\s+from\s+['"]react-router-dom['"]/,
1673
+ (match, imports) => {
1674
+ if (!imports.includes("Navigate")) {
1675
+ return `import { ${imports}, Navigate } from 'react-router-dom'`;
1676
+ }
1677
+ return match;
1678
+ }
1679
+ );
1680
+ }
1643
1681
  content = content.replace(/GMooncSessionProvider/g, "GMooncSupabaseSessionProvider");
1682
+ if (!content.includes("!isAuthenticated")) {
1683
+ content = content.replace(
1684
+ /const\s+{\s*roles,\s*logout\s*}\s+=\s+useGMooncSession\(\);/,
1685
+ "const { roles, logout, isLoading, isAuthenticated } = useGMooncSession();"
1686
+ );
1687
+ content = content.replace(
1688
+ /(\s+}, \[navigate, getBasePath\]\);)\s+(\s+return\s+\()/,
1689
+ `$1
1690
+
1691
+ // Show loading state while checking authentication (verificar primeiro)
1692
+ if (isLoading) {
1693
+ return (
1694
+ <div className="gmoonc-root">
1695
+ <div style={{
1696
+ display: 'flex',
1697
+ justifyContent: 'center',
1698
+ alignItems: 'center',
1699
+ height: '100vh',
1700
+ fontSize: 'var(--gmoonc-font-size-base, 16px)',
1701
+ color: 'var(--gmoonc-color-text, #333)'
1702
+ }}>
1703
+ Loading...
1704
+ </div>
1705
+ </div>
1706
+ );
1707
+ }
1708
+
1709
+ // Route protection: redirect to login if not authenticated
1710
+ if (!isAuthenticated) {
1711
+ return <Navigate to="/login" replace />;
1712
+ }
1713
+
1714
+ $2`
1715
+ );
1716
+ } else {
1717
+ content = content.replace(
1718
+ /\/\/ Route protection: redirect to login if not authenticated\s+if\s+\(!isLoading\s+&&\s+!isAuthenticated\)\s+\{[\s\S]*?return\s+<Navigate[^>]+>;[\s\S]*?\}\s+(\/\/ Show loading state)/,
1719
+ `// Show loading state while checking authentication (verificar primeiro)
1720
+ if (isLoading) {
1721
+ return (
1722
+ <div className="gmoonc-root">
1723
+ <div style={{
1724
+ display: 'flex',
1725
+ justifyContent: 'center',
1726
+ alignItems: 'center',
1727
+ height: '100vh',
1728
+ fontSize: 'var(--gmoonc-font-size-base, 16px)',
1729
+ color: 'var(--gmoonc-color-text, #333)'
1730
+ }}>
1731
+ Loading...
1732
+ </div>
1733
+ </div>
1734
+ );
1735
+ }
1736
+
1737
+ // Route protection: redirect to login if not authenticated
1738
+ if (!isAuthenticated) {
1739
+ return <Navigate to="/login" replace />;
1740
+ }
1741
+
1742
+ $1`
1743
+ );
1744
+ }
1644
1745
  writeFileSafe(layoutPath, content);
1645
- logSuccess("Patched layout/GMooncAppLayout.tsx to use GMooncSupabaseSessionProvider");
1746
+ logSuccess("Patched layout/GMooncAppLayout.tsx to use GMooncSupabaseSessionProvider and add route protection");
1747
+ }
1748
+ updateAllSessionImports(gmooncDir);
1749
+ }
1750
+ function updateAllSessionImports(gmooncDir) {
1751
+ const filesToUpdate = [];
1752
+ function findFiles(dir) {
1753
+ if (!(0, import_fs9.existsSync)(dir)) return;
1754
+ const entries = (0, import_fs9.readdirSync)(dir, { withFileTypes: true });
1755
+ for (const entry of entries) {
1756
+ const fullPath = (0, import_path6.join)(dir, entry.name);
1757
+ if (entry.name.includes(".bak-") || entry.name === "supabase") {
1758
+ continue;
1759
+ }
1760
+ if (entry.isDirectory()) {
1761
+ findFiles(fullPath);
1762
+ } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
1763
+ filesToUpdate.push(fullPath);
1764
+ }
1765
+ }
1766
+ }
1767
+ findFiles(gmooncDir);
1768
+ let updatedCount = 0;
1769
+ for (const filePath of filesToUpdate) {
1770
+ let content = (0, import_fs9.readFileSync)(filePath, "utf-8");
1771
+ let modified = false;
1772
+ const fileDir = (0, import_path6.dirname)(filePath);
1773
+ const supabaseProviderPath = (0, import_path6.join)(gmooncDir, "supabase", "auth", "GMooncSupabaseSessionProvider");
1774
+ let relativePath = (0, import_path6.relative)(fileDir, supabaseProviderPath).replace(/\\/g, "/");
1775
+ if (!relativePath.startsWith(".")) {
1776
+ relativePath = "./" + relativePath;
1777
+ }
1778
+ const oldImportPattern = /from\s+['"](\.\.?\/)+session\/GMooncSessionContext['"]/g;
1779
+ if (oldImportPattern.test(content)) {
1780
+ content = content.replace(
1781
+ /from\s+['"](\.\.?\/)+session\/GMooncSessionContext['"]/g,
1782
+ `from '${relativePath}'`
1783
+ );
1784
+ modified = true;
1785
+ }
1786
+ if (content.includes("GMooncSessionProvider") && !content.includes("GMooncSupabaseSessionProvider")) {
1787
+ content = content.replace(/GMooncSessionProvider/g, "GMooncSupabaseSessionProvider");
1788
+ modified = true;
1789
+ }
1790
+ if (modified) {
1791
+ writeFileSafe(filePath, content);
1792
+ updatedCount++;
1793
+ }
1794
+ }
1795
+ if (updatedCount > 0) {
1796
+ logSuccess(`Updated ${updatedCount} file(s) to use GMooncSupabaseSessionProvider`);
1797
+ } else {
1798
+ logInfo("All files already use GMooncSupabaseSessionProvider");
1646
1799
  }
1647
- logInfo("Auth pages will use Supabase via GMooncSupabaseSessionProvider");
1648
1800
  }
1649
1801
 
1650
1802
  // src/cli/index.ts
1651
1803
  var program = new import_commander.Command();
1652
- program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.17").option("--base <path>", "Base path for dashboard routes", "/app").option("--skip-router-patch", "Skip automatic router integration (only copy files and inject CSS)").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
1804
+ program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.19").option("--base <path>", "Base path for dashboard routes", "/app").option("--skip-router-patch", "Skip automatic router integration (only copy files and inject CSS)").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
1653
1805
  try {
1654
1806
  logInfo("\u{1F680} Starting gmoonc installer...");
1655
1807
  logInfo("\u{1F4E6} Installing complete dashboard into your React project\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gmoonc",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "Goalmoon Ctrl (gmoonc): Complete dashboard installer for React projects",
5
5
  "license": "MIT",
6
6
  "homepage": "https://gmoonc.com",
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback } from 'react';
2
- import { Outlet, useLocation, useNavigate, Link } from 'react-router-dom';
2
+ import { Outlet, useLocation, useNavigate, Link, Navigate } from 'react-router-dom';
3
3
  import { GmooncShell } from '../ui/shell';
4
4
  import { defaultConfig } from '../config/defaultConfig';
5
5
  import { GMooncSessionProvider, useGMooncSession } from '../session/GMooncSessionContext';
@@ -9,7 +9,7 @@ import logoUrl from '../assets/gmoonc-logo.png';
9
9
  function GMooncAppLayoutInner() {
10
10
  const location = useLocation();
11
11
  const navigate = useNavigate();
12
- const { roles, logout } = useGMooncSession();
12
+ const { roles, logout, isLoading, isAuthenticated } = useGMooncSession();
13
13
 
14
14
  // Determine basePath from current location
15
15
  // If path is /app/..., basePath is /app
@@ -47,6 +47,29 @@ function GMooncAppLayoutInner() {
47
47
  navigate(normalizedBasePath);
48
48
  }, [navigate, getBasePath]);
49
49
 
50
+ // Show loading state while checking authentication (verificar primeiro)
51
+ if (isLoading) {
52
+ return (
53
+ <div className="gmoonc-root">
54
+ <div style={{
55
+ display: 'flex',
56
+ justifyContent: 'center',
57
+ alignItems: 'center',
58
+ height: '100vh',
59
+ fontSize: 'var(--gmoonc-font-size-base, 16px)',
60
+ color: 'var(--gmoonc-color-text, #333)'
61
+ }}>
62
+ Loading...
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ // Route protection: redirect to login if not authenticated
69
+ if (!isAuthenticated) {
70
+ return <Navigate to="/login" replace />;
71
+ }
72
+
50
73
  return (
51
74
  <div className="gmoonc-root">
52
75
  <GmooncShell