rvlib-shipkit 0.14.0 → 0.16.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 +129 -0
- package/README.md +6 -22
- package/package.json +1 -1
- package/src/assets/kits/identity-kit.ts +1 -1
- package/src/assets/panels/geometry-rects.ts +3 -17
- package/src/assets/server/studio-server.ts +1 -1
- package/src/cli/cmd_Bump.ts +52 -0
- package/src/cli/cmd_Check.ts +2 -2
- package/src/cli/cmd_Ci.ts +10 -2
- package/src/cli/cmd_Commands.ts +126 -0
- package/src/cli/cmd_Hooks.ts +66 -0
- package/src/cli/cmd_Mv.ts +29 -6
- package/src/cli/cmd_Release.ts +41 -0
- package/src/cli/cmd_Releases.ts +42 -0
- package/src/cli/cmd_Rules.ts +20 -6
- package/src/cli/cmd_Start.ts +2 -2
- package/src/cli.ts +14 -1
- package/src/collections/posts.ts +1 -1
- package/src/collections/providers.ts +1 -1
- package/src/contracts/manifests/discord.contract.ts +1 -1
- package/src/contracts/manifests/itch.contract.ts +2 -4
- package/src/core/canonical-scripts.ts +9 -14
- package/src/core/check-suite.ts +1 -1
- package/src/core/discover-checks.ts +21 -6
- package/src/core/run-check.ts +69 -21
- package/src/core/run-ci.ts +75 -9
- package/src/core/run-fixers.ts +144 -0
- package/src/core/write-status.ts +14 -5
- package/src/hooks/commit-message.ts +42 -0
- package/src/hooks/leak-guard.ts +0 -0
- package/src/hooks/run-hooks.ts +131 -0
- package/src/init-repo.ts +10 -5
- package/src/reflect/prompt-corpus.ts +3 -4
- package/src/release/changelog.ts +53 -0
- package/src/release/git-facts.ts +133 -0
- package/src/release/run-bump.ts +419 -0
- package/src/release/run-release.ts +267 -0
- package/src/release/run-releases.ts +230 -0
- package/src/release/unit-gate.ts +67 -0
- package/src/release/units.ts +246 -0
- package/src/repo-config.ts +56 -2
- package/src/rules/SK001-package-json-shape.ts +39 -17
- package/src/rules/SK002-contracts-fulfillness.ts +1 -0
- package/src/rules/SK003-claude-md-shape.ts +2 -1
- package/src/rules/SK004-gitignore-policy.ts +1 -0
- package/src/rules/SK005-rvlib-versions.ts +22 -3
- package/src/rules/SK006-npm-publish-safety.ts +1 -0
- package/src/rules/SK007-github-visibility.ts +5 -1
- package/src/rules/SK008-reviewer-agent.ts +1 -0
- package/src/rules/SK009-commit-ai-tells.ts +66 -0
- package/src/rules/SK010-undeclared-release-unit.ts +114 -0
- package/src/rules/rule-registry.ts +26 -6
- package/src/rules/rule.ts +8 -1
- package/src/rules/run-fix.ts +15 -8
- package/src/serve/hub-app/col-card.tsx +2 -3
- package/src/serve/hub-app/hub-app.tsx +6 -8
- package/src/serve/hub-app/hub-st.ts +4 -7
- package/src/serve/hub-app/media-view.tsx +2 -2
- package/src/serve/run-serve.ts +4 -5
- package/src/serve/serve-hub.ts +2 -3
- package/src/shipkit.ts +9 -1
- package/src/tui/checks-panel.ts +1 -1
- package/src/tui/run-tui.ts +2 -3
- package/src/tui/tasks-panel.ts +10 -4
- package/src/tui/tui-store.ts +2 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SK010 — undeclared release unit (build/shipkit.md). Release units are
|
|
2
|
+
// DECLARED, never inferred, so resolution is deterministic. This rule is the
|
|
3
|
+
// other half of that deal: a package.json sitting in the repo that no unit
|
|
4
|
+
// claims would be invisible to every release surface, and its version would
|
|
5
|
+
// drift forever with nothing reporting it. Two declarations clear it, so the
|
|
6
|
+
// green is always reachable: `units: [{ dir }]` for a releasable package,
|
|
7
|
+
// `notReleasable: [dir]` for one that ships inside something else. That second
|
|
8
|
+
// escape has teeth on purpose, because it is the one an agent staring at a red
|
|
9
|
+
// gate reaches for: an entry must cover at least one real package and never the
|
|
10
|
+
// whole repo, and the green counts the PACKAGES it excused, so a wide entry
|
|
11
|
+
// cannot read as a narrow one.
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
|
13
|
+
import { join, relative } from 'path'
|
|
14
|
+
import { resolveUnits } from '../release/units.ts'
|
|
15
|
+
import type { Rule } from './rule.ts'
|
|
16
|
+
|
|
17
|
+
const SKIP = new Set(['node_modules', 'references', 'tmp', 'stats', 'dist', 'build'])
|
|
18
|
+
|
|
19
|
+
/** package dirs git knows about (tracked + untracked-not-ignored), so a
|
|
20
|
+
* gitignored vendor tree or a python venv never trips the gate. Null when
|
|
21
|
+
* git cannot answer, and the caller falls back to walking. */
|
|
22
|
+
function gitPackageDirs(root: string): string[] | null {
|
|
23
|
+
const p = Bun.spawnSync(['git', 'ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root })
|
|
24
|
+
if (p.exitCode !== 0) return null
|
|
25
|
+
return p.stdout.toString().split('\0')
|
|
26
|
+
.filter((f) => f === 'package.json' || f.endsWith('/package.json'))
|
|
27
|
+
.map((f) => join(root, f, '..'))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** the fs fallback: an unreadable entry (a dangling symlink, an unmounted
|
|
31
|
+
* volume) is skipped, never thrown — a broken link is not a release fact */
|
|
32
|
+
function walkPackageDirs(root: string): string[] {
|
|
33
|
+
const out: string[] = [root]
|
|
34
|
+
const walk = (dir: string): void => {
|
|
35
|
+
let entries: string[]
|
|
36
|
+
try { entries = readdirSync(dir) } catch { return }
|
|
37
|
+
for (const e of entries) {
|
|
38
|
+
if (SKIP.has(e) || e.startsWith('.')) continue
|
|
39
|
+
const p = join(dir, e)
|
|
40
|
+
try { if (!statSync(p).isDirectory()) continue } catch { continue }
|
|
41
|
+
if (existsSync(join(p, 'package.json'))) out.push(p)
|
|
42
|
+
walk(p)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
walk(root)
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** every package.json below the root that carries a name, root excluded */
|
|
50
|
+
export function packageDirs(root: string): string[] {
|
|
51
|
+
const dirs = gitPackageDirs(root) ?? walkPackageDirs(root)
|
|
52
|
+
const named = dirs.filter((d) => {
|
|
53
|
+
if (d === root) return false
|
|
54
|
+
if (relative(root, d).split('/').some((seg) => SKIP.has(seg) || seg.startsWith('.'))) return false
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(readFileSync(join(d, 'package.json'), 'utf8')) as { name?: string }
|
|
57
|
+
return typeof pkg.name === 'string' && pkg.name !== ''
|
|
58
|
+
} catch { return false } // a broken package.json is SK001's story, not this rule's
|
|
59
|
+
})
|
|
60
|
+
return [...new Set(named)].sort()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const undeclaredReleaseUnit: Rule = {
|
|
64
|
+
id: 'SK010',
|
|
65
|
+
blocking: true,
|
|
66
|
+
title: 'release units declared',
|
|
67
|
+
owner: 'build/shipkit.md',
|
|
68
|
+
lint: (t, env) => {
|
|
69
|
+
const { units, errors } = resolveUnits(env.root, env.config)
|
|
70
|
+
// a DECLARED unit that does not resolve is a declaration bug and
|
|
71
|
+
// belongs here. A root package that is not release-shaped (no version
|
|
72
|
+
// yet) is not: the release path refuses on its own, the day it matters.
|
|
73
|
+
const declared = errors.filter((e) => e.id !== 'root')
|
|
74
|
+
if (declared.length > 0) {
|
|
75
|
+
for (const e of declared) t.fail(`declared unit '${e.id}' does not resolve: ${e.message}`, 'repo.config units[]')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
// an entry is a PREFIX over the packages under it, so `examples/` can
|
|
79
|
+
// excuse examples/a and examples/b without enumerating them. Two shapes
|
|
80
|
+
// are refused, both being the kill switch this rule exists to prevent:
|
|
81
|
+
// an entry covering the whole repo, and one covering nothing today that
|
|
82
|
+
// stands ready to excuse whatever lands there tomorrow. defineRepo
|
|
83
|
+
// refuses the root too, and a repo.config.ts that never calls it is
|
|
84
|
+
// exactly why the check also lives in the blocking rule.
|
|
85
|
+
const decls = env.config.notReleasable ?? []
|
|
86
|
+
const covered = (list: string[], d: string): boolean => list.some((c) => d === c || d.startsWith(`${c}/`))
|
|
87
|
+
const pkgs = packageDirs(env.root)
|
|
88
|
+
const wide = decls.filter((d) => join(env.root, d) === env.root)
|
|
89
|
+
if (wide.length > 0) {
|
|
90
|
+
for (const d of wide)
|
|
91
|
+
t.fail(`notReleasable '${d}' resolves to the repo root, so it excuses every package at once`, `repo.config notReleasable: name one package dir (or one container dir) per entry`)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const empty = decls.filter((d) => !pkgs.some((p) => covered([join(env.root, d)], p)))
|
|
95
|
+
if (empty.length > 0) {
|
|
96
|
+
for (const d of empty)
|
|
97
|
+
t.fail(`notReleasable '${d}' covers no package.json, so it excuses no package today and every package that lands there tomorrow`, `repo.config notReleasable: point it at a package dir (or a dir holding some), or drop the entry`)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
const excused = decls.map((d) => join(env.root, d))
|
|
101
|
+
const claimed = [...units.filter((u) => u.rel !== '').map((u) => u.dir), ...excused]
|
|
102
|
+
const orphans = pkgs.filter((d) => !covered(claimed, d))
|
|
103
|
+
if (orphans.length === 0) {
|
|
104
|
+
// PACKAGES, not entries: one entry over a nested tree excuses more
|
|
105
|
+
// than one, and the count is the only place that shows it
|
|
106
|
+
const n = pkgs.filter((d) => covered(excused, d)).length
|
|
107
|
+
return t.ok(`${units.length} release unit(s) declared${units.length === 1 ? ' (the repo root)' : ''}${n > 0 ? ` + ${n} package(s) declared not releasable` : ''} — no package hides from a release`)
|
|
108
|
+
}
|
|
109
|
+
for (const d of orphans) {
|
|
110
|
+
const rel = relative(env.root, d)
|
|
111
|
+
t.fail(`${rel}/package.json is claimed by nothing — declare what it is, or its version drifts with nothing watching`, `repo.config units: [{ dir: '${rel}' }] if it releases, else notReleasable: ['${rel}']`)
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
}
|
|
@@ -11,6 +11,8 @@ import { rvlibVersions } from './SK005-rvlib-versions.ts'
|
|
|
11
11
|
import { npmPublishSafety } from './SK006-npm-publish-safety.ts'
|
|
12
12
|
import { githubVisibility } from './SK007-github-visibility.ts'
|
|
13
13
|
import { reviewerAgent } from './SK008-reviewer-agent.ts'
|
|
14
|
+
import { commitAiTells } from './SK009-commit-ai-tells.ts'
|
|
15
|
+
import { undeclaredReleaseUnit } from './SK010-undeclared-release-unit.ts'
|
|
14
16
|
|
|
15
17
|
export const RULES: readonly Rule[] = [
|
|
16
18
|
packageJsonShape,
|
|
@@ -21,14 +23,32 @@ export const RULES: readonly Rule[] = [
|
|
|
21
23
|
npmPublishSafety,
|
|
22
24
|
githubVisibility,
|
|
23
25
|
reviewerAgent,
|
|
26
|
+
commitAiTells,
|
|
27
|
+
undeclaredReleaseUnit,
|
|
24
28
|
]
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
for
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// THE id shape, one place: 2-4 uppercase letters + exactly 3 digits. `SK` is
|
|
31
|
+
// reserved for the built-ins above so a repo rule can never shadow one.
|
|
32
|
+
export const RULE_ID_RE = /^[A-Z]{2,4}\d{3}$/
|
|
33
|
+
|
|
34
|
+
/** validate a rule set and its ids against everything already registered.
|
|
35
|
+
* Throws on a malformed id, a duplicate, a missing owner page, or a repo
|
|
36
|
+
* rule squatting the reserved SK prefix. Shared by the built-in registry
|
|
37
|
+
* and by `defineRepo`, so both die loud at import time. */
|
|
38
|
+
export function validateRules(rules: readonly Rule[], o: { label: string; builtin?: boolean; taken?: Iterable<string> } = { label: 'rule-registry' }): void {
|
|
39
|
+
const seen = new Set<string>(o.taken ?? [])
|
|
40
|
+
for (const r of rules) {
|
|
41
|
+
if (!RULE_ID_RE.test(r.id)) throw new Error(`${o.label}: bad id '${r.id}' — rules are PREFIX+3 digits (e.g. SK001, RV001)`)
|
|
42
|
+
if (o.builtin !== true && r.id.startsWith('SK'))
|
|
43
|
+
throw new Error(`${o.label}: id '${r.id}' uses the reserved SK prefix — repo rules pick their own (e.g. RV${r.id.slice(-3)})`)
|
|
44
|
+
if (seen.has(r.id)) throw new Error(`${o.label}: duplicate id ${r.id}`)
|
|
45
|
+
if (r.owner === '') throw new Error(`${o.label}: ${r.id} has no owner page`)
|
|
46
|
+
seen.add(r.id)
|
|
47
|
+
}
|
|
32
48
|
}
|
|
33
49
|
|
|
50
|
+
validateRules(RULES, { label: 'rule-registry', builtin: true })
|
|
51
|
+
|
|
52
|
+
export const BUILTIN_RULE_IDS: readonly string[] = RULES.map((r) => r.id)
|
|
53
|
+
|
|
34
54
|
export const ruleById = (id: string): Rule | undefined => RULES.find((r) => r.id === id)
|
package/src/rules/rule.ts
CHANGED
|
@@ -13,10 +13,17 @@ export type RuleEnv = { root: string; config: RepoConfig }
|
|
|
13
13
|
export type FixResult = { changed: boolean; summary: string }
|
|
14
14
|
|
|
15
15
|
export type Rule = {
|
|
16
|
-
/** stable
|
|
16
|
+
/** stable PREFIX + 3 digits, never reused or renumbered (the registry throws).
|
|
17
|
+
* `SK###` is reserved for shipkit's built-ins; a repo registering its own
|
|
18
|
+
* rules through `repo.config.ts rules: []` picks its own prefix (`RV###`). */
|
|
17
19
|
id: string
|
|
18
20
|
/** check-suite display name — stable: the check name in STATUS.md, the TUI row, and the suite cache */
|
|
19
21
|
title: string
|
|
22
|
+
/** can this stop you? REQUIRED, no default. true = a failure is about THIS
|
|
23
|
+
* repo and blocks. false = it reports the world (a box that is off, a cred
|
|
24
|
+
* nobody minted), so a failure prints as a warning and never blocks.
|
|
25
|
+
* The only question worth asking about a check, asked once, in one place. */
|
|
26
|
+
blocking: boolean
|
|
20
27
|
/** wiki page owning the invariant — the run-check framework appends `docsBase + owner`
|
|
21
28
|
* as the fix: pointer whenever a failing/warning lint attached none */
|
|
22
29
|
owner: string
|
package/src/rules/run-fix.ts
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
import { CheckCtx } from '../core/check-ctx.ts'
|
|
7
7
|
import { formatLine } from '../core/wire.ts'
|
|
8
8
|
import { loadRepoConfig } from '../repo-config.ts'
|
|
9
|
-
import { RULES
|
|
9
|
+
import { RULES } from './rule-registry.ts'
|
|
10
10
|
import { runRuleLint, type RuleEnv } from './rule.ts'
|
|
11
11
|
|
|
12
12
|
export type FixOpts = {
|
|
13
13
|
ids?: string[]
|
|
14
|
+
/** drop the closing advisory: inside the ci fix pass it is noise, and it is
|
|
15
|
+
* not wire-shaped, which the frozen output contract forbids */
|
|
16
|
+
quiet?: boolean
|
|
14
17
|
print?: (line: string) => void
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -19,11 +22,13 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
19
22
|
const config = await loadRepoConfig(root)
|
|
20
23
|
const env: RuleEnv = { root, config }
|
|
21
24
|
const color = process.stdout.isTTY && process.env.NO_COLOR == null
|
|
25
|
+
// the repo's own rules fix exactly like the built-ins, one code path
|
|
26
|
+
const all = [...RULES, ...(config.rules ?? [])]
|
|
22
27
|
|
|
23
|
-
let targets =
|
|
28
|
+
let targets = all.filter((r) => r.fix != null)
|
|
24
29
|
if (o.ids != null && o.ids.length > 0) {
|
|
25
30
|
for (const id of o.ids) {
|
|
26
|
-
const r =
|
|
31
|
+
const r = all.find((x) => x.id === id)
|
|
27
32
|
if (r == null) { print(`FAIL: unknown rule '${id}' — see: shipkit rules`); return 1 }
|
|
28
33
|
if (r.fix == null) { print(`FAIL: [${id}] has no autofix — fix by hand: ${config.docsBase ?? ''}${r.owner}`); return 1 }
|
|
29
34
|
}
|
|
@@ -36,7 +41,9 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
36
41
|
const before = new CheckCtx(new Map())
|
|
37
42
|
await runRuleLint(rule, before, env)
|
|
38
43
|
if (!before.failed && !before.warned) {
|
|
39
|
-
|
|
44
|
+
// inside the ci fix pass, a line per already-clean rule is a wall of
|
|
45
|
+
// nothing above the run that reports those same rules for real
|
|
46
|
+
if (o.quiet !== true) print(`ok: [${rule.id}] already clean — nothing to fix`)
|
|
40
47
|
continue
|
|
41
48
|
}
|
|
42
49
|
const f = await rule.fix!(env)
|
|
@@ -47,11 +54,11 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
47
54
|
await runRuleLint(rule, after, env)
|
|
48
55
|
// the fix already ran: advertising it again on surviving findings
|
|
49
56
|
// would send an agent in a loop
|
|
50
|
-
for (const l of after.lines) print(formatLine({ ...l, text: l.text.replace(/ · autofix: shipkit check --fix
|
|
57
|
+
for (const l of after.lines) print(formatLine({ ...l, text: l.text.replace(/ · autofix: shipkit check --fix [A-Z]{2,4}\d{3}$/, '') }, color))
|
|
51
58
|
if (after.failed) exit = 1
|
|
52
59
|
}
|
|
53
|
-
const unfixable =
|
|
54
|
-
if (unfixable > 0 && (o.ids == null || o.ids.length === 0))
|
|
55
|
-
print(`${unfixable} rule(s) have no autofix —
|
|
60
|
+
const unfixable = all.filter((r) => r.fix == null).length
|
|
61
|
+
if (unfixable > 0 && o.quiet !== true && (o.ids == null || o.ids.length === 0))
|
|
62
|
+
print(`${unfixable} rule(s) have no autofix — they report in the run below`)
|
|
56
63
|
return exit
|
|
57
64
|
}
|
|
@@ -104,7 +104,7 @@ export const ColCard = observer(({ app, col, id }: { app: HubSt; col: string; id
|
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
// collection view — one card per visible instance, the stage-side twin of a
|
|
107
|
-
// sidebar section (
|
|
107
|
+
// sidebar section (every section gets both views)
|
|
108
108
|
export const ColGrid = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
109
109
|
const c = app.cols
|
|
110
110
|
const col = c.sidebarCols.find((x) => x.id === id)
|
|
@@ -145,8 +145,7 @@ export const ColGrid = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
|
145
145
|
)
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
-
// graph item view — the graph ITSELF fills the stage
|
|
149
|
-
// "if I select a graph, I want to see it"), lint line as a slim header
|
|
148
|
+
// graph item view — the graph ITSELF fills the stage, lint line as a slim header
|
|
150
149
|
export const GraphView = observer(({ app, id }: { app: HubSt; id: string }) => {
|
|
151
150
|
const c = app.cols
|
|
152
151
|
const i = c.find('graphs', id)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// THE page (Bun.serve HTML import — no vite, no build step). One surface
|
|
2
|
-
// per repo
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// collection view (grid of cards), row = item view. mobx+react per
|
|
2
|
+
// per repo: media atelier + asks + creds + posts + health + graphs as
|
|
3
|
+
// foldable sections, one severity filter persisted at /?f=needs · /?f=red.
|
|
4
|
+
// Section title = collection view (grid of cards), row = item view. mobx+react per
|
|
6
5
|
// build/app-state-tree: HubSt is the single root (window.app), services
|
|
7
6
|
// own state, components read and call.
|
|
8
7
|
import { runInAction } from 'mobx'
|
|
@@ -66,8 +65,7 @@ const ColSections = observer(() => {
|
|
|
66
65
|
{c.sidebarCols.map((col) => {
|
|
67
66
|
const items = c.visibleInstances(col)
|
|
68
67
|
// every section header ALWAYS renders, 0 items included — a
|
|
69
|
-
// quiet section must stay reachable (
|
|
70
|
-
// done asks, present secrets)
|
|
68
|
+
// quiet section must stay reachable (done asks, present secrets)
|
|
71
69
|
const open = c.isOpen(col)
|
|
72
70
|
const health = col.id === 'health'
|
|
73
71
|
return (
|
|
@@ -121,8 +119,8 @@ const Sidebar = observer(() => (
|
|
|
121
119
|
</aside>
|
|
122
120
|
))
|
|
123
121
|
|
|
124
|
-
// ── the icon rail: quick jump to a section header (
|
|
125
|
-
//
|
|
122
|
+
// ── the icon rail: quick jump to a section header (scroll + unfold,
|
|
123
|
+
// NEVER a navigation) — red dot = the section holds a red ──
|
|
126
124
|
const RAIL_ICON: Record<string, string> = { asks: '🙋', creds: '🔑', posts: '📣', health: '📊', graphs: '🕸' }
|
|
127
125
|
const Rail = observer(() => {
|
|
128
126
|
const c = app.cols
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// HubSt — the ONE root store (build/app-state-tree: one central tree,
|
|
2
2
|
// instantiated once at boot, services with backrefs, exposed as
|
|
3
3
|
// window.app). Owns cross-service state: selection, the severity filter
|
|
4
|
-
// (an ADDRESS: /?f=needs · /?f=red
|
|
5
|
-
//
|
|
4
|
+
// (an ADDRESS: /?f=needs · /?f=red), hash deep-links (#<asset-id> ·
|
|
5
|
+
// #cat-<Category>).
|
|
6
6
|
import { makeAutoObservable } from 'mobx'
|
|
7
7
|
import { ColsSvc } from './cols-svc.ts'
|
|
8
8
|
import { HealthSvc } from './health-svc.ts'
|
|
@@ -22,10 +22,8 @@ export type Jump = { id: string; label: string; section: string; run: () => void
|
|
|
22
22
|
/** all · needs (yellow+red) · red — one severity ladder for every section */
|
|
23
23
|
export type Filter = 'all' | 'needs' | 'red'
|
|
24
24
|
const parseFilter = (search: string): Filter => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
if (f === 'needs' || f === 'red') return f
|
|
28
|
-
return sp.get('needs') === '1' ? 'needs' : 'all' // legacy address keeps working
|
|
25
|
+
const f = new URLSearchParams(search).get('f')
|
|
26
|
+
return f === 'needs' || f === 'red' ? f : 'all'
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
export class HubSt {
|
|
@@ -64,7 +62,6 @@ export class HubSt {
|
|
|
64
62
|
setFilter(v: Filter): void {
|
|
65
63
|
this.filter = v
|
|
66
64
|
const u = new URL(location.href)
|
|
67
|
-
u.searchParams.delete('needs') // legacy param never survives a click
|
|
68
65
|
v === 'all' ? u.searchParams.delete('f') : u.searchParams.set('f', v)
|
|
69
66
|
history.replaceState(null, '', u)
|
|
70
67
|
}
|
|
@@ -197,8 +197,8 @@ export const CatGrid = observer(({ app }: { app: HubSt }) => {
|
|
|
197
197
|
{m.catRows.map((row) => {
|
|
198
198
|
const fl = row.rect?.flip === true
|
|
199
199
|
return (
|
|
200
|
-
// the WHOLE card jumps into the asset
|
|
201
|
-
//
|
|
200
|
+
// the WHOLE card jumps into the asset —
|
|
201
|
+
// inner controls stopPropagation
|
|
202
202
|
<div className="card" key={row.id} onClick={() => app.selectAsset(row.id)}>
|
|
203
203
|
{isAud(row.name)
|
|
204
204
|
? <div className="aud">🔊<audio controls src={rollUrl(row.id, row.name)} onClick={(e) => e.stopPropagation()} />{m.pending.includes(row.id) ? <div className="spin" /> : null}</div>
|
package/src/serve/run-serve.ts
CHANGED
|
@@ -34,7 +34,7 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
|
|
|
34
34
|
const agentGraph = existsSync(agentRoot)
|
|
35
35
|
? { title: `${c.name} agent/`, load: () => scanMdGraph({ root: agentRoot, index: c.serve?.agentIndex }) }
|
|
36
36
|
: null
|
|
37
|
-
// ONE page for every repo
|
|
37
|
+
// ONE page for every repo: the studio ALWAYS
|
|
38
38
|
// mounts — kit-less repos get an empty media section, never a second
|
|
39
39
|
// page. A broken kit degrades to the same empty kit, loudly.
|
|
40
40
|
let kit: ResolvedKit | null = null
|
|
@@ -86,8 +86,8 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// the hub must KNOW when it is outdated
|
|
90
|
-
// process
|
|
89
|
+
// the hub must KNOW when it is outdated — a stale morning
|
|
90
|
+
// process serves broken-looking picks all day: version read at
|
|
91
91
|
// boot vs on disk per attention load; mismatch = red line telling him to
|
|
92
92
|
// restart the task
|
|
93
93
|
const libVersion = (): string => {
|
|
@@ -96,8 +96,7 @@ const libVersion = (): string => {
|
|
|
96
96
|
const BOOT_VERSION = libVersion()
|
|
97
97
|
|
|
98
98
|
// the / dashboard's data now comes from the collection providers
|
|
99
|
-
// (src/collections/providers.ts) —
|
|
100
|
-
// when they landed; never reintroduce a parallel projection.
|
|
99
|
+
// (src/collections/providers.ts) — never a parallel projection.
|
|
101
100
|
|
|
102
101
|
export async function runServe(root: string, io: ServeIo, o?: { port?: number }): Promise<number> {
|
|
103
102
|
const c = await loadRepoConfig(root)
|
package/src/serve/serve-hub.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// shipkit serve — the repo's ONE Bun.serve, serving the ONE page (
|
|
2
|
-
// rules 2026-07-23: one server per repo, one surface, mobx+react). `/` is
|
|
1
|
+
// shipkit serve — the repo's ONE Bun.serve, serving the ONE page (one server per repo, one surface, mobx+react). `/` is
|
|
3
2
|
// the hub app (Bun HTML import — media studio + asks + creds + posts +
|
|
4
3
|
// health + graphs as sections); the studio route table mounts as the media
|
|
5
4
|
// API under /media/*; graph canvases stay as drill-down pages. Route logic
|
|
@@ -32,7 +31,7 @@ export type HubDeps = {
|
|
|
32
31
|
* collection providers (run-serve builds it) */
|
|
33
32
|
needs: () => Promise<NeedItem[]>
|
|
34
33
|
/** BROWSE-ALL: the collections with every instance incl. healthy —
|
|
35
|
-
* the page shows everything that exists
|
|
34
|
+
* the page shows everything that exists, dimmed never hidden */
|
|
36
35
|
collections: () => Collection[]
|
|
37
36
|
/** asks dir (repo-root-relative) for the act-in-place ask verbs */
|
|
38
37
|
asksDir: string
|
package/src/shipkit.ts
CHANGED
|
@@ -11,6 +11,14 @@ export { projectNeeds, redCount } from './collections/collection-types.ts'
|
|
|
11
11
|
export type { Collection, Instance, NeedItem, NeedSeverity, CurationVerb } from './collections/collection-types.ts'
|
|
12
12
|
export { buildCollections } from './collections/providers.ts'
|
|
13
13
|
export { socialPostsCollection, listPosts } from './collections/posts.ts'
|
|
14
|
+
// release units — the ONE discovery every release surface reads (drift
|
|
15
|
+
// probes, per-unit tests, per-unit typecheck, the stranded-tag probe)
|
|
16
|
+
export { resolveUnits, releaseOrder, unitById, renderTag, tagGlobs, prepublishScripts, unitPrepublishScripts } from './release/units.ts'
|
|
17
|
+
export type { ReleaseUnit, ReleaseUnitDecl, UnitResolution } from './release/units.ts'
|
|
18
|
+
export { deriveRemote, dirtyPaths, remoteTags, strandedTags, tagExistsLocally } from './release/git-facts.ts'
|
|
19
|
+
export { collectReports, renderReports, attentionLines } from './release/run-releases.ts'
|
|
20
|
+
export { extractChangelogSection, promoteUnreleased, hasUnreleasedNotes, hasVersionSection } from './release/changelog.ts'
|
|
21
|
+
export type { UnitReport } from './release/run-releases.ts'
|
|
14
22
|
export { discoverChecks } from './core/discover-checks.ts'
|
|
15
23
|
export type { DiscoveredCheck } from './core/discover-checks.ts'
|
|
16
24
|
export { runTasks, selectTasks } from './core/task-runner.ts'
|
|
@@ -50,7 +58,7 @@ export { studioRoutes } from './assets/server/studio-server.ts'
|
|
|
50
58
|
export type { StudioDeps, StudioRoutes } from './assets/server/studio-server.ts'
|
|
51
59
|
export { StudioQueue } from './assets/server/studio-queue.ts'
|
|
52
60
|
export type { Job, QueueState } from './assets/server/studio-queue.ts'
|
|
53
|
-
export { cleanRect, liveRect, defaultLegs
|
|
61
|
+
export { cleanRect, liveRect, defaultLegs } from './assets/panels/geometry-rects.ts'
|
|
54
62
|
export type { GroundRect, LegRect } from './assets/panels/geometry-rects.ts'
|
|
55
63
|
export { generate, submit, waitFor, applyInputs, expectNode, randomSeed, batchFolderName } from './assets/gen/comfy-gen.ts'
|
|
56
64
|
export type { ComfyOpts } from './assets/gen/comfy-gen.ts'
|
package/src/tui/checks-panel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// built-in panel: contracts + every discovered *.check.ts through runCheck
|
|
2
2
|
// (both TTL cache layers intact) — per-check results stream in via the
|
|
3
|
-
// onResult tap. status:false:
|
|
3
|
+
// onResult tap. status:false: no TUI panel writes STATUS.md (tasks-panel.ts).
|
|
4
4
|
import { discoverChecks } from '../core/discover-checks.ts'
|
|
5
5
|
import { runCheck } from '../core/run-check.ts'
|
|
6
6
|
import { formatLine } from '../core/wire.ts'
|
package/src/tui/run-tui.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// The tui command body — a separate module so cli.ts NEVER loads
|
|
2
|
-
// react/ink/mobx for non-tui commands
|
|
3
|
-
// must not touch the react dependency chain
|
|
4
|
-
// crashed every command on repos without react-dom).
|
|
2
|
+
// react/ink/mobx for non-tui commands: a fresh consumer running `shipkit ci`
|
|
3
|
+
// must not touch the react dependency chain.
|
|
5
4
|
import React from 'react'
|
|
6
5
|
import { render } from 'ink'
|
|
7
6
|
import { configure } from 'mobx'
|
package/src/tui/tasks-panel.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// built-in panel: the ci gate (floor + contracts + repo tasks)
|
|
2
|
-
// runCi — results stream in via the onTaskFinish tap.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// built-in panel: the ci gate's TASKS (floor + contracts + repo tasks)
|
|
2
|
+
// through runCi — results stream in via the onTaskFinish tap. The checks are
|
|
3
|
+
// skipped here on purpose: the Checks panel runs the same suite and renders
|
|
4
|
+
// it, and running it twice per refresh costs twice and proves the same thing.
|
|
5
|
+
// no panel writes STATUS.md: a dashboard refresh runs the tasks alone, so
|
|
6
|
+
// stamping a verdict from it would overwrite what a real gate proved with a
|
|
7
|
+
// narrower claim, just by looking at the repo.
|
|
5
8
|
import type { RepoConfig } from '../repo-config.ts'
|
|
6
9
|
import { allTasks } from '../repo-config.ts'
|
|
7
10
|
import { runCi } from '../core/run-ci.ts'
|
|
@@ -38,6 +41,9 @@ export function tasksPanel(root: string, config: RepoConfig): TuiPanelDef {
|
|
|
38
41
|
const byKey = new Map(rows.map((r, i) => [r.key, i]))
|
|
39
42
|
update()
|
|
40
43
|
await runCi(root, {
|
|
44
|
+
config,
|
|
45
|
+
skipChecks: true,
|
|
46
|
+
status: false,
|
|
41
47
|
print: () => {}, // rows carry everything the wire lines would
|
|
42
48
|
onTaskFinish: (r) => {
|
|
43
49
|
const i = byKey.get(r.id)
|
package/src/tui/tui-store.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// TUI state — MobX per the house playbook (build/app-state-tree + build/mobx-mechanics):
|
|
2
|
-
// `makeAutoObservable` WITH exceptions (
|
|
3
|
-
//
|
|
2
|
+
// `makeAutoObservable` WITH exceptions (no decorators), runInAction around
|
|
3
|
+
// every post-await mutation. Panels are
|
|
4
4
|
// plain data providers (no mobx): they signal mutations through update(),
|
|
5
5
|
// which bumps the observable `version` counter every rows() read tracks.
|
|
6
6
|
import { makeAutoObservable, runInAction } from 'mobx'
|