@tonycodes-ui/mortar 0.0.0-stage → 0.11.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/AGENTS.md +10 -0
- package/CHANGELOG.md +276 -0
- package/CONTRIBUTING.md +48 -0
- package/DESIGN.md +51 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/TESTING.md +83 -0
- package/baseline-policy.json +16 -0
- package/bin/mortar.mjs +258 -0
- package/catalog.json +9703 -0
- package/cli/audit.mjs +943 -0
- package/cli/data.mjs +176 -0
- package/cli/mcp.mjs +216 -0
- package/cli/migrations.mjs +128 -0
- package/cli/packet.mjs +267 -0
- package/cli/search.mjs +246 -0
- package/cli/tools.mjs +229 -0
- package/design-principles.json +30 -0
- package/dist/agent-data.json +1 -0
- package/dist/application.d.ts +340 -0
- package/dist/application.js +240 -0
- package/dist/auth.d.ts +121 -0
- package/dist/auth.js +97 -0
- package/dist/brand.d.ts +69 -0
- package/dist/brand.js +266 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.js +310 -0
- package/dist/choice.d.ts +36 -0
- package/dist/choice.js +36 -0
- package/dist/composer.d.ts +28 -0
- package/dist/composer.js +47 -0
- package/dist/display.d.ts +136 -0
- package/dist/display.js +75 -0
- package/dist/docs.d.ts +145 -0
- package/dist/docs.js +141 -0
- package/dist/ecommerce.d.ts +261 -0
- package/dist/ecommerce.js +177 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.js +98 -0
- package/dist/foundation.d.ts +42 -0
- package/dist/foundation.js +121 -0
- package/dist/heading.d.ts +5 -0
- package/dist/heading.js +9 -0
- package/dist/icons.d.ts +50 -0
- package/dist/icons.js +70 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +17 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +14 -0
- package/dist/internal.d.ts +492 -0
- package/dist/internal.js +273 -0
- package/dist/layout.d.ts +107 -0
- package/dist/layout.js +66 -0
- package/dist/marketing-sections.d.ts +161 -0
- package/dist/marketing-sections.js +58 -0
- package/dist/marketing.d.ts +254 -0
- package/dist/marketing.js +150 -0
- package/dist/navigation.d.ts +35 -0
- package/dist/navigation.js +44 -0
- package/dist/primitives.d.ts +258 -0
- package/dist/primitives.js +244 -0
- package/dist/showcase.d.ts +29 -0
- package/dist/showcase.js +10 -0
- package/dist/workspace.d.ts +80 -0
- package/dist/workspace.js +48 -0
- package/docs/agents.md +127 -0
- package/docs/auth-flows.md +71 -0
- package/docs/migrating-to-0.6.md +94 -0
- package/docs/theming.md +170 -0
- package/package.json +94 -4
- package/skills/mortar/SKILL.md +36 -0
- package/src/application.css +1487 -0
- package/src/application.tsx +1413 -0
- package/src/auth.tsx +452 -0
- package/src/brand.ts +335 -0
- package/src/calendar.css +468 -0
- package/src/calendar.tsx +868 -0
- package/src/choice.tsx +240 -0
- package/src/composer.tsx +158 -0
- package/src/data.css +690 -0
- package/src/display.tsx +452 -0
- package/src/docs.css +704 -0
- package/src/docs.tsx +737 -0
- package/src/ecommerce.css +810 -0
- package/src/ecommerce.tsx +1033 -0
- package/src/feedback.tsx +388 -0
- package/src/foundation.css +1867 -0
- package/src/foundation.tsx +227 -0
- package/src/heading.ts +13 -0
- package/src/icons.tsx +237 -0
- package/src/index.ts +230 -0
- package/src/inspection.tsx +91 -0
- package/src/internal.tsx +614 -0
- package/src/layout.tsx +342 -0
- package/src/marketing-sections.tsx +621 -0
- package/src/marketing.css +2400 -0
- package/src/marketing.tsx +1156 -0
- package/src/navigation.tsx +118 -0
- package/src/overlays.css +350 -0
- package/src/primitives.tsx +1135 -0
- package/src/showcase.tsx +120 -0
- package/src/tokens.css +449 -0
- package/src/workspace.tsx +333 -0
- package/styles.css +56 -0
package/TESTING.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Component test contract
|
|
2
|
+
|
|
3
|
+
Mortar tests observable behaviour, not a line-coverage percentage. Everything here runs locally without a service account. CI requires the same checks against a production playground build.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm ci
|
|
7
|
+
npm run check
|
|
8
|
+
npx playwright install chromium
|
|
9
|
+
npm run test:browser
|
|
10
|
+
npm run test:visual
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`npm run check` never rewrites the catalogue: drift fails M007 (a source or preview fingerprint changed) or M008 (the catalogue no longer matches the manifests). Review the change that caused it, then run `npm run catalog`. It regenerates catalog.json and prints which components' fingerprints and contracts changed, so you can confirm the list matches what you meant to change. Formatting source after regeneration requires regenerating again. Never regenerate first: that accepts drift without looking at it.
|
|
14
|
+
|
|
15
|
+
Each browser command builds the playground before serving it. CI sets `MORTAR_PLAYGROUND_PREBUILT=1` so both suites serve the `playground-dist` that `npm run check` already built; set it locally to skip the rebuild when that build is current.
|
|
16
|
+
|
|
17
|
+
`npm test` builds `dist` and runs the node tests. They import the built package, so a single file run with `node --test` refuses a `dist` older than `src` (tests/support/render.mjs).
|
|
18
|
+
|
|
19
|
+
## Manifests and the catalogue
|
|
20
|
+
|
|
21
|
+
Manifests hold only the hand-written contract. `npm run catalog` generates the rest into catalog.json, so a shared change never rewrites every manifest:
|
|
22
|
+
|
|
23
|
+
- `version`: the package version, on the catalogue and on every entry.
|
|
24
|
+
- `integrity`: separate source and preview hashes of the local import closures; source identity also includes shared styles. They identify changes, not approval or a security signature.
|
|
25
|
+
- `contract.propTypes`: for every prop the component itself declares (inherited DOM attributes are omitted), `{ type, required, default? }` from the TypeScript checker. Literal unions and aliases such as `Tone` are expanded and sorted. `contract.props` stays the human-readable description; `tests/manifest-props.test.mjs` fails when a declared prop is undocumented, a documented prop does not exist, or a documented union or prop kind differs from TypeScript.
|
|
26
|
+
- `capabilities`: `theme` and the kit for every component, `keyboard` when it has a registered interaction probe and `responsive` for the composite profile, plus what the manifest declares from the closed vocabulary in `scripts/test-contract.mjs` (`action`, `input`, `validation`, `form`, `navigation`, `status`, `summary`, `table`, and `keyboard` or `responsive` where they are not already derived). Declaring a derived or unknown capability fails M006.
|
|
27
|
+
- `provenance`: `{ kitId, exampleId }`, derived from `kitId`.
|
|
28
|
+
|
|
29
|
+
`contract.dependencies` stays hand-written but must list exactly the catalogue components the source renders, following local helpers and contexts that require a provider (M010).
|
|
30
|
+
|
|
31
|
+
Catalogue shape changes for consumers that read only catalog.json, such as agent tooling and indexers: `contract.propTypes` is new; `contract.states` is removed (it was the same five labels on every component; tested scenarios are `testing.states`); `provenance.sourceDigest` is removed (it was never read); `capabilities` is now complete and ordered (`theme`, kit, then vocabulary order). Every other field keeps its meaning.
|
|
32
|
+
|
|
33
|
+
## Every component
|
|
34
|
+
|
|
35
|
+
Each manifest has a `testing` contract. `target` identifies actual rendered component content inside the preview root, not the catalogue controls. Overlay targets initially identify their opener; the mandatory overlay probe then requires an open dialog. The playground resolves the manifest's exact preview path. A missing component, empty fixture, fallback notice, console error or JavaScript exception fails the harness.
|
|
36
|
+
|
|
37
|
+
Modal's contract, for example:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"testing": {
|
|
42
|
+
"profile": "overlay",
|
|
43
|
+
"target": ".m-button",
|
|
44
|
+
"probe": "dialog",
|
|
45
|
+
"states": []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Profile | Required execution |
|
|
51
|
+
| --------- | -------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| display | Actual component rendering and scoped plus whole-page axe checks, light and dark |
|
|
53
|
+
| control | Display checks plus a registered behaviour probe |
|
|
54
|
+
| overlay | Display checks plus keyboard/pointer opening, open-dialog axe, focus containment and return, dismissal |
|
|
55
|
+
| composite | Display checks plus mobile/desktop page-overflow checks; registered navigation/behaviour probe when applicable |
|
|
56
|
+
|
|
57
|
+
Profiles are reviewed classifications, not an automatic proof that an API has no other states. All public component exports have profiles. The registered probe mapping in `scripts/test-contract.mjs` prevents existing interactive components being downgraded to display-only coverage. A new interactive export needs a probe in that mapping and a corresponding implementation in `tests/support/probes.mjs`. Probe implementations contain real assertions of visible state, input values, callbacks or focus; a prose promise or a test name is insufficient.
|
|
58
|
+
|
|
59
|
+
State fixtures exercise the scenarios a manifest declares in `testing.states`, chosen from the closed list in `scripts/test-contract.mjs`: `disabled`, `loading`, `error`, `empty`, `long`, `success`, `surface`, `inverse`, `coming-soon`, `duplicate-links`, `heading-3`, `scope-container` and `controlled`. ConfirmDialog, for example, declares `loading` and `long`. Every state has a registered browser assertion (`tests/support/states.mjs`, checked by `tests/test-contract.test.mjs`); a declared state without one fails. SignInPanel maps its submitting status to the loading fixture; the suite checks disabled submission, validation errors and an accessible success announcement. Inverse sections additionally check pressed-button and selected-icon contrast for both brands and appearances. New or changed public states must be added to the manifest, fixture and test assertions in the same PR. Existing state coverage is deliberately incremental: it is not a claim of exhaustive coverage of every optional prop. Bug fixes require a reproducing regression test. Reviewers should check state omissions and classifications as carefully as implementation code.
|
|
60
|
+
|
|
61
|
+
The full browser command validates successful evidence for every expected component/theme/state. Missing, skipped, duplicated, failed or incomplete records fail the run, and `test-results/component-contract.json` records the result. Negative tests exercise the validator and preview failures. A contributor able to rewrite the harness could falsify evidence; the report is not a security boundary. Harness, policy, profile and workflow changes need explicit maintainer review.
|
|
62
|
+
|
|
63
|
+
For focused development use `npx playwright test tests/browser/components.spec.mjs --grep Button`. This intentionally does not claim complete-contract coverage; the full command remains mandatory before merge. CI uses the Playwright-pinned browser. `UI_TEST_CHROME` is a local diagnostic override, not the canonical visual environment.
|
|
64
|
+
|
|
65
|
+
ThemeProvider tests check brand, density, light/dark/system changes centrally. The default component sweep covers light/dark, and representative controls/shells exercise green. We do not cross every prop, theme, density and screen size. Global styles and tokens still run the full suite; no dependency graph currently skips required tests.
|
|
66
|
+
|
|
67
|
+
## Visual baselines
|
|
68
|
+
|
|
69
|
+
Sixteen comparisons cover AppShell, DocsShell, PlanComparison and the open ConfirmDialog at 390/1280px in light/dark. Baselines are platform-specific, with CI pinned to Ubuntu 24.04 and the lockfile's Playwright browser. Screenshots wait for fonts and disable animation. They allow a small pixel tolerance, not arbitrary layout changes.
|
|
70
|
+
|
|
71
|
+
Missing baselines fail. Required CI never updates or approves them. A separate read-only workflow renders Linux candidates into an artifact directory, outside the accepted baseline tree. Its success is not a quality gate or approval. To propose new baselines on the matching platform:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npm run test:visual -- --update-snapshots=all
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Review expected/actual/diff artifacts and commit only intentional changes. Initial and changed baseline images are proposals until the maintainer reviews them in the PR. Changes to this command's baseline files do not replace behavioural or accessibility evidence. AI may assist visual review but cannot waive a failed check or authorize merging.
|
|
78
|
+
|
|
79
|
+
GitHub uploads runnable playground, browser evidence, contract report and visual diff artifacts. Inspect mobile/desktop, keyboard/focus and meaningful states; axe and screenshots do not establish complete accessibility or design quality.
|
|
80
|
+
|
|
81
|
+
## Solo-maintainer review
|
|
82
|
+
|
|
83
|
+
Required CI is the automatic merge gate. The repository does not require a second GitHub reviewer: the owner reviews and authorizes merging. Agents may prepare PRs and evidence, but cannot approve themselves or merge without explicit owner instruction. Baseline changes, waivers, policy changes, API removals and release publication remain maintainer decisions. No waiver mechanism is implemented in this test contract; missing obligations fail.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"rules": {
|
|
5
|
+
"M001": "Exports require unique manifests and preview fixtures",
|
|
6
|
+
"M002": "Source and preview paths stay inside the repository",
|
|
7
|
+
"M003": "Dependencies reference known component IDs",
|
|
8
|
+
"M004": "Shared components cannot depend on hosted services",
|
|
9
|
+
"M005": "Component implementations use semantic colour tokens; only src/tokens.css defines colours",
|
|
10
|
+
"M006": "Every component has an executable test profile, supported probe and closed-vocabulary capabilities",
|
|
11
|
+
"M007": "Source and preview import-closure fingerprints must match; generated fields stay out of manifests",
|
|
12
|
+
"M008": "Committed catalogue matches the manifests",
|
|
13
|
+
"M009": "Brands declare only the --m-brand-* contract, and every appearance resolves the accent pair and its inverse",
|
|
14
|
+
"M010": "Manifest dependencies match the catalogue components the source renders"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/bin/mortar.mjs
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mortar: the open, local agent tooling for the Mortar UI kit. An MCP server
|
|
3
|
+
// (`mortar mcp`) and a CLI over the installed package's catalogue and docs.
|
|
4
|
+
// No network, no account, no runtime dependencies.
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { loadData, GUIDES, PACKAGE_ROOT } from "../cli/data.mjs";
|
|
8
|
+
import { searchComponents } from "../cli/search.mjs";
|
|
9
|
+
import { componentDetail, buildPacket } from "../cli/packet.mjs";
|
|
10
|
+
import { auditApp, RULES } from "../cli/audit.mjs";
|
|
11
|
+
|
|
12
|
+
const HELP = `mortar — local agent tooling for the Mortar UI kit (@tonycodes-ui/mortar)
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
mortar mcp Run the MCP server on stdio (for coding agents)
|
|
16
|
+
mortar search <query> [--kit K] [--category C] [--limit N]
|
|
17
|
+
Find components by purpose or name
|
|
18
|
+
mortar component <Name> A component's contract, props and example
|
|
19
|
+
mortar packet <Name...> [--brief "text"]
|
|
20
|
+
A build packet: components, dependencies,
|
|
21
|
+
imports, CSS and theme setup, principles,
|
|
22
|
+
page templates
|
|
23
|
+
mortar audit [path] Read-only audit of an app's Mortar usage
|
|
24
|
+
(exits 1 when it finds errors)
|
|
25
|
+
mortar guide <topic> Print a guide: ${Object.keys(GUIDES).join(", ")}
|
|
26
|
+
mortar kits Kits, categories and components
|
|
27
|
+
mortar skill Print the agent skill (skills/mortar/SKILL.md)
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--json Machine-readable output
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
-v, --version Show the installed Mortar version
|
|
33
|
+
|
|
34
|
+
Add the MCP server to Claude Code:
|
|
35
|
+
claude mcp add mortar -- npx -y @tonycodes-ui/mortar mcp
|
|
36
|
+
|
|
37
|
+
Everything runs locally against the installed package. No account or network.`;
|
|
38
|
+
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const positional = [];
|
|
41
|
+
const flags = {};
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a === "--") {
|
|
45
|
+
positional.push(...argv.slice(i + 1));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (a.startsWith("--")) {
|
|
49
|
+
const [key, inline] = a.slice(2).split(/=(.*)/s);
|
|
50
|
+
if (["json", "help", "version"].includes(key)) flags[key] = true;
|
|
51
|
+
else flags[key] = inline ?? argv[++i];
|
|
52
|
+
} else if (a === "-h") flags.help = true;
|
|
53
|
+
else if (a === "-v") flags.version = true;
|
|
54
|
+
else positional.push(a);
|
|
55
|
+
}
|
|
56
|
+
return { positional, flags };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const out = (text) => process.stdout.write(text + "\n");
|
|
60
|
+
const json = (value) => out(JSON.stringify(value, null, 2));
|
|
61
|
+
const fail = (message) => {
|
|
62
|
+
process.stderr.write(`mortar: ${message}\n`);
|
|
63
|
+
process.exitCode = 2;
|
|
64
|
+
};
|
|
65
|
+
const indent = (text, by = " ") =>
|
|
66
|
+
String(text)
|
|
67
|
+
.split("\n")
|
|
68
|
+
.map((l) => (l ? by + l : l))
|
|
69
|
+
.join("\n");
|
|
70
|
+
|
|
71
|
+
function printComponent(c) {
|
|
72
|
+
out(
|
|
73
|
+
`${c.name} — ${c.kitTitle}${c.categoryTitle ? " › " + c.categoryTitle : ""}`,
|
|
74
|
+
);
|
|
75
|
+
out(c.description);
|
|
76
|
+
out("");
|
|
77
|
+
out(c.import);
|
|
78
|
+
out(`Marker: ${c.marker}`);
|
|
79
|
+
if (c.usage) out(`\nUsage: ${c.usage}`);
|
|
80
|
+
if (c.avoid) out(`Avoid: ${c.avoid}`);
|
|
81
|
+
if (c.accessibility) out(`Accessibility: ${c.accessibility}`);
|
|
82
|
+
out("\nProps:");
|
|
83
|
+
for (const p of c.props)
|
|
84
|
+
out(
|
|
85
|
+
` ${p.name}${p.required ? "" : "?"}: ${p.type}${p.default !== undefined ? ` = ${p.default}` : ""}`,
|
|
86
|
+
);
|
|
87
|
+
if (c.acceptsHtmlAttributes)
|
|
88
|
+
out(" …and the HTML attributes of its root element");
|
|
89
|
+
if (c.dependencies.length) out(`\nRenders: ${c.dependencies.join(", ")}`);
|
|
90
|
+
if (c.testing?.states?.length)
|
|
91
|
+
out(`Tested states: ${c.testing.states.join(", ")}`);
|
|
92
|
+
if (c.templates.length) out(`Page templates: ${c.templates.join(", ")}`);
|
|
93
|
+
if (c.example) out(`\nExample:\n\n${indent(c.example)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
98
|
+
const [command, ...rest] = positional;
|
|
99
|
+
if (flags.version) {
|
|
100
|
+
out(
|
|
101
|
+
JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"))
|
|
102
|
+
.version,
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (flags.help || !command || command === "help") {
|
|
107
|
+
out(HELP);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (command === "mcp") {
|
|
111
|
+
const { serve } = await import("../cli/mcp.mjs");
|
|
112
|
+
await serve();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const data = await loadData();
|
|
116
|
+
switch (command) {
|
|
117
|
+
case "search": {
|
|
118
|
+
const query = rest.join(" ");
|
|
119
|
+
if (!query)
|
|
120
|
+
return fail("search needs a query, e.g. mortar search sign in");
|
|
121
|
+
const results = searchComponents(data, {
|
|
122
|
+
query,
|
|
123
|
+
kit: flags.kit,
|
|
124
|
+
category: flags.category,
|
|
125
|
+
limit: flags.limit ? Number(flags.limit) : 10,
|
|
126
|
+
}).map(({ matched, ...r }) => r);
|
|
127
|
+
if (flags.json) return json(results);
|
|
128
|
+
if (!results.length) return out(`No components match "${query}".`);
|
|
129
|
+
const width = Math.max(...results.map((r) => r.name.length));
|
|
130
|
+
for (const r of results)
|
|
131
|
+
out(
|
|
132
|
+
`${r.name.padEnd(width)} ${r.kit}${r.category ? " › " + r.category : ""}\n${" ".repeat(width + 2)}${r.description}`,
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case "component": {
|
|
137
|
+
if (!rest[0])
|
|
138
|
+
return fail("component needs a name, e.g. mortar component Button");
|
|
139
|
+
const c = data.find(rest[0]);
|
|
140
|
+
if (!c) {
|
|
141
|
+
const near = searchComponents(data, { query: rest[0], limit: 5 }).map(
|
|
142
|
+
(r) => r.name,
|
|
143
|
+
);
|
|
144
|
+
return fail(
|
|
145
|
+
`no component named "${rest[0]}".${near.length ? ` Try: ${near.join(", ")}` : ""}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const detail = componentDetail(data, c);
|
|
149
|
+
return flags.json ? json(detail) : printComponent(detail);
|
|
150
|
+
}
|
|
151
|
+
case "packet": {
|
|
152
|
+
if (!rest.length && !flags.brief)
|
|
153
|
+
return fail('packet needs component names or --brief "…"');
|
|
154
|
+
const packet = buildPacket(data, {
|
|
155
|
+
components: rest,
|
|
156
|
+
brief: flags.brief,
|
|
157
|
+
});
|
|
158
|
+
if (flags.json) return json(packet);
|
|
159
|
+
out(`Build packet — ${packet.package} ${packet.version}`);
|
|
160
|
+
for (const u of packet.unknown ?? [])
|
|
161
|
+
out(
|
|
162
|
+
`Unknown component ${u.name}${u.suggestions.length ? ` (did you mean ${u.suggestions.join(", ")}?)` : ""}`,
|
|
163
|
+
);
|
|
164
|
+
out(`\nInstall:\n ${packet.install}`);
|
|
165
|
+
out(
|
|
166
|
+
`\nCSS (${packet.css.where}):\n${indent(packet.css.code)}\n ${packet.css.notes}`,
|
|
167
|
+
);
|
|
168
|
+
out(`\nTheme:\n${indent(packet.themeProvider.code)}`);
|
|
169
|
+
out(`\nImport:\n ${packet.import}`);
|
|
170
|
+
out("\nComponents:");
|
|
171
|
+
for (const c of packet.components) {
|
|
172
|
+
out(` ${c.name} — ${c.description}`);
|
|
173
|
+
for (const why of c.why ?? []) out(` why: ${why}`);
|
|
174
|
+
const required = c.props.filter((p) => p.required).map((p) => p.name);
|
|
175
|
+
if (required.length) out(` required props: ${required.join(", ")}`);
|
|
176
|
+
}
|
|
177
|
+
if (packet.dependencies.components.length)
|
|
178
|
+
out(
|
|
179
|
+
`\nRendered inside them: ${packet.dependencies.components.map((d) => d.name).join(", ")}`,
|
|
180
|
+
);
|
|
181
|
+
if (packet.templates.length) {
|
|
182
|
+
out("\nPage templates that compose them (playground):");
|
|
183
|
+
for (const t of packet.templates)
|
|
184
|
+
out(` ${t.id} — ${t.title}: ${t.description}`);
|
|
185
|
+
}
|
|
186
|
+
out("\nPrinciples:");
|
|
187
|
+
for (const p of packet.principles)
|
|
188
|
+
out(` ${p.number}. ${p.id} — ${p.instruction}`);
|
|
189
|
+
out(
|
|
190
|
+
`\nGuides: ${packet.guides.map((g) => `mortar guide ${g}`).join(", ")}`,
|
|
191
|
+
);
|
|
192
|
+
out(
|
|
193
|
+
`Full contracts and examples: mortar packet ${packet.components.map((c) => c.name).join(" ")} --json`,
|
|
194
|
+
);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
case "audit": {
|
|
198
|
+
let report;
|
|
199
|
+
try {
|
|
200
|
+
report = await auditApp(rest[0] ?? ".", { data });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return fail(error.message);
|
|
203
|
+
}
|
|
204
|
+
if (flags.json) json(report);
|
|
205
|
+
else {
|
|
206
|
+
out(
|
|
207
|
+
`Mortar audit of ${report.root}\n${report.mortar.range ? `package.json: ${report.mortar.range}, ` : ""}installed: ${report.mortar.installed ?? "none"}, checked against ${report.mortar.checkedAgainst}\n`,
|
|
208
|
+
);
|
|
209
|
+
for (const f of report.findings) {
|
|
210
|
+
out(
|
|
211
|
+
`${f.file}${f.line ? ":" + f.line : ""} ${f.severity} ${f.rule}`,
|
|
212
|
+
);
|
|
213
|
+
out(` ${f.message}`);
|
|
214
|
+
if (f.fix) out(` fix: ${f.fix}`);
|
|
215
|
+
}
|
|
216
|
+
const s = report.summary;
|
|
217
|
+
out(
|
|
218
|
+
`\n${s.error} error${s.error === 1 ? "" : "s"}, ${s.warning} warning${s.warning === 1 ? "" : "s"}, ${s.info} info in ${s.files} files.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (report.summary.error) process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case "guide": {
|
|
225
|
+
const topic = rest[0];
|
|
226
|
+
if (!GUIDES[topic])
|
|
227
|
+
return fail(`guide needs a topic: ${Object.keys(GUIDES).join(", ")}`);
|
|
228
|
+
const text = data.guide(topic);
|
|
229
|
+
return text ? out(text) : fail(`the ${topic} guide is not installed`);
|
|
230
|
+
}
|
|
231
|
+
case "kits": {
|
|
232
|
+
if (flags.json) return json(data.agent.taxonomy.kits);
|
|
233
|
+
for (const k of data.agent.taxonomy.kits) {
|
|
234
|
+
out(`${k.title} (${k.id}) — ${k.description}`);
|
|
235
|
+
for (const c of k.categories)
|
|
236
|
+
out(` ${c.title} (${c.id}): ${c.components.join(", ")}`);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "skill": {
|
|
241
|
+
const file = join(PACKAGE_ROOT, "skills", "mortar", "SKILL.md");
|
|
242
|
+
return existsSync(file)
|
|
243
|
+
? out(readFileSync(file, "utf8").trimEnd())
|
|
244
|
+
: fail("skill not installed");
|
|
245
|
+
}
|
|
246
|
+
case "rules":
|
|
247
|
+
for (const [id, text] of Object.entries(RULES))
|
|
248
|
+
out(`${id.padEnd(24)} ${text}`);
|
|
249
|
+
return;
|
|
250
|
+
default:
|
|
251
|
+
return fail(`unknown command "${command}". Run mortar --help.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
main().catch((error) => {
|
|
256
|
+
process.stderr.write(`mortar: ${error?.stack ?? error}\n`);
|
|
257
|
+
process.exitCode = 2;
|
|
258
|
+
});
|