@wirecat/cli-template 0.1.0 → 0.1.1
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 +12 -0
- package/config/wirecat/biome.base.json +17 -0
- package/config/wirecat/manifest.json +6 -3
- package/config/wirecat/no-sqlite-driver.grit +15 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/{.gitignore → gitignore} +2 -0
- package/kit/README.md +8 -4
- package/kit/dist/new.js +9 -2
- package/kit/dist/rename.js +1 -0
- package/kit/dist/source.js +30 -6
- package/kit/dist/upgrade.js +8 -1
- package/package.json +5 -4
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
Notable changes to `@wirecat/cli-template`. One section per version, newest first; versions follow
|
|
4
4
|
[semantic versioning](https://semver.org), so before `1.0.0` the skeleton may still change.
|
|
5
5
|
|
|
6
|
+
## 0.1.1 — 25.10.2026
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **`npx @wirecat/cli-template new` works.** It stopped with `template.json lists ".gitignore", but the package has
|
|
11
|
+
no such file`, because npm renames `.gitignore` when it installs a package. The package now ships the file under
|
|
12
|
+
another name, and `new` and `upgrade` write it as `.gitignore`.
|
|
13
|
+
- **`new` and `upgrade` work on Windows.** `new` now marks `bin/release` and the other scripts as executable in
|
|
14
|
+
git, so a CLI made on Windows can still run them on Linux and macOS. `upgrade` no longer reads every file as
|
|
15
|
+
edited when git checked it out with Windows line endings, and it downloads the earlier template version without
|
|
16
|
+
needing a shell for `npm`.
|
|
17
|
+
|
|
6
18
|
## 0.1.0 — 25.10.2026
|
|
7
19
|
|
|
8
20
|
### 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": "2b0f124f6375939bba10c28a7f091923323e9a13",
|
|
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",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"release-lock-info.mjs": "4abab1f01ceee7d4c709222e923c9a6fc634bcf7b8b64d745a9ac495993310e0",
|
|
15
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",
|
|
@@ -29,6 +30,8 @@
|
|
|
29
30
|
},
|
|
30
31
|
"fileRevisions": {
|
|
31
32
|
"release-lock.sh": "49e630a99d758e3e113749bef2049b258c7262e3",
|
|
32
|
-
"release-lock-info.mjs": "49e630a99d758e3e113749bef2049b258c7262e3"
|
|
33
|
+
"release-lock-info.mjs": "49e630a99d758e3e113749bef2049b258c7262e3",
|
|
34
|
+
"biome.base.json": "06c612c609d870f0ba00010612e0413f65340408",
|
|
35
|
+
"no-sqlite-driver.grit": "06c612c609d870f0ba00010612e0413f65340408"
|
|
33
36
|
}
|
|
34
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
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.1";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/{.gitignore → gitignore}
RENAMED
package/kit/README.md
CHANGED
|
@@ -116,8 +116,12 @@ A conflict leaves git markers, and `template.json` keeps the new state under `pe
|
|
|
116
116
|
- `"bin": { "cli-template": "kit/dist/main.js" }`, and `kit/dist` in `files`. No ignore file inside `kit/`: npm
|
|
117
117
|
applies it and drops `kit/dist` from the package. npm also never packs `pnpm-lock.yaml`, so it is not a seed; the
|
|
118
118
|
new CLI makes its own on `pnpm install`.
|
|
119
|
-
- Every template-owned file and seed in `files`. npm drops a nested `.gitignore` and any `.npmrc
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
- Every template-owned file and seed in `files`. npm drops a nested `.gitignore` and any `.npmrc` when it packs.
|
|
120
|
+
When it installs a package, it renames `.gitignore` to `.npmignore`. So the package ships the root `.gitignore` as
|
|
121
|
+
`gitignore`, and `new` and `upgrade` read it back as `.gitignore`. The test in `kit/test/package.test.ts` runs the
|
|
122
|
+
bin from a tarball installed with `npm install` to catch that.
|
|
123
|
+
- `kit:build` (`tsc -p kit`, then the copy to `gitignore`) run by `build`, because `bin/release` and CI pack with
|
|
124
|
+
`--ignore-scripts`. Node does not strip types under `node_modules`, so the bin must be JavaScript.
|
|
125
|
+
- `pnpm kit:e2e` packs this repository, installs the tarball with npm, runs `new` and then `pnpm install` and
|
|
126
|
+
`pnpm ci:quick` in the new CLI. It needs the network and takes a few minutes, so it is not part of `pnpm test`.
|
|
123
127
|
- `kit/test/**/*.test.ts` in vitest's `include`; `kit/dist` is ignored by the root `**/dist` rules of Biome and git.
|
package/kit/dist/new.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { ask, choiceList, INTERVIEW, labelOf, NAME_QUESTIONS, parseAnswer } from "./interview.js";
|
|
7
7
|
import { MANIFEST, sha256 } from "./manifest.js";
|
|
8
|
-
import { renderTemplate } from "./source.js";
|
|
8
|
+
import { PACKED_AS, renderTemplate } from "./source.js";
|
|
9
9
|
const run = promisify(execFile);
|
|
10
10
|
const REFERENCES = {
|
|
11
11
|
messenger: ["whatsapp-cli", "tg-cli", "max-cli"],
|
|
@@ -140,7 +140,7 @@ export const cliPackageJson = (text, names) => {
|
|
|
140
140
|
next.bin = Object.fromEntries(Object.entries(pkg.bin).filter(([, value]) => !isKit(value)));
|
|
141
141
|
}
|
|
142
142
|
if (Array.isArray(pkg.files))
|
|
143
|
-
next.files = pkg.files.filter((entry) => !isKit(entry));
|
|
143
|
+
next.files = pkg.files.filter((entry) => !isKit(entry) && !Object.values(PACKED_AS).includes(entry));
|
|
144
144
|
if (typeof pkg.scripts === "object" && pkg.scripts) {
|
|
145
145
|
next.scripts = Object.fromEntries(Object.entries(pkg.scripts)
|
|
146
146
|
.filter(([key]) => !key.startsWith("kit:"))
|
|
@@ -192,6 +192,12 @@ const writeOut = async (dir, path, content, mode = 0o644) => {
|
|
|
192
192
|
await writeFile(full, content);
|
|
193
193
|
await chmod(full, mode);
|
|
194
194
|
};
|
|
195
|
+
// Git for Windows sets core.fileMode=false, so a later `git add -A` there records every file as 644; a mode set
|
|
196
|
+
// in the index survives that add.
|
|
197
|
+
export const markExecutable = async (cwd, paths) => {
|
|
198
|
+
if (paths.length)
|
|
199
|
+
await run("git", ["add", "--chmod=+x", "--", ...paths], { cwd });
|
|
200
|
+
};
|
|
195
201
|
export const manualSteps = (names) => [
|
|
196
202
|
`1. Create the repository ${names.owner}/${names.name} and its main ruleset (cli-base docs/standards/new-cli.md).`,
|
|
197
203
|
`2. Set npm trusted publishing for ${names.package}, naming release.yml (WC-REL-11), and add any secrets.`,
|
|
@@ -232,6 +238,7 @@ export const runNew = async (cwd, source, args, io) => {
|
|
|
232
238
|
};
|
|
233
239
|
await writeOut(target, MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
234
240
|
await run("git", ["init", "-q", "-b", "main"], { cwd: target });
|
|
241
|
+
await markExecutable(target, [...rendered].filter(([, file]) => file.mode & 0o111).map(([path]) => path));
|
|
235
242
|
const warnings = languageWarnings(answers.contentLang ?? []);
|
|
236
243
|
return [
|
|
237
244
|
`Created ${target} from ${source.name}@${source.version}.`,
|
package/kit/dist/rename.js
CHANGED
|
@@ -19,6 +19,7 @@ export const renamer = (placeholders, names) => {
|
|
|
19
19
|
return (text) => text.replace(pattern, (match) => table.get(match) ?? match);
|
|
20
20
|
};
|
|
21
21
|
const isBinary = (content) => content.subarray(0, 8000).includes(0);
|
|
22
|
+
export const withLf = (content) => isBinary(content) || !content.includes(13) ? content : Buffer.from(content.toString("utf8").replaceAll("\r\n", "\n"));
|
|
22
23
|
export const renameContent = (rename, content) => isBinary(content) ? content : Buffer.from(rename(content.toString("utf8")), "utf8");
|
|
23
24
|
const KIT_CONFIG = /(^|\/)(vitest\.config\.ts|biome\.json|cspell\.json|tsconfig[^/]*\.json|\.markdownlint-cli2\.jsonc)$/;
|
|
24
25
|
// The template's configs name kit/ so its own checks cover the copy tool; a CLI has no kit/, and the
|
package/kit/dist/source.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, relative, sep } from "node:path";
|
|
5
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { promisify } from "node:util";
|
|
7
8
|
import { classify, MANIFEST, parseTemplateManifest, readJson, } from "./manifest.js";
|
|
@@ -18,6 +19,15 @@ export const loadSource = async (root) => {
|
|
|
18
19
|
return { root, name: pkg.name, version: pkg.version, manifest };
|
|
19
20
|
};
|
|
20
21
|
const SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
22
|
+
// npm renames .gitignore to .npmignore when it installs a package, so the package ships it as gitignore.
|
|
23
|
+
export const PACKED_AS = { ".gitignore": "gitignore" };
|
|
24
|
+
const onDisk = async (root, path) => {
|
|
25
|
+
const packed = PACKED_AS[path];
|
|
26
|
+
const full = join(root, path);
|
|
27
|
+
if (!packed)
|
|
28
|
+
return full;
|
|
29
|
+
return (await stat(full).catch(() => undefined)) ? full : join(root, packed);
|
|
30
|
+
};
|
|
21
31
|
const walk = async (dir) => {
|
|
22
32
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
23
33
|
const nested = await Promise.all(entries.map(async (entry) => {
|
|
@@ -29,7 +39,7 @@ const walk = async (dir) => {
|
|
|
29
39
|
return nested.flat();
|
|
30
40
|
};
|
|
31
41
|
const listEntry = async (root, entry) => {
|
|
32
|
-
const path =
|
|
42
|
+
const path = await onDisk(root, entry);
|
|
33
43
|
const info = await stat(path).catch(() => undefined);
|
|
34
44
|
if (!info)
|
|
35
45
|
throw new Error(`${MANIFEST} lists "${entry}", but the package has no such ${entry.endsWith("/") ? "folder" : "file"}`);
|
|
@@ -45,26 +55,40 @@ export const listTemplate = async (source) => {
|
|
|
45
55
|
return kind ? [[path, kind]] : [];
|
|
46
56
|
}));
|
|
47
57
|
};
|
|
58
|
+
// npm cannot set the executable bit when it installs on Windows, so a shebang stands in for it there. A .ts or .js
|
|
59
|
+
// entry point carries one for node, not for the shell.
|
|
60
|
+
const isExecutable = (path, content, mode) => (mode & 0o111) !== 0 || (content.subarray(0, 2).toString() === "#!" && !/\.[cm]?[jt]sx?$/.test(path));
|
|
48
61
|
export const renderTemplate = async (source, names) => {
|
|
49
62
|
const rename = renamer(source.manifest.placeholders, names);
|
|
50
63
|
const listed = await listTemplate(source);
|
|
51
64
|
const rendered = await Promise.all([...listed].map(async ([path, kind]) => {
|
|
52
|
-
const full =
|
|
65
|
+
const full = await onDisk(source.root, path);
|
|
53
66
|
const [content, info] = await Promise.all([readFile(full), stat(full)]);
|
|
54
67
|
const renamed = renameContent(rename, stripKit(path, content));
|
|
55
|
-
|
|
68
|
+
const mode = isExecutable(path, content, info.mode) ? 0o755 : 0o644;
|
|
69
|
+
return [rename(path), { kind, content: renamed, mode }];
|
|
56
70
|
}));
|
|
57
71
|
return new Map(rendered);
|
|
58
72
|
};
|
|
73
|
+
// On Windows npm is npm.cmd, which execFile cannot start without a shell; npm-cli.js beside node runs anywhere.
|
|
74
|
+
export const npmCommand = (args) => {
|
|
75
|
+
const home = dirname(process.execPath);
|
|
76
|
+
const cli = [
|
|
77
|
+
join(home, "node_modules/npm/bin/npm-cli.js"),
|
|
78
|
+
join(home, "../lib/node_modules/npm/bin/npm-cli.js"),
|
|
79
|
+
].find((path) => existsSync(path));
|
|
80
|
+
return cli ? [process.execPath, [cli, ...args]] : ["npm", args];
|
|
81
|
+
};
|
|
59
82
|
export const fetchPublished = async (name, version) => {
|
|
60
83
|
const dir = await mkdtemp(join(tmpdir(), "cli-template-"));
|
|
61
|
-
const { stdout } = await run(
|
|
84
|
+
const { stdout } = await run(...npmCommand(["pack", `${name}@${version}`, "--silent", "--pack-destination", dir])).catch((error) => {
|
|
62
85
|
throw new Error(`could not download ${name}@${version} from npm, which upgrade needs as the merge base: ${error.message}`);
|
|
63
86
|
});
|
|
64
87
|
const tarball = stdout.trim().split("\n").pop() ?? "";
|
|
65
88
|
const dispose = () => rm(dir, { recursive: true, force: true });
|
|
66
89
|
try {
|
|
67
|
-
|
|
90
|
+
// GNU tar reads "C:" in a path as a remote host, so the archive is named relative to its folder.
|
|
91
|
+
await run("tar", ["-xzf", tarball], { cwd: dir });
|
|
68
92
|
return { source: await loadSource(join(dir, "package")), dispose };
|
|
69
93
|
}
|
|
70
94
|
catch (error) {
|
package/kit/dist/upgrade.js
CHANGED
|
@@ -5,10 +5,13 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { classify, MANIFEST, readCliManifest, sha256 } from "./manifest.js";
|
|
8
|
+
import { markExecutable } from "./new.js";
|
|
9
|
+
import { withLf } from "./rename.js";
|
|
8
10
|
import { renderTemplate } from "./source.js";
|
|
9
11
|
const run = promisify(execFile);
|
|
10
12
|
const MODES = ["apply", "replace", "skip"];
|
|
11
|
-
|
|
13
|
+
// Git for Windows checks text out with CRLF by default; the template's hashes and merges are of LF text.
|
|
14
|
+
const readMaybe = (path) => readFile(path).then(withLf, () => undefined);
|
|
12
15
|
const requireCleanTree = async (cwd) => {
|
|
13
16
|
const { stdout } = await run("git", ["status", "--porcelain"], { cwd }).catch(() => {
|
|
14
17
|
throw new Error(`${cwd} is not a git repository; upgrade needs one so every change can be seen and undone`);
|
|
@@ -173,11 +176,14 @@ export const runUpgrade = async (cwd, target, fetchVersion, options, io) => {
|
|
|
173
176
|
};
|
|
174
177
|
const results = [];
|
|
175
178
|
const conflicts = [];
|
|
179
|
+
const executable = [];
|
|
176
180
|
try {
|
|
177
181
|
for (const change of changes) {
|
|
178
182
|
const { outcome, content } = await planned(change, await choose(change));
|
|
179
183
|
if (content)
|
|
180
184
|
await writeAs(join(cwd, change.path), content, change.ours ? undefined : change.theirs.mode);
|
|
185
|
+
if (content && !change.ours && change.theirs.mode & 0o111)
|
|
186
|
+
executable.push(change.path);
|
|
181
187
|
if (outcome === "conflict")
|
|
182
188
|
conflicts.push(change.path);
|
|
183
189
|
results.push(` ${change.path} (${change.state}): ${outcome}`);
|
|
@@ -186,6 +192,7 @@ export const runUpgrade = async (cwd, target, fetchVersion, options, io) => {
|
|
|
186
192
|
finally {
|
|
187
193
|
rl?.close();
|
|
188
194
|
}
|
|
195
|
+
await markExecutable(cwd, executable);
|
|
189
196
|
const footer = ["Then re-run community's adopt-standards.py for config/wirecat/, which upgrade never touches."];
|
|
190
197
|
const files = Object.fromEntries([...owned].map(([path, file]) => [path, sha256(file.content)]));
|
|
191
198
|
if (conflicts.length) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-template",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Start a new WireCat CLI: a working skeleton on cli-base and cli-core, copied by new and kept current by upgrade",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"tsconfig.scripts.json",
|
|
47
47
|
"tsconfig.test.json",
|
|
48
48
|
"vitest.config.ts",
|
|
49
|
-
"
|
|
49
|
+
"gitignore"
|
|
50
50
|
],
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsc --build --force && pnpm kit:build",
|
|
@@ -76,8 +76,9 @@
|
|
|
76
76
|
"prepare": "lefthook install || true",
|
|
77
77
|
"prepack": "pnpm build",
|
|
78
78
|
"docs:spell": "cspell lint --config cspell.json --no-progress README.md",
|
|
79
|
-
"kit:build": "tsc -p kit",
|
|
80
|
-
"kit:typecheck": "tsc -p kit/tsconfig.test.json"
|
|
79
|
+
"kit:build": "tsc -p kit && node -e \"require('node:fs').copyFileSync('.gitignore', 'gitignore')\"",
|
|
80
|
+
"kit:typecheck": "tsc -p kit/tsconfig.test.json",
|
|
81
|
+
"kit:e2e": "bash kit/e2e.sh"
|
|
81
82
|
},
|
|
82
83
|
"dependencies": {
|
|
83
84
|
"@modelcontextprotocol/server": "2.3.0",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.1"
|