rvlib-shipkit 0.3.0 → 0.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 +33 -0
- package/package.json +2 -1
- package/src/cli/cmd_Tui.ts +5 -19
- package/src/core/package-json-check.ts +87 -0
- package/src/core/run-check.ts +7 -0
- package/src/init-repo.ts +18 -2
- package/src/tui/run-tui.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-07-22
|
|
4
|
+
|
|
5
|
+
New (wave 1 of the shipkit expansion — uniform repo setup):
|
|
6
|
+
|
|
7
|
+
- built-in `package.json` probe in `shipkit check`
|
|
8
|
+
(`core/package-json-check.ts`): FAILs on `license` present / `private` not
|
|
9
|
+
true / missing capability-required scripts (repo.config → ci check
|
|
10
|
+
contracts tui hub; `.rv-assets/kit.ts` → assets; `.rv-identity/brand.ts`
|
|
11
|
+
→ assets:identity; `.rv-comfy/` → comfy:up; `deploy.sh` → up; ask dir →
|
|
12
|
+
asks); WARNs on banned aliases (`serve`→`hub`, `identity`→
|
|
13
|
+
`assets:identity`, `pretty`→`fmt`, `check-types`→`typecheck`; body-matched
|
|
14
|
+
so an unrelated `serve` is left alone) and a missing `test`/`lint`/`fmt`
|
|
15
|
+
baseline. rvlib-* roots invert (MIT required, private banned). Spec:
|
|
16
|
+
wiki `meta/rv-conventions/package-json.md`. Doctor surface only — not in
|
|
17
|
+
the ci gate.
|
|
18
|
+
- `init` scaffolds the full canonical script set: `ci check contracts tui
|
|
19
|
+
hub` (hub runs `shipkit serve`) + a `test: bun test` baseline — was
|
|
20
|
+
ci/check/contracts only.
|
|
21
|
+
- upgraders from 0.3.x: the probe FAILs on the missing `tui`/`hub` scripts —
|
|
22
|
+
re-run `shipkit init` (non-clobber, it only adds the gaps) and rename any
|
|
23
|
+
`serve` hub-script to `hub`.
|
|
24
|
+
|
|
25
|
+
## 0.3.1 — 2026-07-22
|
|
26
|
+
|
|
27
|
+
- fix: `react-dom` was missing from dependencies AND the CLI entry loaded
|
|
28
|
+
the react/ink chain eagerly — every command crashed in a fresh consumer
|
|
29
|
+
repo without react-dom (found by the rabbits-vs-sheeps swap in the field,
|
|
30
|
+
confirmed against the 0.3.0 tarball). tui now lazy-loads its body; deps
|
|
31
|
+
carry react-dom. Proven by a fresh-repo install test (init → ci green in
|
|
32
|
+
a react-less repo).
|
|
33
|
+
- fix: `init` warns loudly when rvlib-shipkit is not installed in the repo
|
|
34
|
+
(the minted scripts call its bin — previously exit 127 with no hint).
|
|
35
|
+
|
|
3
36
|
## 0.3.0 — 2026-07-22
|
|
4
37
|
|
|
5
38
|
New:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"mobx": "^6.16.1",
|
|
30
30
|
"mobx-react-lite": "^4.1.1",
|
|
31
31
|
"react": "^19.2.7",
|
|
32
|
+
"react-dom": "^19.2.7",
|
|
32
33
|
"rvlib-ts-tools": "^0.2.0",
|
|
33
34
|
"sharp": "^0.35.3"
|
|
34
35
|
}
|
package/src/cli/cmd_Tui.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
// shipkit tui — the live dashboard: Tasks (the ci gate, streamed through
|
|
2
2
|
// the DAG runner) + Checks (contracts + discovered *.check.ts) built in,
|
|
3
3
|
// repo extras injected via repo.config `tui: { panels: [...] }`.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// The body lives in src/tui/run-tui.ts and is loaded ONLY when the command
|
|
5
|
+
// runs: the react/ink chain must never load for `shipkit ci` in a repo
|
|
6
|
+
// without react-dom.
|
|
7
7
|
import { Command } from 'clipanion'
|
|
8
|
-
import { loadRepoConfig } from '../repo-config.ts'
|
|
9
|
-
import { checksPanel } from '../tui/checks-panel.ts'
|
|
10
|
-
import { tasksPanel } from '../tui/tasks-panel.ts'
|
|
11
|
-
import { TuiApp } from '../tui/tui-app.tsx'
|
|
12
|
-
import { TuiStore } from '../tui/tui-store.ts'
|
|
13
8
|
|
|
14
9
|
export class TuiCommand extends Command {
|
|
15
10
|
static override paths = [['tui']]
|
|
@@ -21,16 +16,7 @@ export class TuiCommand extends Command {
|
|
|
21
16
|
})
|
|
22
17
|
|
|
23
18
|
async execute(): Promise<number> {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
// playbook (build/mobx): enforceActions warns on stray mutations — the
|
|
27
|
-
// only signal. Read-warning flags stay off: useInput handler reads are
|
|
28
|
-
// legitimately untracked and would false-positive.
|
|
29
|
-
configure({ enforceActions: 'always' })
|
|
30
|
-
const store = new TuiStore(c.name, [tasksPanel(root, c), checksPanel(root), ...(c.tui?.panels ?? [])])
|
|
31
|
-
store.refresh() // instant static paint; results stream in
|
|
32
|
-
const app = render(React.createElement(TuiApp, { store }))
|
|
33
|
-
await app.waitUntilExit()
|
|
34
|
-
return 0
|
|
19
|
+
const { runTui } = await import('../tui/run-tui.ts')
|
|
20
|
+
return runTui(process.cwd())
|
|
35
21
|
}
|
|
36
22
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// built-in `package.json` probe for `shipkit check` — enforces the canonical
|
|
2
|
+
// shape (meta/rv-conventions/package-json.md): private:true, no license,
|
|
3
|
+
// capability-required script names FAIL, banned aliases + missing baseline
|
|
4
|
+
// trio WARN. Doctor surface only — never wired into the ci gate. Pure local
|
|
5
|
+
// fs, no TTL.
|
|
6
|
+
import { existsSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import type { CheckCtx } from './check-ctx.ts'
|
|
9
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
10
|
+
|
|
11
|
+
type Pkg = { name?: string; private?: boolean; license?: string; scripts?: Record<string, string> }
|
|
12
|
+
|
|
13
|
+
// alias → { canonical, bodyMatch } — WARN only when the body proves the
|
|
14
|
+
// alias really is the canonical capability (a `serve` that serves a dist/
|
|
15
|
+
// folder must not be told to rename)
|
|
16
|
+
const ALIASES: Record<string, { canonical: string; bodyMatch: RegExp }> = {
|
|
17
|
+
'serve': { canonical: 'hub', bodyMatch: /(shipkit|cli\.ts) serve\b/ },
|
|
18
|
+
'identity': { canonical: 'assets:identity', bodyMatch: /assets(:| )identity\b/ },
|
|
19
|
+
'pretty': { canonical: 'fmt', bodyMatch: /./ },
|
|
20
|
+
'check-types': { canonical: 'typecheck', bodyMatch: /./ },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BASELINE = ['test', 'lint', 'fmt'] as const
|
|
24
|
+
|
|
25
|
+
export async function evaluatePackageJson(t: CheckCtx, root: string, c: RepoConfig): Promise<void> {
|
|
26
|
+
const spec = `${c.docsBase ?? ''}meta/rv-conventions/package-json.md`
|
|
27
|
+
let pkg: Pkg
|
|
28
|
+
try {
|
|
29
|
+
pkg = (await Bun.file(join(root, 'package.json')).json()) as Pkg
|
|
30
|
+
} catch (e) {
|
|
31
|
+
t.fail(`package.json unreadable (${String(e).slice(0, 120)})`, spec)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const scripts = pkg.scripts ?? {}
|
|
35
|
+
// rvlib-* as a probe ROOT is theoretical (libs live as sub-packages and
|
|
36
|
+
// the probe only runs where repo.config.ts loads) — field inversion is
|
|
37
|
+
// still handled so a future lib-root repo fails correctly, not absurdly
|
|
38
|
+
const isLib = pkg.name?.startsWith('rvlib-') === true
|
|
39
|
+
|
|
40
|
+
// published rvlib-* roots invert the field rules (distribute/npm owns them)
|
|
41
|
+
if (isLib) {
|
|
42
|
+
if (pkg.license !== 'MIT') t.fail(`package.json: rvlib-* package must carry license MIT`, spec)
|
|
43
|
+
if (pkg.private === true) t.fail(`package.json: rvlib-* package must not be private`, spec)
|
|
44
|
+
} else {
|
|
45
|
+
if (pkg.private !== true) t.fail(`package.json: "private": true is required`, spec)
|
|
46
|
+
if (pkg.license != null) t.fail(`package.json: license field is banned on project repos (rvlib-* sub-packages excepted)`, spec)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// aliases first — an alias satisfies the requirement but earns a WARN,
|
|
50
|
+
// never a double report (alias WARN + missing-canonical FAIL). A banned
|
|
51
|
+
// alias lingering NEXT TO its canonical still WARNs (sweep on sight),
|
|
52
|
+
// it just doesn't satisfy anything.
|
|
53
|
+
const aliased = new Set<string>()
|
|
54
|
+
for (const [name, a] of Object.entries(ALIASES)) {
|
|
55
|
+
const body = scripts[name]
|
|
56
|
+
if (body == null || !a.bodyMatch.test(body)) continue
|
|
57
|
+
if (scripts[a.canonical] == null) aliased.add(a.canonical)
|
|
58
|
+
t.warn(`package.json: script '${name}' is the house '${a.canonical}' — ${scripts[a.canonical] == null ? 'rename it' : `delete it ('${a.canonical}' exists)`}`, spec)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const has = (name: string): boolean => scripts[name] != null || aliased.has(name)
|
|
62
|
+
|
|
63
|
+
// capability → required script names (FAIL); repo.config.ts existence is
|
|
64
|
+
// implied — this probe only runs under `shipkit check`
|
|
65
|
+
const required: Array<{ name: string; why: string }> = [
|
|
66
|
+
{ name: 'ci', why: 'shipkit repo' },
|
|
67
|
+
{ name: 'check', why: 'shipkit repo' },
|
|
68
|
+
{ name: 'contracts', why: 'shipkit repo' },
|
|
69
|
+
{ name: 'tui', why: 'shipkit repo' },
|
|
70
|
+
{ name: 'hub', why: 'shipkit repo' },
|
|
71
|
+
]
|
|
72
|
+
if (existsSync(join(root, '.rv-assets/kit.ts'))) required.push({ name: 'assets', why: '.rv-assets/kit.ts exists' })
|
|
73
|
+
if (existsSync(join(root, '.rv-identity/brand.ts'))) required.push({ name: 'assets:identity', why: '.rv-identity/brand.ts exists' })
|
|
74
|
+
if (existsSync(join(root, '.rv-comfy'))) required.push({ name: 'comfy:up', why: '.rv-comfy/ exists' })
|
|
75
|
+
if (existsSync(join(root, 'deploy.sh'))) required.push({ name: 'up', why: 'deploy.sh exists' })
|
|
76
|
+
if (existsSync(join(root, c.asks?.dir ?? 'asks'))) required.push({ name: 'asks', why: 'ask queue exists' })
|
|
77
|
+
|
|
78
|
+
const missing = required.filter((r) => !has(r.name))
|
|
79
|
+
for (const m of missing) t.fail(`package.json: missing script '${m.name}' (${m.why})`, spec)
|
|
80
|
+
|
|
81
|
+
const missingBaseline = BASELINE.filter((n) => !has(n))
|
|
82
|
+
if (missingBaseline.length > 0)
|
|
83
|
+
t.warn(`package.json: missing baseline script(s): ${missingBaseline.join(', ')}`, spec)
|
|
84
|
+
|
|
85
|
+
if (!t.failed && !t.warned)
|
|
86
|
+
t.ok(`package.json shape ok (private, no license, ${required.length} required scripts present)`)
|
|
87
|
+
}
|
package/src/core/run-check.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { CheckSuite, type CheckOutcome } from './check-suite.ts'
|
|
|
9
9
|
import { discoverChecks } from './discover-checks.ts'
|
|
10
10
|
import { spawnWithTimeout } from './spawn-timeout.ts'
|
|
11
11
|
import { activeContracts, applyContractsResult, contractsConfigFrom, loadRepoConfig } from '../repo-config.ts'
|
|
12
|
+
import { evaluatePackageJson } from './package-json-check.ts'
|
|
12
13
|
import { evaluateContracts } from '../contracts/contracts-engine.ts'
|
|
13
14
|
import { formatLine, parseWireOutput } from './wire.ts'
|
|
14
15
|
import { writeStatus } from './write-status.ts'
|
|
@@ -40,6 +41,12 @@ export async function runCheck(root: string, o: CheckOpts = {}): Promise<number>
|
|
|
40
41
|
applyContractsResult(t, await evaluateContracts(activeContracts(c), contractsConfigFrom(c, root)))
|
|
41
42
|
},
|
|
42
43
|
})
|
|
44
|
+
// built-in shape probe (meta/rv-conventions/package-json.md) — doctor
|
|
45
|
+
// surface only, deliberately absent from the ci gate
|
|
46
|
+
suite.addCheck({
|
|
47
|
+
name: 'package.json',
|
|
48
|
+
logic: (t) => evaluatePackageJson(t, root, c),
|
|
49
|
+
})
|
|
43
50
|
const childEnv: Record<string, string | undefined> = {
|
|
44
51
|
...process.env,
|
|
45
52
|
...(o.fresh === true ? { SHIPKIT_FRESH: '1', RV_CHECK_FRESH: '1' } : {}),
|
package/src/init-repo.ts
CHANGED
|
@@ -40,11 +40,17 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
40
40
|
pkg.scripts ??= {}
|
|
41
41
|
const run = (cmd: string): string => (opts.bin != null ? `bun ${opts.bin} ${cmd}` : `shipkit ${cmd}`)
|
|
42
42
|
let added = 0
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// canonical script NAME → shipkit command (hub runs `shipkit serve`) —
|
|
44
|
+
// vocabulary owned by meta/rv-conventions/package-json.md
|
|
45
|
+
const CANONICAL = { ci: 'ci', check: 'check', contracts: 'contracts', tui: 'tui', hub: 'serve' } as const
|
|
46
|
+
for (const [k, cmd] of Object.entries(CANONICAL)) {
|
|
47
|
+
const v = run(cmd)
|
|
45
48
|
if (pkg.scripts[k] == null) { pkg.scripts[k] = v; added++ }
|
|
46
49
|
else if (pkg.scripts[k] !== v) console.log(`skip: package.json script '${k}' exists — left untouched`)
|
|
47
50
|
}
|
|
51
|
+
// baseline `test` is universal (bun test) — lint/fmt bodies are
|
|
52
|
+
// stack-specific, the scaffold pages own those
|
|
53
|
+
if (pkg.scripts['test'] == null) { pkg.scripts['test'] = 'bun test'; added++ }
|
|
48
54
|
if (added > 0) {
|
|
49
55
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + '\n')
|
|
50
56
|
console.log(`ok: ${added} package.json script(s) added`)
|
|
@@ -54,6 +60,16 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
54
60
|
const floor = resolveFloor(defineRepo({ name }), root)
|
|
55
61
|
for (const t of floor.tasks) console.log(`ok: floor ${t.id} ← ${floor.sources[t.id] ?? 'unknown'}`)
|
|
56
62
|
for (const u of floor.unfilled) console.log(`skip: ${u.id} (${u.reason})`)
|
|
63
|
+
// scripts call the `shipkit` bin — dead on arrival unless the package is
|
|
64
|
+
// actually installed (field bug 2026-07-22: init via bunx, no dep, exit 127)
|
|
65
|
+
if (opts.bin == null) {
|
|
66
|
+
try {
|
|
67
|
+
Bun.resolveSync('rvlib-shipkit', root)
|
|
68
|
+
} catch {
|
|
69
|
+
console.log('WARN rvlib-shipkit not installed here — the scripts above call its bin and will exit 127')
|
|
70
|
+
console.log(' fix: bun add -d rvlib-shipkit')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
57
73
|
console.log('next: fill venues/social in repo.config.ts, then `bun run ci`')
|
|
58
74
|
console.log('next: identity+assets — mint .rv-identity/brand.ts + .rv-assets/kit.ts, `shipkit assets identity` (the identity contract expects public/ favicon+og+logo)')
|
|
59
75
|
return 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The tui command body — a separate module so cli.ts NEVER loads
|
|
2
|
+
// react/ink/mobx for non-tui commands (a fresh consumer running `shipkit ci`
|
|
3
|
+
// must not touch the react dependency chain; field bug 2026-07-22: 0.3.0
|
|
4
|
+
// crashed every command on repos without react-dom).
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { render } from 'ink'
|
|
7
|
+
import { configure } from 'mobx'
|
|
8
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
9
|
+
import { checksPanel } from './checks-panel.ts'
|
|
10
|
+
import { tasksPanel } from './tasks-panel.ts'
|
|
11
|
+
import { TuiApp } from './tui-app.tsx'
|
|
12
|
+
import { TuiStore } from './tui-store.ts'
|
|
13
|
+
|
|
14
|
+
export async function runTui(root: string): Promise<number> {
|
|
15
|
+
const c = await loadRepoConfig(root)
|
|
16
|
+
// playbook (build/mobx): enforceActions warns on stray mutations — the
|
|
17
|
+
// only signal. Read-warning flags stay off: useInput handler reads are
|
|
18
|
+
// legitimately untracked and would false-positive.
|
|
19
|
+
configure({ enforceActions: 'always' })
|
|
20
|
+
const store = new TuiStore(c.name, [tasksPanel(root, c), checksPanel(root), ...(c.tui?.panels ?? [])])
|
|
21
|
+
store.refresh() // instant static paint; results stream in
|
|
22
|
+
const app = render(React.createElement(TuiApp, { store }))
|
|
23
|
+
await app.waitUntilExit()
|
|
24
|
+
return 0
|
|
25
|
+
}
|