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.

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

## 0.3.9

- **`genclass` opens its prompt straight away**: commands and questions, with one line on where this folder's setup
stands. `npx genclass` does the same once the folder is set up (signed in, GenClass in the code, the app in the
account), and walks the setup only before that. `genclass setup` walks it any time, to add or change a part.
- **The installed `genclass` stays current**: a run through npx replaces an older installed copy with its own version,
and the installed copy asks npm once a day and updates itself (`npm install -g genclass@<version>`; a failure says
how by hand and waits a day). After the first install, zsh is told to `rehash` if the window does not see it yet.
- **A coding agent following `status --json` finishes**: every step it can do says `npx genclass setup --yes` (it
said `npx genclass init`, which writes nothing without a terminal, so an agent ran it again and again), and it is
never pointed at a step `setup --yes` cannot do.
- **An app your plan already covers is done**: no third browser page right after paying, nothing to click, and
setup never stops there. Its plan page is in `genclass plan`.
- **Free sees the Pro card once**: later commands say in one line that they come with Pro; the prompt lists what
works on Free (upgrade, profile, plan, status); the card says the price once.
- **Fewer keys in setup**: Enter on a step is its yes, so the Vite settings and the coding agent's files are shown
and written without a second (Y/n) (the runtime's own diff is still asked about). Suggestions in setup show the
first in full and the rest by title, and Enter takes the first choice of the hand-off.
- **Signed out says so**: after `logout`, `genclass` says to sign in again (it said the folder was not set up), and
the price is not quoted to someone signing in again. `plan`, `profile`, `team` and `apps` start the sign-in on a
terminal, as the others do. `login` (in the prompt too) is the setup's own sign-in screen. A sign-in denied twice
ends with how to sign in later.
- **`login` in a folder says what is next in genclass's words**: an app already in the account needs nothing (it
hears which plan it is on), an app without GenClass is pointed at `setup`, which shows its change first; only an
app with GenClass in its code and not in the account is offered `connect`. "<app> is on your Pro plan" is said
once, when the app reaches the account, never before a view.
- One line where there were three: q at Pro's card in setup, and setup inside the prompt after the card was seen
("Setup goes on with Pro"); setup stopped inside the prompt says to type setup. On Free, `plan` ends with what Pro
adds, and neither `status` nor `setup` is followed by the Free line. `events` names each kind as `problems` does.
A long Waiting line wraps instead of being cut; a step that does not apply has no command in `status --json`.
- The prompt: `y` runs what "Did you mean" named, "show problems" is `problems`, a number no command has says so.
`dashboard` in a web app without GenClass asks which app instead of showing another one. `fix` names each problem
in the dashboard's words too. The git hint is one command a line.
- 58 columns: a wrong option, the sign-in's lines and `--version` (now two lines) fit; a label and a link go on two
lines when one is too wide; setup's notes follow `COLUMNS` without a terminal.
- A team member's `plan --json` has the team's Pro as `plan` (the account's own Free is `own_plan`), with its apps,
history and what it includes. A grant team says "set up by GenClass" everywhere, never that the owner pays.
- A sign-in that did not finish ends with how to get a new code; the sign-in step names the host it opens.

## 0.3.8

Fixes from a signed-in walk of 0.3.7 against a fresh account, a team and a grant:
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.8",
"version": "0.3.9",
"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
31 changes: 25 additions & 6 deletions packages/cli/src/actions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export async function signIn(o = {}) {
export async function choosePlan(state, o = {}, deps = {}) {
const cli = state.steps.find((s) => s.id === "plan")?.cli;
if (!cli || cli.paid) return "skipped";
return (await (await import("./paywall.mjs")).startPro(cli, state.account?.offer ?? null, o, deps)) ? "done" : "quit";
// seen once already in this process (the prompt): one line, not the card again
return (await (await import("./paywall.mjs")).startPro(cli, state.account?.offer ?? null, { ...o, again: "Setup goes on with Pro." }, deps)) ? "done" : "quit";
}

/** This app's plan page, in the walk (state.mjs, step "app-plan"): projectPlan with the step's offer. */
Expand Down Expand Up @@ -167,6 +168,16 @@ export async function planNow(o = {}, deps = {}, id = null) {
return "locked";
}
if (o.json) return offer.current ? "covered" : "open";
// covered by the account's plan: it has its plan, so nothing opens and nothing waits (its page is in `genclass plan`).
// Said once, right after the app reached the account (o.announce: login, connect, init, apps new), never before
// a view (the 0.3.9 user stories: four times in one session)
if (offer.current && !offer.needs) {
if (o.announce) {
out();
note(`${clean(app.name, 40)} is on your ${clean(offer.current, 20)} plan.`);
}
return "covered";
}
if (o.yes || !interactive()) {
out();
// an app the account's plan covers has a plan: it goes on with it once, on its page
Expand Down Expand Up @@ -266,7 +277,7 @@ async function waitForPlan(offer, o = {}, deps = {}) {
if (got === "kept") sp.stop("ok", `${app} goes on with ${clean(offer.current, 20)}.`);
else if (got === "paid") sp.stop("ok", `${offer.plan.name} is active${app ? ` for ${app}` : ""}. Thank you.`);
else if (got === "free") sp.stop("ok", `${app} is on ${offer.free.name} for now (limited).`);
else sp.stop("warn", app && !offer.needs ? "No choice yet." : `No payment yet. ${offer.plan.name} starts once it is started on Stripe.`);
else sp.stop("warn", app && !offer.needs ? "No choice yet." : `No payment yet: ${clean(offer.plan.name, 20)} is not started.`);
return got;
}

Expand Down Expand Up @@ -364,7 +375,9 @@ export async function addViteSettings(state, o = {}) {
note('Without them "vite build" fails.');
out();
printChange(plan.change);
if (!o.yes) {
// in the walk (o.confirmed) the step's Enter was the yes: adding GenClass, whose build fails without these two
// lines, or the step "Add two Vite settings" itself (the 0.3.9 user stories: one yes per step, not two)
if (!o.yes && !o.confirmed) {
if (!interactive()) {
note(`Not a terminal: run ${c.cyan("npx genclass setup --yes")} to write them.`);
return "skipped";
Expand Down Expand Up @@ -461,7 +474,10 @@ export async function addWorkflow(state, o = {}) {
}
good(`Wrote ${c.bold(WORKFLOW_PATH)} ${c.gray(`(new file, ${text.split("\n").length - 1} lines)`)}`);
note("It is not committed. When you are ready:");
out(` ${c.cyan(`git add ${WORKFLOW_PATH} && git commit -m "Add GenClass to pull requests" && git push`)}`);
// one command a line, so each fits a narrow terminal and can be copied alone
out(` ${c.cyan(`git add ${WORKFLOW_PATH}`)}`);
out(` ${c.cyan('git commit -m "Add GenClass to pull requests"')}`);
out(` ${c.cyan("git push")}`);
return "done";
}

Expand Down Expand Up @@ -616,12 +632,15 @@ export async function connectAgent(state, o = {}) {
out(` ${c.cyan(`"genclass": ${JSON.stringify(mcpServer())}`)}`);
return "skipped";
}
// what is written is shown and asked first, as the runtime step's change is (in the walk too); --yes writes it
// what is written is shown first; it is asked for on its own, and in the walk the step's Enter was the yes (its
// sentence says what it adds, as the workflow step's does); --yes writes it
if (!o.yes) {
out();
printChange(plan.change);
out(` ${c.gray(`And ${SKILL_PATH}: how the agent uses GenClass's tools (a file you changed is left alone).`)}`);
note(`And ${SKILL_PATH}: how the agent uses GenClass's tools (a file you changed is left alone).`);
out();
}
if (!o.yes && !o.confirmed) {
if (!interactive()) {
note(`Not a terminal: run ${c.cyan("npx genclass setup --yes")} to write it.`);
return "skipped";
Expand Down
13 changes: 8 additions & 5 deletions packages/cli/src/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { c, interactive, out, sym } from "../../runtime/bin/lib/ui.mjs";
import { openBrowser, planNow, signIn } from "./actions.mjs";
import { fetchApp, signInIsOld } from "./api.mjs";
import { planName, readState } from "./state.mjs";
import { ago, bad, clean, header, note, proBox, PROMPT, typed, visible, warn, width, wrap } from "./tui.mjs";
import { ago, bad, clean, header, hint, linkLine, note, proBox, PROMPT, typed, visible, warn, width, wrap } from "./tui.mjs";

/** The views a Free app does not have (the server sends their data to Pro only), and what the Pro box says about each. */
const PRO_VIEWS = {
Expand Down Expand Up @@ -166,7 +166,7 @@ export function render(app, state, o = {}) {
const sg = state?.steps?.find((s) => s.id === "suggestions");
if (sg) part("Suggestions", sg.detail ? (sg.state === "done" ? clean(sg.detail, 60) : `${clean(sg.detail, 60)}${c.gray(" · npx genclass suggest")}`) : c.gray("not run yet: npx genclass suggest"));
out();
if (app.dashboardUrl) out(` ${c.gray("In the browser")} ${c.cyan(app.dashboardUrl)}`);
if (app.dashboardUrl) linkLine("In the browser", app.dashboardUrl);
if (!all) out(` ${c.gray("Every row:")} ${c.cyan("npx genclass dashboard --all")}`);
out();
}
Expand Down Expand Up @@ -234,12 +234,15 @@ export async function pickApp(o = {}, deps = {}, name = "dashboard") {
const projects = state.projects;
// which app: --app by id or by name, else this folder's, else the account's only one
const byId = o.app ? projects.filter((p) => p.id === o.app) : [];
const wanted = byId.length ? byId : o.app ? projects.filter((p) => p.name.toLowerCase() === String(o.app).toLowerCase()) : state.app.token ? projects.filter((p) => p.token === state.app.token) : projects.length === 1 ? projects : [];
// a web app here without GenClass is not the account's only app: on a terminal it is asked, not shown as if it
// were this one (the 0.3.9 user stories: a second folder showed the first app's numbers)
const unsetHere = !state.app.token && state.app.isApp && !o.json && !o.yes && interactive();
const wanted = byId.length ? byId : o.app ? projects.filter((p) => p.name.toLowerCase() === String(o.app).toLowerCase()) : state.app.token ? projects.filter((p) => p.token === state.app.token) : projects.length === 1 && !unsetHere ? projects : [];
// on a terminal: the apps as numbered choices, and the number picks one
if (wanted.length !== 1 && projects.length && !o.json && !o.yes && interactive()) {
const list = (wanted.length > 1 ? wanted : projects).slice(0, 30);
header({ app: state.app.name, framework: state.app.framework, account: state.account?.label, plan: planName(state), host: state.host });
note(wanted.length > 1 ? `More than one app is named "${clean(o.app, 40)}". Which one?` : o.app ? `No app named "${clean(o.app, 40)}". Which one?` : state.app.token ? "This folder's app is not in your account yet. Which app?" : "Which app?");
note(wanted.length > 1 ? `More than one app is named "${clean(o.app, 40)}". Which one?` : o.app ? `No app named "${clean(o.app, 40)}". Which one?` : state.app.token ? "This folder's app is not in your account yet. Which app?" : unsetHere ? `GenClass is not in this folder's app yet: ${hint("setup")} to add it. Which app do you want to see?` : "Which app?");
list.forEach((p, i) => out(` ${c.gray(String(i + 1).padStart(2))} ${c.bold(clean(appLabel(p, projects), 34))}${p.last_event ? c.gray(` last page ${ago(p.last_event, deps.now)}`) : ""}`));
out();
const a = await typed(` ${PROMPT()} ${c.gray("a number ·")} ${c.bold("q")} ${c.gray("stop")} `);
Expand Down Expand Up @@ -319,7 +322,7 @@ export async function appView(o = {}, deps = {}, view) {
if (app.reauth) warn(`This sign-in is from before the command could show dashboards. Run ${c.cyan("npx genclass login")} once, then ${c.cyan(`npx genclass ${view.name}`)}.`);
else if (app.missing) note("This genclass.dev cannot show a dashboard in the terminal yet.");
else bad(`Could not read the dashboard: ${clean(app.reason, 200)}`);
if (target.dashboard_url) out(` ${c.gray("In the browser")} ${c.cyan(target.dashboard_url)}`);
if (target.dashboard_url) linkLine("In the browser", target.dashboard_url);
else out(` ${c.gray("Your apps")} ${c.cyan(hostUrl("/account"))}`);
out();
}
Expand Down
Loading
Loading