@mapled/cli 0.3.0 → 0.4.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
@@ -121,7 +121,7 @@ npx @mapled/cli bindings pull
121
121
 
122
122
  `manifest validate` checks `mapled/manifest.json` the way Mapled will: the shape and limits, duplicate keys, and — against the schema pin or the live schema — collections and fields that don't exist and targets that don't fit the field type, plus files the repository doesn't have. Problems exit 1; warnings only tell you what Mapled would grade down.
123
123
 
124
- `bindings push` sends the manifest to Mapled (the same call your AI agent's `push_site_manifest` makes). Mapled grades every binding — healthy, type mismatch, outdated, missing on site — and the result shows up in **Structure → Bindings**. `--dry-run` validates and stops. Pushing bindings changes structure, so it needs the builder plan.
124
+ `bindings push` sends the manifest to Mapled (the same call your AI agent's `push_site_manifest` makes). Mapled grades every binding — healthy, type mismatch, outdated, missing on site — and the result shows up in **Structure → Bindings**. The push also records the integration hash: one marker over the schema and these bindings, what the site is synced with from now on. `--dry-run` validates and stops. Pushing bindings changes structure, so it needs the builder plan.
125
125
 
126
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`.
127
127
 
@@ -133,7 +133,7 @@ npx @mapled/cli md pull
133
133
 
134
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
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.
136
+ The first line is a stamp with the schema hash, the manifest version and the integration hash. 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
137
 
138
138
  ## Check the integration
139
139
 
@@ -149,6 +149,7 @@ Mapled doctor — Dolphin Landing
149
149
  ✓ Generated types mapled-types.ts matches the schema (3f9a1c2b4d5e)
150
150
  ✓ Schema pin mapled/schema.json matches the schema (3f9a1c2b4d5e)
151
151
  ✓ Site manifest mapled/manifest.json is pushed as manifest v3 (14 bindings on 3 pages)
152
+ ✓ Integration In sync with Mapled (7c1d9e0f2a3b)
152
153
  ✓ MAPLED.md MAPLED.md matches the schema (3f9a1c2b4d5e) and manifest v3
153
154
  ✓ Environment MAPLED_KEY and MAPLED_WEBHOOK_SECRET in .env.local
154
155
  ✓ Secrets in git No env files or Mapled secrets are tracked.
@@ -164,7 +165,7 @@ Mapled doctor — Dolphin Landing
164
165
  1 warning.
165
166
  ```
166
167
 
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.
168
+ The repository side: the link, the sign-in, the generated types, the schema pin, the site manifest (and whether it matches what was pushed), the integration hash — one line saying whether the pin and the manifest here describe the same integration Mapled holds, and if not, what moved (the schema, the bindings, or edits made in Mapled) and which command catches up — `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.
168
169
 
169
170
  Exit code 1 when something is marked ✗; `--json` prints the checks for scripts and agents.
170
171
 
package/dist/commands.js CHANGED
@@ -332,7 +332,8 @@ export async function bindingsPush(ctx, flags) {
332
332
  graded.push(`${s.missing_on_site} missing on site`);
333
333
  if (s.disabled)
334
334
  graded.push(`${s.disabled} disabled`);
335
- ctx.out(`Pushed ${MANIFEST_FILE} to ${session.conn.projectName} — manifest v${result.manifest.version}, ${plural(body.bindings.length, "binding")}: ${graded.join(", ")}.`);
335
+ const recorded = result.manifest.integrationHash ? ` (integration ${result.manifest.integrationHash})` : "";
336
+ ctx.out(`Pushed ${MANIFEST_FILE} to ${session.conn.projectName} — manifest v${result.manifest.version}${recorded}, ${plural(body.bindings.length, "binding")}: ${graded.join(", ")}.`);
336
337
  for (const w of result.warnings)
337
338
  ctx.out(`${paint("warning", GLYPH.warning, color)} ${w}`);
338
339
  if ((s.type_mismatch ?? 0) + (s.outdated ?? 0) + (s.missing_on_site ?? 0) > 0 || result.warnings.length > 0) {
@@ -542,6 +543,7 @@ export async function doctor(ctx, flags) {
542
543
  }
543
544
  }
544
545
  checks.push(dr.checkManifestFile(localManifest ? { manifest: localManifest.manifest, problems: localManifest.problems } : null, remoteManifest));
546
+ checks.push(dr.checkIntegration({ pin: pin?.pin ?? null, manifest: localManifest?.manifest ?? null }, conn && status ? { integration: status.integration, manifest: remoteManifest } : null));
545
547
  checks.push(checkMapledMd(await readFile(path.join(dir, MD_FILE), "utf8").catch(() => null), guide));
546
548
  checks.push(dr.checkEnv(await dr.envNames(dir), ctx.env));
547
549
  checks.push(dr.checkSecrets(await dr.gitFacts(dir)));
package/dist/doctor.d.ts CHANGED
@@ -49,9 +49,25 @@ export type IntegrationStatus = {
49
49
  } | null;
50
50
  summary: Record<string, number>;
51
51
  };
52
+ /** the integration hash (§21.4); missing from an API older than the hash */
53
+ integration?: IntegrationState;
52
54
  previewPath: string;
53
55
  sdk: Record<string, string>;
54
56
  };
57
+ /** Whether the site's integration still matches what Mapled holds:
58
+ `hash` is the live schema plus the bindings as Mapled holds them now,
59
+ `synced` what the last manifest push recorded. */
60
+ export type IntegrationState = {
61
+ schemaHash: string;
62
+ hash: string | null;
63
+ synced: {
64
+ hash: string;
65
+ schemaHash: string;
66
+ } | null;
67
+ inSync: boolean | null;
68
+ schemaChanged: boolean;
69
+ bindingsChanged: boolean;
70
+ };
55
71
  export declare function ago(iso: string): string;
56
72
  export declare const ENV_FILES: string[];
57
73
  /** Variable NAMES defined in the repository's env files. Values stay in
@@ -111,10 +127,26 @@ export type StoredManifest = {
111
127
  version: number;
112
128
  clientName: string;
113
129
  createdAt: string;
130
+ /** what the push recorded; null when it predates the integration hash, missing from an older API */
131
+ schemaHash?: string | null;
132
+ integrationHash?: string | null;
114
133
  manifest: Manifest;
115
134
  };
116
135
  export declare function checkManifestFile(local: {
117
136
  manifest: Manifest;
118
137
  problems: Problem[];
119
138
  } | null, remote: StoredManifest | null | "unknown"): Check;
139
+ /** The one line that says whether the repository and Mapled describe the
140
+ same integration (§21.4): the hash over mapled/schema.json and
141
+ mapled/manifest.json here against the hash Mapled computes over the
142
+ live schema and the bindings it holds. The two lines above it say
143
+ what differs; this one says whether anything does, and whether the
144
+ last push recorded it. */
145
+ export declare function checkIntegration(local: {
146
+ pin: SchemaPin | null;
147
+ manifest: Manifest | null;
148
+ }, remote: {
149
+ integration: IntegrationState | undefined;
150
+ manifest: StoredManifest | null | "unknown";
151
+ } | null): Check;
120
152
  export declare const REMOTE_CHECKS: [string, string][];
package/dist/doctor.js CHANGED
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
+ import { integrationHash } from "./integrationHash.js";
5
6
  import { compareManifests, manifestSummary, MANIFEST_FILE } from "./manifest.js";
6
7
  import { SCHEMA_FILE } from "./pin.js";
7
8
  import { splitGenerated } from "./types.js";
@@ -462,6 +463,73 @@ export function checkManifestFile(local, remote) {
462
463
  detail: `${MANIFEST_FILE} differs from the pushed manifest (v${remote.version}): ${parts.join(", ")} — run \`mapled bindings push\`.`,
463
464
  };
464
465
  }
466
+ /** The one line that says whether the repository and Mapled describe the
467
+ same integration (§21.4): the hash over mapled/schema.json and
468
+ mapled/manifest.json here against the hash Mapled computes over the
469
+ live schema and the bindings it holds. The two lines above it say
470
+ what differs; this one says whether anything does, and whether the
471
+ last push recorded it. */
472
+ export function checkIntegration(local, remote) {
473
+ const key = "integration";
474
+ const label = "Integration";
475
+ if (!local.pin || !local.manifest) {
476
+ const missing = [
477
+ local.pin ? null : `${SCHEMA_FILE} (\`mapled schema pull\`)`,
478
+ local.manifest ? null : `${MANIFEST_FILE} (\`mapled scan --write\`)`,
479
+ ].filter((s) => s !== null);
480
+ return { key, label, status: "skipped", detail: `Needs ${missing.join(" and ")} to compute the integration hash.` };
481
+ }
482
+ if (!remote)
483
+ return { key, label, status: "skipped", detail: "Sign in to compare with Mapled." };
484
+ const theirs = remote.integration;
485
+ if (!theirs) {
486
+ return { key, label, status: "skipped", detail: "Mapled didn't answer with an integration hash — update @mapled/cli, or try again later." };
487
+ }
488
+ const mine = integrationHash(local.pin.hash, local.manifest);
489
+ if (theirs.hash === null) {
490
+ return { key, label, status: "skipped", detail: `Nothing is pushed yet — \`mapled bindings push\` records the first integration hash (${mine} here).` };
491
+ }
492
+ if (mine === theirs.hash) {
493
+ if (theirs.synced === null) {
494
+ return { key, label, status: "warning", detail: `Matches Mapled (${mine}), but the last push predates the integration hash — run \`mapled bindings push\` to record it.` };
495
+ }
496
+ if (theirs.inSync === false) {
497
+ return {
498
+ key,
499
+ label,
500
+ status: "warning",
501
+ detail: `Matches Mapled (${mine}), but the last push was against schema ${theirs.synced.schemaHash} — run \`mapled bindings push\` to record the sync.`,
502
+ };
503
+ }
504
+ return { key, label, status: "passed", detail: `In sync with Mapled (${mine})` };
505
+ }
506
+ const reasons = [];
507
+ const fixes = [];
508
+ if (local.pin.hash !== theirs.schemaHash) {
509
+ reasons.push(`schema ${local.pin.hash} → ${theirs.schemaHash}`);
510
+ fixes.push("`mapled schema diff`");
511
+ }
512
+ if (remote.manifest && remote.manifest !== "unknown") {
513
+ const diff = compareManifests(local.manifest, remote.manifest.manifest);
514
+ if (!diff.same) {
515
+ const parts = [];
516
+ if (diff.onlyLocal.length > 0)
517
+ parts.push(`${diff.onlyLocal.length} not pushed`);
518
+ if (diff.changed.length > 0)
519
+ parts.push(`${diff.changed.length} changed`);
520
+ if (diff.onlyRemote.length > 0)
521
+ parts.push(`${diff.onlyRemote.length} only in Mapled`);
522
+ reasons.push(`the manifest differs (${parts.join(", ")}${theirs.bindingsChanged ? " — edited in Mapled" : ""})`);
523
+ // Mapled's edits win when the repository adds nothing of its own; `pull` refuses a differing file without --force
524
+ fixes.push(theirs.bindingsChanged && diff.onlyLocal.length === 0 ? "`mapled bindings pull --force`" : "`mapled bindings push`");
525
+ }
526
+ }
527
+ if (reasons.length === 0) {
528
+ reasons.push(`the manifest's pages or notes differ (${mine} here, ${theirs.hash} in Mapled)`);
529
+ fixes.push("`mapled bindings push`");
530
+ }
531
+ return { key, label, status: "warning", detail: `Out of sync — ${reasons.join(", ")}. Run ${fixes.join(" and ")}.` };
532
+ }
465
533
  export const REMOTE_CHECKS = [
466
534
  ["reads", "Site reads content"],
467
535
  ["webhook", "Publish webhook"],
@@ -0,0 +1,35 @@
1
+ /** The integration hash (§21.4): one marker over everything the site's
2
+ integration was written against — the schema (its hash, the twelve hex
3
+ characters in mapled-types.ts and mapled/schema.json) and the site
4
+ manifest (bindings, pages, framework, notes). Mapled records it on
5
+ every manifest push and computes it again on read; the repository
6
+ computes the same value from mapled/schema.json and
7
+ mapled/manifest.json, and `mapled doctor` compares the two in one
8
+ line. This is a literal port of apps/api/src/lib/integrationHash.ts —
9
+ the API's tests run both over one manifest and expect one hash.
10
+ Change the two together.
11
+
12
+ Bindings and pages are sorted, so the order in a file never matters;
13
+ optional fields take their defaults (`required: true` written or
14
+ omitted is the same integration, an empty component is none). */
15
+ export type HashedBinding = {
16
+ key: string;
17
+ page: string;
18
+ component?: string | null;
19
+ file?: string | null;
20
+ collection: string;
21
+ field?: string | null;
22
+ target: string;
23
+ required?: boolean | null;
24
+ };
25
+ export type HashedManifest = {
26
+ framework?: string | null;
27
+ integrationMode?: string | null;
28
+ pages?: {
29
+ route: string;
30
+ file?: string | null;
31
+ }[] | null;
32
+ bindings: HashedBinding[];
33
+ notes?: string[] | null;
34
+ };
35
+ export declare function integrationHash(schemaHash: string, manifest: HashedManifest): string;
@@ -0,0 +1,26 @@
1
+ import { createHash } from "node:crypto";
2
+ const byText = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
3
+ export function integrationHash(schemaHash, manifest) {
4
+ const projection = {
5
+ schema: schemaHash,
6
+ framework: manifest.framework || null,
7
+ integrationMode: manifest.integrationMode || null,
8
+ pages: (manifest.pages ?? [])
9
+ .map((p) => ({ route: p.route, file: p.file || null }))
10
+ .sort((a, b) => byText(a.route, b.route) || byText(a.file ?? "", b.file ?? "")),
11
+ bindings: manifest.bindings
12
+ .map((b) => ({
13
+ key: b.key,
14
+ page: b.page,
15
+ collection: b.collection,
16
+ field: b.field || null,
17
+ target: b.target,
18
+ required: b.required ?? true,
19
+ component: b.component || null,
20
+ file: b.file || null,
21
+ }))
22
+ .sort((a, b) => byText(a.key, b.key)),
23
+ notes: manifest.notes ?? [],
24
+ };
25
+ return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 12);
26
+ }
package/dist/md.d.ts CHANGED
@@ -4,9 +4,9 @@ import type { Check } from "./doctor.js";
4
4
  GET /v1/agent/mapled-md answers and keeps everything below the notes
5
5
  marker — that part is the repository's own; `doctor` compares the
6
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. */
7
+ is a stamp (`<!-- mapled: project=… schema=… manifest=… integration=…
8
+ generated=… -->`) and the only line allowed to differ between a fresh
9
+ file and an up-to-date one — by its time. */
10
10
  export declare const MD_FILE = "MAPLED.md";
11
11
  export declare const NOTES_MARKER = "<!-- mapled:notes -->";
12
12
  /** GET /v1/agent/mapled-md */
@@ -15,6 +15,8 @@ export type RenderedMd = {
15
15
  notesMarker: string;
16
16
  markdown: string;
17
17
  schemaHash: string;
18
+ /** the integration hash (§21.4); missing from an API older than the hash */
19
+ integrationHash?: string | null;
18
20
  manifestVersion: number | null;
19
21
  counts: {
20
22
  collections: number;
@@ -29,6 +31,7 @@ export type MdStamp = {
29
31
  project: string | null;
30
32
  schema: string | null;
31
33
  manifest: string | null;
34
+ integration: string | null;
32
35
  };
33
36
  export type MdParts = {
34
37
  stamp: MdStamp;
package/dist/md.js CHANGED
@@ -4,9 +4,9 @@ import { CliError } from "./errors.js";
4
4
  GET /v1/agent/mapled-md answers and keeps everything below the notes
5
5
  marker — that part is the repository's own; `doctor` compares the
6
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. */
7
+ is a stamp (`<!-- mapled: project=… schema=… manifest=… integration=…
8
+ generated=… -->`) and the only line allowed to differ between a fresh
9
+ file and an up-to-date one — by its time. */
10
10
  export const MD_FILE = "MAPLED.md";
11
11
  export const NOTES_MARKER = "<!-- mapled:notes -->";
12
12
  /** Splits a MAPLED.md written by Mapled; null for any other file. */
@@ -16,14 +16,14 @@ export function splitMd(text) {
16
16
  const m = /^<!-- mapled: (.*) -->\s*$/.exec(first);
17
17
  if (!m)
18
18
  return null;
19
- const stamp = { project: null, schema: null, manifest: null };
19
+ const stamp = { project: null, schema: null, manifest: null, integration: null };
20
20
  for (const pair of m[1].split(/\s+/)) {
21
21
  const eq = pair.indexOf("=");
22
22
  if (eq === -1)
23
23
  continue;
24
24
  const key = pair.slice(0, eq);
25
25
  const value = pair.slice(eq + 1);
26
- if (key === "project" || key === "schema" || key === "manifest")
26
+ if (key === "project" || key === "schema" || key === "manifest" || key === "integration")
27
27
  stamp[key] = value;
28
28
  }
29
29
  const marker = lines.indexOf(NOTES_MARKER, 1);
@@ -34,7 +34,10 @@ export function splitMd(text) {
34
34
  /** The same render: the body and what the stamp says about it, never
35
35
  the time it was rendered at. */
36
36
  function sameRender(a, b) {
37
- return a.body === b.body && a.stamp.schema === b.stamp.schema && a.stamp.manifest === b.stamp.manifest;
37
+ return (a.body === b.body &&
38
+ a.stamp.schema === b.stamp.schema &&
39
+ a.stamp.manifest === b.stamp.manifest &&
40
+ a.stamp.integration === b.stamp.integration);
38
41
  }
39
42
  /** The file to write: the fresh render, with the repository's notes kept.
40
43
  A file Mapled did not write is left alone unless forced — then its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapled/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",