@suronai/cli 0.1.23 → 0.1.24

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @suronai/cli
2
2
 
3
- CLI for Suron — configure your deployment and manage apps.
3
+ CLI for [Suron](https://suronai.com)encrypt your `.env`, register your app, and manage secret delivery gated by Telegram approval.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,87 +8,127 @@ CLI for Suron — configure your deployment and manage apps.
8
8
  npm install -g @suronai/cli
9
9
  ```
10
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
+
11
27
  ## Commands
12
28
 
13
29
  ### `suron login`
14
30
 
15
- Prompts for your Convex deployment URL, verifies the backend is reachable, and saves the URL to `~/.suron-config`.
31
+ Saves your Convex deployment URL to `~/.suron-config`. Run this once per machine.
16
32
 
17
33
  ```bash
18
34
  suron login
19
35
  ```
20
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
+
21
45
  ### `suron init`
22
46
 
23
- Run inside your project directory. Does everything needed to protect your app with Suron:
47
+ Run inside your project directory. Does the full setup in one command:
24
48
 
25
- 1. Encrypts `.env` in-place with dotenvx
26
- 2. Registers your app in Convex (stores the encrypted private key)
27
- 3. Installs `@suronai/sdk` into your project (detects npm / pnpm / yarn / bun)
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
28
52
  4. Writes `.suron.json` with your app name and ID
29
53
  5. Deletes `.env.keys`
30
- 6. Patches your entry point replaces `dotenv` with `@suronai/sdk` (interactive, asks before changing)
54
+ 6. Detects `dotenv` in your entry point and offers to replace it with `@suronai/sdk`
31
55
 
32
56
  ```bash
33
57
  cd my-project
34
58
  suron init
35
59
 
36
- # optional: set a custom app name
60
+ # set a custom app name without the interactive prompt
37
61
  suron init --name my-app
38
62
  ```
39
63
 
40
- After `suron init`, both `.env` and `.suron.json` are safe to commit.
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:
41
67
 
42
- **What the patched entry point looks like:**
68
+ ```
69
+ ▶ Found dotenv in index.js
43
70
 
44
- ```js
45
- // before
46
- import { config } from 'dotenv'
47
- config()
71
+ Will replace:
72
+ - import { config } from 'dotenv'
73
+ - config()
74
+ With:
75
+ + import { config } from '@suronai/sdk'
76
+ + await config()
48
77
 
49
- // after
50
- import { config } from '@suronai/sdk'
51
- await config()
78
+ Replace? [Y/n] ›
52
79
  ```
53
80
 
54
- If dotenv isn't detected, `suron init` prints the snippet to add manually.
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
+ ---
55
93
 
56
94
  ### `suron whoami`
57
95
 
58
- Prints the configured Convex API URL.
96
+ Prints the configured Suron API URL.
59
97
 
60
98
  ```bash
61
99
  suron whoami
62
100
  ```
63
101
 
102
+ ---
103
+
64
104
  ### `suron rotate`
65
105
 
66
- Re-encrypts `.env` with a new private key, sends the new key to Convex, deletes `.env.keys`.
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.
67
107
 
68
108
  ```bash
69
109
  suron rotate
70
110
  ```
71
111
 
72
- Use this if your private key may have been compromised, or as a routine rotation.
73
-
74
112
  ---
75
113
 
76
- ## Configuration
77
-
78
- The CLI reads `SURON_API_URL` from the environment first, then falls back to `~/.suron-config`.
114
+ ## How it works
79
115
 
80
- ```bash
81
- # Override without running suron login:
82
- export SURON_API_URL=https://your-deployment.convex.site
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
83
128
  ```
84
129
 
85
130
  ---
86
131
 
87
- ## Files
132
+ ## Related
88
133
 
89
- | File | Commit? | Notes |
90
- |---|---|---|
91
- | `.suron.json` | ✅ yes | app name + id + api_url, no secrets |
92
- | `.env` | ✅ yes | encrypted by dotenvx |
93
- | `.env.keys` | ⛔ no | deleted automatically by `suron init` |
94
- | `~/.suron-config` | ⛔ no | CLI config — lives on developer machine only |
134
+ - [`@suronai/sdk`](https://www.npmjs.com/package/@suronai/sdk) the runtime SDK your app imports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suronai/cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "CLI for Suron — suron login, init, whoami, rotate",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,7 @@ import { existsSync, writeFileSync, readFileSync } from "fs";
3
3
  import { join, basename } from "path";
4
4
  import { execSync } from "child_process";
5
5
  import { requireApiUrl, prompt } from "../utils/config.js";
6
- import { encryptDotenv, readPrivateKey, deleteKeysFile } from "../utils/dotenvx.js";
6
+ import { encryptDotenv, readPrivateKey } from "../utils/dotenvx.js";
7
7
  import c from "../utils/colors.js";
8
8
 
9
9
  export const initCommand = new Command("init")
@@ -78,9 +78,7 @@ export const initCommand = new Command("init")
78
78
 
79
79
  const { app_id } = await res.json();
80
80
 
81
- // ── Write .suron.json + clean up ──────────────────────────────────────────
82
- deleteKeysFile(cwd);
83
-
81
+ // ── Write .suron.json ─────────────────────────────────────────────────────
84
82
  writeFileSync(
85
83
  join(cwd, ".suron.json"),
86
84
  JSON.stringify({ app: appName, id: app_id, api_url: apiUrl }, null, 2) + "\n",
@@ -110,13 +108,13 @@ export const initCommand = new Command("init")
110
108
  }
111
109
  }
112
110
 
113
- // ── Auto-patch entry point ────────────────────────────────────────────────
111
+ // ── Patch entry point ─────────────────────────────────────────────────────
114
112
  await patchEntryPoint(cwd, isEsm);
115
113
 
116
114
  // ── Done ──────────────────────────────────────────────────────────────────
117
115
  console.log();
118
116
  console.log(" " + c.green("✓") + " .env encrypted " + c.dim("(safe to commit)"));
119
- console.log(" " + c.green("✓") + " .env.keys deleted");
117
+ console.log(" " + c.green("✓") + " .env.keys kept " + c.dim("(add to .gitignore)"));
120
118
  console.log(" " + c.green("✓") + " .suron.json written " + c.dim("(safe to commit)"));
121
119
  console.log(" " + c.green("✓") + " @suronai/sdk installed");
122
120
  console.log();
@@ -146,22 +144,17 @@ function sanitiseName(name) {
146
144
  }
147
145
 
148
146
  /**
149
- * Looks for a dotenv bootstrap in the project entry point and offers
150
- * to replace it with the Suron equivalent.
151
- *
152
- * Detects both ESM and CJS patterns:
153
- * ESM: import { config } from "dotenv"; config();
154
- * CJS: require("dotenv").config();
147
+ * Scans the entry point line-by-line for any lines that reference "dotenv".
148
+ * Shows the user exactly what was found and what it will be replaced with,
149
+ * then asks for confirmation before writing.
155
150
  *
156
- * Replaces with the correct form based on the project type:
157
- * ESM: import { config } from "@suronai/sdk"; await config();
158
- * CJS: const { config } = require("@suronai/sdk"); await config();
151
+ * Works for any import/require pattern because it does a literal line search
152
+ * rather than trying to anticipate every possible dotenv usage style.
159
153
  *
160
154
  * @param {string} cwd
161
155
  * @param {boolean} isEsm
162
156
  */
163
157
  async function patchEntryPoint(cwd, isEsm) {
164
- // Candidates in priority order
165
158
  const candidates = isEsm
166
159
  ? ["index.js", "index.mjs", "src/index.js", "src/index.mjs"]
167
160
  : ["index.js", "index.cjs", "src/index.js", "src/index.cjs"];
@@ -173,8 +166,7 @@ async function patchEntryPoint(cwd, isEsm) {
173
166
  }
174
167
 
175
168
  if (!entryPath) {
176
- // No entry point found — just print the manual snippet
177
- console.log(" Add to your app entry point:\n");
169
+ console.log("\n Add to your app entry point:\n");
178
170
  printSnippet(isEsm);
179
171
  return;
180
172
  }
@@ -182,38 +174,72 @@ async function patchEntryPoint(cwd, isEsm) {
182
174
  let src;
183
175
  try { src = readFileSync(entryPath, "utf-8"); } catch { return; }
184
176
 
185
- // Build regex patterns for both dotenv styles
186
- const esmPattern = /^import\s+\{\s*config\s*\}\s+from\s+["']dotenv["'];?[ \t]*(?:\r?\n[ \t]*)*config\(\);?/m;
187
- const cjsPattern = /^(?:const\s+)?require\(["']dotenv["']\)\.config\(\);?/m;
188
- const pattern = isEsm ? esmPattern : cjsPattern;
177
+ const lines = src.split("\n");
189
178
 
190
- const match = src.match(pattern);
179
+ // Find every line that references dotenv (import or require, any style)
180
+ const dotenvLines = [];
181
+ for (let i = 0; i < lines.length; i++) {
182
+ if (lines[i].includes("dotenv")) {
183
+ dotenvLines.push({ index: i, content: lines[i] });
184
+ }
185
+ }
191
186
 
192
- if (!match) {
193
- // dotenv not found — print manual snippet
187
+ if (dotenvLines.length === 0) {
194
188
  console.log("\n Add to your app entry point:\n");
195
189
  printSnippet(isEsm);
196
190
  return;
197
191
  }
198
192
 
199
- // dotenv detected — ask user
200
193
  const relEntry = entryPath.replace(cwd + "\\", "").replace(cwd + "/", "");
201
194
  console.log();
202
- console.log(" " + c.yellow("▶") + " Found " + c.cyan("dotenv") + " in " + c.dim(relEntry));
195
+ console.log(" " + c.yellow("▶") + " Found dotenv in " + c.dim(relEntry) + ":");
203
196
  console.log();
204
- console.log(" " + c.dim("Will replace:"));
205
- console.log(" " + c.red("-") + " " + c.dim(match[0].replace(/\n/g, "\n - ")));
206
- console.log(" " + c.dim("With:"));
207
- if (isEsm) {
208
- console.log(" " + c.green("+") + " " + c.cyan("import") + " { config } from " + c.green("'@suronai/sdk'"));
209
- console.log(" " + c.green("+") + " " + c.cyan("await") + " config()");
210
- } else {
211
- console.log(" " + c.green("+") + " const { config } = " + c.cyan("require") + "(" + c.green("'@suronai/sdk'") + ")");
212
- console.log(" " + c.green("+") + " " + c.cyan("await") + " config()");
197
+
198
+ // For each dotenv line, compute its replacement.
199
+ // We preserve the original line's leading whitespace (indent) and only
200
+ // swap out the dotenv-specific part — nothing else on the line is touched.
201
+ const replacements = dotenvLines.map(({ index, content }) => {
202
+ const trimmed = content.trim();
203
+ const indent = content.match(/^(\s*)/)[1];
204
+ let replacement = null;
205
+
206
+ if (isEsm) {
207
+ // import { config } from "dotenv" → import { config } from "@suronai/sdk"
208
+ // Preserves the quote style and anything else on the line.
209
+ if (trimmed.startsWith("import") && trimmed.includes("dotenv")) {
210
+ replacement = indent + trimmed.replace(/(from\s+)['"]dotenv['"]/, '$1"@suronai/sdk"');
211
+ }
212
+ // config(); → await config();
213
+ // Only matches lines that are purely a config() call, nothing else.
214
+ else if (/^config\s*\(\s*\);?$/.test(trimmed)) {
215
+ replacement = indent + trimmed.replace(/^config\s*\(/, "await config(");
216
+ }
217
+ } else {
218
+ // require("dotenv").config() → const { config } = require("@suronai/sdk"); await config();
219
+ if (trimmed.includes("require") && trimmed.includes("dotenv")) {
220
+ replacement = indent + "const { config } = require(\"@suronai/sdk\");\n" + indent + "await config();";
221
+ }
222
+ }
223
+
224
+ // If we found a dotenv line but don't know how to replace it, show it
225
+ // with a warning so the user knows to handle it manually.
226
+ return { index, content, replacement };
227
+ });
228
+
229
+ // Show the diff preview
230
+ for (const { content, replacement } of replacements) {
231
+ console.log(" " + c.red("-") + " " + c.dim(content.trim()));
232
+ if (replacement !== null) {
233
+ for (const l of replacement.split("\n")) {
234
+ console.log(" " + c.green("+") + " " + c.cyan(l.trim()));
235
+ }
236
+ } else {
237
+ console.log(" " + c.yellow("?") + " " + c.dim("(no automatic replacement — update manually)"));
238
+ }
239
+ console.log();
213
240
  }
214
- console.log();
215
241
 
216
- const answer = await prompt(" Replace? [" + c.green("Y") + "/n] › ");
242
+ const answer = await prompt(" Apply replacements? [" + c.green("Y") + "/n] › ");
217
243
  const confirmed = answer === "" || /^y(es)?$/i.test(answer);
218
244
 
219
245
  if (!confirmed) {
@@ -222,15 +248,35 @@ async function patchEntryPoint(cwd, isEsm) {
222
248
  return;
223
249
  }
224
250
 
225
- // Perform the replacement
226
- const replacement = isEsm
227
- ? `import { config } from '@suronai/sdk';\nawait config();`
228
- : `const { config } = require('@suronai/sdk');\nawait config();`;
251
+ // Apply — walk lines, replace only the matched dotenv lines, touch nothing else
252
+ const indexMap = new Map(replacements.map(r => [r.index, r]));
253
+ const outLines = lines.map((line, i) => {
254
+ const r = indexMap.get(i);
255
+ return (r && r.replacement !== null) ? r.replacement : line;
256
+ });
257
+
258
+ // For ESM: if await config() isn't already right after the imports, move it there
259
+ // automatically as part of the same patch — no second prompt needed.
260
+ if (isEsm) {
261
+ const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(\s*\);?\s*$/.test(l));
262
+ const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
263
+
264
+ if (callLineIndex !== -1 && callLineIndex !== lastImportIndex + 1 && callLineIndex !== lastImportIndex + 2) {
265
+ const callLine = outLines[callLineIndex];
266
+ // Remove from current position (and any adjacent blank line below it)
267
+ outLines.splice(callLineIndex, 1);
268
+ if (outLines[callLineIndex]?.trim() === "") outLines.splice(callLineIndex, 1);
229
269
 
230
- const patched = src.replace(pattern, replacement);
270
+ // Re-find last import index after splice (indices shifted)
271
+ const newLastImport = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
272
+
273
+ // Insert: blank line + await config() + blank line, after last import
274
+ outLines.splice(newLastImport + 1, 0, "", callLine, "");
275
+ }
276
+ }
231
277
 
232
278
  try {
233
- writeFileSync(entryPath, patched, "utf-8");
279
+ writeFileSync(entryPath, outLines.join("\n"), "utf-8");
234
280
  console.log(" " + c.green("✓") + " " + c.dim(relEntry) + " patched");
235
281
  } catch (err) {
236
282
  console.error(" " + c.red("✗") + " Could not write " + relEntry + ": " + err.message);
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { existsSync, readFileSync, unlinkSync } from "fs";
2
+ import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
4
 
5
5
  /**
@@ -23,7 +23,7 @@ export function encryptDotenv(cwd) {
23
23
  }
24
24
 
25
25
  try {
26
- const output = execSync("npx @dotenvx/dotenvx encrypt", {
26
+ const output = execSync("npx @dotenvx/dotenvx encrypt --force", {
27
27
  cwd,
28
28
  env,
29
29
  stdio: ["inherit", "pipe", "pipe"],
@@ -58,11 +58,4 @@ export function readPrivateKey(cwd) {
58
58
  return match[1].trim();
59
59
  }
60
60
 
61
- /**
62
- * Deletes .env.keys after the private key has been stored in Convex.
63
- * @param {string} cwd
64
- */
65
- export function deleteKeysFile(cwd) {
66
- const keysPath = join(cwd, ".env.keys");
67
- if (existsSync(keysPath)) unlinkSync(keysPath);
68
- }
61
+