omni-dsh-plugins 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -4
  2. package/dist/bin.js +268 -113
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,9 +21,10 @@ never ships here.
21
21
 
22
22
  - Node.js 20 or newer.
23
23
  - The official `dsh` executable on `PATH` for add, update and remove operations.
24
- - A public catalog snapshot for discovery. Version `1.0.0` defaults to catalog revision
25
- `f01d1d2d22b80222c121db6dfc0fd4035c24b390` (160 entries); `--revision` overrides it and stays
26
- the trust anchor, because a fetched snapshot whose declared revision differs is rejected.
24
+ - Network access for discovery. With no `--catalog`, the CLI reads the snapshot published at
25
+ <https://dsh-plugins.omniroute.online/catalog.snapshot.json> and accepts the revision that
26
+ snapshot declares, so a plugin merged today is findable today. Passing `--revision` turns the
27
+ fetch into an exact-commit demand and rejects any snapshot declaring a different one.
27
28
 
28
29
  ## Discover and validate
29
30
 
@@ -72,7 +73,7 @@ npx omni-dsh-plugins remove example-plugin \
72
73
  --allow-code-execution
73
74
  ```
74
75
 
75
- Native Windows policy for v1.0.0: code-executing `add`, `update`, and `remove` are disabled
76
+ Native Windows policy for v1.0.1: code-executing `add`, `update`, and `remove` are disabled
76
77
  before any profile, state, cache, or subprocess access because complete descendant containment
77
78
  cannot be proven. Use WSL for mutations. `--dry-run` and the read-only catalog, `search`, `info`,
78
79
  `list`, and `doctor` commands remain available. A native Windows recovery marker is never
@@ -146,3 +147,18 @@ npx omni-dsh-plugins search tui \
146
147
  --catalog https://dsh-plugins.omniroute.online/catalog.snapshot.json \
147
148
  --revision <published-catalog-revision>
148
149
  ```
150
+
151
+ ## Releasing (maintainers)
152
+
153
+ Releases are tag-driven and fail closed. The npm artifact is built from the tagged commit —
154
+ never from a branch tip:
155
+
156
+ 1. Land the release commit (with the final `cli/package.json` version) on `main` through a
157
+ reviewed pull request.
158
+ 2. Tag that commit `cli-v<version>` (the version must equal `cli/package.json` of the tagged
159
+ commit) and push the tag. The push triggers `.github/workflows/release-npm.yml`, which
160
+ verifies the tag/version match, verifies the commit is an ancestor of `main`, runs tests,
161
+ checks the exact tarball manifest, smoke-runs the packed binary, and publishes with npm
162
+ provenance.
163
+ 3. `workflow_dispatch` exists only as a re-run fallback: it refuses to publish unless the
164
+ `cli-v<version>` tag already exists and points at exactly the commit checked out by the run.
package/dist/bin.js CHANGED
@@ -22989,6 +22989,70 @@ var import_semver = __toESM(require_semver2(), 1);
22989
22989
  var import_spdx_expression_parse = __toESM(require_spdx_expression_parse(), 1);
22990
22990
  var import_ssri = __toESM(require_lib(), 1);
22991
22991
  var import_yaml = __toESM(require_dist2(), 1);
22992
+ var MAX_MEDIA_ITEMS = 6;
22993
+ var MAX_MEDIA_ALT_LENGTH = 120;
22994
+ var REPOSITORY_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/u;
22995
+ var RAW_URL = /^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)(\/.*)$/u;
22996
+ var ASSET_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/assets(\/.*)$/u;
22997
+ var MEDIA_PATH = /^(?:\/(?!\.{1,2}(?:\/|$))[A-Za-z0-9._@+-]+)+$/u;
22998
+ function repositorySlug(repository) {
22999
+ const match = REPOSITORY_URL.exec(String(repository ?? ""));
23000
+ return match === null ? null : { owner: match[1], repo: match[2] };
23001
+ }
23002
+ function classifyMediaUrl(url, kind, slug, commit) {
23003
+ const raw = RAW_URL.exec(url);
23004
+ if (raw !== null) {
23005
+ if (!MEDIA_PATH.test(raw[4])) return "unusable";
23006
+ if (commit === "" || raw[3] !== commit) return "not-pinned";
23007
+ return slug !== null && raw[1] === slug.owner && raw[2] === slug.repo ? "ok" : "wrong-repository";
23008
+ }
23009
+ const asset = kind === "video" ? ASSET_URL.exec(url) : null;
23010
+ if (asset !== null) {
23011
+ if (!MEDIA_PATH.test(asset[3])) return "unusable";
23012
+ return slug !== null && asset[1] === slug.owner && asset[2] === slug.repo ? "ok" : "wrong-repository";
23013
+ }
23014
+ return "unusable";
23015
+ }
23016
+ function validateMediaField(media, source) {
23017
+ if (media === void 0) {
23018
+ return [];
23019
+ }
23020
+ if (!Array.isArray(media)) {
23021
+ return ["media must be an array"];
23022
+ }
23023
+ if (media.length === 0) {
23024
+ return ["media must not be an empty list"];
23025
+ }
23026
+ const errors = [];
23027
+ if (media.length > MAX_MEDIA_ITEMS) {
23028
+ errors.push(`media has more than ${MAX_MEDIA_ITEMS} items`);
23029
+ }
23030
+ const slug = repositorySlug(source.repository);
23031
+ const commit = String(source.commit ?? "");
23032
+ media.forEach((item, index) => {
23033
+ const value = typeof item === "object" && item !== null ? item : {};
23034
+ if (value.kind !== "screenshot" && value.kind !== "video") {
23035
+ errors.push(`media[${index}].kind must be "screenshot" or "video"`);
23036
+ }
23037
+ if (typeof value.alt !== "string" || value.alt.trim() === "" || value.alt.length > MAX_MEDIA_ALT_LENGTH) {
23038
+ errors.push(`media[${index}].alt must be 1-${MAX_MEDIA_ALT_LENGTH} characters`);
23039
+ }
23040
+ const url = typeof value.url === "string" ? value.url : "";
23041
+ switch (classifyMediaUrl(url, value.kind, slug, commit)) {
23042
+ case "ok":
23043
+ return;
23044
+ case "not-pinned":
23045
+ errors.push(`media[${index}].url must pin the entry commit, not a branch`);
23046
+ return;
23047
+ case "wrong-repository":
23048
+ errors.push(`media[${index}].url must reference the entry's own repository`);
23049
+ return;
23050
+ default:
23051
+ errors.push(`media[${index}].url must be a GitHub URL pinned to the entry commit`);
23052
+ }
23053
+ });
23054
+ return errors;
23055
+ }
22992
23056
  function parseSpdx(value) {
22993
23057
  try {
22994
23058
  return (0, import_spdx_expression_parse.default)(value);
@@ -23170,6 +23234,9 @@ function semanticIssues(entry) {
23170
23234
  }
23171
23235
  }
23172
23236
  }
23237
+ for (const message of validateMediaField(entry.media, entry.source)) {
23238
+ issues.push({ code: "invalid-media", message });
23239
+ }
23173
23240
  if (entry.verification.smokeTest !== null) {
23174
23241
  try {
23175
23242
  parseExactSemver(entry.verification.smokeTest.check.version);
@@ -23251,6 +23318,9 @@ function isContained2(root, candidate) {
23251
23318
  function hasErrorCode(error, code) {
23252
23319
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
23253
23320
  }
23321
+ function creatorSlug(creatorGithub) {
23322
+ return creatorGithub.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-");
23323
+ }
23254
23324
  function safeEntryLabel(fileName) {
23255
23325
  const safeName = fileName.replaceAll(/[^A-Za-z0-9._-]/gu, "_");
23256
23326
  return `${ENTRY_DIRECTORY}/${safeName || "invalid-entry"}`;
@@ -23347,6 +23417,9 @@ async function loadCatalog(input) {
23347
23417
  entryDirectory = await containedRealPath(root, requestedEntryDirectory) ?? "";
23348
23418
  } catch (error) {
23349
23419
  if (hasErrorCode(error, "ENOENT")) {
23420
+ if (normalized.source.kind === "snapshot") {
23421
+ diagnostics.push(degenerateSnapshotDiagnostic());
23422
+ }
23350
23423
  return snapshotResult(normalized.source, [], diagnostics);
23351
23424
  }
23352
23425
  diagnostics.push({
@@ -23430,6 +23503,28 @@ async function loadCatalog(input) {
23430
23503
  continue;
23431
23504
  }
23432
23505
  const entry = parsed;
23506
+ const baseName = directoryEntry.name.slice(0, -".yaml".length);
23507
+ let violatesIdConvention = false;
23508
+ if (entry.id !== baseName) {
23509
+ violatesIdConvention = true;
23510
+ diagnostics.push({
23511
+ file,
23512
+ code: "id-filename-mismatch",
23513
+ message: "public ID must equal the catalog file's basename"
23514
+ });
23515
+ }
23516
+ const expectedPrefix = `${creatorSlug(entry.creator.github)}-`;
23517
+ if (!entry.id.startsWith(expectedPrefix)) {
23518
+ violatesIdConvention = true;
23519
+ diagnostics.push({
23520
+ file,
23521
+ code: "id-creator-prefix",
23522
+ message: `public ID must start with the creator slug prefix "${expectedPrefix}"`
23523
+ });
23524
+ }
23525
+ if (violatesIdConvention) {
23526
+ continue;
23527
+ }
23433
23528
  try {
23434
23529
  candidates.push({
23435
23530
  canonicalKey: canonicalPluginKey(entry.source.repositoryNodeId, entry.source.subpath),
@@ -23462,8 +23557,18 @@ async function loadCatalog(input) {
23462
23557
  ...duplicateKeys.rejectedFiles
23463
23558
  ]);
23464
23559
  const entries = candidates.filter((candidate) => !rejectedFiles.has(candidate.file)).map((candidate) => candidate.entry).sort((left, right) => compareText2(left.id, right.id));
23560
+ if (normalized.source.kind === "snapshot" && entries.length === 0 && diagnostics.length === 0) {
23561
+ diagnostics.push(degenerateSnapshotDiagnostic());
23562
+ }
23465
23563
  return snapshotResult(normalized.source, entries, diagnostics);
23466
23564
  }
23565
+ function degenerateSnapshotDiagnostic() {
23566
+ return {
23567
+ file: ENTRY_DIRECTORY,
23568
+ code: "degenerate-snapshot",
23569
+ message: "materialized snapshot contains no plugin entries; the published catalog is never empty, so this snapshot is broken or truncated"
23570
+ };
23571
+ }
23467
23572
 
23468
23573
  // src/catalogSnapshot.ts
23469
23574
  import { constants } from "node:fs";
@@ -23671,15 +23776,15 @@ var CATALOG_SNAPSHOT_FORMAT_V1 = "omni-dsh-catalog-snapshot-v1";
23671
23776
  var PUBLIC_CATALOG_RAW_ORIGIN = "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-dsh-plugins";
23672
23777
  var PUBLIC_SNAPSHOT_SITE_URL = "https://dsh-plugins.omniroute.online/catalog.snapshot.json";
23673
23778
  var CATALOG_SNAPSHOT_LIMITS = Object.freeze({
23674
- snapshotBytes: 10 * 1024 * 1024,
23675
- totalFileBytes: 8 * 1024 * 1024,
23779
+ snapshotBytes: 128 * 1024 * 1024,
23780
+ totalFileBytes: 128 * 1024 * 1024,
23676
23781
  fileBytes: 1024 * 1024,
23677
- files: 2048,
23678
- directoryEntries: 4096,
23782
+ files: 32768,
23783
+ directoryEntries: 32768,
23679
23784
  pathBytes: 512,
23680
23785
  redirects: 3,
23681
23786
  gitOutputBytes: 64 * 1024,
23682
- gitTreeBytes: 2 * 1024 * 1024,
23787
+ gitTreeBytes: 32 * 1024 * 1024,
23683
23788
  pathDepth: 16
23684
23789
  });
23685
23790
  var CATALOG_DEADLINE_DEFAULTS = Object.freeze({
@@ -23869,7 +23974,7 @@ async function readLocalSnapshot(path) {
23869
23974
  }
23870
23975
  }
23871
23976
  function validatePublicSnapshotUrl(rawUrl, revision) {
23872
- assertRevision(revision);
23977
+ if (revision !== void 0) assertRevision(revision);
23873
23978
  if (rawUrl.includes("\\") || rawUrl.includes("%")) {
23874
23979
  throw safety("catalog snapshot URL is outside the public allowlist");
23875
23980
  }
@@ -23879,7 +23984,7 @@ function validatePublicSnapshotUrl(rawUrl, revision) {
23879
23984
  } catch {
23880
23985
  throw safety("catalog snapshot URL is invalid");
23881
23986
  }
23882
- const expected = `${PUBLIC_CATALOG_RAW_ORIGIN}/${revision}/catalog.snapshot.json`;
23987
+ const expected = revision === void 0 ? PUBLIC_SNAPSHOT_SITE_URL : `${PUBLIC_CATALOG_RAW_ORIGIN}/${revision}/catalog.snapshot.json`;
23883
23988
  if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.port !== "" || url.search !== "" || url.hash !== "" || url.href !== expected && url.href !== PUBLIC_SNAPSHOT_SITE_URL) {
23884
23989
  throw safety("catalog snapshot URL is outside the public allowlist");
23885
23990
  }
@@ -24395,24 +24500,15 @@ async function materializeCatalog(selection2, dependencies = {}) {
24395
24500
  }
24396
24501
 
24397
24502
  // src/catalogSource.ts
24398
- var DEFAULT_CATALOG_REVISION = "f01d1d2d22b80222c121db6dfc0fd4035c24b390";
24399
- var DEFAULT_EMPTY_SNAPSHOT = {
24400
- source: {
24401
- kind: "snapshot",
24402
- declaredRevision: DEFAULT_CATALOG_REVISION,
24403
- pinStatus: "declared-local"
24404
- },
24405
- entries: [],
24406
- diagnostics: []
24407
- };
24408
24503
  async function loadDefaultCatalog(selection2, dependencies = {}) {
24409
- if (selection2?.root === void 0) {
24410
- if (selection2?.revision !== void 0 && selection2.revision !== DEFAULT_CATALOG_REVISION) {
24411
- throw new CliSafetyError("default catalog revision is invalid");
24412
- }
24413
- return DEFAULT_EMPTY_SNAPSHOT;
24414
- }
24415
- const materialized = await materializeSelection(selection2, dependencies);
24504
+ const materialized = selection2?.root === void 0 ? await materializeCatalog(
24505
+ {
24506
+ kind: "snapshot-url",
24507
+ url: PUBLIC_SNAPSHOT_SITE_URL,
24508
+ ...selection2?.revision === void 0 ? {} : { revision: selection2.revision }
24509
+ },
24510
+ dependencies
24511
+ ) : await materializeSelection(selection2, dependencies);
24416
24512
  try {
24417
24513
  const loaded = materialized.kind === "snapshot" ? await loadCatalog({
24418
24514
  kind: "snapshot",
@@ -24474,8 +24570,8 @@ async function materializeSelection(selection2, dependencies) {
24474
24570
 
24475
24571
  // src/commands/catalog.ts
24476
24572
  var import_yaml3 = __toESM(require_dist2(), 1);
24477
- import { readFile as readFile3, realpath as realpath4 } from "node:fs/promises";
24478
- import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4, sep as sep4 } from "node:path";
24573
+ import { readdir as readdir2, readFile as readFile3, realpath as realpath4 } from "node:fs/promises";
24574
+ import { isAbsolute as isAbsolute4, join as join2, relative as relative4, resolve as resolve4, sep as sep4 } from "node:path";
24479
24575
  function sortedDiagnostics(diagnostics) {
24480
24576
  return [...diagnostics].sort(
24481
24577
  (left, right) => left.file.localeCompare(right.file) || left.code.localeCompare(right.code) || left.message.localeCompare(right.message)
@@ -24569,6 +24665,64 @@ function hasBalancedMarkdownFences(content) {
24569
24665
  }
24570
24666
  return open4 === null;
24571
24667
  }
24668
+ var TRANSLATED_DOCUMENTS = [
24669
+ "README.md",
24670
+ "CONTRIBUTING.md",
24671
+ "SECURITY.md",
24672
+ "docs/SCHEMA.md",
24673
+ "docs/CLI.md",
24674
+ "docs/GOVERNANCE.md",
24675
+ "docs/CATEGORIES.md",
24676
+ "docs/CREDIT.md",
24677
+ "docs/RANKING.md",
24678
+ "docs/UNOFFICIAL.md"
24679
+ ];
24680
+ var LOCALE_NAME = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})?$/u;
24681
+ function documentShape(content) {
24682
+ const lines = content.split(/\r?\n/u);
24683
+ return {
24684
+ sections: lines.filter((line) => line.startsWith("## ")).length,
24685
+ subsections: lines.filter((line) => line.startsWith("### ")).length,
24686
+ rows: lines.filter((line) => line.startsWith("| ")).length
24687
+ };
24688
+ }
24689
+ async function translationShapeDiagnostics(root) {
24690
+ let locales;
24691
+ try {
24692
+ locales = (await readdir2(join2(root, "docs", "i18n"), { withFileTypes: true })).filter((entry) => entry.isDirectory() && LOCALE_NAME.test(entry.name)).map((entry) => entry.name).sort();
24693
+ } catch {
24694
+ return [];
24695
+ }
24696
+ const diagnostics = [];
24697
+ for (const document of TRANSLATED_DOCUMENTS) {
24698
+ let english;
24699
+ try {
24700
+ english = documentShape(await readContained(root, document));
24701
+ } catch {
24702
+ continue;
24703
+ }
24704
+ for (const locale of locales) {
24705
+ const file = `docs/i18n/${locale}/${document.split("/").at(-1)}`;
24706
+ let translated;
24707
+ try {
24708
+ translated = documentShape(await readContained(root, file));
24709
+ } catch {
24710
+ continue;
24711
+ }
24712
+ const drift = ["sections", "subsections", "rows"].filter(
24713
+ (part) => translated[part] !== english[part]
24714
+ );
24715
+ if (drift.length > 0) {
24716
+ diagnostics.push({
24717
+ file,
24718
+ code: "docs",
24719
+ message: `translation does not match the shape of ${document}: ` + drift.map((part) => `${part} ${translated[part]} != ${english[part]}`).join(", ")
24720
+ });
24721
+ }
24722
+ }
24723
+ }
24724
+ return diagnostics;
24725
+ }
24572
24726
  function tableValues(content, heading) {
24573
24727
  const start = content.indexOf(`## ${heading}`);
24574
24728
  if (start < 0) {
@@ -24594,7 +24748,7 @@ function schemaEnum(document, property) {
24594
24748
  function sameValues(left, right) {
24595
24749
  return [...left].sort().join("\0") === [...right].sort().join("\0");
24596
24750
  }
24597
- async function docsCheckCommand(context, rootInput) {
24751
+ async function docsCheckCommand(context, rootInput, options = {}) {
24598
24752
  let root;
24599
24753
  try {
24600
24754
  root = await canonicalRoot(rootInput);
@@ -24633,6 +24787,7 @@ async function docsCheckCommand(context, rootInput) {
24633
24787
  diagnostics.push({ file, code: "docs", message: "unbalanced Markdown fence" });
24634
24788
  }
24635
24789
  }
24790
+ diagnostics.push(...await translationShapeDiagnostics(root));
24636
24791
  try {
24637
24792
  const categories = contents.get("docs/CATEGORIES.md");
24638
24793
  const schema = (0, import_yaml3.parse)(await readContained(root, "schemas/plugin.schema.yaml"), {
@@ -24667,7 +24822,7 @@ async function docsCheckCommand(context, rootInput) {
24667
24822
  const snapshot = await context.loadCatalog({ root: rootInput });
24668
24823
  diagnostics.push(...snapshot.diagnostics);
24669
24824
  const readme = contents.get("README.md");
24670
- if (readme !== void 0 && !readme.includes(`**${snapshot.entries.length} plugins merged.**`)) {
24825
+ if (options.skipCount !== true && readme !== void 0 && !readme.includes(`**${snapshot.entries.length} plugins merged.**`)) {
24671
24826
  diagnostics.push({
24672
24827
  file: "README.md",
24673
24828
  code: "docs",
@@ -24891,12 +25046,12 @@ async function openArtifactDeliveryChannel(lease, options) {
24891
25046
  // src/dsh/installState.ts
24892
25047
  import { randomUUID } from "node:crypto";
24893
25048
  import { homedir, hostname as localHostname } from "node:os";
24894
- import { join as join3, resolve as resolve6 } from "node:path";
24895
- import { lstat as lstat4, mkdir as mkdir3, readFile as readFile4, readdir as readdir2, realpath as realpath6, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
25049
+ import { join as join4, resolve as resolve6 } from "node:path";
25050
+ import { lstat as lstat4, mkdir as mkdir3, readFile as readFile4, readdir as readdir3, realpath as realpath6, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
24896
25051
 
24897
25052
  // src/dsh/paths.ts
24898
25053
  import { lstat as lstat3, mkdir as mkdir2, realpath as realpath5 } from "node:fs/promises";
24899
- import { isAbsolute as isAbsolute5, join as join2, relative as relative5, resolve as resolve5, sep as sep5 } from "node:path";
25054
+ import { isAbsolute as isAbsolute5, join as join3, relative as relative5, resolve as resolve5, sep as sep5 } from "node:path";
24900
25055
  var SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
24901
25056
  function isPathWithin(root, candidate) {
24902
25057
  const child = relative5(root, candidate);
@@ -24925,7 +25080,7 @@ async function ensureContainedDirectory(canonicalRoot2, ...segments) {
24925
25080
  let current = canonicalRoot2;
24926
25081
  for (const segment of segments) {
24927
25082
  assertSafeCacheSegment(segment, "managed path segment");
24928
- const candidate = join2(current, segment);
25083
+ const candidate = join3(current, segment);
24929
25084
  if (!isPathWithin(canonicalRoot2, candidate)) {
24930
25085
  throw new CliSafetyError("managed path escapes its root");
24931
25086
  }
@@ -25069,7 +25224,7 @@ function parseRecoveryMarker(value) {
25069
25224
  }
25070
25225
  function resolveDshHome(env = process.env) {
25071
25226
  const configured = env.DSH_HOME?.trim();
25072
- return resolve6(configured === void 0 || configured === "" ? join3(homedir(), ".dsh") : configured);
25227
+ return resolve6(configured === void 0 || configured === "" ? join4(homedir(), ".dsh") : configured);
25073
25228
  }
25074
25229
  function safeRelativeCachePath(value) {
25075
25230
  const segments = value.split("/");
@@ -25147,7 +25302,7 @@ async function readInstallState(home) {
25147
25302
  }
25148
25303
  throw error;
25149
25304
  }
25150
- const statePath = join3(canonicalHome, ".dsh-plugins", "state.json");
25305
+ const statePath = join4(canonicalHome, ".dsh-plugins", "state.json");
25151
25306
  try {
25152
25307
  const info = await lstat4(statePath);
25153
25308
  if (info.isSymbolicLink() || !info.isFile()) {
@@ -25216,7 +25371,7 @@ async function readStateWriterOwner(lockPath) {
25216
25371
  if (info.isSymbolicLink() || !info.isDirectory()) {
25217
25372
  throw new CliSafetyError("install state publication lock path is unsafe");
25218
25373
  }
25219
- const source = await readFile4(join3(lockPath, "owner.json"), "utf8");
25374
+ const source = await readFile4(join4(lockPath, "owner.json"), "utf8");
25220
25375
  if (source.length > 4096) throw new Error("oversized-owner");
25221
25376
  const value = JSON.parse(source);
25222
25377
  if (value.version !== 1 || typeof value.ownerToken !== "string" || !SAFE_WRITER_TOKEN.test(value.ownerToken) || !Number.isSafeInteger(value.fencingToken) || (value.fencingToken ?? 0) < 0 || !Number.isFinite(value.acquiredAt) || !Number.isFinite(value.leaseMs) || (value.leaseMs ?? 0) <= 0 || !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0 || typeof value.processStartIdentity !== "string" || !SAFE_PROCESS_IDENTITY.test(value.processStartIdentity) || typeof value.hostname !== "string" || value.hostname.length === 0 || value.hostname.length > 255) {
@@ -25257,7 +25412,7 @@ async function writeStateWriterFence(path, value) {
25257
25412
  }
25258
25413
  }
25259
25414
  async function writeStateWriterOwner(lockPath, owner) {
25260
- const temporary = join3(lockPath, `.owner-${randomUUID()}.tmp`);
25415
+ const temporary = join4(lockPath, `.owner-${randomUUID()}.tmp`);
25261
25416
  try {
25262
25417
  await writeFile2(temporary, `${JSON.stringify(owner)}
25263
25418
  `, {
@@ -25265,7 +25420,7 @@ async function writeStateWriterOwner(lockPath, owner) {
25265
25420
  mode: 384,
25266
25421
  flag: "wx"
25267
25422
  });
25268
- await rename(temporary, join3(lockPath, "owner.json"));
25423
+ await rename(temporary, join4(lockPath, "owner.json"));
25269
25424
  } catch {
25270
25425
  await rm2(temporary, { force: true }).catch(() => void 0);
25271
25426
  throw new CliSafetyError("install state publication lock metadata could not be published");
@@ -25275,7 +25430,7 @@ async function latestStateWriterHeartbeat(lockPath, owner) {
25275
25430
  let latest = owner.acquiredAt;
25276
25431
  const prefix = `heartbeat-${owner.ownerToken}-`;
25277
25432
  try {
25278
- for (const entry of await readdir2(lockPath, { withFileTypes: true })) {
25433
+ for (const entry of await readdir3(lockPath, { withFileTypes: true })) {
25279
25434
  if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue;
25280
25435
  const timestamp = Number(entry.name.slice(prefix.length));
25281
25436
  if (Number.isFinite(timestamp)) latest = Math.max(latest, timestamp);
@@ -25313,15 +25468,15 @@ async function acquireStateWriter(stateDirectory, options = {}) {
25313
25468
  if (!Number.isSafeInteger(pid) || pid <= 0 || !SAFE_PROCESS_IDENTITY.test(processStartIdentity) || hostname.length === 0 || hostname.length > 255) {
25314
25469
  throw new CliSafetyError("install state writer identity is invalid");
25315
25470
  }
25316
- const lockPath = join3(stateDirectory, ".state-write.lock");
25317
- const fencePath = join3(stateDirectory, ".state-write.fence.json");
25471
+ const lockPath = join4(stateDirectory, ".state-write.lock");
25472
+ const fencePath = join4(stateDirectory, ".state-write.fence.json");
25318
25473
  const startedAt = now();
25319
25474
  while (true) {
25320
25475
  const ownerToken = makeOwnerToken();
25321
25476
  if (!SAFE_WRITER_TOKEN.test(ownerToken)) {
25322
25477
  throw new CliSafetyError("install state writer owner token is invalid");
25323
25478
  }
25324
- const candidate = join3(stateDirectory, `.state-write.claim-${randomUUID()}`);
25479
+ const candidate = join4(stateDirectory, `.state-write.claim-${randomUUID()}`);
25325
25480
  const provisionalOwner = {
25326
25481
  version: 1,
25327
25482
  ownerToken,
@@ -25336,7 +25491,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
25336
25491
  let fencingToken = 0;
25337
25492
  try {
25338
25493
  await mkdir3(candidate, { mode: 448 });
25339
- await writeFile2(join3(candidate, "owner.json"), `${JSON.stringify(provisionalOwner)}
25494
+ await writeFile2(join4(candidate, "owner.json"), `${JSON.stringify(provisionalOwner)}
25340
25495
  `, {
25341
25496
  encoding: "utf8",
25342
25497
  mode: 384,
@@ -25350,7 +25505,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
25350
25505
  } catch (error) {
25351
25506
  await rm2(candidate, { recursive: true, force: true }).catch(() => void 0);
25352
25507
  if (claimed) {
25353
- const failedPath = join3(stateDirectory, `.state-write.failed-${randomUUID()}`);
25508
+ const failedPath = join4(stateDirectory, `.state-write.failed-${randomUUID()}`);
25354
25509
  try {
25355
25510
  const current2 = await readStateWriterOwner(lockPath);
25356
25511
  if (current2?.ownerToken === ownerToken) {
@@ -25382,7 +25537,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
25382
25537
  if (leaseExpired && ownerProvenDead) {
25383
25538
  const confirmed = await readStateWriterOwner(lockPath);
25384
25539
  if (confirmed?.ownerToken === current.ownerToken && confirmed.fencingToken === current.fencingToken && now() - await latestStateWriterHeartbeat(lockPath, confirmed) > confirmed.leaseMs) {
25385
- const stalePath = join3(stateDirectory, `.state-write.stale-${randomUUID()}`);
25540
+ const stalePath = join4(stateDirectory, `.state-write.stale-${randomUUID()}`);
25386
25541
  try {
25387
25542
  await rename(lockPath, stalePath);
25388
25543
  const detached = await readStateWriterOwner(stalePath);
@@ -25419,7 +25574,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
25419
25574
  const heartbeat = async () => {
25420
25575
  await assertOwned();
25421
25576
  try {
25422
- await mkdir3(join3(lockPath, `heartbeat-${ownerToken}-${now()}`), { mode: 448 });
25577
+ await mkdir3(join4(lockPath, `heartbeat-${ownerToken}-${now()}`), { mode: 448 });
25423
25578
  } catch (error) {
25424
25579
  if (stateWriterErrorCode(error) !== "EEXIST") {
25425
25580
  lost = true;
@@ -25441,7 +25596,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
25441
25596
  if (current === null || current.ownerToken !== ownerToken || current.fencingToken !== fencingToken || current.processStartIdentity !== processStartIdentity) {
25442
25597
  return;
25443
25598
  }
25444
- const releasePath = join3(stateDirectory, `.state-write.release-${ownerToken}`);
25599
+ const releasePath = join4(stateDirectory, `.state-write.release-${ownerToken}`);
25445
25600
  try {
25446
25601
  await rename(lockPath, releasePath);
25447
25602
  const detached = await readStateWriterOwner(releasePath);
@@ -25468,8 +25623,8 @@ async function writeInstallState(home, state, options) {
25468
25623
  const canonicalHome = await ensureCanonicalHome(home);
25469
25624
  const stateDirectory = await ensureContainedDirectory(canonicalHome, ".dsh-plugins");
25470
25625
  const writer = await acquireStateWriter(stateDirectory, options?.writer);
25471
- const target = join3(stateDirectory, "state.json");
25472
- const temporary = join3(stateDirectory, `.state-${randomUUID()}.tmp`);
25626
+ const target = join4(stateDirectory, "state.json");
25627
+ const temporary = join4(stateDirectory, `.state-${randomUUID()}.tmp`);
25473
25628
  try {
25474
25629
  const current = await readInstallState(canonicalHome);
25475
25630
  const currentGeneration = current.generation ?? 0;
@@ -25526,8 +25681,8 @@ async function writeInstallState(home, state, options) {
25526
25681
  // src/dsh/profileLock.ts
25527
25682
  import { randomUUID as randomUUID2 } from "node:crypto";
25528
25683
  import { hostname as localHostname2 } from "node:os";
25529
- import { join as join4 } from "node:path";
25530
- import { lstat as lstat5, mkdir as mkdir4, readFile as readFile5, readdir as readdir3, rename as rename2, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
25684
+ import { join as join5 } from "node:path";
25685
+ import { lstat as lstat5, mkdir as mkdir4, readFile as readFile5, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
25531
25686
  var DEFAULT_LEASE_MS = 3e4;
25532
25687
  var DEFAULT_HEARTBEAT_MS = 1e4;
25533
25688
  var DEFAULT_ACQUIRE_TIMEOUT_MS = 3e4;
@@ -25569,7 +25724,7 @@ async function readOwner(lockPath) {
25569
25724
  if (info.isSymbolicLink() || !info.isDirectory()) {
25570
25725
  throw new CliSafetyError("profile mutation lock path is unsafe");
25571
25726
  }
25572
- const source = await readFile5(join4(lockPath, "owner.json"), "utf8");
25727
+ const source = await readFile5(join5(lockPath, "owner.json"), "utf8");
25573
25728
  if (source.length > 4096) throw new Error("oversized-owner");
25574
25729
  const value = JSON.parse(source);
25575
25730
  if (value.version !== 1 || typeof value.ownerToken !== "string" || !SAFE_TOKEN.test(value.ownerToken) || !Number.isSafeInteger(value.fencingToken) || (value.fencingToken ?? 0) < 0 || typeof value.profile !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(value.profile) || !Number.isFinite(value.acquiredAt) || !Number.isFinite(value.leaseMs) || !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0 || typeof value.hostname !== "string" || value.hostname.length === 0 || value.hostname.length > 255) {
@@ -25583,7 +25738,7 @@ async function readOwner(lockPath) {
25583
25738
  }
25584
25739
  }
25585
25740
  async function writeOwner(lockPath, owner) {
25586
- const temporary = join4(lockPath, `.owner-${randomUUID2()}.tmp`);
25741
+ const temporary = join5(lockPath, `.owner-${randomUUID2()}.tmp`);
25587
25742
  try {
25588
25743
  await writeFile3(temporary, `${JSON.stringify(owner)}
25589
25744
  `, {
@@ -25591,7 +25746,7 @@ async function writeOwner(lockPath, owner) {
25591
25746
  mode: 384,
25592
25747
  flag: "wx"
25593
25748
  });
25594
- await rename2(temporary, join4(lockPath, "owner.json"));
25749
+ await rename2(temporary, join5(lockPath, "owner.json"));
25595
25750
  } catch {
25596
25751
  await rm3(temporary, { force: true }).catch(() => void 0);
25597
25752
  throw new CliSafetyError("profile mutation lock metadata could not be published");
@@ -25627,11 +25782,11 @@ async function writeFence(path, value) {
25627
25782
  async function readLegacyFenceMaximum(lockDirectory) {
25628
25783
  let maximum = 0;
25629
25784
  try {
25630
- for (const entry of await readdir3(lockDirectory, { withFileTypes: true })) {
25785
+ for (const entry of await readdir4(lockDirectory, { withFileTypes: true })) {
25631
25786
  if (!entry.isFile() || !/^[a-z0-9]+(?:-[a-z0-9]+)*\.fence\.json$/u.test(entry.name)) {
25632
25787
  continue;
25633
25788
  }
25634
- maximum = Math.max(maximum, await readFence(join4(lockDirectory, entry.name)));
25789
+ maximum = Math.max(maximum, await readFence(join5(lockDirectory, entry.name)));
25635
25790
  }
25636
25791
  } catch (error) {
25637
25792
  if (errorCode(error) !== "ENOENT") {
@@ -25644,7 +25799,7 @@ async function latestHeartbeat(lockPath, owner) {
25644
25799
  let latest = owner.acquiredAt;
25645
25800
  const prefix = `heartbeat-${owner.ownerToken}-`;
25646
25801
  try {
25647
- for (const entry of await readdir3(lockPath, { withFileTypes: true })) {
25802
+ for (const entry of await readdir4(lockPath, { withFileTypes: true })) {
25648
25803
  if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue;
25649
25804
  const timestamp = Number(entry.name.slice(prefix.length));
25650
25805
  if (Number.isFinite(timestamp)) latest = Math.max(latest, timestamp);
@@ -25677,8 +25832,8 @@ async function acquireProfileLock(home, profile, options = {}) {
25677
25832
  const isProcessAlive = options.isProcessAlive ?? processAlive;
25678
25833
  const canonicalHome = await ensureCanonicalHome(home);
25679
25834
  const lockDirectory = await ensureContainedDirectory(canonicalHome, ".dsh-plugins", "locks");
25680
- const lockPath = join4(lockDirectory, "mutation.lock");
25681
- const fencePath = join4(lockDirectory, "mutation.fence.json");
25835
+ const lockPath = join5(lockDirectory, "mutation.lock");
25836
+ const fencePath = join5(lockDirectory, "mutation.fence.json");
25682
25837
  const startedAt = now();
25683
25838
  while (true) {
25684
25839
  const ownerToken = makeOwnerToken();
@@ -25686,7 +25841,7 @@ async function acquireProfileLock(home, profile, options = {}) {
25686
25841
  throw new CliSafetyError("profile mutation owner token is invalid");
25687
25842
  }
25688
25843
  let fencingToken = 0;
25689
- const candidate = join4(lockDirectory, `.mutation.claim-${randomUUID2()}`);
25844
+ const candidate = join5(lockDirectory, `.mutation.claim-${randomUUID2()}`);
25690
25845
  const provisionalRecord = {
25691
25846
  version: 1,
25692
25847
  ownerToken,
@@ -25700,7 +25855,7 @@ async function acquireProfileLock(home, profile, options = {}) {
25700
25855
  let claimed = false;
25701
25856
  try {
25702
25857
  await mkdir4(candidate, { mode: 448 });
25703
- await writeFile3(join4(candidate, "owner.json"), `${JSON.stringify(provisionalRecord)}
25858
+ await writeFile3(join5(candidate, "owner.json"), `${JSON.stringify(provisionalRecord)}
25704
25859
  `, {
25705
25860
  encoding: "utf8",
25706
25861
  mode: 384,
@@ -25717,7 +25872,7 @@ async function acquireProfileLock(home, profile, options = {}) {
25717
25872
  } catch (error) {
25718
25873
  await rm3(candidate, { recursive: true, force: true }).catch(() => void 0);
25719
25874
  if (claimed) {
25720
- const failedPath = join4(lockDirectory, `.mutation.failed-${randomUUID2()}`);
25875
+ const failedPath = join5(lockDirectory, `.mutation.failed-${randomUUID2()}`);
25721
25876
  try {
25722
25877
  const current2 = await readOwner(lockPath);
25723
25878
  if (current2?.ownerToken === ownerToken) {
@@ -25736,7 +25891,7 @@ async function acquireProfileLock(home, profile, options = {}) {
25736
25891
  const leaseExpired = now() - await latestHeartbeat(lockPath, current) > current.leaseMs;
25737
25892
  const localOwnerAlive = current.hostname === hostname && isProcessAlive(current.pid);
25738
25893
  if (leaseExpired && !localOwnerAlive) {
25739
- const stalePath = join4(lockDirectory, `.mutation.stale-${randomUUID2()}`);
25894
+ const stalePath = join5(lockDirectory, `.mutation.stale-${randomUUID2()}`);
25740
25895
  try {
25741
25896
  await rename2(lockPath, stalePath);
25742
25897
  await rm3(stalePath, { recursive: true, force: true });
@@ -25765,7 +25920,7 @@ async function acquireProfileLock(home, profile, options = {}) {
25765
25920
  const heartbeat = async () => {
25766
25921
  await assertOwned();
25767
25922
  try {
25768
- await mkdir4(join4(lockPath, `heartbeat-${ownerToken}-${now()}`), { mode: 448 });
25923
+ await mkdir4(join5(lockPath, `heartbeat-${ownerToken}-${now()}`), { mode: 448 });
25769
25924
  } catch (error) {
25770
25925
  if (errorCode(error) !== "EEXIST") {
25771
25926
  lost = true;
@@ -25787,7 +25942,7 @@ async function acquireProfileLock(home, profile, options = {}) {
25787
25942
  if (current === null || current.ownerToken !== ownerToken || current.fencingToken !== fencingToken || current.profile !== profile) {
25788
25943
  return;
25789
25944
  }
25790
- const releasePath = join4(lockDirectory, `.mutation.release-${ownerToken}`);
25945
+ const releasePath = join5(lockDirectory, `.mutation.release-${ownerToken}`);
25791
25946
  try {
25792
25947
  await rename2(lockPath, releasePath);
25793
25948
  const detached = await readOwner(releasePath);
@@ -25816,17 +25971,17 @@ async function acquireProfileLock(home, profile, options = {}) {
25816
25971
 
25817
25972
  // src/dsh/profileTransaction.ts
25818
25973
  import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
25819
- import { join as join5 } from "node:path";
25820
- import { cp, lstat as lstat6, readFile as readFile6, readdir as readdir4, rename as rename3, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
25974
+ import { join as join6 } from "node:path";
25975
+ import { cp, lstat as lstat6, readFile as readFile6, readdir as readdir5, rename as rename3, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
25821
25976
  async function profileFingerprint(root) {
25822
25977
  const hash = createHash2("sha512");
25823
25978
  let files = 0;
25824
25979
  let bytes = 0;
25825
25980
  const visit = async (directory, prefix) => {
25826
- const entries = await readdir4(directory, { withFileTypes: true });
25981
+ const entries = await readdir5(directory, { withFileTypes: true });
25827
25982
  entries.sort((left, right) => Buffer.from(left.name).compare(Buffer.from(right.name)));
25828
25983
  for (const entry of entries) {
25829
- const path = join5(directory, entry.name);
25984
+ const path = join6(directory, entry.name);
25830
25985
  const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
25831
25986
  const info = await lstat6(path);
25832
25987
  if (info.isSymbolicLink()) throw new CliSafetyError("profile backup contains a symlink");
@@ -25864,21 +26019,21 @@ async function activeExists(active) {
25864
26019
  async function recoverTransactions(profiles, active, profile, committedFencingToken, assertLockOwned) {
25865
26020
  const prefix = `.dsh-plugins-transaction-${profile}-`;
25866
26021
  const journals = [];
25867
- for (const entry of await readdir4(profiles, { withFileTypes: true })) {
26022
+ for (const entry of await readdir5(profiles, { withFileTypes: true })) {
25868
26023
  if (!entry.isFile() || !entry.name.startsWith(prefix) || !entry.name.endsWith(".json")) continue;
25869
26024
  try {
25870
- const value = JSON.parse(await readFile6(join5(profiles, entry.name), "utf8"));
26025
+ const value = JSON.parse(await readFile6(join6(profiles, entry.name), "utf8"));
25871
26026
  if (value.version !== 1 || value.profile !== profile || !Number.isSafeInteger(value.fencingToken) || typeof value.existed !== "boolean" || !value.backupName.startsWith(`.dsh-plugins-backup-${profile}-`) || value.backupName.includes("/") || value.backupName.includes("\\")) {
25872
26027
  throw new Error("invalid-journal");
25873
26028
  }
25874
- journals.push({ path: join5(profiles, entry.name), value });
26029
+ journals.push({ path: join6(profiles, entry.name), value });
25875
26030
  } catch {
25876
26031
  throw new CliSafetyError("profile transaction journal requires manual recovery");
25877
26032
  }
25878
26033
  }
25879
26034
  journals.sort((left, right) => right.value.fencingToken - left.value.fencingToken);
25880
26035
  for (const journal of journals) {
25881
- const backup = join5(profiles, journal.value.backupName);
26036
+ const backup = join6(profiles, journal.value.backupName);
25882
26037
  if (journal.value.fencingToken <= committedFencingToken) {
25883
26038
  await rm4(backup, { recursive: true, force: true }).catch(() => void 0);
25884
26039
  await rm4(journal.path, { force: true }).catch(() => void 0);
@@ -25903,7 +26058,7 @@ async function beginProfileTransaction(home, profile, options = {}) {
25903
26058
  await assertLockOwned();
25904
26059
  const canonicalHome = await ensureCanonicalHome(home);
25905
26060
  const profiles = await ensureContainedDirectory(canonicalHome, "profiles");
25906
- const active = join5(profiles, profile);
26061
+ const active = join6(profiles, profile);
25907
26062
  await recoverTransactions(
25908
26063
  profiles,
25909
26064
  active,
@@ -25913,8 +26068,8 @@ async function beginProfileTransaction(home, profile, options = {}) {
25913
26068
  );
25914
26069
  const suffix = randomUUID3();
25915
26070
  const backupName = `.dsh-plugins-backup-${profile}-${suffix}`;
25916
- const backup = join5(profiles, backupName);
25917
- const journal = join5(profiles, `.dsh-plugins-transaction-${profile}-${suffix}.json`);
26071
+ const backup = join6(profiles, backupName);
26072
+ const journal = join6(profiles, `.dsh-plugins-transaction-${profile}-${suffix}.json`);
25918
26073
  const journalTemporary = `${journal}.tmp`;
25919
26074
  const existed = await activeExists(active);
25920
26075
  let backupFingerprint = null;
@@ -25992,9 +26147,9 @@ async function recoverProfileTransaction(home, reference, assertLockOwned) {
25992
26147
  }
25993
26148
  const canonicalHome = await ensureCanonicalHome(home);
25994
26149
  const profiles = await ensureContainedDirectory(canonicalHome, "profiles");
25995
- const journal = join5(canonicalHome, reference.journalRelativePath);
25996
- const backup = join5(canonicalHome, reference.backupRelativePath);
25997
- const active = join5(profiles, reference.profile);
26150
+ const journal = join6(canonicalHome, reference.journalRelativePath);
26151
+ const backup = join6(canonicalHome, reference.backupRelativePath);
26152
+ const active = join6(profiles, reference.profile);
25998
26153
  const displaced = `${backup}.displaced`;
25999
26154
  let value;
26000
26155
  try {
@@ -26046,8 +26201,8 @@ async function finalizeProfileTransactionRecovery(home, reference, assertLockOwn
26046
26201
  assertSafeProfileName(reference.profile);
26047
26202
  const canonicalHome = await ensureCanonicalHome(home);
26048
26203
  await ensureContainedDirectory(canonicalHome, "profiles");
26049
- const journal = join5(canonicalHome, reference.journalRelativePath);
26050
- const backup = join5(canonicalHome, reference.backupRelativePath);
26204
+ const journal = join6(canonicalHome, reference.journalRelativePath);
26205
+ const backup = join6(canonicalHome, reference.backupRelativePath);
26051
26206
  await assertLockOwned();
26052
26207
  await rm4(backup, { recursive: true, force: true });
26053
26208
  await rm4(`${backup}.displaced`, { recursive: true, force: true });
@@ -26073,7 +26228,7 @@ import {
26073
26228
  rename as rename4,
26074
26229
  rm as rm5
26075
26230
  } from "node:fs/promises";
26076
- import { join as join6 } from "node:path";
26231
+ import { join as join7 } from "node:path";
26077
26232
  var MutationRecoveryJournalDurabilityAmbiguousError = class extends CliSafetyError {
26078
26233
  possiblyPublished = true;
26079
26234
  constructor(operation) {
@@ -26140,8 +26295,8 @@ async function createPaths(home) {
26140
26295
  const recoveryRoot = await ensureContainedDirectory(canonicalHome, ".dsh-plugins", "recovery");
26141
26296
  return {
26142
26297
  recoveryRoot,
26143
- active: join6(recoveryRoot, "active"),
26144
- marker: join6(recoveryRoot, "active", "marker.json")
26298
+ active: join7(recoveryRoot, "active"),
26299
+ marker: join7(recoveryRoot, "active", "marker.json")
26145
26300
  };
26146
26301
  }
26147
26302
  async function existingPaths(home) {
@@ -26152,9 +26307,9 @@ async function existingPaths(home) {
26152
26307
  if (error.code === "ENOENT") return null;
26153
26308
  throw new CliSafetyError("mutation recovery journal could not be inspected safely");
26154
26309
  }
26155
- const dshPlugins = join6(canonicalHome, ".dsh-plugins");
26156
- const recoveryRoot = join6(dshPlugins, "recovery");
26157
- const active = join6(recoveryRoot, "active");
26310
+ const dshPlugins = join7(canonicalHome, ".dsh-plugins");
26311
+ const recoveryRoot = join7(dshPlugins, "recovery");
26312
+ const active = join7(recoveryRoot, "active");
26158
26313
  try {
26159
26314
  for (const path of [dshPlugins, recoveryRoot, active]) {
26160
26315
  const info = await lstat7(path);
@@ -26167,7 +26322,7 @@ async function existingPaths(home) {
26167
26322
  if (error instanceof CliSafetyError) throw error;
26168
26323
  throw new CliSafetyError("mutation recovery journal could not be inspected safely");
26169
26324
  }
26170
- return { recoveryRoot, active, marker: join6(active, "marker.json") };
26325
+ return { recoveryRoot, active, marker: join7(active, "marker.json") };
26171
26326
  }
26172
26327
  function safeRelative(value, prefix) {
26173
26328
  return typeof value === "string" && value.startsWith(prefix) && !value.includes("\\") && !value.includes("..") && !value.startsWith("/");
@@ -26207,7 +26362,7 @@ function parseJournal(value) {
26207
26362
  return record;
26208
26363
  }
26209
26364
  async function writeMarker(paths, value, dependencies = {}) {
26210
- const temporary = join6(paths.active, `.marker-${randomUUID4()}.tmp`);
26365
+ const temporary = join7(paths.active, `.marker-${randomUUID4()}.tmp`);
26211
26366
  const handle = await open2(temporary, "wx", 384);
26212
26367
  try {
26213
26368
  await handle.writeFile(`${JSON.stringify(value)}
@@ -26279,7 +26434,7 @@ async function removeCompletedMutationRecoveryJournal(home, expected, dependenci
26279
26434
  if (current === null || current.revision !== expected.revision || current.ownerToken !== expected.ownerToken || current.fencingToken !== expected.fencingToken || current.phase !== "completed") {
26280
26435
  throw new CliSafetyError("durable recovery intent ownership changed; recovery required");
26281
26436
  }
26282
- const completed = join6(paths.recoveryRoot, `.completed-${randomUUID4()}`);
26437
+ const completed = join7(paths.recoveryRoot, `.completed-${randomUUID4()}`);
26283
26438
  await dependencies.beforeRename?.();
26284
26439
  await rename4(paths.active, completed);
26285
26440
  try {
@@ -26716,14 +26871,14 @@ import {
26716
26871
  mkdtemp as mkdtemp2,
26717
26872
  open as open3,
26718
26873
  readlink,
26719
- readdir as readdir5,
26874
+ readdir as readdir6,
26720
26875
  realpath as realpath8,
26721
26876
  rename as rename5,
26722
26877
  rm as rm6,
26723
26878
  writeFile as writeFile5
26724
26879
  } from "node:fs/promises";
26725
26880
  import { devNull } from "node:os";
26726
- import { basename, dirname as dirname3, join as join7, relative as relative6, resolve as resolve7, sep as sep6 } from "node:path";
26881
+ import { basename, dirname as dirname3, join as join8, relative as relative6, resolve as resolve7, sep as sep6 } from "node:path";
26727
26882
  import { pipeline } from "node:stream/promises";
26728
26883
  import { createGzip } from "node:zlib";
26729
26884
  var PACKAGE_NAME2 = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u;
@@ -27110,10 +27265,10 @@ async function createDeterministicSourceArchive(sourceRoot, pluginRoot, destinat
27110
27265
  try {
27111
27266
  await emit(tarHeader("package/", 0, 493, "directory"));
27112
27267
  const visit = async (directory, relativeDirectory) => {
27113
- const entries = await readdir5(directory, { withFileTypes: true });
27268
+ const entries = await readdir6(directory, { withFileTypes: true });
27114
27269
  entries.sort((left, right) => Buffer.from(left.name).compare(Buffer.from(right.name)));
27115
27270
  for (const entry of entries) {
27116
- const candidate = join7(directory, entry.name);
27271
+ const candidate = join8(directory, entry.name);
27117
27272
  const relativePath = relativeDirectory === "" ? entry.name : `${relativeDirectory}/${entry.name}`;
27118
27273
  const archivePath = `package/${relativePath}`;
27119
27274
  const info = await lstat8(candidate);
@@ -27169,8 +27324,8 @@ async function createDeterministicSourceArchive(sourceRoot, pluginRoot, destinat
27169
27324
  }
27170
27325
  async function createArtifactLease(cached, home, budgets) {
27171
27326
  const leasesRoot = await ensureContainedDirectory(home, ".dsh-plugins", "artifact-leases");
27172
- const leaseRoot = await mkdtemp2(join7(leasesRoot, ".lease-"));
27173
- const destination = join7(leaseRoot, "artifact.tgz");
27327
+ const leaseRoot = await mkdtemp2(join8(leasesRoot, ".lease-"));
27328
+ const destination = join8(leaseRoot, "artifact.tgz");
27174
27329
  let heldFd;
27175
27330
  try {
27176
27331
  const cache = cached[VERIFIED_CACHE];
@@ -27307,10 +27462,10 @@ async function inspectSourceTree(root, budgets) {
27307
27462
  if (before.isSymbolicLink() || !before.isDirectory()) {
27308
27463
  throw new CliSafetyError("source tree changed during verification");
27309
27464
  }
27310
- const entries = await readdir5(directory, { withFileTypes: true });
27465
+ const entries = await readdir6(directory, { withFileTypes: true });
27311
27466
  entries.sort((left, right) => Buffer.from(left.name).compare(Buffer.from(right.name)));
27312
27467
  for (const entry of entries) {
27313
- const candidate = join7(directory, entry.name);
27468
+ const candidate = join8(directory, entry.name);
27314
27469
  const relativePath = relative6(root, candidate).split(sep6).join("/");
27315
27470
  const candidateDepth = relativePath.split("/").length;
27316
27471
  assertTreeBudget(bytes, files, candidateDepth, budgets);
@@ -27368,9 +27523,9 @@ async function measureCheckout(root, budgets) {
27368
27523
  let files = 0;
27369
27524
  const visit = async (directory, depth) => {
27370
27525
  assertTreeBudget(bytes, files, depth, budgets);
27371
- const entries = await readdir5(directory, { withFileTypes: true });
27526
+ const entries = await readdir6(directory, { withFileTypes: true });
27372
27527
  for (const entry of entries) {
27373
- const candidate = join7(directory, entry.name);
27528
+ const candidate = join8(directory, entry.name);
27374
27529
  const candidateDepth = depth + 1;
27375
27530
  const info = await lstat8(candidate);
27376
27531
  if (info.isDirectory() && !info.isSymbolicLink()) {
@@ -27541,7 +27696,7 @@ async function stageNpm(entry, temporary, fetchImpl, budgets, signal) {
27541
27696
  throw new CliSafetyError("checksum verification failed");
27542
27697
  }
27543
27698
  const artifact = "artifact.tgz";
27544
- await writeFile5(join7(temporary, artifact), bytes, { mode: 384, flag: "wx" });
27699
+ await writeFile5(join8(temporary, artifact), bytes, { mode: 384, flag: "wx" });
27545
27700
  return {
27546
27701
  packageName: entry.package.name,
27547
27702
  installRelativePath: artifact,
@@ -27554,7 +27709,7 @@ async function stageSource(entry, temporary, runProcess, budgets, signal) {
27554
27709
  }
27555
27710
  if (!SHA402.test(entry.source.commit)) throw new CliSafetyError("source commit pin is invalid");
27556
27711
  const subpath = safeSourceSubpath(entry.source.subpath);
27557
- const source = join7(temporary, "source");
27712
+ const source = join8(temporary, "source");
27558
27713
  await mkdir6(source, { mode: 448 });
27559
27714
  await runGit2(runProcess, { args: ["init", "--quiet"], cwd: source }, budgets, signal);
27560
27715
  await runGit2(
@@ -27601,7 +27756,7 @@ async function stageSource(entry, temporary, runProcess, budgets, signal) {
27601
27756
  if (checkedCommit.toLowerCase() !== entry.source.commit.toLowerCase()) {
27602
27757
  throw new CliSafetyError("source checkout does not match the commit pin");
27603
27758
  }
27604
- await rm6(join7(source, ".git"), { recursive: true, force: true });
27759
+ await rm6(join8(source, ".git"), { recursive: true, force: true });
27605
27760
  const tree = await inspectSourceTree(source, budgets);
27606
27761
  const pluginRoot = resolve7(source, subpath);
27607
27762
  const expectedRelativePath = subpath === "." ? "source" : `source/${subpath}`;
@@ -27624,7 +27779,7 @@ async function stageSource(entry, temporary, runProcess, budgets, signal) {
27624
27779
  let packageName;
27625
27780
  try {
27626
27781
  packageName = JSON.parse((await readRegularFile(
27627
- join7(canonicalPluginRoot, "package.json"),
27782
+ join8(canonicalPluginRoot, "package.json"),
27628
27783
  budgets.metadataBytes,
27629
27784
  "source package manifest is unsafe"
27630
27785
  )).toString("utf8")).name;
@@ -27658,7 +27813,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
27658
27813
  throw new CliSafetyError("staging cache key is invalid");
27659
27814
  }
27660
27815
  const metadata = JSON.parse((await readRegularFile(
27661
- join7(canonicalRoot2, "metadata.json"),
27816
+ join8(canonicalRoot2, "metadata.json"),
27662
27817
  budgets.metadataBytes,
27663
27818
  "staging cache metadata is unsafe"
27664
27819
  )).toString("utf8"));
@@ -27699,7 +27854,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
27699
27854
  if (metadata.artifact.kind !== "source" || metadata.artifact.commit !== entry.source.commit.toLowerCase() || metadata.installRelativePath !== expectedRelativePath) {
27700
27855
  throw new CliSafetyError("staging cache metadata is invalid");
27701
27856
  }
27702
- const sourceRoot = join7(canonicalRoot2, "source");
27857
+ const sourceRoot = join8(canonicalRoot2, "source");
27703
27858
  const tree = await inspectSourceTree(sourceRoot, budgets);
27704
27859
  if (tree.digest !== metadata.artifact.treeSha512 || tree.bytes !== metadata.artifact.bytes || tree.files !== metadata.artifact.files) {
27705
27860
  throw new CliSafetyError("staging cache tree verification failed");
@@ -27709,7 +27864,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
27709
27864
  throw new CliSafetyError("staging cache target is unsafe");
27710
27865
  }
27711
27866
  const packageInfo = JSON.parse((await readRegularFile(
27712
- join7(canonicalTarget, "package.json"),
27867
+ join8(canonicalTarget, "package.json"),
27713
27868
  budgets.metadataBytes,
27714
27869
  "staging cache package manifest is unsafe"
27715
27870
  )).toString("utf8"));
@@ -27745,7 +27900,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
27745
27900
  }
27746
27901
  }
27747
27902
  async function quarantineCache(finalRoot, idRoot, hash) {
27748
- const quarantine = join7(idRoot, `.quarantine-${hash}-${randomUUID5()}`);
27903
+ const quarantine = join8(idRoot, `.quarantine-${hash}-${randomUUID5()}`);
27749
27904
  try {
27750
27905
  await rename5(finalRoot, quarantine);
27751
27906
  } catch (error) {
@@ -27761,7 +27916,7 @@ async function stageCatalogEntry(entry, options) {
27761
27916
  const budgets = stageBudgets(options.budgets);
27762
27917
  const home = await ensureCanonicalHome(options.dshHome);
27763
27918
  const idRoot = await ensureContainedDirectory(home, ".dsh-plugins", "cache", id);
27764
- const finalRoot = join7(idRoot, hash);
27919
+ const finalRoot = join8(idRoot, hash);
27765
27920
  try {
27766
27921
  await lstat8(finalRoot);
27767
27922
  try {
@@ -27783,7 +27938,7 @@ async function stageCatalogEntry(entry, options) {
27783
27938
  if (options.offline === true) {
27784
27939
  throw new CliSafetyError("pinned artifact is not cached and offline mode forbids retrieval");
27785
27940
  }
27786
- const temporary = await mkdtemp2(join7(idRoot, ".stage-"));
27941
+ const temporary = await mkdtemp2(join8(idRoot, ".stage-"));
27787
27942
  try {
27788
27943
  const staged = entry.package.ecosystem === "npm" ? await stageNpm(
27789
27944
  entry,
@@ -27804,7 +27959,7 @@ async function stageCatalogEntry(entry, options) {
27804
27959
  descriptorHash: hash,
27805
27960
  ...staged
27806
27961
  };
27807
- await writeFile5(join7(temporary, "metadata.json"), `${JSON.stringify(metadata)}
27962
+ await writeFile5(join8(temporary, "metadata.json"), `${JSON.stringify(metadata)}
27808
27963
  `, {
27809
27964
  encoding: "utf8",
27810
27965
  mode: 384,
@@ -27832,7 +27987,7 @@ async function stageCatalogEntry(entry, options) {
27832
27987
  );
27833
27988
  } catch (error) {
27834
27989
  if (error instanceof GitProcessUnreapedError) {
27835
- const quarantine = join7(idRoot, `.unreaped-${hash}-${randomUUID5()}`);
27990
+ const quarantine = join8(idRoot, `.unreaped-${hash}-${randomUUID5()}`);
27836
27991
  await rename5(temporary, quarantine).catch(() => void 0);
27837
27992
  throw error;
27838
27993
  }
@@ -29380,8 +29535,8 @@ async function runCli(argv, dependencies = defaultDependencies3) {
29380
29535
  addCatalogOptions(catalog.command("validate").description("validate catalog YAML and semantics")).action(async (options) => {
29381
29536
  result = await validateCatalogCommand(dependencies, selection(options), options.json === true);
29382
29537
  });
29383
- catalog.command("docs-check").description("check required public catalog documentation").argument("[root]", "public repository root", ".").action(async (root) => {
29384
- result = await docsCheckCommand(dependencies, root);
29538
+ catalog.command("docs-check").description("check required public catalog documentation").argument("[root]", "public repository root", ".").option("--skip-count", "skip the exact README entry-count assertion (pull-request mode)").action(async (root, options) => {
29539
+ result = await docsCheckCommand(dependencies, root, options);
29385
29540
  });
29386
29541
  catalog.command("github-forms-check").description("check structured public GitHub issue forms").argument("[root]", "public repository root", ".").action(async (root) => {
29387
29542
  result = await githubFormsCheckCommand(dependencies, root);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-dsh-plugins",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Unofficial catalog and safe installer for DeepSeek Harness plugins",
5
5
  "type": "module",
6
6
  "license": "MIT",