@popcraft/cli 0.32.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/popcraft.mjs +100 -17
  2. package/package.json +2 -2
package/dist/popcraft.mjs CHANGED
@@ -594,7 +594,7 @@ var init_openapi = __esm({
594
594
  init_api_limits();
595
595
  init_listing();
596
596
  init_groups();
597
- API_VERSION = "1.3.0";
597
+ API_VERSION = "1.4.0";
598
598
  ref = (name2) => ({ $ref: `#/components/schemas/${name2}` });
599
599
  str = { type: "string" };
600
600
  nstr = { type: ["string", "null"] };
@@ -897,7 +897,7 @@ Lists are paged: follow \`nextCursor\` (passing it as \`?cursor=\`) until it is
897
897
  "/files/{key}": {
898
898
  get: op("Files", "File metadata, your role and document JSON", "files:read", ok(ref("FileDetail")), { parameters: [key, query("depth", { type: "string", enum: ["metadata"] }, "Omit the document")] }),
899
899
  put: op("Files", "Replace the document", "files:write", ok(ref("WriteResult")), {
900
- description: "Body: a `Document`, `{ document }` or a .popcraft export. A live file takes the difference as one edit in its collaborative log, so people editing it see the change merge in; private (PopCorn) layers are never written. Send `If-Match: <revision>` to write only if the file has not changed since you read it. Needs edit access.",
900
+ description: "Body: a `Document`, `{ document }` or a .popcraft export, and may be gzipped (`Content-Encoding: gzip`). Requests are capped at about 4.5 MB on the wire, so upload pictures, video and fonts first (`POST /files/{key}/media`) and send a document that names them as `media://<hash>`. A live file takes the difference as one edit in its collaborative log, so people editing it see the change merge in; private (PopCorn) layers are never written. Send `If-Match: <revision>` to write only if the file has not changed since you read it. Needs edit access.",
901
901
  parameters: [key, { name: "If-Match", in: "header", required: false, schema: str, description: "The `revision` you read" }],
902
902
  requestBody: { required: true, content: { "application/json": { schema: ref("Document") } } },
903
903
  responses: {
@@ -922,6 +922,14 @@ Lists are paged: follow \`nextCursor\` (passing it as \`?cursor=\`) until it is
922
922
  "/files/{key}/nodes": { get: op("Files", "Nodes with their subtrees", "files:read", ok(ref("NodeResult")), { parameters: [key, query("ids", str, "Comma-separated node ids (\u2264 200)", true), query("depth", { type: "string", enum: ["1"] }, "Only the nodes themselves, no descendants")], responses: { "400": { description: "`ids` missing", content: errBody } } }) },
923
923
  "/files/{key}/images": { get: op("Files", "Thumbnail and media assets the file references", "files:read", ok(ref("Images")), { parameters: [key] }) },
924
924
  "/files/{key}/thumbnail": { get: op("Files", "Preview image bytes", "files:read", ok({ type: "string", format: "binary" }, "PNG or WebP bytes", "image/*"), { parameters: [key] }) },
925
+ "/files/{key}/media": {
926
+ post: op("Files", "Upload media the file uses", "files:write", null, {
927
+ description: 'The raw bytes of a picture, video, audio or font file, with `x-media-hash` set to their hash (SHA-256, the first 32 hex characters). The document, or a command such as `font.add` (`src: "media://<hash>"`), can then name them. A workspace stores the same bytes once. Needs edit access.',
928
+ parameters: [key, { name: "x-media-hash", in: "header", required: true, schema: str, description: "The bytes' hash: the first 32 hex characters of their SHA-256" }],
929
+ requestBody: { required: true, content: { "*/*": { schema: { type: "string", format: "binary" } } } },
930
+ responses: { "200": ok(obj({ hash: str, uploaded: bool }, ["hash", "uploaded"]), "Already stored"), "201": ok(obj({ hash: str, uploaded: bool }, ["hash", "uploaded"]), "Stored"), "400": { description: "Missing hash, or the hash does not match the bytes", content: errBody }, "413": { description: "Over the plan's upload size", content: errBody } }
931
+ })
932
+ },
925
933
  "/files/{key}/media/{hash}": { get: op("Files", "Media asset bytes", "files:read", ok({ type: "string", format: "binary" }, "The asset, with its own content type", "*/*"), { parameters: [key, id("hash", "From `/files/{key}/images`")] }) },
926
934
  "/files/{key}/comments": {
927
935
  get: op("Comments", "Comments (threads and replies), oldest first", "comments:read", ok(ref("CommentList")), { parameters: [key, query("limit", { ...int, minimum: 1, maximum: 500, default: 500 }, "Page size"), cursor] }),
@@ -1188,7 +1196,7 @@ function operations(spec = openapi) {
1188
1196
  }
1189
1197
  async function call(target2, method, path, opts = {}) {
1190
1198
  const qs = new URLSearchParams(opts.query ?? {}).toString();
1191
- const headers = { "user-agent": `popcraft-cli/${"0.32.1"}`, ...opts.headers };
1199
+ const headers = { "user-agent": `popcraft-cli/${"0.33.0"}`, ...opts.headers };
1192
1200
  if (target2.token) headers.authorization = `Bearer ${target2.token}`;
1193
1201
  if (opts.contentType) headers["content-type"] = opts.contentType;
1194
1202
  try {
@@ -95224,6 +95232,11 @@ var init_idb = __esm({
95224
95232
  });
95225
95233
 
95226
95234
  // lib/storage/media-hash.ts
95235
+ var media_hash_exports = {};
95236
+ __export(media_hash_exports, {
95237
+ hashBytesSync: () => hashBytesSync,
95238
+ sha256Prefix: () => sha256Prefix
95239
+ });
95227
95240
  function hashBytesSync(bytes2) {
95228
95241
  const run4 = (seed2) => {
95229
95242
  let h1 = 3735928559 ^ seed2, h22 = 1103547991 ^ seed2;
@@ -95238,6 +95251,10 @@ function hashBytesSync(bytes2) {
95238
95251
  };
95239
95252
  return run4(2654435769) + run4(2246822507 ^ bytes2.length);
95240
95253
  }
95254
+ async function sha256Prefix(bytes2) {
95255
+ const digest = await crypto.subtle.digest("SHA-256", bytes2);
95256
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("").slice(0, 32);
95257
+ }
95241
95258
  var init_media_hash = __esm({
95242
95259
  "lib/storage/media-hash.ts"() {
95243
95260
  "use strict";
@@ -310142,6 +310159,7 @@ async function dispatchShaped(key5, options) {
310142
310159
  var onViolation;
310143
310160
  var init_dispatch = __esm({
310144
310161
  "lib/commands/dispatch.ts"() {
310162
+ "use strict";
310145
310163
  init_errors();
310146
310164
  init_registry();
310147
310165
  init_confirm();
@@ -310532,6 +310550,7 @@ function creditLine(c2) {
310532
310550
  var MAX_PHOTO_PAGE, MAX_PHOTO_QUERY, UUID, isPhotoId, photoImagePath;
310533
310551
  var init_stock = __esm({
310534
310552
  "lib/photos/stock.ts"() {
310553
+ "use strict";
310535
310554
  MAX_PHOTO_PAGE = 12;
310536
310555
  MAX_PHOTO_QUERY = 200;
310537
310556
  UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -315656,6 +315675,7 @@ async function exportSettingFile(renderer, node2, setting, doc2, styles) {
315656
315675
  var MIME, DEFAULT_EXPORT, exportSettingsOf;
315657
315676
  var init_export = __esm({
315658
315677
  "lib/io/export.ts"() {
315678
+ "use strict";
315659
315679
  init_download();
315660
315680
  init_scene_graph();
315661
315681
  init_codecs();
@@ -326442,7 +326462,7 @@ var init_package = __esm({
326442
326462
  "packages/runtime/package.json"() {
326443
326463
  package_default = {
326444
326464
  name: "@popcraft/runtime",
326445
- version: "0.32.1",
326465
+ version: "0.33.0",
326446
326466
  description: "What code exported from PopCraft shares: the meaning of expressions (operators and functions) and, as it grows, the parts of a design's behaviour that are common code rather than generated React.",
326447
326467
  license: "MIT",
326448
326468
  homepage: "https://popcraft.app/docs/exporting/nextjs",
@@ -351108,6 +351128,7 @@ function exportsOf(source3) {
351108
351128
  var CODE_LIMITS, packageOf;
351109
351129
  var init_source2 = __esm({
351110
351130
  "lib/code/source.ts"() {
351131
+ "use strict";
351111
351132
  CODE_LIMITS = { modules: 100, sourceLength: 2e5, packages: 12 };
351112
351133
  packageOf = (spec) => spec.split("/").slice(0, spec.startsWith("@") ? 2 : 1).join("/");
351113
351134
  }
@@ -351150,6 +351171,7 @@ function compileModule(module2) {
351150
351171
  var import_sucrase, BUILT_IN, memo4;
351151
351172
  var init_compile2 = __esm({
351152
351173
  "lib/code/compile.ts"() {
351174
+ "use strict";
351153
351175
  import_sucrase = __toESM(require_dist2());
351154
351176
  init_source2();
351155
351177
  init_source2();
@@ -360124,6 +360146,7 @@ var init_publish = __esm({
360124
360146
  var storeOpsSeam;
360125
360147
  var init_seams = __esm({
360126
360148
  "lib/commands/seams.ts"() {
360149
+ "use strict";
360127
360150
  storeOpsSeam = (st) => ({
360128
360151
  booleanOperation: (op4) => st().booleanOperation(op4),
360129
360152
  flatten: () => st().flattenSelection(),
@@ -396078,7 +396101,11 @@ async function fontBytes(args2, ctx2) {
396078
396101
  }
396079
396102
  } else if (args2.src) {
396080
396103
  const { isMediaUrl: isMediaUrl2, loadMediaBlob: loadMediaBlob2 } = await Promise.resolve().then(() => (init_media_store(), media_store_exports));
396081
- if (isMediaUrl2(args2.src)) {
396104
+ if (isMediaUrl2(args2.src) && ctx2.can("media")) {
396105
+ const bytes2 = await ctx2.use("media").read(args2.src);
396106
+ if (!bytes2) throw new CommandError(`${args2.src} is not in this file's media: upload it first (POST /files/{key}/media).`, "not_found");
396107
+ got = { bytes: bytes2 };
396108
+ } else if (isMediaUrl2(args2.src)) {
396082
396109
  const blob = await loadMediaBlob2(args2.src);
396083
396110
  if (!blob) throw new CommandError(`${args2.src} is not on this device.`, "not_found");
396084
396111
  got = { bytes: new Uint8Array(await blob.arrayBuffer()) };
@@ -396155,7 +396182,7 @@ var init_font = __esm({
396155
396182
  params: external_exports.object({
396156
396183
  family: external_exports.string().trim().min(1).max(200).optional().describe("The family name texts will use (default: the name in the file). Texts set in it draw in this file."),
396157
396184
  file: external_exports.string().min(1).max(4096).optional().describe("A font file on this machine, read by the CLI: .ttf, .otf, .ttc, .woff or .woff2."),
396158
- src: external_exports.string().max(MAX_SRC).optional().describe("The font file as a base64 data URL, or a media:// ref already on this device."),
396185
+ src: external_exports.string().max(MAX_SRC).optional().describe("The font file as a base64 data URL, or a media:// ref already on this device (through the API: already uploaded to the file's media)."),
396159
396186
  weight: external_exports.number().int().min(1).max(1e3).optional().describe("The weight this face stands for (default: the file's)."),
396160
396187
  style: external_exports.enum(["normal", "italic"]).optional().describe("normal or italic (default: the file's)."),
396161
396188
  face: faceParam.optional()
@@ -396170,10 +396197,9 @@ var init_font = __esm({
396170
396197
  run: async (args2, ctx2) => {
396171
396198
  const { bytes: bytes2, fileName: fileName2 } = await fontBytes(args2, ctx2);
396172
396199
  const { face, family, weight, style, info: info2 } = await resolveFace(bytes2, args2);
396173
- const { internBytes: internBytes2 } = await Promise.resolve().then(() => (init_media_store(), media_store_exports));
396174
396200
  const { fontMime: fontMime2 } = await Promise.resolve().then(() => (init_sfnt(), sfnt_exports));
396175
396201
  const { addDocumentFont: addDocumentFont2 } = await Promise.resolve().then(() => (init_document_fonts(), document_fonts_exports));
396176
- const src = internBytes2(bytes2, fontMime2(bytes2));
396202
+ const src = ctx2.can("media") ? await ctx2.use("media").store(bytes2, fontMime2(bytes2)) : (await Promise.resolve().then(() => (init_media_store(), media_store_exports))).internBytes(bytes2, fontMime2(bytes2));
396177
396203
  const font = {
396178
396204
  id: `font-${src.slice(8, 20)}-${face.index}-${weight}-${style === "italic" ? "i" : "n"}`,
396179
396205
  family,
@@ -397090,6 +397116,7 @@ function record(source, namespaces, globals, limits) {
397090
397116
  var HELPER_NAMES;
397091
397117
  var init_helper_names = __esm({
397092
397118
  "lib/agent/design-script/helper-names.ts"() {
397119
+ "use strict";
397093
397120
  HELPER_NAMES = ["draw", "marks", "fit", "motion", "captions", "overlays", "texture", "pointer", "windows", "devices", "type", "beat", "web", "social", "video", "site", "effects"];
397094
397121
  }
397095
397122
  });
@@ -397472,6 +397499,7 @@ function designRecorder() {
397472
397499
  var installed;
397473
397500
  var init_recorders = __esm({
397474
397501
  "lib/agent/design-script/recorders.ts"() {
397502
+ "use strict";
397475
397503
  installed = null;
397476
397504
  }
397477
397505
  });
@@ -407837,6 +407865,7 @@ var init_headless2 = __esm({
407837
407865
 
407838
407866
  // packages/cli/src/cloud.ts
407839
407867
  import { existsSync as existsSync4 } from "node:fs";
407868
+ import { gzipSync } from "node:zlib";
407840
407869
  function signedIn2(target2) {
407841
407870
  if (!target2.token) throw new CliError(`Not signed in to ${target2.url}: run \`popcraft login\` (with a files:write token).`);
407842
407871
  return target2;
@@ -407851,25 +407880,79 @@ async function runPush(args2, flags, target2, out) {
407851
407880
  const name2 = flagString(flags, "name");
407852
407881
  if (key5) {
407853
407882
  const ifMatch = flagString(flags, "ifMatch");
407854
- const body2 = JSON.stringify({ document: name2 ? { ...document2, name: name2 } : document2 });
407883
+ const { doc: doc3, media: media3 } = await separateMedia(document2);
407884
+ await uploadMedia2(t3, key5, media3);
407885
+ const body = gzipJson({ document: name2 ? { ...doc3, name: name2 } : doc3 });
407855
407886
  const res2 = await callJson(t3, "PUT", `/files/${encodeURIComponent(key5)}`, {
407856
- body: body2,
407857
- contentType: "application/json",
407858
- headers: ifMatch ? { "if-match": ifMatch } : void 0
407887
+ ...body,
407888
+ headers: { ...body.headers, ...ifMatch ? { "if-match": ifMatch } : {} }
407859
407889
  });
407860
407890
  out.result(res2.changed ? `Updated ${key5} (revision ${res2.revision})${res2.live ? "; open sessions merged it" : ""}.` : `${key5} already matched ${file2}.`, out.json ? { key: key5, ...res2 } : void 0);
407861
407891
  return;
407862
407892
  }
407863
- const body = JSON.stringify({
407864
- document: document2,
407893
+ const { doc: doc2, media: media2 } = await separateMedia(document2);
407894
+ const res = await callJson(t3, "POST", "/files", gzipJson({
407895
+ document: doc2,
407865
407896
  ...name2 ? { name: name2 } : {},
407866
407897
  ...flagString(flags, "workspace") ? { workspaceId: flagString(flags, "workspace") } : {},
407867
407898
  ...flagString(flags, "project") ? { projectId: flagString(flags, "project") } : {},
407868
407899
  ...flagString(flags, "folder") ? { folderId: flagString(flags, "folder") } : {}
407869
- });
407870
- const res = await callJson(t3, "POST", "/files", { body, contentType: "application/json" });
407900
+ }));
407901
+ await uploadMedia2(t3, res.key, media2);
407871
407902
  out.result(`Created ${res.key}: ${t3.url}/pop/${res.key}`, out.json ? { ...res, url: `${t3.url}/pop/${res.key}` } : void 0);
407872
407903
  }
407904
+ async function separateMedia(document2) {
407905
+ const { MEDIA_SCHEME: MEDIA_SCHEME2, isMediaUrl: isMediaUrl2, loadMediaBlob: loadMediaBlob2 } = await Promise.resolve().then(() => (init_media_store(), media_store_exports));
407906
+ const { hashBytesSync: hashBytesSync2 } = await Promise.resolve().then(() => (init_media_hash(), media_hash_exports));
407907
+ const media2 = /* @__PURE__ */ new Map();
407908
+ const pending8 = [];
407909
+ const walk4 = (v) => {
407910
+ if (typeof v === "string") {
407911
+ if (isMediaUrl2(v)) {
407912
+ const hash3 = v.slice(MEDIA_SCHEME2.length);
407913
+ if (!media2.has(hash3)) {
407914
+ media2.set(hash3, { bytes: new Uint8Array(0), type: "" });
407915
+ pending8.push(loadMediaBlob2(v).then(async (blob) => {
407916
+ if (blob) media2.set(hash3, { bytes: new Uint8Array(await blob.arrayBuffer()), type: blob.type });
407917
+ else media2.delete(hash3);
407918
+ }));
407919
+ }
407920
+ return v;
407921
+ }
407922
+ const m = /^data:([^;,]*)(?:;[^,]*)?;base64,/.exec(v);
407923
+ if (m && v.length > 1024) {
407924
+ const bytes2 = Uint8Array.from(Buffer.from(v.slice(m[0].length), "base64"));
407925
+ const hash3 = hashBytesSync2(bytes2);
407926
+ media2.set(hash3, { bytes: bytes2, type: m[1] || "application/octet-stream" });
407927
+ return MEDIA_SCHEME2 + hash3;
407928
+ }
407929
+ return v;
407930
+ }
407931
+ if (Array.isArray(v)) return v.map(walk4);
407932
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k2, x2]) => [k2, walk4(x2)]));
407933
+ return v;
407934
+ };
407935
+ const doc2 = walk4(document2);
407936
+ await Promise.all(pending8);
407937
+ return { doc: doc2, media: media2 };
407938
+ }
407939
+ async function uploadMedia2(t3, key5, media2) {
407940
+ const queue2 = [...media2];
407941
+ const worker2 = async () => {
407942
+ for (let next2 = queue2.shift(); next2; next2 = queue2.shift()) {
407943
+ const [hash3, { bytes: bytes2, type }] = next2;
407944
+ await callJson(t3, "POST", `/files/${encodeURIComponent(key5)}/media`, {
407945
+ body: bytes2,
407946
+ contentType: type || "application/octet-stream",
407947
+ headers: { "x-media-hash": hash3 }
407948
+ });
407949
+ }
407950
+ };
407951
+ await Promise.all(Array.from({ length: Math.min(4, queue2.length) }, worker2));
407952
+ }
407953
+ function gzipJson(value3) {
407954
+ return { body: gzipSync(JSON.stringify(value3)), contentType: "application/json", headers: { "content-encoding": "gzip" } };
407955
+ }
407873
407956
  async function runPull(args2, flags, target2, out) {
407874
407957
  const [key5, dest] = args2;
407875
407958
  if (!key5) throw usage("Usage: popcraft pull <key> [file.popcraft]");
@@ -409792,7 +409875,7 @@ init_resources();
409792
409875
  init_desktop();
409793
409876
  init_hypery();
409794
409877
  import { createInterface } from "node:readline";
409795
- var VERSION2 = "0.32.1";
409878
+ var VERSION2 = "0.33.0";
409796
409879
  var HELP = `popcraft ${VERSION2} \u2014 the PopCraft command line
409797
409880
 
409798
409881
  Files (headless, on .popcraft files):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popcraft/cli",
3
- "version": "0.32.1",
3
+ "version": "0.33.0",
4
4
  "description": "The PopCraft command line: edit .popcraft files headlessly, call the REST API, publish to the marketplace and manage the desktop app's local plugins, themes, fonts, skills and agents.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://popcraft.app/docs/api/cli",
@@ -34,7 +34,7 @@
34
34
  "esbuild": "0.28.2"
35
35
  },
36
36
  "dependencies": {
37
- "@popcraft/kit": "0.32.1"
37
+ "@popcraft/kit": "0.33.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node build.mjs",