@suronai/cli 0.1.35 → 0.1.37

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 DELETED
@@ -1,134 +0,0 @@
1
- # @suronai/cli
2
-
3
- CLI for [Suron](https://suronai.com) — encrypt your `.env`, register your app, and manage secret delivery gated by Telegram approval.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install -g @suronai/cli
9
- ```
10
-
11
- Requires Node.js 18+.
12
-
13
- ---
14
-
15
- ## Quick start
16
-
17
- ```bash
18
- suron login # point the CLI at your Suron deployment
19
- cd my-project
20
- suron init # encrypt .env, register app, patch entry point
21
- ```
22
-
23
- After `suron init`, both `.env` and `.suron.json` are safe to commit.
24
-
25
- ---
26
-
27
- ## Commands
28
-
29
- ### `suron login`
30
-
31
- Saves your Convex deployment URL to `~/.suron-config`. Run this once per machine.
32
-
33
- ```bash
34
- suron login
35
- ```
36
-
37
- You can also set it via environment variable instead:
38
-
39
- ```bash
40
- export SURON_API_URL=https://your-deployment.convex.site
41
- ```
42
-
43
- ---
44
-
45
- ### `suron init`
46
-
47
- Run inside your project directory. Does the full setup in one command:
48
-
49
- 1. Encrypts `.env` in-place with [dotenvx](https://dotenvx.com)
50
- 2. Registers your app with Suron (uploads the encrypted private key)
51
- 3. Installs `@suronai/sdk` into your project
52
- 4. Writes `.suron.json` with your app name and ID
53
- 5. Deletes `.env.keys`
54
- 6. Detects `dotenv` in your entry point and offers to replace it with `@suronai/sdk`
55
-
56
- ```bash
57
- cd my-project
58
- suron init
59
-
60
- # set a custom app name without the interactive prompt
61
- suron init --name my-app
62
- ```
63
-
64
- **Entry point patching**
65
-
66
- If `index.js` (or `src/index.js`) contains a `dotenv` bootstrap, `suron init` shows you exactly what will change and asks before touching anything:
67
-
68
- ```
69
- ▶ Found dotenv in index.js
70
-
71
- Will replace:
72
- - import { config } from 'dotenv'
73
- - config()
74
- With:
75
- + import { config } from '@suronai/sdk'
76
- + await config()
77
-
78
- Replace? [Y/n] ›
79
- ```
80
-
81
- If dotenv isn't found, the snippet is printed for you to add manually.
82
-
83
- **Files after `suron init`**
84
-
85
- | File | Commit? | Notes |
86
- |---|---|---|
87
- | `.env` | ✅ yes | encrypted by dotenvx, no plaintext secrets |
88
- | `.suron.json` | ✅ yes | app name, id, api_url — no secrets |
89
- | `.env.keys` | ⛔ no | keep safe, add to `.gitignore` |
90
- | `~/.suron-config` | ⛔ no | machine-local CLI config |
91
-
92
- ---
93
-
94
- ### `suron whoami`
95
-
96
- Prints the configured Suron API URL.
97
-
98
- ```bash
99
- suron whoami
100
- ```
101
-
102
- ---
103
-
104
- ### `suron rotate`
105
-
106
- Re-encrypts `.env` with a new private key, sends the updated key to Suron, and deletes `.env.keys`. Use this for routine key rotation or if a key may have been exposed.
107
-
108
- ```bash
109
- suron rotate
110
- ```
111
-
112
- ---
113
-
114
- ## How it works
115
-
116
- ```
117
- suron init
118
- └─ dotenvx encrypt .env # encrypts secrets in-place
119
- └─ POST /cli/register-app # sends encrypted private key to Convex
120
- └─ writes .suron.json # stores app_id locally
121
- └─ deletes .env.keys # private key lives in Convex only
122
-
123
- app boot (via @suronai/sdk)
124
- └─ POST /request-access # sends boot request, triggers Telegram message
125
- └─ GET /status (polling) # waits for Approve / Deny tap
126
- └─ POST /fetch-key # retrieves private key (single-use)
127
- └─ dotenvx.parse(.env, key) # decrypts secrets into process.env
128
- ```
129
-
130
- ---
131
-
132
- ## Related
133
-
134
- - [`@suronai/sdk`](https://www.npmjs.com/package/@suronai/sdk) — the runtime SDK your app imports
@@ -1,394 +0,0 @@
1
- import { Command } from "commander";
2
- import { existsSync, writeFileSync, readFileSync } from "fs";
3
- import { join, basename, relative } from "path";
4
- import { execSync } from "child_process";
5
- import { requireApiUrl, prompt } from "../utils/config.js";
6
- import { encryptDotenv, readPrivateKey } from "../utils/dotenvx.js";
7
- import c from "../utils/colors.js";
8
-
9
- const HR = " " + c.dim("─".repeat(55));
10
-
11
- export const initCommand = new Command("init")
12
- .description("Encrypt .env and register this app with Suron")
13
- .option("--name <n>", "App name (skips interactive prompt)")
14
- .action(async (opts) => {
15
- const cwd = process.cwd();
16
- const apiUrl = requireApiUrl();
17
-
18
- console.log();
19
-
20
- if (existsSync(join(cwd, ".suron.json"))) {
21
- console.error(" " + c.red("✗") + " already initialised — .suron.json exists");
22
- console.error(" To restore a lost config: " + c.bold("suron recover") + "\n");
23
- process.exit(1);
24
- }
25
-
26
- if (!existsSync(join(cwd, ".env"))) {
27
- console.error(" " + c.red("✗") + " .env not found");
28
- console.error(" Create a .env file with your secrets, then run: suron init\n");
29
- process.exit(1);
30
- }
31
-
32
- // ── App name ──────────────────────────────────────────────────────────────
33
- // Preserve the folder's original casing as the default suggestion.
34
- const suggested = sanitiseName(basename(cwd) || "myapp");
35
- let appName;
36
-
37
- if (opts.name) {
38
- appName = sanitiseName(opts.name);
39
- } else {
40
- const raw = await prompt(" App name " + c.dim("· ") + c.dim("[" + suggested + "] › "));
41
- appName = sanitiseName(raw || suggested);
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
- }
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 ───────────────────────────────────────────────────────────────
77
- try {
78
- encryptDotenv(cwd);
79
- steps.encrypt.status = "done";
80
- steps.keys.status = "done";
81
- } catch (err) {
82
- steps.encrypt.status = "fail";
83
- printSteps();
84
- console.log(HR);
85
- console.error("\n " + c.red("✗") + " " + err.message + "\n");
86
- process.exit(1);
87
- }
88
-
89
- let privateKey;
90
- try {
91
- privateKey = readPrivateKey(cwd);
92
- } catch (err) {
93
- steps.keys.status = "fail";
94
- printSteps();
95
- console.log(HR);
96
- console.error("\n " + c.red("✗") + " " + err.message + "\n");
97
- process.exit(1);
98
- }
99
-
100
- // ── .gitignore ────────────────────────────────────────────────────────────
101
- const gitignoreResult = ensureGitignore(cwd);
102
- steps.gitignore.status = "done";
103
- steps.gitignore.note = gitignoreResult;
104
-
105
- // ── Register ──────────────────────────────────────────────────────────────
106
- let res;
107
- try {
108
- res = await fetch(`${apiUrl}/cli/register-app`, {
109
- method: "POST",
110
- headers: { "Content-Type": "application/json" },
111
- body: JSON.stringify({ name: appName, private_key: privateKey }),
112
- });
113
- } catch (err) {
114
- steps.register.status = "fail";
115
- printSteps();
116
- console.log(HR);
117
- console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
118
- process.exit(1);
119
- }
120
-
121
- if (!res.ok) {
122
- let body = {};
123
- try { body = await res.json(); } catch { /* ignore */ }
124
- steps.register.status = "fail";
125
- printSteps();
126
- console.log(HR);
127
- if (res.status === 409) {
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");
131
- } else {
132
- console.error("\n " + c.red("✗") + ` ${body?.error ?? `register-app failed (${res.status})`}\n`);
133
- }
134
- process.exit(1);
135
- }
136
-
137
- const { app_id } = await res.json();
138
- steps.register.status = "done";
139
-
140
- // ── Write .suron.json ─────────────────────────────────────────────────────
141
- writeFileSync(
142
- join(cwd, ".suron.json"),
143
- JSON.stringify({ app: appName, id: app_id, api_url: apiUrl }, null, 2) + "\n",
144
- "utf-8"
145
- );
146
-
147
- // ── Install SDK ───────────────────────────────────────────────────────────
148
- const pkgJsonPath = join(cwd, "package.json");
149
- let isEsm = false;
150
-
151
- if (existsSync(pkgJsonPath)) {
152
- let alreadyInstalled = false;
153
- try {
154
- const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
155
- alreadyInstalled = !!(pkg.dependencies?.["@suronai/sdk"] || pkg.devDependencies?.["@suronai/sdk"]);
156
- isEsm = pkg.type === "module";
157
- } catch { /* ignore */ }
158
-
159
- if (!alreadyInstalled) {
160
- const pm = detectPackageManager(cwd);
161
- try {
162
- execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "pipe" });
163
- steps.sdk.status = "done";
164
- } catch {
165
- steps.sdk.status = "fail";
166
- steps.sdk.note = "run: npm install @suronai/sdk";
167
- }
168
- } else {
169
- steps.sdk.status = "skip";
170
- steps.sdk.note = "already installed";
171
- }
172
- } else {
173
- steps.sdk.status = "skip";
174
- steps.sdk.note = "no package.json";
175
- }
176
-
177
- printSteps();
178
- console.log(HR);
179
-
180
- // ── Patch entry point ─────────────────────────────────────────────────────
181
- await patchEntryPoint(cwd, isEsm);
182
-
183
- // ── Done ──────────────────────────────────────────────────────────────────
184
- console.log();
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"));
189
- console.log();
190
- });
191
-
192
- /**
193
- * Ensures standard entries are in .gitignore.
194
- * Creates the file if it doesn't exist.
195
- * @param {string} cwd
196
- * @returns {string} short note for the steps display
197
- */
198
- const GITIGNORE_ENTRIES = [
199
- "node_modules/",
200
- "package-lock.json",
201
- ".env.keys",
202
- ];
203
-
204
- function ensureGitignore(cwd) {
205
- const gitignorePath = join(cwd, ".gitignore");
206
-
207
- if (!existsSync(gitignorePath)) {
208
- writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
209
- return "created";
210
- }
211
-
212
- const content = readFileSync(gitignorePath, "utf-8");
213
- const existing = new Set(content.split("\n").map(l => l.trim()));
214
- const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
215
- if (missing.length === 0) return "already set";
216
-
217
- const separator = content.endsWith("\n") ? "" : "\n";
218
- writeFileSync(gitignorePath, content + separator + missing.join("\n") + "\n", "utf-8");
219
- return "updated";
220
- }
221
-
222
- /** @param {string} cwd @returns {string} */
223
- function detectPackageManager(cwd) {
224
- if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
225
- if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
226
- if (existsSync(join(cwd, "bun.lockb"))) return "bun";
227
- return "npm";
228
- }
229
-
230
- /** @param {string} pm @returns {string} */
231
- function pmAddCmd(pm) {
232
- return pm === "npm" ? "install" : "add";
233
- }
234
-
235
- /**
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.
239
- * @param {string} name
240
- * @returns {string}
241
- */
242
- function sanitiseName(name) {
243
- return name.replace(/[^a-zA-Z0-9]/g, "");
244
- }
245
-
246
- /**
247
- * @param {string} cwd
248
- * @param {boolean} isEsm
249
- */
250
- async function patchEntryPoint(cwd, isEsm) {
251
- const candidates = isEsm
252
- ? ["index.js", "index.mjs", "src/index.js", "src/index.mjs"]
253
- : ["index.js", "index.cjs", "src/index.js", "src/index.cjs"];
254
-
255
- let entryPath = null;
256
- for (const rel of candidates) {
257
- const abs = join(cwd, rel);
258
- if (existsSync(abs)) { entryPath = abs; break; }
259
- }
260
-
261
- if (!entryPath) {
262
- console.log(" " + c.dim("▎"));
263
- console.log(" " + c.dim("▎") + " Add to your app entry point:");
264
- console.log(" " + c.dim("▎"));
265
- printSnippet(isEsm);
266
- return;
267
- }
268
-
269
- let src;
270
- try { src = readFileSync(entryPath, "utf-8"); } catch { return; }
271
-
272
- const lines = src.split("\n");
273
-
274
- // Pass 1 — lines containing "dotenv"
275
- const toReplace = [];
276
- const seenIndices = new Set();
277
-
278
- for (let i = 0; i < lines.length; i++) {
279
- const trimmed = lines[i].trim();
280
- const indent = lines[i].match(/^(\s*)/)[1];
281
-
282
- if (lines[i].includes("dotenv")) {
283
- seenIndices.add(i);
284
- let replacement = null;
285
- if (isEsm) {
286
- if (trimmed.startsWith("import")) {
287
- replacement = indent + trimmed.replace(/(from\s+)['"]dotenv(?:\/config)?['"]/, '$1"@suronai/sdk"');
288
- }
289
- } else {
290
- if (trimmed.includes("require")) {
291
- replacement = indent + "const { config } = require(\"@suronai/sdk\");\n" + indent + "await config();";
292
- }
293
- }
294
- toReplace.push({ index: i, content: lines[i], replacement });
295
- }
296
- }
297
-
298
- // Pass 2 — bare config() call (ESM only — import and call are on separate lines)
299
- if (isEsm) {
300
- for (let i = 0; i < lines.length; i++) {
301
- if (seenIndices.has(i)) continue;
302
- const trimmed = lines[i].trim();
303
- const indent = lines[i].match(/^(\s*)/)[1];
304
- if (/^config\s*\(.*\);?$/.test(trimmed) && !trimmed.startsWith("//")) {
305
- toReplace.push({
306
- index: i,
307
- content: lines[i],
308
- replacement: indent + trimmed.replace(/^config\s*\(/, "await config("),
309
- });
310
- }
311
- }
312
- toReplace.sort((a, b) => a.index - b.index);
313
- }
314
-
315
- if (toReplace.length === 0) {
316
- console.log(" " + c.dim("▎"));
317
- console.log(" " + c.dim("▎") + " Add to your app entry point:");
318
- console.log(" " + c.dim("▎"));
319
- printSnippet(isEsm);
320
- return;
321
- }
322
-
323
- const relEntry = relative(cwd, entryPath);
324
-
325
- // ── Diff preview ───────────────────────────────────────────────────────────
326
- console.log(" " + c.dim("▎"));
327
- console.log(" " + c.dim("▎ ") + c.dim(relEntry));
328
- console.log(" " + c.dim("▎"));
329
-
330
- for (const { content, replacement } of toReplace) {
331
- console.log(" " + c.dim("▎ ") + c.red("⁻") + " " + c.dim(content.trim()));
332
- if (replacement !== null) {
333
- for (const l of replacement.split("\n")) {
334
- console.log(" " + c.dim("▎ ") + c.green("⁺") + " " + c.cyan(l.trim()));
335
- }
336
- } else {
337
- console.log(" " + c.dim("▎ ") + c.yellow("?") + " " + c.dim("(no automatic replacement — update manually)"));
338
- }
339
- console.log(" " + c.dim("▎"));
340
- }
341
-
342
- const answer = await prompt(" " + c.dim("▎") + " apply? " + c.dim("[Y/n] › "));
343
- const confirmed = answer === "" || /^y(es)?$/i.test(answer);
344
- console.log(HR);
345
-
346
- if (!confirmed) {
347
- console.log();
348
- console.log(" " + c.dim("skipped — add manually:"));
349
- console.log();
350
- printSnippet(isEsm);
351
- return;
352
- }
353
-
354
- // Apply
355
- const indexMap = new Map(toReplace.map(r => [r.index, r]));
356
- const outLines = lines.map((line, i) => {
357
- const r = indexMap.get(i);
358
- return (r && r.replacement !== null) ? r.replacement : line;
359
- });
360
-
361
- // Move await config() to right after last import block (ESM)
362
- if (isEsm) {
363
- const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(/.test(l));
364
- const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
365
-
366
- if (callLineIndex !== -1 && callLineIndex > lastImportIndex + 2) {
367
- const callLine = outLines[callLineIndex];
368
- outLines.splice(callLineIndex, 1);
369
- if (outLines[callLineIndex]?.trim() === "") outLines.splice(callLineIndex, 1);
370
- const newLastImport = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
371
- outLines.splice(newLastImport + 1, 0, "", callLine, "");
372
- }
373
- }
374
-
375
- try {
376
- writeFileSync(entryPath, outLines.join("\n"), "utf-8");
377
- } catch (err) {
378
- console.error(" " + c.red("✗") + " Could not write " + relEntry + ": " + err.message);
379
- printSnippet(isEsm);
380
- }
381
- }
382
-
383
- /** @param {boolean} isEsm */
384
- function printSnippet(isEsm) {
385
- if (isEsm) {
386
- console.log(" " + c.dim("▎ ") + c.cyan("import") + " { config } from " + c.green("'@suronai/sdk'"));
387
- console.log(" " + c.dim("▎ ") + c.cyan("await") + " config()");
388
- } else {
389
- console.log(" " + c.dim("▎ ") + "const { config } = " + c.cyan("require") + "(" + c.green("'@suronai/sdk'") + ")");
390
- console.log(" " + c.dim("▎ ") + c.cyan("await") + " config()");
391
- }
392
- console.log(" " + c.dim("▎"));
393
- console.log(HR);
394
- }
@@ -1,56 +0,0 @@
1
- import { Command } from "commander";
2
- import { getApiUrl, saveConfig, prompt } from "../utils/config.js";
3
- import c from "../utils/colors.js";
4
-
5
- export const loginCommand = new Command("login")
6
- .description("Configure the Suron API URL")
7
- .action(async () => {
8
- console.log("\n" + c.bold(" Suron") + " — configure your backend\n");
9
-
10
- let apiUrl = getApiUrl();
11
-
12
- if (apiUrl) {
13
- console.log(" Current URL " + c.cyan(apiUrl));
14
- const change = await prompt(" Change it? (y/N) › ");
15
- if (change.toLowerCase() !== "y") {
16
- console.log("\n " + c.green("✓") + " No changes made.\n");
17
- return;
18
- }
19
- }
20
-
21
- const input = await prompt(" Convex deployment URL › ");
22
- if (!input) {
23
- console.error("\n " + c.red("✗") + " URL is required.\n");
24
- process.exit(1);
25
- }
26
- apiUrl = input.replace(/\/$/, "");
27
-
28
- process.stdout.write("\n Verifying connection...");
29
-
30
- try {
31
- const res = await fetch(`${apiUrl}/cli/verify`, {
32
- method: "POST",
33
- headers: { "Content-Type": "application/json" },
34
- body: JSON.stringify({}),
35
- });
36
- if (!res.ok) {
37
- const text = await res.text().catch(() => "");
38
- process.stdout.write("\r" + " ".repeat(30) + "\r");
39
- console.error(" " + c.red("✗") + ` Backend returned ${res.status}: ${text}\n`);
40
- process.exit(1);
41
- }
42
- } catch {
43
- process.stdout.write("\r" + " ".repeat(30) + "\r");
44
- console.error(" " + c.red("✗") + ` Could not reach ${c.cyan(apiUrl)}`);
45
- console.error(" Is the Convex deployment running?\n");
46
- process.exit(1);
47
- }
48
-
49
- process.stdout.write("\r" + " ".repeat(30) + "\r");
50
-
51
- saveConfig({ apiUrl });
52
-
53
- console.log(" " + c.green("✓") + " Connected");
54
- console.log(" " + c.green("✓") + " Saved to ~/.suron-config");
55
- console.log("\n Next: cd your-project && " + c.bold("suron init") + "\n");
56
- });
@@ -1,95 +0,0 @@
1
- import { Command } from "commander";
2
- import { existsSync, writeFileSync } from "fs";
3
- import { join, basename } from "path";
4
- import { requireApiUrl, prompt } from "../utils/config.js";
5
- import c from "../utils/colors.js";
6
-
7
- export const recoverCommand = new Command("recover")
8
- .description("Restore a lost .suron.json by looking up your app name")
9
- .option("--name <n>", "App name to recover (skips interactive prompt)")
10
- .action(async (opts) => {
11
- const cwd = process.cwd();
12
- const apiUrl = requireApiUrl();
13
-
14
- console.log("\n" + c.bold(" suron recover") + " — " + c.dim(cwd) + "\n");
15
-
16
- if (existsSync(join(cwd, ".suron.json"))) {
17
- console.log(" " + c.yellow("⚠") + " .suron.json already exists here.");
18
- const answer = await prompt(" Overwrite it? [y/N] › ");
19
- if (!/^y(es)?$/i.test(answer)) {
20
- console.log(" " + c.dim("Cancelled.\n"));
21
- process.exit(0);
22
- }
23
- }
24
-
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.
28
- let appName;
29
- if (opts.name) {
30
- // Strip non-alphanumeric but preserve case — backend does case-insensitive match.
31
- appName = opts.name.replace(/[^a-zA-Z0-9]/g, "");
32
- console.log(" App name " + c.cyan(appName));
33
- } else {
34
- const suggested = sanitiseName(basename(cwd) || "myapp");
35
- const raw = await prompt(` App name [${c.dim(suggested)}] › `);
36
- appName = sanitiseName(raw || suggested);
37
- }
38
-
39
- if (!appName) {
40
- console.error(" " + c.red("✗") + " App name is required and must contain at least one letter or digit.\n");
41
- process.exit(1);
42
- }
43
-
44
- // ── Look up app ───────────────────────────────────────────────────────────
45
- console.log("\n " + c.dim("Looking up app..."));
46
-
47
- let res;
48
- try {
49
- res = await fetch(`${apiUrl}/cli/recover-app`, {
50
- method: "POST",
51
- headers: { "Content-Type": "application/json" },
52
- body: JSON.stringify({ name: appName }),
53
- });
54
- } catch (err) {
55
- console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
56
- process.exit(1);
57
- }
58
-
59
- if (!res.ok) {
60
- let body = {};
61
- try { body = await res.json(); } catch { /* ignore */ }
62
- if (res.status === 404) {
63
- console.error("\n " + c.red("✗") + ` No app named "${c.cyan(appName)}" found.`);
64
- console.error(" Lookup is case-insensitive — check spelling (letters and numbers only).\n");
65
- } else {
66
- console.error("\n " + c.red("✗") + ` ${body?.error ?? `recover-app failed (${res.status})`}\n`);
67
- }
68
- process.exit(1);
69
- }
70
-
71
- const { app_id, name } = await res.json();
72
-
73
- // ── Write .suron.json ─────────────────────────────────────────────────────
74
- // Use the canonical name returned by the server (original registration casing).
75
- writeFileSync(
76
- join(cwd, ".suron.json"),
77
- JSON.stringify({ app: name, id: app_id, api_url: apiUrl }, null, 2) + "\n",
78
- "utf-8"
79
- );
80
-
81
- console.log();
82
- console.log(" " + c.green("✓") + " .suron.json restored");
83
- console.log(" " + c.dim(" app: ") + c.cyan(name));
84
- console.log(" " + c.dim(" id: ") + c.cyan(app_id));
85
- console.log();
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
- }
@@ -1,13 +0,0 @@
1
- import { Command } from "commander";
2
- import { requireApiUrl } from "../utils/config.js";
3
- import c from "../utils/colors.js";
4
-
5
- export const whoamiCommand = new Command("whoami")
6
- .description("Show configured Suron API URL")
7
- .action(() => {
8
- const apiUrl = requireApiUrl();
9
- console.log();
10
- console.log(" " + c.bold("API URL") + " " + c.cyan(apiUrl));
11
- console.log(" " + c.bold("Config file") + " ~/.suron-config");
12
- console.log();
13
- });