@suronai/cli 0.1.33 → 0.1.34
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/init.js +113 -80
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -6,6 +6,8 @@ import { requireApiUrl, prompt } from "../utils/config.js";
|
|
|
6
6
|
import { encryptDotenv, readPrivateKey } from "../utils/dotenvx.js";
|
|
7
7
|
import c from "../utils/colors.js";
|
|
8
8
|
|
|
9
|
+
const HR = " " + c.dim("─".repeat(55));
|
|
10
|
+
|
|
9
11
|
export const initCommand = new Command("init")
|
|
10
12
|
.description("Encrypt .env and register this app with Suron")
|
|
11
13
|
.option("--name <n>", "App name (skips interactive prompt)")
|
|
@@ -13,38 +15,66 @@ export const initCommand = new Command("init")
|
|
|
13
15
|
const cwd = process.cwd();
|
|
14
16
|
const apiUrl = requireApiUrl();
|
|
15
17
|
|
|
16
|
-
console.log(
|
|
18
|
+
console.log();
|
|
17
19
|
|
|
18
20
|
if (existsSync(join(cwd, ".suron.json"))) {
|
|
19
|
-
console.error(" " + c.red("✗") + " .suron.json
|
|
20
|
-
console.error(" To restore a lost config
|
|
21
|
+
console.error(" " + c.red("✗") + " already initialised — .suron.json exists");
|
|
22
|
+
console.error(" To restore a lost config: " + c.bold("suron recover") + "\n");
|
|
21
23
|
process.exit(1);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (!existsSync(join(cwd, ".env"))) {
|
|
25
|
-
console.error(" " + c.red("✗") + " .env not found
|
|
27
|
+
console.error(" " + c.red("✗") + " .env not found");
|
|
26
28
|
console.error(" Create a .env file with your secrets, then run: suron init\n");
|
|
27
29
|
process.exit(1);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
// ── App name
|
|
31
|
-
const suggested = basename(cwd) || "my-project";
|
|
32
|
+
// ── App name ──────────────────────────────────────────────────────────────
|
|
33
|
+
const suggested = sanitiseName(basename(cwd) || "my-project");
|
|
32
34
|
let appName;
|
|
33
35
|
|
|
34
36
|
if (opts.name) {
|
|
35
37
|
appName = sanitiseName(opts.name);
|
|
36
|
-
console.log(" App name " + c.cyan(appName));
|
|
37
38
|
} else {
|
|
38
|
-
const raw = await prompt(
|
|
39
|
+
const raw = await prompt(" App name " + c.dim("· ") + c.dim("[" + suggested + "] › "));
|
|
39
40
|
appName = sanitiseName(raw || suggested);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
console.log(
|
|
43
|
+
console.log(" App name " + c.dim("·") + " " + c.cyan(appName));
|
|
44
|
+
console.log(HR);
|
|
45
|
+
|
|
46
|
+
// ── Steps tracker ─────────────────────────────────────────────────────────
|
|
47
|
+
const steps = {
|
|
48
|
+
encrypt: { label: "Encrypt", detail: ".env", status: "pending" },
|
|
49
|
+
keys: { label: "Key", detail: ".env.keys", status: "pending" },
|
|
50
|
+
gitignore:{ label: "Gitignore",detail: ".gitignore", status: "pending" },
|
|
51
|
+
register: { label: "Register", detail: appName, status: "pending" },
|
|
52
|
+
sdk: { label: "SDK", detail: "@suronai/sdk", status: "pending" },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function printSteps() {
|
|
56
|
+
for (const s of Object.values(steps)) {
|
|
57
|
+
const dot = s.status === "done" ? c.green("●")
|
|
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);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
44
68
|
|
|
69
|
+
// ── Encrypt ───────────────────────────────────────────────────────────────
|
|
45
70
|
try {
|
|
46
71
|
encryptDotenv(cwd);
|
|
72
|
+
steps.encrypt.status = "done";
|
|
73
|
+
steps.keys.status = "done";
|
|
47
74
|
} catch (err) {
|
|
75
|
+
steps.encrypt.status = "fail";
|
|
76
|
+
printSteps();
|
|
77
|
+
console.log(HR);
|
|
48
78
|
console.error("\n " + c.red("✗") + " " + err.message + "\n");
|
|
49
79
|
process.exit(1);
|
|
50
80
|
}
|
|
@@ -53,13 +83,19 @@ export const initCommand = new Command("init")
|
|
|
53
83
|
try {
|
|
54
84
|
privateKey = readPrivateKey(cwd);
|
|
55
85
|
} catch (err) {
|
|
86
|
+
steps.keys.status = "fail";
|
|
87
|
+
printSteps();
|
|
88
|
+
console.log(HR);
|
|
56
89
|
console.error("\n " + c.red("✗") + " " + err.message + "\n");
|
|
57
90
|
process.exit(1);
|
|
58
91
|
}
|
|
59
92
|
|
|
60
|
-
// ──
|
|
61
|
-
|
|
93
|
+
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
94
|
+
const gitignoreResult = ensureGitignore(cwd);
|
|
95
|
+
steps.gitignore.status = "done";
|
|
96
|
+
steps.gitignore.note = gitignoreResult;
|
|
62
97
|
|
|
98
|
+
// ── Register ──────────────────────────────────────────────────────────────
|
|
63
99
|
let res;
|
|
64
100
|
try {
|
|
65
101
|
res = await fetch(`${apiUrl}/cli/register-app`, {
|
|
@@ -68,6 +104,9 @@ export const initCommand = new Command("init")
|
|
|
68
104
|
body: JSON.stringify({ name: appName, private_key: privateKey }),
|
|
69
105
|
});
|
|
70
106
|
} catch (err) {
|
|
107
|
+
steps.register.status = "fail";
|
|
108
|
+
printSteps();
|
|
109
|
+
console.log(HR);
|
|
71
110
|
console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
|
|
72
111
|
process.exit(1);
|
|
73
112
|
}
|
|
@@ -75,7 +114,9 @@ export const initCommand = new Command("init")
|
|
|
75
114
|
if (!res.ok) {
|
|
76
115
|
let body = {};
|
|
77
116
|
try { body = await res.json(); } catch { /* ignore */ }
|
|
78
|
-
|
|
117
|
+
steps.register.status = "fail";
|
|
118
|
+
printSteps();
|
|
119
|
+
console.log(HR);
|
|
79
120
|
if (res.status === 409) {
|
|
80
121
|
console.error("\n " + c.red("✗") + ` An app named "${c.cyan(appName)}" is already registered.`);
|
|
81
122
|
console.error(" If you lost .suron.json, run: " + c.bold("suron recover") + "\n");
|
|
@@ -86,9 +127,7 @@ export const initCommand = new Command("init")
|
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
const { app_id } = await res.json();
|
|
89
|
-
|
|
90
|
-
// ── .gitignore ─────────────────────────────────────────────────────────────
|
|
91
|
-
ensureGitignore(cwd);
|
|
130
|
+
steps.register.status = "done";
|
|
92
131
|
|
|
93
132
|
// ── Write .suron.json ─────────────────────────────────────────────────────
|
|
94
133
|
writeFileSync(
|
|
@@ -110,35 +149,43 @@ export const initCommand = new Command("init")
|
|
|
110
149
|
} catch { /* ignore */ }
|
|
111
150
|
|
|
112
151
|
if (!alreadyInstalled) {
|
|
113
|
-
console.log(" " + c.dim("Installing @suronai/sdk..."));
|
|
114
152
|
const pm = detectPackageManager(cwd);
|
|
115
153
|
try {
|
|
116
|
-
execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "
|
|
154
|
+
execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "pipe" });
|
|
155
|
+
steps.sdk.status = "done";
|
|
117
156
|
} catch {
|
|
118
|
-
|
|
157
|
+
steps.sdk.status = "fail";
|
|
158
|
+
steps.sdk.note = "run: npm install @suronai/sdk";
|
|
119
159
|
}
|
|
120
160
|
} else {
|
|
121
|
-
|
|
161
|
+
steps.sdk.status = "skip";
|
|
162
|
+
steps.sdk.note = "already installed";
|
|
122
163
|
}
|
|
164
|
+
} else {
|
|
165
|
+
steps.sdk.status = "skip";
|
|
166
|
+
steps.sdk.note = "no package.json";
|
|
123
167
|
}
|
|
124
168
|
|
|
169
|
+
printSteps();
|
|
170
|
+
console.log(HR);
|
|
171
|
+
|
|
125
172
|
// ── Patch entry point ─────────────────────────────────────────────────────
|
|
126
173
|
await patchEntryPoint(cwd, isEsm);
|
|
127
174
|
|
|
128
175
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
129
176
|
console.log();
|
|
130
|
-
console.log(" " + c.
|
|
131
|
-
console.log(" " + c.
|
|
132
|
-
console.log(" " + c.
|
|
133
|
-
console.log(" " + c.
|
|
177
|
+
console.log(" " + c.dim("◇") + " .env encrypted " + c.dim("safe to commit"));
|
|
178
|
+
console.log(" " + c.dim("◇") + " .env.keys " + c.dim("gitignored, keep it safe"));
|
|
179
|
+
console.log(" " + c.dim("◇") + " .suron.json " + c.dim("safe to commit"));
|
|
180
|
+
console.log(" " + c.dim("◆") + " " + c.bold("ready"));
|
|
134
181
|
console.log();
|
|
135
182
|
});
|
|
136
183
|
|
|
137
184
|
/**
|
|
138
|
-
* Ensures
|
|
139
|
-
* Creates
|
|
140
|
-
* Appends the entry if it's not already present (exact line match).
|
|
185
|
+
* Ensures standard entries are in .gitignore.
|
|
186
|
+
* Creates the file if it doesn't exist.
|
|
141
187
|
* @param {string} cwd
|
|
188
|
+
* @returns {string} short note for the steps display
|
|
142
189
|
*/
|
|
143
190
|
const GITIGNORE_ENTRIES = [
|
|
144
191
|
"node_modules/",
|
|
@@ -151,16 +198,17 @@ function ensureGitignore(cwd) {
|
|
|
151
198
|
|
|
152
199
|
if (!existsSync(gitignorePath)) {
|
|
153
200
|
writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
|
|
154
|
-
return;
|
|
201
|
+
return "created";
|
|
155
202
|
}
|
|
156
203
|
|
|
157
|
-
const content
|
|
204
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
158
205
|
const existing = new Set(content.split("\n").map(l => l.trim()));
|
|
159
|
-
const missing
|
|
160
|
-
if (missing.length === 0) return;
|
|
206
|
+
const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
|
|
207
|
+
if (missing.length === 0) return "already set";
|
|
161
208
|
|
|
162
209
|
const separator = content.endsWith("\n") ? "" : "\n";
|
|
163
210
|
writeFileSync(gitignorePath, content + separator + missing.join("\n") + "\n", "utf-8");
|
|
211
|
+
return "updated";
|
|
164
212
|
}
|
|
165
213
|
|
|
166
214
|
/** @param {string} cwd @returns {string} */
|
|
@@ -177,8 +225,6 @@ function pmAddCmd(pm) {
|
|
|
177
225
|
}
|
|
178
226
|
|
|
179
227
|
/**
|
|
180
|
-
* Strips hyphens, underscores, spaces and other non-alphanumeric characters
|
|
181
|
-
* so the app name is always a single lowercase word. e.g. "camp-haven" -> "camphaven".
|
|
182
228
|
* @param {string} name
|
|
183
229
|
* @returns {string}
|
|
184
230
|
*/
|
|
@@ -187,13 +233,6 @@ function sanitiseName(name) {
|
|
|
187
233
|
}
|
|
188
234
|
|
|
189
235
|
/**
|
|
190
|
-
* Scans the entry point line-by-line for any lines that reference "dotenv".
|
|
191
|
-
* Shows the user exactly what was found and what it will be replaced with,
|
|
192
|
-
* then asks for confirmation before writing.
|
|
193
|
-
*
|
|
194
|
-
* Works for any import/require pattern because it does a literal line search
|
|
195
|
-
* rather than trying to anticipate every possible dotenv usage style.
|
|
196
|
-
*
|
|
197
236
|
* @param {string} cwd
|
|
198
237
|
* @param {boolean} isEsm
|
|
199
238
|
*/
|
|
@@ -209,7 +248,9 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
209
248
|
}
|
|
210
249
|
|
|
211
250
|
if (!entryPath) {
|
|
212
|
-
console.log("
|
|
251
|
+
console.log(" " + c.dim("▎"));
|
|
252
|
+
console.log(" " + c.dim("▎") + " Add to your app entry point:");
|
|
253
|
+
console.log(" " + c.dim("▎"));
|
|
213
254
|
printSnippet(isEsm);
|
|
214
255
|
return;
|
|
215
256
|
}
|
|
@@ -219,11 +260,8 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
219
260
|
|
|
220
261
|
const lines = src.split("\n");
|
|
221
262
|
|
|
222
|
-
//
|
|
223
|
-
|
|
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 = [];
|
|
263
|
+
// Pass 1 — lines containing "dotenv"
|
|
264
|
+
const toReplace = [];
|
|
227
265
|
const seenIndices = new Set();
|
|
228
266
|
|
|
229
267
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -246,13 +284,12 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
246
284
|
}
|
|
247
285
|
}
|
|
248
286
|
|
|
249
|
-
// Pass 2
|
|
287
|
+
// Pass 2 — bare config() call (ESM only — import and call are on separate lines)
|
|
250
288
|
if (isEsm) {
|
|
251
289
|
for (let i = 0; i < lines.length; i++) {
|
|
252
290
|
if (seenIndices.has(i)) continue;
|
|
253
291
|
const trimmed = lines[i].trim();
|
|
254
292
|
const indent = lines[i].match(/^(\s*)/)[1];
|
|
255
|
-
// Matches: config() / config({}) / config({ ... }) — with or without semicolon
|
|
256
293
|
if (/^config\s*\(.*\);?$/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
257
294
|
toReplace.push({
|
|
258
295
|
index: i,
|
|
@@ -261,78 +298,73 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
261
298
|
});
|
|
262
299
|
}
|
|
263
300
|
}
|
|
264
|
-
// Sort by line number so the diff preview is in file order
|
|
265
301
|
toReplace.sort((a, b) => a.index - b.index);
|
|
266
302
|
}
|
|
267
303
|
|
|
268
304
|
if (toReplace.length === 0) {
|
|
269
|
-
console.log("
|
|
305
|
+
console.log(" " + c.dim("▎"));
|
|
306
|
+
console.log(" " + c.dim("▎") + " Add to your app entry point:");
|
|
307
|
+
console.log(" " + c.dim("▎"));
|
|
270
308
|
printSnippet(isEsm);
|
|
271
309
|
return;
|
|
272
310
|
}
|
|
273
311
|
|
|
274
312
|
const relEntry = entryPath.replace(cwd + "\\", "").replace(cwd + "/", "");
|
|
275
|
-
console.log();
|
|
276
|
-
console.log(" " + c.yellow("▶") + " Found dotenv in " + c.dim(relEntry) + ":");
|
|
277
|
-
console.log();
|
|
278
313
|
|
|
279
|
-
|
|
314
|
+
// ── Diff preview ───────────────────────────────────────────────────────────
|
|
315
|
+
console.log(" " + c.dim("▎"));
|
|
316
|
+
console.log(" " + c.dim("▎ ") + c.dim(relEntry));
|
|
317
|
+
console.log(" " + c.dim("▎"));
|
|
280
318
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
console.log(" " + c.red("-") + " " + c.dim(content.trim()));
|
|
319
|
+
for (const { content, replacement } of toReplace) {
|
|
320
|
+
console.log(" " + c.dim("▎ ") + c.red("⁻") + " " + c.dim(content.trim()));
|
|
284
321
|
if (replacement !== null) {
|
|
285
322
|
for (const l of replacement.split("\n")) {
|
|
286
|
-
console.log("
|
|
323
|
+
console.log(" " + c.dim("▎ ") + c.green("⁺") + " " + c.cyan(l.trim()));
|
|
287
324
|
}
|
|
288
325
|
} else {
|
|
289
|
-
console.log("
|
|
326
|
+
console.log(" " + c.dim("▎ ") + c.yellow("?") + " " + c.dim("(no automatic replacement — update manually)"));
|
|
290
327
|
}
|
|
291
|
-
console.log();
|
|
328
|
+
console.log(" " + c.dim("▎"));
|
|
292
329
|
}
|
|
293
330
|
|
|
294
|
-
const answer = await prompt("
|
|
331
|
+
const answer = await prompt(" " + c.dim("▎") + " apply? " + c.dim("[Y/n] › "));
|
|
295
332
|
const confirmed = answer === "" || /^y(es)?$/i.test(answer);
|
|
333
|
+
console.log(HR);
|
|
296
334
|
|
|
297
335
|
if (!confirmed) {
|
|
298
|
-
console.log(
|
|
336
|
+
console.log();
|
|
337
|
+
console.log(" " + c.dim("skipped — add manually:"));
|
|
338
|
+
console.log();
|
|
299
339
|
printSnippet(isEsm);
|
|
300
340
|
return;
|
|
301
341
|
}
|
|
302
342
|
|
|
303
|
-
// Apply
|
|
304
|
-
const indexMap = new Map(
|
|
343
|
+
// Apply
|
|
344
|
+
const indexMap = new Map(toReplace.map(r => [r.index, r]));
|
|
305
345
|
const outLines = lines.map((line, i) => {
|
|
306
346
|
const r = indexMap.get(i);
|
|
307
347
|
return (r && r.replacement !== null) ? r.replacement : line;
|
|
308
348
|
});
|
|
309
349
|
|
|
310
|
-
//
|
|
311
|
-
// automatically as part of the same patch — no second prompt needed.
|
|
350
|
+
// Move await config() to right after last import block (ESM)
|
|
312
351
|
if (isEsm) {
|
|
313
|
-
const callLineIndex
|
|
352
|
+
const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(/.test(l));
|
|
314
353
|
const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
|
|
315
354
|
|
|
316
|
-
if (callLineIndex !== -1 && callLineIndex
|
|
355
|
+
if (callLineIndex !== -1 && callLineIndex > lastImportIndex + 2) {
|
|
317
356
|
const callLine = outLines[callLineIndex];
|
|
318
|
-
// Remove from current position (and any adjacent blank line below it)
|
|
319
357
|
outLines.splice(callLineIndex, 1);
|
|
320
358
|
if (outLines[callLineIndex]?.trim() === "") outLines.splice(callLineIndex, 1);
|
|
321
|
-
|
|
322
|
-
// Re-find last import index after splice (indices shifted)
|
|
323
359
|
const newLastImport = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
|
|
324
|
-
|
|
325
|
-
// Insert: blank line + await config() + blank line, after last import
|
|
326
360
|
outLines.splice(newLastImport + 1, 0, "", callLine, "");
|
|
327
361
|
}
|
|
328
362
|
}
|
|
329
363
|
|
|
330
364
|
try {
|
|
331
365
|
writeFileSync(entryPath, outLines.join("\n"), "utf-8");
|
|
332
|
-
console.log(" " + c.green("✓") + " " + c.dim(relEntry) + " patched");
|
|
333
366
|
} catch (err) {
|
|
334
367
|
console.error(" " + c.red("✗") + " Could not write " + relEntry + ": " + err.message);
|
|
335
|
-
console.log(" Add manually:\n");
|
|
336
368
|
printSnippet(isEsm);
|
|
337
369
|
}
|
|
338
370
|
}
|
|
@@ -340,11 +372,12 @@ async function patchEntryPoint(cwd, isEsm) {
|
|
|
340
372
|
/** @param {boolean} isEsm */
|
|
341
373
|
function printSnippet(isEsm) {
|
|
342
374
|
if (isEsm) {
|
|
343
|
-
console.log("
|
|
344
|
-
console.log("
|
|
375
|
+
console.log(" " + c.dim("▎ ") + c.cyan("import") + " { config } from " + c.green("'@suronai/sdk'"));
|
|
376
|
+
console.log(" " + c.dim("▎ ") + c.cyan("await") + " config()");
|
|
345
377
|
} else {
|
|
346
|
-
console.log("
|
|
347
|
-
console.log("
|
|
378
|
+
console.log(" " + c.dim("▎ ") + "const { config } = " + c.cyan("require") + "(" + c.green("'@suronai/sdk'") + ")");
|
|
379
|
+
console.log(" " + c.dim("▎ ") + c.cyan("await") + " config()");
|
|
348
380
|
}
|
|
349
|
-
console.log();
|
|
381
|
+
console.log(" " + c.dim("▎"));
|
|
382
|
+
console.log(HR);
|
|
350
383
|
}
|