foss-design 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +193 -129
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli/index.ts
4
4
  import { spawnSync as spawnSync3 } from "child_process";
5
5
  import fs37 from "fs";
6
- import path35 from "path";
6
+ import path36 from "path";
7
7
  import { parseArgs } from "util";
8
8
 
9
9
  // src/core/paths.ts
@@ -348,8 +348,8 @@ function routeError(pattern) {
348
348
  }
349
349
  return null;
350
350
  }
351
- function split(path36) {
352
- return path36.split("/").filter(Boolean);
351
+ function split(path37) {
352
+ return path37.split("/").filter(Boolean);
353
353
  }
354
354
  function compileRoute(pattern) {
355
355
  return {
@@ -368,8 +368,8 @@ function decode2(part) {
368
368
  return part;
369
369
  }
370
370
  }
371
- function scoreRoute(route2, path36) {
372
- const parts = split(path36.split(/[?#]/)[0] ?? "").map(decode2);
371
+ function scoreRoute(route2, path37) {
372
+ const parts = split(path37.split(/[?#]/)[0] ?? "").map(decode2);
373
373
  const walk2 = (s, p) => {
374
374
  if (s === route2.segments.length) return p === parts.length ? 0 : null;
375
375
  const segment = route2.segments[s];
@@ -387,11 +387,11 @@ function scoreRoute(route2, path36) {
387
387
  };
388
388
  return walk2(0, 0);
389
389
  }
390
- function matchRoute(entries, path36) {
390
+ function matchRoute(entries, path37) {
391
391
  let best = null;
392
392
  for (const entry of entries) {
393
393
  for (const route2 of entry.routes) {
394
- const score = scoreRoute(route2, path36);
394
+ const score = scoreRoute(route2, path37);
395
395
  if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
396
396
  }
397
397
  }
@@ -518,9 +518,9 @@ var CanvasSchema = z.strictObject({
518
518
  pages: z.array(z.unknown()).min(1, "add at least one page")
519
519
  });
520
520
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
521
- function formatPath(base, path36) {
521
+ function formatPath(base, path37) {
522
522
  let out = base;
523
- for (const part of path36) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
523
+ for (const part of path37) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
524
524
  return out;
525
525
  }
526
526
  function describeZodError(error, base = "") {
@@ -2150,10 +2150,23 @@ async function buildSite(paths, out, options = {}) {
2150
2150
  inputs.push(file);
2151
2151
  }
2152
2152
  fs12.mkdirSync(out, { recursive: true });
2153
+ const moduleMap = {};
2153
2154
  if (inputs.length) {
2154
2155
  const base = baseConfig(project);
2155
2156
  await build({
2156
2157
  ...base,
2158
+ plugins: [
2159
+ ...base.plugins ?? [],
2160
+ options.moduleMap ? {
2161
+ name: "design-module-map",
2162
+ generateBundle(_, bundle) {
2163
+ for (const [file, chunk2] of Object.entries(bundle)) {
2164
+ if (chunk2.type !== "chunk") continue;
2165
+ moduleMap[file] = (chunk2.moduleIds ?? []).map((id2) => id2.split("?")[0]).filter((id2) => path12.isAbsolute(id2) && !id2.includes(`${path12.sep}node_modules${path12.sep}`)).map((id2) => toPosix(path12.relative(paths.root, id2))).filter((rel) => !rel.startsWith(".."));
2166
+ }
2167
+ }
2168
+ } : null
2169
+ ],
2157
2170
  root: staging,
2158
2171
  base: "./",
2159
2172
  logLevel: "warn",
@@ -2173,6 +2186,7 @@ async function buildSite(paths, out, options = {}) {
2173
2186
  fs12.rmSync(out, { recursive: true, force: true });
2174
2187
  fs12.mkdirSync(out, { recursive: true });
2175
2188
  }
2189
+ if (options.moduleMap) fs12.writeFileSync(path12.join(out, "modules.json"), JSON.stringify(moduleMap));
2176
2190
  for (const { source, dir } of htmlScreens) {
2177
2191
  copyTree(
2178
2192
  path12.dirname(source.file),
@@ -2634,7 +2648,7 @@ var CloudClient = class {
2634
2648
  }
2635
2649
  host;
2636
2650
  token;
2637
- async send(method, path36, init) {
2651
+ async send(method, path37, init) {
2638
2652
  const headers = {
2639
2653
  "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2640
2654
  Accept: "application/json"
@@ -2642,7 +2656,7 @@ var CloudClient = class {
2642
2656
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
2643
2657
  if (init.type) headers["Content-Type"] = init.type;
2644
2658
  try {
2645
- return await fetch(`${this.host}/api${path36}`, {
2659
+ return await fetch(`${this.host}/api${path37}`, {
2646
2660
  method,
2647
2661
  headers,
2648
2662
  body: init.body,
@@ -2663,8 +2677,8 @@ var CloudClient = class {
2663
2677
  throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2664
2678
  }
2665
2679
  /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2666
- async request(method, path36, body) {
2667
- const response = await this.send(method, path36, {
2680
+ async request(method, path37, body) {
2681
+ const response = await this.send(method, path37, {
2668
2682
  body: body === void 0 ? void 0 : JSON.stringify(body),
2669
2683
  type: body === void 0 ? void 0 : "application/json",
2670
2684
  timeout: JSON_TIMEOUT
@@ -2676,7 +2690,7 @@ var CloudClient = class {
2676
2690
  try {
2677
2691
  return JSON.parse(text2);
2678
2692
  } catch {
2679
- throw new CloudError(`${this.host} answered ${method} ${path36} with something other than JSON`, 0, "bad_response");
2693
+ throw new CloudError(`${this.host} answered ${method} ${path37} with something other than JSON`, 0, "bad_response");
2680
2694
  }
2681
2695
  }
2682
2696
  /** POST /device/code. */
@@ -3031,11 +3045,25 @@ var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word
3031
3045
  // src/capture/cover.ts
3032
3046
  import fs20 from "fs";
3033
3047
  import os3 from "os";
3048
+ import path19 from "path";
3034
3049
 
3035
3050
  // src/server/snapshots.ts
3036
3051
  import { createHash as createHash5 } from "crypto";
3037
3052
  import fs17 from "fs";
3038
3053
  import path17 from "path";
3054
+ function depsHash(root, rels, cache = /* @__PURE__ */ new Map()) {
3055
+ const hash = createHash5("sha1");
3056
+ for (const rel of [...rels].sort()) {
3057
+ let one = cache.get(rel);
3058
+ if (one === void 0) {
3059
+ one = sourceHash(path17.join(root, rel)) ?? "missing";
3060
+ cache.set(rel, one);
3061
+ }
3062
+ hash.update(`${rel}\0${one}
3063
+ `);
3064
+ }
3065
+ return hash.digest("hex").slice(0, 16);
3066
+ }
3039
3067
  function sourceHash(file) {
3040
3068
  try {
3041
3069
  return createHash5("sha1").update(fs17.readFileSync(file)).digest("hex").slice(0, 16);
@@ -3103,6 +3131,11 @@ var SnapshotStore = class {
3103
3131
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return void 0;
3104
3132
  return this.load(canvas)[id2]?.sources?.[theme2];
3105
3133
  }
3134
+ /** The files the snapshot's frame loaded, when a build told (see `Meta.deps`). */
3135
+ deps(canvas, id2) {
3136
+ if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return void 0;
3137
+ return this.load(canvas)[id2]?.deps;
3138
+ }
3106
3139
  /** Record `hash` for a snapshot taken before hashes were kept: from now on it counts as that content's. */
3107
3140
  adopt(canvas, id2, theme2, hash) {
3108
3141
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return;
@@ -3112,7 +3145,7 @@ var SnapshotStore = class {
3112
3145
  entry.sources = { ...entry.sources, [theme2]: hash };
3113
3146
  this.writeMeta(canvas, data);
3114
3147
  }
3115
- save(canvas, id2, theme2, png, height, source) {
3148
+ save(canvas, id2, theme2, png, height, source, deps) {
3116
3149
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3117
3150
  const dir = path17.join(this.dir, canvas);
3118
3151
  fs17.mkdirSync(dir, { recursive: true });
@@ -3125,7 +3158,7 @@ var SnapshotStore = class {
3125
3158
  const sources = { ...previous?.sources };
3126
3159
  if (source) sources[theme2] = source;
3127
3160
  else delete sources[theme2];
3128
- data[id2] = { ...previous, [theme2]: version, ...height ? { height } : {}, sources };
3161
+ data[id2] = { ...previous, [theme2]: version, ...height ? { height } : {}, sources, ...deps ? { deps } : {} };
3129
3162
  this.writeMeta(canvas, data);
3130
3163
  return { url: this.url(canvas, id2, theme2, version) };
3131
3164
  }
@@ -3150,7 +3183,8 @@ async function serveBuild(paths, name, options) {
3150
3183
  await buildSite(paths, out, {
3151
3184
  canvases: options.canvases,
3152
3185
  includeSystem: options.includeSystem,
3153
- includeViewer: false
3186
+ includeViewer: false,
3187
+ moduleMap: options.moduleMap
3154
3188
  });
3155
3189
  const files = sirv(out, { dev: true });
3156
3190
  const server = http.createServer((req, res) => {
@@ -3371,6 +3405,7 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3371
3405
  const store = new SnapshotStore(paths.snapshots);
3372
3406
  const project = new DesignProject(paths, DEV_URLS, store.lookup);
3373
3407
  const stale = [];
3408
+ const hashes2 = /* @__PURE__ */ new Map();
3374
3409
  for (const id2 of canvasIds) {
3375
3410
  const canvas = project.canvas(id2);
3376
3411
  if (!canvas) continue;
@@ -3383,7 +3418,8 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3383
3418
  if (filter.screens && !filter.screens.has(job.id)) continue;
3384
3419
  const file = files.get(job.id);
3385
3420
  if (!file || !fs20.existsSync(file)) continue;
3386
- const hash = sourceHash(file);
3421
+ const deps = store.deps(id2, job.id);
3422
+ const hash = deps ? depsHash(paths.root, deps, hashes2) : sourceHash(file);
3387
3423
  const pinned = inDark.get(job.id) === job.theme;
3388
3424
  const themes = pinned ? [job.theme] : THEMES2;
3389
3425
  for (const theme2 of themes) {
@@ -3411,9 +3447,12 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3411
3447
  let docOf = (id2) => project.canvas(id2).doc;
3412
3448
  let stop = async () => {
3413
3449
  };
3450
+ let modules = null;
3451
+ const hashes2 = /* @__PURE__ */ new Map();
3414
3452
  try {
3415
- const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false });
3453
+ const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false, moduleMap: true });
3416
3454
  url = build2.base;
3455
+ modules = build2.read("modules.json");
3417
3456
  docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
3418
3457
  stop = build2.close;
3419
3458
  } catch {
@@ -3434,12 +3473,25 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3434
3473
  deviceScaleFactor: Math.min(1, SNAPSHOT_WIDTH / job.width),
3435
3474
  colorScheme: theme2
3436
3475
  });
3476
+ const loaded = /* @__PURE__ */ new Set();
3477
+ page.on("request", (request) => {
3478
+ const at = request.url();
3479
+ if (at.startsWith(url)) loaded.add(decodeURIComponent(new URL(at).pathname.slice(1)));
3480
+ });
3437
3481
  try {
3438
3482
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
3439
3483
  if (report.ready) {
3440
3484
  const png = await page.screenshot({ fullPage: job.fullPage });
3441
3485
  const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
3442
- store.save(canvas, screen, theme2, png, height, source);
3486
+ const file = project.source(canvas, screen)?.file;
3487
+ const deps = modules ? [
3488
+ .../* @__PURE__ */ new Set([
3489
+ ...[...loaded].flatMap((rel) => modules[rel] ?? []),
3490
+ ...file ? [toPosix(path19.relative(paths.root, file))] : []
3491
+ ])
3492
+ ].sort() : [];
3493
+ const hash = deps.length ? depsHash(paths.root, deps, hashes2) : source;
3494
+ store.save(canvas, screen, theme2, png, height, hash, deps.length ? deps : void 0);
3443
3495
  } else failed.push(`${canvas}/${screen} (${theme2})`);
3444
3496
  } catch {
3445
3497
  failed.push(`${canvas}/${screen} (${theme2})`);
@@ -3466,21 +3518,21 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3466
3518
 
3467
3519
  // src/cloud/pull.ts
3468
3520
  import fs24 from "fs";
3469
- import path22 from "path";
3521
+ import path23 from "path";
3470
3522
 
3471
3523
  // src/cloud/merge.ts
3472
3524
  import fs22 from "fs";
3473
- import path20 from "path";
3525
+ import path21 from "path";
3474
3526
  import { merge as diff3 } from "node-diff3";
3475
3527
 
3476
3528
  // src/cloud/write.ts
3477
3529
  import fs21 from "fs";
3478
- import path19 from "path";
3530
+ import path20 from "path";
3479
3531
  function writeInside(root, rel, data) {
3480
- const file = path19.join(root, rel);
3532
+ const file = path20.join(root, rel);
3481
3533
  if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
3482
- fs21.mkdirSync(path19.dirname(file), { recursive: true });
3483
- if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path19.dirname(file))))
3534
+ fs21.mkdirSync(path20.dirname(file), { recursive: true });
3535
+ if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path20.dirname(file))))
3484
3536
  throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
3485
3537
  if (fs21.existsSync(file) && fs21.lstatSync(file).isSymbolicLink()) fs21.unlinkSync(file);
3486
3538
  fs21.writeFileSync(file, data);
@@ -3494,16 +3546,16 @@ function pruneEmpty(dir, stop) {
3494
3546
  } catch {
3495
3547
  return;
3496
3548
  }
3497
- current = path19.dirname(current);
3549
+ current = path20.dirname(current);
3498
3550
  }
3499
3551
  }
3500
3552
 
3501
3553
  // src/cloud/merge.ts
3502
3554
  var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
3503
3555
  var isText = (rel, ...data) => TEXT.test(rel) && data.every((buffer) => !buffer || !buffer.subarray(0, 8192).includes(0));
3504
- var mergeDir = (paths, key) => path20.join(cloudCache(paths), "merge", ...key.split("/"));
3505
- var stateFile = (paths, key) => path20.join(mergeDir(paths, key), "state.json");
3506
- var sideFile = (paths, key, rel, side) => path20.join(mergeDir(paths, key), side, rel);
3556
+ var mergeDir = (paths, key) => path21.join(cloudCache(paths), "merge", ...key.split("/"));
3557
+ var stateFile = (paths, key) => path21.join(mergeDir(paths, key), "state.json");
3558
+ var sideFile = (paths, key, rel, side) => path21.join(mergeDir(paths, key), side, rel);
3507
3559
  function readMergeState(paths, key) {
3508
3560
  try {
3509
3561
  return JSON.parse(fs22.readFileSync(stateFile(paths, key), "utf8"));
@@ -3517,10 +3569,10 @@ function saveState(paths, state) {
3517
3569
  `);
3518
3570
  }
3519
3571
  function mergingUnits(paths) {
3520
- const root = path20.join(cloudCache(paths), "merge");
3572
+ const root = path21.join(cloudCache(paths), "merge");
3521
3573
  const found = [];
3522
3574
  const visit = (dir, prefix) => {
3523
- if (fs22.existsSync(path20.join(dir, "state.json"))) {
3575
+ if (fs22.existsSync(path21.join(dir, "state.json"))) {
3524
3576
  const state = readMergeState(paths, prefix);
3525
3577
  if (state) found.push(state);
3526
3578
  return;
@@ -3531,7 +3583,7 @@ function mergingUnits(paths) {
3531
3583
  } catch {
3532
3584
  }
3533
3585
  for (const entry of entries)
3534
- if (entry.isDirectory()) visit(path20.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3586
+ if (entry.isDirectory()) visit(path21.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3535
3587
  };
3536
3588
  visit(root, "");
3537
3589
  return found;
@@ -3541,7 +3593,7 @@ function openConflicts(paths, state) {
3541
3593
  return state.conflicts.filter((conflict) => {
3542
3594
  if (conflict.kind !== "text") return true;
3543
3595
  try {
3544
- return MARKER.test(fs22.readFileSync(path20.join(paths.design, conflict.path), "utf8"));
3596
+ return MARKER.test(fs22.readFileSync(path21.join(paths.design, conflict.path), "utf8"));
3545
3597
  } catch {
3546
3598
  return false;
3547
3599
  }
@@ -3550,19 +3602,19 @@ function openConflicts(paths, state) {
3550
3602
  function keep(paths, key, rel, side, data) {
3551
3603
  if (!data) return;
3552
3604
  const file = sideFile(paths, key, rel, side);
3553
- fs22.mkdirSync(path20.dirname(file), { recursive: true });
3605
+ fs22.mkdirSync(path21.dirname(file), { recursive: true });
3554
3606
  fs22.writeFileSync(file, data);
3555
3607
  }
3556
3608
  function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
3557
3609
  const files = { added: 0, changed: 0, removed: 0, merged: 0 };
3558
3610
  const conflicts = [];
3559
- const stop = key === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, key));
3611
+ const stop = key === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, key));
3560
3612
  fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3561
- const read = (rel) => fs22.readFileSync(path20.join(paths.design, rel));
3613
+ const read = (rel) => fs22.readFileSync(path21.join(paths.design, rel));
3562
3614
  const remove = (rel) => {
3563
- const file = path20.join(paths.design, rel);
3615
+ const file = path21.join(paths.design, rel);
3564
3616
  fs22.rmSync(file, { force: true });
3565
- pruneEmpty(path20.dirname(file), stop);
3617
+ pruneEmpty(path21.dirname(file), stop);
3566
3618
  };
3567
3619
  for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
3568
3620
  const b = base[rel];
@@ -3618,11 +3670,11 @@ function resolveConflict(paths, state, rel, side) {
3618
3670
  const conflict = state.conflicts.find((item) => item.path === rel);
3619
3671
  if (!conflict) return;
3620
3672
  const kept = sideFile(paths, state.unit, rel, side);
3621
- const stop = state.unit === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, state.unit));
3673
+ const stop = state.unit === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, state.unit));
3622
3674
  if (fs22.existsSync(kept)) writeInside(paths.design, rel, fs22.readFileSync(kept));
3623
3675
  else {
3624
- fs22.rmSync(path20.join(paths.design, rel), { force: true });
3625
- pruneEmpty(path20.dirname(path20.join(paths.design, rel)), stop);
3676
+ fs22.rmSync(path21.join(paths.design, rel), { force: true });
3677
+ pruneEmpty(path21.dirname(path21.join(paths.design, rel)), stop);
3626
3678
  }
3627
3679
  state.conflicts = state.conflicts.filter((item) => item.path !== rel);
3628
3680
  saveState(paths, state);
@@ -3640,7 +3692,7 @@ function finishMerge(paths, link, key, accept = false) {
3640
3692
 
3641
3693
  // src/cloud/meta.ts
3642
3694
  import fs23 from "fs";
3643
- import path21 from "path";
3695
+ import path22 from "path";
3644
3696
  async function syncProjectMeta(paths, client, link) {
3645
3697
  const remote = await client.project(link.project);
3646
3698
  if (!remote) return [];
@@ -3659,7 +3711,7 @@ async function syncProjectMeta(paths, client, link) {
3659
3711
  if (localChanged && !cloudChanged) {
3660
3712
  try {
3661
3713
  if (file) {
3662
- const ext = path21.extname(file).slice(1);
3714
+ const ext = path22.extname(file).slice(1);
3663
3715
  await client.setProjectIcon(link.project, { data: fs23.readFileSync(file), type: ICON_TYPES[ext] });
3664
3716
  lines.push("The project icon went up to the cloud.");
3665
3717
  } else {
@@ -3695,12 +3747,12 @@ function checkManifest(key, manifest) {
3695
3747
  }
3696
3748
  function applyUnit(paths, key, local, target, blobs) {
3697
3749
  const counts = { added: 0, changed: 0, removed: 0 };
3698
- const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
3750
+ const stop = key === SYSTEM_UNIT ? paths.design : path23.dirname(unitRoot(paths, key));
3699
3751
  for (const rel of Object.keys(local)) {
3700
3752
  if (rel in target) continue;
3701
- const file = path22.join(paths.design, rel);
3753
+ const file = path23.join(paths.design, rel);
3702
3754
  fs24.rmSync(file, { force: true });
3703
- pruneEmpty(path22.dirname(file), stop);
3755
+ pruneEmpty(path23.dirname(file), stop);
3704
3756
  counts.removed++;
3705
3757
  }
3706
3758
  for (const [rel, entry] of Object.entries(target)) {
@@ -3799,7 +3851,7 @@ async function pull(ctx, options) {
3799
3851
  }
3800
3852
  const byHash = /* @__PURE__ */ new Map();
3801
3853
  for (const unit of scan.units.values())
3802
- for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path22.join(paths.design, rel));
3854
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path23.join(paths.design, rel));
3803
3855
  const needed = /* @__PURE__ */ new Set();
3804
3856
  const bases = /* @__PURE__ */ new Set();
3805
3857
  for (const plan of plans) {
@@ -3898,7 +3950,7 @@ async function pull(ctx, options) {
3898
3950
 
3899
3951
  // src/cloud/push.ts
3900
3952
  import fs25 from "fs";
3901
- import path23 from "path";
3953
+ import path24 from "path";
3902
3954
  function blockedReport(status, message) {
3903
3955
  return {
3904
3956
  unit: status.key,
@@ -4014,7 +4066,7 @@ async function push(ctx, options) {
4014
4066
  for (const key of toPush) {
4015
4067
  const s = status(key);
4016
4068
  if (s.conflictRev !== void 0) {
4017
- const dir = path23.relative(paths.root, incomingDir(paths, key));
4069
+ const dir = path24.relative(paths.root, incomingDir(paths, key));
4018
4070
  blocked.push(
4019
4071
  blockedReport(
4020
4072
  s,
@@ -4063,7 +4115,7 @@ async function push(ctx, options) {
4063
4115
  const files = /* @__PURE__ */ new Map();
4064
4116
  const addFiles = (manifest, dir) => {
4065
4117
  for (const [rel, entry] of Object.entries(manifest))
4066
- if (!files.has(entry.hash)) files.set(entry.hash, path23.join(dir, rel));
4118
+ if (!files.has(entry.hash)) files.set(entry.hash, path24.join(dir, rel));
4067
4119
  };
4068
4120
  for (const key of pushing) {
4069
4121
  const id2 = unitCanvasId(key);
@@ -4109,7 +4161,7 @@ async function push(ctx, options) {
4109
4161
  await client.putBlob(link.project, hash, data);
4110
4162
  } catch (error) {
4111
4163
  if (error instanceof CloudError && error.status === 413)
4112
- throw new CliError(`${path23.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4164
+ throw new CliError(`${path24.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4113
4165
  throw error;
4114
4166
  }
4115
4167
  sentFiles++;
@@ -4153,7 +4205,7 @@ function refuseLegacyGo(paths, keys) {
4153
4205
  for (const key of keys) {
4154
4206
  if (!key.startsWith("canvas/")) continue;
4155
4207
  const id2 = key.slice("canvas/".length);
4156
- for (const file of canvasCodeFiles(path23.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4208
+ for (const file of canvasCodeFiles(path24.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4157
4209
  for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
4158
4210
  }
4159
4211
  }
@@ -4174,13 +4226,13 @@ import fs29 from "fs";
4174
4226
  // src/server/index.ts
4175
4227
  import fs28 from "fs";
4176
4228
  import http2 from "http";
4177
- import path26 from "path";
4229
+ import path27 from "path";
4178
4230
  import sirv2 from "sirv";
4179
4231
  import { createServer, searchForWorkspaceRoot } from "vite";
4180
4232
 
4181
4233
  // src/server/api.ts
4182
4234
  import fs26 from "fs";
4183
- import path24 from "path";
4235
+ import path25 from "path";
4184
4236
  function sendJson(res, status, body) {
4185
4237
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
4186
4238
  res.end(JSON.stringify(body));
@@ -4231,7 +4283,7 @@ async function handleApi(ctx, req, res, url) {
4231
4283
  }
4232
4284
  if (route2 === "/source") {
4233
4285
  const rel = url.searchParams.get("path") ?? "";
4234
- const file = path24.resolve(project.paths.root, rel);
4286
+ const file = path25.resolve(project.paths.root, rel);
4235
4287
  if (!rel || !isInside(project.paths.root, file) || !fs26.existsSync(file) || fs26.statSync(file).isDirectory()) {
4236
4288
  sendJson(res, 404, { error: "not found" });
4237
4289
  return true;
@@ -4271,8 +4323,10 @@ async function handleApi(ctx, req, res, url) {
4271
4323
  try {
4272
4324
  const body = await readBody(req);
4273
4325
  if (body.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") throw new Error("not a PNG");
4326
+ const deps = snapshots.deps(canvas, id2);
4274
4327
  const file = project.source(canvas, id2)?.file;
4275
- const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height, file ? sourceHash(file) : null);
4328
+ const source = deps ? depsHash(project.paths.root, deps) : file ? sourceHash(file) : null;
4329
+ const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height, source);
4276
4330
  project.invalidateCanvas(canvas);
4277
4331
  events.send({ type: "snapshot", canvas, id: id2, theme: theme2, url: snapshotUrl, height });
4278
4332
  sendJson(res, 200, { url: snapshotUrl });
@@ -4331,9 +4385,9 @@ var EventHub = class {
4331
4385
  // src/server/state.ts
4332
4386
  import fs27 from "fs";
4333
4387
  import os4 from "os";
4334
- import path25 from "path";
4335
- var stateFile2 = (paths) => path25.join(paths.cache, "server.json");
4336
- var logFile = (paths) => path25.join(paths.cache, "server.log");
4388
+ import path26 from "path";
4389
+ var stateFile2 = (paths) => path26.join(paths.cache, "server.json");
4390
+ var logFile = (paths) => path26.join(paths.cache, "server.log");
4337
4391
  function readState(paths) {
4338
4392
  try {
4339
4393
  return JSON.parse(fs27.readFileSync(stateFile2(paths), "utf8"));
@@ -4342,10 +4396,10 @@ function readState(paths) {
4342
4396
  }
4343
4397
  }
4344
4398
  function registryDir() {
4345
- 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");
4346
- return path25.join(base, "foss-design", "previews");
4399
+ const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path26.join(os4.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path26.join(os4.homedir(), ".cache");
4400
+ return path26.join(base, "foss-design", "previews");
4347
4401
  }
4348
- var entryFile = (pid) => path25.join(registryDir(), `${pid}.json`);
4402
+ var entryFile = (pid) => path26.join(registryDir(), `${pid}.json`);
4349
4403
  function writeState(paths, state) {
4350
4404
  fs27.mkdirSync(paths.cache, { recursive: true });
4351
4405
  fs27.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
@@ -4372,7 +4426,7 @@ function listPreviews() {
4372
4426
  }
4373
4427
  const found = [];
4374
4428
  for (const name of files) {
4375
- const file = path25.join(registryDir(), name);
4429
+ const file = path26.join(registryDir(), name);
4376
4430
  let state = null;
4377
4431
  try {
4378
4432
  state = JSON.parse(fs27.readFileSync(file, "utf8"));
@@ -4384,7 +4438,7 @@ function listPreviews() {
4384
4438
  }
4385
4439
  let recorded = null;
4386
4440
  try {
4387
- recorded = JSON.parse(fs27.readFileSync(path25.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4441
+ recorded = JSON.parse(fs27.readFileSync(path26.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4388
4442
  } catch {
4389
4443
  }
4390
4444
  found.push({ ...state, tracked: recorded?.pid === state.pid });
@@ -4399,7 +4453,7 @@ async function stopPreview(entry) {
4399
4453
  const until = Date.now() + 5e3;
4400
4454
  while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
4401
4455
  fs27.rmSync(entryFile(entry.pid), { force: true });
4402
- const stateAt = path25.join(entry.root, ".design", ".cache", "server.json");
4456
+ const stateAt = path26.join(entry.root, ".design", ".cache", "server.json");
4403
4457
  try {
4404
4458
  if (JSON.parse(fs27.readFileSync(stateAt, "utf8")).pid === entry.pid)
4405
4459
  fs27.rmSync(stateAt, { force: true });
@@ -4488,8 +4542,8 @@ async function startDevServer(options) {
4488
4542
  // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4489
4543
  // generated entries are modules the frames load, so those stay watched.
4490
4544
  ignored: (file) => {
4491
- const rel = toPosix(path26.relative(paths.cache, file));
4492
- const inCache = rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
4545
+ const rel = toPosix(path27.relative(paths.cache, file));
4546
+ const inCache = rel !== "" && !rel.startsWith("..") && !path27.isAbsolute(rel);
4493
4547
  return inCache && rel !== "entries" && !rel.startsWith("entries/");
4494
4548
  }
4495
4549
  },
@@ -4498,7 +4552,7 @@ async function startDevServer(options) {
4498
4552
  forwardConsole: false
4499
4553
  },
4500
4554
  optimizeDeps: {
4501
- entries: entries.files().map((file) => toPosix(path26.relative(paths.design, file)))
4555
+ entries: entries.files().map((file) => toPosix(path27.relative(paths.design, file)))
4502
4556
  }
4503
4557
  });
4504
4558
  const publicDir = project.publicDir();
@@ -4511,7 +4565,7 @@ async function startDevServer(options) {
4511
4565
  }
4512
4566
  };
4513
4567
  const viewer = fs28.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
4514
- const viewerIndex = path26.join(PKG.viewer, "index.html");
4568
+ const viewerIndex = path27.join(PKG.viewer, "index.html");
4515
4569
  const serveViewerIndex = (res) => {
4516
4570
  const html = fs28.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4517
4571
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -4519,7 +4573,7 @@ async function startDevServer(options) {
4519
4573
  };
4520
4574
  const ctx = { project, events, snapshots };
4521
4575
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
4522
- const bootUrl = `/${relToDesign(paths, path26.join(entries.dir, HTML_BOOT))}`;
4576
+ const bootUrl = `/${relToDesign(paths, path27.join(entries.dir, HTML_BOOT))}`;
4523
4577
  const frameHead = (source, extra = {}) => {
4524
4578
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
4525
4579
  return {
@@ -4549,7 +4603,7 @@ async function startDevServer(options) {
4549
4603
  };
4550
4604
  const serveHtmlScreen = async (res, url2) => {
4551
4605
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
4552
- const file = path26.resolve(paths.design, rel);
4606
+ const file = path27.resolve(paths.design, rel);
4553
4607
  if (!isInside(paths.design, file) || !fs28.existsSync(file)) {
4554
4608
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
4555
4609
  return;
@@ -4560,7 +4614,7 @@ async function startDevServer(options) {
4560
4614
  key: `${canvas}/${id2}`,
4561
4615
  canvas,
4562
4616
  id: id2,
4563
- title: path26.basename(file),
4617
+ title: path27.basename(file),
4564
4618
  file,
4565
4619
  format: "html",
4566
4620
  props: {},
@@ -4599,7 +4653,7 @@ async function startDevServer(options) {
4599
4653
  }
4600
4654
  if (url2.pathname.startsWith("/_fs/")) {
4601
4655
  const rel = publicRel(url2.pathname.slice("/_fs/".length));
4602
- if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path26.join(publicDir, rel))) {
4656
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path27.join(publicDir, rel))) {
4603
4657
  notFoundPage(res, "Not found", url2.pathname);
4604
4658
  return;
4605
4659
  }
@@ -4611,7 +4665,7 @@ async function startDevServer(options) {
4611
4665
  const rel = publicRel(url2.pathname.slice(1));
4612
4666
  if (servePublic && rel !== null && project.isPublicFile(rel))
4613
4667
  servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4614
- else if (path26.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4668
+ else if (path27.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4615
4669
  else serveViewerIndex(res);
4616
4670
  };
4617
4671
  if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
@@ -4639,7 +4693,7 @@ async function startDevServer(options) {
4639
4693
  };
4640
4694
  vite.watcher.on("all", (event, file) => {
4641
4695
  if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4642
- const rel = toPosix(path26.relative(paths.design, file));
4696
+ const rel = toPosix(path27.relative(paths.design, file));
4643
4697
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
4644
4698
  if (rel === "design.json") {
4645
4699
  queue("config", () => {
@@ -5364,7 +5418,7 @@ function printLinkIssues(issues, project) {
5364
5418
  // src/cli/commands/cloud.ts
5365
5419
  import fs30 from "fs";
5366
5420
  import os5 from "os";
5367
- import path27 from "path";
5421
+ import path28 from "path";
5368
5422
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5369
5423
  async function runLogin() {
5370
5424
  const host = cloudHost();
@@ -5465,7 +5519,7 @@ async function runLink(paths, wanted, options) {
5465
5519
  print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
5466
5520
  } else {
5467
5521
  writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
5468
- fs30.rmSync(path27.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5522
+ fs30.rmSync(path28.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5469
5523
  const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
5470
5524
  print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
5471
5525
  }
@@ -5493,7 +5547,7 @@ async function runLink(paths, wanted, options) {
5493
5547
 
5494
5548
  // src/cli/commands/icon.ts
5495
5549
  import fs31 from "fs";
5496
- import path28 from "path";
5550
+ import path29 from "path";
5497
5551
  async function toCloud(paths, file) {
5498
5552
  const link = readLink(paths);
5499
5553
  if (!link) return;
@@ -5504,7 +5558,7 @@ async function toCloud(paths, file) {
5504
5558
  }
5505
5559
  const client = new CloudClient(link.host, credential.token);
5506
5560
  try {
5507
- const ext = file ? path28.extname(file).slice(1) : null;
5561
+ const ext = file ? path29.extname(file).slice(1) : null;
5508
5562
  await client.setProjectIcon(
5509
5563
  link.project,
5510
5564
  file && ext ? { data: fs31.readFileSync(file), type: ICON_TYPES[ext] } : null
@@ -5520,14 +5574,14 @@ async function runIcon(paths, file, options) {
5520
5574
  if (options.remove) {
5521
5575
  const had = iconFile(paths);
5522
5576
  writeIcon(paths, null);
5523
- print(had ? `${green("Removed")} ${path28.relative(process.cwd(), had)}` : "The project has no icon.");
5577
+ print(had ? `${green("Removed")} ${path29.relative(process.cwd(), had)}` : "The project has no icon.");
5524
5578
  await toCloud(paths, null);
5525
5579
  return;
5526
5580
  }
5527
5581
  if (!file) {
5528
5582
  const current = iconFile(paths);
5529
5583
  if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
5530
- return print(`${path28.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
5584
+ return print(`${path29.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
5531
5585
  }
5532
5586
  let data;
5533
5587
  try {
@@ -5535,20 +5589,20 @@ async function runIcon(paths, file, options) {
5535
5589
  } catch {
5536
5590
  throw new CliError(`No file ${file}`);
5537
5591
  }
5538
- const problem = iconProblem(data, path28.extname(file).slice(1).toLowerCase() || void 0);
5592
+ const problem = iconProblem(data, path29.extname(file).slice(1).toLowerCase() || void 0);
5539
5593
  if (problem) throw new CliError(`${file}: ${problem}`);
5540
5594
  const written = writeIcon(paths, data);
5541
- print(`${green("Set")} the project icon: ${path28.relative(process.cwd(), written)}`);
5595
+ print(`${green("Set")} the project icon: ${path29.relative(process.cwd(), written)}`);
5542
5596
  await toCloud(paths, written);
5543
5597
  }
5544
5598
 
5545
5599
  // src/cli/commands/import.ts
5546
5600
  import fs33 from "fs";
5547
- import path30 from "path";
5601
+ import path31 from "path";
5548
5602
 
5549
5603
  // src/import/claude-design.ts
5550
5604
  import fs32 from "fs";
5551
- import path29 from "path";
5605
+ import path30 from "path";
5552
5606
 
5553
5607
  // src/import/markup.ts
5554
5608
  var VOID = /* @__PURE__ */ new Set([
@@ -6156,7 +6210,7 @@ function boardId(file, used) {
6156
6210
  return id2;
6157
6211
  }
6158
6212
  function importClaudeDesign(dir, options = {}) {
6159
- const canvasFile = path29.join(dir, "canvas.json");
6213
+ const canvasFile = path30.join(dir, "canvas.json");
6160
6214
  if (!fs32.existsSync(canvasFile))
6161
6215
  throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
6162
6216
  const source = JSON.parse(fs32.readFileSync(canvasFile, "utf8"));
@@ -6187,7 +6241,7 @@ function importClaudeDesign(dir, options = {}) {
6187
6241
  try {
6188
6242
  files.set(
6189
6243
  `${name}.jsx`,
6190
- convertFile(name, fs32.readFileSync(path29.join(dir, file), "utf8"), { known, routes, warn: warn2 })
6244
+ convertFile(name, fs32.readFileSync(path30.join(dir, file), "utf8"), { known, routes, warn: warn2 })
6191
6245
  );
6192
6246
  } catch (error) {
6193
6247
  warn2(`could not be converted: ${error.message}`);
@@ -6235,7 +6289,7 @@ function importClaudeDesign(dir, options = {}) {
6235
6289
  screens++;
6236
6290
  }
6237
6291
  const canvas = {
6238
- title: options.title ?? source.title ?? path29.basename(path29.resolve(dir)),
6292
+ title: options.title ?? source.title ?? path30.basename(path30.resolve(dir)),
6239
6293
  description: "Imported from Claude Design.",
6240
6294
  // The templates bring their own styles; the design system's reset would restyle them.
6241
6295
  system: false,
@@ -6247,7 +6301,7 @@ function importClaudeDesign(dir, options = {}) {
6247
6301
  // src/cli/commands/import.ts
6248
6302
  var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
6249
6303
  function runImport(paths, from, options) {
6250
- const dir = path30.resolve(from);
6304
+ const dir = path31.resolve(from);
6251
6305
  if (!fs33.existsSync(dir) || !fs33.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
6252
6306
  let result;
6253
6307
  try {
@@ -6255,13 +6309,13 @@ function runImport(paths, from, options) {
6255
6309
  } catch (error) {
6256
6310
  throw new CliError(error.message);
6257
6311
  }
6258
- const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path30.basename(dir)) || "imported");
6312
+ const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path31.basename(dir)) || "imported");
6259
6313
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
6260
- const out = path30.join(paths.canvases, id2);
6314
+ const out = path31.join(paths.canvases, id2);
6261
6315
  if (fs33.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
6262
- fs33.mkdirSync(path30.join(out, "screens"), { recursive: true });
6263
- for (const [name, content] of result.files) fs33.writeFileSync(path30.join(out, "screens", name), content);
6264
- fs33.writeFileSync(path30.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
6316
+ fs33.mkdirSync(path31.join(out, "screens"), { recursive: true });
6317
+ for (const [name, content] of result.files) fs33.writeFileSync(path31.join(out, "screens", name), content);
6318
+ fs33.writeFileSync(path31.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
6265
6319
  `);
6266
6320
  print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
6267
6321
  print(
@@ -6281,7 +6335,7 @@ function runImport(paths, from, options) {
6281
6335
 
6282
6336
  // src/cli/commands/merge.ts
6283
6337
  import { spawnSync as spawnSync2 } from "child_process";
6284
- import path31 from "path";
6338
+ import path32 from "path";
6285
6339
  import { createInterface } from "readline/promises";
6286
6340
  var KIND = {
6287
6341
  text: "both sides changed lines, the markers show them",
@@ -6297,7 +6351,7 @@ function matchTargets(paths, states, targets) {
6297
6351
  for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
6298
6352
  continue;
6299
6353
  }
6300
- const rel = [path31.relative(paths.design, path31.resolve(target)), target.replace(/^\.design\//, "")];
6354
+ const rel = [path32.relative(paths.design, path32.resolve(target)), target.replace(/^\.design\//, "")];
6301
6355
  const found = states.flatMap(
6302
6356
  (state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
6303
6357
  );
@@ -6313,7 +6367,7 @@ function printOpen(paths, states) {
6313
6367
  print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
6314
6368
  for (const conflict of open2) {
6315
6369
  print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
6316
- const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path31.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6370
+ const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path32.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6317
6371
  print(` ${dim(sides)}`);
6318
6372
  }
6319
6373
  }
@@ -6394,7 +6448,7 @@ async function runMerge(paths, targets, options) {
6394
6448
  if (answer === "s") break;
6395
6449
  if (answer === "e" && edit) {
6396
6450
  const editor = process.env.VISUAL || process.env.EDITOR || "vi";
6397
- spawnSync2(editor, [path31.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6451
+ spawnSync2(editor, [path32.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6398
6452
  if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
6399
6453
  print(yellow(" Conflict markers are still there."));
6400
6454
  }
@@ -6438,7 +6492,7 @@ async function runRename(paths, name) {
6438
6492
 
6439
6493
  // src/cli/commands/scaffold.ts
6440
6494
  import fs35 from "fs";
6441
- import path32 from "path";
6495
+ import path33 from "path";
6442
6496
 
6443
6497
  // src/cli/templates.ts
6444
6498
  var TOKENS_CSS = `/*
@@ -6650,16 +6704,16 @@ function mainScreen(canvasId) {
6650
6704
  // src/cli/commands/scaffold.ts
6651
6705
  function write(file, content, root) {
6652
6706
  if (fs35.existsSync(file)) {
6653
- print(` ${dim("exists ")} ${path32.relative(root, file)}`);
6707
+ print(` ${dim("exists ")} ${path33.relative(root, file)}`);
6654
6708
  return false;
6655
6709
  }
6656
- fs35.mkdirSync(path32.dirname(file), { recursive: true });
6710
+ fs35.mkdirSync(path33.dirname(file), { recursive: true });
6657
6711
  fs35.writeFileSync(file, content);
6658
- print(` ${green("created")} ${path32.relative(root, file)}`);
6712
+ print(` ${green("created")} ${path33.relative(root, file)}`);
6659
6713
  return true;
6660
6714
  }
6661
6715
  function ensureGitignored(root) {
6662
- const file = path32.join(root, ".gitignore");
6716
+ const file = path33.join(root, ".gitignore");
6663
6717
  const current = fs35.existsSync(file) ? fs35.readFileSync(file, "utf8") : "";
6664
6718
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
6665
6719
  if (covered) return "present";
@@ -6671,11 +6725,11 @@ function ensureGitignored(root) {
6671
6725
  }
6672
6726
  function projectName(root) {
6673
6727
  try {
6674
- const manifest = JSON.parse(fs35.readFileSync(path32.join(root, "package.json"), "utf8"));
6728
+ const manifest = JSON.parse(fs35.readFileSync(path33.join(root, "package.json"), "utf8"));
6675
6729
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
6676
6730
  } catch {
6677
6731
  }
6678
- return path32.basename(root);
6732
+ return path33.basename(root);
6679
6733
  }
6680
6734
  function runInit(root, options) {
6681
6735
  const paths = designPaths(root);
@@ -6700,40 +6754,40 @@ function runSystemInit(root, options) {
6700
6754
  } catch {
6701
6755
  }
6702
6756
  const name = options.name ?? `${base} design system`;
6703
- print(`Scaffolding the design system in ${path32.relative(root, paths.system)}`);
6704
- write(path32.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
6757
+ print(`Scaffolding the design system in ${path33.relative(root, paths.system)}`);
6758
+ write(path33.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
6705
6759
  `, root);
6706
- write(path32.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6760
+ write(path33.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6707
6761
  if (!options.empty) {
6708
- write(path32.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
6709
- write(path32.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
6710
- write(path32.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
6762
+ write(path33.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
6763
+ write(path33.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
6764
+ write(path33.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
6711
6765
  }
6712
6766
  }
6713
6767
  function runNew(root, id2, options) {
6714
6768
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
6715
6769
  const paths = designPaths(root);
6716
- const dir = path32.join(paths.canvases, id2);
6717
- if (fs35.existsSync(path32.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
6770
+ const dir = path33.join(paths.canvases, id2);
6771
+ if (fs35.existsSync(path33.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
6718
6772
  print(`Creating canvas "${id2}"`);
6719
6773
  const title = options.title ?? titleize(id2);
6720
6774
  if (options.empty) {
6721
6775
  write(
6722
- path32.join(dir, "canvas.json"),
6776
+ path33.join(dir, "canvas.json"),
6723
6777
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
6724
6778
  `,
6725
6779
  root
6726
6780
  );
6727
- fs35.mkdirSync(path32.join(dir, "screens"), { recursive: true });
6781
+ fs35.mkdirSync(path33.join(dir, "screens"), { recursive: true });
6728
6782
  } else {
6729
- write(path32.join(dir, "canvas.json"), canvasJson(title), root);
6730
- write(path32.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
6783
+ write(path33.join(dir, "canvas.json"), canvasJson(title), root);
6784
+ write(path33.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
6731
6785
  }
6732
6786
  }
6733
6787
 
6734
6788
  // src/cli/commands/shot.ts
6735
6789
  import fs36 from "fs";
6736
- import path33 from "path";
6790
+ import path34 from "path";
6737
6791
  var OVERVIEW_TIMEOUT = 9e4;
6738
6792
  async function runShot(paths, target, options) {
6739
6793
  const [canvasId = "", screenId] = target.split("/");
@@ -6749,8 +6803,8 @@ async function runShot(paths, target, options) {
6749
6803
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
6750
6804
  }
6751
6805
  const theme2 = options.theme ?? doc.theme ?? "light";
6752
- const outBase = path33.resolve(options.out ?? paths.shots);
6753
- const outDir = path33.join(outBase, canvasId);
6806
+ const outBase = path34.resolve(options.out ?? paths.shots);
6807
+ const outDir = path34.join(outBase, canvasId);
6754
6808
  fs36.mkdirSync(outDir, { recursive: true });
6755
6809
  const base = await serverBase(paths);
6756
6810
  const browser = await launchChrome();
@@ -6773,7 +6827,7 @@ async function runShot(paths, target, options) {
6773
6827
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
6774
6828
  }
6775
6829
  await page.waitForTimeout(400);
6776
- const file = path33.join(outDir, `${pageId}.overview.png`);
6830
+ const file = path34.join(outDir, `${pageId}.overview.png`);
6777
6831
  await page.screenshot({ path: file });
6778
6832
  await page.close();
6779
6833
  print(file);
@@ -6802,7 +6856,7 @@ async function shootFrames(browser, jobs, outDir) {
6802
6856
  let failed = false;
6803
6857
  const results = await mapLimit(jobs, 4, async (job) => {
6804
6858
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
6805
- const file = path33.join(outDir, `${job.id}.${job.theme}.png`);
6859
+ const file = path34.join(outDir, `${job.id}.${job.theme}.png`);
6806
6860
  try {
6807
6861
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
6808
6862
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -6834,7 +6888,7 @@ async function shootSystem(paths, project, id2, options) {
6834
6888
  if (id2 && !all.some((job) => job.id === id2)) {
6835
6889
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
6836
6890
  }
6837
- const outDir = path33.join(path33.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
6891
+ const outDir = path34.join(path34.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
6838
6892
  fs36.mkdirSync(outDir, { recursive: true });
6839
6893
  const base = await serverBase(paths);
6840
6894
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
@@ -6859,6 +6913,16 @@ async function runSnapshots(paths, targets, options) {
6859
6913
  }
6860
6914
  if (options.page && picked.size !== 1)
6861
6915
  throw new CliError("--page needs exactly one canvas: `design snapshots <canvas> --page <id>`");
6916
+ const project = new DesignProject(paths, DEV_URLS);
6917
+ for (const [canvas, screens] of picked) {
6918
+ if (!screens) continue;
6919
+ const ids = new Set(project.canvas(canvas).screens.map((screen) => screen.id));
6920
+ const missing = [...screens].filter((id2) => !ids.has(id2));
6921
+ if (missing.length)
6922
+ throw new CliError(
6923
+ `No screen ${missing.map((id2) => `"${id2}"`).join(", ")} in "${canvas}". Screens: ${[...ids].slice(0, 30).join(", ")}${ids.size > 30 ? ", \u2026" : ""}`
6924
+ );
6925
+ }
6862
6926
  const canvases = picked.size ? [...picked.keys()] : known;
6863
6927
  const stale = canvases.flatMap(
6864
6928
  (canvas) => staleSnapshots(paths, [canvas], { page: options.page, screens: picked.get(canvas) ?? void 0 })
@@ -6875,7 +6939,7 @@ async function runSnapshots(paths, targets, options) {
6875
6939
  }
6876
6940
 
6877
6941
  // src/cli/commands/cloud-status.ts
6878
- import path34 from "path";
6942
+ import path35 from "path";
6879
6943
  var STATE_TEXT = {
6880
6944
  in_sync: green,
6881
6945
  local_changes: yellow,
@@ -6898,7 +6962,7 @@ function detail(paths, status) {
6898
6962
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
6899
6963
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
6900
6964
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
6901
- if (status.conflictRev !== void 0) parts.push(`merge ${path34.relative(paths.root, incomingDir(paths, status.key))}`);
6965
+ if (status.conflictRev !== void 0) parts.push(`merge ${path35.relative(paths.root, incomingDir(paths, status.key))}`);
6902
6966
  return parts.join(" \xB7 ");
6903
6967
  }
6904
6968
  async function printCloudStatus(paths, label) {
@@ -7093,7 +7157,7 @@ ${bold("Environment")}
7093
7157
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
7094
7158
  `;
7095
7159
  function projectPaths(rootOption, requireDesign = true) {
7096
- const found = rootOption ? path35.resolve(rootOption) : findProjectRoot();
7160
+ const found = rootOption ? path36.resolve(rootOption) : findProjectRoot();
7097
7161
  if (!found) {
7098
7162
  if (!requireDesign) return designPaths(process.cwd());
7099
7163
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -7146,7 +7210,7 @@ async function main(argv) {
7146
7210
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
7147
7211
  switch (command) {
7148
7212
  case "init":
7149
- return runInit(values.root ? path35.resolve(values.root) : process.cwd(), {
7213
+ return runInit(values.root ? path36.resolve(values.root) : process.cwd(), {
7150
7214
  name: values.name,
7151
7215
  gitignore: !values["no-gitignore"]
7152
7216
  });
@@ -7258,7 +7322,7 @@ function relaunchFromRealPath() {
7258
7322
  } catch {
7259
7323
  return false;
7260
7324
  }
7261
- const same = (a, b) => path35.resolve(a).toLowerCase() === path35.resolve(b).toLowerCase();
7325
+ const same = (a, b) => path36.resolve(a).toLowerCase() === path36.resolve(b).toLowerCase();
7262
7326
  if (same(realScript, script) && same(realCwd, process.cwd())) return false;
7263
7327
  const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
7264
7328
  stdio: "inherit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foss-design",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
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",