From 48194bb05a3259a67ca38ad326d13ce0a880f246 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 30 Jul 2026 18:43:15 +0000 Subject: [PATCH] fix(pwa): the CLI hint printed commands that no longer run The "Connect the CLI" card handed out: LOGICSRC_API=https://app.logicsrc.com logicsrc login logicsrc teams push prod --env .env logicsrc teams pull prod --env .env Two things are wrong with that, and both survived a release. Since #109 addressed vaults as , push and pull take three positionals. The hint passes two, so pasting it exits with a missing- argument error -- the card is not merely stale, it is broken. The LOGICSRC_API prefix sets the variable to the value the CLI already defaults to (DEFAULT_API_URL, #107), so on the hosted app it does nothing while reading like a required step. It is now emitted only when the origin is not the default, which is the case it exists for: self-hosting. `--env .env` is dropped for the same reason -- it restates the option's own default, and sitting next to the new positional it made one flag and one argument look like the same thing. Same stale two-argument form fixed in the post-install hint (install.sh) and the accept-invite message, and in the empty-vault-list prompt on the card. CLI_HINT moves to src/lib/cli-hint.mjs so a test can assert on the rendered commands without standing up express and the database, matching how the other lib-level views are covered. The tests pin the argument count rather than the prose: restyling the card stays free, dropping an argument does not. apps/pwa: 13/13 pass. Co-Authored-By: Claude Opus 5 --- apps/logicsrc-web/public/install.sh | 3 +- apps/pwa/src/lib/cli-hint.mjs | 30 +++++++++++++++++ apps/pwa/src/routes/pages.mjs | 11 ++----- apps/pwa/test/cli-hint.test.mjs | 51 +++++++++++++++++++++++++++++ packages/cli/src/teams.ts | 2 +- 5 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 apps/pwa/src/lib/cli-hint.mjs create mode 100644 apps/pwa/test/cli-hint.test.mjs diff --git a/apps/logicsrc-web/public/install.sh b/apps/logicsrc-web/public/install.sh index 649c381..c5b5ffb 100755 --- a/apps/logicsrc-web/public/install.sh +++ b/apps/logicsrc-web/public/install.sh @@ -109,7 +109,8 @@ EOF *":$LOGICSRC_BIN:"*) : ;; *) warn "add $LOGICSRC_BIN to your PATH: export PATH=\"$LOGICSRC_BIN:\$PATH\"";; esac - printf '\n%s🔐 logicsrc installed.%s Next:\n logicsrc login\n logicsrc teams push prod --env .env\n\n' "$G" "$X" + # -- three positionals. Two exits with a usage error. + printf '\n%s🔐 logicsrc installed.%s Next:\n logicsrc login\n logicsrc teams push \n\n' "$G" "$X" } do_uninstall() { diff --git a/apps/pwa/src/lib/cli-hint.mjs b/apps/pwa/src/lib/cli-hint.mjs new file mode 100644 index 0000000..ac3ccb3 --- /dev/null +++ b/apps/pwa/src/lib/cli-hint.mjs @@ -0,0 +1,30 @@ +// The "Connect the CLI" card on the teams dashboard. +// +// It lives here rather than beside the page that renders it because it hands +// out commands people paste into a shell, and commands that are wrong are worse +// than absent. Keeping it dep-free is what lets a test assert on the rendered +// text without standing up express and the database. +import { esc } from "./html.mjs"; + +// Mirror of DEFAULT_API_URL in plugins/credential-sharing/src/identity.ts. The +// CLI already points here on its own, so telling a user to set LOGICSRC_API to +// this exact value is a no-op that reads like a required step. Only a +// self-hosted origin needs the prefix -- keep the two values in sync. +export const CLI_DEFAULT_API = "https://app.logicsrc.com"; + +/** + * @param {string} origin - the origin this request arrived on + * @returns {string} the card's HTML + */ +// Vaults are addressed as -- three positionals. Anything +// shorter exits with "missing required argument", so a hint that omits one is +// not merely stale, it fails on paste. `--env ` is the local .env file +// and already defaults to .env; spelling it out here only invites confusion +// with the positional next to it. +export const CLI_HINT = (origin) => `
Connect the CLIend-to-end encrypted
+
+

Secrets are encrypted on your machine — decrypt them with the logicsrc CLI, never here.

+
${origin === CLI_DEFAULT_API ? "" : `LOGICSRC_API=${esc(origin)} `}logicsrc login
+logicsrc teams push <team> <project> <env>   # share
+logicsrc teams pull <team> <project> <env>   # receive
+
`; diff --git a/apps/pwa/src/routes/pages.mjs b/apps/pwa/src/routes/pages.mjs index f6fc3fd..28486a5 100644 --- a/apps/pwa/src/routes/pages.mjs +++ b/apps/pwa/src/routes/pages.mjs @@ -8,6 +8,7 @@ import { page, footer, appBar, esc } from "../lib/html.mjs"; import { requireAuth, csrfInput } from "../lib/session.mjs"; import { createApiKey, listApiKeys, revokeApiKey } from "../lib/apikey.mjs"; import { requestOrigin } from "../lib/origin.mjs"; +import { CLI_HINT } from "../lib/cli-hint.mjs"; import { config } from "../config.mjs"; export const pagesRouter = Router(); @@ -15,14 +16,6 @@ export const pagesRouter = Router(); // placeholder replaced per-request (teamCard can't see req to render csrfInput) const CSRF = "__CSRF__"; -const CLI_HINT = (origin) => `
Connect the CLIend-to-end encrypted
-
-

Secrets are encrypted on your machine — decrypt them with the logicsrc CLI, never here.

-
LOGICSRC_API=${esc(origin)} logicsrc login
-logicsrc teams push <team> prod --env .env   # share
-logicsrc teams pull <team> prod --env .env   # receive
-
`; - async function teamCard(team, uid) { const members = await all(`SELECT * FROM credshare_members WHERE team_id = ? ORDER BY created_at`, [team.id]); const me = members.find((m) => m.user_id === uid); @@ -50,7 +43,7 @@ async function teamCard(team, uid) { ` : ""}
Vaults
${vaults.length ? `${vaultRows.join("")}
VaultSecretsYour access
` - : `

No vaults yet — create one from the CLI: logicsrc teams push ${esc(team.slug)} prod

`} + : `

No vaults yet — create one from the CLI: logicsrc teams push ${esc(team.slug)} <project> <env>

`} `; } diff --git a/apps/pwa/test/cli-hint.test.mjs b/apps/pwa/test/cli-hint.test.mjs new file mode 100644 index 0000000..d429d64 --- /dev/null +++ b/apps/pwa/test/cli-hint.test.mjs @@ -0,0 +1,51 @@ +// The dashboard's "Connect the CLI" card kept printing commands that no longer +// ran. It survived two releases of drift: `logicsrc teams push prod` is +// two positionals, and since vaults became the CLI exits +// with a usage error on paste. It also told everyone to set LOGICSRC_API to the +// value the CLI already defaults to, which reads like a required step. +// +// A card that hands out commands is only useful if the commands run, so these +// pin the shape rather than the prose -- restyling the card is free, quietly +// dropping an argument is not. +import assert from "node:assert/strict"; +import test from "node:test"; + +import { CLI_HINT } from "../src/lib/cli-hint.mjs"; + +/** The commands themselves, with the entities decoded back to real syntax. */ +const commands = (origin) => + CLI_HINT(origin) + .match(/margin:0">([\s\S]*?)<\/pre>/)[1] + .replace(/</g, "<") + .replace(/>/g, ">") + .split("\n"); + +const HOSTED = "https://app.logicsrc.com"; + +test("push and pull carry all three vault positionals", () => { + for (const verb of ["push", "pull"]) { + const line = commands(HOSTED).find((l) => l.includes(`teams ${verb}`)); + assert.ok(line, `no teams ${verb} line`); + assert.match(line, /teams (push|pull) /); + // Guards the specific regression: two positionals used to be enough. + // Drop "logicsrc teams " and count only what follows. + const args = line.split("#")[0].trim().split(/\s+/).slice(3); + assert.equal(args.length, 3, `teams ${verb} needs 3 args, got ${args.join(" ")}`); + } +}); + +test("the local .env path is left at its default", () => { + // `--env ` defaults to .env in the CLI. Spelling it out next to the + // positional made two unrelated things look like one. + assert.ok(!commands(HOSTED).some((l) => l.includes("--env"))); +}); + +test("the hosted origin needs no LOGICSRC_API prefix", () => { + const login = commands(HOSTED).find((l) => l.includes("logicsrc login")); + assert.equal(login, "logicsrc login"); +}); + +test("a self-hosted origin still gets the prefix", () => { + const login = commands("http://localhost:8080").find((l) => l.includes("logicsrc login")); + assert.equal(login, "LOGICSRC_API=http://localhost:8080 logicsrc login"); +}); diff --git a/packages/cli/src/teams.ts b/packages/cli/src/teams.ts index 40d1097..f96e530 100644 --- a/packages/cli/src/teams.ts +++ b/packages/cli/src/teams.ts @@ -308,7 +308,7 @@ export async function teamsInviteAction(slug: string, email: string, options: { export async function teamsAcceptAction(token: string, format: OutputFormat): Promise { const { client } = authedClient(); const result = await client.acceptInvite(token); - console.error(`Joined ${result.team?.slug ?? "team"}. Ask a member to grant you a vault, then: logicsrc teams pull `); + console.error(`Joined ${result.team?.slug ?? "team"}. Ask a member to grant you a vault, then: logicsrc teams pull `); print({ joined: result.team?.slug ?? null }, format); }