rvlib-shipkit 0.1.0
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/CHANGELOG.md +21 -0
- package/README.md +58 -0
- package/package.json +19 -0
- package/src/asks/asks-queue.ts +95 -0
- package/src/cli/shipkit-asks.ts +26 -0
- package/src/cli/shipkit-check.ts +26 -0
- package/src/cli/shipkit-ci.ts +30 -0
- package/src/cli/shipkit-contracts.ts +26 -0
- package/src/cli/shipkit-init.ts +40 -0
- package/src/cli/shipkit-wiki.ts +74 -0
- package/src/cli.ts +28 -0
- package/src/contracts/contract-manifests.ts +10 -0
- package/src/contracts/contract-types.ts +77 -0
- package/src/contracts/contracts-engine.ts +186 -0
- package/src/contracts/manifests/discord.contract.ts +15 -0
- package/src/contracts/manifests/identity.contract.ts +19 -0
- package/src/contracts/manifests/itch.contract.ts +16 -0
- package/src/contracts/manifests/social.contract.ts +18 -0
- package/src/contracts/manifests/steam.contract.ts +28 -0
- package/src/core/check-ctx.ts +63 -0
- package/src/core/check-suite.ts +179 -0
- package/src/core/task-runner.ts +274 -0
- package/src/core/wire.ts +36 -0
- package/src/repo-config.ts +119 -0
- package/src/shipkit.ts +23 -0
- package/src/wiki/graph-page.ts +169 -0
- package/src/wiki/lint-md.ts +24 -0
- package/src/wiki/md-graph.ts +6 -0
- package/src/wiki/scan-md.ts +108 -0
- package/src/wiki/serve-graph.ts +19 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Generic markdown-repo scanner → MdGraph, so any repo gets a graph with zero
|
|
2
|
+
// custom parsing. Detected link forms (fenced code is invisible):
|
|
3
|
+
// @path eager — loaded with the file that names it
|
|
4
|
+
// → path.md lazy — resolved from the scan root, then file-relative
|
|
5
|
+
// [text](path.md) lazy — file-relative
|
|
6
|
+
// `path.md` lazy — file-relative, then from the scan root
|
|
7
|
+
// Depth = 0-1 BFS from the index (eager edge keeps depth, lazy adds 1);
|
|
8
|
+
// unreachable files stay in the graph with no depth (lint-md.ts turns those,
|
|
9
|
+
// plus dangling refs, into failures — the graph itself is a viewer).
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
11
|
+
import { dirname, join, relative, resolve } from 'path'
|
|
12
|
+
import type { MdEdge, MdGraph, MdNode } from 'rvlib-shipkit/src/wiki/md-graph.ts'
|
|
13
|
+
|
|
14
|
+
export type ScanMdConfig = {
|
|
15
|
+
root: string // absolute dir to scan
|
|
16
|
+
index?: string // root-relative entry the BFS starts from (default: index.md AND README.md, both)
|
|
17
|
+
skipDirs?: string[] // dir names to skip on top of node_modules/.git/dist/tmp
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type MdDanglingRef = { from: string; token: string } // an @/→/[](…) ref whose target is not in the scan
|
|
21
|
+
export type MdScan = {
|
|
22
|
+
graph: MdGraph
|
|
23
|
+
index: string | null // resolved entrypoint(s), ' + '-joined when both defaults exist; null = none found
|
|
24
|
+
dangling: MdDanglingRef[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function scanMdGraph(cfg: ScanMdConfig): MdGraph {
|
|
28
|
+
return scanMd(cfg).graph
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function scanMd(cfg: ScanMdConfig): MdScan {
|
|
32
|
+
const skip = new Set(['node_modules', '.git', 'dist', 'tmp', ...(cfg.skipDirs ?? [])])
|
|
33
|
+
const files: string[] = []
|
|
34
|
+
const walk = (rel: string) => {
|
|
35
|
+
for (const e of readdirSync(join(cfg.root, rel))) {
|
|
36
|
+
const r = rel === '' ? e : `${rel}/${e}`
|
|
37
|
+
if (statSync(join(cfg.root, r)).isDirectory()) {
|
|
38
|
+
if (!skip.has(e) && !e.startsWith('.')) walk(r)
|
|
39
|
+
} else if (e.endsWith('.md')) files.push(r)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk('')
|
|
43
|
+
files.sort()
|
|
44
|
+
const inGraph = new Set(files)
|
|
45
|
+
|
|
46
|
+
const resolveRef = (fromRel: string, token: string): string | null => {
|
|
47
|
+
for (const cand of [resolve(join(cfg.root, dirname(fromRel)), token), resolve(cfg.root, token)]) {
|
|
48
|
+
const rel = relative(cfg.root, cand)
|
|
49
|
+
if (!rel.startsWith('..') && inGraph.has(rel)) return rel
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
const edgesOf = (rel: string): MdEdge[] => {
|
|
54
|
+
const raw = readFileSync(join(cfg.root, rel), 'utf8')
|
|
55
|
+
const body = raw.startsWith('---\n') ? raw.slice(4).replace(/^[\s\S]*?^---\s*$/m, '') : raw
|
|
56
|
+
const spans: string[] = []
|
|
57
|
+
const prose = body.replace(/^```[\s\S]*?^```\s*$/gm, '').replace(/`([^`\n]+)`/g, (_, s: string) => { spans.push(s); return ' ' })
|
|
58
|
+
const out: MdEdge[] = []
|
|
59
|
+
// strict: @/→/[](…) are deliberate wiki refs — a miss is a dangling ref.
|
|
60
|
+
// Backtick spans mention paths casually (other repos, deleted examples) — resolve or ignore, never fail.
|
|
61
|
+
const push = (token: string, eager: boolean, strict: boolean) => {
|
|
62
|
+
const to = resolveRef(rel, token)
|
|
63
|
+
if (to != null && to !== rel) out.push({ from: rel, to, eager })
|
|
64
|
+
else if (to == null && strict) dangling.push({ from: rel, token })
|
|
65
|
+
}
|
|
66
|
+
for (const m of prose.matchAll(/(?:^|[\s(])@([\w][\w./-]*\.md)/gm)) push(m[1]!, true, true)
|
|
67
|
+
for (const m of prose.matchAll(/→\s*([\w][\w./-]*\.md)\b/g)) push(m[1]!, false, true)
|
|
68
|
+
for (const m of prose.matchAll(/\]\(([^)\s:]+\.md)\)/g)) push(m[1]!, false, true)
|
|
69
|
+
for (const s of spans) for (const m of s.matchAll(/[\w][\w./-]*\.md/g)) push(m[0], false, false)
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
const dangling: MdDanglingRef[] = []
|
|
73
|
+
const edges: MdEdge[] = []
|
|
74
|
+
const seen = new Set<string>()
|
|
75
|
+
for (const f of files) for (const e of edgesOf(f)) {
|
|
76
|
+
const key = `${e.from}>${e.to}:${e.eager}`
|
|
77
|
+
if (!seen.has(key)) { seen.add(key); edges.push(e) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// explicit index = the only entry; default = index.md AND README.md both
|
|
81
|
+
// seed the walk (a repo readme is an entrypoint too, never an orphan)
|
|
82
|
+
const seeds = (cfg.index != null ? [cfg.index] : ['index.md', 'README.md']).filter((s) => inGraph.has(s))
|
|
83
|
+
const depths = new Map<string, number>()
|
|
84
|
+
{
|
|
85
|
+
const deque: string[] = []
|
|
86
|
+
for (const s of seeds) { depths.set(s, 0); deque.push(s) }
|
|
87
|
+
while (deque.length > 0) {
|
|
88
|
+
const cur = deque.shift()!
|
|
89
|
+
for (const e of edges) {
|
|
90
|
+
if (e.from !== cur) continue
|
|
91
|
+
const d = depths.get(cur)! + (e.eager ? 0 : 1)
|
|
92
|
+
const known = depths.get(e.to)
|
|
93
|
+
if (known != null && known <= d) continue
|
|
94
|
+
depths.set(e.to, d)
|
|
95
|
+
if (e.eager) deque.unshift(e.to)
|
|
96
|
+
else deque.push(e.to)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const nodes: MdNode[] = files.map((f) => ({
|
|
101
|
+
id: f,
|
|
102
|
+
label: f.replace(/\.md$/, '').split('/').at(-1)!,
|
|
103
|
+
group: f.includes('/') ? f.split('/')[0]! : null,
|
|
104
|
+
eager: depths.get(f) === 0,
|
|
105
|
+
...(depths.has(f) ? { depth: depths.get(f)! } : {}),
|
|
106
|
+
}))
|
|
107
|
+
return { graph: { nodes, edges }, index: seeds.join(' + ') || null, dangling }
|
|
108
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Local graph server: `load()` runs on EVERY request, so a reload always shows
|
|
2
|
+
// the wiki as it is on disk right now — no stale exports, no build step.
|
|
3
|
+
import { graphHtml } from 'rvlib-shipkit/src/wiki/graph-page.ts'
|
|
4
|
+
import type { MdGraph } from 'rvlib-shipkit/src/wiki/md-graph.ts'
|
|
5
|
+
|
|
6
|
+
export type ServeMdGraphConfig = { port: number; load: () => MdGraph; title?: string }
|
|
7
|
+
|
|
8
|
+
// routes: `/` the interactive graph · `/graph.json` the raw graph data
|
|
9
|
+
export function serveMdGraph(cfg: ServeMdGraphConfig) {
|
|
10
|
+
return Bun.serve({
|
|
11
|
+
port: cfg.port,
|
|
12
|
+
fetch(req) {
|
|
13
|
+
const path = new URL(req.url).pathname
|
|
14
|
+
if (path === '/') return new Response(graphHtml(cfg.load(), { title: cfg.title }), { headers: { 'content-type': 'text/html; charset=utf-8' } })
|
|
15
|
+
if (path === '/graph.json') return Response.json(cfg.load())
|
|
16
|
+
return new Response('not found', { status: 404 })
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
}
|