rvlib-shipkit 0.7.0 → 0.9.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 +30 -0
- package/package.json +1 -1
- package/src/assets/run-assets.ts +1 -1
- package/src/cli/cmd_Mv.ts +34 -0
- package/src/cli/cmd_Start.ts +28 -0
- package/src/cli.ts +4 -0
- package/src/contracts/manifests/discord.contract.ts +1 -1
- package/src/contracts/manifests/identity.contract.ts +1 -1
- package/src/contracts/manifests/social.contract.ts +1 -1
- package/src/core/package-json-check.ts +1 -0
- package/src/init-repo.ts +1 -1
- package/src/mv/mv-md.ts +90 -0
- package/src/serve/run-serve.ts +15 -1
- package/src/shipkit.ts +2 -0
- package/src/start-repo.ts +50 -0
- package/src/tui/run-tui.ts +1 -1
- package/src/tui/tui-store.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 — 2026-07-22
|
|
4
|
+
|
|
5
|
+
New:
|
|
6
|
+
|
|
7
|
+
- `shipkit start <name>`: ensure `<root>/<name>` exists (`--root`, default
|
|
8
|
+
`~/dev`) and, with `--include <line>`, that CLAUDE.md starts with that
|
|
9
|
+
include (`--also-ok <line>` variants count as already wired and are never
|
|
10
|
+
double-added; `--skip <name>` repos never get the include). Stdout is
|
|
11
|
+
ONLY the resolved dir: a sourced shell alias can
|
|
12
|
+
`cd "$(shipkit start <name> …)"`, the one thing a child process can't do
|
|
13
|
+
for its parent shell.
|
|
14
|
+
|
|
15
|
+
## 0.8.0 — 2026-07-22
|
|
16
|
+
|
|
17
|
+
New:
|
|
18
|
+
|
|
19
|
+
- `shipkit mv <old.md> <new.md>` in every repo: rename an md page + its
|
|
20
|
+
sibling asset dir + `<name>.check.ts`, sweeping every tracked file for
|
|
21
|
+
refs (full path, docs-root id, check filename, boundary-guarded so
|
|
22
|
+
lookalikes survive). Dry by default, `--write` applies, `--commit` adds
|
|
23
|
+
one conventional commit behind the repo's pre-commit gate. A repo
|
|
24
|
+
`mv:after` script (NOT `postmv`, bun npm-compat auto-runs post* names)
|
|
25
|
+
runs after the moves for repo-specific re-stamps. `movePage` exported.
|
|
26
|
+
`init` scaffolds the `mv` script; the package.json probe requires it.
|
|
27
|
+
Upgraders: re-run `shipkit init` (idempotent, only adds the gap).
|
|
28
|
+
- the hub detects its own staleness: lib version read at boot vs on disk
|
|
29
|
+
per attention load. A mismatched running process shows a red line telling
|
|
30
|
+
you to restart the task (field bug: a morning process served old studio
|
|
31
|
+
code all day and a pick looked broken).
|
|
32
|
+
|
|
3
33
|
## 0.7.0 — 2026-07-22
|
|
4
34
|
|
|
5
35
|
New (the hub becomes THE dashboard; autonomy over nagging):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
package/src/assets/run-assets.ts
CHANGED
|
@@ -184,7 +184,7 @@ export const runAssetsCheck = (root: string, io: AssetsIo): Promise<number> =>
|
|
|
184
184
|
else if (idn.state === 'rendered') io.print(`ok: identity re-rendered (brand.ts changed) — ${idn.detail}`)
|
|
185
185
|
else if (idn.state === 'blocked') { io.print(`FAIL: identity blocked — ${idn.detail}`); bad++ }
|
|
186
186
|
// manual assets: no rolls = nothing to ship = RED (the missing list
|
|
187
|
-
// is the shopping list — build/audio doctrine); rolls-but-no-pick = WARN
|
|
187
|
+
// is the shopping list — build/game-audio doctrine); rolls-but-no-pick = WARN
|
|
188
188
|
const manuals = allAssets(kit).filter((f) => f.kind.generate.backend === 'manual')
|
|
189
189
|
let manualBad = 0
|
|
190
190
|
for (const f of manuals) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// shipkit mv — md-page rename with repo-wide ref sweep. Thin clipanion
|
|
2
|
+
// surface; logic in mv/mv-md.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { movePage } from '../mv/mv-md.ts'
|
|
5
|
+
|
|
6
|
+
export class MvCommand extends Command {
|
|
7
|
+
static override paths = [['mv']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Wiki',
|
|
11
|
+
description: 'rename an md page + sibling dir + check.ts, sweeping every tracked ref; renames must be CHEAP',
|
|
12
|
+
examples: [
|
|
13
|
+
['dry run (default): show what would change', 'shipkit mv docs/old-name.md docs/new-name.md'],
|
|
14
|
+
['apply + one conventional commit', 'shipkit mv docs/old-name.md docs/new-name.md --commit'],
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
oldPath = Option.String()
|
|
19
|
+
newPath = Option.String()
|
|
20
|
+
write = Option.Boolean('--write', false, { description: 'apply (default is a dry run)' })
|
|
21
|
+
commit = Option.Boolean('--commit', false, { description: 'apply + git commit (pre-commit hook is the gate)' })
|
|
22
|
+
|
|
23
|
+
async execute(): Promise<number> {
|
|
24
|
+
try {
|
|
25
|
+
return movePage(process.cwd(), this.oldPath, this.newPath, {
|
|
26
|
+
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
27
|
+
printErr: (l) => this.context.stderr.write(`${l}\n`),
|
|
28
|
+
}, { write: this.write, commit: this.commit })
|
|
29
|
+
} catch (e) {
|
|
30
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -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 line 1', "shipkit start pong --include '@~/wiki/index.md'"],
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
name = Option.String()
|
|
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' })
|
|
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
|
@@ -10,7 +10,9 @@ import { CheckCommand } from './cli/cmd_Check.ts'
|
|
|
10
10
|
import { CiCommand } from './cli/cmd_Ci.ts'
|
|
11
11
|
import { ContractsCommand } from './cli/cmd_Contracts.ts'
|
|
12
12
|
import { InitCommand } from './cli/cmd_Init.ts'
|
|
13
|
+
import { MvCommand } from './cli/cmd_Mv.ts'
|
|
13
14
|
import { ServeCommand } from './cli/cmd_Serve.ts'
|
|
15
|
+
import { StartCommand } from './cli/cmd_Start.ts'
|
|
14
16
|
import { TuiCommand } from './cli/cmd_Tui.ts'
|
|
15
17
|
import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
|
|
16
18
|
|
|
@@ -27,6 +29,7 @@ const cli = new Cli({
|
|
|
27
29
|
cli.register(Builtins.HelpCommand)
|
|
28
30
|
cli.register(Builtins.VersionCommand)
|
|
29
31
|
cli.register(CiCommand)
|
|
32
|
+
cli.register(MvCommand)
|
|
30
33
|
cli.register(CheckCommand)
|
|
31
34
|
cli.register(TuiCommand)
|
|
32
35
|
cli.register(ServeCommand)
|
|
@@ -43,5 +46,6 @@ cli.register(AssetsGenCommand)
|
|
|
43
46
|
cli.register(AssetsIdentityCommand)
|
|
44
47
|
cli.register(AssetsCheckCommand)
|
|
45
48
|
cli.register(InitCommand)
|
|
49
|
+
cli.register(StartCommand)
|
|
46
50
|
|
|
47
51
|
cli.runExit(process.argv.slice(2))
|
|
@@ -5,7 +5,7 @@ import { defineContract, dirExists, md, png } from 'rvlib-shipkit/src/contracts/
|
|
|
5
5
|
export default defineContract({
|
|
6
6
|
venue: 'discord',
|
|
7
7
|
dir: '.rv-discord',
|
|
8
|
-
owner: 'distribute/
|
|
8
|
+
owner: 'distribute/post-publicly.md',
|
|
9
9
|
appliesTo: 'declared',
|
|
10
10
|
detect: [dirExists('.rv-discord')],
|
|
11
11
|
artifacts: [
|
|
@@ -6,7 +6,7 @@ import { defineContract, file, glob, png } from 'rvlib-shipkit/src/contracts/con
|
|
|
6
6
|
export default defineContract({
|
|
7
7
|
venue: 'identity',
|
|
8
8
|
dir: '.rv-identity',
|
|
9
|
-
owner: 'build/assets.md',
|
|
9
|
+
owner: 'build/generate-assets.md',
|
|
10
10
|
appliesTo: 'always',
|
|
11
11
|
gate: 'public-surface',
|
|
12
12
|
detect: [],
|
|
@@ -5,7 +5,7 @@ import { custom, defineContract, dirExists } from 'rvlib-shipkit/src/contracts/c
|
|
|
5
5
|
export default defineContract({
|
|
6
6
|
venue: 'social',
|
|
7
7
|
dir: '.rv-social',
|
|
8
|
-
owner: 'distribute/
|
|
8
|
+
owner: 'distribute/post-publicly.md',
|
|
9
9
|
appliesTo: 'declared',
|
|
10
10
|
detect: [dirExists('.rv-social')],
|
|
11
11
|
artifacts: [
|
|
@@ -68,6 +68,7 @@ export async function evaluatePackageJson(t: CheckCtx, root: string, c: RepoConf
|
|
|
68
68
|
{ name: 'contracts', why: 'shipkit repo' },
|
|
69
69
|
{ name: 'tui', why: 'shipkit repo' },
|
|
70
70
|
{ name: 'hub', why: 'shipkit repo' },
|
|
71
|
+
{ name: 'mv', why: 'shipkit repo' },
|
|
71
72
|
]
|
|
72
73
|
if (existsSync(join(root, '.rv-assets/kit.ts'))) required.push({ name: 'assets', why: '.rv-assets/kit.ts exists' })
|
|
73
74
|
if (existsSync(join(root, '.rv-identity/brand.ts'))) required.push({ name: 'assets:identity', why: '.rv-identity/brand.ts exists' })
|
package/src/init-repo.ts
CHANGED
|
@@ -42,7 +42,7 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
42
42
|
let added = 0
|
|
43
43
|
// canonical script NAME → shipkit command (hub runs `shipkit serve`) —
|
|
44
44
|
// vocabulary owned by meta/rv-conventions/package-json.md
|
|
45
|
-
const CANONICAL = { ci: 'ci', check: 'check', contracts: 'contracts', tui: 'tui', hub: 'serve' } as const
|
|
45
|
+
const CANONICAL = { ci: 'ci', check: 'check', contracts: 'contracts', tui: 'tui', hub: 'serve', mv: 'mv' } as const
|
|
46
46
|
for (const [k, cmd] of Object.entries(CANONICAL)) {
|
|
47
47
|
const v = run(cmd)
|
|
48
48
|
if (pkg.scripts[k] == null) { pkg.scripts[k] = v; added++ }
|
package/src/mv/mv-md.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// shipkit mv — rename an md page the AGILE way: one command moves the .md,
|
|
2
|
+
// its sibling asset dir and <name>.check.ts, then sweeps EVERY tracked text
|
|
3
|
+
// file for refs (full path, docs-root-relative id, check filename). Dry by
|
|
4
|
+
// default; --write applies; --commit adds one conventional commit (the
|
|
5
|
+
// repo's pre-commit hook is the gate). If package.json has a `mv:after`
|
|
6
|
+
// script it runs after the moves (repo-specific re-stamps). TS module moves
|
|
7
|
+
// are a different concern (a TS-aware renamer owns import graphs).
|
|
8
|
+
import { execSync } from 'child_process'
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
10
|
+
import { basename, dirname, join } from 'path'
|
|
11
|
+
|
|
12
|
+
export type MvIo = { print: (l: string) => void; printErr: (l: string) => void }
|
|
13
|
+
export type MvOpts = { write?: boolean; commit?: boolean }
|
|
14
|
+
|
|
15
|
+
const sh = (cmd: string, cwd: string): string => execSync(cmd, { cwd }).toString()
|
|
16
|
+
|
|
17
|
+
export function movePage(root: string, oldRel: string, newRel: string, io: MvIo, o: MvOpts = {}): number {
|
|
18
|
+
const die = (m: string): number => { io.printErr(`FAIL: ${m}`); return 1 }
|
|
19
|
+
if (!oldRel.endsWith('.md') || !newRel.endsWith('.md')) return die('both paths must be .md (TS moves are not this tool\'s job)')
|
|
20
|
+
if (!existsSync(join(root, oldRel))) return die(`${oldRel} does not exist`)
|
|
21
|
+
if (existsSync(join(root, newRel))) return die(`${newRel} already exists`)
|
|
22
|
+
if (dirname(oldRel) !== dirname(newRel)) return die('cross-directory moves not supported yet, same dir only')
|
|
23
|
+
|
|
24
|
+
const write = o.write === true || o.commit === true
|
|
25
|
+
const oldName = basename(oldRel, '.md')
|
|
26
|
+
const newName = basename(newRel, '.md')
|
|
27
|
+
const oldBase = oldRel.slice(0, -3) // full path sans .md — the sibling dir
|
|
28
|
+
const newBase = newRel.slice(0, -3)
|
|
29
|
+
// docs-root-relative id: refs inside a wiki usually omit the top dir
|
|
30
|
+
// (`build/state`, not `wiki/build/state`) — sweep both forms
|
|
31
|
+
const tail = (p: string): string | null => { const seg = p.split('/'); return seg.length >= 3 ? seg.slice(1).join('/') : null }
|
|
32
|
+
const oldId = tail(oldBase)
|
|
33
|
+
const newId = tail(newBase)
|
|
34
|
+
|
|
35
|
+
// longest-first so `wiki/x/state.md` beats `x/state` beats bare tokens
|
|
36
|
+
const pairs: [string, string][] = [
|
|
37
|
+
[`${oldBase}.md`, `${newBase}.md`],
|
|
38
|
+
...(oldId != null ? [[`${oldId}.md`, `${newId}.md`] as [string, string]] : []),
|
|
39
|
+
// probe file is named after its page; DIR-qualified so another
|
|
40
|
+
// page's same-named check.ts in a different dir never gets rewritten
|
|
41
|
+
[`${oldBase}/${oldName}.check.ts`, `${newBase}/${newName}.check.ts`],
|
|
42
|
+
...(oldId != null ? [[`${oldId}/${oldName}.check.ts`, `${newId}/${newName}.check.ts`] as [string, string]] : []),
|
|
43
|
+
[`${oldBase}/`, `${newBase}/`],
|
|
44
|
+
...(oldId != null ? [[`${oldId}/`, `${newId}/`] as [string, string]] : []),
|
|
45
|
+
[oldBase, newBase],
|
|
46
|
+
...(oldId != null ? [[oldId, newId!] as [string, string]] : []),
|
|
47
|
+
]
|
|
48
|
+
const bound = (t: string): RegExp =>
|
|
49
|
+
new RegExp('(?<![\\w-])' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + (/[/.]$/.test(t) || t.endsWith('.ts') ? '' : '(?![\\w-])'), 'g')
|
|
50
|
+
|
|
51
|
+
const tracked = sh('git ls-files', root).split('\n').filter(Boolean)
|
|
52
|
+
.filter((f) => !/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|zip|gz|lockb|lock)$/i.test(f))
|
|
53
|
+
let touched = 0
|
|
54
|
+
for (const f of tracked) {
|
|
55
|
+
const p = join(root, f)
|
|
56
|
+
if (!existsSync(p)) continue
|
|
57
|
+
const before = readFileSync(p, 'utf8')
|
|
58
|
+
let after = before
|
|
59
|
+
for (const [oldTok, newTok] of pairs) after = after.replace(bound(oldTok), newTok)
|
|
60
|
+
if (after !== before) {
|
|
61
|
+
io.print(` ${f}`)
|
|
62
|
+
if (write) writeFileSync(p, after)
|
|
63
|
+
touched++
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
io.print(`${write ? 'renamed' : 'DRY RUN'}: ${oldBase} → ${newBase}, refs in ${touched} file(s)`)
|
|
67
|
+
if (!write) { io.print('re-run with --write (or --commit) to apply'); return 0 }
|
|
68
|
+
|
|
69
|
+
// moves AFTER the sweep (the sweep rewrote files at their old paths)
|
|
70
|
+
sh(`git mv ${JSON.stringify(oldRel)} ${JSON.stringify(newRel)}`, root)
|
|
71
|
+
if (existsSync(join(root, oldBase))) {
|
|
72
|
+
sh(`git mv ${JSON.stringify(oldBase)} ${JSON.stringify(newBase)}`, root)
|
|
73
|
+
const oldCheck = join(newBase, `${oldName}.check.ts`)
|
|
74
|
+
if (existsSync(join(root, oldCheck))) sh(`git mv ${JSON.stringify(oldCheck)} ${JSON.stringify(join(newBase, `${newName}.check.ts`))}`, root)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// repo-specific re-stamps (e.g. wiki depth frontmatter) — loud on failure
|
|
78
|
+
const pkg = existsSync(join(root, 'package.json')) ? (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { scripts?: Record<string, string> }) : {}
|
|
79
|
+
if (pkg.scripts?.['mv:after'] != null) {
|
|
80
|
+
try { execSync('bun run mv:after', { cwd: root, stdio: 'inherit' }) } catch { return die('mv:after failed, fix before committing') }
|
|
81
|
+
}
|
|
82
|
+
if (o.commit === true) {
|
|
83
|
+
sh('git add -A', root)
|
|
84
|
+
try {
|
|
85
|
+
execSync(`git commit -m ${JSON.stringify(`docs: rename ${oldBase} → ${newBase}`)}`, { cwd: root, stdio: 'inherit' })
|
|
86
|
+
} catch { return die('commit failed (pre-commit gate?). The rename is applied, fix and commit yourself') }
|
|
87
|
+
}
|
|
88
|
+
io.print(`ok: ${oldBase} → ${newBase}${o.commit === true ? ' (committed)' : ''}`)
|
|
89
|
+
return 0
|
|
90
|
+
}
|
package/src/serve/run-serve.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// kit.ts loads. Port: --port > serve.port > a deterministic house-range
|
|
6
6
|
// default (4800-4979, hashed from the repo name — stable across launches;
|
|
7
7
|
// pin serve.port when two repos collide).
|
|
8
|
-
import { existsSync, readdirSync, statSync } from 'fs'
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
9
9
|
import { join } from 'path'
|
|
10
10
|
import { fileURLToPath } from 'url'
|
|
11
11
|
import { loadKit, allAssets, type ResolvedKit } from '../assets/kit-config.ts'
|
|
@@ -63,6 +63,15 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// the hub must KNOW when it is outdated (field bug 2026-07-22: a morning
|
|
67
|
+
// process served stale code all day, picks looked broken): version read at
|
|
68
|
+
// boot vs on disk per attention load; mismatch = red line telling him to
|
|
69
|
+
// restart the task
|
|
70
|
+
const libVersion = (): string => {
|
|
71
|
+
try { return (JSON.parse(readFileSync(fileURLToPath(new URL('../../package.json', import.meta.url)), 'utf8')) as { version?: string }).version ?? '?' } catch { return '?' }
|
|
72
|
+
}
|
|
73
|
+
const BOOT_VERSION = libVersion()
|
|
74
|
+
|
|
66
75
|
// ── the / dashboard's data: everything red, one collector per source, each
|
|
67
76
|
// failure-isolated (a broken source reports itself, never blanks the page)
|
|
68
77
|
export function attentionCollector(root: string, c: RepoConfig, kit: ResolvedKit | null): () => Promise<AttentionSection[]> {
|
|
@@ -71,6 +80,11 @@ export function attentionCollector(root: string, c: RepoConfig, kit: ResolvedKit
|
|
|
71
80
|
}
|
|
72
81
|
return async () => {
|
|
73
82
|
const sections = await Promise.all([
|
|
83
|
+
guard('hub process', 'red', async () => {
|
|
84
|
+
const now = libVersion()
|
|
85
|
+
if (now === BOOT_VERSION) return []
|
|
86
|
+
return [{ text: `hub is running shipkit ${BOOT_VERSION} but ${now} is on disk (stale process, stale pages, old studio code). Restart the 🚀 shipkit task` }]
|
|
87
|
+
}),
|
|
74
88
|
guard('failing checks', 'red', async () => {
|
|
75
89
|
const p = join(root, 'STATUS.md')
|
|
76
90
|
if (!existsSync(p)) return []
|
package/src/shipkit.ts
CHANGED
|
@@ -58,3 +58,5 @@ export { lintMd } from './wiki/lint-md.ts'
|
|
|
58
58
|
export { serveMdGraph } from './wiki/serve-graph.ts'
|
|
59
59
|
export { graphHtml } from './wiki/graph-page.ts'
|
|
60
60
|
export type { MdGraph, MdNode, MdEdge } from './wiki/md-graph.ts'
|
|
61
|
+
export { movePage } from './mv/mv-md.ts'
|
|
62
|
+
export type { MvIo, MvOpts } from './mv/mv-md.ts'
|
|
@@ -0,0 +1,50 @@
|
|
|
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).
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
|
|
10
|
+
export type StartOpts = {
|
|
11
|
+
/** parent dir for repos — default ~/dev */
|
|
12
|
+
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 */
|
|
16
|
+
alsoOk?: string[]
|
|
17
|
+
/** repo names that never get the include */
|
|
18
|
+
skip?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
22
|
+
|
|
23
|
+
export function startRepo(name: string, opts: StartOpts = {}): number {
|
|
24
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
|
|
25
|
+
console.error(`FAIL: bad repo name '${name}' — letters/digits/._- only, no leading dot, no slashes`)
|
|
26
|
+
return 2
|
|
27
|
+
}
|
|
28
|
+
const dir = join(opts.root ?? join(homedir(), 'dev'), name)
|
|
29
|
+
if (!existsSync(dir)) {
|
|
30
|
+
mkdirSync(dir, { recursive: true })
|
|
31
|
+
console.error(`ok: created ${dir}`)
|
|
32
|
+
}
|
|
33
|
+
if (opts.include == null || (opts.skip ?? []).includes(name)) {
|
|
34
|
+
if (opts.include != null) 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, `${opts.include}\n`)
|
|
39
|
+
console.error('ok: CLAUDE.md created with the include')
|
|
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')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log(dir)
|
|
49
|
+
return 0
|
|
50
|
+
}
|
package/src/tui/run-tui.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { TuiStore } from './tui-store.ts'
|
|
|
13
13
|
|
|
14
14
|
export async function runTui(root: string): Promise<number> {
|
|
15
15
|
const c = await loadRepoConfig(root)
|
|
16
|
-
// playbook (build/mobx): enforceActions warns on stray mutations — the
|
|
16
|
+
// playbook (build/mobx-mechanics): enforceActions warns on stray mutations — the
|
|
17
17
|
// only signal. Read-warning flags stay off: useInput handler reads are
|
|
18
18
|
// legitimately untracked and would false-positive.
|
|
19
19
|
configure({ enforceActions: 'always' })
|
package/src/tui/tui-store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// TUI state — MobX per the house playbook (build/state + build/mobx):
|
|
1
|
+
// TUI state — MobX per the house playbook (build/app-state-tree + build/mobx-mechanics):
|
|
2
2
|
// `makeAutoObservable` WITH exceptions (the 2026-07-21 doctrine — no
|
|
3
3
|
// decorators), runInAction around every post-await mutation. Panels are
|
|
4
4
|
// plain data providers (no mobx): they signal mutations through update(),
|