@verentis/cli 0.2.3 → 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 +85 -3
- package/dist/index.js +915 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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,
|
|
6
|
-
import { spawn } from 'child_process';
|
|
5
|
+
import { resolve, basename, dirname, posix, join, isAbsolute, extname, relative, sep } from 'path';
|
|
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';
|
|
9
|
-
import { createWriteStream, createReadStream } from 'fs';
|
|
8
|
+
import { parse, stringify } from 'yaml';
|
|
9
|
+
import { createWriteStream, readFileSync, createReadStream } from 'fs';
|
|
10
10
|
import { Readable } from 'stream';
|
|
11
11
|
import { pipeline } from 'stream/promises';
|
|
12
12
|
import * as tar from 'tar';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
13
14
|
|
|
14
15
|
var configDir = () => process.env.VERENTIS_CONFIG_DIR ?? join(homedir(), ".verentis");
|
|
15
16
|
var configPath = () => join(configDir(), "config.json");
|
|
@@ -129,7 +130,7 @@ async function refreshIdentityToken(apiUrl, refreshToken) {
|
|
|
129
130
|
`Session could not be renewed${payload?.error ? ` (${payload.error})` : ` (${res.status})`}. Run \`verentis login\` again.`
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
|
-
var sleep = (ms) => new Promise((
|
|
133
|
+
var sleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
133
134
|
|
|
134
135
|
// src/http.ts
|
|
135
136
|
var ApiError = class extends Error {
|
|
@@ -192,8 +193,8 @@ async function createApiClient(overrides) {
|
|
|
192
193
|
}
|
|
193
194
|
async function credentialFor(scopes, workspaceId) {
|
|
194
195
|
const cacheKey = `${workspaceId ?? ""}:${[...scopes].sort().join(" ")}`;
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
196
|
+
const cached2 = tokenCache.get(cacheKey);
|
|
197
|
+
if (cached2 && !isExpired(cached2)) return cached2;
|
|
197
198
|
const token = await mintAccessToken({ scopes, workspaceId });
|
|
198
199
|
tokenCache.set(cacheKey, token);
|
|
199
200
|
return token;
|
|
@@ -297,7 +298,6 @@ ${truncate(text)}`;
|
|
|
297
298
|
}
|
|
298
299
|
var SCOPES = {
|
|
299
300
|
publisherRead: "marketplace.publisher.read",
|
|
300
|
-
publisherCreate: "marketplace.publisher.create",
|
|
301
301
|
publisherUpdate: "marketplace.publisher.update",
|
|
302
302
|
packageRead: "marketplace.package.read",
|
|
303
303
|
packageReadAll: "marketplace.package.read-all",
|
|
@@ -366,6 +366,8 @@ var MARKETPLACE = {
|
|
|
366
366
|
packageRead: "marketplace.package.read",
|
|
367
367
|
packageUpdate: "marketplace.package.update",
|
|
368
368
|
packageVisibilityManage: "marketplace.package.visibility.manage",
|
|
369
|
+
publisherCreate: "marketplace.publisher.create",
|
|
370
|
+
publisherUpdate: "marketplace.publisher.update",
|
|
369
371
|
statsRead: "marketplace.stats.read"};
|
|
370
372
|
var ENGINE_DEFAULT_PERMISSIONS = [
|
|
371
373
|
NODE.fileRead,
|
|
@@ -454,6 +456,10 @@ var execution = {
|
|
|
454
456
|
syncEngines: e({ method: "POST", path: "/v1/engines/sync", scopes: [EXECUTION.run], workspaceBound: true })
|
|
455
457
|
};
|
|
456
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] }),
|
|
457
463
|
getPackage: e({ method: "GET", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageRead] }),
|
|
458
464
|
updatePackage: e({ method: "PUT", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageUpdate] }),
|
|
459
465
|
archivePackage: e({ method: "DELETE", path: "/v1/packages/{id}", scopes: [MARKETPLACE.packageDelete] }),
|
|
@@ -844,6 +850,122 @@ function contextEnv(context, contextPath) {
|
|
|
844
850
|
VERENTIS_TIMEOUT: String(context.execution.timeout)
|
|
845
851
|
};
|
|
846
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
|
|
847
969
|
async function readManifestDocument(path) {
|
|
848
970
|
const text = await readFile(path, "utf8");
|
|
849
971
|
const doc = parse(text);
|
|
@@ -873,6 +995,8 @@ function validateManifest(manifest) {
|
|
|
873
995
|
if (!meta[field]) error(`metadata.${field} is required`);
|
|
874
996
|
}
|
|
875
997
|
const spec = manifest.spec ?? {};
|
|
998
|
+
const launchKindError = validateLaunchManifestKind(kind, spec);
|
|
999
|
+
if (launchKindError) error(launchKindError);
|
|
876
1000
|
if (kind === "ExecutionEngine") {
|
|
877
1001
|
const runtimes = spec.runtimes ?? {};
|
|
878
1002
|
if (Object.keys(runtimes).length === 0) {
|
|
@@ -901,12 +1025,62 @@ function validateManifest(manifest) {
|
|
|
901
1025
|
"an Application must declare at least one contribution: spec.entry / spec.entry-points[] (iframe UI), spec.install[] (installed files), or spec.scopes[] (identity)"
|
|
902
1026
|
);
|
|
903
1027
|
}
|
|
1028
|
+
validateApplicationLaunch(spec, issues);
|
|
904
1029
|
}
|
|
905
1030
|
validateInstallDirectives(spec.install, issues);
|
|
1031
|
+
for (const message of validateMarketplaceSection(manifest.marketplace)) error(message);
|
|
906
1032
|
const packaging = manifest.packaging ?? {};
|
|
907
1033
|
if (!packaging.publisher) warning("packaging.publisher is not set \u2014 `verentis pack`/`publish` will refuse the package");
|
|
908
1034
|
return issues;
|
|
909
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
|
+
}
|
|
910
1084
|
function validateInstallDirectives(install, issues) {
|
|
911
1085
|
if (install === void 0) return;
|
|
912
1086
|
const error = (message) => issues.push({ level: "error", message });
|
|
@@ -936,46 +1110,6 @@ function validateInstallDirectives(install, issues) {
|
|
|
936
1110
|
}
|
|
937
1111
|
});
|
|
938
1112
|
}
|
|
939
|
-
var KIND_MAP = {
|
|
940
|
-
application: "Application",
|
|
941
|
-
executionengine: "ExecutionEngine",
|
|
942
|
-
bundle: "Bundle"
|
|
943
|
-
};
|
|
944
|
-
function normalizeKind(kind) {
|
|
945
|
-
const mapped = KIND_MAP[kind.replace(/[^a-z]/gi, "").toLowerCase()];
|
|
946
|
-
if (!mapped) throw new Error(`Unsupported manifest kind '${kind}' \u2014 expected Application, ExecutionEngine or Bundle.`);
|
|
947
|
-
return mapped;
|
|
948
|
-
}
|
|
949
|
-
async function loadManifest(path) {
|
|
950
|
-
const text = await readFile(path, "utf8");
|
|
951
|
-
const manifest = parse(text);
|
|
952
|
-
if (!manifest || typeof manifest !== "object") throw new Error(`Failed to parse manifest at ${path}.`);
|
|
953
|
-
if (!manifest.kind) throw new Error(`Manifest at ${path} is missing 'kind'.`);
|
|
954
|
-
if (!manifest.metadata?.name) throw new Error(`Manifest at ${path} is missing 'metadata.name'.`);
|
|
955
|
-
if (!manifest.metadata?.version) throw new Error(`Manifest at ${path} is missing 'metadata.version'.`);
|
|
956
|
-
normalizeKind(manifest.kind);
|
|
957
|
-
return manifest;
|
|
958
|
-
}
|
|
959
|
-
async function findManifest(dir) {
|
|
960
|
-
const explicit = join(dir, "manifest.yaml");
|
|
961
|
-
try {
|
|
962
|
-
if ((await stat(explicit)).isFile()) return explicit;
|
|
963
|
-
} catch {
|
|
964
|
-
}
|
|
965
|
-
const entries = await readdir(dir);
|
|
966
|
-
const candidates = entries.filter((name) => /\.(app|engine|bundle)\.ya?ml$/.test(name));
|
|
967
|
-
if (candidates.length === 1) return join(dir, candidates[0]);
|
|
968
|
-
if (candidates.length === 0) {
|
|
969
|
-
throw new Error(`No manifest found in ${dir}. Expected manifest.yaml or *.app.yaml / *.engine.yaml / *.bundle.yaml.`);
|
|
970
|
-
}
|
|
971
|
-
throw new Error(`Multiple manifests found in ${dir} (${candidates.join(", ")}). Pass --manifest to disambiguate.`);
|
|
972
|
-
}
|
|
973
|
-
var NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
974
|
-
function validatePackageName(name, label) {
|
|
975
|
-
if (!NAME_PATTERN.test(name)) {
|
|
976
|
-
throw new Error(`Invalid ${label} '${name}' \u2014 use lowercase letters, digits and hyphens (no leading/trailing hyphen).`);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
1113
|
|
|
980
1114
|
// src/commands/dev.ts
|
|
981
1115
|
async function resolveDevScopes(flags) {
|
|
@@ -1325,7 +1459,7 @@ async function followExecution(api, workspaceId, executionId, json) {
|
|
|
1325
1459
|
if (exec.status !== "Completed") process.exitCode = 1;
|
|
1326
1460
|
return;
|
|
1327
1461
|
}
|
|
1328
|
-
await new Promise((
|
|
1462
|
+
await new Promise((resolve11) => setTimeout(resolve11, 1e3));
|
|
1329
1463
|
}
|
|
1330
1464
|
console.error("Timed out waiting for a terminal status \u2014 check `verentis exec get`.");
|
|
1331
1465
|
process.exitCode = 1;
|
|
@@ -1478,10 +1612,31 @@ metadata:
|
|
|
1478
1612
|
version: 0.1.0
|
|
1479
1613
|
author: ""
|
|
1480
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
|
+
|
|
1481
1630
|
spec:
|
|
1482
1631
|
entry: https://example.com # hosted app origin
|
|
1483
1632
|
scope: global
|
|
1484
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
|
+
|
|
1485
1640
|
entry-points: []
|
|
1486
1641
|
capabilities: []
|
|
1487
1642
|
permissions: []
|
|
@@ -1508,6 +1663,20 @@ metadata:
|
|
|
1508
1663
|
version: 0.1.0
|
|
1509
1664
|
author: ""
|
|
1510
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
|
+
|
|
1511
1680
|
spec:
|
|
1512
1681
|
runtimes: {}
|
|
1513
1682
|
file-types: []
|
|
@@ -1529,6 +1698,20 @@ metadata:
|
|
|
1529
1698
|
version: 0.1.0
|
|
1530
1699
|
author: ""
|
|
1531
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
|
+
|
|
1532
1715
|
packaging:
|
|
1533
1716
|
publisher: "" # your publisher name
|
|
1534
1717
|
files:
|
|
@@ -1908,20 +2091,260 @@ function rawX25519Private(key) {
|
|
|
1908
2091
|
function publicKeyFromRaw(raw) {
|
|
1909
2092
|
return createPublicKey({ key: { kty: "OKP", crv: "X25519", x: raw.toString("base64url") }, format: "jwk" });
|
|
1910
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
|
|
1911
2322
|
async function pack(options) {
|
|
1912
2323
|
const manifestPath = resolve(options.manifestPath);
|
|
1913
2324
|
const projectDir = dirname(manifestPath);
|
|
1914
|
-
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
|
+
}
|
|
1915
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(" ")}`);
|
|
1916
2338
|
const name = (options.name ?? manifest.packaging?.name ?? manifest.metadata.name).toLowerCase();
|
|
1917
2339
|
const publisher = options.publisher.toLowerCase();
|
|
1918
2340
|
validatePackageName(name, "package name");
|
|
1919
2341
|
validatePackageName(publisher, "publisher name");
|
|
1920
2342
|
const version = manifest.metadata.version;
|
|
1921
2343
|
const payloadFiles = await resolvePayloadFiles(projectDir, manifestPath, manifest);
|
|
2344
|
+
const catalog = manifest.marketplace ? await buildCatalog(projectDir, manifest.marketplace) : void 0;
|
|
1922
2345
|
const staging = await mkdtemp(join(tmpdir(), "vpkg-"));
|
|
1923
2346
|
try {
|
|
1924
|
-
const manifestText = await readFile(manifestPath, "utf8");
|
|
2347
|
+
const manifestText = overrideApplied ? stringify(manifest) : await readFile(manifestPath, "utf8");
|
|
1925
2348
|
await writeFile(join(staging, "manifest.yaml"), manifestText);
|
|
1926
2349
|
const encrypt = (options.encryptFor?.length ?? 0) > 0;
|
|
1927
2350
|
const contentKey = encrypt ? generateContentKey() : null;
|
|
@@ -1930,13 +2353,24 @@ async function pack(options) {
|
|
|
1930
2353
|
};
|
|
1931
2354
|
for (const file of payloadFiles) {
|
|
1932
2355
|
const sourcePath = join(projectDir, file);
|
|
1933
|
-
const entryName = `files/${
|
|
2356
|
+
const entryName = `files/${toEntryPath2(file)}`;
|
|
1934
2357
|
const targetPath = join(staging, "files", file);
|
|
1935
2358
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
1936
2359
|
const bytes = contentKey ? encryptBytes(contentKey, await readFile(sourcePath)) : await readFile(sourcePath);
|
|
1937
2360
|
await writeFile(targetPath, bytes);
|
|
1938
2361
|
digests[entryName] = sha256Hex(bytes);
|
|
1939
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
|
+
}
|
|
1940
2374
|
await mkdir(join(staging, ".verentis", "signatures"), { recursive: true });
|
|
1941
2375
|
if (contentKey) {
|
|
1942
2376
|
const encryption = {
|
|
@@ -1972,16 +2406,19 @@ async function pack(options) {
|
|
|
1972
2406
|
".verentis/digests.json",
|
|
1973
2407
|
...contentKey ? [".verentis/encryption.json"] : [],
|
|
1974
2408
|
...signed ? [".verentis/signatures/developer.dsse.json"] : [],
|
|
1975
|
-
...payloadFiles.map((file) => `files/${
|
|
2409
|
+
...payloadFiles.map((file) => `files/${toEntryPath2(file)}`),
|
|
2410
|
+
...catalog ? ["catalog/index.json", ...catalog.entries.map((entry) => entry.path)] : []
|
|
1976
2411
|
];
|
|
1977
|
-
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);
|
|
1978
2413
|
return {
|
|
1979
2414
|
outFile,
|
|
1980
2415
|
name,
|
|
1981
2416
|
publisher,
|
|
1982
2417
|
version,
|
|
1983
2418
|
kind,
|
|
1984
|
-
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\//, ""))] : [],
|
|
1985
2422
|
treeDigest: sha256Hex(Buffer.from(digestsJson, "utf8")),
|
|
1986
2423
|
signed,
|
|
1987
2424
|
encrypted: encrypt
|
|
@@ -2008,7 +2445,7 @@ async function resolvePayloadFiles(projectDir, manifestPath, manifest) {
|
|
|
2008
2445
|
}
|
|
2009
2446
|
function defaultIncludes(manifest) {
|
|
2010
2447
|
const sources = /* @__PURE__ */ new Set();
|
|
2011
|
-
collectSchemaSources(manifest, sources);
|
|
2448
|
+
collectSchemaSources(manifest.spec, sources);
|
|
2012
2449
|
collectInstallSources(manifest, sources);
|
|
2013
2450
|
if (sources.size === 0) return [];
|
|
2014
2451
|
return [...sources];
|
|
@@ -2039,7 +2476,7 @@ function collectSchemaSources(node, sources) {
|
|
|
2039
2476
|
}
|
|
2040
2477
|
}
|
|
2041
2478
|
}
|
|
2042
|
-
function
|
|
2479
|
+
function toEntryPath2(file) {
|
|
2043
2480
|
return file.split(sep).join("/");
|
|
2044
2481
|
}
|
|
2045
2482
|
function sha256Hex(data) {
|
|
@@ -2060,21 +2497,18 @@ async function readVpkg(vpkgPath) {
|
|
|
2060
2497
|
});
|
|
2061
2498
|
const metadata = parse(packageYaml);
|
|
2062
2499
|
const declared = JSON.parse(digestsJson);
|
|
2063
|
-
const files2 =
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
const info = await readFile(abs).catch(() => null);
|
|
2067
|
-
if (info === null) continue;
|
|
2068
|
-
files2.push(entry.split(sep).join("/"));
|
|
2069
|
-
}
|
|
2070
|
-
files2.sort();
|
|
2500
|
+
const files2 = await listFiles(staging, "files/**/*");
|
|
2501
|
+
const catalogFiles = await listFiles(staging, "catalog/**/*");
|
|
2502
|
+
const contentFiles = [...files2, ...catalogFiles];
|
|
2071
2503
|
const digestErrors = [];
|
|
2072
2504
|
const entries = declared.entries ?? {};
|
|
2073
2505
|
const manifestDigest = createHash("sha256").update(manifestYaml, "utf8").digest("hex");
|
|
2074
|
-
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) {
|
|
2075
2509
|
digestErrors.push("Digest mismatch for manifest.yaml.");
|
|
2076
2510
|
}
|
|
2077
|
-
for (const file of
|
|
2511
|
+
for (const file of contentFiles) {
|
|
2078
2512
|
const declaredDigest = entries[file];
|
|
2079
2513
|
if (!declaredDigest) {
|
|
2080
2514
|
digestErrors.push(`Entry '${file}' is not covered by digests.json.`);
|
|
@@ -2084,10 +2518,21 @@ async function readVpkg(vpkgPath) {
|
|
|
2084
2518
|
if (actual !== declaredDigest.toLowerCase()) digestErrors.push(`Digest mismatch for '${file}'.`);
|
|
2085
2519
|
}
|
|
2086
2520
|
for (const declaredPath of Object.keys(entries)) {
|
|
2087
|
-
if (declaredPath !== "manifest.yaml" && !
|
|
2521
|
+
if (declaredPath !== "manifest.yaml" && !contentFiles.includes(declaredPath)) {
|
|
2088
2522
|
digestErrors.push(`Digest declared for missing entry '${declaredPath}'.`);
|
|
2089
2523
|
}
|
|
2090
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
|
+
}
|
|
2091
2536
|
const developerSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "developer.dsse.json"));
|
|
2092
2537
|
const marketplaceSignature = await readJsonIfExists(join(staging, ".verentis", "signatures", "marketplace.dsse.json"));
|
|
2093
2538
|
const encryption = await readJsonIfExists(join(staging, ".verentis", "encryption.json"));
|
|
@@ -2100,12 +2545,23 @@ async function readVpkg(vpkgPath) {
|
|
|
2100
2545
|
marketplaceSignature: marketplaceSignature ?? void 0,
|
|
2101
2546
|
encryption: encryption ?? void 0,
|
|
2102
2547
|
files: files2.map((f) => f.replace(/^files\//, "")),
|
|
2548
|
+
catalog,
|
|
2549
|
+
catalogFiles: catalogFiles.map((f) => f.replace(/^catalog\//, "")),
|
|
2103
2550
|
digestErrors
|
|
2104
2551
|
};
|
|
2105
2552
|
} finally {
|
|
2106
2553
|
await rm(staging, { recursive: true, force: true });
|
|
2107
2554
|
}
|
|
2108
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
|
+
}
|
|
2109
2565
|
async function readJsonIfExists(path) {
|
|
2110
2566
|
try {
|
|
2111
2567
|
return JSON.parse(await readFile(path, "utf8"));
|
|
@@ -2116,7 +2572,7 @@ async function readJsonIfExists(path) {
|
|
|
2116
2572
|
|
|
2117
2573
|
// src/commands/pack.ts
|
|
2118
2574
|
function registerPack(program2) {
|
|
2119
|
-
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) => {
|
|
2120
2576
|
const projectDir = resolve(dir ?? ".");
|
|
2121
2577
|
const manifestPath = options.manifest ? resolve(options.manifest) : await findManifest(projectDir);
|
|
2122
2578
|
const manifest = await loadManifest(manifestPath);
|
|
@@ -2151,6 +2607,9 @@ function registerPack(program2) {
|
|
|
2151
2607
|
manifestPath,
|
|
2152
2608
|
publisher,
|
|
2153
2609
|
name: options.name,
|
|
2610
|
+
env: options.env,
|
|
2611
|
+
overlay: options.overlay,
|
|
2612
|
+
version: options.setVersion,
|
|
2154
2613
|
outDir: options.out,
|
|
2155
2614
|
signingKey,
|
|
2156
2615
|
encryptFor
|
|
@@ -2158,6 +2617,7 @@ function registerPack(program2) {
|
|
|
2158
2617
|
console.log(`Packed ${result.publisher}/${result.name}@${result.version} (${result.kind})`);
|
|
2159
2618
|
console.log(` ${result.outFile}`);
|
|
2160
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)`);
|
|
2161
2621
|
});
|
|
2162
2622
|
program2.command("seal-keygen").description("Generate an X25519 sealing keypair (for platform escrow or custom recipients)").action(() => {
|
|
2163
2623
|
const pair = generateSealingKeyPair();
|
|
@@ -2168,7 +2628,11 @@ function registerPack(program2) {
|
|
|
2168
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) => {
|
|
2169
2629
|
const contents = await readVpkg(resolve(vpkgPath));
|
|
2170
2630
|
console.log(`${contents.metadata.publisher}/${contents.metadata.name}@${contents.metadata.version} (${contents.metadata.kind})`);
|
|
2171
|
-
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
|
+
}
|
|
2172
2636
|
if (contents.encryption) {
|
|
2173
2637
|
console.log(` \u{1F512} encrypted (${contents.encryption.algorithm}); recipients: ${contents.encryption.recipients.map((r) => r.id).join(", ")}`);
|
|
2174
2638
|
}
|
|
@@ -2309,19 +2773,62 @@ function registerPackage(program2) {
|
|
|
2309
2773
|
output(pageItems(page), { packageName: "Package", version: "Version", status: "Status", installPath: "Path", id: "Id" }, options, page);
|
|
2310
2774
|
});
|
|
2311
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
|
+
}
|
|
2312
2820
|
|
|
2313
2821
|
// src/commands/publisher.ts
|
|
2314
2822
|
function registerPublisher(program2) {
|
|
2315
2823
|
const publisherCmd = program2.command("publisher").description("Manage your publisher profile");
|
|
2316
|
-
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) => {
|
|
2317
2825
|
const api = await createApiClient();
|
|
2318
2826
|
const existing = await getMyPublisher(api);
|
|
2319
2827
|
if (existing) {
|
|
2320
2828
|
console.log(`This account already has publisher '${existing.name}'.`);
|
|
2321
2829
|
return;
|
|
2322
2830
|
}
|
|
2323
|
-
const id = await api
|
|
2324
|
-
scopes: [SCOPES.publisherCreate],
|
|
2831
|
+
const id = await call(api, marketplace.createPublisher, {
|
|
2325
2832
|
body: {
|
|
2326
2833
|
name: options.name,
|
|
2327
2834
|
displayName: options.displayName,
|
|
@@ -2331,8 +2838,51 @@ function registerPublisher(program2) {
|
|
|
2331
2838
|
}
|
|
2332
2839
|
});
|
|
2333
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
|
+
}
|
|
2334
2845
|
console.log("Next: generate and register a signing key with `verentis keygen`.");
|
|
2335
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
|
+
});
|
|
2336
2886
|
}
|
|
2337
2887
|
function registerRegistry(program2) {
|
|
2338
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) => {
|
|
@@ -2482,6 +3032,271 @@ function registerSettings(program2) {
|
|
|
2482
3032
|
console.log(`Setting ${key} deleted.`);
|
|
2483
3033
|
});
|
|
2484
3034
|
}
|
|
3035
|
+
var PACKAGE_NAME = "@verentis/cli";
|
|
3036
|
+
var cached;
|
|
3037
|
+
function getCliVersion() {
|
|
3038
|
+
if (cached) return cached;
|
|
3039
|
+
cached = readVersion() ?? "0.0.0";
|
|
3040
|
+
return cached;
|
|
3041
|
+
}
|
|
3042
|
+
function readVersion() {
|
|
3043
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
3044
|
+
for (const relative3 of candidates) {
|
|
3045
|
+
try {
|
|
3046
|
+
const raw = readFileSync(fileURLToPath(new URL(relative3, import.meta.url)), "utf8");
|
|
3047
|
+
const pkg = JSON.parse(raw);
|
|
3048
|
+
if (pkg.version && (relative3 === "../package.json" || pkg.name === PACKAGE_NAME)) return pkg.version;
|
|
3049
|
+
} catch {
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
return void 0;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// src/update/registry.ts
|
|
3056
|
+
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
3057
|
+
function registryUrl() {
|
|
3058
|
+
const configured = process.env.VERENTIS_UPDATE_REGISTRY ?? process.env.npm_config_registry ?? DEFAULT_REGISTRY;
|
|
3059
|
+
return configured.replace(/\/+$/, "");
|
|
3060
|
+
}
|
|
3061
|
+
async function fetchLatestVersion(options = {}) {
|
|
3062
|
+
const url = `${registryUrl()}/${PACKAGE_NAME.replace("/", "%2F")}`;
|
|
3063
|
+
const controller = new AbortController();
|
|
3064
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
|
|
3065
|
+
try {
|
|
3066
|
+
const res = await fetch(url, {
|
|
3067
|
+
// The abbreviated packument is far smaller than the full manifest but still carries dist-tags.
|
|
3068
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
3069
|
+
signal: controller.signal
|
|
3070
|
+
});
|
|
3071
|
+
if (!res.ok) return null;
|
|
3072
|
+
const body = await res.json();
|
|
3073
|
+
return body["dist-tags"]?.latest ?? null;
|
|
3074
|
+
} catch {
|
|
3075
|
+
return null;
|
|
3076
|
+
} finally {
|
|
3077
|
+
clearTimeout(timer);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// src/update/semver.ts
|
|
3082
|
+
function isNewerVersion(candidate, base) {
|
|
3083
|
+
return compareVersions(candidate, base) > 0;
|
|
3084
|
+
}
|
|
3085
|
+
function compareVersions(a, b) {
|
|
3086
|
+
const pa = parse5(a);
|
|
3087
|
+
const pb = parse5(b);
|
|
3088
|
+
for (let i = 0; i < 3; i++) {
|
|
3089
|
+
if (pa.main[i] !== pb.main[i]) return pa.main[i] < pb.main[i] ? -1 : 1;
|
|
3090
|
+
}
|
|
3091
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
3092
|
+
if (pa.pre.length === 0) return 1;
|
|
3093
|
+
if (pb.pre.length === 0) return -1;
|
|
3094
|
+
const len = Math.max(pa.pre.length, pb.pre.length);
|
|
3095
|
+
for (let i = 0; i < len; i++) {
|
|
3096
|
+
if (i >= pa.pre.length) return -1;
|
|
3097
|
+
if (i >= pb.pre.length) return 1;
|
|
3098
|
+
const x = pa.pre[i];
|
|
3099
|
+
const y = pb.pre[i];
|
|
3100
|
+
const xNum = /^\d+$/.test(x);
|
|
3101
|
+
const yNum = /^\d+$/.test(y);
|
|
3102
|
+
if (xNum && yNum) {
|
|
3103
|
+
const nx = Number(x);
|
|
3104
|
+
const ny = Number(y);
|
|
3105
|
+
if (nx !== ny) return nx < ny ? -1 : 1;
|
|
3106
|
+
} else if (xNum !== yNum) {
|
|
3107
|
+
return xNum ? -1 : 1;
|
|
3108
|
+
} else if (x !== y) {
|
|
3109
|
+
return x < y ? -1 : 1;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
return 0;
|
|
3113
|
+
}
|
|
3114
|
+
function parse5(version) {
|
|
3115
|
+
const cleaned = version.trim().replace(/^v/i, "").split("+", 1)[0];
|
|
3116
|
+
const dash = cleaned.indexOf("-");
|
|
3117
|
+
const core = dash === -1 ? cleaned : cleaned.slice(0, dash);
|
|
3118
|
+
const pre = dash === -1 ? "" : cleaned.slice(dash + 1);
|
|
3119
|
+
const parts = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
3120
|
+
return { main: [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0], pre: pre ? pre.split(".") : [] };
|
|
3121
|
+
}
|
|
3122
|
+
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3123
|
+
var statePath = () => join(configDir(), "update-check.json");
|
|
3124
|
+
async function readUpdateState() {
|
|
3125
|
+
try {
|
|
3126
|
+
return JSON.parse(await readFile(statePath(), "utf8"));
|
|
3127
|
+
} catch {
|
|
3128
|
+
return null;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
async function writeUpdateState(state) {
|
|
3132
|
+
await mkdir(configDir(), { recursive: true, mode: 448 });
|
|
3133
|
+
await writeFile(statePath(), JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
3134
|
+
}
|
|
3135
|
+
function isStale(state, maxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
3136
|
+
if (!state?.lastCheckAt) return true;
|
|
3137
|
+
const checkedAt = Date.parse(state.lastCheckAt);
|
|
3138
|
+
return !Number.isFinite(checkedAt) || Date.now() - checkedAt > maxAgeMs;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// src/update/notifier.ts
|
|
3142
|
+
var SUPPRESSED_COMMANDS = /* @__PURE__ */ new Set(["update", "__update-check", "help"]);
|
|
3143
|
+
async function notifyOnExit(argv) {
|
|
3144
|
+
if (isUpdateCheckSuppressed(argv)) return;
|
|
3145
|
+
try {
|
|
3146
|
+
const current = getCliVersion();
|
|
3147
|
+
const state = await readUpdateState();
|
|
3148
|
+
if (state?.latestVersion && isNewerVersion(state.latestVersion, current)) {
|
|
3149
|
+
printUpdateNotice(current, state.latestVersion);
|
|
3150
|
+
}
|
|
3151
|
+
if (isStale(state)) spawnBackgroundRefresh();
|
|
3152
|
+
} catch {
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
async function runUpdateCheckRefresh() {
|
|
3156
|
+
const latest = await fetchLatestVersion({ timeoutMs: 4e3 }).catch(() => null);
|
|
3157
|
+
await writeUpdateState({ lastCheckAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion: latest ?? void 0 }).catch(() => {
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
function isUpdateCheckSuppressed(argv) {
|
|
3161
|
+
if (isEnvDisabled()) return true;
|
|
3162
|
+
if (process.env.CI) return true;
|
|
3163
|
+
if (!process.stderr.isTTY) return true;
|
|
3164
|
+
const args = argv.slice(2);
|
|
3165
|
+
if (args.length === 0) return true;
|
|
3166
|
+
if (args.some((a) => a === "--json" || a === "-h" || a === "--help" || a === "-V" || a === "--version")) return true;
|
|
3167
|
+
const command = args.find((a) => !a.startsWith("-"));
|
|
3168
|
+
return command !== void 0 && SUPPRESSED_COMMANDS.has(command);
|
|
3169
|
+
}
|
|
3170
|
+
function isEnvDisabled() {
|
|
3171
|
+
const off = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
3172
|
+
const noCheck = process.env.VERENTIS_NO_UPDATE_CHECK;
|
|
3173
|
+
if (noCheck !== void 0 && noCheck !== "" && !off.has(noCheck.toLowerCase())) return true;
|
|
3174
|
+
const explicit = process.env.VERENTIS_UPDATE_CHECK;
|
|
3175
|
+
return explicit !== void 0 && off.has(explicit.toLowerCase());
|
|
3176
|
+
}
|
|
3177
|
+
function printUpdateNotice(current, latest) {
|
|
3178
|
+
const message = [
|
|
3179
|
+
`Update available ${current} \u2192 ${latest}`,
|
|
3180
|
+
"Run `verentis update` to install the latest version."
|
|
3181
|
+
];
|
|
3182
|
+
const width = Math.max(...message.map((line) => line.length));
|
|
3183
|
+
const border = "\u2500".repeat(width + 2);
|
|
3184
|
+
const lines = ["", `\u256D${border}\u256E`, ...message.map((line) => `\u2502 ${line.padEnd(width)} \u2502`), `\u2570${border}\u256F`, ""];
|
|
3185
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
3186
|
+
}
|
|
3187
|
+
function spawnBackgroundRefresh() {
|
|
3188
|
+
try {
|
|
3189
|
+
const entry = process.argv[1];
|
|
3190
|
+
if (!entry) return;
|
|
3191
|
+
const child = spawn(process.execPath, [entry, "__update-check"], {
|
|
3192
|
+
detached: true,
|
|
3193
|
+
stdio: "ignore",
|
|
3194
|
+
windowsHide: true
|
|
3195
|
+
});
|
|
3196
|
+
child.on("error", () => {
|
|
3197
|
+
});
|
|
3198
|
+
child.unref();
|
|
3199
|
+
} catch {
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/commands/update.ts
|
|
3204
|
+
function registerUpdate(program2) {
|
|
3205
|
+
program2.command("update").description("Update the Verentis CLI to the latest published version").option("--check", "report whether a newer version is available without installing").option("--to <version>", "install a specific version instead of the latest").action(async (options) => {
|
|
3206
|
+
const current = getCliVersion();
|
|
3207
|
+
const target = await resolveTarget(options.to);
|
|
3208
|
+
if (options.check) {
|
|
3209
|
+
reportCheck(current, target, Boolean(options.to));
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
if (!options.to && !isNewerVersion(target, current)) {
|
|
3213
|
+
console.log(`Already on the latest version (${current}).`);
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
const install = resolveInstallCommand(`${PACKAGE_NAME}@${target}`);
|
|
3217
|
+
if (!install) {
|
|
3218
|
+
throw new Error(
|
|
3219
|
+
`Automatic update is only supported for a global install of ${PACKAGE_NAME}.
|
|
3220
|
+
Update manually, for example:
|
|
3221
|
+
npm install -g ${PACKAGE_NAME}@${target}`
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
console.log(`Updating ${PACKAGE_NAME}: ${current} \u2192 ${target}`);
|
|
3225
|
+
console.log(`$ ${install.command} ${install.args.join(" ")}`);
|
|
3226
|
+
const result = spawnSync(install.command, install.args, { stdio: "inherit" });
|
|
3227
|
+
if (result.error) {
|
|
3228
|
+
const code = result.error.code;
|
|
3229
|
+
const hint = code === "ENOENT" ? ` ('${install.command}' was not found on PATH).` : `: ${result.error.message}`;
|
|
3230
|
+
throw new Error(`Could not run the update${hint}
|
|
3231
|
+
Update manually:
|
|
3232
|
+
${install.command} ${install.args.join(" ")}`);
|
|
3233
|
+
}
|
|
3234
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
3235
|
+
throw new Error(
|
|
3236
|
+
`Update failed (${install.command} exited with code ${result.status}).
|
|
3237
|
+
You can retry manually:
|
|
3238
|
+
${install.command} ${install.args.join(" ")}`
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
console.log(`Updated to ${target}. Run \`verentis --version\` to confirm.`);
|
|
3242
|
+
});
|
|
3243
|
+
program2.command("__update-check", { hidden: true }).description("(internal) refresh the cached latest-version marker").action(async () => {
|
|
3244
|
+
await runUpdateCheckRefresh();
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
async function resolveTarget(requested) {
|
|
3248
|
+
if (requested) return requested.replace(/^v/i, "");
|
|
3249
|
+
const latest = await fetchLatestVersion({ timeoutMs: 8e3 });
|
|
3250
|
+
if (!latest) {
|
|
3251
|
+
throw new Error(
|
|
3252
|
+
"Could not reach the package registry to determine the latest version. Check your connection, or point VERENTIS_UPDATE_REGISTRY at a reachable registry."
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
await writeUpdateState({ lastCheckAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion: latest }).catch(() => {
|
|
3256
|
+
});
|
|
3257
|
+
return latest;
|
|
3258
|
+
}
|
|
3259
|
+
function reportCheck(current, target, pinned) {
|
|
3260
|
+
if (pinned) {
|
|
3261
|
+
console.log(`Current version: ${current}`);
|
|
3262
|
+
console.log(`Requested version: ${target}`);
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
if (isNewerVersion(target, current)) {
|
|
3266
|
+
console.log(`A new version is available: ${current} \u2192 ${target}`);
|
|
3267
|
+
console.log("Run `verentis update` to install it.");
|
|
3268
|
+
} else {
|
|
3269
|
+
console.log(`You are on the latest version (${current}).`);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
function resolveInstallCommand(spec) {
|
|
3273
|
+
const location = packageLocation();
|
|
3274
|
+
if (!location || !location.replace(/\\/g, "/").includes("/node_modules/")) return null;
|
|
3275
|
+
switch (detectPackageManager(location)) {
|
|
3276
|
+
case "pnpm":
|
|
3277
|
+
return { command: "pnpm", args: ["add", "-g", spec] };
|
|
3278
|
+
case "yarn":
|
|
3279
|
+
return { command: "yarn", args: ["global", "add", spec] };
|
|
3280
|
+
case "bun":
|
|
3281
|
+
return { command: "bun", args: ["add", "-g", spec] };
|
|
3282
|
+
default:
|
|
3283
|
+
return { command: "npm", args: ["install", "-g", spec] };
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
function packageLocation() {
|
|
3287
|
+
try {
|
|
3288
|
+
return fileURLToPath(import.meta.url);
|
|
3289
|
+
} catch {
|
|
3290
|
+
return process.argv[1];
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
function detectPackageManager(location) {
|
|
3294
|
+
const path = location.replace(/\\/g, "/");
|
|
3295
|
+
if (/\/(\.pnpm|pnpm)\//.test(path)) return "pnpm";
|
|
3296
|
+
if (/\/(\.yarn|yarn)\//.test(path)) return "yarn";
|
|
3297
|
+
if (/\/\.bun\//.test(path)) return "bun";
|
|
3298
|
+
return "npm";
|
|
3299
|
+
}
|
|
2485
3300
|
|
|
2486
3301
|
// src/commands/use.ts
|
|
2487
3302
|
function registerUse(program2) {
|
|
@@ -2573,13 +3388,22 @@ function registerUser(program2) {
|
|
|
2573
3388
|
});
|
|
2574
3389
|
}
|
|
2575
3390
|
function registerValidate(program2) {
|
|
2576
|
-
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) => {
|
|
2577
3392
|
const target = resolve(pathOrDir ?? ".");
|
|
2578
3393
|
const info = await stat(target).catch(() => null);
|
|
2579
3394
|
const manifestPath = info?.isDirectory() ? await findManifest(target) : target;
|
|
2580
3395
|
if (!info) throw new Error(`${target} does not exist.`);
|
|
2581
|
-
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;
|
|
2582
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
|
+
}
|
|
2583
3407
|
const errors = issues.filter((issue) => issue.level === "error");
|
|
2584
3408
|
const warnings = issues.filter((issue) => issue.level === "warning");
|
|
2585
3409
|
console.log(`${manifestPath} \u2014 kind ${manifest.kind ?? "(unknown)"}, ${manifest.metadata?.name ?? "?"}@${manifest.metadata?.version ?? "?"}`);
|
|
@@ -2678,7 +3502,7 @@ function registerWorkspace(program2) {
|
|
|
2678
3502
|
}
|
|
2679
3503
|
|
|
2680
3504
|
// src/index.ts
|
|
2681
|
-
var program = new Command().name("verentis").description("Verentis CLI \u2014 admin, marketplace publishing, and the engine/app developer loop").version(
|
|
3505
|
+
var program = new Command().enablePositionalOptions().name("verentis").description("Verentis CLI \u2014 admin, marketplace publishing, and the engine/app developer loop").version(getCliVersion());
|
|
2682
3506
|
registerLogin(program);
|
|
2683
3507
|
registerUse(program);
|
|
2684
3508
|
registerAccount(program);
|
|
@@ -2697,13 +3521,20 @@ registerPack(program);
|
|
|
2697
3521
|
registerRegistry(program);
|
|
2698
3522
|
registerPackage(program);
|
|
2699
3523
|
registerApi(program);
|
|
2700
|
-
program
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
3524
|
+
registerUpdate(program);
|
|
3525
|
+
async function main() {
|
|
3526
|
+
try {
|
|
3527
|
+
await program.parseAsync();
|
|
3528
|
+
} catch (error) {
|
|
3529
|
+
if (error instanceof ApiError || error instanceof Error) {
|
|
3530
|
+
console.error(`Error: ${error.message}`);
|
|
3531
|
+
} else {
|
|
3532
|
+
console.error(error);
|
|
3533
|
+
}
|
|
3534
|
+
process.exitCode = 1;
|
|
2705
3535
|
}
|
|
2706
|
-
process.
|
|
2707
|
-
}
|
|
3536
|
+
await notifyOnExit(process.argv);
|
|
3537
|
+
}
|
|
3538
|
+
void main();
|
|
2708
3539
|
//# sourceMappingURL=index.js.map
|
|
2709
3540
|
//# sourceMappingURL=index.js.map
|