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 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. Until its first release, build
19
- it from source:
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
- git clone https://github.com/SatPaingOo/kiriya.git
23
- cd kiriya
24
- npm ci
25
- npm run build
26
- npm link
24
+ kiriya doctor
27
25
  ```
28
26
 
29
- [Getting started](docs/getting-started.md) takes it from there.
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 value = spec.type === "string" ? ` ${spec.valueName ?? "<value>"}` : "";
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))]), { width: context.width, paint: (verb) => style.green(verb) }),
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
- /** The values an option lists in its value name, such as `<hex|base64|base64url>`. */
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
- const match = /^<([a-z0-9-]+(?:\|[a-z0-9-]+)+)>$/.exec(option.valueName ?? "");
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", valueName: `<${TEXT_CASES.join("|")}>` },
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", valueName: "<auto|seconds|ms>" } },
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", valueName: "<file|dir>" },
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", valueName: `<${HASH_ALGORITHMS.join("|")}>` },
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", valueName: "<name|size|time>" },
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", valueName: `<${CASE_STYLES.join("|")}>` },
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", valueName: "<files|dirs>" },
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", valueName: `<${HASH_ALGORITHMS.join("|")}>` },
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", valueName: "<hex|base64|base64url>" },
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", valueName: `<${LOOKUP_TYPES.join("|")}>` },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiriya",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "One command-line toolbox for everyday developer work that behaves the same on Windows, Linux and macOS.",
5
5
  "mcpName": "io.github.SatPaingOo/kiriya",
6
6
  "license": "MIT",