@starklab/stark-mcp 0.1.0 → 0.2.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/package.json +10 -4
- package/src/adopt/adoptScanReport.js +124 -0
- package/src/adopt/catalog.js +26 -6
- package/src/adopt/foreignDiscoveryResolver.js +276 -0
- package/src/adopt/foreignPropSchemaResolver.js +134 -0
- package/src/adopt/foreignScanReport.js +210 -0
- package/src/adopt/foreignScoringResolver.js +192 -0
- package/src/adopt/foreignSystemConfig.js +356 -0
- package/src/adopt/installedPackageDiscoveryResolver.js +602 -0
- package/src/adopt/installedPackagePropSchemaResolver.js +279 -0
- package/src/adopt/installedPackageScoringResolver.js +153 -0
- package/src/adopt/installedSystemAutoDetector.js +51 -0
- package/src/adopt/installedSystemScan.js +101 -0
- package/src/adopt/jsxOpportunityHelpers.js +99 -0
- package/src/adopt/moduleGraph.js +39 -8
- package/src/adopt/opportunityResolver.js +255 -0
- package/src/adopt/opportunitySignaturesNative.js +47 -0
- package/src/adopt/usageRulesResolver.js +298 -0
- package/src/adopt/vecnaMaterializer.js +165 -0
- package/src/adopt/vecnaVerifier.js +127 -0
- package/src/cli.js +407 -1
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Home.jsx +0 -21
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Menu.jsx +0 -13
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Profile.jsx +0 -11
- package/src/adopt/__fixtures__/dominion-fixture-app/src/theme.css +0 -34
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/AppButton.jsx +0 -8
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/BrandButton.jsx +0 -9
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/CardBase.jsx +0 -9
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/FeatureCard.jsx +0 -7
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/SectionCard.jsx +0 -12
- package/src/adopt/dominionFixture.test.js +0 -165
- package/src/adopt/propApiResolver.test.js +0 -229
- package/src/adopt/referenceResolver.test.js +0 -213
- package/src/adopt/rnTailwindResolver.test.js +0 -263
- package/src/adopt/rnTokenAliasResolver.test.js +0 -260
- package/src/adopt/tailwindResolver.test.js +0 -178
- package/src/adopt/targetDiscovery.test.js +0 -227
- package/src/adopt/tokenAliasResolver.test.js +0 -319
- package/src/adopt/wrapperResolver.test.js +0 -324
- package/src/data.test.js +0 -231
package/src/cli.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
3
5
|
|
|
4
6
|
import {
|
|
5
7
|
listComponents,
|
|
@@ -13,9 +15,20 @@ import { resolveWrappers } from './adopt/wrapperResolver.js';
|
|
|
13
15
|
import { resolveTokenAliases } from './adopt/tokenAliasResolver.js';
|
|
14
16
|
import { resolveTailwindTokens } from './adopt/tailwindResolver.js';
|
|
15
17
|
import { resolvePropApi } from './adopt/propApiResolver.js';
|
|
18
|
+
import { resolveUsageRules } from './adopt/usageRulesResolver.js';
|
|
19
|
+
import { resolveOpportunities } from './adopt/opportunityResolver.js';
|
|
16
20
|
import { resolveRnTokenAliases } from './adopt/rnTokenAliasResolver.js';
|
|
17
21
|
import { resolveRnTailwindTokens } from './adopt/rnTailwindResolver.js';
|
|
18
22
|
import { discoverTargets, workspacePackageMap } from './adopt/targetDiscovery.js';
|
|
23
|
+
import { verifyVecnaLayout } from './adopt/vecnaVerifier.js';
|
|
24
|
+
import { resolveForeignDiscovery } from './adopt/foreignDiscoveryResolver.js';
|
|
25
|
+
import { resolvePropSchema } from './adopt/foreignPropSchemaResolver.js';
|
|
26
|
+
import { scoreForeignAdoption } from './adopt/foreignScoringResolver.js';
|
|
27
|
+
import { FOREIGN_SYSTEMS, listForeignSystems, systemPackages } from './adopt/foreignSystemConfig.js';
|
|
28
|
+
import { detectForeignSystems } from './adopt/installedSystemAutoDetector.js';
|
|
29
|
+
import { scanInstalledSystem } from './adopt/installedSystemScan.js';
|
|
30
|
+
import { reportForeignScan } from './adopt/foreignScanReport.js';
|
|
31
|
+
import { reportAdoptScan } from './adopt/adoptScanReport.js';
|
|
19
32
|
|
|
20
33
|
function print(value) {
|
|
21
34
|
console.log(JSON.stringify(value, null, 2));
|
|
@@ -26,6 +39,43 @@ function fail(message) {
|
|
|
26
39
|
process.exitCode = 1;
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/** How a foreign system is identified in scan-foreign's human-readable
|
|
43
|
+
* lists: its package name, or — for a system with no umbrella package, whose
|
|
44
|
+
* name would otherwise be one arbitrary sibling of forty — the count. */
|
|
45
|
+
function packageSummary(system) {
|
|
46
|
+
if (system.npmPackages && system.npmPackages.length > 1) {
|
|
47
|
+
return `${system.npmPackages.length} packages, e.g. ${system.npmPackages[0]}`;
|
|
48
|
+
}
|
|
49
|
+
return system.npmPackage ?? system.distribution;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Reported to the tracker alongside every scan, so a coverage number can
|
|
53
|
+
// always be traced back to the code that produced it.
|
|
54
|
+
const SCANNER_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
|
55
|
+
|
|
56
|
+
/** The commit a reported scan describes. CI almost always already knows the
|
|
57
|
+
* SHA it checked out, so those variables are preferred over shelling out;
|
|
58
|
+
* `git rev-parse` covers a local run. A tree with neither reports nothing
|
|
59
|
+
* rather than inventing a placeholder — a coverage row keyed to a fake commit
|
|
60
|
+
* is worse than one that was never written. */
|
|
61
|
+
function resolveCommitSha(root) {
|
|
62
|
+
const fromEnv =
|
|
63
|
+
process.env.GITHUB_SHA ||
|
|
64
|
+
process.env.VERCEL_GIT_COMMIT_SHA ||
|
|
65
|
+
process.env.CI_COMMIT_SHA ||
|
|
66
|
+
process.env.BUILDKITE_COMMIT;
|
|
67
|
+
if (fromEnv) return fromEnv;
|
|
68
|
+
try {
|
|
69
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
70
|
+
cwd: root,
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
73
|
+
}).trim();
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
29
79
|
function hasFlag(args, name) {
|
|
30
80
|
return args.includes(`--${name}`);
|
|
31
81
|
}
|
|
@@ -49,6 +99,20 @@ function runAdopt(root, platform, ignore, workspacePackages) {
|
|
|
49
99
|
// prop-mapping/ is flat, with no rn/ subdirectory — there is no artifact
|
|
50
100
|
// to validate an RN prop against, so metric 6 only runs on web.
|
|
51
101
|
const propApi = platform === 'web' ? resolvePropApi(root, { platform, ignore }) : null;
|
|
102
|
+
// Runs the Vecna conformance gate's own component-usage rules
|
|
103
|
+
// (checkComponentRules — Toolbar count, Button primary-variant count,
|
|
104
|
+
// DropdownMenu empty-items) against this repo's real JSX, closing the R3
|
|
105
|
+
// gap: the scanner and the gate used to have zero references to each
|
|
106
|
+
// other in either direction (ADOPTION_APP_PLAN.md §10 decision #3). Web
|
|
107
|
+
// only — see usageRulesResolver.js's header for why.
|
|
108
|
+
const usageRules = platform === 'web' ? resolveUsageRules(root, { platform, ignore }) : null;
|
|
109
|
+
// Scans for hand-rolled UI patterns resembling a catalog component,
|
|
110
|
+
// regardless of whether this repo has adopted Stark at all — closes R3
|
|
111
|
+
// dimension 1: every other resolver above only has something to say once
|
|
112
|
+
// a @starklab/stk* import is already in scope. DOM/JSX-shape signatures
|
|
113
|
+
// (role="toolbar", native <input>/<button>) — web only, see
|
|
114
|
+
// opportunityResolver.js's header for why.
|
|
115
|
+
const opportunities = platform === 'web' ? resolveOpportunities(root, { platform, ignore }) : null;
|
|
52
116
|
// The RN JS-object token indirection resolver is the inverse: it only
|
|
53
117
|
// makes sense for the native platform (see rnTokenAliasResolver.js).
|
|
54
118
|
const rnTokenAliases = platform === 'native' ? resolveRnTokenAliases(root, { platform, ignore }) : null;
|
|
@@ -65,6 +129,8 @@ function runAdopt(root, platform, ignore, workspacePackages) {
|
|
|
65
129
|
tokenAliases,
|
|
66
130
|
tailwind,
|
|
67
131
|
propApi,
|
|
132
|
+
usageRules,
|
|
133
|
+
opportunities,
|
|
68
134
|
rnTokenAliases,
|
|
69
135
|
rnTailwind,
|
|
70
136
|
};
|
|
@@ -79,7 +145,12 @@ Usage:
|
|
|
79
145
|
stark-cli manifest
|
|
80
146
|
stark-cli eject <Component> [--platform=web|native] [--out=<dir>] [--force]
|
|
81
147
|
stark-cli adopt [path] [--platform=web|native] [--ignore=<glob>,<glob>,...] [--all-targets]
|
|
148
|
+
[--report=<url> --target-dir=<dir> [--commit=<sha>]]
|
|
82
149
|
stark-cli targets [path]
|
|
150
|
+
stark-cli verify-vecna <path-to-layout.json> [--out=<dir>]
|
|
151
|
+
stark-cli scan-foreign <system-id-or-npm-package|auto> [path] [--platform=web|native] [--allow-network] [--ignore=<glob>,<glob>,...]
|
|
152
|
+
[--report=<url> --target-dir=<dir> [--commit=<sha>]]
|
|
153
|
+
stark-cli scan-foreign List the tested systems (pass one's id, or any raw npm package name)
|
|
83
154
|
|
|
84
155
|
Options:
|
|
85
156
|
--platform=<web|native> Target platform for "props", "eject", and "adopt" (default: web)
|
|
@@ -176,6 +247,28 @@ missing feature; population is a separate, later pass. Not run for
|
|
|
176
247
|
--platform=native (prop-mapping/ has no RN equivalent) — metric 6 is a
|
|
177
248
|
visible "n/a" there, never an implicit zero.
|
|
178
249
|
|
|
250
|
+
Also on the web platform, "adopt" scans for hand-rolled UI patterns that
|
|
251
|
+
resemble a catalog component, whether or not the repo has adopted Stark at
|
|
252
|
+
all — reported under "opportunities". Every other check above only has
|
|
253
|
+
something to say about JSX that already imports @starklab/stk-components;
|
|
254
|
+
a 0%-adoption repo produces zero findings from all of them, not "here's
|
|
255
|
+
what you're missing." This closes that gap with a small, fixed set of
|
|
256
|
+
named structural signatures, never fuzzy/statistical scoring: a native
|
|
257
|
+
<button> with both an onClick handler and visual styling (style touching
|
|
258
|
+
background/border/padding, or a className matching /btn|button/) suggests
|
|
259
|
+
Button; a native <input type="text"|"email"|...> paired with a <label>
|
|
260
|
+
suggests TextInput; an element with role="toolbar", or a flex/row container
|
|
261
|
+
whose only significant children are 2+ native <button>s, suggests Toolbar.
|
|
262
|
+
Each finding carries "confidence" ("high"|"medium", a suggestion strength)
|
|
263
|
+
separate from "severity", which is always "Info" — an opportunity is not a
|
|
264
|
+
defect and must never read as a CI-gate failure. Output is deliberately
|
|
265
|
+
keyed "opportunities", the same word "targets"/"adopt --all-targets" use
|
|
266
|
+
for a different, package-granularity idea (a whole excluded workspace
|
|
267
|
+
target that still renders UI) — the two can appear side by side under
|
|
268
|
+
"--all-targets" but never occupy the same key: that one is top-level, this
|
|
269
|
+
one is nested per-target under its own "opportunities". Not run for
|
|
270
|
+
--platform=native (the signatures are DOM/JSX-shape assumptions).
|
|
271
|
+
|
|
179
272
|
On the native platform, "adopt" instead resolves the consumer's own JS-
|
|
180
273
|
object token indirection — RN has no CSS custom properties, so the
|
|
181
274
|
indirection is a local const or object property instead of var(), e.g.
|
|
@@ -253,10 +346,152 @@ without this, a shared internal UI package would score near-0% in every
|
|
|
253
346
|
consuming target and 100% in the one that defines it, the same inverted-
|
|
254
347
|
result problem catalog left-joining exists to prevent, recurring one level
|
|
255
348
|
up at package granularity.
|
|
349
|
+
|
|
350
|
+
"adopt --report=<url>" additionally POSTs the scan to a stark.dominion
|
|
351
|
+
tracker (its /api/adoption-scan endpoint), the Stark-adoption counterpart
|
|
352
|
+
of "scan-foreign --report" and the way a repo-connected target's real
|
|
353
|
+
adoption score gets into the dashboard. It takes the same
|
|
354
|
+
"--target-dir=<dir>" (required), "--commit=<sha>", STARK_DOMINION_TOKEN /
|
|
355
|
+
"--token=<secret>" and never-fail-the-build behaviour documented under
|
|
356
|
+
"scan-foreign" below, and the same path scrubbing. What is sent is a
|
|
357
|
+
reduced projection: every per-site enumeration ("sites", "usages",
|
|
358
|
+
"properties", "checks") is dropped, since the tracker reads counts and the
|
|
359
|
+
three-number reports, never the enumeration behind them. Findings are kept.
|
|
360
|
+
"--report" is not supported together with "--all-targets" — a reported scan
|
|
361
|
+
belongs to exactly one tracker target, so run one "adopt <dir>
|
|
362
|
+
--report --target-dir=<dir>" per target instead of reporting a whole
|
|
363
|
+
discovery sweep under a single dir.
|
|
364
|
+
|
|
365
|
+
"verify-vecna <path-to-layout.json>" is the reverse direction of "adopt":
|
|
366
|
+
instead of scanning a consumer's real code for catalog usage, it checks one
|
|
367
|
+
of Vecna's own generated LayoutConfig JSON files against the same catalog.
|
|
368
|
+
The JSON is compiled into a real .jsx file with a real import statement
|
|
369
|
+
from '@starklab/stk-components' (see adopt/vecnaMaterializer.js), written
|
|
370
|
+
under --out (default ./.vecna-verify), then the real wrapperResolver/
|
|
371
|
+
tokenAliasResolver/propApiResolver run against it unmodified — not a
|
|
372
|
+
second, narrower reimplementation of the checks.
|
|
373
|
+
|
|
374
|
+
Of those three, only "propApi" can produce real findings here: it validates
|
|
375
|
+
literal call-site JSX props (invalid enum values, missing required props,
|
|
376
|
+
deprecated props, className/style escape hatches) against
|
|
377
|
+
prop-mapping/*.mapping.json, which is exactly what a materialized layout is.
|
|
378
|
+
"wrappers" and "tokenAliases" are included for parity with "adopt"'s own
|
|
379
|
+
resolver set, but always report empty by construction — Vecna's output
|
|
380
|
+
never contains a local wrapper component (wrappers' whole domain) or a CSS
|
|
381
|
+
custom-property alias chain (tokenAliases' whole domain). This is marked
|
|
382
|
+
explicitly per-resolver as "vacuousByConstruction" in the output rather than
|
|
383
|
+
silently reported as "0 findings," so it never reads as broader coverage
|
|
384
|
+
than it has.
|
|
385
|
+
|
|
386
|
+
A layout node whose type isn't in the catalog (the 5 hardcoded layout
|
|
387
|
+
primitives — logo/heading/text/divider/spacer — or any future node type
|
|
388
|
+
added without a matching prop-mapping layoutSchema) is skipped and listed
|
|
389
|
+
under "materialized.skippedNodes" with a reason, never guessed.
|
|
390
|
+
|
|
391
|
+
"scan-foreign <system-id-or-npm-package> [path]" is EXPERIMENTAL —
|
|
392
|
+
ADOPTION_APP_PLAN.md §10 decision #25, "Version B": it answers a different
|
|
393
|
+
question than "adopt": not "how much of @starklab/stk does this repo use,"
|
|
394
|
+
but "what does this repo's OWN (non-Stark) design system look like, and how
|
|
395
|
+
consistently does the repo itself use it." No opinionated conformance rules
|
|
396
|
+
and no CI-gate severity — Stark has no house opinion about a design system
|
|
397
|
+
it doesn't own; this reports facts, not defects. Two distribution modes,
|
|
398
|
+
dispatched by the first argument:
|
|
399
|
+
|
|
400
|
+
Phase 1 — "copy-paste" distribution (currently shadcn/ui only, <system> =
|
|
401
|
+
"shadcn"): the component source ships directly in the consumer's repo (e.g.
|
|
402
|
+
via "npx shadcn init"), so there's no node_modules boundary to cross.
|
|
403
|
+
Discovery detects the package (components.json / @radix-ui/* deps), enumerates
|
|
404
|
+
components by parsing every .tsx/.jsx directly under a components/ui
|
|
405
|
+
directory, and detects the token source (CSS custom properties matching
|
|
406
|
+
--background/--primary/--radius). Prop schema is extracted from each
|
|
407
|
+
component's own "NameProps" interface/type, parsed straight from its file.
|
|
408
|
+
Structural scoring covers catalog coverage (which discovered components are
|
|
409
|
+
actually imported elsewhere in the repo) plus hand-rolled-duplicate detection
|
|
410
|
+
for native <button> markup outside the catalog's own directory.
|
|
411
|
+
|
|
412
|
+
Phase 2 — "installed-package" distribution (any React/React Native design
|
|
413
|
+
system published to npm and already installed in the target repo — MUI,
|
|
414
|
+
Chakra, Carbon, or any other; pass a registered id like "mui" for a friendly
|
|
415
|
+
label, or any raw npm package name like "@mui/material" directly — the
|
|
416
|
+
registry is a convenience, never a gate): real component/prop types live in
|
|
417
|
+
node_modules/<package>'s own .d.ts, so discovery and prop-schema extraction
|
|
418
|
+
both use the TypeScript compiler API (checker.getExportsOfModule,
|
|
419
|
+
checker.getPropertiesOfType) against the package's resolved types entry
|
|
420
|
+
instead of parsing local .tsx source — this also resolves generic prop
|
|
421
|
+
aliases like ComponentProps<'div'> that a Babel-AST walk (Phase 1's approach)
|
|
422
|
+
cannot. Component enumeration covers the package's FULL exported catalog,
|
|
423
|
+
not just the components the target repo happens to import. Structural
|
|
424
|
+
scoring covers catalog coverage (bare-import usage, resolveOrigin-based) plus
|
|
425
|
+
hand-rolled-duplicate detection — reusing the same opportunity signatures
|
|
426
|
+
"adopt" uses, web (native <button>-shaped) or native (Pressable/
|
|
427
|
+
TouchableOpacity + onPress + StyleSheet-shaped) per --platform. Reads only
|
|
428
|
+
already-installed node_modules by default: no "npm install" is ever run
|
|
429
|
+
(that would execute install-time lifecycle scripts — real code execution
|
|
430
|
+
from scanning a repo). If the package isn't installed, pass --allow-network
|
|
431
|
+
to fetch its published package.json + .d.ts read-only from a CDN
|
|
432
|
+
(cdn.jsdelivr.net — static file GETs only, still never installed or
|
|
433
|
+
executed); without that flag, an uninstalled package reports
|
|
434
|
+
"unresolvedReason" rather than fetching anything. --platform (default
|
|
435
|
+
"web") selects RN vs DOM component-file conventions and duplicate-detection
|
|
436
|
+
signatures; --ignore works the same as "adopt"'s.
|
|
437
|
+
|
|
438
|
+
A system does not have to be one package. Some ship as many sibling
|
|
439
|
+
packages with no umbrella barrel — Atlaskit's ~100 @atlaskit/* packages are
|
|
440
|
+
the whole system, with no @atlaskit/core to name — so a registered entry may
|
|
441
|
+
declare several, and a raw argument may be a comma-separated list
|
|
442
|
+
("@atlaskit/button,@atlaskit/textfield"). Each package is discovered and scored
|
|
443
|
+
separately and the results are aggregated: output then carries
|
|
444
|
+
"multiPackage": true, a "packages" array, and "totals" (coverage summed,
|
|
445
|
+
never averaged across packages), with repo-wide facts — "root",
|
|
446
|
+
"platform", "duplicates" — hoisted out of the per-package entries. A
|
|
447
|
+
one-package scan's output shape is unchanged.
|
|
448
|
+
|
|
449
|
+
Known gap for Phase 2 (not built): JS-theme-object token detection
|
|
450
|
+
(MUI/Chakra createTheme()/extendTheme() call-site parsing) — token source is
|
|
451
|
+
not reported for installed-package systems in this pass.
|
|
452
|
+
|
|
453
|
+
Two ways to skip typing a package name: "stark-cli scan-foreign" with no
|
|
454
|
+
system argument prints the tested-systems list (id, label, npm package,
|
|
455
|
+
platform) instead of running a scan — pick one's id, or fall through to any
|
|
456
|
+
raw npm package name for a system that isn't listed ("Other" is just
|
|
457
|
+
passing the package name directly, never a separate mode). "stark-cli
|
|
458
|
+
scan-foreign auto [path]" instead reads [path]'s (default ".") own
|
|
459
|
+
package.json dependency graph and matches it against that same tested list
|
|
460
|
+
— if it's a registered dependency already, there's no reason to make the
|
|
461
|
+
caller re-type what the repo itself already declares. Zero matches fails
|
|
462
|
+
with the tested list (nothing registered is installed — pass a system id or
|
|
463
|
+
raw package name explicitly); more than one match fails and lists what
|
|
464
|
+
matched (a repo can depend on more than one design system; "auto" only
|
|
465
|
+
resolves the unambiguous case) rather than guessing which one you meant.
|
|
466
|
+
|
|
467
|
+
"--report=<url>" additionally POSTs the scan to a stark.dominion tracker
|
|
468
|
+
(its /api/foreign-scan endpoint), which is how a foreign coverage number
|
|
469
|
+
gets from a consumer's CI into the dashboard — the scan has to run where
|
|
470
|
+
node_modules exists, so it runs there and pushes, rather than the tracker
|
|
471
|
+
pulling. "--target-dir=<dir>" names the tracker target the scan belongs to
|
|
472
|
+
and is required with it. The bearer secret is read from
|
|
473
|
+
STARK_DOMINION_TOKEN, or from "--token=<secret>" — prefer the environment
|
|
474
|
+
variable, since a flag is visible in the process list and in most CI logs.
|
|
475
|
+
The commit comes from GITHUB_SHA / VERCEL_GIT_COMMIT_SHA / CI_COMMIT_SHA /
|
|
476
|
+
BUILDKITE_COMMIT, then "git rev-parse HEAD", and can be set explicitly with
|
|
477
|
+
"--commit=<sha>".
|
|
478
|
+
|
|
479
|
+
Reporting never fails the build and never changes what is printed. An
|
|
480
|
+
unreachable tracker, a rotated token or a missing target is written to
|
|
481
|
+
stderr as a skipped-report line; stdout is the same JSON document either
|
|
482
|
+
way and the exit code is the scan's own. What gets sent is a reduced
|
|
483
|
+
projection of that document, not a copy of it: prop schemas are dropped
|
|
484
|
+
(the tracker does not read them, and re-scanning re-derives them — on a
|
|
485
|
+
real @mui/material scan this is 2.18 MB down to 10.6 KB), and every
|
|
486
|
+
absolute path is rewritten to "<root>"/"<home>" before the payload leaves
|
|
487
|
+
the machine. Paths are scrubbed only on the reported copy — stdout keeps
|
|
488
|
+
the real ones, which is what is actually useful when debugging a local
|
|
489
|
+
scan.
|
|
256
490
|
`;
|
|
257
491
|
|
|
258
492
|
const [, , command, ...rest] = process.argv;
|
|
259
493
|
|
|
494
|
+
async function main() {
|
|
260
495
|
switch (command) {
|
|
261
496
|
case 'list':
|
|
262
497
|
print(listComponents());
|
|
@@ -321,7 +556,19 @@ switch (command) {
|
|
|
321
556
|
const ignoreArg = flagValue(rest, 'ignore', '');
|
|
322
557
|
const ignore = ignoreArg ? ignoreArg.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
323
558
|
|
|
559
|
+
const reportUrl = flagValue(rest, 'report', undefined);
|
|
560
|
+
|
|
324
561
|
if (hasFlag(rest, 'all-targets')) {
|
|
562
|
+
if (reportUrl) {
|
|
563
|
+
// Refused rather than silently ignored. A reported scan is keyed to
|
|
564
|
+
// one tracker target (targets.dir + the per-target bearer secret), and
|
|
565
|
+
// an --all-targets sweep produces one result per discovered directory
|
|
566
|
+
// — there is no single dir the aggregate honestly belongs to. Filing
|
|
567
|
+
// it under whatever --target-dir happened to be passed would attribute
|
|
568
|
+
// every target's numbers to one of them.
|
|
569
|
+
fail('--report cannot be combined with --all-targets. Run "stark-cli adopt <dir> --report=<url> --target-dir=<dir>" once per target.');
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
325
572
|
try {
|
|
326
573
|
const discovery = discoverTargets(root);
|
|
327
574
|
const workspacePackages = workspacePackageMap(discovery);
|
|
@@ -344,11 +591,37 @@ switch (command) {
|
|
|
344
591
|
break;
|
|
345
592
|
}
|
|
346
593
|
|
|
594
|
+
let adoptResult = null;
|
|
347
595
|
try {
|
|
348
|
-
|
|
596
|
+
adoptResult = runAdopt(root, platform, ignore);
|
|
597
|
+
print(adoptResult);
|
|
349
598
|
} catch (err) {
|
|
599
|
+
adoptResult = null;
|
|
350
600
|
fail(err.message);
|
|
351
601
|
}
|
|
602
|
+
|
|
603
|
+
// Same placement and the same reasoning as scan-foreign's reporting block
|
|
604
|
+
// below: outside the scan's try/catch, so a fault in the reporting path
|
|
605
|
+
// can never reach fail() and turn a successful scan into a non-zero exit.
|
|
606
|
+
// Outcome goes to stderr only; stdout stays a clean JSON document whether
|
|
607
|
+
// or not a tracker is configured.
|
|
608
|
+
if (adoptResult && reportUrl) {
|
|
609
|
+
const token = flagValue(rest, 'token', process.env.STARK_DOMINION_TOKEN);
|
|
610
|
+
const outcome = await reportAdoptScan(adoptResult, {
|
|
611
|
+
url: reportUrl,
|
|
612
|
+
token,
|
|
613
|
+
root,
|
|
614
|
+
targetDir: flagValue(rest, 'target-dir', undefined),
|
|
615
|
+
commitSha: flagValue(rest, 'commit', resolveCommitSha(root)),
|
|
616
|
+
scannerVersion: SCANNER_VERSION,
|
|
617
|
+
}).catch((err) => ({ reported: false, reason: err.message }));
|
|
618
|
+
|
|
619
|
+
if (outcome.reported) {
|
|
620
|
+
console.error(`Reported to ${reportUrl} (adoption scan #${outcome.response?.id ?? '?'}).`);
|
|
621
|
+
} else {
|
|
622
|
+
console.error(`Scan complete; reporting was skipped: ${outcome.reason}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
352
625
|
break;
|
|
353
626
|
}
|
|
354
627
|
|
|
@@ -364,6 +637,136 @@ switch (command) {
|
|
|
364
637
|
break;
|
|
365
638
|
}
|
|
366
639
|
|
|
640
|
+
case 'verify-vecna': {
|
|
641
|
+
const [maybePath] = rest;
|
|
642
|
+
if (!maybePath || maybePath.startsWith('--')) {
|
|
643
|
+
fail('Usage: stark-cli verify-vecna <path-to-layout.json> [--out=<dir>]');
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
const layoutPath = path.resolve(process.cwd(), maybePath);
|
|
647
|
+
const outDir = flagValue(rest, 'out', undefined);
|
|
648
|
+
const slug = path.basename(layoutPath).replace(/\.layout\.json$|\.json$/, '');
|
|
649
|
+
try {
|
|
650
|
+
const layoutConfig = JSON.parse(readFileSync(layoutPath, 'utf-8'));
|
|
651
|
+
print(verifyVecnaLayout(layoutConfig, { outDir, slug }));
|
|
652
|
+
} catch (err) {
|
|
653
|
+
fail(err.message);
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
case 'scan-foreign': {
|
|
659
|
+
const [systemArg, maybePath] = rest;
|
|
660
|
+
if (!systemArg) {
|
|
661
|
+
print({
|
|
662
|
+
hint: 'Pass a system id below, any raw npm package name for one not listed, or "auto" to detect from [path]\'s package.json.',
|
|
663
|
+
systems: listForeignSystems(),
|
|
664
|
+
});
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
const target = maybePath && !maybePath.startsWith('--') ? maybePath : '.';
|
|
668
|
+
const root = path.resolve(process.cwd(), target);
|
|
669
|
+
const ignoreArg = flagValue(rest, 'ignore', '');
|
|
670
|
+
const ignore = ignoreArg ? ignoreArg.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
671
|
+
const platform = flagValue(rest, 'platform', 'web');
|
|
672
|
+
const allowNetwork = hasFlag(rest, 'allow-network');
|
|
673
|
+
|
|
674
|
+
// "auto" resolves to a real registered system id up front, then falls
|
|
675
|
+
// straight into the same dispatch below — it never becomes a third
|
|
676
|
+
// code path of its own. Ambiguous (>1) and no-match cases fail with
|
|
677
|
+
// the candidates/tested list rather than guessing, same "never guess
|
|
678
|
+
// among ambiguous candidates" discipline as the rest of adopt/.
|
|
679
|
+
let resolvedSystemArg = systemArg;
|
|
680
|
+
if (systemArg === 'auto') {
|
|
681
|
+
const detected = detectForeignSystems(root, { ignore });
|
|
682
|
+
if (detected.length === 0) {
|
|
683
|
+
fail(
|
|
684
|
+
`No tested system's dependency was found in ${root}'s package.json — pass a system id or raw npm package name explicitly. Tested systems:\n` +
|
|
685
|
+
listForeignSystems().map((s) => ` ${s.id} — ${s.label} (${packageSummary(s)})`).join('\n')
|
|
686
|
+
);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
if (detected.length > 1) {
|
|
690
|
+
fail(
|
|
691
|
+
`More than one tested system's dependency was found in ${root}'s package.json — pass one explicitly:\n` +
|
|
692
|
+
detected.map((d) => ` ${d.id} — ${d.label} (${packageSummary(d)})`).join('\n')
|
|
693
|
+
);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
resolvedSystemArg = detected[0].id;
|
|
697
|
+
console.error(`Auto-detected "${detected[0].label}" (${packageSummary(detected[0])}) from ${root}'s package.json dependencies.`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// A registered system id (e.g. "shadcn", "mui") dispatches on its own
|
|
701
|
+
// "distribution" shape; anything else is treated as a raw npm package
|
|
702
|
+
// name, defaulting to the "installed-package" path — this is what makes
|
|
703
|
+
// "whatever design system" real rather than a curated list (see
|
|
704
|
+
// foreignSystemConfig.js's header comment).
|
|
705
|
+
const registered = FOREIGN_SYSTEMS[resolvedSystemArg];
|
|
706
|
+
|
|
707
|
+
const reportUrl = flagValue(rest, 'report', undefined);
|
|
708
|
+
|
|
709
|
+
let result = null;
|
|
710
|
+
try {
|
|
711
|
+
if (registered && registered.distribution === 'copy-paste') {
|
|
712
|
+
const discovery = resolveForeignDiscovery(root, resolvedSystemArg, { ignore });
|
|
713
|
+
const propSchema = resolvePropSchema(root, resolvedSystemArg, discovery.components);
|
|
714
|
+
const scoring = scoreForeignAdoption(root, resolvedSystemArg, discovery, { ignore });
|
|
715
|
+
result = { discovery, propSchema, scoring };
|
|
716
|
+
} else {
|
|
717
|
+
// A registered system contributes its declared package(s); an
|
|
718
|
+
// unregistered argument is a raw package name, or a comma-separated
|
|
719
|
+
// list of them — the same multi-package shape the registry can now
|
|
720
|
+
// express, kept available to systems nobody has registered, since
|
|
721
|
+
// this command's whole premise is "whatever design system," not a
|
|
722
|
+
// curated list. Commas are safe as the separator: npm names can't
|
|
723
|
+
// contain one.
|
|
724
|
+
const packages = registered
|
|
725
|
+
? systemPackages(registered)
|
|
726
|
+
: resolvedSystemArg.split(',').map((s) => s.trim()).filter(Boolean);
|
|
727
|
+
result = await scanInstalledSystem(root, packages, {
|
|
728
|
+
platform,
|
|
729
|
+
ignore,
|
|
730
|
+
allowNetwork,
|
|
731
|
+
system: registered?.id ?? null,
|
|
732
|
+
label: registered?.label ?? null,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
print(result);
|
|
736
|
+
} catch (err) {
|
|
737
|
+
fail(err.message);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Reporting sits outside the scan's own try/catch on purpose. A scan that
|
|
741
|
+
// failed has already reported that failure and left `result` null, so
|
|
742
|
+
// nothing is sent; and a fault in the reporting path can never reach
|
|
743
|
+
// `fail()` and turn a successful scan into a non-zero exit. Its outcome
|
|
744
|
+
// goes to stderr only, so stdout stays a clean JSON document whether or
|
|
745
|
+
// not a tracker is configured. See foreignScanReport.js's header for why
|
|
746
|
+
// never breaking the consumer's CI is the governing constraint.
|
|
747
|
+
if (result && reportUrl) {
|
|
748
|
+
// The token is read from the environment by default: a secret passed as
|
|
749
|
+
// a flag is visible in the process list and in most CI logs.
|
|
750
|
+
const token = flagValue(rest, 'token', process.env.STARK_DOMINION_TOKEN);
|
|
751
|
+
const outcome = await reportForeignScan(result, {
|
|
752
|
+
url: reportUrl,
|
|
753
|
+
token,
|
|
754
|
+
root,
|
|
755
|
+
targetDir: flagValue(rest, 'target-dir', undefined),
|
|
756
|
+
commitSha: flagValue(rest, 'commit', resolveCommitSha(root)),
|
|
757
|
+
scannerVersion: SCANNER_VERSION,
|
|
758
|
+
label: registered?.label ?? null,
|
|
759
|
+
}).catch((err) => ({ reported: false, reason: err.message }));
|
|
760
|
+
|
|
761
|
+
if (outcome.reported) {
|
|
762
|
+
console.error(`Reported to ${reportUrl} (foreign scan #${outcome.response?.id ?? '?'}).`);
|
|
763
|
+
} else {
|
|
764
|
+
console.error(`Scan complete; reporting was skipped: ${outcome.reason}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
|
|
367
770
|
case undefined:
|
|
368
771
|
case '--help':
|
|
369
772
|
case '-h':
|
|
@@ -374,3 +777,6 @@ switch (command) {
|
|
|
374
777
|
default:
|
|
375
778
|
fail(`Unknown command "${command}".\n\n${HELP}`);
|
|
376
779
|
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
main();
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Button } from '@starklab/stk-components';
|
|
3
|
-
import { AppButton } from '../wrappers/AppButton';
|
|
4
|
-
import { BrandButton } from '../wrappers/BrandButton';
|
|
5
|
-
import { FeatureCard } from '../wrappers/FeatureCard';
|
|
6
|
-
|
|
7
|
-
export function Home() {
|
|
8
|
-
return (
|
|
9
|
-
<div className="dark:bg-primary">
|
|
10
|
-
<AppButton>Save</AppButton>
|
|
11
|
-
<BrandButton>Buy now</BrandButton>
|
|
12
|
-
<BrandButton>Learn more</BrandButton>
|
|
13
|
-
<BrandButton>Get started</BrandButton>
|
|
14
|
-
{/* Direct (non-wrapper) catalog call site — invalid enum value plus a
|
|
15
|
-
className passthrough, exercising resolvePropApi's two checkable
|
|
16
|
-
rules against the real button.mapping.json data. */}
|
|
17
|
-
<Button variant="primry" className="cta-override">Go</Button>
|
|
18
|
-
<FeatureCard content={<p>Feature</p>} />
|
|
19
|
-
</div>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { DropdownMenu } from '@starklab/stk-components';
|
|
3
|
-
|
|
4
|
-
// Compound Foo.Bar usage — <DropdownMenu.SubTrigger/> is a JSXMemberExpression
|
|
5
|
-
// and must attribute back to the DropdownMenu catalog entry, not go unmatched
|
|
6
|
-
// as a component literally named "DropdownMenu.SubTrigger".
|
|
7
|
-
export function Menu() {
|
|
8
|
-
return (
|
|
9
|
-
<DropdownMenu>
|
|
10
|
-
<DropdownMenu.SubTrigger>Open</DropdownMenu.SubTrigger>
|
|
11
|
-
</DropdownMenu>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
/* nested aliases — app-base -> app-mid -> app-top -> app-deep, terminal
|
|
3
|
-
depth 4 (deeper than tokenAliasResolver's WARN_DEPTH of 3), exercising
|
|
4
|
-
both transitive resolution and the deep-alias-chain finding */
|
|
5
|
-
--app-base: var(--stk-surface-brand-1-strong);
|
|
6
|
-
--app-mid: var(--app-base);
|
|
7
|
-
--app-top: var(--app-mid);
|
|
8
|
-
--app-deep: var(--app-top);
|
|
9
|
-
|
|
10
|
-
/* raw fallback — resolves conformant, still flagged info-level since the
|
|
11
|
-
fallback silently becomes load-bearing the day
|
|
12
|
-
--stk-surface-brand-1-strong is renamed */
|
|
13
|
-
--app-cta: var(--stk-surface-brand-1-strong, #1956dd);
|
|
14
|
-
|
|
15
|
-
/* primitive alias — layer violation: a consumer property pointing straight
|
|
16
|
-
at a base/primitive token instead of a semantic one */
|
|
17
|
-
--app-gap: var(--stk-spacing-md);
|
|
18
|
-
|
|
19
|
-
/* scoped-override target — conformant at :root */
|
|
20
|
-
--app-accent: var(--stk-surface-brand-1-strong);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/* scoped override — same property, different selector, resolving to a raw
|
|
24
|
-
hex under .theme-promo: :root and .theme-promo disagree, so the property
|
|
25
|
-
overall is partial-conformance rather than collapsed to one scope */
|
|
26
|
-
.theme-promo {
|
|
27
|
-
--app-accent: #1956dd;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* Tailwind v4 theme entry — Home.jsx's className="dark:bg-primary" resolves
|
|
31
|
-
through this back to a Stark token */
|
|
32
|
-
@theme {
|
|
33
|
-
--color-primary: var(--stk-surface-brand-1-strong);
|
|
34
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Button } from '@starklab/stk-components';
|
|
3
|
-
|
|
4
|
-
// Divergent wrapper — a hardcoded className makes this a styling override,
|
|
5
|
-
// not a faithful passthrough. Used from several call sites (see
|
|
6
|
-
// pages/Home.jsx and pages/Profile.jsx) to exercise high fanout.
|
|
7
|
-
export function BrandButton(props) {
|
|
8
|
-
return <Button className="brand-button" {...props} />;
|
|
9
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Card } from '@starklab/stk-components';
|
|
3
|
-
|
|
4
|
-
// First hop of a two-level wrapper chain (see FeatureCard.jsx) — transparent
|
|
5
|
-
// passthrough. Only consumed internally by FeatureCard, so its own external
|
|
6
|
-
// fanout should be zero (the chain edge itself is excluded from fanout).
|
|
7
|
-
export function CardBase(props) {
|
|
8
|
-
return <Card {...props} />;
|
|
9
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Card } from '@starklab/stk-components';
|
|
3
|
-
|
|
4
|
-
// An "as"-polymorphic call site. ADOPTION_APP_PLAN.md §4 "Polymorphism" calls
|
|
5
|
-
// for a declared `polymorphicProp` field in prop-mapping/schema.json as
|
|
6
|
-
// future work — no dedicated `as`-detection exists in wrapperResolver.js yet,
|
|
7
|
-
// so a hardcoded `as` attribute is classified through the ordinary
|
|
8
|
-
// hardcodedProps -> "constraining" path, same as any other literal prop.
|
|
9
|
-
// This fixture documents that current (pre-declaration) behavior.
|
|
10
|
-
export function SectionCard(props) {
|
|
11
|
-
return <Card as="section" {...props} />;
|
|
12
|
-
}
|