rvlib-shipkit 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -2
- package/package.json +2 -2
- package/src/assets/kits/identity-kit.ts +5 -2
- package/src/cli/cmd_Commands.ts +30 -5
- package/src/cli/cmd_FleetPush.ts +47 -0
- package/src/cli/cmd_GlobalProcesses.ts +5 -5
- package/src/cli/cmd_Review.ts +52 -11
- package/src/cli/cmd_Sound.ts +209 -0
- package/src/cli/cmd_Trailer.ts +11 -3
- package/src/cli/family-pages.ts +49 -0
- package/src/cli/kit/builtins.ts +1 -0
- package/src/cli/kit/verb.ts +11 -3
- package/src/cli/registry.ts +137 -0
- package/src/cli.ts +47 -133
- package/src/core/app-icon.ts +10 -6
- package/src/core/last-good.ts +217 -0
- package/src/core/load-probe.ts +71 -0
- package/src/core/reexec.ts +5 -3
- package/src/fleet/push-fleet.ts +80 -0
- package/src/home/home-routes.ts +12 -2
- package/src/home/lib-watch.ts +110 -20
- package/src/home/process-rows.ts +4 -2
- package/src/home/run-daemon.ts +1 -1
- package/src/repo-config.ts +3 -0
- package/src/review/format.ts +11 -3
- package/src/review/model.ts +66 -5
- package/src/review/paste.ts +30 -0
- package/src/review/project.ts +20 -4
- package/src/review/routes.ts +22 -4
- package/src/review/store.ts +15 -6
- package/src/review/unit.ts +19 -1
- package/src/scaffold/files/agent-index.md +2 -2
- package/src/scaffold/project-scaffold.ts +2 -2
- package/src/serve/hub-app/activity-st.ts +31 -0
- package/src/serve/hub-app/activity-view.tsx +9 -0
- package/src/serve/hub-app/activity.css +6 -0
- package/src/serve/hub-app/admin-svc.ts +1 -1
- package/src/serve/hub-app/admin-view.tsx +1 -1
- package/src/serve/hub-app/hash-route.ts +12 -4
- package/src/serve/hub-app/home-st.ts +7 -1
- package/src/serve/hub-app/home-strip.tsx +3 -1
- package/src/serve/hub-app/hub.css +38 -1
- package/src/serve/hub-app/icons.tsx +6 -0
- package/src/serve/hub-app/layout-st.ts +2 -2
- package/src/serve/hub-app/machine-st.ts +1 -1
- package/src/serve/hub-app/md.ts +32 -0
- package/src/serve/hub-app/project-list.css +9 -7
- package/src/serve/hub-app/project-list.tsx +5 -2
- package/src/serve/hub-app/projects-svc.ts +3 -2
- package/src/serve/hub-app/repo-st.ts +22 -7
- package/src/serve/hub-app/review/clip-st.ts +57 -7
- package/src/serve/hub-app/review/kind-view.tsx +78 -9
- package/src/serve/hub-app/review/review-answer.tsx +67 -58
- package/src/serve/hub-app/review/review-page.tsx +17 -15
- package/src/serve/hub-app/review/review-st.ts +191 -20
- package/src/serve/hub-app/review/review-views.tsx +154 -9
- package/src/serve/hub-app/review.css +73 -13
- package/src/serve/hub-app/wiki-st.ts +190 -0
- package/src/serve/hub-app/wikis-view.tsx +134 -5
- package/src/serve/hub-runtime.ts +4 -4
- package/src/serve/project-gutter-route.ts +25 -1
- package/src/serve/project-gutter.ts +1 -1
- package/src/serve/run-serve.ts +25 -0
- package/src/serve/serve-hub.ts +15 -1
- package/src/serve/stale-restart.ts +19 -2
- package/src/shot/client.ts +17 -5
- package/src/shot/shooter.ts +19 -3
- package/src/shot/shot-ask.ts +9 -1
- package/src/shot/stepped-clip.ts +53 -0
- package/src/trailer/adapter.ts +33 -4
- package/src/trailer/build.ts +182 -33
- package/src/trailer/ffmpeg.ts +21 -1
- package/src/trailer/fonts.ts +55 -0
- package/src/trailer/footage.ts +4 -4
- package/src/trailer/grammar.ts +429 -67
- package/src/trailer/graph.ts +130 -56
- package/src/trailer/look.ts +3 -0
- package/src/trailer/motion.ts +207 -0
- package/src/trailer/plan.ts +2 -2
- package/src/trailer/prune.ts +13 -2
- package/src/trailer/score/compose.ts +18 -2
- package/src/trailer/score/gm-render.ts +43 -0
- package/src/trailer/score/gm.ts +90 -0
- package/src/trailer/score/midi.ts +143 -0
- package/src/trailer/score/pattern.ts +13 -6
- package/src/trailer/score/score.ts +28 -14
- package/src/trailer/score/sound.ts +12 -1
- package/src/trailer/score/synth.ts +24 -2
- package/src/trailer/sound/audio.ts +148 -0
- package/src/trailer/sound/brand.ts +42 -0
- package/src/trailer/sound/comfy.ts +38 -0
- package/src/trailer/sound/eleven.ts +72 -0
- package/src/trailer/sound/fluid.ts +71 -0
- package/src/trailer/sound/gen.ts +139 -0
- package/src/trailer/sound/index.ts +60 -0
- package/src/trailer/sound/presets.ts +69 -0
- package/src/trailer/sound/run.ts +189 -0
- package/src/trailer/titles.ts +451 -235
- package/src/trailer/url.ts +18 -3
- package/src/trailer/vocab.ts +12 -1
- package/src/trailer/words.ts +29 -8
- package/src/wiki/graph-mount.ts +3 -0
- package/src/wiki/graph-page.ts +45 -9
- package/src/wiki/graph-pages.ts +128 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2.5.0
|
|
2
|
+
|
|
3
|
+
- **A review card hands itself to any agent session.** A small copy icon on the card's header (or C) copies one block: its title and state, its repo, `cd <repo> && sk review show <id>`, its link here and on the phone when the repo has a public host; a toast says what was copied. `sk review show <id> --paste` prints the same block.
|
|
4
|
+
- **The overall comment box sits under the card's header**, after the verdicts, on every card and in every layout.
|
|
5
|
+
- **Push every unpushed repo: `sk fleet push` and a button on the activity page.** Each house repo with commits its remote lacks gets a plain `git push`, never a force; a branch with no upstream goes `-u` to its one remote; a branch that is also behind, a gone upstream or a detached head is skipped with the reason. `--dry` plans only.
|
|
6
|
+
|
|
7
|
+
- **A new repo's agent/index.md routes with `→`.** The scaffold's table rows, the project CLI's row and its `next:` hint write `→ agent/<page>.md`, the wiki's form for a page to open, instead of a backticked path.
|
|
8
|
+
|
|
1
9
|
## 2.3.0
|
|
2
10
|
|
|
3
11
|
- **A sound is a review view.** `sk review add --audio <sound>`, a sound in `--view`, `add-view <id> "<label>" --audio <sound>`, or a before and after of two sounds: the store keeps the sound and draws its waveform (`wave.png`, `before-wave.png`), and the hub shows each waveform over a player, one row a side. A sound takes reactions and words, never a rectangle; a pair is never a sound and an image.
|
|
@@ -15,8 +23,17 @@
|
|
|
15
23
|
- **`sk time` shows turn count, what makes agents slow when no command is.** Every call costs a model turn before it. The summary gives, for the main sessions and for the sub-agents, the model time before each call, the share of single read calls and the share sent in parallel. The sub-agent lines lead with a verdict (turn count or command speed, with a fix line when reads pile up one per turn), then their work split (model, tools, their own waits), run length p50 and p90, and calls per run. The `turns` view lists the runs that worked longest with their turns, read and parallel shares and model per turn, then the read shapes by the model time of the turns that issued them. A read is Read, Grep, Glob, or a Bash command of a read shape whose every part reads (a redirect to a file, `sed -i`, `find -delete` or `-exec`, `tee`, or a second command that writes or runs makes it not one), judged when a call is indexed and again when the rules change (`src/agent-time/turns.ts`). A call is parallel when it starts while the previous call still runs, within 2 s of its start. The model stretch before a call comes from the same sweep that cuts the wall. pkm-multi, every recorded session: sub-agents worked 905.4h, model 67%, 7.7 s of model before each of 283,821 calls, 51% single reads, 2.3% in parallel.
|
|
16
24
|
- **A message relayed into a sub-agent ends its park.** The harness writes "The coordinator sent a message while you were working" (or the user, or another session) into a sub-agent as an `isMeta` user entry with no peer origin, which the transcript reader dropped, so a sub-agent that handed back and sat for hours until someone wrote to it counted those hours as model: 352h of pkm-multi's sub-agent time. It is now a `message` event that counts as a prompt (idle past 30 minutes). The index version moves to 6, so every file is read again once.
|
|
17
25
|
|
|
18
|
-
##
|
|
19
|
-
|
|
26
|
+
## 2.4.0
|
|
27
|
+
|
|
28
|
+
- **The wiki preview opens on the index, beside the graph on the left, right or bottom.** A switch of three icons in the preview's head places it, kept per browser; buttons open the page in VS Code and copy its path. Refs read by kind: on demand (`→`, a link, a path in a code span, written `→ path` whichever the page used) in orange, loaded with the page (`@`) in blue and bold. A page that names `agent/spec.md` opens the agent/ graph's `spec.md`.
|
|
29
|
+
- **The wikis page searches and previews.** A search bar over the graph finds every page by title and full text (`/graph/<id>/search`), a dropdown the arrows walk; enter, or a click on a ball, opens the page in a resizable panel on the right (`/graph/<id>/page`), whose refs open their page and focus its ball. The graph follows the hub's theme. `serve.wikiGraph.dir` names the folder a custom wiki's node ids are relative to (default the repo root).
|
|
30
|
+
- **A choice card.** `sk review add --choice --view "voice A" a.flac --note "low" --view "voice B" b.flac`: every view is an option he scores (1 to 5 stars) and picks one of. His pick is an approval carrying `pick`; `inbox` and `watch` print `picked [voice B]` and each score. An approval with reactions or a pick is no longer counted as a plain one by `inbox`.
|
|
31
|
+
- **Options as thin rows.** A choice, and any card of two clips or sounds or more, shows its views one below the other: ▶, the label, a strip that scrubs it, the time, then stars and « pick » (a choice) or OK, fix, no (a group), ✎ a word, ⤢ the full view. Keys 1 to 9 play a row, ⇧1 to ⇧5 score it, P picks it. « rows | full » switches back to every view stacked.
|
|
32
|
+
- **One clip or sound plays at a time on the whole page.** Any audio or video that starts pauses the one playing (the review page, the trailer page, the atelier's rolls). A card's first clip loops alone; the others wait on their first frame.
|
|
33
|
+
- **The card's header reads.** The title, then one line: the list's own pill, the lane, when it was sent or answered, the commit, an id to copy. No subject row, no id row, no banner. `top` is one column from the top: the header and the verdicts, the views, then the answer. A link to a card on a phone opens the card.
|
|
34
|
+
- **An svg mark is a repo's icon.** `.shipkit/identity/icon.svg` comes right after `icon.png` for the project list's icon and for the app icon, which draws it on the identity plate like a png.
|
|
35
|
+
- **The hub's pages read from the left.** Every page card of a repo (admin, checks, tests, plan, git, when, processes, status, readme) and a review card's pictures start at the left of their pane instead of floating centred; a guard keeps it.
|
|
36
|
+
- **The rail draws bigger icons: each repo's app icon.** The sizes are 24, 32 and 40px (S, M, L) with 6px a side, the rail 36, 44 or 52px; the ask badge sits on the icon's top right corner. Folded, a row wears the repo's app icon, its mark on the identity plate the admin page previews, cropped to the plate (`appIcon` on `/api/projects.json`, `appIconPngs(…, 'plate')`, drawn once per mark and colours); a repo with no png mark keeps its plain icon. A repo closed in VS Code dims its name, never its icon.
|
|
20
37
|
- **The rail's icons come in three sizes.** Folded, the list's foot shows an S, M or L button: 18, 24 or 32px icons, the rail 44, 50 or 58px wide to fit, kept per browser (`hub:rail-icon`). M is the default.
|
|
21
38
|
- **The project list folds to an icon rail.** ⌘B, the fold button at the list's foot, or a drag of its separator below the list's min folds it to 44px of icons: the names, the ⌘ hints and the step hint hide, a badge rides its icon's corner, the hover card still names the row. A drag out or ⌘B again unfolds it to its kept width; the fold is kept per browser (`hub:list-rail`). The dock moves to ⌘⇧B.
|
|
22
39
|
- **⌘; opens the insights**, its filter focused, so a package name is one key and a word away.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"funding": "https://github.com/sponsors/rvion",
|
|
5
5
|
"description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@floating-ui/react": "^0.27.20",
|
|
39
|
-
"@napi-rs/canvas": "^1.0
|
|
39
|
+
"@napi-rs/canvas": "^1.1.0",
|
|
40
40
|
"clipanion": "^4.0.0-rc.4",
|
|
41
41
|
"comfy-ts": "^2.9.0",
|
|
42
42
|
"ink": "^7.1.1",
|
|
@@ -16,6 +16,7 @@ import { dirname, join } from 'path'
|
|
|
16
16
|
import sharp from 'sharp'
|
|
17
17
|
import { defineKind, type CanvasGen, type ComfyGen, type Kind } from '../kit-config.ts'
|
|
18
18
|
import { HOUSE_FAMILIES, type TrailerLookInput } from '../../trailer/look.ts'
|
|
19
|
+
import type { TrailerSoundInput } from '../../trailer/sound/brand.ts'
|
|
19
20
|
|
|
20
21
|
export type Brand = {
|
|
21
22
|
name: string
|
|
@@ -31,8 +32,10 @@ export type Brand = {
|
|
|
31
32
|
* name/tagline right); the hook owns compositing the mark if it wants one */
|
|
32
33
|
drawOg?: (ctx: SKRSContext2D, brand: Brand, size: { w: number; h: number }) => void
|
|
33
34
|
/** the look every trailer of the repo wears (titles, card, face, logo);
|
|
34
|
-
* each value left out is derived from the palette (trailer/look.ts)
|
|
35
|
-
|
|
35
|
+
* each value left out is derived from the palette (trailer/look.ts);
|
|
36
|
+
* its sound section (`comfy`, `elevenKey`, `voice`, `voices`) makes the
|
|
37
|
+
* voice, sfx and music lines (trailer/sound/brand.ts) */
|
|
38
|
+
trailer?: TrailerLookInput & TrailerSoundInput
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export const FAVICON_SIZE = 512
|
package/src/cli/cmd_Commands.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// (the kit in cli/kit/) come out of these functions.
|
|
6
6
|
import { Command, Option } from 'clipanion'
|
|
7
7
|
import { spliceMarkedBlock } from '../core/doc-block.ts'
|
|
8
|
+
import { FAMILY_PAGES } from './family-pages.ts'
|
|
8
9
|
import { type Essential, type Film, marksOf, ROW_OF, runText } from './kit/verb.ts'
|
|
9
10
|
|
|
10
11
|
export type CommandOption = { definition: string; description: string }
|
|
@@ -20,6 +21,8 @@ export type CommandRow = {
|
|
|
20
21
|
/** a filmable command's take version and length (build/trailer.md, Film lines) */
|
|
21
22
|
version?: number | string
|
|
22
23
|
length?: 'own'
|
|
24
|
+
words?: Record<string, unknown>
|
|
25
|
+
stamp?: string[]
|
|
23
26
|
/** listed on the essentials page */
|
|
24
27
|
essential?: Essential
|
|
25
28
|
/** what a kit row runs */
|
|
@@ -81,6 +84,8 @@ export function commandRows(plain: Definition[], detailed: Definition[] = [], bi
|
|
|
81
84
|
if (m.film != null) row.film = m.film
|
|
82
85
|
if (m.version != null) row.version = m.version
|
|
83
86
|
if (m.length != null) row.length = m.length
|
|
87
|
+
if (m.words != null) row.words = m.words
|
|
88
|
+
if (m.stamp != null) row.stamp = m.stamp
|
|
84
89
|
if (m.essential != null) row.essential = m.essential
|
|
85
90
|
const v = ROW_OF.get(c)
|
|
86
91
|
if (v != null) row.runs = runText(v.run)
|
|
@@ -113,12 +118,13 @@ export function renderMarkdown(rows: CommandRow[], bin = 'sk'): string {
|
|
|
113
118
|
|
|
114
119
|
/** the eager form: one line per command, its positional arguments and its
|
|
115
120
|
* description. Flags and examples are `<bin> <command> --help`, so a page every
|
|
116
|
-
* session loads costs a line per command, not a screen
|
|
117
|
-
|
|
121
|
+
* session loads costs a line per command, not a screen. `pages` puts each
|
|
122
|
+
* family's owner page in its heading: `### Trailer → build/trailer.md` */
|
|
123
|
+
export function renderCompact(rows: CommandRow[], bin = 'sk', pages: Readonly<Record<string, string>> | null = null): string {
|
|
118
124
|
const out: string[] = []
|
|
119
125
|
let cat = ''
|
|
120
126
|
for (const r of rows) {
|
|
121
|
-
if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}\n`) }
|
|
127
|
+
if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}${pages?.[cat] != null ? ` → ${pages[cat]}` : ''}\n`) }
|
|
122
128
|
const sig = r.usage.replace(/^\$0/, bin).replace(/\s+\[(?:-|<\w+>)[^\]]*\]/g, '').trim() || `${bin} ${r.path}`
|
|
123
129
|
out.push(`- \`${sig}\` — ${r.description}`)
|
|
124
130
|
}
|
|
@@ -150,6 +156,15 @@ export function renderEssentials(rows: CommandRow[], bin: string, command: strin
|
|
|
150
156
|
return `${out.join('\n')}\n`
|
|
151
157
|
}
|
|
152
158
|
|
|
159
|
+
/** a family with no owner page, and a page for a family no command has: one line each */
|
|
160
|
+
export function familyProblems(rows: CommandRow[], pages: Readonly<Record<string, string>>): string[] {
|
|
161
|
+
const families = new Set(rows.map((r) => r.category))
|
|
162
|
+
return [
|
|
163
|
+
...[...families].filter((f) => pages[f] == null).map((f) => `family ${f}: no owner page in src/cli/family-pages.ts`),
|
|
164
|
+
...Object.keys(pages).filter((f) => !families.has(f)).map((f) => `family ${f}: src/cli/family-pages.ts names a page for a family no command has`),
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
/** what a project CLI's registry must hold for its pages and its listing to
|
|
154
169
|
* teach right: a category, a description under the cap, and an example that
|
|
155
170
|
* starts with the command's own words. One line per breach */
|
|
@@ -205,6 +220,9 @@ export class CommandsCommand extends Command {
|
|
|
205
220
|
* a breach), and its --out creates a missing page */
|
|
206
221
|
static shapeRule = false
|
|
207
222
|
|
|
223
|
+
/** each family's owner page, named in the compact page's headings; a project CLI has none */
|
|
224
|
+
static familyPages: Readonly<Record<string, string>> | null = FAMILY_PAGES
|
|
225
|
+
|
|
208
226
|
json = Option.Boolean('--json', false, { description: 'machine-readable rows, with film and essential where a command carries them' })
|
|
209
227
|
out = Option.String('--out', { description: 'host file carrying the <bin>:commands markers. The block between them is rewritten' })
|
|
210
228
|
check = Option.Boolean('--check', false, { description: 'with --out or --essentials: exit 1 if a page is stale, write nothing' })
|
|
@@ -221,10 +239,17 @@ export class CommandsCommand extends Command {
|
|
|
221
239
|
classes,
|
|
222
240
|
)
|
|
223
241
|
const write = (s: string): void => void this.context.stdout.write(s)
|
|
242
|
+
const pages = (this.constructor as typeof CommandsCommand).familyPages
|
|
243
|
+
const render = (): string => (this.compact ? renderCompact(rows, bin, pages) : renderMarkdown(rows, bin))
|
|
224
244
|
if (this.out == null && this.essentials == null) {
|
|
225
|
-
write(this.json ? `${JSON.stringify(rows)}\n` : `${(
|
|
245
|
+
write(this.json ? `${JSON.stringify(rows)}\n` : `${render()}\n`)
|
|
226
246
|
return 0
|
|
227
247
|
}
|
|
248
|
+
if (this.out != null && this.compact && pages != null) {
|
|
249
|
+
const bad = familyProblems(rows, pages)
|
|
250
|
+
for (const b of bad) write(`FAIL: ${b}\n`)
|
|
251
|
+
if (bad.length > 0) { write(' fix: one row per family in src/cli/family-pages.ts, the wiki page that owns it\n'); return 1 }
|
|
252
|
+
}
|
|
228
253
|
if ((this.constructor as typeof CommandsCommand).shapeRule) {
|
|
229
254
|
const bad = shapeProblems(rows, bin)
|
|
230
255
|
for (const b of bad) write(`FAIL: ${b}\n`)
|
|
@@ -246,7 +271,7 @@ export class CommandsCommand extends Command {
|
|
|
246
271
|
const project = (this.constructor as typeof CommandsCommand).shapeRule
|
|
247
272
|
if (!existsSync(this.out) && (!project || this.check)) { write(`FAIL: ${this.out} does not exist\n`); return 1 }
|
|
248
273
|
const host = existsSync(this.out) ? readFileSync(this.out, 'utf8') : newCommandsPage(bin, this.out)
|
|
249
|
-
const next = spliceBlock(host, (
|
|
274
|
+
const next = spliceBlock(host, render(), bin)
|
|
250
275
|
if (next == null) { write(`FAIL: ${this.out} has no ${begin} / ${end} markers\n`); return 1 }
|
|
251
276
|
if (next === host) { write(`ok: ${this.out} command index current (${rows.length} commands)\n`); return code }
|
|
252
277
|
if (this.check) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// shipkit fleet push — every house repo that holds commits its remote does
|
|
2
|
+
// not, pushed: a plain git push, never a force (fleet/push-fleet.ts, the same
|
|
3
|
+
// path as the activity page's push button). A repo that fell behind waits for
|
|
4
|
+
// a pull, and says so.
|
|
5
|
+
import { Command, Option } from 'clipanion'
|
|
6
|
+
import { homedir } from 'os'
|
|
7
|
+
import { gitRoot } from '../core/git-root.ts'
|
|
8
|
+
import { pushFleet } from '../fleet/push-fleet.ts'
|
|
9
|
+
import { houseRepos } from '../serve/project-gutter.ts'
|
|
10
|
+
import { listFlag } from './list-flag.ts'
|
|
11
|
+
|
|
12
|
+
export class FleetPushCommand extends Command {
|
|
13
|
+
static override paths = [['fleet', 'push']]
|
|
14
|
+
|
|
15
|
+
static override usage = Command.Usage({
|
|
16
|
+
category: 'Health',
|
|
17
|
+
description: 'push every house repo with unpushed commits: a plain git push, never a force',
|
|
18
|
+
examples: [
|
|
19
|
+
['push what is unpushed, everywhere', 'sk fleet push'],
|
|
20
|
+
['what would go out', 'sk fleet push --dry'],
|
|
21
|
+
['some repos only', 'sk fleet push --only my-app --only other'],
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
dry = Option.Boolean('--dry', false, { description: 'plan only: say what would be pushed and what is skipped, push nothing' })
|
|
26
|
+
only = Option.Array('--only', { description: 'limit to these repo names (repeatable)' })
|
|
27
|
+
all = Option.Boolean('--all', false, { description: 'also print the repos that have nothing to push' })
|
|
28
|
+
|
|
29
|
+
async execute(): Promise<number> {
|
|
30
|
+
const root = gitRoot(process.cwd())?.top ?? process.cwd()
|
|
31
|
+
let repos = houseRepos(root, homedir()).repos.map((r) => ({ name: r.name, path: r.path }))
|
|
32
|
+
const only = listFlag(this.only)
|
|
33
|
+
if (only.length > 0) repos = repos.filter((r) => only.includes(r.name))
|
|
34
|
+
const w = (l: string) => this.context.stdout.write(`${l}\n`)
|
|
35
|
+
if (repos.length === 0) {
|
|
36
|
+
w(`skip: no house repo found from ${root}`)
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
const res = await pushFleet(repos, { dry: this.dry })
|
|
40
|
+
for (const r of res) if (this.all || r.action !== 'none') w(r.line)
|
|
41
|
+
const pushed = res.filter((r) => r.ok && (r.action === 'push' || r.action === 'push-upstream')).length
|
|
42
|
+
const failed = res.filter((r) => !r.ok && r.action !== 'skip').length
|
|
43
|
+
const skipped = res.filter((r) => r.action === 'skip').length
|
|
44
|
+
w(`${failed > 0 ? 'FAIL' : 'ok'}: ${repos.length} repos, ${this.dry ? `${res.filter((r) => r.action === 'push' || r.action === 'push-upstream').length} would push` : `${pushed} pushed`}, ${skipped} skipped${failed > 0 ? `, ${failed} failed` : ''}`)
|
|
45
|
+
return failed > 0 ? 1 : 0
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -28,7 +28,7 @@ export type GlobalProcessesAnswer = {
|
|
|
28
28
|
export type GlobalProcessesDeps = {
|
|
29
29
|
pm: () => Promise<PmAnswer | null>
|
|
30
30
|
/** the daemon's identity, for its version; null when it does not answer */
|
|
31
|
-
daemon?: () => Promise<{ version: string } | null>
|
|
31
|
+
daemon?: () => Promise<{ version: string; held?: string | null } | null>
|
|
32
32
|
/** the declared units, read like pm reads them; null when no house is found */
|
|
33
33
|
declared: () => Promise<Pick<PmConfig, 'processes' | 'errors'> | null>
|
|
34
34
|
/** the project list's order */
|
|
@@ -43,18 +43,18 @@ const localPorts = () => ({ pm: pmPort(), daemon: daemonPort(), web: homePort()
|
|
|
43
43
|
* pm does not answer */
|
|
44
44
|
export async function globalProcessesAnswer(d: GlobalProcessesDeps): Promise<GlobalProcessesAnswer> {
|
|
45
45
|
const [pm, daemon] = await Promise.all([d.pm(), d.daemon?.() ?? null])
|
|
46
|
-
const core = { daemonVersion: daemon?.version ?? null, ports: localPorts() }
|
|
46
|
+
const core = { daemonVersion: daemon?.version ?? null, daemonHeld: daemon?.held ?? null, ports: localPorts() }
|
|
47
47
|
if (pm != null) return { pm: { pid: pm.pid, version: pm.version }, now: d.now, groups: processGroups(pm, d.order, core), errors: pm.unitErrors, house: true }
|
|
48
48
|
const c = await d.declared()
|
|
49
49
|
return { pm: null, now: d.now, groups: [coreGroup(null, core), ...groupRows((c?.processes ?? []).map(declaredRow), d.order)], errors: c?.errors ?? [], house: c != null }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** the daemon's identity at /hub.json, or null */
|
|
53
|
-
export async function askDaemon(url = daemonUrl()): Promise<{ version: string } | null> {
|
|
53
|
+
export async function askDaemon(url = daemonUrl()): Promise<{ version: string; held: string | null } | null> {
|
|
54
54
|
try {
|
|
55
55
|
const r = await fetch(`${url}/hub.json`, { signal: AbortSignal.timeout(3_000) })
|
|
56
|
-
const j = r.ok ? ((await r.json()) as { hub?: string; version?: string }) : null
|
|
57
|
-
return j?.hub === 'shipkit-home' && typeof j.version === 'string' ? { version: j.version } : null
|
|
56
|
+
const j = r.ok ? ((await r.json()) as { hub?: string; version?: string; held?: string | null }) : null
|
|
57
|
+
return j?.hub === 'shipkit-home' && typeof j.version === 'string' ? { version: j.version, held: j.held ?? null } : null
|
|
58
58
|
} catch {
|
|
59
59
|
return null
|
|
60
60
|
}
|
package/src/cli/cmd_Review.ts
CHANGED
|
@@ -83,7 +83,7 @@ function tileOf(arg: string): { file: string; label: string } {
|
|
|
83
83
|
|
|
84
84
|
type ViewInput = import('../review/store.ts').ViewInput
|
|
85
85
|
|
|
86
|
-
/** `--view "<label>" a.png [b.png]` and `--gallery "<label>" a.png b.png …` out of the args */
|
|
86
|
+
/** `--view "<label>" a.png [b.png] [--note "<text>"]` and `--gallery "<label>" a.png b.png …` out of the args */
|
|
87
87
|
function takeViews(args: readonly string[]): { views: ViewInput[]; rest: string[] } {
|
|
88
88
|
const views: ViewInput[] = []
|
|
89
89
|
const rest: string[] = []
|
|
@@ -100,6 +100,13 @@ function takeViews(args: readonly string[]): { views: ViewInput[]; rest: string[
|
|
|
100
100
|
if (a === '--gallery') views.push({ label, gallery: files.map(tileOf) })
|
|
101
101
|
else views.push(files.length === 1 ? { label, image: files[0] } : { label, before: files[0], after: files[1] })
|
|
102
102
|
i += 1 + files.length
|
|
103
|
+
if (args[i + 1] === '--note') {
|
|
104
|
+
const note = args[i + 2]
|
|
105
|
+
if (note === undefined || note.startsWith('--')) throw new Error(`--view "${label}" … --note "<text>": a line on that view`)
|
|
106
|
+
const v = views[views.length - 1]
|
|
107
|
+
if (v) v.note = note
|
|
108
|
+
i += 2
|
|
109
|
+
}
|
|
103
110
|
}
|
|
104
111
|
return { views, rest }
|
|
105
112
|
}
|
|
@@ -139,7 +146,8 @@ const ADD_FLAGS = `flags:
|
|
|
139
146
|
--audio <sound.wav> a sound: a waveform and a player (wav, mp3, flac, ogg, opus, m4a)
|
|
140
147
|
--before <a.png> --after <b.png> a before and after: two images, two clips of one size, or two sounds
|
|
141
148
|
--label "<label>" the first view's label
|
|
142
|
-
--view "<label>" <a.png> [<b.png>] another view of the same subject (repeat it)
|
|
149
|
+
--view "<label>" <a.png> [<b.png>] another view of the same subject (repeat it); --note "<text>" right after it: a line on that view
|
|
150
|
+
--choice every view is an option he scores and picks one of (two --view or more)
|
|
143
151
|
--gallery "<label>" <a.png> … a grid of small images; "<tile label>=<c.png>" names a tile (repeat it)
|
|
144
152
|
--topic <topic> what part of the project it is about (sk review topics)
|
|
145
153
|
--subject <kind> what is judged (default shot; the repo's unit names its kinds)
|
|
@@ -150,6 +158,19 @@ const ADD_FLAGS = `flags:
|
|
|
150
158
|
--keeps "<what stays>" with --supersedes on an approved look: what of it stays
|
|
151
159
|
--new --why "<reason>" a new card beside an active one of the same subject`
|
|
152
160
|
|
|
161
|
+
/** the help of add or add-view, then the flags this repo's review unit adds (any kind's, then each kind's under
|
|
162
|
+
* `--subject <kind>`); a repo with no unit, or one that does not load, says so on one line */
|
|
163
|
+
async function helpWithUnit(context: Ctx, usage: string, kinds: boolean): Promise<number> {
|
|
164
|
+
context.stdout.write(usage)
|
|
165
|
+
await run(context, (h) => {
|
|
166
|
+
const lines = Object.entries(h.unit?.flags ?? {}).map(([k, d]) => ` --${k} ${d}`)
|
|
167
|
+
if (kinds) for (const [name, k] of Object.entries(h.unit?.kinds ?? {})) for (const [f, d] of Object.entries(k.flags ?? {})) lines.push(` --${f} ${d} (with --subject ${name})`)
|
|
168
|
+
h.out(lines.length > 0 ? `\nthis repo's review unit adds:\n${lines.join('\n')}` : "\nthis repo's review unit adds no flags")
|
|
169
|
+
return 0
|
|
170
|
+
})
|
|
171
|
+
return 0
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
export class ReviewAddCommand extends Command {
|
|
154
175
|
static override paths = [['review', 'add']]
|
|
155
176
|
static override usage = Command.Usage({
|
|
@@ -160,14 +181,14 @@ export class ReviewAddCommand extends Command {
|
|
|
160
181
|
['one image', 'sk review add --title "the new menu" --what "the menu, bigger" --image shots/menu.png'],
|
|
161
182
|
['a before and after', 'sk review add --title "lamps lit" --what "..." --before a.png --after b.png'],
|
|
162
183
|
['a second version after his answer', 'sk review add --title "lamps lit" --what "..." --image c.png --supersedes <id>'],
|
|
184
|
+
['a pick among three voices', 'sk review add --choice --title "the narrator" --what "..." --view "voice 0" a.flac --note "low, breathy" --view "voice 1" b.flac --view "voice 2" c.flac'],
|
|
163
185
|
],
|
|
164
186
|
})
|
|
165
187
|
args = Option.Proxy()
|
|
166
188
|
|
|
167
189
|
async execute(): Promise<number> {
|
|
168
190
|
if (this.args.some(isHelp)) {
|
|
169
|
-
this.context
|
|
170
|
-
return 0
|
|
191
|
+
return helpWithUnit(this.context, this.cli.usage(ReviewAddCommand, { detailed: true }), true)
|
|
171
192
|
}
|
|
172
193
|
return run(this.context, async (h) => {
|
|
173
194
|
const { addItem } = await import('../review/store.ts')
|
|
@@ -176,7 +197,7 @@ export class ReviewAddCommand extends Command {
|
|
|
176
197
|
const kindName = rest[rest.indexOf('--subject') + 1]
|
|
177
198
|
const kind = rest.includes('--subject') && kindName ? h.unit?.kinds?.[kindName] : undefined
|
|
178
199
|
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'])
|
|
200
|
+
const f = flagsOf(rest, ['title', 'what', 'image', 'video', 'audio', 'before', 'after', 'label', 'topic', 'subject', 'commit', 'lane', 'supersedes', 'keeps', 'why', 'live'], ['answers', ...own], ['new', 'wip', 'choice'])
|
|
180
201
|
if (f.positionals.length > 0) throw new Error(`unexpected ${f.positionals.join(' ')}`)
|
|
181
202
|
const o = f.one
|
|
182
203
|
const repoFlags = Object.fromEntries(own.flatMap((k) => (f.many[k] ? [[k, f.many[k]]] : [])))
|
|
@@ -209,6 +230,7 @@ export class ReviewAddCommand extends Command {
|
|
|
209
230
|
...(o.why ? { why: o.why } : {}),
|
|
210
231
|
...(answers.length > 0 ? { answers } : {}),
|
|
211
232
|
...(f.on.has('new') ? { fresh: true } : {}),
|
|
233
|
+
...(f.on.has('choice') ? { choice: true } : {}),
|
|
212
234
|
...(o.live !== undefined ? { image: wipViews[0]?.image ?? '', views: wipViews.slice(1), wip: { live: o.live } } : { views }),
|
|
213
235
|
}
|
|
214
236
|
if (h.unit?.add) input = await h.unit.add(input, repoFlags)
|
|
@@ -225,27 +247,29 @@ export class ReviewAddViewCommand extends Command {
|
|
|
225
247
|
static override usage = Command.Usage({
|
|
226
248
|
category: 'Review',
|
|
227
249
|
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>
|
|
250
|
+
details: `${DETAILS}\n\nsk review add-view <id> "<label>" <a.png> [<b.png>] | --video <clip.mp4> | --audio <sound.wav> | --gallery <a.png> <b.png> … [--note "<a line on that view>"]`,
|
|
229
251
|
examples: [['a view from above', 'sk review add-view <id> "from above" top.png']],
|
|
230
252
|
})
|
|
231
253
|
args = Option.Proxy()
|
|
232
254
|
|
|
233
255
|
async execute(): Promise<number> {
|
|
234
256
|
if (this.args.some(isHelp)) {
|
|
235
|
-
this.context
|
|
236
|
-
return 0
|
|
257
|
+
return helpWithUnit(this.context, this.cli.usage(ReviewAddViewCommand, { detailed: true }), false)
|
|
237
258
|
}
|
|
238
259
|
return run(this.context, async (h) => {
|
|
239
260
|
const { addView } = await import('../review/store.ts')
|
|
240
261
|
const own = Object.keys(h.unit?.flags ?? {})
|
|
241
262
|
const repoFlags: Record<string, string[]> = {}
|
|
242
263
|
const args: string[] = []
|
|
264
|
+
let note: string | undefined
|
|
243
265
|
for (let i = 0; i < this.args.length; i++) {
|
|
244
266
|
const a = this.args[i] ?? ''
|
|
245
267
|
const name = a.startsWith('--') ? a.slice(2) : ''
|
|
246
268
|
if (own.includes(name)) (repoFlags[name] ??= []).push(this.args[++i] ?? '')
|
|
269
|
+
else if (name === 'note') note = this.args[++i]
|
|
247
270
|
else args.push(a)
|
|
248
271
|
}
|
|
272
|
+
if (this.args.includes('--note') && (note === undefined || note.startsWith('--'))) throw new Error('--note "<text>": a line on that view')
|
|
249
273
|
const [id, label, ...rest] = args
|
|
250
274
|
if (!id || !label) throw new Error('add-view <id> "<label>" <media>')
|
|
251
275
|
const v: ViewInput =
|
|
@@ -262,6 +286,7 @@ export class ReviewAddViewCommand extends Command {
|
|
|
262
286
|
: (() => {
|
|
263
287
|
throw new Error('add-view <id> "<label>" <a.png> [<b.png>] | --video <clip> | --gallery <a.png> …')
|
|
264
288
|
})()
|
|
289
|
+
if (note !== undefined) v.note = note
|
|
265
290
|
const extra = h.unit?.addView ? await h.unit.addView(v, repoFlags) : {}
|
|
266
291
|
const item = await addView(h.dir, id, v, extra)
|
|
267
292
|
h.out(`${id}: view ${item.views.length} "${label}" added`)
|
|
@@ -320,8 +345,14 @@ export class ReviewLsCommand extends Command {
|
|
|
320
345
|
|
|
321
346
|
export class ReviewShowCommand extends Command {
|
|
322
347
|
static override paths = [['review', 'show']]
|
|
323
|
-
static override usage = Command.Usage({
|
|
348
|
+
static override usage = Command.Usage({
|
|
349
|
+
category: 'Review',
|
|
350
|
+
description: 'one item and its thread: the verdict, rectangles, reactions, the whole conversation',
|
|
351
|
+
details: `${DETAILS}\n\nTakes an item id or a player report's ref (live#3). --paste prints the short block that hands the card to another agent session (the hub's copy button gives the same).`,
|
|
352
|
+
examples: [['hand a card to another session', 'sk review show <id> --paste']],
|
|
353
|
+
})
|
|
324
354
|
id = Option.String()
|
|
355
|
+
paste = Option.Boolean('--paste', false, { description: 'the block another agent session acts on: title, repo, this command, the links' })
|
|
325
356
|
|
|
326
357
|
async execute(): Promise<number> {
|
|
327
358
|
return run(this.context, async (h) => {
|
|
@@ -332,6 +363,15 @@ export class ReviewShowCommand extends Command {
|
|
|
332
363
|
const all = readAllCached(h.dir)
|
|
333
364
|
const x = all.items.find((y) => y.item.id === id)
|
|
334
365
|
if (!x) throw new Error(`no item ${this.id}`)
|
|
366
|
+
if (this.paste) {
|
|
367
|
+
const { dirname } = await import('node:path')
|
|
368
|
+
const { pasteHosts, cardUrl } = await import('../review/paste.ts')
|
|
369
|
+
const { pasteBlock, STATUS_LABEL } = await import('../review/model.ts')
|
|
370
|
+
const { statusOf } = await import('../review/threads.ts')
|
|
371
|
+
const w = await pasteHosts(dirname(dirname(h.dir)))
|
|
372
|
+
h.out(pasteBlock(x.item, STATUS_LABEL[statusOf(x, all.items)], { root: w.root, open: cardUrl(w.open, x.item.id), phone: w.phone ? cardUrl(w.phone, x.item.id) : null }))
|
|
373
|
+
return 0
|
|
374
|
+
}
|
|
335
375
|
const t = threadOfItem(x.item.id, threadsOf(all.items))
|
|
336
376
|
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
377
|
return 0
|
|
@@ -380,10 +420,11 @@ export class ReviewInboxCommand extends Command {
|
|
|
380
420
|
const followed = followedIds(all.items)
|
|
381
421
|
// what he or a player raised and nobody closed is the agents' move, read or not
|
|
382
422
|
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
|
|
423
|
+
// a plain approval (no comment, no rectangle, no reaction, no pick, no reply) asks for no work: counted on one
|
|
424
|
+
// line unless --all
|
|
384
425
|
const plain = (x: (typeof news)[number]): boolean => {
|
|
385
426
|
const v = verdictOf(x)
|
|
386
|
-
return v?.status === 'approved' && !v.seen && !v.comment && v.rects.length === 0 && unreadBy(x, 'agent').length === 0
|
|
427
|
+
return v?.status === 'approved' && !v.seen && !v.comment && v.rects.length === 0 && !v.reactions && v.pick === undefined && unreadBy(x, 'agent').length === 0
|
|
387
428
|
}
|
|
388
429
|
const shown = [...(this.all ? news : news.filter((x) => !plain(x))), ...open]
|
|
389
430
|
if (shown.length === 0 && news.length === 0) h.out('review: no unseen feedback or reply')
|