@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 +82 -0
- package/dist/commands/deploy.js +29 -18
- package/dist/commands/dev.js +42 -14
- package/dist/commands/init.js +2 -2
- package/dist/templates/templates/react-vite-ts/clubz.json +21 -0
- package/dist/templates/templates/react-vite-ts/index.html +13 -0
- package/dist/templates/templates/react-vite-ts/package.json +24 -0
- package/dist/templates/templates/react-vite-ts/src/App.tsx +35 -0
- package/dist/templates/templates/react-vite-ts/src/main.tsx +9 -0
- package/dist/templates/templates/react-vite-ts/tsconfig.json +33 -0
- package/dist/templates/templates/react-vite-ts/tsconfig.node.json +12 -0
- package/dist/templates/templates/react-vite-ts/vite.config.ts +18 -0
- package/package.json +3 -2
- package/src/commands/deploy.ts +28 -18
- package/src/commands/dev.ts +44 -15
- package/src/commands/init.ts +2 -2
- package/src/simulator/client/src/SimulatorApp.tsx +42 -6
- package/src/templates/react-vite-ts/clubz.json +1 -1
- package/src/templates/react-vite-ts/package.json +2 -1
- package/src/templates/react-vite-ts/tsconfig.json +33 -0
- package/src/templates/react-vite-ts/tsconfig.node.json +12 -0
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`
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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: ${
|
|
68
|
-
console.log(chalk_1.default.cyan(` Version: ${
|
|
69
|
-
|
|
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')
|
|
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
|
}
|
package/dist/commands/dev.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
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();
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
35
|
-
templateDir = path_1.default.join(__dirname, '
|
|
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,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,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.
|
|
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",
|
package/src/commands/deploy.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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(
|
|
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: ${
|
|
76
|
-
console.log(chalk.cyan(` Version: ${
|
|
77
|
-
|
|
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')
|
|
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
|
}
|
package/src/commands/dev.ts
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
37
|
-
templateDir = path.join(__dirname, '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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={
|
|
118
|
-
|
|
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,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-clubz-widget",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "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
|
+
}
|