rvlib-shipkit 1.9.0 → 2.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 +34 -0
- package/package.json +1 -1
- package/src/check.ts +5 -2
- package/src/checks/SK001-package-json-shape.check.ts +47 -12
- package/src/checks/SK013-one-way-in-policy.check.ts +8 -7
- package/src/checks/SK017-vscode-hub-task.check.ts +109 -106
- package/src/checks/SK019-tests-run-in-units.check.ts +2 -1
- package/src/checks/SK027-agent-wiki-links.check.ts +5 -2
- package/src/checks/SK029-explicit-check-files.check.ts +25 -8
- package/src/checks/SK034-commit-gates-armed.check.ts +90 -0
- package/src/cli/cmd_Ci.ts +3 -3
- package/src/cli/cmd_Commands.ts +21 -5
- package/src/cli/cmd_Hooks.ts +6 -2
- package/src/cli/cmd_Init.ts +1 -1
- package/src/cli/cmd_Serve.ts +1 -2
- package/src/cli/cmd_Start.ts +1 -1
- package/src/cli.ts +2 -4
- package/src/core/budget.ts +6 -8
- package/src/core/bun-builtins.ts +36 -0
- package/src/core/bun-tool-cli.ts +23 -0
- package/src/core/canonical-scripts.ts +6 -4
- package/src/core/check-ctx.ts +104 -10
- package/src/core/check-file-names.ts +2 -3
- package/src/core/check-pass.ts +17 -1
- package/src/core/check-runner.ts +19 -6
- package/src/core/config-rewrite.ts +27 -6
- package/src/core/discover-checks.ts +30 -10
- package/src/core/floor-resolve.ts +16 -7
- package/src/core/job-run.ts +2 -2
- package/src/core/layout.ts +4 -1
- package/src/core/legacy-shapes.ts +40 -0
- package/src/core/one-lib.ts +1 -1
- package/src/core/one-way-in-policy.ts +106 -13
- package/src/core/pre-commit-discovery.ts +106 -0
- package/src/core/retired-checks.ts +2 -0
- package/src/core/run-ci.ts +16 -10
- package/src/core/run-fix.ts +9 -0
- package/src/core/run-fixers.ts +91 -35
- package/src/core/runs-matrix.ts +7 -0
- package/src/core/slots.ts +1 -1
- package/src/core/vendored-read.ts +39 -0
- package/src/core/vendored.ts +82 -0
- package/src/core/waivers.ts +125 -0
- package/src/core/when-block.ts +25 -185
- package/src/core/when.ts +30 -13
- package/src/core/wire.ts +17 -1
- package/src/git-bootstrap.ts +1 -1
- package/src/home/attach.ts +3 -5
- package/src/home/insights.ts +6 -1
- package/src/home/pm-config.ts +2 -2
- package/src/home/run-daemon.ts +2 -2
- package/src/home/run-unit-action.ts +2 -2
- package/src/hooks/commit-message.ts +2 -14
- package/src/hooks/hook-gates.ts +38 -0
- package/src/hooks/leak-guard.ts +8 -3
- package/src/hooks/one-way-in-guard.ts +8 -2
- package/src/hooks/root-identity.ts +9 -6
- package/src/hooks/run-hooks.ts +72 -41
- package/src/hooks/staged.ts +49 -0
- package/src/hooks/tree-shrink.ts +41 -0
- package/src/init-repo.ts +0 -42
- package/src/ledger/check-table.ts +1 -1
- package/src/ledger/gate-checks.ts +45 -9
- package/src/ledger/ledger.ts +6 -1
- package/src/ledger/repo-view.ts +14 -4
- package/src/ledger/toolchain-identity.ts +4 -2
- package/src/mv/mv-md.ts +6 -1
- package/src/problems/extract.ts +27 -5
- package/src/problems/problem-line.ts +2 -3
- package/src/release/run-release.ts +13 -8
- package/src/repo-config.ts +44 -27
- package/src/scaffold/files/bunfig.toml +11 -0
- package/src/scaffold/files/vscode-settings.json +1 -2
- package/src/scaffold/jsonc-spans.ts +16 -0
- package/src/scaffold/project-scaffold.ts +43 -77
- package/src/serve/hub-app/check-view.tsx +4 -4
- package/src/serve/hub-app/check-words.ts +1 -0
- package/src/serve/hub-app/checks-svc.ts +1 -2
- package/src/serve/hub-app/home.html +0 -1
- package/src/serve/hub-app/hub-types.ts +0 -2
- package/src/serve/hub-app/index.html +0 -1
- package/src/serve/hub-app/project-list.css +1 -2
- package/src/serve/hub-app/tree-build.ts +3 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-port.ts +3 -3
- package/src/serve/hub-runtime.ts +0 -3
- package/src/serve/serve-hub.ts +1 -16
- package/src/serve/tracked-watch.ts +1 -1
- package/src/start-repo.ts +15 -3
- package/src/suite/judge-suites.ts +29 -28
- package/src/suite/select.ts +2 -0
- package/src/suite/suite-cli.ts +6 -3
- package/src/suite/suite-cpu-preload.ts +7 -11
- package/src/suite/test-units.ts +7 -4
- package/src/ts/config.ts +12 -5
- package/src/ts/imports.ts +3 -1
- package/src/tui/build-steps-panel.ts +1 -1
- package/src/wiki/scan-md.ts +16 -9
- package/src/checks/SK009-commit-ai-tells.check.ts +0 -131
- package/src/checks/SK014-prose-ste.check.ts +0 -78
- package/src/cli/cmd_Prose.ts +0 -74
- package/src/prose/prose-view.ts +0 -90
- package/src/prose/run-prose.ts +0 -187
- package/src/prose/ste.ts +0 -552
- package/src/scaffold/files/vscode-tasks.json +0 -24
- package/src/serve/hub-app/prose-hits-st.ts +0 -125
- package/src/serve/hub-app/prose-hits-view.tsx +0 -176
- package/src/serve/hub-app/prose-hits.css +0 -21
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// the in-file waiver grammar: a site a check reports can be waived where it
|
|
2
|
+
// stands, in whatever comment the file type has, and only with a reason.
|
|
3
|
+
//
|
|
4
|
+
// sk-disable-next-line <ID>[, <ID>…] -- <reason> the line below it
|
|
5
|
+
// sk-disable-line <ID>[, <ID>…] -- <reason> its own line
|
|
6
|
+
// sk-disable-file <ID>[, <ID>…] -- <reason> every site of the file
|
|
7
|
+
//
|
|
8
|
+
// carriers: `<!-- … -->` in markdown and html (fenced and inline code left
|
|
9
|
+
// out, so a page can show the grammar), `//` and `/* … */` in ts and js, `#`
|
|
10
|
+
// in sh, yaml, toml and python, all of them in an unknown type. A directive
|
|
11
|
+
// inside a string literal still counts: a test builds its samples at runtime.
|
|
12
|
+
// core/check-ctx.ts applies them, through `c`, so the waiving file is read
|
|
13
|
+
// into the check's key.
|
|
14
|
+
|
|
15
|
+
/** the token a file must contain before it is parsed at all */
|
|
16
|
+
export const DIRECTIVE_MARK = 'sk-disable-'
|
|
17
|
+
|
|
18
|
+
export type DirectiveScope = 'next-line' | 'line' | 'file'
|
|
19
|
+
|
|
20
|
+
export type Directive = {
|
|
21
|
+
scope: DirectiveScope
|
|
22
|
+
/** the check ids it names */
|
|
23
|
+
ids: string[]
|
|
24
|
+
/** null when the directive gave none, which waives nothing */
|
|
25
|
+
reason: string | null
|
|
26
|
+
/** 1-based line of the directive itself */
|
|
27
|
+
line: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Carrier = 'html' | 'slash' | 'block' | 'hash'
|
|
31
|
+
|
|
32
|
+
const MARKDOWN = new Set(['md', 'mdx', 'markdown'])
|
|
33
|
+
const HTML = new Set(['html', 'htm', 'xml', 'svg', 'vue'])
|
|
34
|
+
const C_LIKE = new Set(['ts', 'tsx', 'mts', 'cts', 'js', 'jsx', 'mjs', 'cjs', 'css', 'scss', 'less', 'jsonc', 'json5', 'go', 'rs', 'swift', 'java', 'kt', 'c', 'h', 'cpp', 'zig'])
|
|
35
|
+
const HASH = new Set(['sh', 'bash', 'zsh', 'fish', 'yaml', 'yml', 'toml', 'py', 'rb', 'pl', 'r', 'ini', 'conf', 'cfg', 'env', 'gitignore', 'dockerignore', 'nix'])
|
|
36
|
+
const HASH_NAMES = new Set(['Dockerfile', 'Makefile', 'Procfile', 'CODEOWNERS', '.gitignore', '.gitattributes', '.npmignore', '.dockerignore'])
|
|
37
|
+
|
|
38
|
+
function carriersOf(path: string): { carriers: Carrier[]; markdown: boolean } {
|
|
39
|
+
const name = path.split('/').at(-1) ?? path
|
|
40
|
+
const ext = name.includes('.') ? name.split('.').at(-1)!.toLowerCase() : ''
|
|
41
|
+
if (MARKDOWN.has(ext)) return { carriers: ['html'], markdown: true }
|
|
42
|
+
if (HTML.has(ext)) return { carriers: ['html'], markdown: false }
|
|
43
|
+
if (C_LIKE.has(ext)) return { carriers: ['slash', 'block'], markdown: false }
|
|
44
|
+
if (HASH.has(ext) || HASH_NAMES.has(name)) return { carriers: ['hash'], markdown: false }
|
|
45
|
+
return { carriers: ['html', 'slash', 'block', 'hash'], markdown: false }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const KEYWORD = 'sk-disable-(next-line|line|file)(?![\\w-])'
|
|
49
|
+
const OPENERS: Record<Carrier, RegExp> = {
|
|
50
|
+
html: new RegExp(`<!--\\s*${KEYWORD}(.*?)(?:-->|$)`, 'g'),
|
|
51
|
+
slash: new RegExp(`//\\s*${KEYWORD}(.*)$`, 'g'),
|
|
52
|
+
block: new RegExp(`/\\*\\s*${KEYWORD}(.*?)(?:\\*/|$)`, 'g'),
|
|
53
|
+
hash: new RegExp(`(?:^|\\s)#\\s*${KEYWORD}(.*)$`, 'g'),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// a lone dash, never two: `RV027--why` is an id and a glued reason, reported.
|
|
57
|
+
// It ends on a letter or digit, so `RV027: why` is an id and no reason
|
|
58
|
+
const ID = '[A-Za-z0-9](?:(?:[\\w./:]|-(?!-))*[A-Za-z0-9])?'
|
|
59
|
+
const BODY = new RegExp(`^\\s+(${ID}(?:\\s*,\\s*${ID})*)(.*)$`)
|
|
60
|
+
const REASON = /^\s+--\s*(.*?)\s*$/
|
|
61
|
+
|
|
62
|
+
/** a markdown line with its inline code spans blanked, so a page that shows
|
|
63
|
+
* the grammar in backticks declares nothing */
|
|
64
|
+
const withoutInlineCode = (line: string): string => line.replace(/(`+)[\s\S]*?\1/g, (m) => ' '.repeat(m.length))
|
|
65
|
+
|
|
66
|
+
/** every directive in a file, in line order. A keyword followed by no id is
|
|
67
|
+
* not a directive at all (`<ID>` in prose about the grammar). One that names
|
|
68
|
+
* ids but no ` -- <reason>` is kept with `reason: null`, so the check it
|
|
69
|
+
* names can say it was ignored */
|
|
70
|
+
export function parseDirectives(path: string, text: string): Directive[] {
|
|
71
|
+
if (!text.includes(DIRECTIVE_MARK)) return []
|
|
72
|
+
const { carriers, markdown } = carriersOf(path)
|
|
73
|
+
const fenced = markdown ? fences(text.slice(0, text.lastIndexOf(DIRECTIVE_MARK))) : []
|
|
74
|
+
const out: Directive[] = []
|
|
75
|
+
// only the lines holding the mark are read: a journal of a megabyte that
|
|
76
|
+
// mentions the grammar twice costs two lines
|
|
77
|
+
let lineNo = 1
|
|
78
|
+
let counted = 0
|
|
79
|
+
let last = -1
|
|
80
|
+
for (let at = text.indexOf(DIRECTIVE_MARK); at !== -1; at = text.indexOf(DIRECTIVE_MARK, at + 1)) {
|
|
81
|
+
const start = text.lastIndexOf('\n', at) + 1
|
|
82
|
+
if (start === last) continue
|
|
83
|
+
last = start
|
|
84
|
+
for (let n = text.indexOf('\n', counted); n !== -1 && n < start; n = text.indexOf('\n', n + 1)) { lineNo++; counted = n + 1 }
|
|
85
|
+
if (fenced.some(([a, b]) => start >= a && start < b)) continue
|
|
86
|
+
const end = text.indexOf('\n', at)
|
|
87
|
+
const raw = text.slice(start, end === -1 ? text.length : end)
|
|
88
|
+
const line = markdown ? withoutInlineCode(raw) : raw
|
|
89
|
+
for (const c of carriers) {
|
|
90
|
+
for (const m of line.matchAll(OPENERS[c])) {
|
|
91
|
+
const body = BODY.exec(m[2]!)
|
|
92
|
+
if (body == null) continue
|
|
93
|
+
const reason = REASON.exec(body[2]!)?.[1] ?? ''
|
|
94
|
+
out.push({ scope: m[1] as DirectiveScope, ids: body[1]!.split(',').map((s) => s.trim()), reason: reason === '' ? null : reason, line: lineNo })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** the char spans of the fenced blocks of a markdown text, fence lines
|
|
102
|
+
* included, an unclosed one running to the end. Given the text up to its
|
|
103
|
+
* last mark only, so a long journal is not walked past it */
|
|
104
|
+
function fences(text: string): [number, number][] {
|
|
105
|
+
const spans: [number, number][] = []
|
|
106
|
+
let open: { at: number; fence: string } | null = null
|
|
107
|
+
for (const m of text.matchAll(/^[ \t]{0,3}(`{3,}|~{3,})/gm)) {
|
|
108
|
+
const f = m[1]!
|
|
109
|
+
if (open == null) open = { at: m.index!, fence: f }
|
|
110
|
+
else if (f[0] === open.fence[0] && f.length >= open.fence.length) { spans.push([open.at, m.index! + m[0].length]); open = null }
|
|
111
|
+
}
|
|
112
|
+
if (open != null) spans.push([open.at, text.length])
|
|
113
|
+
return spans
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** whether a directive reaches a site of its file: a site with no line is
|
|
117
|
+
* reached by a file directive only */
|
|
118
|
+
export function reaches(d: Directive, line: number | undefined): boolean {
|
|
119
|
+
if (d.scope === 'file') return true
|
|
120
|
+
if (line == null) return false
|
|
121
|
+
return d.scope === 'line' ? line === d.line : line === d.line + 1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** how a directive reads in a report */
|
|
125
|
+
export const spell = (d: Directive, id: string): string => `sk-disable-${d.scope} ${id}`
|
package/src/core/when-block.ts
CHANGED
|
@@ -29,14 +29,9 @@ import { join } from 'path'
|
|
|
29
29
|
import { atomicWrite } from './locked-write.ts'
|
|
30
30
|
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
31
31
|
import type { SlowSuite, TestsConfig } from '../suite/judge-suites.ts'
|
|
32
|
-
import { normWord, proseWordsProblems, type ProseAllowWord, type ProseBanWord, type ProseWords } from '../prose/ste.ts'
|
|
33
32
|
|
|
34
33
|
export const GATE_BEGIN = '// >>> shipkit gate — managed block, the hub writes it, edit outside the markers'
|
|
35
34
|
export const GATE_END = '// <<< shipkit gate'
|
|
36
|
-
/** the second managed block, INSIDE the `prose: {` object: the repo's own
|
|
37
|
-
* word lists for rule SK014. Same writer, same paranoia, its own markers */
|
|
38
|
-
export const WORDS_BEGIN = '// >>> shipkit prose words — managed block, the hub writes it, edit outside the markers'
|
|
39
|
-
export const WORDS_END = '// <<< shipkit prose words'
|
|
40
35
|
|
|
41
36
|
/** every key this block owns. A repo declaring one of them outside the
|
|
42
37
|
* markers keeps it, and the block refuses to write rather than duplicate it */
|
|
@@ -55,13 +50,6 @@ export type GateBlock = {
|
|
|
55
50
|
tests: TestsConfig
|
|
56
51
|
/** true when a key this block owns is declared outside the markers */
|
|
57
52
|
declaredOutside: boolean
|
|
58
|
-
/** the prose word lists as the config loads them */
|
|
59
|
-
words: ProseWords
|
|
60
|
-
/** the words block: `no-prose` when the config has no `prose: {` object
|
|
61
|
-
* to write into, and the lists then mean nothing to rule SK014 anyway */
|
|
62
|
-
wordsState: BlockState | 'no-prose'
|
|
63
|
-
/** true when `prose.words` is declared outside the words markers */
|
|
64
|
-
wordsOutside: boolean
|
|
65
53
|
}
|
|
66
54
|
|
|
67
55
|
export type GateEdit = {
|
|
@@ -71,16 +59,6 @@ export type GateEdit = {
|
|
|
71
59
|
slow?: SlowSuite[]
|
|
72
60
|
/** suite files to drop back to the quick tier */
|
|
73
61
|
slowRemove?: string[]
|
|
74
|
-
/** the prose word lists: entries to add or replace (keyed by word), and
|
|
75
|
-
* words to drop from either list */
|
|
76
|
-
words?: WordsEdit
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export type WordsEdit = {
|
|
80
|
-
allow?: ProseAllowWord[]
|
|
81
|
-
ban?: ProseBanWord[]
|
|
82
|
-
/** words to drop, from whichever list holds them */
|
|
83
|
-
remove?: string[]
|
|
84
62
|
}
|
|
85
63
|
|
|
86
64
|
const sha = (s: string): string => createHash('sha256').update(s).digest('hex').slice(0, 16)
|
|
@@ -98,11 +76,6 @@ function indentOf(lines: string[]): string {
|
|
|
98
76
|
|
|
99
77
|
const isBegin = (l: string): boolean => l.trim().startsWith('// >>> shipkit gate')
|
|
100
78
|
const isEnd = (l: string): boolean => l.trim().startsWith('// <<< shipkit gate')
|
|
101
|
-
const isWordsBegin = (l: string): boolean => l.trim().startsWith('// >>> shipkit prose words')
|
|
102
|
-
const isWordsEnd = (l: string): boolean => l.trim().startsWith('// <<< shipkit prose words')
|
|
103
|
-
/** the one line the words block can be written under: `prose: {` alone */
|
|
104
|
-
const isProseOpen = (l: string): boolean => /^\s*prose:\s*\{\s*$/.test(l)
|
|
105
|
-
|
|
106
79
|
/** a single quoted TS string literal, escapes and all */
|
|
107
80
|
const q = (s: string): string => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
108
81
|
/** an object key: bare when it is a plain identifier, quoted otherwise */
|
|
@@ -138,70 +111,6 @@ export function renderGateBlock(off: Record<string, string>, tests: TestsConfig,
|
|
|
138
111
|
return out
|
|
139
112
|
}
|
|
140
113
|
|
|
141
|
-
/** the bytes between the words markers, markers included. Entries sorted by
|
|
142
|
-
* word, so the block is a function of the lists and a reorder by hand reads
|
|
143
|
-
* as a hand edit */
|
|
144
|
-
export function renderWordsBlock(words: ProseWords, pad: string, unit = ' '): string[] {
|
|
145
|
-
const out = [`${pad}${WORDS_BEGIN}`]
|
|
146
|
-
const allow = [...(words.allow ?? [])].sort((a, b) => normWord(a.word).localeCompare(normWord(b.word)))
|
|
147
|
-
const ban = [...(words.ban ?? [])].sort((a, b) => normWord(a.word).localeCompare(normWord(b.word)))
|
|
148
|
-
if (allow.length > 0 || ban.length > 0) {
|
|
149
|
-
out.push(`${pad}words: {`)
|
|
150
|
-
const entry = (e: object, keys: string[]): string => {
|
|
151
|
-
const r = e as Record<string, unknown>
|
|
152
|
-
return `{ ${keys.filter((f) => typeof r[f] === 'string' && r[f] !== '').map((f) => `${f}: ${q(r[f] as string)}`).join(', ')} },`
|
|
153
|
-
}
|
|
154
|
-
const p1 = pad + unit
|
|
155
|
-
const p2 = p1 + unit
|
|
156
|
-
if (allow.length > 0) {
|
|
157
|
-
out.push(`${p1}allow: [`)
|
|
158
|
-
for (const a of allow) out.push(`${p2}${entry(a, ['word', 'reason'])}`)
|
|
159
|
-
out.push(`${p1}],`)
|
|
160
|
-
}
|
|
161
|
-
if (ban.length > 0) {
|
|
162
|
-
out.push(`${p1}ban: [`)
|
|
163
|
-
for (const b of ban) out.push(`${p2}${entry(b, ['word', 'to', 'reason'])}`)
|
|
164
|
-
out.push(`${p1}],`)
|
|
165
|
-
}
|
|
166
|
-
out.push(`${pad}},`)
|
|
167
|
-
}
|
|
168
|
-
out.push(`${pad}${WORDS_END}`)
|
|
169
|
-
return out
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** the words the loader read, in the shape the renderer writes: empty
|
|
173
|
-
* strings dropped, so a round trip compares content and not spelling */
|
|
174
|
-
const cleanWords = (w: ProseWords | undefined): ProseWords => {
|
|
175
|
-
const strip = <T extends object>(e: T): T => Object.fromEntries(Object.entries(e).filter(([, v]) => v != null && v !== '')) as T
|
|
176
|
-
const out: ProseWords = {}
|
|
177
|
-
if ((w?.allow ?? []).length > 0) out.allow = w!.allow!.map(strip)
|
|
178
|
-
if ((w?.ban ?? []).length > 0) out.ban = w!.ban!.map(strip)
|
|
179
|
-
return out
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** the indentation one level inside the line at `at` */
|
|
183
|
-
const innerPad = (lines: string[], at: number): string => {
|
|
184
|
-
const own = /^([ \t]*)/.exec(lines[at] ?? '')?.[1] ?? ''
|
|
185
|
-
const unit = indentOf(lines)
|
|
186
|
-
return own + unit
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** where the words block sits and what state it is in */
|
|
190
|
-
function locateWords(lines: string[], c: RepoConfig): { state: BlockState | 'no-prose'; b: number; e: number; open: number; pad: string; outside: boolean } {
|
|
191
|
-
const b = lines.findIndex(isWordsBegin)
|
|
192
|
-
const e = lines.findIndex(isWordsEnd)
|
|
193
|
-
const open = lines.findIndex(isProseOpen)
|
|
194
|
-
const words = cleanWords(c.prose?.words)
|
|
195
|
-
if (b !== -1 && e !== -1 && e > b) {
|
|
196
|
-
const pad = /^([ \t]*)/.exec(lines[b]!)?.[1] ?? ''
|
|
197
|
-
const current = lines.slice(b, e + 1).join('\n')
|
|
198
|
-
const expected = renderWordsBlock(words, pad, indentOf(lines)).join('\n')
|
|
199
|
-
return { state: current === expected ? 'current' : 'stale', b, e, open, pad, outside: false }
|
|
200
|
-
}
|
|
201
|
-
if (open === -1) return { state: 'no-prose', b: -1, e: -1, open, pad: '', outside: c.prose?.words != null }
|
|
202
|
-
return { state: 'missing-block', b: -1, e: -1, open, pad: innerPad(lines, open), outside: c.prose?.words != null }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
114
|
/** is repo.config.ts edited and not yet committed? The switch leaves it
|
|
206
115
|
* DIRTY on purpose: a toggle is a change its owner reviews in a git client,
|
|
207
116
|
* never a commit an agent makes for them. So the page has to say so. */
|
|
@@ -219,7 +128,7 @@ export function configDirty(root: string): boolean {
|
|
|
219
128
|
/** what the block holds today, and whether it still describes the config */
|
|
220
129
|
export async function readGateBlock(root: string): Promise<GateBlock> {
|
|
221
130
|
const path = configPath(root)
|
|
222
|
-
if (!existsSync(path)) return { path, hash: '', state: 'missing-file', off: {}, tests: {}, declaredOutside: false
|
|
131
|
+
if (!existsSync(path)) return { path, hash: '', state: 'missing-file', off: {}, tests: {}, declaredOutside: false }
|
|
223
132
|
const text = readFileSync(path, 'utf8')
|
|
224
133
|
const lines = text.split('\n')
|
|
225
134
|
const b = lines.findIndex(isBegin)
|
|
@@ -228,13 +137,11 @@ export async function readGateBlock(root: string): Promise<GateBlock> {
|
|
|
228
137
|
const off = c.off ?? {}
|
|
229
138
|
const tests = c.tests ?? {}
|
|
230
139
|
const hash = sha(text)
|
|
231
|
-
const w = locateWords(lines, c)
|
|
232
|
-
const wordsPart = { words: cleanWords(c.prose?.words), wordsState: w.state, wordsOutside: w.outside }
|
|
233
140
|
if (b === -1 || e === -1 || e < b) {
|
|
234
141
|
// no block: the keys it owns must be unclaimed, or a write would be a
|
|
235
142
|
// second declaration of the same key rather than an edit of the first
|
|
236
143
|
const declaredOutside = OWNED.some((key) => c[key] != null)
|
|
237
|
-
return { path, hash, state: 'missing-block', off, tests, declaredOutside
|
|
144
|
+
return { path, hash, state: 'missing-block', off, tests, declaredOutside }
|
|
238
145
|
}
|
|
239
146
|
// the block DESCRIBES the config when re-rendering the loaded values
|
|
240
147
|
// reproduces it byte for byte. Anything else (a hand edit, a second
|
|
@@ -243,7 +150,7 @@ export async function readGateBlock(root: string): Promise<GateBlock> {
|
|
|
243
150
|
const pad = indentOf(lines.slice(b + 1, e))
|
|
244
151
|
const current = lines.slice(b, e + 1).join('\n')
|
|
245
152
|
const expected = renderGateBlock(off, tests, pad === '' ? indentOf(lines) : pad).join('\n')
|
|
246
|
-
return { path, hash, state: current === expected ? 'current' : 'stale', off, tests, declaredOutside: false
|
|
153
|
+
return { path, hash, state: current === expected ? 'current' : 'stale', off, tests, declaredOutside: false }
|
|
247
154
|
}
|
|
248
155
|
|
|
249
156
|
/** does what the loader read back say what was written? BOTH keys the block
|
|
@@ -252,7 +159,7 @@ export async function readGateBlock(root: string): Promise<GateBlock> {
|
|
|
252
159
|
* what reading the result back exists to catch, and comparing half of the
|
|
253
160
|
* block would let that half through. Key and array ORDER are the renderer's,
|
|
254
161
|
* so the comparison is by content. */
|
|
255
|
-
export function roundTripProblem(wrote: { off: Record<string, string>; tests: TestsConfig
|
|
162
|
+
export function roundTripProblem(wrote: { off: Record<string, string>; tests: TestsConfig }, loaded: RepoConfig): string | null {
|
|
256
163
|
const normOff = (m: Record<string, string> | undefined): string => JSON.stringify(Object.entries(m ?? {}).sort())
|
|
257
164
|
const normTests = (t: TestsConfig | undefined): string =>
|
|
258
165
|
JSON.stringify({
|
|
@@ -262,72 +169,20 @@ export function roundTripProblem(wrote: { off: Record<string, string>; tests: Te
|
|
|
262
169
|
})
|
|
263
170
|
if (normOff(loaded.off) !== normOff(wrote.off)) return `the loader read off ${JSON.stringify(loaded.off ?? {})}`
|
|
264
171
|
if (normTests(loaded.tests) !== normTests(wrote.tests)) return `the loader read tests ${JSON.stringify(loaded.tests ?? {})}`
|
|
265
|
-
if (wrote.words != null) {
|
|
266
|
-
const normWords = (x: ProseWords | undefined): string => {
|
|
267
|
-
const c = cleanWords(x)
|
|
268
|
-
const by = (a: { word: string }, b: { word: string }) => normWord(a.word).localeCompare(normWord(b.word))
|
|
269
|
-
return JSON.stringify({ allow: [...(c.allow ?? [])].sort(by), ban: [...(c.ban ?? [])].sort(by) })
|
|
270
|
-
}
|
|
271
|
-
if (normWords(loaded.prose?.words) !== normWords(wrote.words)) return `the loader read prose.words ${JSON.stringify(loaded.prose?.words ?? {})}`
|
|
272
|
-
}
|
|
273
172
|
return null
|
|
274
173
|
}
|
|
275
174
|
|
|
276
|
-
/** the
|
|
277
|
-
* EITHER list (moving a word from allow to ban is one edit), then `remove`
|
|
278
|
-
* drops words. Judged the way defineRepo judges them */
|
|
279
|
-
export function nextWords(before: ProseWords, edit: WordsEdit): ProseWords {
|
|
280
|
-
const allow = [...(before.allow ?? [])]
|
|
281
|
-
const ban = [...(before.ban ?? [])]
|
|
282
|
-
const drop = (w: string): void => {
|
|
283
|
-
const n = normWord(w)
|
|
284
|
-
for (const list of [allow, ban] as { word: string }[][]) {
|
|
285
|
-
const at = list.findIndex((x) => normWord(x.word) === n)
|
|
286
|
-
if (at !== -1) list.splice(at, 1)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const trimmed = <T extends { word: string }>(e: T): T =>
|
|
290
|
-
Object.fromEntries(Object.entries(e).filter(([, v]) => typeof v === 'string' && v.trim() !== '').map(([key, v]) => [key, (v as string).trim()])) as T
|
|
291
|
-
for (const a of edit.allow ?? []) {
|
|
292
|
-
if (typeof a?.word !== 'string' || normWord(a.word) === '') throw new Error(`prose.words.allow needs a word`)
|
|
293
|
-
drop(a.word)
|
|
294
|
-
allow.push(trimmed({ word: a.word, reason: a.reason }))
|
|
295
|
-
}
|
|
296
|
-
for (const b of edit.ban ?? []) {
|
|
297
|
-
if (typeof b?.word !== 'string' || normWord(b.word) === '') throw new Error(`prose.words.ban needs a word`)
|
|
298
|
-
drop(b.word)
|
|
299
|
-
ban.push(trimmed({ word: b.word, to: b.to, reason: b.reason }))
|
|
300
|
-
}
|
|
301
|
-
for (const w of edit.remove ?? []) drop(w)
|
|
302
|
-
const out: ProseWords = {}
|
|
303
|
-
if (allow.length > 0) out.allow = allow
|
|
304
|
-
if (ban.length > 0) out.ban = ban
|
|
305
|
-
const problems = proseWordsProblems(out)
|
|
306
|
-
if (problems.length > 0) throw new Error(problems[0])
|
|
307
|
-
return out
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** the ONE write path, for the switch, the slow suite form and the prose word
|
|
311
|
-
* lists alike. Each block is rewritten only when the edit touches it, so a
|
|
312
|
-
* word edit never inserts an empty gate block, and the reverse */
|
|
175
|
+
/** the ONE write path, for the switch and the slow suite form alike */
|
|
313
176
|
export async function writeGateBlock(root: string, edit: GateEdit, o: { expect?: string } = {}): Promise<GateBlock> {
|
|
314
177
|
const before = await readGateBlock(root)
|
|
315
178
|
if (before.state === 'missing-file') throw new Error(`no repo.config.ts in ${root} — run: shipkit init`)
|
|
316
179
|
if (o.expect != null && o.expect !== before.hash)
|
|
317
180
|
throw new Error(`repo.config.ts changed since you read it, reload the page and set it again, so this edit cannot overwrite one made elsewhere`)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (!touchGate && !touchWords) throw new Error(`nothing to write: the edit names no off, no slow suite and no prose word`)
|
|
321
|
-
if (touchGate && before.state === 'stale')
|
|
181
|
+
if (edit.off == null && edit.slow == null && edit.slowRemove == null) throw new Error(`nothing to write: the edit names no off and no slow suite`)
|
|
182
|
+
if (before.state === 'stale')
|
|
322
183
|
throw new Error(`the shipkit gate block in repo.config.ts no longer matches what the config loads, so it was hand edited or something else declares these keys — reconcile it by hand, then use the switch again`)
|
|
323
|
-
if (
|
|
184
|
+
if (before.state === 'missing-block' && before.declaredOutside)
|
|
324
185
|
throw new Error(`repo.config.ts declares \`off\` or \`tests\` outside the managed block — move that declaration between the markers first, or the switch would write a duplicate key`)
|
|
325
|
-
if (touchWords && before.wordsState === 'no-prose')
|
|
326
|
-
throw new Error(`repo.config.ts has no \`prose: {\` line to write the word lists under — declare \`prose: { include: [...] }\` first, the lists only matter to a repo rule SK014 checks`)
|
|
327
|
-
if (touchWords && before.wordsState === 'stale')
|
|
328
|
-
throw new Error(`the shipkit prose words block in repo.config.ts no longer matches what the config loads, so it was hand edited — reconcile it by hand, then edit the lists again`)
|
|
329
|
-
if (touchWords && before.wordsState === 'missing-block' && before.wordsOutside)
|
|
330
|
-
throw new Error(`repo.config.ts declares \`prose.words\` outside the managed block — move it between the markers first, or the page would write a duplicate key`)
|
|
331
186
|
|
|
332
187
|
// the next values, validated the way defineRepo validates them, BEFORE
|
|
333
188
|
// anything reaches the file
|
|
@@ -350,47 +205,32 @@ export async function writeGateBlock(root: string, edit: GateEdit, o: { expect?:
|
|
|
350
205
|
}
|
|
351
206
|
const dropped = new Set(edit.slowRemove ?? [])
|
|
352
207
|
const tests: TestsConfig = { ...before.tests, slow: slow.filter((s) => !dropped.has(s.file)) }
|
|
353
|
-
const words = touchWords ? nextWords(before.words, edit.words!) : undefined
|
|
354
208
|
|
|
355
209
|
const text = readFileSync(before.path, 'utf8')
|
|
356
|
-
let out
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
out = [...lines.slice(0, anchor + 1), ...block, ...lines.slice(anchor + 1)]
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (words != null) {
|
|
375
|
-
const lines = out
|
|
376
|
-
const b = lines.findIndex(isWordsBegin)
|
|
377
|
-
const e = lines.findIndex(isWordsEnd)
|
|
378
|
-
const unit = indentOf(lines)
|
|
379
|
-
if (b !== -1 && e > b) {
|
|
380
|
-
const pad = /^([ \t]*)/.exec(lines[b]!)?.[1] ?? ''
|
|
381
|
-
out = [...lines.slice(0, b), ...renderWordsBlock(words, pad, unit), ...lines.slice(e + 1)]
|
|
382
|
-
} else {
|
|
383
|
-
const open = lines.findIndex(isProseOpen)
|
|
384
|
-
out = [...lines.slice(0, open + 1), ...renderWordsBlock(words, innerPad(lines, open), unit), ...lines.slice(open + 1)]
|
|
385
|
-
}
|
|
210
|
+
let out: string[]
|
|
211
|
+
const lines = text.split('\n')
|
|
212
|
+
const b = lines.findIndex(isBegin)
|
|
213
|
+
const e = lines.findIndex(isEnd)
|
|
214
|
+
const pad = b === -1 ? indentOf(lines) : indentOf(lines.slice(b + 1, e)) || indentOf(lines)
|
|
215
|
+
const block = renderGateBlock(off, tests, pad)
|
|
216
|
+
if (b !== -1 && e !== -1) out = [...lines.slice(0, b), ...block, ...lines.slice(e + 1)]
|
|
217
|
+
else {
|
|
218
|
+
// the anchor is the object literal `defineRepo({` opens. Without it
|
|
219
|
+
// there is nothing to write into, so the block is PRINTED for a
|
|
220
|
+
// human to paste rather than guessed at
|
|
221
|
+
const anchor = lines.findIndex((l) => /defineRepo\(\s*\{\s*$/.test(l))
|
|
222
|
+
if (anchor === -1)
|
|
223
|
+
throw new Error(`repo.config.ts has no \`defineRepo({\` line to write into — paste this block inside the config object by hand:\n${block.join('\n')}`)
|
|
224
|
+
out = [...lines.slice(0, anchor + 1), ...block, ...lines.slice(anchor + 1)]
|
|
386
225
|
}
|
|
226
|
+
|
|
387
227
|
atomicWrite(before.path, out.join('\n'))
|
|
388
228
|
|
|
389
229
|
// read it back through the loader the cli uses. A block that does not
|
|
390
230
|
// round trip is a broken config, so the previous bytes go back
|
|
391
231
|
try {
|
|
392
232
|
const after = await loadRepoConfig(root, { fresh: true })
|
|
393
|
-
const problem = roundTripProblem({ off, tests
|
|
233
|
+
const problem = roundTripProblem({ off, tests }, after)
|
|
394
234
|
if (problem != null) throw new Error(`the block did not round trip: ${problem}`)
|
|
395
235
|
} catch (err) {
|
|
396
236
|
atomicWrite(before.path, text)
|
package/src/core/when.ts
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
// THE CHECKS A REPO TURNS OFF: `off: { <id>: '<why>' }` in repo.config.ts,
|
|
2
2
|
// the switch the hub writes inside its managed block. Where a check runs is
|
|
3
|
-
// its own declaration (core/runs.ts);
|
|
4
|
-
//
|
|
3
|
+
// its own declaration (core/runs.ts); a repo says of a whole check it does
|
|
4
|
+
// not own only that it does not run here, and why. An off row runs in
|
|
5
5
|
// no context, and every surface shows it with its reason.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// entry `'off:<why>'` (defineRepo refuses the other words)
|
|
9
|
-
//
|
|
7
|
+
// Four retired spellings still read as off while SK029 ports them: a `when`
|
|
8
|
+
// entry `'off:<why>'` (defineRepo refuses the other words), `ruleExempt`,
|
|
9
|
+
// `hooks.disable` and a floor slot `defaults: { <slot>: false }`. The last two
|
|
10
|
+
// never carried a reason, so they read with one that says so. They die once
|
|
11
|
+
// `shipkit fleet` shows no repo carrying them.
|
|
10
12
|
import type { CheckRow } from '../ledger/gate-checks.ts'
|
|
11
13
|
import type { ResolvedRuns } from './runs.ts'
|
|
12
14
|
import type { RepoConfig } from '../repo-config.ts'
|
|
13
15
|
|
|
14
16
|
/** the coarse word a resolved row reads as on the check page */
|
|
15
|
-
export type WhenAt = 'always' | 'release' | 'health' | 'off'
|
|
17
|
+
export type WhenAt = 'always' | 'release' | 'health' | 'commit' | 'off'
|
|
16
18
|
|
|
17
19
|
/** which declaration is answering: the repo's `off`, one of its two retired
|
|
18
20
|
* spellings, the check file, the `defaults` block, or a derived row */
|
|
19
|
-
export type WhenSource = 'off' | 'when' | 'ruleExempt' | 'check-file' | 'defaults' | 'default'
|
|
21
|
+
export type WhenSource = 'off' | 'when' | 'ruleExempt' | 'hooks.disable' | 'check-file' | 'defaults' | 'default'
|
|
20
22
|
|
|
21
23
|
export type WhenDecl = { at: WhenAt; reason: string | null; source: WhenSource }
|
|
22
24
|
|
|
23
|
-
export const WHEN_AT: readonly WhenAt[] = ['always', 'release', 'health', 'off']
|
|
25
|
+
export const WHEN_AT: readonly WhenAt[] = ['always', 'release', 'health', 'commit', 'off']
|
|
24
26
|
|
|
25
27
|
/** the words a retired `when` value used */
|
|
26
28
|
const WORDS: Record<string, WhenAt> = { always: 'always', release: 'release', health: 'health' }
|
|
@@ -41,15 +43,30 @@ export function parseWhen(raw: unknown): WhenDecl | string {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/** the repo turned this check off, with why and where it said so, or null */
|
|
44
|
-
export type RepoOff = { reason: string; source: 'off' | 'when' | 'ruleExempt' }
|
|
46
|
+
export type RepoOff = { reason: string; source: 'off' | 'when' | 'ruleExempt' | 'hooks.disable' | 'defaults' }
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
/** the reason a spelling that never carried one reads with, and the one the
|
|
49
|
+
* SK029 port writes when no comment beside the entry says more. A machine
|
|
50
|
+
* cannot invent a reason, so every surface shows the gap */
|
|
51
|
+
export const legacyOffReason = (source: 'hooks.disable' | 'defaults', id: string): string =>
|
|
52
|
+
source === 'defaults' ? `was defaults: { ${/^[A-Za-z_$][\w$]*$/.test(id) ? id : `'${id}'`}: false }, no reason recorded` : `was hooks.disable: ['${id}'], no reason recorded`
|
|
53
|
+
|
|
54
|
+
/** the keys `repoOff` reads, so a caller holding part of a config can ask */
|
|
55
|
+
export type OffKeys = Pick<RepoConfig, 'off' | 'when' | 'ruleExempt'> & { hooks?: { disable?: readonly string[] }; defaults?: Record<string, unknown> }
|
|
56
|
+
|
|
57
|
+
/** THE one answer to "is this id off here", for a check, a floor slot and a
|
|
58
|
+
* commit gate alike. A floor slot that is off is off everywhere: no task, no
|
|
59
|
+
* routed script, no deny rule */
|
|
60
|
+
export function repoOff(c: OffKeys, id: string): RepoOff | null {
|
|
47
61
|
const own = c.off?.[id]
|
|
48
62
|
if (own != null) return { reason: own, source: 'off' }
|
|
49
63
|
const w = c.when?.[id] == null ? null : parseWhen(c.when[id])
|
|
50
64
|
if (w != null && typeof w !== 'string' && w.at === 'off') return { reason: w.reason ?? '', source: 'when' }
|
|
51
65
|
const exempt = c.ruleExempt?.[id]
|
|
52
|
-
|
|
66
|
+
if (exempt != null) return { reason: exempt, source: 'ruleExempt' }
|
|
67
|
+
if (c.hooks?.disable?.includes(id) === true) return { reason: legacyOffReason('hooks.disable', id), source: 'hooks.disable' }
|
|
68
|
+
if (c.defaults?.[id] === false) return { reason: legacyOffReason('defaults', id), source: 'defaults' }
|
|
69
|
+
return null
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
/** a resolved row the repo turned off: it runs in no context */
|
|
@@ -57,10 +74,10 @@ export const withRepoOff = (base: ResolvedRuns, off: RepoOff | null): ResolvedRu
|
|
|
57
74
|
|
|
58
75
|
/** the coarse word a resolved row reads as, for the check page. Derived,
|
|
59
76
|
* never declared */
|
|
60
|
-
export function whenSummary(r: ResolvedRuns, off: RepoOff |
|
|
77
|
+
export function whenSummary(r: ResolvedRuns, off: RepoOff | null, fromFile: boolean, o: { hook?: boolean } = {}): WhenDecl {
|
|
61
78
|
const source: WhenSource = off?.source ?? (fromFile ? 'check-file' : 'default')
|
|
62
79
|
if (r.off != null) return { at: 'off', reason: r.off, source }
|
|
63
|
-
const at: WhenAt = r.onCi ? 'always' : r.onFull || r.onRelease ? 'release' : r.onHealth || r.onSave !== false || r.onClock !== false ? 'health' : 'off'
|
|
80
|
+
const at: WhenAt = r.onCi ? 'always' : r.onFull || r.onRelease ? 'release' : r.onHealth || r.onSave !== false || r.onClock !== false ? 'health' : o.hook === true || r.onPreCommit !== false ? 'commit' : 'off'
|
|
64
81
|
return { at, reason: at === 'off' ? 'no context runs it' : null, source }
|
|
65
82
|
}
|
|
66
83
|
|
package/src/core/wire.ts
CHANGED
|
@@ -13,7 +13,23 @@ export { spanShort }
|
|
|
13
13
|
* as a future expiry long after it passed */
|
|
14
14
|
export type Stamp = { at: number; until: number | null; cached?: boolean; ranMs?: number }
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** where a finding sits: a repo-relative file, and its line and column when
|
|
17
|
+
* the check knows them. A site is what an in-file directive can waive
|
|
18
|
+
* (core/waivers.ts) and what the problems pane opens */
|
|
19
|
+
export type Site = { file: string; line?: number; col?: number }
|
|
20
|
+
|
|
21
|
+
export type Line = {
|
|
22
|
+
kind: 'ok' | 'warn' | 'fail' | 'note' | 'fix-warn' | 'fix-fail' | 'skip'
|
|
23
|
+
text: string
|
|
24
|
+
stamp?: Stamp
|
|
25
|
+
at?: Site
|
|
26
|
+
/** a waiver report: `count` prints with the check, `site` lists one
|
|
27
|
+
* waived site and prints only where every line does (--verbose, STATUS.md) */
|
|
28
|
+
waiver?: 'count' | 'site'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `file:line:col`, as far as the site knows */
|
|
32
|
+
export const siteText = (s: Site): string => `${s.file}${s.line == null ? '' : `:${s.line}${s.col == null ? '' : `:${s.col}`}`}`
|
|
17
33
|
|
|
18
34
|
/** `cached, 3h old, kept 57m more` for a replay, `ran 0.4s, kept 1h more`
|
|
19
35
|
* for an answer this run measured, `ran 1d ago, expired 1d ago` for one
|
package/src/git-bootstrap.ts
CHANGED
|
@@ -323,7 +323,7 @@ export function ensureIgnoreBaseline(root: string): void {
|
|
|
323
323
|
|
|
324
324
|
/** never in a first commit, wherever it sits in the tree. `.env.example` and
|
|
325
325
|
* friends are documentation and belong in the repo */
|
|
326
|
-
function isRisky(f: string): boolean {
|
|
326
|
+
export function isRisky(f: string): boolean {
|
|
327
327
|
if (f.split('/').includes('node_modules')) return true
|
|
328
328
|
const base = basename(f)
|
|
329
329
|
return base.startsWith('.env') && !/^\.env\.(example|sample|template|dist)$/.test(base)
|
package/src/home/attach.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// `shipkit serve` while a home runs: the hub itself runs in the home, and
|
|
2
|
-
// this process only streams its lines
|
|
3
|
-
// problem matcher and its clickable url, with no change to any repo's
|
|
4
|
-
// tasks.json, and closing VS Code no longer stops the hub.
|
|
2
|
+
// this process only streams its lines, so stopping it never stops the hub.
|
|
5
3
|
//
|
|
6
|
-
// the log is what the home prints for that hub, so the frames the
|
|
7
|
-
//
|
|
4
|
+
// the log is what the home prints for that hub, so the frames arrive as the
|
|
5
|
+
// hub prints them. On attach, the last complete frame is replayed first,
|
|
8
6
|
// so the problems pane fills at once instead of at the next run.
|
|
9
7
|
import { closeSync, existsSync, openSync, readSync, statSync } from 'fs'
|
|
10
8
|
import { FRAME_BEGIN, FRAME_END_HEAD } from '../problems/problem-line.ts'
|
package/src/home/insights.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { homedir } from 'os'
|
|
|
11
11
|
import { join } from 'path'
|
|
12
12
|
import { gitBin } from '../core/git-bin.ts'
|
|
13
13
|
import { boxesOf, type InsightsAnswer, type Manifest, type Scope } from './insights-shape.ts'
|
|
14
|
+
import { isVendored } from '../core/vendored.ts'
|
|
15
|
+
import { readVendored } from '../core/vendored-read.ts'
|
|
14
16
|
|
|
15
17
|
export * from './insights-shape.ts'
|
|
16
18
|
|
|
@@ -40,7 +42,10 @@ async function manifestPaths(dir: string): Promise<string[]> {
|
|
|
40
42
|
const p = Bun.spawn([gitBin(), 'ls-files', '-z', '--', 'package.json', '**/package.json'], { cwd: dir, stdout: 'pipe', stderr: 'ignore', env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' } })
|
|
41
43
|
const [out, code] = await Promise.all([new Response(p.stdout).text(), p.exited])
|
|
42
44
|
if (code !== 0) return existsSync(join(dir, 'package.json')) ? ['package.json'] : []
|
|
43
|
-
|
|
45
|
+
// a vendored library's manifest is its upstream's, not a package this repo uses
|
|
46
|
+
let vendored: string[] = []
|
|
47
|
+
try { vendored = readVendored(dir) } catch { /* a config the repo cannot read is its own gate's failure */ }
|
|
48
|
+
return out.split('\0').filter((f) => f !== '' && !isVendored(f, vendored))
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/** one repo's manifests, read; a file that does not parse is an error line */
|
package/src/home/pm-config.ts
CHANGED
|
@@ -30,9 +30,9 @@ if (import.meta.main) {
|
|
|
30
30
|
const out: PmConfig = { index: house.index, processes: [], errors: [], inputs, stamp: stampOf(inputs) }
|
|
31
31
|
if (house.index != null) {
|
|
32
32
|
// a unit file imports the lib by name: this copy, like the daemon's
|
|
33
|
-
const {
|
|
33
|
+
const { installOneLib } = await import('../core/one-lib.ts')
|
|
34
34
|
const { scanGlobalUnits } = await import('./global-units.ts')
|
|
35
|
-
|
|
35
|
+
installOneLib()
|
|
36
36
|
const u = await scanGlobalUnits(repos.map((r) => ({ name: r.name, path: r.path })), { files: (r) => files.get(r) ?? [] })
|
|
37
37
|
out.processes = u.processes
|
|
38
38
|
out.errors = u.errors
|
package/src/home/run-daemon.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// at once (lib-watch.ts): the new image follows the jobs still running. The
|
|
8
8
|
// machine resources are pm's.
|
|
9
9
|
import { fileURLToPath } from 'url'
|
|
10
|
-
import {
|
|
10
|
+
import { installOneLib } from '../core/one-lib.ts'
|
|
11
11
|
import { reclaimPreviousImage } from '../core/instance.ts'
|
|
12
12
|
import { reloadOnOwnCode } from './lib-watch.ts'
|
|
13
13
|
import { loadHub } from '../serve/hub-runtime.ts'
|
|
@@ -47,7 +47,7 @@ export async function runDaemon(root: string, io: HomeIo, o: { port?: number } =
|
|
|
47
47
|
}
|
|
48
48
|
// before the first repo config loads: every one of them, and every check
|
|
49
49
|
// file, then imports this copy of the lib
|
|
50
|
-
|
|
50
|
+
installOneLib()
|
|
51
51
|
// a process this one starts runs its gate and its hub in process
|
|
52
52
|
process.env[WORKER_ENV] = '1'
|
|
53
53
|
// a restart in place keeps the pid: the check children its previous
|
|
@@ -12,8 +12,8 @@ if (import.meta.main) {
|
|
|
12
12
|
console.error('usage: bun run-unit-action.ts <unit file> <action id>')
|
|
13
13
|
process.exit(2)
|
|
14
14
|
}
|
|
15
|
-
const {
|
|
16
|
-
|
|
15
|
+
const { installOneLib } = await import('../core/one-lib.ts')
|
|
16
|
+
installOneLib()
|
|
17
17
|
const u = ((await import(file)) as { default?: GlobalProcessUnit }).default
|
|
18
18
|
const run = u?.actions?.[id]?.run
|
|
19
19
|
if (run == null) {
|