omni-dsh-plugins 1.0.1 → 1.0.3
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 +18 -3
- package/dist/bin.js +278 -100
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ never ships here.
|
|
|
22
22
|
- Node.js 20 or newer.
|
|
23
23
|
- The official `dsh` executable on `PATH` for add, update and remove operations.
|
|
24
24
|
- Network access for discovery. With no `--catalog`, the CLI reads the snapshot published at
|
|
25
|
-
<https://dsh-plugins.
|
|
25
|
+
<https://dsh-plugins.omniskill.online/catalog.snapshot.json> and accepts the revision that
|
|
26
26
|
snapshot declares, so a plugin merged today is findable today. Passing `--revision` turns the
|
|
27
27
|
fetch into an exact-commit demand and rejects any snapshot declaring a different one.
|
|
28
28
|
|
|
@@ -136,7 +136,7 @@ granting `--allow-code-execution`.
|
|
|
136
136
|
|
|
137
137
|
`--catalog` accepts a local catalog directory, a local
|
|
138
138
|
`omni-dsh-catalog-snapshot-v1` JSON file, or one of two allowlisted HTTPS URLs: the stable
|
|
139
|
-
site-hosted snapshot `https://dsh-plugins.
|
|
139
|
+
site-hosted snapshot `https://dsh-plugins.omniskill.online/catalog.snapshot.json` (the
|
|
140
140
|
operational remote source) or the raw GitHub form embedding the revision in its path.
|
|
141
141
|
Remote snapshots require `--revision <40-character-commit-sha>` and the snapshot document must
|
|
142
142
|
declare exactly that SHA. Redirects, alternate hosts, traversal, symlinks, oversized inputs,
|
|
@@ -144,6 +144,21 @@ and revision mismatches are rejected before catalog validation.
|
|
|
144
144
|
|
|
145
145
|
```bash
|
|
146
146
|
npx omni-dsh-plugins search tui \
|
|
147
|
-
--catalog https://dsh-plugins.
|
|
147
|
+
--catalog https://dsh-plugins.omniskill.online/catalog.snapshot.json \
|
|
148
148
|
--revision <published-catalog-revision>
|
|
149
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";
|
|
@@ -23669,17 +23774,17 @@ function sanitizedErrorMessage(error) {
|
|
|
23669
23774
|
// src/catalogSnapshot.ts
|
|
23670
23775
|
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
|
-
var PUBLIC_SNAPSHOT_SITE_URL = "https://dsh-plugins.
|
|
23777
|
+
var PUBLIC_SNAPSHOT_SITE_URL = "https://dsh-plugins.omniskill.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({
|
|
@@ -24465,8 +24570,8 @@ async function materializeSelection(selection2, dependencies) {
|
|
|
24465
24570
|
|
|
24466
24571
|
// src/commands/catalog.ts
|
|
24467
24572
|
var import_yaml3 = __toESM(require_dist2(), 1);
|
|
24468
|
-
import { readFile as readFile3, realpath as realpath4 } from "node:fs/promises";
|
|
24469
|
-
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";
|
|
24470
24575
|
function sortedDiagnostics(diagnostics) {
|
|
24471
24576
|
return [...diagnostics].sort(
|
|
24472
24577
|
(left, right) => left.file.localeCompare(right.file) || left.code.localeCompare(right.code) || left.message.localeCompare(right.message)
|
|
@@ -24560,6 +24665,64 @@ function hasBalancedMarkdownFences(content) {
|
|
|
24560
24665
|
}
|
|
24561
24666
|
return open4 === null;
|
|
24562
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
|
+
}
|
|
24563
24726
|
function tableValues(content, heading) {
|
|
24564
24727
|
const start = content.indexOf(`## ${heading}`);
|
|
24565
24728
|
if (start < 0) {
|
|
@@ -24585,7 +24748,7 @@ function schemaEnum(document, property) {
|
|
|
24585
24748
|
function sameValues(left, right) {
|
|
24586
24749
|
return [...left].sort().join("\0") === [...right].sort().join("\0");
|
|
24587
24750
|
}
|
|
24588
|
-
async function docsCheckCommand(context, rootInput) {
|
|
24751
|
+
async function docsCheckCommand(context, rootInput, options = {}) {
|
|
24589
24752
|
let root;
|
|
24590
24753
|
try {
|
|
24591
24754
|
root = await canonicalRoot(rootInput);
|
|
@@ -24624,6 +24787,7 @@ async function docsCheckCommand(context, rootInput) {
|
|
|
24624
24787
|
diagnostics.push({ file, code: "docs", message: "unbalanced Markdown fence" });
|
|
24625
24788
|
}
|
|
24626
24789
|
}
|
|
24790
|
+
diagnostics.push(...await translationShapeDiagnostics(root));
|
|
24627
24791
|
try {
|
|
24628
24792
|
const categories = contents.get("docs/CATEGORIES.md");
|
|
24629
24793
|
const schema = (0, import_yaml3.parse)(await readContained(root, "schemas/plugin.schema.yaml"), {
|
|
@@ -24658,7 +24822,7 @@ async function docsCheckCommand(context, rootInput) {
|
|
|
24658
24822
|
const snapshot = await context.loadCatalog({ root: rootInput });
|
|
24659
24823
|
diagnostics.push(...snapshot.diagnostics);
|
|
24660
24824
|
const readme = contents.get("README.md");
|
|
24661
|
-
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.**`)) {
|
|
24662
24826
|
diagnostics.push({
|
|
24663
24827
|
file: "README.md",
|
|
24664
24828
|
code: "docs",
|
|
@@ -24882,12 +25046,12 @@ async function openArtifactDeliveryChannel(lease, options) {
|
|
|
24882
25046
|
// src/dsh/installState.ts
|
|
24883
25047
|
import { randomUUID } from "node:crypto";
|
|
24884
25048
|
import { homedir, hostname as localHostname } from "node:os";
|
|
24885
|
-
import { join as
|
|
24886
|
-
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";
|
|
24887
25051
|
|
|
24888
25052
|
// src/dsh/paths.ts
|
|
24889
25053
|
import { lstat as lstat3, mkdir as mkdir2, realpath as realpath5 } from "node:fs/promises";
|
|
24890
|
-
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";
|
|
24891
25055
|
var SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
24892
25056
|
function isPathWithin(root, candidate) {
|
|
24893
25057
|
const child = relative5(root, candidate);
|
|
@@ -24916,7 +25080,7 @@ async function ensureContainedDirectory(canonicalRoot2, ...segments) {
|
|
|
24916
25080
|
let current = canonicalRoot2;
|
|
24917
25081
|
for (const segment of segments) {
|
|
24918
25082
|
assertSafeCacheSegment(segment, "managed path segment");
|
|
24919
|
-
const candidate =
|
|
25083
|
+
const candidate = join3(current, segment);
|
|
24920
25084
|
if (!isPathWithin(canonicalRoot2, candidate)) {
|
|
24921
25085
|
throw new CliSafetyError("managed path escapes its root");
|
|
24922
25086
|
}
|
|
@@ -25060,7 +25224,7 @@ function parseRecoveryMarker(value) {
|
|
|
25060
25224
|
}
|
|
25061
25225
|
function resolveDshHome(env = process.env) {
|
|
25062
25226
|
const configured = env.DSH_HOME?.trim();
|
|
25063
|
-
return resolve6(configured === void 0 || configured === "" ?
|
|
25227
|
+
return resolve6(configured === void 0 || configured === "" ? join4(homedir(), ".dsh") : configured);
|
|
25064
25228
|
}
|
|
25065
25229
|
function safeRelativeCachePath(value) {
|
|
25066
25230
|
const segments = value.split("/");
|
|
@@ -25138,7 +25302,7 @@ async function readInstallState(home) {
|
|
|
25138
25302
|
}
|
|
25139
25303
|
throw error;
|
|
25140
25304
|
}
|
|
25141
|
-
const statePath =
|
|
25305
|
+
const statePath = join4(canonicalHome, ".dsh-plugins", "state.json");
|
|
25142
25306
|
try {
|
|
25143
25307
|
const info = await lstat4(statePath);
|
|
25144
25308
|
if (info.isSymbolicLink() || !info.isFile()) {
|
|
@@ -25207,7 +25371,7 @@ async function readStateWriterOwner(lockPath) {
|
|
|
25207
25371
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
25208
25372
|
throw new CliSafetyError("install state publication lock path is unsafe");
|
|
25209
25373
|
}
|
|
25210
|
-
const source = await readFile4(
|
|
25374
|
+
const source = await readFile4(join4(lockPath, "owner.json"), "utf8");
|
|
25211
25375
|
if (source.length > 4096) throw new Error("oversized-owner");
|
|
25212
25376
|
const value = JSON.parse(source);
|
|
25213
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) {
|
|
@@ -25248,7 +25412,7 @@ async function writeStateWriterFence(path, value) {
|
|
|
25248
25412
|
}
|
|
25249
25413
|
}
|
|
25250
25414
|
async function writeStateWriterOwner(lockPath, owner) {
|
|
25251
|
-
const temporary =
|
|
25415
|
+
const temporary = join4(lockPath, `.owner-${randomUUID()}.tmp`);
|
|
25252
25416
|
try {
|
|
25253
25417
|
await writeFile2(temporary, `${JSON.stringify(owner)}
|
|
25254
25418
|
`, {
|
|
@@ -25256,7 +25420,7 @@ async function writeStateWriterOwner(lockPath, owner) {
|
|
|
25256
25420
|
mode: 384,
|
|
25257
25421
|
flag: "wx"
|
|
25258
25422
|
});
|
|
25259
|
-
await rename(temporary,
|
|
25423
|
+
await rename(temporary, join4(lockPath, "owner.json"));
|
|
25260
25424
|
} catch {
|
|
25261
25425
|
await rm2(temporary, { force: true }).catch(() => void 0);
|
|
25262
25426
|
throw new CliSafetyError("install state publication lock metadata could not be published");
|
|
@@ -25266,7 +25430,7 @@ async function latestStateWriterHeartbeat(lockPath, owner) {
|
|
|
25266
25430
|
let latest = owner.acquiredAt;
|
|
25267
25431
|
const prefix = `heartbeat-${owner.ownerToken}-`;
|
|
25268
25432
|
try {
|
|
25269
|
-
for (const entry of await
|
|
25433
|
+
for (const entry of await readdir3(lockPath, { withFileTypes: true })) {
|
|
25270
25434
|
if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue;
|
|
25271
25435
|
const timestamp = Number(entry.name.slice(prefix.length));
|
|
25272
25436
|
if (Number.isFinite(timestamp)) latest = Math.max(latest, timestamp);
|
|
@@ -25304,15 +25468,15 @@ async function acquireStateWriter(stateDirectory, options = {}) {
|
|
|
25304
25468
|
if (!Number.isSafeInteger(pid) || pid <= 0 || !SAFE_PROCESS_IDENTITY.test(processStartIdentity) || hostname.length === 0 || hostname.length > 255) {
|
|
25305
25469
|
throw new CliSafetyError("install state writer identity is invalid");
|
|
25306
25470
|
}
|
|
25307
|
-
const lockPath =
|
|
25308
|
-
const fencePath =
|
|
25471
|
+
const lockPath = join4(stateDirectory, ".state-write.lock");
|
|
25472
|
+
const fencePath = join4(stateDirectory, ".state-write.fence.json");
|
|
25309
25473
|
const startedAt = now();
|
|
25310
25474
|
while (true) {
|
|
25311
25475
|
const ownerToken = makeOwnerToken();
|
|
25312
25476
|
if (!SAFE_WRITER_TOKEN.test(ownerToken)) {
|
|
25313
25477
|
throw new CliSafetyError("install state writer owner token is invalid");
|
|
25314
25478
|
}
|
|
25315
|
-
const candidate =
|
|
25479
|
+
const candidate = join4(stateDirectory, `.state-write.claim-${randomUUID()}`);
|
|
25316
25480
|
const provisionalOwner = {
|
|
25317
25481
|
version: 1,
|
|
25318
25482
|
ownerToken,
|
|
@@ -25327,7 +25491,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
|
|
|
25327
25491
|
let fencingToken = 0;
|
|
25328
25492
|
try {
|
|
25329
25493
|
await mkdir3(candidate, { mode: 448 });
|
|
25330
|
-
await writeFile2(
|
|
25494
|
+
await writeFile2(join4(candidate, "owner.json"), `${JSON.stringify(provisionalOwner)}
|
|
25331
25495
|
`, {
|
|
25332
25496
|
encoding: "utf8",
|
|
25333
25497
|
mode: 384,
|
|
@@ -25341,7 +25505,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
|
|
|
25341
25505
|
} catch (error) {
|
|
25342
25506
|
await rm2(candidate, { recursive: true, force: true }).catch(() => void 0);
|
|
25343
25507
|
if (claimed) {
|
|
25344
|
-
const failedPath =
|
|
25508
|
+
const failedPath = join4(stateDirectory, `.state-write.failed-${randomUUID()}`);
|
|
25345
25509
|
try {
|
|
25346
25510
|
const current2 = await readStateWriterOwner(lockPath);
|
|
25347
25511
|
if (current2?.ownerToken === ownerToken) {
|
|
@@ -25373,7 +25537,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
|
|
|
25373
25537
|
if (leaseExpired && ownerProvenDead) {
|
|
25374
25538
|
const confirmed = await readStateWriterOwner(lockPath);
|
|
25375
25539
|
if (confirmed?.ownerToken === current.ownerToken && confirmed.fencingToken === current.fencingToken && now() - await latestStateWriterHeartbeat(lockPath, confirmed) > confirmed.leaseMs) {
|
|
25376
|
-
const stalePath =
|
|
25540
|
+
const stalePath = join4(stateDirectory, `.state-write.stale-${randomUUID()}`);
|
|
25377
25541
|
try {
|
|
25378
25542
|
await rename(lockPath, stalePath);
|
|
25379
25543
|
const detached = await readStateWriterOwner(stalePath);
|
|
@@ -25410,7 +25574,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
|
|
|
25410
25574
|
const heartbeat = async () => {
|
|
25411
25575
|
await assertOwned();
|
|
25412
25576
|
try {
|
|
25413
|
-
await mkdir3(
|
|
25577
|
+
await mkdir3(join4(lockPath, `heartbeat-${ownerToken}-${now()}`), { mode: 448 });
|
|
25414
25578
|
} catch (error) {
|
|
25415
25579
|
if (stateWriterErrorCode(error) !== "EEXIST") {
|
|
25416
25580
|
lost = true;
|
|
@@ -25432,7 +25596,7 @@ async function acquireStateWriter(stateDirectory, options = {}) {
|
|
|
25432
25596
|
if (current === null || current.ownerToken !== ownerToken || current.fencingToken !== fencingToken || current.processStartIdentity !== processStartIdentity) {
|
|
25433
25597
|
return;
|
|
25434
25598
|
}
|
|
25435
|
-
const releasePath =
|
|
25599
|
+
const releasePath = join4(stateDirectory, `.state-write.release-${ownerToken}`);
|
|
25436
25600
|
try {
|
|
25437
25601
|
await rename(lockPath, releasePath);
|
|
25438
25602
|
const detached = await readStateWriterOwner(releasePath);
|
|
@@ -25459,8 +25623,8 @@ async function writeInstallState(home, state, options) {
|
|
|
25459
25623
|
const canonicalHome = await ensureCanonicalHome(home);
|
|
25460
25624
|
const stateDirectory = await ensureContainedDirectory(canonicalHome, ".dsh-plugins");
|
|
25461
25625
|
const writer = await acquireStateWriter(stateDirectory, options?.writer);
|
|
25462
|
-
const target =
|
|
25463
|
-
const temporary =
|
|
25626
|
+
const target = join4(stateDirectory, "state.json");
|
|
25627
|
+
const temporary = join4(stateDirectory, `.state-${randomUUID()}.tmp`);
|
|
25464
25628
|
try {
|
|
25465
25629
|
const current = await readInstallState(canonicalHome);
|
|
25466
25630
|
const currentGeneration = current.generation ?? 0;
|
|
@@ -25517,8 +25681,8 @@ async function writeInstallState(home, state, options) {
|
|
|
25517
25681
|
// src/dsh/profileLock.ts
|
|
25518
25682
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
25519
25683
|
import { hostname as localHostname2 } from "node:os";
|
|
25520
|
-
import { join as
|
|
25521
|
-
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";
|
|
25522
25686
|
var DEFAULT_LEASE_MS = 3e4;
|
|
25523
25687
|
var DEFAULT_HEARTBEAT_MS = 1e4;
|
|
25524
25688
|
var DEFAULT_ACQUIRE_TIMEOUT_MS = 3e4;
|
|
@@ -25560,7 +25724,7 @@ async function readOwner(lockPath) {
|
|
|
25560
25724
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
25561
25725
|
throw new CliSafetyError("profile mutation lock path is unsafe");
|
|
25562
25726
|
}
|
|
25563
|
-
const source = await readFile5(
|
|
25727
|
+
const source = await readFile5(join5(lockPath, "owner.json"), "utf8");
|
|
25564
25728
|
if (source.length > 4096) throw new Error("oversized-owner");
|
|
25565
25729
|
const value = JSON.parse(source);
|
|
25566
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) {
|
|
@@ -25574,7 +25738,7 @@ async function readOwner(lockPath) {
|
|
|
25574
25738
|
}
|
|
25575
25739
|
}
|
|
25576
25740
|
async function writeOwner(lockPath, owner) {
|
|
25577
|
-
const temporary =
|
|
25741
|
+
const temporary = join5(lockPath, `.owner-${randomUUID2()}.tmp`);
|
|
25578
25742
|
try {
|
|
25579
25743
|
await writeFile3(temporary, `${JSON.stringify(owner)}
|
|
25580
25744
|
`, {
|
|
@@ -25582,7 +25746,7 @@ async function writeOwner(lockPath, owner) {
|
|
|
25582
25746
|
mode: 384,
|
|
25583
25747
|
flag: "wx"
|
|
25584
25748
|
});
|
|
25585
|
-
await rename2(temporary,
|
|
25749
|
+
await rename2(temporary, join5(lockPath, "owner.json"));
|
|
25586
25750
|
} catch {
|
|
25587
25751
|
await rm3(temporary, { force: true }).catch(() => void 0);
|
|
25588
25752
|
throw new CliSafetyError("profile mutation lock metadata could not be published");
|
|
@@ -25618,11 +25782,11 @@ async function writeFence(path, value) {
|
|
|
25618
25782
|
async function readLegacyFenceMaximum(lockDirectory) {
|
|
25619
25783
|
let maximum = 0;
|
|
25620
25784
|
try {
|
|
25621
|
-
for (const entry of await
|
|
25785
|
+
for (const entry of await readdir4(lockDirectory, { withFileTypes: true })) {
|
|
25622
25786
|
if (!entry.isFile() || !/^[a-z0-9]+(?:-[a-z0-9]+)*\.fence\.json$/u.test(entry.name)) {
|
|
25623
25787
|
continue;
|
|
25624
25788
|
}
|
|
25625
|
-
maximum = Math.max(maximum, await readFence(
|
|
25789
|
+
maximum = Math.max(maximum, await readFence(join5(lockDirectory, entry.name)));
|
|
25626
25790
|
}
|
|
25627
25791
|
} catch (error) {
|
|
25628
25792
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -25635,7 +25799,7 @@ async function latestHeartbeat(lockPath, owner) {
|
|
|
25635
25799
|
let latest = owner.acquiredAt;
|
|
25636
25800
|
const prefix = `heartbeat-${owner.ownerToken}-`;
|
|
25637
25801
|
try {
|
|
25638
|
-
for (const entry of await
|
|
25802
|
+
for (const entry of await readdir4(lockPath, { withFileTypes: true })) {
|
|
25639
25803
|
if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue;
|
|
25640
25804
|
const timestamp = Number(entry.name.slice(prefix.length));
|
|
25641
25805
|
if (Number.isFinite(timestamp)) latest = Math.max(latest, timestamp);
|
|
@@ -25668,8 +25832,8 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25668
25832
|
const isProcessAlive = options.isProcessAlive ?? processAlive;
|
|
25669
25833
|
const canonicalHome = await ensureCanonicalHome(home);
|
|
25670
25834
|
const lockDirectory = await ensureContainedDirectory(canonicalHome, ".dsh-plugins", "locks");
|
|
25671
|
-
const lockPath =
|
|
25672
|
-
const fencePath =
|
|
25835
|
+
const lockPath = join5(lockDirectory, "mutation.lock");
|
|
25836
|
+
const fencePath = join5(lockDirectory, "mutation.fence.json");
|
|
25673
25837
|
const startedAt = now();
|
|
25674
25838
|
while (true) {
|
|
25675
25839
|
const ownerToken = makeOwnerToken();
|
|
@@ -25677,7 +25841,7 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25677
25841
|
throw new CliSafetyError("profile mutation owner token is invalid");
|
|
25678
25842
|
}
|
|
25679
25843
|
let fencingToken = 0;
|
|
25680
|
-
const candidate =
|
|
25844
|
+
const candidate = join5(lockDirectory, `.mutation.claim-${randomUUID2()}`);
|
|
25681
25845
|
const provisionalRecord = {
|
|
25682
25846
|
version: 1,
|
|
25683
25847
|
ownerToken,
|
|
@@ -25691,7 +25855,7 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25691
25855
|
let claimed = false;
|
|
25692
25856
|
try {
|
|
25693
25857
|
await mkdir4(candidate, { mode: 448 });
|
|
25694
|
-
await writeFile3(
|
|
25858
|
+
await writeFile3(join5(candidate, "owner.json"), `${JSON.stringify(provisionalRecord)}
|
|
25695
25859
|
`, {
|
|
25696
25860
|
encoding: "utf8",
|
|
25697
25861
|
mode: 384,
|
|
@@ -25708,7 +25872,7 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25708
25872
|
} catch (error) {
|
|
25709
25873
|
await rm3(candidate, { recursive: true, force: true }).catch(() => void 0);
|
|
25710
25874
|
if (claimed) {
|
|
25711
|
-
const failedPath =
|
|
25875
|
+
const failedPath = join5(lockDirectory, `.mutation.failed-${randomUUID2()}`);
|
|
25712
25876
|
try {
|
|
25713
25877
|
const current2 = await readOwner(lockPath);
|
|
25714
25878
|
if (current2?.ownerToken === ownerToken) {
|
|
@@ -25727,7 +25891,7 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25727
25891
|
const leaseExpired = now() - await latestHeartbeat(lockPath, current) > current.leaseMs;
|
|
25728
25892
|
const localOwnerAlive = current.hostname === hostname && isProcessAlive(current.pid);
|
|
25729
25893
|
if (leaseExpired && !localOwnerAlive) {
|
|
25730
|
-
const stalePath =
|
|
25894
|
+
const stalePath = join5(lockDirectory, `.mutation.stale-${randomUUID2()}`);
|
|
25731
25895
|
try {
|
|
25732
25896
|
await rename2(lockPath, stalePath);
|
|
25733
25897
|
await rm3(stalePath, { recursive: true, force: true });
|
|
@@ -25756,7 +25920,7 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25756
25920
|
const heartbeat = async () => {
|
|
25757
25921
|
await assertOwned();
|
|
25758
25922
|
try {
|
|
25759
|
-
await mkdir4(
|
|
25923
|
+
await mkdir4(join5(lockPath, `heartbeat-${ownerToken}-${now()}`), { mode: 448 });
|
|
25760
25924
|
} catch (error) {
|
|
25761
25925
|
if (errorCode(error) !== "EEXIST") {
|
|
25762
25926
|
lost = true;
|
|
@@ -25778,7 +25942,7 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25778
25942
|
if (current === null || current.ownerToken !== ownerToken || current.fencingToken !== fencingToken || current.profile !== profile) {
|
|
25779
25943
|
return;
|
|
25780
25944
|
}
|
|
25781
|
-
const releasePath =
|
|
25945
|
+
const releasePath = join5(lockDirectory, `.mutation.release-${ownerToken}`);
|
|
25782
25946
|
try {
|
|
25783
25947
|
await rename2(lockPath, releasePath);
|
|
25784
25948
|
const detached = await readOwner(releasePath);
|
|
@@ -25807,17 +25971,17 @@ async function acquireProfileLock(home, profile, options = {}) {
|
|
|
25807
25971
|
|
|
25808
25972
|
// src/dsh/profileTransaction.ts
|
|
25809
25973
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
|
|
25810
|
-
import { join as
|
|
25811
|
-
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";
|
|
25812
25976
|
async function profileFingerprint(root) {
|
|
25813
25977
|
const hash = createHash2("sha512");
|
|
25814
25978
|
let files = 0;
|
|
25815
25979
|
let bytes = 0;
|
|
25816
25980
|
const visit = async (directory, prefix) => {
|
|
25817
|
-
const entries = await
|
|
25981
|
+
const entries = await readdir5(directory, { withFileTypes: true });
|
|
25818
25982
|
entries.sort((left, right) => Buffer.from(left.name).compare(Buffer.from(right.name)));
|
|
25819
25983
|
for (const entry of entries) {
|
|
25820
|
-
const path =
|
|
25984
|
+
const path = join6(directory, entry.name);
|
|
25821
25985
|
const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
25822
25986
|
const info = await lstat6(path);
|
|
25823
25987
|
if (info.isSymbolicLink()) throw new CliSafetyError("profile backup contains a symlink");
|
|
@@ -25855,21 +26019,21 @@ async function activeExists(active) {
|
|
|
25855
26019
|
async function recoverTransactions(profiles, active, profile, committedFencingToken, assertLockOwned) {
|
|
25856
26020
|
const prefix = `.dsh-plugins-transaction-${profile}-`;
|
|
25857
26021
|
const journals = [];
|
|
25858
|
-
for (const entry of await
|
|
26022
|
+
for (const entry of await readdir5(profiles, { withFileTypes: true })) {
|
|
25859
26023
|
if (!entry.isFile() || !entry.name.startsWith(prefix) || !entry.name.endsWith(".json")) continue;
|
|
25860
26024
|
try {
|
|
25861
|
-
const value = JSON.parse(await readFile6(
|
|
26025
|
+
const value = JSON.parse(await readFile6(join6(profiles, entry.name), "utf8"));
|
|
25862
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("\\")) {
|
|
25863
26027
|
throw new Error("invalid-journal");
|
|
25864
26028
|
}
|
|
25865
|
-
journals.push({ path:
|
|
26029
|
+
journals.push({ path: join6(profiles, entry.name), value });
|
|
25866
26030
|
} catch {
|
|
25867
26031
|
throw new CliSafetyError("profile transaction journal requires manual recovery");
|
|
25868
26032
|
}
|
|
25869
26033
|
}
|
|
25870
26034
|
journals.sort((left, right) => right.value.fencingToken - left.value.fencingToken);
|
|
25871
26035
|
for (const journal of journals) {
|
|
25872
|
-
const backup =
|
|
26036
|
+
const backup = join6(profiles, journal.value.backupName);
|
|
25873
26037
|
if (journal.value.fencingToken <= committedFencingToken) {
|
|
25874
26038
|
await rm4(backup, { recursive: true, force: true }).catch(() => void 0);
|
|
25875
26039
|
await rm4(journal.path, { force: true }).catch(() => void 0);
|
|
@@ -25894,7 +26058,7 @@ async function beginProfileTransaction(home, profile, options = {}) {
|
|
|
25894
26058
|
await assertLockOwned();
|
|
25895
26059
|
const canonicalHome = await ensureCanonicalHome(home);
|
|
25896
26060
|
const profiles = await ensureContainedDirectory(canonicalHome, "profiles");
|
|
25897
|
-
const active =
|
|
26061
|
+
const active = join6(profiles, profile);
|
|
25898
26062
|
await recoverTransactions(
|
|
25899
26063
|
profiles,
|
|
25900
26064
|
active,
|
|
@@ -25904,8 +26068,8 @@ async function beginProfileTransaction(home, profile, options = {}) {
|
|
|
25904
26068
|
);
|
|
25905
26069
|
const suffix = randomUUID3();
|
|
25906
26070
|
const backupName = `.dsh-plugins-backup-${profile}-${suffix}`;
|
|
25907
|
-
const backup =
|
|
25908
|
-
const journal =
|
|
26071
|
+
const backup = join6(profiles, backupName);
|
|
26072
|
+
const journal = join6(profiles, `.dsh-plugins-transaction-${profile}-${suffix}.json`);
|
|
25909
26073
|
const journalTemporary = `${journal}.tmp`;
|
|
25910
26074
|
const existed = await activeExists(active);
|
|
25911
26075
|
let backupFingerprint = null;
|
|
@@ -25983,9 +26147,9 @@ async function recoverProfileTransaction(home, reference, assertLockOwned) {
|
|
|
25983
26147
|
}
|
|
25984
26148
|
const canonicalHome = await ensureCanonicalHome(home);
|
|
25985
26149
|
const profiles = await ensureContainedDirectory(canonicalHome, "profiles");
|
|
25986
|
-
const journal =
|
|
25987
|
-
const backup =
|
|
25988
|
-
const active =
|
|
26150
|
+
const journal = join6(canonicalHome, reference.journalRelativePath);
|
|
26151
|
+
const backup = join6(canonicalHome, reference.backupRelativePath);
|
|
26152
|
+
const active = join6(profiles, reference.profile);
|
|
25989
26153
|
const displaced = `${backup}.displaced`;
|
|
25990
26154
|
let value;
|
|
25991
26155
|
try {
|
|
@@ -26037,8 +26201,8 @@ async function finalizeProfileTransactionRecovery(home, reference, assertLockOwn
|
|
|
26037
26201
|
assertSafeProfileName(reference.profile);
|
|
26038
26202
|
const canonicalHome = await ensureCanonicalHome(home);
|
|
26039
26203
|
await ensureContainedDirectory(canonicalHome, "profiles");
|
|
26040
|
-
const journal =
|
|
26041
|
-
const backup =
|
|
26204
|
+
const journal = join6(canonicalHome, reference.journalRelativePath);
|
|
26205
|
+
const backup = join6(canonicalHome, reference.backupRelativePath);
|
|
26042
26206
|
await assertLockOwned();
|
|
26043
26207
|
await rm4(backup, { recursive: true, force: true });
|
|
26044
26208
|
await rm4(`${backup}.displaced`, { recursive: true, force: true });
|
|
@@ -26064,7 +26228,7 @@ import {
|
|
|
26064
26228
|
rename as rename4,
|
|
26065
26229
|
rm as rm5
|
|
26066
26230
|
} from "node:fs/promises";
|
|
26067
|
-
import { join as
|
|
26231
|
+
import { join as join7 } from "node:path";
|
|
26068
26232
|
var MutationRecoveryJournalDurabilityAmbiguousError = class extends CliSafetyError {
|
|
26069
26233
|
possiblyPublished = true;
|
|
26070
26234
|
constructor(operation) {
|
|
@@ -26131,8 +26295,8 @@ async function createPaths(home) {
|
|
|
26131
26295
|
const recoveryRoot = await ensureContainedDirectory(canonicalHome, ".dsh-plugins", "recovery");
|
|
26132
26296
|
return {
|
|
26133
26297
|
recoveryRoot,
|
|
26134
|
-
active:
|
|
26135
|
-
marker:
|
|
26298
|
+
active: join7(recoveryRoot, "active"),
|
|
26299
|
+
marker: join7(recoveryRoot, "active", "marker.json")
|
|
26136
26300
|
};
|
|
26137
26301
|
}
|
|
26138
26302
|
async function existingPaths(home) {
|
|
@@ -26143,9 +26307,9 @@ async function existingPaths(home) {
|
|
|
26143
26307
|
if (error.code === "ENOENT") return null;
|
|
26144
26308
|
throw new CliSafetyError("mutation recovery journal could not be inspected safely");
|
|
26145
26309
|
}
|
|
26146
|
-
const dshPlugins =
|
|
26147
|
-
const recoveryRoot =
|
|
26148
|
-
const active =
|
|
26310
|
+
const dshPlugins = join7(canonicalHome, ".dsh-plugins");
|
|
26311
|
+
const recoveryRoot = join7(dshPlugins, "recovery");
|
|
26312
|
+
const active = join7(recoveryRoot, "active");
|
|
26149
26313
|
try {
|
|
26150
26314
|
for (const path of [dshPlugins, recoveryRoot, active]) {
|
|
26151
26315
|
const info = await lstat7(path);
|
|
@@ -26158,7 +26322,7 @@ async function existingPaths(home) {
|
|
|
26158
26322
|
if (error instanceof CliSafetyError) throw error;
|
|
26159
26323
|
throw new CliSafetyError("mutation recovery journal could not be inspected safely");
|
|
26160
26324
|
}
|
|
26161
|
-
return { recoveryRoot, active, marker:
|
|
26325
|
+
return { recoveryRoot, active, marker: join7(active, "marker.json") };
|
|
26162
26326
|
}
|
|
26163
26327
|
function safeRelative(value, prefix) {
|
|
26164
26328
|
return typeof value === "string" && value.startsWith(prefix) && !value.includes("\\") && !value.includes("..") && !value.startsWith("/");
|
|
@@ -26198,7 +26362,7 @@ function parseJournal(value) {
|
|
|
26198
26362
|
return record;
|
|
26199
26363
|
}
|
|
26200
26364
|
async function writeMarker(paths, value, dependencies = {}) {
|
|
26201
|
-
const temporary =
|
|
26365
|
+
const temporary = join7(paths.active, `.marker-${randomUUID4()}.tmp`);
|
|
26202
26366
|
const handle = await open2(temporary, "wx", 384);
|
|
26203
26367
|
try {
|
|
26204
26368
|
await handle.writeFile(`${JSON.stringify(value)}
|
|
@@ -26270,7 +26434,7 @@ async function removeCompletedMutationRecoveryJournal(home, expected, dependenci
|
|
|
26270
26434
|
if (current === null || current.revision !== expected.revision || current.ownerToken !== expected.ownerToken || current.fencingToken !== expected.fencingToken || current.phase !== "completed") {
|
|
26271
26435
|
throw new CliSafetyError("durable recovery intent ownership changed; recovery required");
|
|
26272
26436
|
}
|
|
26273
|
-
const completed =
|
|
26437
|
+
const completed = join7(paths.recoveryRoot, `.completed-${randomUUID4()}`);
|
|
26274
26438
|
await dependencies.beforeRename?.();
|
|
26275
26439
|
await rename4(paths.active, completed);
|
|
26276
26440
|
try {
|
|
@@ -26707,14 +26871,14 @@ import {
|
|
|
26707
26871
|
mkdtemp as mkdtemp2,
|
|
26708
26872
|
open as open3,
|
|
26709
26873
|
readlink,
|
|
26710
|
-
readdir as
|
|
26874
|
+
readdir as readdir6,
|
|
26711
26875
|
realpath as realpath8,
|
|
26712
26876
|
rename as rename5,
|
|
26713
26877
|
rm as rm6,
|
|
26714
26878
|
writeFile as writeFile5
|
|
26715
26879
|
} from "node:fs/promises";
|
|
26716
26880
|
import { devNull } from "node:os";
|
|
26717
|
-
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";
|
|
26718
26882
|
import { pipeline } from "node:stream/promises";
|
|
26719
26883
|
import { createGzip } from "node:zlib";
|
|
26720
26884
|
var PACKAGE_NAME2 = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u;
|
|
@@ -27101,10 +27265,10 @@ async function createDeterministicSourceArchive(sourceRoot, pluginRoot, destinat
|
|
|
27101
27265
|
try {
|
|
27102
27266
|
await emit(tarHeader("package/", 0, 493, "directory"));
|
|
27103
27267
|
const visit = async (directory, relativeDirectory) => {
|
|
27104
|
-
const entries = await
|
|
27268
|
+
const entries = await readdir6(directory, { withFileTypes: true });
|
|
27105
27269
|
entries.sort((left, right) => Buffer.from(left.name).compare(Buffer.from(right.name)));
|
|
27106
27270
|
for (const entry of entries) {
|
|
27107
|
-
const candidate =
|
|
27271
|
+
const candidate = join8(directory, entry.name);
|
|
27108
27272
|
const relativePath = relativeDirectory === "" ? entry.name : `${relativeDirectory}/${entry.name}`;
|
|
27109
27273
|
const archivePath = `package/${relativePath}`;
|
|
27110
27274
|
const info = await lstat8(candidate);
|
|
@@ -27160,8 +27324,8 @@ async function createDeterministicSourceArchive(sourceRoot, pluginRoot, destinat
|
|
|
27160
27324
|
}
|
|
27161
27325
|
async function createArtifactLease(cached, home, budgets) {
|
|
27162
27326
|
const leasesRoot = await ensureContainedDirectory(home, ".dsh-plugins", "artifact-leases");
|
|
27163
|
-
const leaseRoot = await mkdtemp2(
|
|
27164
|
-
const destination =
|
|
27327
|
+
const leaseRoot = await mkdtemp2(join8(leasesRoot, ".lease-"));
|
|
27328
|
+
const destination = join8(leaseRoot, "artifact.tgz");
|
|
27165
27329
|
let heldFd;
|
|
27166
27330
|
try {
|
|
27167
27331
|
const cache = cached[VERIFIED_CACHE];
|
|
@@ -27298,10 +27462,10 @@ async function inspectSourceTree(root, budgets) {
|
|
|
27298
27462
|
if (before.isSymbolicLink() || !before.isDirectory()) {
|
|
27299
27463
|
throw new CliSafetyError("source tree changed during verification");
|
|
27300
27464
|
}
|
|
27301
|
-
const entries = await
|
|
27465
|
+
const entries = await readdir6(directory, { withFileTypes: true });
|
|
27302
27466
|
entries.sort((left, right) => Buffer.from(left.name).compare(Buffer.from(right.name)));
|
|
27303
27467
|
for (const entry of entries) {
|
|
27304
|
-
const candidate =
|
|
27468
|
+
const candidate = join8(directory, entry.name);
|
|
27305
27469
|
const relativePath = relative6(root, candidate).split(sep6).join("/");
|
|
27306
27470
|
const candidateDepth = relativePath.split("/").length;
|
|
27307
27471
|
assertTreeBudget(bytes, files, candidateDepth, budgets);
|
|
@@ -27359,9 +27523,9 @@ async function measureCheckout(root, budgets) {
|
|
|
27359
27523
|
let files = 0;
|
|
27360
27524
|
const visit = async (directory, depth) => {
|
|
27361
27525
|
assertTreeBudget(bytes, files, depth, budgets);
|
|
27362
|
-
const entries = await
|
|
27526
|
+
const entries = await readdir6(directory, { withFileTypes: true });
|
|
27363
27527
|
for (const entry of entries) {
|
|
27364
|
-
const candidate =
|
|
27528
|
+
const candidate = join8(directory, entry.name);
|
|
27365
27529
|
const candidateDepth = depth + 1;
|
|
27366
27530
|
const info = await lstat8(candidate);
|
|
27367
27531
|
if (info.isDirectory() && !info.isSymbolicLink()) {
|
|
@@ -27532,7 +27696,7 @@ async function stageNpm(entry, temporary, fetchImpl, budgets, signal) {
|
|
|
27532
27696
|
throw new CliSafetyError("checksum verification failed");
|
|
27533
27697
|
}
|
|
27534
27698
|
const artifact = "artifact.tgz";
|
|
27535
|
-
await writeFile5(
|
|
27699
|
+
await writeFile5(join8(temporary, artifact), bytes, { mode: 384, flag: "wx" });
|
|
27536
27700
|
return {
|
|
27537
27701
|
packageName: entry.package.name,
|
|
27538
27702
|
installRelativePath: artifact,
|
|
@@ -27545,7 +27709,7 @@ async function stageSource(entry, temporary, runProcess, budgets, signal) {
|
|
|
27545
27709
|
}
|
|
27546
27710
|
if (!SHA402.test(entry.source.commit)) throw new CliSafetyError("source commit pin is invalid");
|
|
27547
27711
|
const subpath = safeSourceSubpath(entry.source.subpath);
|
|
27548
|
-
const source =
|
|
27712
|
+
const source = join8(temporary, "source");
|
|
27549
27713
|
await mkdir6(source, { mode: 448 });
|
|
27550
27714
|
await runGit2(runProcess, { args: ["init", "--quiet"], cwd: source }, budgets, signal);
|
|
27551
27715
|
await runGit2(
|
|
@@ -27592,7 +27756,7 @@ async function stageSource(entry, temporary, runProcess, budgets, signal) {
|
|
|
27592
27756
|
if (checkedCommit.toLowerCase() !== entry.source.commit.toLowerCase()) {
|
|
27593
27757
|
throw new CliSafetyError("source checkout does not match the commit pin");
|
|
27594
27758
|
}
|
|
27595
|
-
await rm6(
|
|
27759
|
+
await rm6(join8(source, ".git"), { recursive: true, force: true });
|
|
27596
27760
|
const tree = await inspectSourceTree(source, budgets);
|
|
27597
27761
|
const pluginRoot = resolve7(source, subpath);
|
|
27598
27762
|
const expectedRelativePath = subpath === "." ? "source" : `source/${subpath}`;
|
|
@@ -27615,7 +27779,7 @@ async function stageSource(entry, temporary, runProcess, budgets, signal) {
|
|
|
27615
27779
|
let packageName;
|
|
27616
27780
|
try {
|
|
27617
27781
|
packageName = JSON.parse((await readRegularFile(
|
|
27618
|
-
|
|
27782
|
+
join8(canonicalPluginRoot, "package.json"),
|
|
27619
27783
|
budgets.metadataBytes,
|
|
27620
27784
|
"source package manifest is unsafe"
|
|
27621
27785
|
)).toString("utf8")).name;
|
|
@@ -27649,7 +27813,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
|
|
|
27649
27813
|
throw new CliSafetyError("staging cache key is invalid");
|
|
27650
27814
|
}
|
|
27651
27815
|
const metadata = JSON.parse((await readRegularFile(
|
|
27652
|
-
|
|
27816
|
+
join8(canonicalRoot2, "metadata.json"),
|
|
27653
27817
|
budgets.metadataBytes,
|
|
27654
27818
|
"staging cache metadata is unsafe"
|
|
27655
27819
|
)).toString("utf8"));
|
|
@@ -27690,7 +27854,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
|
|
|
27690
27854
|
if (metadata.artifact.kind !== "source" || metadata.artifact.commit !== entry.source.commit.toLowerCase() || metadata.installRelativePath !== expectedRelativePath) {
|
|
27691
27855
|
throw new CliSafetyError("staging cache metadata is invalid");
|
|
27692
27856
|
}
|
|
27693
|
-
const sourceRoot =
|
|
27857
|
+
const sourceRoot = join8(canonicalRoot2, "source");
|
|
27694
27858
|
const tree = await inspectSourceTree(sourceRoot, budgets);
|
|
27695
27859
|
if (tree.digest !== metadata.artifact.treeSha512 || tree.bytes !== metadata.artifact.bytes || tree.files !== metadata.artifact.files) {
|
|
27696
27860
|
throw new CliSafetyError("staging cache tree verification failed");
|
|
@@ -27700,7 +27864,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
|
|
|
27700
27864
|
throw new CliSafetyError("staging cache target is unsafe");
|
|
27701
27865
|
}
|
|
27702
27866
|
const packageInfo = JSON.parse((await readRegularFile(
|
|
27703
|
-
|
|
27867
|
+
join8(canonicalTarget, "package.json"),
|
|
27704
27868
|
budgets.metadataBytes,
|
|
27705
27869
|
"staging cache package manifest is unsafe"
|
|
27706
27870
|
)).toString("utf8"));
|
|
@@ -27736,7 +27900,7 @@ async function readCached(finalRoot, id, hash, entry, budgets) {
|
|
|
27736
27900
|
}
|
|
27737
27901
|
}
|
|
27738
27902
|
async function quarantineCache(finalRoot, idRoot, hash) {
|
|
27739
|
-
const quarantine =
|
|
27903
|
+
const quarantine = join8(idRoot, `.quarantine-${hash}-${randomUUID5()}`);
|
|
27740
27904
|
try {
|
|
27741
27905
|
await rename5(finalRoot, quarantine);
|
|
27742
27906
|
} catch (error) {
|
|
@@ -27752,7 +27916,7 @@ async function stageCatalogEntry(entry, options) {
|
|
|
27752
27916
|
const budgets = stageBudgets(options.budgets);
|
|
27753
27917
|
const home = await ensureCanonicalHome(options.dshHome);
|
|
27754
27918
|
const idRoot = await ensureContainedDirectory(home, ".dsh-plugins", "cache", id);
|
|
27755
|
-
const finalRoot =
|
|
27919
|
+
const finalRoot = join8(idRoot, hash);
|
|
27756
27920
|
try {
|
|
27757
27921
|
await lstat8(finalRoot);
|
|
27758
27922
|
try {
|
|
@@ -27774,7 +27938,7 @@ async function stageCatalogEntry(entry, options) {
|
|
|
27774
27938
|
if (options.offline === true) {
|
|
27775
27939
|
throw new CliSafetyError("pinned artifact is not cached and offline mode forbids retrieval");
|
|
27776
27940
|
}
|
|
27777
|
-
const temporary = await mkdtemp2(
|
|
27941
|
+
const temporary = await mkdtemp2(join8(idRoot, ".stage-"));
|
|
27778
27942
|
try {
|
|
27779
27943
|
const staged = entry.package.ecosystem === "npm" ? await stageNpm(
|
|
27780
27944
|
entry,
|
|
@@ -27795,7 +27959,7 @@ async function stageCatalogEntry(entry, options) {
|
|
|
27795
27959
|
descriptorHash: hash,
|
|
27796
27960
|
...staged
|
|
27797
27961
|
};
|
|
27798
|
-
await writeFile5(
|
|
27962
|
+
await writeFile5(join8(temporary, "metadata.json"), `${JSON.stringify(metadata)}
|
|
27799
27963
|
`, {
|
|
27800
27964
|
encoding: "utf8",
|
|
27801
27965
|
mode: 384,
|
|
@@ -27823,7 +27987,7 @@ async function stageCatalogEntry(entry, options) {
|
|
|
27823
27987
|
);
|
|
27824
27988
|
} catch (error) {
|
|
27825
27989
|
if (error instanceof GitProcessUnreapedError) {
|
|
27826
|
-
const quarantine =
|
|
27990
|
+
const quarantine = join8(idRoot, `.unreaped-${hash}-${randomUUID5()}`);
|
|
27827
27991
|
await rename5(temporary, quarantine).catch(() => void 0);
|
|
27828
27992
|
throw error;
|
|
27829
27993
|
}
|
|
@@ -29351,15 +29515,29 @@ function addMutationOptions(command) {
|
|
|
29351
29515
|
"consent to DSH/pnpm lifecycle code (native Windows disabled; use WSL)"
|
|
29352
29516
|
);
|
|
29353
29517
|
}
|
|
29518
|
+
function parseManifestVersion(raw) {
|
|
29519
|
+
const manifest = JSON.parse(raw);
|
|
29520
|
+
const version = manifest.version;
|
|
29521
|
+
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version)) {
|
|
29522
|
+
throw new Error("the package manifest does not declare an exact version");
|
|
29523
|
+
}
|
|
29524
|
+
return version;
|
|
29525
|
+
}
|
|
29354
29526
|
function publishedVersion() {
|
|
29355
|
-
|
|
29356
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
29357
|
-
);
|
|
29358
|
-
return manifest.version;
|
|
29527
|
+
return parseManifestVersion(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
29359
29528
|
}
|
|
29360
29529
|
async function runCli(argv, dependencies = defaultDependencies3) {
|
|
29361
29530
|
let result = 0;
|
|
29362
|
-
|
|
29531
|
+
let version;
|
|
29532
|
+
try {
|
|
29533
|
+
version = publishedVersion();
|
|
29534
|
+
} catch (error) {
|
|
29535
|
+
dependencies.stderr(`${sanitizedErrorMessage(error)}
|
|
29536
|
+
`);
|
|
29537
|
+
dependencies.stderr("Command failed safely; no changes were made.\n");
|
|
29538
|
+
return 1;
|
|
29539
|
+
}
|
|
29540
|
+
const program2 = new Command().name("dsh-plugins").version(version).description("Unofficial catalog and safe installer for DeepSeek Harness plugins.").addHelpText(
|
|
29363
29541
|
"after",
|
|
29364
29542
|
"\nUnofficial community project. Not affiliated with, endorsed by, or sponsored by DeepSeek.\nNative Windows add/update/remove with code execution are disabled; use WSL.\nDry-run and read-only catalog/search/info/list/doctor commands remain available.\nNative Windows recovery markers require documented manual recovery.\n"
|
|
29365
29543
|
).configureOutput({
|
|
@@ -29371,8 +29549,8 @@ async function runCli(argv, dependencies = defaultDependencies3) {
|
|
|
29371
29549
|
addCatalogOptions(catalog.command("validate").description("validate catalog YAML and semantics")).action(async (options) => {
|
|
29372
29550
|
result = await validateCatalogCommand(dependencies, selection(options), options.json === true);
|
|
29373
29551
|
});
|
|
29374
|
-
catalog.command("docs-check").description("check required public catalog documentation").argument("[root]", "public repository root", ".").action(async (root) => {
|
|
29375
|
-
result = await docsCheckCommand(dependencies, root);
|
|
29552
|
+
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) => {
|
|
29553
|
+
result = await docsCheckCommand(dependencies, root, options);
|
|
29376
29554
|
});
|
|
29377
29555
|
catalog.command("github-forms-check").description("check structured public GitHub issue forms").argument("[root]", "public repository root", ".").action(async (root) => {
|
|
29378
29556
|
result = await githubFormsCheckCommand(dependencies, root);
|