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,177 @@
|
|
|
1
|
+
// every run of a CLI, logged: a start line at launch, an end line at any exit
|
|
2
|
+
// with its time, CPU and memory, the same id on both. A start with no end was
|
|
3
|
+
// killed or still runs. JSON lines, one append each, so runs at once never mix.
|
|
4
|
+
// `<cli> calls` reads it back, so a speed pass starts from what agents waited on.
|
|
5
|
+
// `<CLI>_LOG=off` writes nothing, and neither does a run under `bun test`. Past
|
|
6
|
+
// 2 MB the log moves to `<file>.1` and starts again, so it never grows unbounded.
|
|
7
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from 'fs'
|
|
8
|
+
import { dirname, join } from 'path'
|
|
9
|
+
import { LAYOUT } from '../../core/layout.ts'
|
|
10
|
+
|
|
11
|
+
export type CallStart = {
|
|
12
|
+
id: string
|
|
13
|
+
at: string
|
|
14
|
+
argv: string[]
|
|
15
|
+
cwd: string
|
|
16
|
+
pid: number
|
|
17
|
+
/** `<CLI>_AGENT`, else null: who ran it */
|
|
18
|
+
agent: string | null
|
|
19
|
+
/** the run of the same CLI that spawned this one, null for a run of its own */
|
|
20
|
+
parent: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CallEnd = {
|
|
24
|
+
id: string
|
|
25
|
+
/** the command's path, null when none matched (a listing, a typo) */
|
|
26
|
+
cmd: string | null
|
|
27
|
+
/** from the process start */
|
|
28
|
+
ms: number
|
|
29
|
+
/** building the registry: the import of the families the call needed */
|
|
30
|
+
loadMs: number
|
|
31
|
+
cpuMs: number
|
|
32
|
+
rssMb: number
|
|
33
|
+
code: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** where a CLI's calls go: `<root>/.shipkit/logs/<bin>-calls.jsonl` */
|
|
37
|
+
export const callsFile = (root: string, bin: string): string => join(root, LAYOUT.logs, `${bin}-calls.jsonl`)
|
|
38
|
+
|
|
39
|
+
/** the env prefix of a CLI: `my-tool` → `MY_TOOL` */
|
|
40
|
+
export const envPrefix = (bin: string): string => bin.toUpperCase().replace(/[^A-Z0-9]/g, '_')
|
|
41
|
+
|
|
42
|
+
/** past this size the log moves to `<file>.1` (replacing the older one) and starts again */
|
|
43
|
+
export const CALLS_MAX_BYTES = 2 * 1024 * 1024
|
|
44
|
+
|
|
45
|
+
/** a long argument (a pasted text) is cut, so a line stays one append */
|
|
46
|
+
const ARG_MAX = 300
|
|
47
|
+
|
|
48
|
+
/** logs this run's start, and its end on any exit. The caller names the
|
|
49
|
+
* command and the registry load time once it knows them */
|
|
50
|
+
export function logCall(file: string, bin: string, argv: readonly string[], env: NodeJS.ProcessEnv = process.env): { matched(cmd: string | null, loadMs: number): void } {
|
|
51
|
+
const pre = envPrefix(bin)
|
|
52
|
+
if (env[`${pre}_LOG`] === 'off' || env.NODE_ENV === 'test') return { matched: () => {} }
|
|
53
|
+
let warned = false
|
|
54
|
+
try { if (statSync(file).size > CALLS_MAX_BYTES) renameSync(file, `${file}.1`) } catch { /* no log yet */ }
|
|
55
|
+
const append = (line: object): void => {
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
58
|
+
appendFileSync(file, `${JSON.stringify(line)}\n`)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (!warned) console.error(`WARN ${bin}: the call log could not be written (${file}): ${(e as Error).message}`)
|
|
61
|
+
warned = true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const id = `${Date.now().toString(36)}-${process.pid}`
|
|
65
|
+
const end: Pick<CallEnd, 'cmd' | 'loadMs'> = { cmd: null, loadMs: 0 }
|
|
66
|
+
const start: CallStart = {
|
|
67
|
+
id,
|
|
68
|
+
at: new Date().toISOString(),
|
|
69
|
+
argv: argv.map((a) => (a.length > ARG_MAX ? `${a.slice(0, ARG_MAX)}…` : a)),
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
pid: process.pid,
|
|
72
|
+
agent: env[`${pre}_AGENT`]?.trim() || null,
|
|
73
|
+
parent: env[`${pre}_CALL`] ?? null,
|
|
74
|
+
}
|
|
75
|
+
append(start)
|
|
76
|
+
env[`${pre}_CALL`] = id
|
|
77
|
+
process.on('exit', (code) => {
|
|
78
|
+
const cpu = process.cpuUsage()
|
|
79
|
+
append({ id, ...end, ms: Math.round(performance.now()), cpuMs: Math.round((cpu.user + cpu.system) / 1000), rssMb: Math.round(process.resourceUsage().maxRSS / 1024), code } satisfies CallEnd)
|
|
80
|
+
})
|
|
81
|
+
return { matched: (cmd, loadMs) => { end.cmd = cmd; end.loadMs = Math.round(loadMs) } }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type CallsQuery = {
|
|
85
|
+
/** epoch ms: only the runs that started after */
|
|
86
|
+
since?: number
|
|
87
|
+
/** a command path prefix */
|
|
88
|
+
cmd?: string
|
|
89
|
+
/** a folder: only the runs started inside it */
|
|
90
|
+
under?: string
|
|
91
|
+
/** keep the runs another run of the same CLI spawned */
|
|
92
|
+
nested?: boolean
|
|
93
|
+
/** how many slowest runs to list */
|
|
94
|
+
top?: number
|
|
95
|
+
/** the id of the run asking, which has no end yet */
|
|
96
|
+
self?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type CallsRow = { cmd: string; runs: number; noEnd: number; failed: number; p50: number; p95: number; max: number; totalS: number; loadP50: number; rssMax: number }
|
|
100
|
+
export type CallsReport = { runs: number; rows: CallsRow[]; slowest: { ms: number; at: string; agent: string | null; argv: string }[] }
|
|
101
|
+
|
|
102
|
+
/** `30m`, `2h`, `3d` or a date, as epoch ms; null when unreadable */
|
|
103
|
+
export function sinceOf(s: string, now: number): number | null {
|
|
104
|
+
const m = /^(\d+)([mhd])$/.exec(s)
|
|
105
|
+
if (m) return now - Number(m[1]) * { m: 60e3, h: 3600e3, d: 86400e3 }[m[2] as 'm' | 'h' | 'd']
|
|
106
|
+
const t = Date.parse(s)
|
|
107
|
+
return Number.isNaN(t) ? null : t
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const quantile = (xs: number[], p: number): number => {
|
|
111
|
+
if (xs.length === 0) return 0
|
|
112
|
+
const s = [...xs].sort((a, b) => a - b)
|
|
113
|
+
return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** the call log read back: per command how often, how long, how heavy, and the slowest runs */
|
|
117
|
+
export function callsReport(text: string, q: CallsQuery = {}): CallsReport {
|
|
118
|
+
const starts = new Map<string, CallStart>()
|
|
119
|
+
const ends = new Map<string, CallEnd>()
|
|
120
|
+
for (const line of text.split('\n')) {
|
|
121
|
+
if (line.trim() === '') continue
|
|
122
|
+
let r: CallStart | CallEnd
|
|
123
|
+
try { r = JSON.parse(line) as CallStart | CallEnd } catch { continue }
|
|
124
|
+
if ('at' in r) starts.set(r.id, r)
|
|
125
|
+
else ends.set(r.id, r)
|
|
126
|
+
}
|
|
127
|
+
type Run = { s: CallStart; e: CallEnd | null; cmd: string }
|
|
128
|
+
const runs: Run[] = []
|
|
129
|
+
for (const s of starts.values()) {
|
|
130
|
+
if (s.id === q.self || Date.parse(s.at) < (q.since ?? 0) || (q.nested !== true && s.parent != null)) continue
|
|
131
|
+
if (q.under != null && s.cwd !== q.under && !s.cwd.startsWith(`${q.under}/`)) continue
|
|
132
|
+
const e = ends.get(s.id) ?? null
|
|
133
|
+
const flag = s.argv.findIndex((a) => a.startsWith('-'))
|
|
134
|
+
const words = flag === -1 ? s.argv : s.argv.slice(0, flag)
|
|
135
|
+
const cmd = e?.cmd ?? (words.slice(0, 2).join(' ') || '(none)')
|
|
136
|
+
if (q.cmd != null && !cmd.startsWith(q.cmd)) continue
|
|
137
|
+
runs.push({ s, e, cmd })
|
|
138
|
+
}
|
|
139
|
+
const byCmd = new Map<string, Run[]>()
|
|
140
|
+
for (const r of runs) byCmd.set(r.cmd, [...(byCmd.get(r.cmd) ?? []), r])
|
|
141
|
+
const rows = [...byCmd].map(([cmd, rs]): CallsRow => {
|
|
142
|
+
const done = rs.flatMap((r) => (r.e ? [r.e] : []))
|
|
143
|
+
const ms = done.map((e) => e.ms)
|
|
144
|
+
return {
|
|
145
|
+
cmd,
|
|
146
|
+
runs: rs.length,
|
|
147
|
+
noEnd: rs.length - done.length,
|
|
148
|
+
failed: done.filter((e) => e.code !== 0).length,
|
|
149
|
+
p50: quantile(ms, 0.5),
|
|
150
|
+
p95: quantile(ms, 0.95),
|
|
151
|
+
max: Math.max(0, ...ms),
|
|
152
|
+
totalS: Math.round(ms.reduce((a, b) => a + b, 0) / 1000),
|
|
153
|
+
loadP50: quantile(done.map((e) => e.loadMs), 0.5),
|
|
154
|
+
rssMax: Math.max(0, ...done.map((e) => e.rssMb)),
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
rows.sort((a, b) => b.totalS - a.totalS || b.p95 - a.p95 || a.cmd.localeCompare(b.cmd))
|
|
158
|
+
const slowest = runs
|
|
159
|
+
.flatMap((r) => (r.e ? [{ ms: r.e.ms, at: r.s.at, agent: r.s.agent, argv: r.s.argv.join(' ') }] : []))
|
|
160
|
+
.sort((a, b) => b.ms - a.ms)
|
|
161
|
+
.slice(0, q.top ?? 10)
|
|
162
|
+
return { runs: runs.length, rows, slowest }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** the report as a table, then the slowest runs */
|
|
166
|
+
export function renderCallsReport(bin: string, r: CallsReport, file: string): string {
|
|
167
|
+
const head = ['command', 'runs', 'no end', 'failed', 'p50 ms', 'p95 ms', 'max ms', 'total s', 'load p50', 'rss max MB']
|
|
168
|
+
const table = [head, ...r.rows.map((x) => [x.cmd, x.runs, x.noEnd, x.failed, x.p50, x.p95, x.max, x.totalS, x.loadP50, x.rssMax].map(String))]
|
|
169
|
+
const widths = head.map((_, i) => Math.max(...table.map((t) => (t[i] ?? '').length)))
|
|
170
|
+
const out = [`${r.runs} runs · ${file}`, 'a run with no end line was killed or still runs; time is from the process start, load is building the registry', '']
|
|
171
|
+
for (const t of table) out.push(t.map((c, i) => (i === 0 ? c.padEnd(widths[i] ?? 0) : c.padStart(widths[i] ?? 0))).join(' '))
|
|
172
|
+
if (r.slowest.length > 0) {
|
|
173
|
+
out.push('', `the ${r.slowest.length} slowest runs`)
|
|
174
|
+
for (const s of r.slowest) out.push(`${String(s.ms).padStart(8)} ms ${s.at.slice(0, 19)} ${s.agent ?? '-'} ${bin} ${s.argv}`)
|
|
175
|
+
}
|
|
176
|
+
return `${out.join('\n')}\n`
|
|
177
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// a project CLI is a table of families: the first word of a call loads the
|
|
2
|
+
// family that owns it and nothing else, a word nobody owns (a listing, a typo)
|
|
3
|
+
// loads them all. A family file exports `commands` and imports nothing heavy
|
|
4
|
+
// at its top, so even the full load answers in milliseconds.
|
|
5
|
+
import type { CommandClass } from './verb.ts'
|
|
6
|
+
|
|
7
|
+
export type Family = {
|
|
8
|
+
/** the first words its commands start with */
|
|
9
|
+
roots: string[]
|
|
10
|
+
load: () => Promise<{ commands: CommandClass[] }>
|
|
11
|
+
/** its commands read the whole registry (the docs): every family loads with it */
|
|
12
|
+
all?: true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** the families a call needs: the one owning its first word, else every one */
|
|
16
|
+
export function familiesFor<F extends Family>(argv: readonly string[], families: readonly F[]): F[] {
|
|
17
|
+
const own = families.find((f) => f.roots.includes(argv[0] ?? ''))
|
|
18
|
+
return own != null && own.all !== true ? [own] : [...families]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** the first words two families both claim: a call would load the wrong one */
|
|
22
|
+
export function rootClashes(families: readonly Family[]): string[] {
|
|
23
|
+
const seen = new Set<string>()
|
|
24
|
+
const out = new Set<string>()
|
|
25
|
+
for (const f of families) for (const r of f.roots) (seen.has(r) ? out : seen).add(r)
|
|
26
|
+
return [...out]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** what clipanion tells of a registered command */
|
|
30
|
+
export type ListedDefinition = { path: string; category?: string | null; description?: string | null }
|
|
31
|
+
|
|
32
|
+
const HELP_WORDS = ['--help', '-h', 'help']
|
|
33
|
+
|
|
34
|
+
/** a definition's path without the binary name clipanion puts in front */
|
|
35
|
+
export const stripBin = (bin: string, path: string): string => (path.startsWith(`${bin} `) ? path.slice(bin.length + 1) : path)
|
|
36
|
+
|
|
37
|
+
/** the family word a call asks a listing of: '' for every command, a first
|
|
38
|
+
* word for its family, null to run the call. A word that is itself a command runs */
|
|
39
|
+
export function listingOf(bin: string, argv: readonly string[], defs: readonly ListedDefinition[]): string | null {
|
|
40
|
+
if (argv.length === 0 || (argv.length === 1 && HELP_WORDS.includes(argv[0]!))) return ''
|
|
41
|
+
if (argv.length > 2 || (argv.length === 2 && !HELP_WORDS.includes(argv[1]!))) return null
|
|
42
|
+
const word = argv[0]!
|
|
43
|
+
const paths = defs.map((d) => stripBin(bin, d.path))
|
|
44
|
+
if (paths.includes(word) || !paths.some((p) => p.startsWith(`${word} `))) return null
|
|
45
|
+
return word
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** every command of a family (all when ''), one line each, grouped by category */
|
|
49
|
+
export function listing(bin: string, defs: readonly ListedDefinition[], word: string, label?: string): string {
|
|
50
|
+
const rows = defs
|
|
51
|
+
.map((d) => ({ path: stripBin(bin, d.path), category: (d.category ?? '').trim() || 'Other', description: (d.description ?? '').trim() }))
|
|
52
|
+
.filter((r) => !r.path.startsWith('-') && (word === '' || r.path === word || r.path.startsWith(`${word} `)))
|
|
53
|
+
.sort((a, b) => a.category.localeCompare(b.category) || a.path.localeCompare(b.path))
|
|
54
|
+
const width = Math.max(0, ...rows.map((r) => r.path.length)) + bin.length + 3
|
|
55
|
+
const out = word === '' && label != null ? [label, ''] : []
|
|
56
|
+
let category = ''
|
|
57
|
+
for (const r of rows) {
|
|
58
|
+
if (r.category !== category) {
|
|
59
|
+
if (category !== '') out.push('')
|
|
60
|
+
out.push(r.category)
|
|
61
|
+
category = r.category
|
|
62
|
+
}
|
|
63
|
+
out.push(` ${`${bin} ${r.path}`.padEnd(width)}${r.description}`)
|
|
64
|
+
}
|
|
65
|
+
out.push('', `${bin} <command> --help: its usage, its examples and its flags`)
|
|
66
|
+
return `${out.join('\n')}\n`
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// the project CLI kit: what a repo's `src/cli.ts` and its family files import
|
|
2
|
+
// (`rvlib-shipkit/cli-kit`). Command and Option come from here, so a project
|
|
3
|
+
// needs no clipanion of its own and its classes match the kit's.
|
|
4
|
+
export { Command, Option } from 'clipanion'
|
|
5
|
+
export { type Family, familiesFor, listing, listingOf } from './families.ts'
|
|
6
|
+
export { callsFile, logCall } from './calls.ts'
|
|
7
|
+
export { type ProjectCliSpec, projectCli, runProjectCli } from './project-cli.ts'
|
|
8
|
+
export { argvOf, type AskTool, type CommandClass, type Essential, type Film, filmCall, type KitContext, type Run, type Verb, verbs } from './verb.ts'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// the body of a project CLI's pages check (the file `sk start` writes in
|
|
2
|
+
// src/cli/): the CLI's `commands --check` says whether both agent pages match
|
|
3
|
+
// the registry, and the repair writes them. The check file itself declares
|
|
4
|
+
// where it runs, so the policy stays readable in the repo.
|
|
5
|
+
import type { FixResult } from '../../check.ts'
|
|
6
|
+
import type { CheckCtx } from '../../core/check-ctx.ts'
|
|
7
|
+
|
|
8
|
+
/** the first FAIL line of a run, else what it printed on one line */
|
|
9
|
+
const said = (out: string): string => {
|
|
10
|
+
const lines = out.trim().split('\n')
|
|
11
|
+
return lines.find((l) => l.startsWith('FAIL')) ?? lines.join('; ')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function cliPagesCheck(name: string, entry = 'src/cli.ts'): {
|
|
15
|
+
description: string
|
|
16
|
+
inputs: string[]
|
|
17
|
+
run: (c: CheckCtx) => Promise<void>
|
|
18
|
+
repair: (c: CheckCtx) => Promise<FixResult>
|
|
19
|
+
} {
|
|
20
|
+
const all = `agent/${name}.md`
|
|
21
|
+
const essentials = `agent/${name}-essentials.md`
|
|
22
|
+
const gen = ['bun', entry, 'commands', '--out', all, '--essentials', essentials]
|
|
23
|
+
return {
|
|
24
|
+
description: `the ${name} command pages match its registry`,
|
|
25
|
+
inputs: [entry, entry.replace(/\.ts$/, ''), all, essentials],
|
|
26
|
+
run: async (c) => {
|
|
27
|
+
const r = await c.exec([...gen, '--check'], { cwd: c.root, env: { [`${name.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_LOG`]: 'off' } })
|
|
28
|
+
if (r.code === 0) c.ok(said(r.out))
|
|
29
|
+
else c.fail(said(r.out), `${name} ${gen.slice(2).join(' ')}`)
|
|
30
|
+
},
|
|
31
|
+
repair: async (c) => {
|
|
32
|
+
const r = await c.exec(gen, { cwd: c.root })
|
|
33
|
+
return { changed: r.code === 0 && r.out.includes('rewritten'), summary: said(r.out) }
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// a project's own CLI in one call: `runProjectCli({ name, root, families })`.
|
|
2
|
+
// It loads only the family the first word needs, lists one line per command
|
|
3
|
+
// when asked for a listing, logs every run, and adds `commands` (the agent
|
|
4
|
+
// pages, generated) and `calls` (the log, read back). The bar: the bare
|
|
5
|
+
// listing answers in well under a second.
|
|
6
|
+
import { type BaseContext, Builtins, Cli } from 'clipanion'
|
|
7
|
+
import { callsFile, logCall } from './calls.ts'
|
|
8
|
+
import { type Family, familiesFor, listing, listingOf, rootClashes } from './families.ts'
|
|
9
|
+
import type { AskTool, CommandClass, KitContext } from './verb.ts'
|
|
10
|
+
|
|
11
|
+
export type ProjectCliSpec = {
|
|
12
|
+
/** the binary name: the listing, the examples, the log file and its env prefix use it */
|
|
13
|
+
name: string
|
|
14
|
+
/** the repo root: rows run from it, the call log lives under it */
|
|
15
|
+
root: string
|
|
16
|
+
families: Family[]
|
|
17
|
+
/** one line above the bare listing */
|
|
18
|
+
label?: string
|
|
19
|
+
version?: string
|
|
20
|
+
/** the warm path of `{ tool }` rows */
|
|
21
|
+
askTool?: AskTool
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** the kit's own families, after the project's */
|
|
25
|
+
function kitFamilies(bin: string): Family[] {
|
|
26
|
+
const builtins = async () => (await import('./builtins.ts')).builtinCommands(bin)
|
|
27
|
+
return [
|
|
28
|
+
{ roots: ['commands'], load: async () => ({ commands: (await builtins()).docs }), all: true },
|
|
29
|
+
{ roots: ['calls'], load: async () => ({ commands: (await builtins()).calls }) },
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** what the kit adds to clipanion's context */
|
|
34
|
+
export type KitExtra = Omit<KitContext, keyof BaseContext>
|
|
35
|
+
|
|
36
|
+
/** the cli with the families `argv` needs, and the context its commands run in */
|
|
37
|
+
export async function projectCli(spec: ProjectCliSpec, argv: readonly string[], cwd = process.cwd()): Promise<{ cli: Cli<KitContext>; context: KitExtra }> {
|
|
38
|
+
const families = [...spec.families, ...kitFamilies(spec.name)]
|
|
39
|
+
const clash = rootClashes(families)
|
|
40
|
+
if (clash.length > 0) throw new Error(`${spec.name}: two families claim the first word ${clash.map((w) => `'${w}'`).join(', ')}, give each word one family`)
|
|
41
|
+
const cli = new Cli<KitContext>({ binaryLabel: spec.label ?? spec.name, binaryName: spec.name, binaryVersion: spec.version, enableCapture: true, enableColors: process.stdout.isTTY === true && (process.env.NO_COLOR ?? '') === '' })
|
|
42
|
+
cli.register(Builtins.HelpCommand)
|
|
43
|
+
if (spec.version != null) cli.register(Builtins.VersionCommand)
|
|
44
|
+
const need = familiesFor(argv, families)
|
|
45
|
+
const classes: CommandClass[] = []
|
|
46
|
+
for (const mod of await Promise.all(need.map((f) => f.load()))) for (const c of mod.commands) { cli.register(c); classes.push(c) }
|
|
47
|
+
return { cli, context: { root: spec.root, cwd, classes, askTool: spec.askTool } }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** the path of the command a call runs, null when none matches */
|
|
51
|
+
function commandOf(cli: Cli<KitContext>, argv: string[], context: KitExtra): string | null {
|
|
52
|
+
try {
|
|
53
|
+
const path = (cli.process(argv, context).constructor as { paths?: string[][] }).paths?.[0]?.join(' ') ?? null
|
|
54
|
+
return path?.startsWith('-') === true ? 'help' : path
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const closedPipe = (e: unknown): boolean => e instanceof Error && 'code' in e && e.code === 'EPIPE'
|
|
61
|
+
|
|
62
|
+
/** the whole entry of a project CLI: log, load, then list or run */
|
|
63
|
+
export async function runProjectCli(spec: ProjectCliSpec, argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
64
|
+
// a reader that closes early (`<cli> | head`) ends the run quietly
|
|
65
|
+
process.stdout.on('error', (e) => (closedPipe(e) ? process.exit(0) : undefined))
|
|
66
|
+
const call = logCall(callsFile(spec.root, spec.name), spec.name, argv)
|
|
67
|
+
const t0 = performance.now()
|
|
68
|
+
const { cli, context } = await projectCli(spec, argv)
|
|
69
|
+
call.matched(commandOf(cli, argv, context), performance.now() - t0)
|
|
70
|
+
const word = listingOf(spec.name, argv, cli.definitions())
|
|
71
|
+
if (word !== null) { process.stdout.write(listing(spec.name, cli.definitions(), word, spec.label)); return }
|
|
72
|
+
await cli.runExit(argv, context)
|
|
73
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// a project CLI command over code that already exists is a ROW, never a second
|
|
2
|
+
// implementation: `verbs([...])` turns rows into clipanion commands that spawn
|
|
3
|
+
// the script, ask the warm tool, or run the command line, from the repo root.
|
|
4
|
+
// A family file imports this module and nothing heavy at its top: the bare
|
|
5
|
+
// listing loads every family.
|
|
6
|
+
import { type BaseContext, Command, type CommandClass as ClipanionClass, Option } from 'clipanion'
|
|
7
|
+
import { existsSync } from 'fs'
|
|
8
|
+
import { isAbsolute, resolve } from 'path'
|
|
9
|
+
|
|
10
|
+
/** what a kit command sees in `this.context`, beside clipanion's streams */
|
|
11
|
+
export type KitContext = BaseContext & {
|
|
12
|
+
/** the repo root: every row runs from it */
|
|
13
|
+
root: string
|
|
14
|
+
/** where the caller stood: a filmable row's `--out` resolves against it */
|
|
15
|
+
cwd: string
|
|
16
|
+
/** every command class registered in this process (the docs read their marks) */
|
|
17
|
+
classes: CommandClass[]
|
|
18
|
+
/** the warm path of a `{ tool }` row, when the project has a warm host */
|
|
19
|
+
askTool?: AskTool
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CommandClass = ClipanionClass<KitContext>
|
|
23
|
+
|
|
24
|
+
/** asks a warm process for a tool's answer; null = no warm host, run the cold script */
|
|
25
|
+
export type AskTool = (tool: string, args: string[]) => Promise<number | null>
|
|
26
|
+
|
|
27
|
+
/** how a row runs */
|
|
28
|
+
export type Run =
|
|
29
|
+
/** `bun <script> ...pre ...args` */
|
|
30
|
+
| { bun: string; pre?: string[] }
|
|
31
|
+
/** the warm host's answer when it runs (`askTool`), else `bun <cold> ...pre ...args` */
|
|
32
|
+
| { tool: string; cold: string; pre?: string[] }
|
|
33
|
+
/** any command line, the args appended */
|
|
34
|
+
| { cmd: string[] }
|
|
35
|
+
|
|
36
|
+
/** a row that shoots a state: `still` writes one image at `--out`, `clip` a video, its length `--seconds` */
|
|
37
|
+
export type Film = 'still' | 'clip'
|
|
38
|
+
|
|
39
|
+
/** an everyday command, listed on the essentials page every session loads */
|
|
40
|
+
export type Essential = {
|
|
41
|
+
/** its arguments as an agent types them: '<x> <y>' */
|
|
42
|
+
args?: string
|
|
43
|
+
/** how many of its examples the page shows under its line */
|
|
44
|
+
examples?: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Verb = {
|
|
48
|
+
/** the words after the binary: 'world at' */
|
|
49
|
+
path: string
|
|
50
|
+
/** the group the listing prints it under */
|
|
51
|
+
category: string
|
|
52
|
+
/** one line, under 100 characters */
|
|
53
|
+
description: string
|
|
54
|
+
/** the longer text `--help` prints */
|
|
55
|
+
details?: string
|
|
56
|
+
/** [label, command line], each line starting with the binary and the row's words */
|
|
57
|
+
examples: [string, string][]
|
|
58
|
+
run: Run
|
|
59
|
+
/** 'own': `--help` also runs the tool with `--help`, which prints its flags and exits 0 */
|
|
60
|
+
help?: 'own'
|
|
61
|
+
essential?: Essential
|
|
62
|
+
/** filmable: takes `--out <path>`, a clip also `--seconds <n>` (a trailer `film` line calls it) */
|
|
63
|
+
film?: Film
|
|
64
|
+
/** with film: the row's take version, a bump films every trailer take of the row again */
|
|
65
|
+
version?: number | string
|
|
66
|
+
/** with film: `own`, a clip that films as long as it plays (no `--seconds`) */
|
|
67
|
+
length?: 'own'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** the argv a row runs */
|
|
71
|
+
export function argvOf(run: Run, args: readonly string[]): string[] {
|
|
72
|
+
if ('bun' in run) return ['bun', run.bun, ...(run.pre ?? []), ...args]
|
|
73
|
+
if ('tool' in run) return ['bun', run.cold, ...(run.pre ?? []), ...args]
|
|
74
|
+
return [...run.cmd, ...args]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** what a row runs, as the commands page prints it */
|
|
78
|
+
export function runText(run: Run): string {
|
|
79
|
+
if ('bun' in run) return [run.bun, ...(run.pre ?? [])].join(' ')
|
|
80
|
+
if ('tool' in run) return `tool ${run.tool} (cold: ${[run.cold, ...(run.pre ?? [])].join(' ')})`
|
|
81
|
+
return run.cmd.join(' ')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** the value after `flag` in args (`--out x` or `--out=x`), with its index */
|
|
85
|
+
function flagAt(args: readonly string[], flag: string): { i: number; value: string; joined: boolean } | null {
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const a = args[i]!
|
|
88
|
+
if (a === flag && i + 1 < args.length) return { i, value: args[i + 1]!, joined: false }
|
|
89
|
+
if (a.startsWith(`${flag}=`)) return { i, value: a.slice(flag.length + 1), joined: true }
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** the film contract on one call: a usage error, or the args with `--out`
|
|
95
|
+
* made absolute against the caller's cwd and the file the run must leave */
|
|
96
|
+
export function filmCall(film: Film | undefined, args: readonly string[], cwd: string): { error: string } | { args: string[]; out: string | null } {
|
|
97
|
+
if (film == null) return { args: [...args], out: null }
|
|
98
|
+
if (film === 'still' && flagAt(args, '--seconds') != null) return { error: '--seconds films a clip, and this row films a still' }
|
|
99
|
+
const out = flagAt(args, '--out')
|
|
100
|
+
if (out == null) return { args: [...args], out: null }
|
|
101
|
+
const abs = isAbsolute(out.value) ? out.value : resolve(cwd, out.value)
|
|
102
|
+
const next = [...args]
|
|
103
|
+
if (out.joined) next[out.i] = `--out=${abs}`
|
|
104
|
+
else next[out.i + 1] = abs
|
|
105
|
+
return { args: next, out: abs }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const isHelp = (a: string): boolean => a === '--help' || a === '-h'
|
|
109
|
+
|
|
110
|
+
/** runs an argv from `cwd` with the terminal handed over; its exit code */
|
|
111
|
+
export async function spawnHere(argv: string[], cwd: string): Promise<number> {
|
|
112
|
+
const p = Bun.spawn(argv, { cwd, env: process.env, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' })
|
|
113
|
+
return await p.exited
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** the row each class was made from, so the docs print what it runs */
|
|
117
|
+
export const ROW_OF = new WeakMap<object, Verb>()
|
|
118
|
+
|
|
119
|
+
/** one clipanion command per row: the args after the path go to the code as typed */
|
|
120
|
+
export function verbs(rows: Verb[]): CommandClass[] {
|
|
121
|
+
return rows.map((v) => {
|
|
122
|
+
class VerbCommand extends Command<KitContext> {
|
|
123
|
+
static override paths = [v.path.split(' ')]
|
|
124
|
+
static override usage = Command.Usage({ category: v.category, description: v.description, details: v.details, examples: v.examples })
|
|
125
|
+
static essential = v.essential
|
|
126
|
+
static film = v.film
|
|
127
|
+
static filmVersion = v.version
|
|
128
|
+
static filmLength = v.length
|
|
129
|
+
args = Option.Proxy()
|
|
130
|
+
async execute(): Promise<number> {
|
|
131
|
+
const { root, cwd } = this.context
|
|
132
|
+
if (this.args.some(isHelp)) {
|
|
133
|
+
this.context.stdout.write(this.cli.usage(VerbCommand, { detailed: true }))
|
|
134
|
+
if (v.help !== 'own') return 0
|
|
135
|
+
this.context.stdout.write(`\nthe tool's own help:\n\n`)
|
|
136
|
+
return spawnHere(argvOf(v.run, this.args), root)
|
|
137
|
+
}
|
|
138
|
+
const call = filmCall(v.film, this.args, cwd)
|
|
139
|
+
if ('error' in call) { this.context.stderr.write(`FAIL: ${v.path}: ${call.error}\n`); return 2 }
|
|
140
|
+
let code: number | null = null
|
|
141
|
+
if ('tool' in v.run && this.context.askTool != null) code = await this.context.askTool(v.run.tool, [...(v.run.pre ?? []), ...call.args])
|
|
142
|
+
code ??= await spawnHere(argvOf(v.run, call.args), root)
|
|
143
|
+
if (code === 0 && call.out != null && !existsSync(call.out)) {
|
|
144
|
+
this.context.stderr.write(`FAIL: ${v.path} exited 0 and wrote no ${call.out}\n fix: a filmable row writes its --out path\n`)
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
return code
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
ROW_OF.set(VerbCommand, v)
|
|
151
|
+
return VerbCommand
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** the marks a command class carries: set by `verbs()` from its row, or
|
|
156
|
+
* 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' } = {}
|
|
160
|
+
if (typeof o.essential === 'object' && o.essential !== null) out.essential = o.essential as Essential
|
|
161
|
+
if (o.film === 'still' || o.film === 'clip') {
|
|
162
|
+
out.film = o.film
|
|
163
|
+
if (typeof o.filmVersion === 'number' || typeof o.filmVersion === 'string') out.version = o.filmVersion
|
|
164
|
+
if (o.filmLength === 'own') out.length = 'own'
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { Builtins, Cli } from 'clipanion'
|
|
|
7
7
|
import { AskNewCommand, AsksCheckCommand, AsksCommand } from './cli/cmd_Asks.ts'
|
|
8
8
|
import { AssetsAddCommand, AssetsCheckCommand, AssetsFillCommand, AssetsGenCommand, AssetsIdentityCommand } from './cli/cmd_Assets.ts'
|
|
9
9
|
import { BumpCommand } from './cli/cmd_Bump.ts'
|
|
10
|
+
import { CallsCommand } from './cli/cmd_Calls.ts'
|
|
11
|
+
import { TimeCommand } from './cli/cmd_Time.ts'
|
|
10
12
|
import { CheckCommand } from './cli/cmd_Check.ts'
|
|
11
13
|
import { CommandsCommand } from './cli/cmd_Commands.ts'
|
|
12
14
|
import { aliasArgs, CrossRepoActivityCommand } from './cli/cmd_CrossRepoActivity.ts'
|
|
@@ -35,8 +37,11 @@ import { CacheAuditCommand } from './cli/cmd_CacheAudit.ts'
|
|
|
35
37
|
import { TraceAuditCommand } from './cli/cmd_TraceAudit.ts'
|
|
36
38
|
import { ServeCommand } from './cli/cmd_Serve.ts'
|
|
37
39
|
import { SmokeCommand } from './cli/cmd_Smoke.ts'
|
|
40
|
+
import { ShotCommand } from './cli/cmd_Shot.ts'
|
|
38
41
|
import { StatusCommand } from './cli/cmd_Status.ts'
|
|
39
42
|
import { StartCommand } from './cli/cmd_Start.ts'
|
|
43
|
+
import { TRAILER_COMMANDS } from './cli/cmd_Trailer.ts'
|
|
44
|
+
import { REVIEW_COMMANDS } from './cli/cmd_Review.ts'
|
|
40
45
|
import { TuiCommand } from './cli/cmd_Tui.ts'
|
|
41
46
|
import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
|
|
42
47
|
|
|
@@ -67,6 +72,8 @@ export function buildCli(): Cli {
|
|
|
67
72
|
cli.register(MvCommand)
|
|
68
73
|
cli.register(CheckCommand)
|
|
69
74
|
cli.register(TuiCommand)
|
|
75
|
+
for (const c of TRAILER_COMMANDS) cli.register(c)
|
|
76
|
+
for (const c of REVIEW_COMMANDS) cli.register(c)
|
|
70
77
|
cli.register(ServeCommand)
|
|
71
78
|
cli.register(PmCommand)
|
|
72
79
|
cli.register(DaemonCommand)
|
|
@@ -91,6 +98,7 @@ export function buildCli(): Cli {
|
|
|
91
98
|
cli.register(FleetCommand)
|
|
92
99
|
cli.register(PbCollectionCommand)
|
|
93
100
|
cli.register(SmokeCommand)
|
|
101
|
+
cli.register(ShotCommand)
|
|
94
102
|
cli.register(ReflectCommand)
|
|
95
103
|
cli.register(KeysCommand)
|
|
96
104
|
cli.register(GlobalProcessesCommand)
|
|
@@ -109,6 +117,8 @@ export function buildCli(): Cli {
|
|
|
109
117
|
cli.register(TraceAuditCommand)
|
|
110
118
|
cli.register(UnitsAliasCommand)
|
|
111
119
|
cli.register(CommandsCommand)
|
|
120
|
+
cli.register(CallsCommand)
|
|
121
|
+
cli.register(TimeCommand)
|
|
112
122
|
cli.register(ConfigKeysCommand)
|
|
113
123
|
|
|
114
124
|
return cli
|
|
@@ -117,14 +127,17 @@ export function buildCli(): Cli {
|
|
|
117
127
|
// only when this file IS the process, so importing it costs nothing. A repo's
|
|
118
128
|
// installed copy first hands the command to the house source (handoff.ts). One copy
|
|
119
129
|
// of the lib per process: a repo config that imports its own install lands on
|
|
120
|
-
// this one (one-lib.ts), so the machine's version runs in every repo.
|
|
121
|
-
// is
|
|
130
|
+
// this one (one-lib.ts), so the machine's version runs in every repo. The run
|
|
131
|
+
// is logged (cli/kit/calls.ts, read back by `sk calls`). Then pm is started
|
|
132
|
+
// when it is down (autostart.ts), before the command runs
|
|
122
133
|
if (import.meta.main) {
|
|
123
134
|
const argv = aliasArgs(process.argv.slice(2))
|
|
124
135
|
const { handoff } = await import('./home/handoff.ts')
|
|
125
136
|
handoff(argv)
|
|
126
137
|
const { installOneLib } = await import('./core/one-lib.ts')
|
|
127
138
|
installOneLib()
|
|
139
|
+
const { callsFile, logCall } = await import('./cli/kit/calls.ts')
|
|
140
|
+
logCall(callsFile((await import('os')).homedir(), 'sk'), 'sk', argv)
|
|
128
141
|
const { autostart } = await import('./home/autostart.ts')
|
|
129
142
|
await autostart(argv)
|
|
130
143
|
void buildCli().runExit(argv)
|
|
@@ -31,7 +31,12 @@ export const NO_CHECK = 'in no check'
|
|
|
31
31
|
* section a row lives under (hub-app/tree-build.ts KIND_SECTION). There is no
|
|
32
32
|
* catch all kind: a row belongs to an owner, and what belongs to the hub
|
|
33
33
|
* itself is its status (serve/hub-problems.ts), never a row */
|
|
34
|
-
export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'check', 'test', 'reflection', 'process'] as const
|
|
34
|
+
export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'check', 'test', 'reflection', 'process', 'card', 'cut'] as const
|
|
35
|
+
|
|
36
|
+
/** the collection of the review cards that wait on him (collections/review.ts) */
|
|
37
|
+
export const REVIEW_COL = 'review'
|
|
38
|
+
/** the collection of the trailer cuts, one row a cut (collections/review.ts) */
|
|
39
|
+
export const TRAILER_COL = 'trailers'
|
|
35
40
|
|
|
36
41
|
export type ResourceType = (typeof RESOURCE_TYPES)[number]
|
|
37
42
|
|
|
@@ -27,6 +27,7 @@ import { mdBody } from './md-body.ts'
|
|
|
27
27
|
import { socialPostsCollection } from './posts.ts'
|
|
28
28
|
import { assetState, secretState, type SecretLevel, type SecretProbe } from './state-rules.ts'
|
|
29
29
|
import { reflectionsCollection } from './reflections.ts'
|
|
30
|
+
import { reviewCollection, trailersCollection } from './review.ts'
|
|
30
31
|
import { testsCollection } from './tests.ts'
|
|
31
32
|
import { checksCollection } from './checks.ts'
|
|
32
33
|
import { LAYOUT } from '../core/layout.ts'
|
|
@@ -417,5 +418,7 @@ export function buildCollections(root: string, c: RepoConfig, kit: ResolvedKit |
|
|
|
417
418
|
socialPostsCollection(root),
|
|
418
419
|
reflectionsCollection(root),
|
|
419
420
|
testsCollection(root, c),
|
|
421
|
+
// only where the repo has a review store or a trailer folder: the section of a repo with none is greyed
|
|
422
|
+
...[reviewCollection(root), trailersCollection(root)].filter((x): x is Collection => x !== null),
|
|
420
423
|
]
|
|
421
424
|
}
|