rvlib-shipkit 1.2.0 → 1.4.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 +104 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +15 -10
- package/src/check.ts +26 -10
- package/src/checks/SK001-package-json-shape.check.ts +34 -5
- 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 +103 -61
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -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 +50 -0
- package/src/checks/SK028-readme.check.ts +83 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +119 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -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 +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +7 -3
- package/src/collections/test-time.ts +10 -1
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +66 -17
- package/src/core/discover-checks.ts +10 -5
- 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 +1 -1
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +8 -7
- package/src/core/run-ci.ts +46 -15
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +49 -9
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +138 -0
- package/src/home/activity.ts +256 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +10 -2
- package/src/home/home-routes.ts +34 -6
- package/src/home/home-start.ts +33 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +40 -7
- package/src/home/process-rows.ts +222 -0
- package/src/home/resources.ts +99 -20
- package/src/home/run-daemon.ts +12 -7
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +32 -5
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +20 -6
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +30 -2
- package/src/ledger/world-cache.ts +63 -14
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +17 -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-words.ts +2 -0
- package/src/serve/hub-app/checks-svc.ts +2 -1
- package/src/serve/hub-app/col-tables-view.tsx +124 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +28 -0
- 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/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +50 -13
- package/src/serve/hub-app/home-strip.css +81 -23
- package/src/serve/hub-app/home-strip.tsx +243 -102
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +103 -161
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +101 -39
- package/src/serve/hub-app/icons.tsx +27 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +44 -9
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +65 -13
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +13 -9
- 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 +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- 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 +26 -7
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +29 -9
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +34 -13
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/serve/warm-cell.ts +49 -16
- package/src/shipkit-files.ts +20 -4
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -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,50 @@
|
|
|
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 link that leaves agent/ but stays in the repo
|
|
4
|
+
// is checked on disk; a pointer out of the repo (a house wiki) is backticked,
|
|
5
|
+
// never a bare → or @. A repo without agent/index.md skips.
|
|
6
|
+
import { existsSync, readFileSync } from 'fs'
|
|
7
|
+
import { basename, 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', boundary: c.root })
|
|
22
|
+
const linked = lint.pages.filter((p) => !lint.orphans.includes(p))
|
|
23
|
+
for (const f of lint.orphans) {
|
|
24
|
+
const by = namingPage(c.root, f, linked)
|
|
25
|
+
const where = by != null ? `${by}, which already names it (turn that mention into the link)` : 'agent/index.md or the page of its topic'
|
|
26
|
+
c.fail(`agent/${f}: orphan, unreachable from agent/index.md: add a markdown link to it from ${where}; delete it only if the page is dead`)
|
|
27
|
+
}
|
|
28
|
+
for (const d of lint.dangling) c.fail(`agent/${d.from}: dangling ref → ${d.token}, target not found on disk: fix the path or delete the pointer (a pointer out of this repo, into a house wiki or another repo, goes in backticks)`)
|
|
29
|
+
if (!c.failed) c.ok('agent/ has no orphans and no dangling refs')
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/** the linked page whose text already names an orphan (its path, its file
|
|
34
|
+
* name, or a glob its file name matches), the one to turn into a link */
|
|
35
|
+
function namingPage(root: string, orphan: string, pages: string[]): string | null {
|
|
36
|
+
const name = basename(orphan)
|
|
37
|
+
const stem = name.replace(/\.md$/, '')
|
|
38
|
+
for (const p of pages) {
|
|
39
|
+
const text = readFileSync(join(root, 'agent', p), 'utf8')
|
|
40
|
+
if (text.includes(orphan) || text.includes(name) || text.includes(`${stem}.`) || text.includes(`${stem} `)) return `agent/${p}`
|
|
41
|
+
for (const m of text.matchAll(/[\w.-]*\*[\w.*-]*/g)) {
|
|
42
|
+
const g = m[0]
|
|
43
|
+
if (g.replace(/[*.]/g, '').length < 3) continue
|
|
44
|
+
if (new Bun.Glob(g).match(name) || new Bun.Glob(`${g}.md`).match(name)) return `agent/${p}`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default agentWikiLinks
|
|
@@ -0,0 +1,83 @@
|
|
|
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; ${OFF}`,
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** the other way out, for a repo that cannot show one: the rule off, with the
|
|
62
|
+
* reason `defineRepo` requires (core/when.ts) */
|
|
63
|
+
const OFF = `or, when this repo cannot show one, turn the rule off with the reason: when: { SK028: 'off:<why this repo shows no screenshot>' } in repo.config.ts, inside its shipkit gate block`
|
|
64
|
+
|
|
65
|
+
export const readme = check({
|
|
66
|
+
id: 'SK028',
|
|
67
|
+
blocking: false,
|
|
68
|
+
title: 'readme',
|
|
69
|
+
description: 'README.md exists and shows at least one screenshot',
|
|
70
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
71
|
+
run: (c) => {
|
|
72
|
+
const md = c.read(README)
|
|
73
|
+
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; ${OFF}`)
|
|
74
|
+
c.ok(`${README} exists`)
|
|
75
|
+
for (const r of REQUIREMENTS) {
|
|
76
|
+
const f = r.test(c, md)
|
|
77
|
+
if ('ok' in f) c.ok(f.ok)
|
|
78
|
+
else c.warn(f.warn, f.fix)
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
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_Ci.ts
CHANGED
|
@@ -24,6 +24,7 @@ export class CiCommand extends Command {
|
|
|
24
24
|
['run everything, take nothing from the cache', 'shipkit ci --fresh'],
|
|
25
25
|
['include an opt in check', 'shipkit ci --with up:itch'],
|
|
26
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'],
|
|
27
28
|
],
|
|
28
29
|
})
|
|
29
30
|
|
|
@@ -38,6 +39,8 @@ export class CiCommand extends Command {
|
|
|
38
39
|
fresh = Option.Boolean('--fresh', false, { description: 'run every check: no answer comes from the cache, and what this run finds is stored fresh' })
|
|
39
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` })
|
|
40
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
|
+
|
|
41
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' })
|
|
42
45
|
job = Option.String('--job', { hidden: true })
|
|
43
46
|
|
|
@@ -72,6 +75,7 @@ export class CiCommand extends Command {
|
|
|
72
75
|
...this.only.flatMap((v) => ['--only', v]),
|
|
73
76
|
...(this.fresh ? ['--fresh'] : []),
|
|
74
77
|
...(this.full ? ['--full'] : []),
|
|
78
|
+
...(this.verbose ? ['--verbose'] : []),
|
|
75
79
|
]
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -92,6 +96,7 @@ export class CiCommand extends Command {
|
|
|
92
96
|
onlyIds: this.only,
|
|
93
97
|
fresh: this.fresh,
|
|
94
98
|
...(this.full ? { tests: 'full' as const } : {}),
|
|
99
|
+
...(this.verbose ? { verbose: true } : {}),
|
|
95
100
|
}
|
|
96
101
|
const print = (l: string): void => { this.context.stdout.write(`${l}\n`) }
|
|
97
102
|
// on a machine with the home, the repo's hub runs it (home/via-hub.ts)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// shipkit cross-repo-activity: the activity page in the console. The rows come
|
|
2
|
+
// from activityOf, the one computation the daemon's route serves the page, so
|
|
3
|
+
// the two can never disagree; this file only renders them as text. It reads
|
|
4
|
+
// local git and each repo's run record directly and needs no daemon: when one
|
|
5
|
+
// answers, its plan says which checks each repo declares today, as the page's
|
|
6
|
+
// status dot counts them.
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import { Command, Option } from 'clipanion'
|
|
9
|
+
import { activityOf, type Declared } from '../home/activity.ts'
|
|
10
|
+
import { ageShort, clampCap, DEFAULT_CAP, DEFAULT_MINUTES, dirtWords, idleRow, upstreamWords, type ActivityAnswer, type ActivityCommit, type ActivityRow } from '../home/activity-shape.ts'
|
|
11
|
+
import { railRepos } from '../serve/project-gutter-route.ts'
|
|
12
|
+
|
|
13
|
+
/** a span as typed: `10m`, `1h`, `3d`, `90` (minutes); null when it is none */
|
|
14
|
+
export function spanMinutes(v: string): number | null {
|
|
15
|
+
const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/.exec(v.trim())
|
|
16
|
+
if (m == null) return null
|
|
17
|
+
const n = Number(m[1]) * (m[2] === 'h' ? 60 : m[2] === 'd' ? 1440 : 1)
|
|
18
|
+
return n > 0 ? Math.round(n) : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** single dash spellings of the long options, as typed: `-max-commit 3` */
|
|
22
|
+
export function aliasArgs(argv: string[]): string[] {
|
|
23
|
+
return argv.map((a) => (/^-max-(commit|age)(=|$)/.test(a) ? `-${a}` : a))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** the declared check ids from a running daemon's plan; null when none answers */
|
|
27
|
+
export async function daemonDeclared(port = Number(process.env.SHIPKIT_DAEMON_PORT ?? 4798)): Promise<Declared | null> {
|
|
28
|
+
try {
|
|
29
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/home/planned.json`, { signal: AbortSignal.timeout(1_500) })
|
|
30
|
+
if (!r.ok) return null
|
|
31
|
+
const j = (await r.json()) as { rows: { repo: string; check: string }[] }
|
|
32
|
+
const by = new Map<string, Set<string>>()
|
|
33
|
+
for (const row of j.rows) {
|
|
34
|
+
if (!by.has(row.repo)) by.set(row.repo, new Set())
|
|
35
|
+
by.get(row.repo)!.add(row.check)
|
|
36
|
+
}
|
|
37
|
+
return (name) => by.get(name) ?? null
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dot = (r: ActivityRow): string => {
|
|
44
|
+
const c = r.status
|
|
45
|
+
if (c == null) return '·'
|
|
46
|
+
if (c.state === 'red') return `● ${c.reds.length} red`
|
|
47
|
+
if (c.state === 'orange') return `● ${c.warns.length} warn`
|
|
48
|
+
return '● green'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const commitLine = (c: ActivityCommit, now: number, old: boolean): string =>
|
|
52
|
+
` ${ageShort(c.at, now).padStart(3)} ${c.sha} ${c.pushed ? '✓' : '↑'} ${c.branch == null ? '' : `[${c.branch}] `}${c.title}${old ? ' (last commit, before the window)' : ''}`
|
|
53
|
+
|
|
54
|
+
/** the page's rows as text: busy repos first, then the quiet ones, each with
|
|
55
|
+
* its status, branch, upstream, uncommitted line and commits */
|
|
56
|
+
export function renderActivity(a: ActivityAnswer, cap: number): string[] {
|
|
57
|
+
const out: string[] = []
|
|
58
|
+
const busy = a.rows.filter((r) => !idleRow(r))
|
|
59
|
+
const quiet = a.rows.filter(idleRow)
|
|
60
|
+
for (const r of [...busy, ...quiet]) {
|
|
61
|
+
const branch = r.branch ?? (r.detached == null ? '' : `detached ${r.detached}`)
|
|
62
|
+
out.push(`${r.name} ${dot(r)} ${branch} (${upstreamWords(r)})`)
|
|
63
|
+
for (const c of r.failing ?? []) {
|
|
64
|
+
out.push(` ${c.status === 'fail' ? '✗' : '!'} ${c.id}${c.at == null ? '' : ` (${ageShort(c.at, a.now)})`}`)
|
|
65
|
+
for (const l of c.lines) out.push(` ${l}`)
|
|
66
|
+
}
|
|
67
|
+
if (r.error != null) out.push(` git failed: ${r.error}`)
|
|
68
|
+
const dirt = r.error == null ? dirtWords(r) : ''
|
|
69
|
+
if (dirt !== '') {
|
|
70
|
+
out.push(` uncommitted: ${dirt}`)
|
|
71
|
+
const mark = { staged: 'S', modified: 'M', untracked: '?' } as const
|
|
72
|
+
for (const f of r.files) out.push(` ${mark[f.kind]} ${f.path}`)
|
|
73
|
+
}
|
|
74
|
+
for (const c of r.commits.slice(0, cap)) out.push(commitLine(c, a.now, false))
|
|
75
|
+
if (r.commits.length > cap) out.push(` … ${r.commits.length - cap} more`)
|
|
76
|
+
if (r.last != null) out.push(commitLine(r.last, a.now, true))
|
|
77
|
+
if (r.commits.length === 0 && r.last == null && r.error == null) out.push(' no commit yet')
|
|
78
|
+
}
|
|
79
|
+
out.push(`${busy.length} busy, ${quiet.length} quiet, read in ${a.ms}ms`)
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class CrossRepoActivityCommand extends Command {
|
|
84
|
+
static override paths = [['cross-repo-activity'], ['activity']]
|
|
85
|
+
|
|
86
|
+
static override usage = Command.Usage({
|
|
87
|
+
category: 'Home',
|
|
88
|
+
description: 'the activity page in the console: every house repo (archived ones left out), its checks, branch, upstream, uncommitted work and commits in a window. The same computation the page reads, local git only, no daemon needed',
|
|
89
|
+
examples: [
|
|
90
|
+
['the last 30 minutes, 3 commits per repo, as the page opens', 'shipkit cross-repo-activity'],
|
|
91
|
+
['the same, by the page name', 'shipkit activity'],
|
|
92
|
+
['the last 3 hours, up to 10 commits per repo', 'shipkit cross-repo-activity --max-age 3h --max-commit 10'],
|
|
93
|
+
['the route\'s exact payload', 'shipkit cross-repo-activity --max-age 30m --json'],
|
|
94
|
+
],
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
maxAge = Option.String('--max-age', { required: false, description: `how far back: 10m, 1h, 3d, or minutes (default ${DEFAULT_MINUTES}m)` })
|
|
98
|
+
maxCommit = Option.String('--max-commit', { required: false, description: `commits shown per repo, 1 to 15 (default ${DEFAULT_CAP}). -max-commit works too` })
|
|
99
|
+
json = Option.Boolean('--json', false, { description: 'the daemon route\'s exact payload: { now, minutes, rows, ms }' })
|
|
100
|
+
|
|
101
|
+
async execute(): Promise<number> {
|
|
102
|
+
const minutes = this.maxAge == null ? DEFAULT_MINUTES : spanMinutes(this.maxAge)
|
|
103
|
+
if (minutes == null) {
|
|
104
|
+
this.context.stderr.write(`--max-age must be a span like 10m, 1h or 3d, got ${JSON.stringify(this.maxAge)}\n`)
|
|
105
|
+
return 2
|
|
106
|
+
}
|
|
107
|
+
const cap = clampCap(this.maxCommit ?? DEFAULT_CAP)
|
|
108
|
+
const declared = await daemonDeclared()
|
|
109
|
+
// HOME as the shell has it now: the house include names `~`
|
|
110
|
+
const a = await activityOf(railRepos(process.cwd(), process.env.HOME || homedir()), minutes, Date.now(), declared)
|
|
111
|
+
if (this.json) {
|
|
112
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
113
|
+
return 0
|
|
114
|
+
}
|
|
115
|
+
for (const l of renderActivity(a, cap)) this.context.stdout.write(`${l}\n`)
|
|
116
|
+
if (declared == null) this.context.stdout.write('note: no daemon answered, so every check a run record holds counts, declared today or not\n')
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// shipkit global-processes: the overview's processes table in the console,
|
|
2
|
+
// shipkit's own (pm, the daemon, the web) first. The rows come from pm's own
|
|
3
|
+
// answer through process-rows.ts, the one model the page draws, so the two can
|
|
4
|
+
// never disagree; this file only asks pm and the daemon and renders them as
|
|
5
|
+
// text. With pm down it reads the declared units the way pm does
|
|
6
|
+
// (pm-config.ts) and marks each one unknown.
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import { Command, Option } from 'clipanion'
|
|
9
|
+
import { daemonPort, daemonUrl, homePort, pmPort, pmUrl } from '../home/home-paths.ts'
|
|
10
|
+
import { houseRootOf } from '../home/home-start.ts'
|
|
11
|
+
import type { PmAnswer } from '../home/pm.ts'
|
|
12
|
+
import { coreGroup, declaredRow, groupRows, processGroups, stateWords, type ProcessGroup } from '../home/process-rows.ts'
|
|
13
|
+
import type { PmConfig } from '../home/pm-config.ts'
|
|
14
|
+
import { readPmConfig } from '../home/run-pm.ts'
|
|
15
|
+
import { railRepos } from '../serve/project-gutter-route.ts'
|
|
16
|
+
|
|
17
|
+
export type GlobalProcessesAnswer = {
|
|
18
|
+
/** pm as it answered, null when it did not */
|
|
19
|
+
pm: { pid: number; version: string } | null
|
|
20
|
+
now: number
|
|
21
|
+
groups: ProcessGroup[]
|
|
22
|
+
/** the unit files that would not load */
|
|
23
|
+
errors: string[]
|
|
24
|
+
/** with pm down: whether a house was found to read the declared units from */
|
|
25
|
+
house: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type GlobalProcessesDeps = {
|
|
29
|
+
pm: () => Promise<PmAnswer | null>
|
|
30
|
+
/** the daemon's identity, for its version; null when it does not answer */
|
|
31
|
+
daemon?: () => Promise<{ version: string } | null>
|
|
32
|
+
/** the declared units, read like pm reads them; null when no house is found */
|
|
33
|
+
declared: () => Promise<Pick<PmConfig, 'processes' | 'errors'> | null>
|
|
34
|
+
/** the project list's order */
|
|
35
|
+
order: string[]
|
|
36
|
+
now: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** the ports this shell would find pm and its children on */
|
|
40
|
+
const localPorts = () => ({ pm: pmPort(), daemon: daemonPort(), web: homePort() })
|
|
41
|
+
|
|
42
|
+
/** pm's answer, shipkit's own rows first; the declared units as unknown when
|
|
43
|
+
* pm does not answer */
|
|
44
|
+
export async function globalProcessesAnswer(d: GlobalProcessesDeps): Promise<GlobalProcessesAnswer> {
|
|
45
|
+
const [pm, daemon] = await Promise.all([d.pm(), d.daemon?.() ?? null])
|
|
46
|
+
const core = { daemonVersion: daemon?.version ?? null, ports: localPorts() }
|
|
47
|
+
if (pm != null) return { pm: { pid: pm.pid, version: pm.version }, now: d.now, groups: processGroups(pm, d.order, core), errors: pm.unitErrors, house: true }
|
|
48
|
+
const c = await d.declared()
|
|
49
|
+
return { pm: null, now: d.now, groups: [coreGroup(null, core), ...groupRows((c?.processes ?? []).map(declaredRow), d.order)], errors: c?.errors ?? [], house: c != null }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** the daemon's identity at /hub.json, or null */
|
|
53
|
+
export async function askDaemon(url = daemonUrl()): Promise<{ version: string } | null> {
|
|
54
|
+
try {
|
|
55
|
+
const r = await fetch(`${url}/hub.json`, { signal: AbortSignal.timeout(3_000) })
|
|
56
|
+
const j = r.ok ? ((await r.json()) as { hub?: string; version?: string }) : null
|
|
57
|
+
return j?.hub === 'shipkit-home' && typeof j.version === 'string' ? { version: j.version } : null
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** pm's status answer, or null when it does not answer */
|
|
64
|
+
export async function askPm(url = pmUrl()): Promise<PmAnswer | null> {
|
|
65
|
+
try {
|
|
66
|
+
const r = await fetch(`${url}/api/pm/status.json`, { signal: AbortSignal.timeout(1_500) })
|
|
67
|
+
if (!r.ok) return null
|
|
68
|
+
const j = (await r.json()) as PmAnswer
|
|
69
|
+
return j.pm === 'shipkit-pm' ? j : null
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** the project list's order and the declared units, read from disk the way
|
|
76
|
+
* pm reads them: what the table falls back on while pm does not answer */
|
|
77
|
+
export function localProcessDeps(cwd: string): Pick<GlobalProcessesDeps, 'declared' | 'order'> {
|
|
78
|
+
// HOME as the shell has it now: the house include names `~`
|
|
79
|
+
const home = process.env.HOME || homedir()
|
|
80
|
+
let order: string[] = []
|
|
81
|
+
try {
|
|
82
|
+
order = railRepos(cwd, home).map((r) => r.name)
|
|
83
|
+
} catch {
|
|
84
|
+
// no rail from here: the groups sort by name
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
order,
|
|
88
|
+
declared: async () => {
|
|
89
|
+
const root = houseRootOf(cwd)
|
|
90
|
+
if (root == null) return null
|
|
91
|
+
try {
|
|
92
|
+
return await readPmConfig(root)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { processes: [], errors: [(e as Error).message] }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pad = (s: string, n: number): string => (s.length >= n ? s : s + ' '.repeat(n - s.length))
|
|
101
|
+
|
|
102
|
+
/** the table as text: shipkit's own, then per repo, one line per process,
|
|
103
|
+
* its note and last log line under it */
|
|
104
|
+
export function renderGlobalProcesses(a: GlobalProcessesAnswer): string[] {
|
|
105
|
+
const out: string[] = []
|
|
106
|
+
const n = a.groups.reduce((k, g) => k + (g.core ? 0 : g.rows.length), 0)
|
|
107
|
+
if (a.pm != null) out.push(`pm pid ${a.pm.pid}, shipkit ${a.pm.version}: ${n} global process${n === 1 ? '' : 'es'}`)
|
|
108
|
+
else if (!a.house) out.push(`pm does not answer on ${pmUrl()}, and no house wiki is found from ${process.cwd()}, so no declared process is known`)
|
|
109
|
+
else out.push(`pm does not answer on ${pmUrl()}: the ${n} declared process${n === 1 ? '' : 'es'} below, each state unknown`)
|
|
110
|
+
const rows = a.groups.flatMap((g) => g.rows)
|
|
111
|
+
const w = {
|
|
112
|
+
state: Math.max(0, ...rows.map((r) => stateWords(r, a.now).length)),
|
|
113
|
+
label: Math.max(0, ...rows.map((r) => r.label.length)),
|
|
114
|
+
host: Math.max(0, ...rows.map((r) => (r.host ?? '').length)),
|
|
115
|
+
}
|
|
116
|
+
for (const g of a.groups) {
|
|
117
|
+
out.push(g.repo)
|
|
118
|
+
for (const r of g.rows) {
|
|
119
|
+
const cells = [` ● ${pad(stateWords(r, a.now), w.state)}`, pad(r.label, w.label), pad(r.host ?? '', w.host), pad(r.pid == null ? '' : `pid ${r.pid}`, 10), r.version == null ? '' : `shipkit ${r.version}`]
|
|
120
|
+
out.push(cells.join(' ').trimEnd())
|
|
121
|
+
if (r.note != null) out.push(` ${r.note}`)
|
|
122
|
+
// shipkit's own logs only matter while one is not up
|
|
123
|
+
if (r.lastLog != null && !(r.core && r.status === 'up')) out.push(` log: ${r.lastLog}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (a.pm != null && n === 0) out.push('none declared: a repo declares one in a <name>.shipkit.ts file, as export default globalProcess({...})')
|
|
127
|
+
for (const e of a.errors) out.push(`WARN ${e}`)
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class GlobalProcessesCommand extends Command {
|
|
132
|
+
static override paths = [['global-processes'], ['gp']]
|
|
133
|
+
|
|
134
|
+
static override usage = Command.Usage({
|
|
135
|
+
category: 'Home',
|
|
136
|
+
description: 'every process on the machine and its state, as the overview shows them: shipkit\'s own (pm, the daemon, the web, with their version) first, then per repo, one line per global process with its state, url, pid and last log line. When pm is down, the declared ones with their state unknown. Always exits 0',
|
|
137
|
+
examples: [
|
|
138
|
+
['the table', 'shipkit global-processes'],
|
|
139
|
+
['the same, short', 'shipkit gp'],
|
|
140
|
+
['the rows the overview draws, as json', 'shipkit global-processes --json'],
|
|
141
|
+
],
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
json = Option.Boolean('--json', false, { description: 'the rows as json: { pm, now, groups, errors, house }, groups exactly as the overview draws them' })
|
|
145
|
+
|
|
146
|
+
async execute(): Promise<number> {
|
|
147
|
+
const a = await globalProcessesAnswer({ ...localProcessDeps(process.cwd()), pm: () => askPm(), daemon: () => askDaemon(), now: Date.now() })
|
|
148
|
+
if (this.json) {
|
|
149
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
150
|
+
return 0
|
|
151
|
+
}
|
|
152
|
+
for (const l of renderGlobalProcesses(a)) this.context.stdout.write(`${l}\n`)
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/cli/cmd_Keys.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import { createHash } from 'crypto'
|
|
9
9
|
import { Command, Option } from 'clipanion'
|
|
10
10
|
import { loadRepoConfig } from '../repo-config.ts'
|
|
11
|
-
import { envTier, FULL_TESTS_ENV,
|
|
11
|
+
import { envTier, FULL_TESTS_ENV, gateInventory } from '../ledger/gate-checks.ts'
|
|
12
|
+
import { planGap, plannable } from '../plan/repo-source.ts'
|
|
12
13
|
import { KeyLayer } from '../ledger/git-key.ts'
|
|
13
14
|
import { Ledger } from '../ledger/ledger.ts'
|
|
14
15
|
import { gateBinary } from '../ledger/toolchain-identity.ts'
|
|
@@ -42,14 +43,22 @@ export class KeysCommand extends Command {
|
|
|
42
43
|
// every task's env, which is in the key, so a surface that renders keys
|
|
43
44
|
// without it renders a tier nobody asked for
|
|
44
45
|
const tier = this.full ? 'full' : envTier()
|
|
45
|
-
const
|
|
46
|
+
const rows = await gateInventory(root, c, { tier })
|
|
47
|
+
const decls = rows.map((r) => r.decl)
|
|
48
|
+
// the plan's side of the burn-down: a check nothing keys and that
|
|
49
|
+
// declares no cadence is re-asked on a guess. A built-in there is a
|
|
50
|
+
// toolkit bug, a repo check rides the default range until it declares
|
|
51
|
+
const planGaps = rows.filter(plannable).flatMap((r) => {
|
|
52
|
+
const gap = planGap(r)
|
|
53
|
+
return gap == null ? [] : [{ id: r.decl.id, from: r.from, gap }]
|
|
54
|
+
})
|
|
46
55
|
const layer = KeyLayer.open(root, decls, { cliEntry: gateBinary() })
|
|
47
56
|
const keys = layer.keys()
|
|
48
57
|
const out = (l: string): void => { this.context.stdout.write(`${l}\n`) }
|
|
49
58
|
|
|
50
59
|
if (this.json) {
|
|
51
60
|
const drift = this.rescan ? layer.rescan() : null
|
|
52
|
-
out(JSON.stringify({ tier, checks: keys, burnDown: layer.registration.burnDown, errors: layer.registration.errors, layerMs: Math.round(layer.openMs), scanMs: Math.round(layer.scanMs), failure: layer.failure, drift }))
|
|
61
|
+
out(JSON.stringify({ tier, checks: keys, burnDown: layer.registration.burnDown, planGaps, errors: layer.registration.errors, layerMs: Math.round(layer.openMs), scanMs: Math.round(layer.scanMs), failure: layer.failure, drift }))
|
|
53
62
|
return layer.registration.errors.length > 0 || layer.failure != null ? 1 : 0
|
|
54
63
|
}
|
|
55
64
|
|
|
@@ -115,6 +124,11 @@ export class KeysCommand extends Command {
|
|
|
115
124
|
const decided = new Set(layer.decls.filter((d) => d.never != null && d.never !== '').map((d) => d.id))
|
|
116
125
|
const shown = this.burnDown ? keys.filter((k) => k.population === 'none' && !decided.has(k.id)) : keys
|
|
117
126
|
for (const k of shown) out(`${k.population.padEnd(7)} ${k.id.padEnd(28)} ${k.key ?? `— ${k.reason ?? ''}`}`)
|
|
127
|
+
if (planGaps.length > 0) out('')
|
|
128
|
+
for (const g of planGaps)
|
|
129
|
+
out(g.from === 'repo'
|
|
130
|
+
? `WARN ${g.id}: ${g.gap}, so the plan runs it every 20h..28h. Declare inputs, a cacheKey, ttl or every`
|
|
131
|
+
: `FAIL: built-in ${g.id}: ${g.gap}. A toolkit bug: the plan waits for a save until it declares one`)
|
|
118
132
|
|
|
119
133
|
const content = keys.filter((k) => k.population === 'content').length
|
|
120
134
|
const reads = keys.filter((k) => k.population === 'reads').length
|