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 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.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
  }
@@ -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
- import React from 'react'
5
- import { render } from 'ink'
6
- import { configure } from 'mobx'
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 root = process.cwd()
25
- const c = await loadRepoConfig(root)
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
+ }
@@ -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
- for (const k of ['ci', 'check', 'contracts'] as const) {
44
- const v = run(k)
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
+ }