@mapled/cli 0.10.0 → 0.12.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 +54 -3
- package/dist/commands.d.ts +9 -0
- package/dist/commands.js +103 -2
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +99 -1
- package/dist/index.js +11 -2
- package/dist/live.d.ts +156 -0
- package/dist/live.js +1509 -0
- package/dist/manifest.d.ts +14 -0
- package/dist/manifest.js +54 -2
- package/dist/pin.js +7 -0
- package/dist/report.d.ts +127 -0
- package/dist/report.js +425 -0
- package/dist/scan.d.ts +22 -2
- package/dist/scan.js +126 -23
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -128,6 +128,8 @@ Scanned 2 HTML files — 2 pages, 11 bindings.
|
|
|
128
128
|
⚠ This path names nothing in the schema: home.tagline (index.html). A misspelled field empties its place on the page, a misspelled single leaves the page's own HTML.
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
For a Next.js site the scan also records its live updates: `capabilities.realtime` is `true`, with the route in `releaseRoute`, when `<MapledLive />` or `watchRelease` asks — at the default path or one written in the code — a route whose exported `GET` answers with `createReleaseHandler`, and `false` otherwise — `MAPLED.md` then tells the next agent whether open tabs follow Publish. Capabilities aren't part of the integration hash; `doctor`'s Site manifest line says when the pushed ones differ.
|
|
132
|
+
|
|
131
133
|
`--write` creates or updates `mapled/manifest.json`. Bindings the scan can't see (written by your AI agent, or by hand) are kept and listed; `--prune` drops them. Commit the file.
|
|
132
134
|
|
|
133
135
|
## Validate and push the manifest
|
|
@@ -193,6 +195,7 @@ Mapled doctor — Dolphin Landing
|
|
|
193
195
|
✓ mapled CLI 0.9.0 (current)
|
|
194
196
|
✓ Revalidation route app/api/mapled/revalidate/route.ts
|
|
195
197
|
⚠ Preview route app/api/mapled/preview/route.ts is missing — mount createPreviewHandler from "@mapled/next/server" so Preview from Mapled works.
|
|
198
|
+
✓ Live updates app/api/mapled/release/route.ts — open tabs follow Publish (<MapledLive /> in app/layout.tsx)
|
|
196
199
|
✓ Site reads content Last read 3m ago
|
|
197
200
|
✓ Publish webhook Delivered 2h ago to dolphin-landing.example
|
|
198
201
|
✓ Preview on the site Responds at https://dolphin-landing.example/api/mapled/preview
|
|
@@ -201,17 +204,65 @@ Mapled doctor — Dolphin Landing
|
|
|
201
204
|
1 warning.
|
|
202
205
|
```
|
|
203
206
|
|
|
204
|
-
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.
|
|
207
|
+
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, and its live updates — ○ «Not used» while no page mounts `<MapledLive />` or calls `watchRelease`, ✓ when the route they ask answers its `GET` with `createReleaseHandler` (`GET` is the handler, or always returns its call: every return is that call, the last statement is one, and nothing throws), ⚠ when it is missing (the tabs would poll a 404) with the file to add, when the route's `GET` isn't seen to answer with the handler (exported as `POST` they would get a 405; a `GET` that only mentions it, calls it and returns something else, or answers something else on another branch, answers without the release), or when no route can be checked against the endpoint — it is relative to the page, a full address (the tab asks it as written, and doctor can't tell it's the site's own), computed, or comes from spread props; a path from the site's root is checked against the route the App Router picks for it, dynamic and catch-all folders included, among route files with the site's page extensions (`pageExtensions` in next.config; `tsx`, `ts`, `jsx`, `js` by default) and under its `basePath`; ⚠ too when a page of either router, more specific than the route, takes that path first, when the dynamic route that answers sets `dynamicParams`, or when next.config may set `pageExtensions` or `basePath` where doctor can't read them — it reads them only as literals, once, in the object the file exports or passes to a plugin from a package. `src/app` or `src/pages` counts only when the root has no `app` or `pages`, as Next reads them. doctor reads the files, not how Next serves them at run time: middleware, rewrites, other segment config (`dynamic`, `revalidate`) and `output: "export"` aren't checked, and neither is whether a page renders the component that listens. 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.
|
|
205
208
|
|
|
206
|
-
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.
|
|
209
|
+
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; Live updates reads ○ as well: `live` on `MapledProvider` or `data-mapled-live` asks Mapled directly, with no route of the site's. 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.
|
|
207
210
|
|
|
208
211
|
Exit code 1 when something is marked ✗; `--json` prints the checks for scripts and agents.
|
|
209
212
|
|
|
213
|
+
## Read the setup report
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx @mapled/cli integration report
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
When an AI agent set the site up, Mapled keeps the run: the plan a person approved, what was applied, the agent's closing report, and the Verification checklist the server runs — the one on the run's page in Mapled. `integration report` prints the last applied run:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
Integration report — Dolphin Landing
|
|
223
|
+
|
|
224
|
+
Setup run Claude Code • checks failed • applied 2h ago • checked 1h ago
|
|
225
|
+
Plan “Blog with authors, a homepage and a contact form”
|
|
226
|
+
|
|
227
|
+
Verification
|
|
228
|
+
✓ Site reads content from Mapled Last read 3m ago
|
|
229
|
+
✗ Publish webhook works No deliveries since the plan was applied. Send a test ping from Integrations.
|
|
230
|
+
✓ Preview route responds Responds at /api/mapled/preview
|
|
231
|
+
⚠ Field names and help texts filled in 2 of 14 fields have no help text. Editors see it under the field.
|
|
232
|
+
✓ Site builds successfully Reported by the agent
|
|
233
|
+
✓ No secrets in the repository Reported by the agent
|
|
234
|
+
✓ MAPLED.md written Reported by the agent
|
|
235
|
+
|
|
236
|
+
Collections created
|
|
237
|
+
Articles (articles) collection • 6 fields • 12 records imported
|
|
238
|
+
Homepage (homepage) single • 4 fields • 1 record imported
|
|
239
|
+
|
|
240
|
+
Files changed
|
|
241
|
+
app/page.tsx
|
|
242
|
+
app/blog/[slug]/page.tsx
|
|
243
|
+
|
|
244
|
+
Still hardcoded
|
|
245
|
+
footer copyright
|
|
246
|
+
|
|
247
|
+
1 problem, 1 warning. Fix what the checks name, then `mapled integration report --verify` runs them again.
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`--verify` runs the server checks again first, as **Run checks again** does in Mapled. A newer plan that isn't applied yet is named on its own line. The report never shows a key or a token: whatever an agent wrote into its report, anything shaped like a Mapled key is masked, and a reported preview address keeps only its page. Exit code 1 when a check is marked ✗; `--json` for scripts. A site connected without an agent has no run — `doctor` checks it from the repository.
|
|
251
|
+
|
|
252
|
+
## Preview the drafts
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npx @mapled/cli preview
|
|
256
|
+
npx @mapled/cli preview articles hello-world
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Prints a Preview link — the one the **Preview** button in Mapled opens: the site in draft mode, with every saved change that isn't published yet. Name a collection and a record's slug (or id) and the link lands on the record's page, the one the pushed site manifest says shows it; a collection alone opens the page that lists it, a single its page, nothing at all the home page. Mapled makes the link after checking that the site answers and mounts its preview route; it works once, for five minutes. `--open` opens it in your browser instead of printing it; `--json` prints the link with the page and when it expires.
|
|
260
|
+
|
|
210
261
|
## Options
|
|
211
262
|
|
|
212
263
|
- `--api <origin>` — a Mapled API other than `https://api.mapled.io` (or set `MAPLED_API_URL`); `project link` remembers it in `mapled.json`
|
|
213
264
|
- `--no-browser` — print the sign-in link instead of opening a browser
|
|
214
|
-
- `--json` — machine-readable output (`doctor`, `schema diff`, `scan`, `manifest validate`, `forms list`)
|
|
265
|
+
- `--json` — machine-readable output (`doctor`, `integration report`, `schema diff`, `scan`, `manifest validate`, `forms list`, `preview`)
|
|
215
266
|
- `MAPLED_CONFIG_DIR` — where credentials live (default: `$XDG_CONFIG_HOME/mapled` or `~/.config/mapled`)
|
|
216
267
|
|
|
217
268
|
## Files in the repository
|
package/dist/commands.d.ts
CHANGED
|
@@ -34,6 +34,15 @@ export declare function mdPull(ctx: Ctx, flags: Flags): Promise<void>;
|
|
|
34
34
|
done by hand needs no agent for it. */
|
|
35
35
|
export declare function formsCreate(ctx: Ctx, flags: Flags, words: string[]): Promise<void>;
|
|
36
36
|
export declare function formsList(ctx: Ctx, flags: Flags): Promise<void>;
|
|
37
|
+
/** `mapled integration report` — the Verification checklist of the last
|
|
38
|
+
applied setup run (S-42) and what the agent reported; `--verify` runs
|
|
39
|
+
the server checks again first, as "Run checks again" does. */
|
|
40
|
+
export declare function integrationReport(ctx: Ctx, flags: Flags): Promise<number>;
|
|
41
|
+
/** `mapled preview [<collection> [<slug>]]` — a Preview link from Mapled's
|
|
42
|
+
launcher, the one the Preview button opens: the token is Mapled's, the
|
|
43
|
+
CLI only passes it on. `--open` hands it to the browser instead of
|
|
44
|
+
printing it (the link works once). */
|
|
45
|
+
export declare function preview(ctx: Ctx, flags: Flags, words: string[]): Promise<void>;
|
|
37
46
|
export declare function scan(ctx: Ctx, flags: Flags): Promise<void>;
|
|
38
47
|
export declare function doctor(ctx: Ctx, flags: Flags): Promise<number>;
|
|
39
48
|
export {};
|
package/dist/commands.js
CHANGED
|
@@ -11,6 +11,8 @@ import { authorize, clientKnown, exchangeCode, registerClient, revokeToken } fro
|
|
|
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
13
|
import { checkMapledMd, MD_FILE, mergeMd } from "./md.js";
|
|
14
|
+
import { buildReport, clean, formatReport, isChecked, linkLifetime, pickRun, } from "./report.js";
|
|
15
|
+
import { capabilitiesOf, findLive, liveState } from "./live.js";
|
|
14
16
|
import { loadTypeScript, mergeManifest, scanRepository } from "./scan.js";
|
|
15
17
|
import { scanHtml } from "./scanHtml.js";
|
|
16
18
|
import { generateTypes } from "./types.js";
|
|
@@ -357,7 +359,8 @@ export async function bindingsPull(ctx, flags) {
|
|
|
357
359
|
const local = await loadManifest(found.dir);
|
|
358
360
|
if (local && !flags.force) {
|
|
359
361
|
const diff = compareManifests(local.manifest, stored.manifest);
|
|
360
|
-
|
|
362
|
+
// capabilities a scan just recorded are as much the repository's as its bindings
|
|
363
|
+
if (!diff.same || diff.capabilitiesDiffer) {
|
|
361
364
|
const parts = [];
|
|
362
365
|
if (diff.onlyLocal.length > 0)
|
|
363
366
|
parts.push(`${diff.onlyLocal.length} not pushed`);
|
|
@@ -365,6 +368,8 @@ export async function bindingsPull(ctx, flags) {
|
|
|
365
368
|
parts.push(`${diff.changed.length} changed`);
|
|
366
369
|
if (diff.onlyRemote.length > 0)
|
|
367
370
|
parts.push(`${diff.onlyRemote.length} only in Mapled`);
|
|
371
|
+
if (diff.capabilitiesDiffer)
|
|
372
|
+
parts.push("capabilities changed");
|
|
368
373
|
throw new CliError(`${MANIFEST_FILE} differs from the pushed manifest (v${stored.version}): ${parts.join(", ")}. Pass --force to overwrite it, or push yours with \`mapled bindings push\`.`);
|
|
369
374
|
}
|
|
370
375
|
}
|
|
@@ -447,6 +452,85 @@ export async function formsList(ctx, flags) {
|
|
|
447
452
|
ctx.out(`${f.key.padEnd(keyWidth)} ${f.displayName.padEnd(nameWidth)} spam protection ${f.spamProtection ? "on " : "off"} ${submitUrl(session.conn.api, f.key)}`);
|
|
448
453
|
}
|
|
449
454
|
}
|
|
455
|
+
/* ---- the integration report and preview (§31.2) ---- */
|
|
456
|
+
/** `mapled integration report` — the Verification checklist of the last
|
|
457
|
+
applied setup run (S-42) and what the agent reported; `--verify` runs
|
|
458
|
+
the server checks again first, as "Run checks again" does. */
|
|
459
|
+
export async function integrationReport(ctx, flags) {
|
|
460
|
+
const { found, session } = await openSession(ctx, flags);
|
|
461
|
+
const { runs } = await session.get("/v1/agent/runs");
|
|
462
|
+
const { shown, newer } = pickRun(runs);
|
|
463
|
+
let run = null;
|
|
464
|
+
if (shown && flags.verify) {
|
|
465
|
+
if (!isChecked(shown.status)) {
|
|
466
|
+
throw new CliError(`Nothing to check yet — the plan from ${clean(shown.clientName, 80)} hasn't been applied.`);
|
|
467
|
+
}
|
|
468
|
+
run = (await session.send("POST", `/v1/agent/runs/${encodeURIComponent(shown.id)}/verify`, {})).run;
|
|
469
|
+
}
|
|
470
|
+
else if (shown) {
|
|
471
|
+
run = (await session.get(`/v1/agent/runs/${encodeURIComponent(shown.id)}`)).run;
|
|
472
|
+
}
|
|
473
|
+
const report = buildReport({ id: found.config.project, name: session.conn.projectName }, run, newer);
|
|
474
|
+
if (flags.json)
|
|
475
|
+
ctx.out(JSON.stringify(report, null, 2));
|
|
476
|
+
else
|
|
477
|
+
for (const line of formatReport(report, useColor(ctx.env)))
|
|
478
|
+
ctx.out(line);
|
|
479
|
+
return report.checks.some((c) => c.status === "failed") ? 1 : 0;
|
|
480
|
+
}
|
|
481
|
+
/** `mapled preview [<collection> [<slug>]]` — a Preview link from Mapled's
|
|
482
|
+
launcher, the one the Preview button opens: the token is Mapled's, the
|
|
483
|
+
CLI only passes it on. `--open` hands it to the browser instead of
|
|
484
|
+
printing it (the link works once). */
|
|
485
|
+
export async function preview(ctx, flags, words) {
|
|
486
|
+
const [collection, record, ...rest] = words;
|
|
487
|
+
if (rest.length > 0) {
|
|
488
|
+
throw new CliError("Name a collection and, optionally, one record's slug: `mapled preview articles hello-world`.", 2);
|
|
489
|
+
}
|
|
490
|
+
const { session } = await openSession(ctx, flags);
|
|
491
|
+
let answer;
|
|
492
|
+
try {
|
|
493
|
+
answer = await session.send("POST", "/v1/agent/preview", {
|
|
494
|
+
...(collection ? { collection } : {}),
|
|
495
|
+
...(record ? { record } : {}),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
if (err instanceof ApiError && err.code === "NO_SITE") {
|
|
500
|
+
throw new CliError("Mapled doesn't know where the site is yet — Preview opens on it. Point the publish webhook at the deployed site (Integrations → Your site); a site rendered in the browser needs its Production domain in Project settings.");
|
|
501
|
+
}
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
if (answer.status === "not_deployed") {
|
|
505
|
+
throw new CliError(`${clean(answer.origin)} doesn't answer — deploy the site publicly, then try again.`);
|
|
506
|
+
}
|
|
507
|
+
if (answer.status === "not_set_up") {
|
|
508
|
+
throw new CliError(`${clean(answer.origin)} has no preview route — mount createPreviewHandler from "@mapled/next/server" at /api/mapled/preview and redeploy.`);
|
|
509
|
+
}
|
|
510
|
+
// the link is printed as it came (masking would drop the token): printable ASCII only
|
|
511
|
+
if (typeof answer.url !== "string" || !/^https?:\/\/[!-~]+$/.test(answer.url)) {
|
|
512
|
+
throw new CliError("Mapled answered with a link this CLI can't print. Update @mapled/cli and try again.");
|
|
513
|
+
}
|
|
514
|
+
if (flags.json) {
|
|
515
|
+
ctx.out(JSON.stringify({ url: answer.url, path: answer.path, expiresAt: answer.expiresAt ?? null }, null, 2));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const where = answer.path ? clean(answer.path, 500) : "the site's home page";
|
|
519
|
+
const lifetime = linkLifetime(answer.expiresAt);
|
|
520
|
+
const project = clean(session.conn.projectName, 120);
|
|
521
|
+
if (flags.open && (await ctx.openBrowser(answer.url))) {
|
|
522
|
+
ctx.out(`Opened Preview of ${where} in your browser — the drafts of ${project}.`);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
if (flags.open)
|
|
526
|
+
ctx.out("Couldn't open a browser — open this link yourself:");
|
|
527
|
+
ctx.out(answer.url);
|
|
528
|
+
ctx.out(`Preview of ${where} with the drafts of ${project}. The link works once${lifetime ? `, for ${lifetime}` : ""}.`);
|
|
529
|
+
}
|
|
530
|
+
if (collection && !answer.path) {
|
|
531
|
+
ctx.out("Mapled doesn't know which page shows it, so the link opens the home page. Push the site manifest (`mapled scan --write`, then `mapled bindings push`) and Preview lands on its page.");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
450
534
|
/* ---- wave 2: the scanner ---- */
|
|
451
535
|
function describeScan(result, schema) {
|
|
452
536
|
const singles = new Set(schema.collections.filter((c) => c.kind === "single").map((c) => c.key));
|
|
@@ -493,6 +577,13 @@ export async function scan(ctx, flags) {
|
|
|
493
577
|
const framework = found.config.framework ?? (await dr.detectFramework(found.dir)) ?? null;
|
|
494
578
|
// a plain HTML site declares its reads in attributes, not in code
|
|
495
579
|
const result = framework === "plain-html" ? await scanHtml(found.dir, { schema }) : await scanRepository(found.dir, { framework, schema, ts: loadTypeScript(found.dir) });
|
|
580
|
+
// live updates (§18.8) of a Next.js site: the same reading of the code as doctor's line
|
|
581
|
+
const live = framework === "nextjs" ? liveState(await findLive(found.dir)) : null;
|
|
582
|
+
if (live)
|
|
583
|
+
result.capabilities = capabilitiesOf(live);
|
|
584
|
+
if (live && live.kind !== "served" && live.kind !== "unused") {
|
|
585
|
+
result.notes.push(`${live.listener.via} in ${live.listener.file} has no route that doctor can confirm answers it, so the manifest records that the site doesn't follow Publish — \`mapled doctor\` says what to fix.`);
|
|
586
|
+
}
|
|
496
587
|
const existing = await loadManifest(found.dir);
|
|
497
588
|
const merged = mergeManifest(existing?.manifest ?? null, result, { prune: Boolean(flags.prune) });
|
|
498
589
|
if (flags.json) {
|
|
@@ -507,6 +598,10 @@ export async function scan(ctx, flags) {
|
|
|
507
598
|
for (const line of describeScan(result, schema))
|
|
508
599
|
ctx.out(line);
|
|
509
600
|
}
|
|
601
|
+
if (live?.kind === "served") {
|
|
602
|
+
ctx.out("");
|
|
603
|
+
ctx.out(`Live updates: ${live.listener.via} in ${live.listener.file} → ${live.path} (${live.route.file})`);
|
|
604
|
+
}
|
|
510
605
|
if (result.notes.length > 0) {
|
|
511
606
|
ctx.out("");
|
|
512
607
|
for (const n of result.notes)
|
|
@@ -524,6 +619,8 @@ export async function scan(ctx, flags) {
|
|
|
524
619
|
parts.push(`${merged.changed.length} changed`);
|
|
525
620
|
if (merged.unchanged.length > 0)
|
|
526
621
|
parts.push(`${merged.unchanged.length} unchanged`);
|
|
622
|
+
if (merged.capabilitiesChanged)
|
|
623
|
+
parts.push("capabilities updated");
|
|
527
624
|
ctx.out(`Wrote ${MANIFEST_FILE} — ${manifestSummary(merged.manifest)}${parts.length > 0 && existing ? ` (${parts.join(", ")})` : ""}.${keptNote}${droppedNote}`);
|
|
528
625
|
ctx.out("Next: `mapled bindings push`.");
|
|
529
626
|
}
|
|
@@ -537,10 +634,12 @@ export async function scan(ctx, flags) {
|
|
|
537
634
|
if (merged.changed.length > 0)
|
|
538
635
|
parts.push(`${merged.changed.length} changed`);
|
|
539
636
|
parts.push(`${merged.unchanged.length} unchanged`);
|
|
637
|
+
if (merged.capabilitiesChanged)
|
|
638
|
+
parts.push("capabilities changed");
|
|
540
639
|
if (merged.kept.length > 0)
|
|
541
640
|
parts.push(`${merged.kept.length} not found in the code (${merged.kept.slice(0, 5).join(", ")}${merged.kept.length > 5 ? ", …" : ""})`);
|
|
542
641
|
ctx.out(`Compared with ${MANIFEST_FILE}: ${parts.join(", ")}.`);
|
|
543
|
-
if (merged.added.length > 0 || merged.changed.length > 0) {
|
|
642
|
+
if (merged.added.length > 0 || merged.changed.length > 0 || merged.capabilitiesChanged) {
|
|
544
643
|
ctx.out(`Run \`mapled scan --write\` to update ${MANIFEST_FILE}, then \`mapled bindings push\`.`);
|
|
545
644
|
}
|
|
546
645
|
else {
|
|
@@ -613,6 +712,8 @@ export async function doctor(ctx, flags) {
|
|
|
613
712
|
checks.push(dr.checkCli(ctx.version, status?.sdk["@mapled/cli"]));
|
|
614
713
|
checks.push(dr.checkRoute("revalidate", await dr.findRoute(dir, "revalidate"), framework));
|
|
615
714
|
checks.push(dr.checkRoute("preview", await dr.findRoute(dir, "preview"), framework));
|
|
715
|
+
// the code is read only where the check applies: a Next.js site (or one doctor can't place)
|
|
716
|
+
checks.push(dr.checkLive(!framework || framework === "nextjs" ? await findLive(dir) : null, framework));
|
|
616
717
|
if (status) {
|
|
617
718
|
checks.push(dr.checkReads(status.delivery.lastReadAt));
|
|
618
719
|
checks.push(dr.checkWebhook(status.webhook, { local: browser, remote: status.site }));
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Connection } from "./credentials.js";
|
|
2
|
+
import { type LiveFacts } from "./live.js";
|
|
2
3
|
import { type Manifest, type Problem } from "./manifest.js";
|
|
3
4
|
import type { Fetch } from "./oauth.js";
|
|
4
5
|
import { type SchemaChange, type SchemaPin } from "./pin.js";
|
|
@@ -145,6 +146,10 @@ export declare function checkSecrets(facts: GitFacts): Check;
|
|
|
145
146
|
export declare function checkSdk(pkg: PackageVersion, latest: string | undefined, name?: string): Check;
|
|
146
147
|
export declare function checkCli(current: string, latest: string | undefined): Check;
|
|
147
148
|
export declare function checkRoute(kind: RouteKind, found: RouteFile | null, framework: string | undefined): Check;
|
|
149
|
+
/** Live updates (§18.8): whether open tabs follow Publish, and when the
|
|
150
|
+
code listens, whether a route answers it. `facts` is null when the
|
|
151
|
+
code wasn't read (a stack this check doesn't cover). */
|
|
152
|
+
export declare function checkLive(facts: LiveFacts | null, framework: string | undefined): Check;
|
|
148
153
|
export declare function checkReads(lastReadAt: string | null): Check;
|
|
149
154
|
export declare function checkWebhook(webhook: IntegrationStatus["webhook"], site?: {
|
|
150
155
|
local: boolean;
|
package/dist/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ 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";
|
|
6
|
+
import { fullAddress, liveState, RELEASE_ROUTE, routeFileFor } from "./live.js";
|
|
6
7
|
import { compareManifests, manifestSummary, MANIFEST_FILE } from "./manifest.js";
|
|
7
8
|
import { SCHEMA_FILE } from "./pin.js";
|
|
8
9
|
import { splitGenerated } from "./types.js";
|
|
@@ -448,6 +449,100 @@ export function checkRoute(kind, found, framework) {
|
|
|
448
449
|
}
|
|
449
450
|
return { key, label, status: "passed", detail: found.file };
|
|
450
451
|
}
|
|
452
|
+
/** Live updates (§18.8): whether open tabs follow Publish, and when the
|
|
453
|
+
code listens, whether a route answers it. `facts` is null when the
|
|
454
|
+
code wasn't read (a stack this check doesn't cover). */
|
|
455
|
+
export function checkLive(facts, framework) {
|
|
456
|
+
const key = "live";
|
|
457
|
+
const label = "Live updates";
|
|
458
|
+
if (framework === "react-spa") {
|
|
459
|
+
return { key, label, status: "skipped", detail: "No route needed: `live` on MapledProvider (watchRelease by key) asks Mapled directly." };
|
|
460
|
+
}
|
|
461
|
+
if (framework === "plain-html") {
|
|
462
|
+
return { key, label, status: "skipped", detail: "No route needed: data-mapled-live on the script tag (watchRelease by key) asks Mapled directly." };
|
|
463
|
+
}
|
|
464
|
+
if ((framework && framework !== "nextjs") || !facts) {
|
|
465
|
+
return { key, label, status: "skipped", detail: `No live updates check for ${framework ?? "this"} site${framework ? "s" : ""} yet.` };
|
|
466
|
+
}
|
|
467
|
+
const state = liveState(facts);
|
|
468
|
+
const who = (l) => `${l.via} in ${l.file}`;
|
|
469
|
+
const handler = 'createReleaseHandler from "@mapled/next/server"';
|
|
470
|
+
switch (state.kind) {
|
|
471
|
+
case "unused":
|
|
472
|
+
return {
|
|
473
|
+
key,
|
|
474
|
+
label,
|
|
475
|
+
status: "skipped",
|
|
476
|
+
detail: state.handlerRoute
|
|
477
|
+
? `Not used: ${state.handlerRoute.file} is mounted, but no page listens — add <MapledLive /> to the root layout.`
|
|
478
|
+
: "Not used: an open tab shows a publish once it is reloaded.",
|
|
479
|
+
};
|
|
480
|
+
case "served":
|
|
481
|
+
return { key, label, status: "passed", detail: `${state.route.file} — open tabs follow Publish (${who(state.listener)})` };
|
|
482
|
+
case "no_handler":
|
|
483
|
+
return { key, label, status: "warning", detail: `${state.route.file} exists but doesn't use ${handler} — ${who(state.listener)} can't read the release there.` };
|
|
484
|
+
case "not_get":
|
|
485
|
+
return {
|
|
486
|
+
key,
|
|
487
|
+
label,
|
|
488
|
+
status: "warning",
|
|
489
|
+
detail: `${state.route.file} calls ${handler}, but doctor can't see its GET always answer with the handler's response — ${who(state.listener)} asks with GET. Export the handler as GET, or end GET by returning its call, with no other return or throw.`,
|
|
490
|
+
};
|
|
491
|
+
case "unresolved":
|
|
492
|
+
return {
|
|
493
|
+
key,
|
|
494
|
+
label,
|
|
495
|
+
status: "warning",
|
|
496
|
+
detail: state.listener.endpoint === null
|
|
497
|
+
? `${who(state.listener)} takes its endpoint from the code (a computed value or spread props), so no route can be checked. Give the route's path, like ${RELEASE_ROUTE}.`
|
|
498
|
+
: fullAddress(state.listener.endpoint)
|
|
499
|
+
? `${who(state.listener)} asks ${state.listener.endpoint}, a full address — the tab asks it as written, and doctor can't tell it's this site's own, so no route can be checked. Give the route's path, like ${RELEASE_ROUTE}.`
|
|
500
|
+
: `${who(state.listener)} asks ${state.listener.endpoint}, which isn't a path from the site's root — each page resolves it against its own address, so no route can be checked. Give the route's path, like ${RELEASE_ROUTE}.`,
|
|
501
|
+
};
|
|
502
|
+
case "config":
|
|
503
|
+
return {
|
|
504
|
+
key,
|
|
505
|
+
label,
|
|
506
|
+
status: "warning",
|
|
507
|
+
detail: `${state.file} may set pageExtensions or basePath where doctor can't read them — a value or name the code builds, an object from another file or spread in — so no route can be checked for ${who(state.listener)}. Write them as literals in the object it exports, or leave them out.`,
|
|
508
|
+
};
|
|
509
|
+
case "outside":
|
|
510
|
+
return {
|
|
511
|
+
key,
|
|
512
|
+
label,
|
|
513
|
+
status: "warning",
|
|
514
|
+
detail: `${who(state.listener)} asks ${state.endpoint}, outside the site's basePath ${state.basePath} — Next answers nothing there. Give it the path under the basePath: ${state.basePath}${state.endpoint === "/" ? "" : state.endpoint}.`,
|
|
515
|
+
};
|
|
516
|
+
case "taken":
|
|
517
|
+
return {
|
|
518
|
+
key,
|
|
519
|
+
label,
|
|
520
|
+
status: "warning",
|
|
521
|
+
detail: `${who(state.listener)} asks ${state.endpoint}, which ${state.page.file} answers, not a release route. Point the listener at a path of its own, and mount ${handler} there.`,
|
|
522
|
+
};
|
|
523
|
+
case "params":
|
|
524
|
+
return {
|
|
525
|
+
key,
|
|
526
|
+
label,
|
|
527
|
+
status: "warning",
|
|
528
|
+
detail: `${state.route.file} sets dynamicParams, so it answers only the params its code lists — doctor can't tell ${state.endpoint} is one, and ${who(state.listener)} may get a 404 there. Mount ${handler} at ${routeFileFor(state.endpoint, facts)}.`,
|
|
529
|
+
};
|
|
530
|
+
case "missing":
|
|
531
|
+
return state.endpoint === null
|
|
532
|
+
? {
|
|
533
|
+
key,
|
|
534
|
+
label,
|
|
535
|
+
status: "warning",
|
|
536
|
+
detail: `${who(state.listener)} asks a release route the code doesn't have — mount ${handler} (at ${routeFileFor(RELEASE_ROUTE, facts)} by default).`,
|
|
537
|
+
}
|
|
538
|
+
: {
|
|
539
|
+
key,
|
|
540
|
+
label,
|
|
541
|
+
status: "warning",
|
|
542
|
+
detail: `${who(state.listener)} asks ${state.endpoint}, which has no route — mount ${handler} at ${routeFileFor(state.endpoint, facts)}.`,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
451
546
|
export function checkReads(lastReadAt) {
|
|
452
547
|
const label = "Site reads content";
|
|
453
548
|
if (!lastReadAt) {
|
|
@@ -606,7 +701,7 @@ export function checkManifestFile(local, remote) {
|
|
|
606
701
|
if (remote === null)
|
|
607
702
|
return { key: "manifest", label, status: "warning", detail: `${MANIFEST_FILE} isn't pushed yet — run \`mapled bindings push\`.` };
|
|
608
703
|
const diff = compareManifests(local.manifest, remote.manifest);
|
|
609
|
-
if (diff.same) {
|
|
704
|
+
if (diff.same && !diff.capabilitiesDiffer) {
|
|
610
705
|
return { key: "manifest", label, status: "passed", detail: `${MANIFEST_FILE} is pushed as manifest v${remote.version} (${manifestSummary(local.manifest)})` };
|
|
611
706
|
}
|
|
612
707
|
const parts = [];
|
|
@@ -616,6 +711,9 @@ export function checkManifestFile(local, remote) {
|
|
|
616
711
|
parts.push(`${diff.changed.length} changed`);
|
|
617
712
|
if (diff.onlyRemote.length > 0)
|
|
618
713
|
parts.push(`${diff.onlyRemote.length} only in Mapled`);
|
|
714
|
+
// capabilities aren't in the integration hash, so this is the line that notices them
|
|
715
|
+
if (diff.capabilitiesDiffer)
|
|
716
|
+
parts.push("capabilities changed");
|
|
619
717
|
return {
|
|
620
718
|
key: "manifest",
|
|
621
719
|
label,
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { parseArgs } from "./args.js";
|
|
5
5
|
import { openBrowser } from "./browser.js";
|
|
6
|
-
import { bindingsPull, bindingsPush, doctor, formsCreate, formsList, generate, link, login, logout, manifestValidate, mdPull, scan, schemaDiff, schemaPull, } from "./commands.js";
|
|
6
|
+
import { bindingsPull, bindingsPush, doctor, formsCreate, formsList, generate, integrationReport, link, login, logout, manifestValidate, mdPull, preview, 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
|
|
@@ -27,6 +27,8 @@ Usage
|
|
|
27
27
|
mapled forms create <name> [--key <key>] Create a form the site posts submissions to (--no-spam-protection to skip the honeypot)
|
|
28
28
|
mapled forms list [--json] List the project's forms and where the site posts to
|
|
29
29
|
mapled doctor [--json] Check the integration end to end
|
|
30
|
+
mapled integration report [--json] [--verify] Show the last setup run's verification checklist and report
|
|
31
|
+
mapled preview [<collection> [<slug>]] [--open] Print a one-time Preview link that shows the drafts on the site
|
|
30
32
|
|
|
31
33
|
Options
|
|
32
34
|
--version, -v Print the version
|
|
@@ -78,7 +80,8 @@ async function main(argv) {
|
|
|
78
80
|
version: version(),
|
|
79
81
|
credentialsFile: credentialsPath(),
|
|
80
82
|
};
|
|
81
|
-
|
|
83
|
+
// `preview` takes the record it opens as words of its own
|
|
84
|
+
switch (group === "preview" ? group : `${group} ${sub ?? ""}`.trim()) {
|
|
82
85
|
case "auth login":
|
|
83
86
|
await login(ctx, flags);
|
|
84
87
|
return 0;
|
|
@@ -118,6 +121,11 @@ async function main(argv) {
|
|
|
118
121
|
return 0;
|
|
119
122
|
case "doctor":
|
|
120
123
|
return doctor(ctx, flags);
|
|
124
|
+
case "integration report":
|
|
125
|
+
return integrationReport(ctx, flags);
|
|
126
|
+
case "preview":
|
|
127
|
+
await preview(ctx, flags, words.slice(1));
|
|
128
|
+
return 0;
|
|
121
129
|
case "auth":
|
|
122
130
|
case "project":
|
|
123
131
|
case "types":
|
|
@@ -126,6 +134,7 @@ async function main(argv) {
|
|
|
126
134
|
case "bindings":
|
|
127
135
|
case "md":
|
|
128
136
|
case "forms":
|
|
137
|
+
case "integration":
|
|
129
138
|
console.log(HELP);
|
|
130
139
|
return 2;
|
|
131
140
|
default:
|
package/dist/live.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Live updates (§18.8) as the site's code shows them. A Next.js site
|
|
2
|
+
follows Publish in open tabs with two pieces: `<MapledLive />` from
|
|
3
|
+
"@mapled/next/live" (or `watchRelease` from "@mapled/next") asks a
|
|
4
|
+
route of the site every few seconds, and that route answers with
|
|
5
|
+
`createReleaseHandler` from "@mapled/next/server". A listener without
|
|
6
|
+
the route polls a 404 forever and nothing says so — `mapled doctor`
|
|
7
|
+
reads both and says which is missing, and `mapled scan` records the
|
|
8
|
+
result in the manifest (`capabilities.realtime`, `releaseRoute`) so
|
|
9
|
+
MAPLED.md can tell the next agent. The code is read with the scanner's
|
|
10
|
+
tokenizer: comments and strings never count as a use, and neither
|
|
11
|
+
does an imported name the file binds again. */
|
|
12
|
+
/** Where `<MapledLive />` and `watchRelease` ask when no endpoint is given. */
|
|
13
|
+
export declare const RELEASE_ROUTE = "/api/mapled/release";
|
|
14
|
+
export type LiveListener = {
|
|
15
|
+
file: string;
|
|
16
|
+
via: "<MapledLive />" | "watchRelease";
|
|
17
|
+
/** the endpoint as the code writes it; null when the code decides it —
|
|
18
|
+
a computed value, or spread props or options that may carry one */
|
|
19
|
+
endpoint: string | null;
|
|
20
|
+
};
|
|
21
|
+
/** An App Router route handler file and the path it answers. `handler`:
|
|
22
|
+
"GET" when the route's exported GET is seen to answer with
|
|
23
|
+
`createReleaseHandler`, "other" when the file calls it but GET isn't
|
|
24
|
+
(exported as POST or not at all — the tab's GET gets a 405 — or a GET
|
|
25
|
+
whose answer isn't the handler's), null when it doesn't call it.
|
|
26
|
+
`params`: the file sets `dynamicParams` — a dynamic route then answers
|
|
27
|
+
only the params its code lists. */
|
|
28
|
+
export type RouteFile = {
|
|
29
|
+
file: string;
|
|
30
|
+
path: string;
|
|
31
|
+
handler: RouteHandler;
|
|
32
|
+
params?: true;
|
|
33
|
+
};
|
|
34
|
+
export type RouteHandler = "GET" | "other" | null;
|
|
35
|
+
/** A page file and the path it answers: an App Router `page.<ext>`, or any
|
|
36
|
+
Pages Router file (`pages/api/…` included). It takes part in the match
|
|
37
|
+
as Next makes it — a page more specific than a route takes the path. */
|
|
38
|
+
export type PageFile = {
|
|
39
|
+
file: string;
|
|
40
|
+
path: string;
|
|
41
|
+
};
|
|
42
|
+
/** `routeExtension`: what a route file the site lacks would end in — the
|
|
43
|
+
site's page extension, `ts` by default. `appDir`: the App Router folder
|
|
44
|
+
Next reads, when the site has one. `basePath`: what every path of the
|
|
45
|
+
site starts with (next.config). `config`: the next.config file, when it
|
|
46
|
+
may set `pageExtensions` or `basePath` where the reading can't follow —
|
|
47
|
+
then no route can be checked. */
|
|
48
|
+
export type LiveFacts = {
|
|
49
|
+
listeners: LiveListener[];
|
|
50
|
+
routes: RouteFile[];
|
|
51
|
+
pages?: PageFile[];
|
|
52
|
+
routeExtension?: string;
|
|
53
|
+
appDir?: string;
|
|
54
|
+
basePath?: string;
|
|
55
|
+
config?: string;
|
|
56
|
+
};
|
|
57
|
+
export type LiveState = {
|
|
58
|
+
kind: "unused";
|
|
59
|
+
handlerRoute: RouteFile | null;
|
|
60
|
+
} | {
|
|
61
|
+
kind: "served";
|
|
62
|
+
listener: LiveListener;
|
|
63
|
+
route: RouteFile;
|
|
64
|
+
path: string;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "missing";
|
|
67
|
+
listener: LiveListener;
|
|
68
|
+
endpoint: string | null;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "unresolved";
|
|
71
|
+
listener: LiveListener;
|
|
72
|
+
} | {
|
|
73
|
+
kind: "no_handler";
|
|
74
|
+
listener: LiveListener;
|
|
75
|
+
route: RouteFile;
|
|
76
|
+
} | {
|
|
77
|
+
kind: "not_get";
|
|
78
|
+
listener: LiveListener;
|
|
79
|
+
route: RouteFile;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "config";
|
|
82
|
+
listener: LiveListener;
|
|
83
|
+
file: string;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "outside";
|
|
86
|
+
listener: LiveListener;
|
|
87
|
+
endpoint: string;
|
|
88
|
+
basePath: string;
|
|
89
|
+
} | {
|
|
90
|
+
kind: "taken";
|
|
91
|
+
listener: LiveListener;
|
|
92
|
+
page: PageFile;
|
|
93
|
+
endpoint: string;
|
|
94
|
+
} | {
|
|
95
|
+
kind: "params";
|
|
96
|
+
listener: LiveListener;
|
|
97
|
+
route: RouteFile;
|
|
98
|
+
endpoint: string;
|
|
99
|
+
};
|
|
100
|
+
/** What the manifest records (§21.2). */
|
|
101
|
+
export type Capabilities = {
|
|
102
|
+
realtime?: boolean;
|
|
103
|
+
releaseRoute?: string;
|
|
104
|
+
};
|
|
105
|
+
/** The extensions a route file may have when next.config doesn't set `pageExtensions`. */
|
|
106
|
+
export declare const PAGE_EXTENSIONS: string[];
|
|
107
|
+
/** The listeners one file mounts and, for a route file, whether its GET
|
|
108
|
+
serves the release. An imported name the file binds again — a
|
|
109
|
+
parameter, a declaration, destructuring — may not be the import where
|
|
110
|
+
it's used, and the reading can't tell where: none of its uses count.
|
|
111
|
+
A namespace's name the same: its members count only where `ns` itself
|
|
112
|
+
is the import. */
|
|
113
|
+
export declare function liveInFile(file: string, text: string): {
|
|
114
|
+
listeners: LiveListener[];
|
|
115
|
+
handler: RouteHandler;
|
|
116
|
+
params: boolean;
|
|
117
|
+
};
|
|
118
|
+
/** The route files of the App Router with the path each answers. */
|
|
119
|
+
export declare function routeFileOf(file: string, extensions?: string[]): string | null;
|
|
120
|
+
/** The page files of both routers with the path each answers: an App
|
|
121
|
+
Router `page.<ext>`, and any Pages Router file but `_app`, `_document`
|
|
122
|
+
and `_error`, with the longest page extension its name ends in taken
|
|
123
|
+
off, as Next does, and `index` naming its folder. */
|
|
124
|
+
export declare function pageFileOf(file: string, extensions?: string[]): string | null;
|
|
125
|
+
/** What next.config decides about where a file answers: the page
|
|
126
|
+
extensions of route and page files, and the basePath every path of the
|
|
127
|
+
site starts with. */
|
|
128
|
+
export type NextConfig = {
|
|
129
|
+
pageExtensions: string[];
|
|
130
|
+
basePath: string;
|
|
131
|
+
};
|
|
132
|
+
/** next.config's `pageExtensions` and `basePath`, read only where the file
|
|
133
|
+
writes them out: a literal, once, directly in the object it exports —
|
|
134
|
+
`module.exports = {…}`, `export default nextConfig`, or passed to a
|
|
135
|
+
plugin: `withMDX(nextConfig)`. Null when it may set either anywhere
|
|
136
|
+
else: a value or a name the code builds (`["page" + "Extensions"]`, an
|
|
137
|
+
escaped identifier, `Object.assign`, `defineProperty`, a prototype
|
|
138
|
+
set by `setPrototypeOf` or `__proto__`, a member in a loop's head), an object from
|
|
139
|
+
another file or spread in, a member (`config.basePath = …`), a nested
|
|
140
|
+
object, any other mention — then no route can be checked. */
|
|
141
|
+
export declare function nextConfigOf(config: string | null): NextConfig | null;
|
|
142
|
+
export declare function findLive(dir: string, maxFiles?: number): Promise<LiveFacts>;
|
|
143
|
+
/** Whether a literal endpoint names its own origin — `https://…`, `//host/…`
|
|
144
|
+
— which the code doesn't show to be the site's: `watchRelease` fetches it
|
|
145
|
+
as written, and no route file of this site is known to answer it. */
|
|
146
|
+
export declare const fullAddress: (endpoint: string) => boolean;
|
|
147
|
+
/** Whether every listener has a route that answers it. The first one
|
|
148
|
+
that doesn't is the one to name. */
|
|
149
|
+
export declare function liveState(facts: LiveFacts): LiveState;
|
|
150
|
+
/** What `mapled scan` writes into the manifest: a listener nothing answers
|
|
151
|
+
isn't listening; the route is the path the tab asks, not a dynamic route's pattern. */
|
|
152
|
+
export declare function capabilitiesOf(state: LiveState): Capabilities;
|
|
153
|
+
/** Where a route answering `endpoint` goes: in the App Router folder Next
|
|
154
|
+
reads — without one, under `src/app` when the site keeps its code there
|
|
155
|
+
— and without the basePath, which Next adds itself. */
|
|
156
|
+
export declare function routeFileFor(endpoint: string, facts: LiveFacts): string;
|