rvlib-shipkit 0.10.0 → 0.13.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 +204 -0
- package/README.md +17 -3
- package/package.json +5 -2
- package/src/asks/asks-queue.ts +42 -0
- package/src/assets/kit-config.ts +2 -3
- package/src/assets/roll-workspace.ts +2 -1
- package/src/assets/run-assets.ts +0 -11
- package/src/assets/server/studio-server.ts +7 -24
- package/src/cli/cmd_Assets.ts +3 -14
- package/src/cli/cmd_Check.ts +22 -4
- package/src/cli/cmd_Needs.ts +47 -0
- package/src/cli/cmd_Reflect.ts +116 -0
- package/src/cli/cmd_Rules.ts +32 -0
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli.ts +7 -2
- package/src/collections/actions.ts +85 -0
- package/src/collections/collection-types.ts +65 -0
- package/src/collections/graphs.ts +34 -0
- package/src/collections/pings.ts +72 -0
- package/src/collections/posts.ts +125 -0
- package/src/collections/providers.ts +275 -0
- package/src/contracts/apply-contracts.ts +47 -0
- package/src/contracts/run-contracts.ts +8 -3
- package/src/core/canonical-scripts.ts +46 -0
- package/src/core/check-suite.ts +6 -3
- package/src/core/locked-write.ts +39 -0
- package/src/core/motherhome.ts +87 -0
- package/src/core/run-check.ts +28 -15
- package/src/core/run-ci.ts +4 -0
- package/src/core/write-status.ts +7 -26
- package/src/init-repo.ts +24 -11
- package/src/reflect/prompt-corpus.ts +305 -0
- package/src/repo-config.ts +26 -38
- package/src/rules/SK001-package-json-shape.ts +156 -0
- package/src/rules/SK002-contracts-fulfillness.ts +19 -0
- package/src/rules/SK003-claude-md-shape.ts +64 -0
- package/src/rules/SK004-gitignore-policy.ts +105 -0
- package/src/rules/SK005-rvlib-versions.ts +99 -0
- package/src/rules/SK006-npm-publish-safety.ts +42 -0
- package/src/rules/SK007-github-visibility.ts +63 -0
- package/src/rules/rule-registry.ts +32 -0
- package/src/rules/rule.ts +62 -0
- package/src/rules/run-fix.ts +57 -0
- package/src/serve/hub-app/col-card.tsx +192 -0
- package/src/serve/hub-app/cols-svc.ts +136 -0
- package/src/serve/hub-app/geometry-view.tsx +155 -0
- package/src/serve/hub-app/health-svc.ts +49 -0
- package/src/serve/hub-app/hub-app.tsx +240 -127
- package/src/serve/hub-app/hub-st.ts +169 -0
- package/src/serve/hub-app/hub-types.ts +72 -0
- package/src/serve/hub-app/hub.css +286 -35
- package/src/serve/hub-app/index.html +1 -1
- package/src/serve/hub-app/md.ts +24 -0
- package/src/serve/hub-app/media-svc.ts +429 -0
- package/src/serve/hub-app/media-view.tsx +299 -0
- package/src/serve/run-serve.ts +46 -91
- package/src/serve/serve-hub.ts +99 -49
- package/src/shipkit.ts +13 -6
- package/src/assets/server/studio-page.ts +0 -687
- package/src/core/package-json-check.ts +0 -88
- package/src/serve/hub-pages.ts +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,209 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 — 2026-07-26
|
|
4
|
+
|
|
5
|
+
Stable rule ids. Every built-in invariant `shipkit check` enforces is now
|
|
6
|
+
an SK rule — one module per rule (`src/rules/SK###-<slug>.ts`), one
|
|
7
|
+
registry (`rule-registry.ts`, throws at import on a duplicate or malformed
|
|
8
|
+
id), ids never reused or renumbered. The two existing built-ins migrated
|
|
9
|
+
whole: SK001 package.json shape (was `core/package-json-check.ts`, deleted),
|
|
10
|
+
SK002 `.rv-*` contracts fulfillness. Line text changed on every consumer:
|
|
11
|
+
rule-emitted `ok:`/`WARN`/`FAIL:`/`skip:` lines now carry a `[SK###]` tag
|
|
12
|
+
after the wire prefix — on `check`, `shipkit contracts`, AND the ci
|
|
13
|
+
`contracts` task (one tagged rendering path,
|
|
14
|
+
`contracts/apply-contracts.ts`; `contractsConfigFrom`/`activeContracts`/
|
|
15
|
+
`applyContractsResult` moved there, still exported from the barrel). A
|
|
16
|
+
failing/warning rule always carries at least one `fix:` pointer (the
|
|
17
|
+
framework appends `docsBase + rule.owner` when the lint attached none).
|
|
18
|
+
New command: `shipkit rules [--json]` — the id catalog (plain lines, not
|
|
19
|
+
wire). Wire prefixes themselves are unchanged; matchers keying on prefixes
|
|
20
|
+
are unaffected.
|
|
21
|
+
|
|
22
|
+
`shipkit check --fix [SK### …]`: rules may carry an idempotent `fix()` —
|
|
23
|
+
apply, re-lint, show the real end state; never cached, never under ci;
|
|
24
|
+
unknown/unfixable ids fail loud. First fixers: SK001 (private:true, license
|
|
25
|
+
removal, canonical scripts from the ONE shared module
|
|
26
|
+
`core/canonical-scripts.ts` that `shipkit init` also scaffolds from —
|
|
27
|
+
bodies the vocabulary page doesn't state are named in the summary, never
|
|
28
|
+
invented; rvlib-* roots refused), SK003 CLAUDE.md include-index (new rule:
|
|
29
|
+
prose/extra lines/inverted order/untracked-CLAUDE.local.md FAIL; fix routes
|
|
30
|
+
a missing `@./agent/index.md` on an EXISTING file, never creates one),
|
|
31
|
+
SK004 `.gitignore` policy block (new rule: marker-keyed managed block of
|
|
32
|
+
the always-gitignored contract surfaces + tmp/; a well-formed block is
|
|
33
|
+
rewritten IN PLACE so below-block `!`-overrides keep winning; CRLF files
|
|
34
|
+
keep their EOL; repo lines outside the block never touched;
|
|
35
|
+
`defineRepo({ gitignore: [...] })` contributes repo lines INSIDE the
|
|
36
|
+
block, for names a published lib may not carry; `shipkit init` — fresh or
|
|
37
|
+
re-run on an existing repo — writes it through the same fixer). A failing
|
|
38
|
+
rule's fix pointers advertise `autofix: shipkit check --fix SK###`; the
|
|
39
|
+
post-fix re-lint strips the advertisement and an unchanged fix reports as
|
|
40
|
+
a note, not a WARN; `--fix` rejects `--json` and closes with a count of
|
|
41
|
+
rules no fixer covers. SK001's fixer refuses field flips on anything
|
|
42
|
+
publish-marked (files/exports/publishConfig/prepublishOnly) and never
|
|
43
|
+
duplicates a script a banned alias already satisfies.
|
|
44
|
+
|
|
45
|
+
SK005 rvlib versions: every `rvlib-*` dep is compared (real semver order,
|
|
46
|
+
so a dist-tag pointing BEHIND the installed version is reported, never a
|
|
47
|
+
silent downgrade) to npm latest on each `shipkit check` (rules may now
|
|
48
|
+
declare `ttl` — SK005 caches clean passes 24h; registry unreachable = loud
|
|
49
|
+
skip, never red; a `link:` dep that does not resolve is a FAIL instead of
|
|
50
|
+
a cryptic import crash). When a newer version is live the rule
|
|
51
|
+
SELF-UPDATES: `bun update --latest`, run by the lint itself, only when
|
|
52
|
+
package.json/lockfile are git-clean (else WARN deferred), never under ci;
|
|
53
|
+
the applied update itself reports as WARN — the repo was mutated and a
|
|
54
|
+
restart+commit is owed, so it lands on the attention surface and is never
|
|
55
|
+
cached. New `shipkit check --frozen` (or standing `SHIPKIT_FROZEN=1`)
|
|
56
|
+
reports drift without touching anything; the flag threads through
|
|
57
|
+
`CheckOpts` so doctor/TUI/hub callers can pass it too. Committing the bump
|
|
58
|
+
stays with the working agent — shipkit never commits. Suite-level fix
|
|
59
|
+
riding along: a TTL'd check that emitted a `skip` line is no longer
|
|
60
|
+
cached — "only clean passes replay" is now literally true (a dead
|
|
61
|
+
registry was being restated as fact for 24h).
|
|
62
|
+
|
|
63
|
+
SK006 npm publish safety: a `private: true` package carrying publishConfig
|
|
64
|
+
/ prepublish* scripts / an npm-publishing `publish` script FAILs — the
|
|
65
|
+
machinery is dead weight while private holds and a loaded gun the day it
|
|
66
|
+
flips; rvlib-* roots exempt; lint-only on purpose (a `publish` script may
|
|
67
|
+
deploy a site). SK007 github visibility: new repo.config
|
|
68
|
+
`visibility: 'private' | 'public'` (absent = private, defineRepo validates)
|
|
69
|
+
vs `gh repo view --json isPrivate` — mismatch FAILs in BOTH directions
|
|
70
|
+
(public-while-declared-private is the leak case), 7d clean-pass cache,
|
|
71
|
+
every unverifiable path (no gh, no remote, gh error) a loud skip that is
|
|
72
|
+
never red and never cached.
|
|
73
|
+
|
|
74
|
+
Motherhome reporting: `check`/`ci` (and `init` on an already-configured
|
|
75
|
+
repo) upsert one row for the repo — name, path, version (`+src` suffix
|
|
76
|
+
when consumed by path/link), npm|link|file|path mode, last-seen day —
|
|
77
|
+
into `<docsBase>/build/shipkit/local-projects.md`, ONLY when that page
|
|
78
|
+
already exists on this disk. The lib never creates the page and never
|
|
79
|
+
writes into a relative or URL docsBase — a consumer using docsBase as a
|
|
80
|
+
plain fix-pointer prefix will never find surprise files in its docs tree.
|
|
81
|
+
Whole read-modify-write under a file lock with atomic rename
|
|
82
|
+
(core/locked-write, extracted from write-status — one lock concern),
|
|
83
|
+
dead-path rows pruned, data-change-or->7d churn control, every failure a
|
|
84
|
+
WARN note that never breaks the calling command.
|
|
85
|
+
|
|
86
|
+
## 0.12.0 — 2026-07-23
|
|
87
|
+
|
|
88
|
+
ONE server, ONE page. `shipkit serve` runs the repo's single `Bun.serve`,
|
|
89
|
+
and `/` is the single surface: a mobx+react app (Bun HTML import, no build
|
|
90
|
+
step) holding the full media atelier (rolls, compare, pick, geometry
|
|
91
|
+
overlay, custom panels, uploads, generate queue) plus foldable sections
|
|
92
|
+
for asks, creds, posts, health (STATUS.md + streaming check runner) and
|
|
93
|
+
graphs (each lint-checked), with a segmented severity filter (all · needs ·
|
|
94
|
+
red) at `/?f=needs` / `/?f=red` (legacy `/?needs=1` reads as needs) and
|
|
95
|
+
hash deep-links (`#<asset-id>` / `#cat-<Category>`).
|
|
96
|
+
Kit-less repos serve the same page with no media section. Graph canvases
|
|
97
|
+
stay as drill-down pages (`/graph/wiki`, `/graph/agent`) with a home link.
|
|
98
|
+
|
|
99
|
+
New:
|
|
100
|
+
|
|
101
|
+
- Navigation on the one page: a thin icon RAIL that scrolls/unfolds to a
|
|
102
|
+
section (red dot when it holds a red item — never a separate page), a
|
|
103
|
+
sidebar SEARCH filtering rows and grid cards, a `cmd+j` palette jumping
|
|
104
|
+
to any item or section.
|
|
105
|
+
- Instances carry `md` (truth-file markdown body, frontmatter stripped,
|
|
106
|
+
4000-char cap): cards render it via marked (GFM — tables render) behind
|
|
107
|
+
a sanitize-html allowlist — clamped in the grid, full in the item view.
|
|
108
|
+
- Secret cards presented properly: mono name, service line,
|
|
109
|
+
present/missing chip, mint link; copy block only when missing.
|
|
110
|
+
- Roll strips: newest first with a stable tiebreak; the generating
|
|
111
|
+
spinner tile sits at the START of the strip.
|
|
112
|
+
- Every section has BOTH views: title → collection view (one card per
|
|
113
|
+
instance in the stage), row/card → item view with a back link. A graph's
|
|
114
|
+
item view EMBEDS the graph (iframe of /graph/*), never a bare link.
|
|
115
|
+
- Sections show everything, always: asks include the done/ archive
|
|
116
|
+
(quiet, remarkable — consult past answers), the secrets section lists
|
|
117
|
+
every repo-declared cred (present = quiet, missing = red with the mint
|
|
118
|
+
artifact); section headers render even at 0 items. "needs" is THE
|
|
119
|
+
waiting-on-me filter, "red" the hard-blocked cut.
|
|
120
|
+
- Watch mode is the house default for the hub script
|
|
121
|
+
(`bun --watch … serve`, `shipkit init` scaffolds it): lib/config edits
|
|
122
|
+
restart the server live. Accepted tradeoff: a restart drops in-memory
|
|
123
|
+
queue state (`assets fill` recovers).
|
|
124
|
+
- `serve.publicHost` (repo.config): off-machine hostname printed beside
|
|
125
|
+
localhost and appended to ping messages — a phone can't resolve
|
|
126
|
+
localhost.
|
|
127
|
+
- `/api/collections.json`: browse-all — every instance of every
|
|
128
|
+
collection, healthy included (quiet tiles), rendered as foldable
|
|
129
|
+
sections with red/yellow dots.
|
|
130
|
+
- Trust surface: instances carry `file` (the git file the tile IS — shown
|
|
131
|
+
on the card; the file always wins) and `img` (thumbnails via the media
|
|
132
|
+
API).
|
|
133
|
+
- Remarks: cards AND the asset view have a remark box; `/api/act/remark`
|
|
134
|
+
routes the text into the item's own file (post `## Feedback`, the ask
|
|
135
|
+
file, or `.rv-assets/notes.md` for media) and it returns as `remarks`
|
|
136
|
+
on the instance — `shipkit needs --json` briefs agents on them at boot.
|
|
137
|
+
- Posted/rejected post history is VISIBLE (quiet), no longer hidden;
|
|
138
|
+
media collections list ALL kit assets, not only needing ones.
|
|
139
|
+
- `shipkit reflect [repo…] [--short --out <dir> --json]`: your prompts
|
|
140
|
+
across every Claude Code session of a repo, chronological md corpus for a
|
|
141
|
+
retrospective. Corpus fidelity is the whole feature — it unions EVERY
|
|
142
|
+
config root on the machine (`$CLAUDE_CONFIG_DIR` plus `~/.claude` plus
|
|
143
|
+
sibling `~/.claude-<account>`; honoring only the env var hides the account
|
|
144
|
+
you are not currently running as), scans worktree session dirs, includes
|
|
145
|
+
the prompts you QUEUED while the agent was working (`queue-operation`
|
|
146
|
+
entries, marked `origin: 'queued'` — they carry mid-run corrections and
|
|
147
|
+
standing instructions, and when a run ends first they never become user
|
|
148
|
+
messages at all), drops harness-authored user entries (tool results,
|
|
149
|
+
command wrappers, compaction summaries, subagent prompts, SDK-driven
|
|
150
|
+
sessions), rescues the text you typed inside a slash command, strips
|
|
151
|
+
reminder blocks appended inside real prompts, recovers pruned sessions
|
|
152
|
+
from `sessions-index.json`, and removes only MECHANICAL copies — a
|
|
153
|
+
replayed entry, or a queued prompt arriving again as a message — so real
|
|
154
|
+
repetition (saying it twice) survives in the corpus. `--json` emits one
|
|
155
|
+
shape whatever the run looked like: `[{repo, prompts, error?}]`.
|
|
156
|
+
|
|
157
|
+
BREAKING (0.11.0 → 0.12.0):
|
|
158
|
+
|
|
159
|
+
- `shipkit assets serve` DELETED — the studio UI rides the hub page; the
|
|
160
|
+
media API mounts at `/media/*` (was `/assets/*` under the hub).
|
|
161
|
+
- `serveStudio`, `Studio`, `studioPage`, `withRail` exports DELETED
|
|
162
|
+
(`studioRoutes` stays — it IS the media API).
|
|
163
|
+
- `kit.port` DELETED from `defineKit` (nothing standalone listens anymore).
|
|
164
|
+
- `/status` and `/assets/` pages DELETED (404, no redirect); the check
|
|
165
|
+
stream moved `/status/run` → `/api/check/run`; the vanilla
|
|
166
|
+
template-string studio SPA and the injected rail are gone — the react
|
|
167
|
+
app is the only UI code path.
|
|
168
|
+
|
|
169
|
+
## 0.11.0 — 2026-07-23
|
|
170
|
+
|
|
171
|
+
The needs feed: the hub's `/` now answers "what needs a human right now,
|
|
172
|
+
artifact in hand".
|
|
173
|
+
|
|
174
|
+
New:
|
|
175
|
+
|
|
176
|
+
- Collection model (`src/collections/`): every curatable surface projects
|
|
177
|
+
through one interface — media kinds, asks, missing declared secrets
|
|
178
|
+
(mint artifact + `credsCommand` store template as config data), repo
|
|
179
|
+
health, social posts. `projectNeeds` is failure-isolated per collection.
|
|
180
|
+
- `shipkit needs [--json]`: the feed as a CLI — red/yellow by collection,
|
|
181
|
+
REPORT-ONLY (exit 0 always; `check`/`ci` stay the gates).
|
|
182
|
+
- Hub `/` = the needs feed: act-in-place tiles (copy buttons for briefs/
|
|
183
|
+
commands/post text, mint links, done + proof-URL inputs, draft pick
|
|
184
|
+
buttons), reds-only toggle, `/api/needs.json` + `/api/act/*` (every
|
|
185
|
+
mutation hits a git-tracked truth file).
|
|
186
|
+
- RED-only pings while the hub serves: one batched notification per tick
|
|
187
|
+
of NEW red items (`notifyCommand` template + macOS notification).
|
|
188
|
+
Once-ever per item: cache file for non-ask items, the ask file's own
|
|
189
|
+
`pinged:` field for asks. Yellow never pings.
|
|
190
|
+
- Social posts contract v2: slug-only files, variants (`<slug>.a.md`) as
|
|
191
|
+
drafts-as-rolls (pick flips siblings to rejected, files kept),
|
|
192
|
+
frontmatter kind/tone/status/posted/rating, `## Angle` / `## Post` /
|
|
193
|
+
`## Feedback` sections — the copy button pastes ONLY `## Post`;
|
|
194
|
+
feedback appends are non-destructive.
|
|
195
|
+
- Missing manual media tiles carry their brief as the copy artifact.
|
|
196
|
+
|
|
197
|
+
Breaking (0.10.x consumers):
|
|
198
|
+
|
|
199
|
+
- `/api/attention.json` → `/api/needs.json` (NeedItem[] — flat, severity
|
|
200
|
+
red|yellow); `attentionRedCount`/`AttentionSection`/`AttentionLine`
|
|
201
|
+
exports removed (use `redCount`/`NeedItem`/`Instance`). Unpicked-roll
|
|
202
|
+
and stale assets are now YELLOW (taste pending — a default ships), no
|
|
203
|
+
longer red.
|
|
204
|
+
- `.rv-social` post files: `status:` frontmatter is required (missing =
|
|
205
|
+
a red feed item); v1 vocab maps idea→draft, ready→picked, link→posted.
|
|
206
|
+
|
|
3
207
|
## 0.10.0 — 2026-07-22
|
|
4
208
|
|
|
5
209
|
New:
|
package/README.md
CHANGED
|
@@ -34,16 +34,30 @@ shipkit ci commit gate: floor (lint/typecheck/test/imports/cycles —
|
|
|
34
34
|
+ your tasks, parallel DAG; refreshes STATUS.md
|
|
35
35
|
[--bail --skip g --only id --json]
|
|
36
36
|
shipkit contracts release-contract fulfillness alone [--venue v --json]
|
|
37
|
-
shipkit check periodic health: every *.check.ts in
|
|
38
|
-
auto-discovered + TTL-cached
|
|
37
|
+
shipkit check periodic health: built-in SK rules + every *.check.ts in
|
|
38
|
+
the repo, auto-discovered + TTL-cached
|
|
39
|
+
[--json --fresh --frozen --fix [SK### …]]
|
|
40
|
+
--fix applies rule autofixes then re-lints; --frozen =
|
|
41
|
+
self-maintaining rules (rvlib self-update) report only
|
|
42
|
+
shipkit rules the SK rule catalog: id, what it lints, owning doc,
|
|
43
|
+
fixable? [--json]
|
|
39
44
|
shipkit tui live dashboard: ci tasks + discovered checks + panels
|
|
40
45
|
the repo injects via repo.config tui.panels
|
|
41
46
|
shipkit asks human action queue: list · ask "<title>" · asks check (auto-close)
|
|
47
|
+
shipkit needs everything blocked on a human, red/yellow by collection
|
|
48
|
+
report-only, exit 0 always [--json]
|
|
42
49
|
shipkit wiki serve|lint — live md link-graph + orphan/dangling-ref lint
|
|
50
|
+
shipkit mv rename an md page + its sibling dir, sweeping every ref
|
|
43
51
|
shipkit assets serve|fill|add|gen|identity|check — the asset atelier
|
|
44
52
|
(declare kinds in .rv-assets/kit.ts, roll, curate, ship)
|
|
53
|
+
shipkit serve the repo's ONE server: hub page, graphs, studio, status
|
|
54
|
+
shipkit reflect your prompts across every Claude Code session of a repo:
|
|
55
|
+
all config roots, queued-mid-run ones included, harness
|
|
56
|
+
noise out, chronological md corpus for a retrospective
|
|
57
|
+
[repo…] [--short --out <dir> --json]
|
|
45
58
|
shipkit init scaffold config + scripts, print the resolved floor
|
|
46
59
|
[--bin <cli.ts path> for in-repo-source consumers]
|
|
60
|
+
shipkit start ensure <root>/<name> + its CLAUDE.md include; prints the dir
|
|
47
61
|
```
|
|
48
62
|
|
|
49
63
|
Output is a stable line protocol — `ok:` / `WARN` / `FAIL:` / `skip:` with
|
|
@@ -67,7 +81,7 @@ Ship your own contracts via `contracts: { extra: [defineContract({...})] }`
|
|
|
67
81
|
Everything the CLI does is importable: `runTasks`, `CheckSuite`,
|
|
68
82
|
`evaluateContracts`, the asks queue, `scanMdGraph`/`lintMd`/`serveMdGraph`,
|
|
69
83
|
and the assets surface (`defineKit`/`defineKind`, `identityKinds`,
|
|
70
|
-
`
|
|
84
|
+
`studioRoutes`, `saveEndpoint` via `rvlib-shipkit/vite`).
|
|
71
85
|
Ships TypeScript source; Bun ≥ 1.0.
|
|
72
86
|
|
|
73
87
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"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",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,17 +21,20 @@
|
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/bun": "^1.3.14",
|
|
23
23
|
"@types/react": "^19.2.17",
|
|
24
|
-
"@types/react-dom": "^19.2.3"
|
|
24
|
+
"@types/react-dom": "^19.2.3",
|
|
25
|
+
"@types/sanitize-html": "^2.16.1"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@napi-rs/canvas": "^1.0.2",
|
|
28
29
|
"clipanion": "^4.0.0-rc.4",
|
|
29
30
|
"ink": "^7.1.1",
|
|
31
|
+
"marked": "^18.0.7",
|
|
30
32
|
"mobx": "^6.16.1",
|
|
31
33
|
"mobx-react-lite": "^4.1.1",
|
|
32
34
|
"react": "^19.2.7",
|
|
33
35
|
"react-dom": "^19.2.7",
|
|
34
36
|
"rvlib-ts-tools": "^0.2.0",
|
|
37
|
+
"sanitize-html": "^2.17.6",
|
|
35
38
|
"sharp": "^0.35.3"
|
|
36
39
|
}
|
|
37
40
|
}
|
package/src/asks/asks-queue.ts
CHANGED
|
@@ -45,6 +45,14 @@ export function openAsks(dir: string): Ask[] {
|
|
|
45
45
|
return listAsks(dir).filter((a) => a.status !== 'done')
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** the <dir>/done archive, newest-first — READ-ONLY: never mints the dir
|
|
49
|
+
* (numeric filename prefixes are creation order, so reverse-sort = newest) */
|
|
50
|
+
export function doneAsks(dir: string): Ask[] {
|
|
51
|
+
const done = join(dir, 'done')
|
|
52
|
+
if (!existsSync(done)) return []
|
|
53
|
+
return readdirSync(done).filter((f) => f.endsWith('.md')).sort().reverse().map((f) => parse(done, f))
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
export function checkAsks(dir: string, print: (line: string) => void = console.log): { closed: number } {
|
|
49
57
|
const done = join(dir, 'done')
|
|
50
58
|
mkdirSync(done, { recursive: true })
|
|
@@ -99,6 +107,40 @@ ${whyBlock}**Answer**: _(fill in / check off, or just do it — the check probe
|
|
|
99
107
|
return file
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
/** flip an ask to done and archive it (the hub's "done" verb) */
|
|
111
|
+
export function archiveAsk(dir: string, file: string): void {
|
|
112
|
+
const p = join(dir, file)
|
|
113
|
+
if (!existsSync(p)) throw new Error(`no such ask: ${file}`)
|
|
114
|
+
const md = readFileSync(p, 'utf8').replace(/^status:.*$/m, `status: done\nclosed: ${new Date().toISOString().slice(0, 10)}`)
|
|
115
|
+
writeFileSync(p, md)
|
|
116
|
+
const done = join(dir, 'done')
|
|
117
|
+
mkdirSync(done, { recursive: true })
|
|
118
|
+
renameSync(p, join(done, file))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** record the completion proof (human-loop: URL back = done), then archive */
|
|
122
|
+
export function proofAsk(dir: string, file: string, url: string): void {
|
|
123
|
+
const p = join(dir, file)
|
|
124
|
+
if (!existsSync(p)) throw new Error(`no such ask: ${file}`)
|
|
125
|
+
writeFileSync(p, `${readFileSync(p, 'utf8').trimEnd()}\n\n**Proof**: ${url}\n`)
|
|
126
|
+
archiveAsk(dir, file)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** once-ever ping marker, shared with every ping path (notify doctrine):
|
|
130
|
+
* returns false when the ask was already pinged — never double-fire */
|
|
131
|
+
export function markPinged(dir: string, file: string): boolean {
|
|
132
|
+
const p = join(dir, file)
|
|
133
|
+
if (!existsSync(p)) return false
|
|
134
|
+
const md = readFileSync(p, 'utf8')
|
|
135
|
+
if (/^pinged:/m.test(md)) return false
|
|
136
|
+
const line = `pinged: ${new Date().toISOString().slice(0, 10)}`
|
|
137
|
+
const withMark = md.replace(/^status:(.*)$/m, `status:$1\n${line}`)
|
|
138
|
+
// a hand-edited ask without status: still gets a durable marker — a
|
|
139
|
+
// no-op write here would re-fire the ping every tick forever
|
|
140
|
+
writeFileSync(p, withMark !== md ? withMark : `${line}\n${md}`)
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
export function renderAsksTable(dir: string, print: (line: string) => void = console.log, opts?: { emptyText?: string }): number {
|
|
103
145
|
const open = openAsks(dir)
|
|
104
146
|
if (open.length === 0) { print(opts?.emptyText ?? 'no open asks — all caught up 🎉'); return 0 }
|
package/src/assets/kit-config.ts
CHANGED
|
@@ -112,14 +112,13 @@ export type Kind = {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export type KitConfig = {
|
|
115
|
-
port?: number
|
|
116
115
|
/** roll workspace root, repo-root-relative; gitignored */
|
|
117
116
|
workspace?: string
|
|
118
117
|
comfy?: { host: string; workflows: string; heal?: string }
|
|
119
118
|
kinds: Kind[]
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
export type ResolvedKit = KitConfig & {
|
|
121
|
+
export type ResolvedKit = KitConfig & { workspace: string }
|
|
123
122
|
|
|
124
123
|
export function validateKit(k: KitConfig): string[] {
|
|
125
124
|
const errs: string[] = []
|
|
@@ -190,7 +189,7 @@ export function missingPanelScripts(kit: KitConfig, root: string): string[] {
|
|
|
190
189
|
export function defineKit(k: KitConfig): ResolvedKit {
|
|
191
190
|
const errs = validateKit(k)
|
|
192
191
|
if (errs.length > 0) throw new Error(`invalid kit:\n ${errs.join('\n ')}`)
|
|
193
|
-
return { ...k,
|
|
192
|
+
return { ...k, workspace: k.workspace ?? '.rv-assets/out' }
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
export const defineKind = (k: Kind): Kind => k
|
|
@@ -25,7 +25,8 @@ export function listRolls(dir: string): string[] {
|
|
|
25
25
|
if (!existsSync(dir)) return []
|
|
26
26
|
return readdirSync(dir)
|
|
27
27
|
.filter(isRollFile)
|
|
28
|
-
|
|
28
|
+
// newest first; filename tiebreak — same-second batches must not shuffle between polls
|
|
29
|
+
.sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs || a.localeCompare(b))
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export const pickPath = (dir: string): string => join(dir, 'pick.json')
|
package/src/assets/run-assets.ts
CHANGED
|
@@ -10,8 +10,6 @@ import { loadWorkflows } from './gen/workflow-loader.ts'
|
|
|
10
10
|
import { allAssets, findAsset, loadKit, type ResolvedKit } from './kit-config.ts'
|
|
11
11
|
import { NO_PICK_FIX, renderIdentity, type Brand } from './kits/identity-kit.ts'
|
|
12
12
|
import { assetDir, listRolls, readPick, readPromptOverlay } from './roll-workspace.ts'
|
|
13
|
-
import { page } from './server/studio-page.ts'
|
|
14
|
-
import { serveStudio } from './server/studio-server.ts'
|
|
15
13
|
|
|
16
14
|
export type Print = (line: string) => void
|
|
17
15
|
export type AssetsIo = { print: Print; printErr: Print }
|
|
@@ -61,15 +59,6 @@ async function guarded(io: AssetsIo, body: () => Promise<number>): Promise<numbe
|
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
export const runAssetsServe = (root: string, io: AssetsIo): Promise<number> =>
|
|
65
|
-
guarded(io, async () => {
|
|
66
|
-
const kit = await loadKit(root)
|
|
67
|
-
const studio = await serveStudio({ kit, root, page })
|
|
68
|
-
io.print(`ok: studio → http://127.0.0.1:${studio.server.port} (${allAssets(kit).length} assets)`)
|
|
69
|
-
await new Promise(() => {}) // Bun.serve owns the process — never resolve
|
|
70
|
-
return 0
|
|
71
|
-
})
|
|
72
|
-
|
|
73
62
|
export const runAssetsTopUp = (root: string, io: AssetsIo, o: { mode: 'fill' | 'add'; n: number; categories: string[] }): Promise<number> =>
|
|
74
63
|
guarded(io, async () => {
|
|
75
64
|
const kit = await loadKit(root)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The studio HTTP server — roll variants, compare, pick, geometry metadata,
|
|
2
|
-
// category grid data. 127.0.0.1 ONLY, forever.
|
|
3
|
-
//
|
|
2
|
+
// category grid data. 127.0.0.1 ONLY, forever. Watch-mode restarts drop the
|
|
3
|
+
// in-memory queue — accepted tradeoff (his order 2026-07-23); `shipkit
|
|
4
|
+
// assets fill` is the recovery path.
|
|
4
5
|
//
|
|
5
6
|
// ids/names are validated against the kit + the roll-file regex — no path
|
|
6
7
|
// from the client ever reaches the filesystem unchecked.
|
|
@@ -20,14 +21,11 @@ import { StudioQueue } from './studio-queue.ts'
|
|
|
20
21
|
export type StudioDeps = {
|
|
21
22
|
kit: ResolvedKit
|
|
22
23
|
root: string
|
|
23
|
-
page: () => string
|
|
24
24
|
/** test seam — defaults to the real engines. `context` = a derived
|
|
25
25
|
* asset's resolved parent picks (queue resolves it at run time) */
|
|
26
26
|
generateOne?: (found: FoundAsset, prompt: string, seed: number, destDir: string, onProgress?: OnProgress, context?: ParentContext) => Promise<void>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export type Studio = { server: ReturnType<typeof Bun.serve>; queue: StudioQueue; stop: () => void }
|
|
30
|
-
|
|
31
29
|
// what the studio detail view shows about an asset's plumbing (W2-2)
|
|
32
30
|
export type AssetInfo = {
|
|
33
31
|
backend: 'canvas' | 'comfy' | 'manual' | 'gemini'
|
|
@@ -42,9 +40,9 @@ export type AssetInfo = {
|
|
|
42
40
|
kitSource: string
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
// the studio's route table without a server: `handle` answers the
|
|
46
|
-
//
|
|
47
|
-
//
|
|
43
|
+
// the studio's route table without a server: `handle` answers the media
|
|
44
|
+
// path space ('/api/…', '/img', '/panel/…'); the hub — the repo's ONE
|
|
45
|
+
// server — mounts it under /media/*. The page itself is the hub's react app.
|
|
48
46
|
export type StudioRoutes = { handle: (req: Request, pathname: string) => Promise<Response>; queue: StudioQueue }
|
|
49
47
|
|
|
50
48
|
export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
@@ -220,8 +218,6 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
220
218
|
const q = (k: string): string => url.searchParams.get(k) ?? ''
|
|
221
219
|
const body = async (): Promise<Record<string, unknown>> => (await req.json().catch(() => ({}))) as Record<string, unknown>
|
|
222
220
|
|
|
223
|
-
if (pathname === '/') return new Response(deps.page(), { headers: { 'Content-Type': 'text/html; charset=utf-8' } })
|
|
224
|
-
|
|
225
221
|
if (pathname === '/api/state') {
|
|
226
222
|
const assets = await Promise.all(allAssets(kit).map(async (f) => {
|
|
227
223
|
const dir = dirOf(f.asset.id)
|
|
@@ -242,7 +238,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
242
238
|
const dir = dirOf(f.asset.id)
|
|
243
239
|
const pick = await readPick(dir)
|
|
244
240
|
const rolls = listRolls(dir).map((name) => ({ name, picked: name === pick.pick, alt: pick.alts.includes(name), hasMeta: existsSync(metaPath(dir, name)) }))
|
|
245
|
-
const custom = customPanelsOf(f).map((p) => ({ id: p.id, kind: f.kind.id, url:
|
|
241
|
+
const custom = customPanelsOf(f).map((p) => ({ id: p.id, kind: f.kind.id, url: `panel/${encodeURIComponent(f.kind.id)}/${encodeURIComponent(p.id)}.js` }))
|
|
246
242
|
return json({ asset: { ...f.asset, prompt: await promptOf(f), cat: f.kind.category, geometry: geometryOf(f) != null, custom }, info: infoOf(f), rolls, pick })
|
|
247
243
|
}
|
|
248
244
|
|
|
@@ -472,16 +468,3 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
472
468
|
|
|
473
469
|
return { handle, queue }
|
|
474
470
|
}
|
|
475
|
-
|
|
476
|
-
// standalone studio server (`shipkit assets serve`): the same routes on
|
|
477
|
-
// their own port. The hub (`shipkit serve`) mounts studioRoutes under
|
|
478
|
-
// /assets/ instead — one route table, two front doors.
|
|
479
|
-
export async function serveStudio(deps: StudioDeps): Promise<Studio> {
|
|
480
|
-
const routes = await studioRoutes(deps)
|
|
481
|
-
const server = Bun.serve({
|
|
482
|
-
hostname: '127.0.0.1',
|
|
483
|
-
port: deps.kit.port,
|
|
484
|
-
fetch: (req) => routes.handle(req, new URL(req.url).pathname),
|
|
485
|
-
})
|
|
486
|
-
return { server, queue: routes.queue, stop: () => server.stop(true) }
|
|
487
|
-
}
|
package/src/cli/cmd_Assets.ts
CHANGED
|
@@ -2,26 +2,15 @@
|
|
|
2
2
|
// (ComfyUI or procedural canvas), curate in the studio, ship with metadata
|
|
3
3
|
// write-through. Thin clipanion surfaces; logic in assets/run-assets.ts.
|
|
4
4
|
import { Command, Option } from 'clipanion'
|
|
5
|
-
import { runAssetsCheck, runAssetsGen, runAssetsIdentity,
|
|
5
|
+
import { runAssetsCheck, runAssetsGen, runAssetsIdentity, runAssetsTopUp, type AssetsIo } from '../assets/run-assets.ts'
|
|
6
6
|
|
|
7
7
|
const ioOf = (cmd: Command): AssetsIo => ({
|
|
8
8
|
print: (l) => cmd.context.stdout.write(`${l}\n`),
|
|
9
9
|
printErr: (l) => cmd.context.stderr.write(`${l}\n`),
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
static override usage = Command.Usage({
|
|
16
|
-
category: 'Assets',
|
|
17
|
-
description: 'the studio — roll, curate, pick, ship (127.0.0.1, kit port)',
|
|
18
|
-
examples: [['launch the studio for the cwd kit', 'shipkit assets serve']],
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
async execute(): Promise<number> {
|
|
22
|
-
return runAssetsServe(process.cwd(), ioOf(this))
|
|
23
|
-
}
|
|
24
|
-
}
|
|
12
|
+
// the studio UI has no standalone server command — it rides the hub page
|
|
13
|
+
// (`shipkit serve`, the repo's ONE Bun.serve)
|
|
25
14
|
|
|
26
15
|
export class AssetsFillCommand extends Command {
|
|
27
16
|
static override paths = [['assets', 'fill']]
|
package/src/cli/cmd_Check.ts
CHANGED
|
@@ -1,27 +1,45 @@
|
|
|
1
|
-
// shipkit check — periodic health, TTL-cached
|
|
2
|
-
//
|
|
1
|
+
// shipkit check — periodic health, TTL-cached; `--fix` applies rule
|
|
2
|
+
// autofixes then re-lints. Thin clipanion surface; logic in
|
|
3
|
+
// core/run-check.ts and rules/run-fix.ts. ci NEVER fixes.
|
|
3
4
|
import { Command, Option } from 'clipanion'
|
|
4
5
|
import { runCheck } from '../core/run-check.ts'
|
|
6
|
+
import { runFix } from '../rules/run-fix.ts'
|
|
5
7
|
|
|
6
8
|
export class CheckCommand extends Command {
|
|
7
9
|
static override paths = [['check']]
|
|
8
10
|
|
|
9
11
|
static override usage = Command.Usage({
|
|
10
12
|
category: 'Health',
|
|
11
|
-
description: 'periodic health:
|
|
13
|
+
description: 'periodic health: SK rules + every *.check.ts in the repo (auto-discovered, run as subprocesses), clean passes replay from a TTL cache; --fix applies rule autofixes',
|
|
12
14
|
examples: [
|
|
13
15
|
['run (cached where valid)', 'shipkit check'],
|
|
14
16
|
['bust every cache layer', 'shipkit check --fresh'],
|
|
17
|
+
['apply every rule autofix, re-lint', 'shipkit check --fix'],
|
|
18
|
+
['fix one rule', 'shipkit check --fix SK004'],
|
|
19
|
+
['report-only: self-maintaining rules never write', 'shipkit check --frozen'],
|
|
15
20
|
['machine-readable', 'shipkit check --json'],
|
|
16
21
|
],
|
|
17
22
|
})
|
|
18
23
|
|
|
19
24
|
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
20
25
|
fresh = Option.Boolean('--fresh', false, { description: 'bust every TTL cache layer (travels to child checks)' })
|
|
26
|
+
fix = Option.Boolean('--fix', false, { description: 'apply rule autofixes (all fixable rules, or only the listed SK ids), then re-lint' })
|
|
27
|
+
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for self-maintaining rules (SK005 never touches package.json)' })
|
|
28
|
+
fixIds = Option.Rest()
|
|
21
29
|
|
|
22
30
|
async execute(): Promise<number> {
|
|
23
31
|
try {
|
|
24
|
-
|
|
32
|
+
if (this.fixIds.length > 0 && !this.fix) {
|
|
33
|
+
this.context.stderr.write(`FAIL: rule ids need --fix (shipkit check --fix ${this.fixIds.join(' ')})\n`)
|
|
34
|
+
return 2
|
|
35
|
+
}
|
|
36
|
+
if (this.fix && this.json) {
|
|
37
|
+
this.context.stderr.write('FAIL: --fix has no --json shape — run the fix, then `shipkit check --json` for state\n')
|
|
38
|
+
return 2
|
|
39
|
+
}
|
|
40
|
+
if (this.fix)
|
|
41
|
+
return await runFix(process.cwd(), { ids: this.fixIds, print: (l) => this.context.stdout.write(`${l}\n`) })
|
|
42
|
+
return await runCheck(process.cwd(), { json: this.json, fresh: this.fresh, frozen: this.frozen, print: (l) => this.context.stdout.write(`${l}\n`) })
|
|
25
43
|
} catch (e) {
|
|
26
44
|
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
27
45
|
return 1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// shipkit needs — the needs feed as a CLI: everything blocked on a human,
|
|
2
|
+
// red/yellow, grouped by collection. REPORT-ONLY: exit 0 always — `check`
|
|
3
|
+
// and `ci` stay the gates. Thin clipanion surface; logic in collections/.
|
|
4
|
+
import { Command, Option } from 'clipanion'
|
|
5
|
+
import { loadKit } from '../assets/kit-config.ts'
|
|
6
|
+
import { projectNeeds } from '../collections/collection-types.ts'
|
|
7
|
+
import { buildCollections } from '../collections/providers.ts'
|
|
8
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
9
|
+
|
|
10
|
+
export class NeedsCommand extends Command {
|
|
11
|
+
static override paths = [['needs']]
|
|
12
|
+
|
|
13
|
+
static override usage = Command.Usage({
|
|
14
|
+
category: 'Health',
|
|
15
|
+
description: 'everything blocked on a human, red/yellow by collection — report-only, exit 0 always (check/ci are the gates)',
|
|
16
|
+
examples: [
|
|
17
|
+
['what needs me', 'shipkit needs'],
|
|
18
|
+
['machine-readable (agents, drills, cross-repo rollups)', 'shipkit needs --json'],
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
json = Option.Boolean('--json', false, { description: 'emit NeedItem[] JSON' })
|
|
23
|
+
|
|
24
|
+
async execute(): Promise<number> {
|
|
25
|
+
const root = process.cwd()
|
|
26
|
+
const c = await loadRepoConfig(root)
|
|
27
|
+
const kit = await loadKit(root).catch(() => null)
|
|
28
|
+
const needs = await projectNeeds(buildCollections(root, c, kit))
|
|
29
|
+
if (this.json) {
|
|
30
|
+
this.context.stdout.write(`${JSON.stringify(needs, null, 2)}\n`)
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
if (needs.length === 0) {
|
|
34
|
+
this.context.stdout.write('ok: nothing needs you\n')
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
for (const n of needs) {
|
|
38
|
+
const line = `${n.severity === 'red' ? 'red: ' : 'yellow:'} [${n.collection}] ${n.text}`
|
|
39
|
+
this.context.stdout.write(`${line}\n`)
|
|
40
|
+
if (n.artifact?.copy != null) this.context.stdout.write(` paste: ${n.artifact.copy}\n`)
|
|
41
|
+
if (n.artifact?.url != null) this.context.stdout.write(` mint: ${n.artifact.url}\n`)
|
|
42
|
+
}
|
|
43
|
+
const reds = needs.filter((n) => n.severity === 'red').length
|
|
44
|
+
this.context.stdout.write(`${reds} red · ${needs.length - reds} yellow\n`)
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
}
|