@zitadel/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as flagText, $t as normalizePublicCliCommands, At as variableInventoryCells, B as reportSecretOutcome, Bt as loadOrCreateIdentity, C as passwordPolicyFile, Ct as parseVariableValue, D as BRANDING_DIR, Dt as toVariableRows, E as readJsonDirEntries, Et as toVariableRow, F as readSchemaFiles, Ft as buildUserAgent, G as ENABLE_FLAG_KEYS, Gt as portableSuggestions, H as republishCommands, Ht as envEnabled, I as selectSchema, It as installUserAgent, J as SETUP_FLAG_NAMES, Jt as unscopedCommand, K as ENABLE_FLAG_NAMES, Kt as projectCommand, L as SCHEMAS_DIR, Lt as processUserAgentFacts, M as enabledMethods, Mt as variableScope, N as methodsOf, Nt as reportWarning, Ot as valuesOfKind, P as readFlowFiles, Pt as takeReportedWarnings, Q as customFlagValues, Qt as normalizePublicCliCommand, R as publishClientId, Rt as hostAgent, S as parsePasswordMinLength, St as assertVariableName, T as readJsonDir, Tt as toInventoryRows, U as storeClientSecret, Ut as asEnvironmentSelector, V as republishArgs, Vt as resolveConsent, W as askConnectionEndpoints, Wt as inShell, X as customExample, Xt as isTrainVersion, Y as checkGivenCustomFlags, Yt as isPortableShellWord, Z as customFlagText, Zt as maskPublicCliVersion, _ as servesPrimary, _n as stopListeningForInterrupt, _t as storedCustomSettings, a as serverLogAction, an as collectPages, at as applySsoToSchema, b as PASSWORD_POLICY_PATH, bn as isMissingEndpoint, bt as offeredSignInMethods, c as wrapForBox, cn as setTopLevelJsonKey, ct as ssoEditRefusal, d as validateStep, dn as createZitadelClient, dt as IDPS_DIR, en as normalizePublicCliJson, et as givenCustomFlags, f as headSha, fn as escapeControlCharacters, ft as checkCustomSlug, g as rollbackDeployment, gn as listenForInterrupt, gt as refuseChangedIssuer, h as listLiveDeployments, hn as interruptSignal, ht as readConnectionFiles, i as exploreAllCommands, in as sdkVersionForCli, it as applySsoToFlow, j as authMethods, jt as variableReadScope, k as CommandGroups, kt as variableCells, l as DEPLOY_ARGS, ln as stableStringify, lt as ssoProvidersRefusal, m as createDeployment, mn as sanitizeResponse, mt as planConnection, n as brandingGuidanceAction, nn as optionArgs, nt as selectionFromFlags, o as verifyLoginAction, on as isObject, ot as removeSsoFromFlow, p as worktreeIsDirty, pn as sanitizeRejection, pt as credentialVariablesOf, q as SETUP_FLAG_KEYS, qt as shellCommand, r as customizeAndPublishActions, rn as publicCliCommand, rt as ssoEnableArgs, s as renderBoxActions, sn as parseJsonObject, st as removeSsoFromSchema, t as showSkillsHint, tn as normalizePublicCliProse, tt as loopbackIssuerWarning, u as diffStep, un as REQUEST_TIMEOUT_MS$1, ut as CONNECTION_SCHEMA_REF, v as buildBundle, vn as ZitadelError, vt as FLOWS_DIR, w as toPolicyWireBody, wt as readStdin, x as POLICIES_DIR, xn as toZitadelError, xt as VARIABLE_TYPES, yn as isErrno$2, yt as checkFlow, z as reportClientIdOutcome, zt as ciProvider } from "./skills-hint-CBoak9HL.mjs";
1
+ import { $ as checkGivenCustomFlags, $t as isPortableShellWord, A as readJsonDirEntries, At as toVariableRow, B as selectSchema, Bt as installUserAgent, C as isPolicyTestFile, Cn as isErrno$2, Ct as checkFlow, D as policyFileName, Dt as parseVariableValue, E as policyFileIssues, Et as assertVariableName, F as authMethods, Ft as variableReadScope, G as republishArgs, Gt as resolveConsent, H as publishClientId, Ht as hostAgent, I as enabledMethods, It as variableScope, J as askConnectionEndpoints, Jt as inShell, K as republishCommands, Kt as envEnabled, L as methodsOf, Lt as reportWarning, Mt as valuesOfKind, N as CommandGroups, Nt as variableCells, O as toPolicyWireBody, Ot as readStdin, Pt as variableInventoryCells, Q as SETUP_FLAG_NAMES, Qt as unscopedCommand, R as readFlowFiles, Rt as takeReportedWarnings, S as POLICY_TEST_SUFFIX, Sn as ZitadelError, St as FLOWS_DIR, T as passwordPolicyFile, Tn as toZitadelError, Tt as VARIABLE_TYPES, U as reportClientIdOutcome, Ut as ciProvider, V as SCHEMAS_DIR, Vt as processUserAgentFacts, W as reportSecretOutcome, Wt as loadOrCreateIdentity, X as ENABLE_FLAG_NAMES, Xt as projectCommand, Y as ENABLE_FLAG_KEYS, Yt as portableSuggestions, Z as SETUP_FLAG_KEYS, Zt as shellCommand, _ as servesPrimary, _n as sanitizeRejection, _t as credentialVariablesOf, a as serverLogAction, an as normalizePublicCliProse, at as loopbackIssuerWarning, b as PASSWORD_POLICY_PATH, bn as listenForInterrupt, bt as refuseChangedIssuer, c as wrapForBox, cn as sdkVersionForCli, ct as applySsoToFlow, d as validateStep, dn as parseJsonObject, dt as removeSsoFromSchema, en as isTrainVersion, et as customExample, f as headSha, fn as setTopLevelJsonKey, ft as ssoEditRefusal, g as rollbackDeployment, gn as escapeControlCharacters, gt as checkCustomSlug, h as listLiveDeployments, hn as createZitadelClient, ht as IDPS_DIR, i as exploreAllCommands, in as normalizePublicCliJson, it as givenCustomFlags, j as BRANDING_DIR, jt as toVariableRows, k as readJsonDir, kt as toInventoryRows, l as DEPLOY_ARGS, ln as collectPages, lt as applySsoToSchema, m as createDeployment, mn as REQUEST_TIMEOUT_MS$1, mt as CONNECTION_SCHEMA_REF, n as brandingGuidanceAction, nn as normalizePublicCliCommand, nt as customFlagValues, o as verifyLoginAction, on as optionArgs, ot as selectionFromFlags, p as worktreeIsDirty, pn as stableStringify, pt as ssoProvidersRefusal, q as storeClientSecret, qt as asEnvironmentSelector, r as customizeAndPublishActions, rn as normalizePublicCliCommands, rt as flagText, s as renderBoxActions, sn as publicCliCommand, st as ssoEnableArgs, t as showSkillsHint, tn as maskPublicCliVersion, tt as customFlagText, u as diffStep, un as isObject, ut as removeSsoFromFlow, v as buildBundle, vn as sanitizeResponse, vt as planConnection, w as parsePasswordMinLength, wn as isMissingEndpoint, wt as offeredSignInMethods, x as POLICIES_DIR, xn as stopListeningForInterrupt, xt as storedCustomSettings, yn as interruptSignal, yt as readConnectionFiles, z as readSchemaFiles, zt as buildUserAgent } from "./skills-hint-N1H5-ny1.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { Args, Command, Flags } from "@oclif/core";
4
4
  import consola$1, { consola } from "consola";
5
5
  import { ApiError, CSRF_HEADER, request } from "@zitadel/api/runtime/fetch";
6
- import { access, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { access, chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
7
7
  import { text } from "node:stream/consumers";
8
8
  import * as schemas from "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
9
9
  import { CreateGrantBody, CreateIdpBody, CreateTeamBody, CreateUserBody, GetBrandingByIdResponse, GetFlowDefinitionResponse, GetGrantResponse, GetIdpByIdResponse, GetPolicyResponse, GetProjectResponse, GetReleaseByIdResponse, GetSchemaByIdResponse, GetSessionResponse, GetTeamResponse, GetUserByIDResponse, ListBrandingResponse, ListEventsResponse, ListFlowDefinitionsResponse, ListPoliciesResponse, ListReleasesResponse, ListSchemasResponse, PatchProjectBody, PatchUserByIDBody, PublishPolicyBody, QueryGrantsBody, QueryGrantsResponse, QueryIdpsBody, QueryIdpsResponse, QueryProjectsBody, QueryProjectsResponse, QuerySessionsBody, QuerySessionsResponse, QueryTeamsBody, QueryTeamsResponse, QueryUsersBody, QueryUsersResponse, UpdateTeamBody, getClaimStatusQueryChallengeIdRegExp } from "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
@@ -13,7 +13,7 @@ import { execFile, spawn, spawnSync } from "node:child_process";
13
13
  import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
14
14
  import { constants, existsSync } from "node:fs";
15
15
  import { homedir, tmpdir } from "node:os";
16
- import { isDeepStrictEqual, parseEnv } from "node:util";
16
+ import { isDeepStrictEqual, parseEnv, promisify } from "node:util";
17
17
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
18
18
  import { StringDecoder } from "node:string_decoder";
19
19
  import { resolveServerBinary, serverBinaryPath } from "@zitadel/server/binary";
@@ -22,7 +22,7 @@ import pc from "picocolors";
22
22
  import { DEFAULT_SERVER, DEFAULT_SERVER as DEFAULT_SERVER$1 } from "@zitadel/config/server";
23
23
  import mixpanelLib from "mixpanel";
24
24
  import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text as text$1 } from "@clack/prompts";
25
- import { policyConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
25
+ import { RECOMMENDED_PASSWORD_MIN_LENGTH, policyConfigSchema, policyTestFileSchema, schemaConfigSchema } from "@zitadel/config/schemas";
26
26
  import { CUSTOM_PROVIDER, DEFAULT_SCOPES, IDP_PROVIDERS, RESERVED_SLUGS, credentialVariables, idpProvider, isVariableReference, parseClientId, parseClientSecret, parseConnectionSlug, parseDisplayName, parseIssuer, parseScopes, providerOf, selectionName, validated } from "@zitadel/config/idp";
27
27
  import { BRANDING_DESIGNS, DEFAULT_BRANDING_CONFIG_PATH, DEFAULT_BRANDING_DESIGN, DEFAULT_BRANDING_TEMPLATE_PATH, DEFAULT_FLOW_CONFIG_PATH, DEFAULT_FLOW_SCHEMA_URI, DEFAULT_SCHEMA_CONFIG_PATH, DEFAULT_SETUP_PRESET, DEFAULT_SETUP_USE_CASE, SETUP_PRESETS, SETUP_USE_CASES, brandingReadmeContent, flowsReadmeContent, getDefaultBrandingConfig, getDefaultHumanUserSchema, getDefaultLoginFlow, policiesReadmeContent, schemasReadmeContent } from "@zitadel/config/defaults";
28
28
  import { BRANDING_FILE_SCHEMA_REF, META_SCHEMA_DIR, PROJECT_FILE_SCHEMA_REF, metaSchemaFiles } from "@zitadel/config/meta-schemas";
@@ -4316,19 +4316,36 @@ const CLI_COMMAND_STARTED = "cli_command_started";
4316
4316
  const CLI_COMMAND_COMPLETED = "cli_command_completed";
4317
4317
  const CLI_COMMAND_FAILED = "cli_command_failed";
4318
4318
  /**
4319
- * The properties the Vercel Web Analytics copy of a lifecycle event keeps, in
4320
- * priority order, the header first: the intake takes 8.
4319
+ * The one dimension of its own a command's Vercel Web Analytics copy keeps, by
4320
+ * command id; the rest go to Mixpanel only.
4321
4321
  */
4322
- const VERCEL_EVENT_KEYS = [
4323
- "surface",
4324
- "cli_version",
4325
- "server_kind",
4326
- "command",
4327
- "status",
4328
- "reason",
4329
- "host_agent",
4330
- "non_interactive"
4331
- ];
4322
+ const VERCEL_COMMAND_DIMENSIONS = /* @__PURE__ */ new Map([
4323
+ ["setup", "framework"],
4324
+ ["claim", "claim_outcome"],
4325
+ ["doctor", "failed_checks"],
4326
+ ["api", "operation"],
4327
+ ["start", "runtime"]
4328
+ ]);
4329
+ /**
4330
+ * The properties the Vercel Web Analytics copy of `command`'s lifecycle events
4331
+ * keeps, in priority order, the header first: the intake takes 8. The
4332
+ * command's own dimension ranks after `host_agent`, so on a failed run, where
4333
+ * `reason` fills the eighth place, it takes `non_interactive`'s.
4334
+ */
4335
+ function vercelEventKeys(command) {
4336
+ const dimension = VERCEL_COMMAND_DIMENSIONS.get(command);
4337
+ return [
4338
+ "surface",
4339
+ "cli_version",
4340
+ "server_kind",
4341
+ "command",
4342
+ "status",
4343
+ "reason",
4344
+ "host_agent",
4345
+ ...dimension === void 0 ? [] : [dimension],
4346
+ "non_interactive"
4347
+ ];
4348
+ }
4332
4349
  const FIRST_RUN_NOTICE = "Zitadel CLI collects anonymous usage analytics to help improve the tool. No personal data, project details, server URLs, or file contents are ever collected. Opt out any time with DO_NOT_TRACK=1, ZITADEL_TELEMETRY=0, or the --no-telemetry flag.";
4333
4350
  /**
4334
4351
  * Process-stable device facts shared by both the event bag and the user
@@ -4647,7 +4664,7 @@ var BaseCommand = class extends Command {
4647
4664
  env: process.env,
4648
4665
  flag,
4649
4666
  debug: this.meta.debug,
4650
- vercelKeys: VERCEL_EVENT_KEYS
4667
+ vercelKeys: vercelEventKeys(this.meta.command)
4651
4668
  });
4652
4669
  if (!this.telemetry.enabled) return;
4653
4670
  this.telemetry.track(CLI_COMMAND_STARTED, commandEventProperties(this.meta, this.telemetryInvocationId, this.telemetryProps));
@@ -5420,7 +5437,7 @@ async function resolveEnvironment(input) {
5420
5437
  })).value } : void 0
5421
5438
  }];
5422
5439
  if (input.envFile) {
5423
- const values = await readEnvFile(resolve(input.cwd, input.envFile));
5440
+ const values = await readEnvFile(resolve(input.envFile));
5424
5441
  if (values === void 0) throw new ZitadelError("E_VALIDATION", `--env-file ${input.envFile} was not found`);
5425
5442
  sources.push({
5426
5443
  source: `--env-file ${input.envFile}`,
@@ -5603,7 +5620,7 @@ const environmentFlags = {
5603
5620
  char: "e",
5604
5621
  description: "Environment to run against, e.g. production. Defaults to ZITADEL_ENV, then the hosting platform's build (VERCEL_ENV, or Netlify's CONTEXT), then development."
5605
5622
  }),
5606
- "env-file": nonBlankString({ description: "Read ZITADEL_* values from this file ahead of the process environment. Cannot be combined with --env." })
5623
+ "env-file": nonBlankString({ description: "Read ZITADEL_* values from this file ahead of the process environment. A relative path is read from the current directory, not from --cwd. Cannot be combined with --env." })
5607
5624
  };
5608
5625
  /**
5609
5626
  * The flags the `preview` command adds on top of the environment ones: which
@@ -5642,6 +5659,16 @@ function environmentSelectorSuffix(input) {
5642
5659
  if (input.cwd !== void 0) parts.push(shellOption("--cwd", input.cwd));
5643
5660
  return asEnvironmentSelector(parts.length === 0 ? "" : ` ${parts.join(" ")}`);
5644
5661
  }
5662
+ /**
5663
+ * A file in the project directory, as `--env-file` names it: relative to the
5664
+ * shell's directory, so a run with `--cwd` puts that directory in front. Node
5665
+ * reads `--env-file` from anywhere on the command line before the CLI starts,
5666
+ * from the shell's directory, and exits when the file is not there; a path
5667
+ * relative to `--cwd` would stop the suggested command before it runs.
5668
+ */
5669
+ function projectEnvFile(cwd, file) {
5670
+ return cwd === void 0 ? file : posix.join(cwd, file);
5671
+ }
5645
5672
  //#endregion
5646
5673
  //#region src/lib/oclif/environment-command.ts
5647
5674
  /**
@@ -7555,16 +7582,23 @@ async function revokeRelease(client, projectId, releaseId) {
7555
7582
  //#region src/lib/release/pipeline.ts
7556
7583
  /**
7557
7584
  * Run the before-release steps in order, skipping the ones that do not apply to
7558
- * this publish or that write on a dry run. The command creates the release and
7559
- * the deployment itself, between and after the step groups: before-release
7560
- * steps → create release → before-deploy steps → create deployment.
7585
+ * this publish or that write on a dry run, and return what they report, for the
7586
+ * command's `--json` data. The command creates the release and the deployment
7587
+ * itself, between and after the step groups: before-release steps → create
7588
+ * release → before-deploy steps → create deployment.
7561
7589
  */
7562
7590
  async function runSteps(steps, ctx) {
7591
+ let report = {};
7563
7592
  for (const step of steps) {
7564
7593
  if (!step.runsFor.includes(ctx.mode)) continue;
7565
7594
  if (ctx.dryRun && step.writes) continue;
7566
- await step.run(ctx);
7595
+ const reported = await step.run(ctx);
7596
+ if (reported) report = {
7597
+ ...report,
7598
+ ...reported
7599
+ };
7567
7600
  }
7601
+ return report;
7568
7602
  }
7569
7603
  //#endregion
7570
7604
  //#region src/lib/release/origins.ts
@@ -7676,6 +7710,310 @@ function isErrno$1(error, code) {
7676
7710
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
7677
7711
  }
7678
7712
  //#endregion
7713
+ //#region src/lib/policies/tests.ts
7714
+ /**
7715
+ * Policy tests (#1445): `.zitadel/policies/<operation>.test.json` files hold
7716
+ * cases `zitadel policies test` evaluates on the server
7717
+ * (`POST /policies/evaluate`). The CLI never evaluates a policy itself; it
7718
+ * picks the config to evaluate, sends each case and compares the decision.
7719
+ *
7720
+ * The config comes from the sibling policy file, or with `--release` from the
7721
+ * revision a release pins. Neither present means no policy: the cases are
7722
+ * evaluated with `{}`, which is what the server enforces then (only its own
7723
+ * guards), so a case that expects a refusal shows what would get through.
7724
+ *
7725
+ * **Dependency rule.** No upward imports (`commands/`, `release/`).
7726
+ */
7727
+ /** The name a test file must have: its operation, beside the operation's policy file. */
7728
+ function policyTestFileName(operation) {
7729
+ return `${operation}${POLICY_TEST_SUFFIX}`;
7730
+ }
7731
+ /**
7732
+ * What is wrong with a policy test file, each as a line naming the file, or
7733
+ * none: it must parse against the strict `policyTestFileSchema` and be named
7734
+ * after its operation, as a policy file is.
7735
+ */
7736
+ function policyTestFileIssues(data, file) {
7737
+ const result = policyTestFileSchema.safeParse(data);
7738
+ if (!result.success) return result.error.issues.map((issue) => `${file}: ${issue.path.join(".") || "(root)"} ${issue.message}`);
7739
+ const expected = policyTestFileName(result.data.operation);
7740
+ if (file.split("/").at(-1) !== expected) return [`${file} tests the ${result.data.operation} policy, so it must be named ${expected}`];
7741
+ return [];
7742
+ }
7743
+ /**
7744
+ * The test files in `.zitadel/policies/`, checked, and the problems found.
7745
+ * `operations` narrows them to those operations' files, by name and before
7746
+ * anything is parsed, so a broken test file of another operation never
7747
+ * blocks the ones asked for. One without a test file is a problem, so a typo
7748
+ * in the filter never passes silently.
7749
+ */
7750
+ async function readPolicyTests(cwd, operations = []) {
7751
+ const wanted = new Set(operations.map(policyTestFileName));
7752
+ const entries = await readJsonDirEntries(join(cwd, POLICIES_DIR), { skip: (name) => !isPolicyTestFile(name) || wanted.size > 0 && !wanted.has(name) });
7753
+ const suites = [];
7754
+ const issues = [];
7755
+ for (const entry of entries) {
7756
+ const file = `${POLICIES_DIR}/${entry.name}`;
7757
+ const problems = policyTestFileIssues(entry.body, file);
7758
+ if (problems.length > 0) {
7759
+ issues.push(...problems);
7760
+ continue;
7761
+ }
7762
+ suites.push({
7763
+ file,
7764
+ test: policyTestFileSchema.parse(entry.body)
7765
+ });
7766
+ }
7767
+ for (const operation of operations) {
7768
+ const file = policyTestFileName(operation);
7769
+ if (!entries.some((entry) => entry.name === file)) issues.push(`${POLICIES_DIR}/${file} does not exist`);
7770
+ }
7771
+ return {
7772
+ suites,
7773
+ issues
7774
+ };
7775
+ }
7776
+ /**
7777
+ * The policies in `.zitadel/policies/`, by operation, and the problems found.
7778
+ * A policy file that fails its own check is a problem rather than "no
7779
+ * policy": testing `{}` in its place would report on a policy nobody wrote.
7780
+ * `operations` narrows them to those operations' files, by name and before
7781
+ * anything is parsed, as {@link readPolicyTests} does.
7782
+ */
7783
+ async function policiesFromFiles(cwd, operations = []) {
7784
+ const wanted = new Set(operations.map(policyFileName));
7785
+ const entries = await readJsonDirEntries(join(cwd, POLICIES_DIR), { skip: (name) => isPolicyTestFile(name) || wanted.size > 0 && !wanted.has(name) });
7786
+ const policies = /* @__PURE__ */ new Map();
7787
+ const issues = [];
7788
+ for (const entry of entries) {
7789
+ const file = `${POLICIES_DIR}/${entry.name}`;
7790
+ const problems = policyFileIssues(entry.body, file);
7791
+ if (problems.length > 0) {
7792
+ issues.push(...problems);
7793
+ continue;
7794
+ }
7795
+ const { operation, config } = entry.body;
7796
+ policies.set(operation, {
7797
+ config,
7798
+ source: { file }
7799
+ });
7800
+ }
7801
+ return {
7802
+ policies,
7803
+ issues
7804
+ };
7805
+ }
7806
+ /**
7807
+ * The policies a release pins, by operation, read as the revisions it pins.
7808
+ * Whether it is revoked and whether it pins a flow come along: neither stops
7809
+ * the test, but both mean its sign-ins do not use these policies.
7810
+ */
7811
+ async function policiesFromRelease(client, projectId, releaseId) {
7812
+ const release = await client.getReleaseById(releaseId, { project_id: projectId });
7813
+ const pinned = release.pointers.filter((pointer) => pointer.kind === "policy");
7814
+ const revisions = await Promise.all(pinned.map((pointer) => client.getPolicyRevision(pointer.revision_id, { project_id: projectId })));
7815
+ return {
7816
+ policies: new Map(pinned.map((pointer, index) => [pointer.handle, {
7817
+ config: revisions[index]?.config,
7818
+ source: { revision_id: pointer.revision_id }
7819
+ }])),
7820
+ revoked: release.revoked_at !== null,
7821
+ pinsFlow: release.pointers.some((pointer) => pointer.kind === "flow_definition")
7822
+ };
7823
+ }
7824
+ /**
7825
+ * Whether a decision is what a case expects: `allowed` always, `code` when
7826
+ * given, and `violations` when given as exactly that set of rule names, so a
7827
+ * rule that fails unexpectedly fails the case too.
7828
+ */
7829
+ function caseHolds(expected, actual) {
7830
+ if (expected.allowed !== actual.allowed) return false;
7831
+ if (expected.code !== void 0 && expected.code !== actual.code) return false;
7832
+ if (expected.violations !== void 0) {
7833
+ const got = Object.keys(actual.violations ?? {}).sort();
7834
+ const want = [...new Set(expected.violations)].sort();
7835
+ return got.length === want.length && got.every((name, index) => name === want[index]);
7836
+ }
7837
+ return true;
7838
+ }
7839
+ /** How many evaluate requests a run keeps in flight at once. */
7840
+ const EVALUATE_CONCURRENCY = 8;
7841
+ /** `fn` over `items` with at most `limit` calls in flight, results in item order. */
7842
+ async function mapBounded(items, limit, fn) {
7843
+ const out = new Array(items.length);
7844
+ let next = 0;
7845
+ const worker = async () => {
7846
+ while (next < items.length) {
7847
+ const index = next++;
7848
+ out[index] = await fn(items[index]);
7849
+ }
7850
+ };
7851
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
7852
+ return out;
7853
+ }
7854
+ /**
7855
+ * Evaluates every case of every suite against its operation's policy, or `{}`
7856
+ * when there is none: one request per case, a few in flight at once, the
7857
+ * results in file and case order.
7858
+ */
7859
+ async function runPolicyTests(client, projectId, suites, policies) {
7860
+ const outcomes = await mapBounded(suites.flatMap(({ test }, suite) => test.cases.map((testCase) => ({
7861
+ suite,
7862
+ operation: test.operation,
7863
+ testCase
7864
+ }))), EVALUATE_CONCURRENCY, async ({ suite, operation, testCase }) => {
7865
+ const actual = await client.evaluatePolicy({
7866
+ operation,
7867
+ config: policies.get(operation)?.config ?? {},
7868
+ input: testCase.input,
7869
+ ...testCase.facts === void 0 ? {} : { facts: testCase.facts }
7870
+ }, { project_id: projectId });
7871
+ return {
7872
+ suite,
7873
+ result: {
7874
+ name: testCase.name,
7875
+ passed: caseHolds(testCase.expect, actual),
7876
+ expected: testCase.expect,
7877
+ actual
7878
+ }
7879
+ };
7880
+ });
7881
+ return suites.map(({ file, test }, suite) => ({
7882
+ operation: test.operation,
7883
+ file,
7884
+ policy: policies.get(test.operation)?.source ?? null,
7885
+ cases: outcomes.filter((outcome) => outcome.suite === suite).map(({ result }) => result)
7886
+ }));
7887
+ }
7888
+ /**
7889
+ * A decision in words: `allowed`, or refused with its code and its rules,
7890
+ * each when known, so a mismatch in either shows on the line.
7891
+ */
7892
+ function describeDecision(decision) {
7893
+ if (decision.allowed) return "allowed";
7894
+ const rules = decision.violations?.length ? decision.violations.join(", ") : void 0;
7895
+ const reason = [decision.code, rules].filter((part) => part !== void 0).join(": ");
7896
+ return reason === "" ? "refused" : `refused (${reason})`;
7897
+ }
7898
+ /** Where a suite's policy came from, as its heading names it. */
7899
+ function describePolicy(policy, release) {
7900
+ if (policy === null) return release === void 0 ? "no policy file, nothing enforced" : `release ${release} pins no policy, nothing enforced`;
7901
+ if ("file" in policy) return policy.file;
7902
+ return `release ${release}, revision ${policy.revision_id}`;
7903
+ }
7904
+ /**
7905
+ * The results for a terminal: each test file under a heading naming the
7906
+ * policy it ran against, a line per case, and on a failed case what was
7907
+ * expected and what the server decided; then the totals.
7908
+ */
7909
+ function renderPolicyTestResults(results, release) {
7910
+ const lines = [];
7911
+ let passed = 0;
7912
+ let failed = 0;
7913
+ for (const result of results) {
7914
+ lines.push(`${result.operation} (${describePolicy(result.policy, release)})`);
7915
+ for (const testCase of result.cases) {
7916
+ if (testCase.passed) {
7917
+ passed++;
7918
+ lines.push(` ✓ ${testCase.name}`);
7919
+ continue;
7920
+ }
7921
+ failed++;
7922
+ const actual = describeDecision({
7923
+ allowed: testCase.actual.allowed,
7924
+ code: testCase.actual.code,
7925
+ violations: Object.keys(testCase.actual.violations ?? {})
7926
+ });
7927
+ lines.push(` ✗ ${testCase.name}: expected ${describeDecision(testCase.expected)}, got ${actual}`);
7928
+ }
7929
+ }
7930
+ lines.push("", failed === 0 ? `${passed} passed` : `${passed} passed, ${failed} failed`);
7931
+ return lines.join("\n");
7932
+ }
7933
+ //#endregion
7934
+ //#region src/commands/policies/test.ts
7935
+ /**
7936
+ * `zitadel policies test` — evaluate the cases in
7937
+ * `.zitadel/policies/<operation>.test.json` on the server (#1445).
7938
+ *
7939
+ * Against the sibling policy files by default, or with `--release` against
7940
+ * the revisions a built release pins. The server evaluates every case with
7941
+ * the live operation's own check (`POST /policies/evaluate`); nothing is read
7942
+ * from or written to the project's policies. A failing case fails the
7943
+ * command with `E_VALIDATION`, carrying the full results.
7944
+ */
7945
+ var PoliciesTest = class PoliciesTest extends EnvironmentCommand {
7946
+ static description = "Run the test cases in .zitadel/policies/ against the policies";
7947
+ static group = CommandGroups.configuration;
7948
+ static examples = [
7949
+ "<%= config.bin %> policies test",
7950
+ "<%= config.bin %> policies test auth.factor.password.set",
7951
+ "<%= config.bin %> policies test --release rel_01KXB7C2M4N7F0N9WD3P2E4YM7",
7952
+ "<%= config.bin %> policies test --json"
7953
+ ];
7954
+ static args = { operation: Args.string({
7955
+ required: false,
7956
+ description: "Test only this operation's policy, such as auth.factor.password.set."
7957
+ }) };
7958
+ static flags = { release: Flags.string({ description: "Test the policies a release pins (rel_…) instead of the files in .zitadel/policies/." }) };
7959
+ async run() {
7960
+ const { args, flags } = await this.parse(PoliciesTest);
7961
+ await this.toMeta(flags);
7962
+ const { cwd } = this.meta;
7963
+ const operations = args.operation === void 0 ? [] : [args.operation];
7964
+ const { suites, issues } = await readPolicyTests(cwd, operations);
7965
+ if (issues.length > 0) throw new ZitadelError("E_VALIDATION", "The policy test files are not valid", {
7966
+ details: { issues },
7967
+ hint: issues.join("\n")
7968
+ });
7969
+ if (suites.length === 0) return this.emit({
7970
+ status: "skipped",
7971
+ reason: "no-policy-tests",
7972
+ pretty: `No policy test files in ${POLICIES_DIR}/. Add <operation>.test.json beside a policy; ${POLICIES_DIR}/README.md shows one.`
7973
+ });
7974
+ const { client, projectId } = await this.connect();
7975
+ let policies;
7976
+ if (flags.release === void 0) {
7977
+ const fromFiles = await policiesFromFiles(cwd, operations);
7978
+ if (fromFiles.issues.length > 0) throw new ZitadelError("E_VALIDATION", "The policy files are not valid", {
7979
+ details: { issues: fromFiles.issues },
7980
+ hint: fromFiles.issues.join("\n")
7981
+ });
7982
+ policies = fromFiles.policies;
7983
+ } else {
7984
+ const fromRelease = await policiesFromRelease(client, projectId, flags.release);
7985
+ if (fromRelease.revoked) reportWarning(`Release ${flags.release} is revoked, so no origin serves its policies.`);
7986
+ if (!fromRelease.pinsFlow) reportWarning(`Release ${flags.release} pins no flow, so sign-ins served it use the newest policy revisions, not the ones tested here.`);
7987
+ policies = fromRelease.policies;
7988
+ }
7989
+ const results = await runPolicyTests(client, projectId, suites, policies);
7990
+ const cases = results.flatMap((result) => result.cases);
7991
+ const failed = cases.filter((testCase) => !testCase.passed).length;
7992
+ this.recordTelemetry({
7993
+ policy_source: flags.release === void 0 ? "files" : "release",
7994
+ case_count: cases.length,
7995
+ failed_count: failed
7996
+ });
7997
+ const data = {
7998
+ source: flags.release === void 0 ? "files" : "release",
7999
+ ...flags.release === void 0 ? {} : { release_id: flags.release },
8000
+ operations: results,
8001
+ passed: cases.length - failed,
8002
+ failed
8003
+ };
8004
+ const pretty = renderPolicyTestResults(results, flags.release);
8005
+ if (failed > 0) throw new ZitadelError("E_VALIDATION", `${failed} of ${cases.length} policy test cases failed`, {
8006
+ details: data,
8007
+ hint: pretty
8008
+ });
8009
+ return this.emit({
8010
+ status: "ok",
8011
+ data,
8012
+ pretty
8013
+ });
8014
+ }
8015
+ };
8016
+ //#endregion
7679
8017
  //#region src/lib/browser.ts
7680
8018
  /**
7681
8019
  * Openers tried in order on Linux and the BSDs. `xdg-open` is the standard and
@@ -8706,7 +9044,7 @@ var Deploy = class Deploy extends EnvironmentCommand {
8706
9044
  }
8707
9045
  const { bundle, resourceCount, brandingSources, idpFiles, policyFiles } = await buildBundle(cwd);
8708
9046
  this.recordTelemetry({ resources: resourceCount });
8709
- await runSteps([validateStep, diffStep], {
9047
+ const reported = await runSteps([validateStep, diffStep], {
8710
9048
  cwd,
8711
9049
  client,
8712
9050
  projectId,
@@ -8726,7 +9064,8 @@ var Deploy = class Deploy extends EnvironmentCommand {
8726
9064
  data: {
8727
9065
  dry_run: true,
8728
9066
  resources: resourceCount,
8729
- targets: ["primary"]
9067
+ targets: ["primary"],
9068
+ ...reported
8730
9069
  },
8731
9070
  pretty: `Validated ${resourceCount} resource${resourceCount === 1 ? "" : "s"}. Nothing was deployed (dry run).`
8732
9071
  });
@@ -8773,6 +9112,7 @@ var Deploy = class Deploy extends EnvironmentCommand {
8773
9112
  deployment_id: deployment.id,
8774
9113
  resources: resourceCount,
8775
9114
  origins: servedOrigins,
9115
+ ...reported,
8776
9116
  ...nextCommandsData([this.suggest("deployment list")])
8777
9117
  },
8778
9118
  pretty: [
@@ -10274,6 +10614,8 @@ ${themingParagraph}
10274
10614
 
10275
10615
  ${sessionStateParagraph}
10276
10616
 
10617
+ After sign-in the login goes to the path in its page's \`?next=\` parameter, ahead of \`post-sign-in-url\`, so a visitor a protected route sent to sign in lands back on that route; a page of your own can read \`next\` to tell them why they must sign in. Set \`force-post-sign-in-url\` (SDK wrappers: \`forcePostSignInUrl\`) on a login whose page uses \`next\` for something else.
10618
+
10277
10619
  Machine-readable dialect (read these before authoring flow or schema edits):
10278
10620
 
10279
10621
  - \`zitadel.json\` carries \`"$schema": "./.zitadel/meta/project.json"\`, the schema of the project configuration.
@@ -15697,9 +16039,13 @@ var Orca = class {
15697
16039
  details: { entries: target.entries }
15698
16040
  });
15699
16041
  assertNpmSafeScaffoldDirectoryName(cwd);
16042
+ const created = await mkdir(cwd, { recursive: true });
15700
16043
  const before = await listEntries(cwd);
15701
16044
  const stash = await stashScaffoldDir(cwd, target.entries);
15702
- const undo = () => undoScaffold(cwd, before, stash);
16045
+ const undo = () => created === void 0 ? undoScaffold(cwd, before, stash) : rm(created, {
16046
+ recursive: true,
16047
+ force: true
16048
+ });
15703
16049
  let facts;
15704
16050
  try {
15705
16051
  await this.scaffolderFor(framework).scaffold(cwd, framework);
@@ -15788,7 +16134,7 @@ function createOrca() {
15788
16134
  return new Orca(detectors, scaffolders, patchers);
15789
16135
  }
15790
16136
  async function inspectScaffoldTarget(cwd) {
15791
- const names = [...await readdir(cwd)].sort();
16137
+ const names = [...await scaffoldTargetEntries(cwd)].sort();
15792
16138
  const others = [];
15793
16139
  for (const name of names) if (!await isLocalRuntimeEntry(cwd, name)) others.push(name);
15794
16140
  if (others.length === 0) return {
@@ -15802,6 +16148,35 @@ async function inspectScaffoldTarget(cwd) {
15802
16148
  };
15803
16149
  }
15804
16150
  /**
16151
+ * The entries of a scaffold target, or none when it does not exist yet: the
16152
+ * scaffold creates it, as a framework's own generator creates `my-app`, so a
16153
+ * `--cwd` naming the new app's directory works before that directory does.
16154
+ */
16155
+ async function scaffoldTargetEntries(cwd) {
16156
+ try {
16157
+ return await readdir(cwd);
16158
+ } catch (error) {
16159
+ if (isErrno$2(error, "ENOENT")) return [];
16160
+ throw error;
16161
+ }
16162
+ }
16163
+ /**
16164
+ * Refuses a project directory that is a file, or lies under one, before
16165
+ * anything reads inside it. One that does not exist yet passes: a scaffold
16166
+ * creates it.
16167
+ */
16168
+ async function assertDirectoryOrMissing(cwd) {
16169
+ let isDirectory;
16170
+ try {
16171
+ isDirectory = (await stat(cwd)).isDirectory();
16172
+ } catch (error) {
16173
+ if (isErrno$2(error, "ENOENT")) return;
16174
+ if (!isErrno$2(error, "ENOTDIR")) throw error;
16175
+ isDirectory = false;
16176
+ }
16177
+ if (!isDirectory) throw new ZitadelError("E_VALIDATION", `${cwd} is not a directory`, { hint: "Point --cwd at the app's directory, or at one that does not exist yet for setup to create." });
16178
+ }
16179
+ /**
15805
16180
  * Whether `name` in `cwd` is one of the entries `zitadel start` writes
15806
16181
  * (`ensureLocalState`): a `.zitadel` that holds nothing but the local runtime,
15807
16182
  * or a `.gitignore` that holds nothing but the line ignoring it. Either may be
@@ -16557,6 +16932,54 @@ function devCommandFor(packageManager) {
16557
16932
  case "bun": return command("bun", ["run", "dev"]);
16558
16933
  }
16559
16934
  }
16935
+ /**
16936
+ * The flag that runs a package manager in another directory, and whether a
16937
+ * relative directory may be given to it: npm's `--prefix` and pnpm's `--dir`
16938
+ * read one from the shell's directory, while bun documents `--cwd` as an
16939
+ * absolute path and Yarn 2+ has mishandled relative ones, so those two get the
16940
+ * resolved directory.
16941
+ */
16942
+ const DIRECTORY_FLAG = {
16943
+ npm: {
16944
+ flag: "--prefix",
16945
+ relative: true
16946
+ },
16947
+ pnpm: {
16948
+ flag: "--dir",
16949
+ relative: true
16950
+ },
16951
+ yarn: {
16952
+ flag: "--cwd",
16953
+ relative: false
16954
+ },
16955
+ bun: {
16956
+ flag: "--cwd",
16957
+ relative: false
16958
+ }
16959
+ };
16960
+ /**
16961
+ * `packageCommand` as suggested from the shell's directory when the project is
16962
+ * elsewhere (`npm --prefix app run dev`), for a run given `--cwd`. Each
16963
+ * suggested command runs in a shell of its own, so a `cd` in front of one would
16964
+ * not carry over to the next.
16965
+ *
16966
+ * @param typed - `--cwd` as typed; `undefined` leaves the command as it is.
16967
+ * @param resolved - The same directory, absolute.
16968
+ */
16969
+ function inDirectory(packageCommand, typed, resolved) {
16970
+ if (typed === void 0) return packageCommand;
16971
+ const { flag, relative } = DIRECTORY_FLAG[packageCommand.command];
16972
+ const dir = relative ? typed : resolved;
16973
+ return {
16974
+ command: packageCommand.command,
16975
+ args: [...optionArgs(flag, dir), ...packageCommand.args],
16976
+ display: [
16977
+ packageCommand.command,
16978
+ shellOption(flag, dir),
16979
+ ...packageCommand.args
16980
+ ].join(" ")
16981
+ };
16982
+ }
16560
16983
  async function runPackageCommand(packageCommand, options) {
16561
16984
  await new Promise((resolve, reject) => {
16562
16985
  const child = spawn(packageCommand.command, packageCommand.args, {
@@ -17108,6 +17531,7 @@ var Doctor = class Doctor extends BaseCommand {
17108
17531
  const { cwd, dryRun } = this.meta;
17109
17532
  const nearest = await readNearestRuntimeMetadata(cwd);
17110
17533
  const existingRuntime = nearest?.runtime;
17534
+ const parentRuntimeDir = nearest !== void 0 && nearest.dir !== cwd ? nearest.dir : void 0;
17111
17535
  const runtimeBackend = resolveRuntimeBackend$1({
17112
17536
  runtime: flags.runtime,
17113
17537
  image: flags.image,
@@ -17115,7 +17539,7 @@ var Doctor = class Doctor extends BaseCommand {
17115
17539
  existingRuntime
17116
17540
  });
17117
17541
  const image = flags.image ?? this.meta.env.ZITADEL_LOCAL_IMAGE ?? defaultLocalServerImageForCliVersion(this.meta.cliVersion);
17118
- const runtimeChecks = hostedServer === void 0 ? await runLocalRuntimeChecks(nearest?.dir ?? cwd, existingRuntime, runtimeBackend, image, port, this.localServers()) : [];
17542
+ const runtimeChecks = hostedServer === void 0 ? await runLocalRuntimeChecks(nearest?.dir ?? cwd, existingRuntime, runtimeBackend, image, port, this.localServers(), parentRuntimeDir) : [];
17119
17543
  const hasConfig = await hasZitadelConfig(cwd);
17120
17544
  const ctx = {
17121
17545
  cwd,
@@ -17165,7 +17589,7 @@ var Doctor = class Doctor extends BaseCommand {
17165
17589
  }).then((environment) => environment.values.ZITADEL_PROJECT_ID?.value === projectId, () => false);
17166
17590
  const claimSelector = await namesProject(join(cwd, ".env.development.local"), Promise.resolve(projectId)) ? environmentSelectorSuffix({
17167
17591
  cwd: cwdFlag,
17168
- envFile: ".env.development.local",
17592
+ envFile: projectEnvFile(cwdFlag, ".env.development.local"),
17169
17593
  server: projectServer
17170
17594
  }) : bareResolves ? environmentSelectorSuffix({
17171
17595
  cwd: cwdFlag,
@@ -17195,7 +17619,7 @@ var Doctor = class Doctor extends BaseCommand {
17195
17619
  ...warningAdvice ? nextCommandsData(warningAdvice.nextCommands) : {}
17196
17620
  };
17197
17621
  if (failed.length > 0) {
17198
- const advice = failureAdvice(failed, image, port, this.meta.cliVersion, selector, nearest !== void 0 && nearest.dir !== cwd ? nearest.dir : void 0);
17622
+ const advice = failureAdvice(failed, image, port, this.meta.cliVersion, selector, parentRuntimeDir, cwdFlag);
17199
17623
  const code = failed.some((check) => check.name === "port") ? "E_PORT_IN_USE" : failed.find((check) => check.code !== void 0)?.code ?? "E_VALIDATION";
17200
17624
  throw new ZitadelError(code, "Zitadel doctor found issues", {
17201
17625
  hint: advice.hint,
@@ -17213,7 +17637,7 @@ var Doctor = class Doctor extends BaseCommand {
17213
17637
  });
17214
17638
  }
17215
17639
  };
17216
- function failureAdvice(failed, image, port, cliVersion, selector, runtimeDir) {
17640
+ function failureAdvice(failed, image, port, cliVersion, selector, runtimeDir, cwdFlag) {
17217
17641
  const failedNames = new Set(failed.map((check) => check.name));
17218
17642
  if (failedNames.has("server-binary")) return {
17219
17643
  hint: "The Zitadel server npm package is not available. Reinstall the CLI package, then retry.",
@@ -17249,15 +17673,28 @@ function failureAdvice(failed, image, port, cliVersion, selector, runtimeDir) {
17249
17673
  };
17250
17674
  }
17251
17675
  if (failedNames.has("runtime")) {
17252
- const at = runtimeDir === void 0 ? [] : ["--cwd", runtimeDir];
17253
- const suggestedArgs = [["start", ...at], [
17254
- "reset",
17255
- "--force",
17256
- ...at
17257
- ]];
17258
- const nextCommands = portableSuggestions(suggestedArgs, cliVersion);
17676
+ const suggestedArgs = runtimeDir === void 0 ? [["start"], ["reset", "--force"]] : [
17677
+ [
17678
+ "start",
17679
+ "--cwd",
17680
+ runtimeDir
17681
+ ],
17682
+ cwdFlag === void 0 ? ["start"] : [
17683
+ "start",
17684
+ "--cwd",
17685
+ cwdFlag
17686
+ ],
17687
+ [
17688
+ "reset",
17689
+ "--force",
17690
+ "--cwd",
17691
+ runtimeDir
17692
+ ]
17693
+ ];
17694
+ const nextCommands = suggestedArgs.flatMap((args) => portableSuggestions([args], cliVersion));
17695
+ const quotingNote = nextCommands.length < suggestedArgs.length ? ` The runtime is in ${runtimeDir ?? "the working directory"}, a directory that needs shell quoting, so the commands that name it are left out; run \`zitadel doctor --json\` and use details.suggested_args for the exact arguments.` : "";
17259
17696
  return {
17260
- hint: "Existing local runtime metadata was found, but the local Zitadel server is not healthy. Start it again or reset stale local data." + (nextCommands.length === 0 ? ` The runtime is in ${runtimeDir ?? "the working directory"}, a directory that needs shell quoting, so no command is printed; run \`zitadel doctor --json\` and use details.suggested_args for the exact arguments.` : ""),
17697
+ hint: runtimeDir === void 0 ? "Existing local runtime metadata was found, but the local Zitadel server is not healthy. Start it again or reset stale local data." + quotingNote : `This folder has no Zitadel server of its own, so doctor checked the one in the parent folder, ${runtimeDir}, and found a problem with it. To keep using it, fix it or start it again. To give this folder its own server, run start here instead. Resetting deletes the local data in ${runtimeDir}, not in this folder.${quotingNote}`,
17261
17698
  nextCommands,
17262
17699
  suggestedArgs
17263
17700
  };
@@ -17388,7 +17825,7 @@ async function hostedServerWithoutLocalRuntime(cwd, flags) {
17388
17825
  * holding its `.zitadel/local` state, or the current directory when no server
17389
17826
  * was started yet).
17390
17827
  */
17391
- async function runLocalRuntimeChecks(runtimeDir, runtime, runtimeBackend, image, port, servers) {
17828
+ async function runLocalRuntimeChecks(runtimeDir, runtime, runtimeBackend, image, port, servers, inheritedFrom) {
17392
17829
  const managedRuntimeCheck = await checkManagedRuntimeProcesses(runtime, servers);
17393
17830
  if (runtimeBackend === "binary") return [
17394
17831
  await check("server-binary", "Server npm package is available", async () => {
@@ -17399,7 +17836,7 @@ async function runLocalRuntimeChecks(runtimeDir, runtime, runtimeBackend, image,
17399
17836
  return probe.checkedPath === probe.targetPath ? `${probe.targetPath} is writable` : `${probe.targetPath} can be created (${probe.checkedPath} is writable)`;
17400
17837
  }, "warn"),
17401
17838
  await check("port", `Port ${String(port)} is available`, () => checkPortAvailability(runtime, port)),
17402
- await checkRuntime(runtime, runtimeBackend),
17839
+ await checkRuntime(runtime, runtimeBackend, inheritedFrom),
17403
17840
  managedRuntimeCheck
17404
17841
  ];
17405
17842
  const docker = await check("docker-cli", "Docker is reachable", async () => {
@@ -17430,7 +17867,7 @@ async function runLocalRuntimeChecks(runtimeDir, runtime, runtimeBackend, image,
17430
17867
  return probe.checkedPath === probe.targetPath ? `${probe.targetPath} is writable` : `${probe.targetPath} can be created (${probe.checkedPath} is writable)`;
17431
17868
  }, "warn"),
17432
17869
  await check("port", `Port ${String(port)} is available`, () => checkPortAvailability(runtime, port)),
17433
- await checkRuntime(runtime, runtimeBackend),
17870
+ await checkRuntime(runtime, runtimeBackend, inheritedFrom),
17434
17871
  managedRuntimeCheck
17435
17872
  ];
17436
17873
  }
@@ -17488,12 +17925,12 @@ function additionalManagedRuntimeProcesses(processes, runtime) {
17488
17925
  if (runtime?.backend !== "binary") return processes;
17489
17926
  return processes.filter((processInfo) => processInfo.pid !== runtime.pid && processInfo.ppid !== runtime.pid);
17490
17927
  }
17491
- async function checkRuntime(runtime, runtimeBackend) {
17928
+ async function checkRuntime(runtime, runtimeBackend, inheritedFrom) {
17492
17929
  return check("runtime", "Existing local runtime is healthy", async () => {
17493
17930
  if (!runtime) return "No existing runtime metadata";
17494
17931
  if (runtime.backend !== runtimeBackend) throw new Error(`Existing local runtime uses ${runtime.backend}; run start --runtime ${runtimeBackend} to switch backends.`);
17495
17932
  if (!await checkLocalServerHealth(runtime.server_url)) throw new Error(`${runtime.server_url} is not ready: /readyz did not answer ok`);
17496
- return `${runtime.server_url} is healthy`;
17933
+ return inheritedFrom === void 0 ? `${runtime.server_url} is healthy` : `${runtime.server_url} is healthy (server started in the parent folder ${inheritedFrom})`;
17497
17934
  });
17498
17935
  }
17499
17936
  function formatListeners(listeners) {
@@ -18007,9 +18444,10 @@ var EnvAdd = class EnvAdd extends Promptable(BaseCommand) {
18007
18444
  ]
18008
18445
  };
18009
18446
  const writtenKeys = new Set(Object.keys(values));
18447
+ const cwdFlag = typeof flags.cwd === "string" ? flags.cwd : void 0;
18010
18448
  const newEnvironment = environmentSelectorSuffix({
18011
- envFile: file,
18012
- ...typeof flags.cwd === "string" ? { cwd: flags.cwd } : {}
18449
+ envFile: projectEnvFile(cwdFlag, file),
18450
+ ...cwdFlag === void 0 ? {} : { cwd: cwdFlag }
18013
18451
  });
18014
18452
  const needsOrigin = flags.project !== void 0;
18015
18453
  const createdProject = !needsOrigin;
@@ -20335,6 +20773,14 @@ const RESOURCES = {
20335
20773
  field: "schema",
20336
20774
  operations: FILTER_OPERATIONS
20337
20775
  },
20776
+ {
20777
+ field: "schema_object_type",
20778
+ operations: [
20779
+ "equals",
20780
+ "not_equals",
20781
+ "contains"
20782
+ ]
20783
+ },
20338
20784
  {
20339
20785
  field: "status",
20340
20786
  operations: FILTER_OPERATIONS
@@ -21305,6 +21751,8 @@ async function installDependenciesForSetup(input) {
21305
21751
  const packageManager = await detectPackageManager(input.cwd);
21306
21752
  const installCommand = installCommandFor(packageManager);
21307
21753
  const devCommand = devCommandFor(packageManager);
21754
+ const shownInstall = inDirectory(installCommand, input.cwdFlag, input.cwd).display;
21755
+ const shownDev = inDirectory(devCommand, input.cwdFlag, input.cwd).display;
21308
21756
  if (!(input.scaffoldedFramework || input.depsAdded.length > 0)) return outcome({
21309
21757
  install: {
21310
21758
  status: "not-needed",
@@ -21312,7 +21760,8 @@ async function installDependenciesForSetup(input) {
21312
21760
  command: installCommand.display,
21313
21761
  reason: "no-dependency-changes"
21314
21762
  },
21315
- devCommand: devCommand.display,
21763
+ devCommand: shownDev,
21764
+ installCommand: shownInstall,
21316
21765
  cliVersion: input.cliVersion,
21317
21766
  issuer: input.issuer,
21318
21767
  selector: input.selector,
@@ -21325,7 +21774,8 @@ async function installDependenciesForSetup(input) {
21325
21774
  command: installCommand.display,
21326
21775
  reason: input.dryRun ? "dry-run" : "skip-install"
21327
21776
  },
21328
- devCommand: devCommand.display,
21777
+ devCommand: shownDev,
21778
+ installCommand: shownInstall,
21329
21779
  cliVersion: input.cliVersion,
21330
21780
  issuer: input.issuer,
21331
21781
  selector: input.selector,
@@ -21339,7 +21789,7 @@ async function installDependenciesForSetup(input) {
21339
21789
  redirectStdoutToStderr: input.json
21340
21790
  });
21341
21791
  } catch (error) {
21342
- throw installFailed(error, installCommand, devCommand, input.cwd);
21792
+ throw installFailed(error, installCommand, devCommand, shownInstall, input.cwd);
21343
21793
  }
21344
21794
  consola.success("Installed dependencies");
21345
21795
  return outcome({
@@ -21348,7 +21798,8 @@ async function installDependenciesForSetup(input) {
21348
21798
  package_manager: packageManager,
21349
21799
  command: installCommand.display
21350
21800
  },
21351
- devCommand: devCommand.display,
21801
+ devCommand: shownDev,
21802
+ installCommand: shownInstall,
21352
21803
  cliVersion: input.cliVersion,
21353
21804
  issuer: input.issuer,
21354
21805
  selector: input.selector,
@@ -21359,14 +21810,14 @@ function outcome(input) {
21359
21810
  const planCommand = projectCommand("deploy --dry-run", input.selector, input.cliVersion);
21360
21811
  const statusCommand = projectCommand("status", input.selector, input.cliVersion);
21361
21812
  const verifyActions = [
21362
- ...input.includeInstallCommand ? [`Install dependencies: ${input.install.command}`] : [],
21813
+ ...input.includeInstallCommand ? [`Install dependencies: ${input.installCommand}`] : [],
21363
21814
  `Start your project: ${input.devCommand} (then open ${input.issuer}/login)`,
21364
21815
  verifyLoginAction()
21365
21816
  ];
21366
21817
  const boxActions = [
21367
21818
  ...input.includeInstallCommand ? [{
21368
21819
  text: "Install dependencies:",
21369
- command: input.install.command
21820
+ command: input.installCommand
21370
21821
  }] : [],
21371
21822
  {
21372
21823
  text: `Start your project (then open ${input.issuer}/login):`,
@@ -21385,16 +21836,16 @@ function outcome(input) {
21385
21836
  boxActions,
21386
21837
  nextActions: [...verifyActions, ...customizeAndPublishActions(input.cliVersion, input.selector)],
21387
21838
  nextCommands: input.includeInstallCommand ? [
21388
- shellCommand(input.install.command),
21839
+ shellCommand(input.installCommand),
21389
21840
  shellCommand(input.devCommand),
21390
21841
  planCommand
21391
21842
  ] : [shellCommand(input.devCommand), planCommand]
21392
21843
  };
21393
21844
  }
21394
- function installFailed(error, installCommand, devCommand, cwd) {
21845
+ function installFailed(error, installCommand, devCommand, shownInstall, cwd) {
21395
21846
  return new ZitadelError("E_VALIDATION", `Dependency install failed: ${installCommand.display}`, {
21396
21847
  hint: `Run ${installCommand.display} in ${cwd}, then start the app with ${devCommand.display}.`,
21397
- nextCommands: [shellCommand(installCommand.display)],
21848
+ nextCommands: [shellCommand(shownInstall)],
21398
21849
  details: {
21399
21850
  command: installCommand.display,
21400
21851
  cwd,
@@ -21411,6 +21862,328 @@ function errorShape(error) {
21411
21862
  return { message: String(error) };
21412
21863
  }
21413
21864
  //#endregion
21865
+ //#region src/commands/setup/initial-commit.ts
21866
+ const run = promisify(execFile);
21867
+ /**
21868
+ * The subject of the commit setup makes. Conventional-commit style, like every
21869
+ * other commit this repository's tooling writes, and with no trailer of any
21870
+ * kind: the commit belongs to the developer, not to the tool that made it.
21871
+ */
21872
+ const INITIAL_COMMIT_SUBJECT = "chore: scaffold the project with zitadel setup";
21873
+ /**
21874
+ * Paths that must never be committed, whatever `.gitignore` happens to say.
21875
+ *
21876
+ * `.zitadel/local/` holds the local admin credential and the server's data
21877
+ * directory. Only the `.gitignore` line `start` appends keeps it out, so a
21878
+ * project missing that line would otherwise commit the credential.
21879
+ */
21880
+ function isSecretPath(path) {
21881
+ const name = path.split("/").pop() ?? path;
21882
+ if (path === ".zitadel/secret" || path.endsWith("/.zitadel/secret")) return true;
21883
+ if (isLocalRuntimePath(path)) return true;
21884
+ return name.startsWith(".env") && name !== ".env.example";
21885
+ }
21886
+ /**
21887
+ * Paths the CLI writes itself, which therefore do not count as the
21888
+ * developer's uncommitted work. Matched against a path already made relative
21889
+ * to the project directory, never the repository root: in a monorepo the two
21890
+ * differ, and comparing the wrong one refuses every subdirectory project.
21891
+ *
21892
+ * Just the two `start` leaves behind: the `.zitadel/local/` it writes and the
21893
+ * `.gitignore` line that ignores it. It runs immediately before `setup` on the
21894
+ * documented golden path, so counting those would refuse every project that
21895
+ * has ever been started. Nothing else under `.zitadel/` is exempt: a
21896
+ * connection in `.zitadel/idps/` or a policy file is the developer's, and an
21897
+ * uncommitted edit there must not be committed under setup's subject.
21898
+ */
21899
+ function isCliOwned(path) {
21900
+ return path === ".gitignore" || path.endsWith("/.gitignore") || isLocalRuntimePath(path);
21901
+ }
21902
+ /**
21903
+ * `.zitadel/local/` at any depth, not only at the project root: a nested app
21904
+ * inside the project directory has its own, holding its own admin credential.
21905
+ */
21906
+ function isLocalRuntimePath(path) {
21907
+ return /(^|\/)\.zitadel\/local(\/|$)/.test(path);
21908
+ }
21909
+ /**
21910
+ * Run git in `cwd`, returning its stdout.
21911
+ *
21912
+ * `core.quotePath=false` stops git C-quoting any path holding a non-ASCII byte
21913
+ * or a space (`"caf\303\251.txt"`). A quoted path is not the path: it matches
21914
+ * no pathspec and no suffix test, so a secret under such a name would be
21915
+ * neither recognised nor unstaged.
21916
+ */
21917
+ async function git(cwd, args, env) {
21918
+ const { stdout } = await run("git", [
21919
+ "-c",
21920
+ "core.quotePath=false",
21921
+ ...args
21922
+ ], {
21923
+ cwd,
21924
+ maxBuffer: 16777216,
21925
+ ...env ? { env: {
21926
+ ...process.env,
21927
+ ...env
21928
+ } } : {}
21929
+ });
21930
+ return stdout;
21931
+ }
21932
+ /**
21933
+ * Whether `cwd` already sits inside a git repository.
21934
+ *
21935
+ * Only git's own "not a git repository" counts as no. Every other failure — a
21936
+ * dubious-ownership refusal, a permission error, a broken `HOME` — is raised,
21937
+ * because answering "no" to one of those would skip the clean-tree check and
21938
+ * then `git init` a second repository nested inside the first.
21939
+ */
21940
+ async function insideRepository(cwd) {
21941
+ try {
21942
+ await git(cwd, ["rev-parse", "--git-dir"]);
21943
+ return true;
21944
+ } catch (error) {
21945
+ const message = messageOf(error).toLowerCase();
21946
+ if (message.includes("not a git repository") || message.includes("enoent")) return false;
21947
+ throw error;
21948
+ }
21949
+ }
21950
+ /**
21951
+ * Where the project sits inside its repository (`apps/web/`, or `""` at the
21952
+ * root). `git status` reports paths from the repository root whatever
21953
+ * directory it runs in, so this is what turns one into a project-relative path.
21954
+ */
21955
+ async function projectPrefix(cwd) {
21956
+ return (await git(cwd, ["rev-parse", "--show-prefix"])).trim();
21957
+ }
21958
+ /**
21959
+ * Whether the developer has an identity of their own for git to commit with,
21960
+ * from config or from `GIT_AUTHOR_*`/`GIT_COMMITTER_*`/`EMAIL`.
21961
+ *
21962
+ * Without one git does not fail: it guesses from the username and hostname and
21963
+ * commits anyway. `user.useConfigOnly` is what turns that guess back into an
21964
+ * error, so asking under it is the way to tell a real identity from a guessed
21965
+ * one. Nothing is passed to the commit either way — an identity this invented
21966
+ * would be a worse author than the one git derives from the machine.
21967
+ */
21968
+ async function hasOwnIdentity(cwd) {
21969
+ try {
21970
+ await git(cwd, [
21971
+ "-c",
21972
+ "user.useConfigOnly=true",
21973
+ "var",
21974
+ "GIT_AUTHOR_IDENT"
21975
+ ]);
21976
+ return true;
21977
+ } catch {
21978
+ return false;
21979
+ }
21980
+ }
21981
+ /**
21982
+ * Whether git can produce any identity at all, its own guess included.
21983
+ *
21984
+ * A bare CI container has no configured identity and often no passwd entry or
21985
+ * hostname to guess from, and there git refuses to commit. Only then is an
21986
+ * identity supplied, because no commit means no `git_sha` and the next deploy
21987
+ * has nothing to diff against.
21988
+ */
21989
+ async function canBuildIdentity(cwd) {
21990
+ try {
21991
+ await git(cwd, ["var", "GIT_AUTHOR_IDENT"]);
21992
+ return true;
21993
+ } catch {
21994
+ return false;
21995
+ }
21996
+ }
21997
+ /**
21998
+ * Last resort, used only where git can build no identity of its own.
21999
+ *
22000
+ * Passed as environment variables rather than `-c user.*`, because an
22001
+ * inherited `GIT_AUTHOR_NAME=` (empty, as some runners leave it) outranks
22002
+ * `-c` and git then refuses with "empty ident name". The environment beats
22003
+ * both the config and whatever was inherited.
22004
+ */
22005
+ const FALLBACK_IDENTITY = {
22006
+ GIT_AUTHOR_NAME: "Zitadel Setup",
22007
+ GIT_AUTHOR_EMAIL: "setup@zitadel.local",
22008
+ GIT_COMMITTER_NAME: "Zitadel Setup",
22009
+ GIT_COMMITTER_EMAIL: "setup@zitadel.local"
22010
+ };
22011
+ /**
22012
+ * Everything git considers a change under the project directory, untracked
22013
+ * files included. An untracked file counts: setup stages the whole project, so
22014
+ * one left lying about would be swept into setup's commit.
22015
+ *
22016
+ * Scoped to `.` so that in a monorepo a sibling package's work-in-progress —
22017
+ * which `git add` here would never touch — does not refuse the setup.
22018
+ */
22019
+ async function changes(cwd) {
22020
+ const fields = (await git(cwd, [
22021
+ "status",
22022
+ "--porcelain",
22023
+ "-z",
22024
+ "--untracked-files=all",
22025
+ "--",
22026
+ "."
22027
+ ])).split("\0");
22028
+ const found = [];
22029
+ for (let index = 0; index < fields.length; index += 1) {
22030
+ const entry = fields[index];
22031
+ if (entry === void 0 || entry === "") continue;
22032
+ const status = entry.slice(0, 2);
22033
+ if (status.includes("R") || status.includes("C")) index += 1;
22034
+ found.push({
22035
+ path: entry.slice(3),
22036
+ untracked: status === "??"
22037
+ });
22038
+ }
22039
+ return found;
22040
+ }
22041
+ /** The paths staged in `cwd`, relative to it and never quoted. */
22042
+ async function stagedPaths(cwd, env) {
22043
+ return (await git(cwd, [
22044
+ "diff",
22045
+ "--cached",
22046
+ "-z",
22047
+ "--name-only",
22048
+ "--no-renames",
22049
+ "--relative"
22050
+ ], env)).split("\0").filter((path) => path !== "");
22051
+ }
22052
+ /**
22053
+ * Refuse a project whose repository has anything uncommitted, before setup
22054
+ * writes a single file.
22055
+ *
22056
+ * Setup commits what it wrote so the first release records a real commit, and
22057
+ * the honest way to know what it wrote is that the tree was clean beforehand:
22058
+ * then everything uncommitted afterwards is setup's own work and nothing of
22059
+ * the developer's is swept in. A dirty tree makes that impossible to tell
22060
+ * apart, and committing someone's work-in-progress under setup's name is worse
22061
+ * than any diff it would buy. Checked up front rather than at commit time, so
22062
+ * a refusal costs nothing and leaves no half-made project behind.
22063
+ */
22064
+ async function assertCleanWorktree(cwd) {
22065
+ if (!await insideRepository(cwd)) return;
22066
+ let dirty;
22067
+ try {
22068
+ const prefix = await projectPrefix(cwd);
22069
+ dirty = (await changes(cwd)).map((change) => ({
22070
+ ...change,
22071
+ path: change.path.startsWith(prefix) ? change.path.slice(prefix.length) : change.path
22072
+ })).filter((change) => !isCliOwned(change.path));
22073
+ } catch (error) {
22074
+ throw new ZitadelError("E_CONFLICT", `Could not tell whether the git working tree is clean: ${messageOf(error)}`, { hint: "Setup commits the files it writes, and will not do that without knowing the tree started clean. Fix what git is reporting, then run setup again, or pass --no-git to set up without touching version control." });
22075
+ }
22076
+ if (dirty.length === 0) return;
22077
+ const modified = dirty.filter((change) => !change.untracked).map((change) => change.path);
22078
+ const untracked = dirty.filter((change) => change.untracked).map((change) => change.path);
22079
+ const parts = [modified.length > 0 ? `${modified.length} uncommitted change${plural(modified)}` : void 0, untracked.length > 0 ? `${untracked.length} untracked file${plural(untracked)}` : void 0].filter((part) => part !== void 0);
22080
+ throw new ZitadelError("E_CONFLICT", `The git working tree has ${parts.join(" and ")}`, {
22081
+ hint: "Setup commits the project directory so the first release records a commit, and it will not do that while there is work it did not write, which would be committed under setup's name. Run `git status` to see them, commit or stash them, then run setup again. If you have just scaffolded the app yourself, committing it first is the expected step. To set up without touching version control at all, pass --no-git.",
22082
+ details: {
22083
+ modified,
22084
+ untracked
22085
+ }
22086
+ });
22087
+ }
22088
+ /** `s` unless the list holds exactly one entry. */
22089
+ function plural(list) {
22090
+ return list.length === 1 ? "" : "s";
22091
+ }
22092
+ /**
22093
+ * Give the scaffolded project a git repository with one commit, so the release
22094
+ * setup builds next records a real `git_sha` and the deploy after it can diff
22095
+ * against this commit instead of reporting that no diff is available.
22096
+ *
22097
+ * A repository is created only when there is none; an existing one is used as
22098
+ * it stands, never nested inside. Either way the commit covers the project
22099
+ * directory, which is safe because {@link assertCleanWorktree} has already
22100
+ * refused a repository with anything uncommitted: everything there is to commit
22101
+ * by this point is what setup itself wrote.
22102
+ *
22103
+ * Nothing here prompts, so it behaves the same interactively and in CI, and no
22104
+ * failure is fatal: the worst case is the first release carrying no commit,
22105
+ * which is what happened before this existed.
22106
+ */
22107
+ async function ensureInitialCommit(cwd) {
22108
+ let scratch;
22109
+ try {
22110
+ const existing = await insideRepository(cwd);
22111
+ if (!existing) await git(cwd, ["init", "--quiet"]);
22112
+ scratch = await mkdtemp(join(tmpdir(), "zitadel-commit-"));
22113
+ const index = { GIT_INDEX_FILE: join(scratch, "index") };
22114
+ if (existing) await git(cwd, ["read-tree", "HEAD"], index).catch(() => void 0);
22115
+ await git(cwd, [
22116
+ "add",
22117
+ "--all",
22118
+ "--",
22119
+ "."
22120
+ ], index);
22121
+ const paths = await stagedPaths(cwd, index);
22122
+ const secrets = paths.filter(isSecretPath);
22123
+ if (secrets.length > 0) await git(cwd, [
22124
+ "reset",
22125
+ "--quiet",
22126
+ "--",
22127
+ ...secrets
22128
+ ], index);
22129
+ const stillStaged = (await stagedPaths(cwd, index)).filter(isSecretPath);
22130
+ if (stillStaged.length > 0) return {
22131
+ kind: "unavailable",
22132
+ reason: `${stillStaged[0]} could not be kept out of the commit, so nothing was committed`
22133
+ };
22134
+ if (paths.length === secrets.length) return {
22135
+ kind: "unavailable",
22136
+ reason: "there was nothing to commit"
22137
+ };
22138
+ const guessedIdentity = !await hasOwnIdentity(cwd);
22139
+ const invented = !await canBuildIdentity(cwd);
22140
+ await git(cwd, [
22141
+ "-c",
22142
+ "commit.gpgsign=false",
22143
+ "commit",
22144
+ "--quiet",
22145
+ "--no-verify",
22146
+ `--message=${INITIAL_COMMIT_SUBJECT}`
22147
+ ], {
22148
+ ...index,
22149
+ ...invented ? FALLBACK_IDENTITY : {}
22150
+ });
22151
+ const sha = await headSha(cwd);
22152
+ if (sha === null) return {
22153
+ kind: "unavailable",
22154
+ reason: "the commit left no HEAD to record"
22155
+ };
22156
+ await git(cwd, [
22157
+ "reset",
22158
+ "--quiet",
22159
+ "--",
22160
+ "."
22161
+ ]).catch(() => void 0);
22162
+ return {
22163
+ kind: existing ? "committed" : "created",
22164
+ sha,
22165
+ guessedIdentity,
22166
+ invented
22167
+ };
22168
+ } catch (error) {
22169
+ return {
22170
+ kind: "unavailable",
22171
+ reason: messageOf(error)
22172
+ };
22173
+ } finally {
22174
+ if (scratch) await rm(scratch, {
22175
+ recursive: true,
22176
+ force: true
22177
+ });
22178
+ }
22179
+ }
22180
+ /** git's own stderr where it has one, so the warning names the real cause. */
22181
+ function messageOf(error) {
22182
+ const stderr = typeof error === "object" && error !== null ? error.stderr : void 0;
22183
+ if (typeof stderr === "string" && stderr.trim() !== "") return stderr.trim().split("\n")[0] ?? stderr.trim();
22184
+ return error instanceof Error ? error.message : String(error);
22185
+ }
22186
+ //#endregion
21414
22187
  //#region src/commands/setup/move-dev-port.ts
21415
22188
  /**
21416
22189
  * Moves an app setup already patched to another dev port, in one step: the
@@ -21588,27 +22361,28 @@ var FrameworkConfirmPrompt = class {
21588
22361
  var PasswordPolicyPrompt = class {
21589
22362
  async ask(answers, ctx) {
21590
22363
  if (ctx.passwordMinLengthFromFlag) return answers;
22364
+ const options = [
22365
+ {
22366
+ value: RECOMMENDED_PASSWORD_MIN_LENGTH,
22367
+ label: `${RECOMMENDED_PASSWORD_MIN_LENGTH} characters — recommended`,
22368
+ hint: "NIST minimum for password-only sign-in"
22369
+ },
22370
+ {
22371
+ value: 8,
22372
+ label: "8 characters",
22373
+ hint: "NIST minimum with a second factor"
22374
+ },
22375
+ {
22376
+ value: null,
22377
+ label: "No minimum"
22378
+ }
22379
+ ];
21591
22380
  const passwordMinLength = await ctx.input({
21592
22381
  kind: "select",
21593
22382
  name: "minimum password length",
21594
22383
  message: "Which minimum length should passwords have? You can change it later.",
21595
- initialValue: 15,
21596
- options: [
21597
- {
21598
- value: 15,
21599
- label: `15 characters — recommended`,
21600
- hint: "NIST minimum for password-only sign-in"
21601
- },
21602
- {
21603
- value: 8,
21604
- label: "8 characters",
21605
- hint: "NIST minimum with a second factor"
21606
- },
21607
- {
21608
- value: null,
21609
- label: "No minimum"
21610
- }
21611
- ]
22384
+ initialValue: RECOMMENDED_PASSWORD_MIN_LENGTH,
22385
+ options
21612
22386
  });
21613
22387
  return {
21614
22388
  ...answers,
@@ -22146,7 +22920,7 @@ function signInSummary(answers) {
22146
22920
  return [
22147
22921
  answers.preset === "passkey-first" ? "Sign-in starts with a passkey, with email and password as the fallback." : "Sign-in starts with email and password.",
22148
22922
  "Password and passkey are enabled in the user schema.",
22149
- answers.passwordMinLength === null ? "Passwords have no minimum length." : `Passwords need at least ${answers.passwordMinLength ?? 15} characters.`,
22923
+ answers.passwordMinLength === null ? "Passwords have no minimum length." : `Passwords need at least ${answers.passwordMinLength ?? RECOMMENDED_PASSWORD_MIN_LENGTH} characters.`,
22150
22924
  providers.length === 0 ? "No external providers will be added." : `External providers: ${providers.join(", ")}.`
22151
22925
  ].join("\n");
22152
22926
  }
@@ -22180,7 +22954,7 @@ const RENDERER_FLAG_DESCRIPTION = UNAVAILABLE_RENDERER_IDS.length === 0 ? "Rende
22180
22954
  */
22181
22955
  function recoverySelector(cwd) {
22182
22956
  return environmentSelectorSuffix({
22183
- envFile: ".env.development.local",
22957
+ envFile: projectEnvFile(cwd, ".env.development.local"),
22184
22958
  cwd
22185
22959
  });
22186
22960
  }
@@ -22190,7 +22964,7 @@ function recoverySelector(cwd) {
22190
22964
  */
22191
22965
  function localRecoverySelector(cwd) {
22192
22966
  return environmentSelectorSuffix({
22193
- envFile: ".env.development.local",
22967
+ envFile: projectEnvFile(cwd, ".env.development.local"),
22194
22968
  server: "local",
22195
22969
  cwd
22196
22970
  });
@@ -22228,6 +23002,11 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22228
23002
  }),
22229
23003
  "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. In an app already set up, moves it to this port: the dev script, ZITADEL_ISSUER and the allowed origin." }),
22230
23004
  "skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." }),
23005
+ git: Flags.boolean({
23006
+ allowNo: true,
23007
+ default: true,
23008
+ description: "Commit the project setup writes, initialising a repository first when there is none, so a release records the commit it was built from. Refuses a repository that already has uncommitted work. Use --no-git to leave version control alone."
23009
+ }),
22231
23010
  preset: Flags.string({
22232
23011
  description: "Sign-in preset for the scaffolded schema and login flow (default: password-first).",
22233
23012
  options: [...SETUP_PRESETS]
@@ -22262,6 +23041,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22262
23041
  throw localSetupHint(error, retryOptionsFromFlags(flags), this.config.version);
22263
23042
  }
22264
23043
  const { cwd, nonInteractive, dryRun, force } = this.meta;
23044
+ await assertDirectoryOrMissing(cwd);
22265
23045
  if (await hasZitadelConfig(cwd)) {
22266
23046
  if (flags["dev-port"] !== void 0) return this.moveToDevPort(cwd, assertDevPort(flags["dev-port"]), dryRun);
22267
23047
  return this.emit({
@@ -22270,6 +23050,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22270
23050
  });
22271
23051
  }
22272
23052
  if (await hasZitadelSecret(cwd)) throw new ZitadelError("E_CONFLICT", ".zitadel/secret exists without zitadel.json", { hint: "Move the secret aside or restore zitadel.json before running setup." });
23053
+ if (flags.git !== false) await assertCleanWorktree(cwd);
22273
23054
  await assertLocalServerVersion(cwd, this.meta.source, retryOptionsFromFlags(flags), this.config.version);
22274
23055
  const wizardInput = (question) => input(question, {
22275
23056
  interactive: !nonInteractive,
@@ -22539,6 +23320,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22539
23320
  const installOutcome = await installDependenciesForSetup({
22540
23321
  cliVersion: this.meta.cliVersion,
22541
23322
  cwd,
23323
+ ...flags.cwd === void 0 ? {} : { cwdFlag: flags.cwd },
22542
23324
  depsAdded: result.depsAdded,
22543
23325
  dryRun,
22544
23326
  env: this.meta.env,
@@ -22552,6 +23334,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22552
23334
  step: "dependencies_installed",
22553
23335
  package_manager: installOutcome.install.package_manager
22554
23336
  });
23337
+ const commit = await this.makeInitialCommit(cwd, dryRun, flags.git !== false);
22555
23338
  const writtenRel = allFilesWritten.map((file) => relativeDisplayPath(cwd, file));
22556
23339
  let ownedByLocalAdmin;
22557
23340
  const isLocal = serverKind.value(answers.server) === "local";
@@ -22593,7 +23376,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22593
23376
  boxActions: [],
22594
23377
  commands: []
22595
23378
  };
22596
- const consoleCommand = unscopedCommand("console", this.meta.cliVersion);
23379
+ const consoleCommand = this.suggestUnscoped("console");
22597
23380
  const consoleStep = ownedByLocalAdmin ? {
22598
23381
  actions: [`Manage the project in the local console as ${ownedByLocalAdmin.email}: ${consoleCommand}`],
22599
23382
  boxActions: [{
@@ -22660,6 +23443,10 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22660
23443
  })),
22661
23444
  files_skipped: result.filesSkipped.map((file) => relativeDisplayPath(cwd, file)),
22662
23445
  install: installOutcome.install,
23446
+ git: {
23447
+ initialized: commit.kind === "created",
23448
+ commit: commit.kind === "created" || commit.kind === "committed" ? commit.sha : null
23449
+ },
22663
23450
  sso: answers.sso.map((answer) => {
22664
23451
  const slug = providerOf(answer.selection).slug;
22665
23452
  const outcome = ssoOutcomes.get(slug);
@@ -22707,6 +23494,26 @@ var Setup = class Setup extends Promptable(BaseCommand) {
22707
23494
  });
22708
23495
  }
22709
23496
  /**
23497
+ * Put the scaffolded project under version control, unless it already is or
23498
+ * `--no-git` said not to. Narrated but never fatal: without the commit the
23499
+ * first release simply records no provenance, which is what setup did before.
23500
+ */
23501
+ async makeInitialCommit(cwd, dryRun, enabled) {
23502
+ if (!enabled) return { kind: "skipped" };
23503
+ if (dryRun) {
23504
+ consola.info("Would initialise a git repository and commit the scaffolded project (dry run)");
23505
+ return { kind: "skipped" };
23506
+ }
23507
+ const outcome = await ensureInitialCommit(cwd);
23508
+ if (outcome.kind === "created" || outcome.kind === "committed") {
23509
+ consola.success(`Committed the project as ${outcome.sha.slice(0, 7)}`);
23510
+ if (outcome.invented) reportWarning("git could build no author identity on this machine, so the commit was made as Zitadel Setup <setup@zitadel.local>. Set user.name and user.email, then run `git commit --amend --reset-author` to put your own name on it.");
23511
+ else if (outcome.guessedIdentity) reportWarning("This machine has no git user.name/user.email, so git guessed the author from your username and hostname. Set your identity and run `git commit --amend --reset-author` to put your own name on the commit.");
23512
+ }
23513
+ if (outcome.kind === "unavailable") reportWarning(`Could not commit the scaffolded project (${outcome.reason}); the first release records no commit, so the next deploy cannot show a diff.`);
23514
+ return outcome;
23515
+ }
23516
+ /**
22710
23517
  * `setup --dev-port <n>` on an app setup already patched: moves the app to
22711
23518
  * the new port (see {@link moveDevPort}) instead of skipping the directory.
22712
23519
  */
@@ -23380,7 +24187,7 @@ var Start = class Start extends BaseCommand {
23380
24187
  api: serverUrl,
23381
24188
  console: `${serverUrl}/ui/console/`
23382
24189
  },
23383
- ...nextCommandsData([unscopedCommand("start", this.meta.cliVersion)])
24190
+ ...nextCommandsData([this.suggestUnscoped("start")])
23384
24191
  }
23385
24192
  });
23386
24193
  await ensureLocalState(this.meta.cwd);
@@ -23406,7 +24213,7 @@ var Start = class Start extends BaseCommand {
23406
24213
  };
23407
24214
  return this.emit({
23408
24215
  status: "ok",
23409
- data: readyData(runtime, alreadyRunning, this.meta.cliVersion)
24216
+ data: readyData(runtime, alreadyRunning, this.meta.cliVersion, this.suggestUnscoped("setup --server local"))
23410
24217
  });
23411
24218
  }
23412
24219
  };
@@ -23420,7 +24227,7 @@ function platformBootstrapEnabled(env) {
23420
24227
  const pinned = env.NEXTGEN_PLATFORM_PROJECT_ID;
23421
24228
  return !pinned || pinned === "proj_platform";
23422
24229
  }
23423
- function readyData(metadata, alreadyRunning, cliVersion) {
24230
+ function readyData(metadata, alreadyRunning, cliVersion, setupCommand) {
23424
24231
  return {
23425
24232
  title: alreadyRunning ? "Local Zitadel server is already running." : "Local Zitadel server is ready.",
23426
24233
  runtime: {
@@ -23448,7 +24255,7 @@ function readyData(metadata, alreadyRunning, cliVersion) {
23448
24255
  "Setup asks for a framework and sign-in method, creates or updates your app, and installs its dependencies.",
23449
24256
  serverLogAction(cliVersion)
23450
24257
  ],
23451
- ...nextCommandsData([unscopedCommand("setup --server local", cliVersion)])
24258
+ ...nextCommandsData([setupCommand])
23452
24259
  };
23453
24260
  }
23454
24261
  function validatePort(port) {
@@ -24156,6 +24963,7 @@ const COMMANDS = {
24156
24963
  "origin:add": OriginAdd,
24157
24964
  "origin:list": OriginList,
24158
24965
  "origin:rm": OriginRm,
24966
+ "policies:test": PoliciesTest,
24159
24967
  "preview:list": PreviewList,
24160
24968
  "preview:rm": PreviewRm,
24161
24969
  "release:create": ReleaseCreate,