@klyb/cli 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +82 -0
  2. package/bin/klyb.js +4 -0
  3. package/dist/commands/deploy.js +141 -0
  4. package/dist/commands/dev.js +87 -0
  5. package/dist/commands/init.js +77 -0
  6. package/dist/commands/login.js +121 -0
  7. package/dist/credentials.js +35 -0
  8. package/dist/index.js +31 -0
  9. package/dist/simulator/client/src/SimulatorApp.js +133 -0
  10. package/dist/simulator/client/src/main.js +11 -0
  11. package/dist/simulator/client/vite.config.js +15 -0
  12. package/dist/templates/react-vite-ts/src/App.js +27 -0
  13. package/dist/templates/react-vite-ts/src/main.js +11 -0
  14. package/dist/templates/react-vite-ts/vite.config.js +22 -0
  15. package/dist/templates/templates/react-vite-ts/_env.example +12 -0
  16. package/dist/templates/templates/react-vite-ts/clubz.json +20 -0
  17. package/dist/templates/templates/react-vite-ts/index.html +13 -0
  18. package/dist/templates/templates/react-vite-ts/klyb.json +20 -0
  19. package/dist/templates/templates/react-vite-ts/package.json +24 -0
  20. package/dist/templates/templates/react-vite-ts/public/clubz.json +20 -0
  21. package/dist/templates/templates/react-vite-ts/public/preview.png +0 -0
  22. package/dist/templates/templates/react-vite-ts/src/App.tsx +35 -0
  23. package/dist/templates/templates/react-vite-ts/src/main.tsx +9 -0
  24. package/dist/templates/templates/react-vite-ts/tsconfig.json +37 -0
  25. package/dist/templates/templates/react-vite-ts/tsconfig.node.json +12 -0
  26. package/dist/templates/templates/react-vite-ts/vite.config.ts +25 -0
  27. package/package.json +37 -0
  28. package/src/commands/deploy.ts +155 -0
  29. package/src/commands/dev.ts +94 -0
  30. package/src/commands/init.ts +88 -0
  31. package/src/commands/login.ts +94 -0
  32. package/src/credentials.ts +36 -0
  33. package/src/index.ts +37 -0
  34. package/src/simulator/client/index.html +16 -0
  35. package/src/simulator/client/src/SimulatorApp.tsx +163 -0
  36. package/src/simulator/client/src/main.tsx +9 -0
  37. package/src/simulator/client/vite.config.ts +12 -0
  38. package/src/templates/react-vite-ts/_env.example +12 -0
  39. package/src/templates/react-vite-ts/index.html +13 -0
  40. package/src/templates/react-vite-ts/klyb.json +20 -0
  41. package/src/templates/react-vite-ts/package.json +24 -0
  42. package/src/templates/react-vite-ts/public/preview.png +0 -0
  43. package/src/templates/react-vite-ts/src/App.tsx +35 -0
  44. package/src/templates/react-vite-ts/src/main.tsx +9 -0
  45. package/src/templates/react-vite-ts/tsconfig.json +37 -0
  46. package/src/templates/react-vite-ts/tsconfig.node.json +12 -0
  47. package/src/templates/react-vite-ts/vite.config.ts +25 -0
  48. package/tsconfig.json +19 -0
@@ -0,0 +1,94 @@
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
+ import dotenv from 'dotenv';
7
+ import axios from 'axios';
8
+
9
+ export async function devCommand() {
10
+ console.log(chalk.blue('🚀 Starting Klyb Development Environment...'));
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
+
21
+ // 1. Start User's Widget Server (Vite)
22
+ const widgetPort = 3001;
23
+ console.log(chalk.dim(` Starting widget server on port ${widgetPort}...`));
24
+
25
+ const widgetProcess = spawn('npm', ['run', 'dev', '--', '--port', widgetPort.toString()], {
26
+ stdio: 'inherit',
27
+ env: { ...process.env, FORCE_COLOR: 'true' }
28
+ });
29
+
30
+ widgetProcess.on('error', (err) => {
31
+ console.error(chalk.red('❌ Failed to start widget server'), err);
32
+ });
33
+
34
+ // 2. Start Simulator Server
35
+ const app = express();
36
+ const simulatorPort = 3002; // Changed from 3000 to avoid conflict with API
37
+ const realApiUrl = process.env.KLYB_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.KLYB_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
+ });
65
+
66
+ // Resolve path to built simulator client assets
67
+ let simulatorDist = path.join(__dirname, '../../simulator/client');
68
+
69
+ if (!fs.existsSync(simulatorDist) || !fs.existsSync(path.join(simulatorDist, 'index.html'))) {
70
+ console.warn(chalk.yellow('⚠ Simulator build not found. Ensure CLI is built.'));
71
+ simulatorDist = path.join(__dirname, '../../../dist/simulator/client');
72
+ }
73
+
74
+ app.use(express.static(simulatorDist));
75
+
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
+ }
81
+ res.sendFile(path.join(simulatorDist, 'index.html'));
82
+ });
83
+
84
+ app.listen(simulatorPort, () => {
85
+ console.log(chalk.green(`\nđŸ“± Simulator running at http://localhost:${simulatorPort}`));
86
+ console.log(chalk.cyan(` Widget running at http://localhost:${widgetPort}`));
87
+ console.log(chalk.dim(' Press Ctrl+C to stop.'));
88
+ });
89
+
90
+ process.on('SIGINT', () => {
91
+ widgetProcess.kill();
92
+ process.exit();
93
+ });
94
+ }
@@ -0,0 +1,88 @@
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 Klyb 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 from dist but templates are in src (common in local dev)
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 files starting with _ (to avoid npm stripping them)
55
+ const gitignorePath = path.join(targetDir, '_gitignore');
56
+ if (fs.existsSync(gitignorePath)) {
57
+ await fs.move(gitignorePath, path.join(targetDir, '.gitignore'));
58
+ }
59
+
60
+ const envExamplePath = path.join(targetDir, '_env.example');
61
+ if (fs.existsSync(envExamplePath)) {
62
+ await fs.move(envExamplePath, path.join(targetDir, '.env.example'));
63
+ }
64
+
65
+ console.log(chalk.green(`✅ Project created in ${targetDir}`));
66
+
67
+ // 4. Prompt for install
68
+ const { install } = await inquirer.prompt([{
69
+ type: 'confirm',
70
+ name: 'install',
71
+ message: 'Install dependencies now?',
72
+ default: true
73
+ }]);
74
+
75
+ if (install) {
76
+ console.log(chalk.yellow('📩 Installing dependencies...'));
77
+ execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
78
+ console.log(chalk.green('✅ Dependencies installed'));
79
+ }
80
+
81
+ console.log('\nNext steps:');
82
+ console.log(chalk.cyan(` cd ${name}`));
83
+ console.log(chalk.cyan(' npm run dev'));
84
+
85
+ } catch (e: any) {
86
+ console.error(chalk.red('❌ Failed to create project'), e);
87
+ }
88
+ }
@@ -0,0 +1,94 @@
1
+ import chalk from 'chalk';
2
+ import http from 'http';
3
+ import { saveCredentials, credentialsPath } from '../credentials';
4
+
5
+ const DEFAULT_API_URL = process.env.KLYB_API_URL || 'http://localhost:3000';
6
+
7
+ /**
8
+ * `klyb login`
9
+ *
10
+ * 1. Starts a one-shot local HTTP server on a random port
11
+ * 2. Opens the browser to the backend's CLI login page
12
+ * 3. After the user logs in, the backend redirects back with the JWT
13
+ * 4. CLI saves the token to ~/.klyb/credentials.json
14
+ */
15
+ export async function loginCommand() {
16
+ const apiUrl = DEFAULT_API_URL;
17
+
18
+ console.log(chalk.blue('🔑 Logging in to Klyb...'));
19
+ console.log(chalk.dim(` API: ${apiUrl}\n`));
20
+
21
+ const token = await waitForToken(apiUrl);
22
+
23
+ // Persist credentials
24
+ saveCredentials({
25
+ token,
26
+ apiUrl,
27
+ savedAt: new Date().toISOString(),
28
+ });
29
+
30
+ console.log(chalk.green('\n✅ Logged in successfully!'));
31
+ console.log(chalk.dim(` Credentials saved to: ${credentialsPath()}`));
32
+ console.log(chalk.dim(' You can now run: klyb deploy'));
33
+ }
34
+
35
+ function waitForToken(apiUrl: string): Promise<string> {
36
+ return new Promise((resolve, reject) => {
37
+ // One-shot HTTP server to receive the callback
38
+ const server = http.createServer((req, res) => {
39
+ const url = new URL(req.url!, `http://localhost`);
40
+
41
+ if (url.pathname === '/callback') {
42
+ const token = url.searchParams.get('token');
43
+ const error = url.searchParams.get('error');
44
+
45
+ if (token) {
46
+ // Success — show a nice page to the user
47
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
48
+ res.end(`
49
+ <!DOCTYPE html>
50
+ <html>
51
+ <head><title>Klyb CLI</title></head>
52
+ <body style="font-family:sans-serif;text-align:center;padding:4rem;background:#0f172a;color:white">
53
+ <h1 style="color:#22d3ee">✅ Logged in!</h1>
54
+ <p style="color:#94a3b8">You can close this window and return to your terminal.</p>
55
+ </body>
56
+ </html>
57
+ `);
58
+ server.close();
59
+ resolve(token);
60
+ } else {
61
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
62
+ res.end(`<p>Error: ${error || 'unknown'}</p>`);
63
+ server.close();
64
+ reject(new Error(error || 'Login failed'));
65
+ }
66
+ } else {
67
+ res.writeHead(404);
68
+ res.end();
69
+ }
70
+ });
71
+
72
+ // Listen on a random available port
73
+ server.listen(0, '127.0.0.1', () => {
74
+ const address = server.address() as { port: number };
75
+ const callbackUrl = `http://127.0.0.1:${address.port}/callback`;
76
+ const loginUrl = `${apiUrl}/api/auth/cli-login?redirect=${encodeURIComponent(callbackUrl)}`;
77
+
78
+ console.log(chalk.cyan(' Opening browser for authentication...'));
79
+ console.log(chalk.dim(` If the browser doesn't open, visit:\n ${loginUrl}\n`));
80
+
81
+ // Dynamic import to handle ESM-only open package
82
+ import('open').then(({ default: openBrowser }) => openBrowser(loginUrl)).catch(() => { });
83
+ });
84
+
85
+ // Timeout after 5 minutes
86
+ const timeout = setTimeout(() => {
87
+ server.close();
88
+ reject(new Error('Login timed out. Please try again.'));
89
+ }, 5 * 60 * 1000);
90
+
91
+ server.on('close', () => clearTimeout(timeout));
92
+ server.on('error', reject);
93
+ });
94
+ }
@@ -0,0 +1,36 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+
5
+ const CREDENTIALS_PATH = path.join(os.homedir(), '.klyb', 'credentials.json');
6
+
7
+ export interface KlybCredentials {
8
+ token: string; // JWT access token
9
+ apiUrl: string; // API base URL used when logging in
10
+ email?: string;
11
+ savedAt: string;
12
+ }
13
+
14
+ export function getCredentials(): KlybCredentials | null {
15
+ try {
16
+ if (!fs.existsSync(CREDENTIALS_PATH)) return null;
17
+ return fs.readJsonSync(CREDENTIALS_PATH) as KlybCredentials;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export function saveCredentials(creds: KlybCredentials): void {
24
+ fs.ensureDirSync(path.dirname(CREDENTIALS_PATH));
25
+ fs.writeJsonSync(CREDENTIALS_PATH, creds, { spaces: 2 });
26
+ }
27
+
28
+ export function clearCredentials(): void {
29
+ if (fs.existsSync(CREDENTIALS_PATH)) {
30
+ fs.removeSync(CREDENTIALS_PATH);
31
+ }
32
+ }
33
+
34
+ export function credentialsPath(): string {
35
+ return CREDENTIALS_PATH;
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Command } from 'commander';
2
+ import { initCommand } from './commands/init';
3
+ import { devCommand } from './commands/dev';
4
+ import { deployCommand } from './commands/deploy';
5
+ import { loginCommand } from './commands/login';
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('klyb')
11
+ .description('Klyb Developer CLI')
12
+ .version('0.1.0');
13
+
14
+ program
15
+ .command('login')
16
+ .description('Authenticate with Klyb (opens browser)')
17
+ .action(loginCommand);
18
+
19
+ program
20
+ .command('init')
21
+ .description('Initialize a new Klyb widget project')
22
+ .argument('<name>', 'Project name')
23
+ .action(initCommand);
24
+
25
+ program
26
+ .command('dev')
27
+ .description('Start development server with Simulator')
28
+ .action(devCommand);
29
+
30
+ program
31
+ .command('deploy')
32
+ .description('Build and deploy widget to Klyb')
33
+ .option('--submit', 'Submit for validation immediately')
34
+ .action(deployCommand);
35
+
36
+ program.parse();
37
+
@@ -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>Klyb 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,163 @@
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
+ const [userContext, setUserContext] = useState<any>({ id: 'sim-user-1', name: 'Simulator User (Guest)', role: 'guest' });
13
+ const [authStatus, setAuthStatus] = useState<'loading' | 'authenticated' | 'guest'>('loading');
14
+
15
+ // Iframe ref to send responses back
16
+ const iframeRef = useRef<HTMLIFrameElement>(null);
17
+
18
+ // Initial load & Config Fetch
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
+
35
+ const handleMessage = (event: MessageEvent) => {
36
+ const data = event.data as BridgeRequest;
37
+
38
+ // Filter only widget messages
39
+ if (data?.source !== 'klyb-widget') return;
40
+
41
+ log('req', `${data.action} ${data.payload ? JSON.stringify(data.payload) : ''}`);
42
+
43
+ // Process Request
44
+ processBridgeRequest(data, event.source as Window);
45
+ };
46
+
47
+ window.addEventListener('message', handleMessage);
48
+ return () => window.removeEventListener('message', handleMessage);
49
+ }, []);
50
+
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) => {
60
+ setLogs(prev => [{
61
+ time: new Date().toLocaleTimeString().split(' ')[0],
62
+ type,
63
+ msg
64
+ }, ...prev]);
65
+ };
66
+
67
+ const processBridgeRequest = (req: BridgeRequest, sourceWindow: Window) => {
68
+ let responseData: any = null;
69
+ let success = true;
70
+
71
+ switch (req.action) {
72
+ case 'GET_USER':
73
+ // Use the ref to get the latest user context
74
+ responseData = userContextRef.current;
75
+ break;
76
+ case 'VIBRATE':
77
+ // Visual feedback
78
+ if (navigator.vibrate) navigator.vibrate(200);
79
+ alert('📳 VIBRATION TRIGGERED');
80
+ break;
81
+ case 'NAVIGATE':
82
+ alert(`🧭 Navigating to: ${req.payload?.route}`);
83
+ break;
84
+ default:
85
+ console.warn('Unknown action', req.action);
86
+ }
87
+
88
+ // Send Response
89
+ const response: BridgeResponse = {
90
+ id: req.id,
91
+ success,
92
+ data: responseData
93
+ };
94
+
95
+ sourceWindow.postMessage(response, '*');
96
+ log('res', `Sent data for ${req.id}`);
97
+ };
98
+
99
+ return (
100
+ <div className="flex h-screen w-full">
101
+ {/* Left: Device Simulator */}
102
+ <div className="flex-1 flex flex-col items-center justify-center bg-slate-100 p-8">
103
+ <div className="relative border-gray-800 bg-gray-800 border-[14px] rounded-[2.5rem] h-[600px] w-[300px] shadow-xl">
104
+ <div className="w-[148px] h-[18px] bg-gray-800 top-0 rounded-b-[1rem] left-1/2 -translate-x-1/2 absolute"></div>
105
+ <div className="h-[32px] w-[3px] bg-gray-800 absolute -start-[17px] top-[72px] rounded-s-lg"></div>
106
+ <div className="h-[46px] w-[3px] bg-gray-800 absolute -start-[17px] top-[124px] rounded-s-lg"></div>
107
+ <div className="h-[46px] w-[3px] bg-gray-800 absolute -start-[17px] top-[178px] rounded-s-lg"></div>
108
+ <div className="h-[64px] w-[3px] bg-gray-800 absolute -end-[17px] top-[142px] rounded-e-lg"></div>
109
+ <div className="rounded-[2rem] overflow-hidden w-full h-full bg-white relative">
110
+ {/* Iframe Content */}
111
+ <iframe
112
+ ref={iframeRef}
113
+ src={widgetUrl}
114
+ className="w-full h-full border-0"
115
+ title="Widget Simulator"
116
+ />
117
+ </div>
118
+ </div>
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>
127
+ </div>
128
+
129
+ {/* Right: Debug Console */}
130
+ <div className="w-96 bg-white border-l border-slate-200 flex flex-col">
131
+ <div className="p-4 border-b border-slate-200 bg-slate-50">
132
+ <h2 className="font-bold text-slate-700">Bridge Debugger</h2>
133
+ </div>
134
+
135
+ <div className="p-4 border-b border-slate-200">
136
+ <label className="text-xs font-bold text-slate-500 uppercase">Widget URL</label>
137
+ <input
138
+ type="text"
139
+ value={widgetUrl}
140
+ onChange={e => setWidgetUrl(e.target.value)}
141
+ className="w-full mt-1 px-3 py-2 border rounded text-sm font-mono"
142
+ />
143
+ </div>
144
+
145
+ <div className="flex-1 overflow-y-auto p-4 space-y-2 bg-slate-900 font-mono text-xs">
146
+ {logs.length === 0 && <p className="text-slate-500 italic">Waiting for events...</p>}
147
+ {logs.map((l, i) => (
148
+ <div key={i} className="flex gap-2">
149
+ <span className="text-slate-500">[{l.time}]</span>
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' ? '←' : '‱'}
155
+ </span>
156
+ <span className="text-slate-300 break-all">{l.msg}</span>
157
+ </div>
158
+ ))}
159
+ </div>
160
+ </div>
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import SimulatorApp from './SimulatorApp'
4
+
5
+ ReactDOM.createRoot(document.getElementById('root')!).render(
6
+ <React.StrictMode>
7
+ <SimulatorApp />
8
+ </React.StrictMode>,
9
+ )
@@ -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,12 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Configuration Klyb Widget
3
+ # ------------------------------------------------------------------------------
4
+
5
+ # L'URL de l'API Klyb.
6
+ # Par défaut, la CLI essaiera de se connecter à http://localhost:3000 si non défini.
7
+ # Décommentez la ligne ci-dessous pour forcer l'environnement de Production :
8
+ # KLYB_API_URL=https://api.klyb.co
9
+
10
+ # Votre clé d'API Développeur Klyb
11
+ # Vous pouvez la générer depuis votre Espace Développeur sur klyb_comu (Onglet "Clés API")
12
+ KLYB_API_KEY=votre_cle_api_secrete_ici
@@ -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>Klyb 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,20 @@
1
+ {
2
+ "name": "My Klyb Widget",
3
+ "description": "A widget built for Klyb",
4
+ "version": "0.0.1",
5
+ "type": "widget",
6
+ "preview": "preview.png",
7
+ "permissions": [
8
+ "read_community_name"
9
+ ],
10
+ "config": {
11
+ "props": [
12
+ {
13
+ "name": "title",
14
+ "type": "string",
15
+ "label": "Titre du widget",
16
+ "default": "Hello World"
17
+ }
18
+ ]
19
+ }
20
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "my-klyb-widget",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "deploy": "klyb deploy --submit",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^18.2.0",
14
+ "react-dom": "^18.2.0",
15
+ "@klyb/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 '@klyb/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 Klyb 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