rvlib-shipkit 2.3.0 → 2.5.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 (104) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/package.json +2 -2
  3. package/src/assets/kits/identity-kit.ts +5 -2
  4. package/src/cli/cmd_Commands.ts +30 -5
  5. package/src/cli/cmd_FleetPush.ts +47 -0
  6. package/src/cli/cmd_GlobalProcesses.ts +5 -5
  7. package/src/cli/cmd_Review.ts +52 -11
  8. package/src/cli/cmd_Sound.ts +209 -0
  9. package/src/cli/cmd_Trailer.ts +11 -3
  10. package/src/cli/family-pages.ts +49 -0
  11. package/src/cli/kit/builtins.ts +1 -0
  12. package/src/cli/kit/verb.ts +11 -3
  13. package/src/cli/registry.ts +137 -0
  14. package/src/cli.ts +47 -133
  15. package/src/core/app-icon.ts +10 -6
  16. package/src/core/last-good.ts +217 -0
  17. package/src/core/load-probe.ts +71 -0
  18. package/src/core/reexec.ts +5 -3
  19. package/src/fleet/push-fleet.ts +80 -0
  20. package/src/home/home-routes.ts +12 -2
  21. package/src/home/lib-watch.ts +110 -20
  22. package/src/home/process-rows.ts +4 -2
  23. package/src/home/run-daemon.ts +1 -1
  24. package/src/repo-config.ts +3 -0
  25. package/src/review/format.ts +11 -3
  26. package/src/review/model.ts +66 -5
  27. package/src/review/paste.ts +30 -0
  28. package/src/review/project.ts +20 -4
  29. package/src/review/routes.ts +22 -4
  30. package/src/review/store.ts +15 -6
  31. package/src/review/unit.ts +19 -1
  32. package/src/scaffold/files/agent-index.md +2 -2
  33. package/src/scaffold/project-scaffold.ts +2 -2
  34. package/src/serve/hub-app/activity-st.ts +31 -0
  35. package/src/serve/hub-app/activity-view.tsx +9 -0
  36. package/src/serve/hub-app/activity.css +6 -0
  37. package/src/serve/hub-app/admin-svc.ts +1 -1
  38. package/src/serve/hub-app/admin-view.tsx +1 -1
  39. package/src/serve/hub-app/hash-route.ts +12 -4
  40. package/src/serve/hub-app/home-st.ts +7 -1
  41. package/src/serve/hub-app/home-strip.tsx +3 -1
  42. package/src/serve/hub-app/hub.css +38 -1
  43. package/src/serve/hub-app/icons.tsx +6 -0
  44. package/src/serve/hub-app/layout-st.ts +2 -2
  45. package/src/serve/hub-app/machine-st.ts +1 -1
  46. package/src/serve/hub-app/md.ts +32 -0
  47. package/src/serve/hub-app/project-list.css +9 -7
  48. package/src/serve/hub-app/project-list.tsx +5 -2
  49. package/src/serve/hub-app/projects-svc.ts +3 -2
  50. package/src/serve/hub-app/repo-st.ts +22 -7
  51. package/src/serve/hub-app/review/clip-st.ts +57 -7
  52. package/src/serve/hub-app/review/kind-view.tsx +78 -9
  53. package/src/serve/hub-app/review/review-answer.tsx +67 -58
  54. package/src/serve/hub-app/review/review-page.tsx +17 -15
  55. package/src/serve/hub-app/review/review-st.ts +191 -20
  56. package/src/serve/hub-app/review/review-views.tsx +154 -9
  57. package/src/serve/hub-app/review.css +73 -13
  58. package/src/serve/hub-app/wiki-st.ts +190 -0
  59. package/src/serve/hub-app/wikis-view.tsx +134 -5
  60. package/src/serve/hub-runtime.ts +4 -4
  61. package/src/serve/project-gutter-route.ts +25 -1
  62. package/src/serve/project-gutter.ts +1 -1
  63. package/src/serve/run-serve.ts +25 -0
  64. package/src/serve/serve-hub.ts +15 -1
  65. package/src/serve/stale-restart.ts +19 -2
  66. package/src/shot/client.ts +17 -5
  67. package/src/shot/shooter.ts +19 -3
  68. package/src/shot/shot-ask.ts +9 -1
  69. package/src/shot/stepped-clip.ts +53 -0
  70. package/src/trailer/adapter.ts +33 -4
  71. package/src/trailer/build.ts +182 -33
  72. package/src/trailer/ffmpeg.ts +21 -1
  73. package/src/trailer/fonts.ts +55 -0
  74. package/src/trailer/footage.ts +4 -4
  75. package/src/trailer/grammar.ts +429 -67
  76. package/src/trailer/graph.ts +130 -56
  77. package/src/trailer/look.ts +3 -0
  78. package/src/trailer/motion.ts +207 -0
  79. package/src/trailer/plan.ts +2 -2
  80. package/src/trailer/prune.ts +13 -2
  81. package/src/trailer/score/compose.ts +18 -2
  82. package/src/trailer/score/gm-render.ts +43 -0
  83. package/src/trailer/score/gm.ts +90 -0
  84. package/src/trailer/score/midi.ts +143 -0
  85. package/src/trailer/score/pattern.ts +13 -6
  86. package/src/trailer/score/score.ts +28 -14
  87. package/src/trailer/score/sound.ts +12 -1
  88. package/src/trailer/score/synth.ts +24 -2
  89. package/src/trailer/sound/audio.ts +148 -0
  90. package/src/trailer/sound/brand.ts +42 -0
  91. package/src/trailer/sound/comfy.ts +38 -0
  92. package/src/trailer/sound/eleven.ts +72 -0
  93. package/src/trailer/sound/fluid.ts +71 -0
  94. package/src/trailer/sound/gen.ts +139 -0
  95. package/src/trailer/sound/index.ts +60 -0
  96. package/src/trailer/sound/presets.ts +69 -0
  97. package/src/trailer/sound/run.ts +189 -0
  98. package/src/trailer/titles.ts +451 -235
  99. package/src/trailer/url.ts +18 -3
  100. package/src/trailer/vocab.ts +12 -1
  101. package/src/trailer/words.ts +29 -8
  102. package/src/wiki/graph-mount.ts +3 -0
  103. package/src/wiki/graph-page.ts +45 -9
  104. package/src/wiki/graph-pages.ts +128 -0
@@ -0,0 +1,209 @@
1
+ // sk sound: the house sound generators, one verb each, for a trailer as much as a game. Each writes --out and says its
2
+ // measures; a trailer's `voice`, `sfx` and `music` words run them through its sound cache (trailer/sound/). Every
3
+ // module loads inside execute: the cli registers this file on every `sk` call
4
+ import { Command, Option } from 'clipanion'
5
+
6
+ const PAGE = 'Page: wiki build/trailer.md, Sound'
7
+
8
+ async function run(context: { stdout: NodeJS.WritableStream; stderr: NodeJS.WritableStream }, body: (say: (l: string) => void, root: string, cwd: string) => Promise<void>): Promise<number> {
9
+ const say = (l: string): void => void context.stdout.write(`${l}\n`)
10
+ const { findRepoRoot } = await import('../ts/config.ts')
11
+ const cwd = process.cwd()
12
+ try {
13
+ await body(say, findRepoRoot(cwd), cwd)
14
+ return 0
15
+ } catch (e) {
16
+ context.stderr.write(`FAIL sound: ${e instanceof Error ? e.message : String(e)}\n`)
17
+ return 1
18
+ }
19
+ }
20
+
21
+ /** the brand's sound section, when the repo has a brand */
22
+ async function brandSound(root: string): Promise<import('../trailer/sound/brand.ts').TrailerSoundInput | undefined> {
23
+ const { loadBrand } = await import('../assets/run-assets.ts')
24
+ const b = await loadBrand(root)
25
+ if (typeof b === 'string') throw new Error(b)
26
+ return b?.trailer
27
+ }
28
+
29
+ const COMFY = 'the comfy-ts module folder (default: the brand\'s trailer.comfy)'
30
+
31
+ /** --out, which every making verb needs */
32
+ function outOf(out: string | undefined): string {
33
+ if (out === undefined) throw new Error('--out <file>: the file it writes')
34
+ return out
35
+ }
36
+
37
+ const num = (name: string, v: string | undefined, fallback: number): number => {
38
+ if (v === undefined) return fallback
39
+ const n = Number(v)
40
+ if (!Number.isFinite(n)) throw new Error(`${name} takes a number, got ${v}`)
41
+ return n
42
+ }
43
+
44
+ export class SoundVoiceCommand extends Command {
45
+ static override paths = [['sound', 'voice']]
46
+ static override usage = Command.Usage({
47
+ category: 'Sound',
48
+ description: 'one spoken line in a voice: ElevenLabs, or comfy-ts cloning a clip or a designed voice',
49
+ details: `no voice flag: the brand's trailer.voice (--by <name>: one of its trailer.voices), else the house narrator. ElevenLabs is paid: it says the characters spent and left. The line is trimmed at both ends and set at -16 LUFS. ${PAGE}`,
50
+ examples: [
51
+ ['the brand\'s voice', 'sk sound voice "The storm is coming." --out lines/storm.wav'],
52
+ ['an ElevenLabs voice, its second take', 'sk sound voice "[whispers] Hey." --eleven pNInz6obpgDQGcFmaJgB --seed 2 --out hey.wav'],
53
+ ['a clip cloned, free', 'sk sound voice "Ahoy." --clone voices/sailor.flac --out ahoy.wav'],
54
+ ['a voice designed from words, free', 'sk sound voice "Ahoy." --design "an old sailor, gravelly and warm" --out ahoy.wav'],
55
+ ],
56
+ })
57
+ text = Option.String({ required: true, name: 'line' })
58
+ by = Option.String('--by', { description: 'one of the brand\'s trailer.voices' })
59
+ eleven = Option.String('--eleven', { description: 'an ElevenLabs voice id (paid)' })
60
+ model = Option.String('--model', { description: 'the ElevenLabs model (default eleven_v3)' })
61
+ stability = Option.String('--stability')
62
+ similarity = Option.String('--similarity')
63
+ speed = Option.String('--speed')
64
+ clone = Option.String('--clone', { description: 'a clean 5 to 10 s clip to clone, repo relative' })
65
+ transcript = Option.String('--transcript', { description: 'what the clip says (default: <clip>.txt)' })
66
+ design = Option.String('--design', { description: 'a voice designed from its description' })
67
+ designLine = Option.String('--design-line', { description: 'what the designed voice\'s reference reads' })
68
+ designSeed = Option.String('--design-seed')
69
+ engine = Option.String('--engine', { description: 'comfy-ts engine: chatterbox, qwen3, fishs2, cosyvoice3, omnivoice' })
70
+ language = Option.String('--language', { description: 'en, fr… (ElevenLabs) or English, French… (comfy-ts)' })
71
+ comfy = Option.String('--comfy', { description: COMFY })
72
+ seed = Option.String('--seed', { description: 'the take (default 1)' })
73
+ out = Option.String('--out', { description: 'the file it writes (required)' })
74
+
75
+ execute(): Promise<number> {
76
+ return run(this.context, async (say, root, cwd) => {
77
+ const [{ resolve }, { genVoice }, { voiceOf }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts'), import('../trailer/sound/brand.ts')])
78
+ const brand = await brandSound(root)
79
+ const given = [this.eleven, this.clone, this.design].filter((x) => x !== undefined).length
80
+ if (given > 1) throw new Error('one voice: --eleven, --clone or --design')
81
+ const n = (k: string, v: string | undefined): number | undefined => (v === undefined ? undefined : num(k, v, 0))
82
+ const lang = this.language !== undefined ? { language: this.language } : {}
83
+ const eng = this.engine !== undefined ? { engine: this.engine } : {}
84
+ let voice
85
+ if (this.eleven !== undefined) voice = { eleven: this.eleven, ...(this.model ? { model: this.model } : {}), ...strip({ stability: n('--stability', this.stability), similarity: n('--similarity', this.similarity), speed: n('--speed', this.speed) }), ...lang }
86
+ else if (this.clone !== undefined) voice = { clone: this.clone, ...(this.transcript !== undefined ? { transcript: this.transcript } : {}), ...eng, ...lang }
87
+ else if (this.design !== undefined) voice = { design: this.design, ...(this.designLine !== undefined ? { line: this.designLine } : {}), ...strip({ seed: n('--design-seed', this.designSeed) }), ...eng, ...lang }
88
+ else voice = voiceOf(brand, this.by)
89
+ await genVoice({ root, text: this.text, voice, seed: num('--seed', this.seed, 1), comfy: this.comfy ?? brand?.comfy, elevenKey: brand?.elevenKey, out: resolve(cwd, outOf(this.out)), say })
90
+ })
91
+ }
92
+ }
93
+
94
+ const strip = <T extends Record<string, unknown>>(o: T): Partial<T> => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as Partial<T>
95
+
96
+ export class SoundSfxCommand extends Command {
97
+ static override paths = [['sound', 'sfx']]
98
+ static override usage = Command.Usage({
99
+ category: 'Sound',
100
+ description: 'a sound effect from a prompt: comfy-ts stable-audio, trimmed, its peak at -3 dBFS',
101
+ details: `its silence before is cut, its tail cut where it falls 45 dB under its peak with a 30 ms fade; --hit keeps its first hit alone. The GPU box must be up: sk gp start comfyui. ${PAGE}`,
102
+ examples: [
103
+ ['a clash cut to its first hit', 'sk sound sfx "a sword clash, metal on metal" --hit --out sfx/clash.wav'],
104
+ ['four seconds of rain, another take', 'sk sound sfx "rain on a tin roof" --seconds 4 --seed 2 --out sfx/rain.wav'],
105
+ ],
106
+ })
107
+ prompt = Option.String({ required: true, name: 'prompt' })
108
+ seconds = Option.String('--seconds', { description: 'seconds made (default 2)' })
109
+ seed = Option.String('--seed', { description: 'the take (default 1)' })
110
+ hit = Option.Boolean('--hit', false, { description: 'its first hit alone' })
111
+ comfy = Option.String('--comfy', { description: COMFY })
112
+ out = Option.String('--out', { description: 'the file it writes (required)' })
113
+
114
+ execute(): Promise<number> {
115
+ return run(this.context, async (say, root, cwd) => {
116
+ const [{ resolve }, { genSfx }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts')])
117
+ const comfy = this.comfy ?? (await brandSound(root))?.comfy
118
+ await genSfx({ prompt: this.prompt, seconds: num('--seconds', this.seconds, 2), seed: num('--seed', this.seed, 1), hit: this.hit, comfy, out: resolve(cwd, outOf(this.out)), say })
119
+ })
120
+ }
121
+ }
122
+
123
+ export class SoundMusicCommand extends Command {
124
+ static override paths = [['sound', 'music']]
125
+ static override usage = Command.Usage({
126
+ category: 'Sound',
127
+ description: 'a music bed or a song from a prompt: comfy-ts stable-audio or ace-step, -16 LUFS',
128
+ details: `stable-audio plays an instrumental bed (up to 300 s); ace-step reads the prompt's first line as its style caption and sings the lines after it ([Verse], [Chorus]), else plays instrumental. The GPU box must be up: sk gp start comfyui. ${PAGE}`,
129
+ examples: [
130
+ ['a 30 s bed', 'sk sound music "warm lofi piano bed, slow, soft" --seconds 30 --out music/bed.wav'],
131
+ ['a sung theme, its third take', 'sk sound music "an epic orchestral theme, brass and choir" --engine ace-step --seed 3 --out music/theme.wav'],
132
+ ],
133
+ })
134
+ prompt = Option.String({ required: true, name: 'prompt' })
135
+ engine = Option.String('--engine', { description: 'stable-audio (default) or ace-step' })
136
+ seconds = Option.String('--seconds', { description: 'seconds made (default 30)' })
137
+ seed = Option.String('--seed', { description: 'the take (default 1)' })
138
+ comfy = Option.String('--comfy', { description: COMFY })
139
+ out = Option.String('--out', { description: 'the file it writes (required)' })
140
+
141
+ execute(): Promise<number> {
142
+ return run(this.context, async (say, root, cwd) => {
143
+ const [{ resolve }, { genMusic }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts')])
144
+ const engine = this.engine ?? 'stable-audio'
145
+ if (engine !== 'stable-audio' && engine !== 'ace-step') throw new Error(`--engine ${engine}: stable-audio or ace-step`)
146
+ const comfy = this.comfy ?? (await brandSound(root))?.comfy
147
+ await genMusic({ prompt: this.prompt.replaceAll('\\n', '\n'), engine, seconds: num('--seconds', this.seconds, 30), seed: num('--seed', this.seed, 1), comfy, out: resolve(cwd, outOf(this.out)), say })
148
+ })
149
+ }
150
+ }
151
+
152
+ export class SoundMidiCommand extends Command {
153
+ static override paths = [['sound', 'midi']]
154
+ static override usage = Command.Usage({
155
+ category: 'Sound',
156
+ description: 'a MIDI file through the soundfont: FluidSynth, its own room on, -16 LUFS',
157
+ details: `the machine's fluidsynth and ~/.local/share/soundfonts/GeneralUser-GS.sf2 (SK_SOUNDFONT names another). ${PAGE}`,
158
+ examples: [['a .mid as a wav', 'sk sound midi theme.mid --out music/theme.wav']],
159
+ })
160
+ file = Option.String({ required: true, name: 'file.mid' })
161
+ out = Option.String('--out', { description: 'the file it writes (required)' })
162
+
163
+ execute(): Promise<number> {
164
+ return run(this.context, async (say, _root, cwd) => {
165
+ const [{ resolve }, { genMidi }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts')])
166
+ await genMidi({ file: resolve(cwd, this.file), out: resolve(cwd, outOf(this.out)), say })
167
+ })
168
+ }
169
+ }
170
+
171
+ export class SoundMeasureCommand extends Command {
172
+ static override paths = [['sound', 'measure']]
173
+ static override usage = Command.Usage({
174
+ category: 'Sound',
175
+ description: 'a sound as numbers and a picture: length, peak, loudness, onsets, the waveform png',
176
+ details: `nobody hears a build: read these before you report a sound. The png lands at <file>.png. ${PAGE}`,
177
+ examples: [['a made sfx', 'sk sound measure sfx/clash.wav']],
178
+ })
179
+ file = Option.String({ required: true, name: 'file' })
180
+
181
+ execute(): Promise<number> {
182
+ return run(this.context, async (say, _root, cwd) => {
183
+ const [{ resolve }, { measure, measureLine, waveformPng }] = await Promise.all([import('node:path'), import('../trailer/sound/audio.ts')])
184
+ const f = resolve(cwd, this.file)
185
+ say(`${this.file}: ${measureLine(await measure(f))}`)
186
+ await waveformPng(f, `${f}.png`)
187
+ say(`ok: ${f}.png`)
188
+ })
189
+ }
190
+ }
191
+
192
+ export class SoundGmCommand extends Command {
193
+ static override paths = [['sound', 'gm']]
194
+ static override usage = Command.Usage({
195
+ category: 'Sound',
196
+ description: 'the General MIDI programs, drums and kits a score\'s gm: instruments take',
197
+ details: `a score track plays one through the soundfont: track piano gm:acoustic-grand, drums k=gm:kick, or gm('strings', { vol, pan, reverb }) in its code. ${PAGE}`,
198
+ examples: [['every name', 'sk sound gm']],
199
+ })
200
+
201
+ execute(): Promise<number> {
202
+ return run(this.context, async (say) => {
203
+ const { gmTable } = await import('../trailer/score/gm.ts')
204
+ for (const l of gmTable()) say(l)
205
+ })
206
+ }
207
+ }
208
+
209
+ export const SOUND_COMMANDS = [SoundVoiceCommand, SoundSfxCommand, SoundMusicCommand, SoundMidiCommand, SoundMeasureCommand, SoundGmCommand]
@@ -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
  }
@@ -32,17 +35,19 @@ export class TrailerCommand extends Command {
32
35
  ['one cut by its file', 'sk trailer trailer/launch/main.trailer'],
33
36
  ['film again the takes of two shots', 'sk trailer --refilm intro,menu'],
34
37
  ['encoded by x264 for a release', 'sk trailer --final'],
38
+ ['the music variant b on the same picture', 'sk trailer --variant b'],
35
39
  ],
36
40
  })
37
41
 
38
42
  file = Option.String({ required: false })
39
43
  refilm = Option.String('--refilm', { tolerateBoolean: true, description: 'film again: every stale take, or the named shots (a,b)' })
40
44
  final = Option.Boolean('--final', false, { description: 'encode with x264 for a release (default: the fast draft encoder)' })
45
+ variant = Option.String('--variant', { description: 'a music variant the cut names (music … variant <name>): out/<name>-<variant>.mp4' })
41
46
 
42
47
  async execute(): Promise<number> {
43
48
  return run(this.context, async (say, root, cwd) => {
44
49
  const b = await import('../trailer/build.ts')
45
- const l = await b.load(b.trailerFile(this.file, root, cwd), root)
50
+ const l = await b.load(b.trailerFile(this.file, root, cwd), root, this.variant !== undefined ? { variant: this.variant } : {})
46
51
  if (!b.sayErrors(l, say, cwd)) return 1
47
52
  await b.build(l, { refilm: refilmOf(this.refilm), final: this.final, say, cwd })
48
53
  return 0
@@ -64,14 +69,17 @@ export class TrailerCheckCommand extends Command {
64
69
  })
65
70
 
66
71
  file = Option.String({ required: false })
72
+ variant = Option.String('--variant', { description: 'a music variant the cut names' })
67
73
 
68
74
  async execute(): Promise<number> {
69
75
  return run(this.context, async (say, root, cwd) => {
70
76
  const t0 = performance.now()
71
77
  const b = await import('../trailer/build.ts')
72
- const l = await b.load(b.trailerFile(this.file, root, cwd), root)
78
+ const l = await b.load(b.trailerFile(this.file, root, cwd), root, this.variant !== undefined ? { variant: this.variant } : {})
73
79
  if (!b.sayErrors(l, say, cwd)) return 1
74
80
  for (const line of b.timeline(l, b.takesOfLoaded(l), await b.worldOf(l), cwd)) say(line)
81
+ for (const line of await b.soundLines(l)) say(` ${line}`)
82
+ for (const line of await b.fontLines(l)) say(` ${line}`)
75
83
  say(`ok: checked in ${((performance.now() - t0) / 1000).toFixed(2)} s`)
76
84
  return 0
77
85
  })
@@ -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({
@@ -65,6 +65,10 @@ export type Verb = {
65
65
  version?: number | string
66
66
  /** with film: `own`, a clip that films as long as it plays (no `--seconds`) */
67
67
  length?: 'own'
68
+ /** with film: the words it takes and their types (trailer/words.ts), each checked at its line of a .trailer */
69
+ words?: Record<string, unknown>
70
+ /** with film: the paths its takes film, their world */
71
+ stamp?: string[]
68
72
  }
69
73
 
70
74
  /** the argv a row runs */
@@ -126,6 +130,8 @@ export function verbs(rows: Verb[]): CommandClass[] {
126
130
  static film = v.film
127
131
  static filmVersion = v.version
128
132
  static filmLength = v.length
133
+ static filmWords = v.words
134
+ static filmStamp = v.stamp
129
135
  args = Option.Proxy()
130
136
  async execute(): Promise<number> {
131
137
  const { root, cwd } = this.context
@@ -154,14 +160,16 @@ export function verbs(rows: Verb[]): CommandClass[] {
154
160
 
155
161
  /** the marks a command class carries: set by `verbs()` from its row, or
156
162
  * declared as statics on a hand written class */
157
- export function marksOf(c: object): { essential?: Essential; film?: Film; version?: number | string; length?: 'own' } {
158
- const o = c as { essential?: unknown; film?: unknown; filmVersion?: unknown; filmLength?: unknown }
159
- const out: { essential?: Essential; film?: Film; version?: number | string; length?: 'own' } = {}
163
+ export function marksOf(c: object): { essential?: Essential; film?: Film; version?: number | string; length?: 'own'; words?: Record<string, unknown>; stamp?: string[] } {
164
+ const o = c as { essential?: unknown; film?: unknown; filmVersion?: unknown; filmLength?: unknown; filmWords?: unknown; filmStamp?: unknown }
165
+ const out: { essential?: Essential; film?: Film; version?: number | string; length?: 'own'; words?: Record<string, unknown>; stamp?: string[] } = {}
160
166
  if (typeof o.essential === 'object' && o.essential !== null) out.essential = o.essential as Essential
161
167
  if (o.film === 'still' || o.film === 'clip') {
162
168
  out.film = o.film
163
169
  if (typeof o.filmVersion === 'number' || typeof o.filmVersion === 'string') out.version = o.filmVersion
164
170
  if (o.filmLength === 'own') out.length = 'own'
171
+ if (typeof o.filmWords === 'object' && o.filmWords !== null) out.words = o.filmWords as Record<string, unknown>
172
+ if (Array.isArray(o.filmStamp)) out.stamp = o.filmStamp.map(String)
165
173
  }
166
174
  return out
167
175
  }
@@ -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
+ }