rvlib-shipkit 2.2.0 → 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 +38 -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/cli/cmd_Commands.ts +5 -0
- package/src/cli/cmd_Review.ts +948 -0
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +259 -6
- package/src/cli/kit/verb.ts +14 -4
- package/src/cli.ts +6 -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/layout.ts +2 -0
- package/src/reflect/prompt-corpus.ts +15 -119
- 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/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 +9 -5
- package/src/trailer/adapter.ts +58 -3
- package/src/trailer/build.ts +300 -41
- package/src/trailer/ffmpeg.ts +22 -0
- package/src/trailer/footage.ts +109 -40
- package/src/trailer/grammar.ts +444 -69
- package/src/trailer/graph.ts +199 -14
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +20 -3
- package/src/trailer/look.ts +17 -3
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +80 -21
- 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 +92 -9
- package/src/trailer/titles.ts +123 -18
- package/src/trailer/vocab.ts +23 -4
- package/src/trailer/words.ts +254 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
// sk review: an agent shows the owner something as one card of the one store and reads his answer; a dispatcher routes
|
|
2
|
+
// what he, a player or a rule raised, a worker claims and closes it (review/store.ts, wiki build/shipkit/review.md).
|
|
3
|
+
// Every module loads inside execute: the cli registers this file on every `sk` call
|
|
4
|
+
import { Command, Option } from 'clipanion'
|
|
5
|
+
|
|
6
|
+
const DETAILS = `the one feedback store, .shipkit/review/<id>/: one subject is one card (other angles are views of it, a follow up --supersedes it), he answers it in the hub's review entry (approved, ok with a fix, rejected, archived, rectangles, reactions per view), you read his answer with inbox or watch. What he, a player or a rule raises is an item of the same store: a dispatcher routes it to a topic, a worker claims it and closes it. The repo's review unit names its kinds, topics, sources and rules. SK_AGENT names your lane. Page: wiki build/shipkit/review.md`
|
|
7
|
+
|
|
8
|
+
type Ctx = { stdout: NodeJS.WritableStream; stderr: NodeJS.WritableStream }
|
|
9
|
+
type Unit = import('../review/unit.ts').ReviewUnit
|
|
10
|
+
type Say = import('../review/format.ts').Say
|
|
11
|
+
|
|
12
|
+
/** what a verb works on: the store, its repo's unit (null when the verb reads no unit, or the repo has none), and how the unit's lines print */
|
|
13
|
+
interface Here {
|
|
14
|
+
dir: string
|
|
15
|
+
unit: Unit | null
|
|
16
|
+
say: Say
|
|
17
|
+
out: (l: string) => void
|
|
18
|
+
err: (l: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** the store of the repo this runs in, its unit read when the verb needs it, and the body; a refusal is one FAIL line with its reason, exit 1 */
|
|
22
|
+
async function run(context: Ctx, body: (h: Here) => Promise<number> | number, o: { unit?: boolean } = {}): Promise<number> {
|
|
23
|
+
const { findRepoRoot } = await import('../ts/config.ts')
|
|
24
|
+
const out = (l: string): void => void context.stdout.write(`${l}\n`)
|
|
25
|
+
const err = (l: string): void => void context.stderr.write(`${l}\n`)
|
|
26
|
+
try {
|
|
27
|
+
const root = findRepoRoot(process.cwd())
|
|
28
|
+
if (o.unit === false) {
|
|
29
|
+
const { storeDir } = await import('../review/store.ts')
|
|
30
|
+
return await body({ dir: storeDir(root), unit: null, say: { spec: (await import('../review/model.ts')).NO_SPEC, more: () => [] }, out, err })
|
|
31
|
+
}
|
|
32
|
+
const { openReview, factLines } = await import('../review/project.ts')
|
|
33
|
+
const { specOf } = await import('../review/store.ts')
|
|
34
|
+
const { dir, unit } = await openReview(root)
|
|
35
|
+
return await body({ dir, unit, say: { spec: specOf(dir), more: (x) => factLines(unit, x) }, out, err })
|
|
36
|
+
} catch (e) {
|
|
37
|
+
err(`FAIL: review: ${e instanceof Error ? e.message : String(e)}`)
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isHelp = (a: string): boolean => a === '--help' || a === '-h'
|
|
43
|
+
|
|
44
|
+
/** the agent that acts: --as, else SK_AGENT; `need` refuses an unnamed one */
|
|
45
|
+
function actor(as: string | undefined, need: boolean, verb: string): string {
|
|
46
|
+
const n = as?.trim() || process.env.SK_AGENT?.trim()
|
|
47
|
+
if (n === 'owner') throw new Error('the owner acts from the hub, not from here')
|
|
48
|
+
if (n) return n
|
|
49
|
+
if (need) throw new Error(`${verb} needs --as <name> (or SK_AGENT)`)
|
|
50
|
+
return 'agent'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** an item id or a player report's ref (`live#3`) to its item id */
|
|
54
|
+
async function idOf(dir: string, ref: string): Promise<string> {
|
|
55
|
+
const { itemOfRef } = await import('../review/store.ts')
|
|
56
|
+
const x = itemOfRef(dir, ref)
|
|
57
|
+
if (!x) throw new Error(`no item ${ref}`)
|
|
58
|
+
return x.item.id
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** the sources pulled before a read: a line per source down, said once a process */
|
|
62
|
+
const downSaid = new Set<string>()
|
|
63
|
+
async function pull(h: Here): Promise<void> {
|
|
64
|
+
if (!h.unit?.sources?.length) return
|
|
65
|
+
const { pullSources } = await import('../review/project.ts')
|
|
66
|
+
const now = new Set<string>()
|
|
67
|
+
const got = await pullSources(h.dir, h.unit, (l) => {
|
|
68
|
+
now.add(l)
|
|
69
|
+
if (!downSaid.has(l)) h.err(l)
|
|
70
|
+
})
|
|
71
|
+
downSaid.clear()
|
|
72
|
+
for (const l of now) downSaid.add(l)
|
|
73
|
+
if (got.length > 0) h.err(`review: ${got.length} new items pulled`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** a gallery tile as given: `<label>=<file>`, or a file labelled by its name */
|
|
77
|
+
function tileOf(arg: string): { file: string; label: string } {
|
|
78
|
+
const eq = arg.lastIndexOf('=')
|
|
79
|
+
if (eq > 0) return { label: arg.slice(0, eq), file: arg.slice(eq + 1) }
|
|
80
|
+
const name = arg.split('/').pop() ?? arg
|
|
81
|
+
return { file: arg, label: name.replace(/\.[a-z0-9]+$/i, '') }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type ViewInput = import('../review/store.ts').ViewInput
|
|
85
|
+
|
|
86
|
+
/** `--view "<label>" a.png [b.png]` and `--gallery "<label>" a.png b.png …` out of the args */
|
|
87
|
+
function takeViews(args: readonly string[]): { views: ViewInput[]; rest: string[] } {
|
|
88
|
+
const views: ViewInput[] = []
|
|
89
|
+
const rest: string[] = []
|
|
90
|
+
for (let i = 0; i < args.length; i++) {
|
|
91
|
+
const a = args[i] ?? ''
|
|
92
|
+
if (a !== '--gallery' && a !== '--view') {
|
|
93
|
+
rest.push(a)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
const label = args[i + 1]
|
|
97
|
+
const files: string[] = []
|
|
98
|
+
for (let j = i + 2; j < args.length && (a === '--gallery' || files.length < 2) && !(args[j] ?? '').startsWith('--'); j++) files.push(args[j] ?? '')
|
|
99
|
+
if (label === undefined || label.startsWith('--') || files.length === 0) throw new Error(a === '--gallery' ? '--gallery "<label>" a.png b.png …: a label, then its images' : '--view "<label>" a.png [b.png]: a label, then an image or a before and an after')
|
|
100
|
+
if (a === '--gallery') views.push({ label, gallery: files.map(tileOf) })
|
|
101
|
+
else views.push(files.length === 1 ? { label, image: files[0] } : { label, before: files[0], after: files[1] })
|
|
102
|
+
i += 1 + files.length
|
|
103
|
+
}
|
|
104
|
+
return { views, rest }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** the flags of `add`, parsed by name: a value flag repeats where it says so */
|
|
108
|
+
function flagsOf(args: readonly string[], values: readonly string[], repeat: readonly string[], bools: readonly string[]): { one: Record<string, string>; many: Record<string, string[]>; on: Set<string>; positionals: string[] } {
|
|
109
|
+
const one: Record<string, string> = {}
|
|
110
|
+
const many: Record<string, string[]> = {}
|
|
111
|
+
const on = new Set<string>()
|
|
112
|
+
const positionals: string[] = []
|
|
113
|
+
for (let i = 0; i < args.length; i++) {
|
|
114
|
+
const a = args[i] ?? ''
|
|
115
|
+
if (!a.startsWith('--')) {
|
|
116
|
+
positionals.push(a)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const name = a.slice(2)
|
|
120
|
+
if (bools.includes(name)) {
|
|
121
|
+
on.add(name)
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (!values.includes(name) && !repeat.includes(name)) throw new Error(`${a}: no such flag (sk review add --help)`)
|
|
125
|
+
const v = args[i + 1]
|
|
126
|
+
if (v === undefined) throw new Error(`${a}: needs a value`)
|
|
127
|
+
i++
|
|
128
|
+
if (repeat.includes(name)) (many[name] ??= []).push(v)
|
|
129
|
+
else one[name] = v
|
|
130
|
+
}
|
|
131
|
+
return { one, many, on, positionals }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ADD_FLAGS = `flags:
|
|
135
|
+
--title "<title>" one short line: what he looks at
|
|
136
|
+
--what "<what>" what changed and why, short markdown: one line, then - bullets
|
|
137
|
+
--image <a.png> the subject; a clip (webm, mp4) goes anywhere an image goes
|
|
138
|
+
--video <clip.mp4> a clip
|
|
139
|
+
--audio <sound.wav> a sound: a waveform and a player (wav, mp3, flac, ogg, opus, m4a)
|
|
140
|
+
--before <a.png> --after <b.png> a before and after: two images, two clips of one size, or two sounds
|
|
141
|
+
--label "<label>" the first view's label
|
|
142
|
+
--view "<label>" <a.png> [<b.png>] another view of the same subject (repeat it)
|
|
143
|
+
--gallery "<label>" <a.png> … a grid of small images; "<tile label>=<c.png>" names a tile (repeat it)
|
|
144
|
+
--topic <topic> what part of the project it is about (sk review topics)
|
|
145
|
+
--subject <kind> what is judged (default shot; the repo's unit names its kinds)
|
|
146
|
+
--commit <sha> the commit it shows
|
|
147
|
+
--lane <id> the lane that made it; else SK_AGENT
|
|
148
|
+
--supersedes <id> the card it follows up, after his answer
|
|
149
|
+
--answers <id | live#3> the item it answers (a note he raised, a player's report), one thread (repeat it)
|
|
150
|
+
--keeps "<what stays>" with --supersedes on an approved look: what of it stays
|
|
151
|
+
--new --why "<reason>" a new card beside an active one of the same subject`
|
|
152
|
+
|
|
153
|
+
export class ReviewAddCommand extends Command {
|
|
154
|
+
static override paths = [['review', 'add']]
|
|
155
|
+
static override usage = Command.Usage({
|
|
156
|
+
category: 'Review',
|
|
157
|
+
description: 'show the owner a change as one card he answers in the hub: an image, a clip, a pair, views',
|
|
158
|
+
details: `${DETAILS}\n\n${ADD_FLAGS}`,
|
|
159
|
+
examples: [
|
|
160
|
+
['one image', 'sk review add --title "the new menu" --what "the menu, bigger" --image shots/menu.png'],
|
|
161
|
+
['a before and after', 'sk review add --title "lamps lit" --what "..." --before a.png --after b.png'],
|
|
162
|
+
['a second version after his answer', 'sk review add --title "lamps lit" --what "..." --image c.png --supersedes <id>'],
|
|
163
|
+
],
|
|
164
|
+
})
|
|
165
|
+
args = Option.Proxy()
|
|
166
|
+
|
|
167
|
+
async execute(): Promise<number> {
|
|
168
|
+
if (this.args.some(isHelp)) {
|
|
169
|
+
this.context.stdout.write(this.cli.usage(ReviewAddCommand, { detailed: true }))
|
|
170
|
+
return 0
|
|
171
|
+
}
|
|
172
|
+
return run(this.context, async (h) => {
|
|
173
|
+
const { addItem } = await import('../review/store.ts')
|
|
174
|
+
const { views, rest } = takeViews(this.args)
|
|
175
|
+
// the repo's own flags: the subject kind's (--where), and the unit's for any kind (--mark), each repeatable
|
|
176
|
+
const kindName = rest[rest.indexOf('--subject') + 1]
|
|
177
|
+
const kind = rest.includes('--subject') && kindName ? h.unit?.kinds?.[kindName] : undefined
|
|
178
|
+
const own = [...Object.keys(kind?.flags ?? {}), ...Object.keys(h.unit?.flags ?? {})]
|
|
179
|
+
const f = flagsOf(rest, ['title', 'what', 'image', 'video', 'audio', 'before', 'after', 'label', 'topic', 'subject', 'commit', 'lane', 'supersedes', 'keeps', 'why', 'live'], ['answers', ...own], ['new', 'wip'])
|
|
180
|
+
if (f.positionals.length > 0) throw new Error(`unexpected ${f.positionals.join(' ')}`)
|
|
181
|
+
const o = f.one
|
|
182
|
+
const repoFlags = Object.fromEntries(own.flatMap((k) => (f.many[k] ? [[k, f.many[k]]] : [])))
|
|
183
|
+
const parsed = kind?.parse ? kind.parse(Object.fromEntries(Object.keys(kind.flags ?? {}).map((k) => [k, f.many[k] ?? []]))) : null
|
|
184
|
+
if (f.on.has('wip') !== (o.live !== undefined)) throw new Error('--wip --live <name>: a draft card names its live shot, and a live shot makes a draft card')
|
|
185
|
+
let wipViews: ViewInput[] = []
|
|
186
|
+
if (o.live !== undefined) {
|
|
187
|
+
if (!h.unit?.live) throw new Error("--live: the repo's review unit declares no live shots (live: { frames })")
|
|
188
|
+
if (o.image || o.video || o.audio || o.before || o.after || views.length > 0) throw new Error('--wip: the card shows the live shot alone, no --image, --video, pair or --view beside it')
|
|
189
|
+
const fr = h.unit.live.frames(o.live)
|
|
190
|
+
wipViews = [{ label: '', image: fr.image }, ...(fr.history.length > 0 ? [{ label: 'history', gallery: fr.history }] : [])]
|
|
191
|
+
}
|
|
192
|
+
const answers = await Promise.all((f.many.answers ?? []).map((a) => idOf(h.dir, a)))
|
|
193
|
+
const topic = o.topic ?? parsed?.topic
|
|
194
|
+
let input: import('../review/store.ts').AddInput = {
|
|
195
|
+
title: o.title ?? '',
|
|
196
|
+
what: o.what ?? '',
|
|
197
|
+
...(parsed ? { subject: parsed.subject } : o.subject ? { subject: { kind: o.subject } } : {}),
|
|
198
|
+
...(topic ? { topic } : {}),
|
|
199
|
+
...(o.image ? { image: o.image } : {}),
|
|
200
|
+
...(o.video ? { video: o.video } : {}),
|
|
201
|
+
...(o.audio ? { audio: o.audio } : {}),
|
|
202
|
+
...(o.before ? { before: o.before } : {}),
|
|
203
|
+
...(o.after ? { after: o.after } : {}),
|
|
204
|
+
...(o.label ? { label: o.label } : {}),
|
|
205
|
+
...(o.commit ? { commit: o.commit } : {}),
|
|
206
|
+
...(o.lane ? { lane: o.lane } : {}),
|
|
207
|
+
...(o.supersedes ? { supersedes: o.supersedes } : {}),
|
|
208
|
+
...(o.keeps ? { keeps: o.keeps } : {}),
|
|
209
|
+
...(o.why ? { why: o.why } : {}),
|
|
210
|
+
...(answers.length > 0 ? { answers } : {}),
|
|
211
|
+
...(f.on.has('new') ? { fresh: true } : {}),
|
|
212
|
+
...(o.live !== undefined ? { image: wipViews[0]?.image ?? '', views: wipViews.slice(1), wip: { live: o.live } } : { views }),
|
|
213
|
+
}
|
|
214
|
+
if (h.unit?.add) input = await h.unit.add(input, repoFlags)
|
|
215
|
+
const item = await addItem(h.dir, input)
|
|
216
|
+
h.out(item.id)
|
|
217
|
+
if (item.origin.by === 'agent' && !item.origin.lane) h.err('no lane on the card: pass --lane <id> (or SK_AGENT), so the hub colours it')
|
|
218
|
+
return 0
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class ReviewAddViewCommand extends Command {
|
|
224
|
+
static override paths = [['review', 'add-view']]
|
|
225
|
+
static override usage = Command.Usage({
|
|
226
|
+
category: 'Review',
|
|
227
|
+
description: 'another angle on a card he has not answered yet: an image, a pair, a clip or a gallery',
|
|
228
|
+
details: `${DETAILS}\n\nsk review add-view <id> "<label>" <a.png> [<b.png>] | --video <clip.mp4> | --gallery <a.png> <b.png> …`,
|
|
229
|
+
examples: [['a view from above', 'sk review add-view <id> "from above" top.png']],
|
|
230
|
+
})
|
|
231
|
+
args = Option.Proxy()
|
|
232
|
+
|
|
233
|
+
async execute(): Promise<number> {
|
|
234
|
+
if (this.args.some(isHelp)) {
|
|
235
|
+
this.context.stdout.write(this.cli.usage(ReviewAddViewCommand, { detailed: true }))
|
|
236
|
+
return 0
|
|
237
|
+
}
|
|
238
|
+
return run(this.context, async (h) => {
|
|
239
|
+
const { addView } = await import('../review/store.ts')
|
|
240
|
+
const own = Object.keys(h.unit?.flags ?? {})
|
|
241
|
+
const repoFlags: Record<string, string[]> = {}
|
|
242
|
+
const args: string[] = []
|
|
243
|
+
for (let i = 0; i < this.args.length; i++) {
|
|
244
|
+
const a = this.args[i] ?? ''
|
|
245
|
+
const name = a.startsWith('--') ? a.slice(2) : ''
|
|
246
|
+
if (own.includes(name)) (repoFlags[name] ??= []).push(this.args[++i] ?? '')
|
|
247
|
+
else args.push(a)
|
|
248
|
+
}
|
|
249
|
+
const [id, label, ...rest] = args
|
|
250
|
+
if (!id || !label) throw new Error('add-view <id> "<label>" <media>')
|
|
251
|
+
const v: ViewInput =
|
|
252
|
+
rest[0] === '--video' && rest[1]
|
|
253
|
+
? { label, video: rest[1] }
|
|
254
|
+
: rest[0] === '--audio' && rest[1]
|
|
255
|
+
? { label, audio: rest[1] }
|
|
256
|
+
: rest[0] === '--gallery'
|
|
257
|
+
? { label, gallery: rest.slice(1).map(tileOf) }
|
|
258
|
+
: rest.length === 1
|
|
259
|
+
? { label, image: rest[0] }
|
|
260
|
+
: rest.length === 2
|
|
261
|
+
? { label, before: rest[0], after: rest[1] }
|
|
262
|
+
: (() => {
|
|
263
|
+
throw new Error('add-view <id> "<label>" <a.png> [<b.png>] | --video <clip> | --gallery <a.png> …')
|
|
264
|
+
})()
|
|
265
|
+
const extra = h.unit?.addView ? await h.unit.addView(v, repoFlags) : {}
|
|
266
|
+
const item = await addView(h.dir, id, v, extra)
|
|
267
|
+
h.out(`${id}: view ${item.views.length} "${label}" added`)
|
|
268
|
+
return 0
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export class ReviewLsCommand extends Command {
|
|
274
|
+
static override paths = [['review'], ['review', 'ls']]
|
|
275
|
+
static override usage = Command.Usage({
|
|
276
|
+
category: 'Review',
|
|
277
|
+
description: "the store's active threads, one line each: status, id, title; `sk review <verb>` runs a unit's verb",
|
|
278
|
+
details: `${DETAILS}\n\nsk review [--all] lists; sk review <verb> … runs a verb the repo's review unit declares (verbs: {...}).`,
|
|
279
|
+
examples: [['what is open', 'sk review ls'], ['every thread', 'sk review ls --all']],
|
|
280
|
+
})
|
|
281
|
+
args = Option.Proxy()
|
|
282
|
+
|
|
283
|
+
async execute(): Promise<number> {
|
|
284
|
+
const [verb, ...rest] = this.args
|
|
285
|
+
if (verb !== undefined && isHelp(verb)) {
|
|
286
|
+
this.context.stdout.write(this.cli.usage(ReviewLsCommand, { detailed: true }))
|
|
287
|
+
return 0
|
|
288
|
+
}
|
|
289
|
+
// a word that is no flag is a verb of the repo's review unit: a report replayed, the town composer
|
|
290
|
+
if (verb !== undefined && !verb.startsWith('--'))
|
|
291
|
+
return run(this.context, async (h) => {
|
|
292
|
+
const verbs = h.unit?.verbs ?? {}
|
|
293
|
+
const v = verbs[verb]
|
|
294
|
+
if (!v) throw new Error(`no verb « ${verb} »: sk review --help lists sk's own; the repo's unit declares ${Object.keys(verbs).join(', ') || 'none'}`)
|
|
295
|
+
const { dirname } = await import('node:path')
|
|
296
|
+
return v.run(rest, { dir: h.dir, root: dirname(dirname(h.dir)), say: h.out, err: h.err })
|
|
297
|
+
})
|
|
298
|
+
const every = rest.length === 0 && verb === '--all'
|
|
299
|
+
if (verb !== undefined && !every) {
|
|
300
|
+
this.context.stderr.write(`FAIL: review: unexpected ${this.args.join(' ')} (sk review --help)\n`)
|
|
301
|
+
return 1
|
|
302
|
+
}
|
|
303
|
+
// the listing reads no unit: it stays the fast answer, and the unit's refusals show in inbox and the hub
|
|
304
|
+
return run(
|
|
305
|
+
this.context,
|
|
306
|
+
async (h) => {
|
|
307
|
+
const { readAll } = await import('../review/store.ts')
|
|
308
|
+
const { isActive, orderedThreads, statusOf } = await import('../review/threads.ts')
|
|
309
|
+
const all = readAll(h.dir)
|
|
310
|
+
for (const b of all.broken) h.err(`review: FAIL ${b.id}: ${b.error}`)
|
|
311
|
+
const threads = orderedThreads(all.items).filter((t) => every || isActive(t))
|
|
312
|
+
if (threads.length === 0) h.out(every ? 'review: the store is empty' : 'review: nothing active (--all shows every thread)')
|
|
313
|
+
for (const t of threads) h.out(`${statusOf(t.head, all.items).padEnd(17)} ${t.head.item.id} ${t.head.item.title}${t.versions.length > 1 ? ` (v${t.versions.length})` : ''}`)
|
|
314
|
+
return all.broken.length > 0 ? 1 : 0
|
|
315
|
+
},
|
|
316
|
+
{ unit: false },
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export class ReviewShowCommand extends Command {
|
|
322
|
+
static override paths = [['review', 'show']]
|
|
323
|
+
static override usage = Command.Usage({ category: 'Review', description: 'one item and its thread: the verdict, rectangles, reactions, the whole conversation', details: `${DETAILS}\n\nTakes an item id or a player report's ref (live#3).` })
|
|
324
|
+
id = Option.String()
|
|
325
|
+
|
|
326
|
+
async execute(): Promise<number> {
|
|
327
|
+
return run(this.context, async (h) => {
|
|
328
|
+
const { readAllCached } = await import('../review/store.ts')
|
|
329
|
+
const { threadDetail, detail } = await import('../review/format.ts')
|
|
330
|
+
const { threadOfItem, threadsOf } = await import('../review/threads.ts')
|
|
331
|
+
const id = await idOf(h.dir, this.id)
|
|
332
|
+
const all = readAllCached(h.dir)
|
|
333
|
+
const x = all.items.find((y) => y.item.id === id)
|
|
334
|
+
if (!x) throw new Error(`no item ${this.id}`)
|
|
335
|
+
const t = threadOfItem(x.item.id, threadsOf(all.items))
|
|
336
|
+
h.out(t && (t.head.item.id === x.item.id || t.answers.length > 0) ? threadDetail(t, h.dir, true, h.say) : detail(x, h.dir, true, false, h.say))
|
|
337
|
+
return 0
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** --mine or --session <id>: only the cards that session opened; null keeps every item */
|
|
343
|
+
async function sessionOf(mine: boolean, session: string | undefined): Promise<string | null> {
|
|
344
|
+
if (session !== undefined) return session
|
|
345
|
+
if (!mine) return null
|
|
346
|
+
const { sessionId } = await import('../review/store.ts')
|
|
347
|
+
const s = sessionId()
|
|
348
|
+
if (s === null) throw new Error('--mine: no Claude Code session here (CLAUDE_CODE_SESSION_ID): --session <id>')
|
|
349
|
+
return s
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export class ReviewInboxCommand extends Command {
|
|
353
|
+
static override paths = [['review', 'inbox']]
|
|
354
|
+
static override usage = Command.Usage({
|
|
355
|
+
category: 'Review',
|
|
356
|
+
description: 'every answer not marked seen and every open item he raised, frame notes included',
|
|
357
|
+
details: DETAILS,
|
|
358
|
+
examples: [['what waits on you', 'sk review inbox'], ['his notes on trailer frames', 'sk review inbox --subject frame'], ['the cards this session sent', 'sk review inbox --mine']],
|
|
359
|
+
})
|
|
360
|
+
mine = Option.Boolean('--mine', false, { description: "only this Claude Code session's cards" })
|
|
361
|
+
session = Option.String('--session', { description: "only that session's cards" })
|
|
362
|
+
subject = Option.String('--subject', { description: 'only items of this subject kind (frame, shot…)' })
|
|
363
|
+
all = Option.Boolean('--all', false, { description: 'a plain approval too, not only the counted line' })
|
|
364
|
+
|
|
365
|
+
async execute(): Promise<number> {
|
|
366
|
+
return run(this.context, async (h) => {
|
|
367
|
+
const { liveWatchers } = await import('../review/watchers.ts')
|
|
368
|
+
const { readAll } = await import('../review/store.ts')
|
|
369
|
+
const { BUILTIN_KINDS, coversCard, moveOf, stateOf, unreadBy, untakenOf, verdictOf } = await import('../review/model.ts')
|
|
370
|
+
const { followedIds, threadOfItem, threadsOf, unseen } = await import('../review/threads.ts')
|
|
371
|
+
const { detail, threadDetail, untakenLine } = await import('../review/format.ts')
|
|
372
|
+
const kinds = [...Object.keys(h.say.spec.kinds), ...Object.keys(BUILTIN_KINDS)]
|
|
373
|
+
if (this.subject !== undefined && Object.keys(h.say.spec.kinds).length > 0 && !kinds.includes(this.subject)) throw new Error(`--subject: one of ${kinds.join(', ')}`)
|
|
374
|
+
const session = await sessionOf(this.mine, this.session)
|
|
375
|
+
await pull(h)
|
|
376
|
+
const all = readAll(h.dir)
|
|
377
|
+
for (const b of all.broken) h.err(`review: FAIL ${b.id}: ${b.error}`)
|
|
378
|
+
const of = (x: (typeof all.items)[number]): boolean => coversCard({ session, lane: null }, x) && (this.subject === undefined || x.item.subject.kind === this.subject)
|
|
379
|
+
const news = unseen(all.items).filter(of)
|
|
380
|
+
const followed = followedIds(all.items)
|
|
381
|
+
// what he or a player raised and nobody closed is the agents' move, read or not
|
|
382
|
+
const open = all.items.filter((x) => of(x) && x.item.origin.by !== 'agent' && moveOf(stateOf(x, followed.has(x.item.id)).state) === 'agents' && !news.includes(x))
|
|
383
|
+
// a plain approval (no comment, no rectangle, no reply) asks for no work: counted on one line unless --all
|
|
384
|
+
const plain = (x: (typeof news)[number]): boolean => {
|
|
385
|
+
const v = verdictOf(x)
|
|
386
|
+
return v?.status === 'approved' && !v.seen && !v.comment && v.rects.length === 0 && unreadBy(x, 'agent').length === 0
|
|
387
|
+
}
|
|
388
|
+
const shown = [...(this.all ? news : news.filter((x) => !plain(x))), ...open]
|
|
389
|
+
if (shown.length === 0 && news.length === 0) h.out('review: no unseen feedback or reply')
|
|
390
|
+
const threads = threadsOf(all.items)
|
|
391
|
+
const printed = new Set<string>()
|
|
392
|
+
for (const x of shown) {
|
|
393
|
+
const t = threadOfItem(x.item.id, threads)
|
|
394
|
+
if (t && printed.has(t.head.item.id)) continue
|
|
395
|
+
if (t) printed.add(t.head.item.id)
|
|
396
|
+
h.out(`${t ? threadDetail(t, h.dir, false, h.say) : detail(x, h.dir, false, false, h.say)}\n`)
|
|
397
|
+
}
|
|
398
|
+
const watchers = liveWatchers(h.dir)
|
|
399
|
+
const untaken = all.items.flatMap((x) => {
|
|
400
|
+
const why = untakenOf(x, followed.has(x.item.id), watchers, new Date(), h.say.spec.dispatcher)
|
|
401
|
+
return why === null ? [] : [untakenLine(x, why, null)]
|
|
402
|
+
})
|
|
403
|
+
if (untaken.length > 0) h.out(`not taken:\n${untaken.join('\n')}\n`)
|
|
404
|
+
const approvals = this.all ? [] : news.filter(plain)
|
|
405
|
+
if (approvals.length > 0) h.out(`review: ${approvals.length} plain approval(s), nothing to do: sk review seen ${approvals.map((x) => x.item.id).join(' ')}`)
|
|
406
|
+
if (shown.length > 0) h.out('mark each handled: sk review seen <id>; close a raised one: sk review fixed <id> --note "<what was done>"')
|
|
407
|
+
return all.broken.length > 0 ? 1 : 0
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export class ReviewSeenCommand extends Command {
|
|
413
|
+
static override paths = [['review', 'seen']]
|
|
414
|
+
static override usage = Command.Usage({ category: 'Review', description: 'mark his answers and replies on these items read, once you acted on them', details: DETAILS })
|
|
415
|
+
ids = Option.Rest({ required: 1 })
|
|
416
|
+
|
|
417
|
+
async execute(): Promise<number> {
|
|
418
|
+
return run(this.context, async (h) => {
|
|
419
|
+
const { markSeen } = await import('../review/store.ts')
|
|
420
|
+
let bad = 0
|
|
421
|
+
for (const ref of this.ids) {
|
|
422
|
+
const id = await idOf(h.dir, ref).catch(() => ref)
|
|
423
|
+
if (markSeen(h.dir, id)) h.out(`${ref}: seen`)
|
|
424
|
+
else {
|
|
425
|
+
h.err(`FAIL: review: ${ref}: no such item, or nothing of his to read on it`)
|
|
426
|
+
bad++
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return bad > 0 ? 1 : 0
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export class ReviewReplyCommand extends Command {
|
|
435
|
+
static override paths = [['review', 'reply']]
|
|
436
|
+
static override usage = Command.Usage({ category: 'Review', description: "your message on a card's thread: what you changed, the answer to his question", details: DETAILS })
|
|
437
|
+
id = Option.String()
|
|
438
|
+
text = Option.String()
|
|
439
|
+
image = Option.String('--image', { description: 'an image beside the words' })
|
|
440
|
+
|
|
441
|
+
async execute(): Promise<number> {
|
|
442
|
+
return run(this.context, async (h) => {
|
|
443
|
+
const { addMessage, agentName } = await import('../review/store.ts')
|
|
444
|
+
const id = await idOf(h.dir, this.id)
|
|
445
|
+
const m = addMessage(h.dir, id, agentName(), this.text, this.image)
|
|
446
|
+
h.out(`${id}: ${m.id}`)
|
|
447
|
+
return 0
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export class ReviewWithdrawCommand extends Command {
|
|
453
|
+
static override paths = [['review', 'withdraw']]
|
|
454
|
+
static override usage = Command.Usage({ category: 'Review', description: 'take an unanswered card back, with the reason', details: DETAILS })
|
|
455
|
+
ids = Option.Rest({ required: 1 })
|
|
456
|
+
why = Option.String('--why', { required: true, description: 'why it goes' })
|
|
457
|
+
|
|
458
|
+
async execute(): Promise<number> {
|
|
459
|
+
return run(this.context, async (h) => {
|
|
460
|
+
const { withdraw } = await import('../review/store.ts')
|
|
461
|
+
for (const id of this.ids) if (!withdraw(h.dir, id, this.why)) throw new Error(`${id}: no such card, or he answered it already`)
|
|
462
|
+
h.out(`review: ${this.ids.length} withdrawn`)
|
|
463
|
+
return 0
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export class ReviewReopenCommand extends Command {
|
|
469
|
+
static override paths = [['review', 'reopen']]
|
|
470
|
+
static override usage = Command.Usage({ category: 'Review', description: 'an answered card waits for his answer again; the old verdict stays in the thread', details: DETAILS })
|
|
471
|
+
ids = Option.Rest({ required: 1 })
|
|
472
|
+
|
|
473
|
+
async execute(): Promise<number> {
|
|
474
|
+
return run(this.context, async (h) => {
|
|
475
|
+
const { reopen } = await import('../review/store.ts')
|
|
476
|
+
for (const id of this.ids) if (!reopen(h.dir, id)) throw new Error(`${id}: no such card, or no answer to reopen`)
|
|
477
|
+
h.out(`review: ${this.ids.length} reopened`)
|
|
478
|
+
return 0
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
type ActOp = 'fixed' | 'wontfix' | 'claim' | 'release' | 'blocked' | 'done' | 'dispatch' | 'ask' | 'drop' | 'reassign'
|
|
484
|
+
|
|
485
|
+
/** one state change: by a worker on its claim, or by the dispatcher on what it routes; the item, then the new state */
|
|
486
|
+
function actCommand(op: ActOp, description: string, o: { note?: 'required' | 'optional'; text?: string; topic?: 'required' | 'optional'; named: boolean; examples: [string, string][] }) {
|
|
487
|
+
return class extends Command {
|
|
488
|
+
static override paths = [['review', op]]
|
|
489
|
+
static override usage = Command.Usage({ category: 'Review', description, details: `${DETAILS}\n\nTakes an item id or a player report's ref (live#3). ${o.named ? 'Who acts: --as <name>, else SK_AGENT.' : 'Who acts: --as, else SK_AGENT, else agent.'}`, examples: o.examples })
|
|
490
|
+
id = Option.String()
|
|
491
|
+
words = o.text !== undefined || o.topic !== undefined ? Option.String({ required: false, name: o.text ?? 'topic' }) : undefined
|
|
492
|
+
note = o.note === 'required' ? Option.String('--note', { required: true, description: 'what was done, why not, or what is needed' }) : Option.String('--note', { description: 'what was done, or the reason' })
|
|
493
|
+
commit = Option.String('--commit', { description: 'the commit that holds the fix' })
|
|
494
|
+
as = Option.String('--as', { description: 'who acts (else SK_AGENT)' })
|
|
495
|
+
|
|
496
|
+
async execute(): Promise<number> {
|
|
497
|
+
return run(this.context, async (h) => {
|
|
498
|
+
const { act, readItem } = await import('../review/store.ts')
|
|
499
|
+
const { stateOf } = await import('../review/model.ts')
|
|
500
|
+
const { detail } = await import('../review/format.ts')
|
|
501
|
+
if (this.words !== undefined && o.text === undefined && o.topic === undefined) throw new Error(`unexpected ${this.words}`)
|
|
502
|
+
if (o.text && !this.words?.trim()) throw new Error(`${op} <id> "<${o.text}>"`)
|
|
503
|
+
if (o.topic === 'required' && !this.words) throw new Error(`${op} <id> <topic> (sk review topics)`)
|
|
504
|
+
const id = await idOf(h.dir, this.id)
|
|
505
|
+
const who = actor(this.as, o.named, op)
|
|
506
|
+
act(h.dir, id, op, who, {
|
|
507
|
+
...(this.note ? { note: this.note } : {}),
|
|
508
|
+
...(op === 'ask' && this.words ? { text: this.words } : {}),
|
|
509
|
+
...(op === 'done' && this.words ? { note: this.words } : {}),
|
|
510
|
+
...(o.topic && this.words ? { topic: this.words } : {}),
|
|
511
|
+
...(this.commit ? { commit: this.commit } : {}),
|
|
512
|
+
})
|
|
513
|
+
const x = readItem(h.dir, id)
|
|
514
|
+
const st = x ? stateOf(x, false) : null
|
|
515
|
+
h.out(`${id} ${op}: ${st ? `${st.state}${st.why ? ` (${st.why})` : ''}${st.topic ? ` ${st.topic}` : ''}${st.worker ? ` ${st.worker}` : ''}` : 'gone'}`)
|
|
516
|
+
if (op === 'claim' && x) h.out(detail(x, h.dir, false, false, h.say))
|
|
517
|
+
return 0
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export const ReviewFixedCommand = actCommand('fixed', 'close an item he raised as done: the note says what was done (and --commit where)', { note: 'optional', named: false, examples: [['done, with its commit', 'sk review fixed <id> --note "the lamp lit" --commit abc1234']] })
|
|
524
|
+
export const ReviewWontfixCommand = actCommand('wontfix', 'close an item he raised as not done, with the reason', { note: 'required', named: false, examples: [['not done', 'sk review wontfix <id> --note "the room is gone"']] })
|
|
525
|
+
export const ReviewClaimCommand = actCommand('claim', 'take an item to work on it: the hub shows the card in work by your lane', { named: true, examples: [['take it', 'sk review claim <id> --as L12']] })
|
|
526
|
+
export const ReviewReleaseCommand = actCommand('release', 'give back an item you claimed', { note: 'optional', named: true, examples: [['give it back', 'sk review release <id> --as L12']] })
|
|
527
|
+
export const ReviewBlockedCommand = actCommand('blocked', 'an item you claimed goes back to the owner with what is needed', { note: 'required', named: true, examples: [['his decision is needed', 'sk review blocked <id> --note "which of the two doors?" --as L12']] })
|
|
528
|
+
export const ReviewDoneCommand = actCommand('done', 'an « ok with a fix » card made with no new card: closed, the words say what changed', { text: 'what changed', named: false, examples: [['the fix made', 'sk review done <id> "the sign moved one tile up"']] })
|
|
529
|
+
export const ReviewDispatchCommand = actCommand('dispatch', 'the dispatcher sends a raised item to a topic', { note: 'optional', topic: 'required', named: true, examples: [['to a topic', 'sk review dispatch <id> lieux --as orchestrator']] })
|
|
530
|
+
export const ReviewAskCommand = actCommand('ask', 'the dispatcher asks the owner a question on a raised item; his reply answers it', { text: 'question', named: true, examples: [['a question', 'sk review ask <id> "which tower?" --as orchestrator']] })
|
|
531
|
+
export const ReviewDropCommand = actCommand('drop', 'the dispatcher closes a raised item with the reason (a duplicate, already fixed)', { note: 'required', named: true, examples: [['a duplicate', 'sk review drop <id> --note "same as <other id>" --as orchestrator']] })
|
|
532
|
+
export const ReviewReassignCommand = actCommand('reassign', 'the dispatcher takes a claim back, or sends the item to another topic', { note: 'optional', topic: 'optional', named: true, examples: [['a stale claim', 'sk review reassign <id> --as orchestrator']] })
|
|
533
|
+
|
|
534
|
+
export class ReviewDispatcherCommand extends Command {
|
|
535
|
+
static override paths = [['review', 'dispatcher']]
|
|
536
|
+
static override usage = Command.Usage({
|
|
537
|
+
category: 'Review',
|
|
538
|
+
description: 'take the dispatcher role: one session at a time routes what is raised',
|
|
539
|
+
details: `${DETAILS}\n\nRefused while another holder acted in the last 30 minutes, unless --take. Only in a repo whose review unit declares a dispatcher.`,
|
|
540
|
+
examples: [['take the role', 'sk review dispatcher --as orchestrator']],
|
|
541
|
+
})
|
|
542
|
+
as = Option.String('--as', { description: 'who takes it (else SK_AGENT)' })
|
|
543
|
+
take = Option.Boolean('--take', false, { description: 'take it from an active holder' })
|
|
544
|
+
|
|
545
|
+
async execute(): Promise<number> {
|
|
546
|
+
return run(this.context, async (h) => {
|
|
547
|
+
if (!h.say.spec.dispatcher) throw new Error('this repo routes nothing: its review unit declares no dispatcher (dispatcher: true)')
|
|
548
|
+
const { takeRole } = await import('../review/role.ts')
|
|
549
|
+
const name = actor(this.as, true, 'dispatcher')
|
|
550
|
+
const r = takeRole(h.dir, name, this.take)
|
|
551
|
+
if (!r.ok) throw new Error(r.why)
|
|
552
|
+
h.out(`${name} holds the dispatcher role${r.was && r.was.name !== name ? ` (taken from ${r.was.name})` : ''}`)
|
|
553
|
+
return 0
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export class ReviewNextCommand extends Command {
|
|
559
|
+
static override paths = [['review', 'next']]
|
|
560
|
+
static override usage = Command.Usage({
|
|
561
|
+
category: 'Review',
|
|
562
|
+
description: 'claim the oldest open item of a topic, or raised to a lane, and show it with the pages to read first',
|
|
563
|
+
details: DETAILS,
|
|
564
|
+
examples: [['the next of a topic', 'sk review next lieux --as L12'], ['the next raised to a lane', 'sk review next --lane interaction --as interaction']],
|
|
565
|
+
})
|
|
566
|
+
topic = Option.String({ required: false })
|
|
567
|
+
lane = Option.String('--lane', { description: 'the lane he or a rule raised it to' })
|
|
568
|
+
as = Option.String('--as', { description: 'who claims (else SK_AGENT)' })
|
|
569
|
+
|
|
570
|
+
async execute(): Promise<number> {
|
|
571
|
+
return run(this.context, async (h) => {
|
|
572
|
+
const { claimNext } = await import('../review/store.ts')
|
|
573
|
+
const { stateOf } = await import('../review/model.ts')
|
|
574
|
+
const { detail } = await import('../review/format.ts')
|
|
575
|
+
const lane = this.lane?.trim()
|
|
576
|
+
if (!lane && !this.topic) throw new Error(`next <topic> | next --lane <lane>${Object.keys(h.say.spec.topics).length ? ` (topics: ${Object.keys(h.say.spec.topics).join(', ')})` : ''}`)
|
|
577
|
+
if (this.topic && Object.keys(h.say.spec.topics).length > 0 && !h.say.spec.topics[this.topic]) throw new Error(`no topic « ${this.topic} »: ${Object.keys(h.say.spec.topics).join(', ')}`)
|
|
578
|
+
await pull(h)
|
|
579
|
+
const got = claimNext(h.dir, lane ? { lane } : { topic: this.topic ?? '' }, actor(this.as, true, 'next'))
|
|
580
|
+
if (!got) {
|
|
581
|
+
h.out(`nothing open on ${lane ?? this.topic}`)
|
|
582
|
+
return 0
|
|
583
|
+
}
|
|
584
|
+
const topic = stateOf(got, false).topic
|
|
585
|
+
const read = topic ? h.say.spec.topics[topic]?.read : undefined
|
|
586
|
+
h.out(`${detail(got, h.dir, false, false, h.say)}${read?.length ? `\nread first: ${read.join(', ')}` : ''}`)
|
|
587
|
+
return 0
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export class ReviewTopicsCommand extends Command {
|
|
593
|
+
static override paths = [['review', 'topics']]
|
|
594
|
+
static override usage = Command.Usage({ category: 'Review', description: 'the topics a raised item is routed to, and the pages a worker reads first', details: DETAILS })
|
|
595
|
+
|
|
596
|
+
async execute(): Promise<number> {
|
|
597
|
+
return run(this.context, (h) => {
|
|
598
|
+
const t = Object.entries(h.say.spec.topics)
|
|
599
|
+
if (t.length === 0) h.out('no topic: the repo\'s review unit declares none (topics: {...})')
|
|
600
|
+
for (const [id, v] of t) h.out(`${id.padEnd(11)} ${v.label}${v.covers ? `: ${v.covers}` : ''}${v.read?.length ? `\n${' '.repeat(12)}read: ${v.read.join(', ')}` : ''}`)
|
|
601
|
+
return 0
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export class ReviewBoardCommand extends Command {
|
|
607
|
+
static override paths = [['review', 'board']]
|
|
608
|
+
static override usage = Command.Usage({
|
|
609
|
+
category: 'Review',
|
|
610
|
+
description: 'every open raised item by state, topic and worker, stale claims marked; who is the dispatcher',
|
|
611
|
+
details: DETAILS,
|
|
612
|
+
examples: [['what is open', 'sk review board'], ['one topic, closed ones too', 'sk review board --topic lieux --all']],
|
|
613
|
+
})
|
|
614
|
+
topic = Option.String('--topic', { description: 'one topic' })
|
|
615
|
+
all = Option.Boolean('--all', false, { description: 'the closed ones too' })
|
|
616
|
+
|
|
617
|
+
async execute(): Promise<number> {
|
|
618
|
+
return run(this.context, async (h) => {
|
|
619
|
+
const { readAll } = await import('../review/store.ts')
|
|
620
|
+
const { roleOf } = await import('../review/role.ts')
|
|
621
|
+
const { isMarkItem, isStale, stateOf } = await import('../review/model.ts')
|
|
622
|
+
const { followedIds } = await import('../review/threads.ts')
|
|
623
|
+
if (this.topic && Object.keys(h.say.spec.topics).length > 0 && !h.say.spec.topics[this.topic]) throw new Error(`--topic: one of ${Object.keys(h.say.spec.topics).join(', ')}`)
|
|
624
|
+
await pull(h)
|
|
625
|
+
const all = readAll(h.dir)
|
|
626
|
+
for (const b of all.broken) h.err(`review: FAIL ${b.id}: ${b.error}`)
|
|
627
|
+
if (h.say.spec.dispatcher) {
|
|
628
|
+
const role = roleOf(h.dir)
|
|
629
|
+
h.out(role ? `dispatcher: ${role.name} (since ${role.since.slice(0, 16)})` : 'dispatcher: nobody (sk review dispatcher --as <name>)')
|
|
630
|
+
}
|
|
631
|
+
const now = new Date()
|
|
632
|
+
const followed = followedIds(all.items)
|
|
633
|
+
const rows = all.items
|
|
634
|
+
.filter((x) => x.item.origin.by !== 'agent' && !isMarkItem(x))
|
|
635
|
+
.map((x) => ({ x, st: stateOf(x, followed.has(x.item.id)) }))
|
|
636
|
+
.filter((r) => (this.all || r.st.state !== 'closed') && (this.topic === undefined || r.st.topic === this.topic))
|
|
637
|
+
for (const state of ['new', 'question', 'blocked', 'open', 'claimed', 'closed'] as const) {
|
|
638
|
+
const of = rows.filter((r) => r.st.state === state)
|
|
639
|
+
if (of.length === 0) continue
|
|
640
|
+
h.out(`${state} (${of.length})`)
|
|
641
|
+
for (const { x, st } of of) h.out(` ${x.item.id} ${st.topic ?? ''}${st.worker ? ` ${st.worker}` : ''}${st.why ? ` (${st.why})` : ''}${isStale(x, now) ? ' (stale claim)' : ''} ${x.item.title}`)
|
|
642
|
+
}
|
|
643
|
+
if (rows.length === 0) h.out('nothing open')
|
|
644
|
+
return all.broken.length > 0 ? 1 : 0
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export class ReviewReadyCommand extends Command {
|
|
650
|
+
static override paths = [['review', 'ready']]
|
|
651
|
+
static override usage = Command.Usage({ category: 'Review', description: 'a draft card becomes a card he answers: its last frame, or the image given, stays', details: DETAILS })
|
|
652
|
+
id = Option.String()
|
|
653
|
+
image = Option.String('--image', { description: 'the image that stays' })
|
|
654
|
+
|
|
655
|
+
async execute(): Promise<number> {
|
|
656
|
+
return run(this.context, async (h) => {
|
|
657
|
+
const { readItem, readyItem } = await import('../review/store.ts')
|
|
658
|
+
const live = readItem(h.dir, this.id)?.item.wip?.live
|
|
659
|
+
const r = await readyItem(h.dir, this.id, this.image ? { image: this.image } : {})
|
|
660
|
+
if (live !== undefined) h.unit?.live?.stop?.(live, 'ready')
|
|
661
|
+
h.out(`${r.id} ready: he can answer it`)
|
|
662
|
+
return 0
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export class ReviewDeleteCommand extends Command {
|
|
668
|
+
static override paths = [['review', 'delete']]
|
|
669
|
+
static override usage = Command.Usage({
|
|
670
|
+
category: 'Review',
|
|
671
|
+
description: "a player's test report removed at its source and here, or an item the owner raised removed here",
|
|
672
|
+
details: DETAILS,
|
|
673
|
+
examples: [['a test report', 'sk review delete live#3']],
|
|
674
|
+
})
|
|
675
|
+
refs = Option.Rest({ required: 1 })
|
|
676
|
+
|
|
677
|
+
async execute(): Promise<number> {
|
|
678
|
+
return run(this.context, async (h) => {
|
|
679
|
+
const { deleteRaised, deleteReport, itemOfRef } = await import('../review/store.ts')
|
|
680
|
+
for (const ref of this.refs) {
|
|
681
|
+
const x = itemOfRef(h.dir, ref)
|
|
682
|
+
if (!x) throw new Error(`no item ${ref}`)
|
|
683
|
+
if (x.item.origin.by === 'player') {
|
|
684
|
+
const removers = (h.unit?.sources ?? []).filter((s) => s.remove)
|
|
685
|
+
if (removers.length === 0) throw new Error(`${ref}: no source of the repo's review unit removes a report`)
|
|
686
|
+
for (const s of removers) await s.remove?.(x)
|
|
687
|
+
deleteReport(h.dir, x.item.id)
|
|
688
|
+
h.out(`review: ${ref} deleted at its source and here`)
|
|
689
|
+
} else {
|
|
690
|
+
deleteRaised(h.dir, x.item.id)
|
|
691
|
+
h.out(`review: ${ref} deleted`)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return 0
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export class ReviewMarksCommand extends Command {
|
|
700
|
+
static override paths = [['review', 'marks']]
|
|
701
|
+
static override usage = Command.Usage({
|
|
702
|
+
category: 'Review',
|
|
703
|
+
description: "the owner's marks kept in the store: per subject its latest stars, mark and note",
|
|
704
|
+
details: DETAILS,
|
|
705
|
+
examples: [['every mark', 'sk review marks'], ['one kind, for a script', 'sk review marks --subject song --json']],
|
|
706
|
+
})
|
|
707
|
+
subject = Option.String('--subject', { description: 'one subject kind' })
|
|
708
|
+
json = Option.Boolean('--json', false, { description: 'every subject key, its latest mark and its history count' })
|
|
709
|
+
|
|
710
|
+
async execute(): Promise<number> {
|
|
711
|
+
return run(this.context, async (h) => {
|
|
712
|
+
const { readAll } = await import('../review/store.ts')
|
|
713
|
+
const { marksOf } = await import('../review/marks.ts')
|
|
714
|
+
const all = readAll(h.dir)
|
|
715
|
+
for (const b of all.broken) h.err(`review: FAIL ${b.id}: ${b.error}`)
|
|
716
|
+
const marks = marksOf(all.items, h.say.spec).filter((m) => this.subject === undefined || m.kind === this.subject)
|
|
717
|
+
if (this.json) {
|
|
718
|
+
h.out(JSON.stringify(marks, null, 4))
|
|
719
|
+
return 0
|
|
720
|
+
}
|
|
721
|
+
for (const m of marks) {
|
|
722
|
+
const r = m.rate
|
|
723
|
+
const parts = Object.entries(m.parts).map(([p, s]) => `${p} ${s}★`)
|
|
724
|
+
h.out(`${(r.stars ? `${'★'.repeat(r.stars)}${'☆'.repeat(5 - r.stars)}` : (r.mark ?? '')).padEnd(8)} ${m.key}${parts.length ? ` (${parts.join(', ')})` : ''}${m.text ? ` ${m.text}` : ''}`)
|
|
725
|
+
}
|
|
726
|
+
h.out(`${marks.length} subjects marked`)
|
|
727
|
+
return 0
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export class ReviewAnimCommand extends Command {
|
|
733
|
+
static override paths = [['review', 'anim']]
|
|
734
|
+
static override usage = Command.Usage({
|
|
735
|
+
category: 'Review',
|
|
736
|
+
description: "a card's before and after as a looping animation to send: a cut, a fade or a wipe, mp4 or gif",
|
|
737
|
+
details: `${DETAILS}\n\nsk review anim <id> [--view n] | --before a.png --after b.png. Made once and kept under .shipkit/review/.anims/; --out <file | dir/> copies it out (default .shipkit/tmp/).`,
|
|
738
|
+
examples: [
|
|
739
|
+
['a fade, mp4', 'sk review anim <id>'],
|
|
740
|
+
['a wipe of the second view, gif', 'sk review anim <id> --view 1 --mode wipe --format gif'],
|
|
741
|
+
],
|
|
742
|
+
})
|
|
743
|
+
id = Option.String({ required: false })
|
|
744
|
+
view = Option.String('--view', '0', { description: 'the view, 0 the first' })
|
|
745
|
+
mode = Option.String('--mode', 'fade', { description: 'cut, fade or wipe' })
|
|
746
|
+
format = Option.String('--format', 'mp4', { description: 'mp4 or gif' })
|
|
747
|
+
labels = Option.String('--labels', 'en', { description: 'the corner labels: en, fr or none' })
|
|
748
|
+
before = Option.String('--before', { description: 'an image, with --after: no card' })
|
|
749
|
+
after = Option.String('--after', { description: 'an image, with --before' })
|
|
750
|
+
out = Option.String('--out', { description: 'where it goes' })
|
|
751
|
+
|
|
752
|
+
async execute(): Promise<number> {
|
|
753
|
+
return run(this.context, async (h) => {
|
|
754
|
+
const { copyFileSync, mkdirSync, statSync } = await import('node:fs')
|
|
755
|
+
const { dirname, join, resolve } = await import('node:path')
|
|
756
|
+
const { ANIM_FORMATS, ANIM_LABELS, ANIM_MODES, fileAnimation, isAnimFormat, isAnimLabels, isAnimMode, pairAnimation } = await import('../review/anim.ts')
|
|
757
|
+
const pair = this.before && this.after && !this.id ? { before: this.before, after: this.after } : undefined
|
|
758
|
+
if (!pair && (this.before || this.after || !this.id)) throw new Error('anim (<id> [--view n] | --before a.png --after b.png) [--mode cut|fade|wipe] [--format mp4|gif] [--labels en|fr|none] [--out f]')
|
|
759
|
+
const { mode, format, labels } = this
|
|
760
|
+
if (!isAnimMode(mode)) throw new Error(`--mode ${mode}: one of ${ANIM_MODES.join(', ')}`)
|
|
761
|
+
if (!isAnimFormat(format)) throw new Error(`--format ${format}: one of ${ANIM_FORMATS.join(', ')}`)
|
|
762
|
+
if (!isAnimLabels(labels)) throw new Error(`--labels ${labels}: one of ${ANIM_LABELS.join(', ')}`)
|
|
763
|
+
const root = dirname(dirname(h.dir))
|
|
764
|
+
const font = h.unit?.anim?.font ? join(root, h.unit.anim.font) : undefined
|
|
765
|
+
const ask = { view: Number(this.view) || 0, mode, format, labels, ...(font ? { font } : {}) }
|
|
766
|
+
const a = pair ? await fileAnimation(pair.before, pair.after, ask, join(root, '.shipkit/tmp/anims')) : await pairAnimation(h.dir, await idOf(h.dir, this.id ?? ''), ask)
|
|
767
|
+
const o = this.out ?? join(root, '.shipkit/tmp/')
|
|
768
|
+
const to = o.endsWith('/') ? join(o, a.name) : o
|
|
769
|
+
mkdirSync(dirname(resolve(to)), { recursive: true })
|
|
770
|
+
copyFileSync(a.file, to)
|
|
771
|
+
h.out(`${to} ${(statSync(to).size / 1e6).toFixed(2)} MB, ${a.ms ? `${a.ms} ms` : 'cached'}`)
|
|
772
|
+
return 0
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export class ReviewAutoCommand extends Command {
|
|
778
|
+
static override paths = [['review', 'auto']]
|
|
779
|
+
static override usage = Command.Usage({
|
|
780
|
+
category: 'Review',
|
|
781
|
+
description: "the repo's auto rules: period, last run and answer, last item, next run; `auto run <id>` runs one",
|
|
782
|
+
details: `${DETAILS}\n\nsk review auto | sk review auto run <id> [--dry]. The hub's daemon runs them on their clocks.`,
|
|
783
|
+
examples: [['every rule', 'sk review auto'], ['what one would raise', 'sk review auto run veille --dry']],
|
|
784
|
+
})
|
|
785
|
+
verb = Option.String({ required: false })
|
|
786
|
+
rule = Option.String({ required: false })
|
|
787
|
+
dry = Option.Boolean('--dry', false, { description: 'print what it would raise or hold, write nothing' })
|
|
788
|
+
|
|
789
|
+
async execute(): Promise<number> {
|
|
790
|
+
return run(this.context, async (h) => {
|
|
791
|
+
const { autoLogDir, nextRun, readAutoLog, runRule } = await import('../review/auto.ts')
|
|
792
|
+
const { readAll } = await import('../review/store.ts')
|
|
793
|
+
const rules = h.unit?.auto ?? []
|
|
794
|
+
if (this.verb === undefined) {
|
|
795
|
+
if (rules.length === 0) h.out("no auto rule: the repo's review unit declares none (auto: [...])")
|
|
796
|
+
const now = Date.now()
|
|
797
|
+
const ago = (t: number): string => `${Math.round(Math.abs(now - t) / 60_000)} min`
|
|
798
|
+
for (const r of rules) {
|
|
799
|
+
const log = readAutoLog(h.dir, r.id)
|
|
800
|
+
const next = nextRun(r, log, now)
|
|
801
|
+
h.out(
|
|
802
|
+
`${r.id} every ${r.every} ${log ? `ran ${ago(Date.parse(log.at))} ago (${log.ms.toFixed(1)} ms): ${log.answer}${log.error ? `: ${log.error}` : ''}` : 'never ran'}` +
|
|
803
|
+
`${log?.lastRaised ? ` last item ${log.lastRaised.id}` : ''} next ${next <= now ? 'due now' : `in ${ago(next)}`}`,
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
if (rules.length > 0) h.out(`logs: ${autoLogDir(h.dir)}; the hub's daemon runs them`)
|
|
807
|
+
return 0
|
|
808
|
+
}
|
|
809
|
+
if (this.verb !== 'run' || !this.rule) throw new Error('auto | auto run <id> [--dry]')
|
|
810
|
+
const rule = rules.find((r) => r.id === this.rule)
|
|
811
|
+
if (!rule) throw new Error(`no auto rule « ${this.rule} »: ${rules.map((r) => r.id).join(', ') || 'none declared'}`)
|
|
812
|
+
const outcomes = await runRule(h.dir, rule, { items: readAll(h.dir).items, dry: this.dry })
|
|
813
|
+
if (outcomes.length === 0) h.out(`${rule.id}: nothing to raise`)
|
|
814
|
+
for (const o of outcomes)
|
|
815
|
+
h.out(
|
|
816
|
+
'raised' in o
|
|
817
|
+
? `${rule.id}: raised ${o.raised} to ${o.raise.lane}: ${o.raise.title}`
|
|
818
|
+
: 'held' in o
|
|
819
|
+
? `${rule.id}: held by ${o.held} (key ${o.key}, still open): ${o.raise.title}`
|
|
820
|
+
: `${rule.id}: would raise to ${o.raise.lane} (key ${o.key}): ${o.raise.title}\n${o.raise.what}`,
|
|
821
|
+
)
|
|
822
|
+
return 0
|
|
823
|
+
})
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export class ReviewWatchCommand extends Command {
|
|
828
|
+
static override paths = [['review', 'watch']]
|
|
829
|
+
static override usage = Command.Usage({
|
|
830
|
+
category: 'Review',
|
|
831
|
+
description: 'one line per verdict, reply or raised note as it lands; every unseen one at start; never exits',
|
|
832
|
+
details: `${DETAILS}\n\nRun it under a monitor, unfiltered (a grep holds a burst's last line). It registers itself, so the hub shows who listens. An item nobody takes prints 2 minutes after its last message, then every 2 hours.`,
|
|
833
|
+
examples: [['the cards this session sent', 'sk review watch --mine'], ["one lane's items", 'sk review watch --lane interaction']],
|
|
834
|
+
})
|
|
835
|
+
mine = Option.Boolean('--mine', false, { description: "only this Claude Code session's cards" })
|
|
836
|
+
session = Option.String('--session', { description: "only that session's cards" })
|
|
837
|
+
lane = Option.String('--lane', { description: "only that lane's items" })
|
|
838
|
+
|
|
839
|
+
async execute(): Promise<number> {
|
|
840
|
+
return run(this.context, async (h) => {
|
|
841
|
+
const { existsSync, mkdirSync, watch } = await import('node:fs')
|
|
842
|
+
const { readAll, scanStamp } = await import('../review/store.ts')
|
|
843
|
+
const { AGENT_RE, coversCard, laneOf, UNTAKEN_AGAIN_MS, untakenOf } = await import('../review/model.ts')
|
|
844
|
+
const { followedIds, unseen } = await import('../review/threads.ts')
|
|
845
|
+
const { raisedLine, untakenLine, watchLines } = await import('../review/format.ts')
|
|
846
|
+
const { liveWatchers, registerWatch } = await import('../review/watchers.ts')
|
|
847
|
+
const session = await sessionOf(this.mine, this.session)
|
|
848
|
+
const lane = this.lane?.trim() || null
|
|
849
|
+
if (lane !== null && !AGENT_RE.test(lane)) throw new Error(`--lane: « ${lane} » is no lane id (${AGENT_RE.source})`)
|
|
850
|
+
const mine = (x: Parameters<typeof coversCard>[1]): boolean => coversCard({ session, lane }, x)
|
|
851
|
+
registerWatch(h.dir, { session, lane })
|
|
852
|
+
const parent = process.ppid
|
|
853
|
+
const shown = new Set<string>()
|
|
854
|
+
const tick = (): void => {
|
|
855
|
+
const all = readAll(h.dir)
|
|
856
|
+
for (const b of all.broken) {
|
|
857
|
+
const key = `broken ${b.id} ${b.error}`
|
|
858
|
+
if (!shown.has(key)) h.err(`review: FAIL ${b.id}: ${b.error}`)
|
|
859
|
+
shown.add(key)
|
|
860
|
+
}
|
|
861
|
+
for (const x of unseen(all.items).filter(mine))
|
|
862
|
+
for (const l of watchLines(x, all.items)) {
|
|
863
|
+
if (shown.has(l.key)) continue
|
|
864
|
+
shown.add(l.key)
|
|
865
|
+
h.out(l.text)
|
|
866
|
+
}
|
|
867
|
+
for (const x of all.items.filter(mine)) {
|
|
868
|
+
const l = raisedLine(x)
|
|
869
|
+
if (l === null || shown.has(l.key)) continue
|
|
870
|
+
shown.add(l.key)
|
|
871
|
+
h.out(l.text)
|
|
872
|
+
}
|
|
873
|
+
// an item nobody takes: a --lane watch hears its own lane's, an unfiltered or --mine watch every one
|
|
874
|
+
const now = new Date()
|
|
875
|
+
const followed = followedIds(all.items)
|
|
876
|
+
const watchers = liveWatchers(h.dir)
|
|
877
|
+
for (const x of all.items) {
|
|
878
|
+
if (lane !== null && laneOf(x) !== lane) continue
|
|
879
|
+
const why = untakenOf(x, followed.has(x.item.id), watchers, now, h.say.spec.dispatcher)
|
|
880
|
+
if (why === null) continue
|
|
881
|
+
const round = Math.floor((now.getTime() - Date.parse(x.thread.at(-1)?.at ?? x.item.created)) / UNTAKEN_AGAIN_MS)
|
|
882
|
+
const key = `untaken ${x.item.id} ${why} ${round}`
|
|
883
|
+
if (shown.has(key)) continue
|
|
884
|
+
shown.add(key)
|
|
885
|
+
h.out(untakenLine(x, why, lane))
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
tick()
|
|
889
|
+
// the grace and the reminder pass with time alone: one read a minute
|
|
890
|
+
setInterval(tick, 60_000)
|
|
891
|
+
// a change under the folder asks again (debounced); a cheap stat stamp every 5 s catches a missed event
|
|
892
|
+
let soon: ReturnType<typeof setTimeout> | null = null
|
|
893
|
+
const again = (): void => {
|
|
894
|
+
if (soon) clearTimeout(soon)
|
|
895
|
+
soon = setTimeout(() => {
|
|
896
|
+
soon = null
|
|
897
|
+
tick()
|
|
898
|
+
}, 150)
|
|
899
|
+
}
|
|
900
|
+
if (!existsSync(h.dir)) mkdirSync(h.dir, { recursive: true })
|
|
901
|
+
watch(h.dir, { recursive: true }, again)
|
|
902
|
+
// the sources: pulled now and every 30 s, a new item lands and the tick prints it
|
|
903
|
+
await pull(h)
|
|
904
|
+
if (h.unit?.sources?.length) setInterval(() => void pull(h), 30_000)
|
|
905
|
+
let stamp = scanStamp(h.dir)
|
|
906
|
+
setInterval(() => {
|
|
907
|
+
// a watch whose parent died (a monitor's timeout) exits
|
|
908
|
+
if (process.ppid !== parent) process.exit(0)
|
|
909
|
+
const now = scanStamp(h.dir)
|
|
910
|
+
if (now !== stamp) again()
|
|
911
|
+
stamp = now
|
|
912
|
+
}, 5000)
|
|
913
|
+
return new Promise<number>(() => {})
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export const REVIEW_COMMANDS = [
|
|
919
|
+
ReviewLsCommand,
|
|
920
|
+
ReviewAddCommand,
|
|
921
|
+
ReviewAddViewCommand,
|
|
922
|
+
ReviewShowCommand,
|
|
923
|
+
ReviewInboxCommand,
|
|
924
|
+
ReviewSeenCommand,
|
|
925
|
+
ReviewReplyCommand,
|
|
926
|
+
ReviewWithdrawCommand,
|
|
927
|
+
ReviewReopenCommand,
|
|
928
|
+
ReviewFixedCommand,
|
|
929
|
+
ReviewWontfixCommand,
|
|
930
|
+
ReviewClaimCommand,
|
|
931
|
+
ReviewReleaseCommand,
|
|
932
|
+
ReviewBlockedCommand,
|
|
933
|
+
ReviewDoneCommand,
|
|
934
|
+
ReviewDispatcherCommand,
|
|
935
|
+
ReviewDispatchCommand,
|
|
936
|
+
ReviewAskCommand,
|
|
937
|
+
ReviewDropCommand,
|
|
938
|
+
ReviewReassignCommand,
|
|
939
|
+
ReviewNextCommand,
|
|
940
|
+
ReviewTopicsCommand,
|
|
941
|
+
ReviewBoardCommand,
|
|
942
|
+
ReviewReadyCommand,
|
|
943
|
+
ReviewDeleteCommand,
|
|
944
|
+
ReviewMarksCommand,
|
|
945
|
+
ReviewAnimCommand,
|
|
946
|
+
ReviewAutoCommand,
|
|
947
|
+
ReviewWatchCommand,
|
|
948
|
+
]
|