rvlib-shipkit 0.1.1 → 0.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.
- package/CHANGELOG.md +137 -0
- package/README.md +23 -8
- package/package.json +15 -3
- package/src/asks/asks-queue.ts +20 -7
- package/src/assets/gen/comfy-gen.ts +106 -0
- package/src/assets/gen/generate-roll.ts +57 -0
- package/src/assets/gen/workflow-loader.ts +67 -0
- package/src/assets/image-ops.ts +30 -0
- package/src/assets/json-store.ts +27 -0
- package/src/assets/kit-config.ts +179 -0
- package/src/assets/kits/identity-kit.ts +119 -0
- package/src/assets/native-deps.ts +18 -0
- package/src/assets/panels/geometry-rects.ts +84 -0
- package/src/assets/roll-workspace.ts +41 -0
- package/src/assets/run-assets.ts +142 -0
- package/src/assets/server/studio-page.ts +620 -0
- package/src/assets/server/studio-queue.ts +58 -0
- package/src/assets/server/studio-server.ts +412 -0
- package/src/assets/vite.ts +62 -0
- package/src/cli/cmd_Asks.ts +83 -0
- package/src/cli/cmd_Assets.ts +120 -0
- package/src/cli/cmd_Check.ts +30 -0
- package/src/cli/cmd_Ci.ts +41 -0
- package/src/cli/cmd_Contracts.ts +35 -0
- package/src/cli/cmd_Init.ts +28 -0
- package/src/cli/cmd_Serve.ts +34 -0
- package/src/cli/cmd_Tui.ts +36 -0
- package/src/cli/cmd_Wiki.ts +53 -0
- package/src/cli.ts +44 -25
- package/src/contracts/run-contracts.ts +33 -0
- package/src/core/check-ctx.ts +3 -16
- package/src/core/check-suite.ts +16 -2
- package/src/core/discover-checks.ts +48 -0
- package/src/core/run-check.ts +73 -0
- package/src/core/run-ci.ts +54 -0
- package/src/core/spawn-timeout.ts +65 -0
- package/src/core/task-runner.ts +36 -43
- package/src/core/wire.ts +20 -0
- package/src/core/write-status.ts +70 -0
- package/src/init-repo.ts +60 -0
- package/src/repo-config.ts +82 -15
- package/src/serve/hub-pages.ts +84 -0
- package/src/serve/run-serve.ts +85 -0
- package/src/serve/serve-hub.ts +77 -0
- package/src/shipkit.ts +39 -3
- package/src/tui/checks-panel.ts +39 -0
- package/src/tui/tasks-panel.ts +54 -0
- package/src/tui/tui-app.tsx +91 -0
- package/src/tui/tui-panel.ts +57 -0
- package/src/tui/tui-store.ts +98 -0
- package/src/wiki/run-wiki.ts +50 -0
- package/src/cli/shipkit-asks.ts +0 -26
- package/src/cli/shipkit-check.ts +0 -26
- package/src/cli/shipkit-ci.ts +0 -30
- package/src/cli/shipkit-contracts.ts +0 -26
- package/src/cli/shipkit-init.ts +0 -41
- package/src/cli/shipkit-wiki.ts +0 -74
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// shipkit assets — asset atelier: declare kinds in .rv-assets/kit.ts, roll
|
|
2
|
+
// (ComfyUI or procedural canvas), curate in the studio, ship with metadata
|
|
3
|
+
// write-through. Thin clipanion surfaces; logic in assets/run-assets.ts.
|
|
4
|
+
import { Command, Option } from 'clipanion'
|
|
5
|
+
import { runAssetsCheck, runAssetsGen, runAssetsIdentity, runAssetsServe, runAssetsTopUp, type AssetsIo } from '../assets/run-assets.ts'
|
|
6
|
+
|
|
7
|
+
const ioOf = (cmd: Command): AssetsIo => ({
|
|
8
|
+
print: (l) => cmd.context.stdout.write(`${l}\n`),
|
|
9
|
+
printErr: (l) => cmd.context.stderr.write(`${l}\n`),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export class AssetsServeCommand extends Command {
|
|
13
|
+
static override paths = [['assets', 'serve']]
|
|
14
|
+
|
|
15
|
+
static override usage = Command.Usage({
|
|
16
|
+
category: 'Assets',
|
|
17
|
+
description: 'the studio — roll, curate, pick, ship (127.0.0.1, kit port)',
|
|
18
|
+
examples: [['launch the studio for the cwd kit', 'shipkit assets serve']],
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
async execute(): Promise<number> {
|
|
22
|
+
return runAssetsServe(process.cwd(), ioOf(this))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AssetsFillCommand extends Command {
|
|
27
|
+
static override paths = [['assets', 'fill']]
|
|
28
|
+
|
|
29
|
+
static override usage = Command.Usage({
|
|
30
|
+
category: 'Assets',
|
|
31
|
+
description: 'top rolls up to N per asset (canvas or comfy), sparsest-first',
|
|
32
|
+
examples: [
|
|
33
|
+
['every asset up to 8 rolls', 'shipkit assets fill 8'],
|
|
34
|
+
['only two categories', 'shipkit assets fill 8 icons sprites'],
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
n = Option.String({ required: true })
|
|
39
|
+
categories = Option.Rest()
|
|
40
|
+
|
|
41
|
+
async execute(): Promise<number> {
|
|
42
|
+
const n = Number(this.n)
|
|
43
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
44
|
+
this.context.stderr.write(`FAIL: '${this.n}' is not a positive roll count — usage: shipkit assets fill <N> [category…]\n`)
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
return runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'fill', n, categories: this.categories })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class AssetsAddCommand extends Command {
|
|
52
|
+
static override paths = [['assets', 'add']]
|
|
53
|
+
|
|
54
|
+
static override usage = Command.Usage({
|
|
55
|
+
category: 'Assets',
|
|
56
|
+
description: 'N more rolls per asset (canvas or comfy), regardless of what it has',
|
|
57
|
+
examples: [['3 fresh rolls everywhere', 'shipkit assets add 3']],
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
n = Option.String({ required: true })
|
|
61
|
+
categories = Option.Rest()
|
|
62
|
+
|
|
63
|
+
async execute(): Promise<number> {
|
|
64
|
+
const n = Number(this.n)
|
|
65
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
66
|
+
this.context.stderr.write(`FAIL: '${this.n}' is not a positive roll count — usage: shipkit assets add <N> [category…]\n`)
|
|
67
|
+
return 1
|
|
68
|
+
}
|
|
69
|
+
return runAssetsTopUp(process.cwd(), ioOf(this), { mode: 'add', n, categories: this.categories })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class AssetsGenCommand extends Command {
|
|
74
|
+
static override paths = [['assets', 'gen']]
|
|
75
|
+
|
|
76
|
+
static override usage = Command.Usage({
|
|
77
|
+
category: 'Assets',
|
|
78
|
+
description: 'one-shot rolls for a single asset (canvas or comfy)',
|
|
79
|
+
examples: [
|
|
80
|
+
['one roll', 'shipkit assets gen hero'],
|
|
81
|
+
['five rolls', 'shipkit assets gen hero --n 5'],
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
assetId = Option.String({ required: true })
|
|
86
|
+
n = Option.String('--n', '1', { description: 'how many rolls (default 1)' })
|
|
87
|
+
|
|
88
|
+
async execute(): Promise<number> {
|
|
89
|
+
const n = Math.max(1, Number(this.n) || 1)
|
|
90
|
+
return runAssetsGen(process.cwd(), ioOf(this), { assetId: this.assetId, n })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class AssetsIdentityCommand extends Command {
|
|
95
|
+
static override paths = [['assets', 'identity']]
|
|
96
|
+
|
|
97
|
+
static override usage = Command.Usage({
|
|
98
|
+
category: 'Assets',
|
|
99
|
+
description: 'render favicon/logo/OG from .rv-identity/brand.ts into public/',
|
|
100
|
+
examples: [['regenerate the identity outputs', 'shipkit assets identity']],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
async execute(): Promise<number> {
|
|
104
|
+
return runAssetsIdentity(process.cwd(), ioOf(this))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class AssetsCheckCommand extends Command {
|
|
109
|
+
static override paths = [['assets', 'check']]
|
|
110
|
+
|
|
111
|
+
static override usage = Command.Usage({
|
|
112
|
+
category: 'Assets',
|
|
113
|
+
description: 'kit loads, kinds valid, workflows bound, backend reachable, identity outputs present',
|
|
114
|
+
examples: [['health-check the asset pipeline', 'shipkit assets check']],
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
async execute(): Promise<number> {
|
|
118
|
+
return runAssetsCheck(process.cwd(), ioOf(this))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// shipkit check — periodic health, TTL-cached. Thin clipanion surface;
|
|
2
|
+
// logic in core/run-check.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { runCheck } from '../core/run-check.ts'
|
|
5
|
+
|
|
6
|
+
export class CheckCommand extends Command {
|
|
7
|
+
static override paths = [['check']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Health',
|
|
11
|
+
description: 'periodic health: contracts + every *.check.ts in the repo (auto-discovered, run as subprocesses), clean passes replay from a TTL cache',
|
|
12
|
+
examples: [
|
|
13
|
+
['run (cached where valid)', 'shipkit check'],
|
|
14
|
+
['bust every cache layer', 'shipkit check --fresh'],
|
|
15
|
+
['machine-readable', 'shipkit check --json'],
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
20
|
+
fresh = Option.Boolean('--fresh', false, { description: 'bust every TTL cache layer (travels to child checks)' })
|
|
21
|
+
|
|
22
|
+
async execute(): Promise<number> {
|
|
23
|
+
try {
|
|
24
|
+
return await runCheck(process.cwd(), { json: this.json, fresh: this.fresh, print: (l) => this.context.stdout.write(`${l}\n`) })
|
|
25
|
+
} catch (e) {
|
|
26
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
27
|
+
return 1
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// shipkit ci — the commit/deploy gate. Thin clipanion surface; logic in
|
|
2
|
+
// core/run-ci.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { runCi } from '../core/run-ci.ts'
|
|
5
|
+
|
|
6
|
+
export class CiCommand extends Command {
|
|
7
|
+
static override paths = [['ci']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Gate',
|
|
11
|
+
description: 'commit gate: floor + contracts + repo tasks through the DAG runner',
|
|
12
|
+
examples: [
|
|
13
|
+
['full gate', 'shipkit ci'],
|
|
14
|
+
['stop at the first failure', 'shipkit ci --bail'],
|
|
15
|
+
['one task + its dep closure', 'shipkit ci --only typecheck'],
|
|
16
|
+
['include an opt-in task', 'shipkit ci --with up:itch'],
|
|
17
|
+
],
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
bail = Option.Boolean('--bail', false, { description: 'stop at the first failure' })
|
|
21
|
+
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
22
|
+
skip = Option.Array('--skip', [], { description: 'skip a task group (repeatable)' })
|
|
23
|
+
withIds = Option.Array('--with', [], { description: 'include an opt-in task (repeatable)' })
|
|
24
|
+
only = Option.Array('--only', [], { description: 'run only this task + its dep closure (repeatable)' })
|
|
25
|
+
|
|
26
|
+
async execute(): Promise<number> {
|
|
27
|
+
try {
|
|
28
|
+
return await runCi(process.cwd(), {
|
|
29
|
+
bail: this.bail,
|
|
30
|
+
json: this.json,
|
|
31
|
+
skipGroups: this.skip,
|
|
32
|
+
withIds: this.withIds,
|
|
33
|
+
onlyIds: this.only,
|
|
34
|
+
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
35
|
+
})
|
|
36
|
+
} catch (e) {
|
|
37
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// shipkit contracts — venue fulfillness alone. Thin clipanion surface;
|
|
2
|
+
// logic in contracts/run-contracts.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { runContracts } from '../contracts/run-contracts.ts'
|
|
5
|
+
|
|
6
|
+
export class ContractsCommand extends Command {
|
|
7
|
+
static override paths = [['contracts']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Gate',
|
|
11
|
+
description: 'release-contract fulfillness, fast and filesystem-only — deploy tasks dep on it',
|
|
12
|
+
examples: [
|
|
13
|
+
['all applicable contracts', 'shipkit contracts'],
|
|
14
|
+
['one venue', 'shipkit contracts --venue itch'],
|
|
15
|
+
['machine-readable', 'shipkit contracts --json'],
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
venue = Option.String('--venue', { required: false, description: 'check one venue only (itch, steam, …)' })
|
|
20
|
+
json = Option.Boolean('--json', false, { description: 'machine-readable result instead of wire lines' })
|
|
21
|
+
|
|
22
|
+
async execute(): Promise<number> {
|
|
23
|
+
try {
|
|
24
|
+
return await runContracts(process.cwd(), {
|
|
25
|
+
venue: this.venue,
|
|
26
|
+
json: this.json,
|
|
27
|
+
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
28
|
+
printErr: (l) => this.context.stderr.write(`${l}\n`),
|
|
29
|
+
})
|
|
30
|
+
} catch (e) {
|
|
31
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
32
|
+
return 1
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// shipkit init — scaffold repo.config.ts + package.json scripts. Thin
|
|
2
|
+
// clipanion surface; logic in init-repo.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { initRepo } from '../init-repo.ts'
|
|
5
|
+
|
|
6
|
+
export class InitCommand extends Command {
|
|
7
|
+
static override paths = [['init']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Setup',
|
|
11
|
+
description: 'scaffold repo.config.ts + ci/check/contracts package.json scripts, then print how the floor resolved',
|
|
12
|
+
examples: [
|
|
13
|
+
['bootstrap the current repo', 'shipkit init'],
|
|
14
|
+
['in-repo-source consumer: scripts run the CLI by path', 'shipkit init --bin wiki/build/shipkit/rvlib-shipkit/src/cli.ts'],
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
bin = Option.String('--bin', { description: 'path to shipkit cli.ts — scripts become `bun <path> <cmd>` instead of `shipkit <cmd>`' })
|
|
19
|
+
|
|
20
|
+
async execute(): Promise<number> {
|
|
21
|
+
try {
|
|
22
|
+
return await initRepo(process.cwd(), { bin: this.bin })
|
|
23
|
+
} catch (e) {
|
|
24
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
25
|
+
return 1
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// shipkit serve — the ONE hub server per repo (one vscode folder-open task,
|
|
2
|
+
// clickable URLs). Thin clipanion surface; logic in serve/run-serve.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { runServe } from '../serve/run-serve.ts'
|
|
5
|
+
|
|
6
|
+
export class ServeCommand extends Command {
|
|
7
|
+
static override paths = [['serve']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Serve',
|
|
11
|
+
description: 'ONE hub server per repo: landing page linking the wiki/agent graphs, the assets studio, and live status',
|
|
12
|
+
examples: [
|
|
13
|
+
['serve the repo toolbox', 'shipkit serve'],
|
|
14
|
+
['pin the port for this launch', 'shipkit serve --port 4804'],
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
port = Option.String('--port', { required: false, description: 'hub port (default: repo.config serve.port, else hashed from the repo name into 4800-4979)' })
|
|
19
|
+
|
|
20
|
+
async execute(): Promise<number> {
|
|
21
|
+
let port: number | undefined
|
|
22
|
+
if (this.port != null) {
|
|
23
|
+
port = Number(this.port)
|
|
24
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
25
|
+
this.context.stderr.write(`FAIL --port '${this.port}' is not a port number\n`)
|
|
26
|
+
return 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return runServe(process.cwd(), {
|
|
30
|
+
print: (l) => this.context.stdout.write(`${l}\n`),
|
|
31
|
+
printErr: (l) => this.context.stderr.write(`${l}\n`),
|
|
32
|
+
}, { port })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// shipkit tui — the live dashboard: Tasks (the ci gate, streamed through
|
|
2
|
+
// the DAG runner) + Checks (contracts + discovered *.check.ts) built in,
|
|
3
|
+
// repo extras injected via repo.config `tui: { panels: [...] }`.
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render } from 'ink'
|
|
6
|
+
import { configure } from 'mobx'
|
|
7
|
+
import { Command } from 'clipanion'
|
|
8
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
9
|
+
import { checksPanel } from '../tui/checks-panel.ts'
|
|
10
|
+
import { tasksPanel } from '../tui/tasks-panel.ts'
|
|
11
|
+
import { TuiApp } from '../tui/tui-app.tsx'
|
|
12
|
+
import { TuiStore } from '../tui/tui-store.ts'
|
|
13
|
+
|
|
14
|
+
export class TuiCommand extends Command {
|
|
15
|
+
static override paths = [['tui']]
|
|
16
|
+
|
|
17
|
+
static override usage = Command.Usage({
|
|
18
|
+
category: 'Health',
|
|
19
|
+
description: 'live dashboard: ci tasks + discovered checks, plus panels the repo injects via repo.config tui.panels',
|
|
20
|
+
examples: [['open the dashboard', 'shipkit tui']],
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
async execute(): Promise<number> {
|
|
24
|
+
const root = process.cwd()
|
|
25
|
+
const c = await loadRepoConfig(root)
|
|
26
|
+
// playbook (build/mobx): enforceActions warns on stray mutations — the
|
|
27
|
+
// only signal. Read-warning flags stay off: useInput handler reads are
|
|
28
|
+
// legitimately untracked and would false-positive.
|
|
29
|
+
configure({ enforceActions: 'always' })
|
|
30
|
+
const store = new TuiStore(c.name, [tasksPanel(root, c), checksPanel(root), ...(c.tui?.panels ?? [])])
|
|
31
|
+
store.refresh() // instant static paint; results stream in
|
|
32
|
+
const app = render(React.createElement(TuiApp, { store }))
|
|
33
|
+
await app.waitUntilExit()
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// shipkit wiki — md link-graph server + orphan/dangling lint. Two thin
|
|
2
|
+
// commands over wiki/run-wiki.ts.
|
|
3
|
+
import { Command, Option } from 'clipanion'
|
|
4
|
+
import { runWikiLint, runWikiServe } from '../wiki/run-wiki.ts'
|
|
5
|
+
|
|
6
|
+
export class WikiServeCommand extends Command {
|
|
7
|
+
static override paths = [['wiki', 'serve']]
|
|
8
|
+
|
|
9
|
+
static override usage = Command.Usage({
|
|
10
|
+
category: 'Wiki',
|
|
11
|
+
description: 'live md link-graph server — reload = rescan',
|
|
12
|
+
examples: [
|
|
13
|
+
['serve the cwd wiki', 'shipkit wiki serve'],
|
|
14
|
+
['serve a subtree on a port', 'shipkit wiki serve --root agent --index process.md --port 6162'],
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
root = Option.String('--root', '.', { description: 'wiki root directory' })
|
|
19
|
+
port = Option.String('--port', '6161', { description: 'server port' })
|
|
20
|
+
title = Option.String('--title', { required: false, description: 'page title shown in the graph header' })
|
|
21
|
+
index = Option.String('--index', { required: false, description: 'entrypoint page (default: index.md AND README.md)' })
|
|
22
|
+
skip = Option.String('--skip', { required: false, description: 'comma-separated dir names to exclude from the scan' })
|
|
23
|
+
|
|
24
|
+
async execute(): Promise<number> {
|
|
25
|
+
const port = Number(this.port)
|
|
26
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
27
|
+
this.context.stderr.write(`FAIL --port '${this.port}' is not a port number\n`)
|
|
28
|
+
return 1
|
|
29
|
+
}
|
|
30
|
+
return runWikiServe({ root: this.root, index: this.index, skip: this.skip, port, title: this.title })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class WikiLintCommand extends Command {
|
|
35
|
+
static override paths = [['wiki', 'lint']]
|
|
36
|
+
|
|
37
|
+
static override usage = Command.Usage({
|
|
38
|
+
category: 'Wiki',
|
|
39
|
+
description: 'fail on orphan pages and dangling refs (exit 1)',
|
|
40
|
+
examples: [
|
|
41
|
+
['lint the cwd wiki', 'shipkit wiki lint'],
|
|
42
|
+
['lint a subtree', 'shipkit wiki lint --root agent --index process.md'],
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
root = Option.String('--root', '.', { description: 'wiki root directory' })
|
|
47
|
+
index = Option.String('--index', { required: false, description: 'entrypoint page (default: index.md AND README.md)' })
|
|
48
|
+
skip = Option.String('--skip', { required: false, description: 'comma-separated dir names to exclude from the scan' })
|
|
49
|
+
|
|
50
|
+
async execute(): Promise<number> {
|
|
51
|
+
return runWikiLint({ root: this.root, index: this.index, skip: this.skip })
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// shipkit —
|
|
3
|
-
|
|
2
|
+
// shipkit — clipanion entry: one command per file in cli/ (cmd_<Name>.ts),
|
|
3
|
+
// logic in plain modules the commands call. runExit sets the exit code
|
|
4
|
+
// without force-exiting, so serve commands keep the process alive.
|
|
5
|
+
import { readFileSync } from 'fs'
|
|
6
|
+
import { Builtins, Cli } from 'clipanion'
|
|
7
|
+
import { AskNewCommand, AsksCheckCommand, AsksCommand } from './cli/cmd_Asks.ts'
|
|
8
|
+
import { AssetsAddCommand, AssetsCheckCommand, AssetsFillCommand, AssetsGenCommand, AssetsIdentityCommand, AssetsServeCommand } from './cli/cmd_Assets.ts'
|
|
9
|
+
import { CheckCommand } from './cli/cmd_Check.ts'
|
|
10
|
+
import { CiCommand } from './cli/cmd_Ci.ts'
|
|
11
|
+
import { ContractsCommand } from './cli/cmd_Contracts.ts'
|
|
12
|
+
import { InitCommand } from './cli/cmd_Init.ts'
|
|
13
|
+
import { ServeCommand } from './cli/cmd_Serve.ts'
|
|
14
|
+
import { TuiCommand } from './cli/cmd_Tui.ts'
|
|
15
|
+
import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
|
|
4
16
|
|
|
5
|
-
const
|
|
6
|
-
const args = process.argv.slice(3)
|
|
7
|
-
switch (cmd) {
|
|
8
|
-
case 'ci': return (await import('./cli/shipkit-ci.ts')).main(args)
|
|
9
|
-
case 'check': return (await import('./cli/shipkit-check.ts')).main(args)
|
|
10
|
-
case 'contracts': return (await import('./cli/shipkit-contracts.ts')).main(args)
|
|
11
|
-
case 'asks': return (await import('./cli/shipkit-asks.ts')).main(args)
|
|
12
|
-
case 'ask': return (await import('./cli/shipkit-asks.ts')).main(['new', ...args])
|
|
13
|
-
case 'wiki': return (await import('./cli/shipkit-wiki.ts')).main(args)
|
|
14
|
-
case 'init': return (await import('./cli/shipkit-init.ts')).main(args)
|
|
15
|
-
default:
|
|
16
|
-
console.log(`shipkit — repo toolkit
|
|
17
|
-
usage: shipkit <command>
|
|
18
|
-
ci commit gate: floor + contracts + repo tasks [--bail --skip <group> --with <id> --only <id...> --json]
|
|
19
|
-
check periodic health: contracts + repo checks, TTL'd [--json --fresh]
|
|
20
|
-
contracts contract fulfillness alone [--venue <v> --json]
|
|
21
|
-
asks list open asks · asks check · ask "<title>" [--kind --blocks --check "<cmd>"]
|
|
22
|
-
wiki serve|lint — md-wiki graph + link hygiene
|
|
23
|
-
init scaffold repo.config.ts + package.json scripts`)
|
|
24
|
-
return 2
|
|
25
|
-
}
|
|
26
|
-
}
|
|
17
|
+
const version = (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }).version
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
const cli = new Cli({
|
|
20
|
+
binaryLabel: 'shipkit — repo toolkit',
|
|
21
|
+
binaryName: 'shipkit',
|
|
22
|
+
binaryVersion: version,
|
|
23
|
+
enableCapture: true, // console output pipes into the command streams (testable)
|
|
24
|
+
enableColors: true,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
cli.register(Builtins.HelpCommand)
|
|
28
|
+
cli.register(Builtins.VersionCommand)
|
|
29
|
+
cli.register(CiCommand)
|
|
30
|
+
cli.register(CheckCommand)
|
|
31
|
+
cli.register(TuiCommand)
|
|
32
|
+
cli.register(ServeCommand)
|
|
33
|
+
cli.register(ContractsCommand)
|
|
34
|
+
cli.register(AsksCommand)
|
|
35
|
+
cli.register(AsksCheckCommand)
|
|
36
|
+
cli.register(AskNewCommand)
|
|
37
|
+
cli.register(WikiServeCommand)
|
|
38
|
+
cli.register(WikiLintCommand)
|
|
39
|
+
cli.register(AssetsServeCommand)
|
|
40
|
+
cli.register(AssetsFillCommand)
|
|
41
|
+
cli.register(AssetsAddCommand)
|
|
42
|
+
cli.register(AssetsGenCommand)
|
|
43
|
+
cli.register(AssetsIdentityCommand)
|
|
44
|
+
cli.register(AssetsCheckCommand)
|
|
45
|
+
cli.register(InitCommand)
|
|
46
|
+
|
|
47
|
+
cli.runExit(process.argv.slice(2))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// contract fulfillness alone, fast and filesystem-only. Plain module
|
|
2
|
+
// behind cmd_Contracts; venue deploy scripts run it before any upload.
|
|
3
|
+
import { activeContracts, contractsConfigFrom, loadRepoConfig } from '../repo-config.ts'
|
|
4
|
+
import { evaluateContracts } from './contracts-engine.ts'
|
|
5
|
+
import { formatLine } from '../core/wire.ts'
|
|
6
|
+
|
|
7
|
+
export type ContractsOpts = {
|
|
8
|
+
venue?: string
|
|
9
|
+
json?: boolean
|
|
10
|
+
/** line sinks — commands pass their context streams; default to console */
|
|
11
|
+
print?: (line: string) => void
|
|
12
|
+
printErr?: (line: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runContracts(root: string, o: ContractsOpts = {}): Promise<number> {
|
|
16
|
+
const print = o.print ?? ((l: string) => console.log(l))
|
|
17
|
+
const printErr = o.printErr ?? ((l: string) => console.error(l))
|
|
18
|
+
const c = await loadRepoConfig(root)
|
|
19
|
+
let manifests = activeContracts(c)
|
|
20
|
+
if (o.venue != null) {
|
|
21
|
+
manifests = manifests.filter((m) => m.venue === o.venue)
|
|
22
|
+
if (manifests.length === 0) { printErr(`FAIL: unknown venue '${o.venue}'`); return 1 }
|
|
23
|
+
}
|
|
24
|
+
const r = await evaluateContracts(manifests, contractsConfigFrom(c, root))
|
|
25
|
+
if (o.json === true) {
|
|
26
|
+
print(JSON.stringify({ ok: r.exitCode === 0, outcomes: r.outcomes }))
|
|
27
|
+
return r.exitCode
|
|
28
|
+
}
|
|
29
|
+
const color = process.stdout.isTTY && process.env.NO_COLOR == null
|
|
30
|
+
for (const l of r.lines) print(formatLine(l, color))
|
|
31
|
+
if (r.lines.length === 0) print('ok: no contracts apply (nothing declared, nothing detected)')
|
|
32
|
+
return r.exitCode
|
|
33
|
+
}
|
package/src/core/check-ctx.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// CheckCtx — the per-check handle a probe's logic receives: outcome lines
|
|
2
2
|
// (wire.ts kinds), guarded fetch/exec helpers, resolved-tool lookup.
|
|
3
|
-
import {
|
|
3
|
+
import { spawnWithTimeout } from './spawn-timeout.ts'
|
|
4
4
|
import type { Line } from './wire.ts'
|
|
5
5
|
|
|
6
6
|
// thrown by t.fetch on 429 — the suite converts it to WARN, never FAIL
|
|
@@ -43,21 +43,8 @@ export class CheckCtx {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// spawn without the Bun.spawn throw-on-missing-binary gotcha; SIGTERM at
|
|
46
|
-
// timeoutMs, SIGKILL 2s later (
|
|
46
|
+
// timeoutMs, SIGKILL 2s later (one code path: core/spawn-timeout.ts)
|
|
47
47
|
async exec(cmd: string[], opts?: { env?: Record<string, string | undefined>; cwd?: string; timeoutMs?: number }): Promise<{ code: number; out: string; timedOut: boolean }> {
|
|
48
|
-
|
|
49
|
-
if (head == null) return { code: -1, out: 'empty command', timedOut: false }
|
|
50
|
-
const bin = head.includes('/') ? head : Bun.which(head)
|
|
51
|
-
if (bin == null || !existsSync(bin)) return { code: -1, out: `${head}: not found`, timedOut: false }
|
|
52
|
-
const p = Bun.spawn([bin, ...cmd.slice(1)], { stdout: 'pipe', stderr: 'pipe', env: opts?.env as Record<string, string>, cwd: opts?.cwd })
|
|
53
|
-
let timedOut = false
|
|
54
|
-
const ms = opts?.timeoutMs ?? 15_000
|
|
55
|
-
const timer = setTimeout(() => { timedOut = true; p.kill() }, ms)
|
|
56
|
-
const killer = setTimeout(() => p.kill('SIGKILL'), ms + 2000)
|
|
57
|
-
const out = (await new Response(p.stdout).text()) + (await new Response(p.stderr).text())
|
|
58
|
-
const code = await p.exited
|
|
59
|
-
clearTimeout(timer)
|
|
60
|
-
clearTimeout(killer)
|
|
61
|
-
return { code, out, timedOut }
|
|
48
|
+
return spawnWithTimeout(cmd, { timeoutMs: opts?.timeoutMs ?? 15_000, env: opts?.env, cwd: opts?.cwd })
|
|
62
49
|
}
|
|
63
50
|
}
|
package/src/core/check-suite.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type SecretSpec = {
|
|
|
27
27
|
skipHint: string
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export type CheckOutcome = { lines: Line[]; status: 'pass' | 'fail' | 'warn' | 'skip'; cached: boolean }
|
|
31
|
+
|
|
30
32
|
export type CheckSuiteOpts = {
|
|
31
33
|
name: string
|
|
32
34
|
cacheDir: string
|
|
@@ -35,6 +37,9 @@ export type CheckSuiteOpts = {
|
|
|
35
37
|
getSecret?: GetSecret
|
|
36
38
|
/** env var that busts the cache when set (consumer names it) */
|
|
37
39
|
freshEnv?: string
|
|
40
|
+
/** per-check completion tap (the TUI streams results in) — cached replays
|
|
41
|
+
* report cached:true, prep-skips (unstored secret) report status 'skip' */
|
|
42
|
+
onResult?: (name: string, r: CheckOutcome) => void
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export class CheckSuite {
|
|
@@ -45,6 +50,7 @@ export class CheckSuite {
|
|
|
45
50
|
private now: () => number
|
|
46
51
|
private getSecret: GetSecret
|
|
47
52
|
private freshEnv: string
|
|
53
|
+
private onResult?: (name: string, r: CheckOutcome) => void
|
|
48
54
|
|
|
49
55
|
constructor(opts: CheckSuiteOpts) {
|
|
50
56
|
this.name = opts.name
|
|
@@ -52,6 +58,7 @@ export class CheckSuite {
|
|
|
52
58
|
this.now = opts.now ?? Date.now
|
|
53
59
|
this.getSecret = opts.getSecret ?? (() => null)
|
|
54
60
|
this.freshEnv = opts.freshEnv ?? 'SHIPKIT_FRESH'
|
|
61
|
+
this.onResult = opts.onResult
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
addCheck(spec: { name: string; remainValidFor?: string | number; version?: number; logic: Logic }): void {
|
|
@@ -124,7 +131,11 @@ export class CheckSuite {
|
|
|
124
131
|
|
|
125
132
|
for (const r of this.runners) {
|
|
126
133
|
const prep = r.prep?.() ?? {}
|
|
127
|
-
if (prep.skip != null) {
|
|
134
|
+
if (prep.skip != null) {
|
|
135
|
+
skipped.push(prep.skip)
|
|
136
|
+
this.onResult?.(r.name, { lines: [{ kind: 'skip', text: prep.skip }], status: 'skip', cached: false })
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
128
139
|
ran++
|
|
129
140
|
const key = prep.cacheExtra ?? ''
|
|
130
141
|
const entry = cache[r.name]
|
|
@@ -133,7 +144,9 @@ export class CheckSuite {
|
|
|
133
144
|
&& this.now() - entry.at < r.ttlMs && this.now() >= entry.at
|
|
134
145
|
if (fresh) {
|
|
135
146
|
const age = fmtAge(this.now() - entry.at)
|
|
136
|
-
|
|
147
|
+
const lines = entry.lines.map((l, i): Line => (i === 0 ? { ...l, text: `${l.text} (cached ${age} ago)` } : l))
|
|
148
|
+
out.push(...lines)
|
|
149
|
+
this.onResult?.(r.name, { lines, status: 'pass', cached: true }) // only clean passes are cached
|
|
137
150
|
continue
|
|
138
151
|
}
|
|
139
152
|
const t = new CheckCtx(this.toolPaths)
|
|
@@ -145,6 +158,7 @@ export class CheckSuite {
|
|
|
145
158
|
else t.fail(`${r.name} errored (${String(e).slice(0, 200)}) — network down, or a bug in this check`)
|
|
146
159
|
}
|
|
147
160
|
out.push(...t.lines)
|
|
161
|
+
this.onResult?.(r.name, { lines: t.lines, status: t.failed ? 'fail' : t.warned ? 'warn' : 'pass', cached: false })
|
|
148
162
|
if (t.failed) anyFail = true
|
|
149
163
|
if (r.ttlMs > 0) {
|
|
150
164
|
if (!t.failed && !t.warned) { cache[r.name] = { version: r.version, key, at: this.now(), lines: t.lines }; cacheDirty = true }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// *.check.ts auto-discovery — THE way health checks register (repo.config
|
|
2
|
+
// `checks:[]` is dead, one-runner GO 2026-07-22): drop a `<name>.check.ts`
|
|
3
|
+
// anywhere in the repo and `shipkit check` runs it as a subprocess — wire
|
|
4
|
+
// output parsed back, exit code is truth. Skipped dirs: node_modules, dist,
|
|
5
|
+
// tmp, references (templates for other repos), and every dot-dir (.git,
|
|
6
|
+
// caches, .rv-* contract folders).
|
|
7
|
+
|
|
8
|
+
import { readdirSync, readFileSync } from 'fs'
|
|
9
|
+
import { basename, join, relative } from 'path'
|
|
10
|
+
|
|
11
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', 'tmp', 'references'])
|
|
12
|
+
const SUFFIX = '.check.ts'
|
|
13
|
+
|
|
14
|
+
export type DiscoveredCheck = {
|
|
15
|
+
/** absolute path to the check file */
|
|
16
|
+
path: string
|
|
17
|
+
/** display + cache name — file base, disambiguated by relative path on collision */
|
|
18
|
+
name: string
|
|
19
|
+
/** file runs its own CheckSuite (own TTL cache) — the orchestrator must not double-cache it */
|
|
20
|
+
selfCached: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function discoverChecks(root: string): DiscoveredCheck[] {
|
|
24
|
+
const files: string[] = []
|
|
25
|
+
const walk = (dir: string): void => {
|
|
26
|
+
let entries
|
|
27
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
if (e.isDirectory()) {
|
|
30
|
+
if (!IGNORED_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name))
|
|
31
|
+
} else if (e.name.endsWith(SUFFIX)) files.push(join(dir, e.name))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
walk(root)
|
|
35
|
+
files.sort()
|
|
36
|
+
const baseOf = (f: string): string => basename(f).slice(0, -SUFFIX.length)
|
|
37
|
+
const seen = new Map<string, number>()
|
|
38
|
+
for (const f of files) seen.set(baseOf(f), (seen.get(baseOf(f)) ?? 0) + 1)
|
|
39
|
+
return files.map((f) => {
|
|
40
|
+
const base = baseOf(f)
|
|
41
|
+
const name = (seen.get(base) ?? 0) > 1 ? relative(root, f).slice(0, -SUFFIX.length) : base
|
|
42
|
+
// a check built on CheckSuite/SkillValidator replays from its OWN
|
|
43
|
+
// TTL cache — caching it again up here would double the staleness
|
|
44
|
+
let selfCached = false
|
|
45
|
+
try { selfCached = /SkillValidator|CheckSuite/.test(readFileSync(f, 'utf8')) } catch {}
|
|
46
|
+
return { path: f, name, selfCached }
|
|
47
|
+
})
|
|
48
|
+
}
|