rvlib-shipkit 1.7.0 → 1.9.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 +48 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +31 -14
- package/src/assets/gen/generate-roll.ts +20 -7
- package/src/assets/kit-config.ts +9 -0
- package/src/assets/kits/identity-kit.ts +7 -1
- package/src/assets/run-assets.ts +3 -3
- package/src/assets/server/studio-server.ts +3 -3
- package/src/checks/SK027-agent-wiki-links.check.ts +6 -2
- package/src/checks/SK033-global-contribution.check.ts +47 -0
- package/src/cli/cmd_Ci.ts +5 -4
- package/src/cli/cmd_GlobalContext.ts +47 -0
- package/src/cli/cmd_GlobalProcesses.ts +38 -1
- package/src/cli.ts +4 -1
- package/src/collections/providers.ts +1 -1
- package/src/core/check-runner.ts +13 -4
- package/src/core/discover-checks.ts +37 -8
- package/src/core/fix-scope.ts +5 -0
- package/src/core/floor-resolve.ts +2 -1
- package/src/core/git-root.ts +22 -12
- package/src/core/global-contribution.ts +42 -0
- package/src/core/one-lib.ts +4 -2
- package/src/core/run-ci.ts +12 -11
- package/src/core/slots.ts +36 -6
- package/src/home/global-context.ts +145 -0
- package/src/home/gui-app/ShipkitGui.swift +96 -0
- package/src/home/home-routes.ts +10 -0
- package/src/home/resources.ts +3 -0
- package/src/plan/planner.ts +21 -4
- package/src/plan/repo-source.ts +7 -3
- package/src/release/run-release.ts +17 -8
- package/src/release/unit-gate.ts +12 -5
- package/src/repo-config.ts +7 -0
- package/src/serve/hub-runtime.ts +34 -23
- package/src/serve/project-gutter.ts +5 -2
- package/src/serve/run-lanes.ts +138 -0
- package/src/smoke/run-smoke.ts +13 -6
- package/src/suite/judge-suites.ts +79 -11
- package/src/suite/reads-env.ts +8 -0
- package/src/suite/select.ts +510 -0
- package/src/suite/suite-cli.ts +213 -23
- package/src/suite/suite-reads-preload.ts +335 -0
- package/src/ts/typecheck-cache.ts +11 -0
- package/src/ts/typecheck-cli.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
## 1.9.0
|
|
2
|
+
|
|
3
|
+
- **A commit in a linked worktree passes the pre-commit hook.** git sets GIT_DIR for a hook there, and the installed copy then asked git where the house sits and got the worktree back, so it never handed off to the house source. Then it ran itself and failed every built-in check with ENOENT on a `?v=` stamped path. A directory with its own `.git` is now asked with the locating variables stripped, and an installed copy that runs itself (CI, `SHIPKIT_NO_HOME`) loads its own check files.
|
|
4
|
+
- **A broken built-in check no longer aborts every repo's gate.** In a repo that only consumes the toolkit, a built-in that does not load or is refused is one non-blocking WARN that names the file. The repo that hosts the toolkit source still fails on it. Before, it threw a misleading "names the page that OWNS the invariant" error before any check ran.
|
|
5
|
+
- **`sk gp open <id>` probes before it trusts "up".** A process that died since pm last looked now starts before its window opens, instead of a window on a dead port.
|
|
6
|
+
- **A linked worktree of a house repo is no longer a house repo of its own.** The home, the global context and pm skip a sibling whose `.git` is a file, so its global processes no longer collide with the main checkout's ids and ports.
|
|
7
|
+
- **`sk ci --only <check>` leaves the tree alone.** A narrowed run no longer runs the fix pass unless `--fix` is named, so `bun run test`, `lint` and `typecheck`, which route through `--only`, never reformat a shared tree. Before, the formatter ran on every narrowed run and rewrote 478 files of one checkout, other agents' uncommitted work included. A whole gate still repairs first, and `--no-fix` still turns it off.
|
|
8
|
+
|
|
9
|
+
- **The machine pool admits fewer checks on a loaded machine.** A slot is a check, and a check is often a whole multi core tool, so eight slots on eight cores could put several times the cores to work. Past a 1 minute load of one per core the pool now admits slots in proportion to the load, never fewer than one: at load 20 on 8 cores it admits 3 of 8. A slot already held is never taken back. A waiting check prints the load and what the pool admits.
|
|
10
|
+
|
|
11
|
+
- **The plan holds save runs while someone gates the repo.** A check a save moved now also waits 10 minutes after the latest run a terminal, an agent or a click started in its repo. On one repo with several agents, most of the plan's test runs had started within those 10 minutes and measured a tree the agents were gating themselves. The run is held, not dropped, so the tree still gets one verdict once they stop. `sk planned` shows the hold as a `deferred:` line.
|
|
12
|
+
- **A suite over the CPU budget in its lone run runs alone once more, and the smaller figure stands.** On a busy Apple silicon machine the scheduler can spill a lone run onto the efficiency cores, where the same work burns up to 4x the CPU time, so a suite inside its budget failed one run in three and passed a minute later. A spill only raises CPU time, so the second run can clear a false red and never hide a real one. A wait or a suite with no CPU figure keeps its one run, and a green gate pays nothing.
|
|
13
|
+
|
|
14
|
+
- **The gate's test line says the command the suite runner ran.** A plain `bun test` body does not run as written under `sk ci`: the runner adds `--parallel=<n>` and `--no-isolate`. The line now reads `ok: test 412 tests (ran 1.5s) as \`bun test --parallel=7 --no-isolate\` (added --parallel=7 --no-isolate)`, so a reader of package.json knows that `bun run test` outside the gate runs another way. Per file ignores stay out of it.
|
|
15
|
+
|
|
16
|
+
- **A test phase killed at its ceiling names the suite that hung, and both things that hang a worker.** bun lists the suites still running when a phase is killed, and the parallel phase now reads that list, so the line says `with tests/x.test.ts still running`. It names a synchronous loop and `expect(p).resolves` or `.rejects` on a promise that never settles: bun's per test timeout stops neither, while a plain `await` of such a promise fails at the per test timeout. And a red suite is no longer judged on time: its time is its failure's, so a test that timed out no longer runs again alone and no longer adds a budget FAIL.
|
|
17
|
+
- **The gate typecheck keeps its own incremental cache, and a release can publish with bun.** The built-in typecheck passes `--incremental --tsBuildInfoFile <project dir>/node_modules/.cache/shipkit/<config>.tsbuildinfo` for every project, so a `tsc --watch` task or a raw script on the same tsconfig no longer writes the cache the gate reads. A config without `incremental` gets it too: rvlib-shipkit's own typecheck went from 0.81 s to 0.35 s warm. `release.publisher: 'bun'` makes `sk release` and `sk bump` run `bun publish --access public`, with the token from `release.npmTokenCommand` in `NPM_CONFIG_TOKEN`, for a repo that tests its tarball with `bun pm pack`. The default stays npm, and an unknown publisher fails `defineRepo`.
|
|
18
|
+
|
|
19
|
+
- **`alert`, `confirm` and `prompt` work in the shipkit app and in every process app.** The app window answered each one at once with no panel: `confirm` returned false and `prompt` null, so a page button behind one did nothing. Each now shows as a sheet on its window. The prompt's text field holds the default text, selected. Return is OK and Esc is Cancel. An app picks it up the next time `sk gui` or `sk gp open <id> --app` opens it, because a changed source rebuilds the app and quits the running copy.
|
|
20
|
+
|
|
21
|
+
- **The `prompt` and `seed` vars contract binds only the workflows a kit kind drives.** Loading the workflows folder no longer refuses a module that lacks them, so a workflow the repo drives with its own tools (portraits, voice, music) names its vars freely. The check runs when the atelier resolves the workflow for a kind, and the error names that kind.
|
|
22
|
+
|
|
23
|
+
- **`sk assets gen` works on a repo whose comfy-ts is newer than shipkit's.** The one comfy-ts instance of the process is now created through the copy the repo's workflow modules import, so a module gets the vars builder of its own version. Before, shipkit's older copy rooted it, and a module using a newer var kind failed every roll (`v.model is not a function`).
|
|
24
|
+
|
|
25
|
+
- **The og card draws accented names.** The default card names a font family list (Helvetica, Arial, DejaVu Sans, Liberation Sans, Noto Sans, then sans-serif). The generic sans-serif alone resolved on macOS to a face with no Latin-1 accents, so a French name rendered with a missing glyph box.
|
|
26
|
+
|
|
27
|
+
- **`sk smoke` serves canvas apps: a route takes `minChars`**, the visible text a settled route needs (default 40). A game that draws into one canvas declares `{ path: '/', waitFor: 'canvas', minChars: 0 }`, and a missing canvas still fails the route.
|
|
28
|
+
|
|
29
|
+
- **`serve: { assets: false }` hides the asset kit from the hub page and from `sk needs`**, for a repo whose own tool mounts `studioRoutes` and owns the picking. Workflows and posts stay.
|
|
30
|
+
|
|
31
|
+
- **`defineKit({ rollFormat: 'webp-lossless' })` stores new image rolls as lossless webp**, the same pixels at about 60 percent of the png bytes, encoded with sharp. Existing png rolls stay readable beside them.
|
|
32
|
+
|
|
33
|
+
## 1.8.0
|
|
34
|
+
|
|
35
|
+
- **The quick tier can run only the suites a change reaches** (opt in: `tests: { select: true }`, or `SHIPKIT_TEST_SELECT=1` for one run). It keeps the snapshot of the last run and its red suites, and runs those plus every suite whose import closure, or whose recorded runtime reads, a change since then reaches. What it cannot see it runs: an unclaimed change under a code root, a spawn it does not know, a module that threw while loading, a run config file. The full tier still runs every suite.
|
|
36
|
+
|
|
37
|
+
- **Runs of one repo in the home share, then overlap.** A terminal `sk ci` or `sk check` identical to one in flight on the same tree attaches to it, prints its lines and exits with its code. Runs without the repair pass start at once beside the runs in flight, bounded by the machine slot pool. A run with the repair pass still goes alone. Before, every run of a repo queued behind the one in flight, so a one second gate could wait minutes.
|
|
38
|
+
|
|
39
|
+
- **The house context: one generated file every house agent loads.** `sk global-context` builds `generated/global-context.md` beside the house index. It lists every global process with the commands that drive it, every house repo, and each repo's `agent/global-contribution.md`. The daemon rewrites it within a minute of a source moving. A process or repo description joins by its first line only, and every item is scanned with the banned keywords of the commit leak guard: a hit leaves it out and says so. It exists only for a house whose index includes `@generated/global-context.md`. `--write` writes it and `--check` fails when it is stale.
|
|
40
|
+
|
|
41
|
+
- **SK033: every house repo carries `agent/global-contribution.md`.** It is what the repo offers every other agent, capped at 40 lines and 3 KB with no heading above `####`, and `sk ci` writes a comment-only stub when it is missing. It applies only where the house index loads the generated context. It is cached on the file, so the check body runs again only when the file moves. SK027 no longer calls the file an orphan, because the house index links it.
|
|
42
|
+
|
|
43
|
+
- **`sk gp open <id>` opens a global process's url** in the default browser, in Chrome (`--chrome`) or as its own app (`--app`), the overview's open buttons in the console. A process that is down starts first.
|
|
44
|
+
|
|
45
|
+
- **The quick tier honours the worker count a repo names.** A `--parallel=<n>` in the repo's own `bun test` body is now a ceiling over the machine pool's share: the parallel phase takes the smaller of the two, as the one `--parallel` on its line. Before, the runner appended its own count after the body, and bun reads the last one, so a repo that measured fewer workers as faster still ran on every core of an idle machine. A bare `--parallel` keeps the pool's count.
|
|
46
|
+
|
|
47
|
+
- **Declared serial suites and the full tier stay one at a time when the repo's body names `--parallel`.** The runner passed the body as written to the serial phase, to the full tier and to a suspect's alone rerun. So a body naming `--parallel=7` ran its declared serial suites, the ones that cannot share a run, in seven workers, and its full tier too, which runs one suite at a time because its suites spawn git and whole gates. Only the quick tier's parallel phase names a worker count now, and the runner keeps the body's `--parallel` off every other line.
|
|
48
|
+
|
|
1
49
|
## 1.7.0
|
|
2
50
|
|
|
3
51
|
- **Cmd-F in the shipkit app shows a find bar now.** Since the app shipped, the Find menu did nothing visible: NSTextFinder never shows its bar for a web view. The app now draws its own bar on WebKit's find API. Return and cmd-G go to the next match, shift-return and shift-cmd-G to the previous one, Esc closes the bar, and it says "no match" when the page does not hold the text. Rebuilt apps pick it up at their next launch.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|
|
@@ -40,6 +40,19 @@ const varsOf = (wf: DefinedWorkflow): Record<string, AnyVar> => wf.vars as Recor
|
|
|
40
40
|
export const isRollImage = (img: Pick<MediaImage, 'ComfyNode'>): boolean =>
|
|
41
41
|
!/preview/i.test(img.ComfyNode?.class_type ?? '')
|
|
42
42
|
|
|
43
|
+
/** the ComfyTS class of the comfy-ts a bare import from `dir` reaches,
|
|
44
|
+
* shipkit's own when the repo carries none */
|
|
45
|
+
async function comfyTsOf(dir: string): Promise<typeof ComfyTS> {
|
|
46
|
+
let path: string
|
|
47
|
+
try {
|
|
48
|
+
path = Bun.resolveSync('comfy-ts', dir)
|
|
49
|
+
} catch {
|
|
50
|
+
return ComfyTS
|
|
51
|
+
}
|
|
52
|
+
const mod = (await import(path)) as { ComfyTS?: typeof ComfyTS }
|
|
53
|
+
return typeof mod.ComfyTS?.create === 'function' ? mod.ComfyTS : ComfyTS
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
/** dir = the flat folder holding the <id>.cflow.ts workflow modules.
|
|
44
57
|
* root pins `.comfy-ts/` (drafts, hosts cache) to the repo, cwd-independent. */
|
|
45
58
|
export async function loadCflowWorkflows(root: string, dir: string): Promise<Map<string, LoadedWorkflow>> {
|
|
@@ -51,9 +64,12 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
|
|
|
51
64
|
// module then returns this same instance. comfy-ts keeps ONE instance per
|
|
52
65
|
// process, and the daemon serves every repo from one process, so only the
|
|
53
66
|
// first repo roots it. What it roots is the host caches, shared by every
|
|
54
|
-
// repo on the same host; each repo's drafts resolve from its own root
|
|
55
|
-
|
|
67
|
+
// repo on the same host; each repo's drafts resolve from its own root.
|
|
68
|
+
// The instance is created through the copy the modules import, never
|
|
69
|
+
// shipkit's own: a host hands its vars builder to defineWorkflow, so an
|
|
70
|
+
// older copy gives a newer module a v without its newer var kinds
|
|
56
71
|
if (!existsSync(dir)) fail(`${dir}/ missing — ship at least one <id>.cflow.ts comfy-ts workflow module (copy from the rv-comfy template)`)
|
|
72
|
+
if ((globalThis as { comfyts?: unknown }).comfyts == null) (await comfyTsOf(dir)).create({ rootPath: root })
|
|
57
73
|
const out = new Map<string, LoadedWorkflow>()
|
|
58
74
|
for (const f of readdirSync(dir).filter((x) => x.endsWith('.cflow.ts')).sort()) {
|
|
59
75
|
const file = join(dir, f)
|
|
@@ -65,24 +81,25 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
|
|
|
65
81
|
// so letting them diverge would strand every per-asset draft
|
|
66
82
|
if (wf.spec.id != null && wf.spec.id !== id) fail(`workflow file '${f}' declares id '${wf.spec.id}' — the file name IS the id, rename one`)
|
|
67
83
|
wf.spec.id ??= id
|
|
68
|
-
const vars = varsOf(wf)
|
|
69
|
-
if (vars.prompt?.kind !== 'prompt' || vars.seed?.kind !== 'seed')
|
|
70
|
-
fail(`workflow '${id}' must declare vars.prompt (v.prompt) and vars.seed (v.seed) — shipkit drives both`)
|
|
71
84
|
out.set(id, { id, root, file, wf })
|
|
72
85
|
}
|
|
73
86
|
if (out.size === 0) fail(`${dir}/ has no *.cflow.ts workflow modules — seed one from the rv-comfy template`)
|
|
74
87
|
return out
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
/** Resolve a
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
/** Resolve the workflow a kit kind drives: explicit id, else the sole loaded
|
|
91
|
+
* workflow. The atelier writes prompt and seed, so the contract binds here,
|
|
92
|
+
* never at folder load: a workflow no kind drives names its vars freely. */
|
|
93
|
+
export function workflowFor(workflows: Map<string, LoadedWorkflow>, ref: { workflow?: string; kind?: string; assetId?: string }): LoadedWorkflow {
|
|
94
|
+
const lw = ref.workflow != null
|
|
95
|
+
? (workflows.get(ref.workflow) ?? fail(`asset '${ref.assetId ?? '?'}': unknown workflow '${ref.workflow}' (have: ${[...workflows.keys()].join(', ')})`))
|
|
96
|
+
: workflows.size === 1
|
|
97
|
+
? [...workflows.values()][0]!
|
|
98
|
+
: fail(`asset '${ref.assetId ?? '?'}' names no workflow and ${workflows.size} are loaded (${[...workflows.keys()].join(', ')}) — set workflow on the kind`)
|
|
99
|
+
const vars = varsOf(lw.wf)
|
|
100
|
+
if (vars.prompt?.kind !== 'prompt' || vars.seed?.kind !== 'seed')
|
|
101
|
+
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
|
+
return lw
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
/** Apply a name→value record onto a workflow's vars: strings go through
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// in both (before this, canvas draw lived only in the studio and the CLI
|
|
5
5
|
// refused canvas kinds — A30). Comfy context is resolved LAZILY: canvas-only
|
|
6
6
|
// kits never touch a comfy config or the box.
|
|
7
|
-
import { mkdirSync } from 'fs'
|
|
7
|
+
import { mkdirSync, unlinkSync } from 'fs'
|
|
8
8
|
import { join } from 'path'
|
|
9
|
-
import type { FoundAsset } from '../kit-config.ts'
|
|
9
|
+
import type { FoundAsset, RollFormat } from '../kit-config.ts'
|
|
10
10
|
import { generateComfyTs, loadCflowWorkflows, workflowFor, type ComfyTsRuntime, type OnProgress } from './comfyts-gen.ts'
|
|
11
11
|
import { recordParents, type ParentContext } from './derived.ts'
|
|
12
12
|
import { generateGemini } from './gemini-gen.ts'
|
|
@@ -16,11 +16,22 @@ export const CANVAS_SIZE = 1024
|
|
|
16
16
|
|
|
17
17
|
export type ComfyResolver = () => Promise<ComfyTsRuntime>
|
|
18
18
|
|
|
19
|
+
/** re-encode freshly written png rolls in the kit's roll format, before any
|
|
20
|
+
* sidecar names them. `exact` keeps the colour under transparent pixels */
|
|
21
|
+
async function encodeRolls(saved: string[], format: RollFormat | undefined): Promise<void> {
|
|
22
|
+
if (format !== 'webp-lossless') return
|
|
23
|
+
const { default: sharp } = await import('sharp')
|
|
24
|
+
for (const png of saved.filter((p) => p.endsWith('.png'))) {
|
|
25
|
+
await sharp(png).webp({ lossless: true, exact: true }).toFile(png.replace(/\.png$/, '.webp'))
|
|
26
|
+
unlinkSync(png)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
/** Draw/generate one roll for `found` into destDir as v<seed>.png (canvas) or
|
|
20
31
|
* v<seed>.<ext> (comfy). `resolveComfy` is only awaited for comfy backends. */
|
|
21
32
|
export async function generateRoll(
|
|
22
33
|
found: FoundAsset,
|
|
23
|
-
args: { prompt: string; seed: number; destDir: string; onProgress?: OnProgress; context?: ParentContext },
|
|
34
|
+
args: { prompt: string; seed: number; destDir: string; onProgress?: OnProgress; context?: ParentContext; rollFormat?: RollFormat },
|
|
24
35
|
resolveComfy: ComfyResolver,
|
|
25
36
|
): Promise<void> {
|
|
26
37
|
const gen = found.kind.generate
|
|
@@ -31,7 +42,7 @@ export async function generateRoll(
|
|
|
31
42
|
// resolves them (queue/CLI) so a blocker names the parent, loud
|
|
32
43
|
if ((found.asset.from?.length ?? 0) > 0 && args.context == null)
|
|
33
44
|
throw new Error(`'${found.asset.id}' derives from ${found.asset.from!.join(', ')} — parent context missing (are the parents picked?)`)
|
|
34
|
-
await generateGemini({}, { prompt: args.prompt, model: gen.model, destDir: args.destDir, name: `v${args.seed}`, context: args.context?.images ?? [] })
|
|
45
|
+
await encodeRolls(await generateGemini({}, { prompt: args.prompt, model: gen.model, destDir: args.destDir, name: `v${args.seed}`, context: args.context?.images ?? [] }), args.rollFormat)
|
|
35
46
|
if (args.context != null) await recordParents(args.destDir, args.seed, args.context.parents)
|
|
36
47
|
return
|
|
37
48
|
}
|
|
@@ -40,12 +51,14 @@ export async function generateRoll(
|
|
|
40
51
|
const c = createCanvas(CANVAS_SIZE, CANVAS_SIZE)
|
|
41
52
|
await gen.draw(c.getContext('2d'), { ...found.asset, prompt: args.prompt })
|
|
42
53
|
mkdirSync(args.destDir, { recursive: true })
|
|
43
|
-
|
|
54
|
+
const png = join(args.destDir, `v${args.seed}.png`)
|
|
55
|
+
await Bun.write(png, c.toBuffer('image/png'))
|
|
56
|
+
await encodeRolls([png], args.rollFormat)
|
|
44
57
|
return
|
|
45
58
|
}
|
|
46
59
|
const rt = await resolveComfy()
|
|
47
|
-
const lw = workflowFor(rt.workflows, { workflow: gen.workflow, assetId: found.asset.id })
|
|
48
|
-
await generateComfyTs(rt, lw, { prompt: args.prompt, seed: args.seed, assetId: found.asset.id, draftId: found.kind.id, destDir: args.destDir, name: `v${args.seed}`, vars: gen.vars, onProgress: args.onProgress })
|
|
60
|
+
const lw = workflowFor(rt.workflows, { workflow: gen.workflow, kind: found.kind.id, assetId: found.asset.id })
|
|
61
|
+
await encodeRolls(await generateComfyTs(rt, lw, { prompt: args.prompt, seed: args.seed, assetId: found.asset.id, draftId: found.kind.id, destDir: args.destDir, name: `v${args.seed}`, vars: gen.vars, onProgress: args.onProgress }), args.rollFormat)
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
/** Memoized lazy comfy runtime for a resolved kit — loads the cflow modules on
|
package/src/assets/kit-config.ts
CHANGED
|
@@ -127,14 +127,23 @@ export type KitConfig = {
|
|
|
127
127
|
/** workflows = dir of comfy-ts .cflow.ts modules (each module owns its
|
|
128
128
|
* host); heal = shell command shown in error hints, never executed */
|
|
129
129
|
comfy?: { workflows: string; heal?: string }
|
|
130
|
+
/** absent: an image roll lands in the format its backend produced (png
|
|
131
|
+
* from comfy and canvas). 'webp-lossless' re-encodes every new image
|
|
132
|
+
* roll as lossless webp, the same pixels at about 60% of the bytes */
|
|
133
|
+
rollFormat?: RollFormat
|
|
130
134
|
kinds: Kind[]
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
export const ROLL_FORMATS = ['webp-lossless'] as const
|
|
138
|
+
export type RollFormat = (typeof ROLL_FORMATS)[number]
|
|
139
|
+
|
|
133
140
|
export type ResolvedKit = KitConfig & { workspace: string }
|
|
134
141
|
|
|
135
142
|
export function validateKit(k: KitConfig): string[] {
|
|
136
143
|
const errs: string[] = []
|
|
137
144
|
if (!Array.isArray(k.kinds) || k.kinds.length === 0) errs.push('kinds must be a non-empty array')
|
|
145
|
+
if (k.rollFormat != null && !(ROLL_FORMATS as readonly string[]).includes(k.rollFormat))
|
|
146
|
+
errs.push(`unknown rollFormat '${String(k.rollFormat)}' (${ROLL_FORMATS.join(' | ')}, or absent for the backend's own format)`)
|
|
138
147
|
for (const key of Object.keys(k.comfy ?? {}))
|
|
139
148
|
if (key !== 'workflows' && key !== 'heal')
|
|
140
149
|
errs.push(`comfy config has unknown key '${key}'${key === 'host' ? " — host is gone, the workflow's _host.ts owns the box (comfy-ts migration)" : ''}`)
|
|
@@ -51,10 +51,16 @@ export function identityKinds(brand: Brand): Kind[] {
|
|
|
51
51
|
]
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// named faces first: the generic sans-serif can resolve to a face with no
|
|
55
|
+
// Latin-1 accents (macOS). Each family below carries them, and an absent one
|
|
56
|
+
// is skipped (macOS, Windows, then the common Linux faces)
|
|
57
|
+
const OG_FAMILIES = 'Helvetica, Arial, "DejaVu Sans", "Liberation Sans", "Noto Sans", sans-serif'
|
|
58
|
+
export const ogFont = (px: number, bold = false): string => `${bold ? 'bold ' : ''}${px}px ${OG_FAMILIES}`
|
|
59
|
+
|
|
54
60
|
// shrink px until text fits maxW (og cards must never clip the tagline)
|
|
55
61
|
const fitFont = (ctx: SKRSContext2D, text: string, px: number, maxW: number, bold = false): void => {
|
|
56
62
|
for (; px > 12; px -= 2) {
|
|
57
|
-
ctx.font =
|
|
63
|
+
ctx.font = ogFont(px, bold)
|
|
58
64
|
if (ctx.measureText(text).width <= maxW) return
|
|
59
65
|
}
|
|
60
66
|
}
|
package/src/assets/run-assets.ts
CHANGED
|
@@ -50,7 +50,7 @@ async function rollOnce(root: string, io: AssetsIo, kit: ResolvedKit, resolveCom
|
|
|
50
50
|
for (let i = 0; i < count; i++) {
|
|
51
51
|
const seed = randomSeed()
|
|
52
52
|
try {
|
|
53
|
-
await generateRoll(f!, { prompt, seed, destDir: dir, context }, resolveComfy)
|
|
53
|
+
await generateRoll(f!, { prompt, seed, destDir: dir, context, rollFormat: kit.rollFormat }, resolveComfy)
|
|
54
54
|
io.print(`ok: ${assetId} v${seed}`)
|
|
55
55
|
} catch (e) {
|
|
56
56
|
failed++
|
|
@@ -196,7 +196,7 @@ export const runAssetsCheck = (root: string, io: AssetsIo): Promise<number> =>
|
|
|
196
196
|
const gen = kind.generate
|
|
197
197
|
if (gen.backend !== 'comfy') continue
|
|
198
198
|
try {
|
|
199
|
-
const lw = workflowFor(wfs, { workflow: gen.workflow, assetId: kind.assets[0]?.id })
|
|
199
|
+
const lw = workflowFor(wfs, { workflow: gen.workflow, kind: kind.id, assetId: kind.assets[0]?.id })
|
|
200
200
|
if (gen.vars != null) {
|
|
201
201
|
for (const [, d] of lw.wf.entries()) d.reset()
|
|
202
202
|
applyVarValues(lw.wf, gen.vars, `kind '${kind.id}'`)
|
|
@@ -215,7 +215,7 @@ export const runAssetsCheck = (root: string, io: AssetsIo): Promise<number> =>
|
|
|
215
215
|
for (const f of allAssets(kit).filter((x) => x.kind.generate.backend === 'comfy')) {
|
|
216
216
|
let lw: LoadedWorkflow
|
|
217
217
|
try {
|
|
218
|
-
lw = workflowFor(wfs, { workflow: (f.kind.generate as ComfyGen).workflow, assetId: f.asset.id })
|
|
218
|
+
lw = workflowFor(wfs, { workflow: (f.kind.generate as ComfyGen).workflow, kind: f.kind.id, assetId: f.asset.id })
|
|
219
219
|
} catch {
|
|
220
220
|
continue // an unresolvable workflow already FAILed on its kind above
|
|
221
221
|
}
|
|
@@ -110,7 +110,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
110
110
|
for (const f of allAssets(kit).filter((x) => x.kind.generate.backend === 'comfy')) {
|
|
111
111
|
try {
|
|
112
112
|
const gen = f.kind.generate as { workflow?: string }
|
|
113
|
-
const lw = workflowFor(workflows, { workflow: gen.workflow, assetId: f.asset.id })
|
|
113
|
+
const lw = workflowFor(workflows, { workflow: gen.workflow, kind: f.kind.id, assetId: f.asset.id })
|
|
114
114
|
// the KIND's draft, shared by every asset of that kind
|
|
115
115
|
if (ensureAssetDraft(lw, f.kind.id, await promptOf(f))) console.log(`ok: draft '${f.kind.id}' seeded → .comfy-ts/drafts/${lw.id}/${f.kind.id}.json`)
|
|
116
116
|
} catch (e) {
|
|
@@ -127,7 +127,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
127
127
|
return { workflows, heal: kit.comfy.heal }
|
|
128
128
|
}
|
|
129
129
|
const generateOne = deps.generateOne ?? ((found, prompt, seed, destDir, onProgress, context?: ParentContext) =>
|
|
130
|
-
generateRoll(found, { prompt, seed, destDir, onProgress, context }, resolveComfy))
|
|
130
|
+
generateRoll(found, { prompt, seed, destDir, onProgress, context, rollFormat: kit.rollFormat }, resolveComfy))
|
|
131
131
|
|
|
132
132
|
const queue = new StudioQueue(async (job, seed, onProgress) => {
|
|
133
133
|
const found = findAsset(kit, job.assetId)
|
|
@@ -388,7 +388,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
388
388
|
const rgen = running?.kind.generate
|
|
389
389
|
if (workflows != null && rgen?.backend === 'comfy') {
|
|
390
390
|
try {
|
|
391
|
-
await workflowFor(workflows, { workflow: rgen.workflow, assetId: running!.asset.id }).wf.host.interrupt()
|
|
391
|
+
await workflowFor(workflows, { workflow: rgen.workflow, kind: running!.kind.id, assetId: running!.asset.id }).wf.host.interrupt()
|
|
392
392
|
} catch (e) {
|
|
393
393
|
queue.pushError(`interrupt: ${(e as Error).message} — current roll will finish, its job is still dropped`)
|
|
394
394
|
}
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// a dangling ref there fails. A link that leaves agent/ but stays in the repo
|
|
4
4
|
// is checked on disk; a pointer out of the repo (a house wiki) is backticked,
|
|
5
5
|
// never a bare → or @. A repo without agent/index.md skips.
|
|
6
|
+
// agent/global-contribution.md is no orphan: the house index links it (SK033).
|
|
6
7
|
import { existsSync, readFileSync } from 'fs'
|
|
7
8
|
import { basename, join } from 'path'
|
|
8
9
|
import { check } from '../check.ts'
|
|
10
|
+
import { CONTRIBUTION_FILE } from '../core/global-contribution.ts'
|
|
9
11
|
import { lintMd } from '../wiki/lint-md.ts'
|
|
10
12
|
|
|
11
13
|
export const agentWikiLinks = check({
|
|
@@ -22,8 +24,10 @@ export const agentWikiLinks = check({
|
|
|
22
24
|
run: (c) => {
|
|
23
25
|
if (!existsSync(join(c.root, 'agent/index.md'))) return void c.lines.push({ kind: 'skip', text: 'no agent/ wiki in this repo' })
|
|
24
26
|
const lint = lintMd({ root: join(c.root, 'agent'), index: 'index.md', boundary: c.root })
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// the global contribution is the house index's to link (SK033), never agent/'s
|
|
28
|
+
const orphans = lint.orphans.filter((f) => `agent/${f}` !== CONTRIBUTION_FILE)
|
|
29
|
+
const linked = lint.pages.filter((p) => !orphans.includes(p))
|
|
30
|
+
for (const f of orphans) {
|
|
27
31
|
const by = namingPage(c.root, f, linked)
|
|
28
32
|
const where = by != null ? `${by}, which already names it (turn that mention into the link)` : 'agent/index.md or the page of its topic'
|
|
29
33
|
c.fail(`agent/${f}: orphan, unreachable from agent/index.md: add a markdown link to it from ${where}; delete it only if the page is dead`)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SK033 global contribution (meta/agent-folder.md): every house repo carries
|
|
2
|
+
// agent/global-contribution.md, what it offers every agent of every house
|
|
3
|
+
// repo (a house whose index loads @generated/global-context.md), which the house concatenates into the context each session loads
|
|
4
|
+
// (home/global-context.ts). A new one is a comment only, so it contributes
|
|
5
|
+
// nothing until an agent writes to it. Keyed on the file itself: once it is
|
|
6
|
+
// there, the body runs again only when the file moves. Its page is linked from
|
|
7
|
+
// the house index, never from agent/index.md, so SK027 leaves it out.
|
|
8
|
+
import { homedir } from 'os'
|
|
9
|
+
import { check, type FixResult } from '../check.ts'
|
|
10
|
+
import { CONTRIBUTION_FILE, CONTRIBUTION_STUB, CONTEXT_INCLUDE, contributionBody, contributionOver, loadsContext } from '../core/global-contribution.ts'
|
|
11
|
+
import { homeRepo, houseIndex } from '../serve/project-gutter.ts'
|
|
12
|
+
|
|
13
|
+
/** why a repo gives SK033 nothing to do, or null in a repo whose house
|
|
14
|
+
* index loads the generated context */
|
|
15
|
+
export function notHouse(root: string, home: string = homedir()): string | null {
|
|
16
|
+
const index = houseIndex(homeRepo(root), home)
|
|
17
|
+
if (index == null) return 'this repo includes no house index, so no house context reads it'
|
|
18
|
+
if (!loadsContext(index)) return `its house index does not load the generated context (no ${CONTEXT_INCLUDE} line), so nothing reads a contribution`
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const globalContribution = check({
|
|
23
|
+
id: 'SK033',
|
|
24
|
+
blocking: true,
|
|
25
|
+
title: 'global contribution',
|
|
26
|
+
description: 'agent/global-contribution.md exists and fits in the house context',
|
|
27
|
+
owner: 'meta/agent-folder.md',
|
|
28
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
29
|
+
cache: { by: 'files', inputs: [CONTRIBUTION_FILE], key: null },
|
|
30
|
+
fix: 'ci',
|
|
31
|
+
applies: (env) => notHouse(env.root),
|
|
32
|
+
run: (c) => {
|
|
33
|
+
const text = c.read(CONTRIBUTION_FILE)
|
|
34
|
+
if (text == null) return void c.fail(`${CONTRIBUTION_FILE} is missing: every house repo carries one, empty until it has something to offer`, 'shipkit ci writes the empty one')
|
|
35
|
+
const body = contributionBody(text)
|
|
36
|
+
const over = contributionOver(body)
|
|
37
|
+
if (over != null) return void c.fail(`${CONTRIBUTION_FILE} is ${over}: every session of every house repo loads it, so the house context cuts it`, 'keep what another repo\'s agent can act on, move the rest to agent/')
|
|
38
|
+
c.ok(body === '' ? `${CONTRIBUTION_FILE} is there, contributing nothing yet` : `${CONTRIBUTION_FILE} contributes ${body.split('\n').length} line(s)`)
|
|
39
|
+
},
|
|
40
|
+
repair: (c): FixResult => {
|
|
41
|
+
if (c.read(CONTRIBUTION_FILE) != null) return { changed: false, summary: `${CONTRIBUTION_FILE} is there: its size is the author's to fix` }
|
|
42
|
+
c.write(CONTRIBUTION_FILE, CONTRIBUTION_STUB)
|
|
43
|
+
return { changed: true, summary: `wrote the empty ${CONTRIBUTION_FILE}` }
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export default globalContribution
|
package/src/cli/cmd_Ci.ts
CHANGED
|
@@ -20,7 +20,8 @@ export class CiCommand extends Command {
|
|
|
20
20
|
['gate without the repair pass (checks can still write)', 'sk ci --no-fix'],
|
|
21
21
|
['leave repo config files alone', 'sk ci --no-fix-rules'],
|
|
22
22
|
['stop at the first failure', 'sk ci --bail'],
|
|
23
|
-
['one check + its dep closure', 'sk ci --only typecheck'],
|
|
23
|
+
['one check + its dep closure, the tree left alone', 'sk ci --only typecheck'],
|
|
24
|
+
['one check, repairing the mechanical slots first', 'sk ci --only typecheck --fix'],
|
|
24
25
|
['every check declaring one group', 'sk ci --only wiki'],
|
|
25
26
|
['run everything, take nothing from the cache', 'sk ci --fresh'],
|
|
26
27
|
['include an opt in check', 'sk ci --with up:itch'],
|
|
@@ -31,12 +32,12 @@ export class CiCommand extends Command {
|
|
|
31
32
|
|
|
32
33
|
bail = Option.Boolean('--bail', false, { description: 'stop at the first failure' })
|
|
33
34
|
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
34
|
-
fix = Option.Boolean('--fix',
|
|
35
|
+
fix = Option.Boolean('--fix', { description: 'repair the mechanical slots before gating. Default on for a whole gate and off for a --only run, which leaves the tree alone unless --fix is named. --no-fix skips the repair pass, the checks can still write. Under CI the fixers still run but the gate FAILS on any file they rewrite' })
|
|
35
36
|
fixRules = Option.Boolean('--fix-rules', true, { description: 'apply the structural autofixes of the built-in checks too. Default on. --no-fix-rules leaves repo config alone' })
|
|
36
37
|
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for self-maintaining checks: SK005 never touches package.json, and generated paths are never untracked. STATUS.md is still refreshed on disk, but it is gitignored, so a run leaves the tree clean' })
|
|
37
38
|
skip = Option.Array('--skip', [], { description: 'skip a check group (repeatable)' })
|
|
38
39
|
withIds = Option.Array('--with', [], { description: 'include a check the quick gate leaves out (repeatable)' })
|
|
39
|
-
only = Option.Array('--only', [], { description: 'run only this check + its dep closure (repeatable). A group name runs every check declaring that group' })
|
|
40
|
+
only = Option.Array('--only', [], { description: 'run only this check + its dep closure (repeatable). A group name runs every check declaring that group. No repair pass unless --fix is named' })
|
|
40
41
|
fresh = Option.Boolean('--fresh', false, { description: 'run every check: no answer comes from the cache, and what this run finds is stored fresh' })
|
|
41
42
|
full = Option.Boolean('--full', false, { description: `run the FULL test tier: every suite the repo declares, the slow ones it skips while you work included. What the release path always runs. Same as SHIPKIT_TEST_FULL=1` })
|
|
42
43
|
|
|
@@ -68,7 +69,7 @@ export class CiCommand extends Command {
|
|
|
68
69
|
return [
|
|
69
70
|
...(this.bail ? ['--bail'] : []),
|
|
70
71
|
...(this.json ? ['--json'] : []),
|
|
71
|
-
...(this.fix ? [] : ['--no-fix']),
|
|
72
|
+
...(this.fix === true ? ['--fix'] : this.fix === false ? ['--no-fix'] : []),
|
|
72
73
|
...(this.fixRules ? [] : ['--no-fix-rules']),
|
|
73
74
|
...(this.frozen ? ['--frozen'] : []),
|
|
74
75
|
...this.skip.flatMap((v) => ['--skip', v]),
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// shipkit global-context: the house's generated context, printed, written, or
|
|
2
|
+
// checked against the file on disk. houseContext is the one computation, the
|
|
3
|
+
// daemon's writer calls it too.
|
|
4
|
+
import { existsSync, readFileSync } from 'fs'
|
|
5
|
+
import { Command, Option } from 'clipanion'
|
|
6
|
+
import { houseContext, writeIfChanged } from '../home/global-context.ts'
|
|
7
|
+
|
|
8
|
+
export class GlobalContextCommand extends Command {
|
|
9
|
+
static override paths = [['global-context']]
|
|
10
|
+
|
|
11
|
+
static override usage = Command.Usage({
|
|
12
|
+
category: 'Home',
|
|
13
|
+
description: 'the context every house agent loads: global processes, house repos, each repo\'s global contribution',
|
|
14
|
+
details: 'one md file beside the house index (generated/global-context.md), which the index includes, gitignored. The daemon rewrites it when a source moves; this command prints it, writes it, or fails when the file on disk is stale',
|
|
15
|
+
examples: [
|
|
16
|
+
['print what the file would hold', 'sk global-context'],
|
|
17
|
+
['write it beside the house index', 'sk global-context --write'],
|
|
18
|
+
['fail when the file on disk is stale', 'sk global-context --check'],
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
write = Option.Boolean('--write', false, { description: 'write it beside the house index, only when it changed' })
|
|
23
|
+
check = Option.Boolean('--check', false, { description: 'exit 1 when the file on disk differs from what the sources give' })
|
|
24
|
+
|
|
25
|
+
async execute(): Promise<number> {
|
|
26
|
+
const c = await houseContext(process.cwd())
|
|
27
|
+
if (c == null) {
|
|
28
|
+
this.context.stderr.write('FAIL: this repo includes no house index in its CLAUDE.md, so there is no house to read\n')
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
if (this.check) {
|
|
32
|
+
const now = existsSync(c.path) ? readFileSync(c.path, 'utf8') : null
|
|
33
|
+
if (now === c.text) {
|
|
34
|
+
this.context.stdout.write(`ok: ${c.path} is current\n`)
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
this.context.stdout.write(`FAIL: ${c.path} is ${now == null ? 'missing' : 'stale'}\n fix: shipkit global-context --write\n`)
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
if (this.write) {
|
|
41
|
+
this.context.stdout.write(writeIfChanged(c.path, c.text) ? `ok: wrote ${c.path}\n` : `ok: ${c.path} unchanged\n`)
|
|
42
|
+
return 0
|
|
43
|
+
}
|
|
44
|
+
this.context.stdout.write(c.text)
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -161,7 +161,7 @@ export type SwitchAct = 'start' | 'stop' | 'restart'
|
|
|
161
161
|
|
|
162
162
|
export type SwitchDeps = {
|
|
163
163
|
/** pm's answer to the switch: null when it took it, else why not */
|
|
164
|
-
post: (id: string, act: SwitchAct) => Promise<string | null>
|
|
164
|
+
post: (id: string, act: SwitchAct | 'open' | 'chrome' | 'app') => Promise<string | null>
|
|
165
165
|
/** the process as pm sees it now, null when pm does not answer */
|
|
166
166
|
view: (id: string) => Promise<ResourceView | null>
|
|
167
167
|
sleep: (ms: number) => Promise<void>
|
|
@@ -266,3 +266,40 @@ export class GlobalProcessRestartCommand extends SwitchCommand {
|
|
|
266
266
|
examples: [['restart ComfyUI, so it rescans its models', 'sk gp restart comfyui']],
|
|
267
267
|
})
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
/** where `sk gp open` sends the url, by flag, as pm's resource api names it */
|
|
271
|
+
export const openAct = (o: { chrome: boolean; app: boolean }): 'open' | 'chrome' | 'app' => (o.app ? 'app' : o.chrome ? 'chrome' : 'open')
|
|
272
|
+
|
|
273
|
+
export class GlobalProcessOpenCommand extends Command {
|
|
274
|
+
static override paths = [['global-processes', 'open'], ['gp', 'open']]
|
|
275
|
+
static override usage = Command.Usage({
|
|
276
|
+
category: 'Home',
|
|
277
|
+
description: 'open a global process\'s url in the browser, Chrome or its app, starting it first when down',
|
|
278
|
+
details: 'the overview\'s open buttons in the console. pm opens the url at once while the process answers, else starts it and opens once its probe answers. Exit 1 when pm refuses (no such process, no url declared)',
|
|
279
|
+
examples: [
|
|
280
|
+
['the comfy panel in the default browser', 'sk gp open comfy-panel'],
|
|
281
|
+
['in Chrome', 'sk gp open comfy-panel --chrome'],
|
|
282
|
+
['as its own app in the dock', 'sk gp open comfy-panel --app'],
|
|
283
|
+
],
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
id = Option.String({ required: true, name: 'id' })
|
|
287
|
+
chrome = Option.Boolean('--chrome', false, { description: 'open it in Chrome' })
|
|
288
|
+
app = Option.Boolean('--app', false, { description: 'open it as its own app, built on first use' })
|
|
289
|
+
|
|
290
|
+
async execute(): Promise<number> {
|
|
291
|
+
if (this.chrome && this.app) {
|
|
292
|
+
this.context.stderr.write('--chrome and --app name two places: pick one\n')
|
|
293
|
+
return 2
|
|
294
|
+
}
|
|
295
|
+
const act = openAct(this)
|
|
296
|
+
const refused = await pmSwitchDeps(() => {}).post(this.id, act)
|
|
297
|
+
if (refused != null) {
|
|
298
|
+
this.context.stdout.write(`FAIL open ${this.id}: ${refused}\n`)
|
|
299
|
+
return 1
|
|
300
|
+
}
|
|
301
|
+
const where = act === 'app' ? 'as its own app' : act === 'chrome' ? 'in Chrome' : 'in the default browser'
|
|
302
|
+
this.context.stdout.write(`ok: ${this.id} opens ${where}; a process that was down starts first (shipkit gp shows it)\n`)
|
|
303
|
+
return 0
|
|
304
|
+
}
|
|
305
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -11,9 +11,10 @@ import { CheckCommand } from './cli/cmd_Check.ts'
|
|
|
11
11
|
import { CommandsCommand } from './cli/cmd_Commands.ts'
|
|
12
12
|
import { aliasArgs, CrossRepoActivityCommand } from './cli/cmd_CrossRepoActivity.ts'
|
|
13
13
|
import { ConfigKeysCommand } from './cli/cmd_ConfigKeys.ts'
|
|
14
|
-
import { GlobalProcessesCommand, GlobalProcessRestartCommand, GlobalProcessStartCommand, GlobalProcessStopCommand } from './cli/cmd_GlobalProcesses.ts'
|
|
14
|
+
import { GlobalProcessesCommand, GlobalProcessOpenCommand, GlobalProcessRestartCommand, GlobalProcessStartCommand, GlobalProcessStopCommand } from './cli/cmd_GlobalProcesses.ts'
|
|
15
15
|
import { PlannedCommand } from './cli/cmd_Planned.ts'
|
|
16
16
|
import { InsightsCommand } from './cli/cmd_Insights.ts'
|
|
17
|
+
import { GlobalContextCommand } from './cli/cmd_GlobalContext.ts'
|
|
17
18
|
import { OverviewCommand } from './cli/cmd_Overview.ts'
|
|
18
19
|
import { CiCommand } from './cli/cmd_Ci.ts'
|
|
19
20
|
import { ContractsCommand } from './cli/cmd_Contracts.ts'
|
|
@@ -98,9 +99,11 @@ export function buildCli(): Cli {
|
|
|
98
99
|
cli.register(GlobalProcessStartCommand)
|
|
99
100
|
cli.register(GlobalProcessStopCommand)
|
|
100
101
|
cli.register(GlobalProcessRestartCommand)
|
|
102
|
+
cli.register(GlobalProcessOpenCommand)
|
|
101
103
|
cli.register(CrossRepoActivityCommand)
|
|
102
104
|
cli.register(PlannedCommand)
|
|
103
105
|
cli.register(InsightsCommand)
|
|
106
|
+
cli.register(GlobalContextCommand)
|
|
104
107
|
cli.register(OverviewCommand)
|
|
105
108
|
cli.register(ChecksCommand)
|
|
106
109
|
cli.register(WhenCommand)
|
|
@@ -413,7 +413,7 @@ export function buildCollections(root: string, c: RepoConfig, kit: ResolvedKit |
|
|
|
413
413
|
contractsCollection(root, c),
|
|
414
414
|
asksCollection(root, c),
|
|
415
415
|
credsCollection(root, c, { verdicts: () => verdictsOf(checks) }),
|
|
416
|
-
mediaKitCollection(root, kit),
|
|
416
|
+
mediaKitCollection(root, c.serve?.assets === false ? null : kit),
|
|
417
417
|
socialPostsCollection(root),
|
|
418
418
|
reflectionsCollection(root),
|
|
419
419
|
testsCollection(root, c),
|
package/src/core/check-runner.ts
CHANGED
|
@@ -248,6 +248,12 @@ export function testsRan(lastLine: string): number | null {
|
|
|
248
248
|
return m == null ? null : Number(m[1])
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/** the command the suite runner says it ran, read off the same headline
|
|
252
|
+
* (`…, as \`bun test --parallel=8\` (added --parallel=8)`, judge-suites ranAs) */
|
|
253
|
+
export function testsCommand(lastLine: string): string | null {
|
|
254
|
+
return /, (as `[^`]+`(?: \(added [^)]+\))?)/.exec(lastLine)?.[1] ?? null
|
|
255
|
+
}
|
|
256
|
+
|
|
251
257
|
export type Selection = {
|
|
252
258
|
/** what this run measures */
|
|
253
259
|
selected: RunnableCheck[]
|
|
@@ -580,9 +586,12 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
580
586
|
const ran = u.status === 'pass' || u.status === 'warn' ? testsRan(u.lastLine) : null
|
|
581
587
|
const counted = ran != null && head != null && head.kind === 'ok' && (head.text === u.id || head.text.startsWith(`${u.id} (`))
|
|
582
588
|
const id = counted ? `${u.id} ${ran} test${ran === 1 ? '' : 's'}` : u.id
|
|
589
|
+
// and the command the suite runner ran, which can differ from the script's body
|
|
590
|
+
const how = counted ? testsCommand(u.lastLine) : null
|
|
591
|
+
const as = how == null ? '' : ` ${how}`
|
|
583
592
|
const composed = u.synthetic && !u.replayed && head != null
|
|
584
|
-
? [{ ...head, text: `${id} ${ranStamp(u.ms)}${u.tail}` }, ...u.lines.slice(1)]
|
|
585
|
-
: counted ? [{ ...head, text: `${id}${head.text.slice(u.id.length)}` }, ...u.lines.slice(1)] : u.lines
|
|
593
|
+
? [{ ...head, text: `${id} ${ranStamp(u.ms)}${u.tail}${as}` }, ...u.lines.slice(1)]
|
|
594
|
+
: counted ? [{ ...head, text: `${id}${head.text.slice(u.id.length)}${as}` }, ...u.lines.slice(1)] : u.lines
|
|
586
595
|
// every line says where its answer came from: a replay carries its
|
|
587
596
|
// cached stamp already, so one that ran here says so and what it cost
|
|
588
597
|
const shown = u.synthetic || u.replayed || u.attached || u.startedAt === 0 || composed[0] == null
|
|
@@ -798,11 +807,11 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
798
807
|
// attached wait runs nothing, so it holds none
|
|
799
808
|
const slot = pool == null ? null : await takeSlot(pool, { repo: o.config.name, check: u.id }, {
|
|
800
809
|
abort: () => interrupted || bailed,
|
|
801
|
-
onWait: (held) => {
|
|
810
|
+
onWait: (held, why) => {
|
|
802
811
|
if (slotWaitSaid) return
|
|
803
812
|
slotWaitSaid = true
|
|
804
813
|
const who = held.slice(0, 3).map((h) => `${h.repo}/${h.check} pid ${h.pid}`).join(', ')
|
|
805
|
-
emit({ kind: 'ok', text: `waiting for a machine slot: ${held.length}
|
|
814
|
+
emit({ kind: 'ok', text: `waiting for a machine slot: ${held.length} held, ${why}${who === '' ? '' : ` (${who}${held.length > 3 ? ', …' : ''})`}` })
|
|
806
815
|
},
|
|
807
816
|
})
|
|
808
817
|
if (pool != null && slot == null) { u.status = 'skip'; u.note = bailed ? 'bailed' : 'interrupted'; u.lines = []; u.unmeasured = true; return }
|