@mapled/cli 0.1.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.
@@ -0,0 +1,65 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { CliError } from "./errors.js";
5
+ export function configDir(env = process.env) {
6
+ if (env.MAPLED_CONFIG_DIR)
7
+ return env.MAPLED_CONFIG_DIR;
8
+ if (process.platform === "win32" && env.APPDATA)
9
+ return path.join(env.APPDATA, "mapled");
10
+ const base = env.XDG_CONFIG_HOME || path.join(homedir(), ".config");
11
+ return path.join(base, "mapled");
12
+ }
13
+ export function credentialsPath(env = process.env) {
14
+ return path.join(configDir(env), "credentials.json");
15
+ }
16
+ export function emptyCredentials() {
17
+ return { version: 1, clients: {}, connections: [] };
18
+ }
19
+ export async function readCredentials(file) {
20
+ let raw;
21
+ try {
22
+ raw = await readFile(file, "utf8");
23
+ }
24
+ catch (err) {
25
+ if (err.code === "ENOENT")
26
+ return emptyCredentials();
27
+ throw err;
28
+ }
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(raw);
32
+ }
33
+ catch {
34
+ throw new CliError(`${file} is unreadable. Delete it and run \`mapled auth login\` again.`);
35
+ }
36
+ return {
37
+ version: 1,
38
+ clients: parsed.clients && typeof parsed.clients === "object" ? parsed.clients : {},
39
+ connections: Array.isArray(parsed.connections) ? parsed.connections : [],
40
+ };
41
+ }
42
+ /** 0600 in a 0700 directory, through a temp file: a crash never leaves
43
+ a half-written store behind. */
44
+ export async function writeCredentials(file, data) {
45
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
46
+ const tmp = `${file}.${process.pid}.tmp`;
47
+ await writeFile(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
48
+ await chmod(tmp, 0o600).catch(() => { });
49
+ await rename(tmp, file);
50
+ }
51
+ export function findConnection(data, api, projectId) {
52
+ return data.connections.find((c) => c.api === api && c.projectId === projectId);
53
+ }
54
+ export function connectionsFor(data, api) {
55
+ return data.connections.filter((c) => c.api === api);
56
+ }
57
+ export function upsertConnection(data, conn) {
58
+ return {
59
+ ...data,
60
+ connections: [...data.connections.filter((c) => !(c.api === conn.api && c.projectId === conn.projectId)), conn],
61
+ };
62
+ }
63
+ export function removeConnection(data, api, projectId) {
64
+ return { ...data, connections: data.connections.filter((c) => !(c.api === api && c.projectId === projectId)) };
65
+ }
@@ -0,0 +1,106 @@
1
+ import type { Connection } from "./credentials.js";
2
+ import type { Fetch } from "./oauth.js";
3
+ import { type Generated } from "./types.js";
4
+ /** `mapled doctor` (§31.3, wave 1): the checks themselves are pure
5
+ functions of what was gathered — the repository side here, the
6
+ Mapled side from GET /v1/agent/integration — so they test without a
7
+ network or a filesystem. Statuses and glyphs follow the Verification
8
+ checklist (S-42). */
9
+ export type CheckStatus = "passed" | "failed" | "warning" | "skipped";
10
+ export type Check = {
11
+ key: string;
12
+ label: string;
13
+ status: CheckStatus;
14
+ detail: string;
15
+ };
16
+ /** GET /v1/agent/integration */
17
+ export type IntegrationStatus = {
18
+ project: {
19
+ id: string;
20
+ name: string;
21
+ slug: string;
22
+ productionDomain: string | null;
23
+ };
24
+ connection: {
25
+ id: string;
26
+ name: string;
27
+ createdAt: string | null;
28
+ };
29
+ delivery: {
30
+ lastReadAt: string | null;
31
+ };
32
+ webhook: {
33
+ url: string | null;
34
+ lastDelivery: {
35
+ event: string;
36
+ status: string;
37
+ at: string;
38
+ responseStatus: number | null;
39
+ error: string | null;
40
+ } | null;
41
+ };
42
+ bindings: {
43
+ manifest: {
44
+ version: number;
45
+ clientName: string;
46
+ createdAt: string;
47
+ } | null;
48
+ summary: Record<string, number>;
49
+ };
50
+ previewPath: string;
51
+ sdk: Record<string, string>;
52
+ };
53
+ export declare function ago(iso: string): string;
54
+ export declare const ENV_FILES: string[];
55
+ /** Variable NAMES defined in the repository's env files. Values stay in
56
+ the file — they are split off and dropped, never kept or printed. */
57
+ export declare function envNames(dir: string): Promise<Map<string, string>>;
58
+ export type GitFacts = {
59
+ inRepo: boolean;
60
+ trackedEnvFiles: string[];
61
+ filesWithSecrets: string[];
62
+ };
63
+ /** Server keys, agent and OAuth tokens, the webhook signing secret. The
64
+ delivery key (mk_live_) is public by design and not a secret. */
65
+ export declare const SECRET_PATTERN = "(msk_live|mcp_live|mcp_oauth|mcp_refresh)_[A-Za-z0-9_-]{20,}|whsec_[0-9a-f]{16,}";
66
+ export declare function gitFacts(dir: string): Promise<GitFacts>;
67
+ export type PackageVersion = {
68
+ installed: string | null;
69
+ declared: string | null;
70
+ };
71
+ export declare function installedVersion(dir: string, pkg: string): Promise<PackageVersion>;
72
+ export declare function detectFramework(dir: string): Promise<string | undefined>;
73
+ /** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
74
+ export declare function compareVersions(a: string, b: string): number;
75
+ export type RouteKind = "revalidate" | "preview";
76
+ export declare const ROUTE_HANDLER: Record<RouteKind, string>;
77
+ export type RouteFile = {
78
+ file: string;
79
+ usesAdapter: boolean;
80
+ };
81
+ export declare function findRoute(dir: string, kind: RouteKind): Promise<RouteFile | null>;
82
+ /** GET the site's preview route without a token — the adapter answers 400. */
83
+ export declare function probePreview(origin: string, previewPath: string, f: Fetch): Promise<{
84
+ status: number | null;
85
+ }>;
86
+ export declare function checkLink(found: {
87
+ path: string;
88
+ } | null, projectName: string | null): Check;
89
+ export declare function checkAuth(state: {
90
+ conn: Connection | null;
91
+ status: IntegrationStatus | null;
92
+ error: string | null;
93
+ }): Check;
94
+ export declare function checkTypes(existing: string | null, generated: Generated | null, typesPath: string): Check;
95
+ export declare function checkEnv(names: Map<string, string>, env: NodeJS.ProcessEnv): Check;
96
+ export declare function checkSecrets(facts: GitFacts): Check;
97
+ export declare function checkSdk(pkg: PackageVersion, latest: string | undefined): Check;
98
+ export declare function checkCli(current: string, latest: string | undefined): Check;
99
+ export declare function checkRoute(kind: RouteKind, found: RouteFile | null, framework: string | undefined): Check;
100
+ export declare function checkReads(lastReadAt: string | null): Check;
101
+ export declare function checkWebhook(webhook: IntegrationStatus["webhook"]): Check;
102
+ export declare function checkPreviewOnSite(origin: string | null, previewPath: string, probe: {
103
+ status: number | null;
104
+ } | null): Check;
105
+ export declare function checkBindings(bindings: IntegrationStatus["bindings"]): Check;
106
+ export declare const REMOTE_CHECKS: [string, string][];
package/dist/doctor.js ADDED
@@ -0,0 +1,422 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { splitGenerated } from "./types.js";
6
+ export function ago(iso) {
7
+ const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000);
8
+ if (m < 1)
9
+ return "just now";
10
+ if (m < 60)
11
+ return `${m}m ago`;
12
+ const h = Math.floor(m / 60);
13
+ if (h < 24)
14
+ return `${h}h ago`;
15
+ return `${Math.floor(h / 24)}d ago`;
16
+ }
17
+ function hostOf(url) {
18
+ try {
19
+ return new URL(url).host;
20
+ }
21
+ catch {
22
+ return url;
23
+ }
24
+ }
25
+ /* ---- the repository side ---- */
26
+ export const ENV_FILES = [
27
+ ".env",
28
+ ".env.local",
29
+ ".env.development",
30
+ ".env.development.local",
31
+ ".env.production",
32
+ ".env.production.local",
33
+ ];
34
+ const ENV_EXAMPLE = /\.(example|sample|template)$/;
35
+ /** Variable NAMES defined in the repository's env files. Values stay in
36
+ the file — they are split off and dropped, never kept or printed. */
37
+ export async function envNames(dir) {
38
+ const found = new Map();
39
+ for (const name of ENV_FILES) {
40
+ let text;
41
+ try {
42
+ text = await readFile(path.join(dir, name), "utf8");
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ for (const line of text.split(/\r?\n/)) {
48
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
49
+ if (m && !found.has(m[1]))
50
+ found.set(m[1], name);
51
+ }
52
+ }
53
+ return found;
54
+ }
55
+ const run = promisify(execFile);
56
+ /** Server keys, agent and OAuth tokens, the webhook signing secret. The
57
+ delivery key (mk_live_) is public by design and not a secret. */
58
+ export const SECRET_PATTERN = "(msk_live|mcp_live|mcp_oauth|mcp_refresh)_[A-Za-z0-9_-]{20,}|whsec_[0-9a-f]{16,}";
59
+ export async function gitFacts(dir) {
60
+ const git = async (args, okCodes = [0]) => {
61
+ try {
62
+ const { stdout } = await run("git", args, { cwd: dir, maxBuffer: 8 * 1024 * 1024 });
63
+ return stdout;
64
+ }
65
+ catch (err) {
66
+ const code = err.code;
67
+ return typeof code === "number" && okCodes.includes(code) ? "" : null;
68
+ }
69
+ };
70
+ const inside = await git(["rev-parse", "--is-inside-work-tree"]);
71
+ if (inside === null || inside.trim() !== "true")
72
+ return { inRepo: false, trackedEnvFiles: [], filesWithSecrets: [] };
73
+ const tracked = (await git(["ls-files", "-z", "--", ".env", ".env.*", "*/.env", "*/.env.*"])) ?? "";
74
+ const trackedEnvFiles = tracked.split("\0").filter((f) => f && !ENV_EXAMPLE.test(f));
75
+ const grep = await git(["grep", "-I", "-l", "-E", SECRET_PATTERN, "--", ".", ":(exclude)*.md", ":(exclude)*.lock", ":(exclude)*-lock.json"], [0, 1]);
76
+ const filesWithSecrets = (grep ?? "")
77
+ .split("\n")
78
+ .map((s) => s.trim())
79
+ .filter(Boolean);
80
+ return { inRepo: true, trackedEnvFiles, filesWithSecrets };
81
+ }
82
+ export async function installedVersion(dir, pkg) {
83
+ let installed = null;
84
+ try {
85
+ const raw = await readFile(path.join(dir, "node_modules", pkg, "package.json"), "utf8");
86
+ installed = JSON.parse(raw).version ?? null;
87
+ }
88
+ catch {
89
+ installed = null;
90
+ }
91
+ let declared = null;
92
+ try {
93
+ const raw = await readFile(path.join(dir, "package.json"), "utf8");
94
+ const p = JSON.parse(raw);
95
+ declared = p.dependencies?.[pkg] ?? p.devDependencies?.[pkg] ?? null;
96
+ }
97
+ catch {
98
+ declared = null;
99
+ }
100
+ return { installed, declared };
101
+ }
102
+ export async function detectFramework(dir) {
103
+ try {
104
+ const raw = await readFile(path.join(dir, "package.json"), "utf8");
105
+ const p = JSON.parse(raw);
106
+ const deps = { ...p.devDependencies, ...p.dependencies };
107
+ if (deps.next)
108
+ return "nextjs";
109
+ if (deps.astro)
110
+ return "astro";
111
+ if (deps.nuxt)
112
+ return "nuxt";
113
+ if (deps["@sveltejs/kit"])
114
+ return "sveltekit";
115
+ if (deps["@remix-run/react"] || deps["react-router"])
116
+ return "remix";
117
+ return undefined;
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ }
123
+ /** -1, 0 or 1 comparing dotted numeric versions; anything else compares by number. */
124
+ export function compareVersions(a, b) {
125
+ const pa = a.replace(/^[^0-9]*/, "").split(".").map((n) => parseInt(n, 10) || 0);
126
+ const pb = b.replace(/^[^0-9]*/, "").split(".").map((n) => parseInt(n, 10) || 0);
127
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
128
+ const x = pa[i] ?? 0;
129
+ const y = pb[i] ?? 0;
130
+ if (x !== y)
131
+ return x < y ? -1 : 1;
132
+ }
133
+ return 0;
134
+ }
135
+ const ROUTE_FILES = {
136
+ revalidate: [
137
+ "app/api/mapled/revalidate/route",
138
+ "src/app/api/mapled/revalidate/route",
139
+ "pages/api/mapled/revalidate",
140
+ "src/pages/api/mapled/revalidate",
141
+ ],
142
+ preview: [
143
+ "app/api/mapled/preview/route",
144
+ "src/app/api/mapled/preview/route",
145
+ "pages/api/mapled/preview",
146
+ "src/pages/api/mapled/preview",
147
+ ],
148
+ };
149
+ const ROUTE_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".jsx"];
150
+ export const ROUTE_HANDLER = {
151
+ revalidate: "createRevalidateHandler",
152
+ preview: "createPreviewHandler",
153
+ };
154
+ export async function findRoute(dir, kind) {
155
+ for (const base of ROUTE_FILES[kind]) {
156
+ for (const ext of ROUTE_EXTENSIONS) {
157
+ const rel = base + ext;
158
+ let text;
159
+ try {
160
+ text = await readFile(path.join(dir, rel), "utf8");
161
+ }
162
+ catch {
163
+ continue;
164
+ }
165
+ return { file: rel, usesAdapter: text.includes(ROUTE_HANDLER[kind]) };
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ /** GET the site's preview route without a token — the adapter answers 400. */
171
+ export async function probePreview(origin, previewPath, f) {
172
+ try {
173
+ const res = await f(`${origin}${previewPath}`, {
174
+ method: "GET",
175
+ redirect: "manual",
176
+ headers: { "user-agent": "mapled-cli/doctor" },
177
+ signal: AbortSignal.timeout(5000),
178
+ });
179
+ return { status: res.status };
180
+ }
181
+ catch {
182
+ return { status: null };
183
+ }
184
+ }
185
+ /* ---- the checks ---- */
186
+ export function checkLink(found, projectName) {
187
+ const label = "Project link";
188
+ if (!found)
189
+ return { key: "link", label, status: "failed", detail: "No mapled.json here. Run `mapled project link`." };
190
+ return { key: "link", label, status: "passed", detail: projectName ? `${found.path} → ${projectName}` : found.path };
191
+ }
192
+ export function checkAuth(state) {
193
+ const label = "Signed in";
194
+ if (!state.conn) {
195
+ return { key: "auth", label, status: "failed", detail: "Not signed in to this project. Run `mapled auth login`." };
196
+ }
197
+ if (state.error || !state.status)
198
+ return { key: "auth", label, status: "failed", detail: state.error ?? "Mapled didn't answer." };
199
+ return {
200
+ key: "auth",
201
+ label,
202
+ status: "passed",
203
+ detail: `${state.status.connection.name} • ${state.status.project.name}`,
204
+ };
205
+ }
206
+ export function checkTypes(existing, generated, typesPath) {
207
+ const label = "Generated types";
208
+ if (!generated)
209
+ return { key: "types", label, status: "skipped", detail: "Sign in to compare with the schema." };
210
+ if (existing === null) {
211
+ return { key: "types", label, status: "failed", detail: `${typesPath} isn't generated yet. Run \`mapled types generate\`.` };
212
+ }
213
+ if (splitGenerated(existing).body === generated.body) {
214
+ return { key: "types", label, status: "passed", detail: `${typesPath} matches the schema (${generated.hash})` };
215
+ }
216
+ return { key: "types", label, status: "warning", detail: `${typesPath} is out of date — run \`mapled types generate\`.` };
217
+ }
218
+ export function checkEnv(names, env) {
219
+ const label = "Environment";
220
+ const where = (name) => names.get(name) ?? (env[name] ? "the environment" : null);
221
+ const key = where("MAPLED_KEY");
222
+ const secret = where("MAPLED_WEBHOOK_SECRET");
223
+ if (!key) {
224
+ return {
225
+ key: "env",
226
+ label,
227
+ status: "failed",
228
+ detail: "MAPLED_KEY isn't set. Put the delivery key (Integrations → Your site) in .env.local.",
229
+ };
230
+ }
231
+ if (!secret) {
232
+ return {
233
+ key: "env",
234
+ label,
235
+ status: "warning",
236
+ detail: `MAPLED_KEY in ${key}; MAPLED_WEBHOOK_SECRET isn't set — publishes won't refresh the site until it is.`,
237
+ };
238
+ }
239
+ return {
240
+ key: "env",
241
+ label,
242
+ status: "passed",
243
+ detail: `MAPLED_KEY and MAPLED_WEBHOOK_SECRET in ${key === secret ? key : `${key} and ${secret}`}`,
244
+ };
245
+ }
246
+ export function checkSecrets(facts) {
247
+ const label = "Secrets in git";
248
+ if (!facts.inRepo)
249
+ return { key: "secrets", label, status: "skipped", detail: "Not a git repository." };
250
+ if (facts.trackedEnvFiles.length > 0) {
251
+ return {
252
+ key: "secrets",
253
+ label,
254
+ status: "failed",
255
+ detail: `${facts.trackedEnvFiles.join(", ")} is tracked by git — untrack it (git rm --cached) and add it to .gitignore.`,
256
+ };
257
+ }
258
+ if (facts.filesWithSecrets.length > 0) {
259
+ return {
260
+ key: "secrets",
261
+ label,
262
+ status: "failed",
263
+ detail: `Mapled secrets found in ${facts.filesWithSecrets.join(", ")}. Rotate them in Integrations and remove them from the repository.`,
264
+ };
265
+ }
266
+ return { key: "secrets", label, status: "passed", detail: "No env files or Mapled secrets are tracked." };
267
+ }
268
+ export function checkSdk(pkg, latest) {
269
+ const label = "@mapled/next";
270
+ if (!pkg.installed && !pkg.declared) {
271
+ return { key: "sdk", label, status: "failed", detail: "Not installed. Run `npm install @mapled/next`." };
272
+ }
273
+ if (!pkg.installed) {
274
+ return { key: "sdk", label, status: "warning", detail: `${pkg.declared} is declared but not installed — run npm install.` };
275
+ }
276
+ if (!latest)
277
+ return { key: "sdk", label, status: "passed", detail: `${pkg.installed} installed` };
278
+ if (compareVersions(pkg.installed, latest) < 0) {
279
+ return {
280
+ key: "sdk",
281
+ label,
282
+ status: "warning",
283
+ detail: `${pkg.installed} installed, ${latest} available — npm install @mapled/next@latest.`,
284
+ };
285
+ }
286
+ return { key: "sdk", label, status: "passed", detail: `${pkg.installed} (current)` };
287
+ }
288
+ export function checkCli(current, latest) {
289
+ const label = "mapled CLI";
290
+ if (latest && compareVersions(current, latest) < 0) {
291
+ return { key: "cli", label, status: "warning", detail: `${current} running, ${latest} available — npx @mapled/cli@latest.` };
292
+ }
293
+ return { key: "cli", label, status: "passed", detail: latest ? `${current} (current)` : current };
294
+ }
295
+ export function checkRoute(kind, found, framework) {
296
+ const key = `route_${kind}`;
297
+ const label = kind === "revalidate" ? "Revalidation route" : "Preview route";
298
+ const handler = ROUTE_HANDLER[kind];
299
+ if (framework && framework !== "nextjs") {
300
+ return { key, label, status: "skipped", detail: `No ${kind} route check for ${framework} sites yet.` };
301
+ }
302
+ if (!found) {
303
+ return kind === "revalidate"
304
+ ? {
305
+ key,
306
+ label,
307
+ status: "failed",
308
+ detail: `app/api/mapled/revalidate/route.ts is missing — mount ${handler} from "@mapled/next/server".`,
309
+ }
310
+ : {
311
+ key,
312
+ label,
313
+ status: "warning",
314
+ detail: `app/api/mapled/preview/route.ts is missing — mount ${handler} from "@mapled/next/server" so Preview from Mapled works.`,
315
+ };
316
+ }
317
+ if (!found.usesAdapter) {
318
+ return { key, label, status: "warning", detail: `${found.file} exists but doesn't use ${handler} from "@mapled/next/server".` };
319
+ }
320
+ return { key, label, status: "passed", detail: found.file };
321
+ }
322
+ export function checkReads(lastReadAt) {
323
+ const label = "Site reads content";
324
+ if (!lastReadAt) {
325
+ return {
326
+ key: "reads",
327
+ label,
328
+ status: "warning",
329
+ detail: "No reads yet. Deploy the site and open a page that shows Mapled content.",
330
+ };
331
+ }
332
+ return { key: "reads", label, status: "passed", detail: `Last read ${ago(lastReadAt)}` };
333
+ }
334
+ export function checkWebhook(webhook) {
335
+ const label = "Publish webhook";
336
+ if (!webhook.url) {
337
+ return {
338
+ key: "webhook",
339
+ label,
340
+ status: "failed",
341
+ detail: "Not configured. Point it at https://<site>/api/mapled/revalidate in Integrations → Your site, or ask your agent.",
342
+ };
343
+ }
344
+ const host = hostOf(webhook.url);
345
+ const last = webhook.lastDelivery;
346
+ if (!last) {
347
+ return {
348
+ key: "webhook",
349
+ label,
350
+ status: "warning",
351
+ detail: `Configured (${host}), nothing delivered yet — send a test ping from Integrations.`,
352
+ };
353
+ }
354
+ if (last.status === "delivered") {
355
+ return { key: "webhook", label, status: "passed", detail: `Delivered ${ago(last.at)} to ${host}` };
356
+ }
357
+ if (last.status === "pending") {
358
+ return { key: "webhook", label, status: "warning", detail: `The last delivery to ${host} is still pending.` };
359
+ }
360
+ const reason = last.responseStatus ? `responded ${last.responseStatus}` : (last.error ?? "failed");
361
+ return {
362
+ key: "webhook",
363
+ label,
364
+ status: "failed",
365
+ detail: `The last delivery to ${host} ${reason}. Check the route and MAPLED_WEBHOOK_SECRET, then send a test ping.`,
366
+ };
367
+ }
368
+ export function checkPreviewOnSite(origin, previewPath, probe) {
369
+ const label = "Preview on the site";
370
+ if (!origin || !probe) {
371
+ return { key: "preview_site", label, status: "skipped", detail: "Needs the webhook URL to know where the site lives." };
372
+ }
373
+ if (probe.status === 400 || probe.status === 401) {
374
+ return { key: "preview_site", label, status: "passed", detail: `Responds at ${origin}${previewPath}` };
375
+ }
376
+ if (probe.status === null) {
377
+ return { key: "preview_site", label, status: "warning", detail: `${origin} is unreachable from here.` };
378
+ }
379
+ if (probe.status === 404) {
380
+ return {
381
+ key: "preview_site",
382
+ label,
383
+ status: "failed",
384
+ detail: `${previewPath} returned 404 on ${origin} — mount createPreviewHandler and redeploy.`,
385
+ };
386
+ }
387
+ return {
388
+ key: "preview_site",
389
+ label,
390
+ status: "warning",
391
+ detail: `${previewPath} responded with ${probe.status} — expected 400 without a token.`,
392
+ };
393
+ }
394
+ export function checkBindings(bindings) {
395
+ const label = "Bindings";
396
+ if (!bindings.manifest) {
397
+ return { key: "bindings", label, status: "skipped", detail: "Not synced yet — ask your AI agent to push the site manifest." };
398
+ }
399
+ const s = bindings.summary;
400
+ const issues = [];
401
+ if (s.missing_on_site)
402
+ issues.push(`${s.missing_on_site} missing on site`);
403
+ if (s.type_mismatch)
404
+ issues.push(`${s.type_mismatch} type mismatch${s.type_mismatch === 1 ? "" : "es"}`);
405
+ if (s.outdated)
406
+ issues.push(`${s.outdated} outdated`);
407
+ if (issues.length > 0) {
408
+ return { key: "bindings", label, status: "warning", detail: `${issues.join(", ")} — open Structure → Bindings in Mapled.` };
409
+ }
410
+ return {
411
+ key: "bindings",
412
+ label,
413
+ status: "passed",
414
+ detail: `${s.healthy ?? 0} healthy • synced ${ago(bindings.manifest.createdAt)} from ${bindings.manifest.clientName}`,
415
+ };
416
+ }
417
+ export const REMOTE_CHECKS = [
418
+ ["reads", "Site reads content"],
419
+ ["webhook", "Publish webhook"],
420
+ ["preview_site", "Preview on the site"],
421
+ ["bindings", "Bindings"],
422
+ ];
@@ -0,0 +1,6 @@
1
+ /** An error meant for the person at the terminal: printed as one line,
2
+ without a stack, with the exit code it asks for. */
3
+ export declare class CliError extends Error {
4
+ exitCode: number;
5
+ constructor(message: string, exitCode?: number);
6
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,10 @@
1
+ /** An error meant for the person at the terminal: printed as one line,
2
+ without a stack, with the exit code it asks for. */
3
+ export class CliError extends Error {
4
+ exitCode;
5
+ constructor(message, exitCode = 1) {
6
+ super(message);
7
+ this.name = "CliError";
8
+ this.exitCode = exitCode;
9
+ }
10
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};