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.
- package/README.md +20 -4
- package/dist/bin.js +268 -113
- 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
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
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:
|
|
23675
|
-
totalFileBytes:
|
|
23779
|
+
snapshotBytes: 128 * 1024 * 1024,
|
|
23780
|
+
totalFileBytes: 128 * 1024 * 1024,
|
|
23676
23781
|
fileBytes: 1024 * 1024,
|
|
23677
|
-
files:
|
|
23678
|
-
directoryEntries:
|
|
23782
|
+
files: 32768,
|
|
23783
|
+
directoryEntries: 32768,
|
|
23679
23784
|
pathBytes: 512,
|
|
23680
23785
|
redirects: 3,
|
|
23681
23786
|
gitOutputBytes: 64 * 1024,
|
|
23682
|
-
gitTreeBytes:
|
|
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
|
-
|
|
24410
|
-
|
|
24411
|
-
|
|
24412
|
-
|
|
24413
|
-
|
|
24414
|
-
|
|
24415
|
-
|
|
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
|
|
24895
|
-
import { lstat as lstat4, mkdir as mkdir3, readFile as readFile4, readdir as
|
|
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
|
|
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 =
|
|
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 === "" ?
|
|
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 =
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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 =
|
|
25317
|
-
const fencePath =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
25472
|
-
const temporary =
|
|
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
|
|
25530
|
-
import { lstat as lstat5, mkdir as mkdir4, readFile as readFile5, readdir as
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
25681
|
-
const fencePath =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
25820
|
-
import { cp, lstat as lstat6, readFile as readFile6, readdir as
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
25917
|
-
const journal =
|
|
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 =
|
|
25996
|
-
const backup =
|
|
25997
|
-
const active =
|
|
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 =
|
|
26050
|
-
const backup =
|
|
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
|
|
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:
|
|
26144
|
-
marker:
|
|
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 =
|
|
26156
|
-
const recoveryRoot =
|
|
26157
|
-
const 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:
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
27173
|
-
const destination =
|
|
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
|
|
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 =
|
|
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
|
|
27526
|
+
const entries = await readdir6(directory, { withFileTypes: true });
|
|
27372
27527
|
for (const entry of entries) {
|
|
27373
|
-
const candidate =
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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);
|