@codebakers/cli 1.3.0 → 1.4.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.
@@ -0,0 +1,702 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { createInterface } from 'readline';
6
+
7
+ async function prompt(question: string): Promise<string> {
8
+ const rl = createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ });
12
+
13
+ return new Promise((resolve) => {
14
+ rl.question(question, (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ }
20
+
21
+ // Convert to PascalCase
22
+ function toPascalCase(str: string): string {
23
+ return str
24
+ .split(/[-_\s]+/)
25
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
26
+ .join('');
27
+ }
28
+
29
+ // Convert to kebab-case
30
+ function toKebabCase(str: string): string {
31
+ return str
32
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
33
+ .replace(/[\s_]+/g, '-')
34
+ .toLowerCase();
35
+ }
36
+
37
+ // Convert to camelCase
38
+ function toCamelCase(str: string): string {
39
+ const pascal = toPascalCase(str);
40
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
41
+ }
42
+
43
+ // Component template
44
+ function componentTemplate(name: string, hasProps: boolean): string {
45
+ const pascalName = toPascalCase(name);
46
+
47
+ if (hasProps) {
48
+ return `import { cn } from '@/lib/utils';
49
+
50
+ interface ${pascalName}Props {
51
+ className?: string;
52
+ children?: React.ReactNode;
53
+ }
54
+
55
+ export function ${pascalName}({ className, children }: ${pascalName}Props) {
56
+ return (
57
+ <div className={cn('', className)}>
58
+ {children}
59
+ </div>
60
+ );
61
+ }
62
+ `;
63
+ }
64
+
65
+ return `import { cn } from '@/lib/utils';
66
+
67
+ export function ${pascalName}() {
68
+ return (
69
+ <div>
70
+ {/* ${pascalName} content */}
71
+ </div>
72
+ );
73
+ }
74
+ `;
75
+ }
76
+
77
+ // API route template
78
+ function apiRouteTemplate(name: string, methods: string[]): string {
79
+ const lines: string[] = [
80
+ `import { NextRequest, NextResponse } from 'next/server';`,
81
+ `import { z } from 'zod';`,
82
+ `import { db } from '@/db';`,
83
+ ``,
84
+ ];
85
+
86
+ if (methods.includes('GET')) {
87
+ lines.push(`export async function GET(request: NextRequest) {
88
+ try {
89
+ // TODO: Implement GET logic
90
+ return NextResponse.json({ message: 'Success' });
91
+ } catch (error) {
92
+ console.error('GET /${name} error:', error);
93
+ return NextResponse.json(
94
+ { error: 'Internal server error' },
95
+ { status: 500 }
96
+ );
97
+ }
98
+ }
99
+ `);
100
+ }
101
+
102
+ if (methods.includes('POST')) {
103
+ const schemaName = `Create${toPascalCase(name)}Schema`;
104
+ lines.push(`const ${schemaName} = z.object({
105
+ // TODO: Define your schema
106
+ name: z.string().min(1),
107
+ });
108
+
109
+ export async function POST(request: NextRequest) {
110
+ try {
111
+ const body = await request.json();
112
+ const validated = ${schemaName}.parse(body);
113
+
114
+ // TODO: Implement POST logic
115
+ return NextResponse.json({ message: 'Created', data: validated });
116
+ } catch (error) {
117
+ if (error instanceof z.ZodError) {
118
+ return NextResponse.json(
119
+ { error: 'Validation failed', details: error.errors },
120
+ { status: 400 }
121
+ );
122
+ }
123
+ console.error('POST /${name} error:', error);
124
+ return NextResponse.json(
125
+ { error: 'Internal server error' },
126
+ { status: 500 }
127
+ );
128
+ }
129
+ }
130
+ `);
131
+ }
132
+
133
+ if (methods.includes('PUT')) {
134
+ const schemaName = `Update${toPascalCase(name)}Schema`;
135
+ lines.push(`const ${schemaName} = z.object({
136
+ // TODO: Define your update schema
137
+ id: z.string().uuid(),
138
+ name: z.string().min(1).optional(),
139
+ });
140
+
141
+ export async function PUT(request: NextRequest) {
142
+ try {
143
+ const body = await request.json();
144
+ const validated = ${schemaName}.parse(body);
145
+
146
+ // TODO: Implement PUT logic
147
+ return NextResponse.json({ message: 'Updated', data: validated });
148
+ } catch (error) {
149
+ if (error instanceof z.ZodError) {
150
+ return NextResponse.json(
151
+ { error: 'Validation failed', details: error.errors },
152
+ { status: 400 }
153
+ );
154
+ }
155
+ console.error('PUT /${name} error:', error);
156
+ return NextResponse.json(
157
+ { error: 'Internal server error' },
158
+ { status: 500 }
159
+ );
160
+ }
161
+ }
162
+ `);
163
+ }
164
+
165
+ if (methods.includes('DELETE')) {
166
+ lines.push(`export async function DELETE(request: NextRequest) {
167
+ try {
168
+ const { searchParams } = new URL(request.url);
169
+ const id = searchParams.get('id');
170
+
171
+ if (!id) {
172
+ return NextResponse.json(
173
+ { error: 'ID is required' },
174
+ { status: 400 }
175
+ );
176
+ }
177
+
178
+ // TODO: Implement DELETE logic
179
+ return NextResponse.json({ message: 'Deleted' });
180
+ } catch (error) {
181
+ console.error('DELETE /${name} error:', error);
182
+ return NextResponse.json(
183
+ { error: 'Internal server error' },
184
+ { status: 500 }
185
+ );
186
+ }
187
+ }
188
+ `);
189
+ }
190
+
191
+ return lines.join('\n');
192
+ }
193
+
194
+ // Service template
195
+ function serviceTemplate(name: string): string {
196
+ const pascalName = toPascalCase(name);
197
+ const camelName = toCamelCase(name);
198
+
199
+ return `import { db } from '@/db';
200
+ import { z } from 'zod';
201
+
202
+ // Validation schemas
203
+ export const Create${pascalName}Schema = z.object({
204
+ // TODO: Define your schema
205
+ name: z.string().min(1),
206
+ });
207
+
208
+ export const Update${pascalName}Schema = Create${pascalName}Schema.partial();
209
+
210
+ export type Create${pascalName}Input = z.infer<typeof Create${pascalName}Schema>;
211
+ export type Update${pascalName}Input = z.infer<typeof Update${pascalName}Schema>;
212
+
213
+ /**
214
+ * ${pascalName} Service
215
+ * Handles all ${camelName}-related business logic
216
+ */
217
+ export const ${camelName}Service = {
218
+ /**
219
+ * Get all ${camelName}s
220
+ */
221
+ async getAll() {
222
+ // TODO: Implement
223
+ return [];
224
+ },
225
+
226
+ /**
227
+ * Get a single ${camelName} by ID
228
+ */
229
+ async getById(id: string) {
230
+ // TODO: Implement
231
+ return null;
232
+ },
233
+
234
+ /**
235
+ * Create a new ${camelName}
236
+ */
237
+ async create(input: Create${pascalName}Input) {
238
+ const validated = Create${pascalName}Schema.parse(input);
239
+ // TODO: Implement
240
+ return validated;
241
+ },
242
+
243
+ /**
244
+ * Update an existing ${camelName}
245
+ */
246
+ async update(id: string, input: Update${pascalName}Input) {
247
+ const validated = Update${pascalName}Schema.parse(input);
248
+ // TODO: Implement
249
+ return { id, ...validated };
250
+ },
251
+
252
+ /**
253
+ * Delete a ${camelName}
254
+ */
255
+ async delete(id: string) {
256
+ // TODO: Implement
257
+ return true;
258
+ },
259
+ };
260
+ `;
261
+ }
262
+
263
+ // Hook template
264
+ function hookTemplate(name: string): string {
265
+ const hookName = name.startsWith('use') ? name : `use${toPascalCase(name)}`;
266
+ const pascalName = toPascalCase(name.replace(/^use/i, ''));
267
+
268
+ return `'use client';
269
+
270
+ import { useState, useEffect, useCallback } from 'react';
271
+
272
+ interface ${pascalName}State {
273
+ data: unknown | null;
274
+ isLoading: boolean;
275
+ error: Error | null;
276
+ }
277
+
278
+ interface ${hookName}Options {
279
+ // TODO: Add options if needed
280
+ }
281
+
282
+ export function ${hookName}(options?: ${hookName}Options) {
283
+ const [state, setState] = useState<${pascalName}State>({
284
+ data: null,
285
+ isLoading: false,
286
+ error: null,
287
+ });
288
+
289
+ const fetch${pascalName} = useCallback(async () => {
290
+ setState(prev => ({ ...prev, isLoading: true, error: null }));
291
+
292
+ try {
293
+ // TODO: Implement fetch logic
294
+ const data = null;
295
+ setState({ data, isLoading: false, error: null });
296
+ } catch (error) {
297
+ setState(prev => ({
298
+ ...prev,
299
+ isLoading: false,
300
+ error: error instanceof Error ? error : new Error('Unknown error'),
301
+ }));
302
+ }
303
+ }, []);
304
+
305
+ useEffect(() => {
306
+ fetch${pascalName}();
307
+ }, [fetch${pascalName}]);
308
+
309
+ return {
310
+ ...state,
311
+ refetch: fetch${pascalName},
312
+ };
313
+ }
314
+ `;
315
+ }
316
+
317
+ // Page template
318
+ function pageTemplate(name: string, isServer: boolean): string {
319
+ const pascalName = toPascalCase(name);
320
+
321
+ if (isServer) {
322
+ return `import { Suspense } from 'react';
323
+
324
+ export const metadata = {
325
+ title: '${pascalName}',
326
+ description: '${pascalName} page',
327
+ };
328
+
329
+ async function ${pascalName}Content() {
330
+ // TODO: Fetch data server-side
331
+ const data = null;
332
+
333
+ return (
334
+ <div>
335
+ <h1 className="text-2xl font-bold">${pascalName}</h1>
336
+ {/* Content here */}
337
+ </div>
338
+ );
339
+ }
340
+
341
+ export default function ${pascalName}Page() {
342
+ return (
343
+ <main className="container mx-auto py-8">
344
+ <Suspense fallback={<div>Loading...</div>}>
345
+ <${pascalName}Content />
346
+ </Suspense>
347
+ </main>
348
+ );
349
+ }
350
+ `;
351
+ }
352
+
353
+ return `'use client';
354
+
355
+ import { useState } from 'react';
356
+
357
+ export default function ${pascalName}Page() {
358
+ const [loading, setLoading] = useState(false);
359
+
360
+ return (
361
+ <main className="container mx-auto py-8">
362
+ <h1 className="text-2xl font-bold">${pascalName}</h1>
363
+ {/* Content here */}
364
+ </main>
365
+ );
366
+ }
367
+ `;
368
+ }
369
+
370
+ // Schema template for Drizzle
371
+ function schemaTemplate(name: string): string {
372
+ const tableName = toKebabCase(name).replace(/-/g, '_');
373
+ const pascalName = toPascalCase(name);
374
+
375
+ return `import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core';
376
+
377
+ export const ${tableName} = pgTable('${tableName}', {
378
+ id: uuid('id').primaryKey().defaultRandom(),
379
+ // TODO: Add your columns
380
+ name: text('name').notNull(),
381
+ description: text('description'),
382
+ isActive: boolean('is_active').default(true),
383
+ createdAt: timestamp('created_at').defaultNow().notNull(),
384
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
385
+ });
386
+
387
+ export type ${pascalName} = typeof ${tableName}.$inferSelect;
388
+ export type New${pascalName} = typeof ${tableName}.$inferInsert;
389
+ `;
390
+ }
391
+
392
+ // Form template
393
+ function formTemplate(name: string): string {
394
+ const pascalName = toPascalCase(name);
395
+ const camelName = toCamelCase(name);
396
+
397
+ return `'use client';
398
+
399
+ import { useForm } from 'react-hook-form';
400
+ import { zodResolver } from '@hookform/resolvers/zod';
401
+ import { z } from 'zod';
402
+
403
+ const ${camelName}Schema = z.object({
404
+ // TODO: Define your form fields
405
+ name: z.string().min(1, 'Name is required'),
406
+ email: z.string().email('Invalid email'),
407
+ });
408
+
409
+ type ${pascalName}FormData = z.infer<typeof ${camelName}Schema>;
410
+
411
+ interface ${pascalName}FormProps {
412
+ onSubmit: (data: ${pascalName}FormData) => void | Promise<void>;
413
+ defaultValues?: Partial<${pascalName}FormData>;
414
+ isLoading?: boolean;
415
+ }
416
+
417
+ export function ${pascalName}Form({ onSubmit, defaultValues, isLoading }: ${pascalName}FormProps) {
418
+ const {
419
+ register,
420
+ handleSubmit,
421
+ formState: { errors },
422
+ } = useForm<${pascalName}FormData>({
423
+ resolver: zodResolver(${camelName}Schema),
424
+ defaultValues,
425
+ });
426
+
427
+ return (
428
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
429
+ <div>
430
+ <label htmlFor="name" className="block text-sm font-medium">
431
+ Name
432
+ </label>
433
+ <input
434
+ {...register('name')}
435
+ type="text"
436
+ id="name"
437
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
438
+ disabled={isLoading}
439
+ />
440
+ {errors.name && (
441
+ <p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
442
+ )}
443
+ </div>
444
+
445
+ <div>
446
+ <label htmlFor="email" className="block text-sm font-medium">
447
+ Email
448
+ </label>
449
+ <input
450
+ {...register('email')}
451
+ type="email"
452
+ id="email"
453
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
454
+ disabled={isLoading}
455
+ />
456
+ {errors.email && (
457
+ <p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
458
+ )}
459
+ </div>
460
+
461
+ <button
462
+ type="submit"
463
+ disabled={isLoading}
464
+ className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
465
+ >
466
+ {isLoading ? 'Submitting...' : 'Submit'}
467
+ </button>
468
+ </form>
469
+ );
470
+ }
471
+ `;
472
+ }
473
+
474
+ interface GenerateOptions {
475
+ type?: string;
476
+ name?: string;
477
+ }
478
+
479
+ /**
480
+ * Generate code from templates
481
+ */
482
+ export async function generate(options: GenerateOptions): Promise<void> {
483
+ console.log(chalk.blue(`
484
+ ╔═══════════════════════════════════════════════════════════╗
485
+ ║ ║
486
+ ║ ${chalk.bold('CodeBakers Code Generator')} ║
487
+ ║ ║
488
+ ║ Generate production-ready code from templates ║
489
+ ║ ║
490
+ ╚═══════════════════════════════════════════════════════════╝
491
+ `));
492
+
493
+ const cwd = process.cwd();
494
+
495
+ // Check if we're in a project
496
+ const hasPackageJson = existsSync(join(cwd, 'package.json'));
497
+ if (!hasPackageJson) {
498
+ console.log(chalk.yellow(' No package.json found. Run this in a project directory.\n'));
499
+ console.log(chalk.gray(' Use `codebakers scaffold` to create a new project first.\n'));
500
+ return;
501
+ }
502
+
503
+ // Available generators
504
+ const generators = [
505
+ { key: '1', name: 'component', desc: 'React component with TypeScript' },
506
+ { key: '2', name: 'api', desc: 'Next.js API route with validation' },
507
+ { key: '3', name: 'service', desc: 'Business logic service with CRUD' },
508
+ { key: '4', name: 'hook', desc: 'React hook with state management' },
509
+ { key: '5', name: 'page', desc: 'Next.js page (server or client)' },
510
+ { key: '6', name: 'schema', desc: 'Drizzle database table schema' },
511
+ { key: '7', name: 'form', desc: 'Form with React Hook Form + Zod' },
512
+ ];
513
+
514
+ let type = options.type;
515
+ let name = options.name;
516
+
517
+ // If type not provided, ask
518
+ if (!type) {
519
+ console.log(chalk.white(' What would you like to generate?\n'));
520
+ for (const gen of generators) {
521
+ console.log(chalk.gray(` ${gen.key}. `) + chalk.cyan(gen.name) + chalk.gray(` - ${gen.desc}`));
522
+ }
523
+ console.log('');
524
+
525
+ let choice = '';
526
+ while (!['1', '2', '3', '4', '5', '6', '7'].includes(choice)) {
527
+ choice = await prompt(' Enter 1-7: ');
528
+ }
529
+
530
+ type = generators.find(g => g.key === choice)?.name;
531
+ }
532
+
533
+ // If name not provided, ask
534
+ if (!name) {
535
+ name = await prompt(` ${toPascalCase(type!)} name: `);
536
+ if (!name) {
537
+ console.log(chalk.red('\n Name is required.\n'));
538
+ return;
539
+ }
540
+ }
541
+
542
+ const spinner = ora(' Generating...').start();
543
+
544
+ try {
545
+ let filePath: string;
546
+ let content: string;
547
+
548
+ switch (type) {
549
+ case 'component': {
550
+ const hasProps = await prompt(' Include props interface? (Y/n): ');
551
+ content = componentTemplate(name, hasProps.toLowerCase() !== 'n');
552
+ const componentDir = join(cwd, 'src/components');
553
+ if (!existsSync(componentDir)) {
554
+ mkdirSync(componentDir, { recursive: true });
555
+ }
556
+ filePath = join(componentDir, `${toPascalCase(name)}.tsx`);
557
+ break;
558
+ }
559
+
560
+ case 'api': {
561
+ console.log(chalk.gray('\n Select HTTP methods (comma-separated):'));
562
+ console.log(chalk.gray(' Examples: GET,POST or GET,POST,PUT,DELETE\n'));
563
+ const methodsInput = await prompt(' Methods (GET,POST): ') || 'GET,POST';
564
+ const methods = methodsInput.toUpperCase().split(',').map(m => m.trim());
565
+ content = apiRouteTemplate(name, methods);
566
+ const apiDir = join(cwd, 'src/app/api', toKebabCase(name));
567
+ if (!existsSync(apiDir)) {
568
+ mkdirSync(apiDir, { recursive: true });
569
+ }
570
+ filePath = join(apiDir, 'route.ts');
571
+ break;
572
+ }
573
+
574
+ case 'service': {
575
+ content = serviceTemplate(name);
576
+ const serviceDir = join(cwd, 'src/services');
577
+ if (!existsSync(serviceDir)) {
578
+ mkdirSync(serviceDir, { recursive: true });
579
+ }
580
+ filePath = join(serviceDir, `${toKebabCase(name)}.ts`);
581
+ break;
582
+ }
583
+
584
+ case 'hook': {
585
+ content = hookTemplate(name);
586
+ const hookDir = join(cwd, 'src/hooks');
587
+ if (!existsSync(hookDir)) {
588
+ mkdirSync(hookDir, { recursive: true });
589
+ }
590
+ const hookName = name.startsWith('use') ? name : `use${toPascalCase(name)}`;
591
+ filePath = join(hookDir, `${hookName}.ts`);
592
+ break;
593
+ }
594
+
595
+ case 'page': {
596
+ const isServer = await prompt(' Server component? (Y/n): ');
597
+ content = pageTemplate(name, isServer.toLowerCase() !== 'n');
598
+ const pageDir = join(cwd, 'src/app', toKebabCase(name));
599
+ if (!existsSync(pageDir)) {
600
+ mkdirSync(pageDir, { recursive: true });
601
+ }
602
+ filePath = join(pageDir, 'page.tsx');
603
+ break;
604
+ }
605
+
606
+ case 'schema': {
607
+ content = schemaTemplate(name);
608
+ const schemaDir = join(cwd, 'src/db/schemas');
609
+ if (!existsSync(schemaDir)) {
610
+ mkdirSync(schemaDir, { recursive: true });
611
+ }
612
+ filePath = join(schemaDir, `${toKebabCase(name)}.ts`);
613
+ break;
614
+ }
615
+
616
+ case 'form': {
617
+ content = formTemplate(name);
618
+ const formDir = join(cwd, 'src/components/forms');
619
+ if (!existsSync(formDir)) {
620
+ mkdirSync(formDir, { recursive: true });
621
+ }
622
+ filePath = join(formDir, `${toPascalCase(name)}Form.tsx`);
623
+ break;
624
+ }
625
+
626
+ default:
627
+ spinner.fail('Unknown generator type');
628
+ return;
629
+ }
630
+
631
+ // Check if file already exists
632
+ if (existsSync(filePath)) {
633
+ spinner.warn('File already exists');
634
+ const overwrite = await prompt(' Overwrite? (y/N): ');
635
+ if (overwrite.toLowerCase() !== 'y') {
636
+ console.log(chalk.gray('\n Skipped.\n'));
637
+ return;
638
+ }
639
+ }
640
+
641
+ // Write the file
642
+ writeFileSync(filePath, content);
643
+
644
+ const relativePath = filePath.replace(cwd, '').replace(/\\/g, '/');
645
+ spinner.succeed(`Generated ${relativePath}`);
646
+
647
+ console.log(chalk.green(`
648
+ ╔═══════════════════════════════════════════════════════════╗
649
+ ║ ${chalk.bold('✓ Code generated successfully!')} ║
650
+ ╚═══════════════════════════════════════════════════════════╝
651
+ `));
652
+
653
+ console.log(chalk.white(' Created:\n'));
654
+ console.log(chalk.cyan(` ${relativePath}\n`));
655
+
656
+ // Show next steps based on type
657
+ console.log(chalk.white(' Next steps:\n'));
658
+ switch (type) {
659
+ case 'component':
660
+ console.log(chalk.gray(' 1. Import and use the component in your page'));
661
+ console.log(chalk.gray(' 2. Add any additional props you need'));
662
+ console.log(chalk.gray(' 3. Style it with Tailwind classes\n'));
663
+ break;
664
+ case 'api':
665
+ console.log(chalk.gray(' 1. Implement the TODO sections'));
666
+ console.log(chalk.gray(' 2. Add authentication if needed'));
667
+ console.log(chalk.gray(' 3. Test with curl or your frontend\n'));
668
+ break;
669
+ case 'service':
670
+ console.log(chalk.gray(' 1. Update the Zod schemas'));
671
+ console.log(chalk.gray(' 2. Implement database queries'));
672
+ console.log(chalk.gray(' 3. Import and use in your API routes\n'));
673
+ break;
674
+ case 'hook':
675
+ console.log(chalk.gray(' 1. Implement the fetch logic'));
676
+ console.log(chalk.gray(' 2. Update the return type'));
677
+ console.log(chalk.gray(' 3. Use it in your components\n'));
678
+ break;
679
+ case 'page':
680
+ console.log(chalk.gray(` 1. Visit /${toKebabCase(name)} in your browser`));
681
+ console.log(chalk.gray(' 2. Add your page content'));
682
+ console.log(chalk.gray(' 3. Update the metadata\n'));
683
+ break;
684
+ case 'schema':
685
+ console.log(chalk.gray(' 1. Add your column definitions'));
686
+ console.log(chalk.gray(' 2. Export from src/db/schema.ts'));
687
+ console.log(chalk.gray(' 3. Run `npm run db:push` to sync\n'));
688
+ break;
689
+ case 'form':
690
+ console.log(chalk.gray(' 1. Update the Zod schema with your fields'));
691
+ console.log(chalk.gray(' 2. Add form inputs for each field'));
692
+ console.log(chalk.gray(' 3. Handle form submission in parent\n'));
693
+ break;
694
+ }
695
+
696
+ } catch (error) {
697
+ spinner.fail('Generation failed');
698
+ const message = error instanceof Error ? error.message : 'Unknown error';
699
+ console.log(chalk.red(`\n Error: ${message}\n`));
700
+ process.exit(1);
701
+ }
702
+ }