@suronai/cli 0.1.32 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/commands/init.js +113 -80
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suronai/cli",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "CLI for Suron — suron login, init, whoami, recover",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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("\n" + c.bold(" suron init") + " — " + c.dim(cwd) + "\n");
18
+ console.log();
17
19
 
18
20
  if (existsSync(join(cwd, ".suron.json"))) {
19
- console.error(" " + c.red("✗") + " .suron.json already exists — this app is already initialised.");
20
- console.error(" To restore a lost config, run: " + c.bold("suron recover") + "\n");
21
+ 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 in current directory");
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(` App name [${c.dim(sanitiseName(suggested))}] › `);
39
+ const raw = await prompt(" App name " + c.dim("· ") + c.dim("[" + suggested + "] › "));
39
40
  appName = sanitiseName(raw || suggested);
40
41
  }
41
42
 
42
- // ── Encrypt ───────────────────────────────────────────────────────────────
43
- console.log("\n " + c.dim("Encrypting .env..."));
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
- // ── Register ──────────────────────────────────────────────────────────────
61
- console.log(" " + c.dim("Registering app..."));
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: "inherit" });
154
+ execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "pipe" });
155
+ steps.sdk.status = "done";
117
156
  } catch {
118
- console.error(" " + c.yellow("⚠") + " SDK install failed — run manually: npm install @suronai/sdk");
157
+ steps.sdk.status = "fail";
158
+ steps.sdk.note = "run: npm install @suronai/sdk";
119
159
  }
120
160
  } else {
121
- console.log(" " + c.dim("@suronai/sdk already installed — skipped."));
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.green("") + " .env encrypted " + c.dim("(safe to commit)"));
131
- console.log(" " + c.green("") + " .env.keys kept " + c.dim("(gitignored)") );
132
- console.log(" " + c.green("") + " .suron.json written " + c.dim("(safe to commit)"));
133
- console.log(" " + c.green("") + " @suronai/sdk installed");
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 .env.keys is in .gitignore.
139
- * Creates .gitignore if it doesn't exist.
140
- * Appends the entry if it's not already present (exact line match).
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 = readFileSync(gitignorePath, "utf-8");
204
+ const content = readFileSync(gitignorePath, "utf-8");
158
205
  const existing = new Set(content.split("\n").map(l => l.trim()));
159
- const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
160
- if (missing.length === 0) return;
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("\n Add to your app entry point:\n");
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
- // Two-pass scan:
223
- // Pass 1 — lines containing "dotenv" (import or require)
224
- // Pass 2 — bare config() / config({...}) call lines that don't contain "dotenv"
225
- // (these appear after the import was already on a separate line)
226
- const toReplace = [];
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: bare config() call only in ESM where the import and call are separate lines
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("\n Add to your app entry point:\n");
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
- const replacements = toReplace;
314
+ // ── Diff preview ───────────────────────────────────────────────────────────
315
+ console.log(" " + c.dim("▎"));
316
+ console.log(" " + c.dim("▎ ") + c.dim(relEntry));
317
+ console.log(" " + c.dim("▎"));
280
318
 
281
- // Show the diff preview
282
- for (const { content, replacement } of replacements) {
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(" " + c.green("+") + " " + c.cyan(l.trim()));
323
+ console.log(" " + c.dim("▎ ") + c.green("⁺") + " " + c.cyan(l.trim()));
287
324
  }
288
325
  } else {
289
- console.log(" " + c.yellow("?") + " " + c.dim("(no automatic replacement — update manually)"));
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(" Apply replacements? [" + c.green("Y") + "/n] › ");
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(" " + c.dim("Skipped.") + " Add manually:\n");
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 — walk lines, replace only the matched dotenv lines, touch nothing else
304
- const indexMap = new Map(replacements.map(r => [r.index, r]));
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
- // For ESM: if await config() isn't already right after the imports, move it there
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 = outLines.findIndex(l => /^\s*await config\s*\(\s*\);?\s*$/.test(l));
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 !== lastImportIndex + 1 && callLineIndex !== lastImportIndex + 2) {
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(" " + c.cyan("import") + " { config } from " + c.green("'@suronai/sdk'"));
344
- console.log(" " + c.cyan("await") + " config()");
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(" const { config } = " + c.cyan("require") + "(" + c.green("'@suronai/sdk'") + ")");
347
- console.log(" " + c.cyan("await") + " config()");
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
  }