@wirecat/cli-base 0.53.0 → 0.55.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 +41 -0
- package/config/wirecat/biome.base.json +17 -0
- package/config/wirecat/manifest.json +10 -3
- package/config/wirecat/no-sqlite-driver.grit +15 -0
- package/config/wirecat/release-lock.sh +10 -1
- package/dist/dev/land.d.ts +24 -5
- package/dist/dev/land.js +60 -10
- package/dist/dev/main.js +34 -5
- package/dist/release/fold.d.ts +11 -0
- package/dist/release/fold.js +53 -0
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.js +1 -1
- package/docs/standards/README.md +4 -3
- package/docs/standards/architecture.md +1 -1
- package/docs/standards/docs.md +5 -1
- package/docs/standards/kit-surface.md +53 -0
- package/docs/standards/new-cli.md +72 -91
- package/docs/standards/releases.md +18 -9
- package/docs/standards/store.md +3 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,47 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
|
|
|
8
8
|
Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
|
|
9
9
|
[`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
|
|
10
10
|
|
|
11
|
+
## 0.55.0 — 25.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`WC-STORE-5` is checked** ([Store](docs/standards/store.md)): the shared `no-sqlite-driver` lint plugin, now in every repository's `config/wirecat/`, refuses a CLI's import of a SQLite driver or `drizzle-orm` outside tests and tooling scripts. cli-core turns it off. `WC-ARCH-21` now says it is in the handbook since 0.53.0, the first release that carried it.
|
|
16
|
+
|
|
17
|
+
- **`cli-dev changelog restore --land` puts the newest release's notes back to what shipped**
|
|
18
|
+
([releases standard](docs/standards/releases.md#wc-rel-15--fold-a-late-pending-section-into-the-unpublished-version-before-publishing)).
|
|
19
|
+
A pull request checked before a release and merged after it adds its entry to that release's section, though its
|
|
20
|
+
code ships only in the next one, and no pull request check can see it. The command compares the newest tagged
|
|
21
|
+
version's section with its text in the tag; when they differ, it restores the tag's text and moves each entry the
|
|
22
|
+
tag lacks to the unreleased section, through a `chore(release): restore …` pull request. A CLI runs it at the start
|
|
23
|
+
of `bin/release`, before the fold, so the entry lands in the version that ships it. Older sections are not
|
|
24
|
+
compared.
|
|
25
|
+
|
|
26
|
+
### Changed — may break callers
|
|
27
|
+
|
|
28
|
+
- **The changelog guard fails a pull request that adds a line to a dated section with no tag yet**
|
|
29
|
+
([a released section is frozen](docs/standards/docs.md#wc-doc-20--a-released-section-is-frozen-and-a-new-entry-goes-under-an-existing-heading)),
|
|
30
|
+
unless its title has the `release` or `changelog` scope. Between the release pull request and the tag, a rebase onto
|
|
31
|
+
main puts a branch's new entries under the dated heading with no conflict, and when the release already runs
|
|
32
|
+
without that branch the note ships under the wrong version. Put the entry under the unreleased heading:
|
|
33
|
+
`bin/release` folds it into the dated version if it still publishes that commit (`chore(release): fold …`). A
|
|
34
|
+
repository picks this up when it moves its `changelog-guard.yml` pin to this version.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **The kit surface page links the two cli-template jobs** (cli-base's `template.yml`, cli-core's `parity.yml`), and
|
|
39
|
+
the roadmap no longer lists "a new CLI from a template" as next: it is `npx @wirecat/cli-template new`.
|
|
40
|
+
|
|
41
|
+
## 0.54.0 — 25.10.2026
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **A Kit surface page in the standards** ([Kit surface](docs/standards/kit-surface.md)) lists the cli-base and cli-core exports that `@wirecat/cli-template` is built on. A change that makes a CLI change its code for one of them ships with a migration note here and with the template already updated, so `npx @wirecat/cli-template upgrade` brings it. Nothing changes in code.
|
|
46
|
+
- **The New CLI page starts with `npx @wirecat/cli-template new`** ([New CLI](docs/standards/new-cli.md)). It gives the command with every flag, the four steps that still need a person, and the same "Done when" checks. A CLI started by hand still passes the same checks.
|
|
47
|
+
|
|
48
|
+
### Changed — may break callers
|
|
49
|
+
|
|
50
|
+
- **The release lock is one per package, not one per machine** (`WC-REL-3`, [Releases](docs/standards/releases.md)). Two releases of the same package still never run at once on one machine, from any checkout or worktree; releases of different packages now run side by side. The lock is `leemour-release-<package>.lock`, named from `package.json`, so a `package.json` without a name refuses the release. A CLI picks this up by re-running community's `adopt-standards.py --only release-lock`; until then it keeps the old shared lock.
|
|
51
|
+
|
|
11
52
|
## 0.53.0 — 25.10.2026
|
|
12
53
|
|
|
13
54
|
### Added
|
|
@@ -120,6 +120,23 @@
|
|
|
120
120
|
"!**/fixtures/**"
|
|
121
121
|
]
|
|
122
122
|
},
|
|
123
|
+
{
|
|
124
|
+
"path": "./config/wirecat/no-sqlite-driver.grit",
|
|
125
|
+
"includes": [
|
|
126
|
+
"**",
|
|
127
|
+
"!**/*.test.*",
|
|
128
|
+
"!**/*.spec.*",
|
|
129
|
+
"!**/test/**",
|
|
130
|
+
"!**/tests/**",
|
|
131
|
+
"!**/__tests__/**",
|
|
132
|
+
"!**/fixtures/**",
|
|
133
|
+
"!**/testing/**",
|
|
134
|
+
"!**/scripts/**",
|
|
135
|
+
"!**/bench/**",
|
|
136
|
+
"!**/performance/**",
|
|
137
|
+
"!**/contracts/**"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
123
140
|
{
|
|
124
141
|
"path": "./config/wirecat/no-unbounded-spread.grit",
|
|
125
142
|
"includes": [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": "https://github.com/WireCatLabs/community",
|
|
3
3
|
"revision": "b22b7ede5931a4058fc763e0a7d6bfe0fbdb87c8",
|
|
4
4
|
"files": {
|
|
5
|
-
"biome.base.json": "
|
|
5
|
+
"biome.base.json": "63760337a2e68522ad75e0ba163635785a01ec34dbd3cf338a09a4145932cf2a",
|
|
6
6
|
"cspell.json": "8241122ed4d461976a838099b265ef8f2689a066608257b6fdc250d9b49b58aa",
|
|
7
7
|
"lefthook.yml": "2f65a2e3103006d43f6d6f0a07ab333c132286157763222a59a5a22676acd391",
|
|
8
8
|
"markdownlint.json": "5ed7d6980054c539f3f3c0e47a090f1a725b15be2c486a769a836bc6cd48ad2d",
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
"no-shell-true.grit": "d3943cfa1d7ebbf7be0fa22c2aaf2b7d7aa9ff462f2eb14a7798632764e3875c",
|
|
13
13
|
"no-unbounded-spread.grit": "f4178e5b74212fef58fec8916b6783d9f4f192ac618aa4ced041c4167bfbf81b",
|
|
14
14
|
"release-lock-info.mjs": "4abab1f01ceee7d4c709222e923c9a6fc634bcf7b8b64d745a9ac495993310e0",
|
|
15
|
-
"release-lock.sh": "
|
|
15
|
+
"release-lock.sh": "bef364092c4ab595f3b9b4130e23570c8306223228cdfa3e9384230f3b7f4a49",
|
|
16
16
|
"rumdl.toml": "048b929176f13234324f5825d1e811397f1491b9cb6684a1953e268e2c2ed863",
|
|
17
|
-
"check.mjs": "34b266411eaab7707030915f645308e86784b1574b21fa44ca974861a90c9ce1"
|
|
17
|
+
"check.mjs": "34b266411eaab7707030915f645308e86784b1574b21fa44ca974861a90c9ce1",
|
|
18
|
+
"no-sqlite-driver.grit": "19e1f510a88ddd9df44d6db43c1c9b603c3e5bbec03a3f65305b72eac7f09389"
|
|
18
19
|
},
|
|
19
20
|
"packageManager": "pnpm@11.20.0",
|
|
20
21
|
"node": ">=22",
|
|
@@ -26,5 +27,11 @@
|
|
|
26
27
|
"cspell": "10.3.6",
|
|
27
28
|
"lefthook": "2.1.16",
|
|
28
29
|
"markdownlint-cli2": "0.23.3"
|
|
30
|
+
},
|
|
31
|
+
"fileRevisions": {
|
|
32
|
+
"release-lock.sh": "49e630a99d758e3e113749bef2049b258c7262e3",
|
|
33
|
+
"release-lock-info.mjs": "49e630a99d758e3e113749bef2049b258c7262e3",
|
|
34
|
+
"biome.base.json": "06c612c609d870f0ba00010612e0413f65340408",
|
|
35
|
+
"no-sqlite-driver.grit": "06c612c609d870f0ba00010612e0413f65340408"
|
|
29
36
|
}
|
|
30
37
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
`import $_ from $source`,
|
|
5
|
+
`import $source`,
|
|
6
|
+
`export { $_ } from $source`,
|
|
7
|
+
`export * from $source`,
|
|
8
|
+
`export * as $_ from $source`,
|
|
9
|
+
`export type { $_ } from $source`,
|
|
10
|
+
`import($source)`,
|
|
11
|
+
`require($source)`
|
|
12
|
+
} where {
|
|
13
|
+
$source <: r"^[\"'`](?:node:sqlite|bun:sqlite|better-sqlite3|drizzle-orm)(?:/.*)?[\"'`]$",
|
|
14
|
+
register_diagnostic(span=$source, message="The shared store belongs to cli-core (WC-STORE-5): a CLI opens no SQLite driver and writes no SQL against it. Ask cli-core for a store method.", severity="error")
|
|
15
|
+
}
|
|
@@ -8,7 +8,16 @@ wirecat_release_lock() {
|
|
|
8
8
|
echo "release locking requires flock" >&2
|
|
9
9
|
return 1
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
# Keyed by package name, not folder: one package releases from several checkouts and worktrees.
|
|
12
|
+
_wirecat_release_key=$(node -e '
|
|
13
|
+
const { name } = JSON.parse(require("node:fs").readFileSync("package.json", "utf8"))
|
|
14
|
+
if (typeof name !== "string" || !name) throw Error("package.json has no name")
|
|
15
|
+
process.stdout.write(name.replace(/^@/, "").replace(/[^A-Za-z0-9._-]+/g, "-"))
|
|
16
|
+
' 2>/dev/null) && [ -n "$_wirecat_release_key" ] || {
|
|
17
|
+
echo "release locking needs a package name in ./package.json" >&2
|
|
18
|
+
return 1
|
|
19
|
+
}
|
|
20
|
+
_wirecat_release_lock="${XDG_RUNTIME_DIR:-/tmp}/leemour-release-$_wirecat_release_key.lock"
|
|
12
21
|
exec 9>>"$_wirecat_release_lock"
|
|
13
22
|
if flock -E 75 -w "$_wirecat_release_wait" 9; then
|
|
14
23
|
printf '%s pid %s since %s\n' "$(basename "$PWD")" "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$_wirecat_release_lock.who"
|
package/dist/dev/land.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type Change = {
|
|
|
13
13
|
line: string;
|
|
14
14
|
};
|
|
15
15
|
export declare const changelogChange: (text: string, version: string, unreleased: string) => Change | undefined;
|
|
16
|
+
/** Moves the entries that missed the newest tagged version to the unreleased section; see `restoreReleased`. */
|
|
17
|
+
export declare const restoreChange: (text: string, tagText: string, version: string, unreleased: string) => Change | undefined;
|
|
16
18
|
export interface LandOptions {
|
|
17
19
|
file: string;
|
|
18
20
|
version: string;
|
|
@@ -20,10 +22,27 @@ export interface LandOptions {
|
|
|
20
22
|
shell: Shell;
|
|
21
23
|
log: (text: string) => void;
|
|
22
24
|
}
|
|
25
|
+
/** Lands the changelog fold on `main` through a pull request; see `landChange`. */
|
|
26
|
+
export declare const landFold: ({ file, version, unreleased, shell, log }: LandOptions) => string | undefined;
|
|
23
27
|
/**
|
|
24
|
-
* Lands
|
|
25
|
-
* `origin/main`; a pull request that main has since moved under (another session
|
|
26
|
-
* merged a change to CHANGELOG.md) is closed and the
|
|
27
|
-
* `undefined` when there was nothing to
|
|
28
|
+
* Lands a changelog change on `main` through a pull request. Each attempt computes it on a fresh
|
|
29
|
+
* `origin/main`; a pull request that main has since moved under (another session landed first, or
|
|
30
|
+
* merged a change to CHANGELOG.md) is closed and the change redone once. Returns the line to print, or
|
|
31
|
+
* `undefined` when there was nothing to change. Throws when it cannot land.
|
|
28
32
|
*/
|
|
29
|
-
export declare const
|
|
33
|
+
export declare const landChange: ({ file, branch, change: changeOf, done, shell, log, }: {
|
|
34
|
+
file: string;
|
|
35
|
+
branch: string;
|
|
36
|
+
change: (text: string) => Change | undefined;
|
|
37
|
+
done: string;
|
|
38
|
+
shell: Shell;
|
|
39
|
+
log: (text: string) => void;
|
|
40
|
+
}) => string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The newest `v<x.y.z>` tag in this commit's history and the changelog at it. Tags are fetched first: a clone made before the
|
|
43
|
+
* last release does not have its tag, and comparing against an older one would hide the newest drift.
|
|
44
|
+
*/
|
|
45
|
+
export declare const newestTag: (shell: Shell, path: string) => {
|
|
46
|
+
version: string;
|
|
47
|
+
text: string;
|
|
48
|
+
};
|
package/dist/dev/land.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import spawn from "cross-spawn";
|
|
3
|
-
import { foldUnreleased, mergeRepeatedHeadings } from "../release/index.js";
|
|
3
|
+
import { foldUnreleased, mergeRepeatedHeadings, restoreReleased } from "../release/index.js";
|
|
4
4
|
export const processShell = (cwd) => ({
|
|
5
5
|
run: (command, args) => {
|
|
6
6
|
const result = spawn.sync(command, [...args], { cwd, encoding: "utf8" });
|
|
@@ -28,14 +28,41 @@ export const changelogChange = (text, version, unreleased) => {
|
|
|
28
28
|
line: `merged repeated headings in ${version}`,
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
|
+
/** Moves the entries that missed the newest tagged version to the unreleased section; see `restoreReleased`. */
|
|
32
|
+
export const restoreChange = (text, tagText, version, unreleased) => {
|
|
33
|
+
const restored = restoreReleased(text, tagText, version, unreleased);
|
|
34
|
+
if (!restored)
|
|
35
|
+
return undefined;
|
|
36
|
+
const moved = restored.moved.map((entry) => `\n${entry}`).join("");
|
|
37
|
+
return {
|
|
38
|
+
text: restored.text,
|
|
39
|
+
title: `chore(release): restore ${version}'s notes to its tag`,
|
|
40
|
+
body: `bin/release: CHANGELOG.md's ${version} section differed from the v${version} tag. It now matches the tag` +
|
|
41
|
+
(moved
|
|
42
|
+
? `, and ${restored.moved.length} entr${restored.moved.length === 1 ? "y" : "ies"} that missed ${version} ` +
|
|
43
|
+
`moved to ${unreleased}:${moved}`
|
|
44
|
+
: ".") +
|
|
45
|
+
"\nOnly CHANGELOG.md changes.",
|
|
46
|
+
line: `restored ${version}'s notes to its tag${moved ? `; moved to ${unreleased}:${moved}` : ""}`,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
31
49
|
const ATTEMPTS = 2;
|
|
50
|
+
/** Lands the changelog fold on `main` through a pull request; see `landChange`. */
|
|
51
|
+
export const landFold = ({ file, version, unreleased, shell, log }) => landChange({
|
|
52
|
+
file,
|
|
53
|
+
branch: `release/fold-${version}`,
|
|
54
|
+
change: (text) => changelogChange(text, version, unreleased),
|
|
55
|
+
done: `main already has ${version}'s changelog folded`,
|
|
56
|
+
shell,
|
|
57
|
+
log,
|
|
58
|
+
});
|
|
32
59
|
/**
|
|
33
|
-
* Lands
|
|
34
|
-
* `origin/main`; a pull request that main has since moved under (another session
|
|
35
|
-
* merged a change to CHANGELOG.md) is closed and the
|
|
36
|
-
* `undefined` when there was nothing to
|
|
60
|
+
* Lands a changelog change on `main` through a pull request. Each attempt computes it on a fresh
|
|
61
|
+
* `origin/main`; a pull request that main has since moved under (another session landed first, or
|
|
62
|
+
* merged a change to CHANGELOG.md) is closed and the change redone once. Returns the line to print, or
|
|
63
|
+
* `undefined` when there was nothing to change. Throws when it cannot land.
|
|
37
64
|
*/
|
|
38
|
-
export const
|
|
65
|
+
export const landChange = ({ file, branch, change: changeOf, done, shell, log, }) => {
|
|
39
66
|
const must = (command, ...args) => {
|
|
40
67
|
const result = shell.run(command, args);
|
|
41
68
|
if (!result.ok)
|
|
@@ -51,7 +78,6 @@ export const landFold = ({ file, version, unreleased, shell, log }) => {
|
|
|
51
78
|
}
|
|
52
79
|
return "UNKNOWN";
|
|
53
80
|
};
|
|
54
|
-
const branch = `release/fold-${version}`;
|
|
55
81
|
const abandon = (pr) => {
|
|
56
82
|
log(`${pr} no longer merges into main — closing it and folding again on the new main`);
|
|
57
83
|
must("gh", "pr", "close", pr, "--delete-branch");
|
|
@@ -60,10 +86,10 @@ export const landFold = ({ file, version, unreleased, shell, log }) => {
|
|
|
60
86
|
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
61
87
|
must("git", "fetch", "-q", "origin");
|
|
62
88
|
must("git", "merge", "-q", "--ff-only", "origin/main");
|
|
63
|
-
const change =
|
|
89
|
+
const change = changeOf(readFileSync(file, "utf8"));
|
|
64
90
|
if (!change) {
|
|
65
91
|
if (attempt > 1)
|
|
66
|
-
log(
|
|
92
|
+
log(`${done} — nothing to land`);
|
|
67
93
|
return undefined;
|
|
68
94
|
}
|
|
69
95
|
writeFileSync(file, change.text);
|
|
@@ -95,5 +121,29 @@ export const landFold = ({ file, version, unreleased, shell, log }) => {
|
|
|
95
121
|
must("git", "merge", "-q", "--ff-only", "origin/main");
|
|
96
122
|
return `${change.line} (${pr})`;
|
|
97
123
|
}
|
|
98
|
-
throw new Error(`main moved under
|
|
124
|
+
throw new Error(`main moved under ${branch} twice — run bin/release again`);
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The newest `v<x.y.z>` tag in this commit's history and the changelog at it. Tags are fetched first: a clone made before the
|
|
128
|
+
* last release does not have its tag, and comparing against an older one would hide the newest drift.
|
|
129
|
+
*/
|
|
130
|
+
export const newestTag = (shell, path) => {
|
|
131
|
+
const fetched = shell.run("git", ["fetch", "-q", "--tags", "origin"]);
|
|
132
|
+
if (!fetched.ok)
|
|
133
|
+
throw new Error(`git fetch --tags failed: ${fetched.out}`);
|
|
134
|
+
const [version] = shell
|
|
135
|
+
.run("git", ["tag", "--list", "v*", "--merged", "HEAD"])
|
|
136
|
+
.out.split("\n")
|
|
137
|
+
.map((tag) => /^v(\d+\.\d+\.\d+)$/.exec(tag.trim())?.[1])
|
|
138
|
+
.filter((found) => found !== undefined)
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
const [x, y] = [a, b].map((found) => found.split(".").map(Number));
|
|
141
|
+
return (y?.[0] ?? 0) - (x?.[0] ?? 0) || (y?.[1] ?? 0) - (x?.[1] ?? 0) || (y?.[2] ?? 0) - (x?.[2] ?? 0);
|
|
142
|
+
});
|
|
143
|
+
if (!version)
|
|
144
|
+
throw new Error("no v<x.y.z> tag to compare the changelog with");
|
|
145
|
+
const shown = shell.run("git", ["show", `v${version}:${path}`]);
|
|
146
|
+
if (!shown.ok)
|
|
147
|
+
throw new Error(`git show v${version}:${path} failed: ${shown.out}`);
|
|
148
|
+
return { version, text: shown.out };
|
|
99
149
|
};
|
package/dist/dev/main.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { isAbsolute, join } from "node:path";
|
|
7
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
import spawn from "cross-spawn";
|
|
@@ -18,7 +18,7 @@ import { issuesOf, RegistryError } from "../standards/registry.js";
|
|
|
18
18
|
import { VOCABULARY } from "../standards/vocabulary.js";
|
|
19
19
|
import { auditClis, scorecardTable, snakeKeys } from "./audit.js";
|
|
20
20
|
import { readCapabilities } from "./capabilities.js";
|
|
21
|
-
import { changelogChange, landFold, processShell } from "./land.js";
|
|
21
|
+
import { changelogChange, landChange, landFold, newestTag, processShell, restoreChange } from "./land.js";
|
|
22
22
|
import { baselineOf, judge, lintCommands } from "./lint.js";
|
|
23
23
|
import { nextVersion } from "./next-version.js";
|
|
24
24
|
import { slowTests } from "./slow-tests.js";
|
|
@@ -36,6 +36,11 @@ const USAGE = `usage: cli-dev <command> [options]
|
|
|
36
36
|
it changed the file, nothing otherwise. --land lands it on main
|
|
37
37
|
through a pull request, folding again on the new main once if
|
|
38
38
|
main moved under it. Run it only while that version is not on npm
|
|
39
|
+
changelog restore [--unreleased <title>] [--file <path>] [--land]
|
|
40
|
+
puts the newest tagged version's section back to its text in
|
|
41
|
+
the tag; an entry the tag lacks missed that release and moves
|
|
42
|
+
to the unreleased section. Prints a line when it changed the
|
|
43
|
+
file. --land lands it on main through a pull request
|
|
39
44
|
docs-check [--rules <module>] [--ids <A,B>] [--pages]
|
|
40
45
|
links, anchors and the changelog's shape; --pages also
|
|
41
46
|
docs/ against docs/meta.json, the docs portal's structure
|
|
@@ -100,8 +105,8 @@ const docsCheck = async (args, io) => {
|
|
|
100
105
|
};
|
|
101
106
|
const changelog = (args, io) => {
|
|
102
107
|
const [action, ...rest] = args;
|
|
103
|
-
if (action !== "fold") {
|
|
104
|
-
io.err(`cli-dev changelog: no action "${action ?? ""}" — the
|
|
108
|
+
if (action !== "fold" && action !== "restore") {
|
|
109
|
+
io.err(`cli-dev changelog: no action "${action ?? ""}" — the ones there are: fold, restore`);
|
|
105
110
|
return 2;
|
|
106
111
|
}
|
|
107
112
|
const { values } = parseArgs({
|
|
@@ -114,8 +119,32 @@ const changelog = (args, io) => {
|
|
|
114
119
|
},
|
|
115
120
|
});
|
|
116
121
|
const file = isAbsolute(values.file) ? values.file : join(io.cwd, values.file);
|
|
117
|
-
const version = values.version ?? packageVersion(io.cwd);
|
|
118
122
|
try {
|
|
123
|
+
if (action === "restore") {
|
|
124
|
+
const shell = processShell(io.cwd);
|
|
125
|
+
const tag = newestTag(shell, relative(io.cwd, file));
|
|
126
|
+
const change = (text) => restoreChange(text, tag.text, tag.version, values.unreleased);
|
|
127
|
+
if (values.land) {
|
|
128
|
+
const landed = landChange({
|
|
129
|
+
file,
|
|
130
|
+
branch: `release/restore-${tag.version}`,
|
|
131
|
+
change,
|
|
132
|
+
done: `main already has ${tag.version}'s notes as tagged`,
|
|
133
|
+
shell,
|
|
134
|
+
log: io.err,
|
|
135
|
+
});
|
|
136
|
+
if (landed)
|
|
137
|
+
io.out(landed);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
const restored = change(readFileSync(file, "utf8"));
|
|
141
|
+
if (!restored)
|
|
142
|
+
return 0;
|
|
143
|
+
writeFileSync(file, restored.text);
|
|
144
|
+
io.out(restored.line);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
const version = values.version ?? packageVersion(io.cwd);
|
|
119
148
|
if (values.land) {
|
|
120
149
|
const landed = landFold({
|
|
121
150
|
file,
|
package/dist/release/fold.d.ts
CHANGED
|
@@ -13,3 +13,14 @@ export declare const foldUnreleased: (text: string, version: string, unreleased?
|
|
|
13
13
|
* when no heading repeats there. The caller makes sure the version is not on npm.
|
|
14
14
|
*/
|
|
15
15
|
export declare const mergeRepeatedHeadings: (text: string, version: string) => string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Puts the section of `version`, the newest tagged one, back to its text in that tag. A pull request
|
|
18
|
+
* checked before the version was dated or tagged and merged after it adds its entry there with no
|
|
19
|
+
* guard to see it, though that code ships only in the next version: each such entry moves to the unreleased
|
|
20
|
+
* section, after its entries under the same `###` heading. `undefined` when the section matches the tag the
|
|
21
|
+
* way the changelog guard compares them.
|
|
22
|
+
*/
|
|
23
|
+
export declare const restoreReleased: (text: string, tagText: string, version: string, unreleased?: string) => {
|
|
24
|
+
text: string;
|
|
25
|
+
moved: string[];
|
|
26
|
+
} | undefined;
|
package/dist/release/fold.js
CHANGED
|
@@ -66,3 +66,56 @@ export const mergeRepeatedHeadings = (text, version) => {
|
|
|
66
66
|
return undefined;
|
|
67
67
|
return [preamble, render(section.title, [section]), ...rest].join("\n");
|
|
68
68
|
};
|
|
69
|
+
const body = (block) => block
|
|
70
|
+
.split("\n")
|
|
71
|
+
.slice(1)
|
|
72
|
+
.map((line) => line.trimEnd())
|
|
73
|
+
.join("\n")
|
|
74
|
+
.trim();
|
|
75
|
+
const entries = (section) => [...section.headings].flatMap(([heading, lines]) => {
|
|
76
|
+
const found = [];
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
if (!line.trim())
|
|
79
|
+
continue;
|
|
80
|
+
const last = found.at(-1);
|
|
81
|
+
if (line.startsWith("- ") || !last)
|
|
82
|
+
found.push({ heading, text: line });
|
|
83
|
+
else
|
|
84
|
+
last.text += `\n${line}`;
|
|
85
|
+
}
|
|
86
|
+
return found;
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Puts the section of `version`, the newest tagged one, back to its text in that tag. A pull request
|
|
90
|
+
* checked before the version was dated or tagged and merged after it adds its entry there with no
|
|
91
|
+
* guard to see it, though that code ships only in the next version: each such entry moves to the unreleased
|
|
92
|
+
* section, after its entries under the same `###` heading. `undefined` when the section matches the tag the
|
|
93
|
+
* way the changelog guard compares them.
|
|
94
|
+
*/
|
|
95
|
+
export const restoreReleased = (text, tagText, version, unreleased = "Unreleased") => {
|
|
96
|
+
const dated = (block) => block.startsWith(`## ${version} — `);
|
|
97
|
+
const tagged = tagText.split(/\n(?=## )/).find(dated);
|
|
98
|
+
if (!tagged)
|
|
99
|
+
throw new Error(`the v${version} tag's changelog has no "## ${version} — " section`);
|
|
100
|
+
const blocks = text.split(/\n(?=## )/);
|
|
101
|
+
const index = blocks.findIndex(dated);
|
|
102
|
+
if (index < 0)
|
|
103
|
+
throw new Error(`no "## ${version} — " section to compare with the v${version} tag`);
|
|
104
|
+
const current = blocks[index] ?? "";
|
|
105
|
+
if (current.split("\n")[0]?.trimEnd() === tagged.split("\n")[0]?.trimEnd() && body(current) === body(tagged))
|
|
106
|
+
return undefined;
|
|
107
|
+
const shipped = new Set(entries(parseSection(tagged)).map((entry) => entry.text));
|
|
108
|
+
const missed = entries(parseSection(current)).filter((entry) => !shipped.has(entry.text));
|
|
109
|
+
blocks[index] = tagged.replace(/\n+$/, current.endsWith("\n") ? "\n" : "");
|
|
110
|
+
if (missed.length > 0) {
|
|
111
|
+
const headings = new Map();
|
|
112
|
+
for (const { heading, text: entry } of missed)
|
|
113
|
+
headings.set(heading, [...(headings.get(heading) ?? []), entry]);
|
|
114
|
+
const moved = { title: "", headings, repeated: false };
|
|
115
|
+
const top = blocks[1];
|
|
116
|
+
const open = top === `## ${unreleased}` || top?.startsWith(`## ${unreleased}\n`);
|
|
117
|
+
const section = render(`## ${unreleased}`, open && top ? [parseSection(top), moved] : [moved]);
|
|
118
|
+
blocks.splice(1, open ? 1 : 0, section);
|
|
119
|
+
}
|
|
120
|
+
return { text: blocks.join("\n"), moved: missed.map((entry) => entry.text.split("\n")[0] ?? "") };
|
|
121
|
+
};
|
package/dist/release/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { foldUnreleased, mergeRepeatedHeadings } from "./fold.js";
|
|
1
|
+
export { foldUnreleased, mergeRepeatedHeadings, restoreReleased } from "./fold.js";
|
|
2
2
|
export { type ChangelogRules, changelogProblems, type DocsRules, docsProblems, idPattern, JOURNAL_IDS, markdownFiles, slug, } from "./markdown.js";
|
|
3
3
|
export { REQUIRED_PAGES, type StructureRules, structureProblems } from "./structure.js";
|
|
4
4
|
export type Check = {
|
package/dist/release/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
export { foldUnreleased, mergeRepeatedHeadings } from "./fold.js";
|
|
9
|
+
export { foldUnreleased, mergeRepeatedHeadings, restoreReleased } from "./fold.js";
|
|
10
10
|
export { changelogProblems, docsProblems, idPattern, JOURNAL_IDS, markdownFiles, slug, } from "./markdown.js";
|
|
11
11
|
export { REQUIRED_PAGES, structureProblems } from "./structure.js";
|
|
12
12
|
/**
|
package/docs/standards/README.md
CHANGED
|
@@ -9,8 +9,8 @@ It ships in the `@wirecat/cli-base` package. A CLI that depends on cli-base has
|
|
|
9
9
|
|
|
10
10
|
## How to use this for a new CLI
|
|
11
11
|
|
|
12
|
-
Start with the [New CLI](new-cli.md)
|
|
13
|
-
|
|
12
|
+
Start with the [New CLI](new-cli.md) page: `npx @wirecat/cli-template new` makes the repository, and the page lists
|
|
13
|
+
the four steps that need a person. A new CLI is ready for its first release when every check under "Done when" passes.
|
|
14
14
|
|
|
15
15
|
## Which rule wins
|
|
16
16
|
|
|
@@ -44,7 +44,8 @@ rules it keeps. A new CLI is ready for its first release when every check under
|
|
|
44
44
|
| [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
|
|
45
45
|
| [Time](time.md) | `TIME` | 6 | UTC in the store and in answers, the zone at input and display edges, date-only filters, tests in another zone |
|
|
46
46
|
| [Style](style.md) | `STYLE` | 10 | Comments, TypeScript habits, commit messages |
|
|
47
|
-
| [New CLI](new-cli.md) | — | — |
|
|
47
|
+
| [New CLI](new-cli.md) | — | — | Starting a new CLI from the template: the command, the four manual steps, the checks |
|
|
48
|
+
| [Kit surface](kit-surface.md) | — | — | The cli-base and cli-core exports the template is built on, and how a change to one reaches you |
|
|
48
49
|
|
|
49
50
|
Each count leaves out the ids that moved or merged (see "How ids work").
|
|
50
51
|
|
|
@@ -219,7 +219,7 @@ out of search and listed under `skipped`, with the hint to run that CLI once.
|
|
|
219
219
|
`skipped` instead of reading their rows as messages.
|
|
220
220
|
- **Exceptions:** the built-in providers of cli-core need no entry. Two entries that disagree on a kind are an error
|
|
221
221
|
naming both.
|
|
222
|
-
- **Applies to:** cli-core and every CLI with a provider of its own. **Since:** cli-base 0.53.
|
|
222
|
+
- **Applies to:** cli-core and every CLI with a provider of its own. **Since:** cli-base 0.53.0. **Rulings:** none
|
|
223
223
|
(Wave 3 requirements 3.2).
|
|
224
224
|
|
|
225
225
|
## Sources
|
package/docs/standards/docs.md
CHANGED
|
@@ -198,7 +198,11 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
198
198
|
- **Enforced by:** `changelog-guard.yml`.
|
|
199
199
|
- **Example:** the guard fails a change to a section whose `v<version>` tag exists, unless the change restores the
|
|
200
200
|
exact text in that tag. It also fails a new repeated `###` heading in any section that has no tag yet.
|
|
201
|
-
|
|
201
|
+
A pull request whose title has no `release` or `changelog` scope also fails when it adds a line to a dated section
|
|
202
|
+
the base already had: a rebase onto a just-dated release moves its new entries there with no conflict.
|
|
203
|
+
- **Exceptions:** a dated section with no tag is still open to fixes from a title with the `release` or `changelog`
|
|
204
|
+
scope (`chore(release): fold …`, `docs(changelog): …`); a release is tagged only after npm has it.
|
|
205
|
+
Sections that already differed from their tag on 25.10.2026 stay as they are, and nothing reports them.
|
|
202
206
|
- **Applies to:** every repository that calls the changelog guard. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
203
207
|
|
|
204
208
|
## Sources
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Kit surface
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
These are the parts of cli-core and cli-base that the template is built on. A CLI started with
|
|
6
|
+
`npx @wirecat/cli-template new` imports them from its first commit. When one of them changes in a way your code must
|
|
7
|
+
follow, the changelog gives the steps, and the template is already updated, so `npx @wirecat/cli-template upgrade`
|
|
8
|
+
brings the change into your CLI.
|
|
9
|
+
|
|
10
|
+
This page is a reference, not a rule page: it assigns no rule ids. How to start a CLI from the template is on the
|
|
11
|
+
[New CLI](new-cli.md) page. How a release is made is on the [Releases](releases.md) page.
|
|
12
|
+
|
|
13
|
+
## What the template imports
|
|
14
|
+
|
|
15
|
+
The list is what the template's `src/` imports today, type imports included.
|
|
16
|
+
|
|
17
|
+
| Package and path | Exports |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `@wirecat/cli-base` | `catchCrashes`, `CliError`, `configFilePath`, `mcpErrorResult`, `resolvePaths` |
|
|
20
|
+
| `@wirecat/cli-base/commands` | `annotate`, `CommandInfo`, `describeProgram` |
|
|
21
|
+
| `@wirecat/cli-core/cli` | `AppIdentity`, `changesAuthorityAt`, `commandsCommand`, `configCommand`, `Configuration`, `createProgram`, `environmentOf`, `GlobalFlags`, `itemOf`, `LOCAL_READ`, `outputFor`, `ProgramDefinition`, `ProgramOptions`, `readOnlyLockOf`, `REMOTE_WRITE`, `renderAnswer`, `rootOf`, `run`, `RunOptions`, `settingsFor`, `skillCommand`, `upgradeCommand` |
|
|
22
|
+
| `@wirecat/cli-core/mcp` | `McpCommand`, `registerCommands`, `runCommandForAgent`, `toolsFromCommands` |
|
|
23
|
+
| `@wirecat/cli-core/sends` | `Level` |
|
|
24
|
+
| `@wirecat/cli-core/update` | `appUpdate` |
|
|
25
|
+
| `@wirecat/cli-core/testing` | `ARGV_LOG`, `argvLogSetup` |
|
|
26
|
+
|
|
27
|
+
## When one of them changes
|
|
28
|
+
|
|
29
|
+
A change that makes a CLI change its code for one of these exports ships with two things:
|
|
30
|
+
|
|
31
|
+
1. **A migration note in the changelog**, under "Changed — may break callers": what to change in your code, step by
|
|
32
|
+
step. See [Documents](docs.md) for the changelog's shape.
|
|
33
|
+
2. **A green cli-template job before the pull request merges.** cli-base
|
|
34
|
+
([`template.yml`](https://github.com/WireCatLabs/cli-base/blob/main/.github/workflows/template.yml)) and cli-core
|
|
35
|
+
(the `template` job in [`parity.yml`](https://github.com/WireCatLabs/cli-core/blob/main/.github/workflows/parity.yml))
|
|
36
|
+
build the template against each pull request, so the template is updated in the same change, and `upgrade` has
|
|
37
|
+
the new version to bring.
|
|
38
|
+
|
|
39
|
+
## How your CLI stays current
|
|
40
|
+
|
|
41
|
+
- **Exact versions.** The template depends on `@wirecat/cli-base` and `@wirecat/cli-core` at exact versions, the
|
|
42
|
+
normal setup for every WireCat CLI. A CLI made from it starts with the same pins.
|
|
43
|
+
- **Dependabot.** The template's `dependabot.yml` checks `@wirecat/*` every day and opens one pull request for the
|
|
44
|
+
shared packages within a day of a release. Its CI shows whether anything else needs to change.
|
|
45
|
+
- **`upgrade`.** In your CLI, with a clean git working tree:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx @wirecat/cli-template upgrade
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For each template file that changed since your version, it asks: apply (merge the change into your file, keeping
|
|
52
|
+
your edits), replace, skip or diff. Without a terminal, `--apply`, `--replace` or `--skip` answers for every file,
|
|
53
|
+
and `--dry-run` lists what each file would get. Then re-run community's `adopt-standards.py` for `config/wirecat/`.
|
|
@@ -1,100 +1,81 @@
|
|
|
1
1
|
# New CLI
|
|
2
2
|
|
|
3
|
-
<!-- cspell:ignore rumdl whatsapp -->
|
|
4
|
-
|
|
5
3
|
## Scope
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
How to start a new WireCat CLI: one command that copies the template, the four steps that need a person, and the
|
|
6
|
+
checks that prove the result. A new CLI is done when every check in "Done when" passes.
|
|
9
7
|
|
|
10
8
|
This page is a checklist, not a rule page: it assigns no rule ids. The rules are on the area pages it links. How to
|
|
11
|
-
read a rule and which rule wins are in the [index](README.md).
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
store at a temporary directory, and coverage thresholds. Proved by: `pnpm test`. See [Testing](testing.md) and
|
|
82
|
-
[Store](store.md).
|
|
83
|
-
4. **`contracts/plan.json`** and its reviewed snapshots for `cli-contract` (cli-testing): help, discovery JSON, MCP
|
|
84
|
-
tools and the no-leak check. Proved by: `pnpm contracts:check`. See [Testing](testing.md).
|
|
85
|
-
5. **The test matrix**: `docs/dev/test-matrix.md`, built by `cli-dev test-matrix`. Proved by:
|
|
86
|
-
`cli-dev test-matrix --check`.
|
|
87
|
-
6. **The user pages**: `README.md`, `docs/meta.json` and the shared page set, with `docs/commands.md` generated from
|
|
88
|
-
the program. Proved by: `cli-dev docs-check --pages` and the CLI's `generate:check`. See [Documents](docs.md).
|
|
89
|
-
7. **`CHANGELOG.md`** with `## Unreleased` at the top and the shared headings, and
|
|
90
|
-
**`.github/workflows/changelog-guard.yml`** calling cli-base's `changelog-guard.yml` at a pinned commit. Proved by:
|
|
91
|
-
`cli-dev docs-check` and the guard on the first pull request. See [Documents](docs.md).
|
|
92
|
-
8. **`SECURITY.md`**: how to report a vulnerability, linking cli-testing's security method. See
|
|
93
|
-
[Security](security.md).
|
|
94
|
-
9. **`bin/release`**, **`release.yml`** and a release skill, copied from the nearest CLI. `bin/release` takes the
|
|
95
|
-
release lock (WC-REL-3) and npm's trusted publisher settings name `release.yml`. A messenger CLI also checks a signed
|
|
96
|
-
release report (WC-REL-10). See [Releases](releases.md).
|
|
97
|
-
10. **`src/version.ts`** kept in step with `package.json`. Proved by: `pnpm version:check`.
|
|
9
|
+
read a rule and which rule wins are in the [index](README.md). The parts of cli-base and cli-core the template is
|
|
10
|
+
built on are on the [Kit surface](kit-surface.md) page.
|
|
11
|
+
|
|
12
|
+
## Start with the template
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npx @wirecat/cli-template new cal-cli --command cal --package @wirecat/cal-cli --owner WireCatLabs \
|
|
16
|
+
--category calendar --content-lang en,ru --sign-in oauth --api official-client --filters time,person,text \
|
|
17
|
+
--never-store tokens,secret-links --untrusted names,links
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
In a terminal, `new` asks for every answer you leave out. Without a terminal, a missing answer is an error that lists
|
|
21
|
+
the choices. Nothing is guessed.
|
|
22
|
+
|
|
23
|
+
| Flag | What it sets | Choices |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `<name>-cli` | the repository folder | any name |
|
|
26
|
+
| `--command` | the command word people type | one word |
|
|
27
|
+
| `--package` | the npm package name | a scoped name, such as `@wirecat/cal-cli` |
|
|
28
|
+
| `--owner` | the GitHub owner of the repository | an owner |
|
|
29
|
+
| `--category` | what the CLI reads | `messenger`, `mail`, `notes`, `files`, `calendar`, `meetings` |
|
|
30
|
+
| `--content-lang` | the languages of the content | a list, such as `en,ru` |
|
|
31
|
+
| `--sign-in` | how the CLI signs in | `oauth`, `token`, `session`, `none` |
|
|
32
|
+
| `--api` | what the service offers | `official-client`, `official-docs`, `unofficial` |
|
|
33
|
+
| `--filters` | how people narrow a list | a list of `time`, `person`, `container`, `text`, `status` |
|
|
34
|
+
| `--never-store` | what the store must never keep | a list of `tokens`, `codes`, `secret-links`, `content` |
|
|
35
|
+
| `--untrusted` | what text comes from other people | a list of `messages`, `files`, `links`, `names`, or `none` |
|
|
36
|
+
|
|
37
|
+
Every choice question also takes `other:<your answer>`.
|
|
38
|
+
|
|
39
|
+
`new` copies the template of its own package version into `<name>-cli/`, renames the command word everywhere, runs
|
|
40
|
+
`git init`, records the template version in `template.json`, writes `docs/dev/DESIGN.md` from your answers and names
|
|
41
|
+
a reference CLI for your category in `AGENTS.md`. It does not install packages, push, create the repository or
|
|
42
|
+
publish. Then:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
cd cal-cli && pnpm install && pnpm ci:quick
|
|
46
|
+
git add -A && git commit -m "chore: start from @wirecat/cli-template"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Give the agent `docs/dev/DESIGN.md` next. Every example passes a profile explicitly, such as `cal personal doctor`.
|
|
50
|
+
The reference CLI is the one to compare with when a step is unclear.
|
|
51
|
+
|
|
52
|
+
The copy already holds every file this handbook asks for: the required root commands (`commands`, `doctor`,
|
|
53
|
+
`skill show`, `skill install`, `mcp`, `upgrade`) and `config` from cli-core, the MCP server with a tool for every
|
|
54
|
+
command, writes included, the community presets in `config/wirecat/`, the TypeScript, Biome and vitest setup,
|
|
55
|
+
`contracts/`, `lint/`, the test matrix, the user pages, `CHANGELOG.md`, `SECURITY.md`, `bin/release`, and the
|
|
56
|
+
`pr.yml`, `ci.yml`, `main.yml`, `standards.yml`, `changelog-guard.yml`, `monthly.yml` and `release.yml` workflows.
|
|
57
|
+
The rules they keep are on the area pages: [Commands](commands.md), [MCP](mcp.md), [Accounts](accounts.md),
|
|
58
|
+
[Permissions](permissions.md), [Testing](testing.md), [Documents](docs.md), [Releases](releases.md) and [CI](ci.md).
|
|
59
|
+
|
|
60
|
+
Later template changes come in with `npx @wirecat/cli-template upgrade`, file by file: apply, replace, skip or diff.
|
|
61
|
+
See [Kit surface](kit-surface.md#how-your-cli-stays-current).
|
|
62
|
+
|
|
63
|
+
## Four steps that need a person
|
|
64
|
+
|
|
65
|
+
`new` prints these at the end.
|
|
66
|
+
|
|
67
|
+
1. **Create the repository** under the owner, with a ruleset on the default branch: no deletion, no force push,
|
|
68
|
+
changes only through a pull request, required status checks, and no bypass. Add the ruleset after the first
|
|
69
|
+
commit is on `main`, since a pull request rule leaves an empty repository no way to get one, and the required
|
|
70
|
+
check names exist only once CI has run. See [Releases](releases.md).
|
|
71
|
+
2. **Set npm trusted publishing** for the new package, naming `release.yml`, and add any secrets the CI needs. See
|
|
72
|
+
[WC-REL-11](releases.md#wc-rel-11--publish-from-github-actions-with-npm-trusted-publishing).
|
|
73
|
+
3. **Pick a monthly cron day** no other repository uses. `.github/workflows/monthly.yml` holds a placeholder that CI
|
|
74
|
+
rejects. See [CI](ci.md).
|
|
75
|
+
4. **WireCat CLIs only: join the shared lists.** A pull request to cli-base's `PARITY_GROUPS` (at least the `all`
|
|
76
|
+
group, which compares the shared root commands), a line in cli-core's `WIRECAT_TOOLS` and a line in cli-docs'
|
|
77
|
+
`tools.json`. A messenger also joins cli-core's `parity.json`. A CLI outside WireCatLabs keeps its parity groups
|
|
78
|
+
in its own repository and runs `cli-dev audit --groups <file>`. See [Testing](testing.md).
|
|
98
79
|
|
|
99
80
|
## Done when
|
|
100
81
|
|
|
@@ -32,9 +32,10 @@ on the [CI](ci.md) page.
|
|
|
32
32
|
- **Exceptions:** none.
|
|
33
33
|
- **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
34
34
|
|
|
35
|
-
### WC-REL-3 · `bin/release` takes
|
|
35
|
+
### WC-REL-3 · `bin/release` takes its package's release lock before it builds or publishes
|
|
36
36
|
|
|
37
|
-
- **Why:** two sessions published at the same time on one machine
|
|
37
|
+
- **Why:** two sessions published the same package at the same time on one machine. Different packages do not
|
|
38
|
+
conflict, so each package has its own lock and they release side by side.
|
|
38
39
|
- **Enforced by:** `<app>: bin/release`.
|
|
39
40
|
- **Example:**
|
|
40
41
|
|
|
@@ -44,9 +45,12 @@ on the [CI](ci.md) page.
|
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
The helper comes from the community presets (`adopt-standards.py --only release-lock`). It holds
|
|
47
|
-
`${XDG_RUNTIME_DIR:-/tmp}/leemour-release
|
|
48
|
-
|
|
49
|
-
-
|
|
48
|
+
`${XDG_RUNTIME_DIR:-/tmp}/leemour-release-<key>.lock` with `flock` on file descriptor 9 until the release ends.
|
|
49
|
+
`<key>` is the `name` in `package.json`, made safe for a file name: `@wirecat/cli-core` locks
|
|
50
|
+
`leemour-release-wirecat-cli-core.lock`. The key is the package, not the folder, so every checkout and worktree
|
|
51
|
+
of one package shares the lock. A `package.json` without a name refuses the release.
|
|
52
|
+
- **Exceptions:** none.
|
|
53
|
+
- **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** NEED-854, ASK-336.
|
|
50
54
|
|
|
51
55
|
### WC-REL-4 · Never break the release lock; wait for the holder and try again
|
|
52
56
|
|
|
@@ -95,7 +99,8 @@ on the [CI](ci.md) page.
|
|
|
95
99
|
- **Enforced by:** review only.
|
|
96
100
|
- **Example:** tg-cli can release an hour after its last version, and cli-core can ship a breaking change on the same
|
|
97
101
|
day as the previous one. Bump the library in every CLI that uses it on the day it ships.
|
|
98
|
-
- **Exceptions:** none. The release lock (WC-REL-3) still keeps two releases on one machine from
|
|
102
|
+
- **Exceptions:** none. The release lock (WC-REL-3) still keeps two releases of one package on one machine from
|
|
103
|
+
running at once.
|
|
99
104
|
- **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.52.0. **Rulings:** NEED-1741.
|
|
100
105
|
|
|
101
106
|
### WC-REL-9 · Merged into [WC-REL-8](#wc-rel-8--any-package-may-release-at-any-time)
|
|
@@ -162,9 +167,13 @@ on the [CI](ci.md) page.
|
|
|
162
167
|
- **Why:** a pull request merged after the version was dated adds a pending section above it, and that change must
|
|
163
168
|
ship in the version it lands in.
|
|
164
169
|
- **Enforced by:** review only.
|
|
165
|
-
- **Example:** `bin/release` runs `cli-dev changelog
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
- **Example:** `bin/release` first runs `cli-dev changelog restore --land`. It compares the newest tagged version's
|
|
171
|
+
section with its text in the tag: a pull request checked before that release and merged after it put its entry
|
|
172
|
+
there, though its code ships only now. The section goes back to the tag's text and the entry moves to the pending
|
|
173
|
+
section, through a pull request. Then, while npm does not have the version, `bin/release` runs
|
|
174
|
+
`cli-dev changelog fold --land --version <x.y.z>`. It folds the pending section into the dated one, and merges any
|
|
175
|
+
repeated `###` heading, through a pull request.
|
|
176
|
+
- **Exceptions:** only the newest tag is compared; an older section that differs from its tag stays as it is.
|
|
168
177
|
- **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
169
178
|
|
|
170
179
|
### WC-REL-16 · The package ships only the files it names
|
package/docs/standards/store.md
CHANGED
|
@@ -69,11 +69,12 @@ Related pages: which account a read covers, and how a read across accounts is as
|
|
|
69
69
|
### WC-STORE-5 · A CLI writes no SQL against the shared store
|
|
70
70
|
|
|
71
71
|
- **Why:** the schema belongs to cli-core; SQL in a CLI breaks on the next migration.
|
|
72
|
-
- **Enforced by:**
|
|
72
|
+
- **Enforced by:** `<app>: biome no-sqlite-driver`.
|
|
73
73
|
- **Example:** a CLI that needs a new query asks cli-core for an area method, and the method lands in cli-core with
|
|
74
74
|
its test. drive and zoom state "no SQL here" in their own architecture pages.
|
|
75
75
|
- **Exceptions:** a provider engine's own file of keys and device state, outside `wirecat.db`
|
|
76
|
-
([WC-STORE-13](#wc-store-13--keep-keys-and-device-state-out-of-the-shared-store)).
|
|
76
|
+
([WC-STORE-13](#wc-store-13--keep-keys-and-device-state-out-of-the-shared-store)). The check skips tests and tooling
|
|
77
|
+
(`scripts/`, `bench/`, `performance/`, `contracts/`, `testing/`), and cli-core, which owns the store, turns it off.
|
|
77
78
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-383.
|
|
78
79
|
|
|
79
80
|
### WC-STORE-6 · One store method is one whole operation, with no `await` inside a transaction
|
|
@@ -193,8 +194,6 @@ request that builds the helper.
|
|
|
193
194
|
- **A CLI's own tables live in its own SQLite file**, next to the shared store, with its own migrations. They refer to
|
|
194
195
|
core rows by id only, and no other CLI opens that file. A cli-core helper opens it (the path, the migrations, the
|
|
195
196
|
same driver); it is built when the first CLI needs private tables.
|
|
196
|
-
- **WC-STORE-5 gets a check:** a grit plugin, `no-sqlite-driver`, that refuses a CLI's import of a SQLite driver or
|
|
197
|
-
of `drizzle-orm`. cli-core, which owns the store, turns it off.
|
|
198
197
|
|
|
199
198
|
## Sources
|
|
200
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|