@grampro/kit 2.7.1 → 2.7.3
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/.gbs/skills/gbs-components/SKILL.md +233 -207
- package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
- package/.gbs/skills/gbs-components/references/forms.md +12 -0
- package/.gbs/skills/gbs-components/references/install.md +42 -5
- package/CHANGELOG.md +963 -798
- package/README.md +13 -4
- package/index.cjs +250 -138
- package/package.json +1 -1
- package/source/components/accordion/passport.json +1 -1
- package/source/components/alert/passport.json +1 -1
- package/source/components/avatar/passport.json +1 -1
- package/source/components/badge/passport.json +1 -1
- package/source/components/breadcrumb/passport.json +1 -1
- package/source/components/button/passport.json +1 -1
- package/source/components/card/passport.json +1 -1
- package/source/components/checkbox/passport.json +2 -2
- package/source/components/checkbox/passport.manual.json +1 -1
- package/source/components/checkbox/react/Checkbox.tsx +14 -0
- package/source/components/combobox/passport.json +3 -2
- package/source/components/combobox/passport.manual.json +1 -1
- package/source/components/combobox/react/MultiSelect.tsx +13 -0
- package/source/components/combobox/react/Select.tsx +13 -0
- package/source/components/combobox/react/surface.ts +70 -0
- package/source/components/data-grid/passport.json +6 -6
- package/source/components/data-grid/react/DataGrid.tsx +353 -351
- package/source/components/date-picker/passport.json +3 -2
- package/source/components/date-picker/passport.manual.json +1 -1
- package/source/components/date-picker/react/DatePicker.tsx +16 -2
- package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
- package/source/components/date-picker/react/surface.ts +68 -0
- package/source/components/dialog/passport.json +1 -1
- package/source/components/file-uploader/passport.json +1 -1
- package/source/components/input/passport.json +2 -2
- package/source/components/input/passport.manual.json +1 -1
- package/source/components/input/react/Input.tsx +21 -0
- package/source/components/menu/passport.json +1 -1
- package/source/components/modal/passport.json +1 -1
- package/source/components/number-input/passport.json +2 -2
- package/source/components/number-input/passport.manual.json +1 -1
- package/source/components/number-input/react/NumberInput.tsx +30 -0
- package/source/components/popover/passport.json +1 -1
- package/source/components/progress/passport.json +1 -1
- package/source/components/radio-group/passport.json +2 -2
- package/source/components/radio-group/passport.manual.json +1 -1
- package/source/components/radio-group/react/RadioGroup.tsx +33 -0
- package/source/components/shared/core/agent/field-executors.ts +173 -137
- package/source/components/shared/core/agent/index.ts +21 -0
- package/source/components/shared/core/agent/surface.ts +437 -0
- package/source/components/shared/core/agent/webmcp.ts +332 -328
- package/source/components/shared/core/agent/write.ts +62 -58
- package/source/components/shared/index.ts +7 -0
- package/source/components/shared/react/AgentSurface.tsx +145 -0
- package/source/components/shared/react/agentSurfaceContext.ts +92 -0
- package/source/components/shared/react/useFieldAgent.ts +62 -88
- package/source/components/shared/version.json +4 -4
- package/source/components/shared/version.ts +6 -6
- package/source/components/skeleton/passport.json +1 -1
- package/source/components/spinner/passport.json +1 -1
- package/source/components/switch/passport.json +2 -2
- package/source/components/switch/passport.manual.json +1 -1
- package/source/components/switch/react/Switch.tsx +16 -0
- package/source/components/tabs/passport.json +1 -1
- package/source/components/textarea/passport.json +2 -2
- package/source/components/textarea/passport.manual.json +1 -1
- package/source/components/textarea/react/Textarea.tsx +16 -0
- package/source/components/toaster/passport.json +1 -1
- package/source/components/tooltip/passport.json +1 -1
- package/tools/doctor/baseline.cjs +228 -0
- package/tools/doctor/contract.cjs +252 -0
- package/tools/doctor/format.cjs +160 -0
- package/tools/doctor/index.cjs +246 -0
- package/tools/doctor/usage.cjs +95 -0
- package/tools/jsx-tags.cjs +120 -0
- package/tools/mcp/server.cjs +60 -41
- package/tools/passport/extract.cjs +22 -40
- package/tools/passport/hash.cjs +56 -0
- package/tools/passport/index.cjs +4 -2
- package/tools/passport/typescript.cjs +220 -0
- package/tools/prompt.cjs +205 -0
- package/tools/ui.cjs +67 -0
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ Figma design.
|
|
|
14
14
|
npx @grampro/kit add DataGrid
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Or `npx @grampro/kit add` with no names, to pick from the list: type to search,
|
|
18
|
+
space to select, enter to install.
|
|
19
|
+
|
|
17
20
|
The source lands in `component-lib/`, or `src/component-lib/` in a project
|
|
18
21
|
with a `src/` folder; `--dest` puts it anywhere you like, and later installs
|
|
19
22
|
follow the first. There is no runtime package to depend on, and nothing to
|
|
@@ -84,7 +87,8 @@ genuinely headless, and the component READMEs document it under "Headless use".
|
|
|
84
87
|
|
|
85
88
|
| | |
|
|
86
89
|
| --- | --- |
|
|
87
|
-
| `add <Component>...` | Copy components into the project. `--dest` chooses where; `--legacy` reaches the 1.x set. |
|
|
90
|
+
| `add <Component>...` | Copy components into the project, or update ones already there. A component you have edited is left alone unless you pass `--force`. `--dest` chooses where; `--legacy` reaches the 1.x set. |
|
|
91
|
+
| `doctor [Component...]` | What you changed in your copies, what the new version changes, and which of your call sites an update would break. `--json` for agents and CI. |
|
|
88
92
|
| `list` | What is available to add. |
|
|
89
93
|
| `skill` | Install the agent skill. `--for claude,codex,antigravity` narrows it. |
|
|
90
94
|
| `mcp` | Run the MCP server. Agents start this themselves; you rarely type it. |
|
|
@@ -217,6 +221,10 @@ version. What is built and tested:
|
|
|
217
221
|
application supplies. No model is bundled, downloaded or named.
|
|
218
222
|
- **WebMCP** — one tool, `operate_grid`, exposing the same validated boundary to
|
|
219
223
|
a browser agent. Needs no model at all.
|
|
224
|
+
- **`<AgentSurface>`** — wrap a page and every field and grid in it describes
|
|
225
|
+
itself to a browser agent, from its own props, and is operated through its own
|
|
226
|
+
validator. One WebMCP tool, `operate_page`, for the whole page; no tool
|
|
227
|
+
definitions to write
|
|
220
228
|
- A model evaluation harness, with the measurements that justify both
|
|
221
229
|
|
|
222
230
|
Every producer — a model, a browser agent, a form — proposes. The validator
|
|
@@ -226,9 +234,10 @@ it is why no AI path gets a second route into the component.
|
|
|
226
234
|
Nine of the 27 are agent-operable. The executor abstraction was the open
|
|
227
235
|
question — a grid is driven through an imperative API, and a field is not — and
|
|
228
236
|
it is settled: Combobox and DatePicker are driven through their handles,
|
|
229
|
-
Input, Textarea,
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
Input, Textarea, Checkbox and Switch through the element their ref already
|
|
238
|
+
hands back, and NumberInput and RadioGroup, inside an `<AgentSurface>`, through
|
|
239
|
+
their own commit path — a number is not text, and a group is not one radio.
|
|
240
|
+
Neither the contract, the schemas, the validator nor the refusals know which.
|
|
232
241
|
|
|
233
242
|
Still open: the components whose value is in what they contain rather than what
|
|
234
243
|
they hold — Menu, Tabs, Accordion, Modal, FileUploader — and the question of
|
package/index.cjs
CHANGED
|
@@ -24,7 +24,7 @@ const path = require("path");
|
|
|
24
24
|
const crypto = require("crypto");
|
|
25
25
|
const yargs = require("yargs/yargs");
|
|
26
26
|
const { hideBin } = require("yargs/helpers");
|
|
27
|
-
const
|
|
27
|
+
const ui = require("./tools/ui.cjs");
|
|
28
28
|
|
|
29
29
|
// Configuration
|
|
30
30
|
const CONFIG = {
|
|
@@ -391,7 +391,7 @@ const copyCommonFiles = async (destPath) => {
|
|
|
391
391
|
const src = path.join(LEGACY_SOURCE_PATH, ...file.src);
|
|
392
392
|
const dest = path.join(destPath, file.dest);
|
|
393
393
|
await fs.copy(src, dest, { overwrite: true });
|
|
394
|
-
console.log(
|
|
394
|
+
console.log(`${ui.check()} ${file.dest} copied successfully`);
|
|
395
395
|
}
|
|
396
396
|
};
|
|
397
397
|
|
|
@@ -467,7 +467,7 @@ const installShared = async (destPath, { force = false } = {}) => {
|
|
|
467
467
|
|
|
468
468
|
if (order > 0) {
|
|
469
469
|
console.error(
|
|
470
|
-
`\n
|
|
470
|
+
`\n${ui.cross()} This project has shared v${installedVersion}, newer than the v${srcVersion} in this installer.`,
|
|
471
471
|
);
|
|
472
472
|
console.error(
|
|
473
473
|
" Installing it would downgrade the folder and break the components already here.",
|
|
@@ -478,11 +478,17 @@ const installShared = async (destPath, { force = false } = {}) => {
|
|
|
478
478
|
process.exit(1);
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
-
|
|
481
|
+
// The same version is only "already installed" when it is the same files:
|
|
482
|
+
// a release that changed shared/ without bumping it must still reach the
|
|
483
|
+
// project, or the components it brings import files that are not there.
|
|
484
|
+
const sameFiles = listFiles(src).every(
|
|
485
|
+
(file) => manifest.files[file] === sha256(path.join(src, file)),
|
|
486
|
+
);
|
|
487
|
+
if (order === 0 && sameFiles) {
|
|
482
488
|
console.log(
|
|
483
489
|
edited.length > 0
|
|
484
|
-
?
|
|
485
|
-
:
|
|
490
|
+
? `${ui.check()} shared v${installedVersion} is already installed (keeping your ${edited.length} edited file(s))`
|
|
491
|
+
: `${ui.check()} shared v${installedVersion} is already installed`,
|
|
486
492
|
);
|
|
487
493
|
return;
|
|
488
494
|
}
|
|
@@ -490,7 +496,7 @@ const installShared = async (destPath, { force = false } = {}) => {
|
|
|
490
496
|
|
|
491
497
|
if (edited.length > 0 && !force) {
|
|
492
498
|
console.error(
|
|
493
|
-
`\n
|
|
499
|
+
`\n${ui.cross()} These files in component-lib/${SHARED_DIR} have local changes:`,
|
|
494
500
|
);
|
|
495
501
|
edited.forEach((file) => console.error(` - ${file}`));
|
|
496
502
|
console.error(
|
|
@@ -541,8 +547,8 @@ const installShared = async (destPath, { force = false } = {}) => {
|
|
|
541
547
|
|
|
542
548
|
console.log(
|
|
543
549
|
installedVersion
|
|
544
|
-
?
|
|
545
|
-
:
|
|
550
|
+
? `${ui.check()} shared updated ${installedVersion} → ${srcVersion}`
|
|
551
|
+
: `${ui.check()} shared v${srcVersion} installed`,
|
|
546
552
|
);
|
|
547
553
|
};
|
|
548
554
|
|
|
@@ -656,7 +662,7 @@ const writeAntigravityRule = async (root) => {
|
|
|
656
662
|
|
|
657
663
|
/**
|
|
658
664
|
* Codex: a pointer in AGENTS.md rather than the skill itself, because that file
|
|
659
|
-
* is always in context and the skill is
|
|
665
|
+
* is always in context and the skill is about 10 KB. Everything outside our markers is
|
|
660
666
|
* left exactly as it was.
|
|
661
667
|
*/
|
|
662
668
|
const writeAgentsPointer = async (root) => {
|
|
@@ -727,7 +733,7 @@ const installSkill = async (
|
|
|
727
733
|
const edited = locallyEdited(destRoot, manifest);
|
|
728
734
|
|
|
729
735
|
if (edited.length > 0 && !force) {
|
|
730
|
-
console.error(`${NL}
|
|
736
|
+
console.error(`${NL}${ui.cross()} These skill files have local changes:`);
|
|
731
737
|
edited.forEach((file) => console.error(` - ${file}`));
|
|
732
738
|
console.error(
|
|
733
739
|
NL +
|
|
@@ -793,8 +799,8 @@ const installSkill = async (
|
|
|
793
799
|
|
|
794
800
|
console.log(
|
|
795
801
|
installedVersion
|
|
796
|
-
?
|
|
797
|
-
:
|
|
802
|
+
? `${ui.check()} skill updated ${installedVersion} → ${PACKAGE_VERSION}`
|
|
803
|
+
: `${ui.check()} skill v${PACKAGE_VERSION} installed`,
|
|
798
804
|
);
|
|
799
805
|
console.log(` ${SKILL_DEST_DIR}/ (canonical, ${listFiles(src).length} files)`);
|
|
800
806
|
if (targets.includes("claude")) {
|
|
@@ -833,6 +839,45 @@ const parseSkillTargets = (value) => {
|
|
|
833
839
|
return wanted;
|
|
834
840
|
};
|
|
835
841
|
|
|
842
|
+
/* ---------------------------------------------------------------- doctor */
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Report on the components installed here: what was changed since install,
|
|
846
|
+
* what this version of the kit changes, and which call sites an update would
|
|
847
|
+
* break. Read-only. See tools/doctor/index.cjs.
|
|
848
|
+
*/
|
|
849
|
+
const runDoctorCommand = ({ only = [], dest: requestedDest, json = false }) => {
|
|
850
|
+
const dest = resolveDestPath(requestedDest);
|
|
851
|
+
if (!fs.existsSync(dest)) {
|
|
852
|
+
console.error(
|
|
853
|
+
`No ${path.basename(dest)}/ folder here, so there is nothing to check.` +
|
|
854
|
+
`${NL} Install a component first: npx ${CLI_NAME} add Button`,
|
|
855
|
+
);
|
|
856
|
+
process.exitCode = 1;
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const { runDoctor } = require("./tools/doctor/index.cjs");
|
|
861
|
+
const { formatReport, toJson } = require("./tools/doctor/format.cjs");
|
|
862
|
+
const report = runDoctor({
|
|
863
|
+
cwd: process.cwd(),
|
|
864
|
+
dest,
|
|
865
|
+
kitRoot: SOURCE_PATH,
|
|
866
|
+
kitVersion: PACKAGE_VERSION,
|
|
867
|
+
only,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
if (report.rows.length === 0 && report.notInstalled.length === 0) {
|
|
871
|
+
console.log(`No ${CLI_NAME} components found in ${path.relative(process.cwd(), dest) || dest}/.`);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const shown = { ...report, dest: `${(path.relative(process.cwd(), dest) || ".").split(path.sep).join("/")}/` };
|
|
876
|
+
console.log(json ? JSON.stringify(toJson(shown), null, 2) : formatReport(shown));
|
|
877
|
+
// A component someone asked about that is not here is the one failure worth an exit code.
|
|
878
|
+
if (report.notInstalled.length) process.exitCode = 1;
|
|
879
|
+
};
|
|
880
|
+
|
|
836
881
|
/* ------------------------------------------------------------- passports */
|
|
837
882
|
|
|
838
883
|
/**
|
|
@@ -863,10 +908,17 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
|
|
|
863
908
|
process.exit(1);
|
|
864
909
|
}
|
|
865
910
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
911
|
+
/*
|
|
912
|
+
* Each component is stamped with the kit version it was installed from —
|
|
913
|
+
* its install record, else the passport it shipped with — not with this
|
|
914
|
+
* project's own version. Stamping the project's version made every
|
|
915
|
+
* regenerated passport differ from the shipped one, so `passport --check`
|
|
916
|
+
* failed for components nobody had touched.
|
|
917
|
+
*/
|
|
918
|
+
const libraryVersion = (name, dir) =>
|
|
919
|
+
readJson(path.join(dir, ".install-manifest.json"), {})?.version ||
|
|
920
|
+
readJson(path.join(dir, "passport.json"), {})?.source?.libraryVersion ||
|
|
921
|
+
PACKAGE_VERSION;
|
|
870
922
|
|
|
871
923
|
const options = {
|
|
872
924
|
libRoot: dest,
|
|
@@ -874,6 +926,7 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
|
|
|
874
926
|
libraryVersion,
|
|
875
927
|
includeLocal: true,
|
|
876
928
|
coverageThreshold: undefined,
|
|
929
|
+
regenerate: `npx ${CLI_NAME} passport`,
|
|
877
930
|
};
|
|
878
931
|
|
|
879
932
|
try {
|
|
@@ -885,7 +938,7 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
|
|
|
885
938
|
continue;
|
|
886
939
|
}
|
|
887
940
|
for (const issue of row.problems) {
|
|
888
|
-
console.log(` ${issue.level === "error" ?
|
|
941
|
+
console.log(` ${issue.level === "error" ? ui.cross(process.stdout) : ui.c.yellow("!")} ${issue.component}: ${issue.message}`);
|
|
889
942
|
}
|
|
890
943
|
}
|
|
891
944
|
console.log(`${NL}${report.length} component(s) · ${errors} error(s) · ${warnings} warning(s)`);
|
|
@@ -895,13 +948,13 @@ const runPassport = async ({ check: checkOnly, dest: requestedDest }) => {
|
|
|
895
948
|
|
|
896
949
|
const { results, written } = passport.generate(options);
|
|
897
950
|
const errors = results.flatMap((r) => r.issues.filter((i) => i.level === "error"));
|
|
898
|
-
for (const issue of errors) console.error(`
|
|
951
|
+
for (const issue of errors) console.error(` ${ui.cross()} ${issue.component}: ${issue.message}`);
|
|
899
952
|
console.log(
|
|
900
|
-
`${NL}
|
|
953
|
+
`${NL}${ui.check()} ${results.length} passport(s) regenerated · ${written.length} changed`,
|
|
901
954
|
);
|
|
902
955
|
if (errors.length > 0) process.exit(1);
|
|
903
956
|
} catch (error) {
|
|
904
|
-
if (error.code === "ETYPESCRIPT_MISSING" || error.code === "EBADJSON") {
|
|
957
|
+
if (error.code === "ETYPESCRIPT_MISSING" || error.code === "ETYPESCRIPT_API" || error.code === "EBADJSON") {
|
|
905
958
|
console.error(error.message);
|
|
906
959
|
process.exit(1);
|
|
907
960
|
}
|
|
@@ -932,13 +985,27 @@ const checkComponentExists = (component, destPath, current = false) => {
|
|
|
932
985
|
return fs.existsSync(componentPath);
|
|
933
986
|
};
|
|
934
987
|
|
|
935
|
-
|
|
988
|
+
/*
|
|
989
|
+
* Copy one component into the project — unless that would destroy edits.
|
|
990
|
+
*
|
|
991
|
+
* Components are copied in to be edited; that is what owning the code means.
|
|
992
|
+
* Until 2.7.2 this overwrote the folder unconditionally, so running `add` on a
|
|
993
|
+
* component that was already there — by hand, from a script, or by an agent
|
|
994
|
+
* through the MCP server's install_component — silently replaced whatever had
|
|
995
|
+
* been changed in it, and printed a success message while doing so.
|
|
996
|
+
*
|
|
997
|
+
* Now an installed component is compared with the record written when it was
|
|
998
|
+
* installed. Untouched, it is updated. Changed — or installed before there
|
|
999
|
+
* were records, and different from this version — it is left alone unless
|
|
1000
|
+
* `--force` is passed, and the user is told what differs and how to see more.
|
|
1001
|
+
*
|
|
1002
|
+
* Returns "installed", "updated", "unchanged" or "kept".
|
|
1003
|
+
*/
|
|
1004
|
+
const copyComponent = async (component, destPath, current = false, { force = false } = {}) => {
|
|
936
1005
|
try {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
);
|
|
941
|
-
const componentDest = path.join(destPath, folderFor(component, current));
|
|
1006
|
+
const folder = folderFor(component, current);
|
|
1007
|
+
const componentSrc = path.join(getSourcePath(current), folder);
|
|
1008
|
+
const componentDest = path.join(destPath, folder);
|
|
942
1009
|
|
|
943
1010
|
if (!fs.existsSync(componentSrc)) {
|
|
944
1011
|
throw new Error(
|
|
@@ -946,6 +1013,25 @@ const copyComponent = async (component, destPath, current = false) => {
|
|
|
946
1013
|
);
|
|
947
1014
|
}
|
|
948
1015
|
|
|
1016
|
+
let outcome = "installed";
|
|
1017
|
+
let previous = null;
|
|
1018
|
+
// The 1.x set is unmaintained and keeps its old behaviour.
|
|
1019
|
+
if (current && fs.existsSync(componentDest)) {
|
|
1020
|
+
const status = doctorBaseline().inspect(componentDest, {
|
|
1021
|
+
folder,
|
|
1022
|
+
kitDir: componentSrc,
|
|
1023
|
+
kitVersion: PACKAGE_VERSION,
|
|
1024
|
+
});
|
|
1025
|
+
previous = status.installedVersion;
|
|
1026
|
+
|
|
1027
|
+
if (status.destructive && !force) {
|
|
1028
|
+
reportKept(component, status);
|
|
1029
|
+
process.exitCode = 1;
|
|
1030
|
+
return "kept";
|
|
1031
|
+
}
|
|
1032
|
+
outcome = status.state === "pristine" && status.installedVersion === PACKAGE_VERSION ? "unchanged" : "updated";
|
|
1033
|
+
}
|
|
1034
|
+
|
|
949
1035
|
// The whole folder travels, which includes passport.json and
|
|
950
1036
|
// passport.manual.json. The passport is part of the component contract,
|
|
951
1037
|
// not an add-on — a contract you opt into is not a contract.
|
|
@@ -953,23 +1039,65 @@ const copyComponent = async (component, destPath, current = false) => {
|
|
|
953
1039
|
overwrite: true,
|
|
954
1040
|
filter: (source) => !isTestPath(path.relative(componentSrc, source)),
|
|
955
1041
|
});
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1042
|
+
|
|
1043
|
+
// The record `doctor` and the next `add` compare against.
|
|
1044
|
+
if (current) {
|
|
1045
|
+
doctorBaseline().writeManifest(componentDest, {
|
|
1046
|
+
component,
|
|
1047
|
+
version: PACKAGE_VERSION,
|
|
1048
|
+
sourceDir: componentSrc,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (outcome === "updated") {
|
|
1053
|
+
console.log(
|
|
1054
|
+
previous && previous !== PACKAGE_VERSION
|
|
1055
|
+
? `${ui.check()} Component ${component} updated ${previous} → ${PACKAGE_VERSION}`
|
|
1056
|
+
: `${ui.check()} Component ${component} replaced with ${PACKAGE_VERSION}${force ? " (--force)" : ""}`,
|
|
1057
|
+
);
|
|
1058
|
+
} else if (outcome === "unchanged") {
|
|
1059
|
+
console.log(`${ui.check()} Component ${component} already up to date (${PACKAGE_VERSION})`);
|
|
1060
|
+
} else {
|
|
1061
|
+
console.log(
|
|
1062
|
+
`${ui.check()} Component ${component} installed successfully ${
|
|
1063
|
+
component === "Grid"
|
|
1064
|
+
? "This Version of Grid will be deprecated soon. Please Install The New Data Grid Component"
|
|
1065
|
+
: ""
|
|
1066
|
+
}`,
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
return outcome;
|
|
963
1070
|
} catch (error) {
|
|
964
1071
|
console.error(`Error installing component ${component}:`, error.message);
|
|
965
1072
|
process.exit(1);
|
|
966
1073
|
}
|
|
967
1074
|
};
|
|
968
1075
|
|
|
1076
|
+
/* Loaded on first use, so commands that never touch components don't pay for it. */
|
|
1077
|
+
let baselineModule = null;
|
|
1078
|
+
const doctorBaseline = () => (baselineModule ??= require("./tools/doctor/baseline.cjs"));
|
|
1079
|
+
|
|
1080
|
+
/** Why a component was left alone, and the two ways forward. */
|
|
1081
|
+
const reportKept = (component, status) => {
|
|
1082
|
+
const list = (label, files) =>
|
|
1083
|
+
files.length ? ` ${label}: ${files.slice(0, 6).join(", ")}${files.length > 6 ? `, +${files.length - 6} more` : ""}` : null;
|
|
1084
|
+
console.error(
|
|
1085
|
+
status.state === "unknown"
|
|
1086
|
+
? `${ui.cross()} ${component} was not replaced: it was installed before 2.7.2 and differs from ${PACKAGE_VERSION}, so it may hold your changes.`
|
|
1087
|
+
: `${ui.cross()} ${component} was not replaced: it has changes made since it was installed.`,
|
|
1088
|
+
);
|
|
1089
|
+
for (const line of [list("changed", status.changed), list("deleted", status.missing)].filter(Boolean)) {
|
|
1090
|
+
console.error(line);
|
|
1091
|
+
}
|
|
1092
|
+
console.error(` See what you changed and what ${PACKAGE_VERSION} changes: npx ${CLI_NAME} doctor ${component}`);
|
|
1093
|
+
console.error(` Replace it anyway, losing those changes: npx ${CLI_NAME} add ${component} --force`);
|
|
1094
|
+
};
|
|
1095
|
+
|
|
969
1096
|
const installComponentWithDependencies = async (
|
|
970
1097
|
component,
|
|
971
1098
|
destPath,
|
|
972
1099
|
current = false,
|
|
1100
|
+
options = {},
|
|
973
1101
|
) => {
|
|
974
1102
|
// Get dependencies for the component
|
|
975
1103
|
const dependencies = CONFIG.dependencies[component] || [];
|
|
@@ -984,14 +1112,14 @@ const installComponentWithDependencies = async (
|
|
|
984
1112
|
|
|
985
1113
|
if (pendingInstalls.length === 0) {
|
|
986
1114
|
console.log(
|
|
987
|
-
|
|
1115
|
+
`${ui.check()} ${component} and all its dependencies are already installed.`,
|
|
988
1116
|
);
|
|
989
1117
|
return;
|
|
990
1118
|
}
|
|
991
1119
|
|
|
992
1120
|
// Install all pending components
|
|
993
1121
|
for (const comp of pendingInstalls) {
|
|
994
|
-
await copyComponent(comp, destPath, current);
|
|
1122
|
+
await copyComponent(comp, destPath, current, options);
|
|
995
1123
|
}
|
|
996
1124
|
|
|
997
1125
|
if (dependencies.length > 0) {
|
|
@@ -1010,6 +1138,7 @@ const installMultipleComponents = async (
|
|
|
1010
1138
|
components,
|
|
1011
1139
|
destPath,
|
|
1012
1140
|
current = false,
|
|
1141
|
+
options = {},
|
|
1013
1142
|
) => {
|
|
1014
1143
|
const allComponentsToInstall = new Set();
|
|
1015
1144
|
|
|
@@ -1029,7 +1158,7 @@ const installMultipleComponents = async (
|
|
|
1029
1158
|
|
|
1030
1159
|
if (pendingInstalls.length === 0) {
|
|
1031
1160
|
console.log(
|
|
1032
|
-
|
|
1161
|
+
`${ui.check()} All selected components and their dependencies are already installed.`,
|
|
1033
1162
|
);
|
|
1034
1163
|
return;
|
|
1035
1164
|
}
|
|
@@ -1038,7 +1167,7 @@ const installMultipleComponents = async (
|
|
|
1038
1167
|
|
|
1039
1168
|
// Install all pending components
|
|
1040
1169
|
for (const comp of pendingInstalls) {
|
|
1041
|
-
await copyComponent(comp, destPath, current);
|
|
1170
|
+
await copyComponent(comp, destPath, current, options);
|
|
1042
1171
|
}
|
|
1043
1172
|
|
|
1044
1173
|
// Show dependency information
|
|
@@ -1060,95 +1189,44 @@ const installMultipleComponents = async (
|
|
|
1060
1189
|
console.log(`\nFor documentation visit: ${CONFIG.docs}`);
|
|
1061
1190
|
};
|
|
1062
1191
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const isCurrentIndex = index === currentIndex;
|
|
1085
|
-
const deps = CONFIG.dependencies[component]
|
|
1086
|
-
? ` (requires: ${CONFIG.dependencies[component].join(", ")})`
|
|
1087
|
-
: "";
|
|
1088
|
-
|
|
1089
|
-
const prefix = isCurrentIndex ? ">" : " ";
|
|
1090
|
-
const checkbox = isSelected ? "☑" : "☐";
|
|
1091
|
-
const line = `${prefix} ${checkbox} ${component}${deps}`;
|
|
1092
|
-
|
|
1093
|
-
if (isCurrentIndex) {
|
|
1094
|
-
console.log(`\x1b[36m${line}\x1b[0m`); // Cyan highlight
|
|
1095
|
-
} else {
|
|
1096
|
-
console.log(line);
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
console.log(`\nSelected: ${selectedComponents.size} components`);
|
|
1101
|
-
console.log("Press ENTER to install selected components, or 'q' to quit");
|
|
1102
|
-
};
|
|
1103
|
-
|
|
1104
|
-
const handleKeyPress = (key) => {
|
|
1105
|
-
switch (key) {
|
|
1106
|
-
case "[A": // Up arrow
|
|
1107
|
-
currentIndex = Math.max(0, currentIndex - 1);
|
|
1108
|
-
renderMenu();
|
|
1109
|
-
break;
|
|
1110
|
-
case "[B": // Down arrow
|
|
1111
|
-
currentIndex = Math.min(
|
|
1112
|
-
getAvailableComponents(current).length - 1,
|
|
1113
|
-
currentIndex + 1,
|
|
1114
|
-
);
|
|
1115
|
-
renderMenu();
|
|
1116
|
-
break;
|
|
1117
|
-
case " ": // Space bar
|
|
1118
|
-
const component = getAvailableComponents(current)[currentIndex];
|
|
1119
|
-
if (selectedComponents.has(component)) {
|
|
1120
|
-
selectedComponents.delete(component);
|
|
1121
|
-
} else {
|
|
1122
|
-
selectedComponents.add(component);
|
|
1123
|
-
}
|
|
1124
|
-
renderMenu();
|
|
1125
|
-
break;
|
|
1126
|
-
case "\r": // Enter
|
|
1127
|
-
if (selectedComponents.size > 0) {
|
|
1128
|
-
rl.close();
|
|
1129
|
-
resolve(Array.from(selectedComponents));
|
|
1130
|
-
}
|
|
1131
|
-
break;
|
|
1132
|
-
case "q":
|
|
1133
|
-
rl.close();
|
|
1134
|
-
resolve([]);
|
|
1135
|
-
break;
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
|
|
1139
|
-
// Enable raw mode to capture arrow keys
|
|
1140
|
-
process.stdin.setRawMode(true);
|
|
1141
|
-
process.stdin.resume();
|
|
1142
|
-
process.stdin.setEncoding("utf8");
|
|
1143
|
-
process.stdin.on("data", handleKeyPress);
|
|
1144
|
-
|
|
1145
|
-
renderMenu();
|
|
1192
|
+
/*
|
|
1193
|
+
* Where each component sits in the docs' navigation, shown beside it in the
|
|
1194
|
+
* picker and searchable: typing "overlay" narrows the list to the overlays.
|
|
1195
|
+
*/
|
|
1196
|
+
const GROUPS = {
|
|
1197
|
+
Inputs: ["Button", "Checkbox", "Combobox", "DatePicker", "FileUploader", "Input", "NumberInput", "RadioGroup", "Switch", "Textarea"],
|
|
1198
|
+
Data: ["DataGrid"],
|
|
1199
|
+
Overlays: ["Dialog", "Menu", "Modal", "Popover", "Tooltip"],
|
|
1200
|
+
Feedback: ["Alert", "Progress", "Skeleton", "Spinner", "Toaster"],
|
|
1201
|
+
Display: ["Accordion", "Avatar", "Badge", "Card"],
|
|
1202
|
+
Navigation: ["Breadcrumb", "Tabs"],
|
|
1203
|
+
};
|
|
1204
|
+
const groupOf = (component) =>
|
|
1205
|
+
Object.keys(GROUPS).find((group) => GROUPS[group].includes(component)) || "";
|
|
1206
|
+
|
|
1207
|
+
/** The one-line purpose a component's passport gives, for the picker's hint line. */
|
|
1208
|
+
const purposeOf = (component, current) => {
|
|
1209
|
+
if (!current) return "";
|
|
1210
|
+
const passport = readJson(path.join(SOURCE_PATH, folderFor(component, true), "passport.json"));
|
|
1211
|
+
return (passport && passport.purpose) || "";
|
|
1212
|
+
};
|
|
1146
1213
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1214
|
+
const interactiveComponentSelector = async (current = false, destPath) => {
|
|
1215
|
+
const { multiselect } = require("./tools/prompt.cjs");
|
|
1216
|
+
const options = getAvailableComponents(current)
|
|
1217
|
+
.map((component) => ({
|
|
1218
|
+
value: component,
|
|
1219
|
+
tag: current ? groupOf(component) : "",
|
|
1220
|
+
hint: purposeOf(component, current),
|
|
1221
|
+
note: destPath && checkComponentExists(component, destPath, current) ? "installed" : "",
|
|
1222
|
+
}))
|
|
1223
|
+
// The docs' order of groups, alphabetical within each.
|
|
1224
|
+
.sort((a, b) => {
|
|
1225
|
+
const order = Object.keys(GROUPS);
|
|
1226
|
+
return order.indexOf(a.tag) - order.indexOf(b.tag) || a.value.localeCompare(b.value);
|
|
1150
1227
|
});
|
|
1151
|
-
});
|
|
1228
|
+
const picked = await multiselect({ message: "Which components?", options });
|
|
1229
|
+
return picked || [];
|
|
1152
1230
|
};
|
|
1153
1231
|
|
|
1154
1232
|
const parseMultipleComponents = (componentString) => {
|
|
@@ -1190,7 +1268,7 @@ const validateComponents = (components, current = false) => {
|
|
|
1190
1268
|
* projects a week install this; breaking their scripts to tidy an argument
|
|
1191
1269
|
* parser is not a trade worth making.
|
|
1192
1270
|
*/
|
|
1193
|
-
const COMMANDS = new Set(["add", "list", "skill", "passport", "mcp", "secret", "design"]);
|
|
1271
|
+
const COMMANDS = new Set(["add", "list", "skill", "passport", "mcp", "secret", "design", "doctor"]);
|
|
1194
1272
|
|
|
1195
1273
|
/**
|
|
1196
1274
|
* Flags that take a separate value, so the token after one is that value and
|
|
@@ -1281,7 +1359,12 @@ const main = async () => {
|
|
|
1281
1359
|
type: "string",
|
|
1282
1360
|
})
|
|
1283
1361
|
.option("force", {
|
|
1284
|
-
describe: "Replace files you have edited locally
|
|
1362
|
+
describe: "Replace files you have edited locally: a component, shared/ or the skill",
|
|
1363
|
+
type: "boolean",
|
|
1364
|
+
default: false,
|
|
1365
|
+
})
|
|
1366
|
+
.option("json", {
|
|
1367
|
+
describe: "With doctor: print the report as JSON, for agents and CI",
|
|
1285
1368
|
type: "boolean",
|
|
1286
1369
|
default: false,
|
|
1287
1370
|
})
|
|
@@ -1291,6 +1374,8 @@ const main = async () => {
|
|
|
1291
1374
|
.example("$0 skill", "Install the agent skill for every supported agent")
|
|
1292
1375
|
.example("$0 skill --for claude", "Only the Claude Code copy")
|
|
1293
1376
|
.example("$0 passport --check", "Fail if a passport is stale (CI)")
|
|
1377
|
+
.example("$0 doctor", "What you changed, what upstream changed, what an update breaks")
|
|
1378
|
+
.example("$0 doctor DataGrid --json", "One component, as JSON")
|
|
1294
1379
|
.example("$0 add Select --legacy", "Install from the 1.x set")
|
|
1295
1380
|
.example("$0 add Button --dest app/ui", "Choose where they go")
|
|
1296
1381
|
.example("$0 mcp", "Serve the catalogue to a coding agent over MCP")
|
|
@@ -1298,7 +1383,7 @@ const main = async () => {
|
|
|
1298
1383
|
.example("$0 mcp install --for codex", "Only write the Codex config")
|
|
1299
1384
|
.example("$0 secret set figma", "Store a Figma token in the OS credential store")
|
|
1300
1385
|
.example("$0 design ./design.json", "Split a whole-file Figma export into design/")
|
|
1301
|
-
.example("$0
|
|
1386
|
+
.example("$0 add", "Pick components interactively (same as -i)")
|
|
1302
1387
|
.help().argv;
|
|
1303
1388
|
|
|
1304
1389
|
/*
|
|
@@ -1364,16 +1449,26 @@ const main = async () => {
|
|
|
1364
1449
|
return;
|
|
1365
1450
|
}
|
|
1366
1451
|
|
|
1452
|
+
if (parsed && parsed.command === "doctor") {
|
|
1453
|
+
runDoctorCommand({ only: parsed.operands, dest: argv.dest, json: argv.json });
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1367
1457
|
if (parsed) {
|
|
1368
1458
|
if (parsed.command === "list") argv.list = true;
|
|
1369
1459
|
if (parsed.command === "skill") argv.skill = true;
|
|
1370
1460
|
if (parsed.command === "passport") argv.passport = true;
|
|
1371
1461
|
if (parsed.command === "add") {
|
|
1372
1462
|
if (parsed.operands.length === 0) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1463
|
+
// In a terminal, a bare `add` opens the picker; in a script it says what to type.
|
|
1464
|
+
if (process.stdin.isTTY && process.stdout.isTTY) argv.interactive = true;
|
|
1465
|
+
else {
|
|
1466
|
+
console.log(`Which component? Try: ${CLI_NAME} add Button (or: ${CLI_NAME} list)`);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
} else {
|
|
1470
|
+
argv.add = parsed.operands.join(",");
|
|
1375
1471
|
}
|
|
1376
|
-
argv.add = parsed.operands.join(",");
|
|
1377
1472
|
}
|
|
1378
1473
|
} else if (argv.add || argv.list || argv.skill || argv.passport) {
|
|
1379
1474
|
console.log(`Tip: the shorter form is \`${CLI_NAME} add <Component>\`.`);
|
|
@@ -1405,21 +1500,38 @@ const main = async () => {
|
|
|
1405
1500
|
|
|
1406
1501
|
// Interactive mode
|
|
1407
1502
|
if (argv.interactive) {
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1503
|
+
const destPath = resolveDestPath(argv.dest);
|
|
1504
|
+
const where = path.relative(process.cwd(), destPath) || ".";
|
|
1505
|
+
ui.intro(`GramproKit ${PACKAGE_VERSION}`);
|
|
1506
|
+
let selectedComponents;
|
|
1507
|
+
try {
|
|
1508
|
+
selectedComponents = await interactiveComponentSelector(current, destPath);
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
ui.outro(ui.c.red(error.message));
|
|
1511
|
+
process.exitCode = 1;
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1410
1514
|
|
|
1411
1515
|
if (selectedComponents.length === 0) {
|
|
1412
|
-
|
|
1516
|
+
ui.outro(ui.c.dim("Nothing installed."));
|
|
1413
1517
|
return;
|
|
1414
1518
|
}
|
|
1519
|
+
console.log(ui.c.gray(ui.s.bar));
|
|
1520
|
+
console.log(`${ui.c.gray(ui.s.bar)} ${ui.c.dim(`Installing into ${where}/`)}`);
|
|
1521
|
+
console.log("");
|
|
1415
1522
|
|
|
1416
|
-
const destPath = resolveDestPath(argv.dest);
|
|
1417
1523
|
await fs.ensureDir(destPath);
|
|
1418
1524
|
|
|
1419
1525
|
await copySupportFiles(destPath, current, { force: argv.force });
|
|
1420
1526
|
|
|
1421
1527
|
// Install selected components
|
|
1422
|
-
await installMultipleComponents(selectedComponents, destPath, current);
|
|
1528
|
+
await installMultipleComponents(selectedComponents, destPath, current, { force: argv.force });
|
|
1529
|
+
console.log("");
|
|
1530
|
+
ui.outro(
|
|
1531
|
+
process.exitCode
|
|
1532
|
+
? ui.c.yellow("Done, with the warnings above.")
|
|
1533
|
+
: `${ui.c.green("Done.")} Import them from ${ui.c.cyan(`${where}/`)}`,
|
|
1534
|
+
);
|
|
1423
1535
|
return;
|
|
1424
1536
|
}
|
|
1425
1537
|
|
|
@@ -1470,10 +1582,10 @@ const main = async () => {
|
|
|
1470
1582
|
// Install components
|
|
1471
1583
|
if (components.length === 1) {
|
|
1472
1584
|
// Single component installation (existing behavior)
|
|
1473
|
-
await installComponentWithDependencies(components[0], destPath, current);
|
|
1585
|
+
await installComponentWithDependencies(components[0], destPath, current, { force: argv.force });
|
|
1474
1586
|
} else {
|
|
1475
1587
|
// Multiple components installation
|
|
1476
|
-
await installMultipleComponents(components, destPath, current);
|
|
1588
|
+
await installMultipleComponents(components, destPath, current, { force: argv.force });
|
|
1477
1589
|
}
|
|
1478
1590
|
};
|
|
1479
1591
|
|