@clubz/cli 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,82 @@
1
+ # @clubz/cli
2
+
3
+ The official Command Line Interface for building widgets and pages for the **Clubz Community Builder**.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @clubz/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Create a new Widget
14
+
15
+ Scaffold a new widget project with the official React + Vite + TypeScript template.
16
+
17
+ ```bash
18
+ clubz init my-awesome-widget
19
+ cd my-awesome-widget
20
+ npm install
21
+ ```
22
+
23
+ ### 2. Develop Locally (Simulator)
24
+
25
+ Start the local development server. This launches a **Local Simulator** that mimics the Clubz mobile environment (IFrame, SDK communication, etc.), allowing you to test your widget in isolation.
26
+
27
+ ```bash
28
+ npm run dev
29
+ # OR
30
+ clubz dev
31
+ ```
32
+
33
+ The simulator will open at `http://localhost:3000` (or the next available port).
34
+
35
+ ### 3. Deploy
36
+
37
+ Package your widget and deploy it to the Clubz platform.
38
+
39
+ ```bash
40
+ npm run deploy
41
+ # OR
42
+ clubz deploy
43
+ ```
44
+
45
+ ## Project Structure
46
+
47
+ A standard Clubz widget project looks like this:
48
+
49
+ ```
50
+ my-widget/
51
+ ├── src/
52
+ │ ├── App.tsx # Your main widget code
53
+ │ └── main.tsx # Entry point
54
+ ├── clubz.json # Widget metadata (name, version, permissions)
55
+ ├── package.json
56
+ └── vite.config.ts # Vite configuration
57
+ ```
58
+
59
+ ## SDK Integration
60
+
61
+ The CLI templates come pre-configured with `@clubz/sdk`. Use it to interact with the host application:
62
+
63
+ ```ts
64
+ import { bridge } from '@clubz/sdk';
65
+
66
+ // Get user info
67
+ const user = await bridge.getUser();
68
+
69
+ // Show a native toast
70
+ await bridge.showToast({ message: 'Hello from Widget!', type: 'success' });
71
+ ```
72
+
73
+ ## Contributing
74
+
75
+ This CLI is part of the Clubz monorepo.
76
+
77
+ ### Local Setup
78
+
79
+ 1. Clone the repository.
80
+ 2. Install dependencies: `npm install`
81
+ 3. Build the CLI: `npm run build`
82
+ 4. Link globally for testing: `npm link`
@@ -9,6 +9,8 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
10
  const child_process_1 = require("child_process");
11
11
  const archiver_1 = __importDefault(require("archiver"));
12
+ const axios_1 = __importDefault(require("axios"));
13
+ const form_data_1 = __importDefault(require("form-data"));
12
14
  async function deployCommand() {
13
15
  console.log(chalk_1.default.blue('🚀 Deploying Widget to Clubz...'));
14
16
  const cwd = process.cwd();
@@ -39,36 +41,38 @@ async function deployCommand() {
39
41
  }
40
42
  await zipDirectory(distDir, zipPath);
41
43
  console.log(chalk_1.default.green(` Bundle created: ${getSize(zipPath)}`));
42
- // 4. Upload (Mock for now, replacing previous simple push)
44
+ // 4. Upload to Clubz Registry
43
45
  console.log(chalk_1.default.yellow('\n☁️ Uploading to Clubz Registry...'));
44
46
  try {
47
+ // Load .env if present
48
+ const userEnvPath = path_1.default.join(cwd, '.env');
49
+ if (fs_extra_1.default.existsSync(userEnvPath)) {
50
+ require('dotenv').config({ path: userEnvPath });
51
+ }
45
52
  const apiKey = process.env.CLUBZ_API_KEY;
53
+ const apiUrl = process.env.CLUBZ_API_URL || 'http://localhost:3000'; // Default to Community Builder for now
46
54
  if (!apiKey) {
47
- console.warn(chalk_1.default.yellow('⚠️ No CLUBZ_API_KEY found in env. simulating upload...'));
55
+ console.error(chalk_1.default.red(' Authentication required.'));
56
+ console.error(chalk_1.default.yellow(' Please add CLUBZ_API_KEY to your .env file.'));
57
+ process.exit(1);
48
58
  }
49
59
  const isSubmission = process.argv.includes('--submit');
50
60
  const status = isSubmission ? 'pending' : 'draft';
51
- // Real implementation would look like this:
52
- /*
53
- const form = new FormData();
54
- form.append('file', fs.createReadStream(zipPath));
61
+ const form = new form_data_1.default();
62
+ form.append('file', fs_extra_1.default.createReadStream(zipPath));
55
63
  form.append('manifest', JSON.stringify({ ...config, status }));
56
-
57
- await axios.post('http://localhost:3000/api/registry/deploy', form, {
64
+ const response = await axios_1.default.post(`${apiUrl}/api/registry/deploy`, form, {
58
65
  headers: {
59
66
  ...form.getHeaders(),
60
67
  'Authorization': `Bearer ${apiKey}`
61
- }
68
+ },
69
+ maxContentLength: Infinity,
70
+ maxBodyLength: Infinity
62
71
  });
63
- */
64
- // Simulating network delay
65
- await new Promise(r => setTimeout(r, 1500));
66
72
  console.log(chalk_1.default.green('\n✅ Deployment Successful!'));
67
- console.log(chalk_1.default.cyan(` Widget ID: ${config.type}-${Date.now().toString().slice(-4)}`));
68
- console.log(chalk_1.default.cyan(` Version: ${config.version}`));
69
- if (config.tags && config.tags.length > 0) {
70
- console.log(chalk_1.default.cyan(` Tags: ${config.tags.join(', ')}`));
71
- }
73
+ console.log(chalk_1.default.cyan(` Widget ID: ${response.data.widgetId}`));
74
+ console.log(chalk_1.default.cyan(` Version: ${response.data.version}`));
75
+ console.log(chalk_1.default.dim(` Download URL: ${response.data.url}`));
72
76
  if (isSubmission) {
73
77
  console.log(chalk_1.default.magenta(' 🚀 Submitted for validation (Status: Pending)'));
74
78
  console.log(chalk_1.default.dim(' An admin will review your widget shortly.'));
@@ -81,7 +85,14 @@ async function deployCommand() {
81
85
  await fs_extra_1.default.remove(zipPath);
82
86
  }
83
87
  catch (error) {
84
- console.error(chalk_1.default.red('❌ Upload failed'), error.message);
88
+ console.error(chalk_1.default.red('❌ Upload failed'));
89
+ if (error.response) {
90
+ console.error(chalk_1.default.red(` Server Error: ${error.response.status} ${error.response.statusText}`));
91
+ console.error(chalk_1.default.red(` ${JSON.stringify(error.response.data)}`));
92
+ }
93
+ else {
94
+ console.error(chalk_1.default.red(` ${error.message}`));
95
+ }
85
96
  process.exit(1);
86
97
  }
87
98
  }
@@ -9,11 +9,19 @@ const express_1 = __importDefault(require("express"));
9
9
  const cross_spawn_1 = __importDefault(require("cross-spawn"));
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
- // In CommonJS (which we compile to), __dirname is available globally
13
- // const __filename = fileURLToPath(import.meta.url); // Not needed
14
- // const __dirname = path.dirname(__filename); // Not needed
12
+ const dotenv_1 = __importDefault(require("dotenv"));
13
+ const axios_1 = __importDefault(require("axios"));
15
14
  async function devCommand() {
16
15
  console.log(chalk_1.default.blue('🚀 Starting Clubz Development Environment...'));
16
+ // 0. Load Environment Variables from the User's Project
17
+ const userEnvPath = path_1.default.join(process.cwd(), '.env');
18
+ if (fs_1.default.existsSync(userEnvPath)) {
19
+ dotenv_1.default.config({ path: userEnvPath });
20
+ console.log(chalk_1.default.green('✅ Loaded .env file'));
21
+ }
22
+ else {
23
+ console.log(chalk_1.default.yellow('⚠️ No .env file found. API Key integration will be disabled.'));
24
+ }
17
25
  // 1. Start User's Widget Server (Vite)
18
26
  const widgetPort = 3001;
19
27
  console.log(chalk_1.default.dim(` Starting widget server on port ${widgetPort}...`));
@@ -26,24 +34,45 @@ async function devCommand() {
26
34
  });
27
35
  // 2. Start Simulator Server
28
36
  const app = (0, express_1.default)();
29
- const simulatorPort = 3000;
37
+ const simulatorPort = 3002; // Changed from 3000 to avoid conflict with API
38
+ const realApiUrl = process.env.CLUBZ_API_URL || 'http://localhost:3000';
39
+ // API Route for Simulator to get Configuration
40
+ app.get('/simulator/api/config', async (req, res) => {
41
+ const apiKey = process.env.CLUBZ_API_KEY;
42
+ let user = null;
43
+ if (apiKey) {
44
+ try {
45
+ // Determine if apiKey is a JWT or a special key.
46
+ // For now, assuming it's a Bearer Token (Personal Access Token)
47
+ const response = await axios_1.default.get(`${realApiUrl}/api/auth/me`, {
48
+ headers: { Authorization: `Bearer ${apiKey}` }
49
+ });
50
+ user = response.data;
51
+ // Add role/permissions if needed
52
+ }
53
+ catch (error) {
54
+ console.error(chalk_1.default.red('❌ Failed to validate API Key:'), error.message);
55
+ // Don't fail the request, just return guest mode with error
56
+ }
57
+ }
58
+ res.json({
59
+ user: user || { id: 'guest', name: 'Guest Developer', role: 'guest' },
60
+ mode: apiKey ? 'authenticated' : 'guest',
61
+ apiUrl: realApiUrl
62
+ });
63
+ });
30
64
  // Resolve path to built simulator client assets
31
- // In dist structure: dist/src/commands/dev.js -> dist/simulator/client
32
- // adjustable based on actual build output structure
33
65
  let simulatorDist = path_1.default.join(__dirname, '../../simulator/client');
34
- // Fallback for dev execution (ts-node)
35
66
  if (!fs_1.default.existsSync(simulatorDist) || !fs_1.default.existsSync(path_1.default.join(simulatorDist, 'index.html'))) {
36
- // If not built, maybe we can warn or try source (but source needs building)
37
- // Check relative to src: src/commands/dev.ts -> src/simulator/client (source)
38
- // For now, let's assume we need to build it.
39
- // We can point to a mocked path or ensure build happens.
40
67
  console.warn(chalk_1.default.yellow('⚠️ Simulator build not found. Ensure CLI is built.'));
41
- // Try pointing to package root dist if structure is flattened
42
68
  simulatorDist = path_1.default.join(__dirname, '../../../dist/simulator/client');
43
69
  }
44
70
  app.use(express_1.default.static(simulatorDist));
45
- // Fallback to index.html for SPA routing
46
71
  app.get('*', (req, res) => {
72
+ // handle api routes not found
73
+ if (req.path.startsWith('/simulator/api')) {
74
+ return res.status(404).json({ error: 'Not Found' });
75
+ }
47
76
  res.sendFile(path_1.default.join(simulatorDist, 'index.html'));
48
77
  });
49
78
  app.listen(simulatorPort, () => {
@@ -51,7 +80,6 @@ async function devCommand() {
51
80
  console.log(chalk_1.default.cyan(` Widget running at http://localhost:${widgetPort}`));
52
81
  console.log(chalk_1.default.dim(' Press Ctrl+C to stop.'));
53
82
  });
54
- // Handle cleanup
55
83
  process.on('SIGINT', () => {
56
84
  widgetProcess.kill();
57
85
  process.exit();
@@ -31,8 +31,8 @@ async function initCommand(name) {
31
31
  let templateDir = path_1.default.join(__dirname, '../../templates', templateName); // from dist/commands or src/commands
32
32
  // Fallback if structure is different (e.g. src vs dist) - explicit check
33
33
  if (!fs_extra_1.default.existsSync(templateDir)) {
34
- // Try source path if we are running ts-node directly?
35
- templateDir = path_1.default.join(__dirname, '../../../src/templates', templateName);
34
+ // Try source path if we are running from dist but templates are in src (common in local dev)
35
+ templateDir = path_1.default.join(__dirname, '../../src/templates', templateName);
36
36
  }
37
37
  if (!fs_extra_1.default.existsSync(templateDir)) {
38
38
  console.error(chalk_1.default.red(`❌ Template not found at ${templateDir}`));
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "My Amazing Widget",
3
+ "description": "A widget built for Clubz",
4
+ "version": "0.0.1",
5
+ "type": "widget",
6
+ "config": {
7
+ "props": [
8
+ {
9
+ "name": "title",
10
+ "type": "string",
11
+ "label": "Titre du widget",
12
+ "default": "Hello World"
13
+ }
14
+ ]
15
+ },
16
+ "permissions": [],
17
+ "tags": [
18
+ "beta",
19
+ "react"
20
+ ]
21
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Clubz Widget</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ <script src="https://cdn.tailwindcss.com"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "my-clubz-widget",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "deploy": "clubz deploy --submit",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^18.2.0",
14
+ "react-dom": "^18.2.0",
15
+ "@clubz/sdk": "latest"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.2.66",
19
+ "@types/react-dom": "^18.2.22",
20
+ "@vitejs/plugin-react": "^4.2.1",
21
+ "typescript": "^5.2.2",
22
+ "vite": "^5.2.0"
23
+ }
24
+ }
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { bridge } from '@clubz/sdk'
3
+
4
+ function App() {
5
+ const [user, setUser] = useState<{ name: string } | null>(null)
6
+
7
+ useEffect(() => {
8
+ // Determine user context via Bridge
9
+ bridge.getUser().then((u: any) => setUser(u)).catch(() => console.log('Guest mode'))
10
+ }, [])
11
+
12
+ const handleVibrate = () => {
13
+ bridge.vibrate();
14
+ }
15
+
16
+ return (
17
+ <div className="w-full h-full min-h-[200px] bg-white rounded-xl p-6 border border-slate-200 flex flex-col items-center justify-center">
18
+ <h1 className="text-xl font-bold text-slate-800 mb-2">
19
+ Hello {user ? user.name : 'Guest'}!
20
+ </h1>
21
+ <p className="text-slate-500 text-center mb-4">
22
+ Welcome to your new Clubz Widget.
23
+ </p>
24
+
25
+ <button
26
+ onClick={handleVibrate}
27
+ className="px-4 py-2 bg-blue-600 text-white rounded-lg active:bg-blue-700 transition"
28
+ >
29
+ Test Vibration
30
+ </button>
31
+ </div>
32
+ )
33
+ }
34
+
35
+ export default App
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.tsx'
4
+
5
+ ReactDOM.createRoot(document.getElementById('root')!).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>,
9
+ )
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": [
6
+ "ES2020",
7
+ "DOM",
8
+ "DOM.Iterable"
9
+ ],
10
+ "module": "ESNext",
11
+ "skipLibCheck": true,
12
+ /* Bundler mode */
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "noEmit": true,
18
+ "jsx": "react-jsx",
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true
24
+ },
25
+ "include": [
26
+ "src"
27
+ ],
28
+ "references": [
29
+ {
30
+ "path": "./tsconfig.node.json"
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": [
10
+ "vite.config.ts"
11
+ ]
12
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ build: {
8
+ rollupOptions: {
9
+ output: {
10
+ // Ensure we get a single recognizable entry point if possible,
11
+ // though standard vite build is fine for the platform zipper.
12
+ entryFileNames: 'assets/[name].js',
13
+ chunkFileNames: 'assets/[name].js',
14
+ assetFileNames: 'assets/[name].[ext]'
15
+ }
16
+ }
17
+ }
18
+ })
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@clubz/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "bin": {
5
5
  "clubz": "bin/clubz.js"
6
6
  },
7
7
  "main": "dist/index.js",
8
8
  "scripts": {
9
- "build": "tsc",
9
+ "build": "tsc && cp -r src/templates dist/templates",
10
10
  "dev": "tsc -w"
11
11
  },
12
12
  "publishConfig": {
@@ -22,6 +22,7 @@
22
22
  "chalk": "^4.1.2",
23
23
  "commander": "^11.0.0",
24
24
  "cross-spawn": "^7.0.6",
25
+ "dotenv": "^17.2.4",
25
26
  "express": "^5.2.1",
26
27
  "form-data": "^4.0.5",
27
28
  "fs-extra": "^11.3.3",
@@ -42,41 +42,45 @@ export async function deployCommand() {
42
42
  await zipDirectory(distDir, zipPath);
43
43
  console.log(chalk.green(` Bundle created: ${getSize(zipPath)}`));
44
44
 
45
- // 4. Upload (Mock for now, replacing previous simple push)
45
+ // 4. Upload to Clubz Registry
46
46
  console.log(chalk.yellow('\n☁️ Uploading to Clubz Registry...'));
47
47
 
48
48
  try {
49
+ // Load .env if present
50
+ const userEnvPath = path.join(cwd, '.env');
51
+ if (fs.existsSync(userEnvPath)) {
52
+ require('dotenv').config({ path: userEnvPath });
53
+ }
54
+
49
55
  const apiKey = process.env.CLUBZ_API_KEY;
56
+ const apiUrl = process.env.CLUBZ_API_URL || 'http://localhost:3000'; // Default to Community Builder for now
57
+
50
58
  if (!apiKey) {
51
- console.warn(chalk.yellow('⚠️ No CLUBZ_API_KEY found in env. simulating upload...'));
59
+ console.error(chalk.red(' Authentication required.'));
60
+ console.error(chalk.yellow(' Please add CLUBZ_API_KEY to your .env file.'));
61
+ process.exit(1);
52
62
  }
53
63
 
54
64
  const isSubmission = process.argv.includes('--submit');
55
65
  const status = isSubmission ? 'pending' : 'draft';
56
66
 
57
- // Real implementation would look like this:
58
- /*
59
67
  const form = new FormData();
60
68
  form.append('file', fs.createReadStream(zipPath));
61
69
  form.append('manifest', JSON.stringify({ ...config, status }));
62
-
63
- await axios.post('http://localhost:3000/api/registry/deploy', form, {
64
- headers: {
70
+
71
+ const response = await axios.post(`${apiUrl}/api/registry/deploy`, form, {
72
+ headers: {
65
73
  ...form.getHeaders(),
66
74
  'Authorization': `Bearer ${apiKey}`
67
- }
75
+ },
76
+ maxContentLength: Infinity,
77
+ maxBodyLength: Infinity
68
78
  });
69
- */
70
-
71
- // Simulating network delay
72
- await new Promise(r => setTimeout(r, 1500));
73
79
 
74
80
  console.log(chalk.green('\n✅ Deployment Successful!'));
75
- console.log(chalk.cyan(` Widget ID: ${config.type}-${Date.now().toString().slice(-4)}`));
76
- console.log(chalk.cyan(` Version: ${config.version}`));
77
- if (config.tags && config.tags.length > 0) {
78
- console.log(chalk.cyan(` Tags: ${config.tags.join(', ')}`));
79
- }
81
+ console.log(chalk.cyan(` Widget ID: ${response.data.widgetId}`));
82
+ console.log(chalk.cyan(` Version: ${response.data.version}`));
83
+ console.log(chalk.dim(` Download URL: ${response.data.url}`));
80
84
 
81
85
  if (isSubmission) {
82
86
  console.log(chalk.magenta(' 🚀 Submitted for validation (Status: Pending)'));
@@ -91,7 +95,13 @@ export async function deployCommand() {
91
95
  await fs.remove(zipPath);
92
96
 
93
97
  } catch (error: any) {
94
- console.error(chalk.red('❌ Upload failed'), error.message);
98
+ console.error(chalk.red('❌ Upload failed'));
99
+ if (error.response) {
100
+ console.error(chalk.red(` Server Error: ${error.response.status} ${error.response.statusText}`));
101
+ console.error(chalk.red(` ${JSON.stringify(error.response.data)}`));
102
+ } else {
103
+ console.error(chalk.red(` ${error.message}`));
104
+ }
95
105
  process.exit(1);
96
106
  }
97
107
  }
@@ -3,14 +3,21 @@ import express from 'express';
3
3
  import spawn from 'cross-spawn';
4
4
  import chalk from 'chalk';
5
5
  import fs from 'fs';
6
-
7
- // In CommonJS (which we compile to), __dirname is available globally
8
- // const __filename = fileURLToPath(import.meta.url); // Not needed
9
- // const __dirname = path.dirname(__filename); // Not needed
6
+ import dotenv from 'dotenv';
7
+ import axios from 'axios';
10
8
 
11
9
  export async function devCommand() {
12
10
  console.log(chalk.blue('🚀 Starting Clubz Development Environment...'));
13
11
 
12
+ // 0. Load Environment Variables from the User's Project
13
+ const userEnvPath = path.join(process.cwd(), '.env');
14
+ if (fs.existsSync(userEnvPath)) {
15
+ dotenv.config({ path: userEnvPath });
16
+ console.log(chalk.green('✅ Loaded .env file'));
17
+ } else {
18
+ console.log(chalk.yellow('⚠️ No .env file found. API Key integration will be disabled.'));
19
+ }
20
+
14
21
  // 1. Start User's Widget Server (Vite)
15
22
  const widgetPort = 3001;
16
23
  console.log(chalk.dim(` Starting widget server on port ${widgetPort}...`));
@@ -26,28 +33,51 @@ export async function devCommand() {
26
33
 
27
34
  // 2. Start Simulator Server
28
35
  const app = express();
29
- const simulatorPort = 3000;
36
+ const simulatorPort = 3002; // Changed from 3000 to avoid conflict with API
37
+ const realApiUrl = process.env.CLUBZ_API_URL || 'http://localhost:3000';
38
+
39
+ // API Route for Simulator to get Configuration
40
+ app.get('/simulator/api/config', async (req, res) => {
41
+ const apiKey = process.env.CLUBZ_API_KEY;
42
+ let user = null;
43
+
44
+ if (apiKey) {
45
+ try {
46
+ // Determine if apiKey is a JWT or a special key.
47
+ // For now, assuming it's a Bearer Token (Personal Access Token)
48
+ const response = await axios.get(`${realApiUrl}/api/auth/me`, {
49
+ headers: { Authorization: `Bearer ${apiKey}` }
50
+ });
51
+ user = response.data;
52
+ // Add role/permissions if needed
53
+ } catch (error: any) {
54
+ console.error(chalk.red('❌ Failed to validate API Key:'), error.message);
55
+ // Don't fail the request, just return guest mode with error
56
+ }
57
+ }
58
+
59
+ res.json({
60
+ user: user || { id: 'guest', name: 'Guest Developer', role: 'guest' },
61
+ mode: apiKey ? 'authenticated' : 'guest',
62
+ apiUrl: realApiUrl
63
+ });
64
+ });
30
65
 
31
66
  // Resolve path to built simulator client assets
32
- // In dist structure: dist/src/commands/dev.js -> dist/simulator/client
33
- // adjustable based on actual build output structure
34
67
  let simulatorDist = path.join(__dirname, '../../simulator/client');
35
68
 
36
- // Fallback for dev execution (ts-node)
37
69
  if (!fs.existsSync(simulatorDist) || !fs.existsSync(path.join(simulatorDist, 'index.html'))) {
38
- // If not built, maybe we can warn or try source (but source needs building)
39
- // Check relative to src: src/commands/dev.ts -> src/simulator/client (source)
40
- // For now, let's assume we need to build it.
41
- // We can point to a mocked path or ensure build happens.
42
70
  console.warn(chalk.yellow('⚠️ Simulator build not found. Ensure CLI is built.'));
43
- // Try pointing to package root dist if structure is flattened
44
71
  simulatorDist = path.join(__dirname, '../../../dist/simulator/client');
45
72
  }
46
73
 
47
74
  app.use(express.static(simulatorDist));
48
75
 
49
- // Fallback to index.html for SPA routing
50
76
  app.get('*', (req, res) => {
77
+ // handle api routes not found
78
+ if (req.path.startsWith('/simulator/api')) {
79
+ return res.status(404).json({ error: 'Not Found' });
80
+ }
51
81
  res.sendFile(path.join(simulatorDist, 'index.html'));
52
82
  });
53
83
 
@@ -57,7 +87,6 @@ export async function devCommand() {
57
87
  console.log(chalk.dim(' Press Ctrl+C to stop.'));
58
88
  });
59
89
 
60
- // Handle cleanup
61
90
  process.on('SIGINT', () => {
62
91
  widgetProcess.kill();
63
92
  process.exit();
@@ -33,8 +33,8 @@ export async function initCommand(name: string) {
33
33
 
34
34
  // Fallback if structure is different (e.g. src vs dist) - explicit check
35
35
  if (!fs.existsSync(templateDir)) {
36
- // Try source path if we are running ts-node directly?
37
- templateDir = path.join(__dirname, '../../../src/templates', templateName);
36
+ // Try source path if we are running from dist but templates are in src (common in local dev)
37
+ templateDir = path.join(__dirname, '../../src/templates', templateName);
38
38
  }
39
39
 
40
40
  if (!fs.existsSync(templateDir)) {
@@ -9,12 +9,29 @@ export default function SimulatorApp() {
9
9
  // State
10
10
  const [widgetUrl, setWidgetUrl] = useState('http://localhost:3001'); // Default widget port
11
11
  const [logs, setLogs] = useState<{ time: string, type: 'req' | 'res', msg: string }[]>([]);
12
+ const [userContext, setUserContext] = useState<any>({ id: 'sim-user-1', name: 'Simulator User (Guest)', role: 'guest' });
13
+ const [authStatus, setAuthStatus] = useState<'loading' | 'authenticated' | 'guest'>('loading');
12
14
 
13
15
  // Iframe ref to send responses back
14
16
  const iframeRef = useRef<HTMLIFrameElement>(null);
15
17
 
16
- // Initial load
18
+ // Initial load & Config Fetch
17
19
  useEffect(() => {
20
+ // Fetch Simulator Config
21
+ fetch('/simulator/api/config')
22
+ .then(res => res.json())
23
+ .then(data => {
24
+ if (data.user) {
25
+ setUserContext(data.user);
26
+ }
27
+ setAuthStatus(data.mode);
28
+ log('sys', `Loaded configuration: ${data.mode}`);
29
+ })
30
+ .catch(err => {
31
+ console.error('Failed to load simulator config', err);
32
+ setAuthStatus('guest');
33
+ });
34
+
18
35
  const handleMessage = (event: MessageEvent) => {
19
36
  const data = event.data as BridgeRequest;
20
37
 
@@ -31,7 +48,15 @@ export default function SimulatorApp() {
31
48
  return () => window.removeEventListener('message', handleMessage);
32
49
  }, []);
33
50
 
34
- const log = (type: 'req' | 'res', msg: string) => {
51
+ // Update handler to use current state (via ref or dependency)
52
+ // Actually we need to be careful with stale closures in event listeners.
53
+ // The easiest way is to use a ref for userContext or pass it to the handler if it was stable.
54
+ // But since the listener is added once, it will see stale userContext.
55
+ // Let's use a ref for userContext.
56
+ const userContextRef = useRef(userContext);
57
+ useEffect(() => { userContextRef.current = userContext; }, [userContext]);
58
+
59
+ const log = (type: 'req' | 'res' | 'sys', msg: string) => {
35
60
  setLogs(prev => [{
36
61
  time: new Date().toLocaleTimeString().split(' ')[0],
37
62
  type,
@@ -45,7 +70,8 @@ export default function SimulatorApp() {
45
70
 
46
71
  switch (req.action) {
47
72
  case 'GET_USER':
48
- responseData = { id: 'sim-user-1', name: 'Simulator User', role: 'admin' };
73
+ // Use the ref to get the latest user context
74
+ responseData = userContextRef.current;
49
75
  break;
50
76
  case 'VIBRATE':
51
77
  // Visual feedback
@@ -90,7 +116,14 @@ export default function SimulatorApp() {
90
116
  />
91
117
  </div>
92
118
  </div>
93
- <p className="mt-4 text-slate-400 text-sm">iPhone 14 Pro Simulator</p>
119
+ <div className="mt-4 text-center">
120
+ <p className="text-slate-400 text-sm">iPhone 14 Pro Simulator</p>
121
+ <div className="mt-2 text-xs">
122
+ Status: <span className={authStatus === 'authenticated' ? 'text-green-600 font-bold' : 'text-orange-500'}>
123
+ {authStatus === 'authenticated' ? 'Logged In' : 'Guest Mode'}
124
+ </span>
125
+ </div>
126
+ </div>
94
127
  </div>
95
128
 
96
129
  {/* Right: Debug Console */}
@@ -114,8 +147,11 @@ export default function SimulatorApp() {
114
147
  {logs.map((l, i) => (
115
148
  <div key={i} className="flex gap-2">
116
149
  <span className="text-slate-500">[{l.time}]</span>
117
- <span className={l.type === 'req' ? 'text-blue-400' : 'text-green-400'}>
118
- {l.type === 'req' ? '' : '←'}
150
+ <span className={
151
+ l.type === 'req' ? 'text-blue-400' :
152
+ l.type === 'res' ? 'text-green-400' : 'text-yellow-400'
153
+ }>
154
+ {l.type === 'req' ? '→' : l.type === 'res' ? '←' : '•'}
119
155
  </span>
120
156
  <span className="text-slate-300 break-all">{l.msg}</span>
121
157
  </div>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "My Amazing Widget",
3
3
  "description": "A widget built for Clubz",
4
- "version": "1.0.0",
4
+ "version": "0.0.1",
5
5
  "type": "widget",
6
6
  "config": {
7
7
  "props": [
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "my-clubz-widget",
3
3
  "private": true,
4
- "version": "0.0.0",
4
+ "version": "0.0.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
8
  "build": "tsc && vite build",
9
+ "deploy": "clubz deploy --submit",
9
10
  "preview": "vite preview"
10
11
  },
11
12
  "dependencies": {
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": [
6
+ "ES2020",
7
+ "DOM",
8
+ "DOM.Iterable"
9
+ ],
10
+ "module": "ESNext",
11
+ "skipLibCheck": true,
12
+ /* Bundler mode */
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "noEmit": true,
18
+ "jsx": "react-jsx",
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true
24
+ },
25
+ "include": [
26
+ "src"
27
+ ],
28
+ "references": [
29
+ {
30
+ "path": "./tsconfig.node.json"
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": [
10
+ "vite.config.ts"
11
+ ]
12
+ }