@orkestrel/scaffold 0.0.76 → 0.0.78

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.
Files changed (154) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +204 -0
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
  9. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
  10. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
  12. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
  13. package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
  14. package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
  15. package/dist/agents/templates/brief.md +55 -0
  16. package/dist/bin/main.js +4 -2
  17. package/dist/bin/main.js.map +1 -1
  18. package/dist/host/AGENTS.md +77 -135
  19. package/dist/host/agents/orchestration.md +147 -927
  20. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
  21. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  22. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
  23. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
  24. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
  25. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
  26. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
  27. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
  28. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  29. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
  30. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
  31. package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
  32. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
  33. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
  34. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
  35. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
  36. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
  37. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
  38. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
  42. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
  43. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
  44. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
  45. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
  46. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
  47. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
  48. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
  49. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
  50. package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
  51. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
  52. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
  53. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
  54. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
  55. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
  56. package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
  57. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
  58. package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
  59. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
  60. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
  61. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
  62. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
  63. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
  64. package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
  65. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
  66. package/dist/host/agents/templates/brief.md +21 -142
  67. package/dist/host/agents/transports/claude-cli.md +21 -0
  68. package/dist/host/agents/transports/codex.md +38 -159
  69. package/dist/host/agents/transports/cursor.md +16 -65
  70. package/dist/host/claude/AGENTS.md +38 -0
  71. package/dist/host/claude/agents/analyst.md +14 -53
  72. package/dist/host/claude/agents/astra.md +26 -0
  73. package/dist/host/claude/agents/builder.md +14 -30
  74. package/dist/host/claude/agents/checker.md +13 -57
  75. package/dist/host/claude/agents/distiller.md +11 -26
  76. package/dist/host/claude/agents/grok.md +12 -35
  77. package/dist/host/claude/agents/opus.md +14 -30
  78. package/dist/host/claude/agents/planner.md +10 -44
  79. package/dist/host/claude/agents/researcher.md +11 -30
  80. package/dist/host/claude/agents/reviewer.md +11 -95
  81. package/dist/host/claude/agents/scout.md +9 -23
  82. package/dist/host/claude/agents/verifier.md +15 -33
  83. package/dist/host/claude/rules/documentation.md +8 -2
  84. package/dist/host/claude/rules/portability.md +7 -1
  85. package/dist/host/claude/rules/quality.md +36 -96
  86. package/dist/host/claude/rules/styles.md +3 -0
  87. package/dist/host/claude/rules/tests.md +6 -3
  88. package/dist/host/claude/rules/workspace.md +19 -15
  89. package/dist/host/claude/rules/writing.md +57 -108
  90. package/dist/host/claude/settings.json +5 -3
  91. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
  92. package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
  93. package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
  94. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
  95. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
  96. package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
  97. package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
  98. package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
  99. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
  100. package/dist/host/codex/agents/analyst.toml +15 -32
  101. package/dist/host/codex/agents/astra.toml +25 -0
  102. package/dist/host/codex/agents/builder.toml +13 -20
  103. package/dist/host/codex/agents/checker.toml +13 -27
  104. package/dist/host/codex/agents/distiller.toml +9 -22
  105. package/dist/host/codex/agents/grok.toml +11 -30
  106. package/dist/host/codex/agents/opus.toml +14 -22
  107. package/dist/host/codex/agents/orkestrel.toml +1 -1
  108. package/dist/host/codex/agents/planner.toml +11 -28
  109. package/dist/host/codex/agents/researcher.toml +10 -22
  110. package/dist/host/codex/agents/reviewer.toml +11 -27
  111. package/dist/host/codex/agents/scout.toml +11 -17
  112. package/dist/host/codex/agents/verifier.toml +16 -12
  113. package/dist/host/codex/config.toml +20 -23
  114. package/dist/host/cursor/mcp.json +0 -4
  115. package/dist/host/cursor/rules/orchestration.mdc +12 -20
  116. package/dist/host/dotfiles/mcp.json +0 -4
  117. package/dist/host/dotfiles/oxlintrc.json +7 -0
  118. package/dist/host/guides/probe.md +9 -9
  119. package/dist/host/guides/scaffold.md +117 -71
  120. package/dist/host/guides/test.md +1 -1
  121. package/dist/host/manifest.json +322 -185
  122. package/dist/host/scripts/codex.sh +2 -2
  123. package/dist/host/tests/config.test.ts +68 -46
  124. package/dist/host/tests/policy.test.ts +1 -5
  125. package/dist/host/tests/setupPolicy.ts +179 -4
  126. package/dist/src/core/index.cjs +255 -84
  127. package/dist/src/core/index.cjs.map +1 -1
  128. package/dist/src/core/index.d.cts +94 -29
  129. package/dist/src/core/index.d.ts +94 -29
  130. package/dist/src/core/index.js +253 -85
  131. package/dist/src/core/index.js.map +1 -1
  132. package/dist/src/server/index.cjs +55 -9
  133. package/dist/src/server/index.cjs.map +1 -1
  134. package/dist/src/server/index.d.cts +29 -4
  135. package/dist/src/server/index.d.ts +29 -4
  136. package/dist/src/server/index.js +56 -11
  137. package/dist/src/server/index.js.map +1 -1
  138. package/package.json +15 -11
  139. package/dist/host/CLAUDE.md +0 -61
  140. package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
  141. package/dist/host/agents/transports/claude.md +0 -49
  142. package/dist/host/claude/agents/application.md +0 -36
  143. package/dist/host/claude/agents/sol.md +0 -61
  144. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
  145. package/dist/host/codex/agents/application.toml +0 -25
  146. package/dist/host/codex/agents/sol.toml +0 -19
  147. /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
  148. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
  149. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
  150. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
  151. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
  152. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
  153. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
  154. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
@@ -0,0 +1,220 @@
1
+ // Compare the rebuilt dist/ against the published tarball on material content. Run from the
2
+ // publishing package's root after its build:
3
+ // node .agents/skills/orkestrel-publish/scripts/compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]
4
+ // The package defaults to the manifest's name and the version to what the registry serves; --tarball
5
+ // compares against a local .tgz instead of fetching. Sourcemaps are excluded, a text file differs
6
+ // only when it differs with all whitespace removed, and a binary file differs on any byte. The
7
+ // summary lists the paths added, removed, and changed. Exit 0 when nothing material differs, 3 when
8
+ // something does, 2 when the tarball cannot be read, 64 on usage.
9
+ import { existsSync, readFileSync } from 'node:fs'
10
+ import { relative } from 'node:path'
11
+ import { gunzipSync } from 'node:zlib'
12
+ import {
13
+ listFiles,
14
+ readMissingFlags,
15
+ readOption,
16
+ runNpm,
17
+ } from '../../orkestrel-dispatch/scripts/helpers.ts'
18
+
19
+ const BLOCK = 512
20
+ const PACKAGE_PREFIX = 'package/'
21
+
22
+ interface Manifest {
23
+ readonly name: string | undefined
24
+ readonly version: string | undefined
25
+ }
26
+
27
+ function readManifest(): Manifest {
28
+ if (!existsSync('package.json')) return { name: undefined, version: undefined }
29
+ let parsed: unknown
30
+ try {
31
+ parsed = JSON.parse(readFileSync('package.json', 'utf8'))
32
+ } catch {
33
+ return { name: undefined, version: undefined }
34
+ }
35
+ if (typeof parsed !== 'object' || parsed === null) return { name: undefined, version: undefined }
36
+ const record = Object.fromEntries(Object.entries(parsed))
37
+ return {
38
+ name: typeof record.name === 'string' ? record.name : undefined,
39
+ version: typeof record.version === 'string' ? record.version : undefined,
40
+ }
41
+ }
42
+
43
+ function readField(text: string): string | undefined {
44
+ const stripped = text.replace(/^"|"$/gu, '')
45
+ return stripped === '' ? undefined : stripped
46
+ }
47
+
48
+ function readHeaderText(header: Buffer, start: number, length: number): string {
49
+ const slice = header.subarray(start, start + length)
50
+ const end = slice.indexOf(0)
51
+ return slice.subarray(0, end === -1 ? slice.length : end).toString('utf8')
52
+ }
53
+
54
+ function readPaxPath(data: Buffer): string | undefined {
55
+ let offset = 0
56
+ while (offset < data.length) {
57
+ const space = data.indexOf(0x20, offset)
58
+ if (space === -1) return undefined
59
+ const length = Number(data.subarray(offset, space).toString('utf8'))
60
+ if (!Number.isFinite(length) || length <= 0) return undefined
61
+ const record = data.subarray(space + 1, offset + length - 1).toString('utf8')
62
+ if (record.startsWith('path=')) return record.slice('path='.length)
63
+ offset += length
64
+ }
65
+ return undefined
66
+ }
67
+
68
+ function readTar(archive: Buffer): ReadonlyMap<string, Buffer> {
69
+ const files = new Map<string, Buffer>()
70
+ let offset = 0
71
+ let overrideName: string | undefined
72
+ while (offset + BLOCK <= archive.length) {
73
+ const header = archive.subarray(offset, offset + BLOCK)
74
+ if (header.every((byte) => byte === 0)) break
75
+ const size = Number.parseInt(readHeaderText(header, 124, 12).trim() || '0', 8)
76
+ const type = String.fromCharCode(header[156] ?? 0)
77
+ const magic = readHeaderText(header, 257, 6)
78
+ const prefix = magic.startsWith('ustar') ? readHeaderText(header, 345, 155) : ''
79
+ const shortName = readHeaderText(header, 0, 100)
80
+ const name = overrideName ?? (prefix === '' ? shortName : `${prefix}/${shortName}`)
81
+ const data = archive.subarray(offset + BLOCK, offset + BLOCK + size)
82
+ overrideName = undefined
83
+ if (type === 'x') overrideName = readPaxPath(data)
84
+ else if (type === 'L') overrideName = readHeaderText(data, 0, data.length)
85
+ else if (type === '0' || type === '\0') files.set(name, Buffer.from(data))
86
+ offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK
87
+ }
88
+ return files
89
+ }
90
+
91
+ // A dist tree holds nothing a walk skips, so every emitted file counts.
92
+ const NOTHING_SKIPPED: ReadonlySet<string> = new Set()
93
+
94
+ function readNpmValue(args: readonly string[]): string | undefined {
95
+ const result = runNpm(args)
96
+ return result.status === 0 ? readField(result.stdout.trim()) : undefined
97
+ }
98
+
99
+ function isMaterial(path: string): boolean {
100
+ return !path.endsWith('.map')
101
+ }
102
+
103
+ function differs(left: Buffer, right: Buffer): boolean {
104
+ if (left.includes(0) || right.includes(0)) return !left.equals(right)
105
+ return left.toString('utf8').replace(/\s+/gu, '') !== right.toString('utf8').replace(/\s+/gu, '')
106
+ }
107
+
108
+ async function main(argv: readonly string[]): Promise<number> {
109
+ const missing = readMissingFlags(argv, ['--package', '--version', '--tarball', '--dist'])
110
+ if (missing.length > 0) {
111
+ console.error(`compare: ${missing.join(', ')} given with no value`)
112
+ return 64
113
+ }
114
+ const manifest = readManifest()
115
+ const name = readOption(argv, '--package') ?? manifest.name
116
+ const dist = readOption(argv, '--dist') ?? 'dist'
117
+ const tarball = readOption(argv, '--tarball')
118
+ if (
119
+ name === undefined ||
120
+ !existsSync(dist) ||
121
+ (argv.includes('--tarball') && tarball === undefined)
122
+ ) {
123
+ console.error(
124
+ 'usage: compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]',
125
+ )
126
+ return 64
127
+ }
128
+ let version = readOption(argv, '--version')
129
+ let archive: Buffer
130
+ if (tarball !== undefined) {
131
+ if (!existsSync(tarball)) {
132
+ console.error(`compare: ${tarball} does not exist`)
133
+ return 2
134
+ }
135
+ archive = readFileSync(tarball)
136
+ } else {
137
+ version ??= readNpmValue(['view', name, 'version', '--json'])
138
+ const url =
139
+ version === undefined
140
+ ? undefined
141
+ : readNpmValue(['view', `${name}@${version}`, 'dist.tarball', '--json'])
142
+ if (version === undefined || url === undefined) {
143
+ console.error(
144
+ `compare: the registry served no tarball for ${name}${version === undefined ? '' : `@${version}`}`,
145
+ )
146
+ return 2
147
+ }
148
+ const fetched = await fetchTarball(url)
149
+ if (fetched === undefined) {
150
+ console.error(`compare: fetching ${url} failed`)
151
+ return 2
152
+ }
153
+ archive = fetched
154
+ }
155
+ const published = new Map<string, Buffer>()
156
+ let entries: ReadonlyMap<string, Buffer>
157
+ try {
158
+ entries = readTar(gunzipSync(archive))
159
+ } catch (error) {
160
+ console.error(
161
+ `compare: the tarball could not be read: ${error instanceof Error ? error.message : String(error)}`,
162
+ )
163
+ return 2
164
+ }
165
+ for (const [path, content] of entries) {
166
+ const prefixed = `${PACKAGE_PREFIX}${dist}/`
167
+ if (path.startsWith(prefixed) && isMaterial(path))
168
+ published.set(path.slice(prefixed.length), content)
169
+ }
170
+ const local = new Map<string, Buffer>()
171
+ for (const { path } of listFiles(dist, NOTHING_SKIPPED)) {
172
+ const key = relative(dist, path).split('\\').join('/')
173
+ if (isMaterial(key)) local.set(key, readFileSync(path))
174
+ }
175
+ const added = [...local.keys()].filter((key) => !published.has(key)).sort()
176
+ const removed = [...published.keys()].filter((key) => !local.has(key)).sort()
177
+ const changed = [...local.keys()]
178
+ .filter((key) => {
179
+ const other = published.get(key)
180
+ const mine = local.get(key)
181
+ return other !== undefined && mine !== undefined && differs(mine, other)
182
+ })
183
+ .sort()
184
+ const material = added.length + removed.length + changed.length > 0
185
+ const summary = {
186
+ package: name,
187
+ version,
188
+ tarball,
189
+ dist,
190
+ compared: published.size,
191
+ added,
192
+ removed,
193
+ changed,
194
+ material,
195
+ }
196
+ if (argv.includes('--json')) console.log(JSON.stringify(summary))
197
+ else {
198
+ console.log(
199
+ `compare: ${name}${version === undefined ? '' : `@${version}`} — ${published.size} published file(s), ${added.length} added, ${removed.length} removed, ${changed.length} changed`,
200
+ )
201
+ for (const path of added) console.log(`compare: added ${path}`)
202
+ for (const path of removed) console.log(`compare: removed ${path}`)
203
+ for (const path of changed) console.log(`compare: changed ${path}`)
204
+ }
205
+ return material ? 3 : 0
206
+ }
207
+
208
+ async function fetchTarball(url: string): Promise<Buffer | undefined> {
209
+ try {
210
+ const response = await fetch(url)
211
+ if (!response.ok) return undefined
212
+ return Buffer.from(await response.arrayBuffer())
213
+ } catch {
214
+ return undefined
215
+ }
216
+ }
217
+
218
+ main(process.argv.slice(2)).then((code) => {
219
+ process.exitCode = code
220
+ })
@@ -0,0 +1,114 @@
1
+ // Sweep source and tests for a prior version or range literal after a bump or a re-pin. Run from
2
+ // the publishing package's root:
3
+ // node .agents/skills/orkestrel-publish/scripts/pins.ts --version 0.0.76 [--range ^0.0.16 ...] [--paths src,tests] [--json]
4
+ // `--range` repeats. A version hit is the literal bounded by characters other than digits and dots,
5
+ // so `~0.0.76` and `"0.0.76"` both hit and `0.0.760` does not. A range that is one comparator over
6
+ // one version (`^8.3.0`, `~8.3.0`, `>=8.3.0`, `v8.3.0`) hits its bare version in any form, bounded the
7
+ // same way, so `--range ^8.3.0` hits `~8.3.0` and `serves 8.3.0` and misses `^8.3.1`; any other
8
+ // range (`>=1.2.0 <2.0.0`, `8.x`) hits its literal bounded the same way and not preceded by another
9
+ // range operator. Each matching line prints once as path:line: text, naming the first requested
10
+ // literal that matched in the order --version then each --range, so the operator rules on it.
11
+ // Exit 0 with no hits, 3 with hits, 64 on usage.
12
+ import { readFileSync, statSync } from 'node:fs'
13
+ import {
14
+ listFiles,
15
+ readMissingFlags,
16
+ readOptions,
17
+ } from '../../orkestrel-dispatch/scripts/helpers.ts'
18
+
19
+ const BINARY = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|zip|tgz|gz|pdf|wasm)$/u
20
+ const COMPARATOR =
21
+ /^(?:\^|~|>=|>|<=|<|=)?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$/u
22
+
23
+ interface Literal {
24
+ readonly literal: string
25
+ readonly pattern: RegExp
26
+ }
27
+
28
+ interface Hit {
29
+ readonly path: string
30
+ readonly line: number
31
+ readonly literal: string
32
+ readonly text: string
33
+ }
34
+
35
+ function escapePattern(literal: string): string {
36
+ return literal.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
37
+ }
38
+
39
+ function buildVersionPattern(version: string): RegExp {
40
+ return new RegExp(`(?<![0-9.])${escapePattern(version)}(?![0-9.])`, 'u')
41
+ }
42
+
43
+ function buildVersionLiteral(literal: string): Literal {
44
+ return { literal, pattern: buildVersionPattern(literal) }
45
+ }
46
+
47
+ function buildRangeLiteral(literal: string): Literal {
48
+ const version = COMPARATOR.exec(literal)?.[1]
49
+ if (version !== undefined) return { literal, pattern: buildVersionPattern(version) }
50
+ return {
51
+ literal,
52
+ pattern: new RegExp(`(?<![0-9.^~<>=])${escapePattern(literal)}(?![0-9.])`, 'u'),
53
+ }
54
+ }
55
+
56
+ function sweepFile(path: string, literals: readonly Literal[]): readonly Hit[] {
57
+ const content = readFileSync(path)
58
+ if (content.includes(0)) return []
59
+ const hits: Hit[] = []
60
+ content
61
+ .toString('utf8')
62
+ .split(/\r\n|\n/)
63
+ .forEach((text, index) => {
64
+ const match = literals.find((entry) => entry.pattern.test(text))
65
+ if (match !== undefined) {
66
+ hits.push({ path, line: index + 1, literal: match.literal, text: text.trim() })
67
+ }
68
+ })
69
+ return hits
70
+ }
71
+
72
+ function main(argv: readonly string[]): number {
73
+ const missing = readMissingFlags(argv, ['--version', '--range', '--paths'])
74
+ if (missing.length > 0) {
75
+ console.error(`pins: ${missing.join(', ')} given with no value`)
76
+ return 64
77
+ }
78
+ const version = readOptions(argv, '--version')[0]
79
+ const ranges = readOptions(argv, '--range')
80
+ const paths = (readOptions(argv, '--paths')[0] ?? 'src,tests').split(',')
81
+ if (version === undefined && ranges.length === 0) {
82
+ console.error(
83
+ 'usage: pins.ts --version PRIOR [--range PRIOR_RANGE ...] [--paths src,tests] [--json]',
84
+ )
85
+ return 64
86
+ }
87
+ const literals = [
88
+ ...(version === undefined ? [] : [buildVersionLiteral(version)]),
89
+ ...ranges.map(buildRangeLiteral),
90
+ ]
91
+ const present = paths.filter((path) => {
92
+ try {
93
+ return statSync(path).isDirectory()
94
+ } catch {
95
+ return false
96
+ }
97
+ })
98
+ const skipped = paths.filter((path) => !present.includes(path))
99
+ for (const path of skipped) console.error(`pins: ${path} is not a directory; skipped`)
100
+ const hits = present
101
+ .flatMap((directory) => listFiles(directory).map((file) => file.path))
102
+ .filter((file) => !BINARY.test(file))
103
+ .flatMap((file) => sweepFile(file, literals))
104
+ const names = literals.map((entry) => entry.literal)
105
+ if (argv.includes('--json')) {
106
+ console.log(JSON.stringify({ literals: names, paths: present, skipped, hits }))
107
+ } else {
108
+ for (const hit of hits) console.log(`${hit.path}:${hit.line}: ${hit.text}`)
109
+ console.log(`pins: ${hits.length} hit(s) for ${names.join(', ')} under ${present.join(', ')}`)
110
+ }
111
+ return hits.length === 0 ? 0 : 3
112
+ }
113
+
114
+ process.exitCode = main(process.argv.slice(2))