From fcac5692b344c6df7f8a347605fcd0b986cef057 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 2 Jul 2026 13:23:09 +0200 Subject: [PATCH 1/3] feat: scaffold starters from the seamless-templates registry Replace the hardcoded per-framework generators (react/express) and the per-framework configure step with a registry-driven template source (src/core/templates.ts). The CLI reads registry.json from the seamless-templates monorepo at a pinned ref, builds its prompts from it, downloads the selected templates, and applies each template's template.json env contract. Adding a framework is now a templates-repo change. SEAMLESS_TEMPLATES_DIR scaffolds from a local checkout; SEAMLESS_TEMPLATES_REF pins a different ref. --- .changeset/registry-driven-templates.md | 5 + AGENTS.md | 14 +- README.md | 11 +- src/commands/init.ts | 68 ++++-- src/core/configure.ts | 32 --- src/core/images.ts | 7 + src/core/templates.ts | 261 ++++++++++++++++++++++++ src/generators/backend/express.ts | 24 --- src/generators/frontend/react.ts | 23 --- src/prompts/projectSetup.ts | 58 ++++-- src/utils/repoUtils.ts | 32 --- 11 files changed, 374 insertions(+), 161 deletions(-) create mode 100644 .changeset/registry-driven-templates.md delete mode 100644 src/core/configure.ts create mode 100644 src/core/templates.ts delete mode 100644 src/generators/backend/express.ts delete mode 100644 src/generators/frontend/react.ts delete mode 100644 src/utils/repoUtils.ts diff --git a/.changeset/registry-driven-templates.md b/.changeset/registry-driven-templates.md new file mode 100644 index 0000000..944c481 --- /dev/null +++ b/.changeset/registry-driven-templates.md @@ -0,0 +1,5 @@ +--- +"seamless-cli": minor +--- + +Scaffold web and api starters from the seamless-templates registry instead of hardcoded per-framework generators. The CLI now reads the registry to build its prompts, downloads the selected templates from the templates monorepo at a pinned ref, and applies each template's env contract. Adding a new framework is a templates-repo change, not a CLI change. Set SEAMLESS_TEMPLATES_DIR to scaffold from a local checkout, or SEAMLESS_TEMPLATES_REF to pin a different ref. diff --git a/AGENTS.md b/AGENTS.md index 7c07d8f..a23d6ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,8 +23,14 @@ The entry point is [src/index.ts](src/index.ts), which dispatches to a command m ## Commands -- **init** ([src/commands/init.ts](src/commands/init.ts)) scaffolds a project from the generators in - `src/generators/*` (frontend, backend, auth, docker, config), driven by `src/prompts/`. +- **init** ([src/commands/init.ts](src/commands/init.ts)) scaffolds a project, driven by + `src/prompts/`. The web and api starters come from the registry-driven template source + ([src/core/templates.ts](src/core/templates.ts)): it reads `registry.json` from the + `fells-code/seamless-templates` monorepo (pinned by `SEAMLESS_TEMPLATES_REF` in + [src/core/images.ts](src/core/images.ts)), downloads the selected templates, and applies each + template's `template.json` env contract. The auth, docker, and config pieces are still generated + locally in `src/generators/*`. Override the template source for development with + `SEAMLESS_TEMPLATES_DIR` (a local checkout) or `SEAMLESS_TEMPLATES_REF` (a different ref). - **check** health-checks a running stack. - **bootstrap-admin** mints the first admin invite. - **verify** ([src/commands/verify.ts](src/commands/verify.ts)) runs the conformance harness (below). @@ -55,8 +61,8 @@ Modes and sibling repos: ## Important Folders - [src/commands](src/commands): one file per CLI command -- [src/generators](src/generators): project scaffolding (frontend, backend, auth, docker, config) -- [src/core](src/core): shared helpers (exec, env, fetch, secrets, paths, package manager, output) +- [src/generators](src/generators): locally generated scaffolding (auth, docker, config) +- [src/core](src/core): shared helpers (templates, exec, env, fetch, secrets, paths, package manager, output) - [src/prompts](src/prompts): interactive setup prompts (`@clack/prompts`) - [src/utils](src/utils): repo and env-file helpers - [verify](verify): the conformance harness (shipped with the package) diff --git a/README.md b/README.md index 4cb932a..357c46e 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,12 @@ Seamless CLI pulls from the following repositories: - Seamless Auth API [https://github.com/fells-code/seamless-auth-api](https://github.com/fells-code/seamless-auth-api) -- Seamless Auth React Starter - [https://github.com/fells-code/seamless-auth-starter-react](https://github.com/fells-code/seamless-auth-starter-react) +- Seamless Templates (the frontend and API starters) + [https://github.com/fells-code/seamless-templates](https://github.com/fells-code/seamless-templates) -- Seamless Auth API Starter - [https://github.com/fells-code/seamless-auth-starter-express](https://github.com/fells-code/seamless-auth-starter-express) - -Each project can be used independently, but the CLI connects them into a working system. +The starters live in the templates monorepo and are listed in its registry, so the set of +frameworks the CLI offers grows there. Each project can be used independently, but the CLI connects +them into a working system. --- diff --git a/src/commands/init.ts b/src/commands/init.ts index 491b312..6af7599 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,13 +1,21 @@ import path from "path"; import fs from "fs"; + import { runProjectSetupPrompts } from "../prompts/projectSetup.js"; -import { generateReactStarter } from "../generators/frontend/react.js"; -import { generateExpressStarter } from "../generators/backend/express.js"; import { generateAuthServer } from "../generators/auth/auth.js"; -import { configureApiEnv, configureWebEnv } from "../core/configure.js"; import { generateDockerCompose } from "../generators/docker/docker.js"; import { printSuccessOutput } from "../core/output.js"; import { generateSeamlessConfig } from "../generators/config/config.js"; +import { + applyTemplateEnv, + assertCliSupports, + openTemplateSource, + type RegistryEntry, + type TemplateManifest, +} from "../core/templates.js"; + +const AUTH_SERVER_URL = "http://localhost:5312"; +const API_URL = "http://localhost:3000"; export async function runCLI(projectName?: string) { const cwd = process.cwd(); @@ -35,14 +43,30 @@ export async function runCLI(projectName?: string) { return; } - const answers = await runProjectSetupPrompts(); - - if (answers.web && answers.webFramework === "react") { - await generateReactStarter({ root }); - } + const source = await openTemplateSource(); + const answers = await runProjectSetupPrompts(source.registry.templates); - if (answers.api && answers.apiFramework === "express") { - await generateExpressStarter({ root }); + const findEntry = (id: string): RegistryEntry => { + const entry = source.registry.templates.find((t) => t.id === id); + if (!entry) { + throw new Error(`Selected template "${id}" is not in the registry.`); + } + return entry; + }; + + // Resolve the chosen templates, then place their files. Env wiring waits until the + // shared auth config (tokens, key id) exists below. + const selected: { entry: RegistryEntry; manifest: TemplateManifest; dir: string }[] = + []; + for (const id of [answers.webTemplateId, answers.apiTemplateId]) { + const entry = findEntry(id); + const manifest = await source.readManifest(entry); + assertCliSupports(manifest, entry.label); + const dir = path.join(root, manifest.targetDir); + + console.log(`Adding ${entry.label} starter...`); + await source.copyInto(entry, dir); + selected.push({ entry, manifest, dir }); } let sharedConfig: any = {}; @@ -63,18 +87,24 @@ export async function runCLI(projectName?: string) { } } - if (answers.api) { - configureApiEnv(root, sharedConfig); - } + const ctx = { + authServerUrl: AUTH_SERVER_URL, + apiUrl: API_URL, + apiToken: sharedConfig.apiToken, + jwksKid: sharedConfig.kid, + }; - if (answers.web) { - configureWebEnv(root); + for (const { manifest, dir } of selected) { + applyTemplateEnv(dir, manifest, ctx); } + const webEntry = findEntry(answers.webTemplateId); + const apiEntry = findEntry(answers.apiTemplateId); + generateSeamlessConfig(root, { projectName, - webFramework: answers.webFramework, - apiFramework: answers.apiFramework, + webFramework: webEntry.framework, + apiFramework: apiEntry.framework, authMode: answers.authMode, adminMode: answers.adminMode, }); @@ -82,8 +112,8 @@ export async function runCLI(projectName?: string) { printSuccessOutput({ projectName, root, - webFramework: answers.webFramework, - apiFramework: answers.apiFramework, + webFramework: webEntry.framework, + apiFramework: apiEntry.framework, authMode: answers.authMode, useDocker: answers.useDocker, }); diff --git a/src/core/configure.ts b/src/core/configure.ts deleted file mode 100644 index d16660a..0000000 --- a/src/core/configure.ts +++ /dev/null @@ -1,32 +0,0 @@ -import path from "path"; -import fs from "fs"; -import { parseEnv, writeEnv } from "./env.js"; -import { generateSecret } from "./secrets.js"; - -export function configureApiEnv(root: string, shared: any) { - const apiEnvPath = path.join(root, "api", ".env"); - - if (!fs.existsSync(apiEnvPath)) return; - - const env = parseEnv(apiEnvPath); - - env.AUTH_SERVER_URL = "http://localhost:5312"; - env.API_SERVICE_TOKEN = shared.apiToken; - env.JWKS_KID = shared.kid; - env.COOKIE_SIGNING_KEY = generateSecret(32); - - writeEnv(apiEnvPath, env); -} - -export function configureWebEnv(root: string) { - const webEnvPath = path.join(root, "web", ".env"); - - if (!fs.existsSync(webEnvPath)) return; - - const env = parseEnv(webEnvPath); - - env.VITE_AUTH_SERVER_URL = "http://localhost:5312"; - env.VITE_API_URL = "http://localhost:3000"; - - writeEnv(webEnvPath, env); -} diff --git a/src/core/images.ts b/src/core/images.ts index ba88d88..81afa62 100644 --- a/src/core/images.ts +++ b/src/core/images.ts @@ -7,3 +7,10 @@ export const SEAMLESS_AUTH_API_IMAGE = `ghcr.io/fells-code/seamless-auth-api:${S export const SEAMLESS_AUTH_ADMIN_DASHBOARD_VERSION = "v0.1.0"; export const SEAMLESS_AUTH_ADMIN_DASHBOARD_IMAGE = `ghcr.io/fells-code/seamless-auth-admin-dashboard:${SEAMLESS_AUTH_ADMIN_DASHBOARD_VERSION}`; + +// The starter templates monorepo the CLI scaffolds from. Pinned to a tag so a given +// CLI version always produces the same project. Override the ref with +// SEAMLESS_TEMPLATES_REF, or point at a local checkout with SEAMLESS_TEMPLATES_DIR. +export const SEAMLESS_TEMPLATES_REPO = "fells-code/seamless-templates"; + +export const SEAMLESS_TEMPLATES_REF = "v0.1.0"; diff --git a/src/core/templates.ts b/src/core/templates.ts new file mode 100644 index 0000000..96163e4 --- /dev/null +++ b/src/core/templates.ts @@ -0,0 +1,261 @@ +import fs from "fs"; +import path from "path"; + +import AdmZip from "adm-zip"; + +import { VERSION } from "../index.js"; +import { parseEnvString, writeEnv } from "./env.js"; +import { generateSecret } from "./secrets.js"; +import { SEAMLESS_TEMPLATES_REF, SEAMLESS_TEMPLATES_REPO } from "./images.js"; + +export type TemplateKind = "web" | "api"; +export type TemplateStatus = "stable" | "beta" | "coming-soon"; + +export interface RegistryEntry { + id: string; + kind: TemplateKind; + framework: string; + label: string; + status: TemplateStatus; + path: string; +} + +export interface Registry { + schemaVersion: number; + templates: RegistryEntry[]; +} + +export interface TemplateManifest { + id: string; + targetDir: string; + env?: { + fromExample?: string; + set?: Record; + }; + requires?: { cliMin?: string }; +} + +// Values the CLI computes for a scaffold, used to resolve {{placeholders}} in a +// template manifest's env.set. +export interface ScaffoldContext { + authServerUrl: string; + apiUrl: string; + apiToken?: string; + jwksKid?: string; +} + +// Build artifacts and local-only files that must never be copied into a scaffold, +// even if a local template checkout has them lying around. +const IGNORED_NAMES = new Set([ + "node_modules", + ".git", + "dist", + "build", + ".next", + "out", + "coverage", + ".DS_Store", + ".env", + ".env.local", +]); + +// A resolved place to read templates from: either a local checkout (for development, +// via SEAMLESS_TEMPLATES_DIR) or the published monorepo at a pinned ref. +export interface TemplateSource { + registry: Registry; + readManifest(entry: RegistryEntry): Promise; + copyInto(entry: RegistryEntry, destDir: string): Promise; +} + +export async function openTemplateSource(): Promise { + const localDir = process.env.SEAMLESS_TEMPLATES_DIR; + if (localDir) { + return openLocalSource(path.resolve(localDir)); + } + return openRemoteSource( + SEAMLESS_TEMPLATES_REPO, + process.env.SEAMLESS_TEMPLATES_REF ?? SEAMLESS_TEMPLATES_REF, + ); +} + +function readRegistry(raw: string): Registry { + const registry = JSON.parse(raw) as Registry; + if (!registry || !Array.isArray(registry.templates)) { + throw new Error("Template registry is malformed (missing templates array)."); + } + return registry; +} + +function openLocalSource(dir: string): TemplateSource { + const registryPath = path.join(dir, "registry.json"); + if (!fs.existsSync(registryPath)) { + throw new Error( + `SEAMLESS_TEMPLATES_DIR is set to ${dir}, but no registry.json was found there.`, + ); + } + const registry = readRegistry(fs.readFileSync(registryPath, "utf-8")); + + return { + registry, + async readManifest(entry) { + const manifestPath = path.join(dir, entry.path, "template.json"); + return JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as TemplateManifest; + }, + async copyInto(entry, destDir) { + copyDir(path.join(dir, entry.path), destDir); + }, + }; +} + +async function openRemoteSource(repo: string, ref: string): Promise { + const registryUrl = `https://raw.githubusercontent.com/${repo}/${ref}/registry.json`; + const res = await fetch(registryUrl); + if (!res.ok) { + throw new Error( + `Failed to fetch the template registry (${res.status}) from ${registryUrl}.`, + ); + } + const registry = readRegistry(await res.text()); + + // The whole monorepo is downloaded once, lazily, and shared across every template + // a single scaffold pulls in. + let archive: { zip: AdmZip; root: string } | null = null; + const ensureArchive = async () => { + if (archive) return archive; + const url = `https://github.com/${repo}/archive/${ref}.zip`; + const zipRes = await fetch(url); + if (!zipRes.ok) { + throw new Error(`Failed to download templates (${zipRes.status}) from ${url}.`); + } + const zip = new AdmZip(Buffer.from(await zipRes.arrayBuffer())); + const first = zip.getEntries()[0]; + if (!first) { + throw new Error("Downloaded templates archive was empty."); + } + // GitHub archives nest everything under a single top-level directory. + const root = first.entryName.split("/")[0]; + archive = { zip, root }; + return archive; + }; + + return { + registry, + async readManifest(entry) { + const { zip, root } = await ensureArchive(); + const name = `${root}/${entry.path}/template.json`; + const found = zip.getEntry(name); + if (!found) { + throw new Error(`Template "${entry.id}" is missing template.json in the registry.`); + } + return JSON.parse(found.getData().toString("utf-8")) as TemplateManifest; + }, + async copyInto(entry, destDir) { + const { zip, root } = await ensureArchive(); + const prefix = `${root}/${entry.path}/`; + for (const e of zip.getEntries()) { + if (e.isDirectory || !e.entryName.startsWith(prefix)) continue; + const rel = e.entryName.slice(prefix.length); + if (rel.split("/").some((seg) => IGNORED_NAMES.has(seg))) continue; + const out = path.join(destDir, rel); + fs.mkdirSync(path.dirname(out), { recursive: true }); + fs.writeFileSync(out, e.getData()); + } + }, + }; +} + +function copyDir(src: string, dest: string) { + fs.mkdirSync(dest, { recursive: true }); + for (const name of fs.readdirSync(src)) { + if (IGNORED_NAMES.has(name)) continue; + const from = path.join(src, name); + const to = path.join(dest, name); + const stat = fs.statSync(from); + if (stat.isDirectory()) { + copyDir(from, to); + } else if (stat.isFile()) { + fs.copyFileSync(from, to); + } + } +} + +export function assertCliSupports(manifest: TemplateManifest, label: string) { + const min = manifest.requires?.cliMin; + if (min && compareSemver(VERSION, min) < 0) { + throw new Error( + `Template "${label}" requires seamless-cli >= ${min}, but this is ${VERSION}. Please upgrade.`, + ); + } +} + +// Copies the template's example env to .env (if declared), then applies the +// manifest's env.set, resolving {{placeholders}} against the scaffold context. +// This replaces the per-framework configure step. +export function applyTemplateEnv( + destDir: string, + manifest: TemplateManifest, + ctx: ScaffoldContext, +) { + const env = manifest.env ?? {}; + const envPath = path.join(destDir, ".env"); + + let values: Record = {}; + if (env.fromExample) { + const examplePath = path.join(destDir, env.fromExample); + if (fs.existsSync(examplePath)) { + values = parseEnvString(fs.readFileSync(examplePath, "utf-8")); + } + } else if (fs.existsSync(envPath)) { + values = parseEnvString(fs.readFileSync(envPath, "utf-8")); + } + + for (const [key, raw] of Object.entries(env.set ?? {})) { + values[key] = resolvePlaceholders(raw, ctx); + } + + writeEnv(envPath, values); +} + +function resolvePlaceholders(value: string, ctx: ScaffoldContext): string { + return value.replace(/\{\{\s*([\w:]+)\s*\}\}/g, (_match, token: string) => + resolveToken(token, ctx), + ); +} + +function resolveToken(token: string, ctx: ScaffoldContext): string { + const secret = /^secret:(\d+)$/.exec(token); + if (secret) { + return generateSecret(Number(secret[1])); + } + + const known: Record = { + authServerUrl: ctx.authServerUrl, + apiUrl: ctx.apiUrl, + apiToken: ctx.apiToken, + jwksKid: ctx.jwksKid, + }; + + if (token in known) { + const resolved = known[token]; + if (resolved == null) { + throw new Error( + `Template placeholder {{${token}}} has no value in this configuration.`, + ); + } + return resolved; + } + + throw new Error(`Unknown template placeholder {{${token}}}.`); +} + +function compareSemver(a: string, b: string): number { + const norm = (v: string) => + v.replace(/^v/, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0); + const pa = norm(a); + const pb = norm(b); + for (let i = 0; i < 3; i++) { + const diff = (pa[i] ?? 0) - (pb[i] ?? 0); + if (diff !== 0) return diff < 0 ? -1 : 1; + } + return 0; +} diff --git a/src/generators/backend/express.ts b/src/generators/backend/express.ts deleted file mode 100644 index 55c31ab..0000000 --- a/src/generators/backend/express.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from "path"; -import { - cloneRepo, - removeGitDir, - copyEnvExample, -} from "../../utils/repoUtils.js"; - -const API_STARTER_REPO = - "https://github.com/fells-code/seamless-auth-starter-express.git"; - -export async function generateExpressStarter(context: { root: string }) { - const { root } = context; - - const apiDir = path.join(root, "api"); - - console.log("Cloning Seamless Auth Express starter..."); - - await cloneRepo(API_STARTER_REPO, apiDir); - - removeGitDir(apiDir); - copyEnvExample(apiDir); - - console.log("API starter ready."); -} diff --git a/src/generators/frontend/react.ts b/src/generators/frontend/react.ts deleted file mode 100644 index 238e431..0000000 --- a/src/generators/frontend/react.ts +++ /dev/null @@ -1,23 +0,0 @@ -import path from "path"; -import { - cloneRepo, - removeGitDir, - copyEnvExample, -} from "../../utils/repoUtils.js"; - -const WEB_STARTER_REPO = - "https://github.com/fells-code/seamless-auth-starter-react.git"; - -export async function generateReactStarter(context: { root: string }) { - const { root } = context; - const webDir = path.join(root, "web"); - - console.log("Cloning Seamless Auth React starter..."); - - await cloneRepo(WEB_STARTER_REPO, webDir); - - removeGitDir(webDir); - copyEnvExample(webDir); - - console.log("Web starter ready."); -} diff --git a/src/prompts/projectSetup.ts b/src/prompts/projectSetup.ts index 0d964fd..c0e4d58 100644 --- a/src/prompts/projectSetup.ts +++ b/src/prompts/projectSetup.ts @@ -1,28 +1,46 @@ import { confirm, select } from "@clack/prompts"; -type WebFramework = "react"; -type ApiFramework = "express"; +import type { RegistryEntry, TemplateKind } from "../core/templates.js"; + type AuthMode = "local" | "docker"; type AdminMode = "image" | "source"; -export async function runProjectSetupPrompts() { - const webFramework = (await select({ +interface Option { + value: string; + label: string; + disabled?: boolean; +} + +// Builds the framework choices for one layer (web or api) from the registry, so +// adding a template is a registry edit, not a code change here. coming-soon +// templates show as disabled; beta templates are selectable but labelled. +function toOptions(templates: RegistryEntry[], kind: TemplateKind): Option[] { + const forKind = templates.filter((t) => t.kind === kind); + if (forKind.length === 0) { + throw new Error(`The template registry has no ${kind} templates.`); + } + return forKind.map((t) => ({ + value: t.id, + label: + t.status === "coming-soon" + ? `${t.label} (coming soon)` + : t.status === "beta" + ? `${t.label} (beta)` + : t.label, + disabled: t.status === "coming-soon", + })); +} + +export async function runProjectSetupPrompts(templates: RegistryEntry[]) { + const webTemplateId = (await select({ message: "Web framework", - options: [ - { value: "react", label: "React (Vite)" }, - { value: "next", label: "Next.js (coming soon)", disabled: true }, - ], - })) as WebFramework; + options: toOptions(templates, "web"), + })) as string; - const apiFramework = (await select({ + const apiTemplateId = (await select({ message: "Backend framework", - options: [ - { value: "express", label: "Express" }, - { value: "fastify", label: "Fastify (coming soon)", disabled: true }, - { value: "fastAPI", label: "FastAPI (coming soon)", disabled: true }, - { value: "axum", label: "Rust Axum (coming soon)", disabled: true }, - ], - })) as ApiFramework; + options: toOptions(templates, "api"), + })) as string; const authMode = (await select({ message: "How would you like to run SeamlessAuth?", @@ -61,8 +79,6 @@ export async function runProjectSetupPrompts() { })) as AdminMode; } - let useDocker = true; - if (authMode === "local") { const confirmDocker = await confirm({ message: @@ -79,10 +95,10 @@ export async function runProjectSetupPrompts() { return { web: true, - webFramework, + webTemplateId, api: true, - apiFramework, + apiTemplateId, authMode, useDocker: true, diff --git a/src/utils/repoUtils.ts b/src/utils/repoUtils.ts deleted file mode 100644 index 412584b..0000000 --- a/src/utils/repoUtils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { runCommand } from "../core/exec.js"; - -export async function cloneRepo(repoUrl: string, dest: string) { - const parentDir = path.dirname(dest); - const folderName = path.basename(dest); - - fs.mkdirSync(parentDir, { recursive: true }); - - await runCommand( - "git", - ["clone", "--depth", "1", repoUrl, folderName], - parentDir, - ); -} - -export function removeGitDir(projectRoot: string) { - const gitDir = path.join(projectRoot, ".git"); - if (fs.existsSync(gitDir)) { - fs.rmSync(gitDir, { recursive: true, force: true }); - } -} - -export function copyEnvExample(projectRoot: string) { - const envExample = path.join(projectRoot, ".env.example"); - const env = path.join(projectRoot, ".env"); - - if (fs.existsSync(envExample) && !fs.existsSync(env)) { - fs.copyFileSync(envExample, env); - } -} From 57c78198a537a29681492de9b5d072f1d3950a8a Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 2 Jul 2026 13:28:58 +0200 Subject: [PATCH 2/3] ci: repoint conformance from the starter repos to seamless-templates verify.ts resolves the react-vite web template from a sibling seamless-templates checkout by default; the reusable verify-conformance workflow checks out seamless-templates (templates-ref input) and points SEAMLESS_REACT_DIR at the react-vite subpath. Preserves the auth-contract conformance guarantee against the new template source. --- .changeset/conformance-repoint.md | 5 +++++ .github/workflows/verify-conformance.yml | 12 ++++++------ AGENTS.md | 8 +++++--- src/commands/verify.ts | 13 ++++++++----- 4 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 .changeset/conformance-repoint.md diff --git a/.changeset/conformance-repoint.md b/.changeset/conformance-repoint.md new file mode 100644 index 0000000..41e69cd --- /dev/null +++ b/.changeset/conformance-repoint.md @@ -0,0 +1,5 @@ +--- +"seamless-cli": patch +--- + +Point the conformance harness at the seamless-templates monorepo. `seamless verify` now resolves the React web template from `../seamless-templates/templates/web/react-vite` by default (still overridable with `SEAMLESS_REACT_DIR`), and the reusable `verify-conformance.yml` workflow checks out `seamless-templates` (input `templates-ref`) instead of the standalone starter repo. diff --git a/.github/workflows/verify-conformance.yml b/.github/workflows/verify-conformance.yml index 29da174..60ad7b8 100644 --- a/.github/workflows/verify-conformance.yml +++ b/.github/workflows/verify-conformance.yml @@ -23,7 +23,7 @@ on: react-ref: type: string default: '' - starter-ref: + templates-ref: type: string default: '' @@ -64,12 +64,12 @@ jobs: ref: ${{ inputs.react-ref }} path: seamless-auth-react - - name: Checkout seamless-auth-starter-react + - name: Checkout seamless-templates uses: actions/checkout@v4 with: - repository: fells-code/seamless-auth-starter-react - ref: ${{ inputs.starter-ref }} - path: seamless-auth-starter-react + repository: fells-code/seamless-templates + ref: ${{ inputs.templates-ref }} + path: seamless-templates - uses: actions/setup-node@v4 with: @@ -107,7 +107,7 @@ jobs: SEAMLESS_API_DIR: ${{ github.workspace }}/seamless-auth-api SEAMLESS_SERVER_DIR: ${{ github.workspace }}/seamless-auth-server SEAMLESS_REACT_SDK_DIR: ${{ github.workspace }}/seamless-auth-react - SEAMLESS_REACT_DIR: ${{ github.workspace }}/seamless-auth-starter-react + SEAMLESS_REACT_DIR: ${{ github.workspace }}/seamless-templates/templates/web/react-vite run: node dist/index.js verify ${{ inputs.local && '--local' || '' }} - name: Upload conformance report diff --git a/AGENTS.md b/AGENTS.md index a23d6ac..32fed16 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ Modes and sibling repos: default uses the published packages. - The sibling repos are resolved relative to this repo, overridable with `SEAMLESS_API_DIR`, `SEAMLESS_SERVER_DIR`, `SEAMLESS_REACT_SDK_DIR` (the React SDK), and `SEAMLESS_REACT_DIR` (the - starter app). + `react-vite` web template, defaulting to `../seamless-templates/templates/web/react-vite`). - Useful flags: `--api-only`, `--no-react`, `--filter `, `--keep-up`. ## Important Folders @@ -90,7 +90,7 @@ Modes and sibling repos: ## Known Maintenance Traps - **Sibling-repo branches**: the server's integration branch is `dev` (its `main` lags), so the verify - CI workflow defaults the server checkout to `dev`. The api, react, and starter use `main`. + CI workflow defaults the server checkout to `dev`. The api, react SDK, and seamless-templates use `main`. - **`--local` needs SDK dependencies**: it builds the server (pnpm) and the React SDK (npm) from source on the host, so those repos must have their dependencies installed first. CI installs them explicitly. - **OAuth mock networking**: the in-process mock OIDC is reached by the browser and harness via @@ -100,4 +100,6 @@ Modes and sibling repos: limiter (10 per 15 minutes, hardcoded) bounds adapter / react OTP traffic. Keep specs off it where possible (for example, magic-link login instead of a second email-OTP round trip). - **Version pins**: [verify/adapter-app](verify/adapter-app) pins `@seamless-auth/express` and the - starter pins `@seamless-auth/react`. Bump these when new versions publish. + `react-vite` template pins `@seamless-auth/react`. Bump these when new versions publish. +- **Templates ref**: the CLI scaffolds from `seamless-templates` at `SEAMLESS_TEMPLATES_REF` + ([src/core/images.ts](src/core/images.ts)); bump it when a new templates release publishes. diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 0e2ce81..d7c16a6 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -57,16 +57,19 @@ function resolveApiDir(): string { return candidate; } -// The React starter app, served at :5173 and pointed at the adapter. Defaults to -// a sibling checkout; override with SEAMLESS_REACT_DIR. Only needed for browser runs. +// The React web template, served at :5173 and pointed at the adapter. Defaults to the +// react-vite template inside a sibling seamless-templates checkout; override with +// SEAMLESS_REACT_DIR. Only needed for browser runs. +// TODO(#1): resolve this from the registry so every web template is conformance-tested, +// not just react-vite. function resolveReactDir(): string { const candidate = process.env.SEAMLESS_REACT_DIR ?? - path.resolve(REPO_ROOT, "..", "seamless-auth-starter-react"); + path.resolve(REPO_ROOT, "..", "seamless-templates", "templates", "web", "react-vite"); if (!fs.existsSync(path.join(candidate, "package.json"))) { throw new Error( - `Could not find seamless-auth-starter-react at ${candidate}.\n` + - " Set SEAMLESS_REACT_DIR to its local checkout, or run with --no-react.", + `Could not find the react-vite web template at ${candidate}.\n` + + " Set SEAMLESS_REACT_DIR to a local template checkout, or run with --no-react.", ); } return candidate; From 10e76099bf8f026bbb7afa3c22de0a65865ad8e2 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Thu, 2 Jul 2026 13:38:12 +0200 Subject: [PATCH 3/3] chore: pin templates to v0.1.1 Picks up the templates release that drops the inert per-template conformance workflow, so scaffolded web projects no longer inherit it. --- src/core/images.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/images.ts b/src/core/images.ts index 81afa62..696bf99 100644 --- a/src/core/images.ts +++ b/src/core/images.ts @@ -13,4 +13,4 @@ export const SEAMLESS_AUTH_ADMIN_DASHBOARD_IMAGE = `ghcr.io/fells-code/seamless- // SEAMLESS_TEMPLATES_REF, or point at a local checkout with SEAMLESS_TEMPLATES_DIR. export const SEAMLESS_TEMPLATES_REPO = "fells-code/seamless-templates"; -export const SEAMLESS_TEMPLATES_REF = "v0.1.0"; +export const SEAMLESS_TEMPLATES_REF = "v0.1.1";