@micacms/editor 0.5.0-rc.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,21 @@ site's dev server hot-reloads, and Publish turns the reviewed changes into one
39
39
  commit pushed with your own Git configuration (`MICA_LOCAL_PUBLISH=false`
40
40
  keeps Publish out of the editor entirely).
41
41
 
42
+ ## Two more commands
43
+
44
+ ```bash
45
+ npx @micacms/editor init # add Mica to the Astro project in this directory
46
+ npx @micacms/editor check # check every entry against the model
47
+ ```
48
+
49
+ `init` writes a starter `src/mica.ts` and `src/content.config.ts`, and never
50
+ overwrites a file. `check` reads every entry the way Mica and the
51
+ website read it, and reports what they would skip or refuse: fields that do
52
+ not validate, missing media, relations to entries that do not exist, slugs
53
+ that do not match their file names, and files that are not read at all. It
54
+ exits with 1 on an error, so it fits in CI. With the package installed, both
55
+ are also `npx mica init` and `npx mica check`.
56
+
42
57
  ## Working on Mica itself
43
58
 
44
59
  ```bash
@@ -72,6 +87,8 @@ proxies `/api` to the API; the API still needs
72
87
  ```
73
88
  src/
74
89
  ├── cli.ts the `mica` bin: flags, checks, environment, hand-off
90
+ ├── init.ts `mica init`: the starter files
91
+ ├── check.ts `mica check`: every entry against the model
75
92
  ├── index.ts the composition: developer policy + runtime + editor UI
76
93
  ├── config.ts the local slice of the environment, and the refusals
77
94
  ├── guards.ts the Host allowlist
@@ -1 +1 @@
1
- {"version":3,"file":"admin-dir.js","sourceRoot":"","sources":["../src/admin-dir.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,wDAAwD;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"admin-dir.js","sourceRoot":"","sources":["../src/admin-dir.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,wDAAwD;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC","sourcesContent":["/** Where the built editor UI lives: inside the @micacms/editor-ui package. */\n\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nconst require = createRequire(import.meta.url);\n\nexport function editorUiDist(): string | undefined {\n try {\n return path.join(path.dirname(require.resolve(\"@micacms/editor-ui/package.json\")), \"dist\");\n } catch {\n // Running from source before an install has linked the package. The app\n // falls back to its 503 explainer, which names the fix.\n return undefined;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `npx @micacms/editor` says when it works.\n *\n * The startup log lines are written for whoever is reading a container's\n * output later: every field that could matter, flat, greppable, and in no\n * particular order of importance. A person who has just typed a command needs\n * the opposite — the address to open, the directory being edited, and what\n * pressing Save will do to their repository. Three facts, in that order,\n * because the first minute decides whether there is a second one.\n *\n * Everything here is decoration over information the server already has. It is\n * printed instead of the startup log lines, never as well: two accounts of the\n * same event, disagreeing in tone, is worse than either alone.\n */\n\nimport path from \"node:path\";\n\nexport interface Banner {\n version: string;\n /** The origin the person should open, without the `/admin/` path. */\n url: string;\n contentRoot: string;\n collections: number;\n singletons: number;\n}\n\n/** ANSI, or nothing at all when the output is not a terminal. */\nexport function colors(enabled: boolean) {\n const wrap = (code: string) => (text: string) => (enabled ? `\u001b[${code}m${text}\u001b[0m` : text);\n return {\n bold: wrap(\"1\"),\n dim: wrap(\"2\"),\n brand: wrap(\"36\"),\n label: wrap(\"2\"),\n };\n}\n\n/**\n * True unless something says otherwise.\n *\n * `NO_COLOR` is the convention (no-color.org); a pipe or a file is the other\n * case, and escape codes in a redirected log help nobody.\n */\nexport function supportsColor(stream: { isTTY?: boolean }, env: NodeJS.ProcessEnv): boolean {\n if (env[\"NO_COLOR\"]) return false;\n if (env[\"FORCE_COLOR\"]) return true;\n return Boolean(stream.isTTY);\n}\n\n/** Paths inside the working directory read better as the relative ones. */\nfunction readable(target: string, cwd = process.cwd()): string {\n const relative = path.relative(cwd, target);\n if (!relative || relative.startsWith(\"..\") || path.isAbsolute(relative)) return target;\n return `./${relative}`;\n}\n\n/** \"3 collections and 2 singletons\", or whichever half is not zero. */\nfunction modelSummary(collections: number, singletons: number): string {\n const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? \"\" : \"s\"}`;\n const parts: string[] = [];\n if (collections > 0) parts.push(plural(collections, \"collection\"));\n if (singletons > 0) parts.push(plural(singletons, \"singleton\"));\n return parts.join(\" and \") || \"an empty model\";\n}\n\nexport function startupBanner(banner: Banner, useColor: boolean): string {\n const c = colors(useColor);\n const editor = `${banner.url}/admin/`;\n\n const row = (label: string, value: string): string =>\n ` ${c.label(label.padEnd(9))} ${value}`;\n\n const lines = [\n \"\",\n ` ${c.brand(\"◆\")} ${c.bold(\"Mica\")} ${c.dim(banner.version)}`,\n \"\",\n row(\"Editor\", c.bold(editor)),\n row(\"Content\", `${readable(banner.contentRoot)} ${c.dim(`— ${modelSummary(banner.collections, banner.singletons)}`)}`),\n row(\"Saving\", c.dim(\"writes files into that directory; Publish commits and pushes them.\")),\n \"\",\n ` ${c.dim(\"No sign-in on your own machine — the link above opens straight into the editor.\")}`,\n ` ${c.dim(\"Mica keeps no state of its own here: everything you save is a file in that directory.\")}`,\n \"\",\n ` ${c.dim(\"Run your site's dev server beside this to see the changes. Ctrl+C stops Mica.\")}`,\n \"\",\n ];\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * The one failure this command is most likely to hit, and the one the operating\n * system explains worst: `EADDRINUSE` alone tells nobody which port or what to\n * do instead.\n */\nexport function portInUse(port: number, host: string, useColor: boolean): string {\n const c = colors(useColor);\n return (\n `\\n ${c.bold(`Port ${port} is already in use on ${host}.`)}\\n\\n` +\n ` Something else is listening there — another Mica, most likely.\\n` +\n ` Stop it, or give this one a different port:\\n\\n` +\n ` npx @micacms/editor --port ${port + 1}\\n\\n`\n );\n}\n"]}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `mica check` — reads every entry the way Mica and the website read it, and
3
+ * reports what they would skip, refuse or render as a gap. It never opens a
4
+ * browser and exits with 1 when it finds an error, for CI and coding agents.
5
+ */
6
+ export interface CheckProblem {
7
+ level: "error" | "warning";
8
+ /** The file, relative to the content root. */
9
+ path: string;
10
+ message: string;
11
+ }
12
+ export interface CheckReport {
13
+ problems: CheckProblem[];
14
+ /** False when the model could not be read, so nothing else was checked. */
15
+ model: boolean;
16
+ collections: number;
17
+ entries: number;
18
+ singletons: number;
19
+ }
20
+ /** Checks the content under `root` against the model in `<root>/.mica/schema.json`. */
21
+ export declare function checkContent(root: string): Promise<CheckReport>;
22
+ /**
23
+ * What Markdoc renders differently from Markdown, found in the text outside
24
+ * code. A scan, not a parse: it can warn about too much, never about too little.
25
+ */
26
+ export declare function markdocWarnings(source: string): string[];
27
+ /** The report as a person or an agent reads it. */
28
+ export declare function formatReport(report: CheckReport, root: string): string;
29
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2BH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,uFAAuF;AACvF,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwErE;AAoJD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAexD;AAiED,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBtE"}
package/dist/check.js ADDED
@@ -0,0 +1,308 @@
1
+ /**
2
+ * `mica check` — reads every entry the way Mica and the website read it, and
3
+ * reports what they would skip, refuse or render as a gap. It never opens a
4
+ * browser and exits with 1 when it finds an error, for CI and coding agents.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { relative } from "node:path";
8
+ import { DOCUMENT_EXTENSION, documentPath, entryLayout, INDEX_FILE, isValidSlug, MANIFEST_PATH, ORDER_FILE, readCollectionEntry, readSingleton, resolveSlugField, singletonPath, validateFields, } from "@micacms/core";
9
+ import { within } from "@micacms/core/node";
10
+ import { contentFiles, loadSchema, readOnlyParts } from "@micacms/runtime";
11
+ /** Checks the content under `root` against the model in `<root>/.mica/schema.json`. */
12
+ export async function checkContent(root) {
13
+ const files = contentFiles({ root });
14
+ const report = { problems: [], model: false, collections: 0, entries: 0, singletons: 0 };
15
+ const error = (path, message) => report.problems.push({ level: "error", path, message });
16
+ const warning = (path, message) => report.problems.push({ level: "warning", path, message });
17
+ const manifest = await readText(files, MANIFEST_PATH);
18
+ if (manifest === null || manifest instanceof Error) {
19
+ error(MANIFEST_PATH, manifest?.message ?? "There is no content model. Run `npx astro sync` in the Astro project to write it.");
20
+ return report;
21
+ }
22
+ let site;
23
+ try {
24
+ site = await loadSchema(files);
25
+ }
26
+ catch (cause) {
27
+ error(MANIFEST_PATH, cause.message);
28
+ return report;
29
+ }
30
+ report.model = true;
31
+ // A model from a newer @micacms/astro: this check cannot judge those entries.
32
+ const readOnly = readOnlyParts(site);
33
+ for (const reason of readOnly.values())
34
+ error(MANIFEST_PATH, `Not checked. ${reason}`);
35
+ const slugs = new Map();
36
+ for (const [name, def] of Object.entries(site.collections)) {
37
+ slugs.set(name, new Set(await entrySlugs(files, def.fields, name, error, warning)));
38
+ }
39
+ const context = { root, files, site, slugs, error, warning };
40
+ for (const [name, def] of Object.entries(site.collections)) {
41
+ report.collections++;
42
+ if (readOnly.has(`collections.${name}`))
43
+ continue;
44
+ const slugField = resolveSlugField(def);
45
+ for (const slug of slugs.get(name)) {
46
+ report.entries++;
47
+ const directory = entryLayout(def.fields) === "directory";
48
+ const base = directory ? `${name}/${slug}` : null;
49
+ const dataPath = directory ? `${base}/${INDEX_FILE}` : `${name}/${slug}.json`;
50
+ const data = await readOrReport(() => readCollectionEntry(files, def, name, slug), dataPath, error);
51
+ if (!data)
52
+ continue;
53
+ const stored = slugField ? data[slugField] : undefined;
54
+ if (typeof stored === "string" && stored !== "" && stored !== slug) {
55
+ warning(dataPath, `${slugField} is "${stored}", but the entry is named "${slug}". The next save in the editor renames the entry.`);
56
+ }
57
+ await checkEntry(context, def.fields, data, dataPath, base);
58
+ }
59
+ }
60
+ for (const [name, def] of Object.entries(site.singletons)) {
61
+ report.singletons++;
62
+ if (readOnly.has(`singletons.${name}`))
63
+ continue;
64
+ const directory = entryLayout(def.fields) === "directory";
65
+ const base = singletonPath(def.fields, name);
66
+ const dataPath = directory ? `${base}/${INDEX_FILE}` : base;
67
+ const otherLayout = directory ? `${name}.json` : `${name}/${INDEX_FILE}`;
68
+ if ((await readText(files, otherLayout)) !== null) {
69
+ error(otherLayout, `Not read: "${def.label}" is stored as ${dataPath}, because ${directory ? "it has" : "it has no"} rich text or media.`);
70
+ }
71
+ const data = await readOrReport(() => readSingleton(files, def, name), dataPath, error);
72
+ if (data === undefined)
73
+ continue;
74
+ if (data === null) {
75
+ warning(dataPath, `No content for "${def.label}" yet.`);
76
+ continue;
77
+ }
78
+ await checkEntry(context, def.fields, data, dataPath, directory ? base : null);
79
+ }
80
+ return report;
81
+ }
82
+ /** The slugs Mica reads in a collection, reporting what it would skip. */
83
+ async function entrySlugs(files, fields, name, error, warning) {
84
+ const directory = entryLayout(fields) === "directory";
85
+ const shape = directory ? `a folder with ${INDEX_FILE}` : "a <slug>.json file";
86
+ const found = [];
87
+ for (const item of await files.list(name)) {
88
+ const path = `${name}/${item.name}`;
89
+ if (item.name.startsWith("."))
90
+ continue;
91
+ if (item.name === ORDER_FILE) {
92
+ checkOrder(await readText(files, path), path, error);
93
+ continue;
94
+ }
95
+ // An entry in the other layout is lost content, not a stray file: the model gained or lost rich text or media.
96
+ if (directory && item.type === "file" && item.name.endsWith(".json")) {
97
+ error(path, `Not read: entries of "${name}" are folders with ${INDEX_FILE}, because it has rich text or media.`);
98
+ continue;
99
+ }
100
+ if (!directory && item.type === "directory" && (await readText(files, `${path}/${INDEX_FILE}`)) !== null) {
101
+ error(path, `Not read: entries of "${name}" are <slug>.json files, because it has no rich text or media.`);
102
+ continue;
103
+ }
104
+ const slug = directory ? item.name : item.name.replace(/\.json$/, "");
105
+ const looksLikeEntry = directory ? item.type === "directory" : item.type === "file" && item.name.endsWith(".json");
106
+ if (!looksLikeEntry) {
107
+ warning(path, `Not read. Each entry of "${name}" is ${shape}.`);
108
+ continue;
109
+ }
110
+ if (!isValidSlug(slug)) {
111
+ error(path, `Not read: "${slug}" is not a slug. Use lowercase letters, digits and single hyphens.`);
112
+ continue;
113
+ }
114
+ const index = directory ? await readText(files, `${path}/${INDEX_FILE}`) : "";
115
+ if (index === null || index instanceof Error) {
116
+ error(path, `Not read: ${index?.message ?? `the folder has no ${INDEX_FILE}.`}`);
117
+ continue;
118
+ }
119
+ found.push(slug);
120
+ }
121
+ return found;
122
+ }
123
+ /** The file's text, null when it is not there, or the error that reading it gave. */
124
+ async function readText(files, path) {
125
+ try {
126
+ return await files.read(path);
127
+ }
128
+ catch (cause) {
129
+ return cause;
130
+ }
131
+ }
132
+ function checkOrder(text, path, error) {
133
+ if (text === null)
134
+ return;
135
+ if (text instanceof Error)
136
+ return error(path, text.message);
137
+ let order;
138
+ try {
139
+ order = JSON.parse(text);
140
+ }
141
+ catch {
142
+ return error(path, "Not valid JSON. The build fails on it.");
143
+ }
144
+ if (!Array.isArray(order)) {
145
+ error(path, 'Must be a JSON array of slugs, such as ["second", "first"]. The build fails on it.');
146
+ }
147
+ }
148
+ async function readOrReport(read, path, error) {
149
+ try {
150
+ return await read();
151
+ }
152
+ catch (cause) {
153
+ error(path, cause.message.replace(`${path}: `, ""));
154
+ return undefined;
155
+ }
156
+ }
157
+ async function checkEntry(context, fields, data, dataPath, base) {
158
+ const { root, files, slugs, error, warning } = context;
159
+ const result = validateFields(fields, data, "storage");
160
+ if (!result.ok) {
161
+ for (const issue of result.issues)
162
+ error(dataPath, issue.path ? `${issue.path}: ${issue.message}` : issue.message);
163
+ }
164
+ const raw = JSON.parse((await files.read(dataPath)));
165
+ for (const message of unknownKeys(fields, raw, []))
166
+ warning(dataPath, message);
167
+ visit(fields, data, [], (field, value, path) => {
168
+ const key = path.join(".");
169
+ if (field.kind === "media") {
170
+ for (const file of list(value)) {
171
+ let exists = false;
172
+ try {
173
+ exists = existsSync(within(root, file));
174
+ }
175
+ catch {
176
+ // A path outside the content root does not exist for Mica.
177
+ }
178
+ if (!exists)
179
+ error(dataPath, `${key}: the file "${file}" does not exist under the content root.`);
180
+ }
181
+ }
182
+ if (field.kind === "relation") {
183
+ for (const slug of list(value)) {
184
+ if (!slugs.get(field.collection)?.has(slug)) {
185
+ error(dataPath, `${key}: there is no "${slug}" in "${field.collection}".`);
186
+ }
187
+ }
188
+ }
189
+ });
190
+ if (base === null)
191
+ return;
192
+ for (const item of await files.list(base)) {
193
+ if (item.type !== "file" || !item.name.endsWith(DOCUMENT_EXTENSION))
194
+ continue;
195
+ const path = `${base}/${item.name}`;
196
+ if (!namesRichText(fields, documentPath(item.name))) {
197
+ warning(path, "Not read: no rich-text field has this name.");
198
+ continue;
199
+ }
200
+ for (const message of markdocWarnings((await files.read(path)) ?? ""))
201
+ warning(path, message);
202
+ }
203
+ }
204
+ /**
205
+ * What Markdoc renders differently from Markdown, found in the text outside
206
+ * code. A scan, not a parse: it can warn about too much, never about too little.
207
+ */
208
+ export function markdocWarnings(source) {
209
+ const prose = source.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, "").replace(/`[^`\n]*`/g, "");
210
+ const messages = [];
211
+ const html = [...new Set([...prose.matchAll(/<\/?([a-zA-Z][a-zA-Z0-9-]*)(?=[\s/>])[^>\n]*>/g)].map((match) => match[1].toLowerCase()))];
212
+ if (html.length > 0) {
213
+ messages.push(`Raw HTML (${html.map((tag) => `<${tag}>`).join(", ")}) is not rendered: Markdoc escapes it.`);
214
+ }
215
+ const tags = [...new Set([...prose.matchAll(/\{%\s*([a-zA-Z][\w-]*)/g)].map((match) => match[1]))];
216
+ if (tags.length > 0) {
217
+ messages.push(`Markdoc tags (${tags.join(", ")}) render only if the website declares them in renderRichText(source, { config }).`);
218
+ }
219
+ if (/\[\^[^\]]+\]/.test(prose))
220
+ messages.push("Footnotes ([^1]) are not Markdoc: they render as plain text.");
221
+ return messages;
222
+ }
223
+ function list(value) {
224
+ const values = Array.isArray(value) ? value : [value];
225
+ return values.filter((item) => typeof item === "string" && item !== "");
226
+ }
227
+ /** Calls `onField` for every field with a value, nested fields included. */
228
+ function visit(fields, data, path, onField) {
229
+ for (const [key, field] of Object.entries(fields)) {
230
+ const value = data[key];
231
+ if (value === undefined || value === null)
232
+ continue;
233
+ onField(field, value, [...path, key]);
234
+ if (field.kind === "object" && isRecord(value))
235
+ visit(field.fields, value, [...path, key], onField);
236
+ if (field.kind === "repeater" && Array.isArray(value)) {
237
+ value.forEach((row, index) => {
238
+ if (isRecord(row))
239
+ visit(field.fields, row, [...path, key, index], onField);
240
+ });
241
+ }
242
+ }
243
+ }
244
+ /** Keys in the stored JSON that the model does not have; Mica drops them on the next save. */
245
+ function unknownKeys(fields, raw, path) {
246
+ const messages = [];
247
+ for (const [key, value] of Object.entries(raw)) {
248
+ const at = [...path, key].join(".");
249
+ const field = fields[key];
250
+ if (!field) {
251
+ messages.push(`${at}: the model has no such field. Mica drops it on the next save.`);
252
+ }
253
+ else if (field.kind === "richText") {
254
+ messages.push(`${at}: rich text belongs in ${[...path, key].join(".")}${DOCUMENT_EXTENSION}, not in the JSON.`);
255
+ }
256
+ else if (field.kind === "object" && isRecord(value)) {
257
+ messages.push(...unknownKeys(field.fields, value, [...path, key]));
258
+ }
259
+ else if (field.kind === "repeater" && Array.isArray(value)) {
260
+ value.forEach((row, index) => {
261
+ if (isRecord(row))
262
+ messages.push(...unknownKeys(field.fields, row, [...path, key, index]));
263
+ });
264
+ }
265
+ }
266
+ return messages;
267
+ }
268
+ /** Whether a sidecar's dotted path leads to a rich-text field. */
269
+ function namesRichText(fields, path) {
270
+ let map = fields;
271
+ for (const [index, segment] of path.entries()) {
272
+ if (typeof segment === "number")
273
+ continue;
274
+ const field = map?.[segment];
275
+ if (!field)
276
+ return false;
277
+ if (index === path.length - 1)
278
+ return field.kind === "richText";
279
+ map = field.kind === "object" || field.kind === "repeater" ? field.fields : undefined;
280
+ }
281
+ return false;
282
+ }
283
+ function isRecord(value) {
284
+ return value !== null && typeof value === "object" && !Array.isArray(value);
285
+ }
286
+ /** The report as a person or an agent reads it. */
287
+ export function formatReport(report, root) {
288
+ const errors = report.problems.filter((problem) => problem.level === "error").length;
289
+ const warnings = report.problems.length - errors;
290
+ const lines = [
291
+ report.model
292
+ ? `Checked ${report.entries} ${report.entries === 1 ? "entry" : "entries"} in ${report.collections} ` +
293
+ `${report.collections === 1 ? "collection" : "collections"} and ${report.singletons} ` +
294
+ `${report.singletons === 1 ? "singleton" : "singletons"} in ${relative(process.cwd(), root) || "."}.`
295
+ : `Could not read the content model in ${relative(process.cwd(), root) || "."}, so no entry was checked.`,
296
+ "",
297
+ ];
298
+ for (const problem of report.problems) {
299
+ lines.push(` ${problem.level === "error" ? "error " : "warning"} ${problem.path}`);
300
+ for (const line of problem.message.split("\n"))
301
+ lines.push(` ${line.trim()}`);
302
+ }
303
+ if (report.problems.length > 0)
304
+ lines.push("");
305
+ lines.push(`${errors} ${errors === 1 ? "error" : "errors"}, ${warnings} ${warnings === 1 ? "warning" : "warnings"}.`);
306
+ return `${lines.join("\n")}\n`;
307
+ }
308
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `mica check` — reads every entry the way Mica and the website read it, and\n * reports what they would skip, refuse or render as a gap. It never opens a\n * browser and exits with 1 when it finds an error, for CI and coding agents.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\n\nimport {\n DOCUMENT_EXTENSION,\n documentPath,\n entryLayout,\n INDEX_FILE,\n isValidSlug,\n MANIFEST_PATH,\n ORDER_FILE,\n readCollectionEntry,\n readSingleton,\n resolveSlugField,\n singletonPath,\n validateFields,\n type AnyField,\n type ContentSource,\n type FieldMap,\n type PathSegment,\n type Site,\n} from \"@micacms/core\";\nimport { within } from \"@micacms/core/node\";\nimport { contentFiles, loadSchema, readOnlyParts } from \"@micacms/runtime\";\n\nexport interface CheckProblem {\n level: \"error\" | \"warning\";\n /** The file, relative to the content root. */\n path: string;\n message: string;\n}\n\nexport interface CheckReport {\n problems: CheckProblem[];\n /** False when the model could not be read, so nothing else was checked. */\n model: boolean;\n collections: number;\n entries: number;\n singletons: number;\n}\n\n/** Checks the content under `root` against the model in `<root>/.mica/schema.json`. */\nexport async function checkContent(root: string): Promise<CheckReport> {\n const files = contentFiles({ root });\n const report: CheckReport = { problems: [], model: false, collections: 0, entries: 0, singletons: 0 };\n const error = (path: string, message: string) => report.problems.push({ level: \"error\", path, message });\n const warning = (path: string, message: string) => report.problems.push({ level: \"warning\", path, message });\n\n const manifest = await readText(files, MANIFEST_PATH);\n if (manifest === null || manifest instanceof Error) {\n error(MANIFEST_PATH, manifest?.message ?? \"There is no content model. Run `npx astro sync` in the Astro project to write it.\");\n return report;\n }\n let site: Site;\n try {\n site = await loadSchema(files);\n } catch (cause) {\n error(MANIFEST_PATH, (cause as Error).message);\n return report;\n }\n report.model = true;\n\n // A model from a newer @micacms/astro: this check cannot judge those entries.\n const readOnly = readOnlyParts(site);\n for (const reason of readOnly.values()) error(MANIFEST_PATH, `Not checked. ${reason}`);\n\n const slugs = new Map<string, Set<string>>();\n for (const [name, def] of Object.entries(site.collections)) {\n slugs.set(name, new Set(await entrySlugs(files, def.fields, name, error, warning)));\n }\n\n const context: EntryContext = { root, files, site, slugs, error, warning };\n\n for (const [name, def] of Object.entries(site.collections)) {\n report.collections++;\n if (readOnly.has(`collections.${name}`)) continue;\n const slugField = resolveSlugField(def);\n for (const slug of slugs.get(name)!) {\n report.entries++;\n const directory = entryLayout(def.fields) === \"directory\";\n const base = directory ? `${name}/${slug}` : null;\n const dataPath = directory ? `${base}/${INDEX_FILE}` : `${name}/${slug}.json`;\n\n const data = await readOrReport(() => readCollectionEntry(files, def, name, slug), dataPath, error);\n if (!data) continue;\n const stored = slugField ? data[slugField] : undefined;\n if (typeof stored === \"string\" && stored !== \"\" && stored !== slug) {\n warning(dataPath, `${slugField} is \"${stored}\", but the entry is named \"${slug}\". The next save in the editor renames the entry.`);\n }\n await checkEntry(context, def.fields, data, dataPath, base);\n }\n }\n\n for (const [name, def] of Object.entries(site.singletons)) {\n report.singletons++;\n if (readOnly.has(`singletons.${name}`)) continue;\n const directory = entryLayout(def.fields) === \"directory\";\n const base = singletonPath(def.fields, name);\n const dataPath = directory ? `${base}/${INDEX_FILE}` : base;\n const otherLayout = directory ? `${name}.json` : `${name}/${INDEX_FILE}`;\n if ((await readText(files, otherLayout)) !== null) {\n error(otherLayout, `Not read: \"${def.label}\" is stored as ${dataPath}, because ${directory ? \"it has\" : \"it has no\"} rich text or media.`);\n }\n\n const data = await readOrReport(() => readSingleton(files, def, name), dataPath, error);\n if (data === undefined) continue;\n if (data === null) {\n warning(dataPath, `No content for \"${def.label}\" yet.`);\n continue;\n }\n await checkEntry(context, def.fields, data, dataPath, directory ? base : null);\n }\n\n return report;\n}\n\ninterface EntryContext {\n root: string;\n files: ContentSource;\n site: Site;\n slugs: Map<string, Set<string>>;\n error(path: string, message: string): void;\n warning(path: string, message: string): void;\n}\n\n/** The slugs Mica reads in a collection, reporting what it would skip. */\nasync function entrySlugs(\n files: ContentSource,\n fields: FieldMap,\n name: string,\n error: (path: string, message: string) => void,\n warning: (path: string, message: string) => void,\n): Promise<string[]> {\n const directory = entryLayout(fields) === \"directory\";\n const shape = directory ? `a folder with ${INDEX_FILE}` : \"a <slug>.json file\";\n const found: string[] = [];\n\n for (const item of await files.list(name)) {\n const path = `${name}/${item.name}`;\n if (item.name.startsWith(\".\")) continue;\n if (item.name === ORDER_FILE) {\n checkOrder(await readText(files, path), path, error);\n continue;\n }\n\n // An entry in the other layout is lost content, not a stray file: the model gained or lost rich text or media.\n if (directory && item.type === \"file\" && item.name.endsWith(\".json\")) {\n error(path, `Not read: entries of \"${name}\" are folders with ${INDEX_FILE}, because it has rich text or media.`);\n continue;\n }\n if (!directory && item.type === \"directory\" && (await readText(files, `${path}/${INDEX_FILE}`)) !== null) {\n error(path, `Not read: entries of \"${name}\" are <slug>.json files, because it has no rich text or media.`);\n continue;\n }\n\n const slug = directory ? item.name : item.name.replace(/\\.json$/, \"\");\n const looksLikeEntry = directory ? item.type === \"directory\" : item.type === \"file\" && item.name.endsWith(\".json\");\n if (!looksLikeEntry) {\n warning(path, `Not read. Each entry of \"${name}\" is ${shape}.`);\n continue;\n }\n if (!isValidSlug(slug)) {\n error(path, `Not read: \"${slug}\" is not a slug. Use lowercase letters, digits and single hyphens.`);\n continue;\n }\n const index = directory ? await readText(files, `${path}/${INDEX_FILE}`) : \"\";\n if (index === null || index instanceof Error) {\n error(path, `Not read: ${index?.message ?? `the folder has no ${INDEX_FILE}.`}`);\n continue;\n }\n found.push(slug);\n }\n return found;\n}\n\n/** The file's text, null when it is not there, or the error that reading it gave. */\nasync function readText(files: ContentSource, path: string): Promise<string | null | Error> {\n try {\n return await files.read(path);\n } catch (cause) {\n return cause as Error;\n }\n}\n\nfunction checkOrder(text: string | null | Error, path: string, error: (path: string, message: string) => void): void {\n if (text === null) return;\n if (text instanceof Error) return error(path, text.message);\n let order: unknown;\n try {\n order = JSON.parse(text);\n } catch {\n return error(path, \"Not valid JSON. The build fails on it.\");\n }\n if (!Array.isArray(order)) {\n error(path, 'Must be a JSON array of slugs, such as [\"second\", \"first\"]. The build fails on it.');\n }\n}\n\nasync function readOrReport<T>(\n read: () => Promise<T>,\n path: string,\n error: (path: string, message: string) => void,\n): Promise<T | undefined> {\n try {\n return await read();\n } catch (cause) {\n error(path, (cause as Error).message.replace(`${path}: `, \"\"));\n return undefined;\n }\n}\n\nasync function checkEntry(\n context: EntryContext,\n fields: FieldMap,\n data: Record<string, unknown>,\n dataPath: string,\n base: string | null,\n): Promise<void> {\n const { root, files, slugs, error, warning } = context;\n\n const result = validateFields(fields, data, \"storage\");\n if (!result.ok) {\n for (const issue of result.issues) error(dataPath, issue.path ? `${issue.path}: ${issue.message}` : issue.message);\n }\n\n const raw = JSON.parse((await files.read(dataPath))!) as Record<string, unknown>;\n for (const message of unknownKeys(fields, raw, [])) warning(dataPath, message);\n\n visit(fields, data, [], (field, value, path) => {\n const key = path.join(\".\");\n if (field.kind === \"media\") {\n for (const file of list(value)) {\n let exists = false;\n try {\n exists = existsSync(within(root, file));\n } catch {\n // A path outside the content root does not exist for Mica.\n }\n if (!exists) error(dataPath, `${key}: the file \"${file}\" does not exist under the content root.`);\n }\n }\n if (field.kind === \"relation\") {\n for (const slug of list(value)) {\n if (!slugs.get(field.collection)?.has(slug)) {\n error(dataPath, `${key}: there is no \"${slug}\" in \"${field.collection}\".`);\n }\n }\n }\n });\n\n if (base === null) return;\n for (const item of await files.list(base)) {\n if (item.type !== \"file\" || !item.name.endsWith(DOCUMENT_EXTENSION)) continue;\n const path = `${base}/${item.name}`;\n if (!namesRichText(fields, documentPath(item.name))) {\n warning(path, \"Not read: no rich-text field has this name.\");\n continue;\n }\n for (const message of markdocWarnings((await files.read(path)) ?? \"\")) warning(path, message);\n }\n}\n\n/**\n * What Markdoc renders differently from Markdown, found in the text outside\n * code. A scan, not a parse: it can warn about too much, never about too little.\n */\nexport function markdocWarnings(source: string): string[] {\n const prose = source.replace(/^(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?^\\1[ \\t]*$/gm, \"\").replace(/`[^`\\n]*`/g, \"\");\n const messages: string[] = [];\n const html = [...new Set([...prose.matchAll(/<\\/?([a-zA-Z][a-zA-Z0-9-]*)(?=[\\s/>])[^>\\n]*>/g)].map((match) => match[1]!.toLowerCase()))];\n if (html.length > 0) {\n messages.push(`Raw HTML (${html.map((tag) => `<${tag}>`).join(\", \")}) is not rendered: Markdoc escapes it.`);\n }\n const tags = [...new Set([...prose.matchAll(/\\{%\\s*([a-zA-Z][\\w-]*)/g)].map((match) => match[1]!))];\n if (tags.length > 0) {\n messages.push(\n `Markdoc tags (${tags.join(\", \")}) render only if the website declares them in renderRichText(source, { config }).`,\n );\n }\n if (/\\[\\^[^\\]]+\\]/.test(prose)) messages.push(\"Footnotes ([^1]) are not Markdoc: they render as plain text.\");\n return messages;\n}\n\nfunction list(value: unknown): string[] {\n const values = Array.isArray(value) ? value : [value];\n return values.filter((item): item is string => typeof item === \"string\" && item !== \"\");\n}\n\n/** Calls `onField` for every field with a value, nested fields included. */\nfunction visit(\n fields: FieldMap,\n data: Record<string, unknown>,\n path: PathSegment[],\n onField: (field: AnyField, value: unknown, path: PathSegment[]) => void,\n): void {\n for (const [key, field] of Object.entries(fields)) {\n const value = data[key];\n if (value === undefined || value === null) continue;\n onField(field, value, [...path, key]);\n if (field.kind === \"object\" && isRecord(value)) visit(field.fields, value, [...path, key], onField);\n if (field.kind === \"repeater\" && Array.isArray(value)) {\n value.forEach((row, index) => {\n if (isRecord(row)) visit(field.fields, row, [...path, key, index], onField);\n });\n }\n }\n}\n\n/** Keys in the stored JSON that the model does not have; Mica drops them on the next save. */\nfunction unknownKeys(fields: FieldMap, raw: Record<string, unknown>, path: PathSegment[]): string[] {\n const messages: string[] = [];\n for (const [key, value] of Object.entries(raw)) {\n const at = [...path, key].join(\".\");\n const field = fields[key];\n if (!field) {\n messages.push(`${at}: the model has no such field. Mica drops it on the next save.`);\n } else if (field.kind === \"richText\") {\n messages.push(`${at}: rich text belongs in ${[...path, key].join(\".\")}${DOCUMENT_EXTENSION}, not in the JSON.`);\n } else if (field.kind === \"object\" && isRecord(value)) {\n messages.push(...unknownKeys(field.fields, value, [...path, key]));\n } else if (field.kind === \"repeater\" && Array.isArray(value)) {\n value.forEach((row, index) => {\n if (isRecord(row)) messages.push(...unknownKeys(field.fields, row, [...path, key, index]));\n });\n }\n }\n return messages;\n}\n\n/** Whether a sidecar's dotted path leads to a rich-text field. */\nfunction namesRichText(fields: FieldMap, path: PathSegment[]): boolean {\n let map: FieldMap | undefined = fields;\n for (const [index, segment] of path.entries()) {\n if (typeof segment === \"number\") continue;\n const field: AnyField | undefined = map?.[segment];\n if (!field) return false;\n if (index === path.length - 1) return field.kind === \"richText\";\n map = field.kind === \"object\" || field.kind === \"repeater\" ? field.fields : undefined;\n }\n return false;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/** The report as a person or an agent reads it. */\nexport function formatReport(report: CheckReport, root: string): string {\n const errors = report.problems.filter((problem) => problem.level === \"error\").length;\n const warnings = report.problems.length - errors;\n const lines = [\n report.model\n ? `Checked ${report.entries} ${report.entries === 1 ? \"entry\" : \"entries\"} in ${report.collections} ` +\n `${report.collections === 1 ? \"collection\" : \"collections\"} and ${report.singletons} ` +\n `${report.singletons === 1 ? \"singleton\" : \"singletons\"} in ${relative(process.cwd(), root) || \".\"}.`\n : `Could not read the content model in ${relative(process.cwd(), root) || \".\"}, so no entry was checked.`,\n \"\",\n ];\n for (const problem of report.problems) {\n lines.push(` ${problem.level === \"error\" ? \"error \" : \"warning\"} ${problem.path}`);\n for (const line of problem.message.split(\"\\n\")) lines.push(` ${line.trim()}`);\n }\n if (report.problems.length > 0) lines.push(\"\");\n lines.push(`${errors} ${errors === 1 ? \"error\" : \"errors\"}, ${warnings} ${warnings === 1 ? \"warning\" : \"warnings\"}.`);\n return `${lines.join(\"\\n\")}\\n`;\n}\n"]}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AASH,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAsBD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAgD7C;AAkBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAGnF;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAoD1C"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAYH,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA0BD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAgD7C;AAkBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAGnF;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAyD1C"}
package/dist/cli.js CHANGED
@@ -30,11 +30,17 @@ import path from "node:path";
30
30
  import { pathToFileURL } from "node:url";
31
31
  import { MANIFEST_PATH } from "@micacms/core";
32
32
  import { DEFAULT_PORT, VERSION } from "@micacms/runtime";
33
+ import { checkContent, formatReport } from "./check.js";
34
+ import { formatInit, initProject, InitError } from "./init.js";
33
35
  const USAGE = `
34
36
  mica — edit your Astro project's content, on this machine
35
37
 
36
38
  Usage
37
- $ npx @micacms/editor [content directory]
39
+ $ npx @micacms/editor [content directory] open the editor
40
+ $ npx @micacms/editor init add Mica to this Astro project
41
+ $ npx @micacms/editor check [content dir] check every entry against the model
42
+
43
+ With @micacms/editor installed in the project, \`npx mica\` is the same command.
38
44
 
39
45
  Options
40
46
  -c, --content <dir> The content root (default: ./content)
@@ -47,7 +53,7 @@ const USAGE = `
47
53
  There is no sign-in here, because there is nothing to sign in to: this mode
48
54
  has no identity provider, no account and no user. That is right for your own
49
55
  machine and wrong everywhere else — a Mica other people reach runs as a
50
- container with an identity provider. See docs/self-hosting.md.
56
+ container with an identity provider. See https://micacms.com/docs/self-hosting/.
51
57
  `;
52
58
  export function parse(argv) {
53
59
  const options = { content: "./content" };
@@ -121,6 +127,11 @@ export function shouldOpenBrowser(options, env) {
121
127
  return Boolean(process.stdout.isTTY) && !env["CI"];
122
128
  }
123
129
  export async function main() {
130
+ const [command, ...rest] = process.argv.slice(2);
131
+ if (command === "init")
132
+ return init(rest);
133
+ if (command === "check")
134
+ return check(rest);
124
135
  const options = parse(process.argv.slice(2));
125
136
  const content = path.resolve(options.content);
126
137
  if (!existsSync(content)) {
@@ -142,9 +153,10 @@ export async function main() {
142
153
  "Mica ships without one: your Astro project defines the model, and building",
143
154
  "the project writes it there. Run this in the project, then start Mica again:",
144
155
  "",
145
- " npx astro build # or start the dev server once",
156
+ " npx astro sync # or build, or start the dev server once",
146
157
  "",
147
- "If you have not defined a model yet, docs/getting-started.md is five minutes.",
158
+ "If you have not defined a model yet, run `npx @micacms/editor init`, or see",
159
+ "https://micacms.com/docs/getting-started/.",
148
160
  ]);
149
161
  process.exit(78); // EX_CONFIG
150
162
  }
@@ -168,8 +180,29 @@ export async function main() {
168
180
  // handler, and a stack trace that points at the real thing.
169
181
  await import("./index.js");
170
182
  }
171
- // Only when this file *is* the command. Importing it — which the tests do, and
172
- // which a future `mica init` would — must not start a server.
183
+ function init(argv) {
184
+ if (argv.length > 0)
185
+ fail(`init takes no options; it works in the current directory.`);
186
+ try {
187
+ process.stdout.write(formatInit(initProject(process.cwd())));
188
+ }
189
+ catch (error) {
190
+ if (!(error instanceof InitError))
191
+ throw error;
192
+ report(error.message, []);
193
+ process.exit(64); // EX_USAGE
194
+ }
195
+ }
196
+ async function check(argv) {
197
+ const { content } = parse(argv);
198
+ const root = path.resolve(content);
199
+ const result = await checkContent(root);
200
+ process.stdout.write(formatReport(result, root));
201
+ if (result.problems.some((problem) => problem.level === "error"))
202
+ process.exit(1);
203
+ }
204
+ // Only when this file *is* the command. Importing it, as the tests do, must
205
+ // not start a server.
173
206
  //
174
207
  // `realpathSync` matters: npm and npx run this file through a symlink in
175
208
  // `node_modules/.bin`, and Node resolves `import.meta.url` to the symlink's
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * `npx @micacms/editor` — Mica against a project on this machine.\n *\n * The point of this file is that adopting Mica should not mean adopting this\n * repository. Everything it does is set environment variables and hand over to\n * the same entry point a container runs, so there is one server with one\n * configuration path and this is a front door rather than a second one.\n *\n * Two defaults differ from a container's, and both are deliberate:\n *\n * - it binds to loopback. A container binds 0.0.0.0 because it has to; a\n * laptop does not, and this editor has no authentication. Anything\n * reachable by anyone else runs @micacms/self-hosted with an identity\n * provider — see docs/self-hosting.md.\n * - it talks to a person instead of to a log collector: one short banner\n * saying where the editor is and what happens when you save, and the\n * browser opened on it. `MICA_CLI` is how the server is told which of the\n * two audiences it has.\n *\n * There is no state to keep anywhere: the local editor has no database, no\n * account and no session, so nothing is written outside the content\n * directory.\n *\n * Explicit environment wins over every flag default here, and a flag wins over\n * a `.env` file, because `loadConfig` leaves already-set variables alone.\n */\n\nimport { existsSync, realpathSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { MANIFEST_PATH } from \"@micacms/core\";\nimport { DEFAULT_PORT, VERSION } from \"@micacms/runtime\";\n\nimport { checkContent, formatReport } from \"./check.js\";\nimport { formatInit, initProject, InitError } from \"./init.js\";\n\nexport interface Options {\n content: string;\n port?: string;\n host?: string;\n /** Undefined means \"decide from the terminal\" — see `shouldOpenBrowser`. */\n open?: boolean;\n}\n\nconst USAGE = `\n mica — edit your Astro project's content, on this machine\n\n Usage\n $ npx @micacms/editor [content directory] open the editor\n $ npx @micacms/editor init add Mica to this Astro project\n $ npx @micacms/editor check [content dir] check every entry against the model\n\n With @micacms/editor installed in the project, \\`npx mica\\` is the same command.\n\n Options\n -c, --content <dir> The content root (default: ./content)\n -p, --port <number> Port to listen on (default: ${DEFAULT_PORT})\n --host <address> Address to bind (default: 127.0.0.1)\n --no-open Do not open the browser\n -h, --help Show this\n -v, --version Print the version\n\n There is no sign-in here, because there is nothing to sign in to: this mode\n has no identity provider, no account and no user. That is right for your own\n machine and wrong everywhere else — a Mica other people reach runs as a\n container with an identity provider. See https://micacms.com/docs/self-hosting/.\n`;\n\nexport function parse(argv: string[]): Options {\n const options: Options = { content: \"./content\" };\n\n for (let index = 0; index < argv.length; index++) {\n const flag = argv[index]!;\n const value = (): string => {\n const next = argv[++index];\n if (next === undefined || next.startsWith(\"-\")) fail(`${flag} needs a value.`);\n return next!;\n };\n\n switch (flag) {\n case \"-c\":\n case \"--content\":\n options.content = value();\n break;\n case \"-p\":\n case \"--port\":\n options.port = value();\n break;\n case \"--host\":\n options.host = value();\n break;\n case \"--open\":\n options.open = true;\n break;\n case \"--no-open\":\n options.open = false;\n break;\n case \"-h\":\n case \"--help\":\n process.stdout.write(`${USAGE}\\n`);\n process.exit(0);\n\n case \"-v\":\n case \"--version\":\n process.stdout.write(`${VERSION}\\n`);\n process.exit(0);\n\n default:\n // A bare path is the content root, so `npx @micacms/editor ./site/content`\n // does what it looks like it does.\n if (!flag.startsWith(\"-\")) options.content = flag;\n else fail(`Unknown option \"${flag}\".`);\n }\n }\n\n return options;\n}\n\n/**\n * A problem the person can fix, said the way a person reads.\n *\n * The title is the fact, the lines under it are what to do about it. Anything\n * printed here has already decided the command is over: the caller exits.\n */\nfunction report(title: string, lines: string[]): void {\n const body = lines.map((line) => (line ? ` ${line}` : \"\")).join(\"\\n\");\n process.stderr.write(`\\n ${title}\\n\\n${body}\\n\\n`);\n}\n\nfunction fail(message: string): never {\n report(message, [\"Run `npx @micacms/editor --help` to see every option.\"]);\n process.exit(64); // EX_USAGE\n}\n\n/**\n * Opening a browser is a courtesy, so it is offered only where it can be\n * welcome: an interactive terminal that somebody is sitting at. A script, a\n * container and a CI job all get a server and no surprises.\n */\nexport function shouldOpenBrowser(options: Options, env: NodeJS.ProcessEnv): boolean {\n if (options.open !== undefined) return options.open;\n return Boolean(process.stdout.isTTY) && !env[\"CI\"];\n}\n\nexport async function main(): Promise<void> {\n const [command, ...rest] = process.argv.slice(2);\n if (command === \"init\") return init(rest);\n if (command === \"check\") return check(rest);\n\n const options = parse(process.argv.slice(2));\n const content = path.resolve(options.content);\n\n if (!existsSync(content)) {\n report(`There is no content directory at ${content}.`, [\n \"Mica edits the directory your Astro project publishes content into —\",\n \"the `root` in your `defineSite({ root: \\\"./content\\\" })`.\",\n \"\",\n \" npx @micacms/editor ./path/to/content\",\n \"\",\n \"If this is a new project, create the directory and Mica writes the first\",\n \"entry into it.\",\n ]);\n process.exit(64); // EX_USAGE\n }\n\n // A far more useful message than the server's, because here we know the\n // project is on this machine and can name the command that fixes it.\n if (!existsSync(path.join(content, MANIFEST_PATH))) {\n report(`There is no content model at ${path.join(content, MANIFEST_PATH)}.`, [\n \"Mica ships without one: your Astro project defines the model, and building\",\n \"the project writes it there. Run this in the project, then start Mica again:\",\n \"\",\n \" npx astro sync # or build, or start the dev server once\",\n \"\",\n \"If you have not defined a model yet, run `npx @micacms/editor init`, or see\",\n \"https://micacms.com/docs/getting-started/.\",\n ]);\n process.exit(78); // EX_CONFIG\n }\n\n const defaults: Record<string, string> = {\n CONTENT_ROOT: content,\n HOST: options.host ?? \"127.0.0.1\",\n PORT: options.port ?? String(DEFAULT_PORT),\n // Tells the server it is talking to a person: one banner instead of the\n // startup log lines a collector wants.\n MICA_CLI: \"true\",\n MICA_OPEN: String(shouldOpenBrowser(options, process.env)),\n };\n\n for (const [key, value] of Object.entries(defaults)) {\n process.env[key] ??= value;\n }\n // MICA_PUBLIC_URL has to agree with the port, and only matters here for\n // cookie scope and the Origin check — both of which are this process's own\n // address.\n process.env[\"MICA_PUBLIC_URL\"] ??= `http://localhost:${process.env[\"PORT\"]}`;\n\n // The server is imported rather than spawned: one process, one signal\n // handler, and a stack trace that points at the real thing.\n await import(\"./index.js\");\n}\n\nfunction init(argv: string[]): void {\n if (argv.length > 0) fail(`init takes no options; it works in the current directory.`);\n try {\n process.stdout.write(formatInit(initProject(process.cwd())));\n } catch (error) {\n if (!(error instanceof InitError)) throw error;\n report(error.message, []);\n process.exit(64); // EX_USAGE\n }\n}\n\nasync function check(argv: string[]): Promise<void> {\n const { content } = parse(argv);\n const root = path.resolve(content);\n const result = await checkContent(root);\n process.stdout.write(formatReport(result, root));\n if (result.problems.some((problem) => problem.level === \"error\")) process.exit(1);\n}\n\n// Only when this file *is* the command. Importing it, as the tests do, must\n// not start a server.\n//\n// `realpathSync` matters: npm and npx run this file through a symlink in\n// `node_modules/.bin`, and Node resolves `import.meta.url` to the symlink's\n// real target. Comparing against the raw `argv[1]` never matched there, so\n// `npx @micacms/editor` silently did nothing.\nif (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {\n main().catch((error: unknown) => {\n process.stderr.write(`\\n${error instanceof Error ? error.stack : String(error)}\\n`);\n process.exit(1);\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,IAAI,EACJ,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,QAAQ,EACR,UAAU,EACV,aAAa,GAEd,MAAM,kBAAkB,CAAC;AAE1B,MAAM,EACJ,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,SAAS,GACV,GAAG,UAAU,CAAC;AAEf,MAAM,MAAM,GAAG,CAAC;KACb,MAAM,CAAC;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,IAAI;IACJ,eAAe;IACf,YAAY;IACZ,kBAAkB;IAClB,gBAAgB;IAChB,aAAa;IACb,SAAS;IAET;;;OAGG;IACH,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC;IACrB,0EAA0E;IAC1E,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;CACvB,CAAC;KACD,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnB,GAAG,aAAa,CAAC;QACf,GAAG,GAAG;QACN,4DAA4D;QAC5D,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,WAAW;CAC9B,CAAC,CAAC,CAAC;AAIN,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,2EAA2E;IAC3E,sEAAsE;IACtE,IAAI,GAAG,KAAK,OAAO,CAAC,GAAG;QAAE,WAAW,EAAE,CAAC;IAEvC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,MAAM,IAAI,WAAW,CAAC,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;IAE3B,8FAA8F;IAC9F,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,oEAAoE;YAClE,0EAA0E;YAC1E,4EAA4E;YAC5E,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC;IACtD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,0EAA0E,IAAI,MAAM;YAClF,4EAA4E;YAC5E,6DAA6D,CAChE,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,oCAAoC;IACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,WAAW,CACnB,wDAAwD,MAAM,CAAC,IAAI,MAAM;YACvE,4EAA4E;YAC5E,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,IAAI,EACJ,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,QAAQ,EACR,UAAU,EACV,aAAa,GAEd,MAAM,kBAAkB,CAAC;AAE1B,MAAM,EACJ,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,SAAS,GACV,GAAG,UAAU,CAAC;AAEf,MAAM,MAAM,GAAG,CAAC;KACb,MAAM,CAAC;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,IAAI;IACJ,eAAe;IACf,YAAY;IACZ,kBAAkB;IAClB,gBAAgB;IAChB,aAAa;IACb,SAAS;IAET;;;OAGG;IACH,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC;IACrB,0EAA0E;IAC1E,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;CACvB,CAAC;KACD,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnB,GAAG,aAAa,CAAC;QACf,GAAG,GAAG;QACN,4DAA4D;QAC5D,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,WAAW;CAC9B,CAAC,CAAC,CAAC;AAIN,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,2EAA2E;IAC3E,sEAAsE;IACtE,IAAI,GAAG,KAAK,OAAO,CAAC,GAAG;QAAE,WAAW,EAAE,CAAC;IAEvC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,MAAM,IAAI,WAAW,CAAC,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;IAE3B,8FAA8F;IAC9F,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,oEAAoE;YAClE,0EAA0E;YAC1E,4EAA4E;YAC5E,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC;IACtD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,0EAA0E,IAAI,MAAM;YAClF,4EAA4E;YAC5E,6DAA6D,CAChE,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,oCAAoC;IACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,WAAW,CACnB,wDAAwD,MAAM,CAAC,IAAI,MAAM;YACvE,4EAA4E;YAC5E,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * The local editor's configuration: the runtime variables minus everything that\n * describes a deployment. The server has no authentication, so it refuses\n * production and any non-loopback URL or address, with no override.\n */\n\nimport { z } from \"zod\";\n\nimport {\n bool,\n ConfigError,\n deriveRuntime,\n describeConfigIssues,\n loadEnvFile,\n LOOPBACK,\n runtimeEnv,\n withoutBlanks,\n type RuntimeConfig,\n} from \"@micacms/runtime\";\n\nconst {\n NODE_ENV,\n LOG_LEVEL,\n HOST,\n PORT,\n MICA_PUBLIC_URL,\n CONTENT_ROOT,\n MICA_LOCAL_PUBLISH,\n MAX_UPLOAD_BYTES,\n ADMIN_ORIGINS,\n ADMIN_DIR,\n} = runtimeEnv;\n\nconst schema = z\n .object({\n NODE_ENV,\n LOG_LEVEL,\n HOST,\n PORT,\n MICA_PUBLIC_URL,\n CONTENT_ROOT,\n MICA_LOCAL_PUBLISH,\n MAX_UPLOAD_BYTES,\n ADMIN_ORIGINS,\n ADMIN_DIR,\n\n /**\n * Set only by the `mica` bin: a terminal gets one banner, a log collector\n * gets structured lines.\n */\n MICA_CLI: bool(false),\n /** Whether that front door should open a browser once it is listening. */\n MICA_OPEN: bool(false),\n })\n .transform((env) => ({\n ...deriveRuntime({\n ...env,\n // There is no proxy in front of a loopback server to trust.\n TRUST_PROXY: false,\n }),\n HOST: env.HOST ?? \"127.0.0.1\",\n }));\n\nexport type LocalConfig = RuntimeConfig & { MICA_CLI: boolean; MICA_OPEN: boolean };\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): LocalConfig {\n // Only when reading the ambient environment: tests pass an explicit object\n // and must not pick up whatever `.env` sits in the working directory.\n if (env === process.env) loadEnvFile();\n\n const parsed = schema.safeParse(withoutBlanks(env));\n if (!parsed.success) throw new ConfigError(describeConfigIssues(parsed.error));\n\n const config = parsed.data;\n\n // The local editor has no authentication, so it must never be reachable from another machine.\n if (config.isProduction) {\n throw new ConfigError(\n \"The local editor has no authentication, and NODE_ENV=production.\\n\" +\n \" There is no identity provider and no user: every request is allowed.\\n\" +\n \" A deployment runs @micacms/self-hosted with an OIDC provider instead —\\n\" +\n \" see docs/self-hosting.md.\",\n );\n }\n\n const host = new URL(config.MICA_PUBLIC_URL).hostname;\n if (!LOOPBACK.has(host)) {\n throw new ConfigError(\n `The local editor has no authentication, but MICA_PUBLIC_URL points at \"${host}\".\\n` +\n \" It is for a developer's own machine only. Anything reachable by anyone\\n\" +\n \" else runs @micacms/self-hosted with an identity provider.\",\n );\n }\n\n // A `localhost` URL with a socket on `0.0.0.0` opens the editor to the whole\n // network. No override, on purpose.\n if (!LOOPBACK.has(config.HOST)) {\n throw new ConfigError(\n `The local editor has no authentication, but HOST is \"${config.HOST}\".\\n` +\n \" It binds to the loopback address and nothing else. Use HOST=127.0.0.1,\\n\" +\n \" or run @micacms/self-hosted with an identity provider.\",\n );\n }\n\n return config;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAEpE,kEAAkE;AAClE,SAAS,UAAU,CAAC,MAAc;IAChC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAExC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACjD,OAAO,CAAC,CAAC,IAAI,CACX,kEAAkE;gBAChE,IAAI,IAAI,kBAAkB;gBAC1B,0EAA0E;gBAC1E,4EAA4E,EAC9E,GAAG,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAEpE,kEAAkE;AAClE,SAAS,UAAU,CAAC,MAAc;IAChC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAExC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACjD,OAAO,CAAC,CAAC,IAAI,CACX,kEAAkE;gBAChE,IAAI,IAAI,kBAAkB;gBAC1B,0EAA0E;gBAC1E,4EAA4E,EAC9E,GAAG,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The Host allowlist.\n *\n * Under DNS rebinding, a page re-resolves its own name to `127.0.0.1` and the\n * browser then treats requests to it as same-origin. Writes still fail on the\n * Origin check; *reads* would succeed. Local mode keeps no session or token,\n * so this server answers only to names that are this machine. It applies in\n * front of everything, the health endpoints included.\n */\n\nimport type { MiddlewareHandler } from \"hono\";\n\nimport type { Env } from \"@micacms/runtime\";\n\nconst ALLOWED = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\n\n/** The hostname part of a Host header, brackets kept for IPv6. */\nfunction hostnameOf(header: string): string {\n if (header.startsWith(\"[\")) {\n const end = header.indexOf(\"]\");\n return end < 0 ? header : header.slice(0, end + 1);\n }\n return header.split(\":\")[0] ?? \"\";\n}\n\nexport function hostGuard(): MiddlewareHandler<Env> {\n return async (c, next) => {\n const host = c.req.header(\"host\") ?? \"\";\n\n if (!ALLOWED.has(hostnameOf(host).toLowerCase())) {\n return c.text(\n \"This is a local Mica editor. It answers only to this machine —\\n\" +\n `\"${host}\" is not it.\\n\\n` +\n \"To let other people edit, deploy @micacms/self-hosted with an identity\\n\" +\n \"provider: https://github.com/micacms/mica/blob/main/docs/self-hosting.md\\n\",\n 403,\n );\n }\n\n await next();\n };\n}\n"]}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,OAAO,GACR,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,KAAK,UAAU,IAAI;IACjB,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,UAAU,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY;QAChC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvE,2EAA2E;IAC3E,sCAAsC;IACtC,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAEzD,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY;QAChC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;IAE3D,MAAM,GAAG,GAAG,oBAAoB,CAAC;QAC/B,MAAM;QACN,KAAK;QACL,KAAK;QACL,cAAc;QACd,MAAM;QACN,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;YAClB,IAAI,EAAE,wDAAwD;SAC/D,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE;QACzB,MAAM,EAAE,CAAC,SAAS,EAAE,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC,SAAS,IAAI,YAAY,EAAE;KAC7C,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,MAAM,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAEnD,WAAW,CAAC;QACV,GAAG;QACH,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM;QAEN,WAAW;YACT,gFAAgF;YAChF,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACpB,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;gBAE5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,aAAa,CACX;oBACE,OAAO,EAAE,OAAO;oBAChB,GAAG,EAAE,MAAM,CAAC,eAAe;oBAC3B,WAAW,EAAE,KAAK,CAAC,MAAM;oBACzB,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,MAAM;oBAClD,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,MAAM;iBACjD,EACD,QAAQ,CACT,CACF,CAAC;gBAEF,IAAI,MAAM,CAAC,SAAS;oBAAE,aAAa,CAAC,GAAG,MAAM,CAAC,eAAe,SAAS,CAAC,CAAC;gBACxE,OAAO;YACT,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE;gBACvC,GAAG,EAAE,MAAM,CAAC,eAAe;gBAC3B,SAAS,EAAE,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE;gBAC1C,IAAI,EAAE,oCAAoC;gBAC1C,OAAO,EAAE,KAAK,CAAC,MAAM;gBACrB,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC;gBAC3B,iBAAiB,EAAE,WAAW;gBAC9B,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,MAAM;aACnD,CAAC,CAAC;QACL,CAAC;QAED,gBAAgB,EAAE,GAAG,EAAE,CACrB,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAEjF,QAAQ,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE;KAC/B,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The local editor's process entry point: the developer's checkout, the\n * auth-neutral runtime and the Host allowlist. It writes nothing outside the\n * content directory and installs no auth code that can be misconfigured.\n */\n\nimport { MANIFEST_PATH } from \"@micacms/core\";\nimport {\n ConfigError,\n contentFiles,\n createApp,\n createLogger,\n createRuntimeContext,\n loadSchema,\n SchemaError,\n schemaFingerprint,\n schemaOrigin,\n startServer,\n VERSION,\n} from \"@micacms/runtime\";\n\nimport { editorUiDist } from \"./admin-dir.js\";\nimport { portInUse, startupBanner, supportsColor } from \"./banner.js\";\nimport { loadConfig } from \"./config.js\";\nimport { hostGuard } from \"./guards.js\";\nimport { openInBrowser } from \"./open.js\";\n\nasync function main(): Promise<void> {\n let config;\n try {\n config = loadConfig();\n } catch (error) {\n if (error instanceof ConfigError) {\n process.stderr.write(`\\n${error.message}\\n\\n`);\n process.exit(78); // EX_CONFIG\n }\n throw error;\n }\n\n const logger = createLogger({ level: config.LOG_LEVEL, pretty: true });\n\n // A bad or absent model is a project mistake, not a crash: exit like a bad\n // environment, without a stack trace.\n const files = contentFiles({ root: config.contentRoot });\n\n let model;\n try {\n model = await loadSchema(files);\n } catch (error) {\n if (error instanceof SchemaError) {\n process.stderr.write(`\\n${error.message}\\n\\n`);\n process.exit(78); // EX_CONFIG\n }\n throw error;\n }\n\n const schemaRevision = await files.revision(MANIFEST_PATH);\n\n const ctx = createRuntimeContext({\n config,\n model,\n files,\n schemaRevision,\n logger,\n operational: () => ({\n auth: \"none — the local editor has no accounts and no sign-in\",\n }),\n });\n\n const app = createApp(ctx, {\n guards: [hostGuard()],\n adminDir: config.ADMIN_DIR ?? editorUiDist(),\n });\n\n const fingerprint = await schemaFingerprint(model);\n\n startServer({\n app,\n host: config.HOST,\n port: config.PORT,\n logger,\n\n onListening() {\n // A person at a terminal gets a short banner; a log collector gets every field.\n if (config.MICA_CLI) {\n const useColor = supportsColor(process.stdout, process.env);\n\n process.stdout.write(\n startupBanner(\n {\n version: VERSION,\n url: config.MICA_PUBLIC_URL,\n contentRoot: files.origin,\n collections: Object.keys(model.collections).length,\n singletons: Object.keys(model.singletons).length,\n },\n useColor,\n ),\n );\n\n if (config.MICA_OPEN) openInBrowser(`${config.MICA_PUBLIC_URL}/admin/`);\n return;\n }\n\n logger.info(\"Mica local editor started\", {\n url: config.MICA_PUBLIC_URL,\n listening: `${config.HOST}:${config.PORT}`,\n auth: \"none (local editor; loopback only)\",\n content: files.origin,\n schema: schemaOrigin(files),\n schemaFingerprint: fingerprint,\n collections: Object.keys(model.collections).length,\n });\n },\n\n portInUseMessage: () =>\n portInUse(config.PORT, config.HOST, supportsColor(process.stderr, process.env)),\n\n shutdown: () => ctx.shutdown(),\n });\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(`Failed to start: ${error instanceof Error ? error.stack : String(error)}\\n`);\n process.exit(1);\n});\n"]}
package/dist/init.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `mica init` — adds Mica to an Astro project: a starter model and the content
3
+ * collections. It never overwrites a file.
4
+ */
5
+ export declare const MODEL = "import {\n collection,\n date,\n defineSite,\n media,\n richText,\n singleton,\n slug,\n text,\n} from \"@micacms/core\";\n\nexport const site = defineSite({\n root: \"./content\",\n\n collections: {\n news: collection({\n label: \"News\",\n labelSingular: \"Article\",\n fields: {\n title: text({ label: \"Title\", required: true, maxLength: 120, listed: true }),\n slug: slug({ from: \"title\" }),\n publishedOn: date({ label: \"Published on\", listed: true }),\n cover: media({ accept: \"image\" }),\n body: richText(),\n },\n }),\n },\n\n singletons: {\n settings: singleton({\n label: \"Website\",\n fields: {\n siteName: text({ label: \"Site name\", required: true }),\n tagline: text({ label: \"Tagline\" }),\n },\n }),\n },\n});\n";
6
+ export interface InitResult {
7
+ /** Files and directories made, relative to the project. */
8
+ created: string[];
9
+ /** Files that already existed and were left alone. */
10
+ kept: string[];
11
+ /** What the person or agent does next. */
12
+ next: string[];
13
+ }
14
+ export declare class InitError extends Error {
15
+ name: string;
16
+ }
17
+ /** Adds Mica to the Astro project in `project`. */
18
+ export declare function initProject(project: string): InitResult;
19
+ /** The result as a person or an agent reads it. */
20
+ export declare function formatInit(result: InitResult): string;
21
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,eAAO,MAAM,KAAK,o1BAsCjB,CAAC;AAcF,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sDAAsD;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0CAA0C;IAC1C,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,qBAAa,SAAU,SAAQ,KAAK;IACzB,IAAI,SAAe;CAC7B;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAqDvD;AAYD,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAOrD"}
package/dist/init.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * `mica init` — adds Mica to an Astro project: a starter model and the content
3
+ * collections. It never overwrites a file.
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import path from "node:path";
7
+ export const MODEL = `import {
8
+ collection,
9
+ date,
10
+ defineSite,
11
+ media,
12
+ richText,
13
+ singleton,
14
+ slug,
15
+ text,
16
+ } from "@micacms/core";
17
+
18
+ export const site = defineSite({
19
+ root: "./content",
20
+
21
+ collections: {
22
+ news: collection({
23
+ label: "News",
24
+ labelSingular: "Article",
25
+ fields: {
26
+ title: text({ label: "Title", required: true, maxLength: 120, listed: true }),
27
+ slug: slug({ from: "title" }),
28
+ publishedOn: date({ label: "Published on", listed: true }),
29
+ cover: media({ accept: "image" }),
30
+ body: richText(),
31
+ },
32
+ }),
33
+ },
34
+
35
+ singletons: {
36
+ settings: singleton({
37
+ label: "Website",
38
+ fields: {
39
+ siteName: text({ label: "Site name", required: true }),
40
+ tagline: text({ label: "Tagline" }),
41
+ },
42
+ }),
43
+ },
44
+ });
45
+ `;
46
+ const COLLECTIONS = `import { micaCollections } from "@micacms/astro";
47
+
48
+ import { site } from "./mica";
49
+
50
+ export const collections = micaCollections(site);
51
+ `;
52
+ const MERGE = ` import { micaCollections } from "@micacms/astro";
53
+ import { site } from "./mica";
54
+
55
+ export const collections = { ...micaCollections(site), /* your collections */ };`;
56
+ export class InitError extends Error {
57
+ name = "InitError";
58
+ }
59
+ /** Adds Mica to the Astro project in `project`. */
60
+ export function initProject(project) {
61
+ const manifest = path.join(project, "package.json");
62
+ if (!existsSync(manifest)) {
63
+ throw new InitError(`There is no package.json in ${project}. Create an Astro project first: npm create astro@latest`);
64
+ }
65
+ let pkg;
66
+ try {
67
+ pkg = JSON.parse(readFileSync(manifest, "utf8"));
68
+ }
69
+ catch (cause) {
70
+ throw new InitError(`${manifest} is not valid JSON: ${cause.message}`);
71
+ }
72
+ const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };
73
+ if (!dependencies["astro"]) {
74
+ throw new InitError(`${manifest} has no "astro" dependency. Run mica init in an Astro project.`);
75
+ }
76
+ const result = { created: [], kept: [], next: [] };
77
+ const model = existingFile(project, ["src/mica.ts", "src/mica.mts", "src/mica.js", "src/mica.mjs"]);
78
+ if (model)
79
+ result.kept.push(model);
80
+ else
81
+ write(project, "src/mica.ts", MODEL, result);
82
+ const config = existingFile(project, [
83
+ "src/content.config.ts",
84
+ "src/content.config.mts",
85
+ "src/content.config.js",
86
+ "src/content.config.mjs",
87
+ "src/content/config.ts",
88
+ "src/content/config.mts",
89
+ "src/content/config.js",
90
+ "src/content/config.mjs",
91
+ ]);
92
+ if (config) {
93
+ result.kept.push(config);
94
+ if (!readFileSync(path.join(project, config), "utf8").includes("micaCollections")) {
95
+ result.next.push(`Add the Mica collections to ${config}:\n\n${MERGE}`);
96
+ }
97
+ }
98
+ else {
99
+ write(project, "src/content.config.ts", COLLECTIONS, result);
100
+ }
101
+ const missing = ["@micacms/astro", "@micacms/core"].filter((name) => !dependencies[name]);
102
+ if (missing.length > 0)
103
+ result.next.push(`Install the packages: npm install ${missing.join(" ")}`);
104
+ if (!dependencies["@micacms/editor"])
105
+ result.next.push("Install the editor: npm install --save-dev @micacms/editor");
106
+ result.next.push(model
107
+ ? "Add the website's collections and singletons to the model in src/mica.ts."
108
+ : "Replace the starter model in src/mica.ts (news, settings) with the website's model.", "Write the model to content/.mica/schema.json: npx astro sync", "Check the content: npx mica check", "When the content is in place, open the editor in a browser: npx mica");
109
+ return result;
110
+ }
111
+ function existingFile(project, candidates) {
112
+ return candidates.find((candidate) => existsSync(path.join(project, candidate)));
113
+ }
114
+ function write(project, file, contents, result) {
115
+ mkdirSync(path.dirname(path.join(project, file)), { recursive: true });
116
+ writeFileSync(path.join(project, file), contents, { flag: "wx" });
117
+ result.created.push(file);
118
+ }
119
+ /** The result as a person or an agent reads it. */
120
+ export function formatInit(result) {
121
+ const lines = [];
122
+ for (const file of result.created)
123
+ lines.push(` created ${file}`);
124
+ for (const file of result.kept)
125
+ lines.push(` kept ${file}`);
126
+ lines.push("", "Next:", "");
127
+ result.next.forEach((step, index) => lines.push(` ${index + 1}. ${step}`, ...(step.includes("\n") ? [""] : [])));
128
+ return `${lines.join("\n")}\n`;
129
+ }
130
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `mica init` — adds Mica to an Astro project: a starter model and the content\n * collections. It never overwrites a file.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nexport const MODEL = `import {\n collection,\n date,\n defineSite,\n media,\n richText,\n singleton,\n slug,\n text,\n} from \"@micacms/core\";\n\nexport const site = defineSite({\n root: \"./content\",\n\n collections: {\n news: collection({\n label: \"News\",\n labelSingular: \"Article\",\n fields: {\n title: text({ label: \"Title\", required: true, maxLength: 120, listed: true }),\n slug: slug({ from: \"title\" }),\n publishedOn: date({ label: \"Published on\", listed: true }),\n cover: media({ accept: \"image\" }),\n body: richText(),\n },\n }),\n },\n\n singletons: {\n settings: singleton({\n label: \"Website\",\n fields: {\n siteName: text({ label: \"Site name\", required: true }),\n tagline: text({ label: \"Tagline\" }),\n },\n }),\n },\n});\n`;\n\nconst COLLECTIONS = `import { micaCollections } from \"@micacms/astro\";\n\nimport { site } from \"./mica\";\n\nexport const collections = micaCollections(site);\n`;\n\nconst MERGE = ` import { micaCollections } from \"@micacms/astro\";\n import { site } from \"./mica\";\n\n export const collections = { ...micaCollections(site), /* your collections */ };`;\n\nexport interface InitResult {\n /** Files and directories made, relative to the project. */\n created: string[];\n /** Files that already existed and were left alone. */\n kept: string[];\n /** What the person or agent does next. */\n next: string[];\n}\n\nexport class InitError extends Error {\n override name = \"InitError\";\n}\n\n/** Adds Mica to the Astro project in `project`. */\nexport function initProject(project: string): InitResult {\n const manifest = path.join(project, \"package.json\");\n if (!existsSync(manifest)) {\n throw new InitError(`There is no package.json in ${project}. Create an Astro project first: npm create astro@latest`);\n }\n let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };\n try {\n pkg = JSON.parse(readFileSync(manifest, \"utf8\")) as typeof pkg;\n } catch (cause) {\n throw new InitError(`${manifest} is not valid JSON: ${(cause as Error).message}`);\n }\n const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };\n if (!dependencies[\"astro\"]) {\n throw new InitError(`${manifest} has no \"astro\" dependency. Run mica init in an Astro project.`);\n }\n\n const result: InitResult = { created: [], kept: [], next: [] };\n\n const model = existingFile(project, [\"src/mica.ts\", \"src/mica.mts\", \"src/mica.js\", \"src/mica.mjs\"]);\n if (model) result.kept.push(model);\n else write(project, \"src/mica.ts\", MODEL, result);\n\n const config = existingFile(project, [\n \"src/content.config.ts\",\n \"src/content.config.mts\",\n \"src/content.config.js\",\n \"src/content.config.mjs\",\n \"src/content/config.ts\",\n \"src/content/config.mts\",\n \"src/content/config.js\",\n \"src/content/config.mjs\",\n ]);\n if (config) {\n result.kept.push(config);\n if (!readFileSync(path.join(project, config), \"utf8\").includes(\"micaCollections\")) {\n result.next.push(`Add the Mica collections to ${config}:\\n\\n${MERGE}`);\n }\n } else {\n write(project, \"src/content.config.ts\", COLLECTIONS, result);\n }\n\n const missing = [\"@micacms/astro\", \"@micacms/core\"].filter((name) => !dependencies[name]);\n if (missing.length > 0) result.next.push(`Install the packages: npm install ${missing.join(\" \")}`);\n if (!dependencies[\"@micacms/editor\"]) result.next.push(\"Install the editor: npm install --save-dev @micacms/editor\");\n result.next.push(\n model\n ? \"Add the website's collections and singletons to the model in src/mica.ts.\"\n : \"Replace the starter model in src/mica.ts (news, settings) with the website's model.\",\n \"Write the model to content/.mica/schema.json: npx astro sync\",\n \"Check the content: npx mica check\",\n \"When the content is in place, open the editor in a browser: npx mica\",\n );\n return result;\n}\n\nfunction existingFile(project: string, candidates: string[]): string | undefined {\n return candidates.find((candidate) => existsSync(path.join(project, candidate)));\n}\n\nfunction write(project: string, file: string, contents: string, result: InitResult): void {\n mkdirSync(path.dirname(path.join(project, file)), { recursive: true });\n writeFileSync(path.join(project, file), contents, { flag: \"wx\" });\n result.created.push(file);\n}\n\n/** The result as a person or an agent reads it. */\nexport function formatInit(result: InitResult): string {\n const lines: string[] = [];\n for (const file of result.created) lines.push(` created ${file}`);\n for (const file of result.kept) lines.push(` kept ${file}`);\n lines.push(\"\", \"Next:\", \"\");\n result.next.forEach((step, index) => lines.push(` ${index + 1}. ${step}`, ...(step.includes(\"\\n\") ? [\"\"] : [])));\n return `${lines.join(\"\\n\")}\\n`;\n}\n"]}
package/dist/open.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"open.js","sourceRoot":"","sources":["../src/open.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GACnB,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC3B,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC5B,CAAC,CAAC,uEAAuE;gBACvE,2CAA2C;gBAC3C,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAE5B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAQ,EAAE,IAAgB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACrF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5B,2EAA2E;QAC3E,uCAAuC;QACvC,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"open.js","sourceRoot":"","sources":["../src/open.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GACnB,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC3B,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC5B,CAAC,CAAC,uEAAuE;gBACvE,2CAA2C;gBAC3C,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAE5B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAQ,EAAE,IAAgB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACrF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5B,2EAA2E;QAC3E,uCAAuC;QACvC,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC","sourcesContent":["/**\n * Opens a URL in whatever the desktop considers a browser.\n *\n * A courtesy, and treated as one: every failure is swallowed. A machine with no\n * desktop session — over SSH, in a container, in CI — has nothing to open, and\n * a server that refused to start because of it would be absurd.\n */\n\nimport { spawn } from \"node:child_process\";\n\nexport function openInBrowser(url: string): void {\n const [command, args] =\n process.platform === \"darwin\"\n ? [\"open\", [url]]\n : process.platform === \"win32\"\n ? // The empty string is `start`'s window title. Without it, a quoted URL\n // is taken as the title and nothing opens.\n [\"cmd\", [\"/c\", \"start\", \"\", url]]\n : [\"xdg-open\", [url]];\n\n try {\n const child = spawn(command!, args as string[], { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {});\n // Detached and unreferenced, so a browser that outlives Mica does not keep\n // the Node process alive after Ctrl+C.\n child.unref();\n } catch {\n // There is no browser here. That is not an error.\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micacms/editor",
3
- "version": "0.5.0-rc.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "The local Mica editor — `npx @micacms/editor` against a checkout on your own machine. No authentication, no database, loopback only.",
6
6
  "license": "MIT",
@@ -23,9 +23,9 @@
23
23
  "dependencies": {
24
24
  "hono": "^4.13.7",
25
25
  "zod": "^4.3.6",
26
- "@micacms/core": "0.5.0-rc.1",
27
- "@micacms/editor-ui": "0.5.0-rc.1",
28
- "@micacms/runtime": "0.5.0-rc.1"
26
+ "@micacms/core": "0.5.0",
27
+ "@micacms/editor-ui": "0.5.0",
28
+ "@micacms/runtime": "0.5.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.20.1",