Skip to content

V8#26

Open
qodesmith wants to merge 451 commits into
mainfrom
v8
Open

V8#26
qodesmith wants to merge 451 commits into
mainfrom
v8

Conversation

@qodesmith

@qodesmith qodesmith commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

Complete ground-up rewrite of create-new-app from v7 → v8. The old CLI is deleted entirely and replaced with a Bun-native TypeScript CLI that generates one of two opinionated, production-ready templates.

Scope: 376 files changed, 257 added / 116 deleted / 3 modified (~20k lines in, ~17.6k lines out).

What changed

  • CLI rewrite. The Node + commander + EJS + webpack-config-generator pipeline is gone. The new CLI lives in src/cli/:
    • src/cli/index.ts — entrypoint (#!/usr/bin/env bun), prints intro/outro via @clack/prompts.
    • src/cli/options-parser.tsnode:util parseArgs wrapper.
    • src/cli/resolveProjectOptions.ts — turns raw args into validated options; prompts only for values that are missing or invalid (unless --yes).
    • src/cli/generateProject.ts — plans the file copy, replaces {{PROJECT_NAME}} / {{BETTER_AUTH_SECRET}}, applies writes, runs bun install / bunx biomeInit / git init.
  • CLI surface is intentionally small. [project-name], --type (fullstack | client-only), --yes, --help, --version. The v7 flag soup (--router, --express, --mongo, --api, --apiPort, --mongoPort, --mongoPortProd, --mongoUser, --mongoAuthSource, --browserslist, --sandbox, --offline, --force, --noGit, --title, etc.) is gone — replaced by two well-tested project shapes.
  • Bin entries. package.json exposes both create-new-app and cna, both pointing at src/cli/index.ts.
  • Two opinionated templates under src/projects/:
    • fullstack/ — React 19 + TanStack Router (file-based, type-safe) + TanStack Query + TanStack Form + Jotai on the client; Bun serve() + Hono + Drizzle ORM + bun:sqlite + LiteFS (Fly.io) + Better Auth (with @better-auth/passkey) + Resend / React Email + Sharp on the server. Ships with multi-stage Dockerfile, Dockerfile.local, fly.toml, litefs.yml, a deploy.ts script, and a Drizzle Studio build for prod.
    • client-only-react/ — React 19 SPA with the same TanStack stack and Tailwind v4 + shadcn/ui, served by Bun's bundler.
  • Templates as Bun workspace members. Root package.json declares "workspaces": ["src/projects/*"], so the templates can be developed standalone (cd src/projects/fullstack && bun install) while still being shipped as files.
  • -keep rename trick. Files/dirs that npm strips on publish (.gitignore-keep, .vscode-keep, etc.) or that Bun workspaces would over-eagerly resolve (biome.jsonc-keep, .claude-keep) live with -keep suffixes in the repo and are renamed on copy by generateProject.ts.
  • Test suite rewritten with bun:test. tests/ now contains 4 files covering CLI arg parsing, option resolution / prompting, the template planning step, and an end-to-end generate-project integration test. All v7 Jest tests under tests/unit/*.js are deleted.
  • Tooling. Biome (@qodestack/biome-config) replaces Prettier; bun build --compile produces a standalone binary for distribution alongside the npm package; TypeScript 6.
  • Old surface removed in full. main.js, all of modules/, all of file-creators/, all of files/ (the v7 webpack/EJS template tree including Express, MongoDB, React Router, and sandbox files), checkDependencies.js, removeTestFolders.js, versionCheck.js, .npmignore, .prettierignore, .watchmanconfig. All Express/Mongo/sandbox/webpack code paths are gone.
  • README.md rewritten to reflect the new architecture, the two-template model, and the new CLI surface. Instructions are Bun-only (bun install -g, bunx); the npm version badge is preserved.

Migration note

This is a hard break from v7. Anyone wanting the legacy Express/MongoDB/sandbox CLI should pin to v7. v8 is a different tool published under the same name.

Test plan

  • bun install at the repo root succeeds
  • bun test — all 4 test files pass (cli.test.ts, resolve-project-options.test.ts, plan-template.test.ts, generate-project.integration.test.ts)
  • bun run check — Biome passes clean
  • bun run buildbun build --compile produces a working dist/create-new-app binary
  • bun run dev (i.e. running the CLI from source) with no args → guided prompts ask for name + type, then generate
  • bun run dev my-app -t fullstack -y → no prompts, generates fullstack template
  • bun run dev my-app -t client-only -y → no prompts, generates client-only template
  • Invalid name (MyApp, 1app, -app) → validation error or re-prompt
  • Invalid type with --yes → exits with error
  • --help and --version print and exit 0
  • Generated fullstack project: bun dev boots, browser opens, auth flows work, Drizzle Studio reachable via bun run db:view
  • Generated client-only project: bun dev boots and renders
  • git init runs in the generated project (and CLI logs a warning rather than failing if git is unavailable)
  • {{PROJECT_NAME}} placeholders are replaced everywhere (package.json, fly.toml, litefs.yml, Dockerfile, env files)
  • A fresh BETTER_AUTH_SECRET is generated per run for fullstack projects
  • -keep files/dirs are renamed on copy (e.g. .gitignore-keep.gitignore, .claude-keep/.claude/)

…late for the source sqlite database file - dev.sqlite was an old thing
qodesmith added 2 commits May 24, 2026 08:49
Replaces the inline preview with a Dialog that uses react-easy-crop for
square round-shape cropping. Confirm renders the crop region to a canvas,
encodes as webp, and feeds the existing upload mutation; the server still
handles the final resize and quality reduction.
qodesmith added 27 commits May 25, 2026 08:32
Avoids noise in the audit tables by only inserting a purge record when
at least one row was actually deleted.
shared/types.d.ts imports FileRouteTypes from client/routeTree.gen,
which transitively pulls every client route and its components into the
typecheck:shared and typecheck:server programs. Those tsconfigs only
aliased @/shared and @/server, so the dragged-in client files failed to
resolve their own @/client/* imports. Add @/client/* to both, with
comments explaining the import chain.
Eight sequenced feature specs for building out the admin portal on top
of Better Auth. 00 lays the foundation (sectioned /admin layout,
AdminSection wrapper, ConfirmDialog primitive) that 01-07 build on:
users table, full CRUD, impersonation, ban/unban, role management,
session management, and admin-set passwords. Each doc lists its
dependencies, the Better Auth APIs it uses, files to create/modify,
and acceptance criteria.
Restructure /admin from a 2-column AccountCard grid into a sectioned
layout (Users on top, System below) so it can host the upcoming users
table while keeping the existing Backup and Purge cards intact. Add
the shared primitives features 01-07 depend on:

- AdminSection: colocated wrapper with title + optional description and
  right-aligned action slot, reused by both sections.
- ConfirmDialog: controlled AlertDialog wrapper using LoadingButton;
  consumer drives open + isPending from a mutation so async confirms
  don't auto-close mid-flight. Supports default and destructive variants
  and an optional body slot for inline form controls.
- Install shadcn alert-dialog (declined to overwrite local button.tsx).

Exercise ConfirmDialog by gating the Purge Stale Records action behind
a destructive confirm.
Adds a paginated, searchable, sortable Users table to the Users section
of /admin — the host surface for every per-user row action that features
02–07 will plug into.

Behavior
- Server-side search (debounced 250ms) with a field switch between
  email/name, using better-auth's listUsers `searchOperator: 'contains'`.
- Server-side sort by clicking the Name / Email / Role / Created column
  headers; resets to page 1 on sort change.
- Pagination via Prev/Next + "Page X of Y", with 10/25/50 page-size
  selector. `offset = (page - 1) * pageSize`, `limit = pageSize`.
- `placeholderData: keepPreviousData` keeps the previous page visible
  during refetches so pagination/sort don't flash empty.
- Banned rows are muted with a red Banned badge; hovering the badge
  reveals reason + remaining time (or Permanent / Expired).
- Admin users are visually distinct (cyan name + cyan role badge) so the
  viewer knows `user:impersonate-admins` will be required later.
- Verified emails show a green BadgeCheck with a tooltip.

State
- Query key: `['admin', 'users', { searchValue, searchField, sortBy,
  sortDirection, page, pageSize }]` — features 02–07 invalidate
  `['admin', 'users']` on mutation success.
- `currentUser.id` is threaded into `UserRowActions` so future
  destructive items can hide themselves on the signed-in row.

Files
- Add  src/client/routes/_authenticated/admin/-UsersTable.tsx — table +
  header controls (search, field switch, page-size, pagination).
  Wraps in a local TooltipProvider since no app-root provider exists.
- Add  src/client/routes/_authenticated/admin/-usersTableColumns.tsx —
  column defs, SortableHeader, ban-expiry formatter, and the
  `TableUser` type derived from `authClient.admin.listUsers`'s return.
- Add  src/client/routes/_authenticated/admin/-UserRowActions.tsx —
  "..." dropdown trigger with a single disabled placeholder item that
  features 02–07 will replace.
- Modify src/client/routes/_authenticated/admin/route.lazy.tsx — mount
  <UsersTable /> in the Users section and add a placeholder
  "+ Create user" button to the section's action slot (toast for now;
  feature 02 wires the dialog).

Dependencies
- Add @tanstack/react-table.
- Install shadcn primitives: table, dropdown-menu, tooltip, select.

Verification
- `bun run typecheck` clean.
- `bunx biome check src/client/routes/_authenticated/admin/` clean.
- No new knip findings.
- Not yet smoke-tested in a browser.
Adds Create / Edit / Delete on user records for admins, all backed by
Better Auth's admin plugin endpoints (no custom Hono routes).

Create (+ Create user button in the Users section header)
  - New CreateUserDialog with email, first/last name, password, and role.
  - Password field reuses PasswordInput (show/hide) plus a Generate
    button that fills a 16-char crypto-random password from a 70-char
    pool, and a copy-to-clipboard button that surfaces only after
    generation. Password value never appears in toasts or logs.
  - Role Select defaults to "user"; options inlined as a tuple that
    mirrors userRoles in src/server/constants.ts (client cannot import
    from @/server).
  - Calls authClient.admin.createUser with lastName under data.
  - On success: toast, invalidate ['admin','users'], reset, close.

Edit (row action: Edit user)
  - New EditUserDialog editing name and lastName only. Email is not
    editable here -- Better Auth's two-step changeEmail flow belongs on
    the user's own account page.
  - Pre-populates from row data; uses key={user.id} on an inner form
    subcomponent so switching rows re-initializes defaults cleanly.
  - "Nothing to change" warning when both fields are unchanged.

Delete (row action: Delete user, destructive)
  - Hidden when user.id === currentUser.id.
  - ConfirmDialog with explicit copy noting the hard delete cascades to
    sessions / accounts / passkeys / verification rows and suggests Ban
    (plan 04) as the reversible alternative.
  - Calls authClient.admin.removeUser.

Shared validators
  - Lifted emailValidator, passwordValidator, nameValidator from inside
    the auth hook into src/shared/validators.ts so client form
    validation and server hook validation share the exact same rules.

ErrorContext
  - Added client:adminCreateUser:exception,
    client:adminUpdateUser:exception, client:adminRemoveUser:exception
    for centralized client-error logging via useLogClientError.

Verified with bun run typecheck (clean across shared/client/server/root)
and biome check on the touched files. Not yet exercised in a browser.
Lets admins masquerade as another user via a row-action in the users
table, and surfaces a sticky banner across the authenticated app so the
admin always knows they're impersonating and can exit in one click.

Server
- src/server/db/auth/auth.ts: explicitly set `impersonationSessionDuration`
  on the admin plugin to 60 * 60 (1 hour) so the security choice is
  visible in code review rather than relying on the default.

Client
- src/client/components/ImpersonationBanner.tsx (new): sticky amber
  banner driven by `authClient.useSession()`. Renders only when
  `session.session.impersonatedBy` is truthy. The "Stop impersonating"
  button calls `authClient.admin.stopImpersonating()`, then
  `router.invalidate()` and navigates to `/admin` to restore the admin's
  original view.
- src/client/routes/_authenticated.tsx: mounts <ImpersonationBanner />
  above the <Outlet /> so the banner is global across every
  authenticated page.
- src/client/routes/_authenticated/admin/-UserRowActions.tsx: adds the
  "Impersonate" menu item. Hidden for self-impersonation. Disabled when
  the target is an admin and the current admin lacks the
  `user:impersonate-admins` permission (checked client-side via
  `authClient.admin.checkRolePermission`, no network call). Confirming
  in the dialog calls `authClient.admin.impersonateUser({ userId })`,
  invalidates the router, and navigates to `defaultAuthedPath`.
- src/shared/types.d.ts: adds `client:adminImpersonateUser:exception`
  and `client:adminStopImpersonating:exception` to the `ErrorContext`
  union for centralized client error logging.

Notes
- The banner relies on the admin plugin extending the session row with
  `impersonatedBy`; `auth.$Infer.Session` already carries it, so no
  manual type regeneration was required.
- `stopImpersonating` (not session revoke) is the correct way to exit;
  revoking the impersonation session row directly would prevent the
  admin's original session from being restored.
Admins can ban a user with an optional reason and duration (1h, 1d, 1w,
30d, custom hours/days, or permanent), or unban a banned user. Banning
auto-revokes the user's active sessions and blocks subsequent sign-in
with the configured `bannedUserMessage`. The row menu swaps Ban for
Unban based on `user.banned`, and both are hidden on the admin's own row.
Adds a Change role row action that opens a ConfirmDialog with a Select
body. Calls authClient.admin.setRole and invalidates ['admin', 'users'].

- Self is hidden in the row menu to avoid single-admin lockout.
- Last-admin guard runs twice: proactively via useQuery so the dialog
  can render a stronger warning before submit, and authoritatively in
  the confirm handler to close the TOCTOU window.
- Demoting an admin shows a destructive-styled warning in the
  description; if they're the last admin, the warning escalates and the
  confirm is blocked with a toast.

To honor the plan's "do NOT hardcode role strings" while respecting the
biome rule that forbids @/client -> @/server imports, userRoles moves
from src/server/constants.ts to src/shared/constants.ts. All five
server import sites are updated, and CreateUserDialog drops its inline
mirror to use the shared source.

Skips the optional refreshUserSessions endpoint: cookieCache.enabled is
false (auth.ts:262), so role changes are visible on the next request.

Bundled: biome check --write . formatted a few unrelated shadcn UI files
(textarea, tooltip, table, select, dropdown-menu) and -BanUserDialog
to project style.
Adds a "Manage sessions" row action that opens a dialog listing every
active session for the target user. Admins can revoke individual sessions
or all sessions at once via a nested confirmation. Impersonation sessions
and the viewer's own session are visually distinguished to discourage
accidental self-logout.
Adds an admin-driven manual recovery path for setting a user's password
directly, separate from the email-based reset flow.

New `-SetPasswordDialog.tsx`:
- Calls `authClient.admin.setUserPassword`, then conditionally
  `revokeUserSessions` — Better Auth's setUserPassword does NOT revoke
  existing sessions on its own, so the "Also sign user out of all
  devices" checkbox defaults to checked.
- Password generator (16-char, crypto.getRandomValues) with show/hide
  toggle and copy-to-clipboard, mirroring CreateUserDialog.
- Real-time "Passwords do not match" validation via tanstack-form's
  onChangeListenTo. Submit gated on `length >= minPasswordLength` and
  matching confirm.
- After a successful submit the dialog is fully locked until the admin
  ticks "I've saved this password": the X is hidden via
  `showCloseButton={false}` and ESC / outside-click / overlay-pointer
  are all preventDefault'd. Lock state lives in the outer dialog
  component so every close path runs through the same guard.
- Success toast is the literal "Password updated" — the password value
  never appears in toasts, console output, or logClientError context.
- Invalidates `['admin', 'users']` and
  `['admin', 'users', userId, 'sessions']` on success.
- If revokeUserSessions fails after a successful password set, surfaces
  a distinct "Password updated, but failed to revoke sessions" toast so
  the partial state isn't silently swallowed.

`-UserRowActions.tsx`:
- Adds a "Set password" dropdown item between "Manage sessions" and the
  ban items, hidden when `isSelf` (admins should change their own
  password through the normal account flow, which preserves session
  continuity).

`shared/types.d.ts`:
- Adds `client:adminSetUserPassword:exception` to the ErrorContext
  union for `useLogClientError`.
Extract the initials logic into src/client/lib/utils.ts and use it from
both userInitialsAtom and the admin users table, replacing the
table-local initialsFor utility.
Add a `serverValidationErrorCode` marker that the server attaches to every
`APIError` thrown from the Better Auth `before` hook. The client's new
`getSafeAuthErrorMessage` helper surfaces the error message only when this
marker is present — distinguishing our own validation messages from
library-generated ones (e.g. "User already exists") that would leak
implementation details. All auth forms now use the helper instead of
hardcoded fallback strings.

Also switch the admin dialogs from the locally-derived `TableUser` type to
the shared `User` type, removing the now-unnecessary re-derivation from
`AuthClient['admin']['listUsers']`.
Replaces the users-table-specific UsersSortDirection with a generic
SortDirection in src/shared/types.d.ts, reusable by any future sortable table.
Adds null as a third sort state so clicking a sorted-desc column
removes the sort rather than snapping back to asc. Also fixes a
side-effect-in-updater bug where setSortDirection was called inside
setSortBy's pure updater, which React StrictMode could invoke out
of order.
Extract the inline search debounce in UsersTable into a reusable
useDebouncedValue hook. The optional onDebounced callback fires only when
the debounced value actually changes (via an Object.is guard against the
previous value), so it does not fire on mount or on no-op re-settles.
Switch the delete-account confirmation input to type=text (so Bitwarden's
keyword/type heuristics don't detect it) and reproduce the native bullet
masking with CSS -webkit-text-security. Add typeOverride and placeholder
props to PasswordInput so all other password fields stay native, and drop
the PasswordManagerHint from the delete dialog.
Surface the admin and system audit logs as paginated, sortable,
filterable tables in the admin route.

Server:
- Add GET /admin-audit-logs and /system-audit-logs endpoints with
  arktype-validated query params (page, pageSize, sortBy, sortDirection,
  action). Numerics are coerced from query strings via morphs; sorting
  and filtering on `action` reach into the JSON metadata column via
  SQLite's `->>` operator. Admin logs inner-join their acting user.
- Add GET /avatar/:userId to serve an arbitrary user's avatar, mirroring
  the authenticated handler (weak ETag, 304 revalidation, no-cache).

Client:
- Add AdminAuditLogsTable and SystemAuditLogsTable with their column
  defs, wired into route.lazy via new AdminSection blocks.
- Extract SortableHeader into its own component, shared with the users
  table.
- getUserInitials now takes Pick<User, 'name' | 'lastName'>.

Shared:
- Add arrayOfAll helper for exhaustive, type-checked literal arrays.
- Add adminAuditLogActions / systemAuditLogActions as the single source
  of truth for the audit log action filters.

Tests:
- Add adminRoutes integration tests plus a bunfig preload that sets the
  env vars server modules read at module-load time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant