@mapled/cli 0.9.0 → 0.11.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 +65 -2
- package/dist/commands.d.ts +9 -0
- package/dist/commands.js +84 -3
- package/dist/html5.d.ts +66 -0
- package/dist/html5.js +8009 -0
- package/dist/index.js +11 -2
- package/dist/licenses/entities.LICENSE +11 -0
- package/dist/licenses/parse5.LICENSE +19 -0
- package/dist/report.d.ts +127 -0
- package/dist/report.js +425 -0
- package/dist/scan.d.ts +1 -1
- package/dist/scan.js +4 -1
- package/dist/scanHtml.d.ts +38 -0
- package/dist/scanHtml.js +519 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ npx @mapled/cli scan
|
|
|
97
97
|
npx @mapled/cli scan --write
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
The scanner reads the site's source and writes down what it reads from Mapled: the pages (from the framework's file layout — Next.js App and Pages Router, Astro, SvelteKit, Nuxt, Remix) and one binding per place a collection or field is used.
|
|
100
|
+
The scanner reads the site's source and writes down what it reads from Mapled: the pages (from the framework's file layout — Next.js App and Pages Router, Astro, SvelteKit, Nuxt, Remix — or a plain HTML site's `.html` files) and one binding per place a collection or field is used.
|
|
101
101
|
|
|
102
102
|
```text
|
|
103
103
|
Scanned 42 files with TypeScript — 3 pages, 14 bindings.
|
|
@@ -113,6 +113,21 @@ No mapled/manifest.json yet — run `mapled scan --write` to create it, then `ma
|
|
|
113
113
|
|
|
114
114
|
It follows `getRecords` / `getRecord` / `getRecordBySlug` / `getSingle` calls through variables, destructuring, `.map()` callbacks and into the components a record is passed to as a prop, and through helpers in `lib/` that return a read. The parser is your repository's own TypeScript; without it the scanner still finds the collections, but not the fields. Source code never leaves your machine — only the manifest does, when you push it.
|
|
115
115
|
|
|
116
|
+
**A plain HTML site** (`framework` is `plain-html` in `mapled.json` — a folder with an `index.html` and no framework in `package.json`) has no reads in code: the scanner reads the `data-mapled-*` attributes of its `.html` files the way the [`@mapled/vanilla`](https://www.npmjs.com/package/@mapled/vanilla) script does, in the tree a browser builds from the file (the CLI carries the standard HTML parser, parse5, inside — nothing to install). Each file is a page where a static host serves it (`index.html` → `/`, `blog/index.html` → `/blog/`, `post.html` → `/post.html`, `public/` from the root); a record read by `data-mapled-slug-param` has its own address with that parameter in the query, so **Preview** in the record's editor opens the right page — and the scan says when Preview can't open an address. The target of a binding is what the attribute renders — `data-mapled-format="markdown"` rich text, an `<img>` an image, `data-mapled-bind-href` a link — and each different way a field is read is a binding of its own, so a field bound where it can't render shows up as a mismatch even when another element reads it well. With the schema at hand it also names the paths nothing in Mapled answers to: on the page, a misspelled field and an empty one look the same.
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
Scanned 2 HTML files — 2 pages, 11 bindings.
|
|
120
|
+
|
|
121
|
+
/ index.html
|
|
122
|
+
home headline, intro, tagline
|
|
123
|
+
posts list • date, slug, title
|
|
124
|
+
/post.html?slug=[slug] post.html
|
|
125
|
+
home headline
|
|
126
|
+
posts by slug • body, title
|
|
127
|
+
|
|
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
|
+
```
|
|
130
|
+
|
|
116
131
|
`--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.
|
|
117
132
|
|
|
118
133
|
## Validate and push the manifest
|
|
@@ -192,11 +207,59 @@ A site rendered in the visitor's browser — a React single-page app (Vite and t
|
|
|
192
207
|
|
|
193
208
|
Exit code 1 when something is marked ✗; `--json` prints the checks for scripts and agents.
|
|
194
209
|
|
|
210
|
+
## Read the setup report
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx @mapled/cli integration report
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
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:
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
Integration report — Dolphin Landing
|
|
220
|
+
|
|
221
|
+
Setup run Claude Code • checks failed • applied 2h ago • checked 1h ago
|
|
222
|
+
Plan “Blog with authors, a homepage and a contact form”
|
|
223
|
+
|
|
224
|
+
Verification
|
|
225
|
+
✓ Site reads content from Mapled Last read 3m ago
|
|
226
|
+
✗ Publish webhook works No deliveries since the plan was applied. Send a test ping from Integrations.
|
|
227
|
+
✓ Preview route responds Responds at /api/mapled/preview
|
|
228
|
+
⚠ Field names and help texts filled in 2 of 14 fields have no help text. Editors see it under the field.
|
|
229
|
+
✓ Site builds successfully Reported by the agent
|
|
230
|
+
✓ No secrets in the repository Reported by the agent
|
|
231
|
+
✓ MAPLED.md written Reported by the agent
|
|
232
|
+
|
|
233
|
+
Collections created
|
|
234
|
+
Articles (articles) collection • 6 fields • 12 records imported
|
|
235
|
+
Homepage (homepage) single • 4 fields • 1 record imported
|
|
236
|
+
|
|
237
|
+
Files changed
|
|
238
|
+
app/page.tsx
|
|
239
|
+
app/blog/[slug]/page.tsx
|
|
240
|
+
|
|
241
|
+
Still hardcoded
|
|
242
|
+
footer copyright
|
|
243
|
+
|
|
244
|
+
1 problem, 1 warning. Fix what the checks name, then `mapled integration report --verify` runs them again.
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`--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.
|
|
248
|
+
|
|
249
|
+
## Preview the drafts
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
npx @mapled/cli preview
|
|
253
|
+
npx @mapled/cli preview articles hello-world
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
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.
|
|
257
|
+
|
|
195
258
|
## Options
|
|
196
259
|
|
|
197
260
|
- `--api <origin>` — a Mapled API other than `https://api.mapled.io` (or set `MAPLED_API_URL`); `project link` remembers it in `mapled.json`
|
|
198
261
|
- `--no-browser` — print the sign-in link instead of opening a browser
|
|
199
|
-
- `--json` — machine-readable output (`doctor`, `schema diff`, `scan`, `manifest validate`, `forms list`)
|
|
262
|
+
- `--json` — machine-readable output (`doctor`, `integration report`, `schema diff`, `scan`, `manifest validate`, `forms list`, `preview`)
|
|
200
263
|
- `MAPLED_CONFIG_DIR` — where credentials live (default: `$XDG_CONFIG_HOME/mapled` or `~/.config/mapled`)
|
|
201
264
|
|
|
202
265
|
## 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,7 +11,9 @@ 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";
|
|
14
15
|
import { loadTypeScript, mergeManifest, scanRepository } from "./scan.js";
|
|
16
|
+
import { scanHtml } from "./scanHtml.js";
|
|
15
17
|
import { generateTypes } from "./types.js";
|
|
16
18
|
/** The OAuth client id for this API — registered once per machine, and
|
|
17
19
|
again when the server no longer knows it. */
|
|
@@ -446,6 +448,85 @@ export async function formsList(ctx, flags) {
|
|
|
446
448
|
ctx.out(`${f.key.padEnd(keyWidth)} ${f.displayName.padEnd(nameWidth)} spam protection ${f.spamProtection ? "on " : "off"} ${submitUrl(session.conn.api, f.key)}`);
|
|
447
449
|
}
|
|
448
450
|
}
|
|
451
|
+
/* ---- the integration report and preview (§31.2) ---- */
|
|
452
|
+
/** `mapled integration report` — the Verification checklist of the last
|
|
453
|
+
applied setup run (S-42) and what the agent reported; `--verify` runs
|
|
454
|
+
the server checks again first, as "Run checks again" does. */
|
|
455
|
+
export async function integrationReport(ctx, flags) {
|
|
456
|
+
const { found, session } = await openSession(ctx, flags);
|
|
457
|
+
const { runs } = await session.get("/v1/agent/runs");
|
|
458
|
+
const { shown, newer } = pickRun(runs);
|
|
459
|
+
let run = null;
|
|
460
|
+
if (shown && flags.verify) {
|
|
461
|
+
if (!isChecked(shown.status)) {
|
|
462
|
+
throw new CliError(`Nothing to check yet — the plan from ${clean(shown.clientName, 80)} hasn't been applied.`);
|
|
463
|
+
}
|
|
464
|
+
run = (await session.send("POST", `/v1/agent/runs/${encodeURIComponent(shown.id)}/verify`, {})).run;
|
|
465
|
+
}
|
|
466
|
+
else if (shown) {
|
|
467
|
+
run = (await session.get(`/v1/agent/runs/${encodeURIComponent(shown.id)}`)).run;
|
|
468
|
+
}
|
|
469
|
+
const report = buildReport({ id: found.config.project, name: session.conn.projectName }, run, newer);
|
|
470
|
+
if (flags.json)
|
|
471
|
+
ctx.out(JSON.stringify(report, null, 2));
|
|
472
|
+
else
|
|
473
|
+
for (const line of formatReport(report, useColor(ctx.env)))
|
|
474
|
+
ctx.out(line);
|
|
475
|
+
return report.checks.some((c) => c.status === "failed") ? 1 : 0;
|
|
476
|
+
}
|
|
477
|
+
/** `mapled preview [<collection> [<slug>]]` — a Preview link from Mapled's
|
|
478
|
+
launcher, the one the Preview button opens: the token is Mapled's, the
|
|
479
|
+
CLI only passes it on. `--open` hands it to the browser instead of
|
|
480
|
+
printing it (the link works once). */
|
|
481
|
+
export async function preview(ctx, flags, words) {
|
|
482
|
+
const [collection, record, ...rest] = words;
|
|
483
|
+
if (rest.length > 0) {
|
|
484
|
+
throw new CliError("Name a collection and, optionally, one record's slug: `mapled preview articles hello-world`.", 2);
|
|
485
|
+
}
|
|
486
|
+
const { session } = await openSession(ctx, flags);
|
|
487
|
+
let answer;
|
|
488
|
+
try {
|
|
489
|
+
answer = await session.send("POST", "/v1/agent/preview", {
|
|
490
|
+
...(collection ? { collection } : {}),
|
|
491
|
+
...(record ? { record } : {}),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
if (err instanceof ApiError && err.code === "NO_SITE") {
|
|
496
|
+
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.");
|
|
497
|
+
}
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
if (answer.status === "not_deployed") {
|
|
501
|
+
throw new CliError(`${clean(answer.origin)} doesn't answer — deploy the site publicly, then try again.`);
|
|
502
|
+
}
|
|
503
|
+
if (answer.status === "not_set_up") {
|
|
504
|
+
throw new CliError(`${clean(answer.origin)} has no preview route — mount createPreviewHandler from "@mapled/next/server" at /api/mapled/preview and redeploy.`);
|
|
505
|
+
}
|
|
506
|
+
// the link is printed as it came (masking would drop the token): printable ASCII only
|
|
507
|
+
if (typeof answer.url !== "string" || !/^https?:\/\/[!-~]+$/.test(answer.url)) {
|
|
508
|
+
throw new CliError("Mapled answered with a link this CLI can't print. Update @mapled/cli and try again.");
|
|
509
|
+
}
|
|
510
|
+
if (flags.json) {
|
|
511
|
+
ctx.out(JSON.stringify({ url: answer.url, path: answer.path, expiresAt: answer.expiresAt ?? null }, null, 2));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const where = answer.path ? clean(answer.path, 500) : "the site's home page";
|
|
515
|
+
const lifetime = linkLifetime(answer.expiresAt);
|
|
516
|
+
const project = clean(session.conn.projectName, 120);
|
|
517
|
+
if (flags.open && (await ctx.openBrowser(answer.url))) {
|
|
518
|
+
ctx.out(`Opened Preview of ${where} in your browser — the drafts of ${project}.`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
if (flags.open)
|
|
522
|
+
ctx.out("Couldn't open a browser — open this link yourself:");
|
|
523
|
+
ctx.out(answer.url);
|
|
524
|
+
ctx.out(`Preview of ${where} with the drafts of ${project}. The link works once${lifetime ? `, for ${lifetime}` : ""}.`);
|
|
525
|
+
}
|
|
526
|
+
if (collection && !answer.path) {
|
|
527
|
+
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.");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
449
530
|
/* ---- wave 2: the scanner ---- */
|
|
450
531
|
function describeScan(result, schema) {
|
|
451
532
|
const singles = new Set(schema.collections.filter((c) => c.kind === "single").map((c) => c.key));
|
|
@@ -490,8 +571,8 @@ export async function scan(ctx, flags) {
|
|
|
490
571
|
throw new CliError(`The scan needs the schema to name the fields — sign in with \`mapled auth login\` or run \`mapled schema pull\` first.`);
|
|
491
572
|
}
|
|
492
573
|
const framework = found.config.framework ?? (await dr.detectFramework(found.dir)) ?? null;
|
|
493
|
-
|
|
494
|
-
const result = await scanRepository(found.dir, { framework, schema, ts });
|
|
574
|
+
// a plain HTML site declares its reads in attributes, not in code
|
|
575
|
+
const result = framework === "plain-html" ? await scanHtml(found.dir, { schema }) : await scanRepository(found.dir, { framework, schema, ts: loadTypeScript(found.dir) });
|
|
495
576
|
const existing = await loadManifest(found.dir);
|
|
496
577
|
const merged = mergeManifest(existing?.manifest ?? null, result, { prune: Boolean(flags.prune) });
|
|
497
578
|
if (flags.json) {
|
|
@@ -500,7 +581,7 @@ export async function scan(ctx, flags) {
|
|
|
500
581
|
await writeManifestFile(found.dir, merged.manifest);
|
|
501
582
|
return;
|
|
502
583
|
}
|
|
503
|
-
ctx.out(`Scanned ${plural(result.files, "file")} with ${result.parser === "typescript" ? "TypeScript" : "the tokenizer"} — ${plural(result.pages.length, "page")}, ${plural(result.bindings.length, "binding")}.`);
|
|
584
|
+
ctx.out(`${result.parser === "html" ? `Scanned ${plural(result.files, "HTML file")}` : `Scanned ${plural(result.files, "file")} with ${result.parser === "typescript" ? "TypeScript" : "the tokenizer"}`} — ${plural(result.pages.length, "page")}, ${plural(result.bindings.length, "binding")}.`);
|
|
504
585
|
if (result.bindings.length > 0) {
|
|
505
586
|
ctx.out("");
|
|
506
587
|
for (const line of describeScan(result, schema))
|
package/dist/html5.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Token, type TreeAdapter, type TreeAdapterTypeMap } from "parse5";
|
|
2
|
+
/** The HTML parser of `mapled scan` for a plain HTML site: parse5, the
|
|
3
|
+
WHATWG parsing algorithm — the tree a browser builds, and the one
|
|
4
|
+
jsdom builds for the runtime's tests. Tag soup decides which scope a
|
|
5
|
+
binding lands in (a <p> a <div> closes, a table's foster parent, a
|
|
6
|
+
template an end tag can't leave), so the scan can't read a tree of its
|
|
7
|
+
own. build.mjs bundles parse5 (pinned) into dist/html5.js: the CLI
|
|
8
|
+
still installs without dependencies.
|
|
9
|
+
|
|
10
|
+
What a page can make the parser do is held linear in its size — a
|
|
11
|
+
page is the site's to write, and the scan reads up to 2 MB of it:
|
|
12
|
+
- open elements: at most MAX_OPEN (Guarded.onItemPush). Every start tag
|
|
13
|
+
looks down them for a scope; 100,000 unclosed <div>s cost minutes.
|
|
14
|
+
- a tag's attributes: a set of names (Guarded, _leaveAttrName). parse5
|
|
15
|
+
compared each new one with every earlier one — 64,000 on one <div>: 4 s.
|
|
16
|
+
- the tree: children in linked lists (`linked` below), every operation
|
|
17
|
+
O(1). parse5's own tree keeps arrays, and foster parenting (text
|
|
18
|
+
before a <table>: indexOf), the adoption agency (a misnested </b>:
|
|
19
|
+
splice from the front) and repeated <html>/<body> tags (every
|
|
20
|
+
attribute so far) walk them — 160,000 "a<br>" in a table: 11 s.
|
|
21
|
+
The worst page within the cap is the depth times its start tags. */
|
|
22
|
+
/** How deep elements may be open at once — Chrome's own limit of the tree it builds. */
|
|
23
|
+
export declare const MAX_OPEN = 512;
|
|
24
|
+
export declare class TooDeep extends Error {
|
|
25
|
+
}
|
|
26
|
+
export type Html5Attr = Token.Attribute;
|
|
27
|
+
/** A node of the tree: an element (tagName), text, a comment, a doctype,
|
|
28
|
+
the document or a template's content — children linked both ways. Its
|
|
29
|
+
fields are named as parse5's own tree names them; `childNodes` is made
|
|
30
|
+
when asked for (the parser never asks). */
|
|
31
|
+
export declare class Html5Node {
|
|
32
|
+
readonly nodeName: string;
|
|
33
|
+
tagName?: string;
|
|
34
|
+
namespaceURI?: string;
|
|
35
|
+
attrs?: Html5Attr[];
|
|
36
|
+
/** a <template>'s content: what the page's author wrote inside it */
|
|
37
|
+
content?: Html5Node;
|
|
38
|
+
parentNode: Html5Node | null;
|
|
39
|
+
firstChild: Html5Node | null;
|
|
40
|
+
lastChild: Html5Node | null;
|
|
41
|
+
previousSibling: Html5Node | null;
|
|
42
|
+
nextSibling: Html5Node | null;
|
|
43
|
+
/** a text's */
|
|
44
|
+
value?: string;
|
|
45
|
+
/** a comment's */
|
|
46
|
+
data?: string;
|
|
47
|
+
/** a doctype's */
|
|
48
|
+
name?: string;
|
|
49
|
+
publicId?: string;
|
|
50
|
+
systemId?: string;
|
|
51
|
+
/** the document's */
|
|
52
|
+
mode?: string;
|
|
53
|
+
/** the names an <html> or <body> has, as repeated tags add to them */
|
|
54
|
+
names?: Set<string>;
|
|
55
|
+
location?: Token.ElementLocation | null;
|
|
56
|
+
constructor(nodeName: string, extra?: Partial<Html5Node>);
|
|
57
|
+
/** none on a text, a comment or a doctype, as in parse5's tree */
|
|
58
|
+
get childNodes(): Html5Node[] | undefined;
|
|
59
|
+
}
|
|
60
|
+
type Map_ = TreeAdapterTypeMap<Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node>;
|
|
61
|
+
/** parse5's default tree adapter, method for method, on linked children. */
|
|
62
|
+
export declare const linked: TreeAdapter<Map_>;
|
|
63
|
+
/** The document, with scripting on — as the runtime sees it (a <noscript>
|
|
64
|
+
is text). Throws TooDeep past MAX_OPEN open elements. */
|
|
65
|
+
export declare function parseDocument(text: string): Html5Node;
|
|
66
|
+
export {};
|