rvlib-shipkit 0.38.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +367 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// SK004 — the .gitignore policy block (meta/contracts.md git policy;
|
|
2
|
+
// .shipkit/tmp/ spelled out because shipkit writes its per-run logs there in
|
|
3
|
+
// every repo). A marker-keyed managed block so the always-gitignored contract
|
|
4
|
+
// surfaces can never silently rot per repo. Ecosystem-specific names the
|
|
5
|
+
// published lib may not carry ride as repo-contributed lines:
|
|
6
|
+
// `defineRepo({ gitignore: [...] })` renders them INSIDE the block, so the
|
|
7
|
+
// whole policy is machine-enforced. A well-formed block is rewritten IN
|
|
8
|
+
// PLACE — lines below it (e.g. a live venue's `!.shipkit/itch/` override, where
|
|
9
|
+
// last-match-wins) keep their position; everything outside the block is
|
|
10
|
+
// never touched. `shipkit init` writes the same block through this fixer.
|
|
11
|
+
//
|
|
12
|
+
// It reads and writes `.gitignore` through `c`, and asks git through `c.exec`.
|
|
13
|
+
// It still declares the WHOLE TREE as its input: what git ignores and tracks
|
|
14
|
+
// depends on the index, `.git/info/exclude` and the user's global excludes
|
|
15
|
+
// file, which no read set records exactly, and a body that asked git through
|
|
16
|
+
// `c.exec` is never stored on its reads.
|
|
17
|
+
import { readdirSync } from 'fs'
|
|
18
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
19
|
+
import { GENERATED_IGNORE_LINES, GENERATED_TRACKED_PATHS, untrackGenerated } from '../rules/untrack-generated.ts'
|
|
20
|
+
import { check, type CheckCtx, type FixResult } from '../check.ts'
|
|
21
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
22
|
+
import { LAYOUT, SHIPKIT_DIR, TRACKED_DIRS } from '../core/layout.ts'
|
|
23
|
+
import { LEGACY_CONTRACT_PREFIX, LEGACY_IGNORED, LEGACY_TMP, LEGACY_VENUES, legacyLayoutLeft, migrateLayout } from '../core/migrate-layout.ts'
|
|
24
|
+
|
|
25
|
+
/** what this check reads, writes and asks git through */
|
|
26
|
+
type Disk = Pick<CheckCtx, 'root' | 'read' | 'write' | 'exists' | 'exec'>
|
|
27
|
+
|
|
28
|
+
const BEGIN = '# >>> shipkit gitignore policy (SK004) — managed block, edit outside only'
|
|
29
|
+
const END = '# <<< shipkit gitignore policy'
|
|
30
|
+
const CONTRIB = '# repo-contributed lines (repo.config.ts gitignore:)'
|
|
31
|
+
|
|
32
|
+
export const POLICY_LINES = [
|
|
33
|
+
// everything shipkit keeps lives under .shipkit/ (core/layout.ts). Local
|
|
34
|
+
// by default: the private notes, drafts, venue listings and caches, and the
|
|
35
|
+
// leak guard's keyword list, which cannot itself be committable. Only the
|
|
36
|
+
// authored folders come back in. A live venue re-includes its own folder
|
|
37
|
+
// below the block, `!.shipkit/itch/`, where the last match wins
|
|
38
|
+
`${SHIPKIT_DIR}/*`,
|
|
39
|
+
...TRACKED_DIRS.map((d) => `!${d}/`),
|
|
40
|
+
`${LAYOUT.assets}/out/`,
|
|
41
|
+
// already covered by the line above, and spelled out anyway so this check
|
|
42
|
+
// asks git about it: a negation below the block that exposed the keyword
|
|
43
|
+
// list would otherwise pass as a venue override
|
|
44
|
+
`${LAYOUT.private}/`,
|
|
45
|
+
// the scratch and the review files, spelled out for the same reason: a
|
|
46
|
+
// review narrates agent process, and a plan names what nobody published
|
|
47
|
+
`${LAYOUT.tmp}/`,
|
|
48
|
+
`${LAYOUT.reviews}/`,
|
|
49
|
+
// a root scratch stays out of git too: the fix pass moves it under
|
|
50
|
+
// .shipkit/, and until it runs, a plan there is still private
|
|
51
|
+
`${LEGACY_TMP}/`,
|
|
52
|
+
'CLAUDE.local.md',
|
|
53
|
+
// atomicWrite lands every file via `.<name>.tmp-<pid>` next to its target,
|
|
54
|
+
// so a run killed mid write leaves one behind ANYWHERE in the tree, and an
|
|
55
|
+
// agent finds untracked debris it did not create. The ignore row ships in
|
|
56
|
+
// the same policy that started writing them.
|
|
57
|
+
'.*.tmp-*',
|
|
58
|
+
// comfy-ts state and per-workflow lora tags name a private model collection;
|
|
59
|
+
// shipkit itself writes .comfy-ts/ at hub boot, so every consumer repo needs
|
|
60
|
+
// these ignored before its first commit. The DRAFTS are re-included on
|
|
61
|
+
// purpose: a draft is the tuning behind a whole KIND of asset, its loras,
|
|
62
|
+
// steps and cfg, so it is authored state and belongs in the repo, unlike the
|
|
63
|
+
// schema dump, the lora previews and the raw run outputs beside it
|
|
64
|
+
'.comfy-ts/*',
|
|
65
|
+
'!.comfy-ts/drafts/',
|
|
66
|
+
'*.loras.local.txt',
|
|
67
|
+
// shipkit's own per-run output. Ignoring is only half of it: the fixer
|
|
68
|
+
// also removes these from the index, because .gitignore does nothing to a
|
|
69
|
+
// file already tracked (untrack-generated.ts owns the pair)
|
|
70
|
+
...GENERATED_IGNORE_LINES,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const LEGACY = '# the old root layout, ignored until the fix pass moves it under .shipkit/'
|
|
74
|
+
|
|
75
|
+
/** `legacy`: the repo still carries contract folders at its root from before `.shipkit/`. Their names
|
|
76
|
+
* stay ignored until they move, so no private file is ever exposed by a block
|
|
77
|
+
* that is newer than the tree it guards */
|
|
78
|
+
export function expectedBlock(config: RepoConfig, legacy = false): string {
|
|
79
|
+
const extra = config.gitignore ?? []
|
|
80
|
+
return [BEGIN, ...POLICY_LINES, ...(legacy ? [LEGACY, ...LEGACY_IGNORED] : []), ...(extra.length > 0 ? [CONTRIB, ...extra] : []), END].join('\n')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function policyPatterns(config: RepoConfig, legacy = false): string[] {
|
|
84
|
+
return [...POLICY_LINES.filter((l) => !l.startsWith('#')), ...(legacy ? LEGACY_IGNORED : []), ...(config.gitignore ?? [])]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** does the root still hold a contract folder from before `.shipkit/` */
|
|
88
|
+
function hasLegacy(root: string): boolean {
|
|
89
|
+
try { return readdirSync(root).some((e) => e.startsWith(LEGACY_CONTRACT_PREFIX)) } catch { return false }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** the .gitignore text with its policy block rendered, every repo line kept
|
|
93
|
+
* where it was. The layout migration writes it before it moves a single
|
|
94
|
+
* folder, with `legacy` on while an old folder is still on disk */
|
|
95
|
+
export const renderPolicy = (text: string, config: RepoConfig, legacy = false): string => withBlock(text, config, legacy)
|
|
96
|
+
|
|
97
|
+
function endsWithNewline(d: Disk): boolean {
|
|
98
|
+
const text = d.read('.gitignore')
|
|
99
|
+
return text == null || text === '' || text.endsWith('\n')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// a venue folder is DESIGNED to be unignored below the block once a listing
|
|
103
|
+
// is live (last-match-wins), so an override on one of them is the mechanism
|
|
104
|
+
// working. Every other policy path is a claim the block makes, and a claim git
|
|
105
|
+
// contradicts is a bug.
|
|
106
|
+
const OVERRIDABLE = new Set(LEGACY_VENUES)
|
|
107
|
+
|
|
108
|
+
/** a path git can be asked about, for a pattern that may be a glob or a dir */
|
|
109
|
+
function probePath(pattern: string): string {
|
|
110
|
+
if (pattern.endsWith('/')) return `${pattern}probe`
|
|
111
|
+
// `.*.tmp-*` and `*.loras.local.txt`: a literal standing in for the glob
|
|
112
|
+
return pattern.includes('*') ? pattern.replace(/\*/g, 'probe') : pattern
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** which of `paths` git does NOT ignore. null when this is not a git repo, so
|
|
116
|
+
* a rule can say it did not measure rather than report a green it invented */
|
|
117
|
+
export async function notIgnored(d: Disk, paths: string[]): Promise<string[] | null> {
|
|
118
|
+
if (paths.length === 0) return []
|
|
119
|
+
// -n -v: one call, every path, and `-n` makes git print a row for the
|
|
120
|
+
// MISSES too, which is the answer we want. `-z` needs `--stdin`, and the
|
|
121
|
+
// probes are plain ascii paths of our own, so the text rows parse exactly
|
|
122
|
+
const r = await d.exec([gitBin(), 'check-ignore', '-n', '-v', '--', ...paths], { cwd: d.root })
|
|
123
|
+
// exit 1 means "none of them are ignored", which is an ANSWER, and git
|
|
124
|
+
// still wrote the rows we parse. Anything else (no repo, no git) is not
|
|
125
|
+
if (r.code !== 0 && r.code !== 1) return null
|
|
126
|
+
// a row is `source:line:pattern<TAB>path`, and an unignored path has all
|
|
127
|
+
// three empty: `::<TAB>path`
|
|
128
|
+
const miss: string[] = []
|
|
129
|
+
for (const row of r.out.split('\n')) {
|
|
130
|
+
const tab = row.indexOf('\t')
|
|
131
|
+
if (tab > 0 && row.slice(0, tab) === '::') miss.push(row.slice(tab + 1))
|
|
132
|
+
}
|
|
133
|
+
return miss
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** every declared generated path this repo still carries in its index, in
|
|
137
|
+
* ONE git spawn (rules/untrack-generated.ts owns the list) */
|
|
138
|
+
async function trackedGenerated(d: Disk): Promise<string[]> {
|
|
139
|
+
if (!d.exists('.git')) return []
|
|
140
|
+
const r = await d.exec([gitBin(), 'ls-files', '-z', '--', ...GENERATED_TRACKED_PATHS], { cwd: d.root })
|
|
141
|
+
if (r.code !== 0) return []
|
|
142
|
+
const listed = new Set(r.out.split('\0').filter((l) => l !== ''))
|
|
143
|
+
return GENERATED_TRACKED_PATHS.filter((p) => listed.has(p))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
type Parsed = { text: string; crlf: boolean; lines: string[]; b: number; e: number }
|
|
147
|
+
|
|
148
|
+
function parse(d: Disk): Parsed | null {
|
|
149
|
+
return parseText(d.read('.gitignore'))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseText(text: string | null): Parsed | null {
|
|
153
|
+
if (text == null) return null
|
|
154
|
+
const crlf = text.includes('\r\n')
|
|
155
|
+
const lines = text.split('\n').map((l) => l.replace(/\r$/, ''))
|
|
156
|
+
return { text, crlf, lines, b: lines.indexOf(BEGIN), e: lines.indexOf(END) }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type BlockState = 'missing-file' | 'missing-block' | 'corrupted' | 'stale' | 'current'
|
|
160
|
+
|
|
161
|
+
function blockState(d: Disk, config: RepoConfig, legacy: boolean): BlockState {
|
|
162
|
+
const f = parse(d)
|
|
163
|
+
if (f == null) return 'missing-file'
|
|
164
|
+
if (f.b === -1 && f.e === -1) return 'missing-block'
|
|
165
|
+
if (f.b === -1 || f.e === -1 || f.e < f.b) return 'corrupted'
|
|
166
|
+
return f.lines.slice(f.b, f.e + 1).join('\n') === expectedBlock(config, legacy) ? 'current' : 'stale'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writeBlock(d: Disk, config: RepoConfig, legacy: boolean): void {
|
|
170
|
+
d.write('.gitignore', withBlock(d.read('.gitignore'), config, legacy))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** PURE: the .gitignore text with the current block in it */
|
|
174
|
+
function withBlock(text: string | null, config: RepoConfig, legacy: boolean): string {
|
|
175
|
+
const f = parseText(text)
|
|
176
|
+
const block = expectedBlock(config, legacy).split('\n')
|
|
177
|
+
let out: string[]
|
|
178
|
+
if (f == null) out = block
|
|
179
|
+
else if (f.b !== -1 && f.e !== -1 && f.e >= f.b)
|
|
180
|
+
// well-formed block: rewrite IN PLACE so below-block overrides
|
|
181
|
+
// (last-match-wins) keep working
|
|
182
|
+
out = [...f.lines.slice(0, f.b), ...block, ...f.lines.slice(f.e + 1)]
|
|
183
|
+
else {
|
|
184
|
+
// absent or corrupted: drop stray markers, keep every repo line
|
|
185
|
+
// visible, append the canonical block
|
|
186
|
+
const kept = (f.b === -1 && f.e === -1 ? f.lines : f.lines.filter((l) => l !== BEGIN && l !== END))
|
|
187
|
+
while (kept.length > 0 && kept.at(-1)!.trim() === '') kept.pop()
|
|
188
|
+
out = kept.length > 0 ? [...kept, '', ...block] : block
|
|
189
|
+
}
|
|
190
|
+
const eol = f?.crlf === true ? '\r\n' : '\n'
|
|
191
|
+
return `${out.join(eol)}${out.at(-1) === '' ? '' : eol}`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const gitignorePolicy = check({
|
|
195
|
+
id: 'SK004',
|
|
196
|
+
blocking: true,
|
|
197
|
+
title: '.gitignore',
|
|
198
|
+
description: 'the managed .gitignore block keeps private contract folders out of git',
|
|
199
|
+
owner: 'meta/contracts.md',
|
|
200
|
+
// .gitignore plus what git currently TRACKS and IGNORES, which the tree
|
|
201
|
+
// carries and no read set records (the header says why)
|
|
202
|
+
inputs: ['.'],
|
|
203
|
+
// it untracks generated files (a commit) unless the run is frozen
|
|
204
|
+
writes: true,
|
|
205
|
+
run: async (c) => {
|
|
206
|
+
// a generated path still in the index is the half-applied state the
|
|
207
|
+
// managed block cannot fix on its own
|
|
208
|
+
const tracked = await trackedGenerated(c)
|
|
209
|
+
if (tracked.length > 0) {
|
|
210
|
+
c.fail(`${tracked.join(', ')} generated on every run but TRACKED — gitignore does not apply to a tracked file, so it dirties every tree`)
|
|
211
|
+
c.lines.push({ kind: 'fix-fail', text: 'shipkit ci (or check --fix SK004) untracks it in one commit, the file stays on disk' })
|
|
212
|
+
}
|
|
213
|
+
// the old root layout is a move a machine makes, so it is red until made
|
|
214
|
+
const left = legacyLayoutLeft(c.root, c.config)
|
|
215
|
+
if (left.length > 0) {
|
|
216
|
+
c.fail(`old layout: ${left.join(' ')} still at the repo root, shipkit keeps them under ${SHIPKIT_DIR}/`)
|
|
217
|
+
c.lines.push({ kind: 'fix-fail', text: 'shipkit ci (or check --fix SK004) moves them and lands one commit' })
|
|
218
|
+
}
|
|
219
|
+
const legacy = hasLegacy(c.root)
|
|
220
|
+
const s = blockState(c, c.config, legacy)
|
|
221
|
+
switch (s) {
|
|
222
|
+
case 'missing-file': return c.fail('.gitignore missing — the contract git policy is unenforced')
|
|
223
|
+
case 'missing-block': return c.fail('.gitignore has no shipkit policy block — contract surfaces can leak into git')
|
|
224
|
+
case 'corrupted': return c.fail('.gitignore policy block corrupted (unpaired marker)')
|
|
225
|
+
case 'stale': return c.fail('.gitignore policy block is stale — the policy or repo-contributed lines moved since it was written')
|
|
226
|
+
}
|
|
227
|
+
// an unterminated last line is a TRAP, not a cosmetic defect: the next
|
|
228
|
+
// `>> .gitignore` fuses onto it and the result is one pattern that
|
|
229
|
+
// ignores neither path, with the file ending in a newline afterwards
|
|
230
|
+
// so nothing textual looks wrong
|
|
231
|
+
if (!endsWithNewline(c)) {
|
|
232
|
+
c.fail('.gitignore does not end with a newline — the next append fuses onto its last line and ignores neither path')
|
|
233
|
+
c.lines.push({ kind: 'fix-fail', text: 'shipkit ci (or check --fix SK004) terminates it' })
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
// the block states what must be ignored. git is the only thing that
|
|
237
|
+
// knows whether it IS: a stray negation below the block, or a line
|
|
238
|
+
// fused into another, leaves the text current over a broken repo
|
|
239
|
+
// a '!' line is the policy RE-INCLUDING a path on purpose, so git
|
|
240
|
+
// reporting it as not ignored is the rule working, not a break. It is
|
|
241
|
+
// a claim all the same, and the opposite one: a repo line BELOW the
|
|
242
|
+
// block re-ignoring the parent leaves the re-inclusion dead while the
|
|
243
|
+
// block text stays current, which is the same silent contradiction
|
|
244
|
+
const patterns = policyPatterns(c.config, legacy)
|
|
245
|
+
const claimed = patterns.filter((p) => !OVERRIDABLE.has(p) && !p.startsWith('!'))
|
|
246
|
+
const probes = new Map(claimed.map((p) => [probePath(p), p]))
|
|
247
|
+
const backProbes = new Map(patterns.filter((p) => p.startsWith('!')).map((p) => [probePath(p.slice(1)), p]))
|
|
248
|
+
const miss = await notIgnored(c, [...probes.keys(), ...backProbes.keys()])
|
|
249
|
+
if (miss == null) {
|
|
250
|
+
// not a git repo (a fixture, or a dir before `git init`). Say the
|
|
251
|
+
// verification did not run rather than reporting a green nothing
|
|
252
|
+
// measured, and stay quiet: there is nothing wrong here to fix
|
|
253
|
+
c.ok(`.gitignore policy block current (${claimed.length} patterns)`)
|
|
254
|
+
c.lines.push({ kind: 'skip', text: 'not a git repo here, so nothing verified the block against what git actually ignores' })
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
const unignored = miss.filter((m) => probes.has(m))
|
|
258
|
+
if (unignored.length > 0) {
|
|
259
|
+
const names = [...new Set(unignored.map((m) => probes.get(m) ?? m))]
|
|
260
|
+
c.fail(`.gitignore block claims ${names.join(', ')} ignored, git says otherwise — a negation below the block, or a line fused into its neighbour`)
|
|
261
|
+
c.lines.push({ kind: 'fix-fail', text: `git check-ignore -v ${unignored[0]} names the line that wins` })
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
const dead = [...backProbes.keys()].filter((p) => !miss.includes(p))
|
|
265
|
+
if (dead.length > 0) {
|
|
266
|
+
const names = [...new Set(dead.map((d) => backProbes.get(d) ?? d))]
|
|
267
|
+
c.fail(`.gitignore block re-includes ${names.join(', ')}, git ignores it anyway — a line below the block re-ignores its parent folder`)
|
|
268
|
+
c.lines.push({ kind: 'fix-fail', text: `git check-ignore -v ${dead[0]} names the line that wins` })
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
c.ok(`.gitignore policy block current, ${claimed.length + backProbes.size} patterns verified against git`)
|
|
272
|
+
},
|
|
273
|
+
fix: async (c): Promise<FixResult> => {
|
|
274
|
+
// the move first: it writes the new block itself, before any folder moves
|
|
275
|
+
const moved = process.env.SHIPKIT_FROZEN === '1' ? null : migrateLayout(c.root, { config: c.config, renderGitignore: (t, legacy) => renderPolicy(t, c.config, legacy) })
|
|
276
|
+
const layout = moved == null ? null
|
|
277
|
+
: moved.deferred != null ? `layout move deferred: ${moved.deferred}`
|
|
278
|
+
: moved.moved.length === 0 && moved.removed.length === 0 ? null
|
|
279
|
+
: `moved ${moved.moved.map((m) => m.from).join(', ')} under ${SHIPKIT_DIR}/${moved.commit != null ? ` (committed as ${moved.commit})` : moved.uncommitted === true ? ', the commit did not land' : ''}`
|
|
280
|
+
const legacy = hasLegacy(c.root)
|
|
281
|
+
const before = blockState(c, c.config, legacy)
|
|
282
|
+
// terminate an unterminated file even when the block itself is current:
|
|
283
|
+
// that missing byte is what the next append fuses onto
|
|
284
|
+
let terminated: string | null = null
|
|
285
|
+
if (before === 'current' && !endsWithNewline(c)) {
|
|
286
|
+
c.write('.gitignore', `${c.read('.gitignore') ?? ''}\n`)
|
|
287
|
+
terminated = '.gitignore terminated with a newline (the next append would have fused onto its last line)'
|
|
288
|
+
}
|
|
289
|
+
const blockSummary = before === 'current'
|
|
290
|
+
? null
|
|
291
|
+
: before === 'missing-file' ? '.gitignore created with the policy block'
|
|
292
|
+
: before === 'missing-block' ? 'policy block appended (repo lines untouched)'
|
|
293
|
+
: before === 'corrupted' ? 'policy block rebuilt (stray markers dropped, repo lines kept visible)'
|
|
294
|
+
: 'policy block rewritten in place (repo lines untouched)'
|
|
295
|
+
if (blockSummary != null) writeBlock(c, c.config, legacy)
|
|
296
|
+
// the block must be written FIRST: untracking a path the ignore list
|
|
297
|
+
// does not yet carry leaves it staged for re-add on the next `git add`.
|
|
298
|
+
// The untrack is a git COMMIT through the hooks, so it stays in the
|
|
299
|
+
// module that owns that migration (rules/untrack-generated.ts)
|
|
300
|
+
const un = untrackGenerated(c.root, { enabled: c.config.untrackGenerated })
|
|
301
|
+
const parts = [layout, blockSummary, terminated]
|
|
302
|
+
if (un.did === 'untracked') parts.push(`untracked ${un.paths.join(', ')} (generated, now gitignored, committed as ${un.commit})`)
|
|
303
|
+
if (un.did === 'refused') parts.push(`left ${(await trackedGenerated(c)).join(', ')} tracked: ${un.why}`)
|
|
304
|
+
const summary = parts.filter((x): x is string => x != null).join(' · ')
|
|
305
|
+
return { changed: layout != null || blockSummary != null || terminated != null || un.did === 'untracked', summary: summary === '' ? 'policy block already current' : summary }
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
export default gitignorePolicy
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// Per rvlib-* dep:
|
|
11
11
|
// link: → the symlink must resolve (a broken link dies cryptically at
|
|
12
12
|
// import time otherwise) — nothing to update, code is live
|
|
13
|
-
// semver → installed vs npm latest; NEWER LIVE → this lint
|
|
14
|
-
//
|
|
13
|
+
// semver → installed vs npm latest; NEWER LIVE → this lint updates the
|
|
14
|
+
// dep itself, loudly, with the manager the repo's own lock files
|
|
15
|
+
// name (src/core/package-manager.ts), guarded:
|
|
15
16
|
// - SHIPKIT_FROZEN=1 (`check --frozen`, `ci --frozen`) → WARN,
|
|
16
17
|
// never touch
|
|
17
18
|
// - package.json/lockfile git-dirty (or no git) → WARN deferred,
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
// - the update itself failing splits by what it LEFT: a half-
|
|
26
27
|
// written package.json/lockfile is this repo and FAILs, an
|
|
27
28
|
// untouched tree is the registry and only WARNs
|
|
29
|
+
// updating an npm consumer with bun is the failure this detection
|
|
30
|
+
// exists for: package.json moves, package-lock.json does not, and
|
|
31
|
+
// the `npm ci` a deploy runs refuses that pair
|
|
28
32
|
// registry unreachable → loud skip, never red (offline is not a defect)
|
|
29
33
|
// Committing the bump stays with the working agent — shipkit never commits.
|
|
30
34
|
// Clean passes cache 24h (rule ttl), so the registry is fetched at most
|
|
@@ -38,7 +42,8 @@ import { existsSync, readFileSync } from 'fs'
|
|
|
38
42
|
import { join } from 'path'
|
|
39
43
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
40
44
|
import { mutateLease, withMutateLease } from '../core/locked-write.ts'
|
|
41
|
-
import
|
|
45
|
+
import { detectPackageManager } from '../core/package-manager.ts'
|
|
46
|
+
import { check, type CheckEnv } from '../check.ts'
|
|
42
47
|
|
|
43
48
|
type Pkg = { version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
44
49
|
|
|
@@ -56,12 +61,15 @@ async function latestVersion(t: CheckCtx, name: string): Promise<string> {
|
|
|
56
61
|
return v
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
export const rvlibVersions
|
|
64
|
+
export const rvlibVersions = check({
|
|
60
65
|
id: 'SK005',
|
|
61
66
|
blocking: true,
|
|
62
67
|
title: 'rvlib versions',
|
|
68
|
+
description: 'the rvlib packages installed here are as new as the published ones',
|
|
63
69
|
owner: 'build/shipkit.md',
|
|
64
70
|
ttl: '24h',
|
|
71
|
+
// it updates the rvlib deps unless the run is frozen
|
|
72
|
+
writes: true,
|
|
65
73
|
cacheKey: (env) => {
|
|
66
74
|
let pkg: Pkg
|
|
67
75
|
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'unreadable' }
|
|
@@ -71,47 +79,51 @@ export const rvlibVersions: Rule = {
|
|
|
71
79
|
.map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}`)
|
|
72
80
|
.join(',')
|
|
73
81
|
},
|
|
74
|
-
|
|
82
|
+
run: async (c) => {
|
|
75
83
|
let pkg: Pkg
|
|
76
|
-
try { pkg = JSON.parse(readFileSync(join(
|
|
84
|
+
try { pkg = JSON.parse(readFileSync(join(c.root, 'package.json'), 'utf8')) as Pkg } catch { return c.note('no readable package.json — rvlib versions not applicable') }
|
|
77
85
|
const deps = Object.entries({ ...pkg.dependencies, ...pkg.devDependencies }).filter(([n]) => n.startsWith('rvlib-'))
|
|
78
|
-
if (deps.length === 0) return
|
|
86
|
+
if (deps.length === 0) return c.ok('no rvlib-* deps')
|
|
87
|
+
const pm = detectPackageManager(c.root)
|
|
88
|
+
const manifest = ['package.json', ...pm.locks]
|
|
79
89
|
|
|
80
90
|
const stale: Array<{ name: string; installed: string; latest: string }> = []
|
|
81
91
|
for (const [name, spec] of deps) {
|
|
82
92
|
if (spec.startsWith('link:')) {
|
|
83
|
-
if (installedVersion(
|
|
84
|
-
else
|
|
93
|
+
if (installedVersion(c.root, name) == null) c.fail(`${name} is link: but the symlink does not resolve — re-run \`bun link ${name}\` (a broken link dies cryptically at import time)`)
|
|
94
|
+
else c.ok(`${name} (linked, live)`)
|
|
85
95
|
continue
|
|
86
96
|
}
|
|
87
|
-
const installed = installedVersion(
|
|
88
|
-
if (installed == null) {
|
|
97
|
+
const installed = installedVersion(c.root, name)
|
|
98
|
+
if (installed == null) { c.fail(`${name} declared (${spec}) but not installed — run: ${pm.install.join(' ')}`); continue }
|
|
89
99
|
let latest: string
|
|
90
|
-
try { latest = await latestVersion(
|
|
91
|
-
|
|
100
|
+
try { latest = await latestVersion(c, name) } catch (e) {
|
|
101
|
+
c.lines.push({ kind: 'skip', text: `${name} ${installed} — registry unreachable (${String(e).slice(0, 80)}), version drift unknown` })
|
|
92
102
|
continue
|
|
93
103
|
}
|
|
94
104
|
// real semver order, not string difference — a dist-tag pointing
|
|
95
105
|
// BEHIND the installed version (prerelease, unpublish window)
|
|
96
106
|
// must never trigger a silent downgrade
|
|
97
107
|
const cmp = Bun.semver.order(latest, installed)
|
|
98
|
-
if (cmp === 0)
|
|
99
|
-
else if (cmp < 0)
|
|
108
|
+
if (cmp === 0) c.ok(`${name} ${installed} (latest)`)
|
|
109
|
+
else if (cmp < 0) c.ok(`${name} ${installed} ahead of npm latest ${latest} (prerelease or dist-tag lag) — not touching`)
|
|
100
110
|
else stale.push({ name, installed, latest })
|
|
101
111
|
}
|
|
102
112
|
if (stale.length === 0) return
|
|
103
113
|
|
|
104
114
|
const drift = stale.map((s) => `${s.name} ${s.installed} → ${s.latest}`).join(', ')
|
|
115
|
+
const names = stale.map((s) => s.name)
|
|
116
|
+
const cmd = pm.update(names)
|
|
117
|
+
if (pm.warn != null) c.warn(pm.warn)
|
|
105
118
|
if (process.env['SHIPKIT_FROZEN'] === '1') {
|
|
106
|
-
|
|
119
|
+
c.warn(`newer rvlib live: ${drift} (frozen — apply with: ${cmd.join(' ')})`)
|
|
107
120
|
return
|
|
108
121
|
}
|
|
109
|
-
const dirty = await
|
|
122
|
+
const dirty = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
|
|
110
123
|
if (dirty.code !== 0 || dirty.out.trim() !== '') {
|
|
111
|
-
|
|
124
|
+
c.warn(`newer rvlib live: ${drift} — self-update deferred (${dirty.code !== 0 ? `git says: ${dirty.out.trim().split('\n')[0] ?? `exit ${dirty.code}`}` : 'package.json/lockfile dirty'}); commit first, then re-run check`)
|
|
112
125
|
return
|
|
113
126
|
}
|
|
114
|
-
const names = stale.map((s) => s.name)
|
|
115
127
|
// this rule WRITES, and three agents in one checkout is the normal
|
|
116
128
|
// case: two `bun update --latest` runs against one node_modules and
|
|
117
129
|
// one lockfile is how a tree ends up half installed. The lease is what
|
|
@@ -119,17 +131,17 @@ export const rvlibVersions: Rule = {
|
|
|
119
131
|
// report only, which would break "machine clearable red clears ITSELF".
|
|
120
132
|
// The dirty probe above is re-run INSIDE the lease, because the holder
|
|
121
133
|
// we waited for may have just done this exact update.
|
|
122
|
-
const leased = await withMutateLease(mutateLease(
|
|
123
|
-
const again = await
|
|
134
|
+
const leased = await withMutateLease(mutateLease(c.root, 'fix'), { label: 'self-update', print: () => {}, escape: 'SHIPKIT_FROZEN=1' }, async () => {
|
|
135
|
+
const again = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
|
|
124
136
|
if (again.code === 0 && again.out.trim() !== '') return null
|
|
125
|
-
return await
|
|
137
|
+
return await c.exec(cmd, { cwd: c.root, timeoutMs: 120_000 })
|
|
126
138
|
})
|
|
127
139
|
if (!leased.ok) {
|
|
128
|
-
|
|
140
|
+
c.warn(`newer rvlib live: ${drift} — self-update deferred (${leased.warn}); re-run check once that run finishes`)
|
|
129
141
|
return
|
|
130
142
|
}
|
|
131
143
|
if (leased.value == null) {
|
|
132
|
-
|
|
144
|
+
c.warn(`newer rvlib live: ${drift} — self-update deferred (package.json/lockfile went dirty while waiting for the write lease); commit first, then re-run check`)
|
|
133
145
|
return
|
|
134
146
|
}
|
|
135
147
|
const up = leased.value
|
|
@@ -137,21 +149,23 @@ export const rvlibVersions: Rule = {
|
|
|
137
149
|
// which axis the failure sits on is measurable, so measure it: the
|
|
138
150
|
// tree is either half-written (this repo, blocks) or untouched, and
|
|
139
151
|
// a registry drop mid-download must never hard-red a healthy repo
|
|
140
|
-
const after = await
|
|
152
|
+
const after = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
|
|
141
153
|
const half = after.code !== 0 || after.out.trim() !== ''
|
|
142
|
-
const why =
|
|
154
|
+
const why = `${cmd.join(' ')} exited ${up.code}${up.timedOut ? ', timed out' : ''}`
|
|
143
155
|
const tail = up.out.trim().split('\n').at(-1)?.slice(0, 160) ?? ''
|
|
144
|
-
if (half)
|
|
145
|
-
else
|
|
156
|
+
if (half) c.fail(`self-update failed (${why}) and package.json/lockfile are half-written — \`git status\`, then restore or finish it by hand: ${tail}`)
|
|
157
|
+
else c.warn(`self-update failed (${why}) and nothing was written — the registry or the network, not this repo. Retry with: ${cmd.join(' ')}: ${tail}`)
|
|
146
158
|
return
|
|
147
159
|
}
|
|
148
160
|
for (const s of stale) {
|
|
149
|
-
const now = installedVersion(
|
|
161
|
+
const now = installedVersion(c.root, s.name)
|
|
150
162
|
// WARN on purpose: the run MUTATED the repo and a follow-up
|
|
151
163
|
// (restart watchers, commit) is owed — an ok here would be a
|
|
152
164
|
// green, cacheable line replaying a past event as current state
|
|
153
|
-
if (now === s.latest)
|
|
154
|
-
else
|
|
165
|
+
if (now === s.latest) c.warn(`${s.name} ${s.installed} → ${now} self-updated — restart watchers, commit the bump`)
|
|
166
|
+
else c.warn(`${s.name} updated but reads ${now ?? 'missing'} (wanted ${s.latest}) — check the lockfile`)
|
|
155
167
|
}
|
|
156
168
|
},
|
|
157
|
-
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
export default rvlibVersions
|
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
// np/release-it deps, a control room publishing its own sub-libs) is
|
|
13
13
|
// deliberately out of scope — `private: true` on the root already refuses,
|
|
14
14
|
// and sanctioned sub-lib publishing must not turn this rule red.
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import type { Rule } from './rule.ts'
|
|
15
|
+
import { join, relative } from 'path'
|
|
16
|
+
import { checkUnitReader, resolveUnits } from '../release/unit-resolve.ts'
|
|
17
|
+
import { check } from '../check.ts'
|
|
19
18
|
|
|
20
19
|
type Pkg = {
|
|
21
20
|
name?: string
|
|
@@ -32,41 +31,41 @@ type Pkg = {
|
|
|
32
31
|
* arrival, and it fails at install time for a stranger, never here. */
|
|
33
32
|
const LOCAL_PROTOCOL = /^(link|file|portal|workspace):/
|
|
34
33
|
|
|
35
|
-
export const npmPublishSafety
|
|
34
|
+
export const npmPublishSafety = check({
|
|
36
35
|
id: 'SK006',
|
|
37
36
|
blocking: true,
|
|
38
37
|
title: 'npm publish safety',
|
|
38
|
+
description: 'a private package carries no publish machinery',
|
|
39
39
|
owner: 'meta/rv-conventions/package-json.md',
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
// keyed on what it reads: the root package.json and every declared release
|
|
41
|
+
// unit's, at the dirs the config picks
|
|
42
|
+
run: (c) => {
|
|
43
|
+
const json = (rel: string): Pkg | null | 'unparseable' => {
|
|
44
|
+
try { return c.json<Pkg>(rel) } catch { return 'unparseable' }
|
|
45
|
+
}
|
|
46
|
+
const pkg = json('package.json')
|
|
47
|
+
if (pkg == null) return c.ok('no package.json — nothing publishable')
|
|
48
|
+
if (pkg === 'unparseable') return c.note('package.json unreadable — SK001 reports it')
|
|
48
49
|
// every block a CONSUMER resolves, for every unit that can publish:
|
|
49
50
|
// the ROOT is usually private, so scanning it alone guards nothing
|
|
50
51
|
const localHits: string[] = []
|
|
51
|
-
for (const u of [{ dir:
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
let upkg: Pkg
|
|
55
|
-
try { upkg = JSON.parse(readFileSync(up, 'utf8')) as Pkg } catch { continue }
|
|
52
|
+
for (const u of [{ dir: c.root, id: 'root' }, ...resolveUnits(c.root, c.config, checkUnitReader(c)).units.filter((x) => x.rel !== '')]) {
|
|
53
|
+
const upkg = json(relative(c.root, join(u.dir, 'package.json')))
|
|
54
|
+
if (upkg == null || upkg === 'unparseable') continue
|
|
56
55
|
if (upkg.private === true) continue
|
|
57
56
|
for (const block of ['dependencies', 'peerDependencies', 'optionalDependencies'] as const)
|
|
58
57
|
for (const [dep, range] of Object.entries(upkg[block] ?? {}))
|
|
59
58
|
if (LOCAL_PROTOCOL.test(range)) localHits.push(`${u.id}: ${block}.${dep} = ${range}`)
|
|
60
59
|
}
|
|
61
60
|
if (localHits.length > 0)
|
|
62
|
-
|
|
61
|
+
c.fail(
|
|
63
62
|
`${localHits.join(', ')} — a local protocol cannot resolve on a consumer's machine, so publishing that ships a package which fails at install. Declare the version it WILL be, and keep the local wiring in overrides (npm ignores those from a dependency) or in devDependencies`,
|
|
64
63
|
)
|
|
65
64
|
if (pkg.name?.startsWith('rvlib-') === true) {
|
|
66
|
-
if (localHits.length === 0)
|
|
65
|
+
if (localHits.length === 0) c.ok('rvlib-* package — publishes by design (distribute/npm owns it)')
|
|
67
66
|
return
|
|
68
67
|
}
|
|
69
|
-
if (pkg.private !== true) return
|
|
68
|
+
if (pkg.private !== true) return c.note('private:true missing — SK001 reports it; publish machinery not judged until the lock is on')
|
|
70
69
|
const machinery: string[] = []
|
|
71
70
|
if (pkg.publishConfig != null) machinery.push('publishConfig')
|
|
72
71
|
for (const s of ['prepublishOnly', 'prepublish', 'postpublish'])
|
|
@@ -74,7 +73,9 @@ export const npmPublishSafety: Rule = {
|
|
|
74
73
|
if (pkg.scripts?.['publish'] != null && /npm publish|bun publish/.test(pkg.scripts['publish']))
|
|
75
74
|
machinery.push(`scripts.publish (runs npm/bun publish)`)
|
|
76
75
|
if (machinery.length > 0)
|
|
77
|
-
|
|
78
|
-
else if (localHits.length === 0)
|
|
76
|
+
c.fail(`private package carries publish machinery: ${machinery.join(', ')} — dead weight now, a loaded gun the day private flips; delete it or make the repo a published lib on purpose`)
|
|
77
|
+
else if (localHits.length === 0) c.ok(`private package — no publishConfig / prepublish* / npm-publishing 'publish' script on the root, no local dependency protocol in any releasing unit`)
|
|
79
78
|
},
|
|
80
|
-
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
export default npmPublishSafety
|