@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/CHANGELOG.md +323 -0
- package/README.md +67 -13
- package/dist/index.js +2060 -445
- package/package.json +3 -3
- package/skills/plitzi-cli/SKILL.md +23 -17
- package/skills/plitzi-cli/reference/from-space.md +10 -1
- package/skills/plitzi-cli/reference/functions.md +14 -1
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,
|
|
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
|
|
1482
|
-
* process and
|
|
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
|
|
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
|
-
/**
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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")}\`
|
|
1609
|
-
|
|
1610
|
-
|
|
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(
|
|
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 = [
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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, '
|
|
1957
|
+
mkdirSync(path.join(PROJECT_ROOT, 'tmp'), { recursive: true });
|
|
1887
1958
|
writeFileSync(
|
|
1888
|
-
path.join(PROJECT_ROOT, '
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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
|
|
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$
|
|
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
|
-
*
|
|
2288
|
+
* Authors the declaration and says what it found: the check an agent and a person run after every change.
|
|
2147
2289
|
*
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
2352
|
+
return { ...(skill && skill !== sdk ? { skill } : {}), ...(files && files !== sdk ? { files } : {}), sdk };
|
|
2196
2353
|
};
|
|
2197
2354
|
|
|
2198
2355
|
try {
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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
|
|
2366
|
+
const behind = outdated();
|
|
2204
2367
|
if (json) {
|
|
2205
2368
|
console.log(
|
|
2206
|
-
JSON.stringify({ ok: true, pages: schema.pages.length, warnings, suggestions, ...(
|
|
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
|
-
|
|
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
|
-
\`[
|
|
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
|
|
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('
|
|
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: '
|
|
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\
|
|
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: '
|
|
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", "
|
|
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
|
|
4234
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
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))
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
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,
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
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:
|
|
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
|
-
|
|
4297
|
-
...
|
|
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
|
-
|
|
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 =
|
|
4324
|
-
|
|
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
|
|
4343
|
-
if (
|
|
4344
|
-
fail(
|
|
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
|
|
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,
|
|
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
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
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
|
-
|
|
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,
|
|
4483
|
-
if (!draft) return;
|
|
4484
|
-
if (Object.keys(draft.files).length) {
|
|
4485
|
-
fail(`${
|
|
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
|
-
|
|
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(`${
|
|
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:
|
|
4825
|
+
space: space.id,
|
|
4521
4826
|
version: reply.data.version,
|
|
4522
4827
|
files: local
|
|
4523
4828
|
});
|
|
4524
|
-
console.log(chalk.green(`
|
|
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, {
|
|
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,
|
|
5114
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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 ? {
|
|
6353
|
+
return found ? one({
|
|
5950
6354
|
start: found.getStart(sourceFile),
|
|
5951
6355
|
end: found.getEnd(),
|
|
5952
6356
|
text: quoted(edit.to)
|
|
5953
|
-
}
|
|
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 = () => (
|
|
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() :
|
|
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() :
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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/
|
|
7140
|
-
/**
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
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
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
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
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
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(
|
|
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(`${
|
|
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,
|
|
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
|
-
|
|
7656
|
-
|
|
7657
|
-
const
|
|
7658
|
-
if (!
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
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(`${
|
|
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
|
|
8209
|
+
filename,
|
|
7690
8210
|
bucket: bucket.identifier
|
|
7691
8211
|
});
|
|
7692
|
-
const uploaded = await authorizedRequest(
|
|
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(`${
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
7787
|
-
program.command("explain").argument("[name]", "An element, a step, a trigger, a problem’s code or a
|
|
7788
|
-
program.command("
|
|
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");
|