rvlib-shipkit 1.4.0 → 1.6.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 +114 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +73 -103
- package/src/checks/SK001-package-json-shape.check.ts +11 -6
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +7 -3
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +17 -13
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_CacheAudit.ts +43 -0
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +8 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +120 -7
- package/src/cli/cmd_Home.ts +26 -19
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +20 -8
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +5 -4
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +6 -4
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_TraceAudit.ts +62 -0
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +11 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +12 -4
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +2 -2
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +43 -23
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-lib.ts +59 -4
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/read-set.ts +97 -9
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +14 -5
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +48 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/trace-audit.ts +120 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/home-start.ts +11 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/via-hub.ts +14 -0
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +133 -61
- package/src/ledger/git-key.ts +153 -31
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +136 -21
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +248 -45
- package/src/plan/repo-source.ts +18 -39
- package/src/problems/extract.ts +3 -0
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +46 -54
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +46 -12
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +14 -35
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +84 -11
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/cols-svc.ts +5 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +6 -0
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +14 -2
- package/src/serve/hub-app/home-strip.css +43 -12
- package/src/serve/hub-app/home-strip.tsx +115 -64
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +32 -8
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +89 -63
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +15 -4
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +45 -0
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +25 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +131 -0
- package/src/serve/hub-app/repo-st.ts +70 -14
- package/src/serve/hub-app/sidebar.tsx +17 -42
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +151 -155
- package/src/serve/hub-app/tree-row.tsx +55 -12
- package/src/serve/hub-app/tree-svc.ts +97 -56
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +7 -18
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -110,7 +110,7 @@ export function isLegacyCheckFile(path: string): boolean {
|
|
|
110
110
|
/** every built-in id, from the file names alone. Sync and import free, so
|
|
111
111
|
* `defineRepo` can read the namespace at config load. Read once per
|
|
112
112
|
* process: the toolkit's own files cannot appear mid run, and `defineRepo`
|
|
113
|
-
* asks for the list once per `
|
|
113
|
+
* asks for the list once per `off` entry. */
|
|
114
114
|
let builtinIds: string[] | null = null
|
|
115
115
|
export function builtinCheckIds(): string[] {
|
|
116
116
|
if (builtinIds != null) return builtinIds
|
|
@@ -144,6 +144,20 @@ async function load(path: string): Promise<CheckDef | null> {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** the stand in for a check file that does not load: it fails in every
|
|
148
|
+
* context with the load error, so the file cannot drop out of a gate unseen */
|
|
149
|
+
function unloadable(path: string, message: string): CheckDef {
|
|
150
|
+
return {
|
|
151
|
+
description: 'the check file loads',
|
|
152
|
+
blocking: true,
|
|
153
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: false, onClock: false },
|
|
154
|
+
cache: { by: 'never', why: 'the file does not load' },
|
|
155
|
+
fix: 'none',
|
|
156
|
+
applies: 'always',
|
|
157
|
+
run: (c) => c.fail(`${basename(path)} does not load: ${message.replace(`${path}: `, '')}`, 'shipkit check --fix SK029 ports a retired check file with the values it ran on, and shipkit ci does it in its fix pass'),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
/** every check this repo can run: its own files, plus the toolkit's. Throws
|
|
148
162
|
* on a registration nobody can act on (a duplicate id, a repo check squatting
|
|
149
163
|
* the reserved prefix, a built-in with no owner page), so a broken
|
|
@@ -170,7 +184,14 @@ export async function discoverChecks(root: string): Promise<DiscoveredCheck[]> {
|
|
|
170
184
|
const ids = new Set<string>()
|
|
171
185
|
for (const f of [...builtins, ...own, ...units]) {
|
|
172
186
|
const isBuiltin = builtins.includes(f)
|
|
173
|
-
|
|
187
|
+
let def: CheckDef | null
|
|
188
|
+
try {
|
|
189
|
+
def = await load(f)
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// a file that does not load is ONE red row, never a dead inventory:
|
|
192
|
+
// every other check still runs, and this one says why in every run
|
|
193
|
+
def = unloadable(f, (e as Error).message)
|
|
194
|
+
}
|
|
174
195
|
if (def == null) continue
|
|
175
196
|
const rel = isBuiltin ? null : relative(root, f).replace(/\\/g, '/')
|
|
176
197
|
const fallback = isBuiltin
|
|
@@ -201,9 +201,9 @@ function builtinFloorTask(id: string, root: string, io: FloorReader): { task: Ta
|
|
|
201
201
|
return `no package.json '${id}' script`
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
// NOTHING here declares a read set on a repo's behalf
|
|
205
|
-
//
|
|
206
|
-
//
|
|
204
|
+
// NOTHING here declares a read set on a repo's behalf. The composer keys a
|
|
205
|
+
// floor slot with no `inputs` on the whole work tree plus the repo's
|
|
206
|
+
// `ignoredInputs` (ledger/gate-checks.ts), and the traced audit fills those.
|
|
207
207
|
//
|
|
208
208
|
// The tempting shortcut is `['.']`, on the reasoning that the whole tree cannot
|
|
209
209
|
// under declare. It can. `['.']` is the TRACKED tree: git-scan force-adds only a
|
|
@@ -214,10 +214,8 @@ function builtinFloorTask(id: string, root: string, io: FloorReader): { task: Ta
|
|
|
214
214
|
// gate print `ok: test (replayed)` over a suite that exits 1
|
|
215
215
|
// (tests/floor-cache-safety.test.ts holds the repro).
|
|
216
216
|
//
|
|
217
|
-
//
|
|
218
|
-
// `defaults
|
|
219
|
-
// `shipkit init` scaffolds the line, `shipkit keys --burn-down` names what is
|
|
220
|
-
// still undeclared, and the gate prints the cost of it once per run.
|
|
217
|
+
// A narrower key is bought by a REPO saying what its own tasks read, in
|
|
218
|
+
// `defaults`.
|
|
221
219
|
|
|
222
220
|
export function resolveFloor(c: RepoConfig, root: string, io: FloorReader): FloorResolution {
|
|
223
221
|
const pkg = io.text('package.json')
|
package/src/core/git-root.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'fs'
|
|
9
9
|
import { dirname, join, resolve } from 'path'
|
|
10
10
|
import { gitBin } from './git-bin.ts'
|
|
11
|
+
import { LAYOUT } from './layout.ts'
|
|
11
12
|
|
|
12
13
|
export type GitRoot = {
|
|
13
14
|
/** absolute git dir of this work tree (per worktree) */
|
|
@@ -130,3 +131,15 @@ export const gitRoot = (dir: string): GitRoot | null => probeGitRoot(dir).root
|
|
|
130
131
|
export function isLinkedWorktree(root: string): boolean {
|
|
131
132
|
try { return statSync(join(root, '.git')).isFile() } catch { return false }
|
|
132
133
|
}
|
|
134
|
+
|
|
135
|
+
/** the one dir everything this repo remembers hangs off.
|
|
136
|
+
*
|
|
137
|
+
* `--git-common-dir` rather than `--git-dir`: a linked worktree has its own
|
|
138
|
+
* git dir but shares the common one, and that sharing is safe because every
|
|
139
|
+
* key component is content or a version string. A tree that is not a git
|
|
140
|
+
* repo at all still gets one, under `.shipkit/cache/`, because a scaffold in
|
|
141
|
+
* progress must not crash on it. */
|
|
142
|
+
export function shipkitRoot(root: string): string {
|
|
143
|
+
const dir = gitRoot(root)?.commonDir
|
|
144
|
+
return dir == null ? join(root, LAYOUT.cache) : join(dir, 'shipkit')
|
|
145
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// THE IGNORED INPUTS BLOCK in `repo.config.ts`: the one place the traced
|
|
2
|
+
// audit (core/trace-audit.ts) writes the gitignored paths each task reads.
|
|
3
|
+
//
|
|
4
|
+
// Its own markers, apart from the gate block (core/when-block.ts), so a repo
|
|
5
|
+
// that writes `off` or `tests` by hand, with its comments, keeps them exactly
|
|
6
|
+
// as they are. The same paranoia as the gate block:
|
|
7
|
+
// 1. `ignoredInputs` declared OUTSIDE the markers is refused, because a
|
|
8
|
+
// second declaration is a duplicate key rather than a merge
|
|
9
|
+
// 2. a block that no longer renders what the config loads was hand edited,
|
|
10
|
+
// and is refused rather than overwritten
|
|
11
|
+
// 3. the result is read back through the loader the cli uses, and the
|
|
12
|
+
// previous bytes go back when it does not round trip
|
|
13
|
+
import { existsSync, readFileSync } from 'fs'
|
|
14
|
+
import { join } from 'path'
|
|
15
|
+
import { atomicWrite } from './locked-write.ts'
|
|
16
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
17
|
+
import type { IgnoredInputs } from './audit-report.ts'
|
|
18
|
+
|
|
19
|
+
export const IGNORED_BEGIN = '// >>> shipkit ignored inputs — managed block, the traced audit writes it, edit outside the markers'
|
|
20
|
+
export const IGNORED_END = '// <<< shipkit ignored inputs'
|
|
21
|
+
|
|
22
|
+
const isBegin = (l: string): boolean => l.trim().startsWith('// >>> shipkit ignored inputs')
|
|
23
|
+
const isEnd = (l: string): boolean => l.trim().startsWith('// <<< shipkit ignored inputs')
|
|
24
|
+
const q = (s: string): string => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
25
|
+
const key = (s: string): string => (/^[A-Za-z_$][\w$]*$/.test(s) ? s : q(s))
|
|
26
|
+
|
|
27
|
+
/** the same map, tasks and paths sorted, empty tasks dropped */
|
|
28
|
+
export function normalizeIgnored(ii: IgnoredInputs): IgnoredInputs {
|
|
29
|
+
const out: IgnoredInputs = {}
|
|
30
|
+
for (const t of Object.keys(ii).sort()) if ((ii[t] ?? []).length > 0) out[t] = [...new Set(ii[t])].sort()
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** the bytes between the markers, markers included */
|
|
35
|
+
export function renderIgnoredBlock(ii: IgnoredInputs, pad: string): string[] {
|
|
36
|
+
const n = normalizeIgnored(ii)
|
|
37
|
+
return [
|
|
38
|
+
`${pad}${IGNORED_BEGIN}`,
|
|
39
|
+
`${pad}ignoredInputs: {`,
|
|
40
|
+
...Object.entries(n).flatMap(([t, ps]) => [`${pad}${pad}${key(t)}: [`, ...ps.map((p) => `${pad}${pad}${pad}${q(p)},`), `${pad}${pad}],`]),
|
|
41
|
+
`${pad}},`,
|
|
42
|
+
`${pad}${IGNORED_END}`,
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function indentOf(lines: string[]): string {
|
|
47
|
+
for (const l of lines) {
|
|
48
|
+
const m = /^([ \t]+)\S/.exec(l)
|
|
49
|
+
if (m != null) return m[1]!
|
|
50
|
+
}
|
|
51
|
+
return ' '
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const validIgnoredInput = (p: unknown): p is string => typeof p === 'string' && p !== '' && !p.startsWith('/') && !p.split('/').includes('..')
|
|
55
|
+
|
|
56
|
+
/** add `add` (task id → paths) to the repo's ignoredInputs, through the
|
|
57
|
+
* managed block; the map after the write */
|
|
58
|
+
export async function addIgnoredInputs(root: string, add: IgnoredInputs): Promise<IgnoredInputs> {
|
|
59
|
+
const file = join(root, 'repo.config.ts')
|
|
60
|
+
if (!existsSync(file)) throw new Error(`no repo.config.ts in ${root} — run: shipkit init`)
|
|
61
|
+
for (const p of Object.values(add).flat()) if (!validIgnoredInput(p)) throw new Error(`ignoredInputs '${String(p)}' must be a repo-relative path inside the repo`)
|
|
62
|
+
const text = readFileSync(file, 'utf8')
|
|
63
|
+
const lines = text.split('\n')
|
|
64
|
+
const c = await loadRepoConfig(root, { fresh: true })
|
|
65
|
+
const have = c.ignoredInputs ?? {}
|
|
66
|
+
const b = lines.findIndex(isBegin)
|
|
67
|
+
const e = lines.findIndex(isEnd)
|
|
68
|
+
if (b === -1 || e === -1 || e < b) {
|
|
69
|
+
if (Object.keys(have).length > 0) throw new Error('repo.config.ts declares `ignoredInputs` outside the managed block — move it between the markers first, or the audit would write a duplicate key')
|
|
70
|
+
} else {
|
|
71
|
+
const pad = /^([ \t]*)/.exec(lines[b]!)?.[1] ?? ''
|
|
72
|
+
if (lines.slice(b, e + 1).join('\n') !== renderIgnoredBlock(have, pad).join('\n'))
|
|
73
|
+
throw new Error('the shipkit ignored inputs block in repo.config.ts no longer matches what the config loads, so it was hand edited — reconcile it by hand, then run the audit fix again')
|
|
74
|
+
}
|
|
75
|
+
const merged: IgnoredInputs = { ...have }
|
|
76
|
+
for (const [t, ps] of Object.entries(add)) merged[t] = [...(merged[t] ?? []), ...ps]
|
|
77
|
+
const next = normalizeIgnored(merged)
|
|
78
|
+
let out: string[]
|
|
79
|
+
if (b !== -1 && e > b) out = [...lines.slice(0, b), ...renderIgnoredBlock(next, /^([ \t]*)/.exec(lines[b]!)?.[1] ?? ''), ...lines.slice(e + 1)]
|
|
80
|
+
else {
|
|
81
|
+
const anchor = lines.findIndex((l) => /defineRepo\(\s*\{\s*$/.test(l))
|
|
82
|
+
if (anchor === -1) throw new Error(`repo.config.ts has no \`defineRepo({\` line to write into — paste this block inside the config object by hand:\n${renderIgnoredBlock(next, indentOf(lines)).join('\n')}`)
|
|
83
|
+
out = [...lines.slice(0, anchor + 1), ...renderIgnoredBlock(next, indentOf(lines)), ...lines.slice(anchor + 1)]
|
|
84
|
+
}
|
|
85
|
+
atomicWrite(file, out.join('\n'))
|
|
86
|
+
try {
|
|
87
|
+
const after = await loadRepoConfig(root, { fresh: true })
|
|
88
|
+
if (JSON.stringify(normalizeIgnored(after.ignoredInputs ?? {})) !== JSON.stringify(next)) throw new Error(`the loader read ignoredInputs ${JSON.stringify(after.ignoredInputs ?? {})}`)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
atomicWrite(file, text)
|
|
91
|
+
throw new Error(`repo.config.ts was left as it was: ${(err as Error).message}`)
|
|
92
|
+
}
|
|
93
|
+
return next
|
|
94
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// IGNORED INPUTS, the pure half: the kept audit's shape and what a task's key
|
|
2
|
+
// takes from it and from `repo.config.ts ignoredInputs`. No fs here, so a
|
|
3
|
+
// check keyed on its reads can import it (core/audit-report.ts reads the file).
|
|
4
|
+
|
|
5
|
+
export type AuditTask = {
|
|
6
|
+
id: string
|
|
7
|
+
/** `audited` when its trace had no hole, else why it could not be */
|
|
8
|
+
status: 'audited' | 'unaudited'
|
|
9
|
+
why?: string
|
|
10
|
+
/** ignored paths it read that its key does not cover */
|
|
11
|
+
uncovered: string[]
|
|
12
|
+
/** the fewest `ignoredInputs` entries that cover them */
|
|
13
|
+
propose: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AuditReport = {
|
|
17
|
+
at: number
|
|
18
|
+
/** why nothing was traced on this machine, when nothing was */
|
|
19
|
+
skipped?: string
|
|
20
|
+
tasks: AuditTask[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** what the kept audit says this task reads outside its key */
|
|
24
|
+
export function auditedExtras(r: AuditReport | null, id: string): string[] {
|
|
25
|
+
if (r == null || r.skipped != null) return []
|
|
26
|
+
return r.tasks.find((x) => x.id === id)?.propose ?? []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** the audit's entries that `have` does not carry yet, per task */
|
|
30
|
+
export function pendingByTask(r: AuditReport | null, have: IgnoredInputs): IgnoredInputs {
|
|
31
|
+
const out: IgnoredInputs = {}
|
|
32
|
+
if (r == null || r.skipped != null) return out
|
|
33
|
+
for (const t of r.tasks) {
|
|
34
|
+
const missing = (t.propose ?? []).filter((p) => !(have['*'] ?? []).includes(p) && !(have[t.id] ?? []).includes(p))
|
|
35
|
+
if (missing.length > 0) out[t.id] = missing
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `ignoredInputs`: task id → gitignored paths it reads, `'*'` for every task */
|
|
41
|
+
export type IgnoredInputs = Record<string, string[]>
|
|
42
|
+
|
|
43
|
+
/** the entries that join one task's key */
|
|
44
|
+
export const ignoredFor = (ii: IgnoredInputs | undefined, id: string): string[] => [...(ii?.['*'] ?? []), ...(ii?.[id] ?? [])]
|
|
45
|
+
|
|
46
|
+
/** `task: a, b; other: c`, for a line */
|
|
47
|
+
export const describeByTask = (by: IgnoredInputs): string => Object.entries(by).map(([t, ps]) => `${t}: ${ps.join(', ')}`).join('; ')
|
package/src/core/jobs.ts
CHANGED
|
@@ -58,7 +58,14 @@ let seq = 0
|
|
|
58
58
|
/** what a job the daemon starts carries beside its flags: the run's options
|
|
59
59
|
* as the runner takes them, one lib copy for the repo config, and the
|
|
60
60
|
* machine's one background turn for a run nobody waits on */
|
|
61
|
-
export type JobOpts = {
|
|
61
|
+
export type JobOpts = {
|
|
62
|
+
opts: Record<string, unknown>
|
|
63
|
+
oneLib?: boolean
|
|
64
|
+
turn?: boolean
|
|
65
|
+
/** the switches the asking terminal had set (home/via-hub.ts): the job
|
|
66
|
+
* starts from the daemon's env, which never saw them */
|
|
67
|
+
env?: Record<string, string>
|
|
68
|
+
}
|
|
62
69
|
|
|
63
70
|
/** start `shipkit <kind> <args> --job <id>` detached from this process and
|
|
64
71
|
* its terminal, and return at once */
|
|
@@ -69,7 +76,7 @@ export function startJob(root: string, o: { cli: string; kind: JobKind; args: st
|
|
|
69
76
|
mkdirSync(dir, { recursive: true })
|
|
70
77
|
if (o.job != null) atomicWrite(jobOpts(root, id), JSON.stringify(o.job))
|
|
71
78
|
const out = openSync(join(dir, 'out.log'), 'a')
|
|
72
|
-
const child = spawn(process.execPath, [o.cli, o.kind, ...o.args, '--job', id], { cwd: root, detached: true, stdio: ['ignore', out, out], env: process.env })
|
|
79
|
+
const child = spawn(process.execPath, [o.cli, o.kind, ...o.args, '--job', id], { cwd: root, detached: true, stdio: ['ignore', out, out], env: { ...process.env, ...o.job?.env } })
|
|
73
80
|
closeSync(out)
|
|
74
81
|
child.unref()
|
|
75
82
|
const pid = child.pid ?? 0
|
|
@@ -22,6 +22,7 @@ import { fallbackNames, isCheckFilePath, readLegacyCheck, type LegacyCheck } fro
|
|
|
22
22
|
import type { ScanCtx } from './check-ctx.ts'
|
|
23
23
|
import type { RepoConfig } from '../repo-config.ts'
|
|
24
24
|
import { descriptionProblem, type TaskDef } from '../check.ts'
|
|
25
|
+
import { deriveExplicit, type Derived } from './port-check-file.ts'
|
|
25
26
|
|
|
26
27
|
/** the narrow module every rewritten file imports `check` from, and the ONE
|
|
27
28
|
* spelling: the barrel costs 400ms of module load and discovery imports
|
|
@@ -130,16 +131,34 @@ export const PLACEHOLDER_MARK = 'TODO: state what must be true'
|
|
|
130
131
|
* drops none of them. */
|
|
131
132
|
export function scriptPort(read: LegacyCheck, fallbackBlocking: boolean | undefined): string {
|
|
132
133
|
const blocking = read.blocking ?? fallbackBlocking
|
|
134
|
+
// what the ported `run` body resolves to: the script's own declarations,
|
|
135
|
+
// read as the gate read them, on a body keyed on what it reads
|
|
136
|
+
const d = explicitOf(deriveExplicit({
|
|
137
|
+
when: read.gate ? null : 'health',
|
|
138
|
+
build: false,
|
|
139
|
+
body: 'run',
|
|
140
|
+
...(read.inputs != null ? { inputs: `[${read.inputs.map(q).join(', ')}]` } : {}),
|
|
141
|
+
...(read.ttl != null ? { ttl: q(read.ttl) } : {}),
|
|
142
|
+
...(read.never != null ? { never: q(read.never) } : {}),
|
|
143
|
+
repair: false,
|
|
144
|
+
rawAccess: null,
|
|
145
|
+
}, q))
|
|
133
146
|
const fields = [
|
|
134
147
|
blocking == null ? 'blocking: <true | false, it declares none>' : `blocking: ${blocking}`,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
`runs: ${d.runs}`,
|
|
149
|
+
`cache: ${d.cache}`,
|
|
150
|
+
`fix: ${q(d.fix)}`,
|
|
151
|
+
`applies: ${d.applies}`,
|
|
139
152
|
]
|
|
140
153
|
return `a check script, run as its own process: port it by hand into \`export default check({ description, ${fields.join(', ')}, run: (c) => … })\`, each print a call on c (c.ok, c.warn, c.fail)`
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
/** a derivation that cannot fail on what a script or a task can declare */
|
|
157
|
+
function explicitOf(d: Derived | { error: string }): Derived {
|
|
158
|
+
if ('error' in d) throw new Error(`the legacy declaration has no explicit form: ${d.error}`)
|
|
159
|
+
return d
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
/** the check file a task's argv runs through bun, `bun <file>` or `bun run
|
|
144
163
|
* <file>`, relative, `./` prefixed or absolute, resolved against the task's
|
|
145
164
|
* cwd. `rest` is what the argv passes after the file: a task that passes its
|
|
@@ -159,16 +178,24 @@ export function checkFileRunBy(t: Pick<TaskDef, 'cmd' | 'cwd'>, root: string): {
|
|
|
159
178
|
* always was: the gate runs it and the health pass leaves it out. */
|
|
160
179
|
export function taskCheckSource(t: TaskDef, root: string): string {
|
|
161
180
|
const own = t.description != null && descriptionProblem(t.description) == null ? t.description.trim() : null
|
|
162
|
-
|
|
181
|
+
// where it ran and how it was kept, as the gate and the planner read a
|
|
182
|
+
// task: an optIn one ran only by name
|
|
183
|
+
const d = explicitOf(deriveExplicit({
|
|
184
|
+
when: t.optIn === true ? 'health' : null,
|
|
185
|
+
build: true,
|
|
186
|
+
body: 'cmd',
|
|
187
|
+
...(t.inputs != null ? { inputs: `[${t.inputs.map(q).join(', ')}]` } : {}),
|
|
188
|
+
...(t.ttl != null ? { ttl: q(t.ttl) } : {}),
|
|
189
|
+
...(t.never != null ? { never: q(t.never) } : {}),
|
|
190
|
+
repair: false,
|
|
191
|
+
rawAccess: null,
|
|
192
|
+
}, q))
|
|
193
|
+
const fields = [`id: ${q(t.id)},`, `description: ${q(own ?? descriptionPlaceholder())},`, 'blocking: true,', 'build: true,', `runs: ${d.runs},`, `cache: ${d.cache},`, `fix: ${q(d.fix)},`, `applies: ${d.applies},`, `cmd: [${t.cmd!.map(q).join(', ')}],`]
|
|
163
194
|
if (t.deps != null && t.deps.length > 0) fields.push(`deps: [${t.deps.map(q).join(', ')}],`)
|
|
164
|
-
if (t.inputs != null) fields.push(`inputs: [${t.inputs.map(q).join(', ')}],`)
|
|
165
|
-
if (t.ttl != null) fields.push(`ttl: ${q(t.ttl)},`)
|
|
166
|
-
if (t.never != null) fields.push(`never: ${q(t.never)},`)
|
|
167
195
|
if (t.group != null) fields.push(`group: ${q(t.group)},`)
|
|
168
196
|
if (t.timeoutMs != null) fields.push(`timeoutMs: ${t.timeoutMs},`)
|
|
169
197
|
if (t.env != null) fields.push(`env: { ${Object.entries(t.env).map(([k, v]) => `${q(k)}: ${q(v)}`).join(', ')} },`)
|
|
170
198
|
if (t.cwd != null) fields.push(`cwd: ${q(t.cwd)},`)
|
|
171
|
-
if (t.optIn === true) fields.push(`when: 'health',`)
|
|
172
199
|
const w = checkWrapper(root)
|
|
173
200
|
return [...w.head, w.open, ...fields.map((f) => ` ${f}`), w.close, ''].join('\n')
|
|
174
201
|
}
|
|
@@ -189,6 +216,8 @@ export function dropConfigKey(text: string, key: string): string | null {
|
|
|
189
216
|
const start = at[0]!
|
|
190
217
|
const end = statementEnd(lines, start)
|
|
191
218
|
let from = start
|
|
192
|
-
|
|
219
|
+
// the comment block above the key goes with it, never a managed block
|
|
220
|
+
// marker (`// >>> shipkit …`): the key may be the first line inside one
|
|
221
|
+
while (from > 0 && lines[from - 1]!.trim().startsWith('//') && !/^\/\/ (>>>|<<<) shipkit/.test(lines[from - 1]!.trim())) from--
|
|
193
222
|
return [...lines.slice(0, from), ...lines.slice(end + 1)].join('\n')
|
|
194
223
|
}
|
package/src/core/one-lib.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
// (the home) makes every `rvlib-shipkit` import, from any repo config or check
|
|
3
3
|
// file, land on its OWN copy. A repo's install is never loaded, so there is one
|
|
4
4
|
// module instance per lib file: one mobx, one class identity, one version.
|
|
5
|
+
// The same holds for a package whose objects cross between a repo file and
|
|
6
|
+
// the toolkit (SHARED): a repo's brand draws on a canvas the toolkit made,
|
|
7
|
+
// and a native class only accepts instances of its own copy.
|
|
5
8
|
//
|
|
6
9
|
// the one constraint a reader breaks first: register it BEFORE the first repo
|
|
7
10
|
// config is imported. A module already loaded keeps the copy it resolved.
|
|
8
|
-
import { readFileSync, realpathSync } from 'fs'
|
|
9
|
-
import { extname, join } from 'path'
|
|
11
|
+
import { existsSync, readFileSync, realpathSync } from 'fs'
|
|
12
|
+
import { dirname, extname, join } from 'path'
|
|
10
13
|
import { LIB_ROOT } from './lib-root.ts'
|
|
11
14
|
|
|
12
15
|
const NAME = 'rvlib-shipkit'
|
|
@@ -54,6 +57,46 @@ const real = (p: string): string => {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
/** packages a repo file and the toolkit hand objects of to each other */
|
|
61
|
+
export const SHARED: readonly string[] = ['@napi-rs/canvas', 'comfy-ts']
|
|
62
|
+
|
|
63
|
+
/** a file of ANOTHER copy of a shared package, as the path inside that copy,
|
|
64
|
+
* else null. `host` is the root of the copy this process loads */
|
|
65
|
+
export function foreignSharedPath(path: string, name: string, host: string | null): string | null {
|
|
66
|
+
const at = path.lastIndexOf(`/node_modules/${name}/`)
|
|
67
|
+
if (at === -1) return null
|
|
68
|
+
const root = path.slice(0, at + `/node_modules/${name}`.length)
|
|
69
|
+
if (host == null || root === host || real(root) === real(host)) return null
|
|
70
|
+
return path.slice(root.length + 1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hostRoot = (name: string, from: string): string | null => {
|
|
74
|
+
try {
|
|
75
|
+
return dirname(Bun.resolveSync(`${name}/package.json`, from))
|
|
76
|
+
} catch {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** where a foreign copy's file lands in the host copy: its entry on the
|
|
82
|
+
* host's entry (the two can name different files), any other file on the
|
|
83
|
+
* same path, else on the entry */
|
|
84
|
+
const sharedTarget = (path: string, sub: string, name: string, host: string): string => {
|
|
85
|
+
const entry = Bun.resolveSync(name, host)
|
|
86
|
+
const foreignDir = path.slice(0, path.length - sub.length - 1)
|
|
87
|
+
try {
|
|
88
|
+
if (real(Bun.resolveSync(name, foreignDir)) === real(path)) return entry
|
|
89
|
+
} catch {
|
|
90
|
+
/* a foreign copy with no entry of its own: its files map by path */
|
|
91
|
+
}
|
|
92
|
+
return existsSync(join(host, sub)) ? join(host, sub) : entry
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const reexport = (target: string): { contents: string; loader: 'ts' } => {
|
|
96
|
+
const t = JSON.stringify(target)
|
|
97
|
+
return { contents: `export * from ${t}\nimport * as own from ${t}\nexport default (own as { default?: unknown }).default\n`, loader: 'ts' }
|
|
98
|
+
}
|
|
99
|
+
|
|
57
100
|
const LOADERS: Record<string, 'ts' | 'tsx' | 'js' | 'jsx'> = { '.ts': 'ts', '.tsx': 'tsx', '.js': 'js', '.mjs': 'js', '.jsx': 'jsx' }
|
|
58
101
|
|
|
59
102
|
let installed = false
|
|
@@ -77,9 +120,21 @@ export function useOneLib(root: string = LIB_ROOT): void {
|
|
|
77
120
|
const loader = LOADERS[extname(a.path)] ?? 'ts'
|
|
78
121
|
const sub = foreignLibPath(a.path, root)
|
|
79
122
|
if (sub == null) return { contents: readFileSync(a.path, 'utf8'), loader }
|
|
80
|
-
|
|
81
|
-
return { contents: `export * from ${target}\nimport * as own from ${target}\nexport default (own as { default?: unknown }).default\n`, loader: 'ts' }
|
|
123
|
+
return reexport(join(root, sub))
|
|
82
124
|
})
|
|
125
|
+
for (const name of SHARED) {
|
|
126
|
+
const host = hostRoot(name, root)
|
|
127
|
+
if (host == null) continue
|
|
128
|
+
// the host copy never matches: a CommonJS file read back here
|
|
129
|
+
// loses the named exports bun infers for it
|
|
130
|
+
const esc = (s: string): string => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')
|
|
131
|
+
const skip = [...new Set([host, real(host)])].map((h) => `(?!${esc(h)}/)`).join('')
|
|
132
|
+
build.onLoad({ filter: new RegExp(`^${skip}.*/node_modules/${esc(name)}/`) }, (a) => {
|
|
133
|
+
const sub = foreignSharedPath(a.path, name, host)
|
|
134
|
+
if (sub == null) return { contents: readFileSync(a.path, 'utf8'), loader: LOADERS[extname(a.path)] ?? 'js' }
|
|
135
|
+
return reexport(sharedTarget(a.path, sub, name, host))
|
|
136
|
+
})
|
|
137
|
+
}
|
|
83
138
|
},
|
|
84
139
|
})
|
|
85
140
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ONE WAY IN, the harness half (build/shipkit.md). The package.json half
|
|
1
|
+
// ONE WAY IN, the harness half (build/shipkit/one-way-in.md). The package.json half
|
|
2
2
|
// (rule SK001) routes `bun run test` into shipkit, but `bun test` is a bun
|
|
3
3
|
// BUILTIN: no script can intercept it, so a package.json alone leaves the
|
|
4
4
|
// reflex wide open. Claude Code permission rules can close it.
|
|
@@ -76,7 +76,7 @@ export const denyRules = (exemptSlots: Iterable<string> = []): string[] => {
|
|
|
76
76
|
export function refusal(r: RawRunner, segment?: string): string {
|
|
77
77
|
const alt = r.iterate == null ? '' : `, or \`${r.iterate}\` to iterate on ONE suite while you fix a bug`
|
|
78
78
|
const which = segment == null || segment.trim() === '' || segment.trim() === r.head ? '' : ` (refused on \`${segment.trim().slice(0, 80)}\`)`
|
|
79
|
-
return `\`${r.head}\`${which} bypasses shipkit, so nothing is cached and no other agent's run can be attached to. Use \`${r.use}\`${alt}. Lifting this for good: delete the "Bash(${r.head}*)" line from .claude/settings.json permissions.deny (build/shipkit.md
|
|
79
|
+
return `\`${r.head}\`${which} bypasses shipkit, so nothing is cached and no other agent's run can be attached to. Use \`${r.use}\`${alt}. Lifting this for good: delete the "Bash(${r.head}*)" line from .claude/settings.json permissions.deny (build/shipkit/one-way-in.md).`
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/** shell wrappers that do not change what is being run */
|
|
@@ -38,8 +38,24 @@ export const checkVerdictWords = (status: 'pass' | 'warn' | 'fail' | 'skip' | nu
|
|
|
38
38
|
: status === 'skip' ? 'skipped'
|
|
39
39
|
: status === 'pass' ? (o.replayed ? `passed, ${CACHED}` : 'passed') : 'never ran here'
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
export
|
|
41
|
+
/** a span in its one biggest unit: `45s`, `12m`, `3h`, `2d` */
|
|
42
|
+
export function spanShort(ms: number): string {
|
|
43
|
+
const s = Math.max(0, Math.round(ms / 1000))
|
|
44
|
+
if (s < 60) return `${s}s`
|
|
45
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`
|
|
46
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`
|
|
47
|
+
return `${Math.floor(s / 86400)}d`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** what a run cost: `12ms` under a second, `6.4s` past it */
|
|
51
|
+
export const costShort = (ms: number): string => (ms < 1000 ? `${Math.max(0, Math.round(ms))}ms` : `${(ms / 1000).toFixed(1)}s`)
|
|
52
|
+
|
|
53
|
+
/** the stamp on the first line of a cached check: how old the answer is and
|
|
54
|
+
* what measuring it cost, `(cached, 3h old, 5.6s saved)` */
|
|
55
|
+
export const cachedStamp = (savedMs: number, ageMs: number): string => `(${CACHED}, ${spanShort(ageMs)} old, ${costShort(savedMs)} saved)`
|
|
56
|
+
|
|
57
|
+
/** the stamp on the first line of a check that ran in this run: `(ran 6.4s)` */
|
|
58
|
+
export const ranStamp = (ms: number): string => `(${RAN} ${costShort(ms)})`
|
|
43
59
|
|
|
44
60
|
/** the stamp on the first line of an attached check */
|
|
45
61
|
export const attachedStamp = (pid: number, ranMs: number): string => `(${ATTACHED} to pid ${pid}, ${RAN} there in ${(ranMs / 1000).toFixed(1)}s)`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// THE WRITE HALF of the check file port (core/port-check-file.ts), in its own
|
|
2
|
+
// process: whether an undeclared `run` body can be keyed on its reads is a walk
|
|
3
|
+
// of its import closure on disk (core/raw-access.ts), which no check context
|
|
4
|
+
// read can stand for, so SK029's repair reaches it through `c.exec`.
|
|
5
|
+
//
|
|
6
|
+
// bun port-check-cli.ts <root> <rel> [<rel> …] → one json line on stdout
|
|
7
|
+
import { readFileSync } from 'fs'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
import { atomicWrite } from './locked-write.ts'
|
|
10
|
+
import { LIB_ROOT } from './lib-root.ts'
|
|
11
|
+
import { rawAccessInClosure } from './raw-access.ts'
|
|
12
|
+
import { findCheckObject, portCheckSource, retiredKeysIn } from './port-check-file.ts'
|
|
13
|
+
import { plainDefaultFor } from './port-check-files.ts'
|
|
14
|
+
|
|
15
|
+
export type PortRun = { did: string[]; hand: string[] }
|
|
16
|
+
|
|
17
|
+
/** why an undeclared `run` body cannot be keyed on its reads, or null */
|
|
18
|
+
function rawAccessOf(root: string, rel: string, src: string): string | null {
|
|
19
|
+
const obj = findCheckObject(src, { plainDefault: plainDefaultFor(rel) })
|
|
20
|
+
const keys = new Set(obj?.props.map((p) => p.key) ?? [])
|
|
21
|
+
if (!keys.has('run') || keys.has('inputs') || keys.has('ttl') || keys.has('never')) return null
|
|
22
|
+
const r = rawAccessInClosure(root, join(root, rel), { skip: LIB_ROOT })
|
|
23
|
+
return r == null ? null : `its imports reach the disk around c (${r.file}:${r.line}, ${r.what})`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function portFiles(root: string, rels: string[]): PortRun {
|
|
27
|
+
const did: string[] = []
|
|
28
|
+
const hand: string[] = []
|
|
29
|
+
for (const rel of rels) {
|
|
30
|
+
const abs = join(root, rel)
|
|
31
|
+
let src: string
|
|
32
|
+
try { src = readFileSync(abs, 'utf8') } catch (e) { hand.push(`${rel}: unreadable (${(e as Error).message})`); continue }
|
|
33
|
+
const plainDefault = plainDefaultFor(rel)
|
|
34
|
+
const keys = retiredKeysIn(src, { plainDefault })
|
|
35
|
+
if (keys.length === 0) continue
|
|
36
|
+
const r = portCheckSource(src, { plainDefault, rawAccess: rawAccessOf(root, rel, src) })
|
|
37
|
+
if ('error' in r) { hand.push(`${rel}: ${r.error}`); continue }
|
|
38
|
+
if (!r.changed) continue
|
|
39
|
+
atomicWrite(abs, r.source)
|
|
40
|
+
did.push(`${rel} declares runs, cache, fix and applies (was: ${keys.join(', ')})${r.notes.length > 0 ? `. Note: ${r.notes.join('; ')}` : ''}`)
|
|
41
|
+
}
|
|
42
|
+
return { did, hand }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (import.meta.main) {
|
|
46
|
+
const [root, ...rels] = process.argv.slice(2)
|
|
47
|
+
if (root == null) {
|
|
48
|
+
console.error('usage: bun port-check-cli.ts <root> <rel> [<rel> …]')
|
|
49
|
+
process.exit(2)
|
|
50
|
+
}
|
|
51
|
+
console.log(JSON.stringify(portFiles(root, rels)))
|
|
52
|
+
}
|