@clubz/cli 0.1.0
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/bin/clubz.js +4 -0
- package/dist/commands/deploy.js +106 -0
- package/dist/commands/dev.js +59 -0
- package/dist/commands/init.js +73 -0
- package/dist/index.js +26 -0
- package/dist/simulator/client/src/SimulatorApp.js +133 -0
- package/dist/simulator/client/src/main.js +11 -0
- package/dist/simulator/client/vite.config.js +15 -0
- package/dist/templates/react-vite-ts/src/App.js +27 -0
- package/dist/templates/react-vite-ts/src/main.js +11 -0
- package/dist/templates/react-vite-ts/vite.config.js +22 -0
- package/package.json +35 -0
- package/src/commands/deploy.ts +119 -0
- package/src/commands/dev.ts +65 -0
- package/src/commands/init.ts +83 -0
- package/src/index.ts +30 -0
- package/src/simulator/client/index.html +16 -0
- package/src/simulator/client/src/SimulatorApp.tsx +127 -0
- package/src/simulator/client/src/main.tsx +9 -0
- package/src/simulator/client/vite.config.ts +12 -0
- package/src/templates/react-vite-ts/clubz.json +21 -0
- package/src/templates/react-vite-ts/index.html +13 -0
- package/src/templates/react-vite-ts/package.json +23 -0
- package/src/templates/react-vite-ts/src/App.tsx +35 -0
- package/src/templates/react-vite-ts/src/main.tsx +9 -0
- package/src/templates/react-vite-ts/vite.config.ts +18 -0
- package/tsconfig.json +19 -0
package/bin/clubz.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.deployCommand = deployCommand;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const archiver_1 = __importDefault(require("archiver"));
|
|
12
|
+
async function deployCommand() {
|
|
13
|
+
console.log(chalk_1.default.blue('š Deploying Widget to Clubz...'));
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const configPath = path_1.default.join(cwd, 'clubz.json');
|
|
16
|
+
// 1. Validation
|
|
17
|
+
if (!fs_extra_1.default.existsSync(configPath)) {
|
|
18
|
+
console.error(chalk_1.default.red('ā clubz.json not found. Are you in a Clubz project?'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const config = await fs_extra_1.default.readJson(configPath);
|
|
22
|
+
console.log(chalk_1.default.dim(` Project: ${config.name} v${config.version}`));
|
|
23
|
+
// 2. Build
|
|
24
|
+
console.log(chalk_1.default.yellow('\nš¦ Building project...'));
|
|
25
|
+
try {
|
|
26
|
+
(0, child_process_1.execSync)('npm run build', { stdio: 'inherit' });
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.error(chalk_1.default.red('ā Build failed. Fix errors and try again.'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// 3. Zip Dist Folder
|
|
33
|
+
console.log(chalk_1.default.yellow('\nš¤ Zipping assets...'));
|
|
34
|
+
const distDir = path_1.default.join(cwd, 'dist');
|
|
35
|
+
const zipPath = path_1.default.join(cwd, 'bundle.zip');
|
|
36
|
+
if (!fs_extra_1.default.existsSync(distDir)) {
|
|
37
|
+
console.error(chalk_1.default.red('ā dist/ directory not found after build.'));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
await zipDirectory(distDir, zipPath);
|
|
41
|
+
console.log(chalk_1.default.green(` Bundle created: ${getSize(zipPath)}`));
|
|
42
|
+
// 4. Upload (Mock for now, replacing previous simple push)
|
|
43
|
+
console.log(chalk_1.default.yellow('\nāļø Uploading to Clubz Registry...'));
|
|
44
|
+
try {
|
|
45
|
+
const apiKey = process.env.CLUBZ_API_KEY;
|
|
46
|
+
if (!apiKey) {
|
|
47
|
+
console.warn(chalk_1.default.yellow('ā ļø No CLUBZ_API_KEY found in env. simulating upload...'));
|
|
48
|
+
}
|
|
49
|
+
const isSubmission = process.argv.includes('--submit');
|
|
50
|
+
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));
|
|
55
|
+
form.append('manifest', JSON.stringify({ ...config, status }));
|
|
56
|
+
|
|
57
|
+
await axios.post('http://localhost:3000/api/registry/deploy', form, {
|
|
58
|
+
headers: {
|
|
59
|
+
...form.getHeaders(),
|
|
60
|
+
'Authorization': `Bearer ${apiKey}`
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
*/
|
|
64
|
+
// Simulating network delay
|
|
65
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
66
|
+
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
|
+
}
|
|
72
|
+
if (isSubmission) {
|
|
73
|
+
console.log(chalk_1.default.magenta(' š Submitted for validation (Status: Pending)'));
|
|
74
|
+
console.log(chalk_1.default.dim(' An admin will review your widget shortly.'));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(chalk_1.default.dim(' Saved as Draft. PRO TIP: Use --submit to send for validation.'));
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk_1.default.dim(' Available in your "Submissions" tab.'));
|
|
80
|
+
// Cleanup
|
|
81
|
+
await fs_extra_1.default.remove(zipPath);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(chalk_1.default.red('ā Upload failed'), error.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function zipDirectory(source, out) {
|
|
89
|
+
const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
|
|
90
|
+
const stream = fs_extra_1.default.createWriteStream(out);
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
archive
|
|
93
|
+
.directory(source, false)
|
|
94
|
+
.on('error', err => reject(err))
|
|
95
|
+
.pipe(stream);
|
|
96
|
+
stream.on('close', () => resolve());
|
|
97
|
+
archive.finalize();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function getSize(filePath) {
|
|
101
|
+
const stats = fs_extra_1.default.statSync(filePath);
|
|
102
|
+
const bytes = stats.size;
|
|
103
|
+
if (bytes < 1024)
|
|
104
|
+
return bytes + ' B';
|
|
105
|
+
return (bytes / 1024).toFixed(2) + ' KB';
|
|
106
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.devCommand = devCommand;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const express_1 = __importDefault(require("express"));
|
|
9
|
+
const cross_spawn_1 = __importDefault(require("cross-spawn"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
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
|
|
15
|
+
async function devCommand() {
|
|
16
|
+
console.log(chalk_1.default.blue('š Starting Clubz Development Environment...'));
|
|
17
|
+
// 1. Start User's Widget Server (Vite)
|
|
18
|
+
const widgetPort = 3001;
|
|
19
|
+
console.log(chalk_1.default.dim(` Starting widget server on port ${widgetPort}...`));
|
|
20
|
+
const widgetProcess = (0, cross_spawn_1.default)('npm', ['run', 'dev', '--', '--port', widgetPort.toString()], {
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
env: { ...process.env, FORCE_COLOR: 'true' }
|
|
23
|
+
});
|
|
24
|
+
widgetProcess.on('error', (err) => {
|
|
25
|
+
console.error(chalk_1.default.red('ā Failed to start widget server'), err);
|
|
26
|
+
});
|
|
27
|
+
// 2. Start Simulator Server
|
|
28
|
+
const app = (0, express_1.default)();
|
|
29
|
+
const simulatorPort = 3000;
|
|
30
|
+
// 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
|
+
let simulatorDist = path_1.default.join(__dirname, '../../simulator/client');
|
|
34
|
+
// Fallback for dev execution (ts-node)
|
|
35
|
+
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
|
+
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
|
+
simulatorDist = path_1.default.join(__dirname, '../../../dist/simulator/client');
|
|
43
|
+
}
|
|
44
|
+
app.use(express_1.default.static(simulatorDist));
|
|
45
|
+
// Fallback to index.html for SPA routing
|
|
46
|
+
app.get('*', (req, res) => {
|
|
47
|
+
res.sendFile(path_1.default.join(simulatorDist, 'index.html'));
|
|
48
|
+
});
|
|
49
|
+
app.listen(simulatorPort, () => {
|
|
50
|
+
console.log(chalk_1.default.green(`\nš± Simulator running at http://localhost:${simulatorPort}`));
|
|
51
|
+
console.log(chalk_1.default.cyan(` Widget running at http://localhost:${widgetPort}`));
|
|
52
|
+
console.log(chalk_1.default.dim(' Press Ctrl+C to stop.'));
|
|
53
|
+
});
|
|
54
|
+
// Handle cleanup
|
|
55
|
+
process.on('SIGINT', () => {
|
|
56
|
+
widgetProcess.kill();
|
|
57
|
+
process.exit();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initCommand = initCommand;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
// In CommonJS, __dirname is available globally
|
|
13
|
+
async function initCommand(name) {
|
|
14
|
+
const targetDir = path_1.default.resolve(process.cwd(), name);
|
|
15
|
+
if (fs_extra_1.default.existsSync(targetDir)) {
|
|
16
|
+
console.error(chalk_1.default.red(`ā Directory ${name} already exists.`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
console.log(chalk_1.default.blue(`š Creating new Clubz widget project: ${name}`));
|
|
20
|
+
// 1. Copy Template
|
|
21
|
+
// In dev (ts-node/src), template is in src/templates
|
|
22
|
+
// In prod (dist), template is in dist/templates (need to ensure build copies it)
|
|
23
|
+
// For now we assume we are running from src or built dist where structure is preserved.
|
|
24
|
+
// Adjust path based on where we are executing.
|
|
25
|
+
// We need to resolve the template path relative to this file.
|
|
26
|
+
// If we are in dist/commands/init.js, template should be in ../templates/react-vite-ts relative to this file?
|
|
27
|
+
// No, standard is usually to keep templates as assets.
|
|
28
|
+
// Let's assume we are in src/commands/init.ts for dev flow or dist/commands/init.js
|
|
29
|
+
// Quick fix for path resolution in both envs: look for templates dir up the tree
|
|
30
|
+
const templateName = 'react-vite-ts';
|
|
31
|
+
let templateDir = path_1.default.join(__dirname, '../../templates', templateName); // from dist/commands or src/commands
|
|
32
|
+
// Fallback if structure is different (e.g. src vs dist) - explicit check
|
|
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);
|
|
36
|
+
}
|
|
37
|
+
if (!fs_extra_1.default.existsSync(templateDir)) {
|
|
38
|
+
console.error(chalk_1.default.red(`ā Template not found at ${templateDir}`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await fs_extra_1.default.copy(templateDir, targetDir);
|
|
43
|
+
// 2. Customize package.json
|
|
44
|
+
const pkgPath = path_1.default.join(targetDir, 'package.json');
|
|
45
|
+
const pkg = await fs_extra_1.default.readJson(pkgPath);
|
|
46
|
+
pkg.name = name;
|
|
47
|
+
await fs_extra_1.default.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
48
|
+
// 3. Rename .gitignore if needed (npm often renames it to .npmignore on publish)
|
|
49
|
+
const gitignorePath = path_1.default.join(targetDir, '_gitignore');
|
|
50
|
+
if (fs_extra_1.default.existsSync(gitignorePath)) {
|
|
51
|
+
await fs_extra_1.default.move(gitignorePath, path_1.default.join(targetDir, '.gitignore'));
|
|
52
|
+
}
|
|
53
|
+
console.log(chalk_1.default.green(`ā
Project created in ${targetDir}`));
|
|
54
|
+
// 4. Prompt for install
|
|
55
|
+
const { install } = await inquirer_1.default.prompt([{
|
|
56
|
+
type: 'confirm',
|
|
57
|
+
name: 'install',
|
|
58
|
+
message: 'Install dependencies now?',
|
|
59
|
+
default: true
|
|
60
|
+
}]);
|
|
61
|
+
if (install) {
|
|
62
|
+
console.log(chalk_1.default.yellow('š¦ Installing dependencies...'));
|
|
63
|
+
(0, child_process_1.execSync)('npm install', { cwd: targetDir, stdio: 'inherit' });
|
|
64
|
+
console.log(chalk_1.default.green('ā
Dependencies installed'));
|
|
65
|
+
}
|
|
66
|
+
console.log('\nNext steps:');
|
|
67
|
+
console.log(chalk_1.default.cyan(` cd ${name}`));
|
|
68
|
+
console.log(chalk_1.default.cyan(' npm run dev'));
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(chalk_1.default.red('ā Failed to create project'), e);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const init_1 = require("./commands/init");
|
|
5
|
+
const dev_1 = require("./commands/dev");
|
|
6
|
+
const deploy_1 = require("./commands/deploy");
|
|
7
|
+
const program = new commander_1.Command();
|
|
8
|
+
program
|
|
9
|
+
.name('clubz')
|
|
10
|
+
.description('Clubz Developer CLI')
|
|
11
|
+
.version('0.1.0');
|
|
12
|
+
program
|
|
13
|
+
.command('init')
|
|
14
|
+
.description('Initialize a new Clubz widget project')
|
|
15
|
+
.argument('<name>', 'Project name')
|
|
16
|
+
.action(init_1.initCommand);
|
|
17
|
+
program
|
|
18
|
+
.command('dev')
|
|
19
|
+
.description('Start development server with Simulator')
|
|
20
|
+
.action(dev_1.devCommand);
|
|
21
|
+
program
|
|
22
|
+
.command('deploy')
|
|
23
|
+
.description('Build and deploy widget to Clubz')
|
|
24
|
+
.option('--submit', 'Submit for validation immediately')
|
|
25
|
+
.action(deploy_1.deployCommand);
|
|
26
|
+
program.parse();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.default = SimulatorApp;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
function SimulatorApp() {
|
|
39
|
+
// State
|
|
40
|
+
const [widgetUrl, setWidgetUrl] = (0, react_1.useState)('http://localhost:3001'); // Default widget port
|
|
41
|
+
const [logs, setLogs] = (0, react_1.useState)([]);
|
|
42
|
+
// Iframe ref to send responses back
|
|
43
|
+
const iframeRef = (0, react_1.useRef)(null);
|
|
44
|
+
// Initial load
|
|
45
|
+
(0, react_1.useEffect)(() => {
|
|
46
|
+
const handleMessage = (event) => {
|
|
47
|
+
const data = event.data;
|
|
48
|
+
// Filter only widget messages
|
|
49
|
+
if (data?.source !== 'clubz-widget')
|
|
50
|
+
return;
|
|
51
|
+
log('req', `${data.action} ${data.payload ? JSON.stringify(data.payload) : ''}`);
|
|
52
|
+
// Process Request
|
|
53
|
+
processBridgeRequest(data, event.source);
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener('message', handleMessage);
|
|
56
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
57
|
+
}, []);
|
|
58
|
+
const log = (type, msg) => {
|
|
59
|
+
setLogs(prev => [{
|
|
60
|
+
time: new Date().toLocaleTimeString().split(' ')[0],
|
|
61
|
+
type,
|
|
62
|
+
msg
|
|
63
|
+
}, ...prev]);
|
|
64
|
+
};
|
|
65
|
+
const processBridgeRequest = (req, sourceWindow) => {
|
|
66
|
+
let responseData = null;
|
|
67
|
+
let success = true;
|
|
68
|
+
switch (req.action) {
|
|
69
|
+
case 'GET_USER':
|
|
70
|
+
responseData = { id: 'sim-user-1', name: 'Simulator User', role: 'admin' };
|
|
71
|
+
break;
|
|
72
|
+
case 'VIBRATE':
|
|
73
|
+
// Visual feedback
|
|
74
|
+
if (navigator.vibrate)
|
|
75
|
+
navigator.vibrate(200);
|
|
76
|
+
alert('š³ VIBRATION TRIGGERED');
|
|
77
|
+
break;
|
|
78
|
+
case 'NAVIGATE':
|
|
79
|
+
alert(`š§ Navigating to: ${req.payload?.route}`);
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
console.warn('Unknown action', req.action);
|
|
83
|
+
}
|
|
84
|
+
// Send Response
|
|
85
|
+
const response = {
|
|
86
|
+
id: req.id,
|
|
87
|
+
success,
|
|
88
|
+
data: responseData
|
|
89
|
+
};
|
|
90
|
+
sourceWindow.postMessage(response, '*');
|
|
91
|
+
log('res', `Sent data for ${req.id}`);
|
|
92
|
+
};
|
|
93
|
+
return (<div className="flex h-screen w-full">
|
|
94
|
+
{/* Left: Device Simulator */}
|
|
95
|
+
<div className="flex-1 flex flex-col items-center justify-center bg-slate-100 p-8">
|
|
96
|
+
<div className="relative border-gray-800 bg-gray-800 border-[14px] rounded-[2.5rem] h-[600px] w-[300px] shadow-xl">
|
|
97
|
+
<div className="w-[148px] h-[18px] bg-gray-800 top-0 rounded-b-[1rem] left-1/2 -translate-x-1/2 absolute"></div>
|
|
98
|
+
<div className="h-[32px] w-[3px] bg-gray-800 absolute -start-[17px] top-[72px] rounded-s-lg"></div>
|
|
99
|
+
<div className="h-[46px] w-[3px] bg-gray-800 absolute -start-[17px] top-[124px] rounded-s-lg"></div>
|
|
100
|
+
<div className="h-[46px] w-[3px] bg-gray-800 absolute -start-[17px] top-[178px] rounded-s-lg"></div>
|
|
101
|
+
<div className="h-[64px] w-[3px] bg-gray-800 absolute -end-[17px] top-[142px] rounded-e-lg"></div>
|
|
102
|
+
<div className="rounded-[2rem] overflow-hidden w-full h-full bg-white relative">
|
|
103
|
+
{/* Iframe Content */}
|
|
104
|
+
<iframe ref={iframeRef} src={widgetUrl} className="w-full h-full border-0" title="Widget Simulator"/>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<p className="mt-4 text-slate-400 text-sm">iPhone 14 Pro Simulator</p>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Right: Debug Console */}
|
|
111
|
+
<div className="w-96 bg-white border-l border-slate-200 flex flex-col">
|
|
112
|
+
<div className="p-4 border-b border-slate-200 bg-slate-50">
|
|
113
|
+
<h2 className="font-bold text-slate-700">Bridge Debugger</h2>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="p-4 border-b border-slate-200">
|
|
117
|
+
<label className="text-xs font-bold text-slate-500 uppercase">Widget URL</label>
|
|
118
|
+
<input type="text" value={widgetUrl} onChange={e => setWidgetUrl(e.target.value)} className="w-full mt-1 px-3 py-2 border rounded text-sm font-mono"/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-2 bg-slate-900 font-mono text-xs">
|
|
122
|
+
{logs.length === 0 && <p className="text-slate-500 italic">Waiting for events...</p>}
|
|
123
|
+
{logs.map((l, i) => (<div key={i} className="flex gap-2">
|
|
124
|
+
<span className="text-slate-500">[{l.time}]</span>
|
|
125
|
+
<span className={l.type === 'req' ? 'text-blue-400' : 'text-green-400'}>
|
|
126
|
+
{l.type === 'req' ? 'ā' : 'ā'}
|
|
127
|
+
</span>
|
|
128
|
+
<span className="text-slate-300 break-all">{l.msg}</span>
|
|
129
|
+
</div>))}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>);
|
|
133
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const react_1 = __importDefault(require("react"));
|
|
7
|
+
const client_1 = __importDefault(require("react-dom/client"));
|
|
8
|
+
const SimulatorApp_1 = __importDefault(require("./SimulatorApp"));
|
|
9
|
+
client_1.default.createRoot(document.getElementById('root')).render(<react_1.default.StrictMode>
|
|
10
|
+
<SimulatorApp_1.default />
|
|
11
|
+
</react_1.default.StrictMode>);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const vite_1 = require("vite");
|
|
7
|
+
const plugin_react_1 = __importDefault(require("@vitejs/plugin-react"));
|
|
8
|
+
exports.default = (0, vite_1.defineConfig)({
|
|
9
|
+
plugins: [(0, plugin_react_1.default)()],
|
|
10
|
+
root: __dirname, // Ensure root is set to this directory
|
|
11
|
+
build: {
|
|
12
|
+
outDir: '../../../dist/simulator/client', // Build into the CLI dist folder
|
|
13
|
+
emptyOutDir: true
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const react_1 = require("react");
|
|
4
|
+
const sdk_1 = require("@clubz/sdk");
|
|
5
|
+
function App() {
|
|
6
|
+
const [user, setUser] = (0, react_1.useState)(null);
|
|
7
|
+
(0, react_1.useEffect)(() => {
|
|
8
|
+
// Determine user context via Bridge
|
|
9
|
+
sdk_1.bridge.getUser().then((u) => setUser(u)).catch(() => console.log('Guest mode'));
|
|
10
|
+
}, []);
|
|
11
|
+
const handleVibrate = () => {
|
|
12
|
+
sdk_1.bridge.vibrate();
|
|
13
|
+
};
|
|
14
|
+
return (<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">
|
|
15
|
+
<h1 className="text-xl font-bold text-slate-800 mb-2">
|
|
16
|
+
Hello {user ? user.name : 'Guest'}!
|
|
17
|
+
</h1>
|
|
18
|
+
<p className="text-slate-500 text-center mb-4">
|
|
19
|
+
Welcome to your new Clubz Widget.
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<button onClick={handleVibrate} className="px-4 py-2 bg-blue-600 text-white rounded-lg active:bg-blue-700 transition">
|
|
23
|
+
Test Vibration
|
|
24
|
+
</button>
|
|
25
|
+
</div>);
|
|
26
|
+
}
|
|
27
|
+
exports.default = App;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const react_1 = __importDefault(require("react"));
|
|
7
|
+
const client_1 = __importDefault(require("react-dom/client"));
|
|
8
|
+
const App_tsx_1 = __importDefault(require("./App.tsx"));
|
|
9
|
+
client_1.default.createRoot(document.getElementById('root')).render(<react_1.default.StrictMode>
|
|
10
|
+
<App_tsx_1.default />
|
|
11
|
+
</react_1.default.StrictMode>);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const vite_1 = require("vite");
|
|
7
|
+
const plugin_react_1 = __importDefault(require("@vitejs/plugin-react"));
|
|
8
|
+
// https://vitejs.dev/config/
|
|
9
|
+
exports.default = (0, vite_1.defineConfig)({
|
|
10
|
+
plugins: [(0, plugin_react_1.default)()],
|
|
11
|
+
build: {
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
output: {
|
|
14
|
+
// Ensure we get a single recognizable entry point if possible,
|
|
15
|
+
// though standard vite build is fine for the platform zipper.
|
|
16
|
+
entryFileNames: 'assets/[name].js',
|
|
17
|
+
chunkFileNames: 'assets/[name].js',
|
|
18
|
+
assetFileNames: 'assets/[name].[ext]'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clubz/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"clubz": "bin/clubz.js"
|
|
6
|
+
},
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc -w"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@types/archiver": "^7.0.0",
|
|
17
|
+
"@types/cross-spawn": "^6.0.6",
|
|
18
|
+
"@types/express": "^5.0.6",
|
|
19
|
+
"@types/fs-extra": "^11.0.4",
|
|
20
|
+
"archiver": "^7.0.1",
|
|
21
|
+
"axios": "^1.13.4",
|
|
22
|
+
"chalk": "^4.1.2",
|
|
23
|
+
"commander": "^11.0.0",
|
|
24
|
+
"cross-spawn": "^7.0.6",
|
|
25
|
+
"express": "^5.2.1",
|
|
26
|
+
"form-data": "^4.0.5",
|
|
27
|
+
"fs-extra": "^11.3.3",
|
|
28
|
+
"inquirer": "^8.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/inquirer": "^9.0.9",
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import archiver from 'archiver';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import FormData from 'form-data';
|
|
8
|
+
|
|
9
|
+
export async function deployCommand() {
|
|
10
|
+
console.log(chalk.blue('š Deploying Widget to Clubz...'));
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const configPath = path.join(cwd, 'clubz.json');
|
|
14
|
+
|
|
15
|
+
// 1. Validation
|
|
16
|
+
if (!fs.existsSync(configPath)) {
|
|
17
|
+
console.error(chalk.red('ā clubz.json not found. Are you in a Clubz project?'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const config = await fs.readJson(configPath);
|
|
21
|
+
console.log(chalk.dim(` Project: ${config.name} v${config.version}`));
|
|
22
|
+
|
|
23
|
+
// 2. Build
|
|
24
|
+
console.log(chalk.yellow('\nš¦ Building project...'));
|
|
25
|
+
try {
|
|
26
|
+
execSync('npm run build', { stdio: 'inherit' });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error(chalk.red('ā Build failed. Fix errors and try again.'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 3. Zip Dist Folder
|
|
33
|
+
console.log(chalk.yellow('\nš¤ Zipping assets...'));
|
|
34
|
+
const distDir = path.join(cwd, 'dist');
|
|
35
|
+
const zipPath = path.join(cwd, 'bundle.zip');
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(distDir)) {
|
|
38
|
+
console.error(chalk.red('ā dist/ directory not found after build.'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await zipDirectory(distDir, zipPath);
|
|
43
|
+
console.log(chalk.green(` Bundle created: ${getSize(zipPath)}`));
|
|
44
|
+
|
|
45
|
+
// 4. Upload (Mock for now, replacing previous simple push)
|
|
46
|
+
console.log(chalk.yellow('\nāļø Uploading to Clubz Registry...'));
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const apiKey = process.env.CLUBZ_API_KEY;
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
console.warn(chalk.yellow('ā ļø No CLUBZ_API_KEY found in env. simulating upload...'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isSubmission = process.argv.includes('--submit');
|
|
55
|
+
const status = isSubmission ? 'pending' : 'draft';
|
|
56
|
+
|
|
57
|
+
// Real implementation would look like this:
|
|
58
|
+
/*
|
|
59
|
+
const form = new FormData();
|
|
60
|
+
form.append('file', fs.createReadStream(zipPath));
|
|
61
|
+
form.append('manifest', JSON.stringify({ ...config, status }));
|
|
62
|
+
|
|
63
|
+
await axios.post('http://localhost:3000/api/registry/deploy', form, {
|
|
64
|
+
headers: {
|
|
65
|
+
...form.getHeaders(),
|
|
66
|
+
'Authorization': `Bearer ${apiKey}`
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
// Simulating network delay
|
|
72
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
73
|
+
|
|
74
|
+
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
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isSubmission) {
|
|
82
|
+
console.log(chalk.magenta(' š Submitted for validation (Status: Pending)'));
|
|
83
|
+
console.log(chalk.dim(' An admin will review your widget shortly.'));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(chalk.dim(' Saved as Draft. PRO TIP: Use --submit to send for validation.'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(chalk.dim(' Available in your "Submissions" tab.'));
|
|
89
|
+
|
|
90
|
+
// Cleanup
|
|
91
|
+
await fs.remove(zipPath);
|
|
92
|
+
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
console.error(chalk.red('ā Upload failed'), error.message);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function zipDirectory(source: string, out: string): Promise<void> {
|
|
100
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
101
|
+
const stream = fs.createWriteStream(out);
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
archive
|
|
105
|
+
.directory(source, false)
|
|
106
|
+
.on('error', err => reject(err))
|
|
107
|
+
.pipe(stream);
|
|
108
|
+
|
|
109
|
+
stream.on('close', () => resolve());
|
|
110
|
+
archive.finalize();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getSize(filePath: string) {
|
|
115
|
+
const stats = fs.statSync(filePath);
|
|
116
|
+
const bytes = stats.size;
|
|
117
|
+
if (bytes < 1024) return bytes + ' B';
|
|
118
|
+
return (bytes / 1024).toFixed(2) + ' KB';
|
|
119
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import spawn from 'cross-spawn';
|
|
4
|
+
import chalk from 'chalk';
|
|
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
|
|
10
|
+
|
|
11
|
+
export async function devCommand() {
|
|
12
|
+
console.log(chalk.blue('š Starting Clubz Development Environment...'));
|
|
13
|
+
|
|
14
|
+
// 1. Start User's Widget Server (Vite)
|
|
15
|
+
const widgetPort = 3001;
|
|
16
|
+
console.log(chalk.dim(` Starting widget server on port ${widgetPort}...`));
|
|
17
|
+
|
|
18
|
+
const widgetProcess = spawn('npm', ['run', 'dev', '--', '--port', widgetPort.toString()], {
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
env: { ...process.env, FORCE_COLOR: 'true' }
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
widgetProcess.on('error', (err) => {
|
|
24
|
+
console.error(chalk.red('ā Failed to start widget server'), err);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 2. Start Simulator Server
|
|
28
|
+
const app = express();
|
|
29
|
+
const simulatorPort = 3000;
|
|
30
|
+
|
|
31
|
+
// 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
|
+
let simulatorDist = path.join(__dirname, '../../simulator/client');
|
|
35
|
+
|
|
36
|
+
// Fallback for dev execution (ts-node)
|
|
37
|
+
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
|
+
console.warn(chalk.yellow('ā ļø Simulator build not found. Ensure CLI is built.'));
|
|
43
|
+
// Try pointing to package root dist if structure is flattened
|
|
44
|
+
simulatorDist = path.join(__dirname, '../../../dist/simulator/client');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
app.use(express.static(simulatorDist));
|
|
48
|
+
|
|
49
|
+
// Fallback to index.html for SPA routing
|
|
50
|
+
app.get('*', (req, res) => {
|
|
51
|
+
res.sendFile(path.join(simulatorDist, 'index.html'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.listen(simulatorPort, () => {
|
|
55
|
+
console.log(chalk.green(`\nš± Simulator running at http://localhost:${simulatorPort}`));
|
|
56
|
+
console.log(chalk.cyan(` Widget running at http://localhost:${widgetPort}`));
|
|
57
|
+
console.log(chalk.dim(' Press Ctrl+C to stop.'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Handle cleanup
|
|
61
|
+
process.on('SIGINT', () => {
|
|
62
|
+
widgetProcess.kill();
|
|
63
|
+
process.exit();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
|
|
7
|
+
// In CommonJS, __dirname is available globally
|
|
8
|
+
|
|
9
|
+
export async function initCommand(name: string) {
|
|
10
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
11
|
+
|
|
12
|
+
if (fs.existsSync(targetDir)) {
|
|
13
|
+
console.error(chalk.red(`ā Directory ${name} already exists.`));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(chalk.blue(`š Creating new Clubz widget project: ${name}`));
|
|
18
|
+
|
|
19
|
+
// 1. Copy Template
|
|
20
|
+
// In dev (ts-node/src), template is in src/templates
|
|
21
|
+
// In prod (dist), template is in dist/templates (need to ensure build copies it)
|
|
22
|
+
// For now we assume we are running from src or built dist where structure is preserved.
|
|
23
|
+
// Adjust path based on where we are executing.
|
|
24
|
+
|
|
25
|
+
// We need to resolve the template path relative to this file.
|
|
26
|
+
// If we are in dist/commands/init.js, template should be in ../templates/react-vite-ts relative to this file?
|
|
27
|
+
// No, standard is usually to keep templates as assets.
|
|
28
|
+
// Let's assume we are in src/commands/init.ts for dev flow or dist/commands/init.js
|
|
29
|
+
|
|
30
|
+
// Quick fix for path resolution in both envs: look for templates dir up the tree
|
|
31
|
+
const templateName = 'react-vite-ts';
|
|
32
|
+
let templateDir = path.join(__dirname, '../../templates', templateName); // from dist/commands or src/commands
|
|
33
|
+
|
|
34
|
+
// Fallback if structure is different (e.g. src vs dist) - explicit check
|
|
35
|
+
if (!fs.existsSync(templateDir)) {
|
|
36
|
+
// Try source path if we are running ts-node directly?
|
|
37
|
+
templateDir = path.join(__dirname, '../../../src/templates', templateName);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(templateDir)) {
|
|
41
|
+
console.error(chalk.red(`ā Template not found at ${templateDir}`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await fs.copy(templateDir, targetDir);
|
|
47
|
+
|
|
48
|
+
// 2. Customize package.json
|
|
49
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
50
|
+
const pkg = await fs.readJson(pkgPath);
|
|
51
|
+
pkg.name = name;
|
|
52
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
53
|
+
|
|
54
|
+
// 3. Rename .gitignore if needed (npm often renames it to .npmignore on publish)
|
|
55
|
+
const gitignorePath = path.join(targetDir, '_gitignore');
|
|
56
|
+
if (fs.existsSync(gitignorePath)) {
|
|
57
|
+
await fs.move(gitignorePath, path.join(targetDir, '.gitignore'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(chalk.green(`ā
Project created in ${targetDir}`));
|
|
61
|
+
|
|
62
|
+
// 4. Prompt for install
|
|
63
|
+
const { install } = await inquirer.prompt([{
|
|
64
|
+
type: 'confirm',
|
|
65
|
+
name: 'install',
|
|
66
|
+
message: 'Install dependencies now?',
|
|
67
|
+
default: true
|
|
68
|
+
}]);
|
|
69
|
+
|
|
70
|
+
if (install) {
|
|
71
|
+
console.log(chalk.yellow('š¦ Installing dependencies...'));
|
|
72
|
+
execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
|
|
73
|
+
console.log(chalk.green('ā
Dependencies installed'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('\nNext steps:');
|
|
77
|
+
console.log(chalk.cyan(` cd ${name}`));
|
|
78
|
+
console.log(chalk.cyan(' npm run dev'));
|
|
79
|
+
|
|
80
|
+
} catch (e: any) {
|
|
81
|
+
console.error(chalk.red('ā Failed to create project'), e);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { initCommand } from './commands/init';
|
|
3
|
+
import { devCommand } from './commands/dev';
|
|
4
|
+
import { deployCommand } from './commands/deploy';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('clubz')
|
|
10
|
+
.description('Clubz Developer CLI')
|
|
11
|
+
.version('0.1.0');
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command('init')
|
|
15
|
+
.description('Initialize a new Clubz widget project')
|
|
16
|
+
.argument('<name>', 'Project name')
|
|
17
|
+
.action(initCommand);
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('dev')
|
|
21
|
+
.description('Start development server with Simulator')
|
|
22
|
+
.action(devCommand);
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('deploy')
|
|
26
|
+
.description('Build and deploy widget to Clubz')
|
|
27
|
+
.option('--submit', 'Submit for validation immediately')
|
|
28
|
+
.action(deployCommand);
|
|
29
|
+
|
|
30
|
+
program.parse();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Clubz Simulator</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body class="bg-slate-50 h-screen w-screen overflow-hidden">
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
13
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
14
|
+
</body>
|
|
15
|
+
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
// Mock Bridge Types (replicated from SDK for independence)
|
|
4
|
+
type BridgeAction = 'VIBRATE' | 'NAVIGATE' | 'GET_USER' | 'STORAGE_SET' | 'STORAGE_GET';
|
|
5
|
+
interface BridgeRequest { id: string; action: BridgeAction; payload?: any; source?: string; }
|
|
6
|
+
interface BridgeResponse { id: string; success: boolean; data?: any; error?: string; }
|
|
7
|
+
|
|
8
|
+
export default function SimulatorApp() {
|
|
9
|
+
// State
|
|
10
|
+
const [widgetUrl, setWidgetUrl] = useState('http://localhost:3001'); // Default widget port
|
|
11
|
+
const [logs, setLogs] = useState<{ time: string, type: 'req' | 'res', msg: string }[]>([]);
|
|
12
|
+
|
|
13
|
+
// Iframe ref to send responses back
|
|
14
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
15
|
+
|
|
16
|
+
// Initial load
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const handleMessage = (event: MessageEvent) => {
|
|
19
|
+
const data = event.data as BridgeRequest;
|
|
20
|
+
|
|
21
|
+
// Filter only widget messages
|
|
22
|
+
if (data?.source !== 'clubz-widget') return;
|
|
23
|
+
|
|
24
|
+
log('req', `${data.action} ${data.payload ? JSON.stringify(data.payload) : ''}`);
|
|
25
|
+
|
|
26
|
+
// Process Request
|
|
27
|
+
processBridgeRequest(data, event.source as Window);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
window.addEventListener('message', handleMessage);
|
|
31
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const log = (type: 'req' | 'res', msg: string) => {
|
|
35
|
+
setLogs(prev => [{
|
|
36
|
+
time: new Date().toLocaleTimeString().split(' ')[0],
|
|
37
|
+
type,
|
|
38
|
+
msg
|
|
39
|
+
}, ...prev]);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const processBridgeRequest = (req: BridgeRequest, sourceWindow: Window) => {
|
|
43
|
+
let responseData: any = null;
|
|
44
|
+
let success = true;
|
|
45
|
+
|
|
46
|
+
switch (req.action) {
|
|
47
|
+
case 'GET_USER':
|
|
48
|
+
responseData = { id: 'sim-user-1', name: 'Simulator User', role: 'admin' };
|
|
49
|
+
break;
|
|
50
|
+
case 'VIBRATE':
|
|
51
|
+
// Visual feedback
|
|
52
|
+
if (navigator.vibrate) navigator.vibrate(200);
|
|
53
|
+
alert('š³ VIBRATION TRIGGERED');
|
|
54
|
+
break;
|
|
55
|
+
case 'NAVIGATE':
|
|
56
|
+
alert(`š§ Navigating to: ${req.payload?.route}`);
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
console.warn('Unknown action', req.action);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Send Response
|
|
63
|
+
const response: BridgeResponse = {
|
|
64
|
+
id: req.id,
|
|
65
|
+
success,
|
|
66
|
+
data: responseData
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
sourceWindow.postMessage(response, '*');
|
|
70
|
+
log('res', `Sent data for ${req.id}`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex h-screen w-full">
|
|
75
|
+
{/* Left: Device Simulator */}
|
|
76
|
+
<div className="flex-1 flex flex-col items-center justify-center bg-slate-100 p-8">
|
|
77
|
+
<div className="relative border-gray-800 bg-gray-800 border-[14px] rounded-[2.5rem] h-[600px] w-[300px] shadow-xl">
|
|
78
|
+
<div className="w-[148px] h-[18px] bg-gray-800 top-0 rounded-b-[1rem] left-1/2 -translate-x-1/2 absolute"></div>
|
|
79
|
+
<div className="h-[32px] w-[3px] bg-gray-800 absolute -start-[17px] top-[72px] rounded-s-lg"></div>
|
|
80
|
+
<div className="h-[46px] w-[3px] bg-gray-800 absolute -start-[17px] top-[124px] rounded-s-lg"></div>
|
|
81
|
+
<div className="h-[46px] w-[3px] bg-gray-800 absolute -start-[17px] top-[178px] rounded-s-lg"></div>
|
|
82
|
+
<div className="h-[64px] w-[3px] bg-gray-800 absolute -end-[17px] top-[142px] rounded-e-lg"></div>
|
|
83
|
+
<div className="rounded-[2rem] overflow-hidden w-full h-full bg-white relative">
|
|
84
|
+
{/* Iframe Content */}
|
|
85
|
+
<iframe
|
|
86
|
+
ref={iframeRef}
|
|
87
|
+
src={widgetUrl}
|
|
88
|
+
className="w-full h-full border-0"
|
|
89
|
+
title="Widget Simulator"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<p className="mt-4 text-slate-400 text-sm">iPhone 14 Pro Simulator</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Right: Debug Console */}
|
|
97
|
+
<div className="w-96 bg-white border-l border-slate-200 flex flex-col">
|
|
98
|
+
<div className="p-4 border-b border-slate-200 bg-slate-50">
|
|
99
|
+
<h2 className="font-bold text-slate-700">Bridge Debugger</h2>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="p-4 border-b border-slate-200">
|
|
103
|
+
<label className="text-xs font-bold text-slate-500 uppercase">Widget URL</label>
|
|
104
|
+
<input
|
|
105
|
+
type="text"
|
|
106
|
+
value={widgetUrl}
|
|
107
|
+
onChange={e => setWidgetUrl(e.target.value)}
|
|
108
|
+
className="w-full mt-1 px-3 py-2 border rounded text-sm font-mono"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-2 bg-slate-900 font-mono text-xs">
|
|
113
|
+
{logs.length === 0 && <p className="text-slate-500 italic">Waiting for events...</p>}
|
|
114
|
+
{logs.map((l, i) => (
|
|
115
|
+
<div key={i} className="flex gap-2">
|
|
116
|
+
<span className="text-slate-500">[{l.time}]</span>
|
|
117
|
+
<span className={l.type === 'req' ? 'text-blue-400' : 'text-green-400'}>
|
|
118
|
+
{l.type === 'req' ? 'ā' : 'ā'}
|
|
119
|
+
</span>
|
|
120
|
+
<span className="text-slate-300 break-all">{l.msg}</span>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
root: __dirname, // Ensure root is set to this directory
|
|
8
|
+
build: {
|
|
9
|
+
outDir: '../../../dist/simulator/client', // Build into the CLI dist folder
|
|
10
|
+
emptyOutDir: true
|
|
11
|
+
}
|
|
12
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "My Amazing Widget",
|
|
3
|
+
"description": "A widget built for Clubz",
|
|
4
|
+
"version": "1.0.0",
|
|
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,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-clubz-widget",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-dom": "^18.2.0",
|
|
14
|
+
"@clubz/sdk": "latest"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^18.2.66",
|
|
18
|
+
"@types/react-dom": "^18.2.22",
|
|
19
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
20
|
+
"typescript": "^5.2.2",
|
|
21
|
+
"vite": "^5.2.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -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,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/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"src/**/*"
|
|
14
|
+
],
|
|
15
|
+
"exclude": [
|
|
16
|
+
"src/templates/**/*",
|
|
17
|
+
"src/simulator/client/**/*"
|
|
18
|
+
]
|
|
19
|
+
}
|