rvlib-shipkit 0.35.4 → 0.35.6
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 +9 -0
- package/package.json +2 -1
- package/src/cli/cmd_Init.ts +1 -1
- package/src/core/run-check.ts +13 -4
- package/src/core/task-runner.ts +8 -2
- package/src/init-repo.ts +27 -0
- package/src/release/run-release.ts +18 -3
- package/src/scaffold/project-scaffold.ts +39 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.6
|
|
4
|
+
|
|
5
|
+
- **`shipkit init` furnishes an adopted repo with the folder-open half of the scaffold.** The day-one shape was written only for a dir with no commit history, so every repo wired AFTER it already existed got `repo.config.ts` and the scripts and no `.vscode/` at all: opening the folder started no watcher, and the hub the config advertises had no way in. `init` now writes `.vscode/settings.json` and `.vscode/tasks.json` on both its paths, the wiring run and the re-run, through the same writers and the same templates the start scaffold uses. The split it holds: a file that changes no output of any command lands on adopt, and anything saying what the repo IS (`tsconfig.json`, `src/index.ts`) or what an agent may DO in it (the verify skill, the `.claude/` policy files, the `agent/` wiki, the `.rv-reflections/` seed) still needs an explicit caller, because a repo with a history of its own already answered those. Create only, so a `.vscode/` a repo was forked with survives byte for byte, and a second run writes nothing and says so. The advertised port is resolved from `repo.config.ts` (`serve.port`, else derived from the config name), which is where the hub reads it, rather than from the folder name or a stale `package.json`. A `.vscode` path that cannot hold the two files (a read only tree, a path that is a file) costs one WARN naming the errno: this write runs after `repo.config.ts` is on disk and before the publish chain, and it is the least important thing the verb does, so it never kills the run.
|
|
6
|
+
|
|
7
|
+
## 0.35.5
|
|
8
|
+
|
|
9
|
+
- **The `TaskDef.inputs` doc comment stated only half the safety rule.** It said undeclared means never cached, so forgetting costs speed and never correctness, which is true for forgetting a whole task and false for the mistake that actually ships a false green: a list MISSING a path the command reads. The key cannot move when that path changes, so the answer replays over a tree it no longer describes. Observed twice in the field in one day. The comment now carries the asymmetry, names `['.']` as the declaration that cannot under declare, and says that a task carries no code closure, so the script it runs belongs in `inputs` too.
|
|
10
|
+
- **`shipkit release` scopes its dirt check exactly like `shipkit bump`.** `bump` examined the released unit plus every package.json and lockfile a version commit can touch; `release` examined the WHOLE worktree, refused, and then named `shipkit release` as the fix, which refuses identically. Several agents share one checkout here, so a neighbour editing an unrelated package was enough to wedge a release with no way forward that the message described. The reason to refuse at all is unchanged and still applies inside the scope: `npm publish` packs the WORKING TREE rather than the commit, so a half written file inside the unit ships under a tag that describes something else. Dirt in another unit reaches neither the tarball nor the tag, and the refusal now says what it examined.
|
|
11
|
+
|
|
3
12
|
## 0.35.4
|
|
4
13
|
|
|
5
14
|
- **The imports refusal did not follow tsconfig `extends`, so a correctly configured repo went red and was told to add a key it already had.** Inheriting `paths` from a base config is the more common way to carry them, and only `references` was handled. `extends` is now followed (relative and node-resolved, chains and cycles included), and a mapping counts only when its TARGET is on disk: `"src/*": ["./packages/core/src/*"]` maps the prefix and resolves nothing. A package prefix is no longer accepted on the package DIRECTORY existing either, since a stale installed copy missing the file is the same false green one level up. `baseUrl` is no longer proof at all: tsc follows one, but rollup, esbuild and vite do not resolve a bare specifier through it, and the field failure this guard exists for was a rollup one. A multi-segment root (`packages/core`) is named as itself rather than as `packages`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.6",
|
|
4
|
+
"funding": "https://github.com/sponsors/rvion",
|
|
4
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",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"type": "module",
|
package/src/cli/cmd_Init.ts
CHANGED
|
@@ -8,7 +8,7 @@ export class InitCommand extends Command {
|
|
|
8
8
|
|
|
9
9
|
static override usage = Command.Usage({
|
|
10
10
|
category: 'Setup',
|
|
11
|
-
description: 'wire THE CURRENT DIRECTORY as a shipkit repo, like git init: cwd only, never creates a project dir. scaffolds repo.config.ts + ci/check/contracts package.json scripts, prints how the floor resolved, and runs the bootstrap chain around it: git init, then first commit + PRIVATE github repo + push once the scaffold is in. Idempotent on an already-wired repo. To create or adopt ~/dev/<name> from anywhere: `shipkit start <name>`',
|
|
11
|
+
description: 'wire THE CURRENT DIRECTORY as a shipkit repo, like git init: cwd only, never creates a project dir. scaffolds repo.config.ts + ci/check/contracts package.json scripts + the create-only .vscode/ that starts the watchers and the hub on folder open, prints how the floor resolved, and runs the bootstrap chain around it: git init, then first commit + PRIVATE github repo + push once the scaffold is in. Idempotent on an already-wired repo. To create or adopt ~/dev/<name> from anywhere: `shipkit start <name>`',
|
|
12
12
|
examples: [
|
|
13
13
|
['wire the repo you are standing in', 'shipkit init'],
|
|
14
14
|
['local git and the first commit, never touch github', 'shipkit init --no-github'],
|
package/src/core/run-check.ts
CHANGED
|
@@ -77,8 +77,14 @@ export type CheckOpts = {
|
|
|
77
77
|
* check's name AND its path), because the id an operator types is whichever
|
|
78
78
|
* one they last saw on the wire. */
|
|
79
79
|
export function matchesOnly(only: string[] | undefined, ...names: string[]): boolean {
|
|
80
|
-
if (only
|
|
81
|
-
return only
|
|
80
|
+
if (!isNarrowed(only)) return true
|
|
81
|
+
return only!.some((want) => names.some((n) => n.toLowerCase() === want.toLowerCase()))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** did the operator narrow this run? The ONE reading of `--only` everywhere: an
|
|
85
|
+
* empty list is not a selection, it is the absence of one. */
|
|
86
|
+
export function isNarrowed(only: string[] | undefined): boolean {
|
|
87
|
+
return only != null && only.length > 0
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export type CheckRun = {
|
|
@@ -210,8 +216,11 @@ export async function collectChecks(root: string, o: CheckOpts = {}): Promise<Ch
|
|
|
210
216
|
// and that can sit red for months with nothing wrong, is not part of
|
|
211
217
|
// that answer at any interval. `shipkit check` still runs it, and
|
|
212
218
|
// naming it in `--only` still runs it, so opting out is never a way to
|
|
213
|
-
// make a check unreachable.
|
|
214
|
-
|
|
219
|
+
// make a check unreachable. "not narrowed" is an EMPTY list as much as
|
|
220
|
+
// an absent one: the CLI's --only is an Option.Array defaulting to [],
|
|
221
|
+
// so a `== null` test here reads every plain gate run as narrowed and
|
|
222
|
+
// opts nothing out (matchesOnly above uses the same predicate).
|
|
223
|
+
if (o.gateOnly === true && !d.gate && !isNarrowed(o.only)) continue
|
|
215
224
|
const blocking = d.blocking ?? c.checkDefaults?.blocking
|
|
216
225
|
if (blocking == null) undeclared.push(rel)
|
|
217
226
|
if (o.onlyBlocking === true && blocking !== true) continue
|
package/src/core/task-runner.ts
CHANGED
|
@@ -32,8 +32,14 @@ export type TaskDef = {
|
|
|
32
32
|
timeoutMs?: number
|
|
33
33
|
/** CONTENT population: the repo-relative paths this task reads. Declaring
|
|
34
34
|
* them is what makes the task keyable (ledger/git-key.ts). Undeclared is
|
|
35
|
-
* the DEFAULT and means never cached, so forgetting
|
|
36
|
-
* correctness.
|
|
35
|
+
* the DEFAULT and means never cached, so forgetting a whole task costs
|
|
36
|
+
* speed and never correctness. A list MISSING a path the command reads is
|
|
37
|
+
* the one that costs correctness: the key cannot move when that path
|
|
38
|
+
* changes, so the answer replays over a tree it no longer describes. Over
|
|
39
|
+
* declaring costs a re-run, under declaring is a false green, and `['.']`
|
|
40
|
+
* is the whole work tree and cannot under declare. A task carries no code
|
|
41
|
+
* closure, so the script it runs belongs in here too.
|
|
42
|
+
* Declaring both this and `ttl` is a registration failure. */
|
|
37
43
|
inputs?: string[]
|
|
38
44
|
/** WORLD population: this task reads the network, a keychain, $PATH or the
|
|
39
45
|
* clock, so nothing on disk describes its answer and a clean pass is
|
package/src/init-repo.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { reportToMotherhome } from './core/motherhome.ts'
|
|
|
15
15
|
import { ensureGitRepo, publishRepo, type Run } from './git-bootstrap.ts'
|
|
16
16
|
import { defineRepo, loadRepoConfig, resolveFloor } from './repo-config.ts'
|
|
17
17
|
import { gitignorePolicy } from './rules/SK004-gitignore-policy.ts'
|
|
18
|
+
import { editorErgonomics } from './scaffold/project-scaffold.ts'
|
|
19
|
+
import { defaultHubPort } from './serve/hub-port.ts'
|
|
18
20
|
|
|
19
21
|
// This lands as SOURCE in the repo being wired, so its comments answer to that
|
|
20
22
|
// repo's comment style, not to this file's. Dash punctuation is banned there
|
|
@@ -94,6 +96,7 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
94
96
|
// the repos that need ONE WAY IN most are the ones that already exist,
|
|
95
97
|
// so a re-run REPAIRS rather than only seeding a fresh checkout
|
|
96
98
|
await repairOneWayIn(root, cfg, p)
|
|
99
|
+
furnishEditor(root, cfg.name, cfg.serve?.port ?? defaultHubPort(cfg.name), p)
|
|
97
100
|
const mh = reportToMotherhome(root, cfg)
|
|
98
101
|
if (mh.note != null) p(mh.note)
|
|
99
102
|
if (wired && opts.noPublish !== true) await publishRepo(root, { github: opts.github, gh: opts.gh, print: p })
|
|
@@ -127,6 +130,7 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
127
130
|
const g = await gitignorePolicy.fix!({ root, config: defineRepo({ name }) })
|
|
128
131
|
p(`ok: .gitignore policy block ${g.changed ? 'written' : 'already current'}`)
|
|
129
132
|
await repairOneWayIn(root, defineRepo({ name }), p)
|
|
133
|
+
furnishEditor(root, name, defaultHubPort(name), p)
|
|
130
134
|
// what the gate actually runs, in ONE line: a thin floor must be seen at
|
|
131
135
|
// init time rather than discovered after a bad commit slipped through.
|
|
132
136
|
// The per-task resolution is diagnostics, and a `skip:` naming a script
|
|
@@ -157,6 +161,29 @@ export async function initRepo(root: string, opts: InitOpts = {}): Promise<numbe
|
|
|
157
161
|
return 0
|
|
158
162
|
}
|
|
159
163
|
|
|
164
|
+
/** the folder-open story, on the adopt path as well as the start one.
|
|
165
|
+
*
|
|
166
|
+
* `start` scaffolds the whole day-one shape, but only for a dir with no
|
|
167
|
+
* commit history, so every repo that already existed when it was wired got
|
|
168
|
+
* repo.config.ts and the scripts and no .vscode/ at all: opening the folder
|
|
169
|
+
* started nothing, and the hub the config advertises had no way in. These two
|
|
170
|
+
* files are the only half of that scaffold an adopted repo can be handed
|
|
171
|
+
* unasked, because they change no output of any command (project-scaffold.ts
|
|
172
|
+
* owns the line). Create-only, so a .vscode/ the repo brought with it wins. */
|
|
173
|
+
function furnishEditor(root: string, name: string, port: number, p: (line: string) => void): void {
|
|
174
|
+
// the least important thing init does, and it runs AFTER repo.config.ts is
|
|
175
|
+
// on disk: an unwritable .vscode (a read-only tree, a path that is a file)
|
|
176
|
+
// must not abort the run before the publish chain, and must not pass in
|
|
177
|
+
// silence either
|
|
178
|
+
try {
|
|
179
|
+
const wrote = editorErgonomics(root, { name, port })
|
|
180
|
+
if (wrote.length > 0) p(`ok: ${wrote.join(' + ')} written — opening the folder starts the watchers and the hub on ${port}`)
|
|
181
|
+
else p('ok: .vscode/ already yours, left untouched')
|
|
182
|
+
} catch (e) {
|
|
183
|
+
p(`WARN no .vscode/ here: ${e instanceof Error ? e.message : String(e)} — opening the folder starts nothing, everything else landed`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
160
187
|
/** ONE WAY IN, both halves, through the SAME fixers `shipkit check --fix` runs
|
|
161
188
|
* (build/shipkit.md). Nothing here is a second implementation: rule SK001
|
|
162
189
|
* routes the package.json scripts, rule SK013 writes the harness policy, and
|
|
@@ -11,6 +11,7 @@ import { join } from 'path'
|
|
|
11
11
|
import { runCi } from '../core/run-ci.ts'
|
|
12
12
|
import { extractChangelogSection } from './changelog.ts'
|
|
13
13
|
import { deriveRemote, dirtyPaths, git, remoteTagState, tagExistsLocally, type RemoteTagState } from './git-facts.ts'
|
|
14
|
+
import { releaseScope } from './release-scope.ts'
|
|
14
15
|
import { npmPublishArgv, unitBlockers } from './unit-gate.ts'
|
|
15
16
|
import { formatUnitError, resolveUnits, unitById, type ReleaseUnit } from './units.ts'
|
|
16
17
|
import type { RepoConfig } from '../repo-config.ts'
|
|
@@ -172,9 +173,23 @@ export async function releaseUnit(root: string, config: RepoConfig, unit: Releas
|
|
|
172
173
|
if (cur.code !== 0) blockers.push('not a git repository')
|
|
173
174
|
else if (cur.out !== branch) blockers.push(`releases ship from '${branch}', HEAD is on '${cur.out}'`)
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
// scoped exactly like `bump`, and for the same reason: `npm publish` packs
|
|
177
|
+
// the WORKING TREE rather than the commit, so dirt INSIDE what ships can
|
|
178
|
+
// put a half written file under a tag that describes something else. Dirt
|
|
179
|
+
// in another unit reaches neither this tarball nor this tag. Checking the
|
|
180
|
+
// whole tree here made `release` refuse what `bump` allows, and then name
|
|
181
|
+
// `shipkit release` as the fix, which refuses identically: several agents
|
|
182
|
+
// share this checkout, so a neighbour editing an unrelated package was
|
|
183
|
+
// enough to wedge a release with no way forward that the message named.
|
|
184
|
+
// the sibling units matter to the SCOPE: a version write rewrites the
|
|
185
|
+
// rvlib range in every dependent's package.json, so those paths are inside
|
|
186
|
+
// this release even though they are outside this unit
|
|
187
|
+
const scope = releaseScope(root, resolveUnits(root, config).units, [unit])
|
|
188
|
+
const dirty = dirtyPaths(root, config, scope.specs ?? undefined)
|
|
189
|
+
if (dirty !== '' && o.allowDirty !== true) {
|
|
190
|
+
blockers.push(`uncommitted changes in ${scope.describe} (--allow-dirty overrides):\n${dirty.split('\n').slice(0, 10).join('\n')}`)
|
|
191
|
+
if (scope.specs != null) blockers.push('dirt elsewhere in the worktree is deliberately ignored, it cannot reach this tarball or this tag')
|
|
192
|
+
}
|
|
178
193
|
if (o.skipUnitGate !== true) blockers.push(...unitBlockers(root, config, unit))
|
|
179
194
|
|
|
180
195
|
let alreadyPublished = false
|
|
@@ -99,15 +99,51 @@ function put(root: string, rel: string, body: string): boolean {
|
|
|
99
99
|
return true
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** the ERGONOMIC half of the day-one shape: the two files that decide what
|
|
103
|
+
* happens when the folder is opened in vscode, and nothing else. */
|
|
104
|
+
export const ERGONOMIC_FILES = ['.vscode/settings.json', '.vscode/tasks.json'] as const
|
|
105
|
+
|
|
106
|
+
export type ErgonomicsOpts = {
|
|
107
|
+
/** repo name, substituted into the templates */
|
|
108
|
+
name: string
|
|
109
|
+
/** the port the hub actually listens on, resolved by the caller from the
|
|
110
|
+
* repo config (`serve.port`, else the name-derived default), so the task
|
|
111
|
+
* advertises a url something answers on */
|
|
112
|
+
port: number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** the folder-open half of the scaffold, split out because it is the ONLY
|
|
116
|
+
* half an ADOPTED repo can be given unasked.
|
|
117
|
+
*
|
|
118
|
+
* Everything else the day-one shape writes says what the repo IS (a tsconfig,
|
|
119
|
+
* a src/index.ts) or what an agent may DO in it (the verify skill, the
|
|
120
|
+
* .claude/ policy files), and a repo with a history of its own already
|
|
121
|
+
* answered those. A tasks file and an editor settings file change no output
|
|
122
|
+
* of any command: they decide what vscode starts when the folder opens, and
|
|
123
|
+
* a repo where that starts nothing has no folder-open story at all.
|
|
124
|
+
*
|
|
125
|
+
* Create-only, like every other writer here: an adopted repo often carries a
|
|
126
|
+
* .vscode/ from whatever it was forked from, and that one wins byte for
|
|
127
|
+
* byte. Returns the paths it wrote, so a caller can say "already current"
|
|
128
|
+
* when it wrote none. */
|
|
129
|
+
export function editorErgonomics(root: string, opts: ErgonomicsOpts): string[] {
|
|
130
|
+
const vars = { NAME: opts.name, PORT: String(opts.port), DATE: '' }
|
|
131
|
+
const written: string[] = []
|
|
132
|
+
if (put(root, '.vscode/settings.json', template('vscode-settings.json', vars))) written.push('.vscode/settings.json')
|
|
133
|
+
if (put(root, '.vscode/tasks.json', template('vscode-tasks.json', vars))) written.push('.vscode/tasks.json')
|
|
134
|
+
return written
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
103
138
|
const p = opts.print ?? console.log
|
|
104
139
|
// the hub resolves its port from the CONFIG name, which init takes from
|
|
105
140
|
// package.json — a dir whose folder name differs would otherwise get a
|
|
106
141
|
// tasks.json advertising a port nothing listens on
|
|
107
142
|
const configName = packageName(root) ?? opts.name
|
|
143
|
+
const port = defaultHubPort(configName)
|
|
108
144
|
const vars = {
|
|
109
145
|
NAME: opts.name,
|
|
110
|
-
PORT: String(
|
|
146
|
+
PORT: String(port),
|
|
111
147
|
DATE: opts.today ?? new Date().toISOString().slice(0, 10),
|
|
112
148
|
}
|
|
113
149
|
const written: string[] = []
|
|
@@ -116,8 +152,8 @@ export function scaffoldProject(root: string, opts: ScaffoldOpts): string[] {
|
|
|
116
152
|
|
|
117
153
|
write('src/index.ts', 'export {}\n')
|
|
118
154
|
write('tsconfig.json', template('tsconfig.json', vars))
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
for (const rel of editorErgonomics(root, { name: opts.name, port })) written.push(rel)
|
|
156
|
+
for (const rel of ERGONOMIC_FILES) if (!written.includes(rel)) skipped.push(rel)
|
|
121
157
|
write('.claude/skills/verify/SKILL.md', template('verify-skill.md', vars))
|
|
122
158
|
// a permission grant and a git-history policy are decisions about someone
|
|
123
159
|
// else's repo, so they land only for a caller that declared a house, and
|