@openwop/cli 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -578,7 +578,7 @@ function errText(err) {
578
578
  // package.json
579
579
  var package_default = {
580
580
  name: "@openwop/cli",
581
- version: "1.2.3",
581
+ version: "1.3.0",
582
582
  description: "OpenWOP CLI for operating the workflow-engine demo app and OpenWOP-compatible hosts.",
583
583
  license: "Apache-2.0",
584
584
  type: "module",
@@ -1153,6 +1153,17 @@ var V2_PATH_TEMPLATES = [
1153
1153
  "/workflows/{workflowId}"
1154
1154
  ];
1155
1155
  var V2_MANIFEST_OMITTED = ["/.well-known/openwop", "/openapi.json"];
1156
+ var V2_RENAMED = { "/v1/openapi.json": "/openapi.json" };
1157
+ var V2_SEAMS_PROFILE = "openwop-conformance-seams-v2";
1158
+ var V2_SEAM_PREFIXES = {
1159
+ "/v1/host/workspace/files": "/conformance/seams/workspace/files"
1160
+ };
1161
+ function v2SeamTwin(path) {
1162
+ for (const [v1, v2] of Object.entries(V2_SEAM_PREFIXES)) {
1163
+ if (path === v1 || path.startsWith(`${v1}/`) || path.startsWith(`${v1}?`)) return `${v2}${path.slice(v1.length)}`;
1164
+ }
1165
+ return null;
1166
+ }
1156
1167
  var V2_MATCHERS = V2_PATH_TEMPLATES.map(
1157
1168
  (t) => new RegExp(`^${t.replace(/[.:]/g, "\\$&").replace(/\{[A-Za-z]+\}/g, "[^/]+")}$`)
1158
1169
  );
@@ -1161,6 +1172,7 @@ function v2Twin(path) {
1161
1172
  const bare = q === -1 ? path : path.slice(0, q);
1162
1173
  const query = q === -1 ? "" : path.slice(q);
1163
1174
  if (!bare.startsWith("/v1/")) return null;
1175
+ if (V2_RENAMED[bare] !== void 0) return `${V2_RENAMED[bare]}${query}`;
1164
1176
  const unversioned = bare.slice("/v1".length);
1165
1177
  return V2_MATCHERS.some((m) => m.test(unversioned)) ? `${unversioned}${query}` : null;
1166
1178
  }
@@ -1204,6 +1216,7 @@ async function negotiateMajor(ctx) {
1204
1216
  }
1205
1217
  let major = 1;
1206
1218
  let roots = {};
1219
+ let seams = false;
1207
1220
  try {
1208
1221
  const url = new URL(".well-known/openwop", ctx.baseUrl.endsWith("/") ? ctx.baseUrl : `${ctx.baseUrl}/`);
1209
1222
  const res = await ctx.fetchImpl(url, { method: "GET", headers: { accept: "application/json", "openwop-version": "2" } });
@@ -1212,7 +1225,10 @@ async function negotiateMajor(ctx) {
1212
1225
  ctx.discovery = { doc, servedVersion: res.headers?.get?.("openwop-version") ?? void 0 };
1213
1226
  ctx.discoveredStreamBase = streamBaseFrom(doc?.extensions, ctx.baseUrl);
1214
1227
  major = selectMajor(doc?.protocolVersions);
1215
- if (major === 2) roots = hostRootsFrom(doc?.extensions);
1228
+ if (major === 2) {
1229
+ roots = hostRootsFrom(doc?.extensions);
1230
+ seams = doc?.conformance?.seamsProfile === V2_SEAMS_PROFILE;
1231
+ }
1216
1232
  } else if (res.status === 406) {
1217
1233
  major = selectMajor(doc?.details?.protocolVersions);
1218
1234
  }
@@ -1220,6 +1236,7 @@ async function negotiateMajor(ctx) {
1220
1236
  }
1221
1237
  ctx.protocolMajor = major;
1222
1238
  ctx.hostRoots = roots;
1239
+ ctx.seamsV2 = seams;
1223
1240
  return major;
1224
1241
  }
1225
1242
  async function resolveRequest(ctx, path, headers) {
@@ -1227,6 +1244,8 @@ async function resolveRequest(ctx, path, headers) {
1227
1244
  if (major !== 2) return { path, headers };
1228
1245
  const twin = v2Twin(path);
1229
1246
  if (twin !== null) return { path: projectRunIdsInPath(twin), headers: { ...headers, "openwop-version": "2.0" } };
1247
+ const seam = ctx.seamsV2 ? v2SeamTwin(path) : null;
1248
+ if (seam !== null) return { path: seam, headers: { ...headers, "openwop-version": "2.0" } };
1230
1249
  const rooted = hostRootFor(path, ctx.hostRoots ?? {});
1231
1250
  if (rooted !== null) return { path: rooted, headers };
1232
1251
  return { path, headers };
@@ -4389,6 +4408,10 @@ registry (\`openwop packs\` operates packs.openwop.dev):
4389
4408
  get GET /v1/packs/{packName} \u2014 one pack's node types (reverse-DNS name)
4390
4409
  export GET /v1/packs/export \u2014 the installed agent manifests (RFC 0003
4391
4410
  round-trip), re-installable elsewhere; --out writes them to a file.
4411
+ These four are v1 operations: protocol v2 names no installed-packs operation
4412
+ (spec/v2/path-manifest.json, api/v2/openapi.yaml), so they are sent as the v1
4413
+ paths whichever major the host speaks. To read the published catalog, use
4414
+ \`openwop packs\`, which resolves the registry's v2 tree.
4392
4415
 
4393
4416
  \`tools\` reads the portable tool catalog (RFC 0078 \xA7B) \u2014 GET /v1/tools, or
4394
4417
  GET /v1/tools/<toolId> for one descriptor. This is the tools an agent/workflow
@@ -11851,8 +11874,12 @@ ${routesHelp("workspace", WORKSPACE_ROUTES)}
11851
11874
 
11852
11875
  Agent workspace (RFC 0059 \xA7C). A tenant-scoped file area an agent reads and
11853
11876
  writes during a run, with optimistic concurrency via ETag / If-Match (WCT-1
11854
- cross-owner isolation is enforced host-side). Drives the real CRUD surface
11855
- GET/PUT/DELETE /v1/host/workspace/files. (The /v1/host/openwop-app/workspace/op
11877
+ cross-owner isolation is enforced host-side). Drives GET/PUT/DELETE
11878
+ /v1/host/workspace/files. v2 names no canonical workspace operation: its only
11879
+ v2 home is the conformance seams profile, so on a host that speaks major 2 AND
11880
+ advertises conformance.seamsProfile "openwop-conformance-seams-v2" the same
11881
+ calls go to /conformance/seams/workspace/files (OpenWOP-Version: 2.0);
11882
+ otherwise the v1 path is used. (The /v1/host/openwop-app/workspace/op
11856
11883
  cross-owner seam is a conformance-only test seam \u2014 reachable here as
11857
11884
  \`workspace op\`, and only on a host started with OPENWOP_TEST_SEAM_ENABLED=true.)
11858
11885
 
@@ -14972,30 +14999,49 @@ import { existsSync as existsSync6, readFileSync as readFileSync15, writeFileSyn
14972
14999
  import { join as join6, resolve as resolvePath8 } from "node:path";
14973
15000
  import { homedir as homedir3 } from "node:os";
14974
15001
  var PACKS_HELP = `Usage:
14975
- openwop packs search [query] [--registry-url url] [--limit n] [--json]
14976
- openwop packs info <name> [--version v] [--registry-url url] [--json]
14977
- openwop packs install <name>[@version] [--version v] [--dir path] [--no-verify] [--registry-url url] [--json]
14978
- openwop packs publish <dir> [--key ed25519.pem] [--key-id id] [--out dir] [--json]
14979
- openwop packs yank <name>[@version] [--version v] [--undo] [--json]
15002
+ openwop packs search [query] [--registry-url url] [--tree v1|v2] [--limit n] [--json]
15003
+ openwop packs info <name> [--version v] [--registry-url url] [--tree v1|v2] [--json]
15004
+ openwop packs install <name>[@version|@range] [--version v] [--dir path] [--no-verify] [--registry-url url] [--tree v1|v2] [--json]
15005
+ openwop packs publish <dir> [--key ed25519.pem] [--key-id id] [--out dir] [--tree v1|v2] [--json]
15006
+ openwop packs yank <name>[@version] [--version v] [--undo] [--tree v1|v2] [--json]
14980
15007
 
14981
15008
  Operates the signed node-pack registry (default: https://packs.openwop.dev,
14982
15009
  override with --registry-url or OPENWOP_REGISTRY_URL). The registry is a
14983
15010
  separate surface from the host --base-url.
14984
15011
 
14985
- search Reads /v1/index.json and filters the catalog client-side.
14986
- info Reads /v1/packs/{name}/index.json (+ the version manifest with --version).
14987
- install Downloads /v1/packs/{name}/-/{version}.tgz, checks sha256 integrity
14988
- against the manifest, and verifies the detached Ed25519 .sig against
14989
- the publisher key at /keys/{keyId}.pub (skip with --no-verify). Places
14990
- the tarball + manifest under ~/.openwop/packs/{name}/{version}/
15012
+ Every registry path is resolved through the registry's
15013
+ /.well-known/openwop-registry.json \`endpoints\` map (spec/v2/core/packs.md
15014
+ \xA7"The registry tree"), preferring the v2 tree (\`endpoints.v2\`). The v1 tree
15015
+ is used only when the registry names no v2 tree (or publishes no discovery
15016
+ document), or when you pass --tree v1.
15017
+
15018
+ search Reads the registry index (endpoints.registryIndex, /v2/index.json on
15019
+ packs.openwop.dev) and filters the catalog client-side.
15020
+ info Reads the pack index (endpoints.packMetadata) + the version manifest
15021
+ with --version.
15022
+ install Resolves the version, downloads the tarball, checks its sha256
15023
+ integrity against the manifest, and verifies the signature (skip
15024
+ with --no-verify). v2: the detached Ed25519 .sig over the in-tarball
15025
+ pack.json (RFC 8785 canonical JSON), under the registry key named by
15026
+ signing.keyId, whose permittedNamespaces must cover the pack name.
15027
+ Places the tarball + manifest under ~/.openwop/packs/{name}/{version}/
14991
15028
  (override with --dir).
15029
+ Version: none = the pack's latest unyanked version; a range (^1.2,
15030
+ ~1.2.0, 1.x, >=1.0.0 <2.0.0) = the highest matching unyanked version;
15031
+ an exact version is a pin and MAY install a yanked version (with a
15032
+ warning). A deprecated version installs with a warning.
14992
15033
  publish The reference registry has NO write API (publish is PR-based). This
14993
15034
  packages + Ed25519-signs a local pack dir into a signed tarball +
14994
- sidecars (mirrors scripts/build-pack-tarball.mjs --signed), ready to
14995
- commit + open a PR. Private key: --key, else ~/.openwop-keys/{keyId}.private.pem.
15035
+ sidecars, ready to commit + open a PR against openwop-registry. The
15036
+ default (v2) signing block is { keyId, scheme: "ed25519-canonical-json" };
15037
+ --tree v1 writes the legacy v1 block. Private key: --key, else
15038
+ ~/.openwop-keys/{keyId}.private.pem.
14996
15039
  yank Edits a local registry checkout \u2014 flips "yanked": true in the version
14997
- manifest (--undo reverses). The published change lands via PR + a
15040
+ manifest under registry/v2/ (--tree v1 for the frozen v1 tree;
15041
+ --undo reverses). The published change lands via PR + a
14998
15042
  build-index.mjs rerun. Run from inside the repo.
15043
+
15044
+ Exit codes: 0 ok \xB7 1 verification / integrity / resolution failure \xB7 2 usage.
14999
15045
  `;
15000
15046
  async function runPacks(ctx, argv) {
15001
15047
  const sub = argv[0];
@@ -15023,6 +15069,71 @@ Run \`openwop packs --help\` for usage.`);
15023
15069
  function registryUrlFor(options, env) {
15024
15070
  return normalizeBaseUrl(options.registryUrl ?? env.OPENWOP_REGISTRY_URL ?? DEFAULT_REGISTRY_URL);
15025
15071
  }
15072
+ var REQUIRED_ENDPOINTS = ["registryIndex", "packMetadata", "versionManifest", "versionTarball", "versionSignature"];
15073
+ var LEGACY_V1_ENDPOINTS = {
15074
+ registryIndex: "/v1/index.json",
15075
+ packMetadata: "/v1/packs/{name}/index.json",
15076
+ versionManifest: "/v1/packs/{name}/-/{version}.json",
15077
+ versionTarball: "/v1/packs/{name}/-/{version}.tgz",
15078
+ versionSignature: "/v1/packs/{name}/-/{version}.sig",
15079
+ publicKey: "/keys/{keyId}.pub"
15080
+ };
15081
+ function treeOption(options) {
15082
+ if (options.tree === void 0) return void 0;
15083
+ if (options.tree !== "v1" && options.tree !== "v2") throw new CliError(`--tree must be v1 or v2 (got "${options.tree}").`);
15084
+ return options.tree;
15085
+ }
15086
+ function stringTemplates(value) {
15087
+ const out = {};
15088
+ if (!value || typeof value !== "object" || Array.isArray(value)) return out;
15089
+ for (const [k, v] of Object.entries(value)) if (typeof v === "string" && !k.startsWith("_")) out[k] = v;
15090
+ return out;
15091
+ }
15092
+ async function resolveRegistry(ctx, options) {
15093
+ const url = registryUrlFor(options, ctx.env);
15094
+ const forced = treeOption(options);
15095
+ let doc = null;
15096
+ try {
15097
+ doc = await registryJson(ctx, url, "/.well-known/openwop-registry.json");
15098
+ } catch (err) {
15099
+ if (!(err instanceof HttpError) || err.status !== 404) throw err;
15100
+ }
15101
+ const endpoints = doc && typeof doc.endpoints === "object" ? doc.endpoints : null;
15102
+ if (!endpoints) {
15103
+ if (forced === "v2") throw new CliError(`${url} publishes no /.well-known/openwop-registry.json endpoints map, so no v2 tree can be resolved.`, 1);
15104
+ return { url, tree: "v1", endpoints: { ...LEGACY_V1_ENDPOINTS }, signingKeys: null, source: "legacy-v1" };
15105
+ }
15106
+ const flat = stringTemplates(endpoints);
15107
+ const publicKey = flat.publicKey ?? LEGACY_V1_ENDPOINTS.publicKey;
15108
+ const v2 = stringTemplates(endpoints.v2);
15109
+ const hasV2 = REQUIRED_ENDPOINTS.every((k) => typeof v2[k] === "string");
15110
+ let tree;
15111
+ let map;
15112
+ let source;
15113
+ if (forced === "v2" || forced === void 0 && hasV2) {
15114
+ if (!hasV2) throw new CliError(`${url} names no v2 tree in its discovery document (endpoints.v2); retry with --tree v1.`, 1);
15115
+ tree = "v2";
15116
+ map = v2;
15117
+ source = "endpoints.v2";
15118
+ } else {
15119
+ const v1 = stringTemplates(endpoints.v1);
15120
+ tree = "v1";
15121
+ map = { ...flat, ...v1 };
15122
+ source = "endpoints.v1";
15123
+ }
15124
+ for (const k of REQUIRED_ENDPOINTS) {
15125
+ if (typeof map[k] !== "string") throw new CliError(`${url}'s discovery document names no ${tree} "${k}" endpoint.`, 1);
15126
+ }
15127
+ return { url, tree, endpoints: { ...map, publicKey }, signingKeys: Array.isArray(doc.signingKeys) ? doc.signingKeys : null, source };
15128
+ }
15129
+ function registryPath(view, key, params = {}) {
15130
+ const template = view.endpoints[key];
15131
+ if (typeof template !== "string") throw new CliError(`${view.url}'s discovery document names no "${key}" endpoint.`, 1);
15132
+ return template.replace(/\{([A-Za-z]+)\}/g, (_m, p) => {
15133
+ if (params[p] === void 0) throw new CliError(`Registry endpoint ${key} needs {${p}}.`, 1);
15134
+ return encodeURIComponent(params[p]);
15135
+ });
15136
+ }
15026
15137
  async function registryJson(ctx, registryUrl, path) {
15027
15138
  const url = new URL(path, registryUrl + "/");
15028
15139
  const res = await ctx.fetchImpl(url, { method: "GET", headers: { accept: "application/json" } });
@@ -15043,15 +15154,15 @@ async function registryBytes(ctx, registryUrl, path) {
15043
15154
  async function runPacksSearch(ctx, argv) {
15044
15155
  const { options, positionals } = parseOptions(argv, {
15045
15156
  bool: ["--help"],
15046
- value: ["--registry-url", "--limit"]
15157
+ value: ["--registry-url", "--limit", "--tree"]
15047
15158
  });
15048
15159
  if (options.help) {
15049
15160
  write(ctx.io.stdout, PACKS_HELP);
15050
15161
  return 0;
15051
15162
  }
15052
15163
  const query = String(positionals[0] ?? "").toLowerCase();
15053
- const registryUrl = registryUrlFor(options, ctx.env);
15054
- const index = await registryJson(ctx, registryUrl, "/v1/index.json");
15164
+ const registry = await resolveRegistry(ctx, options);
15165
+ const index = await registryJson(ctx, registry.url, registryPath(registry, "registryIndex"));
15055
15166
  const packs = Array.isArray(index?.packs) ? index.packs : [];
15056
15167
  const matched = packs.filter((p) => {
15057
15168
  if (!query) return true;
@@ -15060,7 +15171,7 @@ async function runPacksSearch(ctx, argv) {
15060
15171
  });
15061
15172
  const limit = Number(options.limit ?? 30);
15062
15173
  if (ctx.json) {
15063
- writeJson(ctx.io.stdout, { query: positionals[0] ?? "", total: matched.length, packs: matched });
15174
+ writeJson(ctx.io.stdout, { query: positionals[0] ?? "", tree: registry.tree, total: matched.length, packs: matched });
15064
15175
  return 0;
15065
15176
  }
15066
15177
  if (matched.length === 0) {
@@ -15083,7 +15194,7 @@ async function runPacksSearch(ctx, argv) {
15083
15194
  async function runPacksInfo(ctx, argv) {
15084
15195
  const { options, positionals } = parseOptions(argv, {
15085
15196
  bool: ["--help"],
15086
- value: ["--registry-url", "--version"]
15197
+ value: ["--registry-url", "--version", "--tree"]
15087
15198
  });
15088
15199
  if (options.help) {
15089
15200
  write(ctx.io.stdout, PACKS_HELP);
@@ -15091,15 +15202,11 @@ async function runPacksInfo(ctx, argv) {
15091
15202
  }
15092
15203
  const name = positionals[0];
15093
15204
  if (!name) throw new CliError("packs info requires a pack name.\nUsage: openwop packs info <name> [--version v]");
15094
- const registryUrl = registryUrlFor(options, ctx.env);
15095
- const pack = await registryJson(ctx, registryUrl, `/v1/packs/${encodeURIComponent(name)}/index.json`);
15205
+ const registry = await resolveRegistry(ctx, options);
15206
+ const pack = await packMetadata(ctx, registry, name);
15096
15207
  let versionManifest = null;
15097
15208
  if (options.version) {
15098
- versionManifest = await registryJson(
15099
- ctx,
15100
- registryUrl,
15101
- `/v1/packs/${encodeURIComponent(name)}/-/${encodeURIComponent(options.version)}.json`
15102
- );
15209
+ versionManifest = await registryJson(ctx, registry.url, registryPath(registry, "versionManifest", { name, version: options.version }));
15103
15210
  }
15104
15211
  if (ctx.json) {
15105
15212
  writeJson(ctx.io.stdout, versionManifest ? { ...pack, requestedVersion: versionManifest } : pack);
@@ -15111,7 +15218,8 @@ async function runPacksInfo(ctx, argv) {
15111
15218
  `Latest: ${pack.latest ?? "(none)"}`,
15112
15219
  `License: ${pack.license || "\u2014"}`,
15113
15220
  `Author: ${pack.author || "\u2014"}`,
15114
- `Description: ${pack.description || "\u2014"}`
15221
+ `Description: ${pack.description || "\u2014"}`,
15222
+ `Tree: ${registry.tree}`
15115
15223
  ];
15116
15224
  if (pack.homepage) lines.push(`Homepage: ${pack.homepage}`);
15117
15225
  lines.push("");
@@ -15121,7 +15229,7 @@ async function runPacksInfo(ctx, argv) {
15121
15229
  const rows = versions.map((v) => ({
15122
15230
  version: v.version,
15123
15231
  keyId: v.signingKeyId ?? "",
15124
- flags: v.yanked ? "yanked" : v.deprecated ? "deprecated" : "",
15232
+ flags: versionFlags(v),
15125
15233
  integrity: typeof v.integrity === "string" ? v.integrity.slice(0, 24) + "\u2026" : ""
15126
15234
  }));
15127
15235
  writeLine(ctx.io.stdout, formatTable(rows, ["version", "keyId", "flags", "integrity"]));
@@ -15130,38 +15238,52 @@ async function runPacksInfo(ctx, argv) {
15130
15238
  }
15131
15239
  return 0;
15132
15240
  }
15241
+ async function packMetadata(ctx, registry, name) {
15242
+ try {
15243
+ return await registryJson(ctx, registry.url, registryPath(registry, "packMetadata", { name }));
15244
+ } catch (err) {
15245
+ if (err instanceof HttpError && err.status === 404) throw new CliError(`No pack named ${name} on ${registry.url} (${registry.tree} tree). Try \`openwop packs search\`.`, 2);
15246
+ throw err;
15247
+ }
15248
+ }
15249
+ function versionFlags(v) {
15250
+ return v?.yanked ? "yanked" : v?.versionDeprecated || v?.deprecated ? "deprecated" : "";
15251
+ }
15133
15252
  async function runPacksInstall(ctx, argv) {
15134
15253
  const { options, positionals } = parseOptions(argv, {
15135
15254
  bool: ["--help", "--no-verify"],
15136
- value: ["--registry-url", "--version", "--dir"]
15255
+ value: ["--registry-url", "--version", "--dir", "--tree"]
15137
15256
  });
15138
15257
  if (options.help) {
15139
15258
  write(ctx.io.stdout, PACKS_HELP);
15140
15259
  return 0;
15141
15260
  }
15142
15261
  let name = positionals[0];
15143
- let version = options.version;
15262
+ let requested = options.version;
15144
15263
  if (name && name.includes("@")) {
15145
15264
  const at = name.lastIndexOf("@");
15146
- version = version ?? name.slice(at + 1);
15265
+ requested = requested ?? name.slice(at + 1);
15147
15266
  name = name.slice(0, at);
15148
15267
  }
15149
15268
  if (!name) throw new CliError("packs install requires a pack name.\nUsage: openwop packs install <name>[@version] [--version v]");
15150
- const registryUrl = registryUrlFor(options, ctx.env);
15151
- if (!version) {
15152
- const pack = await registryJson(ctx, registryUrl, `/v1/packs/${encodeURIComponent(name)}/index.json`);
15153
- version = pack?.latest;
15154
- if (!version) throw new CliError(`Could not resolve a version for ${name}; pass --version.`);
15269
+ const registry = await resolveRegistry(ctx, options);
15270
+ const warnings = [];
15271
+ let version;
15272
+ if (requested && isExactVersion(requested)) {
15273
+ version = requested;
15274
+ } else {
15275
+ const pack = await packMetadata(ctx, registry, name);
15276
+ version = resolveVersion(name, pack, requested);
15155
15277
  }
15156
- const manifest = await registryJson(
15157
- ctx,
15158
- registryUrl,
15159
- `/v1/packs/${encodeURIComponent(name)}/-/${encodeURIComponent(version)}.json`
15160
- );
15278
+ const manifest = await registryJson(ctx, registry.url, registryPath(registry, "versionManifest", { name, version }));
15161
15279
  if (manifest?.yanked) {
15162
- throw new CliError(`${name}@${version} has been yanked and cannot be installed.`, 1);
15280
+ warnings.push(`${name}@${version} is yanked${manifest.yankedReason ? ` (${manifest.yankedReason})` : ""}; installing it because it was pinned exactly.`);
15163
15281
  }
15164
- const tgz = await registryBytes(ctx, registryUrl, `/v1/packs/${encodeURIComponent(name)}/-/${encodeURIComponent(version)}.tgz`);
15282
+ if (manifest?.versionDeprecated || manifest?.deprecated) {
15283
+ const reason = manifest.deprecationReason ?? (typeof manifest.deprecated === "string" ? manifest.deprecated : "");
15284
+ warnings.push(`${name}@${version} is deprecated${reason ? ` (${reason})` : ""}${manifest.supersededBy ? `; superseded by ${manifest.supersededBy}` : ""}.`);
15285
+ }
15286
+ const tgz = await registryBytes(ctx, registry.url, registryPath(registry, "versionTarball", { name, version }));
15165
15287
  const integrity = "sha256-" + createHash("sha256").update(tgz).digest("base64");
15166
15288
  if (manifest?.integrity && manifest.integrity !== integrity) {
15167
15289
  throw new CliError(
@@ -15171,19 +15293,7 @@ async function runPacksInstall(ctx, argv) {
15171
15293
  }
15172
15294
  let verifyResult = "skipped";
15173
15295
  if (!options.noVerify) {
15174
- const keyId = manifest?.signing?.keyId ?? manifest?.signing?.publicKeyRef;
15175
- if (!keyId) throw new CliError(`${name}@${version} manifest has no signing key reference; re-run with --no-verify to bypass.`, 1);
15176
- const sig = await registryBytes(ctx, registryUrl, `/v1/packs/${encodeURIComponent(name)}/-/${encodeURIComponent(version)}.sig`);
15177
- if (sig.length !== 64) throw new CliError(`Signature for ${name}@${version} is ${sig.length} bytes; expected 64 for Ed25519.`, 1);
15178
- const pubPem = (await registryBytes(ctx, registryUrl, `/keys/${encodeURIComponent(keyId)}.pub`)).toString("utf8");
15179
- const publicKey = createPublicKey(pubPem);
15180
- const method = manifest?.signing?.method ?? "ed25519";
15181
- const signedBytes = method === "manual" ? extractPackJsonBytes(tgz) : tgz;
15182
- const valid = ed25519Verify(null, signedBytes, publicKey, sig);
15183
- if (!valid) {
15184
- throw new CliError(`Signature verification FAILED for ${name}@${version} (keyId=${keyId}, method=${method}).`, 1);
15185
- }
15186
- verifyResult = `verified (keyId=${keyId}, method=${method})`;
15296
+ verifyResult = registry.tree === "v2" ? await verifyV2(ctx, registry, name, version, manifest, tgz) : await verifyV1(ctx, registry, name, version, manifest, tgz);
15187
15297
  }
15188
15298
  const baseDir = options.dir ? resolvePath8(ctx.cwd, options.dir) : join6(configHomeDir(ctx.env), "packs");
15189
15299
  const destDir = join6(baseDir, name, version);
@@ -15192,27 +15302,182 @@ async function runPacksInstall(ctx, argv) {
15192
15302
  const manifestPath = join6(destDir, `${version}.json`);
15193
15303
  writeFileSync10(tgzPath, tgz);
15194
15304
  writeFileSync10(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
15305
+ for (const w of warnings) writeLine(ctx.io.stderr, `warning: ${w}`);
15195
15306
  if (ctx.json) {
15196
15307
  writeJson(ctx.io.stdout, {
15197
15308
  name,
15198
15309
  version,
15310
+ tree: registry.tree,
15199
15311
  integrity,
15200
15312
  signature: verifyResult,
15313
+ yanked: Boolean(manifest?.yanked),
15314
+ deprecated: Boolean(manifest?.versionDeprecated || manifest?.deprecated),
15201
15315
  tarball: tgzPath,
15202
15316
  manifest: manifestPath
15203
15317
  });
15204
15318
  return 0;
15205
15319
  }
15206
- writeLine(ctx.io.stdout, `Installed ${name}@${version}`);
15320
+ writeLine(ctx.io.stdout, `Installed ${name}@${version} (${registry.tree} tree)`);
15207
15321
  writeLine(ctx.io.stdout, ` signature: ${verifyResult}`);
15208
15322
  writeLine(ctx.io.stdout, ` integrity: ${integrity}`);
15209
15323
  writeLine(ctx.io.stdout, ` tarball: ${tgzPath}`);
15210
15324
  return 0;
15211
15325
  }
15326
+ async function verifyV1(ctx, registry, name, version, manifest, tgz) {
15327
+ const keyId = manifest?.signing?.keyId ?? manifest?.signing?.publicKeyRef;
15328
+ if (!keyId) throw new CliError(`${name}@${version} manifest has no signing key reference; re-run with --no-verify to bypass.`, 1);
15329
+ const sig = await registryBytes(ctx, registry.url, registryPath(registry, "versionSignature", { name, version }));
15330
+ if (sig.length !== 64) throw new CliError(`Signature for ${name}@${version} is ${sig.length} bytes; expected 64 for Ed25519.`, 1);
15331
+ const pubPem = (await registryBytes(ctx, registry.url, registryPath(registry, "publicKey", { keyId }))).toString("utf8");
15332
+ const method = manifest?.signing?.method ?? "ed25519";
15333
+ const signedBytes = method === "manual" ? extractPackJsonBytes(tgz) : tgz;
15334
+ if (!ed25519Verify(null, signedBytes, createPublicKey(pubPem), sig)) {
15335
+ throw new CliError(`Signature verification FAILED for ${name}@${version} (keyId=${keyId}, method=${method}).`, 1);
15336
+ }
15337
+ return `verified (keyId=${keyId}, method=${method})`;
15338
+ }
15339
+ var V2_SCHEME = "ed25519-canonical-json";
15340
+ async function verifyV2(ctx, registry, name, version, manifest, tgz) {
15341
+ const fail3 = (why) => new CliError(`Signature verification FAILED for ${name}@${version} (pack_signature_invalid): ${why}`, 1);
15342
+ const signing = manifest?.signing;
15343
+ if (!signing || typeof signing !== "object") throw fail3("the version manifest has no signing block; re-run with --no-verify to bypass.");
15344
+ const legacy = ["method", "publicKeyRef", "signatureRef"].filter((k) => k in signing);
15345
+ if (legacy.length > 0) throw fail3(`a v2 signing block carries ${legacy.join(", ")} (v1 fields; a v2 block is { keyId, scheme }).`);
15346
+ if (signing.scheme !== V2_SCHEME) throw fail3(`signing.scheme is ${JSON.stringify(signing.scheme)}; the only v2 scheme is ${V2_SCHEME}.`);
15347
+ const keyId = signing.keyId;
15348
+ if (typeof keyId !== "string" || keyId === "") throw fail3("signing.keyId is missing.");
15349
+ if (!registry.signingKeys) throw fail3(`${registry.url} publishes no signingKeys[], so the namespace check cannot run.`);
15350
+ const key = registry.signingKeys.find((k) => k && k.keyId === keyId);
15351
+ if (!key) throw fail3(`keyId ${keyId} is not listed in the registry's signingKeys[].`);
15352
+ const permitted = Array.isArray(key.permittedNamespaces) ? key.permittedNamespaces : [];
15353
+ if (!permitted.some((pattern) => namespaceCovers(pattern, name))) {
15354
+ throw fail3(`key ${keyId} is not permitted to sign ${name} (permittedNamespaces: ${permitted.join(", ") || "none"}).`);
15355
+ }
15356
+ const packJson = extractPackJsonBytes(tgz);
15357
+ let embedded;
15358
+ try {
15359
+ embedded = JSON.parse(packJson.toString("utf8"));
15360
+ } catch {
15361
+ throw fail3("the in-tarball pack.json is not JSON.");
15362
+ }
15363
+ if (embedded?.name !== name || embedded?.version !== version) {
15364
+ throw fail3(`the in-tarball pack.json is ${embedded?.name}@${embedded?.version}, not ${name}@${version}.`);
15365
+ }
15366
+ if (embedded?.signing?.keyId !== void 0 && embedded.signing.keyId !== keyId) {
15367
+ throw fail3(`the in-tarball pack.json names signer ${embedded.signing.keyId}, the version manifest ${keyId}.`);
15368
+ }
15369
+ if (canonicalJsonStringify(embedded) !== packJson.toString("utf8")) {
15370
+ throw fail3("the in-tarball pack.json is not in canonical (RFC 8785) form, so its bytes are not what ed25519-canonical-json signs.");
15371
+ }
15372
+ const sig = await registryBytes(ctx, registry.url, registryPath(registry, "versionSignature", { name, version }));
15373
+ if (sig.length !== 64) throw fail3(`the signature is ${sig.length} bytes; expected 64 for Ed25519.`);
15374
+ const keyPath = typeof key.publicKeyUrl === "string" && key.publicKeyUrl !== "" ? key.publicKeyUrl : registryPath(registry, "publicKey", { keyId });
15375
+ const pubPem = (await registryBytes(ctx, registry.url, keyPath)).toString("utf8");
15376
+ if (!ed25519Verify(null, packJson, createPublicKey(pubPem), sig)) throw fail3(`the signature does not verify under ${keyId}.`);
15377
+ const status = typeof key.status === "string" ? key.status : "active";
15378
+ return `verified (keyId=${keyId}, scheme=${V2_SCHEME}${status !== "active" ? `, key status ${status}` : ""})`;
15379
+ }
15380
+ function namespaceCovers(pattern, name) {
15381
+ if (typeof pattern !== "string") return false;
15382
+ if (pattern.endsWith(".*")) return name.startsWith(pattern.slice(0, -1));
15383
+ return name === pattern;
15384
+ }
15385
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
15386
+ function isExactVersion(v) {
15387
+ return SEMVER_RE.test(v.trim());
15388
+ }
15389
+ function parseSemver(v) {
15390
+ const m = SEMVER_RE.exec(String(v).trim());
15391
+ if (!m) return null;
15392
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: m[4] ? m[4].split(".") : [] };
15393
+ }
15394
+ function compareSemver(a, b) {
15395
+ const x = parseSemver(a);
15396
+ const y = parseSemver(b);
15397
+ if (!x || !y) return String(a).localeCompare(String(b));
15398
+ for (const k of ["major", "minor", "patch"]) if (x[k] !== y[k]) return x[k] - y[k];
15399
+ if (x.pre.length === 0 || y.pre.length === 0) return y.pre.length - x.pre.length;
15400
+ for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
15401
+ const p = x.pre[i];
15402
+ const q = y.pre[i];
15403
+ if (p === void 0) return -1;
15404
+ if (q === void 0) return 1;
15405
+ const pn = /^\d+$/.test(p);
15406
+ const qn = /^\d+$/.test(q);
15407
+ if (pn && qn && Number(p) !== Number(q)) return Number(p) - Number(q);
15408
+ if (pn !== qn) return pn ? -1 : 1;
15409
+ if (p !== q) return p < q ? -1 : 1;
15410
+ }
15411
+ return 0;
15412
+ }
15413
+ function comparator(raw) {
15414
+ const m = /^(\^|~|>=|<=|>|<|=)?\s*v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(-[0-9A-Za-z.-]+)?$/.exec(raw);
15415
+ if (!m) throw new CliError(`Unsupported version range "${raw}". Use an exact version, ^x.y.z, ~x.y.z, x.y.x, or comparators like ">=1.0.0 <2.0.0".`);
15416
+ const op = m[1] ?? "";
15417
+ const wild = (p) => p === void 0 || /^[xX*]$/.test(p);
15418
+ const [maj, min, pat] = [m[2], m[3], m[4]];
15419
+ const n = (p) => wild(p) ? 0 : Number(p);
15420
+ const floor = `${n(maj)}.${n(min)}.${n(pat)}${m[5] ?? ""}`;
15421
+ const cmp = (v) => compareSemver(v, floor);
15422
+ let ceil = null;
15423
+ if (op === "^") {
15424
+ ceil = n(maj) > 0 || wild(min) ? `${n(maj) + 1}.0.0` : n(min) > 0 || wild(pat) ? `0.${n(min) + 1}.0` : `0.0.${n(pat) + 1}`;
15425
+ } else if (op === "~") {
15426
+ ceil = wild(min) ? `${n(maj) + 1}.0.0` : `${n(maj)}.${n(min) + 1}.0`;
15427
+ } else if (op === "" || op === "=") {
15428
+ if (wild(maj)) return () => true;
15429
+ if (wild(min)) ceil = `${n(maj) + 1}.0.0`;
15430
+ else if (wild(pat)) ceil = `${n(maj)}.${n(min) + 1}.0`;
15431
+ else return (v) => cmp(v) === 0;
15432
+ }
15433
+ if (ceil !== null) {
15434
+ const c = ceil;
15435
+ return (v) => cmp(v) >= 0 && compareSemver(v, `${c}-0`) < 0;
15436
+ }
15437
+ switch (op) {
15438
+ case ">=":
15439
+ return (v) => cmp(v) >= 0;
15440
+ case ">":
15441
+ return (v) => cmp(v) > 0;
15442
+ case "<=":
15443
+ return (v) => cmp(v) <= 0;
15444
+ default:
15445
+ return (v) => cmp(v) < 0;
15446
+ }
15447
+ }
15448
+ function satisfiesRange(version, range) {
15449
+ const parsed = parseSemver(version);
15450
+ if (!parsed) return false;
15451
+ return range.split("||").some((set) => {
15452
+ const parts = set.trim().replace(/(>=|<=|>|<|=|\^|~)\s+/g, "$1").split(/\s+/).filter(Boolean);
15453
+ if (parts.length === 0) return true;
15454
+ if (parsed.pre.length > 0 && !parts.some((p) => p.includes("-"))) return false;
15455
+ return parts.every((p) => comparator(p)(version));
15456
+ });
15457
+ }
15458
+ function resolveVersion(name, pack, range) {
15459
+ const versions = Array.isArray(pack?.versions) ? pack.versions : [];
15460
+ const unyanked = versions.filter((v) => v && typeof v.version === "string" && !v.yanked).map((v) => v.version);
15461
+ if (!range || range === "latest") {
15462
+ const latest = typeof pack?.latest === "string" ? pack.latest : void 0;
15463
+ const latestEntry = versions.find((v) => v?.version === latest);
15464
+ if (latest && versions.length === 0) return latest;
15465
+ if (latest && latestEntry && !latestEntry.yanked) return latest;
15466
+ const best = unyanked.filter((v) => (parseSemver(v)?.pre.length ?? 1) === 0).sort(compareSemver).pop() ?? unyanked.sort(compareSemver).pop();
15467
+ if (!best) throw new CliError(`${name} has no installable (unyanked) version; pin an exact version to install a yanked one.`, 1);
15468
+ return best;
15469
+ }
15470
+ const match = unyanked.filter((v) => satisfiesRange(v, range)).sort(compareSemver).pop();
15471
+ if (!match) {
15472
+ const yankedHit = versions.some((v) => v?.yanked && typeof v.version === "string" && satisfiesRange(v.version, range));
15473
+ throw new CliError(`No unyanked version of ${name} satisfies "${range}"${yankedHit ? " (a yanked version does; pin it exactly to install it)" : ""}.`, 1);
15474
+ }
15475
+ return match;
15476
+ }
15212
15477
  async function runPacksPublish(ctx, argv) {
15213
15478
  const { options, positionals } = parseOptions(argv, {
15214
15479
  bool: ["--help"],
15215
- value: ["--key", "--key-id", "--out"]
15480
+ value: ["--key", "--key-id", "--out", "--tree"]
15216
15481
  });
15217
15482
  if (options.help) {
15218
15483
  write(ctx.io.stdout, PACKS_HELP);
@@ -15230,9 +15495,10 @@ async function runPacksPublish(ctx, argv) {
15230
15495
  const version = manifest.version;
15231
15496
  if (!name || !version) throw new CliError('pack.json must declare both "name" and "version".');
15232
15497
  const keyId = options.keyId ?? "openwop-team-1";
15498
+ const tree = treeOption(options) ?? "v2";
15233
15499
  const signedManifest = {
15234
15500
  ...manifest,
15235
- signing: { method: "manual", publicKeyRef: keyId, signatureRef: "keys/pack.json.sig" }
15501
+ signing: tree === "v2" ? { keyId, scheme: V2_SCHEME } : { method: "manual", publicKeyRef: keyId, signatureRef: "keys/pack.json.sig" }
15236
15502
  };
15237
15503
  const canonical = canonicalJsonStringify(signedManifest);
15238
15504
  let privateKey;
@@ -15269,6 +15535,7 @@ async function runPacksPublish(ctx, argv) {
15269
15535
  name,
15270
15536
  version,
15271
15537
  keyId,
15538
+ tree,
15272
15539
  integrity: `sha256:${sha}`,
15273
15540
  tarball: tgzPath,
15274
15541
  signature: sigPath,
@@ -15279,7 +15546,7 @@ async function runPacksPublish(ctx, argv) {
15279
15546
  });
15280
15547
  return 0;
15281
15548
  }
15282
- writeLine(ctx.io.stdout, `Packaged + signed ${name}@${version} (keyId=${keyId})`);
15549
+ writeLine(ctx.io.stdout, `Packaged + signed ${name}@${version} for the ${tree} tree (keyId=${keyId})`);
15283
15550
  writeLine(ctx.io.stdout, ` tarball: ${tgzPath}`);
15284
15551
  writeLine(ctx.io.stdout, ` signature: ${sigPath}`);
15285
15552
  writeLine(ctx.io.stdout, ` manifest: ${manifestOut}`);
@@ -15291,15 +15558,15 @@ async function runPacksPublish(ctx, argv) {
15291
15558
  }
15292
15559
  writeLine(ctx.io.stdout, "");
15293
15560
  writeLine(ctx.io.stdout, "The reference registry has no write API. To publish:");
15294
- writeLine(ctx.io.stdout, ` 1. Copy the artifacts into registry/v1/packs/${name}/-/`);
15295
- writeLine(ctx.io.stdout, " 2. Run `node registry/scripts/build-index.mjs` to refresh the index.");
15561
+ writeLine(ctx.io.stdout, ` 1. Copy the artifacts into registry/${tree}/packs/${name}/-/ of an openwop-registry checkout`);
15562
+ writeLine(ctx.io.stdout, ` 2. Run \`node registry/scripts/build-index.mjs --tree ${tree}\` to refresh the index.`);
15296
15563
  writeLine(ctx.io.stdout, " 3. Open a pull request (publishMethod: github-pull-request).");
15297
15564
  return 0;
15298
15565
  }
15299
15566
  async function runPacksYank(ctx, argv) {
15300
15567
  const { options, positionals } = parseOptions(argv, {
15301
15568
  bool: ["--help", "--undo"],
15302
- value: ["--version"]
15569
+ value: ["--version", "--tree"]
15303
15570
  });
15304
15571
  if (options.help) {
15305
15572
  write(ctx.io.stdout, PACKS_HELP);
@@ -15316,7 +15583,8 @@ async function runPacksYank(ctx, argv) {
15316
15583
  throw new CliError("packs yank requires <name>@<version> (or <name> --version v).");
15317
15584
  }
15318
15585
  const root = requireRepoRoot(ctx);
15319
- const manifestPath = join6(root, "registry", "v1", "packs", name, "-", `${version}.json`);
15586
+ const tree = treeOption(options) ?? "v2";
15587
+ const manifestPath = join6(root, "registry", tree, "packs", name, "-", `${version}.json`);
15320
15588
  if (!existsSync6(manifestPath)) {
15321
15589
  throw new CliError(`Version manifest not found: ${manifestPath}
15322
15590
  (packs yank edits a local registry checkout; the published change lands via PR.)`);
@@ -15330,12 +15598,12 @@ async function runPacksYank(ctx, argv) {
15330
15598
  manifest.yanked = newValue;
15331
15599
  writeFileSync10(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
15332
15600
  if (ctx.json) {
15333
- writeJson(ctx.io.stdout, { name, version, yanked: newValue, manifest: manifestPath, publishMethod: "github-pull-request" });
15601
+ writeJson(ctx.io.stdout, { name, version, tree, yanked: newValue, manifest: manifestPath, publishMethod: "github-pull-request" });
15334
15602
  return 0;
15335
15603
  }
15336
15604
  writeLine(ctx.io.stdout, `${newValue ? "Yanked" : "Un-yanked"} ${name}@${version}`);
15337
15605
  writeLine(ctx.io.stdout, ` edited: ${manifestPath}`);
15338
- writeLine(ctx.io.stdout, " Next: run `node registry/scripts/build-index.mjs`, commit, and open a PR.");
15606
+ writeLine(ctx.io.stdout, ` Next: run \`node registry/scripts/build-index.mjs --tree ${tree}\`, commit, and open a PR.`);
15339
15607
  return 0;
15340
15608
  }
15341
15609
  function canonicalJsonStringify(value) {
@@ -22507,7 +22775,10 @@ pack's main-frame ES module (GET ${BASE18}/trusted/<name>/plugins/<id>/entry.mjs
22507
22775
  \`rpc\` sends one ui-plugin/1 request (POST ${RPC_PRODUCT}, body { message }); the
22508
22776
  message is { openwop: "ui-plugin/1", type: "request", id (integer,
22509
22777
  default 1), method, params } unless you pass a
22510
- whole body with --body/--body-file.
22778
+ whole body with --body/--body-file. --conformance-alias posts to the test-seam
22779
+ alias ${RPC_CONFORMANCE} instead; that seam has no v2 equivalent (the v2
22780
+ seams profile, api/seams-v2.yaml, defines no ui-plugin operation), so it is
22781
+ always sent as the v1 path, on either protocol major.
22511
22782
 
22512
22783
  Exit codes: 0 ok \xB7 2 usage / not found / rejected request \xB7 4 forbidden.
22513
22784
 
@@ -27010,8 +27281,10 @@ var OPENAPI_HELP = `Usage:
27010
27281
  openwop openapi [--out <file>] [--json]
27011
27282
  openwop openapi paths [--filter <text>] [--json]
27012
27283
 
27013
- Fetch the host's OpenAPI document (GET /v1/openapi.json \u2014 the document the
27014
- host serves, which lists the operations it implements).
27284
+ Fetch the host's OpenAPI document \u2014 the document the host serves, which lists
27285
+ the operations it implements. A host that speaks protocol major 2 is asked for
27286
+ GET /openapi.json with OpenWOP-Version: 2.0 (its v2 document); a v1-only host
27287
+ for GET /v1/openapi.json.
27015
27288
 
27016
27289
  (default) Prints the document's title, version and operation count; --json
27017
27290
  prints the whole document; --out <file> saves it.