kiriya 0.1.0 → 0.1.2
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 +33 -0
- package/README.md +38 -13
- package/dist/src/core/presentation/cli/cli-application.js +19 -1
- package/dist/src/core/presentation/cli/help.js +21 -2
- package/dist/src/core/presentation/mcp/tool-definitions.js +4 -2
- package/dist/src/i18n/locales/en.js +1 -0
- package/dist/src/modules/completion/domain/suggest.js +7 -3
- package/dist/src/modules/convert/application/convert-case.use-case.js +1 -1
- package/dist/src/modules/convert/application/convert-time.use-case.js +1 -1
- package/dist/src/modules/files/application/find-files.use-case.js +1 -1
- package/dist/src/modules/files/application/hash-files.use-case.js +1 -1
- package/dist/src/modules/files/application/list-entries.use-case.js +1 -1
- package/dist/src/modules/files/application/rename-paths.use-case.js +2 -2
- package/dist/src/modules/files/application/show-info.use-case.js +1 -1
- package/dist/src/modules/gen/application/generate-tokens.use-case.js +1 -1
- package/dist/src/modules/net/application/lookup-name.use-case.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ output shapes are kiriya's public API.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.2] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- An option that takes one of a list of values now declares that list, so an MCP tool
|
|
15
|
+
offers it as a JSON Schema `enum` instead of naming it only in prose an agent would have
|
|
16
|
+
to read. Eleven options gained one: `files list --sort`, `files find --type`,
|
|
17
|
+
`files hash --algo`, `files info --hash`, `files rename --case` and `--only`,
|
|
18
|
+
`convert case --to`, `convert time --unit`, `gen token --format` and `net dns --type`.
|
|
19
|
+
Help and the generated reference are unchanged, since a declared list names itself.
|
|
20
|
+
- Tab completion reads the same list, rather than parsing the value name it was displayed
|
|
21
|
+
under. `docker logs --tail <n|all>` therefore offers nothing instead of offering `n`,
|
|
22
|
+
which was never a value it took.
|
|
23
|
+
|
|
24
|
+
## [0.1.1] - 2026-09-24
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- A usage error inside a command that was found now points at that command's help, so
|
|
29
|
+
`Missing argument: sources` is followed by `Run kiriya files copy --help to see what it
|
|
30
|
+
takes.` A spelling suggestion, being the more useful hint, still takes precedence.
|
|
31
|
+
- `kiriya help <module>` lists each command's option names under it, so a module's whole
|
|
32
|
+
surface can be read at once instead of one command at a time. The short forms, the
|
|
33
|
+
values and the descriptions stay in the command's own help. The readme now shows the
|
|
34
|
+
three steps help goes through, which nothing pointed out before.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- The readme and the getting started page said kiriya was not on npm and walked people
|
|
39
|
+
through building it from source. Both now install it with `npm install --global kiriya`.
|
|
40
|
+
Since npm keeps the readme it was given at publish time, 0.1.0's page carried the wrong
|
|
41
|
+
instructions until this release replaced them.
|
|
42
|
+
|
|
10
43
|
## [0.1.0] - 2026-09-23
|
|
11
44
|
|
|
12
45
|
The first release.
|
package/README.md
CHANGED
|
@@ -10,36 +10,61 @@ show a plan first, deletions go to the trash, and nothing that cannot be undone
|
|
|
10
10
|
until you type a confirmation. AI agents can use the same commands over MCP, under the same
|
|
11
11
|
rules.
|
|
12
12
|
|
|
13
|
-
> **Status:** in development and not yet published to npm. Every module below is built and
|
|
14
|
-
> tested in CI on Windows, Linux and macOS, with Node.js 22, 24 and the current release.
|
|
15
|
-
|
|
16
13
|
## Install
|
|
17
14
|
|
|
18
|
-
kiriya needs [Node.js](https://nodejs.org/) 22.13 or later.
|
|
19
|
-
|
|
15
|
+
kiriya needs [Node.js](https://nodejs.org/) 22.13 or later.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install --global kiriya
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then check what this machine gives it:
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
|
|
23
|
-
cd kiriya
|
|
24
|
-
npm ci
|
|
25
|
-
npm run build
|
|
26
|
-
npm link
|
|
24
|
+
kiriya doctor
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
[
|
|
27
|
+
Every release is published from CI with [provenance](https://docs.npmjs.com/generating-provenance-statements),
|
|
28
|
+
so `npm audit signatures` can show which workflow built the copy you installed. Every module
|
|
29
|
+
below is tested on Windows, Linux and macOS with Node.js 22, 24 and the current release.
|
|
30
|
+
|
|
31
|
+
[Getting started](docs/getting-started.md) takes it from there, and
|
|
32
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) covers building from source.
|
|
30
33
|
|
|
31
34
|
## Quick start
|
|
32
35
|
|
|
33
36
|
```bash
|
|
34
37
|
kiriya doctor # what this machine offers kiriya
|
|
35
|
-
kiriya --help # every module
|
|
36
|
-
kiriya help port # one module, with examples
|
|
37
38
|
kiriya port who 3000 # which process holds port 3000
|
|
38
39
|
kiriya files find --name "*.log" --older 30d # the same search in every shell
|
|
39
40
|
kiriya files delete dist --dry-run # the plan, before anything changes
|
|
40
41
|
kiriya git status ~/code --json # every repository under a folder, as JSON
|
|
41
42
|
```
|
|
42
43
|
|
|
44
|
+
## Finding your way
|
|
45
|
+
|
|
46
|
+
Help goes three steps deep, and each step names the next, so nothing has to be guessed.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
kiriya --help # the modules, and the options every command accepts
|
|
50
|
+
kiriya help files # one module: its commands, and what each one takes
|
|
51
|
+
kiriya help files delete # one command: its arguments, options and examples
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`kiriya files delete --help` says the same as that third line. The middle step lists each
|
|
55
|
+
command's option names underneath it, so a module's whole surface fits on one screen:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
Commands
|
|
59
|
+
delete Send files and folders to the trash, or remove them for good with --permanent
|
|
60
|
+
--permanent --dry-run --yes --confirm --all
|
|
61
|
+
find Find files and folders by name, extension, type, size, age or emptiness
|
|
62
|
+
--name --ext --type --larger --smaller --newer --older --empty --all --limit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every module also has a guide below with a full reference: every command, argument and
|
|
66
|
+
option, what each does, and whether it can be undone.
|
|
67
|
+
|
|
43
68
|
## Modules
|
|
44
69
|
|
|
45
70
|
<!-- kiriya:modules -->
|
|
@@ -19,6 +19,24 @@ const EXIT_CODES = {
|
|
|
19
19
|
interrupted: 130,
|
|
20
20
|
};
|
|
21
21
|
const verbsOf = (module) => [...module.commands.keys()].filter((verb) => verb !== "");
|
|
22
|
+
/**
|
|
23
|
+
* A usage error raised inside a command that was found says what is wrong with the arguments
|
|
24
|
+
* but not what the command takes, so point at its help. An error that already carries a hint,
|
|
25
|
+
* such as a spelling suggestion, keeps the one it has.
|
|
26
|
+
*/
|
|
27
|
+
function withCommandHint(id, parse) {
|
|
28
|
+
try {
|
|
29
|
+
return parse();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!(error instanceof UsageError) || error.detail.params["hint"] !== undefined)
|
|
33
|
+
throw error;
|
|
34
|
+
throw new UsageError(error.detail.key, {
|
|
35
|
+
...error.detail.params,
|
|
36
|
+
hint: message("core.usage.see-help", { command: `kiriya ${id.split(".").join(" ")} --help` }),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
22
40
|
/** The command line: finds the command, runs it, prints its result, and maps errors to exit codes once. */
|
|
23
41
|
export class CliApplication {
|
|
24
42
|
deps;
|
|
@@ -61,7 +79,7 @@ export class CliApplication {
|
|
|
61
79
|
if (flags.help)
|
|
62
80
|
return this.print(this.deps.stdout, commandHelp(module, entry, help));
|
|
63
81
|
const { spec } = entry.command;
|
|
64
|
-
const input = spec.input.parse(parseCommandArguments(spec.input, args));
|
|
82
|
+
const input = withCommandHint(spec.id, () => spec.input.parse(parseCommandArguments(spec.input, args)));
|
|
65
83
|
const controller = new AbortController();
|
|
66
84
|
const interrupt = () => controller.abort();
|
|
67
85
|
process.once("SIGINT", interrupt);
|
|
@@ -30,6 +30,13 @@ function table(rows, options = {}) {
|
|
|
30
30
|
lines.push(` ${name}${padding} ${wrapped[0] ?? ""}`);
|
|
31
31
|
for (const line of wrapped.slice(1))
|
|
32
32
|
lines.push(`${" ".repeat(start)}${line}`);
|
|
33
|
+
const note = options.note?.(left);
|
|
34
|
+
if (note !== undefined && note !== "") {
|
|
35
|
+
const noteLines = options.width === undefined ? [note] : wrap(note, Math.max(20, options.width - start));
|
|
36
|
+
for (const line of noteLines) {
|
|
37
|
+
lines.push(`${" ".repeat(start)}${options.paintNote === undefined ? line : options.paintNote(line)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
33
40
|
}
|
|
34
41
|
return lines;
|
|
35
42
|
}
|
|
@@ -78,7 +85,8 @@ function wordmark(context) {
|
|
|
78
85
|
}
|
|
79
86
|
/** `-i, --ignore-case` or `--to <file>`, as help and the generated docs show an option. */
|
|
80
87
|
export function optionLabel(name, spec) {
|
|
81
|
-
const
|
|
88
|
+
const named = spec.valueName ?? (spec.choices === undefined ? "<value>" : `<${spec.choices.join("|")}>`);
|
|
89
|
+
const value = spec.type === "string" ? ` ${named}` : "";
|
|
82
90
|
const short = spec.short === undefined ? "" : `-${spec.short}, `;
|
|
83
91
|
return `${short}--${name}${value}`;
|
|
84
92
|
}
|
|
@@ -161,12 +169,23 @@ export function moduleHelp(module, context) {
|
|
|
161
169
|
const commands = [...module.commands.values()]
|
|
162
170
|
.filter((entry) => entry.verb !== "")
|
|
163
171
|
.sort((a, b) => (a.verb < b.verb ? -1 : a.verb > b.verb ? 1 : 0));
|
|
172
|
+
// What each command takes, so its options can be seen here rather than one command at a time.
|
|
173
|
+
// Long forms only: the short ones, the values and the descriptions belong in the command's own help.
|
|
174
|
+
const options = new Map(commands.map((entry) => {
|
|
175
|
+
const names = Object.keys(entry.command.spec.input.options);
|
|
176
|
+
return [entry.verb, names.length === 0 ? undefined : names.map((name) => `--${name}`).join(" ")];
|
|
177
|
+
}));
|
|
164
178
|
return [
|
|
165
179
|
`${style.bold(`kiriya ${module.id}`)} — ${translator.text(message(module.summary))}`,
|
|
166
180
|
...aboutLines(module, context),
|
|
167
181
|
"",
|
|
168
182
|
style.bold(translator.text(message("core.help.commands"))),
|
|
169
|
-
...table(commands.map((entry) => [entry.verb, translator.text(message(entry.command.spec.summary))]), {
|
|
183
|
+
...table(commands.map((entry) => [entry.verb, translator.text(message(entry.command.spec.summary))]), {
|
|
184
|
+
width: context.width,
|
|
185
|
+
paint: (verb) => style.green(verb),
|
|
186
|
+
note: (verb) => options.get(verb),
|
|
187
|
+
paintNote: (line) => style.dim(line),
|
|
188
|
+
}),
|
|
170
189
|
...exampleLines(module.examples, context),
|
|
171
190
|
"",
|
|
172
191
|
...guideLines(module, context),
|
|
@@ -65,13 +65,15 @@ export function inputSchemaOf(commandId, input, translator, access = READING) {
|
|
|
65
65
|
}
|
|
66
66
|
for (const [name, option] of toolOptions(input, access)) {
|
|
67
67
|
const text = translator.text(message(option.description));
|
|
68
|
+
// Choices belong in the enum, where a client can act on them, rather than in prose it must read.
|
|
68
69
|
const description = option.valueName === undefined ? text : `${text} ${option.valueName}`;
|
|
70
|
+
const choices = option.choices === undefined ? {} : { enum: [...option.choices] };
|
|
69
71
|
if (option.type === "boolean")
|
|
70
72
|
claim(name, { type: "boolean", description });
|
|
71
73
|
else if (option.multiple === true)
|
|
72
|
-
claim(name, { type: "array", items: { type: "string" }, description });
|
|
74
|
+
claim(name, { type: "array", items: { type: "string", ...choices }, description });
|
|
73
75
|
else
|
|
74
|
-
claim(name, { type: "string", description });
|
|
76
|
+
claim(name, { type: "string", ...choices, description });
|
|
75
77
|
}
|
|
76
78
|
return { type: "object", properties, ...(required.length > 0 ? { required } : {}), additionalProperties: false };
|
|
77
79
|
}
|
|
@@ -24,6 +24,7 @@ export const en = {
|
|
|
24
24
|
"core.usage.unknown-module": "Unknown module: {name}",
|
|
25
25
|
"core.usage.unknown-command": "Unknown command: {module} {name}",
|
|
26
26
|
"core.usage.did-you-mean": "Did you mean {suggestion}?",
|
|
27
|
+
"core.usage.see-help": "Run {command} to see what it takes.",
|
|
27
28
|
"core.usage.bad-arguments": "{detail}",
|
|
28
29
|
"core.usage.unknown-option": "Unknown option: {option}",
|
|
29
30
|
"core.usage.takes-no-value": "--{option} does not take a value",
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The values an option takes, when it takes only a list of them. It reads the list the
|
|
3
|
+
* command declares, rather than parsing the value name it is displayed under, so an option
|
|
4
|
+
* whose value name merely looks like a list — `--tail <n|all>`, a number or a word — offers
|
|
5
|
+
* nothing instead of offering `n`.
|
|
6
|
+
*/
|
|
2
7
|
export function optionChoices(option) {
|
|
3
|
-
|
|
4
|
-
return match?.[1]?.split("|") ?? [];
|
|
8
|
+
return option.choices === undefined ? [] : [...option.choices];
|
|
5
9
|
}
|
|
6
10
|
function ownCommand(module) {
|
|
7
11
|
return module.commands.find((command) => command.verb === "");
|
|
@@ -11,7 +11,7 @@ export const caseSpec = {
|
|
|
11
11
|
input: {
|
|
12
12
|
positionals: [valuePositional],
|
|
13
13
|
options: {
|
|
14
|
-
to: { type: "string", description: "convert.case.option.to",
|
|
14
|
+
to: { type: "string", description: "convert.case.option.to", choices: TEXT_CASES },
|
|
15
15
|
file: fileOption,
|
|
16
16
|
},
|
|
17
17
|
parse(raw) {
|
|
@@ -16,7 +16,7 @@ export const timeSpec = {
|
|
|
16
16
|
idempotent: false,
|
|
17
17
|
input: {
|
|
18
18
|
positionals: [{ name: "value", description: "convert.time.arg.value", required: false, variadic: false }],
|
|
19
|
-
options: { unit: { type: "string", description: "convert.time.option.unit",
|
|
19
|
+
options: { unit: { type: "string", description: "convert.time.option.unit", choices: EPOCH_UNITS } },
|
|
20
20
|
parse(raw) {
|
|
21
21
|
const reader = new RawReader(raw);
|
|
22
22
|
return { value: reader.positional(0), unit: reader.choice("unit", EPOCH_UNITS, "auto") };
|
|
@@ -26,7 +26,7 @@ export const findSpec = {
|
|
|
26
26
|
options: {
|
|
27
27
|
name: { type: "string", description: "files.find.option.name", valueName: "<glob>" },
|
|
28
28
|
ext: { type: "string", description: "files.find.option.ext", valueName: "<extensions>", multiple: true },
|
|
29
|
-
type: { type: "string", description: "files.find.option.type",
|
|
29
|
+
type: { type: "string", description: "files.find.option.type", choices: FIND_TYPES },
|
|
30
30
|
larger: { type: "string", description: "files.find.option.larger", valueName: "<size>" },
|
|
31
31
|
smaller: { type: "string", description: "files.find.option.smaller", valueName: "<size>" },
|
|
32
32
|
newer: { type: "string", description: "files.find.option.newer", valueName: "<time>" },
|
|
@@ -19,7 +19,7 @@ export const hashSpec = {
|
|
|
19
19
|
input: {
|
|
20
20
|
positionals: [{ name: "paths", description: "files.hash.arg.paths", required: true, variadic: true, path: true }],
|
|
21
21
|
options: {
|
|
22
|
-
algo: { type: "string", description: "files.hash.option.algo",
|
|
22
|
+
algo: { type: "string", description: "files.hash.option.algo", choices: HASH_ALGORITHMS },
|
|
23
23
|
check: { type: "string", description: "files.hash.option.check", valueName: "<hex>" },
|
|
24
24
|
},
|
|
25
25
|
parse(raw) {
|
|
@@ -16,7 +16,7 @@ export const listSpec = {
|
|
|
16
16
|
positionals: [{ name: "path", description: "files.list.arg.path", required: false, variadic: false, path: true }],
|
|
17
17
|
options: {
|
|
18
18
|
all: { type: "boolean", description: "files.option.all" },
|
|
19
|
-
sort: { type: "string", description: "files.list.option.sort",
|
|
19
|
+
sort: { type: "string", description: "files.list.option.sort", choices: LIST_SORTS },
|
|
20
20
|
reverse: { type: "boolean", description: "files.list.option.reverse" },
|
|
21
21
|
},
|
|
22
22
|
parse(raw) {
|
|
@@ -27,11 +27,11 @@ export const renameSpec = {
|
|
|
27
27
|
{ name: "paths", description: "files.rename.arg.paths", required: false, variadic: true, path: true },
|
|
28
28
|
],
|
|
29
29
|
options: {
|
|
30
|
-
case: { type: "string", description: "files.rename.option.case",
|
|
30
|
+
case: { type: "string", description: "files.rename.option.case", choices: CASE_STYLES },
|
|
31
31
|
find: { type: "string", description: "files.rename.option.find", valueName: "<text>" },
|
|
32
32
|
with: { type: "string", description: "files.rename.option.with", valueName: "<text>" },
|
|
33
33
|
recursive: { type: "boolean", description: "files.rename.option.recursive" },
|
|
34
|
-
only: { type: "string", description: "files.rename.option.only",
|
|
34
|
+
only: { type: "string", description: "files.rename.option.only", choices: ONLY },
|
|
35
35
|
apply: { type: "boolean", description: "files.rename.option.apply" },
|
|
36
36
|
yes: { type: "boolean", description: "files.rename.option.yes", short: "y", terminalOnly: true },
|
|
37
37
|
},
|
|
@@ -28,7 +28,7 @@ export const infoSpec = {
|
|
|
28
28
|
input: {
|
|
29
29
|
positionals: [{ name: "paths", description: "files.info.arg.paths", required: true, variadic: true, path: true }],
|
|
30
30
|
options: {
|
|
31
|
-
hash: { type: "string", description: "files.info.option.hash",
|
|
31
|
+
hash: { type: "string", description: "files.info.option.hash", choices: HASH_ALGORITHMS },
|
|
32
32
|
},
|
|
33
33
|
parse(raw) {
|
|
34
34
|
const reader = new RawReader(raw);
|
|
@@ -11,7 +11,7 @@ export const tokenSpec = {
|
|
|
11
11
|
positionals: [],
|
|
12
12
|
options: {
|
|
13
13
|
bytes: { type: "string", description: "gen.token.option.bytes", valueName: "<n>" },
|
|
14
|
-
format: { type: "string", description: "gen.token.option.format",
|
|
14
|
+
format: { type: "string", description: "gen.token.option.format", choices: TOKEN_FORMATS },
|
|
15
15
|
count: countOption,
|
|
16
16
|
},
|
|
17
17
|
parse(raw) {
|
|
@@ -16,7 +16,7 @@ export const dnsSpec = {
|
|
|
16
16
|
input: {
|
|
17
17
|
positionals: [{ name: "name", description: "net.dns.arg.name", required: true, variadic: false }],
|
|
18
18
|
options: {
|
|
19
|
-
type: { type: "string", description: "net.dns.option.type",
|
|
19
|
+
type: { type: "string", description: "net.dns.option.type", choices: LOOKUP_TYPES },
|
|
20
20
|
},
|
|
21
21
|
parse(raw) {
|
|
22
22
|
const reader = new RawReader(raw);
|
package/package.json
CHANGED