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