rvlib-shipkit 0.8.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,32 @@
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
+
18
+ ## 0.9.0 — 2026-07-22
19
+
20
+ New:
21
+
22
+ - `shipkit start <name>`: ensure `<root>/<name>` exists (`--root`, default
23
+ `~/dev`) and, with `--include <line>`, that CLAUDE.md starts with that
24
+ include (`--also-ok <line>` variants count as already wired and are never
25
+ double-added; `--skip <name>` repos never get the include). Stdout is
26
+ ONLY the resolved dir: a sourced shell alias can
27
+ `cd "$(shipkit start <name> …)"`, the one thing a child process can't do
28
+ for its parent shell.
29
+
3
30
  ## 0.8.0 — 2026-07-22
4
31
 
5
32
  New:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "0.8.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",
@@ -0,0 +1,28 @@
1
+ // shipkit start — ensure <root>/<name> exists (+ a CLAUDE.md include),
2
+ // printing the dir on stdout so a shell alias can cd there (a child process
3
+ // can't cd its parent). Thin clipanion surface; logic in start-repo.ts.
4
+ import { Command, Option } from 'clipanion'
5
+ import { startRepo } from '../start-repo.ts'
6
+
7
+ export class StartCommand extends Command {
8
+ static override paths = [['start']]
9
+
10
+ static override usage = Command.Usage({
11
+ category: 'Setup',
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
+ examples: [
14
+ ['start (or enter) a repo', 'shipkit start pong'],
15
+ ['seed CLAUDE.md includes', "shipkit start pong --include '@~/wiki/index.md' --include '@./agent/index.md'"],
16
+ ],
17
+ })
18
+
19
+ name = Option.String()
20
+ root = Option.String('--root', { description: 'parent dir for repos (default ~/dev)' })
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
+ skip = Option.Array('--skip', { description: 'repo names that never get the include' })
24
+
25
+ async execute(): Promise<number> {
26
+ return startRepo(this.name, { root: this.root, include: this.include, alsoOk: this.alsoOk, skip: this.skip })
27
+ }
28
+ }
package/src/cli.ts CHANGED
@@ -12,6 +12,7 @@ import { ContractsCommand } from './cli/cmd_Contracts.ts'
12
12
  import { InitCommand } from './cli/cmd_Init.ts'
13
13
  import { MvCommand } from './cli/cmd_Mv.ts'
14
14
  import { ServeCommand } from './cli/cmd_Serve.ts'
15
+ import { StartCommand } from './cli/cmd_Start.ts'
15
16
  import { TuiCommand } from './cli/cmd_Tui.ts'
16
17
  import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
17
18
 
@@ -45,5 +46,6 @@ cli.register(AssetsGenCommand)
45
46
  cli.register(AssetsIdentityCommand)
46
47
  cli.register(AssetsCheckCommand)
47
48
  cli.register(InitCommand)
49
+ cli.register(StartCommand)
48
50
 
49
51
  cli.runExit(process.argv.slice(2))
@@ -0,0 +1,60 @@
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).
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
8
+ import { homedir } from 'os'
9
+ import { join } from 'path'
10
+
11
+ export type StartOpts = {
12
+ /** parent dir for repos — default ~/dev */
13
+ root?: string
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 */
17
+ alsoOk?: string[]
18
+ /** repo names that never get includes */
19
+ skip?: string[]
20
+ }
21
+
22
+ export function startRepo(name: string, opts: StartOpts = {}): number {
23
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
24
+ console.error(`FAIL: bad repo name '${name}' — letters/digits/._- only, no leading dot, no slashes`)
25
+ return 2
26
+ }
27
+ const dir = join(opts.root ?? join(homedir(), 'dev'), name)
28
+ if (!existsSync(dir)) {
29
+ mkdirSync(dir, { recursive: true })
30
+ console.error(`ok: created ${dir}`)
31
+ }
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
+ } else {
36
+ const claudeMd = join(dir, 'CLAUDE.md')
37
+ if (!existsSync(claudeMd)) {
38
+ writeFileSync(claudeMd, `${want.join('\n')}\n`)
39
+ console.error(`ok: CLAUDE.md created with ${want.length} include line(s)`)
40
+ } else {
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`)
55
+ }
56
+ }
57
+ }
58
+ console.log(dir)
59
+ return 0
60
+ }
@@ -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
  }