rvlib-shipkit 1.5.0 → 1.6.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 +46 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +5 -8
- package/src/checks/SK001-package-json-shape.check.ts +5 -4
- package/src/checks/SK003-claude-md-shape.check.ts +2 -2
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +10 -6
- package/src/cli/cmd_CacheAudit.ts +43 -0
- package/src/cli/cmd_CrossRepoActivity.ts +2 -1
- package/src/cli/cmd_GlobalProcesses.ts +4 -3
- package/src/cli/cmd_Home.ts +3 -2
- package/src/cli/cmd_Keys.ts +12 -1
- package/src/cli/cmd_Overview.ts +1 -1
- package/src/cli/cmd_Serve.ts +2 -1
- package/src/cli/cmd_TraceAudit.ts +62 -0
- package/src/cli.ts +4 -0
- package/src/collections/collection-types.ts +3 -0
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +6 -3
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/one-lib.ts +59 -4
- package/src/core/read-set.ts +97 -9
- package/src/core/run-ci.ts +6 -1
- package/src/core/run-fixers.ts +8 -2
- package/src/core/trace-audit.ts +120 -0
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-start.ts +11 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/via-hub.ts +14 -0
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +21 -3
- package/src/ledger/git-key.ts +148 -30
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +131 -16
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +210 -25
- package/src/problems/extract.ts +3 -0
- package/src/repo-config.ts +18 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +33 -1
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +71 -5
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/home-st.ts +1 -1
- package/src/serve/hub-app/home-strip.css +5 -9
- package/src/serve/hub-app/home-strip.tsx +4 -17
- package/src/serve/hub-app/hub-app.tsx +18 -4
- package/src/serve/hub-app/hub.css +24 -20
- package/src/serve/hub-app/keys.ts +10 -2
- package/src/serve/hub-app/processes-view.tsx +16 -9
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +10 -0
- package/src/serve/hub-app/repo-sections.ts +31 -17
- package/src/serve/hub-app/repo-st.ts +24 -5
- package/src/serve/hub-app/sidebar.tsx +5 -5
- package/src/serve/hub-app/tree-build.ts +2 -0
- package/src/serve/hub-app/tree-row.tsx +18 -5
- package/src/serve/hub-app/tree-svc.ts +27 -7
- package/src/serve/hub-runtime.ts +4 -2
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## 1.6.0
|
|
2
|
+
|
|
3
|
+
- **lint, typecheck, test and every task that declares nothing are cached now.** Such a task is keyed on the whole work tree, plus the gitignored paths the new `ignoredInputs` lists for it (`{ test: ['dist/**'] }`, `'*'` for every task; a path by content, `dir/**` by the names under it), and the lockfile, the installed packages, the bun and node versions and the toolkit code behind it. `sk trace-audit` runs each gate task once under a file tracer and names the gitignored paths it reads that its key misses. Tracing is on wherever it can run (Apple silicon macOS): SK031 audits about daily, `sk trace-audit --disable` opts a machine out and `SHIPKIT_TRACE=0` one run, and each finding joins the key of the task that read it at once, in every worktree of the repo. SK032 and the fix pass of `sk ci` then write the findings into their own block of `repo.config.ts`, once the repo pins 1.6.0 or later. The tracer never decides a verdict. It needs Apple silicon macOS: on Linux and in CI nothing audits, and SK030's daily sample is the only net there.
|
|
4
|
+
|
|
5
|
+
- **Every check that did not replay says why**, one line per reason: `ran: src/a.ts changed (lint, typecheck)`, `ran: nothing stored for it yet`, `ran, not kept: it reached the world through …`. A check its author declared never cached says so only under `--verbose`.
|
|
6
|
+
|
|
7
|
+
- **A cache key carries the code behind the answer, not the whole toolkit.** A check is keyed on its own module graph and the runner's, and a repo task on the toolkit modules the repo's config imports, so a toolkit edit re-runs only the checks it can change.
|
|
8
|
+
|
|
9
|
+
- **A command loads the image and comfy libraries only when it runs**: `sk --help` went from 540 ms to 220 ms.
|
|
10
|
+
|
|
11
|
+
- **SK030 audits the cache.** About once a day, while somebody works in the repo, it re-runs a sample of the checks the cache would answer right now, fresh, and compares. A cached green that a fresh run disagrees with fails, naming the check whose key misses a path its body reads, and the stale answer is dropped so it is never served again. `sk cache-audit` is the same audit on demand (`--sample <n>`, `--only <id>`, `--json`).
|
|
12
|
+
|
|
13
|
+
## Unreleased (patch)
|
|
14
|
+
|
|
15
|
+
- **The hub tree groups assets by kit category.** Under media, each category is a row that folds, and its label opens the category page. An asset row reads by its kit label (`mon/brindon/front`), never its file id.
|
|
16
|
+
|
|
17
|
+
- **SK001 no longer asks a comfy repo for a `comfy:up` script.** The box is a global process that `sk gp start comfyui` starts from any repo, so a per-repo script only copied that command. A `.shipkit/comfy/` folder now asks for nothing, and SK001 no longer probes it.
|
|
18
|
+
|
|
19
|
+
- **Every repo's tree has the same top rows.** A section the repo has nothing in (no process, no contract, no secret, no asks, no reflections, no tests, no media, no wiki) stays in the list, greyed, its title saying why. The processes row still opens its page, which says the repo declares none. The others open nothing, and the arrows step over them. Plan, git and admin still exist only on the home.
|
|
20
|
+
|
|
21
|
+
- **Every command starts faster.** A command that draws images or talks to ComfyUI loads canvas, sharp and comfy-ts when it runs, instead of every command loading them at startup (canvas alone read the system fonts). A fully cached `sk ci` drops from about 1.2s to 0.7s, `sk --help` from 540ms to 220ms.
|
|
22
|
+
|
|
23
|
+
- **Every check that did not come from the cache says why.** Above the verdict, one line per reason: `ran: src changed (lint, test)` names what moved since the last stored answer, `ran: nothing stored for it yet`, `ran, not kept: it reached the world through \`git check-ignore -q\`` for an answer that can never be stored, and `never cached: <reason>` for a check no key describes. A check that can never be kept was counted like a key that moved.
|
|
24
|
+
|
|
25
|
+
- **Every repo's media page loads its comfy workflows, not only the first one's.** comfy-ts keeps one instance per process, rooted at one repo, and the daemon serves every repo from one process: the second repo to load its workflows got "conflicts with the existing instance". The instance is now the first repo's, holding only the host caches every repo on that host shares, and each repo's drafts resolve under its own `.comfy-ts/drafts/`.
|
|
26
|
+
|
|
27
|
+
- **Editing shipkit no longer re-runs every check in every repo.** The cache key hashed the toolkit's whole source, and a repo that links it hashed the linked package again, tests included, so a css tweak of the hub or a new test re-ran every cached check everywhere. A key now carries the runner (the modules that run a check and store its answer, and the templates a check reads), the check's own module closure (modules it runs by path included), and for a check running the repo's code, the toolkit modules the repo imports. An edit no answer depends on moves no key, and `sk keys --why` lists the toolkit files behind a key.
|
|
28
|
+
|
|
29
|
+
- **⌘↑ and ⌘↓ open the repo above or below in the project list**, from any page, round both ends. In a text field they still move the caret. The list's foot hints them, and its sort button now just says sort, the order it is in moving to its title.
|
|
30
|
+
|
|
31
|
+
- **An app icon fills its tile, on the repo's colour.** The shipkit app and every process app drew the repo's mark as it was, transparent margin included, so a small mark was a dot on the dock. The icon now cuts the mark to its own box (faint alpha noise, which generated images carry over the whole canvas, counts as margin) and sets it on a rounded plate in the identity colour it stands out on most, on the macOS icon grid. A mark that already fills its box, a photo or a tile, becomes the plate. New colours rebuild the app at its next open.
|
|
32
|
+
|
|
33
|
+
- **The identity colours moved to the admin page, with a preview of the app icon.** The two pickers left the strip. The admin page opens on an identity card: the app icon drawn in the colours being picked (`/api/projects/app-icon/<repo>`, the same drawing the app build uses), and the repo name on a dark and on a light page. The card reads a repo by the name the project list knows, so a repo whose config names it otherwise (screenplay, named screenplay-shed) shows its own colours and icon.
|
|
34
|
+
|
|
35
|
+
- **A broken comfy setup no longer takes the whole media page down.** The workflows load only when a kind generates with comfy. When they fail to load, the media API still mounts: the rolls, the picks and every other kind stay served, and the reason shows in the page errors and in every comfy roll that fails.
|
|
36
|
+
|
|
37
|
+
- **`@napi-rs/canvas` and `comfy-ts` are one copy per process, like the toolkit.** A repo file that imports its own install gets the host copy. A brand that loaded an image with its own canvas copy and drew it on the toolkit's canvas failed ("Value is not one of these types"), and workflows written against an older comfy-ts failed to load.
|
|
38
|
+
|
|
39
|
+
- **A failing test no longer paints the last test file red.** Bun repeats every failure in a recap after the last file of its report, and the hub read those lines as failures of that file: the tests page and the problems pane blamed an innocent suite. The recap now closes the report.
|
|
40
|
+
|
|
41
|
+
- **The arrows walk the repo tree from anywhere on the page.** Up and down move to the next row and open its page at once, with no need to click the tree first. Left and right fold, unfold and climb as before. In the filter box, up and down walk the rows the filter keeps, and left and right stay the text caret's. With no cursor yet, they start from the section of the page shown. A branch with no page of its own only takes the cursor. A walk with the arrows is one history entry, so a held arrow never floods the browser history and back returns to where the walk started. While an asset is open, left and right stay the atelier's roll keys.
|
|
42
|
+
|
|
43
|
+
- **No repo page is a card grid.** Every collection without a table of its own (contracts, reflections, posts, workflows) is now a list, one line per item, red first. The processes page is the overview's process table for that repo, with the opened process's description and log under it. The tables sit on a panel box for contrast, and the page title and group headers are larger and in plain ink.
|
|
44
|
+
|
|
45
|
+
- **The secrets page is a list.** One line per secret, never a card: a dot, the name, missing, minor or present, and its sentence. Missing ones come first.
|
|
46
|
+
|
|
1
47
|
## 1.5.0
|
|
2
48
|
|
|
3
49
|
- **Every check declares where it runs and how it caches, with no default.** A check file carries `runs` (`onPreCommit`, `onCi`, `onFull`, `onRelease`, `onHealth`, `onSave`, `onClock`), `cache` (`files`, `reads`, `ttl`, `fact` or `never`, each with its one parameter), `fix` (`none`, `ci` or `manual`, the function itself is now `repair`) and `applies`. `check()` refuses a missing key, a retired key (`when`, `inputs`, `ttl`, `never`, `every`, `cron`, `whenInactive`, `planOff`, `cacheKey`) and the combinations that contradict each other. The gate, the fix pass, the release preflight and the planner read the declared axes; a release never applies a check repair. Every built-in carries its own placement: SK024, SK017 and SK028 run in the health pass only, and SK005, SK006, SK011, SK014, SK016 and SK022 are out of the quick gate. A check file that does not load is one failing row instead of a dead inventory.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -12,7 +12,8 @@ import { ComfyTS, type AnyVar, type ComfyExecution, type DefinedWorkflow, type M
|
|
|
12
12
|
export type GenProgress = { value: number; max: number; node: string | null }
|
|
13
13
|
export type OnProgress = (p: GenProgress) => void
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/** root: the repo the workflow belongs to, which holds its drafts */
|
|
16
|
+
export type LoadedWorkflow = { id: string; root: string; file: string; wf: DefinedWorkflow }
|
|
16
17
|
export type ComfyTsRuntime = { workflows: Map<string, LoadedWorkflow>; heal?: string }
|
|
17
18
|
|
|
18
19
|
// function declaration, not const — TS only narrows on never-calls to
|
|
@@ -47,9 +48,11 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
|
|
|
47
48
|
// explicit COMFY_TS_QUIET=0 from the caller.
|
|
48
49
|
process.env.COMFY_TS_QUIET ??= '1'
|
|
49
50
|
// before the first module import: a bare ComfyTS.create() inside the
|
|
50
|
-
// module then returns this same instance.
|
|
51
|
-
//
|
|
52
|
-
|
|
51
|
+
// module then returns this same instance. comfy-ts keeps ONE instance per
|
|
52
|
+
// process, and the daemon serves every repo from one process, so only the
|
|
53
|
+
// 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
|
+
if ((globalThis as { comfyts?: unknown }).comfyts == null) ComfyTS.create({ rootPath: root })
|
|
53
56
|
if (!existsSync(dir)) fail(`${dir}/ missing — ship at least one <id>.cflow.ts comfy-ts workflow module (copy from the rv-comfy template)`)
|
|
54
57
|
const out = new Map<string, LoadedWorkflow>()
|
|
55
58
|
for (const f of readdirSync(dir).filter((x) => x.endsWith('.cflow.ts')).sort()) {
|
|
@@ -65,7 +68,7 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
|
|
|
65
68
|
const vars = varsOf(wf)
|
|
66
69
|
if (vars.prompt?.kind !== 'prompt' || vars.seed?.kind !== 'seed')
|
|
67
70
|
fail(`workflow '${id}' must declare vars.prompt (v.prompt) and vars.seed (v.seed) — shipkit drives both`)
|
|
68
|
-
out.set(id, { id, file, wf })
|
|
71
|
+
out.set(id, { id, root, file, wf })
|
|
69
72
|
}
|
|
70
73
|
if (out.size === 0) fail(`${dir}/ has no *.cflow.ts workflow modules — seed one from the rv-comfy template`)
|
|
71
74
|
return out
|
|
@@ -96,8 +99,9 @@ export function applyVarValues(wf: DefinedWorkflow, vars: Record<string, unknown
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
// ── drafts — comfy-ts disk format,
|
|
100
|
-
//
|
|
102
|
+
// ── drafts — comfy-ts disk format, .comfy-ts/drafts/<wfId>/<draftId>.json
|
|
103
|
+
// under the workflow's own repo, never the process instance's root (which is
|
|
104
|
+
// the first repo's in the daemon).
|
|
101
105
|
//
|
|
102
106
|
// The draft id is the KIND, never the asset: ONE `creature` draft behind all
|
|
103
107
|
// 107 creatures, so tuning the loras once tunes them for the whole kind. The
|
|
@@ -107,7 +111,7 @@ export function applyVarValues(wf: DefinedWorkflow, vars: Record<string, unknown
|
|
|
107
111
|
//
|
|
108
112
|
// Shipkit does its own IO here because comfy-ts exports no drafts API yet;
|
|
109
113
|
// swap when it lands.
|
|
110
|
-
const draftDir = (lw: LoadedWorkflow): string =>
|
|
114
|
+
const draftDir = (lw: LoadedWorkflow): string => join(lw.root, '.comfy-ts', 'drafts', lw.id)
|
|
111
115
|
export const draftPath = (lw: LoadedWorkflow, name: string): string => join(draftDir(lw), `${name}.json`)
|
|
112
116
|
|
|
113
117
|
export function readAssetDraft(lw: LoadedWorkflow, draftId: string): Record<string, unknown> | null {
|
|
@@ -49,7 +49,18 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
49
49
|
const workspace = join(root, kit.workspace)
|
|
50
50
|
const dirOf = (id: string): string => assetDir(workspace, id)
|
|
51
51
|
let workflows: Map<string, LoadedWorkflow> | null = null
|
|
52
|
-
|
|
52
|
+
// only a comfy kind needs them, and a broken load costs only comfy
|
|
53
|
+
// generation: the rolls, the picks and every other kind stay served, and
|
|
54
|
+
// the reason rides in /api/state errors and in every comfy roll that fails
|
|
55
|
+
let comfyErr: string | null = null
|
|
56
|
+
if (kit.comfy != null && kit.kinds.some((k) => k.generate.backend === 'comfy')) {
|
|
57
|
+
try {
|
|
58
|
+
workflows = await loadCflowWorkflows(root, join(root, kit.comfy.workflows))
|
|
59
|
+
} catch (e) {
|
|
60
|
+
comfyErr = `comfy workflows not loaded: ${(e as Error).message}`
|
|
61
|
+
console.log(`WARN ${comfyErr}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
53
64
|
|
|
54
65
|
// custom panel scripts fail-fast here too (tests/embedders may bypass loadKit)
|
|
55
66
|
const missingPanels = missingPanelScripts(kit, root)
|
|
@@ -111,6 +122,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
111
122
|
// workflows are pre-loaded above (fail-fast for a comfy studio); the
|
|
112
123
|
// resolver just hands the shared roll generator the ready runtime.
|
|
113
124
|
const resolveComfy = async (): Promise<ComfyTsRuntime> => {
|
|
125
|
+
if (comfyErr != null) throw new Error(comfyErr)
|
|
114
126
|
if (kit.comfy == null || workflows == null) throw new Error('kit has no comfy config — only canvas kinds can generate')
|
|
115
127
|
return { workflows, heal: kit.comfy.heal }
|
|
116
128
|
}
|
|
@@ -243,7 +255,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
|
|
|
243
255
|
derived: parentIdsOf(f).length > 0, stale: await isStale(kit, workspace, f),
|
|
244
256
|
}
|
|
245
257
|
}))
|
|
246
|
-
return json({ ...queue.state, queued: queue.queuedCount, pending: queue.pendingIds(), errors: queue.errors.slice(-5), assets })
|
|
258
|
+
return json({ ...queue.state, queued: queue.queuedCount, pending: queue.pendingIds(), errors: [...(comfyErr == null ? [] : [comfyErr]), ...queue.errors.slice(-5)], assets })
|
|
247
259
|
}
|
|
248
260
|
|
|
249
261
|
if (pathname === '/api/asset') {
|
package/src/check.ts
CHANGED
|
@@ -238,14 +238,11 @@ export type TaskDef = {
|
|
|
238
238
|
timeoutMs?: number
|
|
239
239
|
/** CONTENT population: the repo-relative paths this task reads. Declaring
|
|
240
240
|
* them is what makes it keyable (ledger/git-key.ts). Undeclared is the
|
|
241
|
-
* DEFAULT
|
|
242
|
-
*
|
|
243
|
-
* that costs correctness: the key cannot
|
|
244
|
-
* a cached answer comes back over a tree
|
|
245
|
-
*
|
|
246
|
-
* key by name and never by content, so a task reading a gitignored
|
|
247
|
-
* fixture, a built dir or a .env under declares with `['.']` exactly as
|
|
248
|
-
* it would with a narrow list. */
|
|
241
|
+
* DEFAULT: the task is keyed on the whole work tree plus the repo's
|
|
242
|
+
* `ignoredInputs` (build/shipkit/cache.md). A list MISSING a path
|
|
243
|
+
* the command reads is the one that costs correctness: the key cannot
|
|
244
|
+
* move when that path changes, so a cached answer comes back over a tree
|
|
245
|
+
* it no longer describes. */
|
|
249
246
|
inputs?: string[]
|
|
250
247
|
/** WORLD population: it reads the network, a keychain, $PATH or the clock,
|
|
251
248
|
* so nothing on disk describes its answer and a clean pass is remembered
|
|
@@ -125,13 +125,14 @@ export function evaluatePackageJson(t: CheckCtx): void {
|
|
|
125
125
|
//
|
|
126
126
|
// a shipkit verb is never on this list. `shipkit ci`, `check`, `contracts`,
|
|
127
127
|
// `tui`, `mv`, `assets`, `asks` are typed as themselves, and a package.json
|
|
128
|
-
// row that only forwards to one is a second name for one capability
|
|
128
|
+
// row that only forwards to one is a second name for one capability. nor is
|
|
129
|
+
// a global process (ComfyUI, TTS): `shipkit gp start <id>` starts it from any
|
|
130
|
+
// repo, so a per repo script would only copy it
|
|
129
131
|
function requiredScripts(c: CheckCtx): Array<{ name: string; why: string }> {
|
|
130
132
|
const required: Array<{ name: string; why: string }> = [
|
|
131
133
|
// watch-mode serve — the ONE shipkit surface the bare verb cannot give
|
|
132
134
|
{ name: 'hub', why: 'shipkit repo' },
|
|
133
135
|
]
|
|
134
|
-
if (c.exists('.shipkit/comfy')) required.push({ name: 'comfy:up', why: '.shipkit/comfy/ exists' })
|
|
135
136
|
if (c.exists('deploy.sh')) required.push({ name: 'up', why: 'deploy.sh exists' })
|
|
136
137
|
return required
|
|
137
138
|
}
|
|
@@ -203,8 +204,8 @@ export const packageJsonShape = check({
|
|
|
203
204
|
applies: 'always',
|
|
204
205
|
run: (c) => evaluatePackageJson(c),
|
|
205
206
|
// mechanical repairs only: field flips + scripts whose canonical body the
|
|
206
|
-
// vocabulary page states.
|
|
207
|
-
//
|
|
207
|
+
// vocabulary page states. A body it does NOT state is named in the
|
|
208
|
+
// summary, never invented. rvlib-* roots untouched — their
|
|
208
209
|
// license/private are publish decisions.
|
|
209
210
|
repair: (c): FixResult => {
|
|
210
211
|
const raw = c.read('package.json')
|
|
@@ -18,8 +18,8 @@ export const claudeMdShape = check({
|
|
|
18
18
|
title: 'CLAUDE.md',
|
|
19
19
|
description: 'CLAUDE.md loads the house wiki into every session',
|
|
20
20
|
owner: 'meta/agent-folder.md',
|
|
21
|
-
// keyed on what it reads: CLAUDE.md
|
|
22
|
-
//
|
|
21
|
+
// keyed on what it reads: CLAUDE.md, whether agent/index.md exists, and
|
|
22
|
+
// the ignore files git read for the gitignore probe (c.exec runs traced)
|
|
23
23
|
runs: { onPreCommit: { budgetMs: 40 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
24
24
|
cache: { by: 'reads' },
|
|
25
25
|
fix: 'ci',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SK030 cache audit (build/shipkit/runs.md owns the cache): a sample of the
|
|
2
|
+
// checks the cache answers is re-run fresh about once a day, and a cached
|
|
3
|
+
// green a fresh run disagrees with fails, naming the check whose key misses a
|
|
4
|
+
// path. The work is `sk cache-audit` (core/cache-audit.ts), in its own
|
|
5
|
+
// process, because it runs other checks.
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { check } from '../check.ts'
|
|
8
|
+
|
|
9
|
+
const CLI = join(import.meta.dir, '../cli.ts')
|
|
10
|
+
|
|
11
|
+
export const cacheAudit = check({
|
|
12
|
+
id: 'SK030',
|
|
13
|
+
title: 'cache audit',
|
|
14
|
+
description: 'a sample of cached answers matches what a fresh run of each check says',
|
|
15
|
+
owner: 'build/shipkit/runs.md',
|
|
16
|
+
blocking: true,
|
|
17
|
+
// never a gate: it re-runs other checks. The daemon asks it about once a
|
|
18
|
+
// day, and only while somebody works in the repo
|
|
19
|
+
runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: ['20h', '28h'], inactive: 'wait' } },
|
|
20
|
+
cache: { by: 'never', why: 'it runs other checks fresh, so its answer is that run itself' },
|
|
21
|
+
fix: 'none',
|
|
22
|
+
applies: 'always',
|
|
23
|
+
run: async (c) => {
|
|
24
|
+
const r = await c.exec([process.execPath, CLI, 'cache-audit', '--json'], { cwd: c.root, timeoutMs: 600_000 })
|
|
25
|
+
let out: { pool: number; rows: { id: string; cached: string; fresh: string; keyedOn: string }[]; stale: { id: string; cached: string; fresh: string; keyedOn: string }[] }
|
|
26
|
+
try { out = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') } catch { return c.fail(`sk cache-audit did not answer (exit ${r.code}: ${r.out.trim().slice(0, 160)})`) }
|
|
27
|
+
if (out.rows.length === 0) return c.note(`nothing to audit: the cache answers none of this repo's checks right now`)
|
|
28
|
+
for (const s of out.stale)
|
|
29
|
+
c.fail(`${s.id}: the cache said ${s.cached}, a fresh run says ${s.fresh}, so its key misses something it reads`, `${s.keyedOn === 'files' ? 'add the path to its cache.inputs' : 'move the read through c'}, then sk keys --why ${s.id}`)
|
|
30
|
+
if (out.stale.length === 0) c.ok(`${out.rows.map((x) => x.id).join(', ')}: every cached answer is what a fresh run says (${out.rows.length} of ${out.pool} cached)`)
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export default cacheAudit
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SK031 traced audit (build/shipkit/cache.md owns the key): about once a day
|
|
2
|
+
// each gate task runs under the tracer, and a gitignored path it reads that
|
|
3
|
+
// its key misses joins that task's key from the kept answer. SK032 carries the
|
|
4
|
+
// kept proposals into repo.config.ts in the gate's fix pass. The work is
|
|
5
|
+
// `sk trace-audit` (core/trace-audit.ts), in its own process, because it runs
|
|
6
|
+
// the gate's tasks. On wherever the tracer runs, unless the machine opted out.
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { check } from '../check.ts'
|
|
9
|
+
import { traceUnavailable } from '../trace/tracer.ts'
|
|
10
|
+
|
|
11
|
+
const CLI = join(import.meta.dir, '../cli.ts')
|
|
12
|
+
|
|
13
|
+
export const traceAuditCheck = check({
|
|
14
|
+
id: 'SK031',
|
|
15
|
+
title: 'traced audit',
|
|
16
|
+
description: 'every gitignored path a gate task reads is in its key',
|
|
17
|
+
owner: 'build/shipkit/cache.md',
|
|
18
|
+
blocking: false,
|
|
19
|
+
// never a gate: it runs the gate's tasks. The daemon asks it about once a
|
|
20
|
+
// day, and only while somebody works in the repo
|
|
21
|
+
runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: false, onClock: { every: ['20h', '28h'], inactive: 'wait' } },
|
|
22
|
+
cache: { by: 'never', why: 'it runs the gate tasks under the tracer, so its answer is that run itself' },
|
|
23
|
+
fix: 'none',
|
|
24
|
+
applies: () => traceUnavailable(),
|
|
25
|
+
run: async (c) => {
|
|
26
|
+
const r = await c.exec([process.execPath, CLI, 'trace-audit', '--json'], { cwd: c.root, timeoutMs: 1_200_000 })
|
|
27
|
+
let out: { skipped?: string; tasks: { id: string; status: string; why?: string; uncovered: string[]; propose: string[] }[] }
|
|
28
|
+
try { out = JSON.parse(r.out.trim().split('\n').at(-1) ?? '') } catch { return c.fail(`sk trace-audit did not answer (exit ${r.code}: ${r.out.trim().slice(0, 160)})`) }
|
|
29
|
+
if (out.skipped != null) return c.note(`nothing traced: ${out.skipped}`)
|
|
30
|
+
for (const t of out.tasks.filter((x) => x.status === 'unaudited')) c.warn(`${t.id} could not be audited: ${t.why}`)
|
|
31
|
+
const missing = out.tasks.filter((x) => x.uncovered.length > 0)
|
|
32
|
+
// a finding joins its task's key at once (core/audit-report.ts), so it
|
|
33
|
+
// is news, never a failure: SK032 carries it into repo.config.ts
|
|
34
|
+
for (const t of missing) c.ok(`${t.id} reads ${t.uncovered.length} gitignored path(s) the config does not name, keyed from this audit: ${t.propose.join(', ')}`)
|
|
35
|
+
if (missing.length === 0) c.ok(`${out.tasks.filter((x) => x.status === 'audited').length} gate task(s) audited: every gitignored path they read is in their key`)
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export default traceAuditCheck
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SK032 ignored inputs current (build/shipkit/cache.md owns the key): what the
|
|
2
|
+
// last traced audit (SK031) found a task reading outside its key is in
|
|
3
|
+
// repo.config.ts ignoredInputs, so other machines and clones key it too. The repair writes the missing entries into
|
|
4
|
+
// the managed block, so the gate's fix pass carries each finding into the
|
|
5
|
+
// config. Nothing is traced here: it reads the audit SK031 kept.
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { check } from '../check.ts'
|
|
8
|
+
import { describeByTask, pendingByTask, type AuditReport } from '../core/ignored-inputs.ts'
|
|
9
|
+
|
|
10
|
+
const CLI = join(import.meta.dir, '../cli.ts')
|
|
11
|
+
|
|
12
|
+
/** the first rvlib-shipkit release whose `defineRepo` knows `ignoredInputs`.
|
|
13
|
+
* A repo whose pin allows an older one loads its config elsewhere (a CI job
|
|
14
|
+
* installing from npm, a fresh clone) with that older copy, which refuses
|
|
15
|
+
* the unknown key: so nothing is written until the pin says at least this */
|
|
16
|
+
export const IGNORED_INPUTS_SINCE = '1.6.0'
|
|
17
|
+
|
|
18
|
+
const parts = (v: string): number[] => v.split('.').map((n) => Number.parseInt(n, 10) || 0)
|
|
19
|
+
const atLeast = (v: string, min: string): boolean => {
|
|
20
|
+
const a = parts(v), b = parts(min)
|
|
21
|
+
for (let i = 0; i < 3; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0)
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** why the repo's pin cannot carry the key yet, or null. No pin (the repo
|
|
26
|
+
* that hosts the toolkit, or one running it from source) can carry it */
|
|
27
|
+
function pinTooOld(pkg: string | null): string | null {
|
|
28
|
+
if (pkg == null) return null
|
|
29
|
+
let range: string | undefined
|
|
30
|
+
try {
|
|
31
|
+
const j = JSON.parse(pkg) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
32
|
+
range = j.devDependencies?.['rvlib-shipkit'] ?? j.dependencies?.['rvlib-shipkit']
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
const min = range == null ? null : /(\d+\.\d+\.\d+)/.exec(range)?.[1] ?? null
|
|
37
|
+
if (min == null || range!.startsWith('link:') || range!.startsWith('file:') || range!.startsWith('workspace:')) return null
|
|
38
|
+
return atLeast(min, IGNORED_INPUTS_SINCE) ? null : `package.json pins rvlib-shipkit ${range}, which allows a release before ${IGNORED_INPUTS_SINCE} that refuses ignoredInputs: raise the pin to ^${IGNORED_INPUTS_SINCE} first`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** the kept audit, read through c so the reads key follows it */
|
|
42
|
+
function kept(text: string | null): AuditReport | null {
|
|
43
|
+
try { return text == null ? null : (JSON.parse(text) as AuditReport) } catch { return null }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const ignoredInputsCheck = check({
|
|
47
|
+
id: 'SK032',
|
|
48
|
+
title: 'ignored inputs current',
|
|
49
|
+
description: 'every gitignored path the traced audit saw a task read is in ignoredInputs',
|
|
50
|
+
owner: 'build/shipkit/cache.md',
|
|
51
|
+
blocking: false,
|
|
52
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: false, onHealth: true, onSave: false, onClock: false },
|
|
53
|
+
cache: { by: 'reads' },
|
|
54
|
+
fix: 'ci',
|
|
55
|
+
applies: 'always',
|
|
56
|
+
run: (c) => {
|
|
57
|
+
const missing = pendingByTask(kept(c.state('trace-audit.json')), c.config.ignoredInputs ?? {})
|
|
58
|
+
const n = Object.values(missing).flat().length
|
|
59
|
+
if (n === 0) return c.ok('repo.config.ts ignoredInputs covers what the last traced audit found')
|
|
60
|
+
// the keys on this machine already read the kept audit (core/audit-report.ts),
|
|
61
|
+
// so what is missing here is the copy other machines and clones see
|
|
62
|
+
const old = pinTooOld(c.read('package.json'))
|
|
63
|
+
if (old != null) return c.ok(`${n} path(s) the traced audit found are keyed on this machine only: ${old}`)
|
|
64
|
+
c.warn(`the traced audit found ${describeByTask(missing)}: keyed on this machine, not yet in repo.config.ts ignoredInputs`, 'sk ci writes them, and sk trace-audit --fix says why when it cannot')
|
|
65
|
+
},
|
|
66
|
+
repair: async (c) => {
|
|
67
|
+
const missing = pendingByTask(kept(c.state('trace-audit.json')), c.config.ignoredInputs ?? {})
|
|
68
|
+
if (Object.keys(missing).length === 0) return { changed: false, summary: 'nothing to add' }
|
|
69
|
+
const old = pinTooOld(c.read('package.json'))
|
|
70
|
+
if (old != null) return { changed: false, summary: old }
|
|
71
|
+
// the managed block writer loads repo.config.ts, which no read through
|
|
72
|
+
// c can stand for, so it runs in its own process
|
|
73
|
+
const r = await c.exec([process.execPath, CLI, 'trace-audit', '--fix'], { cwd: c.root, timeoutMs: 60_000 })
|
|
74
|
+
if (r.code !== 0) return { changed: false, summary: `sk trace-audit --fix failed: ${r.out.trim().split('\n').at(-1) ?? `exit ${r.code}`}` }
|
|
75
|
+
return { changed: true, summary: `repo.config.ts ignoredInputs gained ${describeByTask(missing)}` }
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export default ignoredInputsCheck
|
package/src/cli/cmd_Assets.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// (ComfyUI or procedural canvas), curate in the studio, ship with metadata
|
|
3
3
|
// write-through. Thin clipanion surfaces; logic in assets/run-assets.ts.
|
|
4
4
|
import { Command, Option } from 'clipanion'
|
|
5
|
-
import {
|
|
5
|
+
import type { AssetsIo } from '../assets/run-assets.ts'
|
|
6
|
+
|
|
7
|
+
// loaded when an assets command RUNS: it pulls in canvas, sharp and comfy-ts,
|
|
8
|
+
// which every other command would otherwise pay for at startup
|
|
9
|
+
const assets = (): Promise<typeof import('../assets/run-assets.ts')> => import('../assets/run-assets.ts')
|
|
6
10
|
|
|
7
11
|
const ioOf = (cmd: Command): AssetsIo => ({
|
|
8
12
|
print: (l) => cmd.context.stdout.write(`${l}\n`),
|
|
@@ -33,7 +37,7 @@ export class AssetsFillCommand extends Command {
|
|
|
33
37
|
this.context.stderr.write(`FAIL: '${this.n}' is not a positive roll count — usage: shipkit assets fill <N> [category…]\n`)
|
|
34
38
|
return 1
|
|
35
39
|
}
|
|
36
|
-
return runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'fill', n, categories: this.categories })
|
|
40
|
+
return (await assets()).runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'fill', n, categories: this.categories })
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -55,7 +59,7 @@ export class AssetsAddCommand extends Command {
|
|
|
55
59
|
this.context.stderr.write(`FAIL: '${this.n}' is not a positive roll count — usage: shipkit assets add <N> [category…]\n`)
|
|
56
60
|
return 1
|
|
57
61
|
}
|
|
58
|
-
return runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'add', n, categories: this.categories })
|
|
62
|
+
return (await assets()).runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'add', n, categories: this.categories })
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -76,7 +80,7 @@ export class AssetsGenCommand extends Command {
|
|
|
76
80
|
|
|
77
81
|
async execute(): Promise<number> {
|
|
78
82
|
const n = Math.max(1, Number(this.n) || 1)
|
|
79
|
-
return runAssetsGen(process.cwd(), ioOf(this), { assetId: this.assetId, n })
|
|
83
|
+
return (await assets()).runAssetsGen(process.cwd(), ioOf(this), { assetId: this.assetId, n })
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -90,7 +94,7 @@ export class AssetsIdentityCommand extends Command {
|
|
|
90
94
|
})
|
|
91
95
|
|
|
92
96
|
async execute(): Promise<number> {
|
|
93
|
-
return runAssetsIdentity(process.cwd(), ioOf(this))
|
|
97
|
+
return (await assets()).runAssetsIdentity(process.cwd(), ioOf(this))
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -104,6 +108,6 @@ export class AssetsCheckCommand extends Command {
|
|
|
104
108
|
})
|
|
105
109
|
|
|
106
110
|
async execute(): Promise<number> {
|
|
107
|
-
return runAssetsCheck(process.cwd(), ioOf(this))
|
|
111
|
+
return (await assets()).runAssetsCheck(process.cwd(), ioOf(this))
|
|
108
112
|
}
|
|
109
113
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// sk cache-audit — re-runs a sample of the checks the cache would answer right
|
|
2
|
+
// now, fresh, and compares: a cached green a fresh run disagrees with is a
|
|
3
|
+
// key that misses something its check reads (core/cache-audit.ts).
|
|
4
|
+
import { Command, Option } from 'clipanion'
|
|
5
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
6
|
+
import { auditCache } from '../core/cache-audit.ts'
|
|
7
|
+
|
|
8
|
+
export class CacheAuditCommand extends Command {
|
|
9
|
+
static override paths = [['cache-audit']]
|
|
10
|
+
|
|
11
|
+
static override usage = Command.Usage({
|
|
12
|
+
category: 'Health',
|
|
13
|
+
description: 'is a cached green still true: re-run a sample of cached checks fresh and compare',
|
|
14
|
+
details: 'Picks checks the cache would answer right now (keyed on files or on reads), a different stable sample each day, runs each one fresh and compares its verdict with the cached one. A mismatch means the check key misses a path its body reads: the fresh answer replaces the stale one, and the line names the check. Exit 1 on a mismatch.',
|
|
15
|
+
examples: [
|
|
16
|
+
['audit three cached checks', 'sk cache-audit'],
|
|
17
|
+
['a bigger sample', 'sk cache-audit --sample 10'],
|
|
18
|
+
['one check, by id', 'sk cache-audit --only SK004'],
|
|
19
|
+
['machine-readable', 'sk cache-audit --json'],
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
sample = Option.String('--sample', '3', { description: 'how many cached checks to re-run fresh (default 3)' })
|
|
24
|
+
only = Option.Array('--only', { description: 'audit these check ids instead of a sample' })
|
|
25
|
+
json = Option.Boolean('--json', false, { description: 'machine-readable { pool, rows, stale }' })
|
|
26
|
+
|
|
27
|
+
async execute(): Promise<number> {
|
|
28
|
+
const root = process.cwd()
|
|
29
|
+
const n = Number(this.sample)
|
|
30
|
+
if (!Number.isInteger(n) || n < 1) { this.context.stdout.write(`FAIL: --sample takes a whole number above 0, got '${this.sample}'\n`); return 1 }
|
|
31
|
+
const r = await auditCache(root, await loadRepoConfig(root), { sample: n, only: this.only })
|
|
32
|
+
if (this.json) { this.context.stdout.write(`${JSON.stringify(r)}\n`); return r.stale.length > 0 ? 1 : 0 }
|
|
33
|
+
if (r.rows.length === 0) { this.context.stdout.write(`skip: nothing to audit, the cache answers none of ${this.only?.join(', ') ?? 'the checks'} right now\n`); return 0 }
|
|
34
|
+
for (const row of r.rows) {
|
|
35
|
+
const same = row.fresh === row.cached || row.fresh === 'skip'
|
|
36
|
+
this.context.stdout.write(same
|
|
37
|
+
? `ok: ${row.id} cached ${row.cached}, fresh ${row.fresh} (${row.ms}ms)\n`
|
|
38
|
+
: `FAIL: ${row.id} cached ${row.cached}, but a fresh run says ${row.fresh}: its key misses something it reads\n fix: ${row.keyedOn === 'files' ? `add the path its body reads to its cache.inputs` : 'move the read through c, or declare cache files or never'}, then sk keys --why ${row.id}\n`)
|
|
39
|
+
}
|
|
40
|
+
this.context.stdout.write(r.stale.length === 0 ? `ok: ${r.rows.length} of ${r.pool} cached check(s) audited, every cached answer is what a fresh run says\n` : `FAIL: ${r.stale.length} of ${r.rows.length} audited check(s) served a stale answer\n`)
|
|
41
|
+
return r.stale.length > 0 ? 1 : 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -8,7 +8,6 @@ import { homedir } from 'os'
|
|
|
8
8
|
import { Command, Option } from 'clipanion'
|
|
9
9
|
import { activityOf, type Declared } from '../home/activity.ts'
|
|
10
10
|
import { ageShort, clampCap, DEFAULT_CAP, DEFAULT_MINUTES, dirtWords, idleRow, upstreamWords, type ActivityAnswer, type ActivityCommit, type ActivityRow } from '../home/activity-shape.ts'
|
|
11
|
-
import { railRepos } from '../serve/project-gutter-route.ts'
|
|
12
11
|
|
|
13
12
|
/** a span as typed: `10m`, `1h`, `3d`, `90` (minutes); null when it is none */
|
|
14
13
|
export function spanMinutes(v: string): number | null {
|
|
@@ -108,6 +107,8 @@ export class CrossRepoActivityCommand extends Command {
|
|
|
108
107
|
const cap = clampCap(this.maxCommit ?? DEFAULT_CAP)
|
|
109
108
|
const declared = await daemonDeclared()
|
|
110
109
|
// HOME as the shell has it now: the house include names `~`
|
|
110
|
+
// the hub route module draws app icons, so it loads only when this runs
|
|
111
|
+
const { railRepos } = await import('../serve/project-gutter-route.ts')
|
|
111
112
|
const a = await activityOf(railRepos(process.cwd(), process.env.HOME || homedir()), minutes, Date.now(), declared)
|
|
112
113
|
if (this.json) {
|
|
113
114
|
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
@@ -13,7 +13,6 @@ import type { ResourceView } from '../home/resources.ts'
|
|
|
13
13
|
import { coreGroup, declaredRow, groupRows, processGroups, stateWords, type ProcessGroup } from '../home/process-rows.ts'
|
|
14
14
|
import type { PmConfig } from '../home/pm-config.ts'
|
|
15
15
|
import { readPmConfig } from '../home/run-pm.ts'
|
|
16
|
-
import { railRepos } from '../serve/project-gutter-route.ts'
|
|
17
16
|
|
|
18
17
|
export type GlobalProcessesAnswer = {
|
|
19
18
|
/** pm as it answered, null when it did not */
|
|
@@ -75,11 +74,13 @@ export async function askPm(url = pmUrl()): Promise<PmAnswer | null> {
|
|
|
75
74
|
|
|
76
75
|
/** the project list's order and the declared units, read from disk the way
|
|
77
76
|
* pm reads them: what the table falls back on while pm does not answer */
|
|
78
|
-
export function localProcessDeps(cwd: string): Pick<GlobalProcessesDeps, 'declared' | 'order'
|
|
77
|
+
export async function localProcessDeps(cwd: string): Promise<Pick<GlobalProcessesDeps, 'declared' | 'order'>> {
|
|
79
78
|
// HOME as the shell has it now: the house include names `~`
|
|
80
79
|
const home = process.env.HOME || homedir()
|
|
81
80
|
let order: string[] = []
|
|
82
81
|
try {
|
|
82
|
+
// the hub route module draws app icons: loaded only when this runs
|
|
83
|
+
const { railRepos } = await import('../serve/project-gutter-route.ts')
|
|
83
84
|
order = railRepos(cwd, home).map((r) => r.name)
|
|
84
85
|
} catch {
|
|
85
86
|
// no rail from here: the groups sort by name
|
|
@@ -146,7 +147,7 @@ export class GlobalProcessesCommand extends Command {
|
|
|
146
147
|
json = Option.Boolean('--json', false, { description: 'the rows as json: { pm, now, groups, errors, house }, groups exactly as the overview draws them' })
|
|
147
148
|
|
|
148
149
|
async execute(): Promise<number> {
|
|
149
|
-
const a = await globalProcessesAnswer({ ...localProcessDeps(process.cwd()), pm: () => askPm(), daemon: () => askDaemon(), now: Date.now() })
|
|
150
|
+
const a = await globalProcessesAnswer({ ...(await localProcessDeps(process.cwd())), pm: () => askPm(), daemon: () => askDaemon(), now: Date.now() })
|
|
150
151
|
if (this.json) {
|
|
151
152
|
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
152
153
|
return 0
|
package/src/cli/cmd_Home.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { homeUp, homeUrl, hubBase } from '../home/home-paths.ts'
|
|
|
10
10
|
import { homeRepo, houseIndex } from '../serve/project-gutter.ts'
|
|
11
11
|
import { homedir } from 'os'
|
|
12
12
|
import { execPm, runPm } from '../home/run-pm.ts'
|
|
13
|
-
import { runDaemon } from '../home/run-daemon.ts'
|
|
14
13
|
import { runWeb } from '../home/run-web.ts'
|
|
15
14
|
|
|
16
15
|
const OWN_CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
|
|
@@ -71,6 +70,8 @@ export class DaemonCommand extends Command {
|
|
|
71
70
|
async execute(): Promise<number> {
|
|
72
71
|
const port = portOf(this, this.port)
|
|
73
72
|
if (port === null) return 1
|
|
73
|
+
// the daemon hosts every hub, image tools included: loaded when it runs
|
|
74
|
+
const { runDaemon } = await import('../home/run-daemon.ts')
|
|
74
75
|
return runDaemon(process.cwd(), io(this), { port })
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -169,7 +170,7 @@ export class GuiCommand extends Command {
|
|
|
169
170
|
// `.` = the repo you stand in, named the way the rail names it
|
|
170
171
|
const name = this.repo === '.' && houseIndex(homeRepo(process.cwd()), homedir()) != null ? basename(homeRepo(process.cwd())) : this.repo
|
|
171
172
|
const url = `${homeUrl()}${name != null && name !== '.' ? `${hubBase(name)}/` : '/'}`
|
|
172
|
-
const cmd = openWindow(url, { version: LIB_VERSION, warn: out.printErr })
|
|
173
|
+
const cmd = await openWindow(url, { version: LIB_VERSION, warn: out.printErr })
|
|
173
174
|
out.print(`ok: a home window on ${url}${cmd[0] === 'open' ? ' (the shipkit app)' : ''}`)
|
|
174
175
|
return 0
|
|
175
176
|
}
|
package/src/cli/cmd_Keys.ts
CHANGED
|
@@ -75,6 +75,14 @@ export class KeysCommand extends Command {
|
|
|
75
75
|
return 0
|
|
76
76
|
}
|
|
77
77
|
out(` key ${k.key}`)
|
|
78
|
+
// a lib hash names nothing by itself: the files behind it are what
|
|
79
|
+
// an edit has to touch to move this key
|
|
80
|
+
const lib = k.libFiles ?? []
|
|
81
|
+
if (lib.length > 0) {
|
|
82
|
+
out(` toolkit files in its lib components: ${lib.length}`)
|
|
83
|
+
for (const f of lib.slice(0, 12)) out(` ${f}`)
|
|
84
|
+
if (lib.length > 12) out(` … ${lib.length - 12} more (shipkit keys --json)`)
|
|
85
|
+
}
|
|
78
86
|
if (k.population === 'reads') {
|
|
79
87
|
for (const comp of k.components) out(` ${comp}`)
|
|
80
88
|
// the rest of its identity is what it READ, stored with the
|
|
@@ -89,12 +97,15 @@ export class KeysCommand extends Command {
|
|
|
89
97
|
const lists = Object.entries(set.lists)
|
|
90
98
|
const dirs = Object.entries(set.dirs ?? {})
|
|
91
99
|
const links = Object.entries(set.links ?? {})
|
|
92
|
-
|
|
100
|
+
// outside the repo a traced read is keyed on stat alone
|
|
101
|
+
const outside = Object.keys(set.sigs ?? {}).filter((p) => !(p in set.files))
|
|
102
|
+
out(` read set: ${files.length} file(s), ${exists.length} existence probe(s), ${lists.length} listing(s), ${dirs.length} dir read(s)${links.length > 0 ? `, ${links.length} link probe(s)` : ''}${outside.length > 0 ? `, ${outside.length} file(s) outside the repo, by stat` : ''}`)
|
|
93
103
|
for (const [p, oid] of files) out(` read ${p}=${oid ?? 'absent'}`)
|
|
94
104
|
for (const [p, kind] of exists) out(` exists ${p}=${kind ?? 'absent'}`)
|
|
95
105
|
for (const [g, h] of lists) out(` list ${g}=${h}`)
|
|
96
106
|
for (const [d, h] of dirs) out(` entries ${d}=${h ?? 'absent'}`)
|
|
97
107
|
for (const [p, is] of links) out(` link ${p}=${is ? 'yes' : 'no'}`)
|
|
108
|
+
for (const p of outside) out(` stat ${p}=${set.sigs![p]}`)
|
|
98
109
|
return 0
|
|
99
110
|
}
|
|
100
111
|
// the excluded block is 44 lines here and it buries the component
|
package/src/cli/cmd_Overview.ts
CHANGED
|
@@ -88,7 +88,7 @@ export class OverviewCommand extends Command {
|
|
|
88
88
|
json = Option.Boolean('--json', false, { description: 'the answers as json: { now, pm, daemon, processes, widgets }, processes as `shipkit gp --json`, widgets the daemon route\'s payload' })
|
|
89
89
|
|
|
90
90
|
async execute(): Promise<number> {
|
|
91
|
-
const a = await overviewAnswer({ ...localProcessDeps(process.cwd()), pm: () => askPm(), daemon: () => askDaemon(), widgets: () => askWidgets(), now: Date.now() })
|
|
91
|
+
const a = await overviewAnswer({ ...(await localProcessDeps(process.cwd())), pm: () => askPm(), daemon: () => askDaemon(), widgets: () => askWidgets(), now: Date.now() })
|
|
92
92
|
if (this.json) {
|
|
93
93
|
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
94
94
|
return 0
|
package/src/cli/cmd_Serve.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// shipkit serve — the ONE hub server per repo (one vscode folder-open task,
|
|
2
2
|
// clickable URLs). Thin clipanion surface; logic in serve/run-serve.ts.
|
|
3
3
|
import { Command, Option } from 'clipanion'
|
|
4
|
-
import { runServe } from '../serve/run-serve.ts'
|
|
5
4
|
|
|
6
5
|
export class ServeCommand extends Command {
|
|
7
6
|
static override paths = [['serve']]
|
|
@@ -27,6 +26,8 @@ export class ServeCommand extends Command {
|
|
|
27
26
|
return 1
|
|
28
27
|
}
|
|
29
28
|
}
|
|
29
|
+
// the hub runtime carries the image tools: loaded when it serves
|
|
30
|
+
const { runServe } = await import('../serve/run-serve.ts')
|
|
30
31
|
return runServe(process.cwd(), {
|
|
31
32
|
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
32
33
|
printErr: (l) => this.context.stderr.write(`${l}\n`),
|