@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.
- package/dist/commands/profiles.d.ts.map +1 -1
- package/dist/commands/profiles.js +19 -3
- package/dist/commands/profiles.js.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +1 -1
- package/dist/commands/search.js.map +1 -1
- package/dist/core/client.d.ts +35 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +161 -5
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +56 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +64 -0
- package/dist/core/config.js.map +1 -1
- package/dist/default-plugins/element-template/AGENTS.md +19 -10
- package/dist/default-plugins/element-template/README.md +13 -9
- package/dist/default-plugins/element-template/c8ctl-plugin.js +249 -113
- package/dist/default-plugins/element-template/docs/design.md +68 -26
- package/dist/framework/command-framework.d.ts +24 -4
- package/dist/framework/command-framework.d.ts.map +1 -1
- package/dist/framework/command-framework.js +16 -1
- package/dist/framework/command-framework.js.map +1 -1
- package/dist/framework/command-registry.d.ts +13 -0
- package/dist/framework/command-registry.d.ts.map +1 -1
- package/dist/framework/command-registry.js +16 -0
- package/dist/framework/command-registry.js.map +1 -1
- package/dist/framework/command-validation.d.ts +5 -0
- package/dist/framework/command-validation.d.ts.map +1 -1
- package/dist/framework/command-validation.js +55 -10
- package/dist/framework/command-validation.js.map +1 -1
- package/dist/utils/shared/npm-exec.d.ts +12 -0
- package/dist/utils/shared/npm-exec.d.ts.map +1 -1
- package/dist/utils/shared/npm-exec.js +49 -1
- package/dist/utils/shared/npm-exec.js.map +1 -1
- 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/
|
|
8614
|
-
var
|
|
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
|
-
|
|
8628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
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
|
|
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
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
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
|
-
|
|
8913
|
-
|
|
8914
|
-
const
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
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
|
|
8932
|
-
const
|
|
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
|
-
`${
|
|
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
|
|
8940
|
-
await pool(toFetch, FETCH_CONCURRENCY, async (
|
|
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
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
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(
|
|
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
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
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
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
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 (!
|
|
8978
|
-
for (const tpl of
|
|
8979
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- The endpoint
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
60
|
-
- Each cached template gets
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
-
|
|
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
|
|
83
|
-
|
|
84
|
-
- **`sync`** always re-fetches the
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
* -
|
|
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)
|
|
49
|
-
*
|
|
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
|