@varde-flyt/vfac 0.12.0 → 0.14.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.
Files changed (3) hide show
  1. package/README.md +17 -3
  2. package/dist/vfac.mjs +993 -326
  3. package/package.json +1 -1
package/dist/vfac.mjs CHANGED
@@ -7378,6 +7378,8 @@ var VALUED = /* @__PURE__ */ new Set([
7378
7378
  // an organization and part of a catalogue, silently, once either passed 25.
7379
7379
  "cursor",
7380
7380
  "page-size",
7381
+ // `get operations --resource pri_…`: the history of one resource.
7382
+ "resource",
7381
7383
  "product",
7382
7384
  "name",
7383
7385
  "key",
@@ -7700,263 +7702,7 @@ async function probeMachineApi(args) {
7700
7702
  }
7701
7703
 
7702
7704
  // src/manifest.ts
7703
- var import_yaml = __toESM(require_dist(), 1);
7704
7705
  import { readFileSync as readFileSync2 } from "node:fs";
7705
- function readManifest(path) {
7706
- let text;
7707
- try {
7708
- text = readFileSync2(path, "utf8");
7709
- } catch {
7710
- return { ok: false, message: `Could not read ${path}.` };
7711
- }
7712
- let parsed;
7713
- try {
7714
- parsed = (0, import_yaml.parse)(text);
7715
- } catch (error) {
7716
- return {
7717
- ok: false,
7718
- message: `${path} is not valid YAML or JSON: ${error instanceof Error ? error.message : "unknown error"}`
7719
- };
7720
- }
7721
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
7722
- return { ok: false, message: `${path} does not contain a manifest document.` };
7723
- }
7724
- return { ok: true, document: parsed };
7725
- }
7726
- function resolveSecrets(pairs, environment = process.env) {
7727
- const secrets = {};
7728
- for (const pair of pairs) {
7729
- const separator = pair.indexOf("=");
7730
- if (separator <= 0) {
7731
- return { ok: false, message: `--secret expects name=env:VARIABLE, got "${pair}".` };
7732
- }
7733
- const name = pair.slice(0, separator);
7734
- const source = pair.slice(separator + 1);
7735
- if (!source.startsWith("env:")) {
7736
- return {
7737
- ok: false,
7738
- message: `--secret ${name} must read from the environment: --secret ${name}=env:VARIABLE. A value on the command line is in your shell history and in the process table.`
7739
- };
7740
- }
7741
- const variable = source.slice("env:".length);
7742
- if (variable.length === 0) {
7743
- return { ok: false, message: `--secret ${name}=env: names no variable.` };
7744
- }
7745
- const value = environment[variable];
7746
- if (value === void 0 || value === "") {
7747
- return { ok: false, message: `${variable} is not set, so ${name} has no value.` };
7748
- }
7749
- secrets[name] = value;
7750
- }
7751
- return { ok: true, secrets };
7752
- }
7753
- function manifestKeyOf(document) {
7754
- const metadata = document["metadata"];
7755
- if (typeof metadata !== "object" || metadata === null) return null;
7756
- const key = metadata["key"];
7757
- return typeof key === "string" ? key : null;
7758
- }
7759
-
7760
- // src/endpoint.ts
7761
- function isLoopback(hostname) {
7762
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
7763
- }
7764
- function validateEndpoint(raw, source) {
7765
- let url;
7766
- try {
7767
- url = new URL(raw);
7768
- } catch {
7769
- return { ok: false, message: `${source} is not a URL: "${raw}".` };
7770
- }
7771
- if (url.protocol === "https:") return { ok: true, endpoint: url.origin };
7772
- if (url.protocol === "http:" && isLoopback(url.hostname)) {
7773
- return { ok: true, endpoint: url.origin };
7774
- }
7775
- return {
7776
- ok: false,
7777
- message: `${source} must be https \u2014 a credential is sent to it, and "${url.protocol}//" would send it in the clear. Plain http is accepted for localhost only.`
7778
- };
7779
- }
7780
-
7781
- // src/session.ts
7782
- async function ensureSession(args) {
7783
- const stored = readStored();
7784
- const resolved = resolveEndpoint(args.endpointFlag, stored);
7785
- if (!resolved) {
7786
- return {
7787
- ok: false,
7788
- message: "No endpoint. Run `vfac context set --endpoint https://your-organization.cloud.vardeflyt.no`, or set VFAC_ENDPOINT."
7789
- };
7790
- }
7791
- const checked = validateEndpoint(resolved.value, resolved.source);
7792
- if (!checked.ok) return { ok: false, message: checked.message };
7793
- const endpoint = checked.endpoint;
7794
- const credential = credentialFromEnvironment();
7795
- if (args.forceExchange !== true && sessionUsable(stored.session, endpoint, args.now ?? /* @__PURE__ */ new Date(), credential?.keyId)) {
7796
- return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
7797
- }
7798
- const signedIn = await signIn({
7799
- endpoint,
7800
- ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
7801
- });
7802
- if (!signedIn.ok) return { ok: false, message: signedIn.message };
7803
- const next = { ...stored, session: signedIn.session };
7804
- writeStored(next);
7805
- return {
7806
- ok: true,
7807
- ready: {
7808
- endpoint,
7809
- token: signedIn.session.token,
7810
- stored: next,
7811
- via: "exchanged",
7812
- ...signedIn.gate ? { gate: signedIn.gate } : {}
7813
- }
7814
- };
7815
- }
7816
- function resolveProject(flag, stored) {
7817
- return flag ?? stored.context?.projectId ?? null;
7818
- }
7819
-
7820
- // src/operations.ts
7821
- var OPERATION_STATUSES = [
7822
- "PENDING",
7823
- "RUNNING",
7824
- "SUCCEEDED",
7825
- "FAILED",
7826
- "CANCELLED"
7827
- ];
7828
- var TERMINAL_STATUSES = ["SUCCEEDED", "FAILED", "CANCELLED"];
7829
- var POLL_INTERVAL_MS = 5e3;
7830
- var MAX_WAIT_MS = 60 * 60 * 1e3;
7831
- function withProject(path, projectId) {
7832
- return projectId === null ? path : `${path}?project=${encodeURIComponent(projectId)}`;
7833
- }
7834
- function operationPath(operationId, projectId) {
7835
- return withProject(`/api/v1/operations/${encodeURIComponent(operationId)}`, projectId);
7836
- }
7837
- function classify(data) {
7838
- const status = data["status"];
7839
- if (typeof status !== "string") return "unknown";
7840
- if (TERMINAL_STATUSES.includes(status)) return "terminal";
7841
- if (OPERATION_STATUSES.includes(status)) return "working";
7842
- return "unknown";
7843
- }
7844
- async function readOperation(args) {
7845
- const result = await call({
7846
- endpoint: args.endpoint,
7847
- path: operationPath(args.operationId, args.projectId),
7848
- token: args.token,
7849
- ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
7850
- });
7851
- return result.ok ? { ok: true, data: result.data } : { ok: false, error: result.error };
7852
- }
7853
- async function waitForOperation(args) {
7854
- const deps = args.deps ?? {};
7855
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
7856
- const now = deps.now ?? (() => Date.now());
7857
- const deadline = now() + MAX_WAIT_MS;
7858
- let token = args.token;
7859
- let refreshed = false;
7860
- let last = null;
7861
- for (; ; ) {
7862
- const read = await readOperation({
7863
- endpoint: args.endpoint,
7864
- token,
7865
- operationId: args.operationId,
7866
- projectId: args.projectId,
7867
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
7868
- });
7869
- if (!read.ok) {
7870
- if (read.error.status === 401 && !refreshed && deps.refreshToken) {
7871
- refreshed = true;
7872
- const next = await deps.refreshToken();
7873
- if (next !== null) {
7874
- token = next;
7875
- continue;
7876
- }
7877
- }
7878
- return { kind: "read-failed", error: read.error };
7879
- }
7880
- last = read.data;
7881
- const verdict = classify(read.data);
7882
- if (verdict === "terminal") {
7883
- return { kind: "settled", status: String(read.data["status"]), data: read.data };
7884
- }
7885
- if (verdict === "unknown") {
7886
- return { kind: "unknown-status", status: String(read.data["status"]), data: read.data };
7887
- }
7888
- args.onProgress?.(read.data);
7889
- if (now() + POLL_INTERVAL_MS >= deadline) return { kind: "timed-out", data: last };
7890
- await sleep(POLL_INTERVAL_MS);
7891
- }
7892
- }
7893
- function renderOperation(operationId, data) {
7894
- const status = String(data["status"] ?? "UNKNOWN");
7895
- const phase = data["phase"];
7896
- const lines = [
7897
- typeof phase === "string" && phase !== "" ? `${operationId} ${status} (${phase})` : `${operationId} ${status}`
7898
- ];
7899
- const type = data["type"];
7900
- if (typeof type === "string") lines[0] += ` ${type}`;
7901
- const steps = Array.isArray(data["steps"]) ? data["steps"] : [];
7902
- if (steps.length > 0) {
7903
- lines.push("");
7904
- for (const step of steps) {
7905
- const state = String(step["state"] ?? "");
7906
- lines.push(` ${markerFor(state)} ${String(step["label"] ?? step["key"] ?? "")}`);
7907
- }
7908
- }
7909
- const failure = asRecord(data["failure"]);
7910
- if (failure) {
7911
- lines.push("");
7912
- const stage = failure["stage"];
7913
- lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
7914
- const summary = failure["summary"];
7915
- if (typeof summary === "string") lines.push(` ${summary}`);
7916
- const cause = failure["cause"];
7917
- if (typeof cause === "string") lines.push(` cause: ${cause}`);
7918
- const services = failure["services"];
7919
- if (Array.isArray(services)) {
7920
- for (const entry of services) {
7921
- const one = asRecord(entry);
7922
- if (one === null) continue;
7923
- const dependency = one["dependency"];
7924
- if (typeof dependency !== "string" || dependency === "") continue;
7925
- const code = one["code"];
7926
- lines.push(
7927
- typeof code === "string" && code !== "" ? ` ${dependency} \u2014 the service reported: ${code}` : ` ${dependency} \u2014 the service reported nothing usable`
7928
- );
7929
- }
7930
- }
7931
- const retryable = failure["retryable"];
7932
- if (typeof retryable === "boolean") {
7933
- const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
7934
- const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
7935
- const cause2 = failure["cause"];
7936
- lines.push(
7937
- retryable ? ` Another attempt can succeed: ${again}.` : cause2 === "unknown" ? ` The platform could not classify this, so there is nothing here to correct. Read the whole record with \`vfac get operation ${operationId} --output json\`, and quote that id if you ask for help. Repeating it unchanged fails the same way.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
7938
- );
7939
- }
7940
- }
7941
- return lines.join("\n");
7942
- }
7943
- function markerFor(state) {
7944
- switch (state) {
7945
- case "completed":
7946
- return "\u2713";
7947
- case "failed":
7948
- return "\u2717";
7949
- case "active":
7950
- return "\u203A";
7951
- case "pending":
7952
- return "\xB7";
7953
- default:
7954
- return "-";
7955
- }
7956
- }
7957
- function asRecord(value) {
7958
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
7959
- }
7960
7706
 
7961
7707
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
7962
7708
  var external_exports = {};
@@ -12245,6 +11991,29 @@ var ValueConditionSchema = external_exports.object({
12245
11991
  anyOf: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(20),
12246
11992
  anyRow: external_exports.boolean().optional()
12247
11993
  }).strict();
11994
+ var ValueReferenceSchema = external_exports.object({
11995
+ /**
11996
+ * The setting whose VALUES form the allowed set — a canonical declaration
11997
+ * path, or a bare root key, which is the same string at depth 1.
11998
+ */
11999
+ path: external_exports.string().min(1).max(400)
12000
+ }).strict();
12001
+ var RowCardinalitySchema = external_exports.object({
12002
+ /** A setting declared DIRECTLY on the row. */
12003
+ property: PropertyNameSchema,
12004
+ /** The answers that count toward the limit. Non-empty. */
12005
+ anyOf: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(20),
12006
+ /**
12007
+ * How many rows may answer that way. Zero is meaningful and allowed — "no
12008
+ * row may, under this release" is a thing an upgrade needs to be able to
12009
+ * say.
12010
+ */
12011
+ max: external_exports.number().int().min(0).max(99)
12012
+ }).strict();
12013
+ var RowRulesSchema = external_exports.object({
12014
+ unique: external_exports.array(PropertyNameSchema).min(1).max(MAX_CONFIGURATION_CHILDREN).optional(),
12015
+ atMost: external_exports.array(RowCardinalitySchema).min(1).max(10).optional()
12016
+ }).strict();
12248
12017
  var TitleSchema = external_exports.string().min(2).max(80).describe("Customer-facing field label.");
12249
12018
  var V1_LEAF_BOUNDS = {
12250
12019
  help: 400,
@@ -12318,7 +12087,16 @@ function leafShapes(bounds) {
12318
12087
  * what `requiredSecrets` reports, which is one answer with one reader.
12319
12088
  * Opening it wider is a separate decision with its own blast radius.
12320
12089
  */
12321
- "x-required-when": ValueConditionSchema.optional()
12090
+ "x-required-when": ValueConditionSchema.optional(),
12091
+ /**
12092
+ * The values this one must be chosen from — see {@link ValueReferenceSchema}.
12093
+ *
12094
+ * ON A STRING ONLY, and not on a secret: the platform never reads a
12095
+ * secret's value, so a membership test on one could never be evaluated.
12096
+ * The publish rules say so rather than leaving it to be discovered, the
12097
+ * same way they already do for a condition pointed at a secret.
12098
+ */
12099
+ "x-value-in": ValueReferenceSchema.optional()
12322
12100
  }).strict();
12323
12101
  withJsonSchemaAnnex(string, SECRET_COUPLING_ANNEX);
12324
12102
  const number = external_exports.object({
@@ -12329,14 +12107,23 @@ function leafShapes(bounds) {
12329
12107
  /** Both bounds required — "numeric limits" in the guide is not optional. */
12330
12108
  minimum: external_exports.number(),
12331
12109
  maximum: external_exports.number(),
12332
- default: external_exports.number().optional()
12110
+ default: external_exports.number().optional(),
12111
+ /**
12112
+ * WIDENED FROM SECRETS TO EVERY ROOT SETTING, which this shape's own
12113
+ * comment on the string leaf used to record as a known gap: "a
12114
+ * value-conditioned requirement for an ORDINARY setting is a real gap
12115
+ * too". It is the same keyword, the same evaluator and the same publish
12116
+ * rules; what changes is which settings may carry it.
12117
+ */
12118
+ "x-required-when": ValueConditionSchema.optional()
12333
12119
  }).strict();
12334
12120
  const boolean = external_exports.object({
12335
12121
  type: external_exports.literal("boolean"),
12336
12122
  title: TitleSchema,
12337
12123
  description: help.optional(),
12338
12124
  group: ConfigurationGroupSchema,
12339
- default: external_exports.boolean().optional()
12125
+ default: external_exports.boolean().optional(),
12126
+ "x-required-when": ValueConditionSchema.optional()
12340
12127
  }).strict();
12341
12128
  const items = external_exports.object({
12342
12129
  type: external_exports.literal("string"),
@@ -12353,7 +12140,8 @@ function leafShapes(bounds) {
12353
12140
  /** Required: an unbounded list is an unbounded write. */
12354
12141
  maxItems: external_exports.number().int().min(1).max(100),
12355
12142
  uniqueItems: external_exports.boolean().optional(),
12356
- default: external_exports.array(external_exports.string().max(200)).max(100).optional()
12143
+ default: external_exports.array(external_exports.string().max(200)).max(100).optional(),
12144
+ "x-required-when": ValueConditionSchema.optional()
12357
12145
  }).strict();
12358
12146
  return { string, number, boolean, items, array };
12359
12147
  }
@@ -12373,9 +12161,9 @@ var ConfigurationPropertySchema = external_exports.discriminatedUnion("type", [
12373
12161
  var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true, "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() });
12374
12162
  var V2_NESTED_LEAVES = {
12375
12163
  string: NestedStringPropertySchema,
12376
- number: V2_LEAVES.number.extend({ group: ConfigurationGroupSchema.optional() }),
12377
- boolean: V2_LEAVES.boolean.extend({ group: ConfigurationGroupSchema.optional() }),
12378
- array: V2_LEAVES.array.extend({ group: ConfigurationGroupSchema.optional() }),
12164
+ number: V2_LEAVES.number.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12165
+ boolean: V2_LEAVES.boolean.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12166
+ array: V2_LEAVES.array.omit({ "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() }),
12379
12167
  items: V2_LEAVES.items
12380
12168
  };
12381
12169
  function childBagShape(child) {
@@ -12419,7 +12207,23 @@ function propertyUnion(levelsBelow, leafSet) {
12419
12207
  // discriminator, which is also what keeps a v1 document's
12420
12208
  // `items: {type: 'string'}` reading exactly as it always did.
12421
12209
  leafSet.array.extend({
12422
- items: external_exports.discriminatedUnion("type", [V2_LEAVES.items, groupItems])
12210
+ items: external_exports.discriminatedUnion("type", [V2_LEAVES.items, groupItems]),
12211
+ /**
12212
+ * Rules over the ROWS of this list — see {@link RowRulesSchema}.
12213
+ *
12214
+ * HERE RATHER THAN ON THE LEAF SHAPE, and that placement is the whole
12215
+ * enforcement. `leafShapes().array` is also a v1 list and a last-level
12216
+ * list, both of which hold STRINGS: a rule naming a field of a row would
12217
+ * name nothing there, and `uniqueItems` is already the right keyword for
12218
+ * comparing whole scalar entries. This branch is the only one in the chain
12219
+ * where a list can hold groups, so declaring the keyword here makes "rows
12220
+ * have fields" a property of the shape rather than a rule to remember.
12221
+ *
12222
+ * The refinement still has to refuse a rule on a list of STRINGS, because
12223
+ * `items` is a discriminated union INSIDE this branch — the shape narrows
12224
+ * where a row can exist, not whether this particular list has one.
12225
+ */
12226
+ "x-row-rules": RowRulesSchema.optional()
12423
12227
  }),
12424
12228
  /**
12425
12229
  * A group of related settings.
@@ -13122,6 +12926,9 @@ function canonicalJson(value) {
13122
12926
  }
13123
12927
 
13124
12928
  // ../product-configuration/dist/configuration-tree.js
12929
+ function immediateContainer(node) {
12930
+ return node.containers[node.containers.length - 1] ?? null;
12931
+ }
13125
12932
  function underList(node) {
13126
12933
  return node.containers.some((container) => container.kind === "array");
13127
12934
  }
@@ -13183,6 +12990,11 @@ function walkDeclarations(properties, ancestors, base, onLeaf, onContainer = ()
13183
12990
  walkDeclarations(children.properties, [...ancestors, container], segments, onLeaf, onContainer);
13184
12991
  }
13185
12992
  }
12993
+ function declaredDefault(property) {
12994
+ if (property.type === "object")
12995
+ return void 0;
12996
+ return property.default;
12997
+ }
13186
12998
  function leafAt(schema, path) {
13187
12999
  const declaration = declarationPathOf(path);
13188
13000
  return configurationLeaves(schema).find((leaf) => leaf.path === declaration) ?? null;
@@ -13195,6 +13007,29 @@ function declaresPath(schema, path) {
13195
13007
  function isPlainObject(value) {
13196
13008
  return typeof value === "object" && value !== null && !Array.isArray(value);
13197
13009
  }
13010
+ function expandDeclarationPath(configuration, declaration) {
13011
+ const out = [];
13012
+ const walk = (cursor, remaining, soFar) => {
13013
+ if (remaining.length === 0) {
13014
+ const path = configurationPath(...soFar);
13015
+ if (path !== null)
13016
+ out.push(path);
13017
+ return;
13018
+ }
13019
+ if (Array.isArray(cursor)) {
13020
+ cursor.forEach((element, index) => walk(element, remaining, [...soFar, index]));
13021
+ return;
13022
+ }
13023
+ if (!isPlainObject(cursor))
13024
+ return;
13025
+ const [head, ...rest] = remaining;
13026
+ if (head === void 0)
13027
+ return;
13028
+ walk(cursor[String(head)], rest, [...soFar, head]);
13029
+ };
13030
+ walk(configuration, segmentsOf(declaration), []);
13031
+ return out;
13032
+ }
13198
13033
  function valueAt(configuration, path) {
13199
13034
  let cursor = configuration;
13200
13035
  for (const segment of segmentsOf(path)) {
@@ -13290,6 +13125,7 @@ function validateSubmittedConfiguration(schema, submitted, presence = {}) {
13290
13125
  const secrets = {};
13291
13126
  const configuration = validateObject({ properties: schema.properties, required: schema.required }, input, null, walk, { secrets, supplied });
13292
13127
  drain(walk, crossFieldIssues(schema, supplied, presence, configuration));
13128
+ drain(walk, rowAndReferenceIssues(schema, presence, configuration));
13293
13129
  if (walk.suppressed > 0) {
13294
13130
  walk.issues.push({
13295
13131
  field: "",
@@ -13533,6 +13369,106 @@ function crossFieldIssues(schema, supplied, presence, configuration = {}) {
13533
13369
  }
13534
13370
  return issues;
13535
13371
  }
13372
+ function referenceOf(property) {
13373
+ return property?.["x-value-in"];
13374
+ }
13375
+ function rowRulesOf(property) {
13376
+ return property?.["x-row-rules"];
13377
+ }
13378
+ function comparableValue(value) {
13379
+ if (value === void 0 || value === null)
13380
+ return null;
13381
+ if (typeof value === "string")
13382
+ return value === "" ? null : JSON.stringify(value);
13383
+ if (typeof value === "number" || typeof value === "boolean")
13384
+ return JSON.stringify(value);
13385
+ return null;
13386
+ }
13387
+ function rowAndReferenceIssues(schema, presence, configuration) {
13388
+ const issues = [];
13389
+ const referenced = new Set(presence.referenced ?? []);
13390
+ for (const leaf of configurationLeaves(schema)) {
13391
+ const reference = referenceOf(leaf.property);
13392
+ if (reference === void 0)
13393
+ continue;
13394
+ const targetPath = parseConfigurationPath(reference.path);
13395
+ if (targetPath === null)
13396
+ continue;
13397
+ const allowed = new Set(valuesAt(configuration, targetPath).filter((value) => typeof value === "string"));
13398
+ for (const valuePath of expandDeclarationPath(configuration, leaf.path)) {
13399
+ if (referenced.has(valuePath))
13400
+ continue;
13401
+ const held = valueAt(configuration, valuePath);
13402
+ if (typeof held !== "string" || held === "")
13403
+ continue;
13404
+ if (allowed.has(held))
13405
+ continue;
13406
+ issues.push({
13407
+ field: valuePath,
13408
+ message: `This must be one of the values configured under ${labelOfPath(schema, targetPath)}. ` + (allowed.size === 0 ? "Nothing is configured there yet." : "No entry there currently holds this one.")
13409
+ });
13410
+ }
13411
+ }
13412
+ for (const container of configurationContainers(schema)) {
13413
+ if (container.kind !== "array")
13414
+ continue;
13415
+ const rules = rowRulesOf(container.property);
13416
+ if (rules === void 0)
13417
+ continue;
13418
+ for (const listPath of expandDeclarationPath(configuration, container.path)) {
13419
+ const rows = valueAt(configuration, listPath);
13420
+ if (!Array.isArray(rows))
13421
+ continue;
13422
+ const fieldOf = (index, name) => configurationPath(...pathSegments(listPath), index, name) ?? listPath;
13423
+ for (const name of rules.unique ?? []) {
13424
+ const firstAt = /* @__PURE__ */ new Map();
13425
+ rows.forEach((row, index) => {
13426
+ const held = comparableValue(isPlainObject2(row) ? row[name] : void 0);
13427
+ if (held === null)
13428
+ return;
13429
+ const earlier = firstAt.get(held);
13430
+ if (earlier === void 0) {
13431
+ firstAt.set(held, index);
13432
+ return;
13433
+ }
13434
+ issues.push({
13435
+ field: fieldOf(index, name),
13436
+ message: `${labelOfChild(container, name)} must be different in every entry; entry ${earlier + 1} already uses this one.`
13437
+ });
13438
+ });
13439
+ }
13440
+ for (const rule of rules.atMost ?? []) {
13441
+ let counted = 0;
13442
+ rows.forEach((row, index) => {
13443
+ const held = isPlainObject2(row) ? row[rule.property] : void 0;
13444
+ if (typeof held !== "string" || !rule.anyOf.includes(held))
13445
+ return;
13446
+ counted += 1;
13447
+ if (counted <= rule.max)
13448
+ return;
13449
+ issues.push({
13450
+ field: fieldOf(index, rule.property),
13451
+ message: rule.max === 0 ? `No entry may be set to this.` : `At most ${rule.max} ${rule.max === 1 ? "entry" : "entries"} may be set to this, and ${counted - 1} already ${counted - 1 === 1 ? "is" : "are"}.`
13452
+ });
13453
+ });
13454
+ }
13455
+ }
13456
+ }
13457
+ return issues;
13458
+ }
13459
+ function labelOfPath(schema, path) {
13460
+ const leaf = leafAt(schema, path);
13461
+ const titleOf = (node) => node?.property.title;
13462
+ const field = titleOf(leaf) ?? printableConfigurationPath(path);
13463
+ const container = leaf?.containers[0];
13464
+ const outer = container === void 0 ? void 0 : titleOf(container);
13465
+ return outer === void 0 ? `"${field}"` : `"${field}" in "${outer}"`;
13466
+ }
13467
+ function labelOfChild(container, name) {
13468
+ const items = container.property.items;
13469
+ const title = items?.properties?.[name]?.title;
13470
+ return title === void 0 ? `"${name}"` : `"${title}"`;
13471
+ }
13536
13472
  function conditionOf(property) {
13537
13473
  return property?.["x-required-when"];
13538
13474
  }
@@ -13541,19 +13477,48 @@ function labelOf(schema, name) {
13541
13477
  return property?.title === void 0 ? `"${name}"` : `"${property.title}"`;
13542
13478
  }
13543
13479
  function withoutCrossFieldRules(schema) {
13544
- const conditioned = Object.entries(schema.properties).some(([, property]) => property["x-required-when"] !== void 0);
13545
- if (conditioned) {
13546
- const properties = Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => {
13547
- const { "x-required-when": _dropped, ...bare } = property;
13548
- return [name, bare];
13549
- }));
13550
- return withoutCrossFieldRules({ ...schema, properties });
13551
- }
13552
- if (schema.dependentRequired === void 0)
13553
- return schema;
13554
- const { dependentRequired: _removed, ...rest } = schema;
13480
+ const properties = withoutPropertyRules(schema.properties);
13481
+ const stripped = properties === null ? schema : { ...schema, properties };
13482
+ if (stripped.dependentRequired === void 0)
13483
+ return stripped;
13484
+ const { dependentRequired: _removed, ...rest } = stripped;
13555
13485
  return rest;
13556
13486
  }
13487
+ var PROPERTY_RULE_KEYWORDS = ["x-required-when", "x-value-in", "x-row-rules"];
13488
+ function withoutPropertyRules(properties) {
13489
+ let out = null;
13490
+ const edit = (name) => {
13491
+ out ??= { ...properties };
13492
+ const copy = { ...out[name] };
13493
+ out[name] = copy;
13494
+ return copy;
13495
+ };
13496
+ for (const [name, property] of Object.entries(properties)) {
13497
+ if (!isPlainObject2(property))
13498
+ continue;
13499
+ for (const keyword of PROPERTY_RULE_KEYWORDS) {
13500
+ if (property[keyword] === void 0)
13501
+ continue;
13502
+ delete edit(name)[keyword];
13503
+ }
13504
+ const children = out?.[name] ?? property;
13505
+ if (isPlainObject2(children["properties"])) {
13506
+ const bare = withoutPropertyRules(children["properties"]);
13507
+ if (bare !== null)
13508
+ edit(name)["properties"] = bare;
13509
+ }
13510
+ const items = children["items"];
13511
+ if (isPlainObject2(items) && isPlainObject2(items["properties"])) {
13512
+ const bare = withoutPropertyRules(items["properties"]);
13513
+ if (bare !== null)
13514
+ edit(name)["items"] = { ...items, properties: bare };
13515
+ }
13516
+ }
13517
+ return out;
13518
+ }
13519
+ function declaredSecretNames(schema) {
13520
+ return Object.entries(schema.properties).filter(([, property]) => isSecretProperty(property)).map(([name]) => name).sort();
13521
+ }
13557
13522
  function defaultOf(property) {
13558
13523
  switch (property.type) {
13559
13524
  case "string":
@@ -14760,11 +14725,19 @@ function checkProductDefinition(definition, ctx) {
14760
14725
  if (condition === void 0)
14761
14726
  continue;
14762
14727
  const at = ["configuration", "properties", name, "x-required-when"];
14763
- if (!isSecretProperty(property)) {
14728
+ if ((definition.configuration.required ?? []).includes(name)) {
14729
+ ctx.addIssue({
14730
+ code: external_exports.ZodIssueCode.custom,
14731
+ path: [...at],
14732
+ message: `"${name}" is already required, so "x-required-when" can only narrow a rule that is already unconditional. Drop one: \`required\` is always, \`x-required-when\` is the condition.`
14733
+ });
14734
+ continue;
14735
+ }
14736
+ if (declaredDefault(property) !== void 0) {
14764
14737
  ctx.addIssue({
14765
14738
  code: external_exports.ZodIssueCode.custom,
14766
14739
  path: [...at],
14767
- message: `"${name}" is not a secret, so "x-required-when" has nothing to report through. Declare it "x-secret": true with "writeOnly": true, or use "dependentRequired" to require one ordinary setting alongside another.`
14740
+ message: `"${name}" declares a default, so it is never absent and this condition could never make it required. Remove the default, or the condition.`
14768
14741
  });
14769
14742
  continue;
14770
14743
  }
@@ -14840,6 +14813,8 @@ function checkProductDefinition(definition, ctx) {
14840
14813
  });
14841
14814
  }
14842
14815
  }
14816
+ checkValueReferences(definition.configuration, ctx);
14817
+ checkRowRules(definition.configuration, ctx);
14843
14818
  const provides = definition.serviceInterfaces?.provides ?? [];
14844
14819
  const provideKeys = provides.map((provide) => provide.key);
14845
14820
  const duplicateProvideKeys = provideKeys.filter((key, i) => provideKeys.indexOf(key) !== i);
@@ -14951,6 +14926,203 @@ function checkProductDefinition(definition, ctx) {
14951
14926
  });
14952
14927
  }
14953
14928
  }
14929
+ function declarationIssuePath(node) {
14930
+ const out = ["configuration"];
14931
+ for (const container of node.containers) {
14932
+ out.push("properties", container.name);
14933
+ if (container.kind === "array")
14934
+ out.push("items");
14935
+ }
14936
+ out.push("properties", node.name);
14937
+ return out;
14938
+ }
14939
+ function directChildren(leaves, container) {
14940
+ return leaves.filter((leaf) => immediateContainer(leaf)?.path === container.path);
14941
+ }
14942
+ function valuesOutsideSpace(configuration, leaf, values) {
14943
+ const single = withoutCrossFieldRules({
14944
+ ...configuration,
14945
+ properties: { [leaf.name]: leaf.property },
14946
+ required: [leaf.name]
14947
+ });
14948
+ return values.filter((value) => !validateSubmittedConfiguration(single, { [leaf.name]: value }).ok);
14949
+ }
14950
+ function checkValueReferences(configuration, ctx) {
14951
+ const leaves = configurationLeaves(configuration);
14952
+ for (const leaf of leaves) {
14953
+ const reference = leaf.property["x-value-in"];
14954
+ if (reference === void 0)
14955
+ continue;
14956
+ const at = [...declarationIssuePath(leaf), "x-value-in"];
14957
+ const here = printableConfigurationPath(leaf.path);
14958
+ if (isSecretProperty(leaf.property)) {
14959
+ ctx.addIssue({
14960
+ code: external_exports.ZodIssueCode.custom,
14961
+ path: at,
14962
+ message: `"${here}" is a secret. The platform never reads a secret's value, so it could never check one against a set of values.`
14963
+ });
14964
+ continue;
14965
+ }
14966
+ if (leaf.property.enum !== void 0) {
14967
+ ctx.addIssue({
14968
+ code: external_exports.ZodIssueCode.custom,
14969
+ path: at,
14970
+ message: `"${here}" already fixes its values with "enum". A setting takes its values from the contract or from another setting, not from both.`
14971
+ });
14972
+ continue;
14973
+ }
14974
+ if (declaredDefault(leaf.property) !== void 0) {
14975
+ ctx.addIssue({
14976
+ code: external_exports.ZodIssueCode.custom,
14977
+ path: at,
14978
+ message: `"${here}" declares a default, which would be written into every row and then refused until the setting it points at happens to hold the same value. Remove the default.`
14979
+ });
14980
+ continue;
14981
+ }
14982
+ const parsed = parseConfigurationPath(reference.path);
14983
+ const target = parsed === null ? null : leafAt(configuration, parsed);
14984
+ if (target === null) {
14985
+ const isGroup = parsed !== null && declaresPath(configuration, parsed);
14986
+ ctx.addIssue({
14987
+ code: external_exports.ZodIssueCode.custom,
14988
+ path: [...at, "path"],
14989
+ message: isGroup ? `"${reference.path}" is a group of settings, which holds no value to choose from. Name one setting inside it.` : `"${reference.path}" is not a declared customer configuration property.`
14990
+ });
14991
+ continue;
14992
+ }
14993
+ if (target.path === leaf.path) {
14994
+ ctx.addIssue({
14995
+ code: external_exports.ZodIssueCode.custom,
14996
+ path: [...at, "path"],
14997
+ message: `"${here}" cannot take its values from itself.`
14998
+ });
14999
+ continue;
15000
+ }
15001
+ if (target.property.type !== "string") {
15002
+ ctx.addIssue({
15003
+ code: external_exports.ZodIssueCode.custom,
15004
+ path: [...at, "path"],
15005
+ message: `"${reference.path}" is a ${target.property.type} setting; a reference compares string values.`
15006
+ });
15007
+ continue;
15008
+ }
15009
+ if (isSecretProperty(target.property)) {
15010
+ ctx.addIssue({
15011
+ code: external_exports.ZodIssueCode.custom,
15012
+ path: [...at, "path"],
15013
+ message: `"${reference.path}" is a secret. The platform never reads a secret's value, so it could never be the set a reference is checked against.`
15014
+ });
15015
+ continue;
15016
+ }
15017
+ }
15018
+ }
15019
+ function checkRowRules(configuration, ctx) {
15020
+ const leaves = configurationLeaves(configuration);
15021
+ for (const leaf of leaves) {
15022
+ if (leaf.property["x-row-rules"] === void 0)
15023
+ continue;
15024
+ ctx.addIssue({
15025
+ code: external_exports.ZodIssueCode.custom,
15026
+ path: [...declarationIssuePath(leaf), "x-row-rules"],
15027
+ message: `"${printableConfigurationPath(leaf.path)}" is a list of text entries, whose rows have no fields to name. Use "uniqueItems" to compare whole entries, or declare "items" as a group.`
15028
+ });
15029
+ }
15030
+ for (const container of configurationContainers(configuration)) {
15031
+ const rules = container.property["x-row-rules"];
15032
+ if (rules === void 0)
15033
+ continue;
15034
+ const at = [...declarationIssuePath(container), "x-row-rules"];
15035
+ const here = printableConfigurationPath(container.path);
15036
+ if (rules.unique === void 0 && rules.atMost === void 0) {
15037
+ ctx.addIssue({
15038
+ code: external_exports.ZodIssueCode.custom,
15039
+ path: at,
15040
+ message: `"${here}" declares "x-row-rules" with no rule in it. Declare "unique", "atMost", or both.`
15041
+ });
15042
+ continue;
15043
+ }
15044
+ const children = directChildren(leaves, container);
15045
+ const childAt = (name) => children.find((child) => child.name === name);
15046
+ const maxItems = container.property.maxItems;
15047
+ const seen = /* @__PURE__ */ new Set();
15048
+ (rules.unique ?? []).forEach((name, index) => {
15049
+ if (seen.has(name)) {
15050
+ ctx.addIssue({
15051
+ code: external_exports.ZodIssueCode.custom,
15052
+ path: [...at, "unique", index],
15053
+ message: `"${name}" is named twice.`
15054
+ });
15055
+ return;
15056
+ }
15057
+ seen.add(name);
15058
+ const child = childAt(name);
15059
+ if (child === void 0) {
15060
+ ctx.addIssue({
15061
+ code: external_exports.ZodIssueCode.custom,
15062
+ path: [...at, "unique", index],
15063
+ message: `"${name}" is not a setting declared on a row of "${here}".`
15064
+ });
15065
+ return;
15066
+ }
15067
+ if (child.property.type === "array") {
15068
+ ctx.addIssue({
15069
+ code: external_exports.ZodIssueCode.custom,
15070
+ path: [...at, "unique", index],
15071
+ message: `"${name}" is a list, which holds no single value to compare across rows.`
15072
+ });
15073
+ }
15074
+ if (child.property.type === "boolean") {
15075
+ ctx.addIssue({
15076
+ code: external_exports.ZodIssueCode.custom,
15077
+ path: [...at, "unique", index],
15078
+ message: `"${name}" is a true/false setting, so requiring it to be unique would cap "${here}" at two rows rather than make anything distinct. Use "atMost" to limit how many rows may answer one way.`
15079
+ });
15080
+ }
15081
+ });
15082
+ (rules.atMost ?? []).forEach((rule, index) => {
15083
+ const ruleAt = [...at, "atMost", index];
15084
+ const child = childAt(rule.property);
15085
+ if (child === void 0) {
15086
+ ctx.addIssue({
15087
+ code: external_exports.ZodIssueCode.custom,
15088
+ path: [...ruleAt, "property"],
15089
+ message: `"${rule.property}" is not a setting declared on a row of "${here}".`
15090
+ });
15091
+ return;
15092
+ }
15093
+ if (child.property.type !== "string") {
15094
+ ctx.addIssue({
15095
+ code: external_exports.ZodIssueCode.custom,
15096
+ path: [...ruleAt, "property"],
15097
+ message: `"${rule.property}" is a ${child.property.type} setting; a row rule compares string values.`
15098
+ });
15099
+ return;
15100
+ }
15101
+ if (new Set(rule.anyOf).size !== rule.anyOf.length) {
15102
+ ctx.addIssue({
15103
+ code: external_exports.ZodIssueCode.custom,
15104
+ path: [...ruleAt, "anyOf"],
15105
+ message: "Repeated values in `anyOf`."
15106
+ });
15107
+ }
15108
+ if (maxItems !== void 0 && rule.max >= maxItems) {
15109
+ ctx.addIssue({
15110
+ code: external_exports.ZodIssueCode.custom,
15111
+ path: [...ruleAt, "max"],
15112
+ message: `"${here}" holds at most ${maxItems} rows, so a limit of ${rule.max} could never be exceeded. Lower it, or remove the rule.`
15113
+ });
15114
+ }
15115
+ const outside = valuesOutsideSpace(configuration, child, rule.anyOf);
15116
+ if (outside.length > 0) {
15117
+ ctx.addIssue({
15118
+ code: external_exports.ZodIssueCode.custom,
15119
+ path: [...ruleAt, "anyOf"],
15120
+ message: `"${rule.property}" never accepts ${outside.map((value) => `"${value}"`).join(", ")}, so this rule could never count a row.`
15121
+ });
15122
+ }
15123
+ });
15124
+ }
15125
+ }
14954
15126
 
14955
15127
  // ../product-contracts/dist/announcements.js
14956
15128
  var ANNOUNCED_PRODUCTS = Object.freeze([
@@ -16846,7 +17018,23 @@ function isTerminalAction(action) {
16846
17018
  var ManifestActionSchema = external_exports.enum(MANIFEST_ACTIONS);
16847
17019
 
16848
17020
  // ../product-contracts/dist/contract-issue.js
17021
+ var PATH_SEGMENT = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
17022
+ var UNPRINTABLE_SEGMENT = "<unprintable>";
16849
17023
  var MAX_SEGMENTS = MAX_CONFIGURATION_PATH_SEGMENTS + 2;
17024
+ function printableContractPath(path) {
17025
+ const printable = path.slice(0, MAX_SEGMENTS).map((segment) => typeof segment === "number" || PATH_SEGMENT.test(segment) ? String(segment) : UNPRINTABLE_SEGMENT);
17026
+ return printable.join(".") || "(document)";
17027
+ }
17028
+ function manifestConfigurationField(field) {
17029
+ if (field === "")
17030
+ return "spec.configuration";
17031
+ const parsed = parseConfigurationPath(field);
17032
+ return printableContractPath([
17033
+ "spec",
17034
+ "configuration",
17035
+ ...parsed === null ? [field] : pathSegments(parsed)
17036
+ ]);
17037
+ }
16850
17038
 
16851
17039
  // ../product-contracts/dist/resource-output-reference.js
16852
17040
  var OUTPUT_KEY_RULE = "Output keys are camelCase.";
@@ -16860,6 +17048,9 @@ var ResourceOutputReferenceSchema = external_exports.object({
16860
17048
  }).strict()
16861
17049
  }).strict()
16862
17050
  }).strict();
17051
+ function looksLikeReference(value) {
17052
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "valueFrom" in value;
17053
+ }
16863
17054
 
16864
17055
  // ../product-contracts/dist/dependency-binding.js
16865
17056
  var DependencyBindingSelectorSchema = external_exports.object({
@@ -16995,6 +17186,23 @@ var ProductInstanceTemplateSchema = external_exports.object({
16995
17186
  dependencyBindings: DependencyBindingsSchema.optional()
16996
17187
  }).strict()
16997
17188
  }).strict();
17189
+ function secretValuesInDocument(schema, configuration) {
17190
+ if (!isPlainRecord(configuration) || !isPlainRecord(schema.properties))
17191
+ return [];
17192
+ const declarations = Object.fromEntries(Object.entries(schema.properties).filter(([, property]) => isPlainRecord(property)));
17193
+ return declaredSecretNames({
17194
+ properties: declarations
17195
+ }).filter((name) => Object.hasOwn(configuration, name) && !looksLikeReference(configuration[name]));
17196
+ }
17197
+ function secretValueIssue(name) {
17198
+ return {
17199
+ field: manifestConfigurationField(name),
17200
+ message: `"${name}" is secret input and must never appear in a template. Supply it separately when applying, so it goes to the secret manager rather than into a document.`
17201
+ };
17202
+ }
17203
+ function isPlainRecord(value) {
17204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17205
+ }
16998
17206
 
16999
17207
  // ../product-contracts/dist/product-submission.schema.js
17000
17208
  var PRODUCT_SUBMISSION_KIND = "ProductSubmission";
@@ -17094,50 +17302,383 @@ var PUBLISH_RULE_ANNEX = {
17094
17302
  };
17095
17303
  withJsonSchemaAnnex(ProductSubmissionSchema, PUBLISH_RULE_ANNEX);
17096
17304
 
17097
- // src/render.ts
17098
- function isUnconverged(plan) {
17099
- return plan.health !== void 0 && !plan.health.converged;
17100
- }
17101
- function secretWriteConsequence(plan) {
17102
- if ((plan.secretsToWrite ?? []).length === 0) return [];
17103
- const lines = ["the value you send REPLACES the stored one, and the resource redeploys."];
17104
- if (plan.action === "UPDATE" && (plan.changes ?? []).length === 0) {
17105
- lines.push("Nothing else changed \u2014 sending a secret is itself the change.");
17106
- lines.push(
17107
- "Omit --secret on a run that is not setting or changing one; that is what answers NO_CHANGE."
17108
- );
17109
- }
17110
- return lines;
17111
- }
17112
- function printPlan(plan, json) {
17113
- if (json) {
17114
- process.stdout.write(`${JSON.stringify(plan)}
17115
- `);
17116
- return;
17305
+ // src/manifest.ts
17306
+ var import_yaml = __toESM(require_dist(), 1);
17307
+ function readManifest(path) {
17308
+ let text;
17309
+ try {
17310
+ text = readFileSync2(path, "utf8");
17311
+ } catch {
17312
+ return { ok: false, message: `Could not read ${path}.` };
17117
17313
  }
17118
- const lines = [`${plan.action}${plan.manifestKey ? ` ${plan.manifestKey}` : ""}`];
17119
- for (const change of plan.changes ?? []) {
17120
- lines.push(` ~ ${change.field}`);
17121
- lines.push(` ${change.from} \u2192 ${change.to}`);
17314
+ let parsed;
17315
+ try {
17316
+ parsed = (0, import_yaml.parse)(text, { prettyErrors: false });
17317
+ } catch (error) {
17318
+ return {
17319
+ ok: false,
17320
+ message: `${path} is not valid YAML or JSON${whereItFailed(text, error)}.`
17321
+ };
17122
17322
  }
17123
- for (const reference of plan.references ?? []) {
17124
- lines.push(` ${reference.property}`);
17125
- lines.push(
17126
- ` from ${reference.resourceKey}.${reference.output}${reference.secret ? " [secret]" : ""}`
17127
- );
17323
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
17324
+ return { ok: false, message: `${path} does not contain a manifest document.` };
17128
17325
  }
17129
- const bindings = plan.dependencyBindings ?? [];
17130
- if (bindings.length > 0) {
17131
- lines.push(" dependency bindings:");
17132
- for (const binding of bindings) {
17133
- lines.push(` ${binding.dependencyKey}`);
17134
- lines.push(
17135
- ` resource: ${binding.resourceKey}${binding.projectId ? ` (in ${binding.projectId})` : ""}`
17136
- );
17137
- if (binding.interfaceId !== void 0) {
17138
- lines.push(
17139
- ` interface: ${binding.interfaceId}${binding.interfaceVersion ? `/${binding.interfaceVersion}` : ""}`
17140
- );
17326
+ return { ok: true, document: parsed };
17327
+ }
17328
+ function resolveSecrets(pairs, environment = process.env) {
17329
+ const secrets = {};
17330
+ for (const [index, pair] of pairs.entries()) {
17331
+ const label = pairs.length === 1 ? "--secret" : `--secret #${index + 1}`;
17332
+ const separator = pair.indexOf("=");
17333
+ if (separator <= 0) {
17334
+ return {
17335
+ ok: false,
17336
+ message: `${label} expects name=env:VARIABLE, and there is no name before "=".`
17337
+ };
17338
+ }
17339
+ const name = pair.slice(0, separator);
17340
+ const source = pair.slice(separator + 1);
17341
+ if (!source.startsWith("env:")) {
17342
+ return {
17343
+ ok: false,
17344
+ message: `${label} must read from the environment: --secret <name>=env:VARIABLE. A value on the command line is in your shell history and in the process table.`
17345
+ };
17346
+ }
17347
+ const variable = source.slice("env:".length);
17348
+ if (variable.length === 0) {
17349
+ return { ok: false, message: `${label} names no variable after "env:".` };
17350
+ }
17351
+ const value = environment[variable];
17352
+ if (value === void 0 || value === "") {
17353
+ return {
17354
+ ok: false,
17355
+ message: `${label} reads an environment variable that is not set, or is empty.`
17356
+ };
17357
+ }
17358
+ secrets[name] = value;
17359
+ }
17360
+ return { ok: true, secrets };
17361
+ }
17362
+ function manifestKeyOf(document) {
17363
+ const metadata = document["metadata"];
17364
+ if (typeof metadata !== "object" || metadata === null) return null;
17365
+ const key = metadata["key"];
17366
+ return typeof key === "string" ? key : null;
17367
+ }
17368
+ function whereItFailed(text, error) {
17369
+ if (typeof error !== "object" || error === null) return "";
17370
+ const code = error.code;
17371
+ const pos = error.pos;
17372
+ const kind = typeof code === "string" && /^[A-Z_]+$/.test(code) ? code : null;
17373
+ const offset = Array.isArray(pos) && typeof pos[0] === "number" ? pos[0] : null;
17374
+ if (offset === null) return kind ? ` (${kind})` : "";
17375
+ const before = text.slice(0, offset);
17376
+ const line = before.split("\n").length;
17377
+ const column = offset - before.lastIndexOf("\n");
17378
+ return ` (${kind ?? "error"} at line ${line}, column ${column})`;
17379
+ }
17380
+ function pinnedRelease(document) {
17381
+ const spec = document["spec"];
17382
+ if (typeof spec !== "object" || spec === null || Array.isArray(spec)) return null;
17383
+ const { productId, productVersion } = spec;
17384
+ if (typeof productId !== "string" || productId === "") return null;
17385
+ if (typeof productVersion !== "string" || productVersion === "") return null;
17386
+ return { productId, productVersion };
17387
+ }
17388
+ function carriesLiteralSettings(document) {
17389
+ const configuration = configurationOf(document);
17390
+ return configuration !== null && Object.values(configuration).some((value) => !looksLikeReference(value));
17391
+ }
17392
+ function pastedSecretSettings(document, view) {
17393
+ const pinned = pinnedRelease(document);
17394
+ if (!pinned || typeof view !== "object" || view === null) return [];
17395
+ const { productId, version, configuration } = view;
17396
+ if (productId !== pinned.productId || version !== pinned.productVersion) return [];
17397
+ if (typeof configuration !== "object" || configuration === null) return [];
17398
+ try {
17399
+ return secretValuesInDocument(
17400
+ configuration,
17401
+ configurationOf(document)
17402
+ );
17403
+ } catch {
17404
+ return [];
17405
+ }
17406
+ }
17407
+ function configurationOf(document) {
17408
+ const spec = document["spec"];
17409
+ if (typeof spec !== "object" || spec === null || Array.isArray(spec)) return null;
17410
+ const configuration = spec["configuration"];
17411
+ if (typeof configuration !== "object" || configuration === null || Array.isArray(configuration)) {
17412
+ return null;
17413
+ }
17414
+ return configuration;
17415
+ }
17416
+
17417
+ // src/endpoint.ts
17418
+ function isLoopback(hostname) {
17419
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
17420
+ }
17421
+ function validateEndpoint(raw, source) {
17422
+ let url;
17423
+ try {
17424
+ url = new URL(raw);
17425
+ } catch {
17426
+ return { ok: false, message: `${source} is not a URL: "${raw}".` };
17427
+ }
17428
+ if (url.protocol === "https:") return { ok: true, endpoint: url.origin };
17429
+ if (url.protocol === "http:" && isLoopback(url.hostname)) {
17430
+ return { ok: true, endpoint: url.origin };
17431
+ }
17432
+ return {
17433
+ ok: false,
17434
+ message: `${source} must be https \u2014 a credential is sent to it, and "${url.protocol}//" would send it in the clear. Plain http is accepted for localhost only.`
17435
+ };
17436
+ }
17437
+
17438
+ // src/session.ts
17439
+ async function ensureSession(args) {
17440
+ const stored = readStored();
17441
+ const resolved = resolveEndpoint(args.endpointFlag, stored);
17442
+ if (!resolved) {
17443
+ return {
17444
+ ok: false,
17445
+ message: "No endpoint. Run `vfac context set --endpoint https://your-organization.cloud.vardeflyt.no`, or set VFAC_ENDPOINT."
17446
+ };
17447
+ }
17448
+ const checked = validateEndpoint(resolved.value, resolved.source);
17449
+ if (!checked.ok) return { ok: false, message: checked.message };
17450
+ const endpoint = checked.endpoint;
17451
+ const credential = credentialFromEnvironment();
17452
+ if (args.forceExchange !== true && sessionUsable(stored.session, endpoint, args.now ?? /* @__PURE__ */ new Date(), credential?.keyId)) {
17453
+ return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
17454
+ }
17455
+ const signedIn = await signIn({
17456
+ endpoint,
17457
+ ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
17458
+ });
17459
+ if (!signedIn.ok) return { ok: false, message: signedIn.message };
17460
+ const next = { ...stored, session: signedIn.session };
17461
+ writeStored(next);
17462
+ return {
17463
+ ok: true,
17464
+ ready: {
17465
+ endpoint,
17466
+ token: signedIn.session.token,
17467
+ stored: next,
17468
+ via: "exchanged",
17469
+ ...signedIn.gate ? { gate: signedIn.gate } : {}
17470
+ }
17471
+ };
17472
+ }
17473
+ function resolveProject(flag, stored) {
17474
+ return flag ?? stored.context?.projectId ?? null;
17475
+ }
17476
+
17477
+ // src/operations.ts
17478
+ var OPERATION_STATUSES = [
17479
+ "PENDING",
17480
+ "RUNNING",
17481
+ "SUCCEEDED",
17482
+ "FAILED",
17483
+ "CANCELLED"
17484
+ ];
17485
+ var TERMINAL_STATUSES = ["SUCCEEDED", "FAILED", "CANCELLED"];
17486
+ var POLL_INTERVAL_MS = 5e3;
17487
+ var MAX_WAIT_MS = 60 * 60 * 1e3;
17488
+ function withProject(path, projectId) {
17489
+ return projectId === null ? path : `${path}?project=${encodeURIComponent(projectId)}`;
17490
+ }
17491
+ function operationPath(operationId, projectId) {
17492
+ return withProject(`/api/v1/operations/${encodeURIComponent(operationId)}`, projectId);
17493
+ }
17494
+ function classify(data) {
17495
+ const status = data["status"];
17496
+ if (typeof status !== "string") return "unknown";
17497
+ if (TERMINAL_STATUSES.includes(status)) return "terminal";
17498
+ if (OPERATION_STATUSES.includes(status)) return "working";
17499
+ return "unknown";
17500
+ }
17501
+ async function readOperation(args) {
17502
+ const result = await call({
17503
+ endpoint: args.endpoint,
17504
+ path: operationPath(args.operationId, args.projectId),
17505
+ token: args.token,
17506
+ ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {}
17507
+ });
17508
+ return result.ok ? { ok: true, data: result.data } : { ok: false, error: result.error };
17509
+ }
17510
+ async function waitForOperation(args) {
17511
+ const deps = args.deps ?? {};
17512
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
17513
+ const now = deps.now ?? (() => Date.now());
17514
+ const deadline = now() + MAX_WAIT_MS;
17515
+ let token = args.token;
17516
+ let refreshed = false;
17517
+ let last = null;
17518
+ for (; ; ) {
17519
+ const read = await readOperation({
17520
+ endpoint: args.endpoint,
17521
+ token,
17522
+ operationId: args.operationId,
17523
+ projectId: args.projectId,
17524
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
17525
+ });
17526
+ if (!read.ok) {
17527
+ if (read.error.status === 401 && !refreshed && deps.refreshToken) {
17528
+ refreshed = true;
17529
+ const next = await deps.refreshToken();
17530
+ if (next !== null) {
17531
+ token = next;
17532
+ continue;
17533
+ }
17534
+ }
17535
+ return { kind: "read-failed", error: read.error };
17536
+ }
17537
+ last = read.data;
17538
+ const verdict = classify(read.data);
17539
+ if (verdict === "terminal") {
17540
+ return { kind: "settled", status: String(read.data["status"]), data: read.data };
17541
+ }
17542
+ if (verdict === "unknown") {
17543
+ return { kind: "unknown-status", status: String(read.data["status"]), data: read.data };
17544
+ }
17545
+ args.onProgress?.(read.data);
17546
+ if (now() + POLL_INTERVAL_MS >= deadline) return { kind: "timed-out", data: last };
17547
+ await sleep(POLL_INTERVAL_MS);
17548
+ }
17549
+ }
17550
+ function renderOperation(operationId, data) {
17551
+ const status = String(data["status"] ?? "UNKNOWN");
17552
+ const phase = data["phase"];
17553
+ const lines = [
17554
+ typeof phase === "string" && phase !== "" ? `${operationId} ${status} (${phase})` : `${operationId} ${status}`
17555
+ ];
17556
+ const type = data["type"];
17557
+ if (typeof type === "string") lines[0] += ` ${type}`;
17558
+ const steps = Array.isArray(data["steps"]) ? data["steps"] : [];
17559
+ if (steps.length > 0) {
17560
+ lines.push("");
17561
+ for (const step of steps) {
17562
+ const state = String(step["state"] ?? "");
17563
+ lines.push(` ${markerFor(state)} ${String(step["label"] ?? step["key"] ?? "")}`);
17564
+ }
17565
+ }
17566
+ const failure = asRecord(data["failure"]);
17567
+ if (failure) {
17568
+ lines.push("");
17569
+ const stage = failure["stage"];
17570
+ lines.push(` Failed${typeof stage === "string" ? ` at ${stage}` : ""}`);
17571
+ const summary = failure["summary"];
17572
+ if (typeof summary === "string") lines.push(` ${summary}`);
17573
+ const cause = failure["cause"];
17574
+ if (typeof cause === "string") lines.push(` cause: ${cause}`);
17575
+ const services = failure["services"];
17576
+ if (Array.isArray(services)) {
17577
+ for (const entry of services) {
17578
+ const one = asRecord(entry);
17579
+ if (one === null) continue;
17580
+ const dependency = one["dependency"];
17581
+ if (typeof dependency !== "string" || dependency === "") continue;
17582
+ const code = one["code"];
17583
+ lines.push(
17584
+ typeof code === "string" && code !== "" ? ` ${dependency} \u2014 the service reported: ${code}` : ` ${dependency} \u2014 the service reported nothing usable`
17585
+ );
17586
+ }
17587
+ }
17588
+ const product = failure["product"];
17589
+ const reportedSettings = [];
17590
+ if (Array.isArray(product)) {
17591
+ for (const entry of product) {
17592
+ const one = asRecord(entry);
17593
+ if (one === null) continue;
17594
+ const component = one["component"];
17595
+ const code = one["code"];
17596
+ if (typeof component !== "string" || typeof code !== "string") continue;
17597
+ if (component === "" || code === "") continue;
17598
+ const setting = one["setting"];
17599
+ if (typeof setting === "string" && setting !== "") {
17600
+ reportedSettings.push(setting);
17601
+ lines.push(` ${component} refused to start: ${code} (setting: ${setting})`);
17602
+ } else {
17603
+ lines.push(` ${component} refused to start: ${code}`);
17604
+ }
17605
+ }
17606
+ }
17607
+ const retryable = failure["retryable"];
17608
+ if (typeof retryable === "boolean") {
17609
+ const again = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "`vfac lifecycle upgrade <pri_\u2026>`" : "`vfac lifecycle reconcile <pri_\u2026>`";
17610
+ const changeIt = data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? "" : ", or `vfac apply` a corrected manifest";
17611
+ const cause2 = failure["cause"];
17612
+ const named = reportedSettings[0];
17613
+ lines.push(
17614
+ retryable ? ` Another attempt can succeed: ${again}.` : named !== void 0 ? ` Correct \`${named}\` in your manifest, then \`vfac apply\` it${data["type"] === "PRODUCT_INSTANCE_UPGRADE" ? ` and ${again}` : ""}. Repeating it unchanged fails the same way.` : cause2 === "unknown" ? ` The platform could not classify this, so there is nothing here to correct. Read the whole record with \`vfac get operation ${operationId} --output json\`, and quote that id if you ask for help. Repeating it unchanged fails the same way.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
17615
+ );
17616
+ }
17617
+ }
17618
+ return lines.join("\n");
17619
+ }
17620
+ function markerFor(state) {
17621
+ switch (state) {
17622
+ case "completed":
17623
+ return "\u2713";
17624
+ case "failed":
17625
+ return "\u2717";
17626
+ case "active":
17627
+ return "\u203A";
17628
+ case "pending":
17629
+ return "\xB7";
17630
+ default:
17631
+ return "-";
17632
+ }
17633
+ }
17634
+ function asRecord(value) {
17635
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
17636
+ }
17637
+
17638
+ // src/render.ts
17639
+ function isUnconverged(plan) {
17640
+ return plan.health !== void 0 && !plan.health.converged;
17641
+ }
17642
+ function secretWriteConsequence(plan) {
17643
+ if ((plan.secretsToWrite ?? []).length === 0) return [];
17644
+ const lines = ["the value you send REPLACES the stored one, and the resource redeploys."];
17645
+ if (plan.action === "UPDATE" && (plan.changes ?? []).length === 0) {
17646
+ lines.push("Nothing else changed \u2014 sending a secret is itself the change.");
17647
+ lines.push(
17648
+ "Omit --secret on a run that is not setting or changing one; that is what answers NO_CHANGE."
17649
+ );
17650
+ }
17651
+ return lines;
17652
+ }
17653
+ function printPlan(plan, json) {
17654
+ if (json) {
17655
+ process.stdout.write(`${JSON.stringify(plan)}
17656
+ `);
17657
+ return;
17658
+ }
17659
+ const lines = [`${plan.action}${plan.manifestKey ? ` ${plan.manifestKey}` : ""}`];
17660
+ for (const change of plan.changes ?? []) {
17661
+ lines.push(` ~ ${change.field}`);
17662
+ lines.push(` ${change.from} \u2192 ${change.to}`);
17663
+ }
17664
+ for (const reference of plan.references ?? []) {
17665
+ lines.push(` ${reference.property}`);
17666
+ lines.push(
17667
+ ` from ${reference.resourceKey}.${reference.output}${reference.secret ? " [secret]" : ""}`
17668
+ );
17669
+ }
17670
+ const bindings = plan.dependencyBindings ?? [];
17671
+ if (bindings.length > 0) {
17672
+ lines.push(" dependency bindings:");
17673
+ for (const binding of bindings) {
17674
+ lines.push(` ${binding.dependencyKey}`);
17675
+ lines.push(
17676
+ ` resource: ${binding.resourceKey}${binding.projectId ? ` (in ${binding.projectId})` : ""}`
17677
+ );
17678
+ if (binding.interfaceId !== void 0) {
17679
+ lines.push(
17680
+ ` interface: ${binding.interfaceId}${binding.interfaceVersion ? `/${binding.interfaceVersion}` : ""}`
17681
+ );
17141
17682
  }
17142
17683
  }
17143
17684
  }
@@ -17206,6 +17747,12 @@ async function runApply(verb, parsed, deps = {}) {
17206
17747
  return 1;
17207
17748
  }
17208
17749
  const json = parsed.flags["output"] === "json";
17750
+ const pasted = await pastedSecretsIn(manifest2.document, {
17751
+ endpoint: session.ready.endpoint,
17752
+ token: session.ready.token,
17753
+ projectId
17754
+ });
17755
+ if (pasted.length > 0) return refusePastedSecrets(file, pasted, json);
17209
17756
  const path = `/api/v1/projects/${encodeURIComponent(projectId)}/manifest`;
17210
17757
  const envelope = {
17211
17758
  template: manifest2.document,
@@ -17399,6 +17946,39 @@ Read the operation that failed: \`vfac get operation <id>\`, or \`vfac get produ
17399
17946
  }
17400
17947
  }
17401
17948
  }
17949
+ async function pastedSecretsIn(document, target) {
17950
+ const pinned = pinnedRelease(document);
17951
+ if (!pinned || !carriesLiteralSettings(document)) return [];
17952
+ const view = await call({
17953
+ endpoint: target.endpoint,
17954
+ path: `/api/v1/projects/${encodeURIComponent(target.projectId)}/products/${encodeURIComponent(pinned.productId)}`,
17955
+ token: target.token
17956
+ });
17957
+ return view.ok ? pastedSecretSettings(document, view.data) : [];
17958
+ }
17959
+ function refusePastedSecrets(file, names, json) {
17960
+ const fields = names.map(secretValueIssue);
17961
+ if (json) {
17962
+ return printError(
17963
+ {
17964
+ status: 400,
17965
+ code: "REQUEST_INVALID",
17966
+ message: `${file} contains a secret value, so it was not sent. Nothing was changed.`,
17967
+ fields
17968
+ },
17969
+ true
17970
+ );
17971
+ }
17972
+ for (const field of fields) {
17973
+ process.stderr.write(`${field.field} contains a secret value.
17974
+ `);
17975
+ }
17976
+ process.stderr.write(
17977
+ "\nSecrets must not be stored in a manifest, so this file was not sent and nothing was changed.\nRemove the value and supply it with:\n" + names.map((name) => ` --secret ${name}=env:VARIABLE
17978
+ `).join("") + "\nIf the value was a real credential and this file has been committed or shared,\nrotate it.\n"
17979
+ );
17980
+ return 1;
17981
+ }
17402
17982
 
17403
17983
  // src/commands/context.ts
17404
17984
  function runContext(parsed) {
@@ -17780,9 +18360,9 @@ var MAX_PAGES = 50;
17780
18360
  async function readAllPages(args) {
17781
18361
  const items = [];
17782
18362
  let cursor = args.cursor;
17783
- const onePage = args.cursor !== void 0 || args.pageSize !== void 0;
18363
+ const onePage = args.onePage === true || args.cursor !== void 0 || args.pageSize !== void 0;
17784
18364
  for (let page = 0; page < MAX_PAGES; page += 1) {
17785
- const query = new URLSearchParams();
18365
+ const query = new URLSearchParams(args.query ?? {});
17786
18366
  if (cursor !== void 0) query.set("cursor", cursor);
17787
18367
  if (args.pageSize !== void 0) query.set("pageSize", args.pageSize);
17788
18368
  const suffix = query.toString();
@@ -17817,7 +18397,7 @@ async function readAllPages(args) {
17817
18397
  }
17818
18398
 
17819
18399
  // src/commands/get.ts
17820
- async function runGet(parsed) {
18400
+ async function runGet(parsed, deps = {}) {
17821
18401
  const noun = parsed.command[1];
17822
18402
  const json = parsed.flags["output"] === "json";
17823
18403
  const session = await ensureSession({ endpointFlag: parsed.flags["endpoint"] });
@@ -17894,8 +18474,10 @@ async function runGet(parsed) {
17894
18474
  const label = String(one["displayName"] ?? one["key"] ?? "");
17895
18475
  const state = String(one["state"] ?? "UNKNOWN");
17896
18476
  const reason = one["reason"];
18477
+ const observedAt = one["observedAt"];
18478
+ const stale = one["freshness"] === "STALE" && typeof observedAt === "string" ? ` [stale \u2014 last observed ${ageOf(observedAt, (deps.now ?? (() => /* @__PURE__ */ new Date()))())} ago]` : "";
17897
18479
  lines.push(
17898
- ` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}`
18480
+ ` ${label.padEnd(width)} ${state}${typeof reason === "string" && reason !== "" ? ` (${reason})` : ""}${stale}`
17899
18481
  );
17900
18482
  const description = one["description"];
17901
18483
  if (typeof description === "string") {
@@ -17932,6 +18514,10 @@ async function runGet(parsed) {
17932
18514
  if (allowed.length > 0) {
17933
18515
  lines.push("", ` You may: ${allowed.join(", ")} (vfac lifecycle <command> ${instanceId})`);
17934
18516
  }
18517
+ const notNow = (lifecycle["refused"] ?? []).filter(
18518
+ (entry) => typeof entry["command"] === "string" && typeof entry["reason"] === "string" && entry["reason"] !== "permission_required"
18519
+ ).map((entry) => `${String(entry["command"])} (${String(entry["reason"])})`);
18520
+ if (notNow.length > 0) lines.push(` Not now: ${notNow.join(", ")}`);
17935
18521
  process.stdout.write(`${lines.join("\n")}
17936
18522
  `);
17937
18523
  return 0;
@@ -17978,7 +18564,75 @@ This is one page. Continue with --cursor ${result.nextCursor ?? ""}
17978
18564
  }
17979
18565
  return 0;
17980
18566
  }
17981
- process.stderr.write("Usage: vfac get product-instances | vfac get operation op_\u2026\n");
18567
+ if (noun === "operations") {
18568
+ const projectId = resolveProject(parsed.flags["project"], session.ready.stored);
18569
+ if (!projectId) {
18570
+ process.stderr.write("No Project. Use --project prj_\u2026 or set one in your context.\n");
18571
+ return 2;
18572
+ }
18573
+ const resource = parsed.flags["resource"];
18574
+ const result = await readAllPages({
18575
+ endpoint: session.ready.endpoint,
18576
+ token: session.ready.token,
18577
+ path: `/api/v1/projects/${encodeURIComponent(projectId)}/operations`,
18578
+ key: "operations",
18579
+ cursor: parsed.flags["cursor"],
18580
+ pageSize: parsed.flags["page-size"],
18581
+ onePage: true,
18582
+ ...resource === void 0 ? {} : { query: { productInstanceId: resource } }
18583
+ });
18584
+ if (!result.ok) {
18585
+ if (result.error.status === 404 && result.error.code === "NOT_FOUND") {
18586
+ process.stderr.write(
18587
+ "This Tenant Portal does not list operations yet \u2014 it is running an older build.\n"
18588
+ );
18589
+ return 1;
18590
+ }
18591
+ return printError(result.error, json);
18592
+ }
18593
+ if (json) {
18594
+ process.stdout.write(
18595
+ `${JSON.stringify({ operations: result.items, nextCursor: result.nextCursor })}
18596
+ `
18597
+ );
18598
+ return 0;
18599
+ }
18600
+ if (result.items.length === 0) {
18601
+ process.stdout.write(
18602
+ resource === void 0 ? "No operations in this project.\n" : "No operations for this resource in this project.\n"
18603
+ );
18604
+ return 0;
18605
+ }
18606
+ const rows = result.items.map((operation) => [
18607
+ operation.operationId ?? "(no id)",
18608
+ // `PRODUCT_INSTANCE_UPDATE` is every line's prefix and says nothing a
18609
+ // reader scanning the column needs. JSON keeps the platform's word.
18610
+ (operation.type ?? "").replace(/^PRODUCT_INSTANCE_/, ""),
18611
+ operation.status ?? "",
18612
+ operation.productInstanceId ?? "-",
18613
+ operation.createdAt ?? ""
18614
+ ]);
18615
+ const widths = rows[0].map(
18616
+ (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
18617
+ );
18618
+ for (const row of rows) {
18619
+ process.stdout.write(
18620
+ `${row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ")}
18621
+ `
18622
+ );
18623
+ }
18624
+ if (!result.complete) {
18625
+ process.stderr.write(
18626
+ `
18627
+ This is one page. Continue with --cursor ${result.nextCursor ?? ""}
18628
+ `
18629
+ );
18630
+ }
18631
+ return 0;
18632
+ }
18633
+ process.stderr.write(
18634
+ "Usage: vfac get product-instances | vfac get operation op_\u2026 | vfac get operations [--resource pri_\u2026]\n"
18635
+ );
17982
18636
  return 2;
17983
18637
  }
17984
18638
  function wrapOutput(text, width) {
@@ -17995,6 +18649,13 @@ function wrapOutput(text, width) {
17995
18649
  if (current !== "") out.push(current);
17996
18650
  return out;
17997
18651
  }
18652
+ function ageOf(iso, now) {
18653
+ const seconds = Math.max(0, Math.floor((now.getTime() - Date.parse(iso)) / 1e3));
18654
+ if (Number.isNaN(seconds)) return "an unknown time";
18655
+ if (seconds < 3600) return `${Math.max(1, Math.floor(seconds / 60))}m`;
18656
+ if (seconds < 72 * 3600) return `${Math.floor(seconds / 3600)}h`;
18657
+ return `${Math.floor(seconds / 86400)}d`;
18658
+ }
17998
18659
 
17999
18660
  // src/commands/product.ts
18000
18661
  async function runProduct(parsed) {
@@ -18601,6 +19262,7 @@ var USAGE = `vfac \u2014 deploy Varde Flyt resources from a manifest
18601
19262
  vfac get product-instances
18602
19263
  vfac get product-instance pri_\u2026 what it is, and its outputs
18603
19264
  vfac get operation op_\u2026
19265
+ vfac get operations [--resource pri_\u2026] newest first \u2014 find an operation id again
18604
19266
  vfac lifecycle <suspend|resume|upgrade|reconcile|delete> pri_\u2026
18605
19267
  delete is irreversible and needs --yes
18606
19268
  vfac export product-instance pri_\u2026
@@ -18618,7 +19280,10 @@ Options
18618
19280
  --secret inputs the apply will carry; without them the plan
18619
19281
  answers about a different request. A value on the command line
18620
19282
  is in your shell history and in the process table, so it is
18621
- refused
19283
+ refused. A value for a secret setting written into the
19284
+ manifest itself is refused too: before the file is sent when it
19285
+ pins the release the catalog offers, by the platform otherwise
19286
+ --resource get operations: only that resource's history
18622
19287
  --product which Product to write a starting manifest for
18623
19288
  --name what a person will call the resource
18624
19289
  --key its stable identity. Derived from --name when omitted
@@ -18646,8 +19311,10 @@ Reading the catalogue
18646
19311
  Products are read UNDER a Project, because that is the scope a credential is
18647
19312
  granted in. Pass --project, or store one with: vfac context set --project prj_\u2026
18648
19313
 
18649
- project list and product list read the WHOLE list, following the server's
18650
- cursor. Pass --cursor or --page-size only when you want a single page.
19314
+ project list, product list and get product-instances read the WHOLE list,
19315
+ following the server's cursor. Pass --cursor or --page-size only when you want
19316
+ a single page. get operations reads ONE page, newest first \u2014 a history only
19317
+ grows \u2014 and prints the --cursor that continues into older operations.
18651
19318
 
18652
19319
  Authentication
18653
19320
  As yourself: vfac login \u2014 approve the code in your browser. The terminal then