@design-twin/dtwin 0.1.0-alpha.0 → 0.1.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/README.md +47 -4
- package/figma-plugin/code.js +4289 -0
- package/figma-plugin/manifest.json +28 -0
- package/figma-plugin/ui.html +722 -0
- package/layer/cross-check.js +2746 -0
- package/layer/drift-lint.js +1526 -0
- package/layer/get-component.js +553 -0
- package/layer/map-bootstrap.js +1404 -0
- package/layer/map-validate.js +397 -0
- package/package.json +5 -1
- package/src/commands.ts +14 -1
- package/src/figma-mcp.ts +51 -20
- package/src/figma-pull.ts +5 -0
- package/src/init.ts +37 -12
package/src/init.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Rules it keeps:
|
|
10
10
|
// - Never overwrites. Every file it would write is skipped, and said so, if it already exists;
|
|
11
11
|
// .mcp.json is MERGED (an existing "figma" entry is left exactly as it is). The one exception is
|
|
12
|
-
// an entry of OURS that can no longer start (mcpEntryProblems: a bun or figma-mcp.ts that is gone,
|
|
12
|
+
// an entry of OURS that can no longer start (mcpEntryProblems: a bun or figma-mcp.ts that is gone or spelt through a versioned Homebrew Cellar folder,
|
|
13
13
|
// a Node-era `node …/dist/figma-mcp.js`, bun without its two flags, a `bunx @design-twin/dtwin mcp` that fetches the
|
|
14
14
|
// package at start-up): --mcp re-points that entry, under
|
|
15
15
|
// its own key and keeping its other fields, at this install — and says so (--dry-run only says so).
|
|
@@ -106,8 +106,21 @@ const BUN_FLAGS = ["--no-install", "--no-env-file"] as const;
|
|
|
106
106
|
* process.execPath alone is the resolved file, and for a Homebrew (or any versioned) install that is a versioned path
|
|
107
107
|
* (…/Cellar/bun/1.4.2/bin/bun) the next `brew upgrade` deletes, leaving Claude Code a server it cannot start. */
|
|
108
108
|
function stableBunPath(execPath: string = process.execPath, onPath: string | null = Bun.which("bun")): string {
|
|
109
|
-
if (!onPath) return execPath;
|
|
110
|
-
try { return fs.realpathSync(onPath) === fs.realpathSync(execPath) ? onPath : execPath; } catch { return execPath; }
|
|
109
|
+
if (!onPath) return stableKegPath(execPath);
|
|
110
|
+
try { return fs.realpathSync(onPath) === fs.realpathSync(execPath) ? onPath : stableKegPath(execPath); } catch { return stableKegPath(execPath); }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The upgrade-proof spelling of a path inside a Homebrew keg. Homebrew installs a formula into a versioned folder,
|
|
114
|
+
* `<prefix>/Cellar/<formula>/<version>/…`, that the next `brew upgrade` deletes, and keeps `<prefix>/opt/<formula>` as a
|
|
115
|
+
* symlink to whichever version is current. Bun resolves symlinks, so a file run through that opt link still sees its
|
|
116
|
+
* Cellar path (import.meta.dirname, process.execPath) — and an absolute path written from it (.mcp.json's server and
|
|
117
|
+
* bun, the plugin manifest Figma remembers) would stop working after one upgrade. Returns the opt spelling when it
|
|
118
|
+
* resolves to the very same file, else `p` unchanged (not in a Cellar, or a layout where opt is elsewhere). */
|
|
119
|
+
function stableKegPath(p: string): string {
|
|
120
|
+
const m = /^(.*)[\\/]Cellar[\\/]([^\\/]+)[\\/][^\\/]+(?:[\\/](.*))?$/.exec(p);
|
|
121
|
+
if (!m || m[1] === undefined || m[2] === undefined) return p;
|
|
122
|
+
const opt = path.join(m[1], "opt", m[2], m[3] ?? "");
|
|
123
|
+
try { return fs.realpathSync(opt) === fs.realpathSync(p) ? opt : p; } catch { return p; }
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
/** The bunx cache directory `dir` sits in, or null. `bunx @design-twin/dtwin …` installs the package into a cache tree
|
|
@@ -119,7 +132,7 @@ function bunxCacheOf(dir: string): string | null {
|
|
|
119
132
|
}
|
|
120
133
|
const bunxRefusal = (cache: string): string =>
|
|
121
134
|
`this dtwin runs from a bunx cache (${cache}) that Bun replaces or deletes on its own — a .mcp.json entry pointing into it would stop starting one day. ` +
|
|
122
|
-
"Install it for good (in a clone: `bun install`, then `cd bridge && bun link`), and run `dtwin init --mcp` from that install";
|
|
135
|
+
"Install it for good (`bun add -g @design-twin/dtwin`, or in a clone: `bun install`, then `cd bridge && bun link`), and run `dtwin init --mcp` from that install";
|
|
123
136
|
|
|
124
137
|
/** How to look at the machine (seams for the suites): does a path exist, and what does a bare command resolve to. */
|
|
125
138
|
export interface EntryProbe {
|
|
@@ -129,9 +142,16 @@ export interface EntryProbe {
|
|
|
129
142
|
cwd?: string;
|
|
130
143
|
}
|
|
131
144
|
|
|
145
|
+
/** A path that works today but sits in a versioned Homebrew folder the next `brew upgrade` deletes — when the opt
|
|
146
|
+
* spelling of it exists (stableKegPath), so init's repair has somewhere stable to point. Empty otherwise. */
|
|
147
|
+
function kegProblem(what: string, p: string): string[] {
|
|
148
|
+
const stable = stableKegPath(p);
|
|
149
|
+
return stable === p ? [] : [`${what} ${p} is in a versioned Homebrew folder the next \`brew upgrade\` deletes (the stable spelling is ${stable})`];
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
/** What is wrong with an .mcp.json entry of OURS (isOurMcpEntry), one phrase each — empty when it will start: its command
|
|
133
153
|
* exists, it runs on Bun (not Node, not the old bridge/dist build), it names a server script (not a package bunx would
|
|
134
|
-
* fetch), that script exists outside a bunx cache, and bun gets the two flags before that script. The ONE rule for init (repair it) and doctor (report it). */
|
|
154
|
+
* fetch), that script exists outside a bunx cache, neither is spelt through a versioned Homebrew Cellar folder, and bun gets the two flags before that script. The ONE rule for init (repair it) and doctor (report it). */
|
|
135
155
|
function mcpEntryProblems(e: unknown, { exists = fs.existsSync, which = (c) => Bun.which(c), cwd = process.cwd() }: EntryProbe = {}): string[] {
|
|
136
156
|
if (!isRecord(e) || typeof e.command !== "string" || e.command === "") return ["it has no `command`"];
|
|
137
157
|
const command = e.command;
|
|
@@ -142,6 +162,7 @@ function mcpEntryProblems(e: unknown, { exists = fs.existsSync, which = (c) => B
|
|
|
142
162
|
if (nodeShaped) out.push(`it runs on Node (\`${command}\`) — Design Twin runs on Bun only`);
|
|
143
163
|
const found = /[\\/]/.test(command) ? exists(path.resolve(cwd, command)) : which(command) !== null;
|
|
144
164
|
if (!found) out.push(`its command \`${command}\` ${/[\\/]/.test(command) ? "does not exist" : "is not on PATH"}`);
|
|
165
|
+
else if (/[\\/]/.test(command)) out.push(...kegProblem("its command", path.resolve(cwd, command)));
|
|
145
166
|
const i = args.findIndex((a) => FIGMA_MCP_SCRIPT.test(a));
|
|
146
167
|
const script = args[i];
|
|
147
168
|
// No server script: the package-runner form (`bunx @design-twin/dtwin mcp`, `bun x @design-twin/dtwin mcp`, an older `npx designtwin
|
|
@@ -156,6 +177,7 @@ function mcpEntryProblems(e: unknown, { exists = fs.existsSync, which = (c) => B
|
|
|
156
177
|
// figma-mcp.js / .mjs (bridge/dist, or the older bridge/figma-mcp.mjs) is the Node-era build: the package ships .ts now
|
|
157
178
|
if (/\.m?js$/.test(script)) out.push(`it points at the old Node build (${script})`);
|
|
158
179
|
else if (!exists(abs)) out.push(`its server script ${script} does not exist`);
|
|
180
|
+
else out.push(...kegProblem("its server script", abs));
|
|
159
181
|
const cache = bunxCacheOf(abs);
|
|
160
182
|
if (cache) out.push(`its server script is in a bunx cache (${cache}) that Bun replaces or deletes on its own`);
|
|
161
183
|
if (!nodeShaped) {
|
|
@@ -367,18 +389,21 @@ function main(argv: string[]): void {
|
|
|
367
389
|
// ran init is one this package is known to work with, spelt the way that survives its upgrades
|
|
368
390
|
// (stableBunPath). The flags are the shebang's (figma-pull.ts): no auto-install from the network, and
|
|
369
391
|
// the project's .env stays out of the server's environment.
|
|
370
|
-
|
|
392
|
+
// Under Homebrew this file runs from a versioned Cellar folder, so the directory is spelt through opt (stableKegPath).
|
|
393
|
+
const here = stableKegPath(import.meta.dirname);
|
|
394
|
+
const mcpEntry: McpEntry = { command: stableBunPath(), args: [...BUN_FLAGS, path.join(here, "figma-mcp.ts")] };
|
|
371
395
|
// Run through `bunx @design-twin/dtwin init`: this very file is in bunx's cache, so no entry may point at it.
|
|
372
396
|
const cache = bunxCacheOf(import.meta.dirname);
|
|
373
397
|
const refuseMcp = cache ? bunxRefusal(cache) : undefined;
|
|
374
|
-
if (cache && !argv.includes("--mcp")) log(`note this dtwin runs from a bunx cache (${cache}) — install it for good (in a clone: \`bun install\`, then \`cd bridge && bun link\`) to have \`dtwin\` on PATH and to register the MCP server`);
|
|
398
|
+
if (cache && !argv.includes("--mcp")) log(`note this dtwin runs from a bunx cache (${cache}) — install it for good (\`bun add -g @design-twin/dtwin\`, or in a clone: \`bun install\`, then \`cd bridge && bun link\`) to have \`dtwin\` on PATH and to register the MCP server`);
|
|
375
399
|
const actions = plan(cwd, { mcp: argv.includes("--mcp"), mcpEntry, ...ifDefined("refuseMcp", refuseMcp), token });
|
|
376
400
|
if (dry) for (const a of actions) log(`${a.kind === "write" ? "would write" : a.kind === "mkdir" ? "would create" : a.kind} ${"path" in a ? a.path + " — " : ""}${a.note}`);
|
|
377
401
|
else apply(cwd, actions, log);
|
|
378
402
|
|
|
379
|
-
// The plugin manifest
|
|
380
|
-
//
|
|
381
|
-
|
|
403
|
+
// The Figma plugin's manifest: a repo checkout's (bridge/src/ -> ../../figma-plugin/), else the copy the npm
|
|
404
|
+
// package carries (../figma-plugin/, put there by bridge/pack.ts). Neither: the step names the repo instead.
|
|
405
|
+
// Spelt from `here` too: Figma remembers the path a plugin was imported from, and a Cellar one is gone after an upgrade.
|
|
406
|
+
const manifest = [path.join(here, "..", "..", "figma-plugin", "manifest.json"), path.join(here, "..", "figma-plugin", "manifest.json")].find((m) => fs.existsSync(m));
|
|
382
407
|
// Step 3 is already done for anyone who reached init THROUGH the installed plugin's help skill —
|
|
383
408
|
// which is the documented path. Claude Code records installed plugins under ~/.claude; when the
|
|
384
409
|
// marker is there, saying "install the plugin" is noise at best and confusing at worst.
|
|
@@ -400,7 +425,7 @@ function main(argv: string[]): void {
|
|
|
400
425
|
})();
|
|
401
426
|
|
|
402
427
|
const steps = [
|
|
403
|
-
"In Figma DESKTOP: Plugins → Development → Import plugin from manifest… → " + (
|
|
428
|
+
"In Figma DESKTOP: Plugins → Development → Import plugin from manifest… → " + (manifest ?? "figma-plugin/manifest.json from a clone of the Design Twin repo (this install does not include it)"),
|
|
404
429
|
"Run the plugin (Plugins → Development → Design Twin), paste the bridge token into its \"Bridge token\" field, Save: dtwin --show-token | pbcopy",
|
|
405
430
|
];
|
|
406
431
|
if (!pluginInstalled) {
|
|
@@ -421,4 +446,4 @@ function main(argv: string[]): void {
|
|
|
421
446
|
);
|
|
422
447
|
}
|
|
423
448
|
|
|
424
|
-
export { detectProfile, plan, apply, main, isOurMcpEntry, mcpEntryProblems, stableBunPath, bunxCacheOf, readMcpJson, usesTailwind, usesVite };
|
|
449
|
+
export { detectProfile, plan, apply, main, isOurMcpEntry, mcpEntryProblems, stableBunPath, stableKegPath, bunxCacheOf, readMcpJson, usesTailwind, usesVite };
|