@terminus-ai/cli 0.0.1

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -0
  3. package/bin/agent-discovery.mjs +71 -0
  4. package/bin/agent-icon.mjs +77 -0
  5. package/bin/agent-models.mjs +77 -0
  6. package/bin/agent-type.mjs +51 -0
  7. package/bin/agentdev.mjs +657 -0
  8. package/bin/app-route-script.mjs +59 -0
  9. package/bin/app-runtime-contract.mjs +2 -0
  10. package/bin/appdev-remote.mjs +346 -0
  11. package/bin/appdev.mjs +4446 -0
  12. package/bin/apps.mjs +5512 -0
  13. package/bin/capability-calls.mjs +437 -0
  14. package/bin/capsule-data.mjs +260 -0
  15. package/bin/client.mjs +189 -0
  16. package/bin/commands.mjs +1194 -0
  17. package/bin/dev-capsules.mjs +1599 -0
  18. package/bin/dev-contract.mjs +262 -0
  19. package/bin/dev-data.mjs +287 -0
  20. package/bin/dev-members.mjs +18 -0
  21. package/bin/dev-net.mjs +316 -0
  22. package/bin/dev-notification-popup.mjs +628 -0
  23. package/bin/dev-ports.mjs +567 -0
  24. package/bin/dev-server-binding.mjs +35 -0
  25. package/bin/dev-server-ops.mjs +1086 -0
  26. package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
  27. package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
  28. package/bin/dev-ui/OFL.txt +92 -0
  29. package/bin/dev-ui/agent-robot.webp +0 -0
  30. package/bin/dev-ui/app.js +5217 -0
  31. package/bin/dev-ui/highlight.js +195 -0
  32. package/bin/dev-ui/index.html +34 -0
  33. package/bin/dev-ui/style.css +3640 -0
  34. package/bin/devlint.mjs +112 -0
  35. package/bin/devserver.mjs +2127 -0
  36. package/bin/devtriggers.mjs +367 -0
  37. package/bin/endpoints.mjs +156 -0
  38. package/bin/errors.mjs +61 -0
  39. package/bin/files.mjs +169 -0
  40. package/bin/horizontal-capabilities/v1/contract.json +280 -0
  41. package/bin/http.mjs +500 -0
  42. package/bin/lint-manifests/justbash-commands.json +88 -0
  43. package/bin/lint-manifests/python-stdlib.json +295 -0
  44. package/bin/login-page.mjs +488 -0
  45. package/bin/schedules.mjs +664 -0
  46. package/bin/server-sandbox.mjs +204 -0
  47. package/bin/servicedev.mjs +425 -0
  48. package/bin/sync.mjs +357 -0
  49. package/bin/terminus.js +3666 -0
  50. package/bin/toolchain.mjs +125 -0
  51. package/bin/vendor/app-runtime-v1/app-host.json +124 -0
  52. package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
  53. package/bin/vendor/app-runtime-v1/doors.json +2867 -0
  54. package/bin/vendor/appd/node-harness.mjs +209 -0
  55. package/bin/vendor/appd/python-harness.py +12 -0
  56. package/bin/vendor/appd/server-protocol.json +84 -0
  57. package/bin/vendor/where.mjs +541 -0
  58. package/bin/versioning.mjs +72 -0
  59. package/bin/write-rules.mjs +398 -0
  60. package/package.json +41 -0
@@ -0,0 +1,1194 @@
1
+ /**
2
+ * Every `terminus` command, declared once: its group, one-line summary,
3
+ * usage, options, and examples. The help screens are rendered from this
4
+ * table and `parseFlags` enforces the same option lists (COMMAND_FLAGS), so
5
+ * help and behaviour cannot drift apart. Pure data plus string rendering:
6
+ * this module imports nothing.
7
+ *
8
+ * Options: `{ flag, value?, help, hidden? }`. A `value` placeholder makes it a
9
+ * string option; without one it is a boolean switch. Hidden options parse
10
+ * but never print (a transport detail: the web app a sign-in opens).
11
+ */
12
+ import { DEFAULT_DEV_MEMBER } from "./dev-members.mjs";
13
+
14
+ export const DOCS_URL = "https://www.terminus.build/docs/";
15
+
16
+ const GROUPS = [
17
+ ["account", "Account"],
18
+ ["explore", "Explore"],
19
+ ["develop", "Developing"],
20
+ ["skills", "Install Skills"],
21
+ ["manage", "Management"],
22
+ ["operate", "Your published apps"],
23
+ ["releases", "Releases"],
24
+ ["meta", "Help"],
25
+ ];
26
+
27
+ const JSON_OPTION = { flag: "json", help: "Print the result as JSON" };
28
+
29
+ const SKILL_USE_OPTIONS = [
30
+ { flag: "select", value: "n", help: "Pick result <n> without asking" },
31
+ { flag: "use-first", help: "Pick the first result" },
32
+ { flag: "search", help: "Search even when the argument looks like an address" },
33
+ { flag: "limit", value: "n", help: "How many results to choose from (default: 5)" },
34
+ JSON_OPTION,
35
+ ];
36
+
37
+ const SKILL_FETCH_OPTIONS = [
38
+ { flag: "refresh", help: "Download again even if the cache is current" },
39
+ JSON_OPTION,
40
+ ];
41
+
42
+ /** Accepted by every command. Only --api-base prints; the web app a sign-in
43
+ * opens is a transport detail documented under Environment. A token never
44
+ * rides the command line (where the shell history and `ps` would keep it):
45
+ * TERMINUS_TOKEN is how automation passes one. */
46
+ export const GLOBAL_OPTIONS = [
47
+ { flag: "api-base", value: "url", help: "Use another Terminus API (default: https://api.terminus.build)" },
48
+ { flag: "web-base", value: "url", help: "Terminus web app used for browser sign-in", hidden: true },
49
+ ];
50
+
51
+ export const COMMANDS = [
52
+ // ── Account ──────────────────────────────────────────────────────────
53
+ {
54
+ name: "login",
55
+ group: "account",
56
+ summary: "Sign in to Terminus in your browser",
57
+ usage: ["terminus login [--no-browser]"],
58
+ about:
59
+ "Opens the Terminus sign-in page and waits for you to approve this device. "
60
+ + "The session lasts seven days; you can see and revoke it on the web in "
61
+ + "System Settings → General → Devices. If you are already signed in, login "
62
+ + "just says who you are.",
63
+ options: [
64
+ { flag: "no-browser", help: "Print the sign-in link instead of opening a browser" },
65
+ { flag: "web-base", value: "url", help: "Sign in through a local web app (with --api-base)" },
66
+ ],
67
+ },
68
+ {
69
+ name: "logout",
70
+ group: "account",
71
+ summary: "Sign out and revoke this device's session",
72
+ usage: ["terminus logout"],
73
+ options: [],
74
+ },
75
+ {
76
+ name: "account",
77
+ group: "account",
78
+ summary: "Your account's station",
79
+ usage: ["terminus account [--json]"],
80
+ about:
81
+ "Says hi by name and address, then counts what is waiting for you: "
82
+ + "unread notifications and invitations (what the notification box on "
83
+ + "the web holds) and the creations you have published. Exits with "
84
+ + "status 3 when you are not signed in, so scripts can check.",
85
+ options: [JSON_OPTION],
86
+ },
87
+ {
88
+ name: "notifications",
89
+ group: "account",
90
+ summary: "Check your notifications",
91
+ usage: ["terminus notifications [--all] [--limit <n>]"],
92
+ about:
93
+ "What the notification box on the web holds: invitations to collaborate "
94
+ + "or to join a chat, then unread notifications, newest first. Reading "
95
+ + "them here does not mark them read.",
96
+ options: [
97
+ { flag: "all", help: "Include notifications you have already read" },
98
+ { flag: "limit", value: "n", help: "How many notifications to show (default: 20)" },
99
+ JSON_OPTION,
100
+ ],
101
+ },
102
+ {
103
+ name: "creations",
104
+ group: "manage",
105
+ summary: "Manage your published creations",
106
+ usage: ["terminus creations [--json]"],
107
+ about:
108
+ "Your published apps, agents, services, and skills with their addresses "
109
+ + "and live versions, as Creations on the web lists them; any that "
110
+ + "Terminus has suspended are listed apart. Drafts are in terminus drafts.",
111
+ options: [JSON_OPTION],
112
+ },
113
+ {
114
+ name: "drafts",
115
+ group: "manage",
116
+ summary: "Manage your drafts",
117
+ usage: ["terminus drafts [--json]"],
118
+ about:
119
+ "Your apps, agents, services, and skills that have not been published "
120
+ + "yet. terminus clone <address> gets you a copy to work in and terminus "
121
+ + "push sends changes back; each is published from its page on the web.",
122
+ options: [JSON_OPTION],
123
+ },
124
+
125
+ // ── Developing ──────────────────────────────────────────────────────
126
+ {
127
+ name: "init",
128
+ group: "develop",
129
+ summary: "Create a new app, agent, service, or skill",
130
+ usage: [
131
+ "terminus init app|agent|service|skill [<dir>] [<options>]",
132
+ "terminus init [<dir>] --kind <kind> [<options>]",
133
+ ],
134
+ about:
135
+ "Scaffolds a package in <dir> (default: the current folder). Without a kind, "
136
+ + "init creates a skill. In a folder cloned from an empty creation it fills "
137
+ + "the folder in and keeps its link.",
138
+ options: [
139
+ { flag: "kind", value: "kind", help: "app, agent, service, or skill (same as the first argument)" },
140
+ { flag: "template", value: "name", help: "App starter: vanilla (default), react, or svelte" },
141
+ { flag: "name", value: "name", help: "Package name (default: the folder name)" },
142
+ { flag: "description", value: "text", help: "One-line description (skills)" },
143
+ { flag: "force", help: "Overwrite an existing terminus.json or SKILL.md" },
144
+ { flag: "json", help: "Print the result as JSON (skills)" },
145
+ ],
146
+ examples: [
147
+ "terminus init app my-app --template react",
148
+ "terminus init agent support-bot",
149
+ "terminus init skill slides --description \"Build slide decks from notes\"",
150
+ ],
151
+ },
152
+ {
153
+ name: "clone",
154
+ group: "develop",
155
+ summary: "Copy a creation into a new folder",
156
+ usage: ["terminus clone <address> [<dir>] [--template <name>]"],
157
+ about:
158
+ "Downloads the current draft of your own app, agent, service, or skill into "
159
+ + "<dir> (default: its slug), linked to it and ready to develop and push. "
160
+ + "Creations are made on the web, which gives each its address. A creation "
161
+ + "with nothing in it yet is started from a template on the spot — its kind "
162
+ + "was chosen when it was made, so the folder arrives ready to run instead "
163
+ + "of empty. Someone else's open-source creation clones read-only — its "
164
+ + "latest release, updated with pull, never pushed; to make changes you can "
165
+ + "publish, use terminus fork.",
166
+ options: [
167
+ { flag: "template", value: "name", help: "App starter for an empty creation: vanilla (default), react, or svelte" },
168
+ JSON_OPTION,
169
+ ],
170
+ examples: [
171
+ "terminus clone @you/my-app",
172
+ "terminus clone @you/my-app --template react",
173
+ ],
174
+ },
175
+ {
176
+ name: "fork",
177
+ group: "develop",
178
+ summary: "Fork someone's open-source creation and clone it",
179
+ usage: [
180
+ "terminus fork <address> [<dir>] [--version <version>] [--slug <slug>]",
181
+ "terminus fork <address> --no-clone",
182
+ ],
183
+ about:
184
+ "Makes the fork on Terminus, the way GitHub's Fork does: a draft of the "
185
+ + "same kind at @you/<slug> in your Creations, open source, with \"Forked "
186
+ + "from\" on its pages. Then clones it into <dir>, ready to push. One fork "
187
+ + "per account: forking again answers with yours. Only open-source apps, "
188
+ + "agents, and skills fork, and a fork with a price is bought on the web.",
189
+ options: [
190
+ { flag: "no-clone", help: "Make the fork on Terminus, but do not clone it" },
191
+ { flag: "version", value: "version", help: "Fork this release, by number or version, like 1.2.0 (default: the latest)" },
192
+ { flag: "slug", value: "slug", help: "Slug for your fork (apps and agents; default: the original's)" },
193
+ JSON_OPTION,
194
+ ],
195
+ examples: [
196
+ "terminus fork @acme/ledger",
197
+ "terminus fork @acme/ledger --slug my-ledger",
198
+ "terminus fork @acme/ledger --version 1.2.0",
199
+ "terminus fork @acme/ledger --no-clone",
200
+ ],
201
+ },
202
+ {
203
+ name: "remote",
204
+ group: "develop",
205
+ summary: "Show or change the creation a folder is connected to",
206
+ usage: [
207
+ "terminus remote [<dir>]",
208
+ "terminus remote add <address> [<dir>]",
209
+ "terminus remote remove [<dir>]",
210
+ ],
211
+ about:
212
+ "The creation a folder belongs to, the way git remote is a repository's. "
213
+ + "On its own it says which creation that is and which commit the folder "
214
+ + "matches, reading only what the folder holds. add records it — its "
215
+ + "address in terminus.json, or its id in a skill's SKILL.md — and "
216
+ + "uploads nothing; make the creation on the web first, which is what "
217
+ + "gives it the address. remove disconnects the folder and leaves every "
218
+ + "file where it is.",
219
+ options: [JSON_OPTION],
220
+ examples: ["terminus remote", "terminus remote add @you/my-app"],
221
+ subcommands: [
222
+ {
223
+ name: "add",
224
+ summary: "Connect this folder to a creation",
225
+ usage: ["terminus remote add <address> [<dir>]"],
226
+ options: [JSON_OPTION],
227
+ examples: ["terminus remote add @you/my-app"],
228
+ },
229
+ {
230
+ name: "remove",
231
+ summary: "Disconnect this folder, keeping its files",
232
+ usage: ["terminus remote remove [<dir>]"],
233
+ options: [JSON_OPTION],
234
+ },
235
+ ],
236
+ },
237
+ {
238
+ name: "dev",
239
+ group: "develop",
240
+ summary: "Run a package on this machine",
241
+ usage: [
242
+ "terminus dev [<dir>] [<options>]",
243
+ "terminus dev [<dir>] --remote",
244
+ "terminus dev [<dir>] --prompt <text> [--json]",
245
+ ],
246
+ about:
247
+ "What runs depends on the package in <dir> (default: the current folder):\n"
248
+ + " app the built bundle on a local runtime with fixture data\n"
249
+ + " (run npm run build first)\n"
250
+ + " agent a chat page in your browser; the agent's tools run in a\n"
251
+ + " sandbox on this machine\n"
252
+ + " service the package's dev/ test page, calling your deployed endpoint\n"
253
+ + "\n"
254
+ + "An app is served the same runtime it is served in production — the same "
255
+ + "doors, the same SDK — against fixture data kept in .terminus/dev/, so "
256
+ + "nothing you do here touches anyone's real data. It survives restarts, "
257
+ + "and --fresh starts it empty again.\n"
258
+ + "\n"
259
+ + "--members runs the app as several people at once. Each gets a port of "
260
+ + "their own (8868, 8869, and up), a name, and their own data, and they "
261
+ + "share one live event bus: open two of the ports side by side and watch "
262
+ + "a message, an invitation, or an edit cross between them. That is how "
263
+ + "you develop anything people do together, before it is published.\n"
264
+ + "\n"
265
+ + "--guest adds one more port, after the members': a visitor who is not "
266
+ + "signed in, as an app open to guests meets one. The app gets a guest's "
267
+ + "bootstrap there, its own files and the capability assets it declares, "
268
+ + "and every other door answers 401. Its sign-in (session.signIn()) opens "
269
+ + "a page that picks a member, and the port is that member until it signs "
270
+ + "out — the same origin throughout.\n"
271
+ + "\n"
272
+ + "With --remote, an app uses your real data on Terminus and an agent runs "
273
+ + "on the platform's engine; run it before publishing an agent that runs code.",
274
+ options: [
275
+ { flag: "port", value: "n", help: "Serve on this port; with --members, the first of a consecutive run" },
276
+ { flag: "no-open", help: "Do not open the browser (agents, services)" },
277
+ { flag: "remote", help: "Run against Terminus instead of this machine (apps, agents)" },
278
+ { flag: "members", value: "n|names", help: "Simulate several people: 3, or alice,bob (apps)" },
279
+ { flag: "guest", help: "Add a signed-out visitor's port after the members' (apps)" },
280
+ { flag: "profiles", value: "file", help: "Names and avatars for simulated members, as JSON (apps)" },
281
+ { flag: "fresh", help: "Start from empty fixture data (apps)" },
282
+ { flag: "prompt", value: "text", help: "Send one message, print the reply, and exit (agents)" },
283
+ { flag: "resume", value: "id", help: "Continue this conversation with --prompt (agents)" },
284
+ { flag: "trigger", value: "name", help: "Fire one of the agent's declared triggers and exit (agents)" },
285
+ { flag: "payload", value: "file", help: "JSON payload file for --trigger (agents)" },
286
+ { flag: "model", value: "model", help: "Override the agent's model (agents)" },
287
+ { flag: "sandbox", value: "mode", help: "Agents' sandbox: workspace-write, workspace-write-net, off" },
288
+ { flag: "agentd", value: "path", help: "Use this terminus-agentd binary (agents)" },
289
+ { flag: "json", help: "With --prompt or --trigger, print each event as one line of JSON" },
290
+ ],
291
+ examples: [
292
+ "npm run build && terminus dev",
293
+ "npm run build && terminus dev --members 3",
294
+ "terminus dev --members alice,bob --port 4000",
295
+ "terminus dev --members 2 --guest",
296
+ "terminus dev ./support-bot --prompt \"Summarize today's inbox\" --json",
297
+ ],
298
+ },
299
+ {
300
+ name: "build",
301
+ group: "develop",
302
+ summary: "Build an app's browser bundle",
303
+ usage: ["terminus build [<dir>]"],
304
+ about:
305
+ "Runs the build command configured in terminus.json, or copies ui/ to dist/ "
306
+ + "when there is none. push builds automatically; use this to check the "
307
+ + "build on its own.",
308
+ options: [JSON_OPTION],
309
+ examples: [
310
+ "terminus build",
311
+ "terminus build ./my-app",
312
+ ],
313
+ },
314
+ {
315
+ name: "validate",
316
+ group: "develop",
317
+ summary: "Check a package for problems before pushing",
318
+ usage: ["terminus validate [<dir>]"],
319
+ about:
320
+ "Checks an app, agent, or service package (terminus.json) or a skill "
321
+ + "(SKILL.md) the way Terminus will when you push. Works offline.",
322
+ options: [JSON_OPTION],
323
+ examples: [
324
+ "terminus validate",
325
+ ],
326
+ },
327
+ {
328
+ name: "status",
329
+ group: "develop",
330
+ summary: "Compare your working copy with its draft and live release",
331
+ usage: ["terminus status [<dir>]"],
332
+ about:
333
+ "Like git status: whether the draft has commits your copy hasn't pulled, "
334
+ + "which files you changed since it last matched, and anything a pull "
335
+ + "left for you to resolve. To see which account you are signed in as, "
336
+ + "use terminus account.",
337
+ options: [JSON_OPTION],
338
+ examples: [
339
+ "terminus status",
340
+ "terminus status ./my-app --json",
341
+ ],
342
+ },
343
+ {
344
+ name: "diff",
345
+ group: "develop",
346
+ summary: "Show your changes, or compare two releases",
347
+ usage: [
348
+ "terminus diff",
349
+ "terminus diff <address> [--from <version>] [--to <version>]",
350
+ ],
351
+ about:
352
+ "In a package folder, diff shows what you changed since the folder last "
353
+ + "matched its draft — what a push would send — the way git diff does. "
354
+ + "With an address it compares two published releases, each named by its "
355
+ + "version (1.2.0) or its release number: without --to, the latest with "
356
+ + "the one before it.",
357
+ options: [
358
+ { flag: "from", value: "version", help: "The older release (default: the one before --to)" },
359
+ { flag: "to", value: "version", help: "The newer release (default: the latest)" },
360
+ JSON_OPTION,
361
+ ],
362
+ examples: [
363
+ "terminus diff",
364
+ "terminus diff @acme/ledger --from 1.0.0 --to 1.1.0",
365
+ ],
366
+ },
367
+ {
368
+ name: "pull",
369
+ group: "develop",
370
+ summary: "Merge the draft's changes into your working copy",
371
+ usage: ["terminus pull [<dir>] [--force]"],
372
+ about:
373
+ "Brings in the draft's commits since your copy last matched it, the way "
374
+ + "git pull does: a file only the draft changed is taken, one only you "
375
+ + "changed is kept, and one both changed is merged line by line — "
376
+ + "terminus.json key by key. What cannot be merged is marked in the file "
377
+ + "and named; resolve it, then push. In a read-only copy, pull brings in "
378
+ + "newer releases.",
379
+ options: [
380
+ { flag: "force", help: "Replace your local files with the draft's instead of merging" },
381
+ JSON_OPTION,
382
+ ],
383
+ examples: [
384
+ "terminus pull",
385
+ "terminus pull --force",
386
+ ],
387
+ },
388
+ {
389
+ name: "push",
390
+ group: "develop",
391
+ summary: "Upload your working copy to its online draft",
392
+ usage: ["terminus push [<dir>] [--message <text>]"],
393
+ about:
394
+ "Updates the draft of the creation this folder is linked to — an app, "
395
+ + "agent, service, or skill — building an app first when it has a build "
396
+ + "step. Each push that changes something is one entry in the draft's "
397
+ + "history. What people are using never moves: push never publishes, and "
398
+ + "a creation is published from its page on the web, which push prints.",
399
+ options: [
400
+ { flag: "message", value: "text", help: "Say what changed; the draft's history shows it" },
401
+ { flag: "force", help: "Replace draft commits your copy hasn't pulled" },
402
+ JSON_OPTION,
403
+ ],
404
+ examples: [
405
+ "terminus push",
406
+ "terminus push --message \"Rooms can be renamed\"",
407
+ ],
408
+ },
409
+ {
410
+ name: "log",
411
+ group: "develop",
412
+ summary: "Show a draft's history, or a creation's releases",
413
+ usage: [
414
+ "terminus log [--limit <n>]",
415
+ "terminus log <address>",
416
+ ],
417
+ about:
418
+ "In a package folder, log lists its draft's commits, newest first — "
419
+ + "every push and every save on the web, with who made it, the message, "
420
+ + "and the versions published from it. With an address it lists that "
421
+ + "creation's releases.",
422
+ options: [
423
+ { flag: "limit", value: "n", help: "How many commits to show (default: 20)" },
424
+ JSON_OPTION,
425
+ ],
426
+ examples: [
427
+ "terminus log",
428
+ "terminus log @acme/ledger",
429
+ ],
430
+ },
431
+ {
432
+ name: "restore",
433
+ group: "develop",
434
+ summary: "Put the draft back the way it was at an earlier commit",
435
+ usage: ["terminus restore <commit> [<dir>] [--message <text>]"],
436
+ about:
437
+ "Restores the files, and the settings they compile to, of a commit "
438
+ + "terminus log shows. The restore is a new commit, like git revert: "
439
+ + "nothing is rewritten, and it can be undone the same way. A folder that "
440
+ + "matched the draft follows it; one with changes of its own pulls later.",
441
+ options: [
442
+ { flag: "message", value: "text", help: "Say why; the draft's history shows it" },
443
+ JSON_OPTION,
444
+ ],
445
+ examples: ["terminus log", "terminus restore 1a2b3c4d"],
446
+ },
447
+ {
448
+ name: "logs",
449
+ group: "develop",
450
+ summary: "Show a published app's recent logs",
451
+ usage: ["terminus logs <app> [--level <level>] [--limit <n>]"],
452
+ about: "<app> is the id, slug, or @handle/slug of an app you maintain.",
453
+ options: [
454
+ { flag: "level", value: "level", help: "Only entries at this level, such as error" },
455
+ { flag: "limit", value: "n", help: "How many entries to show" },
456
+ JSON_OPTION,
457
+ ],
458
+ examples: [
459
+ "terminus logs @you/my-app",
460
+ ],
461
+ },
462
+ {
463
+ name: "secrets",
464
+ group: "develop",
465
+ summary: "Manage sealed credentials for your package",
466
+ usage: ["terminus secrets list|set|delete [<name>] [<dir>]"],
467
+ about:
468
+ "Secrets are used by the outbound calls your linked package declares. "
469
+ + "Values are read from stdin (or $TERMINUS_SECRET_VALUE), never from the "
470
+ + "command line, and can never be read back.",
471
+ options: [JSON_OPTION],
472
+ subcommands: [
473
+ { name: "list", summary: "List the secrets that are set", usage: ["terminus secrets list [<dir>]"], options: [JSON_OPTION] },
474
+ {
475
+ name: "set",
476
+ summary: "Set or replace a secret (value from stdin)",
477
+ usage: ["terminus secrets set <name> [<dir>]"],
478
+ options: [JSON_OPTION],
479
+ examples: ["pbpaste | terminus secrets set OPENAI_API_KEY"],
480
+ },
481
+ { name: "delete", summary: "Delete a secret", usage: ["terminus secrets delete <name> [<dir>]"], options: [JSON_OPTION] },
482
+ ],
483
+ },
484
+ {
485
+ name: "data",
486
+ group: "develop",
487
+ summary: "Inspect, export, or import local dev data",
488
+ usage: ["terminus data inspect|export|import [<arguments>] [<options>]"],
489
+ about:
490
+ "Works on the fixture data terminus dev keeps in .terminus/dev/ for "
491
+ + `each simulated member (default member: ${DEFAULT_DEV_MEMBER}).`,
492
+ options: [],
493
+ subcommands: [
494
+ {
495
+ name: "inspect",
496
+ summary: "Summarize a member's local data",
497
+ usage: ["terminus data inspect [<dir>] [--member <name>] [--verify]"],
498
+ options: [
499
+ { flag: "member", value: "name", help: `Simulated member (default: ${DEFAULT_DEV_MEMBER})` },
500
+ { flag: "verify", help: "Also verify every stored object" },
501
+ JSON_OPTION,
502
+ ],
503
+ },
504
+ {
505
+ name: "export",
506
+ summary: "Copy a member's local data to a folder",
507
+ usage: ["terminus data export [<dir>] [<output-dir>] [--member <name>]"],
508
+ options: [
509
+ { flag: "member", value: "name", help: `Simulated member (default: ${DEFAULT_DEV_MEMBER})` },
510
+ { flag: "output", value: "dir", help: "Where to write the export (same as <output-dir>)" },
511
+ JSON_OPTION,
512
+ ],
513
+ },
514
+ {
515
+ name: "import",
516
+ summary: "Replace a member's local data from a folder",
517
+ usage: ["terminus data import [<dir>] <export-dir> [--member <name>] [--force]"],
518
+ about: "<export-dir> is a folder terminus data export wrote.",
519
+ options: [
520
+ { flag: "member", value: "name", help: `Simulated member (default: ${DEFAULT_DEV_MEMBER})` },
521
+ { flag: "force", help: "Replace existing data (a backup is kept)" },
522
+ JSON_OPTION,
523
+ ],
524
+ },
525
+ ],
526
+ },
527
+ {
528
+ name: "service",
529
+ group: "develop",
530
+ summary: "Inspect services, test operations, and run jobs",
531
+ usage: ["terminus service <command> [<arguments>] [<options>]"],
532
+ options: [],
533
+ subcommands: [
534
+ {
535
+ name: "inspect",
536
+ summary: "Show a service's operations and pricing",
537
+ usage: ["terminus service inspect <service>"],
538
+ options: [JSON_OPTION],
539
+ },
540
+ {
541
+ name: "test",
542
+ summary: "Call an operation of your local service package",
543
+ usage: ["terminus service test <dir> <operation> [<options>]"],
544
+ about:
545
+ "Uploads the package as your draft, then calls the operation through "
546
+ + "Terminus. Send a body with --file or --input.",
547
+ options: [
548
+ { flag: "file", value: "path", help: "Send this file as the request body" },
549
+ { flag: "input", value: "json", help: "Send this JSON as the request body" },
550
+ { flag: "query", value: "json", help: "Query parameters, as a JSON object" },
551
+ JSON_OPTION,
552
+ ],
553
+ examples: ["terminus service test . convert-document --file ./sample.docx"],
554
+ },
555
+ {
556
+ name: "submit",
557
+ summary: "Start a job on a published service",
558
+ usage: ["terminus service submit <service-id> <operation> [<options>]"],
559
+ about: "Needs --idempotency-key and exactly one of --input or --file.",
560
+ options: [
561
+ { flag: "input", value: "json", help: "The job's input" },
562
+ { flag: "file", value: "path", help: "Read the job's input from a JSON file" },
563
+ { flag: "idempotency-key", value: "key", help: "Any unique string; resubmitting it returns the same job" },
564
+ JSON_OPTION,
565
+ ],
566
+ },
567
+ {
568
+ name: "job",
569
+ summary: "Show a job's status and output",
570
+ usage: ["terminus service job <job-id> [--wait]"],
571
+ options: [
572
+ { flag: "wait", help: "Keep checking until the job finishes (up to 5 minutes)" },
573
+ JSON_OPTION,
574
+ ],
575
+ },
576
+ {
577
+ name: "jobs",
578
+ summary: "List your recent jobs",
579
+ usage: ["terminus service jobs [--service-id <uuid>]"],
580
+ options: [
581
+ { flag: "service-id", value: "uuid", help: "Only jobs for this service" },
582
+ JSON_OPTION,
583
+ ],
584
+ },
585
+ {
586
+ name: "cancel",
587
+ summary: "Cancel a queued or running job",
588
+ usage: ["terminus service cancel <job-id>"],
589
+ options: [JSON_OPTION],
590
+ },
591
+ {
592
+ name: "save",
593
+ summary: "Save a job's output file to your Terminus home folder",
594
+ usage: ["terminus service save <job-id> <file-id> <home-path> [<options>]"],
595
+ options: [
596
+ { flag: "expected-sha256", value: "hash", help: "Refuse to save unless the file has this hash" },
597
+ JSON_OPTION,
598
+ ],
599
+ },
600
+ ],
601
+ },
602
+ {
603
+ name: "connectors",
604
+ group: "develop",
605
+ summary: "List the connectors your agents can use",
606
+ usage: ["terminus connectors [<slug>]"],
607
+ about:
608
+ "Without a slug, lists each connector and the tool ids to declare in an "
609
+ + "agent's terminus.json. With a slug, shows its operations and the scopes "
610
+ + "they need.",
611
+ options: [JSON_OPTION],
612
+ examples: ["terminus connectors gmail"],
613
+ },
614
+
615
+ // ── Your published apps ─────────────────────────────────────────────
616
+ {
617
+ name: "inspect",
618
+ group: "operate",
619
+ summary: "Show a published app's runtime state",
620
+ usage: ["terminus inspect <app>"],
621
+ about: "<app> is the id, slug, or @handle/slug of an app you maintain.",
622
+ options: [JSON_OPTION],
623
+ },
624
+
625
+ // ── Releases ────────────────────────────────────────────────────────
626
+ {
627
+ name: "outdated",
628
+ group: "releases",
629
+ summary: "Check a creation's dependencies for newer releases",
630
+ usage: ["terminus outdated <address>"],
631
+ about:
632
+ "Lists the services, skills, and agents a published creation's release "
633
+ + "names, each with the version it pins and whether a newer release is "
634
+ + "out. Dependencies are recorded when a release is published.",
635
+ options: [JSON_OPTION],
636
+ examples: ["terminus outdated @you/my-app"],
637
+ },
638
+
639
+ // ── Skills ──────────────────────────────────────────────────────────
640
+ {
641
+ name: "search",
642
+ group: "explore",
643
+ summary: "Search the catalog",
644
+ usage: ["terminus search <query> [--kind <kind>] [--limit <n>]"],
645
+ about:
646
+ "Matches apps, agents, services, and skills by name and description, "
647
+ + "best match first.",
648
+ options: [
649
+ { flag: "kind", value: "kind", help: "Only skill, service, app, or agent results" },
650
+ { flag: "limit", value: "n", help: "How many results to show (default: 10)" },
651
+ JSON_OPTION,
652
+ ],
653
+ examples: ["terminus search \"browser automation\" --kind skill"],
654
+ },
655
+ {
656
+ name: "skills",
657
+ group: "skills",
658
+ summary: "Install and use skills in Claude Code or Codex",
659
+ usage: ["terminus skills <command> [<arguments>] [<options>]"],
660
+ options: [],
661
+ // Bare `terminus skills` (like `terminus help skills`) shows every skill
662
+ // command as a full line with what it does, the way `terminus help` shows
663
+ // the rest; `terminus help skills <command>` has the options. Tested like
664
+ // HELP_SECTIONS.
665
+ sections: [
666
+ ["Install Skills", [
667
+ ["terminus skills install <skill> --agent claude", "Add a skill to Claude Code"],
668
+ ["terminus skills install <skill> --agent codex", "Add a skill to Codex"],
669
+ ["terminus skills list", "List your installed skills"],
670
+ ["terminus skills update", "Update your installed skills"],
671
+ ["terminus skills uninstall <skill>", "Remove an installed skill"],
672
+ ]],
673
+ ["Use Skills", [
674
+ ["terminus skills use <skill>", "Load a skill for your agent"],
675
+ ["terminus skills use \"make a slide deck\"", "Find a skill for your task"],
676
+ ["terminus skills fetch <skill>", "Download a skill's files"],
677
+ ["terminus skills files <skill>", "List a skill's files"],
678
+ ["terminus skills file <skill> <path>", "Print one of a skill's files"],
679
+ ]],
680
+ ],
681
+ subcommands: [
682
+ {
683
+ name: "install",
684
+ summary: "Add a skill to Claude Code or Codex",
685
+ usage: ["terminus skills install <skill> [--agent <agent>] [<options>]"],
686
+ about:
687
+ "Writes a small pointer that loads the skill from Terminus each time it "
688
+ + "is used, so it stays current and every use counts. --agent picks the "
689
+ + "coding agent: claude, codex, or both (default: the agent you are "
690
+ + "running in, else claude). --local copies the whole package instead. "
691
+ + "Only open-source skills install: one whose content is closed is used "
692
+ + "on Terminus itself.",
693
+ options: [
694
+ { flag: "agent", value: "agent", help: "claude, codex, or both" },
695
+ { flag: "global", help: "Install for your user instead of this project" },
696
+ { flag: "dir", value: "dir", help: "Install into this folder instead" },
697
+ { flag: "local", help: "Copy the full package" },
698
+ { flag: "force", help: "Replace an existing install" },
699
+ JSON_OPTION,
700
+ ],
701
+ examples: [
702
+ "terminus skills install @zarazhangrui/frontend-slides --agent claude",
703
+ "terminus skills install @zarazhangrui/frontend-slides --agent codex --global",
704
+ ],
705
+ },
706
+ {
707
+ name: "list",
708
+ summary: "List the skills you have installed",
709
+ usage: ["terminus skills list"],
710
+ about:
711
+ "Every skill terminus installed, in this project and in your home "
712
+ + "folder, for Claude Code and Codex.",
713
+ options: [
714
+ { flag: "dir", value: "dir", help: "Only look in this folder" },
715
+ JSON_OPTION,
716
+ ],
717
+ },
718
+ {
719
+ name: "update",
720
+ summary: "Update the skills you have installed",
721
+ usage: ["terminus skills update [<skill>]"],
722
+ about:
723
+ "Checks every skill terminus installed, in this project and in your "
724
+ + "home folder, against Terminus. A pointer already loads the latest "
725
+ + "instructions each time it runs, so update refreshes the name, "
726
+ + "description, and address it carries. A --local copy downloads the "
727
+ + "new version, which counts as a use. Name a skill to update only that "
728
+ + "one.",
729
+ options: [
730
+ { flag: "dir", value: "dir", help: "Only look in this folder" },
731
+ JSON_OPTION,
732
+ ],
733
+ },
734
+ {
735
+ name: "uninstall",
736
+ summary: "Remove an installed skill",
737
+ usage: ["terminus skills uninstall <skill>"],
738
+ about: "Removes every install terminus made of the skill, in this project and in your home folder.",
739
+ options: [
740
+ { flag: "dir", value: "dir", help: "Only look in this folder" },
741
+ JSON_OPTION,
742
+ ],
743
+ },
744
+ {
745
+ name: "use",
746
+ summary: "Load a skill's instructions for your coding agent",
747
+ usage: ["terminus skills use <skill|query>"],
748
+ about:
749
+ "Given an address or id, loads that skill. Given words, searches and asks "
750
+ + "you to pick one. Each use is counted on your account. Installed skills "
751
+ + "run this when they trigger.",
752
+ options: SKILL_USE_OPTIONS,
753
+ examples: [
754
+ "terminus skills use @zarazhangrui/frontend-slides",
755
+ "terminus skills use \"make a slide deck\"",
756
+ "terminus skills use \"make a slide deck\" --use-first",
757
+ ],
758
+ },
759
+ {
760
+ name: "fetch",
761
+ summary: "Download a skill's files to the local cache",
762
+ usage: ["terminus skills fetch <skill> [--refresh]"],
763
+ about:
764
+ "Prints the cache folder. The files are reused until the skill changes; "
765
+ + "--refresh downloads them again.",
766
+ options: SKILL_FETCH_OPTIONS,
767
+ },
768
+ {
769
+ name: "files",
770
+ summary: "List a skill's files",
771
+ usage: ["terminus skills files <skill>"],
772
+ options: [JSON_OPTION],
773
+ },
774
+ {
775
+ name: "file",
776
+ summary: "Print one of a skill's files",
777
+ usage: ["terminus skills file <skill> <path>"],
778
+ options: [JSON_OPTION],
779
+ },
780
+ ],
781
+ },
782
+ // ── Help ────────────────────────────────────────────────────────────
783
+ {
784
+ name: "help",
785
+ group: "meta",
786
+ summary: "Show help for a command",
787
+ usage: ["terminus help [<command>] [<subcommand>]"],
788
+ options: [],
789
+ },
790
+ {
791
+ name: "commands",
792
+ group: "meta",
793
+ summary: "List every command",
794
+ usage: ["terminus commands"],
795
+ about:
796
+ "Every command with a one-line summary, then the options, environment "
797
+ + "variables, and exit codes all commands share. terminus help shows the "
798
+ + "everyday ones as examples.",
799
+ options: [],
800
+ },
801
+ {
802
+ name: "version",
803
+ group: "meta",
804
+ summary: "Print the CLI version",
805
+ usage: ["terminus version [--json]"],
806
+ about: "--json also names the @terminus-ai/app-sdk version new apps depend on.",
807
+ options: [JSON_OPTION],
808
+ },
809
+ ];
810
+
811
+ /** One name per command: what help lists is what runs, and nothing else. */
812
+ const BY_NAME = new Map(COMMANDS.map((command) => [command.name, command]));
813
+
814
+ export function commandNamed(name) {
815
+ return BY_NAME.get(name) ?? null;
816
+ }
817
+
818
+ export function subcommandNamed(command, name) {
819
+ return command?.subcommands?.find((sub) => sub.name === name) ?? null;
820
+ }
821
+
822
+ function flagSpec(options) {
823
+ const booleans = [];
824
+ const strings = [];
825
+ for (const option of options) {
826
+ (option.value ? strings : booleans).push(option.flag);
827
+ }
828
+ return { booleans: [...new Set(booleans)], strings: [...new Set(strings)] };
829
+ }
830
+
831
+ /** Accepted by every command, on top of its own options. */
832
+ export const COMMON_FLAGS = Object.freeze(flagSpec(GLOBAL_OPTIONS));
833
+
834
+ /** What `parseFlags(args, name)` accepts: a command's options plus every
835
+ * option of its subcommands (the verb is stripped before parsing). */
836
+ export const COMMAND_FLAGS = Object.freeze(Object.fromEntries(
837
+ COMMANDS.flatMap((command) => [
838
+ [
839
+ command.name,
840
+ Object.freeze(flagSpec([
841
+ ...command.options,
842
+ ...(command.subcommands ?? []).flatMap((sub) => sub.options ?? []),
843
+ ])),
844
+ ],
845
+ // "skills install": exactly that subcommand's options.
846
+ ...(command.subcommands ?? []).map((sub) => [
847
+ `${command.name} ${sub.name}`,
848
+ Object.freeze(flagSpec(sub.options ?? [])),
849
+ ]),
850
+ ]),
851
+ ));
852
+
853
+ /* ── Did-you-mean ─────────────────────────────────────────────────────── */
854
+
855
+ function editDistance(a, b) {
856
+ const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
857
+ for (let j = 1; j <= b.length; j += 1) rows[0][j] = j;
858
+ for (let i = 1; i <= a.length; i += 1) {
859
+ for (let j = 1; j <= b.length; j += 1) {
860
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
861
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
862
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
863
+ rows[i][j] = Math.min(rows[i][j], rows[i - 2][j - 2] + 1);
864
+ }
865
+ }
866
+ }
867
+ return rows[a.length][b.length];
868
+ }
869
+
870
+ /** The closest candidate within a small typo budget, or null. */
871
+ export function closest(word, candidates) {
872
+ const wanted = String(word ?? "").toLowerCase();
873
+ if (!wanted) return null;
874
+ const budget = wanted.length <= 4 ? 1 : 2;
875
+ let best = null;
876
+ let bestDistance = Infinity;
877
+ for (const candidate of candidates) {
878
+ const distance = candidate.startsWith(wanted) && wanted.length >= 3 ? 0.5 : editDistance(wanted, candidate);
879
+ if (distance < bestDistance) {
880
+ best = candidate;
881
+ bestDistance = distance;
882
+ }
883
+ }
884
+ return bestDistance <= budget ? best : null;
885
+ }
886
+
887
+ /** A word that is not a command: the nearest one it might be, and where the
888
+ * whole list is. A command this CLI no longer has is simply not a command —
889
+ * nothing shipped under the old name, so nothing has to be explained. */
890
+ export function unknownCommandMessage(name) {
891
+ const lines = [`terminus: '${name}' is not a terminus command.`];
892
+ const suggestion = closest(name, COMMANDS.filter((command) => !command.hidden).map((command) => command.name));
893
+ if (suggestion) lines.push(`Did you mean 'terminus ${suggestion}'?`);
894
+ lines.push("Run 'terminus commands' to see every command.");
895
+ return lines.join("\n");
896
+ }
897
+
898
+ /* ── Rendering ────────────────────────────────────────────────────────── */
899
+
900
+ const WIDTH = 80;
901
+
902
+ function wrap(text, indent = "", width = WIDTH) {
903
+ const out = [];
904
+ for (const paragraph of String(text).split("\n\n")) {
905
+ if (out.length) out.push("");
906
+ // Pre-formatted paragraphs (indented lists) keep their own line breaks.
907
+ if (/^ {2}/m.test(paragraph)) {
908
+ for (const line of paragraph.split("\n")) out.push(`${indent}${line}`);
909
+ continue;
910
+ }
911
+ let line = "";
912
+ for (const word of paragraph.replace(/\s+/g, " ").trim().split(" ")) {
913
+ if (line && indent.length + line.length + 1 + word.length > width) {
914
+ out.push(`${indent}${line}`);
915
+ line = word;
916
+ } else {
917
+ line = line ? `${line} ${word}` : word;
918
+ }
919
+ }
920
+ if (line) out.push(`${indent}${line}`);
921
+ }
922
+ return out;
923
+ }
924
+
925
+ /** Two aligned columns; a left cell too wide for the gutter puts its text on
926
+ * the next line instead of pushing every row right. */
927
+ function table(rows, { indent = " ", maxLeft = 24, width: fixed } = {}) {
928
+ const width = fixed ?? Math.min(maxLeft, Math.max(0, ...rows.map(([left]) => left.length)));
929
+ const pad = " ".repeat(indent.length + width + 2);
930
+ const out = [];
931
+ for (const [left, right] of rows) {
932
+ if (!right) {
933
+ out.push(`${indent}${left}`);
934
+ continue;
935
+ }
936
+ const text = wrap(right, "", WIDTH - pad.length);
937
+ const rest = text.slice(1).map((line) => `${pad}${line}`);
938
+ if (left.length > width) {
939
+ out.push(`${indent}${left}`, `${pad}${text[0] ?? ""}`, ...rest);
940
+ } else {
941
+ out.push(`${indent}${left.padEnd(width)} ${text[0] ?? ""}`, ...rest);
942
+ }
943
+ }
944
+ return out;
945
+ }
946
+
947
+ function optionLabel(option) {
948
+ return option.value ? `--${option.flag} <${option.value}>` : `--${option.flag}`;
949
+ }
950
+
951
+ function visible(options = []) {
952
+ return options.filter((option) => !option.hidden);
953
+ }
954
+
955
+ /** `terminus help`: the commands people reach for, grouped, each with what
956
+ * it does, every row on one line in one column (`terminus help <command>`
957
+ * has the options). <angle> words are placeholders and [ ] is optional.
958
+ * Every row is checked against this registry by the tests; `terminus
959
+ * commands` lists everything. */
960
+ export const HELP_SECTIONS = [
961
+ ["Account", [
962
+ ["terminus login", "Open your browser and sign in"],
963
+ ["terminus account", "Your account's station"],
964
+ ["terminus notifications", "Check your notifications"],
965
+ ]],
966
+ ["Explore", [
967
+ ["terminus search <query>", "Search the Terminus catalog"],
968
+ ]],
969
+ // In the order the work happens: start or connect a folder, run it, check
970
+ // it, compare it, push it, and watch it once it is published.
971
+ ["Developing", [
972
+ ["terminus init <kind> [<dir>]", "Start a new creation"],
973
+ ["terminus clone <address> [<dir>]", "Download a creation to edit"],
974
+ ["terminus remote add <address> [<dir>]", "Connect a folder to a creation"],
975
+ ["terminus dev [<dir>]", "Mock your creation locally"],
976
+ ["terminus validate [<dir>]", "Check a package for problems"],
977
+ ["terminus status [<dir>]", "Compare your copy with Terminus"],
978
+ ["terminus pull [<dir>]", "Merge the draft's changes into yours"],
979
+ ["terminus push [<dir>]", "Upload your changes as a draft"],
980
+ ["terminus logs <app>", "Show a published app's logs"],
981
+ ], [
982
+ {
983
+ // Two arcs, and only the arcs: get it, run it, push it. There is no
984
+ // list of keys above them any more — it named --members and --prompt,
985
+ // then the walkthroughs said both again in the one place they mean
986
+ // something, beside the run they change. A key read next to its own
987
+ // command needs no explaining, and the list was four lines of saying
988
+ // it twice. `status` is a question you ask, not a step you take, and
989
+ // it is one row up.
990
+ title: "Mock it locally so your coding agent can test end to end — an app:",
991
+ lines: [
992
+ "terminus clone @you/app && cd app",
993
+ "npm run build # apps are built before they run",
994
+ "terminus dev --members 3 # three at once; --fresh to start empty",
995
+ "terminus push --message \"Rooms can be renamed\"",
996
+ ],
997
+ },
998
+ {
999
+ title: "An agent, which has no build step:",
1000
+ lines: [
1001
+ "terminus clone @you/agent && cd agent",
1002
+ "terminus dev # a chat page; --remote for real data",
1003
+ "terminus dev --prompt \"Two lines on today's inbox\"",
1004
+ "terminus push --message \"Sharper summary\"",
1005
+ ],
1006
+ },
1007
+ {
1008
+ // What is left once every key has been shown in use: where to read the
1009
+ // ones that were not.
1010
+ // Not opened with "terminus ": every line that starts that way in this
1011
+ // screen is a row of the table above, with its explanation in the
1012
+ // shared column, and the overview test holds it to that.
1013
+ title: "Every other key, --port included, and what each kind runs: terminus help dev",
1014
+ lines: [],
1015
+ },
1016
+ ]],
1017
+ ["Install Skills", [
1018
+ ["terminus skills", "Install skills in Claude Code or Codex"],
1019
+ ]],
1020
+ ["Management", [
1021
+ ["terminus creations", "Manage your published creations"],
1022
+ ["terminus drafts", "Manage your drafts"],
1023
+ ]],
1024
+ ["Further help", [
1025
+ ["terminus commands", "List every command"],
1026
+ ["terminus help [<command>]", "Show how to use a command"],
1027
+ [DOCS_URL, ""],
1028
+ ]],
1029
+ ];
1030
+
1031
+ /** One command column for the whole screen, as wide as its longest row, so
1032
+ * every description starts at the same place. */
1033
+ const HELP_COLUMN = Math.max(
1034
+ ...HELP_SECTIONS.flatMap(([, rows]) => rows.filter(([, text]) => text).map(([usage]) => usage.length)),
1035
+ );
1036
+
1037
+ /** `version` puts "Terminus v0.0.1, created by Terminus Intelligence" on
1038
+ * top, the way bare `terminus` opens. */
1039
+ export function renderOverview({ version } = {}) {
1040
+ const body = HELP_SECTIONS
1041
+ .map(([title, rows, blocks = []]) => [
1042
+ `${title}:`,
1043
+ ...table(rows, { width: HELP_COLUMN }),
1044
+ ...blocks.flatMap((block) => [
1045
+ "",
1046
+ ` ${block.title}`,
1047
+ ...block.lines.map((line) => ` ${line}`),
1048
+ ]),
1049
+ ].join("\n"))
1050
+ .join("\n\n");
1051
+ return version ? `Terminus v${version}, created by Terminus Intelligence\n\n${body}` : body;
1052
+ }
1053
+
1054
+ /** `terminus commands`: every command, grouped, one line each, then what all
1055
+ * of them accept. */
1056
+ export function renderCommands() {
1057
+ const lines = ["Usage: terminus <command> [<arguments>] [<options>]"];
1058
+ // One name column for every group, so the summaries line up down the page.
1059
+ const width = Math.max(...COMMANDS.filter((command) => !command.hidden).map((command) => command.name.length));
1060
+ for (const [group, title] of GROUPS) {
1061
+ const commands = COMMANDS.filter((command) => command.group === group && !command.hidden);
1062
+ if (!commands.length) continue;
1063
+ lines.push("", `${title}:`, ...table(commands.map((command) => [command.name, command.summary]), { width }));
1064
+ }
1065
+ lines.push(
1066
+ "",
1067
+ "Options:",
1068
+ ...table([
1069
+ ["-h, --help", "Show help for terminus or a command"],
1070
+ ["-v, --version", "Print the CLI version"],
1071
+ ["--json", "Print machine-readable output (most commands)"],
1072
+ ...visible(GLOBAL_OPTIONS).map((option) => [optionLabel(option), option.help]),
1073
+ ]),
1074
+ "",
1075
+ "Environment:",
1076
+ ...table([
1077
+ ["TERMINUS_TOKEN", "Use this access token instead of your saved login"],
1078
+ ["TERMINUS_API_BASE", "Same as --api-base"],
1079
+ ]),
1080
+ "",
1081
+ "Exit codes:",
1082
+ " 0 success, 1 error, 2 bad usage, 3 not signed in, 69 Terminus unreachable,",
1083
+ " 75 rate limited, 77 not allowed. With --json, errors are printed to stderr",
1084
+ " as JSON.",
1085
+ "",
1086
+ "Run 'terminus help <command>' for details on a command.",
1087
+ `Docs: ${DOCS_URL}`,
1088
+ );
1089
+ return lines.join("\n");
1090
+ }
1091
+
1092
+ /** "Search the catalog." — help prose and option text read as sentences. */
1093
+ function sentence(text) {
1094
+ const trimmed = String(text ?? "").trim();
1095
+ return !trimmed || /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
1096
+ }
1097
+
1098
+ /** The summary leads the prose, joined to the first paragraph of `about`
1099
+ * unless that paragraph is a pre-formatted list. */
1100
+ function describe(entry) {
1101
+ const summary = sentence(entry.summary);
1102
+ if (!entry.about) return summary;
1103
+ const [first, ...rest] = entry.about.split("\n\n");
1104
+ if (/^ {2}/m.test(first)) return `${summary}\n\n${entry.about}`;
1105
+ return [`${summary} ${first}`, ...rest].join("\n\n");
1106
+ }
1107
+
1108
+ /** One command's help, the way `brew search --help` reads: usage, prose,
1109
+ * then every option (long-only flags indented past `-h, `). */
1110
+ function renderEntry(entry) {
1111
+ const lines = entry.usage.map((usage, index) => `${index ? " " : "Usage: "}${usage}`);
1112
+ lines.push("", ...wrap(describe(entry)));
1113
+ if (entry.sections?.length) {
1114
+ // Full command lines, sectioned like `terminus help`, in one column.
1115
+ const width = Math.max(...entry.sections.flatMap(([, rows]) => rows.map(([usage]) => usage.length)));
1116
+ for (const [title, rows] of entry.sections) {
1117
+ lines.push("", `${title}:`, ...table(rows, { width }));
1118
+ }
1119
+ } else if (entry.subcommands?.length) {
1120
+ lines.push(
1121
+ "",
1122
+ "Commands:",
1123
+ ...table(entry.subcommands.map((sub) => [sub.name, sub.summary]), { maxLeft: 10 }),
1124
+ );
1125
+ }
1126
+ const options = [
1127
+ ...visible(entry.options).map((option) => [` ${optionLabel(option)}`, sentence(option.help)]),
1128
+ ["-h, --help", "Show this message."],
1129
+ ];
1130
+ lines.push("", ...table(options, { maxLeft: 28 }));
1131
+ if (entry.examples?.length) {
1132
+ lines.push("", entry.examples.length === 1 ? "Example:" : "Examples:", ...entry.examples.map((example) => ` ${example}`));
1133
+ }
1134
+ if (entry.subcommands?.length) {
1135
+ lines.push("", `Run 'terminus help ${entry.name} <command>' for details on a command.`);
1136
+ }
1137
+ return lines.join("\n");
1138
+ }
1139
+
1140
+ /** "This command requires <query>." — what a usage line demands outside
1141
+ * its [ ], for bad-usage errors. */
1142
+ export function missingArgumentReason(entry) {
1143
+ const words = entry.usage[0].split(" ");
1144
+ const required = [];
1145
+ let depth = 0;
1146
+ for (const word of words.slice(words.indexOf(entry.name) + 1)) {
1147
+ const outside = depth === 0 && !word.startsWith("[") && !word.startsWith("--");
1148
+ depth += (word.match(/\[/g) ?? []).length - (word.match(/\]/g) ?? []).length;
1149
+ if (outside) required.push(word);
1150
+ }
1151
+ if (!required.length) return "This command takes different arguments.";
1152
+ if (required.length === 1 && !required[0].startsWith("<")) {
1153
+ return `This command requires one of: ${required[0].split("|").join(", ")}.`;
1154
+ }
1155
+ const list = required.length === 1 ? required[0] : `${required.slice(0, -1).join(", ")} and ${required.at(-1)}`;
1156
+ return `This command requires ${list}.`;
1157
+ }
1158
+
1159
+ /** Help for `terminus help [<command>] [<subcommand>]`; throws on an unknown topic
1160
+ * so the caller can exit with the usage code. */
1161
+ export function renderHelp(topic = []) {
1162
+ const [name, subName] = topic;
1163
+ if (!name) return renderOverview();
1164
+ const command = commandNamed(name);
1165
+ if (!command) {
1166
+ const error = new Error(unknownCommandMessage(name));
1167
+ error.usage = true;
1168
+ throw error;
1169
+ }
1170
+ if (subName && command.subcommands?.length) {
1171
+ const sub = subcommandNamed(command, subName);
1172
+ if (!sub) {
1173
+ const error = new Error(
1174
+ `terminus ${name}: '${subName}' is not a command. Commands: ${command.subcommands.map((entry) => entry.name).join(", ")}.`,
1175
+ );
1176
+ error.usage = true;
1177
+ throw error;
1178
+ }
1179
+ return renderEntry(sub);
1180
+ }
1181
+ return renderEntry(command);
1182
+ }
1183
+
1184
+ /** True when `-h`/`--help` appears as a switch rather than as the value of a
1185
+ * string option (`--prompt -h` sends "-h"). */
1186
+ export function helpRequested(name, args) {
1187
+ const strings = new Set([...(COMMAND_FLAGS[name]?.strings ?? []), ...COMMON_FLAGS.strings]);
1188
+ for (let index = 0; index < args.length; index += 1) {
1189
+ const arg = args[index];
1190
+ if (arg === "--help" || arg === "-h") return true;
1191
+ if (arg.startsWith("--") && !arg.includes("=") && strings.has(arg.slice(2))) index += 1;
1192
+ }
1193
+ return false;
1194
+ }