@plitzi/cli 0.38.2 → 0.38.4

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/dist/index.js CHANGED
@@ -12,15 +12,27 @@ import { createServer } from "node:http";
12
12
  import tls from "node:tls";
13
13
  import readline from "node:readline/promises";
14
14
  import { readFileSync, readdirSync, watch } from "node:fs";
15
- import { EXPLAIN_KINDS, actionSpecFromEntry, actionToSource, authorSpace, blankSpaceSource, catalogTemplateFiles, comparePictures, darkScheme, elementTriggers, elementTypeNames, emptySpaceSource, explain, explainKindOf, explainList, explanationText, failedFlowText, importProbe, importedFiles, inspectDocument, inspectPage, pageRegions, planFixes, readDevTools, refusalOf } from "@plitzi/sdk-authoring";
15
+ import { EXPLAIN_KINDS, PAGE_TEXT_LIMIT, actionSpecFromEntry, actionToSource, authorSpace, blankSpaceSource, catalogTemplateFiles, comparePictures, compareTexts, darkScheme, dataIssues, elementTriggers, elementTypeNames, emptySpaceSource, explain, explainKindOf, explainList, explanationText, failedFlowText, importProbe, importedFiles, inspectDocument, inspectPage, loadImages, pageRegions, pageTexts, planFixes, readDevTools, refusalOf, unrollPage } from "@plitzi/sdk-authoring";
16
16
  import { promisify } from "node:util";
17
17
  import { lookup } from "node:dns/promises";
18
18
  import { build } from "esbuild";
19
19
  import { unzipSync, zipSync } from "fflate";
20
20
  import { gzipSync } from "node:zlib";
21
+ import { emitKeypressEvents } from "node:readline";
22
+ var t$3 = `[data-plitzi-devtools] { display: none !important; }`;
23
+ //#endregion
21
24
  //#region ../../packages/sdk-shared/dist/helpers/isRecord.mjs
22
25
  var e$1 = (e) => typeof e == "object" && !!e && !Array.isArray(e);
23
26
  //#endregion
27
+ //#region ../../packages/sdk-shared/dist/theme/themeCookie.mjs
28
+ var t$2 = "theme";
29
+ /** The port `npm start` took and the name its `/health` answers with: what `check`, `shot` and `visual` find it by. */
30
+ var DEV_SERVER_FILE = `tmp/dev-server.json`;
31
+ /** The space as last authored while developing, re-read by the server on every change: never the source of it. */
32
+ var DEV_SPACE_FILE = `tmp/space.json`;
33
+ /** What Playwright leaves of a run: traces, failures' pictures. */
34
+ var VISUAL_OUTPUT = `tmp/visual`;
35
+ //#endregion
24
36
  //#region src/browser/index.ts
25
37
  /**
26
38
  * A browser on the project's own Playwright, and the project's own server to point it at — what `plitzi check`,
@@ -54,7 +66,7 @@ var launchBrowser = async (root) => {
54
66
  /** What `npm start` wrote down when it took a port: the port, and the name its `/health` answers with. */
55
67
  var recorded = async (root) => {
56
68
  try {
57
- const value = JSON.parse(await readFile(path.join(root, ".plitzi/dev-server.json"), "utf8"));
69
+ const value = JSON.parse(await readFile(path.join(root, DEV_SERVER_FILE), "utf8"));
58
70
  if (!e$1(value)) return {};
59
71
  return {
60
72
  ...typeof value.port === "number" ? { port: value.port } : {},
@@ -83,6 +95,39 @@ var projectOrigin = async (root, project) => {
83
95
  var dataUrl = (png) => `data:image/png;base64,${Buffer.from(png).toString("base64")}`;
84
96
  /** A data URL's picture back as bytes, to write to a file. */
85
97
  var fromDataUrl = (url) => Buffer.from(url.slice(url.indexOf(",") + 1), "base64");
98
+ /**
99
+ * A page for the project's own server, in the theme asked for. The machine's preference alone is not enough: a space
100
+ * whose default is dark paints dark whatever the machine prefers, as it would for any visitor. So a scheme asked for
101
+ * is also the space's own choice — the `theme` cookie a visitor's `themeToggle` writes and the server paints `<html>`
102
+ * by — set before the page is asked for, so the first paint is already in it.
103
+ */
104
+ var openProjectPage = async (browser, origin, view) => {
105
+ const page = await browser.newPage({
106
+ viewport: {
107
+ width: view.width,
108
+ height: view.height
109
+ },
110
+ colorScheme: view.scheme ?? "light",
111
+ reducedMotion: view.reducedMotion ? "reduce" : "no-preference"
112
+ });
113
+ if (view.scheme) await page.context().addCookies([{
114
+ name: t$2,
115
+ value: view.scheme,
116
+ url: origin
117
+ }]);
118
+ return page;
119
+ };
120
+ /**
121
+ * The theme a page was painted in: the class on `<html>` (a space's own choice, or its default), else the one the
122
+ * machine prefers — which is what a page with neither follows.
123
+ */
124
+ var paintedScheme = (page, machine) => page.evaluate((prefers) => {
125
+ const { classList } = document.documentElement;
126
+ if (classList.contains("dark")) return "dark";
127
+ return classList.contains("light") ? "light" : prefers;
128
+ }, machine);
129
+ /** The page as a visitor sees it: the dev tools' own chrome — the badge, the panel — left out of whatever is looked at. */
130
+ var withoutDevTools = (page) => page.addStyleTag({ content: t$3 });
86
131
  //#endregion
87
132
  //#region src/commands/terminal.ts
88
133
  /**
@@ -1186,6 +1231,10 @@ const StatCard = ({ label = 'Metric', value = 0, unit = '', series, className }:
1186
1231
  * It is what makes this an ELEMENT rather than a component that happens to be on the page: the element's id and
1187
1232
  * classes land on it, so the CSS authored on it in \`src/space.ts\` applies, the builder can select it, a test
1188
1233
  * can find it by name, and interactions fire on it. A plain tag renders the same pixels and none of that.
1234
+ *
1235
+ * Its \`style\` is inline, and inline outranks every class: what \`CARD\` sets — its display, its padding — the CSS
1236
+ * authored on the element can never change. Keep it to what the element cannot render without, and leave layout and
1237
+ * position (\`position\`, \`width\`, \`margin\`) to the space's classes.
1189
1238
  */
1190
1239
  return (
1191
1240
  <RootElement className={className} style={CARD}>
@@ -1420,6 +1469,8 @@ var managerPackageFields = (manager) => manager === "npm" ? { allowScripts: {
1420
1469
  fsevents: false
1421
1470
  } } : {};
1422
1471
  var SDK_VERSION = `^${createRequire(import.meta.url)("@plitzi/sdk-authoring/package.json").version}`;
1472
+ /** The same version, bare: this CLI's own — what `--version` prints and what wrote a project's machinery. */
1473
+ var CLI_VERSION = SDK_VERSION.slice(1);
1423
1474
  /**
1424
1475
  * What every generated project builds and checks itself with.
1425
1476
  *
@@ -1478,8 +1529,8 @@ var NODE_ENGINES = { node: ">=22.18" };
1478
1529
  *
1479
1530
  * `client` runs Vite, so a save is a hot module replacement — the page updates without reloading, and editing the
1480
1531
  * space is a live loop. `server` runs the page server under Node's `--watch`: there is no client bundle of this
1481
- * project's own to hot-replace (the SDK is served by the server from its own copy), so a save restarts the
1482
- * process and the next request renders the change. Both are one command; only one of them is HMR, and calling
1532
+ * project's own to hot-replace (the SDK is served by the server from its own copy), so a save to the server's code
1533
+ * restarts the process — and a save to a local space is re-authored in place and the open page reloads. Both are one command; only one of them is HMR, and calling
1483
1534
  * the other one HMR would be a promise the loop does not keep.
1484
1535
  */
1485
1536
  var scripts = ({ mode, source }) => ({
@@ -1488,10 +1539,12 @@ var scripts = ({ mode, source }) => ({
1488
1539
  /**
1489
1540
  * Watched by PATH, not wholesale.
1490
1541
  *
1491
- * The server compiles the project's plugins into `.sdk-plugins/` and then IMPORTS what it built, so a
1492
- * bare `--watch` sees its own output land, restarts, compiles again, and never stops.
1542
+ * The server compiles the project's plugins into `tmp/.sdk-plugins/` and then IMPORTS what it built, so a
1543
+ * bare `--watch` sees its own output land, restarts, compiles again, and never stops. A local space is not
1544
+ * among them: `main.ts` re-authors it on save in a process of its own and the open pages load again
1545
+ * (`reloadPages`), so only the server's own code and the plugins restart it.
1493
1546
  */
1494
- "start:dev": "node --watch-path=./src src/main.ts",
1547
+ "start:dev": source === "local" ? "node --watch-path=./src/main.ts --watch-path=./src/plugins src/main.ts" : "node --watch-path=./src src/main.ts",
1495
1548
  /**
1496
1549
  * What production runs: the same entry compiled to JavaScript. Node strips types by loading a TypeScript
1497
1550
  * transformer into the process — ~10 MB a server keeps for its whole life to read one file — so a deployment
@@ -1583,10 +1636,12 @@ var tsconfig$1 = ({ mode }) => `${JSON.stringify({
1583
1636
  * writes `.yarn/install-state.gz` into the project, which is a cache and belongs in no repository.
1584
1637
  */
1585
1638
  var YARN_IGNORES$1 = "\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n";
1586
- /** Where the page server writes the plugin bundles it builds. A build output, and rebuilt whenever it is missing. */
1587
- var SERVER_IGNORES = ".sdk-plugins\n.plitzi\n";
1588
- var gitignore$1 = ({ mode, packageManager }) => `node_modules\ndist\n.env\nvisual/.results\nvisual/screenshots\n${mode === "server" ? SERVER_IGNORES : ""}${packageManager === "yarn" ? YARN_IGNORES$1 : ""}`;
1589
- var startLine = ({ mode, packageManager }) => mode === "server" ? `\`${runCommand(packageManager, "start")}\` serves pages on http://127.0.0.1:8080 — or, when something else holds 8080, the next free port, which it prints and writes to \`.plitzi/dev-server.json\` (set \`PORT\` to choose one). \`${runCommand(packageManager, "start:dev")}\` restarts on save. In production, \`${runCommand(packageManager, "build")}\` once and run \`${runCommand(packageManager, "start:prod")}\` with \`NODE_ENV=production\`: the compiled server, with no TypeScript in the process.` : `\`${runCommand(packageManager, "start")}\` runs Vite on http://127.0.0.1:5173, with hot module replacement.`;
1639
+ /**
1640
+ * `tmp/` is everything the project writes for itself (`./paths`); `.plitzi/` is what the CLI records about it, and is
1641
+ * committed — a clone without it could not pull, push or upgrade.
1642
+ */
1643
+ var gitignore$1 = ({ packageManager }) => `node_modules\ndist\n.env\ntmp\n${packageManager === "yarn" ? YARN_IGNORES$1 : ""}`;
1644
+ var startLine = ({ mode, packageManager, source }) => mode === "server" ? `\`${runCommand(packageManager, "start")}\` serves pages on http://127.0.0.1:8080 — or, when something else holds 8080, the next free port, which it prints and writes to \`${DEV_SERVER_FILE}\` (set \`PORT\` to choose one). \`${runCommand(packageManager, "start:dev")}\` restarts on a save to the server's code${source === "local" ? "; a save to the space reloads the open page" : ""}. In production, \`${runCommand(packageManager, "build")}\` once and run \`${runCommand(packageManager, "start:prod")}\` with \`NODE_ENV=production\`: the compiled server, with no TypeScript in the process.` : `\`${runCommand(packageManager, "start")}\` runs Vite on http://127.0.0.1:5173, with hot module replacement.`;
1590
1645
  var spaceSection = (answers) => {
1591
1646
  if (answers.source === "cloud") return `## Where the space comes from
1592
1647
 
@@ -1605,9 +1660,9 @@ from what is written there, so authoring it twice writes byte-identical document
1605
1660
 
1606
1661
  Nothing is fetched and nothing is signed in to: there is no account, no key and no network in the picture.
1607
1662
 
1608
- \`${runCommand(answers.packageManager, "author")}\` writes the documents out as \`space/offline-data.json\`, for the moment you want the space
1609
- somewhere else — imported into Plitzi, handed to another server, or checked into a repository with no TypeScript
1610
- in it. Nothing here reads that file; the declaration is the source.`;
1663
+ \`${runCommand(answers.packageManager, "author")}\` authors it and says what it found — warnings, suggestions, what a newer SDK changed. It
1664
+ writes nothing: the declaration is the source, and \`npx plitzi push\` puts it on a space in Plitzi when you want it
1665
+ there.`;
1611
1666
  };
1612
1667
  var readme$1 = (answers) => `# ${answers.name}
1613
1668
 
@@ -1623,6 +1678,16 @@ ${startLine(answers)}
1623
1678
 
1624
1679
  ${spaceSection(answers)}
1625
1680
 
1681
+ ## Folders that are not the source
1682
+
1683
+ - \`public/\` is served to anyone who asks, as it is: every file in it is on the internet once the project is deployed.
1684
+ Data and pictures for every visitor go there — never a secret, a key, a private document or what only some visitors
1685
+ may read.
1686
+ - \`tmp/\` is what the project writes for itself while it runs — the plugins it builds, the port it took, test
1687
+ output. Ignored by git, and rebuilt whenever it is missing.
1688
+ - \`.plitzi/\` is what the CLI records about the project — the space it came from, its functions' working copy, the
1689
+ files \`create\` wrote — so \`plitzi pull\`, \`push\` and \`upgrade\` know where they stand. Commit it.
1690
+
1626
1691
  ## The skills
1627
1692
 
1628
1693
  \`.claude/skills/\` carries Plitzi's authoring skill, so an agent working in this repository knows how a space is
@@ -1648,10 +1713,10 @@ var agentsFile = (answers) => {
1648
1713
  `| ${run("visual")} | open the page in a browser and check it rendered |`
1649
1714
  ];
1650
1715
  const zeroWarnings = local ? `Zero warnings from ${run("author")}.` : "Zero warnings from authoring.";
1651
- const port = answers.mode === "server" ? `${run("start")} serves on 8080, or on the next free port when something else holds it — printed, and written to ${code(".plitzi/dev-server.json")}, where ${code("check")}, ${code("shot")} and ${code("visual")} read it. ${code("PORT")} chooses one.` : `${run("start")} runs Vite on 5173.`;
1716
+ const port = answers.mode === "server" ? `${run("start")} serves on 8080, or on the next free port when something else holds it — printed, and written to ${code(DEV_SERVER_FILE)}, where ${code("check")}, ${code("shot")} and ${code("visual")} read it. ${code("PORT")} chooses one.` : `${run("start")} runs Vite on 5173.`;
1652
1717
  const pictures = answers.mode === "server" ? `- **Pictures from other sites** are resized by this server once ${code("src/main.ts")} names their hosts — ${code("images: { domains }")} in ${code("createServer")}, a list of hosts like ${code("images.example.com")} — and ${code("sharp")} is installed: an ${code("image")} then offers a ${code("srcset")} (give it ${code("sizes")}, and ${code("width")}/${code("height")} so nothing jumps).\n` : "";
1653
- const serverData = answers.mode === "server" ? `; one whose ${code("runtime")} is ${code("server")} is read by the server, and the page arrives with it` : "";
1654
- const generated = [...local ? [`- ${code("space/offline-data.json")} — what ${run("author")} writes out; the space is ${code("src/space.ts")}.`] : [], ...answers.mode === "server" ? [`- ${code(".sdk-plugins/")} and ${code(".plitzi/")} — built and written by the server.`] : []];
1718
+ const serverData = answers.mode === "server" ? `; one whose ${code("runtime")} is ${code("server")} is read by the server, and the page arrives with it — bound the same way (${code("products.data.items")}), on a page or a layout, never inside a component` : "";
1719
+ const generated = [`- ${code(`tmp/`)} — what the project writes for itself while it runs: the plugins it built, the port it took${local ? ", the space as last authored" : ""}, test output. Never committed, rebuilt when missing.`, `- ${code(".plitzi/")} — what the CLI records about the project: where it came from, what it wrote. Committed; the CLI's to change.`];
1655
1720
  return `# ${answers.name} — notes for agents
1656
1721
 
1657
1722
  A Plitzi space, rendered ${answers.mode === "server" ? "by the server in this project (SSR + RSC)" : "in the browser by a Vite app"}.
@@ -1667,7 +1732,8 @@ ${commands.join("\n")}
1667
1732
 
1668
1733
  - **Port.** ${port}
1669
1734
  - **Data with no backend** goes in ${code("public/data/*.json")}, served as it is and read by an ${code("apiContainer")} whose ${code("query")} is ${code("/data/products.json")}${serverData}.
1670
- ${pictures}- **Check a page in text first:** ${code(`${runCommand(answers.packageManager, "check")} -- / --width 1440,390`)} says whether every element is on screen, nothing overflows and the console is clean — a picture only when it says something is wrong: ${code(`${runCommand(answers.packageManager, "shot")} -- / --width 390`)} (add ${code("--scheme dark")}; ${code("--frames 4")} to see what moves; ${code("--compare <url>")} against another site, by section). ${run("visual")} runs the checks as tests.
1735
+ - **${code("public/")} is on the internet.** Every file in it is served to anyone who asks for it, as it is, the moment the project is deployed — no sign-in, no check. Never put in it a secret, a key, a ${code(".env")}, a private document, a database dump, or data only some visitors may read: that goes through a server action or a provider that checks who is asking.
1736
+ ${pictures}- **Check a page in text first:** ${code(`${runCommand(answers.packageManager, "check")} -- / --width 1440,390`)} says whether every element is on screen, nothing overflows and the console is clean — a picture only when it says something is wrong: ${code(`${runCommand(answers.packageManager, "shot")} -- / --width 390`)} (add ${code("--scheme dark")}; ${code("--frames 4")} to see what moves; ${code("--compare <url>")} against another site: by section, and each text measured). ${run("visual")} runs the checks as tests.
1671
1737
  - **What the page holds, in text:** ${code(`${runCommand(answers.packageManager, "check")} -- /products --state --element <id>`)} adds its state, every source by name and one element (what it reads, its own state, whether it is on screen); every check already lists the flows that failed. Read it instead of guessing from classes in the DOM.
1672
1738
 
1673
1739
  ## Do not read
@@ -1685,11 +1751,22 @@ layouts, data, templates and flows. The types of ${code("@plitzi/sdk-authoring")
1685
1751
  For a component of your own — a plugin — or anything about packing or uploading one, read
1686
1752
  ${code(".claude/skills/plitzi-cli/SKILL.md")} first: ${code("plitzi add plugin")} writes it in the shape everything reads.
1687
1753
 
1754
+ ## Keep the project clean
1755
+
1756
+ What you leave behind is the next reader's problem — the user's, or the next agent's. Before calling a change done:
1757
+
1758
+ - **Nothing unused.** Delete what you made and no longer use — a file, a page, a component, a class, a token, a data file, an import, a plugin folder. No commented-out code, no ${code("console.log")} left from debugging, no copy of a file kept "just in case": git keeps the history.
1759
+ - **Scratch goes in ${code(`tmp/`)}, or nowhere.** A one-off script, a dump, a picture to look at — never at the root or beside the source, where it reads as part of the project.
1760
+ - **One of everything.** A look used twice is a class; a value used twice is a token; a block used twice is a component, and rows of data are one list. Change it where it is defined, and rename everywhere when you rename.
1761
+ - **Files a reader can find.** One part per file, named after what it is, in the folder of its kind — the shape ${code("src/space.ts")} already has. Do not start a parallel layout of your own.
1762
+ - **Leave it passing.** ${local ? `${run("author")} with zero warnings, ` : ""}${run("typecheck")}, ${run("lint")} and ${run("format")} clean, and the page checked (${run("check")}).
1763
+
1688
1764
  ## The rules that go wrong most
1689
1765
 
1690
1766
  - Never write schema/style JSON by hand; author it. A refusal names the fix — fix the declaration; ${code("npx plitzi fix --write")} writes the ones with a single reading.
1767
+ - After the ${code("@plitzi/*")} packages move, ${code("npx plitzi upgrade")}: what this project's CLI files, scripts, skills and renamed names should now be — ${code("--write")} makes it, a file you changed comes as a diff.
1691
1768
  - ${zeroWarnings}
1692
- - Chrome shared by pages is a layout; a look used twice is a class; a repeated block is a function or a ${code("map")}.
1769
+ - Chrome shared by pages is a layout; a look used twice is a class; a block placed again with other content is a component, and rows of data are one ${code("list")} (a short menu may be a ${code("map")} in code).
1693
1770
  - Ids are one namespace for the whole space: name what is referred to; a helper that runs more than once builds inside ${code("scope()")}.
1694
1771
  - A file per part — the tokens, the layout, each component, each page — short enough to read whole; ${code("src/space.ts")} assembles them. ${code("npx plitzi create <dir> --template catalog")} is a complete example of the shape.
1695
1772
  - Rebuilding a page the user owns: ${code("npx plitzi import <url>")} writes its tokens, outline and lists as a start — then split it into parts and write the content. It reads only a site whose domain the user verified on one of their spaces.
@@ -1756,14 +1833,8 @@ var prettierrc = () => `${JSON.stringify({
1756
1833
  arrowParens: "avoid",
1757
1834
  endOfLine: "lf"
1758
1835
  }, null, 2)}\n`;
1759
- /** What a project writes that is nobody's to format or lint: its builds, its authored documents, its test output. */
1760
- var PROJECT_OUTPUTS = [
1761
- "dist",
1762
- "space",
1763
- ".sdk-plugins",
1764
- "visual/.results",
1765
- "visual/screenshots"
1766
- ];
1836
+ /** What a project writes that is nobody's to format or lint: its builds, and everything it writes for itself. */
1837
+ var PROJECT_OUTPUTS = ["dist", "tmp"];
1767
1838
  /**
1768
1839
  * What is the agents' and not the project's: the skills copied in, whose recipes are TypeScript no program of the
1769
1840
  * project includes — linted with types, they would stop the lint before its first file.
@@ -1772,7 +1843,7 @@ var AGENT_FILES = ".claude";
1772
1843
  var prettierignore = (outputs) => `${[
1773
1844
  "node_modules",
1774
1845
  AGENT_FILES,
1775
- ...outputs.filter((output) => output !== ".sdk-plugins")
1846
+ ...outputs
1776
1847
  ].join("\n")}\n`;
1777
1848
  /**
1778
1849
  * Flat config, and only what earns its place.
@@ -1883,13 +1954,14 @@ const PORT = process.env.PORT
1883
1954
  : await freePort(8080, HOST);`;
1884
1955
  /** Where the server says it is: the port it took, for `npm run shot` and the visual tests to find. */
1885
1956
  var LISTEN_SNIPPET = `server.listen(PORT, HOST);
1886
- mkdirSync(path.join(PROJECT_ROOT, '.plitzi'), { recursive: true });
1957
+ mkdirSync(path.join(PROJECT_ROOT, 'tmp'), { recursive: true });
1887
1958
  writeFileSync(
1888
- path.join(PROJECT_ROOT, '.plitzi/dev-server.json'),
1959
+ path.join(PROJECT_ROOT, '${DEV_SERVER_FILE}'),
1889
1960
  \`\${JSON.stringify({ name: SERVER_NAME, port: PORT, url: \`http://127.0.0.1:\${PORT}\` }, null, 2)}\\n\`
1890
1961
  );
1891
1962
  console.log(\`pages on http://127.0.0.1:\${PORT}/\`);`;
1892
- var localMain = () => `import { mkdirSync, readdirSync, writeFileSync } from 'node:fs';
1963
+ var localMain = () => `import { spawn } from 'node:child_process';
1964
+ import { mkdirSync, readdirSync, watch, writeFileSync } from 'node:fs';
1893
1965
  import path from 'node:path';
1894
1966
 
1895
1967
  import {
@@ -1902,6 +1974,7 @@ import {
1902
1974
  } from '@plitzi/sdk-server';
1903
1975
 
1904
1976
  import { authorSpace } from '@plitzi/sdk-authoring';
1977
+ import { publicData } from '@plitzi/sdk-authoring/node';
1905
1978
 
1906
1979
  import { declarations } from './plugins/declarations.ts';
1907
1980
  import { space } from './space.ts';
@@ -1916,7 +1989,11 @@ ${PORT_SNIPPET}
1916
1989
  * here for the same reason: this restart is the output somebody editing the space is actually watching.
1917
1990
  */
1918
1991
  // \`declarations\`: what the project's plugins fire, answer and read, so the space's use of them is checked too.
1919
- const { schema, style, warnings } = authorSpace(space, { plugins: declarations });
1992
+ // \`data\`: the files of \`public/\` a provider reads, so a binding onto a path one of them does not have is warned.
1993
+ const { schema, style, warnings } = authorSpace(space, {
1994
+ plugins: declarations,
1995
+ data: publicData(new URL('../public/', import.meta.url))
1996
+ });
1920
1997
  const offlineData = { schema, style };
1921
1998
 
1922
1999
  for (const warning of warnings) {
@@ -1925,6 +2002,19 @@ for (const warning of warnings) {
1925
2002
 
1926
2003
  ${PLUGINS}
1927
2004
 
2005
+ /**
2006
+ * While developing, the documents are served from \`${DEV_SPACE_FILE}\`, which the server reads again whenever it changes —
2007
+ * so a save is re-authored into it and shown without restarting anything (see \`watchSpace\` below). A by-product, in
2008
+ * \`tmp/\` with everything else the project writes for itself: the space is \`src/space.ts\`. A deployment keeps the
2009
+ * documents in memory, as authored at boot.
2010
+ */
2011
+ const DEVELOPING = process.env.NODE_ENV !== 'production';
2012
+ const OFFLINE_DATA = path.join(PROJECT_ROOT, '${DEV_SPACE_FILE}');
2013
+ if (DEVELOPING) {
2014
+ mkdirSync(path.dirname(OFFLINE_DATA), { recursive: true });
2015
+ writeFileSync(OFFLINE_DATA, \`\${JSON.stringify(offlineData, null, 2)}\\n\`);
2016
+ }
2017
+
1928
2018
  /**
1929
2019
  * This project's own server code: \`functions/\` — what \`plitzi functions pull\` writes and \`push\` sends — built
1930
2020
  * the way Plitzi builds a space's and run here, in this process. Nothing there, no functions; code that does not build
@@ -1939,14 +2029,16 @@ const functions = await loadFunctions(new URL('../functions/', import.meta.url))
1939
2029
  * page server makes. A real deployment swaps this for adapters onto its own database, or for
1940
2030
  * \`createCloudAdapters\` to read the live space out of Plitzi — the server never learns the difference.
1941
2031
  */
1942
- // What \`/health\` answers with, and \`.plitzi/dev-server.json\` records: how a tool knows it reached THIS project.
2032
+ // What \`/health\` answers with, and \`${DEV_SERVER_FILE}\` records: how a tool knows it reached THIS project.
1943
2033
  const SERVER_NAME = schema.definition.permanentUrl;
1944
2034
  const server = createServer({
1945
2035
  port: PORT,
1946
2036
  devMode: process.env.NODE_ENV !== 'production',
2037
+ // A save to the space re-authors it and the open pages load again (\`watchSpace\` below).
2038
+ devReload: DEVELOPING,
1947
2039
  health: { name: SERVER_NAME },
1948
2040
  adapters: createJsonAdapters({
1949
- offlineData,
2041
+ offlineData: DEVELOPING ? OFFLINE_DATA : offlineData,
1950
2042
  deployment: { spaceId: 1, environment: 'main', revision: 0, pluginNames }
1951
2043
  }),
1952
2044
  plugins,
@@ -1966,6 +2058,56 @@ ${LISTEN_SNIPPET}
1966
2058
  * queue for whichever server runs next. A second ^C exits at once.
1967
2059
  */
1968
2060
  closeOnSignals(server);
2061
+
2062
+ /**
2063
+ * A save to the space, while developing: re-authored by \`src/author.ts\` in a process of its own — the only way to
2064
+ * read every file of it again, which an import never does twice — and every open page loads again once it wrote the
2065
+ * new documents. What it refuses is printed and the page keeps the last space that authored. A change to this file or
2066
+ * to a plugin restarts the server instead (\`start:dev\` watches those).
2067
+ */
2068
+ const RESTARTS = ['main.ts', 'plugins'];
2069
+ const watchSpace = (): void => {
2070
+ let timer: ReturnType<typeof setTimeout> | undefined;
2071
+ let authoring = false;
2072
+ let again = false;
2073
+ const author = (): void => {
2074
+ if (authoring) {
2075
+ again = true;
2076
+
2077
+ return;
2078
+ }
2079
+
2080
+ authoring = true;
2081
+ const child = spawn(process.execPath, [path.join(PROJECT_ROOT, 'src/author.ts'), '--out', OFFLINE_DATA], {
2082
+ cwd: PROJECT_ROOT,
2083
+ stdio: 'inherit'
2084
+ });
2085
+ child.on('close', code => {
2086
+ authoring = false;
2087
+ if (code === 0) {
2088
+ server.reloadPages();
2089
+ }
2090
+
2091
+ if (again) {
2092
+ again = false;
2093
+ author();
2094
+ }
2095
+ });
2096
+ };
2097
+
2098
+ watch(path.join(PROJECT_ROOT, 'src'), { recursive: true }, (_event, file) => {
2099
+ if (!file || RESTARTS.some(name => file === name || file.startsWith(\`\${name}\${path.sep}\`))) {
2100
+ return;
2101
+ }
2102
+
2103
+ clearTimeout(timer);
2104
+ timer = setTimeout(author, 100);
2105
+ });
2106
+ };
2107
+
2108
+ if (DEVELOPING) {
2109
+ watchSpace();
2110
+ }
1969
2111
  `;
1970
2112
  var cloudMain = (name) => `import { mkdirSync, readdirSync, writeFileSync } from 'node:fs';
1971
2113
  import path from 'node:path';
@@ -2015,7 +2157,7 @@ const functions = await loadFunctions(new URL('../functions/', import.meta.url))
2015
2157
  * on every request; a published environment with no \`revision\` serves the latest and releases itself; with a
2016
2158
  * \`revision\` it serves exactly that version, for a deployment that rolls forward on its own schedule.
2017
2159
  */
2018
- // What \`/health\` answers with, and \`.plitzi/dev-server.json\` records: how a tool knows it reached THIS project.
2160
+ // What \`/health\` answers with, and \`${DEV_SERVER_FILE}\` records: how a tool knows it reached THIS project.
2019
2161
  const SERVER_NAME = ${JSON.stringify(name)};
2020
2162
  const server = createServer({
2021
2163
  port: PORT,
@@ -2109,14 +2251,14 @@ var skillVersion = (skill) => /^version: (.+)$/m.exec(skill)?.[1]?.trim();
2109
2251
  * sit beside it and are opened when the task needs them. Copying \`SKILL.md\` alone left every one of those links
2110
2252
  * pointing at nothing.
2111
2253
  */
2112
- var filesUnder$1 = (root, folder = root) => readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
2254
+ var filesUnder$2 = (root, folder = root) => readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
2113
2255
  const path = join(folder, entry.name);
2114
- if (entry.isDirectory()) return filesUnder$1(root, path);
2256
+ if (entry.isDirectory()) return filesUnder$2(root, path);
2115
2257
  return entry.isFile() ? [[relative(root, path).split(sep).join("/"), readFileSync(path, "utf-8")]] : [];
2116
2258
  });
2117
2259
  /**
2118
2260
  * The skills a project carries: both for a project that renders a space, the CLI's alone for a plugin package. With
2119
- * `root`, from the packages that project installed — what `plitzi skills update` brings it up to.
2261
+ * `root`, from the packages that project installed — what `plitzi upgrade skills` brings it up to.
2120
2262
  */
2121
2263
  var skillFiles = (names = SKILL_NAMES, root) => {
2122
2264
  const resolvers = resolversFor(root);
@@ -2125,7 +2267,7 @@ var skillFiles = (names = SKILL_NAMES, root) => {
2125
2267
  const entry = resolveFrom(resolvers, `${SKILLS[name]}/skills/${name}/SKILL.md`);
2126
2268
  if (!entry) continue;
2127
2269
  const version = versionOf(resolvers, SKILLS[name]);
2128
- for (const [path, content] of filesUnder$1(dirname(entry))) files[`.claude/skills/${name}/${path}`] = path === "SKILL.md" && version ? stampVersion(content, version) : content;
2270
+ for (const [path, content] of filesUnder$2(dirname(entry))) files[`.claude/skills/${name}/${path}`] = path === "SKILL.md" && version ? stampVersion(content, version) : content;
2129
2271
  }
2130
2272
  return files;
2131
2273
  };
@@ -2143,24 +2285,27 @@ var skillFiles = (names = SKILL_NAMES, root) => {
2143
2285
  * signing up gives you cannot come apart.
2144
2286
  */
2145
2287
  /**
2146
- * Writes the declaration out as documents.
2288
+ * Authors the declaration and says what it found: the check an agent and a person run after every change.
2147
2289
  *
2148
- * Nothing in the project reads it — the server and the browser both author at boot — so it exists for the moment
2149
- * the space has to go somewhere else: imported into Plitzi, handed to another server, or checked into a
2150
- * repository with no TypeScript in it.
2290
+ * It writes nothing — the space is \`src/space.ts\`, and the server and the browser both author it at boot. Only the
2291
+ * server asks for the documents, while developing: \`--out <file>\` writes them where it re-reads them on a save.
2151
2292
  */
2152
2293
  var authorScript = () => `import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2153
2294
  import { createRequire } from 'node:module';
2295
+ import path from 'node:path';
2154
2296
 
2155
2297
  import { authorSpace, planFixes, refusalOf, SpaceRefusedError } from '@plitzi/sdk-authoring';
2298
+ import { publicData } from '@plitzi/sdk-authoring/node';
2156
2299
 
2157
2300
  import { declarations } from './plugins/declarations.ts';
2158
2301
  import { space } from './space.ts';
2159
2302
 
2160
2303
  // \`--json\` answers in one object, for a tool or an agent; otherwise one line on success, the problems on failure.
2161
2304
  const json = process.argv.includes('--json');
2305
+ // \`--out <file>\`: where to write the documents — the server's, while developing. Left out, nothing is written.
2306
+ const out = process.argv.includes('--out') ? process.argv[process.argv.indexOf('--out') + 1] : undefined;
2162
2307
 
2163
- /** How many of the problems said have one fix \`plitzi fix\` can write in this source — and the line that says so. */
2308
+ /** How many of the warnings and suggestions said have one fix \`plitzi fix\` can write in this source — and the line that says so. */
2164
2309
  const fixableHint = (): string | undefined => {
2165
2310
  let count: number;
2166
2311
  try {
@@ -2172,18 +2317,21 @@ const fixableHint = (): string | undefined => {
2172
2317
  return count === 0 ? undefined : \`[fix] \${count} of these have one fix: npx plitzi fix shows it in your source, --write writes it\`;
2173
2318
  };
2174
2319
 
2175
- /**
2176
- * The skill an agent reads was copied from the SDK at one version: a newer SDK has what the older skill never taught.
2177
- * Both versions, when they differ — \`npx @plitzi/cli skills update\` brings the skill up to the SDK.
2178
- */
2179
- const outdatedSkill = (): { skill: string; sdk: string } | undefined => {
2180
- let skill: string | undefined;
2320
+ /** A file's text, or undefined when there is none to read. */
2321
+ const readOptional = (file: string): string | undefined => {
2181
2322
  try {
2182
- skill = /^version: (.+)$/m.exec(readFileSync('.claude/skills/plitzi-authoring/SKILL.md', 'utf-8'))?.[1]?.trim();
2323
+ return readFileSync(file, 'utf-8');
2183
2324
  } catch {
2184
2325
  return undefined;
2185
2326
  }
2327
+ };
2186
2328
 
2329
+ /**
2330
+ * What of the project is older than the SDK installed: the skill an agent reads, copied from the SDK at one version,
2331
+ * and the files the CLI wrote (\`.plitzi/scaffold.json\`) — a newer SDK has what neither taught nor wired. Each version
2332
+ * that differs — \`npx plitzi upgrade\` brings both up.
2333
+ */
2334
+ const outdated = (): { skill?: string; files?: string; sdk: string } | undefined => {
2187
2335
  const manifest: unknown = JSON.parse(
2188
2336
  readFileSync(createRequire(import.meta.url).resolve('@plitzi/sdk-authoring/package.json'), 'utf-8')
2189
2337
  );
@@ -2191,44 +2339,64 @@ const outdatedSkill = (): { skill: string; sdk: string } | undefined => {
2191
2339
  typeof manifest === 'object' && manifest !== null && 'version' in manifest && typeof manifest.version === 'string'
2192
2340
  ? manifest.version
2193
2341
  : undefined;
2342
+ const skill = /^version: (.+)$/m.exec(readOptional('.claude/skills/plitzi-authoring/SKILL.md') ?? '')?.[1]?.trim();
2343
+ const record: unknown = JSON.parse(readOptional('.plitzi/scaffold.json') ?? 'null');
2344
+ const files =
2345
+ typeof record === 'object' && record !== null && 'cli' in record && typeof record.cli === 'string'
2346
+ ? record.cli
2347
+ : undefined;
2348
+ if (!sdk || ((!skill || skill === sdk) && (!files || files === sdk))) {
2349
+ return undefined;
2350
+ }
2194
2351
 
2195
- return skill && sdk && skill !== sdk ? { skill, sdk } : undefined;
2352
+ return { ...(skill && skill !== sdk ? { skill } : {}), ...(files && files !== sdk ? { files } : {}), sdk };
2196
2353
  };
2197
2354
 
2198
2355
  try {
2199
- const { schema, style, warnings, suggestions } = authorSpace(space, { plugins: declarations });
2200
- mkdirSync('space', { recursive: true });
2201
- writeFileSync('space/offline-data.json', \`\${JSON.stringify({ schema, style }, null, 2)}\\n\`);
2356
+ // \`data\`: the files of \`public/\` a provider reads, so a binding onto a path one of them does not have is said here.
2357
+ const { schema, style, warnings, suggestions } = authorSpace(space, {
2358
+ plugins: declarations,
2359
+ data: publicData(new URL('../public/', import.meta.url))
2360
+ });
2361
+ if (out) {
2362
+ mkdirSync(path.dirname(out), { recursive: true });
2363
+ writeFileSync(out, \`\${JSON.stringify({ schema, style }, null, 2)}\\n\`);
2364
+ }
2202
2365
 
2203
- const outdated = outdatedSkill();
2366
+ const behind = outdated();
2204
2367
  if (json) {
2205
2368
  console.log(
2206
- JSON.stringify({ ok: true, pages: schema.pages.length, warnings, suggestions, ...(outdated ? { outdated } : {}) })
2369
+ JSON.stringify({ ok: true, pages: schema.pages.length, warnings, suggestions, ...(behind ? { outdated: behind } : {}) })
2207
2370
  );
2208
2371
  } else {
2209
2372
  for (const warning of warnings) {
2210
2373
  console.warn(\`[author] \${warning.code} · \${warning.message}\`);
2211
2374
  }
2212
2375
 
2213
- const hint = warnings.length > 0 ? fixableHint() : undefined;
2214
- if (hint) {
2215
- console.warn(hint);
2216
- }
2217
-
2218
2376
  // Not problems: a shorter way to the same page, the one that saves the most elements first.
2219
2377
  for (const suggestion of suggestions) {
2220
2378
  const at = suggestion.at ? \` · \${suggestion.at}\` : '';
2221
2379
  console.warn(\`[suggest] \${suggestion.code} · \${suggestion.message} (saves \${suggestion.saves})\${at}\`);
2222
2380
  }
2223
2381
 
2224
- if (outdated) {
2382
+ // After both: \`plitzi fix\` writes a warning's fix and a suggestion's alike, where it has one reading.
2383
+ const hint = warnings.length + suggestions.length > 0 ? fixableHint() : undefined;
2384
+ if (hint) {
2385
+ console.warn(hint);
2386
+ }
2387
+
2388
+ if (behind) {
2389
+ const older = [
2390
+ behind.skill ? \`the authoring skill is \${behind.skill}\` : '',
2391
+ behind.files ? \`its files are from \${behind.files}\` : ''
2392
+ ].filter(Boolean);
2225
2393
  console.warn(
2226
- \`[skills] the authoring skill is \${outdated.skill} and @plitzi/sdk-authoring is \${outdated.sdk}: npx @plitzi/cli skills update\`
2394
+ \`[upgrade] \${older.join(' and ')}, and @plitzi/sdk-authoring is \${behind.sdk}: npx plitzi upgrade shows what changes, --write makes it\`
2227
2395
  );
2228
2396
  }
2229
2397
 
2230
2398
  console.log(
2231
- \`ok · \${schema.pages.length} pages · \${warnings.length} warnings · \${suggestions.length} suggestions · space/offline-data.json\`
2399
+ \`ok · \${schema.pages.length} pages · \${warnings.length} warnings · \${suggestions.length} suggestions\`
2232
2400
  );
2233
2401
  }
2234
2402
  } catch (error) {
@@ -2327,7 +2495,7 @@ var spaceFiles = (answers) => {
2327
2495
  var RECORDED = `/** What \`npm start\` wrote down when it took a port: the port, and the name its \`/health\` answers with. */
2328
2496
  const recorded = (): { port?: number; name?: string } => {
2329
2497
  try {
2330
- const value: unknown = JSON.parse(readFileSync('.plitzi/dev-server.json', 'utf8'));
2498
+ const value: unknown = JSON.parse(readFileSync('${DEV_SERVER_FILE}', 'utf8'));
2331
2499
  if (typeof value !== 'object' || value === null) {
2332
2500
  return {};
2333
2501
  }
@@ -2351,7 +2519,7 @@ const PORT = Number(process.env.PORT ?? recorded().port ?? ${mode === "server" ?
2351
2519
 
2352
2520
  export default defineConfig({
2353
2521
  testDir: './visual',
2354
- outputDir: './visual/.results',
2522
+ outputDir: './${VISUAL_OUTPUT}',
2355
2523
  use: { baseURL: \`http://127.0.0.1:\${PORT}\` },
2356
2524
  // Playwright starts the project itself, so \`${runCommand(packageManager, "visual")}\` is one command from a
2357
2525
  // cold checkout.
@@ -2703,7 +2871,7 @@ var tsconfig = () => `{
2703
2871
  `;
2704
2872
  /** Yarn's own recommendation: with the `node-modules` linker it writes a cache into `.yarn/` that no repository wants. */
2705
2873
  var YARN_IGNORES = "\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n";
2706
- var gitignore = ({ packageManager, inProject }) => `node_modules\ndist\n*.zip\nvisual/.results\n${packageManager === "yarn" && !inProject ? YARN_IGNORES : ""}`;
2874
+ var gitignore = ({ packageManager, inProject }) => `node_modules\ndist\n*.zip\ntmp\n${packageManager === "yarn" && !inProject ? YARN_IGNORES : ""}`;
2707
2875
  var packageFiles = (names, answers) => ({
2708
2876
  "package.json": packageJson(names, answers),
2709
2877
  "tsconfig.json": tsconfig(),
@@ -2842,7 +3010,7 @@ var previewFiles = ([main, ...others]) => ({
2842
3010
  //#endregion
2843
3011
  //#region ../../packages/sdk-shared/dist/helpers/scroll.mjs
2844
3012
  var e = ["smooth", "auto"];
2845
- var t = [
3013
+ var t$1 = [
2846
3014
  "start",
2847
3015
  "center",
2848
3016
  "end",
@@ -2958,13 +3126,13 @@ var a$1 = {
2958
3126
  type: "select",
2959
3127
  description: "Where the element lands, top to bottom.",
2960
3128
  default: "start",
2961
- options: [...t]
3129
+ options: [...t$1]
2962
3130
  },
2963
3131
  inline: {
2964
3132
  type: "select",
2965
3133
  description: "Where it lands, side to side.",
2966
3134
  default: "nearest",
2967
- options: [...t]
3135
+ options: [...t$1]
2968
3136
  },
2969
3137
  behavior: r$2
2970
3138
  }
@@ -3473,6 +3641,10 @@ const ${name} = ({ label = ${tsString(title)}, start = 0, step = 1, className }:
3473
3641
  * It is what makes this an ELEMENT rather than a component that happens to be on the page: the element's id and
3474
3642
  * classes land on it, so the CSS authored on it applies, the builder can select it, a test can find it by name, and
3475
3643
  * the events and actions above are this element's.
3644
+ *
3645
+ * Its \`style\` is inline, and inline outranks every class: what \`CARD\` sets — its display, its padding — the CSS
3646
+ * authored on the element can never change. Keep it to what the element cannot render without, and leave layout and
3647
+ * position (\`position\`, \`width\`, \`margin\`) to the space's classes.
3476
3648
  */
3477
3649
  return (
3478
3650
  <RootElement
@@ -3684,7 +3856,7 @@ const PORT = 5173;
3684
3856
 
3685
3857
  export default defineConfig({
3686
3858
  testDir: './visual',
3687
- outputDir: './visual/.results',
3859
+ outputDir: './${VISUAL_OUTPUT}',
3688
3860
  use: { baseURL: \`http://127.0.0.1:\${PORT}\` },
3689
3861
  // Playwright starts the preview itself, so \`${runCommand(packageManager, "visual")}\` is one command from a cold checkout.
3690
3862
  webServer: {
@@ -3750,7 +3922,7 @@ var visualFiles = (elements, answers) => ({
3750
3922
  //#endregion
3751
3923
  //#region src/scaffold/pluginPackage/index.ts
3752
3924
  /** What the build writes, and what is nobody's to format or lint. */
3753
- var OUTPUTS = ["dist", "visual/.results"];
3925
+ var OUTPUTS = ["dist", "tmp"];
3754
3926
  /**
3755
3927
  * Every file of a plugin package: the element, the build that publishes it, a space to preview it in, and what checks
3756
3928
  * it — a project of its own, with nothing about it shared with whatever repository it sits in except the folder.
@@ -3815,6 +3987,35 @@ var scaffold = (answers) => ({
3815
3987
  ...visualFiles$1(answers),
3816
3988
  ...skillFiles()
3817
3989
  });
3990
+ /**
3991
+ * The files of a project that are this CLI's machinery rather than the project's own: what `plitzi upgrade` keeps up
3992
+ * with the CLI. Named, not inferred, so a file the scaffold starts a project with — its space, pages, plugins, data,
3993
+ * README, `.env` — can never be taken for one: those are the project's from the moment they are written.
3994
+ *
3995
+ * `package.json` is not among them: it is merged, never replaced (`upgrade packages`). Nor are the skills, which come
3996
+ * from the packages installed (`upgrade skills`).
3997
+ */
3998
+ var MACHINERY = /* @__PURE__ */ new Set([
3999
+ "src/author.ts",
4000
+ "src/main.ts",
4001
+ "src/preflight.css",
4002
+ "index.html",
4003
+ "vite.config.ts",
4004
+ "tsconfig.json",
4005
+ "tsconfig.build.json",
4006
+ ".gitignore",
4007
+ "AGENTS.md",
4008
+ "CLAUDE.md",
4009
+ ".prettierrc",
4010
+ ".prettierignore",
4011
+ "eslint.config.mjs",
4012
+ "playwright.config.ts",
4013
+ "visual/home.spec.ts",
4014
+ "src/plugins/README.md",
4015
+ ".yarnrc.yml",
4016
+ "pnpm-workspace.yaml"
4017
+ ]);
4018
+ var machineryFiles = (answers) => Object.fromEntries(Object.entries(scaffold(answers)).filter(([file]) => MACHINERY.has(file)));
3818
4019
  //#endregion
3819
4020
  //#region src/commands/existingProject.ts
3820
4021
  var LOCKFILES = [
@@ -3822,7 +4023,7 @@ var LOCKFILES = [
3822
4023
  ["pnpm-lock.yaml", "pnpm"],
3823
4024
  ["package-lock.json", "npm"]
3824
4025
  ];
3825
- var exists = async (file) => {
4026
+ var exists$1 = async (file) => {
3826
4027
  try {
3827
4028
  await fs.access(file);
3828
4029
  return true;
@@ -3853,7 +4054,7 @@ var workspaceGlobs = async (dir, packageJson) => {
3853
4054
  var nearestPackage = async (from) => {
3854
4055
  let dir = from;
3855
4056
  for (;;) {
3856
- if (await exists(path.join(dir, "package.json"))) return dir;
4057
+ if (await exists$1(path.join(dir, "package.json"))) return dir;
3857
4058
  const parent = path.dirname(dir);
3858
4059
  if (parent === dir) return;
3859
4060
  dir = parent;
@@ -3872,9 +4073,9 @@ var nearestWorkspace = async (from) => {
3872
4073
  }
3873
4074
  };
3874
4075
  var lockfileManager = async (dirs) => {
3875
- for (const dir of dirs) for (const [file, manager] of LOCKFILES) if (await exists(path.join(dir, file))) return manager;
4076
+ for (const dir of dirs) for (const [file, manager] of LOCKFILES) if (await exists$1(path.join(dir, file))) return manager;
3876
4077
  };
3877
- var readText = async (file) => {
4078
+ var readText$1 = async (file) => {
3878
4079
  try {
3879
4080
  return await fs.readFile(file, "utf-8");
3880
4081
  } catch {
@@ -3886,7 +4087,7 @@ var readText = async (file) => {
3886
4087
  * of its lists byte for byte, which is what says nobody has changed them since the CLI did.
3887
4088
  */
3888
4089
  var packageComponents = async (root) => {
3889
- const [elements, declarations] = await Promise.all([readText(path.join(root, "src/elements.ts")), readText(path.join(root, "src/declarations.ts"))]);
4090
+ const [elements, declarations] = await Promise.all([readText$1(path.join(root, "src/elements.ts")), readText$1(path.join(root, "src/declarations.ts"))]);
3890
4091
  if (elements === void 0 || declarations === void 0) return;
3891
4092
  const components = [...elements.matchAll(/^import (\w+) from '\.\/(\w+)';$/gm)].filter(([, name, folder]) => name === folder).map(([, name]) => name);
3892
4093
  return components.length > 0 && elementsRegistry(components) === elements && declarationsRegistry(components) === declarations ? components : void 0;
@@ -3896,7 +4097,7 @@ var packageComponents = async (root) => {
3896
4097
  * gives the file byte for byte, as with a plugin package's lists.
3897
4098
  */
3898
4099
  var projectDeclared = async (root) => {
3899
- const listed = await readText(path.join(root, "src/plugins/declarations.ts"));
4100
+ const listed = await readText$1(path.join(root, "src/plugins/declarations.ts"));
3900
4101
  if (listed === void 0) return;
3901
4102
  const components = [...listed.matchAll(/^import \w+ from '\.\/(\w+)\/declaration\.ts';$/gm)].map(([, folder]) => folder);
3902
4103
  return projectDeclarations(components) === listed ? components : void 0;
@@ -3910,7 +4111,7 @@ var projectDeclared = async (root) => {
3910
4111
  * in a project written since that was how, by a list written in the file before then.
3911
4112
  */
3912
4113
  var plitziProject = async (root, packageJson) => {
3913
- if (packageJson.peerDependencies?.["@plitzi/plitzi-sdk"] && await exists(path.join(root, "src/elements.ts"))) return {
4114
+ if (packageJson.peerDependencies?.["@plitzi/plitzi-sdk"] && await exists$1(path.join(root, "src/elements.ts"))) return {
3914
4115
  kind: "plugin",
3915
4116
  components: await packageComponents(root)
3916
4117
  };
@@ -3918,13 +4119,13 @@ var plitziProject = async (root, packageJson) => {
3918
4119
  ...packageJson.dependencies,
3919
4120
  ...packageJson.devDependencies
3920
4121
  };
3921
- const main = await readText(path.join(root, "src/main.ts"));
4122
+ const main = await readText$1(path.join(root, "src/main.ts"));
3922
4123
  if (!("@plitzi/plitzi-sdk" in dependencies) || main === void 0 || !/src\/plugins|\.\/plugins\//.test(main)) return;
3923
4124
  const server = "@plitzi/sdk-server" in dependencies;
3924
4125
  return {
3925
4126
  kind: "project",
3926
4127
  mode: server ? "server" : "client",
3927
- source: await exists(path.join(root, "src/space.ts")) ? "local" : "cloud",
4128
+ source: await exists$1(path.join(root, "src/space.ts")) ? "local" : "cloud",
3928
4129
  discovers: server ? main.includes("readdirSync(PLUGINS_DIR") : main.includes("import.meta.glob<{ default: RenderPlugins[string]"),
3929
4130
  declared: await projectDeclared(root)
3930
4131
  };
@@ -4230,12 +4431,16 @@ var loadProjectSpace = async (root) => {
4230
4431
  };
4231
4432
  //#endregion
4232
4433
  //#region src/commands/check.ts
4233
- /** The space the project declares, authored — the handles say what each page owes. Only for one written here. */
4234
- var projectHandles = async (root) => {
4434
+ /** The space the project declares, authored. Only for one written here. */
4435
+ var projectSpace = async (root) => {
4235
4436
  const project = await loadProjectSpace(root);
4236
4437
  if ("problem" in project) return { problem: `${project.problem} There is nothing to check the page against.` };
4237
4438
  try {
4238
- return authorSpace(project.space, { plugins: project.plugins }).handles;
4439
+ const { handles, schema } = authorSpace(project.space, { plugins: project.plugins });
4440
+ return {
4441
+ handles,
4442
+ schema
4443
+ };
4239
4444
  } catch (error) {
4240
4445
  return { problem: `The space does not author — npm run author says why: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
4241
4446
  }
@@ -4248,14 +4453,48 @@ var pageFor = (handles, pathname) => {
4248
4453
  return segments.length === wanted.length && segments.every((segment, index) => segment.startsWith(":") || /^\{\{.*\}\}$/.test(segment) || segment === wanted[index]);
4249
4454
  })?.id;
4250
4455
  };
4251
- var checkAt = async (browser, origin, pathname, width, scheme, handles, asked) => {
4252
- const page = await browser.newPage({
4253
- viewport: {
4254
- width,
4255
- height: 900
4256
- },
4257
- colorScheme: scheme,
4258
- reducedMotion: "no-preference"
4456
+ /** The elements a piece of HTML carries, by the id each is marked with. */
4457
+ var markedIn = (html) => new Set([...html.matchAll(/data-plitzi-el="([^"]+)"/g)].map(([, id]) => id));
4458
+ /** The `runtime: 'server'` provider an element is inside — or is — if any: what the server's HTML owes. */
4459
+ var serverProviderOf = (schema, id) => {
4460
+ const seen = /* @__PURE__ */ new Set();
4461
+ let current = schema.flat[id];
4462
+ while (current && !seen.has(current.id)) {
4463
+ seen.add(current.id);
4464
+ if (current.definition.type === "apiContainer" && current.definition.runtime === "server") return current.id;
4465
+ const parent = current.definition.parentId;
4466
+ current = parent ? schema.flat[parent] : void 0;
4467
+ }
4468
+ };
4469
+ /**
4470
+ * What the page has once hydrated and the server's HTML did not: inside a server provider, an issue per provider — its
4471
+ * answer was meant to be in the page as it arrived; anywhere else, what the browser renders on its own, said apart.
4472
+ */
4473
+ var lateElements = (schema, served, hydrated, width) => {
4474
+ const late = [...new Set(hydrated)].filter((id) => !served.has(id));
4475
+ const byProvider = /* @__PURE__ */ new Map();
4476
+ const browserOnly = [];
4477
+ for (const id of late) {
4478
+ const provider = schema ? serverProviderOf(schema, id) : void 0;
4479
+ if (provider) byProvider.set(provider, [...byProvider.get(provider) ?? [], id]);
4480
+ else browserOnly.push(id);
4481
+ }
4482
+ return {
4483
+ issues: [...byProvider].map(([provider, ids]) => ({
4484
+ code: "not-server-rendered",
4485
+ message: `apiContainer "${provider}" is resolved on the server, and ${String(ids.length)} of what it holds was not in the HTML the server sent (${ids.slice(0, 6).join(", ")}${ids.length > 6 ? ", …" : ""}) — it arrived after the page, so the first paint and a crawler miss it`,
4486
+ elementId: provider,
4487
+ width
4488
+ })),
4489
+ browserOnly
4490
+ };
4491
+ };
4492
+ var checkAt = async (browser, origin, pathname, width, scheme, authored, asked, ssr) => {
4493
+ const handles = authored?.handles;
4494
+ const page = await openProjectPage(browser, origin, {
4495
+ width,
4496
+ height: 900,
4497
+ ...scheme ? { scheme } : {}
4259
4498
  });
4260
4499
  const consoleErrors = [];
4261
4500
  const failedRequests = [];
@@ -4266,35 +4505,75 @@ var checkAt = async (browser, origin, pathname, width, scheme, handles, asked) =
4266
4505
  page.on("response", (response) => {
4267
4506
  if (response.status() >= 400 && response.url().startsWith(origin)) failedRequests.push(`${String(response.status())} ${response.url().slice(origin.length)}`);
4268
4507
  });
4269
- if (!await page.goto(`${origin}${pathname}`, { waitUntil: "networkidle" }).catch(() => null)) return {
4270
- path: pathname,
4271
- width,
4272
- ok: false,
4273
- checked: 0,
4274
- problems: [`nothing answers at ${origin}${pathname}`],
4275
- consoleErrors,
4276
- failedRequests,
4277
- devTools: false
4278
- };
4508
+ if (!await page.goto(`${origin}${pathname}`, { waitUntil: "networkidle" }).catch(() => null)) {
4509
+ const issue = {
4510
+ code: "no-answer",
4511
+ message: `nothing answers at ${origin}${pathname}`,
4512
+ width
4513
+ };
4514
+ return {
4515
+ path: pathname,
4516
+ width,
4517
+ ok: false,
4518
+ checked: 0,
4519
+ problems: [issue.message],
4520
+ issues: [issue],
4521
+ hiddenAtWidth: [],
4522
+ consoleErrors,
4523
+ failedRequests,
4524
+ devTools: false,
4525
+ lists: {}
4526
+ };
4527
+ }
4528
+ await withoutDevTools(page);
4279
4529
  const pageId = handles ? pageFor(handles, pathname) : void 0;
4280
4530
  const report = handles && pageId ? await inspectPage(page, handles, { page: pageId }) : await inspectDocument(page);
4281
- const devTools = await readDevTools(page, asked);
4282
- const problems = [
4283
- ...handles && !pageId ? [`no page of the space answers at ${pathname}`] : [],
4284
- ...report.problems,
4285
- ...devTools.flows.filter((flow) => flow.status === "failed").map(failedFlowText)
4531
+ const devTools = await readDevTools(page, {
4532
+ ...asked,
4533
+ state: true
4534
+ });
4535
+ const data = authored && pageId && devTools.sources ? dataIssues(authored.schema, pageId, devTools.sources) : void 0;
4536
+ const served = ssr ? markedIn(await (await fetch(`${origin}${pathname}`)).text()) : void 0;
4537
+ const late = served ? lateElements(authored?.schema, served, await page.evaluate((selector) => Array.from(document.querySelectorAll(selector), (element) => element.getAttribute("data-plitzi-el") ?? ""), "[data-plitzi-el]"), width) : void 0;
4538
+ const noPage = handles && !pageId ? [{
4539
+ code: "no-page",
4540
+ message: `no page of the space answers at ${pathname}`,
4541
+ width
4542
+ }] : [];
4543
+ const failedFlows = devTools.flows.filter((flow) => flow.status === "failed").map((flow) => ({
4544
+ code: "flow-failed",
4545
+ message: failedFlowText(flow),
4546
+ ...flow.on === void 0 ? {} : { elementId: flow.on },
4547
+ width
4548
+ }));
4549
+ const issues = [
4550
+ ...noPage,
4551
+ ...report.issues.map((issue) => ({
4552
+ ...issue,
4553
+ width
4554
+ })),
4555
+ ...(data?.issues ?? []).map((issue) => ({
4556
+ ...issue,
4557
+ width
4558
+ })),
4559
+ ...late?.issues ?? [],
4560
+ ...failedFlows
4286
4561
  ];
4287
4562
  return {
4288
4563
  path: pathname,
4289
4564
  width,
4290
- ok: problems.length === 0 && consoleErrors.length === 0 && failedRequests.length === 0,
4565
+ ok: issues.length === 0 && consoleErrors.length === 0 && failedRequests.length === 0,
4291
4566
  checked: report.checked,
4292
- problems,
4567
+ problems: issues.map((issue) => issue.message),
4568
+ issues,
4569
+ hiddenAtWidth: report.hiddenAtWidth,
4293
4570
  consoleErrors,
4294
4571
  failedRequests,
4295
4572
  devTools: devTools.available,
4296
- ...devTools.state === void 0 ? {} : { state: devTools.state },
4297
- ...devTools.sources === void 0 ? {} : { sources: devTools.sources },
4573
+ lists: data?.lists ?? {},
4574
+ ...late ? { browserOnly: late.browserOnly } : {},
4575
+ ...!asked.state || devTools.state === void 0 ? {} : { state: devTools.state },
4576
+ ...!asked.state || devTools.sources === void 0 ? {} : { sources: devTools.sources },
4298
4577
  ...devTools.element === void 0 ? {} : { element: devTools.element }
4299
4578
  };
4300
4579
  };
@@ -4318,10 +4597,28 @@ var shapeOf = (value) => {
4318
4597
  const text = value === void 0 ? "undefined" : JSON.stringify(value);
4319
4598
  return text.length > 60 ? `${text.slice(0, 57)}…` : text;
4320
4599
  };
4321
- var reportText = (report, asked) => {
4600
+ /** Each list's rows in a few words — `plan-list 3 rows` — so an empty one is seen, not inferred from a box. */
4601
+ var listsText = (lists) => {
4602
+ const entries = Object.entries(lists);
4603
+ if (entries.length === 0) return [];
4604
+ const rows = entries.map(([id, count]) => `${id} ${count === null ? "reads nothing" : `${String(count)} row${count === 1 ? "" : "s"}`}`);
4605
+ return [chalk.dim(` · lists: ${rows.join(", ")}`)];
4606
+ };
4607
+ /** With `--ssr`, what only the browser renders — not a problem outside a server provider, and said so it is seen. */
4608
+ var browserOnlyText = (report) => report.browserOnly && report.browserOnly.length > 0 ? [chalk.dim(` · rendered only in the browser, outside any server provider: ${report.browserOnly.slice(0, 10).join(", ")}${report.browserOnly.length > 10 ? `, … (${String(report.browserOnly.length)})` : ""}`)] : [];
4609
+ var reportText$1 = (report, asked) => {
4322
4610
  const head = `${report.path} at ${String(report.width)} px`;
4323
- const held = heldText(report, asked);
4324
- if (report.ok) return [chalk.green(`✓ ${head} — ${report.checked > 0 ? `${String(report.checked)} elements on screen, ` : ""}nothing wrong`), ...held].join("\n");
4611
+ const held = [
4612
+ ...listsText(report.lists),
4613
+ ...browserOnlyText(report),
4614
+ ...heldText(report, asked)
4615
+ ];
4616
+ const atWidth = report.hiddenAtWidth.length > 0 ? chalk.dim(` · hidden at this width by a breakpoint, on purpose: ${report.hiddenAtWidth.join(", ")}`) : void 0;
4617
+ if (report.ok) return [
4618
+ chalk.green(`✓ ${head} — ${report.checked > 0 ? `${String(report.checked)} elements on screen, ` : ""}nothing wrong`),
4619
+ ...atWidth ? [atWidth] : [],
4620
+ ...held
4621
+ ].join("\n");
4325
4622
  return [
4326
4623
  chalk.red(`✗ ${head}`),
4327
4624
  ...report.problems.map((problem) => ` - ${problem}`),
@@ -4339,9 +4636,9 @@ var check = async (route, options) => {
4339
4636
  fail(where.problem);
4340
4637
  return;
4341
4638
  }
4342
- const handles = plitzi?.source === "local" ? await projectHandles(project.root) : void 0;
4343
- if (handles && "problem" in handles) {
4344
- fail(handles.problem);
4639
+ const authored = plitzi?.source === "local" ? await projectSpace(project.root) : void 0;
4640
+ if (authored && "problem" in authored) {
4641
+ fail(authored.problem);
4345
4642
  return;
4346
4643
  }
4347
4644
  const browser = await launchBrowser(project.root);
@@ -4351,14 +4648,14 @@ var check = async (route, options) => {
4351
4648
  }
4352
4649
  try {
4353
4650
  const widths = options.width ?? [1440, 390];
4354
- const scheme = options.scheme ?? "light";
4651
+ const { scheme } = options;
4355
4652
  const asked = {
4356
4653
  state: Boolean(options.state),
4357
4654
  ...options.element ? { element: options.element } : {}
4358
4655
  };
4359
4656
  const reports = [];
4360
- for (const width of widths) reports.push(await checkAt(browser, where.origin, route ?? "/", width, scheme, handles, asked));
4361
- console.log(options.json ? JSON.stringify(reports) : reports.map((report) => reportText(report, asked)).join("\n"));
4657
+ for (const width of widths) reports.push(await checkAt(browser, where.origin, route ?? "/", width, scheme, authored, asked, Boolean(options.ssr)));
4658
+ console.log(options.json ? JSON.stringify(reports) : reports.map((report) => reportText$1(report, asked)).join("\n"));
4362
4659
  if (reports.some((report) => !report.ok)) process.exitCode = 1;
4363
4660
  } finally {
4364
4661
  await browser.close();
@@ -4416,6 +4713,15 @@ var writeFunctionsState = async (root, state) => {
4416
4713
  };
4417
4714
  /** The files that differ between two copies — added, removed or changed — sorted. */
4418
4715
  var changedFiles = (a, b) => [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((file) => a[file] !== b[file]).sort();
4716
+ /**
4717
+ * What `functions/` holds that the space does not: nothing at all (`none`, no `functions/index.ts`), something changed
4718
+ * since it was pulled from this space or never pulled (`changed`), or exactly what was pulled (`unchanged`).
4719
+ */
4720
+ var functionsChange = async (root, spaceId) => {
4721
+ const [local, state] = await Promise.all([readLocal(root), readState(root)]);
4722
+ if (!Object.hasOwn(local, "index.ts")) return "none";
4723
+ return state?.space === spaceId && !changedFiles(local, state.files).length ? "unchanged" : "changed";
4724
+ };
4419
4725
  var taskList = (draft) => draft.manifest?.tasks.length ? draft.manifest.tasks.map((task) => `${task.namespace}.${task.action}`).join(", ") : "no tasks";
4420
4726
  var readDraft = async (connection, spaceId) => {
4421
4727
  const answered = await authorizedRequest(connection, `/spaces/${spaceId}/functions`);
@@ -4468,30 +4774,29 @@ var printProblems = (problems) => {
4468
4774
  });
4469
4775
  process.exitCode = 1;
4470
4776
  };
4471
- var pushFunctions = async (options) => {
4472
- const root = await rootOf();
4473
- const connection = root && await connectToSpace(options, "to push to");
4474
- if (!root || !connection || !connection.space) return;
4777
+ /**
4778
+ * `functions/` saved as the draft of the space the connection works in: refused when the space's copy moved on since the
4779
+ * pull, and — for a project that never pulled them — when the space already has functions of its own. `force` replaces
4780
+ * whatever the space holds now, which is what `plitzi push --force` asks of every part.
4781
+ */
4782
+ var pushFunctionsOf = async (root, connection, space, { force = false } = {}) => {
4475
4783
  const [local, state] = await Promise.all([readLocal(root), readState(root)]);
4476
4784
  if (!Object.hasOwn(local, "index.ts")) {
4477
4785
  fail("There is no functions/index.ts here: it is where a space’s functions start. Pull them, or write it.");
4478
- return;
4786
+ return "failed";
4479
4787
  }
4480
- let base = state?.space === connection.space.id ? state.version : void 0;
4788
+ if (state?.space === space.id && !changedFiles(local, state.files).length) return "unchanged";
4789
+ let base = state?.space === space.id && !force ? state.version : void 0;
4481
4790
  if (base === void 0) {
4482
- const draft = await readDraft(connection, connection.space.id);
4483
- if (!draft) return;
4484
- if (Object.keys(draft.files).length) {
4485
- fail(`${connection.space.name} already has functions. Pull them first: plitzi functions pull.`);
4486
- return;
4791
+ const draft = await readDraft(connection, space.id);
4792
+ if (!draft) return "failed";
4793
+ if (Object.keys(draft.files).length && !force) {
4794
+ fail(`${space.name} already has functions. Pull them first: plitzi functions pull.`);
4795
+ return "failed";
4487
4796
  }
4488
4797
  base = draft.version;
4489
4798
  }
4490
- if (state?.space === connection.space.id && !changedFiles(local, state.files).length) {
4491
- console.log("Nothing to push: functions/ is what was pulled.");
4492
- return;
4493
- }
4494
- const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions`, {
4799
+ const answered = await authorizedRequest(connection, `/spaces/${space.id}/functions`, {
4495
4800
  method: "PUT",
4496
4801
  headers: { "Content-Type": "application/json" },
4497
4802
  body: JSON.stringify({
@@ -4501,27 +4806,36 @@ var pushFunctions = async (options) => {
4501
4806
  });
4502
4807
  if (!answered.ok) {
4503
4808
  fail(answered.error);
4504
- return;
4809
+ return "failed";
4505
4810
  }
4506
4811
  const { reply } = answered.value;
4507
4812
  if (reply.status === 422 && reply.data.problems) {
4508
4813
  printProblems(reply.data.problems);
4509
- return;
4814
+ return "failed";
4510
4815
  }
4511
4816
  if (reply.status === 409) {
4512
- fail(`${connection.space.name}’s functions changed since your pull — in the builder, or from another copy. Keep your changes aside, pull, and apply them again.`);
4513
- return;
4817
+ fail(`${space.name}’s functions changed since your pull — in the builder, or from another copy. Keep your changes aside, pull, and apply them again.`);
4818
+ return "failed";
4514
4819
  }
4515
4820
  if (reply.status !== 200 || !reply.data.version) {
4516
4821
  fail(reply.data.refusal?.error ?? reply.data.error ?? `The functions were not saved (${reply.status}).`);
4517
- return;
4822
+ return "failed";
4518
4823
  }
4519
4824
  await writeFunctionsState(root, {
4520
- space: connection.space.id,
4825
+ space: space.id,
4521
4826
  version: reply.data.version,
4522
4827
  files: local
4523
4828
  });
4524
- console.log(chalk.green(`Pushed to ${connection.space.name}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}.`) + chalk.dim(" The live site runs them once the space is published."));
4829
+ console.log(chalk.green(`Functions pushed to ${space.name}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}.`));
4830
+ return "pushed";
4831
+ };
4832
+ var pushFunctions = async (options) => {
4833
+ const root = await rootOf();
4834
+ const connection = root && await connectToSpace(options, "to push to");
4835
+ if (!root || !connection || !connection.space) return;
4836
+ const outcome = await pushFunctionsOf(root, connection, connection.space);
4837
+ if (outcome === "unchanged") console.log("Nothing to push: functions/ is what was pulled.");
4838
+ else if (outcome === "pushed") console.log(chalk.dim("The live site runs them once the space is published."));
4525
4839
  };
4526
4840
  /** What one run of a task did: its logs, then its value — or why it failed, as a failure of the command. */
4527
4841
  var printRun = (task, report) => {
@@ -4820,7 +5134,7 @@ var fromSpaceMain = (options) => {
4820
5134
  import path from 'node:path';
4821
5135
 
4822
5136
  import { closeOnSignals, consoleLogger, ${local ? "createJsonAdapters" : "createCloudAdapters"}, createServer, loadFunctions } from '@plitzi/sdk-server';
4823
- ${hasRuntime ? importLine(`{ ${runtimeImports} }`, "@plitzi/sdk-server/runtime") : ""}${local ? `\n${importLine("{ authorSpace }", "@plitzi/sdk-authoring")}` : ""}
5137
+ ${hasRuntime ? importLine(`{ ${runtimeImports} }`, "@plitzi/sdk-server/runtime") : ""}${local ? `\n${importLine("{ authorSpace }", "@plitzi/sdk-authoring")}${importLine("{ publicData }", "@plitzi/sdk-authoring/node")}` : ""}
4824
5138
  ${importLine("{ lookups }", "./actions.ts")}${local ? importLine("{ declarations }", "./plugins/declarations.ts") : ""}${runtimeEntry ? importLine("spaceRuntime", runtimeEntry) : ""}${local ? importLine("{ space }", "./space.ts") : ""}
4825
5139
  /**
4826
5140
  * The project's settings — the key its actions sign with, the variables the space was given on Plitzi — kept in \`.env\`,
@@ -4842,7 +5156,10 @@ ${builtPlugins ? BUILT_PLUGINS : ""}${local ? `
4842
5156
  * The space, held in this project: \`src/space/\`, authored at boot — saving a page and letting \`--watch\` restart is
4843
5157
  * the whole edit loop, and the warnings are printed where somebody editing it is watching.
4844
5158
  */
4845
- const { schema, style, warnings } = authorSpace(space, { plugins: declarations${builtPlugins ? ", pluginTypes: builtTypes" : ""} });
5159
+ const { schema, style, warnings } = authorSpace(space, {
5160
+ plugins: declarations,${builtPlugins ? "\n pluginTypes: builtTypes," : ""}
5161
+ data: publicData(new URL('../public/', import.meta.url))
5162
+ });
4846
5163
 
4847
5164
  for (const warning of warnings) {
4848
5165
  console.warn(\`[author] \${warning.message}\`);
@@ -4897,6 +5214,22 @@ var placeOf = (paths) => {
4897
5214
  const prefix = paths.length > 0 && paths.every((path) => path.startsWith("src/")) ? "" : "src/";
4898
5215
  return (path) => `${prefix}${path}`;
4899
5216
  };
5217
+ /**
5218
+ * Where the project keeps what the space's runtime and plugins are built from: the module the runtime starts at, and the
5219
+ * files each plugin's elements start at — the paths `create --from` writes them at, which `plitzi push` packs again.
5220
+ */
5221
+ var projectEntries = ({ source }) => {
5222
+ const place = placeOf(Object.keys(source.files));
5223
+ const runtime = source.runtime?.entries[0];
5224
+ return {
5225
+ runtime: runtime ? place(runtime) : null,
5226
+ plugins: source.plugins.map(({ type, entries }) => ({
5227
+ type,
5228
+ entries: entries.map(place)
5229
+ })),
5230
+ files: Object.keys(source.files).map(place)
5231
+ };
5232
+ };
4900
5233
  /** The import `src/main.ts` names the runtime's module by. */
4901
5234
  var fromMain = (path) => `./${path.replace(/^src\//, "")}`;
4902
5235
  /** What the space was given on Plitzi, by name, as `.env.example` lists it — and `.env`, with values where they are known. */
@@ -5058,7 +5391,7 @@ var digestsOf = async (root, file, format) => {
5058
5391
  var readOrigin = async (root) => {
5059
5392
  try {
5060
5393
  const value = JSON.parse(await fs.readFile(path.join(root, ORIGIN_FILE), "utf-8"));
5061
- if (!e$1(value) || value.format !== 1 || typeof value.api !== "string" || !e$1(value.space) || typeof value.space.id !== "number" || typeof value.space.name !== "string" || typeof value.space.permanentUrl !== "string" || value.source !== "local" && value.source !== "cloud" || !e$1(value.version) || typeof value.version.environment !== "string" || value.version.revision !== void 0 && typeof value.version.revision !== "number" || !isStrings(value.files) || !isStrings(value.downloads) || !isStrings(value.dependencies)) return;
5394
+ if (!e$1(value) || value.format !== 1 || typeof value.api !== "string" || !e$1(value.space) || typeof value.space.id !== "number" || typeof value.space.name !== "string" || typeof value.space.permanentUrl !== "string" || value.source !== "local" && value.source !== "cloud" || !e$1(value.version) || typeof value.version.environment !== "string" || value.version.revision !== void 0 && typeof value.version.revision !== "number" || value.draft !== void 0 && typeof value.draft !== "string" || !isStrings(value.files) || !isStrings(value.downloads) || !isStrings(value.dependencies)) return;
5062
5395
  return {
5063
5396
  format: 1,
5064
5397
  api: value.api,
@@ -5072,6 +5405,7 @@ var readOrigin = async (root) => {
5072
5405
  environment: value.version.environment,
5073
5406
  ...typeof value.version.revision === "number" ? { revision: value.version.revision } : {}
5074
5407
  },
5408
+ ...typeof value.draft === "string" ? { draft: value.draft } : {},
5075
5409
  files: value.files,
5076
5410
  downloads: value.downloads,
5077
5411
  dependencies: value.dependencies
@@ -5096,6 +5430,17 @@ var digestsOnDisk = async (root, files, format) => {
5096
5430
  const digests = await Promise.all(files.map(async (file) => [file, (await digestsOf(root, file, format))?.formatted]));
5097
5431
  return Object.fromEntries(digests.filter((entry) => entry[1] !== void 0));
5098
5432
  };
5433
+ /**
5434
+ * The files a space gives a project — its text as the project's formatter writes it, its binaries as they are — by
5435
+ * path: what a pull compares with what is on disk, and what a push records as given once the space holds the project.
5436
+ * The files of its CDN are not among them: they are fetched, and only when the space names another address for one.
5437
+ */
5438
+ var givenFiles = async (project, format) => {
5439
+ const given = /* @__PURE__ */ new Map();
5440
+ for (const [file, text] of Object.entries(project.files)) given.set(file, Buffer.from(await format(file, text)));
5441
+ Object.entries(project.binaries).forEach(([file, base64]) => given.set(file, Buffer.from(base64, "base64")));
5442
+ return given;
5443
+ };
5099
5444
  //#endregion
5100
5445
  //#region src/commands/createFrom.ts
5101
5446
  /**
@@ -5110,30 +5455,49 @@ var versionLabel = ({ environment, revision }) => {
5110
5455
  return revision ? `${environment} r${String(revision)}` : `${environment} (latest)`;
5111
5456
  };
5112
5457
  /**
5113
- * A version of the space named — its id or its permanent URL — put together as a project, or why not, said and
5114
- * `undefined`. `name` is what it is called in what is printed: `from`, unless a pull names it by its id.
5458
+ * A version of the space named — its id or its permanent URL — put together as a project, asked for on a connection that
5459
+ * is signed in: the export, or why not in words. `name` is what the space is called in them.
5115
5460
  */
5116
- var fetchExport = async (api, from, { source, version, name = from }) => {
5117
- const connection = await signedIn(api, `to take ${from} out of Plitzi`);
5118
- if (!connection) return;
5119
- console.log(`\nTaking ${chalk.bold(name)} (${versionLabel(version)}) out of ${api}…`);
5461
+ var requestExport = async (connection, from, { source, version, name = from }) => {
5120
5462
  const query = new URLSearchParams({
5121
5463
  source,
5122
5464
  environment: version.environment,
5123
5465
  ...version.revision ? { revision: String(version.revision) } : {}
5124
5466
  });
5125
5467
  const answered = await authorizedRequest(connection, `/spaces/${encodeURIComponent(from)}/export?${query.toString()}`);
5126
- if (!answered.ok) {
5127
- fail(answered.error);
5128
- return;
5129
- }
5468
+ if (!answered.ok) return answered;
5130
5469
  const { status, data } = answered.value.reply;
5131
- if (status === 200 && isExport(data)) return data;
5470
+ if (status === 200 && isExport(data)) return {
5471
+ ok: true,
5472
+ value: data
5473
+ };
5132
5474
  const said = e$1(data) && typeof data.error === "string" ? data.error : void 0;
5133
- fail({
5134
- 403: `You may not take ${from} out: it takes being able to change it — its owner, an administrator or a writer.`,
5135
- 404: said ?? `There is no space ${from} you can reach.`
5136
- }[status] ?? said ?? `The platform would not hand ${from} over (${String(status)}).`);
5475
+ return {
5476
+ ok: false,
5477
+ error: {
5478
+ 403: `You may not take ${name} out: it takes being able to change it — its owner, an administrator or a writer.`,
5479
+ 404: said ?? `There is no space ${name} you can reach.`
5480
+ }[status] ?? said ?? `The platform would not hand ${name} over (${String(status)}).`
5481
+ };
5482
+ };
5483
+ /**
5484
+ * A version of the space named put together as a project — signing in first when there is no session — or why not,
5485
+ * said and `undefined`. `name` is what it is called in what is printed: `from`, unless a pull names it by its id.
5486
+ */
5487
+ var fetchExport = async (api, from, { source, version, name = from }) => {
5488
+ const connection = await signedIn(api, `to take ${from} out of Plitzi`);
5489
+ if (!connection) return;
5490
+ console.log(`\nTaking ${chalk.bold(name)} (${versionLabel(version)}) out of ${api}…`);
5491
+ const exported = await requestExport(connection, from, {
5492
+ source,
5493
+ version,
5494
+ name
5495
+ });
5496
+ if (!exported.ok) {
5497
+ fail(exported.error);
5498
+ return;
5499
+ }
5500
+ return exported.value;
5137
5501
  };
5138
5502
  /** A file of the space's CDN, fetched. */
5139
5503
  var download = async (url) => {
@@ -5181,6 +5545,7 @@ var recordOrigin = async (root, { api, exported, source, pinned, project }) => {
5181
5545
  environment: exported.version.environment,
5182
5546
  ...pinned ? { revision: exported.version.revision } : {}
5183
5547
  },
5548
+ ...exported.draft ? { draft: exported.draft } : {},
5184
5549
  files,
5185
5550
  downloads: Object.fromEntries(project.downloads.filter(({ to }) => Object.hasOwn(files, to)).map(({ url, to }) => [to, url])),
5186
5551
  dependencies: project.dependencies
@@ -5192,6 +5557,43 @@ var recordOrigin = async (root, { api, exported, source, pinned, project }) => {
5192
5557
  });
5193
5558
  };
5194
5559
  //#endregion
5560
+ //#region src/commands/scaffoldRecord.ts
5561
+ /**
5562
+ * What the CLI last wrote of a project's machinery (`machineryFiles`): a digest of each file as it was written, never
5563
+ * the file — as `.plitzi/space.json` keeps of what a pull wrote. Enough for `plitzi upgrade` to tell a file nobody
5564
+ * touched since, which it may replace, from one somebody made their own, which it only shows.
5565
+ *
5566
+ * Kept where the CLI keeps its other state. A project without one — made before it existed, or a clone that did not
5567
+ * carry it — is not refused anything: every file that differs is then shown as the project's, for its author to take.
5568
+ */
5569
+ var SCAFFOLD_RECORD_FILE = path.join(".plitzi", "scaffold.json");
5570
+ var digestOf = (contents) => createHash("sha256").update(contents).digest("hex");
5571
+ var readScaffoldRecord = async (root) => {
5572
+ let parsed;
5573
+ try {
5574
+ parsed = JSON.parse(await fs.readFile(path.join(root, SCAFFOLD_RECORD_FILE), "utf-8"));
5575
+ } catch {
5576
+ return;
5577
+ }
5578
+ if (!e$1(parsed) || parsed.format !== 1 || typeof parsed.cli !== "string" || !e$1(parsed.files)) return;
5579
+ const files = Object.fromEntries(Object.entries(parsed.files).filter((entry) => typeof entry[1] === "string"));
5580
+ return {
5581
+ format: 1,
5582
+ cli: parsed.cli,
5583
+ files
5584
+ };
5585
+ };
5586
+ /** What the CLI wrote, file → its digest (`digestOf`), as `cli` wrote it. */
5587
+ var writeScaffoldRecord = async (root, cli, digests) => {
5588
+ const record = {
5589
+ format: 1,
5590
+ cli,
5591
+ files: Object.fromEntries(Object.entries(digests).sort(([a], [b]) => a.localeCompare(b)))
5592
+ };
5593
+ await fs.mkdir(path.join(root, ".plitzi"), { recursive: true });
5594
+ await fs.writeFile(path.join(root, SCAFFOLD_RECORD_FILE), `${JSON.stringify(record, null, 2)}\n`);
5595
+ };
5596
+ //#endregion
5195
5597
  //#region src/commands/create.ts
5196
5598
  var MODES = ["server", "client"];
5197
5599
  var SOURCES = ["local", "cloud"];
@@ -5349,6 +5751,7 @@ var create = async (directory, options) => {
5349
5751
  ".env": envFromSpace(exported, answers, randomBytes(32).toString("hex"))
5350
5752
  } : files);
5351
5753
  const missing = fromSpace ? await writeFromSpace(target, fromSpace) : [];
5754
+ await writeScaffoldRecord(target, CLI_VERSION, Object.fromEntries(Object.keys(machineryFiles(answers)).filter((file) => file in files).map((file) => [file, digestOf(files[file])])));
5352
5755
  const wantsInstall = options.install !== false;
5353
5756
  const installed = wantsInstall && await install(packageManager, target);
5354
5757
  /**
@@ -5710,7 +6113,7 @@ var explainCommand = (name, options) => {
5710
6113
  }
5711
6114
  const explanations = explain(name);
5712
6115
  if (explanations.length === 0) {
5713
- fail(`"${name}" is no element, step, trigger, code or transformer. See what there is: --list ${kinds}.`);
6116
+ fail(`"${name}" is no element, step, trigger, code, transformer or helper. See what there is: --list ${kinds}.`);
5714
6117
  return;
5715
6118
  }
5716
6119
  console.log(options.json ? JSON.stringify(explanations) : explanations.map(explanationText).join("\n\n"));
@@ -5815,6 +6218,7 @@ var verdict = (before, written, after) => {
5815
6218
  };
5816
6219
  //#endregion
5817
6220
  //#region src/fix/sourceEdits.ts
6221
+ var one = (change) => ({ changes: [change] });
5818
6222
  var SINGLE = "'";
5819
6223
  var quoted = (text) => `${SINGLE}${text.replace(/\\/g, "\\\\").replaceAll(SINGLE, `\\${SINGLE}`)}${SINGLE}`;
5820
6224
  var literalText = (value) => typeof value === "string" ? quoted(value) : String(value);
@@ -5910,27 +6314,27 @@ var editKey = (ts, sourceFile, object, nested, edit) => {
5910
6314
  const property = holder ? findProperty(ts, holder, edit.key) : void 0;
5911
6315
  if (edit.op === "set") {
5912
6316
  if (edit.value === void 0) return { unplaced: "there is no value to write" };
5913
- if (!holder || !property) return { change: insertInto(sourceFile, inner ?? object, edit.key, literalText(edit.value)) };
6317
+ if (!holder || !property) return one(insertInto(sourceFile, inner ?? object, edit.key, literalText(edit.value)));
5914
6318
  if (!ts.isPropertyAssignment(property) || !isLiteral(ts, property.initializer)) return { unplaced: `\`${edit.key}\` is not written as a value there` };
5915
- return { change: {
6319
+ return one({
5916
6320
  start: property.initializer.getStart(sourceFile),
5917
6321
  end: property.initializer.getEnd(),
5918
6322
  text: literalText(edit.value)
5919
- } };
6323
+ });
5920
6324
  }
5921
6325
  if (!holder || !property) return { unplaced: `\`${edit.key}\` is not written in that call` };
5922
- if (edit.op === "remove") return { change: removeFromList(sourceFile, holder.properties, holder.properties.indexOf(property)) };
6326
+ if (edit.op === "remove") return one(removeFromList(sourceFile, holder.properties, holder.properties.indexOf(property)));
5923
6327
  if (edit.op === "rename" && edit.to !== void 0) {
5924
- if (ts.isShorthandPropertyAssignment(property)) return { change: {
6328
+ if (ts.isShorthandPropertyAssignment(property)) return one({
5925
6329
  start: property.getStart(sourceFile),
5926
6330
  end: property.getEnd(),
5927
6331
  text: `${keyText(edit.to)}: ${property.name.text}`
5928
- } };
5929
- if (ts.isPropertyAssignment(property)) return { change: {
6332
+ });
6333
+ if (ts.isPropertyAssignment(property)) return one({
5930
6334
  start: property.name.getStart(sourceFile),
5931
6335
  end: property.name.getEnd(),
5932
6336
  text: keyText(edit.to)
5933
- } };
6337
+ });
5934
6338
  }
5935
6339
  return { unplaced: `\`${edit.key}\` is not a property that edit applies to` };
5936
6340
  };
@@ -5946,18 +6350,18 @@ var editBinding = (ts, sourceFile, props, edit) => {
5946
6350
  ts.forEachChild(node, visit);
5947
6351
  };
5948
6352
  visit(list);
5949
- return found ? { change: {
6353
+ return found ? one({
5950
6354
  start: found.getStart(sourceFile),
5951
6355
  end: found.getEnd(),
5952
6356
  text: quoted(edit.to)
5953
- } } : { unplaced: `"${edit.key}" is not written as a transformer there` };
6357
+ }) : { unplaced: `"${edit.key}" is not written as a transformer there` };
5954
6358
  }
5955
6359
  if (edit.op !== "remove") return { unplaced: "that change to a binding has no one way to be written" };
5956
- const dropBind = () => ({ change: removeFromList(sourceFile, props.properties, props.properties.indexOf(bind)) });
6360
+ const dropBind = () => one(removeFromList(sourceFile, props.properties, props.properties.indexOf(bind)));
5957
6361
  if (ts.isObjectLiteralExpression(list)) {
5958
6362
  const property = findProperty(ts, list, edit.key);
5959
6363
  if (!property) return { unplaced: `the binding onto "${edit.key}" is not written there` };
5960
- return list.properties.length === 1 ? dropBind() : { change: removeFromList(sourceFile, list.properties, list.properties.indexOf(property)) };
6364
+ return list.properties.length === 1 ? dropBind() : one(removeFromList(sourceFile, list.properties, list.properties.indexOf(property)));
5961
6365
  }
5962
6366
  if (ts.isArrayLiteralExpression(list)) {
5963
6367
  const index = list.elements.findIndex((entry) => {
@@ -5966,10 +6370,95 @@ var editBinding = (ts, sourceFile, props, edit) => {
5966
6370
  return to !== void 0 && ts.isPropertyAssignment(to) && ts.isStringLiteral(to.initializer) && to.initializer.text === edit.key;
5967
6371
  });
5968
6372
  if (index === -1) return { unplaced: `the binding onto "${edit.key}" is not written there` };
5969
- return list.elements.length === 1 ? dropBind() : { change: removeFromList(sourceFile, list.elements, index) };
6373
+ return list.elements.length === 1 ? dropBind() : one(removeFromList(sourceFile, list.elements, index));
5970
6374
  }
5971
6375
  return { unplaced: "`bind` is not written as a literal there" };
5972
6376
  };
6377
+ /** The factories a child of the moved kind is written with: what a move may leave imported and unused. */
6378
+ var WORD_FACTORIES = ["text", "fontAwesome"];
6379
+ /**
6380
+ * `children: [text(words), fontAwesome({ icon })]` written as the element's own `content: words, icon` — the words
6381
+ * kept as written, a literal or an expression (`entry.label`). Only children written exactly so are moved: a `text`
6382
+ * handed options (a class, an id) or an icon with more than its class is something the element's own attributes would
6383
+ * not say, and is left for the author.
6384
+ */
6385
+ var moveChildrenToContent = (ts, sourceFile, props, edit) => {
6386
+ const children = findProperty(ts, props, "children");
6387
+ if (!children || !ts.isPropertyAssignment(children) || !ts.isArrayLiteralExpression(children.initializer)) return { unplaced: "`children` is not written as a list there" };
6388
+ let words;
6389
+ let icon;
6390
+ for (const child of children.initializer.elements) {
6391
+ const call = ts.isCallExpression(child) && ts.isIdentifier(child.expression) && child.arguments.length === 1 ? {
6392
+ factory: child.expression.text,
6393
+ argument: child.arguments[0]
6394
+ } : void 0;
6395
+ if (call?.factory === "text" && !words) {
6396
+ words = call.argument;
6397
+ continue;
6398
+ }
6399
+ const iconProperty = call?.factory === "fontAwesome" && ts.isObjectLiteralExpression(call.argument) && call.argument.properties.length === 1 ? findProperty(ts, call.argument, "icon") : void 0;
6400
+ if (iconProperty && ts.isPropertyAssignment(iconProperty) && !icon) {
6401
+ icon = iconProperty.initializer;
6402
+ continue;
6403
+ }
6404
+ return { unplaced: "a child carries more than its words or its icon there" };
6405
+ }
6406
+ if (Boolean(icon) !== (edit.icon !== void 0) || words === void 0 && edit.value !== "") return { unplaced: "the children written there are not the ones it holds" };
6407
+ if (words && ts.isStringLiteral(words) && words.text !== edit.value) return { unplaced: "the words written there are not the ones it shows" };
6408
+ if (icon && findProperty(ts, props, "icon")) return { unplaced: "it is written with an `icon` of its own already" };
6409
+ const content = findProperty(ts, props, "content");
6410
+ if (content && words && !(ts.isPropertyAssignment(content) && ts.isStringLiteral(content.initializer) && content.initializer.text === "")) return { unplaced: "it is written with words of its own beside its children" };
6411
+ const written = [
6412
+ words && `content: ${words.getText(sourceFile)}`,
6413
+ icon && `icon: ${icon.getText(sourceFile)}`,
6414
+ edit.iconPlacement && `iconPlacement: ${quoted(edit.iconPlacement)}`
6415
+ ].filter((part) => Boolean(part));
6416
+ return {
6417
+ changes: [...content && words ? [removeFromList(sourceFile, props.properties, props.properties.indexOf(content))] : [], {
6418
+ start: children.getStart(sourceFile),
6419
+ end: children.getEnd(),
6420
+ text: written.join(", ")
6421
+ }],
6422
+ orphans: WORD_FACTORIES
6423
+ };
6424
+ };
6425
+ /**
6426
+ * The named imports among `names` that nothing in the file uses any longer — what a move left behind, taken out with
6427
+ * the import that held only them.
6428
+ */
6429
+ var pruneImports = (ts, fileName, text, names) => {
6430
+ const sourceFile = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
6431
+ const used = /* @__PURE__ */ new Set();
6432
+ const visit = (node) => {
6433
+ if (ts.isIdentifier(node) && !ts.isImportSpecifier(node.parent)) used.add(node.text);
6434
+ ts.forEachChild(node, visit);
6435
+ };
6436
+ visit(sourceFile);
6437
+ const changes = [];
6438
+ for (const statement of sourceFile.statements) {
6439
+ const clause = ts.isImportDeclaration(statement) ? statement.importClause : void 0;
6440
+ const bindings = clause?.namedBindings;
6441
+ if (!clause || !bindings || !ts.isNamedImports(bindings)) continue;
6442
+ const elements = bindings.elements;
6443
+ const unused = elements.filter((element) => names.includes(element.name.text) && !used.has(element.name.text));
6444
+ if (unused.length === 0) continue;
6445
+ if (unused.length === elements.length && !clause.name) {
6446
+ changes.push({
6447
+ start: statement.getFullStart(),
6448
+ end: statement.getEnd(),
6449
+ text: ""
6450
+ });
6451
+ continue;
6452
+ }
6453
+ const kept = elements.filter((element) => !unused.includes(element));
6454
+ changes.push({
6455
+ start: bindings.getStart(sourceFile),
6456
+ end: bindings.getEnd(),
6457
+ text: `{ ${kept.map((element) => element.getText(sourceFile)).join(", ")} }`
6458
+ });
6459
+ }
6460
+ return applyChanges(text, changes) ?? text;
6461
+ };
5973
6462
  /**
5974
6463
  * What an edit planned on a spec changes in the source that wrote it: the call at its position, the props it was
5975
6464
  * handed, and in them only what is written as a literal. Anything else — a value computed, props spread from a
@@ -5981,6 +6470,7 @@ var sourceEdit = (ts, sourceFile, position, edit) => {
5981
6470
  const props = propsOf(ts, call);
5982
6471
  if (!props) return { unplaced: "the call that wrote it was handed no props to edit" };
5983
6472
  if (edit.on === "binding") return editBinding(ts, sourceFile, props, edit);
6473
+ if (edit.on === "children") return moveChildrenToContent(ts, sourceFile, props, edit);
5984
6474
  if (edit.on === "step") {
5985
6475
  const step = edit.step ? stepIn(ts, props, edit.step.flow, edit.step.index) : void 0;
5986
6476
  return step ? editKey(ts, sourceFile, step, "params", edit) : { unplaced: "the step is not written in that call" };
@@ -6047,7 +6537,8 @@ var editSources = async (root, fixes) => {
6047
6537
  else placed.push({
6048
6538
  fix,
6049
6539
  file,
6050
- change: outcome.change
6540
+ changes: outcome.changes,
6541
+ orphans: outcome.orphans ?? []
6051
6542
  });
6052
6543
  }
6053
6544
  const format = await projectFormatter(root);
@@ -6055,7 +6546,7 @@ var editSources = async (root, fixes) => {
6055
6546
  const written = [];
6056
6547
  for (const [file, { text }] of sources) {
6057
6548
  const mine = placed.filter((entry) => entry.file === file);
6058
- const changed = applyChanges(text, mine.map((entry) => entry.change));
6549
+ const changed = applyChanges(text, mine.flatMap((entry) => entry.changes));
6059
6550
  if (changed === void 0) {
6060
6551
  left.push(...mine.map((entry) => ({
6061
6552
  fix: entry.fix,
@@ -6064,10 +6555,12 @@ var editSources = async (root, fixes) => {
6064
6555
  continue;
6065
6556
  }
6066
6557
  if (mine.length > 0) {
6558
+ const orphans = [...new Set(mine.flatMap((entry) => entry.orphans))];
6559
+ const pruned = ts && orphans.length > 0 ? pruneImports(ts, file, changed, orphans) : changed;
6067
6560
  files.push({
6068
6561
  file,
6069
6562
  before: text,
6070
- after: await formatLikeBefore(format, path.relative(root, file), text, changed)
6563
+ after: await formatLikeBefore(format, path.relative(root, file), text, pruned)
6071
6564
  });
6072
6565
  written.push(...mine.map((entry) => entry.fix));
6073
6566
  }
@@ -6258,18 +6751,18 @@ var measure = async (browser, url, width, scheme, read = []) => {
6258
6751
  };
6259
6752
  };
6260
6753
  /** Every file under a folder, by its path inside it — none when there is no such folder. */
6261
- var filesUnder = async (folder) => {
6754
+ var filesUnder$1 = async (folder) => {
6262
6755
  return (await readdir(folder, {
6263
6756
  recursive: true,
6264
6757
  withFileTypes: true
6265
6758
  }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => path.relative(folder, path.join(entry.parentPath, entry.name)));
6266
6759
  };
6267
- var count = (amount, one, many = `${one}s`) => `${String(amount)} ${amount === 1 ? one : many}`;
6760
+ var count$1 = (amount, one, many = `${one}s`) => `${String(amount)} ${amount === 1 ? one : many}`;
6268
6761
  var summaryText = (summary, out, stale) => [
6269
6762
  chalk.green(`Imported ${summary.url} into ${out}`),
6270
- ` tokens ${count(summary.colours, "colour")} (${summary.dark ? "light and dark" : "no dark scheme: dark repeats light"}), ${count(summary.shadows, "shadow")}, ${count(summary.radii, "radius", "radii")}${summary.fonts.length > 0 ? `, fonts ${summary.fonts.join(", ")}` : ""}`,
6271
- ` outline ${count(summary.blocks, "block")} at ${summary.widths.join(", ")} px`,
6272
- ...summary.lists.length > 0 ? [` lists ${summary.lists.map((list) => `${list.file} (${count(list.rows, "row")})`).join(", ")}`] : [],
6763
+ ` tokens ${count$1(summary.colours, "colour")} (${summary.dark ? "light and dark" : "no dark scheme: dark repeats light"}), ${count$1(summary.shadows, "shadow")}, ${count$1(summary.radii, "radius", "radii")}${summary.fonts.length > 0 ? `, fonts ${summary.fonts.join(", ")}` : ""}`,
6764
+ ` outline ${count$1(summary.blocks, "block")} at ${summary.widths.join(", ")} px`,
6765
+ ...summary.lists.length > 0 ? [` lists ${summary.lists.map((list) => `${list.file} (${count$1(list.rows, "row")})`).join(", ")}`] : [],
6273
6766
  ` pictures ${String(summary.pictures)} in assets.json`,
6274
6767
  ...stale.length > 0 ? [chalk.yellow(` earlier ${stale.join(", ")} ${stale.length === 1 ? "was" : "were"} there before and not written this time: delete what is stale`)] : [],
6275
6768
  ` next ${path.join(out, "IMPORT.md")} says what was not carried over`
@@ -6294,7 +6787,7 @@ var importPage = async (address, options) => {
6294
6787
  fail(`${out} is outside the project: the import is written inside it.`);
6295
6788
  return;
6296
6789
  }
6297
- const existing = await filesUnder(target);
6790
+ const existing = await filesUnder$1(target);
6298
6791
  if (existing.length > 0 && !options.force) {
6299
6792
  fail(`${out} already has files: choose another --out, or --force to write over them.`);
6300
6793
  return;
@@ -6392,16 +6885,16 @@ var a = [
6392
6885
  pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/
6393
6886
  }
6394
6887
  ];
6395
- var o = (e) => {
6888
+ var o$1 = (e) => {
6396
6889
  let t = e.split("/");
6397
6890
  if (!e || e.startsWith("/") || /^[a-zA-Z]:/.test(e) || e.includes("\\")) return `"${e}" is not a path relative to the project, with forward slashes`;
6398
6891
  if (t.some((e) => e === "" || e === "." || e === "..")) return `"${e}" climbs out of the project, or has an empty part`;
6399
6892
  if (t.some((e) => e === "node_modules" || e === ".git")) return `"${e}" is in node_modules or .git: a package is a dependency, not a file of the project`;
6400
6893
  if (i.some((t) => t.test(e))) return `"${e}" is a file of credentials: they stay where the project runs, never in what Plitzi keeps`;
6401
6894
  };
6402
- var s = (e) => a.find(({ pattern: t }) => t.test(e))?.what;
6403
- var c = (e) => typeof e == "string" && r.some((t) => t === e);
6404
- var l = (e) => typeof e == "string" && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(e);
6895
+ var s$1 = (e) => a.find(({ pattern: t }) => t.test(e))?.what;
6896
+ var c$1 = (e) => typeof e == "string" && r.some((t) => t === e);
6897
+ var l$1 = (e) => typeof e == "string" && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(e);
6405
6898
  var u = (e) => Array.isArray(e) && e.length > 0 && e.every((e) => typeof e == "string");
6406
6899
  var d = (t) => e$1(t) && Object.values(t).every((e) => typeof e == "string");
6407
6900
  var f = (e) => Math.floor(e.length * 3 / 4) - (e.endsWith("==") ? 2 : +!!e.endsWith("="));
@@ -6411,7 +6904,7 @@ var p = (t, i) => {
6411
6904
  problems: ["Not a source snapshot of format 1"]
6412
6905
  };
6413
6906
  let { kind: a, name: p, entries: m, files: h, dependencies: g } = t, _ = [];
6414
- if (c(a) || _.push(`Its kind is one of ${r.join(", ")}`), l(p) || _.push("Its name is a plugin type or \"runtime\": letters, digits, dashes and underscores"), !d(h) || !d(g)) return {
6907
+ if (c$1(a) || _.push(`Its kind is one of ${r.join(", ")}`), l$1(p) || _.push("Its name is a plugin type or \"runtime\": letters, digits, dashes and underscores"), !d(h) || !d(g)) return {
6415
6908
  ok: !1,
6416
6909
  problems: [..._, "Its files and dependencies are maps of text"]
6417
6910
  };
@@ -6420,7 +6913,7 @@ var p = (t, i) => {
6420
6913
  v.length > n.files && _.push(`It holds ${String(v.length)} files; a snapshot holds at most ${String(n.files)}`);
6421
6914
  let y = 0;
6422
6915
  return v.forEach((e) => {
6423
- let t = o(e);
6916
+ let t = o$1(e);
6424
6917
  if (t) {
6425
6918
  _.push(t);
6426
6919
  return;
@@ -6430,9 +6923,9 @@ var p = (t, i) => {
6430
6923
  _.push(`"${e}" is ${(r / 1024 / 1024).toFixed(1)} MB; a file of source is at most 4 MB`);
6431
6924
  return;
6432
6925
  }
6433
- let a = s(i(h[e]));
6926
+ let a = s$1(i(h[e]));
6434
6927
  a && _.push(`"${e}" holds ${a}: take it out of the code and give it to the project as a variable`);
6435
- }), y > n.totalBytes && _.push(`It holds ${(y / 1024 / 1024).toFixed(1)} MB of files; a snapshot holds at most 24 MB`), _.length > 0 || !c(a) || !l(p) || !u(m) ? {
6928
+ }), y > n.totalBytes && _.push(`It holds ${(y / 1024 / 1024).toFixed(1)} MB of files; a snapshot holds at most 24 MB`), _.length > 0 || !c$1(a) || !l$1(p) || !u(m) ? {
6436
6929
  ok: !1,
6437
6930
  problems: _
6438
6931
  } : {
@@ -7050,9 +7543,7 @@ var pull = async (options) => {
7050
7543
  if (!exported) return;
7051
7544
  const next = projectFromSpace(exported, origin.source);
7052
7545
  const format = await projectFormatter(root);
7053
- const given = /* @__PURE__ */ new Map();
7054
- for (const [file, text] of Object.entries(next.files)) given.set(file, Buffer.from(await format(file, text)));
7055
- Object.entries(next.binaries).forEach(([file, base64]) => given.set(file, Buffer.from(base64, "base64")));
7546
+ const given = await givenFiles(next, format);
7056
7547
  const missing = [];
7057
7548
  const untouched = /* @__PURE__ */ new Set();
7058
7549
  for (const { url, to } of next.downloads) {
@@ -7099,10 +7590,12 @@ var pull = async (options) => {
7099
7590
  if (dependencies.changed.length > 0) await fs.writeFile(packageFile, `${JSON.stringify(dependencies.manifest, null, 2)}\n`);
7100
7591
  const files = Object.fromEntries([...[...given].map(([file, bytes]) => [file, digest(bytes)]), ...[...untouched].flatMap((file) => origin.files[file] ? [[file, origin.files[file]]] : [])]);
7101
7592
  await writeOrigin(root, {
7102
- ...origin,
7593
+ format: origin.format,
7103
7594
  api,
7595
+ source: origin.source,
7104
7596
  space: exported.space,
7105
7597
  version: asked.version,
7598
+ ...exported.draft ? { draft: exported.draft } : {},
7106
7599
  files,
7107
7600
  downloads: Object.fromEntries(next.downloads.flatMap(({ url, to }) => {
7108
7601
  if (given.has(to)) return [[to, url]];
@@ -7136,25 +7629,221 @@ var pull = async (options) => {
7136
7629
  console.log("");
7137
7630
  };
7138
7631
  //#endregion
7139
- //#region src/commands/keepSource.ts
7140
- /**
7141
- * A packed source snapshot sent beside what was just uploaded or pushed, and kept on the space (docs/en/projects-from-spaces.md): what lets
7142
- * `plitzi create --from` take the space back out as a project. Never undoes what went up before it — the plugin or the
7143
- * runtime runs either way — so a refusal is said, with why, and the command still succeeds.
7144
- */
7145
- var keepSource = async (connection, spaceId, bytes) => {
7146
- const sent = await authorizedRequest(connection, `/spaces/${String(spaceId)}/sources`, {
7147
- method: "PUT",
7148
- headers: { "Content-Type": "application/octet-stream" },
7149
- body: new Uint8Array(bytes)
7150
- });
7151
- if (!sent.ok) {
7152
- console.log(chalk.yellow(` Its source is not kept: ${sent.error}`));
7153
- return;
7632
+ //#region src/commands/askChecks.ts
7633
+ /** What a key does to the list: a new state, or the end of the question — taken or given up. */
7634
+ var checksAfter = (state, key) => {
7635
+ const count = state.checked.length;
7636
+ if (key.name === "escape" || key.ctrl && key.name === "c") return "cancel";
7637
+ if (key.name === "return" || key.name === "enter") return "done";
7638
+ if (key.name === "up" || key.name === "k") return {
7639
+ ...state,
7640
+ cursor: (state.cursor - 1 + count) % count
7641
+ };
7642
+ if (key.name === "down" || key.name === "j") return {
7643
+ ...state,
7644
+ cursor: (state.cursor + 1) % count
7645
+ };
7646
+ if (key.name === "space") return {
7647
+ ...state,
7648
+ checked: state.checked.map((on, index) => index === state.cursor ? !on : on)
7649
+ };
7650
+ if (key.name === "a") {
7651
+ const all = state.checked.every(Boolean);
7652
+ return {
7653
+ ...state,
7654
+ checked: state.checked.map(() => !all)
7655
+ };
7154
7656
  }
7155
- const { status, data } = sent.value.reply;
7156
- if (status === 200 && data.ok) {
7157
- console.log(chalk.dim(` Its source too (${String(data.files ?? 0)} files): plitzi create --from can take it back out.`));
7657
+ return state;
7658
+ };
7659
+ /** The list as it is drawn: the question, a line per option, and the keys. */
7660
+ var renderChecks = (question, options, state) => [
7661
+ chalk.bold(question),
7662
+ ...options.map(({ label, hint }, index) => {
7663
+ return `${index === state.cursor ? chalk.cyan("›") : " "} ${state.checked[index] ? chalk.green("[x]") : "[ ]"} ${label}${hint ? chalk.dim(` ${hint}`) : ""}`;
7664
+ }),
7665
+ chalk.dim(" ↑/↓ move · space ticks · a all · enter takes · esc cancels")
7666
+ ];
7667
+ /** What is ticked once the person takes the list — `undefined` when they give it up. */
7668
+ var askChecks = async (question, options) => {
7669
+ const { stdin, stdout } = process;
7670
+ let state = {
7671
+ cursor: 0,
7672
+ checked: options.map(({ checked }) => checked)
7673
+ };
7674
+ let drawn = 0;
7675
+ const draw = () => {
7676
+ const lines = renderChecks(question, options, state);
7677
+ stdout.write(`${drawn ? `\x1b[${String(drawn)}A\x1b[0J` : ""}${lines.join("\n")}\n`);
7678
+ drawn = lines.length;
7679
+ };
7680
+ emitKeypressEvents(stdin);
7681
+ const wasRaw = stdin.isRaw;
7682
+ stdin.setRawMode(true);
7683
+ stdin.resume();
7684
+ stdout.write("\x1B[?25l");
7685
+ draw();
7686
+ return new Promise((resolve) => {
7687
+ const onKey = (_text, key) => {
7688
+ const next = checksAfter(state, key ?? {});
7689
+ if (next !== "done" && next !== "cancel") {
7690
+ state = next;
7691
+ draw();
7692
+ return;
7693
+ }
7694
+ stdin.off("keypress", onKey);
7695
+ stdin.setRawMode(wasRaw);
7696
+ stdin.pause();
7697
+ stdout.write("\x1B[?25h");
7698
+ resolve(next === "done" ? options.filter((_option, index) => state.checked[index]).map(({ value }) => value) : void 0);
7699
+ };
7700
+ stdin.on("keypress", onKey);
7701
+ });
7702
+ };
7703
+ //#endregion
7704
+ //#region src/commands/pushSpace.ts
7705
+ /** The JSON documents of a folder, by file: none when there is no such folder. */
7706
+ var readJsonFolder = async (dir) => {
7707
+ let files;
7708
+ try {
7709
+ files = (await fs.readdir(dir)).filter((file) => file.endsWith(".json")).sort();
7710
+ } catch {
7711
+ return;
7712
+ }
7713
+ return Promise.all(files.map(async (file) => ({
7714
+ file,
7715
+ value: JSON.parse(await fs.readFile(path.join(dir, file), "utf-8"))
7716
+ })));
7717
+ };
7718
+ /** The project's server actions, as its `src/actions.ts` serves them — `undefined` when it has no such module. */
7719
+ var projectActions = async (root) => {
7720
+ const file = path.join(root, "src/actions.ts");
7721
+ try {
7722
+ await fs.access(file);
7723
+ } catch {
7724
+ return;
7725
+ }
7726
+ const loaded = await import(pathToFileURL(file).href);
7727
+ const lookups = e$1(loaded) && e$1(loaded.lookups) ? loaded.lookups : void 0;
7728
+ if (!lookups || typeof lookups.listActions !== "function") return { problem: "src/actions.ts exports no `lookups` with a listActions: it is where the project’s actions are." };
7729
+ const listed = await lookups.listActions();
7730
+ if (!Array.isArray(listed)) return { problem: "src/actions.ts: lookups.listActions() answers no list." };
7731
+ return listed.flatMap((entry) => e$1(entry) && typeof entry.id === "string" && e$1(entry.document) ? [{
7732
+ identifier: entry.id,
7733
+ name: typeof entry.document.name === "string" ? entry.document.name : entry.id,
7734
+ document: entry.document
7735
+ }] : []);
7736
+ };
7737
+ /** The connectors in `src/connectors/`, one manifest a file — `undefined` when there is no such folder. */
7738
+ var projectConnectors = async (root) => {
7739
+ const read = await readJsonFolder(path.join(root, "src/connectors"));
7740
+ if (!read) return;
7741
+ const broken = read.find(({ value }) => !e$1(value) || typeof value.id !== "string" || !e$1(value.manifest));
7742
+ return broken ? { problem: `src/connectors/${broken.file} is not a connector: { "id", "name", "manifest" }.` } : read.flatMap(({ value }) => e$1(value) && typeof value.id === "string" && e$1(value.manifest) ? [{
7743
+ identifier: value.id,
7744
+ name: typeof value.name === "string" ? value.name : value.id,
7745
+ manifest: value.manifest
7746
+ }] : []);
7747
+ };
7748
+ /** The element types of plugins the project runs as they were built (`vendor/plugins/<type>/`): authored as known. */
7749
+ var builtPluginTypes = async (root) => {
7750
+ try {
7751
+ return (await fs.readdir(path.join(root, "vendor/plugins"), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
7752
+ } catch {
7753
+ return [];
7754
+ }
7755
+ };
7756
+ var count = (entries, noun) => entries ? `${String(entries.length)} ${noun}${entries.length === 1 ? "" : "s"}` : `the space’s ${noun}s as they are`;
7757
+ /**
7758
+ * The project's space sent as the draft of the space the connection works in: refused when the draft moved on since
7759
+ * `base` (the draft the project last had), or — with no base — when the space holds work of its own; `force` takes it
7760
+ * anyway.
7761
+ */
7762
+ var pushSpaceOf = async (root, connection, space, { base, force }) => {
7763
+ const project = await loadProjectSpace(root);
7764
+ if ("problem" in project) {
7765
+ fail(project.problem);
7766
+ return { outcome: "failed" };
7767
+ }
7768
+ let documents;
7769
+ try {
7770
+ const { schema, style } = authorSpace(project.space, {
7771
+ plugins: project.plugins,
7772
+ pluginTypes: await builtPluginTypes(root)
7773
+ });
7774
+ documents = {
7775
+ schema,
7776
+ style
7777
+ };
7778
+ } catch (error) {
7779
+ fail(`The space does not author, so nothing was pushed — npm run author says why:\n${String(error instanceof Error ? error.message : error)}`);
7780
+ return { outcome: "failed" };
7781
+ }
7782
+ const [actions, connectors] = await Promise.all([projectActions(root), projectConnectors(root)]);
7783
+ for (const read of [actions, connectors]) if (read && "problem" in read) {
7784
+ fail(read.problem);
7785
+ return { outcome: "failed" };
7786
+ }
7787
+ const sent = {
7788
+ format: 1,
7789
+ documents,
7790
+ ...Array.isArray(actions) ? { actions } : {},
7791
+ ...Array.isArray(connectors) ? { connectors } : {},
7792
+ base,
7793
+ force
7794
+ };
7795
+ const answered = await authorizedRequest(connection, `/spaces/${String(space.id)}/import`, {
7796
+ method: "PUT",
7797
+ headers: { "Content-Type": "application/json" },
7798
+ body: JSON.stringify(sent)
7799
+ });
7800
+ if (!answered.ok) {
7801
+ fail(answered.error);
7802
+ return { outcome: "failed" };
7803
+ }
7804
+ const { status, data } = answered.value.reply;
7805
+ if (status === 409) {
7806
+ fail(data.refusal?.code === "DRAFT_NOT_EMPTY" ? `${space.name} already holds work of its own, and this project never had it: pushing would replace it.\nTake it out first (plitzi create --from ${space.permanentUrl}), or pass --force to replace it with this project.` : `${space.name}’s draft changed since this project last had it — in the builder, or from another copy.\nPull first (plitzi pull) and push again, or pass --force to replace it with this project.`);
7807
+ return { outcome: "failed" };
7808
+ }
7809
+ if (status === 422 && data.problems) {
7810
+ fail(`${space.name} would not take the space as it is:${data.problems.map((problem) => `\n ${problem}`).join("")}`);
7811
+ return { outcome: "failed" };
7812
+ }
7813
+ if (status !== 200 || !data.draft) {
7814
+ fail(data.error ?? `The space was not pushed (${String(status)}).`);
7815
+ return { outcome: "failed" };
7816
+ }
7817
+ if (!data.changed) return {
7818
+ outcome: "unchanged",
7819
+ draft: data.draft
7820
+ };
7821
+ console.log(chalk.green(`The space is ${space.name}’s draft now — its pages and styles, ${count(sent.actions, "action")}, ${count(sent.connectors, "connector")}.`) + chalk.dim(" Any builder open on it shows it now."));
7822
+ return {
7823
+ outcome: "pushed",
7824
+ draft: data.draft
7825
+ };
7826
+ };
7827
+ //#endregion
7828
+ //#region src/commands/keepSource.ts
7829
+ /**
7830
+ * A packed source snapshot sent beside what was just uploaded or pushed, and kept on the space (docs/en/projects-from-spaces.md): what lets
7831
+ * `plitzi create --from` take the space back out as a project. Never undoes what went up before it — the plugin or the
7832
+ * runtime runs either way — so a refusal is said, with why, and the command still succeeds.
7833
+ */
7834
+ var keepSource = async (connection, spaceId, bytes) => {
7835
+ const sent = await authorizedRequest(connection, `/spaces/${String(spaceId)}/sources`, {
7836
+ method: "PUT",
7837
+ headers: { "Content-Type": "application/octet-stream" },
7838
+ body: new Uint8Array(bytes)
7839
+ });
7840
+ if (!sent.ok) {
7841
+ console.log(chalk.yellow(` Its source is not kept: ${sent.error}`));
7842
+ return;
7843
+ }
7844
+ const { status, data } = sent.value.reply;
7845
+ if (status === 200 && data.ok) {
7846
+ console.log(chalk.dim(` Its source too (${String(data.files ?? 0)} files): plitzi create --from can take it back out.`));
7158
7847
  return;
7159
7848
  }
7160
7849
  const problems = data.problems ?? [`The platform answered ${String(status)}`];
@@ -7163,7 +7852,6 @@ var keepSource = async (connection, spaceId, bytes) => {
7163
7852
  };
7164
7853
  //#endregion
7165
7854
  //#region src/commands/runtime.ts
7166
- var DEFAULT_ENTRY = path.join("src", "runtime.ts");
7167
7855
  var isPacker = (value) => e$1(value) && typeof value.packRuntime === "function";
7168
7856
  /** This project's own packer — read, since it is the project's. */
7169
7857
  var projectPacker = async (root) => {
@@ -7174,42 +7862,42 @@ var projectPacker = async (root) => {
7174
7862
  } catch {}
7175
7863
  fail("plitzi runtime packs it with this project’s own @plitzi/sdk-server:\n npm install @plitzi/sdk-server");
7176
7864
  };
7177
- /** Packs the project's runtime module and keeps it as the space's draft runtime — which a publish takes live. */
7178
- var pushRuntime = async (options) => {
7179
- const root = (await projectHere("whose runtime to push"))?.root;
7180
- const connection = root && await connectToSpace(options, "to push to");
7181
- if (!root || !connection || !connection.space) return;
7182
- const entry = path.resolve(root, options.entry ?? DEFAULT_ENTRY);
7865
+ var DEFAULT_RUNTIME_ENTRY = path.join("src", "runtime.ts");
7866
+ /**
7867
+ * The project's runtime module packed and kept as the draft runtime of the space the connection works in — which a
7868
+ * publish takes live — with the source it was packed from beside it.
7869
+ */
7870
+ var pushRuntimeOf = async (root, connection, space, entry) => {
7183
7871
  try {
7184
7872
  await fs.access(entry);
7185
7873
  } catch {
7186
7874
  fail(`There is no ${path.relative(root, entry)}: the module whose default export is defineRuntime({ start }).`);
7187
- return;
7875
+ return "failed";
7188
7876
  }
7189
7877
  const packer = await projectPacker(root);
7190
- if (!packer) return;
7878
+ if (!packer) return "failed";
7191
7879
  let bytes;
7192
7880
  try {
7193
7881
  bytes = await packer.packRuntime(entry);
7194
7882
  } catch (error) {
7195
7883
  fail(`It does not pack: ${error instanceof Error ? error.message : String(error)}`);
7196
- return;
7884
+ return "failed";
7197
7885
  }
7198
- const answered = await authorizedRequest(connection, `/spaces/${String(connection.space.id)}/runtime`, {
7886
+ const answered = await authorizedRequest(connection, `/spaces/${String(space.id)}/runtime`, {
7199
7887
  method: "PUT",
7200
7888
  headers: { "content-type": "application/octet-stream" },
7201
7889
  body: new Uint8Array(bytes)
7202
7890
  });
7203
7891
  if (!answered.ok) {
7204
7892
  fail(answered.error);
7205
- return;
7893
+ return "failed";
7206
7894
  }
7207
7895
  const { reply } = answered.value;
7208
7896
  if (reply.status !== 200 || !reply.data.digest) {
7209
7897
  fail(reply.data.error ?? `The runtime was not kept (${String(reply.status)}).`);
7210
- return;
7898
+ return "failed";
7211
7899
  }
7212
- console.log(chalk.green(`${connection.space.name}’s draft runtime is ${reply.data.digest.slice(0, 12)} (${(bytes.byteLength / 1024).toFixed(0)} KB).`) + chalk.dim(" It starts in a moment; publish the space to take it live."));
7900
+ console.log(chalk.green(`${space.name}’s draft runtime is ${reply.data.digest.slice(0, 12)} (${(bytes.byteLength / 1024).toFixed(0)} KB).`) + chalk.dim(" It starts in a moment; publish the space to take it live."));
7213
7901
  try {
7214
7902
  const source = await packSource({
7215
7903
  root,
@@ -7217,11 +7905,19 @@ var pushRuntime = async (options) => {
7217
7905
  name: "runtime",
7218
7906
  entries: [entry]
7219
7907
  });
7220
- await keepSource(answered.value.connection, connection.space.id, source.bytes);
7908
+ await keepSource(answered.value.connection, space.id, source.bytes);
7221
7909
  } catch (error) {
7222
7910
  console.log(chalk.yellow(" Its source is not kept, so a project taken from the space gets it built only:"));
7223
7911
  console.log(chalk.yellow(` ${error instanceof Error ? error.message : String(error)}`));
7224
7912
  }
7913
+ return "pushed";
7914
+ };
7915
+ /** Packs the project's runtime module and keeps it as the space's draft runtime — which a publish takes live. */
7916
+ var pushRuntime = async (options) => {
7917
+ const root = (await projectHere("whose runtime to push"))?.root;
7918
+ const connection = root && await connectToSpace(options, "to push to");
7919
+ if (!root || !connection || !connection.space) return;
7920
+ await pushRuntimeOf(root, connection, connection.space, path.resolve(root, options.entry ?? DEFAULT_RUNTIME_ENTRY));
7225
7921
  };
7226
7922
  var readRuntime = async (connection, spaceId) => {
7227
7923
  const answered = await authorizedRequest(connection, `/spaces/${String(spaceId)}/runtime`);
@@ -7362,179 +8058,6 @@ var unsetRuntimeVariable = async (name, options) => {
7362
8058
  console.log(chalk.green(`${name} removed — the runtime starts again without it.`));
7363
8059
  };
7364
8060
  //#endregion
7365
- //#region src/commands/shot.ts
7366
- /** A region moves when more than this share of it changed between two frames: below is anti-aliasing and carets. */
7367
- var MOVING_PERCENT = .5;
7368
- /** A selector, or an element's name — what `data-plitzi-el` carries. */
7369
- var selectorOf = (target) => /^[#.[]/.test(target) ? target : `[data-plitzi-el="${target}"]`;
7370
- var pictureOf = async (browser, url, view) => {
7371
- const page = await browser.newPage({
7372
- viewport: {
7373
- width: view.width,
7374
- height: view.height
7375
- },
7376
- colorScheme: view.scheme,
7377
- reducedMotion: view.reducedMotion ? "reduce" : "no-preference"
7378
- });
7379
- if (!await page.goto(url, { waitUntil: "networkidle" }).catch(() => null)) return { problem: `Nothing answers at ${url}.` };
7380
- if (view.waitFor) {
7381
- if (!await page.waitForSelector(selectorOf(view.waitFor), { timeout: 1e4 }).catch(() => null)) return { problem: `${view.waitFor} did not appear on ${url} within 10 s.` };
7382
- }
7383
- return {
7384
- page,
7385
- png: await page.screenshot({ fullPage: true })
7386
- };
7387
- };
7388
- var regionsText = (regions) => regions.map((region) => `${region.name} ${String(region.changed)}%`).join(", ");
7389
- /** Compared frame to frame: the most each region changed between two that follow each other. */
7390
- var framesMoved = (diffs) => {
7391
- const most = /* @__PURE__ */ new Map();
7392
- for (const diff of diffs) for (const region of diff.regions) most.set(region.name, Math.max(most.get(region.name) ?? 0, region.changed));
7393
- return [...most].map(([name, changed]) => ({
7394
- name,
7395
- changed
7396
- }));
7397
- };
7398
- var shot = async (route, options) => {
7399
- const project = await projectHere("whose page to picture");
7400
- if (!project) return;
7401
- const where = await projectOrigin(project.root, project.plitzi?.kind === "project" ? project.plitzi : void 0);
7402
- if ("problem" in where) {
7403
- fail(where.problem);
7404
- return;
7405
- }
7406
- const browser = await launchBrowser(project.root);
7407
- if ("problem" in browser) {
7408
- fail(browser.problem);
7409
- return;
7410
- }
7411
- try {
7412
- const pathname = route ?? "/";
7413
- const view = {
7414
- width: options.width ?? 1280,
7415
- height: options.height ?? 800,
7416
- scheme: options.scheme ?? "light",
7417
- reducedMotion: options.reducedMotion === true,
7418
- ...options.waitFor ? { waitFor: options.waitFor } : {}
7419
- };
7420
- const name = pathname === "/" ? "home" : pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "");
7421
- const out = path.resolve(project.root, options.out ?? `visual/.shots/${name}-${String(view.width)}-${view.scheme}.png`);
7422
- const base = out.replace(/\.png$/, "");
7423
- const taken = await pictureOf(browser, `${where.origin}${pathname}`, view);
7424
- if ("problem" in taken) {
7425
- fail(`${taken.problem} Start the project first: npm start`);
7426
- return;
7427
- }
7428
- await mkdir(path.dirname(out), { recursive: true });
7429
- await writeFile(out, taken.png);
7430
- const regions = await taken.page.evaluate(pageRegions, void 0);
7431
- const report = {
7432
- path: pathname,
7433
- width: view.width,
7434
- scheme: view.scheme,
7435
- out
7436
- };
7437
- if (options.compare) {
7438
- const other = new URL(options.compare);
7439
- const target = other.pathname === "/" ? new URL(pathname, other).href : other.href;
7440
- const theirs = await pictureOf(browser, target, view);
7441
- if ("problem" in theirs) {
7442
- fail(theirs.problem);
7443
- return;
7444
- }
7445
- const diff = await comparePictures(taken.page, dataUrl(taken.png), dataUrl(theirs.png), regions);
7446
- await writeFile(`${base}-compare.png`, fromDataUrl(diff.sideBySide));
7447
- await writeFile(`${base}-diff.png`, fromDataUrl(diff.diff));
7448
- report.compare = {
7449
- url: target,
7450
- changed: diff.changed,
7451
- regions: diff.regions,
7452
- sideBySide: `${base}-compare.png`,
7453
- diff: `${base}-diff.png`
7454
- };
7455
- }
7456
- const frames = options.frames ?? 0;
7457
- if (frames > 1) {
7458
- const every = options.every ?? 500;
7459
- const pictures = [taken.png];
7460
- for (let frame = 1; frame < frames; frame += 1) {
7461
- await taken.page.waitForTimeout(every);
7462
- pictures.push(await taken.page.screenshot({ fullPage: true }));
7463
- }
7464
- const diffs = [];
7465
- for (let frame = 1; frame < pictures.length; frame += 1) diffs.push(await comparePictures(taken.page, dataUrl(pictures[frame - 1]), dataUrl(pictures[frame]), regions));
7466
- const moved = framesMoved(diffs);
7467
- report.frames = {
7468
- count: frames,
7469
- every,
7470
- moving: moved.filter((region) => region.changed > MOVING_PERCENT),
7471
- still: moved.filter((region) => region.changed <= MOVING_PERCENT).map((region) => region.name)
7472
- };
7473
- }
7474
- if (options.json) {
7475
- console.log(JSON.stringify(report));
7476
- return;
7477
- }
7478
- console.log(out);
7479
- if (report.compare) {
7480
- console.log(`${chalk.bold(`${String(report.compare.changed)}% differs`)} from ${report.compare.url}${report.compare.regions.length > 0 ? ` — ${regionsText(report.compare.regions)}` : ""}`);
7481
- console.log(chalk.dim(`${report.compare.sideBySide}\n${report.compare.diff}`));
7482
- }
7483
- if (report.frames) console.log(report.frames.moving.length > 0 ? `moving over ${String(report.frames.count)} frames: ${regionsText(report.frames.moving)}` : `nothing moved over ${String(report.frames.count)} frames, ${String(report.frames.every)} ms apart`);
7484
- } finally {
7485
- await browser.close();
7486
- }
7487
- };
7488
- //#endregion
7489
- //#region src/commands/skills.ts
7490
- /**
7491
- * `plitzi skills update`: the Plitzi skills in `.claude/skills/` brought up to the packages the project has installed.
7492
- *
7493
- * A skill is copied into a project when `create` writes it and then stays as it was, while `@plitzi/sdk-authoring` moves
7494
- * on — and an agent reading the older one is taught a surface the SDK no longer has, or misses what it gained. Only the
7495
- * `plitzi-*` skills are touched, each replaced whole (a reference it no longer has goes with it); any other skill in the
7496
- * folder is the project's.
7497
- */
7498
- var readSkill = async (file) => {
7499
- try {
7500
- return await fs.readFile(file, "utf-8");
7501
- } catch {
7502
- return;
7503
- }
7504
- };
7505
- var skillsUpdate = async () => {
7506
- const project = await projectHere("whose skills to update");
7507
- if (!project) return;
7508
- const folder = path.join(project.root, ".claude/skills");
7509
- const present = [];
7510
- for (const name of SKILL_NAMES) {
7511
- const skill = await readSkill(path.join(folder, name, "SKILL.md"));
7512
- if (skill !== void 0) present.push({
7513
- name,
7514
- was: skillVersion(skill)
7515
- });
7516
- }
7517
- if (present.length === 0) {
7518
- fail(`No Plitzi skill in ${path.relative(process.cwd(), folder) || ".claude/skills"}: \`plitzi create\` writes them into a project it makes.`);
7519
- return;
7520
- }
7521
- const files = skillFiles(present.map(({ name }) => name), project.root);
7522
- for (const { name, was } of present) {
7523
- const own = Object.fromEntries(Object.entries(files).filter(([file]) => file.startsWith(`.claude/skills/${name}/`)));
7524
- if (Object.keys(own).length === 0) {
7525
- console.log(chalk.yellow(`${name}: its package is not installed here, so it was left as it was.`));
7526
- continue;
7527
- }
7528
- await fs.rm(path.join(folder, name), {
7529
- recursive: true,
7530
- force: true
7531
- });
7532
- await writeFiles(project.root, own);
7533
- const now = skillVersion(own[`.claude/skills/${name}/SKILL.md`] ?? "");
7534
- console.log(chalk.green(`${name}: ${was ?? "unversioned"} → ${now ?? "unversioned"}`));
7535
- }
7536
- };
7537
- //#endregion
7538
8061
  //#region src/commands/uploadPlugin.ts
7539
8062
  /** The zips `plitzi pack plugin` leaves: at a plugin package's root, or under `dist/plugins` of any other project. */
7540
8063
  var packedZips = async (root) => {
@@ -7652,68 +8175,1158 @@ var chooseTarget = async (cdns, { cdn: givenCdn, bucket: givenBucket }, spaceNam
7652
8175
  rl.close();
7653
8176
  }
7654
8177
  };
7655
- var uploadPluginCommand = async (zipGiven, options) => {
7656
- const api = await apiFor(options);
7657
- const zipPath = api ? await chooseZip(zipGiven) : void 0;
7658
- if (!api || !zipPath) return;
7659
- let zip;
7660
- try {
7661
- zip = await fs.readFile(zipPath);
7662
- } catch {
7663
- fail(`Could not read ${zipPath}.`);
7664
- return;
7665
- }
8178
+ /** The space's CDNs and their buckets, asked for — or why not, in words. */
8179
+ var listCdns = async (connection, space) => {
8180
+ const listed = await authorizedRequest(connection, `/spaces/${space.id}/cdns`);
8181
+ if (!listed.ok) return listed;
8182
+ const { reply } = listed.value;
8183
+ if (reply.status !== 200) return {
8184
+ ok: false,
8185
+ error: reply.data.error ?? `Could not list ${space.name}'s CDNs (${reply.status}).`
8186
+ };
8187
+ return {
8188
+ ok: true,
8189
+ value: {
8190
+ connection: listed.value.connection,
8191
+ cdns: reply.data.cdns ?? []
8192
+ }
8193
+ };
8194
+ };
8195
+ /**
8196
+ * A packed plugin uploaded to a bucket of the space and installed there — and its source kept beside it, when it was
8197
+ * packed with one.
8198
+ */
8199
+ var uploadZip = async (connection, space, { zip, filename, target, source }) => {
7666
8200
  const manifest = manifestOf(zip);
7667
8201
  if (!manifest) {
7668
- fail(`${path.basename(zipPath)} is not a plugin: it has no plugin-manifest.json at its root. Build it with plitzi pack plugin.`);
7669
- return;
7670
- }
7671
- const connection = await connectionWithSpace(api, "to upload to");
7672
- if (!connection?.space) return;
7673
- const { space } = connection;
7674
- const listed = await authorizedRequest(connection, `/spaces/${space.id}/cdns`);
7675
- if (!listed.ok) {
7676
- fail(listed.error);
7677
- return;
7678
- }
7679
- if (listed.value.reply.status !== 200) {
7680
- fail(listed.value.reply.data.error ?? `Could not list ${space.name}'s CDNs (${listed.value.reply.status}).`);
7681
- return;
8202
+ fail(`${filename} is not a plugin: it has no plugin-manifest.json at its root. Build it with plitzi pack plugin.`);
8203
+ return "failed";
7682
8204
  }
7683
- const target = await chooseTarget(listed.value.reply.data.cdns ?? [], options, space.name);
7684
- if (!target) return;
7685
8205
  const { cdn, bucket } = target;
7686
8206
  const label = `${manifest.root}${manifest.version ? ` ${manifest.version}` : ""}`;
7687
8207
  console.log(`\nUploading ${chalk.bold(label)} to ${chalk.bold(space.name)}, in ${cdn.name} — ${bucket.name}…`);
7688
8208
  const query = new URLSearchParams({
7689
- filename: path.basename(zipPath),
8209
+ filename,
7690
8210
  bucket: bucket.identifier
7691
8211
  });
7692
- const uploaded = await authorizedRequest(listed.value.connection, `/spaces/${space.id}/cdns/${encodeURIComponent(cdn.identifier)}/plugins?${query}`, {
8212
+ const uploaded = await authorizedRequest(connection, `/spaces/${space.id}/cdns/${encodeURIComponent(cdn.identifier)}/plugins?${query}`, {
7693
8213
  method: "POST",
7694
8214
  headers: { "Content-Type": "application/zip" },
7695
8215
  body: new Uint8Array(zip)
7696
8216
  });
7697
8217
  if (!uploaded.ok) {
7698
8218
  fail(uploaded.error);
7699
- return;
8219
+ return "failed";
7700
8220
  }
7701
8221
  const { status, data } = uploaded.value.reply;
7702
8222
  if (status === 413) {
7703
- fail(`${path.basename(zipPath)} is larger than ${api} accepts.`);
7704
- return;
8223
+ fail(`${filename} is larger than ${connection.api} accepts.`);
8224
+ return "failed";
7705
8225
  }
7706
8226
  if (status !== 201) {
7707
8227
  fail(data.error ?? `The upload was refused (${status}).`);
7708
- return;
8228
+ return "failed";
7709
8229
  }
7710
8230
  const installed = data.installed === "updated" ? `now loads ${label}` : `loads ${label} from now on`;
7711
- console.log(chalk.green(`\n${space.name} ${installed}.`));
8231
+ console.log(chalk.green(`${space.name} ${installed}.`));
7712
8232
  if (data.resource?.path) console.log(chalk.dim(` ${data.resource.path}`));
7713
8233
  console.log(chalk.dim(" Any builder open on the space loads it now."));
7714
- const source = await fs.readFile(sourceFileOf(zipPath)).catch(() => void 0);
7715
8234
  if (source) await keepSource(uploaded.value.connection, space.id, source);
7716
- console.log("");
8235
+ return "pushed";
8236
+ };
8237
+ var uploadPluginCommand = async (zipGiven, options) => {
8238
+ const api = await apiFor(options);
8239
+ const zipPath = api ? await chooseZip(zipGiven) : void 0;
8240
+ if (!api || !zipPath) return;
8241
+ let zip;
8242
+ try {
8243
+ zip = await fs.readFile(zipPath);
8244
+ } catch {
8245
+ fail(`Could not read ${zipPath}.`);
8246
+ return;
8247
+ }
8248
+ if (!manifestOf(zip)) {
8249
+ fail(`${path.basename(zipPath)} is not a plugin: it has no plugin-manifest.json at its root. Build it with plitzi pack plugin.`);
8250
+ return;
8251
+ }
8252
+ const connection = await connectionWithSpace(api, "to upload to");
8253
+ if (!connection?.space) return;
8254
+ const { space } = connection;
8255
+ const listed = await listCdns(connection, space);
8256
+ if (!listed.ok) {
8257
+ fail(listed.error);
8258
+ return;
8259
+ }
8260
+ const target = await chooseTarget(listed.value.cdns, options, space.name);
8261
+ if (!target) return;
8262
+ const source = await fs.readFile(sourceFileOf(zipPath)).catch(() => void 0);
8263
+ await uploadZip(listed.value.connection, space, {
8264
+ zip,
8265
+ filename: path.basename(zipPath),
8266
+ target,
8267
+ ...source ? { source } : {}
8268
+ });
8269
+ console.log("");
8270
+ };
8271
+ //#endregion
8272
+ //#region src/commands/push.ts
8273
+ /**
8274
+ * `plitzi push`: the project put back on the space it works with — the way back of `plitzi pull`
8275
+ * (docs/en/projects-from-spaces.md). Whatever changed since the project last had the space, or the parts named:
8276
+ *
8277
+ * - `plugins` — each plugin whose source changed, packed and uploaded to the space's CDN, as `plitzi upload plugin`;
8278
+ * - `functions` — `functions/`, as `plitzi functions push`;
8279
+ * - `runtime` — the runtime module, packed, as `plitzi runtime push`;
8280
+ * - `space` — the pages, styles, actions and connectors, as the space's draft.
8281
+ *
8282
+ * In that order: the pages name the plugins' elements and the actions name the functions' tasks, so what is named goes
8283
+ * up before what names it. A part that fails stops the push there.
8284
+ *
8285
+ * Always to the space the CLI is connected to, which must be the one the project came from; always the draft, never a
8286
+ * published environment. A draft edited in the builder since the project last had it is never replaced unseen: the push
8287
+ * is refused, and `--force` replaces it.
8288
+ */
8289
+ var PUSH_PARTS = [
8290
+ "space",
8291
+ "functions",
8292
+ "runtime",
8293
+ "plugins"
8294
+ ];
8295
+ /** What goes up first: what the rest names. */
8296
+ var ORDER = [
8297
+ "plugins",
8298
+ "functions",
8299
+ "runtime",
8300
+ "space"
8301
+ ];
8302
+ var isPart = (value) => PUSH_PARTS.some((part) => part === value);
8303
+ var exists = async (file) => {
8304
+ try {
8305
+ await fs.access(file);
8306
+ return true;
8307
+ } catch {
8308
+ return false;
8309
+ }
8310
+ };
8311
+ /** Every file under a folder of the project, by its path in the project: none when there is no such folder. */
8312
+ var filesUnder = async (root, folder) => {
8313
+ try {
8314
+ return (await fs.readdir(path.join(root, folder), {
8315
+ recursive: true,
8316
+ withFileTypes: true
8317
+ })).filter((entry) => entry.isFile()).map((entry) => path.relative(root, path.join(entry.parentPath, entry.name)));
8318
+ } catch {
8319
+ return [];
8320
+ }
8321
+ };
8322
+ /**
8323
+ * Whether any of the files changed since the project last had the space: one not recorded, or not as it was recorded —
8324
+ * formatting aside. Everything has, for a project that never had it.
8325
+ */
8326
+ var changedSince = async (root, origin, files, format) => {
8327
+ if (!origin) return true;
8328
+ for (const file of files) {
8329
+ const was = origin.files[file];
8330
+ const here = await digestsOf(root, file, format);
8331
+ if (!was || !here || here.raw !== was && here.formatted !== was) return true;
8332
+ }
8333
+ return false;
8334
+ };
8335
+ /** The project's files a build of the runtime or of a plugin is made of — none when that cannot be told. */
8336
+ var closureOf = async (root, kind, name, entries) => {
8337
+ try {
8338
+ return Object.keys((await packSource({
8339
+ root,
8340
+ kind,
8341
+ name,
8342
+ entries
8343
+ })).snapshot.files);
8344
+ } catch {
8345
+ return;
8346
+ }
8347
+ };
8348
+ /** The files the space part is made of, as `create --from` lays them out: the pages, the actions, the connectors. */
8349
+ var SPACE_FILES = ["src/space.ts", "src/actions.ts"];
8350
+ var SPACE_FOLDERS = [
8351
+ "src/space/",
8352
+ "src/actions/",
8353
+ "src/connectors/"
8354
+ ];
8355
+ var isSpaceFile = (file) => SPACE_FILES.includes(file) || SPACE_FOLDERS.some((folder) => file.startsWith(folder));
8356
+ /** A plugin's type as the project's folder spells it: `src/plugins/SeatPicker` is `seatPicker`. */
8357
+ var typeOfFolder = (folder) => {
8358
+ const name = path.basename(folder);
8359
+ return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
8360
+ };
8361
+ /**
8362
+ * What the project can send, and whether each changed since the project last had the space. The plugins are grouped as
8363
+ * the space keeps them — a plugin can be several elements — and an element folder the space has no plugin of is a
8364
+ * plugin of its own.
8365
+ */
8366
+ var survey = async (root, { origin, source, before, spaceId, format }) => {
8367
+ const found = [];
8368
+ if (source === "local" && await exists(path.join(root, "src/space.ts"))) {
8369
+ const files = [...await Promise.all(SPACE_FILES.map(async (file) => await exists(path.join(root, file)) ? [file] : [])), ...await Promise.all(SPACE_FOLDERS.map((folder) => filesUnder(root, folder)))].flat();
8370
+ const gone = Object.keys(origin?.files ?? {}).some((file) => isSpaceFile(file) && !files.includes(file));
8371
+ found.push({
8372
+ part: "space",
8373
+ label: "space — its pages, styles, actions and connectors",
8374
+ changed: gone || await changedSince(root, origin, files.filter((file) => isSpaceFile(file)), format)
8375
+ });
8376
+ }
8377
+ const functions = await functionsChange(root, spaceId);
8378
+ if (functions !== "none") found.push({
8379
+ part: "functions",
8380
+ label: "functions — functions/",
8381
+ changed: functions === "changed"
8382
+ });
8383
+ const entries = before && projectEntries(before);
8384
+ const candidates = [...entries?.runtime ? [entries.runtime] : [], DEFAULT_RUNTIME_ENTRY];
8385
+ const present = await Promise.all(candidates.map((entry) => exists(path.join(root, entry))));
8386
+ const runtimeEntry = candidates.find((_entry, index) => present[index]);
8387
+ if (runtimeEntry) {
8388
+ const entry = path.join(root, runtimeEntry);
8389
+ const files = await closureOf(root, "runtime", "runtime", [entry]);
8390
+ found.push({
8391
+ part: "runtime",
8392
+ label: `runtime — ${runtimeEntry}`,
8393
+ changed: !files || await changedSince(root, origin, files, format),
8394
+ entry,
8395
+ files: files ?? []
8396
+ });
8397
+ }
8398
+ const folders = await elementFolders({ root });
8399
+ const grouped = (entries?.plugins ?? []).map(({ type, entries: starts }) => ({
8400
+ type,
8401
+ folders: starts.map((start) => path.join(root, path.dirname(start)))
8402
+ })).filter((group) => group.folders.every((folder) => folders.includes(folder)));
8403
+ const alone = folders.filter((folder) => !grouped.some((group) => group.folders.includes(folder))).map((folder) => ({
8404
+ type: typeOfFolder(folder),
8405
+ folders: [folder]
8406
+ }));
8407
+ for (const { type, folders: group } of [...grouped, ...alone]) {
8408
+ const files = await closureOf(root, "plugin", type, group.map((folder) => path.join(folder, "index.ts")));
8409
+ found.push({
8410
+ part: "plugins",
8411
+ label: `plugin ${type} — ${group.map((folder) => path.relative(root, folder)).join(", ")}`,
8412
+ changed: !files || await changedSince(root, origin, files, format),
8413
+ folders: group,
8414
+ files: files ?? []
8415
+ });
8416
+ }
8417
+ return found;
8418
+ };
8419
+ /**
8420
+ * What to send: the parts named; at a terminal, what the person ticks — what changed, ticked to begin with; with nobody
8421
+ * there, what changed. `undefined` when there is nothing to send, said why.
8422
+ */
8423
+ var choose = async (found, asked, spaceName) => {
8424
+ if (asked.length > 0) {
8425
+ const unknown = asked.filter((part) => !isPart(part));
8426
+ if (unknown.length > 0) {
8427
+ fail(`plitzi push sends ${PUSH_PARTS.join(", ")} — not ${unknown.join(", ")}.`);
8428
+ return;
8429
+ }
8430
+ const missing = asked.filter((part) => !found.some((item) => item.part === part));
8431
+ if (missing.length > 0) {
8432
+ fail(`This project has no ${missing.join(", ")} to push.`);
8433
+ return;
8434
+ }
8435
+ return found.filter((item) => asked.includes(item.part));
8436
+ }
8437
+ if (found.length === 0) {
8438
+ fail("This project has nothing to push: no src/space.ts, functions/, runtime or plugin.");
8439
+ return;
8440
+ }
8441
+ if (!atTerminal()) return found.filter((item) => item.changed);
8442
+ const chosen = await askChecks(`What goes to ${spaceName}? Ticked: what changed since the project last had it.`, found.map((item) => ({
8443
+ label: item.label,
8444
+ value: item,
8445
+ checked: item.changed,
8446
+ hint: item.changed ? "changed" : "unchanged"
8447
+ })));
8448
+ if (!chosen) console.log("Nothing was pushed.");
8449
+ return chosen;
8450
+ };
8451
+ /** A plugin packed as `plitzi pack plugin` packs it, and uploaded and installed as `plitzi upload plugin` does. */
8452
+ var pushPlugin = async (root, connection, space, { folders, target, version }) => {
8453
+ const base = fileNameOf(folders[0]);
8454
+ const zip = path.join(root, "dist/plugins", `${base}-${version}.zip`);
8455
+ let packed;
8456
+ try {
8457
+ packed = await packPlugin({
8458
+ root,
8459
+ source: {
8460
+ kind: "elements",
8461
+ folders
8462
+ },
8463
+ base,
8464
+ version,
8465
+ outDir: path.join(root, "dist/plugins", base),
8466
+ zip
8467
+ });
8468
+ } catch (error) {
8469
+ if (!(error instanceof PackError)) throw error;
8470
+ fail(error.message);
8471
+ return "failed";
8472
+ }
8473
+ if ("problem" in packed.source) {
8474
+ console.log(chalk.yellow(` Its source is not kept, so the space can be taken out with ${base} built only:`));
8475
+ console.log(chalk.yellow(` ${packed.source.problem}`));
8476
+ }
8477
+ return uploadZip(connection, space, {
8478
+ zip: await fs.readFile(zip),
8479
+ filename: path.basename(zip),
8480
+ target,
8481
+ ..."file" in packed.source ? { source: await fs.readFile(packed.source.file) } : {}
8482
+ });
8483
+ };
8484
+ /**
8485
+ * `.plitzi/space.json` as the space now holds the project, so `plitzi pull` compares with it from here on: the files of
8486
+ * the parts sent recorded as the space gives them back, the rest as they were — a page changed in the builder and not
8487
+ * pushed over is still the builder's change to the next pull. A project that never had the space records it whole.
8488
+ */
8489
+ var recordPush = async (root, connection, space, { origin, source, found, sent, draft }) => {
8490
+ const exported = await requestExport(connection, String(space.id), {
8491
+ source,
8492
+ version: { environment: "main" },
8493
+ name: space.name
8494
+ });
8495
+ if (!exported.ok) {
8496
+ console.log(chalk.yellow(`Pushed, but .plitzi/space.json is not brought up to date — ${exported.error}\nThe next plitzi pull compares with what it recorded before.`));
8497
+ return;
8498
+ }
8499
+ const format = await projectFormatter(root);
8500
+ const next = projectFromSpace(exported.value, source);
8501
+ const given = await givenFiles(next, format);
8502
+ const parts = new Set(sent.map((item) => item.part));
8503
+ const codeOf = (items) => items.flatMap((item) => item.part === "runtime" || item.part === "plugins" ? item.files : []);
8504
+ const built = new Set(codeOf(sent));
8505
+ const code = /* @__PURE__ */ new Set([...codeOf(found), ...projectEntries(exported.value).files]);
8506
+ const downloads = new Set(Object.keys(origin?.downloads ?? {}));
8507
+ const isSent = (file) => {
8508
+ if (!origin || built.has(file)) return true;
8509
+ if (file.startsWith("functions/")) return parts.has("functions");
8510
+ return !code.has(file) && !file.startsWith("vendor/") && parts.has("space");
8511
+ };
8512
+ const files = Object.fromEntries(Object.entries(origin?.files ?? {}).filter(([file]) => downloads.has(file) || !isSent(file) || given.has(file)));
8513
+ given.forEach((bytes, file) => {
8514
+ if (isSent(file)) files[file] = digest(bytes);
8515
+ });
8516
+ const recorded = draft ?? origin?.draft;
8517
+ await writeOrigin(root, {
8518
+ format: 1,
8519
+ api: connection.api,
8520
+ space: exported.value.space,
8521
+ source,
8522
+ version: { environment: "main" },
8523
+ ...recorded ? { draft: recorded } : {},
8524
+ files,
8525
+ downloads: origin?.downloads ?? {},
8526
+ dependencies: !origin || parts.has("runtime") || parts.has("plugins") ? next.dependencies : origin.dependencies
8527
+ });
8528
+ };
8529
+ var push = async (asked, options) => {
8530
+ const project = await findProject(process.cwd());
8531
+ if (!project) {
8532
+ fail("There is no package.json here or above: run plitzi push in the project to send.");
8533
+ return;
8534
+ }
8535
+ if (project.plitzi?.kind === "plugin") {
8536
+ fail("This is a plugin package: plitzi pack plugin and plitzi upload plugin put it on a space.");
8537
+ return;
8538
+ }
8539
+ const { root } = project;
8540
+ const origin = await readOrigin(root);
8541
+ if (origin && origin.version.environment !== "main") {
8542
+ fail(`This project follows ${versionLabel(origin.version)} of ${origin.space.name}, and a push writes the draft.\nFollow the draft first: plitzi pull --environment main.`);
8543
+ return;
8544
+ }
8545
+ const connection = await connectToSpace({
8546
+ ...options,
8547
+ api: options.api ?? origin?.api
8548
+ }, "to push to");
8549
+ const space = connection?.space;
8550
+ if (!connection || !space) return;
8551
+ if (origin && origin.space.id !== space.id) {
8552
+ fail(`This project is ${origin.space.name}’s, and the CLI works in ${space.name}. Switch to it with plitzi space, and push again.`);
8553
+ return;
8554
+ }
8555
+ const source = origin?.source ?? (project.plitzi?.kind === "project" ? project.plitzi.source : "local");
8556
+ let before;
8557
+ if (origin) {
8558
+ const exported = await requestExport(connection, String(space.id), {
8559
+ source: "cloud",
8560
+ version: { environment: "main" },
8561
+ name: space.name
8562
+ });
8563
+ if (!exported.ok) {
8564
+ fail(exported.error);
8565
+ return;
8566
+ }
8567
+ before = exported.value;
8568
+ }
8569
+ const format = await projectFormatter(root);
8570
+ const found = await survey(root, {
8571
+ origin,
8572
+ source,
8573
+ before,
8574
+ spaceId: space.id,
8575
+ format
8576
+ });
8577
+ const chosen = await choose(found, asked, space.name);
8578
+ if (!chosen) return;
8579
+ if (chosen.length === 0) {
8580
+ console.log(`Nothing to push: the project is what ${space.name} last gave it.`);
8581
+ return;
8582
+ }
8583
+ const force = options.force ?? false;
8584
+ const sent = [];
8585
+ const settled = [];
8586
+ const unchanged = [];
8587
+ let draft;
8588
+ let target;
8589
+ const version = (await readPackageJson(root))?.version ?? "0.0.0";
8590
+ for (const item of [...chosen].sort((a, b) => ORDER.indexOf(a.part) - ORDER.indexOf(b.part))) {
8591
+ let outcome;
8592
+ if (item.part === "plugins") {
8593
+ if (!target) {
8594
+ const listed = await listCdns(connection, space);
8595
+ if (!listed.ok) {
8596
+ fail(listed.error);
8597
+ return;
8598
+ }
8599
+ target = await chooseTarget(listed.value.cdns, options, space.name);
8600
+ if (!target) return;
8601
+ }
8602
+ outcome = await pushPlugin(root, connection, space, {
8603
+ folders: item.folders,
8604
+ target,
8605
+ version
8606
+ });
8607
+ } else if (item.part === "functions") outcome = await pushFunctionsOf(root, connection, space, { force });
8608
+ else if (item.part === "runtime") outcome = await pushRuntimeOf(root, connection, space, item.entry);
8609
+ else {
8610
+ const pushed = await pushSpaceOf(root, connection, space, {
8611
+ base: origin?.draft ?? null,
8612
+ force
8613
+ });
8614
+ outcome = pushed.outcome;
8615
+ draft = pushed.draft;
8616
+ }
8617
+ if (outcome === "failed") {
8618
+ if (sent.length > 0) {
8619
+ console.log(chalk.yellow(`\nPushed before it stopped: ${sent.map((done) => done.label).join("; ")}.`));
8620
+ await recordPush(root, connection, space, {
8621
+ origin,
8622
+ source,
8623
+ found,
8624
+ sent: settled,
8625
+ draft
8626
+ });
8627
+ }
8628
+ return;
8629
+ }
8630
+ if (outcome === "pushed") sent.push(item);
8631
+ else unchanged.push(item.label);
8632
+ if (outcome === "pushed" || item.part === "space") settled.push(item);
8633
+ }
8634
+ if (settled.length > 0) await recordPush(root, connection, space, {
8635
+ origin,
8636
+ source,
8637
+ found,
8638
+ sent: settled,
8639
+ draft
8640
+ });
8641
+ if (unchanged.length > 0) console.log(chalk.dim(`Already ${space.name}’s: ${unchanged.join("; ")}.`));
8642
+ if (sent.length > 0) console.log(chalk.green(`\nPushed to ${space.name}’s draft.`) + chalk.dim(" Publish it in the builder to take it live."));
8643
+ };
8644
+ //#endregion
8645
+ //#region ../../packages/sdk-shared/dist/schema/motion.mjs
8646
+ var t = [
8647
+ "fade",
8648
+ "fade-up",
8649
+ "fade-down",
8650
+ "slide-left",
8651
+ "slide-right",
8652
+ "slide-up",
8653
+ "scale",
8654
+ "zoom-in",
8655
+ "zoom-out",
8656
+ "pop",
8657
+ "bounce-in",
8658
+ "tilt",
8659
+ "spin-in"
8660
+ ];
8661
+ var o = {
8662
+ fade: [{
8663
+ at: 0,
8664
+ opacity: 0
8665
+ }],
8666
+ "fade-up": [{
8667
+ at: 0,
8668
+ opacity: 0,
8669
+ translate: "0 24px"
8670
+ }],
8671
+ "fade-down": [{
8672
+ at: 0,
8673
+ opacity: 0,
8674
+ translate: "0 -24px"
8675
+ }],
8676
+ "slide-left": [{
8677
+ at: 0,
8678
+ opacity: 0,
8679
+ translate: "32px 0"
8680
+ }],
8681
+ "slide-right": [{
8682
+ at: 0,
8683
+ opacity: 0,
8684
+ translate: "-32px 0"
8685
+ }],
8686
+ "slide-up": [{
8687
+ at: 0,
8688
+ opacity: 0,
8689
+ translate: "0 72px"
8690
+ }],
8691
+ scale: [{
8692
+ at: 0,
8693
+ opacity: 0,
8694
+ scale: "0.94"
8695
+ }],
8696
+ "zoom-in": [{
8697
+ at: 0,
8698
+ opacity: 0,
8699
+ scale: "0.6"
8700
+ }],
8701
+ "zoom-out": [{
8702
+ at: 0,
8703
+ opacity: 0,
8704
+ scale: "1.25"
8705
+ }],
8706
+ pop: [
8707
+ {
8708
+ at: 0,
8709
+ opacity: 0,
8710
+ scale: "0.5"
8711
+ },
8712
+ {
8713
+ at: 55,
8714
+ opacity: 1,
8715
+ scale: "1.08"
8716
+ },
8717
+ {
8718
+ at: 78,
8719
+ scale: "0.97"
8720
+ }
8721
+ ],
8722
+ "bounce-in": [
8723
+ {
8724
+ at: 0,
8725
+ opacity: 0,
8726
+ translate: "0 -64px"
8727
+ },
8728
+ {
8729
+ at: 55,
8730
+ opacity: 1,
8731
+ translate: "0 10px"
8732
+ },
8733
+ {
8734
+ at: 75,
8735
+ translate: "0 -5px"
8736
+ },
8737
+ {
8738
+ at: 90,
8739
+ translate: "0 2px"
8740
+ }
8741
+ ],
8742
+ tilt: [{
8743
+ at: 0,
8744
+ opacity: 0,
8745
+ translate: "0 32px",
8746
+ rotate: "-6deg"
8747
+ }],
8748
+ "spin-in": [{
8749
+ at: 0,
8750
+ opacity: 0,
8751
+ scale: "0.4",
8752
+ rotate: "-180deg"
8753
+ }]
8754
+ };
8755
+ Object.fromEntries(t.map((e) => {
8756
+ let [{ at: t, ...n }] = o[e];
8757
+ return [e, n];
8758
+ }));
8759
+ var c = (e, t) => {
8760
+ let n = t * 2 * Math.PI, r = Math.round(Math.sin(n) * e * 10) / 10, i = Math.round(-Math.cos(n) * e * 10) / 10;
8761
+ return `translate(${String(r)}px, ${String(i)}px)`;
8762
+ };
8763
+ Array.from({ length: 9 }, (e, t) => ({
8764
+ at: t * 100 / 8,
8765
+ transform: c(6, t / 8)
8766
+ }));
8767
+ var _ = `[data-motion-enter], [data-motion-stagger] > *, [data-motion-loop] { animation: none !important; }`;
8768
+ [
8769
+ "[data-motion-loop] { animation-play-state: running, running !important; }",
8770
+ "[data-motion-on=\"view\"][data-motion-enter], [data-motion-on=\"view\"][data-motion-stagger] > * { animation-play-state: running, running !important; }",
8771
+ "[data-motion-on=\"scroll\"][data-motion-enter], [data-motion-on=\"scroll\"][data-motion-stagger] > * { animation-timeline: auto, auto !important; }"
8772
+ ].join("\n");
8773
+ //#endregion
8774
+ //#region src/commands/shot.ts
8775
+ /** A region moves when more than this share of it changed between two frames: below is anti-aliasing and carets. */
8776
+ var MOVING_PERCENT = .5;
8777
+ /** How long a whole page waits for its pictures before it is taken anyway. */
8778
+ var IMAGES_BUDGET_MS = 8e3;
8779
+ /** How many texts that differ are written out; `--json` has every one. */
8780
+ var TEXTS_SHOWN = 12;
8781
+ /** A shift within this is where it was: a pixel or two is rounding. */
8782
+ var DRIFT_PX = 2;
8783
+ /** An arrival that waits for the scroll and has not had it: one waiting to be seen, or one tied to the scroll. */
8784
+ var WAITING_FOR_SCROLL = "[data-motion-on=\"view\"]:not([data-motion-seen]), [data-motion-on=\"scroll\"]";
8785
+ /** A selector, or an element's name — what `data-plitzi-el` carries. */
8786
+ var selectorOf = (target) => /^[#.[]/.test(target) ? target : `[data-plitzi-el="${target}"]`;
8787
+ var framing = (view) => {
8788
+ if (view.clip) return {
8789
+ kind: "element",
8790
+ selector: selectorOf(view.clip)
8791
+ };
8792
+ return view.viewport || view.scrollTo ? { kind: "viewport" } : { kind: "page" };
8793
+ };
8794
+ var capture = (page, how) => {
8795
+ if (how.kind === "element") return page.locator(how.selector).first().screenshot();
8796
+ return page.screenshot({ fullPage: how.kind === "page" });
8797
+ };
8798
+ /**
8799
+ * One page, pictured: the project's own (`origin` given — in the space's theme when one is asked for) or another site,
8800
+ * in the scheme it is told.
8801
+ */
8802
+ var pictureOf = async (browser, url, view, origin) => {
8803
+ const page = origin ? await openProjectPage(browser, origin, view) : await browser.newPage({
8804
+ viewport: {
8805
+ width: view.width,
8806
+ height: view.height
8807
+ },
8808
+ colorScheme: view.scheme ?? "light",
8809
+ reducedMotion: view.reducedMotion ? "reduce" : "no-preference"
8810
+ });
8811
+ if (!await page.goto(url, { waitUntil: "networkidle" }).catch(() => null)) return { problem: `Nothing answers at ${url}.` };
8812
+ if (view.waitFor) {
8813
+ if (!await page.waitForSelector(selectorOf(view.waitFor), { timeout: 1e4 }).catch(() => null)) return { problem: `${view.waitFor} did not appear on ${url} within 10 s.` };
8814
+ }
8815
+ for (const target of [view.scrollTo, view.clip]) if (target && !await page.waitForSelector(selectorOf(target), { timeout: 1e4 }).catch(() => null)) return { problem: `${target} is not on ${url}.` };
8816
+ if (view.scrollTo) await page.locator(selectorOf(view.scrollTo)).first().scrollIntoViewIfNeeded();
8817
+ await withoutDevTools(page);
8818
+ const settled = view.still ? await page.evaluate((selector) => document.querySelectorAll(selector).length, WAITING_FOR_SCROLL) : 0;
8819
+ const whole = framing(view).kind === "page";
8820
+ const pictures = whole ? await page.evaluate(loadImages, IMAGES_BUDGET_MS) : void 0;
8821
+ if (whole) await page.evaluate(unrollPage, void 0);
8822
+ if (view.still) await page.addStyleTag({ content: _ });
8823
+ return {
8824
+ page,
8825
+ png: await capture(page, framing(view)),
8826
+ scheme: await paintedScheme(page, view.scheme ?? "light"),
8827
+ settled,
8828
+ unloaded: pictures?.missing ?? 0
8829
+ };
8830
+ };
8831
+ var shiftText = (shift) => shift !== void 0 && Math.abs(shift) > DRIFT_PX ? ` (${shift > 0 ? "+" : ""}${String(shift)}px there)` : "";
8832
+ var regionsText = (regions) => regions.map((region) => `${region.name} ${String(region.changed)}%${shiftText(region.shift)}`).join(", ");
8833
+ /** Compared frame to frame: the most each region changed between two that follow each other. */
8834
+ var framesMoved = (diffs) => {
8835
+ const most = /* @__PURE__ */ new Map();
8836
+ for (const diff of diffs) for (const region of diff.regions) most.set(region.name, Math.max(most.get(region.name) ?? 0, region.changed));
8837
+ return [...most].map(([name, changed]) => ({
8838
+ name,
8839
+ changed
8840
+ }));
8841
+ };
8842
+ var differenceText = (entry) => ` ${entry.tag} ${JSON.stringify(entry.text.length > 48 ? `${entry.text.slice(0, 47)}…` : entry.text)} — ${entry.differences.join(" · ")}`;
8843
+ var someOf = (texts) => {
8844
+ return `${texts.slice(0, 3).map((text) => JSON.stringify(text.length > 32 ? `${text.slice(0, 31)}…` : text)).join(", ")}${texts.length > 3 ? ` (+${String(texts.length - 3)})` : ""}`;
8845
+ };
8846
+ /** What a comparison found, in the lines an agent reads before it opens a picture. */
8847
+ var compareText = (compare) => {
8848
+ const lines = [`${chalk.bold(`${String(compare.changed)}% differs`)} from ${compare.url}${compare.regions.length > 0 ? ` — ${regionsText(compare.regions)}` : ""}`];
8849
+ const longer = compare.heights.there - compare.heights.here;
8850
+ const drift = compare.driftFrom;
8851
+ const placed = [Math.abs(longer) > DRIFT_PX ? `the page there is ${String(Math.abs(longer))}px ${longer > 0 ? "longer" : "shorter"} (${String(compare.heights.here)} → ${String(compare.heights.there)})` : "", drift ? `from ${drift.name} on, it sits ${String(Math.abs(drift.shift))}px ${drift.shift > 0 ? "lower" : "higher"} there — each section is compared where it is` : ""].filter(Boolean);
8852
+ if (placed.length > 0) lines.push(placed.join("; "));
8853
+ const { texts } = compare;
8854
+ if (texts.differ.length > 0) lines.push(`texts: ${String(texts.same)} the same, ${String(texts.differ.length)} differ (here → there):`, ...texts.differ.slice(0, TEXTS_SHOWN).map(differenceText), ...texts.differ.length > TEXTS_SHOWN ? [chalk.dim(` …and ${String(texts.differ.length - TEXTS_SHOWN)} more: --json lists them`)] : []);
8855
+ else if (texts.same > 0) lines.push(`texts: the ${String(texts.same)} both pages have look the same`);
8856
+ if (texts.onlyHere.length > 0 || texts.onlyThere.length > 0) lines.push(chalk.dim([texts.onlyHere.length > 0 ? `only here: ${someOf(texts.onlyHere)}` : "", texts.onlyThere.length > 0 ? `only there: ${someOf(texts.onlyThere)}` : ""].filter(Boolean).join(" · ")));
8857
+ if (compare.unloaded > 0) lines.push(chalk.yellow(`${String(compare.unloaded)} picture(s) there had not loaded when it was taken: those parts compare against a hole`));
8858
+ lines.push(chalk.dim(`${compare.sideBySide}\n${compare.diff}`));
8859
+ return lines;
8860
+ };
8861
+ /** The other site beside this page: section by section where each one is, and the words both have, measured. */
8862
+ var compareWith = async (browser, taken, target, view, regions, base) => {
8863
+ const theirs = await pictureOf(browser, target, {
8864
+ ...view,
8865
+ scheme: taken.scheme
8866
+ });
8867
+ if ("problem" in theirs) return theirs;
8868
+ const diff = await comparePictures(taken.page, dataUrl(taken.png), dataUrl(theirs.png), {
8869
+ regions,
8870
+ align: true
8871
+ });
8872
+ await writeFile(`${base}-compare.png`, fromDataUrl(diff.sideBySide));
8873
+ await writeFile(`${base}-diff.png`, fromDataUrl(diff.diff));
8874
+ const rowShift = diff.rowShift ?? [];
8875
+ const shiftAt = (y) => rowShift[Math.min(Math.max(0, Math.round(y)), rowShift.length - 1)] ?? 0;
8876
+ const texts = compareTexts(await taken.page.evaluate(pageTexts, PAGE_TEXT_LIMIT), await theirs.page.evaluate(pageTexts, PAGE_TEXT_LIMIT), shiftAt);
8877
+ return {
8878
+ url: target,
8879
+ changed: diff.changed,
8880
+ heights: {
8881
+ here: diff.heights.a,
8882
+ there: diff.heights.b
8883
+ },
8884
+ ...diff.driftFrom ? { driftFrom: diff.driftFrom } : {},
8885
+ regions: diff.regions,
8886
+ texts,
8887
+ unloaded: theirs.unloaded,
8888
+ sideBySide: `${base}-compare.png`,
8889
+ diff: `${base}-diff.png`
8890
+ };
8891
+ };
8892
+ var shot = async (route, options) => {
8893
+ const project = await projectHere("whose page to picture");
8894
+ if (!project) return;
8895
+ const where = await projectOrigin(project.root, project.plitzi?.kind === "project" ? project.plitzi : void 0);
8896
+ if ("problem" in where) {
8897
+ fail(where.problem);
8898
+ return;
8899
+ }
8900
+ const browser = await launchBrowser(project.root);
8901
+ if ("problem" in browser) {
8902
+ fail(browser.problem);
8903
+ return;
8904
+ }
8905
+ if (options.compare && (options.clip || options.scrollTo || options.viewport)) {
8906
+ await browser.close();
8907
+ fail("`--compare` sets the whole page beside the other site, section by section: leave out --clip, --scroll-to and --viewport.");
8908
+ return;
8909
+ }
8910
+ try {
8911
+ const pathname = route ?? "/";
8912
+ const view = {
8913
+ width: options.width ?? 1280,
8914
+ height: options.height ?? 800,
8915
+ ...options.scheme ? { scheme: options.scheme } : {},
8916
+ reducedMotion: options.reducedMotion === true,
8917
+ ...options.waitFor ? { waitFor: options.waitFor } : {},
8918
+ ...options.clip ? { clip: options.clip } : {},
8919
+ ...options.scrollTo ? { scrollTo: options.scrollTo } : {},
8920
+ ...options.viewport ? { viewport: true } : {},
8921
+ still: !options.clip && !options.scrollTo && !options.viewport && (options.frames ?? 0) <= 1
8922
+ };
8923
+ const how = framing(view);
8924
+ const taken = await pictureOf(browser, `${where.origin}${pathname}`, view, where.origin);
8925
+ if ("problem" in taken) {
8926
+ fail(`${taken.problem} Start the project first: npm start`);
8927
+ return;
8928
+ }
8929
+ const name = pathname === "/" ? "home" : pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "");
8930
+ const out = path.resolve(project.root, options.out ?? `tmp/shots/${name}-${String(view.width)}-${taken.scheme}.png`);
8931
+ const base = out.replace(/\.png$/, "");
8932
+ await mkdir(path.dirname(out), { recursive: true });
8933
+ await writeFile(out, taken.png);
8934
+ const regions = how.kind === "page" ? await taken.page.evaluate(pageRegions, void 0) : [];
8935
+ const report = {
8936
+ path: pathname,
8937
+ width: view.width,
8938
+ scheme: taken.scheme,
8939
+ out,
8940
+ ...taken.settled > 0 ? { settled: taken.settled } : {},
8941
+ ...taken.unloaded > 0 ? { unloaded: taken.unloaded } : {}
8942
+ };
8943
+ if (options.compare) {
8944
+ const other = new URL(options.compare);
8945
+ const compared = await compareWith(browser, taken, other.pathname === "/" ? new URL(pathname, other).href : other.href, view, regions, base);
8946
+ if ("problem" in compared) {
8947
+ fail(compared.problem);
8948
+ return;
8949
+ }
8950
+ report.compare = compared;
8951
+ }
8952
+ const frames = options.frames ?? 0;
8953
+ if (frames > 1) {
8954
+ const every = options.every ?? 500;
8955
+ const pictures = [taken.png];
8956
+ for (let frame = 1; frame < frames; frame += 1) {
8957
+ await taken.page.waitForTimeout(every);
8958
+ pictures.push(await capture(taken.page, how));
8959
+ }
8960
+ const diffs = [];
8961
+ for (let frame = 1; frame < pictures.length; frame += 1) diffs.push(await comparePictures(taken.page, dataUrl(pictures[frame - 1]), dataUrl(pictures[frame]), { regions }));
8962
+ const whole = how.kind === "element" ? options.clip ?? "element" : "viewport";
8963
+ const moved = how.kind === "page" ? framesMoved(diffs) : [{
8964
+ name: whole,
8965
+ changed: Math.max(0, ...diffs.map((diff) => diff.changed))
8966
+ }];
8967
+ report.frames = {
8968
+ count: frames,
8969
+ every,
8970
+ moving: moved.filter((region) => region.changed > MOVING_PERCENT),
8971
+ still: moved.filter((region) => region.changed <= MOVING_PERCENT).map((region) => region.name)
8972
+ };
8973
+ }
8974
+ if (options.json) {
8975
+ console.log(JSON.stringify(report));
8976
+ return;
8977
+ }
8978
+ console.log(out);
8979
+ if (report.settled) console.log(chalk.dim(`${String(report.settled)} arrival${report.settled === 1 ? "" : "s"} waiting for the scroll shown as they end — --scroll-to <id> --viewport --frames 4 shows one arriving`));
8980
+ if (report.unloaded) console.log(chalk.yellow(`${String(report.unloaded)} picture(s) had not loaded when the page was taken`));
8981
+ if (report.compare) console.log(compareText(report.compare).join("\n"));
8982
+ if (report.frames) console.log(report.frames.moving.length > 0 ? `moving over ${String(report.frames.count)} frames: ${regionsText(report.frames.moving)}` : `nothing moved over ${String(report.frames.count)} frames, ${String(report.frames.every)} ms apart`);
8983
+ } finally {
8984
+ await browser.close();
8985
+ }
8986
+ };
8987
+ //#endregion
8988
+ //#region src/commands/upgrade.ts
8989
+ /**
8990
+ * `plitzi upgrade`: a project brought up to the CLI it now has — what `create` would write today, for what is the
8991
+ * CLI's to write. Every part on its own, or all of them:
8992
+ *
8993
+ * - `files`: the machinery (`machineryFiles`) — `author.ts`, `main.ts`, the Playwright and lint configs, AGENTS.md. One
8994
+ * nobody changed since the CLI wrote it is replaced; one the project made its own is shown as a diff and left, unless
8995
+ * `--take` names it.
8996
+ * - `packages`: `package.json` merged — the scripts and dependencies it lacks added, `@plitzi/*` raised to this
8997
+ * version. A script or a dependency the project has is never changed; then the install.
8998
+ * - `skills`: `.claude/skills/plitzi-*` from the packages installed, each replaced whole.
8999
+ * - `renames`: a name a version renamed, still written in the source (`canTemplate`), at its file and line — and with
9000
+ * `--write`, renamed.
9001
+ *
9002
+ * Shown by default; `--write` makes it. What the space is — its pages, plugins, data — is the project's, never named.
9003
+ *
9004
+ * plitzi upgrade # everything, as it would be
9005
+ * plitzi upgrade skills --write # only the skills
9006
+ * plitzi upgrade --write --take src/author.ts
9007
+ */
9008
+ var UPGRADE_PARTS = [
9009
+ "files",
9010
+ "packages",
9011
+ "skills",
9012
+ "renames"
9013
+ ];
9014
+ var RENAMES = [{
9015
+ name: "canTemplate",
9016
+ to: "canSnippet",
9017
+ since: "0.38.0",
9018
+ what: "a plugin’s builder config key"
9019
+ }, ...[
9020
+ "authorTemplate",
9021
+ "validateTemplate",
9022
+ "TemplateSpec",
9023
+ "AuthoredTemplate"
9024
+ ].map((name) => ({
9025
+ name,
9026
+ to: name.replace("Template", "Snippet"),
9027
+ since: "0.38.0",
9028
+ what: "what the builder saves from a subtree is a snippet",
9029
+ from: "@plitzi/sdk-authoring"
9030
+ }))];
9031
+ /** The project's own source: TypeScript and JavaScript under `src/` and `visual/`, never what was installed or built. */
9032
+ var sourceFiles = async (root) => {
9033
+ const found = [];
9034
+ const walk = async (dir) => {
9035
+ let entries;
9036
+ try {
9037
+ entries = await fs.readdir(path.join(root, dir), { withFileTypes: true });
9038
+ } catch {
9039
+ return;
9040
+ }
9041
+ for (const entry of entries) {
9042
+ const relative = path.join(dir, entry.name);
9043
+ if (entry.isDirectory()) await walk(relative);
9044
+ else if (/\.(ts|tsx|mts|js|jsx|mjs)$/.test(entry.name)) found.push(relative);
9045
+ }
9046
+ };
9047
+ await walk("src");
9048
+ await walk("visual");
9049
+ return found;
9050
+ };
9051
+ var wordOf = (name) => new RegExp(`\\b${name}\\b`, "g");
9052
+ var importsFrom = (text, name, from) => new RegExp(`import\\s+(type\\s+)?\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*['"]${from}['"]`).test(text);
9053
+ var renamesIn = (file, text) => RENAMES.filter((rename) => !rename.from || importsFrom(text, rename.name, rename.from)).flatMap((rename) => text.split("\n").flatMap((line, index) => wordOf(rename.name).test(line) ? [{
9054
+ file,
9055
+ line: index + 1,
9056
+ name: rename.name,
9057
+ to: rename.to,
9058
+ since: rename.since,
9059
+ what: rename.what
9060
+ }] : []));
9061
+ /** The lowest version a range allows, as numbers: `^0.38.2` → [0, 38, 2]. */
9062
+ var floorOf = (range) => {
9063
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(range);
9064
+ return match ? match.slice(1).map(Number) : void 0;
9065
+ };
9066
+ var isBelow = (range, than) => {
9067
+ const [a, b] = [floorOf(range), floorOf(than)];
9068
+ if (!a || !b) return false;
9069
+ const at = a.findIndex((part, index) => part !== b[index]);
9070
+ return at !== -1 && a[at] < b[at];
9071
+ };
9072
+ var stringsOf = (value) => e$1(value) ? Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string")) : {};
9073
+ var planPackages = (text, answers) => {
9074
+ const parsed = JSON.parse(text);
9075
+ const ours = JSON.parse(packageJson$1(answers));
9076
+ const project = e$1(parsed) ? { ...parsed } : {};
9077
+ const scaffold = e$1(ours) ? ours : {};
9078
+ const plan = {
9079
+ added: [],
9080
+ raised: [],
9081
+ scripts: [],
9082
+ ownScripts: []
9083
+ };
9084
+ const scripts = stringsOf(project.scripts);
9085
+ for (const [name, command] of Object.entries(stringsOf(scaffold.scripts))) if (!(name in scripts)) {
9086
+ plan.scripts.push({
9087
+ name,
9088
+ command
9089
+ });
9090
+ scripts[name] = command;
9091
+ } else if (scripts[name] !== command) plan.ownScripts.push({
9092
+ name,
9093
+ yours: scripts[name],
9094
+ ours: command
9095
+ });
9096
+ const sections = {
9097
+ dependencies: stringsOf(project.dependencies),
9098
+ devDependencies: stringsOf(project.devDependencies)
9099
+ };
9100
+ for (const section of ["dependencies", "devDependencies"]) for (const [name, range] of Object.entries(stringsOf(scaffold[section]))) {
9101
+ const where = name in sections.dependencies ? "dependencies" : name in sections.devDependencies ? "devDependencies" : void 0;
9102
+ if (!where) {
9103
+ plan.added.push({
9104
+ section,
9105
+ name,
9106
+ range
9107
+ });
9108
+ sections[section][name] = range;
9109
+ } else if (name.startsWith("@plitzi/") && isBelow(sections[where][name], range)) {
9110
+ plan.raised.push({
9111
+ section: where,
9112
+ name,
9113
+ from: sections[where][name],
9114
+ to: range
9115
+ });
9116
+ sections[where][name] = range;
9117
+ }
9118
+ }
9119
+ if (plan.added.length + plan.raised.length + plan.scripts.length === 0) return plan;
9120
+ const sorted = (entries) => Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)));
9121
+ const indent = /^\{\n([ \t]+)"/.exec(text)?.[1] ?? " ";
9122
+ const merged = {
9123
+ ...project,
9124
+ scripts,
9125
+ dependencies: sorted(sections.dependencies),
9126
+ devDependencies: sorted(sections.devDependencies)
9127
+ };
9128
+ return {
9129
+ ...plan,
9130
+ after: `${JSON.stringify(merged, null, indent)}\n`
9131
+ };
9132
+ };
9133
+ var readText = async (file) => {
9134
+ try {
9135
+ return await fs.readFile(file, "utf-8");
9136
+ } catch {
9137
+ return;
9138
+ }
9139
+ };
9140
+ var planFiles = async (root, files, recorded, take) => Promise.all(Object.entries(files).map(async ([file, ours]) => {
9141
+ const yours = await readText(path.join(root, file));
9142
+ if (yours === void 0) return {
9143
+ file,
9144
+ status: "added",
9145
+ ours
9146
+ };
9147
+ if (yours === ours) return {
9148
+ file,
9149
+ status: "current",
9150
+ ours,
9151
+ yours
9152
+ };
9153
+ if (recorded[file] === digestOf(yours)) return {
9154
+ file,
9155
+ status: "updated",
9156
+ ours,
9157
+ yours
9158
+ };
9159
+ return {
9160
+ file,
9161
+ status: take.includes("all") || take.includes(file) ? "taken" : "yours",
9162
+ ours,
9163
+ yours
9164
+ };
9165
+ }));
9166
+ var WRITES = /* @__PURE__ */ new Set([
9167
+ "added",
9168
+ "updated",
9169
+ "taken"
9170
+ ]);
9171
+ var planSkills = async (root) => {
9172
+ const files = skillFiles(SKILL_NAMES, root);
9173
+ const plans = [];
9174
+ for (const name of SKILL_NAMES) {
9175
+ const skill = `.claude/skills/${name}/SKILL.md`;
9176
+ if (!(skill in files)) continue;
9177
+ const now = files[skill];
9178
+ const was = await readText(path.join(root, skill));
9179
+ if (was !== now) plans.push({
9180
+ name,
9181
+ ...was === void 0 ? {} : { was: skillVersion(was) ?? "unversioned" },
9182
+ now: skillVersion(now) ?? "unversioned"
9183
+ });
9184
+ }
9185
+ return {
9186
+ plans,
9187
+ files
9188
+ };
9189
+ };
9190
+ var writeSkills = async (root, plans, files) => {
9191
+ for (const { name } of plans) {
9192
+ await fs.rm(path.join(root, ".claude/skills", name), {
9193
+ recursive: true,
9194
+ force: true
9195
+ });
9196
+ await writeFiles(root, Object.fromEntries(Object.entries(files).filter(([file]) => file.startsWith(`.claude/skills/${name}/`))));
9197
+ }
9198
+ };
9199
+ /** What a project was made with, as far as its machinery cares: none of it reads the key or the environment. */
9200
+ var answersFor = async (root, plitzi, manager) => {
9201
+ const name = (await readPackageJson(root))?.name ?? path.basename(root);
9202
+ const managerVersion = detectManagerVersion(manager, root);
9203
+ return {
9204
+ name,
9205
+ mode: plitzi.mode,
9206
+ source: plitzi.source,
9207
+ key: "",
9208
+ environment: "main",
9209
+ packageManager: manager,
9210
+ ...managerVersion === void 0 ? {} : { managerVersion }
9211
+ };
9212
+ };
9213
+ var statusLine = {
9214
+ current: (file) => chalk.dim(` = ${file}`),
9215
+ added: (file) => chalk.green(` + ${file}`),
9216
+ updated: (file) => chalk.green(` ~ ${file}`),
9217
+ taken: (file) => chalk.yellow(` ~ ${file} (yours, taken)`),
9218
+ yours: (file) => chalk.yellow(` ! ${file} — yours: the CLI's version below; --take ${file} to replace it`)
9219
+ };
9220
+ var reportText = (report) => {
9221
+ const lines = [chalk.bold(`plitzi upgrade → ${report.version}${report.write ? "" : " (shown; --write makes it)"}`)];
9222
+ if (report.files) {
9223
+ const changed = report.files.filter((file) => file.status !== "current");
9224
+ lines.push(changed.length === 0 ? chalk.green("files: up to date") : "files:");
9225
+ for (const file of changed) {
9226
+ lines.push(statusLine[file.status](file.file));
9227
+ if (file.diff) lines.push(chalk.dim(file.diff));
9228
+ }
9229
+ }
9230
+ if (report.packages) {
9231
+ const { added, raised, scripts, ownScripts, install: installed } = report.packages;
9232
+ const none = added.length + raised.length + scripts.length === 0;
9233
+ lines.push(none ? chalk.green("packages: up to date") : "packages:");
9234
+ lines.push(...raised.map((entry) => chalk.green(` ~ ${entry.name} ${entry.from} → ${entry.to}`)), ...added.map((entry) => chalk.green(` + ${entry.name} ${entry.range} (${entry.section})`)), ...scripts.map((entry) => chalk.green(` + script ${entry.name}: ${entry.command}`)), ...ownScripts.map((entry) => chalk.dim(` = script ${entry.name} is yours ("${entry.yours}"; the CLI writes "${entry.ours}")`)));
9235
+ if (installed === "needed") lines.push(chalk.yellow(" then install, for the new versions to be the ones that run"));
9236
+ else if (installed === "failed") lines.push(chalk.red(" the install failed: the reason is in the output above"));
9237
+ }
9238
+ if (report.skills) {
9239
+ lines.push(report.skills.length === 0 ? chalk.green("skills: up to date") : "skills:");
9240
+ lines.push(...report.skills.map((skill) => chalk.green(` ~ ${skill.name}: ${skill.was ?? "none"} → ${skill.now ?? "?"}`)));
9241
+ }
9242
+ if (report.renames) {
9243
+ lines.push(report.renames.length === 0 ? chalk.green("renames: none written") : "renames:");
9244
+ lines.push(...report.renames.map((found) => chalk.yellow(` ${report.write ? "~" : "!"} ${found.file}:${String(found.line)} ${found.name} → ${found.to} (${found.since}: ${found.what})`)));
9245
+ }
9246
+ return lines.join("\n");
9247
+ };
9248
+ var upgrade = async (parts, options) => {
9249
+ const unknown = parts.filter((part) => !UPGRADE_PARTS.includes(part));
9250
+ if (unknown.length > 0) {
9251
+ fail(`Not a part of a project to upgrade: ${unknown.join(", ")}. The parts are ${UPGRADE_PARTS.join(", ")}.`);
9252
+ return;
9253
+ }
9254
+ const project = await projectHere("to upgrade");
9255
+ if (!project) return;
9256
+ const wanted = new Set(parts.length === 0 ? UPGRADE_PARTS : parts);
9257
+ const { root } = project;
9258
+ const manager = project.packageManager ?? "npm";
9259
+ const write = Boolean(options.write);
9260
+ const report = {
9261
+ version: CLI_VERSION,
9262
+ write
9263
+ };
9264
+ const plitzi = project.plitzi?.kind === "project" ? project.plitzi : void 0;
9265
+ if (!plitzi && (wanted.has("files") || wanted.has("packages"))) {
9266
+ fail("Its files and package.json are those of a project `plitzi create` wrote, and this is not one: `plitzi upgrade skills renames` brings up the rest.");
9267
+ return;
9268
+ }
9269
+ const answers = plitzi ? await answersFor(root, plitzi, manager) : void 0;
9270
+ if (answers && wanted.has("files")) {
9271
+ const ours = machineryFiles(answers);
9272
+ const recorded = (await readScaffoldRecord(root))?.files ?? {};
9273
+ const plans = await planFiles(root, ours, recorded, options.take ?? []);
9274
+ if (write) {
9275
+ const written = plans.filter((plan) => WRITES.has(plan.status));
9276
+ await writeFiles(root, Object.fromEntries(written.map((plan) => [plan.file, plan.ours])));
9277
+ await writeScaffoldRecord(root, CLI_VERSION, Object.fromEntries(plans.flatMap((plan) => {
9278
+ if (WRITES.has(plan.status) || plan.status === "current") return [[plan.file, digestOf(plan.ours)]];
9279
+ return plan.file in recorded ? [[plan.file, recorded[plan.file]]] : [];
9280
+ })));
9281
+ }
9282
+ report.files = plans.map((plan) => ({
9283
+ file: plan.file,
9284
+ status: plan.status,
9285
+ ...plan.status === "yours" && plan.yours !== void 0 ? { diff: unifiedDiff(plan.file, plan.yours, plan.ours) } : {}
9286
+ }));
9287
+ }
9288
+ if (answers && wanted.has("packages")) {
9289
+ const text = await readText(path.join(root, "package.json"));
9290
+ const plan = text === void 0 ? void 0 : planPackages(text, answers);
9291
+ if (plan) {
9292
+ const { after, ...rest } = plan;
9293
+ let installed;
9294
+ if (after !== void 0) {
9295
+ installed = "needed";
9296
+ if (write) {
9297
+ await fs.writeFile(path.join(root, "package.json"), after);
9298
+ if (options.install !== false && !options.json) installed = await install(manager, root) ? "done" : "failed";
9299
+ }
9300
+ }
9301
+ report.packages = {
9302
+ ...rest,
9303
+ ...installed === void 0 ? {} : { install: installed }
9304
+ };
9305
+ }
9306
+ }
9307
+ if (wanted.has("skills")) {
9308
+ const { plans, files } = await planSkills(root);
9309
+ if (write) await writeSkills(root, plans, files);
9310
+ report.skills = plans;
9311
+ }
9312
+ if (wanted.has("renames")) {
9313
+ const found = [];
9314
+ for (const file of await sourceFiles(root)) {
9315
+ const text = await readText(path.join(root, file));
9316
+ const inFile = text === void 0 ? [] : renamesIn(file, text);
9317
+ if (write && text !== void 0 && inFile.length > 0) {
9318
+ const renamed = [...new Set(inFile.map((entry) => entry.name))].reduce((result, name) => result.replace(wordOf(name), RENAMES.find((rename) => rename.name === name)?.to ?? name), text);
9319
+ await fs.writeFile(path.join(root, file), renamed);
9320
+ }
9321
+ found.push(...inFile);
9322
+ }
9323
+ report.renames = found;
9324
+ }
9325
+ console.log(options.json ? JSON.stringify(report) : reportText(report));
9326
+ if (report.packages?.install === "failed") {
9327
+ process.exitCode = 1;
9328
+ console.error(chalk.dim(`\`${installCommand(manager)}\` failed.`));
9329
+ }
7717
9330
  };
7718
9331
  //#endregion
7719
9332
  //#region src/options.ts
@@ -7747,7 +9360,7 @@ var positiveInteger = (value) => {
7747
9360
  * documented, which was the problem — a person had to read four things and write three files correctly before they
7748
9361
  * could tell whether any of it worked. The same holds for an element of one's own, which is the second thing it does.
7749
9362
  */
7750
- program.name("plitzi").description("Plitzi command line");
9363
+ program.name("plitzi").description("Plitzi command line").version(CLI_VERSION);
7751
9364
  /** A flag given more than once, as the list of every value. */
7752
9365
  var collect = (value, previous) => [...previous, value];
7753
9366
  /** The flags that shape a PROJECT: given with `--plugin`, they are a mistake worth saying rather than ignoring. */
@@ -7759,7 +9372,7 @@ var PROJECT_ONLY = [
7759
9372
  ];
7760
9373
  /** The widths a page is looked at, as one flag every command that opens a page shares. */
7761
9374
  var widthsOption = (description, fallback) => new Option("--width <px,px>", description).argParser(widths).default(fallback, fallback.join(","));
7762
- var schemeOption = () => new Option("--scheme <scheme>", "The colour scheme").choices(SCHEMES).default("light");
9375
+ var schemeOption = () => new Option("--scheme <scheme>", "The space’s theme, as a visitor’s toggle sets it. The space’s own default when left out").choices(SCHEMES);
7763
9376
  var API_OPTION = ["--api <url>", "The platform’s API. Defaults to PLITZI_API_URL, else the one signed in to, else https://api.plitzi.com."];
7764
9377
  program.command("create").argument("[directory]", "Where to write it. Defaults to the current directory, or asked for with --plugin.").description("Scaffold a project that renders a Plitzi space, or with --plugin a plugin package").addOption(new Option("-m, --mode <mode>", "server (SSR + RSC on a Node tier) or client (browser only)").choices(["server", "client"])).addOption(new Option("-s, --source <source>", "local (the space travels in the project) or cloud (read it from Plitzi)").choices(["local", "cloud"])).option("-k, --key <key>", "Cloud only: the space key (asked for when omitted)").option("-e, --environment <environment>", "Which version: main (the draft), or a published environment — what --from takes out, and what a cloud project serves", "main").option("--revision <n>", "A published environment’s revision, pinned rather than its latest (with --from or --source cloud)").addOption(new Option("-p, --package-manager <manager>", "The package manager the project is written for. Asked for when left out.").choices([...PACKAGE_MANAGERS])).option("--from <space>", "Make it from a space on Plitzi (its id or permanent URL): its pages, actions, functions, runtime, plugins and files, served by the project alone. Signs in as you; the space must be one you can change.").option(...API_OPTION).option("--plugin", "A plugin package instead: one element any space can load, with a preview and a build").option("--name <name>", "Plugin only: its package name (plitzi-plugin-seat-picker). Asked for when left out.").option("--title <title>", "Plugin only: what the builder calls the element").option("--description <description>", "Plugin only: what the element is for, in a sentence").option("--owner <owner>", "Plugin only: who publishes it").option("--elements <names>", "Plugin only: other elements the package holds, by name, separated by commas").addOption(new Option("-t, --template <template>", "What a space written here starts as: welcome (a tour, the default), blank (tokens, a layout, one empty page) or catalog (a shop: layout, card component, data, filtered list, a page per product)").choices([...CREATE_TEMPLATES])).option("--no-install", "Write the files without installing dependencies").option("-f, --force", "Write into a directory that is not empty").option("-y, --yes", "At a terminal: take the defaults for any choice not passed. Without one, every choice must be passed").action((directory, options) => {
7765
9378
  if (!options.plugin) return create(directory, options);
@@ -7768,6 +9381,7 @@ program.command("create").argument("[directory]", "Where to write it. Defaults t
7768
9381
  return createPlugin(directory, options);
7769
9382
  });
7770
9383
  program.command("pull").description("Bring a project made with create --from up to date with its space: what changed there is written, what changed here is kept").option("-f, --force", "Where a file changed both here and on the space, take the space’s copy").option("-e, --environment <environment>", "Follow another version from now on: main (the draft), or a published environment").option("--revision <n>", "Pin a published revision of it, or latest to follow its newest again").option(...API_OPTION).action((options) => pull(options));
9384
+ program.command("push").argument("[parts...]", `Only these: ${PUSH_PARTS.join(", ")}. None named: what changed — ticked at a terminal, to choose from`).description("Put the project back on the space it works with, as its draft: the way back of pull. Refused when the draft changed since").option("-f, --force", "Replace the space’s draft even when it changed since, or holds work this project never had").option("--cdn <identifier>", "Which of the space’s CDNs a plugin goes to, when it has several").option("--bucket <identifier>", "Which public bucket a plugin goes in, when there are several").option(...API_OPTION).action((parts, options) => push(parts, options));
7771
9385
  program.command("add").description("Add something to the project you are in").command("plugin").argument("[names...]", "What each element is called: seat-picker. Asked for when left out.").description("Add elements of your own to this project: each one its component, declaration and builder panel").option("-d, --dir <folder>", "The folder that holds the project’s components. Asked for, unless the project says.").option("--title <title>", "One element only: what the builder calls it").option("--description <description>", "One element only: what it is for, in a sentence").option("--prop <name:type=default>", "One element only: an attribute it takes — interval:number=5000, paused:boolean, label:string=Hi. Repeat it", collect, []).option("--trigger <name:fields>", "An event it fires and what a flow reads — onTick:count. Repeat it", collect, []).option("--callback <name>", "An action a flow can call on it — reset. Repeat it", collect, []).option("--headless", "Nothing to see: hidden on a page, a badge in the builder (a clock, a listener)").option("-f, --force", "Write into a folder that is not empty").action((names, options) => addPlugin(names, options));
7772
9386
  var pack = program.command("pack").description("Build something of this project into what the platform takes");
7773
9387
  pack.command("plugin").argument("[folders...]", "Element folders to pack, the main one first. In a plugin package, left out: all of them.").description("Build a plugin: one ES module, its plugin-manifest.json, and the zip the builder takes under Resources").option("-o, --out <folder>", "Where the build goes. Emptied first; it must be inside the project.").option("--no-zip", "Build without the zip").option("--plugin-version <version>", "The version the manifest carries. Defaults to the one in package.json.").option("--source-root <folder>", "The project its source is kept relative to, when the elements are a project of their own inside this one").action((folders, options) => packPluginCommand(folders, options));
@@ -7776,16 +9390,17 @@ program.command("login").description("Sign in, in your browser. The session is k
7776
9390
  program.command("logout").description("Sign out: the session is revoked on the platform and forgotten here").action(() => logout());
7777
9391
  program.command("whoami").description("Who the CLI is signed in as, and the space it works in").option("--json", "One object: { api, user, space }, for a tool or an agent").option(...API_OPTION).action((options) => whoami(options));
7778
9392
  program.command("space").description("Choose the space to work in, in your browser. One at a time: it replaces the one chosen before").option(...API_OPTION).action((options) => space(options));
7779
- program.command("check").argument("[path]", "The page: /about. The home page when left out").description("Whether a page of the running project is whole, in text: every element on screen, no broken image, no console error, no failed flow").addOption(widthsOption("The widths to check it at, separated by commas", [1440, 390])).addOption(schemeOption()).option("--state", "Also what the page holds: its state, and every source by name").option("--element <id>", "Also one element: what it reads, its own state, whether it is on screen").option("--json", "One object per width, for a tool or an agent").action((path, options) => check(path, options));
9393
+ program.command("check").argument("[path]", "The page: /about. The home page when left out").description("Whether a page of the running project is whole, in text: every element on screen, no broken image, no console error, no failed flow").addOption(widthsOption("The widths to check it at, separated by commas", [1440, 390])).addOption(schemeOption()).option("--state", "Also what the page holds: its state, and every source by name").option("--element <id>", "Also one element: what it reads, its own state, whether it is on screen").option("--ssr", "Also the HTML the server sent against the hydrated page: what a server provider sent late").option("--json", "One object per width, for a tool or an agent").action((path, options) => check(path, options));
7780
9394
  program.command("fix").description("What authoring would fix in the space, as edits to your source: shown as a diff; with --write written, formatted and checked").option("--write", "Write the edits — kept only when the space authors with each fix gone and no problem added").option("--json", "The plan as one object, for a tool or an agent").action((options) => fix(options));
7781
9395
  program.command("import").argument("<url>", "The page to start from: https://example.com/pricing").description("A page of a site you verified, as a place to start writing from: its tokens, the outline of its blocks per breakpoint, its lists as JSON — never its words").option("-o, --out <dir>", "Where it is written, inside the project", "src/imported").addOption(widthsOption("The widths it is measured at: the widest is desktop, then tablet and mobile", [
7782
9396
  1440,
7783
9397
  768,
7784
9398
  390
7785
9399
  ])).option("-f, --force", "Write over what --out already holds").option("--json", "One object, for a tool or an agent").option(...API_OPTION).action((url, options) => importPage(url, options));
7786
- program.command("shot").argument("[path]", "The page: /about. The home page when left out").description("A picture of one page of the running project — and, asked, how it differs from another or what moves").addOption(new Option("--width <px>", "The viewport width").argParser(width).default(1280)).addOption(new Option("--height <px>", "The viewport height (the picture is the whole page)").argParser(positiveInteger).default(800)).addOption(schemeOption()).option("-o, --out <file>", "Where the picture goes. visual/.shots/<page>-<width>-<scheme>.png by default").option("--compare <url>", "Another site: the same page there, side by side, and how much differs by section").addOption(new Option("--frames <n>", "Pictures taken one after another: which sections move (a marquee, an autoplay)").argParser(positiveInteger)).addOption(new Option("--every <ms>", "How far apart --frames are taken").argParser(positiveInteger).default(500)).option("--wait-for <element>", "An element to wait for first: its name (data-plitzi-el) or a CSS selector").option("--reduced-motion", "As a visitor who asked for less motion").option("--json", "One object, for a tool or an agent").action((path, options) => shot(path, options));
7787
- program.command("explain").argument("[name]", "An element, a step, a trigger, a problem’s code or a transformer: container, navigate, class-and-css").description("What a name means when authoring a space — what it takes, fires, reads and how it is written").option("--list <kind>", "Every name of a kind: elements, steps, triggers, codes, transformers").option("--json", "One object, for a tool or an agent").action((name, options) => explainCommand(name, options));
7788
- program.command("skills").description("The Plitzi skills an agent reads in .claude/skills/").command("update").description("Bring them up to the Plitzi packages this project has installed, each replaced whole").action(() => skillsUpdate());
9400
+ program.command("shot").argument("[path]", "The page: /about. The home page when left out").description("A picture of one page of the running project — and, asked, how it differs from another or what moves").addOption(new Option("--width <px>", "The viewport width").argParser(width).default(1280)).addOption(new Option("--height <px>", "The viewport height (the picture is the whole page unless --viewport, --scroll-to or --clip)").argParser(positiveInteger).default(800)).addOption(schemeOption()).option("-o, --out <file>", "Where the picture goes. tmp/shots/<page>-<width>-<scheme>.png by default").option("--compare <url>", "Another site: the same page there, side by side, and how much differs by section").addOption(new Option("--frames <n>", "Pictures taken one after another: which sections move (a marquee, an autoplay)").argParser(positiveInteger)).addOption(new Option("--every <ms>", "How far apart --frames are taken").argParser(positiveInteger).default(500)).option("--wait-for <element>", "An element to wait for first: its name (data-plitzi-el) or a CSS selector").option("--clip <element>", "Only this element — its name or a CSS selector — scrolled to wherever it is").option("--scroll-to <element>", "The page scrolled until this element is in view; the picture is the screen there").option("--viewport", "Only what fits the viewport, not the whole page").option("--reduced-motion", "As a visitor who asked for less motion").option("--json", "One object, for a tool or an agent").action((path, options) => shot(path, options));
9401
+ program.command("explain").argument("[name]", "An element, a step, a trigger, a problem’s code, a transformer or a helper: container, navigate, class-and-css, bindTemplate, motion").description("What a name means when authoring a space — what it takes, fires, reads and how it is written").option("--list <kind>", "Every name of a kind: elements, steps, triggers, codes, transformers, helpers").option("--json", "One object, for a tool or an agent").action((name, options) => explainCommand(name, options));
9402
+ program.command("upgrade").alias("update").argument("[parts...]", `Only these: ${UPGRADE_PARTS.join(", ")} — every one when none is named`).description("Bring the project up to this CLI: its machinery files, package.json, the skills and renamed names — shown; with --write made").option("--write", "Make the changes: files nobody changed replaced, package.json merged and installed, names renamed").option("--take <files...>", "Replace these files of your own too (`all` for every one), after reading their diff").option("--no-install", "Write package.json and leave the install to you").option("--json", "One object, for a tool or an agent").action((parts, options) => upgrade(parts, options));
9403
+ program.command("skills").description("The Plitzi skills an agent reads in .claude/skills/").command("update").description("Bring them up to the Plitzi packages this project has installed, each replaced whole (`upgrade skills --write`)").action(() => upgrade(["skills"], { write: true }));
7789
9404
  program.command("data").description("The JSON a project serves (public/data), without reading it whole").command("describe").argument("<file>", "A JSON file: public/data/products.json").description("Its shape — every field, its type, whether every row has it — and one row of its longest list").option("--json", "One object: { shape, example }").action((file, options) => dataDescribe(file, options));
7790
9405
  program.command("upload").description("Put something of this project on the space you work in").command("plugin").argument("[zip]", "The zip plitzi pack plugin built. Left out: the one packed in this project.").description("Upload a packed plugin to a CDN of the space you work in, and install it there").option("--cdn <identifier>", "Which of the space’s CDNs the bucket is in.").option("--bucket <identifier>", "Which public bucket the plugin goes in. Asked for when there are several.").option(...API_OPTION).action((zip, options) => uploadPluginCommand(zip, options));
7791
9406
  var functions = program.command("functions").description("The space’s own server code: functions/ in this project is a working copy of it");