foss-design 0.9.10 → 0.9.11

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
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { spawnSync as spawnSync3 } from "child_process";
5
- import fs37 from "fs";
6
- import path36 from "path";
5
+ import fs38 from "fs";
6
+ import path37 from "path";
7
7
  import { parseArgs } from "util";
8
8
 
9
9
  // src/core/paths.ts
@@ -136,12 +136,12 @@ function resolveFrom(anchor, request) {
136
136
 
137
137
  // src/cli/commands/build.ts
138
138
  import { spawnSync } from "child_process";
139
- import fs13 from "fs";
140
- import path13 from "path";
139
+ import fs14 from "fs";
140
+ import path14 from "path";
141
141
 
142
142
  // src/build/static.ts
143
- import fs12 from "fs";
144
- import path12 from "path";
143
+ import fs13 from "fs";
144
+ import path13 from "path";
145
145
  import { build } from "vite";
146
146
 
147
147
  // src/core/png.ts
@@ -297,8 +297,8 @@ function ensureThumb(file) {
297
297
  const thumb = thumbPath(file);
298
298
  try {
299
299
  const source = fs2.statSync(file);
300
- const existing = fs2.statSync(thumb, { throwIfNoEntry: false });
301
- if (existing && existing.mtimeMs >= source.mtimeMs) return thumb;
300
+ const existing2 = fs2.statSync(thumb, { throwIfNoEntry: false });
301
+ if (existing2 && existing2.mtimeMs >= source.mtimeMs) return thumb;
302
302
  const small = thumbnail(fs2.readFileSync(file));
303
303
  if (!small) return file;
304
304
  fs2.writeFileSync(thumb, small);
@@ -348,8 +348,8 @@ function routeError(pattern) {
348
348
  }
349
349
  return null;
350
350
  }
351
- function split(path37) {
352
- return path37.split("/").filter(Boolean);
351
+ function split(path38) {
352
+ return path38.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, path37) {
372
- const parts = split(path37.split(/[?#]/)[0] ?? "").map(decode2);
371
+ function scoreRoute(route2, path38) {
372
+ const parts = split(path38.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, path37) {
387
387
  };
388
388
  return walk2(0, 0);
389
389
  }
390
- function matchRoute(entries, path37) {
390
+ function matchRoute(entries, path38) {
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, path37);
394
+ const score = scoreRoute(route2, path38);
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, path37) {
521
+ function formatPath(base, path38) {
522
522
  let out = base;
523
- for (const part of path37) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
523
+ for (const part of path38) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
524
524
  return out;
525
525
  }
526
526
  function describeZodError(error, base = "") {
@@ -1269,6 +1269,16 @@ var SPECIMEN_EXT = /* @__PURE__ */ new Set([".tsx", ".jsx", ".html"]);
1269
1269
  var COMPONENT_EXT = [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html"];
1270
1270
  var IMAGE_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"]);
1271
1271
  var FONT_EXT = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
1272
+ function isShowableSource(paths, file) {
1273
+ if (!isInside(paths.root, file)) return false;
1274
+ const parts = path7.relative(paths.root, file).split(path7.sep);
1275
+ if (parts.some((part, index) => part.startsWith(".") && !(index === 0 && part === DESIGN_DIR))) return false;
1276
+ try {
1277
+ return fs7.statSync(file).isFile() && isInside(fs7.realpathSync(paths.root), fs7.realpathSync(file));
1278
+ } catch {
1279
+ return false;
1280
+ }
1281
+ }
1272
1282
  function listFiles(dir, deep = false) {
1273
1283
  if (!fs7.existsSync(dir)) return [];
1274
1284
  const out = [];
@@ -1343,8 +1353,14 @@ function loadSystem(paths, urls) {
1343
1353
  const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path7.resolve(path7.dirname(file), value));
1344
1354
  for (const source of sources) {
1345
1355
  if (!fs7.existsSync(source)) report("warning", file, `@source not found: ${path7.relative(paths.system, source)}`);
1356
+ else if (!isShowableSource(paths, source))
1357
+ report(
1358
+ "warning",
1359
+ file,
1360
+ `@source not shown (outside the project, or a dot-file): ${path7.relative(paths.system, source)}`
1361
+ );
1346
1362
  }
1347
- const found = sources.filter((source) => fs7.existsSync(source));
1363
+ const found = sources.filter((source) => isShowableSource(paths, source));
1348
1364
  const title = tags.title?.[0] || titleize(stem);
1349
1365
  const format = ext === ".html" ? "html" : "module";
1350
1366
  components.push({
@@ -1354,7 +1370,7 @@ function loadSystem(paths, urls) {
1354
1370
  description: tags.description?.[0] || void 0,
1355
1371
  status: tags.status?.[0] || void 0,
1356
1372
  specimen: relToRoot(paths, file),
1357
- sources: (found.length ? found : guessSources(componentsDir, stem)).map((source) => relToRoot(paths, source)),
1373
+ sources: (found.length ? found : guessSources(componentsDir, stem)).filter((source) => isShowableSource(paths, source)).map((source) => relToRoot(paths, source)),
1358
1374
  url: format === "module" ? urls.screen(SYSTEM_CANVAS, id2) : urls.html(relToDesign(paths, file), SYSTEM_CANVAS, id2)
1359
1375
  });
1360
1376
  specimens.push({
@@ -1957,10 +1973,156 @@ function linkShippedPackages(paths, appDir = null) {
1957
1973
 
1958
1974
  // src/server/vite.ts
1959
1975
  import { createHash as createHash3 } from "crypto";
1960
- import fs11 from "fs";
1961
- import path11 from "path";
1976
+ import fs12 from "fs";
1977
+ import path12 from "path";
1962
1978
  import tailwindcss from "@tailwindcss/vite";
1963
1979
  import react from "@vitejs/plugin-react";
1980
+
1981
+ // src/server/css-guard.ts
1982
+ import fs11 from "fs";
1983
+ import { createRequire as createRequire2 } from "module";
1984
+ import path11 from "path";
1985
+ import { createIdResolver } from "vite";
1986
+ var LOADS_CODE = /@(plugin|config)\b([^;{}]*)|\buse\(\s*(["'])(.*?)\3/gi;
1987
+ var IMPORTS = /@(import|reference|require)\b([^;{}]*)/gi;
1988
+ var STYLESHEET = /\.(css|less|styl|stylus)$/i;
1989
+ var UNTOUCHED = /[?&](?:worker|sharedworker|raw|url)\b/;
1990
+ var JS_EXT = ["", ".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", "/index.js", "/index.mjs", "/index.cjs", "/index.ts"];
1991
+ var STYLE_EXT = ["", ".css", ".less", ".styl", "/index.css", "/index.styl"];
1992
+ var idFile = (id2) => id2.replace(/[?#].*$/, "");
1993
+ function isStylesheetId(id2) {
1994
+ if (UNTOUCHED.test(id2) || id2.startsWith("\0") || id2.includes("/.vite/")) return false;
1995
+ return STYLESHEET.test(idFile(id2)) || /&lang\.(css|less|styl)/.test(id2) || /[?&]index=\d+\.css$/.test(id2);
1996
+ }
1997
+ var stripComments = (text2) => text2.replace(/\/\*[\s\S]*?(\*\/|$)/g, "");
1998
+ function targets(params) {
1999
+ const out = /* @__PURE__ */ new Set();
2000
+ for (const text2 of [params.trim(), stripComments(params).trim()]) {
2001
+ if (!text2) continue;
2002
+ out.add(text2.slice(1, -1));
2003
+ out.add(text2.slice(1, -1).trim());
2004
+ const quoted = /(["'])(.*?)\1/.exec(text2)?.[2];
2005
+ if (quoted) out.add(quoted);
2006
+ const url = /^url\(\s*(["']?)(.*?)\1\s*\)/.exec(text2)?.[2];
2007
+ if (url) out.add(url);
2008
+ const bare = /^[^\s"'()]+/.exec(text2)?.[0];
2009
+ if (bare) out.add(bare);
2010
+ }
2011
+ out.delete("");
2012
+ return [...out];
2013
+ }
2014
+ var isPathLike = (spec) => spec.startsWith(".") || path11.isAbsolute(spec);
2015
+ function existing(base, exts) {
2016
+ for (const ext of exts) {
2017
+ const file = base + ext;
2018
+ try {
2019
+ if (fs11.statSync(file).isFile()) return fs11.realpathSync(file);
2020
+ } catch {
2021
+ }
2022
+ }
2023
+ return null;
2024
+ }
2025
+ function designCodeGuard(design) {
2026
+ let config = null;
2027
+ const pulled = (file) => isInside(design, file) && !toPosix(file).split("/").includes("node_modules");
2028
+ const shown = (file) => toPosix(path11.relative(path11.dirname(design), file));
2029
+ const resolvers = (environment) => {
2030
+ const resolveWith = (options, accept) => {
2031
+ const resolveId = config ? createIdResolver(config, options) : null;
2032
+ return async (spec, importer) => {
2033
+ if (!resolveId) return null;
2034
+ const from = path11.join(path11.dirname(importer), "__placeholder__.ts");
2035
+ for (const aliasOnly of [true, false]) {
2036
+ try {
2037
+ let found = await resolveId(environment, spec, from, aliasOnly);
2038
+ if (found && found !== spec) {
2039
+ if (found.startsWith(".")) found = path11.resolve(path11.dirname(importer), found);
2040
+ found = idFile(found);
2041
+ if (path11.isAbsolute(found) && accept(found)) return found;
2042
+ }
2043
+ } catch {
2044
+ }
2045
+ }
2046
+ return null;
2047
+ };
2048
+ };
2049
+ const style = resolveWith(
2050
+ {
2051
+ ...config?.resolve,
2052
+ extensions: [".css"],
2053
+ mainFields: ["style"],
2054
+ conditions: ["style", "development|production"],
2055
+ tryIndex: false,
2056
+ preferRelative: true
2057
+ },
2058
+ (file) => file.endsWith(".css")
2059
+ );
2060
+ const script = resolveWith({ ...config?.resolve }, (file) => !file.endsWith(".css"));
2061
+ return { style, script };
2062
+ };
2063
+ const candidates = async (spec, importer, resolve, exts) => {
2064
+ const found = /* @__PURE__ */ new Set();
2065
+ const local = path11.resolve(path11.dirname(importer), spec);
2066
+ if (isPathLike(spec)) found.add(local);
2067
+ const file = existing(local, exts);
2068
+ if (file) found.add(file);
2069
+ const viaVite = await resolve(spec, importer);
2070
+ if (viaVite) found.add(viaVite);
2071
+ if (!isPathLike(spec)) {
2072
+ try {
2073
+ found.add(createRequire2(importer).resolve(spec));
2074
+ } catch {
2075
+ }
2076
+ }
2077
+ return [...found];
2078
+ };
2079
+ const check = async (environment, file, code, seen) => {
2080
+ const { style, script } = resolvers(environment);
2081
+ for (const match of code.matchAll(LOADS_CODE)) {
2082
+ const directive = match[1] ? `@${match[1]}` : "use()";
2083
+ const specs = match[1] ? targets(match[2] ?? "") : [match[4] ?? ""];
2084
+ for (const spec of specs) {
2085
+ for (const target of await candidates(spec, file, script, JS_EXT)) {
2086
+ if (!pulled(target)) continue;
2087
+ throw new Error(
2088
+ `${shown(file)}: \`${match[0].trim()}\` loads ${shown(target)}, which is refused. ${directive} runs its code in Node on this machine, and code under .design can come from a teammate through \`design pull\`. Put the plugin in the app\u2019s own source (outside .design) or name an installed package.`
2089
+ );
2090
+ }
2091
+ }
2092
+ }
2093
+ for (const match of code.matchAll(IMPORTS)) {
2094
+ for (const spec of targets(match[2] ?? "")) {
2095
+ if (/^(data:|https?:|\/\/)/i.test(spec)) continue;
2096
+ for (const target of await candidates(spec, file, style, STYLE_EXT)) {
2097
+ if (seen.has(target) || !STYLESHEET.test(target) || toPosix(target).includes("/node_modules/")) continue;
2098
+ seen.add(target);
2099
+ let text2;
2100
+ try {
2101
+ text2 = fs11.readFileSync(target, "utf8");
2102
+ } catch {
2103
+ continue;
2104
+ }
2105
+ await check(environment, target, text2, seen);
2106
+ }
2107
+ }
2108
+ }
2109
+ };
2110
+ return {
2111
+ name: "design:code-guard",
2112
+ enforce: "pre",
2113
+ configResolved(resolved) {
2114
+ config = resolved;
2115
+ },
2116
+ async transform(code, id2) {
2117
+ if (!isStylesheetId(id2)) return null;
2118
+ const file = path11.resolve(idFile(id2));
2119
+ await check(this.environment, file, code, /* @__PURE__ */ new Set([file]));
2120
+ return null;
2121
+ }
2122
+ };
2123
+ }
2124
+
2125
+ // src/server/vite.ts
1964
2126
  var escapeRegExp = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1965
2127
  function aliases(project) {
1966
2128
  const { paths } = project;
@@ -1971,7 +2133,7 @@ function aliases(project) {
1971
2133
  { find: /^@system\//, replacement: `${toPosix(paths.system)}/` }
1972
2134
  ];
1973
2135
  for (const [key, target] of Object.entries(project.config().config.alias ?? {})) {
1974
- const replacement = toPosix(path11.resolve(paths.root, target));
2136
+ const replacement = toPosix(path12.resolve(paths.root, target));
1975
2137
  list.push(
1976
2138
  key.endsWith("/") ? { find: new RegExp(`^${escapeRegExp(key)}`), replacement: `${replacement.replace(/\/$/, "")}/` } : { find: new RegExp(`^${escapeRegExp(key)}(?=/|$)`), replacement }
1977
2139
  );
@@ -1979,23 +2141,23 @@ function aliases(project) {
1979
2141
  return list;
1980
2142
  }
1981
2143
  function dropStaleDepCache(paths, appDir = null) {
1982
- const dir = path11.join(paths.cache, "vite");
1983
- const stamp = path11.join(paths.cache, "vite-runtime");
2144
+ const dir = path12.join(paths.cache, "vite");
2145
+ const stamp = path12.join(paths.cache, "vite-runtime");
1984
2146
  let runtime = "";
1985
2147
  try {
1986
- runtime = createHash3("sha256").update(fs11.readFileSync(PKG.runtime)).digest("hex");
2148
+ runtime = createHash3("sha256").update(fs12.readFileSync(PKG.runtime)).digest("hex");
1987
2149
  } catch {
1988
2150
  }
1989
2151
  if (appDir) runtime += ` ${appDir}`;
1990
2152
  let seen = "";
1991
2153
  try {
1992
- seen = fs11.readFileSync(stamp, "utf8");
2154
+ seen = fs12.readFileSync(stamp, "utf8");
1993
2155
  } catch {
1994
2156
  }
1995
2157
  if (seen === runtime) return;
1996
- fs11.rmSync(dir, { recursive: true, force: true });
1997
- fs11.mkdirSync(paths.cache, { recursive: true });
1998
- fs11.writeFileSync(stamp, runtime);
2158
+ fs12.rmSync(dir, { recursive: true, force: true });
2159
+ fs12.mkdirSync(paths.cache, { recursive: true });
2160
+ fs12.writeFileSync(stamp, runtime);
1999
2161
  }
2000
2162
  function baseConfig(project) {
2001
2163
  const { paths } = project;
@@ -2003,7 +2165,7 @@ function baseConfig(project) {
2003
2165
  configFile: false,
2004
2166
  envDir: false,
2005
2167
  root: paths.design,
2006
- cacheDir: path11.join(paths.cache, "vite"),
2168
+ cacheDir: path12.join(paths.cache, "vite"),
2007
2169
  // design.json `public`: served under the frames' base, so HTML and CSS paths such as `/logo.png` resolve.
2008
2170
  publicDir: project.publicDir() ?? false,
2009
2171
  logLevel: "warn",
@@ -2013,7 +2175,12 @@ function baseConfig(project) {
2013
2175
  // One React for screens, the runtime and project components: resolved from `.design`.
2014
2176
  dedupe: ["react", "react-dom"]
2015
2177
  },
2016
- plugins: [react(), tailwindcss()]
2178
+ // No PostCSS config is looked up (Vite would search from `.design` upward and load it in Node):
2179
+ // screens get Tailwind from the plugin, and the app's own config is for the app's build.
2180
+ css: { postcss: {} },
2181
+ // Pulled code runs in the browser only: the guard refuses stylesheets that would load code from
2182
+ // `.design` in Node (Tailwind's `@plugin` and `@config`), before Tailwind reads them.
2183
+ plugins: [react(), designCodeGuard(paths.design), tailwindcss()]
2017
2184
  };
2018
2185
  }
2019
2186
 
@@ -2023,7 +2190,7 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".jsx", ".mts", ".cts",
2023
2190
  var BUNDLED_REF = /<(script|link|img|source|video|audio|image|use|input)\b[^>]*>/gi;
2024
2191
  var encodePath2 = (rel) => rel.split("/").map(encodeURIComponent).join("/");
2025
2192
  function relUrl(fromDir, file) {
2026
- const rel = toPosix(path12.relative(fromDir, file));
2193
+ const rel = toPosix(path13.relative(fromDir, file));
2027
2194
  return rel.startsWith(".") ? rel : `./${rel}`;
2028
2195
  }
2029
2196
  function staticSnapshots(paths) {
@@ -2033,7 +2200,7 @@ function staticSnapshots(paths) {
2033
2200
  let data = metaCache.get(canvas);
2034
2201
  if (!data) {
2035
2202
  try {
2036
- data = JSON.parse(fs12.readFileSync(path12.join(paths.snapshots, canvas, "meta.json"), "utf8"));
2203
+ data = JSON.parse(fs13.readFileSync(path13.join(paths.snapshots, canvas, "meta.json"), "utf8"));
2037
2204
  } catch {
2038
2205
  }
2039
2206
  data ??= {};
@@ -2047,8 +2214,8 @@ function staticSnapshots(paths) {
2047
2214
  const urls = {};
2048
2215
  const thumbs = {};
2049
2216
  for (const theme2 of THEMES) {
2050
- const file = path12.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
2051
- if (!entry[theme2] || !fs12.existsSync(file)) continue;
2217
+ const file = path13.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
2218
+ if (!entry[theme2] || !fs13.existsSync(file)) continue;
2052
2219
  const rel = `_snap/${canvas}/${id2}.${theme2}.png`;
2053
2220
  copies.set(file, rel);
2054
2221
  urls[theme2] = `${encodePath2(rel)}?v=${entry[theme2]}`;
@@ -2072,29 +2239,29 @@ function rewriteRefs(html, originalDir, stagedDir) {
2072
2239
  (whole, attr2, quote, value) => {
2073
2240
  if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|#|\{)/i.test(value)) return whole;
2074
2241
  const [pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value).slice(1);
2075
- const target = path12.resolve(originalDir, decodeURIComponent(pathname));
2076
- if (!fs12.existsSync(target)) return whole;
2242
+ const target = path13.resolve(originalDir, decodeURIComponent(pathname));
2243
+ if (!fs13.existsSync(target)) return whole;
2077
2244
  return `${attr2}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
2078
2245
  }
2079
2246
  );
2080
2247
  });
2081
2248
  }
2082
2249
  function copyTree(from, to, filter) {
2083
- if (!fs12.existsSync(from)) return;
2084
- for (const entry of fs12.readdirSync(from, { withFileTypes: true })) {
2250
+ if (!fs13.existsSync(from)) return;
2251
+ for (const entry of fs13.readdirSync(from, { withFileTypes: true })) {
2085
2252
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2086
- const source = path12.join(from, entry.name);
2087
- const target = path12.join(to, entry.name);
2253
+ const source = path13.join(from, entry.name);
2254
+ const target = path13.join(to, entry.name);
2088
2255
  if (entry.isDirectory()) copyTree(source, target, filter);
2089
- else if (filter(source) && !fs12.existsSync(target)) {
2090
- fs12.mkdirSync(path12.dirname(target), { recursive: true });
2091
- fs12.copyFileSync(source, target);
2256
+ else if (filter(source) && !fs13.existsSync(target)) {
2257
+ fs13.mkdirSync(path13.dirname(target), { recursive: true });
2258
+ fs13.copyFileSync(source, target);
2092
2259
  }
2093
2260
  }
2094
2261
  }
2095
2262
  function writeJson(file, value) {
2096
- fs12.mkdirSync(path12.dirname(file), { recursive: true });
2097
- fs12.writeFileSync(file, JSON.stringify(value));
2263
+ fs13.mkdirSync(path13.dirname(file), { recursive: true });
2264
+ fs13.writeFileSync(file, JSON.stringify(value));
2098
2265
  }
2099
2266
  async function buildSite(paths, out, options = {}) {
2100
2267
  const snapshots = staticSnapshots(paths);
@@ -2112,18 +2279,18 @@ async function buildSite(paths, out, options = {}) {
2112
2279
  linkShippedPackages(paths, project.appDir());
2113
2280
  const publicDir = project.publicDir();
2114
2281
  const publicFiles2 = project.publicFiles();
2115
- const staging = path12.join(paths.cache, "build", "src");
2116
- fs12.rmSync(staging, { recursive: true, force: true });
2282
+ const staging = path13.join(paths.cache, "build", "src");
2283
+ fs13.rmSync(staging, { recursive: true, force: true });
2117
2284
  const fonts = loadedSystem?.doc.fonts ?? [];
2118
2285
  const sources = [...system?.specimens ?? [], ...canvases.flatMap((canvas) => canvas.screens)];
2119
2286
  const inputs = [];
2120
2287
  const htmlScreens = [];
2121
2288
  for (const source of sources) {
2122
- if (!source.builtin && !fs12.existsSync(source.file)) continue;
2123
- const siteDir = path12.join("_s", source.canvas, source.id);
2124
- const dir = path12.join(staging, siteDir);
2125
- const file = path12.join(dir, "index.html");
2126
- fs12.mkdirSync(dir, { recursive: true });
2289
+ if (!source.builtin && !fs13.existsSync(source.file)) continue;
2290
+ const siteDir = path13.join("_s", source.canvas, source.id);
2291
+ const dir = path13.join(staging, siteDir);
2292
+ const file = path13.join(dir, "index.html");
2293
+ fs13.mkdirSync(dir, { recursive: true });
2127
2294
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
2128
2295
  const head = {
2129
2296
  source,
@@ -2132,24 +2299,24 @@ async function buildSite(paths, out, options = {}) {
2132
2299
  snapshots: false,
2133
2300
  autoHeight: source.autoHeight ?? false,
2134
2301
  // The public folder lands at the site root, which frames reach through `../`.
2135
- public: publicFiles2.length ? { base: `${toPosix(path12.relative(dir, staging))}/`, files: publicFiles2 } : void 0
2302
+ public: publicFiles2.length ? { base: `${toPosix(path13.relative(dir, staging))}/`, files: publicFiles2 } : void 0
2136
2303
  };
2137
2304
  if (source.format === "module") {
2138
- fs12.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
2305
+ fs13.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
2139
2306
  } else {
2140
- const raw = rewriteRefs(fs12.readFileSync(source.file, "utf8"), path12.dirname(source.file), dir);
2307
+ const raw = rewriteRefs(fs13.readFileSync(source.file, "utf8"), path13.dirname(source.file), dir);
2141
2308
  const withSystem = source.system && !optsOutOfSystem(raw);
2142
2309
  const html = injectIntoHtml(raw, {
2143
2310
  ...head,
2144
2311
  systemCss: withSystem ? relUrl(dir, entries.systemCssFile()) : void 0,
2145
- boot: relUrl(dir, path12.join(entries.dir, HTML_BOOT))
2312
+ boot: relUrl(dir, path13.join(entries.dir, HTML_BOOT))
2146
2313
  });
2147
- fs12.writeFileSync(file, html);
2314
+ fs13.writeFileSync(file, html);
2148
2315
  htmlScreens.push({ source, dir: siteDir });
2149
2316
  }
2150
2317
  inputs.push(file);
2151
2318
  }
2152
- fs12.mkdirSync(out, { recursive: true });
2319
+ fs13.mkdirSync(out, { recursive: true });
2153
2320
  const moduleMap = {};
2154
2321
  if (inputs.length) {
2155
2322
  const base = baseConfig(project);
@@ -2162,7 +2329,7 @@ async function buildSite(paths, out, options = {}) {
2162
2329
  generateBundle(_, bundle) {
2163
2330
  for (const [file, chunk2] of Object.entries(bundle)) {
2164
2331
  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(".."));
2332
+ moduleMap[file] = (chunk2.moduleIds ?? []).map((id2) => id2.split("?")[0]).filter((id2) => path13.isAbsolute(id2) && !id2.includes(`${path13.sep}node_modules${path13.sep}`)).map((id2) => toPosix(path13.relative(paths.root, id2))).filter((rel) => !rel.startsWith(".."));
2166
2333
  }
2167
2334
  }
2168
2335
  } : null
@@ -2183,15 +2350,15 @@ async function buildSite(paths, out, options = {}) {
2183
2350
  }
2184
2351
  });
2185
2352
  } else {
2186
- fs12.rmSync(out, { recursive: true, force: true });
2187
- fs12.mkdirSync(out, { recursive: true });
2353
+ fs13.rmSync(out, { recursive: true, force: true });
2354
+ fs13.mkdirSync(out, { recursive: true });
2188
2355
  }
2189
- if (options.moduleMap) fs12.writeFileSync(path12.join(out, "modules.json"), JSON.stringify(moduleMap));
2356
+ if (options.moduleMap) fs13.writeFileSync(path13.join(out, "modules.json"), JSON.stringify(moduleMap));
2190
2357
  for (const { source, dir } of htmlScreens) {
2191
2358
  copyTree(
2192
- path12.dirname(source.file),
2193
- path12.join(out, dir),
2194
- (file) => !SOURCE_EXT.has(path12.extname(file).toLowerCase())
2359
+ path13.dirname(source.file),
2360
+ path13.join(out, dir),
2361
+ (file) => !SOURCE_EXT.has(path13.extname(file).toLowerCase())
2195
2362
  );
2196
2363
  }
2197
2364
  const files = /* @__PURE__ */ new Set();
@@ -2207,36 +2374,36 @@ async function buildSite(paths, out, options = {}) {
2207
2374
  if (icon) files.add(icon.split("?")[0]);
2208
2375
  for (const url of files) {
2209
2376
  const rel = url.slice("_f/".length).split("/").map(decodeURIComponent).join("/");
2210
- const from = path12.join(paths.design, rel);
2211
- if (!fs12.existsSync(from)) continue;
2212
- const to = path12.join(out, "_f", rel);
2213
- fs12.mkdirSync(path12.dirname(to), { recursive: true });
2214
- fs12.copyFileSync(from, to);
2377
+ const from = path13.join(paths.design, rel);
2378
+ if (!fs13.existsSync(from)) continue;
2379
+ const to = path13.join(out, "_f", rel);
2380
+ fs13.mkdirSync(path13.dirname(to), { recursive: true });
2381
+ fs13.copyFileSync(from, to);
2215
2382
  }
2216
2383
  const info = project.info(true);
2217
2384
  const snapshotDirs = canvasIds.map((id2) => `_snap/${id2}/`);
2218
2385
  for (const [from, rel] of snapshots.copies) {
2219
2386
  if (!snapshotDirs.some((dir) => rel.startsWith(dir))) continue;
2220
- const to = path12.join(out, rel);
2221
- fs12.mkdirSync(path12.dirname(to), { recursive: true });
2222
- fs12.copyFileSync(from, to);
2387
+ const to = path13.join(out, rel);
2388
+ fs13.mkdirSync(path13.dirname(to), { recursive: true });
2389
+ fs13.copyFileSync(from, to);
2223
2390
  }
2224
2391
  if (!system) info.system = null;
2225
2392
  info.canvases = info.canvases.filter((canvas) => canvasIds.includes(canvas.id));
2226
- writeJson(path12.join(out, "api", "project.json"), info);
2227
- for (const canvas of canvases) writeJson(path12.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
2393
+ writeJson(path13.join(out, "api", "project.json"), info);
2394
+ for (const canvas of canvases) writeJson(path13.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
2228
2395
  if (system) {
2229
- writeJson(path12.join(out, "api", "system.json"), system.doc);
2396
+ writeJson(path13.join(out, "api", "system.json"), system.doc);
2230
2397
  const texts = {};
2231
2398
  for (const component of system.doc.components) {
2232
2399
  for (const rel of [component.specimen, ...component.sources]) {
2233
- const file = path12.join(paths.root, rel);
2234
- if (!(rel in texts) && fs12.existsSync(file)) texts[rel] = fs12.readFileSync(file, "utf8");
2400
+ const file = path13.join(paths.root, rel);
2401
+ if (!(rel in texts) && fs13.existsSync(file)) texts[rel] = fs13.readFileSync(file, "utf8");
2235
2402
  }
2236
2403
  }
2237
- writeJson(path12.join(out, "api", "sources.json"), texts);
2404
+ writeJson(path13.join(out, "api", "sources.json"), texts);
2238
2405
  }
2239
- fs12.rmSync(staging, { recursive: true, force: true });
2406
+ fs13.rmSync(staging, { recursive: true, force: true });
2240
2407
  const errors = [
2241
2408
  ...only ? [] : project.config().issues,
2242
2409
  ...system?.doc.issues ?? [],
@@ -2245,21 +2412,21 @@ async function buildSite(paths, out, options = {}) {
2245
2412
  const { absoluteAssets } = options.includeViewer === false ? { absoluteAssets: false } : copyViewer(PKG.viewer, out);
2246
2413
  if (publicDir) {
2247
2414
  for (const rel of publicFiles2) {
2248
- const to = path12.join(out, rel);
2249
- if (fs12.existsSync(to)) continue;
2250
- fs12.mkdirSync(path12.dirname(to), { recursive: true });
2251
- fs12.copyFileSync(path12.join(publicDir, rel), to);
2415
+ const to = path13.join(out, rel);
2416
+ if (fs13.existsSync(to)) continue;
2417
+ fs13.mkdirSync(path13.dirname(to), { recursive: true });
2418
+ fs13.copyFileSync(path13.join(publicDir, rel), to);
2252
2419
  }
2253
2420
  }
2254
2421
  return { out, canvases: canvasIds, frames: inputs.length, errors, absoluteAssets };
2255
2422
  }
2256
2423
  function copyViewer(viewerDir, out) {
2257
2424
  copyTree(viewerDir, out, () => true);
2258
- const index = path12.join(out, "index.html");
2259
- const html = fs12.readFileSync(index, "utf8");
2425
+ const index = path13.join(out, "index.html");
2426
+ const html = fs13.readFileSync(index, "utf8");
2260
2427
  const flag2 = "<script>window.__DESIGN_STATIC__=true</script>";
2261
2428
  const head = /<head(\s[^>]*)?>/i.exec(html);
2262
- fs12.writeFileSync(
2429
+ fs13.writeFileSync(
2263
2430
  index,
2264
2431
  head ? html.slice(0, head.index + head[0].length) + flag2 + html.slice(head.index + head[0].length) : flag2 + html
2265
2432
  );
@@ -2341,8 +2508,8 @@ function progress() {
2341
2508
  // src/cli/commands/build.ts
2342
2509
  function pack(out) {
2343
2510
  const archive = `${out.replace(/[\\/]+$/, "")}.tar.gz`;
2344
- fs13.rmSync(archive, { force: true });
2345
- const entries = fs13.readdirSync(out);
2511
+ fs14.rmSync(archive, { force: true });
2512
+ const entries = fs14.readdirSync(out);
2346
2513
  const result = spawnSync("tar", ["-czf", archive, "-C", out, ...entries], {
2347
2514
  stdio: ["ignore", "ignore", "pipe"],
2348
2515
  env: { ...process.env, COPYFILE_DISABLE: "1" }
@@ -2352,10 +2519,10 @@ function pack(out) {
2352
2519
  return archive;
2353
2520
  }
2354
2521
  async function runBuild(paths, canvases, options) {
2355
- if (!fs13.existsSync(path13.join(PKG.viewer, "index.html"))) {
2522
+ if (!fs14.existsSync(path14.join(PKG.viewer, "index.html"))) {
2356
2523
  throw new CliError("The viewer is missing from this foss-design install (dist/viewer). Rebuild or reinstall it.");
2357
2524
  }
2358
- const out = path13.resolve(options.out ?? path13.join(paths.cache, "site"));
2525
+ const out = path14.resolve(options.out ?? path14.join(paths.cache, "site"));
2359
2526
  if (out === paths.root || out === paths.design) throw new CliError(`Refusing to build into ${out}`);
2360
2527
  print(dim(`Building ${canvases.length ? canvases.join(", ") : "every canvas"} and the design system\u2026`));
2361
2528
  let result;
@@ -2380,8 +2547,8 @@ import os from "os";
2380
2547
 
2381
2548
  // src/cloud/units.ts
2382
2549
  import { createHash as createHash4 } from "crypto";
2383
- import fs14 from "fs";
2384
- import path14 from "path";
2550
+ import fs15 from "fs";
2551
+ import path15 from "path";
2385
2552
  var SYSTEM_UNIT = "system";
2386
2553
  var CANVAS_PREFIX = "canvas/";
2387
2554
  var LOCAL_ONLY = /* @__PURE__ */ new Set(["cloud.json", "node_modules", "icon.svg", "icon.png", "icon.webp"]);
@@ -2409,7 +2576,7 @@ function caseClashes(keys) {
2409
2576
  }
2410
2577
  function unitRoot(paths, key) {
2411
2578
  const id2 = unitCanvasId(key);
2412
- return id2 === null ? paths.design : path14.join(paths.canvases, id2);
2579
+ return id2 === null ? paths.design : path15.join(paths.canvases, id2);
2413
2580
  }
2414
2581
  function isUnitPath(key, rel) {
2415
2582
  if (!rel || rel.includes("\\") || rel.includes("\0") || rel.startsWith("/")) return false;
@@ -2423,7 +2590,7 @@ function hashBuffer(data) {
2423
2590
  return createHash4("sha256").update(data).digest("hex");
2424
2591
  }
2425
2592
  function hashFile(file) {
2426
- const data = fs14.readFileSync(file);
2593
+ const data = fs15.readFileSync(file);
2427
2594
  return { hash: hashBuffer(data), size: data.length };
2428
2595
  }
2429
2596
  var warnedLinks = /* @__PURE__ */ new Set();
@@ -2436,34 +2603,34 @@ function warnLink(rel) {
2436
2603
  function walk(dir, base, out) {
2437
2604
  let entries;
2438
2605
  try {
2439
- entries = fs14.readdirSync(dir, { withFileTypes: true });
2606
+ entries = fs15.readdirSync(dir, { withFileTypes: true });
2440
2607
  } catch {
2441
2608
  return;
2442
2609
  }
2443
2610
  for (const entry of entries) {
2444
2611
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2445
- const full = path14.join(dir, entry.name);
2612
+ const full = path15.join(dir, entry.name);
2446
2613
  let isFile = entry.isFile();
2447
2614
  let isDir = entry.isDirectory();
2448
2615
  if (entry.isSymbolicLink()) {
2449
2616
  isDir = false;
2450
2617
  isFile = false;
2451
2618
  try {
2452
- const target = fs14.realpathSync(full);
2453
- const root = fs14.realpathSync(base);
2454
- if (target.startsWith(root + path14.sep)) isFile = fs14.statSync(target).isFile();
2455
- else warnLink(toPosix(path14.relative(base, full)));
2619
+ const target = fs15.realpathSync(full);
2620
+ const root = fs15.realpathSync(base);
2621
+ if (target.startsWith(root + path15.sep)) isFile = fs15.statSync(target).isFile();
2622
+ else warnLink(toPosix(path15.relative(base, full)));
2456
2623
  } catch {
2457
2624
  }
2458
2625
  }
2459
2626
  if (isDir) walk(full, base, out);
2460
- else if (isFile) out[toPosix(path14.relative(base, full))] = hashFile(full);
2627
+ else if (isFile) out[toPosix(path15.relative(base, full))] = hashFile(full);
2461
2628
  }
2462
2629
  }
2463
2630
  function unitManifest(paths, key) {
2464
2631
  const manifest = {};
2465
2632
  if (key === SYSTEM_UNIT) {
2466
- if (fs14.existsSync(paths.config) && fs14.statSync(paths.config).isFile())
2633
+ if (fs15.existsSync(paths.config) && fs15.statSync(paths.config).isFile())
2467
2634
  manifest["design.json"] = hashFile(paths.config);
2468
2635
  walk(paths.system, paths.design, manifest);
2469
2636
  } else walk(unitRoot(paths, key), paths.design, manifest);
@@ -2472,13 +2639,13 @@ function unitManifest(paths, key) {
2472
2639
  function buildManifest(dir) {
2473
2640
  const manifest = {};
2474
2641
  const visit = (current) => {
2475
- for (const entry of fs14.readdirSync(current, { withFileTypes: true })) {
2476
- const full = path14.join(current, entry.name);
2642
+ for (const entry of fs15.readdirSync(current, { withFileTypes: true })) {
2643
+ const full = path15.join(current, entry.name);
2477
2644
  if (entry.isDirectory()) visit(full);
2478
- else if (entry.isFile()) manifest[toPosix(path14.relative(dir, full))] = hashFile(full);
2645
+ else if (entry.isFile()) manifest[toPosix(path15.relative(dir, full))] = hashFile(full);
2479
2646
  }
2480
2647
  };
2481
- if (fs14.existsSync(dir)) visit(dir);
2648
+ if (fs15.existsSync(dir)) visit(dir);
2482
2649
  return sortManifest(manifest);
2483
2650
  }
2484
2651
  function sortManifest(manifest) {
@@ -2487,18 +2654,18 @@ function sortManifest(manifest) {
2487
2654
  function scanLocal(paths) {
2488
2655
  const units = /* @__PURE__ */ new Map();
2489
2656
  const notSynced = [];
2490
- if (!fs14.existsSync(paths.design)) return { units, notSynced };
2657
+ if (!fs15.existsSync(paths.design)) return { units, notSynced };
2491
2658
  const system = unitManifest(paths, SYSTEM_UNIT);
2492
2659
  if (Object.keys(system).length) units.set(SYSTEM_UNIT, { key: SYSTEM_UNIT, kind: "system", manifest: system });
2493
- for (const entry of fs14.readdirSync(paths.design, { withFileTypes: true })) {
2660
+ for (const entry of fs15.readdirSync(paths.design, { withFileTypes: true })) {
2494
2661
  const { name } = entry;
2495
2662
  if (name.startsWith(".") || LOCAL_ONLY.has(name)) continue;
2496
2663
  if (name === "design.json" && entry.isFile()) continue;
2497
2664
  if ((name === "system" || name === "canvas") && entry.isDirectory()) continue;
2498
2665
  notSynced.push(entry.isDirectory() ? `${name}/` : name);
2499
2666
  }
2500
- if (fs14.existsSync(paths.canvases) && fs14.statSync(paths.canvases).isDirectory()) {
2501
- for (const entry of fs14.readdirSync(paths.canvases, { withFileTypes: true })) {
2667
+ if (fs15.existsSync(paths.canvases) && fs15.statSync(paths.canvases).isDirectory()) {
2668
+ for (const entry of fs15.readdirSync(paths.canvases, { withFileTypes: true })) {
2502
2669
  if (entry.name.startsWith(".")) continue;
2503
2670
  if (!entry.isDirectory() || !ID_PATTERN.test(entry.name)) {
2504
2671
  notSynced.push(`canvas/${entry.name}${entry.isDirectory() ? "/" : ""}`);
@@ -2512,11 +2679,11 @@ function scanLocal(paths) {
2512
2679
  return { units, notSynced: notSynced.sort() };
2513
2680
  }
2514
2681
  function unitBuildDir(paths, key) {
2515
- return path14.join(paths.cache, "cloud", "build", ...key.split("/"));
2682
+ return path15.join(paths.cache, "cloud", "build", ...key.split("/"));
2516
2683
  }
2517
2684
  async function buildUnit(paths, key, out = unitBuildDir(paths, key)) {
2518
2685
  const id2 = unitCanvasId(key);
2519
- fs14.rmSync(out, { recursive: true, force: true });
2686
+ fs15.rmSync(out, { recursive: true, force: true });
2520
2687
  return buildSite(paths, out, {
2521
2688
  canvases: id2 === null ? [] : [id2],
2522
2689
  includeSystem: id2 === null,
@@ -2609,7 +2776,13 @@ function parseUnit(value) {
2609
2776
  banned: flag(data.banned, data.bannedAt, data.banned_at),
2610
2777
  manifest: parseManifest(data.sourceManifest ?? data.source_manifest ?? data.source),
2611
2778
  systemRev: num(data.systemRev) ?? num(data.system_rev) ?? null,
2612
- publicId: str(data.publicId) ?? null
2779
+ publicId: str(data.publicId) ?? null,
2780
+ screens: (Array.isArray(data.screens) ? data.screens : []).flatMap((value2) => {
2781
+ const screen = record(value2);
2782
+ const item = str(screen.item);
2783
+ const publicId = str(screen.publicId);
2784
+ return item && publicId ? [{ item, publicId }] : [];
2785
+ })
2613
2786
  };
2614
2787
  }
2615
2788
  function parseRevision(value) {
@@ -2648,7 +2821,7 @@ var CloudClient = class {
2648
2821
  }
2649
2822
  host;
2650
2823
  token;
2651
- async send(method, path37, init) {
2824
+ async send(method, path38, init) {
2652
2825
  const headers = {
2653
2826
  "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2654
2827
  Accept: "application/json"
@@ -2656,7 +2829,7 @@ var CloudClient = class {
2656
2829
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
2657
2830
  if (init.type) headers["Content-Type"] = init.type;
2658
2831
  try {
2659
- return await fetch(`${this.host}/api${path37}`, {
2832
+ return await fetch(`${this.host}/api${path38}`, {
2660
2833
  method,
2661
2834
  headers,
2662
2835
  body: init.body,
@@ -2677,8 +2850,8 @@ var CloudClient = class {
2677
2850
  throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2678
2851
  }
2679
2852
  /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2680
- async request(method, path37, body) {
2681
- const response = await this.send(method, path37, {
2853
+ async request(method, path38, body) {
2854
+ const response = await this.send(method, path38, {
2682
2855
  body: body === void 0 ? void 0 : JSON.stringify(body),
2683
2856
  type: body === void 0 ? void 0 : "application/json",
2684
2857
  timeout: JSON_TIMEOUT
@@ -2690,7 +2863,7 @@ var CloudClient = class {
2690
2863
  try {
2691
2864
  return JSON.parse(text2);
2692
2865
  } catch {
2693
- throw new CloudError(`${this.host} answered ${method} ${path37} with something other than JSON`, 0, "bad_response");
2866
+ throw new CloudError(`${this.host} answered ${method} ${path38} with something other than JSON`, 0, "bad_response");
2694
2867
  }
2695
2868
  }
2696
2869
  /** POST /device/code. */
@@ -2798,6 +2971,16 @@ var CloudClient = class {
2798
2971
  );
2799
2972
  return { publicId: str(body.publicId) ?? null };
2800
2973
  }
2974
+ /** Publish one screen of a canvas on its own, or turn its link off (owner only). */
2975
+ async screenAction(projectId, canvas, screen, action) {
2976
+ const body = record(
2977
+ await this.request(
2978
+ "POST",
2979
+ `/projects/${encodeURIComponent(projectId)}/canvases/${encodeURIComponent(canvas)}/screens/${encodeURIComponent(screen)}/${action}`
2980
+ )
2981
+ );
2982
+ return { publicId: str(body.publicId) ?? null };
2983
+ }
2801
2984
  /** POST /blobs/missing, in batches: the hashes the cloud does not have yet. */
2802
2985
  async missing(projectId, hashes2) {
2803
2986
  const missing = [];
@@ -2852,9 +3035,9 @@ var CloudClient = class {
2852
3035
  };
2853
3036
 
2854
3037
  // src/cloud/credentials.ts
2855
- import fs15 from "fs";
3038
+ import fs16 from "fs";
2856
3039
  import os2 from "os";
2857
- import path15 from "path";
3040
+ import path16 from "path";
2858
3041
  var DEFAULT_CLOUD = "https://app.fossdesign.dev";
2859
3042
  function normalizeHost(value) {
2860
3043
  let url;
@@ -2871,14 +3054,14 @@ function cloudHost() {
2871
3054
  return normalizeHost(process.env.FOSS_DESIGN_CLOUD || DEFAULT_CLOUD);
2872
3055
  }
2873
3056
  function credentialsFile() {
2874
- const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path15.join(os2.homedir(), ".config");
2875
- return path15.join(base, "foss-design", "credentials.json");
3057
+ const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path16.join(os2.homedir(), ".config");
3058
+ return path16.join(base, "foss-design", "credentials.json");
2876
3059
  }
2877
3060
  function readAll() {
2878
3061
  const file = credentialsFile();
2879
- if (!fs15.existsSync(file)) return {};
3062
+ if (!fs16.existsSync(file)) return {};
2880
3063
  try {
2881
- const data = JSON.parse(fs15.readFileSync(file, "utf8"));
3064
+ const data = JSON.parse(fs16.readFileSync(file, "utf8"));
2882
3065
  return data && typeof data === "object" && !Array.isArray(data) ? data : {};
2883
3066
  } catch (error) {
2884
3067
  throw new CliError(`${file} is not valid JSON (${error.message}); fix or delete it`);
@@ -2886,12 +3069,12 @@ function readAll() {
2886
3069
  }
2887
3070
  function writeAll(data) {
2888
3071
  const file = credentialsFile();
2889
- fs15.mkdirSync(path15.dirname(file), { recursive: true, mode: 448 });
3072
+ fs16.mkdirSync(path16.dirname(file), { recursive: true, mode: 448 });
2890
3073
  const temp = `${file}.${process.pid}.tmp`;
2891
- fs15.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
3074
+ fs16.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2892
3075
  `, { mode: 384 });
2893
- fs15.chmodSync(temp, 384);
2894
- fs15.renameSync(temp, file);
3076
+ fs16.chmodSync(temp, 384);
3077
+ fs16.renameSync(temp, file);
2895
3078
  }
2896
3079
  function getCredential(host) {
2897
3080
  const entry = readAll()[host];
@@ -2914,17 +3097,17 @@ function requireCredential(host) {
2914
3097
  }
2915
3098
 
2916
3099
  // src/cloud/state.ts
2917
- import fs16 from "fs";
2918
- import path16 from "path";
2919
- var linkFile = (paths) => path16.join(paths.design, "cloud.json");
2920
- var cloudCache = (paths) => path16.join(paths.cache, "cloud");
2921
- var incomingDir = (paths, key) => path16.join(cloudCache(paths), "incoming", ...key.split("/"));
3100
+ import fs17 from "fs";
3101
+ import path17 from "path";
3102
+ var linkFile = (paths) => path17.join(paths.design, "cloud.json");
3103
+ var cloudCache = (paths) => path17.join(paths.cache, "cloud");
3104
+ var incomingDir = (paths, key) => path17.join(cloudCache(paths), "incoming", ...key.split("/"));
2922
3105
  function readLink(paths) {
2923
3106
  const file = linkFile(paths);
2924
- if (!fs16.existsSync(file)) return null;
3107
+ if (!fs17.existsSync(file)) return null;
2925
3108
  let data;
2926
3109
  try {
2927
- data = JSON.parse(fs16.readFileSync(file, "utf8"));
3110
+ data = JSON.parse(fs17.readFileSync(file, "utf8"));
2928
3111
  } catch (error) {
2929
3112
  throw new CliError(`${file} is not valid JSON (${error.message}); run \`design link <project>\` again`);
2930
3113
  }
@@ -2963,9 +3146,9 @@ function writeLink(paths, link) {
2963
3146
  units: sorted(link.units),
2964
3147
  conflicts: sorted(link.conflicts)
2965
3148
  };
2966
- fs16.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
3149
+ fs17.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2967
3150
  `);
2968
- fs16.renameSync(temp, file);
3151
+ fs17.renameSync(temp, file);
2969
3152
  }
2970
3153
  var hashes = (manifest) => Object.fromEntries(Object.entries(manifest).map(([file, entry]) => [file, entry.hash]));
2971
3154
  function diffFiles(a, b) {
@@ -3043,20 +3226,20 @@ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ?
3043
3226
  var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3044
3227
 
3045
3228
  // src/capture/cover.ts
3046
- import fs20 from "fs";
3229
+ import fs21 from "fs";
3047
3230
  import os3 from "os";
3048
- import path19 from "path";
3231
+ import path20 from "path";
3049
3232
 
3050
3233
  // src/server/snapshots.ts
3051
3234
  import { createHash as createHash5 } from "crypto";
3052
- import fs17 from "fs";
3053
- import path17 from "path";
3235
+ import fs18 from "fs";
3236
+ import path18 from "path";
3054
3237
  function depsHash(root, rels, cache = /* @__PURE__ */ new Map()) {
3055
3238
  const hash = createHash5("sha1");
3056
3239
  for (const rel of [...rels].sort()) {
3057
3240
  let one = cache.get(rel);
3058
3241
  if (one === void 0) {
3059
- one = sourceHash(path17.join(root, rel)) ?? "missing";
3242
+ one = sourceHash(path18.join(root, rel)) ?? "missing";
3060
3243
  cache.set(rel, one);
3061
3244
  }
3062
3245
  hash.update(`${rel}\0${one}
@@ -3066,7 +3249,7 @@ function depsHash(root, rels, cache = /* @__PURE__ */ new Map()) {
3066
3249
  }
3067
3250
  function sourceHash(file) {
3068
3251
  try {
3069
- return createHash5("sha1").update(fs17.readFileSync(file)).digest("hex").slice(0, 16);
3252
+ return createHash5("sha1").update(fs18.readFileSync(file)).digest("hex").slice(0, 16);
3070
3253
  } catch {
3071
3254
  return null;
3072
3255
  }
@@ -3084,20 +3267,20 @@ var SnapshotStore = class {
3084
3267
  urlBase;
3085
3268
  meta = /* @__PURE__ */ new Map();
3086
3269
  metaFile(canvas) {
3087
- return path17.join(this.dir, canvas, "meta.json");
3270
+ return path18.join(this.dir, canvas, "meta.json");
3088
3271
  }
3089
3272
  /** Written aside and renamed: a run stopped mid-write leaves the old file, not half of one. */
3090
3273
  writeMeta(canvas, data) {
3091
3274
  const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
3092
- fs17.writeFileSync(temp, JSON.stringify(data));
3093
- fs17.renameSync(temp, this.metaFile(canvas));
3275
+ fs18.writeFileSync(temp, JSON.stringify(data));
3276
+ fs18.renameSync(temp, this.metaFile(canvas));
3094
3277
  }
3095
3278
  load(canvas) {
3096
3279
  const cached = this.meta.get(canvas);
3097
3280
  if (cached) return cached;
3098
3281
  let data = {};
3099
3282
  try {
3100
- data = JSON.parse(fs17.readFileSync(this.metaFile(canvas), "utf8"));
3283
+ data = JSON.parse(fs18.readFileSync(this.metaFile(canvas), "utf8"));
3101
3284
  } catch {
3102
3285
  }
3103
3286
  this.meta.set(canvas, data);
@@ -3147,11 +3330,11 @@ var SnapshotStore = class {
3147
3330
  }
3148
3331
  save(canvas, id2, theme2, png, height, source, deps) {
3149
3332
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3150
- const dir = path17.join(this.dir, canvas);
3151
- fs17.mkdirSync(dir, { recursive: true });
3152
- const file = path17.join(dir, `${id2}.${theme2}.png`);
3153
- fs17.writeFileSync(`${file}.${process.pid}.tmp`, png);
3154
- fs17.renameSync(`${file}.${process.pid}.tmp`, file);
3333
+ const dir = path18.join(this.dir, canvas);
3334
+ fs18.mkdirSync(dir, { recursive: true });
3335
+ const file = path18.join(dir, `${id2}.${theme2}.png`);
3336
+ fs18.writeFileSync(`${file}.${process.pid}.tmp`, png);
3337
+ fs18.renameSync(`${file}.${process.pid}.tmp`, file);
3155
3338
  const data = this.load(canvas);
3156
3339
  const version = Date.now();
3157
3340
  const previous = data[id2];
@@ -3166,20 +3349,20 @@ var SnapshotStore = class {
3166
3349
  file(canvas, name) {
3167
3350
  const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)(\.thumb)?\.png$/i.exec(name);
3168
3351
  if (!isSafeSegment(canvas) || !match) return null;
3169
- const file = path17.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
3170
- if (!fs17.existsSync(file)) return null;
3352
+ const file = path18.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
3353
+ if (!fs18.existsSync(file)) return null;
3171
3354
  return match[3] ? ensureThumb(file) : file;
3172
3355
  }
3173
3356
  };
3174
3357
 
3175
3358
  // src/capture/build-server.ts
3176
- import fs18 from "fs";
3359
+ import fs19 from "fs";
3177
3360
  import http from "http";
3178
- import path18 from "path";
3361
+ import path19 from "path";
3179
3362
  import sirv from "sirv";
3180
3363
  async function serveBuild(paths, name, options) {
3181
- const out = path18.join(paths.cache, name);
3182
- fs18.rmSync(out, { recursive: true, force: true });
3364
+ const out = path19.join(paths.cache, name);
3365
+ fs19.rmSync(out, { recursive: true, force: true });
3183
3366
  await buildSite(paths, out, {
3184
3367
  canvases: options.canvases,
3185
3368
  includeSystem: options.includeSystem,
@@ -3207,7 +3390,7 @@ async function serveBuild(paths, name, options) {
3207
3390
  base: `http://127.0.0.1:${server.address().port}`,
3208
3391
  read(rel) {
3209
3392
  try {
3210
- return JSON.parse(fs18.readFileSync(path18.join(out, rel), "utf8"));
3393
+ return JSON.parse(fs19.readFileSync(path19.join(out, rel), "utf8"));
3211
3394
  } catch {
3212
3395
  return null;
3213
3396
  }
@@ -3220,7 +3403,7 @@ async function serveBuild(paths, name, options) {
3220
3403
  }
3221
3404
 
3222
3405
  // src/capture/chrome.ts
3223
- import fs19 from "fs";
3406
+ import fs20 from "fs";
3224
3407
  var CANDIDATES = {
3225
3408
  darwin: [
3226
3409
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -3244,8 +3427,8 @@ var CANDIDATES = {
3244
3427
  };
3245
3428
  function findChrome() {
3246
3429
  const configured = process.env.DESIGN_CHROME;
3247
- if (configured && fs19.existsSync(configured)) return configured;
3248
- for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
3430
+ if (configured && fs20.existsSync(configured)) return configured;
3431
+ for (const candidate of CANDIDATES[process.platform] ?? []) if (fs20.existsSync(candidate)) return candidate;
3249
3432
  return null;
3250
3433
  }
3251
3434
  async function launchChrome() {
@@ -3417,7 +3600,7 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3417
3600
  for (const job of inLight) {
3418
3601
  if (filter.screens && !filter.screens.has(job.id)) continue;
3419
3602
  const file = files.get(job.id);
3420
- if (!file || !fs20.existsSync(file)) continue;
3603
+ if (!file || !fs21.existsSync(file)) continue;
3421
3604
  const deps = store.deps(id2, job.id);
3422
3605
  const hash = deps ? depsHash(paths.root, deps, hashes2) : sourceHash(file);
3423
3606
  const pinned = inDark.get(job.id) === job.theme;
@@ -3436,12 +3619,12 @@ function staleSnapshots(paths, canvasIds, filter = {}) {
3436
3619
  }
3437
3620
  return stale;
3438
3621
  }
3439
- async function takeSnapshots(paths, targets, preview, progress2) {
3440
- if (!targets.length) return [];
3622
+ async function takeSnapshots(paths, targets2, preview, progress2) {
3623
+ if (!targets2.length) return [];
3441
3624
  if (!findChrome()) return ["No Chrome here, so screens keep the snapshots they have."];
3442
3625
  const store = new SnapshotStore(paths.snapshots);
3443
3626
  const project = new DesignProject(paths, DEV_URLS, store.lookup);
3444
- const canvases = [...new Set(targets.map((target) => target.canvas))];
3627
+ const canvases = [...new Set(targets2.map((target) => target.canvas))];
3445
3628
  progress2.update(`Building ${canvases.join(", ")} to take snapshots from\u2026`);
3446
3629
  let url;
3447
3630
  let docOf = (id2) => project.canvas(id2).doc;
@@ -3462,10 +3645,10 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3462
3645
  const failed = [];
3463
3646
  const started = Date.now();
3464
3647
  let done = 0;
3465
- const line = () => `Snapshots ${done}/${targets.length} (both themes, as the viewer and the cloud show them)`;
3648
+ const line = () => `Snapshots ${done}/${targets2.length} (both themes, as the viewer and the cloud show them)`;
3466
3649
  progress2.update(line());
3467
3650
  try {
3468
- await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2, source }) => {
3651
+ await mapLimit([...targets2], TABS, async ({ canvas, screen, theme: theme2, source }) => {
3469
3652
  const job = canvasFrames(docOf(canvas), url, theme2, { id: screen })[0];
3470
3653
  if (job) {
3471
3654
  const page = await browser.newPage({
@@ -3487,7 +3670,7 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3487
3670
  const deps = modules ? [
3488
3671
  .../* @__PURE__ */ new Set([
3489
3672
  ...[...loaded].flatMap((rel) => modules[rel] ?? []),
3490
- ...file ? [toPosix(path19.relative(paths.root, file))] : []
3673
+ ...file ? [toPosix(path20.relative(paths.root, file))] : []
3491
3674
  ])
3492
3675
  ].sort() : [];
3493
3676
  const hash = deps.length ? depsHash(paths.root, deps, hashes2) : source;
@@ -3508,7 +3691,7 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3508
3691
  await stop();
3509
3692
  }
3510
3693
  const seconds2 = Math.round((Date.now() - started) / 1e3);
3511
- progress2.done(`Took ${targets.length - failed.length} snapshot${targets.length === 1 ? "" : "s"} in ${seconds2}s`);
3694
+ progress2.done(`Took ${targets2.length - failed.length} snapshot${targets2.length === 1 ? "" : "s"} in ${seconds2}s`);
3512
3695
  if (!failed.length) return [];
3513
3696
  const shown = failed.slice(0, 5).join(", ");
3514
3697
  return [
@@ -3517,73 +3700,73 @@ async function takeSnapshots(paths, targets, preview, progress2) {
3517
3700
  }
3518
3701
 
3519
3702
  // src/cloud/pull.ts
3520
- import fs24 from "fs";
3521
- import path23 from "path";
3703
+ import fs25 from "fs";
3704
+ import path24 from "path";
3522
3705
 
3523
3706
  // src/cloud/merge.ts
3524
- import fs22 from "fs";
3525
- import path21 from "path";
3707
+ import fs23 from "fs";
3708
+ import path22 from "path";
3526
3709
  import { merge as diff3 } from "node-diff3";
3527
3710
 
3528
3711
  // src/cloud/write.ts
3529
- import fs21 from "fs";
3530
- import path20 from "path";
3712
+ import fs22 from "fs";
3713
+ import path21 from "path";
3531
3714
  function writeInside(root, rel, data) {
3532
- const file = path20.join(root, rel);
3715
+ const file = path21.join(root, rel);
3533
3716
  if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
3534
- fs21.mkdirSync(path20.dirname(file), { recursive: true });
3535
- if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path20.dirname(file))))
3717
+ fs22.mkdirSync(path21.dirname(file), { recursive: true });
3718
+ if (!isInside(fs22.realpathSync(root), fs22.realpathSync(path21.dirname(file))))
3536
3719
  throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
3537
- if (fs21.existsSync(file) && fs21.lstatSync(file).isSymbolicLink()) fs21.unlinkSync(file);
3538
- fs21.writeFileSync(file, data);
3720
+ if (fs22.existsSync(file) && fs22.lstatSync(file).isSymbolicLink()) fs22.unlinkSync(file);
3721
+ fs22.writeFileSync(file, data);
3539
3722
  }
3540
3723
  function pruneEmpty(dir, stop) {
3541
3724
  let current = dir;
3542
3725
  while (isInside(stop, current) && current !== stop) {
3543
3726
  try {
3544
- if (fs21.readdirSync(current).length) return;
3545
- fs21.rmdirSync(current);
3727
+ if (fs22.readdirSync(current).length) return;
3728
+ fs22.rmdirSync(current);
3546
3729
  } catch {
3547
3730
  return;
3548
3731
  }
3549
- current = path20.dirname(current);
3732
+ current = path21.dirname(current);
3550
3733
  }
3551
3734
  }
3552
3735
 
3553
3736
  // src/cloud/merge.ts
3554
3737
  var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
3555
3738
  var isText = (rel, ...data) => TEXT.test(rel) && data.every((buffer) => !buffer || !buffer.subarray(0, 8192).includes(0));
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);
3739
+ var mergeDir = (paths, key) => path22.join(cloudCache(paths), "merge", ...key.split("/"));
3740
+ var stateFile = (paths, key) => path22.join(mergeDir(paths, key), "state.json");
3741
+ var sideFile = (paths, key, rel, side) => path22.join(mergeDir(paths, key), side, rel);
3559
3742
  function readMergeState(paths, key) {
3560
3743
  try {
3561
- return JSON.parse(fs22.readFileSync(stateFile(paths, key), "utf8"));
3744
+ return JSON.parse(fs23.readFileSync(stateFile(paths, key), "utf8"));
3562
3745
  } catch {
3563
3746
  return null;
3564
3747
  }
3565
3748
  }
3566
3749
  function saveState(paths, state) {
3567
- fs22.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
3568
- fs22.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
3750
+ fs23.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
3751
+ fs23.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
3569
3752
  `);
3570
3753
  }
3571
3754
  function mergingUnits(paths) {
3572
- const root = path21.join(cloudCache(paths), "merge");
3755
+ const root = path22.join(cloudCache(paths), "merge");
3573
3756
  const found = [];
3574
3757
  const visit = (dir, prefix) => {
3575
- if (fs22.existsSync(path21.join(dir, "state.json"))) {
3758
+ if (fs23.existsSync(path22.join(dir, "state.json"))) {
3576
3759
  const state = readMergeState(paths, prefix);
3577
3760
  if (state) found.push(state);
3578
3761
  return;
3579
3762
  }
3580
3763
  let entries = [];
3581
3764
  try {
3582
- entries = fs22.readdirSync(dir, { withFileTypes: true });
3765
+ entries = fs23.readdirSync(dir, { withFileTypes: true });
3583
3766
  } catch {
3584
3767
  }
3585
3768
  for (const entry of entries)
3586
- if (entry.isDirectory()) visit(path21.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3769
+ if (entry.isDirectory()) visit(path22.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
3587
3770
  };
3588
3771
  visit(root, "");
3589
3772
  return found;
@@ -3593,7 +3776,7 @@ function openConflicts(paths, state) {
3593
3776
  return state.conflicts.filter((conflict) => {
3594
3777
  if (conflict.kind !== "text") return true;
3595
3778
  try {
3596
- return MARKER.test(fs22.readFileSync(path21.join(paths.design, conflict.path), "utf8"));
3779
+ return MARKER.test(fs23.readFileSync(path22.join(paths.design, conflict.path), "utf8"));
3597
3780
  } catch {
3598
3781
  return false;
3599
3782
  }
@@ -3602,19 +3785,19 @@ function openConflicts(paths, state) {
3602
3785
  function keep(paths, key, rel, side, data) {
3603
3786
  if (!data) return;
3604
3787
  const file = sideFile(paths, key, rel, side);
3605
- fs22.mkdirSync(path21.dirname(file), { recursive: true });
3606
- fs22.writeFileSync(file, data);
3788
+ fs23.mkdirSync(path22.dirname(file), { recursive: true });
3789
+ fs23.writeFileSync(file, data);
3607
3790
  }
3608
3791
  function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
3609
3792
  const files = { added: 0, changed: 0, removed: 0, merged: 0 };
3610
3793
  const conflicts = [];
3611
- const stop = key === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, key));
3612
- fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3613
- const read = (rel) => fs22.readFileSync(path21.join(paths.design, rel));
3794
+ const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
3795
+ fs23.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3796
+ const read = (rel) => fs23.readFileSync(path22.join(paths.design, rel));
3614
3797
  const remove = (rel) => {
3615
- const file = path21.join(paths.design, rel);
3616
- fs22.rmSync(file, { force: true });
3617
- pruneEmpty(path21.dirname(file), stop);
3798
+ const file = path22.join(paths.design, rel);
3799
+ fs23.rmSync(file, { force: true });
3800
+ pruneEmpty(path22.dirname(file), stop);
3618
3801
  };
3619
3802
  for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
3620
3803
  const b = base[rel];
@@ -3670,11 +3853,11 @@ function resolveConflict(paths, state, rel, side) {
3670
3853
  const conflict = state.conflicts.find((item) => item.path === rel);
3671
3854
  if (!conflict) return;
3672
3855
  const kept = sideFile(paths, state.unit, rel, side);
3673
- const stop = state.unit === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, state.unit));
3674
- if (fs22.existsSync(kept)) writeInside(paths.design, rel, fs22.readFileSync(kept));
3856
+ const stop = state.unit === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, state.unit));
3857
+ if (fs23.existsSync(kept)) writeInside(paths.design, rel, fs23.readFileSync(kept));
3675
3858
  else {
3676
- fs22.rmSync(path21.join(paths.design, rel), { force: true });
3677
- pruneEmpty(path21.dirname(path21.join(paths.design, rel)), stop);
3859
+ fs23.rmSync(path22.join(paths.design, rel), { force: true });
3860
+ pruneEmpty(path22.dirname(path22.join(paths.design, rel)), stop);
3678
3861
  }
3679
3862
  state.conflicts = state.conflicts.filter((item) => item.path !== rel);
3680
3863
  saveState(paths, state);
@@ -3684,15 +3867,15 @@ function finishMerge(paths, link, key, accept = false) {
3684
3867
  if (state) {
3685
3868
  const open2 = openConflicts(paths, state).filter((conflict) => !(accept && conflict.kind !== "text"));
3686
3869
  if (open2.length) return open2;
3687
- fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3870
+ fs23.rmSync(mergeDir(paths, key), { recursive: true, force: true });
3688
3871
  }
3689
3872
  delete link.conflicts[key];
3690
3873
  return [];
3691
3874
  }
3692
3875
 
3693
3876
  // src/cloud/meta.ts
3694
- import fs23 from "fs";
3695
- import path22 from "path";
3877
+ import fs24 from "fs";
3878
+ import path23 from "path";
3696
3879
  async function syncProjectMeta(paths, client, link) {
3697
3880
  const remote = await client.project(link.project);
3698
3881
  if (!remote) return [];
@@ -3711,8 +3894,8 @@ async function syncProjectMeta(paths, client, link) {
3711
3894
  if (localChanged && !cloudChanged) {
3712
3895
  try {
3713
3896
  if (file) {
3714
- const ext = path22.extname(file).slice(1);
3715
- await client.setProjectIcon(link.project, { data: fs23.readFileSync(file), type: ICON_TYPES[ext] });
3897
+ const ext = path23.extname(file).slice(1);
3898
+ await client.setProjectIcon(link.project, { data: fs24.readFileSync(file), type: ICON_TYPES[ext] });
3716
3899
  lines.push("The project icon went up to the cloud.");
3717
3900
  } else {
3718
3901
  await client.setProjectIcon(link.project, null);
@@ -3747,12 +3930,12 @@ function checkManifest(key, manifest) {
3747
3930
  }
3748
3931
  function applyUnit(paths, key, local, target, blobs) {
3749
3932
  const counts = { added: 0, changed: 0, removed: 0 };
3750
- const stop = key === SYSTEM_UNIT ? paths.design : path23.dirname(unitRoot(paths, key));
3933
+ const stop = key === SYSTEM_UNIT ? paths.design : path24.dirname(unitRoot(paths, key));
3751
3934
  for (const rel of Object.keys(local)) {
3752
3935
  if (rel in target) continue;
3753
- const file = path23.join(paths.design, rel);
3754
- fs24.rmSync(file, { force: true });
3755
- pruneEmpty(path23.dirname(file), stop);
3936
+ const file = path24.join(paths.design, rel);
3937
+ fs25.rmSync(file, { force: true });
3938
+ pruneEmpty(path24.dirname(file), stop);
3756
3939
  counts.removed++;
3757
3940
  }
3758
3941
  for (const [rel, entry] of Object.entries(target)) {
@@ -3822,7 +4005,7 @@ async function pull(ctx, options) {
3822
4005
  }
3823
4006
  if (s.conflictRev !== void 0) {
3824
4007
  finishMerge(paths, link, key, true);
3825
- fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4008
+ fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3826
4009
  }
3827
4010
  report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
3828
4011
  } else if (!s.changed.length) {
@@ -3851,7 +4034,7 @@ async function pull(ctx, options) {
3851
4034
  }
3852
4035
  const byHash = /* @__PURE__ */ new Map();
3853
4036
  for (const unit of scan.units.values())
3854
- for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path23.join(paths.design, rel));
4037
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path24.join(paths.design, rel));
3855
4038
  const needed = /* @__PURE__ */ new Set();
3856
4039
  const bases = /* @__PURE__ */ new Set();
3857
4040
  for (const plan of plans) {
@@ -3872,7 +4055,7 @@ async function pull(ctx, options) {
3872
4055
  const download = [];
3873
4056
  for (const hash of needed) {
3874
4057
  const file = byHash.get(hash);
3875
- if (file) blobs.set(hash, fs24.readFileSync(file));
4058
+ if (file) blobs.set(hash, fs25.readFileSync(file));
3876
4059
  else download.push(hash);
3877
4060
  }
3878
4061
  if (download.length) {
@@ -3908,7 +4091,7 @@ async function pull(ctx, options) {
3908
4091
  if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3909
4092
  else delete link.units[key];
3910
4093
  delete link.conflicts[key];
3911
- fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4094
+ fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3912
4095
  } else {
3913
4096
  const result = mergeUnit(
3914
4097
  paths,
@@ -3919,7 +4102,7 @@ async function pull(ctx, options) {
3919
4102
  plan.remote.manifest,
3920
4103
  (hash) => blobs.get(hash)
3921
4104
  );
3922
- fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4105
+ fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3923
4106
  link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3924
4107
  entry.rev = plan.remote.headRev;
3925
4108
  entry.files = result.files;
@@ -3949,8 +4132,8 @@ async function pull(ctx, options) {
3949
4132
  }
3950
4133
 
3951
4134
  // src/cloud/push.ts
3952
- import fs25 from "fs";
3953
- import path24 from "path";
4135
+ import fs26 from "fs";
4136
+ import path25 from "path";
3954
4137
  function blockedReport(status, message) {
3955
4138
  return {
3956
4139
  unit: status.key,
@@ -4066,7 +4249,7 @@ async function push(ctx, options) {
4066
4249
  for (const key of toPush) {
4067
4250
  const s = status(key);
4068
4251
  if (s.conflictRev !== void 0) {
4069
- const dir = path24.relative(paths.root, incomingDir(paths, key));
4252
+ const dir = path25.relative(paths.root, incomingDir(paths, key));
4070
4253
  blocked.push(
4071
4254
  blockedReport(
4072
4255
  s,
@@ -4115,7 +4298,7 @@ async function push(ctx, options) {
4115
4298
  const files = /* @__PURE__ */ new Map();
4116
4299
  const addFiles = (manifest, dir) => {
4117
4300
  for (const [rel, entry] of Object.entries(manifest))
4118
- if (!files.has(entry.hash)) files.set(entry.hash, path24.join(dir, rel));
4301
+ if (!files.has(entry.hash)) files.set(entry.hash, path25.join(dir, rel));
4119
4302
  };
4120
4303
  for (const key of pushing) {
4121
4304
  const id2 = unitCanvasId(key);
@@ -4147,7 +4330,7 @@ async function push(ctx, options) {
4147
4330
  if (missing.length) {
4148
4331
  const unknown = missing.find((hash) => !files.has(hash));
4149
4332
  if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
4150
- const bytes = missing.reduce((sum, hash) => sum + fs25.statSync(files.get(hash)).size, 0);
4333
+ const bytes = missing.reduce((sum, hash) => sum + fs26.statSync(files.get(hash)).size, 0);
4151
4334
  const bar = ctx.progress();
4152
4335
  const started = Date.now();
4153
4336
  let sentFiles = 0;
@@ -4156,12 +4339,12 @@ async function push(ctx, options) {
4156
4339
  bar.update(line());
4157
4340
  await mapLimit(missing, TRANSFERS, async (hash) => {
4158
4341
  const file = files.get(hash);
4159
- const data = fs25.readFileSync(file);
4342
+ const data = fs26.readFileSync(file);
4160
4343
  try {
4161
4344
  await client.putBlob(link.project, hash, data);
4162
4345
  } catch (error) {
4163
4346
  if (error instanceof CloudError && error.status === 413)
4164
- throw new CliError(`${path24.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4347
+ throw new CliError(`${path25.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
4165
4348
  throw error;
4166
4349
  }
4167
4350
  sentFiles++;
@@ -4194,7 +4377,7 @@ async function push(ctx, options) {
4194
4377
  url: webUrl(link, unit.key)
4195
4378
  });
4196
4379
  }
4197
- for (const key of resolved) fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4380
+ for (const key of resolved) fs26.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4198
4381
  writeLink(paths, link);
4199
4382
  report.units.sort((a, b) => byUnit(a.unit, b.unit));
4200
4383
  return report;
@@ -4205,7 +4388,7 @@ function refuseLegacyGo(paths, keys) {
4205
4388
  for (const key of keys) {
4206
4389
  if (!key.startsWith("canvas/")) continue;
4207
4390
  const id2 = key.slice("canvas/".length);
4208
- for (const file of canvasCodeFiles(path24.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4391
+ for (const file of canvasCodeFiles(path25.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
4209
4392
  for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
4210
4393
  }
4211
4394
  }
@@ -4221,18 +4404,18 @@ Still using go():
4221
4404
 
4222
4405
  // src/cli/commands/preview.ts
4223
4406
  import { spawn } from "child_process";
4224
- import fs29 from "fs";
4407
+ import fs30 from "fs";
4225
4408
 
4226
4409
  // src/server/index.ts
4227
- import fs28 from "fs";
4410
+ import fs29 from "fs";
4228
4411
  import http2 from "http";
4229
- import path27 from "path";
4412
+ import path28 from "path";
4230
4413
  import sirv2 from "sirv";
4231
4414
  import { createServer, searchForWorkspaceRoot } from "vite";
4232
4415
 
4233
4416
  // src/server/api.ts
4234
- import fs26 from "fs";
4235
- import path25 from "path";
4417
+ import fs27 from "fs";
4418
+ import path26 from "path";
4236
4419
  function sendJson(res, status, body) {
4237
4420
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
4238
4421
  res.end(JSON.stringify(body));
@@ -4283,17 +4466,18 @@ async function handleApi(ctx, req, res, url) {
4283
4466
  }
4284
4467
  if (route2 === "/source") {
4285
4468
  const rel = url.searchParams.get("path") ?? "";
4286
- const file = path25.resolve(project.paths.root, rel);
4287
- if (!rel || !isInside(project.paths.root, file) || !fs26.existsSync(file) || fs26.statSync(file).isDirectory()) {
4469
+ const file = path26.resolve(project.paths.root, rel);
4470
+ const listed = project.system()?.doc.components.some((component) => component.specimen === rel || component.sources.includes(rel));
4471
+ if (!rel || !listed || !isShowableSource(project.paths, file)) {
4288
4472
  sendJson(res, 404, { error: "not found" });
4289
4473
  return true;
4290
4474
  }
4291
- if (fs26.statSync(file).size > 2 * 1024 * 1024) {
4475
+ if (fs27.statSync(file).size > 2 * 1024 * 1024) {
4292
4476
  sendJson(res, 413, { error: "file too large to show" });
4293
4477
  return true;
4294
4478
  }
4295
4479
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
4296
- res.end(fs26.readFileSync(file, "utf8"));
4480
+ res.end(fs27.readFileSync(file, "utf8"));
4297
4481
  return true;
4298
4482
  }
4299
4483
  if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
@@ -4343,7 +4527,7 @@ async function handleApi(ctx, req, res, url) {
4343
4527
  return true;
4344
4528
  }
4345
4529
  res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
4346
- fs26.createReadStream(file).pipe(res);
4530
+ fs27.createReadStream(file).pipe(res);
4347
4531
  return true;
4348
4532
  }
4349
4533
  return false;
@@ -4382,63 +4566,82 @@ var EventHub = class {
4382
4566
  }
4383
4567
  };
4384
4568
 
4569
+ // src/server/hosts.ts
4570
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
4571
+ var FRAME_HOST = "127.0.0.1";
4572
+ var VIEWER_HOST = "localhost";
4573
+ function parseHost(header) {
4574
+ const match = /^(\[[0-9a-f:.]+\]|[a-z0-9.-]+)(?::(\d{1,5}))?$/i.exec(header ?? "");
4575
+ if (!match) return null;
4576
+ return { name: match[1].toLowerCase(), port: match[2] ? Number(match[2]) : 80 };
4577
+ }
4578
+ function isAllowedHost(header, port, extra) {
4579
+ const host = parseHost(header);
4580
+ if (!host || host.port !== port) return false;
4581
+ return LOOPBACK.has(host.name) || host.name.endsWith(".localhost") || !!extra && host.name === extra.toLowerCase();
4582
+ }
4583
+ function isFrameApi(pathname, method) {
4584
+ if (pathname === "/api/health") return method === "GET";
4585
+ return /^\/api\/snapshots\/[^/]+\/[^/]+$/.test(pathname) && (method === "GET" || method === "POST");
4586
+ }
4587
+
4385
4588
  // src/server/state.ts
4386
- import fs27 from "fs";
4589
+ import fs28 from "fs";
4387
4590
  import os4 from "os";
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");
4591
+ import path27 from "path";
4592
+ var stateFile2 = (paths) => path27.join(paths.cache, "server.json");
4593
+ var logFile = (paths) => path27.join(paths.cache, "server.log");
4391
4594
  function readState(paths) {
4392
4595
  try {
4393
- return JSON.parse(fs27.readFileSync(stateFile2(paths), "utf8"));
4596
+ return JSON.parse(fs28.readFileSync(stateFile2(paths), "utf8"));
4394
4597
  } catch {
4395
4598
  return null;
4396
4599
  }
4397
4600
  }
4398
4601
  function registryDir() {
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");
4602
+ const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path27.join(os4.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path27.join(os4.homedir(), ".cache");
4603
+ return path27.join(base, "foss-design", "previews");
4401
4604
  }
4402
- var entryFile = (pid) => path26.join(registryDir(), `${pid}.json`);
4605
+ var entryFile = (pid) => path27.join(registryDir(), `${pid}.json`);
4403
4606
  function writeState(paths, state) {
4404
- fs27.mkdirSync(paths.cache, { recursive: true });
4405
- fs27.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4607
+ fs28.mkdirSync(paths.cache, { recursive: true });
4608
+ fs28.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
4406
4609
  `);
4407
4610
  try {
4408
- fs27.mkdirSync(registryDir(), { recursive: true });
4409
- fs27.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
4611
+ fs28.mkdirSync(registryDir(), { recursive: true });
4612
+ fs28.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
4410
4613
  `);
4411
4614
  } catch {
4412
4615
  }
4413
4616
  }
4414
4617
  function clearState(paths, pid) {
4415
4618
  const state = readState(paths);
4416
- fs27.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
4619
+ fs28.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
4417
4620
  if (pid !== void 0 && state && state.pid !== pid) return;
4418
- fs27.rmSync(stateFile2(paths), { force: true });
4621
+ fs28.rmSync(stateFile2(paths), { force: true });
4419
4622
  }
4420
4623
  function listPreviews() {
4421
4624
  let files = [];
4422
4625
  try {
4423
- files = fs27.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
4626
+ files = fs28.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
4424
4627
  } catch {
4425
4628
  return [];
4426
4629
  }
4427
4630
  const found = [];
4428
4631
  for (const name of files) {
4429
- const file = path26.join(registryDir(), name);
4632
+ const file = path27.join(registryDir(), name);
4430
4633
  let state = null;
4431
4634
  try {
4432
- state = JSON.parse(fs27.readFileSync(file, "utf8"));
4635
+ state = JSON.parse(fs28.readFileSync(file, "utf8"));
4433
4636
  } catch {
4434
4637
  }
4435
4638
  if (!state?.pid || !pidAlive(state.pid)) {
4436
- fs27.rmSync(file, { force: true });
4639
+ fs28.rmSync(file, { force: true });
4437
4640
  continue;
4438
4641
  }
4439
4642
  let recorded = null;
4440
4643
  try {
4441
- recorded = JSON.parse(fs27.readFileSync(path26.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4644
+ recorded = JSON.parse(fs28.readFileSync(path27.join(state.root, ".design", ".cache", "server.json"), "utf8"));
4442
4645
  } catch {
4443
4646
  }
4444
4647
  found.push({ ...state, tracked: recorded?.pid === state.pid });
@@ -4452,11 +4655,11 @@ async function stopPreview(entry) {
4452
4655
  }
4453
4656
  const until = Date.now() + 5e3;
4454
4657
  while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
4455
- fs27.rmSync(entryFile(entry.pid), { force: true });
4456
- const stateAt = path26.join(entry.root, ".design", ".cache", "server.json");
4658
+ fs28.rmSync(entryFile(entry.pid), { force: true });
4659
+ const stateAt = path27.join(entry.root, ".design", ".cache", "server.json");
4457
4660
  try {
4458
- if (JSON.parse(fs27.readFileSync(stateAt, "utf8")).pid === entry.pid)
4459
- fs27.rmSync(stateAt, { force: true });
4661
+ if (JSON.parse(fs28.readFileSync(stateAt, "utf8")).pid === entry.pid)
4662
+ fs28.rmSync(stateAt, { force: true });
4460
4663
  } catch {
4461
4664
  }
4462
4665
  }
@@ -4515,8 +4718,8 @@ function notFoundPage(res, title, text2) {
4515
4718
  }
4516
4719
  async function startDevServer(options) {
4517
4720
  const paths = designPaths(options.root);
4518
- if (!fs28.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4519
- fs28.mkdirSync(paths.cache, { recursive: true });
4721
+ if (!fs29.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4722
+ fs29.mkdirSync(paths.cache, { recursive: true });
4520
4723
  const snapshots = new SnapshotStore(paths.snapshots);
4521
4724
  const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
4522
4725
  const entries = new Entries(project);
@@ -4524,6 +4727,7 @@ async function startDevServer(options) {
4524
4727
  const links = linkShippedPackages(paths, project.appDir());
4525
4728
  const events = new EventHub();
4526
4729
  const httpServer = http2.createServer();
4730
+ const listening = () => httpServer.address()?.port ?? 0;
4527
4731
  dropStaleDepCache(paths, project.appDir());
4528
4732
  const base = baseConfig(project);
4529
4733
  const vite = await createServer({
@@ -4542,8 +4746,8 @@ async function startDevServer(options) {
4542
4746
  // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4543
4747
  // generated entries are modules the frames load, so those stay watched.
4544
4748
  ignored: (file) => {
4545
- const rel = toPosix(path27.relative(paths.cache, file));
4546
- const inCache = rel !== "" && !rel.startsWith("..") && !path27.isAbsolute(rel);
4749
+ const rel = toPosix(path28.relative(paths.cache, file));
4750
+ const inCache = rel !== "" && !rel.startsWith("..") && !path28.isAbsolute(rel);
4547
4751
  return inCache && rel !== "entries" && !rel.startsWith("entries/");
4548
4752
  }
4549
4753
  },
@@ -4552,7 +4756,7 @@ async function startDevServer(options) {
4552
4756
  forwardConsole: false
4553
4757
  },
4554
4758
  optimizeDeps: {
4555
- entries: entries.files().map((file) => toPosix(path27.relative(paths.design, file)))
4759
+ entries: entries.files().map((file) => toPosix(path28.relative(paths.design, file)))
4556
4760
  }
4557
4761
  });
4558
4762
  const publicDir = project.publicDir();
@@ -4564,16 +4768,21 @@ async function startDevServer(options) {
4564
4768
  return null;
4565
4769
  }
4566
4770
  };
4567
- const viewer = fs28.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
4568
- const viewerIndex = path27.join(PKG.viewer, "index.html");
4569
- const serveViewerIndex = (res) => {
4570
- const html = fs28.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4771
+ const viewer = fs29.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
4772
+ const viewerIndex = path28.join(PKG.viewer, "index.html");
4773
+ const serveViewerIndex = (req, res, url2) => {
4774
+ if (parseHost(req.headers.host)?.name === FRAME_HOST) {
4775
+ res.writeHead(302, { location: `http://${VIEWER_HOST}:${listening()}${url2.pathname}${url2.search}` });
4776
+ res.end();
4777
+ return;
4778
+ }
4779
+ const html = fs29.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4571
4780
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
4572
4781
  res.end(html);
4573
4782
  };
4574
4783
  const ctx = { project, events, snapshots };
4575
4784
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
4576
- const bootUrl = `/${relToDesign(paths, path27.join(entries.dir, HTML_BOOT))}`;
4785
+ const bootUrl = `/${relToDesign(paths, path28.join(entries.dir, HTML_BOOT))}`;
4577
4786
  const frameHead = (source, extra = {}) => {
4578
4787
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
4579
4788
  return {
@@ -4594,7 +4803,7 @@ async function startDevServer(options) {
4594
4803
  notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
4595
4804
  return;
4596
4805
  }
4597
- if (!fs28.existsSync(entries.entryFile(source))) entries.sync();
4806
+ if (!fs29.existsSync(entries.entryFile(source))) entries.sync();
4598
4807
  const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
4599
4808
  const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
4600
4809
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -4603,8 +4812,8 @@ async function startDevServer(options) {
4603
4812
  };
4604
4813
  const serveHtmlScreen = async (res, url2) => {
4605
4814
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
4606
- const file = path27.resolve(paths.design, rel);
4607
- if (!isInside(paths.design, file) || !fs28.existsSync(file)) {
4815
+ const file = path28.resolve(paths.design, rel);
4816
+ if (!isInside(paths.design, file) || !fs29.existsSync(file)) {
4608
4817
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
4609
4818
  return;
4610
4819
  }
@@ -4614,13 +4823,13 @@ async function startDevServer(options) {
4614
4823
  key: `${canvas}/${id2}`,
4615
4824
  canvas,
4616
4825
  id: id2,
4617
- title: path27.basename(file),
4826
+ title: path28.basename(file),
4618
4827
  file,
4619
4828
  format: "html",
4620
4829
  props: {},
4621
4830
  system: true
4622
4831
  };
4623
- const raw = fs28.readFileSync(file, "utf8");
4832
+ const raw = fs29.readFileSync(file, "utf8");
4624
4833
  const withSystem = source.system && !optsOutOfSystem(raw);
4625
4834
  const html = injectIntoHtml(
4626
4835
  raw,
@@ -4636,8 +4845,16 @@ async function startDevServer(options) {
4636
4845
  if (res.headersSent) return res.end();
4637
4846
  sendJson(res, 500, { error: error?.stack ?? String(error) });
4638
4847
  };
4848
+ if (!isAllowedHost(req.headers.host, listening(), options.host)) {
4849
+ sendJson(res, 403, { error: `This preview server answers on localhost:${listening()} only.` });
4850
+ return;
4851
+ }
4639
4852
  try {
4640
4853
  if (url2.pathname.startsWith("/api/")) {
4854
+ if (parseHost(req.headers.host)?.name === FRAME_HOST && !isFrameApi(url2.pathname, req.method ?? "GET")) {
4855
+ sendJson(res, 403, { error: `Frames' host: open the viewer on http://${VIEWER_HOST}:${listening()}.` });
4856
+ return;
4857
+ }
4641
4858
  handleApi(ctx, req, res, url2).then((handled) => {
4642
4859
  if (!handled) sendJson(res, 404, { error: "unknown endpoint" });
4643
4860
  }).catch(fail);
@@ -4653,7 +4870,7 @@ async function startDevServer(options) {
4653
4870
  }
4654
4871
  if (url2.pathname.startsWith("/_fs/")) {
4655
4872
  const rel = publicRel(url2.pathname.slice("/_fs/".length));
4656
- if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path27.join(publicDir, rel))) {
4873
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs29.existsSync(path28.join(publicDir, rel))) {
4657
4874
  notFoundPage(res, "Not found", url2.pathname);
4658
4875
  return;
4659
4876
  }
@@ -4665,10 +4882,10 @@ async function startDevServer(options) {
4665
4882
  const rel = publicRel(url2.pathname.slice(1));
4666
4883
  if (servePublic && rel !== null && project.isPublicFile(rel))
4667
4884
  servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4668
- else if (path27.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4669
- else serveViewerIndex(res);
4885
+ else if (path28.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4886
+ else serveViewerIndex(req, res, url2);
4670
4887
  };
4671
- if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
4888
+ if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(req, res, url2);
4672
4889
  else viewer(req, res, notViewer);
4673
4890
  } else notFoundPage(res, "Viewer is not built", "Run `pnpm build` in the foss-design package.");
4674
4891
  } catch (error) {
@@ -4693,7 +4910,7 @@ async function startDevServer(options) {
4693
4910
  };
4694
4911
  vite.watcher.on("all", (event, file) => {
4695
4912
  if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4696
- const rel = toPosix(path27.relative(paths.design, file));
4913
+ const rel = toPosix(path28.relative(paths.design, file));
4697
4914
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
4698
4915
  if (rel === "design.json") {
4699
4916
  queue("config", () => {
@@ -4804,8 +5021,8 @@ async function ensureServer(paths, options) {
4804
5021
  const recorded = readState(paths);
4805
5022
  if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
4806
5023
  for (const other of listPreviews()) if (other.root === paths.root) await stopPreview(other);
4807
- fs29.mkdirSync(paths.cache, { recursive: true });
4808
- const log = fs29.openSync(logFile(paths), "w");
5024
+ fs30.mkdirSync(paths.cache, { recursive: true });
5025
+ const log = fs30.openSync(logFile(paths), "w");
4809
5026
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
4810
5027
  if (options.port) args.push("--port", String(options.port));
4811
5028
  const child = spawn(process.execPath, args, {
@@ -4817,7 +5034,7 @@ async function ensureServer(paths, options) {
4817
5034
  child.unref();
4818
5035
  const state = await waitForServer(paths, 4e4);
4819
5036
  if (!state) {
4820
- const tail = fs29.existsSync(logFile(paths)) ? fs29.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
5037
+ const tail = fs30.existsSync(logFile(paths)) ? fs30.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4821
5038
  throw new CliError(`The preview server did not start.
4822
5039
  ${tail.join("\n")}`);
4823
5040
  }
@@ -4979,6 +5196,7 @@ function runPull(paths, units, options) {
4979
5196
 
4980
5197
  // src/cli/commands/canvases.ts
4981
5198
  var publicUrl = (host, publicId) => `${host.replace(/\/+$/, "")}/s/${publicId}`;
5199
+ var screenUrl = (host, publicId) => `${host.replace(/\/+$/, "")}/a/${publicId}`;
4982
5200
  function ago(iso) {
4983
5201
  const seconds2 = Math.max(0, (Date.now() - Date.parse(iso)) / 1e3);
4984
5202
  if (!Number.isFinite(seconds2)) return "";
@@ -5069,16 +5287,35 @@ async function runUrl(paths, value, options) {
5069
5287
  const unit = await remoteCanvas(client, link.project, canvas);
5070
5288
  const url = webUrl(link, canvasUnit(canvas));
5071
5289
  const pub = unit.publicId ? publicUrl(link.host, unit.publicId) : null;
5072
- if (options.json) return print(JSON.stringify({ canvas, url, publicUrl: pub, archived: unit.archived }, null, 2));
5290
+ const screens = unit.screens.map((s) => ({ screen: s.item, publicUrl: screenUrl(link.host, s.publicId) }));
5291
+ if (options.json)
5292
+ return print(JSON.stringify({ canvas, url, publicUrl: pub, screens, archived: unit.archived }, null, 2));
5073
5293
  print(url);
5074
5294
  if (pub) print(`${dim("public")} ${pub}`);
5295
+ for (const screen of screens) print(`${dim(`public ${screen.screen}`)} ${screen.publicUrl}`);
5075
5296
  if (unit.archived) print(dim("archived \u2014 `design unarchive` brings it back"));
5076
5297
  }
5077
5298
  async function runPublish(paths, value, publish) {
5078
5299
  const verb = publish ? "publish" : "unpublish";
5079
- const canvas = canvasArg(value, `design ${verb} <canvas>`);
5300
+ const usage = `design ${verb} <canvas>[/<screen>]`;
5301
+ const slash = value?.indexOf("/") ?? -1;
5302
+ const screen = value && slash > 0 ? value.slice(slash + 1) : null;
5303
+ const canvas = canvasArg(value && slash > 0 ? value.slice(0, slash) : value, usage);
5304
+ if (screen === "") throw new CliError(`Usage: ${usage}`);
5080
5305
  const { link, client } = cloud(paths);
5081
- await remoteCanvas(client, link.project, canvas);
5306
+ const unit = await remoteCanvas(client, link.project, canvas);
5307
+ if (screen) {
5308
+ if (!publish && !unit.screens.some((s) => s.item === screen))
5309
+ return print(dim(`${canvas}/${screen} has no public link`));
5310
+ const { publicId: publicId2 } = await client.screenAction(link.project, canvas, screen, verb);
5311
+ if (publish) {
5312
+ print(
5313
+ `${green("\u2713")} Published ${bold(`${canvas}/${screen}`)} on its own: anyone with the link sees this screen alone`
5314
+ );
5315
+ if (publicId2) print(` ${cyan(screenUrl(link.host, publicId2))}`);
5316
+ } else print(`${green("\u2713")} ${bold(`${canvas}/${screen}`)} has no public link now`);
5317
+ return;
5318
+ }
5082
5319
  const { publicId } = await client.canvasAction(link.project, canvas, verb);
5083
5320
  if (publish) {
5084
5321
  const id2 = publicId ?? (await remoteCanvas(client, link.project, canvas)).publicId;
@@ -5247,11 +5484,11 @@ async function renderFrames(jobs) {
5247
5484
  await browser.close();
5248
5485
  }
5249
5486
  }
5250
- async function runCheck(paths, targets, options) {
5487
+ async function runCheck(paths, targets2, options) {
5251
5488
  const project = new DesignProject(paths, DEV_URLS);
5252
5489
  const known = project.canvasIds();
5253
5490
  const picked = /* @__PURE__ */ new Map();
5254
- for (const target of targets) {
5491
+ for (const target of targets2) {
5255
5492
  const [canvasId = "", screenId] = target.split("/");
5256
5493
  if (!known.includes(canvasId))
5257
5494
  throw new CliError(`No canvas "${canvasId}". Canvases: ${known.join(", ") || "none yet"}`);
@@ -5416,9 +5653,9 @@ function printLinkIssues(issues, project) {
5416
5653
  }
5417
5654
 
5418
5655
  // src/cli/commands/cloud.ts
5419
- import fs30 from "fs";
5656
+ import fs31 from "fs";
5420
5657
  import os5 from "os";
5421
- import path28 from "path";
5658
+ import path29 from "path";
5422
5659
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5423
5660
  async function runLogin() {
5424
5661
  const host = cloudHost();
@@ -5519,7 +5756,7 @@ async function runLink(paths, wanted, options) {
5519
5756
  print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
5520
5757
  } else {
5521
5758
  writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
5522
- fs30.rmSync(path28.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5759
+ fs31.rmSync(path29.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
5523
5760
  const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
5524
5761
  print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
5525
5762
  }
@@ -5546,8 +5783,8 @@ async function runLink(paths, wanted, options) {
5546
5783
  }
5547
5784
 
5548
5785
  // src/cli/commands/icon.ts
5549
- import fs31 from "fs";
5550
- import path29 from "path";
5786
+ import fs32 from "fs";
5787
+ import path30 from "path";
5551
5788
  async function toCloud(paths, file) {
5552
5789
  const link = readLink(paths);
5553
5790
  if (!link) return;
@@ -5558,10 +5795,10 @@ async function toCloud(paths, file) {
5558
5795
  }
5559
5796
  const client = new CloudClient(link.host, credential.token);
5560
5797
  try {
5561
- const ext = file ? path29.extname(file).slice(1) : null;
5798
+ const ext = file ? path30.extname(file).slice(1) : null;
5562
5799
  await client.setProjectIcon(
5563
5800
  link.project,
5564
- file && ext ? { data: fs31.readFileSync(file), type: ICON_TYPES[ext] } : null
5801
+ file && ext ? { data: fs32.readFileSync(file), type: ICON_TYPES[ext] } : null
5565
5802
  );
5566
5803
  link.icon = file ? iconHash(file) : null;
5567
5804
  writeLink(paths, link);
@@ -5574,35 +5811,35 @@ async function runIcon(paths, file, options) {
5574
5811
  if (options.remove) {
5575
5812
  const had = iconFile(paths);
5576
5813
  writeIcon(paths, null);
5577
- print(had ? `${green("Removed")} ${path29.relative(process.cwd(), had)}` : "The project has no icon.");
5814
+ print(had ? `${green("Removed")} ${path30.relative(process.cwd(), had)}` : "The project has no icon.");
5578
5815
  await toCloud(paths, null);
5579
5816
  return;
5580
5817
  }
5581
5818
  if (!file) {
5582
5819
  const current = iconFile(paths);
5583
5820
  if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
5584
- return print(`${path29.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
5821
+ return print(`${path30.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs32.statSync(current).size / 1024)} KB`)}`);
5585
5822
  }
5586
5823
  let data;
5587
5824
  try {
5588
- data = fs31.readFileSync(file);
5825
+ data = fs32.readFileSync(file);
5589
5826
  } catch {
5590
5827
  throw new CliError(`No file ${file}`);
5591
5828
  }
5592
- const problem = iconProblem(data, path29.extname(file).slice(1).toLowerCase() || void 0);
5829
+ const problem = iconProblem(data, path30.extname(file).slice(1).toLowerCase() || void 0);
5593
5830
  if (problem) throw new CliError(`${file}: ${problem}`);
5594
5831
  const written = writeIcon(paths, data);
5595
- print(`${green("Set")} the project icon: ${path29.relative(process.cwd(), written)}`);
5832
+ print(`${green("Set")} the project icon: ${path30.relative(process.cwd(), written)}`);
5596
5833
  await toCloud(paths, written);
5597
5834
  }
5598
5835
 
5599
5836
  // src/cli/commands/import.ts
5600
- import fs33 from "fs";
5601
- import path31 from "path";
5837
+ import fs34 from "fs";
5838
+ import path32 from "path";
5602
5839
 
5603
5840
  // src/import/claude-design.ts
5604
- import fs32 from "fs";
5605
- import path30 from "path";
5841
+ import fs33 from "fs";
5842
+ import path31 from "path";
5606
5843
 
5607
5844
  // src/import/markup.ts
5608
5845
  var VOID = /* @__PURE__ */ new Set([
@@ -6210,11 +6447,11 @@ function boardId(file, used) {
6210
6447
  return id2;
6211
6448
  }
6212
6449
  function importClaudeDesign(dir, options = {}) {
6213
- const canvasFile = path30.join(dir, "canvas.json");
6214
- if (!fs32.existsSync(canvasFile))
6450
+ const canvasFile = path31.join(dir, "canvas.json");
6451
+ if (!fs33.existsSync(canvasFile))
6215
6452
  throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
6216
- const source = JSON.parse(fs32.readFileSync(canvasFile, "utf8"));
6217
- const templates = fs32.readdirSync(dir).filter((file) => file.endsWith(EXT));
6453
+ const source = JSON.parse(fs33.readFileSync(canvasFile, "utf8"));
6454
+ const templates = fs33.readdirSync(dir).filter((file) => file.endsWith(EXT));
6218
6455
  if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
6219
6456
  const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
6220
6457
  const warnings = /* @__PURE__ */ new Map();
@@ -6241,7 +6478,7 @@ function importClaudeDesign(dir, options = {}) {
6241
6478
  try {
6242
6479
  files.set(
6243
6480
  `${name}.jsx`,
6244
- convertFile(name, fs32.readFileSync(path30.join(dir, file), "utf8"), { known, routes, warn: warn2 })
6481
+ convertFile(name, fs33.readFileSync(path31.join(dir, file), "utf8"), { known, routes, warn: warn2 })
6245
6482
  );
6246
6483
  } catch (error) {
6247
6484
  warn2(`could not be converted: ${error.message}`);
@@ -6289,7 +6526,7 @@ function importClaudeDesign(dir, options = {}) {
6289
6526
  screens++;
6290
6527
  }
6291
6528
  const canvas = {
6292
- title: options.title ?? source.title ?? path30.basename(path30.resolve(dir)),
6529
+ title: options.title ?? source.title ?? path31.basename(path31.resolve(dir)),
6293
6530
  description: "Imported from Claude Design.",
6294
6531
  // The templates bring their own styles; the design system's reset would restyle them.
6295
6532
  system: false,
@@ -6301,21 +6538,21 @@ function importClaudeDesign(dir, options = {}) {
6301
6538
  // src/cli/commands/import.ts
6302
6539
  var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
6303
6540
  function runImport(paths, from, options) {
6304
- const dir = path31.resolve(from);
6305
- if (!fs33.existsSync(dir) || !fs33.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
6541
+ const dir = path32.resolve(from);
6542
+ if (!fs34.existsSync(dir) || !fs34.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
6306
6543
  let result;
6307
6544
  try {
6308
6545
  result = importClaudeDesign(dir, { title: options.title });
6309
6546
  } catch (error) {
6310
6547
  throw new CliError(error.message);
6311
6548
  }
6312
- const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path31.basename(dir)) || "imported");
6549
+ const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path32.basename(dir)) || "imported");
6313
6550
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
6314
- const out = path31.join(paths.canvases, id2);
6315
- if (fs33.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
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)}
6551
+ const out = path32.join(paths.canvases, id2);
6552
+ if (fs34.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
6553
+ fs34.mkdirSync(path32.join(out, "screens"), { recursive: true });
6554
+ for (const [name, content] of result.files) fs34.writeFileSync(path32.join(out, "screens", name), content);
6555
+ fs34.writeFileSync(path32.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
6319
6556
  `);
6320
6557
  print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
6321
6558
  print(
@@ -6335,7 +6572,7 @@ function runImport(paths, from, options) {
6335
6572
 
6336
6573
  // src/cli/commands/merge.ts
6337
6574
  import { spawnSync as spawnSync2 } from "child_process";
6338
- import path32 from "path";
6575
+ import path33 from "path";
6339
6576
  import { createInterface } from "readline/promises";
6340
6577
  var KIND = {
6341
6578
  text: "both sides changed lines, the markers show them",
@@ -6343,15 +6580,15 @@ var KIND = {
6343
6580
  deleted_here: "deleted here, changed in the cloud; the cloud version is in place",
6344
6581
  deleted_in_cloud: "deleted in the cloud, changed here; yours is in place"
6345
6582
  };
6346
- function matchTargets(paths, states, targets) {
6583
+ function matchTargets(paths, states, targets2) {
6347
6584
  const picked = [];
6348
- for (const target of targets) {
6585
+ for (const target of targets2) {
6349
6586
  const unit = states.find((state) => state.unit === unitArg(target));
6350
6587
  if (unit) {
6351
6588
  for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
6352
6589
  continue;
6353
6590
  }
6354
- const rel = [path32.relative(paths.design, path32.resolve(target)), target.replace(/^\.design\//, "")];
6591
+ const rel = [path33.relative(paths.design, path33.resolve(target)), target.replace(/^\.design\//, "")];
6355
6592
  const found = states.flatMap(
6356
6593
  (state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
6357
6594
  );
@@ -6367,12 +6604,12 @@ function printOpen(paths, states) {
6367
6604
  print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
6368
6605
  for (const conflict of open2) {
6369
6606
  print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
6370
- const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path32.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6607
+ const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path33.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
6371
6608
  print(` ${dim(sides)}`);
6372
6609
  }
6373
6610
  }
6374
6611
  }
6375
- async function runMerge(paths, targets, options) {
6612
+ async function runMerge(paths, targets2, options) {
6376
6613
  if (options.here && options.cloud) throw new CliError("Pick one side: --here or --cloud");
6377
6614
  const link = requireLink(paths);
6378
6615
  const states = mergingUnits(paths);
@@ -6388,7 +6625,7 @@ async function runMerge(paths, targets, options) {
6388
6625
  };
6389
6626
  if (options.here || options.cloud) {
6390
6627
  const side = options.here ? "here" : "cloud";
6391
- const picked = targets.length ? matchTargets(paths, states, targets) : states.flatMap((state) => openConflicts(paths, state).map((conflict) => ({ state, conflict })));
6628
+ const picked = targets2.length ? matchTargets(paths, states, targets2) : states.flatMap((state) => openConflicts(paths, state).map((conflict) => ({ state, conflict })));
6392
6629
  for (const { state, conflict } of picked) {
6393
6630
  resolveConflict(paths, state, conflict.path, side);
6394
6631
  print(`${green("\u2713")} ${conflict.path} ${dim(`(${side === "here" ? "yours" : "the cloud's"})`)}`);
@@ -6397,7 +6634,7 @@ async function runMerge(paths, targets, options) {
6397
6634
  return;
6398
6635
  }
6399
6636
  if (options.done) {
6400
- const only = targets.length ? targets.map(unitArg) : void 0;
6637
+ const only = targets2.length ? targets2.map(unitArg) : void 0;
6401
6638
  close(true, only);
6402
6639
  const left2 = states.filter(
6403
6640
  (state) => (!only || only.includes(state.unit)) && openConflicts(paths, state).some((c) => c.kind === "text")
@@ -6448,7 +6685,7 @@ async function runMerge(paths, targets, options) {
6448
6685
  if (answer === "s") break;
6449
6686
  if (answer === "e" && edit) {
6450
6687
  const editor = process.env.VISUAL || process.env.EDITOR || "vi";
6451
- spawnSync2(editor, [path32.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6688
+ spawnSync2(editor, [path33.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
6452
6689
  if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
6453
6690
  print(yellow(" Conflict markers are still there."));
6454
6691
  }
@@ -6467,7 +6704,7 @@ async function runMerge(paths, targets, options) {
6467
6704
  }
6468
6705
 
6469
6706
  // src/cli/commands/rename.ts
6470
- import fs34 from "fs";
6707
+ import fs35 from "fs";
6471
6708
  async function runRename(paths, name) {
6472
6709
  const trimmed = name.trim();
6473
6710
  if (!trimmed) throw new CliError("Usage: design rename <name>");
@@ -6482,17 +6719,17 @@ async function runRename(paths, name) {
6482
6719
  }
6483
6720
  let config = {};
6484
6721
  try {
6485
- config = JSON.parse(fs34.readFileSync(paths.config, "utf8"));
6722
+ config = JSON.parse(fs35.readFileSync(paths.config, "utf8"));
6486
6723
  } catch {
6487
6724
  }
6488
- fs34.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
6725
+ fs35.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
6489
6726
  `);
6490
6727
  print(`${green("Renamed")} the project to ${trimmed} (.design/design.json)`);
6491
6728
  }
6492
6729
 
6493
6730
  // src/cli/commands/scaffold.ts
6494
- import fs35 from "fs";
6495
- import path33 from "path";
6731
+ import fs36 from "fs";
6732
+ import path34 from "path";
6496
6733
 
6497
6734
  // src/cli/templates.ts
6498
6735
  var TOKENS_CSS = `/*
@@ -6703,40 +6940,40 @@ function mainScreen(canvasId) {
6703
6940
 
6704
6941
  // src/cli/commands/scaffold.ts
6705
6942
  function write(file, content, root) {
6706
- if (fs35.existsSync(file)) {
6707
- print(` ${dim("exists ")} ${path33.relative(root, file)}`);
6943
+ if (fs36.existsSync(file)) {
6944
+ print(` ${dim("exists ")} ${path34.relative(root, file)}`);
6708
6945
  return false;
6709
6946
  }
6710
- fs35.mkdirSync(path33.dirname(file), { recursive: true });
6711
- fs35.writeFileSync(file, content);
6712
- print(` ${green("created")} ${path33.relative(root, file)}`);
6947
+ fs36.mkdirSync(path34.dirname(file), { recursive: true });
6948
+ fs36.writeFileSync(file, content);
6949
+ print(` ${green("created")} ${path34.relative(root, file)}`);
6713
6950
  return true;
6714
6951
  }
6715
6952
  function ensureGitignored(root) {
6716
- const file = path33.join(root, ".gitignore");
6717
- const current = fs35.existsSync(file) ? fs35.readFileSync(file, "utf8") : "";
6953
+ const file = path34.join(root, ".gitignore");
6954
+ const current = fs36.existsSync(file) ? fs36.readFileSync(file, "utf8") : "";
6718
6955
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
6719
6956
  if (covered) return "present";
6720
6957
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
6721
- fs35.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
6958
+ fs36.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
6722
6959
  .design/
6723
6960
  `);
6724
6961
  return "added";
6725
6962
  }
6726
6963
  function projectName(root) {
6727
6964
  try {
6728
- const manifest = JSON.parse(fs35.readFileSync(path33.join(root, "package.json"), "utf8"));
6965
+ const manifest = JSON.parse(fs36.readFileSync(path34.join(root, "package.json"), "utf8"));
6729
6966
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
6730
6967
  } catch {
6731
6968
  }
6732
- return path33.basename(root);
6969
+ return path34.basename(root);
6733
6970
  }
6734
6971
  function runInit(root, options) {
6735
6972
  const paths = designPaths(root);
6736
6973
  print(`Setting up ${DESIGN_DIR} in ${root}`);
6737
6974
  write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
6738
6975
  `, root);
6739
- fs35.mkdirSync(paths.canvases, { recursive: true });
6976
+ fs36.mkdirSync(paths.canvases, { recursive: true });
6740
6977
  if (options.gitignore) {
6741
6978
  const result = ensureGitignored(root);
6742
6979
  print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
@@ -6750,44 +6987,44 @@ function runSystemInit(root, options) {
6750
6987
  const paths = designPaths(root);
6751
6988
  let base = projectName(root);
6752
6989
  try {
6753
- base = JSON.parse(fs35.readFileSync(paths.config, "utf8")).name || base;
6990
+ base = JSON.parse(fs36.readFileSync(paths.config, "utf8")).name || base;
6754
6991
  } catch {
6755
6992
  }
6756
6993
  const name = options.name ?? `${base} design system`;
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)}
6994
+ print(`Scaffolding the design system in ${path34.relative(root, paths.system)}`);
6995
+ write(path34.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
6759
6996
  `, root);
6760
- write(path33.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6997
+ write(path34.join(paths.system, "tokens.css"), TOKENS_CSS, root);
6761
6998
  if (!options.empty) {
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);
6999
+ write(path34.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
7000
+ write(path34.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
7001
+ write(path34.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
6765
7002
  }
6766
7003
  }
6767
7004
  function runNew(root, id2, options) {
6768
7005
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
6769
7006
  const paths = designPaths(root);
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}`);
7007
+ const dir = path34.join(paths.canvases, id2);
7008
+ if (fs36.existsSync(path34.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
6772
7009
  print(`Creating canvas "${id2}"`);
6773
7010
  const title = options.title ?? titleize(id2);
6774
7011
  if (options.empty) {
6775
7012
  write(
6776
- path33.join(dir, "canvas.json"),
7013
+ path34.join(dir, "canvas.json"),
6777
7014
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
6778
7015
  `,
6779
7016
  root
6780
7017
  );
6781
- fs35.mkdirSync(path33.join(dir, "screens"), { recursive: true });
7018
+ fs36.mkdirSync(path34.join(dir, "screens"), { recursive: true });
6782
7019
  } else {
6783
- write(path33.join(dir, "canvas.json"), canvasJson(title), root);
6784
- write(path33.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
7020
+ write(path34.join(dir, "canvas.json"), canvasJson(title), root);
7021
+ write(path34.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
6785
7022
  }
6786
7023
  }
6787
7024
 
6788
7025
  // src/cli/commands/shot.ts
6789
- import fs36 from "fs";
6790
- import path34 from "path";
7026
+ import fs37 from "fs";
7027
+ import path35 from "path";
6791
7028
  var OVERVIEW_TIMEOUT = 9e4;
6792
7029
  async function runShot(paths, target, options) {
6793
7030
  const [canvasId = "", screenId] = target.split("/");
@@ -6803,15 +7040,15 @@ async function runShot(paths, target, options) {
6803
7040
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
6804
7041
  }
6805
7042
  const theme2 = options.theme ?? doc.theme ?? "light";
6806
- const outBase = path34.resolve(options.out ?? paths.shots);
6807
- const outDir = path34.join(outBase, canvasId);
6808
- fs36.mkdirSync(outDir, { recursive: true });
7043
+ const outBase = path35.resolve(options.out ?? paths.shots);
7044
+ const outDir = path35.join(outBase, canvasId);
7045
+ fs37.mkdirSync(outDir, { recursive: true });
6809
7046
  const base = await serverBase(paths);
6810
7047
  const browser = await launchChrome();
6811
7048
  let failed = false;
6812
7049
  try {
6813
7050
  if (options.overview) {
6814
- if (!fs36.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
7051
+ if (!fs37.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
6815
7052
  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);
6816
7053
  for (const pageId of pages) {
6817
7054
  const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
@@ -6827,7 +7064,7 @@ async function runShot(paths, target, options) {
6827
7064
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
6828
7065
  }
6829
7066
  await page.waitForTimeout(400);
6830
- const file = path34.join(outDir, `${pageId}.overview.png`);
7067
+ const file = path35.join(outDir, `${pageId}.overview.png`);
6831
7068
  await page.screenshot({ path: file });
6832
7069
  await page.close();
6833
7070
  print(file);
@@ -6856,7 +7093,7 @@ async function shootFrames(browser, jobs, outDir) {
6856
7093
  let failed = false;
6857
7094
  const results = await mapLimit(jobs, 4, async (job) => {
6858
7095
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
6859
- const file = path34.join(outDir, `${job.id}.${job.theme}.png`);
7096
+ const file = path35.join(outDir, `${job.id}.${job.theme}.png`);
6860
7097
  try {
6861
7098
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
6862
7099
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -6888,8 +7125,8 @@ async function shootSystem(paths, project, id2, options) {
6888
7125
  if (id2 && !all.some((job) => job.id === id2)) {
6889
7126
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
6890
7127
  }
6891
- const outDir = path34.join(path34.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
6892
- fs36.mkdirSync(outDir, { recursive: true });
7128
+ const outDir = path35.join(path35.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
7129
+ fs37.mkdirSync(outDir, { recursive: true });
6893
7130
  const base = await serverBase(paths);
6894
7131
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
6895
7132
  const browser = await launchChrome();
@@ -6901,10 +7138,10 @@ async function shootSystem(paths, project, id2, options) {
6901
7138
  }
6902
7139
 
6903
7140
  // src/cli/commands/snapshots.ts
6904
- async function runSnapshots(paths, targets, options) {
7141
+ async function runSnapshots(paths, targets2, options) {
6905
7142
  const known = new DesignProject(paths, DEV_URLS).canvasIds();
6906
7143
  const picked = /* @__PURE__ */ new Map();
6907
- for (const target of targets) {
7144
+ for (const target of targets2) {
6908
7145
  const [canvas = "", screen] = target.split("/");
6909
7146
  if (!known.includes(canvas))
6910
7147
  throw new CliError(`No canvas "${canvas}". Canvases: ${known.join(", ") || "none yet"}`);
@@ -6939,7 +7176,7 @@ async function runSnapshots(paths, targets, options) {
6939
7176
  }
6940
7177
 
6941
7178
  // src/cli/commands/cloud-status.ts
6942
- import path35 from "path";
7179
+ import path36 from "path";
6943
7180
  var STATE_TEXT = {
6944
7181
  in_sync: green,
6945
7182
  local_changes: yellow,
@@ -6962,7 +7199,7 @@ function detail(paths, status) {
6962
7199
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
6963
7200
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
6964
7201
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
6965
- if (status.conflictRev !== void 0) parts.push(`merge ${path35.relative(paths.root, incomingDir(paths, status.key))}`);
7202
+ if (status.conflictRev !== void 0) parts.push(`merge ${path36.relative(paths.root, incomingDir(paths, status.key))}`);
6966
7203
  return parts.join(" \xB7 ");
6967
7204
  }
6968
7205
  async function printCloudStatus(paths, label) {
@@ -7138,12 +7375,15 @@ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>;
7138
7375
  merge [<unit|file>\u2026] [--here|--cloud] Conflicts a merge left: lists them (in a terminal, walks
7139
7376
  [--done] [--json] through them); --here / --cloud settles files or units;
7140
7377
  --done closes a merge once no markers are left
7141
- url <canvas> [--json] The canvas in the web app, and its public link if published
7378
+ url <canvas> [--json] The canvas in the web app, and its public links: the canvas's
7379
+ and its screens' published on their own
7142
7380
  history <canvas|system> [--json] Stored revisions, newest first: when, who, screens, size
7143
7381
  rollback <canvas> <rev> Make an old revision current again in the cloud (a new
7144
7382
  revision), then pull it here
7145
- publish <canvas> Give the canvas a public link anyone can open (owner only)
7146
- unpublish <canvas> Turn the public link off
7383
+ publish <canvas>[/<screen>] Give the canvas a public link anyone can open (owner only);
7384
+ with a screen, a link to that screen alone, which names
7385
+ nothing of the project and opens no other screen
7386
+ unpublish <canvas>[/<screen>] Turn the public link off
7147
7387
  archive <canvas> Archive the canvas in the cloud: out of the list and pushes,
7148
7388
  history kept
7149
7389
  unarchive <canvas> Bring an archived canvas back
@@ -7157,7 +7397,7 @@ ${bold("Environment")}
7157
7397
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
7158
7398
  `;
7159
7399
  function projectPaths(rootOption, requireDesign = true) {
7160
- const found = rootOption ? path36.resolve(rootOption) : findProjectRoot();
7400
+ const found = rootOption ? path37.resolve(rootOption) : findProjectRoot();
7161
7401
  if (!found) {
7162
7402
  if (!requireDesign) return designPaths(process.cwd());
7163
7403
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -7210,7 +7450,7 @@ async function main(argv) {
7210
7450
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
7211
7451
  switch (command) {
7212
7452
  case "init":
7213
- return runInit(values.root ? path36.resolve(values.root) : process.cwd(), {
7453
+ return runInit(values.root ? path37.resolve(values.root) : process.cwd(), {
7214
7454
  name: values.name,
7215
7455
  gitignore: !values["no-gitignore"]
7216
7456
  });
@@ -7317,12 +7557,12 @@ function relaunchFromRealPath() {
7317
7557
  let realScript;
7318
7558
  let realCwd;
7319
7559
  try {
7320
- realScript = fs37.realpathSync.native(script);
7321
- realCwd = fs37.realpathSync.native(process.cwd());
7560
+ realScript = fs38.realpathSync.native(script);
7561
+ realCwd = fs38.realpathSync.native(process.cwd());
7322
7562
  } catch {
7323
7563
  return false;
7324
7564
  }
7325
- const same = (a, b) => path36.resolve(a).toLowerCase() === path36.resolve(b).toLowerCase();
7565
+ const same = (a, b) => path37.resolve(a).toLowerCase() === path37.resolve(b).toLowerCase();
7326
7566
  if (same(realScript, script) && same(realCwd, process.cwd())) return false;
7327
7567
  const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
7328
7568
  stdio: "inherit",