@suronai/cli 0.1.40 → 0.1.41

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suronai/cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "Suron CLI — secrets delivery",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.js CHANGED
@@ -1,24 +1,29 @@
1
1
  import { Command } from "commander";
2
- import { intro, outro, spinner, select, text, isCancel } from "@clack/prompts";
2
+ import { spinner, select, text, isCancel } from "@clack/prompts";
3
3
  import { existsSync, readFileSync, writeFileSync } from "fs";
4
- import { join } from "path";
5
- import { spawn } from "child_process";
4
+ import { join, basename, relative } from "path";
5
+ import { spawn, execSync } from "child_process";
6
6
  import ui from "./utils/colors.js";
7
7
  import { getToken, requireToken, saveConfig, clearToken } from "./utils/config.js";
8
8
  import { api, BASE_URL } from "./utils/api.js";
9
9
 
10
- function cancel(msg) {
10
+ // ── Shared helpers ────────────────────────────────────────────────────────────
11
+
12
+ function cancel(msg, hint) {
11
13
  console.log(ui.blank());
12
- console.log(ui.errBlock(msg));
14
+ console.log(ui.errBlock(msg, hint));
13
15
  console.log(ui.blank());
14
16
  process.exit(1);
15
17
  }
16
18
 
19
+ function sanitiseName(name) {
20
+ return name.replace(/[^a-zA-Z0-9]/g, "");
21
+ }
22
+
17
23
  function openBrowser(url) {
18
- const platform = process.platform;
19
- if (platform === "win32") {
24
+ if (process.platform === "win32") {
20
25
  spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
21
- } else if (platform === "darwin") {
26
+ } else if (process.platform === "darwin") {
22
27
  spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
23
28
  } else {
24
29
  spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
@@ -41,6 +46,190 @@ function waitForEnter(prompt) {
41
46
  });
42
47
  }
43
48
 
49
+ function detectPackageManager(cwd) {
50
+ if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
51
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
52
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
53
+ return "npm";
54
+ }
55
+
56
+ function pmAddCmd(pm) {
57
+ return pm === "npm" ? "install" : "add";
58
+ }
59
+
60
+ // ── .gitignore ────────────────────────────────────────────────────────────────
61
+
62
+ const GITIGNORE_ENTRIES = [".env", ".suron.json"];
63
+
64
+ function ensureGitignore(cwd) {
65
+ const gitignorePath = join(cwd, ".gitignore");
66
+ if (!existsSync(gitignorePath)) {
67
+ writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
68
+ return "created";
69
+ }
70
+ const content = readFileSync(gitignorePath, "utf-8");
71
+ const existing = new Set(content.split("\n").map(l => l.trim()));
72
+ const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
73
+ if (missing.length === 0) return "ok";
74
+ const sep = content.endsWith("\n") ? "" : "\n";
75
+ writeFileSync(gitignorePath, content + sep + missing.join("\n") + "\n", "utf-8");
76
+ return `added ${missing.join(", ")}`;
77
+ }
78
+
79
+ // ── SDK install ───────────────────────────────────────────────────────────────
80
+
81
+ function installSdk(cwd) {
82
+ const pkgJsonPath = join(cwd, "package.json");
83
+ if (!existsSync(pkgJsonPath)) return { status: "skip", note: "no package.json" };
84
+
85
+ let alreadyInstalled = false;
86
+ let isEsm = false;
87
+ try {
88
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
89
+ alreadyInstalled = !!(pkg.dependencies?.["@suronai/sdk"] || pkg.devDependencies?.["@suronai/sdk"]);
90
+ isEsm = pkg.type === "module";
91
+ } catch {
92
+ return { status: "skip", note: "could not read package.json" };
93
+ }
94
+
95
+ if (alreadyInstalled) return { status: "skip", note: "already installed", isEsm };
96
+
97
+ const pm = detectPackageManager(cwd);
98
+ try {
99
+ execSync(`${pm} ${pmAddCmd(pm)} @suronai/sdk`, { cwd, stdio: "pipe" });
100
+ return { status: "done", isEsm };
101
+ } catch {
102
+ return { status: "fail", note: `run: npm install @suronai/sdk`, isEsm };
103
+ }
104
+ }
105
+
106
+ // ── Entry-point patching ──────────────────────────────────────────────────────
107
+
108
+ async function patchEntryPoint(cwd, isEsm) {
109
+ const candidates = isEsm
110
+ ? ["index.js", "index.mjs", "src/index.js", "src/index.mjs"]
111
+ : ["index.js", "index.cjs", "src/index.js", "src/index.cjs"];
112
+
113
+ let entryPath = null;
114
+ for (const rel of candidates) {
115
+ const abs = join(cwd, rel);
116
+ if (existsSync(abs)) { entryPath = abs; break; }
117
+ }
118
+
119
+ if (!entryPath) { printSnippet(isEsm); return; }
120
+
121
+ let src;
122
+ try { src = readFileSync(entryPath, "utf-8"); } catch { return; }
123
+
124
+ const lines = src.split("\n");
125
+ const toReplace = [];
126
+ const seenIndices = new Set();
127
+
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const trimmed = lines[i].trim();
130
+ const indent = lines[i].match(/^(\s*)/)[1];
131
+
132
+ if (lines[i].includes("dotenv")) {
133
+ seenIndices.add(i);
134
+ let replacement = null;
135
+ if (isEsm) {
136
+ if (trimmed.startsWith("import")) {
137
+ replacement = indent + trimmed.replace(/(from\s+)['"]dotenv(?:\/config)?['"]/, '$1"@suronai/sdk"');
138
+ }
139
+ } else {
140
+ if (trimmed.includes("require")) {
141
+ replacement = indent + 'const { config } = require("@suronai/sdk");\n' + indent + "await config();";
142
+ }
143
+ }
144
+ toReplace.push({ index: i, content: lines[i], replacement });
145
+ }
146
+ }
147
+
148
+ if (isEsm) {
149
+ for (let i = 0; i < lines.length; i++) {
150
+ if (seenIndices.has(i)) continue;
151
+ const trimmed = lines[i].trim();
152
+ const indent = lines[i].match(/^(\s*)/)[1];
153
+ if (/^config\s*\(.*\);?$/.test(trimmed) && !trimmed.startsWith("//")) {
154
+ toReplace.push({
155
+ index: i,
156
+ content: lines[i],
157
+ replacement: indent + trimmed.replace(/^config\s*\(/, "await config("),
158
+ });
159
+ }
160
+ }
161
+ toReplace.sort((a, b) => a.index - b.index);
162
+ }
163
+
164
+ if (toReplace.length === 0) { printSnippet(isEsm); return; }
165
+
166
+ const relEntry = relative(cwd, entryPath);
167
+
168
+ console.log(ui.kv("ENTRY", relEntry, 8));
169
+ console.log(ui.blank());
170
+
171
+ for (const { content, replacement } of toReplace) {
172
+ console.log(ui.diff("remove", content.trim()));
173
+ if (replacement !== null) {
174
+ for (const l of replacement.split("\n")) console.log(ui.diff("add", l.trim()));
175
+ } else {
176
+ console.log(ui.diff("unknown", ""));
177
+ }
178
+ console.log(ui.blank());
179
+ }
180
+
181
+ await waitForEnter(ui.promptLine("apply patch? [Y/n]"));
182
+ const answer = "";
183
+ const confirmed = answer === "" || /^y(es)?$/i.test(answer);
184
+ console.log(ui.blank());
185
+ console.log(ui.hr());
186
+ console.log(ui.blank());
187
+
188
+ if (!confirmed) { printSnippet(isEsm); return; }
189
+
190
+ const indexMap = new Map(toReplace.map(r => [r.index, r]));
191
+ const outLines = lines.map((line, i) => {
192
+ const r = indexMap.get(i);
193
+ return (r && r.replacement !== null) ? r.replacement : line;
194
+ });
195
+
196
+ if (isEsm) {
197
+ const callLineIndex = outLines.findIndex(l => /^\s*await config\s*\(/.test(l));
198
+ const lastImportIndex = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
199
+ if (callLineIndex !== -1 && callLineIndex > lastImportIndex + 2) {
200
+ const callLine = outLines[callLineIndex];
201
+ outLines.splice(callLineIndex, 1);
202
+ if (outLines[callLineIndex]?.trim() === "") outLines.splice(callLineIndex, 1);
203
+ const newLastImport = outLines.reduce((last, l, i) => l.trimStart().startsWith("import ") ? i : last, -1);
204
+ outLines.splice(newLastImport + 1, 0, "", callLine, "");
205
+ }
206
+ }
207
+
208
+ try {
209
+ writeFileSync(entryPath, outLines.join("\n"), "utf-8");
210
+ } catch (err) {
211
+ console.error(ui.errBlock("could not write " + relEntry, err.message));
212
+ printSnippet(isEsm);
213
+ }
214
+ }
215
+
216
+ function printSnippet(isEsm) {
217
+ console.log(ui.infoBlock("add to your entry point:"));
218
+ console.log(ui.blank());
219
+ if (isEsm) {
220
+ console.log(ui.INDENT + " " + ui.amber('import { config } from "@suronai/sdk"'));
221
+ console.log(ui.INDENT + " " + ui.amber("await config()"));
222
+ } else {
223
+ console.log(ui.INDENT + " " + ui.amber('const { config } = require("@suronai/sdk")'));
224
+ console.log(ui.INDENT + " " + ui.amber("await config()"));
225
+ }
226
+ console.log(ui.blank());
227
+ console.log(ui.hr());
228
+ console.log(ui.blank());
229
+ }
230
+
231
+ // ── Commands ──────────────────────────────────────────────────────────────────
232
+
44
233
  export const loginCommand = new Command("login")
45
234
  .description("Authenticate via browser")
46
235
  .option("--force", "Re-authenticate even if already logged in")
@@ -69,12 +258,12 @@ export const loginCommand = new Command("login")
69
258
 
70
259
  const loginUrl = `${BASE_URL}/clilogin?code=${code}`;
71
260
 
72
- console.log(ui.infoBlock("Press Enter to open the login page in your browser..."));
261
+ console.log(ui.infoBlock("press Enter to open login in your browser"));
73
262
  console.log(ui.blank());
74
263
  console.log(ui.kv("URL", loginUrl, 6));
75
264
  console.log(ui.blank());
76
265
 
77
- await waitForEnter(" Press Enter to open ");
266
+ await waitForEnter(ui.promptLine("open browser"));
78
267
  openBrowser(loginUrl);
79
268
  console.log(ui.blank());
80
269
 
@@ -119,9 +308,7 @@ export const logoutCommand = new Command("logout")
119
308
  .description("Clear local session token")
120
309
  .action(async () => {
121
310
  clearToken();
122
- try {
123
- await api("/auth/logout", { method: "POST" });
124
- } catch {}
311
+ try { await api("/auth/logout", { method: "POST" }); } catch {}
125
312
  console.log(ui.blank());
126
313
  console.log(ui.okBlock("logged out"));
127
314
  console.log(ui.blank());
@@ -129,19 +316,21 @@ export const logoutCommand = new Command("logout")
129
316
 
130
317
  export const initCommand = new Command("init")
131
318
  .description("Initialize Suron in this project")
132
- .action(async () => {
133
- const token = requireToken();
319
+ .option("--name <n>", "App name (skips interactive prompt)")
320
+ .action(async (opts) => {
321
+ requireToken();
322
+ const cwd = process.cwd();
134
323
 
135
- if (existsSync(".suron.json")) {
324
+ if (existsSync(join(cwd, ".suron.json"))) {
136
325
  console.log(ui.blank());
137
326
  console.log(ui.errBlock(".suron.json already exists", "use suron up to push a new version"));
138
327
  console.log(ui.blank());
139
328
  process.exit(1);
140
329
  }
141
330
 
142
- if (!existsSync(".env")) {
331
+ if (!existsSync(join(cwd, ".env"))) {
143
332
  console.log(ui.blank());
144
- console.log(ui.errBlock(".env not found", "create a .env file first"));
333
+ console.log(ui.errBlock(".env not found", "create a .env file with your secrets first"));
145
334
  console.log(ui.blank());
146
335
  process.exit(1);
147
336
  }
@@ -151,40 +340,49 @@ export const initCommand = new Command("init")
151
340
  console.log(ui.hr());
152
341
  console.log(ui.blank());
153
342
 
343
+ // ── App selection ─────────────────────────────────────────────────────────
344
+
154
345
  const mode = await select({
155
346
  message: "How do you want to link this project?",
156
347
  options: [
157
- { value: "new", label: "Create new app" },
348
+ { value: "new", label: "Create new app" },
158
349
  { value: "existing", label: "Link to existing app" },
159
350
  ],
160
351
  });
161
-
162
352
  if (isCancel(mode)) cancel("cancelled");
163
353
 
164
354
  let app_id, app_name;
165
355
 
166
356
  if (mode === "new") {
167
- const name = await text({
168
- message: "App name (alphanumeric)",
169
- validate: v => /^[a-zA-Z0-9]+$/.test(v) ? undefined : "Alphanumeric only",
170
- });
171
- if (isCancel(name)) cancel("cancelled");
357
+ const suggested = sanitiseName(basename(cwd) || "myapp");
358
+ let rawName;
359
+
360
+ if (opts.name) {
361
+ rawName = opts.name;
362
+ } else {
363
+ rawName = await text({
364
+ message: `App name [${suggested}]`,
365
+ placeholder: suggested,
366
+ validate: v => {
367
+ const s = sanitiseName(v || suggested);
368
+ return s.length ? undefined : "Alphanumeric only";
369
+ },
370
+ });
371
+ if (isCancel(rawName)) cancel("cancelled");
372
+ }
373
+
374
+ const name = sanitiseName(rawName || suggested);
172
375
 
173
376
  const result = await api("/apps", {
174
377
  method: "POST",
175
378
  body: JSON.stringify({ name }),
176
379
  }).catch(err => cancel(err.message));
177
380
 
178
- app_id = result.app_id;
381
+ app_id = result.app_id;
179
382
  app_name = result.name;
180
383
  } else {
181
384
  const apps = await api("/apps").catch(err => cancel(err.message));
182
-
183
- if (!apps.length) {
184
- console.log(ui.blank());
185
- console.log(ui.errBlock("no apps found", "use 'Create new app' instead"));
186
- process.exit(1);
187
- }
385
+ if (!apps.length) cancel("no apps found", "use 'Create new app' instead");
188
386
 
189
387
  const chosen = await select({
190
388
  message: "Select an app",
@@ -193,59 +391,119 @@ export const initCommand = new Command("init")
193
391
  if (isCancel(chosen)) cancel("cancelled");
194
392
 
195
393
  const found = apps.find(a => a.app_id === chosen);
196
- app_id = found.app_id;
394
+ app_id = found.app_id;
197
395
  app_name = found.name;
198
396
  }
199
397
 
200
- const env_plaintext = readFileSync(".env", "utf-8");
398
+ console.log(ui.blank());
399
+ console.log(ui.kv("APP", app_name));
400
+ console.log(ui.kv("DIR", ui.slate(cwd)));
401
+ console.log(ui.blank());
402
+ console.log(ui.hr());
403
+ console.log(ui.blank());
404
+
405
+ // ── Step tracker ──────────────────────────────────────────────────────────
201
406
 
202
- const s = spinner();
203
- s.start("Pushing v1");
407
+ const steps = {
408
+ upload: { label: "upload .env", status: "pending" },
409
+ gitignore: { label: "gitignore .gitignore", status: "pending" },
410
+ sdk: { label: "sdk @suronai/sdk", status: "pending" },
411
+ patch: { label: "patch entry point", status: "pending" },
412
+ };
204
413
 
205
- const result = await api(`/apps/${app_id}/versions`, {
206
- method: "POST",
207
- body: JSON.stringify({ env_plaintext }),
208
- }).catch(err => { s.stop("failed"); cancel(err.message); });
414
+ const printSteps = () => {
415
+ for (const s of Object.values(steps)) {
416
+ console.log(ui.step(s.status, s.label, s.note));
417
+ }
418
+ };
209
419
 
210
- s.stop("Done");
420
+ // ── Upload ────────────────────────────────────────────────────────────────
211
421
 
212
- const suronJson = { app_name, app_id, version: result.version };
213
- writeFileSync(".suron.json", JSON.stringify(suronJson, null, 2) + "\n");
422
+ steps.upload.status = "run";
423
+ const env_plaintext = readFileSync(join(cwd, ".env"), "utf-8");
214
424
 
215
- console.log(ui.blank());
216
- console.log(ui.step("done", "encrypted and uploaded .env"));
217
- console.log(ui.step("done", `version ${result.version}`));
218
- console.log(ui.step("done", "wrote .suron.json"));
425
+ let uploadResult;
426
+ try {
427
+ uploadResult = await api(`/apps/${app_id}/versions`, {
428
+ method: "POST",
429
+ body: JSON.stringify({ env_plaintext }),
430
+ });
431
+ steps.upload.status = "done";
432
+ steps.upload.note = `v${uploadResult.version}`;
433
+ } catch (err) {
434
+ steps.upload.status = "fail";
435
+ printSteps();
436
+ console.log(ui.blank());
437
+ cancel(err.message);
438
+ }
439
+
440
+ // ── .suron.json ───────────────────────────────────────────────────────────
441
+
442
+ writeFileSync(
443
+ join(cwd, ".suron.json"),
444
+ JSON.stringify({ app_name, app_id, version: uploadResult.version }, null, 2) + "\n",
445
+ "utf-8"
446
+ );
447
+
448
+ // ── .gitignore ────────────────────────────────────────────────────────────
449
+
450
+ steps.gitignore.status = "run";
451
+ const gitResult = ensureGitignore(cwd);
452
+ steps.gitignore.status = "done";
453
+ steps.gitignore.note = gitResult;
454
+
455
+ // ── SDK install ───────────────────────────────────────────────────────────
456
+
457
+ steps.sdk.status = "run";
458
+ const sdkResult = installSdk(cwd);
459
+ steps.sdk.status = sdkResult.status;
460
+ steps.sdk.note = sdkResult.note;
461
+
462
+ // ── Print steps before patch (patch is interactive) ───────────────────────
463
+
464
+ steps.patch.status = "run";
465
+ printSteps();
219
466
  console.log(ui.blank());
220
467
  console.log(ui.hr());
221
468
  console.log(ui.blank());
222
- console.log(ui.infoBlock("add .suron.json to git · add .env to .gitignore"));
469
+
470
+ // ── Entry-point patch ─────────────────────────────────────────────────────
471
+
472
+ await patchEntryPoint(cwd, sdkResult.isEsm ?? false);
473
+ steps.patch.status = "done";
474
+
475
+ // ── Ready ─────────────────────────────────────────────────────────────────
476
+
477
+ console.log(ui.readyBlock([
478
+ { label: ".env", note: "uploaded · keep out of git" },
479
+ { label: ".suron.json", note: "safe to commit" },
480
+ { label: ".gitignore", note: gitResult },
481
+ ]));
223
482
  console.log(ui.blank());
224
483
  });
225
484
 
226
- export const upCommand = new Command("up")
227
- .description("Push a new version of .env")
485
+ export const whoamiCommand = new Command("whoami")
486
+ .description("Show the current logged-in user")
228
487
  .action(async () => {
229
488
  requireToken();
489
+ const user = await api("/auth/me").catch(err => cancel(err.message));
490
+ console.log(ui.blank());
491
+ console.log(ui.kv("EMAIL", user.email));
492
+ console.log(ui.kv("ROLE", user.role));
493
+ console.log(ui.blank());
494
+ });
230
495
 
231
- if (!existsSync(".suron.json")) {
232
- console.log(ui.blank());
233
- console.log(ui.errBlock(".suron.json not found", "run: suron init"));
234
- console.log(ui.blank());
235
- process.exit(1);
236
- }
237
-
238
- if (!existsSync(".env")) {
239
- console.log(ui.blank());
240
- console.log(ui.errBlock(".env not found"));
241
- console.log(ui.blank());
242
- process.exit(1);
243
- }
496
+ export const upCommand = new Command("up")
497
+ .action(async () => {
498
+ requireToken();
499
+ const cwd = process.cwd();
244
500
 
245
- const suronJson = JSON.parse(readFileSync(".suron.json", "utf-8"));
246
- const { app_id, app_name } = suronJson;
501
+ if (!existsSync(join(cwd, ".suron.json"))) cancel(".suron.json not found", "run: suron init");
502
+ if (!existsSync(join(cwd, ".env"))) cancel(".env not found");
247
503
 
248
- const env_plaintext = readFileSync(".env", "utf-8");
504
+ const suronJson = JSON.parse(readFileSync(join(cwd, ".suron.json"), "utf-8"));
505
+ const { app_id } = suronJson;
506
+ const env_plaintext = readFileSync(join(cwd, ".env"), "utf-8");
249
507
 
250
508
  const s = spinner();
251
509
  s.start("Pushing new version");
@@ -258,7 +516,7 @@ export const upCommand = new Command("up")
258
516
  s.stop("Done");
259
517
 
260
518
  suronJson.version = result.version;
261
- writeFileSync(".suron.json", JSON.stringify(suronJson, null, 2) + "\n");
519
+ writeFileSync(join(cwd, ".suron.json"), JSON.stringify(suronJson, null, 2) + "\n");
262
520
 
263
521
  console.log(ui.blank());
264
522
  console.log(ui.step("done", `version ${result.version} pushed`));
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { Command } from "commander";
4
4
  import { createRequire } from "module";
5
- import { loginCommand, logoutCommand, initCommand, upCommand } from "./commands.js";
5
+ import { loginCommand, logoutCommand, initCommand, upCommand, whoamiCommand } from "./commands.js";
6
6
 
7
7
  const require = createRequire(import.meta.url);
8
8
  const { version } = require("../package.json");
@@ -16,6 +16,7 @@ program
16
16
 
17
17
  program.addCommand(loginCommand);
18
18
  program.addCommand(logoutCommand);
19
+ program.addCommand(whoamiCommand);
19
20
  program.addCommand(initCommand);
20
21
  program.addCommand(upCommand);
21
22