rvlib-shipkit 2.1.1 → 2.3.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 +50 -0
- package/package.json +2 -1
- package/src/agent-time/mask.ts +43 -0
- package/src/agent-time/render.ts +187 -0
- package/src/agent-time/report.ts +335 -0
- package/src/agent-time/shape.ts +271 -0
- package/src/agent-time/signals.ts +84 -0
- package/src/agent-time/store.ts +379 -0
- package/src/agent-time/timeline.ts +218 -0
- package/src/agent-time/turns.ts +135 -0
- package/src/assets/kits/identity-kit.ts +7 -3
- package/src/assets/run-assets.ts +18 -4
- package/src/checks/SK004-gitignore-policy.check.ts +11 -6
- package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
- package/src/cli/cmd_Calls.ts +53 -0
- package/src/cli/cmd_Commands.ts +163 -43
- package/src/cli/cmd_Review.ts +948 -0
- package/src/cli/cmd_Shot.ts +63 -0
- package/src/cli/cmd_Start.ts +8 -4
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +361 -0
- package/src/cli/kit/builtins.ts +40 -0
- package/src/cli/kit/calls.ts +177 -0
- package/src/cli/kit/families.ts +67 -0
- package/src/cli/kit/index.ts +8 -0
- package/src/cli/kit/pages-check.ts +36 -0
- package/src/cli/kit/project-cli.ts +73 -0
- package/src/cli/kit/verb.ts +167 -0
- package/src/cli.ts +15 -2
- package/src/collections/collection-types.ts +6 -1
- package/src/collections/providers.ts +3 -0
- package/src/collections/review.ts +70 -0
- package/src/core/layout.ts +12 -2
- package/src/core/migrate-layout.ts +31 -8
- package/src/core/one-way-in-policy.ts +12 -4
- package/src/reflect/prompt-corpus.ts +15 -114
- package/src/reflect/transcript.ts +254 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +150 -0
- package/src/review/http.ts +23 -0
- package/src/review/marks.ts +43 -0
- package/src/review/media.ts +45 -0
- package/src/review/model.ts +854 -0
- package/src/review/project.ts +110 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +228 -0
- package/src/review/store.ts +1002 -0
- package/src/review/threads.ts +237 -0
- package/src/review/trailer-desk.ts +385 -0
- package/src/review/trailer-model.ts +183 -0
- package/src/review/trailer-routes.ts +136 -0
- package/src/review/unit.ts +165 -0
- package/src/review/watchers.ts +54 -0
- package/src/scaffold/cli-skeleton.ts +149 -0
- package/src/scaffold/files/agent-index.md +2 -2
- package/src/scaffold/files/tsconfig.json +3 -0
- package/src/scaffold/files/verify-skill.md +1 -1
- package/src/scaffold/project-scaffold.ts +90 -12
- package/src/serve/embed.ts +35 -0
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/activity.css +0 -4
- package/src/serve/hub-app/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +28 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +10 -2
- package/src/serve/hub-app/home-strip.tsx +3 -2
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +80 -11
- package/src/serve/hub-app/hub.css +45 -13
- package/src/serve/hub-app/icons.tsx +9 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/insights-view.tsx +4 -3
- package/src/serve/hub-app/keys.ts +14 -10
- package/src/serve/hub-app/layout-st.ts +63 -2
- package/src/serve/hub-app/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +22 -4
- package/src/serve/hub-app/project-list.tsx +41 -8
- package/src/serve/hub-app/projects-svc.ts +4 -0
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +45 -3
- package/src/serve/hub-app/review/clip-st.ts +208 -0
- package/src/serve/hub-app/review/kind-view.tsx +171 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +239 -0
- package/src/serve/hub-app/review/review-board.tsx +46 -0
- package/src/serve/hub-app/review/review-list.tsx +156 -0
- package/src/serve/hub-app/review/review-page.tsx +101 -0
- package/src/serve/hub-app/review/review-st.ts +779 -0
- package/src/serve/hub-app/review/review-views.tsx +503 -0
- package/src/serve/hub-app/review/trailer-page.tsx +416 -0
- package/src/serve/hub-app/review/trailer-st.ts +602 -0
- package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
- package/src/serve/hub-app/review.css +329 -0
- package/src/serve/hub-app/theme-st.ts +56 -0
- package/src/serve/hub-app/tree-build.ts +4 -0
- package/src/serve/hub-app/tree-row.tsx +1 -1
- package/src/serve/hub-app/tree-svc.ts +4 -0
- package/src/serve/hub-runtime.ts +5 -3
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +8 -0
- package/src/shipkit-files.ts +12 -4
- package/src/shot/client.ts +67 -0
- package/src/shot/clip-encoder.ts +146 -0
- package/src/shot/dom-clip.ts +124 -0
- package/src/shot/serve-shot.ts +54 -0
- package/src/shot/shooter.ts +343 -0
- package/src/shot/shot-ask.ts +189 -0
- package/src/shot/shot.shipkit.ts +14 -0
- package/src/smoke/run-smoke.ts +2 -2
- package/src/start-repo.ts +29 -14
- package/src/suite/judge-suites.ts +56 -10
- package/src/suite/select.ts +2 -5
- package/src/suite/suite-cli.ts +3 -2
- package/src/trailer/adapter.ts +78 -0
- package/src/trailer/build.ts +603 -0
- package/src/trailer/ffmpeg.ts +72 -0
- package/src/trailer/footage.ts +173 -0
- package/src/trailer/grammar.ts +1290 -0
- package/src/trailer/graph.ts +508 -0
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +31 -0
- package/src/trailer/look.ts +124 -0
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +192 -0
- package/src/trailer/prune.ts +193 -0
- package/src/trailer/score/compose.ts +240 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +91 -0
- package/src/trailer/score/score.ts +214 -0
- package/src/trailer/score/sound.ts +112 -0
- package/src/trailer/score/synth.ts +361 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +233 -0
- package/src/trailer/titles.ts +379 -0
- package/src/trailer/url.ts +108 -0
- package/src/trailer/vertical-draw.ts +286 -0
- package/src/trailer/vertical.ts +148 -0
- package/src/trailer/vocab.ts +59 -0
- package/src/trailer/words.ts +254 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// the project CLI a started repo gets on day one: `src/cli.ts` on the kit
|
|
2
|
+
// (cli/kit/), named for the project, one example family, and the package.json
|
|
3
|
+
// `cli` script plus a `bin` of the project's name. Pure text here; the scaffold
|
|
4
|
+
// writes it create-only, so a repo that already has any of it keeps its own.
|
|
5
|
+
import { Cli } from 'clipanion'
|
|
6
|
+
import { commandRows, type Definition, newCommandsPage, renderEssentials, renderMarkdown, spliceBlock } from '../cli/cmd_Commands.ts'
|
|
7
|
+
import { builtinCommands } from '../cli/kit/builtins.ts'
|
|
8
|
+
import { type Verb, verbs } from '../cli/kit/verb.ts'
|
|
9
|
+
|
|
10
|
+
/** the entry: the family table and nothing else. `absolute`: the repo's
|
|
11
|
+
* tsconfig maps `src/*`, the house import form; else a relative import */
|
|
12
|
+
export function cliEntry(name: string, absolute = true): string {
|
|
13
|
+
return `#!/usr/bin/env bun
|
|
14
|
+
// ${name}: the project CLI, one command for every act an agent or a human does in this repo.
|
|
15
|
+
// A family is src/cli/cmd_<Family>.ts exporting \`commands\`; the table names the first words
|
|
16
|
+
// each family owns, so a call loads its family alone. \`${name}\` lists every command.
|
|
17
|
+
import { resolve } from 'path'
|
|
18
|
+
import { runProjectCli } from 'rvlib-shipkit/cli-kit'
|
|
19
|
+
|
|
20
|
+
await runProjectCli({
|
|
21
|
+
name: '${name}',
|
|
22
|
+
root: resolve(import.meta.dir, '..'),
|
|
23
|
+
families: [
|
|
24
|
+
{ roots: ['dev', 'shot'], load: () => import('${absolute ? 'src/cli/cmd_Dev.ts' : './cli/cmd_Dev.ts'}') },
|
|
25
|
+
],
|
|
26
|
+
})
|
|
27
|
+
`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** the example family's live row, as data: the source text and the day-one
|
|
31
|
+
* pages are both made from it, so they cannot disagree */
|
|
32
|
+
export function exampleRow(name: string, devScript = false): Verb {
|
|
33
|
+
return {
|
|
34
|
+
path: 'dev',
|
|
35
|
+
category: 'Run',
|
|
36
|
+
description: devScript ? 'the dev process (package.json dev)' : 'the dev process: src/index.ts, restarted on save',
|
|
37
|
+
examples: [['start it', `${name} dev`]],
|
|
38
|
+
run: devScript ? { cmd: ['bun', 'run', 'dev'] } : { cmd: ['bun', '--watch', 'src/index.ts'] },
|
|
39
|
+
essential: {},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const q = (s: string): string => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
44
|
+
|
|
45
|
+
/** the example family: a row over code that exists, and the shot row a visual
|
|
46
|
+
* project uncomments once it has a renderer */
|
|
47
|
+
export function exampleFamily(name: string, devScript = false): string {
|
|
48
|
+
const v = exampleRow(name, devScript)
|
|
49
|
+
const run = 'cmd' in v.run ? `{ cmd: [${v.run.cmd.map(q).join(', ')}] }` : ''
|
|
50
|
+
return `// a command over code that exists is a row: run is { bun: <script> }, { cmd: [...] }
|
|
51
|
+
// or { tool, cold } (a warm host, else the cold script). Mark an everyday row
|
|
52
|
+
// \`essential: {}\`. \`sk ci\` rewrites the agent pages (cli-pages.check.shipkit.ts), or: ${pagesCommand(name)}
|
|
53
|
+
import { verbs } from 'rvlib-shipkit/cli-kit'
|
|
54
|
+
|
|
55
|
+
export const commands = verbs([
|
|
56
|
+
{
|
|
57
|
+
path: ${q(v.path)},
|
|
58
|
+
category: ${q(v.category)},
|
|
59
|
+
description: ${q(v.description)},
|
|
60
|
+
examples: [[${v.examples[0]!.map(q).join(', ')}]],
|
|
61
|
+
run: ${run},
|
|
62
|
+
essential: {},
|
|
63
|
+
},
|
|
64
|
+
// a visual project: any state as a png, the same command filmed by a trailer line.
|
|
65
|
+
// film: 'still' means the script takes --out <path>; 'clip' also takes --seconds <n>.
|
|
66
|
+
// {
|
|
67
|
+
// path: 'shot',
|
|
68
|
+
// category: 'Look',
|
|
69
|
+
// description: 'any state of the app as a png',
|
|
70
|
+
// examples: [['the home screen', '${name} shot home --out shot.png']],
|
|
71
|
+
// run: { bun: 'scripts/shot.ts' },
|
|
72
|
+
// film: 'still',
|
|
73
|
+
// },
|
|
74
|
+
])
|
|
75
|
+
`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** where a project CLI's two agent pages live */
|
|
79
|
+
export const cliPages = (name: string): { all: string; essentials: string } => ({ all: `agent/${name}.md`, essentials: `agent/${name}-essentials.md` })
|
|
80
|
+
|
|
81
|
+
/** the command that writes both pages */
|
|
82
|
+
export const pagesCommand = (name: string): string => `${name} commands --out ${cliPages(name).all} --essentials ${cliPages(name).essentials}`
|
|
83
|
+
|
|
84
|
+
/** the day-one pages, exactly as \`<name> commands --out … --essentials …\` writes them */
|
|
85
|
+
export function skeletonPages(name: string, devScript = false): { all: string; essentials: string } {
|
|
86
|
+
const cli = new Cli({ binaryName: name })
|
|
87
|
+
const builtins = builtinCommands(name)
|
|
88
|
+
const classes = [...verbs([exampleRow(name, devScript)]), ...builtins.docs, ...builtins.calls]
|
|
89
|
+
for (const c of classes) cli.register(c)
|
|
90
|
+
const defs = cli.definitions() as Definition[]
|
|
91
|
+
const rows = commandRows(defs, (cli.definitions as (o?: { detailed?: boolean }) => Definition[])({ detailed: true }), name, classes)
|
|
92
|
+
return {
|
|
93
|
+
all: spliceBlock(newCommandsPage(name, cliPages(name).all), renderMarkdown(rows, name), name)!,
|
|
94
|
+
essentials: renderEssentials(rows, name, pagesCommand(name)),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** the package.json keys the project CLI needs, only where the key is free.
|
|
99
|
+
* Returns the keys it filled */
|
|
100
|
+
export function addCliKeys(pkg: { scripts?: Record<string, string>; bin?: Record<string, string> | string }, name: string): string[] {
|
|
101
|
+
const filled: string[] = []
|
|
102
|
+
pkg.scripts ??= {}
|
|
103
|
+
if (pkg.scripts.cli == null) { pkg.scripts.cli = 'bun src/cli.ts'; filled.push('scripts.cli') }
|
|
104
|
+
if (pkg.bin == null) { pkg.bin = { [name]: './src/cli.ts' }; filled.push(`bin.${name}`) }
|
|
105
|
+
return filled
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** what says a repo already has a CLI, its own or its product's: a `cli`
|
|
109
|
+
* script, any `bin`, any script that runs a cli.ts or cli.js of the repo, a
|
|
110
|
+
* src/cli.ts or a src/cli/ folder. null = the skeleton has room */
|
|
111
|
+
export function existingCli(pkg: { scripts?: Record<string, string>; bin?: unknown } | null, files: { entry: boolean; cliDir: boolean }): string | null {
|
|
112
|
+
if (pkg?.scripts?.cli != null) return `package.json scripts.cli runs \`${pkg.scripts.cli}\``
|
|
113
|
+
if (pkg?.bin != null) return `package.json has a bin (${typeof pkg.bin === 'string' ? pkg.bin : Object.keys(pkg.bin as object).join(', ')})`
|
|
114
|
+
// a script running an installed tool's cli (the house `hub` runs shipkit's) is not the repo's own
|
|
115
|
+
const runner = Object.entries(pkg?.scripts ?? {}).find(([, body]) => body.split(/\s+/).some((w) => /\bcli\.(ts|js)$/.test(w) && !/node_modules\/|rvlib-shipkit/.test(w)))
|
|
116
|
+
if (runner != null) return `package.json scripts.${runner[0]} runs \`${runner[1]}\``
|
|
117
|
+
if (files.entry) return 'src/cli.ts exists'
|
|
118
|
+
if (files.cliDir) return 'src/cli/ exists'
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** whether an installed rvlib-shipkit carries the kit: null = none installed */
|
|
123
|
+
export function kitInstalled(installedPkg: { exports?: Record<string, unknown> } | null): boolean | null {
|
|
124
|
+
if (installedPkg == null) return null
|
|
125
|
+
return installedPkg.exports?.['./cli-kit'] != null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** the check file that keeps both agent pages equal to the registry: the gate
|
|
129
|
+
* fails a stale page and its fix pass writes it again */
|
|
130
|
+
export function pagesCheckFile(name: string): string {
|
|
131
|
+
return `// agent/${name}.md and agent/${name}-essentials.md list the ${name} commands as the registry has them.
|
|
132
|
+
// A stale page sends an agent at a command that does not exist; the gate's fix pass writes both again
|
|
133
|
+
import { check } from 'rvlib-shipkit/src/check.ts'
|
|
134
|
+
import { cliPagesCheck } from 'rvlib-shipkit/src/cli/kit/pages-check.ts'
|
|
135
|
+
|
|
136
|
+
const pages = cliPagesCheck('${name}')
|
|
137
|
+
|
|
138
|
+
export default check({
|
|
139
|
+
description: pages.description,
|
|
140
|
+
blocking: true,
|
|
141
|
+
runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
|
|
142
|
+
cache: { by: 'files', inputs: pages.inputs, key: null },
|
|
143
|
+
fix: 'ci',
|
|
144
|
+
applies: 'always',
|
|
145
|
+
run: pages.run,
|
|
146
|
+
repair: pages.repair,
|
|
147
|
+
})
|
|
148
|
+
`
|
|
149
|
+
}
|
|
@@ -12,14 +12,14 @@ BEFORE the change it describes.
|
|
|
12
12
|
|
|
13
13
|
**Always loaded every session:**
|
|
14
14
|
|
|
15
|
-
@coding.md
|
|
15
|
+
@coding.md{{CLI_INCLUDE}}
|
|
16
16
|
|
|
17
17
|
Read the matching page before touching a surface:
|
|
18
18
|
|
|
19
19
|
| read when touching… | page |
|
|
20
20
|
|---|---|
|
|
21
21
|
| any source file | `agent/coding.md` |
|
|
22
|
-
| recent changes and why | `agent/changelog.md` |
|
|
22
|
+
| recent changes and why | `agent/changelog.md` |{{CLI_ROW}}
|
|
23
23
|
|
|
24
24
|
Grow this table: a subsystem, a service, a runbook each earn their own page,
|
|
25
25
|
cross-linked from here.
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"scripts/*": ["./scripts/*"]
|
|
24
24
|
},
|
|
25
25
|
|
|
26
|
+
// no @types package loads unless named here: bun's globals (Bun, import.meta.dir)
|
|
27
|
+
"types": ["bun"],
|
|
28
|
+
|
|
26
29
|
// every file is a module even with no import/export (standalone scripts)
|
|
27
30
|
"moduleDetection": "force",
|
|
28
31
|
|
|
@@ -12,7 +12,7 @@ description: Prove a change in THIS project actually works — drive the real fl
|
|
|
12
12
|
## The rules (house-wide)
|
|
13
13
|
- Failing repro test BEFORE the fix. Green checks ≠ verification.
|
|
14
14
|
- Logic → fast headless bun tests, run through `sk ci --only test` (tiny ~10-line test files reusing fixtures). While you fix one bug, `bun run test:raw <test file>` is the narrow way in, and one test by name is `bun run test:raw -- -t "<name>"`.
|
|
15
|
-
- Look & feel →
|
|
15
|
+
- Look & feel → your own screenshot is a check before you report, never the verdict; the repo owner playtests it.
|
|
16
16
|
- Verify as the end user would: drive the real flow, observe the real signal.
|
|
17
17
|
|
|
18
18
|
## This project
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
14
14
|
import { dirname, join } from 'path'
|
|
15
15
|
import { defaultHubPort } from '../serve/hub-port.ts'
|
|
16
|
+
import { addCliKeys, cliEntry, cliPages, exampleFamily, existingCli, kitInstalled, pagesCheckFile, skeletonPages } from './cli-skeleton.ts'
|
|
16
17
|
|
|
17
18
|
const FILES = join(import.meta.dir, 'files')
|
|
18
19
|
|
|
@@ -33,6 +34,8 @@ export type ScaffoldOpts = {
|
|
|
33
34
|
* a caller that declares a house (start seeds include lines) turns them
|
|
34
35
|
* on, and the grant is printed the one time it lands */
|
|
35
36
|
claudePolicy?: boolean
|
|
37
|
+
/** false: write no project CLI (the installed rvlib-shipkit predates the kit) */
|
|
38
|
+
projectCli?: boolean
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/** every dep the house toolchain needs for the scripts below to run. The
|
|
@@ -110,16 +113,20 @@ export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
|
110
113
|
// agent index advertising a port nothing listens on, so the caller that
|
|
111
114
|
// knows the name it is about to wire passes the port it resolves
|
|
112
115
|
const port = opts.port ?? defaultHubPort(packageName(root) ?? opts.name)
|
|
113
|
-
const vars = {
|
|
114
|
-
NAME: opts.name,
|
|
115
|
-
PORT: String(port),
|
|
116
|
-
DATE: opts.today ?? new Date().toISOString().slice(0, 10),
|
|
117
|
-
}
|
|
118
116
|
const written: string[] = []
|
|
119
117
|
const skipped: string[] = []
|
|
120
118
|
const write = (rel: string, body: string): void => { (put(root, rel, body) ? written : skipped).push(rel) }
|
|
121
119
|
|
|
122
120
|
write('src/index.ts', 'export {}\n')
|
|
121
|
+
// the project CLI on the kit, unless the dir already carries one, and its
|
|
122
|
+
// essentials page loaded by the agent index
|
|
123
|
+
const cliKeys = opts.projectCli !== false && writeCli(root, opts.name, write, p)
|
|
124
|
+
const vars = {
|
|
125
|
+
NAME: opts.name,
|
|
126
|
+
PORT: String(port),
|
|
127
|
+
DATE: opts.today ?? new Date().toISOString().slice(0, 10),
|
|
128
|
+
...cliIndexVars(opts.name, cliKeys),
|
|
129
|
+
}
|
|
123
130
|
write('tsconfig.json', template('tsconfig.json', vars))
|
|
124
131
|
write('bunfig.toml', template('bunfig.toml', vars))
|
|
125
132
|
// settings.json only here: it turns eslint off and keeps tsserver from
|
|
@@ -128,7 +135,7 @@ export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
|
128
135
|
write('.claude/skills/verify/SKILL.md', template('verify-skill.md', vars))
|
|
129
136
|
writeHouseFiles(write, vars, opts.claudePolicy === true, written, p)
|
|
130
137
|
|
|
131
|
-
addScripts(root, written, p)
|
|
138
|
+
addScripts(root, written, p, cliKeys ? opts.name : null)
|
|
132
139
|
addIgnoreLines(root, written)
|
|
133
140
|
|
|
134
141
|
if (written.length > 0) p(`ok: scaffolded ${written.length} file(s): ${written.slice(0, 6).join(', ')}${written.length > 6 ? `, +${written.length - 6} more` : ''}`)
|
|
@@ -166,9 +173,9 @@ function writeHouseFiles(write: (rel: string, body: string) => void, vars: Recor
|
|
|
166
173
|
* include lines pull it into the house contract, so it gets the house half
|
|
167
174
|
* of the day-one shape and nothing that says what the repo IS. Create-only,
|
|
168
175
|
* so every file the repo already holds wins. Returns the paths it wrote */
|
|
169
|
-
export function adoptHouse(root: string, opts: { name: string; today?: string; print?: (line: string) => void }): string[] {
|
|
176
|
+
export function adoptHouse(root: string, opts: { name: string; today?: string; print?: (line: string) => void; cli?: boolean }): string[] {
|
|
170
177
|
const p = opts.print ?? console.log
|
|
171
|
-
const vars = { NAME: opts.name, PORT: String(defaultHubPort(opts.name)), DATE: opts.today ?? new Date().toISOString().slice(0, 10) }
|
|
178
|
+
const vars = { NAME: opts.name, PORT: String(defaultHubPort(opts.name)), DATE: opts.today ?? new Date().toISOString().slice(0, 10), ...cliIndexVars(opts.name, opts.cli === true) }
|
|
172
179
|
const written: string[] = []
|
|
173
180
|
writeHouseFiles((rel, body) => { if (put(root, rel, body)) written.push(rel) }, vars, true, written, p)
|
|
174
181
|
if (written.length > 0) p(`ok: adopted into the house, ${written.length} file(s) seeded: ${written.join(', ')}`)
|
|
@@ -193,8 +200,78 @@ What broke, which page or lib, what you expected. Gitignored on purpose,
|
|
|
193
200
|
transient by design: it is harvested, then deleted.
|
|
194
201
|
`
|
|
195
202
|
|
|
196
|
-
/** the
|
|
197
|
-
|
|
203
|
+
/** the project CLI skeleton (cli-skeleton.ts): the entry, the example family,
|
|
204
|
+
* and whether package.json still needs its `cli` and `bin` keys. A dir that
|
|
205
|
+
* already has a project CLI keeps it alone */
|
|
206
|
+
function writeCli(root: string, name: string, write: (rel: string, body: string) => void, p: (line: string) => void): boolean {
|
|
207
|
+
const pkg = readPackage(root)
|
|
208
|
+
const found = existingCli(pkg, cliFiles(root))
|
|
209
|
+
if (found != null) { p(`skip: project CLI, ${found}`); return false }
|
|
210
|
+
writeSkeleton(root, name, pkg?.scripts?.dev != null, write)
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const cliFiles = (root: string): { entry: boolean; cliDir: boolean } => ({ entry: existsSync(join(root, 'src/cli.ts')), cliDir: existsSync(join(root, 'src/cli')) })
|
|
215
|
+
|
|
216
|
+
/** the skeleton's five files: the entry, the example family, its two pages
|
|
217
|
+
* and the check that keeps them current. The entry imports its families
|
|
218
|
+
* the house way when tsconfig maps src/, relatively otherwise */
|
|
219
|
+
function writeSkeleton(root: string, name: string, devScript: boolean, write: (rel: string, body: string) => void): void {
|
|
220
|
+
const pages = skeletonPages(name, devScript)
|
|
221
|
+
let absolute = true
|
|
222
|
+
try { absolute = readFileSync(join(root, 'tsconfig.json'), 'utf8').includes('"src/*"') } catch { absolute = false }
|
|
223
|
+
write('src/cli.ts', cliEntry(name, absolute))
|
|
224
|
+
write('src/cli/cmd_Dev.ts', exampleFamily(name, devScript))
|
|
225
|
+
write('src/cli/cli-pages.check.shipkit.ts', pagesCheckFile(name))
|
|
226
|
+
write(cliPages(name).all, pages.all)
|
|
227
|
+
write(cliPages(name).essentials, pages.essentials)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** the installed rvlib-shipkit's manifest, null when none is installed */
|
|
231
|
+
export function installedLib(root: string): { version?: string; exports?: Record<string, unknown> } | null {
|
|
232
|
+
try { return JSON.parse(readFileSync(join(root, 'node_modules/rvlib-shipkit/package.json'), 'utf8')) as { version?: string; exports?: Record<string, unknown> } }
|
|
233
|
+
catch { return null }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** the agent index lines that load the essentials page and list the full one */
|
|
237
|
+
const cliIndexVars = (name: string, cli: boolean): { CLI_INCLUDE: string; CLI_ROW: string } => ({
|
|
238
|
+
CLI_INCLUDE: cli ? `\n@${name}-essentials.md` : '',
|
|
239
|
+
CLI_ROW: cli ? `\n| every command of the project CLI | \`${cliPages(name).all}\` |` : '',
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
/** an ADOPTED repo asking for the project CLI (`sk start <name> --cli`): the
|
|
243
|
+
* skeleton and its package.json keys, refused when the repo already has a
|
|
244
|
+
* CLI of its own, because a second one splits the one door. Returns the
|
|
245
|
+
* paths it wrote, null on a refusal */
|
|
246
|
+
export function adoptCli(root: string, opts: { name: string; print?: (line: string) => void }): string[] | null {
|
|
247
|
+
const p = opts.print ?? console.log
|
|
248
|
+
const pkg = readPackage(root)
|
|
249
|
+
if (pkg == null) { p(`FAIL: no readable package.json in ${root} — the project CLI needs one`); return null }
|
|
250
|
+
const found = existingCli(pkg, cliFiles(root))
|
|
251
|
+
if (found != null) { p(`skip: ${opts.name} already has a CLI (${found}) — extend it; a project CLI beside a product's bin is wired by hand (build/cli-tool.md)`); return null }
|
|
252
|
+
const lib = installedLib(root)
|
|
253
|
+
if (kitInstalled(lib) === false) { p(`skip: the installed rvlib-shipkit ${lib?.version ?? ''} predates the CLI kit — bun add -d rvlib-shipkit@latest once a release carries it, then re-run`); return null }
|
|
254
|
+
const written: string[] = []
|
|
255
|
+
const write = (rel: string, body: string): void => { if (put(root, rel, body)) written.push(rel) }
|
|
256
|
+
writeSkeleton(root, opts.name, pkg.scripts?.dev != null, write)
|
|
257
|
+
addScripts(root, written, p, opts.name, false)
|
|
258
|
+
const index = join(root, 'agent/index.md')
|
|
259
|
+
const include = `@${opts.name}-essentials.md`
|
|
260
|
+
if (existsSync(index) && !readFileSync(index, 'utf8').split('\n').includes(include)) p(`next: in agent/index.md, load the everyday commands with the line ${include} and list \`${cliPages(opts.name).all}\` in its table`)
|
|
261
|
+
const deps = { ...(pkg as { dependencies?: object }).dependencies, ...(pkg as { devDependencies?: object }).devDependencies }
|
|
262
|
+
p(`ok: project CLI written: ${written.join(', ')} — run \`bun run cli\``)
|
|
263
|
+
if (!('rvlib-shipkit' in deps)) p('next: bun add -d rvlib-shipkit (the CLI imports rvlib-shipkit/cli-kit)')
|
|
264
|
+
return written
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readPackage(root: string): { name?: string; scripts?: Record<string, string> } | null {
|
|
268
|
+
try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { name?: string; scripts?: Record<string, string> } }
|
|
269
|
+
catch { return null }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** the baseline scripts, added only where the key is free, plus the project
|
|
273
|
+
* CLI's `cli` and `bin` keys when the skeleton was written */
|
|
274
|
+
function addScripts(root: string, written: string[], p: (line: string) => void, cliName: string | null = null, baseline = true): void {
|
|
198
275
|
const path = join(root, 'package.json')
|
|
199
276
|
if (!existsSync(path)) return void p('WARN no package.json here — the typecheck script was not written')
|
|
200
277
|
const raw = readFileSync(path, 'utf8')
|
|
@@ -203,11 +280,12 @@ function addScripts(root: string, written: string[], p: (line: string) => void):
|
|
|
203
280
|
catch (e) { return void p(`WARN package.json is unparseable (${String(e).slice(0, 80)}) — the typecheck script was not written, repair it then re-run`) }
|
|
204
281
|
pkg.scripts ??= {}
|
|
205
282
|
let added = 0
|
|
206
|
-
for (const [k, v] of Object.entries(SCRIPTS)) if (pkg.scripts[k] == null) { pkg.scripts[k] = v; added++ }
|
|
283
|
+
if (baseline) for (const [k, v] of Object.entries(SCRIPTS)) if (pkg.scripts[k] == null) { pkg.scripts[k] = v; added++ }
|
|
284
|
+
if (cliName != null) added += addCliKeys(pkg, cliName).length
|
|
207
285
|
if (added === 0) return
|
|
208
286
|
const indent = raw.match(/^(\s+)"/m)?.[1] ?? ' '
|
|
209
287
|
writeFileSync(path, JSON.stringify(pkg, null, indent) + '\n')
|
|
210
|
-
written.push(`package.json (${added}
|
|
288
|
+
written.push(`package.json (${added} key(s))`)
|
|
211
289
|
}
|
|
212
290
|
|
|
213
291
|
function packageName(root: string): string | null {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// a repo's review or trailer page alone, for another tool to frame (wiki build/shipkit/review.md, The review page):
|
|
2
|
+
// `<base>/embed/?page=review` serves the same app bundled with RELATIVE asset paths, so the page and every request it
|
|
3
|
+
// makes work under any prefix a proxy puts in front of the hub (pkm-multi's admin frames it under its own origin). The
|
|
4
|
+
// bundle is made once a process, in memory
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const INDEX = join(import.meta.dir, 'hub-app', 'index.html')
|
|
8
|
+
|
|
9
|
+
let bundle: Promise<Map<string, Blob>> | null = null
|
|
10
|
+
|
|
11
|
+
async function build(): Promise<Map<string, Blob>> {
|
|
12
|
+
const r = await Bun.build({ entrypoints: [INDEX], publicPath: './', target: 'browser', minify: true })
|
|
13
|
+
if (!r.success) throw new Error(r.logs.map(String).join('\n'))
|
|
14
|
+
return new Map(r.outputs.map((o) => [o.path.split('/').pop() ?? o.path, o]))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** the embed page and its assets under `<base>/embed/`; null for any other path */
|
|
18
|
+
export async function embedRoute(path: string): Promise<Response | null> {
|
|
19
|
+
if (path === '/embed') return new Response(null, { status: 308, headers: { location: 'embed/' } })
|
|
20
|
+
if (!path.startsWith('/embed/')) return null
|
|
21
|
+
bundle ??= build().catch((e: Error) => {
|
|
22
|
+
bundle = null
|
|
23
|
+
throw e
|
|
24
|
+
})
|
|
25
|
+
let files: Map<string, Blob>
|
|
26
|
+
try {
|
|
27
|
+
files = await bundle
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return new Response(`FAIL: the embed page does not build: ${(e as Error).message}`, { status: 500 })
|
|
30
|
+
}
|
|
31
|
+
const name = path.slice('/embed/'.length) || 'index.html'
|
|
32
|
+
const f = files.get(name)
|
|
33
|
+
if (!f) return new Response(`no ${name} in the embed page`, { status: 404 })
|
|
34
|
+
return new Response(f, { headers: { 'content-type': f.type || 'application/octet-stream', 'cache-control': name === 'index.html' ? 'no-cache' : 'private, max-age=86400' } })
|
|
35
|
+
}
|
|
@@ -32,7 +32,7 @@ const RepoName = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
|
32
32
|
|
|
33
33
|
/** a bar of exclusive buttons */
|
|
34
34
|
const Bar = <T extends number>({ label, values, value, words, set }: { label: string; values: readonly T[]; value: number; words: (v: T) => string; set: (v: T) => void }) => (
|
|
35
|
-
<div className="
|
|
35
|
+
<div className="seg" role="radiogroup" aria-label={label}>
|
|
36
36
|
{values.map((v) => (
|
|
37
37
|
<button key={v} role="radio" aria-checked={value === v} className={value === v ? 'on' : ''} onClick={() => set(v)}>{words(v)}</button>
|
|
38
38
|
))}
|
|
@@ -4,10 +4,6 @@
|
|
|
4
4
|
on main, orange elsewhere; a commit orange until pushed; dirt red. */
|
|
5
5
|
.activity{font-size:13px;container-type:inline-size}
|
|
6
6
|
.actop{display:flex;align-items:center;flex-wrap:wrap;gap:var(--s3);margin-bottom:var(--s3)}
|
|
7
|
-
.acwin{display:inline-flex;border:1px solid var(--line);border-radius:4px;overflow:hidden}
|
|
8
|
-
.acwin button{border:0;border-radius:0;padding:var(--s0) var(--s2);background:none;color:var(--dim);font-size:12px}
|
|
9
|
-
.acwin button:hover{color:var(--ink);background:var(--panel2)}
|
|
10
|
-
.acwin button.on{color:var(--accent);background:var(--sel)}
|
|
11
7
|
.accap{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px}
|
|
12
8
|
.accap input{width:44px;height:22px;box-sizing:border-box;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s0) var(--s1);font:inherit;font-variant-numeric:tabular-nums}
|
|
13
9
|
.accap input:focus{outline:none;border-color:var(--accent)}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// then the status as small pills that fold into +N rather than wrap. The
|
|
5
5
|
// state is dock-st.ts; the height is the page's panel (hub-app.tsx).
|
|
6
6
|
import { observer } from 'mobx-react-lite'
|
|
7
|
-
import { useEffect, useState } from 'react'
|
|
7
|
+
import { useEffect, useState, type CSSProperties } from 'react'
|
|
8
|
+
import { idVars } from './projects-svc.ts'
|
|
8
9
|
import { Tip } from './floating.tsx'
|
|
9
10
|
import { Icon, ICON } from './icons.tsx'
|
|
10
11
|
import type { HomeSt } from './home-st.ts'
|
|
@@ -57,8 +58,8 @@ const GroupLabel = observer(({ app, g }: { app: HomeSt; g: PlacedGroup }) => {
|
|
|
57
58
|
return (
|
|
58
59
|
<button className="dgroup" onClick={() => app.dock.openRepo(g.key)}
|
|
59
60
|
title={`${g.key}: open the repo${g.hidden > 0 ? `\n${g.hidden} check run(s) did not fit in its lanes` : ''}`}>
|
|
60
|
-
{p?.icon != null ? <img src={p.icon} alt="" width={12} height={12} draggable={false} /> : <i className="dgico" style={
|
|
61
|
-
<span style={
|
|
61
|
+
{p?.icon != null ? <img src={p.icon} alt="" width={12} height={12} draggable={false} /> : <i className="dgico idb" style={idVars(id) as CSSProperties} />}
|
|
62
|
+
<span className="idc" style={idVars(id) as CSSProperties}>{g.key}</span>
|
|
62
63
|
{/* slim folds its lanes on purpose: the count is news only open */}
|
|
63
64
|
{g.hidden > 0 && app.dock.size === 'open' ? <b>+{g.hidden}</b> : null}
|
|
64
65
|
</button>
|
|
@@ -72,7 +73,7 @@ const PillFace = observer(({ app, p }: { app: HomeSt; p: DockPill }) => {
|
|
|
72
73
|
<span className={`pchip ${p.tone}`}>
|
|
73
74
|
{p.repo == null ? null : pj?.icon != null
|
|
74
75
|
? <img src={pj.icon} alt="" width={12} height={12} draggable={false} />
|
|
75
|
-
: <i className="dgico" style={
|
|
76
|
+
: <i className="dgico idb" style={idVars(app.projects.identityOf(p.repo)) as CSSProperties} />}
|
|
76
77
|
{p.mark == null ? null : <b className={`pcmark ${p.markTone ?? 'dim'}`}>{p.mark}</b>}
|
|
77
78
|
<span className="pctext">{p.text}</span>
|
|
78
79
|
{p.age == null ? null : <span className="pcage">{p.age}</span>}
|
|
@@ -35,18 +35,26 @@ export type HashSel =
|
|
|
35
35
|
| { t: 'when' }
|
|
36
36
|
/** the checks one owner ships: shipkit's, or the repo's own */
|
|
37
37
|
| { t: 'checks'; from: CheckOwner }
|
|
38
|
+
/** the review cards, `id` the open one, null for the first the list shows */
|
|
39
|
+
| { t: 'review'; id: string | null }
|
|
40
|
+
/** a trailer's cuts, played with their notes: a trailer folder and one of its cuts, null for the first; the note
|
|
41
|
+
* open on it, and the notes filter when it is not the page's default (all) */
|
|
42
|
+
| { t: 'trailer'; trailer: string | null; cut: string | null; note?: string | null; filter?: 'open' | 'all' | null }
|
|
38
43
|
|
|
39
44
|
/** whose a gate check is */
|
|
40
45
|
export type CheckOwner = 'shipkit' | 'repo'
|
|
41
46
|
|
|
42
47
|
export type ParsedHash = {
|
|
43
|
-
kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'readme' | 'processes' | 'plan' | 'git' | 'when' | 'checks' | 'none'
|
|
48
|
+
kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'readme' | 'processes' | 'plan' | 'git' | 'when' | 'checks' | 'review' | 'trailer' | 'none'
|
|
44
49
|
/** the collection, for an item. null for everything else */
|
|
45
50
|
col: string | null
|
|
46
51
|
id: string
|
|
47
52
|
/** the media tool set, when the address carried one. An empty array is a
|
|
48
53
|
* choice (every tool off), null is no statement at all */
|
|
49
54
|
tools: string[] | null
|
|
55
|
+
/** a trailer address only: the note open, the notes filter */
|
|
56
|
+
note?: string
|
|
57
|
+
filter?: 'open' | 'all'
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
const ITEM = 'item:'
|
|
@@ -62,6 +70,8 @@ const PLAN = 'page:plan'
|
|
|
62
70
|
const GIT = 'page:git'
|
|
63
71
|
const WHEN = 'page:when'
|
|
64
72
|
const CHECKS = 'page:checks:'
|
|
73
|
+
const REVIEW = 'page:review'
|
|
74
|
+
const TRAILER = 'page:trailer'
|
|
65
75
|
|
|
66
76
|
/** what this selection puts in the url. null only for no selection at all,
|
|
67
77
|
* and then the hash is left alone rather than wiped */
|
|
@@ -81,6 +91,12 @@ export function hashFor(sel: HashSel | null): string | null {
|
|
|
81
91
|
case 'git': return GIT
|
|
82
92
|
case 'when': return WHEN
|
|
83
93
|
case 'checks': return `${CHECKS}${sel.from}`
|
|
94
|
+
case 'review': return sel.id == null ? REVIEW : `${REVIEW}:${sel.id}`
|
|
95
|
+
case 'trailer': {
|
|
96
|
+
// page:trailer:<trailer>/<cut>/<note>?filter=all, each part only when it says something
|
|
97
|
+
const note = sel.trailer != null && sel.note ? `/${sel.cut ?? ''}/${sel.note}` : sel.cut == null ? '' : `/${sel.cut}`
|
|
98
|
+
return `${sel.trailer == null ? TRAILER : `${TRAILER}:${sel.trailer}${note}`}${sel.filter ? `?filter=${sel.filter}` : ''}`
|
|
99
|
+
}
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
|
|
@@ -95,6 +111,15 @@ export function parseHash(raw: string): ParsedHash {
|
|
|
95
111
|
if (raw === PLAN) return { kind: 'plan', col: null, id: '', tools: null }
|
|
96
112
|
if (raw === GIT) return { kind: 'git', col: null, id: '', tools: null }
|
|
97
113
|
if (raw === WHEN) return { kind: 'when', col: null, id: '', tools: null }
|
|
114
|
+
if (raw === REVIEW || raw.startsWith(`${REVIEW}:`)) return { kind: 'review', col: null, id: raw.slice(REVIEW.length + 1), tools: null }
|
|
115
|
+
// the trailer folder rides in `col`, its cut in `id`
|
|
116
|
+
if (raw === TRAILER || raw.startsWith(`${TRAILER}:`) || raw.startsWith(`${TRAILER}?`)) {
|
|
117
|
+
const qi = raw.indexOf('?')
|
|
118
|
+
const path = qi === -1 ? raw : raw.slice(0, qi)
|
|
119
|
+
const [trailer = '', cut = '', note = ''] = path.slice(TRAILER.length + 1).split('/')
|
|
120
|
+
const f = qi === -1 ? null : new URLSearchParams(raw.slice(qi + 1)).get('filter')
|
|
121
|
+
return { kind: 'trailer', col: trailer === '' ? null : trailer, id: cut, tools: null, ...(note ? { note } : {}), ...(f === 'open' || f === 'all' ? { filter: f } : {}) }
|
|
122
|
+
}
|
|
98
123
|
if (raw === `${CHECKS}shipkit` || raw === `${CHECKS}repo`) return { kind: 'checks', col: null, id: raw.slice(CHECKS.length), tools: null }
|
|
99
124
|
if (raw.startsWith(`${PROCESSES}:`)) {
|
|
100
125
|
const id = raw.slice(PROCESSES.length + 1)
|
|
@@ -148,6 +173,8 @@ export function selOf(p: ParsedHash): HashSel | null {
|
|
|
148
173
|
case 'git': return { t: 'git' }
|
|
149
174
|
case 'when': return { t: 'when' }
|
|
150
175
|
case 'checks': return { t: 'checks', from: p.id as CheckOwner }
|
|
176
|
+
case 'review': return { t: 'review', id: p.id === '' ? null : p.id }
|
|
177
|
+
case 'trailer': return { t: 'trailer', trailer: p.col, cut: p.id === '' ? null : p.id, ...(p.note ? { note: p.note } : {}), ...(p.filter ? { filter: p.filter } : {}) }
|
|
151
178
|
}
|
|
152
179
|
}
|
|
153
180
|
|
|
@@ -14,6 +14,8 @@ import { Icon, ICON } from './icons.tsx'
|
|
|
14
14
|
import { Pop, Tip } from './floating.tsx'
|
|
15
15
|
import { FloatCards, type Card } from './float-cards.tsx'
|
|
16
16
|
import type { RepoSt } from './repo-st.ts'
|
|
17
|
+
import type { CSSProperties } from 'react'
|
|
18
|
+
import { idVars } from './projects-svc.ts'
|
|
17
19
|
|
|
18
20
|
/** the repo's hub has not answered on the home: its reason and its fix show
|
|
19
21
|
* instead of the page. A screen this browser kept shows meanwhile, until
|
|
@@ -113,7 +115,7 @@ export const RepoTools = observer(({ app }: { app: RepoSt }) => (
|
|
|
113
115
|
export const HeaderBar = observer(({ app }: { app: RepoSt }) => (
|
|
114
116
|
<>
|
|
115
117
|
<header className="hdr">
|
|
116
|
-
<span className=
|
|
118
|
+
<span className={app.name === '' ? 'hname' : 'hname idc'} style={app.name === '' ? undefined : (idVars(app.home.projects.identityOf(app.name)) as CSSProperties)}>{app.name !== '' ? app.name : 'hub'}</span>
|
|
117
119
|
<span className="sp" />
|
|
118
120
|
<RepoTools app={app} />
|
|
119
121
|
<RepoRun app={app} />
|
|
@@ -14,6 +14,7 @@ import { makeAutoObservable, observableShallow, reaction, type IReactionDisposer
|
|
|
14
14
|
import { currentHash, hashFor } from './hash-route.ts'
|
|
15
15
|
import { MachineSt, type MachineOpts } from './machine-st.ts'
|
|
16
16
|
import { LayoutSt } from './layout-st.ts'
|
|
17
|
+
import { ThemeSt } from './theme-st.ts'
|
|
17
18
|
import { DockSt } from './dock-st.ts'
|
|
18
19
|
import { PaletteSt } from './palette-st.ts'
|
|
19
20
|
import { PlannedSt } from './planned-st.ts'
|
|
@@ -60,6 +61,8 @@ export const plainClick = (e: { button: number; metaKey: boolean; ctrlKey: boole
|
|
|
60
61
|
export type HomeOpts = MachineOpts & {
|
|
61
62
|
/** true on the home's web; false on a hub serving on its own */
|
|
62
63
|
home: boolean
|
|
64
|
+
/** the one repo's base, read from where the page sits: the embed page under a proxy's prefix (embed.ts) */
|
|
65
|
+
base?: string
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export class HomeSt {
|
|
@@ -78,6 +81,8 @@ export class HomeSt {
|
|
|
78
81
|
layout: LayoutSt
|
|
79
82
|
/** the run chart docked under the whole page */
|
|
80
83
|
dock: DockSt
|
|
84
|
+
/** dark, light or the system's */
|
|
85
|
+
readonly theme = new ThemeSt()
|
|
81
86
|
page: Page
|
|
82
87
|
/** the key of the repo picked last: its name on the home, '' for the one
|
|
83
88
|
* repo of a hub of its own. A global page keeps it (only `shown` says
|
|
@@ -90,7 +95,10 @@ export class HomeSt {
|
|
|
90
95
|
private prefetching = false
|
|
91
96
|
private readonly onUrl = (): void => this.fromUrl()
|
|
92
97
|
|
|
98
|
+
private readonly base: string | undefined
|
|
99
|
+
|
|
93
100
|
constructor(o: HomeOpts) {
|
|
101
|
+
this.base = o.base
|
|
94
102
|
this.projects = new ProjectsSvc()
|
|
95
103
|
this.planned = new PlannedSt(this)
|
|
96
104
|
this.activity = new ActivitySt(this)
|
|
@@ -100,7 +108,7 @@ export class HomeSt {
|
|
|
100
108
|
this.dock = new DockSt(this)
|
|
101
109
|
this.machine = o.home ? new MachineSt(this, o) : null
|
|
102
110
|
this.page = o.home ? 'overview' : 'repo'
|
|
103
|
-
makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching'>(this, { machine: false, projects: false, planned: false, activity: false, insights: false, palette: false, layout: false, dock: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
|
|
111
|
+
makeAutoObservable<this, 'disposers' | 'onUrl' | 'prefetching' | 'base'>(this, { base: false, machine: false, projects: false, planned: false, activity: false, insights: false, palette: false, layout: false, theme: false, dock: false, repos: observableShallow, disposers: false, onUrl: false, prefetching: false })
|
|
104
112
|
const m = this.machine
|
|
105
113
|
if (m != null) {
|
|
106
114
|
this.projects.onSelect = (name) => this.select(name)
|
|
@@ -143,7 +151,7 @@ export class HomeSt {
|
|
|
143
151
|
repo(key: string): RepoSt {
|
|
144
152
|
let r = this.repos.get(key)
|
|
145
153
|
if (r == null) {
|
|
146
|
-
r = new RepoSt(this, key, this.machine == null ? '' : repoBase(key))
|
|
154
|
+
r = new RepoSt(this, key, this.base ?? (this.machine == null ? '' : repoBase(key)))
|
|
147
155
|
this.repos.set(key, r)
|
|
148
156
|
// what this browser kept of it paints before its hub answers
|
|
149
157
|
void r.cols.restore()
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// while its hub cannot answer, with the fix. Every link of the home is a
|
|
16
16
|
// page of the same app: a plain click moves it in place.
|
|
17
17
|
import { observer } from 'mobx-react-lite'
|
|
18
|
-
import type { ReactNode } from 'react'
|
|
18
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
19
|
+
import { idVars } from './projects-svc.ts'
|
|
19
20
|
import type { OpenHow, ProcessIcon, ResourceView } from '../../home/resources.ts'
|
|
20
21
|
import type { WorkerView } from '../../home/hubs.ts'
|
|
21
22
|
import { NOT_INSTALLED } from '../../home/home-words.ts'
|
|
@@ -238,7 +239,7 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
|
238
239
|
<span className="hrepo">
|
|
239
240
|
{/* the icon's box is there before the project list is, so the name never moves */}
|
|
240
241
|
{p?.icon != null ? <img src={p.icon} alt="" width={18} height={18} /> : <i className="hrico" />}
|
|
241
|
-
<b className="hrname" style={
|
|
242
|
+
<b className="hrname idc" style={idVars(id) as CSSProperties}>{name}</b>
|
|
242
243
|
</span>
|
|
243
244
|
)
|
|
244
245
|
})
|