rvlib-shipkit 2.7.0 → 2.8.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 +6 -0
- package/package.json +11 -3
- package/src/assets/gen/comfyts-gen.ts +56 -25
- package/src/assets/kits/identity-kit.ts +23 -4
- package/src/trailer/sound/comfy.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.8.0
|
|
2
|
+
|
|
3
|
+
- **A brand's `icon` draws the favicon family at each size.** `icon: canvas(draw)` in `.shipkit/identity/brand.ts` draws `favicon.png` (512), `favicon-32.png`, `favicon-16.png` and `apple-touch-icon.png` (180, flattened on `palette.bg`), each at its own size, `asset.id` naming the file, so a 16 px tab reads a shape made for it, never a shrunk logo. Without one the favicon is the logo master resized, as before.
|
|
4
|
+
- **comfy-ts is an optional peer, no longer a dependency.** A repo that never declares comfy no longer installs it (npm and bun both skip an optional peer). The toolkit names comfy-ts in no import, a type import included, so importing it, `sk ci`, `sk serve`, the hub and `sk assets identity` never load it. It is loaded at the first workflow load, from the repo's copy. A repo with comfy kinds or `.cflow.ts` modules installs it itself: `bun add -d comfy-ts`. Without it, a workflow load fails loud with that line, and `sk sound` fails loud when no `comfy-ts` is on PATH. `LoadedWorkflow.wf` is typed by the toolkit's own `CflowWorkflow`, the slice of comfy-ts it drives, held to comfy-ts's types by a test.
|
|
5
|
+
- **`og.png` is written small.** The identity writer encodes the og card as a 256 colour palette png at maximum compression: invisible at preview size, and a photo card drops about 3x (pkm-multi's banner card: 970 KB → 280 KB), under the size where some chat apps stop showing a preview. The file name and format stay `og.png`.
|
|
6
|
+
|
|
1
7
|
## 2.7.0
|
|
2
8
|
|
|
3
9
|
- **What waits on the owner is a pill on every repo's row of the project list.** Beside the open asks badge, a pill in the review hue counts the cards that wait on the owner's verdict or answer; zero draws none, and folded it rides the icon's top left corner. A click opens that repo's review page on its first waiting card, the list on the owner's move. One count, `waitingCards` (`collections/review.ts`), read by the tree's review row, `sk needs`, the review page and the list; `/api/projects.json` reads it on every request, so a new card shows within one poll and a verdict at once. Below 768 px the list is always the rail, so the pills show on a phone.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"@types/bun": "^1.3.14",
|
|
26
26
|
"@types/react": "^19.2.17",
|
|
27
27
|
"@types/react-dom": "^19.2.3",
|
|
28
|
-
"@types/sanitize-html": "^2.16.1"
|
|
28
|
+
"@types/sanitize-html": "^2.16.1",
|
|
29
|
+
"comfy-ts": "^2.9.0"
|
|
29
30
|
},
|
|
30
31
|
"overrides": {
|
|
31
32
|
"mobx": "^7.0.0",
|
|
@@ -38,7 +39,6 @@
|
|
|
38
39
|
"@floating-ui/react": "^0.27.20",
|
|
39
40
|
"@napi-rs/canvas": "^1.1.0",
|
|
40
41
|
"clipanion": "^4.0.0-rc.4",
|
|
41
|
-
"comfy-ts": "^2.9.0",
|
|
42
42
|
"ink": "^7.1.1",
|
|
43
43
|
"marked": "^18.0.7",
|
|
44
44
|
"mobx": "^7.0.0",
|
|
@@ -48,5 +48,13 @@
|
|
|
48
48
|
"react-resizable-panels": "^4.13.3",
|
|
49
49
|
"sanitize-html": "^2.17.6",
|
|
50
50
|
"sharp": "^0.35.3"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"comfy-ts": "^2.9.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"comfy-ts": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
}
|
|
@@ -4,16 +4,40 @@
|
|
|
4
4
|
// must disconnectAll() in a finally, or the host websocket keeps bun alive.
|
|
5
5
|
// Workflows are discriminated by duck-typing, never instanceof: a consumer's
|
|
6
6
|
// .cflow.ts may hold a second copy of comfy-ts in its module graph.
|
|
7
|
+
// comfy-ts is an optional peer: this file is the one place that loads it, at
|
|
8
|
+
// the first workflow load, and never names it in an import, so a repo without
|
|
9
|
+
// it imports and typechecks the toolkit (tests/comfy-optional.test.ts).
|
|
7
10
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs'
|
|
8
11
|
import { basename, join } from 'path'
|
|
9
12
|
import { pathToFileURL } from 'url'
|
|
10
|
-
|
|
13
|
+
|
|
14
|
+
// the slice of comfy-ts the toolkit drives, held to comfy-ts's own types by
|
|
15
|
+
// tests/comfy-optional.test.ts
|
|
16
|
+
export type CflowVar = { readonly kind: string; parse(raw: string): boolean; loadJSON(value: unknown): unknown; reset(): unknown }
|
|
17
|
+
export type CflowImage = { readonly ComfyNode: { class_type?: string } | null | undefined; readonly metadata: { type?: string }; readonly buffer: Uint8Array }
|
|
18
|
+
export type CflowExecution = { readonly status: string | null | undefined; data: { error?: unknown }; images: CflowImage[]; imageErrors: unknown[] }
|
|
19
|
+
export type CflowHost = {
|
|
20
|
+
data: { id: string }
|
|
21
|
+
schema: { readonly size: number }
|
|
22
|
+
getServerHostHTTP(): string
|
|
23
|
+
disconnect(): void
|
|
24
|
+
interrupt(): Promise<void>
|
|
25
|
+
}
|
|
26
|
+
export type CflowWorkflow = {
|
|
27
|
+
spec: { id?: string }
|
|
28
|
+
readonly vars: { [key: string]: CflowVar }
|
|
29
|
+
host: CflowHost
|
|
30
|
+
entries(): [string, CflowVar][]
|
|
31
|
+
build(): Promise<{ problems: { title: string }[]; readonly size: number }>
|
|
32
|
+
run(settings?: { onProgress?: (p: { percent: number; nodeName: string | null }) => void }): Promise<CflowExecution>
|
|
33
|
+
}
|
|
34
|
+
type ComfyTsModule = { ComfyTS?: { create(o: { rootPath: string }): unknown } }
|
|
11
35
|
|
|
12
36
|
export type GenProgress = { value: number; max: number; node: string | null }
|
|
13
37
|
export type OnProgress = (p: GenProgress) => void
|
|
14
38
|
|
|
15
39
|
/** root: the repo the workflow belongs to, which holds its drafts */
|
|
16
|
-
export type LoadedWorkflow = { id: string; root: string; file: string; wf:
|
|
40
|
+
export type LoadedWorkflow = { id: string; root: string; file: string; wf: CflowWorkflow }
|
|
17
41
|
export type ComfyTsRuntime = { workflows: Map<string, LoadedWorkflow>; heal?: string }
|
|
18
42
|
|
|
19
43
|
// function declaration, not const — TS only narrows on never-calls to
|
|
@@ -22,35 +46,39 @@ function fail(msg: string): never { throw new Error(msg) }
|
|
|
22
46
|
|
|
23
47
|
// same duck predicate the comfy-ts TUI and serve bind to: a module valid
|
|
24
48
|
// for shipkit is valid for `bunx comfy-ts tui` and vice versa
|
|
25
|
-
function findDefinedWorkflow(mod: Record<string, unknown>):
|
|
26
|
-
const isDefined = (x: unknown): x is
|
|
49
|
+
function findDefinedWorkflow(mod: Record<string, unknown>): CflowWorkflow | null {
|
|
50
|
+
const isDefined = (x: unknown): x is CflowWorkflow =>
|
|
27
51
|
typeof x === 'object' && x != null && 'spec' in x &&
|
|
28
|
-
typeof (x as
|
|
29
|
-
typeof (x as
|
|
52
|
+
typeof (x as CflowWorkflow).run === 'function' &&
|
|
53
|
+
typeof (x as CflowWorkflow).entries === 'function'
|
|
30
54
|
return [mod.default, ...Object.values(mod)].find(isDefined) ?? null
|
|
31
55
|
}
|
|
32
56
|
|
|
33
|
-
const varsOf = (wf: DefinedWorkflow): Record<string, AnyVar> => wf.vars as Record<string, AnyVar>
|
|
34
|
-
|
|
35
57
|
/** true when the image belongs in a roll. Preview-family nodes (PreviewImage,
|
|
36
58
|
* PreviewBridge, …) emit raw temp images that comfy-ts downloads like any
|
|
37
59
|
* output, in completion order, and MediaImage drops the server-side
|
|
38
60
|
* 'temp' marker, so the producing node's class_type is the only signal left.
|
|
39
61
|
* no node json (SaveImageWebsocket frames) = deliberate output, kept. */
|
|
40
|
-
export const isRollImage = (img: Pick<
|
|
62
|
+
export const isRollImage = (img: Pick<CflowImage, 'ComfyNode'>): boolean =>
|
|
41
63
|
!/preview/i.test(img.ComfyNode?.class_type ?? '')
|
|
42
64
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
const COMFY_TS = 'comfy-ts'
|
|
66
|
+
const INSTALL_COMFY_TS = 'Install it in this repo: bun add -d comfy-ts'
|
|
67
|
+
|
|
68
|
+
/** the ComfyTS class of the comfy-ts a bare import from `dir` reaches, else
|
|
69
|
+
* the toolkit's own; loud when neither is installed */
|
|
70
|
+
async function comfyTsOf(dir: string): Promise<NonNullable<ComfyTsModule['ComfyTS']>> {
|
|
71
|
+
for (const from of [dir, import.meta.dir]) {
|
|
72
|
+
let path: string
|
|
73
|
+
try {
|
|
74
|
+
path = Bun.resolveSync(COMFY_TS, from)
|
|
75
|
+
} catch {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const mod = (await import(path)) as ComfyTsModule
|
|
79
|
+
if (typeof mod.ComfyTS?.create === 'function') return mod.ComfyTS
|
|
51
80
|
}
|
|
52
|
-
|
|
53
|
-
return typeof mod.ComfyTS?.create === 'function' ? mod.ComfyTS : ComfyTS
|
|
81
|
+
fail(`comfy-ts is not installed, and ${dir}/ holds comfy-ts workflow modules. ${INSTALL_COMFY_TS}`)
|
|
54
82
|
}
|
|
55
83
|
|
|
56
84
|
/** dir = the flat folder holding the <id>.cflow.ts workflow modules.
|
|
@@ -74,7 +102,10 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
|
|
|
74
102
|
for (const f of readdirSync(dir).filter((x) => x.endsWith('.cflow.ts')).sort()) {
|
|
75
103
|
const file = join(dir, f)
|
|
76
104
|
const id = basename(f, '.cflow.ts')
|
|
77
|
-
const mod = (await import(pathToFileURL(file).href))
|
|
105
|
+
const mod = (await import(pathToFileURL(file).href).catch((e: Error) => {
|
|
106
|
+
if (/Cannot find (package|module) 'comfy-ts'/.test(e.message)) fail(`${file} imports comfy-ts, which this repo does not install. ${INSTALL_COMFY_TS}`)
|
|
107
|
+
throw e
|
|
108
|
+
})) as Record<string, unknown>
|
|
78
109
|
const wf = findDefinedWorkflow(mod)
|
|
79
110
|
if (wf == null) fail(`${file}: exports no DefinedWorkflow — export default host.defineWorkflow({...})`)
|
|
80
111
|
// the file name IS the id: it is also the comfy-ts draft folder key,
|
|
@@ -96,7 +127,7 @@ export function workflowFor(workflows: Map<string, LoadedWorkflow>, ref: { workf
|
|
|
96
127
|
: workflows.size === 1
|
|
97
128
|
? [...workflows.values()][0]!
|
|
98
129
|
: fail(`asset '${ref.assetId ?? '?'}' names no workflow and ${workflows.size} are loaded (${[...workflows.keys()].join(', ')}) — set workflow on the kind`)
|
|
99
|
-
const vars =
|
|
130
|
+
const vars = lw.wf.vars
|
|
100
131
|
if (vars.prompt?.kind !== 'prompt' || vars.seed?.kind !== 'seed')
|
|
101
132
|
fail(`kind '${ref.kind ?? '?'}' drives workflow '${lw.id}', which must declare vars.prompt (v.prompt) and vars.seed (v.seed) — the atelier writes both`)
|
|
102
133
|
return lw
|
|
@@ -105,8 +136,8 @@ export function workflowFor(workflows: Map<string, LoadedWorkflow>, ref: { workf
|
|
|
105
136
|
/** Apply a name→value record onto a workflow's vars: strings go through
|
|
106
137
|
* parse (validated), everything else through loadJSON (the drafts setter).
|
|
107
138
|
* Unknown names throw listing what the workflow has. */
|
|
108
|
-
export function applyVarValues(wf:
|
|
109
|
-
const defs =
|
|
139
|
+
export function applyVarValues(wf: CflowWorkflow, vars: Record<string, unknown>, source: string): void {
|
|
140
|
+
const defs = wf.vars
|
|
110
141
|
for (const [name, value] of Object.entries(vars)) {
|
|
111
142
|
const varDef = defs[name]
|
|
112
143
|
if (varDef == null) fail(`${source}: unknown var '${name}' on workflow '${wf.spec.id ?? '?'}' (has: ${wf.entries().map(([n, d]) => `${n}:${d.kind}`).join(', ')})`)
|
|
@@ -167,7 +198,7 @@ export async function generateComfyTs(
|
|
|
167
198
|
// names no kind. It is what the tune button edits
|
|
168
199
|
const draftId = args.draftId ?? args.assetId
|
|
169
200
|
ensureAssetDraft(lw, draftId, args.prompt)
|
|
170
|
-
const defs =
|
|
201
|
+
const defs = lw.wf.vars
|
|
171
202
|
// modules are import-cached: a previous asset's values must not leak
|
|
172
203
|
for (const [, varDef] of lw.wf.entries()) varDef.reset()
|
|
173
204
|
if (args.vars != null) applyVarValues(lw.wf, args.vars, `asset '${args.assetId}' kit vars`)
|
|
@@ -192,7 +223,7 @@ export async function generateComfyTs(
|
|
|
192
223
|
defs.seed!.parse(`= ${args.seed}`)
|
|
193
224
|
mkdirSync(args.destDir, { recursive: true })
|
|
194
225
|
const healHint = rt.heal != null ? ` — is the backend up? (${rt.heal})` : ''
|
|
195
|
-
let execution:
|
|
226
|
+
let execution: CflowExecution
|
|
196
227
|
try {
|
|
197
228
|
execution = await lw.wf.run({
|
|
198
229
|
onProgress: args.onProgress == null ? undefined : (p) => args.onProgress!({ value: Math.round(p.percent), max: 100, node: p.nodeName }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// kits/identity — favicon / logo / OG as asset kinds. One render path
|
|
2
2
|
// (renderIdentityFromMark: mark png → logo master 1024², favicon 512², og
|
|
3
|
-
// card
|
|
3
|
+
// card; a brand's `icon` draws the favicon family at each size instead), two
|
|
4
|
+
// ways to get the mark:
|
|
4
5
|
// - canvas mark: deterministic, instant — `shipkit assets identity` draws it
|
|
5
6
|
// headlessly and renders the set.
|
|
6
7
|
// - comfy mark: roll-and-pick in the studio; the kind carries a BUILT-IN ship
|
|
@@ -28,6 +29,12 @@ export type Brand = {
|
|
|
28
29
|
/** comfy marks: the FULL generation prompt for identity-logo rolls
|
|
29
30
|
* (style included — the studio shows/edits exactly this) */
|
|
30
31
|
markPrompt?: string
|
|
32
|
+
/** the small mark, when the logo does not read at 16 px: favicon.png,
|
|
33
|
+
* favicon-32.png, favicon-16.png and apple-touch-icon.png (180², flattened
|
|
34
|
+
* on palette.bg) are each drawn at their own size; `asset.id` names the
|
|
35
|
+
* file (apple-touch-icon fills its square: iOS rounds it). Absent, the
|
|
36
|
+
* favicon is the logo master resized */
|
|
37
|
+
icon?: CanvasGen
|
|
31
38
|
/** og layout hook — replaces the WHOLE default card (bg + mark left +
|
|
32
39
|
* name/tagline right); the hook owns compositing the mark if it wants one */
|
|
33
40
|
drawOg?: (ctx: SKRSContext2D, brand: Brand, size: { w: number; h: number }) => void
|
|
@@ -42,6 +49,8 @@ export const FAVICON_SIZE = 512
|
|
|
42
49
|
export const OG_W = 1200
|
|
43
50
|
export const OG_H = 630
|
|
44
51
|
export const LOGO_SIZE = 1024
|
|
52
|
+
/** what a brand's `icon` draws, each at its own size */
|
|
53
|
+
export const ICON_FILES = [['favicon', FAVICON_SIZE], ['favicon-32', 32], ['favicon-16', 16], ['apple-touch-icon', 180]] as const
|
|
45
54
|
|
|
46
55
|
/** the one next step when a comfy mark has no picked roll yet */
|
|
47
56
|
export const NO_PICK_FIX = 'roll + pick in the studio: bun run hub → /assets/#identity-logo'
|
|
@@ -110,13 +119,23 @@ export async function renderIdentityFromMark(brand: Brand, root: string, markPng
|
|
|
110
119
|
.resize(LOGO_SIZE, LOGO_SIZE, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
111
120
|
.png().toBuffer()
|
|
112
121
|
await write('logo.png', logoPng)
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
if (brand.icon != null) {
|
|
123
|
+
for (const [id, px] of ICON_FILES) {
|
|
124
|
+
const c = createCanvas(px, px)
|
|
125
|
+
await brand.icon.draw(c.getContext('2d'), { id, prompt: '' })
|
|
126
|
+
const png = sharp(c.toBuffer('image/png'))
|
|
127
|
+
await write(`${id}.png`, await (id === 'apple-touch-icon' ? png.flatten({ background: brand.palette.bg }) : png).png().toBuffer())
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// favicon = logo master resized to 512² (browsers derive the rest)
|
|
131
|
+
await write('favicon.png', await sharp(logoPng).resize(FAVICON_SIZE, FAVICON_SIZE).png().toBuffer())
|
|
132
|
+
}
|
|
115
133
|
const og = createCanvas(OG_W, OG_H)
|
|
116
134
|
const ctx = og.getContext('2d')
|
|
117
135
|
if (brand.drawOg != null) brand.drawOg(ctx, brand, { w: OG_W, h: OG_H })
|
|
118
136
|
else defaultOg(ctx, brand, { w: OG_W, h: OG_H }, await loadImage(logoPng))
|
|
119
|
-
|
|
137
|
+
// a link preview is small: a 256 colour palette is invisible there and cuts a photo card about 3x (some chat apps drop a preview past ~300 KB)
|
|
138
|
+
await write('og.png', await sharp(og.toBuffer('image/png')).png({ palette: true, quality: 90, effort: 10, compressionLevel: 9 }).toBuffer())
|
|
120
139
|
return out
|
|
121
140
|
}
|
|
122
141
|
|
|
@@ -12,6 +12,7 @@ export function comfyModule(comfy: string | undefined, name: string): string {
|
|
|
12
12
|
const COMFY_TIMEOUT_S = 15 * 60
|
|
13
13
|
|
|
14
14
|
export async function comfyRun(module: string, vars: Record<string, string | number | boolean | undefined>, out: string): Promise<void> {
|
|
15
|
+
if (Bun.which('comfy-ts') == null) throw new Error(`comfy-ts is not on PATH, and ${module} is a comfy-ts module. Install it: bun add -g comfy-ts`)
|
|
15
16
|
const args = ['comfy-ts', 'run', module]
|
|
16
17
|
for (const [k, v] of Object.entries(vars)) if (v !== undefined) args.push(`--${k}`, String(v))
|
|
17
18
|
args.push('--out', out, '--json')
|