@useswarm/cli
Advanced tools
@@ -76,2 +76,4 @@ import { Command } from "commander"; | ||
| .option("--cookies-stdin", "Read cookies JSON from stdin (for scripting)") | ||
| .option("--signup-email <email>", "Base email for sign-up flow — each persona gets a unique +<hash>@ alias") | ||
| .option("--signup-password <password>", "Optional shared password for sign-up forms (insecure — prefer interactive prompt). If omitted, the runtime generates a random per-persona password.") | ||
| .option("--start-url <url>", "URL the agent lands on after auth (recommended with --cookies — the post-auth dashboard)") | ||
@@ -206,2 +208,4 @@ .option("--max-steps <n>", "Maximum steps per agent (default: 30)", "30") | ||
| let startUrl = opts.startUrl; | ||
| let signupEmail = opts.signupEmail; | ||
| let signupPassword = opts.signupPassword; | ||
| if (opts.passwordStdin) { | ||
@@ -246,7 +250,10 @@ const chunks = []; | ||
| } | ||
| if (!authUsername && !authPassword && !cookies && interactive) { | ||
| const wantAuth = await promptConfirm(chalk.bold("\n Does this test require authentication?"), false); | ||
| if (!authUsername && !authPassword && !cookies && !signupEmail && interactive) { | ||
| // Auto-suggest sign-up mode when the goal text describes one. | ||
| const looksLikeSignupGoal = /\b(sign[\s-]?up|signup|register|create (?:an? )?(?:new )?account|new account|onboard(?:ing)?)\b/i.test(goal); | ||
| const wantAuth = await promptConfirm(chalk.bold("\n Does this test require authentication?"), looksLikeSignupGoal); | ||
| if (wantAuth) { | ||
| const authChoice = await promptSelect(chalk.bold(" Auth method:"), [ | ||
| { label: "Login with username + password", value: "login" }, | ||
| { label: "Sign-up — generate a unique +<hash> alias per persona from one base email", value: "signup" }, | ||
| { label: "Inject session cookies (skip login)", value: "cookies" }, | ||
@@ -280,2 +287,19 @@ ]); | ||
| } | ||
| else if (authChoice === "signup") { | ||
| signupEmail = await prompt(chalk.bold(" Base email ") + chalk.dim("(e.g. you@yourdomain.com): ")); | ||
| if (!signupEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(signupEmail.trim())) { | ||
| console.error(chalk.red("A valid base email is required for sign-up mode.")); | ||
| process.exit(1); | ||
| } | ||
| signupEmail = signupEmail.trim(); | ||
| // Show preview so users know what their personas will sign up with. | ||
| const [local, domain] = signupEmail.split("@"); | ||
| const cleanLocal = local.split("+")[0]; | ||
| console.log(chalk.dim(` → ${cleanLocal}+<unique-hash>@${domain} (one per persona)`)); | ||
| const wantPwd = await promptConfirm(chalk.bold(" Provide a shared password for the sign-ups?") + chalk.dim(" (No = generate per-persona random)"), false); | ||
| if (wantPwd) { | ||
| signupPassword = await promptPassword(chalk.bold(" Password: ")); | ||
| } | ||
| startUrl = startUrl || await prompt(chalk.bold(" Sign-up URL ") + chalk.dim("(optional — where the form lives): ")) || undefined; | ||
| } | ||
| else { | ||
@@ -330,2 +354,8 @@ loginUrl = await prompt(chalk.bold(" Login page URL ") + chalk.dim("(optional, press Enter to skip)") + chalk.bold(": ")) || undefined; | ||
| } | ||
| else if (signupEmail) { | ||
| const [local, domain] = signupEmail.split("@"); | ||
| const cleanLocal = local.split("+")[0]; | ||
| console.log(` Auth: ${chalk.green("signup")} (${cleanLocal}+<hash>@${domain})`); | ||
| console.log(` Password: ${signupPassword ? chalk.dim("(shared, provided)") : chalk.dim("(per-persona random, not saved)")}`); | ||
| } | ||
| if (startUrl) { | ||
@@ -527,2 +557,11 @@ console.log(` Start URL: ${chalk.cyan(startUrl)}`); | ||
| } | ||
| else if (signupEmail) { | ||
| // Sign-up mode — base email is fanned out into unique +<hash>@ aliases per persona. | ||
| body.auth = { | ||
| mode: "signup", | ||
| signupEmail, | ||
| ...(signupPassword ? { signupPassword } : {}), | ||
| ...(startUrl ? { startUrl } : {}), | ||
| }; | ||
| } | ||
| else if (authUsername && authPassword) { | ||
@@ -529,0 +568,0 @@ body.auth = { |
+6
-1
| #!/usr/bin/env node | ||
| import { Command } from "commander"; | ||
| import chalk from "chalk"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { loginCommand, logoutCommand } from "./commands/login.js"; | ||
@@ -9,2 +12,4 @@ import { testCommand } from "./commands/test.js"; | ||
| import { getConfig } from "./lib/config.js"; | ||
| const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"); | ||
| const { version: pkgVersion } = JSON.parse(readFileSync(pkgPath, "utf8")); | ||
| const program = new Command(); | ||
@@ -14,3 +19,3 @@ program | ||
| .description("Swarm CLI — run AI-powered UX simulations from the terminal") | ||
| .version("0.3.0"); | ||
| .version(pkgVersion); | ||
| program.addCommand(loginCommand); | ||
@@ -17,0 +22,0 @@ program.addCommand(logoutCommand); |
+12
-2
@@ -46,4 +46,10 @@ export interface InitiateResponse { | ||
| auth?: { | ||
| /** "agent_login" (username + password) or "cookie_injection" (session cookies). Defaults to agent_login server-side. */ | ||
| mode?: "agent_login" | "cookie_injection"; | ||
| /** Auth flow: | ||
| * - "agent_login": username + password against an existing account | ||
| * - "cookie_injection": inject session cookies, skip login | ||
| * - "signup": no account yet — agent fills sign-up form using a unique | ||
| * sub-aliased email (`local+<hash>@domain`) per persona derived from | ||
| * one base email the user supplies. | ||
| * Defaults to "agent_login" server-side. */ | ||
| mode?: "agent_login" | "cookie_injection" | "signup"; | ||
| /** URL to land on after auth — recommended for cookie_injection (the post-auth dashboard). */ | ||
@@ -63,2 +69,6 @@ startUrl?: string; | ||
| }>; | ||
| signupEmail?: string; | ||
| /** Optional shared password. If omitted, the runtime mints a strong random | ||
| * one per persona-run (not persisted). */ | ||
| signupPassword?: string; | ||
| }; | ||
@@ -65,0 +75,0 @@ } |
+8
-7
| { | ||
| "name": "@useswarm/cli", | ||
| "version": "0.3.4", | ||
| "version": "0.3.5", | ||
| "description": "Swarm CLI — AI-powered UX testing from your terminal", | ||
@@ -13,2 +13,8 @@ "type": "module", | ||
| ], | ||
| "scripts": { | ||
| "dev": "tsx src/index.ts", | ||
| "build": "tsc", | ||
| "start": "node dist/index.js", | ||
| "prepublishOnly": "npm run build" | ||
| }, | ||
| "keywords": [ | ||
@@ -44,8 +50,3 @@ "ux", | ||
| "node": ">=18" | ||
| }, | ||
| "scripts": { | ||
| "dev": "tsx src/index.ts", | ||
| "build": "tsc", | ||
| "start": "node dist/index.js" | ||
| } | ||
| } | ||
| } |
+61
-1
@@ -107,2 +107,6 @@ # @useswarm/cli | ||
| Three modes — pick the one that matches what your test needs. | ||
| **Login mode** (existing account): | ||
| | Flag | Description | | ||
@@ -115,2 +119,17 @@ |------|-------------| | ||
| **Sign-up mode** (no account yet — each persona gets a unique sub-aliased email): | ||
| | Flag | Description | | ||
| |------|-------------| | ||
| | `--signup-email <email>` | Base email — each persona registers as `local+<hash>@domain` derived from this | | ||
| | `--signup-password <pass>` | Optional shared password. If omitted, a strong random password is generated per persona-run (not persisted) | | ||
| **Cookie injection** (skip login entirely): | ||
| | Flag | Description | | ||
| |------|-------------| | ||
| | `--cookies <file>` | Path to a JSON file with session cookies | | ||
| | `--cookies-stdin` | Read cookies JSON from stdin | | ||
| | `--start-url <url>` | URL the agent lands on after auth (recommended with `--cookies`) | | ||
| The interactive prompt masks password input. For scripts: | ||
@@ -122,2 +141,4 @@ | ||
| When you run `swarm test` interactively without auth flags, it offers all three modes — and auto-suggests **Sign-up** when your goal text mentions "sign up", "register", "create account", or "onboard". | ||
| #### Other | ||
@@ -225,4 +246,6 @@ | ||
| Test flows that require login: | ||
| Three modes are supported. | ||
| #### Login (existing account) | ||
| ```bash | ||
@@ -235,2 +258,39 @@ swarm test --url localhost:3000 --login-url /login --username test@example.com | ||
| #### Sign-up (one base email, N unique aliases) | ||
| For testing sign-up flows: you supply one base email, each persona registers with a unique sub-aliased address derived from it. | ||
| ```bash | ||
| swarm test --url localhost:3000 --goal "Sign up for an account" \ | ||
| --signup-email you@yourdomain.com \ | ||
| --agents 5 | ||
| # Personas register as: | ||
| # you+lwk3a8x7@yourdomain.com | ||
| # you+lwk3a902@yourdomain.com | ||
| # you+lwk3a9f1@yourdomain.com | ||
| # ... | ||
| # All confirmation emails arrive in your one inbox at you@yourdomain.com. | ||
| ``` | ||
| Each alias is generated at run time as `local+<base36(timestamp)+random>@domain`: | ||
| - The timestamp half guarantees the alias can never be replicated in the future. | ||
| - The random half disambiguates personas spawned in the same millisecond. | ||
| - The agent is told to type the exact alias verbatim — the standard login pre-fill and login tool are suppressed for sign-up runs (the agent fills the form itself). | ||
| - The generated alias is persisted to the run record so confirmation emails can be tied back to a specific persona. | ||
| Pass `--signup-password` to share one password across all personas; omit it to have the runtime mint a strong random password per persona-run (not persisted). | ||
| Plus-addressing works with Gmail, Fastmail, and most custom domains. | ||
| #### Cookie injection (skip login) | ||
| ```bash | ||
| swarm test --url localhost:3000 --goal "Explore settings" \ | ||
| --cookies ./cookies.json \ | ||
| --start-url http://localhost:3000/dashboard | ||
| ``` | ||
| Capture the cookies file with the [Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) browser extension — Export → JSON. When testing localhost, cookie domains captured from a different host are auto-rewritten to match the tunnel. | ||
| ## CI/CD Usage | ||
@@ -237,0 +297,0 @@ |
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
95952
7.06%1957
2.84%353
20.48%