rvlib-shipkit 2.2.0 → 2.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 +47 -0
- package/package.json +1 -1
- package/src/agent-time/mask.ts +43 -0
- package/src/agent-time/render.ts +187 -0
- package/src/agent-time/report.ts +335 -0
- package/src/agent-time/shape.ts +271 -0
- package/src/agent-time/signals.ts +84 -0
- package/src/agent-time/store.ts +379 -0
- package/src/agent-time/timeline.ts +218 -0
- package/src/agent-time/turns.ts +135 -0
- package/src/assets/kits/identity-kit.ts +5 -2
- package/src/cli/cmd_Commands.ts +9 -0
- package/src/cli/cmd_Review.ts +974 -0
- package/src/cli/cmd_Sound.ts +209 -0
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +264 -6
- package/src/cli/kit/verb.ts +22 -4
- package/src/cli.ts +8 -4
- package/src/collections/collection-types.ts +6 -1
- package/src/collections/providers.ts +3 -0
- package/src/collections/review.ts +70 -0
- package/src/core/app-icon.ts +10 -6
- package/src/core/layout.ts +2 -0
- package/src/reflect/prompt-corpus.ts +15 -119
- package/src/reflect/transcript.ts +254 -0
- package/src/repo-config.ts +3 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +158 -0
- package/src/review/http.ts +23 -0
- package/src/review/marks.ts +43 -0
- package/src/review/media.ts +45 -0
- package/src/review/model.ts +896 -0
- package/src/review/project.ts +126 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +239 -0
- package/src/review/store.ts +1011 -0
- package/src/review/threads.ts +237 -0
- package/src/review/trailer-desk.ts +385 -0
- package/src/review/trailer-model.ts +183 -0
- package/src/review/trailer-routes.ts +136 -0
- package/src/review/unit.ts +183 -0
- package/src/review/watchers.ts +54 -0
- package/src/serve/embed.ts +35 -0
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/activity.css +0 -4
- package/src/serve/hub-app/admin-svc.ts +1 -1
- package/src/serve/hub-app/admin-view.tsx +1 -1
- package/src/serve/hub-app/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +36 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +17 -3
- package/src/serve/hub-app/home-strip.tsx +3 -2
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +80 -11
- package/src/serve/hub-app/hub.css +83 -14
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/insights-view.tsx +4 -3
- package/src/serve/hub-app/keys.ts +14 -10
- package/src/serve/hub-app/layout-st.ts +63 -2
- package/src/serve/hub-app/md.ts +32 -0
- package/src/serve/hub-app/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +26 -6
- package/src/serve/hub-app/project-list.tsx +46 -10
- package/src/serve/hub-app/projects-svc.ts +7 -2
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +61 -4
- package/src/serve/hub-app/review/clip-st.ts +258 -0
- package/src/serve/hub-app/review/kind-view.tsx +240 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +269 -0
- package/src/serve/hub-app/review/review-board.tsx +46 -0
- package/src/serve/hub-app/review/review-list.tsx +156 -0
- package/src/serve/hub-app/review/review-page.tsx +96 -0
- package/src/serve/hub-app/review/review-st.ts +902 -0
- package/src/serve/hub-app/review/review-views.tsx +648 -0
- package/src/serve/hub-app/review/trailer-page.tsx +416 -0
- package/src/serve/hub-app/review/trailer-st.ts +602 -0
- package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
- package/src/serve/hub-app/review.css +389 -0
- package/src/serve/hub-app/theme-st.ts +56 -0
- package/src/serve/hub-app/tree-build.ts +4 -0
- package/src/serve/hub-app/tree-row.tsx +1 -1
- package/src/serve/hub-app/tree-svc.ts +4 -0
- package/src/serve/hub-app/wiki-st.ts +190 -0
- package/src/serve/hub-app/wikis-view.tsx +134 -5
- package/src/serve/hub-runtime.ts +9 -7
- package/src/serve/project-gutter-route.ts +25 -1
- package/src/serve/project-gutter.ts +1 -1
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +15 -0
- package/src/shipkit-files.ts +9 -5
- package/src/shot/client.ts +17 -5
- package/src/shot/shooter.ts +19 -3
- package/src/shot/shot-ask.ts +9 -1
- package/src/shot/stepped-clip.ts +53 -0
- package/src/trailer/adapter.ts +90 -6
- package/src/trailer/build.ts +459 -59
- package/src/trailer/ffmpeg.ts +22 -0
- package/src/trailer/fonts.ts +55 -0
- package/src/trailer/footage.ts +112 -43
- package/src/trailer/grammar.ts +864 -127
- package/src/trailer/graph.ts +308 -49
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +20 -3
- package/src/trailer/look.ts +20 -3
- package/src/trailer/motion.ts +207 -0
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +82 -23
- package/src/trailer/prune.ts +204 -0
- package/src/trailer/score/compose.ts +256 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/gm-render.ts +43 -0
- package/src/trailer/score/gm.ts +90 -0
- package/src/trailer/score/midi.ts +143 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +98 -0
- package/src/trailer/score/score.ts +228 -0
- package/src/trailer/score/sound.ts +123 -0
- package/src/trailer/score/synth.ts +383 -0
- package/src/trailer/sound/audio.ts +148 -0
- package/src/trailer/sound/brand.ts +42 -0
- package/src/trailer/sound/comfy.ts +38 -0
- package/src/trailer/sound/eleven.ts +72 -0
- package/src/trailer/sound/fluid.ts +71 -0
- package/src/trailer/sound/gen.ts +139 -0
- package/src/trailer/sound/index.ts +60 -0
- package/src/trailer/sound/presets.ts +69 -0
- package/src/trailer/sound/run.ts +189 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +92 -9
- package/src/trailer/titles.ts +471 -150
- package/src/trailer/url.ts +18 -3
- package/src/trailer/vocab.ts +34 -4
- package/src/trailer/words.ts +275 -0
- package/src/wiki/graph-mount.ts +3 -0
- package/src/wiki/graph-page.ts +45 -9
- 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]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// `sk time`: where the agents' time went in a repo, read from every Claude
|
|
2
|
+
// Code transcript on the machine (every config root, sub-agents included)
|
|
3
|
+
// through the agent time index (agent-time/store.ts). REPORT ONLY: exit 0 with
|
|
4
|
+
// a report, 1 when nothing matched, 2 on a bad flag.
|
|
5
|
+
import { Command, Option } from 'clipanion'
|
|
6
|
+
import { resolve } from 'path'
|
|
7
|
+
|
|
8
|
+
const BIG_BYTES = 50 * 1024 * 1024
|
|
9
|
+
|
|
10
|
+
export class TimeCommand extends Command {
|
|
11
|
+
static override paths = [['time']]
|
|
12
|
+
|
|
13
|
+
static override usage = Command.Usage({
|
|
14
|
+
category: 'Health',
|
|
15
|
+
description: 'where agent time went in a repo, every account: waits, slow and failing commands, feedback',
|
|
16
|
+
details: [
|
|
17
|
+
'Reads every Claude Code transcript under every config root (~/.claude, ~/.claude-<name>), sub-agent runs included, through an index at ~/.shipkit/agent-time.sqlite.',
|
|
18
|
+
'The first run builds the index (niced, resumable, progress on stderr); later runs read only what changed. The index keeps sessions Claude Code has pruned.',
|
|
19
|
+
'Views: summary (default), commands, tools, slow, fails, feedback, sessions, turns. A Bash command is grouped by its shape (`git commit`, `bun test <file>`, `sk ci --only lint`).',
|
|
20
|
+
'Turns: every call costs a model turn before it, so the summary gives the model time before each call, the share of single read calls and the share sent in parallel, for the main sessions and the sub-agents; `turns` lists the runs and the read shapes by the model time they cost.',
|
|
21
|
+
'Wall time is cut once per moment: tools in flight, a sub-agent in flight, waiting on a background task, waiting on you (an idle gap past 30 minutes is left out), else the model.',
|
|
22
|
+
'Commands and prompts are masked through the leak guard keyword list and token shapes before they print.',
|
|
23
|
+
].join(' '),
|
|
24
|
+
examples: [
|
|
25
|
+
['this repo, the last 7 days', 'sk time'],
|
|
26
|
+
['every command shape, 30 days', 'sk time commands --since 30d'],
|
|
27
|
+
['what he said while it ran', 'sk time feedback --since all'],
|
|
28
|
+
['the runs by their turns, and what the reads cost', 'sk time turns --since 30d'],
|
|
29
|
+
['two repos, every recorded session', 'sk time ~/dev/pkm-multi ~/dev/procedural-creatures --since all'],
|
|
30
|
+
['every repo on the machine', 'sk time --all'],
|
|
31
|
+
['for a script', 'sk time --json'],
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
args = Option.Rest()
|
|
36
|
+
all = Option.Boolean('--all', false, { description: 'every repo with transcripts on this machine' })
|
|
37
|
+
since = Option.String('--since', '7d', { description: '30m, 2h, 7d, a date, or all' })
|
|
38
|
+
top = Option.String('--top', '20', { description: 'rows per list in a sub view' })
|
|
39
|
+
json = Option.Boolean('--json', false, { description: 'the whole report as json' })
|
|
40
|
+
|
|
41
|
+
async execute(): Promise<number> {
|
|
42
|
+
const { VIEWS, render } = await import('../agent-time/render.ts')
|
|
43
|
+
const { sinceOf } = await import('./kit/calls.ts')
|
|
44
|
+
const err = this.context.stderr
|
|
45
|
+
const tty = (err as { isTTY?: boolean }).isTTY === true
|
|
46
|
+
const views = this.args.filter((a) => (VIEWS as readonly string[]).includes(a))
|
|
47
|
+
const repoArgs = this.args.filter((a) => !views.includes(a))
|
|
48
|
+
const bad = repoArgs.find((a) => !a.includes('/') && !a.startsWith('.') && !a.startsWith('~'))
|
|
49
|
+
if (views.length > 1 || bad != null) {
|
|
50
|
+
err.write(`FAIL: ${bad == null ? `one view at a time, not ${views.join(' and ')}` : `unknown view '${bad}'`}: ${VIEWS.join(', ')}, or a repo path\n`)
|
|
51
|
+
return 2
|
|
52
|
+
}
|
|
53
|
+
const view = (views[0] ?? 'summary') as (typeof VIEWS)[number]
|
|
54
|
+
const now = Date.now()
|
|
55
|
+
const from = this.since === 'all' ? null : sinceOf(this.since, now)
|
|
56
|
+
if (from === null && this.since !== 'all') {
|
|
57
|
+
err.write(`FAIL: --since ${this.since}: 30m, 2h, 7d, a date, or all\n`)
|
|
58
|
+
return 2
|
|
59
|
+
}
|
|
60
|
+
const top = Number(this.top)
|
|
61
|
+
if (!Number.isInteger(top) || top <= 0) {
|
|
62
|
+
err.write(`FAIL: --top ${this.top}: a positive whole number\n`)
|
|
63
|
+
return 2
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { indexTranscripts, listTranscripts, loadWindow, markGone, openIndex, repoOfProject, agentTimeDbPath } = await import('../agent-time/store.ts')
|
|
67
|
+
const { discoverConfigRoots, projectSlug } = await import('../reflect/prompt-corpus.ts')
|
|
68
|
+
const { buildReport } = await import('../agent-time/report.ts')
|
|
69
|
+
const { loadMasker } = await import('../agent-time/mask.ts')
|
|
70
|
+
|
|
71
|
+
const repos = repoArgs.length > 0 ? repoArgs.map((r) => resolve(r)) : [process.cwd()]
|
|
72
|
+
const slugs = repos.map(projectSlug)
|
|
73
|
+
const keep = this.all ? () => true : (p: string) => slugs.some((s) => p === s || p.startsWith(`${s}--claude-worktrees-`))
|
|
74
|
+
const roots = discoverConfigRoots()
|
|
75
|
+
const db = openIndex()
|
|
76
|
+
try {
|
|
77
|
+
const files = listTranscripts(roots, keep)
|
|
78
|
+
const pending = files.length
|
|
79
|
+
const tIndex = performance.now()
|
|
80
|
+
let lastPrint = 0
|
|
81
|
+
let niced = false
|
|
82
|
+
const res = indexTranscripts(db, files, (p) => {
|
|
83
|
+
if (p.bytes < BIG_BYTES) return
|
|
84
|
+
if (!niced) {
|
|
85
|
+
niced = true
|
|
86
|
+
try { (require('os') as typeof import('os')).setPriority(0, 15) } catch { /* not allowed: index at the normal priority */ }
|
|
87
|
+
}
|
|
88
|
+
const t = performance.now()
|
|
89
|
+
if (t - lastPrint < (tty ? 200 : 10_000) && p.done < p.files) return
|
|
90
|
+
lastPrint = t
|
|
91
|
+
const line = `index: ${p.done}/${p.files} files, ${(p.doneBytes / 1e9).toFixed(1)}/${(p.bytes / 1e9).toFixed(1)} GB, ${Math.round((t - tIndex) / 1000)}s`
|
|
92
|
+
err.write(tty ? `\r${line}` : `${line}\n`)
|
|
93
|
+
})
|
|
94
|
+
if (res.bytes >= BIG_BYTES) err.write(`${tty ? '\n' : ''}index: read ${res.read} of ${pending} files, ${(res.bytes / 1e9).toFixed(2)} GB in ${(res.ms / 1000).toFixed(1)}s (${agentTimeDbPath()})\n`)
|
|
95
|
+
markGone(db, files, keep)
|
|
96
|
+
const loaded = loadWindow(db, keep, from ?? 0, now)
|
|
97
|
+
const labels = this.all ? [...new Set(loaded.files.map((f) => repoOfProject(f.project)))].sort() : slugs.map((s) => repoOfProject(s))
|
|
98
|
+
if (loaded.files.length === 0) {
|
|
99
|
+
err.write(`FAIL: no agent session in ${labels.join(', ') || 'any repo'} ${from == null ? 'ever' : `since ${new Date(from).toISOString().slice(0, 10)}`}\n`)
|
|
100
|
+
err.write(` looked under: ${roots.map((r) => r.projects).join(' · ')}\n`)
|
|
101
|
+
err.write(` fix: widen it, --since all, or name a repo that ran Claude Code sessions\n`)
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
const report = buildReport(loaded, { repos: labels, from, to: now, repoOf: (p) => repoOfProject(p) }, loadMasker(repos[0] ?? null))
|
|
105
|
+
if (this.json) this.context.stdout.write(`${JSON.stringify(report)}\n`)
|
|
106
|
+
else this.context.stdout.write(render(report, view, top))
|
|
107
|
+
return 0
|
|
108
|
+
} finally {
|
|
109
|
+
db.close()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/cli/cmd_Trailer.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Every module loads inside execute: the cli registers this file on every `sk` call
|
|
3
3
|
import { Command, Option } from 'clipanion'
|
|
4
4
|
|
|
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, or a film row of the project CLI), 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. Outputs land in trailer/<trailer>/out/. Page: wiki build/trailer.md`
|
|
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
6
|
|
|
7
7
|
/** --refilm alone: every stale take; --refilm a,b: those shots */
|
|
8
8
|
const refilmOf = (v: string | boolean | undefined): 'stale' | ReadonlySet<string> | null => (v === undefined || v === false ? null : v === true ? 'stale' : new Set(v.split(',')))
|
|
@@ -32,17 +32,19 @@ export class TrailerCommand extends Command {
|
|
|
32
32
|
['one cut by its file', 'sk trailer trailer/launch/main.trailer'],
|
|
33
33
|
['film again the takes of two shots', 'sk trailer --refilm intro,menu'],
|
|
34
34
|
['encoded by x264 for a release', 'sk trailer --final'],
|
|
35
|
+
['the music variant b on the same picture', 'sk trailer --variant b'],
|
|
35
36
|
],
|
|
36
37
|
})
|
|
37
38
|
|
|
38
39
|
file = Option.String({ required: false })
|
|
39
40
|
refilm = Option.String('--refilm', { tolerateBoolean: true, description: 'film again: every stale take, or the named shots (a,b)' })
|
|
40
41
|
final = Option.Boolean('--final', false, { description: 'encode with x264 for a release (default: the fast draft encoder)' })
|
|
42
|
+
variant = Option.String('--variant', { description: 'a music variant the cut names (music … variant <name>): out/<name>-<variant>.mp4' })
|
|
41
43
|
|
|
42
44
|
async execute(): Promise<number> {
|
|
43
45
|
return run(this.context, async (say, root, cwd) => {
|
|
44
46
|
const b = await import('../trailer/build.ts')
|
|
45
|
-
const l = b.load(b.trailerFile(this.file, root, cwd), root)
|
|
47
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root, this.variant !== undefined ? { variant: this.variant } : {})
|
|
46
48
|
if (!b.sayErrors(l, say, cwd)) return 1
|
|
47
49
|
await b.build(l, { refilm: refilmOf(this.refilm), final: this.final, say, cwd })
|
|
48
50
|
return 0
|
|
@@ -64,21 +66,49 @@ export class TrailerCheckCommand extends Command {
|
|
|
64
66
|
})
|
|
65
67
|
|
|
66
68
|
file = Option.String({ required: false })
|
|
69
|
+
variant = Option.String('--variant', { description: 'a music variant the cut names' })
|
|
67
70
|
|
|
68
71
|
async execute(): Promise<number> {
|
|
69
72
|
return run(this.context, async (say, root, cwd) => {
|
|
70
73
|
const t0 = performance.now()
|
|
71
74
|
const b = await import('../trailer/build.ts')
|
|
72
|
-
const l = b.load(b.trailerFile(this.file, root, cwd), root)
|
|
75
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root, this.variant !== undefined ? { variant: this.variant } : {})
|
|
73
76
|
if (!b.sayErrors(l, say, cwd)) return 1
|
|
74
|
-
const
|
|
75
|
-
for (const line of b.
|
|
77
|
+
for (const line of b.timeline(l, b.takesOfLoaded(l), await b.worldOf(l), cwd)) say(line)
|
|
78
|
+
for (const line of await b.soundLines(l)) say(` ${line}`)
|
|
79
|
+
for (const line of await b.fontLines(l)) say(` ${line}`)
|
|
76
80
|
say(`ok: checked in ${((performance.now() - t0) / 1000).toFixed(2)} s`)
|
|
77
81
|
return 0
|
|
78
82
|
})
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
export class TrailerScoreCommand extends Command {
|
|
87
|
+
static override paths = [['trailer', 'score']]
|
|
88
|
+
|
|
89
|
+
static override usage = Command.Usage({
|
|
90
|
+
category: 'Trailer',
|
|
91
|
+
description: 'the score alone, no picture: its wav, its waveform png, each section\'s level and the loudness',
|
|
92
|
+
details: `${DETAILS}. The score renders offline in Bun from the file's score, track, drums, code, section and play lines; a section whose notes are silent fails`,
|
|
93
|
+
examples: [
|
|
94
|
+
['the score of the one cut', 'sk trailer score'],
|
|
95
|
+
['the score of a named cut', 'sk trailer score trailer/launch'],
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
file = Option.String({ required: false })
|
|
100
|
+
|
|
101
|
+
async execute(): Promise<number> {
|
|
102
|
+
return run(this.context, async (say, root, cwd) => {
|
|
103
|
+
const b = await import('../trailer/build.ts')
|
|
104
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
105
|
+
if (!b.sayErrors(l, say, cwd)) return 1
|
|
106
|
+
await b.auditionScore(l, { refilm: null, final: false, say, cwd })
|
|
107
|
+
return 0
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
export class TrailerShotCommand extends Command {
|
|
83
113
|
static override paths = [['trailer', 'shot']]
|
|
84
114
|
|
|
@@ -99,10 +129,238 @@ export class TrailerShotCommand extends Command {
|
|
|
99
129
|
async execute(): Promise<number> {
|
|
100
130
|
return run(this.context, async (say, root, cwd) => {
|
|
101
131
|
const b = await import('../trailer/build.ts')
|
|
102
|
-
const l = b.load(b.trailerFile(this.file, root, cwd), root)
|
|
132
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
103
133
|
if (!b.sayErrors(l, say, cwd)) return 1
|
|
104
134
|
await b.previewShot(l, this.name, { refilm: refilmOf(this.refilm), final: false, say, cwd })
|
|
105
135
|
return 0
|
|
106
136
|
})
|
|
107
137
|
}
|
|
108
138
|
}
|
|
139
|
+
|
|
140
|
+
/** a cut loaded and checked, its errors said; null when it has one */
|
|
141
|
+
async function loaded(file: string | undefined, say: (l: string) => void, root: string, cwd: string) {
|
|
142
|
+
const b = await import('../trailer/build.ts')
|
|
143
|
+
const l = await b.load(b.trailerFile(file, root, cwd), root)
|
|
144
|
+
return b.sayErrors(l, say, cwd) ? l : null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class TrailerSectionCommand extends Command {
|
|
148
|
+
static override paths = [['trailer', 'section']]
|
|
149
|
+
|
|
150
|
+
static override usage = Command.Usage({
|
|
151
|
+
category: 'Trailer',
|
|
152
|
+
description: 'one section alone with its window of the sound: out/<cut>-<section>.mp4',
|
|
153
|
+
details: `${DETAILS}. \`start\` is the shots before the first section; only a section the cache misses renders`,
|
|
154
|
+
examples: [['a section of the one cut', 'sk trailer section drop']],
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
name = Option.String({ required: true })
|
|
158
|
+
file = Option.String({ required: false })
|
|
159
|
+
final = Option.Boolean('--final', false, { description: 'encode with x264 for a release' })
|
|
160
|
+
|
|
161
|
+
async execute(): Promise<number> {
|
|
162
|
+
return run(this.context, async (say, root, cwd) => {
|
|
163
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
164
|
+
if (!l) return 1
|
|
165
|
+
await (await import('../trailer/build.ts')).previewSection(l, this.name, { refilm: null, final: this.final, say, cwd })
|
|
166
|
+
return 0
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class TrailerLevelsCommand extends Command {
|
|
172
|
+
static override paths = [['trailer', 'levels']]
|
|
173
|
+
|
|
174
|
+
static override usage = Command.Usage({
|
|
175
|
+
category: 'Trailer',
|
|
176
|
+
description: 'each section\'s mean and peak in the built mp4, and its step from the section before',
|
|
177
|
+
details: DETAILS,
|
|
178
|
+
examples: [['the levels of the one cut', 'sk trailer levels']],
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
file = Option.String({ required: false })
|
|
182
|
+
|
|
183
|
+
async execute(): Promise<number> {
|
|
184
|
+
return run(this.context, async (say, root, cwd) => {
|
|
185
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
186
|
+
if (!l) return 1
|
|
187
|
+
await (await import('../trailer/passage.ts')).levels(l, say, cwd)
|
|
188
|
+
return 0
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export class TrailerClipCommand extends Command {
|
|
194
|
+
static override paths = [['trailer', 'clip']]
|
|
195
|
+
|
|
196
|
+
static override usage = Command.Usage({
|
|
197
|
+
category: 'Trailer',
|
|
198
|
+
description: 'a passage of the built cut with its sound, from a section, mark, shot or second',
|
|
199
|
+
details: `${DETAILS}. --version cuts the same passage of a kept version beside it, read from versions/<v>.trailer`,
|
|
200
|
+
examples: [
|
|
201
|
+
['a section', 'sk trailer clip drop'],
|
|
202
|
+
['from a shot to a mark, before and after', 'sk trailer clip hero hit --version v3'],
|
|
203
|
+
],
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
from = Option.String({ required: true })
|
|
207
|
+
rest = Option.Rest({ required: 0 })
|
|
208
|
+
version = Option.String('--version', { description: 'a kept version (v<n>): the same passage of it beside' })
|
|
209
|
+
pad = Option.String('--pad', { description: 'seconds of room before and after' })
|
|
210
|
+
|
|
211
|
+
async execute(): Promise<number> {
|
|
212
|
+
return run(this.context, async (say, root, cwd) => {
|
|
213
|
+
const { existsSync } = await import('node:fs')
|
|
214
|
+
const { resolve } = await import('node:path')
|
|
215
|
+
// the second word is a moment unless it names a file or a folder
|
|
216
|
+
const [a, b] = this.rest
|
|
217
|
+
const isPath = (w: string | undefined): boolean => w !== undefined && (w.endsWith('.trailer') || existsSync(resolve(cwd, w)))
|
|
218
|
+
const to = a !== undefined && !isPath(a) ? a : this.from
|
|
219
|
+
const file = a !== undefined && !isPath(a) ? b : a
|
|
220
|
+
const l = await loaded(file, say, root, cwd)
|
|
221
|
+
if (!l) return 1
|
|
222
|
+
const pad = Number(this.pad ?? 0)
|
|
223
|
+
if (!Number.isFinite(pad) || pad < 0) throw new Error(`--pad wants seconds, got ${this.pad}`)
|
|
224
|
+
await (await import('../trailer/passage.ts')).clip(l, this.from, to, pad, this.version, say, cwd)
|
|
225
|
+
return 0
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class TrailerKeepCommand extends Command {
|
|
231
|
+
static override paths = [['trailer', 'keep']]
|
|
232
|
+
|
|
233
|
+
static override usage = Command.Usage({
|
|
234
|
+
category: 'Trailer',
|
|
235
|
+
description: 'keep the built cut as a version: versions/v<n>.trailer, its video, strip and takes',
|
|
236
|
+
details: `${DETAILS}. Refuses a version already kept; prints the index row, commit and tag left to do`,
|
|
237
|
+
examples: [['keep the one cut', 'sk trailer keep v4']],
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
version = Option.String({ required: true })
|
|
241
|
+
file = Option.String({ required: false })
|
|
242
|
+
|
|
243
|
+
async execute(): Promise<number> {
|
|
244
|
+
return run(this.context, async (say, root, cwd) => {
|
|
245
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
246
|
+
if (!l) return 1
|
|
247
|
+
;(await import('../trailer/passage.ts')).keep(l, this.version, say, cwd)
|
|
248
|
+
return 0
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export class TrailerFootageCommand extends Command {
|
|
254
|
+
static override paths = [['trailer', 'footage']]
|
|
255
|
+
|
|
256
|
+
static override usage = Command.Usage({
|
|
257
|
+
category: 'Trailer',
|
|
258
|
+
description: 'the take file of each shot or footage block, the raw take before the stitch trims it',
|
|
259
|
+
details: DETAILS,
|
|
260
|
+
examples: [['two shots of the one cut', 'sk trailer footage intro,hero']],
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
names = Option.String({ required: true })
|
|
264
|
+
file = Option.String({ required: false })
|
|
265
|
+
|
|
266
|
+
async execute(): Promise<number> {
|
|
267
|
+
return run(this.context, async (say, root, cwd) => {
|
|
268
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
269
|
+
if (!l) return 1
|
|
270
|
+
;(await import('../trailer/passage.ts')).footage(l, this.names.split(','), say, cwd)
|
|
271
|
+
return 0
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export class TrailerWatchCommand extends Command {
|
|
277
|
+
static override paths = [['trailer', 'watch']]
|
|
278
|
+
|
|
279
|
+
static override usage = Command.Usage({
|
|
280
|
+
category: 'Trailer',
|
|
281
|
+
description: 'build again after each save in the cut\'s folder (ctrl-c stops)',
|
|
282
|
+
details: DETAILS,
|
|
283
|
+
examples: [['the one cut', 'sk trailer watch']],
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
file = Option.String({ required: false })
|
|
287
|
+
final = Option.Boolean('--final', false, { description: 'encode with x264 for a release' })
|
|
288
|
+
|
|
289
|
+
async execute(): Promise<number> {
|
|
290
|
+
return run(this.context, async (say, root, cwd) => {
|
|
291
|
+
const b = await import('../trailer/build.ts')
|
|
292
|
+
const { watch } = await import('node:fs')
|
|
293
|
+
const { dirname, relative } = await import('node:path')
|
|
294
|
+
const file = b.trailerFile(this.file, root, cwd)
|
|
295
|
+
let busy = false
|
|
296
|
+
let again = false
|
|
297
|
+
const once = async (): Promise<void> => {
|
|
298
|
+
if (busy) {
|
|
299
|
+
again = true
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
busy = true
|
|
303
|
+
try {
|
|
304
|
+
const l = await b.load(file, root, { fresh: true })
|
|
305
|
+
if (b.sayErrors(l, say, cwd)) await b.build(l, { refilm: null, final: this.final, say, cwd })
|
|
306
|
+
} catch (e) {
|
|
307
|
+
say(`FAIL: ${e instanceof Error ? e.message : String(e)}`)
|
|
308
|
+
}
|
|
309
|
+
busy = false
|
|
310
|
+
if (again) {
|
|
311
|
+
again = false
|
|
312
|
+
await once()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
await once()
|
|
316
|
+
say(`watching ${relative(cwd, file)} and its folder's recipes (ctrl-c stops)`)
|
|
317
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
318
|
+
watch(dirname(file), (_, f) => {
|
|
319
|
+
if (!f || f.startsWith('out') || f.startsWith('versions') || f.endsWith('.md')) return
|
|
320
|
+
if (timer) clearTimeout(timer)
|
|
321
|
+
timer = setTimeout(() => void once(), 200)
|
|
322
|
+
})
|
|
323
|
+
await new Promise(() => undefined)
|
|
324
|
+
return 0
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export class TrailerPruneCommand extends Command {
|
|
330
|
+
static override paths = [['trailer', 'prune']]
|
|
331
|
+
|
|
332
|
+
static override usage = Command.Usage({
|
|
333
|
+
category: 'Trailer',
|
|
334
|
+
description: 'trim the trailer cache to what the cuts use, what each folder frees',
|
|
335
|
+
details: `${DETAILS}. Every build, section and shot runs it after its stitch; a file younger than an hour stays`,
|
|
336
|
+
examples: [
|
|
337
|
+
['what it would free', 'sk trailer prune --dry'],
|
|
338
|
+
['free it', 'sk trailer prune'],
|
|
339
|
+
],
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
dry = Option.Boolean('--dry', false, { description: 'say what each folder would free, delete nothing' })
|
|
343
|
+
|
|
344
|
+
async execute(): Promise<number> {
|
|
345
|
+
return run(this.context, async (say, root) => {
|
|
346
|
+
const { prune, sayPruned } = await import('../trailer/prune.ts')
|
|
347
|
+
for (const line of sayPruned(await prune(root, { dry: this.dry }), this.dry)) say(line)
|
|
348
|
+
return 0
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** every `sk trailer` verb, registered by cli.ts */
|
|
354
|
+
export const TRAILER_COMMANDS = [
|
|
355
|
+
TrailerCommand,
|
|
356
|
+
TrailerCheckCommand,
|
|
357
|
+
TrailerScoreCommand,
|
|
358
|
+
TrailerShotCommand,
|
|
359
|
+
TrailerSectionCommand,
|
|
360
|
+
TrailerLevelsCommand,
|
|
361
|
+
TrailerClipCommand,
|
|
362
|
+
TrailerKeepCommand,
|
|
363
|
+
TrailerFootageCommand,
|
|
364
|
+
TrailerWatchCommand,
|
|
365
|
+
TrailerPruneCommand,
|
|
366
|
+
]
|