@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.
- package/README.md +82 -0
- package/bin/klyb.js +4 -0
- package/dist/commands/deploy.js +141 -0
- package/dist/commands/dev.js +87 -0
- package/dist/commands/init.js +77 -0
- package/dist/commands/login.js +121 -0
- package/dist/credentials.js +35 -0
- package/dist/index.js +31 -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/dist/templates/templates/react-vite-ts/_env.example +12 -0
- package/dist/templates/templates/react-vite-ts/clubz.json +20 -0
- package/dist/templates/templates/react-vite-ts/index.html +13 -0
- package/dist/templates/templates/react-vite-ts/klyb.json +20 -0
- package/dist/templates/templates/react-vite-ts/package.json +24 -0
- package/dist/templates/templates/react-vite-ts/public/clubz.json +20 -0
- package/dist/templates/templates/react-vite-ts/public/preview.png +0 -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 +37 -0
- package/dist/templates/templates/react-vite-ts/tsconfig.node.json +12 -0
- package/dist/templates/templates/react-vite-ts/vite.config.ts +25 -0
- package/package.json +37 -0
- package/src/commands/deploy.ts +155 -0
- package/src/commands/dev.ts +94 -0
- package/src/commands/init.ts +88 -0
- package/src/commands/login.ts +94 -0
- package/src/credentials.ts +36 -0
- package/src/index.ts +37 -0
- package/src/simulator/client/index.html +16 -0
- package/src/simulator/client/src/SimulatorApp.tsx +163 -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/_env.example +12 -0
- package/src/templates/react-vite-ts/index.html +13 -0
- package/src/templates/react-vite-ts/klyb.json +20 -0
- package/src/templates/react-vite-ts/package.json +24 -0
- package/src/templates/react-vite-ts/public/preview.png +0 -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/tsconfig.json +37 -0
- package/src/templates/react-vite-ts/tsconfig.node.json +12 -0
- package/src/templates/react-vite-ts/vite.config.ts +25 -0
- 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,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
|
+
}
|
|
Binary file
|
|
@@ -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
|