rvlib-shipkit 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +2 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/assets/kits/identity-kit.ts +7 -3
  12. package/src/assets/run-assets.ts +18 -4
  13. package/src/checks/SK004-gitignore-policy.check.ts +11 -6
  14. package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
  15. package/src/cli/cmd_Calls.ts +53 -0
  16. package/src/cli/cmd_Commands.ts +163 -43
  17. package/src/cli/cmd_Review.ts +948 -0
  18. package/src/cli/cmd_Shot.ts +63 -0
  19. package/src/cli/cmd_Start.ts +8 -4
  20. package/src/cli/cmd_Time.ts +112 -0
  21. package/src/cli/cmd_Trailer.ts +361 -0
  22. package/src/cli/kit/builtins.ts +40 -0
  23. package/src/cli/kit/calls.ts +177 -0
  24. package/src/cli/kit/families.ts +67 -0
  25. package/src/cli/kit/index.ts +8 -0
  26. package/src/cli/kit/pages-check.ts +36 -0
  27. package/src/cli/kit/project-cli.ts +73 -0
  28. package/src/cli/kit/verb.ts +167 -0
  29. package/src/cli.ts +15 -2
  30. package/src/collections/collection-types.ts +6 -1
  31. package/src/collections/providers.ts +3 -0
  32. package/src/collections/review.ts +70 -0
  33. package/src/core/layout.ts +12 -2
  34. package/src/core/migrate-layout.ts +31 -8
  35. package/src/core/one-way-in-policy.ts +12 -4
  36. package/src/reflect/prompt-corpus.ts +15 -114
  37. package/src/reflect/transcript.ts +254 -0
  38. package/src/review/anim.ts +228 -0
  39. package/src/review/auto.ts +141 -0
  40. package/src/review/format.ts +150 -0
  41. package/src/review/http.ts +23 -0
  42. package/src/review/marks.ts +43 -0
  43. package/src/review/media.ts +45 -0
  44. package/src/review/model.ts +854 -0
  45. package/src/review/project.ts +110 -0
  46. package/src/review/role.ts +48 -0
  47. package/src/review/routes.ts +228 -0
  48. package/src/review/store.ts +1002 -0
  49. package/src/review/threads.ts +237 -0
  50. package/src/review/trailer-desk.ts +385 -0
  51. package/src/review/trailer-model.ts +183 -0
  52. package/src/review/trailer-routes.ts +136 -0
  53. package/src/review/unit.ts +165 -0
  54. package/src/review/watchers.ts +54 -0
  55. package/src/scaffold/cli-skeleton.ts +149 -0
  56. package/src/scaffold/files/agent-index.md +2 -2
  57. package/src/scaffold/files/tsconfig.json +3 -0
  58. package/src/scaffold/files/verify-skill.md +1 -1
  59. package/src/scaffold/project-scaffold.ts +90 -12
  60. package/src/serve/embed.ts +35 -0
  61. package/src/serve/hub-app/activity-view.tsx +1 -1
  62. package/src/serve/hub-app/activity.css +0 -4
  63. package/src/serve/hub-app/dock-view.tsx +5 -4
  64. package/src/serve/hub-app/hash-route.ts +28 -1
  65. package/src/serve/hub-app/header-bar.tsx +3 -1
  66. package/src/serve/hub-app/home-st.ts +10 -2
  67. package/src/serve/hub-app/home-strip.tsx +3 -2
  68. package/src/serve/hub-app/home.html +1 -0
  69. package/src/serve/hub-app/hub-app.tsx +80 -11
  70. package/src/serve/hub-app/hub.css +45 -13
  71. package/src/serve/hub-app/icons.tsx +9 -0
  72. package/src/serve/hub-app/index.html +1 -0
  73. package/src/serve/hub-app/insights-view.tsx +4 -3
  74. package/src/serve/hub-app/keys.ts +14 -10
  75. package/src/serve/hub-app/layout-st.ts +63 -2
  76. package/src/serve/hub-app/planned-view.tsx +3 -2
  77. package/src/serve/hub-app/planned.css +0 -1
  78. package/src/serve/hub-app/project-list.css +22 -4
  79. package/src/serve/hub-app/project-list.tsx +41 -8
  80. package/src/serve/hub-app/projects-svc.ts +4 -0
  81. package/src/serve/hub-app/repo-sections.ts +15 -3
  82. package/src/serve/hub-app/repo-st.ts +45 -3
  83. package/src/serve/hub-app/review/clip-st.ts +208 -0
  84. package/src/serve/hub-app/review/kind-view.tsx +171 -0
  85. package/src/serve/hub-app/review/kit.tsx +113 -0
  86. package/src/serve/hub-app/review/review-answer.tsx +239 -0
  87. package/src/serve/hub-app/review/review-board.tsx +46 -0
  88. package/src/serve/hub-app/review/review-list.tsx +156 -0
  89. package/src/serve/hub-app/review/review-page.tsx +101 -0
  90. package/src/serve/hub-app/review/review-st.ts +779 -0
  91. package/src/serve/hub-app/review/review-views.tsx +503 -0
  92. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  93. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  94. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  95. package/src/serve/hub-app/review.css +329 -0
  96. package/src/serve/hub-app/theme-st.ts +56 -0
  97. package/src/serve/hub-app/tree-build.ts +4 -0
  98. package/src/serve/hub-app/tree-row.tsx +1 -1
  99. package/src/serve/hub-app/tree-svc.ts +4 -0
  100. package/src/serve/hub-runtime.ts +5 -3
  101. package/src/serve/same-origin.ts +5 -1
  102. package/src/serve/serve-hub.ts +8 -0
  103. package/src/shipkit-files.ts +12 -4
  104. package/src/shot/client.ts +67 -0
  105. package/src/shot/clip-encoder.ts +146 -0
  106. package/src/shot/dom-clip.ts +124 -0
  107. package/src/shot/serve-shot.ts +54 -0
  108. package/src/shot/shooter.ts +343 -0
  109. package/src/shot/shot-ask.ts +189 -0
  110. package/src/shot/shot.shipkit.ts +14 -0
  111. package/src/smoke/run-smoke.ts +2 -2
  112. package/src/start-repo.ts +29 -14
  113. package/src/suite/judge-suites.ts +56 -10
  114. package/src/suite/select.ts +2 -5
  115. package/src/suite/suite-cli.ts +3 -2
  116. package/src/trailer/adapter.ts +78 -0
  117. package/src/trailer/build.ts +603 -0
  118. package/src/trailer/ffmpeg.ts +72 -0
  119. package/src/trailer/footage.ts +173 -0
  120. package/src/trailer/grammar.ts +1290 -0
  121. package/src/trailer/graph.ts +508 -0
  122. package/src/trailer/holes.ts +72 -0
  123. package/src/trailer/load-unit.ts +31 -0
  124. package/src/trailer/look.ts +124 -0
  125. package/src/trailer/passage.ts +129 -0
  126. package/src/trailer/plan.ts +192 -0
  127. package/src/trailer/prune.ts +193 -0
  128. package/src/trailer/score/compose.ts +240 -0
  129. package/src/trailer/score/dsp.ts +272 -0
  130. package/src/trailer/score/palette.ts +188 -0
  131. package/src/trailer/score/pattern.ts +91 -0
  132. package/src/trailer/score/score.ts +214 -0
  133. package/src/trailer/score/sound.ts +112 -0
  134. package/src/trailer/score/synth.ts +361 -0
  135. package/src/trailer/states.ts +76 -0
  136. package/src/trailer/stitch.ts +233 -0
  137. package/src/trailer/titles.ts +379 -0
  138. package/src/trailer/url.ts +108 -0
  139. package/src/trailer/vertical-draw.ts +286 -0
  140. package/src/trailer/vertical.ts +148 -0
  141. package/src/trailer/vocab.ts +59 -0
  142. package/src/trailer/words.ts +254 -0
@@ -1,8 +1,11 @@
1
1
  // the binary describing itself, in markdown. a hand-copied command list
2
2
  // drifts the moment a flag changes, and then teaches commands that do not
3
- // exist. rendering the live registry makes the doc a build artifact.
3
+ // exist. rendering the live registry makes the doc a build artifact. One
4
+ // renderer for every binary: shipkit's own page and each project CLI's pages
5
+ // (the kit in cli/kit/) come out of these functions.
4
6
  import { Command, Option } from 'clipanion'
5
7
  import { spliceMarkedBlock } from '../core/doc-block.ts'
8
+ import { type Essential, type Film, marksOf, ROW_OF, runText } from './kit/verb.ts'
6
9
 
7
10
  export type CommandOption = { definition: string; description: string }
8
11
  export type CommandRow = {
@@ -12,6 +15,15 @@ export type CommandRow = {
12
15
  usage: string
13
16
  options: CommandOption[]
14
17
  examples: [string, string][]
18
+ /** a filmable command (cli/kit/verb.ts): takes --out, a clip also --seconds */
19
+ film?: Film
20
+ /** a filmable command's take version and length (build/trailer.md, Film lines) */
21
+ version?: number | string
22
+ length?: 'own'
23
+ /** listed on the essentials page */
24
+ essential?: Essential
25
+ /** what a kit row runs */
26
+ runs?: string
15
27
  }
16
28
 
17
29
  export type Definition = {
@@ -30,68 +42,148 @@ export const DESCRIPTION_MAX = 100
30
42
  // clipanion renders a value placeholder as `#0`, which nobody can type
31
43
  const clean = (s: string | null | undefined): string => (s ?? '').trim().replace(/\s+/g, ' ').replace(/ #\d+\b/g, ' <value>')
32
44
 
45
+ /** the names a binary answers to: shipkit is also `sk` */
46
+ const namesOf = (bin: string): string[] => (bin === 'sk' || bin === 'shipkit' ? ['shipkit', 'sk'] : [bin])
47
+
48
+ const stripName = (bin: string, path: string): string => {
49
+ for (const n of namesOf(bin)) if (path.startsWith(`${n} `)) return path.slice(n.length + 1)
50
+ return path
51
+ }
52
+
33
53
  /** rows from the live registry, builtins dropped, sorted by category then path.
34
54
  * clipanion splits what we need across two calls: the plain form carries the
35
55
  * examples, the detailed form carries the flags. Merge by path, or the page
36
56
  * ends up showing a command with no arguments, which is the state that sends
37
- * an agent to `--help` before it can act. */
38
- export function commandRows(plain: Definition[], detailed: Definition[] = []): CommandRow[] {
57
+ * an agent to `--help` before it can act. `classes` (the registered command
58
+ * classes) add each command's marks: film, essential, what a row runs */
59
+ export function commandRows(plain: Definition[], detailed: Definition[] = [], bin = 'sk', classes: readonly { paths?: string[][] }[] = []): CommandRow[] {
39
60
  const flagsByPath = new Map(detailed.map((d) => [d.path, d.options ?? []]))
61
+ const classByPath = new Map<string, object>()
62
+ for (const c of classes) for (const p of c.paths ?? []) classByPath.set(p.join(' '), c)
40
63
  return plain
41
- .filter((d) => !/^(shipkit|sk) (--help|-h|--version|-v)$/.test(d.path))
42
- .map((d) => ({
43
- path: d.path.replace(/^(shipkit|sk) /, ''),
44
- category: clean(d.category) || 'Other',
45
- // verbatim from the CLI so the doc cannot say something it does not
46
- description: clean(d.description),
47
- usage: clean(d.usage),
48
- options: (flagsByPath.get(d.path) ?? [])
49
- .map((o) => ({ definition: clean(o.definition), description: clean(o.description) }))
50
- .filter((o) => o.definition !== ''),
51
- examples: (d.examples ?? []).map(([label, cmd]) => [clean(label), clean(cmd)] as [string, string]),
52
- }))
64
+ .filter((d) => !/^(--help|-h|--version|-v)$/.test(stripName(bin, d.path)))
65
+ .map((d) => {
66
+ const path = stripName(bin, d.path)
67
+ const c = classByPath.get(path)
68
+ const row: CommandRow = {
69
+ path,
70
+ category: clean(d.category) || 'Other',
71
+ // verbatim from the CLI so the doc cannot say something it does not
72
+ description: clean(d.description),
73
+ usage: clean(d.usage),
74
+ options: (flagsByPath.get(d.path) ?? [])
75
+ .map((o) => ({ definition: clean(o.definition), description: clean(o.description) }))
76
+ .filter((o) => o.definition !== ''),
77
+ examples: (d.examples ?? []).map(([label, cmd]) => [clean(label), clean(cmd)] as [string, string]),
78
+ }
79
+ if (c != null) {
80
+ const m = marksOf(c)
81
+ if (m.film != null) row.film = m.film
82
+ if (m.version != null) row.version = m.version
83
+ if (m.length != null) row.length = m.length
84
+ if (m.essential != null) row.essential = m.essential
85
+ const v = ROW_OF.get(c)
86
+ if (v != null) row.runs = runText(v.run)
87
+ }
88
+ return row
89
+ })
53
90
  .sort((a, b) => a.category.localeCompare(b.category) || a.path.localeCompare(b.path))
54
91
  }
55
92
 
56
- /** the markdown block, loaded into every agent's context at start: per
57
- * command its invocation with every flag, one line of description, then its
58
- * examples. What a flag does and the long text are `sk <command> --help`. */
59
- export function renderMarkdown(rows: CommandRow[]): string {
93
+ /** a command's invocation as an agent types it */
94
+ const signature = (r: CommandRow, bin: string): string => r.usage.replace(/^\$0/, bin).trim() || `${bin} ${r.path}`
95
+
96
+ /** the markdown block: per command its invocation with every flag, one line
97
+ * of description, what a row runs, then its examples. What a flag does and
98
+ * the long text are `<bin> <command> --help`. */
99
+ export function renderMarkdown(rows: CommandRow[], bin = 'sk'): string {
60
100
  const out: string[] = []
61
101
  let cat = ''
62
102
  for (const r of rows) {
63
103
  if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}\n`) }
64
- const sig = r.usage.replace(/^\$0/, 'sk').trim() || `sk ${r.path}`
65
104
  const flags = r.options.map((o) => `[${o.definition}]`).join(' ')
105
+ const sig = signature(r, bin)
66
106
  // one line per bullet: a description on its own indented line reads as
67
107
  // hard-wrapped prose to the house lint
68
- out.push(`- \`${sig}${flags === '' ? '' : ` ${flags}`}\` — ${r.description}`)
108
+ out.push(`- \`${sig}${flags === '' ? '' : ` ${flags}`}\` — ${r.description}${r.runs != null ? ` → \`${r.runs}\`` : ''}${r.film != null ? ` (films a ${r.film})` : ''}`)
69
109
  for (const [label, cmd] of r.examples) out.push(` - ${label}: \`${cmd}\``)
70
110
  }
71
111
  return out.join('\n').trim()
72
112
  }
73
113
 
74
114
  /** the eager form: one line per command, its positional arguments and its
75
- * description. Flags and examples are `sk <command> --help`, so a page every
115
+ * description. Flags and examples are `<bin> <command> --help`, so a page every
76
116
  * session loads costs a line per command, not a screen */
77
- export function renderCompact(rows: CommandRow[]): string {
117
+ export function renderCompact(rows: CommandRow[], bin = 'sk'): string {
78
118
  const out: string[] = []
79
119
  let cat = ''
80
120
  for (const r of rows) {
81
121
  if (r.category !== cat) { cat = r.category; out.push(`\n### ${cat}\n`) }
82
- const sig = r.usage.replace(/^\$0/, 'sk').replace(/\s+\[(?:-|<\w+>)[^\]]*\]/g, '').trim() || `sk ${r.path}`
122
+ const sig = r.usage.replace(/^\$0/, bin).replace(/\s+\[(?:-|<\w+>)[^\]]*\]/g, '').trim() || `${bin} ${r.path}`
83
123
  out.push(`- \`${sig}\` — ${r.description}`)
84
124
  }
85
125
  return out.join('\n').trim()
86
126
  }
87
127
 
88
- export const BEGIN = '<!-- shipkit:commands -->'
89
- export const END = '<!-- /shipkit:commands -->'
128
+ /** the page every session loads: per category its everyday commands, a few
129
+ * examples, and how to list the rest. Generated whole */
130
+ export function renderEssentials(rows: CommandRow[], bin: string, command: string): string {
131
+ const out = [`# ${bin}: the everyday commands`, '', `Generated by \`${command}\` from the commands marked \`essential\`. Every command: \`${bin}\`, \`${bin} <family>\`, \`${bin} <command> --help\`. \`${bin} <words>\` runs as \`bun run cli <words>\` from the repo root, and as \`${bin}\` anywhere once \`bun link\` ran there.`]
132
+ const first = (r: CommandRow): string => r.path.split(' ')[0]!
133
+ const quiet: string[] = []
134
+ for (const category of [...new Set(rows.map((r) => r.category))]) {
135
+ const all = rows.filter((r) => r.category === category)
136
+ const rest = [...new Set(all.map(first))]
137
+ .map((w) => [w, all.filter((r) => first(r) === w && r.essential == null).length] as const)
138
+ .filter(([, n]) => n > 0)
139
+ .map(([w, n]) => `\`${bin} ${w}${all.some((r) => r.path === w) ? ' --help' : ''}\` (${n} command${n === 1 ? '' : 's'})`)
140
+ const mine = all.filter((r) => r.essential != null)
141
+ if (mine.length === 0) { quiet.push(...rest); continue }
142
+ out.push('', `## ${category}${rest.length > 0 ? ` (more: ${rest.join(', ')})` : ''}`, '')
143
+ for (const r of mine) {
144
+ const e = r.essential!
145
+ out.push(`- \`${bin} ${r.path}${e.args != null ? ` ${e.args}` : ''}\`: ${r.description}`)
146
+ for (const [label, cmd] of r.examples.slice(0, e.examples ?? 0)) out.push(` - \`${cmd}\`: ${label}`)
147
+ }
148
+ }
149
+ if (quiet.length > 0) out.push('', `## The other families: ${quiet.join(', ')}`)
150
+ return `${out.join('\n')}\n`
151
+ }
152
+
153
+ /** what a project CLI's registry must hold for its pages and its listing to
154
+ * teach right: a category, a description under the cap, and an example that
155
+ * starts with the command's own words. One line per breach */
156
+ export function shapeProblems(rows: CommandRow[], bin: string): string[] {
157
+ const out: string[] = []
158
+ for (const r of rows) {
159
+ if (r.category === 'Other') out.push(`${bin} ${r.path}: no category`)
160
+ if (r.description === '') out.push(`${bin} ${r.path}: no description`)
161
+ else if (r.description.length > DESCRIPTION_MAX) out.push(`${bin} ${r.path}: description over ${DESCRIPTION_MAX} characters (the rest goes in details)`)
162
+ if (!r.examples.some(([, cmd]) => cmd === `${bin} ${r.path}` || cmd.startsWith(`${bin} ${r.path} `))) out.push(`${bin} ${r.path}: no example starting with \`${bin} ${r.path}\``)
163
+ }
164
+ return out
165
+ }
166
+
167
+ /** the markers a page carries around the generated block: shipkit keeps its
168
+ * historical name, a project CLI uses its own */
169
+ export const markersFor = (bin: string): [string, string] => {
170
+ const n = namesOf(bin).includes('shipkit') ? 'shipkit' : bin
171
+ return [`<!-- ${n}:commands -->`, `<!-- /${n}:commands -->`]
172
+ }
173
+
174
+ export const [BEGIN, END] = markersFor('sk')
90
175
 
91
176
  /** replace the marked block in a host file. Returns null when the markers are
92
177
  * missing, so a renamed or hand-mangled host fails loud instead of silently
93
178
  * writing nothing. */
94
- export const spliceBlock = (host: string, block: string): string | null => spliceMarkedBlock(host, block, BEGIN, END)
179
+ export const spliceBlock = (host: string, block: string, bin = 'sk'): string | null => spliceMarkedBlock(host, block, ...markersFor(bin))
180
+
181
+ /** the page a project CLI's `--out` creates when it is missing: a title, how
182
+ * it is made, and the markers the block goes between */
183
+ export const newCommandsPage = (bin: string, path: string): string => {
184
+ const [begin, end] = markersFor(bin)
185
+ return `# ${bin}: every command\n\nGenerated between the markers by \`${bin} commands --out ${path}\` from the registry. \`${bin}\` lists every command, \`${bin} <command> --help\` prints one.\n\n${begin}\n${end}\n`
186
+ }
95
187
 
96
188
  export class CommandsCommand extends Command {
97
189
  static override paths = [['commands']]
@@ -109,33 +201,61 @@ export class CommandsCommand extends Command {
109
201
  ],
110
202
  })
111
203
 
112
- json = Option.Boolean('--json', false, { description: 'machine-readable [{path, category, description}]' })
113
- out = Option.String('--out', { description: 'host file carrying the shipkit:commands markers. The block between them is rewritten' })
114
- check = Option.Boolean('--check', false, { description: 'with --out: exit 1 if the block is stale, write nothing' })
115
- compact = Option.Boolean('--compact', false, { description: 'one line per command: no flags, no examples (they are `sk <command> --help`)' })
204
+ /** a project CLI holds its rows to shapeProblems (--out and --check fail on
205
+ * a breach), and its --out creates a missing page */
206
+ static shapeRule = false
207
+
208
+ json = Option.Boolean('--json', false, { description: 'machine-readable rows, with film and essential where a command carries them' })
209
+ out = Option.String('--out', { description: 'host file carrying the <bin>:commands markers. The block between them is rewritten' })
210
+ check = Option.Boolean('--check', false, { description: 'with --out or --essentials: exit 1 if a page is stale, write nothing' })
211
+ compact = Option.Boolean('--compact', false, { description: 'one line per command: no flags, no examples (they are `<bin> <command> --help`)' })
212
+ essentials = Option.String('--essentials', { description: 'the everyday commands page, rewritten whole from the commands marked essential' })
116
213
 
117
214
  async execute(): Promise<number> {
215
+ const bin = this.cli.binaryName
216
+ const classes = (this.context as { classes?: { paths?: string[][] }[] }).classes ?? []
118
217
  const rows = commandRows(
119
218
  this.cli.definitions() as Definition[],
120
219
  (this.cli.definitions as (o?: { detailed?: boolean }) => Definition[])({ detailed: true }),
220
+ bin,
221
+ classes,
121
222
  )
122
- if (this.out == null) {
123
- this.context.stdout.write(this.json ? `${JSON.stringify(rows)}\n` : `${(this.compact ? renderCompact : renderMarkdown)(rows)}\n`)
223
+ const write = (s: string): void => void this.context.stdout.write(s)
224
+ if (this.out == null && this.essentials == null) {
225
+ write(this.json ? `${JSON.stringify(rows)}\n` : `${(this.compact ? renderCompact : renderMarkdown)(rows, bin)}\n`)
124
226
  return 0
125
227
  }
126
- const { existsSync, readFileSync, writeFileSync } = await import('fs')
127
- if (!existsSync(this.out)) { this.context.stdout.write(`FAIL: ${this.out} does not exist\n`); return 1 }
128
- const host = readFileSync(this.out, 'utf8')
129
- const next = spliceBlock(host, (this.compact ? renderCompact : renderMarkdown)(rows))
130
- if (next == null) { this.context.stdout.write(`FAIL: ${this.out} has no ${BEGIN} / ${END} markers\n`); return 1 }
131
- if (next === host) { this.context.stdout.write(`ok: ${this.out} command index current (${rows.length} commands)\n`); return 0 }
228
+ if ((this.constructor as typeof CommandsCommand).shapeRule) {
229
+ const bad = shapeProblems(rows, bin)
230
+ for (const b of bad) write(`FAIL: ${b}\n`)
231
+ if (bad.length > 0) { write(' fix: give the row a category, a short description and an example that starts with its words\n'); return 1 }
232
+ }
233
+ const { existsSync, mkdirSync, readFileSync, writeFileSync: writeRaw } = await import('fs')
234
+ const { dirname } = await import('path')
235
+ const writeFileSync = (path: string, text: string): void => { mkdirSync(dirname(path), { recursive: true }); writeRaw(path, text) }
236
+ let code = 0
237
+ if (this.essentials != null) {
238
+ const cmd = `${bin} commands${this.out != null ? ` --out ${this.out}` : ''} --essentials ${this.essentials}`
239
+ const text = renderEssentials(rows, bin, cmd)
240
+ if (existsSync(this.essentials) && readFileSync(this.essentials, 'utf8') === text) write(`ok: ${this.essentials} current\n`)
241
+ else if (this.check) { write(`FAIL: ${this.essentials} is STALE — the essential commands moved\n fix: ${cmd}\n`); code = 1 }
242
+ else { writeFileSync(this.essentials, text); write(`ok: ${this.essentials} rewritten\n`) }
243
+ }
244
+ if (this.out == null) return code
245
+ const [begin, end] = markersFor(bin)
246
+ const project = (this.constructor as typeof CommandsCommand).shapeRule
247
+ if (!existsSync(this.out) && (!project || this.check)) { write(`FAIL: ${this.out} does not exist\n`); return 1 }
248
+ const host = existsSync(this.out) ? readFileSync(this.out, 'utf8') : newCommandsPage(bin, this.out)
249
+ const next = spliceBlock(host, (this.compact ? renderCompact : renderMarkdown)(rows, bin), bin)
250
+ if (next == null) { write(`FAIL: ${this.out} has no ${begin} / ${end} markers\n`); return 1 }
251
+ if (next === host) { write(`ok: ${this.out} command index current (${rows.length} commands)\n`); return code }
132
252
  if (this.check) {
133
- this.context.stdout.write(`FAIL: ${this.out} command index is STALE — the binary registers commands the page does not list\n`)
134
- this.context.stdout.write(` fix: shipkit commands${this.compact ? ' --compact' : ''} --out ${this.out}\n`)
253
+ write(`FAIL: ${this.out} command index is STALE — the binary registers commands the page does not list\n`)
254
+ write(` fix: ${bin === 'sk' ? 'shipkit' : bin} commands${this.compact ? ' --compact' : ''} --out ${this.out}\n`)
135
255
  return 1
136
256
  }
137
257
  writeFileSync(this.out, next)
138
- this.context.stdout.write(`ok: ${this.out} command index rewritten (${rows.length} commands)\n`)
139
- return 0
258
+ write(`ok: ${this.out} command index rewritten (${rows.length} commands)\n`)
259
+ return code
140
260
  }
141
261
  }