@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.
- package/CHANGELOG.md +169 -0
- package/README.md +12 -0
- package/index.cjs +44 -2
- package/package.json +1 -1
- package/source/components/accordion/passport.json +11 -7
- package/source/components/accordion/react/Accordion.tsx +5 -0
- package/source/components/alert/passport.json +3 -3
- package/source/components/avatar/passport.json +8 -6
- package/source/components/avatar/react/Avatar.tsx +4 -0
- package/source/components/badge/passport.json +8 -6
- package/source/components/badge/react/Badge.tsx +1 -0
- package/source/components/badge/react/Tag.tsx +1 -0
- package/source/components/breadcrumb/passport.json +8 -6
- package/source/components/breadcrumb/react/Breadcrumb.tsx +1 -0
- package/source/components/button/passport.json +8 -6
- package/source/components/button/react/Button.tsx +1 -0
- package/source/components/card/passport.json +17 -9
- package/source/components/card/react/Card.tsx +5 -0
- package/source/components/card/react/Stat.tsx +5 -0
- package/source/components/checkbox/passport.json +32 -14
- package/source/components/checkbox/react/Checkbox.tsx +17 -0
- package/source/components/checkbox/react/CheckboxGroup.tsx +6 -0
- package/source/components/combobox/passport.json +36 -16
- package/source/components/combobox/react/Select.tsx +6 -1
- package/source/components/combobox/react/props.ts +23 -0
- package/source/components/data-grid/core/types.ts +53 -0
- package/source/components/data-grid/passport.json +82 -30
- package/source/components/data-grid/react/AskGrid.tsx +10 -0
- package/source/components/data-grid/react/DataGrid.tsx +16 -0
- package/source/components/date-picker/passport.json +30 -14
- package/source/components/date-picker/react/DatePicker.tsx +5 -0
- package/source/components/date-picker/react/props.ts +13 -1
- package/source/components/dialog/core/types.ts +14 -0
- package/source/components/dialog/passport.json +26 -12
- package/source/components/dialog/react/Dialog.tsx +4 -0
- package/source/components/file-uploader/passport.json +44 -18
- package/source/components/file-uploader/react/FileUploader.tsx +39 -0
- package/source/components/input/passport.json +14 -8
- package/source/components/input/react/Input.tsx +3 -0
- package/source/components/menu/passport.json +29 -13
- package/source/components/menu/react/Menu.tsx +7 -0
- package/source/components/menu/react/items.tsx +4 -0
- package/source/components/modal/passport.json +14 -8
- package/source/components/modal/react/Modal.tsx +6 -0
- package/source/components/number-input/passport.json +23 -11
- package/source/components/number-input/react/NumberInput.tsx +13 -0
- package/source/components/popover/passport.json +17 -9
- package/source/components/popover/react/Popover.tsx +7 -0
- package/source/components/progress/passport.json +3 -3
- package/source/components/radio-group/passport.json +32 -14
- package/source/components/radio-group/react/Radio.tsx +17 -0
- package/source/components/radio-group/react/RadioGroup.tsx +13 -0
- package/source/components/skeleton/passport.json +8 -6
- package/source/components/skeleton/react/Skeleton.tsx +5 -0
- package/source/components/spinner/passport.json +3 -3
- package/source/components/switch/passport.json +23 -11
- package/source/components/switch/react/Switch.tsx +14 -0
- package/source/components/tabs/passport.json +11 -7
- package/source/components/tabs/react/Tabs.tsx +2 -0
- package/source/components/textarea/passport.json +14 -8
- package/source/components/textarea/react/Textarea.tsx +3 -0
- package/source/components/toaster/passport.json +8 -6
- package/source/components/toaster/react/Toaster.tsx +4 -0
- package/source/components/tooltip/passport.json +3 -3
- package/tools/mcp/README.md +224 -0
- package/tools/mcp/figma.cjs +445 -0
- package/tools/mcp/install.cjs +270 -0
- package/tools/mcp/secret-cli.cjs +174 -0
- package/tools/mcp/secret.cjs +289 -0
- package/tools/mcp/server.cjs +497 -0
- package/tools/passport/cli.cjs +8 -1
- package/tools/passport/extract.cjs +7 -3
- 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 };
|