@suronai/cli 0.1.33 → 0.1.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suronai/cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "CLI for Suron — suron login, init, whoami, recover",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +1,13 @@
1
1
  import { Command } from "commander";
2
2
  import { existsSync, writeFileSync, readFileSync } from "fs";
3
- import { join, basename } from "path";
3
+ import { join, basename, relative } from "path";
4
4
  import { execSync } from "child_process";
5
5
  import { requireApiUrl, prompt } from "../utils/config.js";
6
6
  import { encryptDotenv, readPrivateKey } from "../utils/dotenvx.js";
7
7
  import c from "../utils/colors.js";
8
8
 
9
+ const HR = " " + c.dim("─".repeat(55));
10
+
9
11
  export const initCommand = new Command("init")
10
12
  .description("Encrypt .env and register this app with Suron")
11
13
  .option("--name <n>", "App name (skips interactive prompt)")
@@ -13,38 +15,73 @@ export const initCommand = new Command("init")
13
15
  const cwd = process.cwd();
14
16
  const apiUrl = requireApiUrl();
15
17
 
16
- console.log("\n" + c.bold(" suron init") + " — " + c.dim(cwd) + "\n");
18
+ console.log();
17
19
 
18
20
  if (existsSync(join(cwd, ".suron.json"))) {
19
- console.error(" " + c.red("✗") + " .suron.json already exists — this app is already initialised.");
20
- console.error(" To restore a lost config, run: " + c.bold("suron recover") + "\n");
21
+ console.error(" " + c.red("✗") + " already initialised — .suron.json exists");
22
+ console.error(" To restore a lost config: " + c.bold("suron recover") + "\n");
21
23
  process.exit(1);
22
24
  }
23
25
 
24
26
  if (!existsSync(join(cwd, ".env"))) {
25
- console.error(" " + c.red("✗") + " .env not found in current directory");
27
+ console.error(" " + c.red("✗") + " .env not found");
26
28
  console.error(" Create a .env file with your secrets, then run: suron init\n");
27
29
  process.exit(1);
28
30
  }
29
31
 
30
- // ── App name ─────────────────────────────────────────────────────────────
31
- const suggested = basename(cwd) || "my-project";
32
+ // ── App name ──────────────────────────────────────────────────────────────
33
+ // Preserve the folder's original casing as the default suggestion.
34
+ const suggested = sanitiseName(basename(cwd) || "myapp");
32
35
  let appName;
33
36
 
34
37
  if (opts.name) {
35
38
  appName = sanitiseName(opts.name);
36
- console.log(" App name " + c.cyan(appName));
37
39
  } else {
38
- const raw = await prompt(` App name [${c.dim(sanitiseName(suggested))}] › `);
40
+ const raw = await prompt(" App name " + c.dim("· ") + c.dim("[" + suggested + "] › "));
39
41
  appName = sanitiseName(raw || suggested);
40
42
  }
41
43
 
42
- // ── Encrypt ───────────────────────────────────────────────────────────────
43
- console.log("\n " + c.dim("Encrypting .env..."));
44
+ if (!appName) {
45
+ console.error(" " + c.red("✗") + " App name is required and must contain at least one letter or digit.");
46
+ console.error(" Example: suron init --name MyApp\n");
47
+ process.exit(1);
48
+ }
44
49
 
50
+ console.log(" App name " + c.dim("·") + " " + c.cyan(appName));
51
+ console.log(HR);
52
+
53
+ // ── Steps tracker ─────────────────────────────────────────────────────────
54
+ const steps = {
55
+ encrypt: { label: "Encrypt", detail: ".env", status: "pending" },
56
+ keys: { label: "Key", detail: ".env.keys", status: "pending" },
57
+ gitignore:{ label: "Gitignore",detail: ".gitignore", status: "pending" },
58
+ register: { label: "Register", detail: appName, status: "pending" },
59
+ sdk: { label: "SDK", detail: "@suronai/sdk", status: "pending" },
60
+ };
61
+
62
+ function printSteps() {
63
+ for (const s of Object.values(steps)) {
64
+ const dot = s.status === "done" ? c.green("●")
65
+ : s.status === "skip" ? c.dim("○")
66
+ : s.status === "fail" ? c.red("●")
67
+ : c.dim("○");
68
+ const detail = s.status === "fail" ? c.red(s.detail) : c.dim(s.detail);
69
+ const label = s.label.padEnd(11);
70
+ const dots = c.dim(".".repeat(Math.max(2, 44 - label.length - s.detail.length)));
71
+ const note = s.note ? " " + c.dim(s.note) : "";
72
+ console.log(" " + dot + " " + label + detail + dots + (s.status === "done" ? c.green(" done") : s.status === "skip" ? c.dim(" skip") : s.status === "fail" ? c.red(" fail") : "") + note);
73
+ }
74
+ }
75
+
76
+ // ── Encrypt ───────────────────────────────────────────────────────────────
45
77
  try {
46
78
  encryptDotenv(cwd);
79
+ steps.encrypt.status = "done";
80
+ steps.keys.status = "done";
47
81
  } catch (err) {
82
+ steps.encrypt.status = "fail";
83
+ printSteps();
84
+ console.log(HR);
48
85
  console.error("\n " + c.red("✗") + " " + err.message + "\n");
49
86
  process.exit(1);
50
87
  }
@@ -53,13 +90,19 @@ export const initCommand = new Command("init")
53
90
  try {
54
91
  privateKey = readPrivateKey(cwd);
55
92
  } catch (err) {
93
+ steps.keys.status = "fail";
94
+ printSteps();
95
+ console.log(HR);
56
96
  console.error("\n " + c.red("✗") + " " + err.message + "\n");
57
97
  process.exit(1);
58
98
  }
59
99
 
60
- // ── Register ──────────────────────────────────────────────────────────────
61
- console.log(" " + c.dim("Registering app..."));
100
+ // ── .gitignore ────────────────────────────────────────────────────────────
101
+ const gitignoreResult = ensureGitignore(cwd);
102
+ steps.gitignore.status = "done";
103
+ steps.gitignore.note = gitignoreResult;
62
104
 
105
+ // ── Register ──────────────────────────────────────────────────────────────
63
106
  let res;
64
107
  try {
65
108
  res = await fetch(`${apiUrl}/cli/register-app`, {
@@ -68,6 +111,9 @@ export const initCommand = new Command("init")
68
111
  body: JSON.stringify({ name: appName, private_key: privateKey }),
69
112
  });
70
113
  } catch (err) {
114
+ steps.register.status = "fail";
115
+ printSteps();
116
+ console.log(HR);
71
117
  console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
72
118
  process.exit(1);
73
119
  }
@@ -75,10 +121,13 @@ export const initCommand = new Command("init")
75
121
  if (!res.ok) {
76
122
  let body = {};
77
123
  try { body = await res.json(); } catch { /* ignore */ }
78
-
124
+ steps.register.status = "fail";
125
+ printSteps();
126
+ console.log(HR);
79
127
  if (res.status === 409) {
80
- console.error("\n " + c.red("✗") + ` An app named "${c.cyan(appName)}" is already registered.`);
81
- console.error(" If you lost .suron.json, run: " + c.bold("suron recover") + "\n");
128
+ const canonical = body?.existing_name ?? appName;
129
+ console.error("\n " + c.red("") + ` An app named "${c.cyan(String(canonical))}" is already registered (case-insensitive match).`);
130
+ console.error(" If you lost .suron.json, run: " + c.bold("suron recover --name " + String(canonical)) + "\n");
82
131
  } else {
83
132
  console.error("\n " + c.red("✗") + ` ${body?.error ?? `register-app failed (${res.status})`}\n`);
84
133
  }
@@ -86,9 +135,7 @@ export const initCommand = new Command("init")
86
135
  }
87
136
 
88
137
  const { app_id } = await res.json();
89
-
90
- // ── .gitignore ─────────────────────────────────────────────────────────────
91
- ensureGitignore(cwd);
138
+ steps.register.status = "done";
92
139
 
93
140
  // ── Write .suron.json ─────────────────────────────────────────────────────
94
141
  writeFileSync(
@@ -110,35 +157,43 @@ export const initCommand = new Command("init")
110
157
  } catch { /* ignore */ }
111
158
 
112
159
  if (!alreadyInstalled) {
113
- console.log(" " + c.dim("Installing @suronai/sdk..."));
114
160
  const pm = detectPackageManager(cwd);
115
161
  try {
116
- execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "inherit" });
162
+ execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "pipe" });
163
+ steps.sdk.status = "done";
117
164
  } catch {
118
- console.error(" " + c.yellow("⚠") + " SDK install failed — run manually: npm install @suronai/sdk");
165
+ steps.sdk.status = "fail";
166
+ steps.sdk.note = "run: npm install @suronai/sdk";
119
167
  }
120
168
  } else {
121
- console.log(" " + c.dim("@suronai/sdk already installed — skipped."));
169
+ steps.sdk.status = "skip";
170
+ steps.sdk.note = "already installed";
122
171
  }
172
+ } else {
173
+ steps.sdk.status = "skip";
174
+ steps.sdk.note = "no package.json";
123
175
  }
124
176
 
177
+ printSteps();
178
+ console.log(HR);
179
+
125
180
  // ── Patch entry point ─────────────────────────────────────────────────────
126
181
  await patchEntryPoint(cwd, isEsm);
127
182
 
128
183
  // ── Done ──────────────────────────────────────────────────────────────────
129
184
  console.log();
130
- console.log(" " + c.green("") + " .env encrypted " + c.dim("(safe to commit)"));
131
- console.log(" " + c.green("") + " .env.keys kept " + c.dim("(gitignored)") );
132
- console.log(" " + c.green("") + " .suron.json written " + c.dim("(safe to commit)"));
133
- console.log(" " + c.green("") + " @suronai/sdk installed");
185
+ console.log(" " + c.dim("") + " .env encrypted " + c.dim("safe to commit"));
186
+ console.log(" " + c.dim("") + " .env.keys " + c.dim("gitignored, keep it safe"));
187
+ console.log(" " + c.dim("") + " .suron.json " + c.dim("safe to commit"));
188
+ console.log(" " + c.dim("") + " " + c.bold("ready"));
134
189
  console.log();
135
190
  });
136
191
 
137
192
  /**
138
- * Ensures .env.keys is in .gitignore.
139
- * Creates .gitignore if it doesn't exist.
140
- * Appends the entry if it's not already present (exact line match).
193
+ * Ensures standard entries are in .gitignore.
194
+ * Creates the file if it doesn't exist.
141
195
  * @param {string} cwd
196
+ * @returns {string} short note for the steps display
142
197
  */
143
198
  const GITIGNORE_ENTRIES = [
144
199
  "node_modules/",
@@ -151,16 +206,17 @@ function ensureGitignore(cwd) {
151
206
 
152
207
  if (!existsSync(gitignorePath)) {
153
208
  writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
154
- return;
209
+ return "created";
155
210
  }
156
211
 
157
- const content = readFileSync(gitignorePath, "utf-8");
212
+ const content = readFileSync(gitignorePath, "utf-8");
158
213
  const existing = new Set(content.split("\n").map(l => l.trim()));
159
- const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
160
- if (missing.length === 0) return;
214
+ const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
215
+ if (missing.length === 0) return "already set";
161
216
 
162
217
  const separator = content.endsWith("\n") ? "" : "\n";
163
218
  writeFileSync(gitignorePath, content + separator + missing.join("\n") + "\n", "utf-8");
219
+ return "updated";
164
220
  }
165
221
 
166
222
  /** @param {string} cwd @returns {string} */
@@ -177,23 +233,17 @@ function pmAddCmd(pm) {
177
233
  }
178
234
 
179
235
  /**
180
- * Strips hyphens, underscores, spaces and other non-alphanumeric characters
181
- * so the app name is always a single lowercase word. e.g. "camp-haven" -> "camphaven".
236
+ * Strips everything except letters and digits while preserving the original case.
237
+ * The backend stores names case-sensitively for display but compares lowercase
238
+ * for uniqueness, so "DataHaven" and "datahaven" are the same app.
182
239
  * @param {string} name
183
240
  * @returns {string}
184
241
  */
185
242
  function sanitiseName(name) {
186
- return name.toLowerCase().replace(/[^a-z0-9]/g, "");
243
+ return name.replace(/[^a-zA-Z0-9]/g, "");
187
244
  }
188
245
 
189
246
  /**
190
- * Scans the entry point line-by-line for any lines that reference "dotenv".
191
- * Shows the user exactly what was found and what it will be replaced with,
192
- * then asks for confirmation before writing.
193
- *
194
- * Works for any import/require pattern because it does a literal line search
195
- * rather than trying to anticipate every possible dotenv usage style.
196
- *
197
247
  * @param {string} cwd
198
248
  * @param {boolean} isEsm
199
249
  */
@@ -209,7 +259,9 @@ async function patchEntryPoint(cwd, isEsm) {
209
259
  }
210
260
 
211
261
  if (!entryPath) {
212
- console.log("\n Add to your app entry point:\n");
262
+ console.log(" " + c.dim("▎"));
263
+ console.log(" " + c.dim("▎") + " Add to your app entry point:");
264
+ console.log(" " + c.dim("▎"));
213
265
  printSnippet(isEsm);
214
266
  return;
215
267
  }
@@ -219,11 +271,8 @@ async function patchEntryPoint(cwd, isEsm) {
219
271
 
220
272
  const lines = src.split("\n");
221
273
 
222
- // Two-pass scan:
223
- // Pass 1 — lines containing "dotenv" (import or require)
224
- // Pass 2 — bare config() / config({...}) call lines that don't contain "dotenv"
225
- // (these appear after the import was already on a separate line)
226
- const toReplace = [];
274
+ // Pass 1 — lines containing "dotenv"
275
+ const toReplace = [];
227
276
  const seenIndices = new Set();
228
277
 
229
278
  for (let i = 0; i < lines.length; i++) {
@@ -246,13 +295,12 @@ async function patchEntryPoint(cwd, isEsm) {
246
295
  }
247
296
  }
248
297
 
249
- // Pass 2: bare config() call only in ESM where the import and call are separate lines
298
+ // Pass 2 bare config() call (ESM only import and call are on separate lines)
250
299
  if (isEsm) {
251
300
  for (let i = 0; i < lines.length; i++) {
252
301
  if (seenIndices.has(i)) continue;
253
302
  const trimmed = lines[i].trim();
254
303
  const indent = lines[i].match(/^(\s*)/)[1];
255
- // Matches: config() / config({}) / config({ ... }) — with or without semicolon
256
304
  if (/^config\s*\(.*\);?$/.test(trimmed) && !trimmed.startsWith("//")) {
257
305
  toReplace.push({
258
306
  index: i,
@@ -261,78 +309,73 @@ async function patchEntryPoint(cwd, isEsm) {
261
309
  });
262
310
  }
263
311
  }
264
- // Sort by line number so the diff preview is in file order
265
312
  toReplace.sort((a, b) => a.index - b.index);
266
313
  }
267
314
 
268
315
  if (toReplace.length === 0) {
269
- console.log("\n Add to your app entry point:\n");
316
+ console.log(" " + c.dim("▎"));
317
+ console.log(" " + c.dim("▎") + " Add to your app entry point:");
318
+ console.log(" " + c.dim("▎"));
270
319
  printSnippet(isEsm);
271
320
  return;
272
321
  }
273
322
 
274
- const relEntry = entryPath.replace(cwd + "\\", "").replace(cwd + "/", "");
275
- console.log();
276
- console.log(" " + c.yellow("▶") + " Found dotenv in " + c.dim(relEntry) + ":");
277
- console.log();
323
+ const relEntry = relative(cwd, entryPath);
278
324
 
279
- const replacements = toReplace;
325
+ // ── Diff preview ───────────────────────────────────────────────────────────
326
+ console.log(" " + c.dim("▎"));
327
+ console.log(" " + c.dim("▎ ") + c.dim(relEntry));
328
+ console.log(" " + c.dim("▎"));
280
329
 
281
- // Show the diff preview
282
- for (const { content, replacement } of replacements) {
283
- console.log(" " + c.red("-") + " " + c.dim(content.trim()));
330
+ for (const { content, replacement } of toReplace) {
331
+ console.log(" " + c.dim("▎ ") + c.red("⁻") + " " + c.dim(content.trim()));
284
332
  if (replacement !== null) {
285
333
  for (const l of replacement.split("\n")) {
286
- console.log(" " + c.green("+") + " " + c.cyan(l.trim()));
334
+ console.log(" " + c.dim("▎ ") + c.green("⁺") + " " + c.cyan(l.trim()));
287
335
  }
288
336
  } else {
289
- console.log(" " + c.yellow("?") + " " + c.dim("(no automatic replacement — update manually)"));
337
+ console.log(" " + c.dim("▎ ") + c.yellow("?") + " " + c.dim("(no automatic replacement — update manually)"));
290
338
  }
291
- console.log();
339
+ console.log(" " + c.dim("▎"));
292
340
  }
293
341
 
294
- const answer = await prompt(" Apply replacements? [" + c.green("Y") + "/n] › ");
342
+ const answer = await prompt(" " + c.dim("") + " apply? " + c.dim("[Y/n] › "));
295
343
  const confirmed = answer === "" || /^y(es)?$/i.test(answer);
344
+ console.log(HR);
296
345
 
297
346
  if (!confirmed) {
298
- console.log(" " + c.dim("Skipped.") + " Add manually:\n");
347
+ console.log();
348
+ console.log(" " + c.dim("skipped — add manually:"));
349
+ console.log();
299
350
  printSnippet(isEsm);
300
351
  return;
301
352
  }
302
353
 
303
- // Apply — walk lines, replace only the matched dotenv lines, touch nothing else
304
- const indexMap = new Map(replacements.map(r => [r.index, r]));
354
+ // Apply
355
+ const indexMap = new Map(toReplace.map(r => [r.index, r]));
305
356
  const outLines = lines.map((line, i) => {
306
357
  const r = indexMap.get(i);
307
358
  return (r && r.replacement !== null) ? r.replacement : line;
308
359
  });
309
360
 
310
- // For ESM: if await config() isn't already right after the imports, move it there
311
- // automatically as part of the same patch — no second prompt needed.
361
+ // Move await config() to right after last import block (ESM)
312
362
  if (isEsm) {
313
- const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(\s*\);?\s*$/.test(l));
363
+ const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(/.test(l));
314
364
  const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
315
365
 
316
- if (callLineIndex !== -1 && callLineIndex !== lastImportIndex + 1 && callLineIndex !== lastImportIndex + 2) {
366
+ if (callLineIndex !== -1 && callLineIndex > lastImportIndex + 2) {
317
367
  const callLine = outLines[callLineIndex];
318
- // Remove from current position (and any adjacent blank line below it)
319
368
  outLines.splice(callLineIndex, 1);
320
369
  if (outLines[callLineIndex]?.trim() === "") outLines.splice(callLineIndex, 1);
321
-
322
- // Re-find last import index after splice (indices shifted)
323
370
  const newLastImport = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
324
-
325
- // Insert: blank line + await config() + blank line, after last import
326
371
  outLines.splice(newLastImport + 1, 0, "", callLine, "");
327
372
  }
328
373
  }
329
374
 
330
375
  try {
331
376
  writeFileSync(entryPath, outLines.join("\n"), "utf-8");
332
- console.log(" " + c.green("✓") + " " + c.dim(relEntry) + " patched");
333
377
  } catch (err) {
334
378
  console.error(" " + c.red("✗") + " Could not write " + relEntry + ": " + err.message);
335
- console.log(" Add manually:\n");
336
379
  printSnippet(isEsm);
337
380
  }
338
381
  }
@@ -340,11 +383,12 @@ async function patchEntryPoint(cwd, isEsm) {
340
383
  /** @param {boolean} isEsm */
341
384
  function printSnippet(isEsm) {
342
385
  if (isEsm) {
343
- console.log(" " + c.cyan("import") + " { config } from " + c.green("'@suronai/sdk'"));
344
- console.log(" " + c.cyan("await") + " config()");
386
+ console.log(" " + c.dim("▎ ") + c.cyan("import") + " { config } from " + c.green("'@suronai/sdk'"));
387
+ console.log(" " + c.dim("▎ ") + c.cyan("await") + " config()");
345
388
  } else {
346
- console.log(" const { config } = " + c.cyan("require") + "(" + c.green("'@suronai/sdk'") + ")");
347
- console.log(" " + c.cyan("await") + " config()");
389
+ console.log(" " + c.dim("▎ ") + "const { config } = " + c.cyan("require") + "(" + c.green("'@suronai/sdk'") + ")");
390
+ console.log(" " + c.dim("▎ ") + c.cyan("await") + " config()");
348
391
  }
349
- console.log();
392
+ console.log(" " + c.dim("▎"));
393
+ console.log(HR);
350
394
  }
@@ -23,18 +23,21 @@ export const recoverCommand = new Command("recover")
23
23
  }
24
24
 
25
25
  // ── App name ──────────────────────────────────────────────────────────────
26
+ // Names are case-insensitive for lookup — "datahaven" finds "DataHaven".
27
+ // We preserve the input casing here; the backend returns the canonical name.
26
28
  let appName;
27
29
  if (opts.name) {
28
- appName = opts.name.toLowerCase().replace(/[^a-z0-9]/g, "");
30
+ // Strip non-alphanumeric but preserve case — backend does case-insensitive match.
31
+ appName = opts.name.replace(/[^a-zA-Z0-9]/g, "");
29
32
  console.log(" App name " + c.cyan(appName));
30
33
  } else {
31
- const suggested = basename(cwd) || "my-project";
34
+ const suggested = sanitiseName(basename(cwd) || "myapp");
32
35
  const raw = await prompt(` App name [${c.dim(suggested)}] › `);
33
- appName = (raw || suggested).toLowerCase().replace(/[^a-z0-9]/g, "");
36
+ appName = sanitiseName(raw || suggested);
34
37
  }
35
38
 
36
39
  if (!appName) {
37
- console.error(" " + c.red("✗") + " App name is required.\n");
40
+ console.error(" " + c.red("✗") + " App name is required and must contain at least one letter or digit.\n");
38
41
  process.exit(1);
39
42
  }
40
43
 
@@ -58,7 +61,7 @@ export const recoverCommand = new Command("recover")
58
61
  try { body = await res.json(); } catch { /* ignore */ }
59
62
  if (res.status === 404) {
60
63
  console.error("\n " + c.red("✗") + ` No app named "${c.cyan(appName)}" found.`);
61
- console.error(" Check the name carefully names are lowercase and alphanumeric only.\n");
64
+ console.error(" Lookup is case-insensitivecheck spelling (letters and numbers only).\n");
62
65
  } else {
63
66
  console.error("\n " + c.red("✗") + ` ${body?.error ?? `recover-app failed (${res.status})`}\n`);
64
67
  }
@@ -68,6 +71,7 @@ export const recoverCommand = new Command("recover")
68
71
  const { app_id, name } = await res.json();
69
72
 
70
73
  // ── Write .suron.json ─────────────────────────────────────────────────────
74
+ // Use the canonical name returned by the server (original registration casing).
71
75
  writeFileSync(
72
76
  join(cwd, ".suron.json"),
73
77
  JSON.stringify({ app: name, id: app_id, api_url: apiUrl }, null, 2) + "\n",
@@ -80,3 +84,12 @@ export const recoverCommand = new Command("recover")
80
84
  console.log(" " + c.dim(" id: ") + c.cyan(app_id));
81
85
  console.log();
82
86
  });
87
+
88
+ /**
89
+ * Strips non-alphanumeric characters while preserving case.
90
+ * @param {string} name
91
+ * @returns {string}
92
+ */
93
+ function sanitiseName(name) {
94
+ return name.replace(/[^a-zA-Z0-9]/g, "");
95
+ }