@design-twin/dtwin 0.1.0-alpha.0 → 0.1.0

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.
@@ -0,0 +1,397 @@
1
+ // GENERATED from design-to-code/ by claude-plugin/build-scripts.js — edit the source, then rebuild.
2
+
3
+
4
+ // design-to-code/cli-args.ts
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ // bridge/src/errmsg.ts
9
+ var errMsg = (e) => typeof e === "string" ? e : String(e && e.message || e);
10
+
11
+ // bridge/src/stdio.ts
12
+ import { format, inspect } from "node:util";
13
+ function installQueuedConsole(o = {}) {
14
+ trackWrites(process.stdout);
15
+ trackWrites(process.stderr);
16
+ const out = o.logTo === "stderr" ? process.stderr : process.stdout;
17
+ const err = process.stderr;
18
+ let indent = "";
19
+ const write = (s, text) => {
20
+ s.write((indent ? text.replace(/^/gm, indent) : text) + "\n");
21
+ };
22
+ const line = (s) => (...a) => {
23
+ write(s, format(...a));
24
+ };
25
+ console.log = console.info = console.debug = line(out);
26
+ console.warn = console.error = line(err);
27
+ console.dir = (item, options) => {
28
+ write(out, inspect(item, { customInspect: false, ...options }));
29
+ };
30
+ console.dirxml = line(out);
31
+ console.table = (data) => {
32
+ write(out, inspect(data));
33
+ };
34
+ console.write = (...data) => {
35
+ let n = 0;
36
+ for (const d of data) {
37
+ const b = typeof d === "string" ? Buffer.from(d) : d instanceof ArrayBuffer ? Buffer.from(d) : Buffer.from(d.buffer, d.byteOffset, d.byteLength);
38
+ if (b.length) {
39
+ out.write(b);
40
+ n += b.length;
41
+ }
42
+ }
43
+ return n;
44
+ };
45
+ console.clear = () => {
46
+ if (out.isTTY) out.write("\x1B[1;1H\x1B[0J");
47
+ };
48
+ console.trace = (...a) => {
49
+ const e = new Error(format(...a));
50
+ e.name = "Trace";
51
+ Error.captureStackTrace(e, console.trace);
52
+ write(err, e.stack ?? `Trace: ${e.message}`);
53
+ };
54
+ console.assert = (cond, ...a) => {
55
+ if (!cond) write(err, a.length ? "Assertion failed: " + format(...a) : "Assertion failed");
56
+ };
57
+ console.group = console.groupCollapsed = (...a) => {
58
+ if (a.length) write(out, format(...a));
59
+ indent += " ";
60
+ };
61
+ console.groupEnd = () => {
62
+ indent = indent.slice(0, -2);
63
+ };
64
+ const counts = /* @__PURE__ */ new Map();
65
+ console.count = (label = "default") => {
66
+ const n = (counts.get(label) ?? 0) + 1;
67
+ counts.set(label, n);
68
+ write(out, `${label}: ${n}`);
69
+ };
70
+ console.countReset = (label = "default") => {
71
+ counts.delete(label);
72
+ };
73
+ const timers = /* @__PURE__ */ new Map();
74
+ const elapsed = (label) => {
75
+ const t = timers.get(label);
76
+ return t === void 0 ? null : `${label}: ${(performance.now() - t).toFixed(3)}ms`;
77
+ };
78
+ console.time = (label = "default") => {
79
+ timers.set(label, performance.now());
80
+ };
81
+ console.timeLog = (label = "default", ...a) => {
82
+ const e = elapsed(label);
83
+ if (e === null) write(err, `Warning: No such label '${label}' for console.timeLog()`);
84
+ else write(out, [e, ...a.map((x) => format(x))].join(" "));
85
+ };
86
+ console.timeEnd = (label = "default") => {
87
+ const e = elapsed(label);
88
+ timers.delete(label);
89
+ write(e === null ? err : out, e ?? `Warning: No such label '${label}' for console.timeEnd()`);
90
+ };
91
+ }
92
+ var inFlight = /* @__PURE__ */ new Map();
93
+ function trackWrites(s) {
94
+ if (inFlight.has(s)) return;
95
+ const st = { n: 0, idle: [] };
96
+ inFlight.set(s, st);
97
+ const write = s.write.bind(s);
98
+ const settle = () => {
99
+ if (--st.n === 0) for (const r of st.idle.splice(0)) r();
100
+ };
101
+ s.write = (chunk, encoding, cb) => {
102
+ const done = typeof encoding === "function" ? encoding : cb;
103
+ const enc = typeof encoding === "function" ? void 0 : encoding;
104
+ if (chunk.length === 0) return write(chunk, enc, done);
105
+ st.n++;
106
+ let called = false;
107
+ return write(chunk, enc, (err) => {
108
+ if (!called) {
109
+ called = true;
110
+ settle();
111
+ }
112
+ done?.(err);
113
+ });
114
+ };
115
+ }
116
+ var CliExit = class extends Error {
117
+ code;
118
+ constructor(code) {
119
+ super(`exit ${code}`);
120
+ this.name = "CliExit";
121
+ this.code = code;
122
+ }
123
+ };
124
+ var cliDepth = 0;
125
+ function exitCli(code) {
126
+ if (cliDepth > 0) throw new CliExit(code);
127
+ process.exit(code);
128
+ }
129
+ function runCli(main2, o, argv = process.argv.slice(2)) {
130
+ installQueuedConsole();
131
+ const done = o.done ?? ((code) => {
132
+ process.exitCode = code;
133
+ });
134
+ const fail = (e) => {
135
+ if (e instanceof CliExit) return done(e.code);
136
+ console.error(o.errorLine ? o.errorLine(e) : `${o.name}: ${e instanceof Error && e.stack || String(e)}`);
137
+ done(o.errorCode ?? 1);
138
+ };
139
+ cliDepth++;
140
+ let settled = false;
141
+ const leave = () => {
142
+ if (!settled) {
143
+ settled = true;
144
+ cliDepth--;
145
+ }
146
+ };
147
+ try {
148
+ const r = main2(argv);
149
+ if (typeof r === "number") {
150
+ leave();
151
+ done(r);
152
+ } else r.then((c) => {
153
+ leave();
154
+ done(c);
155
+ }, (e) => {
156
+ leave();
157
+ fail(e);
158
+ });
159
+ } catch (e) {
160
+ leave();
161
+ fail(e);
162
+ }
163
+ }
164
+
165
+ // design-to-code/proc.ts
166
+ var pause = new Int32Array(new SharedArrayBuffer(4));
167
+
168
+ // design-to-code/cli-args.ts
169
+ var SELF = fileURLToPath(import.meta.url);
170
+ var shellQuote = (p) => /["$`\\!]/.test(p) ? `'${p.replaceAll("'", `'\\''`)}'` : `"${p}"`;
171
+ var SCRIPT_RUNNER = "bun --no-install --no-env-file";
172
+ var scriptCmd = (name) => `${SCRIPT_RUNNER} ${shellQuote(path.join(path.dirname(SELF), name + path.extname(SELF)))}`;
173
+
174
+ // bridge/src/read-json.ts
175
+ import fs from "node:fs";
176
+ var anyJson = (_x) => true;
177
+ function readFailure(e) {
178
+ const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
179
+ if (code === "ENOENT") return { error: "does not exist", missing: true };
180
+ return {
181
+ error: code === "EISDIR" ? "is a directory, not a file" : code === "EACCES" ? "is not readable (permission denied)" : `could not be read (${String(code || e)})`
182
+ };
183
+ }
184
+ function readJson(file, guard) {
185
+ let buf;
186
+ try {
187
+ buf = fs.readFileSync(file);
188
+ } catch (e) {
189
+ return readFailure(e);
190
+ }
191
+ if (buf.length >= 2 && (buf[0] === 255 && buf[1] === 254 || buf[0] === 254 && buf[1] === 255)) {
192
+ return { error: "is UTF-16, not UTF-8 \u2014 re-save it as UTF-8" };
193
+ }
194
+ let raw = buf.toString("utf8");
195
+ if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
196
+ let parsed;
197
+ try {
198
+ const value = JSON.parse(raw);
199
+ parsed = value;
200
+ } catch (e) {
201
+ return { error: `is not valid JSON \u2014 ${errMsg(e)}` };
202
+ }
203
+ if (!guard(parsed)) return { error: `is not ${guard.expected || "the expected kind of document"}` };
204
+ return { doc: parsed };
205
+ }
206
+
207
+ // design-to-code/catalog-input.ts
208
+ function readJsonFile(file, what, hint) {
209
+ return readDocFile(file, what, anyJson, hint);
210
+ }
211
+ function readDocFile(file, what, guard, hint) {
212
+ return docOrExit(loadDocFile(file, what, guard, hint));
213
+ }
214
+ function loadDocFile(file, what, guard, hint, readAt = file) {
215
+ const r = readJson(readAt, guard);
216
+ if ("doc" in r) return r;
217
+ const ioFailure = r.missing || /^(is a directory|is not readable|could not be read)/.test(r.error);
218
+ return { error: `${what}: '${file}' ${r.error}${ioFailure ? "." : ""}` + (ioFailure && hint ? `
219
+ ${hint}` : "") };
220
+ }
221
+ function docOrExit(r) {
222
+ if ("doc" in r) return r.doc;
223
+ console.error(`error ${r.error}`);
224
+ exitCli(2);
225
+ }
226
+
227
+ // bridge/src/json-util.ts
228
+ function nullProto() {
229
+ return /* @__PURE__ */ Object.create(null);
230
+ }
231
+ function isRecord(x) {
232
+ return typeof x === "object" && x !== null && !Array.isArray(x);
233
+ }
234
+
235
+ // design-to-code/map-validate-core.ts
236
+ var STATUSES = ["active", "deprecated", "needs-review"];
237
+ var KEYS = {
238
+ root: ["version", "figmaFileKey", "components"],
239
+ entry: ["figma", "code", "props", "variantOverrides", "childrenByLayer", "status", "note"],
240
+ figma: ["key", "id", "name", "unstable"],
241
+ code: ["module", "export", "targets"],
242
+ target: ["module", "export"],
243
+ vo: ["when", "code"],
244
+ voCode: ["module", "export"],
245
+ children: ["layerNamePattern", "slot"],
246
+ prop: Object.assign(nullProto(), {
247
+ enum: ["kind", "codeProp", "values", "default", "omitDefault"],
248
+ boolean: ["kind", "codeProp", "default", "omitDefault"],
249
+ string: ["kind", "codeProp"],
250
+ instance: ["kind", "codeProp", "slot"]
251
+ })
252
+ };
253
+ var PROP_KINDS = Object.keys(KEYS.prop);
254
+ var isStr = (v) => typeof v === "string";
255
+ var isBool = (v) => typeof v === "boolean";
256
+ var noExtra = (obj, allowed, at, err) => {
257
+ if (!Array.isArray(allowed)) return;
258
+ for (const k of Object.keys(obj)) if (!allowed.includes(k)) err(`${at}.${k}`, "unknown property (additionalProperties:false)");
259
+ };
260
+ var optStrings = (obj, keys, at, err) => {
261
+ for (const k of keys) if (obj[k] !== void 0 && !isStr(obj[k])) err(`${at}.${k}`, "must be a string");
262
+ };
263
+ function validateMap(map) {
264
+ const errors = [];
265
+ const err = (path2, message) => errors.push({ path: path2, message });
266
+ if (!isRecord(map)) return { ok: false, errors: [{ path: "", message: "map must be an object" }] };
267
+ noExtra(map, KEYS.root, "", err);
268
+ if (map.version !== 1) err("version", "must be 1");
269
+ if (map.figmaFileKey !== void 0 && !isStr(map.figmaFileKey)) err("figmaFileKey", "must be a string");
270
+ if (!isRecord(map.components)) {
271
+ err("components", "must be an object");
272
+ return { ok: false, errors };
273
+ }
274
+ for (const key of Object.keys(map.components)) {
275
+ const e = map.components[key];
276
+ const at = `components.${key}`;
277
+ if (!isRecord(e)) {
278
+ err(at, "entry must be an object");
279
+ continue;
280
+ }
281
+ noExtra(e, KEYS.entry, at, err);
282
+ if (!isRecord(e.figma)) err(`${at}.figma`, "required object");
283
+ else {
284
+ noExtra(e.figma, KEYS.figma, `${at}.figma`, err);
285
+ if (!isStr(e.figma.name)) err(`${at}.figma.name`, "required string");
286
+ optStrings(e.figma, ["key", "id"], `${at}.figma`, err);
287
+ if (e.figma.unstable !== void 0 && !isBool(e.figma.unstable)) err(`${at}.figma.unstable`, "must be a boolean");
288
+ }
289
+ if (!isRecord(e.code)) err(`${at}.code`, "required object");
290
+ else {
291
+ noExtra(e.code, KEYS.code, `${at}.code`, err);
292
+ if (!isStr(e.code.module)) err(`${at}.code.module`, "required string");
293
+ if (!isStr(e.code.export)) err(`${at}.code.export`, "required string");
294
+ if (e.code.targets !== void 0) {
295
+ if (!isRecord(e.code.targets)) err(`${at}.code.targets`, "must be an object");
296
+ else for (const t of Object.keys(e.code.targets)) {
297
+ const tv = e.code.targets[t], ta = `${at}.code.targets.${t}`;
298
+ if (!isRecord(tv)) err(ta, "must be an object");
299
+ else {
300
+ noExtra(tv, KEYS.target, ta, err);
301
+ optStrings(tv, ["module", "export"], ta, err);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ if (e.status !== void 0 && (!isStr(e.status) || !STATUSES.includes(e.status))) err(`${at}.status`, `must be one of ${STATUSES.join("|")}`);
307
+ optStrings(e, ["note"], at, err);
308
+ if (e.props !== void 0) {
309
+ if (!isRecord(e.props)) err(`${at}.props`, "must be an object");
310
+ else for (const pn of Object.keys(e.props)) validateProp(e.props[pn], `${at}.props.${pn}`, err);
311
+ }
312
+ if (e.variantOverrides !== void 0) {
313
+ if (!Array.isArray(e.variantOverrides)) err(`${at}.variantOverrides`, "must be an array");
314
+ else e.variantOverrides.forEach((vo, i) => {
315
+ const va = `${at}.variantOverrides[${i}]`;
316
+ if (!isRecord(vo)) {
317
+ err(va, "must be an object");
318
+ return;
319
+ }
320
+ noExtra(vo, KEYS.vo, va, err);
321
+ if (!isRecord(vo.when)) err(`${va}.when`, "required object");
322
+ else for (const wk of Object.keys(vo.when)) if (!isStr(vo.when[wk])) err(`${va}.when.${wk}`, "value must be a string");
323
+ if (!isRecord(vo.code)) err(`${va}.code`, "required object");
324
+ else {
325
+ noExtra(vo.code, KEYS.voCode, `${va}.code`, err);
326
+ if (!isStr(vo.code.module)) err(`${va}.code.module`, "required string");
327
+ if (!isStr(vo.code.export)) err(`${va}.code.export`, "required string");
328
+ }
329
+ });
330
+ }
331
+ if (e.childrenByLayer !== void 0) {
332
+ if (!isRecord(e.childrenByLayer)) err(`${at}.childrenByLayer`, "must be an object");
333
+ else {
334
+ noExtra(e.childrenByLayer, KEYS.children, `${at}.childrenByLayer`, err);
335
+ optStrings(e.childrenByLayer, ["layerNamePattern", "slot"], `${at}.childrenByLayer`, err);
336
+ }
337
+ }
338
+ }
339
+ return { ok: errors.length === 0, errors };
340
+ }
341
+ function validateProp(p, at, err) {
342
+ if (!isRecord(p)) {
343
+ err(at, "prop must be an object");
344
+ return;
345
+ }
346
+ const allowed = typeof p.kind === "string" ? KEYS.prop[p.kind] : void 0;
347
+ if (!Array.isArray(allowed)) {
348
+ err(`${at}.kind`, `must be one of ${PROP_KINDS.join("|")}`);
349
+ return;
350
+ }
351
+ noExtra(p, allowed, at, err);
352
+ if (p.kind !== "instance" && !isStr(p.codeProp)) err(`${at}.codeProp`, "required string");
353
+ if (p.kind === "instance" && p.codeProp !== void 0 && !isStr(p.codeProp)) err(`${at}.codeProp`, "must be a string");
354
+ if (p.kind === "instance" && p.slot !== void 0 && !isStr(p.slot)) err(`${at}.slot`, "must be a string");
355
+ if (p.kind === "enum") {
356
+ if (p.values !== void 0 && !isRecord(p.values)) err(`${at}.values`, "must be an object (VARIANT option -> code value)");
357
+ else if (isRecord(p.values)) for (const k of Object.keys(p.values)) {
358
+ const t = typeof p.values[k];
359
+ if (p.values[k] !== null && !["string", "number", "boolean"].includes(t)) err(`${at}.values.${k}`, "value must be string|number|boolean|null");
360
+ }
361
+ if (p.default !== void 0 && !["string", "number", "boolean"].includes(typeof p.default)) err(`${at}.default`, "must be string|number|boolean");
362
+ }
363
+ if (p.kind === "boolean" && p.default !== void 0 && !isBool(p.default)) err(`${at}.default`, "must be a boolean");
364
+ if ((p.kind === "enum" || p.kind === "boolean") && p.omitDefault !== void 0 && !isBool(p.omitDefault)) err(`${at}.omitDefault`, "must be a boolean");
365
+ }
366
+ function isCodeConnectMap(x) {
367
+ return validateMap(x).ok;
368
+ }
369
+ isCodeConnectMap.expected = "a valid component map (the map-validate script lists what is wrong with it)";
370
+
371
+ // design-to-code/map-validate.ts
372
+ function main(argv) {
373
+ const file = argv[0];
374
+ const USAGE = `usage: ${scriptCmd("map-validate")} <map.json>`;
375
+ if (file === "--help" || file === "-h") {
376
+ console.log(USAGE);
377
+ return 0;
378
+ }
379
+ if (!file || file.startsWith("-")) {
380
+ console.error((file ? `map-validate: unknown flag ${file}
381
+ ` : "") + USAGE);
382
+ return 1;
383
+ }
384
+ const res = validateMap(readJsonFile(file, "component map"));
385
+ if (res.ok) {
386
+ console.log("map valid");
387
+ return 0;
388
+ }
389
+ res.errors.forEach((e) => console.error(` ${e.path || "(root)"}: ${e.message}`));
390
+ console.error(`
391
+ ${res.errors.length} error(s)`);
392
+ return 1;
393
+ }
394
+ if (import.meta.main) runCli(main, { name: "map-validate" });
395
+ export {
396
+ main
397
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design-twin/dtwin",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0",
4
4
  "description": "Design Twin bridge: local CLI + MCP server between Claude Code and the Design Twin Figma plugin. Reads a design on the free Figma plan, fully offline.",
5
5
  "license": "MIT",
6
6
  "author": "Mohamed Magdy Omar",
@@ -31,11 +31,15 @@
31
31
  },
32
32
  "files": [
33
33
  "src",
34
+ "layer",
35
+ "figma-plugin",
34
36
  "README.md",
35
37
  "LICENSE"
36
38
  ],
37
39
  "scripts": {
38
40
  "typecheck": "tsc --noEmit",
41
+ "prepack": "bun --no-install --no-env-file pack.ts",
42
+ "postpack": "bun --no-install --no-env-file pack.ts --clean",
39
43
  "pull": "bun src/figma-pull.ts",
40
44
  "pull:selection": "bun src/figma-pull.ts design --selection",
41
45
  "mcp": "bun src/figma-mcp.ts"
package/src/figma-mcp.ts CHANGED
@@ -752,14 +752,16 @@ server.registerTool(
752
752
  // — without these the agent has the figma_* tools and no way in, and the map and the codegen skill drift
753
753
  // into two different formats. Registering them here is what makes that layer exist for its actual
754
754
  // consumer.
755
- // Loaded LAZILY, on call, and never at module scope. ../../design-to-code/ is a SIBLING of this package
756
- // root (bridge/), so npm — whose `files` cannot reach outside the package root — does not ship it.
757
- // Importing it eagerly would take the whole server down with a module-not-found on a `bun add -g @design-twin/dtwin`
758
- // install, killing the other twelve tools over two that merely cannot work. So the server always
759
- // starts and only these two report, precisely, that they need the repo checkout.
755
+ // Loaded LAZILY, on call, and never at module scope, from one of two places. In a repo checkout it is
756
+ // ../../design-to-code/<mod>.ts, a SIBLING of this package root (bridge/) — which npm's `files` cannot reach. So
757
+ // the npm package carries the layer as bundles instead: `prepack` (bridge/pack.ts) copies the committed,
758
+ // self-contained claude-plugin/scripts/<mod>.js into ../layer/. Neither there (a checkout without design-to-code/,
759
+ // a package packed without its prepack): importing eagerly would take the whole server down with a
760
+ // module-not-found, killing the other tools over two that merely cannot work — so the server always starts and only
761
+ // these two report, precisely, what is missing.
760
762
  // The slice of each layer module this server calls, declared HERE (over doc-types.ts shapes) rather
761
763
  // than as `typeof import("../../design-to-code/x.ts")`: even a type-only reference pulls that file and
762
- // its imports into the bridge program, and the published package (src/ alone) would then name files it
764
+ // its imports into the bridge program, and the published package (src/ is TypeScript, the layer only bundles) would name files it
763
765
  // does not ship (see doc-types.ts). So bridge/src has no dependency of any kind on the layer — only the
764
766
  // dynamic import below, which fails soft. Mirrors design-to-code/get-component.ts GetComponentResult and
765
767
  // design-to-code/drift-lint.ts runDriftLint(); keep them in step (test/mcp-smoke.test.ts checks the real modules still satisfy these at compile time).
@@ -775,27 +777,27 @@ export interface DriftLintModule {
775
777
  | { status: "linted"; errors: unknown[]; warnings: unknown[]; freshness: unknown; summary: unknown; catalogs: ReadonlyArray<{ file: string }>; coverage: { coverage: object; warnings: unknown[] } | null };
776
778
  }
777
779
  async function loadLayer<T>(mod: string, tool: string): Promise<T> {
778
- // The layer sits beside bridge/ in a repo checkout and nowhere in the npm package. Whether it is there
780
+ // The source in a checkout first (what the repo's tests run), else the package's bundle. Whether either is there
779
781
  // is asked of the file system, before the import: the import's own failure cannot say it — a missing
780
782
  // dependency OF the layer fails the same way, and the error's code and url fields differ between
781
783
  // runtimes (Bun reports neither the way Node did). Any error from an import of a file that IS there
782
784
  // is passed on as it is.
783
- const url = new URL("../../design-to-code/" + mod, import.meta.url);
784
- if (!fs.existsSync(url)) {
785
+ const url = [new URL(`../../design-to-code/${mod}.ts`, import.meta.url), new URL(`../layer/${mod}.js`, import.meta.url)].find((u) => fs.existsSync(u));
786
+ if (!url) {
785
787
  throw new Error(
786
- `${tool} needs the design-to-code layer, which is not present in this install. It ships with the ` +
787
- `Design Twin repository, not with the published npm package — run this MCP server from a repo ` +
788
- `checkout (bun bridge/src/figma-mcp.ts) to use it. Everything else on this server works either way.`
788
+ `${tool} needs the design-to-code layer, which is not present in this install: neither a repo checkout's ` +
789
+ `design-to-code/${mod}.ts nor the npm package's layer/${mod}.js. Reinstall @design-twin/dtwin, or run this MCP ` +
790
+ `server from a repo checkout (bun bridge/src/figma-mcp.ts). Everything else on this server works either way.`
789
791
  );
790
792
  }
791
793
  return (await import(url.href)) as T;
792
794
  }
793
795
  const getComponent = async (catalogFile: string, handle: string) =>
794
- (await loadLayer<GetComponentModule>("get-component.ts", "design_get_component")).getComponent(
796
+ (await loadLayer<GetComponentModule>("get-component", "design_get_component")).getComponent(
795
797
  catalogFile,
796
798
  handle
797
799
  );
798
- const driftLintLayer = () => loadLayer<DriftLintModule>("drift-lint.ts", "design_drift_lint");
800
+ const driftLintLayer = () => loadLayer<DriftLintModule>("drift-lint", "design_drift_lint");
799
801
 
800
802
  // The export's component catalog is export-reader.ts readComponentCatalog's: the `files.componentsLocal` pointer
801
803
  // (inside the export only), else an older export's inline components[] in design-system.json — the catalog `dtwin
package/src/init.ts CHANGED
@@ -119,7 +119,7 @@ function bunxCacheOf(dir: string): string | null {
119
119
  }
120
120
  const bunxRefusal = (cache: string): string =>
121
121
  `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";
122
+ "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
123
 
124
124
  /** How to look at the machine (seams for the suites): does a path exist, and what does a bare command resolve to. */
125
125
  export interface EntryProbe {
@@ -371,14 +371,14 @@ function main(argv: string[]): void {
371
371
  // Run through `bunx @design-twin/dtwin init`: this very file is in bunx's cache, so no entry may point at it.
372
372
  const cache = bunxCacheOf(import.meta.dirname);
373
373
  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`);
374
+ 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
375
  const actions = plan(cwd, { mcp: argv.includes("--mcp"), mcpEntry, ...ifDefined("refuseMcp", refuseMcp), token });
376
376
  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
377
  else apply(cwd, actions, log);
378
378
 
379
- // The plugin manifest of a repo checkout: bridge/src/ -> ../../figma-plugin/. A package
380
- // install has no figma-plugin/ beside it, so the step below falls back to naming the repo instead.
381
- const manifest = path.join(import.meta.dirname, "..", "..", "figma-plugin", "manifest.json");
379
+ // The Figma plugin's manifest: a repo checkout's (bridge/src/ -> ../../figma-plugin/), else the copy the npm
380
+ // package carries (../figma-plugin/, put there by bridge/pack.ts). Neither: the step names the repo instead.
381
+ const manifest = [path.join(import.meta.dirname, "..", "..", "figma-plugin", "manifest.json"), path.join(import.meta.dirname, "..", "figma-plugin", "manifest.json")].find((m) => fs.existsSync(m));
382
382
  // Step 3 is already done for anyone who reached init THROUGH the installed plugin's help skill —
383
383
  // which is the documented path. Claude Code records installed plugins under ~/.claude; when the
384
384
  // marker is there, saying "install the plugin" is noise at best and confusing at worst.
@@ -400,7 +400,7 @@ function main(argv: string[]): void {
400
400
  })();
401
401
 
402
402
  const steps = [
403
- "In Figma DESKTOP: Plugins → Development → Import plugin from manifest… → " + (fs.existsSync(manifest) ? manifest : "figma-plugin/manifest.json from a clone of the Design Twin repo (the npm package does not include it)"),
403
+ "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
404
  "Run the plugin (Plugins → Development → Design Twin), paste the bridge token into its \"Bridge token\" field, Save: dtwin --show-token | pbcopy",
405
405
  ];
406
406
  if (!pluginInstalled) {