rvlib-shipkit 2.1.1 → 2.3.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 +50 -0
- package/package.json +2 -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 +7 -3
- package/src/assets/run-assets.ts +18 -4
- package/src/checks/SK004-gitignore-policy.check.ts +11 -6
- package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
- package/src/cli/cmd_Calls.ts +53 -0
- package/src/cli/cmd_Commands.ts +163 -43
- package/src/cli/cmd_Review.ts +948 -0
- package/src/cli/cmd_Shot.ts +63 -0
- package/src/cli/cmd_Start.ts +8 -4
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +361 -0
- package/src/cli/kit/builtins.ts +40 -0
- package/src/cli/kit/calls.ts +177 -0
- package/src/cli/kit/families.ts +67 -0
- package/src/cli/kit/index.ts +8 -0
- package/src/cli/kit/pages-check.ts +36 -0
- package/src/cli/kit/project-cli.ts +73 -0
- package/src/cli/kit/verb.ts +167 -0
- package/src/cli.ts +15 -2
- 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/layout.ts +12 -2
- package/src/core/migrate-layout.ts +31 -8
- package/src/core/one-way-in-policy.ts +12 -4
- package/src/reflect/prompt-corpus.ts +15 -114
- package/src/reflect/transcript.ts +254 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +150 -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 +854 -0
- package/src/review/project.ts +110 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +228 -0
- package/src/review/store.ts +1002 -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 +165 -0
- package/src/review/watchers.ts +54 -0
- package/src/scaffold/cli-skeleton.ts +149 -0
- package/src/scaffold/files/agent-index.md +2 -2
- package/src/scaffold/files/tsconfig.json +3 -0
- package/src/scaffold/files/verify-skill.md +1 -1
- package/src/scaffold/project-scaffold.ts +90 -12
- 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/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +28 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +10 -2
- 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 +45 -13
- package/src/serve/hub-app/icons.tsx +9 -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/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +22 -4
- package/src/serve/hub-app/project-list.tsx +41 -8
- package/src/serve/hub-app/projects-svc.ts +4 -0
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +45 -3
- package/src/serve/hub-app/review/clip-st.ts +208 -0
- package/src/serve/hub-app/review/kind-view.tsx +171 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +239 -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 +101 -0
- package/src/serve/hub-app/review/review-st.ts +779 -0
- package/src/serve/hub-app/review/review-views.tsx +503 -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 +329 -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-runtime.ts +5 -3
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +8 -0
- package/src/shipkit-files.ts +12 -4
- package/src/shot/client.ts +67 -0
- package/src/shot/clip-encoder.ts +146 -0
- package/src/shot/dom-clip.ts +124 -0
- package/src/shot/serve-shot.ts +54 -0
- package/src/shot/shooter.ts +343 -0
- package/src/shot/shot-ask.ts +189 -0
- package/src/shot/shot.shipkit.ts +14 -0
- package/src/smoke/run-smoke.ts +2 -2
- package/src/start-repo.ts +29 -14
- package/src/suite/judge-suites.ts +56 -10
- package/src/suite/select.ts +2 -5
- package/src/suite/suite-cli.ts +3 -2
- package/src/trailer/adapter.ts +78 -0
- package/src/trailer/build.ts +603 -0
- package/src/trailer/ffmpeg.ts +72 -0
- package/src/trailer/footage.ts +173 -0
- package/src/trailer/grammar.ts +1290 -0
- package/src/trailer/graph.ts +508 -0
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +31 -0
- package/src/trailer/look.ts +124 -0
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +192 -0
- package/src/trailer/prune.ts +193 -0
- package/src/trailer/score/compose.ts +240 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +91 -0
- package/src/trailer/score/score.ts +214 -0
- package/src/trailer/score/sound.ts +112 -0
- package/src/trailer/score/synth.ts +361 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +233 -0
- package/src/trailer/titles.ts +379 -0
- package/src/trailer/url.ts +108 -0
- package/src/trailer/vertical-draw.ts +286 -0
- package/src/trailer/vertical.ts +148 -0
- package/src/trailer/vocab.ts +59 -0
- package/src/trailer/words.ts +254 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// sk shot: a png of any url, or a clip with --video, through the machine's
|
|
2
|
+
// one warm browser (shot/client.ts). A repo's own CLI wraps it with its setup
|
|
3
|
+
// flags; the trailer's `url` film calls the same functions.
|
|
4
|
+
import { relative } from 'path'
|
|
5
|
+
import { Command, Option } from 'clipanion'
|
|
6
|
+
import { clipUrl, shotUrl, type ShotResult } from '../shot/client.ts'
|
|
7
|
+
import { askOf, isClip } from '../shot/shot-ask.ts'
|
|
8
|
+
|
|
9
|
+
export const renderShot = (r: ShotResult, cwd: string): string[] => {
|
|
10
|
+
const phases = Object.entries(r.phases)
|
|
11
|
+
.map(([k, v]) => `${k} ${v}`)
|
|
12
|
+
.join(', ')
|
|
13
|
+
const out = relative(cwd, r.out)
|
|
14
|
+
const lines = [`ok ${out.startsWith('..') ? r.out : out} in ${r.ms} ms (${phases}; ${r.via}) ${r.note}`]
|
|
15
|
+
for (const e of r.errors.slice(0, 10)) lines.push(`WARN page ${e}`)
|
|
16
|
+
if (r.errors.length > 10) lines.push(`WARN page ${r.errors.length - 10} more errors`)
|
|
17
|
+
return lines
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ShotCommand extends Command {
|
|
21
|
+
static override paths = [['shot']]
|
|
22
|
+
|
|
23
|
+
static override usage = Command.Usage({
|
|
24
|
+
category: 'Serve',
|
|
25
|
+
description: 'a png of any url through the warm browser, or an mp4 clip with --video',
|
|
26
|
+
details:
|
|
27
|
+
'one warm headless chromium for the whole machine (the `shot` global process, started when down). A still reuses the warm page when it already shows the url, so a warm shot answers in about 0.1 s. A page with `<meta name="shot-ready" content="wait">` is shot once it sets content="ready"; any other page --wait ms after its load. --video films a page that is a canvas through its WebCodecs encoder (H.264 muxed with no second encode), and any other page as chromium paints it (its screencast, encoded by ffmpeg as the frames arrive); --dom films the page even when a canvas fills it',
|
|
28
|
+
examples: [
|
|
29
|
+
['a png of a running dev server', 'sk shot localhost:5173/'],
|
|
30
|
+
['a phone screen, at the pixel ratio of a phone', 'sk shot :5173/play --viewport 390,844 --dpr 3 --out shots/phone.png'],
|
|
31
|
+
['a 3 s clip', 'sk shot :4808/ --video 3 --out clips/walk.mp4'],
|
|
32
|
+
['a clip of a whole web app page, canvas or not', 'sk shot :5173/settings --video 3 --dom'],
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
url = Option.String({ required: true, name: 'url' })
|
|
37
|
+
viewport = Option.String('--viewport', { description: 'w,h in css pixels (default 1280,720)' })
|
|
38
|
+
dpr = Option.String('--dpr', { description: 'device pixel ratio (default 1)' })
|
|
39
|
+
out = Option.String('--out', { description: 'the file (default .shipkit/tmp/shots/<url>.png, or .mp4 with --video)' })
|
|
40
|
+
reload = Option.Boolean('--reload', false, { description: 'load the page again even when the warm page shows this url' })
|
|
41
|
+
wait = Option.String('--wait', { description: 'ms after load for a page with no ready mark (default 300)' })
|
|
42
|
+
video = Option.String('--video', { description: 'seconds of clip: an mp4 of the page (its canvas when a canvas fills it)' })
|
|
43
|
+
fps = Option.String('--fps', { description: 'clip frames per second (default 30)' })
|
|
44
|
+
long = Option.Boolean('--long', false, { description: 'allow a clip past 30 s' })
|
|
45
|
+
dom = Option.Boolean('--dom', false, { description: 'clip the page itself even when a canvas fills it' })
|
|
46
|
+
json = Option.Boolean('--json', false, { description: 'one json line: out, ms, phases, errors, note, via' })
|
|
47
|
+
|
|
48
|
+
async execute(): Promise<number> {
|
|
49
|
+
const cwd = process.cwd()
|
|
50
|
+
const err = (l: string) => this.context.stderr.write(`${l}\n`)
|
|
51
|
+
try {
|
|
52
|
+
const a = askOf({ url: this.url, viewport: this.viewport, dpr: this.dpr, out: this.out, reload: this.reload, wait: this.wait, video: this.video, fps: this.fps, long: this.long, dom: this.dom }, cwd)
|
|
53
|
+
const r = isClip(a) ? await clipUrl(a, err) : await shotUrl(a, err)
|
|
54
|
+
if (this.json) this.context.stdout.write(`${JSON.stringify(r)}\n`)
|
|
55
|
+
else for (const l of renderShot(r, cwd)) this.context.stdout.write(`${l}\n`)
|
|
56
|
+
return 0
|
|
57
|
+
} catch (e) {
|
|
58
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
59
|
+
err(msg.startsWith('FAIL') ? msg : `FAIL shot: ${msg}`)
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/cli/cmd_Start.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// shipkit start — ensure <root>/<name> exists (+ a CLAUDE.md include),
|
|
2
2
|
// printing the dir on stdout so a shell alias can cd there (a child process
|
|
3
|
-
// can't cd its parent). Thin clipanion surface; logic in start-repo.ts.
|
|
3
|
+
// can't cd its parent). Thin clipanion surface; logic in start-repo.ts, imported when it runs.
|
|
4
4
|
import { Command, Option } from 'clipanion'
|
|
5
|
-
import { startRepo } from '../start-repo.ts'
|
|
6
5
|
|
|
7
6
|
export class StartCommand extends Command {
|
|
8
7
|
static override paths = [['start']]
|
|
@@ -10,13 +9,14 @@ export class StartCommand extends Command {
|
|
|
10
9
|
static override usage = Command.Usage({
|
|
11
10
|
category: 'Setup',
|
|
12
11
|
description: 'create or enter ~/dev/<name>, ready to work in: a private github repo and a push unless --no-github',
|
|
13
|
-
details: 'create (or enter) ~/dev/<name> from ANY cwd, READY to work in. On any dir with no commit history it writes the day-one shape (package.json + src/index.ts + lint/fmt/typecheck, tsconfig, .vscode tasks + settings, the verify skill, an agent/ wiki, .shipkit/reflections), installs the toolchain, wires shipkit (repo.config.ts via init), and runs the whole bootstrap chain: git init, first commit, PRIVATE github repo, push. Loose files ride in that first commit, and only a repo with real history is left as it is. Writes are create-only for files, additive for package.json scripts and .gitignore. CLAUDE.md is the two-line house template on its own (HOUSE_INCLUDE in src/start-repo.ts: the base wiki include, then `@./agent/index.md`, and --include overrides it), applied only on a machine where the house index exists, and the agent route lands last whenever the repo has agent/index.md. The house lines also opt the repo into the .claude/ policy files (a pre-approved shell allowlist + the attribution silencer), so a start with no house widens no agent permissions. Prints the dir on stdout so a shell alias can cd there. cwd-only sibling: `shipkit init`',
|
|
12
|
+
details: 'create (or enter) ~/dev/<name> from ANY cwd, READY to work in. On any dir with no commit history it writes the day-one shape (package.json + src/index.ts + the project CLI on the kit (src/cli.ts, src/cli/cmd_Dev.ts, the cli script and a bin of the repo name) + lint/fmt/typecheck, tsconfig, .vscode tasks + settings, the verify skill, an agent/ wiki, .shipkit/reflections), installs the toolchain, wires shipkit (repo.config.ts via init), and runs the whole bootstrap chain: git init, first commit, PRIVATE github repo, push. Loose files ride in that first commit, and only a repo with real history is left as it is. Writes are create-only for files, additive for package.json scripts and .gitignore. CLAUDE.md is the two-line house template on its own (HOUSE_INCLUDE in src/start-repo.ts: the base wiki include, then `@./agent/index.md`, and --include overrides it), applied only on a machine where the house index exists, and the agent route lands last whenever the repo has agent/index.md. The house lines also opt the repo into the .claude/ policy files (a pre-approved shell allowlist + the attribution silencer), so a start with no house widens no agent permissions. Prints the dir on stdout so a shell alias can cd there. cwd-only sibling: `shipkit init`',
|
|
14
13
|
examples: [
|
|
15
14
|
['create or enter ~/dev/pong: dir, package.json, shipkit wiring, git, first commit, private github repo, push, the two-line CLAUDE.md', 'sk start pong'],
|
|
16
15
|
['another house: replace the include lines', "sk start pong --include '@~/wiki/index.md' --include '@./agent/index.md'"],
|
|
17
16
|
['adopt a pre-existing repo from anywhere', 'sk start my-old-project'],
|
|
18
17
|
['force the private github repo for a folder that was already there', 'sk start old-notes --github'],
|
|
19
18
|
['local git and the first commit, never touch github', 'sk start pong --no-github'],
|
|
19
|
+
['give an existing repo its project CLI (a started repo has one)', 'sk start my-old-project --cli'],
|
|
20
20
|
],
|
|
21
21
|
})
|
|
22
22
|
|
|
@@ -27,7 +27,11 @@ export class StartCommand extends Command {
|
|
|
27
27
|
skip = Option.Array('--skip', { description: 'repo names shipkit never touches: no CLAUDE.md include, no wiring. Omitted: the house list (HOUSE_SKIP in src/start-repo.ts)' })
|
|
28
28
|
github = Option.Boolean('--github', { description: 'ON by default when a repo is being STARTED (a dir this run created, or one with a package.json): it gets a PRIVATE github repo wired as origin, and the first commit is pushed to it. Pass --github to force it for a pre-existing folder, --no-github to keep everything local, the first commit included (SHIPKIT_NO_GITHUB=1 does it for every run)' })
|
|
29
29
|
|
|
30
|
+
withCli = Option.Boolean('--cli', false, { description: 'on a repo with history: write the project CLI skeleton (src/cli.ts on the kit, an example family, package.json cli and bin), refused when the repo already has a CLI. A started repo always gets it' })
|
|
31
|
+
|
|
30
32
|
async execute(): Promise<number> {
|
|
31
|
-
|
|
33
|
+
// loaded here, so the scaffold and the git chain cost nothing to every other command
|
|
34
|
+
const { startRepo } = await import('../start-repo.ts')
|
|
35
|
+
return startRepo(this.name, { root: this.root, include: this.include, alsoOk: this.alsoOk, skip: this.skip, github: this.github, cli: this.withCli })
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// sk trailer: a cut's .trailer file → its mp4, filming only the takes whose film line changed (trailer/build.ts).
|
|
2
|
+
// Every module loads inside execute: the cli registers this file on every `sk` call
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
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, 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
|
+
|
|
7
|
+
/** --refilm alone: every stale take; --refilm a,b: those shots */
|
|
8
|
+
const refilmOf = (v: string | boolean | undefined): 'stale' | ReadonlySet<string> | null => (v === undefined || v === false ? null : v === true ? 'stale' : new Set(v.split(',')))
|
|
9
|
+
|
|
10
|
+
async function run(context: { stdout: NodeJS.WritableStream; stderr: NodeJS.WritableStream }, body: (say: (l: string) => void, root: string, cwd: string) => Promise<number>): Promise<number> {
|
|
11
|
+
const say = (l: string): void => void context.stdout.write(`${l}\n`)
|
|
12
|
+
const { Refused } = await import('../trailer/build.ts')
|
|
13
|
+
const { findRepoRoot } = await import('../ts/config.ts')
|
|
14
|
+
const cwd = process.cwd()
|
|
15
|
+
try {
|
|
16
|
+
return await body(say, findRepoRoot(cwd), cwd)
|
|
17
|
+
} catch (e) {
|
|
18
|
+
context.stderr.write(`FAIL: ${e instanceof Refused ? `trailer: ${e.message}` : e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`)
|
|
19
|
+
return 1
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class TrailerCommand extends Command {
|
|
24
|
+
static override paths = [['trailer']]
|
|
25
|
+
|
|
26
|
+
static override usage = Command.Usage({
|
|
27
|
+
category: 'Trailer',
|
|
28
|
+
description: 'build a cut from its .trailer: film what is missing, stitch, strip, montage',
|
|
29
|
+
details: DETAILS,
|
|
30
|
+
examples: [
|
|
31
|
+
['the one cut under trailer/', 'sk trailer'],
|
|
32
|
+
['one cut by its file', 'sk trailer trailer/launch/main.trailer'],
|
|
33
|
+
['film again the takes of two shots', 'sk trailer --refilm intro,menu'],
|
|
34
|
+
['encoded by x264 for a release', 'sk trailer --final'],
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
file = Option.String({ required: false })
|
|
39
|
+
refilm = Option.String('--refilm', { tolerateBoolean: true, description: 'film again: every stale take, or the named shots (a,b)' })
|
|
40
|
+
final = Option.Boolean('--final', false, { description: 'encode with x264 for a release (default: the fast draft encoder)' })
|
|
41
|
+
|
|
42
|
+
async execute(): Promise<number> {
|
|
43
|
+
return run(this.context, async (say, root, cwd) => {
|
|
44
|
+
const b = await import('../trailer/build.ts')
|
|
45
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
46
|
+
if (!b.sayErrors(l, say, cwd)) return 1
|
|
47
|
+
await b.build(l, { refilm: refilmOf(this.refilm), final: this.final, say, cwd })
|
|
48
|
+
return 0
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class TrailerCheckCommand extends Command {
|
|
54
|
+
static override paths = [['trailer', 'check']]
|
|
55
|
+
|
|
56
|
+
static override usage = Command.Usage({
|
|
57
|
+
category: 'Trailer',
|
|
58
|
+
description: 'parse and check a .trailer: every error at its line, the timeline, each take\'s state',
|
|
59
|
+
details: DETAILS,
|
|
60
|
+
examples: [
|
|
61
|
+
['the one cut under trailer/', 'sk trailer check'],
|
|
62
|
+
['one cut by its folder', 'sk trailer check trailer/launch'],
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
file = Option.String({ required: false })
|
|
67
|
+
|
|
68
|
+
async execute(): Promise<number> {
|
|
69
|
+
return run(this.context, async (say, root, cwd) => {
|
|
70
|
+
const t0 = performance.now()
|
|
71
|
+
const b = await import('../trailer/build.ts')
|
|
72
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
73
|
+
if (!b.sayErrors(l, say, cwd)) return 1
|
|
74
|
+
for (const line of b.timeline(l, b.takesOfLoaded(l), await b.worldOf(l), cwd)) say(line)
|
|
75
|
+
say(`ok: checked in ${((performance.now() - t0) / 1000).toFixed(2)} s`)
|
|
76
|
+
return 0
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class TrailerScoreCommand extends Command {
|
|
82
|
+
static override paths = [['trailer', 'score']]
|
|
83
|
+
|
|
84
|
+
static override usage = Command.Usage({
|
|
85
|
+
category: 'Trailer',
|
|
86
|
+
description: 'the score alone, no picture: its wav, its waveform png, each section\'s level and the loudness',
|
|
87
|
+
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`,
|
|
88
|
+
examples: [
|
|
89
|
+
['the score of the one cut', 'sk trailer score'],
|
|
90
|
+
['the score of a named cut', 'sk trailer score trailer/launch'],
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
file = Option.String({ required: false })
|
|
95
|
+
|
|
96
|
+
async execute(): Promise<number> {
|
|
97
|
+
return run(this.context, async (say, root, cwd) => {
|
|
98
|
+
const b = await import('../trailer/build.ts')
|
|
99
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
100
|
+
if (!b.sayErrors(l, say, cwd)) return 1
|
|
101
|
+
await b.auditionScore(l, { refilm: null, final: false, say, cwd })
|
|
102
|
+
return 0
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class TrailerShotCommand extends Command {
|
|
108
|
+
static override paths = [['trailer', 'shot']]
|
|
109
|
+
|
|
110
|
+
static override usage = Command.Usage({
|
|
111
|
+
category: 'Trailer',
|
|
112
|
+
description: 'one shot alone with its titles, no sound, and its nine frames on one png',
|
|
113
|
+
details: DETAILS,
|
|
114
|
+
examples: [
|
|
115
|
+
['a shot of the one cut', 'sk trailer shot intro'],
|
|
116
|
+
['a shot of a named cut, its take filmed again', 'sk trailer shot intro trailer/launch --refilm intro'],
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
name = Option.String({ required: true })
|
|
121
|
+
file = Option.String({ required: false })
|
|
122
|
+
refilm = Option.String('--refilm', { tolerateBoolean: true, description: 'film again: every stale take, or the named shots (a,b)' })
|
|
123
|
+
|
|
124
|
+
async execute(): Promise<number> {
|
|
125
|
+
return run(this.context, async (say, root, cwd) => {
|
|
126
|
+
const b = await import('../trailer/build.ts')
|
|
127
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
128
|
+
if (!b.sayErrors(l, say, cwd)) return 1
|
|
129
|
+
await b.previewShot(l, this.name, { refilm: refilmOf(this.refilm), final: false, say, cwd })
|
|
130
|
+
return 0
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** a cut loaded and checked, its errors said; null when it has one */
|
|
136
|
+
async function loaded(file: string | undefined, say: (l: string) => void, root: string, cwd: string) {
|
|
137
|
+
const b = await import('../trailer/build.ts')
|
|
138
|
+
const l = await b.load(b.trailerFile(file, root, cwd), root)
|
|
139
|
+
return b.sayErrors(l, say, cwd) ? l : null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class TrailerSectionCommand extends Command {
|
|
143
|
+
static override paths = [['trailer', 'section']]
|
|
144
|
+
|
|
145
|
+
static override usage = Command.Usage({
|
|
146
|
+
category: 'Trailer',
|
|
147
|
+
description: 'one section alone with its window of the sound: out/<cut>-<section>.mp4',
|
|
148
|
+
details: `${DETAILS}. \`start\` is the shots before the first section; only a section the cache misses renders`,
|
|
149
|
+
examples: [['a section of the one cut', 'sk trailer section drop']],
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
name = Option.String({ required: true })
|
|
153
|
+
file = Option.String({ required: false })
|
|
154
|
+
final = Option.Boolean('--final', false, { description: 'encode with x264 for a release' })
|
|
155
|
+
|
|
156
|
+
async execute(): Promise<number> {
|
|
157
|
+
return run(this.context, async (say, root, cwd) => {
|
|
158
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
159
|
+
if (!l) return 1
|
|
160
|
+
await (await import('../trailer/build.ts')).previewSection(l, this.name, { refilm: null, final: this.final, say, cwd })
|
|
161
|
+
return 0
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class TrailerLevelsCommand extends Command {
|
|
167
|
+
static override paths = [['trailer', 'levels']]
|
|
168
|
+
|
|
169
|
+
static override usage = Command.Usage({
|
|
170
|
+
category: 'Trailer',
|
|
171
|
+
description: 'each section\'s mean and peak in the built mp4, and its step from the section before',
|
|
172
|
+
details: DETAILS,
|
|
173
|
+
examples: [['the levels of the one cut', 'sk trailer levels']],
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
file = Option.String({ required: false })
|
|
177
|
+
|
|
178
|
+
async execute(): Promise<number> {
|
|
179
|
+
return run(this.context, async (say, root, cwd) => {
|
|
180
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
181
|
+
if (!l) return 1
|
|
182
|
+
await (await import('../trailer/passage.ts')).levels(l, say, cwd)
|
|
183
|
+
return 0
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export class TrailerClipCommand extends Command {
|
|
189
|
+
static override paths = [['trailer', 'clip']]
|
|
190
|
+
|
|
191
|
+
static override usage = Command.Usage({
|
|
192
|
+
category: 'Trailer',
|
|
193
|
+
description: 'a passage of the built cut with its sound, from a section, mark, shot or second',
|
|
194
|
+
details: `${DETAILS}. --version cuts the same passage of a kept version beside it, read from versions/<v>.trailer`,
|
|
195
|
+
examples: [
|
|
196
|
+
['a section', 'sk trailer clip drop'],
|
|
197
|
+
['from a shot to a mark, before and after', 'sk trailer clip hero hit --version v3'],
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
from = Option.String({ required: true })
|
|
202
|
+
rest = Option.Rest({ required: 0 })
|
|
203
|
+
version = Option.String('--version', { description: 'a kept version (v<n>): the same passage of it beside' })
|
|
204
|
+
pad = Option.String('--pad', { description: 'seconds of room before and after' })
|
|
205
|
+
|
|
206
|
+
async execute(): Promise<number> {
|
|
207
|
+
return run(this.context, async (say, root, cwd) => {
|
|
208
|
+
const { existsSync } = await import('node:fs')
|
|
209
|
+
const { resolve } = await import('node:path')
|
|
210
|
+
// the second word is a moment unless it names a file or a folder
|
|
211
|
+
const [a, b] = this.rest
|
|
212
|
+
const isPath = (w: string | undefined): boolean => w !== undefined && (w.endsWith('.trailer') || existsSync(resolve(cwd, w)))
|
|
213
|
+
const to = a !== undefined && !isPath(a) ? a : this.from
|
|
214
|
+
const file = a !== undefined && !isPath(a) ? b : a
|
|
215
|
+
const l = await loaded(file, say, root, cwd)
|
|
216
|
+
if (!l) return 1
|
|
217
|
+
const pad = Number(this.pad ?? 0)
|
|
218
|
+
if (!Number.isFinite(pad) || pad < 0) throw new Error(`--pad wants seconds, got ${this.pad}`)
|
|
219
|
+
await (await import('../trailer/passage.ts')).clip(l, this.from, to, pad, this.version, say, cwd)
|
|
220
|
+
return 0
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class TrailerKeepCommand extends Command {
|
|
226
|
+
static override paths = [['trailer', 'keep']]
|
|
227
|
+
|
|
228
|
+
static override usage = Command.Usage({
|
|
229
|
+
category: 'Trailer',
|
|
230
|
+
description: 'keep the built cut as a version: versions/v<n>.trailer, its video, strip and takes',
|
|
231
|
+
details: `${DETAILS}. Refuses a version already kept; prints the index row, commit and tag left to do`,
|
|
232
|
+
examples: [['keep the one cut', 'sk trailer keep v4']],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
version = Option.String({ required: true })
|
|
236
|
+
file = Option.String({ required: false })
|
|
237
|
+
|
|
238
|
+
async execute(): Promise<number> {
|
|
239
|
+
return run(this.context, async (say, root, cwd) => {
|
|
240
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
241
|
+
if (!l) return 1
|
|
242
|
+
;(await import('../trailer/passage.ts')).keep(l, this.version, say, cwd)
|
|
243
|
+
return 0
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class TrailerFootageCommand extends Command {
|
|
249
|
+
static override paths = [['trailer', 'footage']]
|
|
250
|
+
|
|
251
|
+
static override usage = Command.Usage({
|
|
252
|
+
category: 'Trailer',
|
|
253
|
+
description: 'the take file of each shot or footage block, the raw take before the stitch trims it',
|
|
254
|
+
details: DETAILS,
|
|
255
|
+
examples: [['two shots of the one cut', 'sk trailer footage intro,hero']],
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
names = Option.String({ required: true })
|
|
259
|
+
file = Option.String({ required: false })
|
|
260
|
+
|
|
261
|
+
async execute(): Promise<number> {
|
|
262
|
+
return run(this.context, async (say, root, cwd) => {
|
|
263
|
+
const l = await loaded(this.file, say, root, cwd)
|
|
264
|
+
if (!l) return 1
|
|
265
|
+
;(await import('../trailer/passage.ts')).footage(l, this.names.split(','), say, cwd)
|
|
266
|
+
return 0
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class TrailerWatchCommand extends Command {
|
|
272
|
+
static override paths = [['trailer', 'watch']]
|
|
273
|
+
|
|
274
|
+
static override usage = Command.Usage({
|
|
275
|
+
category: 'Trailer',
|
|
276
|
+
description: 'build again after each save in the cut\'s folder (ctrl-c stops)',
|
|
277
|
+
details: DETAILS,
|
|
278
|
+
examples: [['the one cut', 'sk trailer watch']],
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
file = Option.String({ required: false })
|
|
282
|
+
final = Option.Boolean('--final', false, { description: 'encode with x264 for a release' })
|
|
283
|
+
|
|
284
|
+
async execute(): Promise<number> {
|
|
285
|
+
return run(this.context, async (say, root, cwd) => {
|
|
286
|
+
const b = await import('../trailer/build.ts')
|
|
287
|
+
const { watch } = await import('node:fs')
|
|
288
|
+
const { dirname, relative } = await import('node:path')
|
|
289
|
+
const file = b.trailerFile(this.file, root, cwd)
|
|
290
|
+
let busy = false
|
|
291
|
+
let again = false
|
|
292
|
+
const once = async (): Promise<void> => {
|
|
293
|
+
if (busy) {
|
|
294
|
+
again = true
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
busy = true
|
|
298
|
+
try {
|
|
299
|
+
const l = await b.load(file, root, { fresh: true })
|
|
300
|
+
if (b.sayErrors(l, say, cwd)) await b.build(l, { refilm: null, final: this.final, say, cwd })
|
|
301
|
+
} catch (e) {
|
|
302
|
+
say(`FAIL: ${e instanceof Error ? e.message : String(e)}`)
|
|
303
|
+
}
|
|
304
|
+
busy = false
|
|
305
|
+
if (again) {
|
|
306
|
+
again = false
|
|
307
|
+
await once()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
await once()
|
|
311
|
+
say(`watching ${relative(cwd, file)} and its folder's recipes (ctrl-c stops)`)
|
|
312
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
313
|
+
watch(dirname(file), (_, f) => {
|
|
314
|
+
if (!f || f.startsWith('out') || f.startsWith('versions') || f.endsWith('.md')) return
|
|
315
|
+
if (timer) clearTimeout(timer)
|
|
316
|
+
timer = setTimeout(() => void once(), 200)
|
|
317
|
+
})
|
|
318
|
+
await new Promise(() => undefined)
|
|
319
|
+
return 0
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export class TrailerPruneCommand extends Command {
|
|
325
|
+
static override paths = [['trailer', 'prune']]
|
|
326
|
+
|
|
327
|
+
static override usage = Command.Usage({
|
|
328
|
+
category: 'Trailer',
|
|
329
|
+
description: 'trim the trailer cache to what the cuts use, what each folder frees',
|
|
330
|
+
details: `${DETAILS}. Every build, section and shot runs it after its stitch; a file younger than an hour stays`,
|
|
331
|
+
examples: [
|
|
332
|
+
['what it would free', 'sk trailer prune --dry'],
|
|
333
|
+
['free it', 'sk trailer prune'],
|
|
334
|
+
],
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
dry = Option.Boolean('--dry', false, { description: 'say what each folder would free, delete nothing' })
|
|
338
|
+
|
|
339
|
+
async execute(): Promise<number> {
|
|
340
|
+
return run(this.context, async (say, root) => {
|
|
341
|
+
const { prune, sayPruned } = await import('../trailer/prune.ts')
|
|
342
|
+
for (const line of sayPruned(await prune(root, { dry: this.dry }), this.dry)) say(line)
|
|
343
|
+
return 0
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** every `sk trailer` verb, registered by cli.ts */
|
|
349
|
+
export const TRAILER_COMMANDS = [
|
|
350
|
+
TrailerCommand,
|
|
351
|
+
TrailerCheckCommand,
|
|
352
|
+
TrailerScoreCommand,
|
|
353
|
+
TrailerShotCommand,
|
|
354
|
+
TrailerSectionCommand,
|
|
355
|
+
TrailerLevelsCommand,
|
|
356
|
+
TrailerClipCommand,
|
|
357
|
+
TrailerKeepCommand,
|
|
358
|
+
TrailerFootageCommand,
|
|
359
|
+
TrailerWatchCommand,
|
|
360
|
+
TrailerPruneCommand,
|
|
361
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// the commands every project CLI gets for free: its docs pages and its call
|
|
2
|
+
// log. They are shipkit's own `commands` and `calls` classes, given the
|
|
3
|
+
// project's name in their examples and the project's root for the log.
|
|
4
|
+
import { Command } from 'clipanion'
|
|
5
|
+
import { CallsCommand } from '../cmd_Calls.ts'
|
|
6
|
+
import { CommandsCommand } from '../cmd_Commands.ts'
|
|
7
|
+
import { callsFile } from './calls.ts'
|
|
8
|
+
import type { CommandClass, KitContext } from './verb.ts'
|
|
9
|
+
|
|
10
|
+
export function builtinCommands(bin: string): { docs: CommandClass[]; calls: CommandClass[] } {
|
|
11
|
+
class ProjectCommands extends CommandsCommand {
|
|
12
|
+
static override usage = Command.Usage({
|
|
13
|
+
category: 'Docs',
|
|
14
|
+
description: `every ${bin} command as markdown or json; --out writes the commands page`,
|
|
15
|
+
details: `the registry described by itself. --out rewrites the block between the ${bin}:commands markers, --essentials rewrites the everyday page whole, --check fails while a page lags the registry or a row lacks its category, description or example. --json is how another tool reads this registry.`,
|
|
16
|
+
examples: [
|
|
17
|
+
['the markdown', `${bin} commands`],
|
|
18
|
+
['as json, for another tool', `${bin} commands --json`],
|
|
19
|
+
['write both agent pages', `${bin} commands --out agent/${bin}.md --essentials agent/${bin}-essentials.md`],
|
|
20
|
+
['fail when a page is stale', `${bin} commands --out agent/${bin}.md --essentials agent/${bin}-essentials.md --check`],
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
static override shapeRule = true
|
|
24
|
+
}
|
|
25
|
+
class ProjectCalls extends CallsCommand {
|
|
26
|
+
static override usage = Command.Usage({
|
|
27
|
+
category: 'Docs',
|
|
28
|
+
description: `every ${bin} run logged with its time: per command how often, how long, the slowest`,
|
|
29
|
+
details: `reads .shipkit/logs/${bin}-calls.jsonl under the repo root. A run with no end line was killed or still runs. ${bin.toUpperCase()}_LOG=off writes nothing.`,
|
|
30
|
+
examples: [
|
|
31
|
+
['what agents waited on today', `${bin} calls --since 1d`],
|
|
32
|
+
['one family', `${bin} calls --cmd <family>`],
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
override logFile(): string {
|
|
36
|
+
return callsFile((this.context as unknown as KitContext).root, bin)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { docs: [ProjectCommands as unknown as CommandClass], calls: [ProjectCalls as unknown as CommandClass] }
|
|
40
|
+
}
|