rvlib-shipkit 1.1.1 → 1.3.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 +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
// the ban — a green line must never teach the wrong rule. The remedy for
|
|
9
9
|
// pushed history is the owner's decision, never an automatic rewrite; only
|
|
10
10
|
// an unpushed tip commit may be amended, and the fails say so.
|
|
11
|
+
import { createHash } from 'crypto'
|
|
11
12
|
import { readFileSync } from 'fs'
|
|
12
13
|
import { join } from 'path'
|
|
13
14
|
import { hasAiAttribution, hasDashPunctuation } from '../hooks/commit-message.ts'
|
|
14
15
|
import { nestedIn } from '../core/nesting.ts'
|
|
15
|
-
import {
|
|
16
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
17
|
+
import { fileStamp } from '../core/file-stamp.ts'
|
|
18
|
+
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
19
|
+
import { check, type CheckEnv } from '../check.ts'
|
|
20
|
+
import { declaredVisibility } from '../core/repo-facts.ts'
|
|
16
21
|
|
|
17
22
|
// the documented silencer: .claude/settings.json attribution.commit/.pr both
|
|
18
23
|
// empty — the harness then never writes the trailer or the PR footer
|
|
@@ -25,20 +30,39 @@ const attributionSilenced = (root: string): boolean => {
|
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
/** what the answer depends on: the declared visibility, and for a public
|
|
34
|
+
* repo the commit HEAD names, every ref name and the silencer. So a commit,
|
|
35
|
+
* a fetch or a new tag asks again, and nothing else does */
|
|
36
|
+
export function historyKey(env: CheckEnv): string {
|
|
37
|
+
const declared = declaredVisibility(env.config)
|
|
38
|
+
const nested = nestedIn(env.root)
|
|
39
|
+
if (nested != null) return `nested in ${nested}`
|
|
40
|
+
if (declared !== 'public') return `declared ${declared}`
|
|
41
|
+
const git = (args: string[]): string => {
|
|
42
|
+
const p = Bun.spawnSync([gitBin(), ...args], { cwd: env.root, stdout: 'pipe', stderr: 'pipe' })
|
|
43
|
+
return p.exitCode === 0 ? p.stdout.toString() : `exit ${p.exitCode}`
|
|
44
|
+
}
|
|
45
|
+
const refs = git(['for-each-ref', '--format=%(refname) %(objectname)', 'refs/tags', 'refs/heads', 'refs/remotes'])
|
|
46
|
+
const head = git(['rev-parse', 'HEAD'])
|
|
47
|
+
const digest = createHash('sha256').update(head).update('\0').update(refs).digest('hex').slice(0, 16)
|
|
48
|
+
return `public|refs=${digest}|settings=${fileStamp(join(env.root, '.claude', 'settings.json'))}`
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
export const commitAiTells = check({
|
|
29
52
|
id: 'SK009',
|
|
30
53
|
blocking: true,
|
|
31
54
|
title: 'commit ai tells',
|
|
32
55
|
description: 'the history of a public repo carries no AI attribution and no dash punctuation',
|
|
33
56
|
owner: 'identity/how-we-commit.md',
|
|
34
|
-
// it scans the full `git log` and every ref name
|
|
35
|
-
//
|
|
36
|
-
|
|
57
|
+
// it scans the full `git log` and every ref name, which no tree id
|
|
58
|
+
// describes, so the refs themselves are the key
|
|
59
|
+
inputs: [CONFIG_FILE],
|
|
60
|
+
cacheKey: historyKey,
|
|
37
61
|
run: async (c) => {
|
|
38
62
|
const nested = nestedIn(c.root)
|
|
39
63
|
if (nested != null)
|
|
40
64
|
return void c.lines.push({ kind: 'skip', text: `commit history not scanned, nested in ${nested}: that history belongs to the parent repo` })
|
|
41
|
-
const declared = c.config
|
|
65
|
+
const declared = declaredVisibility(c.config)
|
|
42
66
|
if (declared !== 'public')
|
|
43
67
|
return c.ok(`declared ${declared} — the no-AI-attribution ban still applies from commit one (identity/how-we-commit.md); the full-history scan arms at visibility: 'public'`)
|
|
44
68
|
// %x1f separates hash from body, %x1e terminates the record — commit
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { isAbsolute, join } from 'path'
|
|
16
16
|
import { inferBin } from '../core/canonical-scripts.ts'
|
|
17
17
|
import { denyRules, RAW_RUNNERS } from '../core/one-way-in-policy.ts'
|
|
18
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
18
19
|
import type { RepoConfig } from '../repo-config.ts'
|
|
19
20
|
import { check, type CheckCtx, type FixResult } from '../check.ts'
|
|
20
21
|
|
|
@@ -95,8 +96,8 @@ async function guardScriptMissing(d: Disk, command: string): Promise<string | nu
|
|
|
95
96
|
return null // a form this rule cannot resolve is not a form it may condemn
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
/** the guard the fixer writes: the cli path the scripts run when a
|
|
99
|
-
* beside it
|
|
99
|
+
/** the guard the fixer writes: the shipkit cli path the scripts run when a
|
|
100
|
+
* guard sits beside it, else the
|
|
100
101
|
* repo's own dependency, else the copy of shipkit doing the fixing, which is
|
|
101
102
|
* the one the launcher falls back to in a repo that has none */
|
|
102
103
|
export async function guardCommandFor(d: Disk): Promise<string> {
|
|
@@ -109,6 +110,11 @@ export async function guardCommandFor(d: Disk): Promise<string> {
|
|
|
109
110
|
return guardCommand(join(import.meta.dir, '..', 'cli.ts'))
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
/** a package published to npm is worked on by strangers: this file binds
|
|
114
|
+
* their agents, and a deny list with a hook would tie them to a toolkit they
|
|
115
|
+
* never installed */
|
|
116
|
+
const PUBLIC_SKIP = `the root unit publishes to npm: ${SETTINGS_PATH} belongs to every contributor, so it is never written`
|
|
117
|
+
|
|
112
118
|
export const oneWayInPolicy = check({
|
|
113
119
|
id: 'SK013',
|
|
114
120
|
blocking: true,
|
|
@@ -119,6 +125,7 @@ export const oneWayInPolicy = check({
|
|
|
119
125
|
// the declared guard script resolves. A guard at an absolute path lives
|
|
120
126
|
// outside the repo, so an answer that probed one is never stored
|
|
121
127
|
run: async (c) => {
|
|
128
|
+
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
122
129
|
const d = c
|
|
123
130
|
const want = denyRules(exemptSlots(c.config))
|
|
124
131
|
if (want.length === 0) return c.ok('every governed slot is exempt (repo.config oneWayIn/defaults) — nothing to deny')
|
|
@@ -165,6 +172,7 @@ export const oneWayInPolicy = check({
|
|
|
165
172
|
// a repo's own allow list and its own hooks, and clobbering those to fix a
|
|
166
173
|
// deny list would cost more than the finding it repairs
|
|
167
174
|
fix: async (c): Promise<FixResult> => {
|
|
175
|
+
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
168
176
|
const d = c
|
|
169
177
|
const want = denyRules(exemptSlots(c.config))
|
|
170
178
|
if (want.length === 0) return { changed: false, summary: 'every governed slot is exempt — nothing to write' }
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// sites that write history refuse on the disk fact either way (core/nesting.ts),
|
|
4
4
|
// so this rule is about the declaration telling the truth to the next reader.
|
|
5
5
|
import { nestedIn } from '../core/nesting.ts'
|
|
6
|
+
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
6
7
|
import { check } from '../check.ts'
|
|
7
8
|
|
|
8
9
|
export const nestedRepo = check({
|
|
@@ -11,7 +12,10 @@ export const nestedRepo = check({
|
|
|
11
12
|
title: 'nested repo',
|
|
12
13
|
description: 'a config inside a larger work tree declares `nested: true`, and only there',
|
|
13
14
|
owner: 'build/shipkit/config-keys.md',
|
|
14
|
-
|
|
15
|
+
// the declaration, and where the git work tree root sits, which no tree
|
|
16
|
+
// id describes, so its path is the rest of the key
|
|
17
|
+
inputs: [CONFIG_FILE],
|
|
18
|
+
cacheKey: (env) => `nested=${env.config.nested === true}|top=${nestedIn(env.root) ?? 'root'}`,
|
|
15
19
|
run: (c) => {
|
|
16
20
|
const top = nestedIn(c.root)
|
|
17
21
|
const declared = c.config.nested === true
|
|
@@ -11,6 +11,7 @@ import { basename } from 'path'
|
|
|
11
11
|
import { check } from '../check.ts'
|
|
12
12
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
13
13
|
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
14
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
14
15
|
import { whenOfTask } from '../core/when.ts'
|
|
15
16
|
import { hubProblemMatcher } from '../problems/problem-line.ts'
|
|
16
17
|
import { indentAt, parseSpans, plain, removalSpan, type Node } from '../scaffold/jsonc-spans.ts'
|
|
@@ -165,6 +166,10 @@ function ownTasks(c: CheckCtx): { text: string } | 'none' | 'linked' {
|
|
|
165
166
|
return text == null ? 'none' : { text }
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/** a package published to npm is worked on by strangers, whose editor
|
|
170
|
+
* runs these tasks */
|
|
171
|
+
const PUBLIC_SKIP = `the root unit publishes to npm: ${TASKS} belongs to every contributor, so it is never rewritten`
|
|
172
|
+
|
|
168
173
|
export const vscodeHubTask = check({
|
|
169
174
|
id: 'SK017',
|
|
170
175
|
// a stale editor file is behind, not broken: it warns, and the fix pass
|
|
@@ -174,6 +179,7 @@ export const vscodeHubTask = check({
|
|
|
174
179
|
description: 'the vscode hub task carries the problem matcher and no extra tsc watcher runs',
|
|
175
180
|
owner: 'build/vscode-tasks.md',
|
|
176
181
|
run: (c) => {
|
|
182
|
+
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
177
183
|
const own = ownTasks(c)
|
|
178
184
|
if (own === 'none') return c.ok(`no ${TASKS}, so there is no hub task to wire`)
|
|
179
185
|
if (own === 'linked') return c.ok(`.vscode/ is a link to a shared dir, left alone`)
|
|
@@ -185,6 +191,7 @@ export const vscodeHubTask = check({
|
|
|
185
191
|
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
192
|
},
|
|
187
193
|
fix: (c) => {
|
|
194
|
+
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
188
195
|
const own = ownTasks(c)
|
|
189
196
|
if (typeof own === 'string') return { changed: false, summary: `no ${TASKS} to repair` }
|
|
190
197
|
const r = repairTasks(own.text, { typecheckOn: typecheckOn(c) })
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SK022 machine runs the pin (build/shipkit.md): on a machine with a house
|
|
2
|
+
// every shipkit runs the house copy, whatever a repo installed, so a repo's
|
|
3
|
+
// rvlib-shipkit pin is a minimum. It warns when the repo asks for a version
|
|
4
|
+
// newer than the one this process runs: a rule or an api that pin relies on
|
|
5
|
+
// is absent here until the house copy catches up.
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { LIB_VERSION } from '../core/lib-version.ts'
|
|
8
|
+
import { fileStamp } from '../core/file-stamp.ts'
|
|
9
|
+
import { check } from '../check.ts'
|
|
10
|
+
|
|
11
|
+
type Pkg = { version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
12
|
+
|
|
13
|
+
const NAME = 'rvlib-shipkit'
|
|
14
|
+
|
|
15
|
+
/** the lowest version a semver range admits (`^0.30.1` → 0.30.1), null when
|
|
16
|
+
* it names no version (link:, workspace:*, a tag) */
|
|
17
|
+
export function pinFloor(spec: string): string | null {
|
|
18
|
+
const m = /^\s*(?:\^|~|>=|=|v)?\s*(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(spec)
|
|
19
|
+
return m?.[1] ?? null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const machineRunsThePin = check({
|
|
23
|
+
id: 'SK022',
|
|
24
|
+
blocking: false,
|
|
25
|
+
title: 'machine runs the pin',
|
|
26
|
+
description: 'the shipkit this machine runs is at least the version this repo pins',
|
|
27
|
+
owner: 'build/shipkit.md',
|
|
28
|
+
// the pin, the install (gitignored) and the version this machine runs,
|
|
29
|
+
// which is no file of the repo
|
|
30
|
+
inputs: ['package.json'],
|
|
31
|
+
cacheKey: (env) => `${LIB_VERSION}|installed=${fileStamp(join(env.root, 'node_modules', NAME, 'package.json'))}`,
|
|
32
|
+
run: (c) => {
|
|
33
|
+
const pkg = c.json<Pkg>('package.json')
|
|
34
|
+
const spec = pkg?.devDependencies?.[NAME] ?? pkg?.dependencies?.[NAME]
|
|
35
|
+
if (spec == null) return c.ok(`no ${NAME} pin, this machine runs ${LIB_VERSION}`)
|
|
36
|
+
const wants: Array<{ v: string; from: string }> = []
|
|
37
|
+
const floor = pinFloor(spec)
|
|
38
|
+
if (floor != null) wants.push({ v: floor, from: `package.json pins ${spec}` })
|
|
39
|
+
const installed = c.json<Pkg>(`node_modules/${NAME}/package.json`)?.version
|
|
40
|
+
if (installed != null) wants.push({ v: installed, from: `node_modules has ${installed}` })
|
|
41
|
+
const ahead = wants.filter((w) => Bun.semver.order(w.v, LIB_VERSION) > 0)
|
|
42
|
+
if (ahead.length === 0) return c.ok(`${NAME} ${spec}: this machine runs ${LIB_VERSION}, at least that`)
|
|
43
|
+
c.warn(`${NAME}: ${ahead.map((w) => w.from).join(', ')}, newer than the ${LIB_VERSION} this machine runs. Update the house copy (git pull in the repo that hosts the house wiki), or run this repo's own install: bun node_modules/${NAME}/src/cli.ts`)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export default machineRunsThePin
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// SK024 identity colors (core/identity.ts owns the derivation): repo.config.ts
|
|
2
|
+
// declares `identity: { colorLight, colorDark }`, two #rrggbb hex, so every
|
|
3
|
+
// surface that shows the repo (the home page header, its rail tile) draws it
|
|
4
|
+
// in its own colour. The fix derives them from the repo icon, or from a hue
|
|
5
|
+
// hashed from the name, and writes that one key, every other byte kept.
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { check } from '../check.ts'
|
|
8
|
+
import { fileStamp } from '../core/file-stamp.ts'
|
|
9
|
+
import { configName, CONFIG_FILE, deriveIdentity, identityRefusal, INSTALLED_CONFIG_SRC } from '../core/identity.ts'
|
|
10
|
+
import { declaresIdentity, identityOfText, identityProblem, setIdentityText } from '../core/identity-color.ts'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const FIX = 'shipkit check --fix SK024 derives them from the repo icon, or from its name'
|
|
14
|
+
|
|
15
|
+
export const identityColors = check({
|
|
16
|
+
id: 'SK024',
|
|
17
|
+
blocking: false,
|
|
18
|
+
title: 'identity colors',
|
|
19
|
+
description: 'repo.config.ts declares identity colours: colorLight and colorDark, two hex',
|
|
20
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
21
|
+
// the config, and the installed lib that says whether it knows the key
|
|
22
|
+
inputs: [CONFIG_FILE],
|
|
23
|
+
cacheKey: (env) => `installed=${fileStamp(join(env.root, INSTALLED_CONFIG_SRC))}`,
|
|
24
|
+
run: (c) => {
|
|
25
|
+
const text = c.read(CONFIG_FILE)
|
|
26
|
+
if (text == null) return c.note(`no ${CONFIG_FILE}`)
|
|
27
|
+
const declared = declaresIdentity(text)
|
|
28
|
+
const id = identityOfText(text)
|
|
29
|
+
const why = id == null ? (declared ? 'identity is declared but its two colours cannot be read as #rrggbb hex' : 'no identity colours') : identityProblem(id)
|
|
30
|
+
const refused = why == null ? null : identityRefusal(c.read(INSTALLED_CONFIG_SRC))
|
|
31
|
+
if (refused != null) return c.note(`${CONFIG_FILE}: ${why}, and ${refused}`)
|
|
32
|
+
if (why != null) return c.warn(`${CONFIG_FILE}: ${why}`, FIX)
|
|
33
|
+
c.ok(`identity ${id!.colorLight} on light, ${id!.colorDark} on dark`)
|
|
34
|
+
},
|
|
35
|
+
fix: async (c) => {
|
|
36
|
+
const text = c.read(CONFIG_FILE)
|
|
37
|
+
if (text == null) return { changed: false, summary: `no ${CONFIG_FILE}` }
|
|
38
|
+
const have = identityOfText(text)
|
|
39
|
+
if (have != null && identityProblem(have) == null) return { changed: false, summary: 'identity colours already declared' }
|
|
40
|
+
const refused = identityRefusal(c.read(INSTALLED_CONFIG_SRC))
|
|
41
|
+
if (refused != null) return { changed: false, summary: refused }
|
|
42
|
+
const { id, from } = await deriveIdentity(c.root, configName(text, c.root), (rel) => c.bytes(rel))
|
|
43
|
+
let next: string
|
|
44
|
+
try {
|
|
45
|
+
next = setIdentityText(text, id)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { changed: false, summary: (e as Error).message }
|
|
48
|
+
}
|
|
49
|
+
c.write(CONFIG_FILE, next)
|
|
50
|
+
return { changed: true, summary: `identity ${id.colorLight} / ${id.colorDark}, from ${from}` }
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export default identityColors
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SK025 release drift (build/shipkit/built-in-checks.md): every unit this repo
|
|
2
|
+
// releases matches what npm serves and what its push remote holds, the same
|
|
3
|
+
// survey `shipkit releases` prints. It replaces a drift copy per page. A repo
|
|
4
|
+
// that releases nothing (a private root with no release config and no units)
|
|
5
|
+
// skips it. A world probe: the registry and the remote move on a publish this
|
|
6
|
+
// tree cannot see, so a clean answer keeps for a day, keyed on every unit's
|
|
7
|
+
// version so a local bump asks again at once.
|
|
8
|
+
import { check, type CheckCtx } from '../check.ts'
|
|
9
|
+
import { parseWireOutput } from '../core/wire.ts'
|
|
10
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
11
|
+
import { attentionLines, collectReports, type UnitReport } from '../release/run-releases.ts'
|
|
12
|
+
import { resolveUnits, type ReleaseUnit } from '../release/units.ts'
|
|
13
|
+
|
|
14
|
+
/** the units this repo releases: every declared one, and the root only when
|
|
15
|
+
* it publishes to npm or the repo declares a release config */
|
|
16
|
+
export function releasedUnits(root: string, config: RepoConfig): ReleaseUnit[] {
|
|
17
|
+
return resolveUnits(root, config).units.filter((u) => u.id !== 'root' || u.targets.npm || config.release != null)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** the survey as check lines: one ok per unit in step, else its attention lines */
|
|
21
|
+
export function reportDrift(c: CheckCtx, reports: UnitReport[]): void {
|
|
22
|
+
for (const r of reports) {
|
|
23
|
+
const lines = attentionLines(r)
|
|
24
|
+
if (lines.length === 0) {
|
|
25
|
+
// "in sync with npm" only for a unit that HAS an npm target and whose
|
|
26
|
+
// registry read succeeded (attentionLines warns when it did not)
|
|
27
|
+
c.ok(r.npmTarget ? `${r.name}@${r.version} in sync with npm` : `${r.name}@${r.version}, no npm target, tag state only`)
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
for (const l of parseWireOutput(lines.join('\n'))) {
|
|
31
|
+
if (l.kind === 'warn' || l.kind === 'fail') c.warn(l.text)
|
|
32
|
+
else if (l.kind === 'ok') c.ok(l.text)
|
|
33
|
+
else c.note(l.text)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const releaseDrift = check({
|
|
39
|
+
id: 'SK025',
|
|
40
|
+
blocking: false,
|
|
41
|
+
title: 'release drift',
|
|
42
|
+
description: 'every unit this repo releases matches npm and its push remote',
|
|
43
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
44
|
+
when: 'health',
|
|
45
|
+
ttl: '20h',
|
|
46
|
+
every: (last) => (last?.status === 'pass' ? ['20h', '28h'] : '4h'),
|
|
47
|
+
planOff: (env) => (releasedUnits(env.root, env.config).length === 0 ? 'this repo releases nothing' : null),
|
|
48
|
+
cacheKey: (env) => releasedUnits(env.root, env.config).map((u) => `${u.id}@${u.version}`).join(','),
|
|
49
|
+
run: async (c) => {
|
|
50
|
+
const ids = new Set(releasedUnits(c.root, c.config).map((u) => u.id))
|
|
51
|
+
if (ids.size === 0) return void c.lines.push({ kind: 'skip', text: 'this repo releases nothing: no npm root, no release config, no units' })
|
|
52
|
+
const { reports, errors } = await collectReports(c.root, c.config)
|
|
53
|
+
for (const e of errors) c.warn(e)
|
|
54
|
+
reportDrift(c, reports.filter((r) => ids.has(r.id)))
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export default releaseDrift
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SK026 asks auto close (build/shipkit/built-in-checks.md): probe every open
|
|
2
|
+
// ask's own `check:` and archive the ones that pass, so an act done in the
|
|
3
|
+
// world leaves the queue without anyone editing a file. An open queue is no
|
|
4
|
+
// warning: the hub and `shipkit needs` show it. A repo with no queue skips.
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { checkAsks, listAsks } from '../asks/asks-queue.ts'
|
|
8
|
+
import { check } from '../check.ts'
|
|
9
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
10
|
+
|
|
11
|
+
export const asksAutoclose = check({
|
|
12
|
+
id: 'SK026',
|
|
13
|
+
blocking: false,
|
|
14
|
+
title: 'asks auto close',
|
|
15
|
+
description: 'no open ask whose own probe passes stays in the queue',
|
|
16
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
17
|
+
when: 'health',
|
|
18
|
+
never: "runs each open ask's own probe, a shell command that reads the world",
|
|
19
|
+
every: ['20h', '28h'],
|
|
20
|
+
// it only probes an ask that carries one, and archives one marked done
|
|
21
|
+
planOff: (env) => {
|
|
22
|
+
const dir = join(env.root, env.config.asks?.dir ?? LAYOUT.asks)
|
|
23
|
+
if (!existsSync(dir)) return 'no asks queue'
|
|
24
|
+
return listAsks(dir).some((a) => a.check != null || a.status === 'done') ? null : 'no open ask carries a probe'
|
|
25
|
+
},
|
|
26
|
+
run: async (c) => {
|
|
27
|
+
const dir = join(c.root, c.config.asks?.dir ?? LAYOUT.asks)
|
|
28
|
+
if (!existsSync(dir)) return void c.lines.push({ kind: 'skip', text: 'no asks queue in this repo' })
|
|
29
|
+
// archiving RENAMES files, and the release path runs the checks between
|
|
30
|
+
// its clean tree guard and `npm publish`: frozen means report only
|
|
31
|
+
if (c.frozen) return void c.lines.push({ kind: 'skip', text: 'asks not probed: a frozen run archives nothing' })
|
|
32
|
+
const events: string[] = []
|
|
33
|
+
await checkAsks(c.root, dir, (l) => events.push(l))
|
|
34
|
+
// `closed + archived <file>` / `archived <file>` are the queue's frozen stdout contract
|
|
35
|
+
for (const l of events) {
|
|
36
|
+
if (l.startsWith('closed') || l.startsWith('archived')) c.ok(`ask ${l}`)
|
|
37
|
+
// the queue could not run (another agent holds the write lease)
|
|
38
|
+
else if (l.startsWith('WARN')) c.warn(l.replace(/^WARN\s*/, ''))
|
|
39
|
+
}
|
|
40
|
+
if (!c.lines.some((l) => l.kind === 'ok')) c.ok('no open ask has a probe that passes')
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export default asksAutoclose
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SK027 agent wiki links (build/shipkit/built-in-checks.md): a repo's agent/
|
|
2
|
+
// IS an md wiki (entry index.md, the CLAUDE.md include), so an orphan page or
|
|
3
|
+
// a dangling ref there fails. A cross wiki pointer (into a house wiki) is
|
|
4
|
+
// backticked, never a bare → or @, which reads as an agent local link and
|
|
5
|
+
// dangles. A repo without agent/index.md skips.
|
|
6
|
+
import { existsSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { check } from '../check.ts'
|
|
9
|
+
import { lintMd } from '../wiki/lint-md.ts'
|
|
10
|
+
|
|
11
|
+
export const agentWikiLinks = check({
|
|
12
|
+
id: 'SK027',
|
|
13
|
+
blocking: true,
|
|
14
|
+
title: 'agent wiki links',
|
|
15
|
+
description: 'every agent/ page is linked from somewhere, and every agent/ link resolves',
|
|
16
|
+
owner: 'meta/agent-folder.md',
|
|
17
|
+
// the md lint walks agent/ on disk rather than through `c`: agent/ is all it reads
|
|
18
|
+
inputs: ['agent'],
|
|
19
|
+
run: (c) => {
|
|
20
|
+
if (!existsSync(join(c.root, 'agent/index.md'))) return void c.lines.push({ kind: 'skip', text: 'no agent/ wiki in this repo' })
|
|
21
|
+
const lint = lintMd({ root: join(c.root, 'agent'), index: 'index.md' })
|
|
22
|
+
for (const f of lint.orphans) c.fail(`agent/${f}: orphan, unreachable from agent/index.md: link it from a page or delete it`)
|
|
23
|
+
for (const d of lint.dangling) c.fail(`agent/${d.from}: dangling ref → ${d.token}, target not found (backtick a cross wiki ref; fix or delete the pointer)`)
|
|
24
|
+
if (!c.failed) c.ok('agent/ has no orphans and no dangling refs')
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export default agentWikiLinks
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SK028 readme (build/shipkit/built-in-checks.md): the README.md a repo's hub
|
|
2
|
+
// page shows exists and carries what makes the repo recognizable at a glance.
|
|
3
|
+
// A list of named requirements, each reporting its own line, so a new
|
|
4
|
+
// mandatory item is one more entry. Read through `c` only, so the answer is
|
|
5
|
+
// keyed on README.md and the images it points to, never on a clock.
|
|
6
|
+
import { posix } from 'path'
|
|
7
|
+
import { check } from '../check.ts'
|
|
8
|
+
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
9
|
+
|
|
10
|
+
const README = 'README.md'
|
|
11
|
+
|
|
12
|
+
/** every image source the README shows: markdown `` and html
|
|
13
|
+
* `<img src>`, code fences and inline code left out, since a sample there
|
|
14
|
+
* shows nothing */
|
|
15
|
+
export const imageRefs = (md: string): string[] => {
|
|
16
|
+
const prose = md.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[^\n]*$/gm, '').replace(/`[^`\n]*`/g, '')
|
|
17
|
+
const refs: { at: number; src: string }[] = []
|
|
18
|
+
for (const m of prose.matchAll(/!\[[^\]]*\]\(\s*<?([^\s)>]+)>?(?:\s+["'(][^)]*)?\)/g)) refs.push({ at: m.index, src: m[1]! })
|
|
19
|
+
for (const m of prose.matchAll(/<img\b[^>]*?\ssrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) refs.push({ at: m.index, src: (m[1] ?? m[2] ?? m[3])! })
|
|
20
|
+
return refs.sort((a, b) => a.at - b.at).map((r) => r.src)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** a status badge (ci, version, license) shows the repo's state, never what it looks like */
|
|
24
|
+
const isBadge = (src: string): boolean => /(^|[/.])(shields\.io|badgen\.net)\//i.test(src) || /badge[^/]*\.svg([?#]|$)/i.test(src)
|
|
25
|
+
|
|
26
|
+
/** where an image source lands: an https url, a repo file, or nowhere with why */
|
|
27
|
+
const resolveImage = (c: CheckCtx, src: string): { ok: string } | { bad: string } => {
|
|
28
|
+
if (isBadge(src)) return { bad: `${src.slice(0, 60)} is a badge` }
|
|
29
|
+
if (/^https:\/\//i.test(src)) return { ok: src }
|
|
30
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith('//')) return { bad: `${src.slice(0, 60)} is neither an https url nor a repo file` }
|
|
31
|
+
let rel: string
|
|
32
|
+
try {
|
|
33
|
+
rel = posix.normalize(decodeURI(src.replace(/[?#].*$/, '')).replace(/^\/+/, ''))
|
|
34
|
+
} catch {
|
|
35
|
+
return { bad: `${src} is not a readable path` }
|
|
36
|
+
}
|
|
37
|
+
if (rel === '.' || rel.startsWith('../')) return { bad: `${src} points out of the repo` }
|
|
38
|
+
return c.exists(rel, 'file') ? { ok: rel } : { bad: `${rel} is missing` }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Finding = { ok: string } | { warn: string; fix: string }
|
|
42
|
+
type Requirement = { name: string; test: (c: CheckCtx, md: string) => Finding }
|
|
43
|
+
|
|
44
|
+
/** what every README carries, in order. Add an entry to make one more item mandatory */
|
|
45
|
+
const REQUIREMENTS: Requirement[] = [
|
|
46
|
+
{
|
|
47
|
+
name: 'screenshot',
|
|
48
|
+
test: (c, md) => {
|
|
49
|
+
const found = imageRefs(md).map((src) => resolveImage(c, src))
|
|
50
|
+
const shown = found.find((f): f is { ok: string } => 'ok' in f)
|
|
51
|
+
if (shown != null) return { ok: `${README} shows a screenshot: ${shown.ok}` }
|
|
52
|
+
const why = found.map((f) => (f as { bad: string }).bad)
|
|
53
|
+
return {
|
|
54
|
+
warn: `${README}: screenshot: ${why.length === 0 ? 'no image at all' : `no image resolves (${why.join(', ')})`}`,
|
|
55
|
+
fix: 'add a screenshot to README.md: , a file in the repo, or an https url',
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
export const readme = check({
|
|
62
|
+
id: 'SK028',
|
|
63
|
+
blocking: false,
|
|
64
|
+
title: 'readme',
|
|
65
|
+
description: 'README.md exists and shows at least one screenshot',
|
|
66
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
67
|
+
run: (c) => {
|
|
68
|
+
const md = c.read(README)
|
|
69
|
+
if (md == null) return c.warn(`${README}: exists: no ${README} at the repo root`, 'write README.md: what the repo is, and a screenshot of it')
|
|
70
|
+
c.ok(`${README} exists`)
|
|
71
|
+
for (const r of REQUIREMENTS) {
|
|
72
|
+
const f = r.test(c, md)
|
|
73
|
+
if ('ok' in f) c.ok(f.ok)
|
|
74
|
+
else c.warn(f.warn, f.fix)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export default readme
|
package/src/cli/cmd_Assets.ts
CHANGED
|
@@ -99,7 +99,7 @@ export class AssetsCheckCommand extends Command {
|
|
|
99
99
|
|
|
100
100
|
static override usage = Command.Usage({
|
|
101
101
|
category: 'Assets',
|
|
102
|
-
description: 'kit loads, kinds valid, cflow workflows load, drafts present,
|
|
102
|
+
description: 'kit loads, kinds valid, cflow workflows load, drafts present, identity outputs present',
|
|
103
103
|
examples: [['health-check the asset pipeline', 'shipkit assets check']],
|
|
104
104
|
})
|
|
105
105
|
|
package/src/cli/cmd_Check.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// autofixes then re-lints. Thin clipanion surface; logic in
|
|
3
3
|
// core/run-check.ts and core/run-fix.ts. ci NEVER fixes.
|
|
4
4
|
import { Command, Option } from 'clipanion'
|
|
5
|
+
import { runViaHub } from '../home/via-hub.ts'
|
|
6
|
+
import { readJobOpts, recordExit } from '../core/jobs.ts'
|
|
7
|
+
import { runJob } from '../core/job-run.ts'
|
|
5
8
|
import { runCheck } from '../core/run-check.ts'
|
|
6
9
|
import { maybeSelfUpdateAndRestart } from '../core/self-update.ts'
|
|
7
10
|
import { runFix } from '../core/run-fix.ts'
|
|
@@ -27,8 +30,25 @@ export class CheckCommand extends Command {
|
|
|
27
30
|
fix = Option.Boolean('--fix', false, { description: 'apply check autofixes (every fixable check, or only the listed ids), then re-run them' })
|
|
28
31
|
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for the self-maintaining checks (SK005 never touches package.json)' })
|
|
29
32
|
fixIds = Option.Rest()
|
|
33
|
+
job = Option.String('--job', { hidden: true })
|
|
30
34
|
|
|
31
35
|
async execute(): Promise<number> {
|
|
36
|
+
// a gate job the daemon started: its options are in the job folder
|
|
37
|
+
if (this.job != null) {
|
|
38
|
+
const root = process.cwd()
|
|
39
|
+
const job = readJobOpts(root, this.job)
|
|
40
|
+
let code = 1
|
|
41
|
+
if (job == null) this.context.stderr.write(`FAIL: gate job ${this.job} has no options in its folder\n`)
|
|
42
|
+
else {
|
|
43
|
+
try {
|
|
44
|
+
code = await runJob(root, this.job, 'check', job, (l) => { this.context.stdout.write(`${l}\n`) })
|
|
45
|
+
} catch (e) {
|
|
46
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
recordExit(root, this.job, code)
|
|
50
|
+
return code
|
|
51
|
+
}
|
|
32
52
|
// a stale toolkit updates itself and restarts this exact invocation
|
|
33
53
|
// (loop-guarded, skipped under CI/frozen/source consumption)
|
|
34
54
|
await maybeSelfUpdateAndRestart(process.cwd(), { fresh: this.fresh })
|
|
@@ -43,7 +63,12 @@ export class CheckCommand extends Command {
|
|
|
43
63
|
}
|
|
44
64
|
if (this.fix)
|
|
45
65
|
return await runFix(process.cwd(), { ids: this.fixIds, print: (l) => this.context.stdout.write(`${l}\n`) })
|
|
46
|
-
|
|
66
|
+
const opts = { json: this.json, fresh: this.fresh, frozen: this.frozen }
|
|
67
|
+
const print = (l: string): void => { this.context.stdout.write(`${l}\n`) }
|
|
68
|
+
// on a machine with the home, the repo's hub runs it (home/via-hub.ts)
|
|
69
|
+
const via = await runViaHub(process.cwd(), 'check', opts, { print, printErr: (l) => this.context.stderr.write(`${l}\n`) })
|
|
70
|
+
if (via != null) return via
|
|
71
|
+
return await runCheck(process.cwd(), { ...opts, print })
|
|
47
72
|
} catch (e) {
|
|
48
73
|
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
49
74
|
return 1
|
package/src/cli/cmd_Ci.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// core/run-ci.ts.
|
|
3
3
|
import { Command, Option } from 'clipanion'
|
|
4
4
|
import { join } from 'path'
|
|
5
|
-
import { recordExit, startDetached } from '../core/jobs.ts'
|
|
5
|
+
import { readJobOpts, recordExit, startDetached } from '../core/jobs.ts'
|
|
6
|
+
import { runJob } from '../core/job-run.ts'
|
|
6
7
|
import { runCi } from '../core/run-ci.ts'
|
|
8
|
+
import { runViaHub } from '../home/via-hub.ts'
|
|
7
9
|
import { maybeSelfUpdateAndRestart } from '../core/self-update.ts'
|
|
8
10
|
|
|
9
11
|
export class CiCommand extends Command {
|
|
@@ -22,6 +24,7 @@ export class CiCommand extends Command {
|
|
|
22
24
|
['run everything, take nothing from the cache', 'shipkit ci --fresh'],
|
|
23
25
|
['include an opt in check', 'shipkit ci --with up:itch'],
|
|
24
26
|
['in the background, read it back with shipkit status', 'shipkit ci --detach'],
|
|
27
|
+
['every line, the ones nobody acts on included', 'shipkit ci --verbose'],
|
|
25
28
|
],
|
|
26
29
|
})
|
|
27
30
|
|
|
@@ -36,6 +39,8 @@ export class CiCommand extends Command {
|
|
|
36
39
|
fresh = Option.Boolean('--fresh', false, { description: 'run every check: no answer comes from the cache, and what this run finds is stored fresh' })
|
|
37
40
|
full = Option.Boolean('--full', false, { description: `run the FULL test tier: every suite the repo declares, the slow ones it skips while you work included. What the release path always runs. Same as SHIPKIT_TEST_FULL=1` })
|
|
38
41
|
|
|
42
|
+
verbose = Option.Boolean('--verbose', false, { description: 'print every line, the ones nobody acts on included: a clean fix pass, the machine pool, the test tier, the cache declarations, the checks not in this run, the cache counts' })
|
|
43
|
+
|
|
39
44
|
detach = Option.Boolean('--detach', false, { description: 'start this gate in the background and return at once with a run id. `shipkit status <id>` prints it. The gate is an ordinary one: same cache, same run record' })
|
|
40
45
|
job = Option.String('--job', { hidden: true })
|
|
41
46
|
|
|
@@ -46,7 +51,13 @@ export class CiCommand extends Command {
|
|
|
46
51
|
for (const l of d.lines) this.context.stdout.write(`${l}\n`)
|
|
47
52
|
return 0
|
|
48
53
|
}
|
|
49
|
-
const
|
|
54
|
+
const job = this.job != null ? readJobOpts(root, this.job) : null
|
|
55
|
+
let code = 1
|
|
56
|
+
try {
|
|
57
|
+
code = this.job != null && job != null ? await runJob(root, this.job, 'ci', job, (l) => { this.context.stdout.write(`${l}\n`) }) : await this.gate(root)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
60
|
+
}
|
|
50
61
|
if (this.job != null) recordExit(root, this.job, code)
|
|
51
62
|
return code
|
|
52
63
|
}
|
|
@@ -64,6 +75,7 @@ export class CiCommand extends Command {
|
|
|
64
75
|
...this.only.flatMap((v) => ['--only', v]),
|
|
65
76
|
...(this.fresh ? ['--fresh'] : []),
|
|
66
77
|
...(this.full ? ['--full'] : []),
|
|
78
|
+
...(this.verbose ? ['--verbose'] : []),
|
|
67
79
|
]
|
|
68
80
|
}
|
|
69
81
|
|
|
@@ -72,7 +84,7 @@ export class CiCommand extends Command {
|
|
|
72
84
|
// (loop-guarded, skipped under CI/frozen/source consumption)
|
|
73
85
|
await maybeSelfUpdateAndRestart(root, { fresh: this.fresh })
|
|
74
86
|
try {
|
|
75
|
-
|
|
87
|
+
const opts = {
|
|
76
88
|
bail: this.bail,
|
|
77
89
|
json: this.json,
|
|
78
90
|
fix: this.fix,
|
|
@@ -84,8 +96,14 @@ export class CiCommand extends Command {
|
|
|
84
96
|
onlyIds: this.only,
|
|
85
97
|
fresh: this.fresh,
|
|
86
98
|
...(this.full ? { tests: 'full' as const } : {}),
|
|
87
|
-
|
|
88
|
-
}
|
|
99
|
+
...(this.verbose ? { verbose: true } : {}),
|
|
100
|
+
}
|
|
101
|
+
const print = (l: string): void => { this.context.stdout.write(`${l}\n`) }
|
|
102
|
+
// on a machine with the home, the repo's hub runs it (home/via-hub.ts)
|
|
103
|
+
// a job is already the run, wherever it was started from
|
|
104
|
+
const via = this.job != null ? null : await runViaHub(root, 'ci', opts, { print, printErr: (l) => this.context.stderr.write(`${l}\n`) })
|
|
105
|
+
if (via != null) return via
|
|
106
|
+
return await runCi(root, { ...opts, print })
|
|
89
107
|
} catch (e) {
|
|
90
108
|
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
91
109
|
return 1
|