@suronai/cli 0.1.30 → 0.1.32
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 +4 -4
- package/src/commands/init.js +93 -41
- package/src/commands/recover.js +82 -0
- package/src/index.js +5 -5
- package/src/commands/rotate.js +0 -80
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suronai/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for Suron — suron login, init, whoami,
|
|
3
|
+
"version": "0.1.32",
|
|
4
|
+
"description": "CLI for Suron — suron login, init, whoami, recover",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"suron": "./src/index.js"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"src"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/
|
|
14
|
-
"lint": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/
|
|
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
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@dotenvx/dotenvx": "latest",
|
package/src/commands/init.js
CHANGED
|
@@ -15,6 +15,12 @@ export const initCommand = new Command("init")
|
|
|
15
15
|
|
|
16
16
|
console.log("\n" + c.bold(" suron init") + " — " + c.dim(cwd) + "\n");
|
|
17
17
|
|
|
18
|
+
if (existsSync(join(cwd, ".suron.json"))) {
|
|
19
|
+
console.error(" " + c.red("✗") + " .suron.json already exists — this app is already initialised.");
|
|
20
|
+
console.error(" To restore a lost config, run: " + c.bold("suron recover") + "\n");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
if (!existsSync(join(cwd, ".env"))) {
|
|
19
25
|
console.error(" " + c.red("✗") + " .env not found in current directory");
|
|
20
26
|
console.error(" Create a .env file with your secrets, then run: suron init\n");
|
|
@@ -67,17 +73,23 @@ export const initCommand = new Command("init")
|
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
if (!res.ok) {
|
|
70
|
-
let
|
|
71
|
-
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
let body = {};
|
|
77
|
+
try { body = await res.json(); } catch { /* ignore */ }
|
|
78
|
+
|
|
79
|
+
if (res.status === 409) {
|
|
80
|
+
console.error("\n " + c.red("✗") + ` An app named "${c.cyan(appName)}" is already registered.`);
|
|
81
|
+
console.error(" If you lost .suron.json, run: " + c.bold("suron recover") + "\n");
|
|
82
|
+
} else {
|
|
83
|
+
console.error("\n " + c.red("✗") + ` ${body?.error ?? `register-app failed (${res.status})`}\n`);
|
|
84
|
+
}
|
|
76
85
|
process.exit(1);
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
const { app_id } = await res.json();
|
|
80
89
|
|
|
90
|
+
// ── .gitignore ─────────────────────────────────────────────────────────────
|
|
91
|
+
ensureGitignore(cwd);
|
|
92
|
+
|
|
81
93
|
// ── Write .suron.json ─────────────────────────────────────────────────────
|
|
82
94
|
writeFileSync(
|
|
83
95
|
join(cwd, ".suron.json"),
|
|
@@ -105,6 +117,8 @@ export const initCommand = new Command("init")
|
|
|
105
117
|
} catch {
|
|
106
118
|
console.error(" " + c.yellow("⚠") + " SDK install failed — run manually: npm install @suronai/sdk");
|
|
107
119
|
}
|
|
120
|
+
} else {
|
|
121
|
+
console.log(" " + c.dim("@suronai/sdk already installed — skipped."));
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
|
|
@@ -114,12 +128,41 @@ export const initCommand = new Command("init")
|
|
|
114
128
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
115
129
|
console.log();
|
|
116
130
|
console.log(" " + c.green("✓") + " .env encrypted " + c.dim("(safe to commit)"));
|
|
117
|
-
console.log(" " + c.green("✓") + " .env.keys kept " + c.dim("(
|
|
131
|
+
console.log(" " + c.green("✓") + " .env.keys kept " + c.dim("(gitignored)") );
|
|
118
132
|
console.log(" " + c.green("✓") + " .suron.json written " + c.dim("(safe to commit)"));
|
|
119
133
|
console.log(" " + c.green("✓") + " @suronai/sdk installed");
|
|
120
134
|
console.log();
|
|
121
135
|
});
|
|
122
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Ensures .env.keys is in .gitignore.
|
|
139
|
+
* Creates .gitignore if it doesn't exist.
|
|
140
|
+
* Appends the entry if it's not already present (exact line match).
|
|
141
|
+
* @param {string} cwd
|
|
142
|
+
*/
|
|
143
|
+
const GITIGNORE_ENTRIES = [
|
|
144
|
+
"node_modules/",
|
|
145
|
+
"package-lock.json",
|
|
146
|
+
".env.keys",
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
function ensureGitignore(cwd) {
|
|
150
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
151
|
+
|
|
152
|
+
if (!existsSync(gitignorePath)) {
|
|
153
|
+
writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
158
|
+
const existing = new Set(content.split("\n").map(l => l.trim()));
|
|
159
|
+
const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
|
|
160
|
+
if (missing.length === 0) return;
|
|
161
|
+
|
|
162
|
+
const separator = content.endsWith("\n") ? "" : "\n";
|
|
163
|
+
writeFileSync(gitignorePath, content + separator + missing.join("\n") + "\n", "utf-8");
|
|
164
|
+
}
|
|
165
|
+
|
|
123
166
|
/** @param {string} cwd @returns {string} */
|
|
124
167
|
function detectPackageManager(cwd) {
|
|
125
168
|
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -176,15 +219,53 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
176
219
|
|
|
177
220
|
const lines = src.split("\n");
|
|
178
221
|
|
|
179
|
-
//
|
|
180
|
-
|
|
222
|
+
// Two-pass scan:
|
|
223
|
+
// Pass 1 — lines containing "dotenv" (import or require)
|
|
224
|
+
// Pass 2 — bare config() / config({...}) call lines that don't contain "dotenv"
|
|
225
|
+
// (these appear after the import was already on a separate line)
|
|
226
|
+
const toReplace = [];
|
|
227
|
+
const seenIndices = new Set();
|
|
228
|
+
|
|
181
229
|
for (let i = 0; i < lines.length; i++) {
|
|
230
|
+
const trimmed = lines[i].trim();
|
|
231
|
+
const indent = lines[i].match(/^(\s*)/)[1];
|
|
232
|
+
|
|
182
233
|
if (lines[i].includes("dotenv")) {
|
|
183
|
-
|
|
234
|
+
seenIndices.add(i);
|
|
235
|
+
let replacement = null;
|
|
236
|
+
if (isEsm) {
|
|
237
|
+
if (trimmed.startsWith("import")) {
|
|
238
|
+
replacement = indent + trimmed.replace(/(from\s+)['"]dotenv(?:\/config)?['"]/, '$1"@suronai/sdk"');
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
if (trimmed.includes("require")) {
|
|
242
|
+
replacement = indent + "const { config } = require(\"@suronai/sdk\");\n" + indent + "await config();";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
toReplace.push({ index: i, content: lines[i], replacement });
|
|
184
246
|
}
|
|
185
247
|
}
|
|
186
248
|
|
|
187
|
-
|
|
249
|
+
// Pass 2: bare config() call — only in ESM where the import and call are separate lines
|
|
250
|
+
if (isEsm) {
|
|
251
|
+
for (let i = 0; i < lines.length; i++) {
|
|
252
|
+
if (seenIndices.has(i)) continue;
|
|
253
|
+
const trimmed = lines[i].trim();
|
|
254
|
+
const indent = lines[i].match(/^(\s*)/)[1];
|
|
255
|
+
// Matches: config() / config({}) / config({ ... }) — with or without semicolon
|
|
256
|
+
if (/^config\s*\(.*\);?$/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
257
|
+
toReplace.push({
|
|
258
|
+
index: i,
|
|
259
|
+
content: lines[i],
|
|
260
|
+
replacement: indent + trimmed.replace(/^config\s*\(/, "await config("),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Sort by line number so the diff preview is in file order
|
|
265
|
+
toReplace.sort((a, b) => a.index - b.index);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (toReplace.length === 0) {
|
|
188
269
|
console.log("\n Add to your app entry point:\n");
|
|
189
270
|
printSnippet(isEsm);
|
|
190
271
|
return;
|
|
@@ -195,36 +276,7 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
195
276
|
console.log(" " + c.yellow("▶") + " Found dotenv in " + c.dim(relEntry) + ":");
|
|
196
277
|
console.log();
|
|
197
278
|
|
|
198
|
-
|
|
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
|
-
});
|
|
279
|
+
const replacements = toReplace;
|
|
228
280
|
|
|
229
281
|
// Show the diff preview
|
|
230
282
|
for (const { content, replacement } of replacements) {
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
let appName;
|
|
27
|
+
if (opts.name) {
|
|
28
|
+
appName = opts.name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
29
|
+
console.log(" App name " + c.cyan(appName));
|
|
30
|
+
} else {
|
|
31
|
+
const suggested = basename(cwd) || "my-project";
|
|
32
|
+
const raw = await prompt(` App name [${c.dim(suggested)}] › `);
|
|
33
|
+
appName = (raw || suggested).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!appName) {
|
|
37
|
+
console.error(" " + c.red("✗") + " App name is required.\n");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Look up app ───────────────────────────────────────────────────────────
|
|
42
|
+
console.log("\n " + c.dim("Looking up app..."));
|
|
43
|
+
|
|
44
|
+
let res;
|
|
45
|
+
try {
|
|
46
|
+
res = await fetch(`${apiUrl}/cli/recover-app`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify({ name: appName }),
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
let body = {};
|
|
58
|
+
try { body = await res.json(); } catch { /* ignore */ }
|
|
59
|
+
if (res.status === 404) {
|
|
60
|
+
console.error("\n " + c.red("✗") + ` No app named "${c.cyan(appName)}" found.`);
|
|
61
|
+
console.error(" Check the name carefully — names are lowercase and alphanumeric only.\n");
|
|
62
|
+
} else {
|
|
63
|
+
console.error("\n " + c.red("✗") + ` ${body?.error ?? `recover-app failed (${res.status})`}\n`);
|
|
64
|
+
}
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { app_id, name } = await res.json();
|
|
69
|
+
|
|
70
|
+
// ── Write .suron.json ─────────────────────────────────────────────────────
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(cwd, ".suron.json"),
|
|
73
|
+
JSON.stringify({ app: name, id: app_id, api_url: apiUrl }, null, 2) + "\n",
|
|
74
|
+
"utf-8"
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(" " + c.green("✓") + " .suron.json restored");
|
|
79
|
+
console.log(" " + c.dim(" app: ") + c.cyan(name));
|
|
80
|
+
console.log(" " + c.dim(" id: ") + c.cyan(app_id));
|
|
81
|
+
console.log();
|
|
82
|
+
});
|
package/src/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import { loginCommand }
|
|
6
|
-
import { initCommand }
|
|
7
|
-
import { whoamiCommand }
|
|
8
|
-
import {
|
|
5
|
+
import { loginCommand } from "./commands/login.js";
|
|
6
|
+
import { initCommand } from "./commands/init.js";
|
|
7
|
+
import { whoamiCommand } from "./commands/whoami.js";
|
|
8
|
+
import { recoverCommand } from "./commands/recover.js";
|
|
9
9
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const { version } = require("../package.json");
|
|
@@ -20,6 +20,6 @@ program
|
|
|
20
20
|
program.addCommand(loginCommand);
|
|
21
21
|
program.addCommand(initCommand);
|
|
22
22
|
program.addCommand(whoamiCommand);
|
|
23
|
-
program.addCommand(
|
|
23
|
+
program.addCommand(recoverCommand);
|
|
24
24
|
|
|
25
25
|
program.parse(process.argv);
|
package/src/commands/rotate.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { requireApiUrl } from "../utils/config.js";
|
|
5
|
-
import { encryptDotenv, readPrivateKey } from "../utils/dotenvx.js";
|
|
6
|
-
import c from "../utils/colors.js";
|
|
7
|
-
|
|
8
|
-
export const rotateCommand = new Command("rotate")
|
|
9
|
-
.description("Re-encrypt .env with a new private key and update Suron")
|
|
10
|
-
.action(async () => {
|
|
11
|
-
const cwd = process.cwd();
|
|
12
|
-
const apiUrl = requireApiUrl();
|
|
13
|
-
|
|
14
|
-
console.log("\n" + c.bold(" suron rotate") + " — " + c.dim(cwd) + "\n");
|
|
15
|
-
|
|
16
|
-
const suronJsonPath = join(cwd, ".suron.json");
|
|
17
|
-
if (!existsSync(suronJsonPath)) {
|
|
18
|
-
console.error(" " + c.red("✗") + " .suron.json not found — run: suron init\n");
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let suronConfig;
|
|
23
|
-
try {
|
|
24
|
-
suronConfig = JSON.parse(readFileSync(suronJsonPath, "utf-8"));
|
|
25
|
-
} catch {
|
|
26
|
-
console.error(" " + c.red("✗") + " .suron.json is malformed\n");
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!existsSync(join(cwd, ".env"))) {
|
|
31
|
-
console.error(" " + c.red("✗") + " .env not found\n");
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
console.log(" " + c.dim("Re-encrypting .env with a new private key..."));
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
encryptDotenv(cwd);
|
|
39
|
-
} catch (err) {
|
|
40
|
-
console.error("\n " + c.red("✗") + " " + err.message + "\n");
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let newPrivateKey;
|
|
45
|
-
try {
|
|
46
|
-
newPrivateKey = readPrivateKey(cwd);
|
|
47
|
-
} catch (err) {
|
|
48
|
-
console.error("\n " + c.red("✗") + " " + err.message + "\n");
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log(" " + c.dim("Updating private key in Suron..."));
|
|
53
|
-
|
|
54
|
-
let res;
|
|
55
|
-
try {
|
|
56
|
-
res = await fetch(`${apiUrl}/cli/rotate-key`, {
|
|
57
|
-
method: "POST",
|
|
58
|
-
headers: { "Content-Type": "application/json" },
|
|
59
|
-
body: JSON.stringify({ app_id: suronConfig.id, private_key: newPrivateKey }),
|
|
60
|
-
});
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!res.ok) {
|
|
67
|
-
const text = await res.text().catch(() => "");
|
|
68
|
-
console.error("\n " + c.red("✗") + ` rotate-key failed (${res.status}): ${text}\n`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const keysPath = join(cwd, ".env.keys");
|
|
73
|
-
if (existsSync(keysPath)) unlinkSync(keysPath);
|
|
74
|
-
|
|
75
|
-
console.log();
|
|
76
|
-
console.log(" " + c.green("✓") + " Key rotated");
|
|
77
|
-
console.log(" " + c.green("✓") + " .env re-encrypted " + c.dim("(safe to commit)"));
|
|
78
|
-
console.log(" " + c.green("✓") + " .env.keys deleted");
|
|
79
|
-
console.log();
|
|
80
|
-
});
|