@verentis/cli 0.2.4 → 0.2.5
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/README.md +70 -3
- package/dist/index.js +637 -79
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { mkdir, stat, writeFile, readFile, readdir, mkdtemp, rm, glob } from 'fs/promises';
|
|
3
|
+
import { mkdir, stat, writeFile, readFile, readdir, realpath, mkdtemp, rm, glob } from 'fs/promises';
|
|
4
4
|
import { tmpdir, homedir } from 'os';
|
|
5
|
-
import { resolve, basename, dirname, posix, join,
|
|
5
|
+
import { resolve, basename, dirname, posix, join, isAbsolute, extname, relative, sep } from 'path';
|
|
6
6
|
import { spawn, spawnSync } from 'child_process';
|
|
7
7
|
import { generateKeyPairSync, createHash, randomUUID, randomBytes, createCipheriv, createPublicKey, verify, diffieHellman, hkdfSync, sign, createPrivateKey } from 'crypto';
|
|
8
|
-
import { parse } from 'yaml';
|
|
8
|
+
import { parse, stringify } from 'yaml';
|
|
9
9
|
import { createWriteStream, readFileSync, createReadStream } from 'fs';
|
|
10
10
|
import { Readable } from 'stream';
|
|
11
11
|
import { pipeline } from 'stream/promises';
|
|
@@ -130,7 +130,7 @@ async function refreshIdentityToken(apiUrl, refreshToken) {
|
|
|
130
130
|
`Session could not be renewed${payload?.error ? ` (${payload.error})` : ` (${res.status})`}. Run \`verentis login\` again.`
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
|
-
var sleep = (ms) => new Promise((
|
|
133
|
+
var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
134
134
|
|
|
135
135
|
// src/http.ts
|
|
136
136
|
var ApiError = class extends Error {
|
|
@@ -298,7 +298,6 @@ ${truncate(text)}`;
|
|
|
298
298
|
}
|
|
299
299
|
var SCOPES = {
|
|
300
300
|
publisherRead: "marketplace.publisher.read",
|
|
301
|
-
publisherCreate: "marketplace.publisher.create",
|
|
302
301
|
publisherUpdate: "marketplace.publisher.update",
|
|
303
302
|
packageRead: "marketplace.package.read",
|
|
304
303
|
packageReadAll: "marketplace.package.read-all",
|
|
@@ -367,6 +366,8 @@ var MARKETPLACE = {
|
|
|
367
366
|
packageRead: "marketplace.package.read",
|
|
368
367
|
packageUpdate: "marketplace.package.update",
|
|
369
368
|
packageVisibilityManage: "marketplace.package.visibility.manage",
|
|
369
|
+
publisherCreate: "marketplace.publisher.create",
|
|
370
|
+
publisherUpdate: "marketplace.publisher.update",
|
|
370
371
|
statsRead: "marketplace.stats.read"};
|
|
371
372
|
var ENGINE_DEFAULT_PERMISSIONS = [
|
|
372
373
|
NODE.fileRead,
|
|
@@ -455,6 +456,10 @@ var execution = {
|
|
|
455
456
|
syncEngines: e({ method: "POST", path: "/v1/engines/sync", scopes: [EXECUTION.run], workspaceBound: true })
|
|
456
457
|
};
|
|
457
458
|
var marketplace = {
|
|
459
|
+
createPublisher: e({ method: "POST", path: "/v1/publishers", scopes: [MARKETPLACE.publisherCreate] }),
|
|
460
|
+
updatePublisher: e({ method: "PUT", path: "/v1/publishers/{id}", scopes: [MARKETPLACE.publisherUpdate] }),
|
|
461
|
+
setPublisherLogo: e({ method: "PUT", path: "/v1/publishers/{id}/logo", scopes: [MARKETPLACE.publisherUpdate] }),
|
|
462
|
+
removePublisherLogo: e({ method: "DELETE", path: "/v1/publishers/{id}/logo", scopes: [MARKETPLACE.publisherUpdate] }),
|
|
458
463
|
getPackage: e({ method: "GET", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageRead] }),
|
|
459
464
|
updatePackage: e({ method: "PUT", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageUpdate] }),
|
|
460
465
|
archivePackage: e({ method: "DELETE", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageDelete] }),
|
|
@@ -845,6 +850,122 @@ function contextEnv(context, contextPath) {
|
|
|
845
850
|
VERENTIS_TIMEOUT: String(context.execution.timeout)
|
|
846
851
|
};
|
|
847
852
|
}
|
|
853
|
+
var KIND_MAP = {
|
|
854
|
+
application: "Application",
|
|
855
|
+
executionengine: "ExecutionEngine",
|
|
856
|
+
bundle: "Bundle"
|
|
857
|
+
};
|
|
858
|
+
function normalizeKind(kind) {
|
|
859
|
+
const mapped = KIND_MAP[kind.replace(/[^a-z]/gi, "").toLowerCase()];
|
|
860
|
+
if (!mapped) throw new Error(`Unsupported manifest kind '${kind}' \u2014 expected Application, ExecutionEngine or Bundle.`);
|
|
861
|
+
return mapped;
|
|
862
|
+
}
|
|
863
|
+
function validateLaunchManifestKind(kind, spec) {
|
|
864
|
+
if (kind === "Application" || !spec || typeof spec !== "object" || Array.isArray(spec)) return void 0;
|
|
865
|
+
if (!Object.prototype.hasOwnProperty.call(spec, "launch")) return void 0;
|
|
866
|
+
return "spec.launch is only supported for Application manifests";
|
|
867
|
+
}
|
|
868
|
+
async function loadManifest(path) {
|
|
869
|
+
const text = await readFile(path, "utf8");
|
|
870
|
+
const manifest = parse(text);
|
|
871
|
+
if (!manifest || typeof manifest !== "object") throw new Error(`Failed to parse manifest at ${path}.`);
|
|
872
|
+
if (!manifest.kind) throw new Error(`Manifest at ${path} is missing 'kind'.`);
|
|
873
|
+
if (!manifest.metadata?.name) throw new Error(`Manifest at ${path} is missing 'metadata.name'.`);
|
|
874
|
+
if (!manifest.metadata?.version) throw new Error(`Manifest at ${path} is missing 'metadata.version'.`);
|
|
875
|
+
const kind = normalizeKind(manifest.kind);
|
|
876
|
+
const launchKindError = validateLaunchManifestKind(kind, manifest.spec);
|
|
877
|
+
if (launchKindError) throw new Error(`Invalid manifest at ${path}: ${launchKindError}.`);
|
|
878
|
+
const marketplaceErrors = validateMarketplaceSection(manifest.marketplace);
|
|
879
|
+
if (marketplaceErrors.length > 0) {
|
|
880
|
+
throw new Error(`Invalid marketplace section in ${path}: ${marketplaceErrors.join(" ")}`);
|
|
881
|
+
}
|
|
882
|
+
return manifest;
|
|
883
|
+
}
|
|
884
|
+
function validateMarketplaceSection(marketplace2) {
|
|
885
|
+
if (marketplace2 === void 0) return [];
|
|
886
|
+
if (!marketplace2 || typeof marketplace2 !== "object" || Array.isArray(marketplace2)) {
|
|
887
|
+
return ["marketplace must be an object."];
|
|
888
|
+
}
|
|
889
|
+
const section = marketplace2;
|
|
890
|
+
const errors = [];
|
|
891
|
+
for (const field of ["logo", "readme", "changelog"]) {
|
|
892
|
+
const value = section[field];
|
|
893
|
+
if (value !== void 0 && (typeof value !== "string" || value.trim().length === 0)) {
|
|
894
|
+
errors.push(`marketplace.${field} must be a non-empty relative path.`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (section.hero !== void 0) {
|
|
898
|
+
if (!section.hero || typeof section.hero !== "object" || Array.isArray(section.hero)) {
|
|
899
|
+
errors.push("marketplace.hero must be an object.");
|
|
900
|
+
} else {
|
|
901
|
+
const hero = section.hero;
|
|
902
|
+
if (typeof hero.source !== "string" || hero.source.trim().length === 0) {
|
|
903
|
+
errors.push("marketplace.hero.source must be a non-empty relative path.");
|
|
904
|
+
}
|
|
905
|
+
if (typeof hero.alt !== "string" || hero.alt.trim().length === 0) {
|
|
906
|
+
errors.push("marketplace.hero.alt is required and must not be empty.");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (section.images !== void 0) {
|
|
911
|
+
if (!Array.isArray(section.images)) {
|
|
912
|
+
errors.push("marketplace.images must be a list.");
|
|
913
|
+
} else {
|
|
914
|
+
if (section.images.length > 8) errors.push("marketplace.images may contain at most 8 images.");
|
|
915
|
+
section.images.forEach((value, index) => {
|
|
916
|
+
const at = `marketplace.images[${index}]`;
|
|
917
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
918
|
+
errors.push(`${at} must be an object.`);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const image = value;
|
|
922
|
+
if (typeof image.source !== "string" || image.source.trim().length === 0) {
|
|
923
|
+
errors.push(`${at}.source must be a non-empty relative path.`);
|
|
924
|
+
}
|
|
925
|
+
if (typeof image.alt !== "string" || image.alt.trim().length === 0) {
|
|
926
|
+
errors.push(`${at}.alt is required and must not be empty.`);
|
|
927
|
+
}
|
|
928
|
+
if (image.caption !== void 0 && typeof image.caption !== "string") {
|
|
929
|
+
errors.push(`${at}.caption must be a string when provided.`);
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (section.assets !== void 0) {
|
|
935
|
+
if (!Array.isArray(section.assets)) {
|
|
936
|
+
errors.push("marketplace.assets must be a list of relative glob strings.");
|
|
937
|
+
} else {
|
|
938
|
+
section.assets.forEach((value, index) => {
|
|
939
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
940
|
+
errors.push(`marketplace.assets[${index}] must be a non-empty relative glob string.`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return errors;
|
|
946
|
+
}
|
|
947
|
+
async function findManifest(dir) {
|
|
948
|
+
const explicit = join(dir, "manifest.yaml");
|
|
949
|
+
try {
|
|
950
|
+
if ((await stat(explicit)).isFile()) return explicit;
|
|
951
|
+
} catch {
|
|
952
|
+
}
|
|
953
|
+
const entries = await readdir(dir);
|
|
954
|
+
const candidates = entries.filter((name) => /\.(app|engine|bundle)\.ya?ml$/.test(name));
|
|
955
|
+
if (candidates.length === 1) return join(dir, candidates[0]);
|
|
956
|
+
if (candidates.length === 0) {
|
|
957
|
+
throw new Error(`No manifest found in ${dir}. Expected manifest.yaml or *.app.yaml / *.engine.yaml / *.bundle.yaml.`);
|
|
958
|
+
}
|
|
959
|
+
throw new Error(`Multiple manifests found in ${dir} (${candidates.join(", ")}). Pass --manifest to disambiguate.`);
|
|
960
|
+
}
|
|
961
|
+
var NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
962
|
+
function validatePackageName(name, label) {
|
|
963
|
+
if (!NAME_PATTERN.test(name)) {
|
|
964
|
+
throw new Error(`Invalid ${label} '${name}' \u2014 use lowercase letters, digits and hyphens (no leading/trailing hyphen).`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/dev/manifest.ts
|
|
848
969
|
async function readManifestDocument(path) {
|
|
849
970
|
const text = await readFile(path, "utf8");
|
|
850
971
|
const doc = parse(text);
|
|
@@ -874,6 +995,8 @@ function validateManifest(manifest) {
|
|
|
874
995
|
if (!meta[field]) error(`metadata.${field} is required`);
|
|
875
996
|
}
|
|
876
997
|
const spec = manifest.spec ?? {};
|
|
998
|
+
const launchKindError = validateLaunchManifestKind(kind, spec);
|
|
999
|
+
if (launchKindError) error(launchKindError);
|
|
877
1000
|
if (kind === "ExecutionEngine") {
|
|
878
1001
|
const runtimes = spec.runtimes ?? {};
|
|
879
1002
|
if (Object.keys(runtimes).length === 0) {
|
|
@@ -902,12 +1025,62 @@ function validateManifest(manifest) {
|
|
|
902
1025
|
"an Application must declare at least one contribution: spec.entry / spec.entry-points[] (iframe UI), spec.install[] (installed files), or spec.scopes[] (identity)"
|
|
903
1026
|
);
|
|
904
1027
|
}
|
|
1028
|
+
validateApplicationLaunch(spec, issues);
|
|
905
1029
|
}
|
|
906
1030
|
validateInstallDirectives(spec.install, issues);
|
|
1031
|
+
for (const message of validateMarketplaceSection(manifest.marketplace)) error(message);
|
|
907
1032
|
const packaging = manifest.packaging ?? {};
|
|
908
1033
|
if (!packaging.publisher) warning("packaging.publisher is not set \u2014 `verentis pack`/`publish` will refuse the package");
|
|
909
1034
|
return issues;
|
|
910
1035
|
}
|
|
1036
|
+
function validateApplicationLaunch(spec, issues) {
|
|
1037
|
+
const launch = spec.launch;
|
|
1038
|
+
if (launch === void 0) return;
|
|
1039
|
+
const error = (message) => issues.push({ level: "error", message });
|
|
1040
|
+
if (!launch || typeof launch !== "object" || Array.isArray(launch)) {
|
|
1041
|
+
error("spec.launch must be an object");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (launch.pinnable === true && launch.enabled !== true) {
|
|
1045
|
+
error("spec.launch.pinnable requires spec.launch.enabled");
|
|
1046
|
+
}
|
|
1047
|
+
if (launch.enabled !== true) return;
|
|
1048
|
+
const launchEntryPoint = launch["entry-point"];
|
|
1049
|
+
if (launchEntryPoint !== void 0 && (typeof launchEntryPoint !== "string" || !launchEntryPoint.trim())) {
|
|
1050
|
+
error("spec.launch.entry-point must be a non-empty entry-point id");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (typeof launchEntryPoint === "string") {
|
|
1054
|
+
const entryPoint = spec["entry-points"]?.find((candidate) => candidate.id === launchEntryPoint);
|
|
1055
|
+
if (!entryPoint) {
|
|
1056
|
+
error(`spec.launch.entry-point '${launchEntryPoint}' does not match spec.entry-points[].id`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (!resolvesIframeEntry(spec.entry, entryPoint.entry ?? entryPoint.route)) {
|
|
1060
|
+
error("spec.launch.enabled requires the selected entry point to resolve an iframe entry");
|
|
1061
|
+
}
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (!isAbsoluteHttpUrl(spec.entry)) {
|
|
1065
|
+
error("spec.launch.enabled requires spec.entry or spec.launch.entry-point");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function isNonEmptyString(value) {
|
|
1069
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1070
|
+
}
|
|
1071
|
+
function resolvesIframeEntry(baseEntry, routeOrEntry) {
|
|
1072
|
+
if (isAbsoluteHttpUrl(routeOrEntry)) return true;
|
|
1073
|
+
return isAbsoluteHttpUrl(baseEntry);
|
|
1074
|
+
}
|
|
1075
|
+
function isAbsoluteHttpUrl(value) {
|
|
1076
|
+
if (!isNonEmptyString(value)) return false;
|
|
1077
|
+
try {
|
|
1078
|
+
const url = new URL(value);
|
|
1079
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
1080
|
+
} catch {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
911
1084
|
function validateInstallDirectives(install, issues) {
|
|
912
1085
|
if (install === void 0) return;
|
|
913
1086
|
const error = (message) => issues.push({ level: "error", message });
|
|
@@ -937,46 +1110,6 @@ function validateInstallDirectives(install, issues) {
|
|
|
937
1110
|
}
|
|
938
1111
|
});
|
|
939
1112
|
}
|
|
940
|
-
var KIND_MAP = {
|
|
941
|
-
application: "Application",
|
|
942
|
-
executionengine: "ExecutionEngine",
|
|
943
|
-
bundle: "Bundle"
|
|
944
|
-
};
|
|
945
|
-
function normalizeKind(kind) {
|
|
946
|
-
const mapped = KIND_MAP[kind.replace(/[^a-z]/gi, "").toLowerCase()];
|
|
947
|
-
if (!mapped) throw new Error(`Unsupported manifest kind '${kind}' \u2014 expected Application, ExecutionEngine or Bundle.`);
|
|
948
|
-
return mapped;
|
|
949
|
-
}
|
|
950
|
-
async function loadManifest(path) {
|
|
951
|
-
const text = await readFile(path, "utf8");
|
|
952
|
-
const manifest = parse(text);
|
|
953
|
-
if (!manifest || typeof manifest !== "object") throw new Error(`Failed to parse manifest at ${path}.`);
|
|
954
|
-
if (!manifest.kind) throw new Error(`Manifest at ${path} is missing 'kind'.`);
|
|
955
|
-
if (!manifest.metadata?.name) throw new Error(`Manifest at ${path} is missing 'metadata.name'.`);
|
|
956
|
-
if (!manifest.metadata?.version) throw new Error(`Manifest at ${path} is missing 'metadata.version'.`);
|
|
957
|
-
normalizeKind(manifest.kind);
|
|
958
|
-
return manifest;
|
|
959
|
-
}
|
|
960
|
-
async function findManifest(dir) {
|
|
961
|
-
const explicit = join(dir, "manifest.yaml");
|
|
962
|
-
try {
|
|
963
|
-
if ((await stat(explicit)).isFile()) return explicit;
|
|
964
|
-
} catch {
|
|
965
|
-
}
|
|
966
|
-
const entries = await readdir(dir);
|
|
967
|
-
const candidates = entries.filter((name) => /\.(app|engine|bundle)\.ya?ml$/.test(name));
|
|
968
|
-
if (candidates.length === 1) return join(dir, candidates[0]);
|
|
969
|
-
if (candidates.length === 0) {
|
|
970
|
-
throw new Error(`No manifest found in ${dir}. Expected manifest.yaml or *.app.yaml / *.engine.yaml / *.bundle.yaml.`);
|
|
971
|
-
}
|
|
972
|
-
throw new Error(`Multiple manifests found in ${dir} (${candidates.join(", ")}). Pass --manifest to disambiguate.`);
|
|
973
|
-
}
|
|
974
|
-
var NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
975
|
-
function validatePackageName(name, label) {
|
|
976
|
-
if (!NAME_PATTERN.test(name)) {
|
|
977
|
-
throw new Error(`Invalid ${label} '${name}' \u2014 use lowercase letters, digits and hyphens (no leading/trailing hyphen).`);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
1113
|
|
|
981
1114
|
// src/commands/dev.ts
|
|
982
1115
|
async function resolveDevScopes(flags) {
|
|
@@ -1326,7 +1459,7 @@ async function followExecution(api, workspaceId, executionId, json) {
|
|
|
1326
1459
|
if (exec.status !== "Completed") process.exitCode = 1;
|
|
1327
1460
|
return;
|
|
1328
1461
|
}
|
|
1329
|
-
await new Promise((
|
|
1462
|
+
await new Promise((resolve11) => setTimeout(resolve11, 1e3));
|
|
1330
1463
|
}
|
|
1331
1464
|
console.error("Timed out waiting for a terminal status \u2014 check `verentis exec get`.");
|
|
1332
1465
|
process.exitCode = 1;
|
|
@@ -1479,10 +1612,31 @@ metadata:
|
|
|
1479
1612
|
version: 0.1.0
|
|
1480
1613
|
author: ""
|
|
1481
1614
|
|
|
1615
|
+
# Marketplace presentation (optional):
|
|
1616
|
+
# marketplace:
|
|
1617
|
+
# logo: ./marketplace/logo.png
|
|
1618
|
+
# hero:
|
|
1619
|
+
# source: ./marketplace/hero.webp
|
|
1620
|
+
# alt: A concise description of the hero background
|
|
1621
|
+
# images:
|
|
1622
|
+
# - source: ./marketplace/screenshot.png
|
|
1623
|
+
# alt: A concise description of the screenshot
|
|
1624
|
+
# caption: Optional visible caption
|
|
1625
|
+
# readme: ./README.md
|
|
1626
|
+
# changelog: ./CHANGELOG.md
|
|
1627
|
+
# assets:
|
|
1628
|
+
# - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
|
|
1629
|
+
|
|
1482
1630
|
spec:
|
|
1483
1631
|
entry: https://example.com # hosted app origin
|
|
1484
1632
|
scope: global
|
|
1485
1633
|
|
|
1634
|
+
# Opt into a standalone workspace surface. Contextual file handlers should omit this block.
|
|
1635
|
+
# launch:
|
|
1636
|
+
# enabled: true
|
|
1637
|
+
# entry-point: home # optional; otherwise spec.entry is used
|
|
1638
|
+
# pinnable: true
|
|
1639
|
+
|
|
1486
1640
|
entry-points: []
|
|
1487
1641
|
capabilities: []
|
|
1488
1642
|
permissions: []
|
|
@@ -1509,6 +1663,20 @@ metadata:
|
|
|
1509
1663
|
version: 0.1.0
|
|
1510
1664
|
author: ""
|
|
1511
1665
|
|
|
1666
|
+
# Marketplace presentation (optional):
|
|
1667
|
+
# marketplace:
|
|
1668
|
+
# logo: ./marketplace/logo.png
|
|
1669
|
+
# hero:
|
|
1670
|
+
# source: ./marketplace/hero.webp
|
|
1671
|
+
# alt: A concise description of the hero background
|
|
1672
|
+
# images:
|
|
1673
|
+
# - source: ./marketplace/screenshot.png
|
|
1674
|
+
# alt: A concise description of the screenshot
|
|
1675
|
+
# readme: ./README.md
|
|
1676
|
+
# changelog: ./CHANGELOG.md
|
|
1677
|
+
# assets:
|
|
1678
|
+
# - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
|
|
1679
|
+
|
|
1512
1680
|
spec:
|
|
1513
1681
|
runtimes: {}
|
|
1514
1682
|
file-types: []
|
|
@@ -1530,6 +1698,20 @@ metadata:
|
|
|
1530
1698
|
version: 0.1.0
|
|
1531
1699
|
author: ""
|
|
1532
1700
|
|
|
1701
|
+
# Marketplace presentation (optional):
|
|
1702
|
+
# marketplace:
|
|
1703
|
+
# logo: ./marketplace/logo.png
|
|
1704
|
+
# hero:
|
|
1705
|
+
# source: ./marketplace/hero.webp
|
|
1706
|
+
# alt: A concise description of the hero background
|
|
1707
|
+
# images:
|
|
1708
|
+
# - source: ./marketplace/screenshot.png
|
|
1709
|
+
# alt: A concise description of the screenshot
|
|
1710
|
+
# readme: ./README.md
|
|
1711
|
+
# changelog: ./CHANGELOG.md
|
|
1712
|
+
# assets:
|
|
1713
|
+
# - "marketplace/**/*.{png,jpg,jpeg,webp,svg}"
|
|
1714
|
+
|
|
1533
1715
|
packaging:
|
|
1534
1716
|
publisher: "" # your publisher name
|
|
1535
1717
|
files:
|
|
@@ -1909,20 +2091,260 @@ function rawX25519Private(key) {
|
|
|
1909
2091
|
function publicKeyFromRaw(raw) {
|
|
1910
2092
|
return createPublicKey({ key: { kty: "OKP", crv: "X25519", x: raw.toString("base64url") }, format: "jwk" });
|
|
1911
2093
|
}
|
|
2094
|
+
var MIB = 1024 * 1024;
|
|
2095
|
+
var MAX_LOGO_BYTES = MIB;
|
|
2096
|
+
var MAX_IMAGE_BYTES = 5 * MIB;
|
|
2097
|
+
var MAX_TOTAL_IMAGE_BYTES = 25 * MIB;
|
|
2098
|
+
var MAX_MARKDOWN_BYTES = MIB;
|
|
2099
|
+
async function buildCatalog(projectDir, marketplace2) {
|
|
2100
|
+
const root = await realpath(projectDir);
|
|
2101
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2102
|
+
const imageDigests = /* @__PURE__ */ new Map();
|
|
2103
|
+
const index = { schemaVersion: 1 };
|
|
2104
|
+
const addImage = async (source, label, maxBytes) => {
|
|
2105
|
+
const file = await resolveDirectFile(root, source, label);
|
|
2106
|
+
if (file.bytes.length > maxBytes) {
|
|
2107
|
+
throw new Error(`${label} exceeds the ${formatMiB(maxBytes)} MiB limit (${file.bytes.length} bytes).`);
|
|
2108
|
+
}
|
|
2109
|
+
const image = validateImage(file.bytes, file.path, label);
|
|
2110
|
+
const reference = addContentAddressedEntry(file, image, "assets", entries);
|
|
2111
|
+
imageDigests.set(reference.sha256, reference.size);
|
|
2112
|
+
return reference;
|
|
2113
|
+
};
|
|
2114
|
+
const addMarkdown = async (source, label) => {
|
|
2115
|
+
const file = await resolveDirectFile(root, source, label);
|
|
2116
|
+
if (file.bytes.length > MAX_MARKDOWN_BYTES) {
|
|
2117
|
+
throw new Error(`${label} exceeds the 1 MiB limit (${file.bytes.length} bytes).`);
|
|
2118
|
+
}
|
|
2119
|
+
validateMarkdown(file.bytes, file.path, label);
|
|
2120
|
+
return addContentAddressedEntry(file, { extension: ".md", mediaType: "text/markdown" }, "docs", entries);
|
|
2121
|
+
};
|
|
2122
|
+
if (marketplace2.logo) index.logo = await addImage(marketplace2.logo, "marketplace.logo", MAX_LOGO_BYTES);
|
|
2123
|
+
if (marketplace2.hero) {
|
|
2124
|
+
const reference = await addImage(marketplace2.hero.source, "marketplace.hero.source", MAX_IMAGE_BYTES);
|
|
2125
|
+
index.hero = { ...reference, alt: marketplace2.hero.alt.trim() };
|
|
2126
|
+
}
|
|
2127
|
+
if (marketplace2.images?.length) {
|
|
2128
|
+
index.images = [];
|
|
2129
|
+
for (const [position, image] of marketplace2.images.entries()) {
|
|
2130
|
+
const reference = await addImage(image.source, `marketplace.images[${position}].source`, MAX_IMAGE_BYTES);
|
|
2131
|
+
index.images.push({
|
|
2132
|
+
...reference,
|
|
2133
|
+
alt: image.alt.trim(),
|
|
2134
|
+
...image.caption !== void 0 ? { caption: image.caption } : {}
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (marketplace2.readme) index.readme = await addMarkdown(marketplace2.readme, "marketplace.readme");
|
|
2139
|
+
if (marketplace2.changelog) index.changelog = await addMarkdown(marketplace2.changelog, "marketplace.changelog");
|
|
2140
|
+
if (marketplace2.assets?.length) {
|
|
2141
|
+
index.assets = [];
|
|
2142
|
+
const assetFiles = await resolveAssetGlobs(root, marketplace2.assets);
|
|
2143
|
+
for (const file of assetFiles) {
|
|
2144
|
+
if (file.bytes.length > MAX_IMAGE_BYTES) {
|
|
2145
|
+
throw new Error(`marketplace.assets matched '${file.source}', which exceeds the 5 MiB per-image limit (${file.bytes.length} bytes).`);
|
|
2146
|
+
}
|
|
2147
|
+
const image = validateImage(file.bytes, file.path, `marketplace.assets entry '${file.source}'`);
|
|
2148
|
+
const reference = addContentAddressedEntry(file, image, "assets", entries);
|
|
2149
|
+
imageDigests.set(reference.sha256, reference.size);
|
|
2150
|
+
index.assets.push(reference);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const totalImageBytes = [...imageDigests.values()].reduce((sum, size) => sum + size, 0);
|
|
2154
|
+
if (totalImageBytes > MAX_TOTAL_IMAGE_BYTES) {
|
|
2155
|
+
throw new Error(`Marketplace image assets exceed the 25 MiB total limit (${totalImageBytes} bytes).`);
|
|
2156
|
+
}
|
|
2157
|
+
const indexJson = `${JSON.stringify(index, null, 2)}
|
|
2158
|
+
`;
|
|
2159
|
+
return {
|
|
2160
|
+
index,
|
|
2161
|
+
indexJson,
|
|
2162
|
+
entries: [...entries.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([path, bytes]) => ({ path, bytes }))
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
async function resolveDirectFile(root, source, label) {
|
|
2166
|
+
const normalized = normalizeRelativeReference(source, label);
|
|
2167
|
+
return readContainedFile(root, normalized, label);
|
|
2168
|
+
}
|
|
2169
|
+
async function resolveAssetGlobs(root, patterns) {
|
|
2170
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2171
|
+
for (const sourcePattern of patterns) {
|
|
2172
|
+
const pattern = normalizeRelativeReference(sourcePattern, "marketplace.assets glob");
|
|
2173
|
+
for await (const entry of glob(pattern, { cwd: root })) {
|
|
2174
|
+
const normalized = toEntryPath(entry);
|
|
2175
|
+
const info = await stat(join(root, normalized)).catch(() => null);
|
|
2176
|
+
if (info?.isFile()) matched.add(normalized);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
const files2 = [];
|
|
2180
|
+
for (const source of [...matched].sort()) {
|
|
2181
|
+
files2.push(await readContainedFile(root, source, `marketplace.assets entry '${source}'`));
|
|
2182
|
+
}
|
|
2183
|
+
return files2;
|
|
2184
|
+
}
|
|
2185
|
+
async function readContainedFile(root, source, label) {
|
|
2186
|
+
const candidate = resolve(root, source);
|
|
2187
|
+
const actual = await realpath(candidate).catch(() => {
|
|
2188
|
+
throw new Error(`${label} references missing file '${source}'.`);
|
|
2189
|
+
});
|
|
2190
|
+
const fromRoot = relative(root, actual);
|
|
2191
|
+
if (fromRoot === "" || fromRoot.startsWith(`..${sep}`) || fromRoot === ".." || isAbsolute(fromRoot)) {
|
|
2192
|
+
throw new Error(`${label} references '${source}', which escapes the manifest directory.`);
|
|
2193
|
+
}
|
|
2194
|
+
const info = await stat(actual);
|
|
2195
|
+
if (!info.isFile()) throw new Error(`${label} references '${source}', which is not a file.`);
|
|
2196
|
+
return { source: toEntryPath(source), path: actual, bytes: await readFile(actual) };
|
|
2197
|
+
}
|
|
2198
|
+
function normalizeRelativeReference(source, label) {
|
|
2199
|
+
const value = source.trim();
|
|
2200
|
+
if (value.length === 0 || value.includes("\0") || value.includes("\\") || isAbsolute(value) || /^[a-z]:/i.test(value) || value.split("/").includes("..")) {
|
|
2201
|
+
throw new Error(`${label} must be relative to the manifest directory and must not escape it (got '${source}').`);
|
|
2202
|
+
}
|
|
2203
|
+
const normalized = value.replace(/^(?:\.\/)+/, "");
|
|
2204
|
+
if (normalized.length === 0 || normalized === ".") throw new Error(`${label} must reference a file inside the manifest directory.`);
|
|
2205
|
+
return normalized;
|
|
2206
|
+
}
|
|
2207
|
+
function addContentAddressedEntry(file, type, directory, entries) {
|
|
2208
|
+
const sha256 = createHash("sha256").update(file.bytes).digest("hex");
|
|
2209
|
+
const path = `catalog/${directory}/${sha256}${type.extension}`;
|
|
2210
|
+
entries.set(path, file.bytes);
|
|
2211
|
+
return {
|
|
2212
|
+
source: file.source,
|
|
2213
|
+
path,
|
|
2214
|
+
mediaType: type.mediaType,
|
|
2215
|
+
size: file.bytes.length,
|
|
2216
|
+
sha256
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
function validateImage(bytes, path, label) {
|
|
2220
|
+
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2221
|
+
if (bytes.length >= 24 && bytes.subarray(0, 8).equals(pngSignature)) {
|
|
2222
|
+
if (bytes.readUInt32BE(8) !== 13 || bytes.toString("ascii", 12, 16) !== "IHDR") throw new Error(`${label} is not a valid PNG file.`);
|
|
2223
|
+
return { extension: ".png", mediaType: "image/png" };
|
|
2224
|
+
}
|
|
2225
|
+
if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216) {
|
|
2226
|
+
if (bytes[bytes.length - 2] !== 255 || bytes[bytes.length - 1] !== 217) throw new Error(`${label} is not a valid JPEG file.`);
|
|
2227
|
+
return { extension: ".jpg", mediaType: "image/jpeg" };
|
|
2228
|
+
}
|
|
2229
|
+
if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") {
|
|
2230
|
+
const declaredLength = bytes.readUInt32LE(4) + 8;
|
|
2231
|
+
if (declaredLength !== bytes.length) throw new Error(`${label} has an invalid WebP length.`);
|
|
2232
|
+
return { extension: ".webp", mediaType: "image/webp" };
|
|
2233
|
+
}
|
|
2234
|
+
const extension = extname(path).toLowerCase();
|
|
2235
|
+
if (extension === ".svg" || looksLikeSvg(bytes)) {
|
|
2236
|
+
validateSvg(bytes, label);
|
|
2237
|
+
return { extension: ".svg", mediaType: "image/svg+xml" };
|
|
2238
|
+
}
|
|
2239
|
+
throw new Error(`${label} must be a valid PNG, JPEG, WebP or SVG image.`);
|
|
2240
|
+
}
|
|
2241
|
+
function looksLikeSvg(bytes) {
|
|
2242
|
+
const prefix = bytes.subarray(0, Math.min(bytes.length, 512)).toString("utf8").replace(/^\uFEFF/, "").trimStart();
|
|
2243
|
+
return prefix.startsWith("<svg") || prefix.startsWith("<?xml") || prefix.startsWith("<!--");
|
|
2244
|
+
}
|
|
2245
|
+
function validateSvg(bytes, label) {
|
|
2246
|
+
let text;
|
|
2247
|
+
try {
|
|
2248
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2249
|
+
} catch {
|
|
2250
|
+
throw new Error(`${label} is not valid UTF-8 SVG content.`);
|
|
2251
|
+
}
|
|
2252
|
+
const trimmed = text.replace(/^\uFEFF/, "").trim();
|
|
2253
|
+
const root = /^(?:<\?xml[\s\S]*?\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b/i;
|
|
2254
|
+
if (!root.test(trimmed) || !/<\/svg>\s*$/i.test(trimmed) && !/<svg\b[\s\S]*\/>\s*$/i.test(trimmed)) {
|
|
2255
|
+
throw new Error(`${label} is not a well-formed SVG document.`);
|
|
2256
|
+
}
|
|
2257
|
+
if (/&#(?:x[0-9a-f]+|[0-9]+);/i.test(trimmed)) {
|
|
2258
|
+
throw new Error(`${label} contains numeric XML character references, which are not allowed in SVG content.`);
|
|
2259
|
+
}
|
|
2260
|
+
if (/<!DOCTYPE|<!ENTITY/i.test(trimmed) || /<(?:script|foreignObject|iframe|object|embed|audio|video)\b/i.test(trimmed) || /\son[a-z]+\s*=/i.test(trimmed) || /(?:javascript|vbscript)\s*:/i.test(trimmed) || /data\s*:\s*text\/html/i.test(trimmed)) {
|
|
2261
|
+
throw new Error(`${label} contains active SVG content, which is not allowed.`);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function validateMarkdown(bytes, _path, label) {
|
|
2265
|
+
try {
|
|
2266
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2267
|
+
if (text.includes("\0")) throw new Error("NUL");
|
|
2268
|
+
} catch {
|
|
2269
|
+
throw new Error(`${label} must contain valid UTF-8 Markdown text.`);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function formatMiB(bytes) {
|
|
2273
|
+
return bytes / MIB;
|
|
2274
|
+
}
|
|
2275
|
+
function toEntryPath(path) {
|
|
2276
|
+
return path.split(sep).join("/");
|
|
2277
|
+
}
|
|
2278
|
+
function isPlainObject(value) {
|
|
2279
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2280
|
+
}
|
|
2281
|
+
function deepMerge(base, overlay) {
|
|
2282
|
+
if (!isPlainObject(base) || !isPlainObject(overlay)) {
|
|
2283
|
+
return overlay;
|
|
2284
|
+
}
|
|
2285
|
+
const result = { ...base };
|
|
2286
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
2287
|
+
result[key] = key in result ? deepMerge(result[key], value) : value;
|
|
2288
|
+
}
|
|
2289
|
+
return result;
|
|
2290
|
+
}
|
|
2291
|
+
function applyOverlay(base, overlay) {
|
|
2292
|
+
return deepMerge(base, overlay);
|
|
2293
|
+
}
|
|
2294
|
+
function resolveOverlayPath(projectDir, selector) {
|
|
2295
|
+
if (selector.overlay) {
|
|
2296
|
+
return isAbsolute(selector.overlay) ? selector.overlay : resolve(selector.overlay);
|
|
2297
|
+
}
|
|
2298
|
+
if (selector.env) {
|
|
2299
|
+
return join(projectDir, "environments", `${selector.env}.yaml`);
|
|
2300
|
+
}
|
|
2301
|
+
return void 0;
|
|
2302
|
+
}
|
|
2303
|
+
async function loadOverlay(path) {
|
|
2304
|
+
let text;
|
|
2305
|
+
try {
|
|
2306
|
+
text = await readFile(path, "utf8");
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
if (error?.code === "ENOENT") {
|
|
2309
|
+
throw new Error(`Overlay file not found: ${path}`);
|
|
2310
|
+
}
|
|
2311
|
+
throw error;
|
|
2312
|
+
}
|
|
2313
|
+
const parsed = parse(text);
|
|
2314
|
+
if (parsed === null || parsed === void 0) return {};
|
|
2315
|
+
if (!isPlainObject(parsed)) {
|
|
2316
|
+
throw new Error(`Overlay ${path} must be a YAML mapping (got ${Array.isArray(parsed) ? "a list" : typeof parsed}).`);
|
|
2317
|
+
}
|
|
2318
|
+
return parsed;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// src/vpkg/pack.ts
|
|
1912
2322
|
async function pack(options) {
|
|
1913
2323
|
const manifestPath = resolve(options.manifestPath);
|
|
1914
2324
|
const projectDir = dirname(manifestPath);
|
|
1915
|
-
const
|
|
2325
|
+
const base = await loadManifest(manifestPath);
|
|
2326
|
+
const overlayPath = resolveOverlayPath(projectDir, { env: options.env, overlay: options.overlay });
|
|
2327
|
+
let manifest = overlayPath ? applyOverlay(base, await loadOverlay(overlayPath)) : base;
|
|
2328
|
+
let overrideApplied = overlayPath !== void 0;
|
|
2329
|
+
if (options.version !== void 0) {
|
|
2330
|
+
manifest = { ...manifest, metadata: { ...manifest.metadata, version: options.version } };
|
|
2331
|
+
overrideApplied = true;
|
|
2332
|
+
}
|
|
1916
2333
|
const kind = normalizeKind(manifest.kind);
|
|
2334
|
+
const launchKindError = validateLaunchManifestKind(kind, manifest.spec);
|
|
2335
|
+
if (launchKindError) throw new Error(`Invalid manifest: ${launchKindError}.`);
|
|
2336
|
+
const marketplaceErrors = validateMarketplaceSection(manifest.marketplace);
|
|
2337
|
+
if (marketplaceErrors.length > 0) throw new Error(`Invalid marketplace section: ${marketplaceErrors.join(" ")}`);
|
|
1917
2338
|
const name = (options.name ?? manifest.packaging?.name ?? manifest.metadata.name).toLowerCase();
|
|
1918
2339
|
const publisher = options.publisher.toLowerCase();
|
|
1919
2340
|
validatePackageName(name, "package name");
|
|
1920
2341
|
validatePackageName(publisher, "publisher name");
|
|
1921
2342
|
const version = manifest.metadata.version;
|
|
1922
2343
|
const payloadFiles = await resolvePayloadFiles(projectDir, manifestPath, manifest);
|
|
2344
|
+
const catalog = manifest.marketplace ? await buildCatalog(projectDir, manifest.marketplace) : void 0;
|
|
1923
2345
|
const staging = await mkdtemp(join(tmpdir(), "vpkg-"));
|
|
1924
2346
|
try {
|
|
1925
|
-
const manifestText = await readFile(manifestPath, "utf8");
|
|
2347
|
+
const manifestText = overrideApplied ? stringify(manifest) : await readFile(manifestPath, "utf8");
|
|
1926
2348
|
await writeFile(join(staging, "manifest.yaml"), manifestText);
|
|
1927
2349
|
const encrypt = (options.encryptFor?.length ?? 0) > 0;
|
|
1928
2350
|
const contentKey = encrypt ? generateContentKey() : null;
|
|
@@ -1931,13 +2353,24 @@ async function pack(options) {
|
|
|
1931
2353
|
};
|
|
1932
2354
|
for (const file of payloadFiles) {
|
|
1933
2355
|
const sourcePath = join(projectDir, file);
|
|
1934
|
-
const entryName = `files/${
|
|
2356
|
+
const entryName = `files/${toEntryPath2(file)}`;
|
|
1935
2357
|
const targetPath = join(staging, "files", file);
|
|
1936
2358
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
1937
2359
|
const bytes = contentKey ? encryptBytes(contentKey, await readFile(sourcePath)) : await readFile(sourcePath);
|
|
1938
2360
|
await writeFile(targetPath, bytes);
|
|
1939
2361
|
digests[entryName] = sha256Hex(bytes);
|
|
1940
2362
|
}
|
|
2363
|
+
if (catalog) {
|
|
2364
|
+
for (const entry of catalog.entries) {
|
|
2365
|
+
const targetPath = join(staging, entry.path);
|
|
2366
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
2367
|
+
await writeFile(targetPath, entry.bytes);
|
|
2368
|
+
digests[entry.path] = sha256Hex(entry.bytes);
|
|
2369
|
+
}
|
|
2370
|
+
await mkdir(join(staging, "catalog"), { recursive: true });
|
|
2371
|
+
await writeFile(join(staging, "catalog", "index.json"), catalog.indexJson);
|
|
2372
|
+
digests["catalog/index.json"] = sha256Hex(Buffer.from(catalog.indexJson, "utf8"));
|
|
2373
|
+
}
|
|
1941
2374
|
await mkdir(join(staging, ".verentis", "signatures"), { recursive: true });
|
|
1942
2375
|
if (contentKey) {
|
|
1943
2376
|
const encryption = {
|
|
@@ -1973,16 +2406,19 @@ async function pack(options) {
|
|
|
1973
2406
|
".verentis/digests.json",
|
|
1974
2407
|
...contentKey ? [".verentis/encryption.json"] : [],
|
|
1975
2408
|
...signed ? [".verentis/signatures/developer.dsse.json"] : [],
|
|
1976
|
-
...payloadFiles.map((file) => `files/${
|
|
2409
|
+
...payloadFiles.map((file) => `files/${toEntryPath2(file)}`),
|
|
2410
|
+
...catalog ? ["catalog/index.json", ...catalog.entries.map((entry) => entry.path)] : []
|
|
1977
2411
|
];
|
|
1978
|
-
await tar.create({ gzip: true, file: outFile, cwd: staging, portable: true }, entries);
|
|
2412
|
+
await tar.create({ gzip: true, file: outFile, cwd: staging, portable: true, mtime: /* @__PURE__ */ new Date(0) }, entries);
|
|
1979
2413
|
return {
|
|
1980
2414
|
outFile,
|
|
1981
2415
|
name,
|
|
1982
2416
|
publisher,
|
|
1983
2417
|
version,
|
|
1984
2418
|
kind,
|
|
1985
|
-
files: payloadFiles.map(
|
|
2419
|
+
files: payloadFiles.map(toEntryPath2),
|
|
2420
|
+
catalog: catalog?.index,
|
|
2421
|
+
catalogFiles: catalog ? ["index.json", ...catalog.entries.map((entry) => entry.path.replace(/^catalog\//, ""))] : [],
|
|
1986
2422
|
treeDigest: sha256Hex(Buffer.from(digestsJson, "utf8")),
|
|
1987
2423
|
signed,
|
|
1988
2424
|
encrypted: encrypt
|
|
@@ -2009,7 +2445,7 @@ async function resolvePayloadFiles(projectDir, manifestPath, manifest) {
|
|
|
2009
2445
|
}
|
|
2010
2446
|
function defaultIncludes(manifest) {
|
|
2011
2447
|
const sources = /* @__PURE__ */ new Set();
|
|
2012
|
-
collectSchemaSources(manifest, sources);
|
|
2448
|
+
collectSchemaSources(manifest.spec, sources);
|
|
2013
2449
|
collectInstallSources(manifest, sources);
|
|
2014
2450
|
if (sources.size === 0) return [];
|
|
2015
2451
|
return [...sources];
|
|
@@ -2040,7 +2476,7 @@ function collectSchemaSources(node, sources) {
|
|
|
2040
2476
|
}
|
|
2041
2477
|
}
|
|
2042
2478
|
}
|
|
2043
|
-
function
|
|
2479
|
+
function toEntryPath2(file) {
|
|
2044
2480
|
return file.split(sep).join("/");
|
|
2045
2481
|
}
|
|
2046
2482
|
function sha256Hex(data) {
|
|
@@ -2061,21 +2497,18 @@ async function readVpkg(vpkgPath) {
|
|
|
2061
2497
|
});
|
|
2062
2498
|
const metadata = parse(packageYaml);
|
|
2063
2499
|
const declared = JSON.parse(digestsJson);
|
|
2064
|
-
const files2 =
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
const info = await readFile(abs).catch(() => null);
|
|
2068
|
-
if (info === null) continue;
|
|
2069
|
-
files2.push(entry.split(sep).join("/"));
|
|
2070
|
-
}
|
|
2071
|
-
files2.sort();
|
|
2500
|
+
const files2 = await listFiles(staging, "files/**/*");
|
|
2501
|
+
const catalogFiles = await listFiles(staging, "catalog/**/*");
|
|
2502
|
+
const contentFiles = [...files2, ...catalogFiles];
|
|
2072
2503
|
const digestErrors = [];
|
|
2073
2504
|
const entries = declared.entries ?? {};
|
|
2074
2505
|
const manifestDigest = createHash("sha256").update(manifestYaml, "utf8").digest("hex");
|
|
2075
|
-
if (entries["manifest.yaml"]
|
|
2506
|
+
if (!entries["manifest.yaml"]) {
|
|
2507
|
+
digestErrors.push("Entry 'manifest.yaml' is not covered by digests.json.");
|
|
2508
|
+
} else if (entries["manifest.yaml"].toLowerCase() !== manifestDigest) {
|
|
2076
2509
|
digestErrors.push("Digest mismatch for manifest.yaml.");
|
|
2077
2510
|
}
|
|
2078
|
-
for (const file of
|
|
2511
|
+
for (const file of contentFiles) {
|
|
2079
2512
|
const declaredDigest = entries[file];
|
|
2080
2513
|
if (!declaredDigest) {
|
|
2081
2514
|
digestErrors.push(`Entry '${file}' is not covered by digests.json.`);
|
|
@@ -2085,10 +2518,21 @@ async function readVpkg(vpkgPath) {
|
|
|
2085
2518
|
if (actual !== declaredDigest.toLowerCase()) digestErrors.push(`Digest mismatch for '${file}'.`);
|
|
2086
2519
|
}
|
|
2087
2520
|
for (const declaredPath of Object.keys(entries)) {
|
|
2088
|
-
if (declaredPath !== "manifest.yaml" && !
|
|
2521
|
+
if (declaredPath !== "manifest.yaml" && !contentFiles.includes(declaredPath)) {
|
|
2089
2522
|
digestErrors.push(`Digest declared for missing entry '${declaredPath}'.`);
|
|
2090
2523
|
}
|
|
2091
2524
|
}
|
|
2525
|
+
let catalog;
|
|
2526
|
+
if (catalogFiles.includes("catalog/index.json")) {
|
|
2527
|
+
try {
|
|
2528
|
+
catalog = JSON.parse(await readFile(join(staging, "catalog", "index.json"), "utf8"));
|
|
2529
|
+
if (catalog.schemaVersion !== 1) digestErrors.push(`Unsupported catalog schema version '${String(catalog.schemaVersion)}'.`);
|
|
2530
|
+
} catch {
|
|
2531
|
+
digestErrors.push("Invalid catalog/index.json.");
|
|
2532
|
+
}
|
|
2533
|
+
} else if (catalogFiles.length > 0) {
|
|
2534
|
+
digestErrors.push("Catalog entries are present but catalog/index.json is missing.");
|
|
2535
|
+
}
|
|
2092
2536
|
const developerSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "developer.dsse.json"));
|
|
2093
2537
|
const marketplaceSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "marketplace.dsse.json"));
|
|
2094
2538
|
const encryption = await readJsonIfExists(join(staging, ".verentis", "encryption.json"));
|
|
@@ -2101,12 +2545,23 @@ async function readVpkg(vpkgPath) {
|
|
|
2101
2545
|
marketplaceSignature: marketplaceSignature ?? void 0,
|
|
2102
2546
|
encryption: encryption ?? void 0,
|
|
2103
2547
|
files: files2.map((f) => f.replace(/^files\//, "")),
|
|
2548
|
+
catalog,
|
|
2549
|
+
catalogFiles: catalogFiles.map((f) => f.replace(/^catalog\//, "")),
|
|
2104
2550
|
digestErrors
|
|
2105
2551
|
};
|
|
2106
2552
|
} finally {
|
|
2107
2553
|
await rm(staging, { recursive: true, force: true });
|
|
2108
2554
|
}
|
|
2109
2555
|
}
|
|
2556
|
+
async function listFiles(root, pattern) {
|
|
2557
|
+
const files2 = [];
|
|
2558
|
+
for await (const entry of glob(pattern, { cwd: root })) {
|
|
2559
|
+
const abs = join(root, entry);
|
|
2560
|
+
const info = await readFile(abs).catch(() => null);
|
|
2561
|
+
if (info !== null) files2.push(entry.split(sep).join("/"));
|
|
2562
|
+
}
|
|
2563
|
+
return files2.sort();
|
|
2564
|
+
}
|
|
2110
2565
|
async function readJsonIfExists(path) {
|
|
2111
2566
|
try {
|
|
2112
2567
|
return JSON.parse(await readFile(path, "utf8"));
|
|
@@ -2117,7 +2572,7 @@ async function readJsonIfExists(path) {
|
|
|
2117
2572
|
|
|
2118
2573
|
// src/commands/pack.ts
|
|
2119
2574
|
function registerPack(program2) {
|
|
2120
|
-
program2.command("pack [dir]").description("Build a signed .vpkg from a project directory").option("--manifest <path>", "manifest path (default: auto-detected in the project directory)").option("--publisher <name>", "publisher name (default: packaging.publisher in the manifest)").option("--name <name>", "override the package name").option("--out <dir>", "output directory (default: the project directory)").option("--key <name>", "signing key name (default: the configured default key)").option("--no-sign", "produce an unsigned package").option("--encrypt", "seal the payload (AES-256-GCM); requires at least one recipient").option("--recipient <id=base64key...>", "additional X25519 recipient (repeatable)", collectRecipients, []).action(async (dir, options) => {
|
|
2575
|
+
program2.command("pack [dir]").description("Build a signed .vpkg from a project directory").option("--manifest <path>", "manifest path (default: auto-detected in the project directory)").option("--publisher <name>", "publisher name (default: packaging.publisher in the manifest)").option("--name <name>", "override the package name").option("--env <name>", "merge a per-environment overlay (environments/<name>.yaml) before packing").option("--overlay <path>", "merge an explicit overlay file before packing (wins over --env)").option("--set-version <version>", "override metadata.version (e.g. a CI-derived build number)").option("--out <dir>", "output directory (default: the project directory)").option("--key <name>", "signing key name (default: the configured default key)").option("--no-sign", "produce an unsigned package").option("--encrypt", "seal the payload (AES-256-GCM); requires at least one recipient").option("--recipient <id=base64key...>", "additional X25519 recipient (repeatable)", collectRecipients, []).action(async (dir, options) => {
|
|
2121
2576
|
const projectDir = resolve(dir ?? ".");
|
|
2122
2577
|
const manifestPath = options.manifest ? resolve(options.manifest) : await findManifest(projectDir);
|
|
2123
2578
|
const manifest = await loadManifest(manifestPath);
|
|
@@ -2152,6 +2607,9 @@ function registerPack(program2) {
|
|
|
2152
2607
|
manifestPath,
|
|
2153
2608
|
publisher,
|
|
2154
2609
|
name: options.name,
|
|
2610
|
+
env: options.env,
|
|
2611
|
+
overlay: options.overlay,
|
|
2612
|
+
version: options.setVersion,
|
|
2155
2613
|
outDir: options.out,
|
|
2156
2614
|
signingKey,
|
|
2157
2615
|
encryptFor
|
|
@@ -2159,6 +2617,7 @@ function registerPack(program2) {
|
|
|
2159
2617
|
console.log(`Packed ${result.publisher}/${result.name}@${result.version} (${result.kind})`);
|
|
2160
2618
|
console.log(` ${result.outFile}`);
|
|
2161
2619
|
console.log(` ${result.files.length} payload file(s), tree digest ${result.treeDigest.slice(0, 16)}\u2026, ${result.signed ? "signed" : "UNSIGNED"}${result.encrypted ? ", ENCRYPTED" : ""}`);
|
|
2620
|
+
if (result.catalog) console.log(` ${result.catalogFiles.length} plaintext catalog file(s)`);
|
|
2162
2621
|
});
|
|
2163
2622
|
program2.command("seal-keygen").description("Generate an X25519 sealing keypair (for platform escrow or custom recipients)").action(() => {
|
|
2164
2623
|
const pair = generateSealingKeyPair();
|
|
@@ -2169,7 +2628,11 @@ function registerPack(program2) {
|
|
|
2169
2628
|
program2.command("verify <vpkg>").description("Verify the digests (and, when possible, the developer signature) of a .vpkg").option("--key <name>", "verify the developer signature against a local key").action(async (vpkgPath, options) => {
|
|
2170
2629
|
const contents = await readVpkg(resolve(vpkgPath));
|
|
2171
2630
|
console.log(`${contents.metadata.publisher}/${contents.metadata.name}@${contents.metadata.version} (${contents.metadata.kind})`);
|
|
2172
|
-
console.log(` files: ${contents.files.length}, tree digest ${contents.treeDigest.slice(0, 16)}\u2026`);
|
|
2631
|
+
console.log(` payload files: ${contents.files.length}, catalog files: ${contents.catalogFiles.length}, tree digest ${contents.treeDigest.slice(0, 16)}\u2026`);
|
|
2632
|
+
if (contents.catalog) {
|
|
2633
|
+
const docs = Number(!!contents.catalog.readme) + Number(!!contents.catalog.changelog);
|
|
2634
|
+
console.log(` catalog schema v${contents.catalog.schemaVersion}: ${contents.catalog.images?.length ?? 0} gallery image(s), ${docs} document(s)${contents.catalog.logo ? ", logo" : ""}${contents.catalog.hero ? ", hero" : ""}`);
|
|
2635
|
+
}
|
|
2173
2636
|
if (contents.encryption) {
|
|
2174
2637
|
console.log(` \u{1F512} encrypted (${contents.encryption.algorithm}); recipients: ${contents.encryption.recipients.map((r) => r.id).join(", ")}`);
|
|
2175
2638
|
}
|
|
@@ -2310,19 +2773,62 @@ function registerPackage(program2) {
|
|
|
2310
2773
|
output(pageItems(page), { packageName: "Package", version: "Version", status: "Status", installPath: "Path", id: "Id" }, options, page);
|
|
2311
2774
|
});
|
|
2312
2775
|
}
|
|
2776
|
+
var MAX_LOGO_BYTES2 = 1024 * 1024;
|
|
2777
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2778
|
+
async function readPublisherLogo(path) {
|
|
2779
|
+
const absolutePath = resolve(path);
|
|
2780
|
+
const info = await stat(absolutePath).catch(() => {
|
|
2781
|
+
throw new Error(`Publisher logo '${absolutePath}' does not exist.`);
|
|
2782
|
+
});
|
|
2783
|
+
if (!info.isFile()) throw new Error(`Publisher logo '${absolutePath}' is not a file.`);
|
|
2784
|
+
if (info.size > MAX_LOGO_BYTES2) {
|
|
2785
|
+
throw new Error(`Publisher logo exceeds the 1 MiB limit (${info.size} bytes).`);
|
|
2786
|
+
}
|
|
2787
|
+
const bytes = await readFile(absolutePath);
|
|
2788
|
+
return { path: absolutePath, bytes, contentType: detectPublisherLogoContentType(bytes) };
|
|
2789
|
+
}
|
|
2790
|
+
function detectPublisherLogoContentType(bytes) {
|
|
2791
|
+
if (bytes.length >= 24 && bytes.subarray(0, 8).equals(PNG_SIGNATURE) && bytes.readUInt32BE(8) === 13 && bytes.toString("ascii", 12, 16) === "IHDR") {
|
|
2792
|
+
return "image/png";
|
|
2793
|
+
}
|
|
2794
|
+
if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[bytes.length - 2] === 255 && bytes[bytes.length - 1] === 217) {
|
|
2795
|
+
return "image/jpeg";
|
|
2796
|
+
}
|
|
2797
|
+
if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP" && bytes.readUInt32LE(4) + 8 === bytes.length) {
|
|
2798
|
+
return "image/webp";
|
|
2799
|
+
}
|
|
2800
|
+
let text;
|
|
2801
|
+
try {
|
|
2802
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "").trim();
|
|
2803
|
+
} catch {
|
|
2804
|
+
throw unsupportedLogoError();
|
|
2805
|
+
}
|
|
2806
|
+
const root = /^(?:<\?xml[\s\S]*?\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b/i;
|
|
2807
|
+
const closed = /<\/svg>\s*$/i.test(text) || /<svg\b[\s\S]*\/>\s*$/i.test(text);
|
|
2808
|
+
if (!root.test(text) || !closed) throw unsupportedLogoError();
|
|
2809
|
+
if (/&#(?:x[0-9a-f]+|[0-9]+);/i.test(text)) {
|
|
2810
|
+
throw new Error("Publisher logo SVG contains numeric XML character references, which are not allowed.");
|
|
2811
|
+
}
|
|
2812
|
+
if (/<!DOCTYPE|<!ENTITY/i.test(text) || /<(?:script|foreignObject|iframe|object|embed|audio|video)\b/i.test(text) || /\son[a-z]+\s*=/i.test(text) || /(?:javascript|vbscript)\s*:/i.test(text) || /data\s*:\s*text\/html/i.test(text)) {
|
|
2813
|
+
throw new Error("Publisher logo SVG contains active content, which is not allowed.");
|
|
2814
|
+
}
|
|
2815
|
+
return "image/svg+xml";
|
|
2816
|
+
}
|
|
2817
|
+
function unsupportedLogoError() {
|
|
2818
|
+
return new Error("Publisher logo must be a valid PNG, JPEG, WebP or SVG image.");
|
|
2819
|
+
}
|
|
2313
2820
|
|
|
2314
2821
|
// src/commands/publisher.ts
|
|
2315
2822
|
function registerPublisher(program2) {
|
|
2316
2823
|
const publisherCmd = program2.command("publisher").description("Manage your publisher profile");
|
|
2317
|
-
publisherCmd.command("create").description("Onboard your account as a marketplace publisher").requiredOption("--name <name>", "URL-safe publisher name (e.g. acme)").requiredOption("--display-name <displayName>", "human-readable publisher name").option("--description <description>").option("--website <url>").option("--email <email>").action(async (options) => {
|
|
2824
|
+
publisherCmd.command("create").description("Onboard your account as a marketplace publisher").requiredOption("--name <name>", "URL-safe publisher name (e.g. acme)").requiredOption("--display-name <displayName>", "human-readable publisher name").option("--description <description>").option("--website <url>").option("--email <email>").option("--logo <file>", "PNG, JPEG, WebP or SVG logo (maximum 1 MiB)").action(async (options) => {
|
|
2318
2825
|
const api = await createApiClient();
|
|
2319
2826
|
const existing = await getMyPublisher(api);
|
|
2320
2827
|
if (existing) {
|
|
2321
2828
|
console.log(`This account already has publisher '${existing.name}'.`);
|
|
2322
2829
|
return;
|
|
2323
2830
|
}
|
|
2324
|
-
const id = await api
|
|
2325
|
-
scopes: [SCOPES.publisherCreate],
|
|
2831
|
+
const id = await call(api, marketplace.createPublisher, {
|
|
2326
2832
|
body: {
|
|
2327
2833
|
name: options.name,
|
|
2328
2834
|
displayName: options.displayName,
|
|
@@ -2332,8 +2838,51 @@ function registerPublisher(program2) {
|
|
|
2332
2838
|
}
|
|
2333
2839
|
});
|
|
2334
2840
|
console.log(`Publisher '${options.name}' created (${id}).`);
|
|
2841
|
+
if (options.logo) {
|
|
2842
|
+
await uploadPublisherLogo(api, id, options.logo);
|
|
2843
|
+
console.log(`Publisher logo uploaded from ${options.logo}.`);
|
|
2844
|
+
}
|
|
2335
2845
|
console.log("Next: generate and register a signing key with `verentis keygen`.");
|
|
2336
2846
|
});
|
|
2847
|
+
publisherCmd.command("update").description("Update your publisher profile").option("--display-name <displayName>").option("--description <description>").option("--website <url>").option("--email <email>").action(async (options) => {
|
|
2848
|
+
if (Object.values(options).every((value) => value === void 0)) {
|
|
2849
|
+
throw new Error("No publisher fields supplied. Pass --display-name, --description, --website or --email.");
|
|
2850
|
+
}
|
|
2851
|
+
const api = await createApiClient();
|
|
2852
|
+
const current = await requireMyPublisher(api);
|
|
2853
|
+
await call(api, marketplace.updatePublisher, {
|
|
2854
|
+
params: { id: current.id },
|
|
2855
|
+
body: {
|
|
2856
|
+
id: current.id,
|
|
2857
|
+
displayName: options.displayName ?? current.displayName,
|
|
2858
|
+
description: options.description ?? current.description ?? null,
|
|
2859
|
+
websiteUrl: options.website ?? current.websiteUrl ?? null,
|
|
2860
|
+
contactEmail: options.email ?? current.contactEmail ?? null
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
console.log(`Publisher '${current.name}' updated.`);
|
|
2864
|
+
});
|
|
2865
|
+
const logoCmd = publisherCmd.command("logo").description("Manage your publisher logo");
|
|
2866
|
+
logoCmd.command("set <file>").description("Upload a PNG, JPEG, WebP or SVG publisher logo (maximum 1 MiB)").action(async (file) => {
|
|
2867
|
+
const api = await createApiClient();
|
|
2868
|
+
const publisher = await requireMyPublisher(api);
|
|
2869
|
+
await uploadPublisherLogo(api, publisher.id, file);
|
|
2870
|
+
console.log(`Publisher logo uploaded from ${file}.`);
|
|
2871
|
+
});
|
|
2872
|
+
logoCmd.command("remove").description("Remove your publisher logo").action(async () => {
|
|
2873
|
+
const api = await createApiClient();
|
|
2874
|
+
const publisher = await requireMyPublisher(api);
|
|
2875
|
+
await call(api, marketplace.removePublisherLogo, { params: { id: publisher.id } });
|
|
2876
|
+
console.log("Publisher logo removed.");
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
async function uploadPublisherLogo(api, publisherId, path) {
|
|
2880
|
+
const logo = await readPublisherLogo(path);
|
|
2881
|
+
await callRaw(api, marketplace.setPublisherLogo, {
|
|
2882
|
+
params: { id: publisherId },
|
|
2883
|
+
rawBody: new Blob([new Uint8Array(logo.bytes)], { type: logo.contentType }),
|
|
2884
|
+
contentType: logo.contentType
|
|
2885
|
+
});
|
|
2337
2886
|
}
|
|
2338
2887
|
function registerRegistry(program2) {
|
|
2339
2888
|
program2.command("publish <vpkg>").description("Upload a packed .vpkg to the marketplace registry (creates the package on first publish)").option("--visibility <visibility>", "catalog visibility for a newly created package: Public | Private", "Private").action(async (vpkgPath, options) => {
|
|
@@ -2492,11 +3041,11 @@ function getCliVersion() {
|
|
|
2492
3041
|
}
|
|
2493
3042
|
function readVersion() {
|
|
2494
3043
|
const candidates = ["../package.json", "../../package.json"];
|
|
2495
|
-
for (const
|
|
3044
|
+
for (const relative3 of candidates) {
|
|
2496
3045
|
try {
|
|
2497
|
-
const raw = readFileSync(fileURLToPath(new URL(
|
|
3046
|
+
const raw = readFileSync(fileURLToPath(new URL(relative3, import.meta.url)), "utf8");
|
|
2498
3047
|
const pkg = JSON.parse(raw);
|
|
2499
|
-
if (pkg.version && (
|
|
3048
|
+
if (pkg.version && (relative3 === "../package.json" || pkg.name === PACKAGE_NAME)) return pkg.version;
|
|
2500
3049
|
} catch {
|
|
2501
3050
|
}
|
|
2502
3051
|
}
|
|
@@ -2534,8 +3083,8 @@ function isNewerVersion(candidate, base) {
|
|
|
2534
3083
|
return compareVersions(candidate, base) > 0;
|
|
2535
3084
|
}
|
|
2536
3085
|
function compareVersions(a, b) {
|
|
2537
|
-
const pa =
|
|
2538
|
-
const pb =
|
|
3086
|
+
const pa = parse5(a);
|
|
3087
|
+
const pb = parse5(b);
|
|
2539
3088
|
for (let i = 0; i < 3; i++) {
|
|
2540
3089
|
if (pa.main[i] !== pb.main[i]) return pa.main[i] < pb.main[i] ? -1 : 1;
|
|
2541
3090
|
}
|
|
@@ -2562,7 +3111,7 @@ function compareVersions(a, b) {
|
|
|
2562
3111
|
}
|
|
2563
3112
|
return 0;
|
|
2564
3113
|
}
|
|
2565
|
-
function
|
|
3114
|
+
function parse5(version) {
|
|
2566
3115
|
const cleaned = version.trim().replace(/^v/i, "").split("+", 1)[0];
|
|
2567
3116
|
const dash = cleaned.indexOf("-");
|
|
2568
3117
|
const core = dash === -1 ? cleaned : cleaned.slice(0, dash);
|
|
@@ -2839,13 +3388,22 @@ function registerUser(program2) {
|
|
|
2839
3388
|
});
|
|
2840
3389
|
}
|
|
2841
3390
|
function registerValidate(program2) {
|
|
2842
|
-
program2.command("validate [pathOrDir]").description("Validate an engine/app/bundle manifest (structure + registration-time checks)").action(async (pathOrDir) => {
|
|
3391
|
+
program2.command("validate [pathOrDir]").description("Validate an engine/app/bundle manifest (structure + registration-time checks)").option("--env <name>", "merge a per-environment overlay (environments/<name>.yaml) before validating").option("--overlay <path>", "merge an explicit overlay file before validating (wins over --env)").action(async (pathOrDir, options) => {
|
|
2843
3392
|
const target = resolve(pathOrDir ?? ".");
|
|
2844
3393
|
const info = await stat(target).catch(() => null);
|
|
2845
3394
|
const manifestPath = info?.isDirectory() ? await findManifest(target) : target;
|
|
2846
3395
|
if (!info) throw new Error(`${target} does not exist.`);
|
|
2847
|
-
const
|
|
3396
|
+
const base = await readManifestDocument(manifestPath);
|
|
3397
|
+
const overlayPath = resolveOverlayPath(dirname(manifestPath), { env: options.env, overlay: options.overlay });
|
|
3398
|
+
const manifest = overlayPath ? applyOverlay(base, await loadOverlay(overlayPath)) : base;
|
|
2848
3399
|
const issues = validateManifest(manifest);
|
|
3400
|
+
if (manifest.marketplace && !issues.some((issue) => issue.level === "error" && issue.message.startsWith("marketplace"))) {
|
|
3401
|
+
try {
|
|
3402
|
+
await buildCatalog(dirname(manifestPath), manifest.marketplace);
|
|
3403
|
+
} catch (error) {
|
|
3404
|
+
issues.push({ level: "error", message: error instanceof Error ? error.message : String(error) });
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
2849
3407
|
const errors = issues.filter((issue) => issue.level === "error");
|
|
2850
3408
|
const warnings = issues.filter((issue) => issue.level === "warning");
|
|
2851
3409
|
console.log(`${manifestPath} \u2014 kind ${manifest.kind ?? "(unknown)"}, ${manifest.metadata?.name ?? "?"}@${manifest.metadata?.version ?? "?"}`);
|
|
@@ -2944,7 +3502,7 @@ function registerWorkspace(program2) {
|
|
|
2944
3502
|
}
|
|
2945
3503
|
|
|
2946
3504
|
// src/index.ts
|
|
2947
|
-
var program = new Command().name("verentis").description("Verentis CLI \u2014 admin, marketplace publishing, and the engine/app developer loop").version(getCliVersion());
|
|
3505
|
+
var program = new Command().enablePositionalOptions().name("verentis").description("Verentis CLI \u2014 admin, marketplace publishing, and the engine/app developer loop").version(getCliVersion());
|
|
2948
3506
|
registerLogin(program);
|
|
2949
3507
|
registerUse(program);
|
|
2950
3508
|
registerAccount(program);
|