@mapled/cli 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -14,6 +14,8 @@ npx @mapled/cli doctor
14
14
 
15
15
  Node 18 or newer. No dependencies. Prefer a global install? `npm install -g @mapled/cli` puts `mapled` on your PATH, and every command below works as `mapled …`.
16
16
 
17
+ Connecting a site without an AI agent? [Integrate by hand](https://www.npmjs.com/package/@mapled/next#integrate-by-hand) in the `@mapled/next` README walks through the whole integration — modeling the content, reading it, the publish webhook, preview, forms — and uses these commands along the way.
18
+
17
19
  ## Sign in
18
20
 
19
21
  ```bash
@@ -127,6 +129,23 @@ npx @mapled/cli bindings pull
127
129
 
128
130
  `bindings pull` writes the pushed manifest into `mapled/manifest.json` — say, after your agent pushed one from a chat and you want the repository's copy. It refuses to overwrite a local file that differs unless you pass `--force`.
129
131
 
132
+ ## Create a form
133
+
134
+ ```bash
135
+ npx @mapled/cli forms create "Contact"
136
+ npx @mapled/cli forms list
137
+ ```
138
+
139
+ `forms create` makes a form the site can post submissions to — the same call your AI agent's `create_form` makes, so an integration done by hand needs no agent for it:
140
+
141
+ ```text
142
+ Created the form “Contact” (contact) in Dolphin Landing.
143
+ The site posts submissions to https://api.mapled.io/v1/delivery/forms/contact — flat JSON with the delivery key in X-Mapled-Key, an empty hidden _gotcha field, and the consent label as _consent when the form has a consent checkbox.
144
+ Submissions show up in Mapled under Forms.
145
+ ```
146
+
147
+ The key comes from the name; `--key <key>` chooses it (lowercase letters, digits and dashes), `--no-spam-protection` turns the honeypot off. Creating a form changes structure, so it needs the builder plan. `forms list` shows every form with the URL the site posts to (`--json` for scripts). The component that posts to it is in the [`@mapled/next` README](https://www.npmjs.com/package/@mapled/next#forms).
148
+
130
149
  ## Leave a guide for the next agent
131
150
 
132
151
  ```bash
@@ -155,8 +174,8 @@ Mapled doctor — Dolphin Landing
155
174
  ✓ MAPLED.md MAPLED.md matches the schema (3f9a1c2b4d5e) and manifest v3
156
175
  ✓ Environment MAPLED_KEY and MAPLED_WEBHOOK_SECRET in .env.local
157
176
  ✓ Secrets in git No env files or Mapled secrets are tracked.
158
- ✓ @mapled/next 0.7.0 (current)
159
- ✓ mapled CLI 0.2.0 (current)
177
+ ✓ @mapled/next 0.7.1 (current)
178
+ ✓ mapled CLI 0.7.0 (current)
160
179
  ✓ Revalidation route app/api/mapled/revalidate/route.ts
161
180
  ⚠ Preview route app/api/mapled/preview/route.ts is missing — mount createPreviewHandler from "@mapled/next/server" so Preview from Mapled works.
162
181
  ✓ Site reads content Last read 3m ago
@@ -175,7 +194,7 @@ Exit code 1 when something is marked ✗; `--json` prints the checks for scripts
175
194
 
176
195
  - `--api <origin>` — a Mapled API other than `https://api.mapled.io` (or set `MAPLED_API_URL`); `project link` remembers it in `mapled.json`
177
196
  - `--no-browser` — print the sign-in link instead of opening a browser
178
- - `--json` — machine-readable output (`doctor`, `schema diff`, `scan`, `manifest validate`)
197
+ - `--json` — machine-readable output (`doctor`, `schema diff`, `scan`, `manifest validate`, `forms list`)
179
198
  - `MAPLED_CONFIG_DIR` — where credentials live (default: `$XDG_CONFIG_HOME/mapled` or `~/.config/mapled`)
180
199
 
181
200
  ## Files in the repository
package/dist/args.js CHANGED
@@ -2,7 +2,7 @@ import { CliError } from "./errors.js";
2
2
  /** Minimal argument parsing: the leading words name the command;
3
3
  `--flag`, `--flag=value` and `--flag value` (for the flags that take
4
4
  one) follow. Nothing here is user content beyond the command line. */
5
- const VALUE_FLAGS = new Set(["api", "project", "out"]);
5
+ const VALUE_FLAGS = new Set(["api", "project", "out", "key"]);
6
6
  export function parseArgs(argv) {
7
7
  const words = [];
8
8
  const flags = {};
@@ -2,9 +2,9 @@ import { type ParsedArgs } from "./args.js";
2
2
  import { type FoundConfig } from "./config.js";
3
3
  import { type Connection } from "./credentials.js";
4
4
  import { type Fetch } from "./oauth.js";
5
- /** The commands of waves 1 and 2 (§31) and MAPLED.md (§21.3). Everything that talks to the
6
- world comes in through `Ctx`, so the commands run in tests against a
7
- fake API and a temp directory. */
5
+ /** The commands of waves 1 and 2 (§31), MAPLED.md (§21.3) and forms.
6
+ Everything that talks to the world comes in through `Ctx`, so the
7
+ commands run in tests against a fake API and a temp directory. */
8
8
  export type Ctx = {
9
9
  cwd: string;
10
10
  env: NodeJS.ProcessEnv;
@@ -29,6 +29,11 @@ export declare function manifestValidate(ctx: Ctx, flags: Flags): Promise<number
29
29
  export declare function bindingsPush(ctx: Ctx, flags: Flags): Promise<void>;
30
30
  export declare function bindingsPull(ctx: Ctx, flags: Flags): Promise<void>;
31
31
  export declare function mdPull(ctx: Ctx, flags: Flags): Promise<void>;
32
+ /** `mapled forms create "Contact"` — a form the site posts submissions to.
33
+ The same call an AI agent's `create_form` makes, so an integration
34
+ done by hand needs no agent for it. */
35
+ export declare function formsCreate(ctx: Ctx, flags: Flags, words: string[]): Promise<void>;
36
+ export declare function formsList(ctx: Ctx, flags: Flags): Promise<void>;
32
37
  export declare function scan(ctx: Ctx, flags: Flags): Promise<void>;
33
38
  export declare function doctor(ctx: Ctx, flags: Flags): Promise<number>;
34
39
  export {};
package/dist/commands.js CHANGED
@@ -390,6 +390,61 @@ export async function mdPull(ctx, flags) {
390
390
  else
391
391
  ctx.out(`Updated ${MD_FILE} — ${summary}; the notes below the notes line were kept.`);
392
392
  }
393
+ const FORM_KEY = /^[a-z][a-z0-9-]{0,63}$/;
394
+ const submitUrl = (api, key) => `${api}/v1/delivery/forms/${key}`;
395
+ /** `mapled forms create "Contact"` — a form the site posts submissions to.
396
+ The same call an AI agent's `create_form` makes, so an integration
397
+ done by hand needs no agent for it. */
398
+ export async function formsCreate(ctx, flags, words) {
399
+ const displayName = words.join(" ").trim();
400
+ if (!displayName)
401
+ throw new CliError('Name the form: `mapled forms create "Contact"`.');
402
+ if (displayName.length > 80)
403
+ throw new CliError("A form name is up to 80 characters.");
404
+ const key = stringFlag(flags, "key");
405
+ if (flags.key !== undefined && (!key || !FORM_KEY.test(key))) {
406
+ throw new CliError("A form key is lowercase letters, digits and dashes, starting with a letter (up to 64 characters).");
407
+ }
408
+ const { session } = await openSession(ctx, flags);
409
+ let created;
410
+ try {
411
+ created = await session.send("POST", "/v1/agent/forms", {
412
+ displayName,
413
+ ...(key ? { key } : {}),
414
+ ...(flags["no-spam-protection"] ? { spamProtection: false } : {}),
415
+ });
416
+ }
417
+ catch (err) {
418
+ if (err instanceof ApiError && err.code === "BUILDER_PLAN_REQUIRED") {
419
+ throw new CliError("Creating a form changes the project's structure, which needs a builder plan — ask the project owner to upgrade.");
420
+ }
421
+ throw err;
422
+ }
423
+ const form = created.form;
424
+ ctx.out(`Created the form “${form.displayName}” (${form.key}) in ${session.conn.projectName}.`);
425
+ ctx.out(`The site posts submissions to ${submitUrl(session.conn.api, form.key)} — flat JSON with the delivery key in X-Mapled-Key` +
426
+ `${form.spamProtection ? ", an empty hidden _gotcha field" : ""}, and the consent label as _consent when the form has a consent checkbox.`);
427
+ ctx.out("Submissions show up in Mapled under Forms.");
428
+ }
429
+ export async function formsList(ctx, flags) {
430
+ const { session } = await openSession(ctx, flags);
431
+ const { forms } = await session.get("/v1/agent/forms");
432
+ if (flags.json) {
433
+ ctx.out(JSON.stringify({ forms: forms.map((f) => ({ ...f, submitUrl: submitUrl(session.conn.api, f.key) })) }, null, 2));
434
+ return;
435
+ }
436
+ if (forms.length === 0) {
437
+ ctx.out(`No forms in ${session.conn.projectName} yet. Create one with \`mapled forms create "Contact"\`.`);
438
+ return;
439
+ }
440
+ const keyWidth = Math.max(...forms.map((f) => f.key.length));
441
+ const nameWidth = Math.max(...forms.map((f) => f.displayName.length));
442
+ ctx.out(`Forms of ${session.conn.projectName}`);
443
+ ctx.out("");
444
+ for (const f of forms) {
445
+ ctx.out(`${f.key.padEnd(keyWidth)} ${f.displayName.padEnd(nameWidth)} spam protection ${f.spamProtection ? "on " : "off"} ${submitUrl(session.conn.api, f.key)}`);
446
+ }
447
+ }
393
448
  /* ---- wave 2: the scanner ---- */
394
449
  function describeScan(result, schema) {
395
450
  const singles = new Set(schema.collections.filter((c) => c.kind === "single").map((c) => c.key));
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { parseArgs } from "./args.js";
5
5
  import { openBrowser } from "./browser.js";
6
- import { bindingsPull, bindingsPush, doctor, generate, link, login, logout, manifestValidate, mdPull, scan, schemaDiff, schemaPull, } from "./commands.js";
6
+ import { bindingsPull, bindingsPush, doctor, formsCreate, formsList, generate, link, login, logout, manifestValidate, mdPull, scan, schemaDiff, schemaPull, } from "./commands.js";
7
7
  import { credentialsPath } from "./credentials.js";
8
8
  import { CliError } from "./errors.js";
9
9
  /** `mapled` — sign in, link a repository to its project, generate types
@@ -24,6 +24,8 @@ Usage
24
24
  mapled bindings push [--dry-run] Push mapled/manifest.json so Mapled grades every binding
25
25
  mapled bindings pull [--force] Write the pushed manifest into mapled/manifest.json
26
26
  mapled md pull [--force] Write MAPLED.md, the guide for the next agent, from the project
27
+ mapled forms create <name> [--key <key>] Create a form the site posts submissions to (--no-spam-protection to skip the honeypot)
28
+ mapled forms list [--json] List the project's forms and where the site posts to
27
29
  mapled doctor [--json] Check the integration end to end
28
30
 
29
31
  Options
@@ -108,6 +110,12 @@ async function main(argv) {
108
110
  case "md pull":
109
111
  await mdPull(ctx, flags);
110
112
  return 0;
113
+ case "forms create":
114
+ await formsCreate(ctx, flags, words.slice(2));
115
+ return 0;
116
+ case "forms list":
117
+ await formsList(ctx, flags);
118
+ return 0;
111
119
  case "doctor":
112
120
  return doctor(ctx, flags);
113
121
  case "auth":
@@ -117,6 +125,7 @@ async function main(argv) {
117
125
  case "manifest":
118
126
  case "bindings":
119
127
  case "md":
128
+ case "forms":
120
129
  console.log(HELP);
121
130
  return 2;
122
131
  default:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapled/cli",
3
- "version": "0.6.0",
4
- "description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content and check the site's integration.",
3
+ "version": "0.7.0",
4
+ "description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content, create forms and check the site's integration.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mapled.io",
7
7
  "keywords": [