@mapled/cli 0.2.0 → 0.3.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
@@ -8,6 +8,7 @@ npx @mapled/cli project link
8
8
  npx @mapled/cli types generate
9
9
  npx @mapled/cli scan --write
10
10
  npx @mapled/cli bindings push
11
+ npx @mapled/cli md pull
11
12
  npx @mapled/cli doctor
12
13
  ```
13
14
 
@@ -124,6 +125,16 @@ npx @mapled/cli bindings pull
124
125
 
125
126
  `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`.
126
127
 
128
+ ## Leave a guide for the next agent
129
+
130
+ ```bash
131
+ npx @mapled/cli md pull
132
+ ```
133
+
134
+ Writes `MAPLED.md` at the repository root — the guide Mapled renders from the project for whoever works on the site next, person or AI agent: the project and how the site reads it, the content model with every field's type, rules and help text, where each page renders which field (from the pushed manifest), the last setup run with its verification, the working rules, and the commands that verify the integration. It never contains a secret.
135
+
136
+ The first line is a stamp with the schema hash and the manifest version. Everything below the `<!-- mapled:notes -->` line is yours — conventions, what stays hardcoded on purpose, things to avoid — and survives every `md pull`. A `MAPLED.md` Mapled didn't write is left alone unless you pass `--force`, which moves its content below the notes line. `mapled doctor` tells you when the file no longer matches the project; your AI agent gets the same document from the `get_mapled_md` tool.
137
+
127
138
  ## Check the integration
128
139
 
129
140
  ```bash
@@ -138,6 +149,7 @@ Mapled doctor — Dolphin Landing
138
149
  ✓ Generated types mapled-types.ts matches the schema (3f9a1c2b4d5e)
139
150
  ✓ Schema pin mapled/schema.json matches the schema (3f9a1c2b4d5e)
140
151
  ✓ Site manifest mapled/manifest.json is pushed as manifest v3 (14 bindings on 3 pages)
152
+ ✓ MAPLED.md MAPLED.md matches the schema (3f9a1c2b4d5e) and manifest v3
141
153
  ✓ Environment MAPLED_KEY and MAPLED_WEBHOOK_SECRET in .env.local
142
154
  ✓ Secrets in git No env files or Mapled secrets are tracked.
143
155
  ✓ @mapled/next 0.6.0 (current)
@@ -152,7 +164,7 @@ Mapled doctor — Dolphin Landing
152
164
  1 warning.
153
165
  ```
154
166
 
155
- The repository side: the link, the sign-in, the generated types, the schema pin, the site manifest (and whether it matches what was pushed), `MAPLED_KEY` and `MAPLED_WEBHOOK_SECRET` in your env files (names only — values are never read out), env files or Mapled secrets tracked by git, the installed `@mapled/next` and CLI versions, the revalidation and preview routes of a Next.js site. The Mapled side, from the same status the AI agent's `check_integration` tool reads: whether the site has read content with the delivery key, the publish webhook and its last delivery, the preview route on the deployed site, the bindings health.
167
+ The repository side: the link, the sign-in, the generated types, the schema pin, the site manifest (and whether it matches what was pushed), `MAPLED.md` (and whether it is what Mapled would render now), `MAPLED_KEY` and `MAPLED_WEBHOOK_SECRET` in your env files (names only — values are never read out), env files or Mapled secrets tracked by git, the installed `@mapled/next` and CLI versions, the revalidation and preview routes of a Next.js site. The Mapled side, from the same status the AI agent's `check_integration` tool reads: whether the site has read content with the delivery key, the publish webhook and its last delivery, the preview route on the deployed site, the bindings health.
156
168
 
157
169
  Exit code 1 when something is marked ✗; `--json` prints the checks for scripts and agents.
158
170
 
@@ -171,5 +183,6 @@ Exit code 1 when something is marked ✗; `--json` prints the checks for scripts
171
183
  | `mapled-types.ts` | `types generate` | TypeScript types of the published content, with `MapledSchema` |
172
184
  | `mapled/schema.json` | `schema pull`, `types generate` | The schema this site is built against — `schema diff` starts from it |
173
185
  | `mapled/manifest.json` | `scan --write`, `bindings pull`, your agent | Pages and bindings — where the code reads each collection and field |
186
+ | `MAPLED.md` | `md pull`, your agent | The guide for the next agent — project, content model, bindings, working rules; your notes below the marker |
174
187
 
175
- Commit all four. Credentials never live in the repository.
188
+ Commit all five. Credentials never live in the repository.
@@ -2,7 +2,7 @@ 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). Everything that talks to the
5
+ /** The commands of waves 1 and 2 (§31) and MAPLED.md (§21.3). Everything that talks to the
6
6
  world comes in through `Ctx`, so the commands run in tests against a
7
7
  fake API and a temp directory. */
8
8
  export type Ctx = {
@@ -28,6 +28,7 @@ export declare function schemaDiff(ctx: Ctx, flags: Flags): Promise<number>;
28
28
  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
+ export declare function mdPull(ctx: Ctx, flags: Flags): Promise<void>;
31
32
  export declare function scan(ctx: Ctx, flags: Flags): Promise<void>;
32
33
  export declare function doctor(ctx: Ctx, flags: Flags): Promise<number>;
33
34
  export {};
package/dist/commands.js CHANGED
@@ -10,6 +10,7 @@ import { checkManifest, compareManifests, fileExistsIn, MANIFEST_FILE, manifestS
10
10
  import { authorize, clientKnown, exchangeCode, registerClient, revokeToken } from "./oauth.js";
11
11
  import { formatChecks, GLYPH, paint, summarize, useColor } from "./output.js";
12
12
  import { diffSchema, formatChanges, pinSchema, readPin, SCHEMA_FILE, summarizeChanges, writePin } from "./pin.js";
13
+ import { checkMapledMd, MD_FILE, mergeMd } from "./md.js";
13
14
  import { loadTypeScript, mergeManifest, scanRepository } from "./scan.js";
14
15
  import { generateTypes } from "./types.js";
15
16
  /** The OAuth client id for this API — registered once per machine, and
@@ -367,6 +368,27 @@ export async function bindingsPull(ctx, flags) {
367
368
  await writeManifestFile(found.dir, stored.manifest);
368
369
  ctx.out(`Wrote ${MANIFEST_FILE} from manifest v${stored.version} (pushed ${dr.ago(stored.createdAt)} by ${stored.clientName}) — ${manifestSummary(stored.manifest)}.`);
369
370
  }
371
+ /* ---- MAPLED.md ---- */
372
+ export async function mdPull(ctx, flags) {
373
+ const { found, session } = await openSession(ctx, flags);
374
+ const fresh = await session.get("/v1/agent/mapled-md");
375
+ const file = path.join(found.dir, MD_FILE);
376
+ const existing = await readFile(file, "utf8").catch(() => null);
377
+ const merged = mergeMd(fresh.markdown, existing, { force: Boolean(flags.force) });
378
+ if (merged.kind === "unchanged") {
379
+ ctx.out(`${MD_FILE} is up to date (schema ${fresh.schemaHash}).`);
380
+ return;
381
+ }
382
+ await writeFile(file, merged.text);
383
+ const c = fresh.counts;
384
+ const summary = `${plural(c.collections, "collection")}, ${plural(c.singles, "single")}, ${plural(c.bindings, "binding")} (schema ${fresh.schemaHash})`;
385
+ if (merged.kind === "created")
386
+ ctx.out(`Wrote ${MD_FILE} — ${summary}. Commit it: the next agent reads it first.`);
387
+ else if (merged.kind === "replaced")
388
+ ctx.out(`Replaced ${MD_FILE} — ${summary}; the previous content was kept below the notes line.`);
389
+ else
390
+ ctx.out(`Updated ${MD_FILE} — ${summary}; the notes below the notes line were kept.`);
391
+ }
370
392
  /* ---- wave 2: the scanner ---- */
371
393
  function describeScan(result, schema) {
372
394
  const singles = new Set(schema.collections.filter((c) => c.kind === "single").map((c) => c.key));
@@ -478,6 +500,7 @@ export async function doctor(ctx, flags) {
478
500
  const conn = found ? findConnection(store, api, found.config.project) : undefined;
479
501
  let status = null;
480
502
  let schema = null;
503
+ let guide = null;
481
504
  let authError = null;
482
505
  if (conn) {
483
506
  const session = new Session(store, ctx.credentialsFile, conn, ctx.fetch);
@@ -488,6 +511,15 @@ export async function doctor(ctx, flags) {
488
511
  catch (err) {
489
512
  authError = err instanceof Error ? err.message : String(err);
490
513
  }
514
+ if (status) {
515
+ // the guide is one more read; an API without it must not fail the sign-in line
516
+ try {
517
+ guide = await session.get("/v1/agent/mapled-md");
518
+ }
519
+ catch {
520
+ guide = "unavailable";
521
+ }
522
+ }
491
523
  }
492
524
  const checks = [];
493
525
  checks.push(dr.checkLink(found ? { path: path.relative(ctx.cwd, found.path) || CONFIG_FILE } : null, status?.project.name ?? conn?.projectName ?? null));
@@ -510,6 +542,7 @@ export async function doctor(ctx, flags) {
510
542
  }
511
543
  }
512
544
  checks.push(dr.checkManifestFile(localManifest ? { manifest: localManifest.manifest, problems: localManifest.problems } : null, remoteManifest));
545
+ checks.push(checkMapledMd(await readFile(path.join(dir, MD_FILE), "utf8").catch(() => null), guide));
513
546
  checks.push(dr.checkEnv(await dr.envNames(dir), ctx.env));
514
547
  checks.push(dr.checkSecrets(await dr.gitFacts(dir)));
515
548
  checks.push(dr.checkSdk(await dr.installedVersion(dir, "@mapled/next"), status?.sdk["@mapled/next"]));
package/dist/index.js CHANGED
@@ -3,12 +3,13 @@ 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, scan, schemaDiff, schemaPull, } from "./commands.js";
6
+ import { bindingsPull, bindingsPush, doctor, 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
10
- for the content, keep the schema pin and the site manifest in step
11
- with the code, check the integration (§31, waves 1 and 2). */
10
+ for the content, keep the schema pin, the site manifest and MAPLED.md
11
+ in step with the code, check the integration (§31, waves 1 and 2;
12
+ §21.3). */
12
13
  const HELP = `mapled — the Mapled CLI
13
14
 
14
15
  Usage
@@ -22,6 +23,7 @@ Usage
22
23
  mapled manifest validate [--json] Check mapled/manifest.json before pushing it
23
24
  mapled bindings push [--dry-run] Push mapled/manifest.json so Mapled grades every binding
24
25
  mapled bindings pull [--force] Write the pushed manifest into mapled/manifest.json
26
+ mapled md pull [--force] Write MAPLED.md, the guide for the next agent, from the project
25
27
  mapled doctor [--json] Check the integration end to end
26
28
 
27
29
  Options
@@ -103,6 +105,9 @@ async function main(argv) {
103
105
  case "bindings pull":
104
106
  await bindingsPull(ctx, flags);
105
107
  return 0;
108
+ case "md pull":
109
+ await mdPull(ctx, flags);
110
+ return 0;
106
111
  case "doctor":
107
112
  return doctor(ctx, flags);
108
113
  case "auth":
@@ -111,6 +116,7 @@ async function main(argv) {
111
116
  case "schema":
112
117
  case "manifest":
113
118
  case "bindings":
119
+ case "md":
114
120
  console.log(HELP);
115
121
  return 2;
116
122
  default:
package/dist/md.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { Check } from "./doctor.js";
2
+ /** MAPLED.md (§21.3): Mapled renders the guide for the next agent,
3
+ the repository keeps it. `mapled md pull` writes what
4
+ GET /v1/agent/mapled-md answers and keeps everything below the notes
5
+ marker — that part is the repository's own; `doctor` compares the
6
+ rendered part of the local file with a fresh render. The first line
7
+ is a stamp (`<!-- mapled: project=… schema=… manifest=… generated=… -->`)
8
+ and the only line allowed to differ between a fresh file and an
9
+ up-to-date one. */
10
+ export declare const MD_FILE = "MAPLED.md";
11
+ export declare const NOTES_MARKER = "<!-- mapled:notes -->";
12
+ /** GET /v1/agent/mapled-md */
13
+ export type RenderedMd = {
14
+ file: string;
15
+ notesMarker: string;
16
+ markdown: string;
17
+ schemaHash: string;
18
+ manifestVersion: number | null;
19
+ counts: {
20
+ collections: number;
21
+ singles: number;
22
+ forms: number;
23
+ bindings: number;
24
+ pages: number;
25
+ };
26
+ generatedAt: string;
27
+ };
28
+ export type MdStamp = {
29
+ project: string | null;
30
+ schema: string | null;
31
+ manifest: string | null;
32
+ };
33
+ export type MdParts = {
34
+ stamp: MdStamp;
35
+ /** the stamp line as written */
36
+ stampLine: string;
37
+ /** everything between the stamp and the notes marker */
38
+ body: string;
39
+ /** everything after the marker line; null when the file has no marker */
40
+ notes: string | null;
41
+ };
42
+ /** Splits a MAPLED.md written by Mapled; null for any other file. */
43
+ export declare function splitMd(text: string): MdParts | null;
44
+ export type MergedMd = {
45
+ kind: "unchanged";
46
+ } | {
47
+ kind: "created" | "updated" | "replaced";
48
+ text: string;
49
+ };
50
+ /** The file to write: the fresh render, with the repository's notes kept.
51
+ A file Mapled did not write is left alone unless forced — then its
52
+ whole content moves below the notes line, so nothing is lost. */
53
+ export declare function mergeMd(fresh: string, existing: string | null, opts?: {
54
+ force?: boolean;
55
+ }): MergedMd;
56
+ /** `doctor`'s line: is the repository's guide what Mapled would render now?
57
+ `fresh` is null without a sign-in and "unavailable" when Mapled
58
+ answered the other calls but not this one (an API without the route). */
59
+ export declare function checkMapledMd(local: string | null, fresh: RenderedMd | null | "unavailable"): Check;
package/dist/md.js ADDED
@@ -0,0 +1,110 @@
1
+ import { CliError } from "./errors.js";
2
+ /** MAPLED.md (§21.3): Mapled renders the guide for the next agent,
3
+ the repository keeps it. `mapled md pull` writes what
4
+ GET /v1/agent/mapled-md answers and keeps everything below the notes
5
+ marker — that part is the repository's own; `doctor` compares the
6
+ rendered part of the local file with a fresh render. The first line
7
+ is a stamp (`<!-- mapled: project=… schema=… manifest=… generated=… -->`)
8
+ and the only line allowed to differ between a fresh file and an
9
+ up-to-date one. */
10
+ export const MD_FILE = "MAPLED.md";
11
+ export const NOTES_MARKER = "<!-- mapled:notes -->";
12
+ /** Splits a MAPLED.md written by Mapled; null for any other file. */
13
+ export function splitMd(text) {
14
+ const lines = text.split("\n");
15
+ const first = lines[0] ?? "";
16
+ const m = /^<!-- mapled: (.*) -->\s*$/.exec(first);
17
+ if (!m)
18
+ return null;
19
+ const stamp = { project: null, schema: null, manifest: null };
20
+ for (const pair of m[1].split(/\s+/)) {
21
+ const eq = pair.indexOf("=");
22
+ if (eq === -1)
23
+ continue;
24
+ const key = pair.slice(0, eq);
25
+ const value = pair.slice(eq + 1);
26
+ if (key === "project" || key === "schema" || key === "manifest")
27
+ stamp[key] = value;
28
+ }
29
+ const marker = lines.indexOf(NOTES_MARKER, 1);
30
+ const body = lines.slice(1, marker === -1 ? lines.length : marker).join("\n");
31
+ const notes = marker === -1 ? null : lines.slice(marker + 1).join("\n");
32
+ return { stamp, stampLine: first, body, notes };
33
+ }
34
+ /** The same render: the body and what the stamp says about it, never
35
+ the time it was rendered at. */
36
+ function sameRender(a, b) {
37
+ return a.body === b.body && a.stamp.schema === b.stamp.schema && a.stamp.manifest === b.stamp.manifest;
38
+ }
39
+ /** The file to write: the fresh render, with the repository's notes kept.
40
+ A file Mapled did not write is left alone unless forced — then its
41
+ whole content moves below the notes line, so nothing is lost. */
42
+ export function mergeMd(fresh, existing, opts = {}) {
43
+ const next = splitMd(fresh);
44
+ if (!next || next.notes === null)
45
+ throw new CliError("Mapled answered a guide without its stamp or notes marker — update @mapled/cli and try again.");
46
+ if (existing === null)
47
+ return { kind: "created", text: fresh };
48
+ const current = splitMd(existing);
49
+ if (!current) {
50
+ if (!opts.force) {
51
+ throw new CliError(`${MD_FILE} here wasn't written by Mapled. Pass --force to replace it — the current content moves below the notes line, nothing is lost.`);
52
+ }
53
+ const kept = existing.replace(/\s+$/, "");
54
+ return { kind: "replaced", text: `${next.stampLine}\n${next.body}\n${NOTES_MARKER}\n## Notes\n\n${kept}\n` };
55
+ }
56
+ if (sameRender(current, next) && current.notes !== null)
57
+ return { kind: "unchanged" };
58
+ const notes = current.notes === null || current.notes.trim() === "" ? next.notes : current.notes;
59
+ return { kind: "updated", text: `${next.stampLine}\n${next.body}\n${NOTES_MARKER}\n${notes}` };
60
+ }
61
+ /** `doctor`'s line: is the repository's guide what Mapled would render now?
62
+ `fresh` is null without a sign-in and "unavailable" when Mapled
63
+ answered the other calls but not this one (an API without the route). */
64
+ export function checkMapledMd(local, fresh) {
65
+ const label = "MAPLED.md";
66
+ if (local === null) {
67
+ return { key: "mapled_md", label, status: "skipped", detail: `No ${MD_FILE} — run \`mapled md pull\` so the next agent starts from the current integration.` };
68
+ }
69
+ if (fresh === null)
70
+ return { key: "mapled_md", label, status: "skipped", detail: "Sign in to compare with Mapled." };
71
+ if (fresh === "unavailable") {
72
+ return { key: "mapled_md", label, status: "skipped", detail: "Mapled didn't answer with a guide — update @mapled/cli, or try again later." };
73
+ }
74
+ const current = splitMd(local);
75
+ if (!current) {
76
+ return {
77
+ key: "mapled_md",
78
+ label,
79
+ status: "warning",
80
+ detail: `${MD_FILE} wasn't written by Mapled — run \`mapled md pull --force\` to replace it (the current content moves below the notes line).`,
81
+ };
82
+ }
83
+ const next = splitMd(fresh.markdown);
84
+ const manifest = fresh.manifestVersion === null ? "none" : `v${fresh.manifestVersion}`;
85
+ if (next && sameRender(current, next) && current.notes !== null) {
86
+ return {
87
+ key: "mapled_md",
88
+ label,
89
+ status: "passed",
90
+ detail: `${MD_FILE} matches the schema (${fresh.schemaHash})${fresh.manifestVersion === null ? "" : ` and manifest ${manifest}`}`,
91
+ };
92
+ }
93
+ if (current.stamp.schema && current.stamp.schema !== fresh.schemaHash) {
94
+ return {
95
+ key: "mapled_md",
96
+ label,
97
+ status: "warning",
98
+ detail: `${MD_FILE} describes schema ${current.stamp.schema}; the schema is ${fresh.schemaHash} — run \`mapled md pull\`.`,
99
+ };
100
+ }
101
+ if (current.stamp.manifest && current.stamp.manifest !== manifest) {
102
+ return {
103
+ key: "mapled_md",
104
+ label,
105
+ status: "warning",
106
+ detail: `${MD_FILE} describes manifest ${current.stamp.manifest}; ${manifest === "none" ? "none is pushed now" : `${manifest} is pushed`} — run \`mapled md pull\`.`,
107
+ };
108
+ }
109
+ return { key: "mapled_md", label, status: "warning", detail: `${MD_FILE} is out of date — run \`mapled md pull\`.` };
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapled/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content and check the site's integration.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mapled.io",