@suronai/cli 0.1.22 → 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 +75 -35
- package/package.json +1 -1
- package/src/commands/init.js +90 -44
- package/src/utils/dotenvx.js +3 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @suronai/cli
|
|
2
2
|
|
|
3
|
-
CLI for Suron —
|
|
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
|
-
|
|
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
|
|
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
|
|
27
|
-
3. Installs `@suronai/sdk` into your project
|
|
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.
|
|
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
|
-
#
|
|
60
|
+
# set a custom app name without the interactive prompt
|
|
37
61
|
suron init --name my-app
|
|
38
62
|
```
|
|
39
63
|
|
|
40
|
-
|
|
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
|
-
|
|
68
|
+
```
|
|
69
|
+
▶ Found dotenv in index.js
|
|
43
70
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
Will replace:
|
|
72
|
+
- import { config } from 'dotenv'
|
|
73
|
+
- config()
|
|
74
|
+
With:
|
|
75
|
+
+ import { config } from '@suronai/sdk'
|
|
76
|
+
+ await config()
|
|
48
77
|
|
|
49
|
-
|
|
50
|
-
import { config } from '@suronai/sdk'
|
|
51
|
-
await config()
|
|
78
|
+
Replace? [Y/n] ›
|
|
52
79
|
```
|
|
53
80
|
|
|
54
|
-
If dotenv isn't
|
|
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
|
|
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
|
|
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
|
-
##
|
|
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
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
##
|
|
132
|
+
## Related
|
|
88
133
|
|
|
89
|
-
|
|
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
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
// ──
|
|
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
|
|
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
|
-
*
|
|
150
|
-
*
|
|
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
|
-
*
|
|
157
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
const esmPattern = /^import\s+\{\s*config\s*\}\s+from\s+["']dotenv["'];?\s*\n?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
|
-
|
|
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 (
|
|
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
|
|
195
|
+
console.log(" " + c.yellow("▶") + " Found dotenv in " + c.dim(relEntry) + ":");
|
|
203
196
|
console.log();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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("
|
|
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
|
-
//
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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,
|
|
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);
|
package/src/utils/dotenvx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync
|
|
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
|
+
|