@create-mastery/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/biome.json +6 -0
- package/cli.ts +85 -0
- package/commands/add/component.ts +73 -0
- package/commands/add/language.ts +126 -0
- package/commands/generate-schema.ts +56 -0
- package/commands/scripts.ts +15 -0
- package/commands/version.ts +38 -0
- package/package.json +31 -0
- package/utils/arts.ts +14 -0
package/README.md
ADDED
package/biome.json
ADDED
package/cli.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander'
|
|
4
|
+
import gradient from 'gradient-string'
|
|
5
|
+
import addComponent from './commands/add/component'
|
|
6
|
+
import addLanguage from './commands/add/language'
|
|
7
|
+
import { genSchema } from './commands/generate-schema'
|
|
8
|
+
import printScripts from './commands/scripts'
|
|
9
|
+
import printVersion from './commands/version'
|
|
10
|
+
import cliInfo from './package.json'
|
|
11
|
+
import { createMasteryASCIIArtBig } from './utils/arts'
|
|
12
|
+
import { execSync } from 'node:child_process'
|
|
13
|
+
import chalk from 'chalk'
|
|
14
|
+
|
|
15
|
+
const add = program.command('add').description('add a language or component')
|
|
16
|
+
const gen = program.command('gen').description('generate the dictionary schema')
|
|
17
|
+
|
|
18
|
+
program.version(cliInfo.version)
|
|
19
|
+
program.name('cm')
|
|
20
|
+
|
|
21
|
+
program.addHelpText(
|
|
22
|
+
'beforeAll',
|
|
23
|
+
gradient([
|
|
24
|
+
'#8dc4ff',
|
|
25
|
+
'#bddaff',
|
|
26
|
+
]).multiline(createMasteryASCIIArtBig)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// I know this may look silly, but it's for creating space between the prompt and the help message
|
|
30
|
+
program.addHelpText('afterAll', ' ')
|
|
31
|
+
|
|
32
|
+
program.description(cliInfo.description)
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('version')
|
|
36
|
+
.description(
|
|
37
|
+
'display the version of the Create Mastery website and some other useful information'
|
|
38
|
+
)
|
|
39
|
+
.option('-v, --verbose', 'verbose output of version command', false)
|
|
40
|
+
.action((options) => printVersion(options.verbose))
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command('scripts')
|
|
44
|
+
.description('display all the available scripts in package.json')
|
|
45
|
+
.action(() => printScripts())
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.command('clone')
|
|
49
|
+
.description('clone the Create Mastery repo')
|
|
50
|
+
.argument('<destination>', 'the directory where the repo will be cloned')
|
|
51
|
+
.action((destination) => {
|
|
52
|
+
try {
|
|
53
|
+
execSync(
|
|
54
|
+
`git clone https://github.com/Create-Mastery/website.git ${destination}`
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
console.log(chalk.blue('repo cloned in:'), destination)
|
|
58
|
+
console.log(
|
|
59
|
+
chalk.blue('now run `npm install` to install all the dependencies')
|
|
60
|
+
)
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('an error has occurred:', err)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
gen
|
|
67
|
+
.command('schema')
|
|
68
|
+
.description('generates the schema for the dictionaries')
|
|
69
|
+
.action(() => genSchema())
|
|
70
|
+
|
|
71
|
+
add
|
|
72
|
+
.command('component')
|
|
73
|
+
.description('creates a new component (in the src/app/components/ directory)')
|
|
74
|
+
.argument('<name>', 'component to add')
|
|
75
|
+
.option('-p, --props', 'the component is generated with props', false)
|
|
76
|
+
.option('-c, --client', 'the component is a client component', false)
|
|
77
|
+
.action((name, options) => addComponent(name, options.props, options.client))
|
|
78
|
+
|
|
79
|
+
add
|
|
80
|
+
.command('language')
|
|
81
|
+
.description('creates a new language for i18n')
|
|
82
|
+
.argument('<language>', 'language to add')
|
|
83
|
+
.action((language) => addLanguage(language))
|
|
84
|
+
|
|
85
|
+
program.parse()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = path.dirname(__filename)
|
|
8
|
+
const componentsDir = path.resolve(__dirname, '../../../src/components/')
|
|
9
|
+
|
|
10
|
+
export default function addComponent(
|
|
11
|
+
componentName: string,
|
|
12
|
+
props: boolean,
|
|
13
|
+
client: boolean
|
|
14
|
+
) {
|
|
15
|
+
const filePath = path.resolve(componentsDir, `${componentName}.tsx`)
|
|
16
|
+
const dirPath = path.dirname(filePath)
|
|
17
|
+
|
|
18
|
+
const rawName = componentName.trim()
|
|
19
|
+
|
|
20
|
+
if (!rawName) {
|
|
21
|
+
throw new Error('Component name cannot be empty')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fileName = rawName.split('/').pop()
|
|
25
|
+
|
|
26
|
+
if (!fileName) {
|
|
27
|
+
throw new Error('Invalid component name')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const finalName = fileName
|
|
31
|
+
.replace(/[-_](\w)/g, (_, c) => c.toUpperCase())
|
|
32
|
+
.replace(/^\w/, (c) => c.toUpperCase())
|
|
33
|
+
|
|
34
|
+
let content: string = ''
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(filePath)) {
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.red('The component'),
|
|
39
|
+
chalk.reset(`${rawName}.tsx`),
|
|
40
|
+
chalk.red('already exists')
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (client) {
|
|
47
|
+
content += `'use client'\n\n`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (props) {
|
|
51
|
+
content += `type Props = {\n\n}\n\n`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
content += `const ${finalName} = (${props ? '{ }: Props' : ''}) => {
|
|
55
|
+
return (
|
|
56
|
+
<div></div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default ${finalName}`
|
|
61
|
+
|
|
62
|
+
fs.mkdirSync(dirPath, {
|
|
63
|
+
recursive: true,
|
|
64
|
+
})
|
|
65
|
+
fs.writeFileSync(filePath, content)
|
|
66
|
+
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.blue('Component created -'),
|
|
69
|
+
chalk.reset(`${componentName}.tsx`)
|
|
70
|
+
)
|
|
71
|
+
console.log(chalk.blue('Props -'), chalk.reset(props))
|
|
72
|
+
console.log(chalk.blue('Client -'), chalk.reset(client))
|
|
73
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = path.dirname(__filename)
|
|
8
|
+
const dictionarieDir = path.resolve(__dirname, '../../../src/i18n/dictionaries')
|
|
9
|
+
|
|
10
|
+
type Json =
|
|
11
|
+
| string
|
|
12
|
+
| number
|
|
13
|
+
| boolean
|
|
14
|
+
| null
|
|
15
|
+
| Json[]
|
|
16
|
+
| {
|
|
17
|
+
[key: string]: Json
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripValues(obj: Json): Json {
|
|
21
|
+
if (obj === '$schema') return obj
|
|
22
|
+
if (typeof obj === 'string') return ''
|
|
23
|
+
if (Array.isArray(obj)) return obj.map(stripValues)
|
|
24
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(obj).map(([k, v]) => [
|
|
27
|
+
k,
|
|
28
|
+
k === '$schema' ? v : stripValues(v),
|
|
29
|
+
])
|
|
30
|
+
) as Json
|
|
31
|
+
}
|
|
32
|
+
return obj
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function genLocales(files: string[]) {
|
|
36
|
+
const locales = files.map((f) => `'${f.replace('.json', '')}'`).join(' | ')
|
|
37
|
+
const content = `export type locales = ${locales}\n`
|
|
38
|
+
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.resolve(__dirname, '../../../src/i18n/types/locales.ts'),
|
|
41
|
+
content
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function genI18n(files: string[]) {
|
|
46
|
+
const locales = files
|
|
47
|
+
.map((f) => `'${f.replace('.json', '')}'`)
|
|
48
|
+
.join(',\n\t\t')
|
|
49
|
+
const content = `export const i18n = {
|
|
50
|
+
defaultLocale: 'en',
|
|
51
|
+
locales: [
|
|
52
|
+
\t\t${locales}
|
|
53
|
+
],
|
|
54
|
+
}`
|
|
55
|
+
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
path.resolve(__dirname, '../../../src/i18n/i18n.ts'),
|
|
58
|
+
content
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function genDictionaryLoaders(files: string[]) {
|
|
63
|
+
const dictionaries = files.map((f) => f.replace('.json', ''))
|
|
64
|
+
|
|
65
|
+
let dictionariesLoaders: string = `import { type Locale } from '../config'\n\nexport const dictionariesLoaders: Locale = {\n`
|
|
66
|
+
|
|
67
|
+
for (const dictionary of dictionaries) {
|
|
68
|
+
dictionariesLoaders += `\t${dictionary}: () => import('./${dictionary}.json').then((module) => module.default),\n`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
dictionariesLoaders += '}'
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.resolve(
|
|
75
|
+
__dirname,
|
|
76
|
+
'../../../src/i18n/dictionaries/dictionaries-loaders.ts'
|
|
77
|
+
),
|
|
78
|
+
dictionariesLoaders
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default function addLanguage(language: string) {
|
|
83
|
+
const input = JSON.parse(fs.readFileSync(`${dictionarieDir}/en.json`, 'utf8'))
|
|
84
|
+
const output = stripValues(input)
|
|
85
|
+
|
|
86
|
+
let files: string[]
|
|
87
|
+
|
|
88
|
+
files = fs
|
|
89
|
+
.readdirSync(dictionarieDir)
|
|
90
|
+
.filter((f) => f.endsWith('.json') && f !== 'schema.schema.json')
|
|
91
|
+
|
|
92
|
+
if (files.includes(`${language}.json`)) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.red('This language:'),
|
|
95
|
+
chalk.reset(language),
|
|
96
|
+
chalk.red('already exists')
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
`${dictionarieDir}/${language}.json`,
|
|
104
|
+
JSON.stringify(output, null, 2)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
files = fs
|
|
108
|
+
.readdirSync(dictionarieDir)
|
|
109
|
+
.filter((f) => f.endsWith('.json') && f !== 'schema.schema.json')
|
|
110
|
+
|
|
111
|
+
genDictionaryLoaders(files)
|
|
112
|
+
genLocales(files)
|
|
113
|
+
genI18n(files)
|
|
114
|
+
|
|
115
|
+
console.log(chalk.blue('Language - '), chalk.reset(language))
|
|
116
|
+
console.log(
|
|
117
|
+
chalk.blue('File Create at - '),
|
|
118
|
+
chalk.reset(`@/i18n/dictionaries/${language}.json`)
|
|
119
|
+
)
|
|
120
|
+
console.log(chalk.blue('Regenerated files:'))
|
|
121
|
+
console.log(' @/src/i18n/i18n.ts')
|
|
122
|
+
console.log(' @/src/i18n/types/locales.ts')
|
|
123
|
+
console.log(' @/src/i18n/dictionaries/dictionaries-loaders.ts')
|
|
124
|
+
console.log()
|
|
125
|
+
console.log(chalk.blue('Now you need to add the actual values to the file!'))
|
|
126
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as GenerateSchema from 'generate-schema'
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = path.dirname(__filename)
|
|
10
|
+
const dictionarieDir = path.resolve(__dirname, '../../src/i18n/dictionaries/')
|
|
11
|
+
const dictionary = JSON.parse(
|
|
12
|
+
fs.readFileSync(`${dictionarieDir}/en.json`, 'utf8')
|
|
13
|
+
)
|
|
14
|
+
const schemaPath = path.resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
`${dictionarieDir}/schema.schema.json`
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
interface JsonSchema {
|
|
20
|
+
type: string
|
|
21
|
+
properties?: {
|
|
22
|
+
[key: string]: JsonSchema
|
|
23
|
+
}
|
|
24
|
+
required?: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function addRequiredRecursively(
|
|
28
|
+
schemaNode: JsonSchema,
|
|
29
|
+
dataNode: Record<string, unknown>
|
|
30
|
+
) {
|
|
31
|
+
if (
|
|
32
|
+
schemaNode.type === 'object'
|
|
33
|
+
&& dataNode
|
|
34
|
+
&& typeof dataNode === 'object'
|
|
35
|
+
) {
|
|
36
|
+
schemaNode.required = Object.keys(dataNode).filter((k) => k !== '$schema')
|
|
37
|
+
if (schemaNode.properties) {
|
|
38
|
+
for (const key of Object.keys(schemaNode.properties || {})) {
|
|
39
|
+
addRequiredRecursively(
|
|
40
|
+
schemaNode.properties[key],
|
|
41
|
+
dataNode[key] as Record<string, unknown>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function genSchema() {
|
|
49
|
+
const schema = GenerateSchema.json('dictionary schema', dictionary)
|
|
50
|
+
|
|
51
|
+
addRequiredRecursively(schema, dictionary)
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2), 'utf8')
|
|
54
|
+
|
|
55
|
+
console.log(chalk.blue('Schema generated successfully'))
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import siteInfo from '../../package.json'
|
|
3
|
+
import { createMasteryASCIIArtSmall } from '../utils/arts'
|
|
4
|
+
|
|
5
|
+
export default function printScripts() {
|
|
6
|
+
console.log(chalk.blue(createMasteryASCIIArtSmall))
|
|
7
|
+
console.log(chalk.blue('Available scripts:'))
|
|
8
|
+
console.log(chalk.blue('──────────────────────────────────────────'))
|
|
9
|
+
Object.entries(siteInfo.scripts).forEach(([key, value]) => {
|
|
10
|
+
console.log(
|
|
11
|
+
`${chalk.blue(key.padEnd(10))} ${chalk.blue('─')} ${chalk.reset(value)}`
|
|
12
|
+
)
|
|
13
|
+
})
|
|
14
|
+
console.log()
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { globSync } from 'glob'
|
|
6
|
+
import siteInfo from '../../package.json'
|
|
7
|
+
import { createMasteryASCIIArtSmall } from '../utils/arts'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
export default function printVersion(verbose: boolean) {
|
|
13
|
+
const dictionarieDir = path.resolve(__dirname, '../../src/i18n/dictionaries')
|
|
14
|
+
const guideDirectory = path.resolve(__dirname, '../../src/app/**/guide')
|
|
15
|
+
|
|
16
|
+
// since schema.schema.json is also counted we decrement the value by 1 to get the actual number of languages
|
|
17
|
+
const getLanguages = () =>
|
|
18
|
+
fs.readdirSync(dictionarieDir).filter((f) => f.endsWith('.json')).length - 1
|
|
19
|
+
|
|
20
|
+
const deps = Object.keys(siteInfo.dependencies).length
|
|
21
|
+
const devDeps = Object.keys(siteInfo.devDependencies).length
|
|
22
|
+
|
|
23
|
+
const languages: number = getLanguages()
|
|
24
|
+
const guides: number = globSync(`${guideDirectory}/**/*.tsx`).length
|
|
25
|
+
|
|
26
|
+
console.log(chalk.blue(createMasteryASCIIArtSmall))
|
|
27
|
+
console.log(chalk.blue('VERSION ─'), chalk.reset(siteInfo.version))
|
|
28
|
+
console.log(chalk.blue('GUIDES ─'), chalk.reset(guides))
|
|
29
|
+
console.log(chalk.blue('LANGUAGES ─'), chalk.reset(languages))
|
|
30
|
+
|
|
31
|
+
if (verbose) {
|
|
32
|
+
console.log(chalk.blue('──────────────────────────────────────────'))
|
|
33
|
+
console.log(chalk.blue('DEPENDENCIES ─'), chalk.reset(deps))
|
|
34
|
+
console.log(chalk.blue('DEV DEPENDENCIES ─'), chalk.reset(devDeps))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log()
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@create-mastery/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The official Create Website CLI, created to enhance the DX",
|
|
5
|
+
"homepage": "https://github.com/Create-Mastery/website/tree/main/cli#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Create-Mastery/website.git#readme"
|
|
9
|
+
},
|
|
10
|
+
"license": "GPL-3.0-only",
|
|
11
|
+
"author": "Create Mastery",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./cli.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"cm": "cli.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"gradient-string": "^3.0.0",
|
|
19
|
+
"chalk": "^5.6.2",
|
|
20
|
+
"commander": "^14.0.2",
|
|
21
|
+
"generate-schema": "^2.6.0",
|
|
22
|
+
"glob": "^13.0.0",
|
|
23
|
+
"tsx": "^4.21.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "^2.3.11"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"cli"
|
|
30
|
+
]
|
|
31
|
+
}
|
package/utils/arts.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const createMasteryASCIIArtSmall: string = `
|
|
2
|
+
┏━╸┏━┓┏━╸┏━┓╺┳╸┏━╸ ┏┳┓┏━┓┏━┓╺┳╸┏━╸┏━┓╻ ╻
|
|
3
|
+
┃ ┣┳┛┣╸ ┣━┫ ┃ ┣╸ ┃┃┃┣━┫┗━┓ ┃ ┣╸ ┣┳┛┗┳┛
|
|
4
|
+
┗━╸╹┗╸┗━╸╹ ╹ ╹ ┗━╸ ╹ ╹╹ ╹┗━┛ ╹ ┗━╸╹┗╸ ╹
|
|
5
|
+
──────────────────────────────────────────`
|
|
6
|
+
|
|
7
|
+
export const createMasteryASCIIArtBig: string = `
|
|
8
|
+
██████╗██████╗ ███████╗ █████╗ ████████╗███████╗ ███╗ ███╗ █████╗ ███████╗████████╗███████╗██████╗ ██╗ ██╗
|
|
9
|
+
██╔════╝██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██╔════╝ ████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗╚██╗ ██╔╝
|
|
10
|
+
██║ ██████╔╝█████╗ ███████║ ██║ █████╗ ██╔████╔██║███████║███████╗ ██║ █████╗ ██████╔╝ ╚████╔╝
|
|
11
|
+
██║ ██╔══██╗██╔══╝ ██╔══██║ ██║ ██╔══╝ ██║╚██╔╝██║██╔══██║╚════██║ ██║ ██╔══╝ ██╔══██╗ ╚██╔╝
|
|
12
|
+
╚██████╗██║ ██║███████╗██║ ██║ ██║ ███████╗ ██║ ╚═╝ ██║██║ ██║███████║ ██║ ███████╗██║ ██║ ██║
|
|
13
|
+
╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝
|
|
14
|
+
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────`
|