@l4yercak3/cli 1.2.15 â 1.2.18
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/docs/INTEGRATION_PATHS_ARCHITECTURE.md +1543 -0
- package/package.json +1 -1
- package/src/commands/spread.js +101 -6
- package/src/detectors/database-detector.js +245 -0
- package/src/detectors/index.js +17 -4
- package/src/generators/api-only/client.js +683 -0
- package/src/generators/api-only/index.js +96 -0
- package/src/generators/api-only/types.js +618 -0
- package/src/generators/api-only/webhooks.js +377 -0
- package/src/generators/index.js +88 -2
- package/src/generators/mcp-guide-generator.js +256 -0
- package/src/generators/quickstart/components/index.js +1699 -0
- package/src/generators/quickstart/database/convex.js +1257 -0
- package/src/generators/quickstart/database/index.js +34 -0
- package/src/generators/quickstart/database/supabase.js +1132 -0
- package/src/generators/quickstart/hooks/index.js +1047 -0
- package/src/generators/quickstart/index.js +151 -0
- package/src/generators/quickstart/pages/index.js +1466 -0
- package/src/mcp/registry/domains/applications.js +4 -4
- package/src/mcp/registry/domains/benefits.js +798 -0
- package/src/mcp/registry/domains/crm.js +11 -11
- package/src/mcp/registry/domains/events.js +12 -12
- package/src/mcp/registry/domains/forms.js +12 -12
- package/src/mcp/registry/index.js +2 -0
- package/tests/database-detector.test.js +221 -0
- package/tests/generators-index.test.js +215 -3
package/package.json
CHANGED
package/src/commands/spread.js
CHANGED
|
@@ -405,17 +405,83 @@ async function handleSpread() {
|
|
|
405
405
|
choices: [
|
|
406
406
|
{ name: 'CRM (contacts, organizations)', value: 'crm', checked: true },
|
|
407
407
|
{ name: 'Events (event management, registrations)', value: 'events', checked: false },
|
|
408
|
-
{ name: '
|
|
409
|
-
{ name: '
|
|
410
|
-
{ name: '
|
|
411
|
-
{ name: 'Invoicing (
|
|
412
|
-
{ name: '
|
|
413
|
-
{ name: '
|
|
408
|
+
{ name: 'Forms (form builder, submissions)', value: 'forms', checked: false },
|
|
409
|
+
{ name: 'Products (product catalog, inventory)', value: 'products', checked: false },
|
|
410
|
+
{ name: 'Checkout (cart, payments)', value: 'checkout', checked: false },
|
|
411
|
+
{ name: 'Invoicing (B2B/B2C invoices)', value: 'invoicing', checked: false },
|
|
412
|
+
{ name: 'Benefits (claims, commissions)', value: 'benefits', checked: false },
|
|
413
|
+
{ name: 'Certificates (CME, attendance)', value: 'certificates', checked: false },
|
|
414
|
+
{ name: 'Projects (task management)', value: 'projects', checked: false },
|
|
414
415
|
{ name: 'OAuth Authentication', value: 'oauth', checked: false },
|
|
415
416
|
],
|
|
416
417
|
},
|
|
417
418
|
]);
|
|
418
419
|
|
|
420
|
+
// Step 4.5: Integration path selection
|
|
421
|
+
console.log(chalk.cyan('\n đ¤ī¸ Integration Path\n'));
|
|
422
|
+
const { integrationPath } = await inquirer.prompt([
|
|
423
|
+
{
|
|
424
|
+
type: 'list',
|
|
425
|
+
name: 'integrationPath',
|
|
426
|
+
message: 'Choose your integration approach:',
|
|
427
|
+
choices: [
|
|
428
|
+
{
|
|
429
|
+
name: 'Quick Start (Recommended) - Full-stack with UI components & database',
|
|
430
|
+
value: 'quickstart',
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: 'API Only - Just the typed API client, you build the UI',
|
|
434
|
+
value: 'api-only',
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: 'MCP-Assisted - AI-powered custom generation with Claude Code',
|
|
438
|
+
value: 'mcp-assisted',
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
]);
|
|
443
|
+
console.log(chalk.green(` â
Path: ${integrationPath === 'quickstart' ? 'Quick Start' : integrationPath === 'api-only' ? 'API Only' : 'MCP-Assisted'}\n`));
|
|
444
|
+
|
|
445
|
+
// Step 4.6: Database selection (for Quick Start path when no DB detected)
|
|
446
|
+
let selectedDatabase = null;
|
|
447
|
+
if (integrationPath === 'quickstart') {
|
|
448
|
+
const dbDetection = detection.database || { hasDatabase: false };
|
|
449
|
+
|
|
450
|
+
if (!dbDetection.hasDatabase) {
|
|
451
|
+
console.log(chalk.yellow(' âšī¸ No database detected in your project.\n'));
|
|
452
|
+
|
|
453
|
+
const { database } = await inquirer.prompt([
|
|
454
|
+
{
|
|
455
|
+
type: 'list',
|
|
456
|
+
name: 'database',
|
|
457
|
+
message: 'Which database would you like to use?',
|
|
458
|
+
choices: [
|
|
459
|
+
{
|
|
460
|
+
name: 'Convex (Recommended) - Real-time, serverless, TypeScript-first',
|
|
461
|
+
value: 'convex',
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: 'Supabase - PostgreSQL with Auth, Storage, and Edge Functions',
|
|
465
|
+
value: 'supabase',
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'None - I\'ll set up my own database later',
|
|
469
|
+
value: 'none',
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
selectedDatabase = database !== 'none' ? database : null;
|
|
476
|
+
if (selectedDatabase) {
|
|
477
|
+
console.log(chalk.green(` â
Database: ${selectedDatabase}\n`));
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
console.log(chalk.green(` â
Detected ${dbDetection.primary?.type || 'existing'} database\n`));
|
|
481
|
+
selectedDatabase = dbDetection.primary?.type || 'existing';
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
419
485
|
// Step 5: OAuth provider selection (if OAuth enabled)
|
|
420
486
|
let oauthProviders = [];
|
|
421
487
|
if (features.includes('oauth')) {
|
|
@@ -489,15 +555,38 @@ async function handleSpread() {
|
|
|
489
555
|
isTypeScript,
|
|
490
556
|
routerType,
|
|
491
557
|
frameworkType: detection.framework.type || 'unknown',
|
|
558
|
+
integrationPath,
|
|
559
|
+
selectedDatabase,
|
|
492
560
|
};
|
|
493
561
|
|
|
494
562
|
const generatedFiles = await fileGenerator.generate(generationOptions);
|
|
495
563
|
|
|
496
564
|
// Display results
|
|
497
565
|
console.log(chalk.green(' â
Files generated:\n'));
|
|
566
|
+
|
|
567
|
+
// API client files (api-only and quickstart paths)
|
|
498
568
|
if (generatedFiles.apiClient) {
|
|
499
569
|
console.log(chalk.gray(` âĸ ${path.relative(process.cwd(), generatedFiles.apiClient)}`));
|
|
500
570
|
}
|
|
571
|
+
if (generatedFiles.types) {
|
|
572
|
+
console.log(chalk.gray(` âĸ ${path.relative(process.cwd(), generatedFiles.types)}`));
|
|
573
|
+
}
|
|
574
|
+
if (generatedFiles.webhooks) {
|
|
575
|
+
console.log(chalk.gray(` âĸ ${path.relative(process.cwd(), generatedFiles.webhooks)}`));
|
|
576
|
+
}
|
|
577
|
+
if (generatedFiles.index) {
|
|
578
|
+
console.log(chalk.gray(` âĸ ${path.relative(process.cwd(), generatedFiles.index)}`));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// MCP files (mcp-assisted path)
|
|
582
|
+
if (generatedFiles.mcpConfig) {
|
|
583
|
+
console.log(chalk.gray(` âĸ ${path.relative(process.cwd(), generatedFiles.mcpConfig)}`));
|
|
584
|
+
}
|
|
585
|
+
if (generatedFiles.mcpGuide) {
|
|
586
|
+
console.log(chalk.gray(` âĸ ${path.relative(process.cwd(), generatedFiles.mcpGuide)}`));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Common files
|
|
501
590
|
if (generatedFiles.envFile) {
|
|
502
591
|
console.log(chalk.gray(` âĸ ${path.relative(process.cwd(), generatedFiles.envFile)}`));
|
|
503
592
|
}
|
|
@@ -641,6 +730,8 @@ async function handleSpread() {
|
|
|
641
730
|
oauthProviders,
|
|
642
731
|
productionDomain,
|
|
643
732
|
frameworkType: detection.framework.type,
|
|
733
|
+
integrationPath,
|
|
734
|
+
selectedDatabase,
|
|
644
735
|
createdAt: Date.now(),
|
|
645
736
|
updatedAt: isUpdate ? Date.now() : undefined,
|
|
646
737
|
};
|
|
@@ -678,6 +769,10 @@ async function handleSpread() {
|
|
|
678
769
|
|
|
679
770
|
console.log(chalk.gray(' Your project is now connected to L4YERCAK3! đ°\n'));
|
|
680
771
|
|
|
772
|
+
// Show menu for next actions
|
|
773
|
+
const action = await showMainMenu({ isLoggedIn: true, isInProject: true, hasExistingConfig: true });
|
|
774
|
+
await executeMenuAction(action);
|
|
775
|
+
|
|
681
776
|
} catch (error) {
|
|
682
777
|
console.error(chalk.red(`\n â Error: ${error.message}\n`));
|
|
683
778
|
if (error.stack) {
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Detector
|
|
3
|
+
* Detects existing database configurations in a project
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a file exists
|
|
11
|
+
* @param {string} filePath - Path to check
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
function fileExists(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.existsSync(filePath);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read and parse package.json
|
|
24
|
+
* @param {string} projectPath - Project root path
|
|
25
|
+
* @returns {object|null}
|
|
26
|
+
*/
|
|
27
|
+
function readPackageJson(projectPath) {
|
|
28
|
+
try {
|
|
29
|
+
const packagePath = path.join(projectPath, 'package.json');
|
|
30
|
+
if (fileExists(packagePath)) {
|
|
31
|
+
const content = fs.readFileSync(packagePath, 'utf8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Ignore parse errors
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect database configuration in a project
|
|
42
|
+
* @param {string} projectPath - Project root path
|
|
43
|
+
* @returns {object} Detection result
|
|
44
|
+
*/
|
|
45
|
+
function detectDatabase(projectPath = process.cwd()) {
|
|
46
|
+
const detections = [];
|
|
47
|
+
|
|
48
|
+
// Check for Convex
|
|
49
|
+
const convexDir = path.join(projectPath, 'convex');
|
|
50
|
+
if (fileExists(convexDir)) {
|
|
51
|
+
const hasSchema = fileExists(path.join(convexDir, 'schema.ts')) ||
|
|
52
|
+
fileExists(path.join(convexDir, 'schema.js'));
|
|
53
|
+
detections.push({
|
|
54
|
+
type: 'convex',
|
|
55
|
+
confidence: 'high',
|
|
56
|
+
configPath: 'convex/',
|
|
57
|
+
hasSchema,
|
|
58
|
+
details: {
|
|
59
|
+
schemaFile: hasSchema ? 'convex/schema.ts' : null,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for Supabase
|
|
65
|
+
const supabaseDir = path.join(projectPath, 'supabase');
|
|
66
|
+
if (fileExists(supabaseDir)) {
|
|
67
|
+
const hasMigrations = fileExists(path.join(supabaseDir, 'migrations'));
|
|
68
|
+
detections.push({
|
|
69
|
+
type: 'supabase',
|
|
70
|
+
confidence: 'high',
|
|
71
|
+
configPath: 'supabase/',
|
|
72
|
+
hasMigrations,
|
|
73
|
+
details: {
|
|
74
|
+
migrationsDir: hasMigrations ? 'supabase/migrations/' : null,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for Prisma
|
|
80
|
+
const prismaDir = path.join(projectPath, 'prisma');
|
|
81
|
+
if (fileExists(prismaDir)) {
|
|
82
|
+
const hasSchema = fileExists(path.join(prismaDir, 'schema.prisma'));
|
|
83
|
+
detections.push({
|
|
84
|
+
type: 'prisma',
|
|
85
|
+
confidence: 'high',
|
|
86
|
+
configPath: 'prisma/',
|
|
87
|
+
hasSchema,
|
|
88
|
+
details: {
|
|
89
|
+
schemaFile: hasSchema ? 'prisma/schema.prisma' : null,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for Drizzle config
|
|
95
|
+
const drizzleConfig = [
|
|
96
|
+
'drizzle.config.ts',
|
|
97
|
+
'drizzle.config.js',
|
|
98
|
+
'drizzle.config.mjs',
|
|
99
|
+
].find(f => fileExists(path.join(projectPath, f)));
|
|
100
|
+
if (drizzleConfig) {
|
|
101
|
+
detections.push({
|
|
102
|
+
type: 'drizzle',
|
|
103
|
+
confidence: 'high',
|
|
104
|
+
configPath: drizzleConfig,
|
|
105
|
+
details: {
|
|
106
|
+
configFile: drizzleConfig,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check package.json for database dependencies
|
|
112
|
+
const packageJson = readPackageJson(projectPath);
|
|
113
|
+
if (packageJson) {
|
|
114
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
115
|
+
|
|
116
|
+
// Convex from package.json
|
|
117
|
+
if (deps['convex'] && !detections.find(d => d.type === 'convex')) {
|
|
118
|
+
detections.push({
|
|
119
|
+
type: 'convex',
|
|
120
|
+
confidence: 'medium',
|
|
121
|
+
source: 'package.json',
|
|
122
|
+
details: {
|
|
123
|
+
version: deps['convex'],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Supabase from package.json
|
|
129
|
+
if (deps['@supabase/supabase-js'] && !detections.find(d => d.type === 'supabase')) {
|
|
130
|
+
detections.push({
|
|
131
|
+
type: 'supabase',
|
|
132
|
+
confidence: 'medium',
|
|
133
|
+
source: 'package.json',
|
|
134
|
+
details: {
|
|
135
|
+
version: deps['@supabase/supabase-js'],
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prisma from package.json
|
|
141
|
+
if ((deps['prisma'] || deps['@prisma/client']) && !detections.find(d => d.type === 'prisma')) {
|
|
142
|
+
detections.push({
|
|
143
|
+
type: 'prisma',
|
|
144
|
+
confidence: 'medium',
|
|
145
|
+
source: 'package.json',
|
|
146
|
+
details: {
|
|
147
|
+
version: deps['prisma'] || deps['@prisma/client'],
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Drizzle from package.json
|
|
153
|
+
if (deps['drizzle-orm'] && !detections.find(d => d.type === 'drizzle')) {
|
|
154
|
+
detections.push({
|
|
155
|
+
type: 'drizzle',
|
|
156
|
+
confidence: 'medium',
|
|
157
|
+
source: 'package.json',
|
|
158
|
+
details: {
|
|
159
|
+
version: deps['drizzle-orm'],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MongoDB/Mongoose
|
|
165
|
+
if (deps['mongoose']) {
|
|
166
|
+
detections.push({
|
|
167
|
+
type: 'mongodb',
|
|
168
|
+
confidence: 'medium',
|
|
169
|
+
source: 'package.json',
|
|
170
|
+
details: {
|
|
171
|
+
client: 'mongoose',
|
|
172
|
+
version: deps['mongoose'],
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (deps['mongodb'] && !detections.find(d => d.type === 'mongodb')) {
|
|
178
|
+
detections.push({
|
|
179
|
+
type: 'mongodb',
|
|
180
|
+
confidence: 'medium',
|
|
181
|
+
source: 'package.json',
|
|
182
|
+
details: {
|
|
183
|
+
client: 'mongodb',
|
|
184
|
+
version: deps['mongodb'],
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Firebase/Firestore
|
|
190
|
+
if (deps['firebase'] || deps['firebase-admin']) {
|
|
191
|
+
detections.push({
|
|
192
|
+
type: 'firebase',
|
|
193
|
+
confidence: 'medium',
|
|
194
|
+
source: 'package.json',
|
|
195
|
+
details: {
|
|
196
|
+
client: deps['firebase'] ? 'firebase' : 'firebase-admin',
|
|
197
|
+
version: deps['firebase'] || deps['firebase-admin'],
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// PostgreSQL (pg)
|
|
203
|
+
if (deps['pg'] && !detections.find(d => ['prisma', 'drizzle', 'supabase'].includes(d.type))) {
|
|
204
|
+
detections.push({
|
|
205
|
+
type: 'postgresql',
|
|
206
|
+
confidence: 'low',
|
|
207
|
+
source: 'package.json',
|
|
208
|
+
details: {
|
|
209
|
+
client: 'pg',
|
|
210
|
+
version: deps['pg'],
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MySQL
|
|
216
|
+
if (deps['mysql2'] || deps['mysql']) {
|
|
217
|
+
detections.push({
|
|
218
|
+
type: 'mysql',
|
|
219
|
+
confidence: 'low',
|
|
220
|
+
source: 'package.json',
|
|
221
|
+
details: {
|
|
222
|
+
client: deps['mysql2'] ? 'mysql2' : 'mysql',
|
|
223
|
+
version: deps['mysql2'] || deps['mysql'],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort by confidence (high first)
|
|
230
|
+
const sortedDetections = detections.sort((a, b) => {
|
|
231
|
+
const confidenceOrder = { high: 2, medium: 1, low: 0 };
|
|
232
|
+
return (confidenceOrder[b.confidence] || 0) - (confidenceOrder[a.confidence] || 0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
hasDatabase: sortedDetections.length > 0,
|
|
237
|
+
detections: sortedDetections,
|
|
238
|
+
primary: sortedDetections[0] || null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
detect: detectDatabase,
|
|
244
|
+
detectDatabase,
|
|
245
|
+
};
|
package/src/detectors/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const registry = require('./registry');
|
|
|
10
10
|
const githubDetector = require('./github-detector');
|
|
11
11
|
const apiClientDetector = require('./api-client-detector');
|
|
12
12
|
const oauthDetector = require('./oauth-detector');
|
|
13
|
+
const databaseDetector = require('./database-detector');
|
|
13
14
|
|
|
14
15
|
class ProjectDetector {
|
|
15
16
|
/**
|
|
@@ -26,9 +27,10 @@ class ProjectDetector {
|
|
|
26
27
|
const githubInfo = githubDetector.detect(projectPath);
|
|
27
28
|
const apiClientInfo = apiClientDetector.detect(projectPath);
|
|
28
29
|
const oauthInfo = oauthDetector.detect(projectPath);
|
|
30
|
+
const databaseInfo = databaseDetector.detect(projectPath);
|
|
29
31
|
|
|
30
32
|
// Get detector instance if we have a match
|
|
31
|
-
const detector = frameworkDetection.detected
|
|
33
|
+
const detector = frameworkDetection.detected
|
|
32
34
|
? registry.getDetector(frameworkDetection.detected)
|
|
33
35
|
: null;
|
|
34
36
|
|
|
@@ -41,22 +43,33 @@ class ProjectDetector {
|
|
|
41
43
|
supportedFeatures: detector?.getSupportedFeatures() || {},
|
|
42
44
|
availableGenerators: detector?.getAvailableGenerators() || [],
|
|
43
45
|
},
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
// Project metadata (framework-agnostic)
|
|
46
48
|
github: githubInfo,
|
|
47
49
|
apiClient: apiClientInfo,
|
|
48
50
|
oauth: oauthInfo,
|
|
49
|
-
|
|
51
|
+
database: databaseInfo,
|
|
52
|
+
|
|
50
53
|
// Raw detection results (for debugging)
|
|
51
54
|
_raw: {
|
|
52
55
|
frameworkResults: frameworkDetection.allResults,
|
|
53
56
|
},
|
|
54
|
-
|
|
57
|
+
|
|
55
58
|
// Project path
|
|
56
59
|
projectPath,
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Detect database configuration in a project
|
|
65
|
+
*
|
|
66
|
+
* @param {string} projectPath - Path to project directory
|
|
67
|
+
* @returns {object} Database detection results
|
|
68
|
+
*/
|
|
69
|
+
detectDatabase(projectPath = process.cwd()) {
|
|
70
|
+
return databaseDetector.detect(projectPath);
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
/**
|
|
61
74
|
* Get detector for a specific project type
|
|
62
75
|
*
|