@zjaaal/artemis 0.0.0-stage → 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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +568 -2
  3. package/dist/cli/constants.d.ts +30 -0
  4. package/dist/cli/constants.js +34 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +1 -0
  7. package/dist/cli/init-command.d.ts +8 -0
  8. package/dist/cli/init-command.js +42 -0
  9. package/dist/cli/init.d.ts +23 -0
  10. package/dist/cli/init.js +153 -0
  11. package/dist/cli/launch.d.ts +10 -0
  12. package/dist/cli/launch.js +116 -0
  13. package/dist/cli/main.d.ts +2 -0
  14. package/dist/cli/main.js +7 -0
  15. package/dist/cli/resolve-hook.d.ts +9 -0
  16. package/dist/cli/resolve-hook.js +21 -0
  17. package/dist/cli/settings.d.ts +18 -0
  18. package/dist/cli/settings.js +110 -0
  19. package/dist/constants.d.ts +94 -0
  20. package/dist/constants.js +86 -0
  21. package/dist/context/context.d.ts +6 -0
  22. package/dist/context/context.js +35 -0
  23. package/dist/define/define.d.ts +15 -0
  24. package/dist/define/define.js +99 -0
  25. package/dist/discover/discover.d.ts +7 -0
  26. package/dist/discover/discover.js +99 -0
  27. package/dist/edges/files.d.ts +25 -0
  28. package/dist/edges/files.js +111 -0
  29. package/dist/edges/modules.d.ts +2 -0
  30. package/dist/edges/modules.js +6 -0
  31. package/dist/edges/process.d.ts +2 -0
  32. package/dist/edges/process.js +9 -0
  33. package/dist/edges/prompts.d.ts +2 -0
  34. package/dist/edges/prompts.js +14 -0
  35. package/dist/edges/system.d.ts +4 -0
  36. package/dist/edges/system.js +5 -0
  37. package/dist/errors/errors.d.ts +25 -0
  38. package/dist/errors/errors.js +37 -0
  39. package/dist/help/help.d.ts +15 -0
  40. package/dist/help/help.js +169 -0
  41. package/dist/index.d.ts +13 -0
  42. package/dist/index.js +12 -0
  43. package/dist/kits/terminal/constants.d.ts +10 -0
  44. package/dist/kits/terminal/constants.js +10 -0
  45. package/dist/kits/terminal/index.d.ts +3 -0
  46. package/dist/kits/terminal/index.js +2 -0
  47. package/dist/kits/terminal/models.d.ts +57 -0
  48. package/dist/kits/terminal/models.js +2 -0
  49. package/dist/kits/terminal/open-link.d.ts +2 -0
  50. package/dist/kits/terminal/open-link.js +12 -0
  51. package/dist/kits/terminal/open-tabs.d.ts +3 -0
  52. package/dist/kits/terminal/open-tabs.js +5 -0
  53. package/dist/kits/terminal/toml.d.ts +3 -0
  54. package/dist/kits/terminal/toml.js +53 -0
  55. package/dist/kits/terminal/warp.d.ts +6 -0
  56. package/dist/kits/terminal/warp.js +32 -0
  57. package/dist/kits/worktrees/constants.d.ts +25 -0
  58. package/dist/kits/worktrees/constants.js +25 -0
  59. package/dist/kits/worktrees/git.d.ts +2 -0
  60. package/dist/kits/worktrees/git.js +7 -0
  61. package/dist/kits/worktrees/index.d.ts +3 -0
  62. package/dist/kits/worktrees/index.js +2 -0
  63. package/dist/kits/worktrees/instances.d.ts +9 -0
  64. package/dist/kits/worktrees/instances.js +33 -0
  65. package/dist/kits/worktrees/lock-edges.d.ts +7 -0
  66. package/dist/kits/worktrees/lock-edges.js +21 -0
  67. package/dist/kits/worktrees/lock.d.ts +7 -0
  68. package/dist/kits/worktrees/lock.js +104 -0
  69. package/dist/kits/worktrees/models.d.ts +123 -0
  70. package/dist/kits/worktrees/models.js +2 -0
  71. package/dist/kits/worktrees/names.d.ts +5 -0
  72. package/dist/kits/worktrees/names.js +9 -0
  73. package/dist/kits/worktrees/queries.d.ts +20 -0
  74. package/dist/kits/worktrees/queries.js +61 -0
  75. package/dist/kits/worktrees/registration.d.ts +15 -0
  76. package/dist/kits/worktrees/registration.js +107 -0
  77. package/dist/kits/worktrees/registry-store.d.ts +3 -0
  78. package/dist/kits/worktrees/registry-store.js +27 -0
  79. package/dist/kits/worktrees/worktrees.d.ts +10 -0
  80. package/dist/kits/worktrees/worktrees.js +56 -0
  81. package/dist/models.d.ts +485 -0
  82. package/dist/models.js +1 -0
  83. package/dist/parse/parse.d.ts +8 -0
  84. package/dist/parse/parse.js +81 -0
  85. package/dist/resolve/resolve.d.ts +18 -0
  86. package/dist/resolve/resolve.js +96 -0
  87. package/dist/run/report.d.ts +6 -0
  88. package/dist/run/report.js +51 -0
  89. package/dist/run/run.d.ts +8 -0
  90. package/dist/run/run.js +163 -0
  91. package/dist/run/world.d.ts +3 -0
  92. package/dist/run/world.js +12 -0
  93. package/dist/utils/error-code.d.ts +3 -0
  94. package/dist/utils/error-code.js +7 -0
  95. package/dist/utils/error-message.d.ts +3 -0
  96. package/dist/utils/error-message.js +2 -0
  97. package/dist/utils/is-record.d.ts +3 -0
  98. package/dist/utils/is-record.js +2 -0
  99. package/dist/utils/tag.d.ts +3 -0
  100. package/dist/utils/tag.js +8 -0
  101. package/dist/utils/terminator.d.ts +5 -0
  102. package/dist/utils/terminator.js +8 -0
  103. package/package.json +72 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Jalinson Diaz
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.
package/README.md CHANGED
@@ -1,3 +1,569 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">🏹 Artemis</h1>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <p align="center"><strong>Every file an arrow. Every folder a quiver.</strong></p>
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/zJaaal/artemis/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/zJaaal/artemis/actions/workflows/ci.yml/badge.svg"></a>
7
+ <img alt="Node 22 or newer" src="https://img.shields.io/badge/node-%3E%3D22-brightgreen">
8
+ <img alt="ESM only" src="https://img.shields.io/badge/module-ESM-blue">
9
+ <img alt="Types included" src="https://img.shields.io/badge/types-included-blue">
10
+ <img alt="License MIT" src="https://img.shields.io/badge/license-MIT-lightgrey">
11
+ </p>
12
+
13
+ Artemis turns repetitive work into commands an AI agent can call, instead of steps it has to read and redo.
14
+
15
+ An agent that does the same job twice usually pays for it twice: it reads the instructions (a skill, a doc, a prompt), works out the steps, and runs them. With Artemis the agent writes the job down once, as one file in a folder. From then on it just calls it:
16
+
17
+ ```console
18
+ $ art deploy staging --json
19
+ {"ok":true,"command":"deploy","result":{"url":"https://staging.example.com"}}
20
+ ```
21
+
22
+ The script does the work, the same way every time. The agent spends tokens on the call and on the small result it reads back, not on re-reading instructions or re-deriving the steps. That is near zero, not zero: a run still costs the call and its result, and discovery (`help --json`) costs tokens in proportion to how many commands you have. What it saves is the repeated work itself.
23
+
24
+ Every file is a command, every folder is a group of commands, and there is no registry to keep up to date: Artemis hunts down whatever you drop in the folder. Everything an agent needs to call a command (arguments, flags, defaults, allowed values, examples) is machine-readable, and people get a normal CLI with `--help`.
25
+
26
+ ```
27
+ commands/
28
+ hello.js -> art hello
29
+ db/
30
+ _topic.js -> describes the "db" group
31
+ migrate.js -> art db migrate
32
+ reset.js -> art db reset
33
+ ```
34
+
35
+ ## The hunt, in one table
36
+
37
+ The metaphor is branding. In the API, the help text and the errors, things keep their plain names.
38
+
39
+ | In the story | In the code |
40
+ | ------------ | ----------------------------------------------------------- |
41
+ | An **arrow** | A command: one `.js` file |
42
+ | A **quiver** | A folder: `commands/` itself, or a topic inside it |
43
+ | **The hunt** | Discovery: Artemis walks the folder and finds every command |
44
+ | **`art`** | The CLI you build on it (the name is yours) |
45
+
46
+ ## Where it fits
47
+
48
+ - **Instead of a skill, for work that is deterministic.** A skill is instructions a model reads and follows: it costs tokens each time it is loaded, and the model can still get a step wrong. If the steps are always the same, write them as a command and let the agent call it. Keep skills for work that needs judgment.
49
+ - **Like an MCP server, without the server.** The idea is the same: tools with a name, a description and typed arguments that an agent can discover (`help --json`) and call. The difference is that there is no server, no protocol and no process to keep running: a tool is a file, and calling it is a shell command. It is not an MCP implementation; an MCP adapter is possible later, but it is not part of this release.
50
+ - **Local and private, with one caveat.** The work happens on your machine, with your files and credentials, and Artemis itself sends nothing anywhere. What reaches the model is whatever a command returns, so return what the agent needs and not everything the command saw.
51
+ - **Commands are scripts.** They run with your permissions. Treat a command file like any script you run, and review the ones an agent writes.
52
+
53
+ ## Why it is built this way
54
+
55
+ - **One file, one command.** No central list to edit. Delete the file and the command is gone.
56
+ - **Machine-readable by default.** `help --json` lists every command with its arguments, flags, defaults, choices and examples. With `--json`, every call prints exactly one JSON object.
57
+ - **Honest exit codes.** `0` success, `1` the command failed, `2` the command line was wrong.
58
+ - **Never hangs.** `--yes` and `--json` never prompt, and a prompt that nobody can answer fails with a hint instead of waiting forever.
59
+ - **Typed.** `run({ args, flags })` is typed from the definition, even in plain JavaScript with `// @ts-check`.
60
+ - **Small.** ESM only, Node 22 or newer, and one runtime dependency (`prompts`), loaded only when a command asks a question.
61
+
62
+ ## Install
63
+
64
+ ```sh
65
+ pnpm add -D @zjaaal/artemis # in a project: run it with `npx art`
66
+ # or: npm install --save-dev @zjaaal/artemis
67
+
68
+ npm install --global @zjaaal/artemis # anywhere: run it as `art` (or `artemis`, the same command)
69
+ ```
70
+
71
+ Installing the package puts two commands on your path, `art` and `artemis`. They are the same file, so use whichever you like. TypeScript users who use the types of `ctx.prompt` can add `@types/prompts` (an optional peer dependency; plain JavaScript never needs it).
72
+
73
+ ## Quick start
74
+
75
+ No bin file, no boilerplate. Let `art init` set the project up, or do it by hand: a project is a `package.json` and a `commands/` folder next to it.
76
+
77
+ **1. Make a folder a project.**
78
+
79
+ ```console
80
+ $ npx art init
81
+ Created /work/my-project/package.json
82
+ Created /work/my-project/commands
83
+ Created /work/my-project/commands/hello.js
84
+ ```
85
+
86
+ `init` creates a `package.json` (or adds one line, the `artemis` key, to the one you have, leaving everything else as it was), the `commands/` folder and a sample command. It never overwrites anything, and running it again changes nothing. It also tells you if your `package.json` does not say `"type": "module"`: command files are ES modules, and without it Node prints a warning about each one on stderr. `init` takes no arguments, so go to the folder first (`--json` is the only flag; there is nothing to confirm). (`init` is reserved: a command file called `init.js` cannot be run through `art`, and `init` warns you if you have one.)
87
+
88
+ **2. Run the sample, then write your own.**
89
+
90
+ ```console
91
+ $ npx art hello
92
+ hello world
93
+
94
+ $ npx art hello --json
95
+ {"ok":true,"command":"hello","result":{"greeting":"hello world"}}
96
+
97
+ $ npx art help
98
+ ```
99
+
100
+ A command is one file in `commands/`:
101
+
102
+ ```js
103
+ // commands/greet.js
104
+ import { defineCommand } from '@zjaaal/artemis';
105
+
106
+ export default defineCommand({
107
+ description: 'Greet someone, loudly if asked',
108
+ args: [{ name: 'name', description: 'Who to greet', default: 'world' }],
109
+ flags: { shout: { type: 'boolean', short: 's', description: 'Uppercase the greeting' } },
110
+ run({ args, flags, ctx }) {
111
+ const greeting = `hello ${args.name}`;
112
+ const text = flags.shout ? greeting.toUpperCase() : greeting;
113
+
114
+ ctx.log(text);
115
+
116
+ return { greeting: text };
117
+ }
118
+ });
119
+ ```
120
+
121
+ **By hand, with no `init`:** put your commands in a `commands/` folder next to a `package.json` and run `art`. That is all the convention needs.
122
+
123
+ If you installed it globally, drop the `npx` and run `art hello` from the project or any folder inside it. A globally installed `art` does not need a copy of Artemis in the project: it makes `import ... from '@zjaaal/artemis'` in your command files resolve to the copy that is running them. A copy the project installed itself always wins, and errors thrown by either copy keep their hint and exit code.
124
+
125
+ ### How `art` finds your project
126
+
127
+ `art` looks at each `package.json` from the current folder up through its parents, and the **project** is the nearest one that says it is one, in either of two ways:
128
+
129
+ - **It declares itself**: it has an `"artemis"` key. This is what `art init` writes, and it is the explicit way: nothing is guessed.
130
+ - **It follows the convention**: it has a `commands/` folder next to it. This needs no setup.
131
+
132
+ Any other `package.json` is simply not a candidate and is skipped: a `{ "type": "module" }` file in `src/` or `dist/` (with or without other keys, such as `sideEffects`), a template, or a monorepo package that has neither. So `art` works from any folder inside your project, and a monorepo package without its own commands uses the root's. Two projects on one machine never see each other's commands.
133
+
134
+ Both settings are optional, under the `artemis` key of that `package.json`:
135
+
136
+ ```json
137
+ { "artemis": { "commandsDir": "tools/commands", "bin": "mytool" } }
138
+ ```
139
+
140
+ | Setting | What it does | Default |
141
+ | ------------- | ------------------------------------------------------------------- | ---------------------------------------------- |
142
+ | `commandsDir` | The commands folder, relative to the folder of that `package.json`. | `commands` |
143
+ | `bin` | The name help and usage lines show. | `art` (see "Your own command name" for a tool) |
144
+
145
+ `art` always shows `art` (or your `bin` setting) in help, even in a project that publishes a CLI of its own, so what the help tells you to type is what you typed.
146
+
147
+ When it cannot find what to run, `art` exits `1` and says what to create or set, and with `--json` it prints the usual failure object with `command: null`:
148
+
149
+ | Problem | What the hint tells you |
150
+ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
151
+ | no project from here upwards | a project is the nearest `package.json` with an `artemis` key or a `commands/` folder next to it; run it inside one, or run `init` in this folder |
152
+ | the commands folder does not exist | create it, set `"artemis": { "commandsDir" }`, or point it at a parent's commands (`"../commands"`) |
153
+ | the `artemis` key has a bad shape | the shape it should have |
154
+ | any `package.json` on the way up is not valid JSON or not readable | the file, and what is wrong with it. It is never skipped, because running a different project's commands silently would be worse (a deliberately broken fixture between you and the project makes `art` fail until you run it from elsewhere) |
155
+
156
+ ### Your own command name
157
+
158
+ To ship the same CLI under your own name (say you publish a package `mytool`), give it a bin file:
159
+
160
+ ```js
161
+ #!/usr/bin/env node
162
+ // bin/mytool.js
163
+ import { cli } from '@zjaaal/artemis/cli';
164
+
165
+ process.exitCode = await cli({ root: import.meta.url });
166
+ ```
167
+
168
+ and point `package.json` at it, with `@zjaaal/artemis` as a dependency:
169
+
170
+ ```json
171
+ {
172
+ "name": "mytool",
173
+ "type": "module",
174
+ "bin": { "mytool": "bin/mytool.js" },
175
+ "dependencies": { "@zjaaal/artemis": "^0.1.0" }
176
+ }
177
+ ```
178
+
179
+ `root` is where your bin file is: your tool runs **its own** `commands/`, wherever the user happens to be standing, not the commands of whatever project the user is in. It finds its package through the nearest `package.json` with a name, a `bin` or an `artemis` key, and never looks past its own folder in `node_modules`, so it never climbs into the project that installed it, even if you forgot to declare a `bin`. Make sure `commands/` ships with your package (check `files` in your `package.json`): a tool published without its commands fails with "Commands folder not found" and a hint for you, instead of running the commands of the project that installed it. Nothing is reserved for a tool: `init` and every other word is yours. Help and usage lines show `mytool`, taken from the single key of your `bin` object, or from your package name (without its `@scope/`) when `bin` is a plain string or has several keys, or from `"artemis": { "bin" }` if you set it. `cli` resolves to the exit code and never calls `process.exit`; it also takes `argv` and the host options of `run()` (`stdout`, `stderr`, `interactive`, `color`, `env`) if you need them.
180
+
181
+ ### Full control
182
+
183
+ If you want to decide the commands folder, the streams or the exit code yourself, call `run()` from your own bin file. See [`run(argv, options)`](#fully-in-control-runargv-options) below.
184
+
185
+ ## The quiver: how files become commands
186
+
187
+ | What you write | What you get |
188
+ | ----------------------------- | ------------------------------------------------------------------ |
189
+ | `commands/hello.js` | `art hello` |
190
+ | `commands/db/migrate.js` | `art db migrate`, and also `art db:migrate` |
191
+ | `commands/db/_topic.js` | the description of the `db` group (`defineTopic({ description })`) |
192
+ | `commands/_shared.js` | nothing: files and folders starting with `_` or `.` are private |
193
+ | `aliases: ['m']` in a command | `art m` runs it too |
194
+
195
+ - Only `.js` files are commands. A folder with at least one command becomes a topic automatically, however deep.
196
+ - A file that does not default-export `defineCommand(...)`, or that throws when it loads, is **skipped with a warning** on stderr, so one bad file does not take the CLI down.
197
+ - An alias that is already the name of a command, a topic or another alias is ignored with a warning.
198
+ - Words are matched greedily: `art db migrate --force` is the command `db migrate` with the flag `--force`.
199
+ - A mistyped name suggests the closest one: `Did you mean "hello"?`.
200
+
201
+ A topic file:
202
+
203
+ ```js
204
+ // commands/db/_topic.js
205
+ import { defineTopic } from '@zjaaal/artemis';
206
+
207
+ export default defineTopic({ description: 'Database tasks' });
208
+ ```
209
+
210
+ ## Writing an arrow: `defineCommand`
211
+
212
+ ```js
213
+ import { defineCommand, CommandError } from '@zjaaal/artemis';
214
+
215
+ export default defineCommand({
216
+ description: 'Add two numbers', // required, one line
217
+ aliases: ['sum'],
218
+ args: [
219
+ { name: 'a', required: true, description: 'First number' },
220
+ { name: 'b', required: true, description: 'Second number' }
221
+ ],
222
+ flags: {
223
+ round: { type: 'boolean', short: 'r', description: 'Round the result' }
224
+ },
225
+ examples: ['math add 1 2'],
226
+ run({ args, flags, ctx }) {
227
+ const sum = Number(args.a) + Number(args.b);
228
+
229
+ if (Number.isNaN(sum)) {
230
+ throw new CommandError('Both arguments must be numbers', { hint: 'Try: math add 1 2' });
231
+ }
232
+
233
+ ctx.log(`sum: ${sum}`);
234
+
235
+ return { sum: flags.round ? Math.round(sum) : sum };
236
+ }
237
+ });
238
+ ```
239
+
240
+ ### Definition fields
241
+
242
+ | Field | Required | What it is |
243
+ | ------------- | -------- | ----------------------------------------------------------------------------------------- |
244
+ | `description` | yes | One line, shown by `help` and `help --json`. |
245
+ | `run` | yes | `({ args, flags, ctx }) => result`. May be `async`. The result is printed under `--json`. |
246
+ | `args` | | Positional arguments, in order. |
247
+ | `flags` | | Flags, by name. |
248
+ | `aliases` | | Other names the command answers to. |
249
+ | `examples` | | Example invocations, without the binary name. Shown by `--help` and `help --json`. |
250
+ | anything else | | Passes through untouched, so a project can build its own conventions on top. |
251
+
252
+ **Args** (`{ name, description?, required?, default?, choices? }`): a required arg cannot have a default, and a required arg cannot come after an optional one. `choices` limits the allowed values.
253
+
254
+ **Flags** (`{ type, short?, multiple?, default?, choices?, description? }`):
255
+
256
+ - `type` is `'boolean'` or `'string'`.
257
+ - `short` is a single character (`-s`).
258
+ - `multiple: true` lets a string flag repeat (`--tag a --tag b` gives `['a', 'b']`), and its `default` is then an array of strings.
259
+ - `default` must match the type: a boolean for a boolean flag, a string for a string flag.
260
+ - `help`, `json` and `yes` (and `-h`, `-y`) are reserved: every command already has them.
261
+
262
+ `defineCommand` throws a `TypeError` right away, with a message that names the problem, if a definition breaks one of these rules.
263
+
264
+ ### Results and errors
265
+
266
+ - **Return a value** from `run` to give the command a result. With `--json` it becomes the `result` of the output object.
267
+ - **Throw `CommandError(message, { hint })`** for a failure the caller can fix. The message and the hint are shown without a stack trace, and the command exits `1`. Put in `hint` what to do about it.
268
+ - **Throw `UsageError(message, { hint, usage })`** when the command line itself is wrong. It exits `2`.
269
+ - **Any other error** is treated as a bug: it exits `1` and prints a stack trace.
270
+
271
+ ### The `ctx` your command receives
272
+
273
+ | Field | What it does |
274
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
275
+ | `ctx.log(...)` | Human output on stdout. **Silent under `--json`**, so stdout stays one JSON object. |
276
+ | `ctx.warn(...)` | Progress and warnings on stderr. Never mixes with `--json` output. |
277
+ | `ctx.prompt(questions, { hint })` | Asks the user (questions use the [`prompts`](https://github.com/terkelg/prompts) format). When nobody can answer it throws a `CommandError` with `hint`. |
278
+ | `ctx.interactive` | `true` only when a prompt may be shown. See "Prompts" below. |
279
+ | `ctx.json` | Whether the run was asked for `--json`. |
280
+ | `ctx.style` | Color helpers (`bold`, `dim`, `red`, `green`, `yellow`, `cyan`); plain text when color is off. |
281
+ | `ctx.commands` | Every discovered command, for building lists of commands to run in order. |
282
+ | `ctx.invoke(id, argv)` | Runs another command by id or alias and returns its result. It inherits `--json` and `--yes`. A bad call (unknown command, wrong arguments, `--help`) fails the calling command with exit `1`. |
283
+
284
+ ### Typed arguments and flags
285
+
286
+ `run({ args, flags })` is typed from the definition, so command files get editor feedback even in plain JavaScript with `// @ts-check`:
287
+
288
+ ```js
289
+ // @ts-check
290
+ import { defineCommand } from '@zjaaal/artemis';
291
+
292
+ export default defineCommand({
293
+ description: 'Pick a color',
294
+ args: [{ name: 'color', choices: ['red', 'blue'], default: 'red' }],
295
+ flags: { tag: { type: 'string', multiple: true } },
296
+ run({ args, flags }) {
297
+ args.color; // 'red' | 'blue'
298
+ flags.tag; // string[] | undefined
299
+ // args.size -> type error: not declared
300
+ }
301
+ });
302
+ ```
303
+
304
+ | Declared as | `run` sees |
305
+ | ------------------------------------- | --------------------------------------------------- |
306
+ | a required arg, or one with a default | `string` |
307
+ | an optional arg | `string \| undefined` |
308
+ | `choices` | the union of the choices |
309
+ | a boolean flag | `boolean \| undefined` (`boolean` with a default) |
310
+ | a string flag | `string \| undefined` (`string` with a default) |
311
+ | a `multiple` flag | `string[] \| undefined` (`string[]` with a default) |
312
+
313
+ The helper types `InferArgs`, `InferFlags`, `RunInput`, `Command`, `CommandDefinition`, `CommandContext`, `ArgDefinition`, `FlagDefinition` and `RunOptions` are exported for TypeScript users.
314
+
315
+ ## Using the CLI
316
+
317
+ ```
318
+ art <topic> <command> [args] [flags]
319
+ ```
320
+
321
+ ### Flags every command has
322
+
323
+ | Flag | What it does |
324
+ | ------------ | ------------------------------------------------------------- |
325
+ | `-h, --help` | Show help for the command. |
326
+ | `--json` | Print the result (or the error) as one JSON object on stdout. |
327
+ | `-y, --yes` | Never prompt; use flags, saved values or defaults. |
328
+
329
+ Global flags may be typed before or after the command name: `art --json math add 1 2` and `art math add 1 2 --json` are the same. Any other flag has to come after the command. After `--`, everything is a plain argument (`art echo -- --json` echoes the text `--json`).
330
+
331
+ ### Getting help
332
+
333
+ | Ask | You get |
334
+ | ----------------------------------- | ----------------------------------------------------------------------- |
335
+ | `art` or `art help` | the topics and top-level commands |
336
+ | `art <topic>` or `art <topic> help` | every command inside the topic |
337
+ | `art <command> --help` | the command's arguments, flags, defaults, choices, aliases and examples |
338
+ | `art help --json` | the manifest of every command, as data |
339
+ | `art help <topic> --json` | the manifest narrowed to one topic: same shape, only its commands |
340
+ | `art help <command> --json` | the manifest entry of one command |
341
+
342
+ ### The JSON contract
343
+
344
+ With `--json`, every call prints **exactly one object** on stdout and nothing else:
345
+
346
+ ```jsonc
347
+ // success
348
+ { "ok": true, "command": "math add", "result": { "sum": 3 } }
349
+
350
+ // failure
351
+ { "ok": false, "command": "math add", "error": "Both arguments must be numbers", "hint": "Try: math add 1 2" }
352
+ ```
353
+
354
+ `hint` is left out when there is none, and `command` is `null` when no command was resolved (for example an unknown command). Warnings go to stderr, so stdout stays parseable.
355
+
356
+ The manifest from `help --json` has this shape:
357
+
358
+ ```jsonc
359
+ {
360
+ "bin": "art",
361
+ "topics": [{ "id": "math", "description": "Arithmetic" }],
362
+ "commands": [
363
+ {
364
+ "id": "math add",
365
+ "description": "Add two numbers",
366
+ "usage": "art math add <a> <b> [flags]",
367
+ "aliases": ["sum"],
368
+ "args": [
369
+ {
370
+ "name": "a",
371
+ "description": "First number",
372
+ "required": true,
373
+ "default": null,
374
+ "choices": null
375
+ }
376
+ ],
377
+ "flags": [
378
+ {
379
+ "name": "round",
380
+ "type": "boolean",
381
+ "short": "r",
382
+ "multiple": false,
383
+ "description": "Round the result",
384
+ "default": null,
385
+ "choices": null
386
+ }
387
+ ],
388
+ "examples": ["math add 1 2"]
389
+ }
390
+ ],
391
+ "globalFlags": [/* help, json, yes */]
392
+ }
393
+ ```
394
+
395
+ ### Exit codes
396
+
397
+ | Code | Meaning | In code |
398
+ | ---- | ---------------------------------------------------------------------------------- | ------------------ |
399
+ | `0` | success | `EXIT_CODE.OK` |
400
+ | `1` | the command failed (a `CommandError`, or a bug) | `EXIT_CODE.FAILED` |
401
+ | `2` | the command line was wrong: unknown command, bad or missing argument, unknown flag | `EXIT_CODE.USAGE` |
402
+
403
+ ### Prompts
404
+
405
+ - A command asks with `ctx.prompt(...)`. In a terminal the user answers.
406
+ - `--yes` and `--json` mean nobody can answer, so `ctx.interactive` is `false` and `ctx.prompt` throws a `CommandError`. The same happens when there is no terminal (a pipe, an agent, CI).
407
+ - The hint of that error says how to answer without a prompt. Pass your own: `ctx.prompt(question, { hint: 'Pass --yes to accept the default.' })`. Without one, the hint says to pass `--yes` or the flags that answer the question.
408
+ - The good pattern: check `ctx.interactive`, and fall back to flags, saved values or defaults when it is `false`.
409
+
410
+ ```js
411
+ async run({ ctx }) {
412
+ if (!ctx.interactive) return { confirmed: true };
413
+
414
+ const { confirmed } = await ctx.prompt({ type: 'confirm', name: 'confirmed', message: 'Sure?' });
415
+
416
+ return { confirmed };
417
+ }
418
+ ```
419
+
420
+ ## For agents
421
+
422
+ Everything an agent needs to add a command and call one is in [AGENTS.md](AGENTS.md), and `npx art help --json` from the project folder is where it starts. The short version: read `art help --json` to learn what exists, call with `--json` to get one object back, branch on the exit code, and pass `--yes` when nobody can answer.
423
+
424
+ ## Fully in control: `run(argv, options)`
425
+
426
+ A bin file that calls `run()` decides everything itself. `bin` is required: it is the name help and usage lines show.
427
+
428
+ ```js
429
+ #!/usr/bin/env node
430
+ // bin/art.js
431
+ import path from 'node:path';
432
+ import { fileURLToPath } from 'node:url';
433
+ import { run } from '@zjaaal/artemis';
434
+
435
+ const commandsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '../commands');
436
+
437
+ process.exitCode = await run(process.argv.slice(2), { bin: 'art', commandsDir });
438
+ ```
439
+
440
+ `run` resolves to the exit code and **never calls `process.exit`**, so you can embed it and test it.
441
+
442
+ | Option | Required | What it is |
443
+ | ------------------ | -------- | ------------------------------------------------------------------------------------------------------- |
444
+ | `bin` | yes | The name of your CLI, shown in help and usage lines. |
445
+ | `commandsDir` | yes | The folder to hunt for commands in. |
446
+ | `stdout`, `stderr` | no | Where output goes (anything with `write(chunk)`; a Node `Writable` fits). Default: the process streams. |
447
+ | `interactive` | no | Whether prompts may be shown. Default: stdin and stdout are terminals. |
448
+ | `color` | no | Default: stdout is a terminal and `NO_COLOR` is unset. |
449
+ | `env` | no | The environment to read `NO_COLOR` from. Default: `process.env`. |
450
+
451
+ ### Testing your commands
452
+
453
+ Because `run` takes its world as options, a test passes in-memory sinks and asserts on what was written and on the exit code:
454
+
455
+ ```js
456
+ import { run, EXIT_CODE } from '@zjaaal/artemis';
457
+
458
+ const sink = () => {
459
+ let text = '';
460
+
461
+ return {
462
+ write: (chunk) => ((text += chunk), true),
463
+ get text() {
464
+ return text;
465
+ }
466
+ };
467
+ };
468
+
469
+ const stdout = sink();
470
+ const code = await run(['math', 'add', '1', '2', '--json'], {
471
+ bin: 'art',
472
+ commandsDir: new URL('../commands', import.meta.url).pathname,
473
+ stdout,
474
+ stderr: sink(),
475
+ interactive: false,
476
+ color: false
477
+ });
478
+
479
+ console.log(code === EXIT_CODE.OK, JSON.parse(stdout.text).result); // true { sum: 3 }
480
+ ```
481
+
482
+ ## Kits
483
+
484
+ Opt-in pieces, imported from their own subpath so apps that do not need them do not load them.
485
+
486
+ ### Worktrees: `@zjaaal/artemis/worktrees`
487
+
488
+ Give every checkout of a repo its own **instance**: a stable slot, ports shifted by that slot, and a color, so several copies of an app can run side by side. The main checkout is slot 0 and needs no registration; other worktrees register on first use and keep their slot until removed. The kit keeps nothing of its own: the app says where the registry lives (`stateFile`).
489
+
490
+ ```js
491
+ import { defineWorktrees } from '@zjaaal/artemis/worktrees';
492
+
493
+ const worktrees = defineWorktrees({
494
+ stateFile: '/path/to/data/instances.json',
495
+ mainCheckout: '/path/to/main/checkout',
496
+ ports: { http: 8080, frontend: 4200 }
497
+ });
498
+
499
+ const instance = worktrees.ensure(); // registers the checkout you are in, on first use
500
+ instance.ports; // { http: 8180, frontend: 4300 } for slot 1
501
+ ```
502
+
503
+ | Option | What it is |
504
+ | -------------- | ------------------------------------------------------------------------------------------------------------- |
505
+ | `stateFile` | JSON file the registry is kept in (required). |
506
+ | `mainCheckout` | Path of the main checkout, slot 0 (required). |
507
+ | `ports` | Base ports, shifted by `slot * slotStep` (required). |
508
+ | `isCheckout` | `(root) => boolean`: whether a git root is a checkout of this app. Other repos count as outside any checkout. |
509
+ | `envVar` | A variable that pins the instance by name, overriding the folder. |
510
+ | `slotStep` | Port shift per slot. Default `100`. |
511
+ | `colors` | One per slot, cycling. |
512
+ | `mainName` | Name of the main instance. Default `"main"`. |
513
+
514
+ | Method | What it does |
515
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
516
+ | `list()` | Every instance: the main one first, then the registered ones by slot. |
517
+ | `get(name)` | One instance by name, or a `CommandError`. |
518
+ | `current({ cwd })` | The instance for where the app runs. A worktree that is not registered refuses, with a hint, so nothing lands on another instance by mistake. |
519
+ | `ensure({ cwd, root, name })` | The instance for a checkout, registering it on first use, named after its folder (with a suffix if taken). |
520
+ | `ensureDetached({ name, meta })` | A named instance that belongs to no checkout (for example one per container image). |
521
+ | `remove(name)` | Forgets a registered instance and frees its slot. Never touches the worktree itself. |
522
+
523
+ Every write to the registry holds a lock file (`<stateFile>.lock`), so several processes registering at once each get their own slot. A lock left by a process that died is taken over safely.
524
+
525
+ ### Terminal: `@zjaaal/artemis/terminal`
526
+
527
+ Describe terminal tabs as data and let an adapter open them. Only Warp is supported for now.
528
+
529
+ ```js
530
+ import { openTabs, warp } from '@zjaaal/artemis/terminal';
531
+
532
+ openTabs(warp(), [
533
+ {
534
+ id: 'demo',
535
+ name: 'demo',
536
+ panes: [
537
+ { dir: '/work/demo', run: ['art up'] },
538
+ { dir: '/work/demo', run: ['art wait'], focus: true }
539
+ ]
540
+ }
541
+ ]);
542
+ ```
543
+
544
+ - A tab is `{ id, name, title?, color?, panes }`: a flat list of panes laid out left to right. Its `id` is a plain word (letters, digits, `-` and `_`), because it becomes a file name and part of a link.
545
+ - Instead of `panes`, a tab can take a `layout`: a tree of splits, `{ split: 'horizontal' | 'vertical', children: [pane | split, ...] }` (`horizontal` is side by side, `vertical` is stacked).
546
+ - A pane is `{ dir, run?: string[], focus? }`.
547
+ - `warp({ configDir, open })` writes each tab as a Warp Tab Config and opens it through its `warp://` link. Both options are injectable, which is how you test it.
548
+
549
+ ## Requirements
550
+
551
+ - Node 22 or newer. ESM only: use `import`, not `require`.
552
+ - Command files are plain `.js` files (discovery loads `.js` only).
553
+
554
+ ## Development
555
+
556
+ ```sh
557
+ pnpm install
558
+ pnpm run test:all # tsc, eslint, prettier, vitest (including the type tests)
559
+ pnpm run build # emits dist/ (JS and .d.ts)
560
+ pnpm run check-exports # after a build: the entry points resolve, nothing else does
561
+ pnpm run check-bin # after a build: the packed art and artemis, run in temporary projects
562
+ pnpm run check-lock # after a build: real processes racing for the registry lock
563
+ ```
564
+
565
+ The code follows [docs/CODE_STANDARDS.md](docs/CODE_STANDARDS.md). Changes are specified in `specs/`.
566
+
567
+ ## License
568
+
569
+ MIT
@@ -0,0 +1,30 @@
1
+ /** A word the shipped `art` handles itself, like `help`: it makes a folder a project. */
2
+ export declare const INIT_WORD = "init";
3
+ export declare const INIT_USAGE_LINE = "init [--json]";
4
+ export declare const INIT_HELP = "Usage: init [--json]\n\nMakes the current folder a project: a package.json with an \"artemis\" key, a commands/ folder and a sample command. Nothing that exists is overwritten. It takes no arguments: go to the folder first.";
5
+ /** The only flags \`init\` takes: it is a fixed word of the binary, not a command, so it needs no more. */
6
+ export declare const INIT_FLAGS: {
7
+ readonly json: "--json";
8
+ readonly help: "--help";
9
+ readonly helpShort: "-h";
10
+ };
11
+ /** What `init` writes for the declaration, and the fields of a package.json it has to create. */
12
+ export declare const INIT_DECLARATION: {
13
+ readonly commandsDir: "commands";
14
+ };
15
+ export declare const NEW_MANIFEST_FIELDS: {
16
+ readonly private: true;
17
+ readonly type: "module";
18
+ };
19
+ /** The command `init` writes so that `art hello` works right away. */
20
+ export declare const SAMPLE_COMMAND_FILE = "hello.js";
21
+ export declare const SAMPLE_COMMAND_SOURCE = "import { defineCommand } from '@zjaaal/artemis';\n\nexport default defineCommand({\n description: 'Say hello',\n args: [{ name: 'name', description: 'Who to greet', default: 'world' }],\n run({ args, ctx }) {\n const greeting = `hello ${args.name}`;\n\n ctx.log(greeting);\n\n return { greeting };\n }\n});\n";
22
+ export declare const FALLBACK_PACKAGE_NAME = "project";
23
+ export declare const DEFAULT_INDENT_SPACES = 2;
24
+ export declare const INDENT_PATTERN: RegExp;
25
+ export declare const PACKAGE_NAME_INVALID: RegExp;
26
+ export declare const PACKAGE_NAME_EDGES: RegExp;
27
+ export declare const TYPE_WARNING = "Your package.json does not say \"type\": \"module\". Command files are ES modules, so Node prints a warning for each one until it does.";
28
+ export declare const SHADOW_WARNING = "This project has a command or topic called \"init\", and the init word of this binary hides it: it cannot be run through here. Rename it.";
29
+ export declare const CRLF = "\r\n";
30
+ export declare const LF = "\n";