faux-studio 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +180 -38
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -10447,6 +10447,7 @@ function error(message) {
|
|
|
10447
10447
|
|
|
10448
10448
|
// src/credential-store.ts
|
|
10449
10449
|
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
10450
|
+
import { execFile } from "child_process";
|
|
10450
10451
|
import { join } from "path";
|
|
10451
10452
|
import { homedir } from "os";
|
|
10452
10453
|
var FAUX_DIR = join(homedir(), ".faux");
|
|
@@ -10480,17 +10481,29 @@ var FileCredentialStore = class {
|
|
|
10480
10481
|
}
|
|
10481
10482
|
}
|
|
10482
10483
|
};
|
|
10483
|
-
|
|
10484
|
+
function securityExec(...args) {
|
|
10485
|
+
return new Promise((resolve, reject) => {
|
|
10486
|
+
execFile("/usr/bin/security", args, { timeout: 1e4 }, (err, stdout, stderr) => {
|
|
10487
|
+
if (err) return reject(err);
|
|
10488
|
+
resolve({ stdout, stderr });
|
|
10489
|
+
});
|
|
10490
|
+
});
|
|
10491
|
+
}
|
|
10492
|
+
var SecurityCliKeychainStore = class {
|
|
10484
10493
|
name = "keychain";
|
|
10485
|
-
entry;
|
|
10486
10494
|
cached = void 0;
|
|
10487
|
-
constructor(entry) {
|
|
10488
|
-
this.entry = entry;
|
|
10489
|
-
}
|
|
10490
10495
|
async load() {
|
|
10491
10496
|
if (this.cached !== void 0) return this.cached;
|
|
10492
10497
|
try {
|
|
10493
|
-
const
|
|
10498
|
+
const { stdout } = await securityExec(
|
|
10499
|
+
"find-generic-password",
|
|
10500
|
+
"-s",
|
|
10501
|
+
KEYCHAIN_SERVICE,
|
|
10502
|
+
"-a",
|
|
10503
|
+
KEYCHAIN_ACCOUNT,
|
|
10504
|
+
"-w"
|
|
10505
|
+
);
|
|
10506
|
+
const raw = stdout.trim();
|
|
10494
10507
|
if (!raw) {
|
|
10495
10508
|
this.cached = null;
|
|
10496
10509
|
return null;
|
|
@@ -10508,8 +10521,29 @@ var KeychainCredentialStore = class {
|
|
|
10508
10521
|
}
|
|
10509
10522
|
}
|
|
10510
10523
|
async save(creds) {
|
|
10524
|
+
const value = JSON.stringify(creds);
|
|
10525
|
+
try {
|
|
10526
|
+
await securityExec(
|
|
10527
|
+
"delete-generic-password",
|
|
10528
|
+
"-s",
|
|
10529
|
+
KEYCHAIN_SERVICE,
|
|
10530
|
+
"-a",
|
|
10531
|
+
KEYCHAIN_ACCOUNT
|
|
10532
|
+
);
|
|
10533
|
+
} catch {
|
|
10534
|
+
}
|
|
10511
10535
|
try {
|
|
10512
|
-
await
|
|
10536
|
+
await securityExec(
|
|
10537
|
+
"add-generic-password",
|
|
10538
|
+
"-s",
|
|
10539
|
+
KEYCHAIN_SERVICE,
|
|
10540
|
+
"-a",
|
|
10541
|
+
KEYCHAIN_ACCOUNT,
|
|
10542
|
+
"-w",
|
|
10543
|
+
value,
|
|
10544
|
+
"-A",
|
|
10545
|
+
"-U"
|
|
10546
|
+
);
|
|
10513
10547
|
this.cached = creds;
|
|
10514
10548
|
} catch (err) {
|
|
10515
10549
|
warn(`Could not save to keychain: ${err instanceof Error ? err.message : err}`);
|
|
@@ -10517,18 +10551,29 @@ var KeychainCredentialStore = class {
|
|
|
10517
10551
|
}
|
|
10518
10552
|
async clear() {
|
|
10519
10553
|
try {
|
|
10520
|
-
await
|
|
10554
|
+
await securityExec(
|
|
10555
|
+
"delete-generic-password",
|
|
10556
|
+
"-s",
|
|
10557
|
+
KEYCHAIN_SERVICE,
|
|
10558
|
+
"-a",
|
|
10559
|
+
KEYCHAIN_ACCOUNT
|
|
10560
|
+
);
|
|
10521
10561
|
this.cached = null;
|
|
10522
10562
|
} catch {
|
|
10523
10563
|
}
|
|
10524
10564
|
}
|
|
10525
10565
|
};
|
|
10526
10566
|
async function tryCreateKeychainStore() {
|
|
10567
|
+
if (process.platform !== "darwin") {
|
|
10568
|
+
log("[keychain] not macOS \u2014 skipping keychain");
|
|
10569
|
+
return null;
|
|
10570
|
+
}
|
|
10527
10571
|
try {
|
|
10528
|
-
|
|
10529
|
-
const
|
|
10530
|
-
await
|
|
10531
|
-
|
|
10572
|
+
log("[keychain] probing macOS security CLI...");
|
|
10573
|
+
const t0 = Date.now();
|
|
10574
|
+
await securityExec("help");
|
|
10575
|
+
log(`[keychain] security CLI available (${Date.now() - t0}ms)`);
|
|
10576
|
+
return new SecurityCliKeychainStore();
|
|
10532
10577
|
} catch (err) {
|
|
10533
10578
|
warn(
|
|
10534
10579
|
`OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
|
|
@@ -10544,9 +10589,12 @@ function createCredentialStore() {
|
|
|
10544
10589
|
return initPromise;
|
|
10545
10590
|
}
|
|
10546
10591
|
async function initStore() {
|
|
10592
|
+
log("[credential-store] initializing...");
|
|
10593
|
+
const t0 = Date.now();
|
|
10547
10594
|
const fileStore = new FileCredentialStore();
|
|
10548
10595
|
const keychainStore = await tryCreateKeychainStore();
|
|
10549
10596
|
if (!keychainStore) {
|
|
10597
|
+
log(`[credential-store] using file store (${Date.now() - t0}ms)`);
|
|
10550
10598
|
return fileStore;
|
|
10551
10599
|
}
|
|
10552
10600
|
const keychainCreds = await keychainStore.load();
|
|
@@ -10554,6 +10602,7 @@ async function initStore() {
|
|
|
10554
10602
|
const fileCreds = await fileStore.load();
|
|
10555
10603
|
if (fileCreds) {
|
|
10556
10604
|
await keychainStore.save(fileCreds);
|
|
10605
|
+
keychainStore["cached"] = void 0;
|
|
10557
10606
|
const verified = await keychainStore.load();
|
|
10558
10607
|
if (verified) {
|
|
10559
10608
|
await fileStore.clear();
|
|
@@ -10564,6 +10613,7 @@ async function initStore() {
|
|
|
10564
10613
|
}
|
|
10565
10614
|
}
|
|
10566
10615
|
}
|
|
10616
|
+
log(`[credential-store] using keychain store (${Date.now() - t0}ms)`);
|
|
10567
10617
|
return keychainStore;
|
|
10568
10618
|
}
|
|
10569
10619
|
|
|
@@ -10660,6 +10710,8 @@ async function authenticate() {
|
|
|
10660
10710
|
);
|
|
10661
10711
|
}
|
|
10662
10712
|
async function ensureAuth() {
|
|
10713
|
+
log("[auth] ensureAuth() started");
|
|
10714
|
+
const t0 = Date.now();
|
|
10663
10715
|
const apiKey = process.env.FAUX_API_KEY;
|
|
10664
10716
|
if (apiKey) {
|
|
10665
10717
|
log("Using FAUX_API_KEY from environment");
|
|
@@ -10670,7 +10722,9 @@ async function ensureAuth() {
|
|
|
10670
10722
|
source: "api-key"
|
|
10671
10723
|
};
|
|
10672
10724
|
}
|
|
10725
|
+
log("[auth] creating credential store...");
|
|
10673
10726
|
const credStore = await createCredentialStore();
|
|
10727
|
+
log(`[auth] credential store ready (${credStore.name}) in ${Date.now() - t0}ms`);
|
|
10674
10728
|
const saved = await credStore.load();
|
|
10675
10729
|
if (saved) {
|
|
10676
10730
|
if (!isExpiringSoon(saved)) {
|
|
@@ -11227,6 +11281,31 @@ async function probeCdpPorts() {
|
|
|
11227
11281
|
}
|
|
11228
11282
|
return null;
|
|
11229
11283
|
}
|
|
11284
|
+
async function launchFigmaWithCdp() {
|
|
11285
|
+
const figmaPath = findFigmaPath();
|
|
11286
|
+
if (!figmaPath) {
|
|
11287
|
+
throw new Error(
|
|
11288
|
+
"Figma Desktop is not installed. Download from https://figma.com/downloads"
|
|
11289
|
+
);
|
|
11290
|
+
}
|
|
11291
|
+
const port = await findAvailablePort();
|
|
11292
|
+
log(`Launching Figma Desktop (port ${port})...`);
|
|
11293
|
+
launchFigmaProcess(figmaPath, port);
|
|
11294
|
+
const startTime = Date.now();
|
|
11295
|
+
while (Date.now() - startTime < CDP_WAIT_TIMEOUT_MS) {
|
|
11296
|
+
await new Promise((r) => setTimeout(r, CDP_POLL_INTERVAL_MS));
|
|
11297
|
+
const { alive, isFigma } = await isCdpAlive(port);
|
|
11298
|
+
if (alive && isFigma) {
|
|
11299
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
11300
|
+
const targets = await listTargets(port);
|
|
11301
|
+
log("Figma Desktop started");
|
|
11302
|
+
return { port, targets };
|
|
11303
|
+
}
|
|
11304
|
+
}
|
|
11305
|
+
throw new Error(
|
|
11306
|
+
`Figma did not respond on port ${port} within ${CDP_WAIT_TIMEOUT_MS / 1e3}s. Try again.`
|
|
11307
|
+
);
|
|
11308
|
+
}
|
|
11230
11309
|
function launchFigmaProcess(figmaPath, port) {
|
|
11231
11310
|
if (process.platform === "darwin") {
|
|
11232
11311
|
const binary = `${figmaPath}/Contents/MacOS/Figma`;
|
|
@@ -25908,8 +25987,11 @@ var RESOURCES = [
|
|
|
25908
25987
|
];
|
|
25909
25988
|
var INSTRUCTIONS = `You are connected to Figma Desktop via faux-studio. You can create, modify, and inspect designs using the tools below.
|
|
25910
25989
|
|
|
25990
|
+
## Transport
|
|
25991
|
+
faux-studio connects to Figma Desktop via **CDP (Chrome DevTools Protocol)** by default \u2014 no plugin needed. It launches Figma with a debug port and communicates directly. If the Figma plugin is installed and running, it can also connect via plugin WebSocket as a fallback. CDP is the preferred zero-setup transport.
|
|
25992
|
+
|
|
25911
25993
|
## Workflow
|
|
25912
|
-
1. Call \`setup_figma\` first to ensure Figma is running and connected.
|
|
25994
|
+
1. Call \`setup_figma\` first to ensure Figma is running and connected (via CDP or plugin).
|
|
25913
25995
|
2. Call \`get_page_structure\` or \`get_screenshot\` to understand what's on the canvas.
|
|
25914
25996
|
3. Use \`create_from_schema\` for all creation \u2014 it accepts declarative JSON schemas.
|
|
25915
25997
|
4. Use \`modify_via_schema\` to change existing nodes.
|
|
@@ -25949,7 +26031,7 @@ Resources provide quick read-only access to Figma state without tool calls:
|
|
|
25949
26031
|
- Create components for reusable UI patterns.`;
|
|
25950
26032
|
function createMcpServer(deps) {
|
|
25951
26033
|
const server2 = new Server(
|
|
25952
|
-
{ name: "faux-studio", version: "0.4.
|
|
26034
|
+
{ name: "faux-studio", version: "0.4.7" },
|
|
25953
26035
|
{
|
|
25954
26036
|
capabilities: { tools: { listChanged: true }, resources: {}, logging: {} },
|
|
25955
26037
|
instructions: INSTRUCTIONS
|
|
@@ -25964,7 +26046,7 @@ function createMcpServer(deps) {
|
|
|
25964
26046
|
},
|
|
25965
26047
|
{
|
|
25966
26048
|
name: "setup_figma",
|
|
25967
|
-
description: "Ensure Figma Desktop is running and connected. Call this before any design work. Launches Figma if needed, detects open files and the active tab
|
|
26049
|
+
description: "Ensure Figma Desktop is running and connected via CDP (preferred) or plugin WebSocket. Call this before any design work. Launches Figma with CDP debug port if needed, detects open files and the active tab. Returns connection status, transport mode, active file, and list of all open design files. Idempotent \u2014 safe to call multiple times.",
|
|
25968
26050
|
inputSchema: {
|
|
25969
26051
|
type: "object",
|
|
25970
26052
|
properties: {},
|
|
@@ -26178,13 +26260,19 @@ function lazyInit() {
|
|
|
26178
26260
|
return initPromise2;
|
|
26179
26261
|
}
|
|
26180
26262
|
async function doInit() {
|
|
26263
|
+
log("[init] lazyInit triggered \u2014 starting auth + tool fetch");
|
|
26264
|
+
const t0 = Date.now();
|
|
26181
26265
|
auth = await ensureAuth();
|
|
26266
|
+
log(`[init] auth complete in ${Date.now() - t0}ms`);
|
|
26182
26267
|
try {
|
|
26268
|
+
const t1 = Date.now();
|
|
26183
26269
|
tools = await getTools(auth.jwt);
|
|
26270
|
+
log(`[init] tools fetched (${tools.length}) in ${Date.now() - t1}ms`);
|
|
26184
26271
|
} catch (err) {
|
|
26185
26272
|
error(err instanceof Error ? err.message : String(err));
|
|
26186
26273
|
process.exit(1);
|
|
26187
26274
|
}
|
|
26275
|
+
log(`[init] full initialization complete in ${Date.now() - t0}ms`);
|
|
26188
26276
|
}
|
|
26189
26277
|
async function tryConnectCdp() {
|
|
26190
26278
|
try {
|
|
@@ -26208,10 +26296,6 @@ async function tryConnectCdp() {
|
|
|
26208
26296
|
}
|
|
26209
26297
|
}
|
|
26210
26298
|
async function executeScript(script, intents, opts) {
|
|
26211
|
-
if (forceTransport !== "cdp" && pluginServer.hasConnections) {
|
|
26212
|
-
log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
|
|
26213
|
-
return pluginServer.executeScript(script, void 0, intents);
|
|
26214
|
-
}
|
|
26215
26299
|
if (forceTransport !== "plugin") {
|
|
26216
26300
|
const client = await tryConnectCdp();
|
|
26217
26301
|
if (client) {
|
|
@@ -26248,6 +26332,10 @@ async function executeScript(script, intents, opts) {
|
|
|
26248
26332
|
}
|
|
26249
26333
|
}
|
|
26250
26334
|
}
|
|
26335
|
+
if (forceTransport !== "cdp" && pluginServer.hasConnections) {
|
|
26336
|
+
log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
|
|
26337
|
+
return pluginServer.executeScript(script, void 0, intents);
|
|
26338
|
+
}
|
|
26251
26339
|
log("No transport available, waiting for plugin...");
|
|
26252
26340
|
return pluginServer.executeScript(script, void 0, intents);
|
|
26253
26341
|
}
|
|
@@ -26320,9 +26408,6 @@ function pluginReadyResult() {
|
|
|
26320
26408
|
};
|
|
26321
26409
|
}
|
|
26322
26410
|
async function setupFigma(params) {
|
|
26323
|
-
if (pluginServer.hasConnections) {
|
|
26324
|
-
return pluginReadyResult();
|
|
26325
|
-
}
|
|
26326
26411
|
if (forceTransport !== "plugin" && cdpClient?.connected && cdpClient.hasContext) {
|
|
26327
26412
|
if (fileTracker) {
|
|
26328
26413
|
const detection = await fileTracker.detectActiveFile();
|
|
@@ -26340,6 +26425,9 @@ async function setupFigma(params) {
|
|
|
26340
26425
|
message: "Connected via CDP. Ready to design."
|
|
26341
26426
|
};
|
|
26342
26427
|
}
|
|
26428
|
+
if (forceTransport !== "cdp" && pluginServer.hasConnections) {
|
|
26429
|
+
return pluginReadyResult();
|
|
26430
|
+
}
|
|
26343
26431
|
if (forceTransport !== "plugin") {
|
|
26344
26432
|
const existing = await probeCdpPorts();
|
|
26345
26433
|
if (existing) {
|
|
@@ -26375,25 +26463,77 @@ async function setupFigma(params) {
|
|
|
26375
26463
|
return {
|
|
26376
26464
|
status: "not_installed",
|
|
26377
26465
|
transport: "none",
|
|
26378
|
-
message:
|
|
26379
|
-
|
|
26380
|
-
1. Download Figma: https://figma.com/downloads
|
|
26381
|
-
2. Install and open it
|
|
26382
|
-
3. Install the faux-studio plugin: ${PLUGIN_URL}
|
|
26383
|
-
4. Call setup_figma again`
|
|
26466
|
+
message: "Figma Desktop is not installed.\n\n1. Download Figma: https://figma.com/downloads\n2. Install and open it\n3. Call setup_figma again"
|
|
26384
26467
|
};
|
|
26385
26468
|
}
|
|
26386
26469
|
if (!isFigmaRunning()) {
|
|
26387
|
-
|
|
26388
|
-
|
|
26389
|
-
|
|
26390
|
-
|
|
26391
|
-
|
|
26392
|
-
|
|
26393
|
-
|
|
26394
|
-
|
|
26470
|
+
if (forceTransport === "plugin") {
|
|
26471
|
+
try {
|
|
26472
|
+
launchFigma();
|
|
26473
|
+
} catch (err) {
|
|
26474
|
+
return {
|
|
26475
|
+
status: "not_installed",
|
|
26476
|
+
transport: "none",
|
|
26477
|
+
message: err instanceof Error ? err.message : "Failed to launch Figma."
|
|
26478
|
+
};
|
|
26479
|
+
}
|
|
26480
|
+
} else {
|
|
26481
|
+
try {
|
|
26482
|
+
const connection = await launchFigmaWithCdp();
|
|
26483
|
+
const target = findFigmaDesignTarget(connection.targets);
|
|
26484
|
+
if (target) {
|
|
26485
|
+
const client = new CdpClient();
|
|
26486
|
+
await client.connect(target.webSocketDebuggerUrl);
|
|
26487
|
+
await client.discoverFigmaContext();
|
|
26488
|
+
cdpClient = client;
|
|
26489
|
+
log(`CDP connected after launch: ${target.title}`);
|
|
26490
|
+
if (!fileTracker) {
|
|
26491
|
+
fileTracker = new CdpFileTracker(connection.port);
|
|
26492
|
+
}
|
|
26493
|
+
const detection = await fileTracker.detectActiveFile();
|
|
26494
|
+
fileTracker.acknowledgeActiveFile();
|
|
26495
|
+
return {
|
|
26496
|
+
status: "launched",
|
|
26497
|
+
transport: "cdp",
|
|
26498
|
+
message: `Launched Figma and connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
|
|
26499
|
+
activeFile: detection.activeFile?.fileName ?? target.title,
|
|
26500
|
+
openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
|
|
26501
|
+
port: connection.port
|
|
26502
|
+
};
|
|
26503
|
+
}
|
|
26504
|
+
return {
|
|
26505
|
+
status: "launched",
|
|
26506
|
+
transport: "cdp",
|
|
26507
|
+
message: "Launched Figma with CDP enabled but no design file is open.\n\nOpen or create a design file in Figma, then call setup_figma again.",
|
|
26508
|
+
port: connection.port
|
|
26509
|
+
};
|
|
26510
|
+
} catch (err) {
|
|
26511
|
+
return {
|
|
26512
|
+
status: "not_installed",
|
|
26513
|
+
transport: "none",
|
|
26514
|
+
message: err instanceof Error ? err.message : "Failed to launch Figma."
|
|
26515
|
+
};
|
|
26516
|
+
}
|
|
26395
26517
|
}
|
|
26396
26518
|
}
|
|
26519
|
+
if (forceTransport !== "plugin") {
|
|
26520
|
+
return {
|
|
26521
|
+
status: "waiting_for_plugin",
|
|
26522
|
+
transport: "none",
|
|
26523
|
+
message: `Figma is running but the CDP debug port is not enabled.
|
|
26524
|
+
|
|
26525
|
+
To fix this:
|
|
26526
|
+
1. Quit Figma completely (Cmd+Q / Alt+F4)
|
|
26527
|
+
2. Call setup_figma again \u2014 it will relaunch Figma with CDP enabled
|
|
26528
|
+
|
|
26529
|
+
Alternatively, install the faux-studio plugin:
|
|
26530
|
+
1. Download: ${PLUGIN_URL}
|
|
26531
|
+
2. In Figma: Plugins \u2192 Development \u2192 Import plugin from manifest
|
|
26532
|
+
3. Run: Plugins \u2192 Development \u2192 faux-studio
|
|
26533
|
+
4. Call setup_figma again once the plugin shows "Ready".`,
|
|
26534
|
+
port: pluginServer.port
|
|
26535
|
+
};
|
|
26536
|
+
}
|
|
26397
26537
|
log("Waiting for plugin connection...");
|
|
26398
26538
|
const connected = await waitForPlugin();
|
|
26399
26539
|
if (connected) {
|
|
@@ -26420,7 +26560,8 @@ Call setup_figma again once the plugin shows "Ready".`,
|
|
|
26420
26560
|
};
|
|
26421
26561
|
}
|
|
26422
26562
|
async function main() {
|
|
26423
|
-
|
|
26563
|
+
const startupT0 = Date.now();
|
|
26564
|
+
log(`faux-studio v${"0.4.7"} \u2014 process started (PID ${process.pid}, PPID ${process.ppid})`);
|
|
26424
26565
|
try {
|
|
26425
26566
|
const port = await pluginServer.start();
|
|
26426
26567
|
if (forceTransport === "plugin") {
|
|
@@ -26428,7 +26569,7 @@ async function main() {
|
|
|
26428
26569
|
} else if (forceTransport === "cdp") {
|
|
26429
26570
|
log(`Transport: CDP-only mode (FAUX_TRANSPORT=cdp) \u2014 plugin WS on port ${port} (standby)`);
|
|
26430
26571
|
} else {
|
|
26431
|
-
log(`Transport: auto \u2014 plugin WS on port ${port}
|
|
26572
|
+
log(`Transport: auto \u2014 CDP preferred, plugin WS on port ${port} (fallback)`);
|
|
26432
26573
|
}
|
|
26433
26574
|
} catch (err) {
|
|
26434
26575
|
warn(`Plugin WS server failed to start: ${err instanceof Error ? err.message : err}`);
|
|
@@ -26472,6 +26613,7 @@ async function main() {
|
|
|
26472
26613
|
});
|
|
26473
26614
|
await startServer(server2);
|
|
26474
26615
|
setServer(server2);
|
|
26616
|
+
log(`[startup] MCP server ready in ${Date.now() - startupT0}ms \u2014 handshake complete`);
|
|
26475
26617
|
if (forceTransport !== "plugin") {
|
|
26476
26618
|
try {
|
|
26477
26619
|
await tryConnectCdp();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "faux-studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "AI-powered Figma design via MCP — connect any AI client to Figma Desktop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
"url": "https://github.com/uxfreak/faux-studio.git"
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://faux.design",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
47
|
+
"os": [
|
|
48
|
+
"darwin",
|
|
49
|
+
"linux",
|
|
50
|
+
"win32"
|
|
51
|
+
]
|
|
50
52
|
}
|