Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
29c60fa
feat(cli): scaffold React, Vue and Angular SPAs
mridang Jun 11, 2026
d691dc1
feat(cli): match all vite.config extensions and share ViteSupport
mridang Jun 11, 2026
4fdd932
feat(cli): scaffold Nuxt via the sdk-nuxt module
mridang Jun 11, 2026
ad41a7f
refactor(cli): brace all guards and cover every config extension
mridang Jun 11, 2026
c776809
refactor(cli): merge vite-proxy into vite-support
mridang Jun 11, 2026
78a7b32
refactor(cli): hoist PROXY_PATH to a framework-neutral module
mridang Jun 11, 2026
7521246
Merge remote-tracking branch 'origin/main' into feat/cli-spa-frameworks
mridang Jun 11, 2026
4c5a941
refactor(cli): reuse npmDistTagForCliVersion in SPA patchers
mridang Jun 11, 2026
d08319c
fix(cli): repair proxy edits, harden Nuxt merge, drop overwrite footgun
mridang Jun 11, 2026
eccc905
style(cli): apply prettier to SPA patcher files
mridang Jun 11, 2026
1a6c21e
refactor(cli): immutable patcher code + per-patcher READMEs
mridang Jun 11, 2026
16c7436
refactor(cli): group magicast helpers in utils/ and add tests
mridang Jun 11, 2026
a06a3a9
chore(cli): add changeset for SPA framework scaffolding
mridang Jun 11, 2026
b11ac47
refactor(cli): scope NEXT_PUBLIC env to the Next patcher
mridang Jun 11, 2026
220b610
feat(cli): eject surfaces in-place config edits + patcher/scaffolder …
mridang Jun 11, 2026
84ca4ca
fix(cli): remove orphaned app.css in the Angular scaffolder
mridang Jun 11, 2026
af6649b
fix(cli): angular scaffolder must name the project and use --directory
mridang Jun 11, 2026
cf0650b
fix(cli): target the Nuxt 4 app/ srcDir when patching and ejecting
mridang Jun 11, 2026
8e97819
fix(cli): add setup --dev-port and an Angular dev script
mridang Jun 11, 2026
21a0213
Merge remote-tracking branch 'origin/main' into feat/cli-spa-frameworks
mridang Jun 12, 2026
aaf1a2b
Merge remote-tracking branch 'origin/main' into feat/cli-spa-frameworks
mridang Jun 12, 2026
32c7903
fix(cli): drop proxy changeOrigin and register the app origin
mridang Jun 12, 2026
8bdd180
fix(cli): don't double the scheme in zitadel.json issuer_pattern
mridang Jun 12, 2026
a84bfb6
feat(cli): proxy injects sk_<project_id> from env on every request
mridang Jun 12, 2026
03f01c7
Merge branch 'main' into feat/cli-spa-frameworks
mridang Jun 12, 2026
dd90e61
feat(cli): set strictPort in the generated Vite config
mridang Jun 12, 2026
01bccb6
fix(cli): harden config edits against non-object literals
mridang Jun 12, 2026
8a9cbed
fix(cli): report edited config files as a glob in eject
mridang Jun 12, 2026
70ec66d
fix(cli): report Angular appDir as src/app
mridang Jun 12, 2026
314763d
perf(cli): avoid double read when resolving edit candidates
mridang Jun 12, 2026
217c8d7
chore(cli): align magicast to 0.5.3
mridang Jun 12, 2026
925bb95
fix(cli): harden setup config-paths, dev-port, and proxy templates
mridang Jun 12, 2026
4b3b8ce
docs(cli): align config-edit docs with the actual behavior
mridang Jun 12, 2026
bd39cc7
fix(cli): preserve an existing dev script when patching Angular
mridang Jun 12, 2026
3e0b796
docs(cli): document SPA frameworks and dev-port; cover the edit file-op
mridang Jun 12, 2026
d749868
fix(cli): keep the scaffolded dev-server port in sync with the issuer
mridang Jun 12, 2026
50ebfd4
test(cli): cover the react/vue/angular detectors; reuse PROXY_PATH
mridang Jun 12, 2026
4bb7ddb
Merge branch 'main' into feat/cli-spa-frameworks
mridang Jun 12, 2026
f7941a1
docs(cli): clarify the app templates inject a bearer, not a secret
mridang Jun 12, 2026
a432d50
docs(cli): correct dev-port and proxy wording in patcher docs
mridang Jun 12, 2026
c758834
refactor(cli): centralize proxy path and name config files in errors
mridang Jun 12, 2026
804672a
fix(cli): harden Angular project selection and proxy.conf rewrite
mridang Jun 12, 2026
026d5c4
fix(cli): make the package.json and angular.json edits no-op when unc…
mridang Jun 12, 2026
693e117
fix(cli): make scaffolded components compile across framework versions
mridang Jun 12, 2026
38a9645
fix(cli): keep the proxy rewrite valid for a bare prefix + query string
mridang Jun 12, 2026
2fdf884
Merge branch 'main' into feat/cli-spa-frameworks
mridang Jun 12, 2026
1457332
fix(cli): guard missing project id and fail fast on absent package.json
mridang Jun 12, 2026
b10f496
fix(cli): preserve file permissions when an edit op replaces a file
mridang Jun 12, 2026
9542d29
Merge remote-tracking branch 'origin/main' into feat/cli-spa-frameworks
mridang Jun 13, 2026
cff9c72
fix(cli): validate proxy target and report package.json as an Angular…
mridang Jun 13, 2026
ac58f3c
fix(cli): harden editFile candidates/mode and clarify eject package.j…
mridang Jun 13, 2026
00ea67d
fix(cli): transpile Nuxt style deps and stop pinning the Vite dev host
mridang Jun 13, 2026
53bc194
fix(cli): resolve the Vite proxy env dir from process.cwd()
mridang Jun 13, 2026
97c138b
fix(cli): skip rewriting already-complete Vite and Nuxt configs
mridang Jun 13, 2026
06baeab
docs(cli): describe --renderer accurately in SKILLS.md
mridang Jun 13, 2026
1be2d58
fix(cli): gate Angular detection on v17+ and keep --dev-port authorit…
mridang Jun 13, 2026
8f7fbe7
fix(cli): give a targeted error for CommonJS config files
mridang Jun 13, 2026
e3a8632
docs(cli): clarify eject dev-script wording and config-candidates exa…
mridang Jun 13, 2026
1c7bc2d
fix(cli): use a wildcard label for config validation errors
mridang Jun 13, 2026
9857415
fix(cli): narrow CommonJS config detection to an assignment
mridang Jun 13, 2026
283a5a1
fix(cli): reject exports.* CommonJS configs and specify the angular.j…
mridang Jun 13, 2026
ee22f73
fix(cli): detect CommonJS configs from the AST, not raw text
mridang Jun 13, 2026
188ee8b
docs(cli): list the shared ZITADEL_* env keys in the patcher READMEs
mridang Jun 13, 2026
6f45e13
Merge remote-tracking branch 'origin/main' into feat/cli-spa-frameworks
mridang Jun 14, 2026
f556c83
fix(cli): match the source module in importIsPresent
mridang Jun 15, 2026
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
5 changes: 5 additions & 0 deletions .changeset/cli-spa-frameworks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zitadel/cli": minor
---

Add `setup --framework react|vue|angular|nuxt` support to the CLI. Each framework scaffolds its auth entry/pages and wires `/__nextgen/*` calls to the backend with a `sk_<project_id>` bearer attached: React and Vue get a dev proxy magicast-merged into the Vite config (`vite.config.*`) that reads the project id from `ZITADEL_PROJECT_ID`; Angular gets a `proxy.conf.cjs` wired into `angular.json` that reads it from `zitadel.json`; and Nuxt registers the `@zitadel/sdk-nuxt` module in the Nuxt config (`nuxt.config.*`), which adds the proxy via server middleware. A `--dev-port` flag sets the scaffolded dev-server port.
Comment thread
mridang marked this conversation as resolved.
Comment thread
mridang marked this conversation as resolved.
90 changes: 46 additions & 44 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @zitadel/cli

Scaffolds Zitadel auth (login, register, profile, middleware) into a Next.js app.
Scaffolds Zitadel auth (login, register, profile, middleware) into a Next.js, React, Vue, Angular, or Nuxt app.

```sh
npx @zitadel/cli@alpha start
Expand Down Expand Up @@ -74,44 +74,43 @@ passkey can sign in with either credential.
<summary>Full command reference</summary>

<!-- commands -->

- [`zitadel apply`](#zitadel-apply)
- [`zitadel autocomplete [SHELL]`](#zitadel-autocomplete-shell)
- [`zitadel commands`](#zitadel-commands)
- [`zitadel doctor`](#zitadel-doctor)
- [`zitadel eject`](#zitadel-eject)
- [`zitadel help [COMMAND]`](#zitadel-help-command)
- [`zitadel logs`](#zitadel-logs)
- [`zitadel plan`](#zitadel-plan)
- [`zitadel reset`](#zitadel-reset)
- [`zitadel search`](#zitadel-search)
- [`zitadel setup`](#zitadel-setup)
- [`zitadel start`](#zitadel-start)
- [`zitadel status`](#zitadel-status)
- [`zitadel stop`](#zitadel-stop)
- [`zitadel uninstall`](#zitadel-uninstall)
- [`zitadel version`](#zitadel-version)
- [`zitadel which`](#zitadel-which)
* [`zitadel apply`](#zitadel-apply)
* [`zitadel autocomplete [SHELL]`](#zitadel-autocomplete-shell)
* [`zitadel commands`](#zitadel-commands)
* [`zitadel doctor`](#zitadel-doctor)
* [`zitadel eject`](#zitadel-eject)
* [`zitadel help [COMMAND]`](#zitadel-help-command)
* [`zitadel logs`](#zitadel-logs)
* [`zitadel plan`](#zitadel-plan)
* [`zitadel reset`](#zitadel-reset)
* [`zitadel search`](#zitadel-search)
* [`zitadel setup`](#zitadel-setup)
* [`zitadel start`](#zitadel-start)
* [`zitadel status`](#zitadel-status)
* [`zitadel stop`](#zitadel-stop)
* [`zitadel uninstall`](#zitadel-uninstall)
* [`zitadel version`](#zitadel-version)
* [`zitadel which`](#zitadel-which)

## `zitadel apply`

Validate and upload repo config to the platform.

```
USAGE
$ zitadel apply [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug]
[-e development|preview|production]
$ zitadel apply [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug] [-e
development|preview|production]

FLAGS
-c, --cwd=<value> Project directory to operate on.
-e, --environment=<option> Target environment (default: development).
<options: development|preview|production>
-f, --force Overwrite protected files on conflict.
-n, --non-interactive Disable prompts. Required when scripting or running as an agent.
-s, --server=<value> Override the resolved server URL.
--debug Debug logging.
--dry-run Preview without mutating files or the platform.
--verbose Verbose logging.
-c, --cwd=<value> Project directory to operate on.
-e, --environment=<option> Target environment (default: development).
<options: development|preview|production>
-f, --force Overwrite protected files on conflict.
-n, --non-interactive Disable prompts. Required when scripting or running as an agent.
-s, --server=<value> Override the resolved server URL.
--debug Debug logging.
--dry-run Preview without mutating files or the platform.
--verbose Verbose logging.

GLOBAL FLAGS
--json Format output as json.
Expand Down Expand Up @@ -288,19 +287,19 @@ Validate config without mutation and preview the sync diff.

```
USAGE
$ zitadel plan [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug]
[-e development|preview|production]
$ zitadel plan [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug] [-e
development|preview|production]

FLAGS
-c, --cwd=<value> Project directory to operate on.
-e, --environment=<option> Target environment (default: development).
<options: development|preview|production>
-f, --force Overwrite protected files on conflict.
-n, --non-interactive Disable prompts. Required when scripting or running as an agent.
-s, --server=<value> Override the resolved server URL.
--debug Debug logging.
--dry-run Preview without mutating files or the platform.
--verbose Verbose logging.
-c, --cwd=<value> Project directory to operate on.
-e, --environment=<option> Target environment (default: development).
<options: development|preview|production>
-f, --force Overwrite protected files on conflict.
-n, --non-interactive Disable prompts. Required when scripting or running as an agent.
-s, --server=<value> Override the resolved server URL.
--debug Debug logging.
--dry-run Preview without mutating files or the platform.
--verbose Verbose logging.

GLOBAL FLAGS
--json Format output as json.
Expand Down Expand Up @@ -356,17 +355,19 @@ Create a Zitadel project and scaffold local auth.
```
USAGE
$ zitadel setup [--json] [-c <value>] [-s <value>] [-n] [-f] [--dry-run] [--verbose] [--debug]
[--framework next] [--renderer react|web-component] [--skip-install]
[--framework next|nuxt|react|vue|angular] [--renderer react|web-component] [--dev-port <value>] [--skip-install]

FLAGS
-c, --cwd=<value> Project directory to operate on.
-f, --force Overwrite protected files on conflict.
-n, --non-interactive Disable prompts. Required when scripting or running as an agent.
-s, --server=<value> Override the resolved server URL.
--debug Debug logging.
--dev-port=<value> Dev-server port; also the issuer origin registered with Zitadel. Defaults to the detected
port. Use distinct ports to run several scaffolded apps side by side.
--dry-run Preview without mutating files or the platform.
--framework=<option> Framework to target.
<options: next>
<options: next|nuxt|react|vue|angular>
--renderer=<option> Renderer (default: react).
<options: react|web-component>
--skip-install Do not install dependencies after setup updates package.json.
Expand All @@ -380,6 +381,8 @@ DESCRIPTION

EXAMPLES
$ zitadel setup --framework next

$ zitadel setup --framework react --dev-port 3000
```

## `zitadel start`
Expand Down Expand Up @@ -537,7 +540,6 @@ EXAMPLES
```

_See code: [@oclif/plugin-which](https://github.com/oclif/plugin-which/blob/3.2.55/src/commands/which.ts)_

<!-- commandsstop -->

</details>
Expand Down
11 changes: 8 additions & 3 deletions apps/cli/SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ description: >-
Set up and manage Zitadel authentication in a local project with the
agent-friendly `zitadel` CLI. Use when the user wants to add login,
registration, or session handling, create a Zitadel project, scaffold auth
routes for a Next.js App Router app, or plan and apply Zitadel config changes
from repo state.
for a Next.js, React, Vue, Angular, or Nuxt app, or plan and apply Zitadel
config changes from repo state.
---

# Zitadel CLI
Expand Down Expand Up @@ -56,7 +56,12 @@ layer, not the envelope.
and login flow are provisioned server-side at creation, so setup neither
scaffolds nor uploads them. Agents must pass `--framework` when scaffolding
into a fresh directory; interactive humans can omit it and choose from the
prompt. Flags: `--framework`, `--renderer`.
prompt. Flags: `--framework next|react|vue|angular|nuxt`, `--renderer
react|web-component` (selects the Next.js auth-page renderer; accepted for any
framework and recorded in `zitadel.json` branding, but only Next varies its
generated templates by it), `--dev-port` (dev-server port, also the issuer
origin registered with Zitadel — use distinct ports to run several scaffolded
apps side by side), `--skip-install`.
- `plan` — validate config and preview the sync diff without mutating anything.
- `apply` — validate and upload repo config to the platform.
- `doctor` — verify generated app files and local state once `zitadel.json`
Expand Down
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@zitadel/api": "workspace:*",
"ajv": "catalog:",
"consola": "^3.4.2",
"magicast": "^0.5.3",
"safe-stable-stringify": "catalog:",
"zod": "catalog:",
Comment thread
mridang marked this conversation as resolved.
"picocolors": "^1.1.1"
Expand Down
18 changes: 17 additions & 1 deletion apps/cli/src/commands/eject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async function resolveEjectActions(cwd: string): Promise<EjectActions> {
directories: [".zitadel"],
envBackups: [".env.local"],
dependencies: [],
configEdits: [],
};
const orca = createOrca();
const framework = await orca.tryDetect(cwd);
Expand Down Expand Up @@ -155,7 +156,21 @@ export default class Eject extends BaseCommand {
removed.push(rel);
}

if (removed.length === 0 && backedUp.length === 0) {
// In-place config merges (vite.config.ts / angular.json / nuxt.config.ts)
// can't be auto-reverted, so surface them as manual cleanup steps. The
// Angular patcher also edits package.json (a `dev` script, not a config
// block), so word that one accurately.
const manualSteps = actions.configEdits.map((rel) => {
if (rel === "package.json" || rel.endsWith("/package.json")) {
return `Remove the "dev" script setup added to ${rel}`;
}
if (rel === "angular.json" || rel.endsWith("/angular.json")) {
return `Remove the Zitadel proxyConfig (and dev-server port) from the serve target in ${rel}`;
}
return `Remove the Zitadel configuration block from ${rel}`;
});

if (removed.length === 0 && backedUp.length === 0 && manualSteps.length === 0) {
return this.emit({ status: "skipped", reason: "nothing-to-eject", data: { cwd } });
}

Expand All @@ -169,6 +184,7 @@ export default class Eject extends BaseCommand {
files_preserved: preserved,
backed_up: backedUp,
next_commands: nextCommands,
manual_steps: manualSteps,
},
});
}
Expand Down
55 changes: 49 additions & 6 deletions apps/cli/src/commands/setup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,20 @@ const FRAMEWORK_OPTIONS = createOrca()
*/
export default class Setup extends BaseCommand {
static override description = "Create a Zitadel project and scaffold local auth.";
static override examples = ["<%= config.bin %> setup --framework next"];
static override examples = [
"<%= config.bin %> setup --framework next",
"<%= config.bin %> setup --framework react --dev-port 3000",
];
static override flags = {
framework: Flags.string({ description: "Framework to target.", options: FRAMEWORK_OPTIONS }),
renderer: Flags.string({
description: "Renderer (default: react).",
options: [...RENDERER_IDS],
}),
"dev-port": Flags.integer({
description:
"Dev-server port; also the issuer origin registered with Zitadel. Defaults to the detected port. Use distinct ports to run several scaffolded apps side by side.",
}),
Comment thread
mridang marked this conversation as resolved.
"skip-install": Flags.boolean({
description: "Do not install dependencies after setup updates package.json.",
}),
Expand Down Expand Up @@ -103,33 +110,66 @@ export default class Setup extends BaseCommand {
}
}

// An explicit --dev-port overrides the detected port for the whole run, so
// the issuer and the registered origin track the requested port (and several
// apps can be scaffolded on distinct ports). The config edits set the
// dev-server port only when it is unset, so a project that already pins a
// different port in vite.config.*/angular.json keeps it — pass --dev-port to
// match that pin (or remove it) if the origin check rejects requests.
if (flags["dev-port"] !== undefined) {
const devPort = flags["dev-port"];
if (!Number.isInteger(devPort) || devPort < 1 || devPort > 65535) {
throw new ZitadelError(
"E_VALIDATION",
`--dev-port must be an integer in 1..65535, got ${devPort}`,
);
}
framework = {
...framework,
devPort,
url: issuerFromPort(devPort),
};
}

let answers: SetupAnswers = {
server: this.meta.source,
devPort: framework.devPort,
};

if (!nonInteractive && !dryRun) {
intro("Zitadel setup");
const promptCtx = { framework, serverFlag: this.meta.serverFlag };
const promptCtx = {
framework,
serverFlag: this.meta.serverFlag,
devPortFromFlag: flags["dev-port"] !== undefined,
};
for (const prompt of SETUP_PROMPTS) {
answers = await prompt.ask(answers, promptCtx);
}
outro("Configuration captured");
}

const issuer = issuerFromPort(answers.devPort);
// The DevPortPrompt can change the port interactively, so fold the answer
// back into the framework: the patched dev-server config reads
// `framework.devPort`, and it must agree with the issuer and the registered
// origin (both derived from `answers.devPort`).
framework = { ...framework, devPort: answers.devPort, url: issuer };
Comment thread
mridang marked this conversation as resolved.

// `POST /projects` is unauthenticated. Creating the project also
// provisions its default user schema and login flow server-side, so the
// CLI no longer builds, scaffolds, or uploads those resources here.
consola.start(`Creating project on ${answers.server}${dryRun ? " (dry run)" : ""}`);
const unauthClient = createZitadelClient({ baseUrl: answers.server });
// Register the app's own origin so the backend's origin check allows
// requests the dev proxy forwards from it.
const project = dryRun
? dryRunProject()
? dryRunProject(issuer)
: await createProjectWithLocalHint(
unauthClient,
answers.server,
this.meta.cliVersion,
issuer,
);
consola.success(`Created project ${project.id}`);

Expand Down Expand Up @@ -235,12 +275,12 @@ async function resolveScaffoldFramework(
}

/** A deterministic stand-in project for `--dry-run`, so no remote call is made. */
function dryRunProject(): CreateProject201 {
function dryRunProject(issuer: string): CreateProject201 {
return {
id: "dry-run-0000",
projectSecret: "sk_proj_dry_run_full",
previewSecret: "sk_proj_dry_run_preview",
previewOrigins: [],
previewOrigins: [issuer],
createdAt: "2026-04-21T14:03:11.000Z",
};
}
Expand All @@ -249,9 +289,12 @@ async function createProjectWithLocalHint(
client: ReturnType<typeof createZitadelClient>,
server: string,
cliVersion: string,
issuer: string,
): Promise<CreateProject201> {
try {
return await client.createProject({ previewOrigins: [] });
// Register the app's own origin so the backend's origin check allows the
// requests the dev proxy forwards from it.
return await client.createProject({ previewOrigins: [issuer] });
} catch (error) {
const normalized = toZitadelError(error);
throw new ZitadelError(normalized.code, normalized.message, {
Expand Down
8 changes: 7 additions & 1 deletion apps/cli/src/commands/setup/prompts/dev-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import type { PromptContext, SetupAnswers, SetupPrompt } from "./types";
* becomes the issuer URL (`http://localhost:<port>`) via `issuerFromPort`.
*/
export class DevPortPrompt implements SetupPrompt {
async ask(answers: SetupAnswers, _ctx: PromptContext): Promise<SetupAnswers> {
async ask(answers: SetupAnswers, ctx: PromptContext): Promise<SetupAnswers> {
// `--dev-port <n>` is authoritative: skip the prompt so an interactive
// answer can't override the explicit flag (the base command already folded
// it into answers.devPort).
if (ctx.devPortFromFlag) {
return answers;
}
const value = await text({
message: "Dev server port",
placeholder: String(answers.devPort),
Expand Down
6 changes: 6 additions & 0 deletions apps/cli/src/commands/setup/prompts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export type PromptContext = {
* passed a flag" from "we fell back to the cloud default".
*/
readonly serverFlag?: string;
/**
* Whether `--dev-port` was passed explicitly. When set, the flag is
* authoritative and {@link import("./dev-port").DevPortPrompt} skips itself so
* an interactive answer can't override a scripted/flagged port.
*/
readonly devPortFromFlag?: boolean;
};

/**
Expand Down
Loading
Loading