rvlib-shipkit 2.1.0 → 2.1.1

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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## 2.1.1
2
+
3
+ - **`--only a,b` takes a comma list** in `sk ci`, `sk cache-audit` and `sk fleet`, the same as repeating the flag.
4
+ - **`sk gp start` waits for pm to act.** The state the switch leaves (down for a start, up for a stop) counts as pm not there yet for 30 s, so a process that takes a few seconds to launch no longer reads as down. A crash still fails at once.
5
+ - **`sk mv` rewrites relative markdown links on a folder move**, through the same pass the page move uses, and a page move no longer crashes on a repo with a git submodule.
6
+
1
7
  ## 2.1.0
2
8
 
3
9
  - **SK027 requires exhaustive agent/ indexes.** Every `agent/` page is listed by an index (a bullet, numbered item, table row, `→` pointer or `@` include names it or its folder), and every index is loaded from CLAUDE.md or listed by a loaded file. A page linked only from prose now fails, with the page that already names it as the fix. The template's house `[lint]` block makes `typescript/no-empty-object-type` an error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "funding": "https://github.com/sponsors/rvion",
5
5
  "description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
6
6
  "license": "MIT",
@@ -4,6 +4,7 @@
4
4
  import { Command, Option } from 'clipanion'
5
5
  import { loadRepoConfig } from '../repo-config.ts'
6
6
  import { auditCache } from '../core/cache-audit.ts'
7
+ import { listFlag } from './list-flag.ts'
7
8
 
8
9
  export class CacheAuditCommand extends Command {
9
10
  static override paths = [['cache-audit']]
@@ -28,9 +29,9 @@ export class CacheAuditCommand extends Command {
28
29
  const root = process.cwd()
29
30
  const n = Number(this.sample)
30
31
  if (!Number.isInteger(n) || n < 1) { this.context.stdout.write(`FAIL: --sample takes a whole number above 0, got '${this.sample}'\n`); return 1 }
31
- const r = await auditCache(root, await loadRepoConfig(root), { sample: n, only: this.only })
32
+ const r = await auditCache(root, await loadRepoConfig(root), { sample: n, only: this.only == null ? undefined : listFlag(this.only) })
32
33
  if (this.json) { this.context.stdout.write(`${JSON.stringify(r)}\n`); return r.stale.length > 0 ? 1 : 0 }
33
- if (r.rows.length === 0) { this.context.stdout.write(`skip: nothing to audit, the cache answers none of ${this.only?.join(', ') ?? 'the checks'} right now\n`); return 0 }
34
+ if (r.rows.length === 0) { this.context.stdout.write(`skip: nothing to audit, the cache answers none of ${this.only == null ? 'the checks' : listFlag(this.only).join(', ')} right now\n`); return 0 }
34
35
  for (const row of r.rows) {
35
36
  const same = row.fresh === row.cached || row.fresh === 'skip'
36
37
  this.context.stdout.write(same
package/src/cli/cmd_Ci.ts CHANGED
@@ -7,6 +7,7 @@ import { runJob } from '../core/job-run.ts'
7
7
  import { runCi } from '../core/run-ci.ts'
8
8
  import { runViaHub } from '../home/via-hub.ts'
9
9
  import { maybeSelfUpdateAndRestart } from '../core/self-update.ts'
10
+ import { listFlag } from './list-flag.ts'
10
11
 
11
12
  export class CiCommand extends Command {
12
13
  static override paths = [['ci']]
@@ -37,7 +38,7 @@ export class CiCommand extends Command {
37
38
  frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for self-maintaining checks: SK005 never touches package.json, and generated paths are never untracked. STATUS.md is still refreshed on disk, but it is gitignored, so a run leaves the tree clean' })
38
39
  skip = Option.Array('--skip', [], { description: 'skip a check group (repeatable)' })
39
40
  withIds = Option.Array('--with', [], { description: 'include a check the quick gate leaves out (repeatable)' })
40
- only = Option.Array('--only', [], { description: 'run only this check + its dep closure (repeatable). A group name runs every check declaring that group. No repair pass unless --fix is named' })
41
+ only = Option.Array('--only', [], { description: 'run only this check + its dep closure (repeatable, or comma separated). A group name runs every check declaring that group. No repair pass unless --fix is named' })
41
42
  fresh = Option.Boolean('--fresh', false, { description: 'run every check: no answer comes from the cache, and what this run finds is stored fresh' })
42
43
  full = Option.Boolean('--full', false, { description: `run the FULL test tier: every suite the repo declares, the slow ones it skips while you work included. What the release path always runs. Same as SHIPKIT_TEST_FULL=1` })
43
44
 
@@ -74,7 +75,7 @@ export class CiCommand extends Command {
74
75
  ...(this.frozen ? ['--frozen'] : []),
75
76
  ...this.skip.flatMap((v) => ['--skip', v]),
76
77
  ...this.withIds.flatMap((v) => ['--with', v]),
77
- ...this.only.flatMap((v) => ['--only', v]),
78
+ ...listFlag(this.only).flatMap((v) => ['--only', v]),
78
79
  ...(this.fresh ? ['--fresh'] : []),
79
80
  ...(this.full ? ['--full'] : []),
80
81
  ...(this.verbose ? ['--verbose'] : []),
@@ -95,7 +96,7 @@ export class CiCommand extends Command {
95
96
  frozen: this.frozen,
96
97
  skipGroups: this.skip,
97
98
  withIds: this.withIds,
98
- onlyIds: this.only,
99
+ onlyIds: listFlag(this.only),
99
100
  fresh: this.fresh,
100
101
  ...(this.full ? { tests: 'full' as const } : {}),
101
102
  ...(this.verbose ? { verbose: true } : {}),
@@ -6,6 +6,7 @@ import { Command, Option } from 'clipanion'
6
6
  import { join } from 'path'
7
7
  import { npmLatest, resolveLatest, scanFleet, type FleetRepo } from '../fleet/scan-fleet.ts'
8
8
  import { short, updateFleet } from '../fleet/update-fleet.ts'
9
+ import { listFlag } from './list-flag.ts'
9
10
 
10
11
  export class FleetCommand extends Command {
11
12
  static override paths = [['fleet']]
@@ -32,7 +33,8 @@ export class FleetCommand extends Command {
32
33
  async execute(): Promise<number> {
33
34
  const root = this.root ?? join(process.cwd(), '..')
34
35
  let repos = scanFleet(root)
35
- if (this.only != null && this.only.length > 0) repos = repos.filter((r) => this.only!.includes(r.name))
36
+ const only = listFlag(this.only)
37
+ if (only.length > 0) repos = repos.filter((r) => only.includes(r.name))
36
38
  const w = (l: string) => this.context.stdout.write(`${l}\n`)
37
39
  if (repos.length === 0) {
38
40
  w(`skip: no repo under ${root} depends on an rvlib-* package`)
@@ -172,6 +172,8 @@ export type SwitchDeps = {
172
172
  /** the longest a switch is followed: a bring-up polls its health for minutes */
173
173
  export const SWITCH_WAIT_MS = 10 * 60_000
174
174
  const SWITCH_POLL_MS = 1_000
175
+ /** how long the state the switch left counts as pm not having acted yet */
176
+ export const SWITCH_GRACE_MS = 30_000
175
177
 
176
178
  /** flip a global process through pm, then follow pm's state until its probe
177
179
  * settles it: 0 once it is up (or down for stop), 1 otherwise. The launcher's
@@ -183,7 +185,8 @@ export async function switchProcess(id: string, act: SwitchAct, d: SwitchDeps, w
183
185
  return 1
184
186
  }
185
187
  const want = act === 'stop' ? 'down' : 'up'
186
- const end = d.now() + waitMs
188
+ const start = d.now()
189
+ const end = start + waitMs
187
190
  let said = ''
188
191
  for (;;) {
189
192
  const v = await d.view(id)
@@ -195,7 +198,9 @@ export async function switchProcess(id: string, act: SwitchAct, d: SwitchDeps, w
195
198
  said = v.status
196
199
  d.log(`${id}: ${v.status}`)
197
200
  }
198
- const moving = v.status === 'starting' || v.status === 'stopping' || v.want !== want
201
+ // the state it is leaving, read just after the switch, is pm not there yet
202
+ const leaving = v.status === (want === 'up' ? 'down' : 'up') && d.now() - start < SWITCH_GRACE_MS
203
+ const moving = v.status === 'starting' || v.status === 'stopping' || v.want !== want || leaving
199
204
  if (!moving) {
200
205
  if (v.status === want) return 0
201
206
  d.log(`FAIL ${id} is ${v.status}, not ${want}${v.note == null ? '' : `: ${v.note}`}`)
@@ -0,0 +1,3 @@
1
+ /** a repeatable flag's values, each also split on commas: `--only a,b` is `--only a --only b` */
2
+ export const listFlag = (values: readonly string[] | undefined): string[] =>
3
+ (values ?? []).flatMap((v) => v.split(',')).map((v) => v.trim()).filter((v) => v !== '')
package/src/mv/mv-md.ts CHANGED
@@ -154,7 +154,8 @@ export function movePage(root: string, oldRel: string, newRel: string, io: MvIo,
154
154
  let touched = 0
155
155
  for (const f of tracked) {
156
156
  const p = join(root, f)
157
- if (!existsSync(p)) continue
157
+ // a submodule is one ls-files entry and a directory: nothing to read
158
+ if (!isFile(p)) continue
158
159
  const before = readFileSync(p, 'utf8')
159
160
  let after = retargetRelativeRefs(root, before, dirOf(f), dirOf(moves.get(f) ?? f), moves)
160
161
  for (const [oldTok, newTok] of pairs) after = after.replace(bound(oldTok), newTok)
package/src/ts/rename.ts CHANGED
@@ -16,6 +16,7 @@ import { basename, dirname, extname, join, relative, resolve } from 'path'
16
16
  import { canonicalSpecifier, isSkipped, loadConfig, toPosix, type RvtConfig } from 'rvlib-shipkit/src/ts/config.ts'
17
17
  import { extractSpecs } from 'rvlib-shipkit/src/ts/source-graph.ts'
18
18
  import { gitBin } from '../core/git-bin.ts'
19
+ import { retargetRelativeRefs } from '../mv/mv-md.ts'
19
20
 
20
21
  const CODE_RE = /\.[cm]?[jt]sx?$/
21
22
 
@@ -106,7 +107,15 @@ export function runRename(srcArg: string, dstArg: string, opts: { apply?: boolea
106
107
  const isFile = (rel: string): boolean => {
107
108
  try { return statSync(join(repoRoot, rel)).isFile() } catch { return false }
108
109
  }
110
+ // a markdown link is relative to its page: the md engine's pass retargets it
111
+ const mdMoves = new Map(allFiles.flatMap((f): [string, string][] => { const to = movedTo(f); return to == null ? [] : [[f, to]] }))
112
+ const dirOf = (p: string): string => { const d = dirname(p); return d === '.' ? '' : d }
109
113
  const rewriteFile = (file: string, content: string): { text: string; count: number } => {
114
+ if (file.endsWith('.md')) {
115
+ const linked = retargetRelativeRefs(repoRoot, content, dirOf(file), dirOf(movedTo(file) ?? file), mdMoves)
116
+ const scanned = scanPairs(linked, pairs)
117
+ return { text: scanned.text, count: scanned.count + (linked === content ? 0 : 1) }
118
+ }
110
119
  const rel = CODE_RE.test(file) ? relativeRewrites(file, content, movedTo, isFile) : new Map<string, string>()
111
120
  if (rel.size === 0) return scanPairs(content, pairs)
112
121
  // the relative specifiers this move retargets are set aside while the