@chimerai/cli 0.2.73

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +317 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +2126 -0
  9. package/dist/commands/create.d.ts +12 -0
  10. package/dist/commands/create.d.ts.map +1 -0
  11. package/dist/commands/create.js +1703 -0
  12. package/dist/commands/deploy.d.ts +11 -0
  13. package/dist/commands/deploy.d.ts.map +1 -0
  14. package/dist/commands/deploy.js +219 -0
  15. package/dist/commands/dev.d.ts +17 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +206 -0
  18. package/dist/commands/doctor.d.ts +11 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +728 -0
  21. package/dist/commands/generate.d.ts +19 -0
  22. package/dist/commands/generate.d.ts.map +1 -0
  23. package/dist/commands/generate.js +429 -0
  24. package/dist/commands/init.d.ts +11 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +269 -0
  27. package/dist/commands/list.d.ts +12 -0
  28. package/dist/commands/list.d.ts.map +1 -0
  29. package/dist/commands/list.js +328 -0
  30. package/dist/commands/migrate.d.ts +14 -0
  31. package/dist/commands/migrate.d.ts.map +1 -0
  32. package/dist/commands/migrate.js +197 -0
  33. package/dist/commands/plugin.d.ts +10 -0
  34. package/dist/commands/plugin.d.ts.map +1 -0
  35. package/dist/commands/plugin.js +239 -0
  36. package/dist/commands/remove.d.ts +11 -0
  37. package/dist/commands/remove.d.ts.map +1 -0
  38. package/dist/commands/remove.js +472 -0
  39. package/dist/commands/secret.d.ts +12 -0
  40. package/dist/commands/secret.d.ts.map +1 -0
  41. package/dist/commands/secret.js +102 -0
  42. package/dist/commands/setup.d.ts +9 -0
  43. package/dist/commands/setup.d.ts.map +1 -0
  44. package/dist/commands/setup.js +788 -0
  45. package/dist/commands/update.d.ts +14 -0
  46. package/dist/commands/update.d.ts.map +1 -0
  47. package/dist/commands/update.js +211 -0
  48. package/dist/commands/use.d.ts +9 -0
  49. package/dist/commands/use.d.ts.map +1 -0
  50. package/dist/commands/use.js +51 -0
  51. package/dist/index.d.ts +22 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +45 -0
  54. package/dist/license.d.ts +55 -0
  55. package/dist/license.d.ts.map +1 -0
  56. package/dist/license.js +258 -0
  57. package/dist/scanner.d.ts +31 -0
  58. package/dist/scanner.d.ts.map +1 -0
  59. package/dist/scanner.js +113 -0
  60. package/dist/schema-manager.d.ts +26 -0
  61. package/dist/schema-manager.d.ts.map +1 -0
  62. package/dist/schema-manager.js +132 -0
  63. package/dist/templates/admin.d.ts +49 -0
  64. package/dist/templates/admin.d.ts.map +1 -0
  65. package/dist/templates/admin.js +1358 -0
  66. package/dist/templates/ai-routes.d.ts +17 -0
  67. package/dist/templates/ai-routes.d.ts.map +1 -0
  68. package/dist/templates/ai-routes.js +1130 -0
  69. package/dist/templates/ai-service-tools.d.ts +22 -0
  70. package/dist/templates/ai-service-tools.d.ts.map +1 -0
  71. package/dist/templates/ai-service-tools.js +1424 -0
  72. package/dist/templates/ai-service.d.ts +66 -0
  73. package/dist/templates/ai-service.d.ts.map +1 -0
  74. package/dist/templates/ai-service.js +2202 -0
  75. package/dist/templates/api-routes.d.ts +108 -0
  76. package/dist/templates/api-routes.d.ts.map +1 -0
  77. package/dist/templates/api-routes.js +1219 -0
  78. package/dist/templates/auth.d.ts +48 -0
  79. package/dist/templates/auth.d.ts.map +1 -0
  80. package/dist/templates/auth.js +381 -0
  81. package/dist/templates/billing.d.ts +44 -0
  82. package/dist/templates/billing.d.ts.map +1 -0
  83. package/dist/templates/billing.js +551 -0
  84. package/dist/templates/chat.d.ts +63 -0
  85. package/dist/templates/chat.d.ts.map +1 -0
  86. package/dist/templates/chat.js +1979 -0
  87. package/dist/templates/components.d.ts +22 -0
  88. package/dist/templates/components.d.ts.map +1 -0
  89. package/dist/templates/components.js +672 -0
  90. package/dist/templates/config.d.ts +6 -0
  91. package/dist/templates/config.d.ts.map +1 -0
  92. package/dist/templates/config.js +86 -0
  93. package/dist/templates/docker.d.ts +25 -0
  94. package/dist/templates/docker.d.ts.map +1 -0
  95. package/dist/templates/docker.js +165 -0
  96. package/dist/templates/gdpr.d.ts +16 -0
  97. package/dist/templates/gdpr.d.ts.map +1 -0
  98. package/dist/templates/gdpr.js +259 -0
  99. package/dist/templates/index.d.ts +77 -0
  100. package/dist/templates/index.d.ts.map +1 -0
  101. package/dist/templates/index.js +339 -0
  102. package/dist/templates/layout.d.ts +67 -0
  103. package/dist/templates/layout.d.ts.map +1 -0
  104. package/dist/templates/layout.js +670 -0
  105. package/dist/templates/mfa.d.ts +23 -0
  106. package/dist/templates/mfa.d.ts.map +1 -0
  107. package/dist/templates/mfa.js +353 -0
  108. package/dist/templates/middleware.d.ts +12 -0
  109. package/dist/templates/middleware.d.ts.map +1 -0
  110. package/dist/templates/middleware.js +116 -0
  111. package/dist/templates/prisma.d.ts +35 -0
  112. package/dist/templates/prisma.d.ts.map +1 -0
  113. package/dist/templates/prisma.js +724 -0
  114. package/dist/templates/provider-routes.d.ts +21 -0
  115. package/dist/templates/provider-routes.d.ts.map +1 -0
  116. package/dist/templates/provider-routes.js +1203 -0
  117. package/dist/templates/rag.d.ts +48 -0
  118. package/dist/templates/rag.d.ts.map +1 -0
  119. package/dist/templates/rag.js +532 -0
  120. package/dist/templates/widget.d.ts +64 -0
  121. package/dist/templates/widget.d.ts.map +1 -0
  122. package/dist/templates/widget.js +1360 -0
  123. package/dist/utils/provider-db.d.ts +63 -0
  124. package/dist/utils/provider-db.d.ts.map +1 -0
  125. package/dist/utils/provider-db.js +300 -0
  126. package/dist/utils.d.ts +78 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +330 -0
  129. package/package.json +60 -0
package/dist/utils.js ADDED
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ /**
3
+ * Shared CLI utilities - Validation, error handling, workspace checks, env file management
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.validateName = validateName;
10
+ exports.assertValidName = assertValidName;
11
+ exports.validateWorkspace = validateWorkspace;
12
+ exports.handleCliError = handleCliError;
13
+ exports.updateEnvFile = updateEnvFile;
14
+ exports.isChimeraiProject = isChimeraiProject;
15
+ exports.findProjectRoot = findProjectRoot;
16
+ exports.resolveTargetDir = resolveTargetDir;
17
+ exports.registerProject = registerProject;
18
+ exports.setDefaultProject = setDefaultProject;
19
+ exports.listRegisteredProjects = listRegisteredProjects;
20
+ const chalk_1 = __importDefault(require("chalk"));
21
+ const fs_extra_1 = __importDefault(require("fs-extra"));
22
+ const path_1 = __importDefault(require("path"));
23
+ const os_1 = __importDefault(require("os"));
24
+ /* ------------------------------------------------------------------ */
25
+ /* Project name validation */
26
+ /* ------------------------------------------------------------------ */
27
+ const RESERVED_NAMES = new Set([
28
+ 'node_modules',
29
+ 'package',
30
+ 'dist',
31
+ 'build',
32
+ 'src',
33
+ 'lib',
34
+ 'test',
35
+ 'tests',
36
+ 'public',
37
+ 'static',
38
+ 'assets',
39
+ 'vendor',
40
+ 'tmp',
41
+ 'temp',
42
+ '.git',
43
+ '.next',
44
+ '.env',
45
+ 'con',
46
+ 'prn',
47
+ 'aux',
48
+ 'nul', // Windows reserved
49
+ ]);
50
+ /**
51
+ * Validates a project or plugin name.
52
+ * - lowercase alphanumeric + hyphens only
53
+ * - max 214 chars (npm limit)
54
+ * - no leading/trailing hyphens
55
+ * - no reserved names
56
+ * - no path traversal sequences
57
+ */
58
+ function validateName(name, label = 'Name') {
59
+ if (!name || name.trim().length === 0) {
60
+ return { valid: false, error: `${label} cannot be empty` };
61
+ }
62
+ if (name.length > 214) {
63
+ return { valid: false, error: `${label} must be 214 characters or fewer` };
64
+ }
65
+ // Path traversal prevention
66
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) {
67
+ return { valid: false, error: `${label} must not contain path separators or '..'` };
68
+ }
69
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) && !/^[a-z0-9]$/.test(name)) {
70
+ return {
71
+ valid: false,
72
+ error: `${label} must be lowercase alphanumeric with hyphens (e.g., "my-app")`,
73
+ };
74
+ }
75
+ if (name.includes('--')) {
76
+ return { valid: false, error: `${label} must not contain consecutive hyphens` };
77
+ }
78
+ if (RESERVED_NAMES.has(name.toLowerCase())) {
79
+ return { valid: false, error: `"${name}" is a reserved name` };
80
+ }
81
+ return { valid: true };
82
+ }
83
+ /**
84
+ * Validates and exits on failure. For use in Commander actions.
85
+ */
86
+ function assertValidName(name, label = 'Name') {
87
+ const result = validateName(name, label);
88
+ if (!result.valid) {
89
+ handleCliError(result.error);
90
+ }
91
+ }
92
+ /* ------------------------------------------------------------------ */
93
+ /* Workspace validation */
94
+ /* ------------------------------------------------------------------ */
95
+ /**
96
+ * Checks that targetDir is a valid ChimerAI project directory.
97
+ * Verifies package.json exists.
98
+ */
99
+ function validateWorkspace(targetDir) {
100
+ if (!fs_extra_1.default.existsSync(targetDir)) {
101
+ console.log(chalk_1.default.red(`❌ Directory does not exist: ${targetDir}`));
102
+ return false;
103
+ }
104
+ const pkgPath = path_1.default.join(targetDir, 'package.json');
105
+ if (!fs_extra_1.default.existsSync(pkgPath)) {
106
+ console.log(chalk_1.default.red(`❌ Not a valid project directory (no package.json): ${targetDir}`));
107
+ console.log(chalk_1.default.gray(' Run: chimerai create <project-name>'));
108
+ return false;
109
+ }
110
+ return true;
111
+ }
112
+ /* ------------------------------------------------------------------ */
113
+ /* Standardized error handling */
114
+ /* ------------------------------------------------------------------ */
115
+ /**
116
+ * Standard CLI error handler — prints message and exits with code.
117
+ */
118
+ function handleCliError(message, code = 1) {
119
+ console.error(chalk_1.default.red(`\n❌ ${message}\n`));
120
+ process.exit(code);
121
+ }
122
+ /* ------------------------------------------------------------------ */
123
+ /* Environment file utilities */
124
+ /* ------------------------------------------------------------------ */
125
+ /**
126
+ * Generic environment file updater utility (PHASE 2.2 / PHASE 4)
127
+ * Shared across setup.ts and other commands that need to update .env files.
128
+ * Replaces repetitive fs read/write patterns.
129
+ *
130
+ * @param envVars - Object with key-value pairs to add/update
131
+ * @param envPath - Path to .env file (default: '.env')
132
+ */
133
+ async function updateEnvFile(envVars, envPath = '.env') {
134
+ let envContent = fs_extra_1.default.existsSync(envPath) ? fs_extra_1.default.readFileSync(envPath, 'utf-8') : '';
135
+ for (const [key, value] of Object.entries(envVars)) {
136
+ if (envContent.includes(`${key}=`)) {
137
+ // Replace existing variable
138
+ envContent = envContent.replace(new RegExp(`${key}=.*$`, 'm'), `${key}="${value}"`);
139
+ }
140
+ else {
141
+ // Add new variable
142
+ envContent += `\n${key}="${value}"`;
143
+ }
144
+ }
145
+ fs_extra_1.default.writeFileSync(envPath, envContent.trim() + '\n');
146
+ }
147
+ const REGISTRY_DIR = path_1.default.join(os_1.default.homedir(), '.chimerai');
148
+ const REGISTRY_PATH = path_1.default.join(REGISTRY_DIR, 'projects.json');
149
+ /**
150
+ * Walks up directory tree looking for project markers.
151
+ * Priority: .chimerai file > package.json + Next.js/Prisma markers
152
+ */
153
+ function walkUpForProjectRoot(startDir) {
154
+ let current = path_1.default.resolve(startDir);
155
+ const start = current;
156
+ const visited = new Set();
157
+ while (true) {
158
+ // Prevent infinite loops
159
+ if (visited.has(current))
160
+ return null;
161
+ visited.add(current);
162
+ // Primary: .chimerai marker (must be a file, not the ~/.chimerai registry directory)
163
+ const chimeraiMarker = path_1.default.join(current, '.chimerai');
164
+ if (fs_extra_1.default.existsSync(chimeraiMarker) && fs_extra_1.default.statSync(chimeraiMarker).isFile()) {
165
+ return current;
166
+ }
167
+ // Secondary: package.json + Next.js/Prisma markers
168
+ if (fs_extra_1.default.existsSync(path_1.default.join(current, 'package.json'))) {
169
+ const hasNextConfig = fs_extra_1.default.existsSync(path_1.default.join(current, 'next.config.js')) ||
170
+ fs_extra_1.default.existsSync(path_1.default.join(current, 'next.config.mjs')) ||
171
+ fs_extra_1.default.existsSync(path_1.default.join(current, 'next.config.ts'));
172
+ const hasPrisma = fs_extra_1.default.existsSync(path_1.default.join(current, 'prisma'));
173
+ if (hasNextConfig || hasPrisma) {
174
+ return current;
175
+ }
176
+ }
177
+ // Move up one level
178
+ const parent = path_1.default.dirname(current);
179
+ if (parent === current) {
180
+ // Reached filesystem root
181
+ return null;
182
+ }
183
+ current = parent;
184
+ }
185
+ }
186
+ /**
187
+ * Checks if a directory is a valid ChimerAI project.
188
+ */
189
+ function isChimeraiProject(dir) {
190
+ if (!fs_extra_1.default.existsSync(dir))
191
+ return false;
192
+ const marker = path_1.default.join(dir, '.chimerai');
193
+ if (fs_extra_1.default.existsSync(marker) && fs_extra_1.default.statSync(marker).isFile())
194
+ return true;
195
+ if (!fs_extra_1.default.existsSync(path_1.default.join(dir, 'package.json')))
196
+ return false;
197
+ const hasNextConfig = fs_extra_1.default.existsSync(path_1.default.join(dir, 'next.config.js')) ||
198
+ fs_extra_1.default.existsSync(path_1.default.join(dir, 'next.config.mjs')) ||
199
+ fs_extra_1.default.existsSync(path_1.default.join(dir, 'next.config.ts'));
200
+ const hasPrisma = fs_extra_1.default.existsSync(path_1.default.join(dir, 'prisma'));
201
+ return hasNextConfig || hasPrisma;
202
+ }
203
+ /**
204
+ * Reads the global project registry from ~/.chimerai/projects.json
205
+ */
206
+ function getDefaultProjectFromRegistry() {
207
+ if (!fs_extra_1.default.existsSync(REGISTRY_PATH))
208
+ return null;
209
+ try {
210
+ const registry = JSON.parse(fs_extra_1.default.readFileSync(REGISTRY_PATH, 'utf-8'));
211
+ const defaultName = registry.default;
212
+ if (defaultName && registry.projects && registry.projects[defaultName]) {
213
+ return registry.projects[defaultName];
214
+ }
215
+ return null;
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ /**
222
+ * Finds the ChimerAI project root directory.
223
+ *
224
+ * Search order (first match wins):
225
+ * 1. Directory tree walking (.chimerai marker or package.json + Next.js/Prisma)
226
+ * 2. CHIMERAI_PROJECT environment variable
227
+ * 3. Global registry default (~/.chimerai/projects.json)
228
+ * 4. Error with helpful guidance
229
+ */
230
+ function findProjectRoot(startDir) {
231
+ const cwd = startDir || process.cwd();
232
+ // Step 1: Walk up directory tree
233
+ const fromTree = walkUpForProjectRoot(cwd);
234
+ if (fromTree)
235
+ return fromTree;
236
+ // Step 2: Environment variable
237
+ const envDir = process.env.CHIMERAI_PROJECT;
238
+ if (envDir) {
239
+ const resolved = path_1.default.resolve(envDir);
240
+ if (isChimeraiProject(resolved))
241
+ return resolved;
242
+ console.log(chalk_1.default.yellow(`⚠️ CHIMERAI_PROJECT points to "${resolved}", but no project found there.`));
243
+ }
244
+ // Step 3: Global registry
245
+ const fromRegistry = getDefaultProjectFromRegistry();
246
+ if (fromRegistry && isChimeraiProject(fromRegistry))
247
+ return fromRegistry;
248
+ // Step 4: Error
249
+ console.log(chalk_1.default.red(`\n❌ No ChimerAI project found.\n`));
250
+ console.log(chalk_1.default.white(`Options:`));
251
+ console.log(chalk_1.default.gray(` 1. Navigate to a project folder: cd my-project`));
252
+ console.log(chalk_1.default.gray(` 2. Specify the directory explicitly: chimerai <command> --dir /path/to/project`));
253
+ console.log(chalk_1.default.gray(` 3. Set an environment variable: export CHIMERAI_PROJECT=/path/to/project`));
254
+ console.log(chalk_1.default.gray(` 4. Create a new project: chimerai create my-project\n`));
255
+ process.exit(1);
256
+ }
257
+ /**
258
+ * Resolves the target directory for a command.
259
+ *
260
+ * Priority:
261
+ * 1. --dir explicitly provided (not '.')
262
+ * 2. Automatic search via findProjectRoot()
263
+ */
264
+ function resolveTargetDir(dirOption) {
265
+ // If --dir was explicitly set (not the default '.')
266
+ if (dirOption !== '.') {
267
+ const explicit = path_1.default.resolve(dirOption);
268
+ if (!fs_extra_1.default.existsSync(explicit)) {
269
+ console.log(chalk_1.default.red(`❌ Directory does not exist: ${explicit}`));
270
+ process.exit(1);
271
+ }
272
+ return explicit;
273
+ }
274
+ // Automatic search
275
+ return findProjectRoot();
276
+ }
277
+ /**
278
+ * Registers a project in the global registry.
279
+ * Called by create and init commands.
280
+ */
281
+ function registerProject(name, absolutePath) {
282
+ fs_extra_1.default.ensureDirSync(REGISTRY_DIR);
283
+ let registry = { projects: {}, default: null };
284
+ if (fs_extra_1.default.existsSync(REGISTRY_PATH)) {
285
+ try {
286
+ registry = JSON.parse(fs_extra_1.default.readFileSync(REGISTRY_PATH, 'utf-8'));
287
+ }
288
+ catch {
289
+ // Corrupted file — recreate
290
+ }
291
+ }
292
+ registry.projects[name] = absolutePath;
293
+ // First registered project becomes default
294
+ if (!registry.default) {
295
+ registry.default = name;
296
+ }
297
+ fs_extra_1.default.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
298
+ }
299
+ /**
300
+ * Sets the default project in the global registry.
301
+ */
302
+ function setDefaultProject(name) {
303
+ if (!fs_extra_1.default.existsSync(REGISTRY_PATH))
304
+ return false;
305
+ try {
306
+ const registry = JSON.parse(fs_extra_1.default.readFileSync(REGISTRY_PATH, 'utf-8'));
307
+ if (!registry.projects[name])
308
+ return false;
309
+ registry.default = name;
310
+ fs_extra_1.default.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
311
+ return true;
312
+ }
313
+ catch {
314
+ return false;
315
+ }
316
+ }
317
+ /**
318
+ * Lists all registered projects.
319
+ */
320
+ function listRegisteredProjects() {
321
+ if (!fs_extra_1.default.existsSync(REGISTRY_PATH)) {
322
+ return { projects: {}, default: null };
323
+ }
324
+ try {
325
+ return JSON.parse(fs_extra_1.default.readFileSync(REGISTRY_PATH, 'utf-8'));
326
+ }
327
+ catch {
328
+ return { projects: {}, default: null };
329
+ }
330
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@chimerai/cli",
3
+ "version": "0.2.73",
4
+ "description": "CLI wizard for ChimerAI starter kit — scaffold auth, RBAC, AI chat, billing and more into any Next.js project",
5
+ "main": "./dist/index.js",
6
+ "bin": {
7
+ "chimerai": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/armbur19-collab/chimerai-kickstart.git",
20
+ "directory": "packages/cli"
21
+ },
22
+ "homepage": "https://chimerai.dev",
23
+ "bugs": {
24
+ "url": "https://github.com/armbur19-collab/chimerai-kickstart/issues"
25
+ },
26
+ "scripts": {
27
+ "build": "node -e \"const f='package.json',p=JSON.parse(require('fs').readFileSync(f));p.version=p.version.replace(/\\d+$/,n=>+n+1);require('fs').writeFileSync(f,JSON.stringify(p,null,2)+'\\n');console.log('v'+p.version)\" && tsc",
28
+ "dev": "tsc --watch",
29
+ "lint": "eslint .",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "test:coverage": "vitest run --coverage"
33
+ },
34
+ "dependencies": {
35
+ "chalk": "^5.3.0",
36
+ "commander": "^11.1.0",
37
+ "execa": "^8.0.1",
38
+ "fs-extra": "^11.2.0",
39
+ "inquirer": "^9.2.12",
40
+ "jose": "^6.2.2",
41
+ "ora": "^7.0.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/fs-extra": "^11.0.4",
45
+ "@types/inquirer": "^9.0.7",
46
+ "@types/node": "^20.10.6",
47
+ "@vitest/coverage-v8": "^1.0.0",
48
+ "typescript": "^5.3.3",
49
+ "vitest": "^1.0.0"
50
+ },
51
+ "keywords": [
52
+ "cli",
53
+ "wizard",
54
+ "rbac",
55
+ "starter-kit",
56
+ "chimerai"
57
+ ],
58
+ "author": "ChimerAI",
59
+ "license": "MIT"
60
+ }