lopebase 0.6.4 → 0.6.6
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 +29 -0
- package/dist/lopebase.js +308 -12
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -89,6 +89,35 @@ Made for web products, and fine with anything else:
|
|
|
89
89
|
| `npx lopebase mcp` | Run the LopeBase tools as a local MCP server (stdio) for any agent. |
|
|
90
90
|
| `npx lopebase adapter secret` | Print a new signing secret for [@lopebase/adapter](https://www.npmjs.com/package/@lopebase/adapter). Pipe it straight into your secret store. |
|
|
91
91
|
| `npx lopebase adapter check <url>` | Test a running adapter, local or deployed, before you connect it. |
|
|
92
|
+
| `npx lopebase blog check [dir]` | Check the blog posts LopeBase publishes to your repo. Exits 1 on any problem, for CI. `--format <preset>`, `--allow-empty`. |
|
|
93
|
+
|
|
94
|
+
## Check published blog posts in CI
|
|
95
|
+
|
|
96
|
+
When LopeBase publishes articles to your GitHub repo, add this to CI so a
|
|
97
|
+
malformed post fails the check instead of breaking your site build:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
npx lopebase blog check --format astro
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
It reads only the post files in one folder, with no network calls, and prints
|
|
104
|
+
one line per problem as `<file>: <problem>` plus a summary.
|
|
105
|
+
|
|
106
|
+
| Preset | Folder | File name |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `markdown` (default) | `content/blog` | `<slug>.md` |
|
|
109
|
+
| `next-mdx` | `content/blog` | `<slug>.mdx` |
|
|
110
|
+
| `astro` | `src/content/blog` | `<slug>.md` |
|
|
111
|
+
| `hugo` | `content/posts` | `<slug>.md` |
|
|
112
|
+
| `jekyll` | `_posts` | `YYYY-MM-DD-<slug>.md` |
|
|
113
|
+
|
|
114
|
+
Pass a folder to check somewhere else. For each post it checks the front
|
|
115
|
+
matter (required fields, quoted strings, dates, tag and image lists), that the
|
|
116
|
+
slug is lowercase words joined by hyphens and matches the file name, that the
|
|
117
|
+
body has at least one `##` heading, does not open with a `#` heading, and has
|
|
118
|
+
no raw HTML outside code, and that no two posts share a slug. A missing or
|
|
119
|
+
empty folder fails too, unless you pass `--allow-empty` (useful before the
|
|
120
|
+
first post lands).
|
|
92
121
|
|
|
93
122
|
## What it reads, and what it keeps
|
|
94
123
|
|
package/dist/lopebase.js
CHANGED
|
@@ -502,7 +502,7 @@ var require_cross_spawn = __commonJS(function(exports, module) {
|
|
|
502
502
|
|
|
503
503
|
// bin/lopebase.ts
|
|
504
504
|
import { spawn as spawn2 } from "node:child_process";
|
|
505
|
-
import { readFileSync as
|
|
505
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
506
506
|
|
|
507
507
|
// ../../node_modules/.bun/@clack+prompts@0.11.0/node_modules/@clack/prompts/dist/index.mjs
|
|
508
508
|
import { stripVTControlCharacters as S2 } from "node:util";
|
|
@@ -1206,7 +1206,18 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
|
1206
1206
|
};
|
|
1207
1207
|
|
|
1208
1208
|
// ../admin-adapter/src/releases.ts
|
|
1209
|
-
var ADAPTER_VERSION = "0.4.
|
|
1209
|
+
var ADAPTER_VERSION = "0.4.2", ADAPTER_RELEASES = [
|
|
1210
|
+
{
|
|
1211
|
+
version: "0.4.2",
|
|
1212
|
+
date: "2026-11-03",
|
|
1213
|
+
features: [
|
|
1214
|
+
"A service check that fails is tried once more within its time limit before the health address reports it down, so one dropped database query no longer marks the product down. cachedCheck retries the same way before it keeps a failure.",
|
|
1215
|
+
"When health() reports ok: false and no services are declared, the health address now answers 503, so LopeBase sees the product down instead of up.",
|
|
1216
|
+
"A health() hook that throws is reported as unhealthy instead of crashing the request.",
|
|
1217
|
+
"Every answer is sent with Cache-Control: no-store, so a CDN in front of the app never serves an old health result or caches rows."
|
|
1218
|
+
],
|
|
1219
|
+
steps: []
|
|
1220
|
+
},
|
|
1210
1221
|
{
|
|
1211
1222
|
version: "0.4.1",
|
|
1212
1223
|
date: "2026-10-08",
|
|
@@ -1290,7 +1301,7 @@ function releasesSince(version) {
|
|
|
1290
1301
|
function upgradePrompt(fromVersion) {
|
|
1291
1302
|
let todo = releasesSince(fromVersion), breaking = todo.flatMap((r2) => r2.breaking ?? []), steps = todo.flatMap((r2) => r2.steps);
|
|
1292
1303
|
return [
|
|
1293
|
-
`Update this app's LopeBase adapter (@lopebase/adapter) from ${fromVersion ?? "its current version"} to 0.4.
|
|
1304
|
+
`Update this app's LopeBase adapter (@lopebase/adapter) from ${fromVersion ?? "its current version"} to 0.4.2.`,
|
|
1294
1305
|
"",
|
|
1295
1306
|
"1. Install it with this repo's package manager, for example `npm install @lopebase/adapter@latest`. A 0.x range like ^0.1.2 does not pick up 0.2 or later on its own.",
|
|
1296
1307
|
...breaking.length ? [
|
|
@@ -1654,12 +1665,270 @@ function installedAgents() {
|
|
|
1654
1665
|
return AGENTS.filter((a) => isInstalled(a.bin));
|
|
1655
1666
|
}
|
|
1656
1667
|
|
|
1668
|
+
// src/blog-check.ts
|
|
1669
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
1670
|
+
import { join } from "node:path";
|
|
1671
|
+
// ../lopebase-blog/src/contract.ts
|
|
1672
|
+
var shared = {
|
|
1673
|
+
title: { kind: "string", required: !0, role: "title" },
|
|
1674
|
+
description: { kind: "string", required: !0, role: "description" },
|
|
1675
|
+
author: { kind: "string", role: "author" },
|
|
1676
|
+
tags: { kind: "tags", role: "tags" }
|
|
1677
|
+
}, markdownKeys = {
|
|
1678
|
+
...shared,
|
|
1679
|
+
date: { kind: "date", required: !0, role: "date" },
|
|
1680
|
+
slug: { kind: "string", required: !0, role: "slug" },
|
|
1681
|
+
updated: { kind: "date", role: "updated" },
|
|
1682
|
+
image: { kind: "url", role: "image" },
|
|
1683
|
+
image_alt: { kind: "string", role: "imageAlt" },
|
|
1684
|
+
faq: { kind: "faq", role: "faq" }
|
|
1685
|
+
}, BLOG_PRESETS = {
|
|
1686
|
+
markdown: {
|
|
1687
|
+
dir: "content/blog",
|
|
1688
|
+
ext: ".md",
|
|
1689
|
+
keys: markdownKeys,
|
|
1690
|
+
slugKey: "slug"
|
|
1691
|
+
},
|
|
1692
|
+
"next-mdx": {
|
|
1693
|
+
dir: "content/blog",
|
|
1694
|
+
ext: ".mdx",
|
|
1695
|
+
keys: markdownKeys,
|
|
1696
|
+
slugKey: "slug"
|
|
1697
|
+
},
|
|
1698
|
+
astro: {
|
|
1699
|
+
dir: "src/content/blog",
|
|
1700
|
+
ext: ".md",
|
|
1701
|
+
keys: {
|
|
1702
|
+
...shared,
|
|
1703
|
+
pubDate: { kind: "date", required: !0, role: "date" },
|
|
1704
|
+
updatedDate: { kind: "date", role: "updated" },
|
|
1705
|
+
heroImage: { kind: "url", role: "image" }
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
hugo: {
|
|
1709
|
+
dir: "content/posts",
|
|
1710
|
+
ext: ".md",
|
|
1711
|
+
keys: {
|
|
1712
|
+
...shared,
|
|
1713
|
+
date: { kind: "datetime", required: !0, role: "date" },
|
|
1714
|
+
slug: { kind: "string", required: !0, role: "slug" },
|
|
1715
|
+
lastmod: { kind: "datetime", role: "updated" },
|
|
1716
|
+
images: { kind: "urls", role: "images" }
|
|
1717
|
+
},
|
|
1718
|
+
slugKey: "slug"
|
|
1719
|
+
},
|
|
1720
|
+
jekyll: {
|
|
1721
|
+
dir: "_posts",
|
|
1722
|
+
ext: ".md",
|
|
1723
|
+
keys: {
|
|
1724
|
+
layout: { kind: "layout", required: !0 },
|
|
1725
|
+
...shared,
|
|
1726
|
+
date: { kind: "date", required: !0, role: "date" },
|
|
1727
|
+
last_modified_at: { kind: "date", role: "updated" },
|
|
1728
|
+
image: { kind: "url", role: "image" }
|
|
1729
|
+
},
|
|
1730
|
+
datedFileKey: "date"
|
|
1731
|
+
}
|
|
1732
|
+
}, isBlogFormat = (v2) => Object.hasOwn(BLOG_PRESETS, v2), SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/, DATE = /^(\d{4})-(\d{2})-(\d{2})$/, DATETIME = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, JEKYLL_FILE = /^(\d{4}-\d{2}-\d{2})-(.+)$/, HTML_TAG = /<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^<>]*)?\/?>|<!--/;
|
|
1733
|
+
function realDate(y3, m2, d3) {
|
|
1734
|
+
let date = new Date(Date.UTC(+y3, +m2 - 1, +d3));
|
|
1735
|
+
return date.getUTCFullYear() === +y3 && date.getUTCMonth() === +m2 - 1 && date.getUTCDate() === +d3;
|
|
1736
|
+
}
|
|
1737
|
+
function isUrl(v2) {
|
|
1738
|
+
if (typeof v2 !== "string")
|
|
1739
|
+
return !1;
|
|
1740
|
+
try {
|
|
1741
|
+
let u2 = new URL(v2);
|
|
1742
|
+
return u2.protocol === "https:" || u2.protocol === "http:";
|
|
1743
|
+
} catch {
|
|
1744
|
+
return !1;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
var filled = (v2) => typeof v2 === "string" && v2.trim() !== "";
|
|
1748
|
+
function json(raw) {
|
|
1749
|
+
try {
|
|
1750
|
+
return { ok: !0, value: JSON.parse(raw) };
|
|
1751
|
+
} catch {
|
|
1752
|
+
return { ok: !1 };
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
function checkValue(key, kind, raw) {
|
|
1756
|
+
switch (kind) {
|
|
1757
|
+
case "layout":
|
|
1758
|
+
return raw === "post" ? void 0 : `${key} must be post`;
|
|
1759
|
+
case "date": {
|
|
1760
|
+
let m2 = DATE.exec(raw);
|
|
1761
|
+
return m2 && realDate(m2[1], m2[2], m2[3]) ? void 0 : `${key} must be a date like 2026-11-05`;
|
|
1762
|
+
}
|
|
1763
|
+
case "datetime": {
|
|
1764
|
+
let m2 = DATETIME.exec(raw);
|
|
1765
|
+
return m2 && realDate(m2[1], m2[2], m2[3]) ? void 0 : `${key} must be a date and time like 2026-11-05T15:00:00Z`;
|
|
1766
|
+
}
|
|
1767
|
+
case "string": {
|
|
1768
|
+
let v2 = json(raw);
|
|
1769
|
+
if (!v2.ok || typeof v2.value !== "string")
|
|
1770
|
+
return `${key} must be a quoted string`;
|
|
1771
|
+
return v2.value.trim() ? void 0 : `${key} is empty`;
|
|
1772
|
+
}
|
|
1773
|
+
case "url": {
|
|
1774
|
+
let v2 = json(raw);
|
|
1775
|
+
return v2.ok && isUrl(v2.value) ? void 0 : `${key} must be a quoted http or https address`;
|
|
1776
|
+
}
|
|
1777
|
+
case "tags": {
|
|
1778
|
+
let v2 = json(raw);
|
|
1779
|
+
return v2.ok && Array.isArray(v2.value) && v2.value.every((t) => typeof t === "string") ? void 0 : `${key} must be a list of quoted strings, like ["seo"]`;
|
|
1780
|
+
}
|
|
1781
|
+
case "urls": {
|
|
1782
|
+
let v2 = json(raw);
|
|
1783
|
+
return v2.ok && Array.isArray(v2.value) && v2.value.every(isUrl) ? void 0 : `${key} must be a list of quoted http or https addresses`;
|
|
1784
|
+
}
|
|
1785
|
+
case "faq": {
|
|
1786
|
+
let v2 = json(raw);
|
|
1787
|
+
return v2.ok && Array.isArray(v2.value) && v2.value.every((f) => f !== null && typeof f === "object" && filled(f.q) && filled(f.a)) ? void 0 : `${key} must be one line of JSON like [{"q": "A question?", "a": "An answer."}]`;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
function proseOnly(body2) {
|
|
1792
|
+
let out = [], fence;
|
|
1793
|
+
for (let line of body2.split(`
|
|
1794
|
+
`)) {
|
|
1795
|
+
let open = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
|
|
1796
|
+
if (fence) {
|
|
1797
|
+
if (open && open[1][0] === fence[0] && open[1].length >= fence.length)
|
|
1798
|
+
fence = void 0;
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (open) {
|
|
1802
|
+
fence = open[1];
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
out.push(line.replace(/(`+)[^`]*?\1/g, ""));
|
|
1806
|
+
}
|
|
1807
|
+
return out;
|
|
1808
|
+
}
|
|
1809
|
+
function inspectPost(fileName, text, format) {
|
|
1810
|
+
let preset = BLOG_PRESETS[format], problems = [], base = fileName.endsWith(preset.ext) ? fileName.slice(0, fileName.length - preset.ext.length) : fileName.replace(/\.[^.]*$/, ""), fileSlug = base, fileDate;
|
|
1811
|
+
if (preset.datedFileKey) {
|
|
1812
|
+
let m2 = JEKYLL_FILE.exec(base);
|
|
1813
|
+
if (m2)
|
|
1814
|
+
fileDate = m2[1], fileSlug = m2[2];
|
|
1815
|
+
else
|
|
1816
|
+
problems.push("file name must look like YYYY-MM-DD-<slug>.md");
|
|
1817
|
+
}
|
|
1818
|
+
if (!SLUG.test(fileSlug))
|
|
1819
|
+
problems.push(`file name slug "${fileSlug}" must be lowercase words joined by hyphens`);
|
|
1820
|
+
let lines = text.replace(/^/, "").split(/\r?\n/), end = lines[0] === "---" ? lines.indexOf("---", 1) : -1;
|
|
1821
|
+
if (end < 0)
|
|
1822
|
+
return problems.push("front matter must open and close with --- lines at the top"), { slug: fileSlug, problems };
|
|
1823
|
+
let raws = /* @__PURE__ */ new Map;
|
|
1824
|
+
for (let line of lines.slice(1, end)) {
|
|
1825
|
+
if (!line.trim())
|
|
1826
|
+
continue;
|
|
1827
|
+
let m2 = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/.exec(line);
|
|
1828
|
+
if (!m2) {
|
|
1829
|
+
problems.push(`front matter line is not "key: value": ${line.trim()}`);
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
if (raws.has(m2[1]))
|
|
1833
|
+
problems.push(`${m2[1]} appears more than once`);
|
|
1834
|
+
raws.set(m2[1], m2[2].trim());
|
|
1835
|
+
}
|
|
1836
|
+
let values = {};
|
|
1837
|
+
for (let [key, spec] of Object.entries(preset.keys)) {
|
|
1838
|
+
let raw = raws.get(key);
|
|
1839
|
+
if (raw === void 0 || raw === "") {
|
|
1840
|
+
if (spec.required)
|
|
1841
|
+
problems.push(`missing ${key}`);
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
let problem = checkValue(key, spec.kind, raw);
|
|
1845
|
+
if (problem)
|
|
1846
|
+
problems.push(problem);
|
|
1847
|
+
else if (spec.role)
|
|
1848
|
+
values[spec.role] = spec.kind === "date" || spec.kind === "datetime" ? raw : JSON.parse(raw);
|
|
1849
|
+
}
|
|
1850
|
+
let slug = fileSlug;
|
|
1851
|
+
if (preset.slugKey) {
|
|
1852
|
+
let v2 = json(raws.get(preset.slugKey) ?? "");
|
|
1853
|
+
if (v2.ok && typeof v2.value === "string" && v2.value) {
|
|
1854
|
+
if (!SLUG.test(v2.value))
|
|
1855
|
+
problems.push(`${preset.slugKey} "${v2.value}" must be lowercase words joined by hyphens`);
|
|
1856
|
+
else if (v2.value !== fileSlug)
|
|
1857
|
+
problems.push(`${preset.slugKey} "${v2.value}" does not match the file name "${fileSlug}"`);
|
|
1858
|
+
slug = v2.value;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (preset.datedFileKey && fileDate) {
|
|
1862
|
+
let date = raws.get(preset.datedFileKey);
|
|
1863
|
+
if (date && DATE.test(date) && date !== fileDate)
|
|
1864
|
+
problems.push(`${preset.datedFileKey} ${date} does not match the file name date ${fileDate}`);
|
|
1865
|
+
}
|
|
1866
|
+
let body2 = lines.slice(end + 1).join(`
|
|
1867
|
+
`), fields = { slug, values, body: body2 };
|
|
1868
|
+
if (!body2.trim())
|
|
1869
|
+
return problems.push("the post has no body"), { slug, problems, fields };
|
|
1870
|
+
let prose = proseOnly(body2), first = body2.split(`
|
|
1871
|
+
`).find((l2) => l2.trim());
|
|
1872
|
+
if (first && /^\s{0,3}#(?:\s|$)/.test(first))
|
|
1873
|
+
problems.push("the body starts with a # heading; the title comes from front matter");
|
|
1874
|
+
if (!prose.some((l2) => /^\s{0,3}## /.test(l2)))
|
|
1875
|
+
problems.push("the body needs at least one ## heading");
|
|
1876
|
+
if (prose.some((l2) => HTML_TAG.test(l2)))
|
|
1877
|
+
problems.push("the body has raw HTML outside code; use Markdown instead");
|
|
1878
|
+
return { slug, problems, fields };
|
|
1879
|
+
}
|
|
1880
|
+
function checkPost(fileName, text, format) {
|
|
1881
|
+
let { slug, problems } = inspectPost(fileName, text, format);
|
|
1882
|
+
return { slug, problems };
|
|
1883
|
+
}
|
|
1884
|
+
// src/blog-check.ts
|
|
1885
|
+
function checkBlog(opts) {
|
|
1886
|
+
let format = opts.format ?? "markdown", preset = BLOG_PRESETS[format], dir = (opts.dir ?? preset.dir).replace(/\\/g, "/").replace(/\/+$/, ""), abs = join(opts.cwd, dir), problems = [], names;
|
|
1887
|
+
try {
|
|
1888
|
+
if (!statSync(abs).isDirectory())
|
|
1889
|
+
return {
|
|
1890
|
+
dir,
|
|
1891
|
+
posts: 0,
|
|
1892
|
+
problems: [{ file: dir, problem: "is not a folder" }]
|
|
1893
|
+
};
|
|
1894
|
+
names = readdirSync(abs, { withFileTypes: !0 }).filter((e2) => e2.isFile() && e2.name.endsWith(preset.ext)).map((e2) => e2.name).sort();
|
|
1895
|
+
} catch {
|
|
1896
|
+
return {
|
|
1897
|
+
dir,
|
|
1898
|
+
posts: 0,
|
|
1899
|
+
problems: opts.allowEmpty ? [] : [{ file: dir, problem: "folder not found (pass --allow-empty)" }]
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
if (!names.length && !opts.allowEmpty)
|
|
1903
|
+
problems.push({
|
|
1904
|
+
file: dir,
|
|
1905
|
+
problem: `no ${preset.ext} posts found (pass --allow-empty)`
|
|
1906
|
+
});
|
|
1907
|
+
let seen = /* @__PURE__ */ new Map;
|
|
1908
|
+
for (let name of names) {
|
|
1909
|
+
let file = `${dir}/${name}`, res = checkPost(name, readFileSync(join(abs, name), "utf8"), format);
|
|
1910
|
+
for (let problem of res.problems)
|
|
1911
|
+
problems.push({ file, problem });
|
|
1912
|
+
if (res.slug) {
|
|
1913
|
+
let other = seen.get(res.slug);
|
|
1914
|
+
if (other)
|
|
1915
|
+
problems.push({
|
|
1916
|
+
file,
|
|
1917
|
+
problem: `slug "${res.slug}" is also used by ${other}`
|
|
1918
|
+
});
|
|
1919
|
+
else
|
|
1920
|
+
seen.set(res.slug, file);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return { dir, posts: names.length, problems };
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1657
1926
|
// src/config.ts
|
|
1658
1927
|
import {
|
|
1659
1928
|
chmodSync,
|
|
1660
1929
|
existsSync,
|
|
1661
1930
|
mkdirSync,
|
|
1662
|
-
readFileSync,
|
|
1931
|
+
readFileSync as readFileSync2,
|
|
1663
1932
|
writeFileSync
|
|
1664
1933
|
} from "node:fs";
|
|
1665
1934
|
import os from "node:os";
|
|
@@ -1705,7 +1974,7 @@ function loadAuth() {
|
|
|
1705
1974
|
if (!existsSync(authFile()))
|
|
1706
1975
|
return null;
|
|
1707
1976
|
try {
|
|
1708
|
-
let a = JSON.parse(
|
|
1977
|
+
let a = JSON.parse(readFileSync2(authFile(), "utf8"));
|
|
1709
1978
|
if (!a.token)
|
|
1710
1979
|
return null;
|
|
1711
1980
|
return { url: envUrl || a.url || DEFAULT_URL, token: a.token };
|
|
@@ -2530,7 +2799,7 @@ function onboardingPrompt(input) {
|
|
|
2530
2799
|
}
|
|
2531
2800
|
|
|
2532
2801
|
// src/scan.ts
|
|
2533
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
2802
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
2534
2803
|
import path2 from "node:path";
|
|
2535
2804
|
var MONOREPO_DIRS = ["apps", "packages", "services", "sites", "web", "site"], SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2536
2805
|
"node_modules",
|
|
@@ -2588,7 +2857,7 @@ var MONOREPO_DIRS = ["apps", "packages", "services", "sites", "web", "site"], SK
|
|
|
2588
2857
|
], EXAMPLE_ENV = /^(\.env\.(example|sample|template|dist|local\.example|development\.example|production\.example)|example\.env|\.dev\.vars\.example)$/;
|
|
2589
2858
|
function readText(file2) {
|
|
2590
2859
|
try {
|
|
2591
|
-
return
|
|
2860
|
+
return readFileSync3(file2, "utf8");
|
|
2592
2861
|
} catch {
|
|
2593
2862
|
return null;
|
|
2594
2863
|
}
|
|
@@ -2605,14 +2874,14 @@ function readJson(file2) {
|
|
|
2605
2874
|
}
|
|
2606
2875
|
function isDir(p2) {
|
|
2607
2876
|
try {
|
|
2608
|
-
return
|
|
2877
|
+
return statSync2(p2).isDirectory();
|
|
2609
2878
|
} catch {
|
|
2610
2879
|
return !1;
|
|
2611
2880
|
}
|
|
2612
2881
|
}
|
|
2613
2882
|
function listDir(dir) {
|
|
2614
2883
|
try {
|
|
2615
|
-
return
|
|
2884
|
+
return readdirSync2(dir);
|
|
2616
2885
|
} catch {
|
|
2617
2886
|
return [];
|
|
2618
2887
|
}
|
|
@@ -3030,11 +3299,11 @@ function watchRows(items) {
|
|
|
3030
3299
|
}
|
|
3031
3300
|
|
|
3032
3301
|
// src/update.ts
|
|
3033
|
-
import { readFileSync as
|
|
3302
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
3034
3303
|
import path3 from "node:path";
|
|
3035
3304
|
var readJson2 = (file2) => {
|
|
3036
3305
|
try {
|
|
3037
|
-
return JSON.parse(
|
|
3306
|
+
return JSON.parse(readFileSync4(file2, "utf8"));
|
|
3038
3307
|
} catch {
|
|
3039
3308
|
return null;
|
|
3040
3309
|
}
|
|
@@ -3088,6 +3357,8 @@ ${import_picocolors4.default.bold("Usage")}
|
|
|
3088
3357
|
npx lopebase mcp run the LopeBase tools as a stdio MCP server
|
|
3089
3358
|
npx lopebase adapter secret print a new @lopebase/adapter signing secret
|
|
3090
3359
|
npx lopebase adapter check <url> test a running adapter (reads LOPEBASE_SIGNING_SECRET)
|
|
3360
|
+
npx lopebase blog check [dir] check the blog posts LopeBase publishes to your repo
|
|
3361
|
+
(--format <preset>, --allow-empty)
|
|
3091
3362
|
|
|
3092
3363
|
${import_picocolors4.default.bold("Options")}
|
|
3093
3364
|
--print print the agent prompt instead of starting an agent
|
|
@@ -3103,7 +3374,7 @@ ${import_picocolors4.default.bold("Learn more")}
|
|
|
3103
3374
|
}, cwd = process.cwd(), motion = motionAllowed(), TAGLINE = "Every product, one place.", PAGEVIEWS = "LopeBase pageviews";
|
|
3104
3375
|
function version() {
|
|
3105
3376
|
try {
|
|
3106
|
-
return JSON.parse(
|
|
3377
|
+
return JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
3107
3378
|
} catch {
|
|
3108
3379
|
return;
|
|
3109
3380
|
}
|
|
@@ -3400,6 +3671,29 @@ The adapter is in: ${where}. If you have the lopebase MCP tools, call get_onboar
|
|
|
3400
3671
|
` ${brand("3.")} run the adapter check and your tests`
|
|
3401
3672
|
]);
|
|
3402
3673
|
}
|
|
3674
|
+
var BLOG_USAGE = `Usage:
|
|
3675
|
+
lopebase blog check [dir] [--format <preset>] [--allow-empty]
|
|
3676
|
+
|
|
3677
|
+
Presets: ${Object.entries(BLOG_PRESETS).map(([name, preset]) => `${name} (${preset.dir})`).join(", ")}. Default: markdown.`;
|
|
3678
|
+
function blog() {
|
|
3679
|
+
if (rest[0] !== "check")
|
|
3680
|
+
return console.log(BLOG_USAGE), process.exit(rest[0] ? 1 : 0);
|
|
3681
|
+
let format = option("format") ?? "markdown";
|
|
3682
|
+
if (!isBlogFormat(format))
|
|
3683
|
+
return console.error(`Unknown format "${format}".
|
|
3684
|
+
|
|
3685
|
+
${BLOG_USAGE}`), process.exit(1);
|
|
3686
|
+
let dir = rest.slice(1).find((a, i, args) => !a.startsWith("--") && args[i - 1] !== "--format"), res = checkBlog({
|
|
3687
|
+
cwd,
|
|
3688
|
+
format,
|
|
3689
|
+
dir,
|
|
3690
|
+
allowEmpty: flag("allow-empty")
|
|
3691
|
+
});
|
|
3692
|
+
for (let { file: file2, problem } of res.problems)
|
|
3693
|
+
console.log(`${file2}: ${problem}`);
|
|
3694
|
+
let posts = `${res.posts} ${res.posts === 1 ? "post" : "posts"}`;
|
|
3695
|
+
console.log(res.problems.length ? import_picocolors4.default.red(`${res.problems.length} ${res.problems.length === 1 ? "problem" : "problems"} in ${posts} in ${res.dir}.`) : import_picocolors4.default.green(`Checked ${posts} in ${res.dir}. All good.`)), process.exit(res.problems.length ? 1 : 0);
|
|
3696
|
+
}
|
|
3403
3697
|
async function main() {
|
|
3404
3698
|
switch (command) {
|
|
3405
3699
|
case "init":
|
|
@@ -3441,6 +3735,8 @@ async function main() {
|
|
|
3441
3735
|
lopebase adapter secret print a new signing secret
|
|
3442
3736
|
lopebase adapter check <base-url> test a running adapter (reads LOPEBASE_SIGNING_SECRET)`), process.exit(sub ? 1 : 0);
|
|
3443
3737
|
}
|
|
3738
|
+
case "blog":
|
|
3739
|
+
return blog();
|
|
3444
3740
|
case "mcp": {
|
|
3445
3741
|
let auth = loadAuth();
|
|
3446
3742
|
if (!auth)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lopebase",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Set up LopeBase from your project's folder: scan your stack, connect your workspace, and let your coding agent do the rest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@clack/prompts": "^0.11.0",
|
|
30
30
|
"@lopebase/adapter": "workspace:*",
|
|
31
|
+
"@lopebase/blog": "workspace:*",
|
|
31
32
|
"@types/cross-spawn": "^6.0.6",
|
|
32
33
|
"@types/node": "^22.0.0",
|
|
33
34
|
"cross-spawn": "^7.0.6",
|