@hugojosefson/cli 0.13.0 → 0.14.1
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/esm/deno.js +5 -1
- package/esm/src/cli/partition-readme-contributions.js +1 -1
- package/esm/src/features/readme-badge-layout.js +16 -0
- package/esm/src/features/readme-build-checks.js +3 -1
- package/esm/src/features/readme-build-detection.js +4 -0
- package/esm/src/features/readme-build-plans.js +2 -1
- package/esm/src/features/readme-contribution-plans.js +4 -0
- package/esm/src/features/readme-static-feature.js +4 -0
- package/esm/src/features/readme-static-operations.js +33 -1
- package/esm/src/readme/badge-layout.js +150 -0
- package/esm/src/readme/contribution-blocks.js +8 -20
- package/package.json +3 -3
package/esm/deno.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
"name": "@hugojosefson/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"hj": {
|
|
6
6
|
"commandName": "hj"
|
|
@@ -170,6 +170,10 @@ export default {
|
|
|
170
170
|
"publish-check",
|
|
171
171
|
"readme-check"
|
|
172
172
|
]
|
|
173
|
+
},
|
|
174
|
+
"validation-compare": {
|
|
175
|
+
"description": "Compare two fresh validation observation reports without reusing test results",
|
|
176
|
+
"command": "deno run --allow-read scripts/compare-validation.ts"
|
|
173
177
|
}
|
|
174
178
|
},
|
|
175
179
|
"coverage": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { contributionPattern as pattern } from "../readme/badge-layout.js";
|
|
2
2
|
/** Restore reached owners when a later captured snapshot predates their block. */
|
|
3
3
|
export function partitionReadmeContributions(baseline, features, final) {
|
|
4
4
|
const provider = features.find((feature) => feature.plan.featureId === "readme-build" &&
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { badgeLayoutResolution, layoutBadges } from "../readme/badge-layout.js";
|
|
2
|
+
export function badgeLayoutDetection(text, path) {
|
|
3
|
+
if (layoutBadges(text) === text)
|
|
4
|
+
return undefined;
|
|
5
|
+
return {
|
|
6
|
+
state: "drifted",
|
|
7
|
+
evidence: [],
|
|
8
|
+
issues: [{
|
|
9
|
+
code: "readme-badge-layout",
|
|
10
|
+
kind: "readme",
|
|
11
|
+
subject: { kind: "repository-path", identifier: path },
|
|
12
|
+
observation: `${path} badges are not one ordered row after the first paragraph.`,
|
|
13
|
+
resolution: badgeLayoutResolution(text, path),
|
|
14
|
+
}],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** @module Generated README operation safety checks. */
|
|
2
|
+
import { layoutBadges } from "../readme/badge-layout.js";
|
|
2
3
|
import { fileAccess } from "../repository/file-access.js";
|
|
3
4
|
import { sameJson } from "../operations/local-plan-state.js";
|
|
4
5
|
import { denoTaskDefinitions, readmeTaskDefinition } from "./deno-tasks.js";
|
|
5
6
|
import { inspectReadmeBuild, readmeBuildDirectoryPath, readmeBuildFeatureId, readmeBuildRootPath, } from "./readme-build-state.js";
|
|
6
7
|
export async function checkEnableReadmeBuild(context) {
|
|
7
8
|
const state = await inspectReadmeBuild(context);
|
|
8
|
-
if (exact(state)
|
|
9
|
+
if (exact(state) && state.source.kind === "file" &&
|
|
10
|
+
layoutBadges(state.source.content) === state.source.content) {
|
|
9
11
|
return noOp("Generated README is already adopted.");
|
|
10
12
|
}
|
|
11
13
|
if (state.legacy.kind === "conflict")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @module Generated README feature detection. */
|
|
2
|
+
import { badgeLayoutDetection } from "./readme-badge-layout.js";
|
|
2
3
|
import { fileAccess } from "../repository/file-access.js";
|
|
3
4
|
import { inspectReadmeBuild, readmeBuildRootPath, } from "./readme-build-state.js";
|
|
4
5
|
/** Detects exact generated README source, task, aggregate, output, and mode. */
|
|
@@ -35,6 +36,9 @@ export async function detectReadmeBuild(context) {
|
|
|
35
36
|
if (state.exactTask && state.exactDefault && state.rootMatches &&
|
|
36
37
|
state.root.kind === "file" && !fileAccess(state.root).writable &&
|
|
37
38
|
state.source.kind === "file" && fileAccess(state.source).writable) {
|
|
39
|
+
const badges = badgeLayoutDetection(state.source.content, "readme/README.md");
|
|
40
|
+
if (badges)
|
|
41
|
+
return badges;
|
|
38
42
|
return {
|
|
39
43
|
state: "enabled",
|
|
40
44
|
evidence: [{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @module Guarded generated README change plans. */
|
|
2
|
+
import { layoutBadges } from "../readme/badge-layout.js";
|
|
2
3
|
import { fileAccess, repairFileMode } from "../repository/file-access.js";
|
|
3
4
|
import { legacyReadmePreview } from "./readme-build-checks.js";
|
|
4
5
|
import { denoTaskDefinitions, readmeTaskDefinition } from "./deno-tasks.js";
|
|
@@ -21,7 +22,7 @@ export async function planEnableReadmeBuild(context, allowed) {
|
|
|
21
22
|
const initialSource = state.source.kind === "file"
|
|
22
23
|
? state.source.content
|
|
23
24
|
: state.initialSource;
|
|
24
|
-
const sourceContent = buildSourceContent(context, state.legacy.kind === "recognized" ? state.legacy.source : initialSource);
|
|
25
|
+
const sourceContent = layoutBadges(buildSourceContent(context, state.legacy.kind === "recognized" ? state.legacy.source : initialSource));
|
|
25
26
|
if (state.source.kind === "absent") {
|
|
26
27
|
changes.push({ kind: "create-directory", path: readmeBuildDirectoryPath }, {
|
|
27
28
|
kind: "write-file",
|
|
@@ -140,6 +140,10 @@ export async function reconcileReadmePlans(context, plans) {
|
|
|
140
140
|
?.enabled !== false &&
|
|
141
141
|
(!npmName || context.detections.get(npmBadgeOwner)?.state === "ambiguous" ||
|
|
142
142
|
context.detections.get(npmBadgeOwner) === undefined);
|
|
143
|
+
// Ambiguous badge ownership cannot safely be composed with other README edits.
|
|
144
|
+
if (context.detections.get(npmBadgeOwner)?.state === "ambiguous" &&
|
|
145
|
+
preserveNpmBadge)
|
|
146
|
+
return plans;
|
|
143
147
|
const provider = build ? "readme-build" : "readme-static";
|
|
144
148
|
// Remove unchanged legacy JSR API blocks before the library adds its block.
|
|
145
149
|
const owners = [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @module Built-in exact starter README feature. */
|
|
2
|
+
import { badgeLayoutDetection } from "./readme-badge-layout.js";
|
|
2
3
|
import { fileAccess } from "../repository/file-access.js";
|
|
3
4
|
import { inspectReadmeStatic, readmeStaticFeatureId, readmeStaticIssue, readmeStaticSubject, } from "./readme-static-artifact.js";
|
|
4
5
|
import { checkDisableReadmeStatic, checkEnableReadmeStatic, planDisableReadmeStatic, planEnableReadmeStatic, } from "./readme-static-operations.js";
|
|
@@ -18,6 +19,9 @@ async function detectReadmeStatic(context) {
|
|
|
18
19
|
if (inspection.result !== "unreadable" &&
|
|
19
20
|
inspection.observation.kind === "file" &&
|
|
20
21
|
fileAccess(inspection.observation).writable) {
|
|
22
|
+
const badges = badgeLayoutDetection(inspection.observation.content, "README.md");
|
|
23
|
+
if (badges)
|
|
24
|
+
return badges;
|
|
21
25
|
return {
|
|
22
26
|
state: "enabled",
|
|
23
27
|
evidence: [{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @module Exact starter README checks and plans. */
|
|
2
|
+
import { layoutBadges } from "../readme/badge-layout.js";
|
|
2
3
|
import { fileAccess } from "../repository/file-access.js";
|
|
3
4
|
import { planArtifactCreation, planArtifactRemoval, } from "../artifacts/plan-artifact-change.js";
|
|
4
5
|
import { inspectReadmeStatic, readmeStaticBlocker, readmeStaticFeatureId, readmeStaticPath, } from "./readme-static-artifact.js";
|
|
@@ -12,6 +13,26 @@ export async function checkEnableReadmeStatic(context) {
|
|
|
12
13
|
if (inspection.result !== "absent" && inspection.result !== "unreadable" &&
|
|
13
14
|
inspection.observation.kind === "file" &&
|
|
14
15
|
fileAccess(inspection.observation).writable) {
|
|
16
|
+
if (layoutBadges(inspection.observation.content) !==
|
|
17
|
+
inspection.observation.content) {
|
|
18
|
+
return context.repair?.kind === "all-drifted" ||
|
|
19
|
+
context.repair?.kind === "features" &&
|
|
20
|
+
context.repair.featureIds.includes(readmeStaticFeatureId)
|
|
21
|
+
? allowed()
|
|
22
|
+
: {
|
|
23
|
+
result: "blocked",
|
|
24
|
+
warnings: [],
|
|
25
|
+
blockers: [{
|
|
26
|
+
code: "readme-badge-layout",
|
|
27
|
+
message: "README.md badge layout differs.",
|
|
28
|
+
subjects: [{
|
|
29
|
+
kind: "repository-path",
|
|
30
|
+
identifier: readmeStaticPath,
|
|
31
|
+
}],
|
|
32
|
+
resolution: "Use --repair to move the badges after the first paragraph.",
|
|
33
|
+
}],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
15
36
|
return {
|
|
16
37
|
result: "no-op",
|
|
17
38
|
reason: "README.md is already writable.",
|
|
@@ -90,7 +111,18 @@ export async function checkDisableReadmeStatic(context) {
|
|
|
90
111
|
export async function planEnableReadmeStatic(context, allowed) {
|
|
91
112
|
if (replacesBuild(context))
|
|
92
113
|
return changePlan("enable", allowed, []);
|
|
93
|
-
const
|
|
114
|
+
const inspection = await inspectReadmeStatic(context);
|
|
115
|
+
if (inspection.result !== "absent" && inspection.result !== "unreadable" &&
|
|
116
|
+
inspection.observation.kind === "file" &&
|
|
117
|
+
fileAccess(inspection.observation).writable) {
|
|
118
|
+
return changePlan("enable", allowed, [{
|
|
119
|
+
kind: "write-file",
|
|
120
|
+
path: readmeStaticPath,
|
|
121
|
+
content: layoutBadges(inspection.observation.content),
|
|
122
|
+
expectedDigest: inspection.observation.digest,
|
|
123
|
+
}]);
|
|
124
|
+
}
|
|
125
|
+
const plan = planArtifactCreation(inspection);
|
|
94
126
|
const repair = await planRepairReadmeStatic(context, allowed);
|
|
95
127
|
if (repair)
|
|
96
128
|
return repair;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** @module Shared README badge ordering and placement without claiming custom badges. */
|
|
2
|
+
/** Markers may surround block content or appear inline between badges. */
|
|
3
|
+
export const contributionPattern = /<!-- hj:readme ([\w:-]+) ([a-f0-9]+) -->([\s\S]*?)<!-- \/hj:readme -->/g;
|
|
4
|
+
const start = "<!-- deno-fmt-ignore-start -->";
|
|
5
|
+
const end = "<!-- deno-fmt-ignore-end -->";
|
|
6
|
+
const target = String.raw `\((?:[^()\n]|\([^()\n]*\))*\)|\[[^\]\n]*\]`;
|
|
7
|
+
const image = String.raw `!\[[^\]\n]*\](?:${target})`;
|
|
8
|
+
const badgePattern = new RegExp(String
|
|
9
|
+
.raw `\[${image}\](?:${target})|${image}|<a\b[^>]*>\s*<img\b[^>]*>\s*</a>|<img\b[^>]*>`, "gi");
|
|
10
|
+
function isBadge(text, document) {
|
|
11
|
+
const references = [...text.matchAll(/\[([^\]\n]+)\]/g)].map((match) => match[1].toLowerCase());
|
|
12
|
+
const destinations = document.split("\n").filter((line) => {
|
|
13
|
+
const label = /^ {0,3}\[([^\]]+)\]:/.exec(line)?.[1].toLowerCase();
|
|
14
|
+
return label && references.includes(label);
|
|
15
|
+
}).join(" ");
|
|
16
|
+
return /badge|shields\.io|badgen\.net|coveralls\.io/i.test(text + destinations) ||
|
|
17
|
+
/(?:!\[|alt=["'])(?:ci|npm|jsr|build|status|coverage|license|version|score)(?:[\]"' ]|$)/i
|
|
18
|
+
.test(text);
|
|
19
|
+
}
|
|
20
|
+
/** Keep fenced examples, inline code, comments and non-badge owned sections intact. */
|
|
21
|
+
function visibleText(text) {
|
|
22
|
+
let fence;
|
|
23
|
+
return text.split(/(?<=\n)/).map((line) => {
|
|
24
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
25
|
+
const hidden = !!fence || !!marker || /^(?: {4}|\t)/.test(line);
|
|
26
|
+
if (marker) {
|
|
27
|
+
if (!fence)
|
|
28
|
+
fence = marker;
|
|
29
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) {
|
|
30
|
+
fence = undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return hidden ? line.replace(/[^\n]/g, " ") : line;
|
|
34
|
+
}).join("").replace(/(`+)[^\n]*?\1/g, (code) => " ".repeat(code.length));
|
|
35
|
+
}
|
|
36
|
+
function badges(text) {
|
|
37
|
+
let visible = visibleText(text);
|
|
38
|
+
const result = [];
|
|
39
|
+
for (const match of visible.matchAll(contributionPattern)) {
|
|
40
|
+
const body = match[3].trim();
|
|
41
|
+
if (/:badges?$/.test(match[1]) && body.replace(badgePattern, "").trim() === "") {
|
|
42
|
+
result.push({
|
|
43
|
+
start: match.index,
|
|
44
|
+
end: match.index + match[0].length,
|
|
45
|
+
text: text.slice(match.index, match.index + match[0].length),
|
|
46
|
+
owner: match[1].split(":")[0],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
visible = visible.slice(0, match.index) +
|
|
50
|
+
match[0].replace(/[^\n]/g, " ") +
|
|
51
|
+
visible.slice(match.index + match[0].length);
|
|
52
|
+
}
|
|
53
|
+
visible = visible.replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
|
|
54
|
+
for (const match of visible.matchAll(badgePattern)) {
|
|
55
|
+
// A linked screenshot is still an illustration. Recognize badge labels,
|
|
56
|
+
// provider URLs and reference destinations without adopting their ownership.
|
|
57
|
+
if (!isBadge(match[0], text))
|
|
58
|
+
continue;
|
|
59
|
+
result.push({
|
|
60
|
+
start: match.index,
|
|
61
|
+
end: match.index + match[0].length,
|
|
62
|
+
text: text.slice(match.index, match.index + match[0].length),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return result.sort((a, b) => a.start - b.start);
|
|
66
|
+
}
|
|
67
|
+
function order(badge) {
|
|
68
|
+
const owner = badge.owner === "jsr-package"
|
|
69
|
+
? "github-release-publish-jsr"
|
|
70
|
+
: badge.owner;
|
|
71
|
+
return owner?.startsWith("github-release-publish-")
|
|
72
|
+
? `0:${owner}`
|
|
73
|
+
: owner
|
|
74
|
+
? `1:${owner}`
|
|
75
|
+
: "2:";
|
|
76
|
+
}
|
|
77
|
+
/** Locate the first prose paragraph, skipping titles, metadata and other blocks. */
|
|
78
|
+
function paragraphEnd(text) {
|
|
79
|
+
const visible = visibleText(text).replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
|
|
80
|
+
let offset = 0;
|
|
81
|
+
let frontmatter = visible.startsWith("---\n");
|
|
82
|
+
for (const block of visible.split(/\n[ \t]*\n/)) {
|
|
83
|
+
const at = visible.indexOf(block, offset);
|
|
84
|
+
offset = at + block.length;
|
|
85
|
+
if (frontmatter) {
|
|
86
|
+
const close = block.indexOf("\n---", 3);
|
|
87
|
+
if (close >= 0)
|
|
88
|
+
frontmatter = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const body = block.trim();
|
|
92
|
+
// A section body is not the README introduction. With no introduction,
|
|
93
|
+
// place badges after the title instead of nesting them inside an owned guide.
|
|
94
|
+
if (/^#{2,6}\s/.test(body))
|
|
95
|
+
break;
|
|
96
|
+
if (!body ||
|
|
97
|
+
/^(?:#{1,6}\s|[>|]|[-*+]\s|\d+[.)]\s|@@include\(|<|\[[^\]]+\]:)/.test(body) || /\n[=-]+\s*$/.test(body))
|
|
98
|
+
continue;
|
|
99
|
+
const containing = [...text.matchAll(contributionPattern)].find((match) => match.index < offset && offset < match.index + match[0].length);
|
|
100
|
+
return containing ? containing.index + containing[0].length : offset;
|
|
101
|
+
}
|
|
102
|
+
const title = /^# .*(?:\n|$)/m.exec(visible);
|
|
103
|
+
return title ? title.index + title[0].trimEnd().length : 0;
|
|
104
|
+
}
|
|
105
|
+
/** Returns a stable single-line badge row directly after the introduction. */
|
|
106
|
+
export function layoutBadges(text) {
|
|
107
|
+
const found = badges(text);
|
|
108
|
+
if (!found.length) {
|
|
109
|
+
return text.replace(new RegExp(`${start}\\s*${end}\\n?`, "g"), "");
|
|
110
|
+
}
|
|
111
|
+
const removed = "\0hj-badge\0";
|
|
112
|
+
let remaining = text;
|
|
113
|
+
for (const badge of [...found].reverse()) {
|
|
114
|
+
remaining = remaining.slice(0, badge.start) + removed +
|
|
115
|
+
remaining.slice(badge.end);
|
|
116
|
+
}
|
|
117
|
+
// Retire only a formatter wrapper containing badges and whitespace.
|
|
118
|
+
remaining = remaining.replace(new RegExp(`${start}\\s*(?:${removed}\\s*)+${end}`, "g"), removed);
|
|
119
|
+
const gaps = new RegExp(`[ \\t\\r\\n]*(?:${removed}[ \\t\\r\\n]*)+`, "g");
|
|
120
|
+
remaining = remaining.replace(gaps, (gap, at) => {
|
|
121
|
+
if (gap.includes("\n")) {
|
|
122
|
+
return at === 0
|
|
123
|
+
? ""
|
|
124
|
+
: at + gap.length === remaining.length
|
|
125
|
+
? "\n"
|
|
126
|
+
: "\n\n";
|
|
127
|
+
}
|
|
128
|
+
return /\s/.test(gap) ? " " : "";
|
|
129
|
+
});
|
|
130
|
+
const row = found.sort((a, b) => order(a).localeCompare(order(b)))
|
|
131
|
+
.map((badge) => badge.text.replace(/\s+/g, " ").trim()).join(" ");
|
|
132
|
+
// An initial comment must be a separate HTML block so the badges render as Markdown.
|
|
133
|
+
const rendered = row.replace(/^(<!-- hj:readme [\w:-]+ [a-f0-9]+ -->) /, "$1\n\n");
|
|
134
|
+
const at = paragraphEnd(remaining);
|
|
135
|
+
const before = remaining.slice(0, at).trimEnd();
|
|
136
|
+
const after = remaining.slice(at).replace(/^\s*\n/, "");
|
|
137
|
+
const result = (before ? before + "\n\n" : "") + start + "\n" + rendered +
|
|
138
|
+
"\n" +
|
|
139
|
+
end + "\n" +
|
|
140
|
+
(after ? "\n" + after : "");
|
|
141
|
+
const withoutWrapper = (value) => value.replaceAll(start + "\n", "").replaceAll("\n" + end, "");
|
|
142
|
+
return withoutWrapper(result) === withoutWrapper(text) ? text : result;
|
|
143
|
+
}
|
|
144
|
+
/** Specific repair details, including the resulting badge identities and order. */
|
|
145
|
+
export function badgeLayoutResolution(text, path) {
|
|
146
|
+
const names = badges(text).sort((a, b) => order(a).localeCompare(order(b)))
|
|
147
|
+
.map((badge) => badge.owner ?? badge.text);
|
|
148
|
+
return `Move all badges in ${path} onto one horizontal line immediately after the first paragraph. ` +
|
|
149
|
+
`Set badge order to: ${names.join("; ")}. Preserve badge links and ownership comments.`;
|
|
150
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @module Individually owned README blocks that preserve user edits. */
|
|
2
2
|
import { digestBytes } from "../repository/digest-bytes.js";
|
|
3
|
-
|
|
3
|
+
import { contributionPattern as pattern, layoutBadges, } from "./badge-layout.js";
|
|
4
4
|
export async function blockHash(content) {
|
|
5
5
|
return await digestBytes(new TextEncoder().encode(content));
|
|
6
6
|
}
|
|
@@ -53,9 +53,8 @@ export async function reconcileBlocks(text, desired, owner) {
|
|
|
53
53
|
text = text.replace(match[0], await wrap(item));
|
|
54
54
|
else {
|
|
55
55
|
const at = text.indexOf(match[0]);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
text = before + (after ? "\n\n" + after : "\n");
|
|
56
|
+
text = text.slice(0, at) + text.slice(at + match[0].length);
|
|
57
|
+
text = text.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
for (const item of pending.values()) {
|
|
@@ -72,23 +71,12 @@ export async function reconcileBlocks(text, desired, owner) {
|
|
|
72
71
|
? text.includes("[![JSR")
|
|
73
72
|
: item.id === "github-ci:badge"
|
|
74
73
|
? text.includes("[![CI]")
|
|
75
|
-
:
|
|
74
|
+
: item.id === "github-release-publish-npm:badge" &&
|
|
75
|
+
npmBadgeImages(text).length > 0))
|
|
76
76
|
continue;
|
|
77
77
|
const block = await wrap(item);
|
|
78
78
|
if (item.position === "badges") {
|
|
79
|
-
|
|
80
|
-
const marker = text.indexOf("<!-- hj:readme ");
|
|
81
|
-
let at = Math.min(section < 0 ? text.length : section, marker < 0 ? text.length : marker);
|
|
82
|
-
const order = [
|
|
83
|
-
"jsr-package:badges",
|
|
84
|
-
"github-release-publish-npm:badge",
|
|
85
|
-
"github-ci:badge",
|
|
86
|
-
];
|
|
87
|
-
const earlier = [...text.matchAll(pattern)].filter((match) => order.indexOf(match[1]) >= 0 &&
|
|
88
|
-
order.indexOf(match[1]) < order.indexOf(item.id)).at(-1);
|
|
89
|
-
if (earlier)
|
|
90
|
-
at = earlier.index + earlier[0].length;
|
|
91
|
-
text = insert(text, at, block);
|
|
79
|
+
text = insert(text, text.length, block);
|
|
92
80
|
}
|
|
93
81
|
else {
|
|
94
82
|
const order = [
|
|
@@ -104,10 +92,10 @@ export async function reconcileBlocks(text, desired, owner) {
|
|
|
104
92
|
text = insert(text, at, block);
|
|
105
93
|
}
|
|
106
94
|
}
|
|
107
|
-
return text;
|
|
95
|
+
return layoutBadges(text);
|
|
108
96
|
}
|
|
109
97
|
async function wrap(item) {
|
|
110
|
-
return `<!-- hj:readme ${item.id} ${await contributionHash(item.content)} -->\n${item.content}\n<!-- /hj:readme
|
|
98
|
+
return `<!-- hj:readme ${item.id} ${await contributionHash(item.content)} -->\n${item.content}\n<!-- /hj:readme -->`;
|
|
111
99
|
}
|
|
112
100
|
function insert(text, at, block) {
|
|
113
101
|
const before = text.slice(0, at).replace(/\n*$/, "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hugojosefson/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Repository tools for Deno, Node.js and Bun.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"bin": {
|
|
11
11
|
"hj": "./esm/hj.js"
|
|
12
12
|
},
|
|
13
|
-
"gitHead": "
|
|
13
|
+
"gitHead": "c532d6081af46b7cbadf62d754784ed5baa3b158",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=24.0.0",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"access": "public",
|
|
30
30
|
"registry": "https://registry.npmjs.org/"
|
|
31
31
|
},
|
|
32
|
-
"hjNpmArchive": "hugojosefson-cli-0.
|
|
32
|
+
"hjNpmArchive": "hugojosefson-cli-0.14.1.tgz",
|
|
33
33
|
"_generatedBy": "dnt@0.43.2",
|
|
34
34
|
"files": [
|
|
35
35
|
"esm",
|