rvlib-shipkit 1.7.0 → 1.9.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/package.json +1 -1
  3. package/src/assets/gen/comfyts-gen.ts +31 -14
  4. package/src/assets/gen/generate-roll.ts +20 -7
  5. package/src/assets/kit-config.ts +9 -0
  6. package/src/assets/kits/identity-kit.ts +7 -1
  7. package/src/assets/run-assets.ts +3 -3
  8. package/src/assets/server/studio-server.ts +3 -3
  9. package/src/checks/SK027-agent-wiki-links.check.ts +6 -2
  10. package/src/checks/SK033-global-contribution.check.ts +47 -0
  11. package/src/cli/cmd_Ci.ts +5 -4
  12. package/src/cli/cmd_GlobalContext.ts +47 -0
  13. package/src/cli/cmd_GlobalProcesses.ts +38 -1
  14. package/src/cli.ts +4 -1
  15. package/src/collections/providers.ts +1 -1
  16. package/src/core/check-runner.ts +13 -4
  17. package/src/core/discover-checks.ts +37 -8
  18. package/src/core/fix-scope.ts +5 -0
  19. package/src/core/floor-resolve.ts +2 -1
  20. package/src/core/git-root.ts +22 -12
  21. package/src/core/global-contribution.ts +42 -0
  22. package/src/core/one-lib.ts +4 -2
  23. package/src/core/run-ci.ts +12 -11
  24. package/src/core/slots.ts +36 -6
  25. package/src/home/global-context.ts +145 -0
  26. package/src/home/gui-app/ShipkitGui.swift +96 -0
  27. package/src/home/home-routes.ts +10 -0
  28. package/src/home/resources.ts +3 -0
  29. package/src/plan/planner.ts +21 -4
  30. package/src/plan/repo-source.ts +7 -3
  31. package/src/release/run-release.ts +17 -8
  32. package/src/release/unit-gate.ts +12 -5
  33. package/src/repo-config.ts +7 -0
  34. package/src/serve/hub-runtime.ts +34 -23
  35. package/src/serve/project-gutter.ts +5 -2
  36. package/src/serve/run-lanes.ts +138 -0
  37. package/src/smoke/run-smoke.ts +13 -6
  38. package/src/suite/judge-suites.ts +79 -11
  39. package/src/suite/reads-env.ts +8 -0
  40. package/src/suite/select.ts +510 -0
  41. package/src/suite/suite-cli.ts +213 -23
  42. package/src/suite/suite-reads-preload.ts +335 -0
  43. package/src/ts/typecheck-cache.ts +11 -0
  44. package/src/ts/typecheck-cli.ts +2 -1
@@ -0,0 +1,335 @@
1
+ // WHAT EACH MODULE READ, preloaded into every test worker and every bun
2
+ // process a suite spawns. The quick tier runs only the suites whose reads
3
+ // changed since the last green run (select.ts), and a module's static imports
4
+ // are only part of what it reads: a suite also reads files by path, lists
5
+ // dirs, globs and spawns. This preload records each of those, attributed to
6
+ // every module of the repo on the call stack. A memo filled by one suite and
7
+ // used by the next is still charged to the module that holds it, and every
8
+ // suite whose import closure holds that module depends on the read.
9
+ //
10
+ // Lines, appended to the file SHIPKIT_SUITE_READS names: `<kind>\t<module>\t<path>`.
11
+ // r a file read, probed or stat'ed (its content, or its absence, matters)
12
+ // d a dir listed (its names matter)
13
+ // g a tree walked by a glob (every name under it matters)
14
+ // o a process spawned that reads what nobody records (git or a shell run
15
+ // in the repo): the module is opaque, and every suite that reaches it
16
+ // runs every time
17
+ // l a repo module loaded at runtime from a call this module was on the
18
+ // stack of (a require or an import() in a function body)
19
+ // L a repo module loaded while this module itself was being loaded
20
+ // A read with no repo frame on its stack is charged to `-`, which every suite
21
+ // depends on. A bun child records under the module that spawned it.
22
+ //
23
+ // node:fs is replaced through bun:test's mock.module, the one hook that also
24
+ // reaches a named ESM import, and it updates modules imported before it.
25
+ import { mock, afterAll } from 'bun:test'
26
+ import * as fsLive from 'node:fs'
27
+ import * as fspLive from 'node:fs/promises'
28
+ import * as cpLive from 'node:child_process'
29
+ import { appendFileSync, realpathSync } from 'node:fs'
30
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path'
31
+ import { fileURLToPath } from 'node:url'
32
+ import { SUITE_OWNER_ENV, SUITE_READS_ENV, SUITE_ROOT_ENV } from './reads-env.ts'
33
+
34
+ const out = process.env[SUITE_READS_ENV]
35
+ const root = process.env[SUITE_ROOT_ENV]
36
+ const owner = process.env[SUITE_OWNER_ENV]
37
+ const SELF = import.meta.path
38
+
39
+ if (out != null && out !== '' && root != null && root !== '') install(out, resolve(root))
40
+
41
+ function install(outFile: string, rootDir: string): void {
42
+ const realFs = { ...fsLive } as Record<string, unknown>
43
+ const realFsp = { ...fspLive } as Record<string, unknown>
44
+ const realCp = { ...cpLive } as Record<string, unknown>
45
+ // the root as git prints it is a realpath, and a path a module names can
46
+ // go through a symlink to it (macOS: /var is /private/var), so each
47
+ // spelling of the root counts
48
+ const real = ((): string => { try { return realpathSync(rootDir) } catch { return rootDir } })()
49
+ const roots = [...new Set([rootDir, real, ...[rootDir, real].filter((r) => r.startsWith('/private/')).map((r) => r.slice('/private'.length))])]
50
+ const seen = new Set<string>()
51
+ let buf: string[] = []
52
+
53
+ /** repo-relative posix path, null outside the repo, in an install, or in git's own dir */
54
+ const rel = (p: unknown): string | null => {
55
+ let s: string
56
+ if (typeof p === 'string') s = p
57
+ else if (p instanceof URL) { if (p.protocol !== 'file:') return null; s = fileURLToPath(p) }
58
+ else if (p instanceof Uint8Array) s = Buffer.from(p).toString()
59
+ else return null
60
+ if (s.startsWith('file:')) { try { s = fileURLToPath(s) } catch { return null } }
61
+ const abs = isAbsolute(s) ? resolve(s) : resolve(process.cwd(), s)
62
+ const base = roots.find((r) => abs === r || abs.startsWith(r.endsWith(sep) ? r : `${r}${sep}`))
63
+ if (base == null) return null
64
+ const r = relative(base, abs).split(sep).join('/')
65
+ if (r === '' ) return '.'
66
+ if (r === 'node_modules' || r.startsWith('node_modules/') || r.includes('/node_modules/')) return null
67
+ return r
68
+ }
69
+ /** the innermost repo module on the stack, outside this file */
70
+ // a read is charged to EVERY repo module on the stack: the one that read
71
+ // it, the ones that hold a memo of what it read, and the one that asked. A
72
+ // cache a preload installs is asked through the module that holds its
73
+ // hook, and every suite reusing the memo calls that module again
74
+ const callers = (): string[] => {
75
+ if (owner != null && owner !== '') return [owner]
76
+ const limit = Error.stackTraceLimit
77
+ Error.stackTraceLimit = 80
78
+ const stack = new Error().stack ?? ''
79
+ Error.stackTraceLimit = limit
80
+ const out: string[] = []
81
+ for (const line of stack.split('\n')) {
82
+ const m = /\(?((?:\/|[A-Za-z]:\\)[^():]+):\d+:\d+\)?\s*$/.exec(line)
83
+ const file = m?.[1]
84
+ if (file == null || file === SELF) continue
85
+ const r = rel(file)
86
+ if (r != null && r !== '.' && !out.includes(r)) out.push(r)
87
+ }
88
+ return out.length > 0 ? out : ['-']
89
+ }
90
+ /** the module a child records under: the innermost one that spawned it */
91
+ const caller = (): string => callers()[0]!
92
+ /** a read through a symlink reads its target too */
93
+ const target = (path: string): string | null => {
94
+ try {
95
+ const real = rel((realFs['realpathSync'] as (p: string) => string)(join(rootDir, path)))
96
+ return real != null && real !== path ? real : null
97
+ } catch {
98
+ return null
99
+ }
100
+ }
101
+ const note = (kind: string, path: string | null, mods: string | string[] = callers()): void => {
102
+ if (path == null) return
103
+ if (kind !== 'o') {
104
+ const t = target(path)
105
+ if (t != null) note(kind, t, mods)
106
+ }
107
+ for (const mod of typeof mods === 'string' ? [mods] : mods) {
108
+ const line = `${kind}\t${mod}\t${path}`
109
+ if (seen.has(line)) continue
110
+ seen.add(line)
111
+ buf.push(line)
112
+ }
113
+ }
114
+ const flush = (): void => {
115
+ if (buf.length === 0) return
116
+ try { appendFileSync(outFile, `${buf.join('\n')}\n`) } catch { /* no record: the next full run fills it */ }
117
+ buf = []
118
+ }
119
+
120
+ const READS = ['readFileSync', 'readFile', 'existsSync', 'exists', 'statSync', 'stat', 'lstatSync', 'lstat', 'accessSync', 'access', 'openSync', 'open', 'createReadStream', 'readlinkSync', 'readlink', 'realpathSync', 'realpath', 'opendirSync', 'opendir', 'copyFileSync', 'copyFile', 'watchFile', 'statfsSync', 'statfs']
121
+ // a copy or a watch of a dir takes the whole tree under it
122
+ const TREES = ['cpSync', 'cp', 'watch']
123
+ const LISTS = ['readdirSync', 'readdir']
124
+ const GLOBS = ['globSync', 'glob']
125
+ const wrap = (real: Record<string, unknown>, kind: string, names: string[]): Record<string, unknown> => {
126
+ const shim: Record<string, unknown> = {}
127
+ for (const n of names) {
128
+ const f = real[n]
129
+ if (typeof f !== 'function') continue
130
+ const g = function (this: unknown, ...a: unknown[]): unknown {
131
+ if (kind === 'g') note('g', globRoot(a[0], a[1]))
132
+ else if (kind === 't') {
133
+ const r = rel(a[0])
134
+ note(r != null && exists(join(rootDir, r), false) ? 'r' : 'g', r)
135
+ }
136
+ else {
137
+ const r = rel(a[0])
138
+ // a recursive listing is a walk of the whole tree
139
+ const recursive = kind === 'd' && typeof a[1] === 'object' && a[1] != null && (a[1] as { recursive?: unknown }).recursive === true
140
+ note(recursive ? 'g' : n.startsWith('opendir') ? 'd' : kind, r)
141
+ }
142
+ return (f as (...x: unknown[]) => unknown).apply(this, a)
143
+ }
144
+ Object.assign(g, f)
145
+ shim[n] = g
146
+ }
147
+ return shim
148
+ }
149
+ const globRoot = (pattern: unknown, opts: unknown): string | null => {
150
+ const pats = Array.isArray(pattern) ? pattern : [pattern]
151
+ const cwd = typeof opts === 'object' && opts != null && typeof (opts as { cwd?: unknown }).cwd === 'string' ? (opts as { cwd: string }).cwd : process.cwd()
152
+ const roots = pats.filter((p): p is string => typeof p === 'string').map((p) => {
153
+ const lit = p.split('/').filter((_, i, a) => !/[*?[{]/.test(a.slice(0, i + 1).join('/'))).join('/')
154
+ return rel(join(cwd, lit))
155
+ })
156
+ return roots.length === 1 ? roots[0]! : rel(cwd)
157
+ }
158
+ const fsShim: Record<string, unknown> = { ...realFs, ...wrap(realFs, 'r', READS), ...wrap(realFs, 'd', LISTS), ...wrap(realFs, 'g', GLOBS), ...wrap(realFs, 't', TREES) }
159
+ const fspShim: Record<string, unknown> = { ...realFsp, ...wrap(realFsp, 'r', READS), ...wrap(realFsp, 'd', LISTS), ...wrap(realFsp, 'g', GLOBS), ...wrap(realFsp, 't', TREES) }
160
+ fsShim['promises'] = fspShim
161
+ mock.module('node:fs', () => ({ ...fsShim, default: fsShim }))
162
+ mock.module('fs', () => ({ ...fsShim, default: fsShim }))
163
+ mock.module('node:fs/promises', () => ({ ...fspShim, default: fspShim }))
164
+ mock.module('fs/promises', () => ({ ...fspShim, default: fspShim }))
165
+
166
+ // a bun child runs this same preload and records under the spawning module.
167
+ // Any other child reads what nobody records
168
+ /** a path that stands there (a file, or a dir when `dir`) */
169
+ const exists = (abs: string, dir: boolean): boolean => {
170
+ try {
171
+ const st = (realFs['statSync'] as (p: string) => { isFile(): boolean; isDirectory(): boolean })(abs)
172
+ return st.isFile() || (dir && st.isDirectory())
173
+ } catch { return false }
174
+ }
175
+ const SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'fish', 'env', 'xargs', 'nice', 'time', 'timeout'])
176
+ // tools that read only the files their arguments name, nothing they find through them
177
+ const FILE_TOOLS = new Set(['true', 'false', 'echo', 'sleep', 'kill', 'rm', 'mkdir', 'touch', 'cwebp', 'dwebp'])
178
+ const isBun = (bin: unknown): boolean => typeof bin === 'string' && (bin === 'bun' || bin === process.execPath || /(^|\/)bun$/.test(bin))
179
+ const childEnv = (env: unknown): Record<string, string | undefined> => {
180
+ const base = (typeof env === 'object' && env != null ? env : process.env) as Record<string, string | undefined>
181
+ const opts = base['BUN_OPTIONS'] ?? ''
182
+ return {
183
+ ...base,
184
+ [SUITE_READS_ENV]: outFile,
185
+ [SUITE_ROOT_ENV]: rootDir,
186
+ [SUITE_OWNER_ENV]: caller(),
187
+ BUN_OPTIONS: opts.includes(SELF) ? opts : `${opts} --preload=${SELF}`.trim(),
188
+ }
189
+ }
190
+ // a child that is not bun reads what nobody records. It is trusted only
191
+ // when it runs outside the repo and names nothing in it (git in a temp
192
+ // repo); a tool known to read only the files it names (cwebp) records
193
+ // them; any other child in or about the repo makes its module opaque
194
+ const spawned = (cmd: unknown, cwdOpt: unknown): void => {
195
+ const argv = (Array.isArray(cmd) ? cmd : typeof cmd === 'string' ? [cmd] : []).map(String)
196
+ if (isBun(argv[0])) return
197
+ const tool = (argv[0] ?? '?').split('/').pop() ?? '?'
198
+ const cwd = typeof cwdOpt === 'string' ? resolve(cwdOpt) : cwdOpt instanceof URL ? fileURLToPath(cwdOpt) : process.cwd()
199
+ const shell = SHELLS.has(tool)
200
+ const words = argv.slice(1).flatMap((a) => (shell ? a.split(/[\s;&|<>()'"`=]+/) : [a.replace(/^--?[\w-]+=/, '')])).filter((w) => w !== '' && !w.startsWith('-'))
201
+ const inRepo: string[] = []
202
+ for (const w of words) {
203
+ const abs = isAbsolute(w) ? w : join(cwd, w)
204
+ if (!isAbsolute(w) && !w.includes('/') && !exists(abs, true)) continue
205
+ const r = rel(abs)
206
+ if (r != null) inRepo.push(r)
207
+ }
208
+ // a tool known to read only the files it names records them; any other
209
+ // tool is trusted only far from the repo: run outside it, naming nothing in it
210
+ if (!shell && FILE_TOOLS.has(tool)) {
211
+ for (const r of inRepo) note(exists(join(rootDir, r), false) ? 'r' : 'g', r)
212
+ return
213
+ }
214
+ if (rel(cwd) != null || inRepo.length > 0) note('o', tool)
215
+ }
216
+ const realSpawn = Bun.spawn
217
+ const realSpawnSync = Bun.spawnSync
218
+ const viaBun = (real: typeof Bun.spawn | typeof Bun.spawnSync) => function (this: unknown, ...a: unknown[]): unknown {
219
+ const first = a[0]
220
+ const cmd = Array.isArray(first) ? first : (first as { cmd?: unknown } | undefined)?.cmd
221
+ spawned(cmd, ((Array.isArray(first) ? a[1] : first) as { cwd?: unknown } | undefined)?.cwd)
222
+ if (Array.isArray(first)) {
223
+ const opts = (a[1] ?? {}) as { env?: unknown }
224
+ return (real as (...x: unknown[]) => unknown).call(this, first, { ...opts, env: childEnv(opts.env) })
225
+ }
226
+ const opts = (first ?? {}) as { env?: unknown }
227
+ return (real as (...x: unknown[]) => unknown).call(this, { ...opts, env: childEnv(opts.env) })
228
+ }
229
+ Bun.spawn = viaBun(realSpawn) as typeof Bun.spawn
230
+ Bun.spawnSync = viaBun(realSpawnSync) as typeof Bun.spawnSync
231
+ const cpWrap = (n: string, cmdAt: (a: unknown[]) => unknown, optsAt: number | ((a: unknown[]) => number)): unknown => {
232
+ const f = realCp[n]
233
+ if (typeof f !== 'function') return f
234
+ return function (this: unknown, ...a: unknown[]): unknown {
235
+ const cmd = cmdAt(a)
236
+ const i = typeof optsAt === 'number' ? optsAt : optsAt(a)
237
+ const opts = (typeof a[i] === 'object' && a[i] != null && !Array.isArray(a[i]) ? a[i] : {}) as { env?: unknown; cwd?: unknown }
238
+ if (n === 'exec' || n === 'execSync') spawned(['sh', '-c', cmd], opts.cwd)
239
+ else spawned([cmd, ...(Array.isArray(a[1]) ? a[1] : [])], opts.cwd)
240
+ const args = [...a]
241
+ while (args.length < i) args.push(undefined)
242
+ args[i] = { ...opts, env: childEnv(opts.env) }
243
+ return (f as (...x: unknown[]) => unknown).apply(this, args)
244
+ }
245
+ }
246
+ const withArgs = (a: unknown[]): number => (Array.isArray(a[1]) ? 2 : 1)
247
+ const cpShim: Record<string, unknown> = {
248
+ ...realCp,
249
+ spawn: cpWrap('spawn', (a) => a[0], withArgs),
250
+ spawnSync: cpWrap('spawnSync', (a) => a[0], withArgs),
251
+ execFile: cpWrap('execFile', (a) => a[0], withArgs),
252
+ execFileSync: cpWrap('execFileSync', (a) => a[0], withArgs),
253
+ exec: cpWrap('exec', (a) => a[0], 1),
254
+ execSync: cpWrap('execSync', (a) => a[0], 1),
255
+ fork: cpWrap('fork', () => 'bun', withArgs),
256
+ }
257
+ mock.module('node:child_process', () => ({ ...cpShim, default: cpShim }))
258
+ mock.module('child_process', () => ({ ...cpShim, default: cpShim }))
259
+ const realShell = Bun.$
260
+ Bun.$ = new Proxy(realShell, { apply: (t, self, a) => { note('o', 'sh'); return Reflect.apply(t, self, a) } })
261
+
262
+ // a file:// fetch is a read; a Worker runs code no preload of ours sees
263
+ const realFetch = globalThis.fetch
264
+ globalThis.fetch = Object.assign(function (this: unknown, input: unknown, init?: unknown): unknown {
265
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input as { url?: unknown } | null)?.url
266
+ if (typeof url === 'string' && url.startsWith('file:')) note('r', rel(url))
267
+ return (realFetch as (...x: unknown[]) => unknown).call(this, input, init)
268
+ }, realFetch) as typeof fetch
269
+ for (const name of ['Worker', 'SharedWorker']) {
270
+ const Real = (globalThis as Record<string, unknown>)[name] as (new (...a: unknown[]) => object) | undefined
271
+ if (typeof Real !== 'function') continue
272
+ const Wrapped = function (this: unknown, ...a: unknown[]): object {
273
+ note('o', name.toLowerCase())
274
+ return new Real(...a)
275
+ } as unknown as typeof Real
276
+ Wrapped.prototype = Real.prototype
277
+ ;(globalThis as Record<string, unknown>)[name] = Wrapped
278
+ }
279
+
280
+ const realFile = Bun.file
281
+ Bun.file = function (this: unknown, p: unknown, o?: unknown): unknown {
282
+ note('r', rel(p))
283
+ return (realFile as (...x: unknown[]) => unknown).call(this, p, o)
284
+ } as typeof Bun.file
285
+ // a Glob keeps no pattern a caller can read, so each one remembers its own:
286
+ // the walked tree is the cwd joined with the pattern's literal head
287
+ const RealGlob = Bun.Glob
288
+ const patterns = new WeakMap<object, string>()
289
+ const Glob = function (this: unknown, pattern: string): unknown {
290
+ const g = new RealGlob(pattern)
291
+ patterns.set(g, pattern)
292
+ return g
293
+ } as unknown as typeof Bun.Glob
294
+ Glob.prototype = RealGlob.prototype
295
+ Bun.Glob = Glob
296
+ const G = RealGlob.prototype as unknown as Record<string, (...a: unknown[]) => unknown>
297
+ for (const n of ['scan', 'scanSync']) {
298
+ const f = G[n]
299
+ if (typeof f !== 'function') continue
300
+ G[n] = function (this: object, opts?: unknown): unknown {
301
+ const cwd = typeof opts === 'string' ? opts : typeof opts === 'object' && opts != null && typeof (opts as { cwd?: unknown }).cwd === 'string' ? (opts as { cwd: string }).cwd : process.cwd()
302
+ note('g', globRoot(patterns.get(this) ?? '**', { cwd }))
303
+ return f.call(this, opts)
304
+ }
305
+ }
306
+
307
+ // every import resolved at runtime, as an edge from its importer and from
308
+ // every module on the stack: a module a computed import loads is in no
309
+ // static closure, and the edge charges it wherever the load was asked
310
+ // from, whichever suite ran first. A key already seen asks nothing again
311
+ const edges = new Set<string>()
312
+ const imported = (spec: string, importer: string | undefined): void => {
313
+ if (importer == null || importer === '' || spec.startsWith('node:') || spec.startsWith('bun:')) return
314
+ const from = rel(importer)
315
+ if (from == null || from === '.') return
316
+ const key = `${from}\0${spec}\0${new Error().stack ?? ''}`
317
+ if (edges.has(key)) return
318
+ edges.add(key)
319
+ const bare = spec.replace(/\?.*$/, '')
320
+ const base = isAbsolute(bare) ? bare : bare.startsWith('.') ? join(importer, '..', bare) : join(rootDir, bare)
321
+ const abs = [base, `${base}.ts`, `${base}.tsx`, join(base, 'index.ts')].find((x) => exists(x, false))
322
+ if (abs == null) return
323
+ const to = rel(abs)
324
+ // charged like a read: a lazy load inside a hook a preload installed
325
+ // is reached through the modules that called the hook
326
+ if (to == null || to === from) return
327
+ const asked = callers().filter((c) => c !== from && c !== '-')
328
+ if (asked.length === 0) note('L', to, from)
329
+ else note('l', to, [from, ...asked])
330
+ }
331
+ Bun.plugin({ name: 'shipkit-suite-reads', setup(b) { b.onResolve({ filter: /.*/ }, (a) => { imported(a.path, a.importer); return undefined }) } })
332
+
333
+ try { afterAll(flush) } catch { /* not under bun test: the exit hook below records it */ }
334
+ process.on('exit', flush)
335
+ }
@@ -0,0 +1,11 @@
1
+ // the gate's typecheck keeps its own incremental cache, one file per project.
2
+ // a tsconfig's tsBuildInfoFile is shared by every tsc that runs that config (a
3
+ // watcher task, a raw script), and a verdict read off a cache another process
4
+ // writes is not a verdict on this tree. no fs here: it only names paths.
5
+ import { basename, dirname, join } from 'path'
6
+
7
+ export const gateBuildInfoArgs = (tsconfig: string): string[] => [
8
+ '--incremental',
9
+ '--tsBuildInfoFile',
10
+ join(dirname(tsconfig), 'node_modules/.cache/shipkit', `${basename(tsconfig, '.json')}.tsbuildinfo`),
11
+ ]
@@ -10,6 +10,7 @@
10
10
  // who fixes the one error tsc showed and re-runs into a second one has been
11
11
  // told the truth twice and believed it once.
12
12
  import { resolveTypecheckProjects } from 'rvlib-shipkit/src/ts/tsconfig-projects.ts'
13
+ import { gateBuildInfoArgs } from 'rvlib-shipkit/src/ts/typecheck-cache.ts'
13
14
 
14
15
  export function typecheckCli(argv: string[]): number {
15
16
  const [tsc, tsconfig] = argv
@@ -30,7 +31,7 @@ export function typecheckCli(argv: string[]): number {
30
31
 
31
32
  let worst = 0
32
33
  for (const p of resolved.projects) {
33
- const r = Bun.spawnSync([tsc, '--noEmit', '-p', p], { stdout: 'inherit', stderr: 'inherit' })
34
+ const r = Bun.spawnSync([tsc, '--noEmit', '-p', p, ...gateBuildInfoArgs(p)], { stdout: 'inherit', stderr: 'inherit' })
34
35
  if (r.exitCode !== 0) {
35
36
  console.log(`FAIL: ${p}`)
36
37
  worst = r.exitCode ?? 1