rvlib-shipkit 0.38.1 → 1.0.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 +367 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/serve/serve-hub.ts
CHANGED
|
@@ -1,31 +1,80 @@
|
|
|
1
1
|
// shipkit serve — the repo's ONE Bun.serve, serving the ONE page (one server per repo, one surface, mobx+react). `/` is
|
|
2
2
|
// the hub app (Bun HTML import — media studio + asks + creds + posts +
|
|
3
|
-
// health +
|
|
3
|
+
// health + wikis as sections); the studio route table mounts as the media
|
|
4
4
|
// API under /media/*; graph canvases stay as drill-down pages. Route logic
|
|
5
5
|
// only — resolving WHAT to mount from repo.config is run-serve.ts's job.
|
|
6
6
|
import { existsSync } from 'fs'
|
|
7
7
|
import { join } from 'path'
|
|
8
|
-
import {
|
|
8
|
+
import { positionsFile } from 'rvlib-shipkit/src/wiki/graph-positions.ts'
|
|
9
|
+
import { graphRoutes } from 'rvlib-shipkit/src/wiki/serve-graph.ts'
|
|
9
10
|
import type { StudioRoutes } from '../assets/server/studio-server.ts'
|
|
11
|
+
import { comfyBoxUp, type ComfyBox, type ComfyPanelUp } from '../comfy/comfy-panel.ts'
|
|
10
12
|
import { appendAskRemark, appendAssetNote, appendPostFeedback, askDone, askProof, markPosted, pickPostVariant } from '../collections/actions.ts'
|
|
11
|
-
import type
|
|
13
|
+
import { projectResources, type Collection, type Instance, type NeedItem } from '../collections/collection-types.ts'
|
|
14
|
+
import type { CheckEvent } from '../core/run-check.ts'
|
|
15
|
+
import { configDirty, readGateBlock, writeGateBlock, type GateEdit } from '../core/when-block.ts'
|
|
16
|
+
import type { InvokedBy } from '../ledger/run-log.ts'
|
|
12
17
|
import type { GraphMount } from '../wiki/graph-mount.ts'
|
|
18
|
+
import type { ProseView } from '../prose/prose-view.ts'
|
|
19
|
+
import type { Timeline } from '../ledger/run-timeline.ts'
|
|
20
|
+
import type { TestCheckView, TestsRootView } from '../collections/test-time.ts'
|
|
21
|
+
import type { LiveView } from '../ledger/live-timeline.ts'
|
|
22
|
+
|
|
23
|
+
export type { LiveView }
|
|
24
|
+
import type { HealthSchedule } from './health-schedule.ts'
|
|
13
25
|
import hubIndex from './hub-app/index.html'
|
|
26
|
+
import { projectGutter } from './project-gutter-route.ts'
|
|
27
|
+
import { sameOrigin } from './same-origin.ts'
|
|
28
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
29
|
+
|
|
30
|
+
export { sameOrigin }
|
|
14
31
|
|
|
15
32
|
export type HubTool = { path: string; label: string; desc: string }
|
|
33
|
+
|
|
34
|
+
/** what a running gate streams back to the page */
|
|
35
|
+
export type CheckTap = { line: (l: string) => void; check: (e: CheckEvent) => void }
|
|
36
|
+
/** the comfy panel, as the hub needs it: the child process it embeds and the
|
|
37
|
+
* box those workflows generate on (src/comfy/comfy-panel.ts owns both) */
|
|
38
|
+
export type ComfyDeps = {
|
|
39
|
+
ensure: () => Promise<ComfyPanelUp>
|
|
40
|
+
restart: () => Promise<ComfyPanelUp>
|
|
41
|
+
box: () => ComfyBox
|
|
42
|
+
stop: () => void
|
|
43
|
+
}
|
|
16
44
|
export type HubDeps = {
|
|
17
45
|
root: string
|
|
18
46
|
name: string
|
|
19
47
|
/** 0 = random (tests); real launches pass repo.config/default */
|
|
20
48
|
port: number
|
|
21
49
|
/** every md graph this hub serves, in card order: /graph/<id>. run-serve
|
|
22
|
-
* resolves the list, the routes AND the
|
|
50
|
+
* resolves the list, the routes AND the wikis page read it */
|
|
23
51
|
graphs: GraphMount[]
|
|
24
52
|
/** the media API (mounted under /media/*); null only if the route
|
|
25
53
|
* table failed to build — the page shows the WARN, never a second page */
|
|
26
54
|
studio: StudioRoutes | null
|
|
27
|
-
/**
|
|
28
|
-
|
|
55
|
+
/** the check run, IN PROCESS: wire lines and per CHECK results stream out
|
|
56
|
+
* as they finish, which is what lets a tree row recolour the moment its
|
|
57
|
+
* check is done. `only` narrows it to a named set (the run button on a
|
|
58
|
+
* check page, or on a folder header); `gate` asks for the whole gate,
|
|
59
|
+
* tasks included. `auto` marks a run the hub started on its own (at
|
|
60
|
+
* start, after a save): quick tier, no repair pass, so it never writes
|
|
61
|
+
* into a tree someone is editing. run-serve owns the routing between the
|
|
62
|
+
* two engines. */
|
|
63
|
+
runChecks: (tap: CheckTap, o?: { only?: string[]; gate?: boolean; by?: InvokedBy; auto?: boolean }) => Promise<number>
|
|
64
|
+
/** the daily health pass timer (health-schedule.ts). The page reads its
|
|
65
|
+
* state to say when a health check runs next; absent = no timer */
|
|
66
|
+
schedule?: HealthSchedule | null
|
|
67
|
+
/** every run on record, shaped for the timeline page. Absent = the page
|
|
68
|
+
* says there is no record rather than drawing an empty one */
|
|
69
|
+
timeline?: () => Promise<Timeline>
|
|
70
|
+
/** where the test checks' time goes: the `tests` page (check null) or one
|
|
71
|
+
* test check's page (collections/test-time.ts). Absent = 404 */
|
|
72
|
+
tests?: (check: string | null) => Promise<TestsRootView | TestCheckView>
|
|
73
|
+
/** the live strip's view: the recent runs and the ones in flight, here
|
|
74
|
+
* and in the other house repos, shaped on the server clock */
|
|
75
|
+
live?: () => LiveView
|
|
76
|
+
/** another house repo's hub url when it serves, null when it does not */
|
|
77
|
+
repoHub?: (repo: string) => Promise<string | null>
|
|
29
78
|
/** every human-action item, red/yellow, gathered per request from the
|
|
30
79
|
* collection providers (run-serve builds it) */
|
|
31
80
|
needs: () => Promise<NeedItem[]>
|
|
@@ -34,27 +83,48 @@ export type HubDeps = {
|
|
|
34
83
|
collections: () => Collection[]
|
|
35
84
|
/** asks dir (repo-root-relative) for the act-in-place ask verbs */
|
|
36
85
|
asksDir: string
|
|
86
|
+
/** the comfy panel; null when the repo declares no workflow module, which
|
|
87
|
+
* is also when the comfy section is absent from the page */
|
|
88
|
+
comfy: ComfyDeps | null
|
|
89
|
+
/** rule SK014's hits, grouped by word, over the config on disk: the data
|
|
90
|
+
* of the SK014 page. Absent = the endpoint answers 404 */
|
|
91
|
+
prose?: () => Promise<ProseView>
|
|
92
|
+
/** re-read repo.config.ts when its bytes moved. The switch WRITES that
|
|
93
|
+
* file, so every surface built on the config asks for this first, or a
|
|
94
|
+
* toggle reads as a toggle that did nothing until the next restart */
|
|
95
|
+
refresh?: () => Promise<void>
|
|
37
96
|
/** RED-only ping tick, called on an interval while serving (null = off) */
|
|
38
97
|
pingTick?: ((needs: NeedItem[]) => void) | null
|
|
39
98
|
}
|
|
40
|
-
export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; stop: () => void }
|
|
99
|
+
export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; liveReload: boolean; stop: () => void }
|
|
100
|
+
|
|
101
|
+
/** the tab refreshes ITSELF through bun's hot reload client, which rides in
|
|
102
|
+
* the served page: it swaps a hub-app edit in, and reconnects across the
|
|
103
|
+
* process restart a source edit triggers under `bun --watch`. Bun puts that
|
|
104
|
+
* client on the page only in development mode, which it derives from
|
|
105
|
+
* NODE_ENV at process start, so a production shell serves a page that can
|
|
106
|
+
* never refresh itself. Pinned against the real served bytes in
|
|
107
|
+
* tests/hub-server.test.ts, and run-serve turns a false into a WARN line:
|
|
108
|
+
* silent, this reads exactly like a hub that is simply broken */
|
|
109
|
+
export const canLiveReload = (): boolean => process.env.NODE_ENV !== 'production'
|
|
41
110
|
|
|
42
111
|
// the toolbox as data — stdout ok: lines and /hub.json render THIS list,
|
|
43
112
|
// so they can never disagree
|
|
44
113
|
export function hubTools(d: Pick<HubDeps, 'graphs'>): HubTool[] {
|
|
45
114
|
return [
|
|
46
|
-
{ path: '/', label: 'hub', desc: 'THE page — media · asks · creds · posts · health ·
|
|
115
|
+
{ path: '/', label: 'hub', desc: 'THE page — media · asks · creds · posts · health · wikis' },
|
|
47
116
|
...d.graphs.map((g) => ({ path: `/graph/${g.id}`, label: g.label, desc: g.title })),
|
|
48
117
|
]
|
|
49
118
|
}
|
|
50
119
|
|
|
51
120
|
// drill-down pages (graph canvases) get one fixed home link back to THE page
|
|
52
121
|
const HOME_LINK = `<style>.skhome{position:fixed;left:10px;top:10px;z-index:9999;font-size:17px;text-decoration:none;
|
|
53
|
-
background:#
|
|
122
|
+
background:#141619;border:1px solid #282c31;border-radius:3px;padding:5px 9px}</style><a class=skhome href="/" title="hub — THE page">⌂</a>`
|
|
54
123
|
const withHome = (html: string): string => html.replace(/<body([^>]*)>/, `<body$1>${HOME_LINK}`)
|
|
55
124
|
|
|
56
125
|
export function serveHub(d: HubDeps): Hub {
|
|
57
126
|
const tools = hubTools(d)
|
|
127
|
+
const projects = projectGutter({ root: d.root })
|
|
58
128
|
const server: ReturnType<typeof Bun.serve> = Bun.serve({
|
|
59
129
|
hostname: '127.0.0.1',
|
|
60
130
|
port: d.port,
|
|
@@ -62,13 +132,32 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
62
132
|
routes: { '/': hubIndex },
|
|
63
133
|
async fetch(req): Promise<Response> {
|
|
64
134
|
const path = new URL(req.url).pathname
|
|
135
|
+
if (!sameOrigin(req)) return new Response('refused: a request that changes something must come from the hub page', { status: 403 })
|
|
65
136
|
// identity probe: lets a second launch tell "same repo, another
|
|
66
137
|
// window" from "a DIFFERENT repo hashed onto my port"
|
|
67
|
-
|
|
138
|
+
// `root` rides with it so a page can put a file link into his
|
|
139
|
+
// editor: only the server knows where this repo sits on disk
|
|
140
|
+
if (path === '/hub.json') return Response.json({ hub: 'shipkit', name: d.name, root: d.root, tools })
|
|
141
|
+
if (path.startsWith('/api/projects')) return (await projects.handle(req, path)) ?? new Response('not found', { status: 404 })
|
|
142
|
+
// the three surfaces built on the config read the config on disk
|
|
143
|
+
if (path === '/api/needs.json' || path === '/api/resources.json' || path === '/api/checks.json' || path === '/api/collections.json') await d.refresh?.()
|
|
68
144
|
if (path === '/api/needs.json') {
|
|
69
145
|
const needs = await d.needs().catch((e: Error) => [{ collection: 'hub', id: 'hub:broken', text: `needs projection broke: ${e.message}`, severity: 'red' as const }])
|
|
70
146
|
return Response.json({ needs })
|
|
71
147
|
}
|
|
148
|
+
if (path === '/api/resources.json') {
|
|
149
|
+
// ONE flat list for the whole repo: the page groups it by
|
|
150
|
+
// path and by type, and a second endpoint would be a second
|
|
151
|
+
// answer to drift from this one
|
|
152
|
+
const cols = d.collections()
|
|
153
|
+
return Response.json({
|
|
154
|
+
collections: cols.map((c) => ({ id: c.id, title: c.title, media: c.media })),
|
|
155
|
+
resources: await projectResources(cols),
|
|
156
|
+
schedule: d.schedule?.state() ?? null,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
// the previous address, answered while pages opened before the
|
|
160
|
+
// tree are still on screen
|
|
72
161
|
if (path === '/api/collections.json') {
|
|
73
162
|
// browse-all: every instance of every collection, healthy
|
|
74
163
|
// included — failure-isolated per collection
|
|
@@ -77,7 +166,7 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
77
166
|
try {
|
|
78
167
|
out.push({ id: col.id, title: col.title, media: col.media, instances: await col.list() })
|
|
79
168
|
} catch (e) {
|
|
80
|
-
out.push({ id: col.id, title: col.title, media: col.media, instances: [{ id: `${col.id}:broken`, text: `collector broke: ${(e as Error).message}`, severity: 'red' }] })
|
|
169
|
+
out.push({ id: col.id, title: col.title, media: col.media, instances: [{ id: `${col.id}:broken`, type: 'health', path: `health/${col.id}-collector`, virtual: true, text: `collector broke: ${(e as Error).message}`, severity: 'red' }] })
|
|
81
170
|
}
|
|
82
171
|
}
|
|
83
172
|
return Response.json({ collections: out })
|
|
@@ -112,20 +201,124 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
112
201
|
return new Response((e as Error).message, { status: 400 })
|
|
113
202
|
}
|
|
114
203
|
}
|
|
204
|
+
if (path.startsWith('/api/comfy/')) {
|
|
205
|
+
// the panel is comfy-ts's, on its own port: the page embeds it,
|
|
206
|
+
// and these two answers are everything the hub knows about it
|
|
207
|
+
if (d.comfy == null) return new Response('this repo has no .shipkit/comfy/workflows module', { status: 400 })
|
|
208
|
+
if (path === '/api/comfy/panel' && req.method === 'POST') {
|
|
209
|
+
try {
|
|
210
|
+
// comfy-ts serve imports the modules at BOOT, so an
|
|
211
|
+
// edited workflow needs a new process, never a reload
|
|
212
|
+
const b = (await req.json().catch(() => ({}))) as { restart?: boolean }
|
|
213
|
+
return Response.json(b.restart === true ? await d.comfy.restart() : await d.comfy.ensure())
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return new Response((e as Error).message, { status: 400 })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (path === '/api/comfy/box') {
|
|
219
|
+
const box = d.comfy.box()
|
|
220
|
+
return Response.json({ ...box, up: await comfyBoxUp(box.url) })
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (path === '/api/live.json') {
|
|
224
|
+
if (d.live == null) return Response.json({ now: Date.now(), historyMs: 0, cap: 0, busy: 0, invocations: [], repos: [], runs: [], queued: 0, blocked: [] } satisfies LiveView)
|
|
225
|
+
return Response.json(d.live(), { headers: { 'cache-control': 'no-store' } })
|
|
226
|
+
}
|
|
227
|
+
if (path === '/api/repo-hub') {
|
|
228
|
+
const repo = new URL(req.url).searchParams.get('repo') ?? ''
|
|
229
|
+
return Response.json({ url: d.repoHub == null ? null : await d.repoHub(repo) })
|
|
230
|
+
}
|
|
231
|
+
if (path === '/api/timeline.json') {
|
|
232
|
+
if (d.timeline == null) return new Response('this hub keeps no run record', { status: 404 })
|
|
233
|
+
try {
|
|
234
|
+
return Response.json(await d.timeline())
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return new Response(`the run record could not be shaped: ${(e as Error).message}`, { status: 500 })
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (path === '/api/tests.json') {
|
|
240
|
+
if (d.tests == null) return new Response('this hub keeps no run record', { status: 404 })
|
|
241
|
+
await d.refresh?.()
|
|
242
|
+
try {
|
|
243
|
+
return Response.json(await d.tests(new URL(req.url).searchParams.get('check')), { headers: { 'cache-control': 'no-store' } })
|
|
244
|
+
} catch (e) {
|
|
245
|
+
return new Response(`the test times could not be shaped: ${(e as Error).message}`, { status: 500 })
|
|
246
|
+
}
|
|
247
|
+
}
|
|
115
248
|
if (path === '/api/status.json') {
|
|
116
|
-
const p = join(d.root,
|
|
249
|
+
const p = join(d.root, LAYOUT.status)
|
|
117
250
|
return Response.json({ statusMd: existsSync(p) ? await Bun.file(p).text() : null })
|
|
118
251
|
}
|
|
252
|
+
if (path === '/api/checks.json') {
|
|
253
|
+
// the SAME rows /api/resources.json carries, addressable on
|
|
254
|
+
// their own so an agent can curl them. One provider, two
|
|
255
|
+
// addresses, so the two can never disagree
|
|
256
|
+
const checks = d.collections().find((c) => c.id === 'checks')
|
|
257
|
+
if (checks == null) return Response.json({ checks: [] })
|
|
258
|
+
return Response.json({ checks: await checks.list() })
|
|
259
|
+
}
|
|
260
|
+
if (path === '/api/checks/when') {
|
|
261
|
+
// THE SWITCH. One endpoint for one writer: where a check runs
|
|
262
|
+
// and which suites are slow are the same managed block in
|
|
263
|
+
// repo.config.ts, and `expect` is the hash of the file the
|
|
264
|
+
// page last read, so a write can never land on top of an edit
|
|
265
|
+
// it never saw. The file is left DIRTY: committing it is his
|
|
266
|
+
if (req.method === 'GET') {
|
|
267
|
+
const b = await readGateBlock(d.root)
|
|
268
|
+
return Response.json({ ...b, dirty: configDirty(d.root) })
|
|
269
|
+
}
|
|
270
|
+
if (req.method === 'POST') {
|
|
271
|
+
try {
|
|
272
|
+
const body = (await req.json()) as GateEdit & { expect?: string }
|
|
273
|
+
const b = await writeGateBlock(d.root, { when: body.when, slow: body.slow, slowRemove: body.slowRemove, words: body.words }, { expect: body.expect })
|
|
274
|
+
await d.refresh?.()
|
|
275
|
+
return Response.json({ ...b, dirty: configDirty(d.root) })
|
|
276
|
+
} catch (e) {
|
|
277
|
+
return new Response((e as Error).message, { status: 400 })
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (path === '/api/prose.json' && d.prose != null) {
|
|
282
|
+
// every SK014 hit, grouped by word, noisiest first. The word
|
|
283
|
+
// lists are written through /api/checks/when like every other
|
|
284
|
+
// edit of repo.config.ts
|
|
285
|
+
await d.refresh?.()
|
|
286
|
+
try {
|
|
287
|
+
return Response.json(await d.prose())
|
|
288
|
+
} catch (e) {
|
|
289
|
+
return new Response(`the prose check could not run: ${(e as Error).message}`, { status: 500 })
|
|
290
|
+
}
|
|
291
|
+
}
|
|
119
292
|
if (path === '/api/check/run' && req.method === 'POST') {
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
293
|
+
const body = (await req.json().catch(() => ({}))) as { only?: string[]; gate?: boolean }
|
|
294
|
+
// NDJSON: one line per event, so the page colours a row the
|
|
295
|
+
// moment its check finishes instead of waiting for the wall of
|
|
296
|
+
// text a shelled run could only deliver whole
|
|
297
|
+
const enc = new TextEncoder()
|
|
298
|
+
const stream = new ReadableStream({
|
|
299
|
+
async start(ctrl) {
|
|
300
|
+
const send = (e: Record<string, unknown>): void => {
|
|
301
|
+
try { ctrl.enqueue(enc.encode(`${JSON.stringify(e)}\n`)) } catch { /* the tab went away mid-run */ }
|
|
302
|
+
}
|
|
303
|
+
let code = 1
|
|
304
|
+
try {
|
|
305
|
+
code = await d.runChecks({ line: (text) => send({ t: 'line', text }), check: (u) => send({ t: 'check', ...u }) }, { only: body.only, gate: body.gate })
|
|
306
|
+
} catch (e) {
|
|
307
|
+
send({ t: 'line', text: `FAIL: the run broke: ${(e as Error).message}` })
|
|
308
|
+
}
|
|
309
|
+
send({ t: 'done', code })
|
|
310
|
+
ctrl.close()
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
return new Response(stream, { headers: { 'content-type': 'application/x-ndjson; charset=utf-8', 'cache-control': 'no-store' } })
|
|
124
314
|
}
|
|
125
315
|
const page = (s: string): Response => new Response(withHome(s), { headers: { 'content-type': 'text/html; charset=utf-8' } })
|
|
126
316
|
for (const g of d.graphs) {
|
|
127
|
-
if (path
|
|
317
|
+
if (!path.startsWith(`/graph/${g.id}`)) continue
|
|
318
|
+
const routes = graphRoutes({ load: g.load, title: g.title, file: positionsFile(d.root, g.id), saveUrl: `/graph/${g.id}/positions` })
|
|
319
|
+
if (path === `/graph/${g.id}`) return page(routes.page())
|
|
128
320
|
if (path === `/graph/${g.id}/graph.json`) return Response.json(g.load())
|
|
321
|
+
if (path === `/graph/${g.id}/positions`) return routes.save(req)
|
|
129
322
|
}
|
|
130
323
|
// the media API namespace — the studio route table, ONE prefix strip
|
|
131
324
|
if (d.studio != null && path.startsWith('/media/')) return d.studio.handle(req, path.slice('/media'.length))
|
|
@@ -142,5 +335,7 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
142
335
|
tick()
|
|
143
336
|
pingTimer = setInterval(tick, 60_000)
|
|
144
337
|
}
|
|
145
|
-
|
|
338
|
+
// the panel process belongs to this hub: leaving it behind means the next
|
|
339
|
+
// launch adopts a server holding the previous run's modules
|
|
340
|
+
return { server, tools, liveReload: canLiveReload(), stop: () => { if (pingTimer != null) clearInterval(pingTimer); d.schedule?.stop(); d.comfy?.stop(); server.stop(true) } }
|
|
146
341
|
}
|
package/src/shipkit.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// rvlib-shipkit — barrel. One import per repo: defineRepo in repo.config.ts;
|
|
2
2
|
// everything else is the machinery behind the shipkit CLI.
|
|
3
|
-
export { defineRepo, loadRepoConfig, resolveFloor
|
|
3
|
+
export { defineRepo, loadRepoConfig, resolveFloor } from './repo-config.ts'
|
|
4
|
+
export { allTasks, composeChecks, gateInventory, gateRunnables, gateChecks } from './ledger/gate-checks.ts'
|
|
5
|
+
export type { GateCheck, CheckRow } from './ledger/gate-checks.ts'
|
|
6
|
+
// THE one shape every gate check has, and the file suffix that registers one
|
|
7
|
+
export { check } from './check.ts'
|
|
8
|
+
export { defineScan, mdPages, packageJson, type MdPage, type PackageJson, type Scan } from './scan.ts'
|
|
9
|
+
export type { CheckDef, CheckEnv, CheckFrom } from './check.ts'
|
|
10
|
+
export { discoverChecks, builtinCheckIds } from './core/discover-checks.ts'
|
|
11
|
+
export type { DiscoveredCheck } from './core/discover-checks.ts'
|
|
4
12
|
export type { RepoConfig, ServeConfig, FloorResolution, FloorSource } from './repo-config.ts'
|
|
5
13
|
export { contractsConfigFrom, activeContracts, applyContractsResult } from './contracts/apply-contracts.ts'
|
|
6
14
|
// the hub (`shipkit serve`): one server, one port, every tool a click away
|
|
@@ -12,24 +20,24 @@ export { projectNeeds, redCount } from './collections/collection-types.ts'
|
|
|
12
20
|
export type { Collection, Instance, NeedItem, NeedSeverity, CurationVerb } from './collections/collection-types.ts'
|
|
13
21
|
export { buildCollections } from './collections/providers.ts'
|
|
14
22
|
export { socialPostsCollection, listPosts } from './collections/posts.ts'
|
|
15
|
-
//
|
|
23
|
+
// units — the ONE discovery every release surface reads (drift
|
|
16
24
|
// probes, per-unit tests, per-unit typecheck, the stranded-tag probe)
|
|
17
25
|
export { resolveUnits, releaseOrder, unitById, renderTag, tagGlobs, prepublishScripts, unitPrepublishScripts } from './release/units.ts'
|
|
18
|
-
export type { ReleaseUnit,
|
|
26
|
+
export type { ReleaseUnit, UnitDecl, UnitResolution } from './release/units.ts'
|
|
19
27
|
export { deriveRemote, dirtyPaths, remoteTags, strandedTags, tagExistsLocally } from './release/git-facts.ts'
|
|
20
28
|
export { collectReports, renderReports, attentionLines } from './release/run-releases.ts'
|
|
21
29
|
export { extractChangelogSection, promoteUnreleased, hasUnreleasedNotes, hasVersionSection } from './release/changelog.ts'
|
|
22
30
|
export type { UnitReport } from './release/run-releases.ts'
|
|
23
|
-
export {
|
|
24
|
-
export type {
|
|
25
|
-
|
|
26
|
-
export
|
|
27
|
-
export {
|
|
28
|
-
export type {
|
|
31
|
+
export { discoverLegacyChecks } from './core/discover-legacy-checks.ts'
|
|
32
|
+
export type { LegacyCheck } from './core/discover-legacy-checks.ts'
|
|
33
|
+
// THE runner: one DAG over every check, whatever declared it
|
|
34
|
+
export { runChecks, selectChecks } from './core/check-runner.ts'
|
|
35
|
+
export type { RunnableCheck, CheckResult, CheckEvent, RunChecksOpts, RunChecksSummary } from './core/check-runner.ts'
|
|
36
|
+
export type { TaskDef } from './check.ts'
|
|
29
37
|
// tui: type-only here — the store/view/panels import ink+mobx, which must
|
|
30
38
|
// not load on every `shipkit ci`; runtime pieces live at rvlib-shipkit/src/tui/*
|
|
31
39
|
export type { TuiPanelDef, TuiRow, TuiRowStatus, MarkedLine } from './tui/tui-panel.ts'
|
|
32
|
-
export { CheckCtx, RateLimited,
|
|
40
|
+
export { CheckCtx, RateLimited, type ScanCtx } from './core/check-ctx.ts'
|
|
33
41
|
export { spawnWithTimeout } from './core/spawn-timeout.ts'
|
|
34
42
|
export type { SpawnOutcome, SpawnTimeoutOpts } from './core/spawn-timeout.ts'
|
|
35
43
|
export { writeStatus } from './core/write-status.ts'
|
|
@@ -38,7 +46,6 @@ export { formatLine, parseDuration, fmtStamp, parseWireOutput } from './core/wir
|
|
|
38
46
|
export type { Line } from './core/wire.ts'
|
|
39
47
|
// the SK rule registry — stable-id invariants `shipkit check` enforces;
|
|
40
48
|
// consumers (e.g. a wiki lint verifying mentioned ids exist) read RULES
|
|
41
|
-
export { RULES, ruleById } from './rules/rule-registry.ts'
|
|
42
49
|
export { tagRuleLines } from './rules/rule.ts'
|
|
43
50
|
export type { Rule, RuleEnv, FixResult } from './rules/rule.ts'
|
|
44
51
|
export { evaluateContracts, pngDims } from './contracts/contracts-engine.ts'
|
|
@@ -49,7 +56,7 @@ export { allContracts } from './contracts/contract-manifests.ts'
|
|
|
49
56
|
export { listAsks, openAsks, checkAsks, createAsk, renderAsksTable } from './asks/asks-queue.ts'
|
|
50
57
|
export type { Ask, CreateOpts } from './asks/asks-queue.ts'
|
|
51
58
|
// assets surface (folded in from the standalone atelier lib; that npm
|
|
52
|
-
// package is frozen). A repo's .
|
|
59
|
+
// package is frozen). A repo's .shipkit/assets/kit.ts imports defineKit and
|
|
53
60
|
// declares its kinds; `shipkit assets …` derives the rest.
|
|
54
61
|
export { defineKit, defineKind, comfy, canvas, manual, gemini, mediaOf, MEDIA_EXTS, geometryPanel, gridPanel, customPanel, validateKit, missingPanelScripts, loadKit, findAsset, allAssets, categoriesOf } from './assets/kit-config.ts'
|
|
55
62
|
export type { KitConfig, ResolvedKit, Kind, KindMeta, Media, AssetSpec, GeneratorSpec, ComfyGen, CanvasGen, ManualGen, GeminiGen, PanelSpec, GeometryPanelSpec, GridPanelSpec, CustomPanelSpec, FoundAsset, ShipContext, ShipFn } from './assets/kit-config.ts'
|
package/src/smoke/run-smoke.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// exercised in the field, by the run itself.
|
|
16
16
|
import { mkdirSync } from 'fs'
|
|
17
17
|
import { join } from 'path'
|
|
18
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
18
19
|
|
|
19
20
|
export type SmokeRoute = {
|
|
20
21
|
/** the path to visit, always leading `/` */
|
|
@@ -50,7 +51,7 @@ export type SmokeConfig = {
|
|
|
50
51
|
routes: (string | SmokeRoute)[]
|
|
51
52
|
/** absent: the run visits the routes anonymously */
|
|
52
53
|
login?: SmokeLogin
|
|
53
|
-
/** pause between routes, ms. A route change fires a burst of requests, and
|
|
54
|
+
/** pause between routes, ms, default 3000. A route change fires a burst of requests, and
|
|
54
55
|
* a host with a CONCURRENT cap sheds some of them without CORS headers,
|
|
55
56
|
* which then reads as a broken app. 3000 is measured against PocketHost's
|
|
56
57
|
* 15 concurrent per IP: 0 reported 8 of 12 routes, 3000 reports 12 of 12. */
|
|
@@ -66,7 +67,7 @@ const ROUTE_KEYS = new Set(['path', 'waitFor', 'open'])
|
|
|
66
67
|
export const DEFAULT_LOGIN_PATH = '/login'
|
|
67
68
|
|
|
68
69
|
export const DEFAULT_PACE_MS = 3000
|
|
69
|
-
export const DEFAULT_SHOTS =
|
|
70
|
+
export const DEFAULT_SHOTS = `${LAYOUT.tmp}/smoke`
|
|
70
71
|
|
|
71
72
|
/** the whole shape, refused loud on the first thing that is not so. A typo
|
|
72
73
|
* here would otherwise land as a run that visits nothing and exits 0. */
|
package/src/start-repo.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// `shipkit start` — ensure <root>/<name> exists and
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// `shipkit start` — ensure <root>/<name> exists and that CLAUDE.md carries
|
|
2
|
+
// its include lines (in order, at the top): the HOUSE lines below, unless
|
|
3
|
+
// --include overrides them or the house index is not on this disk, then
|
|
4
|
+
// `@./agent/index.md` whenever the repo has that file, because the scaffold
|
|
5
|
+
// writes it and SK003 fails a repo that has it unrouted. Plain module behind
|
|
6
|
+
// cmd_Start. Status lines go to STDERR —
|
|
7
|
+
// the ONLY stdout output is the resolved dir, so a sourced shell alias can
|
|
5
8
|
// `cd "$(shipkit start <name>)"` (the one thing a child process can't do
|
|
6
9
|
// for its parent shell).
|
|
7
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
@@ -9,18 +12,37 @@ import { homedir } from 'os'
|
|
|
9
12
|
import { join } from 'path'
|
|
10
13
|
import { ensureGitRepo, ensureIgnoreBaseline, firstGhLine, isScratch, publishRepo, runner, type Run } from './git-bootstrap.ts'
|
|
11
14
|
import { initRepo } from './init-repo.ts'
|
|
15
|
+
import { AGENT_LINE } from './checks/SK003-claude-md-shape.check.ts'
|
|
12
16
|
import { scaffoldProject, TOOLCHAIN } from './scaffold/project-scaffold.ts'
|
|
13
17
|
import { defaultHubPort } from './serve/hub-port.ts'
|
|
14
18
|
|
|
19
|
+
/** the one template: `shipkit start <name>` is the only thing anyone types,
|
|
20
|
+
* and a repo without the corvion wiki in its context is not a started repo.
|
|
21
|
+
* Base context first, the repo's own router last, so repo docs override.
|
|
22
|
+
* A public repo swaps line 1 for the gitignored `@./CLAUDE.local.md`,
|
|
23
|
+
* which is why that line satisfies the first include (SK003) */
|
|
24
|
+
export const HOUSE_INCLUDE = ['@~/dev/corvion/wiki/index.md', '@./agent/index.md']
|
|
25
|
+
export const HOUSE_ALSO_OK = ['@./CLAUDE.local.md']
|
|
26
|
+
/** the day-job checkout: never wired, never given an include */
|
|
27
|
+
export const HOUSE_SKIP = ['monoloco']
|
|
28
|
+
|
|
29
|
+
/** where the first house line points, `~` resolved against `home` */
|
|
30
|
+
export const housePath = (home: string): string => {
|
|
31
|
+
const p = HOUSE_INCLUDE[0]!.replace(/^@/, '')
|
|
32
|
+
return p.startsWith('~/') ? join(home, p.slice(2)) : p
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
export type StartOpts = {
|
|
16
36
|
/** parent dir for repos — default ~/dev */
|
|
17
37
|
root?: string
|
|
18
|
-
/** lines CLAUDE.md must carry, in order; omitted = only
|
|
38
|
+
/** lines CLAUDE.md must carry, in order; omitted = HOUSE_INCLUDE. An empty list means no house: only the agent route lands */
|
|
19
39
|
include?: string[]
|
|
20
|
-
/** alternative lines that satisfy the FIRST include — never double-add */
|
|
40
|
+
/** alternative lines that satisfy the FIRST include — never double-add; omitted = HOUSE_ALSO_OK */
|
|
21
41
|
alsoOk?: string[]
|
|
22
|
-
/** repo names
|
|
42
|
+
/** repo names shipkit never touches; omitted = HOUSE_SKIP */
|
|
23
43
|
skip?: string[]
|
|
44
|
+
/** the home dir the first house line resolves against; omitted = os.homedir() */
|
|
45
|
+
home?: string
|
|
24
46
|
/** true forces the github half, false refuses it. undefined = the default:
|
|
25
47
|
* a dir this run created, or an existing project, gets its private repo */
|
|
26
48
|
github?: boolean
|
|
@@ -36,38 +58,22 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
|
|
|
36
58
|
console.error(`FAIL: bad repo name '${name}' — letters/digits/._- only, no leading dot, no slashes`)
|
|
37
59
|
return 2
|
|
38
60
|
}
|
|
39
|
-
const
|
|
61
|
+
const home = opts.home ?? homedir()
|
|
62
|
+
const dir = join(opts.root ?? join(home, 'dev'), name)
|
|
40
63
|
const justCreated = !existsSync(dir)
|
|
41
64
|
if (justCreated) {
|
|
42
65
|
mkdirSync(dir, { recursive: true })
|
|
43
66
|
console.error(`ok: created ${dir}`)
|
|
44
67
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const cur = readFileSync(claudeMd, 'utf8')
|
|
55
|
-
const lineSet = new Set(cur.split('\n'))
|
|
56
|
-
const alt = opts.alsoOk ?? []
|
|
57
|
-
const satisfied = (w: string): boolean => lineSet.has(w) || (w === want[0] && alt.some((a) => lineSet.has(a)))
|
|
58
|
-
const missing = want.filter((w) => !satisfied(w))
|
|
59
|
-
if (missing.length > 0) {
|
|
60
|
-
// insert after any leading include/alt lines so order survives
|
|
61
|
-
const lines = cur.split('\n')
|
|
62
|
-
const known = new Set([...want, ...alt])
|
|
63
|
-
let at = 0
|
|
64
|
-
while (at < lines.length && known.has(lines[at]!)) at++
|
|
65
|
-
lines.splice(at, 0, ...missing)
|
|
66
|
-
writeFileSync(claudeMd, lines.join('\n'))
|
|
67
|
-
console.error(`ok: ${missing.length} missing include line(s) inserted into CLAUDE.md`)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
68
|
+
// flags win, else the house, and only on a machine that HAS the house:
|
|
69
|
+
// this lib is on npm, so elsewhere the template would be an include
|
|
70
|
+
// pointing at nothing plus the widened agent permissions riding on it
|
|
71
|
+
const houseOnDisk = existsSync(housePath(home))
|
|
72
|
+
const house = opts.include ?? (houseOnDisk ? HOUSE_INCLUDE : [])
|
|
73
|
+
if (opts.include == null && !houseOnDisk) console.error(`skip: no house wiki at ${housePath(home)} — CLAUDE.md gets the agent route only, and no .claude/ policy files`)
|
|
74
|
+
const alt = opts.alsoOk ?? HOUSE_ALSO_OK
|
|
75
|
+
const skipped = (opts.skip ?? HOUSE_SKIP).includes(name)
|
|
76
|
+
if (skipped && house.length > 0) console.error(`skip: ${name} is include-exempt — CLAUDE.md untouched`)
|
|
71
77
|
// start is the from-anywhere verb, and a STARTED repo comes out READY:
|
|
72
78
|
// any dir with no commit history gets a minimal package.json, a first
|
|
73
79
|
// src/index.ts, the rvlib-shipkit dev dep, repo.config.ts via init, git,
|
|
@@ -78,7 +84,7 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
|
|
|
78
84
|
// --skip means shipkit NEVER TOUCHES a listed repo (a day-job checkout
|
|
79
85
|
// belongs on that list), not just no CLAUDE.md. And the cd contract survives a
|
|
80
86
|
// failed adoption: the dir always prints, wiring trouble is a WARN.
|
|
81
|
-
if (!
|
|
87
|
+
if (!skipped) {
|
|
82
88
|
// git BEFORE the shipkit scaffold, and whether or not the dir is a
|
|
83
89
|
// project yet: an empty new dir is still work that must not live on
|
|
84
90
|
// one disk. The PUBLISH (first commit → private repo → push) is the
|
|
@@ -120,7 +126,7 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
|
|
|
120
126
|
// command line: init takes the caller's name over the manifest's,
|
|
121
127
|
// and a dir found holding a forked package.json carries a name
|
|
122
128
|
// nothing will listen on
|
|
123
|
-
try { scaffoldProject(dir, { name, port: defaultHubPort(name), print, claudePolicy:
|
|
129
|
+
try { scaffoldProject(dir, { name, port: defaultHubPort(name), print, claudePolicy: house.length > 0 }) }
|
|
124
130
|
catch (e) { console.error(`WARN scaffold incomplete in ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
|
|
125
131
|
// the wired scripts call node_modules binaries, so a scaffold
|
|
126
132
|
// whose scripts exit 127 is not a started repo. Skips mirror the
|
|
@@ -150,6 +156,12 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
|
|
|
150
156
|
catch (e) { console.error(`WARN could not wire ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
|
|
151
157
|
} else unscaffolded = true
|
|
152
158
|
}
|
|
159
|
+
// CLAUDE.md after the scaffold and before the publish: the agent/
|
|
160
|
+
// router the scaffold just wrote gets routed, LAST (include order is
|
|
161
|
+
// semantic, SK003), and the file rides inside the first commit
|
|
162
|
+
const want = [...house]
|
|
163
|
+
if (existsSync(join(dir, 'agent/index.md')) && !want.includes(AGENT_LINE)) want.push(AGENT_LINE)
|
|
164
|
+
if (want.length > 0) ensureIncludes(join(dir, 'CLAUDE.md'), want, alt)
|
|
153
165
|
if (wired) {
|
|
154
166
|
try { await publishRepo(dir, { name, github: opts.github, justCreated, gh: opts.gh, print }) }
|
|
155
167
|
catch (e) { console.error(`WARN could not publish ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
|
|
@@ -167,6 +179,33 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
|
|
|
167
179
|
return 0
|
|
168
180
|
}
|
|
169
181
|
|
|
182
|
+
/** CLAUDE.md carries every wanted line, in order, at the top. Idempotent:
|
|
183
|
+
* a line already there, or an `alt` line standing in for the first one, is
|
|
184
|
+
* never added twice */
|
|
185
|
+
function ensureIncludes(claudeMd: string, want: string[], alt: string[]): void {
|
|
186
|
+
if (!existsSync(claudeMd)) {
|
|
187
|
+
writeFileSync(claudeMd, `${want.join('\n')}\n`)
|
|
188
|
+
console.error(`ok: CLAUDE.md created with ${want.length} include line(s)`)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
const lines = readFileSync(claudeMd, 'utf8').split('\n')
|
|
192
|
+
const lineSet = new Set(lines)
|
|
193
|
+
const satisfied = (w: string): boolean => lineSet.has(w) || (w === want[0] && alt.some((a) => lineSet.has(a)))
|
|
194
|
+
const missing = want.filter((w) => !satisfied(w))
|
|
195
|
+
if (missing.length === 0) return
|
|
196
|
+
// insert after any leading include/alt lines so order survives, and
|
|
197
|
+
// BEFORE an agent route already there: a base line arriving later must
|
|
198
|
+
// not land under it and invert the order
|
|
199
|
+
const known = new Set([...want, ...alt])
|
|
200
|
+
let at = 0
|
|
201
|
+
while (at < lines.length && known.has(lines[at]!)) at++
|
|
202
|
+
const agentAt = lines.slice(0, at).indexOf(AGENT_LINE)
|
|
203
|
+
if (agentAt !== -1) at = agentAt
|
|
204
|
+
lines.splice(at, 0, ...missing)
|
|
205
|
+
writeFileSync(claudeMd, lines.join('\n'))
|
|
206
|
+
console.error(`ok: ${missing.length} missing include line(s) inserted into CLAUDE.md`)
|
|
207
|
+
}
|
|
208
|
+
|
|
170
209
|
/** what the closing line is allowed to say, all of it measured */
|
|
171
210
|
export type RepoFacts = { tracked: number; uncommitted: number; hasHead: boolean; origin: string | null; pushed: boolean }
|
|
172
211
|
|