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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// a JSONC parser that remembers WHERE every value sits, so a fixer can edit
|
|
2
|
+
// one value of a hand-written file (vscode's tasks.json carries comments and
|
|
3
|
+
// trailing commas) and leave every other byte exactly as its author wrote it.
|
|
4
|
+
|
|
5
|
+
export type Node =
|
|
6
|
+
| { kind: 'object'; start: number; end: number; props: { key: string; keyStart: number; value: Node }[] }
|
|
7
|
+
| { kind: 'array'; start: number; end: number; items: Node[] }
|
|
8
|
+
| { kind: 'scalar'; start: number; end: number; value: unknown }
|
|
9
|
+
|
|
10
|
+
/** the tree of `text`, or throws on a malformed document. `end` is exclusive */
|
|
11
|
+
export function parseSpans(text: string): Node {
|
|
12
|
+
let i = 0
|
|
13
|
+
const skip = (): void => {
|
|
14
|
+
for (;;) {
|
|
15
|
+
while (i < text.length && /\s/.test(text[i]!)) i++
|
|
16
|
+
if (text.startsWith('//', i)) { while (i < text.length && text[i] !== '\n') i++; continue }
|
|
17
|
+
if (text.startsWith('/*', i)) { const e = text.indexOf('*/', i + 2); i = e < 0 ? text.length : e + 2; continue }
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const fail = (what: string): never => { throw new Error(`jsonc: ${what} at offset ${i}`) }
|
|
22
|
+
const str = (): string => {
|
|
23
|
+
const start = i
|
|
24
|
+
i++
|
|
25
|
+
while (i < text.length && text[i] !== '"') i += text[i] === '\\' ? 2 : 1
|
|
26
|
+
if (i >= text.length) fail('unterminated string')
|
|
27
|
+
i++
|
|
28
|
+
return JSON.parse(text.slice(start, i)) as string
|
|
29
|
+
}
|
|
30
|
+
const value = (): Node => {
|
|
31
|
+
skip()
|
|
32
|
+
const start = i
|
|
33
|
+
const c = text[i]
|
|
34
|
+
if (c === '{') {
|
|
35
|
+
i++
|
|
36
|
+
const props: { key: string; keyStart: number; value: Node }[] = []
|
|
37
|
+
for (;;) {
|
|
38
|
+
skip()
|
|
39
|
+
if (text[i] === '}') { i++; return { kind: 'object', start, end: i, props } }
|
|
40
|
+
if (text[i] !== '"') fail('expected a key')
|
|
41
|
+
const keyStart = i
|
|
42
|
+
const key = str()
|
|
43
|
+
skip()
|
|
44
|
+
if (text[i] !== ':') fail('expected a colon')
|
|
45
|
+
i++
|
|
46
|
+
props.push({ key, keyStart, value: value() })
|
|
47
|
+
skip()
|
|
48
|
+
if (text[i] === ',') i++
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (c === '[') {
|
|
52
|
+
i++
|
|
53
|
+
const items: Node[] = []
|
|
54
|
+
for (;;) {
|
|
55
|
+
skip()
|
|
56
|
+
if (text[i] === ']') { i++; return { kind: 'array', start, end: i, items } }
|
|
57
|
+
items.push(value())
|
|
58
|
+
skip()
|
|
59
|
+
if (text[i] === ',') i++
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (c === '"') {
|
|
63
|
+
const v = str()
|
|
64
|
+
return { kind: 'scalar', start, end: i, value: v }
|
|
65
|
+
}
|
|
66
|
+
const m = /^(?:true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(i))
|
|
67
|
+
if (m == null) return fail('unexpected character')
|
|
68
|
+
i += m[0].length
|
|
69
|
+
return { kind: 'scalar', start, end: i, value: JSON.parse(m[0]) }
|
|
70
|
+
}
|
|
71
|
+
const root = value()
|
|
72
|
+
skip()
|
|
73
|
+
if (i < text.length) fail('trailing content')
|
|
74
|
+
return root
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** the plain value of a node, comments and positions dropped */
|
|
78
|
+
export function plain(text: string, n: Node): unknown {
|
|
79
|
+
if (n.kind === 'scalar') return n.value
|
|
80
|
+
if (n.kind === 'array') return n.items.map((x) => plain(text, x))
|
|
81
|
+
return Object.fromEntries(n.props.map((p) => [p.key, plain(text, p.value)]))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** the whitespace a line starts with, for the line holding offset `at` */
|
|
85
|
+
export function indentAt(text: string, at: number): string {
|
|
86
|
+
const lineStart = text.lastIndexOf('\n', at - 1) + 1
|
|
87
|
+
return /^[ \t]*/.exec(text.slice(lineStart))![0]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** the text span that removes item `idx` of an array cleanly: its own line
|
|
91
|
+
* when it sits alone on it, and the comma that joined it to a neighbour */
|
|
92
|
+
export function removalSpan(text: string, arr: Extract<Node, { kind: 'array' }>, idx: number): [number, number] {
|
|
93
|
+
const item = arr.items[idx]!
|
|
94
|
+
const next = arr.items[idx + 1]
|
|
95
|
+
if (next != null) {
|
|
96
|
+
let j = item.end
|
|
97
|
+
while (j < text.length && /[ \t]/.test(text[j]!)) j++
|
|
98
|
+
if (text[j] === ',') j++
|
|
99
|
+
const lineStart = text.lastIndexOf('\n', item.start - 1) + 1
|
|
100
|
+
const eol = text.indexOf('\n', j)
|
|
101
|
+
// whole lines when the item owns them, so a comment above the next
|
|
102
|
+
// item stays with it
|
|
103
|
+
if (text.slice(lineStart, item.start).trim() === '' && eol >= 0 && text.slice(j, eol).trim() === '') return [lineStart, eol + 1]
|
|
104
|
+
while (j < text.length && /[ \t]/.test(text[j]!)) j++
|
|
105
|
+
return [item.start, j]
|
|
106
|
+
}
|
|
107
|
+
const prev = arr.items[idx - 1]
|
|
108
|
+
if (prev != null) {
|
|
109
|
+
// the last one: drop the comma after the previous item, keep what closes the array
|
|
110
|
+
return [prev.end, item.end]
|
|
111
|
+
}
|
|
112
|
+
return [item.start, item.end]
|
|
113
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
14
14
|
import { dirname, join } from 'path'
|
|
15
15
|
import { defaultHubPort } from '../serve/hub-port.ts'
|
|
16
|
+
import { hubProblemMatcher } from '../problems/problem-line.ts'
|
|
16
17
|
|
|
17
18
|
const FILES = join(import.meta.dir, 'files')
|
|
18
19
|
|
|
@@ -122,23 +123,21 @@ export type ErgonomicsOpts = {
|
|
|
122
123
|
* is the opposite: with no `folderOpen` task nothing starts at all, so there
|
|
123
124
|
* is no existing answer to overrule.
|
|
124
125
|
*
|
|
125
|
-
* What lands INSIDE it is
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* `
|
|
129
|
-
*
|
|
130
|
-
* `references`) type checks ZERO files and reports a clean problems pane
|
|
131
|
-
* forever, which is worse than no task. Every other watcher a stack earns
|
|
132
|
-
* (a dev server, css, tests) stays the analysis step the owner page describes,
|
|
126
|
+
* What lands INSIDE it is only what is true of every wired repo: the hub,
|
|
127
|
+
* whose script `init` writes, carrying the problem matcher that turns every
|
|
128
|
+
* check's findings into the problems pane. There is no type checker task, the
|
|
129
|
+
* hub runs `typecheck` like any other check. Every watcher a stack earns (a
|
|
130
|
+
* dev server, css) stays the analysis step `build/vscode-tasks.md` describes,
|
|
133
131
|
* and the caller says so out loud.
|
|
134
132
|
*
|
|
135
133
|
* Create-only: an adopted repo often carries a `.vscode/` from whatever it was
|
|
136
|
-
* forked from, and that one wins byte for byte
|
|
134
|
+
* forked from, and that one wins byte for byte (rule SK017 then repairs its
|
|
135
|
+
* hub task in place). Returns the paths it wrote. */
|
|
137
136
|
export function editorErgonomics(root: string, opts: ErgonomicsOpts): string[] {
|
|
138
137
|
// a symlinked .vscode is a dotfiles pattern pointing at a SHARED dir, and
|
|
139
138
|
// writing through it lands files outside the repo being wired
|
|
140
139
|
if (isLink(join(root, '.vscode'))) return []
|
|
141
|
-
const body = tasksTemplate({ NAME: opts.name, PORT: String(opts.port) }
|
|
140
|
+
const body = tasksTemplate({ NAME: opts.name, PORT: String(opts.port) })
|
|
142
141
|
return put(root, '.vscode/tasks.json', body) ? ['.vscode/tasks.json'] : []
|
|
143
142
|
}
|
|
144
143
|
|
|
@@ -150,35 +149,11 @@ const isLink = (path: string): boolean => {
|
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
/** the tasks template
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (withTsc) return body
|
|
159
|
-
const parsed = JSON.parse(body.replace(/^\s*\/\/.*$/gm, '')) as { version: string; tasks: Array<Record<string, unknown>> }
|
|
160
|
-
parsed.tasks = parsed.tasks.filter((t) => t['label'] !== TSC_LABEL)
|
|
161
|
-
for (const t of parsed.tasks) {
|
|
162
|
-
const deps = t['dependsOn']
|
|
163
|
-
if (Array.isArray(deps)) t['dependsOn'] = deps.filter((d) => d !== TSC_LABEL)
|
|
164
|
-
}
|
|
165
|
-
return JSON.stringify(parsed, null, 3) + '\n'
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const TSC_LABEL = '👁 TSC'
|
|
169
|
-
|
|
170
|
-
/** whether `tsc --project tsconfig.json` here would check anything at all. A
|
|
171
|
-
* solution config (`files: []` plus `references`) needs `--build`, which the
|
|
172
|
-
* house watcher does not pass, and answers zero files without it. */
|
|
173
|
-
export function typecheckableProject(root: string): boolean {
|
|
174
|
-
try {
|
|
175
|
-
const raw = readFileSync(join(root, 'tsconfig.json'), 'utf8')
|
|
176
|
-
const cfg = JSON.parse(raw.replace(/^\s*\/\/.*$/gm, '')) as { files?: unknown[]; include?: unknown[]; references?: unknown[] }
|
|
177
|
-
if ((cfg.references ?? []).length === 0) return true
|
|
178
|
-
return (cfg.files ?? []).length > 0 || (cfg.include ?? []).length > 0
|
|
179
|
-
} catch {
|
|
180
|
-
return false
|
|
181
|
-
}
|
|
152
|
+
/** the tasks template, the hub's problem matcher rendered in from the one
|
|
153
|
+
* module the hub prints through, so the two can never disagree */
|
|
154
|
+
export function tasksTemplate(vars: Record<string, string>): string {
|
|
155
|
+
return template('vscode-tasks.json', vars).replace(/^(\s*)(.*)\{\{HUB_PROBLEM_MATCHER\}\}/m, (_all, indent: string, head: string) =>
|
|
156
|
+
`${indent}${head}${JSON.stringify(hubProblemMatcher(), null, 3).split('\n').join(`\n${indent}`)}`)
|
|
182
157
|
}
|
|
183
158
|
|
|
184
159
|
export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
@@ -200,8 +175,6 @@ export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
|
200
175
|
|
|
201
176
|
write('src/index.ts', 'export {}\n')
|
|
202
177
|
write('tsconfig.json', template('tsconfig.json', vars))
|
|
203
|
-
// tasks AFTER the tsconfig, so the TSC subtask is derived from the project
|
|
204
|
-
// this run just wrote rather than from the empty dir it found
|
|
205
178
|
;(editorErgonomics(root, { name: opts.name, port }).length > 0 ? written : skipped).push('.vscode/tasks.json')
|
|
206
179
|
// settings.json only here: it turns eslint off and hands formatting to an
|
|
207
180
|
// oxc extension, which is a decision only the repo's creator gets to make
|
|
@@ -223,7 +196,7 @@ export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
|
223
196
|
write('agent/changelog.md', template('agent-changelog.md', vars))
|
|
224
197
|
// gitignored by policy, so it never ships: it exists so an agent hitting
|
|
225
198
|
// friction with the toolchain finds the place to write it down
|
|
226
|
-
write('.
|
|
199
|
+
write('.shipkit/reflections/README.md', REFLECTIONS_README)
|
|
227
200
|
|
|
228
201
|
addScripts(root, written, p)
|
|
229
202
|
addIgnoreLines(root, written)
|
|
@@ -247,7 +220,7 @@ function grantSummary(): string {
|
|
|
247
220
|
}
|
|
248
221
|
}
|
|
249
222
|
|
|
250
|
-
const REFLECTIONS_README = `# .
|
|
223
|
+
const REFLECTIONS_README = `# .shipkit/reflections
|
|
251
224
|
|
|
252
225
|
One file per friction with the shared toolchain: \`YYYYMMDD-<slug>.md\`.
|
|
253
226
|
What broke, which page or lib, what you expected. Gitignored on purpose,
|
package/src/scan.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// A SHARED SCAN: work several checks need (every wiki page parsed, the root
|
|
2
|
+
// package.json), done once per runner pass and handed to each check that asks
|
|
3
|
+
// through `c.use(scan)`. The scan reads through its own context, and every
|
|
4
|
+
// read it makes is recorded into EVERY check that used it, so a cached answer
|
|
5
|
+
// built on a scan busts when anything the scan read moves.
|
|
6
|
+
//
|
|
7
|
+
// The NAME is the scan's identity inside a pass: two scans with one name share
|
|
8
|
+
// one value. A factory that takes an argument (mdPages below) puts the argument
|
|
9
|
+
// in the name and hands back one scan object per argument.
|
|
10
|
+
import type { ScanCtx } from './core/check-ctx.ts'
|
|
11
|
+
|
|
12
|
+
export type Scan<T> = {
|
|
13
|
+
/** unique per pass: the memo key */
|
|
14
|
+
readonly name: string
|
|
15
|
+
readonly run: (c: ScanCtx) => T | Promise<T>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** THE way to declare a scan. It reads through `c` (read, json, exists,
|
|
19
|
+
* list, use) and reports nothing: a finding belongs to the check that asked. */
|
|
20
|
+
export function defineScan<T>(name: string, run: (c: ScanCtx) => T | Promise<T>): Scan<T> {
|
|
21
|
+
if (name.trim() === '') throw new Error('defineScan: a scan needs a name, it is the memo key of the pass')
|
|
22
|
+
return { name, run }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── built-in scans ───────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** one markdown page, parsed */
|
|
28
|
+
export type MdPage = {
|
|
29
|
+
/** repo-relative */
|
|
30
|
+
path: string
|
|
31
|
+
/** the leading `---` block as flat `key: value` pairs. Nested yaml is out
|
|
32
|
+
* of scope: a value keeps the text after the first colon, trimmed */
|
|
33
|
+
frontmatter: Record<string, string>
|
|
34
|
+
/** the text after the frontmatter */
|
|
35
|
+
body: string
|
|
36
|
+
/** every markdown link target in the body, as written, anchors dropped.
|
|
37
|
+
* External (scheme or `//`), mailto and same-page anchors are left out */
|
|
38
|
+
links: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const FRONT = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/
|
|
42
|
+
const LINK = /\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g
|
|
43
|
+
|
|
44
|
+
export function parseMdPage(path: string, text: string): MdPage {
|
|
45
|
+
const m = FRONT.exec(text)
|
|
46
|
+
const frontmatter: Record<string, string> = {}
|
|
47
|
+
if (m != null)
|
|
48
|
+
for (const line of m[1]!.split(/\r?\n/)) {
|
|
49
|
+
const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
|
|
50
|
+
if (kv != null) frontmatter[kv[1]!] = kv[2]!.trim()
|
|
51
|
+
}
|
|
52
|
+
const body = m == null ? text : text.slice(m[0].length)
|
|
53
|
+
const links: string[] = []
|
|
54
|
+
for (const l of body.matchAll(LINK)) {
|
|
55
|
+
const target = l[1]!.split('#')[0]!
|
|
56
|
+
if (target === '' || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('//')) continue
|
|
57
|
+
links.push(target)
|
|
58
|
+
}
|
|
59
|
+
return { path, frontmatter, body, links }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pagesByDir = new Map<string, Scan<MdPage[]>>()
|
|
63
|
+
|
|
64
|
+
/** every `*.md` git sees under `dir` (repo-relative, `.` for the whole repo),
|
|
65
|
+
* parsed, sorted by path */
|
|
66
|
+
export function mdPages(dir: string): Scan<MdPage[]> {
|
|
67
|
+
const d = dir.replace(/\/+$/, '').replace(/^\.\//, '')
|
|
68
|
+
let s = pagesByDir.get(d)
|
|
69
|
+
if (s == null) {
|
|
70
|
+
const glob = d === '' || d === '.' ? '**/*.md' : `${d}/**/*.md`
|
|
71
|
+
s = defineScan(`mdPages:${d === '' ? '.' : d}`, (c) => c.list(glob).map((p) => parseMdPage(p, c.read(p) ?? '')))
|
|
72
|
+
pagesByDir.set(d, s)
|
|
73
|
+
}
|
|
74
|
+
return s
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** the fields of a package.json the built-in checks read */
|
|
78
|
+
export type PackageJson = {
|
|
79
|
+
name?: string
|
|
80
|
+
version?: string
|
|
81
|
+
private?: boolean
|
|
82
|
+
license?: string
|
|
83
|
+
scripts?: Record<string, string>
|
|
84
|
+
dependencies?: Record<string, string>
|
|
85
|
+
devDependencies?: Record<string, string>
|
|
86
|
+
peerDependencies?: Record<string, string>
|
|
87
|
+
optionalDependencies?: Record<string, string>
|
|
88
|
+
publishConfig?: unknown
|
|
89
|
+
[k: string]: unknown
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rootPackageJson = defineScan<PackageJson | null>('packageJson', (c) => c.json<PackageJson>('package.json'))
|
|
93
|
+
|
|
94
|
+
/** the repo root package.json, parsed. null when there is none, and an
|
|
95
|
+
* unparseable one throws naming the file */
|
|
96
|
+
export function packageJson(): Scan<PackageJson | null> {
|
|
97
|
+
return rootPackageJson
|
|
98
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// when the hub runs the quick gate on its own: once at start, then after the
|
|
2
|
+
// tracked files have been quiet for QUIET_MS. The trigger only STARTS ordinary
|
|
3
|
+
// gate runs. Every answer comes out of the one runner, measured or replayed
|
|
4
|
+
// from the ledger like any other run, so nothing here owns a verdict: a missed
|
|
5
|
+
// file event costs a stale problems pane until the next save, never a green.
|
|
6
|
+
//
|
|
7
|
+
// never two runs at once: a request during a run queues exactly one more, run
|
|
8
|
+
// after it ends, however many saves landed meanwhile.
|
|
9
|
+
import { watch, type FSWatcher } from 'fs'
|
|
10
|
+
import { GENERATED_IGNORE_LINES } from '../rules/untrack-generated.ts'
|
|
11
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
12
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
13
|
+
|
|
14
|
+
export const QUIET_MS = 2000
|
|
15
|
+
|
|
16
|
+
export type Timers = {
|
|
17
|
+
set: (fn: () => void, ms: number) => unknown
|
|
18
|
+
clear: (handle: unknown) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const realTimers: Timers = {
|
|
22
|
+
set: (fn, ms) => setTimeout(fn, ms),
|
|
23
|
+
clear: (h) => clearTimeout(h as ReturnType<typeof setTimeout>),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class GateTrigger {
|
|
27
|
+
private timer: unknown = null
|
|
28
|
+
private running: Promise<void> | null = null
|
|
29
|
+
private pending = false
|
|
30
|
+
private readonly timers: Timers
|
|
31
|
+
private readonly quietMs: number
|
|
32
|
+
|
|
33
|
+
constructor(private readonly o: { run: () => Promise<unknown>; quietMs?: number; timers?: Timers; onError?: (e: Error) => void }) {
|
|
34
|
+
this.timers = o.timers ?? realTimers
|
|
35
|
+
this.quietMs = o.quietMs ?? QUIET_MS
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** a tracked file moved: the run starts once nothing has moved for the
|
|
39
|
+
* quiet window, so a save burst is one run */
|
|
40
|
+
touched(): void {
|
|
41
|
+
if (this.timer != null) this.timers.clear(this.timer)
|
|
42
|
+
this.timer = this.timers.set(() => {
|
|
43
|
+
this.timer = null
|
|
44
|
+
this.request()
|
|
45
|
+
}, this.quietMs)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** run now, or once more after the run in flight */
|
|
49
|
+
request(): void {
|
|
50
|
+
if (this.running != null) { this.pending = true; return }
|
|
51
|
+
this.running = (async () => {
|
|
52
|
+
do {
|
|
53
|
+
this.pending = false
|
|
54
|
+
try { await this.o.run() } catch (e) { this.o.onError?.(e instanceof Error ? e : new Error(String(e))) }
|
|
55
|
+
} while (this.pending)
|
|
56
|
+
})().finally(() => { this.running = null })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** resolves when no run is in flight or queued */
|
|
60
|
+
async idle(): Promise<void> {
|
|
61
|
+
while (this.running != null) await this.running
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
stop(): void {
|
|
65
|
+
if (this.timer != null) this.timers.clear(this.timer)
|
|
66
|
+
this.timer = null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** every path a run of the gate or the hub itself writes, whatever the repo
|
|
71
|
+
* ignores: the status files, the per run logs under the scratch dir, an atomic write's
|
|
72
|
+
* temp file, the comfy state the hub writes at boot. A save there is the
|
|
73
|
+
* machine's own, and triggering on it would schedule the next run forever */
|
|
74
|
+
export const GATE_WRITES: readonly string[] = [
|
|
75
|
+
...GENERATED_IGNORE_LINES.filter((l) => !l.startsWith('#')),
|
|
76
|
+
`${LAYOUT.tmp}/`,
|
|
77
|
+
'.*.tmp-*',
|
|
78
|
+
'.comfy-ts/',
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
const globRe = (glob: string): RegExp => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')}$`)
|
|
82
|
+
const WRITE_RULES = GATE_WRITES.map((g) => (g.endsWith('/') ? { dir: g, re: null } : { dir: null, re: globRe(g) }))
|
|
83
|
+
|
|
84
|
+
/** true for a path the gate writes (GATE_WRITES), matched on its basename for
|
|
85
|
+
* a file pattern and on its prefix for a directory. A folder the gate creates
|
|
86
|
+
* on the way (`.shipkit` for `.shipkit/tmp/`) is one of its writes too: an
|
|
87
|
+
* owner file inside it fires its own event */
|
|
88
|
+
export function isGateWrite(rel: string): boolean {
|
|
89
|
+
const base = rel.split('/').at(-1) ?? rel
|
|
90
|
+
return WRITE_RULES.some((r) => (r.dir != null ? r.dir.startsWith(`${rel}/`) || rel.startsWith(r.dir) : r.re!.test(base)))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** which of these repo-relative paths git ignores. A tracked file is never
|
|
94
|
+
* reported, whatever the ignore rules say */
|
|
95
|
+
export async function gitIgnored(root: string, paths: string[]): Promise<Set<string>> {
|
|
96
|
+
if (paths.length === 0) return new Set()
|
|
97
|
+
try {
|
|
98
|
+
const p = Bun.spawn([gitBin(), 'check-ignore', '--stdin', '-z'], { cwd: root, stdin: 'pipe', stdout: 'pipe', stderr: 'ignore' })
|
|
99
|
+
p.stdin.write(paths.join('\0') + '\0')
|
|
100
|
+
await p.stdin.end()
|
|
101
|
+
const out = await new Response(p.stdout).text()
|
|
102
|
+
await p.exited
|
|
103
|
+
return new Set(out.split('\0').filter((s) => s !== ''))
|
|
104
|
+
} catch {
|
|
105
|
+
return new Set()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** calls onChange whenever a file the owner edits moves under root: tracked,
|
|
110
|
+
* or untracked and not ignored. What the gate itself writes (GATE_WRITES,
|
|
111
|
+
* the ledger in .git) never counts, ignored or not, so a run never triggers
|
|
112
|
+
* the next one */
|
|
113
|
+
export function watchTracked(root: string, onChange: () => void, o: { ignored?: (paths: string[]) => Promise<Set<string>>; onError?: (e: Error) => void } = {}): { close: () => void; error: string | null } {
|
|
114
|
+
const ignored = o.ignored ?? ((paths: string[]) => gitIgnored(root, paths))
|
|
115
|
+
const pending = new Set<string>()
|
|
116
|
+
let flushing = false
|
|
117
|
+
const flush = async (): Promise<void> => {
|
|
118
|
+
if (flushing) return
|
|
119
|
+
flushing = true
|
|
120
|
+
try {
|
|
121
|
+
while (pending.size > 0) {
|
|
122
|
+
const batch = [...pending]
|
|
123
|
+
pending.clear()
|
|
124
|
+
const skip = await ignored(batch)
|
|
125
|
+
if (batch.some((p) => !skip.has(p))) onChange()
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
flushing = false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let w: FSWatcher | null = null
|
|
132
|
+
let error: string | null = null
|
|
133
|
+
try {
|
|
134
|
+
w = watch(root, { recursive: true }, (_event, name) => {
|
|
135
|
+
if (name == null) return onChange()
|
|
136
|
+
const rel = String(name).replace(/\\/g, '/')
|
|
137
|
+
if (rel === '.git' || rel.startsWith('.git/') || rel.split('/').includes('node_modules') || isGateWrite(rel)) return
|
|
138
|
+
pending.add(rel)
|
|
139
|
+
void flush()
|
|
140
|
+
})
|
|
141
|
+
// a watcher that dies mid session (the tree was removed, a limit hit)
|
|
142
|
+
// says so, rather than crashing the hub or going quiet
|
|
143
|
+
w.on('error', (e) => o.onError?.(e instanceof Error ? e : new Error(String(e))))
|
|
144
|
+
} catch (e) {
|
|
145
|
+
error = e instanceof Error ? e.message : String(e)
|
|
146
|
+
}
|
|
147
|
+
return { close: () => w?.close(), error }
|
|
148
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// THE HEALTH PASS TIMER. While the hub serves, the whole health pass runs once
|
|
2
|
+
// a day, and once at startup when the last whole pass on record is older than
|
|
3
|
+
// that. One timer for the repo, no per check schedule: a check's own `ttl` is
|
|
4
|
+
// its cadence, and this pass is only the heartbeat that replays an answer
|
|
5
|
+
// still fresh and re-measures one that expired.
|
|
6
|
+
//
|
|
7
|
+
// The run is the hub's own in process runner, the same path as the button, so
|
|
8
|
+
// its rows land in the same run record. "When was the last pass" is read off
|
|
9
|
+
// that record, which is what makes a `shipkit check` in a terminal count too.
|
|
10
|
+
|
|
11
|
+
/** how often the health pass runs while the hub serves */
|
|
12
|
+
export const HEALTH_EVERY_MS = 24 * 60 * 60 * 1000
|
|
13
|
+
|
|
14
|
+
/** how often the timer looks. The pass itself is daily, this only bounds how
|
|
15
|
+
* late after its due time it starts */
|
|
16
|
+
export const SCHEDULE_LOOK_MS = 5 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
export type ScheduleState = {
|
|
19
|
+
everyMs: number
|
|
20
|
+
/** when the last whole health pass on record started, null = none */
|
|
21
|
+
lastPassAt: number | null
|
|
22
|
+
/** when the next one starts: now when it is overdue */
|
|
23
|
+
nextAt: number
|
|
24
|
+
/** the timer's own pass is running right now */
|
|
25
|
+
running: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ScheduleDeps = {
|
|
29
|
+
now: () => number
|
|
30
|
+
/** the last whole health pass on record, whoever ran it */
|
|
31
|
+
lastPassAt: () => number | null
|
|
32
|
+
/** another run in this process is going: the timer never stacks on it */
|
|
33
|
+
busy: () => boolean
|
|
34
|
+
/** the pass itself */
|
|
35
|
+
run: () => Promise<void>
|
|
36
|
+
everyMs?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TickResult = 'ran' | 'fresh' | 'busy'
|
|
40
|
+
|
|
41
|
+
export class HealthSchedule {
|
|
42
|
+
readonly everyMs: number
|
|
43
|
+
running = false
|
|
44
|
+
/** when this timer last STARTED a pass. A pass that dies before it writes
|
|
45
|
+
* a record would otherwise be due again at the very next look */
|
|
46
|
+
private attemptedAt: number | null = null
|
|
47
|
+
private timer: ReturnType<typeof setInterval> | null = null
|
|
48
|
+
|
|
49
|
+
constructor(private readonly d: ScheduleDeps) {
|
|
50
|
+
this.everyMs = d.everyMs ?? HEALTH_EVERY_MS
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** the newest of the record and this timer's own last attempt */
|
|
54
|
+
private lastAt(): number | null {
|
|
55
|
+
const rec = this.d.lastPassAt()
|
|
56
|
+
if (rec == null) return this.attemptedAt
|
|
57
|
+
return this.attemptedAt == null ? rec : Math.max(rec, this.attemptedAt)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
due(): boolean {
|
|
61
|
+
const last = this.lastAt()
|
|
62
|
+
return last == null || this.d.now() - last >= this.everyMs
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
state(): ScheduleState {
|
|
66
|
+
const now = this.d.now()
|
|
67
|
+
const last = this.lastAt()
|
|
68
|
+
return {
|
|
69
|
+
everyMs: this.everyMs,
|
|
70
|
+
lastPassAt: this.d.lastPassAt(),
|
|
71
|
+
nextAt: last == null ? now : Math.max(now, last + this.everyMs),
|
|
72
|
+
running: this.running,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** one look: run the pass when it is due and nothing else is running */
|
|
77
|
+
async tick(): Promise<TickResult> {
|
|
78
|
+
if (this.running || this.d.busy()) return 'busy'
|
|
79
|
+
if (!this.due()) return 'fresh'
|
|
80
|
+
this.running = true
|
|
81
|
+
this.attemptedAt = this.d.now()
|
|
82
|
+
try {
|
|
83
|
+
await this.d.run()
|
|
84
|
+
} finally {
|
|
85
|
+
this.running = false
|
|
86
|
+
}
|
|
87
|
+
return 'ran'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** look now, then every SCHEDULE_LOOK_MS. A second start is a no-op */
|
|
91
|
+
start(onError: (e: Error) => void, lookMs = SCHEDULE_LOOK_MS): void {
|
|
92
|
+
if (this.timer != null) return
|
|
93
|
+
const look = (): void => void this.tick().catch((e: Error) => onError(e))
|
|
94
|
+
look()
|
|
95
|
+
this.timer = setInterval(look, lookMs)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
stop(): void {
|
|
99
|
+
if (this.timer != null) clearInterval(this.timer)
|
|
100
|
+
this.timer = null
|
|
101
|
+
}
|
|
102
|
+
}
|