@mapled/cli 0.7.1 → 0.9.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.9.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`, 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. Plain HTML installs nothing: the line is `@mapled/vanilla`, read from the script tag in the pages (or from a copy of the file next to them) — ⚠ when the address has no version or an older one — and Generated types and Environment read ○: the fields are named in attributes, the delivery key sits in the page. doctor takes the framework from `mapled.json`, or works it out from `package.json` (no `package.json` and an `index.html` is plain HTML). 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);
@@ -580,9 +581,10 @@ export async function doctor(ctx, flags) {
580
581
  const checks = [];
581
582
  checks.push(dr.checkLink(found ? { path: path.relative(ctx.cwd, found.path) || CONFIG_FILE } : null, status?.project.name ?? conn?.projectName ?? null));
582
583
  checks.push(dr.checkAuth({ conn: conn ?? null, status, error: authError }));
584
+ const framework = found?.config.framework ?? (await dr.detectFramework(dir));
583
585
  const typesPath = found?.config.types ?? DEFAULT_TYPES_PATH;
584
586
  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));
587
+ checks.push(dr.checkTypes(existing, schema ? generateTypes(schema, { projectName: status?.project.name, sdk: dr.sdkOf(framework) }) : null, typesPath, framework));
586
588
  const pin = found ? await readPin(dir).catch(() => null) : null;
587
589
  const livePin = schema && found ? pinSchema(schema, found.config.project) : null;
588
590
  const pinChanges = pin && livePin ? diffSchema(pin.pin.collections, livePin.collections) : null;
@@ -600,16 +602,19 @@ export async function doctor(ctx, flags) {
600
602
  checks.push(dr.checkManifestFile(localManifest ? { manifest: localManifest.manifest, problems: localManifest.problems } : null, remoteManifest));
601
603
  checks.push(dr.checkIntegration({ pin: pin?.pin ?? null, manifest: localManifest?.manifest ?? null }, conn && status ? { integration: status.integration, manifest: remoteManifest } : null));
602
604
  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));
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
+ // plain HTML installs nothing: the runtime comes with a script tag
611
+ checks.push(framework === "plain-html" ? dr.checkScriptTag(await dr.findScriptTag(dir), status?.sdk[sdk]) : dr.checkSdk(await dr.installedVersion(dir, sdk), status?.sdk[sdk], sdk));
606
612
  checks.push(dr.checkCli(ctx.version, status?.sdk["@mapled/cli"]));
607
- const framework = found?.config.framework ?? (await dr.detectFramework(dir));
608
613
  checks.push(dr.checkRoute("revalidate", await dr.findRoute(dir, "revalidate"), framework));
609
614
  checks.push(dr.checkRoute("preview", await dr.findRoute(dir, "preview"), framework));
610
615
  if (status) {
611
616
  checks.push(dr.checkReads(status.delivery.lastReadAt));
612
- checks.push(dr.checkWebhook(status.webhook));
617
+ checks.push(dr.checkWebhook(status.webhook, { local: browser, remote: status.site }));
613
618
  let origin = null;
614
619
  try {
615
620
  origin = status.webhook.url ? new URL(status.webhook.url).origin : null;
@@ -617,8 +622,15 @@ export async function doctor(ctx, flags) {
617
622
  catch {
618
623
  origin = null;
619
624
  }
620
- const probe = origin ? await dr.probePreview(origin, status.previewPath, ctx.fetch) : null;
621
- checks.push(dr.checkPreviewOnSite(origin, status.previewPath, probe));
625
+ if (browser) {
626
+ // no route to probe: the page itself, where Mapled knows the site to live
627
+ origin = origin ?? status.site?.url ?? null;
628
+ checks.push(dr.checkPreviewOnSite(origin, status.previewPath, origin ? await dr.probeSite(origin, ctx.fetch) : null, true));
629
+ }
630
+ else {
631
+ const probe = origin ? await dr.probePreview(origin, status.previewPath, ctx.fetch) : null;
632
+ checks.push(dr.checkPreviewOnSite(origin, status.previewPath, probe));
633
+ }
622
634
  checks.push(dr.checkBindings(status.bindings));
623
635
  }
624
636
  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,25 @@ 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;
103
+ /** How a plain HTML site loads "@mapled/vanilla": a script tag in its
104
+ pages — from a CDN, where the version is in the address, or a copy of
105
+ the single file next to the pages, where it is in the file's first
106
+ line. */
107
+ export type ScriptTag = {
108
+ /** the page or the copy the version was read from */
109
+ file: string;
110
+ /** null — a CDN address without a version: it follows every release */
111
+ version: string | null;
112
+ };
113
+ export declare function findScriptTag(dir: string): Promise<ScriptTag | null>;
114
+ export declare function checkScriptTag(tag: ScriptTag | null, latest: string | undefined): Check;
90
115
  export declare function detectFramework(dir: string): Promise<string | undefined>;
91
116
  /** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
92
117
  export declare function compareVersions(a: string, b: string): number;
@@ -101,6 +126,11 @@ export declare function findRoute(dir: string, kind: RouteKind): Promise<RouteFi
101
126
  export declare function probePreview(origin: string, previewPath: string, f: Fetch): Promise<{
102
127
  status: number | null;
103
128
  }>;
129
+ /** A page of a site rendered in the browser: any answer means it is
130
+ deployed (a single-page app's host may say 404 and serve the app anyway). */
131
+ export declare function probeSite(origin: string, f: Fetch): Promise<{
132
+ status: number | null;
133
+ }>;
104
134
  export declare function checkLink(found: {
105
135
  path: string;
106
136
  } | null, projectName: string | null): Check;
@@ -109,17 +139,20 @@ export declare function checkAuth(state: {
109
139
  status: IntegrationStatus | null;
110
140
  error: string | null;
111
141
  }): Check;
112
- 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;
142
+ export declare function checkTypes(existing: string | null, generated: Generated | null, typesPath: string, framework?: string | null): Check;
143
+ export declare function checkEnv(names: Map<string, string>, env: NodeJS.ProcessEnv, framework?: string | null): Check;
114
144
  export declare function checkSecrets(facts: GitFacts): Check;
115
- export declare function checkSdk(pkg: PackageVersion, latest: string | undefined): Check;
145
+ export declare function checkSdk(pkg: PackageVersion, latest: string | undefined, name?: string): Check;
116
146
  export declare function checkCli(current: string, latest: string | undefined): Check;
117
147
  export declare function checkRoute(kind: RouteKind, found: RouteFile | null, framework: string | undefined): Check;
118
148
  export declare function checkReads(lastReadAt: string | null): Check;
119
- export declare function checkWebhook(webhook: IntegrationStatus["webhook"]): Check;
149
+ export declare function checkWebhook(webhook: IntegrationStatus["webhook"], site?: {
150
+ local: boolean;
151
+ remote: IntegrationStatus["site"];
152
+ }): Check;
120
153
  export declare function checkPreviewOnSite(origin: string | null, previewPath: string, probe: {
121
154
  status: number | null;
122
- } | null): Check;
155
+ } | null, browser?: boolean): Check;
123
156
  export declare function checkBindings(bindings: IntegrationStatus["bindings"]): Check;
124
157
  export declare function checkSchemaPin(pin: SchemaPin | null, liveHash: string | null, changes: SchemaChange[] | null): Check;
125
158
  /** 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, readdir, 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,101 @@ 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" : framework === "plain-html" ? "@mapled/vanilla" : "@mapled/client";
112
+ const SKIPPED_DIRS = new Set(["node_modules", ".git", ".next", ".vercel", ".netlify", "vendor"]);
113
+ export async function findScriptTag(dir) {
114
+ // pages first, breadth first, and not forever: a site's pages sit near its root
115
+ const queue = [{ abs: dir, rel: "", depth: 0 }];
116
+ const copies = [];
117
+ let unpinned = null;
118
+ let seen = 0;
119
+ while (queue.length > 0 && seen < 2000) {
120
+ const at = queue.shift();
121
+ const entries = await readdir(at.abs, { withFileTypes: true }).catch(() => []);
122
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
123
+ seen += 1;
124
+ const rel = at.rel ? `${at.rel}/${entry.name}` : entry.name;
125
+ if (entry.isDirectory()) {
126
+ if (at.depth < 3 && !SKIPPED_DIRS.has(entry.name) && !entry.name.startsWith("."))
127
+ queue.push({ abs: path.join(at.abs, entry.name), rel, depth: at.depth + 1 });
128
+ continue;
129
+ }
130
+ if (/^mapled(\.min)?\.js$/.test(entry.name))
131
+ copies.push(rel);
132
+ if (!/\.html?$/i.test(entry.name))
133
+ continue;
134
+ const text = await readFile(path.join(at.abs, entry.name), "utf8").catch(() => "");
135
+ const pinned = /@mapled\/vanilla@(\d+\.\d+\.\d+)/.exec(text);
136
+ if (pinned)
137
+ return { file: rel, version: pinned[1] };
138
+ if (unpinned === null && /@mapled\/vanilla(@latest)?\//.test(text))
139
+ unpinned = rel;
140
+ }
141
+ }
142
+ for (const rel of copies) {
143
+ const head = (await readFile(path.join(dir, rel), "utf8").catch(() => "")).slice(0, 200);
144
+ const banner = /^\/\*! @mapled\/vanilla (\d+\.\d+\.\d+) /.exec(head);
145
+ if (banner)
146
+ return { file: rel, version: banner[1] };
147
+ }
148
+ return unpinned === null ? null : { file: unpinned, version: null };
149
+ }
150
+ export function checkScriptTag(tag, latest) {
151
+ const key = "sdk";
152
+ const label = "@mapled/vanilla";
153
+ const at = latest ?? "x.y.z";
154
+ if (!tag) {
155
+ return {
156
+ key,
157
+ label,
158
+ status: "failed",
159
+ detail: `No page loads it yet. Put <script src="https://cdn.jsdelivr.net/npm/@mapled/vanilla@${at}/dist/mapled.js" data-mapled-key="…" defer></script> into the pages.`,
160
+ };
161
+ }
162
+ if (tag.version === null) {
163
+ return { key, label, status: "warning", detail: `${tag.file} loads it without a version — that address follows every release. Pin it: @mapled/vanilla@${at}.` };
164
+ }
165
+ if (latest && compareVersions(tag.version, latest) < 0) {
166
+ return { key, label, status: "warning", detail: `${tag.version} in ${tag.file}, ${latest} available — change the version in the script tag.` };
167
+ }
168
+ return { key, label, status: "passed", detail: latest ? `${tag.version} in ${tag.file} (current)` : `${tag.version} in ${tag.file}` };
169
+ }
105
170
  export async function detectFramework(dir) {
171
+ const hasIndexHtml = () => access(path.join(dir, "index.html")).then(() => true, () => false);
172
+ let deps;
106
173
  try {
107
174
  const raw = await readFile(path.join(dir, "package.json"), "utf8");
108
175
  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;
176
+ deps = { ...p.devDependencies, ...p.dependencies };
121
177
  }
122
178
  catch {
123
- return undefined;
124
- }
179
+ // no package.json at all: pages written by hand
180
+ return (await hasIndexHtml()) ? "plain-html" : undefined;
181
+ }
182
+ if (deps.next)
183
+ return "nextjs";
184
+ if (deps.astro)
185
+ return "astro";
186
+ if (deps.nuxt)
187
+ return "nuxt";
188
+ if (deps["@sveltejs/kit"])
189
+ return "sveltekit";
190
+ // react-router alone is a library a single-page app routes with; its framework mode brings @react-router/dev
191
+ if (deps["@remix-run/react"] || deps["@react-router/dev"])
192
+ return "remix";
193
+ if (deps.react && (deps.vite || deps["@vitejs/plugin-react"] || deps["@vitejs/plugin-react-swc"] || deps["react-scripts"] || deps["@mapled/react"])) {
194
+ return "react-spa";
195
+ }
196
+ if (!deps.react && !deps.vue && !deps.svelte && !deps["solid-js"] && !deps.express && !deps.fastify && (await hasIndexHtml())) {
197
+ return "plain-html";
198
+ }
199
+ return undefined;
125
200
  }
126
201
  /** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
127
202
  export function compareVersions(a, b) {
@@ -186,6 +261,17 @@ export async function probePreview(origin, previewPath, f) {
186
261
  }
187
262
  }
188
263
  /* ---- the checks ---- */
264
+ /** A page of a site rendered in the browser: any answer means it is
265
+ deployed (a single-page app's host may say 404 and serve the app anyway). */
266
+ export async function probeSite(origin, f) {
267
+ try {
268
+ const res = await f(`${origin}/`, { method: "GET", redirect: "manual", signal: AbortSignal.timeout(5000) });
269
+ return { status: res.status };
270
+ }
271
+ catch {
272
+ return { status: null };
273
+ }
274
+ }
189
275
  export function checkLink(found, projectName) {
190
276
  const label = "Project link";
191
277
  if (!found)
@@ -206,8 +292,11 @@ export function checkAuth(state) {
206
292
  detail: `${state.status.connection.name} • ${state.status.project.name}`,
207
293
  };
208
294
  }
209
- export function checkTypes(existing, generated, typesPath) {
295
+ export function checkTypes(existing, generated, typesPath, framework) {
210
296
  const label = "Generated types";
297
+ if (framework === "plain-html" && existing === null) {
298
+ return { key: "types", label, status: "skipped", detail: "Not needed: plain HTML names its fields in attributes, not in TypeScript." };
299
+ }
211
300
  if (!generated)
212
301
  return { key: "types", label, status: "skipped", detail: "Sign in to compare with the schema." };
213
302
  if (existing === null) {
@@ -218,9 +307,36 @@ export function checkTypes(existing, generated, typesPath) {
218
307
  }
219
308
  return { key: "types", label, status: "warning", detail: `${typesPath} is out of date — run \`mapled types generate\`.` };
220
309
  }
221
- export function checkEnv(names, env) {
310
+ /** Prefixes that put an env var into the browser's bundle. */
311
+ const PUBLIC_ENV = /^(VITE_|REACT_APP_|NEXT_PUBLIC_|NUXT_PUBLIC_|PUBLIC_|GATSBY_)/;
312
+ export function checkEnv(names, env, framework) {
222
313
  const label = "Environment";
223
314
  const where = (name) => names.get(name) ?? (env[name] ? "the environment" : null);
315
+ // whatever the framework: a server key or a signing secret under a public prefix ships to every visitor
316
+ const exposed = [...names.keys()].filter((n) => PUBLIC_ENV.test(n) && /MAPLED/.test(n) && /(SERVER_KEY|SECRET)/.test(n));
317
+ if (exposed.length > 0) {
318
+ return {
319
+ key: "env",
320
+ label,
321
+ status: "failed",
322
+ 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.`,
323
+ };
324
+ }
325
+ if (framework === "plain-html") {
326
+ return { key: "env", label, status: "skipped", detail: "No env files: the delivery key sits in the page — the one key that is safe there." };
327
+ }
328
+ if (framework === "react-spa") {
329
+ const name = ["VITE_MAPLED_KEY", "REACT_APP_MAPLED_KEY"].find((n) => where(n));
330
+ if (!name) {
331
+ return {
332
+ key: "env",
333
+ label,
334
+ status: "failed",
335
+ 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.",
336
+ };
337
+ }
338
+ return { key: "env", label, status: "passed", detail: `${name} in ${where(name)} — no webhook secret needed: the site reads in the browser` };
339
+ }
224
340
  const key = where("MAPLED_KEY");
225
341
  const secret = where("MAPLED_WEBHOOK_SECRET");
226
342
  if (!key) {
@@ -268,10 +384,10 @@ export function checkSecrets(facts) {
268
384
  }
269
385
  return { key: "secrets", label, status: "passed", detail: "No env files or Mapled secrets are tracked." };
270
386
  }
271
- export function checkSdk(pkg, latest) {
272
- const label = "@mapled/next";
387
+ export function checkSdk(pkg, latest, name = "@mapled/next") {
388
+ const label = name;
273
389
  if (!pkg.installed && !pkg.declared) {
274
- return { key: "sdk", label, status: "failed", detail: "Not installed. Run `npm install @mapled/next`." };
390
+ return { key: "sdk", label, status: "failed", detail: `Not installed. Run \`npm install ${name}\`.` };
275
391
  }
276
392
  if (!pkg.installed) {
277
393
  return { key: "sdk", label, status: "warning", detail: `${pkg.declared} is declared but not installed — run npm install.` };
@@ -283,7 +399,7 @@ export function checkSdk(pkg, latest) {
283
399
  key: "sdk",
284
400
  label,
285
401
  status: "warning",
286
- detail: `${pkg.installed} installed, ${latest} available — npm install @mapled/next@latest.`,
402
+ detail: `${pkg.installed} installed, ${latest} available — npm install ${name}@latest.`,
287
403
  };
288
404
  }
289
405
  return { key: "sdk", label, status: "passed", detail: `${pkg.installed} (current)` };
@@ -299,6 +415,16 @@ export function checkRoute(kind, found, framework) {
299
415
  const key = `route_${kind}`;
300
416
  const label = kind === "revalidate" ? "Revalidation route" : "Preview route";
301
417
  const handler = ROUTE_HANDLER[kind];
418
+ if (rendersInBrowser(framework)) {
419
+ return {
420
+ key,
421
+ label,
422
+ status: "skipped",
423
+ detail: kind === "revalidate"
424
+ ? "Not needed: the site reads in the browser, so a publish shows on the next load."
425
+ : "Not needed: the client trades Mapled's preview link on the page itself.",
426
+ };
427
+ }
302
428
  if (framework && framework !== "nextjs") {
303
429
  return { key, label, status: "skipped", detail: `No ${kind} route check for ${framework} sites yet.` };
304
430
  }
@@ -334,8 +460,20 @@ export function checkReads(lastReadAt) {
334
460
  }
335
461
  return { key: "reads", label, status: "passed", detail: `Last read ${ago(lastReadAt)}` };
336
462
  }
337
- export function checkWebhook(webhook) {
463
+ export function checkWebhook(webhook, site) {
338
464
  const label = "Publish webhook";
465
+ if (!webhook.url && site?.local) {
466
+ // the repository renders in the browser; does Mapled know?
467
+ if (site.remote && site.remote.rendering !== "browser") {
468
+ return {
469
+ key: "webhook",
470
+ label,
471
+ status: "warning",
472
+ 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.`,
473
+ };
474
+ }
475
+ return { key: "webhook", label, status: "skipped", detail: "Not needed: the site reads in the browser, so a publish shows on the next load." };
476
+ }
339
477
  if (!webhook.url) {
340
478
  return {
341
479
  key: "webhook",
@@ -377,8 +515,21 @@ export function checkWebhook(webhook) {
377
515
  detail: `The last delivery to ${host} ${reason}. Check the route and MAPLED_WEBHOOK_SECRET, then send a test ping.`,
378
516
  };
379
517
  }
380
- export function checkPreviewOnSite(origin, previewPath, probe) {
518
+ export function checkPreviewOnSite(origin, previewPath, probe, browser = false) {
381
519
  const label = "Preview on the site";
520
+ if (browser) {
521
+ if (!origin || !probe) {
522
+ return {
523
+ key: "preview_site",
524
+ label,
525
+ status: "warning",
526
+ detail: "Mapled doesn't know where the site is deployed — set Production domain in Project settings, or ask your agent (set_site_url).",
527
+ };
528
+ }
529
+ return probe.status === null
530
+ ? { key: "preview_site", label, status: "warning", detail: `${origin} is unreachable from here.` }
531
+ : { key: "preview_site", label, status: "passed", detail: `Opens at ${origin} — the client trades the link in the browser` };
532
+ }
382
533
  if (!origin || !probe) {
383
534
  return { key: "preview_site", label, status: "skipped", detail: "Needs the webhook URL to know where the site lives." };
384
535
  }
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; the HTML runtime has none to call
183
+ const assets = sdk === "@mapled/react" || sdk === "@mapled/vanilla" ? "@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.9.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",