Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 0.3.8

Fixes from a signed-in walk of 0.3.7 against a fresh account, a team and a grant:

- **A team's member sees the team's Pro**, a seat its owner pays for: `plan` shows the team's plan, whose team it is,
the team's apps, its history and what it includes (and `--json` has `team_plan` and `cli.via`); `profile` and
`team` say the same; `team invite` says invites come from the team's owner. It never says Free, or that Free is
for one person.
- **The browser opens only for someone at a terminal**: never with `--no-browser` (`dashboard` and `apps new` lost
it before), and never without a terminal (a coding agent, CI, a pipe): the link is printed instead.
- **Setup never ends "done" with the app outside the account.** A request after a package install (which blocks the
command) gets a fresh connection once if the old one was closed meanwhile; if the app still could not be added,
the runtime step says so and the setup ends with exit code 1. The advice is to run it again, not to sign in again.
- **58 columns**: `help` is laid out for the window (side by side when wide, stacked when narrow), `whoami` stacks an
app's key and link under its name when the row does not fit, and the setup's notes wrap. A wrong option to
`login`, `whoami` or `logout` says so in one line. The width follows `COLUMNS` when there is no terminal.
- A grant (set up by GenClass) is never told to add seats, and its app's plan page never holds up setup or `status`
(Continue is offered once, not required). `upgrade --json` without Pro points at the checkout, not at itself;
`profile --json` has the trial and the price; one price wording everywhere: $15 / developer / month. With
`GENCLASS_HOST`, the lines name that host.

## 0.3.7

The genclass command comes with Pro:
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genclass",
"version": "0.3.7",
"version": "0.3.8",
"description": "GenClass CLI: one command that sets GenClass up, shows your app in the terminal (problems, analytics, events), hands problems to your coding agent, answers questions about your app and tells you what it is missing.",
"type": "module",
"license": "Apache-2.0",
Expand Down
18 changes: 15 additions & 3 deletions packages/cli/src/actions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ const nap = (ms, signal) =>
);
});

/** Opens `url` in the default browser when that makes sense here. Returns whether it tried; never throws. */
/**
* Opens `url` in the default browser when that makes sense here: never with --no-browser, never without a terminal
* (a coding agent, CI, a pipe: the link is printed instead), never over SSH. Returns whether it tried; never throws.
*/
export function openBrowser(url, o = {}) {
const env = process.env;
if (o.noBrowser || env.CI || env.SSH_CONNECTION || env.SSH_TTY) return false;
if (o.noBrowser || !interactive() || env.CI || env.SSH_CONNECTION || env.SSH_TTY) return false;
if (process.platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return false;
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
try {
Expand Down Expand Up @@ -300,7 +303,16 @@ export async function addRuntime(state, o = {}) {
const code = again ? await again() : cmd === "connect" ? await runLogin("connect", args) : await runInit("init", args);
if (code !== 0) return "failed";
// init returns 0 when the person says no to its change: then there is no GenClass to add Vite settings for
if (inspectProject({ cwd: state.cwd }).fresh) return "skipped";
const after = inspectProject({ cwd: state.cwd });
if (after.fresh) return "skipped";
// signed in, GenClass is in the code but the app never reached the account (genclass.dev did not answer): the
// step did not finish, so the walk says so and ends with exit code 1 instead of "done"
const optedOut = args.some((a) => a === "--no-telemetry" || a === "--no-token" || a === "--no-network" || a === "--dry-run");
if (state.signedIn && !after.token && !optedOut) {
warn("GenClass is in your code, but the app is not in your account yet: run npx genclass again to connect it.");
await addViteSettings(state, o);
return "failed";
}
}
return (await addViteSettings(state, o)) === "failed" ? "failed" : "done";
}
Expand Down
26 changes: 23 additions & 3 deletions packages/cli/src/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export async function fetchOverview(creds, opts = {}) {
ok: true,
account,
// `via`: "team" for a member covered by a team's plan (a newer server says it; older ones do not)
plan: j.plan && typeof j.plan === "object" && str(j.plan.id, 40) ? { id: str(j.plan.id, 40), name: str(j.plan.name, 40) ?? str(j.plan.id, 40), ...(str(j.plan.via, 20) ? { via: str(j.plan.via, 20) } : {}) } : null,
// `source`: "paid", "comp" (GenClass's grant) or "open" (a newer server says it)
plan: j.plan && typeof j.plan === "object" && str(j.plan.id, 40) ? { id: str(j.plan.id, 40), name: str(j.plan.name, 40) ?? str(j.plan.id, 40), ...(str(j.plan.via, 20) ? { via: str(j.plan.via, 20) } : {}), ...(["paid", "comp", "open"].includes(j.plan.source) ? { source: j.plan.source } : {}) } : null,
suggestions: s ? { perDay: num(s.per_day), shown: num(s.shown), usedToday: num(s.used_today) ?? 0, model: s.model === true } : null,
projects,
github: j.github && typeof j.github === "object" ? { app: j.github.app === true, installUrl: sameOrigin(j.github.install_url, creds.host), connectUrl: sameOrigin(j.github.connect_url, creds.host) } : null,
Expand Down Expand Up @@ -373,8 +374,27 @@ export async function fetchAccount(creds, opts = {}) {
},
billingUrl: sameOrigin(j.billing_url, creds.host),
offer: offerOf(j.offer, creds.host),
/** the plan of a paying team the account is in (a newer server says it), else null */
teamPlan: j.team_plan && typeof j.team_plan === "object" && str(j.team_plan.id, 40) ? { id: str(j.team_plan.id, 40), name: str(j.team_plan.name, 40) ?? str(j.team_plan.id, 40) } : null,
/**
* the plan of a paying team the account is in (a newer server says it), else null: whose team, the apps it covers
* and has, its history and what it includes (a seat the team's owner pays for)
*/
teamPlan: teamPlanOf(j.team_plan),
};
}

/** A team's plan as /api/cli/account sends it (team_plan), or null. */
function teamPlanOf(t) {
if (!t || typeof t !== "object" || !str(t.id, 40)) return null;
const inc = t.includes && typeof t.includes === "object" ? t.includes : null;
return {
id: str(t.id, 40),
name: str(t.name, 40) ?? str(t.id, 40),
source: ["paid", "comp", "open"].includes(t.source) ? t.source : null,
owner: str(t.owner, 120),
apps: numOrNull(t.apps),
usedApps: Number.isInteger(t.used_apps) && t.used_apps >= 0 ? t.used_apps : null,
historyDays: numOrNull(t.history_days),
includes: inc ? { analytics: inc.analytics === true, forwarding: inc.forwarding === true, export: inc.export === true, privateRepositories: inc.private_repositories === true } : null,
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export async function appView(o = {}, deps = {}, view) {
// the account's plan covers goes on whether or not Continue was clicked; one past what a paying account's plan
// covers has said so (Enterprise, by email), and its numbers are not the plan's
if (!o.json && interactive()) {
const p = await planNow({ cwd: o.cwd, yes: o.yes }, deps, target.id);
const p = await planNow({ cwd: o.cwd, yes: o.yes, noBrowser: o.noBrowser }, deps, target.id);
if (p === "locked") {
out();
return 1;
Expand Down
132 changes: 80 additions & 52 deletions packages/cli/src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,84 @@
import { hostConfig } from "../../runtime/bin/lib/host.mjs";
import { c, err, interactive, out } from "../../runtime/bin/lib/ui.mjs";
import { BUILD } from "./assets.mjs";
import { width, wrap } from "./tui.mjs";

export const USAGE = `genclass: GenClass in your terminal. Set it up, see what it finds, have it fixed.

The genclass command comes with Pro: 3 days free, then US$15 a developer a month, with a card
on Stripe. Without Pro: login, logout, whoami, status, plan, upgrade, profile, remove, help.
Free is on the website: https://genclass.dev

npx genclass where this project stands; on a terminal it walks you through the
next step, then stays open for commands and questions
npx genclass setup the guided setup (--yes: no questions, for a coding agent)
npx genclass status what is done, what is next (--all: every part; --json)

See
npx genclass dashboard your app, whole: problems found, checks, visitors, agent, GitHub App
npx genclass problems every problem found, by kind and page, and what GenClass did about it
npx genclass analytics visitors: visits, pages, time, where they come from, what they click
(one part in full: analytics pages | sources | devices | clicks | paths | funnels)
npx genclass events the latest events, one per line (--all: each one's mode and trigger)
npx genclass health how GenClass itself runs on your pages (--all: every breakdown)
npx genclass report the period as Markdown (--out report.md)
npx genclass export every stored event, as a file (--from, --to, --format csv, --out)

Act
npx genclass fix hand a problem to your coding agent (fix 2 | fix stale | fix all;
--print, --copy, --agent claude | codex | cursor-agent | gemini)
npx genclass ask "..." ask about your app in plain words; the model answers from its numbers
npx genclass suggest [url] what your app is missing, most important first (--yes, --no-model, --all)
npx genclass scan <url> 23 visual checks on one page

Connect
npx genclass connections where alerts go (Slack, Sentry, PostHog, Datadog, a webhook), GitHub, agent
(connections add | test | on | off | remove <kind>)
npx genclass agent your coding agent: is it connected, what it did (agent link: give it
the live site's numbers)
npx genclass funnels funnels (funnels add "Name" /a /b /c | funnels remove "Name")

Account
npx genclass apps your apps (apps new "Name")
npx genclass plan your plan, its limits and what is used (--open: billing in the browser)
npx genclass upgrade start Pro (its free trial first), in the browser
npx genclass profile your account and plan, as numbered choices
npx genclass team who is on the plan (team invite)
npx genclass login sign in (logout, whoami)
npx genclass remove take GenClass out of this app again
/** The help's sections: [heading, [command, what it does]...]; usage() lays them out for the terminal's width. */
const HELP = [
[null, [
["npx genclass", "where this project stands; on a terminal it walks you through the next step, then stays open for commands and questions"],
["npx genclass setup", "the guided setup (--yes: no questions, for a coding agent)"],
["npx genclass status", "what is done, what is next (--all: every part; --json)"],
]],
["See", [
["npx genclass dashboard", "your app, whole: problems found, checks, visitors, agent, GitHub App"],
["npx genclass problems", "every problem found, by kind and page, and what GenClass did about it"],
["npx genclass analytics", "visitors: visits, pages, time, where they come from, what they click (one part in full: analytics pages | sources | devices | clicks | paths | funnels)"],
["npx genclass events", "the latest events, one per line (--all: each one's mode and trigger)"],
["npx genclass health", "how GenClass itself runs on your pages (--all: every breakdown)"],
["npx genclass report", "the period as Markdown (--out report.md)"],
["npx genclass export", "every stored event, as a file (--from, --to, --format csv, --out)"],
]],
["Act", [
["npx genclass fix", "hand a problem to your coding agent (fix 2 | fix stale | fix all; --print, --copy, --agent claude | codex | cursor-agent | gemini)"],
['npx genclass ask "..."', "ask about your app in plain words; the model answers from its numbers"],
["npx genclass suggest [url]", "what your app is missing, most important first (--yes, --no-model, --all)"],
["npx genclass scan <url>", "23 visual checks on one page"],
]],
["Connect", [
["npx genclass connections", "where alerts go (Slack, Sentry, PostHog, Datadog, a webhook), GitHub, agent (connections add | test | on | off | remove <kind>)"],
["npx genclass agent", "your coding agent: is it connected, what it did (agent link: give it the live site's numbers)"],
["npx genclass funnels", 'funnels (funnels add "Name" /a /b /c | funnels remove "Name")'],
]],
["Account", [
["npx genclass apps", 'your apps (apps new "Name")'],
["npx genclass plan", "your plan, its limits and what is used (--open: billing in the browser)"],
["npx genclass upgrade", "start Pro (its free trial first), in the browser"],
["npx genclass profile", "your account and plan, as numbered choices"],
["npx genclass team", "who is on the plan (team invite)"],
["npx genclass login", "sign in (logout, whoami)"],
["npx genclass remove", "take GenClass out of this app again"],
]],
];

Options: --app <name> --range 24h|7d|30d|90d|1y --all --json --cwd <dir> --yes
--no-browser --help --version
/**
* The help, laid out for `cols` columns (the terminal's): a command and its words side by side on a wide terminal,
* the words under the command on a narrow one (58 columns and up), every line within `cols`.
*/
export function usage(cols = width()) {
const w = Math.max(40, cols - 2);
const lines = [];
const text = (t, indent = "") => {
for (const l of wrap(t, w - indent.length)) lines.push(`${indent}${l}`);
};
text("genclass: GenClass in your terminal. Set it up, see what it finds, have it fixed.");
lines.push("");
text("The genclass command comes with Pro: 3 days free, then $15 / developer / month, with a card on Stripe. Without Pro: login, logout, whoami, status, plan, upgrade, profile, remove, help. Free is on the website: https://genclass.dev", " ");
const side = w >= 78;
const col = 30;
for (const [head, rows] of HELP) {
lines.push("");
if (head) lines.push(` ${head}`);
for (const [cmd, what] of rows) {
if (side) {
const words = wrap(what, w - col);
lines.push(` ${cmd.padEnd(col - 2)}${words[0] ?? ""}`);
for (const l of words.slice(1)) lines.push(`${" ".repeat(col)}${l}`);
} else {
lines.push(` ${cmd}`);
for (const l of wrap(what, w - 6)) lines.push(` ${l}`);
}
}
}
lines.push("");
text("Options: --app <name> --range 24h|7d|30d|90d|1y --all --json --cwd <dir> --yes --no-browser --help --version", " ");
lines.push("");
text('Run it in your web app\'s folder. npx runs it without installing; "npm install -g genclass" makes it just "genclass". Every part GenClass sets up is listed by "npx genclass status --all". Docs: https://genclass.dev/docs/cli', " ");
return lines.join("\n");
}

Run it in your web app's folder. npx runs it without installing; "npm install -g genclass" makes it just "genclass".
Every part GenClass sets up is listed by "npx genclass status --all". Docs: https://genclass.dev/docs/cli`;
/** The help at the default width (80 columns), for tests and anything that wants it as one string. */
export const USAGE = usage(80);

const BOOL = { yes: "yes", y: "yes", json: "json", "no-browser": "noBrowser", "no-model": "noModel", all: "all", help: "help", h: "help", version: "version", v: "version", open: "open", print: "print", copy: "copy" };
const VALUE = { cwd: "cwd", url: "url", app: "app", range: "range", from: "from", to: "to", format: "format", out: "out", agent: "agent" };
Expand Down Expand Up @@ -177,15 +205,15 @@ export async function run(argv, ctx = {}) {

const { o, rest, error } = parseArgs(args);
if (error) {
err(`genclass: ${error}\n\n${USAGE}`);
err(`genclass: ${error}\n\n${usage()}`);
return 2;
}
if (o.version || cmd === "version") {
out(`genclass ${BUILD.version} (installs @genclass/runtime ${BUILD.runtimeVersion}, @genclass/mcp ${BUILD.mcpVersion})`);
return 0;
}
if (o.help || cmd === "help") {
out(USAGE);
out(usage());
return 0;
}
const h = hostConfig();
Expand All @@ -210,7 +238,7 @@ export async function run(argv, ctx = {}) {
const url = o.url ?? rest.find((a) => !a.startsWith("-")) ?? null;
const unknown = rest.filter((a) => a.startsWith("-"));
if (unknown.length) {
err(`genclass suggest: unknown option ${unknown[0]}\n\n${USAGE}`);
err(`genclass suggest: unknown option ${unknown[0]}\n\n${usage()}`);
return 2;
}
const gate = await paid();
Expand All @@ -219,7 +247,7 @@ export async function run(argv, ctx = {}) {
}
if (cmd !== null && !COMMANDS.includes(cmd)) {
const near = nearest(cmd);
err(`genclass: unknown command ${c.bold(cmd)}${near ? `. Did you mean ${c.cyan(`npx genclass ${near}`)}?` : ""}\n\n${USAGE}`);
err(`genclass: unknown command ${c.bold(cmd)}${near ? `. Did you mean ${c.cyan(`npx genclass ${near}`)}?` : ""}\n\n${usage()}`);
return 2;
}
// the commands that take words after their name: sections, picks, subcommands, a question
Expand All @@ -228,7 +256,7 @@ export async function run(argv, ctx = {}) {
const WORDY = ["analytics", "fix", "ask", "connections", "agent", "funnels", "apps", "team"];
if (flags.length || (words.length && !WORDY.includes(cmd))) {
const bad = flags[0] ?? words[0];
err(`genclass: unknown ${bad.startsWith("-") ? "option" : "command"} ${c.bold(bad)}\n\n${USAGE}`);
err(`genclass: unknown ${bad.startsWith("-") ? "option" : "command"} ${c.bold(bad)}\n\n${usage()}`);
return 2;
}
// the words of the commands that take them, before anything is asked of genclass.dev
Expand Down
Loading
Loading