moshcode 0.24.0

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +580 -0
  3. package/bin/moshcode.mjs +674 -0
  4. package/bin/moshscript.mjs +29 -0
  5. package/examples/alive.mosh +6 -0
  6. package/examples/scripting-the-cli.mosh +21 -0
  7. package/examples/team-secrets.mosh +20 -0
  8. package/examples/templates/bun-caddy-sqlite/.env.example +14 -0
  9. package/examples/templates/bun-caddy-sqlite/Caddyfile +18 -0
  10. package/examples/templates/bun-caddy-sqlite/README.md +97 -0
  11. package/examples/templates/bun-caddy-sqlite/deploy/moshcode-dns.service +39 -0
  12. package/examples/templates/bun-caddy-sqlite/deploy/moshpit-service.service +38 -0
  13. package/examples/templates/bun-caddy-sqlite/package.json +15 -0
  14. package/examples/templates/bun-caddy-sqlite/src/db.ts +47 -0
  15. package/examples/templates/bun-caddy-sqlite/src/server.ts +44 -0
  16. package/examples/templates/bun-caddy-sqlite/template.json +10 -0
  17. package/examples/templates/caddy-proxy/Caddyfile +36 -0
  18. package/examples/templates/caddy-proxy/README.md +104 -0
  19. package/examples/templates/caddy-proxy/deploy/moshcode-dns.service +39 -0
  20. package/examples/templates/caddy-proxy/template.json +8 -0
  21. package/examples/templates/caddy-static/Caddyfile +16 -0
  22. package/examples/templates/caddy-static/README.md +90 -0
  23. package/examples/templates/caddy-static/deploy/moshcode-dns.service +39 -0
  24. package/examples/templates/caddy-static/site/index.html +11 -0
  25. package/examples/templates/caddy-static/template.json +8 -0
  26. package/install.sh +194 -0
  27. package/package.json +28 -0
  28. package/prd/0000-template.md +49 -0
  29. package/prd/0001-wrap-ugig-and-coinpay-clis.md +121 -0
  30. package/prd/0002-separate-agent-and-raw-engine-launches.md +113 -0
  31. package/prd/0003-cross-engine-mcp-and-skill-installation.md +165 -0
  32. package/prd/0004-moshscript-run-programmable-moshcode.md +344 -0
  33. package/prd/0005-hosted-moshpit-resolver.md +192 -0
  34. package/prd/0006-help.md +359 -0
  35. package/prd/0007-profullstack-site-init.md +1183 -0
  36. package/prd/README.md +26 -0
  37. package/src/ads.mjs +58 -0
  38. package/src/auth.mjs +193 -0
  39. package/src/cli-schema.mjs +533 -0
  40. package/src/cli.mjs +118 -0
  41. package/src/commands.mjs +259 -0
  42. package/src/completion.mjs +594 -0
  43. package/src/console.mjs +244 -0
  44. package/src/dns-system.mjs +404 -0
  45. package/src/dns.mjs +2872 -0
  46. package/src/doh-server.mjs +256 -0
  47. package/src/doh.mjs +218 -0
  48. package/src/engines.mjs +385 -0
  49. package/src/escalate.mjs +85 -0
  50. package/src/help.mjs +443 -0
  51. package/src/integrations.mjs +265 -0
  52. package/src/mcp-catalog.mjs +50 -0
  53. package/src/mcp.mjs +155 -0
  54. package/src/mirror.mjs +187 -0
  55. package/src/notify.mjs +86 -0
  56. package/src/open-url.mjs +34 -0
  57. package/src/parking-http.mjs +65 -0
  58. package/src/pins.mjs +190 -0
  59. package/src/pit-url.mjs +13 -0
  60. package/src/prd.mjs +341 -0
  61. package/src/pty.mjs +176 -0
  62. package/src/pwd.mjs +103 -0
  63. package/src/registry.mjs +37 -0
  64. package/src/release-install.mjs +191 -0
  65. package/src/runtime.mjs +161 -0
  66. package/src/selfupdate.mjs +215 -0
  67. package/src/serve.mjs +502 -0
  68. package/src/skills.mjs +93 -0
  69. package/src/tabs.mjs +144 -0
  70. package/src/templates.mjs +456 -0
  71. package/src/tools.mjs +231 -0
  72. package/src/trade.mjs +137 -0
  73. package/src/trust.mjs +712 -0
  74. package/src/tui.mjs +736 -0
  75. package/src/ui.mjs +49 -0
  76. package/src/uninstall.mjs +113 -0
  77. package/src/upgrade.mjs +217 -0
package/src/help.mjs ADDED
@@ -0,0 +1,443 @@
1
+ // Help, rendered from the command table rather than written out.
2
+ //
3
+ // This replaces an 87-line template literal in bin/moshcode.mjs that had
4
+ // already drifted: `dns` and `version` were dispatchable and absent from it,
5
+ // aliases were missing, and half the flags existed only in the parsers. Nothing
6
+ // here knows a command name — it all comes from src/cli-schema.mjs, so a verb
7
+ // that is added to the table is documented by construction (PRD 0006 R5).
8
+ //
9
+ // Pure and side-effect free by design. Help is the one command guaranteed to
10
+ // run before anything is installed and before anyone has logged in, so it must
11
+ // not touch the network, read credentials, or write to disk (R2). The only
12
+ // impurity is the engine/tool roster, which the caller passes in.
13
+
14
+ import {
15
+ CORE_CLI_COMMANDS,
16
+ COMMAND_GROUPS,
17
+ NOT_IN_PIT,
18
+ PIT_COMMANDS,
19
+ VERB_TABLES,
20
+ } from "./cli-schema.mjs";
21
+
22
+ /** Every rendered line wraps here. 28 lines of the old help ran past it. */
23
+ export const WIDTH = 80;
24
+
25
+ /** What asks for help, at any level. */
26
+ export const HELP_TOKENS = ["--help", "-h", "help"];
27
+
28
+ /**
29
+ * Is this argument list asking for help?
30
+ *
31
+ * Position-independent within its own level (R1), which is the whole fix:
32
+ * `moshcode mcp install --help` and `moshcode mcp --help install` both ask the
33
+ * same question, and today the first one is a usage error.
34
+ *
35
+ * `stopAt` is for `run`, where the boundary matters: `moshcode run --help` is
36
+ * the runner's help, and `moshcode run script.mosh --help` is the script's
37
+ * argument (PRD 0004 R13). Everything at or after the first non-flag token is
38
+ * somebody else's to interpret.
39
+ */
40
+ export function wantsHelp(args = [], { stopAt = false } = {}) {
41
+ for (const arg of args) {
42
+ if (stopAt && !String(arg).startsWith("-")) return false;
43
+ if (HELP_TOKENS.includes(arg)) return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ /** Arguments with the help tokens removed, so the rest can still be read. */
49
+ export function withoutHelp(args = []) {
50
+ return args.filter((a) => !HELP_TOKENS.includes(a));
51
+ }
52
+
53
+ const isAlias = (c) => Boolean(c.aliasOf);
54
+
55
+ /** Commands that are not aliases — the ones with something to say. */
56
+ export const primaryCommands = () => CORE_CLI_COMMANDS.filter((c) => !isAlias(c) && c.group);
57
+
58
+ /** The aliases pointing at a command, as bare names. */
59
+ export const aliasesFor = (name) =>
60
+ CORE_CLI_COMMANDS.filter((c) => c.aliasOf === name).map((c) => c.name);
61
+
62
+ /**
63
+ * Resolve what the user typed to the entry that documents it.
64
+ *
65
+ * Aliases resolve to their target rather than rendering twice (R7): `moshcode
66
+ * help where` is a question about `pwd`, and answering it with a stub that says
67
+ * "alias for pwd" and nothing else would be true and useless.
68
+ */
69
+ export function findCommand(name) {
70
+ const wanted = String(name ?? "").toLowerCase();
71
+ const hit = CORE_CLI_COMMANDS.find((c) => c.name === wanted);
72
+ if (!hit) return null;
73
+ return hit.aliasOf ? CORE_CLI_COMMANDS.find((c) => c.name === hit.aliasOf) ?? null : hit;
74
+ }
75
+
76
+ /** The sub-verb table a command declares, or an empty list. */
77
+ export function verbsFor(command) {
78
+ return (command?.verbs && VERB_TABLES[command.verbs]) || [];
79
+ }
80
+
81
+ export function findVerb(command, name) {
82
+ const wanted = String(name ?? "").toLowerCase();
83
+ return verbsFor(command).find((v) => v.name === wanted) ?? null;
84
+ }
85
+
86
+ /* ------------------------------------------------------------------ suggest */
87
+
88
+ /** Levenshtein, iterative, two rows. Small inputs; this is a typo check. */
89
+ function distance(a, b) {
90
+ if (a === b) return 0;
91
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
92
+ for (let i = 0; i < a.length; i++) {
93
+ const row = [i + 1];
94
+ for (let j = 0; j < b.length; j++) {
95
+ row[j + 1] = Math.min(
96
+ prev[j + 1] + 1,
97
+ row[j] + 1,
98
+ prev[j] + (a[i] === b[j] ? 0 : 1),
99
+ );
100
+ }
101
+ prev = row;
102
+ }
103
+ return prev[b.length];
104
+ }
105
+
106
+ /**
107
+ * The nearest command to something that is not one, or null.
108
+ *
109
+ * Drawn from the same set completion offers (R11), so the two can never
110
+ * disagree about what exists. The threshold is deliberately tight: suggesting
111
+ * `run` for `xyzzy` is noise, and a wrong suggestion is worse than none because
112
+ * it sends people to try a second wrong command.
113
+ */
114
+ export function suggest(input, extra = []) {
115
+ const typed = String(input ?? "").toLowerCase();
116
+ if (!typed) return null;
117
+ const candidates = [...CORE_CLI_COMMANDS.map((c) => c.name), ...extra]
118
+ .filter((n) => !n.startsWith("-"));
119
+
120
+ let best = null;
121
+ let bestScore = Infinity;
122
+ for (const name of candidates) {
123
+ const score = distance(typed, name);
124
+ if (score < bestScore) { best = name; bestScore = score; }
125
+ }
126
+ // Allow one edit for short words, two for longer ones; never more.
127
+ const ceiling = typed.length <= 4 ? 1 : 2;
128
+ return bestScore <= ceiling ? best : null;
129
+ }
130
+
131
+ /* ------------------------------------------------------------------- render */
132
+
133
+ /** Wrap `text` to WIDTH, indenting continuations by `indent` spaces. */
134
+ export function wrap(text, indent = 0, width = WIDTH) {
135
+ // Every line — the first one included — is placed after `indent` columns:
136
+ // the first continues an already-printed label, the rest are padded to sit
137
+ // under it. So they all get the same budget, and the total never exceeds
138
+ // `width`. Getting this wrong is how a "wraps at 80" helper emits 92.
139
+ const budget = Math.max(20, width - indent);
140
+ const pad = " ".repeat(indent);
141
+ const lines = [];
142
+ let line = "";
143
+ for (const word of String(text).split(/\s+/).filter(Boolean)) {
144
+ const candidate = line ? `${line} ${word}` : word;
145
+ if (candidate.length > budget && line) {
146
+ lines.push(line);
147
+ line = word;
148
+ } else line = candidate;
149
+ }
150
+ if (line) lines.push(line);
151
+ return lines.map((l, i) => (i ? pad + l : l)).join("\n");
152
+ }
153
+
154
+ /** ` name description`, wrapped, for a two-column list. */
155
+ function row(left, right, pad = 22) {
156
+ const head = ` ${left.padEnd(pad)}`;
157
+ if (!right) return head.trimEnd();
158
+ const wrapped = wrap(right, head.length, WIDTH);
159
+ return `${head}${wrapped}`;
160
+ }
161
+
162
+ /**
163
+ * The top-level overview: one screen, grouped, with a way in (R10).
164
+ *
165
+ * A menu rather than an index. The old help printed every command, every
166
+ * engine, every tool and the whole moshscript vocabulary in 127 lines — which
167
+ * is why nobody read it and why `moshcode help | grep` became the interface.
168
+ * `--all` keeps that available for the people who grep.
169
+ */
170
+ export function renderOverview({ engines = [], tools = [], version = "" } = {}) {
171
+ const out = [];
172
+ out.push(`moshcode${version ? ` ${version}` : ""} — a metal wrapper for coding engines 🤘`);
173
+ out.push("");
174
+ out.push("usage: moshcode [command] [args…] no command → open the mosh pit");
175
+ out.push("");
176
+
177
+ for (const group of COMMAND_GROUPS) {
178
+ const members = primaryCommands().filter((c) => c.group === group.key);
179
+ if (!members.length) continue;
180
+ const names = members.map((c) => c.name).join(" · ");
181
+ out.push(row(group.title, names, 10));
182
+ }
183
+
184
+ out.push("");
185
+ if (engines.length) out.push(row("engines", engines.join(" · "), 10));
186
+ if (tools.length) out.push(row("tools", tools.join(" · "), 10));
187
+ out.push("");
188
+ out.push(row("moshcode help <command>", "drill into one (flags, examples)", 26));
189
+ out.push(row("moshcode help --all", "the whole wall", 26));
190
+ out.push(row("moshcode help --json", "the machine-readable model", 26));
191
+ out.push("");
192
+ out.push("engines are installed and driven by moshcode — 🤘 no bugs, only features");
193
+ return out.join("\n");
194
+ }
195
+
196
+ /** One command, in full: synopsis, flags, sub-verbs, examples, see also (R4). */
197
+ export function renderCommand(command, { verb = null } = {}) {
198
+ if (!command) return "";
199
+ const target = verb || command;
200
+ const title = verb ? `moshcode ${command.name} ${verb.name}` : `moshcode ${command.name}`;
201
+ const out = [`${title} — ${target.description}`];
202
+
203
+ const synopsis = target.synopsis || [];
204
+ if (synopsis.length) {
205
+ out.push("", "usage:");
206
+ for (const [line, note] of synopsis) out.push(row(line, note, 44));
207
+ }
208
+
209
+ const flags = target.flags || [];
210
+ if (flags.length) {
211
+ out.push("", "flags:");
212
+ for (const [flag, description, fallback] of flags) {
213
+ out.push(row(flag, `${description}${fallback ? ` (default: ${fallback})` : ""}`, 22));
214
+ }
215
+ }
216
+
217
+ if (!verb) {
218
+ const verbs = verbsFor(command);
219
+ if (verbs.length) {
220
+ out.push("", "verbs:");
221
+ for (const v of verbs) out.push(row(v.name, v.description, 22));
222
+ out.push("", `moshcode help ${command.name} <verb> for one of them`);
223
+ }
224
+ }
225
+
226
+ const examples = target.examples || [];
227
+ if (examples.length) {
228
+ out.push("", "examples:");
229
+ for (const [cmd, note] of examples) out.push(row(cmd, note ? `# ${note}` : "", 44));
230
+ }
231
+
232
+ if (!verb) {
233
+ const aliases = aliasesFor(command.name);
234
+ if (aliases.length) out.push("", `aliases: ${aliases.join(", ")}`);
235
+ }
236
+
237
+ if (target.note) out.push("", wrap(target.note, 0));
238
+
239
+ const seeAlso = target.seeAlso || command.seeAlso || [];
240
+ if (seeAlso.length) {
241
+ out.push("", `see also: ${seeAlso.map((s) => `moshcode help ${s}`).join(" · ")}`);
242
+ }
243
+ return out.join("\n");
244
+ }
245
+
246
+ /** Every command, in full — what `--all` and the old wall give you (R10). */
247
+ export function renderAll(context = {}) {
248
+ const blocks = [renderOverview(context), ""];
249
+ for (const command of primaryCommands()) {
250
+ blocks.push("─".repeat(WIDTH - 20), renderCommand(command), "");
251
+ }
252
+ return blocks.join("\n");
253
+ }
254
+
255
+ /**
256
+ * The help model, for an agent (R9).
257
+ *
258
+ * The same shape `engines --json` and `commands --json` already honor: data on
259
+ * stdout, no decoration, exit 0. This is the interface for the consumer that
260
+ * cannot read a terminal layout — a coding engine that moshcode itself
261
+ * launched, shelling back in to learn what it can drive.
262
+ */
263
+ export function helpModel({ engines = [], tools = [], version = "" } = {}) {
264
+ return {
265
+ name: "moshcode",
266
+ version: version || null,
267
+ usage: "moshcode [command] [args…]",
268
+ engines,
269
+ tools,
270
+ groups: COMMAND_GROUPS.map(({ key, title }) => ({
271
+ name: key,
272
+ title,
273
+ commands: primaryCommands().filter((c) => c.group === key).map((c) => c.name),
274
+ })),
275
+ commands: primaryCommands().map((command) => ({
276
+ name: command.name,
277
+ group: command.group,
278
+ description: command.description,
279
+ aliases: aliasesFor(command.name),
280
+ synopsis: (command.synopsis || []).map(([line, note]) => ({ usage: line, note: note || null })),
281
+ flags: (command.flags || []).map(([flags, description, fallback]) => ({
282
+ flags,
283
+ description,
284
+ default: fallback || null,
285
+ })),
286
+ examples: (command.examples || []).map(([cmd, note]) => ({ command: cmd, note: note || null })),
287
+ verbs: verbsFor(command).map((v) => ({
288
+ name: v.name,
289
+ description: v.description,
290
+ synopsis: (v.synopsis || []).map(([line, note]) => ({ usage: line, note: note || null })),
291
+ flags: (v.flags || []).map(([flags, description, fallback]) => ({
292
+ flags,
293
+ description,
294
+ default: fallback || null,
295
+ })),
296
+ })),
297
+ seeAlso: command.seeAlso || [],
298
+ note: command.note || null,
299
+ })),
300
+ };
301
+ }
302
+
303
+ /**
304
+ * The block printed when a command was used wrongly (R3).
305
+ *
306
+ * The command's own usage, never the top-level wall — a mistyped flag on
307
+ * `console` is not a reason to print 127 lines about engines. Callers send this
308
+ * to stderr and exit 1; `renderCommand` on stdout is the same text for the
309
+ * person who asked politely.
310
+ */
311
+ /* ------------------------------------------------------- moshscript verbs */
312
+
313
+ /**
314
+ * A moshscript verb, rendered from the registry (PRD 0006 R14).
315
+ *
316
+ * `moshcode help ask` is a fair question — `ask()` is as much part of the
317
+ * interface as `moshcode prd` — and it used to answer "no help for ask",
318
+ * because the vocabulary lives in a registry that help had never been
319
+ * introduced to.
320
+ *
321
+ * `usage` is optional on a command object, so a verb registered by a host that
322
+ * has not declared one still renders: the signature falls back to `name(…)` and
323
+ * the summary carries the meaning.
324
+ */
325
+ export function renderScriptVerb(command) {
326
+ if (!command) return null;
327
+ const out = [`${command.usage || `${command.name}(…)`} — ${command.summary || "a moshscript verb"}`];
328
+ if (command.detail) out.push("", wrap(command.detail, 0));
329
+ out.push("", "a moshscript verb — call it from a .mosh file, not from the shell.");
330
+ out.push("", "see also: moshcode help run · moshcode help commands");
331
+ return out.join("\n");
332
+ }
333
+
334
+ /* ---------------------------------------------------------------- markdown */
335
+
336
+ export const README_START = "<!-- COMMANDS:START -->";
337
+ export const README_END = "<!-- COMMANDS:END -->";
338
+
339
+ /**
340
+ * The command table as markdown, for README.md (PRD 0006 R13).
341
+ *
342
+ * Generated rather than checked against a hand-written list, and the difference
343
+ * matters: a checker tells you the README is wrong, a generator makes it right.
344
+ * Same shape as the PRD index this repo already keeps between markers, so the
345
+ * convention is one people here already know.
346
+ *
347
+ * Aliases ride along in their target's row instead of getting rows of their
348
+ * own — six extra lines saying "alias for X" is how a table stops being read.
349
+ */
350
+ export function renderMarkdown() {
351
+ const rows = primaryCommands().map((command) => {
352
+ const aliases = aliasesFor(command.name);
353
+ const name = `\`moshcode ${command.name}\`${aliases.length ? ` <br>${aliases.map((a) => `\`${a}\``).join(" ")}` : ""}`;
354
+ return `| ${name} | ${command.group} | ${command.description} |`;
355
+ });
356
+ return [
357
+ "| command | group | what it does |",
358
+ "|---|---|---|",
359
+ ...rows,
360
+ ].join("\n");
361
+ }
362
+
363
+ /**
364
+ * Put the generated table between the markers in `markdown`.
365
+ *
366
+ * Returns the document unchanged when it carries no markers, so this can be
367
+ * pointed at a file that has not opted in without mangling it.
368
+ */
369
+ export function withCommandTable(markdown) {
370
+ const text = String(markdown);
371
+ const from = text.indexOf(README_START);
372
+ const to = text.indexOf(README_END);
373
+ if (from < 0 || to < 0 || to < from) return text;
374
+ return `${text.slice(0, from + README_START.length)}\n${renderMarkdown()}\n${text.slice(to)}`;
375
+ }
376
+
377
+ /* ------------------------------------------------------------------ the pit */
378
+
379
+ /** Resolve a pit command, following its aliases. */
380
+ export function findPitCommand(name) {
381
+ const wanted = String(name ?? "").toLowerCase().replace(/^\//, "");
382
+ return PIT_COMMANDS.find((c) => c.name === wanted || (c.aliases || []).includes(wanted)) ?? null;
383
+ }
384
+
385
+ /**
386
+ * The model behind `/help` (R12).
387
+ *
388
+ * Structure rather than a string, because the pit colours its output and the
389
+ * CLI does not — returning pre-rendered text would mean either losing the
390
+ * colour or teaching this module about ANSI. The tool roster comes from the
391
+ * caller, which gets it from TOOLS: `/help` used to hardcode nine tool names
392
+ * directly beneath a printTools() whose own comment explains why that goes
393
+ * stale.
394
+ */
395
+ export function pitHelpModel({ engines = [], tools = [] } = {}) {
396
+ return {
397
+ commands: PIT_COMMANDS.map((c) => ({
398
+ name: c.name,
399
+ aliases: c.aliases || [],
400
+ args: c.args || "",
401
+ description: c.description,
402
+ usage: `/${c.name}${c.args ? ` ${c.args}` : ""}`,
403
+ })),
404
+ engines,
405
+ tools,
406
+ // Honest about the gap rather than silent about it.
407
+ notInPit: NOT_IN_PIT.map((name) => ({
408
+ name,
409
+ description: findCommand(name)?.description || "",
410
+ })),
411
+ };
412
+ }
413
+
414
+ /**
415
+ * One pit command in detail, for `/help <command>` and `/<command> --help`.
416
+ *
417
+ * Delegates to the CLI block when the verb is the same verb, so flags and
418
+ * examples are written once. Pit-only commands (`/shell`, `/quit`) answer for
419
+ * themselves.
420
+ */
421
+ export function renderPitCommand(name) {
422
+ const entry = findPitCommand(name);
423
+ if (!entry) return null;
424
+ if (entry.cli) {
425
+ const command = findCommand(entry.cli);
426
+ if (command) {
427
+ const body = renderCommand(command);
428
+ // The pit spells them with a slash; say so once rather than rewriting
429
+ // every synopsis line.
430
+ return `${body}\n\nin the pit: /${entry.name}${entry.args ? ` ${entry.args}` : ""}`;
431
+ }
432
+ }
433
+ const out = [`/${entry.name} — ${entry.description}`];
434
+ if (entry.args) out.push("", "usage:", row(`/${entry.name} ${entry.args}`, "", 44));
435
+ if (entry.aliases?.length) out.push("", `aliases: ${entry.aliases.map((a) => `/${a}`).join(", ")}`);
436
+ return out.join("\n");
437
+ }
438
+
439
+ export function usageBlock(name, verb = null) {
440
+ const command = findCommand(name);
441
+ if (!command) return "";
442
+ return renderCommand(command, { verb: verb ? findVerb(command, verb) : null });
443
+ }