rvlib-shipkit 0.2.0 → 0.3.1
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 +71 -0
- package/package.json +2 -1
- package/src/assets/kit-config.ts +19 -1
- package/src/assets/kits/identity-kit.ts +62 -23
- package/src/assets/run-assets.ts +25 -5
- package/src/assets/server/studio-page.ts +37 -4
- package/src/assets/server/studio-server.ts +265 -213
- package/src/cli/cmd_Serve.ts +34 -0
- package/src/cli/cmd_Tui.ts +5 -19
- package/src/cli.ts +2 -0
- package/src/init-repo.ts +10 -0
- package/src/repo-config.ts +16 -1
- package/src/serve/hub-pages.ts +84 -0
- package/src/serve/run-serve.ts +85 -0
- package/src/serve/serve-hub.ts +77 -0
- package/src/shipkit.ts +10 -5
- package/src/tui/run-tui.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-07-22
|
|
4
|
+
|
|
5
|
+
- fix: `react-dom` was missing from dependencies AND the CLI entry loaded
|
|
6
|
+
the react/ink chain eagerly — every command crashed in a fresh consumer
|
|
7
|
+
repo without react-dom (found by the rabbits-vs-sheeps swap in the field,
|
|
8
|
+
confirmed against the 0.3.0 tarball). tui now lazy-loads its body; deps
|
|
9
|
+
carry react-dom. Proven by a fresh-repo install test (init → ci green in
|
|
10
|
+
a react-less repo).
|
|
11
|
+
- fix: `init` warns loudly when rvlib-shipkit is not installed in the repo
|
|
12
|
+
(the minted scripts call its bin — previously exit 127 with no hint).
|
|
13
|
+
|
|
14
|
+
## 0.3.0 — 2026-07-22
|
|
15
|
+
|
|
16
|
+
New:
|
|
17
|
+
|
|
18
|
+
- identity from a PICKED comfy mark (W2-1): `renderIdentityFromMark` is THE
|
|
19
|
+
identity writer (mark png → logo 1024² master → favicon 512² → og card
|
|
20
|
+
compositing bg + mark left + name/tagline right, fonts shrink-to-fit);
|
|
21
|
+
canvas marks and picked comfy rolls both feed it — one render path.
|
|
22
|
+
`identityKinds(brand)` now carries a BUILT-IN ship hook (`Kind.ship.fn`,
|
|
23
|
+
new — a lib-side ShipFn; `ship.fn` + `ship.via` on one kind is a kit
|
|
24
|
+
error), so picking a logo roll in the studio re-renders the whole set
|
|
25
|
+
automatically. `shipkit assets identity` with a comfy mark renders from
|
|
26
|
+
the current pick and FAILs loud with the next step when nothing is picked
|
|
27
|
+
("roll + pick in the studio: bun run assets → #identity-logo") — never a
|
|
28
|
+
placeholder. `Brand.markPrompt` = the full roll prompt for the mark.
|
|
29
|
+
Exports: `renderIdentityFromMark`, `NO_PICK_FIX`, `ShipContext`, `ShipFn`.
|
|
30
|
+
|
|
31
|
+
- `shipkit serve` — ONE hub server per repo, ONE port: a no-build landing
|
|
32
|
+
page whose links are the whole toolbox. Routes: `/graph/wiki` (md link
|
|
33
|
+
graph — repo.config `serve.wikiGraph` plugs a custom provider, generic
|
|
34
|
+
scan otherwise) · `/graph/agent` (when `agent/` exists; `serve.agentIndex`
|
|
35
|
+
picks the entrypoint) · `/assets/` (the studio, mounted IN-PROCESS on the
|
|
36
|
+
same port) · `/status` (STATUS.md rendered + a "run check" button that
|
|
37
|
+
shells `shipkit check` and streams the wire lines). The same URLs print
|
|
38
|
+
to stdout as clickable `ok:` lines — the intended wiring is ONE vscode
|
|
39
|
+
folder-open task. Port: `serve.port`, else hashed from the repo name into
|
|
40
|
+
4800-4979; a taken port is probed via `/hub.json` — the same repo in
|
|
41
|
+
another window exits 0, a DIFFERENT repo fails loud with a fix pointer.
|
|
42
|
+
- studio routes are mountable: `studioRoutes(deps)` returns the route table
|
|
43
|
+
(`handle(req, pathname)`); `serveStudio` (standalone `shipkit assets
|
|
44
|
+
serve`, own port) and the hub's `/assets/` mount share it — one route
|
|
45
|
+
table, two front doors. The studio page now resolves its urls RELATIVE to
|
|
46
|
+
the page (works at `/` and under `/assets/`); custom panel module urls
|
|
47
|
+
are `./panel/…` accordingly.
|
|
48
|
+
- repo.config gains `serve?: { port?, wikiGraph?, agentIndex? }`
|
|
49
|
+
(`ServeConfig`).
|
|
50
|
+
- studio per-asset info surface (W2-2): the detail view shows where every
|
|
51
|
+
roll comes from and where a pick goes — backend, resolved workflow id +
|
|
52
|
+
its `.rv-comfy/` path (or WHY it's unresolved), ship target (built-in /
|
|
53
|
+
hook / record), meta write-through file, kit source. `/api/asset` gains
|
|
54
|
+
an additive `info` block. Deterministic canvas kinds (draw takes no seed
|
|
55
|
+
→ identical rolls) are labelled next to Generate and capped to one roll
|
|
56
|
+
instead of pretending to be a slot machine.
|
|
57
|
+
- `shipkit assets check` prints workflow provenance (W2-3): one `ok:` line
|
|
58
|
+
per bound pair with its repo-relative `.{json,ts}` path — there is no
|
|
59
|
+
global workflow store, git owns each repo's pairs; the sharing doctrine
|
|
60
|
+
lives in build/comfyui.md.
|
|
61
|
+
- a second `shipkit serve` launch on an already-owned port now prints the
|
|
62
|
+
full clickable tool URLs (from the owner's `/hub.json`), not just the hub
|
|
63
|
+
root — a re-run terminal is as clickable as a first run.
|
|
64
|
+
|
|
65
|
+
Fixed:
|
|
66
|
+
|
|
67
|
+
- `/status` page: STATUS.md content is `<`-escaped inside the inline
|
|
68
|
+
script — a literal `</script>` in a check line can no longer break the
|
|
69
|
+
page.
|
|
70
|
+
|
|
71
|
+
Breaking: none (0.2.0 consumers upgrade clean; `Kind.ship.fn` and `serve`
|
|
72
|
+
config are additive).
|
|
73
|
+
|
|
3
74
|
## 0.2.0 — 2026-07-22
|
|
4
75
|
|
|
5
76
|
The one-runner release: shipkit becomes the all-in-one companion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"mobx": "^6.16.1",
|
|
30
30
|
"mobx-react-lite": "^4.1.1",
|
|
31
31
|
"react": "^19.2.7",
|
|
32
|
+
"react-dom": "^19.2.7",
|
|
32
33
|
"rvlib-ts-tools": "^0.2.0",
|
|
33
34
|
"sharp": "^0.35.3"
|
|
34
35
|
}
|
package/src/assets/kit-config.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// Kit edits need a studio restart; prompt tweaks don't (overlay).
|
|
6
6
|
import { existsSync } from 'fs'
|
|
7
7
|
import { join } from 'path'
|
|
8
|
+
import type { alphaHeight, toPng } from './image-ops.ts'
|
|
9
|
+
import type { GroundRect } from './panels/geometry-rects.ts'
|
|
8
10
|
|
|
9
11
|
export type AssetSpec = {
|
|
10
12
|
id: string
|
|
@@ -61,6 +63,19 @@ export type KindMeta = {
|
|
|
61
63
|
validate?: (data: unknown) => string[] | void
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
/** What a ship hook receives when a roll is picked (studio /api/pick). */
|
|
67
|
+
export type ShipContext = {
|
|
68
|
+
assetId: string
|
|
69
|
+
file: string
|
|
70
|
+
asset: AssetSpec
|
|
71
|
+
kind: Kind
|
|
72
|
+
meta: unknown
|
|
73
|
+
rect: GroundRect | null
|
|
74
|
+
projectRoot: string
|
|
75
|
+
img: { toPng: typeof toPng; alphaHeight: typeof alphaHeight }
|
|
76
|
+
}
|
|
77
|
+
export type ShipFn = (ctx: ShipContext) => Promise<string | void>
|
|
78
|
+
|
|
64
79
|
export type Kind = {
|
|
65
80
|
id: string
|
|
66
81
|
category: string
|
|
@@ -68,7 +83,9 @@ export type Kind = {
|
|
|
68
83
|
generate: GeneratorSpec
|
|
69
84
|
/** icon kinds get no geometry, thumbnails only */
|
|
70
85
|
mode?: 'sprite' | 'icon'
|
|
71
|
-
|
|
86
|
+
/** via = repo hook module path (default .rv-assets/ship.ts) · fn = a
|
|
87
|
+
* built-in lib hook (e.g. identityKinds re-rendering the set) — never both */
|
|
88
|
+
ship?: { dir?: string; via?: string; fn?: ShipFn }
|
|
72
89
|
meta?: KindMeta
|
|
73
90
|
}
|
|
74
91
|
|
|
@@ -92,6 +109,7 @@ export function validateKit(k: KitConfig): string[] {
|
|
|
92
109
|
if (kindIds.has(kind.id)) errs.push(`duplicate kind id '${kind.id}'`)
|
|
93
110
|
kindIds.add(kind.id)
|
|
94
111
|
if (kind.generate?.backend === 'comfy' && k.comfy == null) errs.push(`kind '${kind.id}' uses the comfy backend but the kit has no comfy config`)
|
|
112
|
+
if (kind.ship?.fn != null && kind.ship?.via != null) errs.push(`kind '${kind.id}': ship.fn and ship.via are two hooks for one pick — keep one`)
|
|
95
113
|
if (!Array.isArray(kind.assets) || kind.assets.length === 0) errs.push(`kind '${kind.id}' has no assets — a silent no-op kind is a config mistake`)
|
|
96
114
|
for (const a of kind.assets ?? []) {
|
|
97
115
|
if (!a.id || typeof a.prompt !== 'string') errs.push(`kind '${kind.id}': asset needs id + prompt (got ${JSON.stringify(a).slice(0, 80)})`)
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
// kits/identity — favicon / logo / OG as asset kinds.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// -
|
|
5
|
-
//
|
|
1
|
+
// kits/identity — favicon / logo / OG as asset kinds. One render path
|
|
2
|
+
// (renderIdentityFromMark: mark png → logo master 1024², favicon 512², og
|
|
3
|
+
// card), two ways to get the mark:
|
|
4
|
+
// - canvas mark: deterministic, instant — `shipkit assets identity` draws it
|
|
5
|
+
// headlessly and renders the set.
|
|
6
|
+
// - comfy mark: roll-and-pick in the studio; the kind carries a BUILT-IN ship
|
|
7
|
+
// hook, so picking a logo roll re-renders the whole identity set from the
|
|
8
|
+
// picked png. Headless `identity` uses the current pick — no pick yet is a
|
|
9
|
+
// loud FAIL, never a placeholder.
|
|
6
10
|
// Outputs are COMMITTED: public/favicon.png (512² master), public/og.png
|
|
7
11
|
// (1200×630), public/logo.png. Brand data is per-repo and never defaults
|
|
8
12
|
// across repos.
|
|
9
|
-
import { createCanvas, type SKRSContext2D } from '@napi-rs/canvas'
|
|
13
|
+
import { createCanvas, loadImage, type Image, type SKRSContext2D } from '@napi-rs/canvas'
|
|
10
14
|
import { mkdirSync } from 'fs'
|
|
11
15
|
import { dirname, join } from 'path'
|
|
12
16
|
import sharp from 'sharp'
|
|
@@ -19,7 +23,11 @@ export type Brand = {
|
|
|
19
23
|
palette: { bg: string; accent: string; ink: string }
|
|
20
24
|
/** the mark: a canvas draw (deterministic) or a comfy generator (roll-and-pick) */
|
|
21
25
|
mark: CanvasGen | ComfyGen
|
|
22
|
-
/**
|
|
26
|
+
/** comfy marks: the FULL generation prompt for identity-logo rolls
|
|
27
|
+
* (style included — the studio shows/edits exactly this) */
|
|
28
|
+
markPrompt?: string
|
|
29
|
+
/** og layout hook — replaces the WHOLE default card (bg + mark left +
|
|
30
|
+
* name/tagline right); the hook owns compositing the mark if it wants one */
|
|
23
31
|
drawOg?: (ctx: SKRSContext2D, brand: Brand, size: { w: number; h: number }) => void
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -28,36 +36,54 @@ export const OG_W = 1200
|
|
|
28
36
|
export const OG_H = 630
|
|
29
37
|
export const LOGO_SIZE = 1024
|
|
30
38
|
|
|
39
|
+
/** the one next step when a comfy mark has no picked roll yet */
|
|
40
|
+
export const NO_PICK_FIX = 'roll + pick in the studio: bun run assets → #identity-logo'
|
|
41
|
+
|
|
31
42
|
export function identityKinds(brand: Brand): Kind[] {
|
|
32
43
|
return [
|
|
33
44
|
defineKind({
|
|
34
45
|
id: 'identity-logo', category: 'Identity', mode: 'icon',
|
|
35
|
-
assets: [{ id: 'identity-logo', prompt: `logo mark for ${brand.name}`, label: 'logo' }],
|
|
46
|
+
assets: [{ id: 'identity-logo', prompt: brand.markPrompt ?? `logo mark for ${brand.name}`, label: 'logo' }],
|
|
36
47
|
generate: brand.mark,
|
|
48
|
+
// built-in ship: picking a logo roll re-renders the WHOLE identity set
|
|
49
|
+
ship: { fn: async (ctx) => (await renderIdentityFromMark(brand, ctx.projectRoot, await sharp(ctx.file).png().toBuffer())).join(' + ') },
|
|
37
50
|
}),
|
|
38
51
|
]
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
// shrink px until text fits maxW (og cards must never clip the tagline)
|
|
55
|
+
const fitFont = (ctx: SKRSContext2D, text: string, px: number, maxW: number, bold = false): void => {
|
|
56
|
+
for (; px > 12; px -= 2) {
|
|
57
|
+
ctx.font = `${bold ? 'bold ' : ''}${px}px sans-serif`
|
|
58
|
+
if (ctx.measureText(text).width <= maxW) return
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const defaultOg = (ctx: SKRSContext2D, brand: Brand, size: { w: number; h: number }, mark: Image): void => {
|
|
42
63
|
ctx.fillStyle = brand.palette.bg
|
|
43
64
|
ctx.fillRect(0, 0, size.w, size.h)
|
|
44
65
|
ctx.fillStyle = brand.palette.accent
|
|
45
66
|
ctx.fillRect(0, size.h - 14, size.w, 14)
|
|
67
|
+
const pad = 72
|
|
68
|
+
const markSize = size.h - pad * 2
|
|
69
|
+
ctx.drawImage(mark, pad, pad, markSize, markSize)
|
|
70
|
+
const textX = pad + markSize + 64
|
|
71
|
+
const maxW = size.w - textX - 48
|
|
46
72
|
ctx.fillStyle = brand.palette.ink
|
|
47
|
-
ctx.
|
|
73
|
+
fitFont(ctx, brand.name, Math.round(size.h / 6), maxW, true)
|
|
48
74
|
ctx.textBaseline = 'middle'
|
|
49
|
-
ctx.fillText(brand.name,
|
|
75
|
+
ctx.fillText(brand.name, textX, size.h / 2 - (brand.tagline != null ? size.h / 12 : 0))
|
|
50
76
|
if (brand.tagline != null) {
|
|
51
|
-
ctx.font = `${Math.round(size.h / 14)}px sans-serif`
|
|
52
77
|
ctx.fillStyle = brand.palette.accent
|
|
53
|
-
ctx
|
|
78
|
+
fitFont(ctx, brand.tagline, Math.round(size.h / 14), maxW)
|
|
79
|
+
ctx.fillText(brand.tagline, textX + 2, size.h / 2 + size.h / 9)
|
|
54
80
|
}
|
|
55
81
|
}
|
|
56
82
|
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
/** THE identity writer — any mark png (canvas draw or picked comfy roll) →
|
|
84
|
+
* public/logo.png (1024² master) + public/favicon.png (512² resize) +
|
|
85
|
+
* public/og.png (default card: bg + mark left + name/tagline right). */
|
|
86
|
+
export async function renderIdentityFromMark(brand: Brand, root: string, markPng: Buffer): Promise<string[]> {
|
|
61
87
|
const out: string[] = []
|
|
62
88
|
const write = async (rel: string, buf: Buffer): Promise<void> => {
|
|
63
89
|
const path = join(root, rel)
|
|
@@ -65,16 +91,29 @@ export async function renderIdentity(brand: Brand, root: string): Promise<string
|
|
|
65
91
|
await Bun.write(path, buf)
|
|
66
92
|
out.push(rel)
|
|
67
93
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const logoPng = logo.toBuffer('image/png')
|
|
94
|
+
const logoPng = await sharp(markPng)
|
|
95
|
+
.resize(LOGO_SIZE, LOGO_SIZE, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
96
|
+
.png().toBuffer()
|
|
72
97
|
await write('public/logo.png', logoPng)
|
|
73
98
|
// favicon = logo master resized to 512² (browsers derive the rest)
|
|
74
99
|
await write('public/favicon.png', await sharp(logoPng).resize(FAVICON_SIZE, FAVICON_SIZE).png().toBuffer())
|
|
75
|
-
// og card
|
|
76
100
|
const og = createCanvas(OG_W, OG_H)
|
|
77
|
-
|
|
101
|
+
const ctx = og.getContext('2d')
|
|
102
|
+
if (brand.drawOg != null) brand.drawOg(ctx, brand, { w: OG_W, h: OG_H })
|
|
103
|
+
else defaultOg(ctx, brand, { w: OG_W, h: OG_H }, await loadImage(logoPng))
|
|
78
104
|
await write('public/og.png', og.toBuffer('image/png'))
|
|
79
105
|
return out
|
|
80
106
|
}
|
|
107
|
+
|
|
108
|
+
/** Headless render of the full identity set. Canvas marks draw themselves;
|
|
109
|
+
* comfy marks need `markFile` = the PICKED roll (the caller resolves the
|
|
110
|
+
* pick — run-assets does) and FAIL loud without one. */
|
|
111
|
+
export async function renderIdentity(brand: Brand, root: string, opts?: { markFile?: string }): Promise<string[]> {
|
|
112
|
+
if (brand.mark.backend === 'canvas') {
|
|
113
|
+
const logo = createCanvas(LOGO_SIZE, LOGO_SIZE)
|
|
114
|
+
await brand.mark.draw(logo.getContext('2d'), { id: 'identity-logo', prompt: '' })
|
|
115
|
+
return renderIdentityFromMark(brand, root, logo.toBuffer('image/png'))
|
|
116
|
+
}
|
|
117
|
+
if (opts?.markFile == null) throw new Error(`brand '${brand.name}' uses a comfy mark and no roll is picked — ${NO_PICK_FIX}`)
|
|
118
|
+
return renderIdentityFromMark(brand, root, await sharp(opts.markFile).png().toBuffer())
|
|
119
|
+
}
|
package/src/assets/run-assets.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// parsing; these plain functions own the logic, print through injected
|
|
3
3
|
// sinks, and return exit codes — 0 ok · 1 fail).
|
|
4
4
|
import { existsSync } from 'fs'
|
|
5
|
-
import { join } from 'path'
|
|
5
|
+
import { join, relative } from 'path'
|
|
6
6
|
import { randomSeed } from './gen/comfy-gen.ts'
|
|
7
7
|
import { comfyResolver, generateRoll, type ComfyResolver } from './gen/generate-roll.ts'
|
|
8
8
|
import { loadWorkflows } from './gen/workflow-loader.ts'
|
|
9
9
|
import { allAssets, findAsset, loadKit, type ResolvedKit } from './kit-config.ts'
|
|
10
|
-
import { renderIdentity, type Brand } from './kits/identity-kit.ts'
|
|
11
|
-
import { assetDir, listRolls, readPromptOverlay } from './roll-workspace.ts'
|
|
10
|
+
import { NO_PICK_FIX, renderIdentity, type Brand } from './kits/identity-kit.ts'
|
|
11
|
+
import { assetDir, listRolls, readPick, readPromptOverlay } from './roll-workspace.ts'
|
|
12
12
|
import { page } from './server/studio-page.ts'
|
|
13
13
|
import { serveStudio } from './server/studio-server.ts'
|
|
14
14
|
|
|
@@ -91,7 +91,19 @@ export const runAssetsIdentity = (root: string, io: AssetsIo): Promise<number> =
|
|
|
91
91
|
if (!existsSync(brandPath)) die('no .rv-identity/brand.ts — create one exporting a Brand (name, palette, mark)')
|
|
92
92
|
const mod = (await import(brandPath)) as { default?: Brand }
|
|
93
93
|
if (mod.default == null) die('.rv-identity/brand.ts must default-export the Brand')
|
|
94
|
-
const
|
|
94
|
+
const brand = mod.default!
|
|
95
|
+
let markFile: string | undefined
|
|
96
|
+
if (brand.mark.backend === 'comfy') {
|
|
97
|
+
// comfy marks render from the PICKED identity-logo roll — the kit
|
|
98
|
+
// owns the workspace where picks live
|
|
99
|
+
const kit = await loadKit(root).catch((e: Error) => die(`comfy mark needs the kit's roll workspace — ${e.message}`))
|
|
100
|
+
const dir = assetDir(join(root, kit.workspace), 'identity-logo')
|
|
101
|
+
const pick = await readPick(dir)
|
|
102
|
+
if (pick.pick == null) die(`brand '${brand.name}' uses a comfy mark and none is picked — ${NO_PICK_FIX}`)
|
|
103
|
+
markFile = join(dir, pick.pick!)
|
|
104
|
+
if (!existsSync(markFile)) die(`picked roll ${pick.pick} vanished from ${dir} — re-pick in the studio`)
|
|
105
|
+
}
|
|
106
|
+
const written = await renderIdentity(brand, root, { markFile })
|
|
95
107
|
for (const w of written) io.print(`ok: ${w}`)
|
|
96
108
|
return 0
|
|
97
109
|
})
|
|
@@ -105,7 +117,15 @@ export const runAssetsCheck = (root: string, io: AssetsIo): Promise<number> =>
|
|
|
105
117
|
if (kit.comfy != null) {
|
|
106
118
|
try {
|
|
107
119
|
const wfs = await loadWorkflows(join(root, kit.comfy.workflows))
|
|
108
|
-
io.print(`ok: ${wfs.size} workflow pair(s) bound
|
|
120
|
+
io.print(`ok: ${wfs.size} workflow pair(s) bound`)
|
|
121
|
+
// provenance: where each pair lives + that both halves validated
|
|
122
|
+
// (loadWorkflows throws on any invalid pair, so reaching here = valid).
|
|
123
|
+
// There is no global workflow store — git owns each repo's pairs; the
|
|
124
|
+
// house library (wiki/build/comfyui/references) is the copy-from source.
|
|
125
|
+
for (const wf of wfs.values()) {
|
|
126
|
+
const rel = relative(root, wf.jsonPath).replace(/\.json$/, '')
|
|
127
|
+
io.print(`ok: workflow '${wf.id}' → ${rel}.{json,ts} (pair valid)`)
|
|
128
|
+
}
|
|
109
129
|
} catch (e) {
|
|
110
130
|
io.print(`FAIL: ${(e as Error).message}`)
|
|
111
131
|
bad++
|
|
@@ -42,6 +42,14 @@ button.ghost{background:var(--panel2);color:var(--ink);border:1px solid var(--li
|
|
|
42
42
|
button:disabled{opacity:.45;cursor:default}
|
|
43
43
|
select,input[type=number]{background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px}
|
|
44
44
|
.status{font-size:12px;color:var(--dim);min-height:16px}
|
|
45
|
+
.detnote{font-size:11px;color:var(--teal);line-height:1.25;max-width:150px}
|
|
46
|
+
/* ── per-asset info surface (W2-2) ── */
|
|
47
|
+
.info{display:none;flex-wrap:wrap;gap:6px;align-items:center;padding:6px 16px;border-bottom:1px solid var(--line);background:var(--panel2)}
|
|
48
|
+
.info .chip{display:inline-flex;align-items:center;gap:5px;background:var(--bg);border:1px solid var(--line);border-radius:6px;
|
|
49
|
+
padding:2px 8px;font-size:11px;color:var(--ink);max-width:360px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
50
|
+
.info .chip b{color:var(--accent);font-weight:650;letter-spacing:.05em;text-transform:uppercase;font-size:10px}
|
|
51
|
+
.info .chip.warn{border-color:var(--red);color:#f0c0c0}.info .chip.warn b{color:var(--red)}
|
|
52
|
+
.info .chip code{font:11px ui-monospace,Menlo,monospace;color:var(--dim)}
|
|
45
53
|
.errs{background:#3a1717;border-bottom:1px solid #5a2222;color:#f0c0c0;font:12px/1.5 ui-monospace,Menlo,monospace;
|
|
46
54
|
padding:6px 16px;white-space:pre-wrap;display:none}
|
|
47
55
|
.stage{position:relative;flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:18px;overflow:hidden;
|
|
@@ -130,6 +138,7 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
130
138
|
<div class=ctl>
|
|
131
139
|
<select id=n><option>1</option><option selected>3</option><option>6</option><option>9</option><option>12</option></select>
|
|
132
140
|
<button id=go disabled>Generate</button>
|
|
141
|
+
<div id=detnote class=detnote style="display:none">deterministic — rolls identical</div>
|
|
133
142
|
<button id=qclear class=ghost title="drop queued jobs">clear queue</button>
|
|
134
143
|
</div>
|
|
135
144
|
<div class=promptwrap>
|
|
@@ -139,6 +148,7 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
139
148
|
</div>
|
|
140
149
|
<div class=ctl><div class=status id=status></div></div>
|
|
141
150
|
</div>
|
|
151
|
+
<div class=info id=info></div>
|
|
142
152
|
<div class=stage id=stage><div class=ph>Pick an asset on the left, then Generate.</div>
|
|
143
153
|
<div class=plumb id=plumbL></div><div class=plumb id=plumbR></div>
|
|
144
154
|
<div id=ov><div class=hdl></div><div class=arrow></div><div class=sz id=ovsz></div></div>
|
|
@@ -180,10 +190,11 @@ let lastSideSig=null,lastRollSig=null,catMode=null,lastCatSig=null,catRows=[]
|
|
|
180
190
|
let rect=null,rectRatio=0.6,hasGeom=false,saveTimer=null
|
|
181
191
|
const TEX=1024,MIX_UNIT=24,MIX_GROUND=44
|
|
182
192
|
const V=()=>Date.now()>>13 // ~8s cache buckets: thumbs refresh without unique-URL spam
|
|
183
|
-
const
|
|
193
|
+
const R=u=>u.replace(/^\\//,'') // resolve against the page URL: '/' standalone AND '/assets/' under the shipkit hub
|
|
194
|
+
const j=async(u,o)=>{const r=await fetch(R(u),o);return r.json()}
|
|
184
195
|
const post=(u,b)=>j(u,{method:'POST',body:JSON.stringify(b)})
|
|
185
|
-
const imgUrl=(id,n)=>'/img?id='+encodeURIComponent(id)+'&name='+encodeURIComponent(n)+'&t='+V()
|
|
186
|
-
const thumbUrl=id=>'/api/thumb?id='+encodeURIComponent(id)+'&t='+V()
|
|
196
|
+
const imgUrl=(id,n)=>R('/img?id='+encodeURIComponent(id)+'&name='+encodeURIComponent(n)+'&t='+V())
|
|
197
|
+
const thumbUrl=id=>R('/api/thumb?id='+encodeURIComponent(id)+'&t='+V())
|
|
187
198
|
const esc=s=>String(s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"')
|
|
188
199
|
const jsq=s=>String(s).replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'")
|
|
189
200
|
// ── hash deep-links: #<asset-id> or #cat-<Category> ──
|
|
@@ -212,6 +223,24 @@ function renderSide(list){
|
|
|
212
223
|
'</b><span>'+a.rolls+' rolls</span></div><div class="dot '+(a.picked?'done':(a.rolls?'some':''))+'"></div></div>').join('')
|
|
213
224
|
}).join('')
|
|
214
225
|
}
|
|
226
|
+
// ── per-asset info surface (W2-2): backend, workflow provenance, ship target,
|
|
227
|
+
// meta file, kit source — so 'bun assets' is never a blank stare ──
|
|
228
|
+
function renderInfo(info){
|
|
229
|
+
const box=$('#info')
|
|
230
|
+
if(!info){box.style.display='none';box.innerHTML='';return}
|
|
231
|
+
const chip=(k,v,cls)=>'<span class="chip'+(cls?' '+cls:'')+'"><b>'+esc(k)+'</b> '+v+'</span>'
|
|
232
|
+
const chips=[chip('backend',esc(info.backend))]
|
|
233
|
+
if(info.workflow){const w=info.workflow
|
|
234
|
+
if(w.id)chips.push(chip('workflow',esc(w.id)+(w.path?' <code>'+esc(w.path)+'</code>':'')))
|
|
235
|
+
else chips.push(chip('workflow',esc(w.note||'unresolved'),'warn'))}
|
|
236
|
+
const sh=info.ship
|
|
237
|
+
const shv=sh.mode==='hook'?'<code>'+esc(sh.path)+'</code>':
|
|
238
|
+
sh.mode==='built-in'?'built-in hook — '+esc(sh.note||''):esc(sh.note||'pick.json only')
|
|
239
|
+
chips.push(chip('ship',shv))
|
|
240
|
+
if(info.metaFile)chips.push(chip('meta','<code>'+esc(info.metaFile)+'</code>'))
|
|
241
|
+
chips.push(chip('kit','<code>'+esc(info.kitSource)+'</code> · '+esc(info.kindId)))
|
|
242
|
+
box.innerHTML=chips.join('');box.style.display='flex'
|
|
243
|
+
}
|
|
215
244
|
// ── asset view ──
|
|
216
245
|
async function selectAsset(id,writeHash=true){
|
|
217
246
|
exitCat()
|
|
@@ -230,6 +259,10 @@ async function selectAsset(id,writeHash=true){
|
|
|
230
259
|
renderCustomTabs()
|
|
231
260
|
$('#prompt').value=r.asset?r.asset.prompt:a.prompt
|
|
232
261
|
$('#go').disabled=false;$('#saveprompt').disabled=false;$('#promptmsg').textContent=''
|
|
262
|
+
// deterministic canvas kinds: a slot machine with one outcome — cap rolls at 1
|
|
263
|
+
const det=!!(r.info&&r.info.deterministic),n=$('#n')
|
|
264
|
+
n.disabled=det;if(det)n.value='1';$('#detnote').style.display=det?'':'none'
|
|
265
|
+
renderInfo(r.info)
|
|
233
266
|
await renderRolls(true);loadTab()
|
|
234
267
|
}
|
|
235
268
|
function setStage(src){
|
|
@@ -420,7 +453,7 @@ window.addEventListener('resize',positionOverlay)
|
|
|
420
453
|
async function showCat(c,writeHash=true){
|
|
421
454
|
catMode=c;cur=null;lastSideSig=null;lastCatSig=null
|
|
422
455
|
if(writeHash)setHash('cat-'+c)
|
|
423
|
-
$('#topbar').style.display='none';$('#stage').style.display='none';$('#strip').style.display='none'
|
|
456
|
+
$('#topbar').style.display='none';$('#stage').style.display='none';$('#strip').style.display='none';$('#info').style.display='none'
|
|
424
457
|
$('#catgrid').style.display='block';$('#allcat').textContent=c
|
|
425
458
|
openCats.add(c);renderSide(assets)
|
|
426
459
|
await fillCat(true)
|