@suronai/cli 0.1.34 → 0.1.36
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 +1 -1
- package/src/commands/encrypt.js +88 -0
- package/src/commands/init.js +117 -103
- package/src/commands/login.js +45 -18
- package/src/commands/recover.js +51 -21
- package/src/commands/whoami.js +9 -5
- package/src/index.js +2 -0
- package/src/utils/colors.js +179 -9
- package/src/utils/config.js +4 -2
- package/src/utils/dotenvx.js +60 -11
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
});
|
package/src/commands/init.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { existsSync, writeFileSync, readFileSync } from "fs";
|
|
3
|
-
import { join, basename } from "path";
|
|
3
|
+
import { join, basename, relative } from "path";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { requireApiUrl, prompt } from "../utils/config.js";
|
|
6
6
|
import { encryptDotenv, readPrivateKey } from "../utils/dotenvx.js";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
const HR = " " + c.dim("─".repeat(55));
|
|
7
|
+
import ui from "../utils/colors.js";
|
|
10
8
|
|
|
11
9
|
export const initCommand = new Command("init")
|
|
12
10
|
.description("Encrypt .env and register this app with Suron")
|
|
@@ -15,67 +13,89 @@ export const initCommand = new Command("init")
|
|
|
15
13
|
const cwd = process.cwd();
|
|
16
14
|
const apiUrl = requireApiUrl();
|
|
17
15
|
|
|
18
|
-
console.log();
|
|
16
|
+
console.log(ui.logo());
|
|
17
|
+
console.log(ui.blank());
|
|
18
|
+
console.log(ui.hr());
|
|
19
|
+
console.log(ui.blank());
|
|
19
20
|
|
|
20
21
|
if (existsSync(join(cwd, ".suron.json"))) {
|
|
21
|
-
console.error(
|
|
22
|
-
|
|
22
|
+
console.error(ui.errBlock(
|
|
23
|
+
"already initialised",
|
|
24
|
+
"to restore a lost config › suron recover"
|
|
25
|
+
));
|
|
26
|
+
console.log(ui.blank());
|
|
23
27
|
process.exit(1);
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
if (!existsSync(join(cwd, ".env"))) {
|
|
27
|
-
console.error(
|
|
28
|
-
|
|
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());
|
|
29
36
|
process.exit(1);
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
// ── App name ──────────────────────────────────────────────────────────────
|
|
33
|
-
const suggested = sanitiseName(basename(cwd) || "
|
|
40
|
+
const suggested = sanitiseName(basename(cwd) || "myapp");
|
|
34
41
|
let appName;
|
|
35
42
|
|
|
36
43
|
if (opts.name) {
|
|
37
44
|
appName = sanitiseName(opts.name);
|
|
38
45
|
} else {
|
|
39
|
-
|
|
46
|
+
process.stdout.write(ui.promptLine("app name [" + suggested + "]"));
|
|
47
|
+
const raw = await prompt("");
|
|
40
48
|
appName = sanitiseName(raw || suggested);
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
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());
|
|
45
67
|
|
|
46
|
-
// ── Steps
|
|
68
|
+
// ── Steps ─────────────────────────────────────────────────────────────────
|
|
47
69
|
const steps = {
|
|
48
|
-
encrypt:
|
|
49
|
-
keys:
|
|
50
|
-
gitignore:{ label: "
|
|
51
|
-
register:
|
|
52
|
-
sdk:
|
|
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" },
|
|
53
75
|
};
|
|
54
76
|
|
|
55
|
-
|
|
77
|
+
const printSteps = () => {
|
|
56
78
|
for (const s of Object.values(steps)) {
|
|
57
|
-
|
|
58
|
-
: s.status === "skip" ? c.dim("○")
|
|
59
|
-
: s.status === "fail" ? c.red("●")
|
|
60
|
-
: c.dim("○");
|
|
61
|
-
const detail = s.status === "fail" ? c.red(s.detail) : c.dim(s.detail);
|
|
62
|
-
const label = s.label.padEnd(11);
|
|
63
|
-
const dots = c.dim(".".repeat(Math.max(2, 44 - label.length - s.detail.length)));
|
|
64
|
-
const note = s.note ? " " + c.dim(s.note) : "";
|
|
65
|
-
console.log(" " + dot + " " + label + detail + dots + (s.status === "done" ? c.green(" done") : s.status === "skip" ? c.dim(" skip") : s.status === "fail" ? c.red(" fail") : "") + note);
|
|
79
|
+
console.log(ui.step(s.status, s.label, s.note));
|
|
66
80
|
}
|
|
67
|
-
}
|
|
81
|
+
};
|
|
68
82
|
|
|
69
83
|
// ── Encrypt ───────────────────────────────────────────────────────────────
|
|
84
|
+
steps.encrypt.status = "run";
|
|
85
|
+
steps.keys.status = "run";
|
|
70
86
|
try {
|
|
71
87
|
encryptDotenv(cwd);
|
|
72
|
-
steps.encrypt.status
|
|
73
|
-
steps.keys.status
|
|
88
|
+
steps.encrypt.status = "done";
|
|
89
|
+
steps.keys.status = "done";
|
|
74
90
|
} catch (err) {
|
|
75
91
|
steps.encrypt.status = "fail";
|
|
92
|
+
steps.keys.status = "fail";
|
|
76
93
|
printSteps();
|
|
77
|
-
console.log(
|
|
78
|
-
console.
|
|
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());
|
|
79
99
|
process.exit(1);
|
|
80
100
|
}
|
|
81
101
|
|
|
@@ -85,17 +105,22 @@ export const initCommand = new Command("init")
|
|
|
85
105
|
} catch (err) {
|
|
86
106
|
steps.keys.status = "fail";
|
|
87
107
|
printSteps();
|
|
88
|
-
console.log(
|
|
89
|
-
console.
|
|
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());
|
|
90
113
|
process.exit(1);
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
117
|
+
steps.gitignore.status = "run";
|
|
94
118
|
const gitignoreResult = ensureGitignore(cwd);
|
|
95
119
|
steps.gitignore.status = "done";
|
|
96
120
|
steps.gitignore.note = gitignoreResult;
|
|
97
121
|
|
|
98
122
|
// ── Register ──────────────────────────────────────────────────────────────
|
|
123
|
+
steps.register.status = "run";
|
|
99
124
|
let res;
|
|
100
125
|
try {
|
|
101
126
|
res = await fetch(`${apiUrl}/cli/register-app`, {
|
|
@@ -106,8 +131,11 @@ export const initCommand = new Command("init")
|
|
|
106
131
|
} catch (err) {
|
|
107
132
|
steps.register.status = "fail";
|
|
108
133
|
printSteps();
|
|
109
|
-
console.log(
|
|
110
|
-
console.
|
|
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());
|
|
111
139
|
process.exit(1);
|
|
112
140
|
}
|
|
113
141
|
|
|
@@ -116,13 +144,19 @@ export const initCommand = new Command("init")
|
|
|
116
144
|
try { body = await res.json(); } catch { /* ignore */ }
|
|
117
145
|
steps.register.status = "fail";
|
|
118
146
|
printSteps();
|
|
119
|
-
console.log(
|
|
147
|
+
console.log(ui.blank());
|
|
148
|
+
console.log(ui.hr());
|
|
149
|
+
console.log(ui.blank());
|
|
120
150
|
if (res.status === 409) {
|
|
121
|
-
|
|
122
|
-
console.error(
|
|
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
|
+
));
|
|
123
156
|
} else {
|
|
124
|
-
console.error(
|
|
157
|
+
console.error(ui.errBlock(body?.error ?? `register failed (${res.status})`));
|
|
125
158
|
}
|
|
159
|
+
console.log(ui.blank());
|
|
126
160
|
process.exit(1);
|
|
127
161
|
}
|
|
128
162
|
|
|
@@ -139,6 +173,7 @@ export const initCommand = new Command("init")
|
|
|
139
173
|
// ── Install SDK ───────────────────────────────────────────────────────────
|
|
140
174
|
const pkgJsonPath = join(cwd, "package.json");
|
|
141
175
|
let isEsm = false;
|
|
176
|
+
steps.sdk.status = "run";
|
|
142
177
|
|
|
143
178
|
if (existsSync(pkgJsonPath)) {
|
|
144
179
|
let alreadyInstalled = false;
|
|
@@ -167,51 +202,41 @@ export const initCommand = new Command("init")
|
|
|
167
202
|
}
|
|
168
203
|
|
|
169
204
|
printSteps();
|
|
170
|
-
console.log(
|
|
205
|
+
console.log(ui.blank());
|
|
206
|
+
console.log(ui.hr());
|
|
207
|
+
console.log(ui.blank());
|
|
171
208
|
|
|
172
209
|
// ── Patch entry point ─────────────────────────────────────────────────────
|
|
173
210
|
await patchEntryPoint(cwd, isEsm);
|
|
174
211
|
|
|
175
212
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
176
|
-
console.log(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
console.log();
|
|
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());
|
|
182
219
|
});
|
|
183
220
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
* @param {string} cwd
|
|
188
|
-
* @returns {string} short note for the steps display
|
|
189
|
-
*/
|
|
190
|
-
const GITIGNORE_ENTRIES = [
|
|
191
|
-
"node_modules/",
|
|
192
|
-
"package-lock.json",
|
|
193
|
-
".env.keys",
|
|
194
|
-
];
|
|
221
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
const GITIGNORE_ENTRIES = ["node_modules/", "package-lock.json", ".env.keys"];
|
|
195
224
|
|
|
196
225
|
function ensureGitignore(cwd) {
|
|
197
226
|
const gitignorePath = join(cwd, ".gitignore");
|
|
198
|
-
|
|
199
227
|
if (!existsSync(gitignorePath)) {
|
|
200
228
|
writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
|
|
201
229
|
return "created";
|
|
202
230
|
}
|
|
203
|
-
|
|
204
231
|
const content = readFileSync(gitignorePath, "utf-8");
|
|
205
232
|
const existing = new Set(content.split("\n").map(l => l.trim()));
|
|
206
233
|
const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
|
|
207
|
-
if (missing.length === 0) return "
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
writeFileSync(gitignorePath, content + separator + missing.join("\n") + "\n", "utf-8");
|
|
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");
|
|
211
237
|
return "updated";
|
|
212
238
|
}
|
|
213
239
|
|
|
214
|
-
/** @param {string} cwd @returns {string} */
|
|
215
240
|
function detectPackageManager(cwd) {
|
|
216
241
|
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
217
242
|
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
@@ -219,17 +244,17 @@ function detectPackageManager(cwd) {
|
|
|
219
244
|
return "npm";
|
|
220
245
|
}
|
|
221
246
|
|
|
222
|
-
/** @param {string} pm @returns {string} */
|
|
223
247
|
function pmAddCmd(pm) {
|
|
224
248
|
return pm === "npm" ? "install" : "add";
|
|
225
249
|
}
|
|
226
250
|
|
|
227
251
|
/**
|
|
252
|
+
* Strips everything except letters and digits while preserving case.
|
|
228
253
|
* @param {string} name
|
|
229
254
|
* @returns {string}
|
|
230
255
|
*/
|
|
231
256
|
function sanitiseName(name) {
|
|
232
|
-
return name.
|
|
257
|
+
return name.replace(/[^a-zA-Z0-9]/g, "");
|
|
233
258
|
}
|
|
234
259
|
|
|
235
260
|
/**
|
|
@@ -248,9 +273,6 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
248
273
|
}
|
|
249
274
|
|
|
250
275
|
if (!entryPath) {
|
|
251
|
-
console.log(" " + c.dim("▎"));
|
|
252
|
-
console.log(" " + c.dim("▎") + " Add to your app entry point:");
|
|
253
|
-
console.log(" " + c.dim("▎"));
|
|
254
276
|
printSnippet(isEsm);
|
|
255
277
|
return;
|
|
256
278
|
}
|
|
@@ -259,8 +281,6 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
259
281
|
try { src = readFileSync(entryPath, "utf-8"); } catch { return; }
|
|
260
282
|
|
|
261
283
|
const lines = src.split("\n");
|
|
262
|
-
|
|
263
|
-
// Pass 1 — lines containing "dotenv"
|
|
264
284
|
const toReplace = [];
|
|
265
285
|
const seenIndices = new Set();
|
|
266
286
|
|
|
@@ -284,7 +304,6 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
284
304
|
}
|
|
285
305
|
}
|
|
286
306
|
|
|
287
|
-
// Pass 2 — bare config() call (ESM only — import and call are on separate lines)
|
|
288
307
|
if (isEsm) {
|
|
289
308
|
for (let i = 0; i < lines.length; i++) {
|
|
290
309
|
if (seenIndices.has(i)) continue;
|
|
@@ -302,56 +321,49 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
302
321
|
}
|
|
303
322
|
|
|
304
323
|
if (toReplace.length === 0) {
|
|
305
|
-
console.log(" " + c.dim("▎"));
|
|
306
|
-
console.log(" " + c.dim("▎") + " Add to your app entry point:");
|
|
307
|
-
console.log(" " + c.dim("▎"));
|
|
308
324
|
printSnippet(isEsm);
|
|
309
325
|
return;
|
|
310
326
|
}
|
|
311
327
|
|
|
312
|
-
const relEntry =
|
|
328
|
+
const relEntry = relative(cwd, entryPath);
|
|
313
329
|
|
|
314
330
|
// ── Diff preview ───────────────────────────────────────────────────────────
|
|
315
|
-
console.log("
|
|
316
|
-
console.log(
|
|
317
|
-
console.log(" " + c.dim("▎"));
|
|
331
|
+
console.log(ui.kv("ENTRY", relEntry, 8));
|
|
332
|
+
console.log(ui.blank());
|
|
318
333
|
|
|
319
334
|
for (const { content, replacement } of toReplace) {
|
|
320
|
-
console.log(
|
|
335
|
+
console.log(ui.diff("remove", content.trim()));
|
|
321
336
|
if (replacement !== null) {
|
|
322
337
|
for (const l of replacement.split("\n")) {
|
|
323
|
-
console.log(
|
|
338
|
+
console.log(ui.diff("add", l.trim()));
|
|
324
339
|
}
|
|
325
340
|
} else {
|
|
326
|
-
console.log(
|
|
341
|
+
console.log(ui.diff("unknown", ""));
|
|
327
342
|
}
|
|
328
|
-
console.log(
|
|
343
|
+
console.log(ui.blank());
|
|
329
344
|
}
|
|
330
345
|
|
|
331
|
-
|
|
346
|
+
process.stdout.write(ui.promptLine("apply patch? [Y/n]"));
|
|
347
|
+
const answer = await prompt("");
|
|
332
348
|
const confirmed = answer === "" || /^y(es)?$/i.test(answer);
|
|
333
|
-
console.log(
|
|
349
|
+
console.log(ui.blank());
|
|
350
|
+
console.log(ui.hr());
|
|
351
|
+
console.log(ui.blank());
|
|
334
352
|
|
|
335
353
|
if (!confirmed) {
|
|
336
|
-
console.log();
|
|
337
|
-
console.log(" " + c.dim("skipped — add manually:"));
|
|
338
|
-
console.log();
|
|
339
354
|
printSnippet(isEsm);
|
|
340
355
|
return;
|
|
341
356
|
}
|
|
342
357
|
|
|
343
|
-
// Apply
|
|
344
358
|
const indexMap = new Map(toReplace.map(r => [r.index, r]));
|
|
345
359
|
const outLines = lines.map((line, i) => {
|
|
346
360
|
const r = indexMap.get(i);
|
|
347
361
|
return (r && r.replacement !== null) ? r.replacement : line;
|
|
348
362
|
});
|
|
349
363
|
|
|
350
|
-
// Move await config() to right after last import block (ESM)
|
|
351
364
|
if (isEsm) {
|
|
352
|
-
const callLineIndex
|
|
365
|
+
const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(/.test(l));
|
|
353
366
|
const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
|
|
354
|
-
|
|
355
367
|
if (callLineIndex !== -1 && callLineIndex > lastImportIndex + 2) {
|
|
356
368
|
const callLine = outLines[callLineIndex];
|
|
357
369
|
outLines.splice(callLineIndex, 1);
|
|
@@ -364,20 +376,22 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
364
376
|
try {
|
|
365
377
|
writeFileSync(entryPath, outLines.join("\n"), "utf-8");
|
|
366
378
|
} catch (err) {
|
|
367
|
-
console.error(
|
|
379
|
+
console.error(ui.errBlock("could not write " + relEntry, err.message));
|
|
368
380
|
printSnippet(isEsm);
|
|
369
381
|
}
|
|
370
382
|
}
|
|
371
383
|
|
|
372
|
-
/** @param {boolean} isEsm */
|
|
373
384
|
function printSnippet(isEsm) {
|
|
385
|
+
console.log(ui.infoBlock("add to your entry point:"));
|
|
386
|
+
console.log(ui.blank());
|
|
374
387
|
if (isEsm) {
|
|
375
|
-
console.log(
|
|
376
|
-
console.log(
|
|
388
|
+
console.log(ui.INDENT + " " + ui.amber('import { config } from "@suronai/sdk"'));
|
|
389
|
+
console.log(ui.INDENT + " " + ui.amber("await config()"));
|
|
377
390
|
} else {
|
|
378
|
-
console.log(
|
|
379
|
-
console.log(
|
|
391
|
+
console.log(ui.INDENT + " " + ui.amber('const { config } = require("@suronai/sdk")'));
|
|
392
|
+
console.log(ui.INDENT + " " + ui.amber("await config()"));
|
|
380
393
|
}
|
|
381
|
-
console.log(
|
|
382
|
-
console.log(
|
|
394
|
+
console.log(ui.blank());
|
|
395
|
+
console.log(ui.hr());
|
|
396
|
+
console.log(ui.blank());
|
|
383
397
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,56 +1,83 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { getApiUrl, saveConfig, prompt } from "../utils/config.js";
|
|
3
|
-
import
|
|
3
|
+
import ui from "../utils/colors.js";
|
|
4
4
|
|
|
5
5
|
export const loginCommand = new Command("login")
|
|
6
6
|
.description("Configure the Suron API URL")
|
|
7
7
|
.action(async () => {
|
|
8
|
-
console.log(
|
|
8
|
+
console.log(ui.logo());
|
|
9
|
+
console.log(ui.blank());
|
|
10
|
+
console.log(ui.hr());
|
|
11
|
+
console.log(ui.blank());
|
|
9
12
|
|
|
10
13
|
let apiUrl = getApiUrl();
|
|
11
14
|
|
|
12
15
|
if (apiUrl) {
|
|
13
|
-
console.log("
|
|
14
|
-
|
|
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("");
|
|
15
20
|
if (change.toLowerCase() !== "y") {
|
|
16
|
-
console.log(
|
|
21
|
+
console.log(ui.blank());
|
|
22
|
+
console.log(ui.okBlock("no changes"));
|
|
23
|
+
console.log(ui.blank());
|
|
17
24
|
return;
|
|
18
25
|
}
|
|
26
|
+
console.log(ui.blank());
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
process.stdout.write(ui.promptLine("convex deployment URL"));
|
|
30
|
+
const input = await prompt("");
|
|
31
|
+
|
|
22
32
|
if (!input) {
|
|
23
|
-
console.
|
|
33
|
+
console.log(ui.blank());
|
|
34
|
+
console.error(ui.errBlock("URL required"));
|
|
35
|
+
console.log(ui.blank());
|
|
24
36
|
process.exit(1);
|
|
25
37
|
}
|
|
26
|
-
apiUrl = input.replace(/\/$/, "");
|
|
27
38
|
|
|
28
|
-
|
|
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 ···"));
|
|
29
46
|
|
|
47
|
+
let verified = false;
|
|
30
48
|
try {
|
|
31
49
|
const res = await fetch(`${apiUrl}/cli/verify`, {
|
|
32
50
|
method: "POST",
|
|
33
51
|
headers: { "Content-Type": "application/json" },
|
|
34
52
|
body: JSON.stringify({}),
|
|
35
53
|
});
|
|
54
|
+
verified = res.ok;
|
|
36
55
|
if (!res.ok) {
|
|
37
56
|
const text = await res.text().catch(() => "");
|
|
38
|
-
process.stdout.write("\r" + " ".repeat(
|
|
39
|
-
console.error(
|
|
57
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
58
|
+
console.error(ui.errBlock(`backend returned ${res.status}`, text || undefined));
|
|
59
|
+
console.log(ui.blank());
|
|
40
60
|
process.exit(1);
|
|
41
61
|
}
|
|
42
62
|
} catch {
|
|
43
|
-
process.stdout.write("\r" + " ".repeat(
|
|
44
|
-
console.error(
|
|
45
|
-
|
|
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());
|
|
46
69
|
process.exit(1);
|
|
47
70
|
}
|
|
48
71
|
|
|
49
|
-
process.stdout.write("\r" + " ".repeat(
|
|
72
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
50
73
|
|
|
51
74
|
saveConfig({ apiUrl });
|
|
52
75
|
|
|
53
|
-
console.log(
|
|
54
|
-
console.log(
|
|
55
|
-
console.log(
|
|
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());
|
|
56
83
|
});
|
package/src/commands/recover.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { existsSync, writeFileSync } from "fs";
|
|
3
3
|
import { join, basename } from "path";
|
|
4
4
|
import { requireApiUrl, prompt } from "../utils/config.js";
|
|
5
|
-
import
|
|
5
|
+
import ui from "../utils/colors.js";
|
|
6
6
|
|
|
7
7
|
export const recoverCommand = new Command("recover")
|
|
8
8
|
.description("Restore a lost .suron.json by looking up your app name")
|
|
@@ -11,35 +11,52 @@ export const recoverCommand = new Command("recover")
|
|
|
11
11
|
const cwd = process.cwd();
|
|
12
12
|
const apiUrl = requireApiUrl();
|
|
13
13
|
|
|
14
|
-
console.log(
|
|
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());
|
|
15
19
|
|
|
16
20
|
if (existsSync(join(cwd, ".suron.json"))) {
|
|
17
|
-
console.log(
|
|
18
|
-
|
|
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("");
|
|
19
25
|
if (!/^y(es)?$/i.test(answer)) {
|
|
20
|
-
console.log(
|
|
26
|
+
console.log(ui.blank());
|
|
27
|
+
console.log(ui.infoBlock("cancelled"));
|
|
28
|
+
console.log(ui.blank());
|
|
21
29
|
process.exit(0);
|
|
22
30
|
}
|
|
31
|
+
console.log(ui.blank());
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
// ── App name ──────────────────────────────────────────────────────────────
|
|
26
35
|
let appName;
|
|
27
36
|
if (opts.name) {
|
|
28
|
-
appName = opts.name.
|
|
29
|
-
console.log(" App name " + c.cyan(appName));
|
|
37
|
+
appName = opts.name.replace(/[^a-zA-Z0-9]/g, "");
|
|
30
38
|
} else {
|
|
31
|
-
const suggested = basename(cwd) || "
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
if (!appName) {
|
|
37
|
-
console.
|
|
46
|
+
console.log(ui.blank());
|
|
47
|
+
console.error(ui.errBlock("app name required"));
|
|
48
|
+
console.log(ui.blank());
|
|
38
49
|
process.exit(1);
|
|
39
50
|
}
|
|
40
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
|
+
|
|
41
58
|
// ── Look up app ───────────────────────────────────────────────────────────
|
|
42
|
-
|
|
59
|
+
process.stdout.write(ui.INDENT + ui.slate("querying ···"));
|
|
43
60
|
|
|
44
61
|
let res;
|
|
45
62
|
try {
|
|
@@ -49,19 +66,26 @@ export const recoverCommand = new Command("recover")
|
|
|
49
66
|
body: JSON.stringify({ name: appName }),
|
|
50
67
|
});
|
|
51
68
|
} catch (err) {
|
|
52
|
-
|
|
69
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
70
|
+
console.error(ui.errBlock(`cannot reach API`, err.message));
|
|
71
|
+
console.log(ui.blank());
|
|
53
72
|
process.exit(1);
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
76
|
+
|
|
56
77
|
if (!res.ok) {
|
|
57
78
|
let body = {};
|
|
58
79
|
try { body = await res.json(); } catch { /* ignore */ }
|
|
59
80
|
if (res.status === 404) {
|
|
60
|
-
console.error(
|
|
61
|
-
|
|
81
|
+
console.error(ui.errBlock(
|
|
82
|
+
`"${appName}" not found`,
|
|
83
|
+
"lookup is case-insensitive — check spelling"
|
|
84
|
+
));
|
|
62
85
|
} else {
|
|
63
|
-
console.error(
|
|
86
|
+
console.error(ui.errBlock(body?.error ?? `recover failed (${res.status})`));
|
|
64
87
|
}
|
|
88
|
+
console.log(ui.blank());
|
|
65
89
|
process.exit(1);
|
|
66
90
|
}
|
|
67
91
|
|
|
@@ -74,9 +98,15 @@ export const recoverCommand = new Command("recover")
|
|
|
74
98
|
"utf-8"
|
|
75
99
|
);
|
|
76
100
|
|
|
77
|
-
console.log();
|
|
78
|
-
console.log(
|
|
79
|
-
console.log(
|
|
80
|
-
console.log(
|
|
81
|
-
console.log();
|
|
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());
|
|
82
108
|
});
|
|
109
|
+
|
|
110
|
+
function sanitiseName(name) {
|
|
111
|
+
return name.replace(/[^a-zA-Z0-9]/g, "");
|
|
112
|
+
}
|
package/src/commands/whoami.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { requireApiUrl } from "../utils/config.js";
|
|
3
|
-
import
|
|
3
|
+
import ui from "../utils/colors.js";
|
|
4
4
|
|
|
5
5
|
export const whoamiCommand = new Command("whoami")
|
|
6
6
|
.description("Show configured Suron API URL")
|
|
7
7
|
.action(() => {
|
|
8
8
|
const apiUrl = requireApiUrl();
|
|
9
|
-
console.log();
|
|
10
|
-
console.log(
|
|
11
|
-
console.log(
|
|
12
|
-
console.log();
|
|
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());
|
|
13
17
|
});
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { loginCommand } from "./commands/login.js";
|
|
6
6
|
import { initCommand } from "./commands/init.js";
|
|
7
|
+
import { encryptCommand } from "./commands/encrypt.js";
|
|
7
8
|
import { whoamiCommand } from "./commands/whoami.js";
|
|
8
9
|
import { recoverCommand } from "./commands/recover.js";
|
|
9
10
|
|
|
@@ -19,6 +20,7 @@ program
|
|
|
19
20
|
|
|
20
21
|
program.addCommand(loginCommand);
|
|
21
22
|
program.addCommand(initCommand);
|
|
23
|
+
program.addCommand(encryptCommand);
|
|
22
24
|
program.addCommand(whoamiCommand);
|
|
23
25
|
program.addCommand(recoverCommand);
|
|
24
26
|
|
package/src/utils/colors.js
CHANGED
|
@@ -1,11 +1,181 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SURON — Terminal Design System
|
|
3
|
+
*
|
|
4
|
+
* Aesthetic: Dieter Rams × Teenage Engineering
|
|
5
|
+
* Principle: Less, but better. Every character earns its place.
|
|
6
|
+
*
|
|
7
|
+
* Palette:
|
|
8
|
+
* Amber — primary signal, values, focus #FFB347 → \x1b[38;5;214m
|
|
9
|
+
* Warm white — labels, structural text \x1b[97m
|
|
10
|
+
* Slate — secondary / dim \x1b[2m
|
|
11
|
+
* Green — success / done \x1b[38;5;71m
|
|
12
|
+
* Red — error / fail \x1b[38;5;167m
|
|
13
|
+
* No blue. No purple. No cyan.
|
|
14
|
+
*
|
|
15
|
+
* Grid: all content lives at a 2-space left indent.
|
|
16
|
+
* Rule width: 58 chars of content (60 total with indent).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const ESC = "\x1b[";
|
|
20
|
+
const R = "\x1b[0m";
|
|
21
|
+
|
|
22
|
+
// Core palette
|
|
23
|
+
const amber = (s) => `${ESC}38;5;214m${s}${R}`;
|
|
24
|
+
const warm = (s) => `${ESC}97m${s}${R}`;
|
|
25
|
+
const slate = (s) => `${ESC}2m${s}${R}`;
|
|
26
|
+
const green = (s) => `${ESC}38;5;71m${s}${R}`;
|
|
27
|
+
const red = (s) => `${ESC}38;5;167m${s}${R}`;
|
|
28
|
+
const bold = (s) => `${ESC}1m${s}${R}`;
|
|
29
|
+
const italic = (s) => `${ESC}3m${s}${R}`;
|
|
30
|
+
|
|
31
|
+
// Semantic aliases
|
|
32
|
+
const value = amber; // user-supplied values, app names, URLs
|
|
33
|
+
const label = slate; // column headers, static labels
|
|
34
|
+
const ok = green; // success states
|
|
35
|
+
const fail = red; // error states
|
|
36
|
+
const em = warm; // emphasis within a line
|
|
37
|
+
|
|
38
|
+
// ── Structural primitives ────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const INDENT = " ";
|
|
41
|
+
const COL_W = 58;
|
|
42
|
+
|
|
43
|
+
/** Horizontal rule — thin, full-width */
|
|
44
|
+
const hr = () => INDENT + slate("─".repeat(COL_W));
|
|
45
|
+
|
|
46
|
+
/** Thick rule — section separator */
|
|
47
|
+
const hrThick = () => INDENT + slate("━".repeat(COL_W));
|
|
48
|
+
|
|
49
|
+
/** Blank line */
|
|
50
|
+
const blank = () => "";
|
|
51
|
+
|
|
52
|
+
/** Section header — sparse, uppercase, no decoration */
|
|
53
|
+
const header = (text) =>
|
|
54
|
+
INDENT + bold(warm(text.toUpperCase()));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Key-value row — strict two-column layout
|
|
58
|
+
* key is left-padded to `keyWidth` (default 14), value is amber
|
|
59
|
+
* @param {string} key
|
|
60
|
+
* @param {string} val
|
|
61
|
+
* @param {number} [keyWidth=14]
|
|
62
|
+
*/
|
|
63
|
+
const kv = (key, val, keyWidth = 14) =>
|
|
64
|
+
INDENT + label(key.padEnd(keyWidth)) + value(val);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Status row — used in step lists
|
|
68
|
+
* States: "pending" | "run" | "done" | "skip" | "fail"
|
|
69
|
+
* @param {"pending"|"run"|"done"|"skip"|"fail"} state
|
|
70
|
+
* @param {string} text
|
|
71
|
+
* @param {string} [note]
|
|
72
|
+
*/
|
|
73
|
+
const STATUS_GLYPHS = {
|
|
74
|
+
pending: slate("·"),
|
|
75
|
+
run: amber("▶"),
|
|
76
|
+
done: green("✔"),
|
|
77
|
+
skip: slate("○"),
|
|
78
|
+
fail: red("✘"),
|
|
79
|
+
};
|
|
80
|
+
const STATUS_LABELS = {
|
|
81
|
+
pending: slate("···"),
|
|
82
|
+
run: amber("RUN"),
|
|
83
|
+
done: green("OK "),
|
|
84
|
+
skip: slate("SKP"),
|
|
85
|
+
fail: red("ERR"),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const step = (state, text, note) => {
|
|
89
|
+
const glyph = STATUS_GLYPHS[state] ?? STATUS_GLYPHS.pending;
|
|
90
|
+
const status = STATUS_LABELS[state] ?? STATUS_LABELS.pending;
|
|
91
|
+
const body = state === "fail" ? red(text)
|
|
92
|
+
: state === "done" ? slate(text)
|
|
93
|
+
: state === "skip" ? slate(text)
|
|
94
|
+
: state === "run" ? warm(text)
|
|
95
|
+
: slate(text);
|
|
96
|
+
const tail = note ? " " + slate(italic(note)) : "";
|
|
97
|
+
return INDENT + glyph + " " + status + " " + body + tail;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Inline prompt indicator — shown before user input
|
|
102
|
+
* @param {string} question
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
const promptLine = (question) =>
|
|
106
|
+
INDENT + amber("▸") + " " + warm(question) + " " + slate("›") + " ";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Error block — tight, no wasted space
|
|
110
|
+
* @param {string} msg
|
|
111
|
+
* @param {string} [hint]
|
|
112
|
+
*/
|
|
113
|
+
const errBlock = (msg, hint) => {
|
|
114
|
+
const lines = [INDENT + red("✘") + " " + red(msg)];
|
|
115
|
+
if (hint) lines.push(INDENT + " " + slate(hint));
|
|
116
|
+
return lines.join("\n");
|
|
9
117
|
};
|
|
10
118
|
|
|
11
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Success block
|
|
121
|
+
* @param {string} msg
|
|
122
|
+
*/
|
|
123
|
+
const okBlock = (msg) =>
|
|
124
|
+
INDENT + green("✔") + " " + ok(msg);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Info block (neutral)
|
|
128
|
+
* @param {string} msg
|
|
129
|
+
*/
|
|
130
|
+
const infoBlock = (msg) =>
|
|
131
|
+
INDENT + slate("·") + " " + slate(msg);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Code / path snippet — amber, stencil feel
|
|
135
|
+
* @param {string} text
|
|
136
|
+
*/
|
|
137
|
+
const code = (text) =>
|
|
138
|
+
amber(text);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Diff line — used in entry-point patching
|
|
142
|
+
* @param {"remove"|"add"|"unknown"} type
|
|
143
|
+
* @param {string} text
|
|
144
|
+
*/
|
|
145
|
+
const diff = (type, text) => {
|
|
146
|
+
if (type === "remove") return INDENT + " " + red("−") + " " + slate(text);
|
|
147
|
+
if (type === "add") return INDENT + " " + green("+") + " " + amber(text);
|
|
148
|
+
return INDENT + " " + slate("?") + " " + slate("(manual update required)");
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Logo / wordmark — sparse, dot-matrix feel
|
|
153
|
+
* Only shown once per session (login/init headers).
|
|
154
|
+
*/
|
|
155
|
+
const logo = () => [
|
|
156
|
+
blank(),
|
|
157
|
+
INDENT + bold(warm("SURON")) + " " + slate("· secrets delivery"),
|
|
158
|
+
].join("\n");
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Final "ready" block shown at end of init/encrypt
|
|
162
|
+
* @param {Array<{label:string, note:string}>} facts
|
|
163
|
+
*/
|
|
164
|
+
const readyBlock = (facts) => {
|
|
165
|
+
const lines = facts.map(f =>
|
|
166
|
+
INDENT + green("·") + " " + label(f.label.padEnd(16)) + slate(f.note)
|
|
167
|
+
);
|
|
168
|
+
lines.push(blank());
|
|
169
|
+
lines.push(INDENT + green("▶") + " " + bold(ok("READY")));
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default {
|
|
174
|
+
// palette
|
|
175
|
+
amber, warm, slate, green, red, bold, italic, value, label, ok, fail, em,
|
|
176
|
+
// layout
|
|
177
|
+
hr, hrThick, blank, header, kv, step, promptLine, errBlock, okBlock,
|
|
178
|
+
infoBlock, code, diff, logo, readyBlock,
|
|
179
|
+
// constants
|
|
180
|
+
INDENT, COL_W,
|
|
181
|
+
};
|
package/src/utils/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { homedir } from "os";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
4
4
|
import readline from "readline";
|
|
5
|
-
import
|
|
5
|
+
import ui from "./colors.js";
|
|
6
6
|
|
|
7
7
|
const CONFIG_PATH = join(homedir(), ".suron-config");
|
|
8
8
|
|
|
@@ -49,7 +49,9 @@ export function getApiUrl() {
|
|
|
49
49
|
export function requireApiUrl() {
|
|
50
50
|
const url = getApiUrl();
|
|
51
51
|
if (!url) {
|
|
52
|
-
console.error(
|
|
52
|
+
console.error(ui.blank());
|
|
53
|
+
console.error(ui.errBlock("not configured", "run: suron login"));
|
|
54
|
+
console.error(ui.blank());
|
|
53
55
|
process.exit(1);
|
|
54
56
|
}
|
|
55
57
|
return url;
|
package/src/utils/dotenvx.js
CHANGED
|
@@ -1,25 +1,73 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
+
*
|
|
10
56
|
* @param {string} cwd
|
|
11
57
|
*/
|
|
12
58
|
export function encryptDotenv(cwd) {
|
|
13
59
|
if (!existsSync(join(cwd, ".env"))) {
|
|
14
60
|
throw new Error(`.env not found in ${cwd}`);
|
|
15
61
|
}
|
|
62
|
+
|
|
16
63
|
// Strip all dotenvx keypair vars from the child environment.
|
|
17
|
-
// If dotenvx sees DOTENV_PRIVATE_KEY
|
|
18
|
-
// it
|
|
19
|
-
// breaks readPrivateKey() immediately after.
|
|
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.
|
|
20
66
|
const env = { ...process.env };
|
|
21
67
|
for (const k of Object.keys(env)) {
|
|
22
|
-
if (k.startsWith("DOTENV_PRIVATE_KEY") || k.startsWith("DOTENV_PUBLIC_KEY"))
|
|
68
|
+
if (k.startsWith("DOTENV_PRIVATE_KEY") || k.startsWith("DOTENV_PUBLIC_KEY")) {
|
|
69
|
+
delete env[k];
|
|
70
|
+
}
|
|
23
71
|
}
|
|
24
72
|
|
|
25
73
|
try {
|
|
@@ -35,6 +83,9 @@ export function encryptDotenv(cwd) {
|
|
|
35
83
|
if (stdout) process.stdout.write(stdout);
|
|
36
84
|
throw new Error(`dotenvx encrypt failed: ${stderr || err}`);
|
|
37
85
|
}
|
|
86
|
+
|
|
87
|
+
// Always strip the banner dotenvx injects on every encrypt call.
|
|
88
|
+
cleanDotenvComments(cwd);
|
|
38
89
|
}
|
|
39
90
|
|
|
40
91
|
/**
|
|
@@ -57,5 +108,3 @@ export function readPrivateKey(cwd) {
|
|
|
57
108
|
}
|
|
58
109
|
return match[1].trim();
|
|
59
110
|
}
|
|
60
|
-
|
|
61
|
-
|