rvlib-shipkit 0.38.1 → 1.0.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 +367 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// the config surface describing itself: every `repo.config.ts` key, and every
|
|
2
|
+
// key nested under one, rendered from the JSDoc that sits above the field.
|
|
3
|
+
// The TYPE is the one source. A hand-written key list is a second one, and it
|
|
4
|
+
// drifts the day a key lands, which is how eight keys came to exist in the
|
|
5
|
+
// code and on no page.
|
|
6
|
+
//
|
|
7
|
+
// The reader is a tiny scanner rather than the TypeScript compiler: the shapes
|
|
8
|
+
// it must read are plain `export type X = { ... }` object literals, the toolkit
|
|
9
|
+
// already owns a module graph but not a type checker, and a config surface that
|
|
10
|
+
// needs a compiler to document itself is a surface nobody documents.
|
|
11
|
+
import { existsSync, readFileSync } from 'fs'
|
|
12
|
+
import { dirname, resolve } from 'path'
|
|
13
|
+
|
|
14
|
+
export type ConfigKey = {
|
|
15
|
+
/** dotted path from the root type, `serve.port` */
|
|
16
|
+
path: string
|
|
17
|
+
name: string
|
|
18
|
+
/** the type text as the source writes it */
|
|
19
|
+
type: string
|
|
20
|
+
/** false = the field carries no `?`, so a repo that declares the parent must declare this */
|
|
21
|
+
optional: boolean
|
|
22
|
+
/** the JSDoc above the field, collapsed to one line. Empty = undocumented */
|
|
23
|
+
doc: string
|
|
24
|
+
/** 1 for a top-level key */
|
|
25
|
+
depth: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Member = {
|
|
29
|
+
name: string
|
|
30
|
+
optional: boolean
|
|
31
|
+
/** the type text, comments stripped and whitespace collapsed, for display */
|
|
32
|
+
type: string
|
|
33
|
+
/** the type text as the source wrote it, which is what the walk reads: an
|
|
34
|
+
* inline object keeps its line breaks, and those are its separators */
|
|
35
|
+
raw: string
|
|
36
|
+
doc: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** the type text a page prints: no nested doc comments, one line. An inline
|
|
40
|
+
* object separates its fields by NEWLINE in the source, so the newline
|
|
41
|
+
* becomes the `;` a reader expects on one line. */
|
|
42
|
+
export const displayType = (raw: string): string =>
|
|
43
|
+
raw
|
|
44
|
+
// one pass, whichever comment opens first owns the text
|
|
45
|
+
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '\n')
|
|
46
|
+
.replace(/[^\S\n]+/g, ' ')
|
|
47
|
+
.replace(/\s*\n+\s*/g, '; ')
|
|
48
|
+
.replace(/([{([,;])\s*;\s*/g, '$1 ')
|
|
49
|
+
.replace(/;\s*([)\]}])/g, ' $1')
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.replace(/^;\s*|\s*;$/g, '')
|
|
52
|
+
.trim()
|
|
53
|
+
|
|
54
|
+
/** the `{ ... }` body of `type <name> = {`, braces balanced. null = no such type here */
|
|
55
|
+
export function typeBody(src: string, name: string): string | null {
|
|
56
|
+
const decl = new RegExp(String.raw`(?:^|\n)\s*(?:export\s+)?type\s+${name}\s*=\s*\{`).exec(src)
|
|
57
|
+
if (decl == null) return null
|
|
58
|
+
const open = src.indexOf('{', decl.index + decl[0].length - 1)
|
|
59
|
+
const end = matchBrace(src, open)
|
|
60
|
+
return end === -1 ? null : src.slice(open + 1, end)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** index of the `}` closing the `{` at `open`, strings and comments skipped */
|
|
64
|
+
function matchBrace(src: string, open: number): number {
|
|
65
|
+
let depth = 0
|
|
66
|
+
for (let i = open; i < src.length; i++) {
|
|
67
|
+
const ch = src[i]!
|
|
68
|
+
if (ch === '/' && src[i + 1] === '*') { i = src.indexOf('*/', i); if (i === -1) return -1; i++; continue }
|
|
69
|
+
if (ch === '/' && src[i + 1] === '/') { const nl = src.indexOf('\n', i); if (nl === -1) return -1; i = nl; continue }
|
|
70
|
+
if (ch === "'" || ch === '"' || ch === '`') { i = skipString(src, i); continue }
|
|
71
|
+
if (ch === '{') depth++
|
|
72
|
+
else if (ch === '}' && --depth === 0) return i
|
|
73
|
+
}
|
|
74
|
+
return -1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function skipString(src: string, i: number): number {
|
|
78
|
+
const quote = src[i]!
|
|
79
|
+
for (let j = i + 1; j < src.length; j++) {
|
|
80
|
+
if (src[j] === '\\') { j++; continue }
|
|
81
|
+
if (src[j] === quote) return j
|
|
82
|
+
}
|
|
83
|
+
return src.length
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const DOC_LINE = /^\s*\*\s?/
|
|
87
|
+
|
|
88
|
+
/** a JSDoc block collapsed to ONE line: the page renders one bullet per key,
|
|
89
|
+
* and a comment's line breaks are its own formatting, not sentences. */
|
|
90
|
+
export function collapseDoc(raw: string): string {
|
|
91
|
+
return raw
|
|
92
|
+
.replace(/^\/\*\*/, '')
|
|
93
|
+
.replace(/\*\/$/, '')
|
|
94
|
+
.split('\n')
|
|
95
|
+
.map((l) => l.replace(DOC_LINE, '').trim())
|
|
96
|
+
.join(' ')
|
|
97
|
+
.replace(/\s+/g, ' ')
|
|
98
|
+
.trim()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** the members of one object-type body, in source order, each with the JSDoc
|
|
102
|
+
* that precedes it. Anything the scanner cannot read is skipped rather than
|
|
103
|
+
* guessed at: a wrong doc line is worse than a missing one. */
|
|
104
|
+
export function parseMembers(body: string): Member[] {
|
|
105
|
+
const out: Member[] = []
|
|
106
|
+
let doc = ''
|
|
107
|
+
let i = 0
|
|
108
|
+
while (i < body.length) {
|
|
109
|
+
const ch = body[i]!
|
|
110
|
+
if (ch === '/' && body[i + 1] === '*') {
|
|
111
|
+
const end = body.indexOf('*/', i)
|
|
112
|
+
const raw = body.slice(i, end === -1 ? body.length : end + 2)
|
|
113
|
+
doc = body[i + 2] === '*' ? collapseDoc(raw) : doc
|
|
114
|
+
i = end === -1 ? body.length : end + 2
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (ch === '/' && body[i + 1] === '/') { const nl = body.indexOf('\n', i); i = nl === -1 ? body.length : nl + 1; continue }
|
|
118
|
+
if (/\s|;|,/.test(ch)) { i++; continue }
|
|
119
|
+
const m = /^(?:readonly\s+)?([A-Za-z_$][\w$]*)(\?)?\s*:/.exec(body.slice(i))
|
|
120
|
+
if (m == null) { i = skipToSeparator(body, i); doc = ''; continue }
|
|
121
|
+
const start = i + m[0].length
|
|
122
|
+
const end = skipToSeparator(body, start)
|
|
123
|
+
const raw = body.slice(start, end).trim()
|
|
124
|
+
out.push({ name: m[1]!, optional: m[2] != null, type: displayType(raw), raw, doc })
|
|
125
|
+
doc = ''
|
|
126
|
+
i = end
|
|
127
|
+
}
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** index of the `;`, `,`, trailing `//` or newline that ends a member, at
|
|
132
|
+
* nesting depth 0. Generics count as nesting, or `Record<string, string>`
|
|
133
|
+
* ends a key halfway through its own type. */
|
|
134
|
+
function skipToSeparator(body: string, from: number): number {
|
|
135
|
+
let depth = 0
|
|
136
|
+
for (let i = from; i < body.length; i++) {
|
|
137
|
+
const ch = body[i]!
|
|
138
|
+
// a comment INSIDE an inline object is skipped whole: its backticks and
|
|
139
|
+
// apostrophes are prose, and reading them as string quotes walks the
|
|
140
|
+
// scanner past the member it is measuring
|
|
141
|
+
if (ch === '/' && body[i + 1] === '*') { const e = body.indexOf('*/', i); i = e === -1 ? body.length : e + 1; continue }
|
|
142
|
+
if (ch === '/' && body[i + 1] === '/') {
|
|
143
|
+
if (depth === 0) return i
|
|
144
|
+
const nl = body.indexOf('\n', i)
|
|
145
|
+
i = nl === -1 ? body.length : nl
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (ch === "'" || ch === '"' || ch === '`') { i = skipString(body, i); continue }
|
|
149
|
+
if (ch === '{' || ch === '[' || ch === '(' || ch === '<') depth++
|
|
150
|
+
// `=>` is an arrow, never a closing generic
|
|
151
|
+
else if (ch === '}' || ch === ']' || ch === ')' || (ch === '>' && body[i - 1] !== '=')) depth--
|
|
152
|
+
else if (depth === 0 && (ch === ';' || ch === ',' || ch === '\n')) return i
|
|
153
|
+
}
|
|
154
|
+
return body.length
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `import type { A, B } from './x.ts'` → name → absolute file. Where a type
|
|
158
|
+
* LIVES is what lets the walk follow `serve?: ServeConfig` into another
|
|
159
|
+
* module, which is where most of the nested keys are. */
|
|
160
|
+
export function importMap(src: string, file: string): Map<string, string> {
|
|
161
|
+
const out = new Map<string, string>()
|
|
162
|
+
const dir = dirname(file)
|
|
163
|
+
for (const m of src.matchAll(/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
164
|
+
const spec = m[2]!
|
|
165
|
+
if (!spec.startsWith('.')) continue
|
|
166
|
+
for (const raw of m[1]!.split(',')) {
|
|
167
|
+
const name = raw.replace(/^\s*type\s+/, '').trim().split(/\s+as\s+/)[0]
|
|
168
|
+
if (name != null && name !== '') out.set(name, resolve(dir, spec))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** the named type a member's text points at, or null when nothing can be
|
|
175
|
+
* followed: a function type, a union of literals, a `Record<>`, a primitive.
|
|
176
|
+
* Only a bare identifier (with an optional `[]`) is followed, because a
|
|
177
|
+
* scanner guessing its way INTO a type is how a page ends up documenting the
|
|
178
|
+
* wrong shape. */
|
|
179
|
+
export function namedTypeOf(type: string): string | null {
|
|
180
|
+
const t = type.replace(/\[\]$/, '').trim()
|
|
181
|
+
return /^[A-Z][\w$]*$/.test(t) ? t : null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** the inline `{ ... }` body a member declares, or null */
|
|
185
|
+
export function inlineBodyOf(type: string): string | null {
|
|
186
|
+
const open = type.indexOf('{')
|
|
187
|
+
if (open === -1) return null
|
|
188
|
+
const end = matchBrace(type, open)
|
|
189
|
+
return end === -1 ? null : type.slice(open + 1, end)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const MAX_DEPTH = 3
|
|
193
|
+
|
|
194
|
+
/** every key of `root`, depth first, nested keys included. Each file is read
|
|
195
|
+
* once, and a type already on the current branch is never re-entered. */
|
|
196
|
+
export function configKeys(file: string, root: string, maxDepth: number = MAX_DEPTH): ConfigKey[] {
|
|
197
|
+
const cache = new Map<string, string>()
|
|
198
|
+
const read = (f: string): string => {
|
|
199
|
+
const hit = cache.get(f)
|
|
200
|
+
if (hit != null) return hit
|
|
201
|
+
const text = existsSync(f) ? readFileSync(f, 'utf8') : ''
|
|
202
|
+
cache.set(f, text)
|
|
203
|
+
return text
|
|
204
|
+
}
|
|
205
|
+
const out: ConfigKey[] = []
|
|
206
|
+
const walk = (body: string, ownerFile: string, prefix: string, depth: number, seen: string[]): void => {
|
|
207
|
+
for (const m of parseMembers(body)) {
|
|
208
|
+
const path = prefix === '' ? m.name : `${prefix}.${m.name}`
|
|
209
|
+
out.push({ path, name: m.name, type: m.type, optional: m.optional, doc: m.doc, depth })
|
|
210
|
+
if (depth >= maxDepth) continue
|
|
211
|
+
const inline = inlineBodyOf(m.raw)
|
|
212
|
+
if (inline != null) { walk(inline, ownerFile, path, depth + 1, seen); continue }
|
|
213
|
+
const named = namedTypeOf(m.raw)
|
|
214
|
+
if (named == null || seen.includes(named)) continue
|
|
215
|
+
const src = read(ownerFile)
|
|
216
|
+
const own = typeBody(src, named)
|
|
217
|
+
if (own != null) { walk(own, ownerFile, path, depth + 1, [...seen, named]); continue }
|
|
218
|
+
const from = importMap(src, ownerFile).get(named)
|
|
219
|
+
if (from == null) continue
|
|
220
|
+
const nested = typeBody(read(from), named)
|
|
221
|
+
if (nested != null) walk(nested, from, path, depth + 1, [...seen, named])
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const rootBody = typeBody(read(file), root)
|
|
225
|
+
if (rootBody == null) throw new Error(`config-keys: no type ${root} in ${file}`)
|
|
226
|
+
walk(rootBody, file, '', 1, [root])
|
|
227
|
+
return out
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** one bullet per key, nested keys indented under their parent. The type text
|
|
231
|
+
* rides in the bullet because half of these keys are objects, and a name and
|
|
232
|
+
* a sentence do not say what to type. */
|
|
233
|
+
export function renderKeys(keys: ConfigKey[]): string {
|
|
234
|
+
const lines = keys.map((k) => {
|
|
235
|
+
const indent = ' '.repeat(k.depth - 1)
|
|
236
|
+
const sig = `\`${k.name}${k.optional ? '?' : ''}: ${k.type}\``
|
|
237
|
+
const required = k.optional ? '' : ' REQUIRED.'
|
|
238
|
+
const doc = k.doc === '' ? 'undocumented in the source' : k.doc
|
|
239
|
+
return `${indent}- ${sig} —${required} ${doc}`
|
|
240
|
+
})
|
|
241
|
+
return lines.join('\n')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** the keys a source file declares but never documents. The page is generated,
|
|
245
|
+
* so a missing sentence is a defect in the TYPE, and this is what names it. */
|
|
246
|
+
export const undocumented = (keys: ConfigKey[]): string[] => keys.filter((k) => k.doc === '').map((k) => k.path)
|
|
247
|
+
|
|
248
|
+
export const BEGIN = '<!-- shipkit:config-keys -->'
|
|
249
|
+
export const END = '<!-- /shipkit:config-keys -->'
|
|
250
|
+
|
|
251
|
+
/** the second generated block on the same page: the CHECK shape. A check is not
|
|
252
|
+
* config, it is a file, but its fields are the other half of what a repo
|
|
253
|
+
* declares, and a field that lands with no doc line is a surface nobody can
|
|
254
|
+
* find. Generated from the type for the same reason the first block is. */
|
|
255
|
+
export const CHECK_BEGIN = '<!-- shipkit:check-keys -->'
|
|
256
|
+
export const CHECK_END = '<!-- /shipkit:check-keys -->'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// THE CONFIG HALF of the SK016 migration, in its own process. Writing the
|
|
2
|
+
// managed block and reading the result back both LOAD repo.config.ts as a
|
|
3
|
+
// module, which no check context read can stand for, so the fixer reaches it
|
|
4
|
+
// through `c.exec` and the check file itself never imports the loader.
|
|
5
|
+
//
|
|
6
|
+
// bun config-rewrite.ts <root> '<plan json>' → one json line on stdout
|
|
7
|
+
//
|
|
8
|
+
// The order is the migration's: the `when` entries first, then each key is
|
|
9
|
+
// dropped only once what leaned on it has a home, then the result is read
|
|
10
|
+
// back through the loader and the previous bytes go back when it does not
|
|
11
|
+
// survive.
|
|
12
|
+
import { readFileSync } from 'fs'
|
|
13
|
+
import { join } from 'path'
|
|
14
|
+
import { atomicWrite } from './locked-write.ts'
|
|
15
|
+
import { dropConfigKey } from './legacy-shapes.ts'
|
|
16
|
+
import { writeGateBlock } from './when-block.ts'
|
|
17
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
18
|
+
|
|
19
|
+
export type ConfigPlan = {
|
|
20
|
+
/** check id → `when` value, written into the managed block */
|
|
21
|
+
when: Record<string, string>
|
|
22
|
+
/** keys to drop, in order. `afterWhen` drops only once the block took the entries */
|
|
23
|
+
drop: { key: string; why: string; afterWhen?: boolean }[]
|
|
24
|
+
/** the config's `name` before the rewrite: what the read back must still say */
|
|
25
|
+
name: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ConfigRewrite = {
|
|
29
|
+
did: string[]
|
|
30
|
+
hand: string[]
|
|
31
|
+
/** set when the rewrite did not load back and the previous bytes went back */
|
|
32
|
+
restored?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function rewriteConfig(root: string, plan: ConfigPlan): Promise<ConfigRewrite> {
|
|
36
|
+
const did: string[] = []
|
|
37
|
+
const hand: string[] = []
|
|
38
|
+
let whenWritten = false
|
|
39
|
+
if (Object.keys(plan.when).length > 0) {
|
|
40
|
+
try {
|
|
41
|
+
await writeGateBlock(root, { when: plan.when })
|
|
42
|
+
whenWritten = true
|
|
43
|
+
did.push(`${Object.keys(plan.when).length} when entr(ies) written into the managed block`)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
hand.push(`the \`when\` block refused the rewrite (${String(e).slice(0, 160)}) — write ${JSON.stringify(plan.when)} into it by hand`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const path = join(root, 'repo.config.ts')
|
|
49
|
+
const before = readFileSync(path, 'utf8')
|
|
50
|
+
let text = before
|
|
51
|
+
for (const d of plan.drop) {
|
|
52
|
+
if (d.afterWhen === true && !whenWritten) continue
|
|
53
|
+
const next = dropConfigKey(text, d.key)
|
|
54
|
+
if (next == null) hand.push(`repo.config.ts \`${d.key}\`: ${d.why}, and the key could not be read mechanically — delete it by hand`)
|
|
55
|
+
else { text = next; did.push(`repo.config.ts drops \`${d.key}\``) }
|
|
56
|
+
}
|
|
57
|
+
if (text !== before) {
|
|
58
|
+
atomicWrite(path, text)
|
|
59
|
+
try {
|
|
60
|
+
const after = await loadRepoConfig(root, { fresh: true })
|
|
61
|
+
if (after.name !== plan.name) throw new Error(`the config reads as '${String(after.name)}'`)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
atomicWrite(path, before)
|
|
64
|
+
return { did, hand, restored: String(e).slice(0, 160) }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { did, hand }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (import.meta.main) {
|
|
71
|
+
const [root, plan] = process.argv.slice(2)
|
|
72
|
+
if (root == null || plan == null) {
|
|
73
|
+
console.error('usage: bun config-rewrite.ts <root> <plan json>')
|
|
74
|
+
process.exit(2)
|
|
75
|
+
}
|
|
76
|
+
console.log(JSON.stringify(await rewriteConfig(root, JSON.parse(plan) as ConfigPlan)))
|
|
77
|
+
}
|
|
@@ -1,102 +1,178 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// `*.check.ts` discovery — THE way anything gets into a gate. Drop a file
|
|
2
|
+
// anywhere in the repo, default-export `check({...})`, and every surface sees
|
|
3
|
+
// it with no config edit. The toolkit's own built-ins are the same files
|
|
4
|
+
// under `src/checks/`, walked by this same code, so a built-in and a repo
|
|
5
|
+
// check differ by `from` and by nothing else.
|
|
6
|
+
//
|
|
7
|
+
// Discovery IMPORTS each check file. That is what buys the shape a function
|
|
8
|
+
// body, a fixer and a typed declaration instead of four regexes, and it is why
|
|
9
|
+
// a check file keeps its metadata imports at the top level and its BODY's
|
|
10
|
+
// imports inside `run` or behind `import.meta.main`. The budget is 300ms for a
|
|
11
|
+
// real repo, pinned by tests/check-shape.test.ts.
|
|
12
|
+
//
|
|
13
|
+
// The suffix is shared with the legacy check SCRIPT (named `export const`
|
|
14
|
+
// declarations, a body that runs on import), read through 0.40 by
|
|
15
|
+
// core/discover-legacy-checks.ts. A script must never be imported, so the two
|
|
16
|
+
// are told apart by reading the source first (`isLegacyCheckSource`).
|
|
17
|
+
//
|
|
18
|
+
// Skipped dirs: node_modules, dist, tmp, references (templates for other
|
|
19
|
+
// repos), and every dot-dir.
|
|
20
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from 'fs'
|
|
10
21
|
import { basename, join, relative } from 'path'
|
|
22
|
+
import { check, type CheckDef, type CheckFrom } from '../check.ts'
|
|
23
|
+
|
|
24
|
+
import { CHECK_SUFFIX as SUFFIX, fallbackNames, isLegacyCheckSource, SKIPPED_DIRS as IGNORED_DIRS } from './check-file-names.ts'
|
|
25
|
+
|
|
26
|
+
export { fallbackNames, isLegacyCheckSource } from './check-file-names.ts'
|
|
11
27
|
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
/** where the toolkit keeps its own checks. A second discovery root, never a
|
|
29
|
+
* registry: adding a built-in is adding a file here. */
|
|
30
|
+
export const BUILTIN_CHECK_DIR = join(import.meta.dir, '../checks')
|
|
31
|
+
|
|
32
|
+
/** an SK id lives in the FILE NAME, so the built-in id namespace is readable
|
|
33
|
+
* without importing one body. `defineRepo` needs it at config load, which
|
|
34
|
+
* is synchronous, and a second hand written list would drift. */
|
|
35
|
+
const BUILTIN_ID_RE = /^([A-Z]{2,4}\d{3})-/
|
|
14
36
|
|
|
15
37
|
export type DiscoveredCheck = {
|
|
16
|
-
/**
|
|
38
|
+
/** what the file declared */
|
|
39
|
+
def: CheckDef
|
|
40
|
+
/** absolute path to the file */
|
|
17
41
|
path: string
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* the
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
/** repo-relative path, null for a built-in: it lives outside the repo,
|
|
43
|
+
* and the toolchain identity already covers the toolkit's own source */
|
|
44
|
+
rel: string | null
|
|
45
|
+
/** the name every surface uses: the declared id, else the file basename,
|
|
46
|
+
* else the repo-relative path when two files share a basename */
|
|
47
|
+
id: string
|
|
48
|
+
title: string
|
|
49
|
+
/** the one sentence the file wrote about itself, always present: the
|
|
50
|
+
* shape refuses a check file without one */
|
|
51
|
+
description: string
|
|
52
|
+
/** where the answer is owned: a wiki page, else the file itself */
|
|
53
|
+
owner: string
|
|
54
|
+
from: CheckFrom
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function walk(dir: string, out: string[]): void {
|
|
58
|
+
let entries
|
|
59
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
if (e.isDirectory()) {
|
|
62
|
+
if (!IGNORED_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), out)
|
|
63
|
+
} else if (e.name.endsWith(SUFFIX)) out.push(join(dir, e.name))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** every `*.check.ts` file under a repo root, check files and legacy
|
|
68
|
+
* scripts alike, by the same walk discovery takes, without importing one.
|
|
69
|
+
* Sorted. */
|
|
70
|
+
export function repoCheckFiles(root: string): string[] {
|
|
71
|
+
const out: string[] = []
|
|
72
|
+
walk(root, out)
|
|
73
|
+
return out.sort()
|
|
41
74
|
}
|
|
42
75
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const TAIL = String.raw`(?:\s+as\s+const)?\s*;?\s*$`
|
|
49
|
-
const BLOCKING_RE = new RegExp(String.raw`^\s*export\s+const\s+blocking\s*(?::\s*boolean\s*)?=\s*(true|false)` + TAIL, 'm')
|
|
50
|
-
// `export const inputs = ['a', 'b']`, single or multi line. Anything cleverer
|
|
51
|
-
// (a computed list, a spread) is deliberately unreadable here and lands as no
|
|
52
|
-
// declaration, which means never cached: a scanner guessing at a program is
|
|
53
|
-
// how a key comes to describe files the unit does not read.
|
|
54
|
-
const INPUTS_RE = /^\s*export\s+const\s+inputs\s*(?::[^=]*)?=\s*\[([^\]]*)\]/m
|
|
55
|
-
const TTL_RE = /^\s*export\s+const\s+ttl\s*(?::\s*string\s*)?=\s*['"]([^'"]+)['"]/m
|
|
56
|
-
const NEVER_RE = /^\s*export\s+const\s+never\s*(?::\s*string\s*)?=\s*['"]([^'"]+)['"]/m
|
|
57
|
-
// `export const gate = false` takes a check out of `shipkit ci`. Only the
|
|
58
|
-
// literal false opts out: anything unreadable stays in the gate, because a
|
|
59
|
-
// scanner that guesses its way to SKIPPING a check is how coverage disappears
|
|
60
|
-
const GATE_RE = new RegExp(String.raw`^\s*export\s+const\s+gate\s*(?::\s*boolean\s*)?=\s*false` + TAIL, 'm')
|
|
61
|
-
const STRING_ITEM_RE = /['"]([^'"]+)['"]/g
|
|
76
|
+
/** the same question about a file on disk. An unreadable file is a check
|
|
77
|
+
* file, so the import names what is wrong with it */
|
|
78
|
+
export function isLegacyCheckFile(path: string): boolean {
|
|
79
|
+
try { return isLegacyCheckSource(readFileSync(path, 'utf8')) } catch { return false }
|
|
80
|
+
}
|
|
62
81
|
|
|
63
|
-
|
|
82
|
+
/** every built-in id, from the file names alone. Sync and import free, so
|
|
83
|
+
* `defineRepo` can read the namespace at config load. Read once per
|
|
84
|
+
* process: the toolkit's own files cannot appear mid run, and `defineRepo`
|
|
85
|
+
* asks for the list once per `when` entry. */
|
|
86
|
+
let builtinIds: string[] | null = null
|
|
87
|
+
export function builtinCheckIds(): string[] {
|
|
88
|
+
if (builtinIds != null) return builtinIds
|
|
64
89
|
const files: string[] = []
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
walk(BUILTIN_CHECK_DIR, files)
|
|
91
|
+
builtinIds = files.map((f) => {
|
|
92
|
+
const base = basename(f).slice(0, -SUFFIX.length)
|
|
93
|
+
return BUILTIN_ID_RE.exec(base)?.[1] ?? base
|
|
94
|
+
})
|
|
95
|
+
return builtinIds
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** the module, re-imported whenever the file on disk moves. An ES module is
|
|
99
|
+
* cached by path for the life of the process, so a repo that rewrites a
|
|
100
|
+
* check file mid process would keep running the old body without this. */
|
|
101
|
+
async function load(path: string): Promise<CheckDef> {
|
|
102
|
+
let stamp = ''
|
|
103
|
+
// no dot in the stamp: a query ending `.1234` reads as the module's
|
|
104
|
+
// extension, and a load plugin then never sees the file
|
|
105
|
+
try { stamp = String(statSync(path).mtimeMs).replace('.', '_') } catch {}
|
|
106
|
+
// the path rides on every message from here down, because `check()` knows
|
|
107
|
+
// the object and not where it was written, and an agent reading a refused
|
|
108
|
+
// declaration needs the file before it needs the rule
|
|
109
|
+
try {
|
|
110
|
+
// the REAL path: through a linked one, the resolver misses a file
|
|
111
|
+
// created after it first listed the directory, which is exactly the
|
|
112
|
+
// file a fix pass just wrote
|
|
113
|
+
let real = path
|
|
114
|
+
try { real = realpathSync(path) } catch {}
|
|
115
|
+
const mod = (await import(`${real}?v=${stamp}`)) as { default?: unknown }
|
|
116
|
+
const d = mod.default
|
|
117
|
+
if (d == null || typeof d !== 'object' || (!('run' in d) && !('cmd' in d)))
|
|
118
|
+
throw new Error(`a check file must \`export default check({...})\` with a \`run\` or a \`cmd\``)
|
|
119
|
+
// re-validated here rather than trusted: a file can build the object
|
|
120
|
+
// any way it likes, and only what `check()` accepts may reach a gate
|
|
121
|
+
return check(d as CheckDef)
|
|
122
|
+
} catch (e) {
|
|
123
|
+
throw new Error(`${path}: ${(e as Error).message.replace(/^check( \S+)?: /, '')}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** every check this repo can run: its own files, plus the toolkit's. Throws
|
|
128
|
+
* on a registration nobody can act on (a duplicate id, a repo check squatting
|
|
129
|
+
* the reserved prefix, a built-in with no owner page), so a broken
|
|
130
|
+
* declaration dies at load rather than half way through a gate. */
|
|
131
|
+
export async function discoverChecks(root: string): Promise<DiscoveredCheck[]> {
|
|
132
|
+
const builtins: string[] = []
|
|
133
|
+
walk(BUILTIN_CHECK_DIR, builtins)
|
|
134
|
+
const found: string[] = []
|
|
135
|
+
walk(root, found)
|
|
136
|
+
// the repo that HOSTS the toolkit walks over `src/checks/` too, and a file
|
|
137
|
+
// discovered twice is a duplicate id rather than a second check. Real paths,
|
|
138
|
+
// because a consumer reaches the same files through a linked node_modules
|
|
139
|
+
const real = (f: string): string => { try { return realpathSync(f) } catch { return f } }
|
|
140
|
+
const shipped = new Set(builtins.map(real))
|
|
141
|
+
const all = found.filter((f) => !shipped.has(real(f))).sort()
|
|
142
|
+
builtins.sort()
|
|
143
|
+
const names = fallbackNames(root, all)
|
|
144
|
+
// a legacy script is left to its own reader, and never imported here
|
|
145
|
+
const own = all.filter((f) => !isLegacyCheckFile(f))
|
|
146
|
+
const base = (f: string): string => basename(f).slice(0, -SUFFIX.length)
|
|
147
|
+
|
|
148
|
+
const out: DiscoveredCheck[] = []
|
|
149
|
+
const ids = new Set<string>()
|
|
150
|
+
for (const f of [...builtins, ...own]) {
|
|
151
|
+
const isBuiltin = builtins.includes(f)
|
|
152
|
+
const def = await load(f)
|
|
153
|
+
const rel = isBuiltin ? null : relative(root, f).replace(/\\/g, '/')
|
|
154
|
+
const fallback = isBuiltin
|
|
155
|
+
? (BUILTIN_ID_RE.exec(base(f))?.[1] ?? base(f))
|
|
156
|
+
: names.get(f)!
|
|
157
|
+
const id = def.id ?? fallback
|
|
158
|
+
if (isBuiltin) {
|
|
159
|
+
if (id !== fallback) throw new Error(`${f}: declares id '${id}' but its file name says '${fallback}' — a built-in id is its file name`)
|
|
160
|
+
if ((def.owner ?? '') === '') throw new Error(`${f}: a built-in check names the page that OWNS the invariant`)
|
|
161
|
+
} else if (id.startsWith('SK')) {
|
|
162
|
+
throw new Error(`${rel}: id '${id}' uses the reserved SK prefix — a repo check picks its own (e.g. RV${id.slice(-3)}) or plain words`)
|
|
72
163
|
}
|
|
164
|
+
if (ids.has(id)) throw new Error(`${isBuiltin ? f : rel}: duplicate check id '${id}'`)
|
|
165
|
+
ids.add(id)
|
|
166
|
+
out.push({
|
|
167
|
+
def,
|
|
168
|
+
path: f,
|
|
169
|
+
rel,
|
|
170
|
+
id,
|
|
171
|
+
title: def.title ?? id,
|
|
172
|
+
description: def.description,
|
|
173
|
+
owner: def.owner ?? rel ?? f,
|
|
174
|
+
from: isBuiltin ? 'shipkit' : 'repo',
|
|
175
|
+
})
|
|
73
176
|
}
|
|
74
|
-
|
|
75
|
-
files.sort()
|
|
76
|
-
const baseOf = (f: string): string => basename(f).slice(0, -SUFFIX.length)
|
|
77
|
-
const seen = new Map<string, number>()
|
|
78
|
-
for (const f of files) seen.set(baseOf(f), (seen.get(baseOf(f)) ?? 0) + 1)
|
|
79
|
-
return files.map((f) => {
|
|
80
|
-
const base = baseOf(f)
|
|
81
|
-
const name = (seen.get(base) ?? 0) > 1 ? relative(root, f).slice(0, -SUFFIX.length) : base
|
|
82
|
-
let blocking: boolean | null = null
|
|
83
|
-
let inputs: string[] | null = null
|
|
84
|
-
let ttl: string | null = null
|
|
85
|
-
let never: string | null = null
|
|
86
|
-
let gate = true
|
|
87
|
-
try {
|
|
88
|
-
const src = readFileSync(f, 'utf8')
|
|
89
|
-
const m = src.match(BLOCKING_RE)
|
|
90
|
-
blocking = m == null ? null : m[1] === 'true'
|
|
91
|
-
const im = src.match(INPUTS_RE)
|
|
92
|
-
if (im != null) {
|
|
93
|
-
const items = [...im[1]!.matchAll(STRING_ITEM_RE)].map((x) => x[1]!)
|
|
94
|
-
if (items.length > 0) inputs = items
|
|
95
|
-
}
|
|
96
|
-
ttl = src.match(TTL_RE)?.[1] ?? null
|
|
97
|
-
never = src.match(NEVER_RE)?.[1] ?? null
|
|
98
|
-
gate = src.match(GATE_RE) == null
|
|
99
|
-
} catch {}
|
|
100
|
-
return { path: f, name, blocking, inputs, ttl, never, gate }
|
|
101
|
-
})
|
|
177
|
+
return out
|
|
102
178
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// the legacy check SCRIPT reader, for the compatibility window: a
|
|
2
|
+
// `*.check.ts` with named `export const` declarations and a body that runs
|
|
3
|
+
// top to bottom, spawned as a subprocess, wire output parsed back, exit code
|
|
4
|
+
// is truth. It shares the suffix with the check file, and the source decides
|
|
5
|
+
// which one a file is (core/check-file-names.ts `isLegacyCheckSource`). It is
|
|
6
|
+
// read through the window, and SK016 names each one with the check object
|
|
7
|
+
// it becomes: a port by hand. Skipped dirs: the same walk as the check file's.
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'fs'
|
|
10
|
+
import { fallbackNames, readLegacyCheck, type LegacyCheck } from './check-file-names.ts'
|
|
11
|
+
import { repoCheckFiles } from './discover-checks.ts'
|
|
12
|
+
|
|
13
|
+
export type { LegacyCheck } from './check-file-names.ts'
|
|
14
|
+
|
|
15
|
+
export function discoverLegacyChecks(root: string): LegacyCheck[] {
|
|
16
|
+
const all = repoCheckFiles(root)
|
|
17
|
+
const names = fallbackNames(root, all)
|
|
18
|
+
const out: LegacyCheck[] = []
|
|
19
|
+
for (const f of all) {
|
|
20
|
+
let src: string
|
|
21
|
+
try { src = readFileSync(f, 'utf8') } catch { continue }
|
|
22
|
+
const read = readLegacyCheck(f, names.get(f)!, src)
|
|
23
|
+
if (read != null) out.push(read)
|
|
24
|
+
}
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// a generated block inside a hand-written markdown page, between two html
|
|
2
|
+
// comment markers. Two docs pages are built this way (the command index and
|
|
3
|
+
// the config-key reference), and one splice keeps them honest: a renamed or
|
|
4
|
+
// hand-mangled host returns null, so a generator can fail loud instead of
|
|
5
|
+
// writing nothing.
|
|
6
|
+
|
|
7
|
+
/** replace the marked block in a host file. null = the markers are gone. */
|
|
8
|
+
export function spliceMarkedBlock(host: string, block: string, begin: string, end: string): string | null {
|
|
9
|
+
const a = host.indexOf(begin)
|
|
10
|
+
const b = host.indexOf(end)
|
|
11
|
+
if (a === -1 || b === -1 || b < a) return null
|
|
12
|
+
return `${host.slice(0, a + begin.length)}\n\n${block}\n\n${host.slice(b)}`
|
|
13
|
+
}
|