@revturbine/cli 0.1.1 → 0.2.0

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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +126 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -25,6 +25,7 @@ Requires Node ≥ 20.
25
25
  ## Quick start
26
26
 
27
27
  ```bash
28
+ revturbine signup # create an account (email + password + emailed code)
28
29
  revturbine verify ./export-config.json # schema-validate locally (no network)
29
30
  revturbine login # authorize this machine (device flow)
30
31
  revturbine diff ./export-config.json # dry-run: live config vs local, no writes
@@ -38,6 +39,7 @@ revturbine deploy <change-set-id> # submit → approve → deploy (go
38
39
 
39
40
  | Command | What it does |
40
41
  |---|---|
42
+ | `signup` | Create an account headlessly: email + password, then an emailed one-time code to verify, then a token is stored. A business-email domain already claimed by a workspace exits with "an admin must invite you" (no token). |
41
43
  | `login` / `logout` | Device-flow auth; tokens stored at `~/.revturbine/credentials.json` (mode 0600). |
42
44
  | `verify` | Schema-validate a config offline against the bundled schema. |
43
45
  | `diff` | Non-destructive: download the live config, diff against local, dry-run the import. |
package/dist/cli.js CHANGED
@@ -4999,6 +4999,87 @@ async function deviceLogin(baseUrl, log = console.log) {
4999
4999
  return token;
5000
5000
  }
5001
5001
 
5002
+ // src/lib/signup.ts
5003
+ import os3 from "os";
5004
+ function trimUrl2(u) {
5005
+ return u.replace(/\/+$/, "");
5006
+ }
5007
+ async function errorMessage(res, fallback) {
5008
+ const body = await res.json().catch(() => ({}));
5009
+ const detail = Array.isArray(body.detail) ? body.detail.join("; ") : typeof body.detail === "string" ? body.detail : void 0;
5010
+ return body.message ?? detail ?? body.error ?? `${fallback} (${res.status})`;
5011
+ }
5012
+ async function startSignup(baseUrl, body, fetchImpl) {
5013
+ const res = await fetchImpl(`${trimUrl2(baseUrl)}/api/cli/signup`, {
5014
+ method: "POST",
5015
+ headers: { "Content-Type": "application/json" },
5016
+ body: JSON.stringify(body)
5017
+ });
5018
+ if (!res.ok) throw new Error(await errorMessage(res, "Signup failed"));
5019
+ return await res.json();
5020
+ }
5021
+ async function verifyOtp(baseUrl, email, otp, fetchImpl) {
5022
+ const res = await fetchImpl(`${trimUrl2(baseUrl)}/api/cli/signup/verify`, {
5023
+ method: "POST",
5024
+ headers: { "Content-Type": "application/json" },
5025
+ body: JSON.stringify({ email, otp })
5026
+ });
5027
+ if (!res.ok) throw new Error(await errorMessage(res, "Verification failed"));
5028
+ }
5029
+ async function mintSignupToken(baseUrl, email, password, label, fetchImpl) {
5030
+ const res = await fetchImpl(`${trimUrl2(baseUrl)}/api/cli/signup/token`, {
5031
+ method: "POST",
5032
+ headers: { "Content-Type": "application/json" },
5033
+ body: JSON.stringify(label ? { email, password, label } : { email, password })
5034
+ });
5035
+ if (!res.ok) throw new Error(await errorMessage(res, "Token request failed"));
5036
+ return await res.json();
5037
+ }
5038
+ async function signup(opts) {
5039
+ const log = opts.log ?? console.log;
5040
+ const fetchImpl = opts.fetchImpl ?? fetch;
5041
+ const save = opts.saveCredentialImpl ?? saveCredential;
5042
+ const maxAttempts = opts.maxOtpAttempts ?? 3;
5043
+ const outcome = await startSignup(
5044
+ opts.baseUrl,
5045
+ { name: opts.name, email: opts.email, password: opts.password },
5046
+ fetchImpl
5047
+ );
5048
+ if (outcome.outcome === "awaiting_invitation") {
5049
+ log(
5050
+ outcome.message ?? "Your email domain belongs to an existing workspace \u2014 an admin must invite you. Check your email."
5051
+ );
5052
+ return { status: "awaiting_invitation" };
5053
+ }
5054
+ log(`
5055
+ Account created. Enter the verification code emailed to ${opts.email}.`);
5056
+ for (let attempt = 1; ; attempt++) {
5057
+ const otp = (await opts.promptOtp(attempt)).trim();
5058
+ try {
5059
+ await verifyOtp(opts.baseUrl, opts.email, otp, fetchImpl);
5060
+ break;
5061
+ } catch (err) {
5062
+ if (attempt >= maxAttempts) throw err;
5063
+ log(` ${err.message} \u2014 try again (${attempt}/${maxAttempts}).`);
5064
+ }
5065
+ }
5066
+ const token = await mintSignupToken(
5067
+ opts.baseUrl,
5068
+ opts.email,
5069
+ opts.password,
5070
+ os3.hostname(),
5071
+ fetchImpl
5072
+ );
5073
+ save(opts.baseUrl, {
5074
+ token: token.access_token,
5075
+ tenant_id: token.tenant_id ?? null,
5076
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
5077
+ });
5078
+ log(`
5079
+ \u2713 Signed up and logged in. Token stored for ${opts.baseUrl} (${redactToken(token.access_token)}).`);
5080
+ return { status: "signed_in", token };
5081
+ }
5082
+
5002
5083
  // src/lib/config-diff.ts
5003
5084
  var COLLECTIONS = [
5004
5085
  "plans",
@@ -5244,6 +5325,51 @@ program.command("login").description("Authorize this machine via the browser (de
5244
5325
  process.exit(1);
5245
5326
  }
5246
5327
  });
5328
+ async function promptLine(question) {
5329
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
5330
+ try {
5331
+ return (await rl.question(question)).trim();
5332
+ } finally {
5333
+ rl.close();
5334
+ }
5335
+ }
5336
+ async function promptHidden(question) {
5337
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
5338
+ const muted = rl;
5339
+ muted._writeToOutput = (s) => {
5340
+ process.stdout.write(s.includes(question) ? s : "*");
5341
+ };
5342
+ try {
5343
+ const answer = await rl.question(question);
5344
+ process.stdout.write("\n");
5345
+ return answer.trim();
5346
+ } finally {
5347
+ rl.close();
5348
+ }
5349
+ }
5350
+ program.command("signup").description("Create a RevTurbine account and log in (email + password + emailed verification code).").argument("[url]", `RevTurbine instance URL (default: ${DEFAULT_URL})`).option("--name <name>", "Full name (prompted if omitted)").option("--email <email>", "Email address (prompted if omitted)").option("--password <password>", "Password, min 8 chars (prompted hidden if omitted)").action(async (url, opts) => {
5351
+ const baseUrl = normalizeBaseUrl(url ?? DEFAULT_URL);
5352
+ try {
5353
+ const name = opts.name ?? await promptLine("Name: ");
5354
+ const email = opts.email ?? await promptLine("Email: ");
5355
+ const password = opts.password ?? await promptHidden("Password: ");
5356
+ if (!name || !email || password.length < 8) {
5357
+ console.error(`${LOG} \u2717 Name, email, and a password of at least 8 characters are required.`);
5358
+ process.exit(1);
5359
+ }
5360
+ const result = await signup({
5361
+ baseUrl,
5362
+ name,
5363
+ email,
5364
+ password,
5365
+ promptOtp: (attempt) => promptLine(attempt > 1 ? "Verification code (try again): " : "Verification code: ")
5366
+ });
5367
+ if (result.status === "awaiting_invitation") process.exit(0);
5368
+ } catch (err) {
5369
+ console.error(`${LOG} \u2717 Signup failed: ${err.message}`);
5370
+ process.exit(1);
5371
+ }
5372
+ });
5247
5373
  program.command("logout").description("Remove the stored token for <url>.").argument("[url]", `RevTurbine instance URL (default: ${DEFAULT_URL})`).action((url) => {
5248
5374
  const normalized = normalizeBaseUrl(url ?? DEFAULT_URL);
5249
5375
  const removed = removeCredential(normalized);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revturbine/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "revturbine — verify RevTurbine ExportedConfig files and ship them to a RevTurbine instance through the Change Set lifecycle.",
5
5
  "license": "MIT",
6
6
  "repository": {