rvlib-shipkit 0.38.0 → 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 +371 -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 +216 -18
- 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,197 @@
|
|
|
1
|
+
// SK017 vscode hub task (build/vscode-tasks.md): the folder-open hub task
|
|
2
|
+
// carries the problem matcher the hub prints for, it opens with the folder
|
|
3
|
+
// itself when nothing else does, and no `👁 TSC` watcher runs beside a gate
|
|
4
|
+
// that already runs `typecheck`.
|
|
5
|
+
//
|
|
6
|
+
// the file is hand-written jsonc, so the fixer edits spans: it removes the TSC
|
|
7
|
+
// task, a wrapper that only starts the hub, and their dependsOn entries, and
|
|
8
|
+
// writes the hub's matcher and folder-open run option. Every other byte stays
|
|
9
|
+
// as its author left it.
|
|
10
|
+
import { basename } from 'path'
|
|
11
|
+
import { check } from '../check.ts'
|
|
12
|
+
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
13
|
+
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
14
|
+
import { whenOfTask } from '../core/when.ts'
|
|
15
|
+
import { hubProblemMatcher } from '../problems/problem-line.ts'
|
|
16
|
+
import { indentAt, parseSpans, plain, removalSpan, type Node } from '../scaffold/jsonc-spans.ts'
|
|
17
|
+
|
|
18
|
+
export const TSC_LABEL = '👁 TSC'
|
|
19
|
+
const TASKS = '.vscode/tasks.json'
|
|
20
|
+
|
|
21
|
+
type Obj = Extract<Node, { kind: 'object' }>
|
|
22
|
+
type Arr = Extract<Node, { kind: 'array' }>
|
|
23
|
+
|
|
24
|
+
const prop = (o: Obj, key: string): Node | undefined => o.props.find((p) => p.key === key)?.value
|
|
25
|
+
|
|
26
|
+
/** the house type checker task and nothing that only looks like it: the exact
|
|
27
|
+
* label, a tsc binary, in watch mode */
|
|
28
|
+
function isTscWatcher(t: Record<string, unknown>): boolean {
|
|
29
|
+
const cmd = typeof t['command'] === 'string' ? t['command'] : ''
|
|
30
|
+
const args = Array.isArray(t['args']) ? t['args'] : []
|
|
31
|
+
return t['label'] === TSC_LABEL && basename(cmd) === 'tsc' && args.includes('--watch')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** the hub task: `bun run hub`, or the house label */
|
|
35
|
+
function isHub(t: Record<string, unknown>): boolean {
|
|
36
|
+
const args = Array.isArray(t['args']) ? t['args'] : []
|
|
37
|
+
return (t['command'] === 'bun' && args[0] === 'run' && args[1] === 'hub') || (typeof t['label'] === 'string' && /^🚀\s+shipkit\b/.test(t['label']))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** a task that only starts other tasks: no command of its own */
|
|
41
|
+
const isWrapper = (t: Record<string, unknown>): boolean => t['command'] == null && t['type'] == null && t['dependsOn'] != null
|
|
42
|
+
|
|
43
|
+
export type RepairOpts = {
|
|
44
|
+
/** the gate runs an enabled `typecheck` check. Without one the TSC task is
|
|
45
|
+
* the only type checker this repo has, so it stays */
|
|
46
|
+
typecheckOn: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** what this file needs, applied: the text to write, one line per change, and
|
|
50
|
+
* one line per thing it deliberately left. No change means the text comes
|
|
51
|
+
* back byte for byte */
|
|
52
|
+
export function repairTasks(text: string, o: RepairOpts): { text: string; changes: string[]; kept: string[] } {
|
|
53
|
+
const root = parseSpans(text)
|
|
54
|
+
if (root.kind !== 'object') throw new Error('tasks.json is not an object')
|
|
55
|
+
const tasks = prop(root, 'tasks')
|
|
56
|
+
if (tasks == null || tasks.kind !== 'array') return { text, changes: [], kept: [] }
|
|
57
|
+
const edits: { start: number; end: number; body: string }[] = []
|
|
58
|
+
const changes: string[] = []
|
|
59
|
+
const kept: string[] = []
|
|
60
|
+
const record = (t: Node): Record<string, unknown> => plain(text, t) as Record<string, unknown>
|
|
61
|
+
const labelOf = (t: Node): string => String(record(t)['label'] ?? '?')
|
|
62
|
+
const items = tasks.items.filter((n): n is Obj => n.kind === 'object')
|
|
63
|
+
const hubs = items.filter((t) => isHub(record(t)))
|
|
64
|
+
|
|
65
|
+
const tscAt = tasks.items.flatMap((n, i) => (n.kind === 'object' && isTscWatcher(record(n)) ? [i] : []))
|
|
66
|
+
const dropTsc = tscAt.length > 0 && o.typecheckOn
|
|
67
|
+
if (tscAt.length > 0 && !o.typecheckOn) kept.push(`kept the '${TSC_LABEL}' task: this repo's gate has no enabled typecheck check, so it is the only type checker here`)
|
|
68
|
+
if (dropTsc) for (const i of tscAt) {
|
|
69
|
+
const [start, end] = removalSpan(text, tasks, i)
|
|
70
|
+
edits.push({ start, end, body: '' })
|
|
71
|
+
changes.push(`removed the '${TSC_LABEL}' task`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// a folder-open wrapper whose only job is starting the hub is a second
|
|
75
|
+
// task on screen for nothing: the hub opens with the folder itself
|
|
76
|
+
const hub = hubs.length === 1 ? hubs[0]! : null
|
|
77
|
+
const hubLabel = hub == null ? null : labelOf(hub)
|
|
78
|
+
const depsOf = (t: Node): string[] => {
|
|
79
|
+
const d = record(t)['dependsOn']
|
|
80
|
+
const list = Array.isArray(d) ? d.map(String) : [String(d)]
|
|
81
|
+
return dropTsc ? list.filter((x) => x !== TSC_LABEL) : list
|
|
82
|
+
}
|
|
83
|
+
const wrapperAt = hub == null ? -1 : tasks.items.findIndex((n) => n.kind === 'object' && isWrapper(record(n)) && depsOf(n).length === 1 && depsOf(n)[0] === hubLabel)
|
|
84
|
+
const wrapper = wrapperAt < 0 ? null : (tasks.items[wrapperAt] as Obj)
|
|
85
|
+
if (wrapper != null) {
|
|
86
|
+
const [start, end] = removalSpan(text, tasks, wrapperAt)
|
|
87
|
+
edits.push({ start, end, body: '' })
|
|
88
|
+
changes.push(`folded the '${labelOf(wrapper)}' wrapper into '${hubLabel}', which now opens with the folder`)
|
|
89
|
+
if (/\/\/.*(master task|subtask)/i.test(text)) changes.push('a comment still describes a master task with subtasks: rewrite it by hand')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (dropTsc) {
|
|
93
|
+
for (const t of items) {
|
|
94
|
+
if (t === wrapper) continue
|
|
95
|
+
const d = prop(t, 'dependsOn')
|
|
96
|
+
if (d?.kind === 'array') {
|
|
97
|
+
const idx = d.items.findIndex((x) => x.kind === 'scalar' && x.value === TSC_LABEL)
|
|
98
|
+
if (idx < 0) continue
|
|
99
|
+
const [start, end] = removalSpan(text, d as Arr, idx)
|
|
100
|
+
edits.push({ start, end, body: '' })
|
|
101
|
+
changes.push(`dropped '${TSC_LABEL}' from the dependsOn of '${labelOf(t)}'`)
|
|
102
|
+
} else if (d?.kind === 'scalar' && d.value === TSC_LABEL) {
|
|
103
|
+
edits.push({ start: d.start, end: d.end, body: '[]' })
|
|
104
|
+
changes.push(`dropped '${TSC_LABEL}' from a dependsOn`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const want = hubProblemMatcher()
|
|
110
|
+
for (const h of hubs) {
|
|
111
|
+
const first = h.props[0]
|
|
112
|
+
const hubIndent = indentAt(text, h.start)
|
|
113
|
+
const propIndent = first != null ? indentAt(text, first.keyStart) : `${hubIndent} `
|
|
114
|
+
const unit = propIndent.length > hubIndent.length ? propIndent.slice(hubIndent.length) : ' '
|
|
115
|
+
const render = (v: unknown): string => JSON.stringify(v, null, unit).split('\n').join(`\n${propIndent}`)
|
|
116
|
+
const added: string[] = []
|
|
117
|
+
const current = prop(h, 'problemMatcher')
|
|
118
|
+
if (current == null || JSON.stringify(plain(text, current)) !== JSON.stringify(want)) {
|
|
119
|
+
if (current != null) edits.push({ start: current.start, end: current.end, body: render(want) })
|
|
120
|
+
else added.push(`"problemMatcher": ${render(want)}`)
|
|
121
|
+
changes.push(`wrote the shipkit problem matcher on '${labelOf(h)}'`)
|
|
122
|
+
}
|
|
123
|
+
if (h === hub && wrapper != null) {
|
|
124
|
+
const ro = prop(h, 'runOptions')
|
|
125
|
+
const was = ro != null && ro.kind === 'object' ? record(ro) : {}
|
|
126
|
+
const inline = `{ ${Object.entries(Object.assign({ runOn: 'folderOpen', instanceLimit: 1 }, was, { runOn: 'folderOpen' })).map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`).join(', ')} }`
|
|
127
|
+
if (ro != null) edits.push({ start: ro.start, end: ro.end, body: inline })
|
|
128
|
+
else added.push(`"runOptions": ${inline}`)
|
|
129
|
+
const wg = prop(wrapper, 'group')
|
|
130
|
+
if (wg != null) {
|
|
131
|
+
const body = text.slice(wg.start, wg.end)
|
|
132
|
+
const g = prop(h, 'group')
|
|
133
|
+
if (g != null) edits.push({ start: g.start, end: g.end, body })
|
|
134
|
+
else added.push(`"group": ${body}`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (added.length === 0) continue
|
|
138
|
+
const last = h.props.at(-1)
|
|
139
|
+
const joined = added.join(`,\n${propIndent}`)
|
|
140
|
+
if (last == null) edits.push({ start: h.start + 1, end: h.start + 1, body: `\n${propIndent}${joined}\n${hubIndent}` })
|
|
141
|
+
else edits.push({ start: last.value.end, end: last.value.end, body: `,\n${propIndent}${joined}` })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let out = text
|
|
145
|
+
for (const e of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.body + out.slice(e.end)
|
|
146
|
+
return { text: out, changes, kept }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** whether this repo's gate runs a typecheck: a floor slot or a task that
|
|
150
|
+
* resolves, and no `when` turning it off */
|
|
151
|
+
export function typecheckOn(c: CheckCtx): boolean {
|
|
152
|
+
const config = c.config
|
|
153
|
+
let floor: ReturnType<typeof resolveFloor>
|
|
154
|
+
try { floor = resolveFloor(config, c.root, checkFloorReader(c)) } catch { return false }
|
|
155
|
+
const t = [...floor.tasks, ...(config.tasks ?? [])].find((x) => x.id === 'typecheck')
|
|
156
|
+
return t != null && whenOfTask(config, t).at !== 'off'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** the tasks file this check may touch: none, or left alone because .vscode/
|
|
160
|
+
* is a link to a dir shared with other repos */
|
|
161
|
+
function ownTasks(c: CheckCtx): { text: string } | 'none' | 'linked' {
|
|
162
|
+
if (!c.exists(TASKS)) return 'none'
|
|
163
|
+
if (c.isLink('.vscode')) return 'linked'
|
|
164
|
+
const text = c.read(TASKS)
|
|
165
|
+
return text == null ? 'none' : { text }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const vscodeHubTask = check({
|
|
169
|
+
id: 'SK017',
|
|
170
|
+
// a stale editor file is behind, not broken: it warns, and the fix pass
|
|
171
|
+
// repairs it
|
|
172
|
+
blocking: false,
|
|
173
|
+
title: 'vscode hub task',
|
|
174
|
+
description: 'the vscode hub task carries the problem matcher and no extra tsc watcher runs',
|
|
175
|
+
owner: 'build/vscode-tasks.md',
|
|
176
|
+
run: (c) => {
|
|
177
|
+
const own = ownTasks(c)
|
|
178
|
+
if (own === 'none') return c.ok(`no ${TASKS}, so there is no hub task to wire`)
|
|
179
|
+
if (own === 'linked') return c.ok(`.vscode/ is a link to a shared dir, left alone`)
|
|
180
|
+
let r: { changes: string[]; kept: string[] }
|
|
181
|
+
try { r = repairTasks(own.text, { typecheckOn: typecheckOn(c) }) }
|
|
182
|
+
catch (e) { return c.warn(`${TASKS} does not parse (${(e as Error).message}), so the hub task was not checked`) }
|
|
183
|
+
for (const k of r.kept) c.note(`${TASKS}: ${k}`)
|
|
184
|
+
if (r.changes.length === 0) return c.ok(`${TASKS}: the hub task feeds the problems pane`)
|
|
185
|
+
for (const ch of r.changes) c.fail(`${TASKS}: ${ch.replace(/^removed/, 'should remove').replace(/^dropped/, 'should drop').replace(/^wrote/, 'should write').replace(/^folded/, 'should fold')}`)
|
|
186
|
+
},
|
|
187
|
+
fix: (c) => {
|
|
188
|
+
const own = ownTasks(c)
|
|
189
|
+
if (typeof own === 'string') return { changed: false, summary: `no ${TASKS} to repair` }
|
|
190
|
+
const r = repairTasks(own.text, { typecheckOn: typecheckOn(c) })
|
|
191
|
+
if (r.changes.length === 0) return { changed: false, summary: `${TASKS} already current` }
|
|
192
|
+
c.write(TASKS, r.text)
|
|
193
|
+
return { changed: true, summary: `${TASKS}: ${r.changes.join(', ')}` }
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
export default vscodeHubTask
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SK018 — every test file runs in a check (build/shipkit.md, the test checks).
|
|
2
|
+
// A `*.test.ts` that no check runs is a green nobody measured: the file says
|
|
3
|
+
// something is proved, and nothing proves it. Each file belongs to the release
|
|
4
|
+
// unit whose dir holds it (the root takes the rest), and the unit's own
|
|
5
|
+
// `bun test` must reach it. A file kept out on purpose goes under a bunfig
|
|
6
|
+
// `pathIgnorePatterns` entry, which is where the reason is written.
|
|
7
|
+
import { check } from '../check.ts'
|
|
8
|
+
import { isTestFile, TEST_GLOB, testPlanOf } from '../suite/test-units.ts'
|
|
9
|
+
|
|
10
|
+
export const testFilesRun = check({
|
|
11
|
+
id: 'SK018',
|
|
12
|
+
blocking: true,
|
|
13
|
+
title: 'test files run',
|
|
14
|
+
description: 'every test file in the repo runs in a test check or is ignored on purpose',
|
|
15
|
+
owner: 'build/shipkit.md',
|
|
16
|
+
run: (c) => {
|
|
17
|
+
const files = c.list(TEST_GLOB).filter(isTestFile)
|
|
18
|
+
if (files.length === 0) return c.ok('no test file in this repo')
|
|
19
|
+
const plan = testPlanOf({ config: c.config, files, read: (rel) => c.read(rel) })
|
|
20
|
+
for (const o of plan.orphans) c.fail(`${o.file} runs in no check: ${o.why}`, 'move it under a test root, fix the `bun test` arguments, or add it to bunfig.toml [test] pathIgnorePatterns with the reason in a comment')
|
|
21
|
+
for (const id of plan.opaque) c.note(`${id}: its script is not a plain \`bun test\`, so which files it runs cannot be read`)
|
|
22
|
+
const per = [...plan.runs].filter(([id, f]) => f.length > 0 || id === 'test').map(([id, f]) => `${id} ${f.length}`)
|
|
23
|
+
const ignored = plan.ignored.length > 0 ? `, ${plan.ignored.length} ignored on purpose` : ''
|
|
24
|
+
if (plan.orphans.length === 0) c.ok(`${files.length} test file(s) all run: ${per.join(', ')}${ignored}`)
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export default testFilesRun
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SK019 — tests run through the generated test checks only (build/shipkit.md,
|
|
2
|
+
// the test checks). The floor `test` slot and one `test:<unit>` per declared
|
|
3
|
+
// unit are the ONE way tests run: in the unit's dir with its bunfig and
|
|
4
|
+
// preload, parallel, timed against the budget, and planned file by file by
|
|
5
|
+
// SK018. A hand written check that runs `bun test` itself is a second runner
|
|
6
|
+
// none of that reaches. The allowed shapes are never scanned: the generated
|
|
7
|
+
// checks are not files, and `defaults.test` IS the floor slot.
|
|
8
|
+
//
|
|
9
|
+
// Its own id rather than a line of SK018: SK018 answers "does every test file
|
|
10
|
+
// run", this one "does anything else run them", and each is read from
|
|
11
|
+
// different files (test files and bunfigs there, check sources and the config
|
|
12
|
+
// here), so each keeps its own key, row and `when` switch.
|
|
13
|
+
import { dirname, posix, relative } from 'path'
|
|
14
|
+
import { check } from '../check.ts'
|
|
15
|
+
import { repoCheckSources } from '../core/legacy-shapes.ts'
|
|
16
|
+
import { argvRunsTests, commandRunsTests, sourceRunsTests, type ScriptReader } from '../suite/runner-shapes.ts'
|
|
17
|
+
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
18
|
+
|
|
19
|
+
/** the scripts of the package.json in `dir` (repo relative), then the root's */
|
|
20
|
+
const scriptsIn = (c: CheckCtx, dir: string): ScriptReader => {
|
|
21
|
+
const read = (d: string): Record<string, string> => {
|
|
22
|
+
try { return (JSON.parse(c.read(d === '' || d === '.' ? 'package.json' : posix.join(d, 'package.json')) ?? '{}') as { scripts?: Record<string, string> }).scripts ?? {} } catch { return {} }
|
|
23
|
+
}
|
|
24
|
+
const own = read(dir)
|
|
25
|
+
const root = read('')
|
|
26
|
+
return (name) => own[name] ?? root[name] ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** the floor slots besides `test`, whose package.json body must not run tests either */
|
|
30
|
+
const OTHER_SLOTS = ['lint', 'typecheck', 'imports:check', 'cycles']
|
|
31
|
+
|
|
32
|
+
const FIX = 'declare a unit instead (units in repo.config.ts): each unit gets its own generated `test:<unit>` check, run in its dir'
|
|
33
|
+
|
|
34
|
+
export const testsRunInUnits = check({
|
|
35
|
+
id: 'SK019',
|
|
36
|
+
blocking: true,
|
|
37
|
+
title: 'tests run in unit checks',
|
|
38
|
+
description: 'no check runs the test runner itself: tests run only in the generated test checks',
|
|
39
|
+
owner: 'build/shipkit.md',
|
|
40
|
+
run: (c) => {
|
|
41
|
+
// the toolkit's own checks (this file's dir) live inside the repo that
|
|
42
|
+
// hosts it, and a built-in that WRITES a test script (SK001) is not a
|
|
43
|
+
// runner. Read off import.meta, since importing discovery pulls in fs
|
|
44
|
+
const builtins = relative(c.root, import.meta.dir).replace(/\\/g, '/')
|
|
45
|
+
const files = repoCheckSources(c).filter((f) => !f.rel.startsWith(`${builtins}/`))
|
|
46
|
+
let hits = 0
|
|
47
|
+
const say = (where: string, how: string): void => { hits++; c.fail(`${where} runs ${how} itself`, FIX) }
|
|
48
|
+
const root = scriptsIn(c, '')
|
|
49
|
+
const cwdOf = (cwd: string | undefined): string => (cwd == null ? '' : relative(c.root, posix.resolve(c.root, cwd)))
|
|
50
|
+
for (const f of files) for (const how of sourceRunsTests(f.src ?? '', scriptsIn(c, dirname(f.rel)))) say(f.rel, how)
|
|
51
|
+
for (const t of c.config.tasks ?? []) {
|
|
52
|
+
const scripts = scriptsIn(c, cwdOf(t.cwd))
|
|
53
|
+
const how = t.cmd != null ? argvRunsTests(t.cmd, scripts) : null
|
|
54
|
+
if (how != null) say(`repo.config.ts tasks '${t.id}'`, how)
|
|
55
|
+
else if (t.logic != null) for (const h of sourceRunsTests(String(t.logic), scripts)) say(`repo.config.ts tasks '${t.id}'`, h)
|
|
56
|
+
}
|
|
57
|
+
for (const r of c.config.rules ?? []) for (const h of sourceRunsTests(String(r.lint), root)) say(`repo.config.ts rules '${r.id}'`, h)
|
|
58
|
+
const defaults = c.config.defaults ?? {}
|
|
59
|
+
for (const [id, d] of Object.entries(defaults)) {
|
|
60
|
+
if (id === 'test' || d === false || d.cmd == null) continue
|
|
61
|
+
const how = argvRunsTests(d.cmd, scriptsIn(c, cwdOf(d.cwd)))
|
|
62
|
+
if (how != null) say(`repo.config.ts defaults '${id}'`, how)
|
|
63
|
+
}
|
|
64
|
+
// a slot the package.json fills: its raw body, else its own script
|
|
65
|
+
for (const id of OTHER_SLOTS) {
|
|
66
|
+
if (defaults[id] === false || (defaults[id] as { cmd?: unknown } | undefined)?.cmd != null) continue
|
|
67
|
+
const name = root(`${id}:raw`) != null ? `${id}:raw` : id
|
|
68
|
+
const body = root(name)
|
|
69
|
+
const how = body == null ? null : commandRunsTests(body, (n) => (n === name ? null : root(n)))
|
|
70
|
+
if (how != null) say(`package.json script '${name}' (the ${id} slot)`, how)
|
|
71
|
+
}
|
|
72
|
+
if (hits === 0) c.ok(`${files.length} check file(s) read, none runs the test runner: tests run only in the generated test checks`)
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export default testsRunInUnits
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// SK020: a check file never spawns itself (build/shipkit/writing-a-check.md
|
|
2
|
+
// owns the one body shape, `run: (c) => …`). A `cmd` that names the file's
|
|
3
|
+
// own path runs the file a second time, in its own process, behind an
|
|
4
|
+
// `import.meta.main` guard: a second way to write a body, with none of what
|
|
5
|
+
// `c` gives (recorded reads, one pass, one runner).
|
|
6
|
+
//
|
|
7
|
+
// It reads each check file's SOURCE through `c`, never imports it, so it is
|
|
8
|
+
// cached on the listing and the sources. A legacy check SCRIPT (no check
|
|
9
|
+
// object) is SK016's, read through its window, and never counted here.
|
|
10
|
+
import { check } from '../check.ts'
|
|
11
|
+
import { isLegacyCheckSource } from '../core/check-file-names.ts'
|
|
12
|
+
import { repoCheckSources } from '../core/legacy-shapes.ts'
|
|
13
|
+
|
|
14
|
+
const OWNER = 'build/shipkit/writing-a-check.md'
|
|
15
|
+
|
|
16
|
+
/** the source with every comment blanked, so a file that MENTIONS the
|
|
17
|
+
* shape in a comment is not a file that has it, and a `/*` inside a glob
|
|
18
|
+
* string opens no comment. `strings: false` blanks the strings too, for a
|
|
19
|
+
* question about code rather than about an argv */
|
|
20
|
+
export function codeOf(src: string, o: { strings: boolean } = { strings: true }): string {
|
|
21
|
+
let out = ''
|
|
22
|
+
let quote: string | null = null
|
|
23
|
+
for (let i = 0; i < src.length; i++) {
|
|
24
|
+
const ch = src[i]!
|
|
25
|
+
if (quote != null) {
|
|
26
|
+
const keep = o.strings || ch === quote
|
|
27
|
+
if (ch === '\\') {
|
|
28
|
+
i++
|
|
29
|
+
if (o.strings) out += ch + (src[i] ?? '')
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (ch === quote) quote = null
|
|
33
|
+
if (keep) out += ch
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (ch === '/' && src[i + 1] === '/') {
|
|
37
|
+
while (i < src.length && src[i] !== '\n') i++
|
|
38
|
+
out += '\n'
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (ch === '/' && src[i + 1] === '*') {
|
|
42
|
+
const end = src.indexOf('*/', i + 2)
|
|
43
|
+
i = end === -1 ? src.length : end + 1
|
|
44
|
+
out += ' '
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
// a regex literal starts where an expression may: its quotes open no
|
|
48
|
+
// string, and it is code, so it is blanked like a string
|
|
49
|
+
if (ch === '/' && /[(,=:[!&|?{};]$|^$/.test(out.trimEnd().slice(-1))) {
|
|
50
|
+
let j = i + 1
|
|
51
|
+
let cls = false
|
|
52
|
+
for (; j < src.length && src[j] !== '\n'; j++) {
|
|
53
|
+
if (src[j] === '\\') j++
|
|
54
|
+
else if (src[j] === '[') cls = true
|
|
55
|
+
else if (src[j] === ']') cls = false
|
|
56
|
+
else if (src[j] === '/' && !cls) break
|
|
57
|
+
}
|
|
58
|
+
if (src[j] === '/') {
|
|
59
|
+
out += o.strings ? src.slice(i, j + 1) : '//'
|
|
60
|
+
i = j
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (ch === "'" || ch === '"' || ch === '`') quote = ch
|
|
65
|
+
out += ch
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** the elements of every `cmd: [...]` array literal, split at the commas of
|
|
71
|
+
* its own level */
|
|
72
|
+
function cmdElements(code: string): string[][] {
|
|
73
|
+
const out: string[][] = []
|
|
74
|
+
for (const m of code.matchAll(/\bcmd\s*:\s*\[/g)) {
|
|
75
|
+
const items: string[] = []
|
|
76
|
+
let depth = 0
|
|
77
|
+
let quote: string | null = null
|
|
78
|
+
let cur = ''
|
|
79
|
+
for (let i = m.index! + m[0].length; i < code.length; i++) {
|
|
80
|
+
const ch = code[i]!
|
|
81
|
+
if (quote != null) {
|
|
82
|
+
cur += ch
|
|
83
|
+
if (ch === '\\') cur += code[++i] ?? ''
|
|
84
|
+
else if (ch === quote) quote = null
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (ch === "'" || ch === '"' || ch === '`') quote = ch
|
|
88
|
+
else if (ch === '(' || ch === '[' || ch === '{') depth++
|
|
89
|
+
else if ((ch === ')' || ch === ']' || ch === '}') && depth > 0) depth--
|
|
90
|
+
else if (ch === ']') break
|
|
91
|
+
else if (ch === ',' && depth === 0) {
|
|
92
|
+
items.push(cur.trim())
|
|
93
|
+
cur = ''
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
cur += ch
|
|
97
|
+
}
|
|
98
|
+
if (cur.trim() !== '') items.push(cur.trim())
|
|
99
|
+
out.push(items)
|
|
100
|
+
}
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** an argv element that IS the module's own file */
|
|
105
|
+
const SELF = [
|
|
106
|
+
/^import\.meta\.(path|filename)$/,
|
|
107
|
+
/^fileURLToPath\(\s*import\.meta\.url\s*\)$/,
|
|
108
|
+
/^new\s+URL\(\s*import\.meta\.url\s*\)\.pathname$/,
|
|
109
|
+
/^__filename$/,
|
|
110
|
+
/^Bun\.main$/,
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
/** why this check file runs a body of its own in a second process, null when
|
|
114
|
+
* it does not: a `cmd` element that is the file itself, or the
|
|
115
|
+
* `import.meta.main` guard such a body hides behind, which survives any
|
|
116
|
+
* aliasing of the path */
|
|
117
|
+
export function selfSpawn(rel: string, src: string): string | null {
|
|
118
|
+
const code = codeOf(src)
|
|
119
|
+
for (const argv of cmdElements(code))
|
|
120
|
+
for (const el of argv) {
|
|
121
|
+
if (SELF.some((re) => re.test(el))) return `its cmd runs the file itself (${el})`
|
|
122
|
+
const lit = /^(['"`])(.*)\1$/.exec(el)?.[2]
|
|
123
|
+
if (lit != null && (lit.replace(/^\.\//, '') === rel || lit.endsWith(`/${rel}`))) return `its cmd names its own path (${lit})`
|
|
124
|
+
}
|
|
125
|
+
if (/\bimport\.meta\.main\b/.test(codeOf(src, { strings: false }))) return 'it guards a body of its own behind import.meta.main'
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const selfSpawningCheck = check({
|
|
130
|
+
id: 'SK020',
|
|
131
|
+
title: 'self spawning check',
|
|
132
|
+
description: 'no check file runs its own body by spawning itself',
|
|
133
|
+
owner: OWNER,
|
|
134
|
+
blocking: true,
|
|
135
|
+
run: (c) => {
|
|
136
|
+
const found = repoCheckSources(c).flatMap((f) => {
|
|
137
|
+
if (f.src == null || isLegacyCheckSource(f.src)) return []
|
|
138
|
+
const why = selfSpawn(f.rel, f.src)
|
|
139
|
+
return why == null ? [] : [`${f.rel}: ${why}`]
|
|
140
|
+
})
|
|
141
|
+
if (found.length === 0) return c.ok('no check file spawns itself: every body runs through `c`')
|
|
142
|
+
c.fail(`${found.length} check file(s) run a body of their own in a second process: ${found.join(', ')}`)
|
|
143
|
+
c.lines.push({ kind: 'fix-fail', text: `move each body into \`run: (c) => …\` and drop the \`cmd\` and the \`import.meta.main\` guard: a probe reaches the world through c.fetch, c.exec and c.tool (${OWNER})` })
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
export default selfSpawningCheck
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// the contracts check — `.shipkit/*` release-contract fulfillness. It shares ONE
|
|
2
|
+
// evaluation with check SK002 (the `contractsResult` scan), and it tags with
|
|
3
|
+
// that id, because the invariant has one owner and two surfaces report it. A
|
|
4
|
+
// venue deploy step deps on it BY NAME, which needs nothing from this end.
|
|
5
|
+
import { applyContractsResult, contractsResult } from '../contracts/apply-contracts.ts'
|
|
6
|
+
import { check } from '../check.ts'
|
|
7
|
+
import { contractsFulfillness } from './SK002-contracts-fulfillness.check.ts'
|
|
8
|
+
|
|
9
|
+
export const contractsCheck = check({
|
|
10
|
+
id: 'contracts',
|
|
11
|
+
title: 'contracts',
|
|
12
|
+
description: 'every declared venue has the `.shipkit/*` release contract it demands',
|
|
13
|
+
owner: 'meta/contracts.md',
|
|
14
|
+
blocking: true,
|
|
15
|
+
run: async (c) => {
|
|
16
|
+
applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export default contractsCheck
|
package/src/cli/cmd_Asks.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { join } from 'path'
|
|
|
5
5
|
import { Command, Option } from 'clipanion'
|
|
6
6
|
import { checkAsks, createAsk, renderAsksTable } from '../asks/asks-queue.ts'
|
|
7
7
|
import { loadRepoConfig } from '../repo-config.ts'
|
|
8
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
8
9
|
|
|
9
|
-
const asksDir = async (root: string): Promise<string> => join(root, (await loadRepoConfig(root)).asks?.dir ??
|
|
10
|
+
const asksDir = async (root: string): Promise<string> => join(root, (await loadRepoConfig(root)).asks?.dir ?? LAYOUT.asks)
|
|
10
11
|
|
|
11
12
|
export class AsksCommand extends Command {
|
|
12
13
|
static override paths = [['asks']]
|
|
@@ -21,7 +22,7 @@ export class AsksCommand extends Command {
|
|
|
21
22
|
try {
|
|
22
23
|
const root = process.cwd()
|
|
23
24
|
const c = await loadRepoConfig(root)
|
|
24
|
-
renderAsksTable(join(root, c.asks?.dir ??
|
|
25
|
+
renderAsksTable(join(root, c.asks?.dir ?? LAYOUT.asks), undefined, { emptyText: c.asks?.emptyText })
|
|
25
26
|
return 0
|
|
26
27
|
} catch (e) {
|
|
27
28
|
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
package/src/cli/cmd_Assets.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// shipkit assets — asset atelier: declare kinds in .
|
|
1
|
+
// shipkit assets — asset atelier: declare kinds in .shipkit/assets/kit.ts, roll
|
|
2
2
|
// (ComfyUI or procedural canvas), curate in the studio, ship with metadata
|
|
3
3
|
// write-through. Thin clipanion surfaces; logic in assets/run-assets.ts.
|
|
4
4
|
import { Command, Option } from 'clipanion'
|
|
@@ -85,7 +85,7 @@ export class AssetsIdentityCommand extends Command {
|
|
|
85
85
|
|
|
86
86
|
static override usage = Command.Usage({
|
|
87
87
|
category: 'Assets',
|
|
88
|
-
description: 'render favicon/logo/OG from .
|
|
88
|
+
description: 'render favicon/logo/OG from .shipkit/identity/brand.ts into public/',
|
|
89
89
|
examples: [['regenerate the identity outputs', 'shipkit assets identity']],
|
|
90
90
|
})
|
|
91
91
|
|
package/src/cli/cmd_Bump.ts
CHANGED
|
@@ -9,7 +9,7 @@ export class BumpCommand extends Command {
|
|
|
9
9
|
|
|
10
10
|
static override usage = Command.Usage({
|
|
11
11
|
category: 'Gate',
|
|
12
|
-
description: 'ship a version in one word: bumps a
|
|
12
|
+
description: 'ship a version in one word: bumps a unit (default the repo root), run from a feature branch it merges into the release branch first, then promotes the CHANGELOG Unreleased section, bumps package.json, runs the gate and every prepublish script, commits, tags, pushes, and cuts the release. Any failure before the version commit rolls the merge back and returns you to your branch.',
|
|
13
13
|
examples: [
|
|
14
14
|
['a bugfix release of the repo itself', 'shipkit bump patch'],
|
|
15
15
|
['a feature release', 'shipkit bump minor'],
|
package/src/cli/cmd_Check.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// shipkit check — periodic health, TTL-cached; `--fix` applies rule
|
|
2
2
|
// autofixes then re-lints. Thin clipanion surface; logic in
|
|
3
|
-
// core/run-check.ts and
|
|
3
|
+
// core/run-check.ts and core/run-fix.ts. ci NEVER fixes.
|
|
4
4
|
import { Command, Option } from 'clipanion'
|
|
5
5
|
import { runCheck } from '../core/run-check.ts'
|
|
6
6
|
import { maybeSelfUpdateAndRestart } from '../core/self-update.ts'
|
|
7
|
-
import { runFix } from '../
|
|
7
|
+
import { runFix } from '../core/run-fix.ts'
|
|
8
8
|
|
|
9
9
|
export class CheckCommand extends Command {
|
|
10
10
|
static override paths = [['check'], ['doctor']]
|
|
11
11
|
|
|
12
12
|
static override usage = Command.Usage({
|
|
13
13
|
category: 'Health',
|
|
14
|
-
description: '
|
|
14
|
+
description: 'every check except the build steps. `shipkit ci` runs these too and is what you normally want. --fix applies check autofixes',
|
|
15
15
|
examples: [
|
|
16
16
|
['run (cached where valid)', 'shipkit check'],
|
|
17
17
|
['bust every cache layer', 'shipkit check --fresh'],
|
|
@@ -24,8 +24,8 @@ export class CheckCommand extends Command {
|
|
|
24
24
|
|
|
25
25
|
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
26
26
|
fresh = Option.Boolean('--fresh', false, { description: 'bust every TTL cache layer (travels to child checks)' })
|
|
27
|
-
fix = Option.Boolean('--fix', false, { description: 'apply
|
|
28
|
-
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for self-maintaining
|
|
27
|
+
fix = Option.Boolean('--fix', false, { description: 'apply check autofixes (every fixable check, or only the listed ids), then re-run them' })
|
|
28
|
+
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for the self-maintaining checks (SK005 never touches package.json)' })
|
|
29
29
|
fixIds = Option.Rest()
|
|
30
30
|
|
|
31
31
|
async execute(): Promise<number> {
|
|
@@ -34,7 +34,7 @@ export class CheckCommand extends Command {
|
|
|
34
34
|
await maybeSelfUpdateAndRestart(process.cwd())
|
|
35
35
|
try {
|
|
36
36
|
if (this.fixIds.length > 0 && !this.fix) {
|
|
37
|
-
this.context.stderr.write(`FAIL:
|
|
37
|
+
this.context.stderr.write(`FAIL: check ids need --fix (shipkit check --fix ${this.fixIds.join(' ')})\n`)
|
|
38
38
|
return 2
|
|
39
39
|
}
|
|
40
40
|
if (this.fix && this.json) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// shipkit checks — THE inventory: every check this repo can run on one table,
|
|
2
|
+
// with what it declared about itself. `shipkit keys` answers a different
|
|
3
|
+
// question (what is a check keyed on, right now), and nothing else says
|
|
4
|
+
// whether a check blocks you or only reports the world. That answer had no
|
|
5
|
+
// surface, so it was reconstructed by reading source files, which is how a
|
|
6
|
+
// repo ends up with checks nobody can account for.
|
|
7
|
+
//
|
|
8
|
+
// `--fixable` is the narrower catalog: what repairs itself, where the
|
|
9
|
+
// invariant is owned, and how to apply one.
|
|
10
|
+
import { writeFileSync } from 'fs'
|
|
11
|
+
import { resolve } from 'path'
|
|
12
|
+
import { Command, Option, type Usage } from 'clipanion'
|
|
13
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
14
|
+
import { envTier, gateInventory } from '../ledger/gate-checks.ts'
|
|
15
|
+
import { renderFixableLines, renderCheckLines, renderCheckTable } from '../ledger/check-table.ts'
|
|
16
|
+
import { classify } from '../ledger/git-key.ts'
|
|
17
|
+
|
|
18
|
+
export class ChecksCommand extends Command {
|
|
19
|
+
static override paths = [['checks']]
|
|
20
|
+
|
|
21
|
+
// widened so the hidden alias below can drop it: no usage, no help entry
|
|
22
|
+
static override usage: Usage | undefined = Command.Usage({
|
|
23
|
+
category: 'Health',
|
|
24
|
+
description: 'every gate check on one table: where it comes from, whether it blocks or only warns, whether the gate runs it or only `shipkit check`, and how it caches. Reports, never gates',
|
|
25
|
+
examples: [
|
|
26
|
+
['the whole inventory', 'shipkit checks'],
|
|
27
|
+
['the big markdown table, written to a file', 'shipkit checks --out STATUS.full.md'],
|
|
28
|
+
['only the ones the gate never runs', 'shipkit checks --proactive'],
|
|
29
|
+
['everything that repairs itself, with its owner', 'shipkit checks --fixable'],
|
|
30
|
+
['machine-readable', 'shipkit checks --json'],
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
json = Option.Boolean('--json', false, { description: 'machine-readable CheckRow[] with the resolved cache population' })
|
|
35
|
+
md = Option.Boolean('--md', false, { description: 'the full markdown table on stdout, the same bytes --out writes' })
|
|
36
|
+
out = Option.String('--out', { required: false, description: 'write the markdown table to this file instead of stdout' })
|
|
37
|
+
blocking = Option.Boolean('--blocking', false, { description: 'only checks whose failure can stop you' })
|
|
38
|
+
proactive = Option.Boolean('--proactive', false, { description: 'only checks the gate never runs (`shipkit check` reports them)' })
|
|
39
|
+
fixable = Option.Boolean('--fixable', false, { description: 'only checks that carry an autofix, with the page that owns the invariant — `shipkit check --fix <id>` applies one' })
|
|
40
|
+
full = Option.Boolean('--full', false, { description: 'resolve every check the way a FULL-tier run resolves it' })
|
|
41
|
+
|
|
42
|
+
async execute(): Promise<number> {
|
|
43
|
+
const root = process.cwd()
|
|
44
|
+
const c = await loadRepoConfig(root)
|
|
45
|
+
const tier = this.full ? 'full' : envTier()
|
|
46
|
+
let rows = await gateInventory(root, c, { tier })
|
|
47
|
+
if (this.blocking) rows = rows.filter((r) => r.blocking !== false)
|
|
48
|
+
if (this.proactive) rows = rows.filter((r) => r.when.at !== 'always')
|
|
49
|
+
if (this.fixable) rows = rows.filter((r) => r.fixable)
|
|
50
|
+
|
|
51
|
+
if (this.json) {
|
|
52
|
+
this.context.stdout.write(`${JSON.stringify(rows.map((r) => ({ ...r, population: classify(r.decl).population })))}\n`)
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.md || this.out != null) {
|
|
57
|
+
const md = renderCheckTable({ name: c.name, rows })
|
|
58
|
+
if (this.out != null) {
|
|
59
|
+
const target = resolve(root, this.out)
|
|
60
|
+
writeFileSync(target, `${md.trimEnd()}\n`)
|
|
61
|
+
this.context.stdout.write(`ok: ${rows.length} check(s) written to ${this.out}\n`)
|
|
62
|
+
return 0
|
|
63
|
+
}
|
|
64
|
+
this.context.stdout.write(`${md}\n`)
|
|
65
|
+
return 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this.fixable) {
|
|
69
|
+
for (const l of renderFixableLines(rows)) this.context.stdout.write(`${l}\n`)
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// the SAME cells the markdown table is built from, so the terminal
|
|
74
|
+
// and the file can never disagree about a column
|
|
75
|
+
for (const l of renderCheckLines(rows)) this.context.stdout.write(`${l}\n`)
|
|
76
|
+
const undeclared = rows.filter((r) => { const cl = classify(r.decl); return cl.population === 'none' && cl.decided !== true }).length
|
|
77
|
+
if (undeclared > 0) this.context.stdout.write(` why each one: shipkit keys --burn-down\n`)
|
|
78
|
+
this.context.stdout.write(` the full table, refreshed by every gate run: STATUS.full.md\n`)
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `shipkit units`, the name 0.38 shipped for this inventory. Hidden from the
|
|
84
|
+
* help and the command index, it answers the same table with one WARN on
|
|
85
|
+
* stderr, so a `--json` consumer still parses stdout. It dies with the rest
|
|
86
|
+
* of the compatibility window at 0.40. */
|
|
87
|
+
export class UnitsAliasCommand extends ChecksCommand {
|
|
88
|
+
static override paths = [['units']]
|
|
89
|
+
static override usage = undefined
|
|
90
|
+
|
|
91
|
+
override async execute(): Promise<number> {
|
|
92
|
+
this.context.stderr.write('WARN shipkit units is now shipkit checks, the old name goes at 0.40\n')
|
|
93
|
+
return await super.execute()
|
|
94
|
+
}
|
|
95
|
+
}
|