foss-design 0.9.5 → 0.9.7

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/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import path34 from "path";
4
+ import { spawnSync as spawnSync3 } from "child_process";
5
+ import fs37 from "fs";
6
+ import path35 from "path";
5
7
  import { parseArgs } from "util";
6
8
 
7
9
  // src/core/paths.ts
@@ -346,8 +348,8 @@ function routeError(pattern) {
346
348
  }
347
349
  return null;
348
350
  }
349
- function split(path35) {
350
- return path35.split("/").filter(Boolean);
351
+ function split(path36) {
352
+ return path36.split("/").filter(Boolean);
351
353
  }
352
354
  function compileRoute(pattern) {
353
355
  return {
@@ -366,8 +368,8 @@ function decode2(part) {
366
368
  return part;
367
369
  }
368
370
  }
369
- function scoreRoute(route2, path35) {
370
- const parts = split(path35.split(/[?#]/)[0] ?? "").map(decode2);
371
+ function scoreRoute(route2, path36) {
372
+ const parts = split(path36.split(/[?#]/)[0] ?? "").map(decode2);
371
373
  const walk2 = (s, p) => {
372
374
  if (s === route2.segments.length) return p === parts.length ? 0 : null;
373
375
  const segment = route2.segments[s];
@@ -385,11 +387,11 @@ function scoreRoute(route2, path35) {
385
387
  };
386
388
  return walk2(0, 0);
387
389
  }
388
- function matchRoute(entries, path35) {
390
+ function matchRoute(entries, path36) {
389
391
  let best = null;
390
392
  for (const entry of entries) {
391
393
  for (const route2 of entry.routes) {
392
- const score = scoreRoute(route2, path35);
394
+ const score = scoreRoute(route2, path36);
393
395
  if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
394
396
  }
395
397
  }
@@ -516,9 +518,9 @@ var CanvasSchema = z.strictObject({
516
518
  pages: z.array(z.unknown()).min(1, "add at least one page")
517
519
  });
518
520
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
519
- function formatPath(base, path35) {
521
+ function formatPath(base, path36) {
520
522
  let out = base;
521
- for (const part of path35) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
523
+ for (const part of path36) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
522
524
  return out;
523
525
  }
524
526
  function describeZodError(error, base = "") {
@@ -2632,7 +2634,7 @@ var CloudClient = class {
2632
2634
  }
2633
2635
  host;
2634
2636
  token;
2635
- async send(method, path35, init) {
2637
+ async send(method, path36, init) {
2636
2638
  const headers = {
2637
2639
  "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2638
2640
  Accept: "application/json"
@@ -2640,7 +2642,7 @@ var CloudClient = class {
2640
2642
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
2641
2643
  if (init.type) headers["Content-Type"] = init.type;
2642
2644
  try {
2643
- return await fetch(`${this.host}/api${path35}`, {
2645
+ return await fetch(`${this.host}/api${path36}`, {
2644
2646
  method,
2645
2647
  headers,
2646
2648
  body: init.body,
@@ -2661,8 +2663,8 @@ var CloudClient = class {
2661
2663
  throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2662
2664
  }
2663
2665
  /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2664
- async request(method, path35, body) {
2665
- const response = await this.send(method, path35, {
2666
+ async request(method, path36, body) {
2667
+ const response = await this.send(method, path36, {
2666
2668
  body: body === void 0 ? void 0 : JSON.stringify(body),
2667
2669
  type: body === void 0 ? void 0 : "application/json",
2668
2670
  timeout: JSON_TIMEOUT
@@ -2674,7 +2676,7 @@ var CloudClient = class {
2674
2676
  try {
2675
2677
  return JSON.parse(text2);
2676
2678
  } catch {
2677
- throw new CloudError(`${this.host} answered ${method} ${path35} with something other than JSON`, 0, "bad_response");
2679
+ throw new CloudError(`${this.host} answered ${method} ${path36} with something other than JSON`, 0, "bad_response");
2678
2680
  }
2679
2681
  }
2680
2682
  /** POST /device/code. */
@@ -3027,7 +3029,8 @@ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ?
3027
3029
  var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3028
3030
 
3029
3031
  // src/capture/cover.ts
3030
- import fs19 from "fs";
3032
+ import fs20 from "fs";
3033
+ import os3 from "os";
3031
3034
 
3032
3035
  // src/server/snapshots.ts
3033
3036
  import fs17 from "fs";
@@ -3085,11 +3088,15 @@ var SnapshotStore = class {
3085
3088
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3086
3089
  const dir = path17.join(this.dir, canvas);
3087
3090
  fs17.mkdirSync(dir, { recursive: true });
3088
- fs17.writeFileSync(path17.join(dir, `${id2}.${theme2}.png`), png);
3091
+ const file = path17.join(dir, `${id2}.${theme2}.png`);
3092
+ fs17.writeFileSync(`${file}.${process.pid}.tmp`, png);
3093
+ fs17.renameSync(`${file}.${process.pid}.tmp`, file);
3089
3094
  const data = this.load(canvas);
3090
3095
  const version = Date.now();
3091
3096
  data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
3092
- fs17.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
3097
+ const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3098
+ fs17.writeFileSync(temp, JSON.stringify(data));
3099
+ fs17.renameSync(temp, this.metaFile(canvas));
3093
3100
  return { url: this.url(canvas, id2, theme2, version) };
3094
3101
  }
3095
3102
  /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
@@ -3102,8 +3109,54 @@ var SnapshotStore = class {
3102
3109
  }
3103
3110
  };
3104
3111
 
3105
- // src/capture/chrome.ts
3112
+ // src/capture/build-server.ts
3106
3113
  import fs18 from "fs";
3114
+ import http from "http";
3115
+ import path18 from "path";
3116
+ import sirv from "sirv";
3117
+ async function serveBuild(paths, name, options) {
3118
+ const out = path18.join(paths.cache, name);
3119
+ fs18.rmSync(out, { recursive: true, force: true });
3120
+ await buildSite(paths, out, {
3121
+ canvases: options.canvases,
3122
+ includeSystem: options.includeSystem,
3123
+ includeViewer: false
3124
+ });
3125
+ const files = sirv(out, { dev: true });
3126
+ const server = http.createServer((req, res) => {
3127
+ const url = new URL(req.url ?? "/", "http://localhost");
3128
+ const segments = url.pathname.split("/").map((segment) => {
3129
+ try {
3130
+ return decodeURIComponent(segment);
3131
+ } catch {
3132
+ return segment;
3133
+ }
3134
+ });
3135
+ req.url = segments.join("/") + url.search;
3136
+ files(req, res, () => {
3137
+ res.statusCode = 404;
3138
+ res.end("Not found");
3139
+ });
3140
+ });
3141
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
3142
+ return {
3143
+ base: `http://127.0.0.1:${server.address().port}`,
3144
+ read(rel) {
3145
+ try {
3146
+ return JSON.parse(fs18.readFileSync(path18.join(out, rel), "utf8"));
3147
+ } catch {
3148
+ return null;
3149
+ }
3150
+ },
3151
+ close: () => new Promise((resolve) => {
3152
+ server.closeAllConnections();
3153
+ server.close(() => resolve());
3154
+ })
3155
+ };
3156
+ }
3157
+
3158
+ // src/capture/chrome.ts
3159
+ import fs19 from "fs";
3107
3160
  var CANDIDATES = {
3108
3161
  darwin: [
3109
3162
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -3127,8 +3180,8 @@ var CANDIDATES = {
3127
3180
  };
3128
3181
  function findChrome() {
3129
3182
  const configured = process.env.DESIGN_CHROME;
3130
- if (configured && fs18.existsSync(configured)) return configured;
3131
- for (const candidate of CANDIDATES[process.platform] ?? []) if (fs18.existsSync(candidate)) return candidate;
3183
+ if (configured && fs19.existsSync(configured)) return configured;
3184
+ for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
3132
3185
  return null;
3133
3186
  }
3134
3187
  async function launchChrome() {
@@ -3159,6 +3212,8 @@ async function openFrame(page, url, options = {}) {
3159
3212
  record2(text2);
3160
3213
  });
3161
3214
  let ready = false;
3215
+ let loadedAt = 0;
3216
+ let readyMs;
3162
3217
  let reloads = 0;
3163
3218
  const onNavigate = () => {
3164
3219
  reloads++;
@@ -3171,6 +3226,7 @@ async function openFrame(page, url, options = {}) {
3171
3226
  outdated = () => resolve("outdated");
3172
3227
  });
3173
3228
  await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
3229
+ loadedAt = Date.now();
3174
3230
  page.on("load", onNavigate);
3175
3231
  if (!ready) {
3176
3232
  const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
@@ -3179,6 +3235,7 @@ async function openFrame(page, url, options = {}) {
3179
3235
  signal.catch(() => {
3180
3236
  });
3181
3237
  ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
3238
+ if (ready) readyMs = Date.now() - loadedAt;
3182
3239
  }
3183
3240
  if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
3184
3241
  page.off("load", onNavigate);
@@ -3196,7 +3253,12 @@ async function openFrame(page, url, options = {}) {
3196
3253
  const unique2 = errors.filter(
3197
3254
  (error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
3198
3255
  );
3199
- return { errors: [...new Set(unique2)], ready, ...held ? { held } : {} };
3256
+ return {
3257
+ errors: [...new Set(unique2)],
3258
+ ready,
3259
+ ...held ? { held } : {},
3260
+ ...readyMs !== void 0 ? { readyMs } : {}
3261
+ };
3200
3262
  }
3201
3263
 
3202
3264
  // src/capture/frames.ts
@@ -3273,121 +3335,168 @@ async function mapLimit(items, limit, fn) {
3273
3335
 
3274
3336
  // src/capture/cover.ts
3275
3337
  var SNAPSHOT_WIDTH = 900;
3276
- async function refreshCovers(paths, canvasIds, base) {
3338
+ var TABS = Math.min(8, Math.max(2, os3.availableParallelism() - 1));
3339
+ var THEMES2 = ["light", "dark"];
3340
+ function staleSnapshots(paths, canvasIds, filter = {}) {
3277
3341
  const store = new SnapshotStore(paths.snapshots);
3278
3342
  const project = new DesignProject(paths, DEV_URLS, store.lookup);
3279
3343
  const stale = [];
3280
3344
  for (const id2 of canvasIds) {
3281
3345
  const canvas = project.canvas(id2);
3282
- if (!canvas || canvas.doc.cover && "url" in canvas.doc.cover) continue;
3283
- const screen = canvas.doc.cover?.screen ?? canvas.screens[0]?.id;
3284
- const source = canvas.screens.find((item) => item.id === screen);
3285
- if (!screen || !source || !fs19.existsSync(source.file)) continue;
3286
- const taken = store.version(id2, screen, "light");
3287
- if (!taken || taken < fs19.statSync(source.file).mtimeMs) stale.push({ canvas: id2, screen });
3288
- }
3289
- if (!stale.length) return [];
3290
- if (!findChrome()) return ["No Chrome here, so canvas covers keep their last snapshot."];
3291
- const url = await base();
3346
+ if (!canvas) continue;
3347
+ const inLight = canvasFrames(canvas.doc, "", "light", { page: filter.page, includeUrls: false });
3348
+ const inDark = new Map(
3349
+ canvasFrames(canvas.doc, "", "dark", { page: filter.page, includeUrls: false }).map((job) => [job.id, job.theme])
3350
+ );
3351
+ const files = new Map(canvas.screens.map((screen) => [screen.id, screen.file]));
3352
+ for (const job of inLight) {
3353
+ if (filter.screens && !filter.screens.has(job.id)) continue;
3354
+ const file = files.get(job.id);
3355
+ if (!file || !fs20.existsSync(file)) continue;
3356
+ const changed = fs20.statSync(file).mtimeMs;
3357
+ const pinned = inDark.get(job.id) === job.theme;
3358
+ const themes = pinned ? [job.theme] : THEMES2;
3359
+ const screen = { id: job.id };
3360
+ for (const theme2 of themes) {
3361
+ const taken = store.version(id2, screen.id, theme2);
3362
+ if (!taken || taken < changed) stale.push({ canvas: id2, screen: screen.id, theme: theme2 });
3363
+ }
3364
+ }
3365
+ }
3366
+ return stale;
3367
+ }
3368
+ async function takeSnapshots(paths, targets, preview, progress2) {
3369
+ if (!targets.length) return [];
3370
+ if (!findChrome()) return ["No Chrome here, so screens keep the snapshots they have."];
3371
+ const store = new SnapshotStore(paths.snapshots);
3372
+ const project = new DesignProject(paths, DEV_URLS, store.lookup);
3373
+ const canvases = [...new Set(targets.map((target) => target.canvas))];
3374
+ progress2.update(`Building ${canvases.join(", ")} to take snapshots from\u2026`);
3375
+ let url;
3376
+ let docOf = (id2) => project.canvas(id2).doc;
3377
+ let stop = async () => {
3378
+ };
3379
+ try {
3380
+ const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false });
3381
+ url = build2.base;
3382
+ docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
3383
+ stop = build2.close;
3384
+ } catch {
3385
+ url = await preview();
3386
+ }
3292
3387
  const browser = await launchChrome();
3293
- const lines = [];
3388
+ const failed = [];
3389
+ const started = Date.now();
3390
+ let done = 0;
3391
+ const line = () => `Snapshots ${done}/${targets.length} (both themes, as the viewer and the cloud show them)`;
3392
+ progress2.update(line());
3294
3393
  try {
3295
- for (const { canvas, screen } of stale) {
3296
- const doc = project.canvas(canvas).doc;
3297
- const job = canvasFrames(doc, url, "light", { id: screen })[0];
3298
- if (!job) continue;
3299
- const page = await browser.newPage({
3300
- viewport: { width: job.width, height: job.height },
3301
- deviceScaleFactor: Math.min(1, SNAPSHOT_WIDTH / job.width)
3302
- });
3303
- try {
3304
- const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
3305
- if (!report.ready) {
3306
- lines.push(`${canvas}: ${screen} did not finish rendering, so its cover keeps the last snapshot`);
3307
- continue;
3308
- }
3309
- const png = await page.screenshot({ fullPage: job.fullPage });
3310
- const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
3311
- store.save(canvas, screen, "light", png, height);
3312
- } finally {
3313
- await page.close().catch(() => {
3394
+ await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2 }) => {
3395
+ const job = canvasFrames(docOf(canvas), url, theme2, { id: screen })[0];
3396
+ if (job) {
3397
+ const page = await browser.newPage({
3398
+ viewport: { width: job.width, height: job.height },
3399
+ deviceScaleFactor: Math.min(1, SNAPSHOT_WIDTH / job.width),
3400
+ colorScheme: theme2
3314
3401
  });
3402
+ try {
3403
+ const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
3404
+ if (report.ready) {
3405
+ const png = await page.screenshot({ fullPage: job.fullPage });
3406
+ const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
3407
+ store.save(canvas, screen, theme2, png, height);
3408
+ } else failed.push(`${canvas}/${screen} (${theme2})`);
3409
+ } catch {
3410
+ failed.push(`${canvas}/${screen} (${theme2})`);
3411
+ } finally {
3412
+ await page.close().catch(() => {
3413
+ });
3414
+ }
3315
3415
  }
3316
- }
3416
+ done++;
3417
+ progress2.update(line());
3418
+ });
3317
3419
  } finally {
3318
3420
  await browser.close();
3319
- }
3320
- return lines;
3421
+ await stop();
3422
+ }
3423
+ const seconds2 = Math.round((Date.now() - started) / 1e3);
3424
+ progress2.done(`Took ${targets.length - failed.length} snapshot${targets.length === 1 ? "" : "s"} in ${seconds2}s`);
3425
+ if (!failed.length) return [];
3426
+ const shown = failed.slice(0, 5).join(", ");
3427
+ return [
3428
+ `${failed.length} screen${failed.length === 1 ? "" : "s"} did not finish rendering, so they keep their last snapshot: ${shown}${failed.length > 5 ? ", \u2026" : ""}. \`design check --render\` says why.`
3429
+ ];
3321
3430
  }
3322
3431
 
3323
3432
  // src/cloud/pull.ts
3324
- import fs23 from "fs";
3325
- import path21 from "path";
3433
+ import fs24 from "fs";
3434
+ import path22 from "path";
3326
3435
 
3327
3436
  // src/cloud/merge.ts
3328
- import fs21 from "fs";
3329
- import path19 from "path";
3437
+ import fs22 from "fs";
3438
+ import path20 from "path";
3330
3439
  import { merge as diff3 } from "node-diff3";
3331
3440
 
3332
3441
  // src/cloud/write.ts
3333
- import fs20 from "fs";
3334
- import path18 from "path";
3442
+ import fs21 from "fs";
3443
+ import path19 from "path";
3335
3444
  function writeInside(root, rel, data) {
3336
- const file = path18.join(root, rel);
3445
+ const file = path19.join(root, rel);
3337
3446
  if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
3338
- fs20.mkdirSync(path18.dirname(file), { recursive: true });
3339
- if (!isInside(fs20.realpathSync(root), fs20.realpathSync(path18.dirname(file))))
3447
+ fs21.mkdirSync(path19.dirname(file), { recursive: true });
3448
+ if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path19.dirname(file))))
3340
3449
  throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
3341
- if (fs20.existsSync(file) && fs20.lstatSync(file).isSymbolicLink()) fs20.unlinkSync(file);
3342
- fs20.writeFileSync(file, data);
3450
+ if (fs21.existsSync(file) && fs21.lstatSync(file).isSymbolicLink()) fs21.unlinkSync(file);
3451
+ fs21.writeFileSync(file, data);
3343
3452
  }
3344
3453
  function pruneEmpty(dir, stop) {
3345
3454
  let current = dir;
3346
3455
  while (isInside(stop, current) && current !== stop) {
3347
3456
  try {
3348
- if (fs20.readdirSync(current).length) return;
3349
- fs20.rmdirSync(current);
3457
+ if (fs21.readdirSync(current).length) return;
3458
+ fs21.rmdirSync(current);
3350
3459
  } catch {
3351
3460
  return;
3352
3461
  }
3353
- current = path18.dirname(current);
3462
+ current = path19.dirname(current);
3354
3463
  }
3355
3464
  }
3356
3465
 
3357
3466
  // src/cloud/merge.ts
3358
3467
  var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
3359
3468
  var isText = (rel, ...data) => TEXT.test(rel) && data.every((buffer) => !buffer || !buffer.subarray(0, 8192).includes(0));
3360
- var mergeDir = (paths, key) => path19.join(cloudCache(paths), "merge", ...key.split("/"));
3361
- var stateFile = (paths, key) => path19.join(mergeDir(paths, key), "state.json");
3362
- var sideFile = (paths, key, rel, side) => path19.join(mergeDir(paths, key), side, rel);
3469
+ var mergeDir = (paths, key) => path20.join(cloudCache(paths), "merge", ...key.split("/"));
3470
+ var stateFile = (paths, key) => path20.join(mergeDir(paths, key), "state.json");
3471
+ var sideFile = (paths, key, rel, side) => path20.join(mergeDir(paths, key), side, rel);
3363
3472
  function readMergeState(paths, key) {
3364
3473
  try {
3365
- return JSON.parse(fs21.readFileSync(stateFile(paths, key), "utf8"));
3474
+ return JSON.parse(fs22.readFileSync(stateFile(paths, key), "utf8"));
3366
3475
  } catch {
3367
3476
  return null;
3368
3477
  }
3369
3478
  }
3370
3479
  function saveState(paths, state) {
3371
- fs21.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
3372
- fs21.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
3480
+ fs22.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
3481
+ fs22.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
3373
3482
  `);
3374
3483
  }
3375
3484
  function mergingUnits(paths) {
3376
- const root = path19.join(cloudCache(paths), "merge");
3485
+ const root = path20.join(cloudCache(paths), "merge");
3377
3486
  const found = [];
3378
3487
  const visit = (dir, prefix) => {
3379
- if (fs21.existsSync(path19.join(dir, "state.json"))) {
3488
+ if (fs22.existsSync(path20.join(dir, "state.json"))) {
3380
3489
  const state = readMergeState(paths, prefix);
3381
3490
  if (state) found.push(state);
3382
3491
  return;
3383
3492
  }
3384
3493
  let entries = [];
3385
3494
  try {
3386
- entries = fs21.readdirSync(dir, { withFileTypes: true });
3495
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
3387
3496
  } catch {
3388
3497
  }
3389
3498
  for (const entry of entries)
3390
- if (entry.isDirectory()) visit(path19.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3499
+ if (entry.isDirectory()) visit(path20.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3391
3500
  };
3392
3501
  visit(root, "");
3393
3502
  return found;
@@ -3397,7 +3506,7 @@ function openConflicts(paths, state) {
3397
3506
  return state.conflicts.filter((conflict) => {
3398
3507
  if (conflict.kind !== "text") return true;
3399
3508
  try {
3400
- return MARKER.test(fs21.readFileSync(path19.join(paths.design, conflict.path), "utf8"));
3509
+ return MARKER.test(fs22.readFileSync(path20.join(paths.design, conflict.path), "utf8"));
3401
3510
  } catch {
3402
3511
  return false;
3403
3512
  }
@@ -3406,19 +3515,19 @@ function openConflicts(paths, state) {
3406
3515
  function keep(paths, key, rel, side, data) {
3407
3516
  if (!data) return;
3408
3517
  const file = sideFile(paths, key, rel, side);
3409
- fs21.mkdirSync(path19.dirname(file), { recursive: true });
3410
- fs21.writeFileSync(file, data);
3518
+ fs22.mkdirSync(path20.dirname(file), { recursive: true });
3519
+ fs22.writeFileSync(file, data);
3411
3520
  }
3412
3521
  function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
3413
3522
  const files = { added: 0, changed: 0, removed: 0, merged: 0 };
3414
3523
  const conflicts = [];
3415
- const stop = key === SYSTEM_UNIT ? paths.design : path19.dirname(unitRoot(paths, key));
3416
- fs21.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3417
- const read = (rel) => fs21.readFileSync(path19.join(paths.design, rel));
3524
+ const stop = key === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, key));
3525
+ fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3526
+ const read = (rel) => fs22.readFileSync(path20.join(paths.design, rel));
3418
3527
  const remove = (rel) => {
3419
- const file = path19.join(paths.design, rel);
3420
- fs21.rmSync(file, { force: true });
3421
- pruneEmpty(path19.dirname(file), stop);
3528
+ const file = path20.join(paths.design, rel);
3529
+ fs22.rmSync(file, { force: true });
3530
+ pruneEmpty(path20.dirname(file), stop);
3422
3531
  };
3423
3532
  for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
3424
3533
  const b = base[rel];
@@ -3474,11 +3583,11 @@ function resolveConflict(paths, state, rel, side) {
3474
3583
  const conflict = state.conflicts.find((item) => item.path === rel);
3475
3584
  if (!conflict) return;
3476
3585
  const kept = sideFile(paths, state.unit, rel, side);
3477
- const stop = state.unit === SYSTEM_UNIT ? paths.design : path19.dirname(unitRoot(paths, state.unit));
3478
- if (fs21.existsSync(kept)) writeInside(paths.design, rel, fs21.readFileSync(kept));
3586
+ const stop = state.unit === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, state.unit));
3587
+ if (fs22.existsSync(kept)) writeInside(paths.design, rel, fs22.readFileSync(kept));
3479
3588
  else {
3480
- fs21.rmSync(path19.join(paths.design, rel), { force: true });
3481
- pruneEmpty(path19.dirname(path19.join(paths.design, rel)), stop);
3589
+ fs22.rmSync(path20.join(paths.design, rel), { force: true });
3590
+ pruneEmpty(path20.dirname(path20.join(paths.design, rel)), stop);
3482
3591
  }
3483
3592
  state.conflicts = state.conflicts.filter((item) => item.path !== rel);
3484
3593
  saveState(paths, state);
@@ -3488,15 +3597,15 @@ function finishMerge(paths, link, key, accept = false) {
3488
3597
  if (state) {
3489
3598
  const open2 = openConflicts(paths, state).filter((conflict) => !(accept && conflict.kind !== "text"));
3490
3599
  if (open2.length) return open2;
3491
- fs21.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3600
+ fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3492
3601
  }
3493
3602
  delete link.conflicts[key];
3494
3603
  return [];
3495
3604
  }
3496
3605
 
3497
3606
  // src/cloud/meta.ts
3498
- import fs22 from "fs";
3499
- import path20 from "path";
3607
+ import fs23 from "fs";
3608
+ import path21 from "path";
3500
3609
  async function syncProjectMeta(paths, client, link) {
3501
3610
  const remote = await client.project(link.project);
3502
3611
  if (!remote) return [];
@@ -3515,8 +3624,8 @@ async function syncProjectMeta(paths, client, link) {
3515
3624
  if (localChanged && !cloudChanged) {
3516
3625
  try {
3517
3626
  if (file) {
3518
- const ext = path20.extname(file).slice(1);
3519
- await client.setProjectIcon(link.project, { data: fs22.readFileSync(file), type: ICON_TYPES[ext] });
3627
+ const ext = path21.extname(file).slice(1);
3628
+ await client.setProjectIcon(link.project, { data: fs23.readFileSync(file), type: ICON_TYPES[ext] });
3520
3629
  lines.push("The project icon went up to the cloud.");
3521
3630
  } else {
3522
3631
  await client.setProjectIcon(link.project, null);
@@ -3551,12 +3660,12 @@ function checkManifest(key, manifest) {
3551
3660
  }
3552
3661
  function applyUnit(paths, key, local, target, blobs) {
3553
3662
  const counts = { added: 0, changed: 0, removed: 0 };
3554
- const stop = key === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, key));
3663
+ const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
3555
3664
  for (const rel of Object.keys(local)) {
3556
3665
  if (rel in target) continue;
3557
- const file = path21.join(paths.design, rel);
3558
- fs23.rmSync(file, { force: true });
3559
- pruneEmpty(path21.dirname(file), stop);
3666
+ const file = path22.join(paths.design, rel);
3667
+ fs24.rmSync(file, { force: true });
3668
+ pruneEmpty(path22.dirname(file), stop);
3560
3669
  counts.removed++;
3561
3670
  }
3562
3671
  for (const [rel, entry] of Object.entries(target)) {
@@ -3626,7 +3735,7 @@ async function pull(ctx, options) {
3626
3735
  }
3627
3736
  if (s.conflictRev !== void 0) {
3628
3737
  finishMerge(paths, link, key, true);
3629
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3738
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3630
3739
  }
3631
3740
  report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
3632
3741
  } else if (!s.changed.length) {
@@ -3655,7 +3764,7 @@ async function pull(ctx, options) {
3655
3764
  }
3656
3765
  const byHash = /* @__PURE__ */ new Map();
3657
3766
  for (const unit of scan.units.values())
3658
- for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path21.join(paths.design, rel));
3767
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path22.join(paths.design, rel));
3659
3768
  const needed = /* @__PURE__ */ new Set();
3660
3769
  const bases = /* @__PURE__ */ new Set();
3661
3770
  for (const plan of plans) {
@@ -3676,7 +3785,7 @@ async function pull(ctx, options) {
3676
3785
  const download = [];
3677
3786
  for (const hash of needed) {
3678
3787
  const file = byHash.get(hash);
3679
- if (file) blobs.set(hash, fs23.readFileSync(file));
3788
+ if (file) blobs.set(hash, fs24.readFileSync(file));
3680
3789
  else download.push(hash);
3681
3790
  }
3682
3791
  if (download.length) {
@@ -3712,7 +3821,7 @@ async function pull(ctx, options) {
3712
3821
  if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3713
3822
  else delete link.units[key];
3714
3823
  delete link.conflicts[key];
3715
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3824
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3716
3825
  } else {
3717
3826
  const result = mergeUnit(
3718
3827
  paths,
@@ -3723,7 +3832,7 @@ async function pull(ctx, options) {
3723
3832
  plan.remote.manifest,
3724
3833
  (hash) => blobs.get(hash)
3725
3834
  );
3726
- fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3835
+ fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3727
3836
  link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3728
3837
  entry.rev = plan.remote.headRev;
3729
3838
  entry.files = result.files;
@@ -3753,8 +3862,8 @@ async function pull(ctx, options) {
3753
3862
  }
3754
3863
 
3755
3864
  // src/cloud/push.ts
3756
- import fs24 from "fs";
3757
- import path22 from "path";
3865
+ import fs25 from "fs";
3866
+ import path23 from "path";
3758
3867
  function blockedReport(status, message) {
3759
3868
  return {
3760
3869
  unit: status.key,
@@ -3870,7 +3979,7 @@ async function push(ctx, options) {
3870
3979
  for (const key of toPush) {
3871
3980
  const s = status(key);
3872
3981
  if (s.conflictRev !== void 0) {
3873
- const dir = path22.relative(paths.root, incomingDir(paths, key));
3982
+ const dir = path23.relative(paths.root, incomingDir(paths, key));
3874
3983
  blocked.push(
3875
3984
  blockedReport(
3876
3985
  s,
@@ -3919,7 +4028,7 @@ async function push(ctx, options) {
3919
4028
  const files = /* @__PURE__ */ new Map();
3920
4029
  const addFiles = (manifest, dir) => {
3921
4030
  for (const [rel, entry] of Object.entries(manifest))
3922
- if (!files.has(entry.hash)) files.set(entry.hash, path22.join(dir, rel));
4031
+ if (!files.has(entry.hash)) files.set(entry.hash, path23.join(dir, rel));
3923
4032
  };
3924
4033
  for (const key of pushing) {
3925
4034
  const id2 = unitCanvasId(key);
@@ -3951,7 +4060,7 @@ async function push(ctx, options) {
3951
4060
  if (missing.length) {
3952
4061
  const unknown = missing.find((hash) => !files.has(hash));
3953
4062
  if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
3954
- const bytes = missing.reduce((sum, hash) => sum + fs24.statSync(files.get(hash)).size, 0);
4063
+ const bytes = missing.reduce((sum, hash) => sum + fs25.statSync(files.get(hash)).size, 0);
3955
4064
  const bar = ctx.progress();
3956
4065
  const started = Date.now();
3957
4066
  let sentFiles = 0;
@@ -3960,12 +4069,12 @@ async function push(ctx, options) {
3960
4069
  bar.update(line());
3961
4070
  await mapLimit(missing, TRANSFERS, async (hash) => {
3962
4071
  const file = files.get(hash);
3963
- const data = fs24.readFileSync(file);
4072
+ const data = fs25.readFileSync(file);
3964
4073
  try {
3965
4074
  await client.putBlob(link.project, hash, data);
3966
4075
  } catch (error) {
3967
4076
  if (error instanceof CloudError && error.status === 413)
3968
- throw new CliError(`${path22.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4077
+ throw new CliError(`${path23.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
3969
4078
  throw error;
3970
4079
  }
3971
4080
  sentFiles++;
@@ -3975,7 +4084,18 @@ async function push(ctx, options) {
3975
4084
  bar.done(`Uploaded ${plural(missing.length, "file")} (${megabytes(bytes)}) in ${seconds(started)}`);
3976
4085
  }
3977
4086
  log(`Pushing ${plural(units.length, "unit")}\u2026`);
3978
- const revs2 = await client.push(link.project, units);
4087
+ let revs2;
4088
+ try {
4089
+ revs2 = await client.push(link.project, units);
4090
+ } catch (error) {
4091
+ if (!(error instanceof CloudError) || error.status !== 409 || error.error !== "conflict") throw error;
4092
+ report.ok = false;
4093
+ report.error = {
4094
+ code: "pull_first",
4095
+ message: "Nothing was pushed: someone pushed first. Run `design pull`, then push again."
4096
+ };
4097
+ return report;
4098
+ }
3979
4099
  for (const unit of units) {
3980
4100
  const rev = revs2.get(unit.key) ?? unit.baseRev + 1;
3981
4101
  link.units[unit.key] = { rev, files: hashes(unit.source) };
@@ -3987,7 +4107,7 @@ async function push(ctx, options) {
3987
4107
  url: webUrl(link, unit.key)
3988
4108
  });
3989
4109
  }
3990
- for (const key of resolved) fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4110
+ for (const key of resolved) fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3991
4111
  writeLink(paths, link);
3992
4112
  report.units.sort((a, b) => byUnit(a.unit, b.unit));
3993
4113
  return report;
@@ -3998,7 +4118,7 @@ function refuseLegacyGo(paths, keys) {
3998
4118
  for (const key of keys) {
3999
4119
  if (!key.startsWith("canvas/")) continue;
4000
4120
  const id2 = key.slice("canvas/".length);
4001
- for (const file of canvasCodeFiles(path22.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4121
+ for (const file of canvasCodeFiles(path23.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4002
4122
  for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
4003
4123
  }
4004
4124
  }
@@ -4014,18 +4134,18 @@ Still using go():
4014
4134
 
4015
4135
  // src/cli/commands/preview.ts
4016
4136
  import { spawn } from "child_process";
4017
- import fs28 from "fs";
4137
+ import fs29 from "fs";
4018
4138
 
4019
4139
  // src/server/index.ts
4020
- import fs27 from "fs";
4021
- import http from "http";
4022
- import path25 from "path";
4023
- import sirv from "sirv";
4140
+ import fs28 from "fs";
4141
+ import http2 from "http";
4142
+ import path26 from "path";
4143
+ import sirv2 from "sirv";
4024
4144
  import { createServer, searchForWorkspaceRoot } from "vite";
4025
4145
 
4026
4146
  // src/server/api.ts
4027
- import fs25 from "fs";
4028
- import path23 from "path";
4147
+ import fs26 from "fs";
4148
+ import path24 from "path";
4029
4149
  function sendJson(res, status, body) {
4030
4150
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
4031
4151
  res.end(JSON.stringify(body));
@@ -4076,17 +4196,17 @@ async function handleApi(ctx, req, res, url) {
4076
4196
  }
4077
4197
  if (route2 === "/source") {
4078
4198
  const rel = url.searchParams.get("path") ?? "";
4079
- const file = path23.resolve(project.paths.root, rel);
4080
- if (!rel || !isInside(project.paths.root, file) || !fs25.existsSync(file) || fs25.statSync(file).isDirectory()) {
4199
+ const file = path24.resolve(project.paths.root, rel);
4200
+ if (!rel || !isInside(project.paths.root, file) || !fs26.existsSync(file) || fs26.statSync(file).isDirectory()) {
4081
4201
  sendJson(res, 404, { error: "not found" });
4082
4202
  return true;
4083
4203
  }
4084
- if (fs25.statSync(file).size > 2 * 1024 * 1024) {
4204
+ if (fs26.statSync(file).size > 2 * 1024 * 1024) {
4085
4205
  sendJson(res, 413, { error: "file too large to show" });
4086
4206
  return true;
4087
4207
  }
4088
4208
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
4089
- res.end(fs25.readFileSync(file, "utf8"));
4209
+ res.end(fs26.readFileSync(file, "utf8"));
4090
4210
  return true;
4091
4211
  }
4092
4212
  if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
@@ -4133,7 +4253,7 @@ async function handleApi(ctx, req, res, url) {
4133
4253
  return true;
4134
4254
  }
4135
4255
  res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
4136
- fs25.createReadStream(file).pipe(res);
4256
+ fs26.createReadStream(file).pipe(res);
4137
4257
  return true;
4138
4258
  }
4139
4259
  return false;
@@ -4173,26 +4293,82 @@ var EventHub = class {
4173
4293
  };
4174
4294
 
4175
4295
  // src/server/state.ts
4176
- import fs26 from "fs";
4177
- import path24 from "path";
4178
- var stateFile2 = (paths) => path24.join(paths.cache, "server.json");
4179
- var logFile = (paths) => path24.join(paths.cache, "server.log");
4296
+ import fs27 from "fs";
4297
+ import os4 from "os";
4298
+ import path25 from "path";
4299
+ var stateFile2 = (paths) => path25.join(paths.cache, "server.json");
4300
+ var logFile = (paths) => path25.join(paths.cache, "server.log");
4180
4301
  function readState(paths) {
4181
4302
  try {
4182
- return JSON.parse(fs26.readFileSync(stateFile2(paths), "utf8"));
4303
+ return JSON.parse(fs27.readFileSync(stateFile2(paths), "utf8"));
4183
4304
  } catch {
4184
4305
  return null;
4185
4306
  }
4186
4307
  }
4308
+ function registryDir() {
4309
+ const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path25.join(os4.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path25.join(os4.homedir(), ".cache");
4310
+ return path25.join(base, "foss-design", "previews");
4311
+ }
4312
+ var entryFile = (pid) => path25.join(registryDir(), `${pid}.json`);
4187
4313
  function writeState(paths, state) {
4188
- fs26.mkdirSync(paths.cache, { recursive: true });
4189
- fs26.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4314
+ fs27.mkdirSync(paths.cache, { recursive: true });
4315
+ fs27.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4316
+ `);
4317
+ try {
4318
+ fs27.mkdirSync(registryDir(), { recursive: true });
4319
+ fs27.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
4190
4320
  `);
4321
+ } catch {
4322
+ }
4191
4323
  }
4192
4324
  function clearState(paths, pid) {
4193
4325
  const state = readState(paths);
4326
+ fs27.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
4194
4327
  if (pid !== void 0 && state && state.pid !== pid) return;
4195
- fs26.rmSync(stateFile2(paths), { force: true });
4328
+ fs27.rmSync(stateFile2(paths), { force: true });
4329
+ }
4330
+ function listPreviews() {
4331
+ let files = [];
4332
+ try {
4333
+ files = fs27.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
4334
+ } catch {
4335
+ return [];
4336
+ }
4337
+ const found = [];
4338
+ for (const name of files) {
4339
+ const file = path25.join(registryDir(), name);
4340
+ let state = null;
4341
+ try {
4342
+ state = JSON.parse(fs27.readFileSync(file, "utf8"));
4343
+ } catch {
4344
+ }
4345
+ if (!state?.pid || !pidAlive(state.pid)) {
4346
+ fs27.rmSync(file, { force: true });
4347
+ continue;
4348
+ }
4349
+ let recorded = null;
4350
+ try {
4351
+ recorded = JSON.parse(fs27.readFileSync(path25.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4352
+ } catch {
4353
+ }
4354
+ found.push({ ...state, tracked: recorded?.pid === state.pid });
4355
+ }
4356
+ return found.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
4357
+ }
4358
+ async function stopPreview(entry) {
4359
+ try {
4360
+ process.kill(entry.pid, "SIGTERM");
4361
+ } catch {
4362
+ }
4363
+ const until = Date.now() + 5e3;
4364
+ while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
4365
+ fs27.rmSync(entryFile(entry.pid), { force: true });
4366
+ const stateAt = path25.join(entry.root, ".design", ".cache", "server.json");
4367
+ try {
4368
+ if (JSON.parse(fs27.readFileSync(stateAt, "utf8")).pid === entry.pid)
4369
+ fs27.rmSync(stateAt, { force: true });
4370
+ } catch {
4371
+ }
4196
4372
  }
4197
4373
  function pidAlive(pid) {
4198
4374
  try {
@@ -4249,15 +4425,15 @@ function notFoundPage(res, title, text2) {
4249
4425
  }
4250
4426
  async function startDevServer(options) {
4251
4427
  const paths = designPaths(options.root);
4252
- if (!fs27.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4253
- fs27.mkdirSync(paths.cache, { recursive: true });
4428
+ if (!fs28.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4429
+ fs28.mkdirSync(paths.cache, { recursive: true });
4254
4430
  const snapshots = new SnapshotStore(paths.snapshots);
4255
4431
  const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
4256
4432
  const entries = new Entries(project);
4257
4433
  entries.sync();
4258
4434
  const links = linkShippedPackages(paths, project.appDir());
4259
4435
  const events = new EventHub();
4260
- const httpServer = http.createServer();
4436
+ const httpServer = http2.createServer();
4261
4437
  dropStaleDepCache(paths, project.appDir());
4262
4438
  const base = baseConfig(project);
4263
4439
  const vite = await createServer({
@@ -4276,8 +4452,8 @@ async function startDevServer(options) {
4276
4452
  // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4277
4453
  // generated entries are modules the frames load, so those stay watched.
4278
4454
  ignored: (file) => {
4279
- const rel = toPosix(path25.relative(paths.cache, file));
4280
- const inCache = rel !== "" && !rel.startsWith("..") && !path25.isAbsolute(rel);
4455
+ const rel = toPosix(path26.relative(paths.cache, file));
4456
+ const inCache = rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
4281
4457
  return inCache && rel !== "entries" && !rel.startsWith("entries/");
4282
4458
  }
4283
4459
  },
@@ -4286,11 +4462,11 @@ async function startDevServer(options) {
4286
4462
  forwardConsole: false
4287
4463
  },
4288
4464
  optimizeDeps: {
4289
- entries: entries.files().map((file) => toPosix(path25.relative(paths.design, file)))
4465
+ entries: entries.files().map((file) => toPosix(path26.relative(paths.design, file)))
4290
4466
  }
4291
4467
  });
4292
4468
  const publicDir = project.publicDir();
4293
- const servePublic = publicDir ? sirv(publicDir, { dev: true }) : null;
4469
+ const servePublic = publicDir ? sirv2(publicDir, { dev: true }) : null;
4294
4470
  const publicRel = (encoded) => {
4295
4471
  try {
4296
4472
  return decodeURIComponent(encoded);
@@ -4298,16 +4474,16 @@ async function startDevServer(options) {
4298
4474
  return null;
4299
4475
  }
4300
4476
  };
4301
- const viewer = fs27.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
4302
- const viewerIndex = path25.join(PKG.viewer, "index.html");
4477
+ const viewer = fs28.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
4478
+ const viewerIndex = path26.join(PKG.viewer, "index.html");
4303
4479
  const serveViewerIndex = (res) => {
4304
- const html = fs27.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4480
+ const html = fs28.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4305
4481
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
4306
4482
  res.end(html);
4307
4483
  };
4308
4484
  const ctx = { project, events, snapshots };
4309
4485
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
4310
- const bootUrl = `/${relToDesign(paths, path25.join(entries.dir, HTML_BOOT))}`;
4486
+ const bootUrl = `/${relToDesign(paths, path26.join(entries.dir, HTML_BOOT))}`;
4311
4487
  const frameHead = (source, extra = {}) => {
4312
4488
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
4313
4489
  return {
@@ -4328,7 +4504,7 @@ async function startDevServer(options) {
4328
4504
  notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
4329
4505
  return;
4330
4506
  }
4331
- if (!fs27.existsSync(entries.entryFile(source))) entries.sync();
4507
+ if (!fs28.existsSync(entries.entryFile(source))) entries.sync();
4332
4508
  const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
4333
4509
  const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
4334
4510
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -4337,8 +4513,8 @@ async function startDevServer(options) {
4337
4513
  };
4338
4514
  const serveHtmlScreen = async (res, url2) => {
4339
4515
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
4340
- const file = path25.resolve(paths.design, rel);
4341
- if (!isInside(paths.design, file) || !fs27.existsSync(file)) {
4516
+ const file = path26.resolve(paths.design, rel);
4517
+ if (!isInside(paths.design, file) || !fs28.existsSync(file)) {
4342
4518
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
4343
4519
  return;
4344
4520
  }
@@ -4348,13 +4524,13 @@ async function startDevServer(options) {
4348
4524
  key: `${canvas}/${id2}`,
4349
4525
  canvas,
4350
4526
  id: id2,
4351
- title: path25.basename(file),
4527
+ title: path26.basename(file),
4352
4528
  file,
4353
4529
  format: "html",
4354
4530
  props: {},
4355
4531
  system: true
4356
4532
  };
4357
- const raw = fs27.readFileSync(file, "utf8");
4533
+ const raw = fs28.readFileSync(file, "utf8");
4358
4534
  const withSystem = source.system && !optsOutOfSystem(raw);
4359
4535
  const html = injectIntoHtml(
4360
4536
  raw,
@@ -4387,7 +4563,7 @@ async function startDevServer(options) {
4387
4563
  }
4388
4564
  if (url2.pathname.startsWith("/_fs/")) {
4389
4565
  const rel = publicRel(url2.pathname.slice("/_fs/".length));
4390
- if (publicDir && rel !== null && !project.isPublicFile(rel) && fs27.existsSync(path25.join(publicDir, rel))) {
4566
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path26.join(publicDir, rel))) {
4391
4567
  notFoundPage(res, "Not found", url2.pathname);
4392
4568
  return;
4393
4569
  }
@@ -4399,7 +4575,7 @@ async function startDevServer(options) {
4399
4575
  const rel = publicRel(url2.pathname.slice(1));
4400
4576
  if (servePublic && rel !== null && project.isPublicFile(rel))
4401
4577
  servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4402
- else if (path25.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4578
+ else if (path26.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4403
4579
  else serveViewerIndex(res);
4404
4580
  };
4405
4581
  if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
@@ -4427,7 +4603,7 @@ async function startDevServer(options) {
4427
4603
  };
4428
4604
  vite.watcher.on("all", (event, file) => {
4429
4605
  if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4430
- const rel = toPosix(path25.relative(paths.design, file));
4606
+ const rel = toPosix(path26.relative(paths.design, file));
4431
4607
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
4432
4608
  if (rel === "design.json") {
4433
4609
  queue("config", () => {
@@ -4487,7 +4663,7 @@ async function startDevServer(options) {
4487
4663
  // src/cli/commands/preview.ts
4488
4664
  function openBrowser(url) {
4489
4665
  const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
4490
- const child = spawn(command, args, { stdio: "ignore", detached: true });
4666
+ const child = spawn(command, args, { stdio: "ignore", detached: true, windowsHide: true });
4491
4667
  child.on("error", () => {
4492
4668
  });
4493
4669
  child.unref();
@@ -4535,15 +4711,23 @@ async function ensureServer(paths, options) {
4535
4711
  if (stale)
4536
4712
  print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
4537
4713
  if (live2) await stopServer(paths);
4538
- fs28.mkdirSync(paths.cache, { recursive: true });
4539
- const log = fs28.openSync(logFile(paths), "w");
4714
+ const recorded = readState(paths);
4715
+ if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
4716
+ for (const other of listPreviews()) if (other.root === paths.root) await stopPreview(other);
4717
+ fs29.mkdirSync(paths.cache, { recursive: true });
4718
+ const log = fs29.openSync(logFile(paths), "w");
4540
4719
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
4541
4720
  if (options.port) args.push("--port", String(options.port));
4542
- const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
4721
+ const child = spawn(process.execPath, args, {
4722
+ detached: true,
4723
+ windowsHide: true,
4724
+ stdio: ["ignore", log, log],
4725
+ cwd: paths.root
4726
+ });
4543
4727
  child.unref();
4544
4728
  const state = await waitForServer(paths, 4e4);
4545
4729
  if (!state) {
4546
- const tail = fs28.existsSync(logFile(paths)) ? fs28.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4730
+ const tail = fs29.existsSync(logFile(paths)) ? fs29.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4547
4731
  throw new CliError(`The preview server did not start.
4548
4732
  ${tail.join("\n")}`);
4549
4733
  }
@@ -4569,6 +4753,27 @@ async function runPreview(paths, options) {
4569
4753
  async function runStop(paths) {
4570
4754
  print(await stopServer(paths) ? "Stopped the preview server." : "No preview server is running for this project.");
4571
4755
  }
4756
+ var since = (iso) => {
4757
+ const minutes = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 6e4));
4758
+ return minutes < 60 ? `${minutes}m` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
4759
+ };
4760
+ function runPreviews(options) {
4761
+ const previews = listPreviews();
4762
+ if (options.json) return print(JSON.stringify({ previews }, null, 2));
4763
+ if (!previews.length) return print("No preview servers are running.");
4764
+ for (const preview of previews) {
4765
+ const orphan = preview.tracked ? "" : ` ${bold("orphan")} ${dim("(its project names another server)")}`;
4766
+ print(`${preview.url} ${preview.root}${orphan}`);
4767
+ print(dim(` foss-design ${preview.version} \xB7 pid ${preview.pid} \xB7 up ${since(preview.startedAt)}`));
4768
+ }
4769
+ }
4770
+ async function runStopAll() {
4771
+ const previews = listPreviews();
4772
+ for (const preview of previews) await stopPreview(preview);
4773
+ print(
4774
+ previews.length ? `Stopped ${previews.length} preview server${previews.length === 1 ? "" : "s"}.` : "No preview servers are running."
4775
+ );
4776
+ }
4572
4777
 
4573
4778
  // src/cli/commands/sync.ts
4574
4779
  var GOOD = /* @__PURE__ */ new Set(["pushed", "created", "updated", "deleted"]);
@@ -4649,11 +4854,19 @@ async function runSync(paths, command, json, run) {
4649
4854
  if (!report.ok && report.error) throw new CliError(report.error.message, 2);
4650
4855
  }
4651
4856
  function runPush(paths, units, options) {
4652
- const beforeBuild = (canvasIds) => refreshCovers(paths, canvasIds, async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`);
4653
4857
  return runSync(paths, "push", options.json, async (ctx) => {
4858
+ const beforeBuild = (canvasIds) => takeSnapshots(
4859
+ paths,
4860
+ staleSnapshots(paths, canvasIds),
4861
+ async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`,
4862
+ ctx.progress()
4863
+ );
4654
4864
  const first = await push(ctx, { units, resolved: options.resolved, beforeBuild });
4655
4865
  if (first.ok || first.error?.code !== "pull_first") return first;
4656
- ctx.log("The cloud is ahead: merging its changes first\u2026");
4866
+ const raced = first.error.message.includes("someone pushed first");
4867
+ ctx.log(
4868
+ raced ? "Someone pushed while this push was uploading: merging their changes first\u2026" : "The cloud is ahead: merging its changes first\u2026"
4869
+ );
4657
4870
  const pulled = await pull(ctx, { units: [], theirs: [] });
4658
4871
  if (!pulled.ok) {
4659
4872
  pulled.error = {
@@ -4665,7 +4878,7 @@ function runPush(paths, units, options) {
4665
4878
  ctx.link = readLink(paths);
4666
4879
  const second = await push(ctx, { units, resolved: [], beforeBuild });
4667
4880
  second.hints.unshift(
4668
- `Merged the cloud's changes first: ${pulled.units.filter((entry) => entry.action !== "up_to_date").map((entry) => `${entry.unit} (${entry.action.replaceAll("_", " ")})`).join(", ")}`
4881
+ `${raced ? "Someone pushed while this push was uploading; merged" : "Merged"} the cloud's changes first: ${pulled.units.filter((entry) => entry.action !== "up_to_date").map((entry) => `${entry.unit} (${entry.action.replaceAll("_", " ")})`).join(", ")}`
4669
4882
  );
4670
4883
  return second;
4671
4884
  });
@@ -4897,6 +5110,7 @@ async function runMe(paths, options) {
4897
5110
 
4898
5111
  // src/cli/commands/check.ts
4899
5112
  var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
5113
+ var SLOW_MS = 3e3;
4900
5114
  function printIssues(issues) {
4901
5115
  const byFile = /* @__PURE__ */ new Map();
4902
5116
  for (const issue of issues) byFile.set(issue.file, [...byFile.get(issue.file) ?? [], issue]);
@@ -4929,35 +5143,90 @@ async function renderFrames(jobs) {
4929
5143
  await page.close().catch(() => {
4930
5144
  });
4931
5145
  }
4932
- return { key: job.key, url: job.url, ready: report.ready, held: report.held, errors: report.errors, links };
5146
+ return {
5147
+ key: job.key,
5148
+ url: job.url,
5149
+ ready: report.ready,
5150
+ held: report.held,
5151
+ readyMs: report.readyMs,
5152
+ errors: report.errors,
5153
+ links
5154
+ };
4933
5155
  });
4934
5156
  } finally {
4935
5157
  await browser.close();
4936
5158
  }
4937
5159
  }
4938
- async function runCheck(paths, canvases, options) {
5160
+ async function runCheck(paths, targets, options) {
4939
5161
  const project = new DesignProject(paths, DEV_URLS);
4940
5162
  const known = project.canvasIds();
4941
- const unknown = canvases.filter((id2) => !known.includes(id2));
4942
- if (unknown.length) {
4943
- throw new CliError(
4944
- `No canvas ${unknown.map((id2) => `"${id2}"`).join(", ")}. Canvases: ${known.join(", ") || "none yet"}`
4945
- );
5163
+ const picked = /* @__PURE__ */ new Map();
5164
+ for (const target of targets) {
5165
+ const [canvasId = "", screenId] = target.split("/");
5166
+ if (!known.includes(canvasId))
5167
+ throw new CliError(`No canvas "${canvasId}". Canvases: ${known.join(", ") || "none yet"}`);
5168
+ const screens = picked.has(canvasId) ? picked.get(canvasId) : /* @__PURE__ */ new Set();
5169
+ if (!screenId || screens === null) picked.set(canvasId, null);
5170
+ else picked.set(canvasId, screens.add(screenId));
5171
+ }
5172
+ const canvases = [...picked.keys()];
5173
+ if (options.page) {
5174
+ if (canvases.length !== 1)
5175
+ throw new CliError("--page needs exactly one canvas: `design check <canvas> --page <id>`");
5176
+ const doc = project.canvas(canvases[0]).doc;
5177
+ if (!doc.pages.some((page) => page.id === options.page))
5178
+ throw new CliError(`No page "${options.page}" in "${doc.id}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
5179
+ }
5180
+ for (const [canvasId, screens] of picked) {
5181
+ if (!screens) continue;
5182
+ const doc = project.canvas(canvasId).doc;
5183
+ const ids = new Set(canvasFrames(doc, "", "light", { page: options.page, skipMissing: false }).map((job) => job.id));
5184
+ const missing = [...screens].filter((id2) => !ids.has(id2));
5185
+ if (missing.length)
5186
+ throw new CliError(
5187
+ `No screen ${missing.map((id2) => `"${id2}"`).join(", ")} in "${canvasId}"${options.page ? ` on page "${options.page}"` : ""}. Screens: ${[...ids].slice(0, 30).join(", ")}${ids.size > 30 ? ", \u2026" : ""}`
5188
+ );
4946
5189
  }
4947
5190
  const issues = canvases.length ? canvases.flatMap((id2) => project.canvas(id2)?.doc.issues ?? []) : project.issues();
4948
5191
  let frames = [];
4949
5192
  if (options.render) {
4950
- const wasRunning = await liveServer(paths);
4951
- const server = await ensureServer(paths, {});
4952
- if (!wasRunning && !options.json)
4953
- print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
4954
- const base = `http://127.0.0.1:${server.port}`;
4955
- const docs = (canvases.length ? canvases : known).map((id2) => project.canvas(id2).doc);
4956
- const jobs = docs.flatMap((doc) => canvasFrames(doc, base, doc.theme ?? "light", { includeUrls: false }));
4957
- const system = project.system();
4958
- if (!canvases.length && system) jobs.unshift(...systemFrames(system.doc, base, "light"));
4959
- if (!options.json) print(dim(`Loading ${plural2(jobs.length, "frame")} in Chrome via ${server.url}\u2026`));
4960
- frames = await renderFrames(jobs);
5193
+ let base;
5194
+ let docOf = (id2) => project.canvas(id2).doc;
5195
+ let systemDoc = project.system()?.doc ?? null;
5196
+ let close = async () => {
5197
+ };
5198
+ if (options.built) {
5199
+ if (!options.json) print(dim("Building the screens as design push does\u2026"));
5200
+ const build2 = await serveBuild(paths, "check-build", {
5201
+ canvases: canvases.length ? canvases : void 0,
5202
+ includeSystem: !canvases.length
5203
+ });
5204
+ docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
5205
+ systemDoc = canvases.length ? null : build2.read("api/system.json");
5206
+ base = build2.base;
5207
+ close = build2.close;
5208
+ if (!options.json) print(dim(`Serving the build at ${base}`));
5209
+ } else {
5210
+ const wasRunning = await liveServer(paths);
5211
+ const server = await ensureServer(paths, {});
5212
+ if (!wasRunning && !options.json)
5213
+ print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
5214
+ base = `http://127.0.0.1:${server.port}`;
5215
+ }
5216
+ const jobs = (canvases.length ? canvases : known).flatMap((id2) => {
5217
+ const doc = docOf(id2);
5218
+ const theme2 = doc.theme ?? "light";
5219
+ const screens = picked.get(id2);
5220
+ const filter = { includeUrls: false, page: options.page };
5221
+ return screens ? [...screens].flatMap((screen) => canvasFrames(doc, base, theme2, { ...filter, id: screen })) : canvasFrames(doc, base, theme2, filter);
5222
+ });
5223
+ if (!canvases.length && systemDoc) jobs.unshift(...systemFrames(systemDoc, base, "light"));
5224
+ if (!options.json) print(dim(`Loading ${plural2(jobs.length, "frame")} in Chrome\u2026`));
5225
+ try {
5226
+ frames = await renderFrames(jobs);
5227
+ } finally {
5228
+ await close();
5229
+ }
4961
5230
  }
4962
5231
  const linkIssues = options.render ? unroutedLinks(frames, project, canvases.length ? canvases : known) : [];
4963
5232
  const errors = issues.filter((issue) => issue.severity === "error").length;
@@ -4975,10 +5244,20 @@ async function runCheck(paths, canvases, options) {
4975
5244
  if (frame.held) print(` ${red(`holdReady() was not released within 20s (${frame.held} still held)`)}`);
4976
5245
  else if (!frame.ready) print(` ${red("did not finish rendering (no ready signal within 20s)")}`);
4977
5246
  for (const error of frame.errors) print(` ${red(error.split("\n")[0])}`);
5247
+ if (frame.readyMs !== void 0 && frame.readyMs > SLOW_MS)
5248
+ print(` ${yellow(`slow: ${(frame.readyMs / 1e3).toFixed(1)}s to become ready`)}`);
4978
5249
  }
5250
+ const slow = frames.filter((frame) => (frame.readyMs ?? 0) > SLOW_MS);
5251
+ if (slow.length)
5252
+ print(
5253
+ dim(
5254
+ `${plural2(slow.length, "screen")} took over ${SLOW_MS / 1e3}s to become ready; every frame of them costs that in the viewer, check and push snapshots. See what they wait for (retries, timers, polling, entrance gates).`
5255
+ )
5256
+ );
4979
5257
  }
4980
5258
  if (linkIssues.length) printLinkIssues(linkIssues, project);
4981
- const scope = canvases.length ? plural2(canvases.length, "canvas", "canvases") : "the project";
5259
+ const screens = [...picked.values()].reduce((sum, set) => sum + (set?.size ?? 0), 0);
5260
+ const scope = !canvases.length ? "the project" : screens && [...picked.values()].every(Boolean) ? plural2(screens, "screen") : options.page ? `page "${options.page}" of ${canvases[0]}` : plural2(canvases.length, "canvas", "canvases");
4982
5261
  if (!errors && !warnings && !broken.length) {
4983
5262
  print(
4984
5263
  green(
@@ -5047,14 +5326,14 @@ function printLinkIssues(issues, project) {
5047
5326
  }
5048
5327
 
5049
5328
  // src/cli/commands/cloud.ts
5050
- import fs29 from "fs";
5051
- import os3 from "os";
5052
- import path26 from "path";
5329
+ import fs30 from "fs";
5330
+ import os5 from "os";
5331
+ import path27 from "path";
5053
5332
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5054
5333
  async function runLogin() {
5055
5334
  const host = cloudHost();
5056
5335
  const client = new CloudClient(host, null);
5057
- const code = await client.deviceCode(os3.hostname());
5336
+ const code = await client.deviceCode(os5.hostname());
5058
5337
  const url = new URL(code.verification_uri_complete || code.verification_uri, host).toString();
5059
5338
  const expiresIn = code.expires_in ?? 600;
5060
5339
  print(`${bold("Sign in to foss-design Cloud")} ${dim(host)}`);
@@ -5150,7 +5429,7 @@ async function runLink(paths, wanted, options) {
5150
5429
  print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
5151
5430
  } else {
5152
5431
  writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
5153
- fs29.rmSync(path26.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5432
+ fs30.rmSync(path27.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5154
5433
  const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
5155
5434
  print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
5156
5435
  }
@@ -5177,8 +5456,8 @@ async function runLink(paths, wanted, options) {
5177
5456
  }
5178
5457
 
5179
5458
  // src/cli/commands/icon.ts
5180
- import fs30 from "fs";
5181
- import path27 from "path";
5459
+ import fs31 from "fs";
5460
+ import path28 from "path";
5182
5461
  async function toCloud(paths, file) {
5183
5462
  const link = readLink(paths);
5184
5463
  if (!link) return;
@@ -5189,10 +5468,10 @@ async function toCloud(paths, file) {
5189
5468
  }
5190
5469
  const client = new CloudClient(link.host, credential.token);
5191
5470
  try {
5192
- const ext = file ? path27.extname(file).slice(1) : null;
5471
+ const ext = file ? path28.extname(file).slice(1) : null;
5193
5472
  await client.setProjectIcon(
5194
5473
  link.project,
5195
- file && ext ? { data: fs30.readFileSync(file), type: ICON_TYPES[ext] } : null
5474
+ file && ext ? { data: fs31.readFileSync(file), type: ICON_TYPES[ext] } : null
5196
5475
  );
5197
5476
  link.icon = file ? iconHash(file) : null;
5198
5477
  writeLink(paths, link);
@@ -5205,35 +5484,35 @@ async function runIcon(paths, file, options) {
5205
5484
  if (options.remove) {
5206
5485
  const had = iconFile(paths);
5207
5486
  writeIcon(paths, null);
5208
- print(had ? `${green("Removed")} ${path27.relative(process.cwd(), had)}` : "The project has no icon.");
5487
+ print(had ? `${green("Removed")} ${path28.relative(process.cwd(), had)}` : "The project has no icon.");
5209
5488
  await toCloud(paths, null);
5210
5489
  return;
5211
5490
  }
5212
5491
  if (!file) {
5213
5492
  const current = iconFile(paths);
5214
5493
  if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
5215
- return print(`${path27.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs30.statSync(current).size / 1024)} KB`)}`);
5494
+ return print(`${path28.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
5216
5495
  }
5217
5496
  let data;
5218
5497
  try {
5219
- data = fs30.readFileSync(file);
5498
+ data = fs31.readFileSync(file);
5220
5499
  } catch {
5221
5500
  throw new CliError(`No file ${file}`);
5222
5501
  }
5223
- const problem = iconProblem(data, path27.extname(file).slice(1).toLowerCase() || void 0);
5502
+ const problem = iconProblem(data, path28.extname(file).slice(1).toLowerCase() || void 0);
5224
5503
  if (problem) throw new CliError(`${file}: ${problem}`);
5225
5504
  const written = writeIcon(paths, data);
5226
- print(`${green("Set")} the project icon: ${path27.relative(process.cwd(), written)}`);
5505
+ print(`${green("Set")} the project icon: ${path28.relative(process.cwd(), written)}`);
5227
5506
  await toCloud(paths, written);
5228
5507
  }
5229
5508
 
5230
5509
  // src/cli/commands/import.ts
5231
- import fs32 from "fs";
5232
- import path29 from "path";
5510
+ import fs33 from "fs";
5511
+ import path30 from "path";
5233
5512
 
5234
5513
  // src/import/claude-design.ts
5235
- import fs31 from "fs";
5236
- import path28 from "path";
5514
+ import fs32 from "fs";
5515
+ import path29 from "path";
5237
5516
 
5238
5517
  // src/import/markup.ts
5239
5518
  var VOID = /* @__PURE__ */ new Set([
@@ -5841,11 +6120,11 @@ function boardId(file, used) {
5841
6120
  return id2;
5842
6121
  }
5843
6122
  function importClaudeDesign(dir, options = {}) {
5844
- const canvasFile = path28.join(dir, "canvas.json");
5845
- if (!fs31.existsSync(canvasFile))
6123
+ const canvasFile = path29.join(dir, "canvas.json");
6124
+ if (!fs32.existsSync(canvasFile))
5846
6125
  throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
5847
- const source = JSON.parse(fs31.readFileSync(canvasFile, "utf8"));
5848
- const templates = fs31.readdirSync(dir).filter((file) => file.endsWith(EXT));
6126
+ const source = JSON.parse(fs32.readFileSync(canvasFile, "utf8"));
6127
+ const templates = fs32.readdirSync(dir).filter((file) => file.endsWith(EXT));
5849
6128
  if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
5850
6129
  const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
5851
6130
  const warnings = /* @__PURE__ */ new Map();
@@ -5872,7 +6151,7 @@ function importClaudeDesign(dir, options = {}) {
5872
6151
  try {
5873
6152
  files.set(
5874
6153
  `${name}.jsx`,
5875
- convertFile(name, fs31.readFileSync(path28.join(dir, file), "utf8"), { known, routes, warn: warn2 })
6154
+ convertFile(name, fs32.readFileSync(path29.join(dir, file), "utf8"), { known, routes, warn: warn2 })
5876
6155
  );
5877
6156
  } catch (error) {
5878
6157
  warn2(`could not be converted: ${error.message}`);
@@ -5920,7 +6199,7 @@ function importClaudeDesign(dir, options = {}) {
5920
6199
  screens++;
5921
6200
  }
5922
6201
  const canvas = {
5923
- title: options.title ?? source.title ?? path28.basename(path28.resolve(dir)),
6202
+ title: options.title ?? source.title ?? path29.basename(path29.resolve(dir)),
5924
6203
  description: "Imported from Claude Design.",
5925
6204
  // The templates bring their own styles; the design system's reset would restyle them.
5926
6205
  system: false,
@@ -5932,21 +6211,21 @@ function importClaudeDesign(dir, options = {}) {
5932
6211
  // src/cli/commands/import.ts
5933
6212
  var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
5934
6213
  function runImport(paths, from, options) {
5935
- const dir = path29.resolve(from);
5936
- if (!fs32.existsSync(dir) || !fs32.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
6214
+ const dir = path30.resolve(from);
6215
+ if (!fs33.existsSync(dir) || !fs33.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
5937
6216
  let result;
5938
6217
  try {
5939
6218
  result = importClaudeDesign(dir, { title: options.title });
5940
6219
  } catch (error) {
5941
6220
  throw new CliError(error.message);
5942
6221
  }
5943
- const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path29.basename(dir)) || "imported");
6222
+ const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path30.basename(dir)) || "imported");
5944
6223
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
5945
- const out = path29.join(paths.canvases, id2);
5946
- if (fs32.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
5947
- fs32.mkdirSync(path29.join(out, "screens"), { recursive: true });
5948
- for (const [name, content] of result.files) fs32.writeFileSync(path29.join(out, "screens", name), content);
5949
- fs32.writeFileSync(path29.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
6224
+ const out = path30.join(paths.canvases, id2);
6225
+ if (fs33.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
6226
+ fs33.mkdirSync(path30.join(out, "screens"), { recursive: true });
6227
+ for (const [name, content] of result.files) fs33.writeFileSync(path30.join(out, "screens", name), content);
6228
+ fs33.writeFileSync(path30.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
5950
6229
  `);
5951
6230
  print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
5952
6231
  print(
@@ -5966,7 +6245,7 @@ function runImport(paths, from, options) {
5966
6245
 
5967
6246
  // src/cli/commands/merge.ts
5968
6247
  import { spawnSync as spawnSync2 } from "child_process";
5969
- import path30 from "path";
6248
+ import path31 from "path";
5970
6249
  import { createInterface } from "readline/promises";
5971
6250
  var KIND = {
5972
6251
  text: "both sides changed lines, the markers show them",
@@ -5982,7 +6261,7 @@ function matchTargets(paths, states, targets) {
5982
6261
  for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
5983
6262
  continue;
5984
6263
  }
5985
- const rel = [path30.relative(paths.design, path30.resolve(target)), target.replace(/^\.design\//, "")];
6264
+ const rel = [path31.relative(paths.design, path31.resolve(target)), target.replace(/^\.design\//, "")];
5986
6265
  const found = states.flatMap(
5987
6266
  (state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
5988
6267
  );
@@ -5998,7 +6277,7 @@ function printOpen(paths, states) {
5998
6277
  print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
5999
6278
  for (const conflict of open2) {
6000
6279
  print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
6001
- const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path30.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6280
+ const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path31.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6002
6281
  print(` ${dim(sides)}`);
6003
6282
  }
6004
6283
  }
@@ -6079,7 +6358,7 @@ async function runMerge(paths, targets, options) {
6079
6358
  if (answer === "s") break;
6080
6359
  if (answer === "e" && edit) {
6081
6360
  const editor = process.env.VISUAL || process.env.EDITOR || "vi";
6082
- spawnSync2(editor, [path30.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6361
+ spawnSync2(editor, [path31.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6083
6362
  if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
6084
6363
  print(yellow(" Conflict markers are still there."));
6085
6364
  }
@@ -6098,7 +6377,7 @@ async function runMerge(paths, targets, options) {
6098
6377
  }
6099
6378
 
6100
6379
  // src/cli/commands/rename.ts
6101
- import fs33 from "fs";
6380
+ import fs34 from "fs";
6102
6381
  async function runRename(paths, name) {
6103
6382
  const trimmed = name.trim();
6104
6383
  if (!trimmed) throw new CliError("Usage: design rename <name>");
@@ -6113,17 +6392,17 @@ async function runRename(paths, name) {
6113
6392
  }
6114
6393
  let config = {};
6115
6394
  try {
6116
- config = JSON.parse(fs33.readFileSync(paths.config, "utf8"));
6395
+ config = JSON.parse(fs34.readFileSync(paths.config, "utf8"));
6117
6396
  } catch {
6118
6397
  }
6119
- fs33.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
6398
+ fs34.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
6120
6399
  `);
6121
6400
  print(`${green("Renamed")} the project to ${trimmed} (.design/design.json)`);
6122
6401
  }
6123
6402
 
6124
6403
  // src/cli/commands/scaffold.ts
6125
- import fs34 from "fs";
6126
- import path31 from "path";
6404
+ import fs35 from "fs";
6405
+ import path32 from "path";
6127
6406
 
6128
6407
  // src/cli/templates.ts
6129
6408
  var TOKENS_CSS = `/*
@@ -6334,40 +6613,40 @@ function mainScreen(canvasId) {
6334
6613
 
6335
6614
  // src/cli/commands/scaffold.ts
6336
6615
  function write(file, content, root) {
6337
- if (fs34.existsSync(file)) {
6338
- print(` ${dim("exists ")} ${path31.relative(root, file)}`);
6616
+ if (fs35.existsSync(file)) {
6617
+ print(` ${dim("exists ")} ${path32.relative(root, file)}`);
6339
6618
  return false;
6340
6619
  }
6341
- fs34.mkdirSync(path31.dirname(file), { recursive: true });
6342
- fs34.writeFileSync(file, content);
6343
- print(` ${green("created")} ${path31.relative(root, file)}`);
6620
+ fs35.mkdirSync(path32.dirname(file), { recursive: true });
6621
+ fs35.writeFileSync(file, content);
6622
+ print(` ${green("created")} ${path32.relative(root, file)}`);
6344
6623
  return true;
6345
6624
  }
6346
6625
  function ensureGitignored(root) {
6347
- const file = path31.join(root, ".gitignore");
6348
- const current = fs34.existsSync(file) ? fs34.readFileSync(file, "utf8") : "";
6626
+ const file = path32.join(root, ".gitignore");
6627
+ const current = fs35.existsSync(file) ? fs35.readFileSync(file, "utf8") : "";
6349
6628
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
6350
6629
  if (covered) return "present";
6351
6630
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
6352
- fs34.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
6631
+ fs35.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
6353
6632
  .design/
6354
6633
  `);
6355
6634
  return "added";
6356
6635
  }
6357
6636
  function projectName(root) {
6358
6637
  try {
6359
- const manifest = JSON.parse(fs34.readFileSync(path31.join(root, "package.json"), "utf8"));
6638
+ const manifest = JSON.parse(fs35.readFileSync(path32.join(root, "package.json"), "utf8"));
6360
6639
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
6361
6640
  } catch {
6362
6641
  }
6363
- return path31.basename(root);
6642
+ return path32.basename(root);
6364
6643
  }
6365
6644
  function runInit(root, options) {
6366
6645
  const paths = designPaths(root);
6367
6646
  print(`Setting up ${DESIGN_DIR} in ${root}`);
6368
6647
  write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
6369
6648
  `, root);
6370
- fs34.mkdirSync(paths.canvases, { recursive: true });
6649
+ fs35.mkdirSync(paths.canvases, { recursive: true });
6371
6650
  if (options.gitignore) {
6372
6651
  const result = ensureGitignored(root);
6373
6652
  print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
@@ -6381,44 +6660,44 @@ function runSystemInit(root, options) {
6381
6660
  const paths = designPaths(root);
6382
6661
  let base = projectName(root);
6383
6662
  try {
6384
- base = JSON.parse(fs34.readFileSync(paths.config, "utf8")).name || base;
6663
+ base = JSON.parse(fs35.readFileSync(paths.config, "utf8")).name || base;
6385
6664
  } catch {
6386
6665
  }
6387
6666
  const name = options.name ?? `${base} design system`;
6388
- print(`Scaffolding the design system in ${path31.relative(root, paths.system)}`);
6389
- write(path31.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
6667
+ print(`Scaffolding the design system in ${path32.relative(root, paths.system)}`);
6668
+ write(path32.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
6390
6669
  `, root);
6391
- write(path31.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6670
+ write(path32.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6392
6671
  if (!options.empty) {
6393
- write(path31.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
6394
- write(path31.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
6395
- write(path31.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
6672
+ write(path32.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
6673
+ write(path32.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
6674
+ write(path32.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
6396
6675
  }
6397
6676
  }
6398
6677
  function runNew(root, id2, options) {
6399
6678
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
6400
6679
  const paths = designPaths(root);
6401
- const dir = path31.join(paths.canvases, id2);
6402
- if (fs34.existsSync(path31.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
6680
+ const dir = path32.join(paths.canvases, id2);
6681
+ if (fs35.existsSync(path32.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
6403
6682
  print(`Creating canvas "${id2}"`);
6404
6683
  const title = options.title ?? titleize(id2);
6405
6684
  if (options.empty) {
6406
6685
  write(
6407
- path31.join(dir, "canvas.json"),
6686
+ path32.join(dir, "canvas.json"),
6408
6687
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
6409
6688
  `,
6410
6689
  root
6411
6690
  );
6412
- fs34.mkdirSync(path31.join(dir, "screens"), { recursive: true });
6691
+ fs35.mkdirSync(path32.join(dir, "screens"), { recursive: true });
6413
6692
  } else {
6414
- write(path31.join(dir, "canvas.json"), canvasJson(title), root);
6415
- write(path31.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
6693
+ write(path32.join(dir, "canvas.json"), canvasJson(title), root);
6694
+ write(path32.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
6416
6695
  }
6417
6696
  }
6418
6697
 
6419
6698
  // src/cli/commands/shot.ts
6420
- import fs35 from "fs";
6421
- import path32 from "path";
6699
+ import fs36 from "fs";
6700
+ import path33 from "path";
6422
6701
  var OVERVIEW_TIMEOUT = 9e4;
6423
6702
  async function runShot(paths, target, options) {
6424
6703
  const [canvasId = "", screenId] = target.split("/");
@@ -6434,15 +6713,15 @@ async function runShot(paths, target, options) {
6434
6713
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
6435
6714
  }
6436
6715
  const theme2 = options.theme ?? doc.theme ?? "light";
6437
- const outBase = path32.resolve(options.out ?? paths.shots);
6438
- const outDir = path32.join(outBase, canvasId);
6439
- fs35.mkdirSync(outDir, { recursive: true });
6716
+ const outBase = path33.resolve(options.out ?? paths.shots);
6717
+ const outDir = path33.join(outBase, canvasId);
6718
+ fs36.mkdirSync(outDir, { recursive: true });
6440
6719
  const base = await serverBase(paths);
6441
6720
  const browser = await launchChrome();
6442
6721
  let failed = false;
6443
6722
  try {
6444
6723
  if (options.overview) {
6445
- if (!fs35.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
6724
+ if (!fs36.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
6446
6725
  const pages = options.page ? [options.page] : screenId ? doc.pages.filter((page) => page.sections.some((section) => section.items.some((item) => item.id === screenId))).map((page) => page.id) : doc.pages.map((page) => page.id);
6447
6726
  for (const pageId of pages) {
6448
6727
  const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
@@ -6458,7 +6737,7 @@ async function runShot(paths, target, options) {
6458
6737
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
6459
6738
  }
6460
6739
  await page.waitForTimeout(400);
6461
- const file = path32.join(outDir, `${pageId}.overview.png`);
6740
+ const file = path33.join(outDir, `${pageId}.overview.png`);
6462
6741
  await page.screenshot({ path: file });
6463
6742
  await page.close();
6464
6743
  print(file);
@@ -6487,7 +6766,7 @@ async function shootFrames(browser, jobs, outDir) {
6487
6766
  let failed = false;
6488
6767
  const results = await mapLimit(jobs, 4, async (job) => {
6489
6768
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
6490
- const file = path32.join(outDir, `${job.id}.${job.theme}.png`);
6769
+ const file = path33.join(outDir, `${job.id}.${job.theme}.png`);
6491
6770
  try {
6492
6771
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
6493
6772
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -6519,8 +6798,8 @@ async function shootSystem(paths, project, id2, options) {
6519
6798
  if (id2 && !all.some((job) => job.id === id2)) {
6520
6799
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
6521
6800
  }
6522
- const outDir = path32.join(path32.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
6523
- fs35.mkdirSync(outDir, { recursive: true });
6801
+ const outDir = path33.join(path33.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
6802
+ fs36.mkdirSync(outDir, { recursive: true });
6524
6803
  const base = await serverBase(paths);
6525
6804
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
6526
6805
  const browser = await launchChrome();
@@ -6531,8 +6810,36 @@ async function shootSystem(paths, project, id2, options) {
6531
6810
  }
6532
6811
  }
6533
6812
 
6813
+ // src/cli/commands/snapshots.ts
6814
+ async function runSnapshots(paths, targets, options) {
6815
+ const known = new DesignProject(paths, DEV_URLS).canvasIds();
6816
+ const picked = /* @__PURE__ */ new Map();
6817
+ for (const target of targets) {
6818
+ const [canvas = "", screen] = target.split("/");
6819
+ if (!known.includes(canvas))
6820
+ throw new CliError(`No canvas "${canvas}". Canvases: ${known.join(", ") || "none yet"}`);
6821
+ const screens = picked.has(canvas) ? picked.get(canvas) : /* @__PURE__ */ new Set();
6822
+ picked.set(canvas, !screen || screens === null ? null : screens.add(screen));
6823
+ }
6824
+ if (options.page && picked.size !== 1)
6825
+ throw new CliError("--page needs exactly one canvas: `design snapshots <canvas> --page <id>`");
6826
+ const canvases = picked.size ? [...picked.keys()] : known;
6827
+ const stale = canvases.flatMap(
6828
+ (canvas) => staleSnapshots(paths, [canvas], { page: options.page, screens: picked.get(canvas) ?? void 0 })
6829
+ );
6830
+ if (!stale.length) return print(green("\u2713 Every screen has current snapshots in both themes."));
6831
+ print(dim(`${stale.length} snapshot${stale.length === 1 ? "" : "s"} to take`));
6832
+ const lines = await takeSnapshots(
6833
+ paths,
6834
+ stale,
6835
+ async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`,
6836
+ progress()
6837
+ );
6838
+ for (const line of lines) print(line);
6839
+ }
6840
+
6534
6841
  // src/cli/commands/cloud-status.ts
6535
- import path33 from "path";
6842
+ import path34 from "path";
6536
6843
  var STATE_TEXT = {
6537
6844
  in_sync: green,
6538
6845
  local_changes: yellow,
@@ -6555,7 +6862,7 @@ function detail(paths, status) {
6555
6862
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
6556
6863
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
6557
6864
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
6558
- if (status.conflictRev !== void 0) parts.push(`merge ${path33.relative(paths.root, incomingDir(paths, status.key))}`);
6865
+ if (status.conflictRev !== void 0) parts.push(`merge ${path34.relative(paths.root, incomingDir(paths, status.key))}`);
6559
6866
  return parts.join(" \xB7 ");
6560
6867
  }
6561
6868
  async function printCloudStatus(paths, label) {
@@ -6685,15 +6992,23 @@ ${bold("Preview")}
6685
6992
  it; one from another version restarts) and print its URL;
6686
6993
  --open also opens a browser
6687
6994
  [--restart] [--foreground]
6688
- stop Stop the viewer
6995
+ stop [--all] Stop the viewer; --all stops every preview on this machine
6996
+ previews [--json] Every preview running on this machine: project, URL,
6997
+ version, uptime; orphans (a project's untracked server) marked
6689
6998
  status Viewer state, a project summary and, with a cloud link, the
6690
6999
  account, the linked project and each unit: in sync, local
6691
7000
  changes, remote ahead, conflict or archived
6692
7001
 
6693
7002
  ${bold("Verify")}
6694
- check [canvas\u2026] [--render] [--json] Validate canvas.json and the system (go() is an error);
6695
- --render loads every screen in Chrome and reports runtime
6696
- errors and links that lead to no screen
7003
+ check [<canvas>[/<screen>]\u2026] [--page <id>] Validate canvas.json and the system (go() is an error);
7004
+ [--render [--built]] [--json] --render loads every screen in Chrome and reports runtime
7005
+ errors and links that lead to no screen; --built renders
7006
+ the production build design push uploads instead of the
7007
+ dev server. Narrow it to canvases, one page (--page) or
7008
+ single screens
7009
+ snapshots [<canvas>[/<screen>]\u2026] Take the snapshots screens lack, in both themes, for the
7010
+ [--page <id>] viewer to show while frames do not run (push does this
7011
+ for the canvases it pushes)
6697
7012
  shot <canvas>[/<screen>] [--page <id>] Screenshot screens with Chrome (PNG paths are printed);
6698
7013
  [--theme light|dark] [--out <dir>] @system or @system/<id> shoots the design system specimens
6699
7014
  [--overview]
@@ -6712,7 +7027,7 @@ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>;
6712
7027
  push [canvas\u2026] [--json] Build and upload what changed here (or the named units); a
6713
7028
  [--resolved <unit>] system change pushes every active canvas with it. When the
6714
7029
  cloud is ahead it pulls and merges first, then pushes; it
6715
- stops on conflicts. Refreshes each canvas's cover snapshot.
7030
+ stops on conflicts. Takes missing snapshots (both themes).
6716
7031
  Prints the web link of every pushed canvas; refuses screens
6717
7032
  that still call go()
6718
7033
  pull [canvas\u2026] [--json] Take cloud changes into .design; a unit changed on both sides
@@ -6742,7 +7057,7 @@ ${bold("Environment")}
6742
7057
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
6743
7058
  `;
6744
7059
  function projectPaths(rootOption, requireDesign = true) {
6745
- const found = rootOption ? path34.resolve(rootOption) : findProjectRoot();
7060
+ const found = rootOption ? path35.resolve(rootOption) : findProjectRoot();
6746
7061
  if (!found) {
6747
7062
  if (!requireDesign) return designPaths(process.cwd());
6748
7063
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -6769,6 +7084,8 @@ async function main(argv) {
6769
7084
  open: { type: "boolean" },
6770
7085
  port: { type: "string" },
6771
7086
  restart: { type: "boolean" },
7087
+ all: { type: "boolean" },
7088
+ built: { type: "boolean" },
6772
7089
  foreground: { type: "boolean" },
6773
7090
  render: { type: "boolean" },
6774
7091
  json: { type: "boolean" },
@@ -6793,7 +7110,7 @@ async function main(argv) {
6793
7110
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
6794
7111
  switch (command) {
6795
7112
  case "init":
6796
- return runInit(values.root ? path34.resolve(values.root) : process.cwd(), {
7113
+ return runInit(values.root ? path35.resolve(values.root) : process.cwd(), {
6797
7114
  name: values.name,
6798
7115
  gitignore: !values["no-gitignore"]
6799
7116
  });
@@ -6831,11 +7148,20 @@ async function main(argv) {
6831
7148
  });
6832
7149
  }
6833
7150
  case "stop":
6834
- return runStop(projectPaths(values.root));
7151
+ return values.all ? runStopAll() : runStop(projectPaths(values.root));
7152
+ case "previews":
7153
+ return runPreviews({ json: !!values.json });
6835
7154
  case "status":
6836
7155
  return runStatus(projectPaths(values.root));
6837
7156
  case "check":
6838
- return runCheck(projectPaths(values.root), rest, { render: !!values.render, json: !!values.json });
7157
+ return runCheck(projectPaths(values.root), rest, {
7158
+ render: !!values.render,
7159
+ json: !!values.json,
7160
+ page: values.page,
7161
+ built: !!values.built
7162
+ });
7163
+ case "snapshots":
7164
+ return runSnapshots(projectPaths(values.root), rest, { page: values.page });
6839
7165
  case "shot": {
6840
7166
  if (!rest[0]) throw new CliError("Usage: design shot <canvas>[/<screen>] [--page <id>] [--theme light|dark]");
6841
7167
  return runShot(projectPaths(values.root), rest[0], {
@@ -6884,20 +7210,42 @@ async function main(argv) {
6884
7210
  throw new CliError(`Unknown command "${command}". Run \`design --help\`.`);
6885
7211
  }
6886
7212
  }
6887
- main(process.argv.slice(2)).catch((error) => {
6888
- endLine();
6889
- if (error instanceof CliError) {
6890
- process.stderr.write(`${red("error")} ${error.message}
6891
- `);
6892
- process.exit(error.code);
7213
+ function relaunchFromRealPath() {
7214
+ if (process.platform !== "win32" || process.env.FOSS_DESIGN_RELAUNCHED) return false;
7215
+ const script = process.argv[1];
7216
+ if (!script) return false;
7217
+ let realScript;
7218
+ let realCwd;
7219
+ try {
7220
+ realScript = fs37.realpathSync.native(script);
7221
+ realCwd = fs37.realpathSync.native(process.cwd());
7222
+ } catch {
7223
+ return false;
6893
7224
  }
6894
- if (error.code?.startsWith("ERR_PARSE_ARGS")) {
6895
- process.stderr.write(`${red("error")} ${error.message}
7225
+ const same = (a, b) => path35.resolve(a).toLowerCase() === path35.resolve(b).toLowerCase();
7226
+ if (same(realScript, script) && same(realCwd, process.cwd())) return false;
7227
+ const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
7228
+ stdio: "inherit",
7229
+ cwd: realCwd,
7230
+ env: { ...process.env, FOSS_DESIGN_RELAUNCHED: "1" }
7231
+ });
7232
+ process.exit(result.status ?? 1);
7233
+ }
7234
+ if (!relaunchFromRealPath())
7235
+ main(process.argv.slice(2)).catch((error) => {
7236
+ endLine();
7237
+ if (error instanceof CliError) {
7238
+ process.stderr.write(`${red("error")} ${error.message}
7239
+ `);
7240
+ process.exit(error.code);
7241
+ }
7242
+ if (error.code?.startsWith("ERR_PARSE_ARGS")) {
7243
+ process.stderr.write(`${red("error")} ${error.message}
6896
7244
  Run \`design --help\`.
6897
7245
  `);
6898
- process.exit(2);
6899
- }
6900
- process.stderr.write(`${red("error")} ${error?.stack ?? String(error)}
7246
+ process.exit(2);
7247
+ }
7248
+ process.stderr.write(`${red("error")} ${error?.stack ?? String(error)}
6901
7249
  `);
6902
- process.exit(1);
6903
- });
7250
+ process.exit(1);
7251
+ });