@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/package.json +23 -23
- package/src/commands.js +237 -0
- package/src/index.js +4 -7
- package/src/utils/api.js +20 -0
- package/src/utils/colors.js +179 -9
- package/src/utils/config.js +14 -34
- package/src/utils/crypto.js +7 -0
- package/README.md +0 -134
- package/src/commands/init.js +0 -394
- package/src/commands/login.js +0 -56
- package/src/commands/recover.js +0 -95
- package/src/commands/whoami.js +0 -13
- package/src/utils/dotenvx.js +0 -61
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
|
package/src/commands/init.js
DELETED
|
@@ -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
|
-
}
|
package/src/commands/login.js
DELETED
|
@@ -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
|
-
});
|
package/src/commands/recover.js
DELETED
|
@@ -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
|
-
}
|
package/src/commands/whoami.js
DELETED
|
@@ -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
|
-
});
|