@grampro/kit 2.4.3 → 2.5.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.md +12 -0
  3. package/index.cjs +44 -2
  4. package/package.json +1 -1
  5. package/source/components/accordion/passport.json +11 -7
  6. package/source/components/accordion/react/Accordion.tsx +5 -0
  7. package/source/components/alert/passport.json +3 -3
  8. package/source/components/avatar/passport.json +8 -6
  9. package/source/components/avatar/react/Avatar.tsx +4 -0
  10. package/source/components/badge/passport.json +8 -6
  11. package/source/components/badge/react/Badge.tsx +1 -0
  12. package/source/components/badge/react/Tag.tsx +1 -0
  13. package/source/components/breadcrumb/passport.json +8 -6
  14. package/source/components/breadcrumb/react/Breadcrumb.tsx +1 -0
  15. package/source/components/button/passport.json +8 -6
  16. package/source/components/button/react/Button.tsx +1 -0
  17. package/source/components/card/passport.json +17 -9
  18. package/source/components/card/react/Card.tsx +5 -0
  19. package/source/components/card/react/Stat.tsx +5 -0
  20. package/source/components/checkbox/passport.json +32 -14
  21. package/source/components/checkbox/react/Checkbox.tsx +17 -0
  22. package/source/components/checkbox/react/CheckboxGroup.tsx +6 -0
  23. package/source/components/combobox/passport.json +36 -16
  24. package/source/components/combobox/react/Select.tsx +6 -1
  25. package/source/components/combobox/react/props.ts +23 -0
  26. package/source/components/data-grid/core/types.ts +53 -0
  27. package/source/components/data-grid/passport.json +82 -30
  28. package/source/components/data-grid/react/AskGrid.tsx +10 -0
  29. package/source/components/data-grid/react/DataGrid.tsx +16 -0
  30. package/source/components/date-picker/passport.json +30 -14
  31. package/source/components/date-picker/react/DatePicker.tsx +5 -0
  32. package/source/components/date-picker/react/props.ts +13 -1
  33. package/source/components/dialog/core/types.ts +14 -0
  34. package/source/components/dialog/passport.json +26 -12
  35. package/source/components/dialog/react/Dialog.tsx +4 -0
  36. package/source/components/file-uploader/passport.json +44 -18
  37. package/source/components/file-uploader/react/FileUploader.tsx +39 -0
  38. package/source/components/input/passport.json +14 -8
  39. package/source/components/input/react/Input.tsx +3 -0
  40. package/source/components/menu/passport.json +29 -13
  41. package/source/components/menu/react/Menu.tsx +7 -0
  42. package/source/components/menu/react/items.tsx +4 -0
  43. package/source/components/modal/passport.json +14 -8
  44. package/source/components/modal/react/Modal.tsx +6 -0
  45. package/source/components/number-input/passport.json +23 -11
  46. package/source/components/number-input/react/NumberInput.tsx +13 -0
  47. package/source/components/popover/passport.json +17 -9
  48. package/source/components/popover/react/Popover.tsx +7 -0
  49. package/source/components/progress/passport.json +3 -3
  50. package/source/components/radio-group/passport.json +32 -14
  51. package/source/components/radio-group/react/Radio.tsx +17 -0
  52. package/source/components/radio-group/react/RadioGroup.tsx +13 -0
  53. package/source/components/skeleton/passport.json +8 -6
  54. package/source/components/skeleton/react/Skeleton.tsx +5 -0
  55. package/source/components/spinner/passport.json +3 -3
  56. package/source/components/switch/passport.json +23 -11
  57. package/source/components/switch/react/Switch.tsx +14 -0
  58. package/source/components/tabs/passport.json +11 -7
  59. package/source/components/tabs/react/Tabs.tsx +2 -0
  60. package/source/components/textarea/passport.json +14 -8
  61. package/source/components/textarea/react/Textarea.tsx +3 -0
  62. package/source/components/toaster/passport.json +8 -6
  63. package/source/components/toaster/react/Toaster.tsx +4 -0
  64. package/source/components/tooltip/passport.json +3 -3
  65. package/tools/mcp/README.md +224 -0
  66. package/tools/mcp/figma.cjs +445 -0
  67. package/tools/mcp/install.cjs +270 -0
  68. package/tools/mcp/secret-cli.cjs +174 -0
  69. package/tools/mcp/secret.cjs +289 -0
  70. package/tools/mcp/server.cjs +497 -0
  71. package/tools/passport/cli.cjs +8 -1
  72. package/tools/passport/extract.cjs +7 -3
  73. package/tools/passport/merge.cjs +24 -4
@@ -0,0 +1,270 @@
1
+ /*
2
+ * `grampro-kit mcp install` — register the server with whichever agent you use.
3
+ *
4
+ * This exists because the configuration is three lines and still goes wrong.
5
+ * Hand-written config files pick up a byte-order mark from PowerShell's
6
+ * `Out-File`, which every JSON parser rejects with an error that names the
7
+ * first character and explains nothing. Writing the file from here means it
8
+ * is UTF-8 without a BOM, with LF endings, every time.
9
+ *
10
+ * Each agent is given the same stdio server; only the file format differs.
11
+ */
12
+ "use strict";
13
+
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+
18
+ const PACKAGE = "@grampro/kit";
19
+ const SERVER = "grampro";
20
+
21
+ /*
22
+ * Where the project starts, which is not necessarily where you ran this.
23
+ *
24
+ * An agent looks for `.mcp.json` at the root of the folder it opened. Writing
25
+ * to the working directory means that running this from `src/` puts the
26
+ * configuration somewhere nothing will ever read, and the command still
27
+ * reports success — the one way this can quietly do nothing.
28
+ *
29
+ * The nearest marker of either kind wins, rather than `.git` in preference to
30
+ * `package.json`. Someone who opens one workspace of a monorepo should get
31
+ * that workspace, and searching for `.git` first would walk straight past it
32
+ * and write above the folder their agent has open.
33
+ */
34
+ function projectRoot(from) {
35
+ let dir = path.resolve(from);
36
+ for (;;) {
37
+ for (const mark of [".git", "package.json"]) {
38
+ if (fs.existsSync(path.join(dir, mark))) return dir;
39
+ }
40
+ const up = path.dirname(dir);
41
+ if (up === dir) return path.resolve(from);
42
+ dir = up;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * How the agent should start the server.
48
+ *
49
+ * A project that already has the package gets a direct `node` invocation:
50
+ * `npx` would re-resolve the package on every start, which costs a second and
51
+ * needs the network. Anywhere else, `npx -y` is the only thing that works
52
+ * without an install step.
53
+ */
54
+ function launcher(cwd) {
55
+ const local = path.join(cwd, "node_modules", PACKAGE, "index.cjs");
56
+ if (fs.existsSync(local)) {
57
+ // Relative, so the config survives being committed and cloned elsewhere.
58
+ return { command: "node", args: [`node_modules/${PACKAGE}/index.cjs`, "mcp"] };
59
+ }
60
+ return { command: "npx", args: ["-y", PACKAGE, "mcp"] };
61
+ }
62
+
63
+ /** UTF-8, no BOM, LF. The three things that go wrong by hand. */
64
+ function writeText(file, text) {
65
+ fs.mkdirSync(path.dirname(file), { recursive: true });
66
+ fs.writeFileSync(file, text.replace(/\r\n/g, "\n"), { encoding: "utf8" });
67
+ }
68
+
69
+ function readText(file) {
70
+ if (!fs.existsSync(file)) return null;
71
+ // Strip a BOM if one is already there, rather than preserving the bug.
72
+ return fs.readFileSync(file, "utf8").replace(/^/, "");
73
+ }
74
+
75
+ // ------------------------------------------------------------------- json
76
+
77
+ /*
78
+ * Claude Code and Cursor both take `mcpServers` in a JSON file; they differ
79
+ * only in where it lives. An existing file is merged into, because it may
80
+ * already hold servers that are nothing to do with this one.
81
+ */
82
+ function installJson(file, cwd) {
83
+ const existing = readText(file);
84
+ let config = {};
85
+ if (existing && existing.trim()) {
86
+ try {
87
+ config = JSON.parse(existing);
88
+ } catch (error) {
89
+ return { file, status: "failed", detail: `${file} is not valid JSON (${error.message}). Left alone.` };
90
+ }
91
+ }
92
+
93
+ config.mcpServers = config.mcpServers || {};
94
+ const before = JSON.stringify(config.mcpServers[SERVER]);
95
+ config.mcpServers[SERVER] = launcher(cwd);
96
+ const after = JSON.stringify(config.mcpServers[SERVER]);
97
+
98
+ writeText(file, `${JSON.stringify(config, null, 2)}\n`);
99
+ return { file, status: before === after ? "unchanged" : before ? "updated" : "added" };
100
+ }
101
+
102
+ // ------------------------------------------------------------------- toml
103
+
104
+ const tomlString = (value) => `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
105
+
106
+ function tomlBlock({ command, args }) {
107
+ return [
108
+ `[mcp_servers.${SERVER}]`,
109
+ `command = ${tomlString(command)}`,
110
+ `args = [${args.map(tomlString).join(", ")}]`,
111
+ ].join("\n");
112
+ }
113
+
114
+ /*
115
+ * Codex reads TOML, and this edits it as text.
116
+ *
117
+ * Bringing in a TOML parser to add one table is not a trade worth making for
118
+ * a library that ships with no dependencies. The risk in text editing is
119
+ * clobbering something, so the only region touched is the span from our own
120
+ * table header to the next one — and if that table is already right, the file
121
+ * is not rewritten at all.
122
+ */
123
+ function installToml(file, cwd) {
124
+ const launch = launcher(cwd);
125
+ const block = tomlBlock(launch);
126
+ const existing = readText(file);
127
+
128
+ if (!existing || !existing.trim()) {
129
+ writeText(file, `${block}\n`);
130
+ return { file, status: "added" };
131
+ }
132
+
133
+ const text = existing.replace(/\r\n/g, "\n");
134
+ const header = new RegExp(`^\\[mcp_servers\\.${SERVER}\\]\\s*$`, "m");
135
+ const match = header.exec(text);
136
+
137
+ if (!match) {
138
+ const separator = text.endsWith("\n\n") ? "" : text.endsWith("\n") ? "\n" : "\n\n";
139
+ writeText(file, `${text}${separator}${block}\n`);
140
+ return { file, status: "added" };
141
+ }
142
+
143
+ // From our header to the start of the next table, which is where our keys end.
144
+ const rest = text.slice(match.index + match[0].length);
145
+ const next = rest.search(/^\s*\[/m);
146
+ const end = next === -1 ? text.length : match.index + match[0].length + next;
147
+ const current = text.slice(match.index, end).trim();
148
+
149
+ if (current === block) return { file, status: "unchanged" };
150
+
151
+ const tail = text.slice(end);
152
+ writeText(file, `${text.slice(0, match.index)}${block}\n${tail.startsWith("\n") ? "" : "\n"}${tail}`);
153
+ return { file, status: "updated" };
154
+ }
155
+
156
+ // ---------------------------------------------------------------- targets
157
+
158
+ const TARGETS = {
159
+ claude: {
160
+ label: "Claude Code",
161
+ where: (cwd) => path.join(cwd, ".mcp.json"),
162
+ apply: installJson,
163
+ /*
164
+ * A `.mcp.json` server is project-scoped, and Claude Code will not load
165
+ * one without being asked first — it sits at "Pending approval" until
166
+ * you say yes, and nothing asks again. That prompt only appears at the
167
+ * start of a session, so an editor window already open sees nothing at
168
+ * all. Both are worth saying out loud, because the file is written
169
+ * either way and the command otherwise looks like it worked.
170
+ */
171
+ note:
172
+ "Reload the window (VS Code: Developer: Reload Window), approve the server\n" +
173
+ " when asked, then `/mcp` should show it connected. Prefer it in every\n" +
174
+ " project instead? `claude mcp add -s user grampro -- npx -y @grampro/kit mcp`\n" +
175
+ " skips the approval, since user scope is yours rather than the repo's.",
176
+ },
177
+ codex: {
178
+ label: "Codex",
179
+ where: (cwd) => path.join(cwd, ".codex", "config.toml"),
180
+ apply: installToml,
181
+ note: `Project-scoped. For every project instead, put the same block in ${path.join(os.homedir(), ".codex", "config.toml")}.`,
182
+ },
183
+ cursor: {
184
+ label: "Cursor",
185
+ where: (cwd) => path.join(cwd, ".cursor", "mcp.json"),
186
+ apply: installJson,
187
+ note: "Settings → MCP shows whether it connected.",
188
+ },
189
+ };
190
+
191
+ const NAMES = Object.keys(TARGETS);
192
+
193
+ /**
194
+ * Writes the configuration for one or more agents.
195
+ *
196
+ * `for` is a comma-separated list of agent names, or "all". With nothing
197
+ * given it writes for every agent, because the cost of an unused config file
198
+ * is nil and guessing wrong wastes a round trip with the user.
199
+ */
200
+ function install({ cwd = projectRoot(process.cwd()), for: which } = {}) {
201
+ const wanted = !which || which === "all"
202
+ ? NAMES
203
+ : String(which)
204
+ .split(",")
205
+ .map((name) => name.trim().toLowerCase())
206
+ .filter(Boolean);
207
+
208
+ const unknown = wanted.filter((name) => !TARGETS[name]);
209
+ if (unknown.length) {
210
+ return { error: `No such agent: ${unknown.join(", ")}. Known: ${NAMES.join(", ")}.` };
211
+ }
212
+
213
+ const launch = launcher(cwd);
214
+ const results = wanted.map((name) => {
215
+ const target = TARGETS[name];
216
+ try {
217
+ return { agent: name, label: target.label, note: target.note, ...target.apply(target.where(cwd), cwd) };
218
+ } catch (error) {
219
+ return { agent: name, label: target.label, status: "failed", detail: error.message };
220
+ }
221
+ });
222
+
223
+ return { launch, results, root: cwd };
224
+ }
225
+
226
+ /** The CLI face of the above. */
227
+ function main(operands, options = {}) {
228
+ const root = projectRoot(process.cwd());
229
+ const outcome = install({ cwd: root, for: options.for });
230
+ if (outcome.error) {
231
+ console.log(outcome.error);
232
+ process.exitCode = 1;
233
+ return;
234
+ }
235
+
236
+ const { launch, results } = outcome;
237
+
238
+ // Say which directory, always. An agent reads these files from the folder
239
+ // it opened, so the wrong directory is the failure that looks like success.
240
+ console.log(`Project root: ${root}`);
241
+ if (path.resolve(process.cwd()) !== root) {
242
+ console.log(` (you ran this in ${process.cwd()})`);
243
+ }
244
+ console.log(`Server command: ${launch.command} ${launch.args.join(" ")}\n`);
245
+
246
+ for (const result of results) {
247
+ const rel = path.relative(root, result.file) || result.file;
248
+ if (result.status === "failed") {
249
+ console.log(` ${result.label}: ${result.detail}`);
250
+ process.exitCode = 1;
251
+ continue;
252
+ }
253
+ console.log(` ${result.label}: ${result.status} ${rel}`);
254
+ if (result.status !== "unchanged") console.log(` ${result.note}`);
255
+ }
256
+
257
+ console.log(
258
+ "\nAn MCP server is not called by name — there is no slash command for it.\n" +
259
+ "The agent calls its tools when they are relevant. Ask for a component and watch.",
260
+ );
261
+
262
+ if (launch.command === "npx") {
263
+ console.log(
264
+ `\nThis project does not have ${PACKAGE} installed, so the config uses npx.\n` +
265
+ `Installing it (\`npm i -D ${PACKAGE}\`) and running this again gives a faster start.`,
266
+ );
267
+ }
268
+ }
269
+
270
+ module.exports = { install, main, NAMES };
@@ -0,0 +1,174 @@
1
+ /*
2
+ * `grampro-kit secret …` — put a credential where the operating system keeps
3
+ * credentials, and tell the truth about where that is.
4
+ *
5
+ * The value is never passed as an argument. It is typed at a prompt with the
6
+ * echo off, or piped in, because an argument lands in the shell history and
7
+ * in every process listing on the machine for as long as the command runs.
8
+ */
9
+ "use strict";
10
+
11
+ const readline = require("readline");
12
+ const secrets = require("./secret.cjs");
13
+
14
+ /** What each name is for, so `secret list` is readable without the docs. */
15
+ const PURPOSE = {
16
+ figma: "Figma personal access token, for reading a design by URL",
17
+ };
18
+
19
+ /** figd_1234…wxyz — enough to tell two tokens apart, not enough to use one. */
20
+ function mask(value) {
21
+ if (value.length <= 12) return `${value.slice(0, 2)}${"*".repeat(value.length - 2)}`;
22
+ return `${value.slice(0, 7)}${"*".repeat(8)}${value.slice(-4)}`;
23
+ }
24
+
25
+ /** Reads everything piped in, for `echo $TOKEN | grampro-kit secret set figma`. */
26
+ function readPiped() {
27
+ return new Promise((resolve) => {
28
+ let input = "";
29
+ process.stdin.setEncoding("utf8");
30
+ process.stdin.on("data", (chunk) => {
31
+ input += chunk;
32
+ });
33
+ process.stdin.on("end", () => resolve(input.trim()));
34
+ });
35
+ }
36
+
37
+ /** Reads one line with the echo off. */
38
+ function prompt(question) {
39
+ return new Promise((resolve) => {
40
+ const rl = readline.createInterface({
41
+ input: process.stdin,
42
+ output: process.stdout,
43
+ terminal: true,
44
+ });
45
+ let muted = false;
46
+ // readline writes each keystroke back to the terminal; this is where to
47
+ // stop it. There is no supported option for a password prompt.
48
+ rl._writeToOutput = (chunk) => {
49
+ if (!muted) rl.output.write(chunk);
50
+ };
51
+ rl.question(question, (answer) => {
52
+ rl.close();
53
+ process.stdout.write("\n");
54
+ resolve(answer.trim());
55
+ });
56
+ muted = true;
57
+ });
58
+ }
59
+
60
+ const usage = () =>
61
+ [
62
+ "Usage:",
63
+ " grampro-kit secret set <name> store a credential (prompts; echo off)",
64
+ " grampro-kit secret get <name> show whether one is stored, masked",
65
+ " grampro-kit secret remove <name> delete it",
66
+ " grampro-kit secret list what is stored, and where",
67
+ "",
68
+ `Names: ${secrets.KNOWN.join(", ")}`,
69
+ ].join("\n");
70
+
71
+ async function main(operands) {
72
+ const [action, name] = operands;
73
+
74
+ if (!action || action === "help") {
75
+ console.log(usage());
76
+ return;
77
+ }
78
+
79
+ if (action === "list") {
80
+ const stored = secrets.listSecrets();
81
+ console.log(`Stored in: ${secrets.describeStore()}`);
82
+ if (!secrets.isEncrypted()) {
83
+ console.log(
84
+ "\nThis machine offers no credential store, so the file is only protected by\n" +
85
+ "its permissions. It is outside your repository, which is the point.",
86
+ );
87
+ }
88
+ console.log("");
89
+ if (stored.length === 0) {
90
+ console.log("Nothing stored yet. Try: grampro-kit secret set figma");
91
+ return;
92
+ }
93
+ for (const key of stored) {
94
+ const value = secrets.getSecret(key);
95
+ const purpose = PURPOSE[key] ? ` — ${PURPOSE[key]}` : "";
96
+ console.log(` ${key} ${value ? mask(value) : "(unreadable)"}${purpose}`);
97
+ }
98
+ return;
99
+ }
100
+
101
+ if (!name) {
102
+ console.log(`Which secret? Try: grampro-kit secret ${action} figma`);
103
+ return;
104
+ }
105
+
106
+ if (action === "set") {
107
+ /*
108
+ * An unknown name is accepted rather than refused — this package is not
109
+ * the only thing that may want a store — but it is worth a word, because
110
+ * the usual reason for one is a typo.
111
+ */
112
+ if (!secrets.KNOWN.includes(name)) {
113
+ console.log(`Note: "${name}" is not a name this package reads (${secrets.KNOWN.join(", ")}).`);
114
+ }
115
+
116
+ const value = process.stdin.isTTY
117
+ ? await prompt(`Value for "${name}" (not shown): `)
118
+ : await readPiped();
119
+
120
+ if (!value) {
121
+ console.log("Nothing entered; nothing stored.");
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+
126
+ try {
127
+ secrets.setSecret(name, value);
128
+ } catch (error) {
129
+ console.log(`Could not store it: ${error.message}`);
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+
134
+ // Read it back. A store that accepts a write and cannot return it is
135
+ // worse than one that fails loudly, and that has happened.
136
+ if (secrets.getSecret(name) !== value) {
137
+ console.log("Stored, but it did not read back correctly. Treat it as not set.");
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+
142
+ console.log(`Stored "${name}" (${mask(value)}) in ${secrets.describeStore()}.`);
143
+ if (name === "figma") {
144
+ console.log(
145
+ "\nThe MCP server will find it from any project. If you have FIGMA_TOKEN in a\n" +
146
+ ".env, delete that line — the environment still wins over the store.",
147
+ );
148
+ }
149
+ return;
150
+ }
151
+
152
+ if (action === "get") {
153
+ const value = secrets.getSecret(name);
154
+ if (!value) {
155
+ console.log(`No "${name}" stored in ${secrets.describeStore()}.`);
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+ console.log(`${name}: ${mask(value)} (${value.length} characters, in ${secrets.describeStore()})`);
160
+ return;
161
+ }
162
+
163
+ if (action === "remove" || action === "delete") {
164
+ console.log(
165
+ secrets.removeSecret(name) ? `Removed "${name}".` : `There was no "${name}" to remove.`,
166
+ );
167
+ return;
168
+ }
169
+
170
+ console.log(`No such action "${action}".\n\n${usage()}`);
171
+ process.exitCode = 1;
172
+ }
173
+
174
+ module.exports = { main, mask };