@zitadel/cli 0.2.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.
@@ -1,19 +1,19 @@
1
+ import { Args, Flags } from "@oclif/core";
1
2
  import consola$1, { consola } from "consola";
2
3
  import { ApiError, NetworkError, apiErrorCode, apiErrorMessage } from "@zitadel/api/runtime/fetch";
3
4
  import { createZitadelClient } from "@zitadel/api/client";
4
5
  import { stringify } from "safe-stable-stringify";
5
- import { copyFile, mkdtemp, readFile, readdir, rm } from "node:fs/promises";
6
- import { PublishPolicyBody, publishPolicyBodyOneConfigMinLengthMax } from "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
6
+ import { copyFile, lstat, mkdtemp, readFile, readdir, realpath, rm, stat, utimes, writeFile } from "node:fs/promises";
7
+ import { BuildReleaseBody, PublishPolicyBody, publishPolicyBodyOneConfigMinLengthMax } from "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
7
8
  import { randomUUID } from "node:crypto";
8
9
  import { execFile } from "node:child_process";
9
10
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
11
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
11
12
  import { homedir, release, tmpdir } from "node:os";
12
- import { promisify } from "node:util";
13
13
  import { BlockList, isIP } from "node:net";
14
14
  import pc from "picocolors";
15
15
  import { Agent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
16
- import { brandingConfigSchema, flowConfigSchema, idpConnectionConfigSchema, policyConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
16
+ import { RECOMMENDED_PASSWORD_MIN_LENGTH, brandingConfigSchema, flowConfigSchema, idpConnectionConfigSchema, policyConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
17
17
  import { PURPOSE_FLIP_TARGETS, validateFlowDefinition, validateProject } from "@zitadel/config/validate";
18
18
  import { CUSTOM_PROVIDER, IDP_PROVIDERS, RESERVED_SLUGS, credentialVariables, idpProvider, isLoopbackIssuer, isVariableReference, missingEndpoints, parseConnectionSlug, parseDisplayName, parseIssuer, parseProviderSelection, parseScopes, referencedVariable, selectionName } from "@zitadel/config/idp";
19
19
  import { SETUP_PRESETS, getDefaultLoginFlow } from "@zitadel/config/defaults";
@@ -66,6 +66,12 @@ var ZitadelError = class extends Error {
66
66
  return EXIT_CODES[this.code] ?? 1;
67
67
  }
68
68
  };
69
+ /** What to do about a request that got no usable response, by its reason. */
70
+ const NETWORK_HINTS = {
71
+ unreachable: "Check your connection and the server URL (ZITADEL_URL or --server).",
72
+ timeout: "The server accepted the connection but did not answer in time. Check that it is healthy, then retry.",
73
+ unreadable: "The server answered, but its response could not be read. Check any proxy between you and the server, then retry."
74
+ };
69
75
  /**
70
76
  * Normalises any thrown value into a {@link ZitadelError}. Inspection is
71
77
  * ordered most-specific-first (already-normalised, HTTP status, then
@@ -87,7 +93,7 @@ function toZitadelError(error) {
87
93
  };
88
94
  if (error.status === 404) {
89
95
  if (apiErrorCode(error) === "req.not_found") return new ZitadelError("E_NOT_FOUND", `${error.url} has no such endpoint on this Zitadel server`, {
90
- hint: "The server and this CLI are different versions. Run the CLI release that matches the server, or upgrade the server.",
96
+ hint: "The server and this CLI are different versions; a server from before the API moved under /api/v1 answers every call so. Run the CLI release that matches the server, or upgrade the server.",
91
97
  details
92
98
  });
93
99
  return new ZitadelError("E_NOT_FOUND", !isPlatformErrorEnvelope(error.body) ? `${apiErrorMessage(error)} — ${error.url} has no such endpoint; is this a Zitadel platform API?` : apiErrorMessage(error), { details });
@@ -117,7 +123,7 @@ function toZitadelError(error) {
117
123
  });
118
124
  }
119
125
  if (error instanceof NetworkError) return new ZitadelError("E_NETWORK", error.message, {
120
- hint: error.reason === "timeout" ? "The server accepted the connection but did not answer in time. Check that it is healthy, then retry." : "Check your connection and the server URL (ZITADEL_URL or --server).",
126
+ hint: NETWORK_HINTS[error.reason],
121
127
  details: {
122
128
  url: error.url,
123
129
  reason: error.reason
@@ -354,20 +360,13 @@ function sanitizeRejection(error) {
354
360
  * `timeoutMs`.
355
361
  */
356
362
  function createZitadelClient$1(opts, sanitize = {}) {
357
- const onResolve = sanitize.verbatim ? (value) => value : sanitizeResponse;
358
- const client = createZitadelClient({
363
+ return createZitadelClient({
359
364
  signal: interruptSignal(),
360
365
  timeoutMs: REQUEST_TIMEOUT_MS,
361
- ...opts
366
+ ...opts,
367
+ ...sanitize.verbatim ? {} : { onResponse: sanitizeResponse },
368
+ onError: sanitizeRejection
362
369
  });
363
- return new Proxy(client, { get(target, prop, receiver) {
364
- const value = Reflect.get(target, prop, receiver);
365
- if (typeof value !== "function") return value;
366
- return (...args) => {
367
- const result = value(...args);
368
- return result instanceof Promise ? result.then(onResolve, sanitizeRejection) : result;
369
- };
370
- } });
371
370
  }
372
371
  //#endregion
373
372
  //#region src/lib/json.ts
@@ -562,51 +561,6 @@ function isObject$1(value) {
562
561
  return typeof value === "object" && value !== null && !Array.isArray(value);
563
562
  }
564
563
  //#endregion
565
- //#region src/lib/oclif/crud/paging.ts
566
- /** The list commands' own: `--all`, and `--limit` with `--page-token`. */
567
- const LIST_FLAGS = {
568
- all: "--all",
569
- onePage: "--limit and --page-token"
570
- };
571
- /**
572
- * Walk a cursor-paginated endpoint. `request` performs one page; the
573
- * response must carry the items under `items` and, while more remain, a
574
- * the cursor property `nextPageToken` names. With `all` the pages are drained
575
- * in order and `next`
576
- * is `null`; otherwise one page is fetched and `next` is its token. `flags`
577
- * names the caller's own flags in the errors.
578
- */
579
- const collectPages = async (request, { items, all, token, nextPageToken, flags = LIST_FLAGS }) => {
580
- const collected = [];
581
- const seen = /* @__PURE__ */ new Set();
582
- let cursor = token;
583
- for (;;) {
584
- const response = await request(cursor);
585
- const page = items === void 0 ? response : isObject$1(response) ? response[items] : void 0;
586
- if (!Array.isArray(page)) throw new ZitadelError("E_VALIDATION", items === void 0 ? "Unexpected list response: expected an array" : `Unexpected list response: missing "${items}" array`, { details: { response } });
587
- collected.push(...page);
588
- const cursor_ = isObject$1(response) ? response[nextPageToken] : void 0;
589
- const next = typeof cursor_ === "string" ? cursor_ || null : null;
590
- if (!all) return {
591
- items: collected,
592
- next
593
- };
594
- if (!next) return {
595
- items: collected,
596
- next: null
597
- };
598
- if (seen.has(next)) throw new ZitadelError("E_VALIDATION", `The server repeated a page cursor, so ${flags.all} would never finish`, {
599
- hint: `Fetch one page at a time with ${flags.onePage}, and report the repeated cursor.`,
600
- details: {
601
- cursor: next,
602
- pages_fetched: seen.size + 1
603
- }
604
- });
605
- seen.add(next);
606
- cursor = next;
607
- }
608
- };
609
- //#endregion
610
564
  //#region src/lib/public-cli.ts
611
565
  const CLI_PACKAGE_NAME = "@zitadel/cli";
612
566
  function npmDistTagForCliVersion(cliVersion) {
@@ -775,6 +729,41 @@ function inShell(command, shell) {
775
729
  return `${shell.before ?? ""}${command}${shell.after ?? ""}`;
776
730
  }
777
731
  //#endregion
732
+ //#region src/lib/oclif/flags.ts
733
+ /**
734
+ * Refuse a value that is present but holds nothing.
735
+ *
736
+ * `required: true` is no help: it asks whether a token arrived in argv, and an
737
+ * empty one satisfies it. The way this happens is the shell, not a typo —
738
+ * `--client-id "$CLIENT_ID"` with the variable unset sends exactly that, the
739
+ * expansion dropping the value while the quotes keep the word, so argv carries
740
+ * `["--client-id", ""]`. Unquoted, the word vanishes and oclif refuses with
741
+ * "expects a value"; quoted, nothing complained and the empty string travelled
742
+ * on as if it were a real answer.
743
+ *
744
+ * Refusing during parse rather than in each command puts it at the same point
745
+ * oclif's own refusal lands, so both spellings of a missing value fail the same
746
+ * way, and the value that reaches `run` is already trimmed.
747
+ */
748
+ function refuseBlank(input, label, message) {
749
+ const value = input.trim();
750
+ if (value === "") throw new ZitadelError("E_VALIDATION", message, {
751
+ hint: `Pass a value for ${label}, or omit it entirely.`,
752
+ details: { argument: label }
753
+ });
754
+ return value;
755
+ }
756
+ /**
757
+ * A string flag whose value must not be blank. See {@link refuseBlank}.
758
+ *
759
+ * Not for a flag that writes a field value: an empty string is a legitimate
760
+ * thing to store, so the generated resource flags in `crud/fields.ts` keep
761
+ * plain `Flags.string`.
762
+ */
763
+ const nonBlankString = Flags.custom({ parse: async (input, _command, flag) => refuseBlank(input, `--${flag.name}`, "the value is empty") });
764
+ /** A positional argument whose value must not be blank. See {@link refuseBlank}. */
765
+ const nonBlankArg = Args.custom({ parse: async (input, _command, arg) => refuseBlank(input, arg.name, `${arg.name} was given an empty value`) });
766
+ //#endregion
778
767
  //#region src/lib/telemetry/dimensions/env-flag.ts
779
768
  const DISABLED_VALUES$1 = /* @__PURE__ */ new Set([
780
769
  "",
@@ -1111,6 +1100,69 @@ function takeReportedWarnings() {
1111
1100
  return reported.splice(0);
1112
1101
  }
1113
1102
  //#endregion
1103
+ //#region src/lib/oclif/crud/table.ts
1104
+ /**
1105
+ * The machine form of a list: one record per line, fields separated by tabs,
1106
+ * no header and no padding, so `cut -f2` and `awk -F'\t'` work. This is what a
1107
+ * piped run emits — the aligned table is for a terminal, where nothing is
1108
+ * parsing it. Tabs and newlines inside a value would break the record, so they
1109
+ * are escaped rather than passed through.
1110
+ */
1111
+ const renderRows = (columns, items) => items.map((item) => columns.map((column) => readCell(column, item).replaceAll(" ", "\\t").replaceAll("\n", "\\n")).join(" ")).join("\n");
1112
+ /**
1113
+ * Render `items` as an aligned text table, one column per dot-path in
1114
+ * `columns`, headed by each path's last segment. Nested paths resolve
1115
+ * through objects; missing values render empty; non-strings render as JSON.
1116
+ */
1117
+ const renderTable = (columns, items) => {
1118
+ const headers = columns.map((column) => column.split(".").at(-1) ?? column);
1119
+ const rows = items.map((item) => columns.map((column) => readCell(column, item)));
1120
+ const widths = rows.reduce((current, row) => current.map((width, i) => Math.max(width, row[i]?.length ?? 0)), headers.map((header) => header.length));
1121
+ const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
1122
+ return [
1123
+ line(headers),
1124
+ line(widths.map((width) => "-".repeat(width))),
1125
+ ...rows.map(line)
1126
+ ].join("\n");
1127
+ };
1128
+ /**
1129
+ * The whole terminal rendering of a list, so no command re-derives the
1130
+ * plain/TTY/empty/footer logic (and so none forgets `--plain`): a pipe (or
1131
+ * `--plain`) gets tab-separated rows with no header and nothing when empty, so
1132
+ * `cut`/`awk`/`wc -l` work; a terminal gets the aligned table, a blank line, a
1133
+ * `${n} ${singular|plural}` count, and any `footer` lines. Pair it with
1134
+ * `listData()` for the JSON envelope.
1135
+ */
1136
+ const renderList = (columns, rows, opts) => {
1137
+ if (opts.plain) return renderRows(columns, rows);
1138
+ if (rows.length === 0) return opts.empty;
1139
+ const count = `${rows.length} ${rows.length === 1 ? opts.singular : opts.plural ?? `${opts.singular}s`}`;
1140
+ return [
1141
+ renderTable(columns, rows),
1142
+ "",
1143
+ count,
1144
+ ...opts.footer ?? []
1145
+ ].join("\n");
1146
+ };
1147
+ /** One cell: a dot-path resolved through objects, rendered as text. */
1148
+ const readCell = (column, item) => {
1149
+ const value = column.split(".").reduce((current, key) => isObject$1(current) ? current[key] : void 0, item);
1150
+ return value == null ? "" : typeof value === "string" ? value : JSON.stringify(value);
1151
+ };
1152
+ /**
1153
+ * One record for a person: the heading first, then a labelled field per line.
1154
+ * Nested paths are labelled by their last segment, so `metadata.status` reads
1155
+ * as `status`. It is the terminal rendering of `get`; a pipe (and `--json`)
1156
+ * still receives the whole object, since a script wants the record, not a view
1157
+ * of it.
1158
+ */
1159
+ const renderDetail = (heading, fields, item) => {
1160
+ const rows = fields.map((field) => [field.split(".").at(-1) ?? field, readCell(field, item)]).filter(([, value]) => value !== "");
1161
+ const width = Math.max(0, ...rows.map(([label]) => label.length));
1162
+ const body = rows.map(([label, value]) => `${label.padEnd(width)} ${value}`);
1163
+ return [...heading ? [heading, ""] : [], ...body].join("\n");
1164
+ };
1165
+ //#endregion
1114
1166
  //#region src/lib/variables.ts
1115
1167
  /** The scope a variable write or delete addresses, from the project and `--preview`. */
1116
1168
  function variableScope(projectId, preview) {
@@ -1158,6 +1210,14 @@ function toVariableRows(values) {
1158
1210
  }
1159
1211
  /** Stands in for a secret's value, which the platform never discloses. */
1160
1212
  const SECRET_MARKER = "(secret)";
1213
+ /**
1214
+ * The variables a deployment froze, as `deployment get` shows them: the
1215
+ * `variable resolve` table, indented under a heading.
1216
+ */
1217
+ function frozenVariables(rows) {
1218
+ if (rows.length === 0) return " variables none";
1219
+ return [" variables", ...renderTable(["name", "value"], variableCells(rows)).split("\n").map((line) => ` ${line}`)].join("\n");
1220
+ }
1161
1221
  /** Stands in for a kind the name holds no value for at all. */
1162
1222
  const NO_VALUE = "-";
1163
1223
  /**
@@ -2004,24 +2064,29 @@ function stepNamed(flow, name) {
2004
2064
  * replacing it, so enabling a second one later keeps the first.
2005
2065
  */
2006
2066
  function applySsoToSchema(schema, slug) {
2007
- const document = clone(schema);
2008
- const methods = isObject(document["x-auth-methods"]) ? { ...document["x-auth-methods"] } : {};
2067
+ const copy = clone(schema);
2068
+ const current = "x-auth-methods" in copy ? copy["x-auth-methods"] : void 0;
2069
+ const methods = isObject(current) ? current : {};
2009
2070
  const existing = isObject(methods.sso) ? methods.sso : void 0;
2010
- const providers = Array.isArray(existing?.providers) ? [...existing.providers] : [];
2071
+ const providers = Array.isArray(existing?.providers) ? existing.providers : [];
2011
2072
  if (existing?.enabled === true && providers.includes(slug)) return {
2012
2073
  document: schema,
2013
2074
  changed: false,
2014
2075
  skipped: []
2015
2076
  };
2016
2077
  if (!providers.includes(slug)) providers.push(slug);
2017
- methods.sso = {
2018
- ...existing,
2019
- enabled: true,
2020
- providers
2021
- };
2022
- document["x-auth-methods"] = methods;
2023
2078
  return {
2024
- document,
2079
+ document: {
2080
+ ...copy,
2081
+ "x-auth-methods": {
2082
+ ...methods,
2083
+ sso: {
2084
+ ...existing,
2085
+ enabled: true,
2086
+ providers
2087
+ }
2088
+ }
2089
+ },
2025
2090
  changed: true,
2026
2091
  skipped: []
2027
2092
  };
@@ -3232,9 +3297,11 @@ async function readJsonDir(absDir) {
3232
3297
  }
3233
3298
  /**
3234
3299
  * {@link readJsonDir}, keeping each body's file name — for callers that report
3235
- * a problem against the file it came from.
3300
+ * a problem against the file it came from. `skip` leaves out files by name
3301
+ * before they are read, so a file the caller does not own (a policy test file
3302
+ * beside the policies) can never fail it.
3236
3303
  */
3237
- async function readJsonDirEntries(absDir) {
3304
+ async function readJsonDirEntries(absDir, { skip } = {}) {
3238
3305
  let entries;
3239
3306
  try {
3240
3307
  entries = await readdir(absDir);
@@ -3243,7 +3310,7 @@ async function readJsonDirEntries(absDir) {
3243
3310
  throw error;
3244
3311
  }
3245
3312
  const result = [];
3246
- for (const entry of entries.filter((name) => name.endsWith(".json")).sort()) {
3313
+ for (const entry of entries.filter((name) => name.endsWith(".json") && !skip?.(name)).sort()) {
3247
3314
  const abs = join(absDir, entry);
3248
3315
  let raw;
3249
3316
  try {
@@ -3288,6 +3355,15 @@ function toPolicyWireBody(file) {
3288
3355
  config: file.config
3289
3356
  };
3290
3357
  }
3358
+ /**
3359
+ * The suffix of a policy test file (`<operation>.test.json`), which sits
3360
+ * beside the policies but is never deployed: `zitadel policies test` runs it.
3361
+ */
3362
+ const POLICY_TEST_SUFFIX = ".test.json";
3363
+ /** Whether a file in `.zitadel/policies/` is a test file rather than a policy. */
3364
+ function isPolicyTestFile(name) {
3365
+ return name.endsWith(POLICY_TEST_SUFFIX);
3366
+ }
3291
3367
  /** The name a policy file must have: its operation, so one file holds one operation's policy. */
3292
3368
  function policyFileName(operation) {
3293
3369
  return `${operation}.json`;
@@ -3317,7 +3393,7 @@ const PASSWORD_POLICY_PATH = `${POLICIES_DIR}/${policyFileName(PASSWORD_POLICY_O
3317
3393
  * empty `config`, which enforces nothing, for an explicit "none".
3318
3394
  */
3319
3395
  function passwordPolicyFile(minLength) {
3320
- const value = minLength === void 0 ? 15 : minLength;
3396
+ const value = minLength === void 0 ? RECOMMENDED_PASSWORD_MIN_LENGTH : minLength;
3321
3397
  return {
3322
3398
  $schema: POLICY_FILE_SCHEMA_REF,
3323
3399
  operation: PASSWORD_POLICY_OPERATION,
@@ -3367,7 +3443,7 @@ async function buildBundle(cwd) {
3367
3443
  const flow_definitions = (await readJsonDir(join(cwd, FLOWS_DIR))).map(stripSchema);
3368
3444
  const idpEntries = await readJsonDirEntries(join(cwd, IDPS_DIR));
3369
3445
  const idps = idpEntries.map((entry) => stripSchema(entry.body));
3370
- const policyEntries = await readJsonDirEntries(join(cwd, POLICIES_DIR));
3446
+ const policyEntries = await readJsonDirEntries(join(cwd, POLICIES_DIR), { skip: isPolicyTestFile });
3371
3447
  const policies = policyEntries.map((entry) => stripSchema(entry.body));
3372
3448
  const brandingSources = (await readJsonDir(join(cwd, BRANDING_DIR))).map((descriptor) => ({ ...descriptor }));
3373
3449
  if (brandingSources.length > 1) throw new ZitadelError("E_VALIDATION", `A release carries at most one branding, but .zitadel/branding/ holds ${brandingSources.length}`, { hint: "A project has a single branding; keep one descriptor under .zitadel/branding/." });
@@ -3391,31 +3467,44 @@ async function buildBundle(cwd) {
3391
3467
  };
3392
3468
  }
3393
3469
  //#endregion
3394
- //#region src/lib/release/deployment.ts
3470
+ //#region src/lib/release/release.ts
3395
3471
  /**
3396
- * Every live deployment of the project, newest first, drained across pages: one
3397
- * live preview per open pull request can push the primary origins' deployment
3398
- * off the first page, and a guard, a baseline or a rollback read from that page
3399
- * alone would quietly miss it.
3472
+ * Build a release from the `.zitadel/` bundle (`POST /releases/build`). The
3473
+ * server reuses the revisions that already match and allocates the rest, and
3474
+ * dedupes on content: re-posting the same bundle returns the release that
3475
+ * already carries it. (`POST /releases` pins pre-existing revision ids and is
3476
+ * not a CLI flow.)
3400
3477
  */
3401
- async function listLiveDeployments(client, projectId, options = {}) {
3402
- const { items } = await collectPages((token) => client.listDeployments({
3403
- project_id: projectId,
3404
- live: true,
3405
- ...options.origin === void 0 ? {} : { origin: options.origin },
3406
- ...options.withRelease ? { expand: ["release"] } : {},
3407
- ...token === void 0 ? {} : { page_token: token }
3408
- }), {
3409
- items: "deployments",
3410
- all: true,
3411
- nextPageToken: "next_page_token"
3412
- });
3413
- return items;
3478
+ async function createReleaseFromBundle(client, projectId, bundle, source) {
3479
+ const body = {
3480
+ bundle,
3481
+ ...source.message === void 0 ? {} : { message: source.message },
3482
+ ...source.gitSha === null ? {} : { git_sha: source.gitSha },
3483
+ git_dirty: source.gitDirty
3484
+ };
3485
+ assertBuildReleaseBody(body);
3486
+ return client.buildRelease(body, { project_id: projectId });
3487
+ }
3488
+ /** Refuses a body `POST /releases/build` does not take, naming each field it refuses. */
3489
+ function assertBuildReleaseBody(body) {
3490
+ const checked = BuildReleaseBody.safeParse(body);
3491
+ if (!checked.success) {
3492
+ const issues = checked.error.issues;
3493
+ const noun = issues.length === 1 ? "issue" : "issues";
3494
+ throw new ZitadelError("E_VALIDATION", `The release body does not match what the API takes, so nothing was sent (${issues.length} ${noun}):\n` + issues.map((issue) => ` - ${issue.path.join(".") || "(root)"} ${issue.message}`).join("\n"), { details: { issues } });
3495
+ }
3496
+ }
3497
+ /** Revoke a release so its content is never served again. Idempotent. */
3498
+ async function revokeRelease(client, projectId, releaseId) {
3499
+ return client.revokeRelease(releaseId, { project_id: projectId });
3414
3500
  }
3501
+ //#endregion
3502
+ //#region src/lib/release/deployment.ts
3415
3503
  /**
3416
3504
  * Whether a deployment serves a primary origin. A preview deployment's targets
3417
3505
  * carry an expiry; it is never the baseline, the guard or the thing a bare
3418
- * rollback undoes.
3506
+ * rollback undoes. Look for it across every page (`listDeployments.all`): one
3507
+ * live preview per open pull request can push it off the first.
3419
3508
  */
3420
3509
  function servesPrimary(deployment) {
3421
3510
  return deployment.targets.some((target) => target.expires_at == null);
@@ -3450,8 +3539,1148 @@ async function rollbackDeployment(client, projectId, deploymentId, options) {
3450
3539
  return client.rollbackDeployment(deploymentId, { project_id: projectId }, Object.keys(body).length === 0 ? void 0 : body);
3451
3540
  }
3452
3541
  //#endregion
3542
+ //#region src/lib/git/errors.ts
3543
+ /**
3544
+ * A git operation that failed, with a message ready to show the developer.
3545
+ *
3546
+ * What the module throws when git fails. The message is git's own (all of
3547
+ * its stderr, which often says how to fix the failure), or one of the
3548
+ * module's when git said nothing useful: output over 16 MB, a call that timed
3549
+ * out. The temporary staging area's file-system calls can still fail with
3550
+ * Node's own errors. Whether a failure is fatal is the caller's choice.
3551
+ */
3552
+ var RepositoryError = class extends Error {
3553
+ /** Creates the error from its message. */
3554
+ constructor(message) {
3555
+ super(message);
3556
+ this.name = "RepositoryError";
3557
+ }
3558
+ /**
3559
+ * The one line of the message that says why git failed, for a one-line warning; never empty.
3560
+ *
3561
+ * git writes `warning:` and `hint:` lines before and after the line that
3562
+ * gives the reason, so the first `fatal:` or `error:` line is taken, then
3563
+ * the first line, then a fixed phrase when git said nothing.
3564
+ */
3565
+ headline() {
3566
+ const lines = this.message.split("\n").map((line) => line.trim()).filter((line) => line !== "");
3567
+ const reason = lines.find((line) => line.startsWith("fatal:") || line.startsWith("error:"));
3568
+ if (reason !== void 0) return reason;
3569
+ return lines[0] ?? "git gave no reason";
3570
+ }
3571
+ };
3572
+ /**
3573
+ * git could not be started: there is none on `PATH`, or the directory to run it in does not exist.
3574
+ *
3575
+ * Its own type because callers treat it differently from git failing:
3576
+ * {@link Repository.open} answers "no repository" for it, where any other
3577
+ * failure is thrown.
3578
+ */
3579
+ var GitDidNotStartError = class extends RepositoryError {
3580
+ /** Creates the error from the message Node gave. */
3581
+ constructor(message) {
3582
+ super(message);
3583
+ this.name = "GitDidNotStartError";
3584
+ }
3585
+ };
3586
+ //#endregion
3587
+ //#region src/lib/git/git.ts
3588
+ /**
3589
+ * What one git call returned, and ways to read it.
3590
+ *
3591
+ * A call returns one only when it succeeded or exited with a code the caller
3592
+ * allowed; anything else is thrown by {@link Git.run}. Reading lives here so
3593
+ * every caller reads git's output the same way: `paths()` for `-z` lists,
3594
+ * `text()` for one value, `reason()` for why it failed.
3595
+ */
3596
+ var GitResult = class {
3597
+ exitCode;
3598
+ stdout;
3599
+ stderr;
3600
+ /** Wraps what one call returned. */
3601
+ constructor(exitCode, stdout, stderr) {
3602
+ this.exitCode = exitCode;
3603
+ this.stdout = stdout;
3604
+ this.stderr = stderr;
3605
+ }
3606
+ /** Whether git exited 0. */
3607
+ succeeded() {
3608
+ return this.exitCode === 0;
3609
+ }
3610
+ /** What git printed, without its final line break. */
3611
+ text() {
3612
+ return this.stdout.replace(/\n$/, "");
3613
+ }
3614
+ /** The NUL-separated paths git printed, for a `-z` command. */
3615
+ paths() {
3616
+ return this.stdout.split("\0").filter((path) => path !== "");
3617
+ }
3618
+ /**
3619
+ * Everything git wrote to stderr, its own reason for failing; empty when it wrote none.
3620
+ *
3621
+ * All of it, not the first line: git's later lines often say how to fix the
3622
+ * failure, as a refusal over ownership names the `safe.directory` command.
3623
+ */
3624
+ reason() {
3625
+ return this.stderr.trim();
3626
+ }
3627
+ };
3628
+ /**
3629
+ * The git command line, run in one directory with one environment.
3630
+ *
3631
+ * The one place the CLI starts git. Every call goes through {@link run},
3632
+ * which makes it safe the same way (see there), so no caller can forget a
3633
+ * setting. It knows nothing about repositories or Zitadel: it runs git and
3634
+ * reads its answer. `with` gives the same git with more environment, which is
3635
+ * how a temporary staging area, a fallback author or an English locale is
3636
+ * applied to one call.
3637
+ */
3638
+ var Git = class Git {
3639
+ /** The directory git runs in. */
3640
+ dir;
3641
+ env;
3642
+ /** Runs git in `dir`, with `env` added to its environment. */
3643
+ constructor(dir, env = {}) {
3644
+ this.dir = dir;
3645
+ this.env = env;
3646
+ }
3647
+ /** The same git, with `env` added to its environment. */
3648
+ with(env) {
3649
+ return new Git(this.dir, {
3650
+ ...this.env,
3651
+ ...env
3652
+ });
3653
+ }
3654
+ /**
3655
+ * Runs git with `args`; throws a {@link RepositoryError} for an exit code other than 0 or `allow`.
3656
+ *
3657
+ * Every call is made safe the same way:
3658
+ * - `GIT_LITERAL_PATHSPECS`, `GIT_GLOB_PATHSPECS`, `GIT_NOGLOB_PATHSPECS`
3659
+ * and `GIT_ICASE_PATHSPECS` are dropped from the inherited environment.
3660
+ * They change what every path given to git matches, and the literal one
3661
+ * turns off the `:(exclude)` a commit uses to keep a secret out.
3662
+ * - `GIT_TERMINAL_PROMPT=0`, so git never waits on a prompt.
3663
+ * - Output is capped at 16 MB and a call at five minutes, so a stuck or
3664
+ * runaway git cannot hang the CLI.
3665
+ *
3666
+ * `GIT_DIR`, `GIT_WORK_TREE` and `GIT_INDEX_FILE` are honoured, as git
3667
+ * itself does: a build that keeps `.git` elsewhere names it this way. No
3668
+ * flag is set that git passes on to hooks, so a hook that runs during a
3669
+ * commit sees the developer's own settings: a path is made literal where it
3670
+ * is given ({@link Git.literal}), and the locale is set to English only for
3671
+ * the one call whose wording is read.
3672
+ *
3673
+ * Output is read as UTF-8 unless `encoding` says otherwise; `latin1` keeps
3674
+ * every byte as one character, for a path list that must go back to git
3675
+ * unchanged.
3676
+ *
3677
+ * An allowed exit code is an answer, not a failure. Only a git that ran and
3678
+ * exited has one, so no allow list can mistake a missing git for an answer;
3679
+ * that throws {@link GitDidNotStartError}. Every other failure carries git's
3680
+ * own message, or Node's when git wrote nothing.
3681
+ */
3682
+ run(args, allow = [], encoding = "utf8") {
3683
+ const inherited = { ...process.env };
3684
+ for (const name of [
3685
+ "GIT_LITERAL_PATHSPECS",
3686
+ "GIT_GLOB_PATHSPECS",
3687
+ "GIT_NOGLOB_PATHSPECS",
3688
+ "GIT_ICASE_PATHSPECS"
3689
+ ]) delete inherited[name];
3690
+ const env = {
3691
+ ...inherited,
3692
+ GIT_TERMINAL_PROMPT: "0",
3693
+ ...this.env
3694
+ };
3695
+ return new Promise((resolveResult, reject) => {
3696
+ execFile("git", [...args], {
3697
+ cwd: this.dir,
3698
+ env,
3699
+ encoding,
3700
+ maxBuffer: 16777216,
3701
+ timeout: 3e5
3702
+ }, (error, stdout, stderr) => {
3703
+ const failure = error;
3704
+ if (failure === null) {
3705
+ resolveResult(new GitResult(0, stdout, stderr));
3706
+ return;
3707
+ }
3708
+ if (typeof failure.code === "number") {
3709
+ const result = new GitResult(failure.code, stdout, stderr);
3710
+ if (allow.includes(failure.code)) resolveResult(result);
3711
+ else reject(new RepositoryError(result.reason() || failure.message));
3712
+ } else if (failure.code === "ENOENT") reject(new GitDidNotStartError(failure.message));
3713
+ else if (failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") reject(new RepositoryError("git printed more than 16 MB"));
3714
+ else if (failure.killed) reject(new RepositoryError("git did not finish in time"));
3715
+ else reject(new RepositoryError(stderr.trim() || failure.message));
3716
+ });
3717
+ });
3718
+ }
3719
+ /**
3720
+ * Every file staged under the directory, relative to it.
3721
+ *
3722
+ * `--relative` both limits the list to the directory and names each path
3723
+ * from it, as a path filter expects. `--no-renames` lists both ends of a
3724
+ * rename, so neither escapes a check.
3725
+ */
3726
+ async stagedFiles() {
3727
+ return (await this.run([
3728
+ "diff",
3729
+ "--cached",
3730
+ "-z",
3731
+ "--name-only",
3732
+ "--no-renames",
3733
+ "--relative"
3734
+ ])).paths();
3735
+ }
3736
+ /**
3737
+ * `path` as a pathspec that matches only itself, so `app/[id].tsx` does not also match `app/i.tsx`.
3738
+ *
3739
+ * Set per path rather than with `--literal-pathspecs`, which git would pass
3740
+ * on to every hook it runs as `GIT_LITERAL_PATHSPECS`.
3741
+ */
3742
+ static literal(path) {
3743
+ return `:(literal)${path}`;
3744
+ }
3745
+ /** Whether `path`, relative to the directory, names something that exists. */
3746
+ exists(path) {
3747
+ return path !== "" && existsSync(resolve(this.dir, path));
3748
+ }
3749
+ };
3750
+ //#endregion
3751
+ //#region src/lib/git/author-identity.ts
3752
+ /**
3753
+ * Where the author's name on a commit would come from.
3754
+ *
3755
+ * Three cases: the developer has an identity of their own (config or
3756
+ * `GIT_AUTHOR_*`), git can only guess one from the username and hostname, or
3757
+ * git cannot build one at all, as on a bare CI runner. Only the last needs an
3758
+ * author supplied, since an invented author is worse than git's own guess;
3759
+ * the caller is told which it was, so it can say so. Applying an author to a
3760
+ * commit is {@link StagingArea.commit}'s job.
3761
+ */
3762
+ var AuthorIdentity = class AuthorIdentity {
3763
+ /** Where the author's name comes from. */
3764
+ source;
3765
+ /** The identity from `source`. */
3766
+ constructor(source) {
3767
+ this.source = source;
3768
+ }
3769
+ /**
3770
+ * Where the author git would commit with in `git`'s directory comes from.
3771
+ *
3772
+ * Without an identity of the developer's own, git guesses one from the
3773
+ * username and hostname and commits anyway; `user.useConfigOnly` turns that
3774
+ * guess back into an error, which tells a real identity from a guessed one.
3775
+ */
3776
+ static async of(git) {
3777
+ if ((await git.run([
3778
+ "-c",
3779
+ "user.useConfigOnly=true",
3780
+ "var",
3781
+ "GIT_AUTHOR_IDENT"
3782
+ ], [128])).succeeded()) return new AuthorIdentity("yours");
3783
+ if ((await git.run(["var", "GIT_AUTHOR_IDENT"], [128])).succeeded()) return new AuthorIdentity("guessed");
3784
+ return new AuthorIdentity("fallback");
3785
+ }
3786
+ };
3787
+ //#endregion
3788
+ //#region src/lib/git/checks/head-is-on-a-branch.ts
3789
+ /**
3790
+ * HEAD is a branch, so a commit is not lost on a detached HEAD.
3791
+ *
3792
+ * A commit on a detached HEAD belongs to no branch, and the developer's next
3793
+ * checkout leaves it behind. A branch with no commit yet, as in a repository
3794
+ * just created, counts as a branch.
3795
+ */
3796
+ var HeadIsOnABranch = class {
3797
+ /** Why a commit here would be lost, or null. */
3798
+ async problem(git) {
3799
+ if ((await git.run([
3800
+ "symbolic-ref",
3801
+ "--quiet",
3802
+ "HEAD"
3803
+ ], [1])).succeeded()) return null;
3804
+ return "HEAD is detached, so the commit would be on no branch";
3805
+ }
3806
+ };
3807
+ //#endregion
3808
+ //#region src/lib/git/checks/no-operation-under-way.ts
3809
+ /**
3810
+ * No merge, rebase, cherry-pick, revert or bisect is under way, so a commit lands where meant.
3811
+ *
3812
+ * Mid-merge, a commit would conclude the merge with the developer's
3813
+ * half-resolved work; mid-rebase or cherry-pick, it would be woven into a
3814
+ * history being rewritten; mid-bisect, it would land on whichever commit is
3815
+ * being tested.
3816
+ */
3817
+ var NoOperationUnderWay = class {
3818
+ /**
3819
+ * "a merge is in progress" and the like, or null.
3820
+ *
3821
+ * Each operation keeps a marker file in the git directory while it is under
3822
+ * way. `--git-path` finds each one for this worktree, which in a linked
3823
+ * worktree is not under `.git/`.
3824
+ */
3825
+ async problem(git) {
3826
+ const operations = [
3827
+ {
3828
+ marker: "MERGE_HEAD",
3829
+ name: "a merge"
3830
+ },
3831
+ {
3832
+ marker: "rebase-merge",
3833
+ name: "a rebase"
3834
+ },
3835
+ {
3836
+ marker: "rebase-apply",
3837
+ name: "a rebase"
3838
+ },
3839
+ {
3840
+ marker: "CHERRY_PICK_HEAD",
3841
+ name: "a cherry-pick"
3842
+ },
3843
+ {
3844
+ marker: "REVERT_HEAD",
3845
+ name: "a revert"
3846
+ },
3847
+ {
3848
+ marker: "BISECT_LOG",
3849
+ name: "a bisect"
3850
+ }
3851
+ ];
3852
+ const args = operations.flatMap((operation) => ["--git-path", operation.marker]);
3853
+ const paths = (await git.run(["rev-parse", ...args])).text().split("\n");
3854
+ const found = operations.find((_, index) => git.exists(paths[index] ?? ""));
3855
+ if (found === void 0) return null;
3856
+ return `${found.name} is in progress`;
3857
+ }
3858
+ };
3859
+ //#endregion
3860
+ //#region src/lib/git/checks/nothing-excluded-is-staged.ts
3861
+ /**
3862
+ * No staged file is one `except` matches.
3863
+ *
3864
+ * The last guard before a commit. {@link StagingArea.addAll} already leaves
3865
+ * excluded files out, but a file committed once cannot be taken out of
3866
+ * history, so what is about to be committed is checked rather than trusted.
3867
+ */
3868
+ var NothingExcludedIsStaged = class {
3869
+ except;
3870
+ /** Checks against `except`. */
3871
+ constructor(except) {
3872
+ this.except = except;
3873
+ }
3874
+ /** Which excluded file is still staged, or null. */
3875
+ async problem(git) {
3876
+ const leaked = (await git.stagedFiles()).find(this.except);
3877
+ if (leaked === void 0) return null;
3878
+ return `${leaked} could not be kept out of the commit, so nothing was committed`;
3879
+ }
3880
+ };
3881
+ //#endregion
3882
+ //#region src/lib/git/checks/something-is-staged.ts
3883
+ /**
3884
+ * Something is staged, so a commit has something to record.
3885
+ *
3886
+ * When everything was excluded, or nothing changed, the answer is "there was
3887
+ * nothing to commit" rather than an empty commit or git's own refusal.
3888
+ */
3889
+ var SomethingIsStaged = class {
3890
+ /** That nothing is staged, or null. */
3891
+ async problem(git) {
3892
+ if ((await git.stagedFiles()).length > 0) return null;
3893
+ return "there was nothing to commit";
3894
+ }
3895
+ };
3896
+ //#endregion
3897
+ //#region src/lib/git/operations/current-commit.ts
3898
+ /**
3899
+ * The commit the repository is at, or null before its first commit.
3900
+ *
3901
+ * A repository with no commit yet is an answer, null, not a failure.
3902
+ */
3903
+ var CurrentCommit = class {
3904
+ /** Nothing to check. */
3905
+ checks() {
3906
+ return [];
3907
+ }
3908
+ /** Never refused. */
3909
+ refused() {
3910
+ return null;
3911
+ }
3912
+ /** Asks git for HEAD's commit. */
3913
+ async run(git) {
3914
+ const result = await git.run([
3915
+ "rev-parse",
3916
+ "--verify",
3917
+ "--quiet",
3918
+ "HEAD^{commit}"
3919
+ ], [1]);
3920
+ if (!result.succeeded()) return null;
3921
+ return result.text();
3922
+ }
3923
+ };
3924
+ //#endregion
3925
+ //#region src/lib/git/staging-area.ts
3926
+ /**
3927
+ * A temporary staging area (`GIT_INDEX_FILE`), so a commit or a diff is put
3928
+ * together without touching the developer's own, which may hold staged work
3929
+ * of theirs, another package's included.
3930
+ *
3931
+ * Made with {@link temporary}, which removes it however the work ends. A
3932
+ * commit starts it at the last commit ({@link startFromHead}); a diff starts
3933
+ * it from a copy of the developer's ({@link startFromDevelopers}). Its methods
3934
+ * are the steps an operation takes, so the operation reads as a list of them.
3935
+ */
3936
+ var StagingArea = class StagingArea {
3937
+ /** git, pointed at this staging area. */
3938
+ git;
3939
+ index;
3940
+ /** The staging area kept in `index`. */
3941
+ constructor(git, index) {
3942
+ this.index = index;
3943
+ this.git = git.with({ GIT_INDEX_FILE: index });
3944
+ }
3945
+ /**
3946
+ * Runs `work` with a new staging area for `git`'s directory, removed afterwards.
3947
+ *
3948
+ * A failure to remove it is dropped: by then the work is done, and a commit
3949
+ * that landed must not be reported as failed because a temporary file was
3950
+ * briefly held open (as Windows Defender does).
3951
+ */
3952
+ static async temporary(git, work) {
3953
+ const scratch = await mkdtemp(join(tmpdir(), "git-staging-"));
3954
+ try {
3955
+ return await work(new StagingArea(git, join(scratch, "index")));
3956
+ } finally {
3957
+ await rm(scratch, {
3958
+ recursive: true,
3959
+ force: true
3960
+ }).catch(() => void 0);
3961
+ }
3962
+ }
3963
+ /**
3964
+ * Starts at the last commit, keeping what the developer's staging area knows about each file; empty before one.
3965
+ *
3966
+ * It starts as a copy of `developers`' staging area and is then reset to
3967
+ * the last commit with `read-tree -m`, which keeps each entry's flags and
3968
+ * file timestamps where the content is unchanged. The flags matter: a file
3969
+ * the developer marked `skip-worktree` or `assume-unchanged` (a local
3970
+ * config with real credentials, or the files outside a sparse checkout)
3971
+ * keeps its mark, so its local state is never staged. The timestamps spare
3972
+ * git from hashing every file again. Another package's staged work is
3973
+ * dropped by the reset, so the commit carries the rest of the repository as
3974
+ * last committed.
3975
+ */
3976
+ async startFromHead(developers) {
3977
+ await this.startFromDevelopers(developers);
3978
+ const head = await new CurrentCommit().run(this.git);
3979
+ if (head === null) await this.git.run(["read-tree", "--empty"]);
3980
+ else await this.git.run([
3981
+ "read-tree",
3982
+ "-m",
3983
+ head
3984
+ ]);
3985
+ }
3986
+ /**
3987
+ * Starts as a copy of the developer's staging area, keeping its timestamps.
3988
+ *
3989
+ * A copy rather than an empty one, so a tracked file that `.gitignore` now
3990
+ * matches still diffs. The timestamps matter: git trusts an entry only for a
3991
+ * file older than the index, so a copy stamped now hid an edit made in the
3992
+ * same second as the developer's last `git add`. A fresh repository has no
3993
+ * index yet, and the copy then starts empty.
3994
+ */
3995
+ async startFromDevelopers(developers) {
3996
+ const index = resolve(developers.dir, (await developers.run([
3997
+ "rev-parse",
3998
+ "--git-path",
3999
+ "index"
4000
+ ])).text());
4001
+ if (existsSync(index)) {
4002
+ await copyFile(index, this.index);
4003
+ const { atime, mtime } = await stat(index);
4004
+ await utimes(this.index, atime, mtime);
4005
+ }
4006
+ }
4007
+ /**
4008
+ * Stages everything under the directory but what `except` matches, as `git add --all -- .` would.
4009
+ *
4010
+ * The changed and untracked files are listed first to find the ones
4011
+ * `except` matches, and those are then left out of a single
4012
+ * `git add --all -- .` with `:(exclude)`. An excluded file is never read, so
4013
+ * its content is never written to the object store, where it would sit as
4014
+ * a loose object until git cleans it up. Staging stays `git add --all -- .`
4015
+ * in every other way: a file that vanishes meanwhile, a tracked file in an
4016
+ * ignored directory and a sparse checkout all behave as they do for the
4017
+ * developer, and matching stays linear however many files there are. A
4018
+ * file that is already tracked and excluded keeps what the last commit has.
4019
+ *
4020
+ * The pathspecs go through a file beside the staging area, read and written
4021
+ * byte for byte: a name that is not valid UTF-8, which Linux allows, would
4022
+ * otherwise come back changed and match nothing. `except` sees each name as
4023
+ * UTF-8.
4024
+ *
4025
+ * Exit 1 is accepted: git gives it when everything `.` matches is a file the
4026
+ * developer marked `skip-worktree`, having staged nothing, which is the
4027
+ * right result. A real failure, such as a file that cannot be read, exits
4028
+ * 128. Whether anything was staged is for the caller to check.
4029
+ */
4030
+ async addAll(except) {
4031
+ const excluded = (await this.git.run([
4032
+ "ls-files",
4033
+ "-z",
4034
+ "--modified",
4035
+ "--deleted",
4036
+ "--others",
4037
+ "--exclude-standard",
4038
+ "--",
4039
+ "."
4040
+ ], [], "latin1")).paths().filter((path) => except(Buffer.from(path, "latin1").toString("utf8"))).map((path) => `:(exclude,literal)${path}`);
4041
+ const list = join(dirname(this.index), "paths");
4042
+ await writeFile(list, Buffer.from([".", ...excluded].join("\0"), "latin1"));
4043
+ await this.git.run([
4044
+ "add",
4045
+ "--all",
4046
+ `--pathspec-from-file=${list}`,
4047
+ "--pathspec-file-nul"
4048
+ ], [1]);
4049
+ }
4050
+ /**
4051
+ * Marks the untracked files among `paths` "intent to add", so a diff shows them as added.
4052
+ *
4053
+ * Intent to add records the file without its content, so nothing is written
4054
+ * to the object store. Only paths that exist are passed: `git add` refuses
4055
+ * the whole call when one matches nothing. An ignored path makes it mark the
4056
+ * rest and exit 1, which is accepted. Each path is taken literally
4057
+ * ({@link Git.literal}), so `app/[id].tsx` marks only itself.
4058
+ */
4059
+ async markAsAdded(paths) {
4060
+ const present = paths.filter((path) => this.git.exists(path)).map(Git.literal);
4061
+ if (present.length > 0) await this.git.run([
4062
+ "add",
4063
+ "--intent-to-add",
4064
+ "--",
4065
+ ...present
4066
+ ], [1]);
4067
+ }
4068
+ /** The first problem any of `checks` finds with what is staged, or null. */
4069
+ async problem(checks) {
4070
+ for (const check of checks) {
4071
+ const problem = await check.problem(this.git);
4072
+ if (problem !== null) return problem;
4073
+ }
4074
+ return null;
4075
+ }
4076
+ /**
4077
+ * Commits what is staged with `message`, naming `author` as author and committer when given.
4078
+ *
4079
+ * Without `author`, git names whoever it would. An author is set through
4080
+ * the environment rather than `-c user.*`: an inherited empty
4081
+ * `GIT_AUTHOR_NAME=`, as some CI runners leave it, outranks `-c`, and git
4082
+ * would refuse the commit with "empty ident name".
4083
+ *
4084
+ * Signing is turned off, so a configured key cannot block on a passphrase
4085
+ * prompt. `--no-verify` skips the pre-commit and commit-msg hooks, which a
4086
+ * scaffolded app's linter would otherwise fail; post-commit hooks still run,
4087
+ * as they always did.
4088
+ */
4089
+ async commit(message, author) {
4090
+ const commit = [
4091
+ "-c",
4092
+ "commit.gpgsign=false",
4093
+ "commit",
4094
+ "--quiet",
4095
+ "--no-verify",
4096
+ `--message=${message}`
4097
+ ];
4098
+ if (author === null) await this.git.run(commit);
4099
+ else await this.git.with({
4100
+ GIT_AUTHOR_NAME: author.name,
4101
+ GIT_AUTHOR_EMAIL: author.email,
4102
+ GIT_COMMITTER_NAME: author.name,
4103
+ GIT_COMMITTER_EMAIL: author.email
4104
+ }).run(commit);
4105
+ }
4106
+ };
4107
+ //#endregion
4108
+ //#region src/lib/git/operations/commit-all.ts
4109
+ /**
4110
+ * Commits everything under the directory but `options.except`.
4111
+ *
4112
+ * The commit is built in a temporary staging area that starts from the last
4113
+ * commit, never in the developer's own: theirs may hold another package's
4114
+ * staged work, which would be swept in. It carries the developer's per-file
4115
+ * flags, so a file they told git to leave alone stays out of the commit. A pathspec is no way round that,
4116
+ * since `git commit -- .` commits the working tree and ignores the staging
4117
+ * area, so an excluded file that is already tracked would go in anyway.
4118
+ *
4119
+ * Expects nothing uncommitted of the developer's under the directory: once
4120
+ * committed, their staging area there is reset to the new commit, or they
4121
+ * would see the commit's files staged for deletion.
4122
+ */
4123
+ var CommitAll = class {
4124
+ message;
4125
+ options;
4126
+ /** Commits with `message`, as `options` say. */
4127
+ constructor(message, options) {
4128
+ this.message = message;
4129
+ this.options = options;
4130
+ }
4131
+ /**
4132
+ * Nothing is under way that the commit would land in the middle of.
4133
+ *
4134
+ * Mid-merge, a commit would record a merge with the wrong parents; mid-rebase
4135
+ * or on a detached HEAD, it would sit on no branch and be lost.
4136
+ */
4137
+ checks() {
4138
+ return [new NoOperationUnderWay(), new HeadIsOnABranch()];
4139
+ }
4140
+ /** Nothing committed, for `problem`. */
4141
+ refused(problem) {
4142
+ return {
4143
+ committed: false,
4144
+ reason: problem
4145
+ };
4146
+ }
4147
+ /**
4148
+ * Stages everything but the excluded files, checks, and commits.
4149
+ *
4150
+ * The caller's fallback author is named only where git cannot build an
4151
+ * identity at all; a commit with no author would leave nothing to diff a
4152
+ * later deploy against.
4153
+ *
4154
+ * What is about to be committed is checked rather than trusted, since a
4155
+ * file committed once cannot be taken out of history. The reset of the
4156
+ * developer's staging area afterwards is best effort, since the commit has
4157
+ * already landed.
4158
+ *
4159
+ * A commit can land and git still fail afterwards: a post-commit hook that
4160
+ * errors, or one that runs past the five-minute limit. So when the commit
4161
+ * fails, HEAD is read again, and a HEAD that moved means the commit was made
4162
+ * and is reported as made.
4163
+ */
4164
+ async run(git) {
4165
+ const identity = await AuthorIdentity.of(git);
4166
+ const before = await new CurrentCommit().run(git);
4167
+ return StagingArea.temporary(git, async (staging) => {
4168
+ await staging.startFromHead(git);
4169
+ await staging.addAll(this.options.except);
4170
+ const problem = await staging.problem([new NothingExcludedIsStaged(this.options.except), new SomethingIsStaged()]);
4171
+ if (problem !== null) return this.refused(problem);
4172
+ try {
4173
+ if (identity.source === "fallback") await staging.commit(this.message, this.options.authorIfGitHasNone);
4174
+ else await staging.commit(this.message, null);
4175
+ } catch (error) {
4176
+ if (await new CurrentCommit().run(git) === before) throw error;
4177
+ }
4178
+ const sha = await new CurrentCommit().run(git);
4179
+ if (sha === null) throw new RepositoryError("the commit left no HEAD to record");
4180
+ await git.run([
4181
+ "reset",
4182
+ "--quiet",
4183
+ "--",
4184
+ "."
4185
+ ]).catch(() => void 0);
4186
+ return {
4187
+ committed: true,
4188
+ sha,
4189
+ author: identity.source
4190
+ };
4191
+ });
4192
+ }
4193
+ };
4194
+ //#endregion
4195
+ //#region src/lib/git/operations/current-subject.ts
4196
+ /**
4197
+ * The subject line of the commit the repository is at, or null before its first commit.
4198
+ *
4199
+ * `--no-show-signature` overrides a developer's `log.showSignature`, which
4200
+ * would otherwise put gpg's lines ahead of the subject on a signed commit. A
4201
+ * repository with no commit yet makes `git log` exit 128, which is an answer,
4202
+ * null, not a failure.
4203
+ */
4204
+ var CurrentSubject = class {
4205
+ /** Nothing to check. */
4206
+ checks() {
4207
+ return [];
4208
+ }
4209
+ /** Never refused. */
4210
+ refused() {
4211
+ return null;
4212
+ }
4213
+ /** Asks git for HEAD's subject. */
4214
+ async run(git) {
4215
+ const result = await git.run([
4216
+ "log",
4217
+ "-1",
4218
+ "--no-show-signature",
4219
+ "--format=%s"
4220
+ ], [128]);
4221
+ if (!result.succeeded()) return null;
4222
+ return result.text() || null;
4223
+ }
4224
+ };
4225
+ //#endregion
4226
+ //#region src/lib/git/change.ts
4227
+ /**
4228
+ * One changed file, as `git diff --raw -z` lists it: where it is, what
4229
+ * happened to it, and where it came from when it was renamed or copied.
4230
+ *
4231
+ * Read from git's raw list rather than from the patch's `diff --git` headers:
4232
+ * the list is NUL-separated and never quoted, while a header quotes a path
4233
+ * holding a space or a non-ASCII byte and cannot be split reliably.
4234
+ * {@link DiffFilesSince} pairs each change with its part of the patch.
4235
+ */
4236
+ var Change = class Change {
4237
+ path;
4238
+ status;
4239
+ /** Where a renamed or copied file came from. */
4240
+ previousPath;
4241
+ /** A change of `status` to `path`, from `previousPath` for a rename or a copy. */
4242
+ constructor(path, status, previousPath) {
4243
+ this.path = path;
4244
+ this.status = status;
4245
+ this.previousPath = previousPath;
4246
+ }
4247
+ /**
4248
+ * Every change in the NUL-separated `fields` of a `--raw -z` list, in its order.
4249
+ *
4250
+ * Each entry is `:<modes> <ids> <letter>[<score>]`, then the path, or for a
4251
+ * rename or a copy where it came from and where it went. The entries are
4252
+ * read in order rather than by looking for a `:`, since a path may start
4253
+ * with one. M, T (a type change) and U (unmerged) all count as modified.
4254
+ */
4255
+ static listedIn(fields) {
4256
+ const statuses = {
4257
+ A: "added",
4258
+ D: "deleted",
4259
+ R: "renamed",
4260
+ C: "copied"
4261
+ };
4262
+ const unread = fields.values();
4263
+ const changes = [];
4264
+ for (const entry of unread) {
4265
+ const status = statuses[entry.charAt(entry.lastIndexOf(" ") + 1)] ?? "modified";
4266
+ const first = unread.next().value ?? "";
4267
+ if (status === "renamed" || status === "copied") changes.push(new Change(unread.next().value ?? "", status, first));
4268
+ else changes.push(new Change(first, status));
4269
+ }
4270
+ return changes;
4271
+ }
4272
+ /** This change, as a diff reports it, with `patch` as its part of the diff. */
4273
+ diffedWith(patch) {
4274
+ if (this.previousPath === void 0) return {
4275
+ path: this.path,
4276
+ status: this.status,
4277
+ patch
4278
+ };
4279
+ return {
4280
+ path: this.path,
4281
+ status: this.status,
4282
+ previous_path: this.previousPath,
4283
+ patch
4284
+ };
4285
+ }
4286
+ };
4287
+ //#endregion
4288
+ //#region src/lib/git/checks/commit-is-here.ts
4289
+ /**
4290
+ * `commit` is a commit in local history; a shallow clone may lack it.
4291
+ *
4292
+ * Checked before a diff, so a missing commit answers null ("no diff
4293
+ * available") rather than failing, and an empty diff is never mistaken for a
4294
+ * clean tree.
4295
+ */
4296
+ var CommitIsHere = class {
4297
+ commit;
4298
+ /** Checks for `commit`. */
4299
+ constructor(commit) {
4300
+ this.commit = commit;
4301
+ }
4302
+ /**
4303
+ * Why the commit cannot be used, or null.
4304
+ *
4305
+ * Only an object id, full or abbreviated, is handed to git; anything else
4306
+ * could be read as an option or name a branch.
4307
+ */
4308
+ async problem(git) {
4309
+ if (!/^[0-9a-f]{4,64}$/i.test(this.commit)) return `${this.commit} is not a commit id`;
4310
+ if ((await git.run([
4311
+ "cat-file",
4312
+ "-e",
4313
+ `${this.commit}^{commit}`
4314
+ ], [1, 128])).succeeded()) return null;
4315
+ return `${this.commit} is not in this repository's history`;
4316
+ }
4317
+ };
4318
+ //#endregion
4319
+ //#region src/lib/git/operations/diff-since.ts
4320
+ /**
4321
+ * The diff of `paths` since `commit`, untracked files as added, as git prints it.
4322
+ *
4323
+ * One `git diff` over all of `paths`. Not one per file: a path matches
4324
+ * everything under it too, so a file replaced by a directory took in the
4325
+ * directory's files, and hundreds of files meant hundreds of processes, which
4326
+ * ran out of open files. Nothing is left out of the diff: the paths are the
4327
+ * caller's to choose, and whatever they name, a secret included, is shown.
4328
+ */
4329
+ var DiffSince = class {
4330
+ commit;
4331
+ paths;
4332
+ format;
4333
+ /** Diffs `paths` since `commit`, coloured as `options` say, with `extra` added to git's format. */
4334
+ constructor(commit, paths, options, extra = []) {
4335
+ this.commit = commit;
4336
+ this.paths = paths;
4337
+ if (options.color) this.format = ["--color=always", ...extra];
4338
+ else this.format = ["--no-color", ...extra];
4339
+ }
4340
+ /** The commit is in local history. */
4341
+ checks() {
4342
+ return [new CommitIsHere(this.commit)];
4343
+ }
4344
+ /** No diff: there is nothing to diff against. */
4345
+ refused() {
4346
+ return null;
4347
+ }
4348
+ /**
4349
+ * Diffs in a copy of the developer's staging area with untracked files marked as added.
4350
+ *
4351
+ * `git diff` leaves untracked files out, but a new file is part of what a
4352
+ * deploy publishes, so it is marked "intent to add" first, in a copy and
4353
+ * never in the developer's own staging area. When that cannot be done the
4354
+ * plain diff is given instead, which still shows every tracked change.
4355
+ *
4356
+ * `--no-ext-diff` keeps a configured external diff tool out, `--relative`
4357
+ * names paths from the directory, and each path is taken literally
4358
+ * ({@link Git.literal}), so `app/[id].tsx` does not also diff `app/i.tsx`.
4359
+ */
4360
+ async run(git) {
4361
+ const diff = [
4362
+ "diff",
4363
+ "--no-ext-diff",
4364
+ "--relative",
4365
+ ...this.format,
4366
+ this.commit,
4367
+ "--",
4368
+ ...this.paths.map(Git.literal)
4369
+ ];
4370
+ return StagingArea.temporary(git, async (staging) => {
4371
+ try {
4372
+ await staging.startFromDevelopers(git);
4373
+ await staging.markAsAdded(this.paths);
4374
+ return (await staging.git.run(diff)).stdout;
4375
+ } catch {
4376
+ return (await git.run(diff)).stdout;
4377
+ }
4378
+ });
4379
+ }
4380
+ };
4381
+ //#endregion
4382
+ //#region src/lib/git/operations/diff-files-since.ts
4383
+ /**
4384
+ * The changed files under `paths` since `commit`, untracked files as added, each with its part of the diff.
4385
+ *
4386
+ * Built from the same single `git diff` as {@link DiffSince}, so joined back
4387
+ * together the patches are the diff a terminal shows. `--submodule=short`
4388
+ * keeps a submodule's part under a `diff --git` header, which
4389
+ * `diff.submodule=log` would replace with a `Submodule …` line that belongs
4390
+ * to no file; it is set here only, so the terminal diff stays as the
4391
+ * developer configured it.
4392
+ */
4393
+ var DiffFilesSince = class {
4394
+ diff;
4395
+ /** Diffs `paths` since `commit`, uncoloured, with the list of changed files ahead of the patch. */
4396
+ constructor(commit, paths) {
4397
+ this.diff = new DiffSince(commit, paths, {}, [
4398
+ "--raw",
4399
+ "-z",
4400
+ "--patch",
4401
+ "--submodule=short"
4402
+ ]);
4403
+ }
4404
+ /** Those of the diff. */
4405
+ checks() {
4406
+ return this.diff.checks();
4407
+ }
4408
+ /** No diff: there is nothing to diff against. */
4409
+ refused() {
4410
+ return null;
4411
+ }
4412
+ /**
4413
+ * Each listed file paired with its section of the patch.
4414
+ *
4415
+ * `--raw -z` lists every file's status and paths, unquoted, ahead of the
4416
+ * patch; one more NUL ends the list. git writes the list and the patch from
4417
+ * one list of files, in one order, so they pair one to one. The exception is
4418
+ * a file whose type changed (a file replaced by a symlink): git shows it as
4419
+ * two sections under one header, which are joined. The patch is split only
4420
+ * where `diff --git` follows a newline. A file's content may mention it, and
4421
+ * a file with old Mac line endings (a lone `\r`) has no newline before it,
4422
+ * so its section stays whole.
4423
+ */
4424
+ async run(git) {
4425
+ const output = await this.diff.run(git);
4426
+ const end = output.indexOf("\0\0");
4427
+ if (end === -1) return [];
4428
+ const patches = [];
4429
+ for (const section of output.slice(end + 2).split(/(?<=\n)(?=diff --git )/)) {
4430
+ const last = patches.at(-1);
4431
+ if (last !== void 0 && last.slice(0, last.indexOf("\n")) === section.slice(0, section.indexOf("\n"))) patches[patches.length - 1] = last + section;
4432
+ else patches.push(section);
4433
+ }
4434
+ return Change.listedIn(output.slice(0, end).split("\0")).map((change, index) => change.diffedWith(patches[index] ?? ""));
4435
+ }
4436
+ };
4437
+ //#endregion
4438
+ //#region src/lib/git/operations/is-dirty.ts
4439
+ /**
4440
+ * Whether anything in the whole repository is uncommitted, not only under the directory.
4441
+ *
4442
+ * Any change anywhere in the repository counts, including outside the
4443
+ * directory the repository was opened on.
4444
+ */
4445
+ var IsDirty = class {
4446
+ /** Nothing to check. */
4447
+ checks() {
4448
+ return [];
4449
+ }
4450
+ /** Never refused. */
4451
+ refused() {
4452
+ return false;
4453
+ }
4454
+ /** Asks git whether `status` lists anything. */
4455
+ async run(git) {
4456
+ return (await git.run(["status", "--porcelain"])).stdout.trim() !== "";
4457
+ }
4458
+ };
4459
+ //#endregion
4460
+ //#region src/lib/git/operations/ready-to-commit.ts
4461
+ /**
4462
+ * Why a commit here would land where it should not, or null when one would land on a branch as meant.
4463
+ *
4464
+ * The same checks {@link CommitAll} makes, asked on their own, so a caller
4465
+ * can refuse before it does any work rather than find out when it commits.
4466
+ */
4467
+ var ReadyToCommit = class {
4468
+ /** Nothing is under way, and HEAD is on a branch. */
4469
+ checks() {
4470
+ return [new NoOperationUnderWay(), new HeadIsOnABranch()];
4471
+ }
4472
+ /** The first problem a check found. */
4473
+ refused(problem) {
4474
+ return problem;
4475
+ }
4476
+ /** No problem: every check holds. */
4477
+ async run() {
4478
+ return null;
4479
+ }
4480
+ };
4481
+ //#endregion
4482
+ //#region src/lib/git/operations/uncommitted-files.ts
4483
+ /**
4484
+ * The uncommitted files under the directory, untracked ones included, each file of an untracked directory listed.
4485
+ *
4486
+ * What a commit of the whole directory would sweep in. In a monorepo a
4487
+ * sibling package's work is not listed, since a commit here never touches
4488
+ * it. Each path is named from the directory, as the developer sees it.
4489
+ */
4490
+ var UncommittedFiles = class {
4491
+ /** Where the directory sits in the repository, e.g. `apps/web/`: what `status` puts before each path. */
4492
+ prefix;
4493
+ /** Lists the files under the directory at `prefix`. */
4494
+ constructor(prefix) {
4495
+ this.prefix = prefix;
4496
+ }
4497
+ /** Nothing to check. */
4498
+ checks() {
4499
+ return [];
4500
+ }
4501
+ /** Never refused. */
4502
+ refused() {
4503
+ return [];
4504
+ }
4505
+ /**
4506
+ * Asks git for the status of the directory.
4507
+ *
4508
+ * `-- .` keeps a sibling package's work out. `--untracked-files=all` names
4509
+ * each file of an untracked directory rather than the directory alone.
4510
+ * `status` names paths from the repository root, so the prefix is taken off;
4511
+ * a rename or a copy carries a second entry, where it came from, which is
4512
+ * skipped.
4513
+ */
4514
+ async run(git) {
4515
+ const unread = (await git.run([
4516
+ "status",
4517
+ "--porcelain",
4518
+ "-z",
4519
+ "--untracked-files=all",
4520
+ "--",
4521
+ "."
4522
+ ])).paths().values();
4523
+ const files = [];
4524
+ for (const entry of unread) {
4525
+ const path = entry.slice(3);
4526
+ const untracked = entry.startsWith("??");
4527
+ if (path.startsWith(this.prefix)) files.push({
4528
+ path: path.slice(this.prefix.length),
4529
+ untracked
4530
+ });
4531
+ else files.push({
4532
+ path,
4533
+ untracked
4534
+ });
4535
+ if (/^(R|C|.R|.C)/.test(entry)) unread.next();
4536
+ }
4537
+ return files;
4538
+ }
4539
+ };
4540
+ //#endregion
4541
+ //#region src/lib/git/repository.ts
4542
+ /**
4543
+ * The git repository a directory is in, seen from that directory.
4544
+ *
4545
+ * In a monorepo the repository may start several levels up; everything here
4546
+ * is limited to the directory and names paths relative to it, except
4547
+ * {@link isDirty}, which looks at the whole repository. When git fails, a
4548
+ * method throws a `RepositoryError` carrying git's own message.
4549
+ *
4550
+ * Each method is one {@link Operation}: its checks run first, and the first
4551
+ * one that fails decides the answer through the operation's `refused`.
4552
+ */
4553
+ var Repository = class Repository {
4554
+ git;
4555
+ /** Where the directory sits in the repository, e.g. `apps/web/`, or `""` at its root. */
4556
+ prefix;
4557
+ /** Creates the handle; use {@link Repository.open} or {@link Repository.create}. */
4558
+ constructor(dir, prefix) {
4559
+ this.git = new Git(dir);
4560
+ this.prefix = prefix;
4561
+ }
4562
+ /**
4563
+ * The repository `dir` is in, or null when it is in none, does not exist, or there is no git.
4564
+ *
4565
+ * Only those three mean "no repository". Any other failure, such as a
4566
+ * refusal over ownership or a broken `HOME`, is thrown: answering "none"
4567
+ * would let a caller `git init` a second repository inside the first, or
4568
+ * skip a check that the tree is clean.
4569
+ *
4570
+ * `dir` is resolved to its real path first: git answers paths such as
4571
+ * `--git-path MERGE_HEAD` relative to where it really runs, so through a
4572
+ * symlink they would point somewhere else. git is asked in English here,
4573
+ * since "not a git repository" is recognised by its wording.
4574
+ */
4575
+ static async open(dir) {
4576
+ if (!existsSync(dir)) return null;
4577
+ const real = await realpath(dir);
4578
+ const result = await new Git(real).with({ LC_ALL: "C" }).run(["rev-parse", "--show-prefix"], [128]).catch((error) => {
4579
+ if (error instanceof GitDidNotStartError) return null;
4580
+ throw error;
4581
+ });
4582
+ if (result === null) return null;
4583
+ if (/^fatal: not a git repository/m.test(result.stderr)) return null;
4584
+ if (!result.succeeded()) throw new RepositoryError(result.reason());
4585
+ return new Repository(real, result.text());
4586
+ }
4587
+ /**
4588
+ * What `question` answers about the repository `dir` is in, or `otherwise` when there is none or git cannot answer.
4589
+ *
4590
+ * For a caller that records what git can tell and must never fail because
4591
+ * of it, such as one that notes a commit when there is one: every failure,
4592
+ * a missing git included, becomes `otherwise`. A caller that needs to know
4593
+ * why git failed uses {@link open} and the methods directly.
4594
+ */
4595
+ static async answer(dir, question, otherwise) {
4596
+ try {
4597
+ const repository = await Repository.open(dir);
4598
+ if (repository === null) return otherwise;
4599
+ return await question(repository);
4600
+ } catch {
4601
+ return otherwise;
4602
+ }
4603
+ }
4604
+ /**
4605
+ * A new repository in `dir`; when git cannot make one, nothing is left behind.
4606
+ *
4607
+ * `git init` writes `.git` before it is done, so a failure partway (a full
4608
+ * or read-only disk) leaves a half-made repository that every later git
4609
+ * command would trip over. When `dir` had no `.git` before, a failed
4610
+ * `git init` removes it again, so a caller gets either a working repository
4611
+ * or an error, never something in between. The error is always git's: if
4612
+ * the removal fails too, its own failure is dropped, since git's message
4613
+ * names the real cause. This assumes nothing else creates a repository in
4614
+ * `dir` at the same time.
4615
+ *
4616
+ * A `.git` counts as there before even when it is a symlink to something
4617
+ * missing, so a developer's link to an unmounted volume is never removed.
4618
+ */
4619
+ static async create(dir) {
4620
+ const real = await realpath(dir);
4621
+ const gitDir = join(real, ".git");
4622
+ const hadOne = await lstat(gitDir).then(() => true, () => false);
4623
+ try {
4624
+ await new Git(real).run(["init", "--quiet"]);
4625
+ } catch (error) {
4626
+ if (!hadOne) await rm(gitDir, {
4627
+ recursive: true,
4628
+ force: true
4629
+ }).catch(() => void 0);
4630
+ throw error;
4631
+ }
4632
+ return new Repository(real, "");
4633
+ }
4634
+ /** The commit it is at, or null before its first commit. */
4635
+ currentCommit() {
4636
+ return this.perform(new CurrentCommit());
4637
+ }
4638
+ /** The subject line of the commit it is at, or null before its first commit. */
4639
+ currentSubject() {
4640
+ return this.perform(new CurrentSubject());
4641
+ }
4642
+ /** Whether anything in the whole repository is uncommitted. */
4643
+ isDirty() {
4644
+ return this.perform(new IsDirty());
4645
+ }
4646
+ /** The uncommitted files under the directory, untracked ones included. */
4647
+ uncommittedFiles() {
4648
+ return this.perform(new UncommittedFiles(this.prefix));
4649
+ }
4650
+ /** Why a commit here would land where it should not (an operation under way, a detached HEAD), or null. */
4651
+ readyToCommit() {
4652
+ return this.perform(new ReadyToCommit());
4653
+ }
4654
+ /**
4655
+ * Commits everything under the directory but `options.except`; see {@link CommitAll}.
4656
+ *
4657
+ * Meant for a directory with nothing uncommitted of the developer's own:
4658
+ * check {@link uncommittedFiles} first. Afterwards the developer's staging
4659
+ * area under the directory is reset to the new commit, so anything they had
4660
+ * staged there would be unstaged.
4661
+ */
4662
+ commitAll(message, options) {
4663
+ return this.perform(new CommitAll(message, options));
4664
+ }
4665
+ /** The diff of `paths` since `commit`, untracked files as added, or null when that commit is not here. */
4666
+ diffSince(commit, paths, options) {
4667
+ return this.perform(new DiffSince(commit, paths, options));
4668
+ }
4669
+ /** {@link diffSince}, uncoloured and split into one entry per changed file. */
4670
+ diffFilesSince(commit, paths) {
4671
+ return this.perform(new DiffFilesSince(commit, paths));
4672
+ }
4673
+ /** Runs `operation` once all its checks hold; what it returns when refused, otherwise. */
4674
+ async perform(operation) {
4675
+ for (const check of operation.checks()) {
4676
+ const problem = await check.problem(this.git);
4677
+ if (problem !== null) return operation.refused(problem);
4678
+ }
4679
+ return operation.run(this.git);
4680
+ }
4681
+ };
4682
+ //#endregion
3453
4683
  //#region src/lib/release/git.ts
3454
- const run = promisify(execFile);
3455
4684
  /**
3456
4685
  * The directories a release is built from, the only paths a deploy diffs. Not
3457
4686
  * the whole of `.zitadel/`: `state.json` and `meta/` are not published, and
@@ -3464,26 +4693,13 @@ const CONFIG_PATHS = [
3464
4693
  ".zitadel/branding/",
3465
4694
  ".zitadel/idps/"
3466
4695
  ];
3467
- /** Run git in `cwd`, returning its stdout, or null when git cannot answer. */
3468
- async function git(cwd, args, env) {
3469
- try {
3470
- const { stdout } = await run("git", args, {
3471
- cwd,
3472
- maxBuffer: 16777216,
3473
- ...env ? { env: {
3474
- ...process.env,
3475
- ...env
3476
- } } : {}
3477
- });
3478
- return stdout;
3479
- } catch {
3480
- return null;
3481
- }
3482
- }
3483
4696
  /** The current commit, or null when there is no repo or no commit yet. */
3484
4697
  async function headSha(cwd) {
3485
- const stdout = await git(cwd, ["rev-parse", "HEAD"]);
3486
- return stdout === null ? null : stdout.trim() || null;
4698
+ return Repository.answer(cwd, (repository) => repository.currentCommit(), null);
4699
+ }
4700
+ /** The current commit's subject line, or null when there is no repo or no commit yet. */
4701
+ async function headSubject(cwd) {
4702
+ return Repository.answer(cwd, (repository) => repository.currentSubject(), null);
3487
4703
  }
3488
4704
  /**
3489
4705
  * Whether the repository's working tree has any uncommitted change, anywhere —
@@ -3492,55 +4708,47 @@ async function headSha(cwd) {
3492
4708
  * the config depends on, say) makes that provenance just as inexact.
3493
4709
  */
3494
4710
  async function worktreeIsDirty(cwd) {
3495
- const stdout = await git(cwd, ["status", "--porcelain"]);
3496
- return stdout !== null && stdout.trim() !== "";
4711
+ return Repository.answer(cwd, (repository) => repository.isDirty(), false);
3497
4712
  }
3498
4713
  /**
3499
4714
  * The coloured diff of `.zitadel/` against `sha`, or null when no diff can be
3500
4715
  * shown: git is missing, this is not a repo, or the commit is not in local
3501
- * history (a shallow clone, say). A dead commit is told apart from a clean tree
3502
- * by first checking the commit resolves.
4716
+ * history (a shallow clone, say).
3503
4717
  */
3504
4718
  async function diffConfigAgainst(cwd, sha, color, referenced = []) {
3505
4719
  const paths = [...CONFIG_PATHS, ...referenced];
3506
- if (await git(cwd, [
3507
- "cat-file",
3508
- "-e",
3509
- `${sha}^{commit}`
3510
- ]) === null) return null;
3511
- const diffArgs = [
3512
- "diff",
3513
- color ? "--color=always" : "--no-color",
3514
- sha,
3515
- "--",
3516
- ...paths
3517
- ];
3518
- const indexPath = (await git(cwd, [
3519
- "rev-parse",
3520
- "--git-path",
3521
- "index"
3522
- ]))?.trim();
3523
- if (!indexPath) return git(cwd, diffArgs);
3524
- let scratch;
3525
- try {
3526
- scratch = await mkdtemp(join(tmpdir(), "zitadel-diff-"));
3527
- const tempIndex = join(scratch, "index");
3528
- await copyFile(resolve(cwd, indexPath), tempIndex).catch(() => void 0);
3529
- const env = { GIT_INDEX_FILE: tempIndex };
3530
- const present = paths.filter((path) => existsSync(join(cwd, path)));
3531
- if (present.length > 0) await git(cwd, [
3532
- "add",
3533
- "-N",
3534
- "--",
3535
- ...present
3536
- ], env);
3537
- return await git(cwd, diffArgs, env) ?? await git(cwd, diffArgs);
3538
- } finally {
3539
- if (scratch) await rm(scratch, {
3540
- recursive: true,
3541
- force: true
3542
- });
4720
+ return Repository.answer(cwd, (repository) => repository.diffSince(sha, paths, { color }), null);
4721
+ }
4722
+ /**
4723
+ * {@link diffConfigAgainst}, uncoloured and split by file: joined back
4724
+ * together, the patches are the diff the terminal shows. Null when no diff can
4725
+ * be shown, for the same reasons.
4726
+ */
4727
+ async function diffConfigFilesAgainst(cwd, sha, referenced = []) {
4728
+ const paths = [...CONFIG_PATHS, ...referenced];
4729
+ return Repository.answer(cwd, (repository) => repository.diffFilesSince(sha, paths), null);
4730
+ }
4731
+ //#endregion
4732
+ //#region src/lib/release/pipeline.ts
4733
+ /**
4734
+ * Run the before-release steps in order, skipping the ones that do not apply to
4735
+ * this publish or that write on a dry run, and return what they report, for the
4736
+ * command's `--json` data. The command creates the release and the deployment
4737
+ * itself, between and after the step groups: before-release steps → create
4738
+ * release → before-deploy steps → create deployment.
4739
+ */
4740
+ async function runSteps(steps, ctx) {
4741
+ let report = {};
4742
+ for (const step of steps) {
4743
+ if (!step.runsFor.includes(ctx.mode)) continue;
4744
+ if (ctx.dryRun && step.writes) continue;
4745
+ const reported = await step.run(ctx);
4746
+ if (reported) report = {
4747
+ ...report,
4748
+ ...reported
4749
+ };
3543
4750
  }
4751
+ return report;
3544
4752
  }
3545
4753
  //#endregion
3546
4754
  //#region src/lib/release/steps/asset-probe.ts
@@ -4150,6 +5358,97 @@ function assertEnvRefsResolve(ctx) {
4150
5358
  if (missing.size > 0) throw new ZitadelError("E_VALIDATION", `Missing environment variables: ${[...missing].sort().join(", ")}`);
4151
5359
  }
4152
5360
  //#endregion
5361
+ //#region src/lib/release/initial-deploy.ts
5362
+ /**
5363
+ * Publish `.zitadel/` to a project nothing has been deployed to, the way
5364
+ * `deploy` does: build the bundle, validate it, create the release and deploy
5365
+ * it to every primary origin. `deploy`'s diff step is left out, because there
5366
+ * is no live release to diff against, and so is its optimistic guard, because
5367
+ * there is no deployment to race with.
5368
+ */
5369
+ async function deployInitialRelease(input) {
5370
+ const { bundle, brandingSources, idpFiles, policyFiles } = await buildBundle(input.cwd);
5371
+ const ctx = {
5372
+ cwd: input.cwd,
5373
+ client: input.client,
5374
+ projectId: input.projectId,
5375
+ env: input.env,
5376
+ mode: "deploy",
5377
+ dryRun: false,
5378
+ json: input.json,
5379
+ color: input.color,
5380
+ bundle,
5381
+ brandingSources,
5382
+ idpFiles,
5383
+ policyFiles,
5384
+ suggest: input.suggest
5385
+ };
5386
+ await runSteps([validateStep], ctx);
5387
+ const release = await createReleaseFromBundle(input.client, input.projectId, bundle, {
5388
+ message: input.message,
5389
+ gitSha: await headSha(input.cwd),
5390
+ gitDirty: await worktreeIsDirty(input.cwd)
5391
+ });
5392
+ const deployment = await createDeployment(input.client, input.projectId, {
5393
+ releaseId: release.id,
5394
+ targets: ["primary"],
5395
+ message: input.message
5396
+ });
5397
+ return {
5398
+ releaseId: release.id,
5399
+ contentHash: release.content_hash,
5400
+ deploymentId: deployment.id,
5401
+ origins: deployment.targets.map((target) => target.origin)
5402
+ };
5403
+ }
5404
+ //#endregion
5405
+ //#region src/lib/release/message.ts
5406
+ /** The longest message the server records on a release or a deployment, in code points. */
5407
+ const MESSAGE_MAX_LENGTH = 1024;
5408
+ /** `-m/--message`: refused when blank or longer than the server records. */
5409
+ function messageFlag(description) {
5410
+ return Flags.custom({
5411
+ char: "m",
5412
+ description,
5413
+ parse: async (input) => {
5414
+ const value = refuseBlank(input, "--message", "the value is empty");
5415
+ if (Array.from(value).length > MESSAGE_MAX_LENGTH) throw new ZitadelError("E_VALIDATION", `the message is longer than ${MESSAGE_MAX_LENGTH} characters`, {
5416
+ hint: "Pass a shorter summary with -m.",
5417
+ details: { argument: "--message" }
5418
+ });
5419
+ return value;
5420
+ }
5421
+ })();
5422
+ }
5423
+ /**
5424
+ * The message a deploy or preview records: `-m`, else what the build is of —
5425
+ * the pull request's title on GitHub, Vercel's commit message, or the subject
5426
+ * of the current commit when the worktree is clean.
5427
+ */
5428
+ async function deployMessage(flag, cwd, env) {
5429
+ if (flag !== void 0) return flag;
5430
+ if (env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target") return cut(await pullRequestTitle(env.GITHUB_EVENT_PATH));
5431
+ const vercel = env.VERCEL_GIT_COMMIT_MESSAGE?.split("\n")[0];
5432
+ if (vercel?.trim()) return cut(vercel);
5433
+ if (await worktreeIsDirty(cwd)) return;
5434
+ return cut(await headSubject(cwd) ?? void 0);
5435
+ }
5436
+ /** The title in a GitHub pull_request event payload, or undefined when it cannot be read. */
5437
+ async function pullRequestTitle(path) {
5438
+ if (!path) return;
5439
+ try {
5440
+ const title = JSON.parse(await readFile(path, "utf8")).pull_request?.title;
5441
+ return typeof title === "string" ? title : void 0;
5442
+ } catch {
5443
+ return;
5444
+ }
5445
+ }
5446
+ /** Trimmed and cut to the server's limit by code points, or undefined when blank. */
5447
+ function cut(value) {
5448
+ const trimmed = value?.trim();
5449
+ return trimmed ? Array.from(trimmed).slice(0, MESSAGE_MAX_LENGTH).join("") : void 0;
5450
+ }
5451
+ //#endregion
4153
5452
  //#region src/lib/release/steps/diff.ts
4154
5453
  /**
4155
5454
  * Show what `.zitadel/` changed since the release the target serves now.
@@ -4159,48 +5458,61 @@ function assertEnvRefsResolve(ctx) {
4159
5458
  * library and no server fallback: when git is missing, this is not a repo, the
4160
5459
  * commit is not in local history, or the live release carries no `git_sha`, it
4161
5460
  * prints one line saying no diff is available and carries on. Colour is
4162
- * suppressed under `--json`, `--no-color` and a non-terminal, and the whole
4163
- * line is withheld under `--json` so the envelope travels alone on stdout.
5461
+ * suppressed under `--no-color` and a non-terminal. Under `--json` nothing is
5462
+ * printed, so the envelope travels alone on stdout: the diff, split by file, or
5463
+ * the line saying why there is none, is reported as `diff` instead.
4164
5464
  */
4165
5465
  const diffStep = {
4166
5466
  name: "diff",
4167
5467
  runsFor: ["deploy", "preview"],
4168
5468
  async run(ctx) {
4169
- const note = (line) => {
4170
- if (!ctx.json) process.stdout.write(`${line}\n`);
4171
- };
4172
- const deployments = await listLiveDeployments(ctx.client, ctx.projectId, { withRelease: true });
5469
+ const deployments = await ctx.client.listDeployments.all({
5470
+ project_id: ctx.projectId,
5471
+ live: true,
5472
+ expand: ["release"]
5473
+ });
4173
5474
  const live = deployments.find(servesPrimary)?.release?.metadata;
4174
5475
  const sha = live?.git_sha;
4175
- if (!sha) {
4176
- note("No diff available: nothing is deployed yet, or the live release has no commit.");
4177
- return;
4178
- }
4179
- if (live?.git_dirty) {
4180
- note(`No reliable diff available: the live release was built from an uncommitted tree at ${sha.slice(0, 12)}.`);
4181
- return;
4182
- }
5476
+ if (!sha) return noDiff(ctx, null, "No diff available: nothing is deployed yet, or the live release has no commit.");
5477
+ if (live?.git_dirty) return noDiff(ctx, sha, `No reliable diff available: the live release was built from an uncommitted tree at ${sha.slice(0, 12)}.`);
4183
5478
  const releases = new Set(deployments.filter(servesPrimary).map((entry) => entry.release_id));
4184
- if (releases.size > 1) note(`The primary origins serve ${releases.size} different releases; the diff below is against the newest, ${sha.slice(0, 12)}.`);
5479
+ const note = releases.size > 1 ? `The primary origins serve ${releases.size} different releases; the diff is against the newest, ${sha.slice(0, 12)}.` : void 0;
5480
+ const unknown = `No diff available: ${sha.slice(0, 12)} is not in this repository's history.`;
4185
5481
  const referenced = ctx.brandingSources.flatMap((descriptor) => brandingReferencePaths(ctx.cwd, descriptor));
4186
- const diff = await diffConfigAgainst(ctx.cwd, sha, ctx.color, referenced);
4187
- if (diff === null) {
4188
- note(`No diff available: ${sha.slice(0, 12)} is not in this repository's history.`);
4189
- return;
5482
+ if (ctx.json) {
5483
+ const files = await diffConfigFilesAgainst(ctx.cwd, sha, referenced);
5484
+ if (files === null) return noDiff(ctx, sha, unknown);
5485
+ return { diff: {
5486
+ against: sha,
5487
+ files,
5488
+ ...note === void 0 ? {} : { note }
5489
+ } };
4190
5490
  }
4191
- if (diff.trim() === "") {
4192
- note(`No changes to .zitadel/ since ${sha.slice(0, 12)}.`);
4193
- return;
4194
- }
4195
- if (!ctx.json) process.stdout.write(diff.endsWith("\n") ? diff : `${diff}\n`);
5491
+ if (note !== void 0) process.stdout.write(`${note}\n`);
5492
+ const diff = await diffConfigAgainst(ctx.cwd, sha, ctx.color, referenced);
5493
+ if (diff === null) return noDiff(ctx, sha, unknown);
5494
+ const shown = diff.trim() === "" ? `No changes to .zitadel/ since ${sha.slice(0, 12)}.` : diff;
5495
+ process.stdout.write(shown.endsWith("\n") ? shown : `${shown}\n`);
4196
5496
  }
4197
5497
  };
5498
+ /** No diff to show: the line saying why, printed, or reported under `--json`. */
5499
+ function noDiff(ctx, against, note) {
5500
+ if (!ctx.json) {
5501
+ process.stdout.write(`${note}\n`);
5502
+ return;
5503
+ }
5504
+ return { diff: {
5505
+ against,
5506
+ files: null,
5507
+ note
5508
+ } };
5509
+ }
4198
5510
  //#endregion
4199
5511
  //#region src/lib/release/index.ts
4200
5512
  /**
4201
- * The arguments of a suggested publish. `deploy` refuses to run without `-m` when
4202
- * no one can be asked for a message, which is every agent and every `--json`
4203
- * run, so a suggested deploy carries one for the reader to fill in.
5513
+ * The arguments of a suggested publish. A non-interactive `deploy` with no
5514
+ * message to fall back on refuses to run without `-m`, so a suggested deploy
5515
+ * carries one for the reader to fill in.
4204
5516
  */
4205
5517
  const DEPLOY_ARGS = "deploy -m '<what changed>'";
4206
5518
  //#endregion
@@ -4279,7 +5591,7 @@ function exploreAllCommands(cliVersion) {
4279
5591
  * the run was about.
4280
5592
  */
4281
5593
  function customizeAndPublishActions(cliVersion, selector) {
4282
- return ["Customize what you ask for and how people sign in: edit .zitadel/schemas/ to change the information you collect, and .zitadel/flows/ to change the sign-up/sign-in experience. Each folder has a README with examples.", `See your changes before they go live: ${projectCommand("deploy --dry-run", selector, cliVersion)} to preview, then ${projectCommand(DEPLOY_ARGS, selector, cliVersion)} to publish.`];
5594
+ return ["Customize what you ask for and how people sign in: edit .zitadel/schemas/ to change the information you collect, and .zitadel/flows/ to change the sign-up/sign-in experience. Each folder has a README with examples.", `See your changes before they go live: ${projectCommand("deploy --dry-run", selector, cliVersion)} to preview, then ${projectCommand(DEPLOY_ARGS, selector, cliVersion)} to publish. The project serves only what is deployed, so an edit takes effect once you deploy it.`];
4283
5595
  }
4284
5596
  /**
4285
5597
  * How to change the login's look after setup. Setup never ejects a template
@@ -4294,11 +5606,10 @@ function brandingGuidanceAction(cliVersion) {
4294
5606
  //#endregion
4295
5607
  //#region src/lib/skills-hint.ts
4296
5608
  const SHOWN_FILE = "skills-hint.json";
4297
- const INSTALL_COMMAND = "npx skills add zitadel/nextgen --full-depth";
4298
5609
  /**
4299
5610
  * Tell a developer who has AI coding agents installed how to give them the
4300
5611
  * Zitadel skills, once per machine and after the command's own next steps.
4301
- * The CLI never installs anything itself: it names the command and the
5612
+ * The note adds nothing itself: it names `zitadel skills add` and the
4302
5613
  * developer runs it. Modelled on Wrangler's skills suggestion, minus the prompt.
4303
5614
  *
4304
5615
  * The marker lives in the user's CLI config directory (`telemetryConfigDir`),
@@ -4311,7 +5622,7 @@ const INSTALL_COMMAND = "npx skills add zitadel/nextgen --full-depth";
4311
5622
  * fail the command the user actually ran, and losing the race to create the marker
4312
5623
  * (`EEXIST`) just means another run is showing the note.
4313
5624
  */
4314
- async function showSkillsHint({ env, interactive }) {
5625
+ async function showSkillsHint({ env, interactive, cliVersion }) {
4315
5626
  try {
4316
5627
  if (!interactive || isCI(env) || isOptedOut(env)) return;
4317
5628
  const shownPath = join(telemetryConfigDir(env), SHOWN_FILE);
@@ -4323,9 +5634,9 @@ async function showSkillsHint({ env, interactive }) {
4323
5634
  consola$1.log([
4324
5635
  "",
4325
5636
  "Using an AI coding agent?",
4326
- "Install Zitadel skills to help your agent work with Zitadel’s CLI and authentication integrations:",
5637
+ "Add the Zitadel skills to your project to help your agent work with Zitadel’s CLI and authentication integrations:",
4327
5638
  "",
4328
- ` ${command(INSTALL_COMMAND)}`,
5639
+ ` ${command(publicCliCommand("skills add", cliVersion))}`,
4329
5640
  "",
4330
5641
  "Optional. You can continue without installing skills."
4331
5642
  ].join("\n"));
@@ -4343,6 +5654,6 @@ function isOptedOut(env) {
4343
5654
  return value !== void 0 && value !== "" && value !== "0" && value !== "false";
4344
5655
  }
4345
5656
  //#endregion
4346
- export { flagText as $, normalizePublicCliCommands as $t, compareInGroup as A, variableInventoryCells as At, reportSecretOutcome as B, loadOrCreateIdentity as Bt, passwordPolicyFile as C, parseVariableValue as Ct, BRANDING_DIR as D, toVariableRows as Dt, readJsonDirEntries as E, toVariableRow as Et, readSchemaFiles as F, buildUserAgent as Ft, ENABLE_FLAG_KEYS as G, portableSuggestions as Gt, republishCommands as H, envEnabled as Ht, selectSchema as I, installUserAgent as It, SETUP_FLAG_NAMES as J, unscopedCommand as Jt, ENABLE_FLAG_NAMES as K, projectCommand as Kt, SCHEMAS_DIR as L, processUserAgentFacts as Lt, enabledMethods as M, variableScope as Mt, methodsOf as N, reportWarning as Nt, COMMAND_GROUPS as O, valuesOfKind as Ot, readFlowFiles as P, takeReportedWarnings as Pt, customFlagValues as Q, normalizePublicCliCommand as Qt, publishClientId as R, hostAgent as Rt, parsePasswordMinLength as S, assertVariableName as St, readJsonDir as T, toInventoryRows as Tt, storeClientSecret as U, asEnvironmentSelector as Ut, republishArgs as V, resolveConsent as Vt, askConnectionEndpoints as W, inShell as Wt, customExample as X, isTrainVersion as Xt, checkGivenCustomFlags as Y, isPortableShellWord as Yt, customFlagText as Z, maskPublicCliVersion as Zt, servesPrimary as _, stopListeningForInterrupt as _n, storedCustomSettings as _t, serverLogAction as a, collectPages as an, applySsoToSchema as at, PASSWORD_POLICY_PATH as b, isMissingEndpoint as bn, offeredSignInMethods as bt, wrapForBox as c, setTopLevelJsonKey as cn, ssoEditRefusal as ct, validateStep as d, createZitadelClient$1 as dn, IDPS_DIR as dt, normalizePublicCliJson as en, givenCustomFlags as et, headSha as f, escapeControlCharacters as fn, checkCustomSlug as ft, rollbackDeployment as g, listenForInterrupt as gn, refuseChangedIssuer as gt, listLiveDeployments as h, interruptSignal as hn, readConnectionFiles as ht, exploreAllCommands as i, sdkVersionForCli as in, applySsoToFlow as it, authMethods as j, variableReadScope as jt, CommandGroups as k, variableCells as kt, DEPLOY_ARGS as l, stableStringify as ln, ssoProvidersRefusal as lt, createDeployment as m, sanitizeResponse as mn, planConnection as mt, brandingGuidanceAction as n, optionArgs as nn, selectionFromFlags as nt, verifyLoginAction as o, isObject$1 as on, removeSsoFromFlow as ot, worktreeIsDirty as p, sanitizeRejection as pn, credentialVariablesOf as pt, SETUP_FLAG_KEYS as q, shellCommand as qt, customizeAndPublishActions as r, publicCliCommand as rn, ssoEnableArgs as rt, renderBoxActions as s, parseJsonObject as sn, removeSsoFromSchema as st, showSkillsHint as t, normalizePublicCliProse as tn, loopbackIssuerWarning as tt, diffStep as u, REQUEST_TIMEOUT_MS as un, CONNECTION_SCHEMA_REF as ut, buildBundle as v, ZitadelError as vn, FLOWS_DIR as vt, toPolicyWireBody as w, readStdin as wt, POLICIES_DIR as x, toZitadelError as xn, VARIABLE_TYPES as xt, PASSWORD_POLICY_OPERATION as y, isErrno$1 as yn, checkFlow as yt, reportClientIdOutcome as z, ciProvider as zt };
5657
+ export { republishCommands as $, processUserAgentFacts as $t, isPolicyTestFile as A, sanitizeResponse as An, checkFlow as At, CommandGroups as B, valuesOfKind as Bt, createReleaseFromBundle as C, parseJsonObject as Cn, checkCustomSlug as Ct, PASSWORD_POLICY_PATH as D, createZitadelClient$1 as Dn, refuseChangedIssuer as Dt, PASSWORD_POLICY_OPERATION as E, REQUEST_TIMEOUT_MS as En, readConnectionFiles as Et, toPolicyWireBody as F, isErrno$1 as Fn, parseVariableValue as Ft, readFlowFiles as G, renderDetail as Gt, authMethods as H, variableInventoryCells as Ht, readJsonDir as I, isMissingEndpoint as In, readStdin as It, SCHEMAS_DIR as J, renderTable as Jt, readSchemaFiles as K, renderList as Kt, readJsonDirEntries as L, toZitadelError as Ln, toInventoryRows as Lt, passwordPolicyFile as M, listenForInterrupt as Mn, VARIABLE_TYPES as Mt, policyFileIssues as N, stopListeningForInterrupt as Nn, assertVariableName as Nt, POLICIES_DIR as O, escapeControlCharacters as On, storedCustomSettings as Ot, policyFileName as P, ZitadelError as Pn, frozenVariables as Pt, republishArgs as Q, installUserAgent as Qt, BRANDING_DIR as R, toVariableRow as Rt, servesPrimary as S, isObject$1 as Sn, IDPS_DIR as St, buildBundle as T, stableStringify as Tn, planConnection as Tt, enabledMethods as U, variableReadScope as Ut, compareInGroup as V, variableCells as Vt, methodsOf as W, variableScope as Wt, reportClientIdOutcome as X, takeReportedWarnings as Xt, publishClientId as Y, reportWarning as Yt, reportSecretOutcome as Z, buildUserAgent as Zt, worktreeIsDirty as _, normalizePublicCliJson as _n, removeSsoFromFlow as _t, serverLogAction as a, nonBlankArg as an, SETUP_FLAG_NAMES as at, createDeployment as b, publicCliCommand as bn, ssoProvidersRefusal as bt, wrapForBox as c, inShell as cn, customFlagText as ct, deployMessage as d, shellCommand as dn, givenCustomFlags as dt, hostAgent as en, storeClientSecret as et, messageFlag as f, unscopedCommand as fn, loopbackIssuerWarning as ft, headSha as g, normalizePublicCliCommands as gn, applySsoToSchema as gt, runSteps as h, maskPublicCliVersion as hn, applySsoToFlow as ht, exploreAllCommands as i, envEnabled as in, SETUP_FLAG_KEYS as it, parsePasswordMinLength as j, interruptSignal as jn, offeredSignInMethods as jt, POLICY_TEST_SUFFIX as k, sanitizeRejection as kn, FLOWS_DIR as kt, DEPLOY_ARGS as l, portableSuggestions as ln, customFlagValues as lt, validateStep as m, isTrainVersion as mn, ssoEnableArgs as mt, brandingGuidanceAction as n, loadOrCreateIdentity as nn, ENABLE_FLAG_KEYS as nt, verifyLoginAction as o, nonBlankString as on, checkGivenCustomFlags as ot, deployInitialRelease as p, isPortableShellWord as pn, selectionFromFlags as pt, selectSchema as q, renderRows as qt, customizeAndPublishActions as r, resolveConsent as rn, ENABLE_FLAG_NAMES as rt, renderBoxActions as s, asEnvironmentSelector as sn, customExample as st, showSkillsHint as t, ciProvider as tn, askConnectionEndpoints as tt, diffStep as u, projectCommand as un, flagText as ut, Repository as v, normalizePublicCliProse as vn, removeSsoFromSchema as vt, revokeRelease as w, setTopLevelJsonKey as wn, credentialVariablesOf as wt, rollbackDeployment as x, sdkVersionForCli as xn, CONNECTION_SCHEMA_REF as xt, RepositoryError as y, optionArgs as yn, ssoEditRefusal as yt, COMMAND_GROUPS as z, toVariableRows as zt };
4347
5658
 
4348
- //# sourceMappingURL=skills-hint-CBoak9HL.mjs.map
5659
+ //# sourceMappingURL=skills-hint-C8oe8cCo.mjs.map