rvlib-shipkit 1.8.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 +32 -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/cli/cmd_Ci.ts +5 -4
- 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/one-lib.ts +4 -2
- package/src/core/run-ci.ts +12 -11
- package/src/core/slots.ts +36 -6
- package/src/home/gui-app/ShipkitGui.swift +96 -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 +3 -2
- package/src/serve/project-gutter.ts +5 -2
- package/src/serve/run-lanes.ts +7 -3
- package/src/smoke/run-smoke.ts +13 -6
- package/src/suite/judge-suites.ts +52 -4
- package/src/suite/suite-cli.ts +21 -13
- package/src/ts/typecheck-cache.ts +11 -0
- package/src/ts/typecheck-cli.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
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
|
+
|
|
1
33
|
## 1.8.0
|
|
2
34
|
|
|
3
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.
|
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
|
}
|
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]),
|
|
@@ -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 }
|
|
@@ -158,13 +158,28 @@ function unloadable(path: string, message: string): CheckDef {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** a built-in that does not load, as a repo that only CONSUMES the toolkit
|
|
162
|
+
* sees it: one warning, never a red gate, since only the toolkit's own repo
|
|
163
|
+
* can fix the file, and its gate stays red on it */
|
|
164
|
+
function brokenBuiltin(path: string, message: string): CheckDef {
|
|
165
|
+
return {
|
|
166
|
+
...unloadable(path, message),
|
|
167
|
+
blocking: false,
|
|
168
|
+
run: (c) => c.warn(`${basename(path)} does not load, so this gate skips it: ${message.replace(`${path}: `, '')}`, 'the toolkit repo owns the file, and its own gate fails on it'),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const BUILTIN_OWNER = 'build/shipkit/built-in-checks.md'
|
|
173
|
+
|
|
161
174
|
/** every check this repo can run: its own files, plus the toolkit's. Throws
|
|
162
175
|
* on a registration nobody can act on (a duplicate id, a repo check squatting
|
|
163
176
|
* the reserved prefix, a built-in with no owner page), so a broken
|
|
164
|
-
* declaration dies at load rather than half way through a gate.
|
|
165
|
-
|
|
177
|
+
* declaration dies at load rather than half way through a gate. A broken
|
|
178
|
+
* built-in is that only in the toolkit's own repo: elsewhere it warns.
|
|
179
|
+
* `builtinDir` is the toolkit's checks dir, a parameter for tests only */
|
|
180
|
+
export async function discoverChecks(root: string, builtinDir: string = BUILTIN_CHECK_DIR): Promise<DiscoveredCheck[]> {
|
|
166
181
|
const builtins: string[] = []
|
|
167
|
-
walk(
|
|
182
|
+
walk(builtinDir, builtins)
|
|
168
183
|
const found: string[] = []
|
|
169
184
|
walk(root, found)
|
|
170
185
|
const units = repoUnitFiles(root)
|
|
@@ -173,6 +188,9 @@ export async function discoverChecks(root: string): Promise<DiscoveredCheck[]> {
|
|
|
173
188
|
// because a consumer reaches the same files through a linked node_modules
|
|
174
189
|
const real = (f: string): string => { try { return realpathSync(f) } catch { return f } }
|
|
175
190
|
const shipped = new Set(builtins.map(real))
|
|
191
|
+
// its own source, never an install of it
|
|
192
|
+
const inRoot = relative(real(root), real(builtinDir))
|
|
193
|
+
const hostsToolkit = !inRoot.startsWith('..') && !inRoot.split(/[\\/]/).includes('node_modules')
|
|
176
194
|
const all = found.filter((f) => !shipped.has(real(f))).sort()
|
|
177
195
|
builtins.sort()
|
|
178
196
|
const names = fallbackNames(root, all)
|
|
@@ -185,23 +203,34 @@ export async function discoverChecks(root: string): Promise<DiscoveredCheck[]> {
|
|
|
185
203
|
for (const f of [...builtins, ...own, ...units]) {
|
|
186
204
|
const isBuiltin = builtins.includes(f)
|
|
187
205
|
let def: CheckDef | null
|
|
206
|
+
let loadError: string | null = null
|
|
188
207
|
try {
|
|
189
208
|
def = await load(f)
|
|
190
209
|
} catch (e) {
|
|
191
210
|
// a file that does not load is ONE red row, never a dead inventory:
|
|
192
211
|
// every other check still runs, and this one says why in every run
|
|
193
|
-
|
|
212
|
+
loadError = (e as Error).message
|
|
213
|
+
def = unloadable(f, loadError)
|
|
194
214
|
}
|
|
195
215
|
if (def == null) continue
|
|
196
216
|
const rel = isBuiltin ? null : relative(root, f).replace(/\\/g, '/')
|
|
197
217
|
const fallback = isBuiltin
|
|
198
218
|
? (BUILTIN_ID_RE.exec(base(f))?.[1] ?? base(f))
|
|
199
219
|
: f.endsWith(UNIT_SUFFIX) ? unitBase(f) : names.get(f)!
|
|
200
|
-
|
|
220
|
+
let id = def.id ?? fallback
|
|
201
221
|
if (isBuiltin) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
const refused = loadError != null ? null
|
|
223
|
+
: id !== fallback ? `${f}: declares id '${id}' but its file name says '${fallback}' — a built-in id is its file name`
|
|
224
|
+
: (def.owner ?? '') === '' ? `${f}: a built-in check names the page that OWNS the invariant`
|
|
225
|
+
: RETIRED_CHECK_IDS[id] != null ? `${f}: id '${id}' is retired (${RETIRED_CHECK_IDS[id]}), and an id is never reused`
|
|
226
|
+
: null
|
|
227
|
+
// the toolkit's own repo dies on a refused built-in, a consumer
|
|
228
|
+
// only warns: it cannot fix the file, and must not lose its gate
|
|
229
|
+
if (refused != null && hostsToolkit) throw new Error(refused)
|
|
230
|
+
if (refused != null || loadError != null) {
|
|
231
|
+
def = hostsToolkit ? { ...def, owner: BUILTIN_OWNER } : { ...brokenBuiltin(f, refused ?? loadError!), owner: BUILTIN_OWNER }
|
|
232
|
+
id = fallback
|
|
233
|
+
}
|
|
205
234
|
} else if (id.startsWith('SK')) {
|
|
206
235
|
throw new Error(`${rel}: id '${id}' uses the reserved SK prefix — a repo check picks its own (e.g. RV${id.slice(-3)}) or plain words`)
|
|
207
236
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// does a gate run its repair pass (core/run-fixers.ts). A leaf, so the hub's
|
|
2
|
+
// run lanes read the same answer the gate acts on without loading the gate.
|
|
3
|
+
|
|
4
|
+
/** `fix` named wins. Unnamed: a whole gate repairs, a narrowed run does not */
|
|
5
|
+
export const fixPassRuns = (o: { fix?: boolean; onlyIds?: readonly string[] }): boolean => o.fix ?? (o.onlyIds ?? []).length === 0
|
|
@@ -20,6 +20,7 @@ import type { CheckCtx } from './check-ctx.ts'
|
|
|
20
20
|
import { checkUnitReader, type UnitReader } from '../release/unit-resolve.ts'
|
|
21
21
|
import { parseJsonc } from '../ts/jsonc.ts'
|
|
22
22
|
import { resolveTypecheckProjects } from '../ts/tsconfig-resolve.ts'
|
|
23
|
+
import { gateBuildInfoArgs } from '../ts/typecheck-cache.ts'
|
|
23
24
|
import { parseBunTestScript, suiteFlags } from '../suite/judge-suites.ts'
|
|
24
25
|
import { declaredTestUnits, siblingUnits, testCheckId, unitIgnores, unitTestBody, unitTsconfig, type TestUnitDecl } from '../suite/test-units.ts'
|
|
25
26
|
import { describeFirst, type TaskDef } from '../check.ts'
|
|
@@ -194,7 +195,7 @@ function builtinFloorTask(id: string, root: string, io: FloorReader): { task: Ta
|
|
|
194
195
|
const projects = resolveTypecheckProjects(tsconfig, io, root)
|
|
195
196
|
plain = projects.projects.length === 1 && projects.projects[0] === tsconfig && projects.missing.length === 0
|
|
196
197
|
} catch { plain = false }
|
|
197
|
-
if (plain) return { task: { id, cmd: [tsc, '--noEmit'] }, source: 'built-in (tsc)' }
|
|
198
|
+
if (plain) return { task: { id, cmd: [tsc, '--noEmit', ...gateBuildInfoArgs(tsconfig)] }, source: 'built-in (tsc)' }
|
|
198
199
|
const cli = fileURLToPath(import.meta.resolve('rvlib-shipkit/src/ts/typecheck-cli.ts'))
|
|
199
200
|
return { task: { id, cmd: ['bun', cli, tsc, tsconfig] }, source: 'built-in (tsc)' }
|
|
200
201
|
}
|
package/src/core/git-root.ts
CHANGED
|
@@ -58,14 +58,23 @@ export function probeGitRoot(at: string): Probe {
|
|
|
58
58
|
return p
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** the variables that let git find a repo with no `.git` in reach
|
|
61
|
+
/** the variables that let git find a repo with no `.git` in reach. They name
|
|
62
|
+
* ONE repo for the whole process (git sets GIT_DIR for every hook in a linked
|
|
63
|
+
* worktree), so they only answer for a directory with no `.git` of its own:
|
|
64
|
+
* any other directory is asked with them stripped, or the hook's repo would
|
|
65
|
+
* claim the house and every repo the process looks at */
|
|
62
66
|
const LOCATING_ENV = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']
|
|
67
|
+
const locatingEnvSet = (): boolean => LOCATING_ENV.some((k) => (process.env[k] ?? '') !== '')
|
|
68
|
+
const discoveryEnv = (): Record<string, string | undefined> => {
|
|
69
|
+
const env = { ...process.env }
|
|
70
|
+
for (const k of LOCATING_ENV) delete env[k]
|
|
71
|
+
return env
|
|
72
|
+
}
|
|
63
73
|
|
|
64
|
-
/** git's discovery walks up looking for `.git`. With none at or above
|
|
65
|
-
*
|
|
66
|
-
*
|
|
74
|
+
/** git's discovery walks up looking for `.git`. With none at or above, its
|
|
75
|
+
* answer is already known, and a gate or a test suite that probes fresh temp
|
|
76
|
+
* dirs would spawn a process per dir for it */
|
|
67
77
|
function nothingInReach(dir: string): boolean {
|
|
68
|
-
if (LOCATING_ENV.some((k) => (process.env[k] ?? '') !== '')) return false
|
|
69
78
|
let cur: string
|
|
70
79
|
try {
|
|
71
80
|
cur = realpathSync(dir)
|
|
@@ -81,12 +90,11 @@ function nothingInReach(dir: string): boolean {
|
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
/** the common case answered from the disk: `.git` is a plain directory right
|
|
84
|
-
* at `dir`, which git describes as exactly that. Anything unusual (a
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
93
|
+
* at `dir`, which git describes as exactly that. Anything unusual (a gitfile
|
|
94
|
+
* or a symlinked `.git`, a shared common dir, a config that relocates the
|
|
95
|
+
* work tree, marks it bare or includes another file, a `.git` owned by
|
|
96
|
+
* someone else) is left to git */
|
|
88
97
|
function plainRoot(dir: string): GitRoot | null {
|
|
89
|
-
if (LOCATING_ENV.some((k) => (process.env[k] ?? '') !== '')) return null
|
|
90
98
|
try {
|
|
91
99
|
const top = realpathSync(dir)
|
|
92
100
|
const gitDir = join(top, '.git')
|
|
@@ -102,8 +110,9 @@ function plainRoot(dir: string): GitRoot | null {
|
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function ask(dir: string): Probe {
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
const none = nothingInReach(dir)
|
|
114
|
+
if (none && !locatingEnvSet()) return { root: null, error: 'fatal: not a git repository (or any of the parent directories): .git' }
|
|
115
|
+
const plain = none ? null : plainRoot(dir)
|
|
107
116
|
if (plain != null) return { root: plain, error: '' }
|
|
108
117
|
let p: ReturnType<typeof Bun.spawnSync>
|
|
109
118
|
try {
|
|
@@ -111,6 +120,7 @@ function ask(dir: string): Probe {
|
|
|
111
120
|
cwd: dir,
|
|
112
121
|
stdout: 'pipe',
|
|
113
122
|
stderr: 'pipe',
|
|
123
|
+
env: none ? { ...process.env } : discoveryEnv(),
|
|
114
124
|
})
|
|
115
125
|
} catch (e) {
|
|
116
126
|
return { root: null, error: e instanceof Error ? e.message : String(e) }
|
package/src/core/one-lib.ts
CHANGED
|
@@ -119,7 +119,9 @@ export function useOneLib(root: string = LIB_ROOT): void {
|
|
|
119
119
|
build.onLoad({ filter: /\/(node_modules\/rvlib-shipkit|install\/cache\/rvlib-shipkit@[^/]*)\// }, (a) => {
|
|
120
120
|
const loader = LOADERS[extname(a.path)] ?? 'ts'
|
|
121
121
|
const sub = foreignLibPath(a.path, root)
|
|
122
|
-
|
|
122
|
+
// the host copy itself lives under node_modules when it is an
|
|
123
|
+
// install: its path can carry importFresh's `?v=` stamp
|
|
124
|
+
if (sub == null) return { contents: readFileSync(a.path.replace(/\?.*$/, ''), 'utf8'), loader }
|
|
123
125
|
return reexport(join(root, sub))
|
|
124
126
|
})
|
|
125
127
|
for (const name of SHARED) {
|
|
@@ -131,7 +133,7 @@ export function useOneLib(root: string = LIB_ROOT): void {
|
|
|
131
133
|
const skip = [...new Set([host, real(host)])].map((h) => `(?!${esc(h)}/)`).join('')
|
|
132
134
|
build.onLoad({ filter: new RegExp(`^${skip}.*/node_modules/${esc(name)}/`) }, (a) => {
|
|
133
135
|
const sub = foreignSharedPath(a.path, name, host)
|
|
134
|
-
if (sub == null) return { contents: readFileSync(a.path, 'utf8'), loader: LOADERS[extname(a.path)] ?? 'js' }
|
|
136
|
+
if (sub == null) return { contents: readFileSync(a.path.replace(/\?.*$/, ''), 'utf8'), loader: LOADERS[extname(a.path)] ?? 'js' }
|
|
135
137
|
return reexport(sharedTarget(a.path, sub, name, host))
|
|
136
138
|
})
|
|
137
139
|
}
|