@grunnverk/kilde 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.
Files changed (75) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/pull_request_template.md +48 -0
  4. package/.github/workflows/deploy-docs.yml +59 -0
  5. package/.github/workflows/npm-publish.yml +48 -0
  6. package/.github/workflows/test.yml +48 -0
  7. package/CHANGELOG.md +92 -0
  8. package/CONTRIBUTING.md +438 -0
  9. package/LICENSE +190 -0
  10. package/PROJECT_SUMMARY.md +318 -0
  11. package/README.md +444 -0
  12. package/RELEASE_CHECKLIST.md +182 -0
  13. package/dist/application.js +166 -0
  14. package/dist/application.js.map +1 -0
  15. package/dist/commands/release.js +326 -0
  16. package/dist/commands/release.js.map +1 -0
  17. package/dist/constants.js +122 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/logging.js +176 -0
  20. package/dist/logging.js.map +1 -0
  21. package/dist/main.js +24 -0
  22. package/dist/main.js.map +1 -0
  23. package/dist/mcp-server.js +17467 -0
  24. package/dist/mcp-server.js.map +7 -0
  25. package/dist/utils/config.js +89 -0
  26. package/dist/utils/config.js.map +1 -0
  27. package/docs/AI_GUIDE.md +618 -0
  28. package/eslint.config.mjs +85 -0
  29. package/guide/architecture.md +776 -0
  30. package/guide/commands.md +580 -0
  31. package/guide/configuration.md +779 -0
  32. package/guide/mcp-integration.md +708 -0
  33. package/guide/overview.md +225 -0
  34. package/package.json +91 -0
  35. package/scripts/build-mcp.js +115 -0
  36. package/scripts/test-mcp-compliance.js +254 -0
  37. package/src/application.ts +246 -0
  38. package/src/commands/release.ts +450 -0
  39. package/src/constants.ts +162 -0
  40. package/src/logging.ts +210 -0
  41. package/src/main.ts +25 -0
  42. package/src/mcp/prompts/index.ts +98 -0
  43. package/src/mcp/resources.ts +121 -0
  44. package/src/mcp/server.ts +195 -0
  45. package/src/mcp/tools.ts +219 -0
  46. package/src/types.ts +131 -0
  47. package/src/utils/config.ts +181 -0
  48. package/tests/application.test.ts +114 -0
  49. package/tests/commands/commit.test.ts +248 -0
  50. package/tests/commands/release.test.ts +325 -0
  51. package/tests/constants.test.ts +118 -0
  52. package/tests/logging.test.ts +142 -0
  53. package/tests/mcp/prompts/index.test.ts +202 -0
  54. package/tests/mcp/resources.test.ts +166 -0
  55. package/tests/mcp/tools.test.ts +211 -0
  56. package/tests/utils/config.test.ts +212 -0
  57. package/tsconfig.json +32 -0
  58. package/vite.config.ts +107 -0
  59. package/vitest.config.ts +40 -0
  60. package/website/index.html +14 -0
  61. package/website/src/App.css +142 -0
  62. package/website/src/App.tsx +34 -0
  63. package/website/src/components/Commands.tsx +182 -0
  64. package/website/src/components/Configuration.tsx +214 -0
  65. package/website/src/components/Examples.tsx +234 -0
  66. package/website/src/components/Footer.css +99 -0
  67. package/website/src/components/Footer.tsx +93 -0
  68. package/website/src/components/GettingStarted.tsx +94 -0
  69. package/website/src/components/Hero.css +95 -0
  70. package/website/src/components/Hero.tsx +50 -0
  71. package/website/src/components/Navigation.css +102 -0
  72. package/website/src/components/Navigation.tsx +57 -0
  73. package/website/src/index.css +36 -0
  74. package/website/src/main.tsx +10 -0
  75. package/website/vite.config.ts +12 -0
@@ -0,0 +1,212 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ import { tmpdir } from 'os';
5
+ import {
6
+ loadConfig,
7
+ getDefaultConfig,
8
+ mergeWithDefaults,
9
+ getEffectiveConfig,
10
+ createSampleConfig,
11
+ saveSampleConfig,
12
+ configFileExists,
13
+ } from '../../src/utils/config';
14
+ import { KILDE_DEFAULTS } from '../../src/constants';
15
+
16
+ describe('config utilities', () => {
17
+ let testDir: string;
18
+
19
+ beforeEach(async () => {
20
+ testDir = await fs.mkdtemp(path.join(tmpdir(), 'kilde-test-'));
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await fs.rm(testDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe('loadConfig', () => {
28
+ it('should return null when no config file exists', async () => {
29
+ const config = await loadConfig(testDir);
30
+ expect(config).toBeNull();
31
+ });
32
+
33
+ it('should load YAML config from .kilde/config.yaml', async () => {
34
+ const configDir = path.join(testDir, '.kilde');
35
+ await fs.mkdir(configDir, { recursive: true });
36
+ const configPath = path.join(configDir, 'config.yaml');
37
+ await fs.writeFile(configPath, 'verbose: true\nmodel: gpt-4o', 'utf-8');
38
+
39
+ const config = await loadConfig(testDir);
40
+ expect(config).not.toBeNull();
41
+ expect(config?.verbose).toBe(true);
42
+ expect(config?.model).toBe('gpt-4o');
43
+ });
44
+
45
+ it('should load JSON config from .kilderc.json', async () => {
46
+ const configPath = path.join(testDir, '.kilderc.json');
47
+ await fs.writeFile(configPath, JSON.stringify({ verbose: true, debug: true }), 'utf-8');
48
+
49
+ const config = await loadConfig(testDir);
50
+ expect(config).not.toBeNull();
51
+ expect(config?.verbose).toBe(true);
52
+ expect(config?.debug).toBe(true);
53
+ });
54
+
55
+ it('should prioritize .kilde/config.yaml over other files', async () => {
56
+ // Create multiple config files
57
+ const configDir = path.join(testDir, '.kilde');
58
+ await fs.mkdir(configDir, { recursive: true });
59
+ await fs.writeFile(path.join(configDir, 'config.yaml'), 'verbose: true\nmodel: gpt-4o', 'utf-8');
60
+ await fs.writeFile(path.join(testDir, '.kilderc.json'), JSON.stringify({ verbose: false }), 'utf-8');
61
+
62
+ const config = await loadConfig(testDir);
63
+ expect(config?.verbose).toBe(true);
64
+ expect(config?.model).toBe('gpt-4o');
65
+ });
66
+
67
+ it('should handle malformed config gracefully', async () => {
68
+ const configPath = path.join(testDir, '.kilderc.json');
69
+ await fs.writeFile(configPath, 'not valid json', 'utf-8');
70
+
71
+ const config = await loadConfig(testDir);
72
+ expect(config).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe('getDefaultConfig', () => {
77
+ it('should return KILDE_DEFAULTS', () => {
78
+ const config = getDefaultConfig();
79
+ expect(config).toEqual(KILDE_DEFAULTS);
80
+ });
81
+
82
+ it('should include required fields', () => {
83
+ const config = getDefaultConfig();
84
+ expect(config.configDirectory).toBeDefined();
85
+ expect(config.discoveredConfigDirs).toBeDefined();
86
+ expect(config.resolvedConfigDirs).toBeDefined();
87
+ expect(config.verbose).toBeDefined();
88
+ expect(config.debug).toBeDefined();
89
+ expect(config.dryRun).toBeDefined();
90
+ });
91
+ });
92
+
93
+ describe('mergeWithDefaults', () => {
94
+ it('should return defaults when config is null', () => {
95
+ const merged = mergeWithDefaults(null);
96
+ expect(merged).toEqual(KILDE_DEFAULTS);
97
+ });
98
+
99
+ it('should merge config with defaults', () => {
100
+ const config = { verbose: true, model: 'gpt-4o' } as any;
101
+ const merged = mergeWithDefaults(config);
102
+
103
+ expect(merged.verbose).toBe(true);
104
+ expect(merged.model).toBe('gpt-4o');
105
+ expect(merged.debug).toBe(KILDE_DEFAULTS.debug);
106
+ expect(merged.configDirectory).toBe(KILDE_DEFAULTS.configDirectory);
107
+ });
108
+
109
+ it('should deep merge nested objects', () => {
110
+ const config = {
111
+ commit: {
112
+ sendit: true,
113
+ add: false,
114
+ }
115
+ } as any;
116
+ const merged = mergeWithDefaults(config);
117
+
118
+ expect(merged.commit?.sendit).toBe(true);
119
+ expect(merged.commit?.add).toBe(false);
120
+ expect(merged.commit?.cached).toBe(KILDE_DEFAULTS.commit?.cached);
121
+ });
122
+
123
+ it('should not merge arrays', () => {
124
+ const config = {
125
+ discoveredConfigDirs: ['custom-dir']
126
+ } as any;
127
+ const merged = mergeWithDefaults(config);
128
+
129
+ expect(merged.discoveredConfigDirs).toEqual(['custom-dir']);
130
+ });
131
+ });
132
+
133
+ describe('getEffectiveConfig', () => {
134
+ it('should return defaults when no config file exists', async () => {
135
+ const config = await getEffectiveConfig(testDir);
136
+ expect(config).toEqual(KILDE_DEFAULTS);
137
+ });
138
+
139
+ it('should merge loaded config with defaults', async () => {
140
+ const configPath = path.join(testDir, '.kilderc.json');
141
+ await fs.writeFile(configPath, JSON.stringify({ verbose: true }), 'utf-8');
142
+
143
+ const config = await getEffectiveConfig(testDir);
144
+ expect(config.verbose).toBe(true);
145
+ expect(config.debug).toBe(KILDE_DEFAULTS.debug);
146
+ });
147
+ });
148
+
149
+ describe('createSampleConfig', () => {
150
+ it('should return valid YAML config string', () => {
151
+ const yaml = createSampleConfig();
152
+ expect(yaml).toContain('verbose:');
153
+ expect(yaml).toContain('debug:');
154
+ expect(yaml).toContain('model:');
155
+ expect(yaml).toContain('commit:');
156
+ expect(yaml).toContain('release:');
157
+ });
158
+
159
+ it('should have correct default values', () => {
160
+ const yaml = createSampleConfig();
161
+ expect(yaml).toContain('verbose: false');
162
+ expect(yaml).toContain('debug: false');
163
+ expect(yaml).toContain('model: gpt-4o-mini');
164
+ });
165
+ });
166
+
167
+ describe('saveSampleConfig', () => {
168
+ it('should create .kilde directory', async () => {
169
+ await saveSampleConfig(testDir);
170
+ const configDir = path.join(testDir, '.kilde');
171
+ const stats = await fs.stat(configDir);
172
+ expect(stats.isDirectory()).toBe(true);
173
+ });
174
+
175
+ it('should create config.yaml file', async () => {
176
+ const configPath = await saveSampleConfig(testDir);
177
+ expect(configPath).toBe(path.join(testDir, '.kilde', 'config.yaml'));
178
+
179
+ const content = await fs.readFile(configPath, 'utf-8');
180
+ expect(content).toContain('verbose:');
181
+ expect(content).toContain('model:');
182
+ });
183
+
184
+ it('should return the config path', async () => {
185
+ const configPath = await saveSampleConfig(testDir);
186
+ expect(configPath).toMatch(/\.kilde\/config\.yaml$/);
187
+ });
188
+ });
189
+
190
+ describe('configFileExists', () => {
191
+ it('should return false when no config file exists', async () => {
192
+ const exists = await configFileExists(testDir);
193
+ expect(exists).toBe(false);
194
+ });
195
+
196
+ it('should return true when .kilde/config.yaml exists', async () => {
197
+ const configDir = path.join(testDir, '.kilde');
198
+ await fs.mkdir(configDir, { recursive: true });
199
+ await fs.writeFile(path.join(configDir, 'config.yaml'), 'verbose: true', 'utf-8');
200
+
201
+ const exists = await configFileExists(testDir);
202
+ expect(exists).toBe(true);
203
+ });
204
+
205
+ it('should return true when .kilderc.json exists', async () => {
206
+ await fs.writeFile(path.join(testDir, '.kilderc.json'), '{}', 'utf-8');
207
+
208
+ const exists = await configFileExists(testDir);
209
+ expect(exists).toBe(true);
210
+ });
211
+ });
212
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true,
9
+ "outDir": "./dist",
10
+ "rootDir": ".",
11
+ "types": [
12
+ "node",
13
+ "vitest"
14
+ ],
15
+ "incremental": true,
16
+ "allowJs": true,
17
+ "resolveJsonModule": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "*": [
21
+ "src/*"
22
+ ]
23
+ }
24
+ },
25
+ "include": [
26
+ "src/**/*",
27
+ "tests/**/*"
28
+ ],
29
+ "exclude": [
30
+ "node_modules"
31
+ ]
32
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { defineConfig } from 'vite';
2
+ import { VitePluginNode } from 'vite-plugin-node';
3
+ import replace from '@rollup/plugin-replace';
4
+ import { execSync } from 'child_process';
5
+ import shebang from 'rollup-plugin-preserve-shebang';
6
+ import os from 'os';
7
+
8
+ let gitInfo = {
9
+ branch: '',
10
+ commit: '',
11
+ tags: '',
12
+ commitDate: '',
13
+ };
14
+
15
+ try {
16
+ gitInfo = {
17
+ branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
18
+ commit: execSync('git rev-parse --short HEAD').toString().trim(),
19
+ tags: '',
20
+ commitDate: execSync('git log -1 --format=%cd --date=iso').toString().trim(),
21
+ };
22
+
23
+ try {
24
+ gitInfo.tags = execSync('git tag --points-at HEAD | paste -sd "," -').toString().trim();
25
+ } catch {
26
+ gitInfo.tags = '';
27
+ }
28
+ } catch {
29
+ // eslint-disable-next-line no-console
30
+ console.log('Directory does not have a Git repository, skipping git info');
31
+ }
32
+
33
+ // Capture build metadata
34
+ const buildInfo = {
35
+ hostname: os.hostname(),
36
+ timestamp: new Date().toISOString(),
37
+ };
38
+
39
+
40
+ export default defineConfig({
41
+ server: {
42
+ port: 3000
43
+ },
44
+ plugins: [
45
+ ...VitePluginNode({
46
+ adapter: 'express',
47
+ appPath: './src/main.ts',
48
+ exportName: 'viteNodeApp',
49
+ tsCompiler: 'swc',
50
+ swcOptions: {
51
+ sourceMaps: true,
52
+ },
53
+ }),
54
+ replace({
55
+ '__VERSION__': process.env.npm_package_version,
56
+ '__GIT_BRANCH__': gitInfo.branch,
57
+ '__GIT_COMMIT__': gitInfo.commit,
58
+ '__GIT_TAGS__': gitInfo.tags === '' ? '' : `T:${gitInfo.tags}`,
59
+ '__GIT_COMMIT_DATE__': gitInfo.commitDate,
60
+ '__SYSTEM_INFO__': `${process.platform} ${process.arch} ${process.version}`,
61
+ '__BUILD_HOSTNAME__': buildInfo.hostname,
62
+ '__BUILD_TIMESTAMP__': buildInfo.timestamp,
63
+ preventAssignment: true,
64
+ }),
65
+ ],
66
+ build: {
67
+ target: 'esnext',
68
+ outDir: 'dist',
69
+ lib: {
70
+ entry: './src/main.ts',
71
+ formats: ['es'],
72
+ fileName: 'main',
73
+ },
74
+ rollupOptions: {
75
+ external: [
76
+ // Command packages
77
+ '@grunnverk/core',
78
+ '@grunnverk/commands-git',
79
+ // Foundation packages
80
+ '@grunnverk/ai-service',
81
+ '@grunnverk/git-tools',
82
+ '@grunnverk/shared',
83
+ // MCP dependencies
84
+ '@modelcontextprotocol/sdk',
85
+ ],
86
+ input: 'src/main.ts',
87
+ output: {
88
+ dir: 'dist',
89
+ format: 'es',
90
+ entryFileNames: '[name].js',
91
+ chunkFileNames: '[name].js',
92
+ preserveModules: true,
93
+ preserveModulesRoot: 'src',
94
+ exports: 'named',
95
+ },
96
+ plugins: [
97
+ shebang({
98
+ shebang: '#!/usr/bin/env node',
99
+ }),
100
+ ],
101
+ },
102
+ // Make sure Vite generates ESM-compatible code
103
+ modulePreload: false,
104
+ minify: false,
105
+ sourcemap: true
106
+ },
107
+ });
@@ -0,0 +1,40 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: false,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ exclude: ['tests/commands/**/*.test.ts'], // Exclude command integration tests - require real git repo
9
+ env: {
10
+ TZ: 'America/New_York'
11
+ },
12
+ // Add pool configuration to prevent memory issues (moved from poolOptions in Vitest 4)
13
+ pool: 'forks',
14
+ maxForks: 2,
15
+ minForks: 1,
16
+ // Add test timeout and memory limits
17
+ testTimeout: 30000,
18
+ hookTimeout: 10000,
19
+ teardownTimeout: 10000,
20
+ coverage: {
21
+ provider: 'v8',
22
+ reporter: ['text', 'lcov', 'html'],
23
+ all: true,
24
+ include: ['src/**/*.ts'],
25
+ exclude: [
26
+ 'src/main.ts',
27
+ 'src/types/**/*.ts',
28
+ 'src/application.ts', // CLI integration - requires Commander parse
29
+ 'src/commands/release.ts', // Complex integration - requires git + AI
30
+ 'src/mcp/server.ts', // MCP server integration - requires stdio transport
31
+ ],
32
+ thresholds: {
33
+ statements: 70,
34
+ branches: 70,
35
+ functions: 70,
36
+ lines: 70,
37
+ }
38
+ },
39
+ },
40
+ });
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="description" content="Kilde - Universal Git Automation Tool with AI-powered commit messages and release notes" />
8
+ <title>Kilde - Universal Git Automation</title>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,142 @@
1
+ .app {
2
+ min-height: 100vh;
3
+ display: flex;
4
+ flex-direction: column;
5
+ }
6
+
7
+ .section {
8
+ max-width: 1200px;
9
+ margin: 0 auto;
10
+ padding: 4rem 2rem;
11
+ width: 100%;
12
+ }
13
+
14
+ .section h2 {
15
+ font-size: 2.5rem;
16
+ margin-bottom: 2rem;
17
+ color: var(--color-text);
18
+ }
19
+
20
+ .section h3 {
21
+ font-size: 1.75rem;
22
+ margin-top: 2rem;
23
+ margin-bottom: 1rem;
24
+ color: var(--color-text);
25
+ }
26
+
27
+ .section p {
28
+ color: var(--color-text-secondary);
29
+ margin-bottom: 1rem;
30
+ line-height: 1.8;
31
+ }
32
+
33
+ .code-block {
34
+ background-color: var(--color-bg-secondary);
35
+ border: 1px solid var(--color-border);
36
+ border-radius: 8px;
37
+ padding: 1.5rem;
38
+ margin: 1.5rem 0;
39
+ overflow-x: auto;
40
+ }
41
+
42
+ .code-block pre {
43
+ margin: 0;
44
+ color: var(--color-text);
45
+ font-size: 0.9rem;
46
+ line-height: 1.6;
47
+ }
48
+
49
+ .code-inline {
50
+ background-color: var(--color-bg-secondary);
51
+ color: var(--color-primary);
52
+ padding: 0.2rem 0.5rem;
53
+ border-radius: 4px;
54
+ font-family: var(--font-mono);
55
+ font-size: 0.9em;
56
+ }
57
+
58
+ .button {
59
+ display: inline-block;
60
+ padding: 0.75rem 1.5rem;
61
+ background-color: var(--color-primary);
62
+ color: white;
63
+ text-decoration: none;
64
+ border-radius: 8px;
65
+ font-weight: 600;
66
+ transition: background-color 0.2s;
67
+ border: none;
68
+ cursor: pointer;
69
+ font-size: 1rem;
70
+ }
71
+
72
+ .button:hover {
73
+ background-color: var(--color-primary-hover);
74
+ }
75
+
76
+ .button-secondary {
77
+ background-color: transparent;
78
+ border: 2px solid var(--color-primary);
79
+ color: var(--color-primary);
80
+ }
81
+
82
+ .button-secondary:hover {
83
+ background-color: var(--color-primary);
84
+ color: white;
85
+ }
86
+
87
+ .grid {
88
+ display: grid;
89
+ gap: 2rem;
90
+ margin: 2rem 0;
91
+ }
92
+
93
+ .grid-2 {
94
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
95
+ }
96
+
97
+ .grid-3 {
98
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
99
+ }
100
+
101
+ .card {
102
+ background-color: var(--color-bg-secondary);
103
+ border: 1px solid var(--color-border);
104
+ border-radius: 8px;
105
+ padding: 2rem;
106
+ transition: transform 0.2s, border-color 0.2s;
107
+ }
108
+
109
+ .card:hover {
110
+ transform: translateY(-4px);
111
+ border-color: var(--color-primary);
112
+ }
113
+
114
+ .card h3 {
115
+ margin-top: 0;
116
+ color: var(--color-primary);
117
+ }
118
+
119
+ .feature-icon {
120
+ font-size: 2rem;
121
+ margin-bottom: 1rem;
122
+ color: var(--color-accent);
123
+ }
124
+
125
+ @media (max-width: 768px) {
126
+ .section {
127
+ padding: 2rem 1rem;
128
+ }
129
+
130
+ .section h2 {
131
+ font-size: 2rem;
132
+ }
133
+
134
+ .section h3 {
135
+ font-size: 1.5rem;
136
+ }
137
+
138
+ .grid-2,
139
+ .grid-3 {
140
+ grid-template-columns: 1fr;
141
+ }
142
+ }
@@ -0,0 +1,34 @@
1
+ import React, { useState } from 'react';
2
+ import './App.css';
3
+ import Hero from './components/Hero';
4
+ import Navigation from './components/Navigation';
5
+ import GettingStarted from './components/GettingStarted';
6
+ import Commands from './components/Commands';
7
+ import Configuration from './components/Configuration';
8
+ import Examples from './components/Examples';
9
+ import Footer from './components/Footer';
10
+
11
+ function App() {
12
+ const [activeSection, setActiveSection] = useState('home');
13
+
14
+ return (
15
+ <div className="app">
16
+ <Navigation activeSection={activeSection} onNavigate={setActiveSection} />
17
+
18
+ {activeSection === 'home' && (
19
+ <>
20
+ <Hero />
21
+ <GettingStarted />
22
+ </>
23
+ )}
24
+
25
+ {activeSection === 'commands' && <Commands />}
26
+ {activeSection === 'configuration' && <Configuration />}
27
+ {activeSection === 'examples' && <Examples />}
28
+
29
+ <Footer />
30
+ </div>
31
+ );
32
+ }
33
+
34
+ export default App;