rvlib-shipkit 0.38.1 → 1.0.1
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 +379 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/derived.ts +1 -1
- 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} +49 -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 +16 -3
- 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 +257 -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 +616 -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 +88 -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 +152 -189
- package/src/core/run-fix.ts +99 -0
- package/src/core/run-fixers.ts +64 -33
- package/src/core/self-update.ts +11 -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/fleet/scan-fleet.ts +16 -0
- package/src/hooks/leak-guard.ts +20 -9
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +38 -26
- 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 +157 -88
- 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 +9 -4
- 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/scan-md.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,164 @@
|
|
|
1
|
+
// The words a check page says about a declaration, and the link it puts on a
|
|
2
|
+
// file. Pure, so the sentences are pinned by tests rather than read off a
|
|
3
|
+
// screenshot.
|
|
4
|
+
import { CACHED, RAN } from '../../core/outcome-words.ts'
|
|
5
|
+
import { parseDuration } from '../../core/wire.ts'
|
|
6
|
+
import type { WhenAt } from './hub-types.ts'
|
|
7
|
+
import { spanWords } from './next-run.ts'
|
|
8
|
+
|
|
9
|
+
/** the cache population a check declared, and where it runs, as the page
|
|
10
|
+
* receives them */
|
|
11
|
+
export type CachePop = { inputs?: string[]; ttl?: string; never?: string; reads?: boolean; when?: WhenAt }
|
|
12
|
+
|
|
13
|
+
/** `a`, `a and b`, `a, b and c` */
|
|
14
|
+
const list = (xs: string[]): string => (xs.length < 2 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)!}`)
|
|
15
|
+
|
|
16
|
+
/** how often a check that remembers nothing runs: every time the thing
|
|
17
|
+
* it runs in runs */
|
|
18
|
+
const CADENCE: Record<WhenAt, string> = {
|
|
19
|
+
always: 'on every run',
|
|
20
|
+
release: 'on every release run',
|
|
21
|
+
health: 'on every daily pass',
|
|
22
|
+
off: 'nowhere, it is off',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** the value a page shows for the `remembered` row, the paths behind it when
|
|
26
|
+
* it is keyed on files, and the sentence for the row's help. The value is a
|
|
27
|
+
* RHYTHM a reader scans, and the declaration it comes from is in the help */
|
|
28
|
+
export type RememberedFact = { v: string; inputs: string[]; help: string }
|
|
29
|
+
|
|
30
|
+
export function rememberedFact(u: CachePop): RememberedFact {
|
|
31
|
+
const inputs = u.inputs ?? []
|
|
32
|
+
if (inputs.includes('.'))
|
|
33
|
+
return { v: 'until any file changes', inputs: [], help: `keyed on the whole repo tree: the answer is ${CACHED} until a file in the repo changes, then it runs again` }
|
|
34
|
+
if (inputs.length > 0)
|
|
35
|
+
return { v: 'until a file it reads changes', inputs, help: `keyed on ${list(inputs)}: the answer is ${CACHED} until a change under ${inputs.length > 1 ? 'them' : 'it'}, then it runs again` }
|
|
36
|
+
if (u.reads === true && inputs.length === 0 && (u.ttl == null || u.ttl === '') && u.never == null)
|
|
37
|
+
return { v: 'until a file it read changes', inputs: [], help: `keyed on what its last run read: the answer is ${CACHED} until one of those files, existence probes or listings changes, then it runs again. \`shipkit keys --why\` names them` }
|
|
38
|
+
if (u.ttl != null && u.ttl !== '') {
|
|
39
|
+
let every = u.ttl
|
|
40
|
+
try { every = spanWords(parseDuration(u.ttl)) } catch { /* an unparsable ttl is shown as written */ }
|
|
41
|
+
return {
|
|
42
|
+
v: `for ${every}`,
|
|
43
|
+
inputs: [],
|
|
44
|
+
help: `the answer is kept for ${u.ttl} (\`ttl\`), and every run inside that window uses it. A failing answer is kept for an hour at most, so a fix shows up soon`,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const cadence = CADENCE[u.when ?? 'always']
|
|
48
|
+
if (u.never != null)
|
|
49
|
+
return { v: 'never', inputs: [], help: `never cached, so it runs ${cadence}${u.never === '' ? '' : `: ${u.never}`}` }
|
|
50
|
+
return { v: 'never', inputs: [], help: `nothing declared, so it runs ${cadence}. \`inputs\`, \`ttl\` or \`never\` in the check says otherwise` }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** the four places a check runs, named by what they do. The config keeps its
|
|
54
|
+
* own words (`always`, `release`, `health`, `off`), and these are only what
|
|
55
|
+
* the select under `run when` shows */
|
|
56
|
+
export const WHEN_OPTION: Record<WhenAt, string> = {
|
|
57
|
+
always: 'always',
|
|
58
|
+
release: 'before a release',
|
|
59
|
+
health: 'health pass only',
|
|
60
|
+
off: 'never',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** what a check IS: three FACTS, each a short value a reader scans, with the
|
|
64
|
+
* sentence that explains it in the row's own `(?)`. Where it runs is the
|
|
65
|
+
* switch's own row, and what it runs is the command line above the grid */
|
|
66
|
+
export type WhatFacts = { from: 'shipkit' | 'repo'; build: boolean; blocking: boolean | null }
|
|
67
|
+
|
|
68
|
+
export type WhatFact = { k: string; v: string; help: string; tone?: 'red' | 'warn' }
|
|
69
|
+
|
|
70
|
+
export function whatFacts(u: WhatFacts): WhatFact[] {
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
k: 'from',
|
|
74
|
+
v: u.from === 'shipkit' ? 'shipkit' : 'this repo',
|
|
75
|
+
help: u.from === 'shipkit' ? 'the toolkit ships this check, and every repo that installs it gets the same one' : 'this repo declared it, in a file of its own that nothing outside the repo knows about',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
k: 'kind',
|
|
79
|
+
v: u.build ? 'build step' : 'check',
|
|
80
|
+
help: u.build
|
|
81
|
+
? 'it builds rather than reports, which is why the health pass leaves it out and the gate runs it'
|
|
82
|
+
: 'it reports on this repo or on the world rather than building anything, so both the gate and the health pass can ask it',
|
|
83
|
+
},
|
|
84
|
+
u.blocking === true
|
|
85
|
+
? { k: 'on failure', v: 'blocks', tone: 'red', help: 'its failure stops the gate, and stops you' }
|
|
86
|
+
: u.blocking === false
|
|
87
|
+
? { k: 'on failure', v: 'warns', tone: 'warn', help: 'it reports the WORLD rather than this repo, so a failure prints as a warning and never blocks' }
|
|
88
|
+
: { k: 'on failure', v: 'undeclared', tone: 'red', help: 'it never declared whether it blocks, which the runner treats as fatal rather than guessing' },
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** a check row in two parts: the id as a small tag, then the title. A check
|
|
93
|
+
* whose title IS its id (a floor slot) has no tag, the id is its name */
|
|
94
|
+
export function checkName(checkId: string, label: string): { tag: string | null; title: string } {
|
|
95
|
+
if (label.startsWith(`${checkId} `)) {
|
|
96
|
+
const title = label.slice(checkId.length + 1).trim()
|
|
97
|
+
if (title !== '') return { tag: checkId, title }
|
|
98
|
+
}
|
|
99
|
+
return { tag: null, title: label === '' ? checkId : label }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** a path as a reader wants it: short, with the full one for the tooltip */
|
|
103
|
+
export type ShortPath = { short: string; full: string }
|
|
104
|
+
|
|
105
|
+
const HOME = /^\/(?:Users|home)\/[^/]+\//
|
|
106
|
+
|
|
107
|
+
/** repo relative when it sits in the repo, `~/` when it sits in a home. What
|
|
108
|
+
* this returns still resolves from the repo root, so a command stays runnable */
|
|
109
|
+
export function repoPath(p: string, root: string): ShortPath {
|
|
110
|
+
const r = root.replace(/\/+$/, '')
|
|
111
|
+
if (r !== '' && p.startsWith(`${r}/`)) return { short: p.slice(r.length + 1), full: p }
|
|
112
|
+
return { short: p.replace(HOME, '~/'), full: p }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** a docs page: repo relative when it is in the repo, else relative to the
|
|
116
|
+
* wiki it belongs to (`meta/rv-conventions/package-json.md`) */
|
|
117
|
+
export function docPath(p: string, root: string): ShortPath {
|
|
118
|
+
const outside = p.startsWith('/') || p.startsWith('~/')
|
|
119
|
+
const r = root.replace(/\/+$/, '')
|
|
120
|
+
if (!outside || (r !== '' && p.startsWith(`${r}/`))) return repoPath(p, root)
|
|
121
|
+
const at = p.indexOf('/wiki/')
|
|
122
|
+
return at < 0 ? repoPath(p, root) : { short: p.slice(at + '/wiki/'.length), full: p }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** WHAT IT RUNS. A subprocess check is its argv, each token short and never
|
|
126
|
+
* split, plus the package.json script a floor slot resolved to. A check whose
|
|
127
|
+
* body is a function runs inside the gate, and its source file says what */
|
|
128
|
+
export type Command =
|
|
129
|
+
| { kind: 'argv'; tokens: ShortPath[]; copy: string; script: { name: string; body: string } | null }
|
|
130
|
+
| { kind: 'in-process'; file: string | null }
|
|
131
|
+
|
|
132
|
+
const quoted = (t: string): string => (/^[\w@%+=:,./~-]+$/.test(t) ? t : `'${t.replace(/'/g, `'\\''`)}'`)
|
|
133
|
+
|
|
134
|
+
export function commandOf(u: { cmd: string[] | null; file: string | null; script?: { name: string; body: string } | null }, root: string): Command {
|
|
135
|
+
if (u.cmd == null || u.cmd.length === 0) return { kind: 'in-process', file: u.file }
|
|
136
|
+
const tokens = u.cmd.map((t) => {
|
|
137
|
+
const s = t.startsWith('/') ? repoPath(t, root) : { short: t, full: t }
|
|
138
|
+
return { short: quoted(s.short), full: s.full }
|
|
139
|
+
})
|
|
140
|
+
return { kind: 'argv', tokens, copy: tokens.map((t) => t.short).join(' '), script: u.script ?? null }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** how the last run got its answer: it RAN, with a verdict when it did not
|
|
144
|
+
* pass, or its answer came back from the cache. Null when nothing is on
|
|
145
|
+
* record in this checkout */
|
|
146
|
+
export type LastRun = { word: string; verdict: string | null; tone: 'red' | 'warn' | 'quiet' | 'idle' }
|
|
147
|
+
|
|
148
|
+
export function lastRun(u: { status: 'pass' | 'warn' | 'fail' | 'skip' | null; replayed: boolean; advisory: boolean }): LastRun | null {
|
|
149
|
+
if (u.status == null) return null
|
|
150
|
+
if (u.replayed) return { word: CACHED, verdict: null, tone: 'quiet' }
|
|
151
|
+
if (u.status === 'fail') return { word: RAN, verdict: u.advisory ? 'failed, does not block' : 'failed', tone: u.advisory ? 'warn' : 'red' }
|
|
152
|
+
if (u.status === 'warn') return { word: RAN, verdict: 'warned', tone: 'warn' }
|
|
153
|
+
if (u.status === 'skip') return { word: RAN, verdict: 'skipped', tone: 'quiet' }
|
|
154
|
+
return { word: RAN, verdict: null, tone: 'idle' }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** his editor, on the file itself. The root comes from the server, because
|
|
158
|
+
* only it knows where this repo sits on disk */
|
|
159
|
+
export function editorHref(root: string, file: string | null | undefined): string | null {
|
|
160
|
+
if (file == null || file === '') return null
|
|
161
|
+
if (file.startsWith('/')) return `vscode://file${file}`
|
|
162
|
+
if (root === '') return null
|
|
163
|
+
return `vscode://file${root.startsWith('/') ? root : `/${root}`}/${file}`.replace(/([^:])\/\//g, '$1/')
|
|
164
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Gate checks, as the page sees them: what a RUNNING gate has already
|
|
2
|
+
// reported, laid over the rows the server last sent.
|
|
3
|
+
//
|
|
4
|
+
// The server is the truth (collections/checks.ts reads the inventory, the
|
|
5
|
+
// store and the run record), and this service holds one thing only: the
|
|
6
|
+
// results of a run happening RIGHT NOW, so a row recolours the moment its
|
|
7
|
+
// check finishes instead of on the next ten second poll. Everything here is
|
|
8
|
+
// dropped when the run ends and the next poll brings the same facts back
|
|
9
|
+
// from the one place that owns them.
|
|
10
|
+
import { makeAutoObservable, runInAction } from 'mobx'
|
|
11
|
+
import type { HubSt } from './hub-st.ts'
|
|
12
|
+
import { applyRunEvent, idleRun, startRun, stripText, type RunProgress } from './run-strip.ts'
|
|
13
|
+
import type { GateBlock, Resource, ResourceState, SlowSuite, CheckEvent, WhenAt } from './hub-types.ts'
|
|
14
|
+
import { WHEN_OPTION } from './check-words.ts'
|
|
15
|
+
|
|
16
|
+
/** the colour rule, the SAME one the server applies, because a row must not
|
|
17
|
+
* change meaning between a live result and the poll that confirms it */
|
|
18
|
+
const stateOf = (e: CheckEvent): ResourceState => {
|
|
19
|
+
if (e.status === 'fail') return e.advisory ? 'warn' : 'red'
|
|
20
|
+
if (e.status === 'warn') return 'warn'
|
|
21
|
+
if (e.status === 'skip') return 'quiet'
|
|
22
|
+
return e.replayed ? 'quiet' : 'ok'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** what a page keeps from a streaming run: enough to read what happened,
|
|
26
|
+
* bounded so a runaway check cannot grow it forever */
|
|
27
|
+
const MAX_LIVE_LINES = 400
|
|
28
|
+
|
|
29
|
+
export class ChecksSvc {
|
|
30
|
+
/** check id → what it just reported, for the run in flight */
|
|
31
|
+
live = new Map<string, CheckEvent>()
|
|
32
|
+
/** checks still to report, so a row can say it is waiting rather than
|
|
33
|
+
* showing the verdict of a run that is being replaced */
|
|
34
|
+
running = new Set<string>()
|
|
35
|
+
/** the run that is going, and what it was narrowed to */
|
|
36
|
+
runningAll = false
|
|
37
|
+
note = ''
|
|
38
|
+
/** the header's run state: how far this page's run has got, and how it ended */
|
|
39
|
+
progress: RunProgress = idleRun()
|
|
40
|
+
/** the wire lines of the run in flight, so a check page streams while it
|
|
41
|
+
* runs rather than showing nothing until the poll after it ends. They
|
|
42
|
+
* stay after the run: they ARE the last output until the next one */
|
|
43
|
+
liveLines: string[] = []
|
|
44
|
+
private clearTimer: ReturnType<typeof setTimeout> | null = null
|
|
45
|
+
/** the managed block in repo.config.ts as the server last sent it: the
|
|
46
|
+
* hash the next write hands back, and whether the file is waiting for
|
|
47
|
+
* a commit. null until the first read */
|
|
48
|
+
gate: GateBlock | null = null
|
|
49
|
+
gateNote = ''
|
|
50
|
+
gateBusy = false
|
|
51
|
+
/** the reason box, per check id: a check is only turned off WITH a reason */
|
|
52
|
+
reasons = new Map<string, string>()
|
|
53
|
+
/** checks whose select says never while the reason is still being typed:
|
|
54
|
+
* off is written only once it has one */
|
|
55
|
+
pendingOff = new Set<string>()
|
|
56
|
+
/** the slow suite form under the test check */
|
|
57
|
+
suite: SlowSuite = { file: '', catches: '', whyNotFast: '' }
|
|
58
|
+
|
|
59
|
+
constructor(private st: HubSt) {
|
|
60
|
+
makeAutoObservable<this, 'st' | 'clearTimer'>(this, { st: false, clearTimer: false })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get busy(): boolean {
|
|
64
|
+
return this.progress.phase === 'running'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** the one line the header shows, empty when this page runs nothing */
|
|
68
|
+
get strip(): string {
|
|
69
|
+
return stripText(this.progress)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** this check is expected to report in the run that is going */
|
|
73
|
+
isRunning(checkId: string): boolean {
|
|
74
|
+
return this.running.has(checkId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
reasonFor(id: string): string {
|
|
78
|
+
return this.reasons.get(id) ?? ''
|
|
79
|
+
}
|
|
80
|
+
setReason(id: string, v: string): void {
|
|
81
|
+
this.reasons.set(id, v)
|
|
82
|
+
}
|
|
83
|
+
setSuite(f: keyof SlowSuite, v: string): void {
|
|
84
|
+
this.suite = { ...this.suite, [f]: v }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** the block, read before anything is written: every write hands its hash
|
|
88
|
+
* back, which is what makes a concurrent edit a refusal rather than a
|
|
89
|
+
* silent overwrite */
|
|
90
|
+
async loadGate(): Promise<void> {
|
|
91
|
+
try {
|
|
92
|
+
const r = await fetch('/api/checks/when')
|
|
93
|
+
if (!r.ok) throw new Error(await r.text())
|
|
94
|
+
const b = (await r.json()) as GateBlock
|
|
95
|
+
runInAction(() => { this.gate = b })
|
|
96
|
+
} catch (e) {
|
|
97
|
+
runInAction(() => { this.gateNote = `the repo config could not be read: ${(e as Error).message}` })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** ONE write path for the switch, the slow suite form and the prose word
|
|
102
|
+
* lists alike: it edits repo.config.ts in place and leaves it dirty to be
|
|
103
|
+
* reviewed. Resolves true when the server took the edit */
|
|
104
|
+
async writeGate(edit: Record<string, unknown>, said: string): Promise<boolean> {
|
|
105
|
+
if (this.gateBusy) return false
|
|
106
|
+
let took = false
|
|
107
|
+
runInAction(() => { this.gateBusy = true; this.gateNote = 'writing repo.config.ts…' })
|
|
108
|
+
try {
|
|
109
|
+
// the hash of the file the page last read: without it a write
|
|
110
|
+
// could land on top of an edit this page never saw
|
|
111
|
+
if (this.gate == null) await this.loadGate()
|
|
112
|
+
const r = await fetch('/api/checks/when', { method: 'POST', body: JSON.stringify({ ...edit, expect: this.gate?.hash }) })
|
|
113
|
+
if (!r.ok) throw new Error(await r.text())
|
|
114
|
+
const b = (await r.json()) as GateBlock
|
|
115
|
+
runInAction(() => { this.gate = b; this.gateNote = said })
|
|
116
|
+
took = true
|
|
117
|
+
// the rows carry `when`, so they are right only after the server
|
|
118
|
+
// has read the config it just wrote
|
|
119
|
+
await this.st.cols.poll()
|
|
120
|
+
} catch (e) {
|
|
121
|
+
runInAction(() => { this.gateNote = `refused: ${(e as Error).message}` })
|
|
122
|
+
// whatever the file really says now, so the next try is not
|
|
123
|
+
// refused for a hash the page has already outgrown
|
|
124
|
+
await this.loadGate()
|
|
125
|
+
}
|
|
126
|
+
runInAction(() => { this.gateBusy = false })
|
|
127
|
+
return took
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** what the select shows: never while a reason is being typed, else
|
|
131
|
+
* where the check runs today */
|
|
132
|
+
whenShown(u: { checkId: string; when: WhenAt }): WhenAt {
|
|
133
|
+
return this.pendingOff.has(u.checkId) ? 'off' : u.when
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** the select moved. Every place but never writes at once, and never
|
|
137
|
+
* waits for its reason */
|
|
138
|
+
chooseWhen(checkId: string, at: WhenAt): void {
|
|
139
|
+
if (at === 'off') {
|
|
140
|
+
this.pendingOff.add(checkId)
|
|
141
|
+
this.gateNote = 'never needs a reason: type it, then turn it off'
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
this.pendingOff.delete(checkId)
|
|
145
|
+
void this.setWhen(checkId, at)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async setWhen(checkId: string, at: WhenAt): Promise<void> {
|
|
149
|
+
const reason = this.reasonFor(checkId).trim()
|
|
150
|
+
if (at === 'off' && reason === '') {
|
|
151
|
+
runInAction(() => { this.gateNote = 'never needs a reason: a check nobody can explain is a silenced gate' })
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
await this.writeGate({ when: { [checkId]: at === 'off' ? `off:${reason}` : at } }, `${checkId} is set to ${WHEN_OPTION[at]} — repo.config.ts is edited and waiting for your commit`)
|
|
155
|
+
runInAction(() => { this.pendingOff.delete(checkId) })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** back to whatever the check itself declares, by dropping the entry */
|
|
159
|
+
async clearWhen(checkId: string): Promise<void> {
|
|
160
|
+
await this.writeGate({ when: { [checkId]: null } }, `${checkId} is back to what it declares itself — repo.config.ts is edited and waiting for your commit`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async addSlow(): Promise<void> {
|
|
164
|
+
const s = this.suite
|
|
165
|
+
await this.writeGate({ slow: [{ ...s }] }, `${s.file} is a slow suite now — the quick tier leaves it out, the full tier keeps it`)
|
|
166
|
+
runInAction(() => { this.suite = { file: '', catches: '', whyNotFast: '' } })
|
|
167
|
+
}
|
|
168
|
+
async removeSlow(file: string): Promise<void> {
|
|
169
|
+
await this.writeGate({ slowRemove: [file] }, `${file} is back in the quick tier`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private pushLine(text: string): void {
|
|
173
|
+
this.liveLines.push(text)
|
|
174
|
+
// one runaway check must not grow the page without a bound
|
|
175
|
+
if (this.liveLines.length > MAX_LIVE_LINES) this.liveLines.shift()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
apply(e: CheckEvent): void {
|
|
179
|
+
this.live.set(e.id, e)
|
|
180
|
+
// the wait set is keyed by the id `--only` takes, which is the id the
|
|
181
|
+
// stream sends back: a row stops pulsing the moment ITS check lands
|
|
182
|
+
this.running.delete(e.id)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** the resource list with the live results over it */
|
|
186
|
+
overlay(rs: Resource[]): Resource[] {
|
|
187
|
+
if (this.live.size === 0) return rs
|
|
188
|
+
return rs.map((r) => {
|
|
189
|
+
const u = r.check
|
|
190
|
+
if (u == null) return r
|
|
191
|
+
const e = this.live.get(u.checkId)
|
|
192
|
+
if (e == null) return r
|
|
193
|
+
return {
|
|
194
|
+
...r,
|
|
195
|
+
state: stateOf(e),
|
|
196
|
+
severity: e.status === 'fail' ? (e.advisory ? 'yellow' : 'red') : e.status === 'warn' ? 'yellow' : undefined,
|
|
197
|
+
check: { ...u, status: e.status, replayed: e.replayed, lastMs: e.replayed ? null : Math.round(e.ms), lastAt: Date.now(), advisory: e.advisory },
|
|
198
|
+
md: e.lines.length === 0 ? r.md : `\`\`\`\n${e.lines.map((l) => (l.kind === 'fix-fail' || l.kind === 'fix-warn' ? ` fix: ${l.text}` : l.kind === 'fail' ? `FAIL: ${l.text}` : l.kind === 'warn' ? `WARN ${l.text}` : l.kind === 'skip' ? `skip: ${l.text}` : `ok: ${l.text}`)).join('\n')}\n\`\`\``,
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** every check this run will hear from, so a row can pulse from the first
|
|
204
|
+
* second and the header can say "12 of 70" rather than "12" */
|
|
205
|
+
private expected(only: string[] | undefined, gate: boolean): string[] {
|
|
206
|
+
if (only != null) return only
|
|
207
|
+
const checks = this.st.cols.fetched.map((r) => r.check).filter((u) => u != null)
|
|
208
|
+
return checks.filter((u) => gate || !u.build).map((u) => u.checkId)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** run the whole health pass, the whole gate, or a named set of checks.
|
|
212
|
+
* The stream is NDJSON, one event per line: wire lines feed the raw
|
|
213
|
+
* card, check events colour the rows and move the header state. The page never
|
|
214
|
+
* shells anything, and there is no second run mechanism. */
|
|
215
|
+
async run(only?: string[], o: { gate?: boolean; label?: string } = {}): Promise<void> {
|
|
216
|
+
if (this.busy) return
|
|
217
|
+
const gate = o.gate === true
|
|
218
|
+
const waiting = this.expected(only, gate)
|
|
219
|
+
const label = o.label ?? (only == null ? (gate ? 'the gate' : 'the health pass') : only.length === 1 ? only[0]! : `${only.length} checks`)
|
|
220
|
+
if (this.clearTimer != null) clearTimeout(this.clearTimer)
|
|
221
|
+
runInAction(() => {
|
|
222
|
+
this.live.clear()
|
|
223
|
+
this.liveLines = []
|
|
224
|
+
this.running = new Set(waiting)
|
|
225
|
+
this.runningAll = only == null
|
|
226
|
+
this.progress = startRun(label, waiting.length)
|
|
227
|
+
this.note = `running ${label}…`
|
|
228
|
+
})
|
|
229
|
+
try {
|
|
230
|
+
const r = await fetch('/api/check/run', { method: 'POST', body: JSON.stringify({ ...(only == null ? {} : { only }), ...(gate ? { gate } : {}) }) })
|
|
231
|
+
const rd = r.body!.getReader()
|
|
232
|
+
const dec = new TextDecoder()
|
|
233
|
+
let buf = ''
|
|
234
|
+
for (;;) {
|
|
235
|
+
const { done, value } = await rd.read()
|
|
236
|
+
if (done) break
|
|
237
|
+
buf += dec.decode(value, { stream: true })
|
|
238
|
+
const lines = buf.split('\n')
|
|
239
|
+
// the tail is whatever arrived half written; it completes on
|
|
240
|
+
// the next chunk
|
|
241
|
+
buf = lines.pop() ?? ''
|
|
242
|
+
for (const l of lines) {
|
|
243
|
+
if (l === '') continue
|
|
244
|
+
let e: { t?: string; text?: string; code?: number } & Partial<CheckEvent>
|
|
245
|
+
try { e = JSON.parse(l) as typeof e } catch { continue }
|
|
246
|
+
runInAction(() => { this.progress = applyRunEvent(this.progress, e as Record<string, unknown>) })
|
|
247
|
+
if (e.t === 'line' && e.text != null) {
|
|
248
|
+
runInAction(() => this.pushLine(e.text!))
|
|
249
|
+
this.st.health.appendLine(e.text)
|
|
250
|
+
} else if (e.t === 'check') {
|
|
251
|
+
runInAction(() => this.apply(e as unknown as CheckEvent))
|
|
252
|
+
} else if (e.t === 'done') {
|
|
253
|
+
runInAction(() => { this.note = e.code === 0 ? 'done, green' : `done, exit ${e.code ?? '?'}` })
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
runInAction(() => { this.note = `stream broke: ${(e as Error).message}` })
|
|
259
|
+
this.st.health.appendLine(`FAIL: stream broke: ${(e as Error).message}`)
|
|
260
|
+
}
|
|
261
|
+
runInAction(() => {
|
|
262
|
+
this.running.clear()
|
|
263
|
+
this.runningAll = false
|
|
264
|
+
// a stream that died mid way still ends the run: the header must
|
|
265
|
+
// never sit there claiming something is running
|
|
266
|
+
if (this.progress.phase === 'running') this.progress = { ...this.progress, phase: 'done' }
|
|
267
|
+
})
|
|
268
|
+
// the header state clears itself, because the rows ARE the answer once the
|
|
269
|
+
// run is over and a banner that stays is a banner nobody reads
|
|
270
|
+
this.clearTimer = setTimeout(() => runInAction(() => { this.progress = idleRun() }), 5_000)
|
|
271
|
+
// the numbers a run just produced live on the server, in the log it
|
|
272
|
+
// wrote: one poll and the page is reading them rather than a memory
|
|
273
|
+
await this.st.cols.poll()
|
|
274
|
+
void this.st.health.load()
|
|
275
|
+
void this.st.timeline.load()
|
|
276
|
+
// a test page on screen reads the record the run just wrote
|
|
277
|
+
const sel = this.st.sel
|
|
278
|
+
if (sel?.t === 'tests') void this.st.tests.load(sel.check)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// THE clock: one observable second, for every age on the page.
|
|
2
|
+
//
|
|
3
|
+
// An age rendered once is frozen text, and a component that owns its own
|
|
4
|
+
// timer is one timer per age. This is one interval on the root store, and
|
|
5
|
+
// only the small components that READ `now` repaint when it moves. Nothing
|
|
6
|
+
// else on the page observes it.
|
|
7
|
+
import { makeAutoObservable, runInAction } from 'mobx'
|
|
8
|
+
|
|
9
|
+
export class ClockSvc {
|
|
10
|
+
now = Date.now()
|
|
11
|
+
/** the live interval, or null. A second start is a no-op rather than a
|
|
12
|
+
* second timer, because boot runs under StrictMode twice in dev */
|
|
13
|
+
running: ReturnType<typeof setInterval> | null = null
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
makeAutoObservable(this)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
tick(at = Date.now()): void {
|
|
20
|
+
this.now = at
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
start(everyMs = 1_000): void {
|
|
24
|
+
if (this.running != null) return
|
|
25
|
+
this.running = setInterval(() => runInAction(() => this.tick()), everyMs)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
stop(): void {
|
|
29
|
+
if (this.running == null) return
|
|
30
|
+
clearInterval(this.running)
|
|
31
|
+
this.running = null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
// Collection views: the per-collection card grid (ColGrid), the item card
|
|
2
|
-
// (asks · creds · posts), the
|
|
3
|
-
// /graph/* page itself), and the health/status card with the streaming
|
|
2
|
+
// (asks · creds · posts), and the health/status card with the streaming
|
|
4
3
|
// check runner. Every verb posts to /api/act/* and lands in a git-tracked
|
|
5
4
|
// truth file; the truth chip names the file the item IS (on disagreement
|
|
6
5
|
// the file wins).
|
|
7
6
|
import { runInAction } from 'mobx'
|
|
8
7
|
import { observer } from 'mobx-react-lite'
|
|
9
8
|
import type { HubSt } from './hub-st.ts'
|
|
10
|
-
import type {
|
|
9
|
+
import type { Resource } from './hub-types.ts'
|
|
11
10
|
import { renderMd } from './md.ts'
|
|
12
11
|
|
|
13
|
-
// creds carry no truth file
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
12
|
+
// creds carry no truth file: the name is the id, the words after it are the
|
|
13
|
+
// service and what its probe last said, and the slot says present or missing
|
|
14
|
+
const credOf = (i: Resource): { name: string; service: string | null; missing: boolean; state: Resource['state'] } => {
|
|
15
|
+
const name = i.id.replace(/^cred:/, '')
|
|
16
|
+
const rest = i.text.startsWith(name) ? i.text.slice(name.length).trim() : i.text
|
|
17
|
+
return { name, service: rest !== '' ? rest : null, missing: i.slot === 'missing', state: i.state }
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
const CredHead = ({ cred }: { cred: ReturnType<typeof credOf> }) => (
|
|
21
21
|
<>
|
|
22
|
-
<span className=
|
|
22
|
+
<span className={`credname tl ${cred.state}`}>{cred.name}</span>
|
|
23
23
|
<span className={`stchip ${cred.missing ? 'miss' : 'ok'}`}>{cred.missing ? 'missing' : 'present'}</span>
|
|
24
24
|
</>
|
|
25
25
|
)
|
|
@@ -37,9 +37,8 @@ export const ColCard = observer(({ app, col, id }: { app: HubSt; col: string; id
|
|
|
37
37
|
const cred = credOf(i)
|
|
38
38
|
return (
|
|
39
39
|
<div className="colcard">
|
|
40
|
-
<div className="back" onClick={() => app.showCol(col)}>← {c.colTitle(col)}</div>
|
|
41
40
|
<h3>
|
|
42
|
-
<
|
|
41
|
+
<CredHead cred={cred} />
|
|
43
42
|
{i.artifact?.url != null ? <a className="mint" href={i.artifact.url} target="_blank" rel="noreferrer">mint ↗</a> : null}
|
|
44
43
|
</h3>
|
|
45
44
|
{cred.service != null ? <div className="creddesc">{cred.service}</div> : null}
|
|
@@ -58,9 +57,8 @@ export const ColCard = observer(({ app, col, id }: { app: HubSt; col: string; id
|
|
|
58
57
|
}
|
|
59
58
|
return (
|
|
60
59
|
<div className="colcard">
|
|
61
|
-
<div className="back" onClick={() => app.showCol(col)}>← {c.colTitle(col)}</div>
|
|
62
60
|
<h3>
|
|
63
|
-
<span className={`
|
|
61
|
+
<span className={`tl ${i.state}`}>{i.text}</span>
|
|
64
62
|
{i.file != null ? <span className="truth">{i.file}</span> : null}
|
|
65
63
|
</h3>
|
|
66
64
|
{i.md != null ? <div className="md" dangerouslySetInnerHTML={{ __html: renderMd(i.md) }} /> : null}
|
|
@@ -120,7 +118,7 @@ export const ColGrid = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
|
120
118
|
const cred = credOf(i)
|
|
121
119
|
return (
|
|
122
120
|
<div key={i.id} className="cgcard" onClick={() => c.select(col.id, i.id)}>
|
|
123
|
-
<div className="cgtop"><
|
|
121
|
+
<div className="cgtop"><CredHead cred={cred} /></div>
|
|
124
122
|
{cred.service != null ? <div className="creddesc">{cred.service}</div> : null}
|
|
125
123
|
{i.md != null ? <div className="md clamp" dangerouslySetInnerHTML={{ __html: renderMd(i.md) }} /> : null}
|
|
126
124
|
{i.artifact?.url != null ? (
|
|
@@ -131,7 +129,7 @@ export const ColGrid = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
|
131
129
|
}
|
|
132
130
|
return (
|
|
133
131
|
<div key={i.id} className="cgcard" onClick={() => c.select(col.id, i.id)}>
|
|
134
|
-
<div className="cgtop"><
|
|
132
|
+
<div className="cgtop"><b className={`tl ${i.state}`}>{i.text}</b></div>
|
|
135
133
|
{/* a link click opens its tab — it must not ALSO yank into the item view */}
|
|
136
134
|
{i.md != null ? <div className="md clamp" onClick={(e) => { if ((e.target as HTMLElement).tagName === 'A') e.stopPropagation() }} dangerouslySetInnerHTML={{ __html: renderMd(i.md) }} /> : null}
|
|
137
135
|
{i.file != null ? <span className="truth">{i.file}</span> : null}
|
|
@@ -146,23 +144,6 @@ export const ColGrid = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
|
146
144
|
})
|
|
147
145
|
|
|
148
146
|
// graph item view — the graph ITSELF fills the stage, lint line as a slim header
|
|
149
|
-
export const GraphView = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
150
|
-
const c = app.cols
|
|
151
|
-
const i = c.find('graphs', id)
|
|
152
|
-
if (i == null) return <div className="stagewrap"><div className="ph">graph vanished — the truth moved on</div></div>
|
|
153
|
-
if (i.href == null) return <div className="stagewrap"><div className="ph">this graph carries no page to embed</div></div>
|
|
154
|
-
return (
|
|
155
|
-
<div className="graphview">
|
|
156
|
-
<div className="gvhead">
|
|
157
|
-
<span className="back" onClick={() => app.showCol('graphs')}>← {c.colTitle('graphs')}</span>
|
|
158
|
-
<span className={`cdot ${i.severity ?? ''}`} />
|
|
159
|
-
<span className="gvtext">{i.text}</span>
|
|
160
|
-
</div>
|
|
161
|
-
<iframe src={i.href} title={i.text} />
|
|
162
|
-
</div>
|
|
163
|
-
)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
147
|
const wireClass = (l: string): string => {
|
|
167
148
|
const t = l.trimStart()
|
|
168
149
|
if (t.startsWith('FAIL') || t.startsWith('✗')) return 'fail'
|
|
@@ -172,19 +153,22 @@ const wireClass = (l: string): string => {
|
|
|
172
153
|
return ''
|
|
173
154
|
}
|
|
174
155
|
|
|
156
|
+
// the RAW view of a run: every check has its own page, and this is the one
|
|
157
|
+
// node that keeps the whole text reachable
|
|
175
158
|
export const StatusCard = observer(({ app }: { app: HubSt }) => {
|
|
176
159
|
const h = app.health
|
|
160
|
+
const body = h.live.length > 0
|
|
161
|
+
? h.live.join('\n')
|
|
162
|
+
: h.statusMd ?? 'no STATUS.md yet — hit "run the health pass" (or run: shipkit check)'
|
|
177
163
|
return (
|
|
178
164
|
<div className="wrap">
|
|
179
|
-
<h1>{app.name}<small>STATUS.md</small></h1>
|
|
165
|
+
<h1>{app.name}<small>{h.live.length > 0 ? 'this run' : 'STATUS.md'}</small></h1>
|
|
180
166
|
<div className="hbar">
|
|
181
|
-
<button disabled={h.running} onClick={() => void h.
|
|
182
|
-
<span className="note">{h.note !== '' ? h.note : <>
|
|
167
|
+
<button disabled={h.running} onClick={() => void h.runHealthPass()}>run the health pass</button>
|
|
168
|
+
<span className="note">{h.note !== '' ? h.note : <>runs every check but the build steps, in the hub's own process, and each row colours as its check finishes</>}</span>
|
|
183
169
|
</div>
|
|
184
170
|
<pre>
|
|
185
|
-
{(
|
|
186
|
-
.split('\n')
|
|
187
|
-
.map((l, i) => <span key={i} className={wireClass(l)}>{l}{'\n'}</span>)}
|
|
171
|
+
{body.split('\n').map((l, i) => <span key={i} className={wireClass(l)}>{l}{'\n'}</span>)}
|
|
188
172
|
</pre>
|
|
189
173
|
</div>
|
|
190
174
|
)
|