@devkong/cli 0.0.67-alpha.21 → 0.0.67-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -60771,25 +60771,25 @@ async function spawnCommandWithArgs(command, commandArgs, logger = null, shell =
60771
60771
  });
60772
60772
  });
60773
60773
  }
60774
- function printError(message2, error) {
60774
+ function printError(label, error) {
60775
60775
  if (axios_default.isAxiosError(error)) {
60776
- console.error(message2 + ":", error.message);
60777
- console.error("URL:", error.config?.url);
60778
- console.error("Method:", error.config?.method);
60779
- console.error("Headers:", error.config?.headers);
60780
- console.error("Request Data:", error.config?.data);
60776
+ console.error("url:", error.config?.url);
60777
+ console.error("method:", error.config?.method);
60778
+ console.error("headers:", error.config?.headers);
60779
+ console.error("request data:", error.config?.data);
60781
60780
  if (error.response) {
60782
- console.error("Response Status:", error.response.status);
60783
- console.error("Response Headers:", error.response.headers);
60784
- console.error("Response Data:", error.response.data);
60781
+ console.error("response status:", error.response.status);
60782
+ console.error("response headers:", error.response.headers);
60783
+ console.error("response data:", error.response.data);
60785
60784
  } else if (error.request) {
60786
- console.error("Request was made but no response received");
60787
- console.error("Request Data:", error.request);
60785
+ console.error("request was made but no response received");
60786
+ console.error("request data:", error.request);
60788
60787
  }
60788
+ console.error(label + ":", error.message);
60789
60789
  } else if (error instanceof AppError) {
60790
- console.error(message2, error.message);
60790
+ console.error(label + ":", error.message);
60791
60791
  } else {
60792
- console.error(message2, error);
60792
+ console.error(label + ":", error);
60793
60793
  }
60794
60794
  }
60795
60795
  function escapePathSpaces(sourcePath) {
@@ -63543,6 +63543,63 @@ function validateName(value) {
63543
63543
  );
63544
63544
  }
63545
63545
  }
63546
+ var MAX_USES = 5;
63547
+ function parseVersionWeight(text) {
63548
+ const [versionPart, weightPart, ...rest] = text.split(":");
63549
+ if (rest.length > 0) {
63550
+ throw new AppError(
63551
+ `Invalid version config "${text}". Expected "<version>" or "<version>:<weight|shadow>".`
63552
+ );
63553
+ }
63554
+ if (!/^\d+$/.test(versionPart)) {
63555
+ throw new AppError(
63556
+ `Invalid version "${versionPart}" in "${text}". Expected a positive integer.`
63557
+ );
63558
+ }
63559
+ const version = Number(versionPart);
63560
+ if (weightPart === void 0) {
63561
+ return { version, balance: 100 };
63562
+ }
63563
+ if (weightPart.toLowerCase() === "shadow") {
63564
+ return { version, balance: null };
63565
+ }
63566
+ if (!/^\d+$/.test(weightPart)) {
63567
+ throw new AppError(
63568
+ `Invalid weight "${weightPart}" in "${text}". Expected a number 1-100 or "shadow".`
63569
+ );
63570
+ }
63571
+ return { version, balance: Number(weightPart) };
63572
+ }
63573
+ function parseVersionWeightList(list) {
63574
+ if (list.length === 0) {
63575
+ throw new AppError("At least one extension version must be provided.");
63576
+ }
63577
+ if (list.length > MAX_USES) {
63578
+ throw new AppError(`A maximum of ${MAX_USES} version(s) can share an alias.`);
63579
+ }
63580
+ const uses = list.map(parseVersionWeight);
63581
+ const seen = /* @__PURE__ */ new Set();
63582
+ for (const use of uses) {
63583
+ if (seen.has(use.version)) {
63584
+ throw new AppError(`Version ${use.version} is listed more than once.`);
63585
+ }
63586
+ seen.add(use.version);
63587
+ }
63588
+ const live = uses.filter((use) => use.balance !== null);
63589
+ if (live.length === 0) {
63590
+ throw new AppError("At least one non-shadow version with live traffic is required.");
63591
+ }
63592
+ if (live.some((use) => use.balance <= 0)) {
63593
+ throw new AppError("All traffic weights must be greater than 0%.");
63594
+ }
63595
+ const total = live.reduce((sum, use) => sum + use.balance, 0);
63596
+ if (total !== 100) {
63597
+ throw new AppError(
63598
+ `Traffic weights must sum to 100% (got ${total}%). ${total < 100 ? "Add" : "Remove"} ${Math.abs(100 - total)}%.`
63599
+ );
63600
+ }
63601
+ return uses;
63602
+ }
63546
63603
  var DEPLOY_STEPS = [
63547
63604
  "create deployment",
63548
63605
  "save deployment details",
@@ -63568,7 +63625,7 @@ var SetAliasCommand = class {
63568
63625
  this.publicClient = new PublicClient(profile.kongBaseUrl, clientHeaderProvider);
63569
63626
  this.managementClient = new ManagementClient(profile.kongBaseUrl, clientHeaderProvider);
63570
63627
  }
63571
- async execute(aliasName, extensionVersion, timeoutSeconds) {
63628
+ async execute(aliasName, weights, timeoutSeconds) {
63572
63629
  validateName(aliasName);
63573
63630
  const startedAt = utcNow(DEFAULT_TIMEZONE);
63574
63631
  const steps = createDeploySteps();
@@ -63578,25 +63635,29 @@ var SetAliasCommand = class {
63578
63635
  title: "get snapshot details",
63579
63636
  task: async (ctx, task) => {
63580
63637
  ctx.kongJson = getKongJson();
63581
- ctx.snapshot = await this.managementClient.getExtensionSnapshot(
63582
- ctx.kongJson.id,
63583
- extensionVersion
63638
+ ctx.aliasUseList = await Promise.all(
63639
+ weights.map(async (use) => {
63640
+ const snapshot = await this.managementClient.getExtensionSnapshot(
63641
+ ctx.kongJson.id,
63642
+ use.version
63643
+ );
63644
+ if (!snapshot || !snapshot.id) {
63645
+ throw new AppError(`Snapshot for version ${use.version} not found`);
63646
+ }
63647
+ return {
63648
+ snapshot,
63649
+ balance: use.balance,
63650
+ inputFilter: void 0,
63651
+ outputFilter: void 0
63652
+ };
63653
+ })
63584
63654
  );
63585
- if (!ctx.snapshot || !ctx.snapshot.id) {
63586
- throw new AppError(`Snapshot for version ${extensionVersion} not found`);
63587
- }
63588
63655
  }
63589
63656
  },
63590
63657
  {
63591
63658
  title: "assign extension alias",
63592
63659
  task: async (ctx, task) => {
63593
63660
  const now = utcNow(DEFAULT_TIMEZONE);
63594
- const aliasUse = {
63595
- snapshot: ctx.snapshot,
63596
- balance: 100,
63597
- inputFilter: void 0,
63598
- outputFilter: void 0
63599
- };
63600
63661
  const alias = {
63601
63662
  name: aliasName,
63602
63663
  basedOnName: ctx.kongJson.name,
@@ -63608,7 +63669,7 @@ var SetAliasCommand = class {
63608
63669
  triggerSettings: {
63609
63670
  targetKey: `/extensions/${ctx.kongJson.id}/aliases/${aliasName}`
63610
63671
  },
63611
- use: [aliasUse],
63672
+ use: ctx.aliasUseList,
63612
63673
  inputSchema: createEmptyJsonSchema(),
63613
63674
  outputSchema: createEmptyJsonSchema(),
63614
63675
  invoke: void 0,
@@ -63743,17 +63804,20 @@ async function main() {
63743
63804
  printError("list-versions command failed", ex);
63744
63805
  }
63745
63806
  });
63746
- const setAliasCommand = new Command("set-alias").description("Assign alias to an extension").argument("alias-name", "Alias name to assign").argument("extension-version", "Extension version to use. Should be one of existing versions").addOption(new Option("--profile <name>", "Configured Kong profile to use").default("default")).addOption(
63747
- new Option(
63748
- "--timeout <seconds>",
63749
- "How long to wait for the deployment to finish"
63750
- ).default("120")
63751
- ).action(async (aliasName, extensionVersion, options) => {
63807
+ const setAliasCommand = new Command("set-alias").description("Assign alias to one or more extension versions").argument("<alias-name>", "Alias name to assign").argument(
63808
+ "<version-spec...>",
63809
+ "Version assignments. Each is '<version>', '<version>:<weight>', or '<version>:shadow'. Simple form 'set-alias stable 1' sends 100% of traffic to version 1. Advanced form 'set-alias stable 1:80 2:20 3:shadow' load-balances live traffic 80/20 across versions 1 and 2 (weights must sum to 100%) while mirroring it to shadow version 3."
63810
+ ).addOption(new Option("--profile <name>", "Configured Kong profile to use").default("default")).addOption(
63811
+ new Option("--timeout <seconds>", "How long to wait for the deployment to finish").default(
63812
+ "120"
63813
+ )
63814
+ ).action(async (aliasName, versionSpecs, options) => {
63752
63815
  try {
63816
+ const weights = parseVersionWeightList(versionSpecs);
63753
63817
  printProfile(options.profile);
63754
63818
  await new SetAliasCommand(getProfile(options.profile)).execute(
63755
63819
  aliasName,
63756
- extensionVersion,
63820
+ weights,
63757
63821
  Number(options.timeout)
63758
63822
  );
63759
63823
  } catch (ex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devkong/cli",
3
- "version": "0.0.67-alpha.21",
3
+ "version": "0.0.67-alpha.23",
4
4
  "type": "commonjs",
5
5
  "main": "./index.js",
6
6
  "typings": "./index.d.ts",
@@ -1,10 +1,22 @@
1
1
  import { Profile } from "../common/profile";
2
+ /** A single version→weight assignment parsed from the command line. */
3
+ export interface AppAliasVersionWeight {
4
+ version: number;
5
+ /** Traffic percentage, or `null` for a "shadow" version (mirrored, no live traffic). */
6
+ balance: number | null;
7
+ }
8
+ /**
9
+ * Parse and validate all version configs. Live (non-shadow) weights must be > 0 and
10
+ * sum to exactly 100%; shadow versions are excluded from that total. Versions may
11
+ * not repeat and no more than {@link MAX_USES} versions can share an alias.
12
+ */
13
+ export declare function parseVersionWeightList(list: string[]): AppAliasVersionWeight[];
2
14
  export declare class SetAliasCommand {
3
15
  private profile;
4
16
  private publicClient;
5
17
  private managementClient;
6
18
  constructor(profile: Profile);
7
- execute(aliasName: string, extensionVersion: number, timeoutSeconds: number): Promise<void>;
19
+ execute(aliasName: string, weights: AppAliasVersionWeight[], timeoutSeconds: number): Promise<void>;
8
20
  /**
9
21
  * Poll the deployment audit log and settle one step per matching event. Runs
10
22
  * detached from the task list; it settles a step as its event arrives and
@@ -12,5 +12,5 @@ export declare function resolveProjectPath(fileName: string): string;
12
12
  export declare function generateShortId(): ShortUUID;
13
13
  export declare function spawnCommand(commandText: string, logger?: ILogger | null, shell?: boolean): Promise<string>;
14
14
  export declare function spawnCommandWithArgs(command: string, commandArgs: string[], logger?: ILogger | null, shell?: boolean): Promise<string>;
15
- export declare function printError(message: string, error: unknown): void;
15
+ export declare function printError(label: string, error: unknown): void;
16
16
  export declare function escapePathSpaces(sourcePath: string): string;
@@ -20,7 +20,7 @@ export { applyJqFilterReplacements, applyJqObjectReplacements, applyJqTextReplac
20
20
  export type { JqArray, JqFilter, JqObject, JqSerializer, JqText, RefactorReplacement, } from "./lib/jq";
21
21
  export { generateFakeJsonObject, jsonToHumanReadable, prettyJsonObject, prettyJsonText, safeJsonParse, } from "./lib/json";
22
22
  export type { JsonArray, JsonObject, JsonSchemaUrl, JsonText } from "./lib/json";
23
- export { buildExampleObject, buildJSONSchemaType, createEmptyJsonSchema, getNestedValue, getPropertyType, hasJSONSchemaType, isRequired, updateNestedValue, } from "./lib/jsonSchema";
23
+ export { buildExampleObject, buildJSONSchemaType, createEmptyJsonSchema, getNestedValue, getPropertyType, hasJSONSchemaType, isRequired, normalizeNullableUnions, updateNestedValue, } from "./lib/jsonSchema";
24
24
  export { KeyGenerator } from "./lib/keyGenerator";
25
25
  export { findLastIndex, insertItem, moveItem, unique } from "./lib/list";
26
26
  export { deepMerge, deepMergeAll } from "./lib/merge";
@@ -1,5 +1,17 @@
1
1
  import { JSONSchema7, JSONSchema7TypeName } from "json-schema";
2
2
  import { JsonObject, Optional } from "./types";
3
+ /**
4
+ * Collapses Pydantic's optional idiom `anyOf: [X, { "type": "null" }]` (and the `oneOf` form) down
5
+ * to `X`, recursively, and drops the now-dead `$defs`/`definitions`.
6
+ *
7
+ * Pydantic encodes every optional/nullable field as such a union instead of a nullable type, and
8
+ * `$ref` dereferencing inlines the branch but leaves the union in place. A form that reads
9
+ * `properties`/`type` straight off the node then sees an `anyOf` wrapper with no `properties` and
10
+ * renders nothing. When a single non-null branch remains it is merged with the union's sibling
11
+ * keywords (`default`, `description`, `title`, ...); genuine multi-branch unions keep their non-null
12
+ * branches. Run this right after dereferencing, before the schema reaches the form.
13
+ */
14
+ export declare function normalizeNullableUnions<T = JSONSchema7>(schema: T): T;
3
15
  export declare function hasJSONSchemaType(type: JSONSchema7["type"], name: JSONSchema7TypeName): boolean;
4
16
  export declare function getPropertyType(schema: Optional<JSONSchema7> | null): string;
5
17
  export declare function isRequired(schema: Optional<JSONSchema7> | null, propertyKey: string): boolean;