@camunda8/cli 4.0.0 → 4.1.0-alpha.2

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 (35) hide show
  1. package/dist/commands/profiles.d.ts.map +1 -1
  2. package/dist/commands/profiles.js +19 -3
  3. package/dist/commands/profiles.js.map +1 -1
  4. package/dist/commands/search.d.ts.map +1 -1
  5. package/dist/commands/search.js +1 -1
  6. package/dist/commands/search.js.map +1 -1
  7. package/dist/core/client.d.ts +35 -1
  8. package/dist/core/client.d.ts.map +1 -1
  9. package/dist/core/client.js +161 -5
  10. package/dist/core/client.js.map +1 -1
  11. package/dist/core/config.d.ts +56 -1
  12. package/dist/core/config.d.ts.map +1 -1
  13. package/dist/core/config.js +64 -0
  14. package/dist/core/config.js.map +1 -1
  15. package/dist/default-plugins/element-template/AGENTS.md +19 -10
  16. package/dist/default-plugins/element-template/README.md +13 -9
  17. package/dist/default-plugins/element-template/c8ctl-plugin.js +249 -113
  18. package/dist/default-plugins/element-template/docs/design.md +68 -26
  19. package/dist/framework/command-framework.d.ts +24 -4
  20. package/dist/framework/command-framework.d.ts.map +1 -1
  21. package/dist/framework/command-framework.js +16 -1
  22. package/dist/framework/command-framework.js.map +1 -1
  23. package/dist/framework/command-registry.d.ts +13 -0
  24. package/dist/framework/command-registry.d.ts.map +1 -1
  25. package/dist/framework/command-registry.js +16 -0
  26. package/dist/framework/command-registry.js.map +1 -1
  27. package/dist/framework/command-validation.d.ts +5 -0
  28. package/dist/framework/command-validation.d.ts.map +1 -1
  29. package/dist/framework/command-validation.js +55 -10
  30. package/dist/framework/command-validation.js.map +1 -1
  31. package/dist/utils/shared/npm-exec.d.ts +12 -0
  32. package/dist/utils/shared/npm-exec.d.ts.map +1 -1
  33. package/dist/utils/shared/npm-exec.js +49 -1
  34. package/dist/utils/shared/npm-exec.js.map +1 -1
  35. package/package.json +1 -1
@@ -8610,8 +8610,8 @@ function parseTemplateJson(content) {
8610
8610
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
8611
8611
  import { resolve as resolvePath2 } from "node:path";
8612
8612
 
8613
- // default-plugins/element-template/marketplace.ts
8614
- var import_semver2 = __toESM(require_semver2(), 1);
8613
+ // default-plugins/element-template/cache.ts
8614
+ var import_semver3 = __toESM(require_semver2(), 1);
8615
8615
  import {
8616
8616
  closeSync,
8617
8617
  existsSync as existsSync2,
@@ -8624,14 +8624,140 @@ import {
8624
8624
  writeSync
8625
8625
  } from "node:fs";
8626
8626
  import { join } from "node:path";
8627
- var DEFAULT_OOTB_URL = "https://marketplace.cloud.camunda.io/api/v1/ootb-connectors";
8628
- var FETCH_CONCURRENCY = 12;
8627
+
8628
+ // default-plugins/element-template/releases.ts
8629
+ var import_semver2 = __toESM(require_semver2(), 1);
8630
+ import { gunzipSync } from "node:zlib";
8631
+ var DEFAULT_RELEASES_URL = "https://api.github.com/repos/camunda/connectors/releases?per_page=100";
8632
+ var TEMPLATES_ASSET_PREFIX = "connectors-bundle-templates-";
8633
+ var TEMPLATES_ASSET_SUFFIX = ".tar.gz";
8634
+ var RELEASES_FETCH_TIMEOUT_MS = 3e4;
8635
+ var ASSET_FETCH_TIMEOUT_MS = 12e4;
8636
+ var MAX_BUNDLE_BYTES = 128 * 1024 * 1024;
8637
+ var MAX_MINOR_LINES = 4;
8638
+ function getReleasesUrl() {
8639
+ return process.env.C8CTL_CONNECTORS_RELEASES_URL || DEFAULT_RELEASES_URL;
8640
+ }
8641
+ function isReleaseCandidate(version) {
8642
+ const prerelease = import_semver2.default.prerelease(version);
8643
+ if (!prerelease) return false;
8644
+ return prerelease.flatMap((part) => typeof part === "string" ? part.split("-") : []).some((part) => /^rc\d*$/i.test(part));
8645
+ }
8646
+ function findTemplatesAssetUrl(release) {
8647
+ if (!Array.isArray(release.assets)) return null;
8648
+ for (const asset of release.assets) {
8649
+ if (!isRecord(asset)) continue;
8650
+ const { name: name2, browser_download_url: url } = asset;
8651
+ if (typeof name2 !== "string" || typeof url !== "string") continue;
8652
+ if (name2.startsWith(TEMPLATES_ASSET_PREFIX) && name2.endsWith(TEMPLATES_ASSET_SUFFIX)) {
8653
+ return url;
8654
+ }
8655
+ }
8656
+ return null;
8657
+ }
8658
+ function parseReleases(raw) {
8659
+ if (!Array.isArray(raw)) {
8660
+ throw new Error("Connector releases response is not a JSON array");
8661
+ }
8662
+ const releases = [];
8663
+ for (const entry of raw) {
8664
+ if (!isRecord(entry)) continue;
8665
+ if (entry.draft === true) continue;
8666
+ const tag = entry.tag_name;
8667
+ if (typeof tag !== "string") continue;
8668
+ const version = import_semver2.default.valid(tag);
8669
+ if (!version || isReleaseCandidate(version)) continue;
8670
+ const assetUrl = findTemplatesAssetUrl(entry);
8671
+ if (!assetUrl) continue;
8672
+ releases.push({ tag, version, assetUrl });
8673
+ }
8674
+ return releases;
8675
+ }
8676
+ function selectLatestPerMinor(releases) {
8677
+ const latest = /* @__PURE__ */ new Map();
8678
+ for (const release of releases) {
8679
+ const key = `${import_semver2.default.major(release.version)}.${import_semver2.default.minor(release.version)}`;
8680
+ const current = latest.get(key);
8681
+ if (!current || import_semver2.default.gt(release.version, current.version)) {
8682
+ latest.set(key, release);
8683
+ }
8684
+ }
8685
+ return [...latest.values()].sort((a, b) => import_semver2.default.rcompare(a.version, b.version)).slice(0, MAX_MINOR_LINES);
8686
+ }
8687
+ async function fetchConnectorReleases() {
8688
+ const url = getReleasesUrl();
8689
+ const response = await fetch(url, {
8690
+ headers: {
8691
+ "User-Agent": USER_AGENT,
8692
+ Accept: "application/vnd.github+json"
8693
+ },
8694
+ signal: AbortSignal.timeout(RELEASES_FETCH_TIMEOUT_MS)
8695
+ });
8696
+ if (!response.ok) {
8697
+ const hint = response.status === 403 || response.status === 429 ? "\nThe GitHub API rate limit may be exhausted \u2014 retry later, or point C8CTL_CONNECTORS_RELEASES_URL at a mirror of the release listing." : "";
8698
+ throw new Error(
8699
+ `HTTP ${response.status} ${response.statusText} for ${url}${hint}`
8700
+ );
8701
+ }
8702
+ return selectLatestPerMinor(parseReleases(await response.json()));
8703
+ }
8704
+ async function fetchReleaseAsset(url) {
8705
+ const response = await fetch(url, {
8706
+ headers: { "User-Agent": USER_AGENT },
8707
+ signal: AbortSignal.timeout(ASSET_FETCH_TIMEOUT_MS)
8708
+ });
8709
+ if (!response.ok) {
8710
+ throw new Error(
8711
+ `HTTP ${response.status} ${response.statusText} for ${url}`
8712
+ );
8713
+ }
8714
+ return new Uint8Array(await response.arrayBuffer());
8715
+ }
8716
+ var TAR_BLOCK_SIZE = 512;
8717
+ function readTarString(tar, offset, length) {
8718
+ const raw = tar.toString("utf-8", offset, offset + length);
8719
+ const nul = raw.indexOf("\0");
8720
+ return nul === -1 ? raw : raw.slice(0, nul);
8721
+ }
8722
+ function extractJsonEntries(gzipped) {
8723
+ const tar = gunzipSync(gzipped, { maxOutputLength: MAX_BUNDLE_BYTES });
8724
+ const entries = [];
8725
+ let offset = 0;
8726
+ while (offset + TAR_BLOCK_SIZE <= tar.length) {
8727
+ if (tar.subarray(offset, offset + TAR_BLOCK_SIZE).every((b) => b === 0)) {
8728
+ break;
8729
+ }
8730
+ const name2 = readTarString(tar, offset, 100);
8731
+ const prefix2 = readTarString(tar, offset + 345, 155);
8732
+ const sizeField = readTarString(tar, offset + 124, 12).trim();
8733
+ const size = Number.parseInt(sizeField, 8);
8734
+ if (!Number.isFinite(size) || size < 0) {
8735
+ throw new Error(
8736
+ `Malformed tar header (bad size field) at byte ${offset}`
8737
+ );
8738
+ }
8739
+ const typeFlag = String.fromCharCode(tar[offset + 156]);
8740
+ const dataStart = offset + TAR_BLOCK_SIZE;
8741
+ const dataEnd = dataStart + size;
8742
+ if (dataEnd > tar.length) {
8743
+ throw new Error(`Truncated tar entry '${name2}' at byte ${offset}`);
8744
+ }
8745
+ const fullName = prefix2 ? `${prefix2}/${name2}` : name2;
8746
+ if ((typeFlag === "0" || typeFlag === "\0") && fullName.endsWith(".json")) {
8747
+ entries.push({
8748
+ name: fullName,
8749
+ content: tar.toString("utf-8", dataStart, dataEnd)
8750
+ });
8751
+ }
8752
+ offset = dataStart + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
8753
+ }
8754
+ return entries;
8755
+ }
8756
+
8757
+ // default-plugins/element-template/cache.ts
8758
+ var FETCH_CONCURRENCY = 4;
8629
8759
  var STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
8630
- var FETCH_TIMEOUT_MS = 3e4;
8631
8760
  var SYNC_LOCK_STALE_AFTER_MS = 60 * 60 * 1e3;
8632
- function getMarketplaceUrl() {
8633
- return process.env.C8CTL_OOTB_ELEMENT_TEMPLATES_URL || DEFAULT_OOTB_URL;
8634
- }
8635
8761
  function getCacheDir() {
8636
8762
  if (!globalThis.c8ctl?.getUserDataDir) {
8637
8763
  throw new Error(
@@ -8796,7 +8922,7 @@ function atomicWriteFileSync(target, contents) {
8796
8922
  throw error3;
8797
8923
  }
8798
8924
  }
8799
- function saveCache(templates) {
8925
+ function saveCache(templates, { stampFetchedAt = true } = {}) {
8800
8926
  const dir = getCacheDir();
8801
8927
  mkdirSync(dir, { recursive: true });
8802
8928
  atomicWriteFileSync(
@@ -8804,7 +8930,9 @@ function saveCache(templates) {
8804
8930
  `${JSON.stringify(templates, null, 2)}
8805
8931
  `
8806
8932
  );
8807
- atomicWriteFileSync(getFetchedAtPath(), String(Date.now()));
8933
+ if (stampFetchedAt) {
8934
+ atomicWriteFileSync(getFetchedAtPath(), String(Date.now()));
8935
+ }
8808
8936
  }
8809
8937
  function isCacheStale() {
8810
8938
  const fetchedAt = loadFetchedAt();
@@ -8825,50 +8953,41 @@ function nudgeIfStale(logger) {
8825
8953
  `Element template cache is ${ageText}. Run 'c8ctl element-template sync' to refresh.`
8826
8954
  );
8827
8955
  }
8828
- async function fetchJson(url) {
8829
- const response = await fetch(url, {
8830
- headers: { "User-Agent": USER_AGENT },
8831
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
8832
- });
8833
- if (!response.ok) {
8834
- throw new Error(
8835
- `HTTP ${response.status} ${response.statusText} for ${url}`
8836
- );
8837
- }
8838
- return response.json();
8839
- }
8840
- async function fetchIndex() {
8841
- const raw = await fetchJson(getMarketplaceUrl());
8842
- if (!isRecord(raw)) {
8843
- throw new Error("Marketplace index is not a JSON object");
8844
- }
8845
- const result = {};
8846
- for (const [id, value] of Object.entries(raw)) {
8847
- if (!Array.isArray(value)) continue;
8848
- const entries = [];
8849
- for (const entry of value) {
8850
- if (!isRecord(entry)) continue;
8851
- entries.push({
8852
- version: typeof entry.version === "number" ? entry.version : void 0,
8853
- ref: typeof entry.ref === "string" ? entry.ref : void 0,
8854
- engine: isRecord(entry.engine) ? {
8855
- camunda: typeof entry.engine.camunda === "string" ? entry.engine.camunda : void 0
8856
- } : void 0
8857
- });
8858
- }
8859
- result[id] = entries;
8860
- }
8861
- return result;
8862
- }
8863
8956
  function isTemplateLike(value) {
8864
8957
  return isRecord(value) && Array.isArray(value.properties);
8865
8958
  }
8866
- async function fetchTemplate(url) {
8867
- const raw = await fetchJson(url);
8868
- if (!isTemplateLike(raw)) {
8869
- throw new Error(`Fetched template at ${url} did not match expected shape`);
8959
+ function buildUpstreamRef(assetUrl, entryName) {
8960
+ return `${assetUrl}#${entryName}`;
8961
+ }
8962
+ function assetUrlOfRef(ref) {
8963
+ const hash = ref.indexOf("#");
8964
+ return hash === -1 ? ref : ref.slice(0, hash);
8965
+ }
8966
+ async function fetchReleaseTemplates(release) {
8967
+ const bundle = await fetchReleaseAsset(release.assetUrl);
8968
+ const templates = [];
8969
+ for (const entry of extractJsonEntries(bundle)) {
8970
+ let parsed;
8971
+ try {
8972
+ parsed = JSON.parse(entry.content);
8973
+ } catch {
8974
+ continue;
8975
+ }
8976
+ if (!isTemplateLike(parsed) || typeof parsed.version !== "number") {
8977
+ continue;
8978
+ }
8979
+ parsed.metadata = {
8980
+ ...parsed.metadata,
8981
+ upstreamRef: buildUpstreamRef(release.assetUrl, entry.name)
8982
+ };
8983
+ templates.push(parsed);
8984
+ }
8985
+ if (templates.length === 0) {
8986
+ throw new Error(
8987
+ `Bundle for release ${release.tag} contained no element templates`
8988
+ );
8870
8989
  }
8871
- return raw;
8990
+ return templates;
8872
8991
  }
8873
8992
  async function pool(items, concurrency, fn) {
8874
8993
  const queue = items.slice();
@@ -8884,20 +9003,12 @@ async function pool(items, concurrency, fn) {
8884
9003
  );
8885
9004
  await Promise.all(workers);
8886
9005
  }
8887
- function flattenIndex(index) {
8888
- const entries = [];
8889
- for (const [id, versions] of Object.entries(index)) {
8890
- for (const entry of versions) {
8891
- if (entry.version === void 0 || entry.ref === void 0) continue;
8892
- entries.push({
8893
- id,
8894
- version: entry.version,
8895
- ref: entry.ref,
8896
- engine: entry.engine
8897
- });
8898
- }
8899
- }
8900
- return entries;
9006
+ function sortTemplates(templates) {
9007
+ return [...templates].sort((a, b) => {
9008
+ const idCmp = (a.id ?? "").localeCompare(b.id ?? "");
9009
+ if (idCmp !== 0) return idCmp;
9010
+ return (a.version ?? 0) - (b.version ?? 0);
9011
+ });
8901
9012
  }
8902
9013
  async function syncTemplates({
8903
9014
  logger,
@@ -8909,11 +9020,16 @@ async function syncTemplatesLocked({
8909
9020
  logger,
8910
9021
  prune
8911
9022
  }) {
8912
- logger.info(`Fetching index from ${getMarketplaceUrl()} ...`);
8913
- const index = await fetchIndex();
8914
- const entries = flattenIndex(index);
9023
+ const releasesUrl = getReleasesUrl();
9024
+ logger.info(`Fetching connector releases from ${releasesUrl} ...`);
9025
+ const releases = await fetchConnectorReleases();
9026
+ if (releases.length === 0) {
9027
+ throw new Error(
9028
+ `No connector release with an element-template bundle found at ${releasesUrl}.`
9029
+ );
9030
+ }
8915
9031
  logger.info(
8916
- `Index has ${entries.length} template versions across ${Object.keys(index).length} connectors.`
9032
+ `Latest release per minor: ${releases.map((r) => r.tag).join(", ")}.`
8917
9033
  );
8918
9034
  let existing;
8919
9035
  try {
@@ -8923,70 +9039,90 @@ async function syncTemplatesLocked({
8923
9039
  logger.warn(`Corrupt cache \u2014 starting fresh: ${message}`);
8924
9040
  existing = [];
8925
9041
  }
8926
- const byUpstreamRef = /* @__PURE__ */ new Map();
9042
+ const selectedAssets = new Set(releases.map((r) => r.assetUrl));
9043
+ const cachedByAsset = /* @__PURE__ */ new Map();
9044
+ const staleCached = [];
8927
9045
  for (const tpl of existing) {
8928
9046
  const ref = tpl.metadata?.upstreamRef;
8929
- if (ref) byUpstreamRef.set(ref, tpl);
9047
+ const asset = ref ? assetUrlOfRef(ref) : void 0;
9048
+ if (asset && selectedAssets.has(asset)) {
9049
+ const bucket = cachedByAsset.get(asset);
9050
+ if (bucket) bucket.push(tpl);
9051
+ else cachedByAsset.set(asset, [tpl]);
9052
+ } else {
9053
+ staleCached.push(tpl);
9054
+ }
8930
9055
  }
8931
- const freshRefs = new Set(entries.map((e) => e.ref));
8932
- const toFetch = entries.filter((e) => !byUpstreamRef.has(e.ref));
9056
+ const toFetch = releases.filter((r) => !cachedByAsset.has(r.assetUrl));
9057
+ const reusedCount = [...cachedByAsset.values()].reduce(
9058
+ (sum, bucket) => sum + bucket.length,
9059
+ 0
9060
+ );
8933
9061
  logger.info(
8934
- `${byUpstreamRef.size > 0 ? `Reusing ${entries.length - toFetch.length} cached, ` : ""}fetching ${toFetch.length} new...`
9062
+ `${reusedCount > 0 ? `Reusing ${reusedCount} cached templates from ${cachedByAsset.size} bundle(s), ` : ""}downloading ${toFetch.length} bundle(s)...`
8935
9063
  );
8936
- let fetched = 0;
8937
9064
  let errors = 0;
8938
9065
  let progress = 0;
8939
- const fetchedTemplates = [];
8940
- await pool(toFetch, FETCH_CONCURRENCY, async (entry) => {
9066
+ const fetchedByAsset = /* @__PURE__ */ new Map();
9067
+ await pool(toFetch, FETCH_CONCURRENCY, async (release) => {
8941
9068
  progress += 1;
8942
9069
  const myProgress = progress;
8943
- const label = `${entry.id}@${entry.version}`;
8944
9070
  try {
8945
- const template = await fetchTemplate(entry.ref);
8946
- template.metadata = template.metadata || {};
8947
- template.metadata.upstreamRef = entry.ref;
8948
- fetchedTemplates.push(template);
8949
- fetched += 1;
8950
- logger.info(` [${myProgress}/${toFetch.length}] ${label}`);
9071
+ const templates = await fetchReleaseTemplates(release);
9072
+ fetchedByAsset.set(release.assetUrl, templates);
9073
+ logger.info(
9074
+ ` [${myProgress}/${toFetch.length}] ${release.tag} \u2014 ${templates.length} templates`
9075
+ );
8951
9076
  } catch (error3) {
8952
9077
  errors += 1;
8953
9078
  const message = error3 instanceof Error ? error3.message : String(error3);
8954
- logger.warn(` [${myProgress}/${toFetch.length}] ${label} \u2014 ${message}`);
9079
+ logger.warn(
9080
+ ` [${myProgress}/${toFetch.length}] ${release.tag} \u2014 ${message}`
9081
+ );
8955
9082
  }
8956
9083
  });
8957
- fetchedTemplates.sort((a, b) => {
8958
- const idCmp = (a.id ?? "").localeCompare(b.id ?? "");
8959
- if (idCmp !== 0) return idCmp;
8960
- return (a.version ?? 0) - (b.version ?? 0);
8961
- });
8962
9084
  const next = [];
8963
- for (const tpl of existing) {
8964
- const ref = tpl.metadata?.upstreamRef;
8965
- if (ref && freshRefs.has(ref)) {
8966
- next.push(tpl);
9085
+ const seen = /* @__PURE__ */ new Set();
9086
+ let fetched = 0;
9087
+ let cached = 0;
9088
+ const add = (tpl) => {
9089
+ const key = `${tpl.id}@${tpl.version}`;
9090
+ if (seen.has(key)) return false;
9091
+ seen.add(key);
9092
+ next.push(tpl);
9093
+ return true;
9094
+ };
9095
+ for (const release of releases) {
9096
+ const fresh = fetchedByAsset.get(release.assetUrl);
9097
+ const templates = fresh ?? cachedByAsset.get(release.assetUrl) ?? [];
9098
+ for (const tpl of sortTemplates(templates)) {
9099
+ if (!add(tpl)) continue;
9100
+ if (fresh) fetched += 1;
9101
+ else cached += 1;
8967
9102
  }
8968
9103
  }
8969
- next.push(...fetchedTemplates);
8970
- let pruned = 0;
8971
- if (prune) {
8972
- pruned = existing.filter((t) => {
8973
- const ref = t.metadata?.upstreamRef;
8974
- return ref === void 0 || !freshRefs.has(ref);
8975
- }).length;
9104
+ const pruning = prune && errors === 0;
9105
+ if (prune && errors > 0) {
9106
+ logger.warn(
9107
+ `Skipping prune: ${errors} bundle(s) failed to download \u2014 cached templates were kept.`
9108
+ );
8976
9109
  }
8977
- if (!prune) {
8978
- for (const tpl of existing) {
8979
- const ref = tpl.metadata?.upstreamRef;
8980
- if (ref && !freshRefs.has(ref) && !next.includes(tpl)) {
8981
- next.push(tpl);
8982
- }
9110
+ if (!pruning) {
9111
+ for (const tpl of sortTemplates(staleCached)) {
9112
+ add(tpl);
8983
9113
  }
8984
9114
  }
8985
- saveCache(next);
9115
+ const pruned = pruning ? staleCached.length : 0;
9116
+ if (next.length === 0) {
9117
+ throw new Error(
9118
+ `No element templates could be downloaded (${errors} bundle(s) failed). Check the warnings above and retry 'c8ctl element-template sync'.`
9119
+ );
9120
+ }
9121
+ saveCache(next, { stampFetchedAt: errors === 0 });
8986
9122
  const summary = {
8987
9123
  total: next.length,
8988
9124
  fetched,
8989
- cached: entries.length - toFetch.length,
9125
+ cached,
8990
9126
  errors,
8991
9127
  pruned
8992
9128
  };
@@ -9015,12 +9151,12 @@ function pickVersion(templates, { version, executionPlatformVersion } = {}) {
9015
9151
  }
9016
9152
  let candidates = templates.filter((t) => t.version !== void 0);
9017
9153
  if (executionPlatformVersion) {
9018
- const coerced = import_semver2.default.coerce(executionPlatformVersion);
9154
+ const coerced = import_semver3.default.coerce(executionPlatformVersion);
9019
9155
  if (coerced) {
9020
9156
  candidates = candidates.filter((t) => {
9021
9157
  const constraint = t.engines?.camunda;
9022
9158
  if (!constraint) return true;
9023
- return import_semver2.default.satisfies(coerced, constraint);
9159
+ return import_semver3.default.satisfies(coerced, constraint);
9024
9160
  });
9025
9161
  }
9026
9162
  }
@@ -9040,12 +9176,12 @@ function searchTemplates(query, { executionPlatformVersion } = {}) {
9040
9176
  return name2.includes(q) || description.includes(q) || id.includes(q) || keywords.includes(q);
9041
9177
  }).filter((t) => !t.deprecated);
9042
9178
  if (executionPlatformVersion) {
9043
- const coerced = import_semver2.default.coerce(executionPlatformVersion);
9179
+ const coerced = import_semver3.default.coerce(executionPlatformVersion);
9044
9180
  if (coerced) {
9045
9181
  matches = matches.filter((t) => {
9046
9182
  const constraint = t.engines?.camunda;
9047
9183
  if (!constraint) return true;
9048
- return import_semver2.default.satisfies(coerced, constraint);
9184
+ return import_semver3.default.satisfies(coerced, constraint);
9049
9185
  });
9050
9186
  }
9051
9187
  }
@@ -10463,7 +10599,7 @@ var commands = {
10463
10599
  },
10464
10600
  prune: {
10465
10601
  type: "boolean",
10466
- description: "Drop cached entries no longer in the index [sync]"
10602
+ description: "Drop cached entries no longer in a selected release [sync]"
10467
10603
  },
10468
10604
  "no-icon": {
10469
10605
  type: "boolean",
@@ -24,29 +24,61 @@ shipped layout but during development we run directly from
24
24
 
25
25
  ## OOTB template integration
26
26
 
27
- Three sources publish the OOTB connector templates:
27
+ Four sources publish the OOTB connector templates:
28
28
 
29
29
  | | Source-of-truth | Inlines templates? | Has all versions? | Used by |
30
30
  |---|---|---|---|---|
31
+ | `github.com/camunda/connectors` releases (bundle asset) | yes | yes (~1 MB gzip per line) | per minor line | c8ctl |
31
32
  | `marketplace.cloud.camunda.io/api/v1/ootb-connectors` | mirrors GH | no, URL refs | yes | Desktop Modeler |
32
33
  | `github.com/camunda/connectors/connector-templates.json` | yes | no, URL refs | yes | (the marketplace) |
33
34
  | `@camunda/connectors-element-templates` (npm) | derived | yes (~9MB) | yes, may lag | (Web Modeler skill) |
34
35
 
35
- We chose the **marketplace endpoint** for parity with Desktop Modeler:
36
-
37
- - Desktop Modeler is released and deployed widely; the endpoint contract
38
- is effectively stable.
39
- - The endpoint already proxies the GH source-of-truth; using it means
40
- we benefit from any future server-side filtering/versioning the
41
- marketplace adds.
36
+ We use the **release bundle assets** every connectors release ships a
37
+ `connectors-bundle-templates-<tag>.tar.gz` with one JSON file per
38
+ template:
39
+
40
+ - The marketplace endpoint (Desktop Modeler's source, and c8ctl's
41
+ original choice) only publishes *refs* into
42
+ `raw.githubusercontent.com`, a host routinely blocked in enterprise
43
+ networks — the reason for the switch, see c8ctl#530. Release
44
+ downloads are served from `github.com`, which those environments
45
+ generally do reach.
46
+ - One request per minor line replaces ~650 per-template requests, so a
47
+ cold sync is a few MB and a few seconds instead of ~14 MB and ~30 s.
42
48
  - npm package was rejected: confirmed lag against the GH source and
43
49
  some entries are missing `version`/`engines` (pre-versioned legacy
44
50
  templates).
45
- - GH directly was rejected because the marketplace gives us the same
46
- content with one less reason to drift from Modeler's behavior.
47
51
 
48
- The endpoint is overridable via `C8CTL_OOTB_ELEMENT_TEMPLATES_URL`
49
- (useful for testing).
52
+ ### Which releases
53
+
54
+ `sync` lists the releases (`api.github.com/repos/camunda/connectors/
55
+ releases?per_page=100`, overridable via
56
+ `C8CTL_CONNECTORS_RELEASES_URL`) and keeps **the newest release of each
57
+ minor line** — 8.8.x, 8.9.x, 8.10.x, ...:
58
+
59
+ - Bundles are cumulative within a line, so the newest patch of 8.8
60
+ already contains everything 8.8.17 shipped. Across lines they differ,
61
+ which is why one release per line is kept rather than just the newest
62
+ release overall.
63
+ - Alphas count (`8.10.0-alpha3` is the only source for a line that has
64
+ no stable release yet); release candidates do not — they are
65
+ superseded within days. Note that semver ranks `8.10.0-alpha5-rc3`
66
+ *above* `8.10.0-alpha5`, so RCs are filtered explicitly rather than
67
+ by ordering.
68
+ - Draft releases and releases without a bundle asset (a release that is
69
+ still being built) are skipped, so an in-flight release falls back to
70
+ the previous one on that line.
71
+ - Only the **4 newest minor lines** are kept (`MAX_MINOR_LINES` in
72
+ `releases.ts`), roughly Camunda's supported-version window. This keeps
73
+ the selection deterministic: the newest release of each selected line
74
+ is always within the first page of the listing, whereas an EOL line's
75
+ newest release drifts down the listing until it falls off the page and
76
+ would silently vanish from the selection.
77
+
78
+ Bundles contain a superset of the marketplace index: the `-hybrid`
79
+ variants and a few templates the marketplace does not list are cached
80
+ too, and templates without a numeric `version` (pre-versioned legacy
81
+ entries that version resolution cannot rank) are skipped.
50
82
 
51
83
  ## Cache strategy
52
84
 
@@ -56,12 +88,17 @@ We mirror Desktop Modeler's approach
56
88
  - Cache lives in `<userDataDir>/element-templates/`:
57
89
  - `templates.json` — flat array of all template objects (matches
58
90
  Modeler's `.camunda-connector-templates.json` shape).
59
- - `fetched-at` — epoch ms of last index sync.
60
- - Each cached template gets `metadata.upstreamRef = <ref-url>` injected.
61
- Since each `ref` is a commit-pinned `raw.githubusercontent.com` URL,
62
- "upstreamRef unchanged" "content unchanged" no re-fetch needed.
63
- Subsequent syncs only fetch refs that aren't already in the cache.
64
- - Per-template fetch failures are logged + counted, never abort the run.
91
+ - `fetched-at` — epoch ms of last sync.
92
+ - Each cached template gets
93
+ `metadata.upstreamRef = <assetUrl>#<file-in-bundle>` injected. Release
94
+ assets are tag-pinned and therefore immutable, so "asset URL already
95
+ in the cache" "bundle already ingested" no re-download.
96
+ - `id@version` is deduplicated across bundles, newest release wins.
97
+ - Per-release fetch failures are logged + counted, and never abort the
98
+ run — unless *every* bundle failed and nothing was reusable from the
99
+ cache, in which case `sync` errors out rather than writing an empty
100
+ cache. A partial sync also leaves `fetched-at` untouched, so the
101
+ staleness nudge keeps asking for a full refresh.
65
102
 
66
103
  ### Lifecycle
67
104
 
@@ -79,11 +116,16 @@ We mirror Desktop Modeler's approach
79
116
  arg in `parseTemplateRef` before any cache call.
80
117
  - **Stale cache** (>7 days since `fetched-at`): warn-only, suggesting
81
118
  `c8ctl element-template sync`. We don't auto-refresh — surprise
82
- network activity inside `apply` is undesirable and the index is
83
- small enough that manual sync is cheap.
84
- - **`sync`** always re-fetches the index but skips already-cached
85
- refs. **`sync --prune`** drops cached entries no longer in the
86
- fresh index (opt-in: a user may keep a legacy version intentionally).
119
+ network activity inside `apply` is undesirable and a manual sync is
120
+ cheap.
121
+ - **`sync`** always re-fetches the release listing but only downloads
122
+ bundles that aren't cached yet. **`sync --prune`** drops cached
123
+ entries that no longer belong to a selected release (opt-in: a user
124
+ may keep a legacy line intentionally). Pruning is skipped when any
125
+ bundle failed to download: a bundle URL changes with every patch, so
126
+ the previous patch's cached templates are always "stale", and dropping
127
+ them while their replacement failed would delete a whole minor line
128
+ over one transient HTTP error.
87
129
  - **Atomicity**: `sync` writes `templates.json` and `fetched-at`
88
130
  via a sibling temp file + `renameSync`, so a kill mid-sync leaves
89
131
  the previous cache intact. `apply --in-place` uses the same recipe
@@ -100,9 +142,9 @@ We mirror Desktop Modeler's approach
100
142
 
101
143
  Earlier draft considered fetching only the requested `(id, version)`
102
144
  on demand. Rejected because **search needs the template names**, which
103
- live inside the template files (the index has only `id`/`version`/`ref`).
104
- Eager bulk fetch makes search instant; the ~14 MB / ~30 s first-run
105
- cost is acceptable, and incremental syncs are cheap.
145
+ means every template has to be read anyway. Bundles make this moot: the
146
+ whole line arrives in one request, so eager bulk fetch is both simpler
147
+ and faster than any lazy scheme.
106
148
 
107
149
  ## Version resolution
108
150
 
@@ -39,20 +39,37 @@ export type ResolvedPositionals<V extends keyof Registry, R extends string> = Re
39
39
  /**
40
40
  * Map a flag schema to typed handler parameters.
41
41
  *
42
- * - Flags with `validate` the validator's return type
42
+ * - `multiple: true` flags → `R[]` when combined with `validate` (each
43
+ * element validated), else `string[]` — `| undefined` unless
44
+ * `required: true`. Checked before `validate`/`type` so a flag combining
45
+ * `multiple` with `validate` infers an array — matching
46
+ * `deserializeFlags`, which always returns an array for a `multiple`
47
+ * flag regardless of `validate`.
48
+ * - Flags with `validate` (non-`multiple`) → the validator's return type
43
49
  * - Boolean flags → boolean
44
50
  * - Everything else → string
45
51
  *
46
52
  * A flag is non-optional in the handler's view iff it is declared
47
53
  * `required: true` in the FlagDef. `required: true` is enforced at the
48
- * framework boundary by validateFlags (#308), so the handler is guaranteed
49
- * to see a value.
54
+ * framework boundary by validateFlags (#308) including for `multiple`
55
+ * flags: `validateFlags` treats a repeated flag's array as present when
56
+ * *any* element is a non-empty string (not just the last, since every
57
+ * element of a `multiple` flag is independently meaningful), so the
58
+ * handler is guaranteed to see a value there too.
50
59
  *
51
60
  * The type parameter is unconstrained so conditional types like
52
61
  * `ResolvedFlags<V, R>` can be passed through without constraint errors.
53
62
  */
54
63
  export type InferFlags<F extends Record<string, any>> = {
55
64
  [K in keyof F]: F[K] extends {
65
+ multiple: true;
66
+ } ? F[K] extends {
67
+ validate: (v: string) => infer R;
68
+ } ? F[K] extends {
69
+ required: true;
70
+ } ? R[] : R[] | undefined : F[K] extends {
71
+ required: true;
72
+ } ? string[] : string[] | undefined : F[K] extends {
56
73
  validate: (v: string) => infer R;
57
74
  } ? F[K] extends {
58
75
  required: true;
@@ -247,9 +264,12 @@ export declare function defineCommand<V extends keyof Registry, R extends string
247
264
  * Deserialize raw parseArgs values into typed flags.
248
265
  *
249
266
  * For each flag in the schema:
267
+ * - If the flag is boolean, extract the boolean value.
268
+ * - If the flag is `multiple: true`, collect every supplied value into an
269
+ * array (`undefined` when never supplied), applying `validate` to each
270
+ * element when present.
250
271
  * - If the flag has a `validate` function and the raw value is a non-empty
251
272
  * string, call the validator (which returns a branded type).
252
- * - If the flag is boolean, extract the boolean value.
253
273
  * - Otherwise, extract the string value.
254
274
  *
255
275
  * Validators that throw are intentionally NOT caught here — validation