@plitzi/cli 0.37.9 → 0.37.10
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 +588 -0
- package/README.md +132 -3
- package/dist/index.js +2503 -590
- package/package.json +3 -3
- package/skills/plitzi-cli/SKILL.md +59 -41
- package/skills/plitzi-cli/reference/from-space.md +23 -0
- package/skills/plitzi-cli/reference/functions.md +18 -0
package/dist/index.js
CHANGED
|
@@ -1,21 +1,236 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Option, program } from "commander";
|
|
2
|
+
import { InvalidArgumentError, Option, program } from "commander";
|
|
3
|
+
import fs, { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { builtinModules, createRequire } from "node:module";
|
|
5
|
+
import path, { dirname, join, relative, sep } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
3
7
|
import chalk from "chalk";
|
|
4
|
-
import
|
|
8
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
5
9
|
import os from "node:os";
|
|
6
|
-
import path, { dirname, join, relative, sep } from "node:path";
|
|
7
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
8
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
9
11
|
import { createServer } from "node:http";
|
|
10
12
|
import tls from "node:tls";
|
|
11
13
|
import readline from "node:readline/promises";
|
|
12
|
-
import { builtinModules, createRequire } from "node:module";
|
|
13
14
|
import { readFileSync, readdirSync, watch } from "node:fs";
|
|
14
|
-
import { actionSpecFromEntry, actionToSource, blankSpaceSource, elementTypeNames } from "@plitzi/sdk-authoring";
|
|
15
|
-
import {
|
|
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";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import { lookup } from "node:dns/promises";
|
|
16
18
|
import { build } from "esbuild";
|
|
17
19
|
import { unzipSync, zipSync } from "fflate";
|
|
18
20
|
import { gzipSync } from "node:zlib";
|
|
21
|
+
//#region ../../packages/sdk-shared/dist/helpers/isRecord.mjs
|
|
22
|
+
var e$1 = (e) => typeof e == "object" && !!e && !Array.isArray(e);
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/browser/index.ts
|
|
25
|
+
/**
|
|
26
|
+
* A browser on the project's own Playwright, and the project's own server to point it at — what `plitzi check`,
|
|
27
|
+
* `plitzi shot` and `plitzi import` look at a page with (`import` at a site of the person's, not the project's server).
|
|
28
|
+
*
|
|
29
|
+
* Playwright is the project's: `create` installs it for the visual tests, and a second copy in the CLI would be a
|
|
30
|
+
* second set of browsers to download. It is described here by the little that is used of it, and checked to be that
|
|
31
|
+
* when it loads, so the CLI depends on no driver.
|
|
32
|
+
*/
|
|
33
|
+
var SCHEMES = ["light", "dark"];
|
|
34
|
+
var isChromium = (value) => e$1(value) && typeof value.launch === "function";
|
|
35
|
+
/** A property of an object or of a function, which a module's exports can be either of. */
|
|
36
|
+
var propertyOf = (value, key) => (typeof value === "object" || typeof value === "function") && value !== null ? Reflect.get(value, key) : void 0;
|
|
37
|
+
/** The project's `@playwright/test`'s Chromium, launched — or why there is none. */
|
|
38
|
+
var launchBrowser = async (root) => {
|
|
39
|
+
let loaded;
|
|
40
|
+
try {
|
|
41
|
+
const resolved = createRequire(path.join(root, "package.json")).resolve("@playwright/test");
|
|
42
|
+
loaded = await import(pathToFileURL(resolved).href);
|
|
43
|
+
} catch {
|
|
44
|
+
return { problem: "This project has no @playwright/test installed: npm install -D @playwright/test" };
|
|
45
|
+
}
|
|
46
|
+
const chromium = [propertyOf(loaded, "chromium"), propertyOf(propertyOf(loaded, "default"), "chromium")].find(isChromium);
|
|
47
|
+
if (!isChromium(chromium)) return { problem: "The @playwright/test installed here has no Chromium to launch." };
|
|
48
|
+
try {
|
|
49
|
+
return await chromium.launch();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return { problem: `Chromium did not start (${error instanceof Error ? error.message.split("\n")[0] : String(error)}): npx playwright install chromium` };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/** What `npm start` wrote down when it took a port: the port, and the name its `/health` answers with. */
|
|
55
|
+
var recorded = async (root) => {
|
|
56
|
+
try {
|
|
57
|
+
const value = JSON.parse(await readFile(path.join(root, ".plitzi/dev-server.json"), "utf8"));
|
|
58
|
+
if (!e$1(value)) return {};
|
|
59
|
+
return {
|
|
60
|
+
...typeof value.port === "number" ? { port: value.port } : {},
|
|
61
|
+
...typeof value.name === "string" ? { name: value.name } : {}
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Where the project's server answers: `PORT` when set, the port `npm start` took otherwise, else the mode's default —
|
|
69
|
+
* and, when `npm start` named itself, that what answers there is this project. Something else on the port answers too,
|
|
70
|
+
* with a page and a `200`, and a picture of the wrong server looks like one of the right one.
|
|
71
|
+
*/
|
|
72
|
+
var projectOrigin = async (root, project) => {
|
|
73
|
+
const server = await recorded(root);
|
|
74
|
+
const port = Number(process.env.PORT ?? server.port ?? (project?.mode === "client" ? 5173 : 8080));
|
|
75
|
+
const origin = `http://127.0.0.1:${String(port)}`;
|
|
76
|
+
if (!server.name) return { origin };
|
|
77
|
+
const health = await fetch(`${origin}/health`).then((response) => response.json()).catch(() => null);
|
|
78
|
+
const answered = e$1(health) && typeof health.Server === "string" ? health.Server : "";
|
|
79
|
+
if (answered !== server.name) return { problem: answered ? `Port ${String(port)} answers as "${answered}", not this project ("${server.name}"). Start it: npm start` : `Nothing of this project answers on port ${String(port)}. Start it: npm start` };
|
|
80
|
+
return { origin };
|
|
81
|
+
};
|
|
82
|
+
/** A picture as a data URL, which is how a page in the browser is handed one to read. */
|
|
83
|
+
var dataUrl = (png) => `data:image/png;base64,${Buffer.from(png).toString("base64")}`;
|
|
84
|
+
/** A data URL's picture back as bytes, to write to a file. */
|
|
85
|
+
var fromDataUrl = (url) => Buffer.from(url.slice(url.indexOf(",") + 1), "base64");
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/commands/terminal.ts
|
|
88
|
+
/**
|
|
89
|
+
* What every command shares with the person running it: whether anybody is there, how a choice is put to them, and
|
|
90
|
+
* what happens when nobody is.
|
|
91
|
+
*/
|
|
92
|
+
/** Why a command stopped, to stderr, and the exit code that says it did. */
|
|
93
|
+
var fail = (problem) => {
|
|
94
|
+
console.error(chalk.red(problem));
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
};
|
|
97
|
+
/** Somebody who can answer a prompt. An agent's shell, CI and a pipe are nobody, whatever they were asked to do. */
|
|
98
|
+
var atTerminal = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
99
|
+
/**
|
|
100
|
+
* Nobody to ask, so the questions go to whoever ran this — in practice an agent — to put to the person.
|
|
101
|
+
*
|
|
102
|
+
* It offers no way around them. It used to end with "or with --yes to take the defaults", and an agent keen to finish
|
|
103
|
+
* took that exit every time: the person was never asked, which is the whole failure this stop exists to prevent.
|
|
104
|
+
*/
|
|
105
|
+
var refuseWithoutTerminal = (what, questions, stopped = `plitzi create stopped before writing anything: these choices shape the whole ${what}`) => {
|
|
106
|
+
console.error(chalk.red(`\n${stopped}, they belong to the person it is for, and nobody is at this terminal to make them.`));
|
|
107
|
+
console.error(chalk.bold("\nIf you are an AI agent: ask the user each question below, show them the options, and wait for their answers. Do not choose for them and do not fall back to a default.\n"));
|
|
108
|
+
questions.forEach(({ flag, choices, question }, index) => {
|
|
109
|
+
console.error(` ${index + 1}. ${question}`);
|
|
110
|
+
console.error(` ${flag} ${choices.join(" | ")}`);
|
|
111
|
+
});
|
|
112
|
+
const flags = questions.map(({ flag }) => `${flag} <their answer>`).join(" ");
|
|
113
|
+
console.error(`\nThen run the same command again with their answers: ${flags}`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
};
|
|
116
|
+
/** One choice at the terminal: Enter takes the default, anything off the list is refused rather than guessed at. */
|
|
117
|
+
var ask = async (rl, question) => {
|
|
118
|
+
const reply = (await rl.question(`\n${question.question}\n${question.flag} (${question.choices.join("/")}) [${question.fallback}] > `)).trim();
|
|
119
|
+
const chosen = question.choices.find((choice) => choice === (reply || question.fallback));
|
|
120
|
+
if (!chosen) console.error(chalk.red(`"${reply}" is not one of ${question.choices.join(", ")}.`));
|
|
121
|
+
return chosen;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* A free answer at the terminal, with the likely one offered. `check` answers what is wrong with a reply, if anything:
|
|
125
|
+
* a refused reply is said and asked again, since a name or a path is typed rather than picked and a typo is the
|
|
126
|
+
* usual reason for one.
|
|
127
|
+
*/
|
|
128
|
+
var askText = async (rl, question, fallback, check = () => void 0) => {
|
|
129
|
+
for (;;) {
|
|
130
|
+
const offered = fallback ? `[${fallback}]` : "(optional)";
|
|
131
|
+
const reply = (await rl.question(`\n${question}\n${offered} > `)).trim() || fallback;
|
|
132
|
+
const problem = check(reply);
|
|
133
|
+
if (!problem) return reply;
|
|
134
|
+
console.error(chalk.red(problem));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* One of a numbered list, or something else typed out. Enter takes the first entry. `other` is the question for the
|
|
139
|
+
* typed answer, offered as the last number.
|
|
140
|
+
*/
|
|
141
|
+
var askChoice = async (rl, question, options, other) => {
|
|
142
|
+
const menu = [...options.map((option) => option.label), other.label].map((label, index) => ` ${index + 1}. ${label}`).join("\n");
|
|
143
|
+
for (;;) {
|
|
144
|
+
const reply = (await rl.question(`\n${question}\n${menu}\n[1] > `)).trim() || "1";
|
|
145
|
+
const index = Number(reply) - 1;
|
|
146
|
+
if (Number.isInteger(index) && index >= 0 && index < options.length) return options[index].value;
|
|
147
|
+
if (index === options.length) return askText(rl, other.question, other.fallback, other.check);
|
|
148
|
+
console.error(chalk.red(`"${reply}" is not a number from 1 to ${options.length + 1}.`));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/** One of a numbered list and nothing else. Enter takes the first entry. */
|
|
152
|
+
var askPick = async (rl, question, options) => {
|
|
153
|
+
const menu = options.map(({ label }, index) => ` ${index + 1}. ${label}`).join("\n");
|
|
154
|
+
for (;;) {
|
|
155
|
+
const reply = (await rl.question(`\n${question}\n${menu}\n[1] > `)).trim() || "1";
|
|
156
|
+
const index = Number(reply) - 1;
|
|
157
|
+
if (Number.isInteger(index) && index >= 0 && index < options.length) return options[index].value;
|
|
158
|
+
console.error(chalk.red(`"${reply}" is not a number from 1 to ${options.length}.`));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
/** A directory that does not exist yet is as empty as one can be, which is the answer this question wants. */
|
|
162
|
+
var isEmpty = async (target) => {
|
|
163
|
+
try {
|
|
164
|
+
return (await fs.readdir(target)).length === 0;
|
|
165
|
+
} catch {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var isDirectory$1 = async (candidate) => {
|
|
170
|
+
try {
|
|
171
|
+
return (await fs.stat(candidate)).isDirectory();
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/** The closest directory that already exists: the project's own does not until its files are written. */
|
|
177
|
+
var nearestExisting = async (target) => {
|
|
178
|
+
let dir = target;
|
|
179
|
+
while (!await isDirectory$1(dir) && path.dirname(dir) !== dir) dir = path.dirname(dir);
|
|
180
|
+
return dir;
|
|
181
|
+
};
|
|
182
|
+
/** Every file, by the path under `target` it is written to. */
|
|
183
|
+
var writeFiles = async (target, files) => {
|
|
184
|
+
await Promise.all(Object.entries(files).map(async ([file, contents]) => {
|
|
185
|
+
const destination = path.join(target, file);
|
|
186
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
187
|
+
return fs.writeFile(destination, contents);
|
|
188
|
+
}));
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* What a first install is most likely refused over, and what to do about it.
|
|
192
|
+
*
|
|
193
|
+
* The scaffold already exempts what it ships, but not the rest of the tree: a third-party dependency published in
|
|
194
|
+
* the last day is held back by the same age gates, and says so in a code that means nothing to somebody who has
|
|
195
|
+
* not touched the project yet.
|
|
196
|
+
*/
|
|
197
|
+
var INSTALL_HINTS = {
|
|
198
|
+
npm: "A peer-dependency conflict (ERESOLVE) is a bug in this scaffold: please report it. `npm install --legacy-peer-deps` gets past it meanwhile.",
|
|
199
|
+
yarn: "YN0016 (\"quarantined\") means a dependency was published less than a day ago. Wait, or add it to `npmPreapprovedPackages` in .yarnrc.yml.",
|
|
200
|
+
pnpm: "A dependency published too recently is added to `minimumReleaseAgeExclude` in pnpm-workspace.yaml; a skipped build script is approved with `pnpm approve-builds`."
|
|
201
|
+
};
|
|
202
|
+
var install = (manager, cwd) => new Promise((resolve) => {
|
|
203
|
+
const child = spawn(manager, ["install"], {
|
|
204
|
+
cwd,
|
|
205
|
+
stdio: "inherit",
|
|
206
|
+
shell: process.platform === "win32"
|
|
207
|
+
});
|
|
208
|
+
child.on("error", () => resolve(false));
|
|
209
|
+
child.on("close", (code) => resolve(code === 0));
|
|
210
|
+
});
|
|
211
|
+
/**
|
|
212
|
+
* One of the project's own scripts, run quietly: what it prints is the project's business, and whether it worked is
|
|
213
|
+
* all this needs. `run` is understood by all three managers alike.
|
|
214
|
+
*/
|
|
215
|
+
var runScript = (manager, script, cwd) => new Promise((resolve) => {
|
|
216
|
+
const child = spawn(manager, ["run", script], {
|
|
217
|
+
cwd,
|
|
218
|
+
stdio: "ignore",
|
|
219
|
+
shell: process.platform === "win32"
|
|
220
|
+
});
|
|
221
|
+
child.on("error", () => resolve(false));
|
|
222
|
+
child.on("close", (code) => resolve(code === 0));
|
|
223
|
+
});
|
|
224
|
+
/**
|
|
225
|
+
* How to get to `target` from here, in whichever form is shorter.
|
|
226
|
+
*
|
|
227
|
+
* A relative path out of a temp directory is six `../` and unreadable; an absolute one to a sibling folder is noise.
|
|
228
|
+
* These lines are meant to be pasted, so they are printed the way somebody would have typed them.
|
|
229
|
+
*/
|
|
230
|
+
var cdPrefix = (target) => {
|
|
231
|
+
const relative = path.relative(process.cwd(), target);
|
|
232
|
+
return target === process.cwd() ? "" : `cd ${relative.length < target.length ? relative : target} && `;
|
|
233
|
+
};
|
|
19
234
|
/** `$XDG_CONFIG_HOME/plitzi`, `%APPDATA%\plitzi` on Windows, `~/.config/plitzi` otherwise. */
|
|
20
235
|
var configDir = () => {
|
|
21
236
|
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, "plitzi");
|
|
@@ -395,7 +610,7 @@ var spaceOfGrant = async (api, grant) => {
|
|
|
395
610
|
* A new connection, made in the browser, REPLACING whatever connection there was — another space, another account,
|
|
396
611
|
* another platform. The old grant is revoked rather than left alive in nobody's hands.
|
|
397
612
|
*/
|
|
398
|
-
var connect
|
|
613
|
+
var connect = async (api, { scope, open }) => {
|
|
399
614
|
const signedIn = await signInThroughBrowser(api, {
|
|
400
615
|
scope,
|
|
401
616
|
open
|
|
@@ -518,24 +733,20 @@ var authorizedRequest = async (connection, pathname, init = {}) => {
|
|
|
518
733
|
};
|
|
519
734
|
//#endregion
|
|
520
735
|
//#region src/commands/account.ts
|
|
521
|
-
/**
|
|
736
|
+
/**
|
|
737
|
+
* The address to open, printed as well: over SSH, or with no browser to start, it is opened by hand. To stderr, like
|
|
738
|
+
* every prompt on the way to a command's answer, so `--json` output stays only the answer.
|
|
739
|
+
*/
|
|
522
740
|
var openInBrowser = (url) => {
|
|
523
|
-
console.
|
|
741
|
+
console.error(`\nContinue in your browser. If it did not open, go to:\n ${chalk.cyan(url)}`);
|
|
524
742
|
openBrowser(url);
|
|
525
743
|
};
|
|
526
744
|
/** The platform this command talks to, or undefined — said — when `--api` or `PLITZI_API_URL` is not an address. */
|
|
527
745
|
var apiFor = async (options) => {
|
|
528
746
|
const api = resolveApi(options.api, await readConnection());
|
|
529
|
-
if (!api) {
|
|
530
|
-
console.error(chalk.red(`"${options.api ?? process.env.PLITZI_API_URL ?? ""}" is not an address.`));
|
|
531
|
-
process.exitCode = 1;
|
|
532
|
-
}
|
|
747
|
+
if (!api) fail(`"${options.api ?? process.env.PLITZI_API_URL ?? ""}" is not an address.`);
|
|
533
748
|
return api;
|
|
534
749
|
};
|
|
535
|
-
var fail = (error) => {
|
|
536
|
-
console.error(chalk.red(error));
|
|
537
|
-
process.exitCode = 1;
|
|
538
|
-
};
|
|
539
750
|
/** Who the connection signs in as, asked of the platform — which also proves the session still works. */
|
|
540
751
|
var whoIs = async (connection) => {
|
|
541
752
|
const answered = await authorizedRequest(connection, "/auth/session");
|
|
@@ -564,7 +775,7 @@ var login = async (options) => {
|
|
|
564
775
|
}
|
|
565
776
|
}
|
|
566
777
|
const previous = await readConnection();
|
|
567
|
-
const connected = await connect
|
|
778
|
+
const connected = await connect(api, { open: openInBrowser });
|
|
568
779
|
if (!connected.ok) {
|
|
569
780
|
fail(connected.error);
|
|
570
781
|
return;
|
|
@@ -595,6 +806,19 @@ var whoami = async (options) => {
|
|
|
595
806
|
fail(who.error);
|
|
596
807
|
return;
|
|
597
808
|
}
|
|
809
|
+
if (options.json) {
|
|
810
|
+
const { space: chosen } = current.value;
|
|
811
|
+
console.log(JSON.stringify({
|
|
812
|
+
api,
|
|
813
|
+
user: who.value,
|
|
814
|
+
space: chosen ? {
|
|
815
|
+
id: chosen.id,
|
|
816
|
+
name: chosen.name,
|
|
817
|
+
permanentUrl: chosen.permanentUrl
|
|
818
|
+
} : null
|
|
819
|
+
}));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
598
822
|
console.log(`\nSigned in to ${api} as ${chalk.bold(who.value)}.`);
|
|
599
823
|
console.log(`${spaceLine(current.value)}\n`);
|
|
600
824
|
};
|
|
@@ -603,7 +827,7 @@ var whoami = async (options) => {
|
|
|
603
827
|
* works in one space at a time, and the space it worked in before is no longer reachable from it.
|
|
604
828
|
*/
|
|
605
829
|
var chooseSpace = async (api) => {
|
|
606
|
-
const connected = await connect
|
|
830
|
+
const connected = await connect(api, {
|
|
607
831
|
scope: "space",
|
|
608
832
|
open: openInBrowser
|
|
609
833
|
});
|
|
@@ -635,7 +859,7 @@ var connectionWithSpace = async (api, doing = "to work in") => {
|
|
|
635
859
|
return;
|
|
636
860
|
}
|
|
637
861
|
if (current.value?.space) return current.value;
|
|
638
|
-
console.
|
|
862
|
+
console.error(current.value ? `\nChoose the space ${doing}.` : `\nSign in, and choose the space ${doing}.`);
|
|
639
863
|
const chosen = await chooseSpace(api);
|
|
640
864
|
if (!chosen.ok) {
|
|
641
865
|
fail(chosen.error);
|
|
@@ -651,14 +875,19 @@ var connectionWithSpace = async (api, doing = "to work in") => {
|
|
|
651
875
|
var signedIn = async (api, doing) => {
|
|
652
876
|
const current = await currentConnection(api);
|
|
653
877
|
if (current.ok && current.value) return current.value;
|
|
654
|
-
console.
|
|
655
|
-
const connected = await connect
|
|
878
|
+
console.error(`\nSign in ${doing}.`);
|
|
879
|
+
const connected = await connect(api, { open: openInBrowser });
|
|
656
880
|
if (!connected.ok) {
|
|
657
881
|
fail(connected.error);
|
|
658
882
|
return;
|
|
659
883
|
}
|
|
660
884
|
return connected.value;
|
|
661
885
|
};
|
|
886
|
+
/** The platform the options name, then the connection to its chosen space: how a command that works in it starts. */
|
|
887
|
+
var connectToSpace = async (options, doing) => {
|
|
888
|
+
const api = await apiFor(options);
|
|
889
|
+
return api ? connectionWithSpace(api, doing) : void 0;
|
|
890
|
+
};
|
|
662
891
|
//#endregion
|
|
663
892
|
//#region src/scaffold/client.ts
|
|
664
893
|
/**
|
|
@@ -1054,11 +1283,10 @@ to author — and \`YourThing/index.ts\` with an \`export default\`.
|
|
|
1054
1283
|
|
|
1055
1284
|
## Three things that bite
|
|
1056
1285
|
|
|
1057
|
-
**Render \`RootElement\`, not a \`div\`.** It is what makes a plugin an
|
|
1286
|
+
**Render \`RootElement\`, not a \`div\`.** It is what makes a plugin an _element_: the id and classes the space
|
|
1058
1287
|
gave it land on what you render, so the CSS authored on the element applies, the builder can select it, and a test
|
|
1059
1288
|
can find it by name. A plain tag renders the same pixels and none of that.
|
|
1060
1289
|
|
|
1061
|
-
|
|
1062
1290
|
**Do not render anything that differs between the server and the first client render** — a clock, a random number,
|
|
1063
1291
|
anything read out of \`window\`. React answers a hydration mismatch by discarding the whole tree, so it does not
|
|
1064
1292
|
break the plugin, it blanks the page. Put live values in an effect.
|
|
@@ -1086,8 +1314,10 @@ ${list.length <= 120 ? list : `export const declarations: PluginDeclarationData[
|
|
|
1086
1314
|
`;
|
|
1087
1315
|
};
|
|
1088
1316
|
var pluginFiles = (answers) => ({
|
|
1089
|
-
|
|
1090
|
-
|
|
1317
|
+
...(answers.template ?? "welcome") === "welcome" ? {
|
|
1318
|
+
"src/plugins/StatCard/StatCard.tsx": component$1(),
|
|
1319
|
+
"src/plugins/StatCard/index.ts": barrel()
|
|
1320
|
+
} : {},
|
|
1091
1321
|
"src/plugins/README.md": readme$2(answers),
|
|
1092
1322
|
...answers.source === "local" ? { "src/plugins/declarations.ts": projectDeclarations([]) } : {}
|
|
1093
1323
|
});
|
|
@@ -1232,6 +1462,7 @@ var dependencies = ({ mode, source }) => ({
|
|
|
1232
1462
|
/** A server-mode project runs no bundler of its own: the page server builds the plugins, and Node runs the rest. */
|
|
1233
1463
|
var devDependencies = ({ mode }) => ({
|
|
1234
1464
|
...SHARED_DEV_DEPENDENCIES,
|
|
1465
|
+
"@plitzi/cli": SDK_VERSION,
|
|
1235
1466
|
...mode === "server" ? {} : { vite: VITE_VERSION }
|
|
1236
1467
|
});
|
|
1237
1468
|
/**
|
|
@@ -1274,11 +1505,12 @@ var scripts = ({ mode, source }) => ({
|
|
|
1274
1505
|
preview: "vite preview"
|
|
1275
1506
|
},
|
|
1276
1507
|
...source === "local" ? { author: "node src/author.ts" } : {},
|
|
1277
|
-
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
1508
|
+
typecheck: "tsc -p tsconfig.json --noEmit --pretty false",
|
|
1278
1509
|
lint: "eslint .",
|
|
1279
1510
|
format: "prettier --write .",
|
|
1280
1511
|
visual: "playwright test",
|
|
1281
|
-
shot: "
|
|
1512
|
+
shot: "plitzi shot",
|
|
1513
|
+
check: "plitzi check"
|
|
1282
1514
|
});
|
|
1283
1515
|
/**
|
|
1284
1516
|
* `extra` is what a project made from a space needs besides (`plitzi create --from`): the packages its plugins and
|
|
@@ -1341,7 +1573,6 @@ var tsconfig$1 = ({ mode }) => `${JSON.stringify({
|
|
|
1341
1573
|
},
|
|
1342
1574
|
include: [
|
|
1343
1575
|
"src",
|
|
1344
|
-
"scripts",
|
|
1345
1576
|
"visual",
|
|
1346
1577
|
"playwright.config.ts",
|
|
1347
1578
|
...mode === "client" ? ["vite.config.ts"] : []
|
|
@@ -1353,9 +1584,9 @@ var tsconfig$1 = ({ mode }) => `${JSON.stringify({
|
|
|
1353
1584
|
*/
|
|
1354
1585
|
var YARN_IGNORES$1 = "\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n";
|
|
1355
1586
|
/** Where the page server writes the plugin bundles it builds. A build output, and rebuilt whenever it is missing. */
|
|
1356
|
-
var SERVER_IGNORES = ".sdk-plugins\n";
|
|
1587
|
+
var SERVER_IGNORES = ".sdk-plugins\n.plitzi\n";
|
|
1357
1588
|
var gitignore$1 = ({ mode, packageManager }) => `node_modules\ndist\n.env\nvisual/.results\nvisual/screenshots\n${mode === "server" ? SERVER_IGNORES : ""}${packageManager === "yarn" ? YARN_IGNORES$1 : ""}`;
|
|
1358
|
-
var startLine = ({ mode, packageManager }) => mode === "server" ? `\`${runCommand(packageManager, "start")}\` serves pages on http://127.0.0.1:8080. \`${runCommand(packageManager, "start:dev")}\` restarts on save. In production, \`${runCommand(packageManager, "build")}\` once and run \`${runCommand(packageManager, "start:prod")}\` with \`NODE_ENV=production\`: the compiled server, with no TypeScript in the process.` : `\`${runCommand(packageManager, "start")}\` runs Vite on http://127.0.0.1:5173, with hot module replacement.`;
|
|
1589
|
+
var startLine = ({ mode, packageManager }) => mode === "server" ? `\`${runCommand(packageManager, "start")}\` serves pages on http://127.0.0.1:8080 — or, when something else holds 8080, the next free port, which it prints and writes to \`.plitzi/dev-server.json\` (set \`PORT\` to choose one). \`${runCommand(packageManager, "start:dev")}\` restarts on save. In production, \`${runCommand(packageManager, "build")}\` once and run \`${runCommand(packageManager, "start:prod")}\` with \`NODE_ENV=production\`: the compiled server, with no TypeScript in the process.` : `\`${runCommand(packageManager, "start")}\` runs Vite on http://127.0.0.1:5173, with hot module replacement.`;
|
|
1359
1590
|
var spaceSection = (answers) => {
|
|
1360
1591
|
if (answers.source === "cloud") return `## Where the space comes from
|
|
1361
1592
|
|
|
@@ -1413,10 +1644,14 @@ var agentsFile = (answers) => {
|
|
|
1413
1644
|
const commands = [
|
|
1414
1645
|
`| ${code(installCommand(answers.packageManager))} | install |`,
|
|
1415
1646
|
`| ${run("start")} | serve it |`,
|
|
1416
|
-
...local ? [`| ${run("author")} | author the space and print its warnings |`] : [],
|
|
1647
|
+
...local ? [`| ${run("author")} | author the space and print its warnings and suggestions |`] : [],
|
|
1417
1648
|
`| ${run("visual")} | open the page in a browser and check it rendered |`
|
|
1418
1649
|
];
|
|
1419
1650
|
const zeroWarnings = local ? `Zero warnings from ${run("author")}.` : "Zero warnings from authoring.";
|
|
1651
|
+
const port = answers.mode === "server" ? `${run("start")} serves on 8080, or on the next free port when something else holds it — printed, and written to ${code(".plitzi/dev-server.json")}, where ${code("check")}, ${code("shot")} and ${code("visual")} read it. ${code("PORT")} chooses one.` : `${run("start")} runs Vite on 5173.`;
|
|
1652
|
+
const pictures = answers.mode === "server" ? `- **Pictures from other sites** are resized by this server once ${code("src/main.ts")} names their hosts — ${code("images: { domains }")} in ${code("createServer")}, a list of hosts like ${code("images.example.com")} — and ${code("sharp")} is installed: an ${code("image")} then offers a ${code("srcset")} (give it ${code("sizes")}, and ${code("width")}/${code("height")} so nothing jumps).\n` : "";
|
|
1653
|
+
const serverData = answers.mode === "server" ? `; one whose ${code("runtime")} is ${code("server")} is read by the server, and the page arrives with it` : "";
|
|
1654
|
+
const generated = [...local ? [`- ${code("space/offline-data.json")} — what ${run("author")} writes out; the space is ${code("src/space.ts")}.`] : [], ...answers.mode === "server" ? [`- ${code(".sdk-plugins/")} and ${code(".plitzi/")} — built and written by the server.`] : []];
|
|
1420
1655
|
return `# ${answers.name} — notes for agents
|
|
1421
1656
|
|
|
1422
1657
|
A Plitzi space, rendered ${answers.mode === "server" ? "by the server in this project (SSR + RSC)" : "in the browser by a Vite app"}.
|
|
@@ -1428,6 +1663,21 @@ ${where}
|
|
|
1428
1663
|
| --- | --- |
|
|
1429
1664
|
${commands.join("\n")}
|
|
1430
1665
|
|
|
1666
|
+
## This project
|
|
1667
|
+
|
|
1668
|
+
- **Port.** ${port}
|
|
1669
|
+
- **Data with no backend** goes in ${code("public/data/*.json")}, served as it is and read by an ${code("apiContainer")} whose ${code("query")} is ${code("/data/products.json")}${serverData}.
|
|
1670
|
+
${pictures}- **Check a page in text first:** ${code(`${runCommand(answers.packageManager, "check")} -- / --width 1440,390`)} says whether every element is on screen, nothing overflows and the console is clean — a picture only when it says something is wrong: ${code(`${runCommand(answers.packageManager, "shot")} -- / --width 390`)} (add ${code("--scheme dark")}; ${code("--frames 4")} to see what moves; ${code("--compare <url>")} against another site, by section). ${run("visual")} runs the checks as tests.
|
|
1671
|
+
- **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
|
+
|
|
1673
|
+
## Do not read
|
|
1674
|
+
|
|
1675
|
+
${[
|
|
1676
|
+
...generated,
|
|
1677
|
+
`- A large ${code("public/data/*.json")} — ${code("npx plitzi data describe public/data/<file>.json")} prints its fields, their types and one row.`,
|
|
1678
|
+
`- The bundles in ${code("node_modules/@plitzi/*/dist/*.js")}. ${code("npx plitzi explain <name>")} says what an element, a step or a problem's code is; the ${code(".d.ts")} beside them documents the rest — search it, never read it whole.`
|
|
1679
|
+
].join("\n")}
|
|
1680
|
+
|
|
1431
1681
|
## Before anything else
|
|
1432
1682
|
|
|
1433
1683
|
Read ${code(".claude/skills/plitzi-authoring/SKILL.md")} — how a space is written, and the references it links to for
|
|
@@ -1437,10 +1687,12 @@ ${code(".claude/skills/plitzi-cli/SKILL.md")} first: ${code("plitzi add plugin")
|
|
|
1437
1687
|
|
|
1438
1688
|
## The rules that go wrong most
|
|
1439
1689
|
|
|
1440
|
-
- Never write schema/style JSON by hand; author it. A refusal names the fix — fix the declaration.
|
|
1690
|
+
- 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.
|
|
1441
1691
|
- ${zeroWarnings}
|
|
1442
1692
|
- Chrome shared by pages is a layout; a look used twice is a class; a repeated block is a function or a ${code("map")}.
|
|
1443
|
-
- Ids are one namespace for the whole space: name what is referred to
|
|
1693
|
+
- 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
|
+
- 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
|
+
- 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.
|
|
1444
1696
|
- Elements are visible by default. One the logic REVEALS starts hidden (${code("visible")}, or ${code("visible: false")} plus a computed binding) so nothing flashes while loading; one a flag HIDES stays shown while the flag is unset.
|
|
1445
1697
|
- Inside a template a source is spelled in full (${code("apiContainer_stats")}); an attribute only resolves ${code("{{ name|filter }}")}.
|
|
1446
1698
|
- Colours are tokens with light and dark values; times carry an explicit zone and say it.
|
|
@@ -1512,7 +1764,16 @@ var PROJECT_OUTPUTS = [
|
|
|
1512
1764
|
"visual/.results",
|
|
1513
1765
|
"visual/screenshots"
|
|
1514
1766
|
];
|
|
1515
|
-
|
|
1767
|
+
/**
|
|
1768
|
+
* What is the agents' and not the project's: the skills copied in, whose recipes are TypeScript no program of the
|
|
1769
|
+
* project includes — linted with types, they would stop the lint before its first file.
|
|
1770
|
+
*/
|
|
1771
|
+
var AGENT_FILES = ".claude";
|
|
1772
|
+
var prettierignore = (outputs) => `${[
|
|
1773
|
+
"node_modules",
|
|
1774
|
+
AGENT_FILES,
|
|
1775
|
+
...outputs.filter((output) => output !== ".sdk-plugins")
|
|
1776
|
+
].join("\n")}\n`;
|
|
1516
1777
|
/**
|
|
1517
1778
|
* Flat config, and only what earns its place.
|
|
1518
1779
|
*
|
|
@@ -1528,7 +1789,7 @@ import tsEslint from 'typescript-eslint';
|
|
|
1528
1789
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
1529
1790
|
|
|
1530
1791
|
export default tsEslint.config(
|
|
1531
|
-
{ ignores: [${outputs.map((output) => `'${output}'`).join(", ")}] },
|
|
1792
|
+
{ ignores: [${[AGENT_FILES, ...outputs].map((output) => `'${output}'`).join(", ")}] },
|
|
1532
1793
|
js.configs.recommended,
|
|
1533
1794
|
{
|
|
1534
1795
|
/**
|
|
@@ -1608,19 +1869,44 @@ const plugins = Object.fromEntries(
|
|
|
1608
1869
|
* no error anywhere, which is a long afternoon.
|
|
1609
1870
|
*/
|
|
1610
1871
|
const pluginNames = Object.keys(plugins);`;
|
|
1611
|
-
|
|
1872
|
+
/**
|
|
1873
|
+
* Which port the server takes. `PORT` set: that one, and a clear error if something else has it. Not set, while
|
|
1874
|
+
* developing: 8080, or the next free one — so a second project, or anything else on 8080, does not stop this one.
|
|
1875
|
+
*/
|
|
1876
|
+
var PORT_SNIPPET = `// Loopback unless told otherwise: a container publishes a port only from an address it listens on (\`HOST=0.0.0.0\`).
|
|
1877
|
+
const HOST = process.env.HOST ?? '127.0.0.1';
|
|
1878
|
+
// \`PORT\` set: that port. Not set, while developing: 8080 or the next free one, written down below for the scripts.
|
|
1879
|
+
const PORT = process.env.PORT
|
|
1880
|
+
? Number(process.env.PORT)
|
|
1881
|
+
: process.env.NODE_ENV === 'production'
|
|
1882
|
+
? 8080
|
|
1883
|
+
: await freePort(8080, HOST);`;
|
|
1884
|
+
/** Where the server says it is: the port it took, for `npm run shot` and the visual tests to find. */
|
|
1885
|
+
var LISTEN_SNIPPET = `server.listen(PORT, HOST);
|
|
1886
|
+
mkdirSync(path.join(PROJECT_ROOT, '.plitzi'), { recursive: true });
|
|
1887
|
+
writeFileSync(
|
|
1888
|
+
path.join(PROJECT_ROOT, '.plitzi/dev-server.json'),
|
|
1889
|
+
\`\${JSON.stringify({ name: SERVER_NAME, port: PORT, url: \`http://127.0.0.1:\${PORT}\` }, null, 2)}\\n\`
|
|
1890
|
+
);
|
|
1891
|
+
console.log(\`pages on http://127.0.0.1:\${PORT}/\`);`;
|
|
1892
|
+
var localMain = () => `import { mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
1612
1893
|
import path from 'node:path';
|
|
1613
1894
|
|
|
1614
|
-
import {
|
|
1895
|
+
import {
|
|
1896
|
+
closeOnSignals,
|
|
1897
|
+
consoleLogger,
|
|
1898
|
+
createJsonAdapters,
|
|
1899
|
+
createServer,
|
|
1900
|
+
freePort,
|
|
1901
|
+
loadFunctions
|
|
1902
|
+
} from '@plitzi/sdk-server';
|
|
1615
1903
|
|
|
1616
1904
|
import { authorSpace } from '@plitzi/sdk-authoring';
|
|
1617
1905
|
|
|
1618
1906
|
import { declarations } from './plugins/declarations.ts';
|
|
1619
1907
|
import { space } from './space.ts';
|
|
1620
1908
|
|
|
1621
|
-
|
|
1622
|
-
// Loopback unless told otherwise: a container publishes a port only from an address it listens on (\`HOST=0.0.0.0\`).
|
|
1623
|
-
const HOST = process.env.HOST ?? '127.0.0.1';
|
|
1909
|
+
${PORT_SNIPPET}
|
|
1624
1910
|
|
|
1625
1911
|
/**
|
|
1626
1912
|
* The space, held in this project.
|
|
@@ -1653,20 +1939,26 @@ const functions = await loadFunctions(new URL('../functions/', import.meta.url))
|
|
|
1653
1939
|
* page server makes. A real deployment swaps this for adapters onto its own database, or for
|
|
1654
1940
|
* \`createCloudAdapters\` to read the live space out of Plitzi — the server never learns the difference.
|
|
1655
1941
|
*/
|
|
1942
|
+
// What \`/health\` answers with, and \`.plitzi/dev-server.json\` records: how a tool knows it reached THIS project.
|
|
1943
|
+
const SERVER_NAME = schema.definition.permanentUrl;
|
|
1656
1944
|
const server = createServer({
|
|
1657
1945
|
port: PORT,
|
|
1658
1946
|
devMode: process.env.NODE_ENV !== 'production',
|
|
1947
|
+
health: { name: SERVER_NAME },
|
|
1659
1948
|
adapters: createJsonAdapters({
|
|
1660
1949
|
offlineData,
|
|
1661
1950
|
deployment: { spaceId: 1, environment: 'main', revision: 0, pluginNames }
|
|
1662
1951
|
}),
|
|
1663
1952
|
plugins,
|
|
1953
|
+
// \`public/\` served as it is: the data an apiContainer reads (\`/data/home.json\`), images, a favicon.
|
|
1954
|
+
publicDir: path.join(PROJECT_ROOT, 'public'),
|
|
1664
1955
|
functions: { native: functions },
|
|
1956
|
+
// What went wrong and nothing else: \`npm start -- --verbose\` adds a line for every request.
|
|
1957
|
+
logLevel: process.argv.includes('--verbose') ? 'info' : 'warn',
|
|
1665
1958
|
logger: consoleLogger
|
|
1666
1959
|
});
|
|
1667
1960
|
|
|
1668
|
-
|
|
1669
|
-
console.log(\`pages on http://127.0.0.1:\${PORT}/\`);
|
|
1961
|
+
${LISTEN_SNIPPET}
|
|
1670
1962
|
|
|
1671
1963
|
/**
|
|
1672
1964
|
* A deploy, a restart or ^C closes the server instead of dropping it: requests in flight are answered, and once the
|
|
@@ -1675,14 +1967,19 @@ console.log(\`pages on http://127.0.0.1:\${PORT}/\`);
|
|
|
1675
1967
|
*/
|
|
1676
1968
|
closeOnSignals(server);
|
|
1677
1969
|
`;
|
|
1678
|
-
var cloudMain = () => `import { readdirSync } from 'node:fs';
|
|
1970
|
+
var cloudMain = (name) => `import { mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
1679
1971
|
import path from 'node:path';
|
|
1680
1972
|
|
|
1681
|
-
import {
|
|
1973
|
+
import {
|
|
1974
|
+
closeOnSignals,
|
|
1975
|
+
consoleLogger,
|
|
1976
|
+
createCloudAdapters,
|
|
1977
|
+
createServer,
|
|
1978
|
+
freePort,
|
|
1979
|
+
loadFunctions
|
|
1980
|
+
} from '@plitzi/sdk-server';
|
|
1682
1981
|
|
|
1683
|
-
|
|
1684
|
-
// Loopback unless told otherwise: a container publishes a port only from an address it listens on (\`HOST=0.0.0.0\`).
|
|
1685
|
-
const HOST = process.env.HOST ?? '127.0.0.1';
|
|
1982
|
+
${PORT_SNIPPET}
|
|
1686
1983
|
|
|
1687
1984
|
${PLUGINS}
|
|
1688
1985
|
|
|
@@ -1718,9 +2015,12 @@ const functions = await loadFunctions(new URL('../functions/', import.meta.url))
|
|
|
1718
2015
|
* on every request; a published environment with no \`revision\` serves the latest and releases itself; with a
|
|
1719
2016
|
* \`revision\` it serves exactly that version, for a deployment that rolls forward on its own schedule.
|
|
1720
2017
|
*/
|
|
2018
|
+
// What \`/health\` answers with, and \`.plitzi/dev-server.json\` records: how a tool knows it reached THIS project.
|
|
2019
|
+
const SERVER_NAME = ${JSON.stringify(name)};
|
|
1721
2020
|
const server = createServer({
|
|
1722
2021
|
port: PORT,
|
|
1723
2022
|
devMode: process.env.NODE_ENV !== 'production',
|
|
2023
|
+
health: { name: SERVER_NAME },
|
|
1724
2024
|
adapters: createCloudAdapters({
|
|
1725
2025
|
webKey: HOST_KEY,
|
|
1726
2026
|
...(process.env.PLITZI_SERVER_URL ? { serverUrl: process.env.PLITZI_SERVER_URL } : {}),
|
|
@@ -1729,12 +2029,15 @@ const server = createServer({
|
|
|
1729
2029
|
deployment: { pluginNames }
|
|
1730
2030
|
}),
|
|
1731
2031
|
plugins,
|
|
2032
|
+
// \`public/\` served as it is: the data an apiContainer reads (\`/data/home.json\`), images, a favicon.
|
|
2033
|
+
publicDir: path.join(PROJECT_ROOT, 'public'),
|
|
1732
2034
|
functions: { native: functions },
|
|
2035
|
+
// What went wrong and nothing else: \`npm start -- --verbose\` adds a line for every request.
|
|
2036
|
+
logLevel: process.argv.includes('--verbose') ? 'info' : 'warn',
|
|
1733
2037
|
logger: consoleLogger
|
|
1734
2038
|
});
|
|
1735
2039
|
|
|
1736
|
-
|
|
1737
|
-
console.log(\`pages on http://127.0.0.1:\${PORT}/\`);
|
|
2040
|
+
${LISTEN_SNIPPET}
|
|
1738
2041
|
|
|
1739
2042
|
/**
|
|
1740
2043
|
* A deploy, a restart or ^C closes the server instead of dropping it: requests in flight are answered, and once the
|
|
@@ -1743,7 +2046,10 @@ console.log(\`pages on http://127.0.0.1:\${PORT}/\`);
|
|
|
1743
2046
|
*/
|
|
1744
2047
|
closeOnSignals(server);
|
|
1745
2048
|
`;
|
|
1746
|
-
var serverFiles = (answers) => ({
|
|
2049
|
+
var serverFiles = (answers) => ({
|
|
2050
|
+
"src/main.ts": answers.source === "cloud" ? cloudMain(answers.name) : localMain(),
|
|
2051
|
+
"public/data/.gitkeep": ""
|
|
2052
|
+
});
|
|
1747
2053
|
//#endregion
|
|
1748
2054
|
//#region src/scaffold/skills.ts
|
|
1749
2055
|
/**
|
|
@@ -1759,19 +2065,43 @@ var serverFiles = (answers) => ({ "src/main.ts": answers.source === "cloud" ? cl
|
|
|
1759
2065
|
* other one Plitzi ships, and it is deliberately left out: it drives the `plitzi_render` MCP tool, so in a project with
|
|
1760
2066
|
* no MCP connection it would be an instruction to use something that is not there.
|
|
1761
2067
|
*/
|
|
1762
|
-
var
|
|
2068
|
+
var cliRequire = createRequire(import.meta.url);
|
|
2069
|
+
var SKILL_NAMES = ["plitzi-authoring", "plitzi-cli"];
|
|
2070
|
+
/** The package each skill is published in, and copied from. */
|
|
1763
2071
|
var SKILLS = {
|
|
1764
2072
|
"plitzi-authoring": "@plitzi/sdk-authoring",
|
|
1765
2073
|
"plitzi-cli": "@plitzi/cli"
|
|
1766
2074
|
};
|
|
1767
2075
|
/**
|
|
2076
|
+
* How a package is found: the project's own install first — the SDK it renders with is the one its skill has to match —
|
|
2077
|
+
* then this CLI's. `@plitzi/cli` is usually run with npx and installed nowhere in the project, so it falls through.
|
|
2078
|
+
*/
|
|
2079
|
+
var resolversFor = (root) => root ? [createRequire(join(root, "package.json")), cliRequire] : [cliRequire];
|
|
2080
|
+
/**
|
|
1768
2081
|
* Where a skill file sits, resolved through the package's own export map rather than guessed from a path.
|
|
1769
2082
|
*
|
|
1770
2083
|
* `<package>/skills/*` is an export of the packages that ship one, which is what makes this work under Yarn PnP —
|
|
1771
2084
|
* there is no `node_modules` directory to walk there, and a file the package does not export is unreachable
|
|
1772
2085
|
* however present it is on disk.
|
|
1773
2086
|
*/
|
|
1774
|
-
var
|
|
2087
|
+
var resolveFrom = (resolvers, request) => {
|
|
2088
|
+
for (const resolver of resolvers) try {
|
|
2089
|
+
return resolver.resolve(request);
|
|
2090
|
+
} catch {
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
/** The version of the package a skill came from, written into its frontmatter — `version: 0.38.0`. */
|
|
2095
|
+
var versionOf = (resolvers, packageName) => {
|
|
2096
|
+
const manifest = resolveFrom(resolvers, `${packageName}/package.json`);
|
|
2097
|
+
if (!manifest) return;
|
|
2098
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf-8"));
|
|
2099
|
+
return e$1(parsed) && typeof parsed.version === "string" ? parsed.version : void 0;
|
|
2100
|
+
};
|
|
2101
|
+
/** The frontmatter's `version:`, said once: after the skill's `name:`, replacing any already there. */
|
|
2102
|
+
var stampVersion = (skill, version) => skill.replace(/^version: .*\n/m, "").replace(/^(name: .*)$/m, `$1\nversion: ${version}`);
|
|
2103
|
+
/** The version a copied `SKILL.md` says it came from. */
|
|
2104
|
+
var skillVersion = (skill) => /^version: (.+)$/m.exec(skill)?.[1]?.trim();
|
|
1775
2105
|
/**
|
|
1776
2106
|
* Every file under a skill's folder, by its path inside it.
|
|
1777
2107
|
*
|
|
@@ -1779,19 +2109,23 @@ var skillPath = (packageName, skill) => require.resolve(`${packageName}/skills/$
|
|
|
1779
2109
|
* sit beside it and are opened when the task needs them. Copying \`SKILL.md\` alone left every one of those links
|
|
1780
2110
|
* pointing at nothing.
|
|
1781
2111
|
*/
|
|
1782
|
-
var filesUnder = (root, folder = root) => readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
|
|
2112
|
+
var filesUnder$1 = (root, folder = root) => readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
|
|
1783
2113
|
const path = join(folder, entry.name);
|
|
1784
|
-
if (entry.isDirectory()) return filesUnder(root, path);
|
|
2114
|
+
if (entry.isDirectory()) return filesUnder$1(root, path);
|
|
1785
2115
|
return entry.isFile() ? [[relative(root, path).split(sep).join("/"), readFileSync(path, "utf-8")]] : [];
|
|
1786
2116
|
});
|
|
1787
|
-
/**
|
|
1788
|
-
|
|
2117
|
+
/**
|
|
2118
|
+
* The skills a project carries: both for a project that renders a space, the CLI's alone for a plugin package. With
|
|
2119
|
+
* `root`, from the packages that project installed — what `plitzi skills update` brings it up to.
|
|
2120
|
+
*/
|
|
2121
|
+
var skillFiles = (names = SKILL_NAMES, root) => {
|
|
2122
|
+
const resolvers = resolversFor(root);
|
|
1789
2123
|
const files = {};
|
|
1790
|
-
for (const name of names)
|
|
1791
|
-
const
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
2124
|
+
for (const name of names) {
|
|
2125
|
+
const entry = resolveFrom(resolvers, `${SKILLS[name]}/skills/${name}/SKILL.md`);
|
|
2126
|
+
if (!entry) continue;
|
|
2127
|
+
const version = versionOf(resolvers, SKILLS[name]);
|
|
2128
|
+
for (const [path, content] of filesUnder$1(dirname(entry))) files[`.claude/skills/${name}/${path}`] = path === "SKILL.md" && version ? stampVersion(content, version) : content;
|
|
1795
2129
|
}
|
|
1796
2130
|
return files;
|
|
1797
2131
|
};
|
|
@@ -1815,23 +2149,105 @@ var skillFiles = (names = ["plitzi-authoring", "plitzi-cli"]) => {
|
|
|
1815
2149
|
* the space has to go somewhere else: imported into Plitzi, handed to another server, or checked into a
|
|
1816
2150
|
* repository with no TypeScript in it.
|
|
1817
2151
|
*/
|
|
1818
|
-
var authorScript = () => `import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2152
|
+
var authorScript = () => `import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2153
|
+
import { createRequire } from 'node:module';
|
|
1819
2154
|
|
|
1820
|
-
import { authorSpace } from '@plitzi/sdk-authoring';
|
|
2155
|
+
import { authorSpace, planFixes, refusalOf, SpaceRefusedError } from '@plitzi/sdk-authoring';
|
|
1821
2156
|
|
|
1822
2157
|
import { declarations } from './plugins/declarations.ts';
|
|
1823
2158
|
import { space } from './space.ts';
|
|
1824
2159
|
|
|
1825
|
-
|
|
2160
|
+
// \`--json\` answers in one object, for a tool or an agent; otherwise one line on success, the problems on failure.
|
|
2161
|
+
const json = process.argv.includes('--json');
|
|
1826
2162
|
|
|
1827
|
-
|
|
1828
|
-
|
|
2163
|
+
/** How many of the problems said have one fix \`plitzi fix\` can write in this source — and the line that says so. */
|
|
2164
|
+
const fixableHint = (): string | undefined => {
|
|
2165
|
+
let count: number;
|
|
2166
|
+
try {
|
|
2167
|
+
count = planFixes(space, { plugins: declarations }).fixes.length;
|
|
2168
|
+
} catch {
|
|
2169
|
+
return undefined;
|
|
2170
|
+
}
|
|
1829
2171
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
}
|
|
2172
|
+
return count === 0 ? undefined : \`[fix] \${count} of these have one fix: npx plitzi fix shows it in your source, --write writes it\`;
|
|
2173
|
+
};
|
|
1833
2174
|
|
|
1834
|
-
|
|
2175
|
+
/**
|
|
2176
|
+
* The skill an agent reads was copied from the SDK at one version: a newer SDK has what the older skill never taught.
|
|
2177
|
+
* Both versions, when they differ — \`npx @plitzi/cli skills update\` brings the skill up to the SDK.
|
|
2178
|
+
*/
|
|
2179
|
+
const outdatedSkill = (): { skill: string; sdk: string } | undefined => {
|
|
2180
|
+
let skill: string | undefined;
|
|
2181
|
+
try {
|
|
2182
|
+
skill = /^version: (.+)$/m.exec(readFileSync('.claude/skills/plitzi-authoring/SKILL.md', 'utf-8'))?.[1]?.trim();
|
|
2183
|
+
} catch {
|
|
2184
|
+
return undefined;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
const manifest: unknown = JSON.parse(
|
|
2188
|
+
readFileSync(createRequire(import.meta.url).resolve('@plitzi/sdk-authoring/package.json'), 'utf-8')
|
|
2189
|
+
);
|
|
2190
|
+
const sdk =
|
|
2191
|
+
typeof manifest === 'object' && manifest !== null && 'version' in manifest && typeof manifest.version === 'string'
|
|
2192
|
+
? manifest.version
|
|
2193
|
+
: undefined;
|
|
2194
|
+
|
|
2195
|
+
return skill && sdk && skill !== sdk ? { skill, sdk } : undefined;
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2198
|
+
try {
|
|
2199
|
+
const { schema, style, warnings, suggestions } = authorSpace(space, { plugins: declarations });
|
|
2200
|
+
mkdirSync('space', { recursive: true });
|
|
2201
|
+
writeFileSync('space/offline-data.json', \`\${JSON.stringify({ schema, style }, null, 2)}\\n\`);
|
|
2202
|
+
|
|
2203
|
+
const outdated = outdatedSkill();
|
|
2204
|
+
if (json) {
|
|
2205
|
+
console.log(
|
|
2206
|
+
JSON.stringify({ ok: true, pages: schema.pages.length, warnings, suggestions, ...(outdated ? { outdated } : {}) })
|
|
2207
|
+
);
|
|
2208
|
+
} else {
|
|
2209
|
+
for (const warning of warnings) {
|
|
2210
|
+
console.warn(\`[author] \${warning.code} · \${warning.message}\`);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
const hint = warnings.length > 0 ? fixableHint() : undefined;
|
|
2214
|
+
if (hint) {
|
|
2215
|
+
console.warn(hint);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// Not problems: a shorter way to the same page, the one that saves the most elements first.
|
|
2219
|
+
for (const suggestion of suggestions) {
|
|
2220
|
+
const at = suggestion.at ? \` · \${suggestion.at}\` : '';
|
|
2221
|
+
console.warn(\`[suggest] \${suggestion.code} · \${suggestion.message} (saves \${suggestion.saves})\${at}\`);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
if (outdated) {
|
|
2225
|
+
console.warn(
|
|
2226
|
+
\`[skills] the authoring skill is \${outdated.skill} and @plitzi/sdk-authoring is \${outdated.sdk}: npx @plitzi/cli skills update\`
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
console.log(
|
|
2231
|
+
\`ok · \${schema.pages.length} pages · \${warnings.length} warnings · \${suggestions.length} suggestions · space/offline-data.json\`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
// The message is the whole report — every problem, where it was written and what to change. The stack would only
|
|
2236
|
+
// point inside @plitzi/sdk-authoring.
|
|
2237
|
+
const refusals = error instanceof SpaceRefusedError ? error.refusals : undefined;
|
|
2238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2239
|
+
if (json) {
|
|
2240
|
+
console.log(JSON.stringify({ ok: false, refusals: refusals ?? [{ place: '', ...refusalOf(error) }] }));
|
|
2241
|
+
} else {
|
|
2242
|
+
console.error(message);
|
|
2243
|
+
const hint = fixableHint();
|
|
2244
|
+
if (hint) {
|
|
2245
|
+
console.error(hint);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
process.exitCode = 1;
|
|
2250
|
+
}
|
|
1835
2251
|
`;
|
|
1836
2252
|
/** The numbers the example shows: in a data file where one is served, on the element where none is. */
|
|
1837
2253
|
var STATS = {
|
|
@@ -1879,13 +2295,25 @@ var pluginHost = ({ mode }) => mode === "client" ? {
|
|
|
1879
2295
|
...STATS
|
|
1880
2296
|
}
|
|
1881
2297
|
};
|
|
1882
|
-
var spaceFiles = (answers) =>
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2298
|
+
var spaceFiles = (answers) => {
|
|
2299
|
+
if (answers.source === "cloud") return {};
|
|
2300
|
+
if (answers.template === "catalog") return {
|
|
2301
|
+
...catalogTemplateFiles({ name: answers.name }),
|
|
2302
|
+
"src/author.ts": authorScript()
|
|
2303
|
+
};
|
|
2304
|
+
if (answers.template === "blank") return {
|
|
2305
|
+
"src/space.ts": emptySpaceSource({ name: answers.name }),
|
|
2306
|
+
"src/author.ts": authorScript(),
|
|
2307
|
+
"public/data/.gitkeep": ""
|
|
2308
|
+
};
|
|
2309
|
+
return {
|
|
2310
|
+
"src/space.ts": blankSpaceSource({
|
|
2311
|
+
name: answers.name,
|
|
2312
|
+
plugin: pluginHost(answers)
|
|
2313
|
+
}),
|
|
2314
|
+
"src/author.ts": authorScript(),
|
|
2315
|
+
...answers.mode === "client" ? { "public/data/stats.json": `${JSON.stringify(STATS, null, 2)}\n` } : {}
|
|
2316
|
+
};
|
|
1889
2317
|
};
|
|
1890
2318
|
//#endregion
|
|
1891
2319
|
//#region src/scaffold/visual.ts
|
|
@@ -1896,9 +2324,30 @@ var spaceFiles = (answers) => answers.source === "cloud" ? {} : {
|
|
|
1896
2324
|
* a space renders through a stack — schema, style, plugins, hydration — where a mistake shows up as a blank page
|
|
1897
2325
|
* rather than as an exception, and nobody writes the first test for a project that already looks fine.
|
|
1898
2326
|
*/
|
|
1899
|
-
var
|
|
1900
|
-
|
|
1901
|
-
|
|
2327
|
+
var RECORDED = `/** What \`npm start\` wrote down when it took a port: the port, and the name its \`/health\` answers with. */
|
|
2328
|
+
const recorded = (): { port?: number; name?: string } => {
|
|
2329
|
+
try {
|
|
2330
|
+
const value: unknown = JSON.parse(readFileSync('.plitzi/dev-server.json', 'utf8'));
|
|
2331
|
+
if (typeof value !== 'object' || value === null) {
|
|
2332
|
+
return {};
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
return {
|
|
2336
|
+
...('port' in value && typeof value.port === 'number' ? { port: value.port } : {}),
|
|
2337
|
+
...('name' in value && typeof value.name === 'string' ? { name: value.name } : {})
|
|
2338
|
+
};
|
|
2339
|
+
} catch {
|
|
2340
|
+
return {};
|
|
2341
|
+
}
|
|
2342
|
+
};`;
|
|
2343
|
+
var playwrightConfig$1 = ({ mode, packageManager }) => `import { readFileSync } from 'node:fs';
|
|
2344
|
+
|
|
2345
|
+
import { defineConfig } from '@playwright/test';
|
|
2346
|
+
|
|
2347
|
+
${RECORDED}
|
|
2348
|
+
|
|
2349
|
+
// \`PORT\` when set; otherwise the port \`npm start\` took; otherwise the default.
|
|
2350
|
+
const PORT = Number(process.env.PORT ?? recorded().port ?? ${mode === "server" ? "8080" : "5173"});
|
|
1902
2351
|
|
|
1903
2352
|
export default defineConfig({
|
|
1904
2353
|
testDir: './visual',
|
|
@@ -1909,6 +2358,7 @@ export default defineConfig({
|
|
|
1909
2358
|
webServer: {
|
|
1910
2359
|
command: '${runCommand(packageManager, "start")}',
|
|
1911
2360
|
url: \`http://127.0.0.1:\${PORT}\`,
|
|
2361
|
+
env: { PORT: String(PORT) },
|
|
1912
2362
|
reuseExistingServer: true,
|
|
1913
2363
|
timeout: 120_000
|
|
1914
2364
|
}
|
|
@@ -1974,53 +2424,8 @@ test('renders the space without errors', async ({ page }) => {
|
|
|
1974
2424
|
expect(errors).toEqual([]);
|
|
1975
2425
|
});
|
|
1976
2426
|
`;
|
|
1977
|
-
var shotScript = ({ mode }) => `import { mkdir } from 'node:fs/promises';
|
|
1978
|
-
import { dirname } from 'node:path';
|
|
1979
|
-
|
|
1980
|
-
import { chromium } from '@playwright/test';
|
|
1981
|
-
|
|
1982
|
-
/**
|
|
1983
|
-
* A screenshot of one page, to look at a change without writing a test for it.
|
|
1984
|
-
*
|
|
1985
|
-
* npm run shot -- /about --width 390 --scheme dark
|
|
1986
|
-
* npm run shot -- / --out shots/home.png --height 900
|
|
1987
|
-
*
|
|
1988
|
-
* The whole page, not one screen of it. The dev server has to be running (npm start).
|
|
1989
|
-
*/
|
|
1990
|
-
const PORT = ${mode === "server" ? "8080" : "5173"};
|
|
1991
|
-
|
|
1992
|
-
const args = process.argv.slice(2);
|
|
1993
|
-
const option = (name: string, fallback: string): string => {
|
|
1994
|
-
const index = args.indexOf('--' + name);
|
|
1995
|
-
|
|
1996
|
-
return index === -1 ? fallback : (args[index + 1] ?? fallback);
|
|
1997
|
-
};
|
|
1998
|
-
|
|
1999
|
-
const path = args.find(arg => arg.startsWith('/')) ?? '/';
|
|
2000
|
-
const width = Number(option('width', '1280'));
|
|
2001
|
-
const height = Number(option('height', '800'));
|
|
2002
|
-
const scheme = option('scheme', 'light') === 'dark' ? 'dark' : 'light';
|
|
2003
|
-
const name = path === '/' ? 'home' : path.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '');
|
|
2004
|
-
const out = option('out', 'visual/.shots/' + name + '-' + width + '-' + scheme + '.png');
|
|
2005
|
-
|
|
2006
|
-
const browser = await chromium.launch();
|
|
2007
|
-
try {
|
|
2008
|
-
const page = await browser.newPage({ viewport: { width, height }, colorScheme: scheme });
|
|
2009
|
-
const response = await page.goto('http://127.0.0.1:' + PORT + path, { waitUntil: 'networkidle' }).catch(() => null);
|
|
2010
|
-
if (!response) {
|
|
2011
|
-
throw new Error('Nothing answers on port ' + PORT + '. Start the project first: npm start');
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
await mkdir(dirname(out), { recursive: true });
|
|
2015
|
-
await page.screenshot({ path: out, fullPage: true });
|
|
2016
|
-
console.log(out);
|
|
2017
|
-
} finally {
|
|
2018
|
-
await browser.close();
|
|
2019
|
-
}
|
|
2020
|
-
`;
|
|
2021
2427
|
var visualFiles$1 = (answers) => ({
|
|
2022
2428
|
"playwright.config.ts": playwrightConfig$1(answers),
|
|
2023
|
-
"scripts/shot.ts": shotScript(answers),
|
|
2024
2429
|
"visual/home.spec.ts": answers.source === "local" ? authoredSpec() : documentSpec()
|
|
2025
2430
|
});
|
|
2026
2431
|
//#endregion
|
|
@@ -2251,7 +2656,7 @@ var packageJson = ({ packageName, base }, { packageManager, owner }) => `${JSON.
|
|
|
2251
2656
|
},
|
|
2252
2657
|
scripts: {
|
|
2253
2658
|
start: "vite",
|
|
2254
|
-
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
2659
|
+
typecheck: "tsc -p tsconfig.json --noEmit --pretty false",
|
|
2255
2660
|
lint: "eslint .",
|
|
2256
2661
|
format: "prettier --write .",
|
|
2257
2662
|
visual: "playwright test"
|
|
@@ -2435,6 +2840,497 @@ var previewFiles = ([main, ...others]) => ({
|
|
|
2435
2840
|
})
|
|
2436
2841
|
});
|
|
2437
2842
|
//#endregion
|
|
2843
|
+
//#region ../../packages/sdk-shared/dist/helpers/scroll.mjs
|
|
2844
|
+
var e = ["smooth", "auto"];
|
|
2845
|
+
var t = [
|
|
2846
|
+
"start",
|
|
2847
|
+
"center",
|
|
2848
|
+
"end",
|
|
2849
|
+
"nearest"
|
|
2850
|
+
];
|
|
2851
|
+
//#endregion
|
|
2852
|
+
//#region ../../packages/sdk-shared/dist/authoring/elementCallbacks.mjs
|
|
2853
|
+
var r$2 = {
|
|
2854
|
+
type: "select",
|
|
2855
|
+
description: "How it moves: \"smooth\" glides there, \"auto\" jumps.",
|
|
2856
|
+
default: "smooth",
|
|
2857
|
+
options: [...e]
|
|
2858
|
+
};
|
|
2859
|
+
var a$1 = {
|
|
2860
|
+
setState: {
|
|
2861
|
+
title: "Update Element (set attribute / state)",
|
|
2862
|
+
type: "callback",
|
|
2863
|
+
strictParams: !0,
|
|
2864
|
+
params: {
|
|
2865
|
+
category: {
|
|
2866
|
+
type: "select",
|
|
2867
|
+
description: "What to change on the element: \"attribute\" sets one of its props (the common case — e.g. content, disabled); \"state\" sets element state (visibility or a style selector). REQUIRED — always set it.",
|
|
2868
|
+
default: "attribute",
|
|
2869
|
+
options: ["attribute", "state"],
|
|
2870
|
+
required: !0
|
|
2871
|
+
},
|
|
2872
|
+
key: {
|
|
2873
|
+
type: "text",
|
|
2874
|
+
description: "The field to set. When category=\"attribute\", an attribute/prop key of THIS element (e.g. \"content\", \"disabled\"). When category=\"state\", \"visibility\" or \"styleSelectors.<selector>\". REQUIRED.",
|
|
2875
|
+
required: !0
|
|
2876
|
+
},
|
|
2877
|
+
value: {
|
|
2878
|
+
type: "scalar",
|
|
2879
|
+
description: "The value to set — its type follows the target attribute: a real boolean (true/false) for a boolean attribute, a number for a numeric one, otherwise a string (the strings \"true\"/\"false\" also coerce). There is NO separate `type` param (that belongs to the global state setState, not this one). REQUIRED.",
|
|
2880
|
+
when: (e) => !!e.category,
|
|
2881
|
+
required: !0
|
|
2882
|
+
},
|
|
2883
|
+
revertOnFinish: {
|
|
2884
|
+
type: "boolean",
|
|
2885
|
+
description: "When true, this change is UNDONE automatically when the whole flow finishes. Use it for a TEMPORARY change (a \"loading…\" label, disabling a button while it works) instead of adding manual restore steps at the end of the flow.",
|
|
2886
|
+
default: !1,
|
|
2887
|
+
label: "Revert changes after interaction"
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
},
|
|
2891
|
+
toggleState: {
|
|
2892
|
+
title: "Toggle Element (flip attribute / state)",
|
|
2893
|
+
type: "callback",
|
|
2894
|
+
strictParams: !0,
|
|
2895
|
+
params: {
|
|
2896
|
+
category: {
|
|
2897
|
+
type: "select",
|
|
2898
|
+
description: "What to flip on the element: \"attribute\" flips one of its props (e.g. disabled); \"state\" flips element state (\"visibility\", or \"styleSelectors.<selector>\" for an expanded/collapsed look). REQUIRED.",
|
|
2899
|
+
default: "state",
|
|
2900
|
+
options: ["attribute", "state"],
|
|
2901
|
+
required: !0
|
|
2902
|
+
},
|
|
2903
|
+
key: {
|
|
2904
|
+
type: "text",
|
|
2905
|
+
description: "The field to flip. When category=\"attribute\", an attribute/prop key of THIS element. When category=\"state\", \"visibility\" or \"styleSelectors.<selector>\". Anything not already true counts as false, so a field that has never been set flips ON first. REQUIRED.",
|
|
2906
|
+
required: !0
|
|
2907
|
+
},
|
|
2908
|
+
revertOnFinish: {
|
|
2909
|
+
type: "boolean",
|
|
2910
|
+
description: "When true, the flip is UNDONE automatically when the whole flow finishes — a change that lasts only as long as the flow does.",
|
|
2911
|
+
default: !1,
|
|
2912
|
+
label: "Revert changes after interaction"
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
},
|
|
2916
|
+
scrollBy: {
|
|
2917
|
+
title: "Scroll By",
|
|
2918
|
+
type: "callback",
|
|
2919
|
+
strictParams: !0,
|
|
2920
|
+
params: {
|
|
2921
|
+
x: {
|
|
2922
|
+
type: "text",
|
|
2923
|
+
description: "How far across: pixels (\"240\") or a share of what the box shows (\"80%\"); negative goes back.",
|
|
2924
|
+
default: ""
|
|
2925
|
+
},
|
|
2926
|
+
y: {
|
|
2927
|
+
type: "text",
|
|
2928
|
+
description: "How far down, the same way; negative goes up.",
|
|
2929
|
+
default: ""
|
|
2930
|
+
},
|
|
2931
|
+
behavior: r$2
|
|
2932
|
+
}
|
|
2933
|
+
},
|
|
2934
|
+
scrollTo: {
|
|
2935
|
+
title: "Scroll To",
|
|
2936
|
+
type: "callback",
|
|
2937
|
+
strictParams: !0,
|
|
2938
|
+
params: {
|
|
2939
|
+
x: {
|
|
2940
|
+
type: "text",
|
|
2941
|
+
description: "Where across: \"start\", \"end\", pixels from the start, or a share of the way (\"50%\"). Empty stays.",
|
|
2942
|
+
default: ""
|
|
2943
|
+
},
|
|
2944
|
+
y: {
|
|
2945
|
+
type: "text",
|
|
2946
|
+
description: "Where down, the same way. Empty stays.",
|
|
2947
|
+
default: ""
|
|
2948
|
+
},
|
|
2949
|
+
behavior: r$2
|
|
2950
|
+
}
|
|
2951
|
+
},
|
|
2952
|
+
scrollIntoView: {
|
|
2953
|
+
title: "Scroll Into View",
|
|
2954
|
+
type: "callback",
|
|
2955
|
+
strictParams: !0,
|
|
2956
|
+
params: {
|
|
2957
|
+
block: {
|
|
2958
|
+
type: "select",
|
|
2959
|
+
description: "Where the element lands, top to bottom.",
|
|
2960
|
+
default: "start",
|
|
2961
|
+
options: [...t]
|
|
2962
|
+
},
|
|
2963
|
+
inline: {
|
|
2964
|
+
type: "select",
|
|
2965
|
+
description: "Where it lands, side to side.",
|
|
2966
|
+
default: "nearest",
|
|
2967
|
+
options: [...t]
|
|
2968
|
+
},
|
|
2969
|
+
behavior: r$2
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
//#endregion
|
|
2974
|
+
//#region src/scaffold/pluginPackage/shape.ts
|
|
2975
|
+
/**
|
|
2976
|
+
* An element written in its final shape, from what `plitzi add plugin` was told: its props with their types and
|
|
2977
|
+
* defaults, the events it fires and what each hands a flow, the actions it answers to, and whether it draws anything.
|
|
2978
|
+
*
|
|
2979
|
+
* The counter `add plugin` writes without them shows the three ways an element talks to a space, and is rewritten on
|
|
2980
|
+
* day one; told the shape, it writes that shape — four files with nothing to delete.
|
|
2981
|
+
*/
|
|
2982
|
+
var PROP_TYPES = [
|
|
2983
|
+
"string",
|
|
2984
|
+
"number",
|
|
2985
|
+
"boolean"
|
|
2986
|
+
];
|
|
2987
|
+
var IDENTIFIER = /^[a-z][A-Za-z0-9]*$/;
|
|
2988
|
+
var EVENT = /^on[A-Z][A-Za-z0-9]*$/;
|
|
2989
|
+
/** What the runtime hands every element already, so a prop of that name would never be the author's. */
|
|
2990
|
+
var RESERVED_PROPS = [
|
|
2991
|
+
"className",
|
|
2992
|
+
"children",
|
|
2993
|
+
"ref",
|
|
2994
|
+
"key",
|
|
2995
|
+
"id",
|
|
2996
|
+
"style"
|
|
2997
|
+
];
|
|
2998
|
+
var parseProp = (flag) => {
|
|
2999
|
+
const match = /^([^:=]+):([^=]+)(?:=(.*))?$/.exec(flag);
|
|
3000
|
+
if (!match) return `--prop ${flag}: write it name:type or name:type=default — interval:number=5000, paused:boolean, label:string=Hi.`;
|
|
3001
|
+
const [, name, type] = match;
|
|
3002
|
+
const given = match.at(3);
|
|
3003
|
+
if (!IDENTIFIER.test(name) || RESERVED_PROPS.includes(name)) return `--prop ${flag}: "${name}" is not a prop name of its own — camelCase, starting with a lowercase letter, and none of ${RESERVED_PROPS.join(", ")}.`;
|
|
3004
|
+
const propType = PROP_TYPES.find((candidate) => candidate === type);
|
|
3005
|
+
if (!propType) return `--prop ${flag}: the type is ${PROP_TYPES.join(", ")}, not "${type}".`;
|
|
3006
|
+
if (propType === "number") {
|
|
3007
|
+
const value = given === void 0 ? 0 : Number(given);
|
|
3008
|
+
return Number.isFinite(value) && given !== "" ? {
|
|
3009
|
+
name,
|
|
3010
|
+
type: propType,
|
|
3011
|
+
value
|
|
3012
|
+
} : `--prop ${flag}: "${given ?? ""}" is not a number.`;
|
|
3013
|
+
}
|
|
3014
|
+
if (propType === "boolean") {
|
|
3015
|
+
if (given !== void 0 && given !== "true" && given !== "false") return `--prop ${flag}: a boolean's default is true or false, not "${given}".`;
|
|
3016
|
+
return {
|
|
3017
|
+
name,
|
|
3018
|
+
type: propType,
|
|
3019
|
+
value: given === "true"
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
return {
|
|
3023
|
+
name,
|
|
3024
|
+
type: propType,
|
|
3025
|
+
value: given ?? ""
|
|
3026
|
+
};
|
|
3027
|
+
};
|
|
3028
|
+
var parseTrigger = (flag) => {
|
|
3029
|
+
const [name, ...rest] = flag.split(":");
|
|
3030
|
+
const fields = rest.join(":").split(",").map((field) => field.trim()).filter(Boolean);
|
|
3031
|
+
if (!EVENT.test(name)) return `--trigger ${flag}: an event is named on + what happened, in camelCase — onTick, onPick.`;
|
|
3032
|
+
if (Object.hasOwn(elementTriggers, "custom") && elementTriggers.custom.includes(name)) return `--trigger ${flag}: every element already fires "${name}"; name the event after what this one does.`;
|
|
3033
|
+
const wrong = fields.find((field) => !IDENTIFIER.test(field));
|
|
3034
|
+
return wrong ? `--trigger ${flag}: "${wrong}" is not a field name — camelCase, starting with a lowercase letter.` : {
|
|
3035
|
+
name,
|
|
3036
|
+
fields
|
|
3037
|
+
};
|
|
3038
|
+
};
|
|
3039
|
+
var parseCallback = (name) => {
|
|
3040
|
+
if (!IDENTIFIER.test(name)) return { problem: `--callback ${name}: an action is named in camelCase, starting with a lowercase letter — reset, play.` };
|
|
3041
|
+
return Object.hasOwn(a$1, name) ? { problem: `--callback ${name}: every element already answers to "${name}"; name the action after what this one does.` } : name;
|
|
3042
|
+
};
|
|
3043
|
+
var repeated = (names) => names.find((name, index) => names.indexOf(name) !== index);
|
|
3044
|
+
/**
|
|
3045
|
+
* The shape the flags describe — `undefined` when none was given, which keeps the example — or the first thing wrong
|
|
3046
|
+
* with them, said so it can be fixed in one go.
|
|
3047
|
+
*/
|
|
3048
|
+
var shapeFromFlags = (flags) => {
|
|
3049
|
+
if (!([
|
|
3050
|
+
flags.prop,
|
|
3051
|
+
flags.trigger,
|
|
3052
|
+
flags.callback
|
|
3053
|
+
].some((list) => list && list.length > 0) || flags.headless)) return {};
|
|
3054
|
+
const props = [];
|
|
3055
|
+
for (const flag of flags.prop ?? []) {
|
|
3056
|
+
const prop = parseProp(flag);
|
|
3057
|
+
if (typeof prop === "string") return { problem: prop };
|
|
3058
|
+
props.push(prop);
|
|
3059
|
+
}
|
|
3060
|
+
const triggers = [];
|
|
3061
|
+
for (const flag of flags.trigger ?? []) {
|
|
3062
|
+
const trigger = parseTrigger(flag);
|
|
3063
|
+
if (typeof trigger === "string") return { problem: trigger };
|
|
3064
|
+
triggers.push(trigger);
|
|
3065
|
+
}
|
|
3066
|
+
const callbacks = [];
|
|
3067
|
+
for (const flag of flags.callback ?? []) {
|
|
3068
|
+
const callback = parseCallback(flag);
|
|
3069
|
+
if (typeof callback !== "string") return { problem: callback.problem };
|
|
3070
|
+
callbacks.push(callback);
|
|
3071
|
+
}
|
|
3072
|
+
const twice = repeated([
|
|
3073
|
+
...props.map((prop) => prop.name),
|
|
3074
|
+
...triggers.map((trigger) => trigger.name),
|
|
3075
|
+
...callbacks
|
|
3076
|
+
]);
|
|
3077
|
+
if (twice) return { problem: `"${twice}" is named twice: a prop, an event and an action each need a name of their own.` };
|
|
3078
|
+
return { shape: {
|
|
3079
|
+
props,
|
|
3080
|
+
triggers,
|
|
3081
|
+
callbacks,
|
|
3082
|
+
headless: flags.headless === true
|
|
3083
|
+
} };
|
|
3084
|
+
};
|
|
3085
|
+
/** `onTick` → `On Tick`, `pausedOnHover` → `Paused On Hover`: what the builder shows for a name. */
|
|
3086
|
+
var titleOf = (name) => {
|
|
3087
|
+
const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
3088
|
+
return `${words.charAt(0).toUpperCase()}${words.slice(1)}`;
|
|
3089
|
+
};
|
|
3090
|
+
var literal = (value) => typeof value === "string" ? tsString(value) : String(value);
|
|
3091
|
+
var shapedDeclaration = ({ component: name, type }, { title, description, owner }, shape) => `import type { ${name}Props } from './${name}';
|
|
3092
|
+
import type { PluginDeclaration } from '@plitzi/plitzi-sdk';
|
|
3093
|
+
|
|
3094
|
+
/** What this element can be authored with — its component's own props, minus what the runtime supplies. */
|
|
3095
|
+
export type ${name}Attributes = Omit<${name}Props, 'className'>;
|
|
3096
|
+
|
|
3097
|
+
/**
|
|
3098
|
+
* Static declaration for ${name}: its type, the events it fires and the actions it answers to, and the element the
|
|
3099
|
+
* builder adds when somebody drops it on a page. Data only, no React — the build reads it to write the manifest.
|
|
3100
|
+
*/
|
|
3101
|
+
const declaration = {
|
|
3102
|
+
/** What a space names it by: the \`renderType\` of the \`custom\` element that hosts it. Renaming it orphans every one. */
|
|
3103
|
+
type: '${type}',
|
|
3104
|
+
/** The events it fires; \`preview\` names what a flow started by one reads — shown in the builder, never sent. */
|
|
3105
|
+
triggers: {${shape.triggers.map((trigger) => `
|
|
3106
|
+
${trigger.name}: {
|
|
3107
|
+
action: '${trigger.name}',
|
|
3108
|
+
title: ${tsString(titleOf(trigger.name))},
|
|
3109
|
+
type: 'trigger',
|
|
3110
|
+
params: {},
|
|
3111
|
+
preview: { ${trigger.fields.map((field) => `${field}: ''`).join(", ")} }
|
|
3112
|
+
}`).join(",")}${shape.triggers.length > 0 ? "\n " : ""}},
|
|
3113
|
+
/** The actions it answers to, static half: the component adds the function that does each. */
|
|
3114
|
+
callbacks: {${shape.callbacks.map((callback) => `
|
|
3115
|
+
${callback}: { action: '${callback}', title: ${tsString(titleOf(callback))}, type: 'callback', params: {} }`).join(",")}${shape.callbacks.length > 0 ? "\n " : ""}},
|
|
3116
|
+
content: {
|
|
3117
|
+
/** The starting attributes — the component's defaults, written where the builder can show them. */
|
|
3118
|
+
attributes: { ${shape.props.map((prop) => `${prop.name}: ${literal(prop.value)}`).join(", ")} },
|
|
3119
|
+
definition: {
|
|
3120
|
+
label: ${tsString(title)},
|
|
3121
|
+
type: '${type}',
|
|
3122
|
+
/** What it is for: the builder shows it, and an agent connected over MCP reads it to choose the element. */
|
|
3123
|
+
description: ${tsString(description)},
|
|
3124
|
+
items: [],
|
|
3125
|
+
bindings: {},
|
|
3126
|
+
styleSelectors: { base: '' },
|
|
3127
|
+
initialState: { visibility: true }
|
|
3128
|
+
},
|
|
3129
|
+
builder: {
|
|
3130
|
+
canDelete: true,
|
|
3131
|
+
canSelect: true,
|
|
3132
|
+
canDragDrop: true,
|
|
3133
|
+
canMove: true,
|
|
3134
|
+
canSnippet: true,
|
|
3135
|
+
itemsAllowed: [],
|
|
3136
|
+
itemsNotAllowed: []
|
|
3137
|
+
},
|
|
3138
|
+
market: {
|
|
3139
|
+
category: ${tsString(title)},
|
|
3140
|
+
owner: ${tsString(owner)},
|
|
3141
|
+
license: 'MIT',
|
|
3142
|
+
website: '',
|
|
3143
|
+
backgroundColor: '#4422ee',
|
|
3144
|
+
icon: ''
|
|
3145
|
+
},
|
|
3146
|
+
defaultStyle: {
|
|
3147
|
+
name: ${tsString(title)},
|
|
3148
|
+
displayMode: 'desktop',
|
|
3149
|
+
style: { base: { default: {} } },
|
|
3150
|
+
/** The attributes a data source may be pointed at — what the builder offers when somebody connects data to it. */
|
|
3151
|
+
bindingsAllowed: {
|
|
3152
|
+
attributes: [${shape.props.map((prop) => `{ path: '${prop.name}', label: ${tsString(titleOf(prop.name))} }`).join(", ")}],
|
|
3153
|
+
initialState: []
|
|
3154
|
+
}
|
|
3155
|
+
},
|
|
3156
|
+
settings: {}
|
|
3157
|
+
}
|
|
3158
|
+
} satisfies PluginDeclaration<${name}Attributes>;
|
|
3159
|
+
|
|
3160
|
+
export default declaration;
|
|
3161
|
+
`;
|
|
3162
|
+
var eventsSource = (name, triggers) => triggers.length === 0 ? "" : `
|
|
3163
|
+
/** What a flow started by each event reads — \`{{ <step>.${triggers[0].fields[0] ?? "field"} }}\`. */
|
|
3164
|
+
export type ${name}Events = {
|
|
3165
|
+
${triggers.map((trigger) => ` ${trigger.name}: ${trigger.fields.length > 0 ? `{ ${trigger.fields.map((field) => `${field}: unknown`).join("; ")} }` : "Record<string, never>"};`).join("\n")}
|
|
3166
|
+
};
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* Fires one of the element's events: \`fire('${triggers[0].name}', { ${triggers[0].fields.map((field) => `${field}: …`).join(", ")} })\`. A flow on that event
|
|
3170
|
+
* runs, and reads what it was handed.
|
|
3171
|
+
*/
|
|
3172
|
+
export const use${name}Events = () => {
|
|
3173
|
+
const { id } = useElement();
|
|
3174
|
+
const {
|
|
3175
|
+
contexts: { InteractionsContext }
|
|
3176
|
+
} = usePlitziServiceContext();
|
|
3177
|
+
const { interactionsManager } = use(InteractionsContext);
|
|
3178
|
+
|
|
3179
|
+
return useCallback(
|
|
3180
|
+
<Event extends keyof ${name}Events>(event: Event, payload: ${name}Events[Event]) => {
|
|
3181
|
+
void interactionsManager.interactionTrigger(id, event, payload);
|
|
3182
|
+
},
|
|
3183
|
+
[interactionsManager, id]
|
|
3184
|
+
);
|
|
3185
|
+
};
|
|
3186
|
+
`;
|
|
3187
|
+
var callbacksSource = (callbacks) => callbacks.length === 0 ? "" : `
|
|
3188
|
+
/** The declared actions, with what only a mounted element has: the function that does each. */
|
|
3189
|
+
const interactionCallbacks = useMemo<Record<string, InteractionCallback>>(
|
|
3190
|
+
() => ({${callbacks.map((callback) => `
|
|
3191
|
+
${callback}: {
|
|
3192
|
+
...declaration.callbacks.${callback},
|
|
3193
|
+
callback: () => {
|
|
3194
|
+
// What \`${callback}\` does when a flow calls it.
|
|
3195
|
+
}
|
|
3196
|
+
}`).join(",")}
|
|
3197
|
+
}),
|
|
3198
|
+
[]
|
|
3199
|
+
);
|
|
3200
|
+
`;
|
|
3201
|
+
var shapedComponent = ({ component: name, title }, shape) => {
|
|
3202
|
+
const { props, triggers, callbacks, headless } = shape;
|
|
3203
|
+
const reactImports = [...triggers.length > 0 ? ["use", "useCallback"] : [], ...callbacks.length > 0 ? ["useMemo"] : []];
|
|
3204
|
+
const sdkImports = [
|
|
3205
|
+
"RootElement",
|
|
3206
|
+
...triggers.length > 0 ? ["useElement"] : [],
|
|
3207
|
+
...triggers.length > 0 || headless ? ["usePlitziServiceContext"] : []
|
|
3208
|
+
];
|
|
3209
|
+
const imports = [
|
|
3210
|
+
...reactImports.length > 0 ? [`import { ${reactImports.join(", ")} } from 'react';`, ""] : [],
|
|
3211
|
+
`import { ${sdkImports.join(", ")} } from '@plitzi/plitzi-sdk';`,
|
|
3212
|
+
"",
|
|
3213
|
+
"import declaration from './declaration';",
|
|
3214
|
+
"",
|
|
3215
|
+
...callbacks.length > 0 ? ["import type { InteractionCallback } from '@plitzi/plitzi-sdk';"] : [],
|
|
3216
|
+
"import type { CSSProperties } from 'react';"
|
|
3217
|
+
];
|
|
3218
|
+
const params = [...props.map((prop) => `${prop.name} = ${literal(prop.value)}`), "className"].join(", ");
|
|
3219
|
+
const rootProps = [
|
|
3220
|
+
"className={className}",
|
|
3221
|
+
"style={style}",
|
|
3222
|
+
...triggers.length > 0 ? ["interactionTriggers={declaration.triggers}"] : [],
|
|
3223
|
+
...callbacks.length > 0 ? ["interactionCallbacks={interactionCallbacks}"] : []
|
|
3224
|
+
];
|
|
3225
|
+
const shown = (line) => headless ? `{!previewMode && ${line}}` : line;
|
|
3226
|
+
const body = [shown(`<span style={LABEL}>${title}</span>`), ...props.map((prop) => shown(`<span>${titleOf(prop.name)}: {String(${prop.name})}</span>`))].map((line) => ` ${line}`).join("\n");
|
|
3227
|
+
const look = headless ? `
|
|
3228
|
+
const {
|
|
3229
|
+
settings: { previewMode }
|
|
3230
|
+
} = usePlitziServiceContext();
|
|
3231
|
+
const style = previewMode ? HIDDEN : BADGE;
|
|
3232
|
+
` : `
|
|
3233
|
+
const style = CARD;
|
|
3234
|
+
`;
|
|
3235
|
+
const styles = headless ? `const HIDDEN: CSSProperties = { display: 'none' };
|
|
3236
|
+
|
|
3237
|
+
/** In the builder: small, and in the colours of whatever page it is on. */
|
|
3238
|
+
const BADGE: CSSProperties = {
|
|
3239
|
+
display: 'inline-flex',
|
|
3240
|
+
gap: '8px',
|
|
3241
|
+
padding: '4px 10px',
|
|
3242
|
+
borderRadius: '999px',
|
|
3243
|
+
fontSize: '12px',
|
|
3244
|
+
border: '1px dashed color-mix(in srgb, currentColor 35%, transparent)'
|
|
3245
|
+
};` : `/** Colours borrowed rather than chosen: a plugin does not know the palette of the page it is dropped on. */
|
|
3246
|
+
const CARD: CSSProperties = {
|
|
3247
|
+
display: 'inline-flex',
|
|
3248
|
+
flexDirection: 'column',
|
|
3249
|
+
gap: '4px',
|
|
3250
|
+
padding: '12px 16px',
|
|
3251
|
+
borderRadius: '12px',
|
|
3252
|
+
border: '1px solid color-mix(in srgb, currentColor 15%, transparent)'
|
|
3253
|
+
};`;
|
|
3254
|
+
return `${imports.join("\n")}
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* The props ARE the element's attributes: whatever a space writes on the element arrives here by the same name, and so
|
|
3258
|
+
* does whatever a binding writes later. Everything is optional with a default — an attribute nobody has authored yet,
|
|
3259
|
+
* or a binding whose source has not answered, is \`undefined\`.
|
|
3260
|
+
*/
|
|
3261
|
+
export interface ${name}Props {
|
|
3262
|
+
${props.map((prop) => ` ${prop.name}?: ${prop.type};\n`).join("")} /** Supplied by the runtime, not authored: the classes the element's own style rules are written against. */
|
|
3263
|
+
className?: string;
|
|
3264
|
+
}
|
|
3265
|
+
${eventsSource(name, triggers)}
|
|
3266
|
+
/**
|
|
3267
|
+
* ${headless ? "Nothing to see on a page: it is there for what it does. In the builder it shows as a badge with its attributes, so it can be selected; on a page it renders hidden — still an element, so its events and actions are its own." : "What it draws — for now its attributes, to be replaced by what it is. Deterministic on its first render (the same markup on a server and in the browser); anything live goes in an effect."}
|
|
3268
|
+
*/
|
|
3269
|
+
const ${name} = ({ ${params} }: ${name}Props) => {${look}${callbacksSource(callbacks)}
|
|
3270
|
+
return (
|
|
3271
|
+
<RootElement
|
|
3272
|
+
${rootProps.map((prop) => ` ${prop}`).join("\n")}
|
|
3273
|
+
>
|
|
3274
|
+
${body}
|
|
3275
|
+
</RootElement>
|
|
3276
|
+
);
|
|
3277
|
+
};
|
|
3278
|
+
|
|
3279
|
+
${styles}
|
|
3280
|
+
|
|
3281
|
+
const LABEL: CSSProperties = { fontSize: '12px', opacity: 0.65 };
|
|
3282
|
+
|
|
3283
|
+
export default ${name};
|
|
3284
|
+
`;
|
|
3285
|
+
};
|
|
3286
|
+
var settingsControl = (prop) => {
|
|
3287
|
+
const label = titleOf(prop.name);
|
|
3288
|
+
if (prop.type === "boolean") return ` <label style={FIELD}>
|
|
3289
|
+
<input type="checkbox" checked={${prop.name}} onChange={event => onUpdate?.('${prop.name}', event.target.checked)} />
|
|
3290
|
+
${label}
|
|
3291
|
+
</label>`;
|
|
3292
|
+
return ` <label style={FIELD}>
|
|
3293
|
+
${label}
|
|
3294
|
+
<input style={INPUT} ${prop.type === "number" ? `type="number" value={${prop.name}} onChange={event => onUpdate?.('${prop.name}', Number(event.target.value))}` : `value={${prop.name}} onChange={event => onUpdate?.('${prop.name}', event.target.value)}`} />
|
|
3295
|
+
</label>`;
|
|
3296
|
+
};
|
|
3297
|
+
var shapedSettings = ({ component: name }, shape) => {
|
|
3298
|
+
const { props } = shape;
|
|
3299
|
+
const typed = props.some((prop) => prop.type !== "boolean");
|
|
3300
|
+
return `import type { ${name}Attributes } from './declaration';
|
|
3301
|
+
import type { CSSProperties } from 'react';
|
|
3302
|
+
|
|
3303
|
+
/**
|
|
3304
|
+
* The builder's panel for the element: one control per attribute, writing back through \`onUpdate\` the attributes the
|
|
3305
|
+
* component reads. Only the builder loads it.
|
|
3306
|
+
*/
|
|
3307
|
+
export type SettingsProps = ${name}Attributes & {
|
|
3308
|
+
onUpdate?: (attribute: string, value: string | number | boolean) => void;
|
|
3309
|
+
};
|
|
3310
|
+
|
|
3311
|
+
${props.length === 0 ? "const Settings = () => <div style={PANEL}>It has no attributes to set.</div>;" : `const Settings = ({ ${[...props.map((prop) => `${prop.name} = ${literal(prop.value)}`), "onUpdate"].join(", ")} }: SettingsProps) => (
|
|
3312
|
+
<div style={PANEL}>
|
|
3313
|
+
${props.map(settingsControl).join("\n")}
|
|
3314
|
+
</div>
|
|
3315
|
+
);`}
|
|
3316
|
+
|
|
3317
|
+
const PANEL: CSSProperties = { display: 'flex', flexDirection: 'column', gap: '12px', padding: '8px 0' };
|
|
3318
|
+
${props.length > 0 ? `
|
|
3319
|
+
const FIELD: CSSProperties = { display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '12px' };
|
|
3320
|
+
` : ""}${typed ? `
|
|
3321
|
+
const INPUT: CSSProperties = {
|
|
3322
|
+
font: 'inherit',
|
|
3323
|
+
color: 'inherit',
|
|
3324
|
+
padding: '6px 8px',
|
|
3325
|
+
borderRadius: '6px',
|
|
3326
|
+
border: '1px solid color-mix(in srgb, currentColor 25%, transparent)',
|
|
3327
|
+
background: 'transparent'
|
|
3328
|
+
};
|
|
3329
|
+
` : ""}
|
|
3330
|
+
export default Settings;
|
|
3331
|
+
`;
|
|
3332
|
+
};
|
|
3333
|
+
//#endregion
|
|
2438
3334
|
//#region src/scaffold/pluginPackage/source.ts
|
|
2439
3335
|
var declarationFile = ({ component: name, type }, { title, description, owner }) => `import type { ${name}Props } from './${name}';
|
|
2440
3336
|
import type { PluginDeclaration } from '@plitzi/plitzi-sdk';
|
|
@@ -2476,7 +3372,7 @@ const declaration = {
|
|
|
2476
3372
|
canSelect: true,
|
|
2477
3373
|
canDragDrop: true,
|
|
2478
3374
|
canMove: true,
|
|
2479
|
-
|
|
3375
|
+
canSnippet: true,
|
|
2480
3376
|
itemsAllowed: [],
|
|
2481
3377
|
itemsNotAllowed: []
|
|
2482
3378
|
},
|
|
@@ -2758,10 +3654,10 @@ export { elements };
|
|
|
2758
3654
|
export default main;
|
|
2759
3655
|
`;
|
|
2760
3656
|
/** The element alone: its folder's files, by their path inside it. */
|
|
2761
|
-
var elementFiles = (names, text) => ({
|
|
2762
|
-
[`${names.component}.tsx`]: component(names),
|
|
2763
|
-
"declaration.ts": declarationFile(names, text),
|
|
2764
|
-
"Settings.tsx": settings(names),
|
|
3657
|
+
var elementFiles = (names, text, shape) => ({
|
|
3658
|
+
[`${names.component}.tsx`]: shape ? shapedComponent(names, shape) : component(names),
|
|
3659
|
+
"declaration.ts": shape ? shapedDeclaration(names, text, shape) : declarationFile(names, text),
|
|
3660
|
+
"Settings.tsx": shape ? shapedSettings(names, shape) : settings(names),
|
|
2765
3661
|
"index.ts": elementIndex(names)
|
|
2766
3662
|
});
|
|
2767
3663
|
/** Every element of a plugin package in its own folder, the two lists that gather them, and the entry. */
|
|
@@ -2889,10 +3785,17 @@ var scaffoldPlugin = (answers) => {
|
|
|
2889
3785
|
* One element, for a folder of a project that already exists: paths relative to the element's own folder. `title` is
|
|
2890
3786
|
* the label it renders until an attribute says otherwise.
|
|
2891
3787
|
*/
|
|
2892
|
-
var scaffoldElement = (name, text) => elementFiles({
|
|
3788
|
+
var scaffoldElement = (name, text, shape) => elementFiles({
|
|
2893
3789
|
...pluginNames(name),
|
|
2894
3790
|
title: text.title
|
|
2895
|
-
}, text);
|
|
3791
|
+
}, text, shape);
|
|
3792
|
+
//#endregion
|
|
3793
|
+
//#region src/scaffold/types.ts
|
|
3794
|
+
var CREATE_TEMPLATES = [
|
|
3795
|
+
"welcome",
|
|
3796
|
+
"blank",
|
|
3797
|
+
"catalog"
|
|
3798
|
+
];
|
|
2896
3799
|
//#endregion
|
|
2897
3800
|
//#region src/scaffold/index.ts
|
|
2898
3801
|
/**
|
|
@@ -3043,144 +3946,49 @@ var findProject = async (from) => {
|
|
|
3043
3946
|
};
|
|
3044
3947
|
/** Whether `dir` is one of the workspace's packages — a folder its globs cover — so an install there installs it. */
|
|
3045
3948
|
var coveredByWorkspace = (project, dir) => project.workspaceRoot !== void 0 && project.workspaceFolders.some((folder) => path.dirname(dir) === path.join(project.workspaceRoot ?? "", folder));
|
|
3046
|
-
//#endregion
|
|
3047
|
-
//#region src/commands/terminal.ts
|
|
3048
|
-
/** Somebody who can answer a prompt. An agent's shell, CI and a pipe are nobody, whatever they were asked to do. */
|
|
3049
|
-
var atTerminal = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
3050
|
-
/**
|
|
3051
|
-
* Nobody to ask, so the questions go to whoever ran this — in practice an agent — to put to the person.
|
|
3052
|
-
*
|
|
3053
|
-
* It offers no way around them. It used to end with "or with --yes to take the defaults", and an agent keen to finish
|
|
3054
|
-
* took that exit every time: the person was never asked, which is the whole failure this stop exists to prevent.
|
|
3055
|
-
*/
|
|
3056
|
-
var refuseWithoutTerminal = (what, questions, stopped = `plitzi create stopped before writing anything: these choices shape the whole ${what}`) => {
|
|
3057
|
-
console.error(chalk.red(`\n${stopped}, they belong to the person it is for, and nobody is at this terminal to make them.`));
|
|
3058
|
-
console.error(chalk.bold("\nIf you are an AI agent: ask the user each question below, show them the options, and wait for their answers. Do not choose for them and do not fall back to a default.\n"));
|
|
3059
|
-
questions.forEach(({ flag, choices, question }, index) => {
|
|
3060
|
-
console.error(` ${index + 1}. ${question}`);
|
|
3061
|
-
console.error(` ${flag} ${choices.join(" | ")}`);
|
|
3062
|
-
});
|
|
3063
|
-
const flags = questions.map(({ flag }) => `${flag} <their answer>`).join(" ");
|
|
3064
|
-
console.error(`\nThen run the same command again with their answers: ${flags}`);
|
|
3065
|
-
process.exitCode = 1;
|
|
3066
|
-
};
|
|
3067
|
-
/** One choice at the terminal: Enter takes the default, anything off the list is refused rather than guessed at. */
|
|
3068
|
-
var ask = async (rl, question) => {
|
|
3069
|
-
const reply = (await rl.question(`\n${question.question}\n${question.flag} (${question.choices.join("/")}) [${question.fallback}] > `)).trim();
|
|
3070
|
-
const chosen = question.choices.find((choice) => choice === (reply || question.fallback));
|
|
3071
|
-
if (!chosen) console.error(chalk.red(`"${reply}" is not one of ${question.choices.join(", ")}.`));
|
|
3072
|
-
return chosen;
|
|
3073
|
-
};
|
|
3074
3949
|
/**
|
|
3075
|
-
*
|
|
3076
|
-
*
|
|
3077
|
-
*
|
|
3950
|
+
* The project the command runs in, or `undefined` — said — when there is none: every command that works on a project
|
|
3951
|
+
* refuses outside one rather than writing into whatever folder it was run from. `what` ends the sentence: "whose page
|
|
3952
|
+
* to check".
|
|
3078
3953
|
*/
|
|
3079
|
-
var
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
const problem = check(reply);
|
|
3084
|
-
if (!problem) return reply;
|
|
3085
|
-
console.error(chalk.red(problem));
|
|
3086
|
-
}
|
|
3954
|
+
var projectHere = async (what) => {
|
|
3955
|
+
const project = await findProject(process.cwd());
|
|
3956
|
+
if (!project) fail(`There is no package.json here or above: run this in the project ${what}.`);
|
|
3957
|
+
return project;
|
|
3087
3958
|
};
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
console.error(chalk.red(`"${reply}" is not a number from 1 to ${options.length + 1}.`));
|
|
3100
|
-
}
|
|
3101
|
-
};
|
|
3102
|
-
/** One of a numbered list and nothing else. Enter takes the first entry. */
|
|
3103
|
-
var askPick = async (rl, question, options) => {
|
|
3104
|
-
const menu = options.map(({ label }, index) => ` ${index + 1}. ${label}`).join("\n");
|
|
3105
|
-
for (;;) {
|
|
3106
|
-
const reply = (await rl.question(`\n${question}\n${menu}\n[1] > `)).trim() || "1";
|
|
3107
|
-
const index = Number(reply) - 1;
|
|
3108
|
-
if (Number.isInteger(index) && index >= 0 && index < options.length) return options[index].value;
|
|
3109
|
-
console.error(chalk.red(`"${reply}" is not a number from 1 to ${options.length}.`));
|
|
3110
|
-
}
|
|
3111
|
-
};
|
|
3112
|
-
/** A directory that does not exist yet is as empty as one can be, which is the answer this question wants. */
|
|
3113
|
-
var isEmpty = async (target) => {
|
|
3114
|
-
try {
|
|
3115
|
-
return (await fs.readdir(target)).length === 0;
|
|
3116
|
-
} catch {
|
|
3117
|
-
return true;
|
|
3118
|
-
}
|
|
3119
|
-
};
|
|
3120
|
-
var isDirectory$1 = async (candidate) => {
|
|
3959
|
+
//#endregion
|
|
3960
|
+
//#region src/commands/projectFormatter.ts
|
|
3961
|
+
var isPrettier = (value) => e$1(value) && [
|
|
3962
|
+
"format",
|
|
3963
|
+
"resolveConfig",
|
|
3964
|
+
"getFileInfo"
|
|
3965
|
+
].every((name) => typeof value[name] === "function");
|
|
3966
|
+
var unformatted = (_file, text) => Promise.resolve(text);
|
|
3967
|
+
/** Loads the project's Prettier once. A project without one formats nothing. */
|
|
3968
|
+
var projectFormatter = async (root) => {
|
|
3969
|
+
let loaded;
|
|
3121
3970
|
try {
|
|
3122
|
-
|
|
3971
|
+
const resolved = createRequire(path.join(root, "package.json")).resolve("prettier");
|
|
3972
|
+
loaded = await import(pathToFileURL(resolved).href);
|
|
3123
3973
|
} catch {
|
|
3124
|
-
return
|
|
3974
|
+
return unformatted;
|
|
3125
3975
|
}
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
};
|
|
3141
|
-
|
|
3142
|
-
* What a first install is most likely refused over, and what to do about it.
|
|
3143
|
-
*
|
|
3144
|
-
* The scaffold already exempts what it ships, but not the rest of the tree: a third-party dependency published in
|
|
3145
|
-
* the last day is held back by the same age gates, and says so in a code that means nothing to somebody who has
|
|
3146
|
-
* not touched the project yet.
|
|
3147
|
-
*/
|
|
3148
|
-
var INSTALL_HINTS = {
|
|
3149
|
-
npm: "A peer-dependency conflict (ERESOLVE) is a bug in this scaffold: please report it. `npm install --legacy-peer-deps` gets past it meanwhile.",
|
|
3150
|
-
yarn: "YN0016 (\"quarantined\") means a dependency was published less than a day ago. Wait, or add it to `npmPreapprovedPackages` in .yarnrc.yml.",
|
|
3151
|
-
pnpm: "A dependency published too recently is added to `minimumReleaseAgeExclude` in pnpm-workspace.yaml; a skipped build script is approved with `pnpm approve-builds`."
|
|
3152
|
-
};
|
|
3153
|
-
var install = (manager, cwd) => new Promise((resolve) => {
|
|
3154
|
-
const child = spawn(manager, ["install"], {
|
|
3155
|
-
cwd,
|
|
3156
|
-
stdio: "inherit",
|
|
3157
|
-
shell: process.platform === "win32"
|
|
3158
|
-
});
|
|
3159
|
-
child.on("error", () => resolve(false));
|
|
3160
|
-
child.on("close", (code) => resolve(code === 0));
|
|
3161
|
-
});
|
|
3162
|
-
/**
|
|
3163
|
-
* One of the project's own scripts, run quietly: what it prints is the project's business, and whether it worked is
|
|
3164
|
-
* all this needs. `run` is understood by all three managers alike.
|
|
3165
|
-
*/
|
|
3166
|
-
var runScript = (manager, script, cwd) => new Promise((resolve) => {
|
|
3167
|
-
const child = spawn(manager, ["run", script], {
|
|
3168
|
-
cwd,
|
|
3169
|
-
stdio: "ignore",
|
|
3170
|
-
shell: process.platform === "win32"
|
|
3171
|
-
});
|
|
3172
|
-
child.on("error", () => resolve(false));
|
|
3173
|
-
child.on("close", (code) => resolve(code === 0));
|
|
3174
|
-
});
|
|
3175
|
-
/**
|
|
3176
|
-
* How to get to `target` from here, in whichever form is shorter.
|
|
3177
|
-
*
|
|
3178
|
-
* A relative path out of a temp directory is six `../` and unreadable; an absolute one to a sibling folder is noise.
|
|
3179
|
-
* These lines are meant to be pasted, so they are printed the way somebody would have typed them.
|
|
3180
|
-
*/
|
|
3181
|
-
var cdPrefix = (target) => {
|
|
3182
|
-
const relative = path.relative(process.cwd(), target);
|
|
3183
|
-
return target === process.cwd() ? "" : `cd ${relative.length < target.length ? relative : target} && `;
|
|
3976
|
+
const prettier = isPrettier(loaded) ? loaded : e$1(loaded) ? loaded.default : void 0;
|
|
3977
|
+
if (!isPrettier(prettier)) return unformatted;
|
|
3978
|
+
const api = prettier;
|
|
3979
|
+
const ignorePath = [".prettierignore", ".gitignore"].map((file) => path.join(root, file));
|
|
3980
|
+
return async (file, text) => {
|
|
3981
|
+
const filepath = path.join(root, file);
|
|
3982
|
+
const info = await api.getFileInfo(filepath, {
|
|
3983
|
+
ignorePath,
|
|
3984
|
+
resolveConfig: true
|
|
3985
|
+
});
|
|
3986
|
+
if (info.ignored || !info.inferredParser) return text;
|
|
3987
|
+
return api.format(text, {
|
|
3988
|
+
...await api.resolveConfig(filepath),
|
|
3989
|
+
filepath
|
|
3990
|
+
});
|
|
3991
|
+
};
|
|
3184
3992
|
};
|
|
3185
3993
|
//#endregion
|
|
3186
3994
|
//#region src/commands/addPlugin.ts
|
|
@@ -3295,7 +4103,7 @@ src/plugins/declarations.ts is missing or not the list the CLI wrote, so it is y
|
|
|
3295
4103
|
/** A project the CLI did not write: how to register, and the one dependency the elements import. */
|
|
3296
4104
|
var registerElsewhere = async (root, added) => {
|
|
3297
4105
|
console.log(`\nRegister ${added.length > 1 ? "them" : "it"} where the project renders its space:\n\n${chalk.dim(registration(added))}`);
|
|
3298
|
-
const { dependencies = {}, devDependencies = {} } =
|
|
4106
|
+
const { dependencies = {}, devDependencies = {} } = await readPackageJson(root) ?? {};
|
|
3299
4107
|
if (!("@plitzi/plitzi-sdk" in {
|
|
3300
4108
|
...dependencies,
|
|
3301
4109
|
...devDependencies
|
|
@@ -3304,19 +4112,21 @@ var registerElsewhere = async (root, added) => {
|
|
|
3304
4112
|
var addPlugin = async (namesGiven, options) => {
|
|
3305
4113
|
const project = await findProject(process.cwd());
|
|
3306
4114
|
if (!project) {
|
|
3307
|
-
|
|
3308
|
-
process.exitCode = 1;
|
|
4115
|
+
fail("plitzi add plugin adds an element to a project, and there is no package.json here or above. For a plugin of its own, run plitzi create <folder> --plugin.");
|
|
3309
4116
|
return;
|
|
3310
4117
|
}
|
|
3311
4118
|
if (namesGiven.length > 1 && (options.title !== void 0 || options.description !== void 0)) {
|
|
3312
|
-
|
|
3313
|
-
|
|
4119
|
+
fail("--title and --description describe one element: add several without them, or one at a time.");
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
const { shape, problem: shapeProblem } = shapeFromFlags(options);
|
|
4123
|
+
if (shapeProblem ?? (shape && namesGiven.length > 1)) {
|
|
4124
|
+
fail(shapeProblem ?? "--prop, --trigger, --callback and --headless shape one element: add them one at a time.");
|
|
3314
4125
|
return;
|
|
3315
4126
|
}
|
|
3316
4127
|
const givenProblem = namesGiven.length > 0 ? namesProblem(namesGiven) : void 0;
|
|
3317
4128
|
if (givenProblem) {
|
|
3318
|
-
|
|
3319
|
-
process.exitCode = 1;
|
|
4129
|
+
fail(givenProblem);
|
|
3320
4130
|
return;
|
|
3321
4131
|
}
|
|
3322
4132
|
const folderKnown = options.dir !== void 0 || project.plitzi !== void 0;
|
|
@@ -3351,21 +4161,23 @@ var addPlugin = async (namesGiven, options) => {
|
|
|
3351
4161
|
};
|
|
3352
4162
|
});
|
|
3353
4163
|
for (const { target } of planned) if (!options.force && !await isEmpty(target)) {
|
|
3354
|
-
|
|
3355
|
-
process.exitCode = 1;
|
|
4164
|
+
fail(`${target} is not empty. Pass --force to write into it anyway.`);
|
|
3356
4165
|
return;
|
|
3357
4166
|
}
|
|
4167
|
+
const format = await projectFormatter(project.root);
|
|
3358
4168
|
const single = planned.length === 1;
|
|
3359
4169
|
const answer = async (question, given, fallback) => given ?? (rl ? askText(rl, question, fallback) : fallback);
|
|
3360
4170
|
const added = [];
|
|
3361
4171
|
for (const { name, names: elementNames, target } of planned) {
|
|
3362
4172
|
const called = single ? "it" : name;
|
|
3363
4173
|
const title = await answer(`What does the builder call ${called}?`, single ? options.title : void 0, elementNames.title);
|
|
3364
|
-
|
|
4174
|
+
const files = scaffoldElement(name, {
|
|
3365
4175
|
title,
|
|
3366
4176
|
description: await answer(`What is ${called} for, in a sentence? The builder shows it, and an agent reads it to choose the element.`, single ? options.description : void 0, ""),
|
|
3367
4177
|
owner: ""
|
|
3368
|
-
})
|
|
4178
|
+
}, shape);
|
|
4179
|
+
const formatted = await Promise.all(Object.entries(files).map(async ([file, text]) => [file, await format(path.relative(project.root, path.join(target, file)), text)]));
|
|
4180
|
+
await writeFiles(target, Object.fromEntries(formatted));
|
|
3369
4181
|
added.push({
|
|
3370
4182
|
names: {
|
|
3371
4183
|
...elementNames,
|
|
@@ -3393,6 +4205,166 @@ This project lists its plugins in src/main.ts (projects created since find them
|
|
|
3393
4205
|
}
|
|
3394
4206
|
};
|
|
3395
4207
|
//#endregion
|
|
4208
|
+
//#region src/commands/projectSpace.ts
|
|
4209
|
+
/**
|
|
4210
|
+
* What the module exports as `space`, taken as a declaration when it has the shape of one. `authorSpace` checks the
|
|
4211
|
+
* rest of it, field by field, and says what is wrong — so this only has to tell a space from anything else.
|
|
4212
|
+
*/
|
|
4213
|
+
var isSpaceSpec = (value) => e$1(value) && typeof value.name === "string" && typeof value.permanentUrl === "string" && Array.isArray(value.pages);
|
|
4214
|
+
var isDeclarations = (value) => Array.isArray(value) && value.every((entry) => e$1(entry) && typeof entry.type === "string");
|
|
4215
|
+
var importProject = async (file) => import(pathToFileURL(file).href);
|
|
4216
|
+
/**
|
|
4217
|
+
* The project's own declaration, loaded as its `author` script loads it — `src/space.ts`, and the plugins in
|
|
4218
|
+
* `src/plugins/declarations.ts` — for a command that authors it in this process.
|
|
4219
|
+
*/
|
|
4220
|
+
var loadProjectSpace = async (root) => {
|
|
4221
|
+
const module = await importProject(path.join(root, "src/space.ts"));
|
|
4222
|
+
const space = e$1(module) ? module.space : void 0;
|
|
4223
|
+
if (!isSpaceSpec(space)) return { problem: "src/space.ts exports no `space`." };
|
|
4224
|
+
const registry = await importProject(path.join(root, "src/plugins/declarations.ts")).catch(() => void 0);
|
|
4225
|
+
const declarations = e$1(registry) ? registry.declarations : void 0;
|
|
4226
|
+
return {
|
|
4227
|
+
space,
|
|
4228
|
+
plugins: isDeclarations(declarations) ? declarations : []
|
|
4229
|
+
};
|
|
4230
|
+
};
|
|
4231
|
+
//#endregion
|
|
4232
|
+
//#region src/commands/check.ts
|
|
4233
|
+
/** The space the project declares, authored — the handles say what each page owes. Only for one written here. */
|
|
4234
|
+
var projectHandles = async (root) => {
|
|
4235
|
+
const project = await loadProjectSpace(root);
|
|
4236
|
+
if ("problem" in project) return { problem: `${project.problem} There is nothing to check the page against.` };
|
|
4237
|
+
try {
|
|
4238
|
+
return authorSpace(project.space, { plugins: project.plugins }).handles;
|
|
4239
|
+
} catch (error) {
|
|
4240
|
+
return { problem: `The space does not author — npm run author says why: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
/** The page a path is, `:slug` and `{{slug}}` matching any segment. */
|
|
4244
|
+
var pageFor = (handles, pathname) => {
|
|
4245
|
+
const wanted = pathname.replace(/\/$/, "").split("/");
|
|
4246
|
+
return Object.values(handles.pages).find((page) => {
|
|
4247
|
+
const segments = page.path.replace(/\/$/, "").split("/");
|
|
4248
|
+
return segments.length === wanted.length && segments.every((segment, index) => segment.startsWith(":") || /^\{\{.*\}\}$/.test(segment) || segment === wanted[index]);
|
|
4249
|
+
})?.id;
|
|
4250
|
+
};
|
|
4251
|
+
var checkAt = async (browser, origin, pathname, width, scheme, handles, asked) => {
|
|
4252
|
+
const page = await browser.newPage({
|
|
4253
|
+
viewport: {
|
|
4254
|
+
width,
|
|
4255
|
+
height: 900
|
|
4256
|
+
},
|
|
4257
|
+
colorScheme: scheme,
|
|
4258
|
+
reducedMotion: "no-preference"
|
|
4259
|
+
});
|
|
4260
|
+
const consoleErrors = [];
|
|
4261
|
+
const failedRequests = [];
|
|
4262
|
+
page.on("pageerror", (error) => consoleErrors.push(error.message));
|
|
4263
|
+
page.on("console", (message) => {
|
|
4264
|
+
if (message.type() === "error") consoleErrors.push(message.text());
|
|
4265
|
+
});
|
|
4266
|
+
page.on("response", (response) => {
|
|
4267
|
+
if (response.status() >= 400 && response.url().startsWith(origin)) failedRequests.push(`${String(response.status())} ${response.url().slice(origin.length)}`);
|
|
4268
|
+
});
|
|
4269
|
+
if (!await page.goto(`${origin}${pathname}`, { waitUntil: "networkidle" }).catch(() => null)) return {
|
|
4270
|
+
path: pathname,
|
|
4271
|
+
width,
|
|
4272
|
+
ok: false,
|
|
4273
|
+
checked: 0,
|
|
4274
|
+
problems: [`nothing answers at ${origin}${pathname}`],
|
|
4275
|
+
consoleErrors,
|
|
4276
|
+
failedRequests,
|
|
4277
|
+
devTools: false
|
|
4278
|
+
};
|
|
4279
|
+
const pageId = handles ? pageFor(handles, pathname) : void 0;
|
|
4280
|
+
const report = handles && pageId ? await inspectPage(page, handles, { page: pageId }) : await inspectDocument(page);
|
|
4281
|
+
const devTools = await readDevTools(page, asked);
|
|
4282
|
+
const problems = [
|
|
4283
|
+
...handles && !pageId ? [`no page of the space answers at ${pathname}`] : [],
|
|
4284
|
+
...report.problems,
|
|
4285
|
+
...devTools.flows.filter((flow) => flow.status === "failed").map(failedFlowText)
|
|
4286
|
+
];
|
|
4287
|
+
return {
|
|
4288
|
+
path: pathname,
|
|
4289
|
+
width,
|
|
4290
|
+
ok: problems.length === 0 && consoleErrors.length === 0 && failedRequests.length === 0,
|
|
4291
|
+
checked: report.checked,
|
|
4292
|
+
problems,
|
|
4293
|
+
consoleErrors,
|
|
4294
|
+
failedRequests,
|
|
4295
|
+
devTools: devTools.available,
|
|
4296
|
+
...devTools.state === void 0 ? {} : { state: devTools.state },
|
|
4297
|
+
...devTools.sources === void 0 ? {} : { sources: devTools.sources },
|
|
4298
|
+
...devTools.element === void 0 ? {} : { element: devTools.element }
|
|
4299
|
+
};
|
|
4300
|
+
};
|
|
4301
|
+
/** What the page holds, when it was asked for: one compact line each, the shape of a source rather than its rows. */
|
|
4302
|
+
var heldText = (report, asked) => {
|
|
4303
|
+
if (!asked.state && asked.element === void 0) return [];
|
|
4304
|
+
if (!report.devTools) return [" · the page has no dev tools on (debug mode), so its state and elements cannot be read"];
|
|
4305
|
+
return [
|
|
4306
|
+
...report.state === void 0 ? [] : [` · state ${JSON.stringify(report.state)}`],
|
|
4307
|
+
...Object.entries(report.sources ?? {}).map(([name, value]) => ` · source ${name}: ${shapeOf(value)}`),
|
|
4308
|
+
...asked.element === void 0 ? [] : [` · element ${asked.element}: ${JSON.stringify(report.element ?? null)}`]
|
|
4309
|
+
];
|
|
4310
|
+
};
|
|
4311
|
+
/** A value's shape in a few words: a list's length, an object's keys, a short value as it is. */
|
|
4312
|
+
var shapeOf = (value) => {
|
|
4313
|
+
if (Array.isArray(value)) return `a list of ${String(value.length)}`;
|
|
4314
|
+
if (e$1(value)) {
|
|
4315
|
+
const keys = Object.keys(value);
|
|
4316
|
+
return `{ ${keys.slice(0, 8).join(", ")}${keys.length > 8 ? ", …" : ""} }`;
|
|
4317
|
+
}
|
|
4318
|
+
const text = value === void 0 ? "undefined" : JSON.stringify(value);
|
|
4319
|
+
return text.length > 60 ? `${text.slice(0, 57)}…` : text;
|
|
4320
|
+
};
|
|
4321
|
+
var reportText = (report, asked) => {
|
|
4322
|
+
const head = `${report.path} at ${String(report.width)} px`;
|
|
4323
|
+
const held = heldText(report, asked);
|
|
4324
|
+
if (report.ok) return [chalk.green(`✓ ${head} — ${report.checked > 0 ? `${String(report.checked)} elements on screen, ` : ""}nothing wrong`), ...held].join("\n");
|
|
4325
|
+
return [
|
|
4326
|
+
chalk.red(`✗ ${head}`),
|
|
4327
|
+
...report.problems.map((problem) => ` - ${problem}`),
|
|
4328
|
+
...report.consoleErrors.map((error) => ` - console: ${error}`),
|
|
4329
|
+
...report.failedRequests.map((request) => ` - request refused: ${request}`),
|
|
4330
|
+
...held
|
|
4331
|
+
].join("\n");
|
|
4332
|
+
};
|
|
4333
|
+
var check = async (route, options) => {
|
|
4334
|
+
const project = await projectHere("whose page to check");
|
|
4335
|
+
if (!project) return;
|
|
4336
|
+
const plitzi = project.plitzi?.kind === "project" ? project.plitzi : void 0;
|
|
4337
|
+
const where = await projectOrigin(project.root, plitzi);
|
|
4338
|
+
if ("problem" in where) {
|
|
4339
|
+
fail(where.problem);
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
const handles = plitzi?.source === "local" ? await projectHandles(project.root) : void 0;
|
|
4343
|
+
if (handles && "problem" in handles) {
|
|
4344
|
+
fail(handles.problem);
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
const browser = await launchBrowser(project.root);
|
|
4348
|
+
if ("problem" in browser) {
|
|
4349
|
+
fail(browser.problem);
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
try {
|
|
4353
|
+
const widths = options.width ?? [1440, 390];
|
|
4354
|
+
const scheme = options.scheme ?? "light";
|
|
4355
|
+
const asked = {
|
|
4356
|
+
state: Boolean(options.state),
|
|
4357
|
+
...options.element ? { element: options.element } : {}
|
|
4358
|
+
};
|
|
4359
|
+
const reports = [];
|
|
4360
|
+
for (const width of widths) reports.push(await checkAt(browser, where.origin, route ?? "/", width, scheme, handles, asked));
|
|
4361
|
+
console.log(options.json ? JSON.stringify(reports) : reports.map((report) => reportText(report, asked)).join("\n"));
|
|
4362
|
+
if (reports.some((report) => !report.ok)) process.exitCode = 1;
|
|
4363
|
+
} finally {
|
|
4364
|
+
await browser.close();
|
|
4365
|
+
}
|
|
4366
|
+
};
|
|
4367
|
+
//#endregion
|
|
3396
4368
|
//#region ../../packages/sdk-shared/dist/actions/functions.mjs
|
|
3397
4369
|
var r$1 = (e) => /^[A-Za-z0-9_][A-Za-z0-9_./-]*\.(ts|js|mjs|json)$/.test(e) && !e.split("/").some((e) => e === ".." || e === "." || e === "");
|
|
3398
4370
|
var i$1 = async (e, t) => {
|
|
@@ -3414,9 +4386,8 @@ var i$1 = async (e, t) => {
|
|
|
3414
4386
|
//#region src/commands/functions.ts
|
|
3415
4387
|
var FUNCTIONS_DIR = "functions";
|
|
3416
4388
|
var STATE_FILE = path.join(".plitzi", "functions.json");
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
var rootOf = async () => (await findProject(process.cwd()))?.root ?? process.cwd();
|
|
4389
|
+
/** The project's root: where `functions/` and `.plitzi/` go. Undefined — said — outside a project. */
|
|
4390
|
+
var rootOf = async () => (await projectHere("whose functions these are"))?.root;
|
|
3420
4391
|
/** Every source file under `functions/`, by its path there — read by the one rule of what a source is. */
|
|
3421
4392
|
var readLocal = (root) => i$1(path.join(root, FUNCTIONS_DIR), {
|
|
3422
4393
|
list: async (dir) => (await fs.readdir(dir, { withFileTypes: true })).map((entry) => ({
|
|
@@ -3428,7 +4399,7 @@ var readLocal = (root) => i$1(path.join(root, FUNCTIONS_DIR), {
|
|
|
3428
4399
|
var readState = async (root) => {
|
|
3429
4400
|
try {
|
|
3430
4401
|
const value = JSON.parse(await fs.readFile(path.join(root, STATE_FILE), "utf8"));
|
|
3431
|
-
if (!
|
|
4402
|
+
if (!e$1(value) || typeof value.space !== "number" || typeof value.version !== "string" || !e$1(value.files)) return;
|
|
3432
4403
|
const files = Object.fromEntries(Object.entries(value.files).flatMap(([name, text]) => typeof text === "string" ? [[name, text]] : []));
|
|
3433
4404
|
return {
|
|
3434
4405
|
space: value.space,
|
|
@@ -3446,10 +4417,6 @@ var writeFunctionsState = async (root, state) => {
|
|
|
3446
4417
|
/** The files that differ between two copies — added, removed or changed — sorted. */
|
|
3447
4418
|
var changedFiles = (a, b) => [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((file) => a[file] !== b[file]).sort();
|
|
3448
4419
|
var taskList = (draft) => draft.manifest?.tasks.length ? draft.manifest.tasks.map((task) => `${task.namespace}.${task.action}`).join(", ") : "no tasks";
|
|
3449
|
-
var connect$1 = async (options, doing) => {
|
|
3450
|
-
const api = await apiFor(options);
|
|
3451
|
-
return api ? connectionWithSpace(api, doing) : void 0;
|
|
3452
|
-
};
|
|
3453
4420
|
var readDraft = async (connection, spaceId) => {
|
|
3454
4421
|
const answered = await authorizedRequest(connection, `/spaces/${spaceId}/functions`);
|
|
3455
4422
|
if (!answered.ok) {
|
|
@@ -3464,9 +4431,9 @@ var readDraft = async (connection, spaceId) => {
|
|
|
3464
4431
|
return reply.data;
|
|
3465
4432
|
};
|
|
3466
4433
|
var pullFunctions = async (options) => {
|
|
3467
|
-
const connection = await connect$1(options, "to pull from");
|
|
3468
|
-
if (!connection?.space) return;
|
|
3469
4434
|
const root = await rootOf();
|
|
4435
|
+
const connection = root && await connectToSpace(options, "to pull from");
|
|
4436
|
+
if (!root || !connection || !connection.space) return;
|
|
3470
4437
|
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
3471
4438
|
const unpushed = state ? changedFiles(local, state.files) : Object.keys(local);
|
|
3472
4439
|
if (!options.force && (state?.space ?? connection.space.id) !== connection.space.id) {
|
|
@@ -3491,7 +4458,7 @@ var pullFunctions = async (options) => {
|
|
|
3491
4458
|
files: draft.files
|
|
3492
4459
|
});
|
|
3493
4460
|
const count = Object.keys(draft.files).length;
|
|
3494
|
-
console.log(count ? `Pulled ${count} file${count === 1 ? "" : "s"} of ${
|
|
4461
|
+
console.log(count ? chalk.green(`Pulled ${count} file${count === 1 ? "" : "s"} of ${connection.space.name}’s functions into functions/ — ${taskList(draft)}.`) : `${chalk.bold(connection.space.name)} has no functions yet. Write functions/index.ts and plitzi functions push.`);
|
|
3495
4462
|
};
|
|
3496
4463
|
var printProblems = (problems) => {
|
|
3497
4464
|
console.error(chalk.red("The functions were not saved:"));
|
|
@@ -3502,9 +4469,9 @@ var printProblems = (problems) => {
|
|
|
3502
4469
|
process.exitCode = 1;
|
|
3503
4470
|
};
|
|
3504
4471
|
var pushFunctions = async (options) => {
|
|
3505
|
-
const connection = await connect$1(options, "to push to");
|
|
3506
|
-
if (!connection?.space) return;
|
|
3507
4472
|
const root = await rootOf();
|
|
4473
|
+
const connection = root && await connectToSpace(options, "to push to");
|
|
4474
|
+
if (!root || !connection || !connection.space) return;
|
|
3508
4475
|
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
3509
4476
|
if (!Object.hasOwn(local, "index.ts")) {
|
|
3510
4477
|
fail("There is no functions/index.ts here: it is where a space’s functions start. Pull them, or write it.");
|
|
@@ -3554,7 +4521,7 @@ var pushFunctions = async (options) => {
|
|
|
3554
4521
|
version: reply.data.version,
|
|
3555
4522
|
files: local
|
|
3556
4523
|
});
|
|
3557
|
-
console.log(`Pushed to ${
|
|
4524
|
+
console.log(chalk.green(`Pushed to ${connection.space.name}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}.`) + chalk.dim(" The live site runs them once the space is published."));
|
|
3558
4525
|
};
|
|
3559
4526
|
/** What one run of a task did: its logs, then its value — or why it failed, as a failure of the command. */
|
|
3560
4527
|
var printRun = (task, report) => {
|
|
@@ -3572,14 +4539,14 @@ var printRun = (task, report) => {
|
|
|
3572
4539
|
var paramsOf = (given) => {
|
|
3573
4540
|
try {
|
|
3574
4541
|
const params = given ? JSON.parse(given) : {};
|
|
3575
|
-
if (
|
|
4542
|
+
if (e$1(params)) return params;
|
|
3576
4543
|
} catch {}
|
|
3577
4544
|
fail("--params is a JSON object of the task’s params, by name: --params '{\"name\":\"Ada\"}'");
|
|
3578
4545
|
};
|
|
3579
4546
|
var tryFunction = async (task, options) => {
|
|
3580
4547
|
const params = paramsOf(options.params);
|
|
3581
4548
|
if (!params) return;
|
|
3582
|
-
const connection = await
|
|
4549
|
+
const connection = await connectToSpace(options, "to try it in");
|
|
3583
4550
|
if (!connection?.space) return;
|
|
3584
4551
|
const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions/try`, {
|
|
3585
4552
|
method: "POST",
|
|
@@ -3600,7 +4567,7 @@ var tryFunction = async (task, options) => {
|
|
|
3600
4567
|
}
|
|
3601
4568
|
printRun(task, reply.data);
|
|
3602
4569
|
};
|
|
3603
|
-
var isLocalRunner = (value) =>
|
|
4570
|
+
var isLocalRunner = (value) => e$1(value) && typeof value.createLocalFunctions === "function";
|
|
3604
4571
|
/**
|
|
3605
4572
|
* This project's own runner: `dev` runs the functions with the `@plitzi/sdk-server` the project installed, so the build,
|
|
3606
4573
|
* the checks, the isolates and the limits are the platform's own, at the version the project is on.
|
|
@@ -3618,7 +4585,7 @@ var localCredentials = () => {
|
|
|
3618
4585
|
const raw = process.env.PLITZI_FUNCTIONS_CREDENTIALS;
|
|
3619
4586
|
try {
|
|
3620
4587
|
const value = raw ? JSON.parse(raw) : {};
|
|
3621
|
-
return
|
|
4588
|
+
return e$1(value) ? Object.fromEntries(Object.entries(value).flatMap(([id, keys]) => e$1(keys) ? [[id, Object.fromEntries(Object.entries(keys).filter((pair) => typeof pair[1] === "string"))]] : [])) : {};
|
|
3622
4589
|
} catch {
|
|
3623
4590
|
return {};
|
|
3624
4591
|
}
|
|
@@ -3649,9 +4616,9 @@ var devFunction = async (task, options) => {
|
|
|
3649
4616
|
return;
|
|
3650
4617
|
}
|
|
3651
4618
|
const params = paramsOf(options.params);
|
|
3652
|
-
const root = await rootOf();
|
|
3653
|
-
const runner =
|
|
3654
|
-
if (!
|
|
4619
|
+
const root = params && await rootOf();
|
|
4620
|
+
const runner = root ? await projectRunner(root) : void 0;
|
|
4621
|
+
if (!root || !runner) return;
|
|
3655
4622
|
const local = runner.createLocalFunctions({ credentials: localCredentials() });
|
|
3656
4623
|
const run = async () => {
|
|
3657
4624
|
const loaded = await local.load(await readLocal(root));
|
|
@@ -3679,41 +4646,6 @@ var devFunction = async (task, options) => {
|
|
|
3679
4646
|
});
|
|
3680
4647
|
};
|
|
3681
4648
|
//#endregion
|
|
3682
|
-
//#region src/commands/projectFormatter.ts
|
|
3683
|
-
var isRecord$5 = (value) => typeof value === "object" && value !== null;
|
|
3684
|
-
var isPrettier = (value) => isRecord$5(value) && [
|
|
3685
|
-
"format",
|
|
3686
|
-
"resolveConfig",
|
|
3687
|
-
"getFileInfo"
|
|
3688
|
-
].every((name) => typeof value[name] === "function");
|
|
3689
|
-
var unformatted = (_file, text) => Promise.resolve(text);
|
|
3690
|
-
/** Loads the project's Prettier once. A project without one formats nothing. */
|
|
3691
|
-
var projectFormatter = async (root) => {
|
|
3692
|
-
let loaded;
|
|
3693
|
-
try {
|
|
3694
|
-
const resolved = createRequire(path.join(root, "package.json")).resolve("prettier");
|
|
3695
|
-
loaded = await import(pathToFileURL(resolved).href);
|
|
3696
|
-
} catch {
|
|
3697
|
-
return unformatted;
|
|
3698
|
-
}
|
|
3699
|
-
const prettier = isPrettier(loaded) ? loaded : isRecord$5(loaded) ? loaded.default : void 0;
|
|
3700
|
-
if (!isPrettier(prettier)) return unformatted;
|
|
3701
|
-
const api = prettier;
|
|
3702
|
-
const ignorePath = [".prettierignore", ".gitignore"].map((file) => path.join(root, file));
|
|
3703
|
-
return async (file, text) => {
|
|
3704
|
-
const filepath = path.join(root, file);
|
|
3705
|
-
const info = await api.getFileInfo(filepath, {
|
|
3706
|
-
ignorePath,
|
|
3707
|
-
resolveConfig: true
|
|
3708
|
-
});
|
|
3709
|
-
if (info.ignored || !info.inferredParser) return text;
|
|
3710
|
-
return api.format(text, {
|
|
3711
|
-
...await api.resolveConfig(filepath),
|
|
3712
|
-
filepath
|
|
3713
|
-
});
|
|
3714
|
-
};
|
|
3715
|
-
};
|
|
3716
|
-
//#endregion
|
|
3717
4649
|
//#region src/scaffold/fromSpace.ts
|
|
3718
4650
|
var TEXT = /\.((m|c)?(t|j)sx?|json|css|scss|sass|less|md|txt|html|svg|ya?ml|xml|csv|graphql)$/i;
|
|
3719
4651
|
/** Whether a file of the project is text — written, compared and formatted as such — rather than bytes. */
|
|
@@ -4102,8 +5034,7 @@ var projectFromSpace = (exported, spaceSource) => {
|
|
|
4102
5034
|
* copy of the whole project beside the project would be the second source of it nobody keeps in step.
|
|
4103
5035
|
*/
|
|
4104
5036
|
var ORIGIN_FILE = path.join(".plitzi", "space.json");
|
|
4105
|
-
var
|
|
4106
|
-
var isStrings = (value) => isRecord$4(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
5037
|
+
var isStrings = (value) => e$1(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
4107
5038
|
var digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
4108
5039
|
/**
|
|
4109
5040
|
* A file of the project as a pull compares it: the digest of what the project's formatter makes of it — so a file only
|
|
@@ -4127,7 +5058,7 @@ var digestsOf = async (root, file, format) => {
|
|
|
4127
5058
|
var readOrigin = async (root) => {
|
|
4128
5059
|
try {
|
|
4129
5060
|
const value = JSON.parse(await fs.readFile(path.join(root, ORIGIN_FILE), "utf-8"));
|
|
4130
|
-
if (!
|
|
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;
|
|
4131
5062
|
return {
|
|
4132
5063
|
format: 1,
|
|
4133
5064
|
api: value.api,
|
|
@@ -4171,9 +5102,8 @@ var digestsOnDisk = async (root, files, format) => {
|
|
|
4171
5102
|
* `plitzi create --from <space>`: the space as Plitzi keeps it (docs/en/projects-from-spaces.md), asked for as the person — who must be able
|
|
4172
5103
|
* to change it — and the files of it the project serves itself, fetched once.
|
|
4173
5104
|
*/
|
|
4174
|
-
var isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4175
5105
|
/** What the platform answered is an export: of the format this CLI writes, with every part it reads. */
|
|
4176
|
-
var isExport = (value) =>
|
|
5106
|
+
var isExport = (value) => e$1(value) && value.format === 1 && e$1(value.space) && e$1(value.version) && e$1(value.source) && e$1(value.builtOnly) && e$1(value.report) && e$1(value.functions) && Array.isArray(value.actions) && Array.isArray(value.assets) && Array.isArray(value.visitorRoles);
|
|
4177
5107
|
/** How a version is said: `the draft`, `production (latest)`, `production r3`. */
|
|
4178
5108
|
var versionLabel = ({ environment, revision }) => {
|
|
4179
5109
|
if (environment === "main") return "the draft";
|
|
@@ -4194,19 +5124,16 @@ var fetchExport = async (api, from, { source, version, name = from }) => {
|
|
|
4194
5124
|
});
|
|
4195
5125
|
const answered = await authorizedRequest(connection, `/spaces/${encodeURIComponent(from)}/export?${query.toString()}`);
|
|
4196
5126
|
if (!answered.ok) {
|
|
4197
|
-
|
|
4198
|
-
process.exitCode = 1;
|
|
5127
|
+
fail(answered.error);
|
|
4199
5128
|
return;
|
|
4200
5129
|
}
|
|
4201
5130
|
const { status, data } = answered.value.reply;
|
|
4202
5131
|
if (status === 200 && isExport(data)) return data;
|
|
4203
|
-
const said =
|
|
4204
|
-
|
|
5132
|
+
const said = e$1(data) && typeof data.error === "string" ? data.error : void 0;
|
|
5133
|
+
fail({
|
|
4205
5134
|
403: `You may not take ${from} out: it takes being able to change it — its owner, an administrator or a writer.`,
|
|
4206
5135
|
404: said ?? `There is no space ${from} you can reach.`
|
|
4207
|
-
};
|
|
4208
|
-
console.error(chalk.red(why[status] ?? said ?? `The platform would not hand ${from} over (${String(status)}).`));
|
|
4209
|
-
process.exitCode = 1;
|
|
5136
|
+
}[status] ?? said ?? `The platform would not hand ${from} over (${String(status)}).`);
|
|
4210
5137
|
};
|
|
4211
5138
|
/** A file of the space's CDN, fetched. */
|
|
4212
5139
|
var download = async (url) => {
|
|
@@ -4352,20 +5279,17 @@ var create = async (directory, options) => {
|
|
|
4352
5279
|
const name = path.basename(target);
|
|
4353
5280
|
const wasEmpty = await isEmpty(target);
|
|
4354
5281
|
if (!options.force && !wasEmpty) {
|
|
4355
|
-
|
|
4356
|
-
process.exitCode = 1;
|
|
5282
|
+
fail(`${target} is not empty. Pass --force to write into it anyway.`);
|
|
4357
5283
|
return;
|
|
4358
5284
|
}
|
|
4359
5285
|
if (options.from && options.mode === "client") {
|
|
4360
|
-
|
|
4361
|
-
process.exitCode = 1;
|
|
5286
|
+
fail("A project made from a space runs its own server: leave out --mode, or pass --mode server.");
|
|
4362
5287
|
return;
|
|
4363
5288
|
}
|
|
4364
5289
|
const environment = options.environment ?? "main";
|
|
4365
5290
|
const revision = options.revision === void 0 ? void 0 : Number(options.revision);
|
|
4366
5291
|
if (revision !== void 0 && (!Number.isInteger(revision) || revision < 1 || environment === "main")) {
|
|
4367
|
-
|
|
4368
|
-
process.exitCode = 1;
|
|
5292
|
+
fail(environment === "main" ? "The draft (main) has no revisions: name a published environment with --environment, then --revision." : `--revision takes a revision number from 1, not "${options.revision ?? ""}".`);
|
|
4369
5293
|
return;
|
|
4370
5294
|
}
|
|
4371
5295
|
const decisions = await resolveDecisions(options.from ? {
|
|
@@ -4374,9 +5298,13 @@ var create = async (directory, options) => {
|
|
|
4374
5298
|
} : options);
|
|
4375
5299
|
if (!decisions) return;
|
|
4376
5300
|
const { packageManager, mode, source } = decisions;
|
|
5301
|
+
const template = CREATE_TEMPLATES.find((candidate) => candidate === (options.template ?? "welcome"));
|
|
5302
|
+
if (!template || options.template !== void 0 && (source === "cloud" || options.from)) {
|
|
5303
|
+
fail(template ? "--template is what a space written in the project starts as: it goes with --source local, without --from." : `--template is ${CREATE_TEMPLATES.join(" or ")}, not "${options.template ?? ""}".`);
|
|
5304
|
+
return;
|
|
5305
|
+
}
|
|
4377
5306
|
if (revision !== void 0 && !options.from && source === "local") {
|
|
4378
|
-
|
|
4379
|
-
process.exitCode = 1;
|
|
5307
|
+
fail("--revision pins a published version: it goes with --from, or with --source cloud.");
|
|
4380
5308
|
return;
|
|
4381
5309
|
}
|
|
4382
5310
|
if (source === "cloud" && !options.key && !atTerminal()) {
|
|
@@ -4389,14 +5317,14 @@ var create = async (directory, options) => {
|
|
|
4389
5317
|
}
|
|
4390
5318
|
const key = source === "cloud" ? options.key ?? await askForKey(mode) : "";
|
|
4391
5319
|
if (source === "cloud" && !key) {
|
|
4392
|
-
|
|
4393
|
-
process.exitCode = 1;
|
|
5320
|
+
fail("\nNo key. Mint one under Credentials in the builder, then run this again.");
|
|
4394
5321
|
return;
|
|
4395
5322
|
}
|
|
4396
5323
|
const answers = {
|
|
4397
5324
|
name,
|
|
4398
5325
|
mode,
|
|
4399
5326
|
source,
|
|
5327
|
+
...template === "welcome" ? {} : { template },
|
|
4400
5328
|
key,
|
|
4401
5329
|
environment,
|
|
4402
5330
|
...revision ? { revision } : {},
|
|
@@ -4431,9 +5359,8 @@ var create = async (directory, options) => {
|
|
|
4431
5359
|
* `node_modules`.
|
|
4432
5360
|
*/
|
|
4433
5361
|
if (wantsInstall && !installed) {
|
|
4434
|
-
|
|
5362
|
+
fail(`\n\`${installCommand(packageManager)}\` failed. The project is written; the reason is in the output above.`);
|
|
4435
5363
|
console.error(chalk.dim(INSTALL_HINTS[packageManager]));
|
|
4436
|
-
process.exitCode = 1;
|
|
4437
5364
|
}
|
|
4438
5365
|
/**
|
|
4439
5366
|
* Formatted by the project's own Prettier, once, so the first commit is already in its style — the scaffold writes
|
|
@@ -4483,173 +5410,967 @@ var QUESTIONS = {
|
|
|
4483
5410
|
choices: ["<folder>"],
|
|
4484
5411
|
question: "Which folder should the plugin be written to?"
|
|
4485
5412
|
}
|
|
4486
|
-
};
|
|
4487
|
-
var splitNames = (list) => list.split(",").map((name) => name.trim()).filter(Boolean);
|
|
4488
|
-
/**
|
|
4489
|
-
* What is wrong with the other elements asked for, if anything: a name that makes no element, or two elements — the
|
|
4490
|
-
* package's own among them — that would share a type, which a space could not tell apart.
|
|
4491
|
-
*/
|
|
4492
|
-
var elementListProblem = (main, others) => {
|
|
4493
|
-
for (const name of others) {
|
|
4494
|
-
const problem = pluginNameProblem(name);
|
|
4495
|
-
if (problem) return problem;
|
|
5413
|
+
};
|
|
5414
|
+
var splitNames = (list) => list.split(",").map((name) => name.trim()).filter(Boolean);
|
|
5415
|
+
/**
|
|
5416
|
+
* What is wrong with the other elements asked for, if anything: a name that makes no element, or two elements — the
|
|
5417
|
+
* package's own among them — that would share a type, which a space could not tell apart.
|
|
5418
|
+
*/
|
|
5419
|
+
var elementListProblem = (main, others) => {
|
|
5420
|
+
for (const name of others) {
|
|
5421
|
+
const problem = pluginNameProblem(name);
|
|
5422
|
+
if (problem) return problem;
|
|
5423
|
+
}
|
|
5424
|
+
const types = [pluginNames(main).type, ...others.map((name) => pluginNames(name).type)];
|
|
5425
|
+
const repeated = types.find((type, index) => types.indexOf(type) !== index);
|
|
5426
|
+
return repeated ? `Two elements would both be "${repeated}": give each a name of its own.` : void 0;
|
|
5427
|
+
};
|
|
5428
|
+
/** The folders a repository keeps its packages in, as places for this one — or `./<name>` outside of any. */
|
|
5429
|
+
var folderOptions = (project, base) => {
|
|
5430
|
+
if (!project) return [];
|
|
5431
|
+
const root = project.workspaceRoot ?? project.root;
|
|
5432
|
+
return [.../* @__PURE__ */ new Set([...project.workspaceFolders, "plugins"])].map((folder) => {
|
|
5433
|
+
const value = path.join(root, folder, base);
|
|
5434
|
+
return {
|
|
5435
|
+
label: path.relative(process.cwd(), value) || ".",
|
|
5436
|
+
value
|
|
5437
|
+
};
|
|
5438
|
+
});
|
|
5439
|
+
};
|
|
5440
|
+
var askDirectory = async (rl, project, base) => {
|
|
5441
|
+
const typed = {
|
|
5442
|
+
question: "Which folder? (relative to here)",
|
|
5443
|
+
fallback: `./${base}`
|
|
5444
|
+
};
|
|
5445
|
+
if (!project) return path.resolve(await askText(rl, "Which folder should the plugin be written to?", typed.fallback));
|
|
5446
|
+
const chosen = await askChoice(rl, `Which folder of ${path.relative(process.cwd(), project.workspaceRoot ?? project.root) || "this project"} should the plugin go in?`, folderOptions(project, base), {
|
|
5447
|
+
label: "Somewhere else",
|
|
5448
|
+
...typed
|
|
5449
|
+
});
|
|
5450
|
+
return path.resolve(chosen);
|
|
5451
|
+
};
|
|
5452
|
+
/**
|
|
5453
|
+
* The manager: the one the repository already installs with, when it is written into one — asking would only offer a
|
|
5454
|
+
* second lockfile — and otherwise asked, as a project's is.
|
|
5455
|
+
*/
|
|
5456
|
+
var resolveManager = async (rl, options, project) => {
|
|
5457
|
+
const given = PACKAGE_MANAGERS.find((manager) => manager === options.packageManager);
|
|
5458
|
+
if (given ?? project?.packageManager) return given ?? project?.packageManager;
|
|
5459
|
+
const detected = detectPackageManager();
|
|
5460
|
+
if (!rl || options.yes) return options.yes ? detected : void 0;
|
|
5461
|
+
return ask(rl, {
|
|
5462
|
+
flag: "--package-manager",
|
|
5463
|
+
choices: PACKAGE_MANAGERS,
|
|
5464
|
+
question: `Which package manager will you work in? (this was run through ${detected})`,
|
|
5465
|
+
fallback: detected
|
|
5466
|
+
});
|
|
5467
|
+
};
|
|
5468
|
+
var createPlugin = async (directory, options) => {
|
|
5469
|
+
const nameGiven = options.name ?? (directory ? path.basename(path.resolve(directory)) : void 0);
|
|
5470
|
+
const othersGiven = options.elements === void 0 ? void 0 : splitNames(options.elements);
|
|
5471
|
+
const nameProblem = nameGiven === void 0 ? void 0 : pluginNameProblem(nameGiven) ?? elementListProblem(pluginNames(nameGiven).base, othersGiven ?? []);
|
|
5472
|
+
if (nameProblem) {
|
|
5473
|
+
fail(nameProblem);
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
5476
|
+
const here = await findProject(process.cwd());
|
|
5477
|
+
const terminal = atTerminal();
|
|
5478
|
+
if (!terminal && (nameGiven === void 0 || directory === void 0)) {
|
|
5479
|
+
refuseWithoutTerminal("plugin", [...nameGiven === void 0 ? [QUESTIONS.name] : [], ...directory === void 0 ? [QUESTIONS.directory] : []]);
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
const rl = terminal ? readline.createInterface({
|
|
5483
|
+
input: process.stdin,
|
|
5484
|
+
output: process.stdout
|
|
5485
|
+
}) : void 0;
|
|
5486
|
+
try {
|
|
5487
|
+
/**
|
|
5488
|
+
* Everything the plugin is called and says about itself, asked before a file is written — the package name, what
|
|
5489
|
+
* the builder's catalogue shows, what an agent reads to choose it, who publishes it. Each has a default, taken
|
|
5490
|
+
* whole with Enter; only the name and the folder are ever refused for want of an answer.
|
|
5491
|
+
*/
|
|
5492
|
+
const name = nameGiven ?? (rl ? await askText(rl, QUESTIONS.name.question, DEFAULT_NAME, pluginNameProblem) : DEFAULT_NAME);
|
|
5493
|
+
const names = pluginNames(name);
|
|
5494
|
+
const answer = async (question, given, fallback) => given ?? (rl && !options.yes ? askText(rl, question, fallback) : fallback);
|
|
5495
|
+
const title = await answer("What does the builder call the element?", options.title, names.title);
|
|
5496
|
+
const description = await answer("What is it for, in a sentence? The builder shows it, and an agent reads it to choose the element.", options.description, "");
|
|
5497
|
+
const owner = await answer("Who publishes it?", options.owner, "");
|
|
5498
|
+
const others = othersGiven ?? (rl && !options.yes ? splitNames(await askText(rl, "Any other elements in this package? Their names, separated by commas.", "", (reply) => elementListProblem(names.base, splitNames(reply)))) : []);
|
|
5499
|
+
const otherElements = [];
|
|
5500
|
+
for (const other of others) {
|
|
5501
|
+
const otherNames = pluginNames(other);
|
|
5502
|
+
otherElements.push({
|
|
5503
|
+
name: other,
|
|
5504
|
+
title: await answer(`What does the builder call ${other}?`, void 0, otherNames.title),
|
|
5505
|
+
description: await answer(`What is ${other} for, in a sentence?`, void 0, "")
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
const target = directory ? path.resolve(directory) : rl ? await askDirectory(rl, here, names.base) : void 0;
|
|
5509
|
+
if (!target) return;
|
|
5510
|
+
const wasEmpty = await isEmpty(target);
|
|
5511
|
+
if (!options.force && !wasEmpty) {
|
|
5512
|
+
fail(`${target} is not empty. Pass --force to write into it anyway.`);
|
|
5513
|
+
return;
|
|
5514
|
+
}
|
|
5515
|
+
const project = await findProject(path.dirname(target));
|
|
5516
|
+
const packageManager = await resolveManager(rl, options, project);
|
|
5517
|
+
if (!packageManager) {
|
|
5518
|
+
if (!rl) refuseWithoutTerminal("plugin", [{
|
|
5519
|
+
flag: "--package-manager",
|
|
5520
|
+
choices: PACKAGE_MANAGERS,
|
|
5521
|
+
question: "Which package manager will you work in?"
|
|
5522
|
+
}]);
|
|
5523
|
+
process.exitCode = 1;
|
|
5524
|
+
return;
|
|
5525
|
+
}
|
|
5526
|
+
await writeFiles(target, scaffoldPlugin({
|
|
5527
|
+
packageName: name,
|
|
5528
|
+
elements: [{
|
|
5529
|
+
name: names.base,
|
|
5530
|
+
title,
|
|
5531
|
+
description
|
|
5532
|
+
}, ...otherElements],
|
|
5533
|
+
owner,
|
|
5534
|
+
packageManager,
|
|
5535
|
+
managerVersion: detectManagerVersion(packageManager, await nearestExisting(target)),
|
|
5536
|
+
inProject: project !== void 0
|
|
5537
|
+
}));
|
|
5538
|
+
/**
|
|
5539
|
+
* Installed where it will be installed from. Inside a workspace that covers the folder, installing there installs
|
|
5540
|
+
* the workspace; inside a repository that does not list it, Yarn refuses to install a package it does not know —
|
|
5541
|
+
* so that is said, with the fix, rather than failed at.
|
|
5542
|
+
*/
|
|
5543
|
+
const unlisted = project?.workspaceRoot !== void 0 && !coveredByWorkspace(project, target);
|
|
5544
|
+
const wantsInstall = options.install !== false && !unlisted;
|
|
5545
|
+
const installed = wantsInstall && await install(packageManager, target);
|
|
5546
|
+
if (wantsInstall && !installed) {
|
|
5547
|
+
fail(`\n\`${installCommand(packageManager)}\` failed. The plugin is written; the reason is in the output above.`);
|
|
5548
|
+
console.error(chalk.dim(INSTALL_HINTS[packageManager]));
|
|
5549
|
+
}
|
|
5550
|
+
if (installed && wasEmpty) await runScript(packageManager, "format", target);
|
|
5551
|
+
const where = cdPrefix(target);
|
|
5552
|
+
console.log(chalk.green(`\n${name} — the ${names.type} element, as a package any space can load.`));
|
|
5553
|
+
if (unlisted) console.log(chalk.yellow(`\n${path.relative(process.cwd(), target)} is not one of the workspace's packages: add it to \`workspaces\`, then install.`));
|
|
5554
|
+
console.log("");
|
|
5555
|
+
if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
|
|
5556
|
+
console.log(` ${where}${runCommand(packageManager, "start")} # the plugin inside a space`);
|
|
5557
|
+
console.log(` ${where}${runCommand(packageManager, "zip")} # what the builder takes, under Resources`);
|
|
5558
|
+
console.log("");
|
|
5559
|
+
} finally {
|
|
5560
|
+
rl?.close();
|
|
5561
|
+
}
|
|
5562
|
+
};
|
|
5563
|
+
//#endregion
|
|
5564
|
+
//#region src/commands/data.ts
|
|
5565
|
+
var emptyShape = () => ({ primitives: /* @__PURE__ */ new Set() });
|
|
5566
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5567
|
+
var merge = (shape, value) => {
|
|
5568
|
+
if (value === null) {
|
|
5569
|
+
shape.primitives.add("null");
|
|
5570
|
+
return;
|
|
5571
|
+
}
|
|
5572
|
+
if (Array.isArray(value)) {
|
|
5573
|
+
shape.array ??= {
|
|
5574
|
+
occurrences: 0,
|
|
5575
|
+
lengths: /* @__PURE__ */ new Set(),
|
|
5576
|
+
item: emptyShape()
|
|
5577
|
+
};
|
|
5578
|
+
shape.array.occurrences += 1;
|
|
5579
|
+
shape.array.lengths.add(value.length);
|
|
5580
|
+
for (const item of value) merge(shape.array.item, item);
|
|
5581
|
+
return;
|
|
5582
|
+
}
|
|
5583
|
+
if (isPlainObject(value)) {
|
|
5584
|
+
shape.object ??= {
|
|
5585
|
+
count: 0,
|
|
5586
|
+
fields: /* @__PURE__ */ new Map()
|
|
5587
|
+
};
|
|
5588
|
+
shape.object.count += 1;
|
|
5589
|
+
for (const [key, field] of Object.entries(value)) {
|
|
5590
|
+
const entry = shape.object.fields.get(key) ?? {
|
|
5591
|
+
count: 0,
|
|
5592
|
+
shape: emptyShape()
|
|
5593
|
+
};
|
|
5594
|
+
entry.count += 1;
|
|
5595
|
+
merge(entry.shape, field);
|
|
5596
|
+
shape.object.fields.set(key, entry);
|
|
5597
|
+
}
|
|
5598
|
+
return;
|
|
5599
|
+
}
|
|
5600
|
+
shape.primitives.add(typeof value);
|
|
5601
|
+
};
|
|
5602
|
+
/** Past this many fields an object is summarised: a map keyed by ids is data, not a shape. */
|
|
5603
|
+
var MAX_FIELDS = 40;
|
|
5604
|
+
var PAD = " ";
|
|
5605
|
+
var KEY = /^[A-Za-z_$][\w$]*$/;
|
|
5606
|
+
var render = (shape, depth) => {
|
|
5607
|
+
const parts = [...shape.primitives].sort();
|
|
5608
|
+
if (shape.object) parts.push(renderObject(shape.object, depth));
|
|
5609
|
+
if (shape.array) parts.push(renderArray(shape.array, depth));
|
|
5610
|
+
return parts.length > 0 ? parts.join(" | ") : "never";
|
|
5611
|
+
};
|
|
5612
|
+
var renderArray = ({ occurrences, lengths, item }, depth) => {
|
|
5613
|
+
const inner = render(item, depth);
|
|
5614
|
+
if (occurrences === 1) {
|
|
5615
|
+
const [length] = lengths;
|
|
5616
|
+
return length === 0 ? "Array(0)" : `Array(${String(length)}) of ${inner}`;
|
|
5617
|
+
}
|
|
5618
|
+
return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
|
|
5619
|
+
};
|
|
5620
|
+
var renderObject = ({ count, fields }, depth) => {
|
|
5621
|
+
if (fields.size === 0) return "{}";
|
|
5622
|
+
const indent = PAD.repeat(depth + 1);
|
|
5623
|
+
const entries = [...fields].slice(0, MAX_FIELDS).map(([key, field]) => {
|
|
5624
|
+
const name = KEY.test(key) ? key : JSON.stringify(key);
|
|
5625
|
+
const optional = field.count < count;
|
|
5626
|
+
const seen = optional && count > 1 ? ` (in ${String(field.count)} of ${String(count)})` : "";
|
|
5627
|
+
return `${indent}${name}${optional ? "?" : ""}: ${render(field.shape, depth + 1)}${seen}`;
|
|
5628
|
+
});
|
|
5629
|
+
const more = fields.size > MAX_FIELDS ? [`${indent}… ${String(fields.size - MAX_FIELDS)} more keys`] : [];
|
|
5630
|
+
return [
|
|
5631
|
+
"{",
|
|
5632
|
+
...entries,
|
|
5633
|
+
...more,
|
|
5634
|
+
`${PAD.repeat(depth)}}`
|
|
5635
|
+
].join("\n");
|
|
5636
|
+
};
|
|
5637
|
+
/** The longest list of objects in the document, and where it is — the rows a page most likely renders. */
|
|
5638
|
+
var rowsOf = (value) => {
|
|
5639
|
+
const candidates = [];
|
|
5640
|
+
const visit = (node, at) => {
|
|
5641
|
+
if (Array.isArray(node)) {
|
|
5642
|
+
if (node.some(isPlainObject)) candidates.push({
|
|
5643
|
+
path: at,
|
|
5644
|
+
rows: node
|
|
5645
|
+
});
|
|
5646
|
+
node.forEach((item, index) => visit(item, `${at}[${String(index)}]`));
|
|
5647
|
+
return;
|
|
5648
|
+
}
|
|
5649
|
+
if (isPlainObject(node)) for (const [key, field] of Object.entries(node)) visit(field, at ? `${at}.${key}` : key);
|
|
5650
|
+
};
|
|
5651
|
+
visit(value, "");
|
|
5652
|
+
return candidates.sort((a, b) => b.rows.length - a.rows.length)[0];
|
|
5653
|
+
};
|
|
5654
|
+
/** One row, shortened: long text cut, long lists cut to their first items. */
|
|
5655
|
+
var shorten = (value) => {
|
|
5656
|
+
if (typeof value === "string") return value.length > 80 ? `${value.slice(0, 77)}…` : value;
|
|
5657
|
+
if (Array.isArray(value)) return value.length > 3 ? [...value.slice(0, 3).map(shorten), `… ${String(value.length - 3)} more`] : value.map(shorten);
|
|
5658
|
+
if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, field]) => [key, shorten(field)]));
|
|
5659
|
+
return value;
|
|
5660
|
+
};
|
|
5661
|
+
var describeData = (value) => {
|
|
5662
|
+
const shape = emptyShape();
|
|
5663
|
+
merge(shape, value);
|
|
5664
|
+
const rows = rowsOf(value);
|
|
5665
|
+
const first = rows?.rows.find(isPlainObject);
|
|
5666
|
+
return {
|
|
5667
|
+
shape: render(shape, 0),
|
|
5668
|
+
...rows && first ? { example: {
|
|
5669
|
+
path: rows.path || "(the document)",
|
|
5670
|
+
row: shorten(first)
|
|
5671
|
+
} } : {}
|
|
5672
|
+
};
|
|
5673
|
+
};
|
|
5674
|
+
var dataDescribe = async (file, options) => {
|
|
5675
|
+
let value;
|
|
5676
|
+
try {
|
|
5677
|
+
value = JSON.parse(await fs.readFile(file, "utf-8"));
|
|
5678
|
+
} catch (error) {
|
|
5679
|
+
fail(`${file} is not a JSON file this can read: ${error instanceof Error ? error.message : String(error)}`);
|
|
5680
|
+
return;
|
|
5681
|
+
}
|
|
5682
|
+
const description = describeData(value);
|
|
5683
|
+
if (options.json) {
|
|
5684
|
+
console.log(JSON.stringify(description));
|
|
5685
|
+
return;
|
|
5686
|
+
}
|
|
5687
|
+
console.log(description.shape);
|
|
5688
|
+
if (description.example) {
|
|
5689
|
+
console.log(`\n${chalk.dim(`One row of ${description.example.path}:`)}`);
|
|
5690
|
+
console.log(JSON.stringify(description.example.row, null, 2));
|
|
5691
|
+
}
|
|
5692
|
+
};
|
|
5693
|
+
//#endregion
|
|
5694
|
+
//#region src/commands/explain.ts
|
|
5695
|
+
var kinds = Object.values(EXPLAIN_KINDS).join(", ");
|
|
5696
|
+
var explainCommand = (name, options) => {
|
|
5697
|
+
if (options.list !== void 0) {
|
|
5698
|
+
const kind = explainKindOf(options.list);
|
|
5699
|
+
if (!kind) {
|
|
5700
|
+
fail(`--list takes ${kinds}, not "${options.list}".`);
|
|
5701
|
+
return;
|
|
5702
|
+
}
|
|
5703
|
+
const entries = explainList(kind);
|
|
5704
|
+
console.log(options.json ? JSON.stringify(entries) : entries.map((entry) => `${entry.name.padEnd(32)} ${chalk.dim(entry.summary)}`).join("\n"));
|
|
5705
|
+
return;
|
|
5706
|
+
}
|
|
5707
|
+
if (!name) {
|
|
5708
|
+
fail(`Name what to explain — plitzi explain container — or list a kind: --list ${kinds}.`);
|
|
5709
|
+
return;
|
|
5710
|
+
}
|
|
5711
|
+
const explanations = explain(name);
|
|
5712
|
+
if (explanations.length === 0) {
|
|
5713
|
+
fail(`"${name}" is no element, step, trigger, code or transformer. See what there is: --list ${kinds}.`);
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
console.log(options.json ? JSON.stringify(explanations) : explanations.map(explanationText).join("\n\n"));
|
|
5717
|
+
};
|
|
5718
|
+
//#endregion
|
|
5719
|
+
//#region src/fix/diff.ts
|
|
5720
|
+
/** A line diff of a file, as `diff -u` prints it: the changed lines and two of context around each run of them. */
|
|
5721
|
+
var unifiedDiff = (file, before, after, context = 2) => {
|
|
5722
|
+
const a = before.split("\n");
|
|
5723
|
+
const b = after.split("\n");
|
|
5724
|
+
const common = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
5725
|
+
for (let i = a.length - 1; i >= 0; i -= 1) for (let j = b.length - 1; j >= 0; j -= 1) common[i][j] = a[i] === b[j] ? common[i + 1][j + 1] + 1 : Math.max(common[i + 1][j], common[i][j + 1]);
|
|
5726
|
+
const lines = [];
|
|
5727
|
+
let i = 0;
|
|
5728
|
+
let j = 0;
|
|
5729
|
+
while (i < a.length || j < b.length) if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
5730
|
+
lines.push({
|
|
5731
|
+
kind: " ",
|
|
5732
|
+
text: a[i],
|
|
5733
|
+
at: i + 1
|
|
5734
|
+
});
|
|
5735
|
+
i += 1;
|
|
5736
|
+
j += 1;
|
|
5737
|
+
} else if (i < a.length && (j === b.length || common[i + 1][j] >= common[i][j + 1])) {
|
|
5738
|
+
lines.push({
|
|
5739
|
+
kind: "-",
|
|
5740
|
+
text: a[i],
|
|
5741
|
+
at: i + 1
|
|
5742
|
+
});
|
|
5743
|
+
i += 1;
|
|
5744
|
+
} else {
|
|
5745
|
+
lines.push({
|
|
5746
|
+
kind: "+",
|
|
5747
|
+
text: b[j],
|
|
5748
|
+
at: i + 1
|
|
5749
|
+
});
|
|
5750
|
+
j += 1;
|
|
5751
|
+
}
|
|
5752
|
+
const keep = lines.map((_line, index) => lines.slice(Math.max(0, index - context), index + context + 1).some((near) => near.kind !== " "));
|
|
5753
|
+
const hunks = [];
|
|
5754
|
+
let open = false;
|
|
5755
|
+
lines.forEach((line, index) => {
|
|
5756
|
+
if (!keep[index]) {
|
|
5757
|
+
open = false;
|
|
5758
|
+
return;
|
|
5759
|
+
}
|
|
5760
|
+
if (!open) {
|
|
5761
|
+
hunks.push(`@@ line ${String(line.at)} @@`);
|
|
5762
|
+
open = true;
|
|
5763
|
+
}
|
|
5764
|
+
hunks.push(`${line.kind}${line.text}`);
|
|
5765
|
+
});
|
|
5766
|
+
return hunks.length === 0 ? "" : [
|
|
5767
|
+
`--- ${file}`,
|
|
5768
|
+
`+++ ${file}`,
|
|
5769
|
+
...hunks
|
|
5770
|
+
].join("\n");
|
|
5771
|
+
};
|
|
5772
|
+
//#endregion
|
|
5773
|
+
//#region src/fix/format.ts
|
|
5774
|
+
/**
|
|
5775
|
+
* The edited text formatted as the project formats it — when the file was formatted that way before the edit. One the
|
|
5776
|
+
* project does not keep formatted is written as it is, so the change is the edit and nothing else.
|
|
5777
|
+
*/
|
|
5778
|
+
var formatLikeBefore = async (format, file, before, after) => {
|
|
5779
|
+
try {
|
|
5780
|
+
return await format(file, before) === before ? await format(file, after) : after;
|
|
5781
|
+
} catch {
|
|
5782
|
+
return after;
|
|
5783
|
+
}
|
|
5784
|
+
};
|
|
5785
|
+
//#endregion
|
|
5786
|
+
//#region src/fix/plan.ts
|
|
5787
|
+
/** A fix by what it is, to tell whether it is still to be made after the source was edited. */
|
|
5788
|
+
var fixKey = (fix) => `${fix.code} ${fix.elementId ?? ""} ${fix.message}`;
|
|
5789
|
+
/** A problem by its code and its element: the message may change with a fix, the problem is the same problem. */
|
|
5790
|
+
var problemKey = (problem) => `${problem.code} ${problem.elementId ?? ""}`;
|
|
5791
|
+
var projectPlan = ({ space, plugins }) => {
|
|
5792
|
+
try {
|
|
5793
|
+
return planFixes(space, { plugins });
|
|
5794
|
+
} catch (error) {
|
|
5795
|
+
return { problem: `the space is not written as far as its documents — ${refusalOf(error).message.split("\n")[0]}` };
|
|
5796
|
+
}
|
|
5797
|
+
};
|
|
5798
|
+
/**
|
|
5799
|
+
* Why the fixes written are not to be kept, by element: a fix still there to make, a problem that was not there
|
|
5800
|
+
* before. An element named is one whose fixes go back to the author; a reason with no element blames them all.
|
|
5801
|
+
*/
|
|
5802
|
+
var verdict = (before, written, after) => {
|
|
5803
|
+
if ("problem" in after) return {
|
|
5804
|
+
reasons: [`the space no longer authors: ${after.problem}`],
|
|
5805
|
+
elements: /* @__PURE__ */ new Set([null])
|
|
5806
|
+
};
|
|
5807
|
+
const still = new Set(after.fixes.map(fixKey));
|
|
5808
|
+
const known = new Set(before.problems.map(problemKey));
|
|
5809
|
+
const stuck = written.filter((fix) => still.has(fixKey(fix)));
|
|
5810
|
+
const added = after.problems.filter((problem) => !known.has(problemKey(problem)));
|
|
5811
|
+
return {
|
|
5812
|
+
reasons: [...stuck.map((fix) => `still to fix: [${fix.code}] ${fix.message}`), ...added.map((problem) => `new problem: [${problem.code}] ${problem.message}`)],
|
|
5813
|
+
elements: /* @__PURE__ */ new Set([...stuck.map((fix) => fix.elementId), ...added.map((problem) => problem.elementId)])
|
|
5814
|
+
};
|
|
5815
|
+
};
|
|
5816
|
+
//#endregion
|
|
5817
|
+
//#region src/fix/sourceEdits.ts
|
|
5818
|
+
var SINGLE = "'";
|
|
5819
|
+
var quoted = (text) => `${SINGLE}${text.replace(/\\/g, "\\\\").replaceAll(SINGLE, `\\${SINGLE}`)}${SINGLE}`;
|
|
5820
|
+
var literalText = (value) => typeof value === "string" ? quoted(value) : String(value);
|
|
5821
|
+
var keyText = (key) => /^[A-Za-z_$][\w$]*$/.test(key) ? key : quoted(key);
|
|
5822
|
+
/** A property's name as written: `mode`, `'aria-label'`. */
|
|
5823
|
+
var nameOf = (ts, property) => {
|
|
5824
|
+
const name = property.name;
|
|
5825
|
+
if (!name) return;
|
|
5826
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) ? name.text : void 0;
|
|
5827
|
+
};
|
|
5828
|
+
var findProperty = (ts, object, key) => object.properties.find((property) => nameOf(ts, property) === key);
|
|
5829
|
+
/** The object literal a property holds, when it holds one: `attributes: { … }`, `params: { … }`. */
|
|
5830
|
+
var objectAt = (ts, object, key) => {
|
|
5831
|
+
const property = findProperty(ts, object, key);
|
|
5832
|
+
return property && ts.isPropertyAssignment(property) && ts.isObjectLiteralExpression(property.initializer) ? property.initializer : void 0;
|
|
5833
|
+
};
|
|
5834
|
+
/** One item of a comma list taken out, with the comma that went with it. */
|
|
5835
|
+
var removeFromList = (sourceFile, items, index) => {
|
|
5836
|
+
const item = items[index];
|
|
5837
|
+
if (index < items.length - 1) return {
|
|
5838
|
+
start: item.getStart(sourceFile),
|
|
5839
|
+
end: items[index + 1].getStart(sourceFile),
|
|
5840
|
+
text: ""
|
|
5841
|
+
};
|
|
5842
|
+
if (index > 0) return {
|
|
5843
|
+
start: items[index - 1].getEnd(),
|
|
5844
|
+
end: item.getEnd(),
|
|
5845
|
+
text: ""
|
|
5846
|
+
};
|
|
5847
|
+
return {
|
|
5848
|
+
start: item.getStart(sourceFile),
|
|
5849
|
+
end: item.getEnd(),
|
|
5850
|
+
text: ""
|
|
5851
|
+
};
|
|
5852
|
+
};
|
|
5853
|
+
/** The call written at the factory's name: `container(` at that line and column, from 1. */
|
|
5854
|
+
var callAt = (ts, sourceFile, line, column) => {
|
|
5855
|
+
let found;
|
|
5856
|
+
const visit = (node) => {
|
|
5857
|
+
if (found) return;
|
|
5858
|
+
if (ts.isCallExpression(node)) {
|
|
5859
|
+
const callee = ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression;
|
|
5860
|
+
const at = sourceFile.getLineAndCharacterOfPosition(callee.getStart(sourceFile));
|
|
5861
|
+
if (at.line + 1 === line && at.character + 1 === column) {
|
|
5862
|
+
found = node;
|
|
5863
|
+
return;
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
ts.forEachChild(node, visit);
|
|
5867
|
+
};
|
|
5868
|
+
visit(sourceFile);
|
|
5869
|
+
return found;
|
|
5870
|
+
};
|
|
5871
|
+
/** The props a factory call was handed: its last object literal — `text('Hi', { … })`, `container({ … })`. */
|
|
5872
|
+
var propsOf = (ts, call) => call.arguments.filter(ts.isObjectLiteralExpression).at(-1);
|
|
5873
|
+
/**
|
|
5874
|
+
* The object a step was written with: itself when it is a literal, a builder's own props (`setState({ … })`), or —
|
|
5875
|
+
* through a wrapper (`named('x', …)`, `when(rule, …)`) — the step it wraps, which is its last argument.
|
|
5876
|
+
*/
|
|
5877
|
+
var stepObject = (ts, expression) => {
|
|
5878
|
+
if (ts.isObjectLiteralExpression(expression)) return expression;
|
|
5879
|
+
if (!ts.isCallExpression(expression)) return;
|
|
5880
|
+
const last = expression.arguments.at(-1);
|
|
5881
|
+
if (last && ts.isCallExpression(last)) return stepObject(ts, last);
|
|
5882
|
+
return expression.arguments.filter(ts.isObjectLiteralExpression).at(-1);
|
|
5883
|
+
};
|
|
5884
|
+
var stepIn = (ts, props, flow, index) => {
|
|
5885
|
+
const flows = findProperty(ts, props, "flows");
|
|
5886
|
+
if (!flows || !ts.isPropertyAssignment(flows) || !ts.isArrayLiteralExpression(flows.initializer)) return;
|
|
5887
|
+
const steps = flows.initializer.elements.at(flow);
|
|
5888
|
+
const step = steps && ts.isArrayLiteralExpression(steps) ? steps.elements.at(index) : void 0;
|
|
5889
|
+
return step ? stepObject(ts, step) : void 0;
|
|
5890
|
+
};
|
|
5891
|
+
var isLiteral = (ts, node) => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword;
|
|
5892
|
+
/** `key: value` added to an object literal, after what it already holds. */
|
|
5893
|
+
var insertInto = (sourceFile, object, key, value) => {
|
|
5894
|
+
const last = object.properties.at(-1);
|
|
5895
|
+
if (!last) return {
|
|
5896
|
+
start: object.getStart(sourceFile),
|
|
5897
|
+
end: object.getEnd(),
|
|
5898
|
+
text: `{ ${keyText(key)}: ${value} }`
|
|
5899
|
+
};
|
|
5900
|
+
return {
|
|
5901
|
+
start: last.getEnd(),
|
|
5902
|
+
end: last.getEnd(),
|
|
5903
|
+
text: `, ${keyText(key)}: ${value}`
|
|
5904
|
+
};
|
|
5905
|
+
};
|
|
5906
|
+
/** An edit to one key of `object` — or of what it nests the key in (`attributes`, `params`). */
|
|
5907
|
+
var editKey = (ts, sourceFile, object, nested, edit) => {
|
|
5908
|
+
const inner = nested === void 0 ? void 0 : objectAt(ts, object, nested);
|
|
5909
|
+
const holder = findProperty(ts, object, edit.key) ? object : inner && findProperty(ts, inner, edit.key) ? inner : void 0;
|
|
5910
|
+
const property = holder ? findProperty(ts, holder, edit.key) : void 0;
|
|
5911
|
+
if (edit.op === "set") {
|
|
5912
|
+
if (edit.value === void 0) return { unplaced: "there is no value to write" };
|
|
5913
|
+
if (!holder || !property) return { change: insertInto(sourceFile, inner ?? object, edit.key, literalText(edit.value)) };
|
|
5914
|
+
if (!ts.isPropertyAssignment(property) || !isLiteral(ts, property.initializer)) return { unplaced: `\`${edit.key}\` is not written as a value there` };
|
|
5915
|
+
return { change: {
|
|
5916
|
+
start: property.initializer.getStart(sourceFile),
|
|
5917
|
+
end: property.initializer.getEnd(),
|
|
5918
|
+
text: literalText(edit.value)
|
|
5919
|
+
} };
|
|
5920
|
+
}
|
|
5921
|
+
if (!holder || !property) return { unplaced: `\`${edit.key}\` is not written in that call` };
|
|
5922
|
+
if (edit.op === "remove") return { change: removeFromList(sourceFile, holder.properties, holder.properties.indexOf(property)) };
|
|
5923
|
+
if (edit.op === "rename" && edit.to !== void 0) {
|
|
5924
|
+
if (ts.isShorthandPropertyAssignment(property)) return { change: {
|
|
5925
|
+
start: property.getStart(sourceFile),
|
|
5926
|
+
end: property.getEnd(),
|
|
5927
|
+
text: `${keyText(edit.to)}: ${property.name.text}`
|
|
5928
|
+
} };
|
|
5929
|
+
if (ts.isPropertyAssignment(property)) return { change: {
|
|
5930
|
+
start: property.name.getStart(sourceFile),
|
|
5931
|
+
end: property.name.getEnd(),
|
|
5932
|
+
text: keyText(edit.to)
|
|
5933
|
+
} };
|
|
5934
|
+
}
|
|
5935
|
+
return { unplaced: `\`${edit.key}\` is not a property that edit applies to` };
|
|
5936
|
+
};
|
|
5937
|
+
/** A binding by its target: a key of `bind: { … }`, or the entry of `bind: [ … ]` whose `to` it is. */
|
|
5938
|
+
var editBinding = (ts, sourceFile, props, edit) => {
|
|
5939
|
+
const bind = findProperty(ts, props, "bind");
|
|
5940
|
+
if (!bind || !ts.isPropertyAssignment(bind)) return { unplaced: "the binding is not written in that call" };
|
|
5941
|
+
const list = bind.initializer;
|
|
5942
|
+
if (edit.op === "replace" && edit.to !== void 0) {
|
|
5943
|
+
let found;
|
|
5944
|
+
const visit = (node) => {
|
|
5945
|
+
if (!found && ts.isStringLiteral(node) && node.text === edit.key && ts.isPropertyAssignment(node.parent) && nameOf(ts, node.parent) === "action") found = node;
|
|
5946
|
+
ts.forEachChild(node, visit);
|
|
5947
|
+
};
|
|
5948
|
+
visit(list);
|
|
5949
|
+
return found ? { change: {
|
|
5950
|
+
start: found.getStart(sourceFile),
|
|
5951
|
+
end: found.getEnd(),
|
|
5952
|
+
text: quoted(edit.to)
|
|
5953
|
+
} } : { unplaced: `"${edit.key}" is not written as a transformer there` };
|
|
5954
|
+
}
|
|
5955
|
+
if (edit.op !== "remove") return { unplaced: "that change to a binding has no one way to be written" };
|
|
5956
|
+
const dropBind = () => ({ change: removeFromList(sourceFile, props.properties, props.properties.indexOf(bind)) });
|
|
5957
|
+
if (ts.isObjectLiteralExpression(list)) {
|
|
5958
|
+
const property = findProperty(ts, list, edit.key);
|
|
5959
|
+
if (!property) return { unplaced: `the binding onto "${edit.key}" is not written there` };
|
|
5960
|
+
return list.properties.length === 1 ? dropBind() : { change: removeFromList(sourceFile, list.properties, list.properties.indexOf(property)) };
|
|
5961
|
+
}
|
|
5962
|
+
if (ts.isArrayLiteralExpression(list)) {
|
|
5963
|
+
const index = list.elements.findIndex((entry) => {
|
|
5964
|
+
if (!ts.isObjectLiteralExpression(entry)) return false;
|
|
5965
|
+
const to = findProperty(ts, entry, "to");
|
|
5966
|
+
return to !== void 0 && ts.isPropertyAssignment(to) && ts.isStringLiteral(to.initializer) && to.initializer.text === edit.key;
|
|
5967
|
+
});
|
|
5968
|
+
if (index === -1) return { unplaced: `the binding onto "${edit.key}" is not written there` };
|
|
5969
|
+
return list.elements.length === 1 ? dropBind() : { change: removeFromList(sourceFile, list.elements, index) };
|
|
5970
|
+
}
|
|
5971
|
+
return { unplaced: "`bind` is not written as a literal there" };
|
|
5972
|
+
};
|
|
5973
|
+
/**
|
|
5974
|
+
* What an edit planned on a spec changes in the source that wrote it: the call at its position, the props it was
|
|
5975
|
+
* handed, and in them only what is written as a literal. Anything else — a value computed, props spread from a
|
|
5976
|
+
* variable, a flow built elsewhere — is left as it was and said, for the author.
|
|
5977
|
+
*/
|
|
5978
|
+
var sourceEdit = (ts, sourceFile, position, edit) => {
|
|
5979
|
+
const call = callAt(ts, sourceFile, position.line, position.column);
|
|
5980
|
+
if (!call) return { unplaced: "the call that wrote it is not where it was" };
|
|
5981
|
+
const props = propsOf(ts, call);
|
|
5982
|
+
if (!props) return { unplaced: "the call that wrote it was handed no props to edit" };
|
|
5983
|
+
if (edit.on === "binding") return editBinding(ts, sourceFile, props, edit);
|
|
5984
|
+
if (edit.on === "step") {
|
|
5985
|
+
const step = edit.step ? stepIn(ts, props, edit.step.flow, edit.step.index) : void 0;
|
|
5986
|
+
return step ? editKey(ts, sourceFile, step, "params", edit) : { unplaced: "the step is not written in that call" };
|
|
5987
|
+
}
|
|
5988
|
+
return editKey(ts, sourceFile, props, edit.on === "attribute" ? "attributes" : void 0, edit);
|
|
5989
|
+
};
|
|
5990
|
+
/** The text with every change made — from the end back, so each span is where it was found. Overlaps are refused. */
|
|
5991
|
+
var applyChanges = (text, changes) => {
|
|
5992
|
+
const ordered = [...changes].toSorted((a, b) => b.start - a.start);
|
|
5993
|
+
for (let index = 1; index < ordered.length; index += 1) if (ordered[index].end > ordered[index - 1].start) return;
|
|
5994
|
+
return ordered.reduce((result, change) => result.slice(0, change.start) + change.text + result.slice(change.end), text);
|
|
5995
|
+
};
|
|
5996
|
+
//#endregion
|
|
5997
|
+
//#region src/projectTypeScript.ts
|
|
5998
|
+
/**
|
|
5999
|
+
* The TypeScript a project (or a plugin package) installs, resolved from its own folder rather than the CLI's: its
|
|
6000
|
+
* version is the one its source is written for. Absent when it installs none.
|
|
6001
|
+
*/
|
|
6002
|
+
var loadTypeScript = (root) => {
|
|
6003
|
+
try {
|
|
6004
|
+
return createRequire(path.join(root, "package.json"))("typescript");
|
|
6005
|
+
} catch {
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
6008
|
+
};
|
|
6009
|
+
//#endregion
|
|
6010
|
+
//#region src/commands/fix.ts
|
|
6011
|
+
var run = promisify(execFile);
|
|
6012
|
+
var editSources = async (root, fixes) => {
|
|
6013
|
+
const ts = loadTypeScript(root);
|
|
6014
|
+
const placed = [];
|
|
6015
|
+
const left = [];
|
|
6016
|
+
const sources = /* @__PURE__ */ new Map();
|
|
6017
|
+
for (const fix of fixes) {
|
|
6018
|
+
if (!fix.edit || !fix.position) {
|
|
6019
|
+
left.push({
|
|
6020
|
+
fix,
|
|
6021
|
+
why: fix.position ? "it has no one way to be written" : "no call of the project wrote it"
|
|
6022
|
+
});
|
|
6023
|
+
continue;
|
|
6024
|
+
}
|
|
6025
|
+
if (!ts) {
|
|
6026
|
+
left.push({
|
|
6027
|
+
fix,
|
|
6028
|
+
why: "the project has no TypeScript to read its source with"
|
|
6029
|
+
});
|
|
6030
|
+
continue;
|
|
6031
|
+
}
|
|
6032
|
+
const file = path.resolve(process.cwd(), fix.position.file);
|
|
6033
|
+
let source = sources.get(file);
|
|
6034
|
+
if (!source) {
|
|
6035
|
+
const text = await fs.readFile(file, "utf-8");
|
|
6036
|
+
source = {
|
|
6037
|
+
text,
|
|
6038
|
+
sourceFile: ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
6039
|
+
};
|
|
6040
|
+
sources.set(file, source);
|
|
6041
|
+
}
|
|
6042
|
+
const outcome = sourceEdit(ts, source.sourceFile, fix.position, fix.edit);
|
|
6043
|
+
if ("unplaced" in outcome) left.push({
|
|
6044
|
+
fix,
|
|
6045
|
+
why: outcome.unplaced
|
|
6046
|
+
});
|
|
6047
|
+
else placed.push({
|
|
6048
|
+
fix,
|
|
6049
|
+
file,
|
|
6050
|
+
change: outcome.change
|
|
6051
|
+
});
|
|
6052
|
+
}
|
|
6053
|
+
const format = await projectFormatter(root);
|
|
6054
|
+
const files = [];
|
|
6055
|
+
const written = [];
|
|
6056
|
+
for (const [file, { text }] of sources) {
|
|
6057
|
+
const mine = placed.filter((entry) => entry.file === file);
|
|
6058
|
+
const changed = applyChanges(text, mine.map((entry) => entry.change));
|
|
6059
|
+
if (changed === void 0) {
|
|
6060
|
+
left.push(...mine.map((entry) => ({
|
|
6061
|
+
fix: entry.fix,
|
|
6062
|
+
why: "it overlaps another fix in the same place"
|
|
6063
|
+
})));
|
|
6064
|
+
continue;
|
|
6065
|
+
}
|
|
6066
|
+
if (mine.length > 0) {
|
|
6067
|
+
files.push({
|
|
6068
|
+
file,
|
|
6069
|
+
before: text,
|
|
6070
|
+
after: await formatLikeBefore(format, path.relative(root, file), text, changed)
|
|
6071
|
+
});
|
|
6072
|
+
written.push(...mine.map((entry) => entry.fix));
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
return {
|
|
6076
|
+
files,
|
|
6077
|
+
written,
|
|
6078
|
+
left
|
|
6079
|
+
};
|
|
6080
|
+
};
|
|
6081
|
+
/** A plan as `plitzi fix --json` prints it. */
|
|
6082
|
+
var planFromJson = (stdout) => {
|
|
6083
|
+
const parsed = JSON.parse(stdout);
|
|
6084
|
+
if (!e$1(parsed)) return { problem: "the space did not answer as a plan" };
|
|
6085
|
+
if (typeof parsed.problem === "string") return { problem: parsed.problem };
|
|
6086
|
+
const entries = (value) => Array.isArray(value) ? value.filter(e$1) : [];
|
|
6087
|
+
const elementOf = (entry) => typeof entry.elementId === "string" ? entry.elementId : null;
|
|
6088
|
+
return {
|
|
6089
|
+
fixes: entries(parsed.fixes).map((entry) => ({
|
|
6090
|
+
code: String(entry.code),
|
|
6091
|
+
elementId: elementOf(entry),
|
|
6092
|
+
message: String(entry.message)
|
|
6093
|
+
})),
|
|
6094
|
+
problems: entries(parsed.problems).map((entry) => ({
|
|
6095
|
+
code: String(entry.code),
|
|
6096
|
+
elementId: elementOf(entry),
|
|
6097
|
+
message: String(entry.message)
|
|
6098
|
+
}))
|
|
6099
|
+
};
|
|
6100
|
+
};
|
|
6101
|
+
/** The plan, made in a process of its own: the edited files are imported fresh, never from this one's module cache. */
|
|
6102
|
+
var planAfresh = async () => {
|
|
6103
|
+
try {
|
|
6104
|
+
const { stdout } = await run(process.execPath, [
|
|
6105
|
+
process.argv[1],
|
|
6106
|
+
"fix",
|
|
6107
|
+
"--json"
|
|
6108
|
+
], {
|
|
6109
|
+
cwd: process.cwd(),
|
|
6110
|
+
maxBuffer: 16777216
|
|
6111
|
+
});
|
|
6112
|
+
return planFromJson(stdout);
|
|
6113
|
+
} catch (error) {
|
|
6114
|
+
const printed = e$1(error) && typeof error.stdout === "string" ? error.stdout.trim() : "";
|
|
6115
|
+
if (printed.startsWith("{")) return planFromJson(printed);
|
|
6116
|
+
return { problem: error instanceof Error ? error.message.split("\n").slice(0, 3).join(" ") : String(error) };
|
|
6117
|
+
}
|
|
6118
|
+
};
|
|
6119
|
+
var leftText = (left) => left.length === 0 ? [] : [chalk.yellow(`${String(left.length)} left for you:`), ...left.map(({ fix, why }) => ` - ${fix.at ? `${fix.at} ` : ""}[${fix.code}] ${fix.message} (${why})`)];
|
|
6120
|
+
var fix = async (options) => {
|
|
6121
|
+
const project = await findProject(process.cwd());
|
|
6122
|
+
if (!project || project.plitzi?.kind !== "project" || project.plitzi.source !== "local") {
|
|
6123
|
+
fail("Run this in a project whose space is written in it (`src/space.ts`): one kept on Plitzi is fixed in the builder.");
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
const loaded = await loadProjectSpace(project.root);
|
|
6127
|
+
if ("problem" in loaded) {
|
|
6128
|
+
fail(loaded.problem);
|
|
6129
|
+
return;
|
|
6130
|
+
}
|
|
6131
|
+
const plan = projectPlan(loaded);
|
|
6132
|
+
if (options.json) {
|
|
6133
|
+
console.log(JSON.stringify(plan));
|
|
6134
|
+
if ("problem" in plan) process.exitCode = 1;
|
|
6135
|
+
return;
|
|
6136
|
+
}
|
|
6137
|
+
if ("problem" in plan) {
|
|
6138
|
+
fail(`Nothing to plan: ${plan.problem}. \`npm run author\` says what to change first.`);
|
|
6139
|
+
return;
|
|
6140
|
+
}
|
|
6141
|
+
if (plan.fixes.length === 0) {
|
|
6142
|
+
console.log(chalk.green("Nothing to fix."));
|
|
6143
|
+
return;
|
|
6144
|
+
}
|
|
6145
|
+
const edited = await editSources(project.root, plan.fixes);
|
|
6146
|
+
const diffs = edited.files.map(({ file, before, after }) => unifiedDiff(path.relative(process.cwd(), file), before, after));
|
|
6147
|
+
if (!options.write) {
|
|
6148
|
+
console.log([
|
|
6149
|
+
...diffs,
|
|
6150
|
+
...edited.written.length > 0 ? [chalk.green(`${String(edited.written.length)} to write: plitzi fix --write`)] : [],
|
|
6151
|
+
...leftText(edited.left)
|
|
6152
|
+
].join("\n"));
|
|
6153
|
+
return;
|
|
6154
|
+
}
|
|
6155
|
+
let attempt = edited;
|
|
6156
|
+
const refused = [];
|
|
6157
|
+
for (;;) {
|
|
6158
|
+
await Promise.all(attempt.files.map(({ file, after }) => fs.writeFile(file, after)));
|
|
6159
|
+
const { reasons, elements } = verdict(plan, attempt.written, await planAfresh());
|
|
6160
|
+
if (reasons.length === 0) break;
|
|
6161
|
+
await Promise.all(attempt.files.map(({ file, before }) => fs.writeFile(file, before)));
|
|
6162
|
+
const blamed = attempt.written.filter((entry) => elements.has(null) || elements.has(entry.elementId));
|
|
6163
|
+
const why = `writing it would leave the space with ${reasons.join("; ")}`;
|
|
6164
|
+
refused.push(...blamed.map((entry) => ({
|
|
6165
|
+
fix: entry,
|
|
6166
|
+
why
|
|
6167
|
+
})));
|
|
6168
|
+
const rest = attempt.written.filter((entry) => !blamed.includes(entry));
|
|
6169
|
+
if (blamed.length === 0 || rest.length === 0) {
|
|
6170
|
+
fail(["Nothing was written — the source is as it was:", ...reasons.map((reason) => ` - ${reason}`)].join("\n"));
|
|
6171
|
+
console.log(leftText([...edited.left, ...refused]).join("\n"));
|
|
6172
|
+
return;
|
|
6173
|
+
}
|
|
6174
|
+
attempt = await editSources(project.root, rest);
|
|
4496
6175
|
}
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
6176
|
+
console.log([
|
|
6177
|
+
...attempt.files.map(({ file, before, after }) => unifiedDiff(path.relative(process.cwd(), file), before, after)),
|
|
6178
|
+
chalk.green(`Wrote ${String(attempt.written.length)} ${attempt.written.length === 1 ? "fix" : "fixes"} in ${String(attempt.files.length)} ${attempt.files.length === 1 ? "file" : "files"}; the space authors with them gone. Typecheck the project before calling it done.`),
|
|
6179
|
+
...leftText([...edited.left, ...refused])
|
|
6180
|
+
].join("\n"));
|
|
4500
6181
|
};
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
};
|
|
4511
|
-
}
|
|
6182
|
+
//#endregion
|
|
6183
|
+
//#region src/commands/siteOwnership.ts
|
|
6184
|
+
var LOOPBACK = /^(127\.|::1$|::ffff:127\.)/;
|
|
6185
|
+
/** Served from this machine: `localhost`, or a name that resolves only to loopback addresses (a `/etc/hosts` entry). */
|
|
6186
|
+
var isThisMachine = async (host) => {
|
|
6187
|
+
const name = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
6188
|
+
if (name === "localhost" || name.endsWith(".localhost")) return true;
|
|
6189
|
+
try {
|
|
6190
|
+
const addresses = await lookup(name, { all: true });
|
|
6191
|
+
return addresses.length > 0 && addresses.every(({ address }) => LOOPBACK.test(address));
|
|
6192
|
+
} catch {
|
|
6193
|
+
return false;
|
|
6194
|
+
}
|
|
4512
6195
|
};
|
|
4513
|
-
var
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
6196
|
+
var NOT_YOURS = (host) => `${host} is not a verified domain of any space of yours, so it is not imported. If the site is yours: add its domain to one of your spaces under Domains in the dashboard, publish the \`_plitzi\` TXT record it shows, verify it, and import again.`;
|
|
6197
|
+
var siteOwnership = async (url, options) => {
|
|
6198
|
+
if (await isThisMachine(url.hostname)) return {
|
|
6199
|
+
ok: true,
|
|
6200
|
+
said: "served from this machine"
|
|
6201
|
+
};
|
|
6202
|
+
const api = await apiFor(options);
|
|
6203
|
+
const connection = api && await signedIn(api, `to show ${url.hostname} is yours`);
|
|
6204
|
+
if (!connection) return { ok: false };
|
|
6205
|
+
const answered = await authorizedRequest(connection, `/account/domains/covering?host=${encodeURIComponent(url.hostname)}`);
|
|
6206
|
+
if (!answered.ok) return {
|
|
6207
|
+
ok: false,
|
|
6208
|
+
problem: answered.error
|
|
6209
|
+
};
|
|
6210
|
+
const { status, data } = answered.value.reply;
|
|
6211
|
+
const space = e$1(data) && e$1(data.space) && typeof data.space.name === "string" ? data.space.name : "";
|
|
6212
|
+
if (status === 200 && e$1(data) && typeof data.domain === "string") return {
|
|
6213
|
+
ok: true,
|
|
6214
|
+
said: `${data.domain}, verified for ${space || "one of your spaces"}`
|
|
6215
|
+
};
|
|
6216
|
+
if (status === 404) return {
|
|
6217
|
+
ok: false,
|
|
6218
|
+
problem: NOT_YOURS(url.hostname)
|
|
6219
|
+
};
|
|
6220
|
+
return {
|
|
6221
|
+
ok: false,
|
|
6222
|
+
problem: `The platform could not say whether ${url.hostname} is yours (${String(status)}).`
|
|
4517
6223
|
};
|
|
4518
|
-
if (!project) return path.resolve(await askText(rl, "Which folder should the plugin be written to?", typed.fallback));
|
|
4519
|
-
const chosen = await askChoice(rl, `Which folder of ${path.relative(process.cwd(), project.workspaceRoot ?? project.root) || "this project"} should the plugin go in?`, folderOptions(project, base), {
|
|
4520
|
-
label: "Somewhere else",
|
|
4521
|
-
...typed
|
|
4522
|
-
});
|
|
4523
|
-
return path.resolve(chosen);
|
|
4524
6224
|
};
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
*/
|
|
4529
|
-
var
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
6225
|
+
//#endregion
|
|
6226
|
+
//#region src/commands/importPage.ts
|
|
6227
|
+
var OUTLINE_DEPTH = 4;
|
|
6228
|
+
/** Scrolls to the end and back, so pictures and sections that load as they come into view are there to measure. */
|
|
6229
|
+
var revealLazyContent = async (page) => {
|
|
6230
|
+
await page.evaluate(async () => {
|
|
6231
|
+
const step = window.innerHeight;
|
|
6232
|
+
for (let y = 0; y < Math.min(document.body.scrollHeight, 3e4); y += step) {
|
|
6233
|
+
window.scrollTo(0, y);
|
|
6234
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
6235
|
+
}
|
|
6236
|
+
window.scrollTo(0, 0);
|
|
6237
|
+
}, void 0);
|
|
6238
|
+
await page.waitForTimeout(300);
|
|
6239
|
+
};
|
|
6240
|
+
var measure = async (browser, url, width, scheme, read = []) => {
|
|
6241
|
+
const page = await browser.newPage({
|
|
6242
|
+
viewport: {
|
|
6243
|
+
width,
|
|
6244
|
+
height: 900
|
|
6245
|
+
},
|
|
6246
|
+
colorScheme: scheme,
|
|
6247
|
+
reducedMotion: "reduce"
|
|
4539
6248
|
});
|
|
6249
|
+
const answered = await page.goto(url, { waitUntil: "networkidle" }).catch((error) => error);
|
|
6250
|
+
if (answered instanceof Error) return { problem: `${url} did not load at ${String(width)} px: ${answered.message.split("\n")[0] ?? ""}` };
|
|
6251
|
+
await revealLazyContent(page);
|
|
6252
|
+
return {
|
|
6253
|
+
probe: await page.evaluate(importProbe, {
|
|
6254
|
+
depth: OUTLINE_DEPTH,
|
|
6255
|
+
read
|
|
6256
|
+
}),
|
|
6257
|
+
screen: await page.screenshot({ fullPage: true })
|
|
6258
|
+
};
|
|
4540
6259
|
};
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
6260
|
+
/** Every file under a folder, by its path inside it — none when there is no such folder. */
|
|
6261
|
+
var filesUnder = async (folder) => {
|
|
6262
|
+
return (await readdir(folder, {
|
|
6263
|
+
recursive: true,
|
|
6264
|
+
withFileTypes: true
|
|
6265
|
+
}).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => path.relative(folder, path.join(entry.parentPath, entry.name)));
|
|
6266
|
+
};
|
|
6267
|
+
var count = (amount, one, many = `${one}s`) => `${String(amount)} ${amount === 1 ? one : many}`;
|
|
6268
|
+
var summaryText = (summary, out, stale) => [
|
|
6269
|
+
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(", ")}`] : [],
|
|
6273
|
+
` pictures ${String(summary.pictures)} in assets.json`,
|
|
6274
|
+
...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
|
+
` next ${path.join(out, "IMPORT.md")} says what was not carried over`
|
|
6276
|
+
].join("\n");
|
|
6277
|
+
var importPage = async (address, options) => {
|
|
6278
|
+
let url;
|
|
6279
|
+
try {
|
|
6280
|
+
url = new URL(address);
|
|
6281
|
+
} catch {
|
|
6282
|
+
fail(`${address} is not a URL: give the whole address, https://example.com/page.`);
|
|
4548
6283
|
return;
|
|
4549
6284
|
}
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
6285
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
6286
|
+
fail(`${address} is not a web page: only http and https addresses are imported.`);
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
const project = await projectHere("to import into");
|
|
6290
|
+
if (!project) return;
|
|
6291
|
+
const out = options.out ?? "src/imported";
|
|
6292
|
+
const target = path.resolve(project.root, out);
|
|
6293
|
+
if (path.relative(project.root, target).startsWith("..")) {
|
|
6294
|
+
fail(`${out} is outside the project: the import is written inside it.`);
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
const existing = await filesUnder(target);
|
|
6298
|
+
if (existing.length > 0 && !options.force) {
|
|
6299
|
+
fail(`${out} already has files: choose another --out, or --force to write over them.`);
|
|
6300
|
+
return;
|
|
6301
|
+
}
|
|
6302
|
+
const widths = options.width ?? [
|
|
6303
|
+
1440,
|
|
6304
|
+
768,
|
|
6305
|
+
390
|
|
6306
|
+
];
|
|
6307
|
+
const ownership = await siteOwnership(url, options);
|
|
6308
|
+
if (!ownership.ok) {
|
|
6309
|
+
if (ownership.problem) fail(ownership.problem);
|
|
6310
|
+
return;
|
|
6311
|
+
}
|
|
6312
|
+
if (!options.json) console.log(chalk.dim(`${url.hostname} is yours: ${ownership.said}.`));
|
|
6313
|
+
const browser = await launchBrowser(project.root);
|
|
6314
|
+
if ("problem" in browser) {
|
|
6315
|
+
fail(browser.problem);
|
|
4554
6316
|
return;
|
|
4555
6317
|
}
|
|
4556
|
-
const rl = terminal ? readline.createInterface({
|
|
4557
|
-
input: process.stdin,
|
|
4558
|
-
output: process.stdout
|
|
4559
|
-
}) : void 0;
|
|
4560
6318
|
try {
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
const title = await answer("What does the builder call the element?", options.title, names.title);
|
|
4570
|
-
const description = await answer("What is it for, in a sentence? The builder shows it, and an agent reads it to choose the element.", options.description, "");
|
|
4571
|
-
const owner = await answer("Who publishes it?", options.owner, "");
|
|
4572
|
-
const others = othersGiven ?? (rl && !options.yes ? splitNames(await askText(rl, "Any other elements in this package? Their names, separated by commas.", "", (reply) => elementListProblem(names.base, splitNames(reply)))) : []);
|
|
4573
|
-
const otherElements = [];
|
|
4574
|
-
for (const other of others) {
|
|
4575
|
-
const otherNames = pluginNames(other);
|
|
4576
|
-
otherElements.push({
|
|
4577
|
-
name: other,
|
|
4578
|
-
title: await answer(`What does the builder call ${other}?`, void 0, otherNames.title),
|
|
4579
|
-
description: await answer(`What is ${other} for, in a sentence?`, void 0, "")
|
|
4580
|
-
});
|
|
4581
|
-
}
|
|
4582
|
-
const target = directory ? path.resolve(directory) : rl ? await askDirectory(rl, here, names.base) : void 0;
|
|
4583
|
-
if (!target) return;
|
|
4584
|
-
const wasEmpty = await isEmpty(target);
|
|
4585
|
-
if (!options.force && !wasEmpty) {
|
|
4586
|
-
console.error(chalk.red(`${target} is not empty. Pass --force to write into it anyway.`));
|
|
4587
|
-
process.exitCode = 1;
|
|
4588
|
-
return;
|
|
6319
|
+
const measured = [];
|
|
6320
|
+
for (const width of widths) {
|
|
6321
|
+
const result = await measure(browser, url.href, width, "light");
|
|
6322
|
+
if ("problem" in result) {
|
|
6323
|
+
fail(result.problem);
|
|
6324
|
+
return;
|
|
6325
|
+
}
|
|
6326
|
+
measured.push(result);
|
|
4589
6327
|
}
|
|
4590
|
-
const
|
|
4591
|
-
const
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
6328
|
+
const widest = measured.reduce((wide, next) => next.probe.width > wide.probe.width ? next : wide).probe;
|
|
6329
|
+
const darkResult = await measure(browser, url.href, widest.width, "dark", Object.values(widest.colors.samples));
|
|
6330
|
+
const dark = "probe" in darkResult ? darkScheme(widest, darkResult.probe) : void 0;
|
|
6331
|
+
const { files, summary } = importedFiles({
|
|
6332
|
+
probes: measured.map(({ probe }) => probe),
|
|
6333
|
+
...dark ? { dark } : {}
|
|
6334
|
+
});
|
|
6335
|
+
const format = await projectFormatter(project.root);
|
|
6336
|
+
await mkdir(path.join(target, "screens"), { recursive: true });
|
|
6337
|
+
for (const file of files) {
|
|
6338
|
+
const destination = path.join(target, file.path);
|
|
6339
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
6340
|
+
const relative = path.relative(project.root, destination);
|
|
6341
|
+
await writeFile(destination, file.path.endsWith(".ts") ? await format(relative, file.content) : file.content);
|
|
4600
6342
|
}
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
name: names.base,
|
|
4605
|
-
title,
|
|
4606
|
-
description
|
|
4607
|
-
}, ...otherElements],
|
|
4608
|
-
owner,
|
|
4609
|
-
packageManager,
|
|
4610
|
-
managerVersion: detectManagerVersion(packageManager, await nearestExisting(target)),
|
|
4611
|
-
inProject: project !== void 0
|
|
6343
|
+
const screens = measured.map(({ probe, screen }) => ({
|
|
6344
|
+
path: `screens/${String(probe.width)}.png`,
|
|
6345
|
+
screen
|
|
4612
6346
|
}));
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The plugin is written; the reason is in the output above.`));
|
|
4623
|
-
console.error(chalk.dim(INSTALL_HINTS[packageManager]));
|
|
4624
|
-
process.exitCode = 1;
|
|
4625
|
-
}
|
|
4626
|
-
if (installed && wasEmpty) await runScript(packageManager, "format", target);
|
|
4627
|
-
const where = cdPrefix(target);
|
|
4628
|
-
console.log(chalk.green(`\n${name} — the ${names.type} element, as a package any space can load.`));
|
|
4629
|
-
if (unlisted) console.log(chalk.yellow(`\n${path.relative(process.cwd(), target)} is not one of the workspace's packages: add it to \`workspaces\`, then install.`));
|
|
4630
|
-
console.log("");
|
|
4631
|
-
if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
|
|
4632
|
-
console.log(` ${where}${runCommand(packageManager, "start")} # the plugin inside a space`);
|
|
4633
|
-
console.log(` ${where}${runCommand(packageManager, "zip")} # what the builder takes, under Resources`);
|
|
4634
|
-
console.log("");
|
|
6347
|
+
for (const { path: file, screen } of screens) await writeFile(path.join(target, file), screen);
|
|
6348
|
+
const written = /* @__PURE__ */ new Set([...files.map((file) => file.path), ...screens.map((screen) => screen.path)]);
|
|
6349
|
+
const stale = existing.filter((file) => !written.has(file.split(path.sep).join("/")));
|
|
6350
|
+
console.log(options.json ? JSON.stringify({
|
|
6351
|
+
...summary,
|
|
6352
|
+
out,
|
|
6353
|
+
files: [...written],
|
|
6354
|
+
stale
|
|
6355
|
+
}) : summaryText(summary, out, stale));
|
|
4635
6356
|
} finally {
|
|
4636
|
-
|
|
6357
|
+
await browser.close();
|
|
4637
6358
|
}
|
|
4638
6359
|
};
|
|
4639
6360
|
//#endregion
|
|
4640
6361
|
//#region ../../packages/sdk-shared/dist/source/snapshot.mjs
|
|
4641
|
-
var
|
|
6362
|
+
var n = {
|
|
4642
6363
|
files: 2e3,
|
|
4643
6364
|
fileBytes: 4194304,
|
|
4644
6365
|
totalBytes: 25165824
|
|
4645
6366
|
};
|
|
4646
|
-
var
|
|
4647
|
-
var
|
|
6367
|
+
var r = ["plugin", "runtime"];
|
|
6368
|
+
var i = [
|
|
4648
6369
|
/(^|\/)\.env(\.|$)/,
|
|
4649
6370
|
/\.(pem|key|p12|pfx)$/i,
|
|
4650
6371
|
/(^|\/)id_(rsa|dsa|ecdsa|ed25519)$/
|
|
4651
6372
|
];
|
|
4652
|
-
var
|
|
6373
|
+
var a = [
|
|
4653
6374
|
{
|
|
4654
6375
|
what: "a private key",
|
|
4655
6376
|
pattern: /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/
|
|
@@ -4671,55 +6392,54 @@ var i = [
|
|
|
4671
6392
|
pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/
|
|
4672
6393
|
}
|
|
4673
6394
|
];
|
|
4674
|
-
var
|
|
6395
|
+
var o = (e) => {
|
|
4675
6396
|
let t = e.split("/");
|
|
4676
6397
|
if (!e || e.startsWith("/") || /^[a-zA-Z]:/.test(e) || e.includes("\\")) return `"${e}" is not a path relative to the project, with forward slashes`;
|
|
4677
6398
|
if (t.some((e) => e === "" || e === "." || e === "..")) return `"${e}" climbs out of the project, or has an empty part`;
|
|
4678
6399
|
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`;
|
|
4679
|
-
if (
|
|
4680
|
-
};
|
|
4681
|
-
var
|
|
4682
|
-
var
|
|
4683
|
-
var
|
|
4684
|
-
var
|
|
4685
|
-
var
|
|
4686
|
-
var d = (e) => u(e) && Object.values(e).every((e) => typeof e == "string");
|
|
6400
|
+
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
|
+
};
|
|
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);
|
|
6405
|
+
var u = (e) => Array.isArray(e) && e.length > 0 && e.every((e) => typeof e == "string");
|
|
6406
|
+
var d = (t) => e$1(t) && Object.values(t).every((e) => typeof e == "string");
|
|
4687
6407
|
var f = (e) => Math.floor(e.length * 3 / 4) - (e.endsWith("==") ? 2 : +!!e.endsWith("="));
|
|
4688
|
-
var p = (
|
|
4689
|
-
if (!
|
|
6408
|
+
var p = (t, i) => {
|
|
6409
|
+
if (!e$1(t) || t.format !== 1) return {
|
|
4690
6410
|
ok: !1,
|
|
4691
6411
|
problems: ["Not a source snapshot of format 1"]
|
|
4692
6412
|
};
|
|
4693
|
-
let { kind:
|
|
4694
|
-
if (
|
|
6413
|
+
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 {
|
|
4695
6415
|
ok: !1,
|
|
4696
6416
|
problems: [..._, "Its files and dependencies are maps of text"]
|
|
4697
6417
|
};
|
|
4698
|
-
|
|
6418
|
+
u(m) ? m.filter((e) => !Object.hasOwn(h, e)).forEach((e) => _.push(`Its entry "${e}" is not among its files`)) : _.push("It names at least one entry");
|
|
4699
6419
|
let v = Object.keys(h);
|
|
4700
|
-
v.length >
|
|
6420
|
+
v.length > n.files && _.push(`It holds ${String(v.length)} files; a snapshot holds at most ${String(n.files)}`);
|
|
4701
6421
|
let y = 0;
|
|
4702
6422
|
return v.forEach((e) => {
|
|
4703
|
-
let
|
|
4704
|
-
if (
|
|
4705
|
-
_.push(
|
|
6423
|
+
let t = o(e);
|
|
6424
|
+
if (t) {
|
|
6425
|
+
_.push(t);
|
|
4706
6426
|
return;
|
|
4707
6427
|
}
|
|
4708
|
-
let
|
|
4709
|
-
if (y +=
|
|
4710
|
-
_.push(`"${e}" is ${(
|
|
6428
|
+
let r = f(h[e]);
|
|
6429
|
+
if (y += r, r > n.fileBytes) {
|
|
6430
|
+
_.push(`"${e}" is ${(r / 1024 / 1024).toFixed(1)} MB; a file of source is at most 4 MB`);
|
|
4711
6431
|
return;
|
|
4712
6432
|
}
|
|
4713
|
-
let
|
|
4714
|
-
|
|
4715
|
-
}), y >
|
|
6433
|
+
let a = s(i(h[e]));
|
|
6434
|
+
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) ? {
|
|
4716
6436
|
ok: !1,
|
|
4717
6437
|
problems: _
|
|
4718
6438
|
} : {
|
|
4719
6439
|
ok: !0,
|
|
4720
6440
|
snapshot: {
|
|
4721
6441
|
format: 1,
|
|
4722
|
-
kind:
|
|
6442
|
+
kind: a,
|
|
4723
6443
|
name: p,
|
|
4724
6444
|
entries: m,
|
|
4725
6445
|
files: h,
|
|
@@ -4746,13 +6466,6 @@ var EXTENSIONS = [
|
|
|
4746
6466
|
".scss"
|
|
4747
6467
|
];
|
|
4748
6468
|
var BUILTINS = new Set(builtinModules);
|
|
4749
|
-
var loadTypeScript$1 = (root) => {
|
|
4750
|
-
try {
|
|
4751
|
-
return createRequire(path.join(root, "package.json"))("typescript");
|
|
4752
|
-
} catch {
|
|
4753
|
-
return;
|
|
4754
|
-
}
|
|
4755
|
-
};
|
|
4756
6469
|
var isFile = async (file) => {
|
|
4757
6470
|
try {
|
|
4758
6471
|
return (await fs.stat(file)).isFile();
|
|
@@ -4803,7 +6516,7 @@ var dependencyOf = async (root, name) => {
|
|
|
4803
6516
|
* reaches holds a credential.
|
|
4804
6517
|
*/
|
|
4805
6518
|
var packSource = async ({ root, kind, name, entries }) => {
|
|
4806
|
-
const ts = loadTypeScript
|
|
6519
|
+
const ts = loadTypeScript(root);
|
|
4807
6520
|
if (!ts) throw new Error("Packing the source reads its imports with the project’s own TypeScript:\n npm install -D typescript");
|
|
4808
6521
|
const problems = [];
|
|
4809
6522
|
const files = {};
|
|
@@ -4859,13 +6572,6 @@ var packSource = async ({ root, kind, name, entries }) => {
|
|
|
4859
6572
|
};
|
|
4860
6573
|
//#endregion
|
|
4861
6574
|
//#region src/pack/typeDeclarations.ts
|
|
4862
|
-
var loadTypeScript = (root) => {
|
|
4863
|
-
try {
|
|
4864
|
-
return createRequire(path.join(root, "package.json"))("typescript");
|
|
4865
|
-
} catch {
|
|
4866
|
-
return;
|
|
4867
|
-
}
|
|
4868
|
-
};
|
|
4869
6575
|
var emitTypeDeclarations = (root, outDir) => {
|
|
4870
6576
|
const ts = loadTypeScript(root);
|
|
4871
6577
|
if (!ts) return "no-typescript";
|
|
@@ -4929,21 +6635,20 @@ var INLINED = [
|
|
|
4929
6635
|
];
|
|
4930
6636
|
/** A plugin that cannot be packed, with what to change — never a stack trace out of esbuild. */
|
|
4931
6637
|
var PackError = class extends Error {};
|
|
4932
|
-
var isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4933
6638
|
/** The declaration, or what is missing from it — named, so the fix is one edit away. */
|
|
4934
6639
|
var checkDeclaration = (value, where) => {
|
|
4935
6640
|
const missing = (field) => new PackError(`${where}: the declaration has no ${field}. Write it the way \`plitzi add plugin\` does.`);
|
|
4936
|
-
if (!
|
|
6641
|
+
if (!e$1(value) || typeof value.type !== "string" || !value.type) throw missing("`type`");
|
|
4937
6642
|
const { content } = value;
|
|
4938
|
-
if (!
|
|
6643
|
+
if (!e$1(content)) throw missing("`content`");
|
|
4939
6644
|
for (const field of [
|
|
4940
6645
|
"attributes",
|
|
4941
6646
|
"definition",
|
|
4942
6647
|
"builder",
|
|
4943
6648
|
"market",
|
|
4944
6649
|
"defaultStyle"
|
|
4945
|
-
]) if (!
|
|
4946
|
-
if (!
|
|
6650
|
+
]) if (!e$1(content[field])) throw missing(`\`content.${field}\``);
|
|
6651
|
+
if (!e$1(content.definition) || typeof content.definition.label !== "string") throw missing("`content.definition.label`");
|
|
4947
6652
|
return value;
|
|
4948
6653
|
};
|
|
4949
6654
|
var toImportPath = (file) => file.split(path.sep).join("/");
|
|
@@ -5125,10 +6830,6 @@ var elementFolders = async (project) => {
|
|
|
5125
6830
|
const packable = await Promise.all(folders.map(hasElement));
|
|
5126
6831
|
return folders.filter((_folder, index) => packable[index]);
|
|
5127
6832
|
};
|
|
5128
|
-
var readVersion = async (root) => {
|
|
5129
|
-
const { version } = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf-8"));
|
|
5130
|
-
return version ?? "0.0.0";
|
|
5131
|
-
};
|
|
5132
6833
|
/** The `-kebab-` spelling of a type, for the files it names: `seatPicker` writes `seat-picker.mjs`. */
|
|
5133
6834
|
var fileNameOf = (folder) => path.basename(folder).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
5134
6835
|
/** Which folders to pack, when none were named: asked for at a terminal, from the ones the project has. */
|
|
@@ -5154,8 +6855,7 @@ var chosenFolders = async (project, foldersGiven) => {
|
|
|
5154
6855
|
if (foldersGiven.length > 0) return foldersGiven.map((folder) => path.resolve(folder));
|
|
5155
6856
|
const candidates = await elementFolders(project);
|
|
5156
6857
|
if (candidates.length === 0) {
|
|
5157
|
-
|
|
5158
|
-
process.exitCode = 1;
|
|
6858
|
+
fail("No element to pack: name its folder (plitzi pack plugin src/components/SeatPicker), or add one with plitzi add plugin.");
|
|
5159
6859
|
return;
|
|
5160
6860
|
}
|
|
5161
6861
|
if (!atTerminal()) {
|
|
@@ -5171,16 +6871,15 @@ var chosenFolders = async (project, foldersGiven) => {
|
|
|
5171
6871
|
var packPluginCommand = async (foldersGiven, options) => {
|
|
5172
6872
|
const project = await findProject(process.cwd());
|
|
5173
6873
|
if (!project) {
|
|
5174
|
-
|
|
5175
|
-
process.exitCode = 1;
|
|
6874
|
+
fail("plitzi pack plugin packs a plugin of a project, and there is no package.json here or above.");
|
|
5176
6875
|
return;
|
|
5177
6876
|
}
|
|
5178
6877
|
const inPackage = project.plitzi?.kind === "plugin" && foldersGiven.length === 0;
|
|
5179
6878
|
const folders = inPackage ? [] : await chosenFolders(project, foldersGiven);
|
|
5180
6879
|
if (!folders) return;
|
|
5181
|
-
const
|
|
5182
|
-
const base = inPackage ? pluginNames(
|
|
5183
|
-
const version = options.pluginVersion ??
|
|
6880
|
+
const manifest = await readPackageJson(project.root);
|
|
6881
|
+
const base = inPackage ? pluginNames(manifest?.name ?? path.basename(project.root)).base : fileNameOf(folders[0]);
|
|
6882
|
+
const version = options.pluginVersion ?? manifest?.version ?? "0.0.0";
|
|
5184
6883
|
const source = inPackage ? {
|
|
5185
6884
|
kind: "package",
|
|
5186
6885
|
entry: path.join(project.root, "src/index.ts"),
|
|
@@ -5218,8 +6917,7 @@ var packPluginCommand = async (foldersGiven, options) => {
|
|
|
5218
6917
|
console.log("");
|
|
5219
6918
|
} catch (error) {
|
|
5220
6919
|
if (!(error instanceof PackError)) throw error;
|
|
5221
|
-
|
|
5222
|
-
process.exitCode = 1;
|
|
6920
|
+
fail(error.message);
|
|
5223
6921
|
}
|
|
5224
6922
|
};
|
|
5225
6923
|
//#endregion
|
|
@@ -5231,8 +6929,7 @@ var packPluginCommand = async (foldersGiven, options) => {
|
|
|
5231
6929
|
var packSourceCommand = async (entries, options) => {
|
|
5232
6930
|
const root = options.root ? path.resolve(options.root) : (await findProject(process.cwd()))?.root;
|
|
5233
6931
|
if (!root) {
|
|
5234
|
-
|
|
5235
|
-
process.exitCode = 1;
|
|
6932
|
+
fail("plitzi pack source packs a project’s source, and there is no package.json here or above.");
|
|
5236
6933
|
return;
|
|
5237
6934
|
}
|
|
5238
6935
|
try {
|
|
@@ -5247,8 +6944,7 @@ var packSourceCommand = async (entries, options) => {
|
|
|
5247
6944
|
await fs.writeFile(out, bytes);
|
|
5248
6945
|
console.log(chalk.green(`${options.name}: ${String(Object.keys(snapshot.files).length)} files`) + chalk.dim(` — ${path.relative(process.cwd(), out)} (${(bytes.byteLength / 1024).toFixed(0)} KB)`));
|
|
5249
6946
|
} catch (error) {
|
|
5250
|
-
|
|
5251
|
-
process.exitCode = 1;
|
|
6947
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
5252
6948
|
}
|
|
5253
6949
|
};
|
|
5254
6950
|
//#endregion
|
|
@@ -5297,13 +6993,12 @@ var verdictOf = ({ given, was, here }) => {
|
|
|
5297
6993
|
if (here === was) return "write";
|
|
5298
6994
|
return given === was ? "keep" : "conflict";
|
|
5299
6995
|
};
|
|
5300
|
-
var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5301
6996
|
/**
|
|
5302
6997
|
* The packages the space's code asks for, added to the project's `package.json`: a package it does not list is added,
|
|
5303
6998
|
* one still at the range the space last asked for follows the space, and one the project changed stays as it is.
|
|
5304
6999
|
*/
|
|
5305
7000
|
var mergeDependencies = (manifest, asked, before) => {
|
|
5306
|
-
const current =
|
|
7001
|
+
const current = e$1(manifest.dependencies) ? manifest.dependencies : {};
|
|
5307
7002
|
const dependencies = { ...current };
|
|
5308
7003
|
const changed = [];
|
|
5309
7004
|
const kept = [];
|
|
@@ -5400,7 +7095,7 @@ var pull = async (options) => {
|
|
|
5400
7095
|
await Promise.all(removed.map((file) => fs.rm(path.join(root, file), { force: true })));
|
|
5401
7096
|
const packageFile = path.join(root, "package.json");
|
|
5402
7097
|
const manifest = JSON.parse(await fs.readFile(packageFile, "utf-8"));
|
|
5403
|
-
const dependencies = mergeDependencies(
|
|
7098
|
+
const dependencies = mergeDependencies(e$1(manifest) ? manifest : {}, next.dependencies, origin.dependencies);
|
|
5404
7099
|
if (dependencies.changed.length > 0) await fs.writeFile(packageFile, `${JSON.stringify(dependencies.manifest, null, 2)}\n`);
|
|
5405
7100
|
const files = Object.fromEntries([...[...given].map(([file, bytes]) => [file, digest(bytes)]), ...[...untouched].flatMap((file) => origin.files[file] ? [[file, origin.files[file]]] : [])]);
|
|
5406
7101
|
await writeOrigin(root, {
|
|
@@ -5469,8 +7164,7 @@ var keepSource = async (connection, spaceId, bytes) => {
|
|
|
5469
7164
|
//#endregion
|
|
5470
7165
|
//#region src/commands/runtime.ts
|
|
5471
7166
|
var DEFAULT_ENTRY = path.join("src", "runtime.ts");
|
|
5472
|
-
var
|
|
5473
|
-
var isPacker = (value) => isRecord(value) && typeof value.packRuntime === "function";
|
|
7167
|
+
var isPacker = (value) => e$1(value) && typeof value.packRuntime === "function";
|
|
5474
7168
|
/** This project's own packer — read, since it is the project's. */
|
|
5475
7169
|
var projectPacker = async (root) => {
|
|
5476
7170
|
try {
|
|
@@ -5480,15 +7174,11 @@ var projectPacker = async (root) => {
|
|
|
5480
7174
|
} catch {}
|
|
5481
7175
|
fail("plitzi runtime packs it with this project’s own @plitzi/sdk-server:\n npm install @plitzi/sdk-server");
|
|
5482
7176
|
};
|
|
5483
|
-
var connect = async (options, doing) => {
|
|
5484
|
-
const api = await apiFor(options);
|
|
5485
|
-
return api ? connectionWithSpace(api, doing) : void 0;
|
|
5486
|
-
};
|
|
5487
7177
|
/** Packs the project's runtime module and keeps it as the space's draft runtime — which a publish takes live. */
|
|
5488
7178
|
var pushRuntime = async (options) => {
|
|
5489
|
-
const
|
|
5490
|
-
|
|
5491
|
-
|
|
7179
|
+
const root = (await projectHere("whose runtime to push"))?.root;
|
|
7180
|
+
const connection = root && await connectToSpace(options, "to push to");
|
|
7181
|
+
if (!root || !connection || !connection.space) return;
|
|
5492
7182
|
const entry = path.resolve(root, options.entry ?? DEFAULT_ENTRY);
|
|
5493
7183
|
try {
|
|
5494
7184
|
await fs.access(entry);
|
|
@@ -5519,7 +7209,7 @@ var pushRuntime = async (options) => {
|
|
|
5519
7209
|
fail(reply.data.error ?? `The runtime was not kept (${String(reply.status)}).`);
|
|
5520
7210
|
return;
|
|
5521
7211
|
}
|
|
5522
|
-
console.log(
|
|
7212
|
+
console.log(chalk.green(`${connection.space.name}’s draft runtime is ${reply.data.digest.slice(0, 12)} (${(bytes.byteLength / 1024).toFixed(0)} KB).`) + chalk.dim(" It starts in a moment; publish the space to take it live."));
|
|
5523
7213
|
try {
|
|
5524
7214
|
const source = await packSource({
|
|
5525
7215
|
root,
|
|
@@ -5552,10 +7242,20 @@ var readRuntime = async (connection, spaceId) => {
|
|
|
5552
7242
|
};
|
|
5553
7243
|
/** How each environment's runtime is, and the names of its variables. */
|
|
5554
7244
|
var runtimeStatus = async (options) => {
|
|
5555
|
-
const connection = await
|
|
7245
|
+
const connection = await connectToSpace(options, "to read");
|
|
5556
7246
|
if (!connection?.space) return;
|
|
5557
7247
|
const runtime = await readRuntime(connection, connection.space.id);
|
|
5558
7248
|
if (!runtime) return;
|
|
7249
|
+
if (options.json) {
|
|
7250
|
+
console.log(JSON.stringify({
|
|
7251
|
+
space: {
|
|
7252
|
+
id: connection.space.id,
|
|
7253
|
+
name: connection.space.name
|
|
7254
|
+
},
|
|
7255
|
+
...runtime
|
|
7256
|
+
}));
|
|
7257
|
+
return;
|
|
7258
|
+
}
|
|
5559
7259
|
if (!runtime.environments.length) console.log(`${connection.space.name} has no runtime. Push one: plitzi runtime push.`);
|
|
5560
7260
|
const spendOf = (name) => {
|
|
5561
7261
|
const size = runtime.sizes.find((option) => option.name === name);
|
|
@@ -5578,7 +7278,7 @@ var runtimeStatus = async (options) => {
|
|
|
5578
7278
|
};
|
|
5579
7279
|
/** Starts an environment's runtime again, or stops it — kept stopped until started. */
|
|
5580
7280
|
var powerRuntime = async (power, options) => {
|
|
5581
|
-
const connection = await
|
|
7281
|
+
const connection = await connectToSpace(options, "to configure");
|
|
5582
7282
|
if (!connection?.space) return;
|
|
5583
7283
|
const environment = options.environment ?? "main";
|
|
5584
7284
|
const answered = await authorizedRequest(connection, `/spaces/${String(connection.space.id)}/runtime/${power}`, {
|
|
@@ -5595,11 +7295,11 @@ var powerRuntime = async (power, options) => {
|
|
|
5595
7295
|
fail(reply.data.error ?? `The runtime was not ${power === "start" ? "started" : "stopped"} (${String(reply.status)}).`);
|
|
5596
7296
|
return;
|
|
5597
7297
|
}
|
|
5598
|
-
console.log(
|
|
7298
|
+
console.log(chalk.green(`${environment} ${power === "start" ? "starts in a moment" : "stops, and stays stopped until started"}.`));
|
|
5599
7299
|
};
|
|
5600
7300
|
/** Chooses the size an environment's runtime runs at — one the space's plan includes — and it starts again at it. */
|
|
5601
7301
|
var setRuntimeSize = async (size, options) => {
|
|
5602
|
-
const connection = await
|
|
7302
|
+
const connection = await connectToSpace(options, "to configure");
|
|
5603
7303
|
if (!connection?.space) return;
|
|
5604
7304
|
const environment = options.environment ?? "main";
|
|
5605
7305
|
const answered = await authorizedRequest(connection, `/spaces/${String(connection.space.id)}/runtime/size`, {
|
|
@@ -5619,7 +7319,7 @@ var setRuntimeSize = async (size, options) => {
|
|
|
5619
7319
|
fail(reply.data.error ?? `The size was not changed (${String(reply.status)}).`);
|
|
5620
7320
|
return;
|
|
5621
7321
|
}
|
|
5622
|
-
console.log(
|
|
7322
|
+
console.log(chalk.green(`${environment} runs at ${size} — the runtime starts again at it.`));
|
|
5623
7323
|
};
|
|
5624
7324
|
/** Everything sent on standard input: a value piped in rather than typed where the shell history keeps it. */
|
|
5625
7325
|
var readStdin = async () => {
|
|
@@ -5632,7 +7332,7 @@ var readStdin = async () => {
|
|
|
5632
7332
|
* input (`printf %s "$URL" | plitzi runtime vars set REDIS_URL`), which keeps a secret out of the shell's history.
|
|
5633
7333
|
*/
|
|
5634
7334
|
var setRuntimeVariable = async (name, value, options) => {
|
|
5635
|
-
const connection = await
|
|
7335
|
+
const connection = await connectToSpace(options, "to configure");
|
|
5636
7336
|
if (!connection?.space) return;
|
|
5637
7337
|
const given = value ?? await readStdin();
|
|
5638
7338
|
const answered = await authorizedRequest(connection, `/spaces/${String(connection.space.id)}/runtime/variables/${encodeURIComponent(name)}`, {
|
|
@@ -5649,17 +7349,190 @@ var setRuntimeVariable = async (name, value, options) => {
|
|
|
5649
7349
|
fail(reply.data.error ?? `${name} was not set (${String(reply.status)}).`);
|
|
5650
7350
|
return;
|
|
5651
7351
|
}
|
|
5652
|
-
console.log(
|
|
7352
|
+
console.log(chalk.green(`${name} set — the runtime starts again with it.`));
|
|
5653
7353
|
};
|
|
5654
7354
|
var unsetRuntimeVariable = async (name, options) => {
|
|
5655
|
-
const connection = await
|
|
7355
|
+
const connection = await connectToSpace(options, "to configure");
|
|
5656
7356
|
if (!connection?.space) return;
|
|
5657
7357
|
const answered = await authorizedRequest(connection, `/spaces/${String(connection.space.id)}/runtime/variables/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
5658
7358
|
if (!answered.ok) {
|
|
5659
7359
|
fail(answered.error);
|
|
5660
7360
|
return;
|
|
5661
7361
|
}
|
|
5662
|
-
console.log(
|
|
7362
|
+
console.log(chalk.green(`${name} removed — the runtime starts again without it.`));
|
|
7363
|
+
};
|
|
7364
|
+
//#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
|
+
}
|
|
5663
7536
|
};
|
|
5664
7537
|
//#endregion
|
|
5665
7538
|
//#region src/commands/uploadPlugin.ts
|
|
@@ -5843,6 +7716,29 @@ var uploadPluginCommand = async (zipGiven, options) => {
|
|
|
5843
7716
|
console.log("");
|
|
5844
7717
|
};
|
|
5845
7718
|
//#endregion
|
|
7719
|
+
//#region src/options.ts
|
|
7720
|
+
/**
|
|
7721
|
+
* What a flag's value has to be, read once where the command line is declared. A value that is not one is refused
|
|
7722
|
+
* there, saying what it takes — never turned into a default the person did not ask for, which is how a typo used to
|
|
7723
|
+
* check a page at no width at all and say nothing.
|
|
7724
|
+
*/
|
|
7725
|
+
var MIN_WIDTH = 320;
|
|
7726
|
+
var MAX_WIDTH = 3840;
|
|
7727
|
+
/** A viewport width: a whole number of pixels, from a small phone to a 4K screen. */
|
|
7728
|
+
var width = (value) => {
|
|
7729
|
+
const parsed = Number(value.trim());
|
|
7730
|
+
if (!Number.isInteger(parsed) || parsed < MIN_WIDTH || parsed > MAX_WIDTH) throw new InvalidArgumentError(`A width is a whole number of pixels from ${String(MIN_WIDTH)} to ${String(MAX_WIDTH)}: 1440 or 390.`);
|
|
7731
|
+
return parsed;
|
|
7732
|
+
};
|
|
7733
|
+
/** Widths separated by commas, each once, in the order given. */
|
|
7734
|
+
var widths = (value) => [...new Set(value.split(",").map(width))];
|
|
7735
|
+
/** A whole number from 1: a height in pixels, a count, milliseconds. */
|
|
7736
|
+
var positiveInteger = (value) => {
|
|
7737
|
+
const parsed = Number(value.trim());
|
|
7738
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("It takes a whole number from 1.");
|
|
7739
|
+
return parsed;
|
|
7740
|
+
};
|
|
7741
|
+
//#endregion
|
|
5846
7742
|
//#region src/index.ts
|
|
5847
7743
|
/**
|
|
5848
7744
|
* The command line for Plitzi.
|
|
@@ -5852,28 +7748,45 @@ var uploadPluginCommand = async (zipGiven, options) => {
|
|
|
5852
7748
|
* could tell whether any of it worked. The same holds for an element of one's own, which is the second thing it does.
|
|
5853
7749
|
*/
|
|
5854
7750
|
program.name("plitzi").description("Plitzi command line");
|
|
7751
|
+
/** A flag given more than once, as the list of every value. */
|
|
7752
|
+
var collect = (value, previous) => [...previous, value];
|
|
5855
7753
|
/** The flags that shape a PROJECT: given with `--plugin`, they are a mistake worth saying rather than ignoring. */
|
|
5856
7754
|
var PROJECT_ONLY = [
|
|
5857
7755
|
"mode",
|
|
5858
7756
|
"source",
|
|
5859
|
-
"key"
|
|
7757
|
+
"key",
|
|
7758
|
+
"template"
|
|
5860
7759
|
];
|
|
7760
|
+
/** The widths a page is looked at, as one flag every command that opens a page shares. */
|
|
7761
|
+
var widthsOption = (description, fallback) => new Option("--width <px,px>", description).argParser(widths).default(fallback, fallback.join(","));
|
|
7762
|
+
var schemeOption = () => new Option("--scheme <scheme>", "The colour scheme").choices(SCHEMES).default("light");
|
|
5861
7763
|
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."];
|
|
5862
|
-
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").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) => {
|
|
7764
|
+
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) => {
|
|
5863
7765
|
if (!options.plugin) return create(directory, options);
|
|
5864
7766
|
const stray = PROJECT_ONLY.filter((flag) => options[flag] !== void 0);
|
|
5865
7767
|
if (stray.length > 0) program.error(`--${stray.join(", --")} shape a project, not a plugin: leave them out with --plugin.`);
|
|
5866
7768
|
return createPlugin(directory, options);
|
|
5867
7769
|
});
|
|
5868
7770
|
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));
|
|
5869
|
-
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("-f, --force", "Write into a folder that is not empty").action((names, options) => addPlugin(names, options));
|
|
7771
|
+
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));
|
|
5870
7772
|
var pack = program.command("pack").description("Build something of this project into what the platform takes");
|
|
5871
7773
|
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));
|
|
5872
7774
|
pack.command("source").argument("<entries...>", "The files the build starts from: a runtime module, or each element’s index").description("Write the source snapshot plitzi upload and plitzi runtime push keep beside what they send").addOption(new Option("--kind <kind>", "What it is the source of").choices(["plugin", "runtime"]).makeOptionMandatory()).requiredOption("--name <name>", "The plugin’s type, or runtime").option("--root <folder>", "The project the paths are relative to. Defaults to the nearest package.json’s folder.").requiredOption("-o, --out <file>", "Where the gzipped snapshot goes").action((entries, options) => packSourceCommand(entries, options));
|
|
5873
7775
|
program.command("login").description("Sign in, in your browser. The session is kept and renewed until plitzi logout").option(...API_OPTION).action((options) => login(options));
|
|
5874
7776
|
program.command("logout").description("Sign out: the session is revoked on the platform and forgotten here").action(() => logout());
|
|
5875
|
-
program.command("whoami").description("Who the CLI is signed in as, and the space it works in").option(...API_OPTION).action((options) => whoami(options));
|
|
7777
|
+
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));
|
|
5876
7778
|
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));
|
|
7780
|
+
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
|
+
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
|
+
1440,
|
|
7783
|
+
768,
|
|
7784
|
+
390
|
|
7785
|
+
])).option("-f, --force", "Write over what --out already holds").option("--json", "One object, for a tool or an agent").option(...API_OPTION).action((url, options) => importPage(url, options));
|
|
7786
|
+
program.command("shot").argument("[path]", "The page: /about. The home page when left out").description("A picture of one page of the running project — and, asked, how it differs from another or what moves").addOption(new Option("--width <px>", "The viewport width").argParser(width).default(1280)).addOption(new Option("--height <px>", "The viewport height (the picture is the whole page)").argParser(positiveInteger).default(800)).addOption(schemeOption()).option("-o, --out <file>", "Where the picture goes. visual/.shots/<page>-<width>-<scheme>.png by default").option("--compare <url>", "Another site: the same page there, side by side, and how much differs by section").addOption(new Option("--frames <n>", "Pictures taken one after another: which sections move (a marquee, an autoplay)").argParser(positiveInteger)).addOption(new Option("--every <ms>", "How far apart --frames are taken").argParser(positiveInteger).default(500)).option("--wait-for <element>", "An element to wait for first: its name (data-plitzi-el) or a CSS selector").option("--reduced-motion", "As a visitor who asked for less motion").option("--json", "One object, for a tool or an agent").action((path, options) => shot(path, options));
|
|
7787
|
+
program.command("explain").argument("[name]", "An element, a step, a trigger, a problem’s code or a transformer: container, navigate, class-and-css").description("What a name means when authoring a space — what it takes, fires, reads and how it is written").option("--list <kind>", "Every name of a kind: elements, steps, triggers, codes, transformers").option("--json", "One object, for a tool or an agent").action((name, options) => explainCommand(name, options));
|
|
7788
|
+
program.command("skills").description("The Plitzi skills an agent reads in .claude/skills/").command("update").description("Bring them up to the Plitzi packages this project has installed, each replaced whole").action(() => skillsUpdate());
|
|
7789
|
+
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));
|
|
5877
7790
|
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));
|
|
5878
7791
|
var functions = program.command("functions").description("The space’s own server code: functions/ in this project is a working copy of it");
|
|
5879
7792
|
functions.command("pull").description("Write the space’s functions into functions/ — refused when that would overwrite what is not pushed").option("-f, --force", "Overwrite what is not pushed, or a copy of another space").option(...API_OPTION).action((options) => pullFunctions(options));
|
|
@@ -5882,11 +7795,11 @@ functions.command("try").argument("<task>", "The task, <namespace>.<action>").de
|
|
|
5882
7795
|
functions.command("dev").argument("<task>", "The task, <namespace>.<action>").description("Run one task from functions/ on this machine, as the platform runs it — nothing reaches the space").option("--params <json>", "Its params, as a JSON object").option("-w, --watch", "Run it again every time a file of functions/ is saved").action((task, options) => devFunction(task, options));
|
|
5883
7796
|
var runtime = program.command("runtime").description("The space’s runtime: its own server code, run as a process of its own beside the platform");
|
|
5884
7797
|
runtime.command("push").description("Pack this project’s runtime module and keep it as the space’s draft runtime").option("--entry <path>", "The module whose default export is defineRuntime(…)", "src/runtime.ts").option(...API_OPTION).action((options) => pushRuntime(options));
|
|
5885
|
-
runtime.command("status").description("How each environment’s runtime is, and the names of its variables").option(...API_OPTION).action((options) => runtimeStatus(options));
|
|
5886
|
-
for (const power of ["start", "stop"]) runtime.command(power).description(power === "start" ? "Start an environment’s runtime again — one stopped for being unused, or by hand" : "Stop an environment’s runtime, and keep it stopped until it is started").option("--environment <name>", "The environment: main (the draft) or a published one", "main").option(...API_OPTION).action((options) => powerRuntime(power, options));
|
|
5887
|
-
runtime.command("size").description("Choose the size an environment’s runtime runs at, among those the space’s plan includes").argument("<size>", "small, medium or large — plitzi runtime status says which the plan includes").option("--environment <name>", "The environment: main (the draft) or a published one", "main").option(...API_OPTION).action((size, options) => setRuntimeSize(size, options));
|
|
7798
|
+
runtime.command("status").description("How each environment’s runtime is, and the names of its variables").option("--json", "One object: { space, environments, variables, sizes }, for a tool or an agent").option(...API_OPTION).action((options) => runtimeStatus(options));
|
|
7799
|
+
for (const power of ["start", "stop"]) runtime.command(power).description(power === "start" ? "Start an environment’s runtime again — one stopped for being unused, or by hand" : "Stop an environment’s runtime, and keep it stopped until it is started").option("-e, --environment <name>", "The environment: main (the draft) or a published one", "main").option(...API_OPTION).action((options) => powerRuntime(power, options));
|
|
7800
|
+
runtime.command("size").description("Choose the size an environment’s runtime runs at, among those the space’s plan includes").argument("<size>", "small, medium or large — plitzi runtime status says which the plan includes").option("-e, --environment <name>", "The environment: main (the draft) or a published one", "main").option(...API_OPTION).action((size, options) => setRuntimeSize(size, options));
|
|
5888
7801
|
var vars = runtime.command("vars").description("What the runtime starts with — written, never read back");
|
|
5889
|
-
vars.command("set").argument("<name>", "The variable, in capitals: REDIS_URL").argument("[value]", "Its value — read from standard input when left out, which keeps it out of the shell history").option(...API_OPTION).action((name, value, options) => setRuntimeVariable(name, value, options));
|
|
5890
|
-
vars.command("unset").argument("<name>", "The variable").option(...API_OPTION).action((name, options) => unsetRuntimeVariable(name, options));
|
|
7802
|
+
vars.command("set").description("Give the runtime a variable, or a new value for one it has").argument("<name>", "The variable, in capitals: REDIS_URL").argument("[value]", "Its value — read from standard input when left out, which keeps it out of the shell history").option(...API_OPTION).action((name, value, options) => setRuntimeVariable(name, value, options));
|
|
7803
|
+
vars.command("unset").description("Take a variable away from the runtime").argument("<name>", "The variable").option(...API_OPTION).action((name, options) => unsetRuntimeVariable(name, options));
|
|
5891
7804
|
program.parse(process.argv);
|
|
5892
7805
|
//#endregion
|