@zitadel/cli 0.3.0 → 0.4.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,19 +1,22 @@
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";
1
+ import { $ as republishCommands, $t as processUserAgentFacts, A as isPolicyTestFile, An as sanitizeResponse, At as checkFlow, B as CommandGroups, Bt as valuesOfKind, C as createReleaseFromBundle, Cn as parseJsonObject, Ct as checkCustomSlug, D as PASSWORD_POLICY_PATH, Dn as createZitadelClient$1, Dt as refuseChangedIssuer, En as REQUEST_TIMEOUT_MS$1, Et as readConnectionFiles, F as toPolicyWireBody, Fn as isErrno$2, Ft as parseVariableValue, G as readFlowFiles, Gt as renderDetail, H as authMethods, Ht as variableInventoryCells, I as readJsonDir, In as isMissingEndpoint, It as readStdin, J as SCHEMAS_DIR, Jt as renderTable, K as readSchemaFiles, Kt as renderList, L as readJsonDirEntries, Ln as toZitadelError, Lt as toInventoryRows, M as passwordPolicyFile, Mn as listenForInterrupt, Mt as VARIABLE_TYPES, N as policyFileIssues, Nn as stopListeningForInterrupt, Nt as assertVariableName, O as POLICIES_DIR, On as escapeControlCharacters, Ot as storedCustomSettings, P as policyFileName, Pn as ZitadelError, Pt as frozenVariables, Q as republishArgs, Qt as installUserAgent, R as BRANDING_DIR, Rt as toVariableRow, S as servesPrimary, Sn as isObject, St as IDPS_DIR, T as buildBundle, Tn as stableStringify, Tt as planConnection, U as enabledMethods, Ut as variableReadScope, Vt as variableCells, W as methodsOf, Wt as variableScope, X as reportClientIdOutcome, Xt as takeReportedWarnings, Y as publishClientId, Yt as reportWarning, Z as reportSecretOutcome, Zt as buildUserAgent, _ as worktreeIsDirty, _n as normalizePublicCliJson, _t as removeSsoFromFlow, a as serverLogAction, an as nonBlankArg, at as SETUP_FLAG_NAMES, b as createDeployment, bn as publicCliCommand, bt as ssoProvidersRefusal, c as wrapForBox, cn as inShell, ct as customFlagText, d as deployMessage, dn as shellCommand, dt as givenCustomFlags, en as hostAgent, et as storeClientSecret, f as messageFlag, fn as unscopedCommand, ft as loopbackIssuerWarning, g as headSha, gn as normalizePublicCliCommands, gt as applySsoToSchema, h as runSteps, hn as maskPublicCliVersion, ht as applySsoToFlow, i as exploreAllCommands, in as envEnabled, it as SETUP_FLAG_KEYS, j as parsePasswordMinLength, jn as interruptSignal, jt as offeredSignInMethods, k as POLICY_TEST_SUFFIX, kn as sanitizeRejection, kt as FLOWS_DIR, l as DEPLOY_ARGS, ln as portableSuggestions, lt as customFlagValues, m as validateStep, mn as isTrainVersion, mt as ssoEnableArgs, n as brandingGuidanceAction, nn as loadOrCreateIdentity, nt as ENABLE_FLAG_KEYS, o as verifyLoginAction, on as nonBlankString, ot as checkGivenCustomFlags, p as deployInitialRelease, pn as isPortableShellWord, pt as selectionFromFlags, q as selectSchema, qt as renderRows, r as customizeAndPublishActions, rn as resolveConsent, rt as ENABLE_FLAG_NAMES, s as renderBoxActions, sn as asEnvironmentSelector, st as customExample, t as showSkillsHint, tn as ciProvider, tt as askConnectionEndpoints, u as diffStep, un as projectCommand, ut as flagText, v as Repository, vn as normalizePublicCliProse, vt as removeSsoFromSchema, w as revokeRelease, wn as setTopLevelJsonKey, wt as credentialVariablesOf, x as rollbackDeployment, xn as sdkVersionForCli, xt as CONNECTION_SCHEMA_REF, y as RepositoryError, yn as optionArgs, yt as ssoEditRefusal, zt as toVariableRows } from "./skills-hint-C8oe8cCo.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, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { itemsOf, nextToken, pages } from "@zitadel/api/runtime/paging";
7
+ import { pagedFunctions } from "@zitadel/api/client";
8
+ import { access, chmod, cp, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
7
9
  import { text } from "node:stream/consumers";
8
10
  import * as schemas from "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
9
11
  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";
12
+ import { API_PREFIX, apiBaseUrl } from "@zitadel/api/runtime/base-url";
10
13
  import { operations } from "@zitadel/api/generated/operations";
11
14
  import { createHash, pbkdf2Sync, randomBytes, randomUUID } from "node:crypto";
12
15
  import { execFile, spawn, spawnSync } from "node:child_process";
13
16
  import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
14
17
  import { constants, existsSync } from "node:fs";
15
18
  import { homedir, tmpdir } from "node:os";
16
- import { isDeepStrictEqual, parseEnv, promisify } from "node:util";
19
+ import { isDeepStrictEqual, parseEnv } from "node:util";
17
20
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
18
21
  import { StringDecoder } from "node:string_decoder";
19
22
  import { resolveServerBinary, serverBinaryPath } from "@zitadel/server/binary";
@@ -24,12 +27,41 @@ import mixpanelLib from "mixpanel";
24
27
  import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text as text$1 } from "@clack/prompts";
25
28
  import { RECOMMENDED_PASSWORD_MIN_LENGTH, policyConfigSchema, policyTestFileSchema, schemaConfigSchema } from "@zitadel/config/schemas";
26
29
  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
- 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";
30
+ 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, getDefaultHumanUserSchemaBody, getDefaultLoginFlow, policiesReadmeContent, schemasReadmeContent } from "@zitadel/config/defaults";
28
31
  import { BRANDING_FILE_SCHEMA_REF, META_SCHEMA_DIR, PROJECT_FILE_SCHEMA_REF, metaSchemaFiles } from "@zitadel/config/meta-schemas";
29
32
  import { defaultHumanUserSchemaUrl } from "@zitadel/config";
30
33
  import semver from "semver";
31
34
  import { builders, generateCode, parseModule } from "magicast";
35
+ import { ListEventsCategoryItem, ListFlowDefinitionsRevisions, ListSchemasRevisions, QueryGrantsBodyFilterItemOperation, QueryIdpsBodyFilterItemOperation, QueryProjectsBodyFilterItemOperation, QuerySessionsBodyFilterItemOperation, QueryTeamsBodyFilterItemOperation, QueryUsersBodyFilterItemOperation } from "@zitadel/api/generated/model";
32
36
  import { normalizeFlowBody, normalizeIdpBody, normalizeSchemaBody } from "@zitadel/config/normalize";
37
+ import * as rosie from "rosie-skills";
38
+ //#region src/lib/oclif/crud/paging.ts
39
+ /**
40
+ * The first page `pages` yields, or with `all` every page in order, as one
41
+ * list of the items `items` takes from each page. `next` reads a page's
42
+ * cursor to report it: the cursor to the page after the first, or `null` once
43
+ * `all` has read the last page.
44
+ *
45
+ * Following the cursor is the walk's job, not this one's: a client function's
46
+ * `.pages()` reads it from the page as the server sent it, before the CLI
47
+ * escapes the page, so it goes back to the server unchanged. Without `all`
48
+ * the walk is left after its first page, so no second one is fetched.
49
+ */
50
+ const collectPages = async (pages, { items, all, next }) => {
51
+ const collected = [];
52
+ for await (const page of pages) {
53
+ collected.push(...items(page));
54
+ if (!all) return {
55
+ items: collected,
56
+ next: next(page) || null
57
+ };
58
+ }
59
+ return {
60
+ items: collected,
61
+ next: null
62
+ };
63
+ };
64
+ //#endregion
33
65
  //#region src/lib/oclif/crud/zod.ts
34
66
  /**
35
67
  * Depth limit for peeling wrappers. A generated schema nests a handful at
@@ -549,7 +581,7 @@ const fieldExample = (fields) => {
549
581
  //#endregion
550
582
  //#region src/lib/api/operations.ts
551
583
  /** Every operation of the API the CLI was built with. */
552
- const OPERATIONS = operations;
584
+ const OPERATIONS$1 = operations;
553
585
  const LIST = unscopedCommand("api ls --json");
554
586
  /**
555
587
  * Why the CLI cannot call an operation, or `undefined` when it can. It holds a
@@ -566,7 +598,7 @@ const uncallable = (operation) => {
566
598
  * The operations `zitadel api ls` lists: those of one tag, and those whose id,
567
599
  * path, summary or tag contains the filter, both in any case.
568
600
  */
569
- const findOperations = ({ filter, tag }) => OPERATIONS.filter((operation) => (tag === void 0 || operation.tag.toLowerCase() === tag.toLowerCase()) && (filter === void 0 || [
601
+ const findOperations = ({ filter, tag }) => OPERATIONS$1.filter((operation) => (tag === void 0 || operation.tag.toLowerCase() === tag.toLowerCase()) && (filter === void 0 || [
570
602
  operation.id,
571
603
  operation.path,
572
604
  operation.summary,
@@ -576,12 +608,13 @@ const findOperations = ({ filter, tag }) => OPERATIONS.filter((operation) => (ta
576
608
  const pathParameterNames = (operation) => [...operation.path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1] ?? "");
577
609
  /**
578
610
  * The operation an argument names: an `operationId` (in any case, as the docs
579
- * and `@zitadel/api` spell it), or a path starting with `/`, matched against
580
- * the spec's templates. `method` is `-X`: with a path it picks among the
611
+ * and `@zitadel/api` spell it), or a path starting with `/`, with or without
612
+ * the API prefix, matched against the spec's templates. `method` is `-X`: with a path it picks among the
581
613
  * path's methods; with an id it must be the operation's own.
582
614
  */
583
615
  const resolveTarget = (ref, method) => {
584
616
  const wanted = method?.toUpperCase();
617
+ if (ref.startsWith(`${API_PREFIX}/`)) return byPath(ref.slice(API_PREFIX.length), wanted);
585
618
  if (!ref.startsWith("/")) {
586
619
  const operation = byId(ref);
587
620
  if (wanted !== void 0 && wanted !== operation.method) throw new ZitadelError("E_VALIDATION", `${operation.id} is ${operation.method} ${operation.path}, not ${wanted}`, { hint: `Leave out -X, or pass -X ${operation.method}.` });
@@ -594,10 +627,10 @@ const resolveTarget = (ref, method) => {
594
627
  return byPath(ref, wanted);
595
628
  };
596
629
  const byId = (ref) => {
597
- const operation = OPERATIONS.find((candidate) => candidate.id.toLowerCase() === ref.toLowerCase());
630
+ const operation = OPERATIONS$1.find((candidate) => candidate.id.toLowerCase() === ref.toLowerCase());
598
631
  if (operation) return operation;
599
632
  const wanted = ref.toLowerCase();
600
- const near = OPERATIONS.filter((candidate) => {
633
+ const near = OPERATIONS$1.filter((candidate) => {
601
634
  const id = candidate.id.toLowerCase();
602
635
  return id.includes(wanted) || distance(id, wanted) <= 2;
603
636
  }).map((candidate) => candidate.id);
@@ -653,7 +686,7 @@ const byPath = (ref, method) => {
653
686
  const path = at === -1 ? ref : ref.slice(0, at);
654
687
  const query = new URLSearchParams(at === -1 ? "" : ref.slice(at + 1));
655
688
  const segments = segmentsOf(path.length > 1 ? path.replace(/\/$/, "") : path);
656
- const matches = (segments ? OPERATIONS : []).map((operation) => ({
689
+ const matches = (segments ? OPERATIONS$1 : []).map((operation) => ({
657
690
  operation,
658
691
  match: matchOf(operation.path, segments ?? [])
659
692
  })).filter((candidate) => candidate.match !== void 0);
@@ -663,7 +696,7 @@ const byPath = (ref, method) => {
663
696
  hint: "List the paths with `zitadel api ls`.",
664
697
  nextCommands: [LIST]
665
698
  });
666
- const methods = OPERATIONS.filter((operation) => operation.path === template.operation.path);
699
+ const methods = OPERATIONS$1.filter((operation) => operation.path === template.operation.path);
667
700
  const operation = method === void 0 ? methods.length === 1 ? methods[0] : methods.find((candidate) => candidate.method === "GET") : methods.find((candidate) => candidate.method === method);
668
701
  if (!operation) {
669
702
  const offered = methods.map((candidate) => candidate.method).join(", ");
@@ -679,11 +712,11 @@ const byPath = (ref, method) => {
679
712
  //#region src/lib/api/request.ts
680
713
  /** The flag a field is given with, which errors name. */
681
714
  const FIELD = "--field";
682
- /** The URL a request goes to on `server`. */
715
+ /** The URL a request goes to on `server`: the API under `/api/v1` there, then the spec's path. */
683
716
  const urlOf = (server, request) => {
684
717
  const query = new URLSearchParams(request.query.map(([name, value]) => [name, value]));
685
718
  const search = query.size > 0 ? `?${query}` : "";
686
- return `${server.replace(/\/+$/, "")}${request.path}${search}`;
719
+ return `${apiBaseUrl(server)}${request.path}${search}`;
687
720
  };
688
721
  /**
689
722
  * The generated Zod schema for one part of an operation's request:
@@ -834,7 +867,15 @@ const readBody = async (res) => {
834
867
  * (`createZitadelClient`), and what the server says is escaped the same way:
835
868
  * a non-2xx is the {@link ApiError} `toZitadelError` maps to an exit code.
836
869
  */
837
- const send = async (server, token, request$1) => {
870
+ const send = async (server, token, request) => {
871
+ const { status, raw } = await sendRaw(server, token, request);
872
+ return {
873
+ status,
874
+ body: sanitizeResponse(raw)
875
+ };
876
+ };
877
+ /** {@link send}, answering the body as the server sent it; a failure is escaped all the same. */
878
+ const sendRaw = async (server, token, request$1) => {
838
879
  const url = urlOf(server, request$1);
839
880
  const { method } = request$1.operation;
840
881
  const headers = new Headers({ accept: "application/json" });
@@ -851,7 +892,7 @@ const send = async (server, token, request$1) => {
851
892
  if (!res.ok) sanitizeRejection(new ApiError(res.status, url, rawBody, `${method} ${url} returned ${res.status}`));
852
893
  return {
853
894
  status: res.status,
854
- body: sanitizeResponse(rawBody)
895
+ raw: rawBody
855
896
  };
856
897
  };
857
898
  /** The request for the page `token` names; `undefined` leaves the request as it is. */
@@ -872,33 +913,39 @@ const pageOf = (request, token) => {
872
913
  };
873
914
  /**
874
915
  * Send a paged request until the server's `next_page_token` runs out, and
875
- * answer the last page with the page field holding every page's items. The
876
- * walk is `collectPages`, the resource lists' `--all`, so a server that
877
- * repeats a cursor is caught the same way.
916
+ * answer the last page with the page field holding every page's items.
878
917
  */
879
918
  const sendAll = async (server, token, request) => {
880
919
  const paging = request.operation.paging;
881
920
  if (paging === null) return send(server, token, request);
882
921
  let last = {
883
922
  status: 200,
884
- body: void 0
923
+ raw: void 0
885
924
  };
886
- const { items } = await collectPages(async (cursor) => {
887
- last = await send(server, token, pageOf(request, cursor));
888
- return last.body;
889
- }, {
890
- items: paging.items,
925
+ const walk = pages(async (cursor) => {
926
+ last = await sendRaw(server, token, pageOf(request, cursor));
927
+ return last.raw;
928
+ }, { next: nextToken });
929
+ const { items } = await collectPages(walk, {
930
+ items: (raw) => {
931
+ try {
932
+ return sanitizeResponse(itemsOf(raw, paging.items));
933
+ } catch (error) {
934
+ throw new ZitadelError("E_VALIDATION", error instanceof Error ? error.message : String(error), { details: { response: sanitizeResponse(raw) } });
935
+ }
936
+ },
891
937
  all: true,
892
- nextPageToken: "next_page_token",
893
- flags: {
894
- all: "--paginate",
895
- onePage: "-F limit=<n> and -F page_token=<token>"
896
- }
938
+ next: nextToken
939
+ });
940
+ const page = isObject(last.raw) ? last.raw : {};
941
+ const body = sanitizeResponse({
942
+ ...page,
943
+ [paging.items]: []
897
944
  });
898
945
  return {
899
946
  status: last.status,
900
947
  body: {
901
- ...last.body,
948
+ ...body,
902
949
  [paging.items]: items
903
950
  }
904
951
  };
@@ -919,41 +966,6 @@ const curlOf = (server, request) => [
919
966
  ...request.body === void 0 ? [] : ["-H 'Content-Type: application/json'", `-d ${quote(JSON.stringify(request.body))}`]
920
967
  ].join(" \\\n ");
921
968
  //#endregion
922
- //#region src/lib/oclif/flags.ts
923
- /**
924
- * Refuse a value that is present but holds nothing.
925
- *
926
- * `required: true` is no help: it asks whether a token arrived in argv, and an
927
- * empty one satisfies it. The way this happens is the shell, not a typo —
928
- * `--client-id "$CLIENT_ID"` with the variable unset sends exactly that, the
929
- * expansion dropping the value while the quotes keep the word, so argv carries
930
- * `["--client-id", ""]`. Unquoted, the word vanishes and oclif refuses with
931
- * "expects a value"; quoted, nothing complained and the empty string travelled
932
- * on as if it were a real answer.
933
- *
934
- * Refusing during parse rather than in each command puts it at the same point
935
- * oclif's own refusal lands, so both spellings of a missing value fail the same
936
- * way, and the value that reaches `run` is already trimmed.
937
- */
938
- function refuseBlank(input, label, message) {
939
- const value = input.trim();
940
- if (value === "") throw new ZitadelError("E_VALIDATION", message, {
941
- hint: `Pass a value for ${label}, or omit it entirely.`,
942
- details: { argument: label }
943
- });
944
- return value;
945
- }
946
- /**
947
- * A string flag whose value must not be blank. See {@link refuseBlank}.
948
- *
949
- * Not for a flag that writes a field value: an empty string is a legitimate
950
- * thing to store, so the generated resource flags in `crud/fields.ts` keep
951
- * plain `Flags.string`.
952
- */
953
- const nonBlankString = Flags.custom({ parse: async (input, _command, flag) => refuseBlank(input, `--${flag.name}`, "the value is empty") });
954
- /** A positional argument whose value must not be blank. See {@link refuseBlank}. */
955
- const nonBlankArg = Args.custom({ parse: async (input, _command, arg) => refuseBlank(input, arg.name, `${arg.name} was given an empty value`) });
956
- //#endregion
957
969
  //#region src/lib/local-server/processes.ts
958
970
  /** The real answer: the machine's processes, read with `ps`. */
959
971
  var PsLocalServers = class {
@@ -4428,6 +4440,36 @@ function deviceProfileProperties(meta, distinctId) {
4428
4440
  };
4429
4441
  }
4430
4442
  //#endregion
4443
+ //#region src/lib/oclif/next-steps.ts
4444
+ /** Made right after a command printed its next steps itself; nothing else calls it. */
4445
+ function asStepsShown() {
4446
+ return {};
4447
+ }
4448
+ /**
4449
+ * The steps as envelope keys: `next_actions` and `next_commands`, each one
4450
+ * present exactly when the command set that list.
4451
+ */
4452
+ function nextStepsData(next) {
4453
+ return {
4454
+ ...next.actions === void 0 ? {} : { next_actions: [...next.actions] },
4455
+ ...next.commands === void 0 ? {} : { next_commands: [...next.commands] }
4456
+ };
4457
+ }
4458
+ /**
4459
+ * The block a run ends with: the prose, then each command on a `$` line, or
4460
+ * `""` when there is nothing to do next.
4461
+ */
4462
+ function renderNextSteps(next) {
4463
+ const actions = next.actions ?? [];
4464
+ const commands = next.commands ?? [];
4465
+ if (actions.length === 0 && commands.length === 0) return "";
4466
+ return [
4467
+ "Next:",
4468
+ ...actions.map((action) => ` ${action}`),
4469
+ ...commands.map((command) => ` $ ${command}`)
4470
+ ].join("\n");
4471
+ }
4472
+ //#endregion
4431
4473
  //#region src/lib/shell.ts
4432
4474
  /**
4433
4475
  * Quote a value for safe interpolation into a `nextCommands` string an agent or
@@ -4687,9 +4729,10 @@ var BaseCommand = class extends Command {
4687
4729
  return Boolean(process.stdout.isTTY && process.stdin.isTTY);
4688
4730
  }
4689
4731
  /**
4690
- * Final step of every command: in human mode it prints the rendered result
4691
- * (oclif suppresses {@link Command.log} under `--json`); it returns the
4692
- * envelope so oclif's `--json` path serialises it.
4732
+ * Final step of every command: in human mode it prints the rendered result,
4733
+ * then the result's next steps, then its footer (oclif suppresses
4734
+ * {@link Command.log} under `--json`); it returns the envelope so oclif's
4735
+ * `--json` path serialises it.
4693
4736
  */
4694
4737
  emit(result) {
4695
4738
  const normalized = normalizeCommandResult(result, this.meta);
@@ -4698,8 +4741,11 @@ var BaseCommand = class extends Command {
4698
4741
  status: result.status,
4699
4742
  duration_ms: Date.now() - this.telemetryStartedAt
4700
4743
  }));
4701
- const rendered = renderPretty(normalized, this.meta);
4744
+ const next = normalized.status === "ok" && normalized.nextShown !== void 0 ? "" : renderNextSteps(normalized.next ?? {});
4745
+ const beside = normalized.status === "ok" && normalized.machineReadable === true;
4746
+ const rendered = renderPretty(normalized, this.meta, beside ? "" : next);
4702
4747
  if (rendered !== "") this.log(rendered);
4748
+ if (beside && next !== "") this.logToStderr(rendered !== "" && process.stdout.isTTY ? `\n${next}` : next);
4703
4749
  if (normalized.status === "ok" && normalized.footer !== void 0) this.log(`\n${normalized.footer}`);
4704
4750
  const reported = takeReportedWarnings();
4705
4751
  const envelope = toEnvelope(withReportedWarnings(normalized, reported), this.meta);
@@ -4774,22 +4820,26 @@ var BaseCommand = class extends Command {
4774
4820
  };
4775
4821
  }
4776
4822
  };
4823
+ /** Rewrites each suggested `zitadel …` command to the public form the reader can run. */
4777
4824
  function normalizeCommandResult(result, meta) {
4778
- if (result.status === "ok") return {
4779
- ...result,
4780
- data: normalizeDataNextCommands(result.data, meta)
4781
- };
4825
+ if (result.next?.commands === void 0) return result;
4826
+ const commands = normalizePublicCliCommands(result.next.commands, meta.cliVersion);
4782
4827
  return {
4783
4828
  ...result,
4784
- data: normalizeDataNextCommands(result.data, meta),
4785
- nextCommands: normalizePublicCliCommands(result.nextCommands, meta.cliVersion)
4829
+ next: {
4830
+ ...result.next,
4831
+ commands
4832
+ }
4786
4833
  };
4787
4834
  }
4788
- function normalizeDataNextCommands(data, meta) {
4789
- if (!isObject(data) || !Array.isArray(data.next_commands)) return data;
4790
- return {
4835
+ /**
4836
+ * The data with its next steps written in. Data that is not an object (a
4837
+ * write the server answered without a body) has nowhere to hold them.
4838
+ */
4839
+ function withNextSteps(data, next) {
4840
+ return next === void 0 || !isObject(data) ? data : {
4791
4841
  ...data,
4792
- next_commands: data.next_commands.map((command) => typeof command === "string" ? normalizePublicCliCommand(command, meta.cliVersion) : command)
4842
+ ...nextStepsData(next)
4793
4843
  };
4794
4844
  }
4795
4845
  /** Add the warnings reported while the command ran to an ok result. */
@@ -4820,7 +4870,7 @@ function toEnvelope(result, meta) {
4820
4870
  if (result.status === "ok") return {
4821
4871
  ...base,
4822
4872
  status: "ok",
4823
- data: result.data,
4873
+ data: withNextSteps(result.data, result.next),
4824
4874
  warnings: result.warnings ? [...result.warnings] : []
4825
4875
  };
4826
4876
  return {
@@ -4828,7 +4878,7 @@ function toEnvelope(result, meta) {
4828
4878
  status: "skipped",
4829
4879
  reason: result.reason,
4830
4880
  data: result.data,
4831
- next_commands: result.nextCommands ? [...result.nextCommands] : void 0
4881
+ next_commands: result.next?.commands ? [...result.next.commands] : void 0
4832
4882
  };
4833
4883
  }
4834
4884
  /** Builds the failure envelope from a {@link ZitadelError} and the invocation metadata. */
@@ -4849,20 +4899,21 @@ function toErrorEnvelope(error, meta) {
4849
4899
  * Renders a {@link CommandResult} as human-facing text for non-JSON mode. A
4850
4900
  * command may supply a bespoke `pretty` string (e.g. `deploy`'s summary);
4851
4901
  * otherwise success payloads are summarised by {@link formatData} and skips are
4852
- * shown with their reason and follow-up commands.
4902
+ * shown with their reason. `next` is the rendered next-step block, which
4903
+ * follows the rendering whichever it is, and comes before the warnings.
4853
4904
  */
4854
- function renderPretty(result, meta) {
4905
+ function renderPretty(result, meta, next) {
4855
4906
  if (result.pretty !== void 0) {
4856
4907
  const warnings = result.status === "ok" ? result.warnings ?? [] : [];
4857
- return [result.pretty, ...warnings.map((warning) => `Warning: ${warning}`)].filter((line) => line !== "").join("\n");
4858
- }
4859
- if (result.status === "ok") return formatData(result.data, result.warnings ? [...result.warnings] : [], meta);
4860
- const lines = [`Skipped: ${result.reason}${suffixBlock(meta)}`];
4861
- if (result.nextCommands && result.nextCommands.length > 0) {
4862
- lines.push("Next:");
4863
- for (const cmd of result.nextCommands) lines.push(` $ ${cmd}`);
4908
+ const gap = result.status === "ok" ? "\n" : "";
4909
+ return [
4910
+ next === "" || result.pretty === "" ? result.pretty : `${result.pretty.replace(/\n+$/, "")}${gap}`,
4911
+ next,
4912
+ ...warnings.map((warning) => `Warning: ${warning}`)
4913
+ ].filter((line) => line !== "").join("\n");
4864
4914
  }
4865
- return lines.join("\n");
4915
+ if (result.status === "ok") return formatData(result.data, result.warnings ? [...result.warnings] : [], meta, next);
4916
+ return [`Skipped: ${result.reason}${suffixBlock(meta)}`, next].filter(Boolean).join("\n");
4866
4917
  }
4867
4918
  /**
4868
4919
  * Renders a {@link ZitadelError} as a human-readable block for stderr: the
@@ -4871,38 +4922,23 @@ function renderPretty(result, meta) {
4871
4922
  function renderError(error, meta) {
4872
4923
  const lines = [`Error ${error.code}: ${error.message}`];
4873
4924
  if (error.hint) lines.push(error.hint);
4874
- const nextCommands = normalizePublicCliCommands(error.nextCommands, meta.cliVersion);
4875
- if (nextCommands && nextCommands.length > 0) {
4876
- lines.push("Next:");
4877
- for (const cmd of nextCommands) lines.push(` $ ${cmd}`);
4878
- }
4925
+ const next = renderNextSteps({ commands: normalizePublicCliCommands(error.nextCommands, meta.cliVersion) });
4926
+ if (next !== "") lines.push(next);
4879
4927
  return lines.join("\n");
4880
4928
  }
4881
- function formatData(data, warnings, opts) {
4929
+ function formatData(data, warnings, opts, next) {
4882
4930
  if (typeof data === "string") {
4883
4931
  const suffix = sourceSuffix(opts);
4884
- return suffix ? `${data}\n${suffix}` : data;
4932
+ const text = suffix ? `${data}\n${suffix}` : data;
4933
+ return next === "" ? text : `${text}\n\n${next}`;
4885
4934
  }
4886
4935
  const lines = [];
4887
4936
  const titleLine = isObject(data) && typeof data.title === "string" ? String(data.title) : "Zitadel command completed.";
4888
4937
  lines.push(titleLine);
4889
4938
  const suffix = sourceSuffix(opts);
4890
4939
  if (suffix) lines.push(suffix);
4891
- if (isObject(data)) {
4892
- renderKnownSections(lines, data);
4893
- if (Array.isArray(data.next_actions) && data.next_actions.length > 0) {
4894
- lines.push("");
4895
- lines.push("Next:");
4896
- for (const action of data.next_actions) lines.push(` ${String(action)}`);
4897
- }
4898
- if (Array.isArray(data.next_commands) && data.next_commands.length > 0) {
4899
- if (!Array.isArray(data.next_actions) || data.next_actions.length === 0) {
4900
- lines.push("");
4901
- lines.push("Next:");
4902
- }
4903
- for (const cmd of data.next_commands) lines.push(` $ ${String(cmd)}`);
4904
- }
4905
- }
4940
+ if (isObject(data)) renderKnownSections(lines, data);
4941
+ if (next !== "") lines.push("", next);
4906
4942
  for (const warning of warnings.filter((warning) => !warningRenderedInChecks(data, warning))) lines.push(`Warning: ${warning}`);
4907
4943
  return lines.join("\n");
4908
4944
  }
@@ -5772,8 +5808,8 @@ var EnvironmentCommand = class extends BaseCommand {
5772
5808
  * terminal: on a pipe the result travels alone, and `--json` silences them
5773
5809
  * either way. `quiet` is for a command whose own output is that summary.
5774
5810
  *
5775
- * `verbatim` is for a caller that stores or compares what it reads (see
5776
- * `SanitizeOptions`).
5811
+ * `verbatim` is for a caller that stores, compares or sends back what it
5812
+ * reads (see `SanitizeOptions`).
5777
5813
  * `prompt: false` is for a best-effort probe that must never stop to ask
5778
5814
  * (or write `.env.<name>.local`): a missing value then fails as it would
5779
5815
  * non-interactively.
@@ -5797,7 +5833,7 @@ var EnvironmentCommand = class extends BaseCommand {
5797
5833
  consola.info(`Server ${server}`);
5798
5834
  }
5799
5835
  return {
5800
- client: createZitadelClient({
5836
+ client: createZitadelClient$1({
5801
5837
  baseUrl: server,
5802
5838
  token
5803
5839
  }, { verbatim: options.verbatim }),
@@ -6149,71 +6185,6 @@ async function publishedIdsOf(cwd, path) {
6149
6185
  }
6150
6186
  }
6151
6187
  //#endregion
6152
- //#region src/lib/oclif/envelope.ts
6153
- /**
6154
- * Builders for the `data` of a command's JSON envelope.
6155
- *
6156
- * Every command emits `data`, and an agent reads all of them, so the shapes
6157
- * must not drift. These builders are the one place each shape is defined; the
6158
- * spec harness (`assertEnvelope`) enforces the same shapes on every `--json`
6159
- * invocation, so a command that hand-builds a divergent shape fails in its own
6160
- * spec. Use the builder rather than an object literal.
6161
- */
6162
- /**
6163
- * The canonical list/collection envelope: `{ items, count, next_page_token }`,
6164
- * plus optional `next_commands`. Every command that returns a collection emits
6165
- * this, so `{ items, count, next_page_token }` reads uniformly across all of
6166
- * them. A single-page endpoint passes `nextPageToken: null` (the default); a
6167
- * paginated one passes the server cursor (or `null` once drained).
6168
- */
6169
- function listData(items, opts = {}) {
6170
- const base = {
6171
- items,
6172
- count: items.length,
6173
- next_page_token: opts.nextPageToken ?? null
6174
- };
6175
- return opts.nextCommands && opts.nextCommands.length > 0 ? {
6176
- ...base,
6177
- next_commands: opts.nextCommands
6178
- } : base;
6179
- }
6180
- /**
6181
- * The canonical delete/remove confirmation: the resource's identifier plus
6182
- * `deleted: true`. The verb is always `deleted` — never `removed` — so an agent
6183
- * reads "did the delete succeed?" the same way for every resource, whether it is
6184
- * addressed by `id` or by a natural key (`url`, `pattern`, `name`). A lifecycle
6185
- * verb (revoke, deactivate) uses {@link mutatedData} instead.
6186
- */
6187
- function deletedData(identifier) {
6188
- return {
6189
- ...identifier,
6190
- deleted: true
6191
- };
6192
- }
6193
- /**
6194
- * A mutation confirmation whose verb is the lifecycle action, not `deleted`:
6195
- * the identifier plus `<verb>: true` (e.g. `revoked`, `deactivated`). Keeps the
6196
- * `{ identifier, <past-tense>: true }` shape of {@link deletedData} while naming
6197
- * the specific action.
6198
- */
6199
- function mutatedData(identifier, verb) {
6200
- return {
6201
- ...identifier,
6202
- [verb]: true
6203
- };
6204
- }
6205
- /**
6206
- * The `next_commands` a command's `data` carries, spread into the data object:
6207
- * `{ ...result, ...nextCommandsData(commands) }`. It takes `SuggestedCommand`s,
6208
- * which only the `lib/suggestion` builders make, so every entry has had its
6209
- * environment selector decided (#1689). A Biome GritQL rule
6210
- * (`biome-plugins/no-raw-next-commands.grit`) rejects a hand-written
6211
- * `next_commands:` key anywhere else.
6212
- */
6213
- function nextCommandsData(commands) {
6214
- return { next_commands: commands };
6215
- }
6216
- //#endregion
6217
6188
  //#region src/lib/oclif/promptable.ts
6218
6189
  /**
6219
6190
  * Marks a command that can ask a person a question, and gives it the one way
@@ -6353,7 +6324,8 @@ var AuthMethodCommand = class extends Promptable(EnvironmentCommand) {
6353
6324
  }
6354
6325
  /**
6355
6326
  * The follow-up commands of a result, as strings when every argument is
6356
- * safe to run as written, and always as argument lists.
6327
+ * safe to run as written (the result's `next` commands), and always as
6328
+ * argument lists (its `next_args`).
6357
6329
  *
6358
6330
  * @param changed - Whether a file changed, which `deploy` then publishes.
6359
6331
  * @param cwdArgs - `--cwd` for `deploy`, from {@link target}.
@@ -6372,8 +6344,8 @@ var AuthMethodCommand = class extends Promptable(EnvironmentCommand) {
6372
6344
  ...cwdArgs
6373
6345
  ]] : []];
6374
6346
  return {
6375
- ...nextCommandsData(portableSuggestions(args, this.meta.cliVersion, [DEPLOY_MESSAGE])),
6376
- next_args: args
6347
+ commands: portableSuggestions(args, this.meta.cliVersion, [DEPLOY_MESSAGE]),
6348
+ args
6377
6349
  };
6378
6350
  }
6379
6351
  /**
@@ -6443,12 +6415,14 @@ var AuthMethodCommand = class extends Promptable(EnvironmentCommand) {
6443
6415
  consola.success(`Updated ${schema.path}`);
6444
6416
  } else consola.info(`${method} is already ${verb}d for ${schema.name}`);
6445
6417
  this.reportOutcome(outcome);
6418
+ const followUps = this.followUps(change.changed, cwdArgs);
6446
6419
  return this.emit({
6447
6420
  status: "ok",
6448
6421
  data: {
6449
6422
  ...data,
6450
- ...this.followUps(change.changed, cwdArgs)
6423
+ next_args: followUps.args
6451
6424
  },
6425
+ next: { commands: followUps.commands },
6452
6426
  pretty: change.changed ? `${enabled ? "Enabled" : "Disabled"} ${method} for ${schema.name}` : `Nothing to ${verb} for ${schema.name}`
6453
6427
  });
6454
6428
  }
@@ -6607,7 +6581,7 @@ var Api = class Api extends Promptable(BaseCommand) {
6607
6581
  }
6608
6582
  /** Choose an operation the CLI can call: its part of the API, then the operation. */
6609
6583
  async chooseOperation() {
6610
- const callable = OPERATIONS.filter((operation) => uncallable(operation) === void 0);
6584
+ const callable = OPERATIONS$1.filter((operation) => uncallable(operation) === void 0);
6611
6585
  const retry = this.suggestUnscoped("api ls --json");
6612
6586
  const tag = await this.input({
6613
6587
  kind: "select",
@@ -6652,69 +6626,6 @@ var Api = class Api extends Promptable(BaseCommand) {
6652
6626
  }
6653
6627
  };
6654
6628
  //#endregion
6655
- //#region src/lib/oclif/crud/table.ts
6656
- /**
6657
- * The machine form of a list: one record per line, fields separated by tabs,
6658
- * no header and no padding, so `cut -f2` and `awk -F'\t'` work. This is what a
6659
- * piped run emits — the aligned table is for a terminal, where nothing is
6660
- * parsing it. Tabs and newlines inside a value would break the record, so they
6661
- * are escaped rather than passed through.
6662
- */
6663
- const renderRows = (columns, items) => items.map((item) => columns.map((column) => readCell(column, item).replaceAll(" ", "\\t").replaceAll("\n", "\\n")).join(" ")).join("\n");
6664
- /**
6665
- * Render `items` as an aligned text table, one column per dot-path in
6666
- * `columns`, headed by each path's last segment. Nested paths resolve
6667
- * through objects; missing values render empty; non-strings render as JSON.
6668
- */
6669
- const renderTable = (columns, items) => {
6670
- const headers = columns.map((column) => column.split(".").at(-1) ?? column);
6671
- const rows = items.map((item) => columns.map((column) => readCell(column, item)));
6672
- const widths = rows.reduce((current, row) => current.map((width, i) => Math.max(width, row[i]?.length ?? 0)), headers.map((header) => header.length));
6673
- const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
6674
- return [
6675
- line(headers),
6676
- line(widths.map((width) => "-".repeat(width))),
6677
- ...rows.map(line)
6678
- ].join("\n");
6679
- };
6680
- /**
6681
- * The whole terminal rendering of a list, so no command re-derives the
6682
- * plain/TTY/empty/footer logic (and so none forgets `--plain`): a pipe (or
6683
- * `--plain`) gets tab-separated rows with no header and nothing when empty, so
6684
- * `cut`/`awk`/`wc -l` work; a terminal gets the aligned table, a blank line, a
6685
- * `${n} ${singular|plural}` count, and any `footer` lines. Pair it with
6686
- * `listData()` for the JSON envelope.
6687
- */
6688
- const renderList = (columns, rows, opts) => {
6689
- if (opts.plain) return renderRows(columns, rows);
6690
- if (rows.length === 0) return opts.empty;
6691
- const count = `${rows.length} ${rows.length === 1 ? opts.singular : opts.plural ?? `${opts.singular}s`}`;
6692
- return [
6693
- renderTable(columns, rows),
6694
- "",
6695
- count,
6696
- ...opts.footer ?? []
6697
- ].join("\n");
6698
- };
6699
- /** One cell: a dot-path resolved through objects, rendered as text. */
6700
- const readCell = (column, item) => {
6701
- const value = column.split(".").reduce((current, key) => isObject(current) ? current[key] : void 0, item);
6702
- return value == null ? "" : typeof value === "string" ? value : JSON.stringify(value);
6703
- };
6704
- /**
6705
- * One record for a person: the heading first, then a labelled field per line.
6706
- * Nested paths are labelled by their last segment, so `metadata.status` reads
6707
- * as `status`. It is the terminal rendering of `get`; a pipe (and `--json`)
6708
- * still receives the whole object, since a script wants the record, not a view
6709
- * of it.
6710
- */
6711
- const renderDetail = (heading, fields, item) => {
6712
- const rows = fields.map((field) => [field.split(".").at(-1) ?? field, readCell(field, item)]).filter(([, value]) => value !== "");
6713
- const width = Math.max(0, ...rows.map(([label]) => label.length));
6714
- const body = rows.map(([label, value]) => `${label.padEnd(width)} ${value}`);
6715
- return [...heading ? [heading, ""] : [], ...body].join("\n");
6716
- };
6717
- //#endregion
6718
6629
  //#region src/commands/api/ls.ts
6719
6630
  /** One operation as `api ls` lists it. */
6720
6631
  const entryOf = (operation) => ({
@@ -6920,12 +6831,14 @@ var AuthMethodSsoDisable = class AuthMethodSsoDisable extends AuthMethodCommand
6920
6831
  for (const path of changed) consola.success(`Updated ${path}`);
6921
6832
  if (changed.length === 0) consola.info(`${schema.name} does not offer ${provider}`);
6922
6833
  this.reportOutcome(outcome);
6834
+ const followUps = this.followUps(changed.length > 0, cwdArgs);
6923
6835
  return this.emit({
6924
6836
  status: "ok",
6925
6837
  data: {
6926
6838
  ...data,
6927
- ...this.followUps(changed.length > 0, cwdArgs)
6839
+ next_args: followUps.args
6928
6840
  },
6841
+ next: { commands: followUps.commands },
6929
6842
  pretty: changed.length > 0 ? `Removed ${provider} from ${where}` : `${schema.name} does not offer ${provider}`
6930
6843
  });
6931
6844
  }
@@ -7166,11 +7079,11 @@ var AuthMethodSsoEnable = class AuthMethodSsoEnable extends AuthMethodCommand {
7166
7079
  const unpublished = !reusing || written.length > 0;
7167
7080
  const stored = secret?.published === "stored" && (clientIdState === void 0 || clientIdState === "stored") ? ["Provider credentials are stored on your Project."] : [];
7168
7081
  const name = entry.displayName;
7082
+ const followUps = this.followUps(unpublished, scopeArgs, recovery);
7169
7083
  return this.emit({
7170
7084
  status: "ok",
7171
7085
  data: {
7172
7086
  title: unpublished ? [`${name} sign-in is configured locally.`, ...stored].join(" ") : `${name} sign-in is already configured locally.`,
7173
- ...unpublished ? { next_actions: [`Deploy the configuration to make ${name} sign-in available in your app, then reload your app and test ${name} sign-in.`] } : {},
7174
7087
  ...this.payload({
7175
7088
  projectId,
7176
7089
  provider,
@@ -7183,7 +7096,11 @@ var AuthMethodSsoEnable = class AuthMethodSsoEnable extends AuthMethodCommand {
7183
7096
  changed: written,
7184
7097
  skipped: edits.skipped
7185
7098
  }),
7186
- ...this.followUps(unpublished, scopeArgs, recovery)
7099
+ next_args: followUps.args
7100
+ },
7101
+ next: {
7102
+ ...unpublished ? { actions: [`Deploy the configuration to make ${name} sign-in available in your app, then reload your app and test ${name} sign-in.`] } : {},
7103
+ commands: followUps.commands
7187
7104
  }
7188
7105
  });
7189
7106
  }
@@ -7557,50 +7474,6 @@ async function linkOnPath(cwd, path) {
7557
7474
  }
7558
7475
  }
7559
7476
  //#endregion
7560
- //#region src/lib/release/release.ts
7561
- /**
7562
- * Build a release from the `.zitadel/` bundle (`POST /releases/build`). The
7563
- * server reuses the revisions that already match and allocates the rest, and
7564
- * dedupes on content: re-posting the same bundle returns the release that
7565
- * already carries it. (`POST /releases` pins pre-existing revision ids and is
7566
- * not a CLI flow.)
7567
- */
7568
- async function createReleaseFromBundle(client, projectId, bundle, source) {
7569
- const body = {
7570
- bundle,
7571
- ...source.message === void 0 ? {} : { message: source.message },
7572
- ...source.gitSha === null ? {} : { git_sha: source.gitSha },
7573
- git_dirty: source.gitDirty
7574
- };
7575
- return await client.buildRelease(body, { project_id: projectId });
7576
- }
7577
- /** Revoke a release so its content is never served again. Idempotent. */
7578
- async function revokeRelease(client, projectId, releaseId) {
7579
- return client.revokeRelease(releaseId, { project_id: projectId });
7580
- }
7581
- //#endregion
7582
- //#region src/lib/release/pipeline.ts
7583
- /**
7584
- * Run the before-release steps in order, skipping the ones that do not apply to
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.
7589
- */
7590
- async function runSteps(steps, ctx) {
7591
- let report = {};
7592
- for (const step of steps) {
7593
- if (!step.runsFor.includes(ctx.mode)) continue;
7594
- if (ctx.dryRun && step.writes) continue;
7595
- const reported = await step.run(ctx);
7596
- if (reported) report = {
7597
- ...report,
7598
- ...reported
7599
- };
7600
- }
7601
- return report;
7602
- }
7603
- //#endregion
7604
7477
  //#region src/lib/release/origins.ts
7605
7478
  /**
7606
7479
  * Whether an origin pattern covers a concrete origin URL, matching the server's
@@ -7681,9 +7554,11 @@ var BrandingEject = class BrandingEject extends Promptable(BaseCommand) {
7681
7554
  status: "ok",
7682
7555
  data: {
7683
7556
  design,
7684
- files_written: filesWritten,
7685
- next_actions: [`Edit ${DEFAULT_BRANDING_TEMPLATE_PATH} — the login UI is yours now.`, "Deploy --dry-run validates the template and shows the diff; deploy publishes it."],
7686
- ...nextCommandsData([this.suggestUnscoped("deploy --dry-run"), this.suggestUnscoped(DEPLOY_ARGS)])
7557
+ files_written: filesWritten
7558
+ },
7559
+ next: {
7560
+ actions: [`Edit ${DEFAULT_BRANDING_TEMPLATE_PATH} — the login UI is yours now.`, "Deploy --dry-run validates the template and shows the diff; deploy publishes it."],
7561
+ commands: [this.suggestUnscoped("deploy --dry-run"), this.suggestUnscoped(DEPLOY_ARGS)]
7687
7562
  }
7688
7563
  });
7689
7564
  }
@@ -7811,12 +7686,15 @@ async function policiesFromFiles(cwd, operations = []) {
7811
7686
  async function policiesFromRelease(client, projectId, releaseId) {
7812
7687
  const release = await client.getReleaseById(releaseId, { project_id: projectId });
7813
7688
  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,
7689
+ const revisions = await Promise.all(pinned.map(async (pointer) => {
7690
+ const revision = await client.getPolicyRevision(pointer.revision_id, { project_id: projectId });
7691
+ return [pointer.handle, {
7692
+ config: revision.config,
7818
7693
  source: { revision_id: pointer.revision_id }
7819
- }])),
7694
+ }];
7695
+ }));
7696
+ return {
7697
+ policies: new Map(revisions),
7820
7698
  revoked: release.revoked_at !== null,
7821
7699
  pinsFlow: release.pointers.some((pointer) => pointer.kind === "flow_definition")
7822
7700
  };
@@ -8408,7 +8286,7 @@ var Claim = class Claim extends EnvironmentCommand {
8408
8286
  project_id: projectId,
8409
8287
  would: localRecord && !flags["no-wait"] ? `${action}, then record the owning team in .zitadel/secret.` : `${action}.`
8410
8288
  },
8411
- nextCommands: [challengeId === void 0 ? startOver : this.suggest(`claim --challenge-id ${challengeId}`)]
8289
+ next: { commands: [challengeId === void 0 ? startOver : this.suggest(`claim --challenge-id ${challengeId}`)] }
8412
8290
  });
8413
8291
  }
8414
8292
  const { client } = prompted ?? await this.connect();
@@ -8478,9 +8356,9 @@ var Claim = class Claim extends EnvironmentCommand {
8478
8356
  project_id: projectId,
8479
8357
  team_id: completed.team_id,
8480
8358
  claimed_at: completed.claimed_at,
8481
- dashboard_url: completed.dashboard_url,
8482
- next_actions: ["Manage your Project and collaborate in the Console:", completed.dashboard_url]
8483
- }
8359
+ dashboard_url: completed.dashboard_url
8360
+ },
8361
+ next: { actions: ["Manage your Project and collaborate in the Console:", completed.dashboard_url] }
8484
8362
  });
8485
8363
  }
8486
8364
  /**
@@ -8558,9 +8436,11 @@ var Claim = class Claim extends EnvironmentCommand {
8558
8436
  project_id: projectId,
8559
8437
  claim_url: challenge.claim_url,
8560
8438
  challenge_id: challenge.challenge_id,
8561
- expires_at: challenge.expires_at,
8562
- next_actions: ["Wait for the browser step to finish with:"],
8563
- ...nextCommandsData(isChallengeId(challenge.challenge_id) ? [this.suggest(`claim --challenge-id ${challenge.challenge_id}`)] : [])
8439
+ expires_at: challenge.expires_at
8440
+ },
8441
+ next: {
8442
+ actions: ["Wait for the browser step to finish with:"],
8443
+ commands: isChallengeId(challenge.challenge_id) ? [this.suggest(`claim --challenge-id ${challenge.challenge_id}`)] : []
8564
8444
  }
8565
8445
  });
8566
8446
  }
@@ -8834,14 +8714,18 @@ async function consoleSignInUrl(serverUrl, admin) {
8834
8714
  const { handoffToken } = await signIn(serverUrl, admin);
8835
8715
  return `${serverUrl}/ui/console/login?handoff=${encodeURIComponent(handoffToken)}`;
8836
8716
  }
8717
+ /** The names the server sets its session cookie under (ADR 083), over plain http to a loopback host and over https. */
8718
+ const SESSION_COOKIES = ["zitadel_session", "__Host-zitadel_session"];
8837
8719
  /**
8838
- * Signs in and trades the handoff token for the `__nextgen_session` cookie —
8839
- * the credential endpoints like `claim/complete` authenticate against, which a
8840
- * bare handoff token cannot satisfy.
8720
+ * Signs in and trades the handoff token for the session cookie — the
8721
+ * credential endpoints like `claim/complete` authenticate against, which a bare
8722
+ * handoff token cannot satisfy. It is `zitadel_session` from a server on
8723
+ * loopback http and `__Host-zitadel_session` from one on https, and goes back
8724
+ * under the name it came with.
8841
8725
  */
8842
8726
  async function adminSessionCookie(serverUrl, admin) {
8843
8727
  const { handoffToken, publishableKey } = await signIn(serverUrl, admin);
8844
- const { res } = await request(`${serverUrl}/sessions/exchange?project_id=${encodeURIComponent(PLATFORM_PROJECT_ID)}`, {
8728
+ const { res } = await request(`${apiBaseUrl(serverUrl)}/sessions/exchange?project_id=${encodeURIComponent(PLATFORM_PROJECT_ID)}`, {
8845
8729
  ...localAdminRequest(serverUrl, {
8846
8730
  "content-type": "application/json",
8847
8731
  authorization: `Bearer ${publishableKey}`
@@ -8849,7 +8733,7 @@ async function adminSessionCookie(serverUrl, admin) {
8849
8733
  method: "POST",
8850
8734
  body: JSON.stringify({ handoff_token: handoffToken })
8851
8735
  }, { signal: interruptSignal() });
8852
- const cookie = res.headers.getSetCookie().find((value) => value.startsWith("__nextgen_session="))?.split(";", 1)[0];
8736
+ const cookie = res.headers.getSetCookie().map((value) => value.split(";", 1)[0] ?? "").find((pair) => SESSION_COOKIES.some((name) => pair.startsWith(`${name}=`)));
8853
8737
  if (!res.ok || !cookie) throw new ZitadelError("E_AUTH", `Local admin session exchange failed (${String(res.status)})`, { details: { status: res.status } });
8854
8738
  return cookie;
8855
8739
  }
@@ -8864,7 +8748,7 @@ async function signIn(serverUrl, admin) {
8864
8748
  nextCommands: [unscopedCommand("stop"), unscopedCommand("start")],
8865
8749
  details: { server_url: serverUrl }
8866
8750
  });
8867
- const client = createZitadelClient({
8751
+ const client = createZitadelClient$1({
8868
8752
  baseUrl: serverUrl,
8869
8753
  token: runtime.publishable_key
8870
8754
  });
@@ -8957,9 +8841,9 @@ var Console = class Console extends BaseCommand {
8957
8841
  title: "Local console sign-in link.",
8958
8842
  signed_in_as: admin.email,
8959
8843
  sign_in_url: signInUrl,
8960
- browser_opened: opened,
8961
- next_actions: ["The link signs you in once. Run `zitadel console` again for a fresh one."]
8962
- }
8844
+ browser_opened: opened
8845
+ },
8846
+ next: { actions: ["The link signs you in once. Run `zitadel console` again for a fresh one."] }
8963
8847
  });
8964
8848
  }
8965
8849
  };
@@ -9017,30 +8901,30 @@ var Deploy = class Deploy extends EnvironmentCommand {
9017
8901
  static group = CommandGroups.configuration;
9018
8902
  static groupOrder = 1;
9019
8903
  static examples = ["<%= config.bin %> deploy -m 'add phone_number to human-user'", "<%= config.bin %> deploy --dry-run"];
9020
- static flags = { message: Flags.string({
9021
- char: "m",
9022
- description: "A short summary of what this release changes."
9023
- }) };
8904
+ static flags = { message: messageFlag("A short summary of what this release changes. Defaults to the pull request's title or the current commit's subject.") };
9024
8905
  async run() {
9025
8906
  const { flags } = await this.parse(Deploy);
9026
8907
  await this.toMeta(flags);
9027
8908
  const { cwd, env, dryRun, isTTY, nonInteractive } = this.meta;
9028
8909
  const json = this.jsonEnabled();
9029
8910
  const color = !json && isTTY && flags.color !== false && !process.env.NO_COLOR;
9030
- const message = flags.message?.trim() ? flags.message.trim() : void 0;
9031
- if (nonInteractive && !dryRun && message === void 0) throw new ZitadelError("E_VALIDATION", "A deploy needs a message in non-interactive mode", { hint: "Pass -m/--message with a short summary of what this release changes." });
8911
+ const message = dryRun ? void 0 : await deployMessage(flags.message, cwd, env);
8912
+ if (nonInteractive && !dryRun && message === void 0) throw new ZitadelError("E_VALIDATION", "A deploy needs a message in non-interactive mode", { hint: "Pass -m/--message with a short summary of what this release changes, or deploy from a clean git commit." });
9032
8913
  const { client, projectId } = await this.connect();
9033
8914
  let landing = [];
9034
8915
  if (!dryRun) {
9035
- const origins = (await client.getProject(projectId)).origins ?? [];
8916
+ const origins = (await client.getProject(projectId)).origins;
9036
8917
  landing = origins.filter((entry) => entry.kind === "primary").map((entry) => entry.pattern);
9037
- if (landing.length === 0) throw noPrimaryOriginError({
9038
- origins,
9039
- issuer: this.environment.values.ZITADEL_ISSUER?.value,
9040
- setupProject: await setupProjectId(cwd) === projectId,
9041
- selector: this.environmentSelector(),
9042
- cliVersion: this.meta.cliVersion
9043
- });
8918
+ if (landing.length === 0) {
8919
+ const setupProject = await setupProjectId(cwd);
8920
+ throw noPrimaryOriginError({
8921
+ origins,
8922
+ issuer: this.environment.values.ZITADEL_ISSUER?.value,
8923
+ setupProject: setupProject === projectId,
8924
+ selector: this.environmentSelector(),
8925
+ cliVersion: this.meta.cliVersion
8926
+ });
8927
+ }
9044
8928
  }
9045
8929
  const { bundle, resourceCount, brandingSources, idpFiles, policyFiles } = await buildBundle(cwd);
9046
8930
  this.recordTelemetry({ resources: resourceCount });
@@ -9076,7 +8960,10 @@ var Deploy = class Deploy extends EnvironmentCommand {
9076
8960
  gitSha: await headSha(cwd),
9077
8961
  gitDirty
9078
8962
  });
9079
- const observed = (await listLiveDeployments(client, projectId)).find((deployment) => deployment.targets.some((entry) => landing.includes(entry.origin)))?.id;
8963
+ const observed = (await client.listDeployments.all({
8964
+ project_id: projectId,
8965
+ live: true
8966
+ })).find((deployment) => deployment.targets.some((entry) => landing.includes(entry.origin)))?.id;
9080
8967
  const targets = ["primary"];
9081
8968
  let deployment;
9082
8969
  try {
@@ -9095,7 +8982,10 @@ var Deploy = class Deploy extends EnvironmentCommand {
9095
8982
  content_hash: release.content_hash
9096
8983
  }
9097
8984
  });
9098
- const current = (await listLiveDeployments(client, projectId)).find(servesPrimary);
8985
+ const current = (await client.listDeployments.all({
8986
+ project_id: projectId,
8987
+ live: true
8988
+ })).find(servesPrimary);
9099
8989
  throw new ZitadelError("E_CONFLICT", "Another deploy changed the targets since this one was prepared", {
9100
8990
  hint: `Re-run \`zitadel ${DEPLOY_ARGS}${this.environmentSelector()}\`; it will build and deploy against the current state.`,
9101
8991
  details: current === void 0 ? void 0 : { current_deployment: current },
@@ -9112,9 +9002,9 @@ var Deploy = class Deploy extends EnvironmentCommand {
9112
9002
  deployment_id: deployment.id,
9113
9003
  resources: resourceCount,
9114
9004
  origins: servedOrigins,
9115
- ...reported,
9116
- ...nextCommandsData([this.suggest("deployment list")])
9005
+ ...reported
9117
9006
  },
9007
+ next: { commands: [this.suggest("deployment list")] },
9118
9008
  pretty: [
9119
9009
  `Deployed ${release.content_hash}`,
9120
9010
  ` ${resourceCount} resource${resourceCount === 1 ? "" : "s"} to ${servedOrigins.length} origin${servedOrigins.length === 1 ? "" : "s"}`,
@@ -9222,9 +9112,9 @@ var DeploymentCreate = class DeploymentCreate extends EnvironmentCommand {
9222
9112
  data: {
9223
9113
  deployment_id: deployment.id,
9224
9114
  release_id: deployment.release_id,
9225
- origins,
9226
- ...nextCommandsData([this.suggest("deployment list")])
9115
+ origins
9227
9116
  },
9117
+ next: { commands: [this.suggest("deployment list")] },
9228
9118
  pretty: [`Deployed ${deployment.release_id} to ${origins.length} origin${origins.length === 1 ? "" : "s"}`, ...origins.map((origin) => ` - ${origin}`)].join("\n")
9229
9119
  });
9230
9120
  }
@@ -9233,7 +9123,8 @@ var DeploymentCreate = class DeploymentCreate extends EnvironmentCommand {
9233
9123
  //#region src/commands/deployment/get.ts
9234
9124
  /**
9235
9125
  * `zitadel deployment get <deployment_id>` — read one deployment record: the
9236
- * release it made live, the origins it went to, and when.
9126
+ * release it made live, the origins it went to, when, and the variables it
9127
+ * froze.
9237
9128
  */
9238
9129
  var DeploymentGet = class DeploymentGet extends EnvironmentCommand {
9239
9130
  static description = "Read one deployment by id.";
@@ -9247,7 +9138,8 @@ var DeploymentGet = class DeploymentGet extends EnvironmentCommand {
9247
9138
  const { args, flags } = await this.parse(DeploymentGet);
9248
9139
  await this.toMeta(flags);
9249
9140
  const { client, projectId } = await this.connect();
9250
- const deployment = await client.getDeploymentById(args.deployment_id, { project_id: projectId });
9141
+ const scope = { project_id: projectId };
9142
+ const [deployment, variables] = await Promise.all([client.getDeploymentById(args.deployment_id, scope), this.jsonEnabled() ? void 0 : client.getDeploymentVariables(args.deployment_id, scope)]);
9251
9143
  const origins = deployment.targets.map((target) => target.origin);
9252
9144
  return this.emit({
9253
9145
  status: "ok",
@@ -9257,12 +9149,62 @@ var DeploymentGet = class DeploymentGet extends EnvironmentCommand {
9257
9149
  ` release ${deployment.release_id}`,
9258
9150
  ` reason ${deployment.metadata.reason ?? "deploy"}`,
9259
9151
  ` deployed ${deployment.deployed_at}`,
9260
- ` origins ${origins.join(", ")}`
9152
+ ` origins ${origins.join(", ")}`,
9153
+ ...variables === void 0 ? [] : ["", frozenVariables(toVariableRows(variables))]
9261
9154
  ].join("\n")
9262
9155
  });
9263
9156
  }
9264
9157
  };
9265
9158
  //#endregion
9159
+ //#region src/lib/oclif/envelope.ts
9160
+ /**
9161
+ * Builders for the `data` of a command's JSON envelope.
9162
+ *
9163
+ * Every command emits `data`, and an agent reads all of them, so the shapes
9164
+ * must not drift. These builders are the one place each shape is defined; the
9165
+ * spec harness (`assertEnvelope`) enforces the same shapes on every `--json`
9166
+ * invocation, so a command that hand-builds a divergent shape fails in its own
9167
+ * spec. Use the builder rather than an object literal.
9168
+ */
9169
+ /**
9170
+ * The canonical list/collection envelope: `{ items, count, next_page_token }`.
9171
+ * Every command that returns a collection emits this, so it reads uniformly
9172
+ * across all of them. A single-page endpoint passes `nextPageToken: null` (the
9173
+ * default); a paginated one passes the server cursor (or `null` once drained).
9174
+ */
9175
+ function listData(items, opts = {}) {
9176
+ return {
9177
+ items,
9178
+ count: items.length,
9179
+ next_page_token: opts.nextPageToken ?? null
9180
+ };
9181
+ }
9182
+ /**
9183
+ * The canonical delete/remove confirmation: the resource's identifier plus
9184
+ * `deleted: true`. The verb is always `deleted` — never `removed` — so an agent
9185
+ * reads "did the delete succeed?" the same way for every resource, whether it is
9186
+ * addressed by `id` or by a natural key (`url`, `pattern`, `name`). A lifecycle
9187
+ * verb (revoke, deactivate) uses {@link mutatedData} instead.
9188
+ */
9189
+ function deletedData(identifier) {
9190
+ return {
9191
+ ...identifier,
9192
+ deleted: true
9193
+ };
9194
+ }
9195
+ /**
9196
+ * A mutation confirmation whose verb is the lifecycle action, not `deleted`:
9197
+ * the identifier plus `<verb>: true` (e.g. `revoked`, `deactivated`). Keeps the
9198
+ * `{ identifier, <past-tense>: true }` shape of {@link deletedData} while naming
9199
+ * the specific action.
9200
+ */
9201
+ function mutatedData(identifier, verb) {
9202
+ return {
9203
+ ...identifier,
9204
+ [verb]: true
9205
+ };
9206
+ }
9207
+ //#endregion
9266
9208
  //#region src/commands/deployment/list.ts
9267
9209
  /** The columns `list` renders, in order. */
9268
9210
  const COLUMNS$6 = [
@@ -9310,24 +9252,22 @@ var DeploymentList = class DeploymentList extends EnvironmentCommand {
9310
9252
  const { flags } = await this.parse(DeploymentList);
9311
9253
  await this.toMeta(flags);
9312
9254
  const { client, projectId } = await this.connect();
9313
- const request = (token) => client.listDeployments({
9255
+ const params = {
9314
9256
  project_id: projectId,
9315
- ...flags.origin === void 0 ? {} : { origin: flags.origin },
9316
- ...flags.live ? { live: true } : {},
9317
- ...flags.limit === void 0 ? {} : { limit: flags.limit },
9318
- ...token === void 0 ? {} : { page_token: token }
9319
- });
9320
- const { items, next } = await collectPages(request, {
9321
- items: "deployments",
9257
+ origin: flags.origin,
9258
+ live: flags.live,
9259
+ limit: flags.limit,
9260
+ page_token: flags["page-token"]
9261
+ };
9262
+ const { items: deployments, next } = await collectPages(client.listDeployments.pages(params), {
9263
+ items: (page) => page.deployments,
9322
9264
  all: flags.all === true,
9323
- token: flags["page-token"],
9324
- nextPageToken: "next_page_token"
9265
+ next: (page) => page.next_page_token
9325
9266
  });
9326
- const deployments = items;
9327
9267
  const rows = deployments.map((deployment) => ({
9328
9268
  id: deployment.id,
9329
9269
  release_id: deployment.release_id,
9330
- reason: deployment.metadata.reason ?? "deploy",
9270
+ reason: deployment.metadata.reason,
9331
9271
  origins: deployment.targets.map((target) => target.origin).join(", "),
9332
9272
  deployed_at: deployment.deployed_at
9333
9273
  }));
@@ -9342,10 +9282,9 @@ var DeploymentList = class DeploymentList extends EnvironmentCommand {
9342
9282
  const plain = flags.plain || !process.stdout.isTTY;
9343
9283
  return this.emit({
9344
9284
  status: "ok",
9345
- data: listData(deployments, {
9346
- nextPageToken: next,
9347
- nextCommands: nextCommand === void 0 ? void 0 : [nextCommand]
9348
- }),
9285
+ data: listData(deployments, { nextPageToken: next }),
9286
+ ...nextCommand === void 0 ? {} : { next: { commands: [nextCommand] } },
9287
+ machineReadable: plain,
9349
9288
  pretty: renderList(COLUMNS$6, rows, {
9350
9289
  plain,
9351
9290
  singular: "deployment",
@@ -9380,31 +9319,26 @@ var DeploymentRollback = class DeploymentRollback extends Promptable(Environment
9380
9319
  required: false,
9381
9320
  description: "The deployment to re-apply. Omit to undo the last deploy."
9382
9321
  }) };
9383
- static flags = { force: Flags.boolean({
9384
- char: "f",
9385
- description: "Roll back without the confirmation prompt. Required when non-interactive."
9386
- }) };
9322
+ static flags = {
9323
+ force: Flags.boolean({
9324
+ char: "f",
9325
+ description: "Roll back without the confirmation prompt. Required when non-interactive."
9326
+ }),
9327
+ message: messageFlag("A short summary of why this rolls back.")
9328
+ };
9387
9329
  async run() {
9388
9330
  const { args, flags } = await this.parse(DeploymentRollback);
9389
9331
  await this.toMeta(flags);
9390
9332
  const { dryRun } = this.meta;
9391
9333
  const { client, projectId } = await this.connect();
9392
9334
  let targetId = args.deployment_id;
9393
- let bareGuard;
9335
+ let expectedDeploymentId;
9394
9336
  if (targetId === void 0) {
9395
- const { items } = await collectPages((token) => client.listDeployments({
9396
- project_id: projectId,
9397
- ...token === void 0 ? {} : { page_token: token }
9398
- }), {
9399
- items: "deployments",
9400
- all: true,
9401
- nextPageToken: "next_page_token"
9402
- });
9403
- const [latest, prior] = items.filter(servesPrimary);
9337
+ const [latest, prior] = (await client.listDeployments.all({ project_id: projectId })).filter(servesPrimary);
9404
9338
  if (!latest) throw new ZitadelError("E_VALIDATION", "There is no deployment to roll back", { hint: `Nothing has been deployed yet; check \`zitadel deployment list${this.environmentSelector()}\`.` });
9405
9339
  if (!prior) throw new ZitadelError("E_VALIDATION", "There is no earlier deployment to roll back to", { hint: `Name a deployment to re-apply, or check \`zitadel deployment list${this.environmentSelector()}\`.` });
9406
9340
  targetId = prior.id;
9407
- bareGuard = latest.id;
9341
+ expectedDeploymentId = latest.id;
9408
9342
  }
9409
9343
  if (dryRun) return this.emit({
9410
9344
  status: "ok",
@@ -9414,7 +9348,10 @@ var DeploymentRollback = class DeploymentRollback extends Promptable(Environment
9414
9348
  },
9415
9349
  pretty: `Would re-apply ${targetId}. Nothing was rolled back (dry run).`
9416
9350
  });
9417
- const expectedDeploymentId = bareGuard ?? await liveGuard(client, projectId);
9351
+ if (expectedDeploymentId === void 0) expectedDeploymentId = (await client.listDeployments.all({
9352
+ project_id: projectId,
9353
+ live: true
9354
+ })).find(servesPrimary)?.id;
9418
9355
  if (!await this.input({
9419
9356
  kind: "confirm",
9420
9357
  name: `re-apply ${targetId}`,
@@ -9424,7 +9361,10 @@ var DeploymentRollback = class DeploymentRollback extends Promptable(Environment
9424
9361
  status: "skipped",
9425
9362
  reason: "rollback-cancelled"
9426
9363
  });
9427
- const deployment = await rollbackDeployment(client, projectId, targetId, { ...expectedDeploymentId === void 0 ? {} : { expectedDeploymentId } });
9364
+ const deployment = await rollbackDeployment(client, projectId, targetId, {
9365
+ ...flags.message === void 0 ? {} : { message: flags.message },
9366
+ ...expectedDeploymentId === void 0 ? {} : { expectedDeploymentId }
9367
+ });
9428
9368
  let pointers;
9429
9369
  try {
9430
9370
  pointers = (await client.getReleaseById(deployment.release_id, { project_id: projectId })).pointers;
@@ -9454,10 +9394,6 @@ var DeploymentRollback = class DeploymentRollback extends Promptable(Environment
9454
9394
  });
9455
9395
  }
9456
9396
  };
9457
- /** The project's newest primary deployment, which a rollback is guarded by. */
9458
- async function liveGuard(client, projectId) {
9459
- return (await listLiveDeployments(client, projectId)).find(servesPrimary)?.id;
9460
- }
9461
9397
  //#endregion
9462
9398
  //#region src/lib/orca/detectors/package-json.ts
9463
9399
  /**
@@ -10590,7 +10526,7 @@ function agentsGuidanceSection(ctx) {
10590
10526
  const jsxTypesNote = ctx.framework.id === "next" ? " React JSX types for the `<zitadel-*>` elements ship with the SDK — `custom-elements.d.ts` references `@zitadel/sdk-next/jsx`." : "";
10591
10527
  const presentationParagraph = ctx.posture === "widget" ? "Presentation, by contrast, is edited in the generated pages: this app pre-dates setup, so they embed the sign-in widgets as `variant=\"widget\"` cards (with `theme=\"auto\"`) inside your existing layout; switch to `variant=\"page\"` for the widget's own full-page chrome, and set `theme` (`light` | `dark` | `auto`) to pick the color scheme — `auto` follows the OS `prefers-color-scheme`, not this app's own theme, so pin `light` or `dark` when the app fixes its scheme." : "Presentation, by contrast, is edited in the generated pages: they pin the sign-in widgets to `variant=\"page\"` (full-page chrome); switch to `variant=\"widget\"` to embed a card inside your own layout, and set `theme` (`light` | `dark` | `auto`) to pick the color scheme — `auto` follows the OS `prefers-color-scheme`, not this app's own theme, so pin `light` or `dark` when the app fixes its scheme.";
10592
10528
  const themingParagraph = "Theming the widgets from your app: two hooks in your own stylesheet reach into their shadow DOM — the stable `--zl-*` design tokens, set on the element, and the parts `<zitadel-login>` forwards from its inner components as `zitadel-login::part(<atom>-<part>)` (the `card` part of `zl-card` is `zitadel-login::part(card-card)`). Both are listed under \"Style the login from your app\" in https://preview.zitadel.com/docs/guides/customize-design#style-the-login-from-your-app; any other `--zl-*` variable is internal. When your page already carries the heading, set the `suppress-header` attribute on `<zitadel-login>` / `<zitadel-session>` (SDK wrappers: `suppressHeader`) to visually hide the widget's own heading block — it stays in the accessibility tree. The login moves between sign-in and sign-up by itself (its own switch link, and an email it does or does not know), so a heading of your own must follow its `zitadel-flow-step` event, which the SDK wrappers forward: `step.purpose` is `login` while it signs in and `register` while it signs up. When it switches without being asked, the login tells the visitor why itself. To only reword the widget's heading, pass `locales` instead. Page layout around the widget (split screens, hero panes, marketing copy) is your app's code, not a Zitadel template.";
10593
- const sessionStateParagraph = ctx.framework.id === "next" ? "Your app's own chrome (header navigation, account menus) reads session state with `getSession({ projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID })` from `@zitadel/sdk-next/session` — a no-store client read of the same-origin `/__nextgen/sessions/me?project_id=…` that the session card itself uses. It returns identity only for 200 with a non-empty `user_id` in this app's project, and signed-out for the backend's canonical `401/auth.unauthorized`, `403/sess.project_mismatch` (a session of another project on the same host, such as the console's on localhost) or `404/sess.not_found`; any other response is unknown/error, never signed-out. In Server Components, `auth()` from `@zitadel/sdk-next/server` works too, on every page: the request boundary's `matcher` covers every route, which also makes `protectedRoutes` the one list of routes that need a sign-in (a protected route that a narrower `matcher` leaves out stays public). Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load without extra wiring." : ctx.framework.id === "nuxt" ? "Your app's own chrome (header navigation, account menus) reads session state with the auto-imported `useAuth()` composable — seeded from the server on every render by the scaffolded auth plugin. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so the state is fresh on every page load." : "Your app's own chrome (header navigation, account menus) can read session state from the same-origin `/__nextgen/sessions/me?project_id=<project id>`, naming the project id the app passes to `configureZitadel()` — the same server-answered read the session card performs. Fetch with `credentials: \"include\"`, `cache: \"no-store\"`, and `Accept: application/json`; treat 200 as signed-in only with a non-empty `user_id` and that `project_id`, and signed-out only for the canonical error envelope `401/auth.unauthorized`, `403/sess.project_mismatch` (a session of another project on the same host, such as the console's on localhost) or `404/sess.not_found`. Any other response is unknown/error, never signed-out. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load.";
10529
+ const sessionStateParagraph = ctx.framework.id === "next" ? "Your app's own chrome (header navigation, account menus) reads session state with `getSession({ projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID })` from `@zitadel/sdk-next/session` — a no-store client read of the same-origin `/__nextgen/api/v1/sessions/me?project_id=…` that the session card itself uses. It returns identity only for 200 with a non-empty `user_id` in this app's project, and signed-out for the backend's canonical `401/auth.unauthorized`, `403/sess.project_mismatch` (a session of another project on the same host, such as the console's on localhost) or `404/sess.not_found`; any other response is unknown/error, never signed-out. In Server Components, `auth()` from `@zitadel/sdk-next/server` works too, on every page: the request boundary's `matcher` covers every route, which also makes `protectedRoutes` the one list of routes that need a sign-in (a protected route that a narrower `matcher` leaves out stays public). Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load without extra wiring." : ctx.framework.id === "nuxt" ? "Your app's own chrome (header navigation, account menus) reads session state with the auto-imported `useAuth()` composable — seeded from the server on every render by the scaffolded auth plugin. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so the state is fresh on every page load." : "Your app's own chrome (header navigation, account menus) can read session state from the same-origin `/__nextgen/api/v1/sessions/me?project_id=<project id>`, naming the project id the app passes to `configureZitadel()` — the same server-answered read the session card performs. Fetch with `credentials: \"include\"`, `cache: \"no-store\"`, and `Accept: application/json`; treat 200 as signed-in only with a non-empty `user_id` and that `project_id`, and signed-out only for the canonical error envelope `401/auth.unauthorized`, `403/sess.project_mismatch` (a session of another project on the same host, such as the console's on localhost) or `404/sess.not_found`. Any other response is unknown/error, never signed-out. Sign-in and sign-out navigate (`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page load.";
10594
10530
  return `## Authentication (Zitadel)
10595
10531
 
10596
10532
  This app's login is managed by Zitadel. Local config is the source of truth for what the login collects and how sign-in works (step 3 below). The app files setup generated are yours to change, from the pages to the request boundary that protects routes in a server-rendered app: \`doctor\` reports a file you changed as \`edited\`, or as \`adopted\` once you delete its \`zitadel-cli: managed-file\` comment, and \`doctor --fix\` restores only a missing file, never one you changed.
@@ -10866,6 +10802,11 @@ var AbstractRulePatcher = class {
10866
10802
  path,
10867
10803
  wiring: op.wiring,
10868
10804
  state: verdict
10805
+ } : "outdated" in verdict ? {
10806
+ path,
10807
+ wiring: op.wiring,
10808
+ state: "outdated",
10809
+ reason: verdict.outdated
10869
10810
  } : {
10870
10811
  path,
10871
10812
  wiring: op.wiring,
@@ -11571,8 +11512,27 @@ function angularRoutesEdit() {
11571
11512
  const PROXY_PATH = "/__nextgen";
11572
11513
  //#endregion
11573
11514
  //#region src/lib/orca/patchers/rule/proxy-credential-policy.ts
11574
- /** Marker emitted after the exchange-only credential policy was introduced. */
11575
- const PROXY_CREDENTIAL_POLICY_MARKER = "zitadel:proxy:v2";
11515
+ /**
11516
+ * Marker emitted with the exchange-only credential policy. Its v3 also attaches
11517
+ * the secret to the exchange under the API prefix (ADR 077).
11518
+ */
11519
+ const PROXY_CREDENTIAL_POLICY_MARKER = "zitadel:proxy:v3";
11520
+ /** The marker of the exchange-only policy as it was before it knew the API prefix. */
11521
+ const PROXY_CREDENTIAL_POLICY_V2_MARKER = "zitadel:proxy:v2";
11522
+ /** Whether `source` carries the credential-policy marker of either version. */
11523
+ function hasProxyCredentialPolicyMarker(source) {
11524
+ return source.includes("zitadel:proxy:v3") || source.includes("zitadel:proxy:v2");
11525
+ }
11526
+ /** The session exchange under the API prefix (ADR 077), as a browser calls it through the proxy. */
11527
+ const PREFIXED_EXCHANGE_PATH = `${API_PREFIX}/sessions/exchange`;
11528
+ /**
11529
+ * The generated proxies' path check: the exchange with and without the API
11530
+ * prefix, since the server answers both until 1.0.0 and an app's packages may
11531
+ * call either.
11532
+ */
11533
+ const EXCHANGE_PATH_CHECK = `(pathname === "/sessions/exchange" || pathname === "${PREFIXED_EXCHANGE_PATH}")`;
11534
+ /** The path check of a v2 proxy, which knew only the unprefixed exchange. */
11535
+ const V2_EXCHANGE_PATH_CHECK = "pathname === \"/sessions/exchange\"";
11576
11536
  /**
11577
11537
  * Rejects a browser-reachable proxy that still appears to attach the project
11578
11538
  * secret without the complete exchange-only guard. Known generated legacy
@@ -11582,7 +11542,47 @@ const PROXY_CREDENTIAL_POLICY_MARKER = "zitadel:proxy:v2";
11582
11542
  */
11583
11543
  function assertNoUnreviewedProjectSecretProxy(source, label) {
11584
11544
  if (!source.includes("/__nextgen") || !source.includes("ZITADEL_PROJECT_SECRET") || !/authorization/i.test(source) || hasExchangeOnlyGuard(source)) return;
11585
- throw new ZitadelError("E_VALIDATION", `${label} may forward ZITADEL_PROJECT_SECRET outside POST /sessions/exchange`, { hint: "Review the proxy manually: restrict the bearer to the exact POST /sessions/exchange pathname and preserve caller-provided authorization." });
11545
+ throw new ZitadelError("E_VALIDATION", `${label} may forward ZITADEL_PROJECT_SECRET outside POST /sessions/exchange`, { hint: `Review the proxy manually: restrict the bearer to the exact POST /sessions/exchange or ${PREFIXED_EXCHANGE_PATH} pathname and preserve caller-provided authorization.` });
11546
+ }
11547
+ /** `code` without whitespace, with double quotes, so a formatter's layout compares equal. */
11548
+ function normalized(code) {
11549
+ return code.replace(/\s+/g, "").replace(/'/g, "\"");
11550
+ }
11551
+ /** The terms of the conditions a guard may be built from, normalized. */
11552
+ const POST_TERM = "proxyReq.method===\"POST\"";
11553
+ const NO_OVERWRITE_TERM = "!proxyReq.getHeader(\"authorization\")";
11554
+ const PATH_TERMS = /* @__PURE__ */ new Set([
11555
+ normalized(V2_EXCHANGE_PATH_CHECK),
11556
+ normalized(EXCHANGE_PATH_CHECK),
11557
+ normalized(`(pathname === "${PREFIXED_EXCHANGE_PATH}" || pathname === "/sessions/exchange")`)
11558
+ ]);
11559
+ /** `condition`'s terms joined by `&&` outside any parentheses. */
11560
+ function andTerms(condition) {
11561
+ const terms = [];
11562
+ let depth = 0;
11563
+ let start = 0;
11564
+ for (let index = 0; index < condition.length; index++) {
11565
+ const char = condition[index];
11566
+ if (char === "(") depth++;
11567
+ else if (char === ")") depth--;
11568
+ else if (depth === 0 && condition.startsWith("&&", index)) {
11569
+ terms.push(condition.slice(start, index));
11570
+ start = index + 2;
11571
+ index++;
11572
+ }
11573
+ }
11574
+ terms.push(condition.slice(start));
11575
+ return terms;
11576
+ }
11577
+ /**
11578
+ * Whether an `if` condition holds the bearer to the exchange: exactly the POST
11579
+ * check, the path check (the exchange, or both its spellings joined by `||`)
11580
+ * and the no-overwrite check, in any order. Anything more could widen it, as
11581
+ * `… || true` would, so it does not qualify.
11582
+ */
11583
+ function isExchangeOnlyCondition(condition) {
11584
+ const terms = andTerms(normalized(condition));
11585
+ return terms.length === 3 && terms.includes(POST_TERM) && terms.includes(NO_OVERWRITE_TERM) && terms.some((term) => PATH_TERMS.has(term));
11586
11586
  }
11587
11587
  function hasExchangeOnlyGuard(source) {
11588
11588
  const setterOffsets = [...source.matchAll(/proxyReq\s*\.\s*setHeader\s*\(\s*["']authorization["']\s*,/gi)].map((match) => match.index);
@@ -11590,7 +11590,7 @@ function hasExchangeOnlyGuard(source) {
11590
11590
  const safeBodyRanges = [];
11591
11591
  for (const match of source.matchAll(/if\s*\(([\s\S]*?)\)\s*\{([\s\S]*?)\}/g)) {
11592
11592
  const [whole = "", condition = "", body = ""] = match;
11593
- if (!/proxyReq\s*\.\s*method\s*===\s*["']POST["']/i.test(condition) || !/pathname\s*===\s*["']\/sessions\/exchange["']/i.test(condition) || !/!\s*proxyReq\s*\.\s*getHeader\s*\(\s*["']authorization["']\s*\)/i.test(condition) || !/proxyReq\s*\.\s*setHeader\s*\(\s*["']authorization["']\s*,/i.test(body)) continue;
11593
+ if (!isExchangeOnlyCondition(condition) || !/proxyReq\s*\.\s*setHeader\s*\(\s*["']authorization["']\s*,/i.test(body)) continue;
11594
11594
  const bodyOffset = whole.lastIndexOf(body);
11595
11595
  const start = match.index + bodyOffset;
11596
11596
  safeBodyRanges.push({
@@ -11600,21 +11600,61 @@ function hasExchangeOnlyGuard(source) {
11600
11600
  }
11601
11601
  return setterOffsets.every((offset) => safeBodyRanges.some(({ start, end }) => offset >= start && offset < end));
11602
11602
  }
11603
+ //#endregion
11604
+ //#region src/lib/orca/patchers/rule/session-cookie.ts
11605
+ /**
11606
+ * The session-cookie hooks of the dev proxies the CLI writes for the SPA
11607
+ * frameworks (ADR 073, ADR 083). The server sets one session cookie,
11608
+ * `__Host-zitadel_session`, or `zitadel_session` over plain http to a loopback
11609
+ * host, and reads it as `__Host-zitadel_session`; a browser shares a host's
11610
+ * cookies across every port, so under that name alone an SPA on `localhost`
11611
+ * and the Console of a local server would keep their sessions in one slot and
11612
+ * sign each other out. The hooks keep the app's session in
11613
+ * `zitadel_session_<project id>` on a dev server reached over plain http to a
11614
+ * loopback host, and in `__Host-zitadel_session_<project id>` on any other,
11615
+ * the names the server SDKs use (`sessionCookieName` in `@zitadel/sdk-core`):
11616
+ *
11617
+ * - on the way up, the app's cookie goes once, under the server's
11618
+ * `__Host-zitadel_session`, and every other session cookie stays back, those
11619
+ * from before ADR 083 included;
11620
+ * - on the way back, the server's `Set-Cookie`, under either name, becomes the
11621
+ * app's: its value and expiry with `Path=/`, `HttpOnly`, `SameSite=Lax`, and
11622
+ * `Secure` unless the dev server is loopback http. A `Set-Cookie` of any
11623
+ * other session cookie is dropped.
11624
+ *
11625
+ * The dev server's scheme is the first `X-Forwarded-Proto` (the entry's `xfwd`
11626
+ * sets it), else its own connection's, and its host the first
11627
+ * `X-Forwarded-Host`, else `Host`: the rule of `secureFor` in sdk-core.
11628
+ *
11629
+ * Without a project id the cookies pass through unchanged. The code is plain
11630
+ * JavaScript, so it runs in a `vite.config.*` and in Angular's CommonJS
11631
+ * `proxy.conf.cjs` alike, and in a `vite.config.ts` it type-checks under
11632
+ * `strict`: every parameter is one Vite's `configure(proxy)` typing reaches.
11633
+ */
11634
+ /** Marks a generated dev proxy that carries the session-cookie hooks. */
11635
+ const SESSION_COOKIE_MARKER = "zitadel:session-cookie:v2";
11636
+ /**
11637
+ * Marks the hooks the CLI wrote before ADR 083, which name the
11638
+ * `__nextgen_session` cookies. The doctor fails on them, and `doctor --fix`
11639
+ * replaces them where they are as the CLI wrote them.
11640
+ */
11641
+ const SESSION_COOKIE_MARKER_V1 = "zitadel:session-cookie:v1";
11603
11642
  /** Says what the hooks do, above them in the generated proxy. */
11604
11643
  const EXPLANATION = [
11605
- `/* zitadel:session-cookie:v1 */`,
11644
+ `/* ${SESSION_COOKIE_MARKER} */`,
11606
11645
  "// The browser keeps this app's session in a cookie named after its project,",
11607
- "// the server in __nextgen_session (ADR 073). Without a project id, cookies",
11608
- "// pass through."
11646
+ "// zitadel_session_<project id>, prefixed __Host- unless this dev server is",
11647
+ "// plain http on a loopback host; the server takes it as __Host-zitadel_session",
11648
+ "// (ADR 083). Without a project id, cookies pass through."
11609
11649
  ];
11610
11650
  /**
11611
- * `appCookie`, the app's cookie name for `projectId`, or `undefined` without
11612
- * one. It percent-encodes every byte outside `[A-Za-z0-9_-]`, as
11613
- * `sessionCookieName` in sdk-core does.
11651
+ * `appCookieSuffix`, what the app's cookie name adds for `projectId`, or
11652
+ * `undefined` without one. It percent-encodes every byte outside
11653
+ * `[A-Za-z0-9_-]`, as `sessionCookieName` in sdk-core does.
11614
11654
  */
11615
- const APP_COOKIE = [
11616
- "const appCookie = projectId",
11617
- " ? \"__nextgen_session_\" +",
11655
+ const APP_COOKIE_SUFFIX = [
11656
+ "const appCookieSuffix = projectId",
11657
+ " ? \"_\" +",
11618
11658
  " projectId.replace(/[^\\w-]/gu, (char) =>",
11619
11659
  " Array.from(new TextEncoder().encode(char), (byte) =>",
11620
11660
  " \"%\" + byte.toString(16).toUpperCase().padStart(2, \"0\"),",
@@ -11622,11 +11662,28 @@ const APP_COOKIE = [
11622
11662
  " )",
11623
11663
  " : undefined;"
11624
11664
  ];
11665
+ /**
11666
+ * `secure` and `appCookie` for the browser's request `req`: the app's cookie
11667
+ * carries the prefix and `Secure` unless the dev server was reached over plain
11668
+ * http to a loopback host.
11669
+ */
11670
+ const APP_COOKIE = [
11671
+ "const proto = String(req.headers[\"x-forwarded-proto\"] ?? \"\").split(\",\")[0]?.trim() ||",
11672
+ " (\"encrypted\" in req.socket && req.socket.encrypted ? \"https\" : \"http\");",
11673
+ "const host = String(req.headers[\"x-forwarded-host\"] ?? \"\").split(\",\")[0]?.trim() || (req.headers.host ?? \"\");",
11674
+ "const secure =",
11675
+ " proto.toLowerCase() !== \"http\" ||",
11676
+ " !/^(?:localhost|127(?:\\.\\d{1,3}){3}|\\[::1\\])(?::\\d+)?$/i.test(host);",
11677
+ "const appCookie = (secure ? \"__Host-zitadel_session\" : \"zitadel_session\") + appCookieSuffix;"
11678
+ ];
11679
+ /** Matches a cookie of any session cookie family, the server's or an app's, on either transport or from before ADR 083. */
11680
+ const ANY_SESSION_COOKIE = "/^(?:__Host-)?(?:zitadel_session|__nextgen_session)(?:_[^=]*)?=/";
11625
11681
  /** The request hook's body, on `proxyReq`: only the app's cookie goes up, under the server's name. */
11626
11682
  const FORWARD_BODY = [
11627
- "if (!appCookie) {",
11683
+ "if (!appCookieSuffix) {",
11628
11684
  " return;",
11629
11685
  "}",
11686
+ ...APP_COOKIE,
11630
11687
  "const cookies = String(proxyReq.getHeader(\"cookie\") ?? \"\")",
11631
11688
  " .split(\";\")",
11632
11689
  " .map((cookie) => cookie.trim())",
@@ -11634,9 +11691,9 @@ const FORWARD_BODY = [
11634
11691
  "const own = cookies.find(",
11635
11692
  " (cookie) => cookie.startsWith(appCookie + \"=\") && cookie.length > appCookie.length + 1,",
11636
11693
  ");",
11637
- "const forwarded = cookies.filter((cookie) => !/^__nextgen_session(?:_[^=]*)?=/.test(cookie));",
11694
+ `const forwarded = cookies.filter((cookie) => !${ANY_SESSION_COOKIE}.test(cookie));`,
11638
11695
  "if (own) {",
11639
- " forwarded.push(\"__nextgen_session\" + own.slice(appCookie.length));",
11696
+ " forwarded.push(\"__Host-zitadel_session\" + own.slice(appCookie.length));",
11640
11697
  "}",
11641
11698
  "if (forwarded.length > 0) {",
11642
11699
  " proxyReq.setHeader(\"cookie\", forwarded.join(\"; \"));",
@@ -11644,39 +11701,64 @@ const FORWARD_BODY = [
11644
11701
  " proxyReq.removeHeader(\"cookie\");",
11645
11702
  "}"
11646
11703
  ];
11647
- /** The response hook's body, on `proxyRes`: the server's `Set-Cookie` comes back under the app's name. */
11704
+ /**
11705
+ * The response hook's body, on `proxyRes`: the server's `Set-Cookie` comes back
11706
+ * as the app's, and one of any other session cookie not at all.
11707
+ */
11648
11708
  const RENAME_BODY = [
11649
11709
  "const setCookies = proxyRes.headers[\"set-cookie\"];",
11650
- "if (appCookie && setCookies) {",
11651
- " proxyRes.headers[\"set-cookie\"] = setCookies.map((setCookie) =>",
11652
- " setCookie.replace(/^__nextgen_session=/, appCookie + \"=\"),",
11653
- " );",
11654
- "}"
11710
+ "if (!appCookieSuffix || !setCookies) {",
11711
+ " return;",
11712
+ "}",
11713
+ ...APP_COOKIE,
11714
+ "proxyRes.headers[\"set-cookie\"] = setCookies.flatMap((setCookie) => {",
11715
+ " if (/^(?:__Host-)?zitadel_session=/.test(setCookie)) {",
11716
+ " const [pair = \"\", ...attributes] = setCookie.split(\";\");",
11717
+ " const expiry = attributes",
11718
+ " .map((attribute) => attribute.trim())",
11719
+ " .filter((attribute) => /^(?:max-age|expires)=/i.test(attribute));",
11720
+ " return [",
11721
+ " [",
11722
+ " appCookie + pair.slice(pair.indexOf(\"=\")),",
11723
+ " \"Path=/\",",
11724
+ " ...expiry,",
11725
+ " \"HttpOnly\",",
11726
+ " \"SameSite=Lax\",",
11727
+ " ...(secure ? [\"Secure\"] : []),",
11728
+ " ].join(\"; \"),",
11729
+ " ];",
11730
+ " }",
11731
+ ` return ${ANY_SESSION_COOKIE}.test(setCookie) ? [] : [setCookie];`,
11732
+ "});"
11655
11733
  ];
11656
11734
  /** `lines`, each indented by two more spaces. */
11657
11735
  function nested(lines) {
11658
11736
  return lines.map((line) => line === "" ? "" : ` ${line}`);
11659
11737
  }
11738
+ /** How the Vite proxy entry reads the project id: from `.env.local` through Vite's `loadEnv`. */
11739
+ const VITE_PROJECT_ID = "const projectId = loadEnv(\"development\", process.cwd(), \"ZITADEL_\").ZITADEL_PROJECT_ID;";
11660
11740
  /**
11661
11741
  * The hooks as statements of a Vite proxy entry's `configure(proxy)` body,
11662
11742
  * after its bearer hook. The project id comes from `.env.local` through Vite's
11663
11743
  * `loadEnv`, as the project secret does. The hooks are the listeners
11664
- * themselves, not functions they call, so Vite's types reach `proxyReq` and
11665
- * `proxyRes` and a strict `vite.config.ts` type-checks.
11744
+ * themselves, not functions they call, so Vite's types reach `proxyReq`,
11745
+ * `proxyRes` and `req`, and a strict `vite.config.ts` type-checks.
11666
11746
  */
11667
11747
  function viteSessionCookieStatements() {
11668
11748
  return [
11669
11749
  ...EXPLANATION,
11670
- "const projectId = loadEnv(\"development\", process.cwd(), \"ZITADEL_\").ZITADEL_PROJECT_ID;",
11671
- ...APP_COOKIE,
11672
- "proxy.on(\"proxyReq\", (proxyReq) => {",
11750
+ VITE_PROJECT_ID,
11751
+ ...APP_COOKIE_SUFFIX,
11752
+ "proxy.on(\"proxyReq\", (proxyReq, req) => {",
11673
11753
  ...nested(FORWARD_BODY),
11674
11754
  "});",
11675
- "proxy.on(\"proxyRes\", (proxyRes) => {",
11755
+ "proxy.on(\"proxyRes\", (proxyRes, req) => {",
11676
11756
  ...nested(RENAME_BODY),
11677
11757
  "});"
11678
11758
  ];
11679
11759
  }
11760
+ /** How Angular's `proxy.conf.cjs` reads the project id: from the environment, else `.env.local`. */
11761
+ const PROXY_CONF_PROJECT_ID = ["const projectId =", " process.env.ZITADEL_PROJECT_ID ?? (typeof env === \"undefined\" ? undefined : env.ZITADEL_PROJECT_ID);"];
11680
11762
  /**
11681
11763
  * The hooks as top-level statements of Angular's `proxy.conf.cjs`, after its
11682
11764
  * bearer hook: the functions its entry attaches as `onProxyReq`/`onProxyRes`
@@ -11688,19 +11770,93 @@ function viteSessionCookieStatements() {
11688
11770
  function proxyConfSessionCookieStatements() {
11689
11771
  return [
11690
11772
  ...EXPLANATION,
11691
- "const projectId =",
11692
- " process.env.ZITADEL_PROJECT_ID ?? (typeof env === \"undefined\" ? undefined : env.ZITADEL_PROJECT_ID);",
11693
- ...APP_COOKIE,
11773
+ ...PROXY_CONF_PROJECT_ID,
11774
+ ...APP_COOKIE_SUFFIX,
11694
11775
  "",
11695
- "function forwardSessionCookie(proxyReq) {",
11776
+ "function forwardSessionCookie(proxyReq, req) {",
11696
11777
  ...nested(FORWARD_BODY),
11697
11778
  "}",
11698
11779
  "",
11699
- "function renameSessionCookie(proxyRes) {",
11780
+ "function renameSessionCookie(proxyRes, req) {",
11700
11781
  ...nested(RENAME_BODY),
11701
11782
  "}"
11702
11783
  ];
11703
11784
  }
11785
+ const EXPLANATION_V1 = [
11786
+ `/* ${SESSION_COOKIE_MARKER_V1} */`,
11787
+ "// The browser keeps this app's session in a cookie named after its project,",
11788
+ "// the server in __nextgen_session (ADR 073). Without a project id, cookies",
11789
+ "// pass through."
11790
+ ];
11791
+ const APP_COOKIE_V1 = [
11792
+ "const appCookie = projectId",
11793
+ " ? \"__nextgen_session_\" +",
11794
+ " projectId.replace(/[^\\w-]/gu, (char) =>",
11795
+ " Array.from(new TextEncoder().encode(char), (byte) =>",
11796
+ " \"%\" + byte.toString(16).toUpperCase().padStart(2, \"0\"),",
11797
+ " ).join(\"\"),",
11798
+ " )",
11799
+ " : undefined;"
11800
+ ];
11801
+ const FORWARD_BODY_V1 = [
11802
+ "if (!appCookie) {",
11803
+ " return;",
11804
+ "}",
11805
+ "const cookies = String(proxyReq.getHeader(\"cookie\") ?? \"\")",
11806
+ " .split(\";\")",
11807
+ " .map((cookie) => cookie.trim())",
11808
+ " .filter((cookie) => cookie !== \"\");",
11809
+ "const own = cookies.find(",
11810
+ " (cookie) => cookie.startsWith(appCookie + \"=\") && cookie.length > appCookie.length + 1,",
11811
+ ");",
11812
+ "const forwarded = cookies.filter((cookie) => !/^__nextgen_session(?:_[^=]*)?=/.test(cookie));",
11813
+ "if (own) {",
11814
+ " forwarded.push(\"__nextgen_session\" + own.slice(appCookie.length));",
11815
+ "}",
11816
+ "if (forwarded.length > 0) {",
11817
+ " proxyReq.setHeader(\"cookie\", forwarded.join(\"; \"));",
11818
+ "} else {",
11819
+ " proxyReq.removeHeader(\"cookie\");",
11820
+ "}"
11821
+ ];
11822
+ const RENAME_BODY_V1 = [
11823
+ "const setCookies = proxyRes.headers[\"set-cookie\"];",
11824
+ "if (appCookie && setCookies) {",
11825
+ " proxyRes.headers[\"set-cookie\"] = setCookies.map((setCookie) =>",
11826
+ " setCookie.replace(/^__nextgen_session=/, appCookie + \"=\"),",
11827
+ " );",
11828
+ "}"
11829
+ ];
11830
+ /** The statements {@link viteSessionCookieStatements} produced before ADR 083. */
11831
+ function viteSessionCookieStatementsV1() {
11832
+ return [
11833
+ ...EXPLANATION_V1,
11834
+ VITE_PROJECT_ID,
11835
+ ...APP_COOKIE_V1,
11836
+ "proxy.on(\"proxyReq\", (proxyReq) => {",
11837
+ ...nested(FORWARD_BODY_V1),
11838
+ "});",
11839
+ "proxy.on(\"proxyRes\", (proxyRes) => {",
11840
+ ...nested(RENAME_BODY_V1),
11841
+ "});"
11842
+ ];
11843
+ }
11844
+ /** The statements {@link proxyConfSessionCookieStatements} produced before ADR 083. */
11845
+ function proxyConfSessionCookieStatementsV1() {
11846
+ return [
11847
+ ...EXPLANATION_V1,
11848
+ ...PROXY_CONF_PROJECT_ID,
11849
+ ...APP_COOKIE_V1,
11850
+ "",
11851
+ "function forwardSessionCookie(proxyReq) {",
11852
+ ...nested(FORWARD_BODY_V1),
11853
+ "}",
11854
+ "",
11855
+ "function renameSessionCookie(proxyRes) {",
11856
+ ...nested(RENAME_BODY_V1),
11857
+ "}"
11858
+ ];
11859
+ }
11704
11860
  /** `lines`, each indented by `indent` (blank lines stay blank), joined with `eol`. */
11705
11861
  function indented(lines, indent, eol = "\n") {
11706
11862
  return lines.map((line) => line === "" ? "" : `${indent}${line}`).join(eol);
@@ -11819,7 +11975,8 @@ function appTemplateHtml(ctx) {
11819
11975
  /**
11820
11976
  * The managed `proxy.conf.cjs` for `ng serve`: forwards `/__nextgen/*` to the
11821
11977
  * backend and attaches the project's service-key secret only to the current
11822
- * app-plane `POST /sessions/exchange` request. Both the backend URL
11978
+ * app-plane `POST /sessions/exchange` request, which it knows with and without
11979
+ * the API prefix (`/api/v1`, ADR 077). Both the backend URL
11823
11980
  * (`ZITADEL_URL`) and the secret
11824
11981
  * (`ZITADEL_PROJECT_SECRET`) are read from `.env.local`, which `zitadel setup`
11825
11982
  * writes and `.gitignore` excludes — Angular's CLI does not auto-load env files
@@ -11831,7 +11988,8 @@ function appTemplateHtml(ctx) {
11831
11988
  * backend by its own name (`changeOrigin`), which a remote https server
11832
11989
  * requires, and forwards the app's origin in `X-Forwarded-Host` and
11833
11990
  * `X-Forwarded-Proto` (`xfwd`), and it keeps the session cookie under the
11834
- * app's project's name in the browser (`ZITADEL_PROJECT_ID`, ADR 073).
11991
+ * app's project's name in the browser (`ZITADEL_PROJECT_ID`, ADR 073,
11992
+ * ADR 083).
11835
11993
  */
11836
11994
  function proxyConfTemplate() {
11837
11995
  return `${MANAGED_MARKER}
@@ -11884,25 +12042,37 @@ ${SESSION_COOKIE_HOOKS} },
11884
12042
  const LEGACY_UNCONDITIONAL_BEARER = `function setBearer(proxyReq) {
11885
12043
  proxyReq.setHeader("authorization", bearer);
11886
12044
  }`;
11887
- const EXCHANGE_ONLY_BEARER = `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */
12045
+ /** The bearer hook, marked with the credential policy `marker`, testing the path with `pathCheck`. */
12046
+ function exchangeOnlyBearer(marker, pathCheck) {
12047
+ return `/* ${marker} */
11888
12048
  function setBearer(proxyReq) {
11889
12049
  const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
11890
12050
  if (
11891
12051
  proxyReq.method === "POST" &&
11892
- pathname === "/sessions/exchange" &&
12052
+ ${pathCheck} &&
11893
12053
  !proxyReq.getHeader("authorization")
11894
12054
  ) {
11895
12055
  proxyReq.setHeader("authorization", bearer);
11896
12056
  }
11897
12057
  }`;
11898
- /** The session-cookie hooks' functions, after the bearer hook (ADR 073). */
12058
+ }
12059
+ const EXCHANGE_ONLY_BEARER = exchangeOnlyBearer(PROXY_CREDENTIAL_POLICY_MARKER, EXCHANGE_PATH_CHECK);
12060
+ /** The bearer hook as the CLI wrote it before it knew the API prefix (ADR 077). */
12061
+ const V2_EXCHANGE_ONLY_BEARER = exchangeOnlyBearer(PROXY_CREDENTIAL_POLICY_V2_MARKER, V2_EXCHANGE_PATH_CHECK);
12062
+ /** The exchange-only bearer hook `text` carries, of either version, or `undefined`. */
12063
+ function bearerIn(text) {
12064
+ return [EXCHANGE_ONLY_BEARER, V2_EXCHANGE_ONLY_BEARER].find((bearer) => text.includes(bearer));
12065
+ }
12066
+ /** The session-cookie hooks' functions, after the bearer hook (ADR 073, ADR 083). */
11899
12067
  const SESSION_COOKIE_FUNCTIONS = indented(proxyConfSessionCookieStatements(), "");
12068
+ /** The session-cookie hooks' functions as the CLI wrote them before ADR 083. */
12069
+ const SESSION_COOKIE_FUNCTIONS_V1 = indented(proxyConfSessionCookieStatementsV1(), "");
11900
12070
  /** How the managed entry attached its hooks before it carried the session-cookie ones. */
11901
12071
  const BEARER_ONLY_HOOKS = ` onProxyReq: setBearer,
11902
12072
  configure: (proxy) => proxy.on("proxyReq", setBearer),
11903
12073
  `;
11904
- /** How the managed entry attaches the bearer and the session-cookie hooks. */
11905
- const SESSION_COOKIE_HOOKS = ` onProxyReq: (proxyReq) => {
12074
+ /** How the managed entry attached the bearer and the session-cookie hooks before ADR 083. */
12075
+ const SESSION_COOKIE_HOOKS_V1 = ` onProxyReq: (proxyReq) => {
11906
12076
  setBearer(proxyReq);
11907
12077
  forwardSessionCookie(proxyReq);
11908
12078
  },
@@ -11914,6 +12084,21 @@ const SESSION_COOKIE_HOOKS = ` onProxyReq: (proxyReq) => {
11914
12084
  },
11915
12085
  `;
11916
12086
  /**
12087
+ * How the managed entry attaches the bearer and the session-cookie hooks. Each
12088
+ * cookie hook takes the browser's request too, whose scheme names the cookie.
12089
+ */
12090
+ const SESSION_COOKIE_HOOKS = ` onProxyReq: (proxyReq, req) => {
12091
+ setBearer(proxyReq);
12092
+ forwardSessionCookie(proxyReq, req);
12093
+ },
12094
+ onProxyRes: renameSessionCookie,
12095
+ configure: (proxy) => {
12096
+ proxy.on("proxyReq", setBearer);
12097
+ proxy.on("proxyReq", forwardSessionCookie);
12098
+ proxy.on("proxyRes", renameSessionCookie);
12099
+ },
12100
+ `;
12101
+ /**
11917
12102
  * The managed entry as emitted before it forwarded the app's origin, and what
11918
12103
  * replaces it. Keeping the browser's `Host` also kept `localhost` as the TLS
11919
12104
  * server name, so that proxy could reach no remote https server.
@@ -11949,21 +12134,31 @@ function proxyConfEdit() {
11949
12134
  }
11950
12135
  /** Whether `text` (LF line ends) is the managed proxy as an earlier CLI wrote it, without the session-cookie hooks. */
11951
12136
  function hasBearerOnlyHooks(text) {
11952
- return text.includes("// zitadel-cli: managed-file v1") && text.includes(EXCHANGE_ONLY_BEARER) && text.includes(BEARER_ONLY_HOOKS);
12137
+ return text.includes("// zitadel-cli: managed-file v1") && bearerIn(text) !== void 0 && text.includes(BEARER_ONLY_HOOKS);
12138
+ }
12139
+ /** Whether `text` (LF line ends) is the managed proxy with the session-cookie hooks the CLI wrote before ADR 083. */
12140
+ function hasV1Hooks(text) {
12141
+ return text.includes("// zitadel-cli: managed-file v1") && text.includes(SESSION_COOKIE_FUNCTIONS_V1) && text.includes(SESSION_COOKIE_HOOKS_V1);
11953
12142
  }
11954
12143
  /**
11955
- * Gives the managed proxy the session-cookie hooks (ADR 073) when it is the
11956
- * exact file an earlier CLI wrote: the hook functions go after the bearer hook,
11957
- * and the entry attaches them beside it. A file that carries them already, an
11958
- * unmanaged one, and a managed one whose hooks were changed by hand are left as
11959
- * they are; {@link proxyConfSessionCookieVerdict} tells the doctor which it is.
11960
- * Line ends are the file's own (CRLF or LF). Creates the managed proxy when it
11961
- * is absent, as {@link proxyConfEdit} does.
12144
+ * Gives the managed proxy the session-cookie hooks (ADR 073, ADR 083) when it
12145
+ * is the exact file an earlier CLI wrote: the hook functions go after the
12146
+ * bearer hook, and the entry attaches them beside it; the hooks from before
12147
+ * ADR 083 are replaced where they stand. A file that carries the current hooks
12148
+ * already, an unmanaged one, and a managed one whose hooks were changed by
12149
+ * hand are left as they are; {@link proxyConfSessionCookieVerdict} tells the
12150
+ * doctor which it is. Line ends are the file's own (CRLF or LF). Creates the
12151
+ * managed proxy when it is absent, as {@link proxyConfEdit} does.
11962
12152
  */
11963
12153
  function proxyConfSessionCookieEdit() {
11964
12154
  return (source) => {
11965
12155
  if (source === void 0) return proxyConfTemplate();
11966
- return withLfLineEnds(source, (text) => text.includes("zitadel:session-cookie:v1") || !hasBearerOnlyHooks(text) ? text : text.replace(EXCHANGE_ONLY_BEARER, `${EXCHANGE_ONLY_BEARER}\n\n${SESSION_COOKIE_FUNCTIONS}`).replace(BEARER_ONLY_HOOKS, SESSION_COOKIE_HOOKS));
12156
+ return withLfLineEnds(source, (text) => {
12157
+ if (text.includes("zitadel:session-cookie:v2")) return text;
12158
+ if (hasV1Hooks(text)) return text.replace(SESSION_COOKIE_FUNCTIONS_V1, SESSION_COOKIE_FUNCTIONS).replace(SESSION_COOKIE_HOOKS_V1, SESSION_COOKIE_HOOKS);
12159
+ const bearer = bearerIn(text);
12160
+ return bearer && hasBearerOnlyHooks(text) ? text.replace(bearer, `${bearer}\n\n${SESSION_COOKIE_FUNCTIONS}`).replace(BEARER_ONLY_HOOKS, SESSION_COOKIE_HOOKS) : text;
12161
+ });
11967
12162
  };
11968
12163
  }
11969
12164
  /**
@@ -11971,13 +12166,48 @@ function proxyConfSessionCookieEdit() {
11971
12166
  * The managed file as an earlier CLI wrote it is `detached` (`doctor --fix`
11972
12167
  * adds them). One the edit cannot place them in, a managed file changed by
11973
12168
  * hand or a proxy of the developer's own, is `unknown`: only the developer can.
12169
+ * Hooks from before ADR 083 are `outdated`: they keep the session in cookies
12170
+ * the server no longer reads.
11974
12171
  */
11975
12172
  function proxyConfSessionCookieVerdict(source) {
11976
12173
  const text = lfText(source);
11977
- if (text.includes("zitadel:session-cookie:v1")) return "applied";
12174
+ if (text.includes("zitadel:session-cookie:v2")) return "applied";
12175
+ if (text.includes("zitadel:session-cookie:v1")) return { outdated: hasV1Hooks(text) ? "its session-cookie hooks name the session cookies before ADR 083; doctor --fix updates them" : "its session-cookie hooks name the session cookies before ADR 083 and were changed since zitadel setup wrote them, so they must be updated by hand" };
11978
12176
  if (hasBearerOnlyHooks(text)) return "detached";
11979
12177
  return { unknown: text.includes("// zitadel-cli: managed-file v1") ? "it was changed since zitadel setup wrote it, so the session-cookie hooks (ADR 073) must be added by hand" : "it is not the proxy zitadel setup writes, so the session-cookie hooks (ADR 073) must be added by hand" };
11980
12178
  }
12179
+ /**
12180
+ * Moves the managed proxy's bearer hook from v2, which knew only
12181
+ * `/sessions/exchange`, to v3, which knows the exchange under the API prefix
12182
+ * too (ADR 077): once an app's packages call `/api/v1`, a v2 proxy no longer
12183
+ * attaches the project secret to the exchange. Only the exact hook an earlier
12184
+ * CLI wrote is replaced; a file that carries v3 already, an unmanaged one and a
12185
+ * hook changed by hand are left as they are, and
12186
+ * {@link proxyConfApiPrefixVerdict} tells the doctor which it is. Line ends are
12187
+ * the file's own. Creates the managed proxy when it is absent, as
12188
+ * {@link proxyConfEdit} does.
12189
+ */
12190
+ function proxyConfApiPrefixEdit() {
12191
+ return (source) => {
12192
+ if (source === void 0) return proxyConfTemplate();
12193
+ return withLfLineEnds(source, (text) => text.includes("// zitadel-cli: managed-file v1") && text.includes(V2_EXCHANGE_ONLY_BEARER) ? text.replace(V2_EXCHANGE_ONLY_BEARER, EXCHANGE_ONLY_BEARER) : text);
12194
+ };
12195
+ }
12196
+ /**
12197
+ * Whether `proxy.conf.cjs` attaches the project secret to the exchange under
12198
+ * the API prefix too, for the doctor. The managed file with an earlier CLI's
12199
+ * v2 hook is `detached` (`doctor --fix` moves it to v3). A managed file whose
12200
+ * hook was changed by hand, and a proxy of the developer's own that attaches
12201
+ * the secret but never names `/api/v1/sessions/exchange`, are `unknown`: only
12202
+ * the developer can change them.
12203
+ */
12204
+ function proxyConfApiPrefixVerdict(source) {
12205
+ const text = lfText(source);
12206
+ if (text.includes(EXCHANGE_ONLY_BEARER)) return "applied";
12207
+ if (text.includes("// zitadel-cli: managed-file v1") && text.includes(V2_EXCHANGE_ONLY_BEARER)) return "detached";
12208
+ if (!text.includes("ZITADEL_PROJECT_SECRET") || text.includes(PREFIXED_EXCHANGE_PATH)) return "applied";
12209
+ return { unknown: text.includes("// zitadel-cli: managed-file v1") ? `it was changed since zitadel setup wrote it, so its project-secret check must accept ${PREFIXED_EXCHANGE_PATH} by hand (ADR 077)` : `it is not the proxy zitadel setup writes, so its project-secret check must accept ${PREFIXED_EXCHANGE_PATH} by hand (ADR 077)` };
12210
+ }
11981
12211
  //#endregion
11982
12212
  //#region src/lib/orca/patchers/rule/angular/index.ts
11983
12213
  const SDK_DEPENDENCY$10 = "@zitadel/sdk-angular";
@@ -12076,6 +12306,13 @@ var AngularPatcher = class extends AbstractRulePatcher {
12076
12306
  wiring: "convenience",
12077
12307
  verify: proxyConfSessionCookieVerdict
12078
12308
  },
12309
+ {
12310
+ kind: "edit",
12311
+ path: "proxy.conf.cjs",
12312
+ edit: proxyConfApiPrefixEdit(),
12313
+ wiring: "infrastructure",
12314
+ verify: proxyConfApiPrefixVerdict
12315
+ },
12079
12316
  {
12080
12317
  kind: "edit",
12081
12318
  path: "angular.json",
@@ -12372,7 +12609,7 @@ const ZitadelSession = dynamic(
12372
12609
  "@zitadel/sdk-next/components"
12373
12610
  );
12374
12611
  // Build the SDK project handle and pass it to the session card via the
12375
- // \`project\` prop. The card reads identity from "/__nextgen/sessions/me"
12612
+ // \`project\` prop. The card reads identity from "/__nextgen/api/v1/sessions/me"
12376
12613
  // and exposes a Sign out action. Your own components (a header, an
12377
12614
  // account menu) can make the same read with getSession() from
12378
12615
  // "@zitadel/sdk-next/session" to swap sign-in CTAs for account chrome.
@@ -13039,7 +13276,8 @@ var NuxtPatcher = class extends AbstractRulePatcher {
13039
13276
  * `/__nextgen/*` calls to the
13040
13277
  * backend, strips the prefix, and attaches the project's service-key secret
13041
13278
  * (read from `ZITADEL_PROJECT_SECRET` in `.env.local`) only to the current
13042
- * app-plane `POST /sessions/exchange` operation. The secret stays server-side:
13279
+ * app-plane `POST /sessions/exchange` operation, which it knows with and
13280
+ * without the API prefix (`/api/v1`, ADR 077). The secret stays server-side:
13043
13281
  * Vite only exposes vars with the configured `envPrefix` (default `VITE_`) to
13044
13282
  * client bundles, so this server-only key never leaks into the browser.
13045
13283
  *
@@ -13053,7 +13291,8 @@ var NuxtPatcher = class extends AbstractRulePatcher {
13053
13291
  * browser's `Origin`, which the proxy passes on untouched.
13054
13292
  *
13055
13293
  * The session cookie travels under the app's project's name in the browser and
13056
- * the server's name upstream ({@link viteSessionCookieStatements}, ADR 073).
13294
+ * the server's name upstream ({@link viteSessionCookieStatements}, ADR 073,
13295
+ * ADR 083).
13057
13296
  */
13058
13297
  function proxyEntryCode(server) {
13059
13298
  return `/* ${PROXY_CREDENTIAL_POLICY_MARKER} */ {
@@ -13072,13 +13311,17 @@ ${indented(viteSessionCookieStatements(), " ")}
13072
13311
  },
13073
13312
  }`;
13074
13313
  }
13075
- /** The generated hook that attaches the project secret to the session exchange only. */
13076
- function bearerListener(indent) {
13314
+ /**
13315
+ * The generated hook that attaches the project secret to the session exchange
13316
+ * only. `pathCheck` is the exchange's path test: both spellings, or the one a
13317
+ * v2 proxy knew.
13318
+ */
13319
+ function bearerListener(indent, pathCheck = EXCHANGE_PATH_CHECK) {
13077
13320
  return `${indent}proxy.on("proxyReq", (proxyReq) => {
13078
13321
  ${indent} const pathname = new URL(proxyReq.path, "http://zitadel.local").pathname;
13079
13322
  ${indent} if (
13080
13323
  ${indent} proxyReq.method === "POST" &&
13081
- ${indent} pathname === "/sessions/exchange" &&
13324
+ ${indent} ${pathCheck} &&
13082
13325
  ${indent} !proxyReq.getHeader("authorization")
13083
13326
  ${indent} ) {
13084
13327
  ${indent} proxyReq.setHeader("authorization", bearer);
@@ -13096,7 +13339,7 @@ ${bearerListener(indent)}`;
13096
13339
  * surrounding `/__nextgen` proxy and any user changes elsewhere in the entry.
13097
13340
  */
13098
13341
  function isLegacyManagedProxy(source) {
13099
- if (source.includes("zitadel:proxy:v2")) return false;
13342
+ if (hasProxyCredentialPolicyMarker(source)) return false;
13100
13343
  return [
13101
13344
  "loadEnv(\"development\", process.cwd(), \"ZITADEL_\").ZITADEL_PROJECT_SECRET",
13102
13345
  "throw new Error(\"ZITADEL_PROJECT_SECRET is not set; add it to .env.local (zitadel setup writes it).\")",
@@ -13109,7 +13352,7 @@ function isLegacyManagedProxy(source) {
13109
13352
  * also kept `localhost` as the TLS server name, so that proxy could reach no
13110
13353
  * remote https server.
13111
13354
  */
13112
- const HOST_KEEPING_ENTRY = /(["']\/__nextgen["']:\s*(?:\/\* zitadel:proxy:v2 \*\/\s*)?\{\r?\n([\t ]+)target: [^\r\n]+,\r?\n)\2changeOrigin: false,(\r?\n)/;
13355
+ const HOST_KEEPING_ENTRY = /(["']\/__nextgen["']:\s*(?:\/\* zitadel:proxy:v[23] \*\/\s*)?\{\r?\n([\t ]+)target: [^\r\n]+,\r?\n)\2changeOrigin: false,(\r?\n)/;
13113
13356
  /**
13114
13357
  * Moves the CLI's own host-keeping entry to the origin-forwarding shape of
13115
13358
  * {@link proxyEntryCode}. Only the generated layout in a config that carries
@@ -13118,7 +13361,7 @@ const HOST_KEEPING_ENTRY = /(["']\/__nextgen["']:\s*(?:\/\* zitadel:proxy:v2 \*\
13118
13361
  * is never touched.
13119
13362
  */
13120
13363
  function forwardAppOrigin(source) {
13121
- if (!source.includes("zitadel:proxy:v2") || /\bxfwd\s*:/.test(source)) return source;
13364
+ if (!hasProxyCredentialPolicyMarker(source) || /\bxfwd\s*:/.test(source)) return source;
13122
13365
  return source.replace(HOST_KEEPING_ENTRY, (_entry, head, indent, eol) => `${head}${indent}changeOrigin: true,${eol}${indent}xfwd: true,${eol}`);
13123
13366
  }
13124
13367
  /** The imports that the injected proxy entry depends on. */
@@ -13189,42 +13432,81 @@ function propertyOf(node, name) {
13189
13432
  function fingerprint(source) {
13190
13433
  return source.replace(/\s+/g, "").replace(/'/g, "\"").replace(/;/g, "").replace(/,(?=[)\]}])/g, "").replace(/(^|[(,:=])([A-Za-z_$][\w$]*)=>/g, "$1($2)=>");
13191
13434
  }
13435
+ /** The bearer hook as the CLI writes it now, and as a v2 proxy carries it, fingerprinted. */
13436
+ const HOOK_FINGERPRINTS = [{
13437
+ version: "v3",
13438
+ fingerprint: fingerprint(bearerListener(""))
13439
+ }, {
13440
+ version: "v2",
13441
+ fingerprint: fingerprint(bearerListener("", V2_EXCHANGE_PATH_CHECK))
13442
+ }];
13192
13443
  /**
13193
- * The statements of the `configure(proxy)` body of the `/__nextgen` entry the
13194
- * CLI wrote: one carrying the credential-policy marker and the CLI's own
13195
- * exchange-only bearer hook, however it was formatted since. A hand-written
13196
- * entry, or one whose hook was changed, yields `undefined`.
13444
+ * The `/__nextgen` entry the CLI wrote: one carrying the credential-policy
13445
+ * marker and the CLI's own exchange-only bearer hook, of either version,
13446
+ * however it was formatted since. A hand-written entry, or one whose hook was
13447
+ * changed, yields `undefined`.
13197
13448
  */
13198
- function cliConfigureStatements(config, source) {
13449
+ function cliEntryOf(config, source) {
13199
13450
  const root = config.$ast;
13200
13451
  const proxy = propertyOf(propertyOf(root, "server")?.value, "proxy")?.value;
13201
13452
  const entry = propertyOf(proxy, PROXY_PATH);
13202
13453
  const configure = propertyOf(entry?.value, "configure");
13203
13454
  const body = (configure?.type === "ObjectMethod" ? configure : configure?.value)?.body;
13204
- if (!entry || !body || Array.isArray(body) || body.type !== "BlockStatement") return;
13205
- const block = body;
13206
- const statements = block.body;
13207
- return source.slice(entry.start, entry.end).includes("zitadel:proxy:v2") && fingerprint(source.slice(block.start, block.end)).includes(fingerprint(bearerListener(""))) && statements.length > 0 ? statements : void 0;
13455
+ if (!entry || !body || Array.isArray(body) || body.type !== "BlockStatement" || !hasProxyCredentialPolicyMarker(source.slice(entry.start, entry.end))) return;
13456
+ const statements = body.body;
13457
+ for (const node of statements) {
13458
+ const printed = fingerprint(source.slice(node.start, node.end));
13459
+ const known = HOOK_FINGERPRINTS.find((hook) => hook.fingerprint === printed);
13460
+ if (known) return {
13461
+ entry,
13462
+ statements,
13463
+ hook: {
13464
+ node,
13465
+ version: known.version
13466
+ }
13467
+ };
13468
+ }
13208
13469
  }
13209
13470
  /** What the session-cookie edit and its verdict call the file they read. */
13210
13471
  const VITE_CONFIG_LABEL = "the Vite config (vite.config.*)";
13211
13472
  /**
13212
- * The statements of the CLI's own `/__nextgen` entry's `configure(proxy)` body
13213
- * in `source` (LF line ends), or `undefined` when there is no such entry.
13473
+ * The CLI's own `/__nextgen` entry in `source` (LF line ends), or `undefined`
13474
+ * when there is no such entry.
13214
13475
  */
13215
- function cliEntryStatements(source) {
13216
- return cliConfigureStatements(resolveDefaultExportObject(parseConfigModule(source, VITE_CONFIG_LABEL, { tabWidth: 1 }), VITE_CONFIG_LABEL), source);
13476
+ function cliEntry(source) {
13477
+ return cliEntryOf(resolveDefaultExportObject(parseConfigModule(source, VITE_CONFIG_LABEL, { tabWidth: 1 }), VITE_CONFIG_LABEL), source);
13478
+ }
13479
+ /** How many statements the hooks before ADR 083 are: the project id, the cookie name and the two listeners. */
13480
+ const V1_HOOK_STATEMENTS = 4;
13481
+ /**
13482
+ * `text` (LF line ends) with the session-cookie hooks the CLI wrote before
13483
+ * ADR 083 replaced by the current ones, at their indentation, or `undefined`
13484
+ * when the CLI's entry does not carry them as the CLI wrote them, whatever a
13485
+ * formatter did to their layout.
13486
+ */
13487
+ function withCurrentHooks(text) {
13488
+ const marker = text.indexOf(`/* ${SESSION_COOKIE_MARKER_V1} */`);
13489
+ const statements = marker === -1 ? void 0 : cliEntry(text)?.statements;
13490
+ const first = statements?.findIndex((statement) => statement.start > marker) ?? -1;
13491
+ const last = statements?.[first + V1_HOOK_STATEMENTS - 1];
13492
+ if (first === -1 || !last) return;
13493
+ const lineStart = text.lastIndexOf("\n", marker - 1) + 1;
13494
+ if (fingerprint(text.slice(lineStart, last.end)) !== fingerprint(viteSessionCookieStatementsV1().join("\n"))) return;
13495
+ const indent = /^[\t ]*/.exec(text.slice(lineStart, marker))?.[0] ?? "";
13496
+ return `${text.slice(0, lineStart)}${indented(viteSessionCookieStatements(), indent)}${text.slice(last.end)}`;
13217
13497
  }
13218
13498
  /**
13219
13499
  * Builds the `edit` transform that gives the CLI's own `/__nextgen` entry the
13220
- * session-cookie hooks (ADR 073), for apps set up before the entry carried
13221
- * them: the hooks go after the last statement of its `configure(proxy)` body,
13222
- * at that statement's indentation, and nothing else in the file changes. A
13223
- * config whose entry carries them already, a hand-written entry, and one whose
13224
- * bearer hook was changed are returned untouched; {@link viteSessionCookieVerdict}
13225
- * tells the doctor which it is. Line ends are the file's own (CRLF or LF).
13226
- * Throws `E_VALIDATION`, as {@link viteProxyEdit} does, when the file is absent
13227
- * or unreadable.
13500
+ * session-cookie hooks (ADR 073, ADR 083), for apps set up before the entry
13501
+ * carried them: the hooks go after the last statement of its `configure(proxy)`
13502
+ * body, at that statement's indentation, and nothing else in the file changes.
13503
+ * The hooks the CLI wrote before ADR 083 are replaced where they stand. A
13504
+ * config whose entry carries the current hooks already, a hand-written entry,
13505
+ * one whose bearer hook was changed and one whose hooks before ADR 083 were
13506
+ * changed are returned untouched; {@link viteSessionCookieVerdict} tells the
13507
+ * doctor which it is. Line ends are the file's own (CRLF or LF). Throws
13508
+ * `E_VALIDATION`, as {@link viteProxyEdit} does, when the file is absent or
13509
+ * unreadable.
13228
13510
  */
13229
13511
  function viteSessionCookieEdit() {
13230
13512
  return (source) => {
@@ -13233,8 +13515,9 @@ function viteSessionCookieEdit() {
13233
13515
  return "";
13234
13516
  }
13235
13517
  return withLfLineEnds(source, (text) => {
13236
- if (text.includes("zitadel:session-cookie:v1")) return text;
13237
- const last = cliEntryStatements(text)?.at(-1);
13518
+ if (text.includes("zitadel:session-cookie:v2")) return text;
13519
+ if (text.includes("zitadel:session-cookie:v1")) return withCurrentHooks(text) ?? text;
13520
+ const last = cliEntry(text)?.statements.at(-1);
13238
13521
  if (!last) return text;
13239
13522
  const lineStart = text.lastIndexOf("\n", last.start - 1) + 1;
13240
13523
  const indent = /^[\t ]*/.exec(text.slice(lineStart, last.start))?.[0] ?? "";
@@ -13247,13 +13530,61 @@ function viteSessionCookieEdit() {
13247
13530
  * The CLI's entry without them is `detached` (`doctor --fix` adds them), and so
13248
13531
  * is a config without the CLI's entry, which the proxy edit writes with them.
13249
13532
  * An entry the CLI wrote whose bearer hook was changed by hand is `unknown`:
13250
- * the edit cannot place the hooks there, and only the developer can.
13533
+ * the edit cannot place the hooks there, and only the developer can. Hooks
13534
+ * from before ADR 083 are `outdated`: they keep the session in cookies the
13535
+ * server no longer reads.
13251
13536
  */
13252
13537
  function viteSessionCookieVerdict(source) {
13253
13538
  const text = lfText(source);
13254
- if (text.includes("zitadel:session-cookie:v1")) return "applied";
13255
- if (cliEntryStatements(text)) return "detached";
13256
- return text.includes("zitadel:proxy:v2") ? { unknown: "its /__nextgen proxy entry was changed since zitadel setup wrote it, so the session-cookie hooks (ADR 073) must be added by hand" } : "detached";
13539
+ if (text.includes("zitadel:session-cookie:v2")) return "applied";
13540
+ if (text.includes("zitadel:session-cookie:v1")) return { outdated: withCurrentHooks(text) === void 0 ? "its session-cookie hooks name the session cookies before ADR 083 and were changed since zitadel setup wrote them, so they must be updated by hand" : "its session-cookie hooks name the session cookies before ADR 083; doctor --fix updates them" };
13541
+ if (cliEntry(text)) return "detached";
13542
+ return hasProxyCredentialPolicyMarker(text) ? { unknown: "its /__nextgen proxy entry was changed since zitadel setup wrote it, so the session-cookie hooks (ADR 073) must be added by hand" } : "detached";
13543
+ }
13544
+ /**
13545
+ * Builds the `edit` transform that moves the CLI's own `/__nextgen` entry from
13546
+ * v2, whose bearer hook knew only `/sessions/exchange`, to v3, which knows the
13547
+ * exchange under the API prefix too (ADR 077): once an app's packages call
13548
+ * `/api/v1`, a v2 proxy no longer attaches the project secret to the exchange.
13549
+ * Only the hook statement and the entry's marker change, whatever the
13550
+ * formatter did to the rest; a hand-written entry, a hook changed by hand and
13551
+ * a v3 entry are returned untouched, and {@link viteApiPrefixVerdict} tells
13552
+ * the doctor which it is. Line ends are the file's own. Throws `E_VALIDATION`,
13553
+ * as {@link viteProxyEdit} does, when the file is absent or unreadable.
13554
+ */
13555
+ function viteApiPrefixEdit() {
13556
+ return (source) => {
13557
+ if (source === void 0) {
13558
+ parseConfigModule(source, VITE_CONFIG_LABEL, { tabWidth: 1 });
13559
+ return "";
13560
+ }
13561
+ return withLfLineEnds(source, (text) => {
13562
+ const found = cliEntry(text);
13563
+ if (found?.hook.version !== "v2") return text;
13564
+ const { entry, hook } = found;
13565
+ const lineStart = text.lastIndexOf("\n", hook.node.start - 1) + 1;
13566
+ const indent = /^[\t ]*/.exec(text.slice(lineStart, hook.node.start))?.[0] ?? "";
13567
+ const migrated = text.slice(entry.start, hook.node.start) + bearerListener(indent).slice(indent.length) + text.slice(hook.node.end, entry.end);
13568
+ return text.slice(0, entry.start) + migrated.replaceAll(PROXY_CREDENTIAL_POLICY_V2_MARKER, PROXY_CREDENTIAL_POLICY_MARKER) + text.slice(entry.end);
13569
+ });
13570
+ };
13571
+ }
13572
+ /**
13573
+ * Whether the Vite config's dev proxy attaches the project secret to the
13574
+ * exchange under the API prefix too, for the doctor. The CLI's v2 entry is
13575
+ * `detached` (`doctor --fix` moves it to v3); an entry the CLI wrote whose hook
13576
+ * was changed by hand, and a hand-written proxy that attaches the secret but
13577
+ * never names `/api/v1/sessions/exchange`, are `unknown`: only the developer can
13578
+ * change them. A config with no proxy that attaches the secret has nothing to
13579
+ * move.
13580
+ */
13581
+ function viteApiPrefixVerdict(source) {
13582
+ const text = lfText(source);
13583
+ const found = cliEntry(text);
13584
+ if (found) return found.hook.version === "v3" ? "applied" : "detached";
13585
+ if (hasProxyCredentialPolicyMarker(text)) return { unknown: `its /__nextgen proxy entry was changed since zitadel setup wrote it, so its project-secret check must accept ${PREFIXED_EXCHANGE_PATH} by hand (ADR 077)` };
13586
+ if (text.includes("/__nextgen") && text.includes("ZITADEL_PROJECT_SECRET") && !text.includes(PREFIXED_EXCHANGE_PATH)) return { unknown: `its /__nextgen proxy is not the one zitadel setup writes, so its project-secret check must accept ${PREFIXED_EXCHANGE_PATH} by hand (ADR 077)` };
13587
+ return "applied";
13257
13588
  }
13258
13589
  /**
13259
13590
  * Candidate Vite config filenames, in resolution priority. The patcher hands
@@ -13280,6 +13611,16 @@ function buildViteSessionCookieOp() {
13280
13611
  verify: viteSessionCookieVerdict
13281
13612
  };
13282
13613
  }
13614
+ /** Builds the shared API-prefix {@link FileOp} for a {@link ViteSupport} patcher. */
13615
+ function buildViteApiPrefixOp() {
13616
+ return {
13617
+ kind: "edit",
13618
+ path: [...VITE_CONFIG_PATHS$1],
13619
+ edit: viteApiPrefixEdit(),
13620
+ wiring: "infrastructure",
13621
+ verify: viteApiPrefixVerdict
13622
+ };
13623
+ }
13283
13624
  //#endregion
13284
13625
  //#region src/lib/orca/patchers/rule/qwik/templates.ts
13285
13626
  /**
@@ -13388,6 +13729,9 @@ var QwikPatcher = class extends AbstractRulePatcher {
13388
13729
  viteSessionCookieOp() {
13389
13730
  return buildViteSessionCookieOp();
13390
13731
  }
13732
+ viteApiPrefixOp() {
13733
+ return buildViteApiPrefixOp();
13734
+ }
13391
13735
  async preflight(_ctx, cwd) {
13392
13736
  await assertMountsRootComponent(cwd, await viteEntry(cwd), ROOT$4);
13393
13737
  }
@@ -13403,6 +13747,7 @@ var QwikPatcher = class extends AbstractRulePatcher {
13403
13747
  },
13404
13748
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
13405
13749
  this.viteSessionCookieOp(),
13750
+ this.viteApiPrefixOp(),
13406
13751
  {
13407
13752
  kind: "merge-env",
13408
13753
  path: ".env.example",
@@ -13759,6 +14104,9 @@ var ReactPatcher = class extends AbstractRulePatcher {
13759
14104
  viteSessionCookieOp() {
13760
14105
  return buildViteSessionCookieOp();
13761
14106
  }
14107
+ viteApiPrefixOp() {
14108
+ return buildViteApiPrefixOp();
14109
+ }
13762
14110
  async preflight(_ctx, cwd) {
13763
14111
  await assertMountsRootComponent(cwd, await viteEntry(cwd), ROOT$3);
13764
14112
  }
@@ -13774,6 +14122,7 @@ var ReactPatcher = class extends AbstractRulePatcher {
13774
14122
  },
13775
14123
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
13776
14124
  this.viteSessionCookieOp(),
14125
+ this.viteApiPrefixOp(),
13777
14126
  {
13778
14127
  kind: "merge-env",
13779
14128
  path: ".env.example",
@@ -13913,6 +14262,9 @@ var SolidPatcher = class extends AbstractRulePatcher {
13913
14262
  viteSessionCookieOp() {
13914
14263
  return buildViteSessionCookieOp();
13915
14264
  }
14265
+ viteApiPrefixOp() {
14266
+ return buildViteApiPrefixOp();
14267
+ }
13916
14268
  async preflight(_ctx, cwd) {
13917
14269
  await assertMountsRootComponent(cwd, await viteEntry(cwd), ROOT$2);
13918
14270
  }
@@ -13928,6 +14280,7 @@ var SolidPatcher = class extends AbstractRulePatcher {
13928
14280
  },
13929
14281
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
13930
14282
  this.viteSessionCookieOp(),
14283
+ this.viteApiPrefixOp(),
13931
14284
  {
13932
14285
  kind: "merge-env",
13933
14286
  path: ".env.example",
@@ -14387,6 +14740,9 @@ var SveltePatcher = class extends AbstractRulePatcher {
14387
14740
  viteSessionCookieOp() {
14388
14741
  return buildViteSessionCookieOp();
14389
14742
  }
14743
+ viteApiPrefixOp() {
14744
+ return buildViteApiPrefixOp();
14745
+ }
14390
14746
  async preflight(_ctx, cwd) {
14391
14747
  await assertMountsRootComponent(cwd, await viteEntry(cwd), ROOT$1);
14392
14748
  }
@@ -14402,6 +14758,7 @@ var SveltePatcher = class extends AbstractRulePatcher {
14402
14758
  },
14403
14759
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
14404
14760
  this.viteSessionCookieOp(),
14761
+ this.viteApiPrefixOp(),
14405
14762
  {
14406
14763
  kind: "merge-env",
14407
14764
  path: ".env.example",
@@ -15149,6 +15506,9 @@ var VuePatcher = class extends AbstractRulePatcher {
15149
15506
  viteSessionCookieOp() {
15150
15507
  return buildViteSessionCookieOp();
15151
15508
  }
15509
+ viteApiPrefixOp() {
15510
+ return buildViteApiPrefixOp();
15511
+ }
15152
15512
  async preflight(_ctx, cwd) {
15153
15513
  await assertMountsRootComponent(cwd, await viteEntry(cwd), ROOT);
15154
15514
  }
@@ -15164,6 +15524,7 @@ var VuePatcher = class extends AbstractRulePatcher {
15164
15524
  },
15165
15525
  this.viteProxyOp(ctx.framework.devPort, ctx.server),
15166
15526
  this.viteSessionCookieOp(),
15527
+ this.viteApiPrefixOp(),
15167
15528
  {
15168
15529
  kind: "merge-env",
15169
15530
  path: ".env.example",
@@ -16588,6 +16949,7 @@ var ManagedFilesCheck = class {
16588
16949
  const detachedInfrastructure = wiring.filter((s) => s.state === "detached" && s.wiring === "infrastructure");
16589
16950
  const detachedConvenience = wiring.filter((s) => s.state === "detached" && s.wiring === "convenience");
16590
16951
  const unverifiable = wiring.filter((s) => s.state === "unknown");
16952
+ const outdated = wiring.filter((s) => s.state === "outdated");
16591
16953
  const conflicts = boundaries.filter((b) => b.disposition === "conflict");
16592
16954
  const removable = boundaries.filter((b) => b.disposition === "removable");
16593
16955
  const details = {
@@ -16599,6 +16961,7 @@ var ManagedFilesCheck = class {
16599
16961
  const failures = [];
16600
16962
  if (missingInfrastructure.length > 0) failures.push(`missing scaffolded infrastructure file(s): ${listPaths(missingInfrastructure)}`);
16601
16963
  if (detachedInfrastructure.length > 0) failures.push(`detached managed config wiring: ${detachedInfrastructure.map((s) => s.path).join(", ")}`);
16964
+ if (outdated.length > 0) failures.push(`outdated managed config wiring: ${outdated.map((s) => s.reason ? `${s.path} (${s.reason})` : s.path).join(", ")}`);
16602
16965
  for (const conflict of conflicts) failures.push(`conflicting request boundaries: ${conflict.retired} is user-modified while the current templates use ${conflict.current} — migrate your changes and remove one of them manually`);
16603
16966
  if (failures.length > 0) return {
16604
16967
  name: this.name,
@@ -17338,13 +17701,13 @@ async function inspect(cwd) {
17338
17701
  status: "pass",
17339
17702
  message: `${issuer} is not recorded as a preview origin`
17340
17703
  };
17341
- const client = createZitadelClient({
17704
+ const client = createZitadelClient$1({
17342
17705
  baseUrl: server,
17343
17706
  token: secret.project_secret
17344
17707
  });
17345
17708
  let origins;
17346
17709
  try {
17347
- origins = (await client.getProject(secret.project_id)).origins ?? [];
17710
+ origins = (await client.getProject(secret.project_id)).origins;
17348
17711
  } catch (error) {
17349
17712
  return {
17350
17713
  status: "pass",
@@ -17614,18 +17977,24 @@ var Doctor = class Doctor extends BaseCommand {
17614
17977
  port
17615
17978
  } : {},
17616
17979
  project: { lifecycle: hasConfig ? "configured" : "not-configured" },
17617
- checks,
17618
- ...nextActions.length > 0 ? { next_actions: nextActions } : {},
17619
- ...warningAdvice ? nextCommandsData(warningAdvice.nextCommands) : {}
17980
+ checks
17981
+ };
17982
+ const next = {
17983
+ ...nextActions.length > 0 ? { actions: nextActions } : {},
17984
+ ...warningAdvice ? { commands: warningAdvice.nextCommands } : {}
17620
17985
  };
17621
17986
  if (failed.length > 0) {
17622
17987
  const advice = failureAdvice(failed, image, port, this.meta.cliVersion, selector, parentRuntimeDir, cwdFlag);
17988
+ const report = {
17989
+ ...data,
17990
+ ...nextStepsData(next)
17991
+ };
17623
17992
  const code = failed.some((check) => check.name === "port") ? "E_PORT_IN_USE" : failed.find((check) => check.code !== void 0)?.code ?? "E_VALIDATION";
17624
17993
  throw new ZitadelError(code, "Zitadel doctor found issues", {
17625
17994
  hint: advice.hint,
17626
17995
  nextCommands: advice.nextCommands,
17627
- details: advice.suggestedArgs === void 0 ? data : {
17628
- ...data,
17996
+ details: advice.suggestedArgs === void 0 ? report : {
17997
+ ...report,
17629
17998
  suggested_args: advice.suggestedArgs
17630
17999
  }
17631
18000
  });
@@ -17633,6 +18002,7 @@ var Doctor = class Doctor extends BaseCommand {
17633
18002
  return this.emit({
17634
18003
  status: "ok",
17635
18004
  data,
18005
+ next,
17636
18006
  warnings: warnings.map((warning) => `${warning.name}: ${warning.message}`)
17637
18007
  });
17638
18008
  }
@@ -17798,7 +18168,7 @@ async function runEnvironmentChecks(input) {
17798
18168
  const token = environment.values.ZITADEL_PROJECT_SECRET?.value;
17799
18169
  if (server === void 0 || projectId === void 0) throw new Error(`ZITADEL_URL and ZITADEL_PROJECT_ID must resolve for the ${name} environment`);
17800
18170
  if (token === void 0) throw new Error(`ZITADEL_PROJECT_SECRET must resolve to verify project ${projectId} on ${server}`);
17801
- const project = await createZitadelClient({
18171
+ const project = await createZitadelClient$1({
17802
18172
  baseUrl: server,
17803
18173
  token
17804
18174
  }).getProject(projectId);
@@ -18191,9 +18561,9 @@ var Eject = class Eject extends Promptable(BaseCommand) {
18191
18561
  files_removed: removed,
18192
18562
  files_preserved: preserved,
18193
18563
  backed_up: backedUp,
18194
- ...nextCommandsData(nextCommands),
18195
18564
  manual_steps: manualSteps
18196
- }
18565
+ },
18566
+ next: { commands: nextCommands }
18197
18567
  });
18198
18568
  }
18199
18569
  };
@@ -18276,7 +18646,7 @@ var Env = class Env extends EnvironmentCommand {
18276
18646
  /** The frameworks whose server needs the project secret at runtime (the session exchange). */
18277
18647
  const SERVER_RENDERED = ["next", "nuxt"];
18278
18648
  /** The message the release this command publishes carries. */
18279
- const INITIAL_DEPLOY_MESSAGE = "Initial deployment from zitadel env add";
18649
+ const INITIAL_DEPLOY_MESSAGE$1 = "Initial deployment from zitadel env add";
18280
18650
  /**
18281
18651
  * `zitadel env add` — bind an environment to a project: create one on the
18282
18652
  * server, or bind an id a teammate sent over, and write the
@@ -18356,7 +18726,7 @@ var EnvAdd = class EnvAdd extends Promptable(BaseCommand) {
18356
18726
  let projectSecret = env.ZITADEL_PROJECT_SECRET;
18357
18727
  let previewToken = env.ZITADEL_PREVIEW_TOKEN;
18358
18728
  if (projectId === void 0) {
18359
- const created = await createZitadelClient({ baseUrl: server }).createProject({
18729
+ const created = await createZitadelClient$1({ baseUrl: server }).createProject({
18360
18730
  name: `${basename(cwd)}-${name}`,
18361
18731
  ...issuer === void 0 ? {} : { origins: [{
18362
18732
  pattern: issuer,
@@ -18488,19 +18858,16 @@ var EnvAdd = class EnvAdd extends Promptable(BaseCommand) {
18488
18858
  content_hash: deployment.contentHash,
18489
18859
  deployment_id: deployment.deploymentId,
18490
18860
  origins: [...deployment.origins]
18491
- },
18492
- ...nextCommandsData([
18493
- ...needsOrigin && issuer !== void 0 && writtenKeys.has("ZITADEL_PROJECT_SECRET") ? [projectCommand(`origin add ${shellArg(issuer)} --kind primary`, newEnvironment, cliVersion)] : [],
18494
- ...hasSomewhereToLand && deployment === void 0 ? [projectCommand(DEPLOY_ARGS, newEnvironment, cliVersion)] : [],
18495
- projectCommand("env", newEnvironment, cliVersion)
18496
- ])
18861
+ }
18497
18862
  },
18863
+ next: { commands: [
18864
+ ...needsOrigin && issuer !== void 0 && writtenKeys.has("ZITADEL_PROJECT_SECRET") ? [projectCommand(`origin add ${shellArg(issuer)} --kind primary`, newEnvironment, cliVersion)] : [],
18865
+ ...hasSomewhereToLand && deployment === void 0 ? [projectCommand(DEPLOY_ARGS, newEnvironment, cliVersion)] : [],
18866
+ projectCommand("env", newEnvironment, cliVersion)
18867
+ ] },
18498
18868
  pretty: [
18499
18869
  `Wrote ${file} with ${Object.keys(values).join(", ")}`,
18500
- "",
18501
- ...needsOrigin ? [writtenKeys.has("ZITADEL_PROJECT_SECRET") ? `Allow this project's origin: zitadel origin add ${issuer === void 0 ? "'<your-app-url>'" : shellArg(issuer)} --kind primary${newEnvironment}` : `Allow this project's origin once ZITADEL_PROJECT_SECRET is in ${file}: zitadel origin add ${issuer === void 0 ? "'<your-app-url>'" : shellArg(issuer)} --kind primary${newEnvironment}`] : [],
18502
- ...createdProject && deployment === void 0 ? [`Nothing is deployed to ${name} yet, so it serves the server's defaults rather than .zitadel/.`, ...hasSomewhereToLand ? [`Deploy to it: zitadel ${DEPLOY_ARGS}${newEnvironment}`] : [`Give it an origin first: zitadel origin add '<your-app-url>' --kind primary${newEnvironment}`, `Then deploy to it: zitadel ${DEPLOY_ARGS}${newEnvironment}`]] : [],
18503
- `Check what resolves: zitadel env${newEnvironment}`,
18870
+ ...guidanceBlock([...needsOrigin ? writtenKeys.has("ZITADEL_PROJECT_SECRET") ? issuer === void 0 ? [`Allow this project's origin: zitadel origin add '<your-app-url>' --kind primary${newEnvironment}`] : [] : [`Allow this project's origin once ZITADEL_PROJECT_SECRET is in ${file}: zitadel origin add ${issuer === void 0 ? "'<your-app-url>'" : shellArg(issuer)} --kind primary${newEnvironment}`] : [], ...createdProject && deployment === void 0 ? [`Nothing is deployed to ${name} yet, so it serves the server's defaults rather than .zitadel/.`, ...hasSomewhereToLand ? [] : [`Give it an origin first: zitadel origin add '<your-app-url>' --kind primary${newEnvironment}`, `Then deploy to it: zitadel ${DEPLOY_ARGS}${newEnvironment}`]] : []]),
18504
18871
  "",
18505
18872
  "The deployed app never reads that file. Set these where it is built and run.",
18506
18873
  "",
@@ -18516,10 +18883,8 @@ var EnvAdd = class EnvAdd extends Promptable(BaseCommand) {
18516
18883
  });
18517
18884
  }
18518
18885
  /**
18519
- * Publish `.zitadel/` to the project this run just created, the way `deploy`
18520
- * does: build the bundle, validate it, create the release and deploy it to
18521
- * every primary origin. `deploy`'s diff step is left out, because nothing
18522
- * has ever been deployed here and there is no baseline to diff against.
18886
+ * Publish `.zitadel/` to the project this run just created (see
18887
+ * {@link deployInitialRelease}).
18523
18888
  *
18524
18889
  * It authenticates with the new project's own secret rather than the one
18525
18890
  * this directory resolves to, since `env add` binds a different project from
@@ -18528,53 +18893,33 @@ var EnvAdd = class EnvAdd extends Promptable(BaseCommand) {
18528
18893
  */
18529
18894
  async deployInitialRelease(opts) {
18530
18895
  const json = this.jsonEnabled();
18531
- const client = createZitadelClient({
18532
- baseUrl: opts.server,
18533
- token: opts.projectSecret
18534
- });
18535
18896
  consola.start("Deploying .zitadel/ to the new environment");
18536
18897
  try {
18537
- const { bundle, brandingSources, idpFiles, policyFiles } = await buildBundle(opts.cwd);
18538
- const ctx = {
18539
- cwd: opts.cwd,
18540
- client,
18898
+ const deployment = await deployInitialRelease({
18899
+ client: createZitadelClient$1({
18900
+ baseUrl: opts.server,
18901
+ token: opts.projectSecret
18902
+ }),
18541
18903
  projectId: opts.projectId,
18904
+ cwd: opts.cwd,
18542
18905
  env: this.meta.env,
18543
- mode: "deploy",
18544
- dryRun: false,
18545
18906
  json,
18546
18907
  color: !json && Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
18547
- bundle,
18548
- brandingSources,
18549
- idpFiles,
18550
- policyFiles,
18908
+ message: INITIAL_DEPLOY_MESSAGE$1,
18551
18909
  suggest: (args) => projectCommand(args, opts.selector, this.meta.cliVersion)
18552
- };
18553
- await runSteps([validateStep], ctx);
18554
- const release = await createReleaseFromBundle(client, opts.projectId, bundle, {
18555
- message: INITIAL_DEPLOY_MESSAGE,
18556
- gitSha: await headSha(opts.cwd),
18557
- gitDirty: await worktreeIsDirty(opts.cwd)
18558
18910
  });
18559
- const deployment = await createDeployment(client, opts.projectId, {
18560
- releaseId: release.id,
18561
- targets: ["primary"],
18562
- message: INITIAL_DEPLOY_MESSAGE
18563
- });
18564
- const origins = deployment.targets.map((target) => target.origin);
18565
- consola.success(`Deployed ${release.content_hash} to ${origins.length} origin${origins.length === 1 ? "" : "s"}`);
18566
- return {
18567
- releaseId: release.id,
18568
- contentHash: release.content_hash,
18569
- deploymentId: deployment.id,
18570
- origins
18571
- };
18911
+ consola.success(`Deployed ${deployment.contentHash} to ${deployment.origins.length} origin${deployment.origins.length === 1 ? "" : "s"}`);
18912
+ return deployment;
18572
18913
  } catch (error) {
18573
18914
  reportWarning(`The environment was bound but the first deploy failed: ${toZitadelError(error).message}. Run \`zitadel ${DEPLOY_ARGS}${opts.selector}\` to publish it.`);
18574
18915
  return;
18575
18916
  }
18576
18917
  }
18577
18918
  };
18919
+ /** Lines under a blank line that sets them apart, or nothing when there are none. */
18920
+ function guidanceBlock(lines) {
18921
+ return lines.length === 0 ? [] : ["", ...lines];
18922
+ }
18578
18923
  //#endregion
18579
18924
  //#region src/commands/env/list.ts
18580
18925
  /** The columns `list` renders, in order. */
@@ -18975,9 +19320,9 @@ var OriginAdd = class OriginAdd extends EnvironmentCommand {
18975
19320
  status: "ok",
18976
19321
  data: {
18977
19322
  pattern: added.pattern,
18978
- kind: added.kind,
18979
- ...nextCommandsData([this.suggest("origin list")])
19323
+ kind: added.kind
18980
19324
  },
19325
+ next: { commands: [this.suggest("origin list")] },
18981
19326
  pretty: `Added ${added.pattern} (${added.kind})`
18982
19327
  });
18983
19328
  }
@@ -19002,7 +19347,7 @@ var OriginList = class OriginList extends EnvironmentCommand {
19002
19347
  const { flags } = await this.parse(OriginList);
19003
19348
  await this.toMeta(flags);
19004
19349
  const { client, projectId } = await this.connect();
19005
- const origins = (await client.getProject(projectId)).origins ?? [];
19350
+ const origins = (await client.getProject(projectId)).origins;
19006
19351
  const plain = flags.plain || !process.stdout.isTTY;
19007
19352
  return this.emit({
19008
19353
  status: "ok",
@@ -19059,7 +19404,7 @@ var OriginRm = class OriginRm extends Promptable(EnvironmentCommand) {
19059
19404
  reason: "remove-cancelled"
19060
19405
  });
19061
19406
  await client.removeOrigin(projectId, { pattern });
19062
- const remaining = await client.getProject(projectId).then((project) => project.origins ?? [], (error) => {
19407
+ const remaining = await client.getProject(projectId).then((project) => project.origins, (error) => {
19063
19408
  reportWarning(`${pattern} was removed, but the project's remaining origins could not be read: ${error instanceof Error ? error.message : String(error)}`);
19064
19409
  });
19065
19410
  if (remaining?.length === 0) reportWarning(`${pattern} was the project's last origin, so the project now admits every origin. Add the ones it should admit with \`zitadel origin add <url> --kind primary${this.environmentSelector()}\`.`);
@@ -19192,10 +19537,12 @@ var Preview = class Preview extends PreviewCommand {
19192
19537
  static examples = [
19193
19538
  "<%= config.bin %> preview",
19194
19539
  "<%= config.bin %> preview --origin https://acme-git-sso-acmeinc.vercel.app",
19195
- "<%= config.bin %> preview --ttl 24h"
19540
+ "<%= config.bin %> preview --ttl 24h",
19541
+ "<%= config.bin %> preview -m 'add phone_number to human-user'"
19196
19542
  ];
19197
19543
  static flags = {
19198
19544
  ...previewFlags,
19545
+ message: messageFlag("A short summary of what this release changes. Defaults to the pull request's title or the current commit's subject."),
19199
19546
  strict: Flags.boolean({ description: `Fail instead of warning when no preview credential (${CREDENTIALS[PREVIEW_CREDENTIAL]}) is set.` })
19200
19547
  };
19201
19548
  async run() {
@@ -19266,13 +19613,16 @@ var Preview = class Preview extends PreviewCommand {
19266
19613
  },
19267
19614
  pretty: [`Validated the preview for ${urls.length} URL${urls.length === 1 ? "" : "s"}. Nothing was deployed (dry run).`, ...urls.map((url) => ` - ${url}`)].join("\n")
19268
19615
  });
19616
+ const message = await deployMessage(flags.message, cwd, env);
19269
19617
  const release = await createReleaseFromBundle(client, projectId, bundle, {
19618
+ message,
19270
19619
  gitSha: await headSha(cwd),
19271
19620
  gitDirty: await worktreeIsDirty(cwd)
19272
19621
  });
19273
19622
  const deployment = await createDeployment(client, projectId, {
19274
19623
  releaseId: release.id,
19275
19624
  targets: urls,
19625
+ message,
19276
19626
  ...ttl === void 0 ? {} : { ttl: ttl.iso }
19277
19627
  });
19278
19628
  const previews = deployment.targets.map((target) => ({
@@ -19304,7 +19654,7 @@ var Preview = class Preview extends PreviewCommand {
19304
19654
  */
19305
19655
  async function primaryOrigins(client, projectId) {
19306
19656
  try {
19307
- return ((await client.getProject(projectId)).origins ?? []).filter((entry) => entry.kind === "primary").map((entry) => entry.pattern);
19657
+ return (await client.getProject(projectId)).origins.filter((entry) => entry.kind === "primary").map((entry) => entry.pattern);
19308
19658
  } catch (error) {
19309
19659
  if (error instanceof ApiError && error.status === 403) return [];
19310
19660
  throw error;
@@ -19361,17 +19711,15 @@ var PreviewList = class PreviewList extends PreviewCommand {
19361
19711
  const { flags } = await this.parse(PreviewList);
19362
19712
  await this.toMeta(flags);
19363
19713
  const { client, projectId } = await this.connect();
19364
- const request = (token) => client.listPreviews(projectId, {
19365
- ...flags.limit === void 0 ? {} : { limit: flags.limit },
19366
- ...token === void 0 ? {} : { page_token: token }
19367
- });
19368
- const { items, next } = await collectPages(request, {
19369
- items: "previews",
19714
+ const params = {
19715
+ limit: flags.limit,
19716
+ page_token: flags["page-token"]
19717
+ };
19718
+ const { items: previews, next } = await collectPages(client.listPreviews.pages(projectId, params), {
19719
+ items: (page) => page.previews,
19370
19720
  all: flags.all === true,
19371
- token: flags["page-token"],
19372
- nextPageToken: "next_page_token"
19721
+ next: (page) => page.next_page_token
19373
19722
  });
19374
- const previews = items;
19375
19723
  const rows = previews.map((preview) => ({
19376
19724
  url: preview.origin,
19377
19725
  expires: preview.expires_at,
@@ -19386,10 +19734,9 @@ var PreviewList = class PreviewList extends PreviewCommand {
19386
19734
  const plain = flags.plain || !process.stdout.isTTY;
19387
19735
  return this.emit({
19388
19736
  status: "ok",
19389
- data: listData(previews, {
19390
- nextPageToken: next,
19391
- nextCommands: nextCommand === void 0 ? void 0 : [nextCommand]
19392
- }),
19737
+ data: listData(previews, { nextPageToken: next }),
19738
+ ...nextCommand === void 0 ? {} : { next: { commands: [nextCommand] } },
19739
+ machineReadable: plain,
19393
19740
  pretty: renderList(COLUMNS$3, rows, {
19394
19741
  plain,
19395
19742
  singular: "preview",
@@ -19433,16 +19780,11 @@ var PreviewRm = class PreviewRm extends Promptable(PreviewCommand) {
19433
19780
  });
19434
19781
  }
19435
19782
  const { client, projectId } = await this.connect();
19436
- const { items } = await collectPages((token) => client.listPreviews(projectId, token === void 0 ? {} : { page_token: token }), {
19437
- items: "previews",
19438
- all: true,
19439
- nextPageToken: "next_page_token"
19440
- });
19441
- if (!items.some((preview) => preview.origin === url)) return this.emit({
19783
+ if (!(await client.listPreviews.all(projectId)).some((preview) => preview.origin === url)) return this.emit({
19442
19784
  status: "skipped",
19443
19785
  reason: "not-live",
19444
19786
  pretty: `${url} is not a live preview of the project; nothing was removed`,
19445
- nextCommands: [this.suggest("preview list")]
19787
+ next: { commands: [this.suggest("preview list")] }
19446
19788
  });
19447
19789
  if (!await this.input({
19448
19790
  kind: "confirm",
@@ -19628,9 +19970,9 @@ var Pull = class Pull extends EnvironmentCommand {
19628
19970
  release_id: release.id,
19629
19971
  pulled,
19630
19972
  skipped,
19631
- ...dryRun ? { dry_run: true } : {},
19632
- ...nextCommandsData(nextCommands)
19973
+ ...dryRun ? { dry_run: true } : {}
19633
19974
  },
19975
+ next: { commands: nextCommands },
19634
19976
  pretty: dryRun ? `Would write ${pulled.length} file${pulled.length === 1 ? "" : "s"} from ${release.id} (dry run).\n${summary}` : `Pulled ${pulled.length} file${pulled.length === 1 ? "" : "s"} from ${release.id}.\n${summary}`
19635
19977
  });
19636
19978
  }
@@ -19645,7 +19987,10 @@ var Pull = class Pull extends EnvironmentCommand {
19645
19987
  if (classified.kind === "release") return classified.id;
19646
19988
  if (classified.kind === "deployment") return (await client.getDeploymentById(classified.id, { project_id: projectId })).release_id;
19647
19989
  if (classified.kind === "latest") {
19648
- const deployment = (await listLiveDeployments(client, projectId)).find(servesPrimary);
19990
+ const deployment = (await client.listDeployments.all({
19991
+ project_id: projectId,
19992
+ live: true
19993
+ })).find(servesPrimary);
19649
19994
  if (deployment === void 0) throw new ZitadelError("E_NOT_FOUND", "Nothing is deployed to a primary origin yet.", { hint: `Deploy first with \`zitadel ${DEPLOY_ARGS}${this.environmentSelector()}\`, then pull.` });
19650
19995
  return deployment.release_id;
19651
19996
  }
@@ -19727,9 +20072,9 @@ var ReleaseCreate = class ReleaseCreate extends EnvironmentCommand {
19727
20072
  data: {
19728
20073
  release_id: release.id,
19729
20074
  content_hash: release.content_hash,
19730
- resources: resourceCount,
19731
- ...nextCommandsData([this.suggest(`deployment create --release ${release.id}`)])
20075
+ resources: resourceCount
19732
20076
  },
20077
+ next: { commands: [this.suggest(`deployment create --release ${release.id}`)] },
19733
20078
  pretty: `Created ${release.id} (${release.content_hash})`
19734
20079
  });
19735
20080
  }
@@ -19819,9 +20164,9 @@ var Reset = class Reset extends Promptable(BaseCommand) {
19819
20164
  log_path: runtime.log_path
19820
20165
  } : { container_name: containerName },
19821
20166
  data_deleted: true
19822
- },
19823
- ...nextCommandsData([this.suggestUnscoped("reset --force")])
19824
- }
20167
+ }
20168
+ },
20169
+ next: { commands: [this.suggestUnscoped("reset --force")] }
19825
20170
  });
19826
20171
  if (!await this.input({
19827
20172
  kind: "confirm",
@@ -19872,9 +20217,13 @@ var ResourceCommand = class extends Promptable(EnvironmentCommand) {
19872
20217
  args: parsed.args
19873
20218
  }, meta));
19874
20219
  }
19875
- /** Open the platform connection; deferred so `--dry-run` never connects. */
19876
- context() {
19877
- return this.definition.options.connect(() => this.connect());
20220
+ /**
20221
+ * Open the platform connection; deferred so `--dry-run` never connects.
20222
+ * `verbatim` is for a caller that sends back what it reads, as a list sends
20223
+ * back its cursor.
20224
+ */
20225
+ context(options = {}) {
20226
+ return this.definition.options.connect(() => this.connect(options));
19878
20227
  }
19879
20228
  /**
19880
20229
  * The operation's work. Every concrete operation overrides this; the base
@@ -19968,8 +20317,10 @@ var DeleteOperation = class extends ResourceCommand {
19968
20317
  *
19969
20318
  * These live behind {@link WireConventions} rather than inline in the list
19970
20319
  * operation so the factory stays honest about what it assumes: a platform
19971
- * whose cursor is `next_cursor`, or whose filters are a map rather than a
19972
- * list, overrides the vocabulary instead of forking the factory.
20320
+ * that sends its cursor as `cursor`, reports it as `next_cursor`, or takes its
20321
+ * filters as a map rather than a list, overrides the vocabulary instead of
20322
+ * forking the factory. A response is never read by these names: each list
20323
+ * reads its items and cursor through its own typed `items` and `next`.
19973
20324
  */
19974
20325
  const DEFAULT_WIRE = {
19975
20326
  limit: "limit",
@@ -19988,8 +20339,12 @@ const wireOf = (overrides) => ({
19988
20339
  });
19989
20340
  //#endregion
19990
20341
  //#region src/lib/oclif/crud/query.ts
19991
- /** `field=operation:value`, or `field=value` for `equals`. */
19992
- const parseFilter = (raw, filters) => {
20342
+ /**
20343
+ * `field=operation:value`, or `field=value` for `equals`. `known` is every
20344
+ * operation the platform has, so one the field does not offer is refused
20345
+ * rather than read as part of a value.
20346
+ */
20347
+ const parseFilter = (raw, filters, known) => {
19993
20348
  const names = filters.map((filter) => filter.field);
19994
20349
  const [name, ...rest] = raw.split("=");
19995
20350
  if (!name || rest.length === 0) throw new ZitadelError("E_VALIDATION", `Invalid --filter "${raw}"`, { hint: `Use field=operation:value, e.g. ${names[0]}=${filters[0]?.operations[0]}:<value>.` });
@@ -20008,7 +20363,7 @@ const parseFilter = (raw, filters) => {
20008
20363
  const expression = rest.join("=");
20009
20364
  const [candidate = "", ...rawValue] = expression.split(":");
20010
20365
  const operations = field.operations;
20011
- if (operations.includes(candidate) && rawValue.length > 0) return checked({
20366
+ if (operations.includes(candidate) && rawValue.length > 0) return checked$1({
20012
20367
  field,
20013
20368
  operation: candidate,
20014
20369
  value: rawValue.join(":")
@@ -20023,7 +20378,7 @@ const parseFilter = (raw, filters) => {
20023
20378
  operations: [...operations]
20024
20379
  }
20025
20380
  });
20026
- if (!operations.includes(candidate) && KNOWN_OPERATIONS.has(candidate)) throw new ZitadelError("E_VALIDATION", `Filter field "${name}" does not support the ${candidate} operation`, {
20381
+ if (!operations.includes(candidate) && known.includes(candidate)) throw new ZitadelError("E_VALIDATION", `Filter field "${name}" does not support the ${candidate} operation`, {
20027
20382
  hint: `${name} accepts: ${operations.join(", ")}.`,
20028
20383
  details: {
20029
20384
  field: name,
@@ -20039,29 +20394,14 @@ const parseFilter = (raw, filters) => {
20039
20394
  operations: [...operations]
20040
20395
  }
20041
20396
  });
20042
- return checked({
20397
+ return checked$1({
20043
20398
  field,
20044
20399
  operation: "equals",
20045
20400
  value: expression
20046
20401
  });
20047
20402
  };
20048
- /**
20049
- * Operation names the CLI recognises as operations at all, so a value that
20050
- * merely looks like `something:else` is not mistaken for one.
20051
- */
20052
- const KNOWN_OPERATIONS = /* @__PURE__ */ new Set([
20053
- "equals",
20054
- "not_equals",
20055
- "contains",
20056
- "not_contains",
20057
- "less_than",
20058
- "less_than_or_equal",
20059
- "greater_than",
20060
- "greater_than_or_equal",
20061
- "array_contains"
20062
- ]);
20063
20403
  /** Reject a value outside a field's closed set before any request is made. */
20064
- const checked = (parsed) => {
20404
+ const checked$1 = (parsed) => {
20065
20405
  const { values } = parsed.field;
20066
20406
  if (values && !values.includes(parsed.value)) throw new ZitadelError("E_VALIDATION", `Invalid value "${parsed.value}" for filter field "${parsed.field.field}"`, {
20067
20407
  hint: `Accepted values: ${values.join(", ")}.`,
@@ -20122,14 +20462,6 @@ const shapeOf = (schema) => {
20122
20462
  const merged = Object.assign({}, ...options.map((option) => shapeOf(option) ?? {}));
20123
20463
  return Object.keys(merged).length > 0 ? merged : void 0;
20124
20464
  };
20125
- /**
20126
- * The element schema of a list response's item array. `key` names the property
20127
- * holding the array; without one the response schema is the array itself.
20128
- */
20129
- const itemSchemaOf = (response, key) => {
20130
- const array = key === void 0 ? unwrap(response) : unwrap(shapeOf(response)?.[key]);
20131
- return unwrap(array?.def?.element) ?? void 0;
20132
- };
20133
20465
  /** Walk a record schema into the paths a caller may name. */
20134
20466
  const fieldPaths = (schema, depth = 3) => {
20135
20467
  const shape = shapeOf(schema);
@@ -20257,15 +20589,15 @@ var ListOperation = class extends ResourceCommand {
20257
20589
  async execute({ flags }, _meta) {
20258
20590
  const { topic, resource, spec, options } = this.definition;
20259
20591
  const wire = wireOf(options.wire);
20260
- const shape = fieldPaths(itemSchemaOf(spec.response, spec.items));
20592
+ const shape = fieldPaths(spec.item);
20261
20593
  const early = shape ? chosenColumns(flags.fields, resource.columns, [], shape) : void 0;
20262
- const ctx = await this.context();
20594
+ const ctx = await this.context({ verbatim: true });
20263
20595
  const paged = spec.paged !== false;
20264
20596
  const paging = (token) => paged ? {
20265
20597
  ...typeof flags.limit === "number" && { [wire.limit]: flags.limit },
20266
20598
  ...token && { [wire.pageToken]: token }
20267
20599
  } : {};
20268
- const given = (Array.isArray(flags.filter) ? flags.filter : []).map((raw) => parseFilter(String(raw), spec.filters ?? []));
20600
+ const given = (Array.isArray(flags.filter) ? flags.filter : []).map((raw) => parseFilter(String(raw), spec.filters ?? [], options.operations));
20269
20601
  const defaults = (spec.filters ?? []).flatMap((field) => field.default !== void 0 && !given.some((one) => one.field.field === field.field) ? [{
20270
20602
  field,
20271
20603
  operation: "equals",
@@ -20292,16 +20624,17 @@ var ListOperation = class extends ResourceCommand {
20292
20624
  progress?.start(`Fetching ${topic}`);
20293
20625
  let fetched = 0;
20294
20626
  let drained = false;
20295
- const { items, next } = await collectPages(async (token) => {
20296
- const page = await request(token);
20627
+ const first = typeof flags["page-token"] === "string" ? flags["page-token"] : void 0;
20628
+ const walk = pages(async (token) => {
20629
+ const page = await request(token ?? first);
20297
20630
  fetched += 1;
20298
20631
  progress?.message(`Fetching ${topic} — page ${fetched}`);
20299
20632
  return page;
20300
- }, {
20301
- items: spec.items,
20633
+ }, { next: (response) => spec.next?.(response) || void 0 });
20634
+ const { items, next } = await collectPages(walk, {
20635
+ items: (response) => sanitizeResponse(spec.items(response)),
20302
20636
  all,
20303
- nextPageToken: wire.nextPageToken,
20304
- token: paged && typeof flags["page-token"] === "string" ? flags["page-token"] : void 0
20637
+ next: (response) => sanitizeResponse(spec.next?.(response))
20305
20638
  }).then((page) => {
20306
20639
  drained = true;
20307
20640
  return page;
@@ -20322,9 +20655,10 @@ var ListOperation = class extends ResourceCommand {
20322
20655
  data: {
20323
20656
  items,
20324
20657
  count: items.length,
20325
- [wire.nextPageToken]: next,
20326
- ...nextCommands.length > 0 ? nextCommandsData(nextCommands) : {}
20658
+ [wire.nextPageToken]: next
20327
20659
  },
20660
+ ...nextCommands.length > 0 ? { next: { commands: nextCommands } } : {},
20661
+ machineReadable: plain,
20328
20662
  pretty: plain ? renderRows(columns, items) : items.length === 0 ? `No ${topic} found.` : [
20329
20663
  renderTable(columns, items),
20330
20664
  "",
@@ -20394,10 +20728,11 @@ var GetOperation = class extends ResourceCommand {
20394
20728
  data: item,
20395
20729
  pretty: JSON.stringify(item, null, 2)
20396
20730
  };
20731
+ const sections = flags.fields === void 0 ? spec.sections?.(item) ?? [] : [];
20397
20732
  return {
20398
20733
  status: "ok",
20399
20734
  data: item,
20400
- pretty: renderDetail(headingOf(resource.heading ?? resource.idField, item), fields, item)
20735
+ pretty: [renderDetail(headingOf(resource.heading ?? resource.idField, item), fields, item), ...sections].join("\n\n")
20401
20736
  };
20402
20737
  }
20403
20738
  };
@@ -20577,15 +20912,12 @@ var WriteOperation = class extends ResourceCommand {
20577
20912
  references: options.references
20578
20913
  });
20579
20914
  const result = await this.send(await this.context(), body, id);
20580
- const resultId = isObject(result) ? result[resource.idField] : void 0;
20581
- const nextCommands = typeof resultId === "string" && resource.get ? [this.suggest(`${topic} get ${resultId} --json`)] : [];
20582
20915
  return {
20583
20916
  status: "ok",
20584
- data: isObject(result) && nextCommands.length > 0 ? {
20585
- ...result,
20586
- ...nextCommandsData(nextCommands)
20587
- } : result ?? {},
20588
- pretty: JSON.stringify(result, null, 2)
20917
+ data: result ?? {},
20918
+ ...result && resource.get ? { next: { commands: [this.suggest(`${topic} get ${String(resource.idOf(result))} --json`)] } } : {},
20919
+ pretty: JSON.stringify(result, null, 2),
20920
+ machineReadable: true
20589
20921
  };
20590
20922
  }
20591
20923
  };
@@ -20705,26 +21037,49 @@ const update = (schema, call, shape = {}) => ({
20705
21037
  schema,
20706
21038
  call
20707
21039
  });
21040
+ const checked = Symbol("checked");
21041
+ const ranged = Symbol("ranged");
21042
+ /**
21043
+ * A field a `GET` list sends as one parameter per operation (a range, such as
21044
+ * `created_after` and `created_before`), keyed by operation: the operations it
21045
+ * offers are the keys, so each has the parameter it travels as.
21046
+ */
21047
+ const range = (params) => ({
21048
+ operations: Object.keys(params),
21049
+ params,
21050
+ [ranged]: true
21051
+ });
20708
21052
  const list = (spec) => spec;
20709
- /** Filter operations of `POST /<resource>/query` endpoints (ADR 031). */
20710
- const FILTER_OPERATIONS = [
20711
- "equals",
20712
- "not_equals",
20713
- "contains",
20714
- "not_contains",
20715
- "less_than",
20716
- "less_than_or_equal",
20717
- "greater_than",
20718
- "greater_than_or_equal"
20719
- ];
20720
- const EVENT_CATEGORIES = [
20721
- "request",
20722
- "auth",
20723
- "session",
20724
- "admin",
20725
- "entity",
20726
- "signal"
20727
- ];
21053
+ const get = (spec) => spec;
21054
+ const resource = (descriptor) => ({
21055
+ ...descriptor,
21056
+ idOf: (written) => written[descriptor.idField],
21057
+ [checked]: true
21058
+ });
21059
+ const USER_OPERATIONS = Object.values(QueryUsersBodyFilterItemOperation);
21060
+ const TEAM_OPERATIONS = Object.values(QueryTeamsBodyFilterItemOperation);
21061
+ const SESSION_OPERATIONS = Object.values(QuerySessionsBodyFilterItemOperation);
21062
+ const GRANT_OPERATIONS = Object.values(QueryGrantsBodyFilterItemOperation);
21063
+ const IDP_OPERATIONS = Object.values(QueryIdpsBodyFilterItemOperation);
21064
+ const PROJECT_OPERATIONS = Object.values(QueryProjectsBodyFilterItemOperation);
21065
+ /** Every filter operation the API knows, so `--filter` tells one from a value. */
21066
+ const OPERATIONS = [.../* @__PURE__ */ new Set([
21067
+ ...USER_OPERATIONS,
21068
+ ...TEAM_OPERATIONS,
21069
+ ...SESSION_OPERATIONS,
21070
+ ...GRANT_OPERATIONS,
21071
+ ...IDP_OPERATIONS,
21072
+ ...PROJECT_OPERATIONS
21073
+ ])];
21074
+ /** A release's pointers as an indented table, one pinned revision per row. */
21075
+ const releasePointers = (release) => {
21076
+ const { pointers } = release;
21077
+ return ["resources", ...renderTable([
21078
+ "kind",
21079
+ "handle",
21080
+ "revision_id"
21081
+ ], pointers).split("\n").map((line) => ` ${line}`)].join("\n");
21082
+ };
20728
21083
  /** The operations a policy can guard, as the generated publish body names them. */
20729
21084
  const POLICY_OPERATIONS = PublishPolicyBody.shape.operation.options;
20730
21085
  /**
@@ -20735,7 +21090,7 @@ const POLICY_OPERATIONS = PublishPolicyBody.shape.operation.options;
20735
21090
  * truth. Adding a backend resource is adding an entry here.
20736
21091
  */
20737
21092
  const RESOURCES = {
20738
- users: {
21093
+ users: resource({
20739
21094
  group: CommandGroups.usersAndTeams,
20740
21095
  groupOrder: 1,
20741
21096
  singular: "user",
@@ -20757,21 +21112,20 @@ const RESOURCES = {
20757
21112
  "metadata.updated_at"
20758
21113
  ],
20759
21114
  list: list({
20760
- items: "users",
20761
21115
  body: QueryUsersBody,
20762
- response: QueryUsersResponse,
21116
+ item: QueryUsersResponse.shape.users.element,
20763
21117
  filters: [
20764
21118
  {
20765
21119
  field: "created_at",
20766
- operations: FILTER_OPERATIONS
21120
+ operations: USER_OPERATIONS
20767
21121
  },
20768
21122
  {
20769
21123
  field: "id",
20770
- operations: FILTER_OPERATIONS
21124
+ operations: USER_OPERATIONS
20771
21125
  },
20772
21126
  {
20773
21127
  field: "schema",
20774
- operations: FILTER_OPERATIONS
21128
+ operations: USER_OPERATIONS
20775
21129
  },
20776
21130
  {
20777
21131
  field: "schema_object_type",
@@ -20783,15 +21137,15 @@ const RESOURCES = {
20783
21137
  },
20784
21138
  {
20785
21139
  field: "status",
20786
- operations: FILTER_OPERATIONS
21140
+ operations: USER_OPERATIONS
20787
21141
  },
20788
21142
  {
20789
21143
  field: "team_id",
20790
- operations: FILTER_OPERATIONS
21144
+ operations: USER_OPERATIONS
20791
21145
  },
20792
21146
  {
20793
21147
  field: "lifecycle_owner_team_id",
20794
- operations: FILTER_OPERATIONS
21148
+ operations: USER_OPERATIONS
20795
21149
  }
20796
21150
  ],
20797
21151
  sorts: [
@@ -20801,17 +21155,19 @@ const RESOURCES = {
20801
21155
  "status",
20802
21156
  "lifecycle_owner_team_id"
20803
21157
  ],
20804
- call: ({ client }, body) => client.queryUsers(body)
21158
+ call: ({ client }, body) => client.queryUsers(body),
21159
+ items: pagedFunctions.queryUsers.items,
21160
+ next: pagedFunctions.queryUsers.next
20805
21161
  }),
20806
- get: {
21162
+ get: get({
20807
21163
  call: ({ client }, id) => client.getUserByID(id),
20808
21164
  response: GetUserByIDResponse
20809
- },
21165
+ }),
20810
21166
  create: create(CreateUserBody, ({ client, projectId }, body) => client.createUser(body, { project_id: projectId })),
20811
21167
  update: update(PatchUserByIDBody, ({ client }, id, body) => client.patchUserByID(id, body)),
20812
21168
  delete: { call: ({ client }, id) => client.deleteUserByID(id) }
20813
- },
20814
- teams: {
21169
+ }),
21170
+ teams: resource({
20815
21171
  group: CommandGroups.usersAndTeams,
20816
21172
  groupOrder: 2,
20817
21173
  singular: "team",
@@ -20830,21 +21186,20 @@ const RESOURCES = {
20830
21186
  "updated_at"
20831
21187
  ],
20832
21188
  list: list({
20833
- items: "teams",
20834
21189
  body: QueryTeamsBody,
20835
- response: QueryTeamsResponse,
21190
+ item: QueryTeamsResponse.shape.teams.element,
20836
21191
  filters: [
20837
21192
  {
20838
21193
  field: "created_at",
20839
- operations: FILTER_OPERATIONS
21194
+ operations: TEAM_OPERATIONS
20840
21195
  },
20841
21196
  {
20842
21197
  field: "name",
20843
- operations: FILTER_OPERATIONS
21198
+ operations: TEAM_OPERATIONS
20844
21199
  },
20845
21200
  {
20846
21201
  field: "status",
20847
- operations: FILTER_OPERATIONS
21202
+ operations: TEAM_OPERATIONS
20848
21203
  }
20849
21204
  ],
20850
21205
  sorts: [
@@ -20852,20 +21207,22 @@ const RESOURCES = {
20852
21207
  "name",
20853
21208
  "status"
20854
21209
  ],
20855
- call: ({ client, projectId }, body) => client.queryTeams(body, { project_id: projectId })
21210
+ call: ({ client, projectId }, body) => client.queryTeams(body, { project_id: projectId }),
21211
+ items: pagedFunctions.queryTeams.items,
21212
+ next: pagedFunctions.queryTeams.next
20856
21213
  }),
20857
- get: {
21214
+ get: get({
20858
21215
  call: ({ client }, id) => client.getTeam(id),
20859
21216
  response: GetTeamResponse
20860
- },
21217
+ }),
20861
21218
  create: create(CreateTeamBody, ({ client, projectId }, body) => client.createTeam(body, { project_id: projectId })),
20862
21219
  update: update(UpdateTeamBody, ({ client }, id, body) => client.updateTeam(id, body)),
20863
21220
  delete: {
20864
21221
  verb: "deactivate",
20865
21222
  call: ({ client }, id) => client.deleteTeam(id)
20866
21223
  }
20867
- },
20868
- sessions: {
21224
+ }),
21225
+ sessions: resource({
20869
21226
  group: CommandGroups.authentication,
20870
21227
  singular: "session",
20871
21228
  idField: "session_id",
@@ -20885,40 +21242,41 @@ const RESOURCES = {
20885
21242
  "expires_at"
20886
21243
  ],
20887
21244
  list: list({
20888
- items: "sessions",
20889
21245
  body: QuerySessionsBody,
20890
- response: QuerySessionsResponse,
21246
+ item: QuerySessionsResponse.shape.sessions.element,
20891
21247
  filters: [
20892
21248
  {
20893
21249
  field: "created_at",
20894
- operations: FILTER_OPERATIONS
21250
+ operations: SESSION_OPERATIONS
20895
21251
  },
20896
21252
  {
20897
21253
  field: "user_id",
20898
- operations: FILTER_OPERATIONS
21254
+ operations: SESSION_OPERATIONS
20899
21255
  },
20900
21256
  {
20901
21257
  field: "state",
20902
- operations: FILTER_OPERATIONS
21258
+ operations: SESSION_OPERATIONS
20903
21259
  },
20904
21260
  {
20905
21261
  field: "lifecycle_owner_team_id",
20906
- operations: FILTER_OPERATIONS
21262
+ operations: SESSION_OPERATIONS
20907
21263
  }
20908
21264
  ],
20909
21265
  sorts: ["created_at", "user_id"],
20910
- call: ({ client, projectId }, body) => client.querySessions(body, { project_id: projectId })
21266
+ call: ({ client, projectId }, body) => client.querySessions(body, { project_id: projectId }),
21267
+ items: pagedFunctions.querySessions.items,
21268
+ next: pagedFunctions.querySessions.next
20911
21269
  }),
20912
- get: {
21270
+ get: get({
20913
21271
  call: ({ client }, id) => client.getSession(id),
20914
21272
  response: GetSessionResponse
20915
- },
21273
+ }),
20916
21274
  delete: {
20917
21275
  verb: "revoke",
20918
21276
  call: ({ client }, id) => client.revokeSession(id)
20919
21277
  }
20920
- },
20921
- events: {
21278
+ }),
21279
+ events: resource({
20922
21280
  group: CommandGroups.diagnostics,
20923
21281
  singular: "event",
20924
21282
  idField: "id",
@@ -20940,14 +21298,13 @@ const RESOURCES = {
20940
21298
  "request_id",
20941
21299
  "occurred_at"
20942
21300
  ],
20943
- list: {
20944
- items: "data",
20945
- response: ListEventsResponse,
21301
+ list: list({
21302
+ item: ListEventsResponse.shape.data.element,
20946
21303
  filters: [
20947
21304
  {
20948
21305
  field: "category",
20949
21306
  operations: ["equals"],
20950
- values: EVENT_CATEGORIES,
21307
+ values: Object.values(ListEventsCategoryItem),
20951
21308
  combine: "or"
20952
21309
  },
20953
21310
  {
@@ -20985,11 +21342,10 @@ const RESOURCES = {
20985
21342
  },
20986
21343
  {
20987
21344
  field: "created_at",
20988
- operations: ["greater_than_or_equal", "less_than"],
20989
- params: {
21345
+ ...range({
20990
21346
  greater_than_or_equal: "created_after",
20991
21347
  less_than: "created_before"
20992
- }
21348
+ })
20993
21349
  }
20994
21350
  ],
20995
21351
  sorts: ["occurred_at"],
@@ -20997,11 +21353,13 @@ const RESOURCES = {
20997
21353
  call: ({ client, projectId }, params) => client.listEvents({
20998
21354
  project_id: projectId,
20999
21355
  ...params
21000
- })
21001
- },
21002
- get: { call: ({ client, projectId }, id) => client.getEvent(id, { project_id: projectId }) }
21003
- },
21004
- grants: {
21356
+ }),
21357
+ items: pagedFunctions.listEvents.items,
21358
+ next: pagedFunctions.listEvents.next
21359
+ }),
21360
+ get: get({ call: ({ client, projectId }, id) => client.getEvent(id, { project_id: projectId }) })
21361
+ }),
21362
+ grants: resource({
21005
21363
  group: CommandGroups.usersAndTeams,
21006
21364
  groupOrder: 3,
21007
21365
  singular: "grant",
@@ -21024,29 +21382,28 @@ const RESOURCES = {
21024
21382
  "expires_at"
21025
21383
  ],
21026
21384
  list: list({
21027
- items: "grants",
21028
21385
  body: QueryGrantsBody,
21029
- response: QueryGrantsResponse,
21386
+ item: QueryGrantsResponse.shape.grants.element,
21030
21387
  filters: [
21031
21388
  {
21032
21389
  field: "created_at",
21033
- operations: FILTER_OPERATIONS
21390
+ operations: GRANT_OPERATIONS
21034
21391
  },
21035
21392
  {
21036
21393
  field: "user_id",
21037
- operations: FILTER_OPERATIONS
21394
+ operations: GRANT_OPERATIONS
21038
21395
  },
21039
21396
  {
21040
21397
  field: "team_id",
21041
- operations: FILTER_OPERATIONS
21398
+ operations: GRANT_OPERATIONS
21042
21399
  },
21043
21400
  {
21044
21401
  field: "relation",
21045
- operations: FILTER_OPERATIONS
21402
+ operations: GRANT_OPERATIONS
21046
21403
  },
21047
21404
  {
21048
21405
  field: "expires_at",
21049
- operations: FILTER_OPERATIONS
21406
+ operations: GRANT_OPERATIONS
21050
21407
  }
21051
21408
  ],
21052
21409
  sorts: [
@@ -21054,16 +21411,18 @@ const RESOURCES = {
21054
21411
  "expires_at",
21055
21412
  "id"
21056
21413
  ],
21057
- call: ({ client, projectId }, body) => client.queryGrants(body, { project_id: projectId })
21414
+ call: ({ client, projectId }, body) => client.queryGrants(body, { project_id: projectId }),
21415
+ items: pagedFunctions.queryGrants.items,
21416
+ next: pagedFunctions.queryGrants.next
21058
21417
  }),
21059
- get: {
21418
+ get: get({
21060
21419
  call: ({ client, projectId }, id) => client.getGrant(id, { project_id: projectId }),
21061
21420
  response: GetGrantResponse
21062
- },
21421
+ }),
21063
21422
  create: create(CreateGrantBody, ({ client, projectId }, body) => client.createGrant(body, { project_id: projectId }), { example: "--relation viewer --user-identifier alice@acme.com" }),
21064
21423
  delete: { call: ({ client, projectId }, id) => client.deleteGrant(id, { project_id: projectId }) }
21065
- },
21066
- idps: {
21424
+ }),
21425
+ idps: resource({
21067
21426
  group: CommandGroups.authentication,
21068
21427
  singular: "identity provider connection",
21069
21428
  idField: "id",
@@ -21083,29 +21442,30 @@ const RESOURCES = {
21083
21442
  "updated_at"
21084
21443
  ],
21085
21444
  list: list({
21086
- items: "idps",
21087
21445
  body: QueryIdpsBody,
21088
- response: QueryIdpsResponse,
21446
+ item: QueryIdpsResponse.shape.idps.element,
21089
21447
  filters: [{
21090
21448
  field: "slug",
21091
- operations: FILTER_OPERATIONS
21449
+ operations: IDP_OPERATIONS
21092
21450
  }, {
21093
21451
  field: "created_at",
21094
- operations: FILTER_OPERATIONS
21452
+ operations: IDP_OPERATIONS
21095
21453
  }],
21096
21454
  sorts: ["slug", "created_at"],
21097
- call: ({ client, projectId }, body) => client.queryIdps(body, { project_id: projectId })
21455
+ call: ({ client, projectId }, body) => client.queryIdps(body, { project_id: projectId }),
21456
+ items: pagedFunctions.queryIdps.items,
21457
+ next: pagedFunctions.queryIdps.next
21098
21458
  }),
21099
- get: {
21459
+ get: get({
21100
21460
  call: ({ client, projectId }, id) => client.getIdpById(id, { project_id: projectId }),
21101
21461
  response: GetIdpByIdResponse
21102
- },
21462
+ }),
21103
21463
  create: create(CreateIdpBody, ({ client, projectId }, body) => client.createIdp(body, { project_id: projectId }), {
21104
21464
  root: "idp",
21105
21465
  example: "--slug acme-sso --protocol oidc --display-name 'Acme SSO' --oidc-issuer https://sso.acme.example --oidc-client-id <client_id> --oidc-client-secret '${{ ACME_SSO_CLIENT_SECRET }}' --oidc-scopes openid --oidc-scopes profile --oidc-scopes email"
21106
21466
  })
21107
- },
21108
- projects: {
21467
+ }),
21468
+ projects: resource({
21109
21469
  group: CommandGroups.configuration,
21110
21470
  singular: "project",
21111
21471
  idField: "id",
@@ -21122,23 +21482,24 @@ const RESOURCES = {
21122
21482
  "updated_at"
21123
21483
  ],
21124
21484
  list: list({
21125
- items: "projects",
21126
21485
  body: QueryProjectsBody,
21127
- response: QueryProjectsResponse,
21486
+ item: QueryProjectsResponse.shape.projects.element,
21128
21487
  filters: [{
21129
21488
  field: "created_at",
21130
- operations: FILTER_OPERATIONS
21489
+ operations: PROJECT_OPERATIONS
21131
21490
  }],
21132
21491
  sorts: ["created_at"],
21133
- call: ({ client }, body) => client.queryProjects(body)
21492
+ call: ({ client }, body) => client.queryProjects(body),
21493
+ items: pagedFunctions.queryProjects.items,
21494
+ next: pagedFunctions.queryProjects.next
21134
21495
  }),
21135
- get: {
21496
+ get: get({
21136
21497
  call: ({ client }, id) => client.getProject(id),
21137
21498
  response: GetProjectResponse
21138
- },
21499
+ }),
21139
21500
  update: update(PatchProjectBody, ({ client }, id, body) => client.patchProject(id, body))
21140
- },
21141
- schemas: {
21501
+ }),
21502
+ schemas: resource({
21142
21503
  group: CommandGroups.configuration,
21143
21504
  singular: "schema",
21144
21505
  idField: "id",
@@ -21157,9 +21518,8 @@ const RESOURCES = {
21157
21518
  "schema.kind",
21158
21519
  "metadata.created_at"
21159
21520
  ],
21160
- list: {
21161
- items: "schemas",
21162
- response: ListSchemasResponse,
21521
+ list: list({
21522
+ item: ListSchemasResponse.shape.schemas.element,
21163
21523
  filters: [
21164
21524
  {
21165
21525
  field: "object_type",
@@ -21172,16 +21532,18 @@ const RESOURCES = {
21172
21532
  {
21173
21533
  field: "revisions",
21174
21534
  operations: ["equals"],
21175
- values: ["all", "latest"],
21176
- default: "latest"
21535
+ values: Object.values(ListSchemasRevisions),
21536
+ default: ListSchemasRevisions.latest
21177
21537
  }
21178
21538
  ],
21179
21539
  call: ({ client, projectId }, params) => client.listSchemas({
21180
21540
  ...params,
21181
21541
  project_id: projectId
21182
- })
21183
- },
21184
- get: {
21542
+ }),
21543
+ items: pagedFunctions.listSchemas.items,
21544
+ next: pagedFunctions.listSchemas.next
21545
+ }),
21546
+ get: get({
21185
21547
  response: GetSchemaByIdResponse,
21186
21548
  call: async ({ client, projectId }, ref) => {
21187
21549
  try {
@@ -21198,9 +21560,9 @@ const RESOURCES = {
21198
21560
  if (!current) throw new ZitadelError("E_NOT_FOUND", `No schema named "${ref}"`, { hint: "Run `schemas list` to see the object types this project has." });
21199
21561
  return current;
21200
21562
  }
21201
- }
21202
- },
21203
- release: {
21563
+ })
21564
+ }),
21565
+ release: resource({
21204
21566
  group: CommandGroups.configuration,
21205
21567
  singular: "release",
21206
21568
  idField: "id",
@@ -21208,7 +21570,8 @@ const RESOURCES = {
21208
21570
  "id",
21209
21571
  "metadata.message",
21210
21572
  "metadata.git_sha",
21211
- "revoked_at"
21573
+ "revoked_at",
21574
+ "metadata.created_at"
21212
21575
  ],
21213
21576
  heading: "id",
21214
21577
  detail: [
@@ -21220,20 +21583,22 @@ const RESOURCES = {
21220
21583
  "metadata.git_sha",
21221
21584
  "metadata.git_dirty"
21222
21585
  ],
21223
- list: {
21224
- items: "releases",
21225
- response: ListReleasesResponse,
21586
+ list: list({
21587
+ item: ListReleasesResponse.shape.releases.element,
21226
21588
  call: ({ client, projectId }, params) => client.listReleases({
21227
21589
  ...params,
21228
21590
  project_id: projectId
21229
- })
21230
- },
21231
- get: {
21591
+ }),
21592
+ items: pagedFunctions.listReleases.items,
21593
+ next: pagedFunctions.listReleases.next
21594
+ }),
21595
+ get: get({
21232
21596
  call: ({ client, projectId }, id) => client.getReleaseById(id, { project_id: projectId }),
21233
- response: GetReleaseByIdResponse
21234
- }
21235
- },
21236
- "flow-definitions": {
21597
+ response: GetReleaseByIdResponse,
21598
+ sections: (release) => [releasePointers(release)]
21599
+ })
21600
+ }),
21601
+ "flow-definitions": resource({
21237
21602
  group: CommandGroups.configuration,
21238
21603
  singular: "flow definition",
21239
21604
  idField: "id",
@@ -21253,9 +21618,8 @@ const RESOURCES = {
21253
21618
  "created_at",
21254
21619
  "updated_at"
21255
21620
  ],
21256
- list: {
21257
- items: "flow_definitions",
21258
- response: ListFlowDefinitionsResponse,
21621
+ list: list({
21622
+ item: ListFlowDefinitionsResponse.shape.flow_definitions.element,
21259
21623
  filters: [
21260
21624
  {
21261
21625
  field: "name",
@@ -21268,16 +21632,18 @@ const RESOURCES = {
21268
21632
  {
21269
21633
  field: "revisions",
21270
21634
  operations: ["equals"],
21271
- values: ["all", "latest"],
21272
- default: "latest"
21635
+ values: Object.values(ListFlowDefinitionsRevisions),
21636
+ default: ListFlowDefinitionsRevisions.latest
21273
21637
  }
21274
21638
  ],
21275
21639
  call: ({ client, projectId }, params) => client.listFlowDefinitions({
21276
21640
  ...params,
21277
21641
  project_id: projectId
21278
- })
21279
- },
21280
- get: {
21642
+ }),
21643
+ items: pagedFunctions.listFlowDefinitions.items,
21644
+ next: pagedFunctions.listFlowDefinitions.next
21645
+ }),
21646
+ get: get({
21281
21647
  response: GetFlowDefinitionResponse,
21282
21648
  call: async ({ client, projectId }, ref) => {
21283
21649
  if (ref.startsWith("flowdef_")) return client.getFlowDefinition(ref);
@@ -21289,26 +21655,27 @@ const RESOURCES = {
21289
21655
  if (!current) throw new ZitadelError("E_NOT_FOUND", `No flow definition named "${ref}"`, { hint: "Run `flow-definitions list` to see the flows this project has." });
21290
21656
  return current;
21291
21657
  }
21292
- }
21293
- },
21294
- branding: {
21658
+ })
21659
+ }),
21660
+ branding: resource({
21295
21661
  group: CommandGroups.configuration,
21296
21662
  singular: "branding revision",
21297
21663
  idField: "id",
21298
21664
  columns: ["id", "created_at"],
21299
21665
  heading: "id",
21300
21666
  detail: ["id", "created_at"],
21301
- list: {
21667
+ list: list({
21302
21668
  paged: false,
21303
- response: ListBrandingResponse,
21304
- call: ({ client, projectId }) => client.listBranding({ project_id: projectId })
21305
- },
21306
- get: {
21669
+ item: ListBrandingResponse.element,
21670
+ call: ({ client, projectId }) => client.listBranding({ project_id: projectId }),
21671
+ items: (page) => page
21672
+ }),
21673
+ get: get({
21307
21674
  call: ({ client }, id) => client.getBrandingById(id),
21308
21675
  response: GetBrandingByIdResponse
21309
- }
21310
- },
21311
- policies: {
21676
+ })
21677
+ }),
21678
+ policies: resource({
21312
21679
  group: CommandGroups.configuration,
21313
21680
  singular: "policy",
21314
21681
  idField: "id",
@@ -21330,9 +21697,8 @@ const RESOURCES = {
21330
21697
  "created_at",
21331
21698
  "updated_at"
21332
21699
  ],
21333
- list: {
21334
- items: "policies",
21335
- response: ListPoliciesResponse,
21700
+ list: list({
21701
+ item: ListPoliciesResponse.shape.policies.element,
21336
21702
  filters: [{
21337
21703
  field: "operation",
21338
21704
  operations: ["equals"]
@@ -21340,9 +21706,11 @@ const RESOURCES = {
21340
21706
  call: ({ client, projectId }, params) => client.listPolicies({
21341
21707
  ...params,
21342
21708
  project_id: projectId
21343
- })
21344
- },
21345
- get: {
21709
+ }),
21710
+ items: pagedFunctions.listPolicies.items,
21711
+ next: pagedFunctions.listPolicies.next
21712
+ }),
21713
+ get: get({
21346
21714
  response: GetPolicyResponse,
21347
21715
  call: async ({ client, projectId }, ref) => {
21348
21716
  if (ref.startsWith("pol_")) return client.getPolicy(ref, { project_id: projectId });
@@ -21357,8 +21725,8 @@ const RESOURCES = {
21357
21725
  if (!current) throw notFound();
21358
21726
  return current;
21359
21727
  }
21360
- }
21361
- }
21728
+ })
21729
+ })
21362
21730
  };
21363
21731
  /**
21364
21732
  * The `<resource> <verb>` commands, ready for the explicit command table. They
@@ -21366,7 +21734,7 @@ const RESOURCES = {
21366
21734
  * `EnvironmentCommand`), so they take `--env` and `--env-file` too.
21367
21735
  */
21368
21736
  const RESOURCE_COMMANDS = buildResourceCommands(RESOURCES, {
21369
- operations: FILTER_OPERATIONS,
21737
+ operations: OPERATIONS,
21370
21738
  references: {
21371
21739
  matches: isVariableReference,
21372
21740
  example: "'${{ NAME }}'"
@@ -21437,12 +21805,12 @@ var ResourcesList = class ResourcesList extends BaseCommand {
21437
21805
  */
21438
21806
  async function claimProjectAsAdmin(input) {
21439
21807
  const { serverUrl, projectId, projectSecret, admin } = input;
21440
- const { challenge_id } = await createZitadelClient({
21808
+ const { challenge_id } = await createZitadelClient$1({
21441
21809
  baseUrl: serverUrl,
21442
21810
  token: projectSecret
21443
21811
  }).initClaim(projectId, localAdminRequest(serverUrl));
21444
21812
  const cookie = await adminSessionCookie(serverUrl, admin);
21445
- const session = createZitadelClient({ baseUrl: serverUrl });
21813
+ const session = createZitadelClient$1({ baseUrl: serverUrl });
21446
21814
  const { csrf_token } = await session.getMySessionCsrfToken(void 0, localAdminRequest(serverUrl, { cookie }));
21447
21815
  const { team_id, claimed_at } = await session.completeClaim(projectId, { challenge_id }, localAdminRequest(serverUrl, {
21448
21816
  cookie,
@@ -21507,7 +21875,10 @@ async function materializeSetupResources(opts) {
21507
21875
  const filesWritten = [];
21508
21876
  const preset = opts.preset ?? DEFAULT_SETUP_PRESET;
21509
21877
  const useCase = opts.useCase ?? DEFAULT_SETUP_USE_CASE;
21510
- const schemaTemplate = defaultSchemaTemplate(preset, useCase);
21878
+ const schemaTemplate = getDefaultHumanUserSchemaBody({
21879
+ preset,
21880
+ useCase
21881
+ });
21511
21882
  const planned = planConnections(schemaTemplate, opts.sso);
21512
21883
  await refuseResourceConflicts(opts.cwd, opts.force, planned);
21513
21884
  const slugs = planned.map((entry) => entry.slug);
@@ -21516,7 +21887,7 @@ async function materializeSetupResources(opts) {
21516
21887
  await mkdir(join(opts.cwd, IDPS_DIR), { recursive: true });
21517
21888
  if (await writeResourceFile(opts.cwd, connectionPath, connection, false, connectionExists(selection))) filesWritten.push(join(opts.cwd, connectionPath));
21518
21889
  await updateState(opts.cwd, connectionPath, {
21519
- id: requiredString(created.id, "created identity provider connection id"),
21890
+ id: created.id,
21520
21891
  hash: hashForState({ normalize: normalizeIdpBody }, connection)
21521
21892
  });
21522
21893
  }
@@ -21524,7 +21895,7 @@ async function materializeSetupResources(opts) {
21524
21895
  if (await writeResourceFile(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, schemaBody, opts.force)) filesWritten.push(join(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH));
21525
21896
  const schema = await opts.client.createSchema(schemaBody, { project_id: opts.projectId });
21526
21897
  await updateState(opts.cwd, DEFAULT_SCHEMA_CONFIG_PATH, {
21527
- id: requiredString(schema.id, "created schema id"),
21898
+ id: schema.id,
21528
21899
  hash: hashForState({ normalize: normalizeSchemaBody }, schemaBody)
21529
21900
  });
21530
21901
  const flowTemplate = getDefaultLoginFlow({
@@ -21539,7 +21910,7 @@ async function materializeSetupResources(opts) {
21539
21910
  flow_definition: flowBody
21540
21911
  });
21541
21912
  await updateState(opts.cwd, DEFAULT_FLOW_CONFIG_PATH, {
21542
- id: requiredString(flow.id, "created flow definition id"),
21913
+ id: flow.id,
21543
21914
  hash: hashForState({ normalize: normalizeFlowBody }, flowBody),
21544
21915
  name: flowBody.name,
21545
21916
  status: flowBody.status
@@ -21557,7 +21928,7 @@ async function materializeSetupResources(opts) {
21557
21928
  await mkdir(join(opts.cwd, POLICIES_DIR), { recursive: true });
21558
21929
  if ((kept === void 0 || policy !== kept) && await writeResourceFile(opts.cwd, PASSWORD_POLICY_PATH, policy, opts.force)) filesWritten.push(join(opts.cwd, PASSWORD_POLICY_PATH));
21559
21930
  await updateState(opts.cwd, PASSWORD_POLICY_PATH, {
21560
- id: requiredString(published.id, "published policy id"),
21931
+ id: published.id,
21561
21932
  hash: hashForState({ normalize: (body) => toPolicyWireBody(body) }, policy)
21562
21933
  });
21563
21934
  }
@@ -21583,16 +21954,11 @@ async function materializeSetupResources(opts) {
21583
21954
  * only the keys that file is missing.
21584
21955
  */
21585
21956
  async function assertNoSetupResourceConflicts(opts) {
21586
- const schemaTemplate = defaultSchemaTemplate(opts.preset ?? DEFAULT_SETUP_PRESET, opts.useCase ?? DEFAULT_SETUP_USE_CASE);
21587
- await refuseResourceConflicts(opts.cwd, opts.force, planConnections(schemaTemplate, opts.sso));
21588
- }
21589
- /** The default human user schema for a preset and use case, without the template's `$id`. */
21590
- function defaultSchemaTemplate(preset, useCase) {
21591
- const { $id: _templateId, ...schemaTemplate } = getDefaultHumanUserSchema({
21592
- preset,
21593
- useCase
21957
+ const schemaTemplate = getDefaultHumanUserSchemaBody({
21958
+ preset: opts.preset ?? DEFAULT_SETUP_PRESET,
21959
+ useCase: opts.useCase ?? DEFAULT_SETUP_USE_CASE
21594
21960
  });
21595
- return schemaTemplate;
21961
+ await refuseResourceConflicts(opts.cwd, opts.force, planConnections(schemaTemplate, opts.sso));
21596
21962
  }
21597
21963
  /** The connection each chosen provider gets, and the file it is written to. */
21598
21964
  function planConnections(schemaTemplate, sso) {
@@ -21738,10 +22104,6 @@ function resourceExists(relPath, onExists) {
21738
22104
  nextCommands: onExists?.nextCommands
21739
22105
  });
21740
22106
  }
21741
- function requiredString(value, label) {
21742
- if (typeof value === "string" && value.length > 0) return value;
21743
- throw new ZitadelError("E_VALIDATION", `Missing ${label} in server response.`);
21744
- }
21745
22107
  function isErrno(error, code) {
21746
22108
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
21747
22109
  }
@@ -21862,8 +22224,31 @@ function errorShape(error) {
21862
22224
  return { message: String(error) };
21863
22225
  }
21864
22226
  //#endregion
22227
+ //#region src/lib/secret-paths.ts
22228
+ /**
22229
+ * Whether `path` must never be committed: a `.zitadel/secret`, anything under `.zitadel/local/`, or an `.env*` file other than `.env.example`.
22230
+ *
22231
+ * Matched without regard to case, since `.ENV` is the same file as `.env` on
22232
+ * macOS and Windows.
22233
+ */
22234
+ function isSecretPath(path) {
22235
+ const lower = path.toLowerCase();
22236
+ const name = lower.split("/").pop() ?? lower;
22237
+ if (lower === ".zitadel/secret" || lower.endsWith("/.zitadel/secret")) return true;
22238
+ if (isLocalRuntimePath(lower)) return true;
22239
+ return name.startsWith(".env") && name !== ".env.example";
22240
+ }
22241
+ /**
22242
+ * Whether `path` is under `.zitadel/local/`, at any depth, where the local server keeps its admin credential and data.
22243
+ *
22244
+ * Matched with its case, since it names the directory `start` writes; on a
22245
+ * case-sensitive filesystem `.Zitadel/Local/` is someone else's.
22246
+ */
22247
+ function isLocalRuntimePath(path) {
22248
+ return /(^|\/)\.zitadel\/local(\/|$)/.test(path);
22249
+ }
22250
+ //#endregion
21865
22251
  //#region src/commands/setup/initial-commit.ts
21866
- const run = promisify(execFile);
21867
22252
  /**
21868
22253
  * The subject of the commit setup makes. Conventional-commit style, like every
21869
22254
  * other commit this repository's tooling writes, and with no trailer of any
@@ -21871,19 +22256,6 @@ const run = promisify(execFile);
21871
22256
  */
21872
22257
  const INITIAL_COMMIT_SUBJECT = "chore: scaffold the project with zitadel setup";
21873
22258
  /**
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
22259
  * Paths the CLI writes itself, which therefore do not count as the
21888
22260
  * developer's uncommitted work. Matched against a path already made relative
21889
22261
  * to the project directory, never the repository root: in a monorepo the two
@@ -21900,156 +22272,16 @@ function isCliOwned(path) {
21900
22272
  return path === ".gitignore" || path.endsWith("/.gitignore") || isLocalRuntimePath(path);
21901
22273
  }
21902
22274
  /**
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.
22275
+ * Last resort, used only where git can build no identity of its own: a bare
22276
+ * CI container has no configured identity and often no passwd entry or
22277
+ * hostname to guess from, and there git refuses to commit. No commit means no
22278
+ * `git_sha`, and the next deploy has nothing to diff against.
21939
22279
  */
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"
22280
+ const FALLBACK_AUTHOR = {
22281
+ name: "Zitadel Setup",
22282
+ email: "setup@zitadel.local"
22010
22283
  };
22011
22284
  /**
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
22285
  * Refuse a project whose repository has anything uncommitted, before setup
22054
22286
  * writes a single file.
22055
22287
  *
@@ -22060,19 +22292,22 @@ async function stagedPaths(cwd, env) {
22060
22292
  * apart, and committing someone's work-in-progress under setup's name is worse
22061
22293
  * than any diff it would buy. Checked up front rather than at commit time, so
22062
22294
  * a refusal costs nothing and leaves no half-made project behind.
22295
+ *
22296
+ * A repository where a commit would land where it should not (during a
22297
+ * merge, rebase, cherry-pick, revert or bisect, or on a detached HEAD) is
22298
+ * refused here too, for the same reason: found only at commit time, setup
22299
+ * would already have done all its work.
22300
+ *
22301
+ * When git cannot answer, setup refuses rather than carries on: the commit
22302
+ * does not check again, so going ahead would stage and commit the very work
22303
+ * this guard exists to protect.
22063
22304
  */
22064
22305
  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
- }
22306
+ const repository = await Repository.open(cwd);
22307
+ if (repository === null) return;
22308
+ const blocked = await repository.readyToCommit().catch(cannotTell);
22309
+ if (blocked !== null) throw new ZitadelError("E_CONFLICT", `Setup cannot commit here: ${blocked}`, { hint: "Setup commits the project so the first release records a commit. Finish or abort what is under way, or check out a branch, then run setup again, or pass --no-git to set up without touching version control." });
22310
+ const dirty = (await repository.uncommittedFiles().catch(cannotTell)).filter((file) => !isCliOwned(file.path));
22076
22311
  if (dirty.length === 0) return;
22077
22312
  const modified = dirty.filter((change) => !change.untracked).map((change) => change.path);
22078
22313
  const untracked = dirty.filter((change) => change.untracked).map((change) => change.path);
@@ -22085,6 +22320,10 @@ async function assertCleanWorktree(cwd) {
22085
22320
  }
22086
22321
  });
22087
22322
  }
22323
+ /** The refusal for a repository git could not answer about. */
22324
+ function cannotTell(error) {
22325
+ 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." });
22326
+ }
22088
22327
  /** `s` unless the list holds exactly one entry. */
22089
22328
  function plural(list) {
22090
22329
  return list.length === 1 ? "" : "s";
@@ -22102,87 +22341,79 @@ function plural(list) {
22102
22341
  *
22103
22342
  * Nothing here prompts, so it behaves the same interactively and in CI, and no
22104
22343
  * failure is fatal: the worst case is the first release carrying no commit,
22105
- * which is what happened before this existed.
22344
+ * which is what happened before this existed. When the commit fails after
22345
+ * setup ran `git init`, the outcome says so, since the repository now exists.
22106
22346
  */
22107
22347
  async function ensureInitialCommit(cwd) {
22108
- let scratch;
22109
22348
  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 {
22349
+ const existing = await Repository.open(cwd);
22350
+ const outcome = await (existing ?? await Repository.create(cwd)).commitAll(INITIAL_COMMIT_SUBJECT, {
22351
+ except: isSecretPath,
22352
+ authorIfGitHasNone: FALLBACK_AUTHOR
22353
+ }).catch((error) => ({
22354
+ committed: false,
22355
+ reason: firstLineOf(error)
22356
+ }));
22357
+ if (!outcome.committed) return {
22131
22358
  kind: "unavailable",
22132
- reason: `${stillStaged[0]} could not be kept out of the commit, so nothing was committed`
22359
+ reason: outcome.reason,
22360
+ createdRepository: existing === null
22133
22361
  };
22134
- if (paths.length === secrets.length) return {
22135
- kind: "unavailable",
22136
- reason: "there was nothing to commit"
22362
+ const commit = {
22363
+ sha: outcome.sha,
22364
+ guessedIdentity: outcome.author !== "yours",
22365
+ invented: outcome.author === "fallback"
22137
22366
  };
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"
22367
+ if (existing === null) return {
22368
+ kind: "created",
22369
+ ...commit
22155
22370
  };
22156
- await git(cwd, [
22157
- "reset",
22158
- "--quiet",
22159
- "--",
22160
- "."
22161
- ]).catch(() => void 0);
22162
22371
  return {
22163
- kind: existing ? "committed" : "created",
22164
- sha,
22165
- guessedIdentity,
22166
- invented
22372
+ kind: "committed",
22373
+ ...commit
22167
22374
  };
22168
22375
  } catch (error) {
22169
22376
  return {
22170
22377
  kind: "unavailable",
22171
- reason: messageOf(error)
22378
+ reason: firstLineOf(error),
22379
+ createdRepository: false
22172
22380
  };
22173
- } finally {
22174
- if (scratch) await rm(scratch, {
22175
- recursive: true,
22176
- force: true
22177
- });
22178
22381
  }
22179
22382
  }
22180
- /** git's own stderr where it has one, so the warning names the real cause. */
22383
+ /** The one line that says why the commit failed, since it goes into a one-line warning. */
22384
+ function firstLineOf(error) {
22385
+ if (error instanceof RepositoryError) return error.headline();
22386
+ return messageOf(error).split("\n")[0] ?? "";
22387
+ }
22388
+ /** git's own message, so the warning names the real cause. */
22181
22389
  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
22390
  return error instanceof Error ? error.message : String(error);
22185
22391
  }
22392
+ /** What `setup --json` reports under `git`: whether setup ran `git init`, even if the commit then failed, and the commit, or null. */
22393
+ function gitReport(outcome) {
22394
+ if (outcome.kind === "created") return {
22395
+ initialized: true,
22396
+ commit: outcome.sha
22397
+ };
22398
+ if (outcome.kind === "committed") return {
22399
+ initialized: false,
22400
+ commit: outcome.sha
22401
+ };
22402
+ if (outcome.kind === "unavailable") return {
22403
+ initialized: outcome.createdRepository,
22404
+ commit: null
22405
+ };
22406
+ return {
22407
+ initialized: false,
22408
+ commit: null
22409
+ };
22410
+ }
22411
+ /** The warning for a commit that could not be made, saying so when setup left a new, empty repository behind. */
22412
+ function unavailableWarning(outcome) {
22413
+ const warning = `Could not commit the scaffolded project (${outcome.reason || "git gave no reason"}); the first release records no commit, so the next deploy cannot show a diff.`;
22414
+ if (outcome.createdRepository) return `${warning} Setup created a new, empty git repository here.`;
22415
+ return warning;
22416
+ }
22186
22417
  //#endregion
22187
22418
  //#region src/commands/setup/move-dev-port.ts
22188
22419
  /**
@@ -22204,7 +22435,7 @@ async function moveDevPort(opts) {
22204
22435
  const issuer = issuerFromPort(devPort);
22205
22436
  const secret = await readZitadelSecret(cwd);
22206
22437
  const moving = previousIssuer !== issuer;
22207
- const client = createZitadelClient({
22438
+ const client = createZitadelClient$1({
22208
22439
  baseUrl: source,
22209
22440
  token: secret.project_secret
22210
22441
  });
@@ -22943,6 +23174,8 @@ const FRAMEWORK_OPTIONS = createOrca().availableFrameworks().map((framework) =>
22943
23174
  */
22944
23175
  const UNAVAILABLE_RENDERER_IDS = RENDERER_IDS.filter((id) => !AVAILABLE_RENDERER_IDS.includes(id));
22945
23176
  const RENDERER_FLAG_DESCRIPTION = UNAVAILABLE_RENDERER_IDS.length === 0 ? "Renderer (default: react)." : `Renderer (default: react). Not yet available: ${UNAVAILABLE_RENDERER_IDS.join(", ")}.`;
23177
+ /** The message the release setup publishes carries. */
23178
+ const INITIAL_DEPLOY_MESSAGE = "Initial deployment from zitadel setup";
22946
23179
  /**
22947
23180
  * The selector appended to setup's credential-recovery commands. `variable set`
22948
23181
  * is an `EnvironmentCommand` that resolves the project and server from the
@@ -23185,7 +23418,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23185
23418
  force
23186
23419
  });
23187
23420
  consola.start(`Creating project on ${answers.server}${dryRun ? " (dry run)" : ""}`);
23188
- const unauthClient = createZitadelClient({ baseUrl: answers.server });
23421
+ const unauthClient = createZitadelClient$1({ baseUrl: answers.server });
23189
23422
  const projectName = defaultProjectName(cwd, app.framework);
23190
23423
  project = dryRun ? dryRunProject(issuer) : await createProjectWithLocalHint(unauthClient, answers.server, this.meta.cliVersion, projectName, issuer, {
23191
23424
  ...retryOptionsFromFlags(flags),
@@ -23235,7 +23468,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23235
23468
  passwordMinLengthSet: false
23236
23469
  } : await materializeSetupResources({
23237
23470
  cwd,
23238
- client: createZitadelClient({
23471
+ client: createZitadelClient$1({
23239
23472
  baseUrl: answers.server,
23240
23473
  token: project.project_secret
23241
23474
  }, { verbatim: true }),
@@ -23335,6 +23568,26 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23335
23568
  package_manager: installOutcome.install.package_manager
23336
23569
  });
23337
23570
  const commit = await this.makeInitialCommit(cwd, dryRun, flags.git !== false);
23571
+ const deployment = await this.deployScaffoldedProject({
23572
+ cwd,
23573
+ server: answers.server,
23574
+ project,
23575
+ dryRun,
23576
+ selector: recoverySelector(flags.cwd)
23577
+ });
23578
+ const deployCommand = projectCommand(DEPLOY_ARGS, recoverySelector(flags.cwd), this.meta.cliVersion);
23579
+ const deployStep = deployment === void 0 && !dryRun ? {
23580
+ actions: [`Deploy .zitadel/ so the project serves its release: ${deployCommand}`],
23581
+ boxActions: [{
23582
+ text: "Deploy .zitadel/ so the project serves its release:",
23583
+ command: deployCommand
23584
+ }],
23585
+ commands: [deployCommand]
23586
+ } : {
23587
+ actions: [],
23588
+ boxActions: [],
23589
+ commands: []
23590
+ };
23338
23591
  const writtenRel = allFilesWritten.map((file) => relativeDisplayPath(cwd, file));
23339
23592
  let ownedByLocalAdmin;
23340
23593
  const isLocal = serverKind.value(answers.server) === "local";
@@ -23389,6 +23642,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23389
23642
  boxActions: [],
23390
23643
  commands: []
23391
23644
  };
23645
+ let nextShown;
23392
23646
  if (!this.jsonEnabled()) {
23393
23647
  const sections = buildSummary({
23394
23648
  projectFacts: await detectProjectFacts(cwd, app.framework),
@@ -23407,6 +23661,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23407
23661
  renderSummary(sections),
23408
23662
  "",
23409
23663
  renderBoxActions([
23664
+ ...deployStep.boxActions,
23410
23665
  ...installOutcome.boxActions,
23411
23666
  ...consoleStep.boxActions,
23412
23667
  ...claimNudge.boxActions
@@ -23418,10 +23673,12 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23418
23673
  borderColor: "green"
23419
23674
  }
23420
23675
  });
23676
+ nextShown = asStepsShown();
23421
23677
  consola.log(`\n${exploreAllCommands(this.meta.cliVersion)}`);
23422
23678
  await showSkillsHint({
23423
23679
  env: process.env,
23424
- interactive: !dryRun && !flags["non-interactive"] && Boolean(process.stdout.isTTY && process.stdin.isTTY)
23680
+ interactive: !dryRun && !flags["non-interactive"] && Boolean(process.stdout.isTTY && process.stdin.isTTY),
23681
+ cliVersion: this.meta.cliVersion
23425
23682
  });
23426
23683
  }
23427
23684
  return this.emit({
@@ -23443,10 +23700,7 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23443
23700
  })),
23444
23701
  files_skipped: result.filesSkipped.map((file) => relativeDisplayPath(cwd, file)),
23445
23702
  install: installOutcome.install,
23446
- git: {
23447
- initialized: commit.kind === "created",
23448
- commit: commit.kind === "created" || commit.kind === "committed" ? commit.sha : null
23449
- },
23703
+ git: gitReport(commit),
23450
23704
  sso: answers.sso.map((answer) => {
23451
23705
  const slug = providerOf(answer.selection).slug;
23452
23706
  const outcome = ssoOutcomes.get(slug);
@@ -23466,13 +23720,22 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23466
23720
  } : null
23467
23721
  };
23468
23722
  }),
23469
- next_actions: [
23723
+ deployment: deployment === void 0 ? null : {
23724
+ release_id: deployment.releaseId,
23725
+ content_hash: deployment.contentHash,
23726
+ deployment_id: deployment.deploymentId,
23727
+ origins: [...deployment.origins]
23728
+ }
23729
+ },
23730
+ next: {
23731
+ actions: [
23732
+ ...deployStep.actions,
23470
23733
  ...installOutcome.nextActions,
23471
23734
  brandingGuidanceAction(this.meta.cliVersion),
23472
23735
  ...consoleStep.actions,
23473
23736
  ...claimNudge.actions
23474
23737
  ],
23475
- ...nextCommandsData([
23738
+ commands: [
23476
23739
  ...republishCommands(answers.sso.flatMap((answer) => {
23477
23740
  const slug = providerOf(answer.selection).slug;
23478
23741
  const outcome = ssoOutcomes.get(slug);
@@ -23486,11 +23749,13 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23486
23749
  published: outcome?.secret.published
23487
23750
  }];
23488
23751
  }), this.meta.cliVersion, recoverySelector(flags.cwd)),
23752
+ ...deployStep.commands,
23489
23753
  ...installOutcome.nextCommands,
23490
23754
  ...consoleStep.commands,
23491
23755
  ...claimNudge.commands
23492
- ])
23493
- }
23756
+ ]
23757
+ },
23758
+ nextShown
23494
23759
  });
23495
23760
  }
23496
23761
  /**
@@ -23510,10 +23775,43 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23510
23775
  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
23776
  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
23777
  }
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.`);
23778
+ if (outcome.kind === "unavailable") reportWarning(unavailableWarning(outcome));
23514
23779
  return outcome;
23515
23780
  }
23516
23781
  /**
23782
+ * Deploy `.zitadel/` to the project setup just created, to its primary
23783
+ * origin. Narrated but never fatal: the app is scaffolded and committed, and
23784
+ * a failure leaves the deploy one command away.
23785
+ */
23786
+ async deployScaffoldedProject(opts) {
23787
+ if (opts.dryRun) {
23788
+ consola.info("Would deploy .zitadel/ to the project (dry run)");
23789
+ return;
23790
+ }
23791
+ const json = this.jsonEnabled();
23792
+ consola.start("Deploying .zitadel/ to the project");
23793
+ try {
23794
+ const deployment = await deployInitialRelease({
23795
+ client: createZitadelClient$1({
23796
+ baseUrl: opts.server,
23797
+ token: opts.project.project_secret
23798
+ }),
23799
+ projectId: opts.project.id,
23800
+ cwd: opts.cwd,
23801
+ env: this.meta.env,
23802
+ json,
23803
+ color: !json && Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
23804
+ message: INITIAL_DEPLOY_MESSAGE,
23805
+ suggest: (args) => projectCommand(args, opts.selector, this.meta.cliVersion)
23806
+ });
23807
+ consola.success(`Deployed ${deployment.contentHash} to ${deployment.origins.length} origin${deployment.origins.length === 1 ? "" : "s"}`);
23808
+ return deployment;
23809
+ } catch (error) {
23810
+ reportWarning(`The project was set up but its first deploy failed: ${toZitadelError(error).message}. Run \`zitadel ${DEPLOY_ARGS}${opts.selector}\` to publish .zitadel/.`);
23811
+ return;
23812
+ }
23813
+ }
23814
+ /**
23517
23815
  * `setup --dev-port <n>` on an app setup already patched: moves the app to
23518
23816
  * the new port (see {@link moveDevPort}) instead of skipping the directory.
23519
23817
  */
@@ -23542,9 +23840,9 @@ var Setup = class Setup extends Promptable(BaseCommand) {
23542
23840
  previous_issuer: move.previousIssuer,
23543
23841
  issuer: move.issuer,
23544
23842
  dev_port: devPort,
23545
- files_written: move.result.filesWritten.map((file) => relativeDisplayPath(cwd, file)),
23546
- next_actions: [`Restart the dev server and open ${move.issuer}/login.`]
23547
- }
23843
+ files_written: move.result.filesWritten.map((file) => relativeDisplayPath(cwd, file))
23844
+ },
23845
+ next: { actions: [`Restart the dev server and open ${move.issuer}/login.`] }
23548
23846
  });
23549
23847
  }
23550
23848
  };
@@ -23801,7 +24099,7 @@ function retryOptionsFromFlags(flags) {
23801
24099
  */
23802
24100
  function ssoCredentialPublisher(server, project) {
23803
24101
  if (server === "mock") return;
23804
- const client = createZitadelClient({
24102
+ const client = createZitadelClient$1({
23805
24103
  baseUrl: server,
23806
24104
  token: project.project_secret
23807
24105
  });
@@ -24138,6 +24436,99 @@ function buildSummary(opts) {
24138
24436
  ];
24139
24437
  }
24140
24438
  //#endregion
24439
+ //#region src/lib/skills/add.ts
24440
+ /** Where most coding agents read a project's skills: Codex, Cursor, GitHub Copilot, Gemini CLI and more. */
24441
+ const SHARED_SKILLS_DIR = ".agents/skills";
24442
+ /** Where Claude Code reads them, which reads no other folder. */
24443
+ const CLAUDE_SKILLS_DIR = ".claude/skills";
24444
+ /**
24445
+ * The skills this CLI ships, which its build copied into `dist/skills/`
24446
+ * (`./bundle.ts`): the set of the release the user runs.
24447
+ */
24448
+ async function bundledSkills(packageRoot) {
24449
+ return (await readdir(bundleDir(packageRoot), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
24450
+ }
24451
+ /**
24452
+ * The project's folders to add the skills to: the shared one, and Claude
24453
+ * Code's as well when Claude Code is installed on this machine or the project
24454
+ * already has a `.claude/` folder, as one a teammate works on with it does.
24455
+ */
24456
+ async function skillDirs(projectDir) {
24457
+ return existsSync(join(projectDir, ".claude")) || await claudeCodeInstalled() ? [SHARED_SKILLS_DIR, CLAUDE_SKILLS_DIR] : [SHARED_SKILLS_DIR];
24458
+ }
24459
+ /**
24460
+ * Whether Claude Code is installed on this machine. A detection that fails
24461
+ * counts as no: it only decides the extra folder, never whether the skills
24462
+ * are added.
24463
+ */
24464
+ async function claudeCodeInstalled() {
24465
+ try {
24466
+ return (await rosie.agents()).some((agent) => agent.name === "claude" && agent.detected);
24467
+ } catch {
24468
+ return false;
24469
+ }
24470
+ }
24471
+ /**
24472
+ * Puts each skill's bundled folder at `<dir>/<skill>` in the project,
24473
+ * replacing whatever was there: an older version's copy, or the link
24474
+ * `npx skills add` leaves, which is removed rather than followed.
24475
+ */
24476
+ async function addSkills(options) {
24477
+ for (const dir of options.dirs) for (const skill of options.skills) {
24478
+ const target = join(options.projectDir, dir, skill);
24479
+ await rm(target, {
24480
+ recursive: true,
24481
+ force: true
24482
+ });
24483
+ await cp(join(bundleDir(options.packageRoot), skill), target, { recursive: true });
24484
+ }
24485
+ }
24486
+ function bundleDir(packageRoot) {
24487
+ return join(packageRoot, "dist", "skills");
24488
+ }
24489
+ //#endregion
24490
+ //#region src/commands/skills/add.ts
24491
+ /**
24492
+ * `zitadel skills add` — give the project's coding agents the Zitadel skills.
24493
+ *
24494
+ * The skills ship inside the CLI, so the ones added are those of the release
24495
+ * the user runs, which `setup` also pins the app's `@zitadel/*` packages to.
24496
+ * Adding them is this command's whole job, so it asks nothing; running it
24497
+ * again replaces them, which is how they follow a CLI update.
24498
+ */
24499
+ var SkillsAdd = class SkillsAdd extends BaseCommand {
24500
+ static description = "Add the Zitadel agent skills to this project.\n\nAdds the skills of this CLI's version to .agents/skills, which most coding agents read, and to .claude/skills for Claude Code. Run it again after updating the CLI to replace them with the new version's.";
24501
+ static group = CommandGroups.gettingStarted;
24502
+ static groupOrder = 5;
24503
+ static examples = ["<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> --cwd my-app"];
24504
+ async run() {
24505
+ const { flags } = await this.parse(SkillsAdd);
24506
+ await this.toMeta(flags, { resolveServer: false });
24507
+ const skills = await bundledSkills(this.config.root);
24508
+ const dirs = await skillDirs(this.meta.cwd);
24509
+ if (!this.meta.dryRun) await addSkills({
24510
+ packageRoot: this.config.root,
24511
+ projectDir: this.meta.cwd,
24512
+ skills,
24513
+ dirs
24514
+ });
24515
+ return this.emit({
24516
+ status: "ok",
24517
+ data: {
24518
+ title: this.meta.dryRun ? "Zitadel agent skills to add." : "Zitadel agent skills added.",
24519
+ skills,
24520
+ directories: dirs,
24521
+ ...this.meta.dryRun ? { dry_run: true } : {}
24522
+ },
24523
+ pretty: [
24524
+ `${this.meta.dryRun ? "Would add" : "Added"} the ${skills.length} Zitadel agent skills of CLI ${this.meta.cliVersion} to:`,
24525
+ ...dirs.map((dir) => ` ${dir}${dir === ".claude/skills" ? " (Claude Code)" : ""}`),
24526
+ "Run this again after updating the CLI to keep them in step with it."
24527
+ ].join("\n")
24528
+ });
24529
+ }
24530
+ };
24531
+ //#endregion
24141
24532
  //#region src/commands/start.ts
24142
24533
  var Start = class Start extends BaseCommand {
24143
24534
  static description = "Start a local Zitadel server.";
@@ -24186,9 +24577,9 @@ var Start = class Start extends BaseCommand {
24186
24577
  urls: {
24187
24578
  api: serverUrl,
24188
24579
  console: `${serverUrl}/ui/console/`
24189
- },
24190
- ...nextCommandsData([this.suggestUnscoped("start")])
24191
- }
24580
+ }
24581
+ },
24582
+ next: { commands: [this.suggestUnscoped("start")] }
24192
24583
  });
24193
24584
  await ensureLocalState(this.meta.cwd);
24194
24585
  const env = await loadProjectEnv(this.meta.cwd, this.meta.env);
@@ -24213,7 +24604,8 @@ var Start = class Start extends BaseCommand {
24213
24604
  };
24214
24605
  return this.emit({
24215
24606
  status: "ok",
24216
- data: readyData(runtime, alreadyRunning, this.meta.cliVersion, this.suggestUnscoped("setup --server local"))
24607
+ data: readyData(runtime, alreadyRunning),
24608
+ next: readyNext(this.meta.cliVersion, this.suggestUnscoped("setup --server local"))
24217
24609
  });
24218
24610
  }
24219
24611
  };
@@ -24227,7 +24619,7 @@ function platformBootstrapEnabled(env) {
24227
24619
  const pinned = env.NEXTGEN_PLATFORM_PROJECT_ID;
24228
24620
  return !pinned || pinned === "proj_platform";
24229
24621
  }
24230
- function readyData(metadata, alreadyRunning, cliVersion, setupCommand) {
24622
+ function readyData(metadata, alreadyRunning) {
24231
24623
  return {
24232
24624
  title: alreadyRunning ? "Local Zitadel server is already running." : "Local Zitadel server is ready.",
24233
24625
  runtime: {
@@ -24249,13 +24641,17 @@ function readyData(metadata, alreadyRunning, cliVersion, setupCommand) {
24249
24641
  urls: {
24250
24642
  api: metadata.server_url,
24251
24643
  console: `${metadata.server_url}/ui/console/`
24252
- },
24253
- next_actions: [
24644
+ }
24645
+ };
24646
+ }
24647
+ function readyNext(cliVersion, setupCommand) {
24648
+ return {
24649
+ actions: [
24254
24650
  "Create an app with authentication. Run setup in this directory or a subdirectory.",
24255
24651
  "Setup asks for a framework and sign-in method, creates or updates your app, and installs its dependencies.",
24256
24652
  serverLogAction(cliVersion)
24257
24653
  ],
24258
- ...nextCommandsData([setupCommand])
24654
+ commands: [setupCommand]
24259
24655
  };
24260
24656
  }
24261
24657
  function validatePort(port) {
@@ -24316,9 +24712,11 @@ var Status = class Status extends EnvironmentCommand {
24316
24712
  url: server.runtime?.server_url
24317
24713
  }
24318
24714
  },
24319
- project,
24320
- next_actions: nextActions,
24321
- ...nextCommandsData(nextCommands)
24715
+ project
24716
+ },
24717
+ next: {
24718
+ actions: nextActions,
24719
+ commands: nextCommands
24322
24720
  },
24323
24721
  footer: exploreAllCommands(this.meta.cliVersion)
24324
24722
  });
@@ -24433,9 +24831,9 @@ var Stop = class Stop extends BaseCommand {
24433
24831
  } : { container_name: containerName },
24434
24832
  data_preserved: true,
24435
24833
  data_dir: runtime?.data_dir
24436
- },
24437
- ...nextCommandsData([this.suggestUnscoped("stop")])
24438
- }
24834
+ }
24835
+ },
24836
+ next: { commands: [this.suggestUnscoped("stop")] }
24439
24837
  });
24440
24838
  const { stopped, outcome } = await LocalServer.at(this.meta.cwd, { cliVersion: this.meta.cliVersion }).stop();
24441
24839
  return this.emit({
@@ -24620,7 +25018,11 @@ var VariableResolve = class VariableResolve extends EnvironmentCommand {
24620
25018
  await this.toMeta(flags);
24621
25019
  const { client, projectId } = await this.connect();
24622
25020
  const origin = flags.origin;
24623
- const deployments = await listLiveDeployments(client, projectId, origin === void 0 ? {} : { origin });
25021
+ const deployments = await client.listDeployments.all({
25022
+ project_id: projectId,
25023
+ live: true,
25024
+ origin
25025
+ });
24624
25026
  if (origin === void 0 && deployments.length > 1) {
24625
25027
  const origins = [...new Set(deployments.flatMap((deployment) => deployment.targets.map((t) => t.origin)))].sort();
24626
25028
  throw new ZitadelError("E_VALIDATION", "More than one deployment is live; name which origin to resolve with --origin.", {
@@ -24661,7 +25063,7 @@ var VariableResolve = class VariableResolve extends EnvironmentCommand {
24661
25063
  hint: `Deploy first with \`zitadel ${DEPLOY_ARGS}${this.environmentSelector()}\`, then resolve its variables.`,
24662
25064
  nextCommands: [this.suggest(DEPLOY_ARGS)]
24663
25065
  });
24664
- const origins = (await client.getProject(projectId)).origins ?? [];
25066
+ const origins = (await client.getProject(projectId)).origins;
24665
25067
  if (origins.some((entry) => entry.kind === "primary" && entry.pattern === origin)) return new ZitadelError("E_NOT_FOUND", `Nothing is deployed to ${origin}.`, {
24666
25068
  hint: `Deploy to it first with \`zitadel ${DEPLOY_ARGS}${this.environmentSelector()}\`, then resolve its variables.`,
24667
25069
  nextCommands: [this.suggest(DEPLOY_ARGS)]
@@ -24968,6 +25370,7 @@ const COMMANDS = {
24968
25370
  "preview:rm": PreviewRm,
24969
25371
  "release:create": ReleaseCreate,
24970
25372
  "release:revoke": ReleaseRevoke,
25373
+ "skills:add": SkillsAdd,
24971
25374
  "variable:set": VariableSet,
24972
25375
  "variable:get": VariableGet,
24973
25376
  "variable:list": VariableList,