mechanica 2.1.0-beta.7 → 2.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.
package/README.md CHANGED
@@ -359,10 +359,11 @@ These drive a locally installed Chrome or Edge (or `--browser <path>`) and need
359
359
  | `mechanica shot <blockId \| /path>` | Screenshot a block or a page. Flags: `--width`, `--data`, `--out`, `--full`, `--server`, `--browser` |
360
360
  | `mechanica thumbs [--blocks]` | Regenerate page or block thumbnails for the editor |
361
361
  | `mechanica images` | Generate image dimensions and blur-up previews for `.mech/assets` (needs `sharp`; `--force` redoes all) |
362
- | `mechanica login` | Sign in to the Mechanica platform and save an access token for this machine (`--host`, `--token`) |
362
+ | `mechanica login` | Sign in to the Mechanica platform: it prints a link to open and approve in the browser (signing in or registering on the way) and keeps the access token for this machine. In a terminal it waits for the approval; run from a coding agent, it prints the link and returns, and the next `link` or `push` finishes the sign-in (`--wait`, `--no-wait`, `--host`, or `--token` to store a token made in the dashboard) |
363
363
  | `mechanica link <slug>` | Point this repository at a site on the platform (`--create` makes the site); uploads are then kept by the platform, so `.mech/assets/` goes into `.gitignore` |
364
364
  | `mechanica push` | Publish the committed project to the platform: pull what was edited online, send the uploads it lacks, `git push`, build and upload the bundle when the code changed, wait for the deploy. It pushes the branch that is checked out, under its own name: the first branch a site is pushed is the site itself, any other is deployed to an address of its own (`<slug>--<branch>`) with its own online editor — a branch name is lowercase letters, digits and single hyphens |
365
365
  | `mechanica assets pull` | Download the uploads made in the platform's online editor that this project lacks (the dev server fetches them one by one on demand) |
366
+ | `mechanica migrate` | Bring `.mech` content written by an older version up to the current format: uploads are addressed as `/media/<file>` (2.0 wrote `/@mechanica/assets/<file>`) |
366
367
 
367
368
  ## Plugin options
368
369
 
@@ -393,6 +394,12 @@ From `mechanica`:
393
394
 
394
395
  From `mechanica/plugin`: `mechanica` and the `PageProvider` type. From `mechanica/widgets`: `defineWidget` and `useWidgetServices` (uploads and the image picker inside widget editors). `mechanica/editor` and `mechanica/composer` are loaded by the plugin in development; you don't import them. From `mechanica/export`: `exportBuilt(projectDir, options)`, which statically renders an already built project (`dist/` + `.mech/`) without running a build and resolves to `{ pages, warnings }` — for render services that receive a prebuilt bundle; `options.outDir` sets where the site is written, and `options.fetchJson` replaces how `useFetch` requests are made.
395
396
 
397
+ ## Upgrading from 2.0
398
+
399
+ - Uploaded files are addressed as `/media/<file>` in page files, shared data and rich text; 2.0 wrote `/@mechanica/assets/<file>`. The old address still works everywhere, so nothing breaks — run `npx mechanica migrate` once to rewrite it, and don't serve anything of your own under `/media/`.
400
+ - A file uploaded through the editor now gets a short content hash in its name (`photo-9f2c1a7b.jpg`). Files already in `.mech/assets/` keep their names.
401
+ - In the page browser, "Mark as draft" / "Publish" became "Hide from site" / "Show on site". `draft: true` in the page file means the same as before.
402
+
396
403
  ## Upgrading from 1.x
397
404
 
398
405
  Mechanica 2 is a rewrite, and a 1.x project has to be ported by hand; there is no automatic migration. What changes:
@@ -1,44 +1,8 @@
1
- import { G as isAssetName, J as readImageManifest, X as updateImageManifest } from "./rich-text-codec-BSD35k2U.js";
1
+ import { G as isAssetName, J as readImageManifest, X as updateImageManifest } from "./rich-text-codec-DCv1X0eZ.js";
2
2
  import { join } from "node:path";
3
- import { chmod, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { chmod, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import { spawn } from "node:child_process";
6
- /** Normalize a host argument to an origin (`https://example.com`, no trailing slash or path). */
7
- function hostOrigin(host) {
8
- const value = host ?? process.env.MECHANICA_HOST ?? "https://mechanica.jt3.ru";
9
- return new URL(/^https?:\/\//.test(value) ? value : `https://${value}`).origin;
10
- }
11
- /** Where access tokens are kept: one file per user, outside any project. */
12
- function credentialsFile() {
13
- return join(process.env.MECHANICA_CONFIG_DIR ?? join(os.homedir(), ".mechanica"), "credentials.json");
14
- }
15
- async function readAll() {
16
- try {
17
- const parsed = JSON.parse(await readFile(credentialsFile(), "utf-8"));
18
- return parsed && typeof parsed === "object" ? parsed : {};
19
- } catch {
20
- return {};
21
- }
22
- }
23
- /**
24
- * The access token for a platform origin: `MECHANICA_TOKEN` (CI) wins over the
25
- * one `mechanica login` saved.
26
- */
27
- async function getToken(origin) {
28
- if (process.env.MECHANICA_TOKEN) return process.env.MECHANICA_TOKEN;
29
- return (await readAll())[origin] ?? null;
30
- }
31
- /** Remember the token for an origin, in a file only the user can read. */
32
- async function saveToken(origin, token) {
33
- const file = credentialsFile();
34
- const all = await readAll();
35
- all[origin] = token;
36
- await mkdir(join(file, ".."), { recursive: true });
37
- await writeFile(file, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
38
- await chmod(file, 384).catch(() => {});
39
- return file;
40
- }
41
- //#endregion
42
6
  //#region src/cli/platform/api.ts
43
7
  /** A failed platform request: the HTTP status plus the message the platform gave. */
44
8
  var PlatformError = class extends Error {
@@ -80,12 +44,6 @@ async function request(origin, method, path, options = {}) {
80
44
  if (!response.ok) throw new PlatformError(response.status, errorMessage(response.status, text));
81
45
  return text ? JSON.parse(text) : null;
82
46
  }
83
- /** The saved token for `origin`, or an error telling the user to log in. */
84
- async function requireToken(origin) {
85
- const token = await getToken(origin);
86
- if (!token) throw new Error(`Not logged in to ${origin} — run \`mechanica login\` first`);
87
- return token;
88
- }
89
47
  /** The name of the git remote `mechanica link` manages. */
90
48
  var REMOTE_NAME = "mechanica";
91
49
  /**
@@ -105,6 +63,70 @@ function parseRemoteUrl(url) {
105
63
  return null;
106
64
  }
107
65
  }
66
+ /** Normalize a host argument to an origin (`https://example.com`, no trailing slash or path). */
67
+ function hostOrigin(host) {
68
+ const value = host ?? process.env.MECHANICA_HOST ?? "https://mechanica.jt3.ru";
69
+ return new URL(/^https?:\/\//.test(value) ? value : `https://${value}`).origin;
70
+ }
71
+ /** Where the CLI keeps what it knows about the user: one folder per user, outside any project. */
72
+ function configDir() {
73
+ return process.env.MECHANICA_CONFIG_DIR ?? join(os.homedir(), ".mechanica");
74
+ }
75
+ /** Access tokens, by platform origin. */
76
+ var credentialsFile = () => join(configDir(), "credentials.json");
77
+ /** Sign-ins started by `mechanica login` that wait for their approval, by platform origin. */
78
+ var pendingFile = () => join(configDir(), "pending-logins.json");
79
+ async function readJson(file) {
80
+ try {
81
+ const parsed = JSON.parse(await readFile(file, "utf-8"));
82
+ return parsed && typeof parsed === "object" ? parsed : {};
83
+ } catch {
84
+ return {};
85
+ }
86
+ }
87
+ /** Write one of the files, readable by the user alone. */
88
+ async function writeJson(file, value) {
89
+ await mkdir(join(file, ".."), { recursive: true });
90
+ await writeFile(file, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
91
+ await chmod(file, 384).catch(() => {});
92
+ }
93
+ /**
94
+ * The access token for a platform origin: `MECHANICA_TOKEN` (CI) wins over the
95
+ * one `mechanica login` saved.
96
+ */
97
+ async function getToken(origin) {
98
+ if (process.env.MECHANICA_TOKEN) return process.env.MECHANICA_TOKEN;
99
+ return (await readJson(credentialsFile()))[origin] ?? null;
100
+ }
101
+ /** Remember the token for an origin, in a file only the user can read. */
102
+ async function saveToken(origin, token) {
103
+ const file = credentialsFile();
104
+ const all = await readJson(file);
105
+ all[origin] = token;
106
+ await writeJson(file, all);
107
+ return file;
108
+ }
109
+ /** The sign-in waiting for an origin, or null. An expired one is reported too: whoever reads it says so. */
110
+ async function getPendingLogin(origin) {
111
+ const entry = (await readJson(pendingFile()))[origin];
112
+ return entry && typeof entry.deviceCode === "string" && typeof entry.verificationUrl === "string" ? entry : null;
113
+ }
114
+ /** Keep a started sign-in for an origin (replacing an earlier one). */
115
+ async function savePendingLogin(origin, login) {
116
+ const file = pendingFile();
117
+ const all = await readJson(file);
118
+ all[origin] = login;
119
+ await writeJson(file, all);
120
+ }
121
+ /** Forget the sign-in waiting for an origin — it was finished, denied or has expired. */
122
+ async function clearPendingLogin(origin) {
123
+ const file = pendingFile();
124
+ const all = await readJson(file);
125
+ if (!(origin in all)) return;
126
+ delete all[origin];
127
+ if (Object.keys(all).length) await writeJson(file, all);
128
+ else await rm(file, { force: true });
129
+ }
108
130
  //#endregion
109
131
  //#region src/cli/platform/git.ts
110
132
  /**
@@ -319,4 +341,4 @@ async function runAssetsPull(options = {}) {
319
341
  return done;
320
342
  }
321
343
  //#endregion
322
- export { git as a, PlatformError as c, request as d, requireToken as f, uploadsIgnored as i, REMOTE_NAME as l, saveToken as m, remoteAssetsFor as n, gitAuthEnv as o, hostOrigin as p, runAssetsPull as r, gitOk as s, pushUploads as t, parseRemoteUrl as u };
344
+ export { request as _, git as a, clearPendingLogin as c, hostOrigin as d, savePendingLogin as f, parseRemoteUrl as g, REMOTE_NAME as h, uploadsIgnored as i, getPendingLogin as l, PlatformError as m, remoteAssetsFor as n, gitAuthEnv as o, saveToken as p, runAssetsPull as r, gitOk as s, pushUploads as t, getToken as u };
@@ -1,5 +1,5 @@
1
- import { n as configureSite, r as indexGeneratedPages } from "./site-DfazSr3_.js";
2
- import { A as pageVersion, C as harvestPageImages, D as movePage, E as listPages, I as setPageDraft, L as translationsOf, M as renamePage, N as savePage, Q as uploadResponse, R as contentFilesOf, T as listFolders, U as contentTypeOf, W as fsAssetStore, Y as storeUpload, _ as deletePage, a as folderOf, b as fillContentDefaults, c as mergeLocaleSiteData, f as readSiteData, g as createTranslation, h as createPage, j as readPage, l as mergeSiteData, m as PageExistsError, o as mergeFolderData, q as listUploads, s as mergeLocaleFolderData, u as readFolderData, v as deleteTranslation, w as imageNamesOf, x as fillImageMeta, y as duplicatePage } from "./rich-text-codec-BSD35k2U.js";
1
+ import { n as configureSite, r as indexGeneratedPages } from "./site-BVqx8iKj.js";
2
+ import { A as pageVersion, C as harvestPageImages, D as movePage, E as listPages, I as setPageDraft, L as translationsOf, M as renamePage, N as savePage, Q as uploadResponse, R as contentFilesOf, T as listFolders, U as contentTypeOf, W as fsAssetStore, Y as storeUpload, _ as deletePage, a as folderOf, b as fillContentDefaults, c as mergeLocaleSiteData, f as readSiteData, g as createTranslation, h as createPage, j as readPage, l as mergeSiteData, m as PageExistsError, o as mergeFolderData, q as listUploads, s as mergeLocaleFolderData, u as readFolderData, v as deleteTranslation, w as imageNamesOf, x as fillImageMeta, y as duplicatePage } from "./rich-text-codec-DCv1X0eZ.js";
3
3
  import fs, { readFileSync, readdirSync } from "node:fs";
4
4
  import { basename, join, resolve, sep } from "node:path";
5
5
  import { EDITOR_DIST_DIR, mergeTranslation, parseLocalePath, passDataToHTML, resolveQueryKey, serializeState } from "mechanica-shared";
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import { dirname, join, parse, relative, resolve, sep } from "node:path";
3
3
  import { UPLOADS_PREFIX as UPLOADS_PREFIX$1, diffTranslation, migrateContent, passDefaultValue, uploadNameOf, walkSchema, walkTree } from "mechanica-shared";
4
4
  import { createHash } from "node:crypto";
5
- import { parsePage, serializePage } from "mechanica-shared/page-format";
5
+ import { PageParseError, parsePage, serializePage } from "mechanica-shared/page-format";
6
6
  import { Readable } from "node:stream";
7
7
  import { createRequire } from "node:module";
8
8
  import { unfoldSchema } from "compact-json-schema";
@@ -671,7 +671,13 @@ var emptyPage = () => ({
671
671
  var fileOf = (mech, urlPath, locale) => pageFile(contentFilesOf(mech), urlPath, locale);
672
672
  var has = (mech, file) => contentFilesOf(mech).has(file);
673
673
  var remove = (mech, file) => contentFilesOf(mech).remove(file);
674
- var readFile = (mech, file) => parsePage(contentFilesOf(mech).read(file) ?? "", codecOptions(mech));
674
+ var readFile = (mech, file) => {
675
+ try {
676
+ return parsePage(contentFilesOf(mech).read(file) ?? "", codecOptions(mech));
677
+ } catch (error) {
678
+ throw error instanceof PageParseError ? error.inFile(file) : error;
679
+ }
680
+ };
675
681
  var writeFile = (mech, file, page) => {
676
682
  contentFilesOf(mech).write(file, serializePage(page, codecOptions(mech)));
677
683
  };
@@ -1,4 +1,4 @@
1
- import { F as setPageCodec, P as setPageBlocks, i as toBlockMeta, n as richTextCodecFromBlocks, r as composedBlockMeta } from "./rich-text-codec-BSD35k2U.js";
1
+ import { F as setPageCodec, P as setPageBlocks, i as toBlockMeta, n as richTextCodecFromBlocks, r as composedBlockMeta } from "./rich-text-codec-DCv1X0eZ.js";
2
2
  import fs from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { SITE_MANIFEST_FILE, areFieldSchemasRegistered, localePath, registerFieldSchemas } from "mechanica-shared";
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as buildSiteManifest } from "./chunks/site-DfazSr3_.js";
2
- import { J as readImageManifest, K as isDerivedAsset, W as fsAssetStore, X as updateImageManifest, et as withRemoteAssets, it as isRasterImage, nt as analyzeImageBuffer, rt as hasSharp } from "./chunks/rich-text-codec-BSD35k2U.js";
3
- import { a as git, c as PlatformError, d as request, f as requireToken, i as uploadsIgnored, l as REMOTE_NAME, m as saveToken, n as remoteAssetsFor, o as gitAuthEnv, p as hostOrigin, r as runAssetsPull, s as gitOk, t as pushUploads, u as parseRemoteUrl } from "./chunks/assets-nujjbCx8.js";
1
+ import { t as buildSiteManifest } from "./chunks/site-BVqx8iKj.js";
2
+ import { J as readImageManifest, K as isDerivedAsset, W as fsAssetStore, X as updateImageManifest, et as withRemoteAssets, it as isRasterImage, nt as analyzeImageBuffer, rt as hasSharp } from "./chunks/rich-text-codec-DCv1X0eZ.js";
3
+ import { _ as request, a as git, c as clearPendingLogin, d as hostOrigin, f as savePendingLogin, g as parseRemoteUrl, h as REMOTE_NAME, i as uploadsIgnored, l as getPendingLogin, m as PlatformError, n as remoteAssetsFor, o as gitAuthEnv, p as saveToken, r as runAssetsPull, s as gitOk, t as pushUploads, u as getToken } from "./chunks/assets-5a2WdmFg.js";
4
4
  import { exportBuilt } from "./export.js";
5
5
  import fs, { readFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
@@ -10,7 +10,6 @@ import os from "node:os";
10
10
  import { spawn } from "node:child_process";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { build } from "vite";
13
- import readline from "node:readline";
14
13
  //#region src/cli/args.ts
15
14
  /** Parse `argv` (without node/script) into a command, positionals and a `--flag[=value]` map. */
16
15
  function parseArgs(argv) {
@@ -234,6 +233,89 @@ async function runMigrate(options = {}) {
234
233
  return changed;
235
234
  }
236
235
  //#endregion
236
+ //#region src/cli/platform/session.ts
237
+ /** Start a sign-in: the platform makes a request, its codes are kept for this origin. */
238
+ async function startDeviceLogin(origin) {
239
+ const started = await request(origin, "POST", "/api/account/device", { body: { name: `CLI on ${os.hostname()}` } });
240
+ const pending = {
241
+ deviceCode: started.deviceCode,
242
+ userCode: started.userCode,
243
+ verificationUrl: started.verificationUrl,
244
+ expiresAt: new Date(started.expiresAt).toISOString(),
245
+ interval: Math.max(1, Number(started.interval) || 3)
246
+ };
247
+ await savePendingLogin(origin, pending);
248
+ return pending;
249
+ }
250
+ /** What the user is shown: the link, and how long it stays open. */
251
+ function describeDeviceLogin(origin, pending) {
252
+ const minutes = Math.max(1, Math.round((Date.parse(pending.expiresAt) - Date.now()) / 6e4));
253
+ return [
254
+ `To sign in to ${origin}, open this link and approve the request:`,
255
+ "",
256
+ ` ${pending.verificationUrl}`,
257
+ "",
258
+ `The link is valid for ${minutes} minute${minutes === 1 ? "" : "s"}.`
259
+ ].join("\n");
260
+ }
261
+ /** Ask the platform once whether the request was answered. */
262
+ async function checkDeviceLogin(origin, pending) {
263
+ try {
264
+ const answer = await request(origin, "POST", "/api/account/device/token", { body: { deviceCode: pending.deviceCode } });
265
+ if (answer?.status === "approved" && typeof answer.token === "string") return {
266
+ status: "approved",
267
+ token: answer.token
268
+ };
269
+ if (answer?.status === "denied") return { status: "denied" };
270
+ return { status: "pending" };
271
+ } catch (error) {
272
+ if (error instanceof PlatformError && error.status === 404) return { status: "expired" };
273
+ throw error;
274
+ }
275
+ }
276
+ /**
277
+ * Finish a started sign-in if it was answered: save the token and say who
278
+ * signed in. Null while the request still waits. A denied or expired request
279
+ * is forgotten and reported — the user runs `mechanica login` for a new link.
280
+ */
281
+ async function completeDeviceLogin(origin, pending) {
282
+ const outcome = await checkDeviceLogin(origin, pending);
283
+ if (outcome.status === "pending") return null;
284
+ await clearPendingLogin(origin);
285
+ if (outcome.status === "denied") throw new Error(`The sign-in request for ${origin} was denied in the browser. Run \`mechanica login\` for a new link.`);
286
+ if (outcome.status === "expired") throw new Error(`The sign-in link for ${origin} has expired (a link is valid for ten minutes). Run \`mechanica login\` for a new link.`);
287
+ const account = await request(origin, "GET", "/api/account", { token: outcome.token });
288
+ const file = await saveToken(origin, outcome.token);
289
+ console.info(`Signed in to ${origin} as ${account.email}`);
290
+ console.info(`Access token saved to ${file}`);
291
+ return outcome.token;
292
+ }
293
+ var sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
294
+ /** Wait for the approval, asking the platform every `interval` seconds until it comes or the link expires. */
295
+ async function waitForDeviceLogin(origin, pending) {
296
+ for (;;) {
297
+ const token = await completeDeviceLogin(origin, pending);
298
+ if (token) return token;
299
+ await sleep$1(pending.interval * 1e3);
300
+ }
301
+ }
302
+ /**
303
+ * The token for `origin`: the saved one, or — when `mechanica login` printed
304
+ * a link that has been approved since — the one the platform now hands over.
305
+ * Otherwise an error that says what to do next.
306
+ */
307
+ async function requireToken(origin) {
308
+ const token = await getToken(origin);
309
+ if (token) return token;
310
+ const pending = await getPendingLogin(origin);
311
+ if (pending) {
312
+ const collected = await completeDeviceLogin(origin, pending);
313
+ if (collected) return collected;
314
+ throw new Error(`Not signed in to ${origin} yet — open ${pending.verificationUrl} and approve the request, then run this command again`);
315
+ }
316
+ throw new Error(`Not logged in to ${origin} — run \`mechanica login\` first`);
317
+ }
318
+ //#endregion
237
319
  //#region src/cli/platform/link.ts
238
320
  /**
239
321
  * `mechanica link <slug>`: attach this project to a site on the platform by
@@ -317,55 +399,35 @@ async function ignoreUploads(cwd) {
317
399
  }
318
400
  //#endregion
319
401
  //#region src/cli/platform/login.ts
320
- /** Ask one line on the terminal; with `hidden`, the typed characters are not echoed. */
321
- function prompt(question, hidden = false) {
322
- return new Promise((resolve) => {
323
- const rl = readline.createInterface({
324
- input: process.stdin,
325
- output: process.stdout,
326
- terminal: true
327
- });
328
- if (hidden) {
329
- const write = rl._writeToOutput.bind(rl);
330
- let asked = false;
331
- rl._writeToOutput = (text) => {
332
- if (!asked) write(text);
333
- asked = true;
334
- };
335
- }
336
- rl.question(question, (answer) => {
337
- rl.close();
338
- if (hidden) process.stdout.write("\n");
339
- resolve(answer.trim());
340
- });
341
- });
342
- }
343
402
  /**
344
403
  * `mechanica login`: sign in to the platform and keep an access token for this
345
404
  * machine. With `--token` the given token is stored as is (CI, or a token made
346
- * in the dashboard); otherwise it asks for email + password and creates one.
405
+ * in the dashboard). Otherwise the sign-in happens in the browser (see
406
+ * `session.ts`): a link is printed, and the token arrives once it is approved.
407
+ *
408
+ * In a terminal the command waits for the approval. Without one — a coding
409
+ * agent runs it, and shows its output only once it has ended — it prints the
410
+ * link and returns: the next `mechanica link` / `push` finishes the sign-in.
411
+ * `--wait` / `--no-wait` decide either way.
347
412
  */
348
413
  async function runLogin(options) {
349
414
  const origin = hostOrigin(options.host);
350
- let token = options.token;
351
- if (!token) {
352
- if (!process.stdin.isTTY) throw new Error("No terminal to ask for credentials — pass --token <access token>");
353
- console.info(`Signing in to ${origin}`);
354
- const login = await prompt("Email: ");
355
- const password = await prompt("Password: ", true);
356
- const session = await request(origin, "POST", "/api/account/login", { body: {
357
- login,
358
- password
359
- } });
360
- token = (await request(origin, "POST", "/api/account/tokens", {
361
- token: session.accessToken,
362
- body: { name: `CLI on ${os.hostname()}` }
363
- })).token;
415
+ if (options.token) {
416
+ const account = await request(origin, "GET", "/api/account", { token: options.token });
417
+ const file = await saveToken(origin, options.token);
418
+ console.info(`Logged in to ${origin} as ${account.email}`);
419
+ console.info(`Access token saved to ${file}`);
420
+ return;
364
421
  }
365
- const account = await request(origin, "GET", "/api/account", { token });
366
- const file = await saveToken(origin, token);
367
- console.info(`Logged in to ${origin} as ${account.email}`);
368
- console.info(`Access token saved to ${file}`);
422
+ const pending = await startDeviceLogin(origin);
423
+ console.info(describeDeviceLogin(origin, pending));
424
+ console.info("");
425
+ if (!(options.wait ?? process.stdin.isTTY === true)) {
426
+ console.info("Once it is approved, run `mechanica link <slug>` or `mechanica push`: the next command finishes the sign-in.\nWorking through a coding agent? Show the link to the person you work for, and go on when they say it is approved.");
427
+ return;
428
+ }
429
+ console.info("Waiting for the approval in the browser… (Ctrl+C to stop; the next `mechanica` command would finish the sign-in)");
430
+ await waitForDeviceLogin(origin, pending);
369
431
  }
370
432
  //#endregion
371
433
  //#region src/cli/platform/bundle-format.ts
@@ -1237,8 +1299,12 @@ Usage:
1237
1299
  --site-name <name> adds WebSite JSON-LD;
1238
1300
  --assets-url <origin> serves /assets and /media from
1239
1301
  a CDN base — files still land in export/)
1240
- mechanica login Sign in to the Mechanica platform and save an access
1241
- token for this machine (--host <url>, --token <token>)
1302
+ mechanica login Sign in to the Mechanica platform: prints a link to
1303
+ open and approve in the browser, and keeps the access
1304
+ token for this machine. In a terminal it waits for the
1305
+ approval; otherwise the next link/push finishes the
1306
+ sign-in (--wait, --no-wait, --host <url>, or
1307
+ --token <token> to store a token made in the dashboard)
1242
1308
  mechanica link <slug> Point this repository at a site on the platform
1243
1309
  (--create makes the site, --name "Site name")
1244
1310
  mechanica push Publish the committed project: pull what was edited
@@ -1288,7 +1354,8 @@ async function run(argv) {
1288
1354
  return;
1289
1355
  case "login": return runLogin({
1290
1356
  host: str(flags.host),
1291
- token: str(flags.token)
1357
+ token: str(flags.token),
1358
+ wait: flags.wait === true ? true : flags["no-wait"] === true ? false : void 0
1292
1359
  });
1293
1360
  case "link": return runLink(args[0], {
1294
1361
  host: str(flags.host),
package/dist/editor.css CHANGED
@@ -5027,7 +5027,7 @@ from {
5027
5027
  justify-content: flex-end;
5028
5028
  gap: 8px;
5029
5029
  margin-top: 4px;
5030
- }.mech-pages[data-v-efe45d0e] {
5030
+ }.mech-pages[data-v-56e0f852] {
5031
5031
  display: flex;
5032
5032
  align-items: stretch;
5033
5033
  justify-content: center;
@@ -5036,7 +5036,7 @@ from {
5036
5036
  max-width: 1200px;
5037
5037
  height: min(620px, 82vh);
5038
5038
  }
5039
- .mech-pages__card[data-v-efe45d0e] {
5039
+ .mech-pages__card[data-v-56e0f852] {
5040
5040
  display: flex;
5041
5041
  flex-direction: column;
5042
5042
  flex: 0 1 840px;
@@ -5046,20 +5046,20 @@ from {
5046
5046
  box-shadow: var(--mech-shadow-dialog);
5047
5047
  overflow: hidden;
5048
5048
  }
5049
- .mech-pages__header[data-v-efe45d0e] {
5049
+ .mech-pages__header[data-v-56e0f852] {
5050
5050
  display: flex;
5051
5051
  align-items: center;
5052
5052
  gap: 10px;
5053
5053
  padding: 18px 14px 12px 24px;
5054
5054
  flex: none;
5055
5055
  }
5056
- .mech-pages__title[data-v-efe45d0e] {
5056
+ .mech-pages__title[data-v-56e0f852] {
5057
5057
  margin: 0;
5058
5058
  font-size: 16px;
5059
5059
  font-weight: 600;
5060
5060
  letter-spacing: -0.01em;
5061
5061
  }
5062
- .mech-pages__count[data-v-efe45d0e] {
5062
+ .mech-pages__count[data-v-56e0f852] {
5063
5063
  padding: 2px 8px;
5064
5064
  border-radius: var(--mech-radius-pill);
5065
5065
  background: var(--mech-active);
@@ -5067,7 +5067,7 @@ from {
5067
5067
  font-size: 11.5px;
5068
5068
  font-weight: 600;
5069
5069
  }
5070
- .mech-pages__close[data-v-efe45d0e] {
5070
+ .mech-pages__close[data-v-56e0f852] {
5071
5071
  display: flex;
5072
5072
  align-items: center;
5073
5073
  justify-content: center;
@@ -5080,25 +5080,25 @@ from {
5080
5080
  border-radius: var(--mech-radius-pill);
5081
5081
  cursor: pointer;
5082
5082
  }
5083
- .mech-pages__close .vicon[data-v-efe45d0e] {
5083
+ .mech-pages__close .vicon[data-v-56e0f852] {
5084
5084
  width: 18px;
5085
5085
  height: 18px;
5086
5086
  }
5087
- .mech-pages__close[data-v-efe45d0e]:hover {
5087
+ .mech-pages__close[data-v-56e0f852]:hover {
5088
5088
  background: var(--mech-hover);
5089
5089
  color: var(--mech-fg);
5090
5090
  }
5091
- .mech-pages__toolbar[data-v-efe45d0e] {
5091
+ .mech-pages__toolbar[data-v-56e0f852] {
5092
5092
  display: flex;
5093
5093
  gap: 8px;
5094
5094
  padding: 0 24px 12px;
5095
5095
  flex: none;
5096
5096
  }
5097
- .mech-pages__search-wrap[data-v-efe45d0e] {
5097
+ .mech-pages__search-wrap[data-v-56e0f852] {
5098
5098
  position: relative;
5099
5099
  flex: 1;
5100
5100
  }
5101
- .mech-pages__filters[data-v-efe45d0e] {
5101
+ .mech-pages__filters[data-v-56e0f852] {
5102
5102
  display: flex;
5103
5103
  align-items: center;
5104
5104
  justify-content: space-between;
@@ -5106,23 +5106,23 @@ from {
5106
5106
  padding: 0 24px 14px;
5107
5107
  flex: none;
5108
5108
  }
5109
- .mech-pages__sort[data-v-efe45d0e] {
5109
+ .mech-pages__sort[data-v-56e0f852] {
5110
5110
  display: flex;
5111
5111
  align-items: center;
5112
5112
  gap: 9px;
5113
5113
  }
5114
- .mech-pages__sort-label[data-v-efe45d0e] {
5114
+ .mech-pages__sort-label[data-v-56e0f852] {
5115
5115
  font-size: 10.5px;
5116
5116
  font-weight: 600;
5117
5117
  text-transform: uppercase;
5118
5118
  letter-spacing: 0.05em;
5119
5119
  color: var(--mech-muted);
5120
5120
  }
5121
- .mech-pages__toggle[data-v-efe45d0e] {
5121
+ .mech-pages__toggle[data-v-56e0f852] {
5122
5122
  flex: none;
5123
5123
  white-space: nowrap;
5124
5124
  }
5125
- .mech-pages__search-icon[data-v-efe45d0e] {
5125
+ .mech-pages__search-icon[data-v-56e0f852] {
5126
5126
  position: absolute;
5127
5127
  left: 12px;
5128
5128
  top: 50%;
@@ -5132,18 +5132,18 @@ from {
5132
5132
  color: var(--mech-muted);
5133
5133
  pointer-events: none;
5134
5134
  }
5135
- .mech-pages__search[data-v-efe45d0e] {
5135
+ .mech-pages__search[data-v-56e0f852] {
5136
5136
  width: 100%;
5137
5137
  padding-left: 36px;
5138
5138
  }
5139
- .mech-pages__content[data-v-efe45d0e] {
5139
+ .mech-pages__content[data-v-56e0f852] {
5140
5140
  display: flex;
5141
5141
  gap: 14px;
5142
5142
  flex: 1;
5143
5143
  min-height: 0;
5144
5144
  padding: 0 24px 20px;
5145
5145
  }
5146
- .mech-pages__rail[data-v-efe45d0e] {
5146
+ .mech-pages__rail[data-v-56e0f852] {
5147
5147
  flex: none;
5148
5148
  width: 172px;
5149
5149
  display: flex;
@@ -5152,7 +5152,7 @@ from {
5152
5152
  overflow-y: auto;
5153
5153
  padding-right: 2px;
5154
5154
  }
5155
- .mech-pages__rail-item[data-v-efe45d0e] {
5155
+ .mech-pages__rail-item[data-v-56e0f852] {
5156
5156
  display: flex;
5157
5157
  align-items: center;
5158
5158
  gap: 8px;
@@ -5166,51 +5166,51 @@ from {
5166
5166
  color: var(--mech-fg-alt);
5167
5167
  cursor: pointer;
5168
5168
  }
5169
- .mech-pages__rail-item .vicon[data-v-efe45d0e] {
5169
+ .mech-pages__rail-item .vicon[data-v-56e0f852] {
5170
5170
  flex: none;
5171
5171
  width: 15px;
5172
5172
  height: 15px;
5173
5173
  color: var(--mech-muted);
5174
5174
  }
5175
- .mech-pages__rail-item[data-v-efe45d0e]:hover {
5175
+ .mech-pages__rail-item[data-v-56e0f852]:hover {
5176
5176
  background: var(--mech-hover);
5177
5177
  }
5178
- .mech-pages__rail-item.is-active[data-v-efe45d0e] {
5178
+ .mech-pages__rail-item.is-active[data-v-56e0f852] {
5179
5179
  background: var(--mech-accent-soft);
5180
5180
  color: var(--mech-accent);
5181
5181
  font-weight: 500;
5182
5182
  }
5183
- .mech-pages__rail-item.is-active .vicon[data-v-efe45d0e] {
5183
+ .mech-pages__rail-item.is-active .vicon[data-v-56e0f852] {
5184
5184
  color: var(--mech-accent);
5185
5185
  }
5186
- .mech-pages__rail-name[data-v-efe45d0e] {
5186
+ .mech-pages__rail-name[data-v-56e0f852] {
5187
5187
  flex: 1;
5188
5188
  min-width: 0;
5189
5189
  overflow: hidden;
5190
5190
  text-overflow: ellipsis;
5191
5191
  white-space: nowrap;
5192
5192
  }
5193
- .mech-pages__rail-count[data-v-efe45d0e] {
5193
+ .mech-pages__rail-count[data-v-56e0f852] {
5194
5194
  flex: none;
5195
5195
  font-size: 11px;
5196
5196
  color: var(--mech-placeholder);
5197
5197
  }
5198
- .mech-pages__rail-item.is-active .mech-pages__rail-count[data-v-efe45d0e] {
5198
+ .mech-pages__rail-item.is-active .mech-pages__rail-count[data-v-56e0f852] {
5199
5199
  color: var(--mech-accent);
5200
5200
  opacity: 0.7;
5201
5201
  }
5202
- .mech-pages__list[data-v-efe45d0e] {
5202
+ .mech-pages__list[data-v-56e0f852] {
5203
5203
  flex: 1;
5204
5204
  min-width: 0;
5205
5205
  overflow-y: auto;
5206
5206
  }
5207
- .mech-pages__row[data-v-efe45d0e], .mech-pages__thead[data-v-efe45d0e] {
5207
+ .mech-pages__row[data-v-56e0f852], .mech-pages__thead[data-v-56e0f852] {
5208
5208
  display: grid;
5209
5209
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) 96px;
5210
5210
  align-items: center;
5211
5211
  column-gap: 12px;
5212
5212
  }
5213
- .mech-pages__thead[data-v-efe45d0e] {
5213
+ .mech-pages__thead[data-v-56e0f852] {
5214
5214
  position: sticky;
5215
5215
  top: 0;
5216
5216
  z-index: 1;
@@ -5218,14 +5218,14 @@ from {
5218
5218
  background: var(--mech-bg);
5219
5219
  box-shadow: 0 1px 0 var(--mech-border);
5220
5220
  }
5221
- .mech-pages__thead span[data-v-efe45d0e] {
5221
+ .mech-pages__thead span[data-v-56e0f852] {
5222
5222
  font-size: 10.5px;
5223
5223
  font-weight: 600;
5224
5224
  text-transform: uppercase;
5225
5225
  letter-spacing: 0.05em;
5226
5226
  color: var(--mech-muted);
5227
5227
  }
5228
- .mech-pages__group[data-v-efe45d0e] {
5228
+ .mech-pages__group[data-v-56e0f852] {
5229
5229
  display: flex;
5230
5230
  align-items: center;
5231
5231
  gap: 7px;
@@ -5244,36 +5244,36 @@ from {
5244
5244
  border-radius: var(--mech-radius-sm);
5245
5245
  cursor: pointer;
5246
5246
  }
5247
- .mech-pages__group .vicon[data-v-efe45d0e] {
5247
+ .mech-pages__group .vicon[data-v-56e0f852] {
5248
5248
  width: 13px;
5249
5249
  height: 13px;
5250
5250
  }
5251
- .mech-pages__group[data-v-efe45d0e]:hover {
5251
+ .mech-pages__group[data-v-56e0f852]:hover {
5252
5252
  color: var(--mech-fg);
5253
5253
  }
5254
- .mech-pages__row[data-v-efe45d0e] {
5254
+ .mech-pages__row[data-v-56e0f852] {
5255
5255
  padding: 0 10px;
5256
5256
  height: 38px;
5257
5257
  border-radius: var(--mech-radius-sm);
5258
5258
  cursor: pointer;
5259
5259
  }
5260
- .mech-pages__row.is-active[data-v-efe45d0e] {
5260
+ .mech-pages__row.is-active[data-v-56e0f852] {
5261
5261
  background: var(--mech-accent-soft);
5262
5262
  }
5263
- .mech-pages__cell-name[data-v-efe45d0e] {
5263
+ .mech-pages__cell-name[data-v-56e0f852] {
5264
5264
  display: flex;
5265
5265
  align-items: center;
5266
5266
  gap: 8px;
5267
5267
  min-width: 0;
5268
5268
  }
5269
- .mech-pages__name[data-v-efe45d0e] {
5269
+ .mech-pages__name[data-v-56e0f852] {
5270
5270
  font-size: 13.5px;
5271
5271
  font-weight: 500;
5272
5272
  overflow: hidden;
5273
5273
  text-overflow: ellipsis;
5274
5274
  white-space: nowrap;
5275
5275
  }
5276
- .mech-pages__badge[data-v-efe45d0e] {
5276
+ .mech-pages__badge[data-v-56e0f852] {
5277
5277
  flex: none;
5278
5278
  font-size: 10px;
5279
5279
  font-weight: 600;
@@ -5285,17 +5285,17 @@ from {
5285
5285
  border-radius: var(--mech-radius-pill);
5286
5286
  padding: 1px 7px;
5287
5287
  }
5288
- .mech-pages__badge.is-draft[data-v-efe45d0e] {
5288
+ .mech-pages__badge.is-draft[data-v-56e0f852] {
5289
5289
  color: var(--mech-muted);
5290
5290
  }
5291
- .mech-pages__locales[data-v-efe45d0e] {
5291
+ .mech-pages__locales[data-v-56e0f852] {
5292
5292
  flex: none;
5293
5293
  display: inline-flex;
5294
5294
  align-items: center;
5295
5295
  gap: 3px;
5296
5296
  margin-left: 2px;
5297
5297
  }
5298
- .mech-pages__loc[data-v-efe45d0e] {
5298
+ .mech-pages__loc[data-v-56e0f852] {
5299
5299
  min-width: 15px;
5300
5300
  padding: 1px 4px;
5301
5301
  border-radius: var(--mech-radius-sm);
@@ -5309,12 +5309,12 @@ from {
5309
5309
  letter-spacing: 0.02em;
5310
5310
  opacity: 0.5;
5311
5311
  }
5312
- .mech-pages__loc.is-on[data-v-efe45d0e] {
5312
+ .mech-pages__loc.is-on[data-v-56e0f852] {
5313
5313
  background: var(--mech-accent-soft);
5314
5314
  color: var(--mech-accent);
5315
5315
  opacity: 1;
5316
5316
  }
5317
- .mech-pages__cell-path[data-v-efe45d0e] {
5317
+ .mech-pages__cell-path[data-v-56e0f852] {
5318
5318
  min-width: 0;
5319
5319
  overflow: hidden;
5320
5320
  text-overflow: ellipsis;
@@ -5322,16 +5322,16 @@ from {
5322
5322
  font-size: 12.5px;
5323
5323
  color: var(--mech-muted);
5324
5324
  }
5325
- .mech-pages__actions[data-v-efe45d0e] {
5325
+ .mech-pages__actions[data-v-56e0f852] {
5326
5326
  display: flex;
5327
5327
  justify-content: flex-end;
5328
5328
  gap: 1px;
5329
5329
  opacity: 0;
5330
5330
  }
5331
- .mech-pages__row.is-active .mech-pages__actions[data-v-efe45d0e], .mech-pages__row:hover .mech-pages__actions[data-v-efe45d0e] {
5331
+ .mech-pages__row.is-active .mech-pages__actions[data-v-56e0f852], .mech-pages__row:hover .mech-pages__actions[data-v-56e0f852] {
5332
5332
  opacity: 1;
5333
5333
  }
5334
- .mech-pages__actions button[data-v-efe45d0e] {
5334
+ .mech-pages__actions button[data-v-56e0f852] {
5335
5335
  display: flex;
5336
5336
  align-items: center;
5337
5337
  justify-content: center;
@@ -5344,24 +5344,24 @@ from {
5344
5344
  cursor: pointer;
5345
5345
  line-height: 1;
5346
5346
  }
5347
- .mech-pages__actions button .vicon[data-v-efe45d0e] {
5347
+ .mech-pages__actions button .vicon[data-v-56e0f852] {
5348
5348
  width: 15px;
5349
5349
  height: 15px;
5350
5350
  }
5351
- .mech-pages__actions button[data-v-efe45d0e]:hover {
5351
+ .mech-pages__actions button[data-v-56e0f852]:hover {
5352
5352
  background: var(--mech-active);
5353
5353
  color: var(--mech-fg);
5354
5354
  }
5355
- .mech-pages__actions button.is-danger[data-v-efe45d0e]:hover {
5355
+ .mech-pages__actions button.is-danger[data-v-56e0f852]:hover {
5356
5356
  color: var(--mech-error);
5357
5357
  }
5358
- .mech-pages__empty[data-v-efe45d0e] {
5358
+ .mech-pages__empty[data-v-56e0f852] {
5359
5359
  padding: 24px 10px;
5360
5360
  margin: 0;
5361
5361
  color: var(--mech-muted);
5362
5362
  font-size: 13px;
5363
5363
  }
5364
- .mech-pages__preview[data-v-efe45d0e] {
5364
+ .mech-pages__preview[data-v-56e0f852] {
5365
5365
  flex: none;
5366
5366
  align-self: flex-start;
5367
5367
  max-height: 100%;
@@ -5373,18 +5373,18 @@ from {
5373
5373
  box-shadow: var(--mech-shadow-dialog);
5374
5374
  overflow: hidden;
5375
5375
  }
5376
- .mech-pages__preview-frame[data-v-efe45d0e] {
5376
+ .mech-pages__preview-frame[data-v-56e0f852] {
5377
5377
  flex: 0 1 auto;
5378
5378
  min-height: 0;
5379
5379
  overflow: hidden;
5380
5380
  background: var(--mech-app-bg);
5381
5381
  }
5382
- .mech-pages__preview-frame img[data-v-efe45d0e] {
5382
+ .mech-pages__preview-frame img[data-v-56e0f852] {
5383
5383
  display: block;
5384
5384
  width: 100%;
5385
5385
  height: auto;
5386
5386
  }
5387
- .mech-pages__preview-empty[data-v-efe45d0e] {
5387
+ .mech-pages__preview-empty[data-v-56e0f852] {
5388
5388
  display: flex;
5389
5389
  flex-direction: column;
5390
5390
  align-items: center;
@@ -5393,16 +5393,16 @@ from {
5393
5393
  padding: 48px 16px;
5394
5394
  color: var(--mech-muted);
5395
5395
  }
5396
- .mech-pages__preview-empty p[data-v-efe45d0e] {
5396
+ .mech-pages__preview-empty p[data-v-56e0f852] {
5397
5397
  margin: 8px 0 0;
5398
5398
  font-size: 13px;
5399
5399
  font-weight: 500;
5400
5400
  }
5401
- .mech-pages__preview-empty code[data-v-efe45d0e] {
5401
+ .mech-pages__preview-empty code[data-v-56e0f852] {
5402
5402
  font-size: 11.5px;
5403
5403
  color: var(--mech-placeholder);
5404
5404
  }
5405
- .mech-pages__preview-monogram[data-v-efe45d0e] {
5405
+ .mech-pages__preview-monogram[data-v-56e0f852] {
5406
5406
  display: flex;
5407
5407
  align-items: center;
5408
5408
  justify-content: center;
@@ -5413,7 +5413,7 @@ from {
5413
5413
  font-size: 22px;
5414
5414
  font-weight: 600;
5415
5415
  }
5416
- .mech-pages__preview-caption[data-v-efe45d0e] {
5416
+ .mech-pages__preview-caption[data-v-56e0f852] {
5417
5417
  flex: none;
5418
5418
  display: flex;
5419
5419
  flex-direction: column;
@@ -5421,21 +5421,21 @@ from {
5421
5421
  padding: 12px 16px 14px;
5422
5422
  border-top: 1px solid var(--mech-border);
5423
5423
  }
5424
- .mech-pages__preview-name[data-v-efe45d0e] {
5424
+ .mech-pages__preview-name[data-v-56e0f852] {
5425
5425
  font-size: 13px;
5426
5426
  font-weight: 600;
5427
5427
  overflow: hidden;
5428
5428
  text-overflow: ellipsis;
5429
5429
  white-space: nowrap;
5430
5430
  }
5431
- .mech-pages__preview-path[data-v-efe45d0e] {
5431
+ .mech-pages__preview-path[data-v-56e0f852] {
5432
5432
  font-size: 11.5px;
5433
5433
  color: var(--mech-muted);
5434
5434
  overflow: hidden;
5435
5435
  text-overflow: ellipsis;
5436
5436
  white-space: nowrap;
5437
5437
  }
5438
- .mech-pages__preview-tag[data-v-efe45d0e] {
5438
+ .mech-pages__preview-tag[data-v-56e0f852] {
5439
5439
  display: inline-flex;
5440
5440
  align-items: center;
5441
5441
  gap: 5px;
@@ -5449,13 +5449,13 @@ from {
5449
5449
  color: var(--mech-fg-alt);
5450
5450
  background: var(--mech-active);
5451
5451
  }
5452
- .mech-pages__preview-tag .vicon[data-v-efe45d0e] {
5452
+ .mech-pages__preview-tag .vicon[data-v-56e0f852] {
5453
5453
  width: 12px;
5454
5454
  height: 12px;
5455
5455
  color: var(--mech-muted);
5456
5456
  }
5457
5457
  @media (max-width: 1160px) {
5458
- .mech-pages__preview[data-v-efe45d0e] {
5458
+ .mech-pages__preview[data-v-56e0f852] {
5459
5459
  display: none;
5460
5460
  }
5461
5461
  }.mech-pagebar[data-v-43756991] {
package/dist/editor.js CHANGED
@@ -2133,7 +2133,8 @@ var _hoisted_18 = { class: "mech-pages__cell-name" };
2133
2133
  var _hoisted_19 = { class: "mech-pages__name" };
2134
2134
  var _hoisted_20 = {
2135
2135
  key: 0,
2136
- class: "mech-pages__badge is-draft"
2136
+ class: "mech-pages__badge is-draft",
2137
+ title: "Editable, but not on the site"
2137
2138
  };
2138
2139
  var _hoisted_21 = {
2139
2140
  key: 1,
@@ -2318,7 +2319,7 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2318
2319
  onClick: () => startDuplicate(page)
2319
2320
  },
2320
2321
  {
2321
- label: page.draft ? "Publish" : "Mark as draft",
2322
+ label: page.draft ? "Show on site" : "Hide from site",
2322
2323
  separatorBefore: true,
2323
2324
  onClick: () => void toggleDraft(page)
2324
2325
  },
@@ -2467,7 +2468,7 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2467
2468
  }, [
2468
2469
  createElementVNode("span", _hoisted_18, [
2469
2470
  createElementVNode("span", _hoisted_19, toDisplayString(page.name || page.path), 1),
2470
- page.draft ? (openBlock(), createElementBlock("span", _hoisted_20, "Draft")) : createCommentVNode("", true),
2471
+ page.draft ? (openBlock(), createElementBlock("span", _hoisted_20, "Hidden")) : createCommentVNode("", true),
2471
2472
  page.generated ? (openBlock(), createElementBlock("span", _hoisted_21, "Generated")) : createCommentVNode("", true),
2472
2473
  page.path === currentLogical.value ? (openBlock(), createElementBlock("span", _hoisted_22, "Current")) : createCommentVNode("", true),
2473
2474
  unref(localeConfig) && page.locales ? (openBlock(), createElementBlock("span", _hoisted_23, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(localeCodes), (code) => {
@@ -2519,7 +2520,7 @@ var PagesDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
2519
2520
  ])) : createCommentVNode("", true)])]);
2520
2521
  };
2521
2522
  }
2522
- }), [["__scopeId", "data-v-efe45d0e"]]);
2523
+ }), [["__scopeId", "data-v-56e0f852"]]);
2523
2524
  //#endregion
2524
2525
  //#region src/editor/components/PageBar.vue?vue&type=script&setup=true&lang.ts
2525
2526
  var _hoisted_1$3 = { class: "mech-pagebar__icon" };
package/dist/export.js CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as withHostedAssets, E as listPages, F as setPageCodec, H as assetFileOf, K as isDerivedAsset, S as getPagePath, V as UPLOADS_PREFIX$1, W as fsAssetStore, d as readFoldersData, f as readSiteData, i as toBlockMeta, nt as analyzeImageBuffer, p as readSiteLocaleOverride, r as composedBlockMeta, rt as hasSharp, t as buildRichTextCodec, u as readFolderData } from "./chunks/rich-text-codec-BSD35k2U.js";
1
+ import { $ as withHostedAssets, E as listPages, F as setPageCodec, H as assetFileOf, K as isDerivedAsset, S as getPagePath, V as UPLOADS_PREFIX$1, W as fsAssetStore, d as readFoldersData, f as readSiteData, i as toBlockMeta, nt as analyzeImageBuffer, p as readSiteLocaleOverride, r as composedBlockMeta, rt as hasSharp, t as buildRichTextCodec, u as readFolderData } from "./chunks/rich-text-codec-DCv1X0eZ.js";
2
2
  import { n as blockAssetLinks, t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-DsJXmoDs.js";
3
3
  import { dirname, join, parse, relative, resolve } from "node:path";
4
4
  import { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, applySeoTags, auditPageHtml, buildRobotsTxt, buildSitemap, findUnknownBlocks, generateProject, isPaginatedQuery, localePath, mergeTranslation, migrateContent, paginationVariantPath, registerFieldSchemas, resolveQueryKey, templateBlockIds, validateLinks, walkSchema, walkTree } from "mechanica-shared";
5
- import { parsePage } from "mechanica-shared/page-format";
5
+ import { PageParseError, parsePage } from "mechanica-shared/page-format";
6
6
  import { access, copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
7
7
  import { pathToFileURL } from "node:url";
8
8
  //#region src/cli/export.ts
@@ -13,6 +13,14 @@ function setHtmlLang(html, lang) {
13
13
  if (/<html\b[^>]*\blang\s*=/i.test(html)) return html;
14
14
  return html.replace(/<html\b([^>]*)>/i, `<html$1 lang="${lang}">`);
15
15
  }
16
+ /** Parse a page file's text, naming the file (relative to `.mech`) in a parse error. */
17
+ function parsePageFile(raw, file, richText) {
18
+ try {
19
+ return parsePage(raw, richText ? { richText } : void 0);
20
+ } catch (error) {
21
+ throw error instanceof PageParseError ? error.inFile(file) : error;
22
+ }
23
+ }
16
24
  /** Read every page JSON under `<mech>/pages` as an exportable page. */
17
25
  async function readPages(pagesDir, foldersData = {}, richText) {
18
26
  let entries;
@@ -30,7 +38,7 @@ async function readPages(pagesDir, foldersData = {}, richText) {
30
38
  const base = parsed.base.slice(0, -8);
31
39
  if (VARIANT_STEM_RE.test(base)) continue;
32
40
  const filePath = join(pagesDir, relative);
33
- const file = parsePage(await readFile(filePath, "utf-8"), richText ? { richText } : void 0);
41
+ const file = parsePageFile(await readFile(filePath, "utf-8"), `pages/${relative.replace(/\\/g, "/")}`, richText);
34
42
  if (file.draft) continue;
35
43
  const path = `/${dir}/${base === "index" ? "" : base}`.replace("//", "/").replace(/\/$/, "") || "/";
36
44
  const data = {
@@ -71,9 +79,9 @@ async function readTranslation(mechDir, base, locale, config, richText) {
71
79
  } catch {
72
80
  return null;
73
81
  }
74
- const doc = parsePage(raw, richText ? { richText } : void 0);
75
- if (doc.draft) return null;
76
82
  const rel = relative(join(mechDir, "pages"), file).replace(/\\/g, "/");
83
+ const doc = parsePageFile(raw, `pages/${rel}`, richText);
84
+ if (doc.draft) return null;
77
85
  const dir = dirname(rel) === "." ? "" : dirname(rel);
78
86
  let content = doc.content ?? [];
79
87
  let pageData = doc.data ?? {};
package/dist/plugin.js CHANGED
@@ -1,7 +1,7 @@
1
- import { a as collectGeneratedPages, n as configureSite, r as indexGeneratedPages, t as buildSiteManifest } from "./chunks/site-DfazSr3_.js";
2
- import { a as renderEditablePage, c as fillStateImages, d as loadComposedDefinitions, n as toNodeMiddleware, o as buildGeneratedState, r as createEditorService, s as buildPageState, t as toAssetMiddleware, u as collectComposed } from "./chunks/node-adapter-CSkDXYGO.js";
3
- import { W as fsAssetStore, et as withRemoteAssets, k as pageUrlOf, tt as wasRecentlyMutated } from "./chunks/rich-text-codec-BSD35k2U.js";
4
- import { i as uploadsIgnored, n as remoteAssetsFor } from "./chunks/assets-nujjbCx8.js";
1
+ import { a as collectGeneratedPages, n as configureSite, r as indexGeneratedPages, t as buildSiteManifest } from "./chunks/site-BVqx8iKj.js";
2
+ import { a as renderEditablePage, c as fillStateImages, d as loadComposedDefinitions, n as toNodeMiddleware, o as buildGeneratedState, r as createEditorService, s as buildPageState, t as toAssetMiddleware, u as collectComposed } from "./chunks/node-adapter-Bm_SloHQ.js";
3
+ import { W as fsAssetStore, et as withRemoteAssets, k as pageUrlOf, tt as wasRecentlyMutated } from "./chunks/rich-text-codec-DCv1X0eZ.js";
4
+ import { i as uploadsIgnored, n as remoteAssetsFor } from "./chunks/assets-5a2WdmFg.js";
5
5
  import { t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-DsJXmoDs.js";
6
6
  import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { join, relative } from "node:path";
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
- import { i as readSiteManifest, n as configureSite, t as buildSiteManifest } from "./chunks/site-DfazSr3_.js";
2
- import { a as renderEditablePage, i as readEditorTemplate, n as toNodeMiddleware, r as createEditorService, t as toAssetMiddleware } from "./chunks/node-adapter-CSkDXYGO.js";
3
- import { $ as withHostedAssets, B as memoryContentFiles, G as isAssetName, K as isDerivedAsset, O as pageOfFile, Q as uploadResponse, U as contentTypeOf, W as fsAssetStore, Z as uploadName, et as withRemoteAssets, z as fsContentFiles } from "./chunks/rich-text-codec-BSD35k2U.js";
1
+ import { i as readSiteManifest, n as configureSite, t as buildSiteManifest } from "./chunks/site-BVqx8iKj.js";
2
+ import { a as renderEditablePage, i as readEditorTemplate, n as toNodeMiddleware, r as createEditorService, t as toAssetMiddleware } from "./chunks/node-adapter-Bm_SloHQ.js";
3
+ import { $ as withHostedAssets, B as memoryContentFiles, G as isAssetName, K as isDerivedAsset, O as pageOfFile, Q as uploadResponse, U as contentTypeOf, W as fsAssetStore, Z as uploadName, et as withRemoteAssets, z as fsContentFiles } from "./chunks/rich-text-codec-DCv1X0eZ.js";
4
4
  import { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, uploadNameOf } from "mechanica-shared";
5
5
  export { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, buildSiteManifest, configureSite, contentTypeOf, createEditorService, fsAssetStore, fsContentFiles, isAssetName, isDerivedAsset, memoryContentFiles, pageOfFile, readEditorTemplate, readSiteManifest, renderEditablePage, toAssetMiddleware, toNodeMiddleware, uploadName, uploadNameOf, uploadResponse, withHostedAssets, withRemoteAssets };
@@ -12,8 +12,6 @@ export interface RequestOptions {
12
12
  }
13
13
  /** One request to the platform API; resolves with the parsed JSON body (or null). */
14
14
  export declare function request<T = any>(origin: string, method: string, path: string, options?: RequestOptions): Promise<T>;
15
- /** The saved token for `origin`, or an error telling the user to log in. */
16
- export declare function requireToken(origin: string): Promise<string>;
17
15
  export interface PlatformSite {
18
16
  uuid: string;
19
17
  slug: string;
@@ -9,3 +9,25 @@ export declare function hostOrigin(host: string | undefined): string;
9
9
  export declare function getToken(origin: string): Promise<string | null>;
10
10
  /** Remember the token for an origin, in a file only the user can read. */
11
11
  export declare function saveToken(origin: string, token: string): Promise<string>;
12
+ /**
13
+ * A sign-in started by `mechanica login` and not yet approved in the browser.
14
+ * The device code is the secret the platform hands the token to; the user
15
+ * code is what the link carries. Kept so that the next command can finish
16
+ * the sign-in once the link was approved — a coding agent runs `login`,
17
+ * shows the link, and goes on with `link` or `push` after the user's click.
18
+ */
19
+ export interface PendingLogin {
20
+ deviceCode: string;
21
+ userCode: string;
22
+ verificationUrl: string;
23
+ /** ISO date: when the platform forgets the request. */
24
+ expiresAt: string;
25
+ /** How often the platform wants to be asked, in seconds. */
26
+ interval: number;
27
+ }
28
+ /** The sign-in waiting for an origin, or null. An expired one is reported too: whoever reads it says so. */
29
+ export declare function getPendingLogin(origin: string): Promise<PendingLogin | null>;
30
+ /** Keep a started sign-in for an origin (replacing an earlier one). */
31
+ export declare function savePendingLogin(origin: string, login: PendingLogin): Promise<void>;
32
+ /** Forget the sign-in waiting for an origin — it was finished, denied or has expired. */
33
+ export declare function clearPendingLogin(origin: string): Promise<void>;
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * `mechanica login`: sign in to the platform and keep an access token for this
3
3
  * machine. With `--token` the given token is stored as is (CI, or a token made
4
- * in the dashboard); otherwise it asks for email + password and creates one.
4
+ * in the dashboard). Otherwise the sign-in happens in the browser (see
5
+ * `session.ts`): a link is printed, and the token arrives once it is approved.
6
+ *
7
+ * In a terminal the command waits for the approval. Without one — a coding
8
+ * agent runs it, and shows its output only once it has ended — it prints the
9
+ * link and returns: the next `mechanica link` / `push` finishes the sign-in.
10
+ * `--wait` / `--no-wait` decide either way.
5
11
  */
6
12
  export declare function runLogin(options: {
7
13
  host?: string;
8
14
  token?: string;
15
+ wait?: boolean;
9
16
  }): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { type PendingLogin } from './credentials';
2
+ /** Start a sign-in: the platform makes a request, its codes are kept for this origin. */
3
+ export declare function startDeviceLogin(origin: string): Promise<PendingLogin>;
4
+ /** What the user is shown: the link, and how long it stays open. */
5
+ export declare function describeDeviceLogin(origin: string, pending: PendingLogin): string;
6
+ export type DeviceLoginOutcome = {
7
+ status: 'pending';
8
+ } | {
9
+ status: 'approved';
10
+ token: string;
11
+ } | {
12
+ status: 'denied';
13
+ } | {
14
+ status: 'expired';
15
+ };
16
+ /** Ask the platform once whether the request was answered. */
17
+ export declare function checkDeviceLogin(origin: string, pending: PendingLogin): Promise<DeviceLoginOutcome>;
18
+ /**
19
+ * Finish a started sign-in if it was answered: save the token and say who
20
+ * signed in. Null while the request still waits. A denied or expired request
21
+ * is forgotten and reported — the user runs `mechanica login` for a new link.
22
+ */
23
+ export declare function completeDeviceLogin(origin: string, pending: PendingLogin): Promise<string | null>;
24
+ /** Wait for the approval, asking the platform every `interval` seconds until it comes or the link expires. */
25
+ export declare function waitForDeviceLogin(origin: string, pending: PendingLogin): Promise<string>;
26
+ /**
27
+ * The token for `origin`: the saved one, or — when `mechanica login` printed
28
+ * a link that has been approved since — the one the platform now hands over.
29
+ * Otherwise an error that says what to do next.
30
+ */
31
+ export declare function requireToken(origin: string): Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mechanica",
3
- "version": "2.1.0-beta.7",
3
+ "version": "2.1.0",
4
4
  "description": "Build Vue 3 websites with a visual block editor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -66,7 +66,7 @@
66
66
  "prepublishOnly": "bun ../../scripts/ensure-bun-publish.js && bun run build"
67
67
  },
68
68
  "dependencies": {
69
- "mechanica-shared": "2.1.0-beta.7",
69
+ "mechanica-shared": "2.1.0",
70
70
  "@vue/compiler-sfc": "^3.5.38",
71
71
  "compact-json-schema": "^0.1.5",
72
72
  "estree-walker": "^3.0.3",