@varde-flyt/vfac 0.9.0 → 0.11.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 +50 -10
  2. package/dist/vfac.mjs +257 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,9 +27,11 @@ vfac whoami
27
27
  ```
28
28
 
29
29
  `doctor` checks the endpoint, which Project this terminal has selected, the API,
30
- whether this client is new enough for the platform it is pointed at, your
31
- credential and what that credential reaches, in that order, and reports the first
32
- thing that is actually wrong. A platform declares the oldest client it supports;
30
+ whether this client is new enough for the platform it is pointed at, how this
31
+ terminal is signed in and what that credential reaches, in that order, and reports
32
+ the first thing that is actually wrong. Both ways of signing in count: a Context
33
+ Gate exchanged from `VFAC_CREDENTIAL_ID` and `VFAC_CREDENTIAL_SECRET`, or a
34
+ sign-in from `vfac login` that is still valid. A platform declares the oldest client it supports;
33
35
  below that, `doctor` fails and says so before sending your credential anywhere.
34
36
 
35
37
  The selected Project and the Projects your credential reaches are two different
@@ -89,13 +91,14 @@ identifier in a manifest that is yours: a resource key is 3–63 characters of
89
91
  lowercase letters, digits and hyphens, starting with a letter and ending with a
90
92
  letter or digit.
91
93
 
92
- **A key is claimed for good, and a delete does not release it.** The record of a
93
- deleted resource keeps its key, so a manifest that named it can never apply again
94
- `vfac plan` answers `GONE` and a replacement needs a different key. That is
95
- worth knowing before the delete rather than after it, because after it there is
96
- nothing to undo; `vfac lifecycle delete` says the same thing at the prompt. If
97
- what you want is to stop the service and keep the option of bringing it back,
98
- `vfac lifecycle suspend` is the reversible one.
94
+ **A live resource holds its key, and a finished delete releases it.** The key
95
+ stays claimed while the teardown runs, so nothing can take the name before the
96
+ original is gone. Once the delete completes the name is free and the same
97
+ manifest applies again as a `CREATE`, building a NEW resource with nothing in
98
+ it. Reuse is not a restore, which is worth knowing before the delete rather than
99
+ after it: the apply succeeds, so nothing will stop you. `vfac lifecycle delete`
100
+ says the same thing at the prompt. If what you want is to stop the service and
101
+ keep its data, `vfac lifecycle suspend` is the reversible one.
99
102
 
100
103
  ## Reading a setting from another resource
101
104
 
@@ -191,6 +194,43 @@ credential the connection delivers never appears in your manifest, in a plan, or
191
194
  in any `vfac` output — the providing service issues it and the platform stores
192
195
  it.
193
196
 
197
+ ## For an agent or a pipeline
198
+
199
+ Every command takes `--output json`, and it is the same data rather than a
200
+ prettier subset — the human format of `vfac product show` prints a readable
201
+ selection on purpose, and the JSON carries the whole Product contract.
202
+
203
+ ```sh
204
+ vfac product show <productId> --output json
205
+ vfac plan -f resource.yaml --output json
206
+ vfac get product-instance <productInstanceId> --output json
207
+ vfac get product-instances --output json
208
+ ```
209
+
210
+ **Decide from the action, not from the exit code.** A plan that REFUSES still
211
+ exits 0: a plan reporting `UPGRADE_REQUIRED` did what it was asked, and the
212
+ answer is "not like this". A job that reads only the exit code treats that as
213
+ success and applies nothing.
214
+
215
+ ```sh
216
+ ACTION="$(vfac plan -f resource.yaml --output json | jq -r .action)"
217
+ ```
218
+
219
+ **And a non-zero exit is a real failure.** The code is not decoration: a
220
+ `NO_CHANGE` against a resource that is not actually serving exits 1, with
221
+ `--output json` as without it, because "nothing needed applying" and "it works"
222
+ are different answers. Read the action for what the plan MEANS and the exit code
223
+ for whether anything is WRONG — neither substitutes for the other.
224
+
225
+ The envelopes are stable at the top level:
226
+
227
+ | Command | Answers with |
228
+ | --- | --- |
229
+ | `vfac get product-instance <id> --output json` | `resource` and `lifecycle` — `status`, `outputs` and `conditions` are under `resource` |
230
+ | `vfac get product-instances --output json` | `productInstances` and `nextCursor` |
231
+
232
+ `vfac guide` is authoritative for what is inside them.
233
+
194
234
  ## Running it again
195
235
 
196
236
  **Send no secret.** A pipeline re-applies the same manifest on every push:
package/dist/vfac.mjs CHANGED
@@ -7793,7 +7793,7 @@ async function ensureSession(args) {
7793
7793
  const endpoint = checked.endpoint;
7794
7794
  const credential = credentialFromEnvironment();
7795
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 } };
7796
+ return { ok: true, ready: { endpoint, token: stored.session.token, stored, via: "stored" } };
7797
7797
  }
7798
7798
  const signedIn = await signIn({
7799
7799
  endpoint,
@@ -7802,7 +7802,16 @@ async function ensureSession(args) {
7802
7802
  if (!signedIn.ok) return { ok: false, message: signedIn.message };
7803
7803
  const next = { ...stored, session: signedIn.session };
7804
7804
  writeStored(next);
7805
- return { ok: true, ready: { endpoint, token: signedIn.session.token, stored: 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
+ };
7806
7815
  }
7807
7816
  function resolveProject(flag, stored) {
7808
7817
  return flag ?? stored.context?.projectId ?? null;
@@ -7922,8 +7931,9 @@ function renderOperation(operationId, data) {
7922
7931
  const retryable = failure["retryable"];
7923
7932
  if (typeof retryable === "boolean") {
7924
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";
7925
7935
  lines.push(
7926
- retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}.`
7936
+ retryable ? ` Another attempt can succeed: ${again}.` : ` Repeating this unchanged fails again \u2014 fix what the lines above name, then ${again}${changeIt}.`
7927
7937
  );
7928
7938
  }
7929
7939
  }
@@ -12224,6 +12234,16 @@ var ConfigurationGroupSchema = external_exports.enum([
12224
12234
  ]);
12225
12235
  var CONFIGURATION_PROPERTY_NAME_REGEX = /^[a-z][a-zA-Z0-9]{1,47}$/;
12226
12236
  var PropertyNameSchema = external_exports.string().regex(CONFIGURATION_PROPERTY_NAME_REGEX, "Configuration property names must be camelCase, start with a lowercase letter, and be 2\u201348 characters.");
12237
+ var ValueConditionSchema = external_exports.object({
12238
+ /**
12239
+ * The setting, as a canonical path — or a bare root key, which is the same
12240
+ * string at depth 1. With `anyRow`, a DECLARATION path: `/servers/kind`
12241
+ * names the setting each row answers, never one row's answer.
12242
+ */
12243
+ path: external_exports.string().min(1).max(400),
12244
+ anyOf: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(20),
12245
+ anyRow: external_exports.boolean().optional()
12246
+ }).strict();
12227
12247
  var TitleSchema = external_exports.string().min(2).max(80).describe("Customer-facing field label.");
12228
12248
  var V1_LEAF_BOUNDS = {
12229
12249
  help: 400,
@@ -12280,7 +12300,24 @@ function leafShapes(bounds) {
12280
12300
  * without the other a validation error.
12281
12301
  */
12282
12302
  writeOnly: external_exports.boolean().optional(),
12283
- "x-secret": external_exports.boolean().optional()
12303
+ "x-secret": external_exports.boolean().optional(),
12304
+ /**
12305
+ * The setting VALUE that makes this credential necessary.
12306
+ *
12307
+ * WHY IT CANNOT BE `dependentRequired`. That mechanism is keyed on
12308
+ * PRESENCE — "if the author answered X, they must also answer Y" — and
12309
+ * says so in as many words: "PRESENCE, NEVER A VALUE. A caller passes
12310
+ * NAMES." A Product whose credential is needed only when a setting has one
12311
+ * particular value could not say so, so `vfac plan`'s `requiredSecrets`
12312
+ * could not warn before a deploy, and the refusal arrived at apply.
12313
+ *
12314
+ * ON A SECRET ONLY, and that is narrower than the mechanism could be. A
12315
+ * value-conditioned requirement for an ORDINARY setting is a real gap too,
12316
+ * but it changes what every door accepts for every property; this changes
12317
+ * what `requiredSecrets` reports, which is one answer with one reader.
12318
+ * Opening it wider is a separate decision with its own blast radius.
12319
+ */
12320
+ "x-required-when": ValueConditionSchema.optional()
12284
12321
  }).strict();
12285
12322
  withJsonSchemaAnnex(string, SECRET_COUPLING_ANNEX);
12286
12323
  const number = external_exports.object({
@@ -12332,7 +12369,7 @@ var ConfigurationPropertySchema = external_exports.discriminatedUnion("type", [
12332
12369
  BooleanPropertySchema,
12333
12370
  ArrayPropertySchema
12334
12371
  ]);
12335
- var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true }).extend({ group: ConfigurationGroupSchema.optional() });
12372
+ var NestedStringPropertySchema = V2_LEAVES.string.omit({ "x-secret": true, writeOnly: true, "x-required-when": true }).extend({ group: ConfigurationGroupSchema.optional() });
12336
12373
  var V2_NESTED_LEAVES = {
12337
12374
  string: NestedStringPropertySchema,
12338
12375
  number: V2_LEAVES.number.extend({ group: ConfigurationGroupSchema.optional() }),
@@ -13038,6 +13075,9 @@ function pathSegments(path) {
13038
13075
  const raw = path.startsWith("/") ? path.slice(1).split("/") : [path];
13039
13076
  return raw.map((segment) => isIndexSegment(segment) ? Number(segment) : segment);
13040
13077
  }
13078
+ function rootSegment(path) {
13079
+ return String(pathSegments(path)[0]);
13080
+ }
13041
13081
  function declarationPathOf(path) {
13042
13082
  const segments = pathSegments(path).filter((segment) => typeof segment !== "number");
13043
13083
  return configurationPath(...segments) ?? path;
@@ -13151,11 +13191,71 @@ function declaresPath(schema, path) {
13151
13191
  return true;
13152
13192
  return configurationContainers(schema).some((container) => container.path === declarationPathOf(path));
13153
13193
  }
13194
+ function isPlainObject(value) {
13195
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13196
+ }
13197
+ function valueAt(configuration, path) {
13198
+ let cursor = configuration;
13199
+ for (const segment of segmentsOf(path)) {
13200
+ if (typeof segment === "number") {
13201
+ if (!Array.isArray(cursor))
13202
+ return void 0;
13203
+ cursor = cursor[segment];
13204
+ continue;
13205
+ }
13206
+ if (!isPlainObject(cursor))
13207
+ return void 0;
13208
+ cursor = cursor[segment];
13209
+ }
13210
+ return cursor;
13211
+ }
13212
+ function valuesAt(configuration, path) {
13213
+ let cursors = [configuration];
13214
+ for (const segment of segmentsOf(path)) {
13215
+ const next = [];
13216
+ for (const cursor of cursors) {
13217
+ if (typeof segment === "number") {
13218
+ if (Array.isArray(cursor))
13219
+ next.push(cursor[segment]);
13220
+ continue;
13221
+ }
13222
+ if (!isPlainObject(cursor))
13223
+ continue;
13224
+ const value = cursor[segment];
13225
+ if (Array.isArray(value))
13226
+ next.push(...value);
13227
+ else
13228
+ next.push(value);
13229
+ }
13230
+ cursors = next;
13231
+ }
13232
+ return cursors.filter((value) => value !== void 0);
13233
+ }
13234
+ function segmentsOf(path) {
13235
+ const raw = path.startsWith("/") ? path.slice(1).split("/") : [path];
13236
+ return raw.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
13237
+ }
13238
+
13239
+ // ../product-configuration/dist/value-condition.js
13240
+ function conditionSetting(condition) {
13241
+ return "path" in condition ? condition.path : condition.property;
13242
+ }
13243
+ function conditionRoot(condition) {
13244
+ const path = parseConfigurationPath(conditionSetting(condition));
13245
+ return path === null ? conditionSetting(condition) : rootSegment(path);
13246
+ }
13247
+ function conditionHolds(condition, configuration) {
13248
+ const path = parseConfigurationPath(conditionSetting(condition));
13249
+ if (path === null)
13250
+ return false;
13251
+ const matches2 = (held) => typeof held === "string" && condition.anyOf.includes(held);
13252
+ return "anyRow" in condition && condition.anyRow === true ? valuesAt(configuration, path).some(matches2) : matches2(valueAt(configuration, path));
13253
+ }
13154
13254
 
13155
13255
  // ../product-configuration/dist/customer-configuration-input.js
13156
13256
  var ABSOLUTE_MAX_STRING = MAX_DECLARABLE_STRING_LENGTH;
13157
13257
  var MAX_REPORTED_UNDECLARED_KEYS = 10;
13158
- function isPlainObject(value) {
13258
+ function isPlainObject2(value) {
13159
13259
  return typeof value === "object" && value !== null && !Array.isArray(value);
13160
13260
  }
13161
13261
  function report(walk, field, message) {
@@ -13177,17 +13277,17 @@ function validateSubmittedConfiguration(schema, submitted, presence = {}) {
13177
13277
  referenced: new Set(presence.referenced ?? [])
13178
13278
  };
13179
13279
  const supplied = /* @__PURE__ */ new Set();
13180
- if (submitted !== void 0 && submitted !== null && !isPlainObject(submitted)) {
13280
+ if (submitted !== void 0 && submitted !== null && !isPlainObject2(submitted)) {
13181
13281
  return {
13182
13282
  ok: false,
13183
13283
  issues: [{ field: "", message: "Configuration must be an object." }],
13184
13284
  partial: {}
13185
13285
  };
13186
13286
  }
13187
- const input = isPlainObject(submitted) ? submitted : {};
13287
+ const input = isPlainObject2(submitted) ? submitted : {};
13188
13288
  const secrets = {};
13189
13289
  const configuration = validateObject({ properties: schema.properties, required: schema.required }, input, null, walk, { secrets, supplied });
13190
- drain(walk, crossFieldIssues(schema, supplied, presence));
13290
+ drain(walk, crossFieldIssues(schema, supplied, presence, configuration));
13191
13291
  if (walk.suppressed > 0) {
13192
13292
  walk.issues.push({
13193
13293
  field: "",
@@ -13342,7 +13442,7 @@ function validateContainer(name, property, children, value, field, walk) {
13342
13442
  if (children.kind === "object") {
13343
13443
  if (value === void 0)
13344
13444
  return void 0;
13345
- if (!isPlainObject(value)) {
13445
+ if (!isPlainObject2(value)) {
13346
13446
  report(walk, field, "Expected a group of settings.");
13347
13447
  return void 0;
13348
13448
  }
@@ -13369,7 +13469,7 @@ function validateContainer(name, property, children, value, field, walk) {
13369
13469
  report(walk, field, "This configuration has too many values.");
13370
13470
  break;
13371
13471
  }
13372
- if (!isPlainObject(element)) {
13472
+ if (!isPlainObject2(element)) {
13373
13473
  report(walk, `${field}`, `Item ${index + 1} is not a group of settings.`);
13374
13474
  continue;
13375
13475
  }
@@ -13385,9 +13485,10 @@ function validateContainer(name, property, children, value, field, walk) {
13385
13485
  }
13386
13486
  return out;
13387
13487
  }
13388
- function crossFieldIssues(schema, supplied, presence) {
13488
+ function crossFieldIssues(schema, supplied, presence, configuration = {}) {
13389
13489
  const clauses = Object.entries(schema.dependentRequired ?? {});
13390
- if (clauses.length === 0)
13490
+ const conditional = Object.entries(schema.properties).filter(([, property]) => conditionOf(property) !== void 0);
13491
+ if (clauses.length === 0 && conditional.length === 0)
13391
13492
  return [];
13392
13493
  const alsoPresent = /* @__PURE__ */ new Set([...presence.present ?? [], ...presence.referenced ?? []]);
13393
13494
  const unknown = new Set(presence.unknown ?? []);
@@ -13410,13 +13511,42 @@ function crossFieldIssues(schema, supplied, presence) {
13410
13511
  });
13411
13512
  }
13412
13513
  }
13514
+ for (const [name, property] of conditional) {
13515
+ const condition = conditionOf(property);
13516
+ if (condition === void 0 || !known(name) || held(name))
13517
+ continue;
13518
+ if (!conditionHolds(condition, configuration))
13519
+ continue;
13520
+ if (reported.has(name))
13521
+ continue;
13522
+ reported.add(name);
13523
+ issues.push({
13524
+ field: name,
13525
+ // THE SETTING THAT MADE IT NECESSARY, never the value it holds. A message
13526
+ // naming the value would be fine here and wrong one refactor later, when
13527
+ // somebody conditions a secret on another secret — which the publish rules
13528
+ // refuse precisely because nothing may read one.
13529
+ message: `This setting is required because of what ${labelOf(schema, conditionRoot(condition))} is set to.`
13530
+ });
13531
+ }
13413
13532
  return issues;
13414
13533
  }
13534
+ function conditionOf(property) {
13535
+ return property?.["x-required-when"];
13536
+ }
13415
13537
  function labelOf(schema, name) {
13416
13538
  const property = schema.properties[name];
13417
13539
  return property?.title === void 0 ? `"${name}"` : `"${property.title}"`;
13418
13540
  }
13419
13541
  function withoutCrossFieldRules(schema) {
13542
+ const conditioned = Object.entries(schema.properties).some(([, property]) => property["x-required-when"] !== void 0);
13543
+ if (conditioned) {
13544
+ const properties = Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => {
13545
+ const { "x-required-when": _dropped, ...bare } = property;
13546
+ return [name, bare];
13547
+ }));
13548
+ return withoutCrossFieldRules({ ...schema, properties });
13549
+ }
13420
13550
  if (schema.dependentRequired === void 0)
13421
13551
  return schema;
13422
13552
  const { dependentRequired: _removed, ...rest } = schema;
@@ -14619,6 +14749,91 @@ function checkProductDefinition(definition, ctx) {
14619
14749
  });
14620
14750
  }
14621
14751
  });
14752
+ for (const [name, property] of Object.entries(definition.configuration.properties ?? {})) {
14753
+ const condition = property["x-required-when"];
14754
+ if (condition === void 0)
14755
+ continue;
14756
+ const at = ["configuration", "properties", name, "x-required-when"];
14757
+ if (!isSecretProperty(property)) {
14758
+ ctx.addIssue({
14759
+ code: external_exports.ZodIssueCode.custom,
14760
+ path: [...at],
14761
+ 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.`
14762
+ });
14763
+ continue;
14764
+ }
14765
+ if (new Set(condition.anyOf).size !== condition.anyOf.length) {
14766
+ ctx.addIssue({
14767
+ code: external_exports.ZodIssueCode.custom,
14768
+ path: [...at, "anyOf"],
14769
+ message: "Repeated values in `anyOf`."
14770
+ });
14771
+ }
14772
+ if (condition.path === name) {
14773
+ ctx.addIssue({
14774
+ code: external_exports.ZodIssueCode.custom,
14775
+ path: [...at, "path"],
14776
+ message: `"${name}" cannot be conditioned on its own value.`
14777
+ });
14778
+ continue;
14779
+ }
14780
+ const parsed = parseConfigurationPath(condition.path);
14781
+ const leaf = parsed === null ? null : leafAt(definition.configuration, parsed);
14782
+ if (leaf === null) {
14783
+ const isGroup = parsed !== null && declaresPath(definition.configuration, parsed);
14784
+ ctx.addIssue({
14785
+ code: external_exports.ZodIssueCode.custom,
14786
+ path: [...at, "path"],
14787
+ message: isGroup ? `"${condition.path}" is a group of settings, which holds no single value to compare. Name one setting inside it.` : `"${condition.path}" is not a declared customer configuration property.`
14788
+ });
14789
+ continue;
14790
+ }
14791
+ if (underList(leaf) && condition.anyRow !== true) {
14792
+ ctx.addIssue({
14793
+ code: external_exports.ZodIssueCode.custom,
14794
+ path: [...at, "path"],
14795
+ message: `"${condition.path}" is inside a list, so it names one value per item rather than one value. Add "anyRow": true to require the secret when ANY row chose one of these values, or name a setting the instance holds exactly once.`
14796
+ });
14797
+ continue;
14798
+ }
14799
+ if (!underList(leaf) && condition.anyRow === true) {
14800
+ ctx.addIssue({
14801
+ code: external_exports.ZodIssueCode.custom,
14802
+ path: [...at, "anyRow"],
14803
+ message: `"${condition.path}" is not inside a list, so there are no rows to quantify over.`
14804
+ });
14805
+ }
14806
+ const target = leaf.property;
14807
+ if (target.type !== "string") {
14808
+ ctx.addIssue({
14809
+ code: external_exports.ZodIssueCode.custom,
14810
+ path: [...at, "path"],
14811
+ message: `"${condition.path}" is a ${target.type} setting; a condition compares string values.`
14812
+ });
14813
+ continue;
14814
+ }
14815
+ if (isSecretProperty(target)) {
14816
+ ctx.addIssue({
14817
+ code: external_exports.ZodIssueCode.custom,
14818
+ path: [...at, "path"],
14819
+ message: `"${condition.path}" is a secret. The platform never reads a secret's value, so a condition on one could never be evaluated.`
14820
+ });
14821
+ continue;
14822
+ }
14823
+ const single = withoutCrossFieldRules({
14824
+ ...definition.configuration,
14825
+ properties: { [leaf.name]: target },
14826
+ required: [leaf.name]
14827
+ });
14828
+ const outside = condition.anyOf.filter((value) => !validateSubmittedConfiguration(single, { [leaf.name]: value }).ok);
14829
+ if (outside.length > 0) {
14830
+ ctx.addIssue({
14831
+ code: external_exports.ZodIssueCode.custom,
14832
+ path: [...at, "anyOf"],
14833
+ message: `"${condition.path}" never accepts ${outside.map((v) => `"${v}"`).join(", ")}, so this condition could never make the secret required.`
14834
+ });
14835
+ }
14836
+ }
14622
14837
  const provides = definition.serviceInterfaces?.provides ?? [];
14623
14838
  const provideKeys = provides.map((provide) => provide.key);
14624
14839
  const duplicateProvideKeys = provideKeys.filter((key, i) => provideKeys.indexOf(key) !== i);
@@ -16589,9 +16804,16 @@ var MANIFEST_ACTIONS = [
16589
16804
  /** The key names a resource and the document matches it. A pipeline that runs
16590
16805
  * on every push and changes nothing reports this, and it is SUCCESS. */
16591
16806
  "NO_CHANGE",
16592
- /** The key names a resource that was DELETED. Its own verb, because both
16807
+ /** `adopt` points at a resource that was DELETED. Its own verb, because both
16593
16808
  * alternatives are wrong: `NO_CHANGE` would call a missing resource fine, and
16594
- * `CREATE` cannot be honoured while the tombstone holds the key. */
16809
+ * `CREATE` would silently ignore the `adopt` the caller asked for.
16810
+ *
16811
+ * IT USED TO COVER A SECOND, COMMONER CASE: a manifest whose `metadata.key`
16812
+ * named a deleted resource, back when a key was claimed for ever. A completed
16813
+ * delete now releases the key, so that manifest is planned as a `CREATE` and
16814
+ * the verb narrowed to the one case a caller reaches by naming a `pri_…` by
16815
+ * hand. Kept in the vocabulary rather than removed: it is still produced, and
16816
+ * a Control Plane that still answers it must still be understood. */
16595
16817
  "GONE",
16596
16818
  /** The document pins a different release. Done through `:upgrade`, which has
16597
16819
  * the Definition's own capability check, its own version resolution and its
@@ -17002,7 +17224,7 @@ async function runApply(verb, parsed, deps = {}) {
17002
17224
  if (!planned.ok) return printError(planned.error, json);
17003
17225
  if (verb === "plan") {
17004
17226
  printPlan(planned.data, json);
17005
- if (isUnconverged(planned.data)) {
17227
+ if (isUnconverged(planned.data) && planned.data.action === "NO_CHANGE") {
17006
17228
  if (!json) {
17007
17229
  process.stderr.write(
17008
17230
  `${planned.data.manifestKey ?? "This resource"} is ${planned.data.health?.status ?? "not serving"}. Its desired state is applied; it is not running. \`vfac get operation <id>\` says why.
@@ -17011,6 +17233,12 @@ async function runApply(verb, parsed, deps = {}) {
17011
17233
  }
17012
17234
  return 1;
17013
17235
  }
17236
+ if (isUnconverged(planned.data) && !json) {
17237
+ process.stderr.write(
17238
+ `${planned.data.manifestKey ?? "This resource"} is ${planned.data.health?.status ?? "not serving"}. This plan changes it.
17239
+ `
17240
+ );
17241
+ }
17014
17242
  return 0;
17015
17243
  }
17016
17244
  const parsedAction = ManifestActionSchema.safeParse(planned.data.action);
@@ -17310,23 +17538,20 @@ async function runDoctor(parsed) {
17310
17538
  const compatible = compatibility(cliVersion(), probe.minimumCliVersion);
17311
17539
  checks.push(compatible);
17312
17540
  if (!compatible.ok) return report2(checks, json);
17313
- const signedIn = await signIn({ endpoint });
17314
- if (!signedIn.ok) {
17315
- checks.push({ name: "Context Gate", ok: false, detail: signedIn.message });
17541
+ const session = await ensureSession({ endpointFlag: parsed.flags["endpoint"] });
17542
+ if (!session.ok) {
17543
+ checks.push({ name: "Sign-in", ok: false, detail: session.message });
17316
17544
  return report2(checks, json);
17317
17545
  }
17318
17546
  checks.push({
17319
- name: "Context Gate",
17547
+ name: "Sign-in",
17320
17548
  ok: true,
17321
- detail: signedIn.gate ? `authenticated as "${signedIn.gate.name}" (${signedIn.gate.id})` : "authenticated"
17549
+ detail: session.ready.via === "stored" ? "a stored sign-in is still valid, so no credential was spent" : session.ready.gate ? `authenticated as "${session.ready.gate.name}" (${session.ready.gate.id})` : "authenticated"
17322
17550
  });
17323
- if (!sessionUsable(stored.session, endpoint, /* @__PURE__ */ new Date())) {
17324
- writeStored({ ...stored, session: signedIn.session });
17325
- }
17326
17551
  const who = await call({
17327
17552
  endpoint,
17328
17553
  path: "/api/v1/whoami",
17329
- token: signedIn.session.token
17554
+ token: session.ready.token
17330
17555
  });
17331
17556
  if (!who.ok) {
17332
17557
  checks.push({
@@ -18219,8 +18444,9 @@ Run \`vfac get product-instance pri_\u2026\` to see which ones this credential m
18219
18444
  if (command === "delete" && !parsed.booleans.has("yes")) {
18220
18445
  process.stderr.write(
18221
18446
  `delete removes ${instanceId} and everything it is serving. This cannot be undone.
18222
- If it was deployed from a manifest, its \`metadata.key\` stays claimed: a replacement
18223
- needs a different key, and re-applying the same file will answer GONE.
18447
+ If it was deployed from a manifest, its \`metadata.key\` is held until this delete
18448
+ FINISHES, then released. Applying the same file afterwards creates a new, empty
18449
+ resource under that key. It does not bring this one back.
18224
18450
  Re-run with --yes to confirm: vfac lifecycle delete ${instanceId} --yes
18225
18451
  Or suspend it instead, which stops it and keeps it: \`vfac lifecycle suspend\`.
18226
18452
  `
@@ -18379,7 +18605,10 @@ Authentication
18379
18605
 
18380
18606
  Exit codes
18381
18607
  0 done \u2014 including "no change", which is what a pipeline that changes nothing
18382
- should report
18608
+ should report, AND including a plan that refuses. plan answers what would
18609
+ happen; "it would be refused" is an answer, so it exits 0. Read the action,
18610
+ not the code: vfac plan -f resource.yaml --output json, then branch on
18611
+ .action
18383
18612
  1 refused, the request was wrong, or the resource matches your manifest and is
18384
18613
  NOT serving. "Nothing needed applying" is not the same as "it works", and a
18385
18614
  NO_CHANGE that could be a false green is the case this one exists for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varde-flyt/vfac",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Deploy and manage Varde Flyt Products from a manifest.",
5
5
  "repository": {
6
6
  "type": "git",