rvlib-shipkit 2.4.0 → 2.6.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +2 -2
  3. package/src/cli/cmd_Commands.ts +26 -5
  4. package/src/cli/cmd_FleetPush.ts +47 -0
  5. package/src/cli/cmd_GlobalProcesses.ts +5 -5
  6. package/src/cli/cmd_Review.ts +16 -1
  7. package/src/cli/cmd_Trailer.ts +4 -1
  8. package/src/cli/family-pages.ts +49 -0
  9. package/src/cli/kit/builtins.ts +1 -0
  10. package/src/cli/registry.ts +137 -0
  11. package/src/cli.ts +47 -135
  12. package/src/core/last-good.ts +217 -0
  13. package/src/core/load-probe.ts +71 -0
  14. package/src/core/reexec.ts +5 -3
  15. package/src/fleet/push-fleet.ts +80 -0
  16. package/src/home/home-routes.ts +12 -2
  17. package/src/home/lib-watch.ts +110 -20
  18. package/src/home/process-rows.ts +4 -2
  19. package/src/home/run-daemon.ts +1 -1
  20. package/src/review/model.ts +19 -0
  21. package/src/review/paste.ts +30 -0
  22. package/src/review/routes.ts +10 -3
  23. package/src/scaffold/files/agent-index.md +2 -2
  24. package/src/scaffold/project-scaffold.ts +2 -2
  25. package/src/serve/hub-app/activity-st.ts +31 -0
  26. package/src/serve/hub-app/activity-view.tsx +9 -0
  27. package/src/serve/hub-app/activity.css +6 -0
  28. package/src/serve/hub-app/home-strip.tsx +3 -1
  29. package/src/serve/hub-app/machine-st.ts +1 -1
  30. package/src/serve/hub-app/review/review-answer.tsx +16 -37
  31. package/src/serve/hub-app/review/review-page.tsx +8 -1
  32. package/src/serve/hub-app/review/review-st.ts +51 -3
  33. package/src/serve/hub-app/review/review-views.tsx +1 -1
  34. package/src/serve/hub-app/review.css +4 -4
  35. package/src/serve/run-serve.ts +25 -0
  36. package/src/serve/serve-hub.ts +8 -1
  37. package/src/serve/stale-restart.ts +19 -2
  38. package/src/trailer/build.ts +16 -7
  39. package/src/trailer/ffmpeg.ts +21 -1
  40. package/src/trailer/grammar.ts +56 -11
  41. package/src/trailer/look.ts +31 -16
  42. package/src/trailer/prune.ts +59 -12
  43. package/src/trailer/score/dsp.ts +87 -27
  44. package/src/trailer/score/pattern.ts +1 -1
  45. package/src/trailer/score/synth.ts +26 -5
  46. package/src/trailer/titles.ts +63 -34
  47. package/src/trailer/vocab.ts +65 -6
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 2.6.0
2
+
3
+ - **A trailer's score sounds as the game's browser synth, section for section.** A note that starts inside a 128 frame block takes the phase the browser gives it (it runs at the default 440 Hz until its own frame of that block), so overlapping notes of one pitch, a timpani roll, add or cancel as in the browser; `osc: 'square'` is the browser's own square wave, no longer a sine; the master's compressor and limiter are the browser's kernel (32 frame divisions, adaptive release, the sine warp on the gain). pkm-multi's main4 score: 38 of 38 sections within 0.1 dB of the game's render (were 18), each octave band within 0.15 dB. Every score renders again once.
4
+
5
+ ## 2.5.0
6
+
7
+ - **A review card hands itself to any agent session.** A small copy icon on the card's header (or C) copies one block: its title and state, its repo, `cd <repo> && sk review show <id>`, its link here and on the phone when the repo has a public host; a toast says what was copied. `sk review show <id> --paste` prints the same block.
8
+ - **The overall comment box sits under the card's header**, after the verdicts, on every card and in every layout.
9
+ - **Push every unpushed repo: `sk fleet push` and a button on the activity page.** Each house repo with commits its remote lacks gets a plain `git push`, never a force; a branch with no upstream goes `-u` to its one remote; a branch that is also behind, a gone upstream or a detached head is skipped with the reason. `--dry` plans only.
10
+
11
+ - **A new repo's agent/index.md routes with `→`.** The scaffold's table rows, the project CLI's row and its `next:` hint write `→ agent/<page>.md`, the wiki's form for a page to open, instead of a backticked path.
12
+
1
13
  ## 2.3.0
2
14
 
3
15
  - **A sound is a review view.** `sk review add --audio <sound>`, a sound in `--view`, `add-view <id> "<label>" --audio <sound>`, or a before and after of two sounds: the store keeps the sound and draws its waveform (`wave.png`, `before-wave.png`), and the hub shows each waveform over a player, one row a side. A sound takes reactions and words, never a rectangle; a pair is never a sound and an image.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "funding": "https://github.com/sponsors/rvion",
5
5
  "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",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@floating-ui/react": "^0.27.20",
39
- "@napi-rs/canvas": "^1.0.2",
39
+ "@napi-rs/canvas": "^1.1.0",
40
40
  "clipanion": "^4.0.0-rc.4",
41
41
  "comfy-ts": "^2.9.0",
42
42
  "ink": "^7.1.1",
@@ -5,6 +5,7 @@
5
5
  // (the kit in cli/kit/) come out of these functions.
6
6
  import { Command, Option } from 'clipanion'
7
7
  import { spliceMarkedBlock } from '../core/doc-block.ts'
8
+ import { FAMILY_PAGES } from './family-pages.ts'
8
9
  import { type Essential, type Film, marksOf, ROW_OF, runText } from './kit/verb.ts'
9
10
 
10
11
  export type CommandOption = { definition: string; description: string }
@@ -117,12 +118,13 @@ export function renderMarkdown(rows: CommandRow[], bin = 'sk'): string {
117
118
 
118
119
  /** the eager form: one line per command, its positional arguments and its
119
120
  * description. Flags and examples are `<bin> <command> --help`, so a page every
120
- * session loads costs a line per command, not a screen */
121
- export function renderCompact(rows: CommandRow[], bin = 'sk'): string {
121
+ * session loads costs a line per command, not a screen. `pages` puts each
122
+ * family's owner page in its heading: `### Trailer → build/trailer.md` */
123
+ export function renderCompact(rows: CommandRow[], bin = 'sk', pages: Readonly<Record<string, string>> | null = null): string {
122
124
  const out: string[] = []
123
125
  let cat = ''
124
126
  for (const r of rows) {
125
- if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}\n`) }
127
+ if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}${pages?.[cat] != null ? ` → ${pages[cat]}` : ''}\n`) }
126
128
  const sig = r.usage.replace(/^\$0/, bin).replace(/\s+\[(?:-|<\w+>)[^\]]*\]/g, '').trim() || `${bin} ${r.path}`
127
129
  out.push(`- \`${sig}\` — ${r.description}`)
128
130
  }
@@ -154,6 +156,15 @@ export function renderEssentials(rows: CommandRow[], bin: string, command: strin
154
156
  return `${out.join('\n')}\n`
155
157
  }
156
158
 
159
+ /** a family with no owner page, and a page for a family no command has: one line each */
160
+ export function familyProblems(rows: CommandRow[], pages: Readonly<Record<string, string>>): string[] {
161
+ const families = new Set(rows.map((r) => r.category))
162
+ return [
163
+ ...[...families].filter((f) => pages[f] == null).map((f) => `family ${f}: no owner page in src/cli/family-pages.ts`),
164
+ ...Object.keys(pages).filter((f) => !families.has(f)).map((f) => `family ${f}: src/cli/family-pages.ts names a page for a family no command has`),
165
+ ]
166
+ }
167
+
157
168
  /** what a project CLI's registry must hold for its pages and its listing to
158
169
  * teach right: a category, a description under the cap, and an example that
159
170
  * starts with the command's own words. One line per breach */
@@ -209,6 +220,9 @@ export class CommandsCommand extends Command {
209
220
  * a breach), and its --out creates a missing page */
210
221
  static shapeRule = false
211
222
 
223
+ /** each family's owner page, named in the compact page's headings; a project CLI has none */
224
+ static familyPages: Readonly<Record<string, string>> | null = FAMILY_PAGES
225
+
212
226
  json = Option.Boolean('--json', false, { description: 'machine-readable rows, with film and essential where a command carries them' })
213
227
  out = Option.String('--out', { description: 'host file carrying the <bin>:commands markers. The block between them is rewritten' })
214
228
  check = Option.Boolean('--check', false, { description: 'with --out or --essentials: exit 1 if a page is stale, write nothing' })
@@ -225,10 +239,17 @@ export class CommandsCommand extends Command {
225
239
  classes,
226
240
  )
227
241
  const write = (s: string): void => void this.context.stdout.write(s)
242
+ const pages = (this.constructor as typeof CommandsCommand).familyPages
243
+ const render = (): string => (this.compact ? renderCompact(rows, bin, pages) : renderMarkdown(rows, bin))
228
244
  if (this.out == null && this.essentials == null) {
229
- write(this.json ? `${JSON.stringify(rows)}\n` : `${(this.compact ? renderCompact : renderMarkdown)(rows, bin)}\n`)
245
+ write(this.json ? `${JSON.stringify(rows)}\n` : `${render()}\n`)
230
246
  return 0
231
247
  }
248
+ if (this.out != null && this.compact && pages != null) {
249
+ const bad = familyProblems(rows, pages)
250
+ for (const b of bad) write(`FAIL: ${b}\n`)
251
+ if (bad.length > 0) { write(' fix: one row per family in src/cli/family-pages.ts, the wiki page that owns it\n'); return 1 }
252
+ }
232
253
  if ((this.constructor as typeof CommandsCommand).shapeRule) {
233
254
  const bad = shapeProblems(rows, bin)
234
255
  for (const b of bad) write(`FAIL: ${b}\n`)
@@ -250,7 +271,7 @@ export class CommandsCommand extends Command {
250
271
  const project = (this.constructor as typeof CommandsCommand).shapeRule
251
272
  if (!existsSync(this.out) && (!project || this.check)) { write(`FAIL: ${this.out} does not exist\n`); return 1 }
252
273
  const host = existsSync(this.out) ? readFileSync(this.out, 'utf8') : newCommandsPage(bin, this.out)
253
- const next = spliceBlock(host, (this.compact ? renderCompact : renderMarkdown)(rows, bin), bin)
274
+ const next = spliceBlock(host, render(), bin)
254
275
  if (next == null) { write(`FAIL: ${this.out} has no ${begin} / ${end} markers\n`); return 1 }
255
276
  if (next === host) { write(`ok: ${this.out} command index current (${rows.length} commands)\n`); return code }
256
277
  if (this.check) {
@@ -0,0 +1,47 @@
1
+ // shipkit fleet push — every house repo that holds commits its remote does
2
+ // not, pushed: a plain git push, never a force (fleet/push-fleet.ts, the same
3
+ // path as the activity page's push button). A repo that fell behind waits for
4
+ // a pull, and says so.
5
+ import { Command, Option } from 'clipanion'
6
+ import { homedir } from 'os'
7
+ import { gitRoot } from '../core/git-root.ts'
8
+ import { pushFleet } from '../fleet/push-fleet.ts'
9
+ import { houseRepos } from '../serve/project-gutter.ts'
10
+ import { listFlag } from './list-flag.ts'
11
+
12
+ export class FleetPushCommand extends Command {
13
+ static override paths = [['fleet', 'push']]
14
+
15
+ static override usage = Command.Usage({
16
+ category: 'Health',
17
+ description: 'push every house repo with unpushed commits: a plain git push, never a force',
18
+ examples: [
19
+ ['push what is unpushed, everywhere', 'sk fleet push'],
20
+ ['what would go out', 'sk fleet push --dry'],
21
+ ['some repos only', 'sk fleet push --only my-app --only other'],
22
+ ],
23
+ })
24
+
25
+ dry = Option.Boolean('--dry', false, { description: 'plan only: say what would be pushed and what is skipped, push nothing' })
26
+ only = Option.Array('--only', { description: 'limit to these repo names (repeatable)' })
27
+ all = Option.Boolean('--all', false, { description: 'also print the repos that have nothing to push' })
28
+
29
+ async execute(): Promise<number> {
30
+ const root = gitRoot(process.cwd())?.top ?? process.cwd()
31
+ let repos = houseRepos(root, homedir()).repos.map((r) => ({ name: r.name, path: r.path }))
32
+ const only = listFlag(this.only)
33
+ if (only.length > 0) repos = repos.filter((r) => only.includes(r.name))
34
+ const w = (l: string) => this.context.stdout.write(`${l}\n`)
35
+ if (repos.length === 0) {
36
+ w(`skip: no house repo found from ${root}`)
37
+ return 0
38
+ }
39
+ const res = await pushFleet(repos, { dry: this.dry })
40
+ for (const r of res) if (this.all || r.action !== 'none') w(r.line)
41
+ const pushed = res.filter((r) => r.ok && (r.action === 'push' || r.action === 'push-upstream')).length
42
+ const failed = res.filter((r) => !r.ok && r.action !== 'skip').length
43
+ const skipped = res.filter((r) => r.action === 'skip').length
44
+ w(`${failed > 0 ? 'FAIL' : 'ok'}: ${repos.length} repos, ${this.dry ? `${res.filter((r) => r.action === 'push' || r.action === 'push-upstream').length} would push` : `${pushed} pushed`}, ${skipped} skipped${failed > 0 ? `, ${failed} failed` : ''}`)
45
+ return failed > 0 ? 1 : 0
46
+ }
47
+ }
@@ -28,7 +28,7 @@ export type GlobalProcessesAnswer = {
28
28
  export type GlobalProcessesDeps = {
29
29
  pm: () => Promise<PmAnswer | null>
30
30
  /** the daemon's identity, for its version; null when it does not answer */
31
- daemon?: () => Promise<{ version: string } | null>
31
+ daemon?: () => Promise<{ version: string; held?: string | null } | null>
32
32
  /** the declared units, read like pm reads them; null when no house is found */
33
33
  declared: () => Promise<Pick<PmConfig, 'processes' | 'errors'> | null>
34
34
  /** the project list's order */
@@ -43,18 +43,18 @@ const localPorts = () => ({ pm: pmPort(), daemon: daemonPort(), web: homePort()
43
43
  * pm does not answer */
44
44
  export async function globalProcessesAnswer(d: GlobalProcessesDeps): Promise<GlobalProcessesAnswer> {
45
45
  const [pm, daemon] = await Promise.all([d.pm(), d.daemon?.() ?? null])
46
- const core = { daemonVersion: daemon?.version ?? null, ports: localPorts() }
46
+ const core = { daemonVersion: daemon?.version ?? null, daemonHeld: daemon?.held ?? null, ports: localPorts() }
47
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
48
  const c = await d.declared()
49
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
50
  }
51
51
 
52
52
  /** the daemon's identity at /hub.json, or null */
53
- export async function askDaemon(url = daemonUrl()): Promise<{ version: string } | null> {
53
+ export async function askDaemon(url = daemonUrl()): Promise<{ version: string; held: string | null } | null> {
54
54
  try {
55
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
56
+ const j = r.ok ? ((await r.json()) as { hub?: string; version?: string; held?: string | null }) : null
57
+ return j?.hub === 'shipkit-home' && typeof j.version === 'string' ? { version: j.version, held: j.held ?? null } : null
58
58
  } catch {
59
59
  return null
60
60
  }
@@ -345,8 +345,14 @@ export class ReviewLsCommand extends Command {
345
345
 
346
346
  export class ReviewShowCommand extends Command {
347
347
  static override paths = [['review', 'show']]
348
- static override usage = Command.Usage({ category: 'Review', description: 'one item and its thread: the verdict, rectangles, reactions, the whole conversation', details: `${DETAILS}\n\nTakes an item id or a player report's ref (live#3).` })
348
+ static override usage = Command.Usage({
349
+ category: 'Review',
350
+ description: 'one item and its thread: the verdict, rectangles, reactions, the whole conversation',
351
+ details: `${DETAILS}\n\nTakes an item id or a player report's ref (live#3). --paste prints the short block that hands the card to another agent session (the hub's copy button gives the same).`,
352
+ examples: [['hand a card to another session', 'sk review show <id> --paste']],
353
+ })
349
354
  id = Option.String()
355
+ paste = Option.Boolean('--paste', false, { description: 'the block another agent session acts on: title, repo, this command, the links' })
350
356
 
351
357
  async execute(): Promise<number> {
352
358
  return run(this.context, async (h) => {
@@ -357,6 +363,15 @@ export class ReviewShowCommand extends Command {
357
363
  const all = readAllCached(h.dir)
358
364
  const x = all.items.find((y) => y.item.id === id)
359
365
  if (!x) throw new Error(`no item ${this.id}`)
366
+ if (this.paste) {
367
+ const { dirname } = await import('node:path')
368
+ const { pasteHosts, cardUrl } = await import('../review/paste.ts')
369
+ const { pasteBlock, STATUS_LABEL } = await import('../review/model.ts')
370
+ const { statusOf } = await import('../review/threads.ts')
371
+ const w = await pasteHosts(dirname(dirname(h.dir)))
372
+ h.out(pasteBlock(x.item, STATUS_LABEL[statusOf(x, all.items)], { root: w.root, open: cardUrl(w.open, x.item.id), phone: w.phone ? cardUrl(w.phone, x.item.id) : null }))
373
+ return 0
374
+ }
360
375
  const t = threadOfItem(x.item.id, threadsOf(all.items))
361
376
  h.out(t && (t.head.item.id === x.item.id || t.answers.length > 0) ? threadDetail(t, h.dir, true, h.say) : detail(x, h.dir, true, false, h.say))
362
377
  return 0
@@ -1,8 +1,10 @@
1
1
  // sk trailer: a cut's .trailer file → its mp4, filming only the takes whose film line changed (trailer/build.ts).
2
2
  // Every module loads inside execute: the cli registers this file on every `sk` call
3
3
  import { Command, Option } from 'clipanion'
4
+ import { pageAt } from './family-pages.ts'
4
5
 
5
- const DETAILS = `the trailer language: shots with a film line each (clip <file>, image <file>, card, run "<cmd {out}>", url <route> through the shot service, grid and split of other takes, or a film row of the project CLI), footage blocks, titles in the brand's look, cuts, sections in bars, music, sfx and voice; a \`vertical\` header finishes a short with the brand's lockup, watermark and end card. A repo plugs in one trailer({...}) unit: sounds by id, the world's paths, content checks, a line's own take. Outputs land in trailer/<trailer>/out/. Page: wiki build/trailer.md`
6
+ const PAGE = pageAt('build/trailer.md')
7
+ const DETAILS = `the trailer language: shots with a film line each (clip <file>, image <file>, card, run "<cmd {out}>", url <route> through the shot service, grid and split of other takes, or a film row of the project CLI), footage blocks, titles in the brand's look, cuts, sections in bars, music, sfx and voice; a \`vertical\` header finishes a short with the brand's lockup, watermark and end card. A repo plugs in one trailer({...}) unit: sounds by id, the world's paths, content checks, a line's own take. Outputs land in trailer/<trailer>/out/. Read first: ${PAGE}, then its folder build/trailer/ (the score, the craft, a book of ideas, extending the language)`
6
8
 
7
9
  /** --refilm alone: every stale take; --refilm a,b: those shots */
8
10
  const refilmOf = (v: string | boolean | undefined): 'stale' | ReadonlySet<string> | null => (v === undefined || v === false ? null : v === true ? 'stale' : new Set(v.split(',')))
@@ -16,6 +18,7 @@ async function run(context: { stdout: NodeJS.WritableStream; stderr: NodeJS.Writ
16
18
  return await body(say, findRepoRoot(cwd), cwd)
17
19
  } catch (e) {
18
20
  context.stderr.write(`FAIL: ${e instanceof Refused ? `trailer: ${e.message}` : e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`)
21
+ if (e instanceof Refused) context.stderr.write(` fix: the language is ${PAGE}\n`)
19
22
  return 1
20
23
  }
21
24
  }
@@ -0,0 +1,49 @@
1
+ // the wiki page that owns each sk command family, a path from the root of the wiki the lib lives in. The always
2
+ // loaded command page names it in the family's heading, so `sk commands --out` fails on a family missing here
3
+ import { existsSync } from 'fs'
4
+ import { homedir } from 'os'
5
+ import { join } from 'path'
6
+
7
+ export const FAMILY_PAGES: Readonly<Record<string, string>> = {
8
+ Asks: 'world/human-loop.md',
9
+ Assets: 'build/generate-assets.md',
10
+ Gate: 'build/shipkit.md',
11
+ Health: 'build/shipkit.md',
12
+ Home: 'build/shipkit/home.md',
13
+ Refactor: 'build/typescript.md',
14
+ Review: 'build/shipkit/review.md',
15
+ Scaffold: 'build/pocketbase.md',
16
+ Serve: 'build/shipkit/home.md',
17
+ Setup: 'meta/rv-conventions.md',
18
+ Sound: 'build/trailer.md',
19
+ Trailer: 'build/trailer.md',
20
+ Wiki: 'build/mdwiki.md',
21
+ }
22
+
23
+ /** a page as a path any repo can read, when the lib runs from inside its wiki (`<wiki>/build/shipkit/rvlib-shipkit`);
24
+ * from an install, the page's wiki path */
25
+ export function pageAt(rel: string): string {
26
+ const abs = join(import.meta.dir, '../../../../..', rel)
27
+ if (!existsSync(abs)) return `wiki ${rel}`
28
+ const home = homedir()
29
+ return abs.startsWith(`${home}/`) ? `~${abs.slice(home.length)}` : abs
30
+ }
31
+
32
+ type Listed = { contexts?: { commandClass: { usage?: { category?: string | null } } }[]; commands?: number[]; execute(): Promise<number | void>; context: { stdout: { write(s: string): unknown } } }
33
+
34
+ /** `sk <family> --help` lists the family's commands (clipanion's help for several matches): the family's page
35
+ * follows the list. Any other command passes through */
36
+ export function withFamilyPage<C extends object>(command: C, pages: Readonly<Record<string, string>> = FAMILY_PAGES): C {
37
+ const h = command as C & Listed
38
+ if (!Array.isArray(h.contexts) || !Array.isArray(h.commands) || h.commands.length < 2) return command
39
+ const families = new Set(h.commands.map((i) => h.contexts?.[i]?.commandClass.usage?.category ?? ''))
40
+ const page = families.size === 1 ? pages[[...families][0]!] : undefined
41
+ if (page == null) return command
42
+ const list = h.execute.bind(h)
43
+ h.execute = async () => {
44
+ const code = await list()
45
+ h.context.stdout.write(`Read first: ${pageAt(page)}\n`)
46
+ return code
47
+ }
48
+ return command
49
+ }
@@ -21,6 +21,7 @@ export function builtinCommands(bin: string): { docs: CommandClass[]; calls: Com
21
21
  ],
22
22
  })
23
23
  static override shapeRule = true
24
+ static override familyPages = null
24
25
  }
25
26
  class ProjectCalls extends CallsCommand {
26
27
  static override usage = Command.Usage({
@@ -0,0 +1,137 @@
1
+ // the command registry: one command FAMILY per file in this folder
2
+ // (cmd_<Name>.ts), logic in plain modules the commands call. The boot
3
+ // (../cli.ts) loads it with an await import, so a file that fails to load is
4
+ // caught there. A new command registers here.
5
+ import { LIB_VERSION } from '../core/lib-version.ts'
6
+ import { Builtins, Cli, type Command } from 'clipanion'
7
+ import { AskNewCommand, AsksCheckCommand, AsksCommand } from './cmd_Asks.ts'
8
+ import { AssetsAddCommand, AssetsCheckCommand, AssetsFillCommand, AssetsGenCommand, AssetsIdentityCommand } from './cmd_Assets.ts'
9
+ import { BumpCommand } from './cmd_Bump.ts'
10
+ import { CallsCommand } from './cmd_Calls.ts'
11
+ import { TimeCommand } from './cmd_Time.ts'
12
+ import { CheckCommand } from './cmd_Check.ts'
13
+ import { CommandsCommand } from './cmd_Commands.ts'
14
+ import { withFamilyPage } from './family-pages.ts'
15
+ import { aliasArgs, CrossRepoActivityCommand } from './cmd_CrossRepoActivity.ts'
16
+ import { ConfigKeysCommand } from './cmd_ConfigKeys.ts'
17
+ import { GlobalProcessesCommand, GlobalProcessOpenCommand, GlobalProcessRestartCommand, GlobalProcessStartCommand, GlobalProcessStopCommand } from './cmd_GlobalProcesses.ts'
18
+ import { PlannedCommand } from './cmd_Planned.ts'
19
+ import { InsightsCommand } from './cmd_Insights.ts'
20
+ import { GlobalContextCommand } from './cmd_GlobalContext.ts'
21
+ import { OverviewCommand } from './cmd_Overview.ts'
22
+ import { CiCommand } from './cmd_Ci.ts'
23
+ import { ContractsCommand } from './cmd_Contracts.ts'
24
+ import { FleetCommand } from './cmd_Fleet.ts'
25
+ import { FleetPushCommand } from './cmd_FleetPush.ts'
26
+ import { DaemonCommand, GuiCommand, PmCommand, PmInstallCommand, PmUninstallCommand, WebCommand } from './cmd_Home.ts'
27
+ import { HooksCommitMsgCommand, HooksInstallCommand, HooksListCommand, HooksPreCommitCommand } from './cmd_Hooks.ts'
28
+ import { InitCommand } from './cmd_Init.ts'
29
+ import { KeysCommand } from './cmd_Keys.ts'
30
+ import { MvCommand } from './cmd_Mv.ts'
31
+ import { NeedsCommand } from './cmd_Needs.ts'
32
+ import { PbCollectionCommand } from './cmd_Pb.ts'
33
+ import { ReflectCommand } from './cmd_Reflect.ts'
34
+ import { ReleaseCommand } from './cmd_Release.ts'
35
+ import { ReleasesCommand } from './cmd_Releases.ts'
36
+ import { ChecksCommand, UnitsAliasCommand } from './cmd_Checks.ts'
37
+ import { WhenCommand } from './cmd_When.ts'
38
+ import { CacheAuditCommand } from './cmd_CacheAudit.ts'
39
+ import { TraceAuditCommand } from './cmd_TraceAudit.ts'
40
+ import { ServeCommand } from './cmd_Serve.ts'
41
+ import { SmokeCommand } from './cmd_Smoke.ts'
42
+ import { ShotCommand } from './cmd_Shot.ts'
43
+ import { StatusCommand } from './cmd_Status.ts'
44
+ import { StartCommand } from './cmd_Start.ts'
45
+ import { TRAILER_COMMANDS } from './cmd_Trailer.ts'
46
+ import { SOUND_COMMANDS } from './cmd_Sound.ts'
47
+ import { REVIEW_COMMANDS } from './cmd_Review.ts'
48
+ import { TuiCommand } from './cmd_Tui.ts'
49
+ import { WikiLintCommand, WikiServeCommand } from './cmd_Wiki.ts'
50
+
51
+
52
+ /** `sk <family> --help` names the family's page under its list */
53
+ class FamilyHelpCli extends Cli {
54
+ override process(...args: unknown[]): Command {
55
+ return withFamilyPage((super.process as (...a: unknown[]) => Command).apply(this, args))
56
+ }
57
+ }
58
+
59
+ /** the registry, built but NOT run: a test drives the real binary in process
60
+ * instead of paying a cold bun start per assertion (~250ms each), and the
61
+ * wiring it exercises is the wiring that ships. */
62
+ export function buildCli(): Cli {
63
+ const cli = new FamilyHelpCli({
64
+ binaryLabel: 'shipkit — repo toolkit',
65
+ binaryName: 'sk',
66
+ binaryVersion: LIB_VERSION,
67
+ enableCapture: true, // console output pipes into the command streams (testable)
68
+ enableColors: true,
69
+ })
70
+
71
+ cli.register(Builtins.HelpCommand)
72
+ cli.register(Builtins.VersionCommand)
73
+ cli.register(CiCommand)
74
+ cli.register(StatusCommand)
75
+ cli.register(BumpCommand)
76
+ cli.register(ReleaseCommand)
77
+ cli.register(ReleasesCommand)
78
+ cli.register(HooksInstallCommand)
79
+ cli.register(HooksPreCommitCommand)
80
+ cli.register(HooksCommitMsgCommand)
81
+ cli.register(HooksListCommand)
82
+ cli.register(MvCommand)
83
+ cli.register(CheckCommand)
84
+ cli.register(TuiCommand)
85
+ for (const c of TRAILER_COMMANDS) cli.register(c)
86
+ for (const c of SOUND_COMMANDS) cli.register(c)
87
+ for (const c of REVIEW_COMMANDS) cli.register(c)
88
+ cli.register(ServeCommand)
89
+ cli.register(PmCommand)
90
+ cli.register(DaemonCommand)
91
+ cli.register(WebCommand)
92
+ cli.register(PmInstallCommand)
93
+ cli.register(PmUninstallCommand)
94
+ cli.register(GuiCommand)
95
+ cli.register(ContractsCommand)
96
+ cli.register(AsksCommand)
97
+ cli.register(AsksCheckCommand)
98
+ cli.register(AskNewCommand)
99
+ cli.register(WikiServeCommand)
100
+ cli.register(WikiLintCommand)
101
+ cli.register(AssetsFillCommand)
102
+ cli.register(AssetsAddCommand)
103
+ cli.register(AssetsGenCommand)
104
+ cli.register(AssetsIdentityCommand)
105
+ cli.register(AssetsCheckCommand)
106
+ cli.register(InitCommand)
107
+ cli.register(StartCommand)
108
+ cli.register(NeedsCommand)
109
+ cli.register(FleetCommand)
110
+ cli.register(FleetPushCommand)
111
+ cli.register(PbCollectionCommand)
112
+ cli.register(SmokeCommand)
113
+ cli.register(ShotCommand)
114
+ cli.register(ReflectCommand)
115
+ cli.register(KeysCommand)
116
+ cli.register(GlobalProcessesCommand)
117
+ cli.register(GlobalProcessStartCommand)
118
+ cli.register(GlobalProcessStopCommand)
119
+ cli.register(GlobalProcessRestartCommand)
120
+ cli.register(GlobalProcessOpenCommand)
121
+ cli.register(CrossRepoActivityCommand)
122
+ cli.register(PlannedCommand)
123
+ cli.register(InsightsCommand)
124
+ cli.register(GlobalContextCommand)
125
+ cli.register(OverviewCommand)
126
+ cli.register(ChecksCommand)
127
+ cli.register(WhenCommand)
128
+ cli.register(CacheAuditCommand)
129
+ cli.register(TraceAuditCommand)
130
+ cli.register(UnitsAliasCommand)
131
+ cli.register(CommandsCommand)
132
+ cli.register(CallsCommand)
133
+ cli.register(TimeCommand)
134
+ cli.register(ConfigKeysCommand)
135
+
136
+ return cli
137
+ }
package/src/cli.ts CHANGED
@@ -1,146 +1,58 @@
1
1
  #!/usr/bin/env bun
2
- // shipkit — clipanion entry: one command FAMILY per file in cli/ (cmd_<Name>.ts),
3
- // logic in plain modules the commands call. runExit sets the exit code
4
- // without force-exiting, so serve commands keep the process alive.
5
- import { LIB_VERSION } from './core/lib-version.ts'
6
- import { Builtins, Cli } from 'clipanion'
7
- import { AskNewCommand, AsksCheckCommand, AsksCommand } from './cli/cmd_Asks.ts'
8
- import { AssetsAddCommand, AssetsCheckCommand, AssetsFillCommand, AssetsGenCommand, AssetsIdentityCommand } from './cli/cmd_Assets.ts'
9
- import { BumpCommand } from './cli/cmd_Bump.ts'
10
- import { CallsCommand } from './cli/cmd_Calls.ts'
11
- import { TimeCommand } from './cli/cmd_Time.ts'
12
- import { CheckCommand } from './cli/cmd_Check.ts'
13
- import { CommandsCommand } from './cli/cmd_Commands.ts'
14
- import { aliasArgs, CrossRepoActivityCommand } from './cli/cmd_CrossRepoActivity.ts'
15
- import { ConfigKeysCommand } from './cli/cmd_ConfigKeys.ts'
16
- import { GlobalProcessesCommand, GlobalProcessOpenCommand, GlobalProcessRestartCommand, GlobalProcessStartCommand, GlobalProcessStopCommand } from './cli/cmd_GlobalProcesses.ts'
17
- import { PlannedCommand } from './cli/cmd_Planned.ts'
18
- import { InsightsCommand } from './cli/cmd_Insights.ts'
19
- import { GlobalContextCommand } from './cli/cmd_GlobalContext.ts'
20
- import { OverviewCommand } from './cli/cmd_Overview.ts'
21
- import { CiCommand } from './cli/cmd_Ci.ts'
22
- import { ContractsCommand } from './cli/cmd_Contracts.ts'
23
- import { FleetCommand } from './cli/cmd_Fleet.ts'
24
- import { DaemonCommand, GuiCommand, PmCommand, PmInstallCommand, PmUninstallCommand, WebCommand } from './cli/cmd_Home.ts'
25
- import { HooksCommitMsgCommand, HooksInstallCommand, HooksListCommand, HooksPreCommitCommand } from './cli/cmd_Hooks.ts'
26
- import { InitCommand } from './cli/cmd_Init.ts'
27
- import { KeysCommand } from './cli/cmd_Keys.ts'
28
- import { MvCommand } from './cli/cmd_Mv.ts'
29
- import { NeedsCommand } from './cli/cmd_Needs.ts'
30
- import { PbCollectionCommand } from './cli/cmd_Pb.ts'
31
- import { ReflectCommand } from './cli/cmd_Reflect.ts'
32
- import { ReleaseCommand } from './cli/cmd_Release.ts'
33
- import { ReleasesCommand } from './cli/cmd_Releases.ts'
34
- import { ChecksCommand, UnitsAliasCommand } from './cli/cmd_Checks.ts'
35
- import { WhenCommand } from './cli/cmd_When.ts'
36
- import { CacheAuditCommand } from './cli/cmd_CacheAudit.ts'
37
- import { TraceAuditCommand } from './cli/cmd_TraceAudit.ts'
38
- import { ServeCommand } from './cli/cmd_Serve.ts'
39
- import { SmokeCommand } from './cli/cmd_Smoke.ts'
40
- import { ShotCommand } from './cli/cmd_Shot.ts'
41
- import { StatusCommand } from './cli/cmd_Status.ts'
42
- import { StartCommand } from './cli/cmd_Start.ts'
43
- import { TRAILER_COMMANDS } from './cli/cmd_Trailer.ts'
44
- import { SOUND_COMMANDS } from './cli/cmd_Sound.ts'
45
- import { REVIEW_COMMANDS } from './cli/cmd_Review.ts'
46
- import { TuiCommand } from './cli/cmd_Tui.ts'
47
- import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
2
+ // the way in: the launcher, a repo's install handing off, the commit hooks
3
+ // and pm's roles all run this file. It loads the registry (cli/registry.ts)
4
+ // and the boot steps with await import, so a file of the tree that fails to
5
+ // load is caught here and the last good copy runs instead, with one line
6
+ // (core/last-good.ts). An error once the command runs is the command's own.
7
+ //
8
+ // the one constraint a reader breaks first: no static import but
9
+ // core/last-good.ts, else a broken file stops this file before it can fall
10
+ // back (tests/cli-boot.test.ts).
11
+ import { bootLoaded, runLastGood } from './core/last-good.ts'
48
12
 
13
+ /** a long running role's runner, which its command loads by await import:
14
+ * loaded here, a role pm starts on a broken tree runs the copy too */
15
+ const loadRole = (cmd: string | undefined): Promise<unknown> | null =>
16
+ cmd === 'daemon' ? import('./home/run-daemon.ts')
17
+ : cmd === 'web' ? import('./home/run-web.ts')
18
+ : cmd === 'pm' ? import('./home/run-pm.ts')
19
+ : cmd === 'serve' ? import('./serve/run-serve.ts')
20
+ : null
49
21
 
50
- /** the registry, built but NOT run: a test drives the real binary in process
51
- * instead of paying a cold bun start per assertion (~250ms each), and the
52
- * wiring it exercises is the wiring that ships. */
53
- export function buildCli(): Cli {
54
- const cli = new Cli({
55
- binaryLabel: 'shipkit — repo toolkit',
56
- binaryName: 'sk',
57
- binaryVersion: LIB_VERSION,
58
- enableCapture: true, // console output pipes into the command streams (testable)
59
- enableColors: true,
60
- })
22
+ /** every module the boot runs, loaded, with the runner of the role `cmd`
23
+ * names. The load probe asks it too (core/load-probe.ts), so a restart is
24
+ * tried on exactly this */
25
+ export const loadBoot = (cmd?: string) =>
26
+ Promise.all([
27
+ import('./cli/registry.ts'),
28
+ import('./cli/cmd_CrossRepoActivity.ts'),
29
+ import('./home/handoff.ts'),
30
+ import('./core/one-lib.ts'),
31
+ import('./cli/kit/calls.ts'),
32
+ import('./home/autostart.ts'),
33
+ loadRole(cmd),
34
+ ])
61
35
 
62
- cli.register(Builtins.HelpCommand)
63
- cli.register(Builtins.VersionCommand)
64
- cli.register(CiCommand)
65
- cli.register(StatusCommand)
66
- cli.register(BumpCommand)
67
- cli.register(ReleaseCommand)
68
- cli.register(ReleasesCommand)
69
- cli.register(HooksInstallCommand)
70
- cli.register(HooksPreCommitCommand)
71
- cli.register(HooksCommitMsgCommand)
72
- cli.register(HooksListCommand)
73
- cli.register(MvCommand)
74
- cli.register(CheckCommand)
75
- cli.register(TuiCommand)
76
- for (const c of TRAILER_COMMANDS) cli.register(c)
77
- for (const c of SOUND_COMMANDS) cli.register(c)
78
- for (const c of REVIEW_COMMANDS) cli.register(c)
79
- cli.register(ServeCommand)
80
- cli.register(PmCommand)
81
- cli.register(DaemonCommand)
82
- cli.register(WebCommand)
83
- cli.register(PmInstallCommand)
84
- cli.register(PmUninstallCommand)
85
- cli.register(GuiCommand)
86
- cli.register(ContractsCommand)
87
- cli.register(AsksCommand)
88
- cli.register(AsksCheckCommand)
89
- cli.register(AskNewCommand)
90
- cli.register(WikiServeCommand)
91
- cli.register(WikiLintCommand)
92
- cli.register(AssetsFillCommand)
93
- cli.register(AssetsAddCommand)
94
- cli.register(AssetsGenCommand)
95
- cli.register(AssetsIdentityCommand)
96
- cli.register(AssetsCheckCommand)
97
- cli.register(InitCommand)
98
- cli.register(StartCommand)
99
- cli.register(NeedsCommand)
100
- cli.register(FleetCommand)
101
- cli.register(PbCollectionCommand)
102
- cli.register(SmokeCommand)
103
- cli.register(ShotCommand)
104
- cli.register(ReflectCommand)
105
- cli.register(KeysCommand)
106
- cli.register(GlobalProcessesCommand)
107
- cli.register(GlobalProcessStartCommand)
108
- cli.register(GlobalProcessStopCommand)
109
- cli.register(GlobalProcessRestartCommand)
110
- cli.register(GlobalProcessOpenCommand)
111
- cli.register(CrossRepoActivityCommand)
112
- cli.register(PlannedCommand)
113
- cli.register(InsightsCommand)
114
- cli.register(GlobalContextCommand)
115
- cli.register(OverviewCommand)
116
- cli.register(ChecksCommand)
117
- cli.register(WhenCommand)
118
- cli.register(CacheAuditCommand)
119
- cli.register(TraceAuditCommand)
120
- cli.register(UnitsAliasCommand)
121
- cli.register(CommandsCommand)
122
- cli.register(CallsCommand)
123
- cli.register(TimeCommand)
124
- cli.register(ConfigKeysCommand)
125
-
126
- return cli
127
- }
128
-
129
- // only when this file IS the process, so importing it costs nothing. A repo's
130
- // installed copy first hands the command to the house source (handoff.ts). One copy
131
- // of the lib per process: a repo config that imports its own install lands on
132
- // this one (one-lib.ts), so the machine's version runs in every repo. The run
133
- // is logged (cli/kit/calls.ts, read back by `sk calls`). Then pm is started
134
- // when it is down (autostart.ts), before the command runs
36
+ // only when this file IS the process. A repo's installed copy first hands the
37
+ // command to the house source (handoff.ts). One copy of the lib per process:
38
+ // a repo config that imports its own install lands on this one (one-lib.ts).
39
+ // The run is logged (cli/kit/calls.ts, read back by `sk calls`). Then pm is
40
+ // started when it is down (autostart.ts), before the command runs
135
41
  if (import.meta.main) {
136
- const argv = aliasArgs(process.argv.slice(2))
137
- const { handoff } = await import('./home/handoff.ts')
42
+ const raw = process.argv.slice(2)
43
+ let m
44
+ try {
45
+ m = await loadBoot(raw[0])
46
+ } catch (e) {
47
+ runLastGood(e, raw)
48
+ throw e
49
+ }
50
+ bootLoaded()
51
+ const [{ buildCli }, { aliasArgs }, { handoff }, { installOneLib }, { callsFile, logCall }, { autostart }] = m
52
+ const argv = aliasArgs(raw)
138
53
  handoff(argv)
139
- const { installOneLib } = await import('./core/one-lib.ts')
140
54
  installOneLib()
141
- const { callsFile, logCall } = await import('./cli/kit/calls.ts')
142
55
  logCall(callsFile((await import('os')).homedir(), 'sk'), 'sk', argv)
143
- const { autostart } = await import('./home/autostart.ts')
144
56
  await autostart(argv)
145
57
  void buildCli().runExit(argv)
146
58
  }