rvlib-shipkit 1.3.0 → 1.5.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 +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
package/src/core/wire.ts
CHANGED
|
@@ -3,22 +3,47 @@
|
|
|
3
3
|
// exit 0/1. FROZEN: consumers grep startsWith('FAIL'/'WARN'/'skip:') — a
|
|
4
4
|
// prefix change is a breaking change of the whole toolkit.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { CACHED, costShort, RAN, spanShort } from './outcome-words.ts'
|
|
7
|
+
|
|
8
|
+
export { spanShort }
|
|
9
|
+
|
|
10
|
+
/** when a remembered world answer was measured, and until when it keeps
|
|
11
|
+
* (null: until its declaration changes). Kept as data, never as text, and
|
|
12
|
+
* rendered against the reader's clock: a time frozen into the line reads
|
|
13
|
+
* as a future expiry long after it passed */
|
|
14
|
+
export type Stamp = { at: number; until: number | null; cached?: boolean; ranMs?: number }
|
|
15
|
+
|
|
16
|
+
export type Line = { kind: 'ok' | 'warn' | 'fail' | 'note' | 'fix-warn' | 'fix-fail' | 'skip'; text: string; stamp?: Stamp }
|
|
17
|
+
|
|
18
|
+
/** `cached, 3h old, kept 57m more` for a replay, `ran 0.4s, kept 1h more`
|
|
19
|
+
* for an answer this run measured, `ran 1d ago, expired 1d ago` for one
|
|
20
|
+
* read back later */
|
|
21
|
+
export function stampWords(s: Stamp, now: number): string {
|
|
22
|
+
const age = now - s.at
|
|
23
|
+
const keeps = s.until == null ? 'kept until its declaration changes' : s.until > now ? `kept ${spanShort(s.until - now)} more` : `expired ${spanShort(now - s.until)} ago`
|
|
24
|
+
if (s.cached === true) return `${CACHED}, ${spanShort(age)} old, ${keeps}`
|
|
25
|
+
if (s.ranMs != null) return `${RAN} ${costShort(s.ranMs)}, ${keeps}`
|
|
26
|
+
return `${age < 1000 ? `${RAN} just now` : `${RAN} ${spanShort(age)} ago`}, ${keeps}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** a line's text as a reader sees it now, its stamp rendered */
|
|
30
|
+
export const lineText = (l: Line, now: number = Date.now()): string => (l.stamp == null ? l.text : `${l.text} (${stampWords(l.stamp, now)})`)
|
|
7
31
|
|
|
8
32
|
const green = (s: string): string => `\x1b[32m${s}\x1b[0m`
|
|
9
33
|
const red = (s: string): string => `\x1b[31m${s}\x1b[0m`
|
|
10
34
|
const yellow = (s: string): string => `\x1b[33m${s}\x1b[0m`
|
|
11
35
|
|
|
12
|
-
export function formatLine(l: Line, color: boolean): string {
|
|
36
|
+
export function formatLine(l: Line, color: boolean, now: number = Date.now()): string {
|
|
13
37
|
const c = (f: (s: string) => string, s: string) => (color ? f(s) : s)
|
|
38
|
+
const text = lineText(l, now)
|
|
14
39
|
switch (l.kind) {
|
|
15
|
-
case 'ok': return `${c(green, 'ok:')} ${
|
|
16
|
-
case 'warn': return c(yellow, `WARN ${
|
|
17
|
-
case 'fail': return c(red, `FAIL: ${
|
|
18
|
-
case 'fix-warn': return ` ${c(yellow, `fix: ${
|
|
19
|
-
case 'fix-fail': return ` ${c(red, `fix: ${
|
|
20
|
-
case 'skip': return `skip: ${
|
|
21
|
-
case 'note': return
|
|
40
|
+
case 'ok': return `${c(green, 'ok:')} ${text}`
|
|
41
|
+
case 'warn': return c(yellow, `WARN ${text}`)
|
|
42
|
+
case 'fail': return c(red, `FAIL: ${text}`)
|
|
43
|
+
case 'fix-warn': return ` ${c(yellow, `fix: ${text}`)}`
|
|
44
|
+
case 'fix-fail': return ` ${c(red, `fix: ${text}`)}`
|
|
45
|
+
case 'skip': return `skip: ${text}`
|
|
46
|
+
case 'note': return text
|
|
22
47
|
}
|
|
23
48
|
}
|
|
24
49
|
|
package/src/fleet/scan-fleet.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
|
14
14
|
import { join } from 'path'
|
|
15
15
|
import { isArchived } from '../core/archived.ts'
|
|
16
16
|
import { detectPackageManager, isRefusal } from '../core/package-manager.ts'
|
|
17
|
-
import { isBreaking } from '../core/update-policy.ts'
|
|
17
|
+
import { isBreaking, isLinkedInstall, pinFloor } from '../core/update-policy.ts'
|
|
18
18
|
|
|
19
19
|
export type Pkg = { name?: string; version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
20
20
|
|
|
@@ -24,6 +24,11 @@ export type FleetDep = {
|
|
|
24
24
|
pin: string
|
|
25
25
|
/** what node_modules actually carries, null when nothing is installed */
|
|
26
26
|
installed: string | null
|
|
27
|
+
/** node_modules/<name> is a `bun link` symlink to a source tree */
|
|
28
|
+
linkedInstall: boolean
|
|
29
|
+
/** the version freshness is judged by: the pin's floor under a link
|
|
30
|
+
* (a fresh clone installs the pin, never the source), else `installed` */
|
|
31
|
+
at: string | null
|
|
27
32
|
/** npm latest, null when the registry was not consulted or not reachable */
|
|
28
33
|
latest: string | null
|
|
29
34
|
}
|
|
@@ -54,6 +59,13 @@ const readPkg = (dir: string): Pkg | null => {
|
|
|
54
59
|
|
|
55
60
|
export const installedVersion = (dir: string, name: string): string | null => readPkg(join(dir, 'node_modules', name))?.version ?? null
|
|
56
61
|
|
|
62
|
+
/** what `FleetDep.at` reads now, for a verdict after an update ran */
|
|
63
|
+
export const judgedVersion = (dir: string, name: string): string | null => {
|
|
64
|
+
if (!isLinkedInstall(dir, name)) return installedVersion(dir, name)
|
|
65
|
+
const pkg = readPkg(dir)
|
|
66
|
+
return pinFloor(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name] ?? '')
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
/** semver-ish compare, enough to answer "is this one behind that one". A
|
|
58
70
|
* prerelease tag sorts before its release, which is the safe direction: it
|
|
59
71
|
* reports behind rather than pretending parity. */
|
|
@@ -89,7 +101,11 @@ export function scanFleet(root: string, o: { prefix?: string } = {}): FleetRepo[
|
|
|
89
101
|
const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }
|
|
90
102
|
const names = Object.keys(all).filter((n) => n.startsWith(prefix))
|
|
91
103
|
if (names.length === 0) continue
|
|
92
|
-
const deps: FleetDep[] = names.sort().map((name) =>
|
|
104
|
+
const deps: FleetDep[] = names.sort().map((name) => {
|
|
105
|
+
const installed = installedVersion(dir, name)
|
|
106
|
+
const linkedInstall = isLinkedInstall(dir, name)
|
|
107
|
+
return { name, pin: all[name]!, installed, linkedInstall, at: linkedInstall ? pinFloor(all[name]!) : installed, latest: null }
|
|
108
|
+
})
|
|
93
109
|
out.push({
|
|
94
110
|
name: e,
|
|
95
111
|
dir,
|
|
@@ -120,8 +136,8 @@ export async function resolveLatest(repos: FleetRepo[], fetchLatest: (name: stri
|
|
|
120
136
|
)
|
|
121
137
|
for (const r of repos) {
|
|
122
138
|
for (const d of r.deps) d.latest = latest.get(d.name) ?? null
|
|
123
|
-
r.behind = r.deps.filter((d) => d.
|
|
124
|
-
r.breaking = r.behind.filter((d) => isBreaking(d.
|
|
139
|
+
r.behind = r.deps.filter((d) => d.at != null && d.latest != null && isBehind(d.at, d.latest))
|
|
140
|
+
r.breaking = r.behind.filter((d) => isBreaking(d.at!, d.latest!))
|
|
125
141
|
}
|
|
126
142
|
return latest
|
|
127
143
|
}
|
|
@@ -7,7 +7,7 @@ import { join } from 'path'
|
|
|
7
7
|
import { spawnWithTimeout } from '../core/spawn-timeout.ts'
|
|
8
8
|
import { detectPackageManager, isRefusal } from '../core/package-manager.ts'
|
|
9
9
|
import { ALLOW_MAJOR_ENV, breakingAdvice } from '../core/update-policy.ts'
|
|
10
|
-
import {
|
|
10
|
+
import { isBehind, judgedVersion, updateRefusal, type FleetRepo } from './scan-fleet.ts'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
export const short = (name: string): string => name.replace(/^rvlib-/, '')
|
|
@@ -64,15 +64,15 @@ export async function updateFleet(behind: FleetRepo[], o: FleetUpdateOpts): Prom
|
|
|
64
64
|
const env: Record<string, string> = Object.fromEntries(Object.entries(process.env).filter((e): e is [string, string] => e[1] != null && e[0] !== 'SHIPKIT_SELF_UPDATED' && e[0] !== ALLOW_MAJOR_ENV))
|
|
65
65
|
if (o.major) env[ALLOW_MAJOR_ENV] = '1'
|
|
66
66
|
const res = await spawnWithTimeout(['bun', cli, 'check', '--fresh'], { cwd: r.dir, env, timeoutMs: o.timeoutMs ?? 600_000 })
|
|
67
|
-
// judge by what node_modules carries now
|
|
68
|
-
// printed line: the code covers every rule
|
|
69
|
-
// format belongs to whatever version that repo runs
|
|
67
|
+
// judge by what node_modules carries now (the pin, for a linked dep),
|
|
68
|
+
// never by the exit code or a printed line: the code covers every rule
|
|
69
|
+
// in that repo, and the line format belongs to whatever version that repo runs
|
|
70
70
|
const stuck = wanted.filter((d) => {
|
|
71
|
-
const now =
|
|
71
|
+
const now = judgedVersion(r.dir, d.name)
|
|
72
72
|
return now == null || isBehind(now, d.latest!)
|
|
73
73
|
})
|
|
74
74
|
if (stuck.length === 0) {
|
|
75
|
-
w(`ok: ${r.name} — ${wanted.map((d) => `${short(d.name)} ${d.
|
|
75
|
+
w(`ok: ${r.name} — ${wanted.map((d) => `${short(d.name)}${d.linkedInstall ? ' pin' : ''} ${d.at} → ${judgedVersion(r.dir, d.name)}`).join(', ')}`)
|
|
76
76
|
continue
|
|
77
77
|
}
|
|
78
78
|
failed++
|
|
@@ -64,8 +64,11 @@ export type StatusCheck = {
|
|
|
64
64
|
status: 'fail' | 'warn'
|
|
65
65
|
/** when that outcome was recorded, epoch ms */
|
|
66
66
|
at: number | null
|
|
67
|
-
/** the
|
|
67
|
+
/** the worst line it printed about the problem, '' when it printed none */
|
|
68
68
|
line: string
|
|
69
|
+
/** every fail and warn line it printed, the worst weight first: a check
|
|
70
|
+
* reporting three findings lists three */
|
|
71
|
+
lines: string[]
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export type ActivityAnswer = { now: number; minutes: number; rows: ActivityRow[]; ms: number }
|
package/src/home/activity.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { gitBin } from '../core/git-bin.ts'
|
|
12
12
|
import { readLastStatusReadonly } from '../ledger/run-log.ts'
|
|
13
13
|
import { checkStateOf, recordOf } from '../serve/project-gutter.ts'
|
|
14
|
-
import type
|
|
14
|
+
import { lineText, type Line } from '../core/wire.ts'
|
|
15
15
|
import { MAX_COMMITS, MAX_FILES, type ActivityAnswer, type ActivityCommit, type ActivityRepo, type ActivityRow, type FileKind, type StatusCheck } from './activity-shape.ts'
|
|
16
16
|
|
|
17
17
|
export * from './activity-shape.ts'
|
|
@@ -93,26 +93,32 @@ export type Declared = (name: string) => ReadonlySet<string> | null
|
|
|
93
93
|
/** each declared check's title by id; null = not known, the id stands in */
|
|
94
94
|
export type Titles = (name: string) => ReadonlyMap<string, string> | null
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
* weight (a fail for a failure, a warning for a
|
|
98
|
-
* line that is not a pass, without the id
|
|
99
|
-
|
|
96
|
+
/** every line a check printed about its problems: its fail and warn lines,
|
|
97
|
+
* those of its own weight first (a fail for a failure, a warning for a
|
|
98
|
+
* warning), else its first line that is not a pass, each without the id it
|
|
99
|
+
* leads with */
|
|
100
|
+
export function findingsOf(lines: string, status: 'fail' | 'warn', id: string): string[] {
|
|
100
101
|
let parsed: Line[]
|
|
101
102
|
try {
|
|
102
103
|
parsed = JSON.parse(lines) as Line[]
|
|
103
104
|
} catch {
|
|
104
|
-
return
|
|
105
|
+
return []
|
|
105
106
|
}
|
|
106
|
-
if (!Array.isArray(parsed)) return
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
if (!Array.isArray(parsed)) return []
|
|
108
|
+
const of = (k: Line['kind']): Line[] => parsed.filter((l) => l.kind === k)
|
|
109
|
+
let hits = status === 'fail' ? [...of('fail'), ...of('warn')] : [...of('warn'), ...of('fail')]
|
|
110
|
+
if (hits.length === 0) hits = parsed.filter((l) => l.kind !== 'ok' && l.kind !== 'skip').slice(0, 1)
|
|
111
|
+
return hits.map((l) => {
|
|
112
|
+
const t = lineText(l).trim()
|
|
113
|
+
// `[ID] text`, and a runner line's `ID — text`: the list names the id already
|
|
114
|
+
for (const lead of [`[${id}]`, `${id} —`]) if (t.startsWith(lead)) return t.slice(lead.length).trim()
|
|
115
|
+
return t
|
|
116
|
+
})
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
/** the worst of them, '' when it printed none */
|
|
120
|
+
export const findingOf = (lines: string, status: 'fail' | 'warn', id: string): string => findingsOf(lines, status, id)[0] ?? ''
|
|
121
|
+
|
|
116
122
|
/** a repo's checks as its run record last saw them, counted over what it
|
|
117
123
|
* declares, and the failing and warning ones with what each said */
|
|
118
124
|
export function statusOf(repo: ActivityRepo, declared: Declared | null = null, titles: Titles | null = null): Pick<ActivityRow, 'status' | 'failing'> {
|
|
@@ -125,7 +131,8 @@ export function statusOf(repo: ActivityRepo, declared: Declared | null = null, t
|
|
|
125
131
|
const named = titles?.(repo.name) ?? null
|
|
126
132
|
const one = (id: string, st: 'fail' | 'warn'): StatusCheck => {
|
|
127
133
|
const r = byId.get(id)
|
|
128
|
-
|
|
134
|
+
const lines = r == null ? [] : findingsOf(r.lines, st, id)
|
|
135
|
+
return { id, title: named?.get(id) ?? id, status: st, at: status.since[id] ?? null, line: lines[0] ?? '', lines }
|
|
129
136
|
}
|
|
130
137
|
return { status, failing: [...status.reds.map((id) => one(id, 'fail')), ...status.warns.map((id) => one(id, 'warn'))] }
|
|
131
138
|
}
|
|
@@ -1,24 +1,94 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
1
|
+
// one web app window on macOS: its own app, so the dock and cmd-tab show its
|
|
2
|
+
// own icon, never a second browser. The same source is the shipkit window and
|
|
3
|
+
// every per-process app (home/gui-app.ts builds both with swiftc, no Xcode
|
|
4
|
+
// project): the bundle's Info.plist names the app (CFBundleName) and the url
|
|
5
|
+
// a bare launch opens (ShipkitStartURL). One WKWebView per window; every
|
|
6
|
+
// `open -a <app> <url>` opens one more window, cmd-n too. A link to another
|
|
7
|
+
// site opens in the default browser.
|
|
6
8
|
import AppKit
|
|
7
9
|
import WebKit
|
|
8
10
|
|
|
9
|
-
let
|
|
11
|
+
let info = Bundle.main.infoDictionary ?? [:]
|
|
12
|
+
let appName = (info["CFBundleName"] as? String) ?? "shipkit"
|
|
13
|
+
let startURL = URL(string: (info["ShipkitStartURL"] as? String) ?? "") ?? URL(string: "http://127.0.0.1:4799/")!
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
/** a url that stays in the app: the machine itself, or the host the app is for */
|
|
16
|
+
func stays(_ url: URL) -> Bool {
|
|
17
|
+
url.host == "127.0.0.1" || url.host == "localhost" || url.host == startURL.host
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** a window's content: the web view, and the find bar above it when shown.
|
|
21
|
+
* WKWebView is an NSTextFinderClient, so the platform find bar searches the
|
|
22
|
+
* page; this view is the bar's container */
|
|
23
|
+
final class Pane: NSView, NSTextFinderBarContainer {
|
|
24
|
+
let web: WKWebView
|
|
25
|
+
let finder = NSTextFinder()
|
|
26
|
+
|
|
27
|
+
init(web: WKWebView) {
|
|
28
|
+
self.web = web
|
|
29
|
+
super.init(frame: .zero)
|
|
30
|
+
addSubview(web)
|
|
31
|
+
finder.client = web
|
|
32
|
+
finder.findBarContainer = self
|
|
33
|
+
finder.isIncrementalSearchingEnabled = true
|
|
34
|
+
finder.incrementalSearchingShouldDimContentView = false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
required init?(coder: NSCoder) { fatalError("not from a nib") }
|
|
38
|
+
|
|
39
|
+
override var isFlipped: Bool { true }
|
|
40
|
+
|
|
41
|
+
var findBarView: NSView? {
|
|
42
|
+
didSet {
|
|
43
|
+
oldValue?.removeFromSuperview()
|
|
44
|
+
if let bar = findBarView { addSubview(bar) }
|
|
45
|
+
needsLayout = true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var isFindBarVisible = false {
|
|
50
|
+
didSet {
|
|
51
|
+
findBarView?.isHidden = !isFindBarVisible
|
|
52
|
+
needsLayout = true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func findBarViewDidChangeHeight() { needsLayout = true }
|
|
57
|
+
|
|
58
|
+
func contentView() -> NSView? { web }
|
|
59
|
+
|
|
60
|
+
override func layout() {
|
|
61
|
+
super.layout()
|
|
62
|
+
var top: CGFloat = 0
|
|
63
|
+
if let bar = findBarView, isFindBarVisible {
|
|
64
|
+
let h = bar.frame.height
|
|
65
|
+
bar.frame = NSRect(x: 0, y: 0, width: bounds.width, height: h)
|
|
66
|
+
top = h
|
|
67
|
+
}
|
|
68
|
+
web.frame = NSRect(x: 0, y: top, width: bounds.width, height: max(0, bounds.height - top))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate, NSMenuItemValidation {
|
|
12
73
|
var windows: [NSWindow] = []
|
|
13
74
|
var opened = false
|
|
14
75
|
|
|
15
76
|
func applicationDidFinishLaunching(_ note: Notification) {
|
|
16
77
|
buildMenu()
|
|
78
|
+
// esc closes the find bar of the key window, and reaches the page otherwise
|
|
79
|
+
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { e in
|
|
80
|
+
if e.keyCode == 53, let pane = self.keyPane(), pane.isFindBarVisible {
|
|
81
|
+
pane.finder.performAction(.hideFindInterface)
|
|
82
|
+
pane.window?.makeFirstResponder(pane.web)
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
return e
|
|
86
|
+
}
|
|
17
87
|
// an open event may deliver the url right after launch: wait for it
|
|
18
88
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
19
89
|
if !self.opened {
|
|
20
90
|
let arg = CommandLine.arguments.dropFirst().first { $0.hasPrefix("http") }
|
|
21
|
-
self.open(URL(string:
|
|
91
|
+
self.open(arg.flatMap { URL(string: $0) } ?? startURL)
|
|
22
92
|
}
|
|
23
93
|
}
|
|
24
94
|
}
|
|
@@ -30,26 +100,27 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
30
100
|
func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool { true }
|
|
31
101
|
|
|
32
102
|
func applicationShouldHandleReopen(_ app: NSApplication, hasVisibleWindows: Bool) -> Bool {
|
|
33
|
-
if !hasVisibleWindows { open(
|
|
103
|
+
if !hasVisibleWindows { open(startURL) }
|
|
34
104
|
return true
|
|
35
105
|
}
|
|
36
106
|
|
|
107
|
+
func keyPane() -> Pane? { NSApp.keyWindow?.contentView as? Pane }
|
|
108
|
+
|
|
37
109
|
@objc func newWindow(_ sender: Any?) {
|
|
38
|
-
|
|
39
|
-
open(current ?? URL(string: homeFallback)!)
|
|
110
|
+
open(keyPane()?.web.url ?? startURL)
|
|
40
111
|
}
|
|
41
112
|
|
|
42
113
|
@objc func reload(_ sender: Any?) {
|
|
43
|
-
(
|
|
114
|
+
keyPane()?.web.reload()
|
|
44
115
|
}
|
|
45
116
|
|
|
46
|
-
// the zoom is one value for every window, kept across restarts
|
|
117
|
+
// the zoom is one value for every window of the app, kept across restarts
|
|
47
118
|
var zoom: CGFloat {
|
|
48
119
|
get { let z = UserDefaults.standard.double(forKey: "pageZoom"); return z > 0 ? CGFloat(z) : 1 }
|
|
49
120
|
set {
|
|
50
121
|
let z = min(3, max(0.3, newValue))
|
|
51
122
|
UserDefaults.standard.set(Double(z), forKey: "pageZoom")
|
|
52
|
-
for w in windows { (w.contentView as?
|
|
123
|
+
for w in windows { (w.contentView as? Pane)?.web.pageZoom = z }
|
|
53
124
|
}
|
|
54
125
|
}
|
|
55
126
|
|
|
@@ -57,19 +128,51 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
57
128
|
@objc func zoomOut(_ sender: Any?) { zoom = zoom / 1.1 }
|
|
58
129
|
@objc func zoomReset(_ sender: Any?) { zoom = 1 }
|
|
59
130
|
|
|
131
|
+
// the menu item's tag is the NSTextFinder.Action it performs
|
|
132
|
+
@objc func findAction(_ sender: NSMenuItem) {
|
|
133
|
+
guard let pane = keyPane(), let action = NSTextFinder.Action(rawValue: sender.tag) else { return }
|
|
134
|
+
pane.finder.performAction(action)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// no public api opens the inspector: `_inspector` then `show` is the
|
|
138
|
+
// WebKit call the context menu's Inspect Element makes. Where it is gone,
|
|
139
|
+
// say where the inspector still is
|
|
140
|
+
@objc func devTools(_ sender: Any?) {
|
|
141
|
+
guard let web = keyPane()?.web else { return }
|
|
142
|
+
let get = NSSelectorFromString("_inspector"), show = NSSelectorFromString("show")
|
|
143
|
+
if web.responds(to: get), let inspector = web.perform(get)?.takeUnretainedValue() as? NSObject, inspector.responds(to: show) {
|
|
144
|
+
inspector.perform(show)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
let alert = NSAlert()
|
|
148
|
+
alert.messageText = "This WebKit has no way to open the inspector from a menu"
|
|
149
|
+
alert.informativeText = "Right click the page and pick Inspect Element."
|
|
150
|
+
alert.runModal()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
|
154
|
+
if item.action == #selector(findAction(_:)) {
|
|
155
|
+
guard let pane = keyPane(), let action = NSTextFinder.Action(rawValue: item.tag) else { return false }
|
|
156
|
+
return pane.finder.validateAction(action)
|
|
157
|
+
}
|
|
158
|
+
if item.action == #selector(devTools(_:)) || item.action == #selector(reload(_:)) { return keyPane() != nil }
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
60
162
|
func open(_ url: URL) {
|
|
61
163
|
opened = true
|
|
62
164
|
let config = WKWebViewConfiguration()
|
|
63
165
|
config.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
|
64
166
|
let web = WKWebView(frame: .zero, configuration: config)
|
|
167
|
+
if #available(macOS 13.3, *) { web.isInspectable = true }
|
|
65
168
|
web.navigationDelegate = self
|
|
66
169
|
web.uiDelegate = self
|
|
67
170
|
web.allowsBackForwardNavigationGestures = true
|
|
68
171
|
web.pageZoom = zoom
|
|
69
172
|
let frame = NSRect(x: 0, y: 0, width: 1400, height: 900)
|
|
70
173
|
let win = NSWindow(contentRect: frame, styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
|
|
71
|
-
win.contentView = web
|
|
72
|
-
win.title =
|
|
174
|
+
win.contentView = Pane(web: web)
|
|
175
|
+
win.title = appName
|
|
73
176
|
win.delegate = self
|
|
74
177
|
win.isReleasedWhenClosed = false
|
|
75
178
|
win.setFrameAutosaveName("shipkit-window")
|
|
@@ -77,6 +180,7 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
77
180
|
windows.append(win)
|
|
78
181
|
web.load(URLRequest(url: url))
|
|
79
182
|
win.makeKeyAndOrderFront(nil)
|
|
183
|
+
win.makeFirstResponder(web)
|
|
80
184
|
NSApp.activate(ignoringOtherApps: true)
|
|
81
185
|
}
|
|
82
186
|
|
|
@@ -84,35 +188,40 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
84
188
|
windows.removeAll { $0 === note.object as? NSWindow }
|
|
85
189
|
}
|
|
86
190
|
|
|
87
|
-
// the page title names the window, so cmd-tab and the window menu tell
|
|
191
|
+
// the page title names the window, so cmd-tab and the window menu tell pages apart
|
|
88
192
|
func webView(_ web: WKWebView, didFinish nav: WKNavigation!) {
|
|
89
|
-
web.window?.title = (web.title?.isEmpty == false) ? web.title! :
|
|
193
|
+
web.window?.title = (web.title?.isEmpty == false) ? web.title! : appName
|
|
90
194
|
}
|
|
91
195
|
|
|
92
|
-
//
|
|
196
|
+
// the app's own host: stay in the window; anything else: the default browser
|
|
93
197
|
func webView(_ web: WKWebView, decidePolicyFor action: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
94
198
|
guard let url = action.request.url, action.navigationType == .linkActivated else { return decisionHandler(.allow) }
|
|
95
|
-
|
|
96
|
-
if local { return decisionHandler(.allow) }
|
|
199
|
+
if stays(url) { return decisionHandler(.allow) }
|
|
97
200
|
NSWorkspace.shared.open(url)
|
|
98
201
|
decisionHandler(.cancel)
|
|
99
202
|
}
|
|
100
203
|
|
|
101
|
-
// window.open and target=_blank: one more
|
|
204
|
+
// window.open and target=_blank: one more window of this app for its own pages
|
|
102
205
|
func webView(_ web: WKWebView, createWebViewWith config: WKWebViewConfiguration, for action: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
103
206
|
if let url = action.request.url {
|
|
104
|
-
if url
|
|
207
|
+
if stays(url) { open(url) } else { NSWorkspace.shared.open(url) }
|
|
105
208
|
}
|
|
106
209
|
return nil
|
|
107
210
|
}
|
|
108
211
|
|
|
212
|
+
func item(_ menu: NSMenu, _ title: String, _ action: Selector, _ key: String, _ mods: NSEvent.ModifierFlags = [.command], tag: Int = 0) {
|
|
213
|
+
let i = menu.addItem(withTitle: title, action: action, keyEquivalent: key)
|
|
214
|
+
i.keyEquivalentModifierMask = mods
|
|
215
|
+
i.tag = tag
|
|
216
|
+
}
|
|
217
|
+
|
|
109
218
|
func buildMenu() {
|
|
110
219
|
let main = NSMenu()
|
|
111
220
|
let appItem = NSMenuItem()
|
|
112
221
|
main.addItem(appItem)
|
|
113
222
|
let appMenu = NSMenu()
|
|
114
|
-
appMenu.addItem(withTitle: "Hide
|
|
115
|
-
appMenu.addItem(withTitle: "Quit
|
|
223
|
+
appMenu.addItem(withTitle: "Hide \(appName)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
|
|
224
|
+
appMenu.addItem(withTitle: "Quit \(appName)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
|
116
225
|
appItem.submenu = appMenu
|
|
117
226
|
let fileItem = NSMenuItem()
|
|
118
227
|
main.addItem(fileItem)
|
|
@@ -130,6 +239,14 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
130
239
|
edit.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
|
131
240
|
edit.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
132
241
|
edit.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
242
|
+
edit.addItem(.separator())
|
|
243
|
+
let findItem = edit.addItem(withTitle: "Find", action: nil, keyEquivalent: "")
|
|
244
|
+
let find = NSMenu(title: "Find")
|
|
245
|
+
item(find, "Find…", #selector(findAction(_:)), "f", tag: NSTextFinder.Action.showFindInterface.rawValue)
|
|
246
|
+
item(find, "Find Next", #selector(findAction(_:)), "g", tag: NSTextFinder.Action.nextMatch.rawValue)
|
|
247
|
+
item(find, "Find Previous", #selector(findAction(_:)), "g", [.command, .shift], tag: NSTextFinder.Action.previousMatch.rawValue)
|
|
248
|
+
item(find, "Hide Find Bar", #selector(findAction(_:)), "", [], tag: NSTextFinder.Action.hideFindInterface.rawValue)
|
|
249
|
+
edit.setSubmenu(find, for: findItem)
|
|
133
250
|
editItem.submenu = edit
|
|
134
251
|
let viewItem = NSMenuItem()
|
|
135
252
|
main.addItem(viewItem)
|
|
@@ -141,6 +258,8 @@ final class Delegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKU
|
|
|
141
258
|
alsoIn.isAlternate = true
|
|
142
259
|
alsoIn.keyEquivalentModifierMask = [.command]
|
|
143
260
|
view.addItem(withTitle: "Zoom Out", action: #selector(zoomOut(_:)), keyEquivalent: "-")
|
|
261
|
+
view.addItem(.separator())
|
|
262
|
+
item(view, "Developer Tools", #selector(devTools(_:)), "i", [.command, .option])
|
|
144
263
|
viewItem.submenu = view
|
|
145
264
|
let winItem = NSMenuItem()
|
|
146
265
|
main.addItem(winItem)
|