@suronai/cli 0.1.35 → 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 +108 -105
- package/src/commands/login.js +45 -18
- package/src/commands/recover.js +44 -27
- 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
|
@@ -4,9 +4,7 @@ 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,74 +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
|
-
// Preserve the folder's original casing as the default suggestion.
|
|
34
40
|
const suggested = sanitiseName(basename(cwd) || "myapp");
|
|
35
41
|
let appName;
|
|
36
42
|
|
|
37
43
|
if (opts.name) {
|
|
38
44
|
appName = sanitiseName(opts.name);
|
|
39
45
|
} else {
|
|
40
|
-
|
|
46
|
+
process.stdout.write(ui.promptLine("app name [" + suggested + "]"));
|
|
47
|
+
const raw = await prompt("");
|
|
41
48
|
appName = sanitiseName(raw || suggested);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
if (!appName) {
|
|
45
|
-
console.
|
|
46
|
-
console.error(
|
|
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());
|
|
47
58
|
process.exit(1);
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
console.log(
|
|
51
|
-
console.log(
|
|
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());
|
|
52
67
|
|
|
53
|
-
// ── Steps
|
|
68
|
+
// ── Steps ─────────────────────────────────────────────────────────────────
|
|
54
69
|
const steps = {
|
|
55
|
-
encrypt:
|
|
56
|
-
keys:
|
|
57
|
-
gitignore:{ label: "
|
|
58
|
-
register:
|
|
59
|
-
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" },
|
|
60
75
|
};
|
|
61
76
|
|
|
62
|
-
|
|
77
|
+
const printSteps = () => {
|
|
63
78
|
for (const s of Object.values(steps)) {
|
|
64
|
-
|
|
65
|
-
: s.status === "skip" ? c.dim("○")
|
|
66
|
-
: s.status === "fail" ? c.red("●")
|
|
67
|
-
: c.dim("○");
|
|
68
|
-
const detail = s.status === "fail" ? c.red(s.detail) : c.dim(s.detail);
|
|
69
|
-
const label = s.label.padEnd(11);
|
|
70
|
-
const dots = c.dim(".".repeat(Math.max(2, 44 - label.length - s.detail.length)));
|
|
71
|
-
const note = s.note ? " " + c.dim(s.note) : "";
|
|
72
|
-
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));
|
|
73
80
|
}
|
|
74
|
-
}
|
|
81
|
+
};
|
|
75
82
|
|
|
76
83
|
// ── Encrypt ───────────────────────────────────────────────────────────────
|
|
84
|
+
steps.encrypt.status = "run";
|
|
85
|
+
steps.keys.status = "run";
|
|
77
86
|
try {
|
|
78
87
|
encryptDotenv(cwd);
|
|
79
|
-
steps.encrypt.status
|
|
80
|
-
steps.keys.status
|
|
88
|
+
steps.encrypt.status = "done";
|
|
89
|
+
steps.keys.status = "done";
|
|
81
90
|
} catch (err) {
|
|
82
91
|
steps.encrypt.status = "fail";
|
|
92
|
+
steps.keys.status = "fail";
|
|
83
93
|
printSteps();
|
|
84
|
-
console.log(
|
|
85
|
-
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());
|
|
86
99
|
process.exit(1);
|
|
87
100
|
}
|
|
88
101
|
|
|
@@ -92,17 +105,22 @@ export const initCommand = new Command("init")
|
|
|
92
105
|
} catch (err) {
|
|
93
106
|
steps.keys.status = "fail";
|
|
94
107
|
printSteps();
|
|
95
|
-
console.log(
|
|
96
|
-
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());
|
|
97
113
|
process.exit(1);
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
117
|
+
steps.gitignore.status = "run";
|
|
101
118
|
const gitignoreResult = ensureGitignore(cwd);
|
|
102
119
|
steps.gitignore.status = "done";
|
|
103
120
|
steps.gitignore.note = gitignoreResult;
|
|
104
121
|
|
|
105
122
|
// ── Register ──────────────────────────────────────────────────────────────
|
|
123
|
+
steps.register.status = "run";
|
|
106
124
|
let res;
|
|
107
125
|
try {
|
|
108
126
|
res = await fetch(`${apiUrl}/cli/register-app`, {
|
|
@@ -113,8 +131,11 @@ export const initCommand = new Command("init")
|
|
|
113
131
|
} catch (err) {
|
|
114
132
|
steps.register.status = "fail";
|
|
115
133
|
printSteps();
|
|
116
|
-
console.log(
|
|
117
|
-
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());
|
|
118
139
|
process.exit(1);
|
|
119
140
|
}
|
|
120
141
|
|
|
@@ -123,14 +144,19 @@ export const initCommand = new Command("init")
|
|
|
123
144
|
try { body = await res.json(); } catch { /* ignore */ }
|
|
124
145
|
steps.register.status = "fail";
|
|
125
146
|
printSteps();
|
|
126
|
-
console.log(
|
|
147
|
+
console.log(ui.blank());
|
|
148
|
+
console.log(ui.hr());
|
|
149
|
+
console.log(ui.blank());
|
|
127
150
|
if (res.status === 409) {
|
|
128
151
|
const canonical = body?.existing_name ?? appName;
|
|
129
|
-
console.error(
|
|
130
|
-
|
|
152
|
+
console.error(ui.errBlock(
|
|
153
|
+
`"${canonical}" already registered`,
|
|
154
|
+
`to restore a lost config › suron recover --name ${canonical}`
|
|
155
|
+
));
|
|
131
156
|
} else {
|
|
132
|
-
console.error(
|
|
157
|
+
console.error(ui.errBlock(body?.error ?? `register failed (${res.status})`));
|
|
133
158
|
}
|
|
159
|
+
console.log(ui.blank());
|
|
134
160
|
process.exit(1);
|
|
135
161
|
}
|
|
136
162
|
|
|
@@ -147,6 +173,7 @@ export const initCommand = new Command("init")
|
|
|
147
173
|
// ── Install SDK ───────────────────────────────────────────────────────────
|
|
148
174
|
const pkgJsonPath = join(cwd, "package.json");
|
|
149
175
|
let isEsm = false;
|
|
176
|
+
steps.sdk.status = "run";
|
|
150
177
|
|
|
151
178
|
if (existsSync(pkgJsonPath)) {
|
|
152
179
|
let alreadyInstalled = false;
|
|
@@ -175,51 +202,41 @@ export const initCommand = new Command("init")
|
|
|
175
202
|
}
|
|
176
203
|
|
|
177
204
|
printSteps();
|
|
178
|
-
console.log(
|
|
205
|
+
console.log(ui.blank());
|
|
206
|
+
console.log(ui.hr());
|
|
207
|
+
console.log(ui.blank());
|
|
179
208
|
|
|
180
209
|
// ── Patch entry point ─────────────────────────────────────────────────────
|
|
181
210
|
await patchEntryPoint(cwd, isEsm);
|
|
182
211
|
|
|
183
212
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
184
|
-
console.log(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
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());
|
|
190
219
|
});
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
* @param {string} cwd
|
|
196
|
-
* @returns {string} short note for the steps display
|
|
197
|
-
*/
|
|
198
|
-
const GITIGNORE_ENTRIES = [
|
|
199
|
-
"node_modules/",
|
|
200
|
-
"package-lock.json",
|
|
201
|
-
".env.keys",
|
|
202
|
-
];
|
|
221
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
const GITIGNORE_ENTRIES = ["node_modules/", "package-lock.json", ".env.keys"];
|
|
203
224
|
|
|
204
225
|
function ensureGitignore(cwd) {
|
|
205
226
|
const gitignorePath = join(cwd, ".gitignore");
|
|
206
|
-
|
|
207
227
|
if (!existsSync(gitignorePath)) {
|
|
208
228
|
writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
|
|
209
229
|
return "created";
|
|
210
230
|
}
|
|
211
|
-
|
|
212
231
|
const content = readFileSync(gitignorePath, "utf-8");
|
|
213
232
|
const existing = new Set(content.split("\n").map(l => l.trim()));
|
|
214
233
|
const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
|
|
215
|
-
if (missing.length === 0) return "
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
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");
|
|
219
237
|
return "updated";
|
|
220
238
|
}
|
|
221
239
|
|
|
222
|
-
/** @param {string} cwd @returns {string} */
|
|
223
240
|
function detectPackageManager(cwd) {
|
|
224
241
|
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
225
242
|
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
@@ -227,15 +244,12 @@ function detectPackageManager(cwd) {
|
|
|
227
244
|
return "npm";
|
|
228
245
|
}
|
|
229
246
|
|
|
230
|
-
/** @param {string} pm @returns {string} */
|
|
231
247
|
function pmAddCmd(pm) {
|
|
232
248
|
return pm === "npm" ? "install" : "add";
|
|
233
249
|
}
|
|
234
250
|
|
|
235
251
|
/**
|
|
236
|
-
* Strips everything except letters and digits while preserving
|
|
237
|
-
* The backend stores names case-sensitively for display but compares lowercase
|
|
238
|
-
* for uniqueness, so "DataHaven" and "datahaven" are the same app.
|
|
252
|
+
* Strips everything except letters and digits while preserving case.
|
|
239
253
|
* @param {string} name
|
|
240
254
|
* @returns {string}
|
|
241
255
|
*/
|
|
@@ -259,9 +273,6 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
259
273
|
}
|
|
260
274
|
|
|
261
275
|
if (!entryPath) {
|
|
262
|
-
console.log(" " + c.dim("▎"));
|
|
263
|
-
console.log(" " + c.dim("▎") + " Add to your app entry point:");
|
|
264
|
-
console.log(" " + c.dim("▎"));
|
|
265
276
|
printSnippet(isEsm);
|
|
266
277
|
return;
|
|
267
278
|
}
|
|
@@ -270,8 +281,6 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
270
281
|
try { src = readFileSync(entryPath, "utf-8"); } catch { return; }
|
|
271
282
|
|
|
272
283
|
const lines = src.split("\n");
|
|
273
|
-
|
|
274
|
-
// Pass 1 — lines containing "dotenv"
|
|
275
284
|
const toReplace = [];
|
|
276
285
|
const seenIndices = new Set();
|
|
277
286
|
|
|
@@ -295,7 +304,6 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
295
304
|
}
|
|
296
305
|
}
|
|
297
306
|
|
|
298
|
-
// Pass 2 — bare config() call (ESM only — import and call are on separate lines)
|
|
299
307
|
if (isEsm) {
|
|
300
308
|
for (let i = 0; i < lines.length; i++) {
|
|
301
309
|
if (seenIndices.has(i)) continue;
|
|
@@ -313,9 +321,6 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
if (toReplace.length === 0) {
|
|
316
|
-
console.log(" " + c.dim("▎"));
|
|
317
|
-
console.log(" " + c.dim("▎") + " Add to your app entry point:");
|
|
318
|
-
console.log(" " + c.dim("▎"));
|
|
319
324
|
printSnippet(isEsm);
|
|
320
325
|
return;
|
|
321
326
|
}
|
|
@@ -323,46 +328,42 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
323
328
|
const relEntry = relative(cwd, entryPath);
|
|
324
329
|
|
|
325
330
|
// ── Diff preview ───────────────────────────────────────────────────────────
|
|
326
|
-
console.log("
|
|
327
|
-
console.log(
|
|
328
|
-
console.log(" " + c.dim("▎"));
|
|
331
|
+
console.log(ui.kv("ENTRY", relEntry, 8));
|
|
332
|
+
console.log(ui.blank());
|
|
329
333
|
|
|
330
334
|
for (const { content, replacement } of toReplace) {
|
|
331
|
-
console.log(
|
|
335
|
+
console.log(ui.diff("remove", content.trim()));
|
|
332
336
|
if (replacement !== null) {
|
|
333
337
|
for (const l of replacement.split("\n")) {
|
|
334
|
-
console.log(
|
|
338
|
+
console.log(ui.diff("add", l.trim()));
|
|
335
339
|
}
|
|
336
340
|
} else {
|
|
337
|
-
console.log(
|
|
341
|
+
console.log(ui.diff("unknown", ""));
|
|
338
342
|
}
|
|
339
|
-
console.log(
|
|
343
|
+
console.log(ui.blank());
|
|
340
344
|
}
|
|
341
345
|
|
|
342
|
-
|
|
346
|
+
process.stdout.write(ui.promptLine("apply patch? [Y/n]"));
|
|
347
|
+
const answer = await prompt("");
|
|
343
348
|
const confirmed = answer === "" || /^y(es)?$/i.test(answer);
|
|
344
|
-
console.log(
|
|
349
|
+
console.log(ui.blank());
|
|
350
|
+
console.log(ui.hr());
|
|
351
|
+
console.log(ui.blank());
|
|
345
352
|
|
|
346
353
|
if (!confirmed) {
|
|
347
|
-
console.log();
|
|
348
|
-
console.log(" " + c.dim("skipped — add manually:"));
|
|
349
|
-
console.log();
|
|
350
354
|
printSnippet(isEsm);
|
|
351
355
|
return;
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
// Apply
|
|
355
358
|
const indexMap = new Map(toReplace.map(r => [r.index, r]));
|
|
356
359
|
const outLines = lines.map((line, i) => {
|
|
357
360
|
const r = indexMap.get(i);
|
|
358
361
|
return (r && r.replacement !== null) ? r.replacement : line;
|
|
359
362
|
});
|
|
360
363
|
|
|
361
|
-
// Move await config() to right after last import block (ESM)
|
|
362
364
|
if (isEsm) {
|
|
363
|
-
const callLineIndex
|
|
365
|
+
const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(/.test(l));
|
|
364
366
|
const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
|
|
365
|
-
|
|
366
367
|
if (callLineIndex !== -1 && callLineIndex > lastImportIndex + 2) {
|
|
367
368
|
const callLine = outLines[callLineIndex];
|
|
368
369
|
outLines.splice(callLineIndex, 1);
|
|
@@ -375,20 +376,22 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
375
376
|
try {
|
|
376
377
|
writeFileSync(entryPath, outLines.join("\n"), "utf-8");
|
|
377
378
|
} catch (err) {
|
|
378
|
-
console.error(
|
|
379
|
+
console.error(ui.errBlock("could not write " + relEntry, err.message));
|
|
379
380
|
printSnippet(isEsm);
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
383
|
|
|
383
|
-
/** @param {boolean} isEsm */
|
|
384
384
|
function printSnippet(isEsm) {
|
|
385
|
+
console.log(ui.infoBlock("add to your entry point:"));
|
|
386
|
+
console.log(ui.blank());
|
|
385
387
|
if (isEsm) {
|
|
386
|
-
console.log(
|
|
387
|
-
console.log(
|
|
388
|
+
console.log(ui.INDENT + " " + ui.amber('import { config } from "@suronai/sdk"'));
|
|
389
|
+
console.log(ui.INDENT + " " + ui.amber("await config()"));
|
|
388
390
|
} else {
|
|
389
|
-
console.log(
|
|
390
|
-
console.log(
|
|
391
|
+
console.log(ui.INDENT + " " + ui.amber('const { config } = require("@suronai/sdk")'));
|
|
392
|
+
console.log(ui.INDENT + " " + ui.amber("await config()"));
|
|
391
393
|
}
|
|
392
|
-
console.log(
|
|
393
|
-
console.log(
|
|
394
|
+
console.log(ui.blank());
|
|
395
|
+
console.log(ui.hr());
|
|
396
|
+
console.log(ui.blank());
|
|
394
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,38 +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
|
-
// Names are case-insensitive for lookup — "datahaven" finds "DataHaven".
|
|
27
|
-
// We preserve the input casing here; the backend returns the canonical name.
|
|
28
35
|
let appName;
|
|
29
36
|
if (opts.name) {
|
|
30
|
-
// Strip non-alphanumeric but preserve case — backend does case-insensitive match.
|
|
31
37
|
appName = opts.name.replace(/[^a-zA-Z0-9]/g, "");
|
|
32
|
-
console.log(" App name " + c.cyan(appName));
|
|
33
38
|
} else {
|
|
34
39
|
const suggested = sanitiseName(basename(cwd) || "myapp");
|
|
35
|
-
|
|
40
|
+
process.stdout.write(ui.promptLine("app name [" + suggested + "]"));
|
|
41
|
+
const raw = await prompt("");
|
|
36
42
|
appName = sanitiseName(raw || suggested);
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
if (!appName) {
|
|
40
|
-
console.
|
|
46
|
+
console.log(ui.blank());
|
|
47
|
+
console.error(ui.errBlock("app name required"));
|
|
48
|
+
console.log(ui.blank());
|
|
41
49
|
process.exit(1);
|
|
42
50
|
}
|
|
43
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
|
+
|
|
44
58
|
// ── Look up app ───────────────────────────────────────────────────────────
|
|
45
|
-
|
|
59
|
+
process.stdout.write(ui.INDENT + ui.slate("querying ···"));
|
|
46
60
|
|
|
47
61
|
let res;
|
|
48
62
|
try {
|
|
@@ -52,44 +66,47 @@ export const recoverCommand = new Command("recover")
|
|
|
52
66
|
body: JSON.stringify({ name: appName }),
|
|
53
67
|
});
|
|
54
68
|
} catch (err) {
|
|
55
|
-
|
|
69
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
70
|
+
console.error(ui.errBlock(`cannot reach API`, err.message));
|
|
71
|
+
console.log(ui.blank());
|
|
56
72
|
process.exit(1);
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
76
|
+
|
|
59
77
|
if (!res.ok) {
|
|
60
78
|
let body = {};
|
|
61
79
|
try { body = await res.json(); } catch { /* ignore */ }
|
|
62
80
|
if (res.status === 404) {
|
|
63
|
-
console.error(
|
|
64
|
-
|
|
81
|
+
console.error(ui.errBlock(
|
|
82
|
+
`"${appName}" not found`,
|
|
83
|
+
"lookup is case-insensitive — check spelling"
|
|
84
|
+
));
|
|
65
85
|
} else {
|
|
66
|
-
console.error(
|
|
86
|
+
console.error(ui.errBlock(body?.error ?? `recover failed (${res.status})`));
|
|
67
87
|
}
|
|
88
|
+
console.log(ui.blank());
|
|
68
89
|
process.exit(1);
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
const { app_id, name } = await res.json();
|
|
72
93
|
|
|
73
94
|
// ── Write .suron.json ─────────────────────────────────────────────────────
|
|
74
|
-
// Use the canonical name returned by the server (original registration casing).
|
|
75
95
|
writeFileSync(
|
|
76
96
|
join(cwd, ".suron.json"),
|
|
77
97
|
JSON.stringify({ app: name, id: app_id, api_url: apiUrl }, null, 2) + "\n",
|
|
78
98
|
"utf-8"
|
|
79
99
|
);
|
|
80
100
|
|
|
81
|
-
console.log();
|
|
82
|
-
console.log(
|
|
83
|
-
console.log(
|
|
84
|
-
console.log(
|
|
85
|
-
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());
|
|
86
108
|
});
|
|
87
109
|
|
|
88
|
-
/**
|
|
89
|
-
* Strips non-alphanumeric characters while preserving case.
|
|
90
|
-
* @param {string} name
|
|
91
|
-
* @returns {string}
|
|
92
|
-
*/
|
|
93
110
|
function sanitiseName(name) {
|
|
94
111
|
return name.replace(/[^a-zA-Z0-9]/g, "");
|
|
95
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
|
-
|