@plitzi/cli 0.37.8 → 0.37.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,12 +9,13 @@ import { createHash, randomBytes } from "node:crypto";
9
9
  import { createServer } from "node:http";
10
10
  import tls from "node:tls";
11
11
  import readline from "node:readline/promises";
12
- import { createRequire } from "node:module";
12
+ import { builtinModules, createRequire } from "node:module";
13
13
  import { readFileSync, readdirSync, watch } from "node:fs";
14
- import { blankSpaceSource, elementTypeNames } from "@plitzi/sdk-authoring";
14
+ import { actionSpecFromEntry, actionToSource, blankSpaceSource, elementTypeNames } from "@plitzi/sdk-authoring";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { build } from "esbuild";
17
17
  import { unzipSync, zipSync } from "fflate";
18
+ import { gzipSync } from "node:zlib";
18
19
  /** `$XDG_CONFIG_HOME/plitzi`, `%APPDATA%\plitzi` on Windows, `~/.config/plitzi` otherwise. */
19
20
  var configDir = () => {
20
21
  if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, "plitzi");
@@ -642,6 +643,22 @@ var connectionWithSpace = async (api, doing = "to work in") => {
642
643
  }
643
644
  return chosen.value;
644
645
  };
646
+ /**
647
+ * A signed-in connection, whichever space it works in — or none: signing in in the browser when there is no session.
648
+ * What a command that names its space itself needs (`plitzi create --from`), since the session reaches every space the
649
+ * person can.
650
+ */
651
+ var signedIn = async (api, doing) => {
652
+ const current = await currentConnection(api);
653
+ if (current.ok && current.value) return current.value;
654
+ console.log(`\nSign in ${doing}.`);
655
+ const connected = await connect$2(api, { open: openInBrowser });
656
+ if (!connected.ok) {
657
+ fail(connected.error);
658
+ return;
659
+ }
660
+ return connected.value;
661
+ };
645
662
  //#endregion
646
663
  //#region src/scaffold/client.ts
647
664
  /**
@@ -1263,13 +1280,21 @@ var scripts = ({ mode, source }) => ({
1263
1280
  visual: "playwright test",
1264
1281
  shot: "node scripts/shot.ts"
1265
1282
  });
1266
- var packageJson$1 = (answers) => `${JSON.stringify({
1283
+ /**
1284
+ * `extra` is what a project made from a space needs besides (`plitzi create --from`): the packages its plugins and
1285
+ * runtime import. The scaffold's own win where both name one — the SDK and React are this CLI's versions, which the
1286
+ * project's plugins are rebuilt against.
1287
+ */
1288
+ var packageJson$1 = (answers, extra = {}) => `${JSON.stringify({
1267
1289
  name: answers.name,
1268
1290
  version: "0.0.0",
1269
1291
  private: true,
1270
1292
  type: "module",
1271
1293
  scripts: scripts(answers),
1272
- dependencies: dependencies(answers),
1294
+ dependencies: Object.fromEntries(Object.entries({
1295
+ ...extra,
1296
+ ...dependencies(answers)
1297
+ }).sort(([a], [b]) => a.localeCompare(b))),
1273
1298
  devDependencies: devDependencies(answers),
1274
1299
  engines: NODE_ENGINES,
1275
1300
  ...managerPackageFields(answers.packageManager)
@@ -1430,14 +1455,14 @@ ${code(".claude/skills/plitzi-cli/SKILL.md")} first: ${code("plitzi add plugin")
1430
1455
  * prefix — ship a server credential to every visitor. The two modes therefore name different variables, because
1431
1456
  * they hold different keys.
1432
1457
  */
1433
- var envFile = ({ key, environment, mode }) => mode === "server" ? `# The space's self-hosting key. Secret: never commit it, never ship it in a page.
1458
+ var envFile = ({ key, environment, revision, mode }) => mode === "server" ? `# The space's self-hosting key. Secret: never commit it, never ship it in a page.
1434
1459
  # Credentials, in the builder.
1435
1460
  PLITZI_HOST_KEY=${key}
1436
1461
 
1437
1462
  # Which version this serves. 'main' is what the builder is editing; a published environment serves the latest
1438
1463
  # release, and PLITZI_REVISION pins one exact version.
1439
1464
  PLITZI_ENVIRONMENT=${environment}
1440
- # PLITZI_REVISION=12
1465
+ ${revision ? `PLITZI_REVISION=${String(revision)}` : "# PLITZI_REVISION=12"}
1441
1466
 
1442
1467
  PORT=8080
1443
1468
  ` : `# The space's public render key. It ships in the page by design; the origin the browser states is what
@@ -3368,198 +3393,1126 @@ This project lists its plugins in src/main.ts (projects created since find them
3368
3393
  }
3369
3394
  };
3370
3395
  //#endregion
3371
- //#region src/commands/create.ts
3372
- var MODES = ["server", "client"];
3373
- var SOURCES = ["local", "cloud"];
3374
- /**
3375
- * The choices a project is BUILT around, which belong to whoever it is for — never to a default nobody chose.
3376
- *
3377
- * Each one is written into the project: the package manager into every command its README, lockfile, Playwright config
3378
- * and install use; the mode into whether there is a Node tier at all; the source into whether the space is in the repo.
3379
- * Assumed, they are the wrong project for somebody, and an agent running this was the one assuming. So what was not
3380
- * passed is asked for when a person is at the terminal, and refused when nobody is — with the exact questions to put
3381
- * to the person.
3382
- */
3383
- var resolveDecisions = async (options) => {
3384
- const detected = detectPackageManager();
3385
- const packageManager = {
3386
- flag: "--package-manager",
3387
- choices: PACKAGE_MANAGERS,
3388
- question: `Which package manager will you work in? (this was run through ${detected})`,
3389
- given: PACKAGE_MANAGERS.find((manager) => manager === options.packageManager),
3390
- fallback: detected
3391
- };
3392
- const mode = {
3393
- flag: "--mode",
3394
- choices: MODES,
3395
- question: "Should pages render on a Node server of your own (server: SSR + RSC), or only in the browser (client)?",
3396
- given: MODES.find((candidate) => candidate === options.mode),
3397
- fallback: "server"
3398
- };
3399
- const source = {
3400
- flag: "--source",
3401
- choices: SOURCES,
3402
- question: "Should the space live in the project as code you edit (local), or be read live from your Plitzi account (cloud, which needs a key from Credentials in the builder)?",
3403
- given: SOURCES.find((candidate) => candidate === options.source),
3404
- fallback: "local"
3405
- };
3406
- const missing = [
3407
- packageManager,
3408
- mode,
3409
- source
3410
- ].filter((question) => question.given === void 0);
3411
- if (missing.length === 0 || options.yes && atTerminal()) return {
3412
- packageManager: packageManager.given ?? packageManager.fallback,
3413
- mode: mode.given ?? mode.fallback,
3414
- source: source.given ?? source.fallback
3415
- };
3416
- if (!atTerminal()) {
3417
- refuseWithoutTerminal("project", missing);
3418
- return;
3419
- }
3420
- const rl = readline.createInterface({
3421
- input: process.stdin,
3422
- output: process.stdout
3423
- });
3424
- try {
3425
- const manager = packageManager.given ?? await ask(rl, packageManager);
3426
- const tier = manager && (mode.given ?? await ask(rl, mode));
3427
- const origin = tier && (source.given ?? await ask(rl, source));
3428
- if (!manager || !tier || !origin) {
3429
- process.exitCode = 1;
3396
+ //#region ../../packages/sdk-shared/dist/actions/functions.mjs
3397
+ 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
+ var i$1 = async (e, t) => {
3399
+ let n = {}, i = async (a) => {
3400
+ let o;
3401
+ try {
3402
+ o = await t.list(a ? `${e}/${a}` : e);
3403
+ } catch {
3430
3404
  return;
3431
3405
  }
3406
+ for (let s of o) {
3407
+ let o = a ? `${a}/${s.name}` : s.name;
3408
+ s.directory ? s.name !== "node_modules" && !s.name.startsWith(".") && await i(o) : r$1(o) && (n[o] = await t.read(`${e}/${o}`));
3409
+ }
3410
+ };
3411
+ return await i(""), n;
3412
+ };
3413
+ //#endregion
3414
+ //#region src/commands/functions.ts
3415
+ var FUNCTIONS_DIR = "functions";
3416
+ var STATE_FILE = path.join(".plitzi", "functions.json");
3417
+ var isRecord$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3418
+ /** The project's root: where `functions/` and `.plitzi/` go. */
3419
+ var rootOf = async () => (await findProject(process.cwd()))?.root ?? process.cwd();
3420
+ /** Every source file under `functions/`, by its path there — read by the one rule of what a source is. */
3421
+ var readLocal = (root) => i$1(path.join(root, FUNCTIONS_DIR), {
3422
+ list: async (dir) => (await fs.readdir(dir, { withFileTypes: true })).map((entry) => ({
3423
+ name: entry.name,
3424
+ directory: entry.isDirectory()
3425
+ })),
3426
+ read: (file) => fs.readFile(file, "utf8")
3427
+ });
3428
+ var readState = async (root) => {
3429
+ try {
3430
+ const value = JSON.parse(await fs.readFile(path.join(root, STATE_FILE), "utf8"));
3431
+ if (!isRecord$6(value) || typeof value.space !== "number" || typeof value.version !== "string" || !isRecord$6(value.files)) return;
3432
+ const files = Object.fromEntries(Object.entries(value.files).flatMap(([name, text]) => typeof text === "string" ? [[name, text]] : []));
3432
3433
  return {
3433
- packageManager: manager,
3434
- mode: tier,
3435
- source: origin
3434
+ space: value.space,
3435
+ version: value.version,
3436
+ files
3436
3437
  };
3437
- } finally {
3438
- rl.close();
3438
+ } catch {
3439
+ return;
3439
3440
  }
3440
3441
  };
3441
- var keyQuestion = (mode) => `The ${mode === "server" ? "self-hosting" : "public render"} key the project reads the space with (Credentials, in the builder).`;
3442
- var askForKey = async (mode) => {
3443
- const rl = readline.createInterface({
3444
- input: process.stdin,
3445
- output: process.stdout
3446
- });
3447
- try {
3448
- return (await rl.question(`\n${keyQuestion(mode)}\n> `)).trim();
3449
- } finally {
3450
- rl.close();
3451
- }
3442
+ var writeFunctionsState = async (root, state) => {
3443
+ await fs.mkdir(path.join(root, ".plitzi"), { recursive: true });
3444
+ await fs.writeFile(path.join(root, STATE_FILE), `${JSON.stringify(state, null, 2)}\n`);
3452
3445
  };
3453
- var create = async (directory, options) => {
3454
- const target = path.resolve(directory ?? ".");
3455
- const name = path.basename(target);
3456
- const wasEmpty = await isEmpty(target);
3457
- if (!options.force && !wasEmpty) {
3458
- console.error(chalk.red(`${target} is not empty. Pass --force to write into it anyway.`));
3459
- process.exitCode = 1;
3446
+ /** The files that differ between two copies — added, removed or changed — sorted. */
3447
+ var changedFiles = (a, b) => [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((file) => a[file] !== b[file]).sort();
3448
+ 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
+ var readDraft = async (connection, spaceId) => {
3454
+ const answered = await authorizedRequest(connection, `/spaces/${spaceId}/functions`);
3455
+ if (!answered.ok) {
3456
+ fail(answered.error);
3460
3457
  return;
3461
3458
  }
3462
- const decisions = await resolveDecisions(options);
3463
- if (!decisions) return;
3464
- const { packageManager, mode, source } = decisions;
3465
- if (source === "cloud" && !options.key && !atTerminal()) {
3466
- refuseWithoutTerminal("project", [{
3467
- flag: "--key",
3468
- choices: ["<key>"],
3469
- question: keyQuestion(mode)
3470
- }]);
3459
+ const { reply } = answered.value;
3460
+ if (reply.status !== 200) {
3461
+ fail(reply.data.error ?? `Could not read the space’s functions (${reply.status}).`);
3471
3462
  return;
3472
3463
  }
3473
- const key = source === "cloud" ? options.key ?? await askForKey(mode) : "";
3474
- if (source === "cloud" && !key) {
3475
- console.error(chalk.red("\nNo key. Mint one under Credentials in the builder, then run this again."));
3476
- process.exitCode = 1;
3464
+ return reply.data;
3465
+ };
3466
+ var pullFunctions = async (options) => {
3467
+ const connection = await connect$1(options, "to pull from");
3468
+ if (!connection?.space) return;
3469
+ const root = await rootOf();
3470
+ const [local, state] = await Promise.all([readLocal(root), readState(root)]);
3471
+ const unpushed = state ? changedFiles(local, state.files) : Object.keys(local);
3472
+ if (!options.force && (state?.space ?? connection.space.id) !== connection.space.id) {
3473
+ fail("functions/ is a copy of another space’s. Pull into another project, or pass --force to replace it.");
3477
3474
  return;
3478
3475
  }
3479
- await writeFiles(target, scaffold({
3480
- name,
3481
- mode,
3482
- source,
3483
- key,
3484
- environment: options.environment ?? "main",
3485
- packageManager,
3486
- managerVersion: detectManagerVersion(packageManager, await nearestExisting(target))
3487
- }));
3488
- const wantsInstall = options.install !== false;
3489
- const installed = wantsInstall && await install(packageManager, target);
3490
- /**
3491
- * A failed install is a failed command, not a next step.
3492
- *
3493
- * The files are already written, so the summary below still prints — but printed on its own after a screen of
3494
- * resolver errors it reads as success, and a script that ran `create` carries on into a project with no
3495
- * `node_modules`.
3496
- */
3497
- if (wantsInstall && !installed) {
3498
- console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The project is written; the reason is in the output above.`));
3499
- console.error(chalk.dim(INSTALL_HINTS[packageManager]));
3500
- process.exitCode = 1;
3501
- }
3502
- /**
3503
- * Formatted by the project's own Prettier, once, so the first commit is already in its style — the scaffold writes
3504
- * tables and lists whose layout depends on what they hold. Only into a folder that was empty: `--force` into one
3505
- * with work in it must never reformat that work.
3506
- */
3507
- if (installed && wasEmpty) await runScript(packageManager, "format", target);
3508
- const where = cdPrefix(target);
3509
- console.log(chalk.green(`\n${name} — ${mode === "server" ? "server-rendered" : "browser-rendered"}, space ${source === "cloud" ? "from Plitzi" : "in the project"}.`));
3510
- console.log("");
3511
- if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
3512
- console.log(` ${where}${runCommand(packageManager, "start")}`);
3513
- console.log("");
3514
- console.log(chalk.dim(`.claude/skills carries Plitzi's authoring skill; \`${runCommand(packageManager, "visual")}\` opens the page and checks it.`));
3515
- if (source === "cloud") console.log(chalk.dim(".env holds the key and is already git-ignored."));
3516
- console.log("");
3517
- };
3518
- //#endregion
3519
- //#region src/commands/createPlugin.ts
3520
- var DEFAULT_NAME = "plitzi-plugin-my-element";
3521
- /** What an agent is told to ask for, and the flags that carry the answers. */
3522
- var QUESTIONS = {
3523
- name: {
3524
- flag: "--name",
3525
- choices: ["<package name>"],
3526
- question: "What is the plugin called? Its package name, e.g. plitzi-plugin-seat-picker."
3527
- },
3528
- directory: {
3529
- flag: "<directory> (the first argument)",
3530
- choices: ["<folder>"],
3531
- question: "Which folder should the plugin be written to?"
3476
+ if (!options.force && unpushed.length) {
3477
+ fail(`Pulling would overwrite what is not pushed yet: ${unpushed.join(", ")}.\nPush it first, or pass --force to throw it away.`);
3478
+ return;
3532
3479
  }
3533
- };
3534
- var splitNames = (list) => list.split(",").map((name) => name.trim()).filter(Boolean);
3535
- /**
3536
- * What is wrong with the other elements asked for, if anything: a name that makes no element, or two elements — the
3537
- * package's own among them — that would share a type, which a space could not tell apart.
3538
- */
3539
- var elementListProblem = (main, others) => {
3540
- for (const name of others) {
3541
- const problem = pluginNameProblem(name);
3542
- if (problem) return problem;
3480
+ const draft = await readDraft(connection, connection.space.id);
3481
+ if (!draft) return;
3482
+ const base = path.join(root, FUNCTIONS_DIR);
3483
+ await Promise.all(Object.keys(local).filter((file) => !Object.hasOwn(draft.files, file)).map((file) => fs.rm(path.join(base, file))));
3484
+ for (const [file, text] of Object.entries(draft.files)) {
3485
+ await fs.mkdir(path.dirname(path.join(base, file)), { recursive: true });
3486
+ await fs.writeFile(path.join(base, file), text);
3543
3487
  }
3544
- const types = [pluginNames(main).type, ...others.map((name) => pluginNames(name).type)];
3545
- const repeated = types.find((type, index) => types.indexOf(type) !== index);
3546
- return repeated ? `Two elements would both be "${repeated}": give each a name of its own.` : void 0;
3547
- };
3548
- /** The folders a repository keeps its packages in, as places for this one — or `./<name>` outside of any. */
3549
- var folderOptions = (project, base) => {
3550
- if (!project) return [];
3551
- const root = project.workspaceRoot ?? project.root;
3552
- return [.../* @__PURE__ */ new Set([...project.workspaceFolders, "plugins"])].map((folder) => {
3553
- const value = path.join(root, folder, base);
3554
- return {
3555
- label: path.relative(process.cwd(), value) || ".",
3556
- value
3557
- };
3488
+ await writeFunctionsState(root, {
3489
+ space: connection.space.id,
3490
+ version: draft.version,
3491
+ files: draft.files
3558
3492
  });
3493
+ const count = Object.keys(draft.files).length;
3494
+ console.log(count ? `Pulled ${count} file${count === 1 ? "" : "s"} of ${chalk.bold(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.`);
3559
3495
  };
3560
- var askDirectory = async (rl, project, base) => {
3561
- const typed = {
3562
- question: "Which folder? (relative to here)",
3496
+ var printProblems = (problems) => {
3497
+ console.error(chalk.red("The functions were not saved:"));
3498
+ problems.forEach(({ file, line, column, message }) => {
3499
+ const where = file ? `functions/${file}${line ? `:${String(line)}${column ? `:${String(column)}` : ""}` : ""}` : "functions";
3500
+ console.error(` ${chalk.bold(where)} ${message}`);
3501
+ });
3502
+ process.exitCode = 1;
3503
+ };
3504
+ var pushFunctions = async (options) => {
3505
+ const connection = await connect$1(options, "to push to");
3506
+ if (!connection?.space) return;
3507
+ const root = await rootOf();
3508
+ const [local, state] = await Promise.all([readLocal(root), readState(root)]);
3509
+ if (!Object.hasOwn(local, "index.ts")) {
3510
+ fail("There is no functions/index.ts here: it is where a space’s functions start. Pull them, or write it.");
3511
+ return;
3512
+ }
3513
+ let base = state?.space === connection.space.id ? state.version : void 0;
3514
+ if (base === void 0) {
3515
+ const draft = await readDraft(connection, connection.space.id);
3516
+ if (!draft) return;
3517
+ if (Object.keys(draft.files).length) {
3518
+ fail(`${connection.space.name} already has functions. Pull them first: plitzi functions pull.`);
3519
+ return;
3520
+ }
3521
+ base = draft.version;
3522
+ }
3523
+ if (state?.space === connection.space.id && !changedFiles(local, state.files).length) {
3524
+ console.log("Nothing to push: functions/ is what was pulled.");
3525
+ return;
3526
+ }
3527
+ const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions`, {
3528
+ method: "PUT",
3529
+ headers: { "Content-Type": "application/json" },
3530
+ body: JSON.stringify({
3531
+ files: local,
3532
+ base
3533
+ })
3534
+ });
3535
+ if (!answered.ok) {
3536
+ fail(answered.error);
3537
+ return;
3538
+ }
3539
+ const { reply } = answered.value;
3540
+ if (reply.status === 422 && reply.data.problems) {
3541
+ printProblems(reply.data.problems);
3542
+ return;
3543
+ }
3544
+ if (reply.status === 409) {
3545
+ fail(`${connection.space.name}’s functions changed since your pull — in the builder, or from another copy. Keep your changes aside, pull, and apply them again.`);
3546
+ return;
3547
+ }
3548
+ if (reply.status !== 200 || !reply.data.version) {
3549
+ fail(reply.data.refusal?.error ?? reply.data.error ?? `The functions were not saved (${reply.status}).`);
3550
+ return;
3551
+ }
3552
+ await writeFunctionsState(root, {
3553
+ space: connection.space.id,
3554
+ version: reply.data.version,
3555
+ files: local
3556
+ });
3557
+ console.log(`Pushed to ${chalk.bold(connection.space.name)}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}. ` + chalk.dim("The live site runs them once the space is published."));
3558
+ };
3559
+ /** What one run of a task did: its logs, then its value — or why it failed, as a failure of the command. */
3560
+ var printRun = (task, report) => {
3561
+ const step = report.steps?.find((entry) => entry.action === task);
3562
+ const took = step ? `${String(step.endTime - step.startTime)} ms` : "";
3563
+ step?.logs?.forEach((line) => console.log(chalk.dim(`log ${line}`)));
3564
+ if (step?.error) {
3565
+ fail(`${task} failed after ${took}: ${step.error}`);
3566
+ return;
3567
+ }
3568
+ console.log(`${chalk.green(report.status ?? "completed")} ${chalk.dim(took ? `in ${took}` : "")}`);
3569
+ console.log(JSON.stringify(report.output?.value ?? null, null, 2));
3570
+ };
3571
+ /** `--params`, read: an object of the task's params by name, or nothing when it is not one. */
3572
+ var paramsOf = (given) => {
3573
+ try {
3574
+ const params = given ? JSON.parse(given) : {};
3575
+ if (isRecord$6(params)) return params;
3576
+ } catch {}
3577
+ fail("--params is a JSON object of the task’s params, by name: --params '{\"name\":\"Ada\"}'");
3578
+ };
3579
+ var tryFunction = async (task, options) => {
3580
+ const params = paramsOf(options.params);
3581
+ if (!params) return;
3582
+ const connection = await connect$1(options, "to try it in");
3583
+ if (!connection?.space) return;
3584
+ const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions/try`, {
3585
+ method: "POST",
3586
+ headers: { "Content-Type": "application/json" },
3587
+ body: JSON.stringify({
3588
+ task,
3589
+ params
3590
+ })
3591
+ });
3592
+ if (!answered.ok) {
3593
+ fail(answered.error);
3594
+ return;
3595
+ }
3596
+ const { reply } = answered.value;
3597
+ if (reply.status !== 200) {
3598
+ fail(reply.data.error ?? `The task could not be tried (${reply.status}).`);
3599
+ return;
3600
+ }
3601
+ printRun(task, reply.data);
3602
+ };
3603
+ var isLocalRunner = (value) => isRecord$6(value) && typeof value.createLocalFunctions === "function";
3604
+ /**
3605
+ * This project's own runner: `dev` runs the functions with the `@plitzi/sdk-server` the project installed, so the build,
3606
+ * the checks, the isolates and the limits are the platform's own, at the version the project is on.
3607
+ */
3608
+ var projectRunner = async (root) => {
3609
+ try {
3610
+ const entry = createRequire(path.join(root, "package.json")).resolve("@plitzi/sdk-server/functions-runner");
3611
+ const loaded = await import(pathToFileURL(entry).href);
3612
+ if (isLocalRunner(loaded)) return loaded;
3613
+ } catch {}
3614
+ fail("plitzi functions dev runs them with this project’s own @plitzi/sdk-server, and the isolates it needs:\n npm install --save-dev @plitzi/sdk-server isolated-vm core-js");
3615
+ };
3616
+ /** `PLITZI_FUNCTIONS_CREDENTIALS`, a JSON object of credential id → its keys: what `ctx.fetch` may name locally. */
3617
+ var localCredentials = () => {
3618
+ const raw = process.env.PLITZI_FUNCTIONS_CREDENTIALS;
3619
+ try {
3620
+ const value = raw ? JSON.parse(raw) : {};
3621
+ return isRecord$6(value) ? Object.fromEntries(Object.entries(value).flatMap(([id, keys]) => isRecord$6(keys) ? [[id, Object.fromEntries(Object.entries(keys).filter((pair) => typeof pair[1] === "string"))]] : [])) : {};
3622
+ } catch {
3623
+ return {};
3624
+ }
3625
+ };
3626
+ var NO_NODE_SNAPSHOT = "--no-node-snapshot";
3627
+ /**
3628
+ * The isolates `dev` runs need Node's startup snapshot off, and a process cannot turn it off once started: `dev` runs
3629
+ * itself again with it, and answers with that run's exit code.
3630
+ */
3631
+ var rerunWithoutNodeSnapshot = () => new Promise((resolve, reject) => {
3632
+ const child = spawn(process.execPath, [
3633
+ ...process.execArgv,
3634
+ NO_NODE_SNAPSHOT,
3635
+ ...process.argv.slice(1)
3636
+ ], { stdio: "inherit" });
3637
+ child.once("error", reject);
3638
+ child.once("exit", (code) => {
3639
+ resolve(code ?? 1);
3640
+ });
3641
+ });
3642
+ /**
3643
+ * `plitzi functions dev <task>`: the task run from `functions/` on this machine, as the platform would run it — and,
3644
+ * with `--watch`, again every time a file is saved. Nothing reaches the space: its draft is what `push` sends.
3645
+ */
3646
+ var devFunction = async (task, options) => {
3647
+ if (!process.execArgv.includes(NO_NODE_SNAPSHOT)) {
3648
+ process.exitCode = await rerunWithoutNodeSnapshot();
3649
+ return;
3650
+ }
3651
+ const params = paramsOf(options.params);
3652
+ const root = await rootOf();
3653
+ const runner = params ? await projectRunner(root) : void 0;
3654
+ if (!params || !runner) return;
3655
+ const local = runner.createLocalFunctions({ credentials: localCredentials() });
3656
+ const run = async () => {
3657
+ const loaded = await local.load(await readLocal(root));
3658
+ if (!loaded.ok) {
3659
+ printProblems(loaded.problems);
3660
+ return;
3661
+ }
3662
+ if (!loaded.tasks.includes(task)) {
3663
+ fail(`functions/ declares no task ${task}. It declares: ${loaded.tasks.join(", ") || "none"}.`);
3664
+ return;
3665
+ }
3666
+ printRun(task, await local.tryTask(task, params));
3667
+ };
3668
+ await run();
3669
+ if (!options.watch) return;
3670
+ console.log(chalk.dim("\nWatching functions/ — every save runs it again. ^C to stop."));
3671
+ let pending;
3672
+ watch(path.join(root, FUNCTIONS_DIR), { recursive: true }, () => {
3673
+ clearTimeout(pending);
3674
+ pending = setTimeout(() => {
3675
+ process.exitCode = void 0;
3676
+ console.log(chalk.dim(`\n— ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`));
3677
+ run();
3678
+ }, 150);
3679
+ });
3680
+ };
3681
+ //#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
+ //#region src/scaffold/fromSpace.ts
3718
+ var TEXT = /\.((m|c)?(t|j)sx?|json|css|scss|sass|less|md|txt|html|svg|ya?ml|xml|csv|graphql)$/i;
3719
+ /** Whether a file of the project is text — written, compared and formatted as such — rather than bytes. */
3720
+ var isTextFile = (path) => TEXT.test(path);
3721
+ /** The example plugin and space `create` writes for a project of its own, which one made from a space replaces. */
3722
+ var EXAMPLE = ["src/plugins/StatCard/StatCard.tsx", "src/plugins/StatCard/index.ts"];
3723
+ var decode = (base64) => Buffer.from(base64, "base64").toString("utf-8");
3724
+ /** One import line of the generated code. */
3725
+ var importLine = (names, from) => `import ${names} from '${from}';\n`;
3726
+ /** `export { pizarra as space }`: the name the project's own code imports the space under, whatever it was exported as. */
3727
+ var spaceEntry = (exportName) => `/**
3728
+ * The space, as Plitzi kept it, written out as the code that authors it (\`./space/\`): a file per page, the shared
3729
+ * classes in \`styles.ts\`. It is this project's now — edit it, and the next start renders the change.
3730
+ */
3731
+ export { ${exportName} as space } from './space/index.ts';
3732
+ `;
3733
+ /** The lines that import every action written as code, each under a name of its own however their ids camelCase. */
3734
+ var codedImports = (coded) => {
3735
+ const taken = /* @__PURE__ */ new Set();
3736
+ const imports = coded.map(({ identifier, exportName }) => {
3737
+ let name = exportName;
3738
+ for (let next = 2; taken.has(name); next += 1) name = `${exportName}${String(next)}`;
3739
+ taken.add(name);
3740
+ return {
3741
+ name,
3742
+ line: importLine(`{ ${name === exportName ? name : `${exportName} as ${name}`} }`, `./actions/${identifier}.ts`)
3743
+ };
3744
+ });
3745
+ return {
3746
+ lines: imports.map(({ line }) => line).join(""),
3747
+ names: imports.map(({ name }) => name)
3748
+ };
3749
+ };
3750
+ var actionsModule = (coded) => {
3751
+ const { lines, names } = codedImports(coded);
3752
+ return `import { readdirSync, readFileSync } from 'node:fs';
3753
+ import path from 'node:path';
3754
+ ${lines ? `\n${lines}` : ""}
3755
+ import type { ActionLookups } from '@plitzi/sdk-server/actions';
3756
+
3757
+ type ActionEntry = NonNullable<Awaited<ReturnType<ActionLookups['getAction']>>>;
3758
+ type Connector = NonNullable<Awaited<ReturnType<NonNullable<ActionLookups['getConnector']>>>>;
3759
+
3760
+ /** The JSON documents of a folder beside this file: none when there is no such folder. */
3761
+ const read = (folder: string): unknown[] => {
3762
+ const dir = path.join(import.meta.dirname, folder);
3763
+ try {
3764
+ return readdirSync(dir)
3765
+ .filter(file => file.endsWith('.json'))
3766
+ .map((file): unknown => JSON.parse(readFileSync(path.join(dir, file), 'utf-8')));
3767
+ } catch {
3768
+ return [];
3769
+ }
3770
+ };
3771
+
3772
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
3773
+ typeof value === 'object' && value !== null && !Array.isArray(value);
3774
+
3775
+ /**
3776
+ * The space's server actions, in \`src/actions/\`: one \`defineAction\` each — edit one, and the next start runs it —
3777
+ * and, as JSON, any the builder wrote in a form code has no words for. Their ways in say whether they are on.
3778
+ */
3779
+ const actions: ActionEntry[] = [${names.length > 0 ? `\n ${names.join(",\n ")},` : ""}
3780
+ ...read('actions').flatMap(entry =>
3781
+ isRecord(entry) && typeof entry.id === 'string' && isRecord(entry.document)
3782
+ ? // A document Plitzi checked when it was saved, and that the server checks again on every run.
3783
+ [{ id: entry.id, document: entry.document as ActionEntry['document'] }]
3784
+ : []
3785
+ )
3786
+ ];
3787
+
3788
+ /** The space's connectors, as Plitzi kept them: one JSON manifest each, in \`src/connectors/\`. */
3789
+ const connectors = new Map(
3790
+ read('connectors').flatMap(entry =>
3791
+ isRecord(entry) && typeof entry.id === 'string' && isRecord(entry.manifest)
3792
+ ? // A manifest Plitzi checked when it was saved.
3793
+ [[entry.id, entry.manifest as Connector] as const]
3794
+ : []
3795
+ )
3796
+ );
3797
+
3798
+ /**
3799
+ * The one space this server serves, as its adapters name it — 1, whether they read it from this project or from
3800
+ * Plitzi: what the scheduler watches for actions on a clock.
3801
+ */
3802
+ const SPACE_ID = 1;
3803
+
3804
+ export const lookups: ActionLookups = {
3805
+ getAction: (_spaceId, actionId) => Promise.resolve(actions.find(entry => entry.id === actionId)),
3806
+ listActions: () => Promise.resolve(actions),
3807
+ getConnector: (_spaceId, connectorId) => Promise.resolve(connectors.get(connectorId)),
3808
+ listScheduledSpaces: () => Promise.resolve([SPACE_ID])
3809
+ };
3810
+ `;
3811
+ };
3812
+ /** What a space with visitor roles needs of a server of its own, said where its auth would be handed over. */
3813
+ var visitorsNote = (roles) => `
3814
+ /**
3815
+ * The space declares visitor roles (${roles.join(", ")}). On Plitzi its visitors sign in with their Plitzi account, and
3816
+ * hold the roles the space gives them by email. Here nobody signs in until this server does it itself: \`createAuth\`
3817
+ * from \`@plitzi/sdk-server/auth\`, handed to \`createServer\` as \`auth\`, over the accounts it keeps — giving each
3818
+ * person the permissions of the roles they hold (\`visitorAccess\` from \`@plitzi/sdk-shared/auth/visitorRoles\`, over
3819
+ * the space's \`settings.visitorRoles\`). Until then every action that asks for a role refuses. See Self-hosting, in
3820
+ * Plitzi's docs.
3821
+ */`;
3822
+ var BUILT_PLUGINS = `
3823
+ /**
3824
+ * The space's plugins no source of was kept, as they were built: \`vendor/plugins/<type>/\`, each beside the manifest it
3825
+ * was published with. They run and render as they are — on the server too — and cannot be changed here: upload one
3826
+ * again from its source (\`plitzi upload plugin\`), and \`plitzi pull\` brings its code in their place.
3827
+ */
3828
+ type BuiltManifest = {
3829
+ version?: string;
3830
+ pluginSchema?: Record<string, unknown>;
3831
+ assets?: Record<string, { src: string; type: string; isMain?: boolean }>;
3832
+ };
3833
+
3834
+ const VENDOR_PLUGINS_DIR = path.resolve(import.meta.dirname, '../vendor/plugins');
3835
+ const built = readdirSync(VENDOR_PLUGINS_DIR, { withFileTypes: true })
3836
+ .filter(entry => entry.isDirectory())
3837
+ .map(entry => {
3838
+ const dir = path.join(VENDOR_PLUGINS_DIR, entry.name);
3839
+ // What \`plitzi pack plugin\` wrote beside the bundle when it was published.
3840
+ const manifest = JSON.parse(readFileSync(path.join(dir, 'plugin-manifest.json'), 'utf-8')) as BuiltManifest;
3841
+ const assets = Object.values(manifest.assets ?? {});
3842
+ const script = assets.find(asset => asset.type === 'script' && asset.isMain) ?? assets.find(asset => asset.type === 'script');
3843
+ const style = assets.find(asset => asset.type === 'style');
3844
+ if (!script) {
3845
+ throw new Error(\`vendor/plugins/\${entry.name}/plugin-manifest.json names no script to run.\`);
3846
+ }
3847
+
3848
+ return {
3849
+ type: entry.name,
3850
+ provides: Object.keys(manifest.pluginSchema ?? {}),
3851
+ source: {
3852
+ js: path.join(dir, script.src),
3853
+ ...(style ? { css: path.join(dir, style.src) } : {}),
3854
+ action: 'copy' as const,
3855
+ version: manifest.version ?? '1.0.0'
3856
+ }
3857
+ };
3858
+ });
3859
+ const builtPlugins = Object.fromEntries(built.map(({ type, source }) => [type, source]));
3860
+ /** Every element type they provide, for the space's use of them to be authored as a plugin's rather than a typo's. */
3861
+ const builtTypes = built.flatMap(({ type, provides }) => [type, ...provides]);
3862
+ `;
3863
+ var runtimeLines = ({ runtimeEntry, packedRuntime }) => {
3864
+ if (runtimeEntry) return `/**
3865
+ * The space's runtime — its own server code, run by Plitzi beside the space — run here, in this process: its tasks join
3866
+ * the functions, and its endpoints answer before anything else. Its variables are this process's environment.
3867
+ */
3868
+ const runtime = await serveRuntime(spaceRuntime, { env: process.env, publicUrl });`;
3869
+ if (packedRuntime) return `/**
3870
+ * The space's runtime, as it was built: no source of it was kept, so it runs as it is and cannot be changed here. Push
3871
+ * it again from its source (\`plitzi runtime push\`), and \`plitzi pull\` brings its code in its place.
3872
+ */
3873
+ const runtime = await serveRuntime(
3874
+ await loadRuntime(readFileSync(path.join(PROJECT_ROOT, 'vendor/runtime.bundle')), path.join(PROJECT_ROOT, '.runtime')),
3875
+ { env: process.env, publicUrl }
3876
+ );`;
3877
+ return "";
3878
+ };
3879
+ var fromSpaceMain = (options) => {
3880
+ const { source, runtimeEntry, packedRuntime, builtPlugins, visitorRoles } = options;
3881
+ const hasRuntime = Boolean(runtimeEntry) || packedRuntime;
3882
+ const local = source === "local";
3883
+ const fsImports = ["readdirSync", ...packedRuntime || builtPlugins ? ["readFileSync"] : []].join(", ");
3884
+ const allPlugins = builtPlugins ? "{ ...plugins, ...builtPlugins }" : "plugins";
3885
+ const allNames = builtPlugins ? "[...pluginNames, ...Object.keys(builtPlugins)]" : "pluginNames";
3886
+ const runtimeImports = [...packedRuntime ? ["loadRuntime"] : [], ...hasRuntime ? ["serveRuntime"] : []].join(", ");
3887
+ return `import { ${fsImports} } from 'node:fs';
3888
+ import path from 'node:path';
3889
+
3890
+ import { closeOnSignals, consoleLogger, ${local ? "createJsonAdapters" : "createCloudAdapters"}, createServer, loadFunctions } from '@plitzi/sdk-server';
3891
+ ${hasRuntime ? importLine(`{ ${runtimeImports} }`, "@plitzi/sdk-server/runtime") : ""}${local ? `\n${importLine("{ authorSpace }", "@plitzi/sdk-authoring")}` : ""}
3892
+ ${importLine("{ lookups }", "./actions.ts")}${local ? importLine("{ declarations }", "./plugins/declarations.ts") : ""}${runtimeEntry ? importLine("spaceRuntime", runtimeEntry) : ""}${local ? importLine("{ space }", "./space.ts") : ""}
3893
+ /**
3894
+ * The project's settings — the key its actions sign with, the variables the space was given on Plitzi — kept in \`.env\`,
3895
+ * out of git. \`plitzi create\` wrote one with a fresh signing key; \`.env.example\` names the rest.
3896
+ */
3897
+ try {
3898
+ process.loadEnvFile(new URL('../.env', import.meta.url));
3899
+ } catch {
3900
+ // None: the environment the process was started with is all there is.
3901
+ }
3902
+
3903
+ const PORT = Number(process.env.PORT ?? 8080);
3904
+ // Loopback unless told otherwise: a container publishes a port only from an address it listens on (\`HOST=0.0.0.0\`).
3905
+ const HOST = process.env.HOST ?? '127.0.0.1';
3906
+ /** Where people reach this server: \`PUBLIC_URL\` behind a proxy, its own address otherwise. */
3907
+ const publicUrl = (process.env.PUBLIC_URL ?? \`http://127.0.0.1:\${String(PORT)}\`).replace(/\\/+$/, '');
3908
+ ${builtPlugins ? BUILT_PLUGINS : ""}${local ? `
3909
+ /**
3910
+ * The space, held in this project: \`src/space/\`, authored at boot — saving a page and letting \`--watch\` restart is
3911
+ * the whole edit loop, and the warnings are printed where somebody editing it is watching.
3912
+ */
3913
+ const { schema, style, warnings } = authorSpace(space, { plugins: declarations${builtPlugins ? ", pluginTypes: builtTypes" : ""} });
3914
+
3915
+ for (const warning of warnings) {
3916
+ console.warn(\`[author] \${warning.message}\`);
3917
+ }
3918
+ ` : `
3919
+ /** The space's HOST key (Credentials, in the builder): secret, never committed, never shipped in a page. */
3920
+ const HOST_KEY = process.env.PLITZI_HOST_KEY ?? '';
3921
+
3922
+ if (!HOST_KEY) {
3923
+ throw new Error('Set PLITZI_HOST_KEY in .env — Credentials, in the builder.');
3924
+ }
3925
+ `}
3926
+ ${PLUGINS}
3927
+
3928
+ /** The space's functions: \`functions/\`, built the way Plitzi builds them and run here, in this process. */
3929
+ const functions = await loadFunctions(new URL('../functions/', import.meta.url));
3930
+ ${hasRuntime ? `\n${runtimeLines(options)}\n` : ""}${visitorRoles.length > 0 ? `${visitorsNote(visitorRoles)}\n` : ""}
3931
+ /**
3932
+ * Everything the space is, served by this server alone: its pages, its plugins, its server actions (\`src/actions/\`),
3933
+ * its functions${hasRuntime ? " and runtime" : ""}, and its files (\`public/\`, where Plitzi's CDN served them from).
3934
+ */
3935
+ const server = createServer(
3936
+ {
3937
+ port: PORT,
3938
+ devMode: process.env.NODE_ENV !== 'production',
3939
+ adapters: ${local ? `createJsonAdapters({
3940
+ offlineData: { schema, style },
3941
+ deployment: { spaceId: 1, environment: 'main', revision: 0, pluginNames: ${allNames} }
3942
+ })` : `createCloudAdapters({
3943
+ webKey: HOST_KEY,
3944
+ ...(process.env.PLITZI_SERVER_URL ? { serverUrl: process.env.PLITZI_SERVER_URL } : {}),
3945
+ environment: (process.env.PLITZI_ENVIRONMENT ?? 'main') as 'main' | 'production',
3946
+ deployment: { pluginNames: ${allNames} }
3947
+ })`},
3948
+ plugins: ${allPlugins},
3949
+ publicDir: path.join(PROJECT_ROOT, 'public'),
3950
+ functions: { native: ${hasRuntime ? "[...functions, ...runtime.native]" : "functions"} },
3951
+ // What \`ctx.sign\` and \`ctx.verify\` sign with: the key that was the space's on Plitzi stays there.
3952
+ action: { lookups, signingSecret: process.env.PLITZI_SIGNING_SECRET },
3953
+ logger: consoleLogger
3954
+ }${hasRuntime ? ",\n { preAuth: [runtime.stage] }" : ""}
3955
+ );
3956
+
3957
+ server.listen(PORT, HOST);
3958
+ console.log(\`pages on \${publicUrl}/\`);
3959
+
3960
+ closeOnSignals(server${hasRuntime ? ", { afterClose: () => runtime.close() }" : ""});
3961
+ `;
3962
+ };
3963
+ /** A path of the source tree, where the project keeps it: under `src/`, unless it was already a project's `src/`. */
3964
+ var placeOf = (paths) => {
3965
+ const prefix = paths.length > 0 && paths.every((path) => path.startsWith("src/")) ? "" : "src/";
3966
+ return (path) => `${prefix}${path}`;
3967
+ };
3968
+ /** The import `src/main.ts` names the runtime's module by. */
3969
+ var fromMain = (path) => `./${path.replace(/^src\//, "")}`;
3970
+ /** What the space was given on Plitzi, by name, as `.env.example` lists it — and `.env`, with values where they are known. */
3971
+ var settingsLines = (exported) => [
3972
+ "# What the space was given on Plitzi, by name — the values never leave it. Copy this to .env and fill them in.",
3973
+ "# What the project signs with (ctx.sign): at least 32 characters, secret.",
3974
+ "PLITZI_SIGNING_SECRET=",
3975
+ ...exported.variables.map((name) => `${name}=`),
3976
+ ...exported.credentials.map(({ identifier, name, provider }) => `# ${name} (${provider}): the credential "${identifier}"`),
3977
+ "PORT=8080",
3978
+ ""
3979
+ ];
3980
+ /**
3981
+ * The project's own `.env`, ready to run: a signing key made for it — the space's own stays on Plitzi — and the rest
3982
+ * left for whoever has the values. Read from Plitzi, the key it is read with comes first, as a project of its own has
3983
+ * it. Written once, by `create`: it is the project's, and nothing pulled ever touches it.
3984
+ */
3985
+ var envFromSpace = (exported, answers, signingSecret) => {
3986
+ const settings = settingsLines(exported).join("\n").replace("PLITZI_SIGNING_SECRET=", `PLITZI_SIGNING_SECRET=${signingSecret}`);
3987
+ return answers.source === "cloud" ? `${envFile(answers)}\n${settings}\n` : `${settings}\nPORT=8080\n`;
3988
+ };
3989
+ /** What a project made from a space holds, from what the platform answered for it — the same for `create` and `pull`. */
3990
+ var projectFromSpace = (exported, spaceSource) => {
3991
+ const { source, builtOnly, assets, report } = exported;
3992
+ const place = placeOf(Object.keys(source.files));
3993
+ const local = (text) => assets.reduce((written, { url, path }) => written.split(url).join(`/${path}`), text);
3994
+ const files = {};
3995
+ const binaries = {};
3996
+ Object.entries(source.files).forEach(([path, content]) => {
3997
+ if (isTextFile(path)) files[place(path)] = local(decode(content));
3998
+ else binaries[place(path)] = content;
3999
+ });
4000
+ if (exported.authoring) {
4001
+ Object.entries(exported.authoring.files).forEach(([path, text]) => {
4002
+ files[`src/space/${path}`] = local(text);
4003
+ });
4004
+ files["src/space.ts"] = spaceEntry(exported.authoring.exportName);
4005
+ files["src/plugins/declarations.ts"] = projectDeclarations(Object.keys(files).map((path) => /^src\/plugins\/([^/]+)\/declaration\.ts$/.exec(path)?.[1]).filter((folder) => folder !== void 0).sort());
4006
+ }
4007
+ const coded = [];
4008
+ const asJson = [];
4009
+ exported.actions.forEach(({ identifier, name, document }) => {
4010
+ const reading = actionSpecFromEntry({
4011
+ id: identifier,
4012
+ document
4013
+ });
4014
+ if (reading.ok) {
4015
+ const { exportName, source: code } = actionToSource(reading.spec);
4016
+ files[`src/actions/${identifier}.ts`] = local(code);
4017
+ coded.push({
4018
+ identifier,
4019
+ exportName
4020
+ });
4021
+ return;
4022
+ }
4023
+ files[`src/actions/${identifier}.json`] = `${local(JSON.stringify({
4024
+ id: identifier,
4025
+ name,
4026
+ document
4027
+ }, null, 2))}\n`;
4028
+ asJson.push(`src/actions/${identifier}.json stays JSON: ${reading.reason}`);
4029
+ });
4030
+ exported.connectors.forEach(({ identifier, name, manifest }) => {
4031
+ files[`src/connectors/${identifier}.json`] = `${JSON.stringify({
4032
+ id: identifier,
4033
+ name,
4034
+ manifest
4035
+ }, null, 2)}\n`;
4036
+ });
4037
+ files["src/actions.ts"] = actionsModule(coded);
4038
+ const functions = Object.fromEntries(Object.entries(exported.functions.files).map(([path, text]) => [path, local(text)]));
4039
+ Object.entries(functions).forEach(([path, text]) => {
4040
+ files[`functions/${path}`] = text;
4041
+ });
4042
+ const runtimeEntry = source.runtime?.entries[0];
4043
+ if (builtOnly.runtime) binaries["vendor/runtime.bundle"] = builtOnly.runtime;
4044
+ files["src/main.ts"] = fromSpaceMain({
4045
+ source: spaceSource,
4046
+ ...runtimeEntry ? { runtimeEntry: fromMain(place(runtimeEntry)) } : {},
4047
+ packedRuntime: Boolean(builtOnly.runtime),
4048
+ builtPlugins: builtOnly.plugins.length > 0,
4049
+ visitorRoles: exported.visitorRoles
4050
+ });
4051
+ const extra = Object.fromEntries(Object.entries(source.dependencies).filter(([name]) => !name.startsWith("@plitzi/") && !["react", "react-dom"].includes(name)));
4052
+ const plitzi = Object.fromEntries([...Object.keys(source.dependencies), ...coded.length > 0 ? ["@plitzi/sdk-authoring"] : []].filter((name) => name.startsWith("@plitzi/")).map((name) => [name, SDK_VERSION]));
4053
+ const variables = [...exported.variables, ...exported.credentials.map(({ identifier }) => identifier)];
4054
+ files[".env.example"] = `${settingsLines(exported).join("\n")}\nPORT=8080\n`;
4055
+ files[".prettierignore"] = `${prettierignore(PROJECT_OUTPUTS)}public\nvendor\n`;
4056
+ const lines = [
4057
+ ...report.conflicts.map(({ path, kept, others }) => `${place(path)}: ${kept} and ${others.join(", ")} held different copies; ${kept}'s is kept`),
4058
+ ...report.rangeConflicts.map(({ name, kept, others }) => `${name}: asked for at ${[kept, ...others].join(" and ")}; ${kept} is written`),
4059
+ ...Object.entries(source.dependencies).filter(([name, range]) => name.startsWith("@plitzi/") && range !== SDK_VERSION).map(([name, range]) => `${name}: the source was written against ${range}, and the project runs ${SDK_VERSION}`),
4060
+ ...source.plugins.flatMap(({ type, entries }) => {
4061
+ const entry = place(entries[0] ?? "");
4062
+ const folder = /^src\/plugins\/([^/]+)\/index\.ts$/.exec(entry)?.[1];
4063
+ return folder && `${folder.charAt(0).toLowerCase()}${folder.slice(1)}` === type ? [] : [`${type}: its source starts at ${entry}, and the project registers src/plugins/<Name>/index.ts — move it there`];
4064
+ }),
4065
+ ...builtOnly.plugins.map(({ type }) => `${type}: no source of this plugin was kept, so it runs as it was built (vendor/plugins/${type}/) and cannot be changed — upload it again from its source with plitzi upload plugin`),
4066
+ ...builtOnly.runtime ? ["The runtime came across built only (vendor/runtime.bundle): it runs, and cannot be changed"] : [],
4067
+ ...asJson,
4068
+ ...exported.visitorRoles.length > 0 ? [`Its visitors (${exported.visitorRoles.join(", ")}) signed in with Plitzi: here nobody signs in until the server does it itself — see the note in src/main.ts`] : [],
4069
+ ...report.corrections,
4070
+ ...variables.length > 0 ? [`Give the project its variables and credentials in .env: ${variables.join(", ")}`] : []
4071
+ ];
4072
+ return {
4073
+ files,
4074
+ binaries,
4075
+ downloads: [...assets.map(({ url, path }) => ({
4076
+ url,
4077
+ to: `public/${path}`
4078
+ })), ...builtOnly.plugins.flatMap(({ type, files: built }) => built.map(({ url, path }) => ({
4079
+ url,
4080
+ to: `vendor/plugins/${type}/${path}`
4081
+ })))],
4082
+ dependencies: {
4083
+ ...extra,
4084
+ ...plitzi
4085
+ },
4086
+ functions: {
4087
+ version: exported.functions.version,
4088
+ files: functions
4089
+ },
4090
+ omit: EXAMPLE.filter((path) => !Object.hasOwn(files, path)),
4091
+ report: lines
4092
+ };
4093
+ };
4094
+ //#endregion
4095
+ //#region src/commands/spaceOrigin.ts
4096
+ /**
4097
+ * Where a project `plitzi create --from` made came from, and what the space gave it: `.plitzi/space.json`, committed
4098
+ * with the project so everyone working on it pulls from the same point.
4099
+ *
4100
+ * What it was given is kept as a digest of each file as it was written, never the file: that is enough for `plitzi
4101
+ * pull` to tell a file changed here from one the space changed — the two questions a pull asks of every file — and a
4102
+ * copy of the whole project beside the project would be the second source of it nobody keeps in step.
4103
+ */
4104
+ var ORIGIN_FILE = path.join(".plitzi", "space.json");
4105
+ var isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4106
+ var isStrings = (value) => isRecord$4(value) && Object.values(value).every((entry) => typeof entry === "string");
4107
+ var digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
4108
+ /**
4109
+ * 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
4110
+ * formatted since is not a file changed — and of its bytes as they are, which is what a project recorded before it had
4111
+ * a formatter. Nothing when there is no such file.
4112
+ */
4113
+ var digestsOf = async (root, file, format) => {
4114
+ let bytes;
4115
+ try {
4116
+ bytes = await fs.readFile(path.join(root, file));
4117
+ } catch {
4118
+ return;
4119
+ }
4120
+ const raw = digest(bytes);
4121
+ return {
4122
+ formatted: isTextFile(file) ? digest(await format(file, bytes.toString("utf-8"))) : raw,
4123
+ raw
4124
+ };
4125
+ };
4126
+ /** The project's origin, or `undefined` when it has none — it was not made from a space, or the file is not one. */
4127
+ var readOrigin = async (root) => {
4128
+ try {
4129
+ const value = JSON.parse(await fs.readFile(path.join(root, ORIGIN_FILE), "utf-8"));
4130
+ if (!isRecord$4(value) || value.format !== 1 || typeof value.api !== "string" || !isRecord$4(value.space) || typeof value.space.id !== "number" || typeof value.space.name !== "string" || typeof value.space.permanentUrl !== "string" || value.source !== "local" && value.source !== "cloud" || !isRecord$4(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
+ return {
4132
+ format: 1,
4133
+ api: value.api,
4134
+ space: {
4135
+ id: value.space.id,
4136
+ name: value.space.name,
4137
+ permanentUrl: value.space.permanentUrl
4138
+ },
4139
+ source: value.source,
4140
+ version: {
4141
+ environment: value.version.environment,
4142
+ ...typeof value.version.revision === "number" ? { revision: value.version.revision } : {}
4143
+ },
4144
+ files: value.files,
4145
+ downloads: value.downloads,
4146
+ dependencies: value.dependencies
4147
+ };
4148
+ } catch {
4149
+ return;
4150
+ }
4151
+ };
4152
+ /** Kept sorted, so a pull that changed one file changes one line of it. */
4153
+ var sorted = (record) => Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
4154
+ var writeOrigin = async (root, origin) => {
4155
+ await fs.mkdir(path.join(root, ".plitzi"), { recursive: true });
4156
+ await fs.writeFile(path.join(root, ORIGIN_FILE), `${JSON.stringify({
4157
+ ...origin,
4158
+ files: sorted(origin.files),
4159
+ downloads: sorted(origin.downloads),
4160
+ dependencies: sorted(origin.dependencies)
4161
+ }, null, 2)}\n`);
4162
+ };
4163
+ /** What the project was given, once it is on disk: each file's digest, formatted as the project formats it. */
4164
+ var digestsOnDisk = async (root, files, format) => {
4165
+ const digests = await Promise.all(files.map(async (file) => [file, (await digestsOf(root, file, format))?.formatted]));
4166
+ return Object.fromEntries(digests.filter((entry) => entry[1] !== void 0));
4167
+ };
4168
+ //#endregion
4169
+ //#region src/commands/createFrom.ts
4170
+ /**
4171
+ * `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
+ * to change it — and the files of it the project serves itself, fetched once.
4173
+ */
4174
+ var isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4175
+ /** What the platform answered is an export: of the format this CLI writes, with every part it reads. */
4176
+ var isExport = (value) => isRecord$3(value) && value.format === 1 && isRecord$3(value.space) && isRecord$3(value.version) && isRecord$3(value.source) && isRecord$3(value.builtOnly) && isRecord$3(value.report) && isRecord$3(value.functions) && Array.isArray(value.actions) && Array.isArray(value.assets) && Array.isArray(value.visitorRoles);
4177
+ /** How a version is said: `the draft`, `production (latest)`, `production r3`. */
4178
+ var versionLabel = ({ environment, revision }) => {
4179
+ if (environment === "main") return "the draft";
4180
+ return revision ? `${environment} r${String(revision)}` : `${environment} (latest)`;
4181
+ };
4182
+ /**
4183
+ * A version of the space named — its id or its permanent URL — put together as a project, or why not, said and
4184
+ * `undefined`. `name` is what it is called in what is printed: `from`, unless a pull names it by its id.
4185
+ */
4186
+ var fetchExport = async (api, from, { source, version, name = from }) => {
4187
+ const connection = await signedIn(api, `to take ${from} out of Plitzi`);
4188
+ if (!connection) return;
4189
+ console.log(`\nTaking ${chalk.bold(name)} (${versionLabel(version)}) out of ${api}…`);
4190
+ const query = new URLSearchParams({
4191
+ source,
4192
+ environment: version.environment,
4193
+ ...version.revision ? { revision: String(version.revision) } : {}
4194
+ });
4195
+ const answered = await authorizedRequest(connection, `/spaces/${encodeURIComponent(from)}/export?${query.toString()}`);
4196
+ if (!answered.ok) {
4197
+ console.error(chalk.red(answered.error));
4198
+ process.exitCode = 1;
4199
+ return;
4200
+ }
4201
+ const { status, data } = answered.value.reply;
4202
+ if (status === 200 && isExport(data)) return data;
4203
+ const said = isRecord$3(data) && typeof data.error === "string" ? data.error : void 0;
4204
+ const why = {
4205
+ 403: `You may not take ${from} out: it takes being able to change it — its owner, an administrator or a writer.`,
4206
+ 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;
4210
+ };
4211
+ /** A file of the space's CDN, fetched. */
4212
+ var download = async (url) => {
4213
+ const response = await fetch(url);
4214
+ if (!response.ok) throw new Error(String(response.status));
4215
+ return Buffer.from(await response.arrayBuffer());
4216
+ };
4217
+ /** Why a file could not be fetched, as the report says it. */
4218
+ var notFetched = (to, url, error) => `${to}: could not be fetched from ${url} (${error instanceof Error ? error.message : String(error)})`;
4219
+ /** The project's binary files written, and the space's files fetched from its CDN into the project to be served by it. */
4220
+ var writeFromSpace = async (target, project) => {
4221
+ for (const [file, base64] of Object.entries(project.binaries)) {
4222
+ await fs.mkdir(path.dirname(path.join(target, file)), { recursive: true });
4223
+ await fs.writeFile(path.join(target, file), Buffer.from(base64, "base64"));
4224
+ }
4225
+ const missing = [];
4226
+ for (const { url, to } of project.downloads) try {
4227
+ const bytes = await download(url);
4228
+ await fs.mkdir(path.dirname(path.join(target, to)), { recursive: true });
4229
+ await fs.writeFile(path.join(target, to), bytes);
4230
+ } catch (error) {
4231
+ missing.push(notFetched(to, url, error));
4232
+ }
4233
+ return missing;
4234
+ };
4235
+ /** The space's functions as they are on disk: what `plitzi functions push` compares the project's with. */
4236
+ var functionsOnDisk = async (root, project) => Object.fromEntries(await Promise.all(Object.keys(project.functions.files).map(async (file) => [file, await fs.readFile(path.join(root, "functions", file), "utf-8")])));
4237
+ /**
4238
+ * What the project was given, recorded once it is on disk as it will stay — installed and formatted — so `plitzi pull`
4239
+ * tells what changed here from what changed on the space; and its functions as a pulled working copy, so `plitzi
4240
+ * functions push` sends them back to the space they came from.
4241
+ */
4242
+ var recordOrigin = async (root, { api, exported, source, pinned, project }) => {
4243
+ const files = await digestsOnDisk(root, [
4244
+ ...Object.keys(project.files),
4245
+ ...Object.keys(project.binaries),
4246
+ ...project.downloads.map(({ to }) => to)
4247
+ ], await projectFormatter(root));
4248
+ await writeOrigin(root, {
4249
+ format: 1,
4250
+ api,
4251
+ space: exported.space,
4252
+ source,
4253
+ version: {
4254
+ environment: exported.version.environment,
4255
+ ...pinned ? { revision: exported.version.revision } : {}
4256
+ },
4257
+ files,
4258
+ downloads: Object.fromEntries(project.downloads.filter(({ to }) => Object.hasOwn(files, to)).map(({ url, to }) => [to, url])),
4259
+ dependencies: project.dependencies
4260
+ });
4261
+ await writeFunctionsState(root, {
4262
+ space: exported.space.id,
4263
+ version: project.functions.version,
4264
+ files: await functionsOnDisk(root, project)
4265
+ });
4266
+ };
4267
+ //#endregion
4268
+ //#region src/commands/create.ts
4269
+ var MODES = ["server", "client"];
4270
+ var SOURCES = ["local", "cloud"];
4271
+ /**
4272
+ * The choices a project is BUILT around, which belong to whoever it is for — never to a default nobody chose.
4273
+ *
4274
+ * Each one is written into the project: the package manager into every command its README, lockfile, Playwright config
4275
+ * and install use; the mode into whether there is a Node tier at all; the source into whether the space is in the repo.
4276
+ * Assumed, they are the wrong project for somebody, and an agent running this was the one assuming. So what was not
4277
+ * passed is asked for when a person is at the terminal, and refused when nobody is — with the exact questions to put
4278
+ * to the person.
4279
+ */
4280
+ var resolveDecisions = async (options) => {
4281
+ const detected = detectPackageManager();
4282
+ const packageManager = {
4283
+ flag: "--package-manager",
4284
+ choices: PACKAGE_MANAGERS,
4285
+ question: `Which package manager will you work in? (this was run through ${detected})`,
4286
+ given: PACKAGE_MANAGERS.find((manager) => manager === options.packageManager),
4287
+ fallback: detected
4288
+ };
4289
+ const mode = {
4290
+ flag: "--mode",
4291
+ choices: MODES,
4292
+ question: "Should pages render on a Node server of your own (server: SSR + RSC), or only in the browser (client)?",
4293
+ given: MODES.find((candidate) => candidate === options.mode),
4294
+ fallback: "server"
4295
+ };
4296
+ const source = {
4297
+ flag: "--source",
4298
+ choices: SOURCES,
4299
+ question: "Should the space live in the project as code you edit (local), or be read live from your Plitzi account (cloud, which needs a key from Credentials in the builder)?",
4300
+ given: SOURCES.find((candidate) => candidate === options.source),
4301
+ fallback: "local"
4302
+ };
4303
+ const missing = [
4304
+ packageManager,
4305
+ mode,
4306
+ source
4307
+ ].filter((question) => question.given === void 0);
4308
+ if (missing.length === 0 || options.yes && atTerminal()) return {
4309
+ packageManager: packageManager.given ?? packageManager.fallback,
4310
+ mode: mode.given ?? mode.fallback,
4311
+ source: source.given ?? source.fallback
4312
+ };
4313
+ if (!atTerminal()) {
4314
+ refuseWithoutTerminal("project", missing);
4315
+ return;
4316
+ }
4317
+ const rl = readline.createInterface({
4318
+ input: process.stdin,
4319
+ output: process.stdout
4320
+ });
4321
+ try {
4322
+ const manager = packageManager.given ?? await ask(rl, packageManager);
4323
+ const tier = manager && (mode.given ?? await ask(rl, mode));
4324
+ const origin = tier && (source.given ?? await ask(rl, source));
4325
+ if (!manager || !tier || !origin) {
4326
+ process.exitCode = 1;
4327
+ return;
4328
+ }
4329
+ return {
4330
+ packageManager: manager,
4331
+ mode: tier,
4332
+ source: origin
4333
+ };
4334
+ } finally {
4335
+ rl.close();
4336
+ }
4337
+ };
4338
+ var keyQuestion = (mode) => `The ${mode === "server" ? "self-hosting" : "public render"} key the project reads the space with (Credentials, in the builder).`;
4339
+ var askForKey = async (mode) => {
4340
+ const rl = readline.createInterface({
4341
+ input: process.stdin,
4342
+ output: process.stdout
4343
+ });
4344
+ try {
4345
+ return (await rl.question(`\n${keyQuestion(mode)}\n> `)).trim();
4346
+ } finally {
4347
+ rl.close();
4348
+ }
4349
+ };
4350
+ var create = async (directory, options) => {
4351
+ const target = path.resolve(directory ?? ".");
4352
+ const name = path.basename(target);
4353
+ const wasEmpty = await isEmpty(target);
4354
+ if (!options.force && !wasEmpty) {
4355
+ console.error(chalk.red(`${target} is not empty. Pass --force to write into it anyway.`));
4356
+ process.exitCode = 1;
4357
+ return;
4358
+ }
4359
+ if (options.from && options.mode === "client") {
4360
+ console.error(chalk.red("A project made from a space runs its own server: leave out --mode, or pass --mode server."));
4361
+ process.exitCode = 1;
4362
+ return;
4363
+ }
4364
+ const environment = options.environment ?? "main";
4365
+ const revision = options.revision === void 0 ? void 0 : Number(options.revision);
4366
+ if (revision !== void 0 && (!Number.isInteger(revision) || revision < 1 || environment === "main")) {
4367
+ console.error(chalk.red(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 ?? ""}".`));
4368
+ process.exitCode = 1;
4369
+ return;
4370
+ }
4371
+ const decisions = await resolveDecisions(options.from ? {
4372
+ ...options,
4373
+ mode: "server"
4374
+ } : options);
4375
+ if (!decisions) return;
4376
+ const { packageManager, mode, source } = decisions;
4377
+ if (revision !== void 0 && !options.from && source === "local") {
4378
+ console.error(chalk.red("--revision pins a published version: it goes with --from, or with --source cloud."));
4379
+ process.exitCode = 1;
4380
+ return;
4381
+ }
4382
+ if (source === "cloud" && !options.key && !atTerminal()) {
4383
+ refuseWithoutTerminal("project", [{
4384
+ flag: "--key",
4385
+ choices: ["<key>"],
4386
+ question: keyQuestion(mode)
4387
+ }]);
4388
+ return;
4389
+ }
4390
+ const key = source === "cloud" ? options.key ?? await askForKey(mode) : "";
4391
+ if (source === "cloud" && !key) {
4392
+ console.error(chalk.red("\nNo key. Mint one under Credentials in the builder, then run this again."));
4393
+ process.exitCode = 1;
4394
+ return;
4395
+ }
4396
+ const answers = {
4397
+ name,
4398
+ mode,
4399
+ source,
4400
+ key,
4401
+ environment,
4402
+ ...revision ? { revision } : {},
4403
+ packageManager,
4404
+ managerVersion: detectManagerVersion(packageManager, await nearestExisting(target))
4405
+ };
4406
+ const api = options.from ? await apiFor(options) : void 0;
4407
+ const exported = options.from && api ? await fetchExport(api, options.from, {
4408
+ source,
4409
+ version: {
4410
+ environment,
4411
+ ...revision ? { revision } : {}
4412
+ }
4413
+ }) : void 0;
4414
+ if (options.from && !exported) return;
4415
+ const fromSpace = exported ? projectFromSpace(exported, source) : void 0;
4416
+ const files = Object.fromEntries(Object.entries(scaffold(answers)).filter(([file]) => !fromSpace?.omit.includes(file)));
4417
+ await writeFiles(target, exported && fromSpace ? {
4418
+ ...files,
4419
+ ...fromSpace.files,
4420
+ "package.json": packageJson$1(answers, fromSpace.dependencies),
4421
+ ".env": envFromSpace(exported, answers, randomBytes(32).toString("hex"))
4422
+ } : files);
4423
+ const missing = fromSpace ? await writeFromSpace(target, fromSpace) : [];
4424
+ const wantsInstall = options.install !== false;
4425
+ const installed = wantsInstall && await install(packageManager, target);
4426
+ /**
4427
+ * A failed install is a failed command, not a next step.
4428
+ *
4429
+ * The files are already written, so the summary below still prints — but printed on its own after a screen of
4430
+ * resolver errors it reads as success, and a script that ran `create` carries on into a project with no
4431
+ * `node_modules`.
4432
+ */
4433
+ if (wantsInstall && !installed) {
4434
+ console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The project is written; the reason is in the output above.`));
4435
+ console.error(chalk.dim(INSTALL_HINTS[packageManager]));
4436
+ process.exitCode = 1;
4437
+ }
4438
+ /**
4439
+ * Formatted by the project's own Prettier, once, so the first commit is already in its style — the scaffold writes
4440
+ * tables and lists whose layout depends on what they hold. Only into a folder that was empty: `--force` into one
4441
+ * with work in it must never reformat that work.
4442
+ */
4443
+ if (installed && wasEmpty) await runScript(packageManager, "format", target);
4444
+ if (api && exported && fromSpace) await recordOrigin(target, {
4445
+ api,
4446
+ exported,
4447
+ source,
4448
+ pinned: revision !== void 0,
4449
+ project: fromSpace
4450
+ });
4451
+ const where = cdPrefix(target);
4452
+ console.log(chalk.green(`\n${name} — ${mode === "server" ? "server-rendered" : "browser-rendered"}, space ${source === "cloud" ? "from Plitzi" : "in the project"}.`));
4453
+ console.log("");
4454
+ if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
4455
+ console.log(` ${where}${runCommand(packageManager, "start")}`);
4456
+ console.log("");
4457
+ console.log(chalk.dim(`.claude/skills carries Plitzi's authoring skill; \`${runCommand(packageManager, "visual")}\` opens the page and checks it.`));
4458
+ if (source === "cloud") console.log(chalk.dim(".env holds the key and is already git-ignored."));
4459
+ if (exported) {
4460
+ const { version } = exported;
4461
+ const said = version.snapshot ? ` — “${version.snapshot.description}”, made ${version.snapshot.publishedAt}` : "";
4462
+ console.log(chalk.dim(`Taken from ${versionLabel(version)}${said}.`));
4463
+ }
4464
+ const notes = [...fromSpace?.report ?? [], ...missing];
4465
+ if (exported && notes.length > 0) {
4466
+ console.log(chalk.yellow(`\nWhat to know about ${exported.space.name} here:`));
4467
+ notes.forEach((note) => console.log(chalk.yellow(` - ${note}`)));
4468
+ }
4469
+ console.log("");
4470
+ };
4471
+ //#endregion
4472
+ //#region src/commands/createPlugin.ts
4473
+ var DEFAULT_NAME = "plitzi-plugin-my-element";
4474
+ /** What an agent is told to ask for, and the flags that carry the answers. */
4475
+ var QUESTIONS = {
4476
+ name: {
4477
+ flag: "--name",
4478
+ choices: ["<package name>"],
4479
+ question: "What is the plugin called? Its package name, e.g. plitzi-plugin-seat-picker."
4480
+ },
4481
+ directory: {
4482
+ flag: "<directory> (the first argument)",
4483
+ choices: ["<folder>"],
4484
+ question: "Which folder should the plugin be written to?"
4485
+ }
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;
4496
+ }
4497
+ const types = [pluginNames(main).type, ...others.map((name) => pluginNames(name).type)];
4498
+ const repeated = types.find((type, index) => types.indexOf(type) !== index);
4499
+ return repeated ? `Two elements would both be "${repeated}": give each a name of its own.` : void 0;
4500
+ };
4501
+ /** The folders a repository keeps its packages in, as places for this one — or `./<name>` outside of any. */
4502
+ var folderOptions = (project, base) => {
4503
+ if (!project) return [];
4504
+ const root = project.workspaceRoot ?? project.root;
4505
+ return [.../* @__PURE__ */ new Set([...project.workspaceFolders, "plugins"])].map((folder) => {
4506
+ const value = path.join(root, folder, base);
4507
+ return {
4508
+ label: path.relative(process.cwd(), value) || ".",
4509
+ value
4510
+ };
4511
+ });
4512
+ };
4513
+ var askDirectory = async (rl, project, base) => {
4514
+ const typed = {
4515
+ question: "Which folder? (relative to here)",
3563
4516
  fallback: `./${base}`
3564
4517
  };
3565
4518
  if (!project) return path.resolve(await askText(rl, "Which folder should the plugin be written to?", typed.fallback));
@@ -3658,316 +4611,251 @@ var createPlugin = async (directory, options) => {
3658
4611
  inProject: project !== void 0
3659
4612
  }));
3660
4613
  /**
3661
- * Installed where it will be installed from. Inside a workspace that covers the folder, installing there installs
3662
- * the workspace; inside a repository that does not list it, Yarn refuses to install a package it does not know —
3663
- * so that is said, with the fix, rather than failed at.
3664
- */
3665
- const unlisted = project?.workspaceRoot !== void 0 && !coveredByWorkspace(project, target);
3666
- const wantsInstall = options.install !== false && !unlisted;
3667
- const installed = wantsInstall && await install(packageManager, target);
3668
- if (wantsInstall && !installed) {
3669
- console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The plugin is written; the reason is in the output above.`));
3670
- console.error(chalk.dim(INSTALL_HINTS[packageManager]));
3671
- process.exitCode = 1;
3672
- }
3673
- if (installed && wasEmpty) await runScript(packageManager, "format", target);
3674
- const where = cdPrefix(target);
3675
- console.log(chalk.green(`\n${name} — the ${names.type} element, as a package any space can load.`));
3676
- 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.`));
3677
- console.log("");
3678
- if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
3679
- console.log(` ${where}${runCommand(packageManager, "start")} # the plugin inside a space`);
3680
- console.log(` ${where}${runCommand(packageManager, "zip")} # what the builder takes, under Resources`);
3681
- console.log("");
3682
- } finally {
3683
- rl?.close();
3684
- }
3685
- };
3686
- //#endregion
3687
- //#region ../../packages/sdk-shared/dist/actions/functions.mjs
3688
- var n = (e) => /^[A-Za-z0-9_][A-Za-z0-9_./-]*\.(ts|js|mjs|json)$/.test(e) && !e.split("/").some((e) => e === ".." || e === "." || e === "");
3689
- var r = async (e, t) => {
3690
- let r = {}, i = async (a) => {
3691
- let o;
3692
- try {
3693
- o = await t.list(a ? `${e}/${a}` : e);
3694
- } catch {
3695
- return;
3696
- }
3697
- for (let s of o) {
3698
- let o = a ? `${a}/${s.name}` : s.name;
3699
- s.directory ? s.name !== "node_modules" && !s.name.startsWith(".") && await i(o) : n(o) && (r[o] = await t.read(`${e}/${o}`));
3700
- }
3701
- };
3702
- return await i(""), r;
3703
- };
3704
- //#endregion
3705
- //#region src/commands/functions.ts
3706
- var FUNCTIONS_DIR = "functions";
3707
- var STATE_FILE = path.join(".plitzi", "functions.json");
3708
- var isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3709
- /** The project's root: where `functions/` and `.plitzi/` go. */
3710
- var rootOf = async () => (await findProject(process.cwd()))?.root ?? process.cwd();
3711
- /** Every source file under `functions/`, by its path there — read by the one rule of what a source is. */
3712
- var readLocal = (root) => r(path.join(root, FUNCTIONS_DIR), {
3713
- list: async (dir) => (await fs.readdir(dir, { withFileTypes: true })).map((entry) => ({
3714
- name: entry.name,
3715
- directory: entry.isDirectory()
3716
- })),
3717
- read: (file) => fs.readFile(file, "utf8")
3718
- });
3719
- var readState = async (root) => {
3720
- try {
3721
- const value = JSON.parse(await fs.readFile(path.join(root, STATE_FILE), "utf8"));
3722
- if (!isRecord$2(value) || typeof value.space !== "number" || typeof value.version !== "string" || !isRecord$2(value.files)) return;
3723
- const files = Object.fromEntries(Object.entries(value.files).flatMap(([name, text]) => typeof text === "string" ? [[name, text]] : []));
3724
- return {
3725
- space: value.space,
3726
- version: value.version,
3727
- files
3728
- };
3729
- } catch {
3730
- return;
3731
- }
3732
- };
3733
- var writeState = async (root, state) => {
3734
- await fs.mkdir(path.join(root, ".plitzi"), { recursive: true });
3735
- await fs.writeFile(path.join(root, STATE_FILE), `${JSON.stringify(state, null, 2)}\n`);
3736
- };
3737
- /** The files that differ between two copies — added, removed or changed — sorted. */
3738
- var changedFiles = (a, b) => [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((file) => a[file] !== b[file]).sort();
3739
- var taskList = (draft) => draft.manifest?.tasks.length ? draft.manifest.tasks.map((task) => `${task.namespace}.${task.action}`).join(", ") : "no tasks";
3740
- var connect$1 = async (options, doing) => {
3741
- const api = await apiFor(options);
3742
- return api ? connectionWithSpace(api, doing) : void 0;
3743
- };
3744
- var readDraft = async (connection, spaceId) => {
3745
- const answered = await authorizedRequest(connection, `/spaces/${spaceId}/functions`);
3746
- if (!answered.ok) {
3747
- fail(answered.error);
3748
- return;
3749
- }
3750
- const { reply } = answered.value;
3751
- if (reply.status !== 200) {
3752
- fail(reply.data.error ?? `Could not read the space’s functions (${reply.status}).`);
3753
- return;
3754
- }
3755
- return reply.data;
3756
- };
3757
- var pullFunctions = async (options) => {
3758
- const connection = await connect$1(options, "to pull from");
3759
- if (!connection?.space) return;
3760
- const root = await rootOf();
3761
- const [local, state] = await Promise.all([readLocal(root), readState(root)]);
3762
- const unpushed = state ? changedFiles(local, state.files) : Object.keys(local);
3763
- if (!options.force && (state?.space ?? connection.space.id) !== connection.space.id) {
3764
- fail("functions/ is a copy of another space’s. Pull into another project, or pass --force to replace it.");
3765
- return;
3766
- }
3767
- if (!options.force && unpushed.length) {
3768
- fail(`Pulling would overwrite what is not pushed yet: ${unpushed.join(", ")}.\nPush it first, or pass --force to throw it away.`);
3769
- return;
3770
- }
3771
- const draft = await readDraft(connection, connection.space.id);
3772
- if (!draft) return;
3773
- const base = path.join(root, FUNCTIONS_DIR);
3774
- await Promise.all(Object.keys(local).filter((file) => !Object.hasOwn(draft.files, file)).map((file) => fs.rm(path.join(base, file))));
3775
- for (const [file, text] of Object.entries(draft.files)) {
3776
- await fs.mkdir(path.dirname(path.join(base, file)), { recursive: true });
3777
- await fs.writeFile(path.join(base, file), text);
3778
- }
3779
- await writeState(root, {
3780
- space: connection.space.id,
3781
- version: draft.version,
3782
- files: draft.files
3783
- });
3784
- const count = Object.keys(draft.files).length;
3785
- console.log(count ? `Pulled ${count} file${count === 1 ? "" : "s"} of ${chalk.bold(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.`);
3786
- };
3787
- var printProblems = (problems) => {
3788
- console.error(chalk.red("The functions were not saved:"));
3789
- problems.forEach(({ file, line, column, message }) => {
3790
- const where = file ? `functions/${file}${line ? `:${String(line)}${column ? `:${String(column)}` : ""}` : ""}` : "functions";
3791
- console.error(` ${chalk.bold(where)} ${message}`);
3792
- });
3793
- process.exitCode = 1;
3794
- };
3795
- var pushFunctions = async (options) => {
3796
- const connection = await connect$1(options, "to push to");
3797
- if (!connection?.space) return;
3798
- const root = await rootOf();
3799
- const [local, state] = await Promise.all([readLocal(root), readState(root)]);
3800
- if (!Object.hasOwn(local, "index.ts")) {
3801
- fail("There is no functions/index.ts here: it is where a space’s functions start. Pull them, or write it.");
3802
- return;
3803
- }
3804
- let base = state?.space === connection.space.id ? state.version : void 0;
3805
- if (base === void 0) {
3806
- const draft = await readDraft(connection, connection.space.id);
3807
- if (!draft) return;
3808
- if (Object.keys(draft.files).length) {
3809
- fail(`${connection.space.name} already has functions. Pull them first: plitzi functions pull.`);
3810
- return;
3811
- }
3812
- base = draft.version;
3813
- }
3814
- if (state?.space === connection.space.id && !changedFiles(local, state.files).length) {
3815
- console.log("Nothing to push: functions/ is what was pulled.");
3816
- return;
3817
- }
3818
- const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions`, {
3819
- method: "PUT",
3820
- headers: { "Content-Type": "application/json" },
3821
- body: JSON.stringify({
3822
- files: local,
3823
- base
3824
- })
3825
- });
3826
- if (!answered.ok) {
3827
- fail(answered.error);
3828
- return;
3829
- }
3830
- const { reply } = answered.value;
3831
- if (reply.status === 422 && reply.data.problems) {
3832
- printProblems(reply.data.problems);
3833
- return;
3834
- }
3835
- if (reply.status === 409) {
3836
- fail(`${connection.space.name}’s functions changed since your pull — in the builder, or from another copy. Keep your changes aside, pull, and apply them again.`);
3837
- return;
3838
- }
3839
- if (reply.status !== 200 || !reply.data.version) {
3840
- fail(reply.data.refusal?.error ?? reply.data.error ?? `The functions were not saved (${reply.status}).`);
3841
- return;
4614
+ * Installed where it will be installed from. Inside a workspace that covers the folder, installing there installs
4615
+ * the workspace; inside a repository that does not list it, Yarn refuses to install a package it does not know —
4616
+ * so that is said, with the fix, rather than failed at.
4617
+ */
4618
+ const unlisted = project?.workspaceRoot !== void 0 && !coveredByWorkspace(project, target);
4619
+ const wantsInstall = options.install !== false && !unlisted;
4620
+ const installed = wantsInstall && await install(packageManager, target);
4621
+ if (wantsInstall && !installed) {
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("");
4635
+ } finally {
4636
+ rl?.close();
3842
4637
  }
3843
- await writeState(root, {
3844
- space: connection.space.id,
3845
- version: reply.data.version,
3846
- files: local
3847
- });
3848
- console.log(`Pushed to ${chalk.bold(connection.space.name)}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}. ` + chalk.dim("The live site runs them once the space is published."));
3849
4638
  };
3850
- /** What one run of a task did: its logs, then its value — or why it failed, as a failure of the command. */
3851
- var printRun = (task, report) => {
3852
- const step = report.steps?.find((entry) => entry.action === task);
3853
- const took = step ? `${String(step.endTime - step.startTime)} ms` : "";
3854
- step?.logs?.forEach((line) => console.log(chalk.dim(`log ${line}`)));
3855
- if (step?.error) {
3856
- fail(`${task} failed after ${took}: ${step.error}`);
3857
- return;
4639
+ //#endregion
4640
+ //#region ../../packages/sdk-shared/dist/source/snapshot.mjs
4641
+ var t = {
4642
+ files: 2e3,
4643
+ fileBytes: 4194304,
4644
+ totalBytes: 25165824
4645
+ };
4646
+ var n = ["plugin", "runtime"];
4647
+ var r = [
4648
+ /(^|\/)\.env(\.|$)/,
4649
+ /\.(pem|key|p12|pfx)$/i,
4650
+ /(^|\/)id_(rsa|dsa|ecdsa|ed25519)$/
4651
+ ];
4652
+ var i = [
4653
+ {
4654
+ what: "a private key",
4655
+ pattern: /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/
4656
+ },
4657
+ {
4658
+ what: "an AWS access key",
4659
+ pattern: /\bAKIA[0-9A-Z]{16}\b/
4660
+ },
4661
+ {
4662
+ what: "a Stripe live key",
4663
+ pattern: /\b[sr]k_live_[0-9a-zA-Z]{20,}\b/
4664
+ },
4665
+ {
4666
+ what: "a GitHub token",
4667
+ pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/
4668
+ },
4669
+ {
4670
+ what: "a Slack token",
4671
+ pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/
3858
4672
  }
3859
- console.log(`${chalk.green(report.status ?? "completed")} ${chalk.dim(took ? `in ${took}` : "")}`);
3860
- console.log(JSON.stringify(report.output?.value ?? null, null, 2));
4673
+ ];
4674
+ var a = (e) => {
4675
+ let t = e.split("/");
4676
+ 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
+ if (t.some((e) => e === "" || e === "." || e === "..")) return `"${e}" climbs out of the project, or has an empty part`;
4678
+ 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 (r.some((t) => t.test(e))) return `"${e}" is a file of credentials: they stay where the project runs, never in what Plitzi keeps`;
4680
+ };
4681
+ var o = (e) => i.find(({ pattern: t }) => t.test(e))?.what;
4682
+ var s = (e) => typeof e == "string" && n.some((t) => t === e);
4683
+ var c = (e) => typeof e == "string" && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(e);
4684
+ var l = (e) => Array.isArray(e) && e.length > 0 && e.every((e) => typeof e == "string");
4685
+ var u = (e) => typeof e == "object" && !!e && !Array.isArray(e);
4686
+ var d = (e) => u(e) && Object.values(e).every((e) => typeof e == "string");
4687
+ var f = (e) => Math.floor(e.length * 3 / 4) - (e.endsWith("==") ? 2 : +!!e.endsWith("="));
4688
+ var p = (e, r) => {
4689
+ if (!u(e) || e.format !== 1) return {
4690
+ ok: !1,
4691
+ problems: ["Not a source snapshot of format 1"]
4692
+ };
4693
+ let { kind: i, name: p, entries: m, files: h, dependencies: g } = e, _ = [];
4694
+ if (s(i) || _.push(`Its kind is one of ${n.join(", ")}`), c(p) || _.push("Its name is a plugin type or \"runtime\": letters, digits, dashes and underscores"), !d(h) || !d(g)) return {
4695
+ ok: !1,
4696
+ problems: [..._, "Its files and dependencies are maps of text"]
4697
+ };
4698
+ l(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
+ let v = Object.keys(h);
4700
+ v.length > t.files && _.push(`It holds ${String(v.length)} files; a snapshot holds at most ${String(t.files)}`);
4701
+ let y = 0;
4702
+ return v.forEach((e) => {
4703
+ let n = a(e);
4704
+ if (n) {
4705
+ _.push(n);
4706
+ return;
4707
+ }
4708
+ let i = f(h[e]);
4709
+ if (y += i, i > t.fileBytes) {
4710
+ _.push(`"${e}" is ${(i / 1024 / 1024).toFixed(1)} MB; a file of source is at most 4 MB`);
4711
+ return;
4712
+ }
4713
+ let s = o(r(h[e]));
4714
+ s && _.push(`"${e}" holds ${s}: take it out of the code and give it to the project as a variable`);
4715
+ }), y > t.totalBytes && _.push(`It holds ${(y / 1024 / 1024).toFixed(1)} MB of files; a snapshot holds at most 24 MB`), _.length > 0 || !s(i) || !c(p) || !l(m) ? {
4716
+ ok: !1,
4717
+ problems: _
4718
+ } : {
4719
+ ok: !0,
4720
+ snapshot: {
4721
+ format: 1,
4722
+ kind: i,
4723
+ name: p,
4724
+ entries: m,
4725
+ files: h,
4726
+ dependencies: g
4727
+ }
4728
+ };
3861
4729
  };
3862
- /** `--params`, read: an object of the task's params by name, or nothing when it is not one. */
3863
- var paramsOf = (given) => {
4730
+ //#endregion
4731
+ //#region src/pack/source.ts
4732
+ var CODE = /\.(m|c)?(t|j)sx?$/;
4733
+ var STYLES = /\.(css|scss|sass|less)$/;
4734
+ /** Where an import of a file without its extension can be, in the order a bundler looks. */
4735
+ var EXTENSIONS = [
4736
+ ".ts",
4737
+ ".tsx",
4738
+ ".mts",
4739
+ ".cts",
4740
+ ".js",
4741
+ ".jsx",
4742
+ ".mjs",
4743
+ ".cjs",
4744
+ ".json",
4745
+ ".css",
4746
+ ".scss"
4747
+ ];
4748
+ var BUILTINS = new Set(builtinModules);
4749
+ var loadTypeScript$1 = (root) => {
3864
4750
  try {
3865
- const params = given ? JSON.parse(given) : {};
3866
- if (isRecord$2(params)) return params;
3867
- } catch {}
3868
- fail("--params is a JSON object of the task’s params, by name: --params '{\"name\":\"Ada\"}'");
3869
- };
3870
- var tryFunction = async (task, options) => {
3871
- const params = paramsOf(options.params);
3872
- if (!params) return;
3873
- const connection = await connect$1(options, "to try it in");
3874
- if (!connection?.space) return;
3875
- const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions/try`, {
3876
- method: "POST",
3877
- headers: { "Content-Type": "application/json" },
3878
- body: JSON.stringify({
3879
- task,
3880
- params
3881
- })
3882
- });
3883
- if (!answered.ok) {
3884
- fail(answered.error);
3885
- return;
3886
- }
3887
- const { reply } = answered.value;
3888
- if (reply.status !== 200) {
3889
- fail(reply.data.error ?? `The task could not be tried (${reply.status}).`);
4751
+ return createRequire(path.join(root, "package.json"))("typescript");
4752
+ } catch {
3890
4753
  return;
3891
4754
  }
3892
- printRun(task, reply.data);
3893
- };
3894
- var isLocalRunner = (value) => isRecord$2(value) && typeof value.createLocalFunctions === "function";
3895
- /**
3896
- * This project's own runner: `dev` runs the functions with the `@plitzi/sdk-server` the project installed, so the build,
3897
- * the checks, the isolates and the limits are the platform's own, at the version the project is on.
3898
- */
3899
- var projectRunner = async (root) => {
3900
- try {
3901
- const entry = createRequire(path.join(root, "package.json")).resolve("@plitzi/sdk-server/functions-runner");
3902
- const loaded = await import(pathToFileURL(entry).href);
3903
- if (isLocalRunner(loaded)) return loaded;
3904
- } catch {}
3905
- fail("plitzi functions dev runs them with this project’s own @plitzi/sdk-server, and the isolates it needs:\n npm install --save-dev @plitzi/sdk-server isolated-vm core-js");
3906
4755
  };
3907
- /** `PLITZI_FUNCTIONS_CREDENTIALS`, a JSON object of credential id → its keys: what `ctx.fetch` may name locally. */
3908
- var localCredentials = () => {
3909
- const raw = process.env.PLITZI_FUNCTIONS_CREDENTIALS;
4756
+ var isFile = async (file) => {
3910
4757
  try {
3911
- const value = raw ? JSON.parse(raw) : {};
3912
- return isRecord$2(value) ? Object.fromEntries(Object.entries(value).flatMap(([id, keys]) => isRecord$2(keys) ? [[id, Object.fromEntries(Object.entries(keys).filter((pair) => typeof pair[1] === "string"))]] : [])) : {};
4758
+ return (await fs.stat(file)).isFile();
3913
4759
  } catch {
3914
- return {};
4760
+ return false;
3915
4761
  }
3916
4762
  };
3917
- var NO_NODE_SNAPSHOT = "--no-node-snapshot";
3918
4763
  /**
3919
- * The isolates `dev` runs need Node's startup snapshot off, and a process cannot turn it off once started: `dev` runs
3920
- * itself again with it, and answers with that run's exit code.
4764
+ * The file a relative import names, as a bundler finds it: as written, with an extension added, the TypeScript file a
4765
+ * `.js` import stands for, or a folder's index.
3921
4766
  */
3922
- var rerunWithoutNodeSnapshot = () => new Promise((resolve, reject) => {
3923
- const child = spawn(process.execPath, [
3924
- ...process.execArgv,
3925
- NO_NODE_SNAPSHOT,
3926
- ...process.argv.slice(1)
3927
- ], { stdio: "inherit" });
3928
- child.once("error", reject);
3929
- child.once("exit", (code) => {
3930
- resolve(code ?? 1);
3931
- });
3932
- });
4767
+ var resolveFile = async (from, specifier) => {
4768
+ const target = path.resolve(path.dirname(from), specifier);
4769
+ const candidates = [
4770
+ target,
4771
+ .../\.(m|c)?jsx?$/.test(target) ? [target.replace(/\.(m|c)?js(x?)$/, ".$1ts$2")] : [],
4772
+ ...EXTENSIONS.map((extension) => `${target}${extension}`),
4773
+ ...EXTENSIONS.map((extension) => path.join(target, `index${extension}`))
4774
+ ];
4775
+ for (const candidate of candidates) if (await isFile(candidate)) return candidate;
4776
+ };
4777
+ /** A package's name from what an import asks for: `@scope/name/sub` is `@scope/name`, `name/sub` is `name`. */
4778
+ var packageOf = (specifier) => specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
4779
+ /** What a stylesheet brings in: its `@import`s and the files its `url()`s point at — not a data URI or an address. */
4780
+ var styleImports = (text) => [...text.matchAll(/@import\s+(?:url\()?\s*['"]([^'"]+)['"]/g), ...text.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/g)].map((match) => match[1].trim()).filter((specifier) => !/^(data:|https?:|\/\/|#)/.test(specifier));
4781
+ var codeImports = (ts, text) => ts.preProcessFile(text, true, true).importedFiles.map((file) => file.fileName);
4782
+ /** A file's path in the snapshot: relative to the root, with forward slashes whatever the system writes. */
4783
+ var projectPath = (root, file) => path.relative(root, file).split(path.sep).join("/");
4784
+ /** The package of type declarations TypeScript reads for `name`: `geojson` is `@types/geojson`, `@a/b` is `@types/a__b`. */
4785
+ var typesPackageOf = (name) => `@types/${name.startsWith("@") ? name.slice(1).replace("/", "__") : name}`;
3933
4786
  /**
3934
- * `plitzi functions dev <task>`: the task run from `functions/` on this machine, as the platform would run it — and,
3935
- * with `--watch`, again every time a file is saved. Nothing reaches the space: its draft is what `push` sends.
4787
+ * The package an import needs, and the range the project asks for it: itself, or — for an import only its types are
4788
+ * read from (`import type { Feature } from 'geojson'`) — the package of its declarations.
3936
4789
  */
3937
- var devFunction = async (task, options) => {
3938
- if (!process.execArgv.includes(NO_NODE_SNAPSHOT)) {
3939
- process.exitCode = await rerunWithoutNodeSnapshot();
3940
- return;
3941
- }
3942
- const params = paramsOf(options.params);
3943
- const root = await rootOf();
3944
- const runner = params ? await projectRunner(root) : void 0;
3945
- if (!params || !runner) return;
3946
- const local = runner.createLocalFunctions({ credentials: localCredentials() });
3947
- const run = async () => {
3948
- const loaded = await local.load(await readLocal(root));
3949
- if (!loaded.ok) {
3950
- printProblems(loaded.problems);
3951
- return;
4790
+ var dependencyOf = async (root, name) => {
4791
+ const manifestDir = await nearestPackage(root);
4792
+ const manifest = manifestDir ? await readPackageJson(manifestDir) : void 0;
4793
+ const rangeOf = (dependency) => manifest?.dependencies?.[dependency] ?? manifest?.devDependencies?.[dependency] ?? manifest?.peerDependencies?.[dependency];
4794
+ const range = rangeOf(name);
4795
+ if (range) return [name, range];
4796
+ const types = typesPackageOf(name);
4797
+ const typesRange = rangeOf(types);
4798
+ return typesRange ? [types, typesRange] : void 0;
4799
+ };
4800
+ /**
4801
+ * The snapshot of what `entries` reach, gzipped as it travels, and as read. Refused, every reason at once, when an
4802
+ * import leaves the project, names a file that is not there or a package the project does not declare, or a file it
4803
+ * reaches holds a credential.
4804
+ */
4805
+ var packSource = async ({ root, kind, name, entries }) => {
4806
+ const ts = loadTypeScript$1(root);
4807
+ if (!ts) throw new Error("Packing the source reads its imports with the project’s own TypeScript:\n npm install -D typescript");
4808
+ const problems = [];
4809
+ const files = {};
4810
+ const packages = /* @__PURE__ */ new Set();
4811
+ const queue = entries.map((entry) => path.resolve(entry));
4812
+ const seen = /* @__PURE__ */ new Set();
4813
+ while (queue.length > 0) {
4814
+ const file = queue.shift();
4815
+ if (file === void 0 || seen.has(file)) continue;
4816
+ seen.add(file);
4817
+ const relative = projectPath(root, file);
4818
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
4819
+ problems.push(`${relative} is outside the project (${root}): a project's source is the files inside it`);
4820
+ continue;
3952
4821
  }
3953
- if (!loaded.tasks.includes(task)) {
3954
- fail(`functions/ declares no task ${task}. It declares: ${loaded.tasks.join(", ") || "none"}.`);
3955
- return;
4822
+ const bytes = await fs.readFile(file);
4823
+ files[relative] = bytes.toString("base64");
4824
+ let specifiers = [];
4825
+ if (CODE.test(file)) specifiers = codeImports(ts, bytes.toString("utf-8"));
4826
+ else if (STYLES.test(file)) specifiers = styleImports(bytes.toString("utf-8"));
4827
+ for (const raw of specifiers) {
4828
+ const specifier = raw.replace(/[?#].*$/, "");
4829
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
4830
+ const found = await resolveFile(file, specifier);
4831
+ if (found) queue.push(found);
4832
+ else problems.push(`${relative} imports "${raw}", which is not there`);
4833
+ continue;
4834
+ }
4835
+ if (!specifier.startsWith("node:") && !BUILTINS.has(specifier)) packages.add(packageOf(specifier));
3956
4836
  }
3957
- printRun(task, await local.tryTask(task, params));
4837
+ }
4838
+ const dependencies = {};
4839
+ for (const dependency of [...packages].sort()) {
4840
+ const found = await dependencyOf(root, dependency);
4841
+ if (found) dependencies[found[0]] = found[1];
4842
+ else problems.push(`It imports "${dependency}", which the project's package.json does not list`);
4843
+ }
4844
+ const snapshot = {
4845
+ format: 1,
4846
+ kind,
4847
+ name,
4848
+ entries: entries.map((entry) => projectPath(root, path.resolve(entry))),
4849
+ files,
4850
+ dependencies
4851
+ };
4852
+ const reading = p(snapshot, (base64) => Buffer.from(base64, "base64").toString("utf-8"));
4853
+ problems.push(...reading.ok ? [] : reading.problems);
4854
+ if (problems.length > 0) throw new Error(`The source of ${name} cannot be kept:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
4855
+ return {
4856
+ snapshot,
4857
+ bytes: new Uint8Array(gzipSync(JSON.stringify(snapshot)))
3958
4858
  };
3959
- await run();
3960
- if (!options.watch) return;
3961
- console.log(chalk.dim("\nWatching functions/ — every save runs it again. ^C to stop."));
3962
- let pending;
3963
- watch(path.join(root, FUNCTIONS_DIR), { recursive: true }, () => {
3964
- clearTimeout(pending);
3965
- pending = setTimeout(() => {
3966
- process.exitCode = void 0;
3967
- console.log(chalk.dim(`\n— ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`));
3968
- run();
3969
- }, 150);
3970
- });
3971
4859
  };
3972
4860
  //#endregion
3973
4861
  //#region src/pack/typeDeclarations.ts
@@ -4041,21 +4929,21 @@ var INLINED = [
4041
4929
  ];
4042
4930
  /** A plugin that cannot be packed, with what to change — never a stack trace out of esbuild. */
4043
4931
  var PackError = class extends Error {};
4044
- var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4932
+ var isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4045
4933
  /** The declaration, or what is missing from it — named, so the fix is one edit away. */
4046
4934
  var checkDeclaration = (value, where) => {
4047
4935
  const missing = (field) => new PackError(`${where}: the declaration has no ${field}. Write it the way \`plitzi add plugin\` does.`);
4048
- if (!isRecord$1(value) || typeof value.type !== "string" || !value.type) throw missing("`type`");
4936
+ if (!isRecord$2(value) || typeof value.type !== "string" || !value.type) throw missing("`type`");
4049
4937
  const { content } = value;
4050
- if (!isRecord$1(content)) throw missing("`content`");
4938
+ if (!isRecord$2(content)) throw missing("`content`");
4051
4939
  for (const field of [
4052
4940
  "attributes",
4053
4941
  "definition",
4054
4942
  "builder",
4055
4943
  "market",
4056
4944
  "defaultStyle"
4057
- ]) if (!isRecord$1(content[field])) throw missing(`\`content.${field}\``);
4058
- if (!isRecord$1(content.definition) || typeof content.definition.label !== "string") throw missing("`content.definition.label`");
4945
+ ]) if (!isRecord$2(content[field])) throw missing(`\`content.${field}\``);
4946
+ if (!isRecord$2(content.definition) || typeof content.definition.label !== "string") throw missing("`content.definition.label`");
4059
4947
  return value;
4060
4948
  };
4061
4949
  var toImportPath = (file) => file.split(path.sep).join("/");
@@ -4114,7 +5002,31 @@ var integrityOf = async (file) => {
4114
5002
  const content = await fs.readFile(file);
4115
5003
  return `sha384-${createHash("sha384").update(content).digest("base64")}`;
4116
5004
  };
4117
- var packPlugin = async ({ root, source, base, version, outDir, zip, types = false }) => {
5005
+ /** The files a plugin is built from: the package's entry, or each element's index. */
5006
+ var sourceEntries = (source) => source.kind === "package" ? [source.entry] : source.folders.map((folder) => path.join(folder, "index.ts"));
5007
+ /** Where the source snapshot of a plugin goes: beside its zip, named after it. */
5008
+ var sourceFileOf = (zip) => zip.replace(/\.zip$/, ".source.json.gz");
5009
+ /**
5010
+ * The plugin's source snapshot written beside its zip — or, when it cannot be kept, why: the plugin still packs, and
5011
+ * goes up built only.
5012
+ */
5013
+ var writeSource = async (name, source, root, zip) => {
5014
+ try {
5015
+ const { bytes } = await packSource({
5016
+ root,
5017
+ kind: "plugin",
5018
+ name,
5019
+ entries: sourceEntries(source)
5020
+ });
5021
+ const file = sourceFileOf(zip);
5022
+ await fs.mkdir(path.dirname(file), { recursive: true });
5023
+ await fs.writeFile(file, bytes);
5024
+ return { file };
5025
+ } catch (error) {
5026
+ return { problem: error instanceof Error ? error.message : String(error) };
5027
+ }
5028
+ };
5029
+ var packPlugin = async ({ root, source, base, version, outDir, zip, sourceRoot = root, types = false }) => {
4118
5030
  const relative = path.relative(root, outDir);
4119
5031
  if (relative.startsWith("..") || path.isAbsolute(relative) || relative === "") throw new PackError(`${outDir} is not a folder inside ${root}: the build empties it first, so it has to be.`);
4120
5032
  if (source.kind === "elements") for (const folder of source.folders) for (const file of ["index.ts", "declaration.ts"]) try {
@@ -4186,7 +5098,8 @@ var packPlugin = async ({ root, source, base, version, outDir, zip, types = fals
4186
5098
  types: declarations.map((declaration) => declaration.type),
4187
5099
  files,
4188
5100
  zip,
4189
- typesOutcome
5101
+ typesOutcome,
5102
+ source: await writeSource(main.type, source, sourceRoot, zip ?? path.join(outDir, `${base}.zip`))
4190
5103
  };
4191
5104
  };
4192
5105
  //#endregion
@@ -4286,6 +5199,7 @@ var packPluginCommand = async (foldersGiven, options) => {
4286
5199
  version,
4287
5200
  outDir,
4288
5201
  zip,
5202
+ sourceRoot: options.sourceRoot ? path.resolve(options.sourceRoot) : project.root,
4289
5203
  types: inPackage
4290
5204
  });
4291
5205
  console.log(chalk.green(`\n${result.types.join(", ")} — packed at ${path.relative(process.cwd(), outDir) || "."}`));
@@ -4296,6 +5210,11 @@ var packPluginCommand = async (foldersGiven, options) => {
4296
5210
  console.log(`\n ${path.relative(process.cwd(), result.zip)}`);
4297
5211
  console.log(chalk.dim(" Upload it in the builder under Resources, as a plugin."));
4298
5212
  }
5213
+ if ("file" in result.source) console.log(chalk.dim(` ${path.relative(process.cwd(), result.source.file)} — its source, which plitzi upload keeps too`));
5214
+ else {
5215
+ console.log(chalk.yellow("\n Its source is not kept, so the space can be taken back out with this plugin built only:"));
5216
+ console.log(chalk.yellow(` ${result.source.problem.split("\n").join("\n ")}`));
5217
+ }
4299
5218
  console.log("");
4300
5219
  } catch (error) {
4301
5220
  if (!(error instanceof PackError)) throw error;
@@ -4304,6 +5223,250 @@ var packPluginCommand = async (foldersGiven, options) => {
4304
5223
  }
4305
5224
  };
4306
5225
  //#endregion
5226
+ //#region src/commands/packSource.ts
5227
+ /**
5228
+ * `plitzi pack source`: the snapshot `plitzi upload plugin` and `plitzi runtime push` keep beside what they send (RFC
5229
+ * 0020), written to a file — to see what of a project would be kept, or for a tool that keeps it itself (the seeder).
5230
+ */
5231
+ var packSourceCommand = async (entries, options) => {
5232
+ const root = options.root ? path.resolve(options.root) : (await findProject(process.cwd()))?.root;
5233
+ if (!root) {
5234
+ console.error(chalk.red("plitzi pack source packs a project’s source, and there is no package.json here or above."));
5235
+ process.exitCode = 1;
5236
+ return;
5237
+ }
5238
+ try {
5239
+ const { snapshot, bytes } = await packSource({
5240
+ root,
5241
+ kind: options.kind,
5242
+ name: options.name,
5243
+ entries: entries.map((entry) => path.resolve(entry))
5244
+ });
5245
+ const out = path.resolve(options.out);
5246
+ await fs.mkdir(path.dirname(out), { recursive: true });
5247
+ await fs.writeFile(out, bytes);
5248
+ 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
+ } catch (error) {
5250
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
5251
+ process.exitCode = 1;
5252
+ }
5253
+ };
5254
+ //#endregion
5255
+ //#region src/commands/pull.ts
5256
+ /** The version a pull asks for: the one the project follows, or the one it is told to follow from now on. */
5257
+ var versionToPull = (options, followed) => {
5258
+ const environment = options.environment ?? followed.environment;
5259
+ const kept = environment === followed.environment ? followed.revision : void 0;
5260
+ if (options.revision === void 0 || options.revision === "latest") {
5261
+ const revision = options.revision === "latest" ? void 0 : kept;
5262
+ return {
5263
+ ok: true,
5264
+ version: {
5265
+ environment,
5266
+ ...revision ? { revision } : {}
5267
+ }
5268
+ };
5269
+ }
5270
+ const revision = Number(options.revision);
5271
+ if (environment === "main") return {
5272
+ ok: false,
5273
+ error: "The draft (main) has no revisions: name a published environment with --environment."
5274
+ };
5275
+ if (!Number.isInteger(revision) || revision < 1) return {
5276
+ ok: false,
5277
+ error: `--revision takes a revision number from 1, or latest — not "${options.revision}".`
5278
+ };
5279
+ return {
5280
+ ok: true,
5281
+ version: {
5282
+ environment,
5283
+ revision
5284
+ }
5285
+ };
5286
+ };
5287
+ var verdictOf = ({ given, was, here }) => {
5288
+ if (given === void 0) {
5289
+ if (here === void 0 || was === void 0) return "same";
5290
+ return here === was ? "remove" : "keep";
5291
+ }
5292
+ if (here === given) return "same";
5293
+ if (here === void 0) {
5294
+ if (was === void 0) return "write";
5295
+ return given === was ? "keep" : "conflict";
5296
+ }
5297
+ if (here === was) return "write";
5298
+ return given === was ? "keep" : "conflict";
5299
+ };
5300
+ var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5301
+ /**
5302
+ * The packages the space's code asks for, added to the project's `package.json`: a package it does not list is added,
5303
+ * one still at the range the space last asked for follows the space, and one the project changed stays as it is.
5304
+ */
5305
+ var mergeDependencies = (manifest, asked, before) => {
5306
+ const current = isRecord$1(manifest.dependencies) ? manifest.dependencies : {};
5307
+ const dependencies = { ...current };
5308
+ const changed = [];
5309
+ const kept = [];
5310
+ Object.entries(asked).forEach(([name, range]) => {
5311
+ const listed = current[name];
5312
+ if (listed === range) return;
5313
+ if (listed === void 0 || listed === before[name]) {
5314
+ dependencies[name] = range;
5315
+ changed.push(`${name}@${range}`);
5316
+ return;
5317
+ }
5318
+ kept.push(`${name}: the space's code now asks for ${range}, and package.json keeps ${JSON.stringify(listed)}`);
5319
+ });
5320
+ return {
5321
+ manifest: {
5322
+ ...manifest,
5323
+ dependencies: Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)))
5324
+ },
5325
+ changed,
5326
+ kept
5327
+ };
5328
+ };
5329
+ var writeBytes = async (root, file, bytes) => {
5330
+ await fs.mkdir(path.dirname(path.join(root, file)), { recursive: true });
5331
+ await fs.writeFile(path.join(root, file), bytes);
5332
+ };
5333
+ var list = (files) => files.map((file) => `\n ${file}`).join("");
5334
+ var plural = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
5335
+ var pull = async (options) => {
5336
+ const project = await findProject(process.cwd());
5337
+ const root = project?.root ?? process.cwd();
5338
+ const origin = await readOrigin(root);
5339
+ if (!origin) {
5340
+ fail("This project was not made from a space: there is no .plitzi/space.json here. plitzi create --from <space> makes one.");
5341
+ return;
5342
+ }
5343
+ const api = options.api ? await apiFor(options) : origin.api;
5344
+ if (!api) return;
5345
+ const asked = versionToPull(options, origin.version);
5346
+ if (!asked.ok) {
5347
+ fail(asked.error);
5348
+ return;
5349
+ }
5350
+ const exported = await fetchExport(api, String(origin.space.id), {
5351
+ source: origin.source,
5352
+ version: asked.version,
5353
+ name: origin.space.name
5354
+ });
5355
+ if (!exported) return;
5356
+ const next = projectFromSpace(exported, origin.source);
5357
+ const format = await projectFormatter(root);
5358
+ const given = /* @__PURE__ */ new Map();
5359
+ for (const [file, text] of Object.entries(next.files)) given.set(file, Buffer.from(await format(file, text)));
5360
+ Object.entries(next.binaries).forEach(([file, base64]) => given.set(file, Buffer.from(base64, "base64")));
5361
+ const missing = [];
5362
+ const untouched = /* @__PURE__ */ new Set();
5363
+ for (const { url, to } of next.downloads) {
5364
+ if (origin.downloads[to] === url && await digestsOf(root, to, format) !== void 0) {
5365
+ untouched.add(to);
5366
+ continue;
5367
+ }
5368
+ try {
5369
+ given.set(to, await download(url));
5370
+ } catch (error) {
5371
+ missing.push(notFetched(to, url, error));
5372
+ untouched.add(to);
5373
+ }
5374
+ }
5375
+ const paths = [.../* @__PURE__ */ new Set([...given.keys(), ...Object.keys(origin.files)])].filter((file) => !untouched.has(file));
5376
+ const verdicts = /* @__PURE__ */ new Map();
5377
+ for (const file of paths) {
5378
+ const bytes = given.get(file);
5379
+ const here = await digestsOf(root, file, format);
5380
+ const was = origin.files[file];
5381
+ const unchanged = here !== void 0 && (here.raw === was || here.formatted === was);
5382
+ verdicts.set(file, verdictOf({
5383
+ given: bytes ? digest(bytes) : void 0,
5384
+ was: unchanged ? here.formatted : was,
5385
+ here: here?.formatted
5386
+ }));
5387
+ }
5388
+ const of = (verdict) => [...verdicts].filter(([, value]) => value === verdict).map(([file]) => file);
5389
+ const conflicts = of("conflict");
5390
+ if (conflicts.length > 0 && !options.force) {
5391
+ fail(`Nothing was pulled: these changed here and on ${origin.space.name} too:${list(conflicts)}\nCommit or set aside the changes here and pull again, or pass --force to take the space’s copy of them.`);
5392
+ return;
5393
+ }
5394
+ const written = [...of("write"), ...conflicts];
5395
+ for (const file of written) {
5396
+ const bytes = given.get(file);
5397
+ if (bytes) await writeBytes(root, file, bytes);
5398
+ }
5399
+ const removed = of("remove");
5400
+ await Promise.all(removed.map((file) => fs.rm(path.join(root, file), { force: true })));
5401
+ const packageFile = path.join(root, "package.json");
5402
+ const manifest = JSON.parse(await fs.readFile(packageFile, "utf-8"));
5403
+ const dependencies = mergeDependencies(isRecord$1(manifest) ? manifest : {}, next.dependencies, origin.dependencies);
5404
+ if (dependencies.changed.length > 0) await fs.writeFile(packageFile, `${JSON.stringify(dependencies.manifest, null, 2)}\n`);
5405
+ const files = Object.fromEntries([...[...given].map(([file, bytes]) => [file, digest(bytes)]), ...[...untouched].flatMap((file) => origin.files[file] ? [[file, origin.files[file]]] : [])]);
5406
+ await writeOrigin(root, {
5407
+ ...origin,
5408
+ api,
5409
+ space: exported.space,
5410
+ version: asked.version,
5411
+ files,
5412
+ downloads: Object.fromEntries(next.downloads.flatMap(({ url, to }) => {
5413
+ if (given.has(to)) return [[to, url]];
5414
+ const before = origin.downloads[to];
5415
+ return before && Object.hasOwn(files, to) ? [[to, before]] : [];
5416
+ })),
5417
+ dependencies: next.dependencies
5418
+ });
5419
+ if (!of("keep").some((file) => file.startsWith("functions/"))) await writeFunctionsState(root, {
5420
+ space: exported.space.id,
5421
+ version: next.functions.version,
5422
+ files: await functionsOnDisk(root, next)
5423
+ });
5424
+ const kept = of("keep");
5425
+ console.log(chalk.green(`\nPulled ${exported.space.name} (${versionLabel(exported.version)}): ${plural(written.length, "file")} written, ${plural(removed.length, "file")} removed.`));
5426
+ if (written.length > 0) console.log(chalk.dim(list(written)));
5427
+ if (kept.length > 0) console.log(`Changed here, and kept:${list(kept)}`);
5428
+ if (dependencies.changed.length > 0) {
5429
+ const manager = project?.packageManager ?? "npm";
5430
+ console.log(`package.json now asks for ${dependencies.changed.join(", ")}: run ${installCommand(manager)}.`);
5431
+ }
5432
+ const notes = [
5433
+ ...dependencies.kept,
5434
+ ...missing,
5435
+ ...next.report
5436
+ ];
5437
+ if (notes.length > 0) {
5438
+ console.log(chalk.yellow(`\nWhat to know about ${exported.space.name} here:`));
5439
+ notes.forEach((note) => console.log(chalk.yellow(` - ${note}`)));
5440
+ }
5441
+ console.log("");
5442
+ };
5443
+ //#endregion
5444
+ //#region src/commands/keepSource.ts
5445
+ /**
5446
+ * A packed source snapshot sent beside what was just uploaded or pushed, and kept on the space (docs/en/projects-from-spaces.md): what lets
5447
+ * `plitzi create --from` take the space back out as a project. Never undoes what went up before it — the plugin or the
5448
+ * runtime runs either way — so a refusal is said, with why, and the command still succeeds.
5449
+ */
5450
+ var keepSource = async (connection, spaceId, bytes) => {
5451
+ const sent = await authorizedRequest(connection, `/spaces/${String(spaceId)}/sources`, {
5452
+ method: "PUT",
5453
+ headers: { "Content-Type": "application/octet-stream" },
5454
+ body: new Uint8Array(bytes)
5455
+ });
5456
+ if (!sent.ok) {
5457
+ console.log(chalk.yellow(` Its source is not kept: ${sent.error}`));
5458
+ return;
5459
+ }
5460
+ const { status, data } = sent.value.reply;
5461
+ if (status === 200 && data.ok) {
5462
+ console.log(chalk.dim(` Its source too (${String(data.files ?? 0)} files): plitzi create --from can take it back out.`));
5463
+ return;
5464
+ }
5465
+ const problems = data.problems ?? [`The platform answered ${String(status)}`];
5466
+ console.log(chalk.yellow(" Its source is not kept, so a project taken from the space gets it built only:"));
5467
+ problems.forEach((problem) => console.log(chalk.yellow(` - ${problem}`)));
5468
+ };
5469
+ //#endregion
4307
5470
  //#region src/commands/runtime.ts
4308
5471
  var DEFAULT_ENTRY = path.join("src", "runtime.ts");
4309
5472
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -4357,6 +5520,18 @@ var pushRuntime = async (options) => {
4357
5520
  return;
4358
5521
  }
4359
5522
  console.log(`${chalk.green("✓")} ${connection.space.name}’s draft runtime is ${reply.data.digest.slice(0, 12)} (${(bytes.byteLength / 1024).toFixed(0)} KB). It starts in a moment; publish the space to take it live.`);
5523
+ try {
5524
+ const source = await packSource({
5525
+ root,
5526
+ kind: "runtime",
5527
+ name: "runtime",
5528
+ entries: [entry]
5529
+ });
5530
+ await keepSource(answered.value.connection, connection.space.id, source.bytes);
5531
+ } catch (error) {
5532
+ console.log(chalk.yellow(" Its source is not kept, so a project taken from the space gets it built only:"));
5533
+ console.log(chalk.yellow(` ${error instanceof Error ? error.message : String(error)}`));
5534
+ }
4360
5535
  };
4361
5536
  var readRuntime = async (connection, spaceId) => {
4362
5537
  const answered = await authorizedRequest(connection, `/spaces/${String(spaceId)}/runtime`);
@@ -4662,7 +5837,10 @@ var uploadPluginCommand = async (zipGiven, options) => {
4662
5837
  const installed = data.installed === "updated" ? `now loads ${label}` : `loads ${label} from now on`;
4663
5838
  console.log(chalk.green(`\n${space.name} ${installed}.`));
4664
5839
  if (data.resource?.path) console.log(chalk.dim(` ${data.resource.path}`));
4665
- console.log(chalk.dim(" Any builder open on the space loads it now.\n"));
5840
+ console.log(chalk.dim(" Any builder open on the space loads it now."));
5841
+ const source = await fs.readFile(sourceFileOf(zipPath)).catch(() => void 0);
5842
+ if (source) await keepSource(uploaded.value.connection, space.id, source);
5843
+ console.log("");
4666
5844
  };
4667
5845
  //#endregion
4668
5846
  //#region src/index.ts
@@ -4680,15 +5858,18 @@ var PROJECT_ONLY = [
4680
5858
  "source",
4681
5859
  "key"
4682
5860
  ];
4683
- 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 to serve: main, or a published environment", "main").addOption(new Option("-p, --package-manager <manager>", "The package manager the project is written for. Asked for when left out.").choices([...PACKAGE_MANAGERS])).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) => {
5861
+ 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) => {
4684
5863
  if (!options.plugin) return create(directory, options);
4685
5864
  const stray = PROJECT_ONLY.filter((flag) => options[flag] !== void 0);
4686
5865
  if (stray.length > 0) program.error(`--${stray.join(", --")} shape a project, not a plugin: leave them out with --plugin.`);
4687
5866
  return createPlugin(directory, options);
4688
5867
  });
5868
+ 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));
4689
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));
4690
- program.command("pack").description("Build something of this project into what the platform takes").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.").action((folders, options) => packPluginCommand(folders, options));
4691
- 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."];
5870
+ var pack = program.command("pack").description("Build something of this project into what the platform takes");
5871
+ 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
+ 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));
4692
5873
  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));
4693
5874
  program.command("logout").description("Sign out: the session is revoked on the platform and forgotten here").action(() => logout());
4694
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));