@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 +1 -1
- package/src/commands/init.js +129 -85
- package/src/commands/recover.js +18 -5
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -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(
|
|
18
|
+
console.log();
|
|
17
19
|
|
|
18
20
|
if (existsSync(join(cwd, ".suron.json"))) {
|
|
19
|
-
console.error(" " + c.red("✗") + " .suron.json
|
|
20
|
-
console.error(" To restore a lost config
|
|
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
|
|
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
|
-
|
|
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(
|
|
40
|
+
const raw = await prompt(" App name " + c.dim("· ") + c.dim("[" + suggested + "] › "));
|
|
39
41
|
appName = sanitiseName(raw || suggested);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
// ──
|
|
61
|
-
|
|
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
|
-
|
|
81
|
-
console.error("
|
|
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: "
|
|
162
|
+
execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "pipe" });
|
|
163
|
+
steps.sdk.status = "done";
|
|
117
164
|
} catch {
|
|
118
|
-
|
|
165
|
+
steps.sdk.status = "fail";
|
|
166
|
+
steps.sdk.note = "run: npm install @suronai/sdk";
|
|
119
167
|
}
|
|
120
168
|
} else {
|
|
121
|
-
|
|
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.
|
|
131
|
-
console.log(" " + c.
|
|
132
|
-
console.log(" " + c.
|
|
133
|
-
console.log(" " + c.
|
|
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
|
|
139
|
-
* Creates
|
|
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
|
|
212
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
158
213
|
const existing = new Set(content.split("\n").map(l => l.trim()));
|
|
159
|
-
const missing
|
|
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
|
|
181
|
-
*
|
|
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.
|
|
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("
|
|
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
|
-
//
|
|
223
|
-
|
|
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
|
|
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("
|
|
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 =
|
|
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
|
-
|
|
325
|
+
// ── Diff preview ───────────────────────────────────────────────────────────
|
|
326
|
+
console.log(" " + c.dim("▎"));
|
|
327
|
+
console.log(" " + c.dim("▎ ") + c.dim(relEntry));
|
|
328
|
+
console.log(" " + c.dim("▎"));
|
|
280
329
|
|
|
281
|
-
|
|
282
|
-
|
|
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("
|
|
334
|
+
console.log(" " + c.dim("▎ ") + c.green("⁺") + " " + c.cyan(l.trim()));
|
|
287
335
|
}
|
|
288
336
|
} else {
|
|
289
|
-
console.log("
|
|
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("
|
|
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(
|
|
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
|
|
304
|
-
const indexMap = new Map(
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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("
|
|
344
|
-
console.log("
|
|
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("
|
|
347
|
-
console.log("
|
|
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
|
}
|
package/src/commands/recover.js
CHANGED
|
@@ -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
|
-
|
|
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) || "
|
|
34
|
+
const suggested = sanitiseName(basename(cwd) || "myapp");
|
|
32
35
|
const raw = await prompt(` App name [${c.dim(suggested)}] › `);
|
|
33
|
-
appName = (raw || suggested)
|
|
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("
|
|
64
|
+
console.error(" Lookup is case-insensitive — check 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
|
+
}
|