@codyswann/lisa 2.24.0 → 2.25.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/.claude-plugin/marketplace.json +12 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/rules/config-resolution.md +32 -1
- package/plugins/lisa/skills/atlassian-access/SKILL.md +32 -1
- package/plugins/lisa/skills/notion-access/SKILL.md +32 -1
- package/plugins/lisa/skills/setup-atlassian/SKILL.md +32 -1
- package/plugins/lisa/skills/setup-linear/SKILL.md +32 -1
- package/plugins/lisa/skills/setup-notion/SKILL.md +32 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +8 -0
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +32 -0
- package/plugins/lisa-wiki/ci/lisa-wiki-validate.yml +32 -0
- package/plugins/lisa-wiki/commands/add-ingest.md +6 -0
- package/plugins/lisa-wiki/commands/add-role.md +6 -0
- package/plugins/lisa-wiki/commands/doctor.md +6 -0
- package/plugins/lisa-wiki/commands/ingest.md +6 -0
- package/plugins/lisa-wiki/commands/lint.md +6 -0
- package/plugins/lisa-wiki/commands/migrate.md +6 -0
- package/plugins/lisa-wiki/commands/onboard-me.md +6 -0
- package/plugins/lisa-wiki/commands/query.md +6 -0
- package/plugins/lisa-wiki/commands/setup.md +6 -0
- package/plugins/lisa-wiki/schema/lisa-wiki-config.schema.json +118 -0
- package/plugins/lisa-wiki/schema/wiki-structure.schema.json +51 -0
- package/plugins/lisa-wiki/scripts/_wiki-lib.mjs +185 -0
- package/plugins/lisa-wiki/scripts/diff-guard.mjs +116 -0
- package/plugins/lisa-wiki/scripts/ingest-git.mjs +189 -0
- package/plugins/lisa-wiki/scripts/ingest-memory.mjs +130 -0
- package/plugins/lisa-wiki/scripts/ingest-roles.mjs +85 -0
- package/plugins/lisa-wiki/scripts/ingest_slack_channel.py +329 -0
- package/plugins/lisa-wiki/scripts/lint-wiki.mjs +320 -0
- package/plugins/lisa-wiki/scripts/mcp-doctor.mjs +72 -0
- package/plugins/lisa-wiki/scripts/render-contract.mjs +107 -0
- package/plugins/lisa-wiki/scripts/rewrite-refs.mjs +144 -0
- package/plugins/lisa-wiki/scripts/slack_oauth_user.py +179 -0
- package/plugins/lisa-wiki/scripts/validate-config.mjs +232 -0
- package/plugins/lisa-wiki/scripts/verify-migration.mjs +199 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-add-role/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-git/SKILL.md +25 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-connector-web/SKILL.md +23 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-doctor/SKILL.md +47 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-ingest/SKILL.md +43 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-lint/SKILL.md +32 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-migrate/SKILL.md +43 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-query/SKILL.md +30 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-setup/SKILL.md +45 -0
- package/plugins/lisa-wiki/skills/lisa-wiki-usage/SKILL.md +50 -0
- package/plugins/lisa-wiki/templates/agents/role-agent.claude.md +16 -0
- package/plugins/lisa-wiki/templates/agents/role-agent.codex.toml +15 -0
- package/plugins/lisa-wiki/templates/index.md +17 -0
- package/plugins/lisa-wiki/templates/llm-wiki-contract.md +60 -0
- package/plugins/lisa-wiki/templates/log.md +8 -0
- package/plugins/lisa-wiki/templates/page-types/architecture.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/concept.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/decision.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/entity.md +19 -0
- package/plugins/lisa-wiki/templates/page-types/open-question.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/playbook.md +18 -0
- package/plugins/lisa-wiki/templates/page-types/project.md +19 -0
- package/plugins/lisa-wiki/templates/page-types/requirement.md +19 -0
- package/plugins/lisa-wiki/templates/page-types/staff.md +26 -0
- package/plugins/lisa-wiki/templates/start-here.md +24 -0
- package/plugins/lisa-wiki/templates/state-readme.md +20 -0
- package/plugins/src/base/rules/config-resolution.md +32 -1
- package/plugins/src/base/skills/atlassian-access/SKILL.md +32 -1
- package/plugins/src/base/skills/notion-access/SKILL.md +32 -1
- package/plugins/src/base/skills/setup-atlassian/SKILL.md +32 -1
- package/plugins/src/base/skills/setup-linear/SKILL.md +32 -1
- package/plugins/src/base/skills/setup-notion/SKILL.md +32 -1
- package/plugins/src/wiki/.claude-plugin/plugin.json +6 -0
- package/plugins/src/wiki/ci/lisa-wiki-validate.yml +32 -0
- package/plugins/src/wiki/commands/add-ingest.md +6 -0
- package/plugins/src/wiki/commands/add-role.md +6 -0
- package/plugins/src/wiki/commands/doctor.md +6 -0
- package/plugins/src/wiki/commands/ingest.md +6 -0
- package/plugins/src/wiki/commands/lint.md +6 -0
- package/plugins/src/wiki/commands/migrate.md +6 -0
- package/plugins/src/wiki/commands/onboard-me.md +6 -0
- package/plugins/src/wiki/commands/query.md +6 -0
- package/plugins/src/wiki/commands/setup.md +6 -0
- package/plugins/src/wiki/schema/lisa-wiki-config.schema.json +118 -0
- package/plugins/src/wiki/schema/wiki-structure.schema.json +51 -0
- package/plugins/src/wiki/scripts/_wiki-lib.mjs +185 -0
- package/plugins/src/wiki/scripts/diff-guard.mjs +116 -0
- package/plugins/src/wiki/scripts/ingest-git.mjs +189 -0
- package/plugins/src/wiki/scripts/ingest-memory.mjs +130 -0
- package/plugins/src/wiki/scripts/ingest-roles.mjs +85 -0
- package/plugins/src/wiki/scripts/ingest_slack_channel.py +329 -0
- package/plugins/src/wiki/scripts/lint-wiki.mjs +320 -0
- package/plugins/src/wiki/scripts/mcp-doctor.mjs +72 -0
- package/plugins/src/wiki/scripts/render-contract.mjs +107 -0
- package/plugins/src/wiki/scripts/rewrite-refs.mjs +144 -0
- package/plugins/src/wiki/scripts/slack_oauth_user.py +179 -0
- package/plugins/src/wiki/scripts/validate-config.mjs +232 -0
- package/plugins/src/wiki/scripts/verify-migration.mjs +199 -0
- package/plugins/src/wiki/skills/lisa-wiki-add-ingest/SKILL.md +34 -0
- package/plugins/src/wiki/skills/lisa-wiki-add-role/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-confluence/SKILL.md +25 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-docs/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-git/SKILL.md +25 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-jira/SKILL.md +28 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-memory/SKILL.md +28 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-notion/SKILL.md +25 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-roles/SKILL.md +22 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-slack/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-connector-web/SKILL.md +23 -0
- package/plugins/src/wiki/skills/lisa-wiki-doctor/SKILL.md +47 -0
- package/plugins/src/wiki/skills/lisa-wiki-ingest/SKILL.md +43 -0
- package/plugins/src/wiki/skills/lisa-wiki-lint/SKILL.md +32 -0
- package/plugins/src/wiki/skills/lisa-wiki-migrate/SKILL.md +43 -0
- package/plugins/src/wiki/skills/lisa-wiki-onboard-me/SKILL.md +33 -0
- package/plugins/src/wiki/skills/lisa-wiki-query/SKILL.md +30 -0
- package/plugins/src/wiki/skills/lisa-wiki-setup/SKILL.md +45 -0
- package/plugins/src/wiki/skills/lisa-wiki-usage/SKILL.md +50 -0
- package/plugins/src/wiki/templates/agents/role-agent.claude.md +16 -0
- package/plugins/src/wiki/templates/agents/role-agent.codex.toml +15 -0
- package/plugins/src/wiki/templates/index.md +17 -0
- package/plugins/src/wiki/templates/llm-wiki-contract.md +60 -0
- package/plugins/src/wiki/templates/log.md +8 -0
- package/plugins/src/wiki/templates/page-types/architecture.md +18 -0
- package/plugins/src/wiki/templates/page-types/concept.md +18 -0
- package/plugins/src/wiki/templates/page-types/decision.md +18 -0
- package/plugins/src/wiki/templates/page-types/entity.md +19 -0
- package/plugins/src/wiki/templates/page-types/open-question.md +18 -0
- package/plugins/src/wiki/templates/page-types/playbook.md +18 -0
- package/plugins/src/wiki/templates/page-types/project.md +19 -0
- package/plugins/src/wiki/templates/page-types/requirement.md +19 -0
- package/plugins/src/wiki/templates/page-types/staff.md +26 -0
- package/plugins/src/wiki/templates/start-here.md +24 -0
- package/plugins/src/wiki/templates/state-readme.md +20 -0
- package/scripts/build-plugins.sh +29 -21
- package/scripts/check-plugins-sync.sh +38 -1
- package/scripts/generate-codex-plugin-artifacts.mjs +22 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-wiki.mjs — deterministic, dependency-free integrity checker for a lisa-wiki.
|
|
4
|
+
*
|
|
5
|
+
* Read-only: it reports findings, it never modifies the wiki. Default mode is
|
|
6
|
+
* "warning" (exit 1 only on FAIL items); `--strict` (hard-enforcement) also fails
|
|
7
|
+
* on WARN items.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node lint-wiki.mjs [--wiki <wikiRoot>] [--config <path>] [--strict] [--json]
|
|
10
|
+
* Exit 0 = clean (for the active mode), 1 = blocking findings.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import {
|
|
16
|
+
loadConfig,
|
|
17
|
+
loadStructure,
|
|
18
|
+
pluginRootFrom,
|
|
19
|
+
walkFiles,
|
|
20
|
+
parseFrontmatter,
|
|
21
|
+
extractMarkdownLinks,
|
|
22
|
+
extractCitations,
|
|
23
|
+
SECRET_PATTERNS,
|
|
24
|
+
TEXT_EXTS,
|
|
25
|
+
makeReport,
|
|
26
|
+
} from "./_wiki-lib.mjs";
|
|
27
|
+
|
|
28
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const pluginRoot = pluginRootFrom(scriptDir);
|
|
30
|
+
|
|
31
|
+
const argv = process.argv.slice(2);
|
|
32
|
+
const flag = name => argv.includes(name);
|
|
33
|
+
const opt = name => {
|
|
34
|
+
const i = argv.indexOf(name);
|
|
35
|
+
return i !== -1 ? argv[i + 1] : undefined;
|
|
36
|
+
};
|
|
37
|
+
const strict = flag("--strict");
|
|
38
|
+
const asJson = flag("--json");
|
|
39
|
+
|
|
40
|
+
const { config } = loadConfig(opt("--config"));
|
|
41
|
+
const structure = loadStructure(pluginRoot) ?? {};
|
|
42
|
+
const wikiRoot = path.resolve(opt("--wiki") ?? config?.wikiRoot ?? "wiki");
|
|
43
|
+
const report = makeReport();
|
|
44
|
+
|
|
45
|
+
if (!config) {
|
|
46
|
+
report.add(
|
|
47
|
+
"config",
|
|
48
|
+
"not-loaded",
|
|
49
|
+
"WARN",
|
|
50
|
+
"config not found/invalid; using structure-manifest defaults"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(wikiRoot)) {
|
|
55
|
+
console.error(`✗ wiki root not found: ${wikiRoot}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rel = p => path.relative(process.cwd(), p);
|
|
60
|
+
const wrel = p => path.relative(wikiRoot, p);
|
|
61
|
+
const categories = config?.categories ?? structure.categoryDirs?.default ?? [];
|
|
62
|
+
const frontmatterRequired = config?.frontmatter !== false;
|
|
63
|
+
|
|
64
|
+
const allMd = walkFiles(wikiRoot, { ext: ".md" });
|
|
65
|
+
const allFiles = walkFiles(wikiRoot);
|
|
66
|
+
const exists = p => fs.existsSync(p);
|
|
67
|
+
const isUnder = (p, dir) => {
|
|
68
|
+
const r = path.relative(path.join(wikiRoot, dir), p);
|
|
69
|
+
return r !== "" && !r.startsWith("..") && !path.isAbsolute(r);
|
|
70
|
+
};
|
|
71
|
+
const isSynthesisPage = p => categories.some(c => isUnder(p, c));
|
|
72
|
+
const isSourceNote = p => isUnder(p, "sources");
|
|
73
|
+
|
|
74
|
+
// --- A. structure conformance ---------------------------------------------
|
|
75
|
+
for (const f of structure.requiredFiles ?? []) {
|
|
76
|
+
report.add(
|
|
77
|
+
"structure",
|
|
78
|
+
`required-file:${f}`,
|
|
79
|
+
exists(path.join(wikiRoot, f)) ? "PASS" : "FAIL",
|
|
80
|
+
exists(path.join(wikiRoot, f))
|
|
81
|
+
? `present: ${f}`
|
|
82
|
+
: `missing required file: ${f}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
for (const d of structure.requiredDirs ?? []) {
|
|
86
|
+
const ok =
|
|
87
|
+
fs.existsSync(path.join(wikiRoot, d)) &&
|
|
88
|
+
fs.statSync(path.join(wikiRoot, d)).isDirectory();
|
|
89
|
+
report.add(
|
|
90
|
+
"structure",
|
|
91
|
+
`required-dir:${d}`,
|
|
92
|
+
ok ? "PASS" : "FAIL",
|
|
93
|
+
ok ? `present: ${d}/` : `missing required dir: ${d}/`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- B. frontmatter on synthesis pages + source notes ---------------------
|
|
98
|
+
if (frontmatterRequired) {
|
|
99
|
+
for (const f of allMd) {
|
|
100
|
+
if (!isSynthesisPage(f) && !isSourceNote(f)) continue;
|
|
101
|
+
const fm = parseFrontmatter(fs.readFileSync(f, "utf8"));
|
|
102
|
+
if (!fm.has) {
|
|
103
|
+
report.add("frontmatter", "missing", "WARN", `no frontmatter`, wrel(f));
|
|
104
|
+
} else {
|
|
105
|
+
const missing = ["type", "created", "updated"].filter(
|
|
106
|
+
k => !fm.keys.includes(k)
|
|
107
|
+
);
|
|
108
|
+
if (missing.length) {
|
|
109
|
+
report.add(
|
|
110
|
+
"frontmatter",
|
|
111
|
+
"incomplete",
|
|
112
|
+
"WARN",
|
|
113
|
+
`frontmatter missing keys: ${missing.join(", ")}`,
|
|
114
|
+
wrel(f)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- C/D. links: index coverage, broken links, citations ------------------
|
|
122
|
+
const indexPath = path.join(wikiRoot, "index.md");
|
|
123
|
+
const indexText = exists(indexPath) ? fs.readFileSync(indexPath, "utf8") : "";
|
|
124
|
+
const indexTargets = new Set(
|
|
125
|
+
extractMarkdownLinks(indexText).map(t => path.resolve(wikiRoot, t))
|
|
126
|
+
);
|
|
127
|
+
// dangling index links
|
|
128
|
+
for (const t of indexTargets) {
|
|
129
|
+
if (t.endsWith(".md") && !exists(t)) {
|
|
130
|
+
report.add(
|
|
131
|
+
"links",
|
|
132
|
+
"index-dangling",
|
|
133
|
+
"FAIL",
|
|
134
|
+
`index links to a missing page: ${wrel(t)}`,
|
|
135
|
+
"index.md"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// pages missing from index
|
|
140
|
+
for (const f of allMd) {
|
|
141
|
+
if (!isSynthesisPage(f)) continue;
|
|
142
|
+
if (!indexTargets.has(f)) {
|
|
143
|
+
report.add(
|
|
144
|
+
"index",
|
|
145
|
+
"page-missing",
|
|
146
|
+
"WARN",
|
|
147
|
+
`synthesis page not linked from index.md`,
|
|
148
|
+
wrel(f)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// broken internal links + citations across all md; build link graph for orphans.
|
|
153
|
+
// A link resolves if it exists relative to the file, OR as a wiki-root-relative /
|
|
154
|
+
// repo-root-relative / "wiki/"-prefixed form (reduces false positives).
|
|
155
|
+
const wikiBase = path.basename(wikiRoot);
|
|
156
|
+
const linkResolution = (f, target) => {
|
|
157
|
+
const primary = path.resolve(path.dirname(f), target);
|
|
158
|
+
const stripped = target
|
|
159
|
+
.replace(/^\/+/, "")
|
|
160
|
+
.replace(new RegExp(`^${wikiBase}/`), "");
|
|
161
|
+
const cands = [
|
|
162
|
+
primary,
|
|
163
|
+
path.resolve(wikiRoot, target),
|
|
164
|
+
path.resolve(wikiRoot, "..", target),
|
|
165
|
+
path.resolve(wikiRoot, stripped),
|
|
166
|
+
];
|
|
167
|
+
return { primary, hit: cands.find(exists) };
|
|
168
|
+
};
|
|
169
|
+
const linkedTo = new Set();
|
|
170
|
+
for (const f of allMd) {
|
|
171
|
+
const text = fs.readFileSync(f, "utf8");
|
|
172
|
+
for (const target of extractMarkdownLinks(text)) {
|
|
173
|
+
const { primary, hit } = linkResolution(f, target);
|
|
174
|
+
if (target.endsWith(".md") || primary.endsWith(".md")) {
|
|
175
|
+
linkedTo.add(hit ?? primary);
|
|
176
|
+
if (!hit) {
|
|
177
|
+
report.add(
|
|
178
|
+
"links",
|
|
179
|
+
"broken",
|
|
180
|
+
"FAIL",
|
|
181
|
+
`broken link → ${target}`,
|
|
182
|
+
wrel(f)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
} else if (!hit) {
|
|
186
|
+
report.add(
|
|
187
|
+
"links",
|
|
188
|
+
"broken-asset",
|
|
189
|
+
"WARN",
|
|
190
|
+
`link to missing path → ${target}`,
|
|
191
|
+
wrel(f)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const cite of extractCitations(text)) {
|
|
196
|
+
const candidates = [
|
|
197
|
+
path.resolve(wikiRoot, cite),
|
|
198
|
+
path.resolve(wikiRoot, "..", cite),
|
|
199
|
+
path.resolve(path.dirname(f), cite),
|
|
200
|
+
];
|
|
201
|
+
if (!candidates.some(exists)) {
|
|
202
|
+
report.add(
|
|
203
|
+
"links",
|
|
204
|
+
"citation-unresolved",
|
|
205
|
+
"WARN",
|
|
206
|
+
`citation path not found → ${cite}`,
|
|
207
|
+
wrel(f)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- E. orphan pages ------------------------------------------------------
|
|
214
|
+
for (const f of allMd) {
|
|
215
|
+
if (!isSynthesisPage(f)) continue;
|
|
216
|
+
if (!indexTargets.has(f) && !linkedTo.has(f)) {
|
|
217
|
+
report.add(
|
|
218
|
+
"orphans",
|
|
219
|
+
"orphan",
|
|
220
|
+
"WARN",
|
|
221
|
+
`page is unreferenced (not in index, not linked)`,
|
|
222
|
+
wrel(f)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- F. log non-empty -----------------------------------------------------
|
|
228
|
+
const logPath = path.join(wikiRoot, "log.md");
|
|
229
|
+
if (exists(logPath)) {
|
|
230
|
+
// Accept the canonical table row (| YYYY-MM-DD | ...) AND the legacy heading
|
|
231
|
+
// formats (## YYYY-MM-DD or ## [YYYY-MM-DD]) tolerated during migration.
|
|
232
|
+
const rows = fs
|
|
233
|
+
.readFileSync(logPath, "utf8")
|
|
234
|
+
.split("\n")
|
|
235
|
+
.filter(
|
|
236
|
+
l =>
|
|
237
|
+
/^\|\s*\d{4}-\d{2}-\d{2}\s*\|/.test(l) ||
|
|
238
|
+
/^##\s*\[?\d{4}-\d{2}-\d{2}/.test(l)
|
|
239
|
+
);
|
|
240
|
+
report.add(
|
|
241
|
+
"log",
|
|
242
|
+
"non-empty",
|
|
243
|
+
rows.length > 0 ? "PASS" : "WARN",
|
|
244
|
+
rows.length > 0
|
|
245
|
+
? `${rows.length} log entr${rows.length === 1 ? "y" : "ies"}`
|
|
246
|
+
: "log.md has no dated entries"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- G. secret + contamination + binaries ---------------------------------
|
|
251
|
+
const terms = (config?.contaminationTerms ?? []).filter(Boolean);
|
|
252
|
+
for (const f of allFiles) {
|
|
253
|
+
const ext = path.extname(f);
|
|
254
|
+
if (!TEXT_EXTS.has(ext) && path.basename(f) !== ".gitkeep") {
|
|
255
|
+
report.add(
|
|
256
|
+
"binaries",
|
|
257
|
+
"stray",
|
|
258
|
+
"WARN",
|
|
259
|
+
`non-text file under wiki`,
|
|
260
|
+
wrel(f)
|
|
261
|
+
);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
let text;
|
|
265
|
+
try {
|
|
266
|
+
text = fs.readFileSync(f, "utf8");
|
|
267
|
+
} catch {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
for (const { name, re } of SECRET_PATTERNS) {
|
|
271
|
+
if (re.test(text))
|
|
272
|
+
report.add("secrets", "leak", "FAIL", `possible ${name}`, wrel(f));
|
|
273
|
+
}
|
|
274
|
+
// The config file legitimately lists the contamination terms it scans for; don't flag it.
|
|
275
|
+
if (path.basename(f) !== "lisa-wiki.config.json") {
|
|
276
|
+
for (const term of terms) {
|
|
277
|
+
if (text.toLowerCase().includes(term.toLowerCase())) {
|
|
278
|
+
report.add(
|
|
279
|
+
"contamination",
|
|
280
|
+
"term",
|
|
281
|
+
"FAIL",
|
|
282
|
+
`contamination term "${term}"`,
|
|
283
|
+
wrel(f)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- output + verdict -----------------------------------------------------
|
|
291
|
+
const fails = report.items.filter(i => i.status === "FAIL");
|
|
292
|
+
const warns = report.items.filter(i => i.status === "WARN");
|
|
293
|
+
const blocking = strict ? fails.length + warns.length : fails.length;
|
|
294
|
+
|
|
295
|
+
if (asJson) {
|
|
296
|
+
console.log(
|
|
297
|
+
JSON.stringify(
|
|
298
|
+
{
|
|
299
|
+
wikiRoot: rel(wikiRoot),
|
|
300
|
+
strict,
|
|
301
|
+
fails: fails.length,
|
|
302
|
+
warns: warns.length,
|
|
303
|
+
items: report.items.filter(i => i.status !== "PASS"),
|
|
304
|
+
},
|
|
305
|
+
null,
|
|
306
|
+
2
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
} else {
|
|
310
|
+
for (const i of report.items.filter(i => i.status !== "PASS")) {
|
|
311
|
+
console.log(
|
|
312
|
+
`${i.status === "FAIL" ? "✗" : "⚠"} [${i.group}] ${i.message}${i.file ? ` (${i.file})` : ""}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
console.log(
|
|
316
|
+
`\n${fails.length} fail, ${warns.length} warn${strict ? " (strict: warnings block)" : ""} — ${blocking === 0 ? "OK" : "BLOCKING"}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
process.exit(blocking === 0 ? 0 : 1);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-doctor.mjs — inspect-and-report MCP wiring for the wiki's MCP connectors.
|
|
4
|
+
* Dependency-free. Print-only: it never edits .mcp.json / .codex/config.toml or any
|
|
5
|
+
* auth file — it reports PASS/MISSING and prints setup snippets for the user to apply.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node mcp-doctor.mjs [--config <p>] [--repo <dir>]
|
|
8
|
+
* Exit 0 always (advisory). Used by /setup --doctor and /doctor group D.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { loadConfig } from "./_wiki-lib.mjs";
|
|
13
|
+
|
|
14
|
+
const argv = process.argv.slice(2);
|
|
15
|
+
const opt = (n, d) => {
|
|
16
|
+
const i = argv.indexOf(n);
|
|
17
|
+
return i !== -1 ? argv[i + 1] : d;
|
|
18
|
+
};
|
|
19
|
+
const repo = path.resolve(opt("--repo", "."));
|
|
20
|
+
const { config } = loadConfig(opt("--config"));
|
|
21
|
+
|
|
22
|
+
// Connectors that require an MCP server, and the server name to look for.
|
|
23
|
+
const MCP_CONNECTORS = {
|
|
24
|
+
jira: "atlassian",
|
|
25
|
+
confluence: "atlassian",
|
|
26
|
+
notion: "notion",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const mcpJsonPath = path.join(repo, ".mcp.json");
|
|
30
|
+
const codexTomlPath = path.join(repo, ".codex", "config.toml");
|
|
31
|
+
const mcpJson = fs.existsSync(mcpJsonPath)
|
|
32
|
+
? fs.readFileSync(mcpJsonPath, "utf8")
|
|
33
|
+
: "";
|
|
34
|
+
const codexToml = fs.existsSync(codexTomlPath)
|
|
35
|
+
? fs.readFileSync(codexTomlPath, "utf8")
|
|
36
|
+
: "";
|
|
37
|
+
|
|
38
|
+
const enabled = Object.entries(config?.connectors ?? {}).filter(
|
|
39
|
+
([name, c]) => c?.enabled && name in MCP_CONNECTORS
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (enabled.length === 0) {
|
|
43
|
+
console.log(
|
|
44
|
+
"mcp-doctor: no MCP-backed connectors enabled — nothing to check."
|
|
45
|
+
);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let missing = 0;
|
|
50
|
+
for (const [name] of enabled) {
|
|
51
|
+
const server = MCP_CONNECTORS[name];
|
|
52
|
+
const inClaude = mcpJson.includes(`"${server}"`);
|
|
53
|
+
const inCodex = codexToml.includes(`[mcp_servers.${server}]`);
|
|
54
|
+
const status = inClaude || inCodex ? "PASS" : "MISSING";
|
|
55
|
+
if (status === "MISSING") missing += 1;
|
|
56
|
+
console.log(
|
|
57
|
+
`${status === "PASS" ? "✓" : "⚠"} ${name} → MCP server "${server}": Claude=${inClaude ? "yes" : "no"} Codex=${inCodex ? "yes" : "no"}`
|
|
58
|
+
);
|
|
59
|
+
if (status === "MISSING") {
|
|
60
|
+
console.log(
|
|
61
|
+
` Add to .mcp.json: { "mcpServers": { "${server}": { ... } } }`
|
|
62
|
+
);
|
|
63
|
+
console.log(` Add to .codex/config.toml: [mcp_servers.${server}]`);
|
|
64
|
+
console.log(
|
|
65
|
+
` Then authenticate per the ${server} MCP instructions (project-owned auth).`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(
|
|
71
|
+
`\nmcp-doctor: ${enabled.length - missing}/${enabled.length} MCP connector(s) wired${missing ? ` — ${missing} MISSING (connector disabled until wired)` : ""}.`
|
|
72
|
+
);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* render-contract.mjs — render the repo-local contract snapshot from the plugin
|
|
4
|
+
* template + the project's config. Dependency-free.
|
|
5
|
+
*
|
|
6
|
+
* The plugin owns the canonical contract template (templates/llm-wiki-contract.md);
|
|
7
|
+
* this script materializes wiki/schema/llm-wiki-contract.md so the wiki stays
|
|
8
|
+
* readable/maintainable even without the plugin installed. The kernelVersion is
|
|
9
|
+
* read from the built plugin manifest and stamped into the snapshot.
|
|
10
|
+
*
|
|
11
|
+
* Usage: node render-contract.mjs [path-to-config] [--out <path>]
|
|
12
|
+
* default config: wiki/lisa-wiki.config.json (relative to cwd)
|
|
13
|
+
* default out: <wikiRoot>/schema/llm-wiki-contract.md
|
|
14
|
+
* Exit code 0 = rendered, 1 = error.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
function fail(msg) {
|
|
21
|
+
console.error(`✗ ${msg}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const pluginRoot = path.dirname(scriptDir);
|
|
27
|
+
const templatePath = path.join(pluginRoot, "templates", "llm-wiki-contract.md");
|
|
28
|
+
const manifestPath = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
29
|
+
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
const outFlagIdx = argv.indexOf("--out");
|
|
32
|
+
let outOverride;
|
|
33
|
+
if (outFlagIdx !== -1) {
|
|
34
|
+
outOverride = argv[outFlagIdx + 1];
|
|
35
|
+
if (!outOverride || outOverride.startsWith("--"))
|
|
36
|
+
fail("--out requires a path argument");
|
|
37
|
+
}
|
|
38
|
+
const configArg = argv.find(
|
|
39
|
+
(a, i) => !a.startsWith("--") && i !== outFlagIdx + 1
|
|
40
|
+
);
|
|
41
|
+
const configPath = path.resolve(configArg ?? "wiki/lisa-wiki.config.json");
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(templatePath))
|
|
44
|
+
fail(`contract template missing: ${templatePath}`);
|
|
45
|
+
if (!fs.existsSync(configPath)) fail(`config not found: ${configPath}`);
|
|
46
|
+
|
|
47
|
+
let config;
|
|
48
|
+
try {
|
|
49
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
fail(`config is not valid JSON: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let kernelVersion = "0.0.0";
|
|
55
|
+
try {
|
|
56
|
+
kernelVersion =
|
|
57
|
+
JSON.parse(fs.readFileSync(manifestPath, "utf8")).version ?? kernelVersion;
|
|
58
|
+
} catch {
|
|
59
|
+
// manifest may be absent in the src tree before build; leave default.
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const enabledConnectors = Object.entries(config.connectors ?? {})
|
|
63
|
+
.filter(([, c]) => c && c.enabled)
|
|
64
|
+
.map(([name]) => name);
|
|
65
|
+
|
|
66
|
+
const subs = {
|
|
67
|
+
org: config.org ?? "",
|
|
68
|
+
displayName: config.displayName ?? config.org ?? "LLM Wiki",
|
|
69
|
+
purpose: config.purpose ?? "(purpose not set — run /setup to define it)",
|
|
70
|
+
mode: config.mode ?? "",
|
|
71
|
+
wikiRoot: config.wikiRoot ?? "wiki",
|
|
72
|
+
kernelVersion,
|
|
73
|
+
schemaVersion: config.schemaVersion ?? "",
|
|
74
|
+
categories: (config.categories ?? []).join(", "),
|
|
75
|
+
connectors: enabledConnectors.length
|
|
76
|
+
? enabledConnectors.join(", ")
|
|
77
|
+
: "(none enabled)",
|
|
78
|
+
sourceLayout: config.sources?.layout ?? "by-system",
|
|
79
|
+
sourceRetention: config.sourceRetention ?? "sanitized-note-only",
|
|
80
|
+
sensitivityDefault: config.sensitivity?.default ?? "internal",
|
|
81
|
+
prPolicy: config.git?.prPerIngestion
|
|
82
|
+
? `PR per ingestion to ${config.git?.targetBranch ?? "main"}${config.git?.autoMerge ? " (auto-merge)" : ""}`
|
|
83
|
+
: "no PR per ingestion",
|
|
84
|
+
readmeMode: config.readme?.mode ?? "rich",
|
|
85
|
+
generatedDate: new Date().toISOString().slice(0, 10),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let rendered = fs.readFileSync(templatePath, "utf8");
|
|
89
|
+
rendered = rendered.replace(/\{\{(\w+)\}\}/g, (match, key) =>
|
|
90
|
+
key in subs ? String(subs[key]) : match
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const leftover = [
|
|
94
|
+
...new Set([...rendered.matchAll(/\{\{(\w+)\}\}/g)].map(m => m[1])),
|
|
95
|
+
];
|
|
96
|
+
if (leftover.length) {
|
|
97
|
+
fail(`unresolved template tokens in contract: ${leftover.join(", ")}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const outPath = path.resolve(
|
|
101
|
+
outOverride ?? path.join(subs.wikiRoot, "schema", "llm-wiki-contract.md")
|
|
102
|
+
);
|
|
103
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
104
|
+
fs.writeFileSync(outPath, rendered);
|
|
105
|
+
console.log(
|
|
106
|
+
`✓ rendered contract → ${path.relative(process.cwd(), outPath)} (kernelVersion ${kernelVersion})`
|
|
107
|
+
);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* rewrite-refs.mjs — deterministically fix wiki references after files have moved.
|
|
4
|
+
* Dependency-free. Assumes the files are ALREADY moved (e.g. by absorb-docs/migrate
|
|
5
|
+
* via `git mv`); this script only repairs references and verifies no dangling links.
|
|
6
|
+
*
|
|
7
|
+
* It correctly handles both directions:
|
|
8
|
+
* - a moved file's relative links to unmoved targets (re-based to its new location),
|
|
9
|
+
* - unmoved files' relative links to moved targets,
|
|
10
|
+
* - moved → moved,
|
|
11
|
+
* - plain-text `Source: <path>` citations (wiki-root-relative).
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node rewrite-refs.mjs --wiki <root> --move <oldPath>:<newPath> [--move ...] [--check]
|
|
15
|
+
* paths are resolved relative to CWD; --check verifies only (no writes).
|
|
16
|
+
* Exit 0 = rewritten/clean, 1 = dangling links remain (or bad args).
|
|
17
|
+
*/
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { walkFiles, extractMarkdownLinks } from "./_wiki-lib.mjs";
|
|
21
|
+
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
const check = argv.includes("--check");
|
|
24
|
+
const wikiArgIdx = argv.indexOf("--wiki");
|
|
25
|
+
if (
|
|
26
|
+
wikiArgIdx !== -1 &&
|
|
27
|
+
(!argv[wikiArgIdx + 1] || argv[wikiArgIdx + 1].startsWith("--"))
|
|
28
|
+
) {
|
|
29
|
+
console.error("✗ --wiki requires a path argument");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const wikiRoot = path.resolve(
|
|
33
|
+
wikiArgIdx !== -1 ? argv[wikiArgIdx + 1] : "wiki"
|
|
34
|
+
);
|
|
35
|
+
const moves = [];
|
|
36
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
37
|
+
if (argv[i] === "--move") {
|
|
38
|
+
const spec = argv[i + 1] ?? "";
|
|
39
|
+
const sep = spec.lastIndexOf(":");
|
|
40
|
+
if (sep === -1) {
|
|
41
|
+
console.error(`✗ bad --move "${spec}" (expected old:new)`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
moves.push([
|
|
45
|
+
path.resolve(spec.slice(0, sep)),
|
|
46
|
+
path.resolve(spec.slice(sep + 1)),
|
|
47
|
+
]);
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const moveMap = new Map(moves); // oldAbs -> newAbs
|
|
53
|
+
const newToOld = new Map(moves.map(([o, n]) => [n, o])); // newAbs -> oldAbs
|
|
54
|
+
const currentLocation = abs => moveMap.get(abs) ?? abs;
|
|
55
|
+
const toPosix = p => p.split(path.sep).join("/");
|
|
56
|
+
|
|
57
|
+
const isExternal = t =>
|
|
58
|
+
!t ||
|
|
59
|
+
t.startsWith("#") ||
|
|
60
|
+
/^[a-z][a-z0-9+.-]*:\/\//i.test(t) ||
|
|
61
|
+
t.startsWith("mailto:") ||
|
|
62
|
+
t.includes("{{") ||
|
|
63
|
+
t.includes("<") ||
|
|
64
|
+
t.includes(">");
|
|
65
|
+
|
|
66
|
+
let rewriteCount = 0;
|
|
67
|
+
const mdFiles = walkFiles(wikiRoot, { ext: ".md" });
|
|
68
|
+
|
|
69
|
+
for (const file of mdFiles) {
|
|
70
|
+
const oldOfFile = newToOld.get(file) ?? file; // where this file's links were authored relative to
|
|
71
|
+
const authorBase = path.dirname(oldOfFile);
|
|
72
|
+
const curDir = path.dirname(file);
|
|
73
|
+
const original = fs.readFileSync(file, "utf8");
|
|
74
|
+
|
|
75
|
+
// 1) markdown links
|
|
76
|
+
let updated = original.replace(
|
|
77
|
+
/(\]\()([^)]+)(\))/g,
|
|
78
|
+
(whole, open, target, close) => {
|
|
79
|
+
// split off an optional markdown link title: (url "title") — preserve it
|
|
80
|
+
const tm = target.match(/^(\S+)(\s[\s\S]*)?$/);
|
|
81
|
+
if (!tm) return whole;
|
|
82
|
+
const urlAnchor = tm[1];
|
|
83
|
+
const title = tm[2] ?? "";
|
|
84
|
+
if (isExternal(urlAnchor)) return whole;
|
|
85
|
+
const [pathPart, anchor] = urlAnchor.split("#");
|
|
86
|
+
const resolvedFromAuthor = path.resolve(authorBase, pathPart);
|
|
87
|
+
const correctAbs = currentLocation(resolvedFromAuthor);
|
|
88
|
+
let nextRel = toPosix(path.relative(curDir, correctAbs));
|
|
89
|
+
if (!nextRel.startsWith(".")) nextRel = `./${nextRel}`;
|
|
90
|
+
const nextUrl = anchor !== undefined ? `${nextRel}#${anchor}` : nextRel;
|
|
91
|
+
const nextTarget = `${nextUrl}${title}`;
|
|
92
|
+
return nextTarget === target ? whole : `${open}${nextTarget}${close}`;
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// 2) plain-text citations: Source: <wiki-root-relative path>
|
|
97
|
+
updated = updated.replace(
|
|
98
|
+
/(Source:\s*)([^\s)]+\.md)/g,
|
|
99
|
+
(whole, pre, cite) => {
|
|
100
|
+
if (cite.includes("{{") || cite.includes("<")) return whole;
|
|
101
|
+
// citations are wiki-root-relative (or repo-relative); map via moveMap if they point to a moved file
|
|
102
|
+
const candidates = [
|
|
103
|
+
path.resolve(wikiRoot, cite),
|
|
104
|
+
path.resolve(wikiRoot, "..", cite),
|
|
105
|
+
];
|
|
106
|
+
const hit = candidates.find(c => moveMap.has(c));
|
|
107
|
+
if (!hit) return whole;
|
|
108
|
+
const nextAbs = moveMap.get(hit);
|
|
109
|
+
const nextCite = toPosix(path.relative(wikiRoot, nextAbs));
|
|
110
|
+
return `${pre}${nextCite}`;
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (updated !== original) {
|
|
115
|
+
rewriteCount += 1;
|
|
116
|
+
if (!check) fs.writeFileSync(file, updated);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// verification: no dangling internal .md links remain (at current locations)
|
|
121
|
+
const dangling = [];
|
|
122
|
+
for (const file of mdFiles) {
|
|
123
|
+
const text = fs.readFileSync(file, "utf8");
|
|
124
|
+
for (const target of extractMarkdownLinks(text)) {
|
|
125
|
+
const resolved = path.resolve(path.dirname(file), target);
|
|
126
|
+
if (resolved.endsWith(".md") && !fs.existsSync(resolved)) {
|
|
127
|
+
dangling.push(`${toPosix(path.relative(wikiRoot, file))} → ${target}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (dangling.length > 0) {
|
|
133
|
+
console.error(
|
|
134
|
+
`✗ ${dangling.length} dangling internal link(s) ${check ? "found" : "remain after rewrite"}:`
|
|
135
|
+
);
|
|
136
|
+
for (const d of dangling) console.error(` - ${d}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(
|
|
141
|
+
check
|
|
142
|
+
? `✓ no dangling internal links (${mdFiles.length} files checked).`
|
|
143
|
+
: `✓ rewrote refs in ${rewriteCount} file(s); no dangling internal links remain.`
|
|
144
|
+
);
|