@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/dist/manifest.d.ts
CHANGED
|
@@ -22,12 +22,21 @@ export type ManifestPage = {
|
|
|
22
22
|
route: string;
|
|
23
23
|
file?: string;
|
|
24
24
|
};
|
|
25
|
+
/** What the site can do beyond reading (§21.2): `realtime` — open tabs
|
|
26
|
+
follow Publish (§18.8), through `releaseRoute` when the site answers
|
|
27
|
+
the release itself. Not part of the integration hash: Mapled grades
|
|
28
|
+
nothing by it, it only tells the next agent (MAPLED.md). */
|
|
29
|
+
export type ManifestCapabilities = {
|
|
30
|
+
realtime?: boolean;
|
|
31
|
+
releaseRoute?: string;
|
|
32
|
+
};
|
|
25
33
|
export type Manifest = {
|
|
26
34
|
framework?: string;
|
|
27
35
|
integrationMode?: string;
|
|
28
36
|
pages?: ManifestPage[];
|
|
29
37
|
bindings: ManifestBinding[];
|
|
30
38
|
notes?: string[];
|
|
39
|
+
capabilities?: ManifestCapabilities;
|
|
31
40
|
};
|
|
32
41
|
export declare const KEY_PATTERN: RegExp;
|
|
33
42
|
export declare const LIMITS: {
|
|
@@ -48,6 +57,7 @@ export type Problem = {
|
|
|
48
57
|
path: string;
|
|
49
58
|
message: string;
|
|
50
59
|
};
|
|
60
|
+
export declare const CAPABILITY_KEYS: readonly ["realtime", "releaseRoute"];
|
|
51
61
|
/** Parses the file's text into a manifest with only the known fields,
|
|
52
62
|
reporting every shape problem the API would refuse (as errors) and the
|
|
53
63
|
parts it would silently drop (as warnings). */
|
|
@@ -83,15 +93,19 @@ export declare function readManifestFile(dir: string): Promise<{
|
|
|
83
93
|
the order they came in. */
|
|
84
94
|
export declare function writeManifestFile(dir: string, manifest: Manifest): Promise<string>;
|
|
85
95
|
export declare function orderManifest(manifest: Manifest): Manifest;
|
|
96
|
+
/** Whether two manifests declare the same capabilities (missing and empty are the same). */
|
|
97
|
+
export declare function sameCapabilities(a: ManifestCapabilities | undefined, b: ManifestCapabilities | undefined): boolean;
|
|
86
98
|
export declare function orderBinding(b: ManifestBinding): ManifestBinding;
|
|
87
99
|
export declare function plural(n: number, word: string, pluralWord?: string): string;
|
|
88
100
|
export declare function manifestSummary(manifest: Manifest): string;
|
|
89
101
|
/** Bindings that mean the same thing to Mapled (what a push stores). */
|
|
90
102
|
export declare function sameBinding(a: ManifestBinding, b: ManifestBinding): boolean;
|
|
103
|
+
/** `same` is about the bindings; `capabilitiesDiffer` says whether the capabilities do. */
|
|
91
104
|
export type ManifestComparison = {
|
|
92
105
|
same: boolean;
|
|
93
106
|
onlyLocal: string[];
|
|
94
107
|
onlyRemote: string[];
|
|
95
108
|
changed: string[];
|
|
109
|
+
capabilitiesDiffer: boolean;
|
|
96
110
|
};
|
|
97
111
|
export declare function compareManifests(local: Manifest, remote: Manifest): ManifestComparison;
|
package/dist/manifest.js
CHANGED
|
@@ -91,7 +91,8 @@ export function typeLabel(type) {
|
|
|
91
91
|
}
|
|
92
92
|
const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
93
93
|
const isText = (v, max, min = 1) => typeof v === "string" && v.length >= min && v.length <= max;
|
|
94
|
-
const TOP_KEYS = new Set(["framework", "integrationMode", "pages", "bindings", "notes"]);
|
|
94
|
+
const TOP_KEYS = new Set(["framework", "integrationMode", "pages", "bindings", "notes", "capabilities"]);
|
|
95
|
+
export const CAPABILITY_KEYS = ["realtime", "releaseRoute"];
|
|
95
96
|
const BINDING_KEYS = new Set(["key", "page", "component", "file", "collection", "field", "target", "required"]);
|
|
96
97
|
/** Parses the file's text into a manifest with only the known fields,
|
|
97
98
|
reporting every shape problem the API would refuse (as errors) and the
|
|
@@ -236,6 +237,40 @@ export function parseManifest(raw, file) {
|
|
|
236
237
|
manifest.notes = data.notes;
|
|
237
238
|
}
|
|
238
239
|
}
|
|
240
|
+
if (data.capabilities !== undefined) {
|
|
241
|
+
const caps = data.capabilities;
|
|
242
|
+
if (!isObject(caps)) {
|
|
243
|
+
error("capabilities", `"capabilities" must be an object — { realtime, releaseRoute }.`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const kept = {};
|
|
247
|
+
let ok = true;
|
|
248
|
+
for (const key of Object.keys(caps)) {
|
|
249
|
+
if (!CAPABILITY_KEYS.includes(key)) {
|
|
250
|
+
error(`capabilities.${key}`, `capabilities.${key} isn't a capability Mapled knows — only ${CAPABILITY_KEYS.join(" and ")}; Mapled refuses the push.`);
|
|
251
|
+
ok = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (caps.realtime !== undefined) {
|
|
255
|
+
if (typeof caps.realtime === "boolean")
|
|
256
|
+
kept.realtime = caps.realtime;
|
|
257
|
+
else {
|
|
258
|
+
error("capabilities.realtime", "capabilities.realtime must be true or false.");
|
|
259
|
+
ok = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (caps.releaseRoute !== undefined) {
|
|
263
|
+
if (isText(caps.releaseRoute, 200) && caps.releaseRoute.startsWith("/"))
|
|
264
|
+
kept.releaseRoute = caps.releaseRoute;
|
|
265
|
+
else {
|
|
266
|
+
error("capabilities.releaseRoute", `capabilities.releaseRoute must be the route's path on the site, e.g. "/api/mapled/release".`);
|
|
267
|
+
ok = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (ok)
|
|
271
|
+
manifest.capabilities = kept;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
239
274
|
return { manifest, problems };
|
|
240
275
|
}
|
|
241
276
|
export function indexSchema(schema) {
|
|
@@ -345,8 +380,19 @@ export function orderManifest(manifest) {
|
|
|
345
380
|
...(manifest.pages ? { pages: manifest.pages.map((p) => ({ route: p.route, ...(p.file !== undefined ? { file: p.file } : {}) })) } : {}),
|
|
346
381
|
bindings: manifest.bindings.map(orderBinding),
|
|
347
382
|
...(manifest.notes && manifest.notes.length > 0 ? { notes: [...manifest.notes] } : {}),
|
|
383
|
+
...(manifest.capabilities ? { capabilities: orderCapabilities(manifest.capabilities) } : {}),
|
|
348
384
|
};
|
|
349
385
|
}
|
|
386
|
+
function orderCapabilities(c) {
|
|
387
|
+
return {
|
|
388
|
+
...(c.realtime !== undefined ? { realtime: c.realtime } : {}),
|
|
389
|
+
...(c.releaseRoute !== undefined ? { releaseRoute: c.releaseRoute } : {}),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** Whether two manifests declare the same capabilities (missing and empty are the same). */
|
|
393
|
+
export function sameCapabilities(a, b) {
|
|
394
|
+
return (a?.realtime ?? null) === (b?.realtime ?? null) && (a?.releaseRoute ?? null) === (b?.releaseRoute ?? null);
|
|
395
|
+
}
|
|
350
396
|
export function orderBinding(b) {
|
|
351
397
|
const out = { key: b.key, page: b.page, collection: b.collection, target: b.target };
|
|
352
398
|
if (b.field !== undefined)
|
|
@@ -382,5 +428,11 @@ export function compareManifests(local, remote) {
|
|
|
382
428
|
const onlyLocal = local.bindings.filter((b) => !remoteByKey.has(b.key)).map((b) => b.key);
|
|
383
429
|
const onlyRemote = remote.bindings.filter((b) => !localByKey.has(b.key)).map((b) => b.key);
|
|
384
430
|
const changed = local.bindings.filter((b) => remoteByKey.has(b.key) && !sameBinding(b, remoteByKey.get(b.key))).map((b) => b.key);
|
|
385
|
-
return {
|
|
431
|
+
return {
|
|
432
|
+
same: onlyLocal.length === 0 && onlyRemote.length === 0 && changed.length === 0,
|
|
433
|
+
onlyLocal,
|
|
434
|
+
onlyRemote,
|
|
435
|
+
changed,
|
|
436
|
+
capabilitiesDiffer: !sameCapabilities(local.capabilities, remote.capabilities),
|
|
437
|
+
};
|
|
386
438
|
}
|
package/dist/pin.js
CHANGED
|
@@ -47,6 +47,9 @@ function pinField(f) {
|
|
|
47
47
|
// the formula is part of the schema hash: a pin without it would never match the live schema
|
|
48
48
|
if (f.type === "computed" && f.computed)
|
|
49
49
|
out.computed = { expression: f.computed.expression, resultType: f.computed.resultType };
|
|
50
|
+
// translation is part of the schema hash too (§28.2)
|
|
51
|
+
if (f.localized === true)
|
|
52
|
+
out.localized = true;
|
|
50
53
|
if (f.aliases && f.aliases.length > 0)
|
|
51
54
|
out.aliases = [...f.aliases];
|
|
52
55
|
return out;
|
|
@@ -195,6 +198,10 @@ export function diffSchema(before, after) {
|
|
|
195
198
|
const under = af.key;
|
|
196
199
|
const push = (op, detail, severity) => changes.push({ ...base, scope: "field", field: under, op, detail, severity });
|
|
197
200
|
diffCommon(bf, af, names, push);
|
|
201
|
+
// §28.2: the site reads the default language either way until it asks for a locale
|
|
202
|
+
if ((bf.localized === true) !== (af.localized === true)) {
|
|
203
|
+
push("changed", af.localized === true ? "now translated — the site still reads the default language" : "no longer translated — the default language's value stays", "safe");
|
|
204
|
+
}
|
|
198
205
|
if (bf.type === "relation" && af.type === "relation" && bf.relation && af.relation) {
|
|
199
206
|
if (bf.relation.target !== af.relation.target) {
|
|
200
207
|
push("changed", `relation to ${names.get(bf.relation.target) ?? bf.relation.target} → ${names.get(af.relation.target) ?? af.relation.target}`, "breaking");
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type Check } from "./doctor.js";
|
|
2
|
+
/** `mapled integration report` (§29.9, §31.2): the last setup run Mapled
|
|
3
|
+
holds for the project with its Verification checklist — the lines,
|
|
4
|
+
statuses and fix hints of S-42 — and what the agent reported. Nearly
|
|
5
|
+
every string here was typed by an agent or a member: it is cleaned of
|
|
6
|
+
control characters, and anything shaped like a Mapled key or token is
|
|
7
|
+
masked before it reaches the terminal or the JSON (§36.12 — the report
|
|
8
|
+
carries no secret, whatever an agent wrote into it). */
|
|
9
|
+
export type RunStatus = "awaiting_approval" | "approved" | "rejected" | "applying" | "verifying" | "checks_failed" | "completed" | "completed_with_warnings" | "failed";
|
|
10
|
+
/** A run as `GET /v1/agent/runs` lists it. */
|
|
11
|
+
export type RunSummary = {
|
|
12
|
+
id: string;
|
|
13
|
+
clientName: string;
|
|
14
|
+
status: RunStatus;
|
|
15
|
+
summary: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
appliedAt: string | null;
|
|
18
|
+
verifiedAt: string | null;
|
|
19
|
+
completedAt: string | null;
|
|
20
|
+
};
|
|
21
|
+
/** A run as `GET /v1/agent/runs/:id` shows it. */
|
|
22
|
+
export type RunDetail = RunSummary & {
|
|
23
|
+
result: {
|
|
24
|
+
collections?: {
|
|
25
|
+
key: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
fieldKeys?: string[];
|
|
29
|
+
recordsCreated?: number;
|
|
30
|
+
}[];
|
|
31
|
+
warnings?: string[];
|
|
32
|
+
report?: {
|
|
33
|
+
filesChanged?: string[];
|
|
34
|
+
stillHardcoded?: string[];
|
|
35
|
+
warnings?: string[];
|
|
36
|
+
previewUrl?: string | null;
|
|
37
|
+
};
|
|
38
|
+
checks?: Check[];
|
|
39
|
+
} | null;
|
|
40
|
+
};
|
|
41
|
+
export type IntegrationReport = {
|
|
42
|
+
project: {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
};
|
|
46
|
+
run: {
|
|
47
|
+
id: string;
|
|
48
|
+
client: string;
|
|
49
|
+
status: RunStatus;
|
|
50
|
+
summary: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
appliedAt: string | null;
|
|
53
|
+
verifiedAt: string | null;
|
|
54
|
+
} | null;
|
|
55
|
+
/** A later plan than the one reported, not applied (yet). */
|
|
56
|
+
newer: {
|
|
57
|
+
id: string;
|
|
58
|
+
client: string;
|
|
59
|
+
status: RunStatus;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
} | null;
|
|
62
|
+
checks: Check[];
|
|
63
|
+
collections: {
|
|
64
|
+
key: string;
|
|
65
|
+
displayName: string;
|
|
66
|
+
kind: string;
|
|
67
|
+
fields: number;
|
|
68
|
+
records: number;
|
|
69
|
+
}[];
|
|
70
|
+
filesChanged: string[];
|
|
71
|
+
stillHardcoded: string[];
|
|
72
|
+
warnings: string[];
|
|
73
|
+
previewUrl: string | null;
|
|
74
|
+
};
|
|
75
|
+
export declare const isChecked: (status: RunStatus) => boolean;
|
|
76
|
+
/** Mapled's keys and tokens, with the length and alphabet of the random
|
|
77
|
+
part Mapled issues: delivery and server keys — 16 bytes in hex
|
|
78
|
+
(`lib/deliveryKeys.ts`); agent, OAuth and code tokens — 32 bytes in
|
|
79
|
+
base64url (`generateToken`); preview links, draft sessions and the
|
|
80
|
+
webhook secret — 24 bytes in hex. The webhook secret's prefix is the
|
|
81
|
+
one of Standard Webhooks, and other issuers (Polar, Svix) write it
|
|
82
|
+
before base64 of 24 bytes or more, `+`, `/` and the `=` padding
|
|
83
|
+
included: the last kind, read up to its shortest length, so a site's
|
|
84
|
+
own secret doesn't show either. Mapled doesn't issue that one, so
|
|
85
|
+
behind a prefix written wholly in look-alikes it reads as text
|
|
86
|
+
(`keyLike`). */
|
|
87
|
+
export declare const KEYS: readonly (readonly [prefix: string, length: number, alphabet: "hex" | "base64url" | "base64"])[];
|
|
88
|
+
/** One line of text fit for a terminal: no control, blank-looking or
|
|
89
|
+
invisible characters, nothing that reads as a Mapled key. */
|
|
90
|
+
export declare function clean(text: unknown, max?: number): string;
|
|
91
|
+
/** The page an agent reported as its preview, without what may carry a
|
|
92
|
+
credential: no query, no fragment, no user info. The path is shown
|
|
93
|
+
decoded, so a key written in percent escapes is masked like any other;
|
|
94
|
+
a path that doesn't decode cleanly — malformed, or still escaped after
|
|
95
|
+
one decoding (encoded twice) — is dropped, and only the origin shows. */
|
|
96
|
+
export declare function reportedPage(url: unknown): string | null;
|
|
97
|
+
/** The run to report: the one applied last (an older plan can be applied
|
|
98
|
+
after a newer one); before any was applied, the newest there is. The
|
|
99
|
+
newer plan is the newest one proposed after it and not applied. */
|
|
100
|
+
export declare function pickRun(runs: RunSummary[]): {
|
|
101
|
+
shown: RunSummary | null;
|
|
102
|
+
newer: RunSummary | null;
|
|
103
|
+
};
|
|
104
|
+
export declare function buildReport(project: {
|
|
105
|
+
id: string;
|
|
106
|
+
name: string;
|
|
107
|
+
}, run: RunDetail | null, newer: RunSummary | null): IntegrationReport;
|
|
108
|
+
/** The closing line: what to do next. */
|
|
109
|
+
export declare function reportSummary(report: IntegrationReport): string;
|
|
110
|
+
export declare function formatReport(report: IntegrationReport, color: boolean): string[];
|
|
111
|
+
/** What `POST /v1/agent/preview` answers. */
|
|
112
|
+
export type PreviewAnswer = {
|
|
113
|
+
status: "ok";
|
|
114
|
+
url: string;
|
|
115
|
+
expiresAt?: string;
|
|
116
|
+
path: string | null;
|
|
117
|
+
} | {
|
|
118
|
+
status: "not_deployed";
|
|
119
|
+
origin: string;
|
|
120
|
+
path: string | null;
|
|
121
|
+
} | {
|
|
122
|
+
status: "not_set_up";
|
|
123
|
+
origin: string;
|
|
124
|
+
path: string | null;
|
|
125
|
+
};
|
|
126
|
+
/** "5 minutes" until the link stops working, or null when unknown. */
|
|
127
|
+
export declare function linkLifetime(expiresAt: string | undefined, now?: number): string | null;
|