rvlib-shipkit 0.9.0 → 0.10.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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 — 2026-07-22
4
+
5
+ New:
6
+
7
+ - `shipkit start`: `--include` is now repeatable — every given line is
8
+ ensured in CLAUDE.md, in order, inserted after any leading include lines
9
+ already present (never double-added; `--also-ok` satisfies the first
10
+ include).
11
+
12
+ Fixed:
13
+
14
+ - md graph scanner: a backticked/arrow token carrying the scan root's own
15
+ dir name (`agent/architecture.md` while scanning `agent/`) now resolves —
16
+ routing tables written repo-root-relative finally produce edges.
17
+
3
18
  ## 0.9.0 — 2026-07-22
4
19
 
5
20
  New:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "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",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,14 +12,14 @@ export class StartCommand extends Command {
12
12
  description: 'ensure <root>/<name> exists, optionally with a CLAUDE.md include line; prints the dir on stdout so a shell alias can cd there',
13
13
  examples: [
14
14
  ['start (or enter) a repo', 'shipkit start pong'],
15
- ['seed CLAUDE.md line 1', "shipkit start pong --include '@~/wiki/index.md'"],
15
+ ['seed CLAUDE.md includes', "shipkit start pong --include '@~/wiki/index.md' --include '@./agent/index.md'"],
16
16
  ],
17
17
  })
18
18
 
19
19
  name = Option.String()
20
20
  root = Option.String('--root', { description: 'parent dir for repos (default ~/dev)' })
21
- include = Option.String('--include', { description: 'line CLAUDE.md must start with; omitted = only ensure the dir' })
22
- alsoOk = Option.Array('--also-ok', { description: 'alternative lines that already count as wired — never double-add' })
21
+ include = Option.Array('--include', { description: 'line CLAUDE.md must carry, repeatable, order kept; omitted = only ensure the dir' })
22
+ alsoOk = Option.Array('--also-ok', { description: 'alternative lines satisfying the first --include — never double-add' })
23
23
  skip = Option.Array('--skip', { description: 'repo names that never get the include' })
24
24
 
25
25
  async execute(): Promise<number> {
package/src/start-repo.ts CHANGED
@@ -1,8 +1,9 @@
1
- // `shipkit start` — ensure <root>/<name> exists and, when --include is
2
- // given, that CLAUDE.md carries that include line. Plain module behind
3
- // cmd_Start. Status lines go to STDERR — the ONLY stdout output is the
4
- // resolved dir, so a sourced shell alias can `cd "$(shipkit start <name>)"`
5
- // (the one thing a child process can't do for its parent shell).
1
+ // `shipkit start` — ensure <root>/<name> exists and, when --include lines
2
+ // are given, that CLAUDE.md carries them (in order, at the top). Plain
3
+ // module behind cmd_Start. Status lines go to STDERR — the ONLY stdout
4
+ // output is the resolved dir, so a sourced shell alias can
5
+ // `cd "$(shipkit start <name>)"` (the one thing a child process can't do
6
+ // for its parent shell).
6
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
7
8
  import { homedir } from 'os'
8
9
  import { join } from 'path'
@@ -10,16 +11,14 @@ import { join } from 'path'
10
11
  export type StartOpts = {
11
12
  /** parent dir for repos — default ~/dev */
12
13
  root?: string
13
- /** line CLAUDE.md must start with; omitted = only ensure the dir */
14
- include?: string
15
- /** alternative lines that already count as wired — never double-add */
14
+ /** lines CLAUDE.md must carry, in order; omitted = only ensure the dir */
15
+ include?: string[]
16
+ /** alternative lines that satisfy the FIRST include — never double-add */
16
17
  alsoOk?: string[]
17
- /** repo names that never get the include */
18
+ /** repo names that never get includes */
18
19
  skip?: string[]
19
20
  }
20
21
 
21
- const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
22
-
23
22
  export function startRepo(name: string, opts: StartOpts = {}): number {
24
23
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
25
24
  console.error(`FAIL: bad repo name '${name}' — letters/digits/._- only, no leading dot, no slashes`)
@@ -30,18 +29,29 @@ export function startRepo(name: string, opts: StartOpts = {}): number {
30
29
  mkdirSync(dir, { recursive: true })
31
30
  console.error(`ok: created ${dir}`)
32
31
  }
33
- if (opts.include == null || (opts.skip ?? []).includes(name)) {
34
- if (opts.include != null) console.error(`skip: ${name} is include-exempt — CLAUDE.md untouched`)
32
+ const want = opts.include ?? []
33
+ if (want.length === 0 || (opts.skip ?? []).includes(name)) {
34
+ if (want.length > 0) console.error(`skip: ${name} is include-exempt — CLAUDE.md untouched`)
35
35
  } else {
36
36
  const claudeMd = join(dir, 'CLAUDE.md')
37
37
  if (!existsSync(claudeMd)) {
38
- writeFileSync(claudeMd, `${opts.include}\n`)
39
- console.error('ok: CLAUDE.md created with the include')
38
+ writeFileSync(claudeMd, `${want.join('\n')}\n`)
39
+ console.error(`ok: CLAUDE.md created with ${want.length} include line(s)`)
40
40
  } else {
41
- const wiredRe = new RegExp([opts.include, ...(opts.alsoOk ?? [])].map((l) => `^${escapeRe(l)}$`).join('|'), 'm')
42
- if (!wiredRe.test(readFileSync(claudeMd, 'utf8'))) {
43
- writeFileSync(claudeMd, `${opts.include}\n\n${readFileSync(claudeMd, 'utf8')}`)
44
- console.error('ok: include prepended to CLAUDE.md')
41
+ const cur = readFileSync(claudeMd, 'utf8')
42
+ const lineSet = new Set(cur.split('\n'))
43
+ const alt = opts.alsoOk ?? []
44
+ const satisfied = (w: string): boolean => lineSet.has(w) || (w === want[0] && alt.some((a) => lineSet.has(a)))
45
+ const missing = want.filter((w) => !satisfied(w))
46
+ if (missing.length > 0) {
47
+ // insert after any leading include/alt lines so order survives
48
+ const lines = cur.split('\n')
49
+ const known = new Set([...want, ...alt])
50
+ let at = 0
51
+ while (at < lines.length && known.has(lines[at]!)) at++
52
+ lines.splice(at, 0, ...missing)
53
+ writeFileSync(claudeMd, lines.join('\n'))
54
+ console.error(`ok: ${missing.length} missing include line(s) inserted into CLAUDE.md`)
45
55
  }
46
56
  }
47
57
  }
@@ -8,7 +8,7 @@
8
8
  // unreachable files stay in the graph with no depth (lint-md.ts turns those,
9
9
  // plus dangling refs, into failures — the graph itself is a viewer).
10
10
  import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
11
- import { dirname, join, relative, resolve } from 'path'
11
+ import { basename, dirname, join, relative, resolve } from 'path'
12
12
  import type { MdEdge, MdGraph, MdNode } from 'rvlib-shipkit/src/wiki/md-graph.ts'
13
13
 
14
14
  export type ScanMdConfig = {
@@ -43,8 +43,13 @@ export function scanMd(cfg: ScanMdConfig): MdScan {
43
43
  files.sort()
44
44
  const inGraph = new Set(files)
45
45
 
46
+ // writers inside a scanned dir often name paths from the REPO root
47
+ // ("agent/architecture.md" while scanning agent/) — accept that form too
48
+ const rootBase = `${basename(cfg.root)}/`
46
49
  const resolveRef = (fromRel: string, token: string): string | null => {
47
- for (const cand of [resolve(join(cfg.root, dirname(fromRel)), token), resolve(cfg.root, token)]) {
50
+ const cands = [resolve(join(cfg.root, dirname(fromRel)), token), resolve(cfg.root, token)]
51
+ if (token.startsWith(rootBase)) cands.push(resolve(cfg.root, token.slice(rootBase.length)))
52
+ for (const cand of cands) {
48
53
  const rel = relative(cfg.root, cand)
49
54
  if (!rel.startsWith('..') && inGraph.has(rel)) return rel
50
55
  }