inkbridge 0.1.0-beta.2 → 0.1.0-beta.21

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.
Files changed (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
package/bin/inkbridge.mjs CHANGED
@@ -7,8 +7,9 @@ import { fileURLToPath } from "url";
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const PACKAGE_DIR = join(__dirname, "..");
9
9
 
10
- // When running via postinstall, INIT_CWD is the consumer project root.
11
- // Fall back to process.cwd() for direct invocations.
10
+ // `INIT_CWD` is set by package managers to the project where the user
11
+ // invoked the command (vs `process.cwd()` which can be `node_modules/inkbridge`
12
+ // when called via `pnpm exec`). Fall back for direct node invocations.
12
13
  const PROJECT_ROOT = process.env.INIT_CWD || process.cwd();
13
14
 
14
15
  const [, , command = "help"] = process.argv;
@@ -39,106 +40,306 @@ function detectStorybookPaths(root) {
39
40
  }
40
41
 
41
42
  // ---------------------------------------------------------------------------
42
- // postinstall called automatically after `pnpm add -D inkbridge`
43
+ // Patch next.config.* to allow the Figma plugin UI (null origin) to fetch
44
+ // assets from the dev server. Without this, <Image> srcs, SVGs, etc. fail CORS.
43
45
  // ---------------------------------------------------------------------------
44
- async function postinstall() {
45
- const pkg = JSON.parse(await readFile(join(PACKAGE_DIR, "package.json"), "utf8"));
46
- const manifestPath = join(PROJECT_ROOT, "node_modules/inkbridge/manifest.json");
46
+ async function patchNextConfig(root) {
47
+ const candidates = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
48
+ let configPath = null;
49
+ for (const candidate of candidates) {
50
+ const full = join(root, candidate);
51
+ if (existsSync(full)) { configPath = full; break; }
52
+ }
53
+ if (!configPath) {
54
+ console.log(" ~ next.config not found — skipping CORS headers patch");
55
+ return;
56
+ }
47
57
 
48
- console.log("");
49
- console.log(` ✓ inkbridge v${pkg.version} installed`);
50
- console.log("");
51
- console.log(" Next: run setup to wire up the scanner endpoint and scripts:");
52
- console.log(" pnpm exec inkbridge setup");
53
- console.log("");
54
- console.log(" Then load the plugin in Figma Desktop:");
55
- console.log(" Plugins Development Import plugin from manifest");
56
- console.log(` ${manifestPath}`);
57
- console.log("");
58
+ const source = await readFile(configPath, "utf8");
59
+ if (source.includes("Access-Control-Allow-Origin")) {
60
+ console.log(" ~ next.config already exposes CORS headers, skipping");
61
+ return;
62
+ }
63
+
64
+ const headersBlock = `
65
+ // Allow the Figma plugin UI iframe (null origin) to read the dev
66
+ // server. Two scopes:
67
+ // - /api/inkbridge/* gets full CORS (POST/OPTIONS) for the token
68
+ // patch route. The inkbridge API routes also set per-response
69
+ // CORS headers, so this is mostly belt-and-suspenders for
70
+ // CORS preflight handling.
71
+ // - /:path* gets GET-only CORS so the plugin can fetch the
72
+ // static assets your components reference (e.g.
73
+ // <Image src="/assets/logo.svg">). GET-only on all paths means
74
+ // other origins can READ your local dev server — that's fine
75
+ // for design-system assets and matches what every Storybook
76
+ // and Next.js dev server does in practice.
77
+ async headers() {
78
+ return [
79
+ {
80
+ source: "/api/inkbridge/:path*",
81
+ headers: [
82
+ { key: "Access-Control-Allow-Origin", value: "*" },
83
+ { key: "Access-Control-Allow-Methods", value: "GET, POST, OPTIONS" },
84
+ { key: "Access-Control-Allow-Headers", value: "Content-Type" },
85
+ ],
86
+ },
87
+ {
88
+ source: "/:path*",
89
+ headers: [
90
+ { key: "Access-Control-Allow-Origin", value: "*" },
91
+ { key: "Access-Control-Allow-Methods", value: "GET" },
92
+ ],
93
+ },
94
+ ];
95
+ },`;
96
+
97
+ // Inject right after the opening `{` of the config object. Matches either
98
+ // `const nextConfig: NextConfig = {` or `const nextConfig = {` or `module.exports = {`.
99
+ const patterns = [
100
+ /(const\s+\w+\s*(?::\s*\w+\s*)?=\s*)\{/,
101
+ /(module\.exports\s*=\s*)\{/,
102
+ /(export\s+default\s*)\{/,
103
+ ];
104
+ let patched = null;
105
+ for (const re of patterns) {
106
+ const match = source.match(re);
107
+ if (!match) continue;
108
+ const idx = match.index + match[0].length;
109
+ patched = source.slice(0, idx) + headersBlock + source.slice(idx);
110
+ break;
111
+ }
112
+
113
+ if (!patched) {
114
+ console.log(" ! could not locate config object in " + configPath + " — add CORS headers manually");
115
+ return;
116
+ }
117
+
118
+ await writeFile(configPath, patched, "utf8");
119
+ console.log(" ✓ patched " + configPath.replace(root + "/", "") + " with CORS headers");
58
120
  }
59
121
 
60
122
  // ---------------------------------------------------------------------------
61
123
  // setup — patches package.json + creates scanner route
62
124
  // ---------------------------------------------------------------------------
125
+ function detectEnvironment(root) {
126
+ // Read consumer package.json to detect Next.js, Tailwind, Storybook.
127
+ let pkg = null;
128
+ try { pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); } catch (_e) {}
129
+ const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
130
+ const nextConfigCandidates = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
131
+ const nextConfig = nextConfigCandidates.find(f => existsSync(join(root, f))) || null;
132
+ const storybookMain = ["main.ts", "main.js", "main.mjs", "main.cjs"]
133
+ .map(f => `.storybook/${f}`)
134
+ .find(p => existsSync(join(root, p))) || null;
135
+ const storybookPaths = storybookMain ? detectStorybookPaths(root) : [];
136
+ return {
137
+ nextVersion: deps.next || null,
138
+ nextConfig,
139
+ tailwindVersion: deps.tailwindcss || null,
140
+ storybookMain,
141
+ storybookPaths,
142
+ componentPaths: storybookPaths.length > 0 ? storybookPaths : ["src"],
143
+ };
144
+ }
145
+
146
+ function statusGlyph(present) { return present ? "✓" : "✗"; }
147
+
148
+ function printEnvironment(env) {
149
+ console.log(" Detected:");
150
+ console.log(` ${statusGlyph(!!env.nextVersion)} Next.js ${env.nextVersion || "(missing — install `next` first)"}`);
151
+ console.log(` ${statusGlyph(!!env.nextConfig)} next.config ${env.nextConfig || "(missing)"}`);
152
+ console.log(` ${statusGlyph(!!env.tailwindVersion)} Tailwind CSS ${env.tailwindVersion || "(missing — install `tailwindcss` first)"}`);
153
+ console.log(` ${statusGlyph(!!env.storybookMain)} Storybook config ${env.storybookMain || "(none — design generation needs .stories.tsx files to scan)"}`);
154
+ console.log(` • Component paths ${env.componentPaths.join(", ")}${env.storybookPaths.length === 0 ? " (default — edit inkbridge.config.json to customise)" : ""}`);
155
+ console.log("");
156
+ }
157
+
63
158
  async function setup() {
64
- const scanRouteDest = join(PROJECT_ROOT, "src/app/api/figma/scan-components/route.ts");
159
+ const dryRun = process.argv.includes("--dry-run");
160
+ // `--force` re-creates the inkbridge-managed files (route templates,
161
+ // inkbridge.config.json) even when they already exist. Useful when a
162
+ // newer plugin version ships an updated template (e.g. the
163
+ // INKBRIDGE_LOCAL check in the scanner route) and the consumer wants
164
+ // to refresh without hand-editing. Never touches package.json or
165
+ // next.config — those are partial-edit territory and force-rewriting
166
+ // them would clobber consumer state.
167
+ const force = process.argv.includes("--force");
168
+ const scanRouteDest = join(PROJECT_ROOT, "src/app/api/inkbridge/scan-components/route.ts");
65
169
  const scanRouteSrc = join(PACKAGE_DIR, "templates/scan-components-route.ts");
66
- const patchRouteDest = join(PROJECT_ROOT, "src/app/api/figma/patch-tokens/route.ts");
170
+ const patchRouteDest = join(PROJECT_ROOT, "src/app/api/inkbridge/patch-tokens/route.ts");
67
171
  const patchRouteSrc = join(PACKAGE_DIR, "templates/patch-tokens-route.ts");
68
172
  const pkgPath = join(PROJECT_ROOT, "package.json");
69
-
70
- // 1. Create inkbridge.config.json
71
173
  const inkbridgeCfgPath = join(PROJECT_ROOT, "inkbridge.config.json");
72
- if (existsSync(inkbridgeCfgPath)) {
73
- console.log(" ~ inkbridge.config.json already exists, skipping");
74
- } else {
75
- const detectedPaths = detectStorybookPaths(PROJECT_ROOT);
76
- const componentPaths = detectedPaths.length > 0 ? detectedPaths : ["src"];
77
- const cfg = {
78
- componentPaths,
79
- exclude: [],
80
- onlyWithStories: true,
81
- };
82
- await writeFile(inkbridgeCfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
83
- if (detectedPaths.length > 0) {
84
- console.log(` ✓ created inkbridge.config.json (detected paths: ${detectedPaths.join(", ")})`);
85
- } else {
86
- console.log(" ✓ created inkbridge.config.json (defaulting to src/ — edit to customise)");
87
- }
174
+
175
+ console.log("");
176
+ console.log(` inkbridge setup${dryRun ? " — dry run (no files will be written)" : ""}${force ? " — force (overwrite existing inkbridge files)" : ""}`);
177
+ console.log("");
178
+
179
+ const env = detectEnvironment(PROJECT_ROOT);
180
+ printEnvironment(env);
181
+
182
+ // ------------------------------------------------------------------
183
+ // Plan phase: figure out which writes are needed.
184
+ // ------------------------------------------------------------------
185
+ const plan = [];
186
+
187
+ if (!existsSync(inkbridgeCfgPath) || force) {
188
+ plan.push({
189
+ kind: existsSync(inkbridgeCfgPath) ? "overwrite" : "create",
190
+ path: "inkbridge.config.json",
191
+ detail: `componentPaths: [${env.componentPaths.join(", ")}]`,
192
+ });
193
+ }
194
+ if (!existsSync(scanRouteDest) || force) {
195
+ plan.push({
196
+ kind: existsSync(scanRouteDest) ? "overwrite" : "create",
197
+ path: "src/app/api/inkbridge/scan-components/route.ts",
198
+ });
199
+ }
200
+ if (!existsSync(patchRouteDest) || force) {
201
+ plan.push({
202
+ kind: existsSync(patchRouteDest) ? "overwrite" : "create",
203
+ path: "src/app/api/inkbridge/patch-tokens/route.ts",
204
+ });
88
205
  }
89
206
 
90
- // 3. Copy scanner route
91
- if (existsSync(scanRouteDest)) {
92
- console.log(" ~ scanner route already exists, skipping: src/app/api/figma/scan-components/route.ts");
93
- } else {
94
- await mkdir(dirname(scanRouteDest), { recursive: true });
95
- await copyFile(scanRouteSrc, scanRouteDest);
96
- console.log(" created src/app/api/figma/scan-components/route.ts");
97
- }
98
-
99
- if (existsSync(patchRouteDest)) {
100
- console.log(" ~ token patch route already exists, skipping: src/app/api/figma/patch-tokens/route.ts");
101
- } else {
102
- await mkdir(dirname(patchRouteDest), { recursive: true });
103
- await copyFile(patchRouteSrc, patchRouteDest);
104
- console.log(" ✓ created src/app/api/figma/patch-tokens/route.ts");
105
- }
106
-
107
- // 4. Patch package.json scripts
108
- if (!existsSync(pkgPath)) {
109
- console.warn(" ! package.json not found at project root skipping script injection");
110
- } else {
111
- const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
112
- pkg.scripts = pkg.scripts || {};
113
- const toAdd = {
114
- "figma:dev": "next dev",
115
- "figma:scan": "tsx node_modules/inkbridge/scanner/cli.ts",
116
- };
117
- const added = [];
118
- for (const [k, v] of Object.entries(toAdd)) {
119
- if (!pkg.scripts[k]) {
120
- pkg.scripts[k] = v;
121
- added.push(k);
122
- }
123
- }
124
- if (added.length > 0) {
207
+ let pkg = null;
208
+ if (existsSync(pkgPath)) {
209
+ try { pkg = JSON.parse(readFileSync(pkgPath, "utf8")); } catch (_e) {}
210
+ }
211
+ const scriptsToAdd = {
212
+ "inkbridge:dev": "next dev",
213
+ "inkbridge:scan": "tsx node_modules/inkbridge/scanner/cli.ts",
214
+ };
215
+ const pkgScriptAdditions = pkg
216
+ ? Object.entries(scriptsToAdd).filter(([k]) => !(pkg.scripts && pkg.scripts[k]))
217
+ : [];
218
+ if (pkgScriptAdditions.length > 0) {
219
+ plan.push({
220
+ kind: "modify",
221
+ path: "package.json",
222
+ detail: pkgScriptAdditions.map(([k, v]) => `scripts.${k} = "${v}"`).join("; "),
223
+ });
224
+ }
225
+
226
+ const nextConfigSource = env.nextConfig ? await readFile(join(PROJECT_ROOT, env.nextConfig), "utf8") : null;
227
+ const nextConfigNeedsPatch = !!env.nextConfig && !nextConfigSource.includes("Access-Control-Allow-Origin");
228
+ if (nextConfigNeedsPatch) {
229
+ plan.push({
230
+ kind: "modify",
231
+ path: env.nextConfig,
232
+ detail: "headers() → Access-Control-Allow-* on /api/inkbridge/:path*",
233
+ });
234
+ }
235
+
236
+ // Make sure the scanner-output directory is gitignored. The API
237
+ // route writes `.inkbridge/component-definitions.json` on every
238
+ // plugin run; without this entry consumers see the file in their
239
+ // diff after every `pnpm inkbridge:scan`.
240
+ const gitignorePath = join(PROJECT_ROOT, ".gitignore");
241
+ const gitignoreSource = existsSync(gitignorePath)
242
+ ? readFileSync(gitignorePath, "utf8")
243
+ : "";
244
+ const gitignoreNeedsPatch = existsSync(join(PROJECT_ROOT, ".git"))
245
+ && !gitignoreSource.split("\n").some(line => {
246
+ const trimmed = line.trim();
247
+ return trimmed === ".inkbridge" || trimmed === ".inkbridge/";
248
+ });
249
+ if (gitignoreNeedsPatch) {
250
+ plan.push({
251
+ kind: "modify",
252
+ path: ".gitignore",
253
+ detail: "+ .inkbridge/ (scanner output)",
254
+ });
255
+ }
256
+
257
+ if (plan.length === 0) {
258
+ console.log(" Nothing to do — setup is already complete.");
259
+ console.log("");
260
+ printPostSetupHint();
261
+ return;
262
+ }
263
+
264
+ console.log(" Setup will make these changes:");
265
+ for (const item of plan) {
266
+ const prefix = item.kind === "create"
267
+ ? "+ create"
268
+ : item.kind === "overwrite"
269
+ ? "↻ overwrite"
270
+ : "~ modify";
271
+ console.log(` ${prefix} ${item.path}${item.detail ? ` (${item.detail})` : ""}`);
272
+ }
273
+ console.log("");
274
+
275
+ if (dryRun) {
276
+ console.log(" Dry run — no changes written. Re-run without --dry-run to apply.");
277
+ console.log("");
278
+ return;
279
+ }
280
+
281
+ // ------------------------------------------------------------------
282
+ // Apply phase.
283
+ // ------------------------------------------------------------------
284
+ for (const item of plan) {
285
+ if (item.path === "inkbridge.config.json") {
286
+ const cfg = {
287
+ componentPaths: env.componentPaths,
288
+ exclude: [],
289
+ onlyWithStories: true,
290
+ };
291
+ await writeFile(inkbridgeCfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
292
+ } else if (item.path === "src/app/api/inkbridge/scan-components/route.ts") {
293
+ await mkdir(dirname(scanRouteDest), { recursive: true });
294
+ await copyFile(scanRouteSrc, scanRouteDest);
295
+ } else if (item.path === "src/app/api/inkbridge/patch-tokens/route.ts") {
296
+ await mkdir(dirname(patchRouteDest), { recursive: true });
297
+ await copyFile(patchRouteSrc, patchRouteDest);
298
+ } else if (item.path === "package.json") {
299
+ pkg.scripts = pkg.scripts || {};
300
+ for (const [k, v] of pkgScriptAdditions) pkg.scripts[k] = v;
125
301
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
126
- for (const k of added) console.log(` ✓ added script: ${k}`);
127
- } else {
128
- console.log(" ~ scripts already present, skipping");
302
+ } else if (env.nextConfig && item.path === env.nextConfig) {
303
+ await patchNextConfig(PROJECT_ROOT);
304
+ continue; // patchNextConfig already prints
305
+ } else if (item.path === ".gitignore") {
306
+ const existing = existsSync(gitignorePath)
307
+ ? readFileSync(gitignorePath, "utf8")
308
+ : "";
309
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
310
+ const block = (needsLeadingNewline ? "\n" : "")
311
+ + "\n# Inkbridge scanner output (regenerated on every plugin run)\n.inkbridge/\n";
312
+ await writeFile(gitignorePath, existing + block, "utf8");
129
313
  }
314
+ const prefix = item.kind === "create"
315
+ ? "✓ created"
316
+ : item.kind === "overwrite"
317
+ ? "✓ overwrote"
318
+ : "✓ modified";
319
+ console.log(` ${prefix} ${item.path}`);
130
320
  }
131
321
 
132
- // 5. Print manifest path
322
+ console.log("");
323
+ printPostSetupHint();
324
+ }
325
+
326
+ function printPostSetupHint() {
133
327
  const manifestPath = resolve(PROJECT_ROOT, "node_modules/inkbridge/manifest.json");
328
+ console.log(" About the routes:");
329
+ console.log(" src/app/api/inkbridge/scan-components/route.ts");
330
+ console.log(" runs the scanner and returns your component definitions");
331
+ console.log(" (the plugin calls this every time you click Generate).");
332
+ console.log(" src/app/api/inkbridge/patch-tokens/route.ts");
333
+ console.log(" writes Figma-edited tokens back to your CSS source on Push.");
334
+ console.log(" Don't move or delete these — the plugin won't work without them.");
134
335
  console.log("");
135
- console.log(" Setup complete. Load the plugin in Figma Desktop:");
336
+ console.log(" Load the plugin in Figma Desktop:");
136
337
  console.log(" Plugins → Development → Import plugin from manifest");
137
338
  console.log(` ${manifestPath}`);
138
339
  console.log("");
139
340
  console.log(" Start developing:");
140
- console.log(" pnpm figma:dev (starts Next.js dev server for scan + token patch routes)");
141
- console.log(" pnpm figma:scan (manually re-scan components)");
341
+ console.log(" pnpm inkbridge:dev (Next.js dev server for the scan + token patch routes)");
342
+ console.log(" pnpm inkbridge:scan (manually re-scan components)");
142
343
  console.log("");
143
344
  }
144
345
 
@@ -150,22 +351,92 @@ function printPath() {
150
351
  console.log(manifestPath);
151
352
  }
152
353
 
354
+ // ---------------------------------------------------------------------------
355
+ // skill — install the Inkbridge AI skill into the consuming project's
356
+ // .claude/skills/ folder. Maintainer-only convenience: lets developers who
357
+ // extend the plugin (us, the starter, future internal projects) get the
358
+ // canonical agent context without copy-pasting Markdown around.
359
+ //
360
+ // Two modes, picked automatically:
361
+ // - **Sibling mode**: if `../inkbridge/.claude/skills/inkbridge-figma-plugin`
362
+ // exists relative to PROJECT_ROOT, create a symlink. Edits in inkbridge
363
+ // propagate immediately. This is what the starter / co-located projects
364
+ // should use.
365
+ // - **Fetch mode**: otherwise, clone the skill folder from
366
+ // github.com/inkn9ne/inkbridge via degit. Snapshot install — re-run to
367
+ // update. This is what non-co-located projects should use.
368
+ // ---------------------------------------------------------------------------
369
+ import { symlink, rm } from "fs/promises";
370
+
371
+ const SKILL_REL_PATH = ".claude/skills/inkbridge-figma-plugin";
372
+
373
+ async function installSkill() {
374
+ const target = join(PROJECT_ROOT, SKILL_REL_PATH);
375
+ const siblingSource = resolve(PROJECT_ROOT, "..", "inkbridge", SKILL_REL_PATH);
376
+
377
+ await mkdir(dirname(target), { recursive: true });
378
+
379
+ if (existsSync(siblingSource)) {
380
+ // Sibling mode — symlink for live updates from the inkbridge repo
381
+ if (existsSync(target)) {
382
+ console.log(` ~ ${SKILL_REL_PATH} already exists — removing to relink`);
383
+ await rm(target, { recursive: true, force: true });
384
+ }
385
+ const relativeSource = resolve(siblingSource);
386
+ await symlink(relativeSource, target, "dir");
387
+ console.log(` ✓ symlinked ${SKILL_REL_PATH} → ${relativeSource}`);
388
+ console.log("");
389
+ console.log(" Sibling-mode install. Edits in the inkbridge repo's");
390
+ console.log(" .claude/skills/inkbridge-figma-plugin/ will propagate here");
391
+ console.log(" immediately. Reload Claude Code to pick up the skill.");
392
+ return;
393
+ }
394
+
395
+ // Fetch mode — pull the current main from GitHub via degit
396
+ if (existsSync(target)) {
397
+ console.log(` ~ ${SKILL_REL_PATH} already exists — re-fetching to update`);
398
+ await rm(target, { recursive: true, force: true });
399
+ }
400
+ const { spawn } = await import("child_process");
401
+ const args = ["degit", "inkn9ne/inkbridge/.claude/skills/inkbridge-figma-plugin", target];
402
+ console.log(` ↓ npx ${args.join(" ")}`);
403
+ await new Promise((resolve, reject) => {
404
+ const child = spawn("npx", args, { stdio: "inherit" });
405
+ child.on("exit", code => (code === 0 ? resolve() : reject(new Error(`degit exited ${code}`))));
406
+ child.on("error", reject);
407
+ });
408
+ console.log(` ✓ installed ${SKILL_REL_PATH} from inkn9ne/inkbridge`);
409
+ console.log("");
410
+ console.log(" Fetch-mode install (snapshot from main). Re-run");
411
+ console.log(" `pnpm exec inkbridge skill install` to update.");
412
+ }
413
+
153
414
  // ---------------------------------------------------------------------------
154
415
  // dispatch
155
416
  // ---------------------------------------------------------------------------
156
417
  switch (command) {
157
- case "postinstall":
158
- await postinstall();
159
- break;
160
418
  case "setup":
161
419
  await setup();
162
420
  break;
163
421
  case "path":
164
422
  printPath();
165
423
  break;
424
+ case "skill": {
425
+ const sub = process.argv[3];
426
+ if (sub === "install") {
427
+ await installSkill();
428
+ } else {
429
+ console.log("Usage:");
430
+ console.log(" inkbridge skill install Install the AI skill into .claude/skills/");
431
+ console.log(" (symlinks to ../inkbridge if co-located,");
432
+ console.log(" otherwise fetches via degit from GitHub)");
433
+ }
434
+ break;
435
+ }
166
436
  default:
167
437
  console.log("Usage:");
168
- console.log(" inkbridge setup Wire up scanner/token-patch routes + scripts (run once after install)");
169
- console.log(" inkbridge path Print the manifest.json path for Figma Desktop");
438
+ console.log(" inkbridge setup Wire up scanner/token-patch routes + scripts (run once after install)");
439
+ console.log(" inkbridge path Print the manifest.json path for Figma Desktop");
440
+ console.log(" inkbridge skill install Install the AI development skill (maintainers only)");
170
441
  break;
171
442
  }