foss-design 0.8.1 → 0.9.1
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 +1443 -831
- package/dist/viewer/assets/{index-COUW2y60.js → index-Bcfi2Cps.js} +2 -2
- package/dist/viewer/assets/index-CH7RhGQ5.css +2 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer-lib/index.js +7 -4
- package/dist/viewer-lib/styles.css +4 -0
- package/dist/viewer-lib/types/src/shared/types.d.ts +6 -0
- package/dist/viewer-lib/types/viewer/src/lib/viewer.d.ts +3 -1
- package/package.json +2 -1
- package/dist/viewer/assets/index-CiqSYqfx.css +0 -2
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import
|
|
4
|
+
import path34 from "path";
|
|
5
5
|
import { parseArgs } from "util";
|
|
6
6
|
|
|
7
7
|
// src/core/paths.ts
|
|
@@ -346,8 +346,8 @@ function routeError(pattern) {
|
|
|
346
346
|
}
|
|
347
347
|
return null;
|
|
348
348
|
}
|
|
349
|
-
function split(
|
|
350
|
-
return
|
|
349
|
+
function split(path35) {
|
|
350
|
+
return path35.split("/").filter(Boolean);
|
|
351
351
|
}
|
|
352
352
|
function compileRoute(pattern) {
|
|
353
353
|
return {
|
|
@@ -366,8 +366,8 @@ function decode2(part) {
|
|
|
366
366
|
return part;
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
-
function scoreRoute(route2,
|
|
370
|
-
const parts = split(
|
|
369
|
+
function scoreRoute(route2, path35) {
|
|
370
|
+
const parts = split(path35.split(/[?#]/)[0] ?? "").map(decode2);
|
|
371
371
|
const walk2 = (s, p) => {
|
|
372
372
|
if (s === route2.segments.length) return p === parts.length ? 0 : null;
|
|
373
373
|
const segment = route2.segments[s];
|
|
@@ -385,11 +385,11 @@ function scoreRoute(route2, path31) {
|
|
|
385
385
|
};
|
|
386
386
|
return walk2(0, 0);
|
|
387
387
|
}
|
|
388
|
-
function matchRoute(entries,
|
|
388
|
+
function matchRoute(entries, path35) {
|
|
389
389
|
let best = null;
|
|
390
390
|
for (const entry of entries) {
|
|
391
391
|
for (const route2 of entry.routes) {
|
|
392
|
-
const score = scoreRoute(route2,
|
|
392
|
+
const score = scoreRoute(route2, path35);
|
|
393
393
|
if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
|
|
394
394
|
}
|
|
395
395
|
}
|
|
@@ -511,12 +511,14 @@ var CanvasSchema = z.strictObject({
|
|
|
511
511
|
system: z.boolean().optional(),
|
|
512
512
|
/** Theme screens open in (default follows the viewer). */
|
|
513
513
|
theme: theme.optional(),
|
|
514
|
+
/** The canvas's picture in lists: a screen id, or an image in the canvas folder (PNG, JPEG, WebP, SVG, 512 KB). */
|
|
515
|
+
cover: z.string().min(1).optional(),
|
|
514
516
|
pages: z.array(z.unknown()).min(1, "add at least one page")
|
|
515
517
|
});
|
|
516
518
|
var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
517
|
-
function formatPath(base,
|
|
519
|
+
function formatPath(base, path35) {
|
|
518
520
|
let out = base;
|
|
519
|
-
for (const part of
|
|
521
|
+
for (const part of path35) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
|
|
520
522
|
return out;
|
|
521
523
|
}
|
|
522
524
|
function describeZodError(error, base = "") {
|
|
@@ -754,8 +756,37 @@ function loadCanvas(paths, canvasId, urls, snapshots) {
|
|
|
754
756
|
doc.pages.push(out);
|
|
755
757
|
});
|
|
756
758
|
checkNavigation(doc, canvasCodeFiles(dir, screens), issues, fileRel, (file2) => relToRoot(paths, file2));
|
|
759
|
+
if (top.success && top.data.cover) doc.cover = resolveCover(top.data.cover, dir, screens, paths, urls, report);
|
|
757
760
|
return { doc, screens };
|
|
758
761
|
}
|
|
762
|
+
var MAX_COVER_BYTES = 512 * 1024;
|
|
763
|
+
var COVER_IMAGE = /\.(png|jpe?g|webp|svg)$/i;
|
|
764
|
+
function resolveCover(cover, dir, screens, paths, urls, report) {
|
|
765
|
+
if (screens.some((screen) => screen.id === cover)) return { screen: cover };
|
|
766
|
+
const file = path3.resolve(dir, cover);
|
|
767
|
+
if (!isInside(dir, file) || !COVER_IMAGE.test(file)) {
|
|
768
|
+
report(
|
|
769
|
+
"error",
|
|
770
|
+
`cover "${cover}" is neither a screen id nor a PNG, JPEG, WebP or SVG in the canvas folder`,
|
|
771
|
+
"cover"
|
|
772
|
+
);
|
|
773
|
+
return void 0;
|
|
774
|
+
}
|
|
775
|
+
if (!fs3.existsSync(file)) {
|
|
776
|
+
report("error", `cover "${cover}" does not exist`, "cover");
|
|
777
|
+
return void 0;
|
|
778
|
+
}
|
|
779
|
+
const { size: size2, mtimeMs } = fs3.statSync(file);
|
|
780
|
+
if (size2 > MAX_COVER_BYTES) {
|
|
781
|
+
report(
|
|
782
|
+
"error",
|
|
783
|
+
`cover "${cover}" is ${Math.ceil(size2 / 1024)} KB; the limit is ${MAX_COVER_BYTES / 1024} KB`,
|
|
784
|
+
"cover"
|
|
785
|
+
);
|
|
786
|
+
return void 0;
|
|
787
|
+
}
|
|
788
|
+
return { url: `${urls.file(relToDesign(paths, file))}?v=${Math.round(mtimeMs)}` };
|
|
789
|
+
}
|
|
759
790
|
var GO_IMPORT = /import\s*\{[^}]*\bgo\b[^}]*\}\s*from\s*['"]@design\/runtime['"]/;
|
|
760
791
|
var GO_USE = /\bgo\s*\(/g;
|
|
761
792
|
var GO_REMOVED = 'go() is no longer supported: link to the screen instead (<a href="/verify">, or history.pushState in code) and give that screen a "route" in canvas.json';
|
|
@@ -820,7 +851,8 @@ function checkNavigation(doc, codeFiles, issues, canvasFile, rel) {
|
|
|
820
851
|
function summarize(canvas) {
|
|
821
852
|
const { doc } = canvas;
|
|
822
853
|
const screens = doc.pages.flatMap((page) => page.sections.flatMap((section) => section.items));
|
|
823
|
-
const
|
|
854
|
+
const chosen = doc.cover && "screen" in doc.cover ? doc.cover.screen : null;
|
|
855
|
+
const firstScreen = chosen ? screens.find((item) => item.kind === "screen" && item.id === chosen) : screens.find((item) => item.kind === "screen" && item.snapshots);
|
|
824
856
|
return {
|
|
825
857
|
id: doc.id,
|
|
826
858
|
title: doc.title,
|
|
@@ -829,19 +861,21 @@ function summarize(canvas) {
|
|
|
829
861
|
screens: screens.filter((item) => item.kind === "screen" || item.kind === "url").length,
|
|
830
862
|
updatedAt: doc.updatedAt,
|
|
831
863
|
issues: doc.issues.filter((issue) => issue.severity === "error").length,
|
|
832
|
-
cover: firstScreen && firstScreen.kind === "screen" ? firstScreen.snapshots?.light ?? firstScreen.snapshots?.dark : void 0
|
|
864
|
+
cover: doc.cover && "url" in doc.cover ? doc.cover.url : firstScreen && firstScreen.kind === "screen" ? firstScreen.snapshots?.light ?? firstScreen.snapshots?.dark : void 0
|
|
833
865
|
};
|
|
834
866
|
}
|
|
835
867
|
|
|
836
868
|
// src/core/icon.ts
|
|
869
|
+
import { createHash as createHash2 } from "crypto";
|
|
837
870
|
import fs4 from "fs";
|
|
838
871
|
import path4 from "path";
|
|
839
872
|
var ICON_EXTENSIONS = ["svg", "png", "webp"];
|
|
840
873
|
var MAX_ICON_BYTES = 256 * 1024;
|
|
841
874
|
var iconName = (ext) => `icon.${ext}`;
|
|
842
875
|
function iconFile(paths) {
|
|
876
|
+
migrateIcon(paths);
|
|
843
877
|
for (const ext of ICON_EXTENSIONS) {
|
|
844
|
-
const file = path4.join(paths.
|
|
878
|
+
const file = path4.join(paths.design, iconName(ext));
|
|
845
879
|
if (fs4.existsSync(file) && fs4.statSync(file).isFile()) return file;
|
|
846
880
|
}
|
|
847
881
|
return null;
|
|
@@ -864,21 +898,33 @@ function iconProblem(data, ext) {
|
|
|
864
898
|
return null;
|
|
865
899
|
}
|
|
866
900
|
function writeIcon(paths, data) {
|
|
867
|
-
for (const ext2 of ICON_EXTENSIONS) fs4.rmSync(path4.join(paths.
|
|
901
|
+
for (const ext2 of ICON_EXTENSIONS) fs4.rmSync(path4.join(paths.design, iconName(ext2)), { force: true });
|
|
868
902
|
if (!data) return null;
|
|
869
903
|
const ext = sniffIcon(data);
|
|
870
|
-
fs4.mkdirSync(paths.
|
|
871
|
-
const file = path4.join(paths.
|
|
904
|
+
fs4.mkdirSync(paths.design, { recursive: true });
|
|
905
|
+
const file = path4.join(paths.design, iconName(ext));
|
|
872
906
|
fs4.writeFileSync(file, data);
|
|
873
907
|
return file;
|
|
874
908
|
}
|
|
875
|
-
function
|
|
909
|
+
function migrateIcon(paths) {
|
|
910
|
+
for (const ext of ICON_EXTENSIONS) {
|
|
911
|
+
const old = path4.join(paths.system, iconName(ext));
|
|
912
|
+
if (!fs4.existsSync(old)) continue;
|
|
913
|
+
const target = path4.join(paths.design, iconName(ext));
|
|
914
|
+
if (ICON_EXTENSIONS.some((other) => fs4.existsSync(path4.join(paths.design, iconName(other))))) fs4.rmSync(old);
|
|
915
|
+
else fs4.renameSync(old, target);
|
|
916
|
+
}
|
|
876
917
|
try {
|
|
877
|
-
|
|
918
|
+
fs4.rmdirSync(paths.system);
|
|
878
919
|
} catch {
|
|
879
|
-
return false;
|
|
880
920
|
}
|
|
881
921
|
}
|
|
922
|
+
var iconHash = (file) => createHash2("sha256").update(fs4.readFileSync(file)).digest("hex");
|
|
923
|
+
var ICON_TYPES = {
|
|
924
|
+
svg: "image/svg+xml",
|
|
925
|
+
png: "image/png",
|
|
926
|
+
webp: "image/webp"
|
|
927
|
+
};
|
|
882
928
|
|
|
883
929
|
// src/core/public.ts
|
|
884
930
|
import fs5 from "fs";
|
|
@@ -1251,7 +1297,7 @@ function guessSources(componentsDir, stem) {
|
|
|
1251
1297
|
return [];
|
|
1252
1298
|
}
|
|
1253
1299
|
function loadSystem(paths, urls) {
|
|
1254
|
-
if (!fs7.existsSync(paths.system)
|
|
1300
|
+
if (!fs7.existsSync(paths.system)) return null;
|
|
1255
1301
|
const issues = [];
|
|
1256
1302
|
const configFile = path7.join(paths.system, "system.json");
|
|
1257
1303
|
const report = (severity, file, message, at) => issues.push({ severity, file: relToRoot(paths, file), message, at });
|
|
@@ -1480,6 +1526,11 @@ var DesignProject = class {
|
|
|
1480
1526
|
return this.publicCache?.set.has(rel) ?? false;
|
|
1481
1527
|
}
|
|
1482
1528
|
name() {
|
|
1529
|
+
try {
|
|
1530
|
+
const linked = JSON.parse(fs8.readFileSync(path8.join(this.paths.design, "cloud.json"), "utf8")).name;
|
|
1531
|
+
if (typeof linked === "string" && linked) return linked;
|
|
1532
|
+
} catch {
|
|
1533
|
+
}
|
|
1483
1534
|
const configured = this.config().config.name;
|
|
1484
1535
|
if (configured) return configured;
|
|
1485
1536
|
try {
|
|
@@ -1902,7 +1953,7 @@ function linkShippedPackages(paths, appDir = null) {
|
|
|
1902
1953
|
}
|
|
1903
1954
|
|
|
1904
1955
|
// src/server/vite.ts
|
|
1905
|
-
import { createHash as
|
|
1956
|
+
import { createHash as createHash3 } from "crypto";
|
|
1906
1957
|
import fs11 from "fs";
|
|
1907
1958
|
import path11 from "path";
|
|
1908
1959
|
import tailwindcss from "@tailwindcss/vite";
|
|
@@ -1928,7 +1979,7 @@ function dropStaleDepCache(paths, appDir = null) {
|
|
|
1928
1979
|
const stamp = path11.join(paths.cache, "vite-runtime");
|
|
1929
1980
|
let runtime = "";
|
|
1930
1981
|
try {
|
|
1931
|
-
runtime =
|
|
1982
|
+
runtime = createHash3("sha256").update(fs11.readFileSync(PKG.runtime)).digest("hex");
|
|
1932
1983
|
} catch {
|
|
1933
1984
|
}
|
|
1934
1985
|
if (appDir) runtime += ` ${appDir}`;
|
|
@@ -2127,12 +2178,14 @@ async function buildSite(paths, out, options = {}) {
|
|
|
2127
2178
|
}
|
|
2128
2179
|
const files = /* @__PURE__ */ new Set();
|
|
2129
2180
|
for (const canvas of canvases) {
|
|
2181
|
+
const cover = canvas.doc.cover && "url" in canvas.doc.cover ? canvas.doc.cover.url.split("?")[0] : null;
|
|
2182
|
+
if (cover?.startsWith("_f/")) files.add(cover);
|
|
2130
2183
|
for (const page of canvas.doc.pages)
|
|
2131
2184
|
for (const section of page.sections)
|
|
2132
2185
|
for (const item of section.items) if (item.kind === "image" && item.url.startsWith("_f/")) files.add(item.url);
|
|
2133
2186
|
}
|
|
2134
2187
|
for (const asset of system?.doc.assets ?? []) files.add(asset.url);
|
|
2135
|
-
const icon = project.iconUrl();
|
|
2188
|
+
const icon = options.includeViewer === false ? null : project.iconUrl();
|
|
2136
2189
|
if (icon) files.add(icon.split("?")[0]);
|
|
2137
2190
|
for (const url of files) {
|
|
2138
2191
|
const rel = url.slice("_f/".length).split("/").map(decodeURIComponent).join("/");
|
|
@@ -2308,12 +2361,12 @@ async function runBuild(paths, canvases, options) {
|
|
|
2308
2361
|
import os from "os";
|
|
2309
2362
|
|
|
2310
2363
|
// src/cloud/units.ts
|
|
2311
|
-
import { createHash as
|
|
2364
|
+
import { createHash as createHash4 } from "crypto";
|
|
2312
2365
|
import fs14 from "fs";
|
|
2313
2366
|
import path14 from "path";
|
|
2314
2367
|
var SYSTEM_UNIT = "system";
|
|
2315
2368
|
var CANVAS_PREFIX = "canvas/";
|
|
2316
|
-
var LOCAL_ONLY = /* @__PURE__ */ new Set(["cloud.json", "node_modules"]);
|
|
2369
|
+
var LOCAL_ONLY = /* @__PURE__ */ new Set(["cloud.json", "node_modules", "icon.svg", "icon.png", "icon.webp"]);
|
|
2317
2370
|
var canvasUnit = (id2) => `${CANVAS_PREFIX}${id2}`;
|
|
2318
2371
|
function unitCanvasId(key) {
|
|
2319
2372
|
return key.startsWith(CANVAS_PREFIX) ? key.slice(CANVAS_PREFIX.length) : null;
|
|
@@ -2340,10 +2393,6 @@ function unitRoot(paths, key) {
|
|
|
2340
2393
|
const id2 = unitCanvasId(key);
|
|
2341
2394
|
return id2 === null ? paths.design : path14.join(paths.canvases, id2);
|
|
2342
2395
|
}
|
|
2343
|
-
function unitRelative(key, rel) {
|
|
2344
|
-
const id2 = unitCanvasId(key);
|
|
2345
|
-
return id2 === null ? rel : rel.slice(`${CANVAS_PREFIX}${id2}/`.length);
|
|
2346
|
-
}
|
|
2347
2396
|
function isUnitPath(key, rel) {
|
|
2348
2397
|
if (!rel || rel.includes("\\") || rel.includes("\0") || rel.startsWith("/")) return false;
|
|
2349
2398
|
const segments = rel.split("/");
|
|
@@ -2353,7 +2402,7 @@ function isUnitPath(key, rel) {
|
|
|
2353
2402
|
return id2 !== null && segments[0] === "canvas" && segments[1] === id2 && segments.length > 2;
|
|
2354
2403
|
}
|
|
2355
2404
|
function hashBuffer(data) {
|
|
2356
|
-
return
|
|
2405
|
+
return createHash4("sha256").update(data).digest("hex");
|
|
2357
2406
|
}
|
|
2358
2407
|
function hashFile(file) {
|
|
2359
2408
|
const data = fs14.readFileSync(file);
|
|
@@ -2526,7 +2575,8 @@ function parseProject(value, fallbackRole) {
|
|
|
2526
2575
|
name: text(data.name) ?? id2,
|
|
2527
2576
|
role: text(data.role) ?? fallbackRole,
|
|
2528
2577
|
archived: flag(data.archived, data.archivedAt, data.archived_at),
|
|
2529
|
-
banned: flag(data.banned, data.bannedAt, data.banned_at)
|
|
2578
|
+
banned: flag(data.banned, data.bannedAt, data.banned_at),
|
|
2579
|
+
icon: /[?&]v=([0-9a-f]{64})/.exec(text(data.icon) ?? "")?.[1] ?? null
|
|
2530
2580
|
};
|
|
2531
2581
|
}
|
|
2532
2582
|
function parseUnit(value) {
|
|
@@ -2580,7 +2630,7 @@ var CloudClient = class {
|
|
|
2580
2630
|
}
|
|
2581
2631
|
host;
|
|
2582
2632
|
token;
|
|
2583
|
-
async send(method,
|
|
2633
|
+
async send(method, path35, init) {
|
|
2584
2634
|
const headers = {
|
|
2585
2635
|
"User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
|
|
2586
2636
|
Accept: "application/json"
|
|
@@ -2588,7 +2638,7 @@ var CloudClient = class {
|
|
|
2588
2638
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
2589
2639
|
if (init.type) headers["Content-Type"] = init.type;
|
|
2590
2640
|
try {
|
|
2591
|
-
return await fetch(`${this.host}/api${
|
|
2641
|
+
return await fetch(`${this.host}/api${path35}`, {
|
|
2592
2642
|
method,
|
|
2593
2643
|
headers,
|
|
2594
2644
|
body: init.body,
|
|
@@ -2609,8 +2659,8 @@ var CloudClient = class {
|
|
|
2609
2659
|
throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
|
|
2610
2660
|
}
|
|
2611
2661
|
/** JSON in, JSON out; any non-2xx answer throws a CloudError. */
|
|
2612
|
-
async request(method,
|
|
2613
|
-
const response = await this.send(method,
|
|
2662
|
+
async request(method, path35, body) {
|
|
2663
|
+
const response = await this.send(method, path35, {
|
|
2614
2664
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
2615
2665
|
type: body === void 0 ? void 0 : "application/json",
|
|
2616
2666
|
timeout: JSON_TIMEOUT
|
|
@@ -2622,7 +2672,7 @@ var CloudClient = class {
|
|
|
2622
2672
|
try {
|
|
2623
2673
|
return JSON.parse(text2);
|
|
2624
2674
|
} catch {
|
|
2625
|
-
throw new CloudError(`${this.host} answered ${method} ${
|
|
2675
|
+
throw new CloudError(`${this.host} answered ${method} ${path35} with something other than JSON`, 0, "bad_response");
|
|
2626
2676
|
}
|
|
2627
2677
|
}
|
|
2628
2678
|
/** POST /device/code. */
|
|
@@ -2663,6 +2713,34 @@ var CloudClient = class {
|
|
|
2663
2713
|
if (!project) throw new CloudError(`${this.host} did not return the new project`, 0, "bad_response");
|
|
2664
2714
|
return project;
|
|
2665
2715
|
}
|
|
2716
|
+
/** One of the caller's projects, from the project list; null when it is not among them. */
|
|
2717
|
+
async project(projectId) {
|
|
2718
|
+
const { owned, shared } = await this.projects();
|
|
2719
|
+
return [...owned, ...shared].find((item) => item.id === projectId) ?? null;
|
|
2720
|
+
}
|
|
2721
|
+
/** PATCH /projects/:id: the owner renames the project. */
|
|
2722
|
+
async renameProject(projectId, name) {
|
|
2723
|
+
const body = record(await this.request("PATCH", `/projects/${projectId}`, { name }));
|
|
2724
|
+
const project = parseProject(body.project, "owner");
|
|
2725
|
+
if (!project) throw new CloudError(`${this.host} sent no project back`, 0, "bad_response");
|
|
2726
|
+
return project;
|
|
2727
|
+
}
|
|
2728
|
+
/** GET /projects/:id/icon: the icon's bytes and type, or null without one. */
|
|
2729
|
+
async projectIcon(projectId) {
|
|
2730
|
+
const response = await this.send("GET", `/projects/${projectId}/icon`, { timeout: JSON_TIMEOUT });
|
|
2731
|
+
if (response.status === 404) return null;
|
|
2732
|
+
if (!response.ok) await this.fail(response);
|
|
2733
|
+
return { data: Buffer.from(await response.arrayBuffer()), type: response.headers.get("content-type") ?? "" };
|
|
2734
|
+
}
|
|
2735
|
+
/** PUT or DELETE /projects/:id/icon (owner and editors). */
|
|
2736
|
+
async setProjectIcon(projectId, icon) {
|
|
2737
|
+
const response = await this.send(icon ? "PUT" : "DELETE", `/projects/${projectId}/icon`, {
|
|
2738
|
+
body: icon ? new Uint8Array(icon.data) : void 0,
|
|
2739
|
+
type: icon?.type,
|
|
2740
|
+
timeout: JSON_TIMEOUT
|
|
2741
|
+
});
|
|
2742
|
+
if (!response.ok) await this.fail(response);
|
|
2743
|
+
}
|
|
2666
2744
|
async units(projectId) {
|
|
2667
2745
|
const body = await this.request("GET", `/projects/${encodeURIComponent(projectId)}/units`);
|
|
2668
2746
|
const list = Array.isArray(body) ? body : record(body).units;
|
|
@@ -2841,7 +2919,14 @@ function readLink(paths) {
|
|
|
2841
2919
|
}
|
|
2842
2920
|
const conflicts = {};
|
|
2843
2921
|
for (const [key, rev] of Object.entries(data.conflicts ?? {})) if (typeof rev === "number") conflicts[key] = rev;
|
|
2844
|
-
return {
|
|
2922
|
+
return {
|
|
2923
|
+
host: data.host,
|
|
2924
|
+
project: data.project,
|
|
2925
|
+
units,
|
|
2926
|
+
conflicts,
|
|
2927
|
+
...typeof data.name === "string" ? { name: data.name } : {},
|
|
2928
|
+
...typeof data.icon === "string" || data.icon === null ? { icon: data.icon } : {}
|
|
2929
|
+
};
|
|
2845
2930
|
}
|
|
2846
2931
|
function requireLink(paths) {
|
|
2847
2932
|
const link = readLink(paths);
|
|
@@ -2855,6 +2940,8 @@ function writeLink(paths, link) {
|
|
|
2855
2940
|
const data = {
|
|
2856
2941
|
host: link.host,
|
|
2857
2942
|
project: link.project,
|
|
2943
|
+
...link.name !== void 0 ? { name: link.name } : {},
|
|
2944
|
+
...link.icon !== void 0 ? { icon: link.icon } : {},
|
|
2858
2945
|
units: sorted(link.units),
|
|
2859
2946
|
conflicts: sorted(link.conflicts)
|
|
2860
2947
|
};
|
|
@@ -2937,9 +3024,178 @@ var unique = (keys) => [...new Set(keys)];
|
|
|
2937
3024
|
var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ? -1 : a > b ? 1 : 0;
|
|
2938
3025
|
var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
|
|
2939
3026
|
|
|
2940
|
-
// src/
|
|
3027
|
+
// src/capture/cover.ts
|
|
3028
|
+
import fs19 from "fs";
|
|
3029
|
+
|
|
3030
|
+
// src/server/snapshots.ts
|
|
2941
3031
|
import fs17 from "fs";
|
|
2942
3032
|
import path17 from "path";
|
|
3033
|
+
var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
|
|
3034
|
+
function isSafeSegment(value) {
|
|
3035
|
+
return SAFE.test(value);
|
|
3036
|
+
}
|
|
3037
|
+
var SnapshotStore = class {
|
|
3038
|
+
constructor(dir, urlBase = "/api/snapshots") {
|
|
3039
|
+
this.dir = dir;
|
|
3040
|
+
this.urlBase = urlBase;
|
|
3041
|
+
}
|
|
3042
|
+
dir;
|
|
3043
|
+
urlBase;
|
|
3044
|
+
meta = /* @__PURE__ */ new Map();
|
|
3045
|
+
metaFile(canvas) {
|
|
3046
|
+
return path17.join(this.dir, canvas, "meta.json");
|
|
3047
|
+
}
|
|
3048
|
+
load(canvas) {
|
|
3049
|
+
const cached = this.meta.get(canvas);
|
|
3050
|
+
if (cached) return cached;
|
|
3051
|
+
let data = {};
|
|
3052
|
+
try {
|
|
3053
|
+
data = JSON.parse(fs17.readFileSync(this.metaFile(canvas), "utf8"));
|
|
3054
|
+
} catch {
|
|
3055
|
+
}
|
|
3056
|
+
this.meta.set(canvas, data);
|
|
3057
|
+
return data;
|
|
3058
|
+
}
|
|
3059
|
+
url(canvas, id2, theme2, version, thumb = false) {
|
|
3060
|
+
const name = `${encodeURIComponent(id2)}.${theme2}${thumb ? ".thumb" : ""}.png`;
|
|
3061
|
+
return `${this.urlBase}/${encodeURIComponent(canvas)}/${name}?v=${version}`;
|
|
3062
|
+
}
|
|
3063
|
+
lookup = (canvas, id2) => {
|
|
3064
|
+
if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return null;
|
|
3065
|
+
const entry = this.load(canvas)[id2];
|
|
3066
|
+
if (!entry) return null;
|
|
3067
|
+
const urls = {};
|
|
3068
|
+
const thumbs = {};
|
|
3069
|
+
for (const theme2 of ["light", "dark"]) {
|
|
3070
|
+
const version = entry[theme2];
|
|
3071
|
+
if (!version) continue;
|
|
3072
|
+
urls[theme2] = this.url(canvas, id2, theme2, version);
|
|
3073
|
+
thumbs[theme2] = this.url(canvas, id2, theme2, version, true);
|
|
3074
|
+
}
|
|
3075
|
+
return { urls, thumbs, height: entry.height };
|
|
3076
|
+
};
|
|
3077
|
+
/** When the snapshot was taken (ms), or null without one. */
|
|
3078
|
+
version(canvas, id2, theme2) {
|
|
3079
|
+
if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return null;
|
|
3080
|
+
return this.load(canvas)[id2]?.[theme2] ?? null;
|
|
3081
|
+
}
|
|
3082
|
+
save(canvas, id2, theme2, png, height) {
|
|
3083
|
+
if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
|
|
3084
|
+
const dir = path17.join(this.dir, canvas);
|
|
3085
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
3086
|
+
fs17.writeFileSync(path17.join(dir, `${id2}.${theme2}.png`), png);
|
|
3087
|
+
const data = this.load(canvas);
|
|
3088
|
+
const version = Date.now();
|
|
3089
|
+
data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
|
|
3090
|
+
fs17.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
|
|
3091
|
+
return { url: this.url(canvas, id2, theme2, version) };
|
|
3092
|
+
}
|
|
3093
|
+
/** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
|
|
3094
|
+
file(canvas, name) {
|
|
3095
|
+
const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)(\.thumb)?\.png$/i.exec(name);
|
|
3096
|
+
if (!isSafeSegment(canvas) || !match) return null;
|
|
3097
|
+
const file = path17.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
|
|
3098
|
+
if (!fs17.existsSync(file)) return null;
|
|
3099
|
+
return match[3] ? ensureThumb(file) : file;
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
|
|
3103
|
+
// src/capture/chrome.ts
|
|
3104
|
+
import fs18 from "fs";
|
|
3105
|
+
var CANDIDATES = {
|
|
3106
|
+
darwin: [
|
|
3107
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
3108
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
3109
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
3110
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
3111
|
+
],
|
|
3112
|
+
linux: [
|
|
3113
|
+
"/usr/bin/google-chrome",
|
|
3114
|
+
"/usr/bin/google-chrome-stable",
|
|
3115
|
+
"/usr/bin/chromium",
|
|
3116
|
+
"/usr/bin/chromium-browser",
|
|
3117
|
+
"/snap/bin/chromium",
|
|
3118
|
+
"/usr/bin/microsoft-edge"
|
|
3119
|
+
],
|
|
3120
|
+
win32: [
|
|
3121
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
3122
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
3123
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
3124
|
+
]
|
|
3125
|
+
};
|
|
3126
|
+
function findChrome() {
|
|
3127
|
+
const configured = process.env.DESIGN_CHROME;
|
|
3128
|
+
if (configured && fs18.existsSync(configured)) return configured;
|
|
3129
|
+
for (const candidate of CANDIDATES[process.platform] ?? []) if (fs18.existsSync(candidate)) return candidate;
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3132
|
+
async function launchChrome() {
|
|
3133
|
+
const executablePath = findChrome();
|
|
3134
|
+
if (!executablePath) {
|
|
3135
|
+
throw new Error(
|
|
3136
|
+
"No Chrome, Chromium or Edge found. Install one, or point DESIGN_CHROME at a Chromium-based browser binary."
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
3139
|
+
const { chromium } = await import("playwright-core");
|
|
3140
|
+
return chromium.launch({ executablePath, headless: true, args: ["--hide-scrollbars", "--force-color-profile=srgb"] });
|
|
3141
|
+
}
|
|
3142
|
+
var OUTDATED_DEP = /Outdated Optimize Dep/;
|
|
3143
|
+
async function openFrame(page, url, options = {}) {
|
|
3144
|
+
let errors = [];
|
|
3145
|
+
let outdated = () => {
|
|
3146
|
+
};
|
|
3147
|
+
const record2 = (text2) => {
|
|
3148
|
+
errors.push(text2);
|
|
3149
|
+
if (OUTDATED_DEP.test(text2)) outdated();
|
|
3150
|
+
};
|
|
3151
|
+
page.on("pageerror", (error) => record2(error.message));
|
|
3152
|
+
page.on("console", (message) => {
|
|
3153
|
+
if (message.type() !== "error") return;
|
|
3154
|
+
let text2 = message.text().replace(/^(?:\s*%[osdifcO])+\s*/, "");
|
|
3155
|
+
const where = message.location().url;
|
|
3156
|
+
if (/^Failed to load resource/.test(text2) && where && !text2.includes(where)) text2 += ` (${where})`;
|
|
3157
|
+
record2(text2);
|
|
3158
|
+
});
|
|
3159
|
+
let ready = false;
|
|
3160
|
+
let reloads = 0;
|
|
3161
|
+
const onNavigate = () => {
|
|
3162
|
+
reloads++;
|
|
3163
|
+
};
|
|
3164
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3165
|
+
errors = [];
|
|
3166
|
+
reloads = 0;
|
|
3167
|
+
ready = options.waitForReady === false;
|
|
3168
|
+
const turnedAway = new Promise((resolve) => {
|
|
3169
|
+
outdated = () => resolve("outdated");
|
|
3170
|
+
});
|
|
3171
|
+
await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
|
|
3172
|
+
page.on("load", onNavigate);
|
|
3173
|
+
if (!ready) {
|
|
3174
|
+
const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
|
|
3175
|
+
timeout: options.timeoutMs ?? 2e4
|
|
3176
|
+
}).then(() => "ready");
|
|
3177
|
+
signal.catch(() => {
|
|
3178
|
+
});
|
|
3179
|
+
ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
|
|
3180
|
+
}
|
|
3181
|
+
if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
|
|
3182
|
+
page.off("load", onNavigate);
|
|
3183
|
+
if (!reloads && !errors.some((error) => OUTDATED_DEP.test(error))) break;
|
|
3184
|
+
}
|
|
3185
|
+
if (reloads) errors.push(`the page reloaded itself ${reloads === 1 ? "once" : `${reloads} times`} while it loaded`);
|
|
3186
|
+
const held = ready ? 0 : await page.evaluate(() => window.__DESIGN_HOLDS__ ?? 0).catch(() => 0);
|
|
3187
|
+
const runtimeErrors = await page.evaluate(() => window.__DESIGN_ERRORS__ ?? []).catch(() => []);
|
|
3188
|
+
for (const error of runtimeErrors) if (!errors.includes(error)) errors.push(error);
|
|
3189
|
+
const overlay = await page.evaluate(() => {
|
|
3190
|
+
const el = document.querySelector("vite-error-overlay");
|
|
3191
|
+
return el?.shadowRoot?.querySelector(".message")?.textContent ?? null;
|
|
3192
|
+
}).catch(() => null);
|
|
3193
|
+
if (overlay) errors.unshift(overlay.trim());
|
|
3194
|
+
const unique2 = errors.filter(
|
|
3195
|
+
(error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
|
|
3196
|
+
);
|
|
3197
|
+
return { errors: [...new Set(unique2)], ready, ...held ? { held } : {} };
|
|
3198
|
+
}
|
|
2943
3199
|
|
|
2944
3200
|
// src/capture/frames.ts
|
|
2945
3201
|
var AUTO_VIEWPORT_HEIGHT = 900;
|
|
@@ -3013,73 +3269,323 @@ async function mapLimit(items, limit, fn) {
|
|
|
3013
3269
|
return results;
|
|
3014
3270
|
}
|
|
3015
3271
|
|
|
3016
|
-
// src/
|
|
3017
|
-
var
|
|
3018
|
-
function
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3272
|
+
// src/capture/cover.ts
|
|
3273
|
+
var SNAPSHOT_WIDTH = 900;
|
|
3274
|
+
async function refreshCovers(paths, canvasIds, base) {
|
|
3275
|
+
const store = new SnapshotStore(paths.snapshots);
|
|
3276
|
+
const project = new DesignProject(paths, DEV_URLS, store.lookup);
|
|
3277
|
+
const stale = [];
|
|
3278
|
+
for (const id2 of canvasIds) {
|
|
3279
|
+
const canvas = project.canvas(id2);
|
|
3280
|
+
if (!canvas || canvas.doc.cover && "url" in canvas.doc.cover) continue;
|
|
3281
|
+
const screen = canvas.doc.cover?.screen ?? canvas.screens[0]?.id;
|
|
3282
|
+
const source = canvas.screens.find((item) => item.id === screen);
|
|
3283
|
+
if (!screen || !source || !fs19.existsSync(source.file)) continue;
|
|
3284
|
+
const taken = store.version(id2, screen, "light");
|
|
3285
|
+
if (!taken || taken < fs19.statSync(source.file).mtimeMs) stale.push({ canvas: id2, screen });
|
|
3286
|
+
}
|
|
3287
|
+
if (!stale.length) return [];
|
|
3288
|
+
if (!findChrome()) return ["No Chrome here, so canvas covers keep their last snapshot."];
|
|
3289
|
+
const url = await base();
|
|
3290
|
+
const browser = await launchChrome();
|
|
3291
|
+
const lines = [];
|
|
3292
|
+
try {
|
|
3293
|
+
for (const { canvas, screen } of stale) {
|
|
3294
|
+
const doc = project.canvas(canvas).doc;
|
|
3295
|
+
const job = canvasFrames(doc, url, "light", { id: screen })[0];
|
|
3296
|
+
if (!job) continue;
|
|
3297
|
+
const page = await browser.newPage({
|
|
3298
|
+
viewport: { width: job.width, height: job.height },
|
|
3299
|
+
deviceScaleFactor: Math.min(1, SNAPSHOT_WIDTH / job.width)
|
|
3300
|
+
});
|
|
3301
|
+
try {
|
|
3302
|
+
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
3303
|
+
if (!report.ready) {
|
|
3304
|
+
lines.push(`${canvas}: ${screen} did not finish rendering, so its cover keeps the last snapshot`);
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
const png = await page.screenshot({ fullPage: job.fullPage });
|
|
3308
|
+
const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
|
|
3309
|
+
store.save(canvas, screen, "light", png, height);
|
|
3310
|
+
} finally {
|
|
3311
|
+
await page.close().catch(() => {
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
} finally {
|
|
3316
|
+
await browser.close();
|
|
3022
3317
|
}
|
|
3318
|
+
return lines;
|
|
3023
3319
|
}
|
|
3320
|
+
|
|
3321
|
+
// src/cloud/pull.ts
|
|
3322
|
+
import fs23 from "fs";
|
|
3323
|
+
import path21 from "path";
|
|
3324
|
+
|
|
3325
|
+
// src/cloud/merge.ts
|
|
3326
|
+
import fs21 from "fs";
|
|
3327
|
+
import path19 from "path";
|
|
3328
|
+
import { merge as diff3 } from "node-diff3";
|
|
3329
|
+
|
|
3330
|
+
// src/cloud/write.ts
|
|
3331
|
+
import fs20 from "fs";
|
|
3332
|
+
import path18 from "path";
|
|
3024
3333
|
function writeInside(root, rel, data) {
|
|
3025
|
-
const file =
|
|
3334
|
+
const file = path18.join(root, rel);
|
|
3026
3335
|
if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
|
|
3027
|
-
|
|
3028
|
-
if (!isInside(
|
|
3336
|
+
fs20.mkdirSync(path18.dirname(file), { recursive: true });
|
|
3337
|
+
if (!isInside(fs20.realpathSync(root), fs20.realpathSync(path18.dirname(file))))
|
|
3029
3338
|
throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
|
|
3030
|
-
if (
|
|
3031
|
-
|
|
3339
|
+
if (fs20.existsSync(file) && fs20.lstatSync(file).isSymbolicLink()) fs20.unlinkSync(file);
|
|
3340
|
+
fs20.writeFileSync(file, data);
|
|
3032
3341
|
}
|
|
3033
3342
|
function pruneEmpty(dir, stop) {
|
|
3034
3343
|
let current = dir;
|
|
3035
3344
|
while (isInside(stop, current) && current !== stop) {
|
|
3036
3345
|
try {
|
|
3037
|
-
if (
|
|
3038
|
-
|
|
3346
|
+
if (fs20.readdirSync(current).length) return;
|
|
3347
|
+
fs20.rmdirSync(current);
|
|
3039
3348
|
} catch {
|
|
3040
3349
|
return;
|
|
3041
3350
|
}
|
|
3042
|
-
current =
|
|
3351
|
+
current = path18.dirname(current);
|
|
3043
3352
|
}
|
|
3044
3353
|
}
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
writeInside(paths.design, rel, blobs.get(entry.hash));
|
|
3058
|
-
if (local[rel]) counts.changed++;
|
|
3059
|
-
else counts.added++;
|
|
3354
|
+
|
|
3355
|
+
// src/cloud/merge.ts
|
|
3356
|
+
var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
|
|
3357
|
+
var isText = (rel, ...data) => TEXT.test(rel) && data.every((buffer) => !buffer || !buffer.subarray(0, 8192).includes(0));
|
|
3358
|
+
var mergeDir = (paths, key) => path19.join(cloudCache(paths), "merge", ...key.split("/"));
|
|
3359
|
+
var stateFile = (paths, key) => path19.join(mergeDir(paths, key), "state.json");
|
|
3360
|
+
var sideFile = (paths, key, rel, side) => path19.join(mergeDir(paths, key), side, rel);
|
|
3361
|
+
function readMergeState(paths, key) {
|
|
3362
|
+
try {
|
|
3363
|
+
return JSON.parse(fs21.readFileSync(stateFile(paths, key), "utf8"));
|
|
3364
|
+
} catch {
|
|
3365
|
+
return null;
|
|
3060
3366
|
}
|
|
3061
|
-
return counts;
|
|
3062
3367
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
const
|
|
3070
|
-
const
|
|
3071
|
-
const
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3368
|
+
function saveState(paths, state) {
|
|
3369
|
+
fs21.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
|
|
3370
|
+
fs21.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
|
|
3371
|
+
`);
|
|
3372
|
+
}
|
|
3373
|
+
function mergingUnits(paths) {
|
|
3374
|
+
const root = path19.join(cloudCache(paths), "merge");
|
|
3375
|
+
const found = [];
|
|
3376
|
+
const visit = (dir, prefix) => {
|
|
3377
|
+
if (fs21.existsSync(path19.join(dir, "state.json"))) {
|
|
3378
|
+
const state = readMergeState(paths, prefix);
|
|
3379
|
+
if (state) found.push(state);
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
let entries = [];
|
|
3383
|
+
try {
|
|
3384
|
+
entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
3385
|
+
} catch {
|
|
3386
|
+
}
|
|
3387
|
+
for (const entry of entries)
|
|
3388
|
+
if (entry.isDirectory()) visit(path19.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
3389
|
+
};
|
|
3390
|
+
visit(root, "");
|
|
3391
|
+
return found;
|
|
3392
|
+
}
|
|
3393
|
+
var MARKER = /^(<{7}|={7}|>{7})( |$)/m;
|
|
3394
|
+
function openConflicts(paths, state) {
|
|
3395
|
+
return state.conflicts.filter((conflict) => {
|
|
3396
|
+
if (conflict.kind !== "text") return true;
|
|
3397
|
+
try {
|
|
3398
|
+
return MARKER.test(fs21.readFileSync(path19.join(paths.design, conflict.path), "utf8"));
|
|
3399
|
+
} catch {
|
|
3400
|
+
return false;
|
|
3401
|
+
}
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
function keep(paths, key, rel, side, data) {
|
|
3405
|
+
if (!data) return;
|
|
3406
|
+
const file = sideFile(paths, key, rel, side);
|
|
3407
|
+
fs21.mkdirSync(path19.dirname(file), { recursive: true });
|
|
3408
|
+
fs21.writeFileSync(file, data);
|
|
3409
|
+
}
|
|
3410
|
+
function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
|
|
3411
|
+
const files = { added: 0, changed: 0, removed: 0, merged: 0 };
|
|
3412
|
+
const conflicts = [];
|
|
3413
|
+
const stop = key === SYSTEM_UNIT ? paths.design : path19.dirname(unitRoot(paths, key));
|
|
3414
|
+
fs21.rmSync(mergeDir(paths, key), { recursive: true, force: true });
|
|
3415
|
+
const read = (rel) => fs21.readFileSync(path19.join(paths.design, rel));
|
|
3416
|
+
const remove = (rel) => {
|
|
3417
|
+
const file = path19.join(paths.design, rel);
|
|
3418
|
+
fs21.rmSync(file, { force: true });
|
|
3419
|
+
pruneEmpty(path19.dirname(file), stop);
|
|
3420
|
+
};
|
|
3421
|
+
for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
|
|
3422
|
+
const b = base[rel];
|
|
3423
|
+
const h = here[rel]?.hash;
|
|
3424
|
+
const c = cloud2[rel]?.hash;
|
|
3425
|
+
if (h === c || c === b) continue;
|
|
3426
|
+
if (h === b) {
|
|
3427
|
+
if (c) {
|
|
3428
|
+
writeInside(paths.design, rel, blob(c));
|
|
3429
|
+
if (h) files.changed++;
|
|
3430
|
+
else files.added++;
|
|
3431
|
+
} else {
|
|
3432
|
+
remove(rel);
|
|
3433
|
+
files.removed++;
|
|
3434
|
+
}
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
const cloudData = c ? blob(c) : void 0;
|
|
3438
|
+
const hereData = h ? read(rel) : void 0;
|
|
3439
|
+
if (!h || !c) {
|
|
3440
|
+
const kind = h ? "deleted_in_cloud" : "deleted_here";
|
|
3441
|
+
keep(paths, key, rel, "here", hereData);
|
|
3442
|
+
keep(paths, key, rel, "cloud", cloudData);
|
|
3443
|
+
if (!h && cloudData) writeInside(paths.design, rel, cloudData);
|
|
3444
|
+
conflicts.push({ path: rel, kind });
|
|
3445
|
+
continue;
|
|
3446
|
+
}
|
|
3447
|
+
const baseData = b ? blob(b) : void 0;
|
|
3448
|
+
if (isText(rel, hereData, cloudData, baseData)) {
|
|
3449
|
+
const lines = (data) => data ? data.toString("utf8").split("\n") : [];
|
|
3450
|
+
const result = diff3(lines(hereData), lines(baseData), lines(cloudData), {
|
|
3451
|
+
label: { a: "here", b: "cloud" }
|
|
3452
|
+
});
|
|
3453
|
+
writeInside(paths.design, rel, Buffer.from(result.result.join("\n")));
|
|
3454
|
+
if (!result.conflict) {
|
|
3455
|
+
files.merged++;
|
|
3456
|
+
continue;
|
|
3457
|
+
}
|
|
3458
|
+
keep(paths, key, rel, "base", baseData);
|
|
3459
|
+
keep(paths, key, rel, "here", hereData);
|
|
3460
|
+
keep(paths, key, rel, "cloud", cloudData);
|
|
3461
|
+
conflicts.push({ path: rel, kind: "text" });
|
|
3462
|
+
continue;
|
|
3463
|
+
}
|
|
3464
|
+
keep(paths, key, rel, "here", hereData);
|
|
3465
|
+
keep(paths, key, rel, "cloud", cloudData);
|
|
3466
|
+
conflicts.push({ path: rel, kind: "binary" });
|
|
3467
|
+
}
|
|
3468
|
+
if (conflicts.length) saveState(paths, { unit: key, headRev, conflicts });
|
|
3469
|
+
return { files, conflicts };
|
|
3470
|
+
}
|
|
3471
|
+
function resolveConflict(paths, state, rel, side) {
|
|
3472
|
+
const conflict = state.conflicts.find((item) => item.path === rel);
|
|
3473
|
+
if (!conflict) return;
|
|
3474
|
+
const kept = sideFile(paths, state.unit, rel, side);
|
|
3475
|
+
const stop = state.unit === SYSTEM_UNIT ? paths.design : path19.dirname(unitRoot(paths, state.unit));
|
|
3476
|
+
if (fs21.existsSync(kept)) writeInside(paths.design, rel, fs21.readFileSync(kept));
|
|
3477
|
+
else {
|
|
3478
|
+
fs21.rmSync(path19.join(paths.design, rel), { force: true });
|
|
3479
|
+
pruneEmpty(path19.dirname(path19.join(paths.design, rel)), stop);
|
|
3480
|
+
}
|
|
3481
|
+
state.conflicts = state.conflicts.filter((item) => item.path !== rel);
|
|
3482
|
+
saveState(paths, state);
|
|
3483
|
+
}
|
|
3484
|
+
function finishMerge(paths, link, key, accept = false) {
|
|
3485
|
+
const state = readMergeState(paths, key);
|
|
3486
|
+
if (state) {
|
|
3487
|
+
const open2 = openConflicts(paths, state).filter((conflict) => !(accept && conflict.kind !== "text"));
|
|
3488
|
+
if (open2.length) return open2;
|
|
3489
|
+
fs21.rmSync(mergeDir(paths, key), { recursive: true, force: true });
|
|
3490
|
+
}
|
|
3491
|
+
delete link.conflicts[key];
|
|
3492
|
+
return [];
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
// src/cloud/meta.ts
|
|
3496
|
+
import fs22 from "fs";
|
|
3497
|
+
import path20 from "path";
|
|
3498
|
+
async function syncProjectMeta(paths, client, link) {
|
|
3499
|
+
const remote = await client.project(link.project);
|
|
3500
|
+
if (!remote) return [];
|
|
3501
|
+
const lines = [];
|
|
3502
|
+
link.name = remote.name;
|
|
3503
|
+
const file = iconFile(paths);
|
|
3504
|
+
const local = file ? iconHash(file) : null;
|
|
3505
|
+
const cloud2 = remote.icon;
|
|
3506
|
+
if (local === cloud2) {
|
|
3507
|
+
link.icon = cloud2;
|
|
3508
|
+
return lines;
|
|
3509
|
+
}
|
|
3510
|
+
const base = link.icon;
|
|
3511
|
+
const localChanged = base === void 0 ? cloud2 === null : local !== base;
|
|
3512
|
+
const cloudChanged = base === void 0 ? local === null : cloud2 !== base;
|
|
3513
|
+
if (localChanged && !cloudChanged) {
|
|
3514
|
+
try {
|
|
3515
|
+
if (file) {
|
|
3516
|
+
const ext = path20.extname(file).slice(1);
|
|
3517
|
+
await client.setProjectIcon(link.project, { data: fs22.readFileSync(file), type: ICON_TYPES[ext] });
|
|
3518
|
+
lines.push("The project icon went up to the cloud.");
|
|
3519
|
+
} else {
|
|
3520
|
+
await client.setProjectIcon(link.project, null);
|
|
3521
|
+
lines.push("The project icon was removed in the cloud too.");
|
|
3522
|
+
}
|
|
3523
|
+
link.icon = local;
|
|
3524
|
+
} catch (error) {
|
|
3525
|
+
if (!(error instanceof CloudError) || error.status === 0) throw error;
|
|
3526
|
+
lines.push(`The project icon stays here only: ${error.message}`);
|
|
3527
|
+
}
|
|
3528
|
+
return lines;
|
|
3529
|
+
}
|
|
3530
|
+
const icon = cloud2 ? await client.projectIcon(link.project) : null;
|
|
3531
|
+
if (icon && !sniffIcon(icon.data))
|
|
3532
|
+
return [...lines, "The cloud sent a project icon that is no image; it was left out."];
|
|
3533
|
+
writeIcon(paths, icon?.data ?? null);
|
|
3534
|
+
link.icon = cloud2;
|
|
3535
|
+
lines.push(
|
|
3536
|
+
localChanged ? "The project icon changed here and in the cloud; the cloud's is kept." : cloud2 ? "The project icon came down from the cloud." : "The project icon was removed in the cloud, so here too."
|
|
3537
|
+
);
|
|
3538
|
+
return lines;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
// src/cloud/pull.ts
|
|
3542
|
+
var HASH = /^[0-9a-f]{64}$/;
|
|
3543
|
+
var MERGE_HINT = "Resolve with `design merge` (it lists the files; `--here` or `--cloud` settles one or a unit), then `design push`.";
|
|
3544
|
+
function checkManifest(key, manifest) {
|
|
3545
|
+
for (const [rel, entry] of Object.entries(manifest)) {
|
|
3546
|
+
if (!isUnitPath(key, rel)) throw new CliError(`The cloud sent a path that does not belong to ${key}: "${rel}"`);
|
|
3547
|
+
if (!HASH.test(entry.hash)) throw new CliError(`The cloud sent a bad hash for ${rel} in ${key}`);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
function applyUnit(paths, key, local, target, blobs) {
|
|
3551
|
+
const counts = { added: 0, changed: 0, removed: 0 };
|
|
3552
|
+
const stop = key === SYSTEM_UNIT ? paths.design : path21.dirname(unitRoot(paths, key));
|
|
3553
|
+
for (const rel of Object.keys(local)) {
|
|
3554
|
+
if (rel in target) continue;
|
|
3555
|
+
const file = path21.join(paths.design, rel);
|
|
3556
|
+
fs23.rmSync(file, { force: true });
|
|
3557
|
+
pruneEmpty(path21.dirname(file), stop);
|
|
3558
|
+
counts.removed++;
|
|
3559
|
+
}
|
|
3560
|
+
for (const [rel, entry] of Object.entries(target)) {
|
|
3561
|
+
if (local[rel]?.hash === entry.hash) continue;
|
|
3562
|
+
writeInside(paths.design, rel, blobs.get(entry.hash));
|
|
3563
|
+
if (local[rel]) counts.changed++;
|
|
3564
|
+
else counts.added++;
|
|
3565
|
+
}
|
|
3566
|
+
return counts;
|
|
3567
|
+
}
|
|
3568
|
+
async function pull(ctx, options) {
|
|
3569
|
+
const { paths, client } = ctx;
|
|
3570
|
+
const link = structuredClone(ctx.link);
|
|
3571
|
+
const report = newReport("pull", link);
|
|
3572
|
+
for (const line of await syncProjectMeta(ctx.paths, ctx.client, link)) report.hints.push(line);
|
|
3573
|
+
const selected = unique(options.units.map(unitArg));
|
|
3574
|
+
const theirs = unique(options.theirs.map(unitArg));
|
|
3575
|
+
const scan = scanLocal(paths);
|
|
3576
|
+
const remote = await client.units(link.project);
|
|
3577
|
+
const local = (key) => scan.units.get(key)?.manifest ?? {};
|
|
3578
|
+
const keys = selected.length || theirs.length ? unique([...selected, ...theirs]) : allUnitKeys(link, /* @__PURE__ */ new Map(), remote);
|
|
3579
|
+
const clashes = caseClashes([...remote.keys(), ...scan.units.keys(), ...Object.keys(link.units)]);
|
|
3580
|
+
const plans = [];
|
|
3581
|
+
for (const key of keys.sort(byUnit)) {
|
|
3582
|
+
const clash = clashes.get(key);
|
|
3583
|
+
if (clash !== void 0) {
|
|
3584
|
+
report.units.push({
|
|
3585
|
+
unit: key,
|
|
3586
|
+
action: "conflict",
|
|
3587
|
+
message: `differs from ${clash} only by letter case; rename one of them before pulling`
|
|
3588
|
+
});
|
|
3083
3589
|
continue;
|
|
3084
3590
|
}
|
|
3085
3591
|
const unit = remote.get(key) ?? null;
|
|
@@ -3110,9 +3616,15 @@ async function pull(ctx, options) {
|
|
|
3110
3616
|
entry.message = "local changes discarded";
|
|
3111
3617
|
plans.push({ key, kind: "apply", remote: unit, report: entry });
|
|
3112
3618
|
} else if (!s.ahead) {
|
|
3619
|
+
const merging = readMergeState(paths, key);
|
|
3620
|
+
const open2 = merging ? openConflicts(paths, merging) : [];
|
|
3621
|
+
if (open2.length) {
|
|
3622
|
+
report.units.push({ ...reportFor("conflict"), rev: s.baseRev, conflicts: open2, message: MERGE_HINT });
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3113
3625
|
if (s.conflictRev !== void 0) {
|
|
3114
|
-
|
|
3115
|
-
|
|
3626
|
+
finishMerge(paths, link, key, true);
|
|
3627
|
+
fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3116
3628
|
}
|
|
3117
3629
|
report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
|
|
3118
3630
|
} else if (!s.changed.length) {
|
|
@@ -3136,31 +3648,33 @@ async function pull(ctx, options) {
|
|
|
3136
3648
|
message: "deleted in the cloud; your local changes are kept and `design push` creates it again"
|
|
3137
3649
|
});
|
|
3138
3650
|
} else {
|
|
3139
|
-
|
|
3140
|
-
...reportFor("conflict"),
|
|
3141
|
-
rev: void 0,
|
|
3142
|
-
headRev: unit.headRev,
|
|
3143
|
-
incoming: incomingDir(paths, key)
|
|
3144
|
-
};
|
|
3145
|
-
entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${path17.relative(paths.root, entry.incoming)}`;
|
|
3146
|
-
if (s.conflictRev === unit.headRev && fs17.existsSync(entry.incoming)) report.units.push(entry);
|
|
3147
|
-
else plans.push({ key, kind: "incoming", remote: unit, report: entry });
|
|
3651
|
+
plans.push({ key, kind: "merge", remote: unit, report: { ...reportFor("merged"), headRev: unit.headRev } });
|
|
3148
3652
|
}
|
|
3149
3653
|
}
|
|
3150
3654
|
const byHash = /* @__PURE__ */ new Map();
|
|
3151
3655
|
for (const unit of scan.units.values())
|
|
3152
|
-
for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash,
|
|
3656
|
+
for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path21.join(paths.design, rel));
|
|
3153
3657
|
const needed = /* @__PURE__ */ new Set();
|
|
3658
|
+
const bases = /* @__PURE__ */ new Set();
|
|
3154
3659
|
for (const plan of plans) {
|
|
3155
3660
|
if (!plan.remote) continue;
|
|
3156
3661
|
checkManifest(plan.key, plan.remote.manifest);
|
|
3157
3662
|
for (const entry of Object.values(plan.remote.manifest)) needed.add(entry.hash);
|
|
3663
|
+
if (plan.kind !== "merge") continue;
|
|
3664
|
+
const base = link.units[plan.key]?.files ?? {};
|
|
3665
|
+
const here = local(plan.key);
|
|
3666
|
+
for (const [rel, hash] of Object.entries(base)) {
|
|
3667
|
+
const h = here[rel]?.hash;
|
|
3668
|
+
const c = plan.remote.manifest[rel]?.hash;
|
|
3669
|
+
if (h && c && h !== hash && c !== hash && h !== c) bases.add(hash);
|
|
3670
|
+
}
|
|
3158
3671
|
}
|
|
3672
|
+
for (const hash of bases) if (!needed.has(hash) && byHash.has(hash)) needed.add(hash);
|
|
3159
3673
|
const blobs = /* @__PURE__ */ new Map();
|
|
3160
3674
|
const download = [];
|
|
3161
3675
|
for (const hash of needed) {
|
|
3162
3676
|
const file = byHash.get(hash);
|
|
3163
|
-
if (file) blobs.set(hash,
|
|
3677
|
+
if (file) blobs.set(hash, fs23.readFileSync(file));
|
|
3164
3678
|
else download.push(hash);
|
|
3165
3679
|
}
|
|
3166
3680
|
if (download.length) {
|
|
@@ -3179,6 +3693,16 @@ async function pull(ctx, options) {
|
|
|
3179
3693
|
});
|
|
3180
3694
|
bar.done(`Downloaded ${plural(download.length, "file")} (${megabytes(gotBytes)}) in ${seconds(started)}`);
|
|
3181
3695
|
}
|
|
3696
|
+
await mapLimit(
|
|
3697
|
+
[...bases].filter((hash) => !blobs.has(hash)),
|
|
3698
|
+
TRANSFERS,
|
|
3699
|
+
async (hash) => {
|
|
3700
|
+
try {
|
|
3701
|
+
blobs.set(hash, await client.getBlob(link.project, hash));
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
);
|
|
3182
3706
|
for (const plan of plans) {
|
|
3183
3707
|
const { key, report: entry } = plan;
|
|
3184
3708
|
if (plan.kind === "apply") {
|
|
@@ -3186,14 +3710,30 @@ async function pull(ctx, options) {
|
|
|
3186
3710
|
if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
|
|
3187
3711
|
else delete link.units[key];
|
|
3188
3712
|
delete link.conflicts[key];
|
|
3189
|
-
|
|
3713
|
+
fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3190
3714
|
} else {
|
|
3191
|
-
const
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3715
|
+
const result = mergeUnit(
|
|
3716
|
+
paths,
|
|
3717
|
+
key,
|
|
3718
|
+
plan.remote.headRev,
|
|
3719
|
+
link.units[key]?.files ?? {},
|
|
3720
|
+
local(key),
|
|
3721
|
+
plan.remote.manifest,
|
|
3722
|
+
(hash) => blobs.get(hash)
|
|
3723
|
+
);
|
|
3724
|
+
fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3725
|
+
link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
|
|
3726
|
+
entry.rev = plan.remote.headRev;
|
|
3727
|
+
entry.files = result.files;
|
|
3728
|
+
if (result.conflicts.length) {
|
|
3729
|
+
link.conflicts[key] = plan.remote.headRev;
|
|
3730
|
+
entry.action = "conflict";
|
|
3731
|
+
entry.conflicts = result.conflicts;
|
|
3732
|
+
entry.message = `${plural(result.conflicts.length, "file")} changed on both sides did not merge. ${MERGE_HINT}`;
|
|
3733
|
+
} else {
|
|
3734
|
+
delete link.conflicts[key];
|
|
3735
|
+
entry.message = `merged with the cloud's changes (r${plan.remote.headRev})${result.files.merged ? `, ${plural(result.files.merged, "file")} line by line` : ""}; \`design push\` takes your changes up`;
|
|
3736
|
+
}
|
|
3197
3737
|
}
|
|
3198
3738
|
report.units.push(entry);
|
|
3199
3739
|
}
|
|
@@ -3204,15 +3744,15 @@ async function pull(ctx, options) {
|
|
|
3204
3744
|
report.ok = false;
|
|
3205
3745
|
report.error = {
|
|
3206
3746
|
code: "conflict",
|
|
3207
|
-
message: `${plural(conflicts.length, "unit")} changed both here and in the cloud
|
|
3747
|
+
message: `${plural(conflicts.length, "unit")} changed both here and in the cloud and did not merge cleanly. ${MERGE_HINT}`
|
|
3208
3748
|
};
|
|
3209
3749
|
}
|
|
3210
3750
|
return report;
|
|
3211
3751
|
}
|
|
3212
3752
|
|
|
3213
3753
|
// src/cloud/push.ts
|
|
3214
|
-
import
|
|
3215
|
-
import
|
|
3754
|
+
import fs24 from "fs";
|
|
3755
|
+
import path22 from "path";
|
|
3216
3756
|
function blockedReport(status, message) {
|
|
3217
3757
|
return {
|
|
3218
3758
|
unit: status.key,
|
|
@@ -3226,6 +3766,7 @@ async function push(ctx, options) {
|
|
|
3226
3766
|
const { paths, client, log } = ctx;
|
|
3227
3767
|
const link = structuredClone(ctx.link);
|
|
3228
3768
|
const report = newReport("push", link);
|
|
3769
|
+
for (const line of await syncProjectMeta(ctx.paths, ctx.client, link)) report.hints.push(line);
|
|
3229
3770
|
const selected = unique(options.units.map(unitArg));
|
|
3230
3771
|
const resolved = unique(options.resolved.map(unitArg));
|
|
3231
3772
|
const scan = scanLocal(paths);
|
|
@@ -3237,10 +3778,19 @@ async function push(ctx, options) {
|
|
|
3237
3778
|
`These canvases differ only by letter case: ${pairs.join(", ")}. On macOS and Windows they share one folder; rename one of them.`
|
|
3238
3779
|
);
|
|
3239
3780
|
}
|
|
3781
|
+
for (const key of Object.keys(link.conflicts)) if (readMergeState(paths, key)) finishMerge(paths, link, key);
|
|
3240
3782
|
for (const key of resolved) {
|
|
3241
3783
|
const head = link.conflicts[key];
|
|
3242
|
-
if (head === void 0)
|
|
3784
|
+
if (head === void 0) {
|
|
3785
|
+
if (link.units[key]) continue;
|
|
3243
3786
|
throw new CliError(`${key} has no pending conflict; --resolved is for units \`design pull\` reported in conflict`);
|
|
3787
|
+
}
|
|
3788
|
+
if (readMergeState(paths, key)) {
|
|
3789
|
+
const open2 = finishMerge(paths, link, key, true);
|
|
3790
|
+
if (open2.length)
|
|
3791
|
+
throw new CliError(`${key} still has conflict markers in ${open2.map((item) => item.path).join(", ")}`);
|
|
3792
|
+
continue;
|
|
3793
|
+
}
|
|
3244
3794
|
link.units[key] = { rev: head, files: link.units[key]?.files ?? {} };
|
|
3245
3795
|
delete link.conflicts[key];
|
|
3246
3796
|
}
|
|
@@ -3318,9 +3868,12 @@ async function push(ctx, options) {
|
|
|
3318
3868
|
for (const key of toPush) {
|
|
3319
3869
|
const s = status(key);
|
|
3320
3870
|
if (s.conflictRev !== void 0) {
|
|
3321
|
-
const dir =
|
|
3871
|
+
const dir = path22.relative(paths.root, incomingDir(paths, key));
|
|
3322
3872
|
blocked.push(
|
|
3323
|
-
blockedReport(
|
|
3873
|
+
blockedReport(
|
|
3874
|
+
s,
|
|
3875
|
+
readMergeState(paths, key) ? "a merge with the cloud has conflicts left: `design merge` lists them and settles them" : `pull left the cloud version in ${dir}; merge it, then \`design push --resolved ${key}\``
|
|
3876
|
+
)
|
|
3324
3877
|
);
|
|
3325
3878
|
} else if (s.ahead) {
|
|
3326
3879
|
blocked.push(
|
|
@@ -3356,13 +3909,15 @@ async function push(ctx, options) {
|
|
|
3356
3909
|
return false;
|
|
3357
3910
|
});
|
|
3358
3911
|
if (!pushing.length) return report;
|
|
3912
|
+
const canvasIds = pushing.map(unitCanvasId).filter((id2) => id2 !== null);
|
|
3913
|
+
if (options.beforeBuild) for (const line of await options.beforeBuild(canvasIds)) report.hints.push(line);
|
|
3359
3914
|
const project = new DesignProject(paths, STATIC_URLS);
|
|
3360
3915
|
const systemRev = remote.get(SYSTEM_UNIT)?.headRev ?? 0;
|
|
3361
3916
|
const units = [];
|
|
3362
3917
|
const files = /* @__PURE__ */ new Map();
|
|
3363
3918
|
const addFiles = (manifest, dir) => {
|
|
3364
3919
|
for (const [rel, entry] of Object.entries(manifest))
|
|
3365
|
-
if (!files.has(entry.hash)) files.set(entry.hash,
|
|
3920
|
+
if (!files.has(entry.hash)) files.set(entry.hash, path22.join(dir, rel));
|
|
3366
3921
|
};
|
|
3367
3922
|
for (const key of pushing) {
|
|
3368
3923
|
const id2 = unitCanvasId(key);
|
|
@@ -3394,7 +3949,7 @@ async function push(ctx, options) {
|
|
|
3394
3949
|
if (missing.length) {
|
|
3395
3950
|
const unknown = missing.find((hash) => !files.has(hash));
|
|
3396
3951
|
if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
|
|
3397
|
-
const bytes = missing.reduce((sum, hash) => sum +
|
|
3952
|
+
const bytes = missing.reduce((sum, hash) => sum + fs24.statSync(files.get(hash)).size, 0);
|
|
3398
3953
|
const bar = ctx.progress();
|
|
3399
3954
|
const started = Date.now();
|
|
3400
3955
|
let sentFiles = 0;
|
|
@@ -3403,12 +3958,12 @@ async function push(ctx, options) {
|
|
|
3403
3958
|
bar.update(line());
|
|
3404
3959
|
await mapLimit(missing, TRANSFERS, async (hash) => {
|
|
3405
3960
|
const file = files.get(hash);
|
|
3406
|
-
const data =
|
|
3961
|
+
const data = fs24.readFileSync(file);
|
|
3407
3962
|
try {
|
|
3408
3963
|
await client.putBlob(link.project, hash, data);
|
|
3409
3964
|
} catch (error) {
|
|
3410
3965
|
if (error instanceof CloudError && error.status === 413)
|
|
3411
|
-
throw new CliError(`${
|
|
3966
|
+
throw new CliError(`${path22.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
|
|
3412
3967
|
throw error;
|
|
3413
3968
|
}
|
|
3414
3969
|
sentFiles++;
|
|
@@ -3430,7 +3985,7 @@ async function push(ctx, options) {
|
|
|
3430
3985
|
url: webUrl(link, unit.key)
|
|
3431
3986
|
});
|
|
3432
3987
|
}
|
|
3433
|
-
for (const key of resolved)
|
|
3988
|
+
for (const key of resolved) fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3434
3989
|
writeLink(paths, link);
|
|
3435
3990
|
report.units.sort((a, b) => byUnit(a.unit, b.unit));
|
|
3436
3991
|
return report;
|
|
@@ -3441,7 +3996,7 @@ function refuseLegacyGo(paths, keys) {
|
|
|
3441
3996
|
for (const key of keys) {
|
|
3442
3997
|
if (!key.startsWith("canvas/")) continue;
|
|
3443
3998
|
const id2 = key.slice("canvas/".length);
|
|
3444
|
-
for (const file of canvasCodeFiles(
|
|
3999
|
+
for (const file of canvasCodeFiles(path22.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
|
|
3445
4000
|
for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
|
|
3446
4001
|
}
|
|
3447
4002
|
}
|
|
@@ -3455,496 +4010,50 @@ Still using go():
|
|
|
3455
4010
|
);
|
|
3456
4011
|
}
|
|
3457
4012
|
|
|
3458
|
-
// src/cli/commands/
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
4013
|
+
// src/cli/commands/preview.ts
|
|
4014
|
+
import { spawn } from "child_process";
|
|
4015
|
+
import fs28 from "fs";
|
|
4016
|
+
|
|
4017
|
+
// src/server/index.ts
|
|
4018
|
+
import fs27 from "fs";
|
|
4019
|
+
import http from "http";
|
|
4020
|
+
import path25 from "path";
|
|
4021
|
+
import sirv from "sirv";
|
|
4022
|
+
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
4023
|
+
|
|
4024
|
+
// src/server/api.ts
|
|
4025
|
+
import fs25 from "fs";
|
|
4026
|
+
import path23 from "path";
|
|
4027
|
+
function sendJson(res, status, body) {
|
|
4028
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
4029
|
+
res.end(JSON.stringify(body));
|
|
3469
4030
|
}
|
|
3470
|
-
function
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
entry.files.changed && `~${entry.files.changed}`,
|
|
3485
|
-
entry.files.removed && `-${entry.files.removed}`
|
|
3486
|
-
].filter(Boolean).join(" ")
|
|
3487
|
-
) : "";
|
|
3488
|
-
const detail2 = [entry.action.replaceAll("_", " "), revs(entry), files].filter(Boolean).join(" ");
|
|
3489
|
-
print(` ${mark} ${entry.unit.padEnd(width)} ${detail2}`);
|
|
3490
|
-
if (entry.url && GOOD.has(entry.action)) print(` ${dim("\u2192")} ${entry.url}`);
|
|
3491
|
-
if (entry.message) print(` ${dim(entry.message)}`);
|
|
3492
|
-
}
|
|
3493
|
-
if (quiet) print(dim(` ${plural(quiet, "unit")} ${report.command === "push" ? "unchanged" : "up to date"}`));
|
|
3494
|
-
for (const hint of report.hints) print(dim(hint));
|
|
4031
|
+
function readBody(req, limit = 25 * 1024 * 1024) {
|
|
4032
|
+
return new Promise((resolve, reject) => {
|
|
4033
|
+
const chunks = [];
|
|
4034
|
+
let size2 = 0;
|
|
4035
|
+
req.on("data", (chunk2) => {
|
|
4036
|
+
size2 += chunk2.length;
|
|
4037
|
+
if (size2 > limit) {
|
|
4038
|
+
reject(new Error("body too large"));
|
|
4039
|
+
req.destroy();
|
|
4040
|
+
} else chunks.push(chunk2);
|
|
4041
|
+
});
|
|
4042
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
4043
|
+
req.on("error", reject);
|
|
4044
|
+
});
|
|
3495
4045
|
}
|
|
3496
|
-
async function
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
let link = null;
|
|
3504
|
-
try {
|
|
3505
|
-
link = readLink(paths);
|
|
3506
|
-
} catch {
|
|
3507
|
-
}
|
|
3508
|
-
const cloud2 = error instanceof CloudError ? error : null;
|
|
3509
|
-
const failure = {
|
|
3510
|
-
ok: false,
|
|
3511
|
-
command,
|
|
3512
|
-
host: link?.host ?? "",
|
|
3513
|
-
project: link?.project ?? "",
|
|
3514
|
-
url: link ? webUrl(link) : "",
|
|
3515
|
-
units: [],
|
|
3516
|
-
hints: [],
|
|
3517
|
-
error: {
|
|
3518
|
-
code: cloud2?.error ?? (error instanceof CliError ? "error" : "internal"),
|
|
3519
|
-
message: error.message,
|
|
3520
|
-
status: cloud2?.status || void 0,
|
|
3521
|
-
limit: cloud2?.limit
|
|
3522
|
-
}
|
|
3523
|
-
};
|
|
3524
|
-
print(JSON.stringify(failure, null, 2));
|
|
3525
|
-
process.exitCode = error instanceof CliError ? error.code : 1;
|
|
3526
|
-
return;
|
|
4046
|
+
async function handleApi(ctx, req, res, url) {
|
|
4047
|
+
const { project, events, snapshots } = ctx;
|
|
4048
|
+
const route2 = url.pathname.slice("/api".length);
|
|
4049
|
+
const method = req.method ?? "GET";
|
|
4050
|
+
if (route2 === "/health") {
|
|
4051
|
+
sendJson(res, 200, { ok: true, root: project.paths.root, version: packageVersion(), pid: process.pid });
|
|
4052
|
+
return true;
|
|
3527
4053
|
}
|
|
3528
|
-
if (
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
return;
|
|
3532
|
-
}
|
|
3533
|
-
printReport(report);
|
|
3534
|
-
if (!report.ok && report.error) throw new CliError(report.error.message, 2);
|
|
3535
|
-
}
|
|
3536
|
-
function runPush(paths, units, options) {
|
|
3537
|
-
return runSync(paths, "push", options.json, (ctx) => push(ctx, { units, resolved: options.resolved }));
|
|
3538
|
-
}
|
|
3539
|
-
function runPull(paths, units, options) {
|
|
3540
|
-
return runSync(paths, "pull", options.json, (ctx) => pull(ctx, { units, theirs: options.theirs }));
|
|
3541
|
-
}
|
|
3542
|
-
|
|
3543
|
-
// src/cli/commands/canvases.ts
|
|
3544
|
-
var publicUrl = (host, publicId) => `${host.replace(/\/+$/, "")}/s/${publicId}`;
|
|
3545
|
-
function ago(iso) {
|
|
3546
|
-
const seconds2 = Math.max(0, (Date.now() - Date.parse(iso)) / 1e3);
|
|
3547
|
-
if (!Number.isFinite(seconds2)) return "";
|
|
3548
|
-
if (seconds2 < 60) return "just now";
|
|
3549
|
-
const [value, unit] = seconds2 < 3600 ? [seconds2 / 60, "minute"] : seconds2 < 86400 ? [seconds2 / 3600, "hour"] : seconds2 < 86400 * 60 ? [seconds2 / 86400, "day"] : [seconds2 / (86400 * 30), "month"];
|
|
3550
|
-
return `${plural(Math.floor(value), unit)} ago`;
|
|
3551
|
-
}
|
|
3552
|
-
function cloud(paths) {
|
|
3553
|
-
const link = requireLink(paths);
|
|
3554
|
-
const credential = requireCredential(link.host);
|
|
3555
|
-
return { link, client: new CloudClient(link.host, credential.token) };
|
|
3556
|
-
}
|
|
3557
|
-
function canvasArg(value, usage) {
|
|
3558
|
-
if (!value) throw new CliError(`Usage: ${usage}`);
|
|
3559
|
-
const key = unitArg(value);
|
|
3560
|
-
if (key === SYSTEM_UNIT) throw new CliError(`The design system is not a canvas. Usage: ${usage}`);
|
|
3561
|
-
return key.slice("canvas/".length);
|
|
3562
|
-
}
|
|
3563
|
-
async function remoteCanvas(client, project, canvas) {
|
|
3564
|
-
const unit = (await client.units(project)).get(canvasUnit(canvas));
|
|
3565
|
-
if (!unit) throw new CliError(`${canvas} is not in the cloud yet: \`design push ${canvas}\` uploads it`);
|
|
3566
|
-
return unit;
|
|
3567
|
-
}
|
|
3568
|
-
async function runCanvases(paths, options) {
|
|
3569
|
-
const project = new DesignProject(paths, STATIC_URLS);
|
|
3570
|
-
const local = project.canvasIds().map((id2) => project.canvas(id2).doc);
|
|
3571
|
-
const link = readLink(paths);
|
|
3572
|
-
let remote = null;
|
|
3573
|
-
let cloudNote = link ? null : "not linked to foss-design Cloud \u2014 `design link`";
|
|
3574
|
-
if (link) {
|
|
3575
|
-
const credential = getCredential(link.host);
|
|
3576
|
-
if (!credential) cloudNote = `not signed in to ${link.host} \u2014 \`design login\``;
|
|
3577
|
-
else {
|
|
3578
|
-
try {
|
|
3579
|
-
remote = await new CloudClient(link.host, credential.token).units(link.project);
|
|
3580
|
-
} catch (error) {
|
|
3581
|
-
cloudNote = `cloud unavailable: ${error.message}`;
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
}
|
|
3585
|
-
const scan = link ? scanLocal(paths) : null;
|
|
3586
|
-
const ids = [
|
|
3587
|
-
.../* @__PURE__ */ new Set([
|
|
3588
|
-
...local.map((doc) => doc.id),
|
|
3589
|
-
...[...remote?.keys() ?? []].filter((key) => key.startsWith("canvas/")).map((key) => key.slice(7))
|
|
3590
|
-
])
|
|
3591
|
-
].sort();
|
|
3592
|
-
const rows = ids.map((id2) => {
|
|
3593
|
-
const doc = local.find((entry) => entry.id === id2);
|
|
3594
|
-
const unit = remote?.get(canvasUnit(id2));
|
|
3595
|
-
const status = link && remote ? unitStatus(canvasUnit(id2), link, scan?.units.get(canvasUnit(id2))?.manifest, remote) : null;
|
|
3596
|
-
const frames = doc?.pages.flatMap((page) => page.sections.flatMap((section) => section.items)).filter((item) => item.kind === "screen" || item.kind === "url");
|
|
3597
|
-
return {
|
|
3598
|
-
id: id2,
|
|
3599
|
-
title: doc?.title ?? unit?.title ?? id2,
|
|
3600
|
-
local: !!doc,
|
|
3601
|
-
pages: doc?.pages.length ?? null,
|
|
3602
|
-
screens: frames?.length ?? null,
|
|
3603
|
-
errors: doc?.issues.filter((issue) => issue.severity === "error").length ?? 0,
|
|
3604
|
-
cloud: unit ? {
|
|
3605
|
-
rev: unit.headRev,
|
|
3606
|
-
state: status?.state ?? null,
|
|
3607
|
-
archived: unit.archived,
|
|
3608
|
-
url: webUrl(link, canvasUnit(id2)),
|
|
3609
|
-
publicUrl: unit.publicId ? publicUrl(link.host, unit.publicId) : null
|
|
3610
|
-
} : null
|
|
3611
|
-
};
|
|
3612
|
-
});
|
|
3613
|
-
if (options.json) return print(JSON.stringify({ canvases: rows, cloud: cloudNote }, null, 2));
|
|
3614
|
-
if (!rows.length) print(dim("No canvases yet: `design new <canvas>` creates one."));
|
|
3615
|
-
const width = Math.max(0, ...rows.map((row) => row.id.length));
|
|
3616
|
-
for (const row of rows) {
|
|
3617
|
-
const facts = row.local ? [`${plural(row.pages ?? 0, "page")} \xB7 ${plural(row.screens ?? 0, "screen")}`] : [yellow("only in the cloud \u2014 `design pull`")];
|
|
3618
|
-
if (row.errors) facts.push(red(plural(row.errors, "error")));
|
|
3619
|
-
if (row.cloud) {
|
|
3620
|
-
const state = row.cloud.archived ? dim("archived") : row.cloud.state === "in_sync" ? green("in sync") : row.cloud.state ? yellow(row.cloud.state.replaceAll("_", " ")) : "";
|
|
3621
|
-
facts.push([`r${row.cloud.rev}`, state].filter(Boolean).join(" "));
|
|
3622
|
-
} else if (remote && row.local) facts.push(dim("not pushed"));
|
|
3623
|
-
print(`${bold(row.id.padEnd(width))} ${row.title} ${dim("\xB7")} ${facts.join(dim(" \xB7 "))}`);
|
|
3624
|
-
if (row.cloud && !row.cloud.archived) print(`${" ".repeat(width)} ${dim("\u2192")} ${row.cloud.url}`);
|
|
3625
|
-
if (row.cloud?.publicUrl) print(`${" ".repeat(width)} ${dim("public")} ${cyan(row.cloud.publicUrl)}`);
|
|
3626
|
-
}
|
|
3627
|
-
if (cloudNote) print(dim(cloudNote));
|
|
3628
|
-
}
|
|
3629
|
-
async function runUrl(paths, value, options) {
|
|
3630
|
-
const canvas = canvasArg(value, "design url <canvas>");
|
|
3631
|
-
const { link, client } = cloud(paths);
|
|
3632
|
-
const unit = await remoteCanvas(client, link.project, canvas);
|
|
3633
|
-
const url = webUrl(link, canvasUnit(canvas));
|
|
3634
|
-
const pub = unit.publicId ? publicUrl(link.host, unit.publicId) : null;
|
|
3635
|
-
if (options.json) return print(JSON.stringify({ canvas, url, publicUrl: pub, archived: unit.archived }, null, 2));
|
|
3636
|
-
print(url);
|
|
3637
|
-
if (pub) print(`${dim("public")} ${pub}`);
|
|
3638
|
-
if (unit.archived) print(dim("archived \u2014 `design unarchive` brings it back"));
|
|
3639
|
-
}
|
|
3640
|
-
async function runPublish(paths, value, publish) {
|
|
3641
|
-
const verb = publish ? "publish" : "unpublish";
|
|
3642
|
-
const canvas = canvasArg(value, `design ${verb} <canvas>`);
|
|
3643
|
-
const { link, client } = cloud(paths);
|
|
3644
|
-
await remoteCanvas(client, link.project, canvas);
|
|
3645
|
-
const { publicId } = await client.canvasAction(link.project, canvas, verb);
|
|
3646
|
-
if (publish) {
|
|
3647
|
-
const id2 = publicId ?? (await remoteCanvas(client, link.project, canvas)).publicId;
|
|
3648
|
-
print(`${green("\u2713")} Published ${bold(canvas)}: anyone with the link can view it`);
|
|
3649
|
-
if (id2) print(` ${cyan(publicUrl(link.host, id2))}`);
|
|
3650
|
-
} else print(`${green("\u2713")} ${bold(canvas)} has no public link now; members still open it in the project`);
|
|
3651
|
-
}
|
|
3652
|
-
async function runArchive(paths, value, archive) {
|
|
3653
|
-
const verb = archive ? "archive" : "unarchive";
|
|
3654
|
-
const canvas = canvasArg(value, `design ${verb} <canvas>`);
|
|
3655
|
-
const { link, client } = cloud(paths);
|
|
3656
|
-
await remoteCanvas(client, link.project, canvas);
|
|
3657
|
-
await client.canvasAction(link.project, canvas, verb);
|
|
3658
|
-
print(
|
|
3659
|
-
archive ? `${green("\u2713")} Archived ${bold(canvas)} in the cloud; its history is kept. \`design unarchive ${canvas}\` brings it back.` : `${green("\u2713")} ${bold(canvas)} is active again ${dim(webUrl(link, canvasUnit(canvas)))}`
|
|
3660
|
-
);
|
|
3661
|
-
}
|
|
3662
|
-
async function runHistory(paths, value, options) {
|
|
3663
|
-
if (!value) throw new CliError("Usage: design history <canvas|system>");
|
|
3664
|
-
const key = unitArg(value);
|
|
3665
|
-
const { link, client } = cloud(paths);
|
|
3666
|
-
let revisions;
|
|
3667
|
-
try {
|
|
3668
|
-
revisions = await client.revisions(link.project, key);
|
|
3669
|
-
} catch (error) {
|
|
3670
|
-
if (error instanceof CloudError && error.status === 404)
|
|
3671
|
-
throw new CliError(`${key} is not in the cloud yet: \`design push\` uploads it`);
|
|
3672
|
-
throw error;
|
|
3673
|
-
}
|
|
3674
|
-
if (options.json) return print(JSON.stringify({ unit: key, revisions }, null, 2));
|
|
3675
|
-
print(`${bold(key)} ${dim(webUrl(link, key))}`);
|
|
3676
|
-
for (const rev of revisions) {
|
|
3677
|
-
const parts = [
|
|
3678
|
-
rev.head ? green("current") : rev.rollbackAllowed ? "" : dim("built on an older system"),
|
|
3679
|
-
ago(rev.createdAt),
|
|
3680
|
-
rev.author ?? "",
|
|
3681
|
-
rev.screens !== null ? plural(rev.screens, "screen") : "",
|
|
3682
|
-
rev.bytes !== null ? megabytes(rev.bytes) : ""
|
|
3683
|
-
].filter(Boolean);
|
|
3684
|
-
print(` ${bold(`r${rev.rev}`.padEnd(5))} ${parts.join(dim(" \xB7 "))}`);
|
|
3685
|
-
}
|
|
3686
|
-
const target = revisions.find((rev) => rev.rollbackAllowed);
|
|
3687
|
-
if (key !== SYSTEM_UNIT && target)
|
|
3688
|
-
print(dim(`\`design rollback ${key.slice(7)} ${target.rev}\` makes a revision current again`));
|
|
3689
|
-
if (key === SYSTEM_UNIT) print(dim("The design system is not rolled back; change it and push."));
|
|
3690
|
-
}
|
|
3691
|
-
async function runRollback(paths, value, revArg) {
|
|
3692
|
-
const canvas = canvasArg(value, "design rollback <canvas> <rev>");
|
|
3693
|
-
const rev = Number((revArg ?? "").replace(/^r/i, ""));
|
|
3694
|
-
if (!Number.isInteger(rev) || rev < 1)
|
|
3695
|
-
throw new CliError("Usage: design rollback <canvas> <rev> (see `design history <canvas>`)");
|
|
3696
|
-
const { link, client } = cloud(paths);
|
|
3697
|
-
const head = await client.rollback(link.project, canvasUnit(canvas), rev);
|
|
3698
|
-
print(`${green("\u2713")} ${bold(canvas)}: r${rev} is current again as r${head} in the cloud`);
|
|
3699
|
-
await runPull(paths, [canvas], { theirs: [], json: false });
|
|
3700
|
-
}
|
|
3701
|
-
var obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3702
|
-
var n = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
3703
|
-
async function runMe(paths, options) {
|
|
3704
|
-
const link = paths ? readLink(paths) : null;
|
|
3705
|
-
const host = link?.host ?? cloudHost();
|
|
3706
|
-
const credential = requireCredential(host);
|
|
3707
|
-
const client = new CloudClient(host, credential.token);
|
|
3708
|
-
const [me, usage, activity] = await Promise.all([
|
|
3709
|
-
client.request("GET", "/me").then(obj),
|
|
3710
|
-
client.request("GET", "/me/usage").then(obj),
|
|
3711
|
-
client.request("GET", "/me/activity").then(obj)
|
|
3712
|
-
]);
|
|
3713
|
-
const plan = obj(usage.plan);
|
|
3714
|
-
const limits = obj(usage.limits);
|
|
3715
|
-
const totals = obj(activity.totals);
|
|
3716
|
-
let linked = null;
|
|
3717
|
-
if (link) {
|
|
3718
|
-
const projects = await client.projects().catch(() => null);
|
|
3719
|
-
const project = projects && [...projects.owned, ...projects.shared].find((p) => p.id === link.project);
|
|
3720
|
-
linked = project ? { id: project.id, name: project.name, role: project.role } : { id: link.project, name: "", role: "" };
|
|
3721
|
-
}
|
|
3722
|
-
const data = {
|
|
3723
|
-
host,
|
|
3724
|
-
email: me.email,
|
|
3725
|
-
name: me.name,
|
|
3726
|
-
memberSince: me.createdAt,
|
|
3727
|
-
plan: { id: plan.id, name: plan.name, expiresAt: plan.expiresAt ?? null },
|
|
3728
|
-
storage: { usedBytes: n(usage.storageBytes), limitBytes: n(limits.storage_bytes) },
|
|
3729
|
-
activeProjects: { used: n(usage.activeProjects), limit: n(limits.active_projects) },
|
|
3730
|
-
canvases: n(totals.canvases),
|
|
3731
|
-
screens: n(totals.screens),
|
|
3732
|
-
pushesLastYear: n(totals.pushesLastYear),
|
|
3733
|
-
pushesPerHour: n(limits.pushes_per_hour),
|
|
3734
|
-
linked
|
|
3735
|
-
};
|
|
3736
|
-
if (options.json) return print(JSON.stringify(data, null, 2));
|
|
3737
|
-
const label = (text2) => dim(text2.padEnd(10));
|
|
3738
|
-
const date = (iso) => typeof iso === "string" ? new Date(iso).toDateString().slice(4) : "";
|
|
3739
|
-
const share = data.storage.limitBytes ? Math.round(data.storage.usedBytes / data.storage.limitBytes * 100) : 0;
|
|
3740
|
-
print(
|
|
3741
|
-
`${bold(String(data.email ?? ""))}${data.name && data.name !== data.email ? ` ${dim(`(${data.name})`)}` : ""} ${dim(host)}`
|
|
3742
|
-
);
|
|
3743
|
-
print(
|
|
3744
|
-
`${label("Plan")}${bold(String(data.plan.name ?? "Free"))} ${dim(data.plan.expiresAt ? `until ${date(data.plan.expiresAt)}` : "no end date")}`
|
|
3745
|
-
);
|
|
3746
|
-
print(
|
|
3747
|
-
`${label("Storage")}${megabytes(data.storage.usedBytes)} of ${megabytes(data.storage.limitBytes)} ${(share >= 90 ? red : share >= 80 ? yellow : dim)(`(${share}%)`)}`
|
|
3748
|
-
);
|
|
3749
|
-
print(`${label("Projects")}${data.activeProjects.used} of ${data.activeProjects.limit} active`);
|
|
3750
|
-
print(
|
|
3751
|
-
`${label("Canvases")}${plural(data.canvases, "canvas", "canvases")} in your projects \xB7 ${plural(data.screens, "screen")}`
|
|
3752
|
-
);
|
|
3753
|
-
print(
|
|
3754
|
-
`${label("Pushes")}${plural(data.pushesLastYear, "push", "pushes")} in the last year ${dim(`\xB7 up to ${data.pushesPerHour} an hour`)}`
|
|
3755
|
-
);
|
|
3756
|
-
if (linked)
|
|
3757
|
-
print(
|
|
3758
|
-
`${label("Linked")}${linked.name || linked.id}${linked.role ? dim(` as ${linked.role}`) : ""} ${dim(webUrl(link))}`
|
|
3759
|
-
);
|
|
3760
|
-
else if (paths) print(`${label("Linked")}${dim("no \u2014 `design link`")}`);
|
|
3761
|
-
print(`${label("Since")}${date(data.memberSince)} ${dim(`\xB7 plans and upgrades: ${host.replace(/\/+$/, "")}/plans`)}`);
|
|
3762
|
-
}
|
|
3763
|
-
|
|
3764
|
-
// src/capture/chrome.ts
|
|
3765
|
-
import fs19 from "fs";
|
|
3766
|
-
var CANDIDATES = {
|
|
3767
|
-
darwin: [
|
|
3768
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
3769
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
3770
|
-
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
3771
|
-
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
3772
|
-
],
|
|
3773
|
-
linux: [
|
|
3774
|
-
"/usr/bin/google-chrome",
|
|
3775
|
-
"/usr/bin/google-chrome-stable",
|
|
3776
|
-
"/usr/bin/chromium",
|
|
3777
|
-
"/usr/bin/chromium-browser",
|
|
3778
|
-
"/snap/bin/chromium",
|
|
3779
|
-
"/usr/bin/microsoft-edge"
|
|
3780
|
-
],
|
|
3781
|
-
win32: [
|
|
3782
|
-
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
3783
|
-
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
3784
|
-
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
3785
|
-
]
|
|
3786
|
-
};
|
|
3787
|
-
function findChrome() {
|
|
3788
|
-
const configured = process.env.DESIGN_CHROME;
|
|
3789
|
-
if (configured && fs19.existsSync(configured)) return configured;
|
|
3790
|
-
for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
|
|
3791
|
-
return null;
|
|
3792
|
-
}
|
|
3793
|
-
async function launchChrome() {
|
|
3794
|
-
const executablePath = findChrome();
|
|
3795
|
-
if (!executablePath) {
|
|
3796
|
-
throw new Error(
|
|
3797
|
-
"No Chrome, Chromium or Edge found. Install one, or point DESIGN_CHROME at a Chromium-based browser binary."
|
|
3798
|
-
);
|
|
3799
|
-
}
|
|
3800
|
-
const { chromium } = await import("playwright-core");
|
|
3801
|
-
return chromium.launch({ executablePath, headless: true, args: ["--hide-scrollbars", "--force-color-profile=srgb"] });
|
|
3802
|
-
}
|
|
3803
|
-
var OUTDATED_DEP = /Outdated Optimize Dep/;
|
|
3804
|
-
async function openFrame(page, url, options = {}) {
|
|
3805
|
-
let errors = [];
|
|
3806
|
-
let outdated = () => {
|
|
3807
|
-
};
|
|
3808
|
-
const record2 = (text2) => {
|
|
3809
|
-
errors.push(text2);
|
|
3810
|
-
if (OUTDATED_DEP.test(text2)) outdated();
|
|
3811
|
-
};
|
|
3812
|
-
page.on("pageerror", (error) => record2(error.message));
|
|
3813
|
-
page.on("console", (message) => {
|
|
3814
|
-
if (message.type() !== "error") return;
|
|
3815
|
-
let text2 = message.text().replace(/^(?:\s*%[osdifcO])+\s*/, "");
|
|
3816
|
-
const where = message.location().url;
|
|
3817
|
-
if (/^Failed to load resource/.test(text2) && where && !text2.includes(where)) text2 += ` (${where})`;
|
|
3818
|
-
record2(text2);
|
|
3819
|
-
});
|
|
3820
|
-
let ready = false;
|
|
3821
|
-
let reloads = 0;
|
|
3822
|
-
const onNavigate = () => {
|
|
3823
|
-
reloads++;
|
|
3824
|
-
};
|
|
3825
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3826
|
-
errors = [];
|
|
3827
|
-
reloads = 0;
|
|
3828
|
-
ready = options.waitForReady === false;
|
|
3829
|
-
const turnedAway = new Promise((resolve) => {
|
|
3830
|
-
outdated = () => resolve("outdated");
|
|
3831
|
-
});
|
|
3832
|
-
await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
|
|
3833
|
-
page.on("load", onNavigate);
|
|
3834
|
-
if (!ready) {
|
|
3835
|
-
const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
|
|
3836
|
-
timeout: options.timeoutMs ?? 2e4
|
|
3837
|
-
}).then(() => "ready");
|
|
3838
|
-
signal.catch(() => {
|
|
3839
|
-
});
|
|
3840
|
-
ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
|
|
3841
|
-
}
|
|
3842
|
-
if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
|
|
3843
|
-
page.off("load", onNavigate);
|
|
3844
|
-
if (!reloads && !errors.some((error) => OUTDATED_DEP.test(error))) break;
|
|
3845
|
-
}
|
|
3846
|
-
if (reloads) errors.push(`the page reloaded itself ${reloads === 1 ? "once" : `${reloads} times`} while it loaded`);
|
|
3847
|
-
const held = ready ? 0 : await page.evaluate(() => window.__DESIGN_HOLDS__ ?? 0).catch(() => 0);
|
|
3848
|
-
const runtimeErrors = await page.evaluate(() => window.__DESIGN_ERRORS__ ?? []).catch(() => []);
|
|
3849
|
-
for (const error of runtimeErrors) if (!errors.includes(error)) errors.push(error);
|
|
3850
|
-
const overlay = await page.evaluate(() => {
|
|
3851
|
-
const el = document.querySelector("vite-error-overlay");
|
|
3852
|
-
return el?.shadowRoot?.querySelector(".message")?.textContent ?? null;
|
|
3853
|
-
}).catch(() => null);
|
|
3854
|
-
if (overlay) errors.unshift(overlay.trim());
|
|
3855
|
-
const unique2 = errors.filter(
|
|
3856
|
-
(error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
|
|
3857
|
-
);
|
|
3858
|
-
return { errors: [...new Set(unique2)], ready, ...held ? { held } : {} };
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
// src/server/state.ts
|
|
3862
|
-
import fs20 from "fs";
|
|
3863
|
-
import path19 from "path";
|
|
3864
|
-
var stateFile = (paths) => path19.join(paths.cache, "server.json");
|
|
3865
|
-
var logFile = (paths) => path19.join(paths.cache, "server.log");
|
|
3866
|
-
function readState(paths) {
|
|
3867
|
-
try {
|
|
3868
|
-
return JSON.parse(fs20.readFileSync(stateFile(paths), "utf8"));
|
|
3869
|
-
} catch {
|
|
3870
|
-
return null;
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
function writeState(paths, state) {
|
|
3874
|
-
fs20.mkdirSync(paths.cache, { recursive: true });
|
|
3875
|
-
fs20.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
|
|
3876
|
-
`);
|
|
3877
|
-
}
|
|
3878
|
-
function clearState(paths, pid) {
|
|
3879
|
-
const state = readState(paths);
|
|
3880
|
-
if (pid !== void 0 && state && state.pid !== pid) return;
|
|
3881
|
-
fs20.rmSync(stateFile(paths), { force: true });
|
|
3882
|
-
}
|
|
3883
|
-
function pidAlive(pid) {
|
|
3884
|
-
try {
|
|
3885
|
-
process.kill(pid, 0);
|
|
3886
|
-
return true;
|
|
3887
|
-
} catch (error) {
|
|
3888
|
-
return error.code === "EPERM";
|
|
3889
|
-
}
|
|
3890
|
-
}
|
|
3891
|
-
async function liveServer(paths) {
|
|
3892
|
-
const state = readState(paths);
|
|
3893
|
-
if (!state || !pidAlive(state.pid)) return null;
|
|
3894
|
-
try {
|
|
3895
|
-
const response = await fetch(`http://127.0.0.1:${state.port}/api/health`, { signal: AbortSignal.timeout(1500) });
|
|
3896
|
-
if (!response.ok) return null;
|
|
3897
|
-
const health = await response.json();
|
|
3898
|
-
return health.root === paths.root ? state : null;
|
|
3899
|
-
} catch {
|
|
3900
|
-
return null;
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
|
|
3904
|
-
// src/cli/commands/preview.ts
|
|
3905
|
-
import { spawn } from "child_process";
|
|
3906
|
-
import fs24 from "fs";
|
|
3907
|
-
|
|
3908
|
-
// src/server/index.ts
|
|
3909
|
-
import fs23 from "fs";
|
|
3910
|
-
import http from "http";
|
|
3911
|
-
import path22 from "path";
|
|
3912
|
-
import sirv from "sirv";
|
|
3913
|
-
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
3914
|
-
|
|
3915
|
-
// src/server/api.ts
|
|
3916
|
-
import fs21 from "fs";
|
|
3917
|
-
import path20 from "path";
|
|
3918
|
-
function sendJson(res, status, body) {
|
|
3919
|
-
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
3920
|
-
res.end(JSON.stringify(body));
|
|
3921
|
-
}
|
|
3922
|
-
function readBody(req, limit = 25 * 1024 * 1024) {
|
|
3923
|
-
return new Promise((resolve, reject) => {
|
|
3924
|
-
const chunks = [];
|
|
3925
|
-
let size2 = 0;
|
|
3926
|
-
req.on("data", (chunk2) => {
|
|
3927
|
-
size2 += chunk2.length;
|
|
3928
|
-
if (size2 > limit) {
|
|
3929
|
-
reject(new Error("body too large"));
|
|
3930
|
-
req.destroy();
|
|
3931
|
-
} else chunks.push(chunk2);
|
|
3932
|
-
});
|
|
3933
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3934
|
-
req.on("error", reject);
|
|
3935
|
-
});
|
|
3936
|
-
}
|
|
3937
|
-
async function handleApi(ctx, req, res, url) {
|
|
3938
|
-
const { project, events, snapshots } = ctx;
|
|
3939
|
-
const route2 = url.pathname.slice("/api".length);
|
|
3940
|
-
const method = req.method ?? "GET";
|
|
3941
|
-
if (route2 === "/health") {
|
|
3942
|
-
sendJson(res, 200, { ok: true, root: project.paths.root, version: packageVersion(), pid: process.pid });
|
|
3943
|
-
return true;
|
|
3944
|
-
}
|
|
3945
|
-
if (route2 === "/events") {
|
|
3946
|
-
events.attach(req, res);
|
|
3947
|
-
return true;
|
|
4054
|
+
if (route2 === "/events") {
|
|
4055
|
+
events.attach(req, res);
|
|
4056
|
+
return true;
|
|
3948
4057
|
}
|
|
3949
4058
|
if (route2 === "/project") {
|
|
3950
4059
|
sendJson(res, 200, project.info(false));
|
|
@@ -3965,17 +4074,17 @@ async function handleApi(ctx, req, res, url) {
|
|
|
3965
4074
|
}
|
|
3966
4075
|
if (route2 === "/source") {
|
|
3967
4076
|
const rel = url.searchParams.get("path") ?? "";
|
|
3968
|
-
const file =
|
|
3969
|
-
if (!rel || !isInside(project.paths.root, file) || !
|
|
4077
|
+
const file = path23.resolve(project.paths.root, rel);
|
|
4078
|
+
if (!rel || !isInside(project.paths.root, file) || !fs25.existsSync(file) || fs25.statSync(file).isDirectory()) {
|
|
3970
4079
|
sendJson(res, 404, { error: "not found" });
|
|
3971
4080
|
return true;
|
|
3972
4081
|
}
|
|
3973
|
-
if (
|
|
4082
|
+
if (fs25.statSync(file).size > 2 * 1024 * 1024) {
|
|
3974
4083
|
sendJson(res, 413, { error: "file too large to show" });
|
|
3975
4084
|
return true;
|
|
3976
4085
|
}
|
|
3977
4086
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
3978
|
-
res.end(
|
|
4087
|
+
res.end(fs25.readFileSync(file, "utf8"));
|
|
3979
4088
|
return true;
|
|
3980
4089
|
}
|
|
3981
4090
|
if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
|
|
@@ -4022,7 +4131,7 @@ async function handleApi(ctx, req, res, url) {
|
|
|
4022
4131
|
return true;
|
|
4023
4132
|
}
|
|
4024
4133
|
res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
|
|
4025
|
-
|
|
4134
|
+
fs25.createReadStream(file).pipe(res);
|
|
4026
4135
|
return true;
|
|
4027
4136
|
}
|
|
4028
4137
|
return false;
|
|
@@ -4061,73 +4170,48 @@ var EventHub = class {
|
|
|
4061
4170
|
}
|
|
4062
4171
|
};
|
|
4063
4172
|
|
|
4064
|
-
// src/server/
|
|
4065
|
-
import
|
|
4066
|
-
import
|
|
4067
|
-
var
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
this.urlBase = urlBase;
|
|
4075
|
-
}
|
|
4076
|
-
dir;
|
|
4077
|
-
urlBase;
|
|
4078
|
-
meta = /* @__PURE__ */ new Map();
|
|
4079
|
-
metaFile(canvas) {
|
|
4080
|
-
return path21.join(this.dir, canvas, "meta.json");
|
|
4081
|
-
}
|
|
4082
|
-
load(canvas) {
|
|
4083
|
-
const cached = this.meta.get(canvas);
|
|
4084
|
-
if (cached) return cached;
|
|
4085
|
-
let data = {};
|
|
4086
|
-
try {
|
|
4087
|
-
data = JSON.parse(fs22.readFileSync(this.metaFile(canvas), "utf8"));
|
|
4088
|
-
} catch {
|
|
4089
|
-
}
|
|
4090
|
-
this.meta.set(canvas, data);
|
|
4091
|
-
return data;
|
|
4092
|
-
}
|
|
4093
|
-
url(canvas, id2, theme2, version, thumb = false) {
|
|
4094
|
-
const name = `${encodeURIComponent(id2)}.${theme2}${thumb ? ".thumb" : ""}.png`;
|
|
4095
|
-
return `${this.urlBase}/${encodeURIComponent(canvas)}/${name}?v=${version}`;
|
|
4173
|
+
// src/server/state.ts
|
|
4174
|
+
import fs26 from "fs";
|
|
4175
|
+
import path24 from "path";
|
|
4176
|
+
var stateFile2 = (paths) => path24.join(paths.cache, "server.json");
|
|
4177
|
+
var logFile = (paths) => path24.join(paths.cache, "server.log");
|
|
4178
|
+
function readState(paths) {
|
|
4179
|
+
try {
|
|
4180
|
+
return JSON.parse(fs26.readFileSync(stateFile2(paths), "utf8"));
|
|
4181
|
+
} catch {
|
|
4182
|
+
return null;
|
|
4096
4183
|
}
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
fs22.mkdirSync(dir, { recursive: true });
|
|
4115
|
-
fs22.writeFileSync(path21.join(dir, `${id2}.${theme2}.png`), png);
|
|
4116
|
-
const data = this.load(canvas);
|
|
4117
|
-
const version = Date.now();
|
|
4118
|
-
data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
|
|
4119
|
-
fs22.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
|
|
4120
|
-
return { url: this.url(canvas, id2, theme2, version) };
|
|
4184
|
+
}
|
|
4185
|
+
function writeState(paths, state) {
|
|
4186
|
+
fs26.mkdirSync(paths.cache, { recursive: true });
|
|
4187
|
+
fs26.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
|
|
4188
|
+
`);
|
|
4189
|
+
}
|
|
4190
|
+
function clearState(paths, pid) {
|
|
4191
|
+
const state = readState(paths);
|
|
4192
|
+
if (pid !== void 0 && state && state.pid !== pid) return;
|
|
4193
|
+
fs26.rmSync(stateFile2(paths), { force: true });
|
|
4194
|
+
}
|
|
4195
|
+
function pidAlive(pid) {
|
|
4196
|
+
try {
|
|
4197
|
+
process.kill(pid, 0);
|
|
4198
|
+
return true;
|
|
4199
|
+
} catch (error) {
|
|
4200
|
+
return error.code === "EPERM";
|
|
4121
4201
|
}
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4202
|
+
}
|
|
4203
|
+
async function liveServer(paths) {
|
|
4204
|
+
const state = readState(paths);
|
|
4205
|
+
if (!state || !pidAlive(state.pid)) return null;
|
|
4206
|
+
try {
|
|
4207
|
+
const response = await fetch(`http://127.0.0.1:${state.port}/api/health`, { signal: AbortSignal.timeout(1500) });
|
|
4208
|
+
if (!response.ok) return null;
|
|
4209
|
+
const health = await response.json();
|
|
4210
|
+
return health.root === paths.root ? state : null;
|
|
4211
|
+
} catch {
|
|
4212
|
+
return null;
|
|
4129
4213
|
}
|
|
4130
|
-
}
|
|
4214
|
+
}
|
|
4131
4215
|
|
|
4132
4216
|
// src/server/index.ts
|
|
4133
4217
|
var DEFAULT_PORT = 0;
|
|
@@ -4163,8 +4247,8 @@ function notFoundPage(res, title, text2) {
|
|
|
4163
4247
|
}
|
|
4164
4248
|
async function startDevServer(options) {
|
|
4165
4249
|
const paths = designPaths(options.root);
|
|
4166
|
-
if (!
|
|
4167
|
-
|
|
4250
|
+
if (!fs27.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
|
|
4251
|
+
fs27.mkdirSync(paths.cache, { recursive: true });
|
|
4168
4252
|
const snapshots = new SnapshotStore(paths.snapshots);
|
|
4169
4253
|
const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
|
|
4170
4254
|
const entries = new Entries(project);
|
|
@@ -4190,8 +4274,8 @@ async function startDevServer(options) {
|
|
|
4190
4274
|
// `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
|
|
4191
4275
|
// generated entries are modules the frames load, so those stay watched.
|
|
4192
4276
|
ignored: (file) => {
|
|
4193
|
-
const rel = toPosix(
|
|
4194
|
-
const inCache = rel !== "" && !rel.startsWith("..") && !
|
|
4277
|
+
const rel = toPosix(path25.relative(paths.cache, file));
|
|
4278
|
+
const inCache = rel !== "" && !rel.startsWith("..") && !path25.isAbsolute(rel);
|
|
4195
4279
|
return inCache && rel !== "entries" && !rel.startsWith("entries/");
|
|
4196
4280
|
}
|
|
4197
4281
|
},
|
|
@@ -4200,7 +4284,7 @@ async function startDevServer(options) {
|
|
|
4200
4284
|
forwardConsole: false
|
|
4201
4285
|
},
|
|
4202
4286
|
optimizeDeps: {
|
|
4203
|
-
entries: entries.files().map((file) => toPosix(
|
|
4287
|
+
entries: entries.files().map((file) => toPosix(path25.relative(paths.design, file)))
|
|
4204
4288
|
}
|
|
4205
4289
|
});
|
|
4206
4290
|
const publicDir = project.publicDir();
|
|
@@ -4212,16 +4296,16 @@ async function startDevServer(options) {
|
|
|
4212
4296
|
return null;
|
|
4213
4297
|
}
|
|
4214
4298
|
};
|
|
4215
|
-
const viewer =
|
|
4216
|
-
const viewerIndex =
|
|
4299
|
+
const viewer = fs27.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
|
|
4300
|
+
const viewerIndex = path25.join(PKG.viewer, "index.html");
|
|
4217
4301
|
const serveViewerIndex = (res) => {
|
|
4218
|
-
const html =
|
|
4302
|
+
const html = fs27.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
|
|
4219
4303
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
4220
4304
|
res.end(html);
|
|
4221
4305
|
};
|
|
4222
4306
|
const ctx = { project, events, snapshots };
|
|
4223
4307
|
const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
|
|
4224
|
-
const bootUrl = `/${relToDesign(paths,
|
|
4308
|
+
const bootUrl = `/${relToDesign(paths, path25.join(entries.dir, HTML_BOOT))}`;
|
|
4225
4309
|
const frameHead = (source, extra = {}) => {
|
|
4226
4310
|
const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
|
|
4227
4311
|
return {
|
|
@@ -4242,7 +4326,7 @@ async function startDevServer(options) {
|
|
|
4242
4326
|
notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
|
|
4243
4327
|
return;
|
|
4244
4328
|
}
|
|
4245
|
-
if (!
|
|
4329
|
+
if (!fs27.existsSync(entries.entryFile(source))) entries.sync();
|
|
4246
4330
|
const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
|
|
4247
4331
|
const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
|
|
4248
4332
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -4251,8 +4335,8 @@ async function startDevServer(options) {
|
|
|
4251
4335
|
};
|
|
4252
4336
|
const serveHtmlScreen = async (res, url2) => {
|
|
4253
4337
|
const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
|
|
4254
|
-
const file =
|
|
4255
|
-
if (!isInside(paths.design, file) || !
|
|
4338
|
+
const file = path25.resolve(paths.design, rel);
|
|
4339
|
+
if (!isInside(paths.design, file) || !fs27.existsSync(file)) {
|
|
4256
4340
|
notFoundPage(res, "Screen not found", `No file .design/${rel}`);
|
|
4257
4341
|
return;
|
|
4258
4342
|
}
|
|
@@ -4262,13 +4346,13 @@ async function startDevServer(options) {
|
|
|
4262
4346
|
key: `${canvas}/${id2}`,
|
|
4263
4347
|
canvas,
|
|
4264
4348
|
id: id2,
|
|
4265
|
-
title:
|
|
4349
|
+
title: path25.basename(file),
|
|
4266
4350
|
file,
|
|
4267
4351
|
format: "html",
|
|
4268
4352
|
props: {},
|
|
4269
4353
|
system: true
|
|
4270
4354
|
};
|
|
4271
|
-
const raw =
|
|
4355
|
+
const raw = fs27.readFileSync(file, "utf8");
|
|
4272
4356
|
const withSystem = source.system && !optsOutOfSystem(raw);
|
|
4273
4357
|
const html = injectIntoHtml(
|
|
4274
4358
|
raw,
|
|
@@ -4301,7 +4385,7 @@ async function startDevServer(options) {
|
|
|
4301
4385
|
}
|
|
4302
4386
|
if (url2.pathname.startsWith("/_fs/")) {
|
|
4303
4387
|
const rel = publicRel(url2.pathname.slice("/_fs/".length));
|
|
4304
|
-
if (publicDir && rel !== null && !project.isPublicFile(rel) &&
|
|
4388
|
+
if (publicDir && rel !== null && !project.isPublicFile(rel) && fs27.existsSync(path25.join(publicDir, rel))) {
|
|
4305
4389
|
notFoundPage(res, "Not found", url2.pathname);
|
|
4306
4390
|
return;
|
|
4307
4391
|
}
|
|
@@ -4313,7 +4397,7 @@ async function startDevServer(options) {
|
|
|
4313
4397
|
const rel = publicRel(url2.pathname.slice(1));
|
|
4314
4398
|
if (servePublic && rel !== null && project.isPublicFile(rel))
|
|
4315
4399
|
servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
|
|
4316
|
-
else if (
|
|
4400
|
+
else if (path25.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
|
|
4317
4401
|
else serveViewerIndex(res);
|
|
4318
4402
|
};
|
|
4319
4403
|
if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
|
|
@@ -4341,7 +4425,7 @@ async function startDevServer(options) {
|
|
|
4341
4425
|
};
|
|
4342
4426
|
vite.watcher.on("all", (event, file) => {
|
|
4343
4427
|
if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
|
|
4344
|
-
const rel = toPosix(
|
|
4428
|
+
const rel = toPosix(path25.relative(paths.design, file));
|
|
4345
4429
|
if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
|
|
4346
4430
|
if (rel === "design.json") {
|
|
4347
4431
|
queue("config", () => {
|
|
@@ -4412,76 +4496,401 @@ function printUrls(paths, url) {
|
|
|
4412
4496
|
for (const id2 of project.canvasIds()) print(` ${dim("canvas")} ${url}/c/${id2}`);
|
|
4413
4497
|
if (project.system()) print(` ${dim("system")} ${url}/system`);
|
|
4414
4498
|
}
|
|
4415
|
-
async function waitForServer(paths, timeoutMs) {
|
|
4416
|
-
const until = Date.now() + timeoutMs;
|
|
4417
|
-
while (Date.now() < until) {
|
|
4418
|
-
const live2 = await liveServer(paths);
|
|
4419
|
-
if (live2) return live2;
|
|
4420
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
4421
|
-
}
|
|
4422
|
-
return null;
|
|
4499
|
+
async function waitForServer(paths, timeoutMs) {
|
|
4500
|
+
const until = Date.now() + timeoutMs;
|
|
4501
|
+
while (Date.now() < until) {
|
|
4502
|
+
const live2 = await liveServer(paths);
|
|
4503
|
+
if (live2) return live2;
|
|
4504
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
4505
|
+
}
|
|
4506
|
+
return null;
|
|
4507
|
+
}
|
|
4508
|
+
async function stopServer(paths) {
|
|
4509
|
+
const state = readState(paths);
|
|
4510
|
+
if (!state) return false;
|
|
4511
|
+
try {
|
|
4512
|
+
process.kill(state.pid, "SIGTERM");
|
|
4513
|
+
} catch {
|
|
4514
|
+
clearState(paths);
|
|
4515
|
+
return false;
|
|
4516
|
+
}
|
|
4517
|
+
const until = Date.now() + 5e3;
|
|
4518
|
+
while (Date.now() < until) {
|
|
4519
|
+
try {
|
|
4520
|
+
process.kill(state.pid, 0);
|
|
4521
|
+
} catch {
|
|
4522
|
+
break;
|
|
4523
|
+
}
|
|
4524
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4525
|
+
}
|
|
4526
|
+
clearState(paths);
|
|
4527
|
+
return true;
|
|
4528
|
+
}
|
|
4529
|
+
async function ensureServer(paths, options) {
|
|
4530
|
+
const live2 = await liveServer(paths);
|
|
4531
|
+
const stale = live2 && live2.version !== packageVersion();
|
|
4532
|
+
if (live2 && !options.restart && !stale) return live2;
|
|
4533
|
+
if (stale)
|
|
4534
|
+
print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
|
|
4535
|
+
if (live2) await stopServer(paths);
|
|
4536
|
+
fs28.mkdirSync(paths.cache, { recursive: true });
|
|
4537
|
+
const log = fs28.openSync(logFile(paths), "w");
|
|
4538
|
+
const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
|
|
4539
|
+
if (options.port) args.push("--port", String(options.port));
|
|
4540
|
+
const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
|
|
4541
|
+
child.unref();
|
|
4542
|
+
const state = await waitForServer(paths, 4e4);
|
|
4543
|
+
if (!state) {
|
|
4544
|
+
const tail = fs28.existsSync(logFile(paths)) ? fs28.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
|
|
4545
|
+
throw new CliError(`The preview server did not start.
|
|
4546
|
+
${tail.join("\n")}`);
|
|
4547
|
+
}
|
|
4548
|
+
return state;
|
|
4549
|
+
}
|
|
4550
|
+
async function runPreview(paths, options) {
|
|
4551
|
+
const openPath = typeof options.open === "string" ? options.open : "";
|
|
4552
|
+
if (options.foreground) {
|
|
4553
|
+
const server = await startDevServer({ root: paths.root, port: options.port });
|
|
4554
|
+
printUrls(paths, server.url);
|
|
4555
|
+
if (options.open) openBrowser(`${server.url}${openPath}`);
|
|
4556
|
+
const shutdown = () => {
|
|
4557
|
+
void server.close().finally(() => process.exit(0));
|
|
4558
|
+
};
|
|
4559
|
+
process.on("SIGINT", shutdown);
|
|
4560
|
+
process.on("SIGTERM", shutdown);
|
|
4561
|
+
return;
|
|
4562
|
+
}
|
|
4563
|
+
const state = await ensureServer(paths, options);
|
|
4564
|
+
printUrls(paths, state.url);
|
|
4565
|
+
if (options.open) openBrowser(`${state.url}${openPath}`);
|
|
4566
|
+
}
|
|
4567
|
+
async function runStop(paths) {
|
|
4568
|
+
print(await stopServer(paths) ? "Stopped the preview server." : "No preview server is running for this project.");
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// src/cli/commands/sync.ts
|
|
4572
|
+
var GOOD = /* @__PURE__ */ new Set(["pushed", "created", "updated", "deleted"]);
|
|
4573
|
+
var BAD = /* @__PURE__ */ new Set(["conflict", "remote_ahead"]);
|
|
4574
|
+
var QUIET = /* @__PURE__ */ new Set(["up_to_date", "unchanged"]);
|
|
4575
|
+
function revs(entry) {
|
|
4576
|
+
if (entry.action === "created") return entry.rev !== void 0 ? `r${entry.rev}` : "";
|
|
4577
|
+
if (entry.action === "deleted") return "";
|
|
4578
|
+
if (GOOD.has(entry.action))
|
|
4579
|
+
return entry.baseRev && entry.baseRev !== entry.rev ? `r${entry.baseRev} \u2192 r${entry.rev}` : `r${entry.rev}`;
|
|
4580
|
+
if (BAD.has(entry.action) && entry.headRev !== void 0) return `r${entry.baseRev ?? 0} \u2192 r${entry.headRev}`;
|
|
4581
|
+
return "";
|
|
4582
|
+
}
|
|
4583
|
+
function printReport(report) {
|
|
4584
|
+
const verb = report.command === "push" ? "Pushed to" : "Pulled from";
|
|
4585
|
+
const shown = report.units.filter((entry) => !QUIET.has(entry.action));
|
|
4586
|
+
const quiet = report.units.length - shown.length;
|
|
4587
|
+
const moved = report.units.some((entry) => GOOD.has(entry.action));
|
|
4588
|
+
if (report.ok && !moved) print(`${bold(`Nothing to ${report.command}`)} ${dim(report.url)}`);
|
|
4589
|
+
else if (report.ok) print(`${bold(verb)} ${report.url}`);
|
|
4590
|
+
else print(`${bold(report.command === "push" ? "Push stopped" : "Pull finished with conflicts")} ${dim(report.host)}`);
|
|
4591
|
+
const width = Math.max(0, ...shown.map((entry) => entry.unit.length));
|
|
4592
|
+
for (const entry of shown) {
|
|
4593
|
+
const mark = GOOD.has(entry.action) ? green("\u2713") : BAD.has(entry.action) ? red("\u2717") : yellow("!");
|
|
4594
|
+
const files = entry.files ? dim(
|
|
4595
|
+
[
|
|
4596
|
+
entry.files.added && `+${entry.files.added}`,
|
|
4597
|
+
entry.files.changed && `~${entry.files.changed}`,
|
|
4598
|
+
entry.files.removed && `-${entry.files.removed}`
|
|
4599
|
+
].filter(Boolean).join(" ")
|
|
4600
|
+
) : "";
|
|
4601
|
+
const detail2 = [entry.action.replaceAll("_", " "), revs(entry), files].filter(Boolean).join(" ");
|
|
4602
|
+
print(` ${mark} ${entry.unit.padEnd(width)} ${detail2}`);
|
|
4603
|
+
if (entry.url && GOOD.has(entry.action)) print(` ${dim("\u2192")} ${entry.url}`);
|
|
4604
|
+
if (entry.message) print(` ${dim(entry.message)}`);
|
|
4605
|
+
}
|
|
4606
|
+
if (quiet) print(dim(` ${plural(quiet, "unit")} ${report.command === "push" ? "unchanged" : "up to date"}`));
|
|
4607
|
+
for (const hint of report.hints) print(dim(hint));
|
|
4608
|
+
}
|
|
4609
|
+
async function runSync(paths, command, json, run) {
|
|
4610
|
+
let report;
|
|
4611
|
+
try {
|
|
4612
|
+
report = await run(json ? openSync(paths, () => {
|
|
4613
|
+
}) : openSync(paths, (line) => print(dim(line)), progress));
|
|
4614
|
+
} catch (error) {
|
|
4615
|
+
if (!json) throw error;
|
|
4616
|
+
let link = null;
|
|
4617
|
+
try {
|
|
4618
|
+
link = readLink(paths);
|
|
4619
|
+
} catch {
|
|
4620
|
+
}
|
|
4621
|
+
const cloud2 = error instanceof CloudError ? error : null;
|
|
4622
|
+
const failure = {
|
|
4623
|
+
ok: false,
|
|
4624
|
+
command,
|
|
4625
|
+
host: link?.host ?? "",
|
|
4626
|
+
project: link?.project ?? "",
|
|
4627
|
+
url: link ? webUrl(link) : "",
|
|
4628
|
+
units: [],
|
|
4629
|
+
hints: [],
|
|
4630
|
+
error: {
|
|
4631
|
+
code: cloud2?.error ?? (error instanceof CliError ? "error" : "internal"),
|
|
4632
|
+
message: error.message,
|
|
4633
|
+
status: cloud2?.status || void 0,
|
|
4634
|
+
limit: cloud2?.limit
|
|
4635
|
+
}
|
|
4636
|
+
};
|
|
4637
|
+
print(JSON.stringify(failure, null, 2));
|
|
4638
|
+
process.exitCode = error instanceof CliError ? error.code : 1;
|
|
4639
|
+
return;
|
|
4640
|
+
}
|
|
4641
|
+
if (json) {
|
|
4642
|
+
print(JSON.stringify(report, null, 2));
|
|
4643
|
+
if (!report.ok) process.exitCode = 2;
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
printReport(report);
|
|
4647
|
+
if (!report.ok && report.error) throw new CliError(report.error.message, 2);
|
|
4648
|
+
}
|
|
4649
|
+
function runPush(paths, units, options) {
|
|
4650
|
+
const beforeBuild = (canvasIds) => refreshCovers(paths, canvasIds, async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`);
|
|
4651
|
+
return runSync(paths, "push", options.json, async (ctx) => {
|
|
4652
|
+
const first = await push(ctx, { units, resolved: options.resolved, beforeBuild });
|
|
4653
|
+
if (first.ok || first.error?.code !== "pull_first") return first;
|
|
4654
|
+
ctx.log("The cloud is ahead: merging its changes first\u2026");
|
|
4655
|
+
const pulled = await pull(ctx, { units: [], theirs: [] });
|
|
4656
|
+
if (!pulled.ok) {
|
|
4657
|
+
pulled.error = {
|
|
4658
|
+
code: pulled.error?.code ?? "conflict",
|
|
4659
|
+
message: `Nothing was pushed: ${pulled.error?.message ?? "the pull did not finish"}`
|
|
4660
|
+
};
|
|
4661
|
+
return pulled;
|
|
4662
|
+
}
|
|
4663
|
+
ctx.link = readLink(paths);
|
|
4664
|
+
const second = await push(ctx, { units, resolved: [], beforeBuild });
|
|
4665
|
+
second.hints.unshift(
|
|
4666
|
+
`Merged the cloud's changes first: ${pulled.units.filter((entry) => entry.action !== "up_to_date").map((entry) => `${entry.unit} (${entry.action.replaceAll("_", " ")})`).join(", ")}`
|
|
4667
|
+
);
|
|
4668
|
+
return second;
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
function runPull(paths, units, options) {
|
|
4672
|
+
return runSync(paths, "pull", options.json, (ctx) => pull(ctx, { units, theirs: options.theirs }));
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4675
|
+
// src/cli/commands/canvases.ts
|
|
4676
|
+
var publicUrl = (host, publicId) => `${host.replace(/\/+$/, "")}/s/${publicId}`;
|
|
4677
|
+
function ago(iso) {
|
|
4678
|
+
const seconds2 = Math.max(0, (Date.now() - Date.parse(iso)) / 1e3);
|
|
4679
|
+
if (!Number.isFinite(seconds2)) return "";
|
|
4680
|
+
if (seconds2 < 60) return "just now";
|
|
4681
|
+
const [value, unit] = seconds2 < 3600 ? [seconds2 / 60, "minute"] : seconds2 < 86400 ? [seconds2 / 3600, "hour"] : seconds2 < 86400 * 60 ? [seconds2 / 86400, "day"] : [seconds2 / (86400 * 30), "month"];
|
|
4682
|
+
return `${plural(Math.floor(value), unit)} ago`;
|
|
4683
|
+
}
|
|
4684
|
+
function cloud(paths) {
|
|
4685
|
+
const link = requireLink(paths);
|
|
4686
|
+
const credential = requireCredential(link.host);
|
|
4687
|
+
return { link, client: new CloudClient(link.host, credential.token) };
|
|
4688
|
+
}
|
|
4689
|
+
function canvasArg(value, usage) {
|
|
4690
|
+
if (!value) throw new CliError(`Usage: ${usage}`);
|
|
4691
|
+
const key = unitArg(value);
|
|
4692
|
+
if (key === SYSTEM_UNIT) throw new CliError(`The design system is not a canvas. Usage: ${usage}`);
|
|
4693
|
+
return key.slice("canvas/".length);
|
|
4694
|
+
}
|
|
4695
|
+
async function remoteCanvas(client, project, canvas) {
|
|
4696
|
+
const unit = (await client.units(project)).get(canvasUnit(canvas));
|
|
4697
|
+
if (!unit) throw new CliError(`${canvas} is not in the cloud yet: \`design push ${canvas}\` uploads it`);
|
|
4698
|
+
return unit;
|
|
4699
|
+
}
|
|
4700
|
+
async function runCanvases(paths, options) {
|
|
4701
|
+
const project = new DesignProject(paths, STATIC_URLS);
|
|
4702
|
+
const local = project.canvasIds().map((id2) => project.canvas(id2).doc);
|
|
4703
|
+
const link = readLink(paths);
|
|
4704
|
+
let remote = null;
|
|
4705
|
+
let cloudNote = link ? null : "not linked to foss-design Cloud \u2014 `design link`";
|
|
4706
|
+
if (link) {
|
|
4707
|
+
const credential = getCredential(link.host);
|
|
4708
|
+
if (!credential) cloudNote = `not signed in to ${link.host} \u2014 \`design login\``;
|
|
4709
|
+
else {
|
|
4710
|
+
try {
|
|
4711
|
+
remote = await new CloudClient(link.host, credential.token).units(link.project);
|
|
4712
|
+
} catch (error) {
|
|
4713
|
+
cloudNote = `cloud unavailable: ${error.message}`;
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
const scan = link ? scanLocal(paths) : null;
|
|
4718
|
+
const ids = [
|
|
4719
|
+
.../* @__PURE__ */ new Set([
|
|
4720
|
+
...local.map((doc) => doc.id),
|
|
4721
|
+
...[...remote?.keys() ?? []].filter((key) => key.startsWith("canvas/")).map((key) => key.slice(7))
|
|
4722
|
+
])
|
|
4723
|
+
].sort();
|
|
4724
|
+
const rows = ids.map((id2) => {
|
|
4725
|
+
const doc = local.find((entry) => entry.id === id2);
|
|
4726
|
+
const unit = remote?.get(canvasUnit(id2));
|
|
4727
|
+
const status = link && remote ? unitStatus(canvasUnit(id2), link, scan?.units.get(canvasUnit(id2))?.manifest, remote) : null;
|
|
4728
|
+
const frames = doc?.pages.flatMap((page) => page.sections.flatMap((section) => section.items)).filter((item) => item.kind === "screen" || item.kind === "url");
|
|
4729
|
+
return {
|
|
4730
|
+
id: id2,
|
|
4731
|
+
title: doc?.title ?? unit?.title ?? id2,
|
|
4732
|
+
local: !!doc,
|
|
4733
|
+
pages: doc?.pages.length ?? null,
|
|
4734
|
+
screens: frames?.length ?? null,
|
|
4735
|
+
errors: doc?.issues.filter((issue) => issue.severity === "error").length ?? 0,
|
|
4736
|
+
cloud: unit ? {
|
|
4737
|
+
rev: unit.headRev,
|
|
4738
|
+
state: status?.state ?? null,
|
|
4739
|
+
archived: unit.archived,
|
|
4740
|
+
url: webUrl(link, canvasUnit(id2)),
|
|
4741
|
+
publicUrl: unit.publicId ? publicUrl(link.host, unit.publicId) : null
|
|
4742
|
+
} : null
|
|
4743
|
+
};
|
|
4744
|
+
});
|
|
4745
|
+
if (options.json) return print(JSON.stringify({ canvases: rows, cloud: cloudNote }, null, 2));
|
|
4746
|
+
if (!rows.length) print(dim("No canvases yet: `design new <canvas>` creates one."));
|
|
4747
|
+
const width = Math.max(0, ...rows.map((row) => row.id.length));
|
|
4748
|
+
for (const row of rows) {
|
|
4749
|
+
const facts = row.local ? [`${plural(row.pages ?? 0, "page")} \xB7 ${plural(row.screens ?? 0, "screen")}`] : [yellow("only in the cloud \u2014 `design pull`")];
|
|
4750
|
+
if (row.errors) facts.push(red(plural(row.errors, "error")));
|
|
4751
|
+
if (row.cloud) {
|
|
4752
|
+
const state = row.cloud.archived ? dim("archived") : row.cloud.state === "in_sync" ? green("in sync") : row.cloud.state ? yellow(row.cloud.state.replaceAll("_", " ")) : "";
|
|
4753
|
+
facts.push([`r${row.cloud.rev}`, state].filter(Boolean).join(" "));
|
|
4754
|
+
} else if (remote && row.local) facts.push(dim("not pushed"));
|
|
4755
|
+
print(`${bold(row.id.padEnd(width))} ${row.title} ${dim("\xB7")} ${facts.join(dim(" \xB7 "))}`);
|
|
4756
|
+
if (row.cloud && !row.cloud.archived) print(`${" ".repeat(width)} ${dim("\u2192")} ${row.cloud.url}`);
|
|
4757
|
+
if (row.cloud?.publicUrl) print(`${" ".repeat(width)} ${dim("public")} ${cyan(row.cloud.publicUrl)}`);
|
|
4758
|
+
}
|
|
4759
|
+
if (cloudNote) print(dim(cloudNote));
|
|
4760
|
+
}
|
|
4761
|
+
async function runUrl(paths, value, options) {
|
|
4762
|
+
const canvas = canvasArg(value, "design url <canvas>");
|
|
4763
|
+
const { link, client } = cloud(paths);
|
|
4764
|
+
const unit = await remoteCanvas(client, link.project, canvas);
|
|
4765
|
+
const url = webUrl(link, canvasUnit(canvas));
|
|
4766
|
+
const pub = unit.publicId ? publicUrl(link.host, unit.publicId) : null;
|
|
4767
|
+
if (options.json) return print(JSON.stringify({ canvas, url, publicUrl: pub, archived: unit.archived }, null, 2));
|
|
4768
|
+
print(url);
|
|
4769
|
+
if (pub) print(`${dim("public")} ${pub}`);
|
|
4770
|
+
if (unit.archived) print(dim("archived \u2014 `design unarchive` brings it back"));
|
|
4771
|
+
}
|
|
4772
|
+
async function runPublish(paths, value, publish) {
|
|
4773
|
+
const verb = publish ? "publish" : "unpublish";
|
|
4774
|
+
const canvas = canvasArg(value, `design ${verb} <canvas>`);
|
|
4775
|
+
const { link, client } = cloud(paths);
|
|
4776
|
+
await remoteCanvas(client, link.project, canvas);
|
|
4777
|
+
const { publicId } = await client.canvasAction(link.project, canvas, verb);
|
|
4778
|
+
if (publish) {
|
|
4779
|
+
const id2 = publicId ?? (await remoteCanvas(client, link.project, canvas)).publicId;
|
|
4780
|
+
print(`${green("\u2713")} Published ${bold(canvas)}: anyone with the link can view it`);
|
|
4781
|
+
if (id2) print(` ${cyan(publicUrl(link.host, id2))}`);
|
|
4782
|
+
} else print(`${green("\u2713")} ${bold(canvas)} has no public link now; members still open it in the project`);
|
|
4783
|
+
}
|
|
4784
|
+
async function runArchive(paths, value, archive) {
|
|
4785
|
+
const verb = archive ? "archive" : "unarchive";
|
|
4786
|
+
const canvas = canvasArg(value, `design ${verb} <canvas>`);
|
|
4787
|
+
const { link, client } = cloud(paths);
|
|
4788
|
+
await remoteCanvas(client, link.project, canvas);
|
|
4789
|
+
await client.canvasAction(link.project, canvas, verb);
|
|
4790
|
+
print(
|
|
4791
|
+
archive ? `${green("\u2713")} Archived ${bold(canvas)} in the cloud; its history is kept. \`design unarchive ${canvas}\` brings it back.` : `${green("\u2713")} ${bold(canvas)} is active again ${dim(webUrl(link, canvasUnit(canvas)))}`
|
|
4792
|
+
);
|
|
4423
4793
|
}
|
|
4424
|
-
async function
|
|
4425
|
-
|
|
4426
|
-
|
|
4794
|
+
async function runHistory(paths, value, options) {
|
|
4795
|
+
if (!value) throw new CliError("Usage: design history <canvas|system>");
|
|
4796
|
+
const key = unitArg(value);
|
|
4797
|
+
const { link, client } = cloud(paths);
|
|
4798
|
+
let revisions;
|
|
4427
4799
|
try {
|
|
4428
|
-
|
|
4429
|
-
} catch {
|
|
4430
|
-
|
|
4431
|
-
|
|
4800
|
+
revisions = await client.revisions(link.project, key);
|
|
4801
|
+
} catch (error) {
|
|
4802
|
+
if (error instanceof CloudError && error.status === 404)
|
|
4803
|
+
throw new CliError(`${key} is not in the cloud yet: \`design push\` uploads it`);
|
|
4804
|
+
throw error;
|
|
4432
4805
|
}
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4806
|
+
if (options.json) return print(JSON.stringify({ unit: key, revisions }, null, 2));
|
|
4807
|
+
print(`${bold(key)} ${dim(webUrl(link, key))}`);
|
|
4808
|
+
for (const rev of revisions) {
|
|
4809
|
+
const parts = [
|
|
4810
|
+
rev.head ? green("current") : rev.rollbackAllowed ? "" : dim("built on an older system"),
|
|
4811
|
+
ago(rev.createdAt),
|
|
4812
|
+
rev.author ?? "",
|
|
4813
|
+
rev.screens !== null ? plural(rev.screens, "screen") : "",
|
|
4814
|
+
rev.bytes !== null ? megabytes(rev.bytes) : ""
|
|
4815
|
+
].filter(Boolean);
|
|
4816
|
+
print(` ${bold(`r${rev.rev}`.padEnd(5))} ${parts.join(dim(" \xB7 "))}`);
|
|
4441
4817
|
}
|
|
4442
|
-
|
|
4443
|
-
|
|
4818
|
+
const target = revisions.find((rev) => rev.rollbackAllowed);
|
|
4819
|
+
if (key !== SYSTEM_UNIT && target)
|
|
4820
|
+
print(dim(`\`design rollback ${key.slice(7)} ${target.rev}\` makes a revision current again`));
|
|
4821
|
+
if (key === SYSTEM_UNIT) print(dim("The design system is not rolled back; change it and push."));
|
|
4444
4822
|
}
|
|
4445
|
-
async function
|
|
4446
|
-
const
|
|
4447
|
-
const
|
|
4448
|
-
if (
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
|
|
4455
|
-
if (options.port) args.push("--port", String(options.port));
|
|
4456
|
-
const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
|
|
4457
|
-
child.unref();
|
|
4458
|
-
const state = await waitForServer(paths, 4e4);
|
|
4459
|
-
if (!state) {
|
|
4460
|
-
const tail = fs24.existsSync(logFile(paths)) ? fs24.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
|
|
4461
|
-
throw new CliError(`The preview server did not start.
|
|
4462
|
-
${tail.join("\n")}`);
|
|
4463
|
-
}
|
|
4464
|
-
return state;
|
|
4823
|
+
async function runRollback(paths, value, revArg) {
|
|
4824
|
+
const canvas = canvasArg(value, "design rollback <canvas> <rev>");
|
|
4825
|
+
const rev = Number((revArg ?? "").replace(/^r/i, ""));
|
|
4826
|
+
if (!Number.isInteger(rev) || rev < 1)
|
|
4827
|
+
throw new CliError("Usage: design rollback <canvas> <rev> (see `design history <canvas>`)");
|
|
4828
|
+
const { link, client } = cloud(paths);
|
|
4829
|
+
const head = await client.rollback(link.project, canvasUnit(canvas), rev);
|
|
4830
|
+
print(`${green("\u2713")} ${bold(canvas)}: r${rev} is current again as r${head} in the cloud`);
|
|
4831
|
+
await runPull(paths, [canvas], { theirs: [], json: false });
|
|
4465
4832
|
}
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4833
|
+
var obj = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4834
|
+
var n = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
4835
|
+
async function runMe(paths, options) {
|
|
4836
|
+
const link = paths ? readLink(paths) : null;
|
|
4837
|
+
const host = link?.host ?? cloudHost();
|
|
4838
|
+
const credential = requireCredential(host);
|
|
4839
|
+
const client = new CloudClient(host, credential.token);
|
|
4840
|
+
const [me, usage, activity] = await Promise.all([
|
|
4841
|
+
client.request("GET", "/me").then(obj),
|
|
4842
|
+
client.request("GET", "/me/usage").then(obj),
|
|
4843
|
+
client.request("GET", "/me/activity").then(obj)
|
|
4844
|
+
]);
|
|
4845
|
+
const plan = obj(usage.plan);
|
|
4846
|
+
const limits = obj(usage.limits);
|
|
4847
|
+
const totals = obj(activity.totals);
|
|
4848
|
+
let linked = null;
|
|
4849
|
+
if (link) {
|
|
4850
|
+
const projects = await client.projects().catch(() => null);
|
|
4851
|
+
const project = projects && [...projects.owned, ...projects.shared].find((p) => p.id === link.project);
|
|
4852
|
+
linked = project ? { id: project.id, name: project.name, role: project.role } : { id: link.project, name: "", role: "" };
|
|
4478
4853
|
}
|
|
4479
|
-
const
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4854
|
+
const data = {
|
|
4855
|
+
host,
|
|
4856
|
+
email: me.email,
|
|
4857
|
+
name: me.name,
|
|
4858
|
+
memberSince: me.createdAt,
|
|
4859
|
+
plan: { id: plan.id, name: plan.name, expiresAt: plan.expiresAt ?? null },
|
|
4860
|
+
storage: { usedBytes: n(usage.storageBytes), limitBytes: n(limits.storage_bytes) },
|
|
4861
|
+
activeProjects: { used: n(usage.activeProjects), limit: n(limits.active_projects) },
|
|
4862
|
+
canvases: n(totals.canvases),
|
|
4863
|
+
screens: n(totals.screens),
|
|
4864
|
+
pushesLastYear: n(totals.pushesLastYear),
|
|
4865
|
+
pushesPerHour: n(limits.pushes_per_hour),
|
|
4866
|
+
linked
|
|
4867
|
+
};
|
|
4868
|
+
if (options.json) return print(JSON.stringify(data, null, 2));
|
|
4869
|
+
const label = (text2) => dim(text2.padEnd(10));
|
|
4870
|
+
const date = (iso) => typeof iso === "string" ? new Date(iso).toDateString().slice(4) : "";
|
|
4871
|
+
const share = data.storage.limitBytes ? Math.round(data.storage.usedBytes / data.storage.limitBytes * 100) : 0;
|
|
4872
|
+
print(
|
|
4873
|
+
`${bold(String(data.email ?? ""))}${data.name && data.name !== data.email ? ` ${dim(`(${data.name})`)}` : ""} ${dim(host)}`
|
|
4874
|
+
);
|
|
4875
|
+
print(
|
|
4876
|
+
`${label("Plan")}${bold(String(data.plan.name ?? "Free"))} ${dim(data.plan.expiresAt ? `until ${date(data.plan.expiresAt)}` : "no end date")}`
|
|
4877
|
+
);
|
|
4878
|
+
print(
|
|
4879
|
+
`${label("Storage")}${megabytes(data.storage.usedBytes)} of ${megabytes(data.storage.limitBytes)} ${(share >= 90 ? red : share >= 80 ? yellow : dim)(`(${share}%)`)}`
|
|
4880
|
+
);
|
|
4881
|
+
print(`${label("Projects")}${data.activeProjects.used} of ${data.activeProjects.limit} active`);
|
|
4882
|
+
print(
|
|
4883
|
+
`${label("Canvases")}${plural(data.canvases, "canvas", "canvases")} in your projects \xB7 ${plural(data.screens, "screen")}`
|
|
4884
|
+
);
|
|
4885
|
+
print(
|
|
4886
|
+
`${label("Pushes")}${plural(data.pushesLastYear, "push", "pushes")} in the last year ${dim(`\xB7 up to ${data.pushesPerHour} an hour`)}`
|
|
4887
|
+
);
|
|
4888
|
+
if (linked)
|
|
4889
|
+
print(
|
|
4890
|
+
`${label("Linked")}${linked.name || linked.id}${linked.role ? dim(` as ${linked.role}`) : ""} ${dim(webUrl(link))}`
|
|
4891
|
+
);
|
|
4892
|
+
else if (paths) print(`${label("Linked")}${dim("no \u2014 `design link`")}`);
|
|
4893
|
+
print(`${label("Since")}${date(data.memberSince)} ${dim(`\xB7 plans and upgrades: ${host.replace(/\/+$/, "")}/plans`)}`);
|
|
4485
4894
|
}
|
|
4486
4895
|
|
|
4487
4896
|
// src/cli/commands/check.ts
|
|
@@ -4636,9 +5045,9 @@ function printLinkIssues(issues, project) {
|
|
|
4636
5045
|
}
|
|
4637
5046
|
|
|
4638
5047
|
// src/cli/commands/cloud.ts
|
|
4639
|
-
import
|
|
5048
|
+
import fs29 from "fs";
|
|
4640
5049
|
import os3 from "os";
|
|
4641
|
-
import
|
|
5050
|
+
import path26 from "path";
|
|
4642
5051
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4643
5052
|
async function runLogin() {
|
|
4644
5053
|
const host = cloudHost();
|
|
@@ -4739,12 +5148,14 @@ async function runLink(paths, wanted, options) {
|
|
|
4739
5148
|
print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
|
|
4740
5149
|
} else {
|
|
4741
5150
|
writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
|
|
4742
|
-
|
|
5151
|
+
fs29.rmSync(path26.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
|
|
4743
5152
|
const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
|
|
4744
5153
|
print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
|
|
4745
5154
|
}
|
|
4746
5155
|
if (project.archived) print(yellow("The project is archived: unarchive it in the web app before pushing or pulling."));
|
|
4747
5156
|
const link = readLink(paths);
|
|
5157
|
+
for (const line of await syncProjectMeta(paths, client, link)) print(dim(line));
|
|
5158
|
+
writeLink(paths, link);
|
|
4748
5159
|
const remote = project.archived ? null : await client.units(project.id);
|
|
4749
5160
|
const scan = scanLocal(paths);
|
|
4750
5161
|
const states = allUnitKeys(link, scan.units, remote).map(
|
|
@@ -4764,40 +5175,63 @@ async function runLink(paths, wanted, options) {
|
|
|
4764
5175
|
}
|
|
4765
5176
|
|
|
4766
5177
|
// src/cli/commands/icon.ts
|
|
4767
|
-
import
|
|
4768
|
-
import
|
|
4769
|
-
function
|
|
5178
|
+
import fs30 from "fs";
|
|
5179
|
+
import path27 from "path";
|
|
5180
|
+
async function toCloud(paths, file) {
|
|
5181
|
+
const link = readLink(paths);
|
|
5182
|
+
if (!link) return;
|
|
5183
|
+
const credential = getCredential(link.host);
|
|
5184
|
+
if (!credential) {
|
|
5185
|
+
print(yellow("Not signed in: the icon goes to the cloud on the next `design push` after `design login`."));
|
|
5186
|
+
return;
|
|
5187
|
+
}
|
|
5188
|
+
const client = new CloudClient(link.host, credential.token);
|
|
5189
|
+
try {
|
|
5190
|
+
const ext = file ? path27.extname(file).slice(1) : null;
|
|
5191
|
+
await client.setProjectIcon(
|
|
5192
|
+
link.project,
|
|
5193
|
+
file && ext ? { data: fs30.readFileSync(file), type: ICON_TYPES[ext] } : null
|
|
5194
|
+
);
|
|
5195
|
+
link.icon = file ? iconHash(file) : null;
|
|
5196
|
+
writeLink(paths, link);
|
|
5197
|
+
print(dim(`The cloud project has it too.`));
|
|
5198
|
+
} catch (error) {
|
|
5199
|
+
print(yellow(`Set here only: ${error.message}`));
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
async function runIcon(paths, file, options) {
|
|
4770
5203
|
if (options.remove) {
|
|
4771
5204
|
const had = iconFile(paths);
|
|
4772
5205
|
writeIcon(paths, null);
|
|
4773
|
-
print(had ? `${green("Removed")} ${
|
|
5206
|
+
print(had ? `${green("Removed")} ${path27.relative(process.cwd(), had)}` : "The project has no icon.");
|
|
5207
|
+
await toCloud(paths, null);
|
|
4774
5208
|
return;
|
|
4775
5209
|
}
|
|
4776
5210
|
if (!file) {
|
|
4777
5211
|
const current = iconFile(paths);
|
|
4778
5212
|
if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
|
|
4779
|
-
return print(`${
|
|
5213
|
+
return print(`${path27.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs30.statSync(current).size / 1024)} KB`)}`);
|
|
4780
5214
|
}
|
|
4781
5215
|
let data;
|
|
4782
5216
|
try {
|
|
4783
|
-
data =
|
|
5217
|
+
data = fs30.readFileSync(file);
|
|
4784
5218
|
} catch {
|
|
4785
5219
|
throw new CliError(`No file ${file}`);
|
|
4786
5220
|
}
|
|
4787
|
-
const problem = iconProblem(data,
|
|
5221
|
+
const problem = iconProblem(data, path27.extname(file).slice(1).toLowerCase() || void 0);
|
|
4788
5222
|
if (problem) throw new CliError(`${file}: ${problem}`);
|
|
4789
5223
|
const written = writeIcon(paths, data);
|
|
4790
|
-
print(`${green("Set")} the project icon: ${
|
|
4791
|
-
|
|
5224
|
+
print(`${green("Set")} the project icon: ${path27.relative(process.cwd(), written)}`);
|
|
5225
|
+
await toCloud(paths, written);
|
|
4792
5226
|
}
|
|
4793
5227
|
|
|
4794
5228
|
// src/cli/commands/import.ts
|
|
4795
|
-
import
|
|
4796
|
-
import
|
|
5229
|
+
import fs32 from "fs";
|
|
5230
|
+
import path29 from "path";
|
|
4797
5231
|
|
|
4798
5232
|
// src/import/claude-design.ts
|
|
4799
|
-
import
|
|
4800
|
-
import
|
|
5233
|
+
import fs31 from "fs";
|
|
5234
|
+
import path28 from "path";
|
|
4801
5235
|
|
|
4802
5236
|
// src/import/markup.ts
|
|
4803
5237
|
var VOID = /* @__PURE__ */ new Set([
|
|
@@ -5405,11 +5839,11 @@ function boardId(file, used) {
|
|
|
5405
5839
|
return id2;
|
|
5406
5840
|
}
|
|
5407
5841
|
function importClaudeDesign(dir, options = {}) {
|
|
5408
|
-
const canvasFile =
|
|
5409
|
-
if (!
|
|
5842
|
+
const canvasFile = path28.join(dir, "canvas.json");
|
|
5843
|
+
if (!fs31.existsSync(canvasFile))
|
|
5410
5844
|
throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
|
|
5411
|
-
const source = JSON.parse(
|
|
5412
|
-
const templates =
|
|
5845
|
+
const source = JSON.parse(fs31.readFileSync(canvasFile, "utf8"));
|
|
5846
|
+
const templates = fs31.readdirSync(dir).filter((file) => file.endsWith(EXT));
|
|
5413
5847
|
if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
|
|
5414
5848
|
const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
|
|
5415
5849
|
const warnings = /* @__PURE__ */ new Map();
|
|
@@ -5436,7 +5870,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5436
5870
|
try {
|
|
5437
5871
|
files.set(
|
|
5438
5872
|
`${name}.jsx`,
|
|
5439
|
-
convertFile(name,
|
|
5873
|
+
convertFile(name, fs31.readFileSync(path28.join(dir, file), "utf8"), { known, routes, warn: warn2 })
|
|
5440
5874
|
);
|
|
5441
5875
|
} catch (error) {
|
|
5442
5876
|
warn2(`could not be converted: ${error.message}`);
|
|
@@ -5484,7 +5918,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5484
5918
|
screens++;
|
|
5485
5919
|
}
|
|
5486
5920
|
const canvas = {
|
|
5487
|
-
title: options.title ?? source.title ??
|
|
5921
|
+
title: options.title ?? source.title ?? path28.basename(path28.resolve(dir)),
|
|
5488
5922
|
description: "Imported from Claude Design.",
|
|
5489
5923
|
// The templates bring their own styles; the design system's reset would restyle them.
|
|
5490
5924
|
system: false,
|
|
@@ -5496,21 +5930,21 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5496
5930
|
// src/cli/commands/import.ts
|
|
5497
5931
|
var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
|
|
5498
5932
|
function runImport(paths, from, options) {
|
|
5499
|
-
const dir =
|
|
5500
|
-
if (!
|
|
5933
|
+
const dir = path29.resolve(from);
|
|
5934
|
+
if (!fs32.existsSync(dir) || !fs32.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
|
|
5501
5935
|
let result;
|
|
5502
5936
|
try {
|
|
5503
5937
|
result = importClaudeDesign(dir, { title: options.title });
|
|
5504
5938
|
} catch (error) {
|
|
5505
5939
|
throw new CliError(error.message);
|
|
5506
5940
|
}
|
|
5507
|
-
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(
|
|
5941
|
+
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path29.basename(dir)) || "imported");
|
|
5508
5942
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
|
|
5509
|
-
const out =
|
|
5510
|
-
if (
|
|
5511
|
-
|
|
5512
|
-
for (const [name, content] of result.files)
|
|
5513
|
-
|
|
5943
|
+
const out = path29.join(paths.canvases, id2);
|
|
5944
|
+
if (fs32.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
|
|
5945
|
+
fs32.mkdirSync(path29.join(out, "screens"), { recursive: true });
|
|
5946
|
+
for (const [name, content] of result.files) fs32.writeFileSync(path29.join(out, "screens", name), content);
|
|
5947
|
+
fs32.writeFileSync(path29.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
|
|
5514
5948
|
`);
|
|
5515
5949
|
print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
|
|
5516
5950
|
print(
|
|
@@ -5528,9 +5962,166 @@ function runImport(paths, from, options) {
|
|
|
5528
5962
|
print(`Next: \`design check ${id2} --render\`, then \`design preview --open /c/${id2}\`.`);
|
|
5529
5963
|
}
|
|
5530
5964
|
|
|
5965
|
+
// src/cli/commands/merge.ts
|
|
5966
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5967
|
+
import path30 from "path";
|
|
5968
|
+
import { createInterface } from "readline/promises";
|
|
5969
|
+
var KIND = {
|
|
5970
|
+
text: "both sides changed lines, the markers show them",
|
|
5971
|
+
binary: "both sides changed it; yours is in place",
|
|
5972
|
+
deleted_here: "deleted here, changed in the cloud; the cloud version is in place",
|
|
5973
|
+
deleted_in_cloud: "deleted in the cloud, changed here; yours is in place"
|
|
5974
|
+
};
|
|
5975
|
+
function matchTargets(paths, states, targets) {
|
|
5976
|
+
const picked = [];
|
|
5977
|
+
for (const target of targets) {
|
|
5978
|
+
const unit = states.find((state) => state.unit === unitArg(target));
|
|
5979
|
+
if (unit) {
|
|
5980
|
+
for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
|
|
5981
|
+
continue;
|
|
5982
|
+
}
|
|
5983
|
+
const rel = [path30.relative(paths.design, path30.resolve(target)), target.replace(/^\.design\//, "")];
|
|
5984
|
+
const found = states.flatMap(
|
|
5985
|
+
(state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
|
|
5986
|
+
);
|
|
5987
|
+
if (!found.length) throw new CliError(`No open conflict in ${target}. \`design merge\` lists them.`);
|
|
5988
|
+
picked.push(...found);
|
|
5989
|
+
}
|
|
5990
|
+
return picked;
|
|
5991
|
+
}
|
|
5992
|
+
function printOpen(paths, states) {
|
|
5993
|
+
for (const state of states) {
|
|
5994
|
+
const open2 = openConflicts(paths, state);
|
|
5995
|
+
if (!open2.length) continue;
|
|
5996
|
+
print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
|
|
5997
|
+
for (const conflict of open2) {
|
|
5998
|
+
print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
|
|
5999
|
+
const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path30.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
|
|
6000
|
+
print(` ${dim(sides)}`);
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
async function runMerge(paths, targets, options) {
|
|
6005
|
+
if (options.here && options.cloud) throw new CliError("Pick one side: --here or --cloud");
|
|
6006
|
+
const link = requireLink(paths);
|
|
6007
|
+
const states = mergingUnits(paths);
|
|
6008
|
+
const close = (accept, only) => {
|
|
6009
|
+
const closed = [];
|
|
6010
|
+
for (const state of states) {
|
|
6011
|
+
if (only && !only.includes(state.unit)) continue;
|
|
6012
|
+
if (!finishMerge(paths, link, state.unit, accept).length) closed.push(state.unit);
|
|
6013
|
+
}
|
|
6014
|
+
writeLink(paths, link);
|
|
6015
|
+
for (const unit of closed) print(`${green("\u2713")} ${unit} ${dim("merged; `design push` takes it up")}`);
|
|
6016
|
+
return closed;
|
|
6017
|
+
};
|
|
6018
|
+
if (options.here || options.cloud) {
|
|
6019
|
+
const side = options.here ? "here" : "cloud";
|
|
6020
|
+
const picked = targets.length ? matchTargets(paths, states, targets) : states.flatMap((state) => openConflicts(paths, state).map((conflict) => ({ state, conflict })));
|
|
6021
|
+
for (const { state, conflict } of picked) {
|
|
6022
|
+
resolveConflict(paths, state, conflict.path, side);
|
|
6023
|
+
print(`${green("\u2713")} ${conflict.path} ${dim(`(${side === "here" ? "yours" : "the cloud's"})`)}`);
|
|
6024
|
+
}
|
|
6025
|
+
close(false);
|
|
6026
|
+
return;
|
|
6027
|
+
}
|
|
6028
|
+
if (options.done) {
|
|
6029
|
+
const only = targets.length ? targets.map(unitArg) : void 0;
|
|
6030
|
+
close(true, only);
|
|
6031
|
+
const left2 = states.filter(
|
|
6032
|
+
(state) => (!only || only.includes(state.unit)) && openConflicts(paths, state).some((c) => c.kind === "text")
|
|
6033
|
+
);
|
|
6034
|
+
if (left2.length) {
|
|
6035
|
+
printOpen(paths, left2);
|
|
6036
|
+
throw new CliError(
|
|
6037
|
+
"Conflict markers are left in the files above; edit them out, or settle with --here or --cloud."
|
|
6038
|
+
);
|
|
6039
|
+
}
|
|
6040
|
+
return;
|
|
6041
|
+
}
|
|
6042
|
+
const open2 = states.filter((state) => openConflicts(paths, state).length);
|
|
6043
|
+
if (options.json) {
|
|
6044
|
+
print(
|
|
6045
|
+
JSON.stringify({ units: open2.map((state) => ({ ...state, conflicts: openConflicts(paths, state) })) }, null, 2)
|
|
6046
|
+
);
|
|
6047
|
+
if (open2.length) process.exitCode = 2;
|
|
6048
|
+
return;
|
|
6049
|
+
}
|
|
6050
|
+
if (!open2.length) {
|
|
6051
|
+
close(false);
|
|
6052
|
+
if (!states.length) print("Nothing to merge.");
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
6056
|
+
printOpen(paths, open2);
|
|
6057
|
+
print(
|
|
6058
|
+
dim(
|
|
6059
|
+
"Settle with `design merge <file|unit> --here` or `--cloud`, or edit the markers out; then `design merge --done`."
|
|
6060
|
+
)
|
|
6061
|
+
);
|
|
6062
|
+
process.exitCode = 2;
|
|
6063
|
+
return;
|
|
6064
|
+
}
|
|
6065
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6066
|
+
try {
|
|
6067
|
+
for (const state of open2) {
|
|
6068
|
+
for (const conflict of openConflicts(paths, state)) {
|
|
6069
|
+
print(`${bold(state.unit)} ${conflict.path} ${dim(`\u2014 ${KIND[conflict.kind]}`)}`);
|
|
6070
|
+
for (; ; ) {
|
|
6071
|
+
const edit = conflict.kind === "text" ? ", [e]dit" : "";
|
|
6072
|
+
const answer = (await rl.question(` keep [h]ere, take [c]loud${edit}, [s]kip? `)).trim().toLowerCase();
|
|
6073
|
+
if (answer === "h" || answer === "c") {
|
|
6074
|
+
resolveConflict(paths, state, conflict.path, answer === "h" ? "here" : "cloud");
|
|
6075
|
+
break;
|
|
6076
|
+
}
|
|
6077
|
+
if (answer === "s") break;
|
|
6078
|
+
if (answer === "e" && edit) {
|
|
6079
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
6080
|
+
spawnSync2(editor, [path30.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
|
|
6081
|
+
if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
|
|
6082
|
+
print(yellow(" Conflict markers are still there."));
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
6087
|
+
} finally {
|
|
6088
|
+
rl.close();
|
|
6089
|
+
}
|
|
6090
|
+
close(false);
|
|
6091
|
+
const left = mergingUnits(paths).filter((state) => openConflicts(paths, state).length);
|
|
6092
|
+
if (left.length) {
|
|
6093
|
+
printOpen(paths, left);
|
|
6094
|
+
process.exitCode = 2;
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
|
|
6098
|
+
// src/cli/commands/rename.ts
|
|
6099
|
+
import fs33 from "fs";
|
|
6100
|
+
async function runRename(paths, name) {
|
|
6101
|
+
const trimmed = name.trim();
|
|
6102
|
+
if (!trimmed) throw new CliError("Usage: design rename <name>");
|
|
6103
|
+
const link = readLink(paths);
|
|
6104
|
+
if (link) {
|
|
6105
|
+
const client = new CloudClient(link.host, requireCredential(link.host).token);
|
|
6106
|
+
const project = await client.renameProject(link.project, trimmed);
|
|
6107
|
+
link.name = project.name;
|
|
6108
|
+
writeLink(paths, link);
|
|
6109
|
+
print(`${green("Renamed")} the cloud project to ${project.name}`);
|
|
6110
|
+
return;
|
|
6111
|
+
}
|
|
6112
|
+
let config = {};
|
|
6113
|
+
try {
|
|
6114
|
+
config = JSON.parse(fs33.readFileSync(paths.config, "utf8"));
|
|
6115
|
+
} catch {
|
|
6116
|
+
}
|
|
6117
|
+
fs33.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
|
|
6118
|
+
`);
|
|
6119
|
+
print(`${green("Renamed")} the project to ${trimmed} (.design/design.json)`);
|
|
6120
|
+
}
|
|
6121
|
+
|
|
5531
6122
|
// src/cli/commands/scaffold.ts
|
|
5532
|
-
import
|
|
5533
|
-
import
|
|
6123
|
+
import fs34 from "fs";
|
|
6124
|
+
import path31 from "path";
|
|
5534
6125
|
|
|
5535
6126
|
// src/cli/templates.ts
|
|
5536
6127
|
var TOKENS_CSS = `/*
|
|
@@ -5741,40 +6332,40 @@ function mainScreen(canvasId) {
|
|
|
5741
6332
|
|
|
5742
6333
|
// src/cli/commands/scaffold.ts
|
|
5743
6334
|
function write(file, content, root) {
|
|
5744
|
-
if (
|
|
5745
|
-
print(` ${dim("exists ")} ${
|
|
6335
|
+
if (fs34.existsSync(file)) {
|
|
6336
|
+
print(` ${dim("exists ")} ${path31.relative(root, file)}`);
|
|
5746
6337
|
return false;
|
|
5747
6338
|
}
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
print(` ${green("created")} ${
|
|
6339
|
+
fs34.mkdirSync(path31.dirname(file), { recursive: true });
|
|
6340
|
+
fs34.writeFileSync(file, content);
|
|
6341
|
+
print(` ${green("created")} ${path31.relative(root, file)}`);
|
|
5751
6342
|
return true;
|
|
5752
6343
|
}
|
|
5753
6344
|
function ensureGitignored(root) {
|
|
5754
|
-
const file =
|
|
5755
|
-
const current =
|
|
6345
|
+
const file = path31.join(root, ".gitignore");
|
|
6346
|
+
const current = fs34.existsSync(file) ? fs34.readFileSync(file, "utf8") : "";
|
|
5756
6347
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
|
|
5757
6348
|
if (covered) return "present";
|
|
5758
6349
|
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
5759
|
-
|
|
6350
|
+
fs34.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
|
|
5760
6351
|
.design/
|
|
5761
6352
|
`);
|
|
5762
6353
|
return "added";
|
|
5763
6354
|
}
|
|
5764
6355
|
function projectName(root) {
|
|
5765
6356
|
try {
|
|
5766
|
-
const manifest = JSON.parse(
|
|
6357
|
+
const manifest = JSON.parse(fs34.readFileSync(path31.join(root, "package.json"), "utf8"));
|
|
5767
6358
|
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
5768
6359
|
} catch {
|
|
5769
6360
|
}
|
|
5770
|
-
return
|
|
6361
|
+
return path31.basename(root);
|
|
5771
6362
|
}
|
|
5772
6363
|
function runInit(root, options) {
|
|
5773
6364
|
const paths = designPaths(root);
|
|
5774
6365
|
print(`Setting up ${DESIGN_DIR} in ${root}`);
|
|
5775
6366
|
write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
|
|
5776
6367
|
`, root);
|
|
5777
|
-
|
|
6368
|
+
fs34.mkdirSync(paths.canvases, { recursive: true });
|
|
5778
6369
|
if (options.gitignore) {
|
|
5779
6370
|
const result = ensureGitignored(root);
|
|
5780
6371
|
print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
|
|
@@ -5788,44 +6379,44 @@ function runSystemInit(root, options) {
|
|
|
5788
6379
|
const paths = designPaths(root);
|
|
5789
6380
|
let base = projectName(root);
|
|
5790
6381
|
try {
|
|
5791
|
-
base = JSON.parse(
|
|
6382
|
+
base = JSON.parse(fs34.readFileSync(paths.config, "utf8")).name || base;
|
|
5792
6383
|
} catch {
|
|
5793
6384
|
}
|
|
5794
6385
|
const name = options.name ?? `${base} design system`;
|
|
5795
|
-
print(`Scaffolding the design system in ${
|
|
5796
|
-
write(
|
|
6386
|
+
print(`Scaffolding the design system in ${path31.relative(root, paths.system)}`);
|
|
6387
|
+
write(path31.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
|
|
5797
6388
|
`, root);
|
|
5798
|
-
write(
|
|
6389
|
+
write(path31.join(paths.system, "tokens.css"), TOKENS_CSS, root);
|
|
5799
6390
|
if (!options.empty) {
|
|
5800
|
-
write(
|
|
5801
|
-
write(
|
|
5802
|
-
write(
|
|
6391
|
+
write(path31.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
|
|
6392
|
+
write(path31.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
|
|
6393
|
+
write(path31.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
|
|
5803
6394
|
}
|
|
5804
6395
|
}
|
|
5805
6396
|
function runNew(root, id2, options) {
|
|
5806
6397
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
|
|
5807
6398
|
const paths = designPaths(root);
|
|
5808
|
-
const dir =
|
|
5809
|
-
if (
|
|
6399
|
+
const dir = path31.join(paths.canvases, id2);
|
|
6400
|
+
if (fs34.existsSync(path31.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
|
|
5810
6401
|
print(`Creating canvas "${id2}"`);
|
|
5811
6402
|
const title = options.title ?? titleize(id2);
|
|
5812
6403
|
if (options.empty) {
|
|
5813
6404
|
write(
|
|
5814
|
-
|
|
6405
|
+
path31.join(dir, "canvas.json"),
|
|
5815
6406
|
`${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
|
|
5816
6407
|
`,
|
|
5817
6408
|
root
|
|
5818
6409
|
);
|
|
5819
|
-
|
|
6410
|
+
fs34.mkdirSync(path31.join(dir, "screens"), { recursive: true });
|
|
5820
6411
|
} else {
|
|
5821
|
-
write(
|
|
5822
|
-
write(
|
|
6412
|
+
write(path31.join(dir, "canvas.json"), canvasJson(title), root);
|
|
6413
|
+
write(path31.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
|
|
5823
6414
|
}
|
|
5824
6415
|
}
|
|
5825
6416
|
|
|
5826
6417
|
// src/cli/commands/shot.ts
|
|
5827
|
-
import
|
|
5828
|
-
import
|
|
6418
|
+
import fs35 from "fs";
|
|
6419
|
+
import path32 from "path";
|
|
5829
6420
|
var OVERVIEW_TIMEOUT = 9e4;
|
|
5830
6421
|
async function runShot(paths, target, options) {
|
|
5831
6422
|
const [canvasId = "", screenId] = target.split("/");
|
|
@@ -5841,15 +6432,15 @@ async function runShot(paths, target, options) {
|
|
|
5841
6432
|
throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
5842
6433
|
}
|
|
5843
6434
|
const theme2 = options.theme ?? doc.theme ?? "light";
|
|
5844
|
-
const outBase =
|
|
5845
|
-
const outDir =
|
|
5846
|
-
|
|
6435
|
+
const outBase = path32.resolve(options.out ?? paths.shots);
|
|
6436
|
+
const outDir = path32.join(outBase, canvasId);
|
|
6437
|
+
fs35.mkdirSync(outDir, { recursive: true });
|
|
5847
6438
|
const base = await serverBase(paths);
|
|
5848
6439
|
const browser = await launchChrome();
|
|
5849
6440
|
let failed = false;
|
|
5850
6441
|
try {
|
|
5851
6442
|
if (options.overview) {
|
|
5852
|
-
if (!
|
|
6443
|
+
if (!fs35.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
|
|
5853
6444
|
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);
|
|
5854
6445
|
for (const pageId of pages) {
|
|
5855
6446
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
|
@@ -5865,7 +6456,7 @@ async function runShot(paths, target, options) {
|
|
|
5865
6456
|
warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
|
|
5866
6457
|
}
|
|
5867
6458
|
await page.waitForTimeout(400);
|
|
5868
|
-
const file =
|
|
6459
|
+
const file = path32.join(outDir, `${pageId}.overview.png`);
|
|
5869
6460
|
await page.screenshot({ path: file });
|
|
5870
6461
|
await page.close();
|
|
5871
6462
|
print(file);
|
|
@@ -5894,7 +6485,7 @@ async function shootFrames(browser, jobs, outDir) {
|
|
|
5894
6485
|
let failed = false;
|
|
5895
6486
|
const results = await mapLimit(jobs, 4, async (job) => {
|
|
5896
6487
|
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
5897
|
-
const file =
|
|
6488
|
+
const file = path32.join(outDir, `${job.id}.${job.theme}.png`);
|
|
5898
6489
|
try {
|
|
5899
6490
|
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
5900
6491
|
await page.screenshot({ path: file, fullPage: job.fullPage });
|
|
@@ -5926,8 +6517,8 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
5926
6517
|
if (id2 && !all.some((job) => job.id === id2)) {
|
|
5927
6518
|
throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
|
|
5928
6519
|
}
|
|
5929
|
-
const outDir =
|
|
5930
|
-
|
|
6520
|
+
const outDir = path32.join(path32.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
|
|
6521
|
+
fs35.mkdirSync(outDir, { recursive: true });
|
|
5931
6522
|
const base = await serverBase(paths);
|
|
5932
6523
|
const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
|
|
5933
6524
|
const browser = await launchChrome();
|
|
@@ -5939,7 +6530,7 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
5939
6530
|
}
|
|
5940
6531
|
|
|
5941
6532
|
// src/cli/commands/cloud-status.ts
|
|
5942
|
-
import
|
|
6533
|
+
import path33 from "path";
|
|
5943
6534
|
var STATE_TEXT = {
|
|
5944
6535
|
in_sync: green,
|
|
5945
6536
|
local_changes: yellow,
|
|
@@ -5962,7 +6553,7 @@ function detail(paths, status) {
|
|
|
5962
6553
|
if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
|
|
5963
6554
|
if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
|
|
5964
6555
|
parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
|
|
5965
|
-
if (status.conflictRev !== void 0) parts.push(`merge ${
|
|
6556
|
+
if (status.conflictRev !== void 0) parts.push(`merge ${path33.relative(paths.root, incomingDir(paths, status.key))}`);
|
|
5966
6557
|
return parts.join(" \xB7 ");
|
|
5967
6558
|
}
|
|
5968
6559
|
async function printCloudStatus(paths, label) {
|
|
@@ -6078,8 +6669,10 @@ ${bold("Project")}
|
|
|
6078
6669
|
system init [--name <name>] [--empty] Scaffold .design/system: tokens, a guideline, a component
|
|
6079
6670
|
new <canvas> [--title <title>] [--empty] Scaffold .design/canvas/<canvas>
|
|
6080
6671
|
icon [<file>] [--remove] Show, set or remove the project icon: an SVG, PNG or WebP of
|
|
6081
|
-
at most 256 KB
|
|
6082
|
-
|
|
6672
|
+
at most 256 KB in .design/icon.*; a linked project's icon is
|
|
6673
|
+
the cloud project's, set there at once
|
|
6674
|
+
rename <name> Rename the project: the cloud project when linked (owner),
|
|
6675
|
+
otherwise the name in design.json
|
|
6083
6676
|
import <folder> [--canvas <id>] Turn an exported Claude Design project (canvas.json and
|
|
6084
6677
|
[--title <title>] *.dc.html) into a canvas of React screens, once
|
|
6085
6678
|
canvases [--json] Every canvas: pages and screens; with a cloud link also its
|
|
@@ -6115,13 +6708,19 @@ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>;
|
|
|
6115
6708
|
link [<project>] [--new <name>] No args: list your projects. Link .design to a cloud project
|
|
6116
6709
|
(linking to another project replaces the link)
|
|
6117
6710
|
push [canvas\u2026] [--json] Build and upload what changed here (or the named units); a
|
|
6118
|
-
[--resolved <unit>] system change pushes every active canvas with it.
|
|
6119
|
-
|
|
6711
|
+
[--resolved <unit>] system change pushes every active canvas with it. When the
|
|
6712
|
+
cloud is ahead it pulls and merges first, then pushes; it
|
|
6713
|
+
stops on conflicts. Refreshes each canvas's cover snapshot.
|
|
6120
6714
|
Prints the web link of every pushed canvas; refuses screens
|
|
6121
6715
|
that still call go()
|
|
6122
6716
|
pull [canvas\u2026] [--json] Take cloud changes into .design; a unit changed on both sides
|
|
6123
|
-
[--theirs <unit>]
|
|
6124
|
-
|
|
6717
|
+
[--theirs <unit>] is merged three ways (files changed on one side take it,
|
|
6718
|
+
text changed on both merges line by line); what does not
|
|
6719
|
+
merge waits for design merge. --theirs takes the cloud
|
|
6720
|
+
version and drops local changes
|
|
6721
|
+
merge [<unit|file>\u2026] [--here|--cloud] Conflicts a merge left: lists them (in a terminal, walks
|
|
6722
|
+
[--done] [--json] through them); --here / --cloud settles files or units;
|
|
6723
|
+
--done closes a merge once no markers are left
|
|
6125
6724
|
url <canvas> [--json] The canvas in the web app, and its public link if published
|
|
6126
6725
|
history <canvas|system> [--json] Stored revisions, newest first: when, who, screens, size
|
|
6127
6726
|
rollback <canvas> <rev> Make an old revision current again in the cloud (a new
|
|
@@ -6141,7 +6740,7 @@ ${bold("Environment")}
|
|
|
6141
6740
|
FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
|
|
6142
6741
|
`;
|
|
6143
6742
|
function projectPaths(rootOption, requireDesign = true) {
|
|
6144
|
-
const found = rootOption ?
|
|
6743
|
+
const found = rootOption ? path34.resolve(rootOption) : findProjectRoot();
|
|
6145
6744
|
if (!found) {
|
|
6146
6745
|
if (!requireDesign) return designPaths(process.cwd());
|
|
6147
6746
|
throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
|
|
@@ -6160,6 +6759,9 @@ async function main(argv) {
|
|
|
6160
6759
|
title: { type: "string" },
|
|
6161
6760
|
canvas: { type: "string" },
|
|
6162
6761
|
remove: { type: "boolean" },
|
|
6762
|
+
here: { type: "boolean" },
|
|
6763
|
+
cloud: { type: "boolean" },
|
|
6764
|
+
done: { type: "boolean" },
|
|
6163
6765
|
"no-gitignore": { type: "boolean" },
|
|
6164
6766
|
empty: { type: "boolean" },
|
|
6165
6767
|
open: { type: "boolean" },
|
|
@@ -6189,7 +6791,7 @@ async function main(argv) {
|
|
|
6189
6791
|
if (values.theme && !theme2) throw new CliError("--theme is light or dark");
|
|
6190
6792
|
switch (command) {
|
|
6191
6793
|
case "init":
|
|
6192
|
-
return runInit(values.root ?
|
|
6794
|
+
return runInit(values.root ? path34.resolve(values.root) : process.cwd(), {
|
|
6193
6795
|
name: values.name,
|
|
6194
6796
|
gitignore: !values["no-gitignore"]
|
|
6195
6797
|
});
|
|
@@ -6203,6 +6805,16 @@ async function main(argv) {
|
|
|
6203
6805
|
}
|
|
6204
6806
|
case "icon":
|
|
6205
6807
|
return runIcon(projectPaths(values.root), rest[0], { remove: !!values.remove });
|
|
6808
|
+
case "rename":
|
|
6809
|
+
if (!rest.length) throw new CliError("Usage: design rename <name>");
|
|
6810
|
+
return runRename(projectPaths(values.root), rest.join(" "));
|
|
6811
|
+
case "merge":
|
|
6812
|
+
return runMerge(projectPaths(values.root), rest, {
|
|
6813
|
+
here: !!values.here,
|
|
6814
|
+
cloud: !!values.cloud,
|
|
6815
|
+
done: !!values.done,
|
|
6816
|
+
json: !!values.json
|
|
6817
|
+
});
|
|
6206
6818
|
case "import": {
|
|
6207
6819
|
if (!rest[0]) throw new CliError("Usage: design import <folder> [--canvas <id>] [--title <title>]");
|
|
6208
6820
|
return runImport(projectPaths(values.root), rest[0], { canvas: values.canvas, title: values.title });
|