@osdlc/command-seam 0.0.0-stage → 0.0.1-stage

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 (54) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/CONTRIBUTING.md +58 -0
  3. package/LICENSE +21 -0
  4. package/README.md +741 -2
  5. package/SECURITY.md +23 -0
  6. package/async.ts +466 -0
  7. package/defaults.ts +83 -0
  8. package/dist/async.d.ts +146 -0
  9. package/dist/async.d.ts.map +1 -0
  10. package/dist/async.js +368 -0
  11. package/dist/defaults.d.ts +52 -0
  12. package/dist/defaults.d.ts.map +1 -0
  13. package/dist/defaults.js +51 -0
  14. package/dist/groups.d.ts +155 -0
  15. package/dist/groups.d.ts.map +1 -0
  16. package/dist/groups.js +331 -0
  17. package/dist/index.d.ts +30 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +29 -0
  20. package/dist/process.d.ts +146 -0
  21. package/dist/process.d.ts.map +1 -0
  22. package/dist/process.js +175 -0
  23. package/dist/process.test.d.ts +1 -0
  24. package/dist/process.test.d.ts.map +1 -0
  25. package/dist/process.test.js +1 -0
  26. package/dist/quote.d.ts +18 -0
  27. package/dist/quote.d.ts.map +1 -0
  28. package/dist/quote.js +28 -0
  29. package/dist/result.d.ts +211 -0
  30. package/dist/result.d.ts.map +1 -0
  31. package/dist/result.js +219 -0
  32. package/dist/shell.d.ts +84 -0
  33. package/dist/shell.d.ts.map +1 -0
  34. package/dist/shell.js +83 -0
  35. package/dist/stream.d.ts +90 -0
  36. package/dist/stream.d.ts.map +1 -0
  37. package/dist/stream.js +73 -0
  38. package/dist/sync.d.ts +311 -0
  39. package/dist/sync.d.ts.map +1 -0
  40. package/dist/sync.js +406 -0
  41. package/dist/testing.d.ts +158 -0
  42. package/dist/testing.d.ts.map +1 -0
  43. package/dist/testing.js +173 -0
  44. package/groups.ts +360 -0
  45. package/index.ts +98 -0
  46. package/package.json +94 -3
  47. package/process.ts +270 -0
  48. package/quote.ts +31 -0
  49. package/result.ts +403 -0
  50. package/shell.ts +131 -0
  51. package/smoke.mjs +61 -0
  52. package/stream.ts +134 -0
  53. package/sync.ts +604 -0
  54. package/testing.ts +263 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,328 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.4.0] - 2026-10-16
9
+
10
+ The package is renamed, and the repository moved from `sksizer/sdlc` to
11
+ [osdlc/osdlc](https://github.com/osdlc/osdlc).
12
+
13
+ ### Breaking
14
+
15
+ - The package is now `@osdlc/command-seam`, renamed from
16
+ `@sksizer/command-seam`. To move, replace `@sksizer/command-seam` with
17
+ `@osdlc/command-seam` in `package.json` and in every import, including
18
+ `@osdlc/command-seam/testing`. `@sksizer/command-seam` gets no further
19
+ releases.
20
+
21
+ ### Added
22
+
23
+ These exports are experimental (see the README's Stability section).
24
+
25
+ - `installGroupSignalForwarding(opts?)`, with options type
26
+ `GroupSignalForwardingOptions`: opt-in. While a group started with
27
+ `processGroup: true` (async and managed runners) is live, `SIGINT`,
28
+ `SIGTERM` and `SIGHUP` send `SIGTERM` to every group, `SIGKILL` after
29
+ `graceMs` (default 1000), then exit with `128 + signal number`. A repeat
30
+ signal escalates to `SIGKILL` at once; an `exit` handler `SIGKILL`s groups
31
+ whose run is still in flight. Listeners are attached only while a group is
32
+ starting or live, ahead of the host's own. A host with its own listener, `process.once`
33
+ included, keeps the exit; its groups still get `SIGKILL` after `graceMs`, or
34
+ on exit if sooner and the run is in flight. Returns an uninstall function.
35
+ POSIX only; a no-op on Windows. `SIGKILL` of the host still orphans its groups. When
36
+ forwarding owns the exit, a new `processGroup: true` run after the first
37
+ forwarded signal is refused: it starts nothing and settles with
38
+ `reason: 'abort'`. The uninstall function resets this.
39
+ - `forwardedSignal()`: the signal forwarded when forwarding owns the exit, or
40
+ `undefined`. A host that runs one command after another checks it to stop.
41
+ - `forwardedExitCode()`: `128 + signal number` for `forwardedSignal()`, or
42
+ `undefined`. A host that sets its own exit code keeps this one over it.
43
+ - `liveProcessGroups()`: the pids of the groups currently registered. A group
44
+ stays registered until it is empty, also after its run settles or its
45
+ leader exits, so a forwarded signal still kills a background member or one
46
+ waiting for the `SIGKILL` follow-up. The host's normal exit kills only the
47
+ groups whose run is still in flight, so a daemon a settled run left behind
48
+ keeps running.
49
+ - `unref` on the managed-process runner's options: the parent can exit while
50
+ the (usually `processGroup`) child keeps running. Such a group is not tracked
51
+ by `installGroupSignalForwarding`: a signal to the parent does not reach it
52
+ and the parent's exit does not `SIGKILL` it.
53
+
54
+ ## [0.3.0] - 2026-10-15
55
+
56
+ The first release on npm. Version numbers before 0.3.0 were used only during
57
+ development and were never published, so this entry lists everything the
58
+ package does.
59
+
60
+ ### Added
61
+
62
+ #### Runners
63
+
64
+ Each runner factory takes a binary, as a name looked up on `PATH` or a path,
65
+ and returns an object with one `run` method. Code takes the runner as a
66
+ parameter, so a test can pass a fake.
67
+
68
+ - `spawnRunner(binary)`: the sync capture runner (`CommandRunner`). `run`
69
+ returns a `CommandResult` with `exitCode`, `stdout` and `stderr`. When the
70
+ child never reported an exit status, the result also has
71
+ `neverExited: true` and a `reason`, plus `signal` when the runner knows it.
72
+ `error` holds the runner's own message (see Results and output). A death by
73
+ a signal from elsewhere has none. The options (`CommandRunOptions`) are
74
+ `cwd` (required), `timeoutMs`, `input` and `env`.
75
+ - `spawnRunnerAsync(binary)`: the async capture runner
76
+ (`AsyncCommandRunner`). Its options (`AsyncCommandRunOptions`) add
77
+ `signal` (an `AbortSignal`), `onSpawn` (called with the child's pid as soon
78
+ as it starts) and `processGroup`.
79
+ - `streamRunner(binary)`: runs with inherited stdio, so the child's output
80
+ and prompts reach the terminal live (`StreamRunner`, `StreamRunOptions`,
81
+ `StreamResult`). The result says only how the child ended.
82
+ - `managedProcessRunner(binary)`: starts a long-running process and returns a
83
+ handle at once (`ManagedProcessRunner`, `ManagedProcessRunOptions`,
84
+ `ManagedProcessHandle`, `ManagedProcessExit`). The handle has `pid`,
85
+ `kill()` and an `exited` promise. `processGroup: true` makes `kill()`
86
+ signal the whole process group.
87
+ - `NeverExitedReason`: why a child never reported an exit status. `spawn`:
88
+ it never started. `timeout`: `timeoutMs` ran out. `abort`: the caller
89
+ stopped it (an `AbortSignal`, an `onSpawn` that threw, or `kill()` on a
90
+ managed process). `signal`: a signal from elsewhere ended it. `output`:
91
+ the sync runner killed it for passing its 256 MB output cap.
92
+ - `EnvOverrides`: variables merged over `process.env`. An `undefined` value
93
+ removes that variable from the child's environment.
94
+ - `asAsync(runner)`: wraps a sync runner as an async one, so one test fake
95
+ serves both.
96
+ - `resolveRunner(binary, injected?)` and `resolveRunnerAsync`: the injected
97
+ runner when given, else the default runner for `binary`.
98
+ - `withDefaults(runner, opts)`, with `DefaultedRunner`: a runner that fills
99
+ in default options on every call. It works with every runner type. A
100
+ per-call option replaces the default of the same name, and a per-call
101
+ `undefined` keeps the default. The exception is `env`: a per-call `env` is
102
+ merged over the bound `env`, key by key, and a per-call `undefined` value
103
+ removes a bound variable.
104
+ - `stopProcess(handle, { graceMs })`: stops a managed process with
105
+ `SIGTERM`, then `SIGKILL` if it is still running after `graceMs` (default
106
+ 5 s). Resolves with the exit. Works with any `ManagedProcessHandle`.
107
+
108
+ #### Shell helpers
109
+
110
+ - `runShell(verb, opts)` and `runShellAsync`: run a shell string once as
111
+ `sh -c -- <verb>` and capture the result. `scriptName` sets `$0` and
112
+ `args` sets `$1` onward. They are passed as separate arguments, so they
113
+ need no quoting (`ShellPositionals`, `ShellRunOptions`,
114
+ `AsyncShellRunOptions`).
115
+ - `shellRunner(sh?)` and `shellRunnerAsync`: shell runners over a given
116
+ runner, for injection (`ShellRunner`, `AsyncShellRunner`). The injected
117
+ runner receives `['-c', '--', verb]`, then `scriptName` and `args` when
118
+ given.
119
+ - `shellQuote(word)` and `shellJoin(argv)`: POSIX quoting for a command line
120
+ printed for a person to paste. `shellJoin` quotes a first word that
121
+ contains `=`, so it runs as a command, not as an assignment.
122
+
123
+ #### JSON helpers
124
+
125
+ - `runJson(runner, argv, opts)` and `runJsonAsync`: run a command and parse
126
+ its stdout as JSON.
127
+ - `parseJsonResult(result, opts?)`: parse the stdout of a finished run. It
128
+ takes the same `okExitCodes`.
129
+ - `JsonRun`: the outcome. It is `{ ok: true, value, result }`, or
130
+ `ok: false` with a `reason`: `failed` (the command did not run or exited
131
+ non-zero), `unparseable` (stdout was not JSON) or `invalid` (the `parse`
132
+ option threw, and `cause` holds what it threw).
133
+ - `RunJsonOptions`: `tolerateLeadingLog` skips log lines and banners before
134
+ the JSON. The JSON starts at the first line that begins with `{` or `[`
135
+ and parses to the end of stdout. `okExitCodes` lists the exit codes that
136
+ count as an answer, such as `[0, 1]` for `npm outdated --json`. Default
137
+ `[0]`. A run that never exited is still `failed`, and the output must
138
+ still parse.
139
+ - `runJson` and `runJsonAsync` take an explicit type with a runner that needs
140
+ no options, such as one with `cwd` bound: `runJson<T>(bound, argv)`
141
+ compiles.
142
+ - The `parse` option takes a function that checks the parsed value and
143
+ returns it typed, such as a schema's `parse`. The value's type is what
144
+ `parse` returns. `parse` must be sync: one that returns a promise gives
145
+ `invalid`.
146
+
147
+ #### Failure helpers
148
+
149
+ - `runChecked(runner, argv, opts)` and `runCheckedAsync`: run a command and
150
+ throw `CommandFailed` when `exitCode` is not 0. That includes a missing
151
+ binary and a timeout. They take any runner whose `run` returns a
152
+ `CommandResult`, such as a `withDefaults` runner.
153
+ - `CommandFailed`: an error with `argv`, the full `result` and `detail`.
154
+ `detail` is both output streams, trimmed and newline-joined, for showing a
155
+ person why the command failed. `message` quotes argv with `shellJoin`, so
156
+ `['a b']` and `['a', 'b']` look different. It carries the full argv, so
157
+ pass secrets through `env` or `input`, not argv.
158
+ - `mapFailed` and `mapFailedAsync`: rethrow a `CommandFailed` as another
159
+ error. `catchFailed` and `catchFailedAsync`: turn a `CommandFailed` into a
160
+ value. Other errors pass through. The async mapper and handler may return
161
+ a promise.
162
+ - `ran(result)`: `true` when the child started and exited 0. It reads the
163
+ result of any runner.
164
+ - `failureText(result, opts?)`, with `FailureTextOptions`: the text to show
165
+ a person for a failed run. By default it is trimmed stderr, else the
166
+ trimmed `error`, else trimmed stdout, else `exit N`. `{ both: true }` joins both streams, as
167
+ `CommandFailed.detail` does.
168
+ - `firstLine(text, fallback?)`: the first non-blank line, trimmed.
169
+ - `stderrOrError(result)`: trimmed stderr, else the runner's trimmed
170
+ `error`, else `''`. `failureText` and `CommandFailed` use it.
171
+
172
+ #### Test fakes
173
+
174
+ A second entry point, `@sksizer/command-seam/testing`, holds the fakes. It
175
+ has the same `bun`, `types` and `default` export conditions as the main
176
+ entry.
177
+
178
+ - `fakeRunner(answers)`: a `CommandRunner` that answers from a table, or from
179
+ a function of the call, and records each call. The table is keyed by each
180
+ argument quoted with `shellQuote`, joined with spaces, so `['-m', 'a b']`
181
+ and `['-m', 'a', 'b']` do not share a key. A key whose arguments hold a
182
+ space or a shell character must be quoted, such as
183
+ `"tag -m 'Release 1.2.0'"`. A call with no answer reports a failed spawn,
184
+ so `ran()` is false and `runChecked` throws. The "no answer for" message
185
+ prints the same key. It also returns `asyncRunner`, the same fake as an
186
+ `AsyncCommandRunner`.
187
+ - `fakeProcess(opts?)`: a `ManagedProcessHandle` that starts nothing. The
188
+ test ends it with `exit()`, and `kill()` ends it as an abort, unless
189
+ `exitOnKill: false`. `exitOnKill` also takes a list of signals, such as
190
+ `['SIGKILL']`, for a process that survives `SIGTERM`. It records the
191
+ signals sent and comes with a runner that returns it.
192
+ - `fakeNeverExited(reason, fields?)`: builds a fake answer for a run that
193
+ never exited, with the fields the runners set together for that reason.
194
+ - The types `FakeRunner`, `FakeCall`, `FakeProcess` and
195
+ `FakeProcessOptions`, so a test helper can name what the fakes return.
196
+
197
+ ### Behaviour
198
+
199
+ #### Timeouts
200
+
201
+ - `timeoutMs` follows one rule on every runner that has it: the sync, async,
202
+ stream and shell runners. `undefined` and `Infinity` mean no cap. A
203
+ fraction rounds up, so `1.2` is `2`. A value above 2^31 - 1 ms (about 24.8
204
+ days) is clamped to it. `0`, a negative value and `NaN` are a caller error
205
+ (`RangeError`). A value that is not a number is a `TypeError`.
206
+ - A timeout kills the child with `SIGKILL`, so a child that ignores
207
+ `SIGTERM` still stops. The child gets no chance to clean up, so a timed-out
208
+ git can leave its lock files. For a graceful stop on the async runners,
209
+ pass `signal: AbortSignal.timeout(ms)` instead: an abort sends `SIGTERM`
210
+ first.
211
+ - The result of a timeout has `neverExited: true`, `reason: 'timeout'` and
212
+ the output captured so far. On the async runner, a deadline that fires
213
+ after the child exited, inside the 250 ms exit grace, keeps the child's
214
+ exit code instead.
215
+ - The async runner resolves at the deadline. It does not wait for output
216
+ pipes that a surviving grandchild still holds open. Its timeout message
217
+ is `timed out after <N>ms`, in `error`.
218
+ - The streaming runner writes the spawn error, the timeout message or
219
+ `<binary> was killed by <signal>` to the parent's stderr, since it has no
220
+ captured stderr.
221
+
222
+ #### Process groups
223
+
224
+ - The async runners (`spawnRunnerAsync`, `runShellAsync`,
225
+ `shellRunnerAsync`) take `processGroup: true`. The child then runs as its
226
+ own process-group leader. A timeout or an abort signals the whole group,
227
+ so processes the child started do not outlive it.
228
+ - `managedProcessRunner` takes `processGroup: true` too. `kill()` then
229
+ signals the whole group.
230
+ - The sync runners (`spawnRunner`, `runShell`, `shellRunner`) and
231
+ `streamRunner` kill the direct child only on timeout. Processes it started
232
+ keep running. These runners have no `processGroup` option.
233
+ - A process group does not get the terminal's Ctrl-C. To stop it on Ctrl-C,
234
+ listen for `SIGINT` and abort through `signal`, or call `kill()`.
235
+ - Process groups are POSIX only.
236
+
237
+ #### Abort and exit
238
+
239
+ - An abort sends `SIGTERM`, then `SIGKILL` 1 s later if the child is still running. The `SIGTERM`
240
+ lets a child such as git remove its lock files. The promise resolves at once with
241
+ `neverExited: true`, `reason: 'abort'` and `The operation was aborted` in `error`. An abort that
242
+ lands after the child exited keeps the child's exit code. While a child ignores `SIGTERM`, the
243
+ pending `SIGKILL` keeps the host alive for up to 1 s.
244
+ - The async runner settles when the child's stdio closes, or 250 ms after
245
+ the child exits if a process it started still holds the pipes. In the
246
+ second case `exitCode` is the child's own, and the pipes are closed: that
247
+ process loses any later output. With `processGroup`, the group gets
248
+ `SIGKILL` at that point.
249
+ - If `onSpawn` throws, the child is killed and the run resolves with
250
+ `neverExited: true`, `reason: 'abort'` and `onSpawn threw: <message>`.
251
+ - Without `processGroup`, a managed process's `kill()` does nothing once the
252
+ process has exited, so a reused pid is never signalled. With
253
+ `processGroup`, it still signals any member of the group that is left. A
254
+ death by a signal sent through `kill()` reports `reason: 'abort'`.
255
+
256
+ #### What throws
257
+
258
+ - `run` never throws, and an async `run` never rejects, for a failed
259
+ command. A missing binary, a timeout or an abort is a result with a
260
+ non-zero `exitCode`, `neverExited: true` and a `reason`.
261
+ - Programmer errors throw from a sync runner and reject from an async one:
262
+ an invalid `timeoutMs` and a NUL byte in argv or env.
263
+ - A managed process reports a failed spawn through `exited`, which never
264
+ rejects. A NUL byte in argv or env throws from `run`.
265
+ - `runChecked` and `runCheckedAsync` throw `CommandFailed` for a failed run.
266
+ `runJson`, `parseJsonResult`, `ran` and `failureText` never throw for one.
267
+ - `asAsync` turns a throw from the sync runner into a rejected promise.
268
+ `shellRunnerAsync` does the same for the runner it wraps.
269
+
270
+ #### `bun test`
271
+
272
+ - Under `bun test`, once a large module graph is loaded, Bun can hand child
273
+ processes stdio that the parent reads back as empty. `spawnRunner` works
274
+ around this when it runs under Bun with `NODE_ENV=test`. It runs the
275
+ command through `/bin/sh` with stdin, stdout and stderr redirected to temp
276
+ files, then reads the files back. Arguments are still not parsed by the
277
+ shell.
278
+ - Results on that path match `spawnRunner` outside `bun test`. For example,
279
+ a missing binary is a failed spawn with `exitCode: 1`, `neverExited: true`
280
+ and a spawn message. The path has no output cap.
281
+ - Jest and Vitest on Node do not take that path. The async runner has no
282
+ workaround.
283
+
284
+ #### Results and output
285
+
286
+ - `stderr` is only the child's output. When a capture runner has its own
287
+ message (spawn failure, timeout, abort, `onSpawn` throw, output cap,
288
+ failed stdin write), it is only in `error`, never copied into `stderr`.
289
+ `stderrOrError(result)` reads both: trimmed stderr, else trimmed `error`.
290
+ `failureText` and `CommandFailed` use it, so their text names the failure
291
+ either way.
292
+ - `error` can come with a real exit code and no `reason`: after a failed
293
+ stdin write, or, under Bun, a child that exits right at the output cap.
294
+ - If the child closes stdin before `input` is fully written, the result
295
+ keeps the child's exit code, and the host process does not crash. The
296
+ async runner, and the sync runner on Node, put the write error in `error`.
297
+ The sync runner under Bun does not report it.
298
+ - Output is decoded as UTF-8. A character split across pipe chunks is kept
299
+ whole. Output that ends in the middle of a character shows U+FFFD.
300
+ - `spawnRunner` (outside `bun test`) caps each stream at 256 MB and usually
301
+ kills a child that passes it, with `reason: 'output'`. The async runner
302
+ has no cap.
303
+
304
+ #### Arguments, environment and `cwd`
305
+
306
+ - `argv` is never parsed by a shell. Spaces, quotes and `$()` in an argument
307
+ reach the binary unchanged. Only the shell helpers run a shell string.
308
+ - `env` merges over `process.env`, read at call time on every runner, so a
309
+ variable set after startup reaches the child.
310
+ - `cwd` is required on every call. An empty, missing or non-directory `cwd`
311
+ is a failed spawn whose message names the `cwd`.
312
+ - Optional option fields are typed `?: T | undefined`, so callers can pass
313
+ an explicit `undefined` under `exactOptionalPropertyTypes`. A custom
314
+ runner must accept `undefined` for these fields.
315
+
316
+ #### Requirements and stability
317
+
318
+ - Node.js 22 or later, tested on Node 22 and 24 on Linux and macOS. Windows
319
+ is not supported. Bun also works, and resolves to the TypeScript source
320
+ through the `bun` export condition. ESM only. TypeScript 5.4 or later, with
321
+ `@types/node` as an optional peer dependency.
322
+ - Each export is stable or experimental. Any release may change or remove an
323
+ experimental export. The README's Stability section lists the experimental
324
+ exports. A breaking change to a stable export will be listed under
325
+ "Breaking".
326
+
327
+ [0.4.0]: https://github.com/osdlc/osdlc/tree/command-seam/v0.4.0/packages/ts/command-seam
328
+ [0.3.0]: https://github.com/sksizer/sdlc/tree/command-seam/v0.3.0/packages/command-seam
@@ -0,0 +1,58 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in `@osdlc/command-seam`.
4
+
5
+ ## Issues
6
+
7
+ Report bugs and ask for features in the
8
+ [issue tracker](https://github.com/osdlc/osdlc/issues). For a bug,
9
+ include the library version, your runtime (Node or Bun) and version, your
10
+ operating system, and a minimal example. Report security problems privately.
11
+ See [SECURITY.md](./SECURITY.md).
12
+
13
+ ## Pull requests
14
+
15
+ This package lives in [osdlc/osdlc](https://github.com/osdlc/osdlc) under
16
+ `packages/ts/command-seam`. Open pull requests there. They are reviewed and
17
+ merged there.
18
+
19
+ ## Running the tests
20
+
21
+ The repository is a pnpm workspace. Install from the repository root, then
22
+ work in the package directory. The tests run under Bun today, so install
23
+ [Bun](https://bun.sh) as well as Node 22 or later.
24
+
25
+ ```sh
26
+ pnpm install
27
+ cd packages/ts/command-seam
28
+ pnpm run build
29
+ pnpm test
30
+ ```
31
+
32
+ Build before testing: the examples import the package by name, and under Node
33
+ that name resolves to `dist/`. After a build, `pnpm run smoke` runs a short
34
+ check of the built package under Node.
35
+
36
+ Other checks:
37
+
38
+ ```sh
39
+ pnpm run typecheck
40
+ pnpm run lint
41
+ pnpm run format:check
42
+ ```
43
+
44
+ `npm run <script>` and `bun run <script>` work the same way once the workspace
45
+ is installed.
46
+
47
+ ## Code style
48
+
49
+ - TypeScript, ES modules, and explicit `.ts` import paths.
50
+ - Prettier formats the code. Run `npm run format` before you commit.
51
+ - oxlint checks the code. Run `npm run lint`.
52
+ - No semicolons and single quotes, as Prettier enforces.
53
+ - Add a test for each behavior change or bug fix.
54
+ - For an API addition or change, add an example in `examples/` that shows the
55
+ code a user would write. The change is judged by how that code reads. See
56
+ [D-15](https://github.com/osdlc/osdlc/blob/main/packages/ts/command-seam/docs/planning/decisions/D-15-api-changes-start-from-consumer-code.md).
57
+ - Keep comments short. Say why, not what.
58
+ - Update `CHANGELOG.md` for changes users can see.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SK Sizer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.