foss-design 0.3.1 → 0.3.2

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +119 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@ Or install the `design` command globally: `npm i -g foss-design`.
37
37
  | `build [canvas…] [--out] [--tar]` | Static site. |
38
38
  | `login`, `logout` | Sign this machine in to foss-design Cloud (a link with the code in it). |
39
39
  | `link [<project>] [--new <name>]` | List cloud projects, or link `.design` to one. |
40
- | `push [canvas…] [--resolved <unit>]`, `pull [canvas…] [--theirs <unit>]` | Sync with the cloud; exit 2 on a conflict. |
40
+ | `push [canvas…] [--resolved <unit>]`, `pull [canvas…] [--theirs <unit>]` | Sync with the cloud, with progress; push prints the web link of each pushed canvas; exit 2 on a conflict. |
41
41
 
42
42
  Requirements: Node.js 20.19+. `check --render` and `shot` need an installed Chrome, Chromium, Edge or Brave, or `DESIGN_CHROME` pointing at one.
43
43
 
package/dist/cli.js CHANGED
@@ -1440,10 +1440,10 @@ function optsOutOfSystem(html) {
1440
1440
  }
1441
1441
  function injectIntoHtml(html, head) {
1442
1442
  const tags = headTags(head);
1443
- const open = /<head(\s[^>]*)?>/i.exec(html);
1444
- if (open) return `${html.slice(0, open.index + open[0].length)}
1443
+ const open2 = /<head(\s[^>]*)?>/i.exec(html);
1444
+ if (open2) return `${html.slice(0, open2.index + open2[0].length)}
1445
1445
  ${tags}
1446
- ${html.slice(open.index + open[0].length)}`;
1446
+ ${html.slice(open2.index + open2[0].length)}`;
1447
1447
  const htmlOpen = /<html(\s[^>]*)?>/i.exec(html);
1448
1448
  if (htmlOpen)
1449
1449
  return html.slice(0, htmlOpen.index + htmlOpen[0].length) + `
@@ -1806,14 +1806,55 @@ for (const stream of [process.stdout, process.stderr]) {
1806
1806
  throw error;
1807
1807
  });
1808
1808
  }
1809
+ var live = process.stdout.isTTY === true;
1810
+ var open = false;
1811
+ function endLine() {
1812
+ if (!open) return;
1813
+ process.stdout.write("\n");
1814
+ open = false;
1815
+ }
1809
1816
  function print(line = "") {
1817
+ endLine();
1810
1818
  process.stdout.write(`${line}
1811
1819
  `);
1812
1820
  }
1813
1821
  function warn(line) {
1822
+ endLine();
1814
1823
  process.stderr.write(`${yellow("warning")} ${line}
1815
1824
  `);
1816
1825
  }
1826
+ var silentProgress = { update() {
1827
+ }, done() {
1828
+ } };
1829
+ function progress() {
1830
+ let last = 0;
1831
+ let latest = "";
1832
+ const show = (line) => {
1833
+ if (live) {
1834
+ process.stdout.write(`\r\x1B[2K${dim(line)}`);
1835
+ open = true;
1836
+ } else process.stdout.write(`${dim(line)}
1837
+ `);
1838
+ };
1839
+ return {
1840
+ update(line) {
1841
+ latest = line;
1842
+ const now = Date.now();
1843
+ if (now - last < (live ? 100 : 5e3)) return;
1844
+ last = now;
1845
+ show(line);
1846
+ },
1847
+ done(line = latest) {
1848
+ if (!line) return;
1849
+ if (live) {
1850
+ process.stdout.write(`\r\x1B[2K${dim(line)}
1851
+ `);
1852
+ open = false;
1853
+ } else process.stdout.write(`${dim(line)}
1854
+ `);
1855
+ }
1856
+ };
1857
+ }
1817
1858
 
1818
1859
  // src/cli/commands/build.ts
1819
1860
  function pack(out) {
@@ -2505,8 +2546,8 @@ function printUrls(paths, url) {
2505
2546
  async function waitForServer(paths, timeoutMs) {
2506
2547
  const until = Date.now() + timeoutMs;
2507
2548
  while (Date.now() < until) {
2508
- const live = await liveServer(paths);
2509
- if (live) return live;
2549
+ const live2 = await liveServer(paths);
2550
+ if (live2) return live2;
2510
2551
  await new Promise((resolve) => setTimeout(resolve, 150));
2511
2552
  }
2512
2553
  return null;
@@ -2533,12 +2574,12 @@ async function stopServer(paths) {
2533
2574
  return true;
2534
2575
  }
2535
2576
  async function ensureServer(paths, options) {
2536
- const live = await liveServer(paths);
2537
- const stale = live && live.version !== packageVersion();
2538
- if (live && !options.restart && !stale) return live;
2577
+ const live2 = await liveServer(paths);
2578
+ const stale = live2 && live2.version !== packageVersion();
2579
+ if (live2 && !options.restart && !stale) return live2;
2539
2580
  if (stale)
2540
- print(dim(`Restarting the preview server: it runs foss-design ${live.version}, this is ${packageVersion()}.`));
2541
- if (live) await stopServer(paths);
2581
+ print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
2582
+ if (live2) await stopServer(paths);
2542
2583
  fs16.mkdirSync(paths.cache, { recursive: true });
2543
2584
  const log = fs16.openSync(logFile(paths), "w");
2544
2585
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
@@ -2866,6 +2907,7 @@ function errorFor(host, status, body, retryAfter) {
2866
2907
  if (status === 401)
2867
2908
  return new CloudError(`Not signed in to ${host}, or the token was revoked. Run \`design login\`.`, status, code);
2868
2909
  let message = text(data.message) ?? DEFAULT_MESSAGE[status] ?? `${host} answered ${status}`;
2910
+ if (status === 413 && !str(data.error)) message = `A proxy in front of ${host} refused it as too large (HTTP 413)`;
2869
2911
  if (code === "account_banned") message = `Your account is banned: ${message}`;
2870
2912
  if (limit) message += ` (limit: ${limit})`;
2871
2913
  if (status === 429 && retryAfter) message += `; retry in ${retryAfter}s`;
@@ -3222,13 +3264,26 @@ function allUnitKeys(link, local, remote) {
3222
3264
  }
3223
3265
 
3224
3266
  // src/cloud/sync.ts
3225
- function openSync(paths, log) {
3267
+ function openSync(paths, log, progress2 = () => silentProgress) {
3226
3268
  const link = requireLink(paths);
3227
3269
  const credential = requireCredential(link.host);
3228
- return { paths, link, client: new CloudClient(link.host, credential.token), log };
3270
+ return { paths, link, client: new CloudClient(link.host, credential.token), log, progress: progress2 };
3271
+ }
3272
+ var TRANSFERS = 8;
3273
+ var megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
3274
+ function rate(bytes, started) {
3275
+ const elapsed = (Date.now() - started) / 1e3;
3276
+ return elapsed < 1 ? "" : ` \xB7 ${(bytes / 1024 / 1024 / elapsed).toFixed(1)} MB/s`;
3277
+ }
3278
+ var seconds = (started) => `${((Date.now() - started) / 1e3).toFixed(1)}s`;
3279
+ function webUrl(link, unit) {
3280
+ const base = `${link.host.replace(/\/+$/, "")}/p/${encodeURIComponent(link.project)}`;
3281
+ if (!unit) return base;
3282
+ if (unit === SYSTEM_UNIT) return `${base}/system`;
3283
+ return `${base}/c/${encodeURIComponent(unit.slice("canvas/".length))}`;
3229
3284
  }
3230
3285
  function newReport(command, link) {
3231
- return { ok: true, command, host: link.host, project: link.project, units: [], hints: [] };
3286
+ return { ok: true, command, host: link.host, project: link.project, url: webUrl(link), units: [], hints: [] };
3232
3287
  }
3233
3288
  function unitArg(value) {
3234
3289
  if (value === SYSTEM_UNIT || value === "@system") return SYSTEM_UNIT;
@@ -3953,7 +4008,7 @@ function applyUnit(paths, key, local, target, blobs) {
3953
4008
  return counts;
3954
4009
  }
3955
4010
  async function pull(ctx, options) {
3956
- const { paths, client, log } = ctx;
4011
+ const { paths, client } = ctx;
3957
4012
  const link = structuredClone(ctx.link);
3958
4013
  const report = newReport("pull", link);
3959
4014
  const selected = unique(options.units.map(unitArg));
@@ -4056,10 +4111,20 @@ async function pull(ctx, options) {
4056
4111
  else download.push(hash);
4057
4112
  }
4058
4113
  if (download.length) {
4059
- log(`Downloading ${plural2(download.length, "file")}\u2026`);
4060
- await mapLimit(download, 4, async (hash) => {
4061
- blobs.set(hash, await client.getBlob(link.project, hash));
4114
+ const bar = ctx.progress();
4115
+ const started = Date.now();
4116
+ let gotFiles = 0;
4117
+ let gotBytes = 0;
4118
+ const line = () => `Downloading ${gotFiles}/${download.length} files \xB7 ${megabytes(gotBytes)}${rate(gotBytes, started)}`;
4119
+ bar.update(line());
4120
+ await mapLimit(download, TRANSFERS, async (hash) => {
4121
+ const data = await client.getBlob(link.project, hash);
4122
+ blobs.set(hash, data);
4123
+ gotFiles++;
4124
+ gotBytes += data.length;
4125
+ bar.update(line());
4062
4126
  });
4127
+ bar.done(`Downloaded ${plural2(download.length, "file")} (${megabytes(gotBytes)}) in ${seconds(started)}`);
4063
4128
  }
4064
4129
  for (const plan of plans) {
4065
4130
  const { key, report: entry } = plan;
@@ -4095,7 +4160,6 @@ async function pull(ctx, options) {
4095
4160
  // src/cloud/push.ts
4096
4161
  import fs24 from "fs";
4097
4162
  import path24 from "path";
4098
- var megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
4099
4163
  function blockedReport(status, message) {
4100
4164
  return {
4101
4165
  unit: status.key,
@@ -4234,7 +4298,7 @@ async function push(ctx, options) {
4234
4298
  const pushing = toPush.filter((key) => {
4235
4299
  const s = status(key);
4236
4300
  if (systemPush || resolved.includes(key) || s.changed.length || !remote.has(key)) return true;
4237
- report.units.push({ unit: key, action: "unchanged", rev: s.baseRev });
4301
+ report.units.push({ unit: key, action: "unchanged", rev: s.baseRev, url: webUrl(link, key) });
4238
4302
  return false;
4239
4303
  });
4240
4304
  if (!pushing.length) return report;
@@ -4277,17 +4341,40 @@ async function push(ctx, options) {
4277
4341
  const unknown = missing.find((hash) => !files.has(hash));
4278
4342
  if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
4279
4343
  const bytes = missing.reduce((sum, hash) => sum + fs24.statSync(files.get(hash)).size, 0);
4280
- log(`Uploading ${plural2(missing.length, "file")} (${megabytes(bytes)})\u2026`);
4281
- await mapLimit(missing, 4, async (hash) => {
4282
- await client.putBlob(link.project, hash, fs24.readFileSync(files.get(hash)));
4344
+ const bar = ctx.progress();
4345
+ const started = Date.now();
4346
+ let sentFiles = 0;
4347
+ let sentBytes = 0;
4348
+ const line = () => `Uploading ${sentFiles}/${missing.length} files \xB7 ${megabytes(sentBytes)} of ${megabytes(bytes)}${rate(sentBytes, started)}`;
4349
+ bar.update(line());
4350
+ await mapLimit(missing, TRANSFERS, async (hash) => {
4351
+ const file = files.get(hash);
4352
+ const data = fs24.readFileSync(file);
4353
+ try {
4354
+ await client.putBlob(link.project, hash, data);
4355
+ } catch (error) {
4356
+ if (error instanceof CloudError && error.status === 413)
4357
+ throw new CliError(`${path24.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4358
+ throw error;
4359
+ }
4360
+ sentFiles++;
4361
+ sentBytes += data.length;
4362
+ bar.update(line());
4283
4363
  });
4364
+ bar.done(`Uploaded ${plural2(missing.length, "file")} (${megabytes(bytes)}) in ${seconds(started)}`);
4284
4365
  }
4285
4366
  log(`Pushing ${plural2(units.length, "unit")}\u2026`);
4286
4367
  const revs2 = await client.push(link.project, units);
4287
4368
  for (const unit of units) {
4288
4369
  const rev = revs2.get(unit.key) ?? unit.baseRev + 1;
4289
4370
  link.units[unit.key] = { rev, files: hashes(unit.source) };
4290
- report.units.push({ unit: unit.key, action: unit.baseRev ? "pushed" : "created", rev, baseRev: unit.baseRev });
4371
+ report.units.push({
4372
+ unit: unit.key,
4373
+ action: unit.baseRev ? "pushed" : "created",
4374
+ rev,
4375
+ baseRev: unit.baseRev,
4376
+ url: webUrl(link, unit.key)
4377
+ });
4291
4378
  }
4292
4379
  for (const key of resolved) fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4293
4380
  writeLink(paths, link);
@@ -4312,8 +4399,8 @@ function printReport(report) {
4312
4399
  const shown = report.units.filter((entry) => !QUIET.has(entry.action));
4313
4400
  const quiet = report.units.length - shown.length;
4314
4401
  const moved = report.units.some((entry) => GOOD.has(entry.action));
4315
- if (report.ok && !moved) print(`${bold(`Nothing to ${report.command}`)} ${dim(`${report.project} \xB7 ${report.host}`)}`);
4316
- else if (report.ok) print(`${bold(verb)} ${report.project} ${dim(report.host)}`);
4402
+ if (report.ok && !moved) print(`${bold(`Nothing to ${report.command}`)} ${dim(report.url)}`);
4403
+ else if (report.ok) print(`${bold(verb)} ${report.url}`);
4317
4404
  else print(`${bold(report.command === "push" ? "Push stopped" : "Pull finished with conflicts")} ${dim(report.host)}`);
4318
4405
  const width = Math.max(0, ...shown.map((entry) => entry.unit.length));
4319
4406
  for (const entry of shown) {
@@ -4327,6 +4414,7 @@ function printReport(report) {
4327
4414
  ) : "";
4328
4415
  const detail2 = [entry.action.replaceAll("_", " "), revs(entry), files].filter(Boolean).join(" ");
4329
4416
  print(` ${mark} ${entry.unit.padEnd(width)} ${detail2}`);
4417
+ if (entry.url && GOOD.has(entry.action)) print(` ${dim("\u2192")} ${entry.url}`);
4330
4418
  if (entry.message) print(` ${dim(entry.message)}`);
4331
4419
  }
4332
4420
  if (quiet) print(dim(` ${plural2(quiet, "unit")} ${report.command === "push" ? "unchanged" : "up to date"}`));
@@ -4335,8 +4423,8 @@ function printReport(report) {
4335
4423
  async function runSync(paths, command, json, run) {
4336
4424
  let report;
4337
4425
  try {
4338
- report = await run(openSync(paths, json ? () => {
4339
- } : (line) => print(dim(line))));
4426
+ report = await run(json ? openSync(paths, () => {
4427
+ }) : openSync(paths, (line) => print(dim(line)), progress));
4340
4428
  } catch (error) {
4341
4429
  if (!json) throw error;
4342
4430
  let link = null;
@@ -4350,6 +4438,7 @@ async function runSync(paths, command, json, run) {
4350
4438
  command,
4351
4439
  host: link?.host ?? "",
4352
4440
  project: link?.project ?? "",
4441
+ url: link ? webUrl(link) : "",
4353
4442
  units: [],
4354
4443
  hints: [],
4355
4444
  error: {
@@ -4416,7 +4505,8 @@ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>;
4416
4505
  (linking to another project replaces the link)
4417
4506
  push [canvas\u2026] [--json] Build and upload what changed here (or the named units); a
4418
4507
  [--resolved <unit>] system change pushes every active canvas with it. Stops if
4419
- the cloud is ahead; --resolved marks a merged conflict
4508
+ the cloud is ahead; --resolved marks a merged conflict.
4509
+ Prints the web link of every pushed canvas
4420
4510
  pull [canvas\u2026] [--json] Take cloud changes into .design; a unit changed on both sides
4421
4511
  [--theirs <unit>] goes to .design/.cache/cloud/incoming/<unit> (exit 2);
4422
4512
  --theirs takes the cloud version and drops local changes
@@ -4530,6 +4620,7 @@ async function main(argv) {
4530
4620
  }
4531
4621
  }
4532
4622
  main(process.argv.slice(2)).catch((error) => {
4623
+ endLine();
4533
4624
  if (error instanceof CliError) {
4534
4625
  process.stderr.write(`${red("error")} ${error.message}
4535
4626
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foss-design",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Design canvases and design systems for coding agents: live, animated, clickable screens on an infinite canvas, served locally from a .design folder.",
5
5
  "type": "module",
6
6
  "license": "MIT",