@mapled/cli 0.7.1 → 0.8.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
@@ -175,7 +175,7 @@ Mapled doctor — Dolphin Landing
175
175
  ✓ Environment MAPLED_KEY and MAPLED_WEBHOOK_SECRET in .env.local
176
176
  ✓ Secrets in git No env files or Mapled secrets are tracked.
177
177
  ✓ @mapled/next 0.8.0 (current)
178
- ✓ mapled CLI 0.7.1 (current)
178
+ ✓ mapled CLI 0.8.0 (current)
179
179
  ✓ Revalidation route app/api/mapled/revalidate/route.ts
180
180
  ⚠ Preview route app/api/mapled/preview/route.ts is missing — mount createPreviewHandler from "@mapled/next/server" so Preview from Mapled works.
181
181
  ✓ Site reads content Last read 3m ago
@@ -188,6 +188,8 @@ Mapled doctor — Dolphin Landing
188
188
 
189
189
  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.
190
190
 
191
+ A site rendered in the visitor's browser — a React single-page app (Vite and the like), plain HTML — is checked for what it has: the package line is `@mapled/react` (or `@mapled/client`), Environment wants `VITE_MAPLED_KEY` and no webhook secret, and the routes and the webhook read ○ «Not needed» — such a site shows a publish on its next load and trades Mapled's preview link on the page itself. doctor takes the framework from `mapled.json`, or works it out from `package.json`. Whatever the framework, a server key or a signing secret under a public prefix (`VITE_…`, `NEXT_PUBLIC_…`) is a ✗: it would ship to every visitor.
192
+
191
193
  Exit code 1 when something is marked ✗; `--json` prints the checks for scripts and agents.
192
194
 
193
195
  ## Options
package/dist/commands.js CHANGED
@@ -142,7 +142,8 @@ async function openSession(ctx, flags) {
142
142
  export async function generate(ctx, flags) {
143
143
  const { found, session } = await openSession(ctx, flags);
144
144
  const schema = await session.get("/v1/agent/schema");
145
- const generated = generateTypes(schema, { projectName: session.conn.projectName });
145
+ const framework = found.config.framework ?? (await dr.detectFramework(found.dir));
146
+ const generated = generateTypes(schema, { projectName: session.conn.projectName, sdk: dr.sdkOf(framework) });
146
147
  const out = stringFlag(flags, "out") ?? found.config.types ?? DEFAULT_TYPES_PATH;
147
148
  const file = path.resolve(found.dir, out);
148
149
  await writeFile(file, generated.text);
@@ -582,7 +583,7 @@ export async function doctor(ctx, flags) {
582
583
  checks.push(dr.checkAuth({ conn: conn ?? null, status, error: authError }));
583
584
  const typesPath = found?.config.types ?? DEFAULT_TYPES_PATH;
584
585
  const existing = await readFile(path.join(dir, typesPath), "utf8").catch(() => null);
585
- checks.push(dr.checkTypes(existing, schema ? generateTypes(schema, { projectName: status?.project.name }) : null, typesPath));
586
+ checks.push(dr.checkTypes(existing, schema ? generateTypes(schema, { projectName: status?.project.name, sdk: dr.sdkOf(found?.config.framework ?? (await dr.detectFramework(dir))) }) : null, typesPath));
586
587
  const pin = found ? await readPin(dir).catch(() => null) : null;
587
588
  const livePin = schema && found ? pinSchema(schema, found.config.project) : null;
588
589
  const pinChanges = pin && livePin ? diffSchema(pin.pin.collections, livePin.collections) : null;
@@ -600,16 +601,19 @@ export async function doctor(ctx, flags) {
600
601
  checks.push(dr.checkManifestFile(localManifest ? { manifest: localManifest.manifest, problems: localManifest.problems } : null, remoteManifest));
601
602
  checks.push(dr.checkIntegration({ pin: pin?.pin ?? null, manifest: localManifest?.manifest ?? null }, conn && status ? { integration: status.integration, manifest: remoteManifest } : null));
602
603
  checks.push(checkMapledMd(await readFile(path.join(dir, MD_FILE), "utf8").catch(() => null), guide));
603
- checks.push(dr.checkEnv(await dr.envNames(dir), ctx.env));
604
+ const framework = found?.config.framework ?? (await dr.detectFramework(dir));
605
+ // a site rendered in the browser has no webhook, no routes and no secret to check
606
+ const browser = dr.rendersInBrowser(framework);
607
+ const sdk = dr.sdkOf(framework);
608
+ checks.push(dr.checkEnv(await dr.envNames(dir), ctx.env, framework));
604
609
  checks.push(dr.checkSecrets(await dr.gitFacts(dir)));
605
- checks.push(dr.checkSdk(await dr.installedVersion(dir, "@mapled/next"), status?.sdk["@mapled/next"]));
610
+ checks.push(dr.checkSdk(await dr.installedVersion(dir, sdk), status?.sdk[sdk], sdk));
606
611
  checks.push(dr.checkCli(ctx.version, status?.sdk["@mapled/cli"]));
607
- const framework = found?.config.framework ?? (await dr.detectFramework(dir));
608
612
  checks.push(dr.checkRoute("revalidate", await dr.findRoute(dir, "revalidate"), framework));
609
613
  checks.push(dr.checkRoute("preview", await dr.findRoute(dir, "preview"), framework));
610
614
  if (status) {
611
615
  checks.push(dr.checkReads(status.delivery.lastReadAt));
612
- checks.push(dr.checkWebhook(status.webhook));
616
+ checks.push(dr.checkWebhook(status.webhook, { local: browser, remote: status.site }));
613
617
  let origin = null;
614
618
  try {
615
619
  origin = status.webhook.url ? new URL(status.webhook.url).origin : null;
@@ -617,8 +621,15 @@ export async function doctor(ctx, flags) {
617
621
  catch {
618
622
  origin = null;
619
623
  }
620
- const probe = origin ? await dr.probePreview(origin, status.previewPath, ctx.fetch) : null;
621
- checks.push(dr.checkPreviewOnSite(origin, status.previewPath, probe));
624
+ if (browser) {
625
+ // no route to probe: the page itself, where Mapled knows the site to live
626
+ origin = origin ?? status.site?.url ?? null;
627
+ checks.push(dr.checkPreviewOnSite(origin, status.previewPath, origin ? await dr.probeSite(origin, ctx.fetch) : null, true));
628
+ }
629
+ else {
630
+ const probe = origin ? await dr.probePreview(origin, status.previewPath, ctx.fetch) : null;
631
+ checks.push(dr.checkPreviewOnSite(origin, status.previewPath, probe));
632
+ }
622
633
  checks.push(dr.checkBindings(status.bindings));
623
634
  }
624
635
  else {
package/dist/doctor.d.ts CHANGED
@@ -51,6 +51,12 @@ export type IntegrationStatus = {
51
51
  };
52
52
  /** the integration hash (§21.4); missing from an API older than the hash */
53
53
  integration?: IntegrationState;
54
+ /** what Mapled takes the site for, and where Preview opens it; missing from an older API */
55
+ site?: {
56
+ framework: string;
57
+ rendering: "server" | "browser";
58
+ url: string | null;
59
+ };
54
60
  previewPath: string;
55
61
  sdk: Record<string, string>;
56
62
  };
@@ -87,6 +93,13 @@ export type PackageVersion = {
87
93
  declared: string | null;
88
94
  };
89
95
  export declare function installedVersion(dir: string, pkg: string): Promise<PackageVersion>;
96
+ /** A site rendered in the visitor's browser — a React single-page app,
97
+ plain HTML — reads Mapled from the page: a publish shows on the next
98
+ load, so it has no webhook, no routes to mount and no secret. The two
99
+ keys are the ones Mapled knows by (lib/siteStack.ts of the API). */
100
+ export declare const rendersInBrowser: (framework: string | null | undefined) => boolean;
101
+ /** The first-party package a site of this framework reads through. */
102
+ export declare const sdkOf: (framework: string | null | undefined) => string;
90
103
  export declare function detectFramework(dir: string): Promise<string | undefined>;
91
104
  /** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
92
105
  export declare function compareVersions(a: string, b: string): number;
@@ -101,6 +114,11 @@ export declare function findRoute(dir: string, kind: RouteKind): Promise<RouteFi
101
114
  export declare function probePreview(origin: string, previewPath: string, f: Fetch): Promise<{
102
115
  status: number | null;
103
116
  }>;
117
+ /** A page of a site rendered in the browser: any answer means it is
118
+ deployed (a single-page app's host may say 404 and serve the app anyway). */
119
+ export declare function probeSite(origin: string, f: Fetch): Promise<{
120
+ status: number | null;
121
+ }>;
104
122
  export declare function checkLink(found: {
105
123
  path: string;
106
124
  } | null, projectName: string | null): Check;
@@ -110,16 +128,19 @@ export declare function checkAuth(state: {
110
128
  error: string | null;
111
129
  }): Check;
112
130
  export declare function checkTypes(existing: string | null, generated: Generated | null, typesPath: string): Check;
113
- export declare function checkEnv(names: Map<string, string>, env: NodeJS.ProcessEnv): Check;
131
+ export declare function checkEnv(names: Map<string, string>, env: NodeJS.ProcessEnv, framework?: string | null): Check;
114
132
  export declare function checkSecrets(facts: GitFacts): Check;
115
- export declare function checkSdk(pkg: PackageVersion, latest: string | undefined): Check;
133
+ export declare function checkSdk(pkg: PackageVersion, latest: string | undefined, name?: string): Check;
116
134
  export declare function checkCli(current: string, latest: string | undefined): Check;
117
135
  export declare function checkRoute(kind: RouteKind, found: RouteFile | null, framework: string | undefined): Check;
118
136
  export declare function checkReads(lastReadAt: string | null): Check;
119
- export declare function checkWebhook(webhook: IntegrationStatus["webhook"]): Check;
137
+ export declare function checkWebhook(webhook: IntegrationStatus["webhook"], site?: {
138
+ local: boolean;
139
+ remote: IntegrationStatus["site"];
140
+ }): Check;
120
141
  export declare function checkPreviewOnSite(origin: string | null, previewPath: string, probe: {
121
142
  status: number | null;
122
- } | null): Check;
143
+ } | null, browser?: boolean): Check;
123
144
  export declare function checkBindings(bindings: IntegrationStatus["bindings"]): Check;
124
145
  export declare function checkSchemaPin(pin: SchemaPin | null, liveHash: string | null, changes: SchemaChange[] | null): Check;
125
146
  /** The pushed manifest as `GET /v1/agent/manifest` answers it. */
package/dist/doctor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- import { readFile } from "node:fs/promises";
2
+ import { access, readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { integrationHash } from "./integrationHash.js";
@@ -102,26 +102,43 @@ export async function installedVersion(dir, pkg) {
102
102
  }
103
103
  return { installed, declared };
104
104
  }
105
+ /** A site rendered in the visitor's browser — a React single-page app,
106
+ plain HTML — reads Mapled from the page: a publish shows on the next
107
+ load, so it has no webhook, no routes to mount and no secret. The two
108
+ keys are the ones Mapled knows by (lib/siteStack.ts of the API). */
109
+ export const rendersInBrowser = (framework) => framework === "react-spa" || framework === "plain-html";
110
+ /** The first-party package a site of this framework reads through. */
111
+ export const sdkOf = (framework) => !framework || framework === "nextjs" ? "@mapled/next" : framework === "react-spa" ? "@mapled/react" : "@mapled/client";
105
112
  export async function detectFramework(dir) {
113
+ const hasIndexHtml = () => access(path.join(dir, "index.html")).then(() => true, () => false);
114
+ let deps;
106
115
  try {
107
116
  const raw = await readFile(path.join(dir, "package.json"), "utf8");
108
117
  const p = JSON.parse(raw);
109
- const deps = { ...p.devDependencies, ...p.dependencies };
110
- if (deps.next)
111
- return "nextjs";
112
- if (deps.astro)
113
- return "astro";
114
- if (deps.nuxt)
115
- return "nuxt";
116
- if (deps["@sveltejs/kit"])
117
- return "sveltekit";
118
- if (deps["@remix-run/react"] || deps["react-router"])
119
- return "remix";
120
- return undefined;
118
+ deps = { ...p.devDependencies, ...p.dependencies };
121
119
  }
122
120
  catch {
123
- return undefined;
124
- }
121
+ // no package.json at all: pages written by hand
122
+ return (await hasIndexHtml()) ? "plain-html" : undefined;
123
+ }
124
+ if (deps.next)
125
+ return "nextjs";
126
+ if (deps.astro)
127
+ return "astro";
128
+ if (deps.nuxt)
129
+ return "nuxt";
130
+ if (deps["@sveltejs/kit"])
131
+ return "sveltekit";
132
+ // react-router alone is a library a single-page app routes with; its framework mode brings @react-router/dev
133
+ if (deps["@remix-run/react"] || deps["@react-router/dev"])
134
+ return "remix";
135
+ if (deps.react && (deps.vite || deps["@vitejs/plugin-react"] || deps["@vitejs/plugin-react-swc"] || deps["react-scripts"] || deps["@mapled/react"])) {
136
+ return "react-spa";
137
+ }
138
+ if (!deps.react && !deps.vue && !deps.svelte && !deps["solid-js"] && !deps.express && !deps.fastify && (await hasIndexHtml())) {
139
+ return "plain-html";
140
+ }
141
+ return undefined;
125
142
  }
126
143
  /** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
127
144
  export function compareVersions(a, b) {
@@ -186,6 +203,17 @@ export async function probePreview(origin, previewPath, f) {
186
203
  }
187
204
  }
188
205
  /* ---- the checks ---- */
206
+ /** A page of a site rendered in the browser: any answer means it is
207
+ deployed (a single-page app's host may say 404 and serve the app anyway). */
208
+ export async function probeSite(origin, f) {
209
+ try {
210
+ const res = await f(`${origin}/`, { method: "GET", redirect: "manual", signal: AbortSignal.timeout(5000) });
211
+ return { status: res.status };
212
+ }
213
+ catch {
214
+ return { status: null };
215
+ }
216
+ }
189
217
  export function checkLink(found, projectName) {
190
218
  const label = "Project link";
191
219
  if (!found)
@@ -218,9 +246,36 @@ export function checkTypes(existing, generated, typesPath) {
218
246
  }
219
247
  return { key: "types", label, status: "warning", detail: `${typesPath} is out of date — run \`mapled types generate\`.` };
220
248
  }
221
- export function checkEnv(names, env) {
249
+ /** Prefixes that put an env var into the browser's bundle. */
250
+ const PUBLIC_ENV = /^(VITE_|REACT_APP_|NEXT_PUBLIC_|NUXT_PUBLIC_|PUBLIC_|GATSBY_)/;
251
+ export function checkEnv(names, env, framework) {
222
252
  const label = "Environment";
223
253
  const where = (name) => names.get(name) ?? (env[name] ? "the environment" : null);
254
+ // whatever the framework: a server key or a signing secret under a public prefix ships to every visitor
255
+ const exposed = [...names.keys()].filter((n) => PUBLIC_ENV.test(n) && /MAPLED/.test(n) && /(SERVER_KEY|SECRET)/.test(n));
256
+ if (exposed.length > 0) {
257
+ return {
258
+ key: "env",
259
+ label,
260
+ status: "failed",
261
+ detail: `${exposed.join(", ")} in ${names.get(exposed[0])} is bundled into the browser. Only the delivery key may be: rename it and rotate the value in Integrations.`,
262
+ };
263
+ }
264
+ if (framework === "plain-html") {
265
+ return { key: "env", label, status: "skipped", detail: "No env files: the delivery key sits in the page — the one key that is safe there." };
266
+ }
267
+ if (framework === "react-spa") {
268
+ const name = ["VITE_MAPLED_KEY", "REACT_APP_MAPLED_KEY"].find((n) => where(n));
269
+ if (!name) {
270
+ return {
271
+ key: "env",
272
+ label,
273
+ status: "failed",
274
+ detail: "VITE_MAPLED_KEY isn't set. Put the delivery key (Integrations → Your site) in .env — it is the one key that is safe in a browser.",
275
+ };
276
+ }
277
+ return { key: "env", label, status: "passed", detail: `${name} in ${where(name)} — no webhook secret needed: the site reads in the browser` };
278
+ }
224
279
  const key = where("MAPLED_KEY");
225
280
  const secret = where("MAPLED_WEBHOOK_SECRET");
226
281
  if (!key) {
@@ -268,10 +323,10 @@ export function checkSecrets(facts) {
268
323
  }
269
324
  return { key: "secrets", label, status: "passed", detail: "No env files or Mapled secrets are tracked." };
270
325
  }
271
- export function checkSdk(pkg, latest) {
272
- const label = "@mapled/next";
326
+ export function checkSdk(pkg, latest, name = "@mapled/next") {
327
+ const label = name;
273
328
  if (!pkg.installed && !pkg.declared) {
274
- return { key: "sdk", label, status: "failed", detail: "Not installed. Run `npm install @mapled/next`." };
329
+ return { key: "sdk", label, status: "failed", detail: `Not installed. Run \`npm install ${name}\`.` };
275
330
  }
276
331
  if (!pkg.installed) {
277
332
  return { key: "sdk", label, status: "warning", detail: `${pkg.declared} is declared but not installed — run npm install.` };
@@ -283,7 +338,7 @@ export function checkSdk(pkg, latest) {
283
338
  key: "sdk",
284
339
  label,
285
340
  status: "warning",
286
- detail: `${pkg.installed} installed, ${latest} available — npm install @mapled/next@latest.`,
341
+ detail: `${pkg.installed} installed, ${latest} available — npm install ${name}@latest.`,
287
342
  };
288
343
  }
289
344
  return { key: "sdk", label, status: "passed", detail: `${pkg.installed} (current)` };
@@ -299,6 +354,16 @@ export function checkRoute(kind, found, framework) {
299
354
  const key = `route_${kind}`;
300
355
  const label = kind === "revalidate" ? "Revalidation route" : "Preview route";
301
356
  const handler = ROUTE_HANDLER[kind];
357
+ if (rendersInBrowser(framework)) {
358
+ return {
359
+ key,
360
+ label,
361
+ status: "skipped",
362
+ detail: kind === "revalidate"
363
+ ? "Not needed: the site reads in the browser, so a publish shows on the next load."
364
+ : "Not needed: the client trades Mapled's preview link on the page itself.",
365
+ };
366
+ }
302
367
  if (framework && framework !== "nextjs") {
303
368
  return { key, label, status: "skipped", detail: `No ${kind} route check for ${framework} sites yet.` };
304
369
  }
@@ -334,8 +399,20 @@ export function checkReads(lastReadAt) {
334
399
  }
335
400
  return { key: "reads", label, status: "passed", detail: `Last read ${ago(lastReadAt)}` };
336
401
  }
337
- export function checkWebhook(webhook) {
402
+ export function checkWebhook(webhook, site) {
338
403
  const label = "Publish webhook";
404
+ if (!webhook.url && site?.local) {
405
+ // the repository renders in the browser; does Mapled know?
406
+ if (site.remote && site.remote.rendering !== "browser") {
407
+ return {
408
+ key: "webhook",
409
+ label,
410
+ status: "warning",
411
+ detail: `Mapled takes this site for ${site.remote.framework} and will ask for a webhook: run \`mapled scan --write\` and \`mapled bindings push\` so it knows the site renders in the browser.`,
412
+ };
413
+ }
414
+ return { key: "webhook", label, status: "skipped", detail: "Not needed: the site reads in the browser, so a publish shows on the next load." };
415
+ }
339
416
  if (!webhook.url) {
340
417
  return {
341
418
  key: "webhook",
@@ -377,8 +454,21 @@ export function checkWebhook(webhook) {
377
454
  detail: `The last delivery to ${host} ${reason}. Check the route and MAPLED_WEBHOOK_SECRET, then send a test ping.`,
378
455
  };
379
456
  }
380
- export function checkPreviewOnSite(origin, previewPath, probe) {
457
+ export function checkPreviewOnSite(origin, previewPath, probe, browser = false) {
381
458
  const label = "Preview on the site";
459
+ if (browser) {
460
+ if (!origin || !probe) {
461
+ return {
462
+ key: "preview_site",
463
+ label,
464
+ status: "warning",
465
+ detail: "Mapled doesn't know where the site is deployed — set Production domain in Project settings, or ask your agent (set_site_url).",
466
+ };
467
+ }
468
+ return probe.status === null
469
+ ? { key: "preview_site", label, status: "warning", detail: `${origin} is unreachable from here.` }
470
+ : { key: "preview_site", label, status: "passed", detail: `Opens at ${origin} — the client trades the link in the browser` };
471
+ }
382
472
  if (!origin || !probe) {
383
473
  return { key: "preview_site", label, status: "skipped", detail: "Needs the webhook URL to know where the site lives." };
384
474
  }
package/dist/types.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare function typeName(key: string, kind: "collection" | "single"): st
21
21
  export declare function typeNames(collections: SchemaCollection[]): Map<string, string>;
22
22
  export declare function generateTypes(schema: Schema, opts?: {
23
23
  projectName?: string;
24
+ sdk?: string;
24
25
  }): Generated;
25
26
  /** Splits a generated file into its header and body, with the hash the
26
27
  header names when it has one. */
package/dist/types.js CHANGED
@@ -177,14 +177,18 @@ export function typeNames(collections) {
177
177
  return out;
178
178
  }
179
179
  export function generateTypes(schema, opts = {}) {
180
+ // the package the site reads through: "@mapled/next" unless the framework says otherwise
181
+ const sdk = opts.sdk ?? "@mapled/next";
182
+ // a React app's assetUrl comes from the client its hooks are built on
183
+ const assets = sdk === "@mapled/react" ? "@mapled/client" : sdk;
180
184
  const hash = schemaHash(schema);
181
185
  const names = typeNames(schema.collections);
182
186
  const blocks = [];
183
187
  blocks.push([
184
- "// Types of the published content this site reads through @mapled/next.",
188
+ `// Types of the published content this site reads through ${sdk}.`,
185
189
  "// Sensitive fields never reach the site and are left out.",
186
190
  "",
187
- '/** An asset id — pass it to assetUrl(id, { width }) from "@mapled/next". */',
191
+ `/** An asset id — pass it to assetUrl(id, { width }) from "${assets}". */`,
188
192
  "export type MapledAssetId = string;",
189
193
  '/** The id of a record in another collection — attach it with { expand: ["field"] }. */',
190
194
  "export type MapledRecordId = string;",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mapled/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
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",