kiriya 0.1.1 → 0.1.3

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.
Files changed (25) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -0
  3. package/dist/src/config/modules.js +2 -0
  4. package/dist/src/core/infrastructure/node/node-network.adapter.js +55 -0
  5. package/dist/src/core/presentation/cli/help.js +2 -1
  6. package/dist/src/core/presentation/mcp/tool-definitions.js +4 -2
  7. package/dist/src/i18n/locales/en.js +19 -0
  8. package/dist/src/main.js +7 -1
  9. package/dist/src/modules/completion/domain/suggest.js +7 -3
  10. package/dist/src/modules/convert/application/convert-case.use-case.js +1 -1
  11. package/dist/src/modules/convert/application/convert-time.use-case.js +1 -1
  12. package/dist/src/modules/doctor/presentation/doctor.view.js +3 -0
  13. package/dist/src/modules/files/application/find-files.use-case.js +1 -1
  14. package/dist/src/modules/files/application/hash-files.use-case.js +1 -1
  15. package/dist/src/modules/files/application/list-entries.use-case.js +1 -1
  16. package/dist/src/modules/files/application/rename-paths.use-case.js +2 -2
  17. package/dist/src/modules/files/application/show-info.use-case.js +1 -1
  18. package/dist/src/modules/gen/application/generate-tokens.use-case.js +1 -1
  19. package/dist/src/modules/net/application/lookup-name.use-case.js +1 -1
  20. package/dist/src/modules/self-update/application/update-self.use-case.js +86 -0
  21. package/dist/src/modules/self-update/domain/install-source.js +17 -0
  22. package/dist/src/modules/self-update/domain/versions.js +52 -0
  23. package/dist/src/modules/self-update/presentation/self-update.view.js +19 -0
  24. package/dist/src/modules/self-update/self-update.module.js +13 -0
  25. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ output shapes are kiriya's public API.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.3] - 2026-10-03
11
+
12
+ ### Added
13
+
14
+ - `self-update` asks npm's registry whether a newer kiriya exists, and installs it with
15
+ `--apply`. Without `--apply` nothing changes, so it is safe to run just to find out.
16
+ It names the exact version the registry answered with rather than `latest`, never offers
17
+ a prerelease, and refuses to run npm over an install npm did not make, such as a source
18
+ checkout or a future Homebrew tap. It is never offered to AI agents: an agent must not
19
+ upgrade, mid-session, the tool it is calling.
20
+ - `doctor` ends with a line naming `self-update`. It still reaches no network itself —
21
+ until now nothing told anyone that a newer version existed at all.
22
+
23
+ ## [0.1.2] - 2026-09-24
24
+
25
+ ### Changed
26
+
27
+ - An option that takes one of a list of values now declares that list, so an MCP tool
28
+ offers it as a JSON Schema `enum` instead of naming it only in prose an agent would have
29
+ to read. Eleven options gained one: `files list --sort`, `files find --type`,
30
+ `files hash --algo`, `files info --hash`, `files rename --case` and `--only`,
31
+ `convert case --to`, `convert time --unit`, `gen token --format` and `net dns --type`.
32
+ Help and the generated reference are unchanged, since a declared list names itself.
33
+ - Tab completion reads the same list, rather than parsing the value name it was displayed
34
+ under. `docker logs --tail <n|all>` therefore offers nothing instead of offering `n`,
35
+ which was never a value it took.
36
+
10
37
  ## [0.1.1] - 2026-09-24
11
38
 
12
39
  ### Changed
package/README.md CHANGED
@@ -87,6 +87,7 @@ option, what each does, and whether it can be undone.
87
87
  | [`open`](docs/modules/open.md) | Open a file or folder in its default application, or a web address in the browser. |
88
88
  | [`port`](docs/modules/port.md) | See which process listens on a TCP port, end it, or find a free port, the same on every OS. |
89
89
  | [`proc`](docs/modules/proc.md) | List, find and end processes, and show them as a tree, the same on every OS. |
90
+ | [`self-update`](docs/modules/self-update.md) | Ask the registry whether a newer kiriya exists, and install it with --apply. |
90
91
  | [`sys`](docs/modules/sys.md) | This machine and the developer tools on it, described the same way on every OS. |
91
92
  | [`wait`](docs/modules/wait.md) | Wait until a port listens, a web address answers or a file appears, the same way on every OS. |
92
93
  <!-- /kiriya:modules -->
@@ -13,6 +13,7 @@ import { netModule } from "../modules/net/net.module.js";
13
13
  import { openModule } from "../modules/open/open.module.js";
14
14
  import { portModule } from "../modules/port/port.module.js";
15
15
  import { procModule } from "../modules/proc/proc.module.js";
16
+ import { selfUpdateModule } from "../modules/self-update/self-update.module.js";
16
17
  import { sysModule } from "../modules/sys/sys.module.js";
17
18
  import { waitModule } from "../modules/wait/wait.module.js";
18
19
  /** Built-in modules, in no particular order; help lists them sorted. Adding a module is one line here. */
@@ -34,5 +35,6 @@ export const BUILT_IN_MODULES = [
34
35
  openModule,
35
36
  completionModule,
36
37
  waitModule,
38
+ selfUpdateModule,
37
39
  ];
38
40
  //# sourceMappingURL=modules.js.map
@@ -141,6 +141,61 @@ export class NodeNetworkAdapter {
141
141
  outgoing.end();
142
142
  });
143
143
  }
144
+ fetchText(url, timeoutMs, maxBytes, signal) {
145
+ if (signal.aborted)
146
+ return Promise.reject(new InterruptedError("core.error.interrupted"));
147
+ return new Promise((resolve, reject) => {
148
+ const started = performance.now();
149
+ const elapsed = () => Math.round(performance.now() - started);
150
+ const target = new URL(url);
151
+ const send = target.protocol === "https:" ? httpsRequest : httpRequest;
152
+ const outgoing = send(target, {
153
+ method: "GET",
154
+ headers: { "user-agent": "kiriya", accept: "application/json" },
155
+ });
156
+ let settled = false;
157
+ const settle = (outcome) => {
158
+ if (settled)
159
+ return;
160
+ settled = true;
161
+ clearTimeout(timer);
162
+ signal.removeEventListener("abort", onAbort);
163
+ outgoing.destroy();
164
+ if (outcome === null)
165
+ reject(new InterruptedError("core.error.interrupted"));
166
+ else
167
+ resolve(outcome);
168
+ };
169
+ const onAbort = () => settle(null);
170
+ const timer = setTimeout(() => settle({ ok: false, failure: "timeout", status: null, code: null, ms: elapsed() }), timeoutMs);
171
+ signal.addEventListener("abort", onAbort, { once: true });
172
+ outgoing.once("response", (response) => {
173
+ const status = response.statusCode ?? 0;
174
+ const chunks = [];
175
+ let size = 0;
176
+ response.on("data", (chunk) => {
177
+ size += chunk.length;
178
+ // Stop at the limit instead of holding whatever the other end decides to send.
179
+ if (size > maxBytes) {
180
+ response.destroy();
181
+ settle({ ok: false, failure: "too-large", status, code: null, ms: elapsed() });
182
+ return;
183
+ }
184
+ chunks.push(chunk);
185
+ });
186
+ response.once("end", () => settle({ ok: true, status, body: Buffer.concat(chunks).toString("utf8"), ms: elapsed() }));
187
+ response.once("error", (error) => {
188
+ const code = errorCode(error);
189
+ settle({ ok: false, failure: httpFailure(code), status, code, ms: elapsed() });
190
+ });
191
+ });
192
+ outgoing.on("error", (error) => {
193
+ const code = errorCode(error);
194
+ settle({ ok: false, failure: httpFailure(code), status: null, code, ms: elapsed() });
195
+ });
196
+ outgoing.end();
197
+ });
198
+ }
144
199
  async lookup(name) {
145
200
  try {
146
201
  const found = await systemLookup(name, { all: true });
@@ -85,7 +85,8 @@ function wordmark(context) {
85
85
  }
86
86
  /** `-i, --ignore-case` or `--to <file>`, as help and the generated docs show an option. */
87
87
  export function optionLabel(name, spec) {
88
- 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}` : "";
89
90
  const short = spec.short === undefined ? "" : `-${spec.short}, `;
90
91
  return `${short}--${name}${value}`;
91
92
  }
@@ -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
  }
@@ -390,6 +390,7 @@ export const en = {
390
390
  "doctor.status.warn": "warn",
391
391
  "doctor.status.fail": "fail",
392
392
  "doctor.check.kiriya": "{version}",
393
+ "doctor.newer": "doctor asks no network. Run kiriya self-update to see whether a newer version is out.",
393
394
  "doctor.check.node": "{version}",
394
395
  "doctor.check.node-old": "Node.js {version} is older than {minimum}, the oldest release kiriya supports",
395
396
  "doctor.check.os": "{os}",
@@ -666,6 +667,24 @@ export const en = {
666
667
  "wait.file.gone": "{path} is gone, after {seconds} s",
667
668
  "wait.file.timed-out": "{path} did not appear within {seconds} s",
668
669
  "wait.file.still-there": "{path} was still there after {seconds} s",
670
+ "self-update.summary": "Ask the registry whether a newer kiriya exists, and install it with --apply",
671
+ "self-update.about": "Asks npm's registry which version it serves as latest and compares it with this one. " +
672
+ "Without --apply it changes nothing, so it is safe to run just to find out. With --apply " +
673
+ "it installs the newer version with npm, and refuses when npm was not what installed " +
674
+ "kiriya in the first place. This is the only command that reaches the network for a " +
675
+ "reason other than the one you asked for, which is why it has to be run on purpose: " +
676
+ "nothing in kiriya checks for updates by itself. A prerelease is never offered.",
677
+ "self-update.option.apply": "Install the newer version; without it nothing changes",
678
+ "self-update.current": "kiriya {version} is the newest version",
679
+ "self-update.available": "kiriya {latest} is out; this is {installed}",
680
+ "self-update.how-apply": "Run kiriya self-update --apply to install it.",
681
+ "self-update.how-elsewhere": "npm did not install this kiriya, so update it the way you installed it, to {latest}.",
682
+ "self-update.applied": "Updated to kiriya {version}",
683
+ "self-update.unreachable": "The registry could not be reached ({failure}), so the newest version is unknown",
684
+ "self-update.unreadable": "The registry answered {status}, which does not say which version is latest",
685
+ "self-update.not-npm": "kiriya {latest} is out, but npm did not install this one, so npm will not replace it",
686
+ "self-update.no-npm": "npm is not on PATH, so the newer version cannot be installed",
687
+ "self-update.failed": "npm could not install the newer version (exit code {code}): {detail}",
669
688
  "core.processes.failed": "{program} could not list processes (exit code {code}): {detail}",
670
689
  "core.processes.unreadable": "Processes could not be read from {source}: {detail}",
671
690
  "core.ports.failed": "{program} could not list listening ports (exit code {code}): {detail}",
package/dist/src/main.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { readFileSync } from "node:fs";
7
7
  import { dirname } from "node:path";
8
+ import { fileURLToPath } from "node:url";
8
9
  import { BUILT_IN_MODULES } from "./config/modules.js";
9
10
  import { CommandRegistry } from "./core/application/command-registry.js";
10
11
  import { pluginEntries } from "./core/application/config-values.js";
@@ -124,7 +125,12 @@ const ports = {
124
125
  protectedPaths: new PathGuard(environment),
125
126
  config,
126
127
  plugins,
127
- runtime: { kiriyaVersion: version, nodeVersion: process.version },
128
+ runtime: {
129
+ kiriyaVersion: version,
130
+ nodeVersion: process.version,
131
+ // dist/src/main.js, so two levels up is the folder holding package.json.
132
+ installDirectory: fileURLToPath(new URL("../../", import.meta.url)),
133
+ },
128
134
  };
129
135
  for (const module of BUILT_IN_MODULES)
130
136
  registry.register(module, ports);
@@ -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") };
@@ -16,6 +16,9 @@ export const doctorView = (output, format) => {
16
16
  const status = format.text(message(STATUS_KEYS[check.status])).padEnd(statusWidth);
17
17
  return ` ${paint(check.status, status, format)} ${check.name.padEnd(nameWidth)} ${format.text(check.detail)}`;
18
18
  });
19
+ // doctor reaches no network, so it cannot know whether this version is behind. It says where
20
+ // to find out instead, because nothing else told anyone that a newer kiriya exists.
21
+ lines.push("", format.dim(format.text(message("doctor.newer"))));
19
22
  if (output.plugins.length > 0) {
20
23
  lines.push("", format.bold(format.text(message("doctor.plugins"))));
21
24
  const idWidth = Math.max(...output.plugins.map((plugin) => plugin.id.length));
@@ -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);
@@ -0,0 +1,86 @@
1
+ import { done } from "../../../core/domain/command.js";
2
+ import { CapabilityUnavailableError } from "../../../core/domain/errors.js";
3
+ import { RawReader } from "../../../core/domain/input-schema.js";
4
+ import { message } from "../../../core/domain/message.js";
5
+ import { installSource } from "../domain/install-source.js";
6
+ import { compareVersions, parseVersion, readLatestVersion } from "../domain/versions.js";
7
+ /** The registry entry for the version npm serves as `latest`. */
8
+ export const LATEST_URL = "https://registry.npmjs.org/kiriya/latest";
9
+ /** Long enough for a slow network, short enough that nobody waits on a broken one. */
10
+ export const ASK_TIMEOUT_MS = 10_000;
11
+ /** That endpoint answers a few kilobytes; a body past this is something else. */
12
+ export const MAX_BODY_BYTES = 262_144;
13
+ /** An npm install over a slow network outlasts the runner's default minute. */
14
+ export const INSTALL_TIMEOUT_MS = 300_000;
15
+ export const selfUpdateSpec = {
16
+ id: "self-update",
17
+ summary: "self-update.summary",
18
+ examples: ["kiriya self-update", "kiriya self-update --apply", "kiriya self-update --json"],
19
+ // It replaces an installed program, which a reinstall puts back.
20
+ safety: "write",
21
+ idempotent: true,
22
+ usesNetwork: true,
23
+ runsUserCommands: false,
24
+ // An agent must never upgrade, mid-session, the tool it is calling.
25
+ terminalOnly: true,
26
+ input: {
27
+ positionals: [],
28
+ options: {
29
+ apply: { type: "boolean", description: "self-update.option.apply" },
30
+ },
31
+ parse(raw) {
32
+ const reader = new RawReader(raw);
33
+ return { apply: reader.flag("apply") };
34
+ },
35
+ },
36
+ };
37
+ export class UpdateSelf {
38
+ network;
39
+ processRunner;
40
+ runtime;
41
+ spec = selfUpdateSpec;
42
+ constructor(network, processRunner, runtime) {
43
+ this.network = network;
44
+ this.processRunner = processRunner;
45
+ this.runtime = runtime;
46
+ }
47
+ async execute(input, context) {
48
+ const installed = this.runtime.kiriyaVersion;
49
+ const source = installSource(this.runtime.installDirectory);
50
+ const outcome = await this.network.fetchText(LATEST_URL, ASK_TIMEOUT_MS, MAX_BODY_BYTES, context.signal);
51
+ if (!outcome.ok) {
52
+ const data = { installed, latest: null, newer: false, source, applied: false };
53
+ return done(data, { failures: [message("self-update.unreachable", { failure: outcome.failure })] });
54
+ }
55
+ const latest = outcome.status === 200 ? readLatestVersion(outcome.body) : null;
56
+ if (latest === null) {
57
+ const data = { installed, latest: null, newer: false, source, applied: false };
58
+ return done(data, { failures: [message("self-update.unreadable", { status: outcome.status })] });
59
+ }
60
+ const here = parseVersion(installed);
61
+ const there = parseVersion(latest);
62
+ const newer = here !== null && there !== null && compareVersions(there, here) > 0;
63
+ const base = { installed, latest, newer, source };
64
+ if (!newer || !input.apply)
65
+ return done({ ...base, applied: false });
66
+ // Only npm put kiriya here, so only npm is asked to replace it.
67
+ if (source !== "npm") {
68
+ return done({ ...base, applied: false }, { failures: [message("self-update.not-npm", { latest })] });
69
+ }
70
+ const npm = await this.processRunner.find("npm");
71
+ if (npm === null)
72
+ throw new CapabilityUnavailableError("self-update.no-npm");
73
+ const result = await this.processRunner.run(npm, ["install", "--global", `kiriya@${latest}`], {
74
+ signal: context.signal,
75
+ // An install over a slow network outlasts the default minute.
76
+ timeoutMs: INSTALL_TIMEOUT_MS,
77
+ // Shown as it arrives, because this is the one command that can take a while.
78
+ onOutput: (text, stream) => context.passthrough.write(text, stream),
79
+ });
80
+ if (result.code !== 0) {
81
+ return done({ ...base, applied: false }, { failures: [message("self-update.failed", { code: result.code, detail: result.stderr.trim() })] });
82
+ }
83
+ return done({ ...base, applied: true });
84
+ }
85
+ }
86
+ //# sourceMappingURL=update-self.use-case.js.map
@@ -0,0 +1,17 @@
1
+ /** How this kiriya got onto the machine, which decides whether it may run a package manager. */
2
+ export const INSTALL_SOURCES = ["npm", "elsewhere"];
3
+ /**
4
+ * `npm` when kiriya's own files sit inside a `node_modules/kiriya` folder, which is where npm
5
+ * puts a package and nowhere else puts anything. A source checkout linked with `npm link`
6
+ * resolves to the repository instead, and a future Homebrew tap or single file would too, so
7
+ * both read as `elsewhere` and self-update refuses to run npm over them.
8
+ */
9
+ export function installSource(installDirectory) {
10
+ const parts = installDirectory.replace(/\\/g, "/").split("/").filter(Boolean);
11
+ for (let index = 0; index + 1 < parts.length; index += 1) {
12
+ if (parts[index] === "node_modules" && parts[index + 1] === "kiriya")
13
+ return "npm";
14
+ }
15
+ return "elsewhere";
16
+ }
17
+ //# sourceMappingURL=install-source.js.map
@@ -0,0 +1,52 @@
1
+ /** Comparing two released versions, and reading the newest one out of what a registry answers. */
2
+ const PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
3
+ export function parseVersion(text) {
4
+ const match = PATTERN.exec(text.trim());
5
+ if (match === null)
6
+ return null;
7
+ const [, major, minor, patch, prerelease] = match;
8
+ return {
9
+ major: Number(major),
10
+ minor: Number(minor),
11
+ patch: Number(patch),
12
+ prerelease: prerelease ?? null,
13
+ };
14
+ }
15
+ /**
16
+ * Negative when `a` is older, positive when newer, zero when the same. A prerelease is older
17
+ * than the release it leads to, which is all kiriya needs: it never offers one as an upgrade.
18
+ */
19
+ export function compareVersions(a, b) {
20
+ for (const part of ["major", "minor", "patch"]) {
21
+ if (a[part] !== b[part])
22
+ return a[part] - b[part];
23
+ }
24
+ if (a.prerelease === b.prerelease)
25
+ return 0;
26
+ if (a.prerelease === null)
27
+ return 1;
28
+ if (b.prerelease === null)
29
+ return -1;
30
+ return a.prerelease < b.prerelease ? -1 : 1;
31
+ }
32
+ /**
33
+ * The version the registry calls `latest`, from the body of
34
+ * `https://registry.npmjs.org/<name>/latest`. null when the body is not what that endpoint
35
+ * answers, so a proxy returning a login page is a failure rather than a version.
36
+ */
37
+ export function readLatestVersion(body) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(body);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ if (typeof parsed !== "object" || parsed === null)
46
+ return null;
47
+ const version = parsed.version;
48
+ if (typeof version !== "string")
49
+ return null;
50
+ return parseVersion(version) === null ? null : version;
51
+ }
52
+ //# sourceMappingURL=versions.js.map
@@ -0,0 +1,19 @@
1
+ import { message } from "../../../core/domain/message.js";
2
+ /** A version that could not be read arrives as a failure, so this prints nothing for it. */
3
+ export const selfUpdateView = (output, format) => {
4
+ if (output.latest === null)
5
+ return [];
6
+ if (output.applied) {
7
+ return [format.green(format.text(message("self-update.applied", { version: output.latest })))];
8
+ }
9
+ if (!output.newer) {
10
+ return [format.text(message("self-update.current", { version: output.installed }))];
11
+ }
12
+ const found = format.text(message("self-update.available", { installed: output.installed, latest: output.latest }));
13
+ // Say what to type next, because finding out is the whole point of running this.
14
+ const next = output.source === "npm"
15
+ ? message("self-update.how-apply")
16
+ : message("self-update.how-elsewhere", { latest: output.latest });
17
+ return [format.green(found), format.dim(format.text(next))];
18
+ };
19
+ //# sourceMappingURL=self-update.view.js.map
@@ -0,0 +1,13 @@
1
+ import { UpdateSelf } from "./application/update-self.use-case.js";
2
+ import { selfUpdateView } from "./presentation/self-update.view.js";
3
+ export const selfUpdateModule = {
4
+ id: "self-update",
5
+ summary: "self-update.summary",
6
+ about: "self-update.about",
7
+ // A module that is one command shows that command's examples, as doctor does.
8
+ guide: "https://github.com/SatPaingOo/kiriya/blob/main/docs/modules/self-update.md",
9
+ register(registrar, ports) {
10
+ registrar.add(new UpdateSelf(ports.network, ports.processRunner, ports.runtime), selfUpdateView);
11
+ },
12
+ };
13
+ //# sourceMappingURL=self-update.module.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiriya",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",