@suronai/cli 0.1.36 → 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 CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "name": "@suronai/cli",
3
- "version": "0.1.36",
4
- "description": "CLI for Suron suron login, init, whoami, recover",
5
- "type": "module",
6
- "bin": {
7
- "suron": "./src/index.js"
8
- },
9
- "files": [
10
- "src"
11
- ],
12
- "scripts": {
13
- "build": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/whoami.js src/commands/recover.js src/utils/config.js src/utils/dotenvx.js src/utils/colors.js",
14
- "lint": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/whoami.js src/commands/recover.js src/utils/config.js src/utils/dotenvx.js src/utils/colors.js"
15
- },
16
- "dependencies": {
17
- "@dotenvx/dotenvx": "latest",
18
- "commander": "latest"
19
- },
20
- "engines": {
21
- "node": ">=18.0.0"
22
- }
23
- }
1
+ {
2
+ "name": "@suronai/cli",
3
+ "version": "0.1.37",
4
+ "description": "Suron CLI — secrets delivery",
5
+ "type": "module",
6
+ "bin": {
7
+ "suron": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "start": "bun src/index.js",
14
+ "dev": "bun --watch src/index.js"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^1.3.0",
18
+ "commander": "^14.0.3"
19
+ },
20
+ "engines": {
21
+ "bun": ">=1.0.0"
22
+ }
23
+ }
@@ -0,0 +1,237 @@
1
+ import { Command } from "commander";
2
+ import { intro, outro, spinner, select, text, isCancel } from "@clack/prompts";
3
+ import { existsSync, readFileSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { exec } from "child_process";
6
+ import ui from "./utils/colors.js";
7
+ import { getToken, requireToken, saveConfig, clearToken } from "./utils/config.js";
8
+ import { api, BASE_URL } from "./utils/api.js";
9
+
10
+ function cancel(msg) {
11
+ console.log(ui.blank());
12
+ console.log(ui.errBlock(msg));
13
+ console.log(ui.blank());
14
+ process.exit(1);
15
+ }
16
+
17
+ export const loginCommand = new Command("login")
18
+ .description("Authenticate via browser")
19
+ .option("--force", "Re-authenticate even if already logged in")
20
+ .action(async (opts) => {
21
+ const existing = getToken();
22
+ if (existing && !opts.force) {
23
+ console.log(ui.blank());
24
+ console.log(ui.errBlock("already logged in", "use --force to re-authenticate"));
25
+ console.log(ui.blank());
26
+ process.exit(1);
27
+ }
28
+
29
+ console.log(ui.logo());
30
+ console.log(ui.blank());
31
+ console.log(ui.hr());
32
+ console.log(ui.blank());
33
+
34
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
35
+ let code = "";
36
+ for (let i = 0; i < 6; i++) code += chars[Math.floor(Math.random() * chars.length)];
37
+
38
+ await api("/cli/auth-code", {
39
+ method: "POST",
40
+ body: JSON.stringify({ code }),
41
+ });
42
+
43
+ const loginUrl = `${BASE_URL}/clilogin?code=${code}`;
44
+
45
+ console.log(ui.infoBlock("Opening browser to complete login..."));
46
+ console.log(ui.blank());
47
+ console.log(ui.kv("URL", loginUrl, 6));
48
+ console.log(ui.blank());
49
+
50
+ exec(`open "${loginUrl}" 2>/dev/null || xdg-open "${loginUrl}" 2>/dev/null || cmd /c start "" "${loginUrl}" 2>/dev/null`);
51
+
52
+ const s = spinner();
53
+ s.start("Waiting for approval");
54
+
55
+ const deadline = Date.now() + 300_000;
56
+ let token = null;
57
+
58
+ while (Date.now() < deadline) {
59
+ await new Promise(r => setTimeout(r, 2000));
60
+ try {
61
+ const result = await api(`/cli/auth-code/status?code=${code}`);
62
+ if (result.status === "approved" && result.token) {
63
+ token = result.token;
64
+ break;
65
+ }
66
+ } catch {}
67
+ }
68
+
69
+ s.stop(token ? "Approved" : "Timed out");
70
+
71
+ if (!token) {
72
+ console.log(ui.blank());
73
+ console.log(ui.errBlock("login timed out", "try again"));
74
+ process.exit(1);
75
+ }
76
+
77
+ saveConfig({ token });
78
+
79
+ console.log(ui.blank());
80
+ console.log(ui.step("done", "logged in"));
81
+ console.log(ui.step("done", "saved ~/.suron-config"));
82
+ console.log(ui.blank());
83
+ console.log(ui.hr());
84
+ console.log(ui.blank());
85
+ console.log(ui.infoBlock("next › cd your-project && " + ui.code("suron init")));
86
+ console.log(ui.blank());
87
+ });
88
+
89
+ export const logoutCommand = new Command("logout")
90
+ .description("Clear local session token")
91
+ .action(async () => {
92
+ clearToken();
93
+ try {
94
+ await api("/auth/logout", { method: "POST" });
95
+ } catch {}
96
+ console.log(ui.blank());
97
+ console.log(ui.okBlock("logged out"));
98
+ console.log(ui.blank());
99
+ });
100
+
101
+ export const initCommand = new Command("init")
102
+ .description("Initialize Suron in this project")
103
+ .action(async () => {
104
+ const token = requireToken();
105
+
106
+ if (existsSync(".suron.json")) {
107
+ console.log(ui.blank());
108
+ console.log(ui.errBlock(".suron.json already exists", "use suron up to push a new version"));
109
+ console.log(ui.blank());
110
+ process.exit(1);
111
+ }
112
+
113
+ if (!existsSync(".env")) {
114
+ console.log(ui.blank());
115
+ console.log(ui.errBlock(".env not found", "create a .env file first"));
116
+ console.log(ui.blank());
117
+ process.exit(1);
118
+ }
119
+
120
+ console.log(ui.logo());
121
+ console.log(ui.blank());
122
+ console.log(ui.hr());
123
+ console.log(ui.blank());
124
+
125
+ const mode = await select({
126
+ message: "How do you want to link this project?",
127
+ options: [
128
+ { value: "new", label: "Create new app" },
129
+ { value: "existing", label: "Link to existing app" },
130
+ ],
131
+ });
132
+
133
+ if (isCancel(mode)) cancel("cancelled");
134
+
135
+ let app_id, app_name;
136
+
137
+ if (mode === "new") {
138
+ const name = await text({
139
+ message: "App name (alphanumeric)",
140
+ validate: v => /^[a-zA-Z0-9]+$/.test(v) ? undefined : "Alphanumeric only",
141
+ });
142
+ if (isCancel(name)) cancel("cancelled");
143
+
144
+ const result = await api("/apps", {
145
+ method: "POST",
146
+ body: JSON.stringify({ name }),
147
+ }).catch(err => cancel(err.message));
148
+
149
+ app_id = result.app_id;
150
+ app_name = result.name;
151
+ } else {
152
+ const apps = await api("/apps").catch(err => cancel(err.message));
153
+
154
+ if (!apps.length) {
155
+ console.log(ui.blank());
156
+ console.log(ui.errBlock("no apps found", "use 'Create new app' instead"));
157
+ process.exit(1);
158
+ }
159
+
160
+ const chosen = await select({
161
+ message: "Select an app",
162
+ options: apps.map(a => ({ value: a.app_id, label: a.name })),
163
+ });
164
+ if (isCancel(chosen)) cancel("cancelled");
165
+
166
+ const found = apps.find(a => a.app_id === chosen);
167
+ app_id = found.app_id;
168
+ app_name = found.name;
169
+ }
170
+
171
+ const env_plaintext = readFileSync(".env", "utf-8");
172
+
173
+ const s = spinner();
174
+ s.start("Pushing v1");
175
+
176
+ const result = await api(`/apps/${app_id}/versions`, {
177
+ method: "POST",
178
+ body: JSON.stringify({ env_plaintext }),
179
+ }).catch(err => { s.stop("failed"); cancel(err.message); });
180
+
181
+ s.stop("Done");
182
+
183
+ const suronJson = { app_name, app_id, version: result.version };
184
+ writeFileSync(".suron.json", JSON.stringify(suronJson, null, 2) + "\n");
185
+
186
+ console.log(ui.blank());
187
+ console.log(ui.step("done", "encrypted and uploaded .env"));
188
+ console.log(ui.step("done", `version ${result.version}`));
189
+ console.log(ui.step("done", "wrote .suron.json"));
190
+ console.log(ui.blank());
191
+ console.log(ui.hr());
192
+ console.log(ui.blank());
193
+ console.log(ui.infoBlock("add .suron.json to git · add .env to .gitignore"));
194
+ console.log(ui.blank());
195
+ });
196
+
197
+ export const upCommand = new Command("up")
198
+ .description("Push a new version of .env")
199
+ .action(async () => {
200
+ requireToken();
201
+
202
+ if (!existsSync(".suron.json")) {
203
+ console.log(ui.blank());
204
+ console.log(ui.errBlock(".suron.json not found", "run: suron init"));
205
+ console.log(ui.blank());
206
+ process.exit(1);
207
+ }
208
+
209
+ if (!existsSync(".env")) {
210
+ console.log(ui.blank());
211
+ console.log(ui.errBlock(".env not found"));
212
+ console.log(ui.blank());
213
+ process.exit(1);
214
+ }
215
+
216
+ const suronJson = JSON.parse(readFileSync(".suron.json", "utf-8"));
217
+ const { app_id, app_name } = suronJson;
218
+
219
+ const env_plaintext = readFileSync(".env", "utf-8");
220
+
221
+ const s = spinner();
222
+ s.start("Pushing new version");
223
+
224
+ const result = await api(`/apps/${app_id}/versions`, {
225
+ method: "POST",
226
+ body: JSON.stringify({ env_plaintext }),
227
+ }).catch(err => { s.stop("failed"); cancel(err.message); });
228
+
229
+ s.stop("Done");
230
+
231
+ suronJson.version = result.version;
232
+ writeFileSync(".suron.json", JSON.stringify(suronJson, null, 2) + "\n");
233
+
234
+ console.log(ui.blank());
235
+ console.log(ui.step("done", `version ${result.version} pushed`));
236
+ console.log(ui.blank());
237
+ });
package/src/index.js CHANGED
@@ -2,13 +2,9 @@
2
2
 
3
3
  import { Command } from "commander";
4
4
  import { createRequire } from "module";
5
- import { loginCommand } from "./commands/login.js";
6
- import { initCommand } from "./commands/init.js";
7
- import { encryptCommand } from "./commands/encrypt.js";
8
- import { whoamiCommand } from "./commands/whoami.js";
9
- import { recoverCommand } from "./commands/recover.js";
5
+ import { loginCommand, logoutCommand, initCommand, upCommand } from "./commands.js";
10
6
 
11
- const require = createRequire(import.meta.url);
7
+ const require = createRequire(import.meta.url);
12
8
  const { version } = require("../package.json");
13
9
 
14
10
  const program = new Command();
@@ -19,9 +15,8 @@ program
19
15
  .version(version);
20
16
 
21
17
  program.addCommand(loginCommand);
18
+ program.addCommand(logoutCommand);
22
19
  program.addCommand(initCommand);
23
- program.addCommand(encryptCommand);
24
- program.addCommand(whoamiCommand);
25
- program.addCommand(recoverCommand);
20
+ program.addCommand(upCommand);
26
21
 
27
22
  program.parse(process.argv);
@@ -0,0 +1,20 @@
1
+ import { getToken } from "./config.js";
2
+
3
+ const BASE_URL = "https://suronai.com";
4
+
5
+ export async function api(path, options = {}) {
6
+ const token = getToken();
7
+ const headers = { "Content-Type": "application/json", ...options.headers };
8
+ if (token) headers["Authorization"] = `Bearer ${token}`;
9
+
10
+ const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
11
+
12
+ if (!res.ok) {
13
+ const data = await res.json().catch(() => ({}));
14
+ throw new Error(data.error ?? `HTTP ${res.status}`);
15
+ }
16
+
17
+ return res.json();
18
+ }
19
+
20
+ export { BASE_URL };
@@ -1,14 +1,10 @@
1
1
  import { homedir } from "os";
2
2
  import { join } from "path";
3
3
  import { readFileSync, writeFileSync, existsSync } from "fs";
4
- import readline from "readline";
5
4
  import ui from "./colors.js";
6
5
 
7
6
  const CONFIG_PATH = join(homedir(), ".suron-config");
8
7
 
9
- /**
10
- * @returns {{ apiUrl?: string }}
11
- */
12
8
  export function readConfig() {
13
9
  if (!existsSync(CONFIG_PATH)) return {};
14
10
  try {
@@ -19,7 +15,7 @@ export function readConfig() {
19
15
  const key = line.slice(0, eq).trim();
20
16
  const val = line.slice(eq + 1).trim();
21
17
  if (!val) continue;
22
- if (key === "SURON_API_URL") config.apiUrl = val;
18
+ if (key === "SURON_TOKEN") config.token = val;
23
19
  }
24
20
  return config;
25
21
  } catch {
@@ -27,46 +23,28 @@ export function readConfig() {
27
23
  }
28
24
  }
29
25
 
30
- /**
31
- * @param {{ apiUrl?: string }} values
32
- */
33
26
  export function saveConfig(values) {
34
27
  const merged = { ...readConfig(), ...values };
35
28
  const lines = [];
36
- if (merged.apiUrl) lines.push(`SURON_API_URL=${merged.apiUrl}`);
29
+ if (merged.token) lines.push(`SURON_TOKEN=${merged.token}`);
37
30
  writeFileSync(CONFIG_PATH, lines.join("\n") + "\n", { mode: 0o600 });
38
31
  }
39
32
 
40
- /** @returns {string | null} */
41
- export function getApiUrl() {
42
- // Strip trailing slash from both sources so callers always get a clean URL.
43
- const fromEnv = process.env.SURON_API_URL?.replace(/\/$/, "");
44
- const fromConfig = readConfig().apiUrl?.replace(/\/$/, "");
45
- return fromEnv ?? fromConfig ?? null;
33
+ export function getToken() {
34
+ return readConfig().token ?? null;
46
35
  }
47
36
 
48
- /** @returns {string} */
49
- export function requireApiUrl() {
50
- const url = getApiUrl();
51
- if (!url) {
37
+ export function requireToken() {
38
+ const token = getToken();
39
+ if (!token) {
52
40
  console.error(ui.blank());
53
- console.error(ui.errBlock("not configured", "run: suron login"));
41
+ console.error(ui.errBlock("not logged in", "run: suron login"));
54
42
  console.error(ui.blank());
55
43
  process.exit(1);
56
44
  }
57
- return url;
45
+ return token;
58
46
  }
59
47
 
60
- /**
61
- * @param {string} question
62
- * @returns {Promise<string>}
63
- */
64
- export function prompt(question) {
65
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
66
- return new Promise((resolve) => {
67
- rl.question(question, (answer) => {
68
- rl.close();
69
- resolve(answer.trim());
70
- });
71
- });
48
+ export function clearToken() {
49
+ writeFileSync(CONFIG_PATH, "", { mode: 0o600 });
72
50
  }
@@ -0,0 +1,7 @@
1
+ export function encryptEnv(plaintext) {
2
+ return plaintext;
3
+ }
4
+
5
+ export function decryptEnv(plaintext) {
6
+ return plaintext;
7
+ }
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,88 +0,0 @@
1
- import { Command } from "commander";
2
- import { existsSync, readFileSync } from "fs";
3
- import { join } from "path";
4
- import { encryptDotenv } from "../utils/dotenvx.js";
5
- import ui from "../utils/colors.js";
6
-
7
- export const encryptCommand = new Command("encrypt")
8
- .description("Encrypt new plaintext values added to .env (safe to run any time after init)")
9
- .action(async () => {
10
- const cwd = process.cwd();
11
-
12
- console.log(ui.blank());
13
- console.log(ui.hr());
14
- console.log(ui.blank());
15
-
16
- if (!existsSync(join(cwd, ".suron.json"))) {
17
- console.error(ui.errBlock(
18
- ".suron.json not found",
19
- "run suron init first"
20
- ));
21
- console.log(ui.blank());
22
- process.exit(1);
23
- }
24
-
25
- if (!existsSync(join(cwd, ".env"))) {
26
- console.error(ui.errBlock(".env not found"));
27
- console.log(ui.blank());
28
- process.exit(1);
29
- }
30
-
31
- // ── Scan for plaintext values ─────────────────────────────────────────────
32
- const envLines = readFileSync(join(cwd, ".env"), "utf-8").split("\n");
33
-
34
- const plaintextKeys = envLines
35
- .filter(line => {
36
- const t = line.trim();
37
- if (!t || t.startsWith("#") || t.startsWith("#/")) return false;
38
- const eq = t.indexOf("=");
39
- if (eq === -1) return false;
40
- const val = t.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
41
- return val.length > 0 && !val.startsWith("encrypted:");
42
- })
43
- .map(line => line.slice(0, line.indexOf("=")).trim());
44
-
45
- if (plaintextKeys.length === 0) {
46
- console.log(ui.step("skip", "all values already encrypted"));
47
- console.log(ui.blank());
48
- console.log(ui.hr());
49
- console.log(ui.blank());
50
- console.log(ui.infoBlock("to add secrets › edit .env then run " + ui.code("suron encrypt")));
51
- console.log(ui.blank());
52
- process.exit(0);
53
- }
54
-
55
- console.log(ui.kv("PENDING", String(plaintextKeys.length) + " value" + (plaintextKeys.length === 1 ? "" : "s")));
56
- console.log(ui.blank());
57
- for (const k of plaintextKeys) {
58
- console.log(ui.INDENT + " " + ui.slate("· ") + ui.amber(k));
59
- }
60
- console.log(ui.blank());
61
- console.log(ui.hr());
62
- console.log(ui.blank());
63
-
64
- // ── Encrypt ───────────────────────────────────────────────────────────────
65
- process.stdout.write(ui.INDENT + ui.slate("encrypting ···"));
66
-
67
- try {
68
- encryptDotenv(cwd);
69
- } catch (err) {
70
- process.stdout.write("\r" + " ".repeat(40) + "\r");
71
- console.error(ui.errBlock(err.message));
72
- console.log(ui.blank());
73
- process.exit(1);
74
- }
75
-
76
- process.stdout.write("\r" + " ".repeat(40) + "\r");
77
-
78
- console.log(ui.step("done", "encrypted .env"));
79
- console.log(ui.step("done", "cleaned .env + .env.keys"));
80
- console.log(ui.blank());
81
- console.log(ui.hr());
82
- console.log(ui.blank());
83
- console.log(ui.readyBlock([
84
- { label: ".env", note: "encrypted · safe to commit" },
85
- { label: ".env.keys", note: "keep safe · never commit" },
86
- ]));
87
- console.log(ui.blank());
88
- });
@@ -1,397 +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 ui from "../utils/colors.js";
8
-
9
- export const initCommand = new Command("init")
10
- .description("Encrypt .env and register this app with Suron")
11
- .option("--name <n>", "App name (skips interactive prompt)")
12
- .action(async (opts) => {
13
- const cwd = process.cwd();
14
- const apiUrl = requireApiUrl();
15
-
16
- console.log(ui.logo());
17
- console.log(ui.blank());
18
- console.log(ui.hr());
19
- console.log(ui.blank());
20
-
21
- if (existsSync(join(cwd, ".suron.json"))) {
22
- console.error(ui.errBlock(
23
- "already initialised",
24
- "to restore a lost config › suron recover"
25
- ));
26
- console.log(ui.blank());
27
- process.exit(1);
28
- }
29
-
30
- if (!existsSync(join(cwd, ".env"))) {
31
- console.error(ui.errBlock(
32
- ".env not found",
33
- "create a .env file with your secrets, then run: suron init"
34
- ));
35
- console.log(ui.blank());
36
- process.exit(1);
37
- }
38
-
39
- // ── App name ──────────────────────────────────────────────────────────────
40
- const suggested = sanitiseName(basename(cwd) || "myapp");
41
- let appName;
42
-
43
- if (opts.name) {
44
- appName = sanitiseName(opts.name);
45
- } else {
46
- process.stdout.write(ui.promptLine("app name [" + suggested + "]"));
47
- const raw = await prompt("");
48
- appName = sanitiseName(raw || suggested);
49
- }
50
-
51
- if (!appName) {
52
- console.log(ui.blank());
53
- console.error(ui.errBlock(
54
- "app name required",
55
- "example › suron init --name MyApp"
56
- ));
57
- console.log(ui.blank());
58
- process.exit(1);
59
- }
60
-
61
- console.log(ui.blank());
62
- console.log(ui.kv("APP", appName));
63
- console.log(ui.kv("DIR", ui.slate(cwd)));
64
- console.log(ui.blank());
65
- console.log(ui.hr());
66
- console.log(ui.blank());
67
-
68
- // ── Steps ─────────────────────────────────────────────────────────────────
69
- const steps = {
70
- encrypt: { label: "encrypt .env", status: "pending" },
71
- keys: { label: "key .env.keys", status: "pending" },
72
- gitignore: { label: "gitignore .gitignore", status: "pending" },
73
- register: { label: "register " + appName, status: "pending" },
74
- sdk: { label: "sdk @suronai/sdk", status: "pending" },
75
- };
76
-
77
- const printSteps = () => {
78
- for (const s of Object.values(steps)) {
79
- console.log(ui.step(s.status, s.label, s.note));
80
- }
81
- };
82
-
83
- // ── Encrypt ───────────────────────────────────────────────────────────────
84
- steps.encrypt.status = "run";
85
- steps.keys.status = "run";
86
- try {
87
- encryptDotenv(cwd);
88
- steps.encrypt.status = "done";
89
- steps.keys.status = "done";
90
- } catch (err) {
91
- steps.encrypt.status = "fail";
92
- steps.keys.status = "fail";
93
- printSteps();
94
- console.log(ui.blank());
95
- console.log(ui.hr());
96
- console.log(ui.blank());
97
- console.error(ui.errBlock(err.message));
98
- console.log(ui.blank());
99
- process.exit(1);
100
- }
101
-
102
- let privateKey;
103
- try {
104
- privateKey = readPrivateKey(cwd);
105
- } catch (err) {
106
- steps.keys.status = "fail";
107
- printSteps();
108
- console.log(ui.blank());
109
- console.log(ui.hr());
110
- console.log(ui.blank());
111
- console.error(ui.errBlock(err.message));
112
- console.log(ui.blank());
113
- process.exit(1);
114
- }
115
-
116
- // ── .gitignore ────────────────────────────────────────────────────────────
117
- steps.gitignore.status = "run";
118
- const gitignoreResult = ensureGitignore(cwd);
119
- steps.gitignore.status = "done";
120
- steps.gitignore.note = gitignoreResult;
121
-
122
- // ── Register ──────────────────────────────────────────────────────────────
123
- steps.register.status = "run";
124
- let res;
125
- try {
126
- res = await fetch(`${apiUrl}/cli/register-app`, {
127
- method: "POST",
128
- headers: { "Content-Type": "application/json" },
129
- body: JSON.stringify({ name: appName, private_key: privateKey }),
130
- });
131
- } catch (err) {
132
- steps.register.status = "fail";
133
- printSteps();
134
- console.log(ui.blank());
135
- console.log(ui.hr());
136
- console.log(ui.blank());
137
- console.error(ui.errBlock(`cannot reach API`, err.message));
138
- console.log(ui.blank());
139
- process.exit(1);
140
- }
141
-
142
- if (!res.ok) {
143
- let body = {};
144
- try { body = await res.json(); } catch { /* ignore */ }
145
- steps.register.status = "fail";
146
- printSteps();
147
- console.log(ui.blank());
148
- console.log(ui.hr());
149
- console.log(ui.blank());
150
- if (res.status === 409) {
151
- const canonical = body?.existing_name ?? appName;
152
- console.error(ui.errBlock(
153
- `"${canonical}" already registered`,
154
- `to restore a lost config › suron recover --name ${canonical}`
155
- ));
156
- } else {
157
- console.error(ui.errBlock(body?.error ?? `register failed (${res.status})`));
158
- }
159
- console.log(ui.blank());
160
- process.exit(1);
161
- }
162
-
163
- const { app_id } = await res.json();
164
- steps.register.status = "done";
165
-
166
- // ── Write .suron.json ─────────────────────────────────────────────────────
167
- writeFileSync(
168
- join(cwd, ".suron.json"),
169
- JSON.stringify({ app: appName, id: app_id, api_url: apiUrl }, null, 2) + "\n",
170
- "utf-8"
171
- );
172
-
173
- // ── Install SDK ───────────────────────────────────────────────────────────
174
- const pkgJsonPath = join(cwd, "package.json");
175
- let isEsm = false;
176
- steps.sdk.status = "run";
177
-
178
- if (existsSync(pkgJsonPath)) {
179
- let alreadyInstalled = false;
180
- try {
181
- const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
182
- alreadyInstalled = !!(pkg.dependencies?.["@suronai/sdk"] || pkg.devDependencies?.["@suronai/sdk"]);
183
- isEsm = pkg.type === "module";
184
- } catch { /* ignore */ }
185
-
186
- if (!alreadyInstalled) {
187
- const pm = detectPackageManager(cwd);
188
- try {
189
- execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "pipe" });
190
- steps.sdk.status = "done";
191
- } catch {
192
- steps.sdk.status = "fail";
193
- steps.sdk.note = "run: npm install @suronai/sdk";
194
- }
195
- } else {
196
- steps.sdk.status = "skip";
197
- steps.sdk.note = "already installed";
198
- }
199
- } else {
200
- steps.sdk.status = "skip";
201
- steps.sdk.note = "no package.json";
202
- }
203
-
204
- printSteps();
205
- console.log(ui.blank());
206
- console.log(ui.hr());
207
- console.log(ui.blank());
208
-
209
- // ── Patch entry point ─────────────────────────────────────────────────────
210
- await patchEntryPoint(cwd, isEsm);
211
-
212
- // ── Done ──────────────────────────────────────────────────────────────────
213
- console.log(ui.readyBlock([
214
- { label: ".env", note: "encrypted · safe to commit" },
215
- { label: ".env.keys", note: "gitignored · keep safe" },
216
- { label: ".suron.json", note: "safe to commit" },
217
- ]));
218
- console.log(ui.blank());
219
- });
220
-
221
- // ── Helpers ──────────────────────────────────────────────────────────────────
222
-
223
- const GITIGNORE_ENTRIES = ["node_modules/", "package-lock.json", ".env.keys"];
224
-
225
- function ensureGitignore(cwd) {
226
- const gitignorePath = join(cwd, ".gitignore");
227
- if (!existsSync(gitignorePath)) {
228
- writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
229
- return "created";
230
- }
231
- const content = readFileSync(gitignorePath, "utf-8");
232
- const existing = new Set(content.split("\n").map(l => l.trim()));
233
- const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
234
- if (missing.length === 0) return "ok";
235
- const sep = content.endsWith("\n") ? "" : "\n";
236
- writeFileSync(gitignorePath, content + sep + missing.join("\n") + "\n", "utf-8");
237
- return "updated";
238
- }
239
-
240
- function detectPackageManager(cwd) {
241
- if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
242
- if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
243
- if (existsSync(join(cwd, "bun.lockb"))) return "bun";
244
- return "npm";
245
- }
246
-
247
- function pmAddCmd(pm) {
248
- return pm === "npm" ? "install" : "add";
249
- }
250
-
251
- /**
252
- * Strips everything except letters and digits while preserving case.
253
- * @param {string} name
254
- * @returns {string}
255
- */
256
- function sanitiseName(name) {
257
- return name.replace(/[^a-zA-Z0-9]/g, "");
258
- }
259
-
260
- /**
261
- * @param {string} cwd
262
- * @param {boolean} isEsm
263
- */
264
- async function patchEntryPoint(cwd, isEsm) {
265
- const candidates = isEsm
266
- ? ["index.js", "index.mjs", "src/index.js", "src/index.mjs"]
267
- : ["index.js", "index.cjs", "src/index.js", "src/index.cjs"];
268
-
269
- let entryPath = null;
270
- for (const rel of candidates) {
271
- const abs = join(cwd, rel);
272
- if (existsSync(abs)) { entryPath = abs; break; }
273
- }
274
-
275
- if (!entryPath) {
276
- printSnippet(isEsm);
277
- return;
278
- }
279
-
280
- let src;
281
- try { src = readFileSync(entryPath, "utf-8"); } catch { return; }
282
-
283
- const lines = src.split("\n");
284
- const toReplace = [];
285
- const seenIndices = new Set();
286
-
287
- for (let i = 0; i < lines.length; i++) {
288
- const trimmed = lines[i].trim();
289
- const indent = lines[i].match(/^(\s*)/)[1];
290
-
291
- if (lines[i].includes("dotenv")) {
292
- seenIndices.add(i);
293
- let replacement = null;
294
- if (isEsm) {
295
- if (trimmed.startsWith("import")) {
296
- replacement = indent + trimmed.replace(/(from\s+)['"]dotenv(?:\/config)?['"]/, '$1"@suronai/sdk"');
297
- }
298
- } else {
299
- if (trimmed.includes("require")) {
300
- replacement = indent + "const { config } = require(\"@suronai/sdk\");\n" + indent + "await config();";
301
- }
302
- }
303
- toReplace.push({ index: i, content: lines[i], replacement });
304
- }
305
- }
306
-
307
- if (isEsm) {
308
- for (let i = 0; i < lines.length; i++) {
309
- if (seenIndices.has(i)) continue;
310
- const trimmed = lines[i].trim();
311
- const indent = lines[i].match(/^(\s*)/)[1];
312
- if (/^config\s*\(.*\);?$/.test(trimmed) && !trimmed.startsWith("//")) {
313
- toReplace.push({
314
- index: i,
315
- content: lines[i],
316
- replacement: indent + trimmed.replace(/^config\s*\(/, "await config("),
317
- });
318
- }
319
- }
320
- toReplace.sort((a, b) => a.index - b.index);
321
- }
322
-
323
- if (toReplace.length === 0) {
324
- printSnippet(isEsm);
325
- return;
326
- }
327
-
328
- const relEntry = relative(cwd, entryPath);
329
-
330
- // ── Diff preview ───────────────────────────────────────────────────────────
331
- console.log(ui.kv("ENTRY", relEntry, 8));
332
- console.log(ui.blank());
333
-
334
- for (const { content, replacement } of toReplace) {
335
- console.log(ui.diff("remove", content.trim()));
336
- if (replacement !== null) {
337
- for (const l of replacement.split("\n")) {
338
- console.log(ui.diff("add", l.trim()));
339
- }
340
- } else {
341
- console.log(ui.diff("unknown", ""));
342
- }
343
- console.log(ui.blank());
344
- }
345
-
346
- process.stdout.write(ui.promptLine("apply patch? [Y/n]"));
347
- const answer = await prompt("");
348
- const confirmed = answer === "" || /^y(es)?$/i.test(answer);
349
- console.log(ui.blank());
350
- console.log(ui.hr());
351
- console.log(ui.blank());
352
-
353
- if (!confirmed) {
354
- printSnippet(isEsm);
355
- return;
356
- }
357
-
358
- const indexMap = new Map(toReplace.map(r => [r.index, r]));
359
- const outLines = lines.map((line, i) => {
360
- const r = indexMap.get(i);
361
- return (r && r.replacement !== null) ? r.replacement : line;
362
- });
363
-
364
- if (isEsm) {
365
- const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(/.test(l));
366
- const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
367
- if (callLineIndex !== -1 && callLineIndex > lastImportIndex + 2) {
368
- const callLine = outLines[callLineIndex];
369
- outLines.splice(callLineIndex, 1);
370
- if (outLines[callLineIndex]?.trim() === "") outLines.splice(callLineIndex, 1);
371
- const newLastImport = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
372
- outLines.splice(newLastImport + 1, 0, "", callLine, "");
373
- }
374
- }
375
-
376
- try {
377
- writeFileSync(entryPath, outLines.join("\n"), "utf-8");
378
- } catch (err) {
379
- console.error(ui.errBlock("could not write " + relEntry, err.message));
380
- printSnippet(isEsm);
381
- }
382
- }
383
-
384
- function printSnippet(isEsm) {
385
- console.log(ui.infoBlock("add to your entry point:"));
386
- console.log(ui.blank());
387
- if (isEsm) {
388
- console.log(ui.INDENT + " " + ui.amber('import { config } from "@suronai/sdk"'));
389
- console.log(ui.INDENT + " " + ui.amber("await config()"));
390
- } else {
391
- console.log(ui.INDENT + " " + ui.amber('const { config } = require("@suronai/sdk")'));
392
- console.log(ui.INDENT + " " + ui.amber("await config()"));
393
- }
394
- console.log(ui.blank());
395
- console.log(ui.hr());
396
- console.log(ui.blank());
397
- }
@@ -1,83 +0,0 @@
1
- import { Command } from "commander";
2
- import { getApiUrl, saveConfig, prompt } from "../utils/config.js";
3
- import ui 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(ui.logo());
9
- console.log(ui.blank());
10
- console.log(ui.hr());
11
- console.log(ui.blank());
12
-
13
- let apiUrl = getApiUrl();
14
-
15
- if (apiUrl) {
16
- console.log(ui.kv("ENDPOINT", apiUrl));
17
- console.log(ui.blank());
18
- process.stdout.write(ui.promptLine("replace? [y/N]"));
19
- const change = await prompt("");
20
- if (change.toLowerCase() !== "y") {
21
- console.log(ui.blank());
22
- console.log(ui.okBlock("no changes"));
23
- console.log(ui.blank());
24
- return;
25
- }
26
- console.log(ui.blank());
27
- }
28
-
29
- process.stdout.write(ui.promptLine("convex deployment URL"));
30
- const input = await prompt("");
31
-
32
- if (!input) {
33
- console.log(ui.blank());
34
- console.error(ui.errBlock("URL required"));
35
- console.log(ui.blank());
36
- process.exit(1);
37
- }
38
-
39
- apiUrl = input.trim().replace(/\/$/, "");
40
- console.log(ui.blank());
41
- console.log(ui.hr());
42
- console.log(ui.blank());
43
-
44
- // Verify
45
- process.stdout.write(ui.INDENT + ui.slate("verifying ···"));
46
-
47
- let verified = false;
48
- try {
49
- const res = await fetch(`${apiUrl}/cli/verify`, {
50
- method: "POST",
51
- headers: { "Content-Type": "application/json" },
52
- body: JSON.stringify({}),
53
- });
54
- verified = res.ok;
55
- if (!res.ok) {
56
- const text = await res.text().catch(() => "");
57
- process.stdout.write("\r" + " ".repeat(40) + "\r");
58
- console.error(ui.errBlock(`backend returned ${res.status}`, text || undefined));
59
- console.log(ui.blank());
60
- process.exit(1);
61
- }
62
- } catch {
63
- process.stdout.write("\r" + " ".repeat(40) + "\r");
64
- console.error(ui.errBlock(
65
- `cannot reach ${apiUrl}`,
66
- "is the convex deployment running?"
67
- ));
68
- console.log(ui.blank());
69
- process.exit(1);
70
- }
71
-
72
- process.stdout.write("\r" + " ".repeat(40) + "\r");
73
-
74
- saveConfig({ apiUrl });
75
-
76
- console.log(ui.step("done", "connection verified"));
77
- console.log(ui.step("done", "saved ~/.suron-config"));
78
- console.log(ui.blank());
79
- console.log(ui.hr());
80
- console.log(ui.blank());
81
- console.log(ui.infoBlock("next › cd your-project && " + ui.code("suron init")));
82
- console.log(ui.blank());
83
- });
@@ -1,112 +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 ui 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(ui.blank());
15
- console.log(ui.hr());
16
- console.log(ui.blank());
17
- console.log(ui.kv("DIR", ui.slate(cwd)));
18
- console.log(ui.blank());
19
-
20
- if (existsSync(join(cwd, ".suron.json"))) {
21
- console.log(ui.infoBlock(".suron.json already exists here"));
22
- console.log(ui.blank());
23
- process.stdout.write(ui.promptLine("overwrite? [y/N]"));
24
- const answer = await prompt("");
25
- if (!/^y(es)?$/i.test(answer)) {
26
- console.log(ui.blank());
27
- console.log(ui.infoBlock("cancelled"));
28
- console.log(ui.blank());
29
- process.exit(0);
30
- }
31
- console.log(ui.blank());
32
- }
33
-
34
- // ── App name ──────────────────────────────────────────────────────────────
35
- let appName;
36
- if (opts.name) {
37
- appName = opts.name.replace(/[^a-zA-Z0-9]/g, "");
38
- } else {
39
- const suggested = sanitiseName(basename(cwd) || "myapp");
40
- process.stdout.write(ui.promptLine("app name [" + suggested + "]"));
41
- const raw = await prompt("");
42
- appName = sanitiseName(raw || suggested);
43
- }
44
-
45
- if (!appName) {
46
- console.log(ui.blank());
47
- console.error(ui.errBlock("app name required"));
48
- console.log(ui.blank());
49
- process.exit(1);
50
- }
51
-
52
- console.log(ui.blank());
53
- console.log(ui.kv("LOOKUP", appName));
54
- console.log(ui.blank());
55
- console.log(ui.hr());
56
- console.log(ui.blank());
57
-
58
- // ── Look up app ───────────────────────────────────────────────────────────
59
- process.stdout.write(ui.INDENT + ui.slate("querying ···"));
60
-
61
- let res;
62
- try {
63
- res = await fetch(`${apiUrl}/cli/recover-app`, {
64
- method: "POST",
65
- headers: { "Content-Type": "application/json" },
66
- body: JSON.stringify({ name: appName }),
67
- });
68
- } catch (err) {
69
- process.stdout.write("\r" + " ".repeat(40) + "\r");
70
- console.error(ui.errBlock(`cannot reach API`, err.message));
71
- console.log(ui.blank());
72
- process.exit(1);
73
- }
74
-
75
- process.stdout.write("\r" + " ".repeat(40) + "\r");
76
-
77
- if (!res.ok) {
78
- let body = {};
79
- try { body = await res.json(); } catch { /* ignore */ }
80
- if (res.status === 404) {
81
- console.error(ui.errBlock(
82
- `"${appName}" not found`,
83
- "lookup is case-insensitive — check spelling"
84
- ));
85
- } else {
86
- console.error(ui.errBlock(body?.error ?? `recover failed (${res.status})`));
87
- }
88
- console.log(ui.blank());
89
- process.exit(1);
90
- }
91
-
92
- const { app_id, name } = await res.json();
93
-
94
- // ── Write .suron.json ─────────────────────────────────────────────────────
95
- writeFileSync(
96
- join(cwd, ".suron.json"),
97
- JSON.stringify({ app: name, id: app_id, api_url: apiUrl }, null, 2) + "\n",
98
- "utf-8"
99
- );
100
-
101
- console.log(ui.step("done", ".suron.json written"));
102
- console.log(ui.blank());
103
- console.log(ui.kv("APP", name));
104
- console.log(ui.kv("ID", app_id));
105
- console.log(ui.blank());
106
- console.log(ui.hr());
107
- console.log(ui.blank());
108
- });
109
-
110
- function sanitiseName(name) {
111
- return name.replace(/[^a-zA-Z0-9]/g, "");
112
- }
@@ -1,17 +0,0 @@
1
- import { Command } from "commander";
2
- import { requireApiUrl } from "../utils/config.js";
3
- import ui 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(ui.blank());
10
- console.log(ui.hr());
11
- console.log(ui.blank());
12
- console.log(ui.kv("ENDPOINT", apiUrl));
13
- console.log(ui.kv("CONFIG", "~/.suron-config"));
14
- console.log(ui.blank());
15
- console.log(ui.hr());
16
- console.log(ui.blank());
17
- });
@@ -1,110 +0,0 @@
1
- import { execSync } from "child_process";
2
- import { existsSync, readFileSync, writeFileSync } from "fs";
3
- import { join } from "path";
4
-
5
- /**
6
- * Strips the dotenvx banner comment block from a string of file content.
7
- *
8
- * dotenvx injects a block like this into both .env and .env.keys on every
9
- * encrypt call:
10
- *
11
- * #/------------------!DOTENV_PRIVATE_KEYS!---...
12
- * #/ private decryption keys. DO NOT commit ...
13
- * #/ [how it works](https://dotenvx.com/encryption)
14
- * #/ backup with: `dotenvx ops backup`
15
- * #/----------------------------------------------------------/
16
- *
17
- * These lines all start with "#/" which regular user comments never do
18
- * (user comments use a plain "#" prefix), so this is a safe, targeted strip.
19
- *
20
- * @param {string} content Raw file content
21
- * @returns {string} Content with all #/ banner lines removed
22
- */
23
- export function stripDotenvxComments(content) {
24
- return content
25
- .split("\n")
26
- .filter(line => !line.startsWith("#/"))
27
- .join("\n")
28
- .replace(/\n{3,}/g, "\n\n") // collapse excess blank lines left behind
29
- .trimStart();
30
- }
31
-
32
- /**
33
- * Strips dotenvx banner comments from .env and .env.keys in-place.
34
- * Called automatically by encryptDotenv() after every encrypt run,
35
- * but exported so callers can run it standalone if needed.
36
- * @param {string} cwd
37
- */
38
- export function cleanDotenvComments(cwd) {
39
- for (const filename of [".env", ".env.keys"]) {
40
- const filePath = join(cwd, filename);
41
- if (!existsSync(filePath)) continue;
42
- const raw = readFileSync(filePath, "utf-8");
43
- const cleaned = stripDotenvxComments(raw);
44
- if (cleaned !== raw) writeFileSync(filePath, cleaned, "utf-8");
45
- }
46
- }
47
-
48
- /**
49
- * Encrypts .env in-place using dotenvx, then strips the injected banner
50
- * comments from both .env and .env.keys automatically.
51
- *
52
- * Idempotent — dotenvx only encrypts new plaintext values and leaves
53
- * already-encrypted values untouched, reusing the existing keypair.
54
- * Safe to call on first init AND any time new values are added to .env.
55
- *
56
- * @param {string} cwd
57
- */
58
- export function encryptDotenv(cwd) {
59
- if (!existsSync(join(cwd, ".env"))) {
60
- throw new Error(`.env not found in ${cwd}`);
61
- }
62
-
63
- // Strip all dotenvx keypair vars from the child environment.
64
- // If dotenvx sees DOTENV_PRIVATE_KEY or DOTENV_PUBLIC_KEY already set
65
- // it may skip writing .env.keys, breaking readPrivateKey() on first run.
66
- const env = { ...process.env };
67
- for (const k of Object.keys(env)) {
68
- if (k.startsWith("DOTENV_PRIVATE_KEY") || k.startsWith("DOTENV_PUBLIC_KEY")) {
69
- delete env[k];
70
- }
71
- }
72
-
73
- try {
74
- const output = execSync("npx @dotenvx/dotenvx encrypt", {
75
- cwd,
76
- env,
77
- stdio: ["inherit", "pipe", "pipe"],
78
- }).toString();
79
- if (output) process.stdout.write(output);
80
- } catch (err) {
81
- const stderr = err.stderr?.toString() ?? "";
82
- const stdout = err.stdout?.toString() ?? "";
83
- if (stdout) process.stdout.write(stdout);
84
- throw new Error(`dotenvx encrypt failed: ${stderr || err}`);
85
- }
86
-
87
- // Always strip the banner dotenvx injects on every encrypt call.
88
- cleanDotenvComments(cwd);
89
- }
90
-
91
- /**
92
- * Reads the private key out of .env.keys after encryption.
93
- * @param {string} cwd
94
- * @returns {string}
95
- */
96
- export function readPrivateKey(cwd) {
97
- const keysPath = join(cwd, ".env.keys");
98
- if (!existsSync(keysPath)) {
99
- throw new Error(
100
- ".env.keys not found after encryption.\n" +
101
- " Your .env may already be encrypted — restore plaintext values and run: suron init"
102
- );
103
- }
104
- const content = readFileSync(keysPath, "utf-8");
105
- const match = content.match(/DOTENV_PRIVATE_KEY(?:_\w+)?="?([^"\n]+)"?/);
106
- if (!match?.[1]) {
107
- throw new Error("Could not parse DOTENV_PRIVATE_KEY from .env.keys");
108
- }
109
- return match[1].trim();
110
- }