@wirecat/cli-template 0.1.5 → 0.1.7
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/.github/workflows/changelog-guard.yml +1 -1
- package/.github/workflows/ci.yml +7 -3
- package/.github/workflows/main.yml +2 -1
- package/.markdownlint-cli2.jsonc +11 -1
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +56 -0
- package/README.md +26 -6
- package/SECURITY.md +1 -1
- package/bin/release +24 -3
- package/config/parity-groups.json +6 -0
- package/contracts/help.snap +2 -2
- package/contracts/mcp-tools.json +2 -2
- package/cspell.json +1 -1
- package/dist/commands/doctor.d.ts +69 -36
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +42 -15
- package/dist/commands/doctor.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/commands.md +22 -6
- package/docs/configuration.md +1 -1
- package/docs/dev/TESTING.md +2 -0
- package/docs/dev/skills/release/SKILL.md +3 -1
- package/docs/dev/test-matrix.md +3 -1
- package/docs/index.md +3 -3
- package/docs/installation.md +3 -3
- package/docs/security.md +6 -2
- package/docs/troubleshooting.md +2 -1
- package/docs/usage.md +1 -1
- package/kit/dist/main.js +1 -1
- package/kit/dist/manifest.js +18 -30
- package/kit/dist/new.js +14 -6
- package/kit/dist/rename.js +1 -0
- package/kit/dist/source.js +7 -4
- package/package.json +8 -7
- package/pnpm-workspace.yaml +2 -0
- package/scripts/commands.ts +1 -1
- package/scripts/docs-rules.ts +1 -1
- package/scripts/smoke.ts +19 -10
- package/skills/clidev-cli/SKILL.md +3 -3
- package/src/commands/doctor.ts +53 -16
- package/src/mcp.ts +1 -1
- package/src/program.test.ts +35 -2
- package/src/roots.test.ts +16 -0
- package/src/testing/sandbox.ts +4 -7
- package/src/version.ts +1 -1
- package/vitest.config.ts +3 -1
package/docs/commands.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Every command, option and exit code, generated from the program itself. For the same list as JSON, run
|
|
6
6
|
`clidev commands --json`.
|
|
7
7
|
|
|
8
|
-
**The first word is the profile**, a name you choose: `clidev personal
|
|
8
|
+
**The first word is the profile**, a name you choose: `clidev personal config show`. `CLIDEV_PROFILE`
|
|
9
9
|
does the same for a whole shell session. No profile is assumed: a command that uses an account stops without one and
|
|
10
10
|
lists the profiles on this computer.
|
|
11
11
|
|
|
@@ -62,7 +62,7 @@ clidev doctor
|
|
|
62
62
|
|
|
63
63
|
**Returns:** check this installation: version, paths, config, profiles; no keyring or network
|
|
64
64
|
|
|
65
|
-
**Example:** `clidev
|
|
65
|
+
**Example:** `clidev doctor`, `clidev doctor --json`
|
|
66
66
|
|
|
67
67
|
## `clidev skill`
|
|
68
68
|
|
|
@@ -70,16 +70,20 @@ the instructions an agent is given for this tool
|
|
|
70
70
|
|
|
71
71
|
### `clidev skill show`
|
|
72
72
|
|
|
73
|
-
print SKILL.md — `clidev skill install` puts it where Claude Code, Codex and Gemini CLI look for it
|
|
73
|
+
print SKILL.md — `clidev skill install` puts it where Claude Code, Codex and Gemini CLI look for it; --for prints the copy already there
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
|
-
clidev skill show [name]
|
|
76
|
+
clidev skill show [name] [options]
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
| Argument | | What it is |
|
|
80
80
|
|---|---|---|
|
|
81
81
|
| `name` | optional | one of the skills shipped for a task: follow-ups, link-conversations. |
|
|
82
82
|
|
|
83
|
+
| Option | What it does |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `--for <agents>` | print the copy installed for these agents instead of this version's. One of: `claude`, `agents`. |
|
|
86
|
+
|
|
83
87
|
**Returns:** print the tool's instructions for AI agents
|
|
84
88
|
|
|
85
89
|
**Example:** `clidev skill show`, `clidev skill show follow-ups`
|
|
@@ -102,6 +106,18 @@ clidev skill install [options]
|
|
|
102
106
|
|
|
103
107
|
**Example:** `clidev skill install --for agents`
|
|
104
108
|
|
|
109
|
+
### `clidev skill status`
|
|
110
|
+
|
|
111
|
+
for each place `clidev skill install` writes to: the path, the version installed there, and whether it is current, stale, missing or unversioned
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
clidev skill status
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Returns:** whether the installed instructions for AI agents match this version
|
|
118
|
+
|
|
119
|
+
**Example:** `clidev skill status`, `clidev skill status --json`
|
|
120
|
+
|
|
105
121
|
## `clidev mcp`
|
|
106
122
|
|
|
107
123
|
serve this profile's commands to an agent over MCP, on stdin and stdout, writes included; the profile's permissions decide — `claude mcp add clidev -- clidev personal mcp`
|
|
@@ -224,7 +240,7 @@ clidev runs list [query] [options]
|
|
|
224
240
|
| `--limit <n>` | how many to show. Default: `20`. |
|
|
225
241
|
| `--page <n>` | which page, starting at 1. Default: `1`. |
|
|
226
242
|
|
|
227
|
-
**Returns:** run — run_id, command, started_at, status, cli_version, profile, completed_at, requests, and
|
|
243
|
+
**Returns:** run — run_id, command, started_at, status, cli_version, profile, completed_at, requests, and 20 more; every field: clidev commands info runs list
|
|
228
244
|
|
|
229
245
|
**Example:** `clidev personal runs list --limit 10`
|
|
230
246
|
|
|
@@ -244,7 +260,7 @@ clidev runs show <run-id> [options]
|
|
|
244
260
|
|---|---|
|
|
245
261
|
| `--problems` | only failed responses, warnings and crashes. |
|
|
246
262
|
|
|
247
|
-
**Returns:** run details — run_id, command, started_at, status, cli_version, directory, event_count, events, and
|
|
263
|
+
**Returns:** run details — run_id, command, started_at, status, cli_version, directory, event_count, events, and 19 more; every field: clidev commands info runs show
|
|
248
264
|
|
|
249
265
|
**Example:** `clidev personal runs show run-101`
|
|
250
266
|
|
package/docs/configuration.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Settings
|
|
2
2
|
|
|
3
|
-
Settings live in one file, `config.json`, in the folder `clidev
|
|
3
|
+
Settings live in one file, `config.json`, in the folder `clidev doctor` shows. Each profile has its own part,
|
|
4
4
|
and `defaults` apply to every profile. This page lists how to see and change them.
|
|
5
5
|
|
|
6
6
|
```sh
|
package/docs/dev/TESTING.md
CHANGED
|
@@ -12,5 +12,7 @@ How the tests run, and what they never touch. The rules are the handbook's
|
|
|
12
12
|
`scripts/test-matrix-untested.ts`.
|
|
13
13
|
- `pnpm contracts:check` runs the built CLI against `contracts/plan.json`: help, discovery, errors and the MCP tool
|
|
14
14
|
list. `cli-contract contracts/plan.json --update` rewrites the snapshots; review the diff.
|
|
15
|
+
- `pnpm contracts:upgrade` does not exist yet: the skeleton has no store of its own, and `.github/workflows/ci.yml`
|
|
16
|
+
skips the step. Add it with the first store, and remove the skip.
|
|
15
17
|
- `pnpm smoke` installs the packed package into a temporary folder and runs it offline.
|
|
16
18
|
- `pnpm audit:commands` audits the command tree beside the latest published WireCat CLIs.
|
|
@@ -24,4 +24,6 @@ and runs `bin/release`.
|
|
|
24
24
|
`package.json`, run `pnpm version:sync`, date the section as `## <version> — DD.MM.YYYY`, run
|
|
25
25
|
`pnpm release:check` again, and open the pull request `chore(release): <version>`.
|
|
26
26
|
7. **The owner** merges it and runs `bin/release` on a clean `main`. `bin/release --local` publishes from this
|
|
27
|
-
computer when GitHub Actions cannot, with the owner's own npm login
|
|
27
|
+
computer when GitHub Actions cannot, with `NPM_TOKEN`, the npm token in the keyring, or the owner's own npm login.
|
|
28
|
+
The keyring is read only when `NPM_KEYRING_ACCOUNT` names the account the token is stored under (service `npm`);
|
|
29
|
+
there is no default account.
|
package/docs/dev/test-matrix.md
CHANGED
|
@@ -7,7 +7,7 @@ ran it · ⛔ not tested offline, with the reason and where it is checked instea
|
|
|
7
7
|
Measured from the test run (`coverage/argv.jsonl`), not searched for — see
|
|
8
8
|
[TESTING.md](TESTING.md) for how, and for the states and failures that cut across commands.
|
|
9
9
|
|
|
10
|
-
**
|
|
10
|
+
**45 ✅ · 2 ⛔ · 0 ❌** — 15 commands, 32 options.
|
|
11
11
|
|
|
12
12
|
| Command | Option | | Note |
|
|
13
13
|
|---|---|---|---|
|
|
@@ -15,8 +15,10 @@ Measured from the test run (`coverage/argv.jsonl`), not searched for — see
|
|
|
15
15
|
| `commands info` | | ✅ | |
|
|
16
16
|
| `doctor` | | ✅ | |
|
|
17
17
|
| `skill show` | | ✅ | |
|
|
18
|
+
| `skill show` | `--for` | ✅ | |
|
|
18
19
|
| `skill install` | | ✅ | |
|
|
19
20
|
| `skill install` | `--for` | ✅ | |
|
|
21
|
+
| `skill status` | | ✅ | |
|
|
20
22
|
| `mcp` | | ✅ | |
|
|
21
23
|
| `upgrade` | | ✅ | |
|
|
22
24
|
| `upgrade` | `--check` | ✅ | |
|
package/docs/index.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# clidev
|
|
2
2
|
|
|
3
|
-
`clidev`
|
|
4
|
-
|
|
3
|
+
`clidev` is a WireCat command-line tool for the terminal, for scripts and for AI agents. It does not connect to a
|
|
4
|
+
service yet.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
It has the commands every WireCat CLI shares: `doctor`, `commands`, `config`, `skill`, `mcp`, `runs` and `upgrade`.
|
|
7
7
|
Start with [installation](installation.md), then [usage](usage.md).
|
package/docs/installation.md
CHANGED
|
@@ -10,8 +10,8 @@ clidev --version
|
|
|
10
10
|
Check the installation, then pick a profile name for your account, such as `personal`:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
clidev
|
|
13
|
+
clidev doctor
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`doctor` needs no network and opens no keyring. It shows the version, where settings live
|
|
17
|
-
exist. Next: [use it](usage.md).
|
|
16
|
+
`doctor` needs no network and opens no keyring. It shows the version, where settings live, the profiles that
|
|
17
|
+
exist, and whether other users of the computer can read the settings. Next: [use it](usage.md).
|
package/docs/security.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
What WireCat CLIs share — the keyring, permissions, MCP and what never reaches a log — is on the
|
|
4
4
|
[shared security page](https://wirecat.dev/en/docs/security). This page keeps only what is particular to `clidev`.
|
|
5
5
|
|
|
6
|
-
`clidev` keeps its settings in `config.json` and its state in the folders `clidev
|
|
7
|
-
to any service yet.
|
|
6
|
+
`clidev` keeps its settings in `config.json` and its state in the folders `clidev doctor` shows. It sends nothing
|
|
7
|
+
to any service yet.
|
|
8
|
+
|
|
9
|
+
`config.json` is saved readable by you only (`0600`), and its folder is created for you only (`0700`). A file or
|
|
10
|
+
folder made by an older version may be open to other users of the computer: `clidev doctor` lists each under
|
|
11
|
+
`files.problems` and prints the `chmod` that fixes it. Windows is not checked. Report a vulnerability as [SECURITY.md](https://github.com/WireCatLabs/cli-template/blob/main/SECURITY.md)
|
|
8
12
|
says.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -5,10 +5,11 @@ support request.
|
|
|
5
5
|
|
|
6
6
|
| Symptom | What to do |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| Not sure what is set up | Run `clidev
|
|
8
|
+
| Not sure what is set up | Run `clidev doctor`. It reads no keyring and uses no network. |
|
|
9
9
|
| `clidev` is not found | Open a new terminal, and check that npm's global folder is on `PATH`. |
|
|
10
10
|
| "name a profile" | Put the profile first, as in `clidev personal config show`, or set `CLIDEV_PROFILE`. |
|
|
11
11
|
| The settings file does not read | `doctor` lists every problem in the file under `config.issues`. Run the `config unset` each one gives, or fix the file by hand. |
|
|
12
|
+
| `doctor` lists `files.problems` | Other users of the computer can read `config.json` or enter its folder. Run each `chmod` that `doctor` prints after "to make it private:". `doctor` never changes a file mode itself. |
|
|
12
13
|
| A write is refused | The profile's permissions or `CLIDEV_READ_ONLY` stop it; see [settings](configuration.md#permissions). |
|
|
13
14
|
|
|
14
15
|
Never paste a token, a password or private content into a support request.
|
package/docs/usage.md
CHANGED
|
@@ -15,7 +15,7 @@ command without one stops with exit code 2 and lists the profiles on this comput
|
|
|
15
15
|
|
|
16
16
|
## Output for scripts
|
|
17
17
|
|
|
18
|
-
Every command takes `--json`, and prints one JSON value on stdout. Errors are JSON on stderr, with a code to branch
|
|
18
|
+
Every command except `mcp` takes `--json`, and prints one JSON value on stdout. Errors are JSON on stderr, with a code to branch
|
|
19
19
|
on. The [command reference](commands.md) lists the codes.
|
|
20
20
|
|
|
21
21
|
## Agents
|
package/kit/dist/main.js
CHANGED
|
@@ -65,6 +65,6 @@ const io = {
|
|
|
65
65
|
main(process.argv.slice(2), io).then((text) => {
|
|
66
66
|
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
67
67
|
}, (error) => {
|
|
68
|
-
process.stderr.write(`cli-template: ${error.message}\n`);
|
|
68
|
+
process.stderr.write(`cli-template: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
69
69
|
process.exitCode = 1;
|
|
70
70
|
});
|
package/kit/dist/manifest.js
CHANGED
|
@@ -19,7 +19,11 @@ const fail = (file, problem) => {
|
|
|
19
19
|
throw new Error(`${file}: ${problem}`);
|
|
20
20
|
};
|
|
21
21
|
const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0);
|
|
22
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
export const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
const isPlaceholder = (key) => key in DEFAULT_PLACEHOLDERS;
|
|
24
|
+
const isNames = (value) => isRecord(value) && ["name", "command", "package", "owner"].every((key) => typeof value[key] === "string");
|
|
25
|
+
const isHashes = (value) => isRecord(value) && Object.values(value).every((hash) => typeof hash === "string");
|
|
26
|
+
const isPending = (value) => isRecord(value) && typeof value.version === "string" && isHashes(value.files) && isStringList(value.conflicts);
|
|
23
27
|
const checkEntries = (file, key, entries) => {
|
|
24
28
|
for (const entry of entries) {
|
|
25
29
|
if (entry.startsWith("/") || entry.split("/").includes(".."))
|
|
@@ -35,20 +39,18 @@ export const parseTemplateManifest = (file, raw) => {
|
|
|
35
39
|
return fail(file, `"seeds" must be a list of paths`);
|
|
36
40
|
checkEntries(file, "files", raw.files);
|
|
37
41
|
checkEntries(file, "seeds", raw.seeds);
|
|
38
|
-
const
|
|
39
|
-
if (!isRecord(
|
|
42
|
+
const given = raw.placeholders ?? {};
|
|
43
|
+
if (!isRecord(given))
|
|
40
44
|
return fail(file, `"placeholders" must be an object`);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
const placeholders = { ...DEFAULT_PLACEHOLDERS };
|
|
46
|
+
for (const [key, value] of Object.entries(given)) {
|
|
47
|
+
if (!isPlaceholder(key))
|
|
48
|
+
return fail(file, `unknown placeholder "${key}"`);
|
|
44
49
|
if (typeof value !== "string" || value.length === 0)
|
|
45
|
-
fail(file, `placeholder "${key}" must be a string`);
|
|
50
|
+
return fail(file, `placeholder "${key}" must be a string`);
|
|
51
|
+
placeholders[key] = value;
|
|
46
52
|
}
|
|
47
|
-
return {
|
|
48
|
-
files: raw.files,
|
|
49
|
-
seeds: raw.seeds,
|
|
50
|
-
placeholders: { ...DEFAULT_PLACEHOLDERS, ...placeholders },
|
|
51
|
-
};
|
|
53
|
+
return { files: raw.files, seeds: raw.seeds, placeholders };
|
|
52
54
|
};
|
|
53
55
|
export const parseCliManifest = (file, raw) => {
|
|
54
56
|
if (!isRecord(raw))
|
|
@@ -58,29 +60,15 @@ export const parseCliManifest = (file, raw) => {
|
|
|
58
60
|
return fail(file, `"template" must be the template's package name`);
|
|
59
61
|
if (typeof version !== "string")
|
|
60
62
|
return fail(file, `"version" must be the template version this CLI is on`);
|
|
61
|
-
if (!
|
|
63
|
+
if (!isNames(names))
|
|
62
64
|
return fail(file, `"names" must hold name, command, package and owner`);
|
|
63
|
-
|
|
64
|
-
if (!isRecord(files) || !Object.values(files).every((hash) => typeof hash === "string")) {
|
|
65
|
+
if (!isHashes(files))
|
|
65
66
|
return fail(file, `"files" must map each template-owned path to its sha256`);
|
|
66
|
-
}
|
|
67
67
|
if (!isStringList(seeds))
|
|
68
68
|
return fail(file, `"seeds" must be a list of paths`);
|
|
69
|
-
if (pending !== undefined &&
|
|
70
|
-
(!isRecord(pending) ||
|
|
71
|
-
typeof pending.version !== "string" ||
|
|
72
|
-
!isRecord(pending.files) ||
|
|
73
|
-
!isStringList(pending.conflicts))) {
|
|
69
|
+
if (pending !== undefined && !isPending(pending))
|
|
74
70
|
return fail(file, `"pending" must hold version, files and conflicts`);
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
template,
|
|
78
|
-
version,
|
|
79
|
-
names: names,
|
|
80
|
-
files: files,
|
|
81
|
-
seeds,
|
|
82
|
-
...(pending ? { pending: pending } : {}),
|
|
83
|
-
};
|
|
71
|
+
return { template, version, names, files, seeds, ...(pending ? { pending } : {}) };
|
|
84
72
|
};
|
|
85
73
|
export const readJson = async (path) => {
|
|
86
74
|
const text = await readFile(path, "utf8").catch(() => fail(path, "not found"));
|
package/kit/dist/new.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
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
|
-
import { MANIFEST, sha256 } from "./manifest.js";
|
|
7
|
+
import { isRecord, MANIFEST, sha256 } from "./manifest.js";
|
|
8
8
|
import { PACKED_AS, renderTemplate } from "./source.js";
|
|
9
9
|
const run = promisify(execFile);
|
|
10
10
|
const REFERENCES = {
|
|
@@ -133,6 +133,8 @@ const removeKit = (command) => command
|
|
|
133
133
|
.join(" && ");
|
|
134
134
|
export const cliPackageJson = (text, names) => {
|
|
135
135
|
const pkg = JSON.parse(text);
|
|
136
|
+
if (!isRecord(pkg))
|
|
137
|
+
throw new Error("package.json must be a JSON object");
|
|
136
138
|
const isKit = (value) => typeof value === "string" && /^(\.\/)?kit(\/|$)/.test(value);
|
|
137
139
|
const repoUrl = `git+https://github.com/${names.owner}/${names.name}.git`;
|
|
138
140
|
const next = { ...pkg, name: names.package, version: "0.0.0" };
|
|
@@ -143,9 +145,9 @@ export const cliPackageJson = (text, names) => {
|
|
|
143
145
|
};
|
|
144
146
|
if (Array.isArray(pkg.files))
|
|
145
147
|
next.files = pkg.files.filter((entry) => !isKit(entry) && !Object.values(PACKED_AS).includes(entry));
|
|
146
|
-
if (
|
|
148
|
+
if (isRecord(pkg.scripts)) {
|
|
147
149
|
next.scripts = Object.fromEntries(Object.entries(pkg.scripts)
|
|
148
|
-
.filter((
|
|
150
|
+
.filter((entry) => typeof entry[1] === "string" && !entry[0].startsWith("kit:"))
|
|
149
151
|
.map(([key, value]) => [key, removeKit(value)])
|
|
150
152
|
.filter(([, value]) => value.length > 0));
|
|
151
153
|
}
|
|
@@ -181,12 +183,16 @@ export const cliReadme = (names) => [
|
|
|
181
183
|
"```sh",
|
|
182
184
|
"pnpm install",
|
|
183
185
|
"pnpm build",
|
|
184
|
-
`node dist/bin/${names.command}.js
|
|
186
|
+
`node dist/bin/${names.command}.js doctor`,
|
|
185
187
|
"```",
|
|
186
188
|
"",
|
|
187
|
-
|
|
189
|
+
`\`doctor\` needs no profile. Every command that uses an account starts with one, a name you choose: \`${names.command} personal config show\`.`,
|
|
188
190
|
"",
|
|
189
191
|
].join("\n");
|
|
192
|
+
// The template's own security links point at cli-template, where a report about the template belongs; the placeholder
|
|
193
|
+
// repository does not exist. A CLI's copy names its own repository.
|
|
194
|
+
const TEMPLATE_REPOSITORY = "https://github.com/WireCatLabs/cli-template/";
|
|
195
|
+
const ownRepository = (names) => (text) => text.replaceAll(TEMPLATE_REPOSITORY, `https://github.com/${names.owner}/${names.name}/`);
|
|
190
196
|
export const cliChangelog = (names, source) => [
|
|
191
197
|
"# Changelog",
|
|
192
198
|
"",
|
|
@@ -227,6 +233,8 @@ export const runNew = async (cwd, source, args, io) => {
|
|
|
227
233
|
const rewrites = {
|
|
228
234
|
"package.json": (text) => cliPackageJson(text, names),
|
|
229
235
|
"README.md": () => cliReadme(names),
|
|
236
|
+
"SECURITY.md": ownRepository(names),
|
|
237
|
+
"docs/security.md": ownRepository(names),
|
|
230
238
|
"CHANGELOG.md": () => cliChangelog(names, source),
|
|
231
239
|
[VERSION_FILE]: (text) => text.replace(/VERSION = "[^"]*"/, 'VERSION = "0.0.0"'),
|
|
232
240
|
[IDENTITY_FILE]: (text) => withMessenger(withContentLanguages(text, answers.contentLang ?? []), category),
|
|
@@ -264,7 +272,7 @@ export const runNew = async (cwd, source, args, io) => {
|
|
|
264
272
|
" pnpm ci:quick",
|
|
265
273
|
" git add -A",
|
|
266
274
|
' git commit -m "chore: start from @wirecat/cli-template"',
|
|
267
|
-
` then give the agent docs/dev/DESIGN.md; every
|
|
275
|
+
` then give the agent docs/dev/DESIGN.md; every account command passes a profile, e.g. ${names.command} personal config show`,
|
|
268
276
|
"",
|
|
269
277
|
"Four steps need a person:",
|
|
270
278
|
...manualSteps(names).map((step) => ` ${step}`),
|
package/kit/dist/rename.js
CHANGED
|
@@ -29,6 +29,7 @@ export const stripKit = (path, content) => {
|
|
|
29
29
|
return content;
|
|
30
30
|
const text = content.toString("utf8");
|
|
31
31
|
const stripped = text
|
|
32
|
+
.replace(/^[ \t]*\/\/[^\n]*\n(?=[ \t]*"!?(\.\/)?kit\/)/gm, "")
|
|
32
33
|
.replace(/\[\s*"!?(\.\/)?kit\/[^"]*"\s*\]/g, "[]")
|
|
33
34
|
.replace(/,\s*"!?(\.\/)?kit\/[^"]*"/g, "")
|
|
34
35
|
.replace(/"!?(\.\/)?kit\/[^"]*",\s*/g, "");
|
package/kit/dist/source.js
CHANGED
|
@@ -5,13 +5,13 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { dirname, join, relative, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
-
import { classify, MANIFEST, parseTemplateManifest, readJson, } from "./manifest.js";
|
|
8
|
+
import { classify, isRecord, MANIFEST, parseTemplateManifest, readJson, } from "./manifest.js";
|
|
9
9
|
import { renameContent, renamer, stripKit } from "./rename.js";
|
|
10
10
|
const run = promisify(execFile);
|
|
11
11
|
export const ownRoot = () => fileURLToPath(new URL("../../", import.meta.url));
|
|
12
12
|
export const loadSource = async (root) => {
|
|
13
|
-
const pkg =
|
|
14
|
-
if (typeof pkg.name !== "string" || typeof pkg.version !== "string") {
|
|
13
|
+
const pkg = await readJson(join(root, "package.json"));
|
|
14
|
+
if (!isRecord(pkg) || typeof pkg.name !== "string" || typeof pkg.version !== "string") {
|
|
15
15
|
throw new Error(`${join(root, "package.json")}: needs a name and a version`);
|
|
16
16
|
}
|
|
17
17
|
const manifestPath = join(root, MANIFEST);
|
|
@@ -82,7 +82,10 @@ export const npmCommand = (args) => {
|
|
|
82
82
|
export const fetchPacked = async (spec) => {
|
|
83
83
|
const dir = await mkdtemp(join(tmpdir(), "cli-template-"));
|
|
84
84
|
const { stdout } = await run(...npmCommand(["pack", spec, "--silent", "--pack-destination", dir])).catch((error) => {
|
|
85
|
-
|
|
85
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
86
|
+
throw new Error(`could not download ${spec} from npm, which upgrade needs as the merge base: ${why}`, {
|
|
87
|
+
cause: error,
|
|
88
|
+
});
|
|
86
89
|
});
|
|
87
90
|
const tarball = stdout.trim().split("\n").pop() ?? "";
|
|
88
91
|
const dispose = () => rm(dir, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-template",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"smoke": "node --experimental-strip-types scripts/smoke.ts",
|
|
64
64
|
"smoke:bun": "bun scripts/smoke.ts",
|
|
65
65
|
"package:check": "pnpm smoke",
|
|
66
|
-
"docs:check": "cli-dev docs-check --rules scripts/docs-rules.ts --pages && pnpm docs:spell && rumdl check --config config/wirecat/rumdl.toml README.md SECURITY.md CLAUDE.md AGENTS.md docs/*.md docs/dev/*.md && pnpm docs:lint",
|
|
66
|
+
"docs:check": "cli-dev docs-check --rules scripts/docs-rules.ts --pages && pnpm docs:spell && rumdl check --config config/wirecat/rumdl.toml README.md SECURITY.md CHANGELOG.md CLAUDE.md AGENTS.md docs/*.md docs/dev/*.md && pnpm docs:lint && pnpm kit:docs",
|
|
67
67
|
"docs:lint": "markdownlint-cli2",
|
|
68
68
|
"contracts:check": "cli-contract contracts/plan.json",
|
|
69
69
|
"security:no-leak": "cli-testing security no-leak . contracts/no-leak.json",
|
|
@@ -76,15 +76,16 @@
|
|
|
76
76
|
"release:check": "pnpm standards:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm lint:commands && pnpm audit:commands && pnpm contracts:check && pnpm test:matrix:check && pnpm docs:check && pnpm generate:check && pnpm version:check",
|
|
77
77
|
"prepare": "lefthook install || true",
|
|
78
78
|
"prepack": "pnpm build",
|
|
79
|
-
"docs:spell": "cspell lint --config cspell.json --no-progress --no-summary README.md SECURITY.md 'docs/*.md'",
|
|
79
|
+
"docs:spell": "cspell lint --config cspell.json --no-progress --no-summary README.md SECURITY.md CHANGELOG.md 'docs/*.md'",
|
|
80
80
|
"kit:build": "tsc -p kit && node -e \"require('node:fs').copyFileSync('.gitignore', 'gitignore')\"",
|
|
81
81
|
"kit:typecheck": "tsc -p kit/tsconfig.test.json",
|
|
82
|
-
"kit:e2e": "bash kit/e2e.sh"
|
|
82
|
+
"kit:e2e": "bash kit/e2e.sh",
|
|
83
|
+
"kit:docs": "cspell lint --config cspell.json --no-progress --no-summary kit/README.md && rumdl check --config config/wirecat/rumdl.toml kit/README.md"
|
|
83
84
|
},
|
|
84
85
|
"dependencies": {
|
|
85
|
-
"@modelcontextprotocol/server": "2.
|
|
86
|
-
"@wirecat/cli-base": "0.
|
|
87
|
-
"@wirecat/cli-core": "0.
|
|
86
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
87
|
+
"@wirecat/cli-base": "0.57.0",
|
|
88
|
+
"@wirecat/cli-core": "0.428.0",
|
|
88
89
|
"commander": "15.0.0"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
package/pnpm-workspace.yaml
CHANGED
package/scripts/commands.ts
CHANGED
|
@@ -19,7 +19,7 @@ const page = commandsPage({
|
|
|
19
19
|
intro: `Every command, option and exit code, generated from the program itself. For the same list as JSON, run
|
|
20
20
|
\`${IDENTITY.command} commands --json\`.
|
|
21
21
|
|
|
22
|
-
**The first word is the profile**, a name you choose: \`${IDENTITY.command} personal
|
|
22
|
+
**The first word is the profile**, a name you choose: \`${IDENTITY.command} personal config show\`. \`${IDENTITY.envPrefix}_PROFILE\`
|
|
23
23
|
does the same for a whole shell session. No profile is assumed: a command that uses an account stops without one and
|
|
24
24
|
lists the profiles on this computer.`,
|
|
25
25
|
globalHeading: "Options for every command",
|
package/scripts/docs-rules.ts
CHANGED
|
@@ -6,7 +6,7 @@ export const CHANGELOG = {
|
|
|
6
6
|
ids: JOURNAL_IDS,
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const USER_PAGES = /^(README|SECURITY)\.md$|^docs\/[^/]+\.md$/
|
|
9
|
+
const USER_PAGES = /^(README|SECURITY|CHANGELOG)\.md$|^kit\/README\.md$|^docs\/[^/]+\.md$/
|
|
10
10
|
|
|
11
11
|
export const docsRules = (root: string): DocsRules => ({
|
|
12
12
|
files: markdownFiles(root, new Set(["node_modules", "dist", "coverage", ".git"])),
|
package/scripts/smoke.ts
CHANGED
|
@@ -5,12 +5,20 @@ import { tmpdir } from "node:os"
|
|
|
5
5
|
import { join, resolve } from "node:path"
|
|
6
6
|
import { packContents } from "@wirecat/cli-base/release"
|
|
7
7
|
|
|
8
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
9
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
10
|
+
|
|
11
|
+
const isPackage = (value: unknown): value is { name: string; version: string; files: string[] } =>
|
|
12
|
+
isRecord(value) &&
|
|
13
|
+
typeof value.name === "string" &&
|
|
14
|
+
typeof value.version === "string" &&
|
|
15
|
+
Array.isArray(value.files) &&
|
|
16
|
+
value.files.every((entry) => typeof entry === "string")
|
|
17
|
+
|
|
8
18
|
const root = resolve(import.meta.dirname, "..")
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
files: string[]
|
|
13
|
-
}
|
|
19
|
+
const pkg: unknown = JSON.parse(readFileSync(join(root, "package.json"), "utf8"))
|
|
20
|
+
assert.ok(isPackage(pkg), "package.json needs a name, a version and a files list")
|
|
21
|
+
const { name, version, files } = pkg
|
|
14
22
|
// npm reads `files` like .gitignore: a name with no leading slash also matches the same name in any folder.
|
|
15
23
|
const shipped = files
|
|
16
24
|
.map((entry) => entry.replace(/^\//, ""))
|
|
@@ -52,12 +60,13 @@ try {
|
|
|
52
60
|
execFileSync(process.execPath, [bin, ...args], { cwd: temp, env, encoding: "utf8", timeout: 20000 })
|
|
53
61
|
assert.equal(run(["--version"]).trim(), version)
|
|
54
62
|
assert.match(run(["--help"]), /Usage: clidev/)
|
|
55
|
-
const tree = JSON.parse(run(["commands", "--json"]))
|
|
63
|
+
const tree: unknown = JSON.parse(run(["commands", "--json"]))
|
|
64
|
+
const names =
|
|
65
|
+
isRecord(tree) && Array.isArray(tree.commands)
|
|
66
|
+
? tree.commands.map((command) => isRecord(command) && command.name)
|
|
67
|
+
: []
|
|
56
68
|
for (const root of ["commands", "doctor", "skill", "mcp", "upgrade", "config"])
|
|
57
|
-
assert.ok(
|
|
58
|
-
tree.commands.some((command) => command.name === root),
|
|
59
|
-
`${root} is missing from the installed package`,
|
|
60
|
-
)
|
|
69
|
+
assert.ok(names.includes(root), `${root} is missing from the installed package`)
|
|
61
70
|
assert.equal(JSON.parse(run(["doctor", "--json"])).item.version, version)
|
|
62
71
|
assert.match(run(["skill", "show"]), /name: clidev-cli/)
|
|
63
72
|
console.log(`installed ${name}@${version}: help, version, commands, doctor and skill answer offline`)
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clidev-cli
|
|
3
|
-
description: Use clidev, a WireCat CLI started from cli-template, from an agent — find a command, run it with the owner's profile, read JSON answers. Use when a task needs clidev
|
|
3
|
+
description: Use clidev, a WireCat CLI started from cli-template, from an agent — find a command, run it with the owner's profile, read JSON answers. Use when a task needs clidev. It does not connect to a service yet.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# clidev
|
|
7
7
|
|
|
8
|
-
Every account command starts with the owner's profile: `clidev personal
|
|
8
|
+
Every account command starts with the owner's profile: `clidev personal config show`. Never pick a profile yourself; if none
|
|
9
9
|
is given, run a command without one and read the profiles the error lists.
|
|
10
10
|
|
|
11
11
|
Find your way with the tool itself, not this file:
|
|
12
12
|
|
|
13
13
|
- `clidev commands --json` is the full command tree; `clidev commands config --json` one group.
|
|
14
14
|
- `clidev help config set` is one command's help, with what it returns.
|
|
15
|
-
- `clidev
|
|
15
|
+
- `clidev doctor --json` says what is set up on this computer.
|
|
16
16
|
|
|
17
17
|
## Rules
|
|
18
18
|
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
|
+
import { dirname } from "node:path"
|
|
2
3
|
import { CliError, configFilePath, resolvePaths } from "@wirecat/cli-base"
|
|
3
4
|
import { annotate } from "@wirecat/cli-base/commands"
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
environmentOf,
|
|
7
|
+
itemOf,
|
|
8
|
+
LOCAL_READ,
|
|
9
|
+
outputFor,
|
|
10
|
+
privateFiles,
|
|
11
|
+
profileFree,
|
|
12
|
+
readOnlyLockOf,
|
|
13
|
+
renderAnswer,
|
|
14
|
+
} from "@wirecat/cli-core/cli"
|
|
5
15
|
import { Command } from "commander"
|
|
6
16
|
import { configuration } from "../config.js"
|
|
7
17
|
import { IDENTITY } from "../identity.js"
|
|
@@ -11,7 +21,19 @@ const failure = (error: unknown) => {
|
|
|
11
21
|
throw error
|
|
12
22
|
}
|
|
13
23
|
|
|
14
|
-
|
|
24
|
+
const configFiles = (path: string) => {
|
|
25
|
+
const files = privateFiles({ files: [path], dirs: [dirname(path)] })
|
|
26
|
+
if (!files.checked) return { files, fixes: [] }
|
|
27
|
+
return {
|
|
28
|
+
files: { ...files, problems: files.problems.map(({ path, mode, want }) => ({ path, mode, want })) },
|
|
29
|
+
fixes: files.problems.map((problem) => problem.fix),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* It is run when everything else is broken, so a problem in the config file is a field in the answer, never a throw.
|
|
35
|
+
* `fixes` are the `chmod` commands that make the config private; they go to stderr, not into the answer.
|
|
36
|
+
*/
|
|
15
37
|
export const doctorReport = (env: NodeJS.ProcessEnv) => {
|
|
16
38
|
const paths = resolvePaths({ appName: IDENTITY.appName, prefix: IDENTITY.envPrefix, env })
|
|
17
39
|
const common = {
|
|
@@ -22,31 +44,46 @@ export const doctorReport = (env: NodeJS.ProcessEnv) => {
|
|
|
22
44
|
const readOnly = readOnlyLockOf(IDENTITY, env) ?? null
|
|
23
45
|
try {
|
|
24
46
|
const settings = configuration.resolveSettings({}, { env, readOnly: true, needsProfile: false, withIssues: true })
|
|
47
|
+
const { files, fixes } = configFiles(settings.configPath)
|
|
25
48
|
return {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
report: {
|
|
50
|
+
...common,
|
|
51
|
+
config: { path: settings.configPath, found: settings.configFound, issues: settings.configIssues ?? [] },
|
|
52
|
+
profiles: settings.configuredProfiles,
|
|
53
|
+
read_only: readOnly,
|
|
54
|
+
files,
|
|
55
|
+
},
|
|
56
|
+
fixes,
|
|
30
57
|
}
|
|
31
58
|
} catch (error) {
|
|
32
59
|
const file = configFilePath(paths.config)
|
|
60
|
+
const { files, fixes } = configFiles(file)
|
|
33
61
|
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
report: {
|
|
63
|
+
...common,
|
|
64
|
+
config: { path: file, found: existsSync(file), issues: [] },
|
|
65
|
+
settings: { error: failure(error) },
|
|
66
|
+
profiles: null,
|
|
67
|
+
read_only: readOnly,
|
|
68
|
+
files,
|
|
69
|
+
},
|
|
70
|
+
fixes,
|
|
39
71
|
}
|
|
40
72
|
}
|
|
41
73
|
}
|
|
42
74
|
|
|
43
75
|
export const doctorCommand = (): Command =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
profileFree(
|
|
77
|
+
annotate(new Command("doctor"), {
|
|
78
|
+
effects: LOCAL_READ,
|
|
79
|
+
examples: [`${IDENTITY.command} doctor`, `${IDENTITY.command} doctor --json`],
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
48
82
|
.description("check this installation: version, paths, config, profiles; no keyring or network")
|
|
49
83
|
.action(function (this: Command) {
|
|
50
84
|
const env = environmentOf(this).env ?? process.env
|
|
51
|
-
|
|
85
|
+
const { report, fixes } = doctorReport(env)
|
|
86
|
+
const output = outputFor(this)
|
|
87
|
+
renderAnswer(this, output, itemOf(report))
|
|
88
|
+
for (const fix of fixes) output.renderer.note(`to make it private: ${fix}`)
|
|
52
89
|
})
|
package/src/mcp.ts
CHANGED
|
@@ -61,7 +61,7 @@ export const createMcpServer = (options: McpOptions): McpServer => {
|
|
|
61
61
|
},
|
|
62
62
|
]),
|
|
63
63
|
)
|
|
64
|
-
registerCommands(server, IDENTITY.command, commands)
|
|
64
|
+
registerCommands(server, IDENTITY.command, commands, undefined, undefined, IDENTITY.messenger)
|
|
65
65
|
return server
|
|
66
66
|
}
|
|
67
67
|
|